From 311fccad7fa7a39858e76cd5c1c2408aef151a01 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 11:44:36 +0000 Subject: [PATCH 1/3] test(server): reproduce #27552, forks lose their compaction chain Forking a compacted Claude session through the history worker remaps message UUIDs but leaves compact_boundary preservedSegment and preservedMessages pointing at the source session's UUIDs. This test fails on the pinned Claude Agent SDK (0.3.276) and passes with 0.3.284. Co-Authored-By: Claude Opus 5.5 --- .../ClaudeAgentSdkQueryRunner.test.ts | 124 +++++++++++++++--- 1 file changed, 104 insertions(+), 20 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts index dac2095d0d22..a1761a256bb7 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts @@ -36,32 +36,34 @@ vi.mock("@anthropic-ai/claude-agent-sdk", { spy: true }); const FORK_SESSION_ID = "11111111-1111-4111-8111-111111111111"; -const writeClaudeSession = Effect.fnUntraced(function* (configDir: string) { +const sessionEntry = (uuid: string, parentUuid: string | null, message: unknown) => ({ + type: parentUuid === null ? "user" : "assistant", + uuid, + parentUuid, + sessionId: FORK_SESSION_ID, + cwd: "/repo", + isSidechain: false, + timestamp: "2026-10-01T00:00:00.000Z", + message, +}); + +const writeClaudeSession = Effect.fnUntraced(function* ( + configDir: string, + entries: ReadonlyArray = [ + sessionEntry("aaaaaaaa-0000-4000-8000-000000000001", null, { role: "user", content: "hello" }), + sessionEntry("aaaaaaaa-0000-4000-8000-000000000002", "aaaaaaaa-0000-4000-8000-000000000001", { + role: "assistant", + content: [{ type: "text", text: "hi" }], + }), + ], +) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const projectDir = path.join(configDir, "projects", "-repo"); - const entry = (uuid: string, parentUuid: string | null, message: unknown) => ({ - type: parentUuid === null ? "user" : "assistant", - uuid, - parentUuid, - sessionId: FORK_SESSION_ID, - cwd: "/repo", - isSidechain: false, - timestamp: "2026-10-01T00:00:00.000Z", - message, - }); yield* fileSystem.makeDirectory(projectDir, { recursive: true }); yield* fileSystem.writeFileString( path.join(projectDir, `${FORK_SESSION_ID}.jsonl`), - [ - entry("aaaaaaaa-0000-4000-8000-000000000001", null, { role: "user", content: "hello" }), - entry("aaaaaaaa-0000-4000-8000-000000000002", "aaaaaaaa-0000-4000-8000-000000000001", { - role: "assistant", - content: [{ type: "text", text: "hi" }], - }), - ] - .map((line) => JSON.stringify(line)) - .join("\n"), + entries.map((line) => JSON.stringify(line)).join("\n"), ); return projectDir; }); @@ -111,6 +113,88 @@ it.effect("forks a session stored under the instance's own CLAUDE_CONFIG_DIR", ( }).pipe(Effect.provide(forkRunnerLayer)), ).pipe(Effect.provide(NodeServices.layer)), ); +it.effect("forked Claude sessions keep their compaction chain", () => + Effect.scoped( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const configDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-claude-fork-compact-", + }); + const uuid = (n: number) => `bbbbbbbb-0000-4000-8000-00000000000${n}`; + const turn = (id: number, parent: string | null, role: "user" | "assistant") => ({ + ...sessionEntry(uuid(id), parent, { + role, + content: role === "user" ? `question ${id}` : [{ type: "text", text: `answer ${id}` }], + }), + type: role, + }); + const projectDir = yield* writeClaudeSession(configDir, [ + turn(1, null, "user"), + turn(2, uuid(1), "assistant"), + turn(3, uuid(2), "user"), + turn(4, uuid(3), "assistant"), + { + ...sessionEntry(uuid(5), null, undefined), + type: "system", + subtype: "compact_boundary", + content: "Conversation compacted", + logicalParentUuid: uuid(4), + compactMetadata: { + trigger: "manual", + preTokens: 19979, + preservedSegment: { headUuid: uuid(3), anchorUuid: uuid(6), tailUuid: uuid(4) }, + preservedMessages: { + anchorUuid: uuid(6), + uuids: [uuid(3), uuid(4)], + allUuids: [uuid(3), uuid(4)], + }, + postTokens: 3589, + }, + }, + { + ...turn(6, uuid(5), "user"), + isCompactSummary: true, + isVisibleInTranscriptOnly: true, + }, + turn(7, uuid(4), "user"), + turn(8, uuid(7), "assistant"), + ]); + + const runner = yield* ClaudeAgentSdkQueryRunner; + const forked = yield* runner.forkSession({ + sessionId: FORK_SESSION_ID, + options: { dir: "/repo" }, + environment: { ...process.env, CLAUDE_CONFIG_DIR: configDir }, + threadId: ThreadId.make("thread-fork-compaction"), + providerSessionId: ProviderSessionId.make("session-fork-compaction"), + }); + + const entries: ReadonlyArray<{ + readonly uuid?: string; + readonly subtype?: string; + readonly compactMetadata?: { + readonly preservedSegment: Record<"headUuid" | "anchorUuid" | "tailUuid", string>; + readonly preservedMessages: { readonly anchorUuid: string; readonly uuids: string[] }; + }; + }> = (yield* fileSystem.readFileString(path.join(projectDir, `${forked.sessionId}.jsonl`))) + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const forkedUuids = new Set(entries.map((entry) => entry.uuid)); + const boundary = entries.find((entry) => entry.subtype === "compact_boundary"); + assert.isDefined(boundary?.compactMetadata); + const { preservedSegment, preservedMessages } = boundary!.compactMetadata!; + const danglingRefs = [ + ...Object.values(preservedSegment), + preservedMessages.anchorUuid, + ...preservedMessages.uuids, + ].filter((ref) => !forkedUuids.has(ref)); + assert.deepEqual(danglingRefs, [], "compaction refs missing from the forked transcript"); + }).pipe(Effect.provide(forkRunnerLayer)), + ).pipe(Effect.provide(NodeServices.layer)), +); + const realQueryRunner = layerQueryRunner.pipe( Layer.provide(PortScanner.layerTest), Layer.provide( From f7bc1f0534ac0ee6705a26d459814947b7f977c4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 11:51:28 +0000 Subject: [PATCH 2/3] fix(server): forked Claude threads keep their compaction chain Forking a compacted Claude thread left the source session's UUIDs in the fork's compact_boundary metadata (preservedSegment, preservedMessages), so Claude could not rebuild the compacted chain and the fork's first turn sent a different context than the source. forkSession in @anthropic-ai/claude-agent-sdk before 0.3.284 did not remap those refs. Bump the SDK to 0.3.284, the first release that remaps them. It is past the release-age window, so the stale 0.3.276 minimumReleaseAgeExclude entry goes. Co-Authored-By: Claude Opus 5.5 --- apps/server/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 789d612e6fe3..e576e064755d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -33,7 +33,7 @@ "generate:expo-schema": "node scripts/fetch-expo-schema.ts" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.3.276", + "@anthropic-ai/claude-agent-sdk": "^0.3.284", "@cursor/sdk": "1.0.38", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 381ff78688eb..f3734e81588d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -826,8 +826,8 @@ importers: apps/server: dependencies: '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.276 - version: 0.3.276(@anthropic-ai/sdk@0.93.0(zod@4.6.5))(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@4.6.5))(zod@4.6.5) + specifier: ^0.3.284 + version: 0.3.284(@anthropic-ai/sdk@0.93.0(zod@4.6.5))(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@4.6.5))(zod@4.6.5) '@cursor/sdk': specifier: 1.0.38 version: 1.0.38(zod@4.6.5) @@ -2574,8 +2574,8 @@ packages: pg: optional: true - '@anthropic-ai/claude-agent-sdk@0.3.276': - resolution: {integrity: sha512-Dic43v4uuGLhibPAArWy3RSfq3zHdn+4Oh2AI2/18a5tlbNKkvChEpazyp8c+grUJvzxkzt0jETCa1wrADbkyw==} + '@anthropic-ai/claude-agent-sdk@0.3.284': + resolution: {integrity: sha512-NSoJwEq6nFSf8dtaacYx37QdGgqApI3eHFUlxclMLYi8irb6ZJwEaUnPnLUCyAyg9W/tMkgZC0GXWLRCc01I0w==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -12525,7 +12525,7 @@ snapshots: '@effect/sql-pg': 4.0.5(effect@4.0.5) pg: 8.23.0 - '@anthropic-ai/claude-agent-sdk@0.3.276(@anthropic-ai/sdk@0.93.0(zod@4.6.5))(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@4.6.5))(zod@4.6.5)': + '@anthropic-ai/claude-agent-sdk@0.3.284(@anthropic-ai/sdk@0.93.0(zod@4.6.5))(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@4.6.5))(zod@4.6.5)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.6.5) '@modelcontextprotocol/sdk': 1.29.0(supports-color@8.1.1)(zod@4.6.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index acd8c742f836..72e0a09d6e6e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -171,7 +171,6 @@ minimumReleaseAgeExclude: - expo-sqlite@58.0.13 - expo-symbols@58.0.4 - electron@44.10.0 - - "@anthropic-ai/claude-agent-sdk@0.3.276" - "@shadcn/lint@0.1.5" - cn@0.3.2 - uniwind@1.13.0 From d8c3429173738c529b680409ebc93dd0d1c5da75 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 12:03:52 +0000 Subject: [PATCH 3/3] test(server): also check preservedMessages.allUuids in the fork Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAgentSdkQueryRunner.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts index a1761a256bb7..d00d3eb231c0 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAgentSdkQueryRunner.test.ts @@ -175,7 +175,11 @@ it.effect("forked Claude sessions keep their compaction chain", () => readonly subtype?: string; readonly compactMetadata?: { readonly preservedSegment: Record<"headUuid" | "anchorUuid" | "tailUuid", string>; - readonly preservedMessages: { readonly anchorUuid: string; readonly uuids: string[] }; + readonly preservedMessages: { + readonly anchorUuid: string; + readonly uuids: string[]; + readonly allUuids: string[]; + }; }; }> = (yield* fileSystem.readFileString(path.join(projectDir, `${forked.sessionId}.jsonl`))) .trim() @@ -189,6 +193,7 @@ it.effect("forked Claude sessions keep their compaction chain", () => ...Object.values(preservedSegment), preservedMessages.anchorUuid, ...preservedMessages.uuids, + ...preservedMessages.allUuids, ].filter((ref) => !forkedUuids.has(ref)); assert.deepEqual(danglingRefs, [], "compaction refs missing from the forked transcript"); }).pipe(Effect.provide(forkRunnerLayer)),