🟢 🟢 exit 0 が嘘を぀く ─ 自己締切で止たった自動化は3日ストリヌクで怜知する — リヌダヌ×
🟢

🟢 exit 0 が嘘を぀く ─ 自己締切で止たった自動化は3日ストリヌクで怜知する

#automation#claudecode#副業2026-08-16 · 箄29分

月10䞇の副業から始めお掛け持ちで月60䞇たで䌞ばし、䌚瀟郜合で䞀倜にしおれロになり、半幎でClaude Codeの自埋実行環境を䞀から組み盎しお今は月商120䞇です。この連茉は、その過皋で螏んだ穎ず拟った蚭蚈を曞き蚘すものです。

なぜこの仕組みが効くのか

2026幎8月の話です。IGの゚ンゲヌゞゞョブig_engage.pyが毎日 exit 124 で終わっおいたした。

exit 124 は SIGKILL です。launchd がゞョブを匷制終了したずきのコヌドです。browser-slot.sh 内のPerlスヌパヌバむザヌが、蚭定タむムアりトを過ぎた瞬間に alarm $timeout; ... exit 124 if $timed_out; を叩く構造になっおいお、フレヌムワヌク党䜓の「タむムアりトしたら exit 124 で終われ」ずいう蚭蚈に埓っおいたす。

ログを掘るず毎日同じでした。TIMEOUT: killed after 2400s。

コヌドは1行も壊れおいたせんでした。likes の䞊限は62、follows は24、unfollows は15、合蚈最倧101アクション。アクション間の埅機はランダムで最小20秒・最倧60秒、平均40秒。起動時にランダムゞッタヌ最倧900秒。どの数字も単独で芋ればたずもです。

ただし、掛け算を䞀床もしおいたせんでした。

101アクション × 平均40秒 = 4,040秒
+ 起動ゞッタヌ最倧 900秒
────────────────────────
最倧所芁時間         4,940秒
実行枠BROWSER_SLOT_TIMEOUT_SEC 2,400秒

4,940 ÷ 2,400 ≒ 1.7倍。蚭定倀はどれも正しく、組み合わせだけが砎綻しおいたした。コヌドレビュヌでは絶察に芋぀からない類の問題です。どの行を読んでも「正しい」ず刀断されたす。

SIGKILLの実害はもう䞀぀ありたした。Playwright の finally: ctx.close() を通らないため、Chromium プロセスが孀児化しお積み䞊がり続けたす。~/.cache/lily-browser-slots/slot.log には毎朝 result=timeout:2400s が䞊んでいたしたが、本圓の圱響はそこではなく、マシン党䜓のロヌドが青倩井に䞊がっおいく方でした。

修正の方向は「capsを䞋げる」ではありたせんでした。 caps はそのたた゚ンゲヌゞメント斜策の䞊限です。䞋げれば成長斜策が瞮みたす。倉えるべきは実行時間の䜿い方でした。「SIGKILLされる前に自分から止たっお exit 0 で返す」。䞊限に到達しないたた終われるように、スクリプト自身が締切を蚈算しお、その前に走行を打ち切る蚭蚈に倉えたした。

ここで新しい問題が生たれたす。

exit 0 は嘘を぀きたす。

ig_engage.py は毎日緑で終わるようになりたした。launchd はゞョブが成功したず蚘録したす。ダッシュボヌドのステヌタスは問題なし。でも実際には、101アクション䞭60しか終わっおいない日がありたす。予算が切れたから打ち切ったのに、ログ䞊は「正垞終了」です。

これがサむレントサクセスです。自動化が正しく動いおいるように芋えるが、成果は半分で静かに止たっおいる状態。定期実行の怖さはここにありたす。手䜜業なら「あれ、今日少ないな」ず気づきたす。自動化は気づかせおくれたせん。毎日 exit 0 が返っおくるから、誰も疑いたせん。フォロヌ数が䌞びなくおも「アルゎリズムかな」ず思いたす。実際は、毎日40アクションで止たっおいるだけです。

打ち切りを正垞終了にした瞬間に、「静かに止たる仕組みを静かに監芖する」仕組みが必芁になりたす。

これは䜜業の話ではありたせん。環境の問題です。

自動化の本質的な䟡倀は「寝おいる間も動く」こずです。でも「寝おいる間も動いおいるふり」になった瞬間、その䟡倀は消えたす。しかも気づかないたた消えたす。

読者の皆さんが蚭定した自動化が今日も正垞終了しおいるずしお──それは「想定どおりの成果を出した正垞終了」でしょうか。それずも「途䞭で打ち切った正垞終了」でしょうか。exit code だけでは区別が぀きたせん。

この区別を぀ける仕組みずしお蚭蚈したのが、「3日連続打ち切りで1日1回だけアラヌト」ずいう閟倀です。

1日では鳎らしたせん。

重い日は1回の打ち切りで終わるこずがありたす。前日にフォロワヌが急増しお゚ンゲヌゞが増えた、APIが重かった、別のゞョブず枠が競合した──単発の理由はいくらでもありたす。1日で鳎らすず、本圓に問題になる前に狌少幎になりたす。通知が毎日来るず、人間は読たなくなりたす。

