Skip to content

[Bug]: Claude rewind fails after a /compact that preserved recent messages, and leaves the forked transcript on disk #13638

Description

@Zane-0x5a

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 Claude (claudeAgent) thread, run a manual /compact that keeps recent messages verbatim, so the compact_boundary entry gets compactMetadata.preservedSegment / preservedMessages.
  2. Keep working for a few more turns.
  3. Use "Edit from here" on the latest user message (rewind 1 turn).

Expected behavior

The thread rewinds to just before that message, as it does in threads that were never compacted.

Actual behavior

The rewind fails:

Provider adapter request failed (claudeAgent) for thread/rollback: Claude fork history did not preserve the retained turn boundaries.

The transcript forkSession already wrote is never removed. It is a full copy of the source transcript (107 MB in my case) left in ~/.claude/projects/<project>/, and each failed attempt leaves another one.

I measured the real session read-only with the SDK's own getSessionMessages (includeSystemMessages: true):

I ported remapClaudeForkTurnBoundaries from main @ 1c12706 and ran it against the same two transcripts: retained conversation 389, fork conversation 388, offset -1, rejected. The rejection is right, because the fork really did lose a retained assistant message. So on main, rewind stays broken for any thread with a preserved-segment compaction, as long as it goes through the fork helper and the SDK bug is open.

Possible fixes (none need the SDK fix)

  1. Rewind in place with resume + resumeSessionAt instead of forkSession(). Leave the source transcript untouched and restart with resume: <source session> and resumeSessionAt: <cut>. The CLI's own loader then truncates the transcript it wrote, and the SDK bug is specific to the standalone helper. New turns branch from the cut in the same file, and getSessionMessages already follows the newest leaf. This removes the full-transcript copy, the UUID remap, the fork verification step, and the orphaned files. Retained turn boundaries keep their original UUIDs.
    • Take the cut from durable history (getSessionMessages), not from in-memory turns. That was the recovered-session problem raised in review of fix(server): point Claude resume cursor at reverted transcript #7198.
    • Cut at the last retained assistant message rather than messages[firstRemoved - 1], so a trailing stop_hook_summary is never the cut point.
    • Persist the pinned resumeSessionAt until the first new message lands on the new branch, then clear it. That keeps the existing guard against passing a stale resumeSessionAt on ordinary resume.
    • I have not run this live against a preserved-segment compaction, so that is the case to test first.
  2. Keep forkSession() and repair the fork. The helper copies message entries in source order, so the k-th copied entry corresponds to the k-th source entry. Check each pair by type and message body, as fix(server): keep Claude rewind when fork history length changes #11954 already does, and build the old-to-new UUID map. Then rewrite preservedSegment/preservedMessages in the fork's compact_boundary and run the existing verification. Only do this when those UUIDs don't resolve in the fork, so it becomes a no-op once the SDK bug is fixed. The catch is that it depends on the SDK's private transcript layout.
  3. Either way:
    • Delete the fork transcript when verification rejects it.
    • Before forking, detect a retained compact_boundary with preserved-message metadata and fail fast with the existing "Start a new thread instead" message, instead of writing a full copy first.

Impact

Major degradation or frequent failure

Version or commit

v0.0.42 desktop (bundled @anthropic-ai/claude-agent-sdk 0.3.260). Rewind logic also checked against main @ 1c12706 (SDK ^0.3.276; the SDK issue reports the bug on 0.3.274 and 0.3.280).

Environment

Windows 11 (10.0.26200), T3 Code desktop v0.0.42, claudeAgent provider, two Stop hooks configured.

Logs or stack traces

Provider adapter request failed (claudeAgent) for thread/rollback: Claude fork history did not preserve the retained turn boundaries.

Workaround

Resend in the same thread instead of rewinding, or start a new thread.

