[Bug]: Stale "Waiting on background task T3 Worktree Handoff" banner persists after a Claude worktree handoff#15993

openedOct 5, 2026byLletrandat

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In a top-level claudeAgent / claude-opus-5-5 thread (full access), have the agent call t3_worktree_handoff with a continuationPrompt (startFromOrigin: true, runSetupScript: false).
  2. Let the thread continue. The worktree is created, the thread is rebound, and later turns run in the worktree.
  3. Finish several more turns, then look at the composer once the thread is idle.

Expected behavior

Once the handoff's turn ends, its tool item is settled, and the composer shows no pending work after later turns complete.

Actual behavior

The composer keeps showing "Waiting on background task T3 Worktree Handoff" with a Stop button, after 7 later turns that all completed normally. The agent has nothing running. When asked what it was waiting for, it confirmed the handoff had finished long before.

From the thread's projection (read from a copy of the state DB):

RunStatusNote
1failed"The provider event stream closed unexpectedly…" about 1.4 s after the handoff tool_use
2cancelledthe handoff continuation
3–8completednormal turns inside the worktree

The dynamic_tool turn item for mcp__t3-code__t3_worktree_handoff in run 1 is still status: "running" with completedAt: null. Nothing will ever complete it, because the Claude session that owned it was released by the workspace-change detach.

Two pieces combine:

  • When a run reaches its final status, RunExecutionService closes open run-owned subagent rows (L687), but deliberately leaves dynamic_tool and command_execution items open so a late completion can arrive (L93-L101). That holds while the provider process is alive. Here the session was released, so the item stays running forever.
  • pendingBackgroundTurnItems (L178-L195) lists any active background-type item from any run that is not rolled_back, so the orphan from failed run 1 shows up again every time a later run settles.

This is related to #15136, which has the same trigger: the planned detach on handoff is treated as an unexpected stream close for Claude. There, the handoff item ended cancelled and the continuation stayed held. Here, the continuation was cancelled, later turns ran fine, and the handoff item was never settled. That suggests the item cleanup on detach is racy, and #15203 may not cover it because it changes how the detach ends the turn, not what happens to in-flight tool items. The same leftover probably appears whenever a session is released while a tool item is in flight. A quick scan of the same DB found 3 other threads with command_execution items still running from older runs.

Pressing Stop is the only way out, and it targets work that no longer exists.

Impact

Minor bug or occasional failure

Version or commit

0.0.46-nightly.20261004.2657

Environment

macOS, desktop app, local server, claudeAgent / claude-opus-5-5, full access

Logs or stack traces

1run 1 failed error item: "The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs."
2item dynamic_tool toolName=mcp__t3-code__t3_worktree_handoff title="T3 Worktree Handoff" status=running completedAt=null

Filed by Claude Opus 5.5, running in T3 Code through the Claude Code harness, on behalf of @letrandat.

Note

Grok responding on behalf of Julius.

Triage

Thanks for the thorough write-up, @letrandat. Your projection snapshot matches current main (cf3e714b0f), and 0.0.46-nightly.20261004.2657 isn't missing a fix.

What I found

t3_worktree_handoff rebinds the thread and schedules provider-session.detach. Claude sessions are exclusive, so that detach releases the whole session. releaseEntry fails every event subscriber with ProviderAdapterEventStreamError before it closes the adapter, and the run is stored as failed with "The provider event stream closed unexpectedly…".

finalizeActiveTurn does cancel open tool calls, but it runs while the session scope is closing, after those subscribers are gone, so the cancellation never reaches the run. Run finalization only closes open subagent rows, so dynamic_tool and command_execution items stay running.

A later run doesn't adopt them either. Inheritance requires the same provider thread, and the released Claude session no longer exists. Meanwhile pendingBackgroundTurnItems still lists any active command, dynamic tool, or subagent whose run isn't rolled_back. So once a later turn settles, the composer shows Waiting on background task … for the handoff item. Stop works from the same list and marks items on a dead provider session as interrupted, which is why it clears the banner even though nothing is running.

The two outcomes depend on timing. If the Claude process exits first, finalizeActiveTurn can publish the cancellation while subscribers are still attached, and the item ends cancelled (what #15136 recorded). If the detach releases the session first, the cancellation is dropped and the item stays running, which is what you're seeing. The same leftover can happen whenever an exclusive session is released while a command or dynamic tool is still running.

#15136 is the same detach recorded as an unexpected stream close. Open PR #15203 turns a planned workspace detach into an interruption so the continuation can start, but it leaves in-flight dynamic_tool and command_execution rows alone, so this banner could still appear with that change.

Workaround: press Stop on the banner. That settles the leftover items on the dead session, though it doesn't revive the handoff.

Likely fix area

  • RunExecutionService / run finalization. One option is to terminalize open dynamic_tool and command_execution rows when a run settles, the same way subagent rows are handled.
  • releaseEntry / finalizeActiveTurn ordering. Another option is to publish the tool cancellations before failing subscribers on an exclusive-session release.
  • packages/shared/src/orchestrationV2PendingBackgroundWork.ts (pendingBackgroundTurnItems). A third option is to skip items whose provider session is already gone.

A maintainer will decide on the fix direction.

Synced from GitHub

Additional reproduction on macOS desktop connected to a remote Linux server (0.0.46-nightly.20261007.2774), using Claude Sonnet 5.5 in Full access mode.

The same stale-task pattern occurs, but the suggested Stop workaround does not clear it:

  • The composer shows Waiting on 2 background tasks: advisor, mcp__t3-code__t3_worktree_handoff.
  • Thread activity records both as dynamic_tool items still marked running.
  • Run 3 failed with "The provider event stream closed unexpectedly." The handoff continuation (run 4) was cancelled; runs 5–8 subsequently completed. The thread has no active run, but those two tool items remain pending.
  • Clicking Stop briefly changes the button to Stopping..., then it returns to Stop with both tasks still listed. This was reproduced directly in the macOS UI.

The worktree handoff did take effect and later turns ran there, so this appears to be stale tool state after the failed handoff. The additional issue is that Stop fails to settle the leftover items in this case.

Synced from GitHub

Still reproducible on 0.0.46-nightly.20261010.2935 (macOS desktop, Claude Opus 5.5, auto mode).

  • t3_worktree_handoff called at 07:27:03; provider log shows query.close at 07:27:07 with no tool_result for the call.
  • The handoff run ended failed; the continuation run started in the worktree and completed normally.
  • The dynamic_tool item for mcp__t3-code__t3_worktree_handoff stayed running for ~2h, so the composer showed Waiting on background task mcp__t3-code__t3_worktree_handoff after every settled turn.
  • In this case Stop did clear it: a later interrupted run moved the item to interrupted.

Happens on most sessions that use the handoff.

Synced from GitHub

Still reproducible on 0.0.46-nightly.20261013.3042 (macOS desktop, local server, claudeAgent / claude-opus-5-5, full access). One thread showed both this bug and #15136.

From the thread's projection (read-only query of statev2.sqlite):

RunStatusNote
8failedCalled t3_worktree_handoff (with continuationPrompt, startFromOrigin: false, runSetupScript: false) at 04:08:23; the run ended at 04:08:24
9cancelledThe worktree continuation (userMessageId message:mcp:…:worktree-continuation:…). queueHeld: true, never started, and stayed held for 56 minutes until it was cancelled at 05:04:37
10, 13, 17, 18completedNormal turns in the worktree, after the user typed "continue"

The dynamic_tool item for mcp__t3-code__t3_worktree_handoff in run 8 is still status: "running", completedAt: null, two hours later. After every settled turn the composer shows Waiting on background task mcp__t3-code__t3_worktree_handoff, which matches pendingBackgroundTurnItems picking up an active item from a non-rolled_back run. I did not press Stop.

The run 9 data matches #15136: the continuation stays held after the planned detach, so the agent does nothing until the user sends another message. #15203 and #16749 are both still open.


Filed by Claude Opus 5.5, running in T3 Code through the Claude Code harness, on behalf of @letrandat.

Synced from GitHub

Follow-up to my comment above: pressing Stop on the "Waiting on background task mcp__t3-code__t3_worktree_handoff" banner did not clear it either. I clicked it several times and nothing changed. This was a local macOS server on 0.0.46-nightly.20261013.3042, so the Stop workaround failing is not limited to the remote-server setup @LetterXbox reported.

Synced from GitHub

Still reproducible on v0.1.0 (Linux x64 server, desktop AppImage backend, used remotely from the Windows desktop app; claudeAgent / claude-opus-5-5). After t3_worktree_handoff, the composer kept showing "Waiting on background task mcp__t3-code__t3_worktree_handoff", and pressing Stop did not clear it. Details, including the interaction with a stale AskUserQuestion, are in https://github.com/pingdotgg/t3code/issues/15517#issuecomment-6219354737.


Produced by Claude Code (Claude Opus 5.5) via t3 triage.

Synced from GitHub

Additional reproduction on v0.1.0 (macOS, local server, grok / grok-4.7, full access). The provider event stream closed while t3_worktree_handoff was in flight, the tool row stayed pending, and the composer showed the banner again after later turns had finished.

The server process started at 2026-10-18T17:45:09Z and was still that process the next day. Two Grok threads in the same database hit this within half an hour.

Thread A

t3_worktree_handoff ran at 2026-10-18T17:59:46Z with startFromOrigin: false, an explicit worktree path, runSetupScript: false, and a continuationPrompt.

The dynamic_tool row was stored as pending (toolName t3-code.t3_worktree_handoff, completedAt null). It never received a tool result. At that same timestamp the run failed with:

The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs.

The continuation run was queued at that timestamp and later cancelled. Later runs in the worktree completed. The handoff item had no further turn-item.updated until 2026-10-19T19:34:59Z, about 25.5 hours later. The next day, with the thread idle, the composer still showed Waiting on background task t3-code.t3_worktree_handoff.

RunStatusNote
1failedHandoff in flight; stream closed at 17:59:46Z
2cancelledContinuation queued at the same timestamp
3–10completedLater turns in the worktree, while the handoff row stayed pending

Thread B

Same provider and runtime mode. Handoff at 2026-10-18T18:21:26Z, this time with startFromOrigin: true and runSetupScript: true, plus a continuationPrompt. The item stayed pending with completedAt null. Run 1 failed with the same stream-closed message, run 2 was cancelled, and later runs completed. That item also had no update until 2026-10-19T19:34:59Z.

What this adds

The provider was Grok. The item status stayed pending; it never moved to running. The session here is not an exclusive Claude session, so this is the same banner from the stream closing before the tool result is written.

pendingBackgroundTurnItems on current main (f740a97947cc) still lists any active command_execution, dynamic_tool, or subagent whose run is not rolled_back. pending is an active status. After the latest run settles, derivePendingBackgroundWork shows the handoff again. The last commit that touched packages/shared/src/orchestrationV2PendingBackgroundWork.ts is 11ce406d2 (#19751), and that change does not drop this leftover.

The event log has no settle of either handoff item during the day they stayed pending. That matches the later reports that Stop leaves the banner up on an already-failed idle thread (https://github.com/pingdotgg/t3code/issues/15993#issuecomment-6056139370, https://github.com/pingdotgg/t3code/issues/15993#issuecomment-6139632858, https://github.com/pingdotgg/t3code/issues/15993#issuecomment-6219355182 on v0.1.0).

A later turn-item.updated that sets the row to interrupted is what removes it from that list. I wrote that event locally to clear the composer. It is not a product fix.

Synced from GitHub

Still reproduces on 0.1.0 (T3 Code (Beta) desktop app with its local server, macOS 27 arm64, top-level thread, claudeAgent / claude-opus-5-5, full access, Claude Code 2.1.304).

  • The agent called t3_worktree_handoff (startFromOrigin: true, with a continuationPrompt). The worktree was created, then the run ended failed with "The provider event stream closed unexpectedly". The continuation stayed queued and never started on its own (#15136).
  • After that, every later turn completed normally inside the worktree, but the composer kept "Waiting on background task mcp__t3-code__t3_worktree_handoff". Stop does nothing, and only restarting T3 Code clears it.
  • Side effect: since the thread never looks idle, it never signals that the agent is done. It never settles or comes back as finished, so I only notice by opening it.
Synced from GitHub

Another trigger for the same leftover running turn item: #25054. A Claude thread is switched to Full access while an approval is pending, and the user types a reply. The adapter replaces the query, the old run fails with "provider event stream closed unexpectedly", and the declined tool's dynamic_tool / command_execution item is never finalized. A fix that closes background-capable items when a run fails because its query is gone would likely cover both.

Synced from GitHub

Also seen on 0.1.3-nightly.20261027.3754 with claudeAgent / claude-sonnet-5-5 (full access).

After a t3_worktree_handoff call (with continuationPrompt and the project setup script enabled), the thread keeps showing "Waiting on background task mcp__t3-code__t3_worktree_handoff", even though:

  • the handoff completed (t3_worktree_status reports attached: true),
  • the setup script had exited (~25 s after the handoff) and no related processes were left,
  • the queue was empty (t3_queue_list), and the only running thread was the current one in a normal turn.

So nothing is actually pending; the handoff tool call just never settles, same as the running / completedAt: null tool item described above.

Synced from GitHub

Still reproducible on the stable 0.1.2 (T3 Code (Beta) desktop app with its local server, macOS 27 arm64; top-level thread, claudeAgent / claude-opus-5-5, full access). It also shows up on mobile: the Background work sheet lists mcp__t3-code__t3_worktree_handoff / "Background task" for hours after the thread went idle.

What happened:

  • The first run of a new thread failed during workspace preparation (git fetch could not authenticate). In the next run the agent fetched over HTTPS itself and called t3_worktree_handoff (baseRef: origin/main, startFromOrigin: false, with a continuationPrompt).
  • That run (2) ended failed 0.5 s after the call, at 07:21:24.
  • Unlike #15136, the continuation started at once (run 3) and completed normally. About 15 later runs, including delegated subagents and workflows, all finished.
  • t3_worktree_status reports attached: true on the new worktree, and no related processes are running.

From a read-only copy of statev2.sqlite: the dynamic_tool item for mcp__t3-code__t3_worktree_handoff in run 2 is still status: "running", completedAt: null, more than 6 hours later. Every other dynamic_tool and command_execution item in the thread is completed.

So the leftover item appears even when the handoff otherwise works perfectly. It looks like the handoff's own tool item is never settled once the detach fails the run. As earlier comments noted, it also keeps the thread from looking finished.

Synced from GitHub

Two more triggers for this, both reachable without a worktree handoff, and a trace of where the item gets left behind. Reproduced on main at 8970a32104 (Linux, Claude Haiku 5.5) and d487078f11 (macOS, Cursor), on isolated test servers.

Triggers

  • Signing a provider out from Settings → Providers while another thread runs a shell command. The dialog warns that it stops running threads, and it does, but that thread is left with "The provider event stream closed unexpectedly. Retry the turn…" and a "Running: / Stop" row that survives reloads, signing back in, later successful turns and auto-settle. (A /logout message does the same.)
  • Changing Permissions mid-command and steering, from the web composer: Claude reports supportsRuntimeModeSwitchInSession: false, so the server detaches the session ("Runtime mode changed."). Same result: run failed, provider turn interrupted, the command_execution item and its tool_call node still running with no completedAt, and the row stays after the shell loop has long exited.

Where it's left behind All of these, along with archive (#15135), worktree/workspace changes (#14245, this issue) and provider selection changes (#22755), end in ProviderSessionManager.releaseEntry. It calls failSubscribers before closeScopeWithin, so the adapter's turn finalizer, which normally settles open tool calls (ClaudeAdapterV2 endToolCalls, Cursor finalizeTurn, ACP terminalizeOpenRunOwnedItems), never emits. RunExecutionService's ingestion-failure handler then interrupts the provider turn and writes the failed terminal run events, but only cascades subagents (cascadeTerminalizeRunOwnedSubagents); the run's own command_execution/dynamic_tool items and their tool_call nodes aren't tracked there (activeBackgroundTurnItems holds ids only). The release reason ("Provider sign-in changed.", "Runtime mode changed.") never reaches that handler, which is why the message is the generic stream failure.

Two smaller gaps nearby: the row's Stop clears the row and marks the item interrupted but leaves the tool_call node running, and startup recovery (ProviderRuntimeRecoveryService) selects threads by active items, so a node-only leftover is never cleaned up. A server restart does clear the item-plus-node case.

A narrower fix than #16749 / #17306 One point covers every trigger above: in that ingestion-failure handler, when the failure is the confirmed provider loss from a session release (the ProviderAdapterEventStreamError from failSubscribers), settle the run's open command and tool items and their nodes as interrupted in the same writeFinalRunEvents write, and never on normalization or persistence failures, which addresses #17306's objection. Roughly 40–80 lines in RunExecutionService.ts plus focused tests. Passing the release reason through would also let the message say why the turn stopped.

I'm happy to send that as a PR, or leave it for a maintainer, whichever you prefer.

Claude Opus 5.5, an AI agent, on Jackson's behalf.

Synced from GitHub

A correction and a pointer, after building the fix I offered above.

  • The narrower fix already exists. After #17306 was closed, @kristianbonnici narrowed it exactly as asked (comment on #17306, 2026-10-17): tool calls are interrupted only when the session manager failed or ended the run's stream, and normalization and persistence failures leave them running. It's on their branch, fix/close-tool-calls-on-lost-provider-stream (f042e4de1a), and isn't visible on the closed PR. I built the same approach independently before seeing it, and it fixed both triggers above in tests, so I'd suggest reopening #17306 rather than taking a new PR from me.
  • One part of my trace was wrong. The release reason does reach the ingestion-failure handler, as the error's cause; it's the message builder (knownProviderFailureMessage) that drops it. And the error tag alone can't identify a provider loss, since the session manager may forward the adapter's own failure. Keying on the run's subscription stream, as #17306 now does, is what works.

Claude Opus 5.5, an AI agent, on Jackson's behalf.