毎日は鳎らしたせん。

3日連続で打ち切りが起きおいれば、それは構造問題です。capsず実行時間のバランスが恒垞的にずれおいたす。でもその事実を毎日アラヌトにしおも意味がありたせん。「たた来た」になるだけで、察凊する気にならない。

3日連続ずいう閟倀は「単発ノむズの倖」にあり、1日1回ずいう頻床は「察凊できる量」に収たっおいたす。この2぀の倱敗の間に、ちょうど良い点がありたす。

党䜓の流れ

構造をひず぀の図で瀺したす。

launchd が ig_engage.py を起動
        │
        ▌
  compute_budget()
  ┌─────────────────────────────────────────────┐
  │ 1. IG_ENGAGE_BUDGET_SEC環境倉数を読む    │
  │ 2. なければ BROWSER_SLOT_TIMEOUT_SEC を読む  │  ← 2,400秒
  │ 3. どちらもなければ 0党刀定を無効化      │
  │ deadline = START_TS + budget - 120秒         │  ← BUDGET_MARGIN_S
  └─────────────────────────────────────────────┘
        │
        ▌
  起動ゞッタヌランダム埅機
  ┌───────────────────────────────────────┐
  │ wait = min(900, (deadline - now) × 0.2) │  ← 残予算の20%でクランプ
  └───────────────────────────────────────┘
        │
        ▌
  アクションルヌプlikes → follows → unfollows
  ┌──────────────────────────────────────────────────────┐
  │ ルヌプ先頭: if blocked["hit"] or over_budget(): break │  ← 既存刀定に盞乗り4箇所
  │                                                        │
  │ over_budget() の内蚳:                                 │
  │   残予算 < action_min_s(20秒) → True                 │
  │   deadline を過ぎおいる        → True                 │
  │                                                        │
  │ action_sleep() で埅機するずき:                        │
  │   sleep時間を残予算内に収める                         │
  └──────────────────────────────────────────────────────┘
        │
        ▌
  return 0  ← exit 124 の代わりに "正垞終了"
  ブロック怜知・ログむン切れの exit 1 経路は䞀切觊れない
        │
        ▌
  state/engage_budget.json を曎新
  ┌─────────────────────────────────────────┐
  │ {                                         │
  │   "streak": N,          // 連続打ち切り回数 │
  │   "last_date": "YYYY-MM-DD",              │
  │   "last_alert_date": "YYYY-MM-DD"         │
  │ }                                         │
  └─────────────────────────────────────────┘
        │
        ├── 今日も打ち切りだった堎合
        │       streak += 1
        │       streak ≥ 3 か぀ last_alert_date ≠ today
        │              ↓
        │         alerts ぞ通知1日1回だけ
        │         "capsが実行時間に察しお過倧予算到達での打ち切りが
        │          N日連続likes XX/62, follows XX/24, unfollows XX/15"
        │
        └── 打ち切りなしだった堎合
                streak = 0 にリセット

コヌドの倉曎量は想像より少ないです。新しい制埡構造やクラスは䜜っおいたせん。既存の if blocked["hit"]: ずいう刀定が4箇所あり、それぞれに or over_budget() を远加しただけです。打ち切り時の凊理は return 0 の1行。新機胜の既定倀は「䜕もしない偎」に眮くずいうルヌルどおり、compute_budget() が予算を取れない倀が 0 たたは未蚭定ずきは deadline = None にしお党刀定を無効化し、既存の挙動を1ミリも倉えたせん。 環境倉数が消えた瞬間にゞョブが止たる䜜りにしない、ずいうのが原則です。

起動ゞッタヌのクランプが min(900, (deadline - now) × 0.2) になっおいる理由も算術です。元の start_jitter_max_s=900 をそのたた䜿うず、残予算2,400秒のうち最倧900秒がゞッタヌで消えたす。比率にしお37.5%。アクション前の埅機だけで予算の4割近くが飛ぶ蚈算です。残予算の20%ずいう䞊限を蚭けるこずで、jitterが長くなっおもアクション本䜓の時間が確保されたす。2,400秒の20%は480秒なので、元の900秒より短く、か぀短い予算のずきほどjitterも短くなりたす。

browser-slot.sh が BROWSER_SLOT_TIMEOUT_SEC をタむムアりト蚭定ずしお枡し、Perlスヌパヌバむザヌが時間超過時に exit 124 を返す構造は倉わりたせん。ig_engage.py が自分から締切前に終われば、スヌパヌバむザヌは exit $? >> 8=exit 0を返したす。exit 124 がログに出なくなった時点で、倖偎のフレヌムワヌク芳点では「正垞動䜜しおいる」になりたす。

だからこそ内偎のストリヌク監芖が芁りたす。倖から芋お緑なのに、䞭では毎日40アクションで止たっおいる──その差を怜知できるのは、ゞョブ自身が自分のパフォヌマンスを蚘録しおいるずきだけです。

実装の詳现

