Skip to content

[Bug]: Forking a running Claude thread replaces native history with a context handoff #34856

Open

Description

@Alb11747

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. Use the Claude provider and let a turn finish.
  2. Send another message in that thread and leave it running.
  3. Fork from the finished turn, or open a side chat.
  4. Send the fork's first message before the parent finishes.

Expected behavior

The fork resumes a native copy of the Claude session up to the finished turn, with its real tool calls, outputs and prompt cache, while the parent keeps running.

Actual behavior

The fork starts a new Claude session whose first input is a T3-written "Fork context" summary (full_thread_summary). In my case the handoff kept 5 items and dropped 6, and native tool and reasoning state was not carried over. Idle forks of the same thread fork natively.

The cause is the busy-source rule added in #26901: decideForkExecution requires !input.sourceBusy, and startup checks again. #26901 gives the reason that Claude cannot fork a running session.

Why Claude may not need the fallback

The Agent SDK's forkSession doesn't use the live session. It reads the session's .jsonl transcript, cuts it at upToMessageId, rewrites UUIDs, and writes a new transcript. It has no check that the source is idle. A finished turn's messages are already in the transcript, so forking at that point should not need the source to stop.

The adapter's own guards in ClaudeAdapterV2.forkThread (the active-turn refusal and closeLiveQueryForNativeThread) look at the calling session's state. For a new fork, that is the target session, not the running parent. So I couldn't confirm from the code why a busy Claude fork failed before #26901.

#29239 (open, waiting for maintainer confirmation) adds an optional provider capability for native forks while the source runs, and enables it for Codex only. If that lands, Claude could set the same capability, possibly after an adapter change.

Open questions before enabling it for Claude

  • What failed when a Claude fork of a running thread was tried before feat: side chats beside the main thread #26901? A repro or log would show whether the limit is in the SDK, the CLI, or the adapter.
  • Does forkSession handle a source transcript that the running session is still appending to, including a half-written last line?
  • Is the fork's upToMessageId always a message in the finished turn, so nothing from the running turn gets in?

Impact

Minor bug or occasional failure

Version or commit

Seen on a personal fork built on nightly 0.1.8-nightly.20261109. The rule is unchanged on upstream 3a30b011c3.

Workaround

Wait for the parent to go idle before sending the fork's or side chat's first message.

Filed by Claude Opus 5.5 through Claude Code in T3 Code.

Activity

  1. Alb11747 commented on Nov 13, 2026

    @Alb11747
    ContributorAuthor

    An SDK-level test shows that Claude can natively fork a finished turn while a later turn in the source session is still running. This answers the second and third open questions above. I didn't test T3's adapter path, so the first question (what failed before #26901) is still open.

    Setup: @anthropic-ai/claude-agent-sdk 0.3.321 (the version upstream locks), Claude Code CLI 2.1.322, real claude-haiku-5-5 turns. Like T3, it uses one streaming query() for the source session, cuts the fork at the finished turn's last assistant message uuid with forkSession(sessionId, { upToMessageId }), and resumes the fork in a separate query({ resume }).

    1. Turn 1 stores a codeword.
    2. Turn 2 runs sleep 20 && echo SLEPT through Bash.
    3. Turn 3 streams a long paragraph about tide pools.

    Results (two full runs):

    • Fork while turn 2's Bash tool was running: succeeded at once (about 7 ms). The forked transcript held only turn 1, with no trace of the sleep command or its output.
    • Fork while turn 3's text was streaming: succeeded. It also held only turn 1.
    • Forks in a loop during the busy turns, to catch a half-written transcript line: 7,103/7,103 and 1,500/1,500 succeeded. Afterward the source transcript had 0 unparseable lines.
    • Source session: unaffected. Turn 2 returned SLEPT and turn 3 completed in the same live query.
    • Resumed forks: asked for the codeword and whether they had run shell commands or written about tide pools, both answered "PINEAPPLE … no".

    So the SDK and CLI don't need the source to be idle, and the busy-source fallback is stricter than Claude requires. If #29239 lands, Claude could probably advertise the same capability. That would still need a T3-level check of ClaudeAdapterV2.forkThread with a busy parent.

    Posted by Claude Opus 5.5 on behalf of Alb11747.

  2. Alb11747 commented on Nov 13, 2026

    @Alb11747
    ContributorAuthor

    I've prepared a fix for maintainers to use or ignore: Alb11747:fix/claude-busy-fork. It's one commit, aef9c955e0, stacked on #29239 because it uses that PR's canForkWhileRunning capability. Compare it with #29239's head here: Alb11747/t3code@fix/codex-busy-fork...fix/claude-busy-fork

    Change:

    • ClaudeAdapterV2 declares canForkWhileRunning: true. Claude runs one provider session per thread (supportsMultipleProviderThreadsPerSession: false), so the fork runs in the target's own session. The adapter's active-turn guard and live-query close never touch the source's running turn.
    • ProviderTurnStartService passes a new optional sourceBusy on ProviderAdapterV2ForkThreadInput when later source runs are active.
    • resolveClaudeForkUpToMessageId no longer falls back to the conversation head for a busy fork whose boundary turn has no recorded assistant cursor (a synthetic turn: id). That head is usually unset, so the SDK would copy the whole transcript, including the running turn. The fork now fails, and startup takes the existing portable fallback.

    Verification:

    • A new ClaudeAdapterV2 test forks a busy source up to the boundary cursor and rejects a cursorless boundary. It fails without the adapter change.
    • The busy side-chat case in ProviderTurnStartService.test.ts now asserts that sourceBusy is passed.
    • ClaudeAdapterV2.test.ts, ProviderTurnStartService.test.ts and CommandPolicy.test.ts pass (473 tests), and so does vp run check:changed.
    • The SDK-level results above cover the busy fork itself.

    Not verified: an end-to-end busy fork through a running T3 server with live Claude turns.

    Known limitation: a busy fork from a turn with no main-thread assistant message retries the deterministic adapter error before falling back with "Native fork failed". Such turns end before Claude writes any reply, so this should be rare. Routing that case straight to portable context would need the policy to tell Claude's synthetic turn refs apart from real ones.

    Posted by Claude Opus 5.5 on behalf of Alb11747.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions