Skip to content

Windows: terminal-agent watchdog respawn-loops every 60s — console flash + unbounded bun.exe leak; circuit-breaker can never trip #2295

Description

@rroojrooj

Summary

On Windows, the browse daemon's terminal-agent watchdog gets stuck in a permanent respawn loop: it spawns a fresh bun run terminal-agent.ts every 60s (AGENT_WATCHDOG_TICK_MS) even though the previous agent is alive and its pid record is fresh. Each spawn flashes a visible bun.exe console window (polyfill drops windowsHide — fix in PR #2294), old agents are never reaped, and the respawn circuit-breaker can mathematically never trip.

Environment

  • Windows 11 Pro, build 26200 (desktop)
  • gstack @ a3259400 (update-check reports UP_TO_DATE 1.60.1.0)
  • Browse daemon running as node.exe dist/server-node.mjs with bun-polyfill.cjs (the documented Windows fallback)

Measured behavior

Respawn cadence — process-creation watcher, one daemon (pid 35744), exact 60s beat:

13:19:43.206 START bun run ...\terminal-agent.ts  (parent 35744)
13:20:43.368 START bun run ...\terminal-agent.ts  (parent 35744)
13:21:43.500 START bun run ...\terminal-agent.ts  (parent 35744)

A second daemon (pid 47248, different project state dir) interleaved its own 60s loop → a console flash every ~30s wall-clock.

Leak — 7 spawns vs 1 exit in a 6-minute capture; process count 3 → 9 in ~10 minutes, ~90 MB working set. One daemon had been orphaned (its parent process dead) and looping like this for 2 days. The daemon-side kill path (spawnTerminalAgent kills the pid in the record before spawning) doesn't cover them: 9 agents alive with only 2 pid records in existence.

The respawn happens despite a healthy agent. At the time of the respawns, the terminal-agent-pid records existed, were freshly written (seconds-old), and pointed at running pids.

isProcessAlive is exonerated. I ran the exact implementation under node --require bun-polyfill.cjs (the daemon's real runtime): it returned true for a live agent pid and false for a dead one, with clean tasklist CSV output. So the mis-detection is somewhere else — possibly a stateDir/projectDir mismatch between where the daemon looks and where the agent writes (I did not isolate it; occasionally a tick is skipped, so the check does sometimes pass).

The circuit-breaker is dead code. RESPAWN_GUARD_MAX = 3 within RESPAWN_GUARD_WINDOW_MS = 60_000, but the watchdog tick is also 60s — entries older than the window are pruned before counting, so respawnHistory can hold at most ~1 entry at each tick and the guard can never reach 3. The "manual restart required" log line is unreachable at default settings.

Suggested directions

  1. Merge PR fix(browse): pass windowsHide in bun-polyfill spawn wrappers (Windows console flash every 60s) #2294 (windowsHide in the polyfill) — stops the visible flashing; no-op off Windows.
  2. Find why the watchdog considers a live, freshly-recorded agent dead (stateDir resolution seems the prime suspect).
  3. Make the guard window meaningfully larger than the tick (e.g. 10 min window / tick 60s), so a genuine crash-loop actually trips it.
  4. Reap: kill the previous agent by pid before spawning even when the record lookup misses, or track children directly — observed agents do self-exit when their daemon dies, but not when superseded.
  5. Consider a daemon-side "am I orphaned?" check — the 2-day looper's parent was long dead.

Happy to provide the raw watcher logs or test a branch on this machine.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions