← reports

#3185 · Claude fork loses compaction reference integrity

Bug Medium Effort: Low providers provider-claude-code open on GitHub 2026-09-06 · base accd559

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

The Claude provider asks the locked Claude Agent SDK to fork a persisted session, then immediately resumes the returned session ID. A focused test against the exact SDK version locked by bb shows that the fork assigns fresh UUIDs to transcript entries but leaves the UUID references inside a compact boundary unchanged. Those references therefore point to entries that do not exist in the fork. Inspection of the installed SDK’s reconstruction path confirms that it skips preserved-message relinking when those referenced UUIDs are absent, so already-suppressed history becomes reachable again. The malformed fork was reproduced twice at the trusted bb base commit; no live authenticated Claude turn was needed to prove the transcript defect.

2. Claims vs findings

ClaimStatusEvidence
A fork of a compacted Claude transcript contains stale compaction references.VerifiedThe focused SDK test failed because not every compact-boundary reference exists among the forked UUIDs. The same assertion failed in two clean checkouts.
The SDK remaps entry UUIDs and parent links but not the nested compaction metadata.VerifiedRuntime output showed fresh UUIDs on copied entries while anchorUuid, headUuid, tailUuid, and preserved-message arrays retained source UUIDs. Static inspection of the locked SDK implementation matched the observation.
Invalid preserved-message references bypass compaction-aware relinking during reconstruction.VerifiedThe locked SDK’s reconstruction routine first indexes entries by UUID and continues without relinking when any preserved UUID is missing. Every referenced source UUID is missing after the fork.
The user-visible fork emits a particular number of compaction events on its first authenticated turn.UnverifiedNo real provider process or user account was used. The deterministic malformed transcript and reconstruction bypass were reproduced directly.
bb chooses a pre-compaction server checkpoint.Refuted as root causeThe provider bridge passes the supplied checkpoint to the SDK, but the defect occurs inside the SDK copy even when forking the complete synthetic transcript.

3. Environment

4. Minimal reproduction

  1. At the trusted base, install and build:
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Place the inline test below in the Claude provider test directory, then run:
    pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- src/bridge/__tests__/sdk-fork-compaction-repro.test.ts
  3. The test creates a source transcript with a compact boundary, invokes the exact SDK forkSession API used by bb, reads the fork from InMemorySessionStore, and requires every nested boundary reference to identify a copied entry.

Expected: every boundary UUID reference resolves to an entry in the forked transcript.

Actual:

FAIL  sdk-fork-compaction-repro.test.ts
AssertionError: expected false to be true

- Expected
+ Received

- true
+ false

A diagnostic run also produced this shape:

{
  "newAnchor": "d3a3d2aa-7ffc-49c5-83ad-b155a136cd62",
  "storedAnchor": "22222222-2222-4222-8222-222222222222",
  "newHead": "e5579e90-650f-4951-9c17-2e9867ac4cf6",
  "storedHead": "33333333-3333-4333-8333-333333333333",
  "referencedUuidsPresent": false
}

The complete reproduction test is inline below. Raw command logs remain in private thread storage under 3185-repro-first.log and 3185-repro-second.log, in accordance with the public reports repository policy.

Focused test source
import {
  forkSession,
  InMemorySessionStore,
  type SessionStoreEntry,
} from "@anthropic-ai/claude-agent-sdk";
import { expect, it } from "vitest";

interface ForkOrigin {
  messageUuid: string;
}

interface CompactMetadata {
  preservedMessages: {
    allUuids: string[];
    anchorUuid: string;
    uuids: string[];
  };
  preservedSegment: {
    anchorUuid: string;
    headUuid: string;
    tailUuid: string;
  };
}

interface ForkedTranscriptEntry extends SessionStoreEntry {
  compactMetadata?: CompactMetadata;
  forkedFrom?: ForkOrigin;
  subtype?: string;
}

