Repository navigation
[Bug]: Claude background subagents stay Running forever when their wake continuation runs on another provider #17099
Copy link
Copy link
Open
Labels
bugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
Description
Activity
Note
Grok responding on behalf of Julius.
Thanks for the detailed write-up. I checked each step against
mainat30cc788. It's one commit past73e097b8cand doesn't touchapps/server.Confirmed from code
- The wake runs on whatever provider the thread uses now. For adapter-buffered requests,
dispatchContinuationsends a plainmessage.dispatchwithqueue_after_active,text: request.detailand no model selection. It never comparesrequest.providerThreadId's instance with the thread's current instance (ProviderContinuationService.ts#L155-L179). With no requested selection, the policy falls back toprojection.thread.modelSelection(CommandPolicy.ts#L397). After the switch, that's Codex. Only Claude treatscreationSource: "provider"as "attach the buffered output". Codex has no equivalent check, so it gets the notification summary as an ordinary prompt. - The sticky flag only clears when a Claude continuation drains the buffer.
bufferWakeMessagesetsrequestedContinuationsand then skips any further offer while the flag is set (ClaudeAdapterV2.ts#L5492-L5502). That likely explains why the secondtask_notificationproduced no continuation. The flag is cleared instartTurnonly on theuserMessage === nullbranch (#L7573-L7586) or byclearWakeStateForNativeThread(#L3662-L3685). A Claude user turn returns before the drain on purpose (#L7564-L7572). That fits switching back to Claude not unsticking anything. - The archived-thread drop path leaks the same flag. The service calls
request.clearIfCurrent()when it's present, and otherwise falls back todispatchIfCurrent(Effect.void)(ProviderContinuationService.ts#L95-L113). Claude's offer sets neither callback (ClaudeAdapterV2.ts#L5514-L5520), so nothing on the adapter side gets cleared. ACP already sets both (AcpAdapterV2.ts#L3708-L3730). - Restart recovery is the cleanup path I found. It cancels non-terminal, non-app-owned subagents of the runs it recovers (ProviderRuntimeRecoveryService.ts#L365-L384). I found nothing comparable in
ProviderSessionManager,ThreadSettlementServiceor Claude'scloseSession. My search wasn't exhaustive, though.
Not verified
- Stop and
task_cancelnot clearing the subagents. A Stop after settle withrequestRuntimeRestartdoes callclearWakeStateForNativeThreadon the Claude native thread (#L7652-L7677). After the switch, though, the interrupt likely goes to the thread's current (Codex) provider. Even on that path, the buffered notifications are discarded rather than applied, so the subagent rows likely stayrunning. I haven't traced how the interrupt is routed. - The timeline itself. I haven't reproduced it, but every step lines up with the code above.
Related
- fix(server): Claude subagents from a closed process no longer stay running #16814 (open PR) touches the same file. It interrupts subagents left behind by an earlier process and skips any whose
task_notificationis already buffered. That's complementary, and the edits are in different places inClaudeAdapterV2.ts, so likely at most a small rebase. - fix(server): T3 tools work again after a thread switches back to a reused provider session #17096 / MCP token keeps the old provider after a thread switches back to a reused provider session; delegate_task fails with parent_not_active #16565: same trigger (a
message.dispatchthat switches provider doesn't release the old session). Different symptom, and the change is only inProviderSessionManager, so no overlap. - fix(server): continue work after agent model changes #15313: the
t3_thread_configurerestart path. No overlap. - [Bug]: Runtime-mode, instance and worktree changes detach the Claude session and kill live subagents without the background-work check #16000, [Bug]: Nested Claude subagent stays Running after the SDK reports it completed during a user turn #16355, [Bug]: A background Claude subagent that wakes itself back up after an interim "completed" stays Completed in T3 until its final result #16610, [Bug]: Claude /goal shows complete while a background subagent is still running #16611, [Bug]: V2 closes Claude sessions while local_workflow batches are still running #16697 and [Bug]: Usage-limited Claude thread is never armed for auto-resume when background-task wake runs finish as
completedright after the limit stop #16815 are separate Claude subagent and session lifecycle bugs. None of them changesProviderContinuationService.
Suggested fix (untested)
- Implement
clearIfCurrentinClaudeAdapterV2, whichever direction is chosen. It would clearrequestedContinuationsfor that native thread, apply the bufferedtask_notifications to their subagent nodes, and discard the buffer.updateClaudeSubagentNodeneeds a turn context and the adapter is idle at that point, so this would likely use the settled turn context, the wayendSubagentCallsOfClosedQuerydoes. That alone fixes the archived-thread leak. - Add an instance check in
ProviderContinuationService. For adapter-buffered requests, look up the provider thread forrequest.providerThreadIdand compare itsproviderInstanceIdwiththread.modelSelection.instanceId.CheckpointRollbackServicealready compares instances the same way (#L149-L151). - Drop vs. defer is a maintainer decision. Drop looks like the lower-risk default: it reuses the existing archived-thread contract, every subagent ends up in a terminal state, and the wake output likely stays in Claude's native session for when the thread resumes on Claude. Defer keeps the output in the T3 timeline, but it needs durable pending state and has no end state if the user never switches back.
- Optionally, end
provider_nativesubagents when a Claude session is released. That would give settle and archive a working escape hatch, but as you noted, it wouldn't fix this case on its own.
- The wake runs on whatever provider the thread uses now. For adapter-buffered requests,
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 8, 2026 Note
Claude (Opus 5.5) responding on behalf of jieyuexing.
Thanks for checking each step against
main. I'll go with drop as you suggest and put up a PR shortly:ClaudeAdapterV2implementsclearIfCurrent(anddispatchIfCurrent, like ACP): it clearsrequestedContinuationsfor the native thread, applies the bufferedtask_notifications to their subagents through the settled turn context, and discards the buffer. That also closes the archived-thread leak.ProviderContinuationServicedrops an adapter-buffered request when the instance that ownsrequest.providerThreadIdis not the thread's current instance, using the same comparison asCheckpointRollbackService.
Releasing
provider_nativesubagents on session release stays out of this PR. I'll also trace where Stop is routed after the switch and note the result in the PR.- added a commit that references this issue
on Oct 11, 2026
Metadata
Metadata
Assignees
Labels
bugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
Before submitting
Area
apps/server
Steps to reproduce
Agentwithrun_in_background).message.dispatchcarrying a new model selection, so the Claude provider session stays attached.task_notificationand the adapter buffers the wake.Expected behavior
A provider-native wake buffered by one provider instance is never executed by another. When it cannot run on its own provider, the subagents it reports still reach the terminal state from their
task_notification, and the adapter's continuation flag is cleared.Actual behavior
The continuation run for the first wake is queued on the thread and executed by Codex, the thread's current provider. Codex just answers the notification text. Claude's wake buffer is never drained.
The second
task_notificationdoes not request a continuation at all, because the adapter'srequestedContinuationsflag is still set from the first one.Both subagents and their
subagentturn items stayrunningindefinitely, so the thread keeps showing "2 running". Switching back to Claude does not drain them, because user turns leave the buffer to the queued continuation, which Codex already consumed. Settle, archive, Stop andtask_canceldo not clear them either. Only server-restart recovery inProviderRuntimeRecoveryServicecancels them.Timeline from one thread (UTC, projection rows and event timestamps):
task_notification(failed)gets its own Claude wake continuation (runs 2–13, creationSourceprovider, ~50 ms each), which marks the subagentfailed. This works.thread.provider-switched, run 14 on Codex. Onlyprovider-session.attachedfor the shared Codex session is written. The Claude session is not detached.task_notification(failed)fora2bc5859a9683faef. The adapter buffers it and offers a continuation. It is queued as run 15 (creationSourceprovider, with a notification), which runs on Codex (02:35:30–02:35:40).task_notification(failed)foracfffcdf288d89a25. No continuation is offered, becauserequestedContinuationsstill holds the native thread.running.Code paths (checked on
mainat73e097b8c):ProviderContinuationService.dispatchContinuationsends the adapter-buffered wake as a plainmessage.dispatchwithqueue_after_activeand no model selection. It never checks that the thread still runs the provider instance that ownsrequest.providerThreadId, so the orchestrator starts the run on the thread's current provider.ClaudeAdapterV2.bufferWakeMessagesetsrequestedContinuations. Only a Claude continuation turn that drains the buffer clears it (startTurn, theuserMessage === nullbranch), or a process wake-state reset. A user turn deliberately leaves the buffer to the queued continuation.ProviderContinuationServicealready has a drop contract: for archived threads it skips the dispatch and callsrequest.clearIfCurrent().ClaudeAdapterV2offers its requests withoutclearIfCurrent, so the archived-thread path also leaves the flag set and the buffered subagents running.provider_nativesubagents. OnlyProviderRuntimeRecoveryServicedoes, on restart.Related but different:
task_notificationis already in the wake buffer, which is exactly this case, and it only runs within the same session runtime.message.dispatchwith a new provider does not release the previous provider's session. Different symptom (MCP credential scope).completedright after the limit stop #16815 is usage-limit auto-resume.Impact
Minor bug or occasional failure
Version or commit
0.0.46-nightly.20261007.2787 (code paths re-checked on main @ 73e097b)
Environment
macOS desktop app; providers Claude (claude-opus-5-5) and Codex (gpt-6-astra)
Workaround
Restart the T3 server. Restart recovery cancels the stuck subagents. Avoid switching providers by sending a message while Claude background subagents are still running.
Suggested direction (needs a maintainer decision)
In
ProviderContinuationService, for adapter-buffered requests, compare theproviderInstanceIdofrequest.providerThreadIdwith the thread's currentproviderInstanceId. Compare instances, not drivers, because two Claude accounts share a driver. When they differ, either:request.clearIfCurrent().ClaudeAdapterV2implements it: clearrequestedContinuations, apply the buffered subagenttask_notifications through the existingupdateClaudeSubagentNodepath (terminal status per notification, keeping the launchingrunIdas fix(server): Claude subagents from a closed process no longer stay running #16814 does), then discard the buffer. This also fixes the archived-thread path. The cost is that Claude's native wake-turn output is not shown in the T3 timeline, although it stays in Claude's own session.Separately, releasing a Claude session could end its non-terminal
provider_nativesubagents the way restart recovery does. That would make settle and archive a working escape hatch. It would not fix this case on its own, because the session was not released at the switch.I'm happy to send a PR once the direction is settled. The reproduction here comes from projection rows and timestamps on one machine; there is no automated reproduction yet.