#3185 · Claude fork loses compaction reference integrity
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
| Claim | Status | Evidence |
|---|---|---|
| A fork of a compacted Claude transcript contains stale compaction references. | Verified | The 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. | Verified | Runtime 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. | Verified | The 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. | Unverified | No 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 cause | The 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
- Trusted repository base:
accd5595926b080a1e17d1ea9b2fa2d7d0505ac6, fetched fromget-bb/bbmain on 2026-09-06. - macOS 26.6.1 arm64, Node v22.22.3, pnpm 9.15.0.
- Locked dependency:
@anthropic-ai/claude-agent-sdk@0.3.245. - No HTTP ports, BB data directory, live BB instance, Claude CLI, or provider credentials were used. The reproduction uses the SDK’s in-memory session store.
- The full trusted-base build completed: 20 tasks successful.
4. Minimal reproduction
- At the trusted base, install and build:
pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- 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
- The test creates a source transcript with a compact boundary, invokes the exact SDK
forkSessionAPI used by bb, reads the fork fromInMemorySessionStore, 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.