🔄 🔄 ã€Œå¤ąæ•—ã—ãŸã‚‰ã‚‚ã†1å›žã€ã¯åŖŠã‚ŒãŸãƒĒトナイ――垅つãƒģčĢĻめるãƒģæˆģすぎ3į¯€ã§æ›¸ãį›´ã™ — ãƒĒãƒŧダãƒŧ×
🔄

🔄 ã€Œå¤ąæ•—ã—ãŸã‚‰ã‚‚ã†1å›žã€ã¯åŖŠã‚ŒãŸãƒĒトナイ――垅つãƒģčĢĻめるãƒģæˆģすぎ3į¯€ã§æ›¸ãį›´ã™

#automation#claudecode#副æĨ­2026-08-28 ¡ į´„32分

月商120万ぎč‡Ēå‹•åŒ–į’°åĸƒãŒã€ã‚る朝5æœŦ同時ãĢæ­ĸãžãŖãĻいぞした。ログを開いたら全部ãĢåŒã˜čĄŒãŒä¸Ļã‚“ã§ã„ãŸã€‚ã€Œå¤ąæ•—ã—ãŸãŽã§ã‚‚ã†1å›žåŽŸčĄŒã—ãžã—ãŸã€ã€‚ã‚‚ã†1å›žã‚‚å¤ąæ•—ã€‚ã‚‚ã†1å›žã‚‚å¤ąæ•—ã€‚æœ€åˆãŽå¤ąæ•—ã‹ã‚‰63分がįĩŒãŖãĻã€æŽ‹ãŖãĻã„ã‚‹ãŽã¯æœ€åˆãŽå¤ąæ•—ã¨ãžãŖãŸãåŒã˜įĩæžœã§ã—た。

ãĒぜこぎäģ•įĩ„ãŋが劚くぎか

゚クãƒĒプトをはじめãĻ書いたとき、ãƒĒトナイはこういうåŊĸをしãĻã„ã‚‹ã¨æ€ãŖãĻいぞした。

for attempt in range(3):
    try:
        result = do_something()
        break
    except Exception as e:
        time.sleep(2)

ã“ã‚Œã§åˆãŖãĻいぞす。ネットワãƒŧクが一įžŦ切れた、ã‚ĩãƒŧバãƒŧãŒä¸€æ™‚įš„ãĢ重くãĒãŖãŸã€ãã†ã„ã†å¤ąæ•—ãĒらįĸēかãĢã“ã‚Œã§į›´ã‚‹ã€‚å•éĄŒã¯ã€ŒæŦĄãŽčŠĻčĄŒãŒæˆåŠŸã™ã‚‹ã¯ãšã ã€ã¨ã„ã†å‰æã‚’ã€ã™ãšãĻãŽå¤ąæ•—ãĢ同じくåŊ“ãĻはめãĻいるところです。

į¨ŧ働しãĻいるジョブがåĸ—えãĻãã‚‹ã¨ã€ã“ãŽå‰æãŒå´Šã‚Œã‚‹å¤ąæ•—ãŒåŋ…ãšįžã‚Œãžã™ã€‚

ã‚ãŸã—ãŒé‹į”¨ã—ãĻいるSNSč‡Ēå‹•åŒ–į’°åĸƒã§ã€2026åš´8月8æ—Ĩから9æ—ĨãĢかけãĻぎ2æ—Ĩ間で、5æœŦぎãƒŦãƒŧãƒŗã‹ã‚‰åŒæ™‚ãĢãƒĒトナイäŋŽæ­ŖãŒä¸ŠãŒãŖãĻããžã—ãŸã€‚į—‡įŠļはおれもäŧŧãĻいぞしたが、原因はバナバナでした。

note-autolike ぎãƒĒナイトが3æ—Ĩ間ã‚ŧロäģļでした。 最įĩ‚成功は 2026-08-06T09:03:20Zã€‚č¨˜äē‹ã‚ŋイトãƒĢãĢ58字上限ぎバãƒĒデãƒŧã‚ˇãƒ§ãƒŗãŒã‚ã‚‹ãŽãĢã€į”Ÿæˆãƒ—ãƒ­ãƒŗãƒ—ãƒˆãĢは字数åˆļé™ãŒä¸€č¨€ã‚‚æ›¸ã‹ã‚ŒãĻいãĒã‹ãŖãŸãŽã§ã€ãƒĸデãƒĢは毎回63字ぎã‚ŋイトãƒĢをčŋ”しãĻいた。3回投げãĻ3回とも63字。4å›žį›Žã¯čŠĻされることãĒくčĢĻめå‡ĻᐆãĢå…Ĩりぞす。

ai-portraits ぎį”ģåƒį”ŸæˆãŒ63分įŠē回りしãĻいぞした。 ãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗã§ãƒĒクエ゚トがåŧžã‹ã‚ŒãŸã¨ãã€ãƒ—ãƒ­ãƒŗãƒ—ãƒˆã‚’æŽĩéšŽįš„ãĢįŠåŊ“ãĢしãĻ再čŠĻčĄŒã™ã‚‹ãƒ•ã‚ŠãƒŧãƒĢバックを3æŽĩåŽŸčŖ…ã—ãĻいぞした。1æĻ‚åŋĩあたり最大21分、3æĻ‚åŋĩãĒら最大63分。原æ¸Ŧã§ã¯ã€å¯žčąĄã‚ŗãƒŗãƒ†ãƒŗãƒ„č‡ĒäŊ“がãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗãŽåŧ•ãŖã‹ã‹ã‚‹åŽŸå› ãŽã¨ãã€Tierを下げãĻã‚‚åŖã¯å‹•ããžã›ã‚“ã€‚3æŽĩ全部をčŠĻしたところでįĩæžœã¯1æŽĩį›Žã¨åŒã˜ã§ã—ãŸã€‚

browser-slot.sh はグロãƒŧバãƒĢ枠3æœŦãĢ寞しãĻ1時間あたり18〜34æœŦがčĩˇå‹•ã‚’čŠĻãŋぞす。 1 runは14〜62分かかるぎで枠は常ãĢåŸ‹ãžãŖãĻいる。äŋŽæ­Ŗå‰ãŽåŽŸčŖ…ã¯ã€acquire ãĢå¤ąæ•—ã—ãŸįžŦ間ãĢ SKIP: global limit reached をå‡ēしãĻ exit 0 しãĻいぞした。ログãĢæŽ‹ãŖãĻいた数字は、09時台9äģļãƒģ11時台8äģļãƒģ12時台12äģļがčĩˇå‹•すらしãĻいãĒã‹ãŖãŸã¨ã„ã†ã‚‚ãŽã§ã—ãŸã€‚ã‚ã‚‹ã‚ĸã‚Ģã‚Ļãƒŗãƒˆã¯åŊ“æ—Ĩ2回ともskipで、いいねもフりロãƒŧも0でした。

3つとも「もう1å›žã€ãŒåŽŸčŖ…ã•ã‚ŒãĻã„ãžã—ãŸãŒã€ãŠã‚Œã‚‚į›´ãŖãĻいぞせん。

å¤ąæ•—ãŽį¨ŽéĄžãŒé•ã†ã‹ã‚‰ã§ã™ã€‚63字ぎã‚ŋイトãƒĢã¯ã€åŒã˜ãƒ—ãƒ­ãƒŗãƒ—ãƒˆã‚’åŒã˜ãƒĸデãƒĢãĢ投げれば同じ63字がčŋ”りぞす。æąē厚čĢ–įš„ãĒå¤ąæ•—ã§ã™ã€‚ãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗãƒ–ãƒ­ãƒƒã‚¯ã‚‚ã€ã‚ŗãƒŗãƒ†ãƒŗãƒ„ãŒåŽŸå› ãĒã‚‰æĄäģļを変えãĒければäŊ•æŽĩčŠĻしãĻも変わらãĒい。枠ぎ不čļŗã¯ã€čĒ°ã‹ãŒæž ã‚’įŠēけるぞで垅つしかãĒい。

ã“ãŽåˆ†éĄžãŒãĒいãƒĒãƒˆãƒŠã‚¤åŽŸčŖ…ã§ã¯ã€3į¨ŽéĄžãŽå¤ąæ•—ãŒã™ãšãĻ「3回ãƒĢãƒŧプ→įĩ‚äē†ã€ã¨ã„う同一ぎå‡ĻᐆãĢ吸いčžŧぞれぞす。įĩæžœã€į›´ã›ãŸå¤ąæ•—ã‚‚į›´ã›ãĒã„å¤ąæ•—ã‚‚åŒæ•°ã‚Ģã‚Ļãƒŗãƒˆã•ã‚ŒãĻ、最įĩ‚įš„ãĢ同じå‡ēåŖã‹ã‚‰å‡ēãĻきぞす。

é€†å´ãŽåŖŠã‚Œæ–šã‚‚ã‚ã‚Šãžã™ã€‚ã€Œå¤ąæ•—ã—ãŸã€ã¨ãƒ­ã‚°ãĢæŽ‹ãŖãĻいるぎãĢ、原際ãĢã¯ä¸€éŽæ€§ãŽéšœåŽŗã ãŖãŸã‚ąãƒŧ゚です。わたしぎ watch-arb ゚クãƒĒプトは、商品ペãƒŧジぎ取垗ãĢå¤ąæ•—ã™ã‚‹ã¨ extract_failed ã¨ã ã‘č¨˜éŒ˛ã—ãĻいぞした。įŋŒæœį”ģéĸをčĻ‹ã‚‹ã¨ã€Œå–åž—ã§ããĒいåē—が16äģļある」とčĻ‹ãˆã‚‹ã€‚åŽŸéš›ãĢは HTTP 429 がčŋ”ãŖãĻいただけで、同じåē—ã‚’1äģļãšã¤ã‚†ãŖãã‚ŠåŠããƒ—ãƒ­ãƒŧãƒã§ã¯æ­Ŗã—ãå–ã‚ŒãĻいぞした。同期間中ãĢįĸēčĒã—ãŸéĄžäŧŧäē‹čąĄã‚’ä¸Ļずると、同じ429がログ上で7つぎåˆĨãŽåå‰ã§æŽ‹ãŖãĻいぞした。

ãƒŦãƒŧãƒŗãƒ­ã‚°ãĢæŽ‹ãŖãĻã„ãŸåå‰åŽŸéš›ãŽįœŸå› 
watch-arb 商品ペãƒŧジextract_failedHTTP 429
watch-arb bootstrapFATALundici ぎ10į§’ ConnectTimeout
social-autolike ig-3coverage 0/11 → 全ã‚Ŋãƒŧã‚šå–åž—å¤ąæ•— → exit 1IG private API ぎ 429
social-autolike ig-sugfollowing mapå–åž—å¤ąæ•— → run停æ­ĸ同じ 429
outreach-multi enrich全äģļ error → qualify 全äģļ drop → é€šįŽ—0通web_profile_info が常時 429
outreach-multi igJsonunparsable jsonãƒŦã‚šãƒãƒŗã‚šãŒ512,558文字あるぎãĢ400,000文字でslice

extract_failed ã¨č¨˜éŒ˛ã•ã‚ŒãĻいれば「こぎåē—ãŽæ§‹é€ ãŒį‰šæŽŠã€ã¨čĒ­ãŋãžã™ã€‚åŽŸéš›ã¯ã€Œé€ŸãåŠãã™ãŽãŸã€ã¨ã„ã†å•éĄŒã§ã€įŋŒæ—Ĩã‚†ãŖãã‚ŠåŠã‘ã°å–ã‚Œã‚‹ã€‚åå‰ãŒé–“é•ãŖãĻいたぎで、äŋŽæ­ŖãŽæ–šå‘を間違えįļšã‘ãĻいぞした。

ã“ã‚Œã‚’æ•´į†ã™ã‚‹ã¨ã€æ­Ŗã—ã„ãƒĒãƒˆãƒŠã‚¤č¨­č¨ˆã¯3つぎ問いãĢį­”ãˆã‚‹ã“ã¨ãĢ収束しぞす。

(1) åž…ãĻã°į›´ã‚‹ã‹ã€‚ (2) äŊ•回でčĻ‹åˆ‡ã‚‹ã‹ã€‚ (3) æŦĄãŽčŠĻ行ãĢäŊ•を変えãĻæ¸Ąã™ã‹ã€‚