browser-slot.sh が倖偎の締切を䜜る

たず党䜓の倖枠を敎理したす。ig_engage.py は盎接 launchd から起動されるのではなく、browser-slot.sh を経由しお起動されたす。この shell スクリプトが「䜕個たで䞊列で Chromium を動かすか」を制埡するスロット管理局です。

スクリプトの䞭栞は Perl で曞かれたスヌパヌバむザヌです。bash ヒアドキュメントで埋め蟌たれおいお、実行時に perl -e で展開されたす。

my $timed_out = 0;
local $SIG{ALRM} = sub { $timed_out = 1; stop_tree(); };
alarm $timeout;
while (waitpid($child, 0) == -1) {
  next if $!{EINTR};
  exit 1;
}
alarm 0;
exit 124 if $timed_out;
exit $? >> 8;

alarm $timeout でタむマヌをセットし、子プロセスが終了するたで waitpid で埅ちたす。タむムアりトが来るず SIGALRM で $timed_out = 1 がセットされ、stop_tree() が走りたす。stop_tree() は /bin/ps -axo pid=,ppid= でプロセスツリヌを構築し、子孫党員に TERM → KILL の順で送信したす。

sub stop_tree {
  return if $stopping++;
  my @pids = descendants($child);
  kill "TERM", reverse(@pids), $child;
  select undef, undef, undef, 1;
  kill "KILL", grep { kill 0, $_ } reverse(@pids), $child;
}

ここが重芁です。kill "KILL" はプロセスに SIGKILL を送りたす。Playwright が async with browser.new_context() as ctx: の finally ブロックで ctx.close() を呌んでいおも、SIGKILL には抗えたせん。finally は実行されず、Chromium プロセスが孀児ずしお残りたす。

browser-slot.sh の末尟では exit code によっお RESULT を分けたす。

if [ "$status" -eq 124 ]; then
  echo "TIMEOUT: killed after ${TIMEOUT_SEC}s"
  RESULT="timeout:${TIMEOUT_SEC}s"
else
  RESULT="exit:$status"
fi

぀たり ig_engage.py が自分から exit 0 で終わった堎合、$? >> 8 は 0 になり、bash 偎のステヌタスも 0 です。RESULT は exit:0 ずしおログに蚘録されたす。スヌパヌバむザヌの exit 124 に到達しない限り、タむムアりトずは蚘録されたせん。これが「内偎から先に止たる」蚭蚈の効果です。

deadline の算出ず compute_budget()

Python 偎の実装は compute_budget() ずいう関数から始たりたす。環境倉数の読み取り順序は3段階になっおいたす。

def compute_budget() -> float | None:
    for key in ("IG_ENGAGE_BUDGET_SEC", "BROWSER_SLOT_TIMEOUT_SEC"):
        val = os.environ.get(key, "")
        if val.strip().isdigit() and int(val) > 0:
            return float(val)
    return None

IG_ENGAGE_BUDGET_SEC を先に読む理由は、ゞョブ固有の予算を BROWSER_SLOT_TIMEOUT_SEC ず切り離せるようにするためです。スロット偎のタむムアりトを倉えずに、゚ンゲヌゞゞョブだけ予算を絞りたい堎面がありたす。䞡方なければ None を返したす。

呌び出し偎では deadline をこう決めたす。

BUDGET_MARGIN_S = 120  # スヌパヌバむザヌがSIGKILLを送る前に確実に終わるための䜙裕

budget = compute_budget()
if budget is not None:
    deadline = START_TS + budget - BUDGET_MARGIN_S
else:
    deadline = None  # 党刀定を無効化

BUDGET_MARGIN_S = 120 ずいう120秒の䜙裕は、スヌパヌバむザヌの alarm $timeout が鳎るより先に Python 偎が return 0 たで到達するためのバッファです。最埌のアクションが終わっおから engage_budget.json を曞いおアラヌトを送るたでの時間を芋蟌んでいたす。

deadline が None のずき、続く over_budget() は垞に False を返したす。぀たり環境倉数が消えた瞬間、スクリプトは無制限に動くのではなく、スヌパヌバむザヌ任せの以前の状態に戻りたす。新機胜の既定倀は「䜕もしない偎」に眮くずいう原則です。

over_budget() は既存の刀定点に盞乗りする

ig_engage.py のアクションルヌプには4箇所の䞭断刀定がありたした。もずもずはブロック怜知だけが目的でした。

# 倉曎前ブロック怜知のみ
if blocked["hit"]:
    break

# 倉曎埌予算チェックを盞乗り
if blocked["hit"] or over_budget():
    break

この4箇所に or over_budget() を远加しただけです。新しい if ブロックもクラスも䜜っおいたせん。over_budget() の䞭身はこうです。

def over_budget() -> bool:
    if deadline is None:
        return False
    now = time.time()
    if now >= deadline:
        return True
    remaining = deadline - now
    return remaining < ACTION_MIN_S  # 20秒

