diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts index 68bc0f7d1edccdcf5d48d3ab3945c1d207c5c9e5..21b3254b6d63546c63f9350aa54b67e11d7496f6 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -386,7 +386,7 @@ describe("ClaudeCodeAdapterLive", () => { provider: "claudeCode", resumeCursor: { threadId: "resume-thread-1", - resume: "resume-session-1", + resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }, @@ -395,13 +395,13 @@ describe("ClaudeCodeAdapterLive", () => { assert.equal(session.threadId, "resume-thread-1"); assert.deepEqual(session.resumeCursor, { threadId: "resume-thread-1", - resume: "resume-session-1", + resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }); const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.resume, "resume-session-1"); + assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); assert.equal(createInput?.options.resumeSessionAt, "assistant-99"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), @@ -409,6 +409,28 @@ describe("ClaudeCodeAdapterLive", () => { ); }); + it.effect("does not synthesize resume session id from generated thread ids", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + provider: "claudeCode", + }); + + assert.equal( + "resume" in (session.resumeCursor as Record), + false, + ); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.resume, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { const harness = makeHarness(); return Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts index 7ea84f9bda7d99756e1837a4eb0bbb384d6dec7d..6975b5ee7a67d4fcc1f64aed2740018ea374b8f8 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts @@ -88,6 +88,7 @@ interface ClaudeSessionContext { readonly promptQueue: Queue.Queue; readonly query: ClaudeQueryRuntime; readonly startedAt: string; + resumeSessionId: string | undefined; readonly pendingApprovals: Map; readonly turns: Array<{ id: ReturnType; @@ -115,6 +116,12 @@ export interface ClaudeCodeAdapterLiveOptions { }) => ClaudeQueryRuntime; } +function isUuid(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + value, + ); +} + function toMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; @@ -148,12 +155,13 @@ function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undef }; const threadId = typeof cursor.threadId === "string" ? cursor.threadId : undefined; - const resume = + const resumeCandidate = typeof cursor.resume === "string" ? cursor.resume : typeof cursor.sessionId === "string" ? cursor.sessionId : undefined; + const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; const resumeSessionAt = typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; @@ -351,7 +359,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { const resumeCursor = { threadId, - resume: threadId, + ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), turnCount: context.turns.length, }; @@ -369,6 +377,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { ): Effect.Effect => Effect.gen(function* () { const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id); + context.resumeSessionId = message.session_id; const changed = context.session.threadId !== nextThreadId; if (changed) { @@ -935,7 +944,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { threadId, resumeCursor: { threadId, - resume: resumeState?.resume ?? threadId, + ...(resumeState?.resume ? { resume: resumeState.resume } : {}), ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), @@ -950,6 +959,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { promptQueue, query: queryRuntime, startedAt, + resumeSessionId: resumeState?.resume, pendingApprovals, turns: [], inFlightTools,