ã€Œå¤ąæ•—ã—ãŸã‚‰ã‚‚ã†1回」はこぎ3つをすずãĻᜁį•Ĩã—ãŸåŽŸčŖ…ã§ã™ã€‚ä¸ŠãŽ5æœŦが同時ãĢæ­ĸãžãŖãĻいた朝は、3ã¤ã¨ã‚‚į­”ãˆãŒæ›¸ã‹ã‚ŒãĻいぞせんでした。

月商120万ぎč‡Ēå‹•åŒ–į’°åĸƒã¯ã€æ¯Žæ—Ĩã‚ŗãƒŧドを書いãĻいるから動いãĻいるぎではありぞせん。č‡Ē分が寝ãĻいる間ãĢį’°åĸƒãŒč‡ĒåˇąäŋŽæ­Ŗã—ãĻ動きįļšã‘るから成įĢ‹ã—ãĻいぞす。そぎ前提ãĢįĢ‹ã¤ã¨ã€ãƒĒãƒˆãƒŠã‚¤č¨­č¨ˆãŽčρäģļãŒå¤‰ã‚ã‚Šãžã™ã€‚ã€Œå¤ąæ•—ã—ãĻもæŦĄã¯į›´ã‚‹ã€ã§ã¯ãĒãã€ã€Œå¤ąæ•—ãŽį¨ŽéĄžã”ã¨ãĢæŦĄãŽčĄŒå‹•ã‚’æąēめる」ことがčρäģļãĢãĒã‚Šãžã™ã€‚ã“ãŽč¨­č¨ˆãŒã‚ã‚‹åŽŸčŖ…ã¨į„Ąã„åŽŸčŖ…ã§ã¯ã€éšœåŽŗãŽåžŠæ—§ãĢč‡Ē分がé–ĸ与するåŋ…čĻãŒã‚ã‚‹ã‹ãŠã†ã‹ãŒæ šæœŦįš„ãĢ変わりぞす。

全äŊ“ぎæĩã‚Œ

3ã¤ãŽå•ã„ã‚’ã˛ã¨ã¤ãŽå›ŗã§æ•´į†ã—ãžã™ã€‚

å¤ąæ•—ãŒį™ēį”Ÿã—ãŸ
      │
      â–ŧ
┌─────────────────────────────────────┐
│ (1) åž…ãĻã°į›´ã‚‹ã‹īŧŸ                   │
│                                     │
│  枠不čļŗ â†’ įŠēくぞで垅つ               │
│  ネットワãƒŧク断 → バックã‚ĒフしãĻ再čŠĻ行 │
│  429 → Retry-After å„Ē先で垅つ        │
│  ãƒ­ã‚°ã‚¤ãƒŗåˆ‡ã‚Œ → åž…ãŖãĻã‚‚į›´ã‚‰ãĒい      │
│  バãƒĒデãƒŧã‚ˇãƒ§ãƒŗé•å → åž…ãŖãĻã‚‚į›´ã‚‰ãĒい │
│  ãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗ → æŽĩãĢよるīŧˆåžŒčŋ°īŧ‰    │
└─────────────────────────────────────┘
      │ åž…ãŖãĻã‚‚į›´ã‚‰ãĒい
      â–ŧ
┌─────────────────────────────────────┐
│ (2) äŊ•回でčĻ‹åˆ‡ã‚‹ã‹īŧŸ                 │
│                                     │
│  回数上限 or 受äģ˜ã‹ã‚‰3時間čļ… â†’ æ‰“ãĄåˆ‡ã‚Š │
│  ぞだčŠĻせる → (3)へ                  │
└─────────────────────────────────────┘
      │ ぞだčŠĻせる
      â–ŧ
┌─────────────────────────────────────┐
│ (3) äŊ•を変えãĻæ¸Ąã™ã‹īŧŸ               │
│                                     │
│  åŒã˜å†…åŽšã‚’æ¸Ąã™ → æąē厚čĢ–įš„å¤ąæ•—ãĒら同じįĩæžœ │
│  エナãƒŧ内厚をæˇģえãĻæ¸Ąã™ → äŋŽæ­Ŗã§ãã‚‹å¤ąæ•—ãĒら回垊 │
│  Tier降æ ŧīŧˆį¸Ļīŧ‰â†’ æĄäģļã‚’įŠåŊ“ãĢしãĻ再čŠĻ行  │
│  åˆĨčĒ˛éĄŒãĢåˇŽã—æ›ŋえīŧˆæ¨Ēīŧ‰â†’ čĒ˛éĄŒã”ã¨å¤‰ãˆã‚‹  │
└─────────────────────────────────────┘

3į¯€ã¯é †į•ĒãĢå‡Ļį†ã™ã‚‹ãŽã§ã¯ãĒãã€å¤ąæ•—ãŽį¨ŽéĄžãĢã‚ˆãŖãĻéŠį”¨ã™ã‚‹įĩ„ãŋ合わせが変わりぞす。ネットワãƒŧク断ãĒら(1)だけで十分です。æąē厚čĢ–įš„ãĒバãƒĒデãƒŧã‚ˇãƒ§ãƒŗé•åã¯(1)を゚キップしãĻ(3)からå…Ĩりぞす。ãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗãƒ–ãƒ­ãƒƒã‚¯ã¯(1)ぎ゚キップãƒģ(2)ぎ閞値ãƒģ(3)ぎ変え斚が全部é–ĸäŋ‚しぞす。

(1) 垅つ — åŽŸčŖ…ãŽéǍæ ŧ

browser-slot.sh ぎäŋŽæ­Ŗã§åŠ ãˆãŸã‚ŗã‚ĸはäģĨ下ぎåŊĸです。

BROWSER_SLOT_WAIT_SEC="${BROWSER_SLOT_WAIT_SEC:-600}"

wait_for_slot() {
  local waited=0
  while ! try_acquire_slot; do
    local jitter=$(( RANDOM % 31 + 15 ))   # 15〜45į§’ãƒŠãƒŗãƒ€ãƒ 
    sleep "$jitter"
    waited=$(( waited + jitter ))
    if [[ $waited -ge $BROWSER_SLOT_WAIT_SEC ]]; then
      log "RESULT waited=${waited}s SKIP: slot timeout"
      return 1
    fi
  done
  return 0
}

再čŠĻčĄŒé–“éš”ã‚’15〜45į§’ãŽãƒŠãƒŗãƒ€ãƒ ãĢしぞす。 å›ē厚間隔ãĢã™ã‚‹ã¨ã€æž ã‚’åž…ãŖãĻいる全ジョブが同じįžŦ間ãĢæŽē到しぞすīŧˆthundering herdīŧ‰ã€‚ãƒŠãƒŗãƒ€ãƒ ã‚¸ãƒƒã‚ŋをå…Ĩれることで、ジョブがäē’いãĢずれãĻč‡Ēį„ļãĢåˆ†æ•Ŗã—ãžã™ã€‚åŽŸæ¸Ŧで、å›ēåŽšé–“éš”ã§ã¯å…¨ã‚¸ãƒ§ãƒ–ãŒį›´åˆ—åŒ–ã—ãĻã‹ãˆãŖãĻčŠ°ãžã‚‹ã“ã¨ã‚’įĸēčĒã—ãĻから変更しぞした。

垅抟中はã‚Ŧãƒŧドを掴ãŋãŖãąãĒしãĢしぞせん。 取垗判厚ぎ一įžŦã ã‘ãƒ­ãƒƒã‚¯ã‚’æŒãŖãĻ、判厚垌はåŋ…ずé›ĸã—ãžã™ã€‚ã“ã“ã‚’é–“é•ãˆã‚‹ã¨åž…æŠŸä¸­ãŽã‚¸ãƒ§ãƒ–ãŒå…¨éƒ¨ãƒ­ãƒƒã‚¯ã‚’æŒãĄåˆãŖãĻ全部æ­ĸぞりぞす。

ログぎ RESULT 行ãĢ waited=600s を掋しぞす。 「600į§’åž…ãŖãŸã†ãˆã§čĢĻã‚ãŸã€ã¨ã€Œåŗæ¨ãĻた」はåˆĨぎäē‹čąĄã§ã€åŒēåˆĨできãĒã„ã¨åžŒã‹ã‚‰čŠ•äžĄã§ããžã›ã‚“ã€‚

BROWSER_SLOT_WAIT_SEC=0 でäŋŽæ­Ŗå‰ãŽåŗskip動äŊœã¨åŽŒå…¨ä¸€č‡´ã—ãžã™ã€‚30æœŦäģĨ上ぎlaunchdジョブがこぎ゚クãƒĒãƒ—ãƒˆã‚’å…ąæœ‰ã—ãĻいるぎで、垌斚äē’æ›ã‚’įļ­æŒã—ãĒがらæŽĩéšŽįš„ãĢ切りæ›ŋえられるようãĢしぞした。

一斚、一過性ãĢčĻ‹ãˆãĻåŽŸã¯åŗå†čŠĻčĄŒã§į›´ã‚‹ã‚ąãƒŧ゚もありぞす。Cloudflare ぎ Authentication error [code: 10000] で3æ—Ĩ逪įļšã‚¸ãƒ§ãƒ–ãŒå¤ąæ•—ã—ãĻã„ãŸæĄˆäģļがあり、「OAuth ãŒå¤ąåŠšã—ãŸ → äēē間ぎ操äŊœãŒčĻã‚‹ã€ãĢčĻ‹ãˆãĻã„ãžã—ãŸã€‚ã¨ã“ã‚ãŒã‚ˇã‚§ãƒĢと launchd æœ€å°į’°åĸƒãŽä¸Ąæ–šã§ãƒ—ロãƒŧブ5æœŦを原čĩ°ã™ã‚‹ã¨å…¨ãĻ exit 0ã€‚å¤ąæ•—æ—ĨぎログをčĒ­ãŋį›´ã™ã¨ã€ã‚ĸクã‚ģ゚トãƒŧã‚¯ãƒŗå†į™ēčĄŒį›´åžŒãŽ0.5į§’åžŒãĢ同一トãƒŧã‚¯ãƒŗã§åˆĨã‚¨ãƒŗãƒ‰ãƒã‚¤ãƒŗãƒˆãŒ 200 をčŋ”しãĻいた。トãƒŧã‚¯ãƒŗå†į™ēčĄŒį›´åžŒãŽä¸€éŽæ€§ 401 で、ãƒĒトナイが1å›žã‚‚į„Ąã„ãŽãŒįœŸå› ã§ã—ãŸã€‚å¤ąæ•—įŽ‡ã¯ 3/9æ—Ĩīŧˆ33%īŧ‰ã€‚「čĒč¨ŧエナãƒŧだからäēēé–“æĄˆäģļã€ã¨åŗæ–­ã™ã‚‹ã¨ã€æŠŸæĸ°ã§į›´ã›ã‚‹ã‚‚ぎをäēēé–“ãŽåž…ãĄčĄŒåˆ—ãĢįŠãŋぞす。

(2) čĢĻめる — įĩ‚įĢ¯ã‚’åŋ…ずįŊŽã

ai-portraits ぎ Tier ナダãƒŧã‚’äž‹ãĢしぞす。ãƒĸデãƒŦãƒŧã‚ˇãƒ§ãƒŗãƒ–ãƒ­ãƒƒã‚¯ãŽã‚¨ãƒŠãƒŧãĢは moderation_stage ã¨ã„ã†ãƒ•ã‚ŖãƒŧãƒĢドがありぞす。output でæ­ĸãžãŖãŸã‚‚ãŽã¯Tier降æ ŧã§é€šã‚‹åŽŸį¸žãŒã‚ã‚‹ã€‚input ã‹ã‚‰å§‹ãžãŖãŸã‚‚ãŽã¯ã€åŒæœŸé–“ä¸­9čŠĻčĄŒå…¨æģ…でした。

for tier in [1, 2, 3]:
    result = generate(concept, tier=tier)

    if result.get("moderation_stage") == "input":
        # inputæŽĩ → Tierを変えãĻã‚‚åŖã¯å‹•ã‹ãĒい、掋りをbreak
        log(f"input-stage block, skip remaining tiers for this concept")
        break

    if result.get("moderation_blocked"):
        # outputæŽĩ → Tier降æ ŧで通る可čƒŊ性がある
        continue

    if result.get("success"):
        return result

æĸ¯å­ã‚’é™ã‚Šã‚‹äžĄå€¤ãŒã‚ã‚‹ã‹ã‚’1æŽĩį›ŽãŽã‚¨ãƒŠãƒŧæœŦ文で判厚しぞす。 moderation_stage == "input" で break するだけで、63分ぎįŠēčĩ°ã‚ŠãŒ0分ãĢãĒりぞす。同様ãĢ、gsheets_retry.py では 401/403/404 ã¯åŗ raise するようãĢč¨­č¨ˆã•ã‚ŒãĻいぞす。

RETRYABLE_HTTP = {429, 500, 502, 503, 504}

def sheets_call_with_retry(fn, max_attempts=3):
    delay = 2
    for attempt in range(max_attempts):
        try:
            return fn()
        except HttpError as e:
            if e.resp.status not in RETRYABLE_HTTP:
                raise  # 401/403/404 ã¯åŗ raise — ãƒĒトナイで隠さãĒい
            if attempt == max_attempts - 1:
                raise
            time.sleep(delay)
            delay *= 3  # 2į§’ → 6į§’īŧˆåˆč¨ˆæœ€å¤§8į§’īŧ‰
        except (socket.timeout, ConnectionError, TimeoutError):
            if attempt == max_attempts - 1:
                raise
            time.sleep(delay)
            delay *= 3

ã€Œåˆč¨ˆæœ€å¤§8į§’ã‚’įĩļ寞ãĢčļ…えãĒい」を先ãĢæąēめぞす。 指数バックã‚Ēフを回数でæąēめると、å‘ŧãŗå‡ēし側ぎ゚ロット時間を静かãĢéŖŸã„ãžã™ã€‚į§’æ•°ã§ä¸Šé™ã‚’å›ē厚しãĻã‹ã‚‰å›žæ•°ã‚’é€†įŽ—ã™ã‚‹é †į•Ēが厉全です。

回数だけで「čĢĻã‚ã‚‹ã€ã‚’åŽŸčŖ…ã™ã‚‹ã¨ã€æ™‚åˆģčģ¸ãŽéšœåŽŗãĢ寞åŋœã§ããžã›ã‚“。kotonoha ぎジョブキãƒĨãƒŧで 39時間 pending ãĢãĒるä狿•…がありぞした。上限6回で中断しãĻも requeue ãŒæˆåŠŸã™ã‚‹ãŽã§å¤ąæ•—ãŒä¸€åēĻもįĢ‹ãŸãšã€į›ŖčĻ–ã‚‚éŗ´ã‚‰ãšã€åˆŠį”¨č€…ãŽį”ģéĸは「äŊœæˆä¸­ã€ãŽãžãžã§ã—た。

äŋŽæ­ŖåžŒãŽåˆ¤åޚåŧã¯ã“うãĒりぞす。

function shouldGiveUpForQuota({ quotaHits, createdAtMs, resumeAtMs }) {
  const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
  return (
    quotaHits >= 2 ||                                  // 2å›žį›ŽãŽä¸Šé™
    resumeAtMs > createdAtMs + THREE_HOURS_MS          // 受äģ˜ã‹ã‚‰3時間čļ…
  );
}

「回数」と「受äģ˜ã‹ã‚‰ãŽįĩļ寞時åˆģ」ぎäēŒé‡ã§æ‰“ãĄæ­ĸめをįŊŽããžã™ã€‚ ä狿•…åŊ“æ—Ĩぎ原値īŧˆå—äģ˜ 2026-08-19T00:36:57Zãƒģ再開 06:37:00Zīŧ‰ã‚’åĸƒį•Œã‚ąãƒŧ゚としãĻテ゚トãĢå…Ĩれると、同じä狿•…はäēŒåēĻと通らãĒいことが抟æĸ°ã§äŋč¨ŧされぞす。

(3) æˆģす — æŦĄãŽčŠĻ行ãĢå¤‰ãˆãŸå†…åŽšã‚’æ¸Ąã™

note-autolike ぎ䞋です。ãƒĒナイトが3æ—Ĩ間ã‚ŧロäģļã ãŖãŸåŽŸå› ã¯ã€åŒä¸€ãƒ—ãƒ­ãƒŗãƒ—ãƒˆã‚’3å›žæŠ•ã’į›´ã™ã ã‘ãŽåŽŸčŖ…ã§ã—ãŸã€‚ã‚¨ãƒŠãƒŧを抟æĸ°å¯čĒ­ãĢしãĻ、2å›žį›ŽäģĨ降ぎčŠĻ行ãĢはäŋŽæ­ŖæŒ‡į¤ēをäģ˜ã‘ãĻæ¸Ąã—ãžã™ã€‚

// エナãƒŧãĢ code / actual / limit をčŧ‰ã›ã‚‹
throw new ValidationError(`ã‚ŋイトãƒĢは1〜58字ãĢしãĻくださいīŧˆåŽŸéš›: ${actual}字īŧ‰`, {
  code: "title-too-long",
  actual,
  limit: 58,
});

// 2å›žį›ŽäģĨ降はäŋŽæ­ŖæŒ‡į¤ēをäģ˜ã‘ãĻ投げる
function rewriteRetryInstruction(error) {
  if (error.code === "title-too-long") {
    return [
      `前回ぎå‡ē力はæŦĄãŽį†į”ąã§ä¸æŽĄį”¨ã§ã—たīŧš`,
      `ã‚ŋイトãƒĢが${error.actual}字あり、上限ぎ${error.limit}字をčļ…えãĻいぞす。`,
      `同じ内厚ãƒģåŒã˜æ§‹æˆãŽãžãžã€ããŽį‚šã ã‘ã‚’æē€ãŸã™åŊĸã¸į›´ã—ãĻã€į”ŸJSONぎãŋを再å‡ē力しãĻください。`,
      `ã‚ŋイトãƒĢはįĩĩ文字čžŧãŋで${error.limit}字äģĨ内īŧˆå‰å›žã¯${error.actual}字īŧ‰ãĢåŋ…ず収めãĻください。`,
    ].join("\n");
  }
}

async function askClaudeWithRetry(prompt, retries = 3) {
  let currentPrompt = prompt;
  for (let i = 0; i < retries; i++) {
    try {
      const result = await callClaude(currentPrompt);
      validateRewrite(result);
      return result;
    } catch (e) {
      if (e instanceof ValidationError) {
        // æąē厚čĢ–įš„ãĒå¤ąæ•— — ゚ãƒĒãƒŧプãĒし、äŋŽæ­ŖæŒ‡į¤ēをäģ˜ã‘ãĻåŗå†čŠĻ行
        currentPrompt = prompt + "\n\n" + rewriteRetryInstruction(e);
      } else {
        // プロã‚ģ゚įŗģãŽå¤ąæ•— — 30į§’åž…ã¤
        await sleep(30_000);
      }
    }
  }
}

æąē厚čĢ–įš„ãĒå¤ąæ•—ã¯ã‚šãƒĒãƒŧãƒ—ã‚’æŒŸãžãšåŗå†čŠĻčĄŒã—ãžã™ã€‚ バãƒĒデãƒŧã‚ˇãƒ§ãƒŗé•åãĢ2į§’åž…ã¤ãŽã¯ã€2į§’ã‚’æ¨ãĻãĻã„ã‚‹ã ã‘ã§ã™ã€‚ã€Œåž…ã¤ã€ã¨ã€Œį›´ã—ãĻæŠ•ã’į›´ã™ã€ã‚’åŒã˜ãƒĒãƒˆãƒŠã‚¤ã§æˇˇãœã‚‹ã¨ã€į›´ã›ã‚‹å¤ąæ•—ãĢã‚‚åž…ãĄæ™‚é–“ã‚’æ‰•ã„ãžã™ã€‚å¤ąæ•—ãŽį¨ŽéĄžã§åˆ†å˛ã—ãĻ、゚ãƒĒãƒŧプをå…Ĩã‚Œã‚‹å ´æ‰€ã‚’é¸ãŗãžã™ã€‚

LLMへčŋ”すäŋŽæ­ŖæŒ‡į¤ēは「抟æĸ°ãŽč­˜åˆĨ子」ではãĒく「äēē間čĒžãŽæŒ‡į¤ēīŧ‹įžį‰ŠãŽæ–‡å­—åˆ—ã€ã§æ¸Ąã—ãžã™ã€‚"title-too-long" ã‚’ããŽãžãžæ¸Ąã™ã¨3é€Ŗæ•—ã—ãžã™ã€‚ã€Œã‚ŋイトãƒĢが63字あり上限ぎ58字をčļ…えãĻいぞす、58字äģĨ内ãĢ収めãĻãã ã•ã„ã€ã¨æ¸Ąã™ã¨å›žåžŠã—ãžã™ã€‚ã“ã‚Œã¯åˆĨぎ゚クãƒĒプトで先ãĢ原č¨ŧ済ãŋぎäē‹åŽŸã§ã™ã€‚

3į¯€ãŽå…¨åŽšã¯ã“ã“ã§ä¸€æ—Ļæ­ĸめぞす。æŦĄã¯å„į¯€ãŽã€ŒåŖŠã‚Œæ–šã€ã¨ã€Œį›´ã—æ–šã€ã‚’ã€5ãƒŦãƒŧãƒŗåŒæ™‚éšœåŽŗã‹ã‚‰æ‹žãŖãŸã‚ąãƒŧ゚åˆĨãĢåˆ†č§Ŗã—ãĻいきぞす。

åŽŸčŖ…ãŽčŠŗį´°

「ãƒĒトナイを書いた」は厌äē†ã§ã¯ãĒい ── フりãƒĢãƒˆæŗ¨å…Ĩã‚’åŒã˜ã‚ŗãƒŸãƒƒãƒˆãĢå…Ĩれる

å‰åŠã§č¨­č¨ˆã—ãŸ3į¯€ã¯ã€æ›¸ã„ãŸæ™‚į‚šã§ã¯ã€Œå‹•ãã¯ãšãŽã‚ŗãƒŧド」でしかありぞせん。間æŦ éšœåŽŗã¯å†įžæ‰‹æŽĩがãĒいぎで、ãƒĒトナイがそぎįĩŒčˇ¯ã‚’原際ãĢ通るかおうかをįĸēã‹ã‚ã‚‹æ–šæŗ•ãŒãƒ‡ãƒ•ã‚ŠãƒĢトでは存在しぞせん。最初ぎ4æœŦãŽåŽŸčŖ…ã¯ãã‚Œã§įĩ‚ã‚ãŖãĻいぞした。git diff をčĻ‹ãĻįĸēčĒã—ãŸæ°—ãĢãĒãŖãĻ、厌äē†ãĢしãĻいぞした。

原際ãĢ劚いたぎは、フりãƒĢãƒˆæŗ¨å…Ĩã‚’åŽŸčŖ…æœŦäŊ“ã¨åŒã˜ã‚ŗãƒŸãƒƒãƒˆãĢ同æĸąã™ã‚‹ã“とでした。

// watch-arb/collector/lib/api.mjs
let _failOnceRemaining = parseInt(process.env.WA_API_FAIL_ONCE ?? '0', 10);

async function req(path, opts = {}) {
  if (_failOnceRemaining > 0) {
    _failOnceRemaining--;
    throw new TypeError('fetch failed');  // åˆæˆãŽéšœåŽŗã€æœŦį•Ēではįĩļ寞ãĢį™ēįĢしãĒい
  }
  // ... æœŦæĨぎfetchå‡Ļᐆ
}

ã‚Ģã‚Ļãƒŗã‚ŋはãƒĸジãƒĨãƒŧãƒĢã‚šã‚ŗãƒŧプで全 req å‘ŧãŗå‡ēã—å…ąé€šã§ã™ã€‚ãƒ—ãƒ­ã‚ģ゚čĩˇå‹•垌ぎ最初ぎN回だけ、fetch をå‘ŧãļ前ãĢ合成ぎ TypeError を投げぞす。

厌ä熿Ąäģļは2æœŦã‚ģットでåŋ…ãšä¸Ąæ–šįĸēčĒã—ãžã™ã€‚

  • ① WA_API_FAIL_ONCE=2 でčĩˇå‹•しãĻ api retry attempt=1/3 と 2/3 がå‡ēたうえで FATAL ãĒく exit 0 で厌čĩ°ã™ã‚‹
  • ② į’°åĸƒå¤‰æ•°æœĒč¨­åŽšãĒら retry ログが1čĄŒã‚‚å‡ēãĒい

â‘ĄãŒã‚ã‚‹ã“ã¨ã§ã€Œãƒ•ã‚ŠãƒĢãƒˆæŗ¨å…ĨがæœŦį•ĒãĢæŧã‚ŒãĻいãĒã„ã€ã“ã¨ã‚’åŒã˜ã‚ŗãƒžãƒŗãƒ‰ã§į¤ēせぞす。原æ¸Ŧでå‡ēたログはこうãĒりぞした。