「残り20秒未満なら打ち切る」ずいう刀定が入っおいたす。20秒は action_min_s の倀です。次のアクションを始めおも途䞭でスヌパヌバむザヌに打ち切られる可胜性が高い、ずいう蚈算です。䞭途半端に始めるより、今の時点で return 0 しお蚘録を残す方が良い。

アクション間の埅機 action_sleep() も予算内に収めたす。

def action_sleep(min_s: float, max_s: float) -> None:
    if deadline is not None:
        remaining = deadline - time.time()
        max_s = min(max_s, remaining - ACTION_MIN_S)
        if max_s <= 0:
            return  # 埅たずに即返す
    time.sleep(random.uniform(min_s, min(min_s, max_s)))

「残予算から次のアクション1回分20秒を匕いた倀」が埅機の䞊限です。残りが20秒を䞋回っおいればスリヌプせず即返したす。次の over_budget() チェックで打ち切られたす。

ゞッタヌクランプの算術

起動ゞッタヌのコヌドは1行です。

wait = min(START_JITTER_MAX_S, (deadline - time.time()) * 0.2)

START_JITTER_MAX_S はもずもず 900 秒でした。元の蚭蚈では「起動時刻を分散させるためランダム最倧900秒埅぀」ずいう意図でした。しかし予算 2,400 秒に察しお最倧 900 秒のゞッタヌは 37.5% です。4割近くが埅機だけで消えたす。

残予算の20%ずいう䞊限を蚭けるこずで、予算が長ければゞッタヌも長く分散効果を維持、予算が短ければゞッタヌも短くなりたすアクション時間を食わない。2,400秒の20%は480秒なので、元の900秒よりは短い。

実際に蚈算しおみたす。

deadline = START_TS + 2400 - 120 = START_TS + 2280秒
起動盎埌の残予算 ≒ 2280秒
20% = 456秒
min(900, 456) = 456秒

これで起動ゞッタヌは最倧456秒に収たりたす。残りの1,824秒でアクションを凊理したす。101アクション×平均40秒=4,040秒には届きたせんが、2,280秒では72アクション盞圓です。党量は凊理できないものの、SIGKILLなしで正垞終了できたす。

ストリヌク管理の実装

state/engage_budget.json の構造はシンプルです。

{
  "streak": 2,
  "last_date": "2026-08-14",
  "last_alert_date": "2026-08-12"
}

ゞョブ終了時にこのファむルを曎新したす。打ち切りが起きたかどうかは、実行したアクション数ず caps の比范で刀定したす。

was_truncated = (
    likes_done < LIKES_CAP or
    follows_done < FOLLOWS_CAP or
    unfollows_done < UNFOLLOWS_CAP
) and budget_hit  # 䞊限到達で打ち切った堎合のみ

budget_hit フラグを別に持぀のは、「ブロック怜知で止たった」堎合ず「予算切れで止たった」堎合を区別するためです。ブロック怜知による䞭断はストリヌクに加算したせん。あくたで「caps × delays の掛け算が枠に収たっおいない」ずいう構造問題を数えたい。

アラヌト条件は2぀の and です。

today = datetime.date.today().isoformat()
if streak >= 3 and state.get("last_alert_date") != today:
    send_alert(
        f"capsが実行時間に察しお過倧予算到達での打ち切りが{streak}日連続\n"
        f"likes {likes_done}/{LIKES_CAP}, "
        f"follows {follows_done}/{FOLLOWS_CAP}, "
        f"unfollows {unfollows_done}/{UNFOLLOWS_CAP}"
    )
    state["last_alert_date"] = today

last_alert_date != today ずいう条件が「1日1回だけ」を実珟しおいたす。streak >= 3 を満たした日に通知が飛び、その日䞭に䜕床ゞョブが走っおも last_alert_date が今日付けになっおいるので重耇したせん。翌日も streak が 3 以䞊を維持しおいれば、たた1通飛びたす。


私が詰たった話

蚭蚈ずしお「正しい」実装が、動かしおみるず別の問題を生み出すずいう経隓が3回ありたした。

詰たり① TypeError で党アクションが止たった

deadline=None のずき over_budget() は False を返す、ずいう蚭蚈は正しかったです。問題は action_sleep() の方でした。

# バグのあった版
def action_sleep(min_s, max_s):
    remaining = deadline - time.time()  # deadline が None → TypeError
    max_s = min(max_s, remaining - ACTION_MIN_S)
    ...

deadline を None チェックせずに参照しおいたした。症状は「ゞョブが起動盎埌に死ぬ、exit code 1」です。BROWSER_SLOT_TIMEOUT_SEC が蚭定されおいない開発環境でのみ発生したので、本番では1週間気づきたせんでした。

# slot.log で確認したログ
2026-08-09T06:01:03+0900 label=ig-engage group=engage result=exit:1
2026-08-10T06:01:14+0900 label=ig-engage group=engage result=exit:1

result=exit:1 はブロック怜知ず区別が぀きたせん。ログの芋た目はブロックず同じです。それが発芋を遅らせた原因でした。

盎し方は単玔です。action_sleep() の先頭にガヌドを入れたした。

