Agent archiving its own thread with t3_thread_organize fails the running turn with "provider event stream closed unexpectedly"#15135

openedOct 3, 2026by00pilatos0

What happened

When an agent calls t3_thread_organize with action: "archive" and no threadId (its own thread) during its turn, the archive succeeds. The run then ends as failed with "The provider event stream closed unexpectedly", even though every earlier step in that turn succeeded. Expected: either the archive waits until the turn ends, or self-archive is refused with a clear error. Reproduces 4 out of 4 runs, desktop app, macOS arm64.

Diagnosis

thread.archive has no active-run guard, and it tears down the caller's live provider session.

  1. t3_thread_organize (apps/server/src/mcp/toolkits/thread/handlers.ts:275) dispatches thread.archive for the caller's thread. assertLiveCaller (mcp/threadAccess.ts:40) requires the caller to have an active run, so a self-archive always targets a thread in the middle of a turn.
  2. The decider only rejects threads that are already archived (orchestration-v2/Orchestrator.ts:2264). Archive cancels queued runs only (:2964) and leaves the running run alone. It then detaches every provider session with detail: "Thread archived." and revokeMcpCredential: true (:3033-3106). The comment at :3026 says this is safe for settle because settle rejects active runs. Archive has no such check.
  3. For a single-thread session (Claude here), ProviderSessionManager.detach releases the entry (:1871). releaseEntry then calls failSubscribers (:512), which pushes ProviderAdapterEventStreamError("Thread archived.") into the run's event stream.
  4. RunExecutionService (:1267-1307) still sees the run as running, so it writes a failed terminal. ProviderFailure.ts:47 maps that to the generic "stream closed unexpectedly" text, and "Thread archived." is never shown.

For comparison: settle rejects active runs (Orchestrator.ts:2352), delete cancels them before detaching (ThreadDeletion.ts:70), and the web UI disables Archive while a thread is running (threadActionMenu.logic.ts:51). That UI code's comment says the server rejects the action, which v2 no longer does. Codex and OpenCode2 sessions take the interruptTurn path in detach, so they would likely end as interrupted instead (not tested). On main (e8545b29), Orchestrator.ts and the MCP toolkit are unchanged since this tag, and releaseEntry still fails subscribers.

Possible fixes: reject thread.archive while a run is active, as settle does, so the agent gets a clear error. Or defer a self-archive until the turn ends.

Steps to reproduce

  1. In the desktop app, start a thread on Claude.
  2. Ask the agent to call t3_thread_organize with action: "archive" and no threadId, then reply "done".
  3. The thread is archived within about 10 ms of the tool call starting, and the run fails with "The provider event stream closed unexpectedly". The tool call item stays running.

Version

0.0.46-nightly.20261003.2623 (fed41fa8)

Environment

macOS 27.0 (26A428) arm64, Node v26.8.2, Claude Code 2.1.288, desktop app with local server

Evidence

shell
1# statev2.sqlite projections, 2026-10-03 (UTC). These are the only failed runs in the database.
2thread tool started archived_at run failed last tool call
3cd17f2c6 09:29:55.154 09:29:55.165 09:29:55.172 t3_thread_organize {"action":"archive"} [running]
4feb9e7ee 09:35:33.715 09:35:33.722 09:35:33.729 same
55f2febba 09:55:35.674 09:55:35.683 09:55:35.689 same
63d61eeab 09:55:36.278 09:55:36.289 09:55:36.294 same
794e4b217 09:58:11.197 09:58:11.200 09:58:11.204 same
867d809c6 09:58:17.022 09:58:17.026 09:58:17.029 same
9
10error turn item: {"class":"unknown","message":"The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs.","code":null,"retryable":null}

#14203 (closed) asked for archive to stop the provider session, which v2 now does, but nothing handles a running turn. #14907 (open PR) covers archived threads starting work after setup, which is a different path. I found no existing issue for archiving a thread with a running turn.

Fix applied or workaround

Nothing changed on the machine. Workaround: archive from another thread, or manually after the turn ends. Settle is not a workaround from inside the turn, because it rejects active runs.

Filed by

Claude Code 2.1.288 (claude-opus-5-5) via t3 triage

Note

Grok responding on behalf of Julius.

Triage

Thanks @0pilatos0 for the detailed diagnosis and the database evidence! I confirmed this on current main (263097a8). The nightly you reported on (fed41fa8) is an ancestor of that commit, and nothing since has changed this path.

What happens

thread.archive only refuses a thread that's already archived. It cancels queued runs, then detaches every live provider session with detail Thread archived. and revokeMcpCredential: true. A run that is preparing, starting, or running is left in place. By comparison, settle rejects active work before it detaches, and delete cancels active runs first. Archive does neither.

The clients already treat archiving a live turn as off-limits. threadRuntimeCanArchive allows dropping queued work but not detaching a provider that's preparing, starting, or running, and the web and mobile archive actions are disabled in that state. The menu comment saying the server rejects the action is out of date.

A self-archive through t3_thread_organize (with action: "archive" and no threadId) always hits this. readWritableThread requires the caller to own an active run, then dispatches thread.archive on that same thread. For a single-thread session like Claude, detach releases the session, and failSubscribers pushes a ProviderAdapterEventStreamError with cause Thread archived.. RunExecutionService still sees the run as running, so it writes a failed terminal. ProviderFailure then swaps the cause for the generic "provider event stream closed unexpectedly" text, which matches all six rows you posted. The tool item stays running because the stream dies before the tool result comes back.

Shared multi-thread sessions (Codex, OpenCode) interrupt or unload rather than going through releaseEntry, so they may end up interrupted instead of failed. That's still untested.

#14203 (closed) was about stopping an idle provider process on archive. That teardown is now what kills the live turn. Open PR #14907 is a different path: setup finishing and starting work after the thread is already archived.

Likely fix area

One option is to make thread.archive reject a thread with a preparing, starting, or running run, the way settle does. The agent would then get a clear error and the turn could finish, while queued work can still be cancelled. Deferring a self-archive until the turn ends would be a new workflow. A maintainer will decide on the fix direction.

Your workaround holds: archive the thread only after its turn is idle, either from the UI or from another thread. Archiving it from anywhere while the turn is still running goes through the same server path.

Reproduced on current main 89fe509232, Linux x86_64, with vp run dev in an isolated worktree using copied development state. In the real web client, asked Claude Opus 5.5 to call t3_thread_organize with {"action":"archive"} and no threadId, then reply done.

Read-only projection evidence (UTC, 2026-10-22):

text
1Claude: tool started 01:07:39.462; archived 01:07:39.524; run failed 01:07:39.555
2Tool input: {"action":"archive"}; tool item remains running
3Error: The provider event stream closed unexpectedly. Retry the turn; if it keeps failing, check the provider and server logs.
4Codex GPT-6-Astra variant: archived 01:06:58.689; run interrupted 01:06:58.763

No matching open issue or PR found in the last 90 days. #22748 has a different trigger (switching providers); #14907 addresses workspace preparation, not an already running turn.

Leaving open for maintainer decision: refusing or deferring an agent's self-archive changes the MCP tool's allowed behavior, which is outside the automated triage fix scope. No implementation or PR created. macOS/Electron was not verified here.

Reproduced on current main defb7e6e6b, Linux x86_64, using an isolated vp run dev server and the real web client with Claude Opus 5.5. Asked it to call t3_thread_organize with {"action":"archive"} and no threadId, then reply done. At 03:16:58.046 UTC the thread archived; at 03:16:58.079 the run failed with “The provider event stream closed unexpectedly.” The tool item remained running. No matching duplicate found in the open issues from the last 90 days. macOS/Electron has not been verified here.

Synced from GitHub

Adding a real caller for the "defer" option mentioned in the issue. We close agent sessions by having the finished agent hand its leftover items to a coordinator thread and then archive its own thread as its last step (t3_thread_organize with action: "archive", no threadId). Observed on 0.1.6-nightly.20261030.3940 (Linux): the thread archived, the Claude process ended two seconds later, and the run showed "The provider event stream closed unexpectedly".

Refusing the archive while a run is active (#23451) fixes the misleading error, and a clear refusal is much better than today's behaviour. I'm noting that deferring the archive until the run reaches a terminal state would keep this closing step working from inside the turn, whereas a refusal means the agent has to end its turn and wait for someone else to archive the thread. Happy with either direction; this is only a use case for the deferral.

Synced from GitHub

More data from a long-lived database (Linux, nightlies 0.1.4-0.1.6, statev2.sqlite read-only). Of 734 archived threads with runs, 97 had their last run end within 2 s of the archive. None of those 97 ended completed: 50 failed, 46 interrupted, 1 cancelled. By provider the split is consistent with the diagnosis above: Claude-based instances and muse end failed, codex and opencode end interrupted. Of the other 637 archived threads, 600 end completed. In the failed ones, 22 of 50 had a completed assistant message in the same run before the failure and 7 have the t3_thread_organize call stuck running. Measured: the counts and the status split. Not checked: which consumers read the last run's status, and the exact adapter line that writes the failure (the mechanism above is from the existing diagnosis). This is why a refusal (#23451) fixes the misleading error, while a deferral would keep self-archive working as a closing step.