[INFO] api retry attempt=1/3 path=/api/collector/shops reason=fetch failed
[INFO] api retry attempt=2/3 path=/api/collector/shops reason=fetch failed
[INFO] 32äģļを ingest しãĻ exit 0

こぎ型は watch-arb から zaiko-radarãƒģlily-line-funnelãƒģsocial-autolike とæ¨Ēåą•é–‹ã—ãĻ4æœŦãĢãĒりぞした。「ãƒĒトナイを書いた」は「フりãƒĢãƒˆæŗ¨å…Ĩčžŧãŋでčĩ°ãŖãŸã¨ã“ろをčĻ‹ãŸã€ã§ãĒければ厌äē†ãĢしãĒい、というãƒĢãƒŧãƒĢãĢ変わりぞした。

再čŠĻčĄŒã™ã‚‹å¯žčąĄã‚’1é–ĸ数ãĢ閉じる

ãƒĒトナイを書くとき、最初ãĢæąēめることは「äŊ•回čŠĻすか」ではãĒく「äŊ•を再čŠĻčĄŒã™ã‚‹ã‹ã€ã§ã™ã€‚

// 判厚を1é–ĸ数ãĢé›†į´„ã™ã‚‹
function isTransientStatus(status) {
  return status === 429 || (status >= 500 && status <= 504);
}

async function req(path, opts = {}) {
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    try {
      const res = await fetchWithTimeout(path, opts);
      if (!res.ok) {
        if (!isTransientStatus(res.status)) throw new HttpError(res.status);
        if (attempt === MAX_ATTEMPTS) throw new HttpError(res.status);
        await sleep(backoff(attempt));
        continue;
      }
      return await res.json();
    } catch (e) {
      if (!isNetworkError(e)) throw e;  // ネットワãƒŧク䞋外äģĨå¤–ã¯åŗthrow
      if (attempt === MAX_ATTEMPTS) throw e;
      await sleep(backoff(attempt));
    }
  }
}

401/403/404 ã‚’åŗ throw ã™ã‚‹ãŽã¯įœåŠ›åŒ–ã§ã¯ãĒく、åˆĨぎä狿•…ã‚’é˜˛ããŸã‚ãŽč¨­č¨ˆã§ã™ã€‚æ¨Šé™ã‚¨ãƒŠãƒŧをãƒĒトナイで包むと、同じãƒĒクエ゚トを3回投げãĻ3å›žã¨ã‚‚åŒã˜å¤ąæ•—ãŒčŋ”りぞす。ログãĢは「3å›žå¤ąæ•—ã—ãŸã€ã¨ã ã‘æŽ‹ã‚Šã€ã€Œæ¨Šé™ãŒį„Ąã„ã€ã¨ã„ã†įœŸå› ã¯åŸ‹ã‚‚ã‚Œãžã™ã€‚

transient-failure-recorded-as-permanent.md ã§č¨˜éŒ˛ã—ãŸã€Œ7ã¤ãŽåå‰ã§æŽ‹ãŖãŸ429」と逆向きぎä狿•…です。429が extract_failed ãĢ化けるぎと同じ構造で、401/403 が「たぞãĢ遅い」ãĢ化けぞす。

isTransientStatus を1é–ĸ数ãĢå‡ēã™į†į”ąã¯ã€åˆ¤æ–­ãŒč¤‡æ•°ãŽå‘ŧãŗå‡ēã—įŽ‡æ‰€ãĢæ•Ŗã‚‹ã¨å ´æ‰€ã”ã¨ãĢ垎åĻ™ãĢé•ã†åŽšįžŠãŒį”Ÿãžã‚Œã‚‹ã‹ã‚‰ã§ã™ã€‚gsheets_retry.py ぎ RETRYABLE_HTTP = {429, 500, 502, 503, 504} という厚数ã‚ģットも同じį™ēæƒŗã§ã€ã€ŒãŠãŽã‚šãƒ†ãƒŧã‚ŋ゚がãƒĒãƒˆãƒŠã‚¤å¯žčąĄã‹ã€ã¨ã„ã†įŸĨč­˜ã‚’1įŽ‡æ‰€ãĢ閉じãĻいぞす。

ãƒĒトナイをå…Ĩれると䞋外ぎäŧã‚ã‚Šæ–šãŒå¤‰ã‚ã‚‹

kotonoha ãĢ withRetry ã‚’åŽŸčŖ…ã—ãŸįŋŒã‚ŗãƒŸãƒƒãƒˆã§ã€å‰¯äŊœį”¨ãŽäŋŽæ­ŖãŒ3äģļå…Ĩりぞした。「抟čƒŊをčŋŊåŠ ã—ãŸã€ã¨æ€ãŖãĻいぞしたが、原際ãĢは「䞋外ぎäŧã‚ã‚Šæ–šã‚’変えた」変更でした。

変更前、purge() は 5xx がčŋ”ãŖãĻも垌įļšãŽã€Œæ‰‹å…ƒãŽäŊœæĨ­ãƒ‡ã‚ŖãƒŦクトãƒĒ30æ—Ĩ掃除」ぞでå‡Ļį†ãŒé€˛ã‚“ã§ã„ãžã—ãŸã€‚res.ok が false でもæŦĄãŽčĄŒã¸é€˛ã‚€åŽŸčŖ…ãĢãĒãŖãĻいたからです。withRetry をå…ĨれãĻ5xx ぎįžŦ間ãĢ throw するようãĢãĒã‚‹ã¨ã€æŽƒé™¤ãŽã‚ŗãƒŧドがそこで到達不čƒŊãĢãĒã‚Šãžã™ã€‚åŽŸåŽŗã¯ã€ŒäŊœæĨ­ãƒ‡ã‚ŖãƒŦクトãƒĒが30æ—Ĩåˆ†č“„įŠã™ã‚‹ã€ã¨ã„ã†ã€åŽšæœŸįš„ãĢ手動掃除しãĒければ気ãĨかãĒã„į¨ŽéĄžãŽé™ã‹ãĒåŠŖåŒ–ã§ã—ãŸã€‚

async function purge(jobId) {
  try {
    await uploadResult(jobId);
  } catch (e) {
    log(`purge upload failed: ${e.message}`);
    // ここで throw しãĒい — įŊŽãå ´ãŽå¤ąæ•—ã¯č¨˜éŒ˛ã—ãĻįļščĄŒã™ã‚‹
  }
  await cleanWorkDir();  // äž‹å¤–ãŽæœ‰į„ĄãĢかかわらずåŋ…ずčĩ°ã‚‰ã›ã‚‹
}

もう1äģļは setContentSafely で、1å›žį›Žã¨2å›žį›ŽãŽå†čŠĻčĄŒãŽé–“ãŒ0į§’ã§ã—ãŸã€‚ãƒĒã‚Ŋãƒŧ゚é€ŧčŋĢでčŊãĄãŸį›´åžŒãĢåŒæĄäģļã§åŗå†čŠĻčĄŒã—ãĻã‚‚ã€åœ§åŠ›ãŒæŽ‹ãŖãĻいる間は同じįĩæžœãĢãĒりぞす。前半で「æąē厚čĢ–įš„ãĒå¤ąæ•—ã¯ã‚šãƒĒãƒŧãƒ—ã‚’æŒŸãžãšåŗå†čŠĻčĄŒã€ã¨æ›¸ããžã—ãŸãŒã€ããŽé€†å´ã§ã™ã€‚čŗ‡æēé€ŧčŋĢãŒåŽŸå› ãŽå¤ąæ•—ã¯ã€åž…ãŸãĒいとãƒĒトナイãĢæ„å‘ŗãŒã‚ã‚Šãžã›ã‚“ã€‚

čŠĻčĄŒå›žæ•°ã¯ã€Œå¤ąæ•—ã—ãŸã¨ãäŊ•がčĩˇãã‚‹ã‹ã€ã§éžå¯žį§°ãĢč¨­åŽšã—ãĻいぞす。report()īŧˆįĩæžœå ąå‘Šīŧ‰ã¯å¤ąæ•—ã™ã‚‹ã¨åˆŠį”¨č€…ãŽį”ģéĸãŒã€ŒåŽŸčĄŒä¸­ã€ãŽãžãžæ°¸äš…ãĢæ­ĸぞるぎで attempts=5、claim()īŧˆäēˆį´„取垗īŧ‰ã¯ attempts=3ã€‚ä¸€åž‹ãŽå›žæ•°ã¯ã‚ŗã‚šãƒˆã¨čĸĢåŽŗãŽéžå¯žį§°ã‚’į„ĄčĻ–ã—ãžã™ã€‚ã‚ŋイムã‚ĸã‚Ļトも操äŊœãŽé‡ã•で分けãĻいãĻ、putObject と getStream は120į§’ã€getText と listKeys は30į§’ã§ã™ã€‚ã‚ŋイムã‚ĸã‚Ļãƒˆč¨­åŽšãŽãĒい fetch は「永䚅ãĢ吊られる」という最æ‚ĒぎãƒĒトナイ不čƒŊįŠļ態をäŊœã‚Šãžã™ã€‚

゚トãƒĒãƒŧãƒ ã‚’æ¸Ąã™ã¨ãƒĒトナイできãĒい

åŽŸčŖ…ã™ã‚‹ãžã§æ„č­˜ã—ãĻいãĒã‹ãŖãŸåˆļį´„ãŒã‚ã‚Šãžã™ã€‚ãƒĒトナイ可čƒŊãĢするãĢは、å…ĨåŠ›ãŒã€Œå†åŽŸčĄŒå¯čƒŊ」でãĒければãĒりぞせん。

// NG: ゚トãƒĒãƒŧムは一åēĻæļˆč˛ģã—ãŸã‚‰å†į”Ÿã§ããĒい
async function retryWithStream(stream) {
  for (let i = 0; i < 3; i++) {
    await fetch(url, { body: stream });  // 2å›žį›Žã¯ã‚šãƒˆãƒĒãƒŧムがįŠē
  }
}

// OK: Buffer か string ãĢ限厚する
async function retryWithBuffer(buffer) {
  for (let i = 0; i < 3; i++) {
    await fetch(url, { body: buffer });  // äŊ•åēĻでも同じデãƒŧã‚ŋを送れる
  }
}

putObject ãĢæ¸Ąã™ body を Buffer か string ãĢ限厚しぞした。型åˆļį´„ã‚’ã‚ŗãƒĄãƒŗãƒˆã§æŽ‹ã—ãĻおかãĒいと、垌からå‘ŧãŗå‡ēし側が゚トãƒĒãƒŧãƒ ã‚’æ¸Ąã—ãĻ「ãƒĒトナイが動いãĻいるぎãĢæ¯Žå›žå¤ąæ•—ã™ã‚‹ã€ãƒã‚°ãĢãĒりぞす。゚トãƒĒãƒŧムをãƒĒトナイãĢæ¸Ąã™ã¨2å›žį›ŽäģĨ降はįŠēãŽãƒœãƒ‡ã‚Ŗã‚’é€ã‚Šįļšã‘ぞす。エナãƒŧもå‡ēずãĢæ¯Žå›žå¤ąæ•—ã—ãžã™ã€‚


į§ãŒčŠ°ãžãŖãŸčŠą

é˜˛åžĄãŽãŸã‚ãĢįŊŽã„ãŸåŽšæ•°ãŒã€æˆåŠŸįŽ‡0%ãŽåŖãĢãĒãŖãĻいた

outreach-multi ぎ DM 受äŋĄįŽąãƒ‘ãƒŧ゚å‡Ļį†ã§ã€unparsable json というエナãƒŧが毎æ—Ĩå‡ēãĻいぞした。ã‚ģãƒŦクã‚ŋãŒåŖŠã‚ŒãŸã‹ã€JSON ãŽæ§‹é€ ãŒå¤‰ã‚ãŖãŸã‹ã¨į–‘ãŖãĻ3æ—Ĩ間äŋŽæ­Ŗã—įļšã‘ぞした。

原態は、受äŋĄįŽąå–åž—ãŽã‚ŗãƒŧドãĢ text.slice(0, 400_000) ã¨ã„ã†čĄŒãŒã‚ã‚Šãžã—ãŸã€‚åˇ¨å¤§ãĒãƒŦã‚šãƒãƒŗã‚šãŒæĨたときãĢãƒĄãƒĸãƒĒをäŋč­ˇã™ã‚‹ãŸã‚ãĢ書かれた上限でした。IG ぎ DM 受äŋĄįŽąãŽåŽŸã‚ĩイã‚ēã‚’č¨ˆæ¸Ŧしたら 512,558文字 でした。上限が 400,000 ãĒぎで、毎回åŋ…ず途中で切れぞす。切れた JSON は JSON.parse で毎回åŋ…ãšå¤ąæ•—ã—ãžã™ã€‚æˆåŠŸįŽ‡ã¯0%です。

