Skip to content

Claude keeps running after T3 marks the run interrupted, blocking rollback and new turns #25715

Closed

Description

@luquibu

What happened

While attempting to revert a conversation to an earlier message and switching threads during the operation, the Windows desktop app reported:

The provider could not roll back this conversation. Try again; if it keeps failing, check the provider and server logs.

The timeline displayed “Run interrupted before provider start,” but the original Claude turn continued executing tools, compacting context, and eventually asking a question.

A follow-up message failed because the previous provider turn was still active. Repeated rollback attempts failed for the same reason.

The original provider turn eventually completed successfully, while its application run remained marked interrupted. Successful rollback or follow-up submission after that completion was not verified.

From the failed revert onward, I could neither continue the session normally nor read the agent’s latest responses in T3 Code. Although the provider logs show that Claude continued working and eventually completed, that did not restore normal access to the conversation in the UI.

Diagnosis

The application run and provider execution diverged:

  • Application run 100 was marked interrupted.
  • Its provider-turn.start effect was marked cancelled, with a reason stating that interruption occurred before the provider turn started.
  • Claude subsequently emitted tool, compaction, and question events for that same run.
  • The adapter rejected rollback and another turn because it retained an active provider turn.
  • Follow-up run 101 was marked failed.

Relevant code in release commit 044f07257c9684ae4543bd1e1ebf9c394e9e3830:

  • Orchestrator.ts:9710 selects the “before provider start” interruption path when no matching provider turn is found.
  • Orchestrator.ts:9806 marks the run interrupted and requests cancellation of start/restart effects.
  • The Claude adapter rejects rollback and another turn while activeTurn is non-null.

This is consistent with a startup/interruption race. The precise cancellation or event-ingestion interleaving remains unverified. Cancellation machinery exists; the evidence does not establish why it failed to stop this provider execution.

Steps to reproduce

Observed sequence from one incident; not independently reproduced:

  1. A new message was submitted in an existing Claude conversation.
  2. The user attempted to revert to an earlier message and switched threads during the operation. The trace records thread.stop approximately two seconds after message dispatch, followed by checkpoint.rollback. It does not establish a separate manual Stop click.
  3. Rollback failed, while the timeline reported that the run had been interrupted before provider start.
  4. Sending “Continue where you left off” failed because the previous provider turn was still active.
  5. The original turn continued and produced an AskUserQuestion prompt.

Whether switching threads contributes to the failure is unknown.

Expected: after Stop, provider execution and application state should settle consistently. While cancellation is pending, the UI should reflect that work remains active rather than reporting it as already interrupted.

Version

Local backend 0.1.2, verified during triage. Triage CLI version, separately: 0.1.2-nightly.20261022.3541.

Environment

Windows x64, build 10.0.26200. T3 Code desktop app with local backend. Provider: claudeAgent. Claude Code 2.1.311, reported by the provider’s initialization event. Model: claude-fable-5-1. Plan mode, High effort, Full access.

Evidence

Sanitized timeline. All timestamps are UTC on 2026-10-27:

01:13:36.653  Application run 100 requested.
01:13:38.258  Server receives thread.stop.
01:13:38.373  Application run 100 marked interrupted.
01:13:38.473  provider-turn.start effect marked cancelled:
              "interrupted before its provider turn started"
01:13:38.807  Claude emits system/init.
01:13:39.927  Rollback rejected: provider turn is active.

After interruption, the original provider session continues
emitting tool calls and tool results.

01:18:17.344  Starting run 101 rejected: previous turn still active.
01:18:44–46   Another rollback attempt fails.
01:19:14–15   Another rollback attempt fails.
01:20:06.104  Original provider session emits compact_boundary.
01:20:39.060  Original turn's user_input request recorded.
01:24:49.074  That request recorded as resolved.
01:28:15.473  Original provider emits result:
              subtype=success, is_error=false.

Subsequent persisted state:
  Application run 100: interrupted
  Application run 101: failed
  Original provider turn: completed

Related issues

#8618 describes older stop/state inconsistencies. This incident concerns Orchestration V2 and demonstrates actual Claude activity after the application run was marked interrupted, followed by rejection of rollback and new turns. An exact duplicate or released fix for this sequence was not confirmed.

Fix applied or workaround

No repair was performed during triage. Investigation used read-only database queries, logs, and the existing release-pinned source checkout.

The original provider turn eventually completed successfully. That completion does not establish that rollback or subsequent message submission recovered.

Filed by

Codex, GPT-6 family, via t3 triage. Reviewed by three dedicated agents.

