{"slug": "i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks", "title": "I Never Multiplied Two Config Values — and Got SIGKILLed Every Day for Two Weeks", "summary": "A developer's Instagram automation script was killed with SIGKILL every day for two weeks because two configuration values—action caps and delays—were never multiplied together, exceeding the execution window. The bug left orphaned Chromium processes, accumulating to 1,527 processes and degrading system performance. The developer's setup runs over 160 launchd-managed jobs generating ¥1.2M in monthly revenue.", "body_md": "Every setting in my Instagram automation script was correct. The action caps were correct. The delays were correct. And the job died with SIGKILL every single morning for two weeks — because nobody, including me, had ever multiplied those two correct numbers together.\n\nMy Mac currently runs more than 160 launchd-managed jobs. IG engagement, X auto-likes, note auto-posting, Threads, TikTok, follow management for each social network — all of them are registered as `.plist`\n\nfiles, and they keep grinding away in the background while I sleep, while I eat, and while I'm sitting in job interviews.\n\nThis setup is the skeleton of ¥1.2M in monthly revenue. My own hands-on time is close to zero: Claude Code writes code autonomously, Codex handles implementation, launchd handles scheduling. It took me six months to build a system where all I do is design and diagnose anomalies.\n\nBut a setup like this leaves room for **a bug you can never find, no matter how many lines of code you read**.\n\nThat's what this article is about.\n\nOne morning in August 2026, I noticed that the IG engagement job `brand-404/sns/ig_engage.py`\n\nwas finishing with **exit 124** every day.\n\nExit 124 means timeout. It's the exit code you get when SIGKILL is sent to a process. Thinking something was wrong with the code, I opened `ig_engage.py`\n\nand read it. The logic was correct. Exception handling was there. Playwright session management was fine. There was no bug anywhere.\n\nAnd yet it was being SIGKILLed every day.\n\nThe cause wasn't a single line of code. It was a missing piece of arithmetic: **I had never once multiplied two configuration values together.**\n\nWhen you write an automation script, you think about two things separately.\n\n**Caps** — how many actions per day. For IG, you decide something like \"62 likes, 24 follows, 15 unfollows.\" Sensible values, chosen with Instagram's rate limits in mind.\n\n**Delays** — random intervals between actions to look human. You set something like \"minimum 20 seconds, maximum 60 seconds.\" Also a sensible value, chosen to avoid bot detection.\n\nBoth are correct. Both are reasonable. But **I had never once computed their product.**\n\n```\n最大アクション数: likes 62 + follows 24 + unfollows 15 = 101回\n平均待機時間:     (20 + 60) / 2 = 40秒\n合計待機時間のみ: 101 × 40 = 4,040秒\n起動jitter最大:   900秒（人間らしく起動タイミングをばらつかせる設定）\n実行枠（launchd + browser-slot）: BROWSER_SLOT_TIMEOUT_SEC = 2,400秒\n```\n\n(Max actions: likes 62 + follows 24 + unfollows 15 = 101. Average delay: (20 + 60) / 2 = 40s. Total sleep time alone: 101 × 40 = 4,040s. Max startup jitter: 900s — a setting that randomizes launch timing to look human. Execution window via launchd + browser-slot: `BROWSER_SLOT_TIMEOUT_SEC`\n\n= 2,400s.)\n\n4,040s + 900s = 4,940s. That's **1.7×** the 2,400-second execution window. Both settings were correct; only the combination was broken.\n\nEvery day, the window expired before all actions finished and the process was SIGKILLed. This went on for two weeks.\n\nHere's the core of the story.\n\nIf the only consequence were \"it stopped partway and didn't finish the remaining actions,\" the loss would be a partially completed growth campaign. But the actual damage went further.\n\nWhen you do browser automation with Playwright, the standard practice is to put session cleanup in a `finally`\n\nblock.\n\n```\nctx = await browser.new_context(...)\ntry:\n    await do_engage_actions(ctx)\nfinally:\n    await ctx.close()  # ← ここが大事\n```\n\n**SIGKILL does not pass through that finally block.**\n\n`SIGTERM`\n\n(signal 15) can be caught by Python so it can clean up, but `SIGKILL`\n\n(signal 9) has the kernel kill the process immediately, so no handler runs at all. As a result, `ctx.close()`\n\nis never called and the Chromium process is left dangling.\n\nWhat happens when this repeats every day?\n\nMy `~/Documents/claude-obsidian/wiki/learning/mac-fleet-resource-leaks.md`\n\nrecords the measured numbers. On August 5, 2026, accumulated processes including orphaned Chromium peaked at **1,527**, of which 395 were node. CPU idle dropped to 18%, and load average hit **24.6**. Chrome headless startup hit its 180-second timeout — meaning it couldn't even launch — and not just IG but X, Threads, TikTok, and note all went down simultaneously.\n\nThe IG engagement SIGKILL was surfacing as a completely different phenomenon: total collapse of social-media automation.\n\n**Blowing the time budget shows up as machine-wide trouble that looks unrelated.** That's the nastiest part of this problem. You can't find it by reading code, and because the symptom appears somewhere else entirely, root-causing it takes a long time.\n\nLet me lay out why this pattern is so easy to create.\n\nWhen you're developing an automation script and verifying it locally, you don't think about the execution window. While debugging you narrow `caps`\n\ndown to 5 items, and once it works you restore production values. You shorten `delay`\n\nto make verification faster, and lengthen it in production.\n\nThis split between the \"verification phase\" and the \"production settings phase\" is what causes the multiplication to fall through the cracks. The code only ever runs with the combination of \"production caps × production delay\" in production — and that's the exact place where nobody has computed whether the total time fits in the execution window.\n\nlaunchd's `BROWSER_SLOT_TIMEOUT_SEC=2400`\n\nlives outside the script, in the plist or environment variables. `likes_cap=62`\n\nlives in the script's config values. `action_min_s=20`\n\nlives somewhere else again. In each of those places, every value looks correct. There is no place where the product gets computed.\n\nFirst, a diagram of the environment where the problem occurred.\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  macOS launchd（160本以上のジョブを管理）                      │\n│                                                             │\n│  com.lily.ig-engage（毎日06:00発火）                          │\n│     └─ StartInterval: 86400                                  │\n│     └─ BROWSER_SLOT_TIMEOUT_SEC: 2400  ←── 実行枠            │\n└─────────────────┬───────────────────────────────────────────┘\n                  │ 発火\n                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│  ~/.claude/scripts/browser-slot.sh                          │\n│  グローバル同時3本制限 + groupごとの上限を管理                   │\n│  取得できなければ exit 0（skip）                               │\n│  取得できたら BROWSER_SLOT_TIMEOUT_SEC を子プロセスに継承         │\n└─────────────────┬───────────────────────────────────────────┘\n                  │ スロット取得成功\n                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│  brand-404/sns/ig_engage.py                                 │\n│                                                             │\n│  設定値（スクリプト内）:                                        │\n│    likes_cap:      62                                        │\n│    follows_cap:    24                                        │\n│    unfollows_cap:  15                                        │\n│    action_min_s:   20   ← delay の下限                       │\n│    action_max_s:   60   ← delay の上限                       │\n│    start_jitter_max_s: 900                                   │\n│                                                             │\n│  ※ BROWSER_SLOT_TIMEOUT_SEC との積を計算する箇所が存在しない      │\n└─────────────────┬───────────────────────────────────────────┘\n                  │ Playwright起動\n                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│  Chromium（Playwright管理）                                   │\n│  Instagram へのアクション実行                                  │\n│                                                             │\n│  アクション間 sleep(random(20, 60))                           │\n│  101回 × 平均40秒 = 4,040秒  ←── 実行枠2400秒を大幅超過         │\n└─────────────────┬───────────────────────────────────────────┘\n                  │ 2400秒経過\n                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│  browser-slot.sh が SIGKILL を送信                            │\n│                                                             │\n│  ┌──────────────────────────────────────────────────────┐   │\n│  │  ig_engage.py の finally 節はスキップされる              │   │\n│  │  ctx.close() が呼ばれない                               │   │\n│  │  Chromium プロセスが孤児化して常駐                       │   │\n│  └──────────────────────────────────────────────────────┘   │\n│                                                             │\n│  翌日も同じことが繰り返される（exit 124）                       │\n└─────────────────────────────────────────────────────────────┘\n                  │ 孤児Chromiumが蓄積\n                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│  Macリソースの圧迫（mac-fleet-resource-leaks.md 実測値）         │\n│                                                             │\n│  プロセス総数: → 1,527本（nodeだけで395本）                     │\n│  CPU idle:    → 18%                                         │\n│  load avg:    → 24.6                                        │\n│  Chrome起動:  → 180秒タイムアウト（全レーン機能停止）             │\n└─────────────────────────────────────────────────────────────┘\n```\n\nThere were two options.\n\n**A. Lower the caps** — drop `likes_cap`\n\nfrom 62 to 30 and the product fits in the window. But that means cutting the Instagram account growth campaign itself in half. It means lowering a number tied directly to revenue.\n\n**B. Make the runtime self-adjust within the budget** — leave the caps alone. Instead, have the script always know how much time it has left and exit 0 on its own before the window closes.\n\nI chose B.\n\n`compute_budget()`\n\nThe idea is simple. When the script starts, compute a deadline once — \"by when do I have to be done?\" After that, before each action, check whether sleeping now would blow past the deadline, and return immediately if it would.\n\n``` python\nimport os\nimport time\n\nBUDGET_MARGIN_S = 120  # デッドライン直前の余裕（クリーンアップ用）\n\ndef compute_budget() -> float | None:\n    \"\"\"\n    IG_ENGAGE_BUDGET_SEC → BROWSER_SLOT_TIMEOUT_SEC → 0 の順で読む。\n    0（または未設定）のときは None を返し、全判定を無効化する。\n    環境変数が消えた瞬間にジョブが止まる作りにしない。\n    \"\"\"\n    raw = int(\n        os.getenv(\"IG_ENGAGE_BUDGET_SEC\")\n        or os.getenv(\"BROWSER_SLOT_TIMEOUT_SEC\")\n        or 0\n    )\n    if raw == 0:\n        return None\n    return time.time() + raw - BUDGET_MARGIN_S\n\ndef over_budget(deadline: float | None) -> bool:\n    if deadline is None:\n        return False\n    return time.time() >= deadline\n```\n\nThe important part is the `deadline=None`\n\nfallback. When the environment variable isn't set (local debug runs, test environments, and so on), budget control is completely disabled and existing behavior is unchanged. The principle is: **a new feature's default should sit on the \"do nothing\" side.**\n\n`ig_engage.py`\n\nhad a setting that inserts an initial random wait of up to 900 seconds, to make launch timing look human.\n\n```\nstart_jitter_max_s = 900\n\n# 修正前\njitter = random.uniform(0, start_jitter_max_s)\ntime.sleep(jitter)\n```\n\nThe problem is that this jitter can eat the entire execution window. In the worst case, startup jitter alone consumes 900 seconds, leaving only 1,500 seconds for the main work. And then the 4,040 seconds of caps × delay begins on top of that.\n\nAfter the fix, jitter is **clamped to 20% of the remaining budget**.\n\n``` php\ndef compute_jitter(deadline: float | None, jitter_max: float) -> float:\n    if deadline is None:\n        return random.uniform(0, jitter_max)\n    remaining = deadline - time.time()\n    # 残予算の20%を超えないようにクランプ\n    capped_max = min(jitter_max, remaining * 0.2)\n    return random.uniform(0, max(0, capped_max))\n```\n\nWith `BROWSER_SLOT_TIMEOUT_SEC=2400`\n\n, the remaining budget right after startup is about 2,280 seconds (after subtracting `BUDGET_MARGIN_S`\n\n). 20% of that is 456 seconds. Startup jitter is limited to a maximum of 456 seconds, guaranteeing at least 1,824 seconds for the main work.\n\nThe existing code had logic in four places that exits early on block detection.\n\n```\n# 修正前\nif blocked[\"hit\"]:\n    logger.warning(\"ブロック検知: 終了します\")\n    return 1\n```\n\nThe fix is just adding `or over_budget(deadline)`\n\nat each of those places. No new if-blocks, no new classes. It piggybacks on the existing decision points.\n\n```\n# 修正後\nif blocked[\"hit\"] or over_budget(deadline):\n    if over_budget(deadline):\n        logger.info(\"実行予算切れ: 正常終了します (exit 0)\")\n    else:\n        logger.warning(\"ブロック検知: 終了します (exit 1)\")\n    return 0 if over_budget(deadline) else 1\n```\n\nBlock detection is still `exit 1`\n\n. Budget exhaustion is `exit 0`\n\n. This distinction matters, so the monitoring system doesn't confuse \"something is wrong\" with \"planned early termination.\"\n\nThe sleeps between actions also need to respect the budget.\n\n``` php\ndef action_sleep(min_s: float, max_s: float, deadline: float | None) -> bool:\n    \"\"\"\n    残予算が action_min_s を切ったら、sleepせずに False を返す（打ち切り合図）。\n    それ以外は残予算に収まる範囲でsleepしてTrueを返す。\n    \"\"\"\n    if deadline is None:\n        time.sleep(random.uniform(min_s, max_s))\n        return True\n\n    remaining = deadline - time.time()\n    if remaining < min_s:\n        # 1アクション分も残っていない → 次のアクションを実行しても終わらない\n        return False\n\n    # 残予算に収まる上限でsleep\n    actual_max = min(max_s, remaining - min_s)\n    time.sleep(random.uniform(min_s, max(min_s, actual_max)))\n    return True\n```\n\nWhen `remaining < min_s`\n\n(remaining time is below the minimum wait time), it's certain that the next action would be cut off before completing. At that point, the function skips the sleep and returns `False`\n\n, and the caller decides to terminate normally.\n\nPut it all together and the main loop looks like this.\n\n``` php\nasync def run_engage(account: str) -> int:\n    deadline = compute_budget()\n\n    # 起動jitter（予算の20%上限）\n    jitter = compute_jitter(deadline, start_jitter_max_s)\n    if jitter > 0:\n        logger.info(f\"起動jitter: {jitter:.0f}秒待機 (残予算: {deadline - time.time():.0f}秒)\")\n        time.sleep(jitter)\n\n    async with async_playwright() as pw:\n        ctx = await pw.chromium.launch_persistent_context(profile_dir, **launch_opts)\n        try:\n            page = await ctx.new_page()\n            await login_if_needed(page, account)\n\n            liked = followed = unfollowed = 0\n\n            # いいねループ\n            for target in get_like_targets():\n                # 予算チェック: ブロック検知と同じ条件に相乗り\n                if blocked[\"hit\"] or over_budget(deadline):\n                    break\n                await like_post(page, target)\n                liked += 1\n                if not action_sleep(action_min_s, action_max_s, deadline):\n                    logger.info(f\"予算切れでlikeループ打ち切り: {liked}件完了\")\n                    break\n\n            # フォローループ（同様の構造）\n            for candidate in get_follow_candidates():\n                if blocked[\"hit\"] or over_budget(deadline):\n                    break\n                await follow_user(page, candidate)\n                followed += 1\n                if not action_sleep(action_min_s, action_max_s, deadline):\n                    logger.info(f\"予算切れでfollowループ打ち切り: {followed}件完了\")\n                    break\n\n            # アンフォローループ（同様の構造）\n            # ...\n\n            logger.info(f\"完了: likes={liked}, follows={followed}, unfollows={unfollowed}\")\n            return 0\n\n        finally:\n            # SIGKILLではなく正常終了なので、ここが必ず実行される\n            await ctx.close()\n            logger.info(\"Chromiumセッション正常クローズ\")\n```\n\n`finally: await ctx.close()`\n\nis guaranteed to run because this is exit 0 — a normal termination. Unlike when it was getting SIGKILLed, Chromium doesn't get orphaned.\n\nLet's recompute the runtime after the fix.\n\n```\n予算: BROWSER_SLOT_TIMEOUT_SEC=2400, BUDGET_MARGIN_S=120\n実効予算: 2400 - 120 = 2,280秒\n\n起動jitter上限: min(900, 2280 × 0.2) = min(900, 456) = 456秒\n\n起動jitterが最大456秒だったとして、本体処理への残予算:\n2,280 - 456 = 1,824秒\n\n1,824秒で何アクション実行できるか（average delay 40秒として）:\n1,824 ÷ 40 ≒ 45アクション\n\nlikes_cap=62, follows_cap=24, unfollows_cap=15 の合計101アクションには届かないが、\nSIGKILLされるより45アクション完遂して exit 0 する方が遥かに良い。\nChromiumも孤児化しない。\n```\n\n(Budget: `BROWSER_SLOT_TIMEOUT_SEC=2400`\n\n, `BUDGET_MARGIN_S=120`\n\n. Effective budget: 2400 − 120 = 2,280s. Jitter ceiling: min(900, 2280 × 0.2) = 456s. If jitter hits its 456s max, the main work gets 2,280 − 456 = 1,824s. At an average delay of 40s, 1,824 ÷ 40 ≈ 45 actions. That falls short of the 101 total actions from the caps — but completing 45 actions and exiting 0 is vastly better than being SIGKILLed, and Chromium doesn't get orphaned.)\n\nIn practice, startup jitter rarely reaches the 456-second ceiling; the average is around 228 seconds. In that case the main work gets 2,052 seconds of remaining budget, and the action count lands around 51.\n\nHere a new problem appears.\n\nThe moment budget exhaustion turns into `exit 0`\n\n, the monitoring system sees \"normal termination.\" Every day it \"runs fine,\" but the actual action count is less than half the cap — and that state is invisible to everyone.\n\n`~/Documents/claude-obsidian/wiki/learning/execution-budget-vs-caps.md`\n\nalso records how I handled this.\n\n```\nSTREAK_ALERT_THRESHOLD = 3  # 何日連続で鳴らすか\nENGAGE_BUDGET_STATE_FILE = \"~/dev/brand-404/state/engage_budget.json\"\n\ndef update_budget_streak(was_budget_limited: bool, \n                         likes: int, likes_cap: int,\n                         follows: int, follows_cap: int) -> None:\n    state = load_json(ENGAGE_BUDGET_STATE_FILE, default={\n        \"streak\": 0,\n        \"last_date\": None,\n        \"last_alert_date\": None,\n    })\n\n    today = date.today().isoformat()\n    if state[\"last_date\"] == today:\n        return  # 同日の2回目以降は無視\n\n    if was_budget_limited:\n        state[\"streak\"] = state.get(\"streak\", 0) + 1\n    else:\n        state[\"streak\"] = 0\n\n    state[\"last_date\"] = today\n\n    # 3日連続 かつ 今日まだアラートを出していない場合のみ通知\n    if state[\"streak\"] >= STREAK_ALERT_THRESHOLD:\n        if state.get(\"last_alert_date\") != today:\n            send_alert(\n                f\"⚠️ IGエンゲージが実行予算で打ち切られています\\n\"\n                f\"連続 {state['streak']} 日\\n\"\n                f\"likes: {likes}/{likes_cap}, \"\n                f\"follows: {follows}/{follows_cap}\"\n            )\n            state[\"last_alert_date\"] = today\n\n    save_json(ENGAGE_BUDGET_STATE_FILE, state)\n```\n\n**Don't fire on day one** (a single heavy day would make you the boy who cried wolf). **Don't fire every day** (continuous alarms stop being read). Fire only after three consecutive days, and only once per day.\n\nThis design separates \"stopping myself and exiting 0\" from \"detecting an anomaly.\" It treats normal early termination and a structurally-over-window caps setting as two different things.\n\n`compute_budget()`\n\nBelongs\nLet me also lay out the criteria for deciding \"which scripts should get this.\"\n\n**Required conditions (when all apply)**\n\n`browser-slot.sh`\n\nor launchd's StartCalendarInterval**Not needed (when any one applies)**\n\nIn this case, besides `ig_engage.py`\n\n, the same pattern existed in the X automation (`x_engage.py`\n\n) and Threads follow management (`threads_follow.py`\n\n). I did the work of adding `compute_budget()`\n\nto each of them at the same time.\n\n(Continued in the second half)\n\n`deadline`\n\nIs Exposed in Function Signatures\nThe first design question I wrestled with during implementation was \"where should `deadline`\n\nlive?\"\n\nClass variable, singleton, global — there were several options, but I rejected all of them and went with \"thread `deadline: float | None`\n\nthrough every function signature.\" There are two reasons.\n\nThe first is **testability**. `over_budget(None)`\n\nalways returns `False`\n\n. `action_sleep(20, 60, None)`\n\nbehaves exactly like the existing sleep. When the test side wants to disable budget control, it doesn't have to mock environment variables or rewrite config files — just pass `None`\n\nand all of the control disappears.\n\nThe second is **making call paths visible**. When `deadline`\n\nis lined up as an argument, the fact that \"this function is budget-aware\" shows up in the signature. Hide it in a class variable and you can't tell from looking at a function whether it has a time constraint.\n\n``` python\n# deadline を渡す側（明示的）\nasync def run_like_loop(page, targets, deadline: float | None) -> int:\n    liked = 0\n    for target in targets:\n        if over_budget(deadline):\n            break\n        await like_post(page, target)\n        liked += 1\n        if not action_sleep(action_min_s, action_max_s, deadline):\n            break\n    return liked\n```\n\nOn the calling side you write `run_like_loop(page, targets, deadline)`\n\n. Call it with `deadline=None`\n\nand you get an unlimited debug mode.\n\nInside `compute_budget()`\n\n, variables are read in this order.\n\n```\nraw = int(\n    os.getenv(\"IG_ENGAGE_BUDGET_SEC\")\n    or os.getenv(\"BROWSER_SLOT_TIMEOUT_SEC\")\n    or 0\n)\n```\n\n`IG_ENGAGE_BUDGET_SEC`\n\nis a script-specific override variable. It isn't set in production. It's used **when you want to set a shorter-than-production value to test just the budget control**. For example, run the script manually with `IG_ENGAGE_BUDGET_SEC=300`\n\nand you can confirm it cuts off after five minutes.\n\n`BROWSER_SLOT_TIMEOUT_SEC`\n\nis the value that `browser-slot.sh`\n\npasses to child processes. It's configured on the plist side. The script inherits \"by when should I finish\" from the parent that launched it. The script itself manages nothing.\n\nThe `0`\n\nfallback means \"`deadline=None`\n\n, all checks disabled.\" For manual runs that don't go through `browser-slot.sh`\n\n, or other environments where the variable isn't set, budget control is quietly disabled. This is intentional design. **A feature flag's default should sit on the \"do nothing\" side.** I don't build things that stop the job the moment an environment variable disappears.\n\n`action_sleep()`\n\n— Why Compare Against `min_s`\n\n```\nremaining = deadline - time.time()\nif remaining < min_s:\n    return False\n```\n\nThe key point is that this compares against `min_s`\n\n, not `max_s`\n\n.\n\n`remaining < max_s`\n\nwould mean \"give up unless the maximum wait time can be secured.\" But `action_sleep()`\n\nhas the ability to clamp the sleep shorter, so even if it can't reach `max_s`\n\n, it can sleep as long as it has at least `min_s`\n\n.\n\nThe reason for `remaining < min_s`\n\nis that I want to detect the moment when **the next action cannot complete within the deadline even if executed**. If you can't even secure the minimum sleep value, then sleeping and starting the action means a high chance of being SIGKILLed mid-action. Returning `False`\n\nand ending the action loop is cleaner than that.\n\n```\n# sleepのクランプ\nactual_max = min(max_s, remaining - min_s)\ntime.sleep(random.uniform(min_s, max(min_s, actual_max)))\nreturn True\n```\n\n`remaining - min_s`\n\nis the ceiling. This guarantees that \"after the sleep ends, at least `min_s`\n\nworth of execution window remains for the next action.\" With 50 seconds remaining and `min_s=20, max_s=60`\n\n, the actual sleep lands at 30 seconds max (50−20).\n\nThe code already had four places for block detection, expired-login detection, and other abnormal-termination decisions. Each of them is a path that returns `1`\n\n(error).\n\nCreating a new if-block would mean \"budget exhaustion\" starts existing as an independent control flow. That leaves a debt for the future maintainer (including future me): \"you have to read here to understand budget-exhaustion behavior.\"\n\nWith the approach of adding `or over_budget(deadline)`\n\nto existing decision points, reading \"the paths by which this script terminates\" naturally surfaces budget exhaustion too. One existing control flow gains a condition; no new control flow is born.\n\n```\n# 4箇所のうち1箇所\nif blocked[\"hit\"] or over_budget(deadline):\n    reason = \"予算切れ\" if over_budget(deadline) else \"ブロック検知\"\n    code   = 0           if over_budget(deadline) else 1\n    logger.info(f\"{reason}で終了 (exit {code})\")\n    return code\n```\n\n`blocked[\"hit\"]`\n\nand `over_budget(deadline)`\n\nmean different things. The former is an anomaly — \"Instagram detected us.\" The latter is normal — \"I stopped myself on schedule.\" Preserving that distinction in both the return code and the log message lets the monitoring system sort by exit code.\n\nScripts with the same structure as `ig_engage.py`\n\nincluded the X automation (`x_engage.py`\n\n) and Threads follow management (`threads_follow.py`\n\n). Both launch via `browser-slot.sh`\n\nand have sleeps in their action loops — the same pattern.\n\nThe rollout started by extracting `compute_budget()`\n\nand `action_sleep()`\n\ninto a utility file that can be shared across scripts.\n\n``` python\n# brand-404/sns/_budget.py\nimport os, time, random\n\nBUDGET_MARGIN_S = 120\n\ndef compute_budget(env_specific: str | None = None) -> float | None:\n    raw = int(\n        (os.getenv(env_specific) if env_specific else None)\n        or os.getenv(\"BROWSER_SLOT_TIMEOUT_SEC\")\n        or 0\n    )\n    return None if raw == 0 else time.time() + raw - BUDGET_MARGIN_S\n\ndef over_budget(deadline: float | None) -> bool:\n    return deadline is not None and time.time() >= deadline\n\ndef action_sleep(min_s: float, max_s: float, deadline: float | None) -> bool:\n    if deadline is None:\n        time.sleep(random.uniform(min_s, max_s))\n        return True\n    remaining = deadline - time.time()\n    if remaining < min_s:\n        return False\n    actual_max = min(max_s, remaining - min_s)\n    time.sleep(random.uniform(min_s, max(min_s, actual_max)))\n    return True\n```\n\nEach script's import became a single line.\n\n``` python\nfrom _budget import compute_budget, over_budget, action_sleep\n```\n\nIt took more than two weeks before I noticed the IG engagement job was finishing with `exit 124`\n\n.\n\nThe symptom I saw first was something else entirely. My Discord alert channel was piling up with metrics-hub collection failure notifications every day. All lanes — X, Threads, TikTok, note — kept emitting `browserType.launchPersistentContext: Timeout 180000ms exceeded`\n\n. Chromium couldn't launch even after 180 seconds.\n\nI started investigating on the assumption that it was \"a Chrome problem.\" I suspected a version mismatch and checked Playwright's update history. I suspected a leftover `SingletonLock`\n\nand cleaned out profile directories. Neither was it.\n\nThe correct diagnostic viewpoint is recorded in `mac-fleet-resource-leaks.md`\n\nlike this: \"Because **both** Chrome and `claude -p`\n\nwere down, I could immediately conclude it wasn't Chrome-specific but a compute-resource problem.\" If either one is alive, you can narrow it to a Chrome-specific issue. If both are dead, it's a problem with the resources underneath Chrome.\n\nCounting processes gave 1,527 (395 from node alone). CPU idle 18%. Load average 24.6. Orphaned Chromium had accumulated, and the 160+ constantly running jobs were all fighting over CPU time.\n\nI discovered that IG engagement was being SIGKILLed every day when I followed the logs chronologically. Two weeks of `exit 124`\n\nrecords, lined up at the same time every day. Each SIGKILL skipped the `finally`\n\nblock, and orphaned Chromium kept piling up. That accumulation manifested in a completely different form: Chrome's 180-second startup timeout.\n\nThere was not a single line of bug in the code itself. No matter how many times I read `ig_engage.py`\n\n, it was correct. The logic was correct. Exception handling was there. Playwright session management was fine. **The very act of reading code was ineffective against this class of bug** — that experience stuck with me.\n\nThe night I deployed the fix, the monitoring dashboard went all green. Naturally, since it now finished with `exit 0`\n\n. It felt like \"fixed.\"\n\nThree days later, a notification came into Discord. \"⚠️ IG engagement is being cut off by execution budget / 3 consecutive days / likes: 43/62, follows: 18/24\"\n\nThat was the first I learned that for three days, action counts had been getting cut off at around 70% of the caps every day.\n\nI added the streak alert on the same day as the fix, but this was the moment I felt it had \"really been necessary.\" Without it, the state of \"terminating normally every day, but at 70% results\" would have continued indefinitely, and I would never have seen that fact.\n\n\"Normal termination\" is not normal. There are two kinds of normal termination: \"finished having achieved the goal\" and \"wrapped up because time ran out.\" Monitoring systems normally don't distinguish the two.\n\nThe streak alert design of not firing on one day and only firing on three consecutive days comes from this dilemma. Fire on a single day and you get an alert every time a heavy day causes a one-off cutoff, and people stop reading them. Fire every day and it becomes a chronic alarm that gets ignored. Three consecutive days is the minimum sample size that demonstrates the fact \"the caps are structurally too large for the execution window.\"\n\nIn response to this notification, instead of lowering the caps, I raised `BROWSER_SLOT_TIMEOUT_SEC`\n\nfrom 2,400 to 3,600 seconds. There was slack before and after the time window when IG engagement runs, so widening the window let me avoid cutting the growth campaign.\n\nMy first implementation placed `over_budget(deadline)`\n\nonly at the top of each loop.\n\n```\n# 最初の実装\nfor target in get_like_targets():\n    if blocked[\"hit\"] or over_budget(deadline):\n        break\n    await like_post(page, target)\n    time.sleep(random.uniform(action_min_s, action_max_s))  # ← ここが問題\n```\n\nAfter `like_post()`\n\nfinishes and `time.sleep()`\n\nbegins, even if the deadline passes mid-sleep, nothing stops until the check at the top of the next loop. If a 60-second sleep starts when only 20 seconds of budget remain, it overruns by 40 seconds before trying to proceed to the next action, and only then does `over_budget()`\n\nbecome True.\n\nIn practice, the `BUDGET_MARGIN_S=120`\n\nslack meant a 40-second overrun wasn't a problem. But on days when startup jitter landed near its 456-second ceiling, the remaining budget for the main work got tight. In those cases the sleep ate deep into the remaining budget, and there were cases that exceeded the margin.\n\nThat's the motivation for creating `action_sleep()`\n\n. Give the sleep itself a notion of remaining budget, and this overrun structurally stops happening.\n\n```\n# 修正後\nfor target in get_like_targets():\n    if blocked[\"hit\"] or over_budget(deadline):\n        break\n    await like_post(page, target)\n    if not action_sleep(action_min_s, action_max_s, deadline):\n        # 残予算が action_min_s を切った → 次のアクションを実行しても終わらない\n        logger.info(f\"予算切れでlikeループ打ち切り: {liked}件完了\")\n        break\n```\n\nWhen `action_sleep()`\n\nreturns `False`\n\n, the remaining budget is under `action_min_s`\n\n. Running another loop iteration risks being SIGKILLed before the action completes. That's why receiving `False`\n\ntriggers an immediate `break`\n\n.\n\nAfter extracting `_budget.py`\n\ninto a shared utility, I wired it into `threads_follow.py`\n\n. The code is correctly implemented. I put `deadline = compute_budget(\"THREADS_FOLLOW_BUDGET_SEC\")`\n\nat the top and added `over_budget(deadline)`\n\nto each loop.\n\nBut when I ran a test locally, budget exhaustion never happened at all.\n\nThe cause was that `BROWSER_SLOT_TIMEOUT_SEC`\n\nwasn't set.\n\n`threads_follow.py`\n\nwas originally launched directly, without going through `browser-slot.sh`\n\n. It was one of the \"22 out of 49 that weren't going through it\" state recorded in `mac-fleet-resource-leaks.md`\n\n. Without going through browser-slot, `BROWSER_SLOT_TIMEOUT_SEC`\n\nnever arrives as an environment variable. `compute_budget()`\n\nreads `raw=0`\n\nand returns `deadline=None`\n\n. Every budget check is quietly disabled.\n\nNothing shows up in the logs either. `deadline=None`\n\noperates normally as \"no budget control,\" so no error or warning occurs. And reading the code, it looks like \"budget control is in place.\"\n\nThe fix had two stages. First, I changed `threads_follow.py`\n\nto launch via `browser-slot.sh`\n\nso that `BROWSER_SLOT_TIMEOUT_SEC`\n\ngets inherited. Second, I made `compute_budget()`\n\nlog the deadline state.\n\n``` php\ndef compute_budget(env_specific: str | None = None) -> float | None:\n    raw = int(\n        (os.getenv(env_specific) if env_specific else None)\n        or os.getenv(\"BROWSER_SLOT_TIMEOUT_SEC\")\n        or 0\n    )\n    if raw == 0:\n        logger.debug(\"budget制御: 無効（環境変数なし）\")\n        return None\n    deadline = time.time() + raw - BUDGET_MARGIN_S\n    logger.info(\n        f\"budget制御: 有効 (raw={raw}s, margin={BUDGET_MARGIN_S}s, \"\n        f\"deadline=T+{raw - BUDGET_MARGIN_S}s)\"\n    )\n    return deadline\n```\n\nStartup logs now carry either `budget制御: 有効 (raw=2400s, margin=120s, deadline=T+2280s)`\n\nor `budget制御: 無効（環境変数なし）`\n\n(\"budget control: enabled/disabled\"). Whether budget control is in effect can be confirmed by looking at a single line in the log file.\n\nThe reason I didn't want to change the `deadline=None`\n\ndesign is to uphold the principle \"don't build things that stop the job the moment an environment variable disappears.\" When running the script in debug runs, non-production environments, or test environments, having budget control cut execution off partway is confusing. Being disabled by `None`\n\nis the correct behavior. But if the fact that it *was* disabled is in the log, the investigation of \"why isn't it cutting off?\" gets answered in one second.\n\nWhat these four sticking points have in common is the property of \"invisible from reading code.\"\n\nIn #1, the product of config values is invisible. In #2, the meaning in the exit code is invisible. In #3, the time consumed inside the sleep is invisible. In #4, the presence or absence of the environment variable is invisible.\n\nMany problems in automation scripts are hard to find via code review. The case where **the code is correct but the combination of configuration, environment, and runtime is broken** is only visible through logs and measured values.\n\nThe pattern recorded in `execution-budget-vs-caps.md`\n\nas \"the implementation doesn't know the size of the container\" is a broader problem that includes these four. Discord's 2,000-character limit per message, the window size of Chrome screenshots, the output dimensions of AI image generation — four instances of the same pattern showed up in the same window at once. Write the limit and the time/capacity it takes to reach that limit in separate places, and both can be correct while only the combination breaks. Hold constraints **as the multiplied result**, and decide in advance what happens when they're exceeded — that's the biggest principle I took from this whole series of fixes.\n\n**The \"every line is correct\" kind of stuck takes the most time.** As written above, I read `ig_engage.py`\n\nthree times and found no bug. The answer isn't in the code, so the act of reading code is ineffective. With this type of sticking point, the assumptions \"I'm reading it wrong\" and \"one more pass and I'll spot it\" are what drag it out. There is no way to discover a problem in the product of config values other than **logs and measured values**.\n\n**The indirection of \"the symptom appears somewhere else\" slows down diagnosis.** The IG engagement SIGKILL caused all lanes — X, Threads, TikTok, note — to stop. As recorded in `mac-fleet-resource-leaks.md`\n\n, the total process count on August 5, 2026 was 1,527 (395 from node alone), and load average was 24.6. What surfaced was the Playwright error `browserType.launchPersistentContext: Timeout 180000ms exceeded`\n\n(Chrome's 180-second startup timeout), which looks like \"a Chrome problem.\" The real cause was the IG script orphaning Chromium every day.\n\n**The question \"are Chrome and claude -p both dead?\" reduces diagnosis to one line.** If either is alive, it's a Chrome-specific problem. If both are dead, you can immediately conclude it's a compute-resource problem. Knowing this, you decide to \"dig into resources\" before even considering the version-mismatch theory and the leftover-\n\n`SingletonLock`\n\ntheory.**A reversal happens where \"the processes you launched become part of the failure.\"** `mac-fleet-resource-leaks.md`\n\nrecords that an over-broad grep I launched during investigation burned 73% CPU for 43 minutes. The investigation tool eats resources and worsens the symptom further. The loop of \"the reason it isn't fixed is my own investigation command\" really does happen. If you launch a broad grep, you need the awareness that **you are responsible for it until you reap it**.\n\n**Beyond Playwright, \"implementations that don't know the size of the container\" showed up four times in the same window.** `execution-budget-vs-caps.md`\n\nrecords this as a \"same-window pattern.\"\n\n**Discord's 2,000-character per-message limit** — `sendDiscordReport`\n\nin `lily-line-funnel/scripts/pdca.mjs`\n\nwas POSTing the full text in one shot. On days when the report was long, the entire daily notification failed. The limit is a Discord spec, the character count is the script's output; those two lived in separate places and nobody was multiplying them.\n\n**Chrome screenshot window size** — an in-code comment saying \"use a large window so the height is automatic (content fit)\" was wrong; in reality it captures at the window size as-is. Table images for note were always 1760×4000px, i.e. published with a huge white margin below the table. The correct approach is two passes (measure scrollHeight with `--dump-dom`\n\n, then capture), which fits within 1760×1178. The key is setting the first pass's window height to 200 — leave it at 2,000 and scrollHeight never goes below 2,000, so the same bug remains.\n\n**Built-in imagegen output dimensions** — even when instructed \"4:5 portrait 1024×1280,\" raw output comes back as 1122×1402 or 1003×1568. Generation tools' aspect specifications can't be trusted, so forced normalization after saving is required every time.\n\nThese three are exactly the same pattern as the IG engagement SIGKILL problem. Write \"the limit\" and \"the time/capacity it takes to reach that limit\" in separate places, and both are correct while only the combination breaks.\n\n**The moment I switched to exit 0, \"only running at 70% every day\" was invisible to everyone for three days.** `execution-budget-vs-caps.md`\n\nrecords that \"the moment you turn a cutoff into a normal termination, you enter the silent-success antipattern.\" Until \"⚠️ IG engagement is being cut off by execution budget / 3 consecutive days / likes: 43/62, follows: 18/24\" arrived in Discord, the monitoring dashboard was all green. Had I not included the streak alert in the same change, this state would have continued indefinitely.\n\n**Adding compute_budget() to a script that doesn't go through browser-slot silently disables it.** As detailed in part 2,\n\n`threads_follow.py`\n\nwas originally launched directly without `browser-slot.sh`\n\n. `BROWSER_SLOT_TIMEOUT_SEC`\n\nnever arrives, so `raw=0`\n\n, `deadline=None`\n\n, and every check is quietly disabled. Nothing appears in the logs — no warning, nothing. `mac-fleet-resource-leaks.md`\n\nrecords that \"22 out of 49 were slipping straight past the concurrency gate.\" Roll out without checking whether a script goes through browser-slot, and \"I added budget control\" ends up not matching reality.**You can't notice that \"the reaper isn't working\" until you fix the reaper.** `auto-reboot.sh`\n\nwas judging on `vm.swapusage`\n\n's `total`\n\n(allocated size), so transient Spotlight spikes were falsely detected as emergency reboots. On top of that, the condition \"don't reboot if any job is running\" was structurally impossible to trigger in an environment where 130 jobs run constantly. The `reboot-requested`\n\ncount was zero. I couldn't notice that the safety net had never once worked until I started debugging.\n\n**The \"double acquisition\" that consumed two global slots was something I built into the wiring myself.** `run-account.sh`\n\nacquires a slot internally, but the plist also wrapped it in `browser-slot.sh`\n\n, so one job consumed two slots. Effective capacity was halved, and I only noticed because a log's group name differed from what I expected. After wiring something up, you need to look first for \"behavior different from intent,\" not for \"evidence that it worked.\"\n\n** dasd (macOS's Duet Activity Scheduler) had 47GB of swap despite 264MB of physical RSS.** Looking at the top 10 processes, it never shows up. A reaper has to work on compressed memory (CMPRS), not RSS, or it walks right past the real culprit. The correct command is a single\n\n`sudo killall dasd`\n\n, and launchd rebuilds a new 28MB process. No reboot was needed.**① Compute max_actions × avg_delay before launch and write it in one place as the execution-budget.** Stop managing caps and delays as separate variables; compute the multiplied duration up front as\n\n`EXPECTED_DURATION_SEC`\n\n. If that value exceeds the execution window, handle it as a design problem before you ever get into the script.**② Put compute_budget()'s default on the \"do nothing\" side (deadline=None).** Don't design things that stop the job the moment an environment variable disappears. Read in three stages —\n\n`IG_ENGAGE_BUDGET_SEC`\n\n→ `BROWSER_SLOT_TIMEOUT_SEC`\n\n→ 0 — and if it's 0, disable all checks and preserve existing behavior exactly.**③ Always log whether budget control is enabled or disabled at startup.**\n\n```\nif raw == 0:\n    logger.debug(\"budget制御: 無効（環境変数なし）\")\n    return None\nlogger.info(f\"budget制御: 有効 (raw={raw}s, deadline=T+{raw - BUDGET_MARGIN_S}s)\")\n```\n\nThe investigation \"why isn't it cutting off?\" gets answered in one second. The purpose is to make visible the state where `compute_budget()`\n\nis quietly returning `None`\n\n.\n\n**④ Clamp startup jitter to 20% of the remaining budget.** Capping with `min(jitter_max, remaining * 0.2)`\n\nprevents the case where jitter eats the entire execution window. With `BROWSER_SLOT_TIMEOUT_SEC=2400`\n\n, the jitter ceiling becomes 456 seconds, guaranteeing at least 1,824 seconds for the main work.\n\n**⑤ Give action_sleep() the remaining budget too; don't rely on a check at the top of the loop alone.** Even with a check at the top of the loop, if the deadline passes during a sleep nothing stops until the next top-of-loop check. A design where\n\n`False`\n\nis returned when `remaining < action_min_s`\n\nand the caller immediately `break`\n\ns is what creates a structure that stops just short of SIGKILL.**⑥ Clearly distinguish budget exhaustion as exit 0 from block detection as exit 1.** This keeps the monitoring system from confusing \"something is wrong\" with \"planned early termination.\" Piggybacking on the same condition is enough; don't create a new control flow.\n\n```\nif blocked[\"hit\"] or over_budget(deadline):\n    code = 0 if over_budget(deadline) else 1\n    logger.info(f\"{'予算切れ' if code==0 else 'ブロック検知'}で終了 (exit {code})\")\n    return code\n```\n\n**⑦ Fire the alert on three consecutive days — not on one day, and not every day.** Keep `{streak, last_date, last_alert_date}`\n\nin `state/engage_budget.json`\n\nand notify only when it's three consecutive days and today hasn't been notified yet. Fire on one day and a one-off heavy day makes you the boy who cried wolf; fire every day and it gets ignored. Three days is the minimum sample size to demonstrate \"the caps are structurally too large for the execution window.\"\n\n**⑧ Thread deadline through every function signature to make it visible.** Hide it in a class variable or a global and the fact that \"this function is budget-aware\" disappears from the signature. Writing\n\n`action_sleep(min_s, max_s, deadline)`\n\nmeans switching between debug mode (call with `deadline=None`\n\n) and production mode (pass a real value) is complete with a single argument. The test side just passes `None`\n\ninstead of mocking environment variables.**⑨ Every time you roll out, check \"does it go through browser-slot?\" and extract shared utilities.** Even if you build a shared module like `_budget.py`\n\n, budget control is silently disabled in scripts where `BROWSER_SLOT_TIMEOUT_SEC`\n\nnever arrives. Check whether the script you're rolling out to goes through `browser-slot.sh`\n\n, and if it doesn't, wire that up first before integrating.\n\n**⑩ Keep a separate reaper for orphaned processes, and design on the assumption that you will be SIGKILLed.** `chrome-reaper.sh`\n\nTERMs-then-KILLs orphaned Chrome with parent PID=1 (where `--user-data-dir`\n\nis under `~/dev/`\n\n) and Chromium / chrome-headless-shell under `ms-playwright`\n\nthat exceed 30 minutes. Even with perfect budget control, other jobs will get SIGKILLed. The fact that \"SIGKILL doesn't pass through `finally`\n\n\" doesn't change, so running a reaper every 10 minutes is your fail-safe.\n\n**⑪ The broader the symptom, the more you narrow the root cause by \"what is not dead simultaneously.\"** If both Chrome and\n\n`claude -p`\n\nare dead, it's compute resources. If only Chrome is dead, it's a Chrome-specific problem. When multiple lanes go down at once, the first thing to check is the resource side (process count, load average, swap usage, remaining disk). In the August 5, 2026 case, `ps aux | wc -l`\n\ngave 1,527 and load average was 24.6. The moment those numbers appear, the order becomes \"crush the resource side before reading the individual logs of every lane.\"**⑫ Periodically confirm that \"the safety net has never once fired.\"** Just as `auto-reboot.sh`\n\n's `reboot-requested`\n\ncount was zero, a safety net can look configured while its conditions actually make firing impossible. Print `trigger=`\n\n/ `decision=`\n\n/ `blocking_jobs=`\n\nall in the dry-run output and the state \"the trigger condition was met, but a running job means no reboot\" reads in one line. Safety-net effectiveness needs to be verified at least once a month.\n\n**⑬ Rigorously avoid reporting temporal sequence as causation.** `mac-fleet-resource-leaks.md`\n\ncontains a record where \"load dropped from 82 to 24\" was reported as \"the effect of the exclusion.\" In reality the exclusion itself was ineffective and it was a timing coincidence. When an ineffective move gets enshrined as canon, you try it first at the next incident. Even when the fix and the improved numbers sit close together in time, writing it as causation requires a separate comparison against \"what would have happened without the exclusion.\"\n\nIG engagement was being SIGKILLed every day for two weeks. There wasn't a single line of bug in `ig_engage.py`\n\n's code, and reading the code couldn't find it. The cause was a missing piece of arithmetic: I had never once multiplied the cap `likes_cap=62`\n\nwith the wait times `action_min_s=20 / action_max_s=60`\n\n. 101 actions × an average of 40 seconds = 4,040 seconds, plus up to 900 seconds of startup jitter, gives 4,940 seconds. That's 1.7× the execution window of `BROWSER_SLOT_TIMEOUT_SEC=2400`\n\n.\n\nSIGKILL does not pass through Python's `finally`\n\nblock. Without `ctx.close()`\n\never being called, Chromium was orphaned and accumulated every day. The result was 1,527 processes, load average 24.6, Chrome's 180-second startup timeout, and every social-media lane going down. One script's time overrun surfaced as machine-wide trouble that looked completely unrelated.\n\nThe direction of the solution wasn't \"lower the caps and cut the growth campaign\" but \"have the script itself know its remaining time and stop on its own with `exit 0`\n\nbefore the window closes.\" `compute_budget()`\n\ncomputes the deadline exactly once at startup, `over_budget(deadline)`\n\npiggybacks on each loop's existing checks, and `action_sleep()`\n\ngives the sleep itself a notion of remaining budget. On top of that, a streak alert that detects \"three consecutive days of budget cutoff\" makes the quiet degradation of \"it became exit 0 but results are at 70%\" visible.\n\nIn the same window, three more instances showed up at once: \"a report POSTed without knowing Discord's 2,000-character limit,\" \"the gap between Chrome screenshot window size and output size,\" and \"the built-in imagegen's aspect specification can't be trusted.\" The pattern is identical in all of them. **Write \"the limit\" and \"the time/capacity it takes to reach that limit\" in separate places, and both are correct while only the combination breaks.** Holding constraints as the multiplied result, and deciding in advance what happens when they're exceeded, is the only way to get ahead of failures that reading code can't reveal.\n\nMany problems in automation scripts are hard to find via code review. The case where the code is correct but the combination of configuration, environment, and runtime is broken is only visible through logs and measured values. \"Running\" and \"earning\" are not the same thing — and neither are \"terminating normally\" and \"achieving the goal.\"\n\nI've written up the full picture of the setup, the breakdown of the ¥1.2M/month, and the 30-day procedure in a paid note.\n\n📕 [Claude Code自律環境で、実際どう稼ぐか ― 仕組み・実例・始め方・サポート](https://note.com/bokuwalily/n/n849b3a07784a)\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks", "canonical_source": "https://dev.to/bokuwalily/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks-5hgo", "published_at": "2026-08-27 05:00:06+00:00", "updated_at": "2026-08-27 05:18:25.489423+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Instagram", "Claude Code", "Codex", "launchd", "Playwright", "Chromium"], "alternates": {"html": "https://wpnews.pro/news/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks", "markdown": "https://wpnews.pro/news/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks.md", "text": "https://wpnews.pro/news/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks.txt", "jsonld": "https://wpnews.pro/news/i-never-multiplied-two-config-values-and-got-sigkilled-every-day-for-two-weeks.jsonld"}}