Repository navigation
fix(server): background subagents stop showing as running after their Claude session is released - #27487
fix(server): background subagents stop showing as running after their Claude session is released#27487Pyseph wants to merge 2 commits into
Conversation
… Claude session is released
| ); | ||
| } | ||
| if (events.length > 0) { | ||
| yield* eventSink.write({ events }); |
There was a problem hiding this comment.
🟠 High orchestration-v2/ProviderSessionManager.ts:920
A replacement can resume a subagent after this snapshot is read, and this unconditional eventSink.write then overwrites the resumed subagent as interrupted and can end its child root turn. The startedAt check occurs before the child projection reads, so it does not protect the commit; check the captured start in the commit transaction and guard the associated child updates too.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/ProviderSessionManager.ts around line 920:
A replacement can resume a subagent after this snapshot is read, and this unconditional `eventSink.write` then overwrites the resumed subagent as `interrupted` and can end its child root turn. The `startedAt` check occurs before the child projection reads, so it does not protect the commit; check the captured start in the commit transaction and guard the associated child updates too.
There was a problem hiding this comment.
Fixed in cabc7ae. Each released subagent and its child turns now commit together through a new EventSink.writeIfSubagentActive, which re-reads the subagent's status and started_at inside the commit transaction and skips the write when it is no longer active with the start the release read. The new test "keeps a native subagent resumed after release read it, until the commit" resumes the subagent between the read and the commit; it fails with the transaction check removed and passes with it.
There was a problem hiding this comment.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — The change clears stale running states when a session's process ends. The unresolved concurrency finding concerns cleanup overwriting resumed subagents and ending their child turns; commit-time protection needs resolution. Needs human approval 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. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Guard writes against inactive subagents apps/server/src/orchestration-v2/EventSink.ts, apps/server/src/orchestration-v2/ProjectionStore.ts |
The event sink adds writeIfSubagentActive, which commits events only when the expected subagent still exists, has the same start time, and remains active. The projection store adds encodeSubagentStartedAt to obtain the encoded start time. |
Recover and record released subagents apps/server/src/orchestration-v2/ProviderSessionManager.ts |
Release processing selects threads losing background work, rereads subagent projections, and records interruption events for matching subagents and eligible runless child root turns. |
Test release behavior and races apps/server/src/orchestration-v2/ProviderSessionManager.test.ts |
Tests check projected states after detach and close, and verify that subagent updates racing with release remain running when the guarded write does not match the current active subagent. |
Priority:
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant ProviderSessionManager
participant ThreadProjections
participant EventSink
ProviderSessionManager->>ThreadProjections: Read eligible thread and subagent projections
ThreadProjections-->>ProviderSessionManager: Return current subagent and child-thread state
ProviderSessionManager->>EventSink: Write interruption events if subagent remains active
Suggested reviewers: t3dotgg
Merge Risk: 
Minimal · up to cabc7
-
No identified issue blocks merging after normal checks.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description includes all required sections. It explains the problem and reproduction context, the implementation, scope and approval rationale, focused verification results, limitations, and the agent and harness used.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
Release cleanup read each subagent's start and then wrote unconditionally, so a replacement that resumed the subagent between the read and the commit was overwritten as interrupted along with its child root turn. Each subagent and its child turns now commit together through EventSink.writeIfSubagentActive, which re-reads the subagent's status and started_at inside the transaction and skips the write when it is no longer active with the start the release read.
|
Note Written by Verified in a real dev environmentpartial verification on linux at
short runtime transcript, read from the dev server's isolated database after the UI actions: Not verifiedthe actual changed behavior remains unverified. both main and the pr report Claude native desktop, iOS/Android, remote/tunnel connections, OpenCode session survival, and real-provider resume races were not exercised. the first native-child turn's response became visible after a reload; the later follow-up rendered live. that observation is not attributed to this server-only diff. |



Problem
When T3 releases a Claude provider session, closing the session ends the Claude Code process and every background subagent running inside it. A session is released when its thread is settled or archived, when the workspace, runtime mode or model selection changes, after the idle pin expires, or on a runtime error. The release already cleared the session's background roster and pending approvals, but not its subagents. Their subagent record, node, turn item and child thread root turn stayed
running, so the thread showed them as running and active with no end.Seen on 2026-10-30 in a desktop thread with two background
Agentsubagents, started at 05:36 and 05:38 UTC:query.closeat 05:58:41, while both are still in the SDK's lastbackground_tasks_changedroster, and notask_notificationarrives for either. No turn was running and noquery.interruptcame before the close, so it came from the session release, not from Stop. The thread record hasunsettledAt06:35:49 and no message between 05:43 and 07:56, so the thread was settled in between, and settling detaches its provider sessions.resumeat 07:56:42, the SDK sent two separatesystem/task_notificationframes withstatus: "stopped", one per task id. The model saw them merged into one<task-notification>. The new adapter runtime had no record of either task id andupdateClaudeSubagentNodedrops a terminal update for an unknown task, so the server trace shows no subagent writes then either. The records only changed when both agents were resumed with SendMessage at 07:58:40.Startup recovery already ends orphaned native subagents, and Stop ends them through
thread.background-work.settle. A release while the server keeps running did neither.Change
ProviderSessionManagernow also ends the provider-native subagents of a released runtime whose background tasks end with its process (backgroundTasksEndWithProcess, which today is only Claude). It sits beside the existing roster cleanup. When the session leaves the live map, it reads the running native subagents whose turn item belongs to one of that session's provider threads, using the bounded recovery projection. After the session scope closes, it marks each subagent record, node and turn iteminterruptedand ends its child thread's runless root turn. It uses the sameendOrphanedNativeSubagentandendRunlessRootTurnshelpers that startup recovery and Stop use. Server shutdown is still left to startup recovery, and OpenCode's work, which outlives its session, is left alone.startedAtthe release read. A resume reopens the same record with a new start. Each subagent and its child turns commit together through the newEventSink.writeIfSubagentActive, which re-readsstatusandstarted_atinside the commit transaction, so a resume that lands at any point before the commit keeps the subagent and its child turn running. A resume after the release reopens the record as before.Scope and approval
No prior issue. This is a focused fix for an obvious bug: the UI shows background agents as running after the process that ran them is gone. It changes one server file and its tests, with no contract or client changes.
Verification
ProviderSessionManager.test.ts: the detach roster test, now "ends the background work of a session a detach releases", also seeds a running native subagent with a child thread on each of two provider sessions. After the detach, the released session's subagent record, node, turn item and child root turn areinterrupted, and the other session's stayrunning. Onmainthis fails: all four stayrunning.32ecd71726,vp run check:changed(format, lint, knip, graphql, typecheck oft3,scriptsandevals, tests beside the change) passes on Windows.Not checked: a live run in the desktop app with a real Claude session being released. Open tool calls inside a dead subagent's child thread are not ended by this change; Stop's settle path does not end them either.
Done with Claude Opus 5.5 in Claude Code, running in T3 Code.