def action_sleep(min_s, max_s):
    if deadline is None:
        time.sleep(random.uniform(min_s, max_s))
        return
    remaining = deadline - time.time()
    ...

孊んだこずは「deadline=None を無効化ずしお蚭蚈するなら、deadline を参照する党箇所に None チェックが芁る」です。圓たり前ですが、over_budget() だけ盎しお action_sleep() を芋萜ずすのは十分起きたす。

詰たり② 起動ゞッタヌで予算を党郚䜿い切っおいた

クランプを入れる前の話です。予算2,400秒に察しお起動ゞッタヌが最倧900秒でした。ゞッタヌが長く匕かれた日は、起動埌に残予算が1,500秒しかなく、even that was not the issue.

実際の問題は、over_budget() のチェックより前にゞッタヌが走るずいう順序でした。ゞッタヌで900秒埅ちながら、その間に deadline を過ぎおいた日がありたした。ゞッタヌ終了時点で残予算がマむナスになっおおり、最初の over_budget() チェックで即打ち切り、0アクションで return 0 したした。

# state/engage_budget.json を確認
{"streak": 1, "last_date": "2026-08-05", "last_alert_date": null}
# ただし likes_done=0, follows_done=0 ずいう状況

likes が 0 なのに打ち切りのストリヌクが積たれる。アラヌトはただ来ない。でも1日分の凊理が完党にスキップされおいたした。

クランプを入れる前にやるべきだったこずは、ゞッタヌも予算から匕くずいう意識を最初から持぀こずでした。「ゞッタヌ・埅機・アクション時間」の党郚を足し合わせお、実行枠に収たるかチェックする。郚分最適の積み重ねで党䜓が壊れたす。

詰たり③ 3日連続で鳎るはずの通知が来なかった

ストリヌクが3を超えおも通知が来ない日がありたした。ログを掘るず streak が毎日リセットされおいたした。

// 月曜
{"streak": 1, "last_date": "2026-08-11"}
// 火曜
{"streak": 1, "last_date": "2026-08-12"}  // ← 積み䞊がっおいない

原因は last_date の曎新ロゞックでした。初期実装では「今日打ち切りが起きたか」を「今日の日付が last_date ず䞀臎するかで刀定」しおいたした。

# バグのあった版
today = datetime.date.today().isoformat()
if was_truncated:
    if state.get("last_date") == today:
        pass  # 今日は既にカりント枈み
    else:
        state["streak"] = state.get("streak", 0) + 1
        state["last_date"] = today

䞀芋正しいように芋えたす。でも else ブランチで last_date を今日に曎新するずき、前日の last_date が昚日以倖のケヌスを想定しおいたせんでした。ゞョブが2日間スキップされたスロット競合で skip:global-limit になった埌に再開するず、streak が前日たで戻らずリセットされたす。

正しくは「前日が打ち切りだったか」を streak の継続条件にするこずです。

today = datetime.date.today()
yesterday = (today - datetime.timedelta(days=1)).isoformat()
today_str = today.isoformat()

if was_truncated:
    if state.get("last_date") == yesterday:
        state["streak"] = state.get("streak", 0) + 1
    elif state.get("last_date") != today_str:
        state["streak"] = 1  # 連続が途切れた、1から再スタヌト
    state["last_date"] = today_str
else:
    state["streak"] = 0
    state["last_date"] = today_str

「昚日が打ち切り」なら継続、「昚日以倖の日に打ち切り蚘録がある」なら streak を1にリセット、ずいう刀定です。これで2日スキップ埌に再開しおも、連続カりントが誀っお継続されなくなりたす。

この修正を入れおから1週間埌、初めおアラヌトが飛んできたした。「capsが実行時間に察しお過倧予算到達での打ち切りが3日連続likes 58/62, follows 18/24, unfollows 10/15」。通知が来お初めお、「連続3日ずいう意味でのバランス厩れ」が確認できたした。

共通する倱敗の型

3぀の詰たりに共通する型がありたす。

実装が「どこか」は正しいが、「接続郚分」が抜けおいる。

deadline=None は over_budget() では動くが action_sleep() では動かない。クランプはアクション時間には効くがゞッタヌには効かない。last_date の曎新はカりント継続には䜿えるが連続性の定矩が緩い。どの箇所も単独で読めば「正しい」ように芋えたす。問題は耇数の郚品が぀ながる境目にありたす。

自動化の怖さはここです。接続郚分のバグは、実行が成功しおいるexit 0ので発芚が遅れたす。ログを芋おも「正垞」に芋えたす。数字を時系列で䞊べお「増えるべきものが増えおいない」ず気づくたで、静かに間違ったたた動き続けたす。

ストリヌク監芖を䜜ったのは、そういう「静かに動いおいるが䞭身は半分」な状態を数字ずしお可芖化するためでした。ただし監芖コヌド自䜓も詰たりたす。監芖の監芖たで䜜る気にはなれないので、監芖コヌドは「シンプルに、ロゞックが䞀目でわかる行数に」ずいう方針で曞いおいたす。耇雑な監芖コヌドはそれ自䜓がバグの枩床になりたす。

぀たずきポむント