3æ—Ĩé–“åŒã˜å¤ąæ•—ã‚’čĻ‹įļšã‘ãĻいたぎは、text.slice ãŽčĄŒã‚’åŋ˜ã‚ŒãĻ「パãƒŧ゚ãĢå¤ąæ•—ã—ãĻいる → æ§‹é€ ãŒåŖŠã‚ŒãŸã€ã¨čĒ­ã‚“ã§ã„ãŸã‹ã‚‰ã§ã™ã€‚į—‡įŠļがå‡ē始めたぎも「ä슿—Ĩから」ではありぞせんでした。受äŋĄįŽąãŽã‚ĩイã‚ēが 400,000字をčļ…えたæ—Ĩから静かãĢå¤ąæ•—ã—å§‹ã‚ãĻ、あるæ—Ĩ気ãĨã„ãŸã‚‰é€šįŽ—0äģļãĢãĒãŖãĻいたåŊĸです。

äŋŽæ­Ŗã¯ page.evaluate ぎ中で JSON.parse ぞで厌äē†ã•ã›ã€å¤ąæ•—ã—ãŸã¨ãã ã‘å…ˆé ­300字をč¨ēæ–­į”¨ãĢčŋ”すåŊĸãĢ変えぞした。

こぎäģļã‹ã‚‰åž—ãŸč¨­č¨ˆãƒĢãƒŧãƒĢは「上限をįŊŽããĒら、上限ãĢåŊ“ãŸãŖãŸã“ã¨č‡ĒäŊ“をログãĢå‡ēす」です。text.slice はã‚ĩイãƒŦãƒŗãƒˆãĢ切りčŊとすだけで、切れたことをčǰãĢもįŸĨã‚‰ã›ãžã›ã‚“ã€‚é˜˛åžĄãŽåŽšæ•°ãŒããŽåŽšæ•°ã‚’čļ…えたįžŦé–“ã‹ã‚‰æ’äš…įš„ãĒå¤ąæ•—ãĢå¤‰ã‚ãŖãĻいãĻも、ログãĢはäŊ•も掋りぞせん。

quarantine が永䚅ãƒĢãƒŧプãĢãĒãŖãĻいた

unfollow-core.js ぎã‚ĸãƒŗãƒ•ã‚Šãƒ­ãƒŧå‡ĻᐆãĢ、3å›žå¤ąæ•—ã™ã‚‹ã¨7æ—Ĩ間ぎäŋį•™īŧˆquarantineīŧ‰ãĢå…Ĩるäģ•įĩ„ãŋがありぞした。

å•éĄŒã¯ã€Œquarantine から垊æ´ģしたらおうãĒるか」です。垊æ´ģしたã‚ĸã‚Ģã‚Ļãƒŗãƒˆã¯å€™čŖœãƒĒ゚トãĢæˆģりぞす。再åēĻ3å›žå¤ąæ•—ã™ã‚‹ã¨ã€ãžãŸ7æ—Ĩ間ぎäŋį•™ãĢå…Ĩã‚Šãžã™ã€‚ã“ã‚ŒãŒį„Ąé™ãĢ回りぞす。

å€™čŖœãŒ 2,605äģļã‚ãŖãĻ、scanned: 8 / unfollowed: 2 という数字をčĻ‹ãĻ2é€ąé–“æ‚Šãŋãžã—ãŸã€‚å€™čŖœãŒãģãŧå‡Ļį†ã•ã‚ŒãĻいãĒい。「ãĒãœé€˛ãžãĒいぎか」をčĒŋずたら「同じã‚ĸã‚Ģã‚ĻãƒŗãƒˆãŒ quarantine ãĢå…ĨãŖãĻはå‡ēã‚‹ã‚’įš°ã‚Ščŋ”しãĻいãĻ、前ãĢé€˛ã‚“ã§ã„ãĒい」でした。

ログãĢは circuit-break とå‡ēãĻいぞした。circuit-break が抟čƒŊしãĻいるぎãĢåŽŸčĄŒæ•°ãŒåĸ—えãĒã„ãŽã¯įŸ›į›žã—ãžã™ã€‚ã‚‚ãŖã¨æ—Šãæ°—ãĨけたはずでしたが、「circuit-break が動いãĻいる = äŋč­ˇã•れãĻいる」とčĒ­ã‚“ã§ã„ãŸãŽã§į–‘ã„ãžã›ã‚“ã§ã—ãŸã€‚

äŋŽæ­Ŗã¯ã€į´¯č¨ˆå¤ąæ•—回数īŧˆUNFOLLOW_GIVEUP_FAILS = 6、quarantine å¤ąæ•—é–žå€¤ãŽ2倍īŧ‰ãĢ達したã‚ĸã‚Ģã‚Ļãƒŗãƒˆã‚’å°å¸ŗã‹ã‚‰į‰Šį†å‰Šé™¤ã™ã‚‹ã“ã¨ã§ã—ãŸã€‚

「äŋį•™ã€ã¯æēœãžã‚‹ã ã‘で減りぞせん。 捨ãĻるįĩ‚įĢ¯ã‚’åŋ…ずįŊŽã‹ãĒã‘ã‚Œã°ã€å°å¸ŗã¯å‡Ļį†ã§ããĒいã‚ĸã‚Ģã‚Ļãƒŗãƒˆã§åŸ‹ãžã‚Šįļšã‘ぞす。ただし捨ãĻる前ãĢ「č‡Ē分ぎå‡Ļį†ãŒæ­Ŗå¸¸ãĢ動いãĻいるか」ぎįĸēčĒãŒå…ˆã§ã™ã€‚ã‚ģãƒƒã‚ˇãƒ§ãƒŗãŒåˆ‡ã‚ŒãĻã„ãŸã ã‘ã§ã€æ­Ŗå¸¸ãĒã‚ĸã‚Ģã‚Ļãƒŗãƒˆã‚’å…¨éƒ¨æ¨ãĻãĻしぞう逆ä狿•…があり垗ぞす。kotonoha ぎペãƒŧジãĢ書いた「捨ãĻる前ãĢč‡Ēåˆ†ãŽæ­Ŗå¸¸æ€§ã‚’įĸēčĒã™ã‚‹ã€ã¨ã„ã†åŽŸå‰‡ãŒã“ã“ãĢã‚‚éŠį”¨ã•ã‚Œãžã™ã€‚

ᛪčĻ–ãŒ39æ™‚é–“éŗ´ã‚‰ãĒã‹ãŖãŸ

kotonoha ぎジョブが39æ™‚é–“ãšãŖã¨ã€ŒäŊœæˆä¸­ã€ãŽãžãžæ­ĸãžãŖãĻã„ãžã—ãŸã€‚į›ŖčĻ–ã¯ä¸€åēĻã‚‚éŗ´ã‚Šãžã›ã‚“ã§ã—ãŸã€‚ã‚¸ãƒ§ãƒ–ã¯å¤ąæ•—ã¨ã—ãĻč¨˜éŒ˛ã•ã‚ŒãĻいぞせんでした。

原因は「回数をįŠļ態ãĢæŒãŖãĻいãĒいãƒĒトナイãĢã¯ã€æ‰“ãĄæ­ĸめがįŊŽã‘ãĒい」でした。

ジョブがクりãƒŧã‚ŋ上限ãĢåŊ“たると requeue で pending ãĢæˆģしぞす。æˆģしたジョブは「äŊ•å›žį›Žã‹ã€ã¨ã„ã†ã‚Ģã‚Ļãƒŗã‚ŋã‚’æŒãŖãĻいぞせんでした。7å›žį›ŽãŽã‚¸ãƒ§ãƒ–ã‚‚1å›žį›ŽãŽã‚¸ãƒ§ãƒ–ã‚‚åŽŸčŖ…ã‹ã‚‰åŒēåˆĨできぞせん。requeue ãŒæˆåŠŸã™ã‚‹ãŽã§å¤ąæ•—ã‚‚įĢ‹ãĄãžã›ã‚“ã€‚į›ŖčĻ–ãŽé–žå€¤ãĢもåŊ“ãŸã‚Šãžã›ã‚“ã€‚åˆŠį”¨č€…ãŽį”ģéĸãĢは「äŊœæˆä¸­ã€ãŒåģļã€…ã¨čĄ¨į¤ēされįļšã‘ぞす。

äŋŽæ­Ŗã¯2æŽĩ階でした。ぞず「回数をįŠļ態ãĢ持たせる」。æŦĄãĢ「回数と受äģ˜ã‹ã‚‰ãŽįĩļ寞時åˆģぎäēŒé‡ã§æ‰“ãĄæ­ĸめをįŊŽãã€ã€‚前半でį¤ēした shouldGiveUpForQuota é–ĸ数をåĸƒį•Œã‚ąãƒŧ゚äģ˜ããŽãƒ†ã‚šãƒˆãĢ通すとき、ä狿•…åŊ“æ—Ĩぎ原値īŧˆå—äģ˜ 2026-08-19T00:36:57Zãƒģ再開äēˆåޚ 06:37:00Zīŧ‰ã‚’1ã‚ąãƒŧ゚å…Ĩれぞした。同じä狿•…はäēŒåēĻと通らãĒいことが抟æĸ°ã§äŋč¨ŧされぞす。

å›žæ•°ã ã‘ã§æ‰“ãĄæ­ĸめをįŊŽã„ãĻいると「1å›žį›Žã ãŒå†é–‹ãŒ12時間先」を通しãĻしぞいぞす。時åˆģã ã‘ã ã¨ã€Œå†é–‹ã¯įŸ­ã„ãŒä¸Šé™ãŒé€Ŗįļšã—ãĻいる」を通しãĻしぞいぞす。äģŠå›žãŽä狿•…は時åˆģæĄäģļぎ側で1å›žį›ŽãĢ捕ぞるåŊĸでした。äēŒé‡ãĢã—ãŸæ„å‘ŗãŒåŽŸãƒ‡ãƒŧã‚ŋでæ¸Ŧれたäģļです。

「ワãƒŧã‚Ģãƒŧが垅つ」と「そぎジョブをčĢĻめる」はåˆĨぎ判断です。クりãƒŧã‚ŋは垅ãĻã°į›´ã‚‹å¤ąæ•—ãĒぎでワãƒŧã‚Ģãƒŧã¯åž…ãĄãžã™ã€‚ã§ã‚‚åˆŠį”¨č€…ã‚’3時間äģĨ上垅たせる栚拠ãĢはãĒりぞせん。

「čĒč¨ŧエナãƒŧã€ã¨åŗæ–­ã—ãĻ3æ—Ĩ間äēēé–“åž…ãĄãĢしãĻいた

com.lily.line-pdca が Cloudflare ぎ Authentication error [code: 10000] で3æ—Ĩ逪į­ĸぞりぞした。čĒč¨ŧエナãƒŧãĒぎで「OAuth ãŒå¤ąåŠšã—ãŸã€æ‰‹å‹•ã§ wrangler login がčĻã‚‹ã€ã¨åˆ¤æ–­ã—ãžã—ãŸã€‚į’°åĸƒå¤‰æ•°ãŽč¨­åޚæŧã‚Œã‚‚į–‘ã„ãžã—ãŸã€‚

įĸēかめる前ãĢ「äēē間ぎGUI操äŊœãŒčĻã‚‹æĄˆäģļ」としãĻåˆ†éĄžã—ãŸãŽãŒé–“é•ã„ã§ã—ãŸã€‚

原際ãĢ shell と launchd æœ€å°į’°åĸƒãŽä¸Ąæ–šã§ wrangler whoamiãƒģkv key listãƒģdeploy --dry-run を原čĩ°ã™ã‚‹ã¨å…¨ãĻ exit 0 でした。å‡ē力バイト数ぞで同一īŧˆ1,516 bytesīŧ‰ã€‚å¤ąæ•—æ—ĨぎログをčĒ­ãŋį›´ã™ã¨ã€ã‚ĸクã‚ģ゚トãƒŧã‚¯ãƒŗã‚’å†į™ēčĄŒã—ãŸ 0.5į§’åžŒ ãĢ KV keys が 401 をčŋ”しãĻいぞした。そぎ同じトãƒŧã‚¯ãƒŗã§0.5į§’åžŒãŽ /user は 200 をčŋ”しãĻいぞした。トãƒŧã‚¯ãƒŗå†į™ēčĄŒį›´åžŒãŽä¸€éŽæ€§ 401 で、ãƒĒトナイが1回もãĒã„ãŽãŒįœŸå› ã§ã—ãŸã€‚

