Repository navigation
Claude subagent watchers never idle-exit: leaked kcap watch processes accumulate for the life of the parent session #514
Description
Summary
Claude subagent (child) watchers have no idle-timeout backstop. ShouldEndOnIdle is gated to codex/antigravity/cursor, so a Claude child watcher's only exit paths are the StopWatcher signal driven by SubagentStop and the parent-exit watchdog. If SubagentStop is missed for any reason, the watcher survives until the entire parent Claude process quits — which for a long-lived session means days.
if (vendor != "codex" && vendor != "antigravity" && vendor != "cursor") return false;#140 fixed the dominant cause of missed SubagentStop (fd inheritance hanging the hook, documented at ProcessHelpers.cs:279-290). This issue is about the missing backstop: when a SubagentStop is missed for any other reason — interrupted/aborted subagent, crashed hook, killed shell — nothing reaps the watcher.
Evidence
Observed on kcap 0.11.16 (macOS ARM64), from a live process table. One Claude session (b1e7a6ed…, open 12d 23h) owned 9 kcap watch processes: 1 legitimate session watcher plus 8 subagent watchers whose subagents had finished days earlier. Parent PID was alive in every case, so the parent-exit watchdog correctly never fired.
Watcher age vs. last write to the subagent transcript it was watching:
| watcher age | subagent transcript last written |
|---|---|
| 11d 21h | Jul 29 21:51 |
| 10d 01h | Jul 31 18:11 |
| 9d 15h | Aug 1 04:34 |
| 7d 22h | Aug 2 20:25 |
| 7d 00h | Aug 3 17:16 |
| 6d 16h | Aug 4 01:34 |
| 1d 13h | Aug 9 04:28 |
Roughly 8 leaked out of ~170 subagent transcripts for that session (~5%), so the miss is intermittent rather than systematic — consistent with the "disrupted flow never fires the matching SubagentStop" path rather than a wholesale failure.
Each idle watcher holds ~40 MB RSS and ~0% CPU, so the symptom is memory and process-table pressure, not load. Across a machine running 11 concurrent agent sessions the leaked watchers were ~280 MB.
Impact
- Unbounded process/RSS growth proportional to session lifetime × subagent count.
- Likely leaves the corresponding server-side sessions stuck
Active, since the watcher never posts its end. - Scales badly with the long-lived-session + heavy-subagent-fan-out workflow the product encourages.
Suggested fix
Give Claude child watchers a bounded lifetime that does not depend on the parent process. Two options:
- Extend the idle ceiling to Claude child watchers. The Cursor child-watcher exemption at
WatchCommand.cs:1080-1084already handles the "child watchers never buffer, soThresholdReachednever flips" problem — the same carve-out would apply. Cheapest change, reuses tested policy. - Hard ceiling on child-watcher lifetime independent of vendor, on the grounds that a subagent is inherently bounded work and a child watcher outliving its transcript by hours is always wrong.
Option 1 is probably right — it keeps one idle policy rather than adding a second concept. Either way the tool-in-flight suppression already in ShouldEndOnIdle should be retained so a genuinely long-running subagent tool call isn't cut off.
Worth confirming whether the session watcher's end-synthesis semantics should apply to a child watcher exiting on idle, the way CursorSuppressesEndPost scopes it for Cursor.
Repro
Not deterministic (depends on hitting a missed SubagentStop). Observable in aggregate: run a Claude session with heavy subagent fan-out for several days, then compare kcap watch --agent-id process ages against the mtimes of the agent-*.jsonl files they reference.
Activity
linear-code commentedon Aug 10, 2026
AI-1844