前節で deadline=None 参照挏れ・ゞッタヌ予算超過・streak の last_date バグの3件を詳述したした。ここではそれらを含め、同じ窓で出た党倱敗を集玄したす。

  • caps × delays の積を䞀床も蚈算しなかった。likes 62follows 24unfollows 15 = 最倧101アクション、平均埅機40秒。101 × 40 = 4,040秒。実行枠 BROWSER_SLOT_TIMEOUT_SEC=2400 の1.7倍。コヌドの1行も間違っおいない。どの蚭定倀も単独で芋ればたずも。積だけが砎綻しおいたした。

  • 䞊限を「名目倀×0.8」で削ったが実瞟に届かなかった。Claudeクォヌタ枯枇で2026幎8月13日に投皿0本になったずき、党レヌンを䞀埋2割カットしたした。しかし x-autoreply の実瞟は63〜93、䞊限は150です。150×0.8=120では実瞟93に届きたせん。削れたように芋えお䜕も削れおいたせんでした。正しい䞊限は実瞟×0.8で眮く必芁がありたす。ig-autoreply は OPEN_POLICY ? Infinity をやめお160実瞟200の8割に倉えたした。䞊限が「実行を瞛る倀」ではなく「実行の蚘述」にしかなっおいない堎面がありたす。

  • SIGKILLでChromiumが孀児化し、マシンロヌドが青倩井になった。~/.cache/lily-browser-slots/slot.log には毎朝 result=timeout:2400s が䞊んでいたした。しかし本圓の被害はそこではありたせんでした。Playwright の finally: ctx.close() はSIGKILLに抗えたせん。孀児Chromiumが積み䞊がり続けおマシン党䜓が䞍調になりたした。時間予算の超過が、無関係に芋えるシステム障害ずしお珟れる兞型です。

  • SLOT_MAXを思い蟌みで絞り、30%のゞョブがスキップされた。browser-slot.sh のコメントにその蚘録が残っおいたす。

    # 実枬(2026-08-09): Chrome系ゞョブ9個で合蚈0.7GB。swap枯枇の䞻犯は dasd(47GB)/
    # ComfyUI(12GB)/iii(3.5GB)であっおブラりザゞョブではなかった。䞊限3は過剰に厳しく
    # 1日で92回のskip(党䜓の30%)を出しおいたので5に緩める。
    SLOT_MAX="${BROWSER_SLOT_MAX:-6}"
    

    swap枯枇の原因をブラりザゞョブず思い蟌んで SLOT_MAX=3 に絞ったら、1日92回・党䜓の30%がスキップされおいたした。実枬したら䞻犯は dasd47GBず ComfyUI12GBで、Chromeは0.7GBしか䜿っおいたせんでした。

  • 投皿ゞョブが党スロット占有䞭に9回スキップされ、時間垯の枠が消えた。browser-slot.sh が圓時の経緯を蚘録しおいたす。

    # 2026-08-15: 朝の7本同時timeoutで枠6が死んだrunに占有され、投皿レヌン(xpilot.autopost
    # 等)が global-limit で9回skipした。いいね/フォロヌは1回萜ちおも翌回で取り返せるが、
    # 投皿はその時間垯の枠が消えるず二床ず埋たらない。
    SLOT_RESERVED_GROUPS="${BROWSER_SLOT_RESERVED_GROUPS:-post}"
    SLOT_RESERVE_COUNT="${BROWSER_SLOT_RESERVE:-1}"
    

    「いいねは翌日取り返せる、投皿の9時枠は9時を過ぎたら消える」ずいう優先床の差が、党スロットをフラットに扱う蚭蚈に埋もれおいたした。

  • Discord 2000字䞊限で日次レポヌトが䞞ごず萜ちた。lily-line-funnel/scripts/pdca.mjs の sendDiscordReport がレポヌト党文を1回のPOSTで送っおいたした。レポヌトが長い日は通知そのものが消えたす。倱敗しおいるのに倱敗ログがないサむレントな状態です。盎し方は行単䜍チャンク分割ず、文字数カりントを [...s].lengthサロゲヌトペア察応にするこず、Discord送信゚ラヌの本文は先頭800字で切っおログは党文のたた残す構成にするこずです。

  • Chrome screenshotの高さが「自動fit」しなかった。コヌド内コメントに「高さは自動コンテンツfitにするため倧きめwindow」ずありたしたが誀りでした。--headless=new --screenshot はりィンドりサむズをそのたた撮りたす。コンテンツが短くおも指定サむズの䜙癜が生たれる。note甚の衚画像が垞に1760×4000px䞋に巚倧な癜䜙癜のたた公開されおいたした。盎し方は2パス化。1パス目で --dump-dom から scrollHeight を読み、2パス目でその高さを指定しお撮圱したす。ただし1パス目のりィンドり高さは200にする必芁がありたす。2000のたたでは scrollHeight が2000未満にならず同じバグが残りたす。結果は 1760×4000 → 1760×1178 になりたした。

  • imagegen の出力寞法を指定どおりず信じおいた。「4:5瞊 1024×1280」ず指瀺しおも実際の出力は 1122×1402 や 1003×1568 で返りたす。生成ツヌルのアスペクト比指定は保蚌ではありたせん。レヌンによっお sips 正芏化したりしなかったりの䞍統䞀が残り、AI portraits の仕䞊がりがレヌン間でバラバラになりたした。生成埌に毎回匷制正芏化しお実寞を確認するのが正解です。

  • 「埋速は䞊限か䟛絊か」がログから刀別できなかった。フォロヌ数が䌞びない原因の調査で、「䞊限に達しお止たっおいるのか」「候補が枯枇しお凊理できないのか」がログを読んでも刀別できない状態がありたした。日次䞊限に到達しお打ち切り: ig-autoreply 160/160 の1行を打ち切り時に出力するだけで、「アルゎリズムかな」ずいう誀解による調査時間が消えたす。

  • result=exit:1 が゚ラヌの皮類を教えおくれなかった。deadline=None 時のTypeError、ブロック怜知による䞭断、ログむン切れによる䞭断がすべお同じ exit:1 で蚘録されおいたした。前節で詳述したずおりです。exit codeが同じなのに原因が異なるずき、調査はログ以倖の手がかりを探すこずから始たりたす。


ベストプラクティス

1. caps × delays × jitter の積を先に蚈算する

蚭定倀を曞いたあず、最悪ケヌスの所芁時間最倧caps × 最倧delay + jitter䞊限を出しお BROWSER_SLOT_TIMEOUT_SEC ず比べおください。「capsが正しい」「delaysが正しい」は別々の確認です。組み合わせが初めお砎綻したす。今回は 101 × 60 + 900 = 6,960秒 > 2,400秒が実行前に蚈算できた答えです。

2. 締切は「capsを䞋げる」ではなく「自分から先に止たる」で解決する

capsは成長斜策の䞊限です。䞋げれば斜策の効果が瞮みたす。compute_budget() で締切を蚈算しお、締切前に return 0 する蚭蚈に倉える方が成果の損倱が少ない。SIGKILLを受ける偎から、自ら止たる偎に立堎を倉える実装です。

3. 新機胜の既定倀は「䜕もしない偎」に眮く

def compute_budget() -> float | None:
    for key in ("IG_ENGAGE_BUDGET_SEC", "BROWSER_SLOT_TIMEOUT_SEC"):
        val = os.environ.get(key, "")
        if val.strip().isdigit() and int(val) > 0:
            return float(val)
    return None  # 党刀定を無効化

deadline = None のずき over_budget() は垞に False を返したす。環境倉数が消えた瞬間、以前の動䜜に戻りたす。環境倉数が消えた瞬間にゞョブが止たる䜜りにしない、が基本原則です。

4. 既存の刀定点に盞乗りしお、新しい制埡構造を䜜らない

# 倉曎前
if blocked["hit"]:
    break

# 倉曎埌4箇所に远加するだけ
if blocked["hit"] or over_budget():
    break

新しいクラスも if ブロックも䜜りたせん。倉曎量が小さいほど既存の挙動を壊すリスクが䞋がりたす。今回の䞻芁なロゞック倉曎は、4箇所ぞの or over_budget() の远加ず return 0 の1行です。

5. jitterも予算から差し匕き、残予算の20%でクランプする

wait = min(START_JITTER_MAX_S, (deadline - time.time()) * 0.2)

予算2,400秒に察しお最倧900秒のjitterは37.5%です。残予算の20%480秒でクランプするこずで、予算が短いほどjitterも短くなりたす。2,400秒の20%は480秒なので元の900秒より短く、か぀予算が削られるほどjitterも自動的に瞮みたす。ゞッタヌの埌に予算が残っおいるか確認しない実装は、0アクションで正垞終了したす。

6. 打ち切りずその他の゚ラヌをフラグで区別する

was_truncated = (
    likes_done < LIKES_CAP or
    follows_done < FOLLOWS_CAP or
    unfollows_done < UNFOLLOWS_CAP
) and budget_hit

budget_hit フラグを別に持぀こずで、ブロック怜知による䞭断ず予算切れによる䞭断を分離したす。ストリヌクは「capsず実行時間のミスマッチ」だけを数えたす。ブロックされた日はストリヌクを加算したせん。

7. 䞊限は「名目倀×0.8」ではなく「実瞟×0.8」に眮く

レヌン実瞟/日旧䞊限新䞊限実瞟×0.8
x-autoreply63〜9315075
ig-autoreplyDM200Infinity160
x-outbound12012096
threads-engage out47〜505040

実瞟93に察しお䞊限150を0.8倍した120は、実瞟に届きたせん。たず実瞟を枬り、その䞋に䞊限を眮く順序が正しい。䞊限が実瞟を䞋回っおいないなら、それは䞊限ではなく単なる数字です。

8. 重芁なゞョブ甚の予玄枠をスロット管理に組み蟌む

SLOT_RESERVED_GROUPS="${BROWSER_SLOT_RESERVED_GROUPS:-post}"
SLOT_RESERVE_COUNT="${BROWSER_SLOT_RESERVE:-1}"

「投皿は今日の時間垯が消えるず二床ず埋たらない」「いいねは翌日取り返せる」ずいう優先床の差をスロット蚭蚈に反映したす。党スロットをフラットに扱うず、重芁なゞョブが間匕かれたす。engage系いいね・フォロヌは effective_max = SLOT_MAX - SLOT_RESERVE_COUNT の実効䞊限で動かし、post系は SLOT_MAX たで䜿えたす。

9. 「噚のサむズ」は必ず掛け算した結果で管理する

Discord 2000字・Chrome scrollHeight・imagegen 出力寞法──いずれも「指定した倀が出る」前提で実装するず壊れたす。制玄は「曞いた数字」ではなく「出おくる数字の最悪ケヌス」で持぀必芁がありたす。送信前にチャンク分割、撮圱前にサむズ蚈枬、生成埌に匷制正芏化を工皋に組み蟌むこずで、「指定ず実際の乖離」が埌になっお発芚するバグを防げたす。

10. アラヌトは「3日連続 + 1日1回」の閟倀で蚭蚈する

1日で鳎らすず単発ノむズで読たれなくなりたす。毎日鳎らすず「たた来た」になりたす。3日連続は「単発の重い日の倖」にある最短の連続です。1日1回は「人間が察凊できる量」です。last_alert_date != today の条件1぀で、同日䞭の重耇通知を防げたす。

today = datetime.date.today().isoformat()
if streak >= 3 and state.get("last_alert_date") != today:
    send_alert(
        f"capsが実行時間に察しお過倧予算到達での打ち切りが{streak}日連続\n"
        f"likes {likes_done}/{LIKES_CAP}, "
        f"follows {follows_done}/{FOLLOWS_CAP}, "
        f"unfollows {unfollows_done}/{UNFOLLOWS_CAP}"
    )
    state["last_alert_date"] = today

11. streakカりントは「前日が打ち切り」で継続条件を定矩する

「last_dateが今日か」で刀定するず、2日スキップ埌の再開でstreakが正しく積たれたせん。「last_dateが昚日か」を継続条件にするこずで、スキップをたたいでも連続性が正しく定矩されたす。

yesterday = (today - datetime.timedelta(days=1)).isoformat()
if was_truncated:
    if state.get("last_date") == yesterday:
        state["streak"] = state.get("streak", 0) + 1
    elif state.get("last_date") != today_str:
        state["streak"] = 1  # 連続が途切れた
    state["last_date"] = today_str
else:
    state["streak"] = 0
    state["last_date"] = today_str

12. 「埋速は䞊限か䟛絊か」を打ち切り時の1行で残す

日次䞊限に到達しお打ち切り: ig-autoreply 160/160

この1行があるだけで、「フォロヌ数が䌞びない原因は䞊限か䟛絊䞍足か」の調査が2分で終わりたす。ないず、ログを2時間掘っおも刀別できたせん。凊理が止たった理由は、止たった時点で曞き出しおください。

13. SLOT_MAXは実枬倀から決め、コメントに根拠を残す

browser-slot.sh のコメントは蚈枬した数倀ずその刀断根拠を曞いおいたす。「Chromeç³»9個で0.7GB・䞻犯はdasd47GB」ずいう実枬が、次回の調査を2分で終わらせたす。蚭定倀にコメントがないずき、次の担圓者未来の自分は同じ調査を䞀から始めたす。


たずめ

自動化が「静かに動いおいるが䞭身は半分」になっおいた期間、ダッシュボヌドは問題なし、launchdは成功を蚘録し、毎朝 exit 0 が返っおきおいたした。

問題の構造は単玔です。capsず実行時間を別々の堎所に曞いお、䞀床も掛け算しなかった。その結果、毎日SIGKILLされ、Chromiumが孀児化し、マシン党䜓が䞍調になりたした。Discord の2000字、Chrome の scrollHeight、imagegen の出力寞法も同じ型です。コヌドのどの行を読んでも「正しい」。組み合わせだけが砎綻しおいる。このバグはコヌドレビュヌでは芋぀かりたせん。蚭定倀どうしの積を出す習慣だけが防ぎたす。

修正は倉曎量が少ない方が正しい実装です。compute_budget() で締切を蚈算し、over_budget() を既存の4箇所の刀定点に盞乗りし、return 0 で自分から止たる。コアのロゞック倉曎は2桁の行数で終わりたした。

ただし「自分から止たっお exit 0 にする」を遞んだ瞬間、「静かに止たる仕組みを静かに監芖する」責任が生たれたす。3日連続+1日1回アラヌトずいう蚭蚈は、その責任を果たす最小の構造です。1日では鳎らさない、毎日も鳎らさないずいう閟倀の間にある点が、狌少幎を避けながら構造問題を怜知できる唯䞀の堎所でした。

exit 0 を信じるか、exit 0 の䞭身を確認するか──その差が、月商120䞇を維持する自埋環境ず、静かに止たり続ける環境の分岐点です。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。 📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす