fix(subagents): stop resurrecting finished subagents as new spawns - #142
Merged
Conversation
An agent-<id>.jsonl is never deleted, but knownSubagents dropped completed entries five minutes after completion. The next time the subagents dir mtime moved — which is exactly when a new subagent starts — the whole directory was rescanned, every dropped file came back as unknown, and each one was announced as a fresh spawn. The running dot lit up on every historical subagent except the one that had just finished, and readSubagentMeta re-read every transcript on the main thread. Record an unknown file whose mtime is already stale as finished, silently, and forget an entry only once its file has left the disk, which bounds the map by the directory's own size. Post-bootstrap that staleness verdict is only an assumption: flushChanges' debounce is shared across PROJECTS_DIR and unbounded, so a burst of parallel subagents can delay the first sighting of a live agent past the threshold. Such an entry therefore keeps a recheck window and is rehabilitated — with the spawn that was withheld — as soon as its file grows; the window closes once the file has been seen motionless for a full stability window. Historical files never move, so they never take that path. A fork/resume re-key now also clears the subagent scan state, since it switches to another directory. Renderer side, a _heartbeat refreshes an agent already tracked but never creates one, in sidebar.js and grid-view.js alike. Fix the grid-view IPC handlers' arity while there: preload passes the payload as the callback's only argument, so (event, data) saw data undefined and the grid pills never rendered at all.
This was referenced Aug 22, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Reported from real use: the moment a new subagent starts, every historical subagent row in the sidebar lights up as "running" — except the one that had just finished. It settles again after 30–60s.
Mechanism:
session.knownSubagentsgarbage-collected finished agents 5 minutes after completion, but theiragent-<id>.jsonlfiles stay on disk. On the next scan they were "unknown" again, and the post-bootstrap spawn branch had no age filter — so every historical file was announced as a fresh spawn. The agent that had just completed was the only one spared, because its entry was still in the map markedcompleted. Hence "all of them except the real one".Side effect beyond the cosmetics:
readSubagentMetawas re-read for every historical file, in a burst, on the main thread, each time a subagent started.A test reproduces it exactly on the old code:
expected 1 spawn, got 31.The fix
Also fixed here
grid-view.jssubscribed with(event, data)whilepreload.jsinvokes callbacks with the payload alone —datawas alwaysundefinedand the guard returned immediately, so the grid's live subagent pills have never worked since they were introduced. Fixed and covered by tests; the heartbeat guard is applied there too, since the handler now actually runs.Tests
11 new tests. On the base commit, 10 of them fail — including the 30+1 reproduction and the renderer inversion test (three rows, one active, none left running after completion and a full re-render). A pre-existing flaky test (
concurrent monitoring, busy-waiting on Windows filesystem mtime granularity) was made deterministic; coverage is unchanged.task check: 601 tests, 594 pass, 0 fail, 7 pre-existing skips, 0 lint errors.Rationale in
.ai/contexts/subagent-observability.md, including an explicit "what is guaranteed and what is not" section — the 30s stability window remains the only completion signal, so a subagent silent longer than that can still be called finished while alive. Real liveness would have to come from the parent process, not from file mtime.Known residual (follow-up, not blocking)
If a batch of historical files is discovered late, after bootstrap has already been consumed on an empty directory, each gets a recheck window — and an accidental
touchon one of them within 30s would emit a single spurious spawn. Far narrower than the original defect (which fired unconditionally, for every agent, forever) and self-correcting. Worth extending the mass-spawn test to the post-bootstrap case.