Skip to content

fix(server): background subagents stop showing as running after their Claude session is released - #27487

Open
Pyseph wants to merge 2 commits into
pingdotgg:mainfrom
Pyseph:fix/subagents-end-with-released-session
Open

Pyseph wants to merge 2 commits into
pingdotgg:mainfrom
Pyseph:fix/subagents-end-with-released-session

Conversation

@Pyseph

@Pyseph Pyseph commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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 Agent subagents, started at 05:36 and 05:38 UTC:

  • The provider event log has both subagents streaming until 05:58:36 UTC. T3 sends query.close at 05:58:41, while both are still in the SDK's last background_tasks_changed roster, and no task_notification arrives for either. No turn was running and no query.interrupt came before the close, so it came from the session release, not from Stop. The thread record has unsettledAt 06:35:49 and no message between 05:43 and 07:56, so the thread was settled in between, and settling detaches its provider sessions.
  • Both subagents showed as running and active for about two hours, until I asked.
  • When the next turn reopened the session with resume at 07:56:42, the SDK sent two separate system/task_notification frames with status: "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 and updateClaudeSubagentNode drops 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

  • ProviderSessionManager now 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 item interrupted and ends its child thread's runless root turn. It uses the same endOrphanedNativeSubagent and endRunlessRootTurns helpers 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.
  • A subagent is only ended while it is still active with the startedAt the release read. A resume reopens the same record with a new start. Each subagent and its child turns commit together through the new EventSink.writeIfSubagentActive, which re-reads status and started_at inside 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 are interrupted, and the other session's stay running. On main this fails: all four stay running.
  • The test for a runtime whose tasks outlive it now also checks that its subagents stay running.
  • New "keeps a native subagent that a replacement resumed during release": the resume lands while the old session scope is still closing, and the subagent stays running. It fails if the start check is removed.
  • New "keeps a native subagent resumed after release read it, until the commit": the resume lands between the release's read and its commit (Macroscope's race). It fails when the transaction's start check is removed and passes with it.
  • All 51 tests in the file pass. After rebasing onto main 32ecd71726, vp run check:changed (format, lint, knip, graphql, typecheck of t3, scripts and evals, 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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 30, 2026
);
}
if (events.length > 0) {
yield* eventSink.write({ events });

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.

🟠 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp

macroscopeapp Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

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

  • 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.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between bb20db5 and cabc7ae.

Files selected for processing: 4
  • apps/server/src/orchestration-v2/EventSink.ts
  • apps/server/src/orchestration-v2/ProjectionStore.ts
  • apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
  • apps/server/src/orchestration-v2/ProviderSessionManager.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

When eligible sessions are released, the manager now recovers active native subagents and eligible runless root turns in their child threads. Tests cover detach, runtimes with outliving tasks, and races during release.

Changes: Native subagent release recovery

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

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
Loading

Suggested reviewers: t3dotgg

Merge Risk: Minimal · up to cabc7

No identified issue blocks merging after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary fix: background subagents no longer remain marked as running after their Claude session is released.
Description check Passed 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 a…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
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.

maria-rcks commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

partial verification on linux at cabc7ae803c2f2366822a03da6ce5c5c0da608e1. no code changes or pushes.

  • started github/main (5fe18d79ac) in a separate worktree after vp i, then stopped its captured process group before starting the exact pr head. main used web/server ports 8693/16733; the pr used 5733/13773 with isolated .t3 state.
  • drove the web app using repo Playwright and /usr/bin/google-chrome. added a project pointing to a real git repo through the app. used real Codex gpt-6.1-sol turns, with no mocks or seeded scenario data.
  • main's Codex turn read the repo's README successfully. on the pr head, a native Codex child read it and completed. settled the parent through the sidebar, reopened it with Un-settle, then sent another prompt in the same thread. the follow-up streamed FOLLOWUP_OK real provider verification workspace. the completed child remained completed.
  • ran the existing ProviderSessionManager.test.ts suite through blacksmith: all 50 tests passed. existing GitHub CI checks are passing; skipped jobs were not treated as runtime proof.

short runtime transcript, read from the dev server's isolated database after the UI actions:

pr HEAD: cabc7ae803c2f2366822a03da6ce5c5c0da608e1
parent run ordinal 1: completed, gpt-6.1-sol
native child: provider_native, completed
parent settled -> Un-settle -> new user prompt
parent run ordinal 2: completed, gpt-6.1-sol
native child afterward: completed
ProviderSessionManager.test.ts: 50 passed

completed codex native child and follow-up after settle and reopen

real codex follow-up streaming after settle and reopen

Not verified

the actual changed behavior remains unverified. both main and the pr report Claude Not authenticated, and claude auth status returns loggedIn: false. the new cleanup is gated on backgroundTasksEndWithProcess, which the Claude adapter sets and the Codex adapter does not. working Codex turns cannot exercise that gate. consequently i could not reproduce the original Claude bug on main, verify interruption of a still-running Claude child on session release, or exercise the replacement/resume race with real Claude sessions. the automated race coverage passed, but it is not a live-provider demonstration.

claude authentication blocker in the running pr app

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.

This branch has not been deployed

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

Labels

size:L 100-499 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.

2 participants