Skip to content

fix(claude): resumed subagents no longer read as stopped - #9408

Open
murayy wants to merge 3 commits into
pingdotgg:mainfrom
murayy:fix/agents-panel-resume-reopen
Open

fix(claude): resumed subagents no longer read as stopped#9408
murayy wants to merge 3 commits into
pingdotgg:mainfrom
murayy:fix/agents-panel-resume-reopen

Conversation

@murayy

@murayy murayy commented Sep 3, 2026

Copy link
Copy Markdown

What Changed

A subagent resumed with SendMessage now reopens its Agents panel row instead of staying settled.

  • ClaudeAdapter tracks the task ids that settled in this process, including ones the CLI's resume sweep settled before anything here registered them, and marks a task_started that re-registers one under a different tool use as status: "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.started gains an optional status.
  • foldSubagentActivities applies 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.progress rows the fold declines to act on, so the row contradicts itself.

On resume the CLI emits a fresh task_started for the same task id, attributed to the SendMessage tool 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.progress cannot reopen a terminal row either, but mergeUsageMax still 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_started re-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 with SendMessage.

UI Changes

Before

Before: the row reads Stopped with the sweep message and a frozen timer while the agent runs

After

After: the row reopens as run 2, reports Working, and its timer restarts

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-runtime
  • Targeted lint and format on the changed files
  • Two new tests: the adapter stamping a real re-dispatch but not a duplicate, and the fold reopening a settled row and settling the new run on its own terminal row

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • A video is not applicable because no animation or interaction changed

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.started after terminal states.

Overview
Resuming a settled Claude subagent via SendMessage no longer leaves the Agents row stuck on Stopped while progress keeps updating.

ClaudeAdapter tracks per-session settled task ids (from terminal task_updated / task_notification). When a new task_started reuses a settled id under a different tool_use_id, it emits task.started with status: "running"; a duplicate start under the original spawn tool use stays unstamped so a later real re-dispatch is still recognized.

TaskStartedPayload gains an optional status, and foldSubagentActivities applies 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.started with running status

  • Adds an optional status field to TaskStartedPayload in providerRuntime.ts so runtime validation accepts a status on task.started payloads
  • Tracks settled task IDs in ClaudeSessionContext; when a settled task is re-registered under a different tool use, ClaudeAdapter emits a running status on the task.started event and clears the settled marker. A duplicate of the original tool use is left unstamped
  • Updates foldSubagentActivities in subagentRuntime.ts to apply an explicit status carried by task.started rows, so a running start reopens a previously terminal agent as a new activation instead of being treated as late metadata
  • Risk: foldSubagentActivities now trusts the status field on task.started rows; any provider emitting a spurious running status 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

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.
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Sep 3, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Sep 3, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@macroscopeapp
macroscopeapp Bot dismissed their stale review September 6, 2026 09:46

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant