fix(claude): resumed subagents no longer read as stopped - #9408
Conversation
Resuming a settled subagent with SendMessage makes the Claude CLI emit a fresh task_started for the same task id under the SendMessage tool use. The adapter passed that through with no status, and the client fold treats a start row after a terminal state as a late delivery that only fills metadata, so the row stayed Stopped with the stale sweep message and a frozen timer while the agent worked on and its token count climbed. The adapter now tracks which task ids settled in this process and marks a re-registration by a different tool use as running on the wire; task.started carries an optional status for that. The fold applies an explicit start status through the normal transition, so the run count bumps and the prior run's terminal detail clears. A start carrying the spawn's own tool use is still a duplicate and still only fills metadata.
ApprovabilityVerdict: Would Approve Macroscope's review found this PR approvable — This is a narrow, backward-compatible fix for resumed Claude subagents: only genuine re-dispatches are marked running, while existing task-start behavior remains unchanged and is covered by tests. The session-level settled-task set is currently unbounded, so long-lived sessions may retain task IDs and should address that memory-growth risk. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
Dismissing prior approval to re-evaluate d71ab9c
| patch.status !== undefined ? CLAUDE_TASK_PATCH_STATUS[patch.status] : undefined; | ||
| if (status === "completed" || status === "failed" || status === "cancelled") { | ||
| context.liveTaskIds.delete(message.task_id); | ||
| context.settledTaskIds.add(message.task_id); |
There was a problem hiding this comment.
🟡 Medium Layers/ClaudeAdapter.ts:3581
settledTaskIds retains every terminal task ID that is never re-dispatched, so a long-lived session accumulates task IDs without bound and grows memory indefinitely. Bound the set or prune entries after they can no longer participate in reactivation, applying the same policy to both task_updated and task_notification.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ClaudeAdapter.ts around line 3581:
`settledTaskIds` retains every terminal task ID that is never re-dispatched, so a long-lived session accumulates task IDs without bound and grows memory indefinitely. Bound the set or prune entries after they can no longer participate in reactivation, applying the same policy to both `task_updated` and `task_notification`.
What Changed
A subagent resumed with
SendMessagenow reopens its Agents panel row instead of staying settled.ClaudeAdaptertracks the task ids that settled in this process, including ones the CLI's resume sweep settled before anything here registered them, and marks atask_startedthat re-registers one under a different tool use asstatus: "running". A start carrying the spawn's own tool use is a late or duplicate delivery and leaves the marker in place, so a real re-dispatch after it is still recognized.task.startedgains an optionalstatus.foldSubagentActivitiesapplies an explicit start status through the normal transition: the run count bumps, the prior run's terminal detail clears, and the next terminal row settles the new run instead of staying pinned to the stale stop. A start row without a status keeps its existing metadata-only behavior.45 lines of implementation across three files.
Why
Resuming a settled subagent leaves its row reading Stopped while the agent works on. The row shows the CLI's "No completion record was found" sweep message instead of its live activity, its timer stays frozen at the stale stop, and it counts as settled. Its token counter keeps climbing the whole time, from the same
task.progressrows the fold declines to act on, so the row contradicts itself.On resume the CLI emits a fresh
task_startedfor the same task id, attributed to theSendMessagetool use. The adapter passed it through with no status. The fold deliberately treats a start row arriving after a terminal state as a late or out-of-order delivery and only fills metadata, so nothing reopened the run.task.progresscannot reopen a terminal row either, butmergeUsageMaxstill applies its usage, which is where the climbing counter comes from.Stamping the transition on the wire keeps the decision where the evidence is. The adapter is the only place that knows whether a
task_startedre-registers a settled task or merely repeats its spawn, and the fold's existing guard against out-of-order delivery stays intact for every start row that does not say otherwise.Reproduced by spawning a background subagent,
SIGKILLing the server mid-run, restarting it, and resuming the agent withSendMessage.UI Changes
Before
After
The model chip still shows the thread's model rather than the subagent's. That is #7281 and out of scope here.
Related
Adjacent and non-conflicting. This is the only change that touches the fold's start-after-terminal guard.
Verification
vp test run apps/server/src/provider/Layers/ClaudeAdapter.test.ts(79 passed)vp test run packages/client-runtime/src/state/subagentRuntime.test.ts(50 passed)vp test run packages/contracts/src/providerRuntime.test.ts(10 passed)vp run --filter t3 typecheck,--filter @t3tools/contracts,--filter @t3tools/client-runtimeChecklist
Note
Medium Risk
Changes subagent/task lifecycle semantics on the wire and in the activity fold; behavior is narrow and covered by tests but affects how clients interpret
task.startedafter terminal states.Overview
Resuming a settled Claude subagent via
SendMessageno longer leaves the Agents row stuck on Stopped while progress keeps updating.ClaudeAdaptertracks per-session settled task ids (from terminaltask_updated/task_notification). When a newtask_startedreuses a settled id under a differenttool_use_id, it emitstask.startedwithstatus: "running"; a duplicate start under the original spawn tool use stays unstamped so a later real re-dispatch is still recognized.TaskStartedPayloadgains an optionalstatus, andfoldSubagentActivitiesapplies an explicit status on start rows as a real reactivation (new run, cleared terminal detail) instead of metadata-only late delivery.New adapter and fold tests cover re-dispatch vs duplicate and settle-after-resume.
Reviewed by Cursor Bugbot for commit 58970ab. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix resumed subagents reading as stopped by stamping
task.startedwithrunningstatusstatusfield toTaskStartedPayloadin providerRuntime.ts so runtime validation accepts a status ontask.startedpayloadsClaudeSessionContext; when a settled task is re-registered under a different tool use,ClaudeAdapteremits arunningstatus on thetask.startedevent and clears the settled marker. A duplicate of the original tool use is left unstampedfoldSubagentActivitiesin subagentRuntime.ts to apply an explicit status carried bytask.startedrows, so arunningstart reopens a previously terminal agent as a new activation instead of being treated as late metadatafoldSubagentActivitiesnow trusts thestatusfield ontask.startedrows; any provider emitting a spuriousrunningstatus on a stale start event would cause an unexpected reactivation📊 Macroscope summarized d71ab9c. 3 files reviewed, 1 issue evaluated, 0 issues filtered, 1 comment posted
🗂️ Filtered Issues