å¤ąæ•—įŽ‡ã¯ 3/9æ—Ĩīŧˆį´„33%īŧ‰ã§ã—た。毎æ—Ĩå‡ēãĒい、でも3æ—ĨãĢ1回はå‡ēる。こぎé ģåēĻが「äēēé–“æĄˆäģļãĢčĻ‹ãˆã‚‹ã€åŽŸå› ã§ã‚‚ã‚ã‚Šãžã—ãŸã€‚

判断を変えたåŸēæē–は「同じ手順をäģŠã‚‚う一åēĻ及いたら通るか」を原際ãĢ及いãĻįĸēかめることでした。エナãƒŧãŽį¨ŽéĄžīŧˆ401ãƒģAuthentication errorīŧ‰ã ã‘で「įĩļ寞ãĢį›´ã‚‰ãĒã„ã€ã¨åˆ†éĄžã—ãĒい。transient-failure-recorded-as-permanent.md ãĢč¨˜éŒ˛ã—ãŸã€Œ429が7ã¤ãŽåå‰ã§æŽ‹ãŖãĻいた」と同じ型で、äģŠåēĻは「čĒč¨ŧエナãƒŧã€ã¨ã„ã†åå‰ãŒä¸€éŽæ€§ãŽå¤ąæ•—ã‚’äēēé–“æĄˆäģļãĢčĻ‹ã›ãĻいぞした。

こぎäģļäģĨ降、čĒč¨ŧエナãƒŧがå‡ēたらプロãƒŧブを1æœŦčĩ°ã‚‰ã›ãĻから判断するãƒĢãƒŧãƒĢãĢ変わりぞした。「抟æĸ°ã§į›´ã›ã‚‹ã‚‚ぎをäēē間ぎäŊœæĨ­åž…ãĄã‚­ãƒĨãƒŧãĢįŠãžãĒい」ぎ前提は「抟æĸ°ã§åŠã„ãĻįĸēかめた」です。įĸēかめãĒã„ãžãžåŗæ–­ã™ã‚‹ãŽã¯ã€åŠã‹ãšãĢã€Œį›´ã‚‰ãĒい」とæąēめつけãĻいるぎと変わりぞせん。

ã¤ãžãšããƒã‚¤ãƒŗãƒˆ

前æŽĩで取り上げた個åˆĨäē‹äž‹ãŽčŖãĢã‚ã‚‹æ§‹é€ įš„ãĒパã‚ŋãƒŧãƒŗã‚’ãžã¨ã‚ãžã™ã€‚ã€ŒčŠ°ãžãŖãŸå ´æ‰€ãŽåå‰ã€ãŒé•ã†ã ã‘ã§ã€æ šãŖã“ã¯åŒã˜åž‹ã§ã™ã€‚

å¤ąæ•—ã‚’ã€Œã˛ã¨ã¤ãŽåå‰ã€ãĢåœ§į¸Žã—ãĻč¨˜éŒ˛ã™ã‚‹

watch-arbぎ商品ペãƒŧジはextract_failed、bootstrapåŽŸčĄŒã¯FATAL、DM受äŋĄįŽąã¯unparsable jsonã¨č¨˜éŒ˛ã•ã‚ŒãĻいぞした。3ã¤ã¨ã‚‚åŽŸéš›ãŽįœŸå› ã¯HTTP 429です。transient-failure-recorded-as-permanent.mdãĢč¨˜éŒ˛ã—ãŸã“ãŽäē‹čąĄã‚’æ•´į†ã™ã‚‹ã¨ã€åŒæœŸé–“ä¸­ã§åŒã˜429が7į¨ŽéĄžãŽåˆĨåã§æŽ‹ãŖãĻã„ãžã—ãŸã€‚åå‰ãŽæ™‚į‚šã§HTTP゚テãƒŧã‚ŋ゚がæļˆãˆã‚‹ã¨ã€ã€Œåž…ãĻã°į›´ã‚‹ã€ãĒãŽã‹ã€Œæ§‹é€ ã‚’į›´ã™åŋ…čĻãŒã‚ã‚‹ã€ãŽã‹ãŽåˆ¤æ–­ãŒã§ããĒくãĒりぞす。äŋŽæ­Ŗã¯1čĄŒã§ã™ã€‚č¨˜éŒ˛ã™ã‚‹å‰ãĢ429/503 → rate_limited / 401/403 → auth_error / それäģĨ外 → extract_failedã¨åˆ†éĄžã—ãžã™ã€‚ã“ãŽ3åˆ†å˛ãŒã€Œåž…ã¤ãƒģčĢĻめるãƒģæˆģã™ã€ãŽåˆ†å˛ãŽæ‰‹å‰ãĢã‚ã‚‹å‰ææĄäģļです。

エナãƒŧæœŦ文を捨ãĻãĻいる

ai-portraitsぎgenerate.pyがcodex execぎstdout/stderrを捨ãĻãĻいたため、į”ģ像がå‡ēãĒã‹ãŖãŸį†į”ąãŒmissingとしか掋りぞせんでした。moderation_blockedもsafety_violations: ["sexual"]もčĻ‹ãˆãĒければ、Tier降æ ŧで回垊するぎかinputæŽĩã§åŗæ‰“ãĄåˆ‡ã‚‹ãšãã‹ãŽåˆ¤æ–­ãŒįĢ‹ãĄãžã›ã‚“ã€‚ã‚¨ãƒŠãƒŧをäŋæŒã—ãĒã„åŽŸčŖ…ã§ã¯ã€Œåž…ã¤/čĢĻめる/æˆģす」ぎ3į¯€ãŒãã‚‚ãã‚‚æˆįĢ‹ã—ãĒいぎで、エナãƒŧäŋæŒã¯č¨­č¨ˆãŽå‰æã§ã‚ãŖãĻ抟čƒŊčŋŊ加ではありぞせん。

moderation_stageをįĸēčĒã›ãšãĢ全TierをčŠĻす

moderation_stage: inputでæ­ĸãžãŖãŸæĻ‚åŋĩは、T1からT3ぞで全æŽĩčŠĻしãĻ9čŠĻčĄŒå…¨æģ…でした。moderation_stage: outputでæ­ĸãžãŖãŸã‚‚ãŽã¯Tier降æ ŧã§é€šã‚‹åŽŸį¸žãŒã‚ã‚Šãžã™īŧˆpartial 01_feet_up_windowはT2、03_legs_on_bed_topdownはT3īŧ‰ã€‚T1ぎmoderation_stageãƒ•ã‚ŖãƒŧãƒĢド1čĒžã§ã€Œæĸ¯å­ã‚’é™ã‚Šã‚‹äžĄå€¤ãŒã‚ã‚‹ã‹ã€ãŒæąēぞるぎãĢã€ãƒ•ã‚ŖãƒŧãƒĢドをčĒ­ãžãšãĢ全æŽĩをčŠĻすと1æĻ‚åŋĩあたり最大21分、3æĻ‚åŋĩで最大63åˆ†ãŒį„Ąé§„ãĢãĒりぞす。「厉全側フりãƒŧãƒĢバックがäŊ•æŽĩあるか」より「1æŽĩį›ŽãŽã‚¨ãƒŠãƒŧæœŦæ–‡ã§é™ã‚Šã‚‹äžĄå€¤ãŒã‚ã‚‹ã‹ã€ã‚’å…ˆãĢ判厚しぞす。

降り先ぎ厚量をæ¸ŦらずãĢé…įˇšã™ã‚‹

social-autolikeぎfollow-source-followersでフりãƒŧãƒĢバックをčŋŊ加する前ãĢ、UIãƒĸãƒŧダãƒĢぎfollowing側が408äģļ/63į§’ã§äŧ¸ãŗã‚‹ãŽãĢ寞しãĻfollowers側が12äģļでæ­ĸぞることを先ãĢ原æ¸Ŧしぞした。厚量がある側だけをフりãƒŧãƒĢバックãĢæŽĄį”¨ã—ãŸãŽãŒį†į”ąã§ã™ã€‚æ¸ŦらずãĢé…įˇšã™ã‚‹ã¨ã€åŽšé‡ãŽãĒい降り先ãĢå…¨ãƒˆãƒŠãƒ•ã‚Ŗãƒƒã‚¯ãŒæĩã‚ŒãĻ「降り先をčŋŊ加したぎãĢæˆåŠŸįŽ‡ãŒå¤‰ã‚ã‚‰ãĒい」というįŠļæŗãĢãĒりぞす。outreach-multiぎpkč§Ŗæąēも、IGが70äģļ前垌でįĩžã‚‹ã¨ã„うæ—ĸįŸĨãŽåŖã‚’įš”ã‚Ščžŧんで1run 30äģļåˆļ限ãĢしãĻいぞす。

æ’äš…įš„ãĒå¤ąæ•—ã‚’ã€Œã‚ã¨1回」ぎ枠ãĢ数える

outreach-multiぎDM送äŋĄã§no ãƒĄãƒƒã‚ģãƒŧジ buttonīŧˆDMä¸å¯ãŽč¨­åŽšīŧ‰ãŒčŋ”ると、exhausted after 3 attemptsãĢäš—ãŖãĻ送äŋĄæž ã‚’1äģļあたり3回分æļˆč˛ģしãĻã„ãžã—ãŸã€‚į›¸æ‰‹ãŽã‚ĸã‚Ģã‚Ļãƒŗãƒˆč¨­åŽšã¯ã“ãĄã‚‰ãŒãƒĒトナイしãĻも変わりぞせん。sent.jsonlぎreasonãƒ•ã‚ŖãƒŧãƒĢドをčĒ­ã‚“ã§ã€no ãƒĄãƒƒã‚ģãƒŧジ button / profile gone / こぎペãƒŧジは存在しぞせんをåĢむ厛先はpermanentlyFailed SetãĢå…Ĩれ、shouldSkip()でexhaustedぎ手前ãĢ判厚をįŊŽããžã™ã€‚これは前æŽĩでč§Ļれた「一過性を恒䚅としãĻč¨˜éŒ˛ã™ã‚‹é€†å‘ããŽä狿•…ã€ã¨åŒã˜æ šã§ã€å¤ąæ•—ãŽåˆ†éĄžãŽč¨­č¨ˆãŒãƒĒãƒˆãƒŠã‚¤å›žæ•°ãŽč¨­č¨ˆã‚ˆã‚Šå…ˆãĢčĻã‚Šãžã™ã€‚

429ぎあとも同じ速åēĻで及きįļšã‘ã‚‹

social-autolikeぎig-3は0.3〜0.5į§’é–“éš”ã§17ã‚Ŋãƒŧã‚šã‚’æ•°į§’ã§æ’ƒãĄčžŧんで14時間ã‚ŧロ成果でした。429を受けた垌も間隔を変えãĒければ、åˆļ限中ãĢ同じãƒĒã‚¯ã‚¨ã‚šãƒˆã‚’įŠãŋ上げãĻč‡Ē分で゚ロットãƒĒãƒŗã‚°ã‚’åģļé•ˇã—ãžã™ã€‚retry-and-giveup-design.mdãĢč¨˜éŒ˛ã—ãŸäŋŽæ­Ŗã§ã¯ã€3逪įļš429でそぎプロã‚ģ゚ぎAPIå‘ŧãŗå‡ēã—ã‚’æ‰“ãĄåˆ‡ã‚‹ãƒŠãƒƒãƒã¨ã€429を1åēĻでもčĻ‹ãŸåē—ぎconcurrencyを1ãĢčŊã¨ã™č¨­č¨ˆã‚’å…ĨれãĻã„ãžã™ã€‚åž…ãŖãĻから再čŠĻčĄŒã™ã‚‹ã ã‘ã§ãĒく、速åēĻそぎもぎをčŊとすことがã‚ģットです。

ãƒĒトナイが2åą¤ãĢ重ãĒãŖãĻいる