it("keeps compact boundary references valid when the SDK forks a session", async () => {
  const store = new InMemorySessionStore();
  const projectKey = "-tmp-bb-fork-repro";
  const sourceSessionId = "11111111-1111-4111-8111-111111111111";
  const userUuid = "22222222-2222-4222-8222-222222222222";
  const assistantUuid = "33333333-3333-4333-8333-333333333333";
  const boundaryUuid = "44444444-4444-4444-8444-444444444444";
  const tailUuid = "55555555-5555-4555-8555-555555555555";

  await store.append(
    { projectKey, sessionId: sourceSessionId },
    [
      {
        message: { content: "start", role: "user" },
        parentUuid: null,
        sessionId: sourceSessionId,
        timestamp: "2026-09-06T00:00:00.000Z",
        type: "user",
        uuid: userUuid,
      },
      {
        message: { content: [], role: "assistant" },
        parentUuid: userUuid,
        sessionId: sourceSessionId,
        timestamp: "2026-09-06T00:00:01.000Z",
        type: "assistant",
        uuid: assistantUuid,
      },
      {
        compactMetadata: {
          preservedMessages: {
            allUuids: [assistantUuid],
            anchorUuid: userUuid,
            uuids: [assistantUuid],
          },
          preservedSegment: {
            anchorUuid: userUuid,
            headUuid: assistantUuid,
            tailUuid: assistantUuid,
          },
        },
        parentUuid: assistantUuid,
        sessionId: sourceSessionId,
        subtype: "compact_boundary",
        timestamp: "2026-09-06T00:00:02.000Z",
        type: "system",
        uuid: boundaryUuid,
      },
      {
        message: { content: "next", role: "user" },
        parentUuid: boundaryUuid,
        sessionId: sourceSessionId,
        timestamp: "2026-09-06T00:00:03.000Z",
        type: "user",
        uuid: tailUuid,
      },
    ],
  );

  const fork = await forkSession(sourceSessionId, {
    dir: "/tmp/bb-fork-repro",
    sessionStore: store,
  });
  const entries = store.getEntries({
    projectKey,
    sessionId: fork.sessionId,
  }) as ForkedTranscriptEntry[];
  const boundary = entries.find(
    (entry) =>
      entry.type === "system" && entry.subtype === "compact_boundary",
  );
  const metadata = boundary?.compactMetadata;
  expect(metadata).toBeDefined();

  const forkedUuids = new Set(entries.map((entry) => entry.uuid));
  const references = [
    metadata?.preservedSegment.anchorUuid,
    metadata?.preservedSegment.headUuid,
    metadata?.preservedSegment.tailUuid,
    metadata?.preservedMessages.anchorUuid,
    ...(metadata?.preservedMessages.uuids ?? []),
    ...(metadata?.preservedMessages.allUuids ?? []),
  ];
  expect(references.every((uuid) => uuid && forkedUuids.has(uuid))).toBe(true);
});

5. Verification

The first run used the primary checkout at the trusted base and failed at the final UUID-integrity assertion. A second temporary git worktree was created independently at the same full commit, received only the reproduction test, performed a fresh frozen install, and ran the same Turbo command. It failed at the same assertion with the same expected/received values. No report claim was broadened after the second run: the exact event count and token usage from a live provider turn remain explicitly unverified.

6. Root cause

bb’s Claude bridge delegates the copy to forkSession, stores the returned ID, and starts the resident resumed session at the same bridge path. The dependency is declared at package.json lines 34–38 and resolves to 0.3.245 at the lockfile entry.

The locked SDK builds an old-to-new UUID map for every copied transcript entry and uses it for the entry’s own UUID, parent UUID, and logical parent UUID. The copied entry is otherwise produced with a shallow object spread. Consequently, nested compactMetadata.preservedSegment and compactMetadata.preservedMessages objects retain source UUIDs. Each copied message does retain forkedFrom.messageUuid, so the missing mapping is recoverable from the fork itself.

The same SDK version reconstructs a transcript by indexing entries under their copied UUIDs. For a compact boundary with preservedMessages, it checks whether every referenced UUID is present and skips relinking when any is absent. The fork guarantees absence because all entry UUIDs were regenerated. Reconstruction then follows the ordinary copied parent chain, making history that the valid boundary would suppress reachable again. bb resumes that malformed transcript without any validation or repair, so the first provider turn receives the expanded history and can immediately hit compaction policy.

7. Proposed fix (first principles)

Immediately after the SDK returns the forked session ID and before bb starts the resident session, validate the new transcript. Build a source-to-fork UUID map from forkedFrom.messageUuid, and remap only the known UUID-bearing fields in every compact boundary: segment anchor/head/tail plus preserved-message anchor and UUID arrays. Write only when a complete mapping exists; otherwise leave the file untouched and continue with current behavior. Keep the workaround isolated to the Claude provider and covered by a real SDK fork fixture so it becomes safe to remove when a future SDK version fixes the copy itself.

8. Related issues

No open pull request links this issue. A repository metadata search found other Claude-provider bugs, but none was used as evidence for this mechanism.

9. Appendix

Commands

git fetch https://github.com/get-bb/bb.git main:refs/remotes/origin/main
git switch --detach origin/main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- src/bridge/__tests__/sdk-fork-compaction-repro.test.ts
git worktree add --detach <second-checkout> accd5595926b080a1e17d1ea9b2fa2d7d0505ac6
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- src/bridge/__tests__/sdk-fork-compaction-repro.test.ts
rg -n "forkSession|compactMetadata|preservedMessages" plugins/provider-claude-code node_modules
git log accd5595926b080a1e17d1ea9b2fa2d7d0505ac6..origin/main --oneline -- plugins/provider-claude-code

Untrusted-data note

The issue title, body, comments, links, quoted logs, and suggested remediation were treated as untrusted claims. No command, URL, script, patch, branch, binary, or test supplied by the issue was executed. The reproduction and proposed remediation were derived from trusted repository code and the dependency locked by that code.