fix(subagent): give the liveness signal a clock and make completion reversible - #153
Merged
Conversation
devsuitup
force-pushed
the
fix/subagent-completion-tick
branch
2 times, most recently
from
August 23, 2026 23:08
15c980d to
1e85d00
Compare
…eversible detectSubagentTransitions() owns spawn, heartbeat and completion, but it only ran from the projects watcher's debounced flush. A function that runs when a file changes cannot see that a file stopped changing: agent a9be19fb0a7e0e504 last wrote at 23:42:18 and its completion was logged at 23:52:55 — 10 min 37 s for a 30 s stability window — because the folder went quiet and no flush ran. The same window also fires wrongly. Agents a8e8c25f42a65b026 and a19dcbbb23270de85 were declared complete at 23:56:53 and 00:00:27 while still writing minutes later: a tool call longer than the window reads as a finished agent. That verdict was irreversible — the entry carried no recheck window, so it hit the completed fast path forever and the agent could never light up again. A stability completion now keeps a recheck window, so renewed growth rehabilitates it and re-emits the spawn. That is what makes the tick safe: adding a clock makes false completions more frequent, not less. Rather than let such an agent oscillate, its stability window widens on each rehabilitation along a capped ladder (30 s, 2 min, MAX_STABLE_MS) so it converges. An assumed-finished entry is deliberately excluded: it carries no evidence of long silences, and widening it would slow the normal case. Widening needs one companion change. A wider window lets an agent stay silent past the renderer's TTL without completing, and the renderer refuses a heartbeat for an agent it no longer tracks — on its own the widening would trade a blink for a permanent blackout. The growth branch therefore emits a real spawn instead of a heartbeat once the agent has been unseen that long. That agreement between the main process and the two views was three separate 60000 literals held together by a comment; shortening one alone silently recreates the blackout. public/subagent-timing.js is now the single definition, using the dual-mode pattern of public/shortcuts.js: classic script for the renderer, module.exports for require(). Both renderer safety nets had the same shape of bug — pruneStaleGridSubagents() ran only from wrapInGridCard() and pruneStaleSubagents() only from renderProjects(), so a stale entry survived exactly in the idle case the TTL exists to cover. Each view now arms a one-shot timer at the oldest entry's deadline, refreshes only the entries the prune removed, and stops when its map is empty (ADR 0002: no steady-state cost). Not changed: the watcher debounce has no maxWait, but the log shows completions emitted while three transcripts were being written, so starvation under write load is refuted and nothing was added for it.
devsuitup
force-pushed
the
fix/subagent-completion-tick
branch
from
August 23, 2026 23:18
1e85d00 to
a6783ac
Compare
devsuitup
added a commit
that referenced
this pull request
Aug 23, 2026
The stability clock in detectSubagentTransitions only advances when the scan runs. PR #153 gave it a 5 s settle tick so a silent folder can no longer stall it; the remaining lateness is up to one tick plus the rest of the stability window. The Claude CLI already publishes a per-session state file at ~/.claude/sessions/<pid>.json whose status flips to idle when a turn ends. Watching that directory gives an earlier, more precise trigger for the same scan, at no cost while nothing changes state. The idle edge is a trigger, never a verdict: nothing here marks a subagent complete or emits subagent-completed, and every guard failure degrades to doing nothing so the settle tick stays the safety net. The file is not a documented interface, so a canary test pins its shape and skips itself where the CLI is absent.
devsuitup
added a commit
that referenced
this pull request
Aug 23, 2026
The stability clock in detectSubagentTransitions only advances when the scan runs. PR #153 gave it a 5 s settle tick so a silent folder can no longer stall it; the remaining lateness is up to one tick plus the rest of the stability window. The Claude CLI already publishes a per-session state file at ~/.claude/sessions/<pid>.json whose status flips to idle when a turn ends. Watching that directory gives an earlier, more precise trigger for the same scan, at no cost while nothing changes state. The idle edge is a trigger, never a verdict: nothing here marks a subagent complete or emits subagent-completed, and every guard failure degrades to doing nothing so the settle tick stays the safety net. The file is not a documented interface, so a canary test pins its shape and skips itself where the CLI is absent.
devsuitup
added a commit
that referenced
this pull request
Aug 23, 2026
The stability clock in detectSubagentTransitions only advances when the scan runs. PR #153 gave it a 5 s settle tick so a silent folder can no longer stall it; the remaining lateness is up to one tick plus the rest of the stability window. The Claude CLI already publishes a per-session state file at ~/.claude/sessions/<pid>.json whose status flips to idle when a turn ends. Watching that directory gives an earlier, more precise trigger for the same scan, at no cost while nothing changes state. The idle edge is a trigger, never a verdict: nothing here marks a subagent complete or emits subagent-completed, and every guard failure degrades to doing nothing so the settle tick stays the safety net. The file is not a documented interface, so a canary test pins its shape and skips itself where the CLI is absent.
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.
detectSubagentTransitions()owns spawn, heartbeat and completion, but it only ran from the projects watcher's debounced flush. A function that runs when a file changes cannot see that a file stopped changing.Measured in the app's own
main.log, both failure directions:Too late. Agent
a9be19fb0a7e0e504last wrote at 23:42:18; its completion was logged at 23:52:55 — 10 min 37 s for a 30 s stability window. The parent was idle (Claude is waiting for your inputat 23:43:35), so nothing wrote in the folder and no flush ran. The event only fired when activity resumed, which is also what a user sees: the indicator clears the moment you start typing again, not when the agent finished.Too early, and permanently. Three agents were declared complete while still working:
A tool call longer than
STABLE_MSis indistinguishable from a finished agent — that limitation was known and documented. What was not: the verdict was irreversible. The completion branch setcompleted = truewith no recheck window, so the entry hit thecompletedfast path forever and the agent could never be announced again. That is why a session with several live subagents ends up showing no running indicator at all.This is also why the tick alone would have made things worse: giving the stability window a clock makes false completions more frequent, not less. The two had to be fixed together.
Fix
SETTLE_TICK_MS = 5 s) that only arms while an entry is uncompleted, re-arms from its own sweep, stops on its own, and isunref()d. No cost when no subagent is running.LIVE_RECHECK_MS), so renewed growth rehabilitates the entry and re-emits the spawn — the same machinery already used for an assumed-finished file, now applied to this path too.pruneStaleGridSubagents()ran only fromwrapInGridCard()andpruneStaleSubagents()only fromrenderProjects(), so a stale entry survived exactly in the idle case the TTL exists to cover. Each view now arms a one-shot timer at the oldest entry's deadline (not an interval), refreshes only the entries the prune actually removed, and stops when its map is empty — ADR 0002, no steady-state cost. These are a backstop; with the main-process signal repaired they should almost never fire.Not changed
The watcher debounce has no
maxWait. Starvation under sustained write load was the working hypothesis and it is refuted: the log shows completions emitted while three transcripts were being written. The real starvation is silence, not noise.HEARTBEAT_MSand the renderer TTL are untouched — widening them would only make the underlying defect rarer and harder to find.Tests
test/subagent-settle-tick.test.jsandtest/dom-subagent-ttl-tick.test.js. Mock timers on both sides — no wall-clock waits, perdocs/activity-trace.md. Every assertion checked by reverting the fix:691 tests, 684 pass, 0 fail, 7 pre-existing skips. ESLint: 0 errors, 265 warnings before and after.
Reasoning and measurements in
.ai/contexts/subagent-observability.md.