Skip to content

fix(server): Claude subagents that end between turns no longer stay Running - #301

Merged
bk-agent-01 merged 6 commits into
stagefrom
fix/claude-subagent-end-between-turns
Oct 11, 2026
Merged

bk-agent-01 merged 6 commits into
stagefrom
fix/claude-subagent-end-between-turns

Conversation

@bk-agent-01

@bk-agent-01 bk-agent-01 commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A Claude subagent can finish while T3 still shows it as Working for hours. On stage, the reviewer subagent "Review swap undo diff" on "CI/CD Improvement" finished at 2026-10-10 09:08:11 UTC. One day later, T3 still showed it as running.

The cause has two parts:

  1. The adapter waits for a continuation run. Between turns, the Claude adapter puts a subagent's task_notification only in its wake buffer. The card completes only when a continuation run drains that buffer. The end of a nested subagent never offers a continuation.
  2. A held queue blocks the continuation forever. Stop holds every queued run, and each later queued run copies the hold. On this thread, a Stop on 2026-10-06 held the queue. On 2026-10-10 the continuation for a turn that Claude started by itself (run 173) copied that hold and never started. From then on, the adapter kept every later frame in its buffer. That included the subagent's completion and three agent replies, which never appeared in the thread.

Evidence: the Claude stream log has task_notification a4439e85fa530c9ab completed at 09:08:11.564. The stage database has that subagent as running, and run 173 as queued + queueHeld since 09:07:58. The journal has no wake-turn log at 09:08:11. The latest upstream main (8d199b4f7, 2026-10-11) still has the same code.

Fix

  1. The subagent end reaches its card at once. Between turns, a running registered subagent's task_notification now completes its card through the last settled turn, as subagent progress already does. The frame stays in the wake buffer, so the wake label and the continuation offer do not change. The drain does not apply the card update a second time. The undrained end keeps the session pinned, as the running subagent did before.
  2. A provider continuation starts past a held queue. A continuation run (createdBy: "agent", creationSource: "provider") no longer copies the hold. The queue starts the first queued run that is not held. Held user runs keep waiting for the user, in their order. Stop still holds every queued run, including a continuation that is already queued.
    • For Claude, ACP and OpenCode, a continuation only attaches output that the CLI already produced. For Codex, the continuation text is a real prompt, so a Codex continuation can now run before held user messages. A Codex continuation that arrives with no active run already did that before this change.

All edits in upstream files have T3-CUSTOM(expbkt3) markers. The queue helper is in the fork-owned QueueHeldContinuation.expbkt3.ts.

Tests

  • ClaudeAdapterV2.subagentEndBetweenTurns.expbkt3.test.ts: a nested subagent ends between turns. A root background subagent ends between turns and the continuation is still offered. A drain replay does not change the card.
  • QueueHeldContinuation.expbkt3.test.ts: a continuation starts past a held queue. A user message still copies the hold. A queue with only held runs starts nothing. canStartQueuedRun gives the same result for SQLite and in memory.

I ran no tests locally (shared-host rule). CI verifies.

Risks

  • If you press Stop while a continuation is already queued, Stop still holds that continuation, and its replies can still wait. The subagent cards still complete in that case.
  • After a Stop, you can now see output that Claude produced by itself. That output already exists, but before this fix T3 hid it.

Model and harness: Claude Opus 5.5 in Claude Code (T3 Code session).

🤖 Generated with Claude Code

bk-agent-01 and others added 2 commits October 11, 2026 09:52
…s Running

A background subagent's task_notification that arrives while the root turn is
idle only went to the wake buffer, so its card completed when a continuation
run drained it. A nested subagent's end never offers a continuation, and a
held queue can keep the continuation from ever starting; on stage a reviewer
subagent showed Running for a day after it finished.

Between turns, a running registered subagent's end now completes its card at
once through the turn that settled last, as its progress already does. The
frame still waits in the wake buffer, so the wake label and continuation
offer are unchanged; the drain skips the card update it already made, and the
undrained end keeps pinning the session until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop holds every queued run, and each later queued run copied that hold. A
continuation that ProviderContinuationService queues for output the Claude CLI
already produced (it started a turn by itself) was held with them, so it never
ran: the adapter kept that output, and every later wake on the thread, in its
buffer. On stage this lost three agent replies and a subagent's completion.

An adapter continuation (createdBy agent, creationSource provider) no longer
copies the hold, and the queue starts the first queued run that is not held.
Held user runs keep waiting for the user, in their order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 11, 2026
bk-agent-01 and others added 2 commits October 11, 2026 09:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inuations

A second end that arrives behind a buffered resume must wait for the drain
that re-opens the card, or the card stays Running. The queue helper's comment
now says that a Codex continuation is a real prompt and also starts past a
held queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bk-agent-01

Copy link
Copy Markdown
Collaborator Author

Fresh-context review: no blockers. These are the answers to the three minor points.

  1. The queue exemption also covers Codex, where a continuation is a real prompt. I kept this behaviour. A Codex continuation that arrives with no active run already started past a held queue before this PR, so this change only makes the queued case match. The helper comment (QueueHeldContinuation.expbkt3.ts) and the PR body now say this plainly. Fixed in 1be3edc6c.
  2. No test for the hasBufferedWork resume guard. I added the test "ends a subagent resumed between turns only after the drain re-opens it". First the end N1 shows. Then a resume task_started goes into the buffer while no turn runs, and the end N2 arrives. The test asserts that the drain re-opens the card and then completes it with N2's summary. It fails if the guard is removed. Fixed in 1be3edc6c.
  3. The drain no longer repairs a completion that was lost. I accept this edge case. The end that shows between turns goes through the launch run's ingestion stream. That stream stays alive while the subagent counts as running. It fails only if that stream already died from an ingestion error, and the card update that subagent progress sends uses the same route today.

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

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant