Repository navigation
fix(server): background subagents keep T3 tools after the parent turn ends - #19751
maria-rcks wants to merge 6 commits into
Conversation
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR not approvable — This PR changes the MCP credential-liveness and authorization boundary, allowing background subagents to continue performing actions after the parent run ends, with supporting changes to SQL/in-memory projections and contracts. The access-control impact and broader runtime authority require human review. You can add or adjust custom eligibility rules. Learn more. |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: 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. |
Replaces #19237, moved from a fork head to this repo.
Problem
A Claude background subagent (Agent tool with
run_in_background: true) keeps running after the parent turn ends, and it calls T3 tools with the parent thread's MCP credential. Once the parent run settles,assertLiveCallerseesactiveRunId === nulland every tool that acts as the caller (html_render,preview_open,preview_click,t3_thread_send, ...) fails withparent_not_active: The calling provider no longer owns an active thread run.Agents then fall back to other browsers or hold the parent turn open with blocking waits.Seen on
0.0.46-nightly.20261013.3042: 10 preview tool failures from 7 Claude background subagents, and onet3_thread_sendfrom a Codex native subagent 6 s after its parent run completed. Each came after the parent run had completed and before the next run started.Related but separate: #17096 (credential slot left on another provider instance) and #17885 (wake turns stuck behind a held queue).
Change
A thread caller is also live while its shell lists a running background subagent owned by the caller's provider instance (
pendingBackgroundTaskswithkind: "subagent"and a matchingproviderInstanceId). Ownership comes from the item's provider thread, including child provider threads, so background work from an earlier provider cannot keep a new provider's credential live. This list is the one that drives the sidebar's Waiting state, and it is already gated on the latest run having settled. The same check backs the cross-thread/cross-project check inOrchestratorMcpService. When the projection has no active run, it decides from one fresh shell snapshot, accepting a matching run that started in between.Everything else still applies: archived threads, a different provider instance, and credentials revoked when the provider process goes away are all refused. After Stop ends the background work, the thread has no running subagent, so calls are refused again.
delegate_task,create_threadsandrequest_secretstill need an active run, because they attach to its node.Verification
Same prompt on a dev server before and after: a Claude parent starts a background subagent, then ends its turn. The subagent runs
ping -c 30and then callshtml_render.html_renderfails withparent_not_active33 s after the parent run completed.html_renderreturnshtmlRender33 s after the parent run completed, with no run active.McpToolAccess.test.tsadds rows to the access table. A caller whose run ended but whose subagent still runs can act and write (ran). It is refused (parent_not_active) when the subagent belongs to an earlier provider, when the caller's provider instance differs, when the thread is archived, and when the subagent has no known owner. The existing "ended" rows still returnparent_not_active.Not covered: a subagent that finished and is then resumed by its own background task. T3 marks it completed and does not see it running again until the next wake run starts, so its calls in that gap still fail. That fix belongs in the Claude adapter's wake handling.
Re-verified on
30869a2A server built from
30869a2, with Claude Sonnet 5.5 and Codex GPT-6-Astra. Times are UTC.html_renderran at 04:05:24.219 and returnedhtmlRender. The wake run was requested at 04:05:29.008, so no run was active during the call.ping -c 30failed at once in this sandbox (nocap_net_raw), so the gap is 6 s instead of 30 s.t3_thread_listat 04:11:58.461, after asleep 40, and got the thread list. No run was active.sleepran. When the sleep finished it resumed and calledhtml_renderbetween 04:12:25 and 04:12:32, and gotparent_not_active. This is the "Not covered" case below. Pressing Stop on a running subagent ends it, so no call follows the Stop.Written by Claude Opus 5.5 (
claude-opus-5-5) in Claude Code.