Activity

  1. jamesdchen commented on Nov 2, 2026

    @jamesdchen

    Another case of the run and the provider process diverging, on v0.1.6 (Linux server, t3 serve under systemd). Here it led to two Claude processes running the same session at once.

    Timeline for one thread (server trace, UTC 2026-11-02):

    • 03:02:21Z: a Bash tool call in the turn blocks. The host's sshfs mount had hung, and the Claude process was stuck in D state on FUSE.
    • 03:04:37Z: the user sends a follow-up. The timeline shows The provider event stream closed unexpectedly. ClaudeAdapterV2.interruptTurn fails (ProviderAdapterInterruptError: Failed to interrupt claudeAgent provider turn …), and ClaudeAdapterV2.startTurn / ClaudeAgentSdkQueryRunner.open time out after 10 s.
    • 03:04:38Z: T3 starts a second Claude scope with --resume=<same session id> and --permission-mode bypassPermissions. The first scope, started 02:59:44Z with --session-id=<same id>, is still alive. Both t3code-claude-*.scope units were active together.
    • 03:06:10Z / 03:06:16Z: follow-ups are refused with No running provider turn found for active run …:ordinal:5 (see Explicit steer is refused with "No running provider turn found" during run startup/completion gaps #23271).
    • 03:08:48Z: the run fails with Claude timed out while registering its MCP tools.

    Expected: when an interrupt fails, T3 kills the old provider process (or stops its scope) before it resumes the same session in a new one. Two processes writing to one Claude session can corrupt the transcript.

    The trigger here was environmental: --add-dir pointed at the hung FUSE mount, so the new process couldn't finish starting either. A plain hang in a tool call should cause the same divergence.

    🤖 Generated with Claude Code

  2. cmaciasjimenez commented on Nov 3, 2026

    @cmaciasjimenez

    Another case, on 0.1.7-nightly.20261103.4119 (cefb544, currently the tip of main): macOS arm64, desktop app with local backend, claudeAgent, claude-opus-5-5. The trigger here is different. It is steering a queued message past the automatic "on return" idle compaction, not Stop or rollback. The logs pin down the interleaving this issue left unverified.

    Sequence (UTC, 2026-11-03, from the event store, effect outbox, server trace and the Claude provider log)

    09:46:44.514  run 120 created: client-sent `/compact` (idleCompaction: "onReturn"), user returning to an idle thread
    09:46:44.609  run 121 created: the user's message, queued behind it
    09:46:44.656  run 120 -> running; its provider-turn.start effect is running (Claude query.open with resume, ~1 s)
    09:46:45.354  queued-message.promote-to-steer (queued run 121, target run 120) arrives over ws
                  (the user clicked "Skip compaction and send this message" in the queued-runs control)
    09:46:45.659  run 120's provider-turn.start effect settles `succeeded`: prompt.offer "/compact" sent to Claude
    09:46:45.661  provider-turn.updated (run 120, status running)
    09:46:45.671  Claude command_lifecycle started; 45.735 status=compacting
    09:46:45.739  steer command commits (dispatch span 45.355 -> 45.739):
                    run 120 "Interrupt requested: Skipped compaction to send a queued message"
                    run 120 "Run interrupted before provider start"; run, attempt and node -> interrupted
                  No provider-turn.interrupt effect is enqueued. In the outbox, run 120's start effect is still
                  `succeeded`, so cancelUnsettled matched nothing.
    09:46:45.890  run 121 start rejected in the adapter: previous_turn_active
    09:46:55 - 09:47:20  runs 122-125 (two user retries, each with its own onReturn `/compact`) all rejected the
                  same way. None of them reached Claude (no prompt.offer in the provider log).
    09:47:52.289  Claude's original `/compact` finishes: compact_result=success, 218,899 -> 16,217 tokens, 66 s.
    

    Why the interrupt missed

    queued-message.promote-to-steer -> stopRunForQueuedCommand -> dispatchRunInterrupt. When the projection was read, run 120 was running but had no provider turn for its active attempt yet: the Claude resume takes about 1 s, and the provider turn is recorded only once the prompt is offered. So it took the providerTurn === undefined branch (Orchestrator.ts#L11405). That branch stops the provider only by cancelling unsettled provider-turn.start / provider-turn.restart effects (Orchestrator.ts#L11552), and cancelUnsettled only matches pending/running rows (EffectOutbox.ts#L452). The start effect had settled 80 ms before the command committed, so nothing was cancelled and no interrupt reached Claude. The adapter's activeTurn stayed set (ClaudeAdapterV2.ts#L10711), and every start was refused until the compaction ended on its own.

    The "before provider start" decision is made on a snapshot that can be stale by commit time. A start effect that settles in that window should lead to a real provider-turn.interrupt, not just a cancellation of effects that have already finished.

    Knock-on effect (related: #28178)

    At run 124, ProviderTurnStartService logged Recovering provider context in a fresh native session with reason: "uncertain_history_delivery", errorTag: "ProviderAdapterTurnStartError" (ProviderTurnStartService.ts#L1326). The thread's nativeThreadRef moved to a new, never-created session id, and a full_thread_summary handoff (runs 1-123) was queued with the delta handoffs. The UI showed a "Context handoff Opus 5.5 -> Opus 5.5" divider. So the native Claude session, which went on to compact successfully, is abandoned. The failures behind this were only local previous_turn_active refusals, and none of them reached the provider.

    Repro (from this one incident; not independently reproduced)

    1. Claude thread, idle compaction set to "On return".
    2. Leave the thread idle past the prompt-cache window, then send a message. T3 sends /compact first and queues the message.
    3. Within about 1 s, before "Compacting context" appears, click "Skip compaction and send this message" on the queued message. That was the trigger here; Cmd+Shift+Enter sends the same command.
    4. Result: "Provider error: The provider could not start this turn: the previous turn is still running." on the message and on every retry until the compaction finishes. The longer the Claude resume takes, the wider the window.

    Posted from a t3 triage session by Claude Code (Claude Opus 5.5, claude-opus-5-5).

    Image
  3. Chinoman10 commented on Nov 6, 2026

    @Chinoman10
    Contributor

    Corroborating this with Pi, on a Linux server running a locally patched T3 Code 0.1.6. The installed bundle already contains #27141's superseded-turn lookup. This is the same accepted-start / application-stop divergence as the November 3 comment, although the original Stop snapshot timing was not retained, so I cannot prove every internal interleaving from that incident alone.

    Sanitized persisted ordering, all UTC on 2026-11-05:

    12:25:46.448  Native provider turn's startedAt.
    12:25:46.449  provider-turn.start effect succeeded.
    12:25:46.457  provider-turn.updated/running persisted.
    12:25:46.466  provider-thread.updated/active persisted.
    12:25:46.754  thread.stop accepted; run/attempt marked interrupted,
                  result says "Run interrupted before provider start".
                  No provider-turn.interrupt effect was queued.
    12:28:14.852  Follow-up start failed: previous_turn_active.
    

    The event sequence places the running provider-turn event before Stop's event batch, while Stop's attempt record still had no providerTurnId. Pi continued emitting actual tools after the application run was marked interrupted. A later T3 thread-Stop request returned no_active_run. Recovery required terminating only that thread's orphaned native runtime, preserving its session, then resuming successfully.

    We reproduced the missing-turn snapshot / accepted-start race deterministically on pristine upstream 337c5858c33bc7f8f6dfd391a2c279fa0f7e4d17, using the real orchestration/outbox/session services and a controlled provider. A barrier holds Stop's missing-turn snapshot, lets startup accept and succeed, then releases Stop; upstream reports a terminal interruption while the native turn remains unresolved. This is not only an adapter timeout or a Pi-specific guard problem.

    The correction is now submitted as PR #31933, exact head bac71650033ae560a28f7bf51b997b399b75ba9a. It incorporates pinned upstream 202d5b56 and preserves the newer EventSink/ProviderCommitBatch ingestion refactor rather than reverting it. Regression checks cover cancellation while startup is pending, delayed identity ingestion, exact-attempt isolation, definite refusals, teardown and late subscriptions. The first remote CI run exposed buffered-event and definite-preparation lifetime gaps; these were corrected without weakening the replay/recovery behavior assertions. The current exact-head changed-code check passed 499 tests and 36 affected-package typechecks; the previously failing targeted CI/replay/batching suites passed 213 tests locally. Fresh independent correction review approved this head and independently passed all 18 race cases plus 55 manager tests.

    An isolated actual Pi 1.5.0/Luna OAuth canary on that exact merged head confirmed authentic acceptance/streaming, nonterminal Stop, actual abort with its terminal buffered behind held ingestion, successful terminal drain/exact-runtime closure and old owner None before replacement, then replacement completion without process overlap. There were two native starts, one abort, a maximum of one live native process, and both captured PIDs were absent after cleanup. Independent review findings during development were corrected, not waived. This is controlled local evidence, not proof of every original timing or production release verification. No live backend activation, restart or selector change was performed; CI and upstream review remain separate.

    AI-assisted investigation and implementation: GPT-6.1 Sol through Pi in T3 Code; bounded native canary used GPT-6 Luna through Pi subscription authentication. No private conversation contents, credentials or raw provider traces are included here.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions