Repository navigation
feat(server): background wakes say which subagent, command, or monitor finished - #13948
Conversation
…r finished A background wake used to land as "Background activity updated" for every provider, and the Waiting roster only carried a free-form taskType. Adapters now name what they saw end: - Claude records each task_notification with the roster description or the subagent's title and child thread, and names them on the next wake. - Grok (ACP) reads kind, description, command and exit code from task_completed, and the title and child thread from subagent_finished. - Codex background commands use the same wording, with the exit code. - Delegated tasks name the task and say "2 of 3" when only some finished. Contracts gain optional fields only: `kind` and `childThreadId` on pending background tasks, `workKind` and `childThreadId` on notifications. No new source literal, so older clients keep decoding. The text sent to the provider is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a substantial cross-provider feature that changes wake scheduling state, persisted notification contracts, background-work classification, and user-visible timeline behavior. The unresolved high-severity compatibility concern around notification wire formats further warrants human verification. 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. |
…ing-matched task types Notifications and pending background tasks carry their kind as a discriminated union: subagent (with its child thread), command, monitor, delegated_task, and a generic background_task. Adapters build the member from structured data they already have, so shared code no longer guesses a kind from provider taskType strings. A decode-only fallback arm maps kinds a build does not know, and rows stored before kinds existed, to background_task (a stored background_command decodes as command). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| // One subagent is the thing the row opens; several open nothing. | ||
| return rest.length === 0 && first.childThreadId !== undefined | ||
| ? { kind: "subagent", childThreadId: first.childThreadId } | ||
| : { kind: "subagent" }; |
There was a problem hiding this comment.
🟠 High orchestration-v2/Notification.ts:121
reportsSource emits source.kind: "command" and source.kind: "subagent", which pre-change clients cannot decode; those clients reject the entire timeline item/projection instead of ignoring the new metadata. Preserve the legacy background_command and delegated_task wire literals, adding optional work details such as childThreadId rather than introducing incompatible source kinds.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Notification.ts around line 121:
`reportsSource` emits `source.kind: "command"` and `source.kind: "subagent"`, which pre-change clients cannot decode; those clients reject the entire timeline item/projection instead of ignoring the new metadata. Preserve the legacy `background_command` and `delegated_task` wire literals, adding optional work details such as `childThreadId` rather than introducing incompatible source kinds.
…ications Clients from before specific background work kinds reject a notification source kind they do not know, which fails the whole thread load. Store and send commands as `background_command` and subagents as `background_task` with `work: "subagent"`, and decode those back into the typed `command` and `subagent` members, so current clients still switch on `kind`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…was queued A continuation turn cleared every wake report when it started, including ones recorded after its offer went out. Its sticky offer blocked another offer until then, so the next wake no longer named that work. The offer now remembers which reports it named, and its turn drops only those. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Recording a wake report read the user turn count and wrote the report in separate Ref operations, so a user turn starting in between stamped the report with the previous turn and it expired one turn early. The turn count and the reports now live in one Ref and change in one update. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The set of backgrounded subagent task ids only grew. A task id stayed in it after its subagent ended, so a later foreground re-run of that subagent was named in the next unrelated wake. Its terminal notification now removes it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eports The user turn count that ages wake reports went up before the prompt was built, so a turn that failed on a missing attachment still used up a queued wake's grace turn. It now goes up when the prompt is handed to Claude. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It string-matched provider taskType values, which this change removes from the pending-task shape. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a background task, monitor or subagent finished after a turn, every provider's wake landed in the timeline as "Background activity updated". The Waiting roster only carried a free-form
taskType, so clients could not tell a subagent from a shell command either.What changed
Adapters now name what they saw end, through one summary builder in
Notification.ts. Thesourcecolumn is the decoded member clients switch on. See "Older and newer data still decode" for what goes over the wire:sourceAgent(subagent)Subagent "Agent B" was stopped{ kind: "subagent", childThreadId }Bash(local_bash)Command "Background sleep test" finished{ kind: "command" }Subagent "Agent B" and command "Sleep 60 seconds then echo B_DONE" were stopped{ kind: "background_task" }(mixed kinds)spawn_subagent(background)Subagent "Sleep then reply done" finished{ kind: "subagent", childThreadId }Monitor "Watch three tick echoes" finished{ kind: "monitor" }Command "Run three tock echoes in the background" finished (exit 0){ kind: "command" }Command "sleep 20 && echo …" finished (exit 0)(was "Background command finished"){ kind: "command" }Delegated task "Review src/math.ts" finished, or2 of 3 delegated tasks finished: A, B{ kind: "delegated_task", taskIds, childThreadId? }The kind is part of the type
Both unions in
packages/contracts/src/orchestrationV2.tsare discriminated onkind, and each member carries only its own fields:OrchestrationV2NotificationSource:delegated_task { taskIds, childThreadId? },subagent { childThreadId? },command,monitor, and a genericbackground_taskfor mixed or unnamed work.childThreadIdis set only when the notification reports one subagent or one delegated task.OrchestrationV2PendingBackgroundTask:subagent { taskId, description?, childThreadId? },command,monitorandbackground_task, each with{ taskId, description? }.taskTypeis gone.Adapters build the member directly from data they already have. Provider strings are interpreted only inside each adapter:
CLAUDE_OPAQUE_BACKGROUND_TASK_KINDSmaps SDKtask_typeto a roster kind (local_bashbecomescommand) at the place that already filtered on it.task_notificationcarries only the task id and status, and the wake'sresultcarries no task id at all, so the adapter records a report per notification (the roster entry's kind and description, or the registered subagent's title and child thread) and names them on the next wake offer. Reports survive one user turn, so a wake that runs after a queued prompt is still named. They are dropped when a user turn runs the wake itself. An emptybackground_tasks_changedlevel can arrive before the notification, so the last roster entry per task is kept for naming (bounded).task_completed.task_snapshot.kind(bash/monitor,TaskSnapshotincrates/common/xai-tool-runtime/src/notification.rsat f0e3be1), withdescription,command,display_commandandexit_code, becomes acommandormonitorreport.subagent_finishedgives the child session, which maps to the tracked subagent's title and child thread. Reports are recorded only after the prompt settled, and cleared with the wake buffer.commandExecutionitem is acommand.derivePendingBackgroundWork): the item type picks the member (subagent,command_executionbecomescommand, anything elsebackground_task). The providertaskTypestring sets that the shared module used to match on are deleted.Older and newer data still decode
Clients from before this PR. Their notification source union is strict and predates the #13941 fallback, so a
source.kindthey don't know fails the whole timeline item. Since notifications are persisted, that would break loading every thread holding one, including messages stored before an upgrade. Sosubagentandcommandnever go over the wire or into storage:{ kind: "background_command" }{ kind: "background_task", work: "subagent", childThreadId? }. Old clients dropworkandchildThreadIdand see the generic member they already render.delegated_task,monitorandbackground_taskare unchanged.childThreadIdondelegated_taskis a new optional field that old clients strip.The schema does the mapping, not the adapters. The union lists the wire forms first as
decodeTomembers:background_commanddecodes tocommand, andbackground_taskwithwork: "subagent"decodes tosubagent. Because a union encodes with the first member that fits, a server value encodes back to the legacy form. The plainsubagentandcommandmembers come after them, so a value that was already decoded once still decodes. New clients keep a typedkindand switch on it exhaustively, with no string matching. I picked this over "legacy kind plus a side field that new clients read" because it keeps adapters and clients on the typed union, and the compat shape stays inside one schema.Roster entries (
OrchestrationV2PendingBackgroundTask) need nothing extra. Old clients decode them as{ taskId, description?, taskType? }and ignore the newkind, and every member still carriestaskId.Newer servers and old rows. A small
kindUnionWithFallbackhelper in contracts adds a decode-only arm after the known members, following #13941. An object whose encodedkindthis build does not know, or that has none, decodes to a known member instead of failing. A known kind whose fields do not decode still fails. Encoding the arm is forbidden, and the server never builds it. Both unions use it. So:{ kind: "background_task", nativeRef }and{ kind: "monitor" }decode as themselves (the unusednativeRefis dropped), and{ kind: "background_command" }decodes ascommand. Rosters persisted withtaskTypeand nokinddecode asbackground_task, so a Claude background Bash from before this change reads "background task" rather than "command" until it ends.The provider-facing continuation text is unchanged.
The client changes that show these (composer strip grouping, row icons, opening the child thread) follow in #13949.
Wake report bookkeeping
Verification
OrchestratorReplayFixtures.integration.test.tsand.contract.test.ts: 98 passed.ClaudeReplayFixtures.integration.test.tspasses. The assertions pin summary, outcome and the decodedsourcemember ingrok_background_subagent,grok_monitor,grok_background_bash,claude_background_subagent_after_root,claude_background_subagent_lifecycle(three wakes: A finished, B stopped, A finished) andclaude_background_task_wake, which also pins the roster entry{ taskId, description, kind: "command" }built from Claude'stask_type.claude_background_wake_before_queued_promptpins the two-task stop wake asbackground_task.packages/contractsorchestrationV2.test.ts: 31 passed. New wire-compat test: the pre-feat(server): background wakes say which subagent, command, or monitor finished #13948 notification schema is copied in as a fixture. Every source the server builds (subagentwith and without a child thread,command,monitor,background_task,delegated_taskwith a child thread) is encoded through both the storage codec and the RPC JSON codec. The old schema decodes each result, and the current schema decodes it back to the same typed member. The roster test checks that the old{ taskId, description?, taskType? }struct reads every encoded entry. Older tests still cover stored sources, unknown kinds from a newer server, and known kinds with bad fields failing.apps/server:AcpAdapterV2.test.tsandGrokAdapterV2.test.ts: 132 passed.ClaudeAdapterV2.test.ts: 123 passed.Notification.test.ts,FoundationPersistence.test.ts,ProjectionRecovery.test.ts,ProjectionSettlement.test.ts,SubagentProjection.test.ts,CodexAdapterV2.test.ts: 177 passed. Each bookkeeping fix except the Claude single-Ref change has a test that fails with the fix reverted:a wake names work that ended while the previous wake was queued(ACP),a subagent re-run in the foreground does not join a later wakeanda turn that fails to start does not expire a queued wake's report(Claude).tsc --noEmitpasses for contracts, shared, server, client-runtime, web and mobile.vp linton touched files: no errors.vp run knipshows nothing in touched files.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code