Activity

  1. juliusmarminge commented on Sep 25, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on current main (1c12706638) and on v0.0.42 (719a76ca1d). A Claude rewind whose fork drops a retained user or assistant message fails with this error, and the fork transcript is left on disk. No other open issue tracks it.

    What the report gets right

    Rewind does not truncate the source session. It calls the standalone forkSession(sessionId, { upToMessageId }), reads the fork back, and only then swaps the live session. The cut is the entry immediately before the first removed human turn, which is a trailing stop_hook_summary when Stop hooks are on:

          const rollbackAt = retainedCount > 0 ? messages[firstRemoved - 1]?.uuid : undefined;
          // ...
            const remappedBoundaries = remapClaudeForkTurnBoundaries(
              messages,
              forkMessages,
              firstRemoved,
              retainedBoundaries,
            );
            if (!remappedBoundaries) {
              return yield* new ProviderAdapterRequestError({
                provider: PROVIDER,
                method: "thread/rollback",
                detail: "Claude fork history did not preserve the retained turn boundaries.",
              });
            }

    #11954 only makes a length change acceptable when every retained user and assistant body is still present. System entries are ignored, so a trailing stop_hook_summary disappearing from getSessionMessages does not fail the check. A shorter conversation does:

    const remapClaudeForkTurnBoundaries = (
      messages: ReadonlyArray<ClaudeHistoryMessage>,
      forkMessages: ReadonlyArray<ClaudeHistoryMessage>,
      firstRemoved: number,
      retainedBoundaries: ReadonlyArray<string | null>,
    ): Array<string | null> | undefined => {
      const retainedConversation = messages.slice(0, firstRemoved).filter(isClaudeConversationMessage);
      const forkConversation = forkMessages.filter(isClaudeConversationMessage);
      // ...
      const offset = forkConversation.length - retainedConversation.length;
      if (
        offset < 0 ||
        retainedConversation.some((message, index) => {
          const forkMessage = forkConversation[index + offset];
          return (
            forkMessage === undefined ||
            forkMessage.type !== message.type ||
            !NodeUtil.isDeepStrictEqual(forkMessage.message, message.message)
          );
        })
      ) {
        return undefined;
      }

    offset === -1 with one missing assistant body is the reject path. The compact-prefix test is the opposite shape: the fork has extra earlier user/assistant messages and alignment from the end succeeds. A preserved assistant that getSessionMessages can no longer splice in is supposed to fail.

    That splice fails because of anthropics/claude-agent-sdk-typescript#472, still open. forkSession remaps message UUIDs and parentUuid, and copies compactMetadata (preservedSegment / preservedMessages) unchanged. The pinned SDK is 0.3.276. The report reproduces the helper on 0.3.274 and 0.3.280, so a newer lockfile does not clear this. v0.0.42 already contains the same error string.

    The failure returns before stopSessionInternal, which matches the adapter tests (closeCalls stays 0, the resume cursor is unchanged). The source thread is still the one the user had. The fork file is not. Nothing in the server calls deleteSession, and the history worker only implements getSessionMessages and forkSession. Each failed attempt writes another full copy and leaves it.

    What to be careful with

    resume + resumeSessionAt is the SDK's truncation API on the Agent SDK path, and this adapter deliberately does not send it. A stored resumeSessionAt is kept on the cursor and omitted from query() ("passes Claude resume ids without pinning a stale assistant checkpoint"). #7198 added a pin and was closed as superseded by #11338. The review note on that PR is already how main behaves: the cut comes from getSessionMessages, not from the empty in-memory turn list on a recovered session. Using resumeSessionAt means a new pin that startSession actually honors, cleared after the next message, not a reuse of the current cursor field.

    Cutting at the last retained assistant is the wrong general rule for that API. SDK 0.3.276 says to fork at the kept turn's last chain entry, whatever it is. The current messages[firstRemoved - 1] is that entry. It is a bad forkSession readback point when it is a system leaf, because getSessionMessages then drops it. resumeSessionAt accepts that UUID. Cutting earlier drops a kept-turn tool_result. This path is still untested against a preserved-segment compact, as the report says.

    Repairing preservedSegment inside the fork depends on the transcript layout the SDK bug is about. A fail-fast check before forkSession also cannot see that metadata on the desktop path: a distinct CLAUDE_CONFIG_DIR reads history through the worker, and decodeSessionMessages keeps only type, uuid, parent_tool_use_id, and message.

    Workaround

    Send the next message in the same thread, or start a new thread. Rewind did not stop the source session. The extra files are the rejected fork transcripts under the Claude project directory, not the session T3 is still resumed on.

    Labels / next

    • Type: bug, accepted.
    • Labels: bug, accepted.
    • Next: a rejected fork has to be removed with deleteSession (new worker method on the isolated-home path). Rewind after a preserved-segment compact stays broken until it stops using the standalone forkSession helper, or until Add run button for runnable markdown command code blocks #472 remaps compactMetadata. Prefer resume + a one-shot resumeSessionAt taken from durable history over editing the fork file. Do not send resumeSessionAt on an ordinary resume.
  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 25, 2026
  3. cpakken commented on Sep 25, 2026

    @cpakken

    Reproduced on macOS arm64 (Darwin 27.0.0), T3 Code desktop nightly 0.0.43-nightly.20260925.2251.

    Local logs show four single-turn rollback failures across two Claude threads on September 25. Both threads previously emitted compact_boundary with preserved_segment and preserved_messages. One compaction was automatic; the other was manual.

    All four failed with: Claude fork history did not preserve the retained turn boundaries.

    This adds evidence that automatic compaction also precedes this failure. No application state or transcripts were modified during investigation.

    Investigated by Codex (GPT-6) via T3 triage.

  4. juliusmarminge commented on Oct 2, 2026

    @juliusmarminge
    Member

    Thanks for taking the time to report this and provide the details. We revisited it during the orchestrator V2 cleanup.

    V2 rollback records a native conversation-head reference and resumes at that checkpoint. It no longer calls forkSession for rewind, removing the standalone fork UUID-remapping and rejected-copy mechanism in this report.

    I’m closing this report because the implementation it targets has been replaced. That does not mean every similar symptom is fixed.

    Source reviewed.

    Explicit fork still uses forkSession and is a separate action. No live preserved-compaction rewind was run.

    If you still hit this on a current build, please reply with the app/server versions and the steps that reproduce it. We can reopen this if the original problem is still there.

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

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.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