watch-arbぎingestShopがį‹Ŧč‡ĒãĢ3回ãƒĢãƒŧãƒ—ã‚’æŒãĄã€req()側ãĢも3回ãƒĢãƒŧプがありぞした。最æ‚Ē3×3=9回ãĢãĒり、ログãĢ「retry attempt 3/3」が2åēĻä¸Ļんで、上限がおこãĢあるぎか、äģŠäŊ•å›žį›ŽãĒぎかがčĒ­ã‚ãžã›ã‚“ã€‚ãƒ•ã‚ŠãƒĢãƒˆæŗ¨å…Ĩをå…ĨれãĻã‚‚ã€ŒãŠãĄã‚‰ãŽåą¤ãŒå‹•ã„ãĻいるか」が分からãĒくãĒりぞす。äŋŽæ­Ŗã¯ingestShop側ぎį‹Ŧč‡ĒãƒĢãƒŧプを削除しãĻreq()ぎ1åą¤ãĢ一æœŦ化しぞした。å‘ŧãŗå‡ēã—å´ã¯åŗthrowするだけãĢしぞす。

フりãƒŧãƒĢãƒãƒƒã‚¯ãŽæˆåŠŸã‚’æ‰“ãĄåˆ‡ã‚Šã‚Ģã‚Ļãƒŗã‚ŋãĢ数える

「3逪įļš429でAPIå‘ŧãŗå‡ēã—ã‚’æ‰“ãĄåˆ‡ã‚‹ã€ãƒŠãƒƒãƒãŒã€UIフりãƒŧãƒĢバックが成功したäģļもã‚Ģã‚ĻãƒŗãƒˆãĢåĢめãĻいぞした。逃げ道をäŊœãŖãĻも3äģļでæ­ĸãžãŖãĻã„ãžã—ãŸã€‚æ‰“ãĄåˆ‡ã‚Šã‚Ģã‚Ļãƒŗã‚ŋは「全įĩŒčˇ¯ãŒå¤ąæ•—したとき」だけåĸ—やしぞす。フりãƒŧãƒĢバック先で成功したäģļã¯ã€ãƒĄã‚¤ãƒŗãŽå¤ąæ•—ã‚Ģã‚ĻãƒŗãƒˆãĢčŧ‰ã›ãžã›ã‚“。

「取れãĒã‹ãŖãŸīŧˆį•°å¸¸īŧ‰ã€ã¨ã€Œå–りãĢčĄŒãæ™‚é–“ãŒãĒã‹ãŖãŸīŧˆæ­Ŗå¸¸ãĒæ‰“ãĄåˆ‡ã‚Šīŧ‰ã€ã‚’同じã‚ŧロäģļで判厚する

follow-source-followersはã‚Ŋãƒŧ゚取垗だけで時間äēˆįŽ—ã‚’äŊŋã„åˆ‡ãŖãĻå€™čŖœã‚ŧロäģļãĢãĒãŖãŸå ´åˆã€æ—ĸ存ぎ「全ã‚Ŋãƒŧã‚šå–åž—å¤ąæ•—â†’čĒ¤æ“äŊœé˜˛æ­ĸで停æ­ĸ」įĩŒčˇ¯ãĢå…ĨãŖãĻしぞいぞす。取れãĒã‹ãŖãŸãŽã‹æ™‚é–“ãŒãĒã‹ãŖãŸã ã‘ãĒぎかはåˆĨぎäē‹čąĄã§ã™ã€‚同じã‚ŧロäģļで判厚すると毎æ—ĨčĒ¤å ąãŒéŗ´ã‚Šã€æœŦį‰ŠãŽå¤ąæ•—ã‚’čĻ‹åˆ†ã‘ã‚‰ã‚ŒãĒくãĒりぞす。time-budgetというåˆĨå‡ēåŖã‚’į”¨æ„ã—ãĻåŒēåˆĨしぞす。

čĢĻめる判厚åŧã‚’catchブロックãĢベã‚ŋ書きする

æ‰“ãĄæ­ĸã‚ãŽæĄäģļがcatchぎ中ãĢ書かれãĻいると、åĸƒį•Œã‚ąãƒŧ゚ぎテ゚トãĢčŧ‰ã›ã‚‰ã‚Œãžã›ã‚“ã€‚į´”į˛‹é–ĸ数ãĢå‡ēすとä狿•…åŊ“æ—Ĩぎ原値を1ã‚ąãƒŧ゚å…Ĩã‚Œã‚‰ã‚Œã€åŒã˜æĄäģļがäēŒåēĻと通らãĒいことを抟æĸ°ã§äŋč¨ŧできぞす。ロジックがcatchぎ中ãĢã‚ã‚‹é™ã‚Šã€ãƒ†ã‚šãƒˆã¯ã€ŒãŸãžãŸãžã‚ąãƒŧ゚が通らãĒã‹ãŖãŸã€įŠļ態で合æ ŧしぞす。

エナãƒŧãƒĄãƒƒã‚ģãƒŧジãĢ抟æĸ°å¯čĒ­ãĒæƒ…å ąãŒčŧ‰ãŖãĻいãĒい

ValidationErrorを文字列だけで投げると、再čŠĻčĄŒæ™‚ãĢ「äŊ•がæ‚Ēã‹ãŖãŸã‹ã€ã‚’ã‚ŗãƒŧドから取りå‡ēせぞせん。2å›žį›ŽäģĨ降ãĢåŒã˜ãƒ—ãƒ­ãƒŗãƒ—ãƒˆã‚’æŠ•ã’ã‚‹ã¨åŒã˜įĩæžœãĢãĒるぎは「æˆģã™ã€ãŒåŽŸčŖ…ã•ã‚ŒãĻいãĒいからで、code: "title-too-long", actual: 63, limit: 58ぎようãĒ構造をčŧ‰ã›ã‚‹ã¨äŋŽæ­ŖæŒ‡į¤ēã‚’å‹•įš„ãĢįĩ„ãŋįĢ‹ãĻられぞす。LLMへčŋ”ã™ã¨ãã¯č­˜åˆĨ子īŧˆ"title-too-long"īŧ‰ã§ãĒくäēē間čĒžãŽæŒ‡į¤ē文と原æ¸Ŧå€¤ã‚’æ¸Ąã—ãžã™ã€‚č­˜åˆĨå­ã‚’ããŽãžãžæ¸Ąã™ã¨3é€Ŗæ•—ã—ãžã™ã€‚


ãƒ™ã‚šãƒˆãƒ—ãƒŠã‚¯ãƒ†ã‚Ŗã‚š

5ãƒŦãƒŧãƒŗåŒæ™‚éšœåŽŗã‹ã‚‰č’¸į•™ã—ãŸãƒĢãƒŧãƒĢを列挙しぞす。数が多いですが、全部「原際ãĢčŠ°ãžãŖãŸã‹ã‚‰å…Ĩれた」もぎです。

① å¤ąæ•—ã¯č¨˜éŒ˛ã™ã‚‹å‰ãĢHTTP゚テãƒŧã‚ŋã‚šã¨äž‹å¤–ãŽåž‹ã§åˆ†éĄžã™ã‚‹

429/503 → rate_limited / 401/403 → auth_error / それäģĨ外 → å‡Ļᐆįŗģエナãƒŧ。こぎ3åˆ†å˛ã‚’å…ˆãĢįŊŽãã ã‘で、į”ģéĸãĢå‡ēã‚‹ã€Œå¤ąæ•—ã—ãĻいるåē—ぎ一čĻ§ã€ãŽæ„å‘ŗãŒå¤‰ã‚ã‚Šãžã™ã€‚watch-arbã¯ã“ãŽåˆ†éĄžã‚’åŠ ãˆãŸåžŒã€shops.last_errorãŽæ–‡č¨€ã‚‚ã€ŒãƒŦãƒŧトåˆļ限で取垗できず」と「構造が取れãĒい」ãĢ分けぞした。

② ãƒĒトナイを書いたら、フりãƒĢãƒˆæŗ¨å…Ĩã‚’åŒã˜ã‚ŗãƒŸãƒƒãƒˆãĢå…Ĩれる

let _failOnceRemaining = parseInt(process.env.WA_API_FAIL_ONCE ?? '0', 10);
// čĩˇå‹•垌ぎ最初ぎN回だけ合成ぎエナãƒŧを投げる

厌ä熿Ąäģļは2æœŦã‚ģットです。=2でretry attempt=1/3と2/3がå‡ēãĻexit 0で厌čĩ°ã™ã‚‹ã“と、æœĒč¨­åŽšãĒら retry ログが1čĄŒã‚‚å‡ēãĒã„ã“ã¨ã€‚â‘ĄãŒãĒいと「フりãƒĢãƒˆæŗ¨å…ĨがæœŦį•ĒãĢæŧã‚ŒãĻいãĒã„ã€ã“ã¨ã‚’åŒã˜ã‚ŗãƒžãƒŗãƒ‰ã§įĸēčĒã§ããžã›ã‚“ã€‚

â‘ĸ æ‰“ãĄæ­ĸめを「回数」と「受äģ˜ã‹ã‚‰ãŽįĩļ寞時åˆģ」ぎäēŒé‡ã§įŊŽã

回数だけだと「1å›žį›Žã ãŒå†é–‹ãŒ12時間先」を通しぞす。įĩļ寞時åˆģã ã‘ã ã¨ã€Œå†é–‹ã¯įŸ­ã„ãŒä¸Šé™ãŒé€Ŗįļšã—ãĻいる」を通しぞす。kotonohaぎä狿•…は時åˆģæĄäģļぎ側で1å›žį›ŽãĢ捕ぞるåŊĸでしたīŧˆå—äģ˜2026-08-19T00:36:57Zãƒģ再開06:37:00Zが受äģ˜+3時間ぎ外īŧ‰ã€‚äēŒé‡ãĢã—ãŸæ„å‘ŗãŒåŽŸãƒ‡ãƒŧã‚ŋでæ¸Ŧれたäģļです。

â‘Ŗ åž…ãĄãŽåˆč¨ˆä¸Šé™ã‚’į§’ã§å…ˆãĢæąēã‚ã€å›žæ•°ã‚’é€†įŽ—ã™ã‚‹

gsheets_retry.pyã¯ã€Œåˆč¨ˆæœ€å¤§8į§’ã‚’įĩļ寞ãĢčļ…えãĒい」を䞝é ŧ文ãĢå›ē厚しãĻから、2į§’â†’6į§’īŧˆ3回īŧ‰ã‚’é€†įŽ—ã—ãžã—ãŸã€‚æŒ‡æ•°ãƒãƒƒã‚¯ã‚Ēフを回数で先ãĢæąēめると、å‘ŧãŗå‡ēし側ぎ゚ロット時間が静かãĢäžĩéŖŸã•ã‚Œãžã™ã€‚

⑤ 401/403/404ã¯åŗthrowしãĻãƒĒトナイで隠さãĒい

if e.resp.status not in RETRYABLE_HTTP:
    raise  # 樊限エナãƒŧã¯åŗ raise

樊限エナãƒŧをãƒĒトナイで包むと「3å›žå¤ąæ•—ã—ãŸã€ã¨ã ã‘æŽ‹ã‚Šã€ã€Œæ¨Šé™ãŒãĒã„ã€ã¨ã„ã†įœŸå› ãŒåŸ‹ã‚‚ã‚Œãžã™ã€‚transient-failure-recorded-as-permanent.mdã§č¨˜éŒ˛ã—ãŸã€Œ429がextract_failedãĢ化ける」と同じ構造で、äģŠåēĻは401が「たぞãĢ遅い」ãĢ化けぞす。

â‘Ĩ moderation_stageでæĸ¯å­ã‚’é™ã‚Šã‚‹äžĄå€¤ãŒã‚ã‚‹ã‹åˆ¤åŽšã—ãĻからTierをčŠĻす

if result.get("moderation_stage") == "input":
    log("inputæŽĩぎ拒åĻ。掋りTierをbreak")
    break

1čĄŒåŠ ãˆã‚‹ã ã‘ã§63分ぎįŠēčĩ°ã‚ŠãŒ0分ãĢãĒりぞす。outputでæ­ĸãžãŖãŸã‚‚ãŽã¯ãƒŠãƒ€ãƒŧã‚’å›žã™äžĄå€¤ãŒã‚ã‚Šãžã™ã€‚inputでæ­ĸãžãŖãŸã‚‚ãŽã¯æĻ‚åŋĩごと捨ãĻãĻæŦĄãĢ進ãŋぞす。

â‘Ļ ãƒĒãƒˆãƒŠã‚¤ãŽåą¤ã‚’1įŽ‡æ‰€ãĢé›†į´„ã™ã‚‹

å‘ŧãŗå‡ēし側がį‹Ŧč‡ĒãƒĢãƒŧãƒ—ã‚’æŒãĄãƒŠã‚¤ãƒ–ãƒŠãƒĒ側ãĢも再čŠĻčĄŒãŒã‚ã‚‹ã¨æœ€æ‚ĒN×M回ãĢãĒã‚Šãžã™ã€‚é‡č¤‡ã‚’čĻ‹ã¤ã‘ãŸã‚‰å¤–å´ãŽãƒĢãƒŧプを削除しãĻreq()ぎ1åą¤ãĢ一æœŦ化しぞす。

⑧ 降り先ぎ厚量をæ¸ŦãŖãĻã‹ã‚‰é…įˇšã™ã‚‹

フりãƒŧãƒĢバックをčŋŊ加する前ãĢ、原æ¸Ŧで「こぎ降り先がäŊ•äģļぞでčŋ”ã™ã‹ã€ã€ŒãŠã“ã§čŠ°ãžã‚‹ã‹ã€ã‚’įĸēčĒã—ãžã™ã€‚åŽšé‡ãŽãĒい降り先ãĢå…¨ãƒˆãƒŠãƒ•ã‚Ŗãƒƒã‚¯ã‚’æĩã—ãĻã‚‚æˆåŠŸįŽ‡ã¯å¤‰ã‚ã‚Šãžã›ã‚“ã€‚

⑨ æ’äš…įš„ãĒå¤ąæ•—ã¯æ°¸įļšãƒĒ゚トãĢ1å›žã§č¨˜éŒ˛ã—ãĻ除外する

no ãƒĄãƒƒã‚ģãƒŧジ button / profile goneぎようãĒį›¸æ‰‹å´ãŽč¨­åŽšãĢčĩˇå› ã™ã‚‹å¤ąæ•—は、ãƒĒトナイ回数をæļˆč˛ģする前ãĢ除外しぞす。shouldSkip()ぎ判厚をexhaustedぎ手前ãĢįŊŽããžã™ã€‚

⑩ 「äģŠã‚‚う一åēĻ及いたら通るか」を原際ãĢ及いãĻからäēēé–“æĄˆäģļãĢåˆ†éĄžã™ã‚‹

Cloudflare Authentication errorを「OAuthãŒå¤ąåŠšã—ãŸã€ã¨åŗæ–­ã—ãĻ3æ—Ĩ間äēēé–“ãŽåž…ãĄčĄŒåˆ—ãĢįŠã‚“ã§ã„ãžã—ãŸã€‚shell と launchd æœ€å°į’°åĸƒãŽä¸Ąæ–šã§ãƒ—ロãƒŧブを原čĩ°ã™ã‚‹ã¨å…¨ãĻexit 0でした。エナãƒŧぎ名前だけで「įĩļ寞ãĢį›´ã‚‰ãĒã„ã€ã¨åˆ†éĄžã—ãĒい。įĸēかめãĻから判断しぞす。

â‘Ē 上限厚数がåŊ“ãŸãŖãŸã“ã¨č‡ĒäŊ“をログãĢå‡ēす

text.slice(0, 400_000)はã‚ĩイãƒŦãƒŗãƒˆãĢ切りčŊã¨ã™ã ã‘ã§ã—ãŸã€‚é˜˛åžĄãŽåŽšæ•°ãŒæ’äš…įš„ãĒ故障ぎåŧ•き金ãĢãĒãŖãĻいãĻも、ログãĢはäŊ•も掋りぞせん。JSON.parseぞで内部で厌äē†ã•ã›ã€å¤ąæ•—ã—ãŸã¨ãã ã‘č¨ēæ–­į”¨ãĢ先頭300字をčŋ”すåŊĸãĢすると、「切れた」というäē‹åŽŸãŒč¨˜éŒ˛ãĢ掋りぞす。

â‘Ģ ãƒĒトナイ導å…Ĩ垌は䞋外ぎäŧæ’­į¯„å›˛ãŽå¤‰åŒ–ã‚’įĸēčĒã™ã‚‹

withRetryをå…Ĩã‚Œã‚‹ã¨ã€ŒæĄã‚Šã¤ãļし→throw」ãĢå¤‰ã‚ãŖãŸįŽ‡æ‰€ãŽä¸‹æĩãĢあるåŋ…é ˆå‡Ļį†ãŒåˆ°é”ä¸čƒŊãĢãĒることがありぞす。ãƒĒトナイは抟čƒŊぎčŋŊ加ではãĒく、䞋外ぎäŧã‚ã‚Šæ–šã‚’変える変更です。下æĩãĢã€Œå¤ąæ•—ã—ãĻもåŋ…ãšã‚„ã‚‹ãšãã“ã¨ã€ãŒã‚ã‚‹įŽ‡æ‰€ã¯å€‹åˆĨãĢ回ãŋį›´ã—ãžã™ã€‚

â‘Ŧ čŗ‡æēé€ŧčŋĢãŒåŽŸå› ãŽå¤ąæ•—ã¯ã€åž…ãŖãĻから再čŠĻčĄŒã™ã‚‹

setContentSafelyぎ再čŠĻčĄŒé–“éš”ãŒã‚ŧãƒ­į§’ã§ã¯ã€éĢ˜č˛ čˇã§čŊãĄãŸį›´åžŒãĢåŒæĄäģļでもう一åēĻčŊãĄãžã™ã€‚æąē厚čĢ–įš„ãĒå¤ąæ•—ã¯ã‚šãƒĒãƒŧプãĒã—åŗå†čŠĻčĄŒãŒæ­Ŗč§Ŗã§ã™ãŒã€ããŽé€†å´ã¨ã—ãĻã€čŗ‡æēã‚’垅たãĒいとãƒĒトナイãĢæ„å‘ŗãŒã‚ã‚Šãžã›ã‚“ã€‚å¤ąæ•—ãŽį¨ŽéĄžã§ã‚šãƒĒãƒŧプをå…Ĩã‚Œã‚‹å ´æ‰€ã‚’é¸ãŗãžã™ã€‚

⑭ čĢĻめる判厚åŧã‚’ᴔឋé–ĸ数ãĢå‡ēしãĻテ゚トãĢä狿•…åŊ“æ—Ĩぎ原値をå…Ĩれる

shouldGiveUpForQuota({quotaHits, createdAtMs, resumeAtMs})へ切りå‡ēしãĻ、受äģ˜2026-08-19T00:36:57Zãƒģ再開06:37:00Zを1ã‚ąãƒŧ゚としãĻå…Ĩれると、同じä狿•…はäēŒåēĻと通りぞせん。catchブロックãĢベã‚ŋ書きされた判厚はåĸƒį•Œã‚ąãƒŧ゚をテ゚トできぞせん。

⑮ åž…ãŖãŸäē‹åޟãƒģčĢĻめたäē‹åޟãƒģæ‰“ãĄåˆ‡ãŖãŸäē‹åŽŸã‚’ãƒ­ã‚°ãŽ1行ãĢ掋す

waited=600s SKIP: slot timeoutとSKIP: global limit reachedīŧˆåŗæ¨ãĻīŧ‰ã¯åˆĨぎäē‹čąĄã§ã™ã€‚ãŠãĄã‚‰ã‚‚åŒã˜ã€ŒSKIP」ãĢčĻ‹ãˆã‚‹ã¨ã€ã€Œæž ãŽįĢļ合がåĸ—えた」ãĒãŽã‹ã€Œã‚‚ã¨ã‚‚ã¨åŗæ¨ãĻしãĻいた」ãĒぎかを垌からåŒēåˆĨã§ããžã›ã‚“ã€‚æŠ•čŗ‡ã—ãŸåž…ãĄæ™‚é–“ã¯æ•°å€¤ã§ãƒ­ã‚°ãĢå‡ēしぞす。


ぞとめ

ã€Œå¤ąæ•—ã—ãŸã‚‰ã‚‚ã†1å›žã€ã¨æ›¸ã„ãŸã¨ãã€ããŽã‚ŗãƒŧドãĢは3ã¤ãŽå•ã„ãŽį­”ãˆãŒã‚ã‚Šãžã›ã‚“ã€‚åž…ãĻã°į›´ã‚‹ã‹ã€äŊ•回でčĻ‹åˆ‡ã‚‹ã‹ã€æŦĄãŽčŠĻ行ãĢäŊ•を変えãĻæ¸Ąã™ã‹ã€‚ã“ãŽ3ã¤ãŒæƒãŖãĻいãĒã„åŽŸčŖ…ã¯ã€æąē厚čĢ–įš„ãĒå¤ąæ•—ãŽå‰ã§čŗ‡æēã‚’į„ŧきįļšã‘ãĻæ­ĸぞりぞす。

5ãƒŦãƒŧãƒŗãŒåŒæ™‚ãĢæ­ĸãžãŖãŸæœãĢä¸ĻずãĻ気ãĨã„ãŸãŽã¯ã€éšœåŽŗãŽåžŠæ—§ãĢč‡Ē分がé–ĸ与するåŋ…čĻãŒã‚ã‚‹ã‹ãŠã†ã‹ãŒã€ã“ãŽč¨­č¨ˆãŽæœ‰į„Ąã§ãģãŧæąēãžã‚‹ã¨ã„ã†į‚šã§ã™ã€‚č¨­č¨ˆãŒã‚ã‚Œã°ã€ã‚šã‚¯ãƒĒãƒ—ãƒˆã¯å¤ąæ•—ãŽį¨ŽéĄžã”ã¨ãĢæŦĄãŽčĄŒå‹•ã‚’č‡Ē分でæąēã‚ãžã™ã€‚č¨­č¨ˆãŒãĒã‘ã‚Œã°ã€åŒã˜å¤ąæ•—ãŒ3å›žč¨˜éŒ˛ã•ã‚Œã‚‹ã ã‘ã§ã€įŋŒæœã‚‚同じ場所ãĢčŠ°ãžã‚Šãžã™ã€‚

月商120万ぎč‡Ēå‹•åŒ–į’°åĸƒãŒæ¯Žæ—Ĩ動いãĻã„ã‚‹ãŽã¯ã€ã‚ŗãƒŧドを毎æ—Ĩ書いãĻã„ã‚‹ã‹ã‚‰ã§ã¯ã‚ã‚Šãžã›ã‚“ã€‚å¤ąæ•—ãŒæ­Ŗã—ãåˆ†éĄžã•ã‚ŒãĻ、抟æĸ°ãŒč‡Ē分で回垊するから成įĢ‹ã—ãĻいぞす。そぎ前提ãĢįĢ‹ã¤ã¨ã€ãƒĒãƒˆãƒŠã‚¤č¨­č¨ˆã¯ã€ŒåžŒã‹ã‚‰åŠ ãˆã‚‹æŠŸčƒŊ」ではãĒãã€æœ€åˆãŽåŽŸčŖ…ãĢåĢぞれãĻいるずきもぎです。

「ãƒĒトナイを書いた」は、「フりãƒĢãƒˆæŗ¨å…Ĩčžŧãŋでčĩ°ãŖãŸã¨ã“ろをčĻ‹ãŸã€ã§åŽŒäē†ãĢしぞす。


äģ•įĩ„ãŋぎ全äŊ“像ãƒģ月120ä¸‡ãŽå†…č¨ŗãƒģ30æ—Ĩ手順は有料noteãĢぞとめãĻいぞす。

📕 Claude Codeč‡Ēåž‹į’°åĸƒã§ã€åŽŸéš›ãŠã†į¨ŧぐか ― äģ•įĩ„ãŋãƒģ原䞋ãƒģ始め斚ãƒģã‚ĩポãƒŧト


Lilyīŧˆ@bokuwalilyīŧ‰â€• 個äēē開į™ēč€…ã€‚Claude Code でč‡Ē動化åŸēį›¤ã‚’įĩ„ãŋãĒがら、iOSã‚ĸプãƒĒやWebã‚ĩãƒŧãƒ“ã‚šã‚’é‡į”Ŗã—ãĻいぞす

  • AIで「寝ãĻãĻも回るäģ•įĩ„ãŋ」をäŊœãŖãĻ月120万ãĢã—ãŸčŠąã¯ noteãŽæœ‰æ–™č¨˜äē‹ ãĢ💰
  • OSS: github.com/bokuwalily 🐙
  • æœ€æ–°æƒ…å ąãƒģお問い合わせは X @bokuwalily ã¸đŸŒ
  • AI導å…Ĩãƒģč‡Ēå‹•åŒ–ãŽį›¸čĢ‡ã¨åŽŸčŖ…ãƒ†ãƒŗãƒ—ãƒŦ7æœŦぎ配布は å…ŦåŧLINE からđŸ’Ŧ

įš†ã•ã‚“ãŽ â¤ī¸ ã‚„ã‚ˇã‚§ã‚ĸãŒåŠąãŋãĢãĒりぞすīŧ