authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-07 10:51:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 10:51:10-07:00
logf570bd21663f56ce94c41829d3b7d72886e25a34
treeddcbebf0eab2657a2377c6a3860266be5d97c158
parent41985b44ff1b3a353ab805a31d4e4810842cbf2b
signature Signed by PGP key B5690EEEBB952194

fix(server): Claude prompts no longer hang on a uuid the session already holds (#16897)

Co-authored-by: saphid <saphid@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

3 files changed, 220 insertions(+), 80 deletions(-)

apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+163-54
......@@ -2229,6 +2229,12 @@ describe("ClaudeAdapterV2 background wake turns", () => {
22292229 processQueues,
22302230 offerAndWait,
22312231 offeredMessages,
2232 // The uuid Claude echoes on the turn answering the nth offered prompt.
2233 promptUuid: (index: number) => {
2234 const uuid = offeredMessages[index]?.uuid;
2235 if (uuid === undefined) throw new Error(`No prompt offered at index ${index}.`);
2236 return uuid;
2237 },
22322238 permissionModeChanges,
22332239 continuationRequests,
22342240 events,
......@@ -4421,12 +4427,11 @@ describe("ClaudeAdapterV2 background wake turns", () => {
44214427 const continuationAttempt = RunAttemptId.make("attempt-claude-wake-callback-3");
44224428 const planToolUseId = "toolu_01WakePlanExitPlanMode";
44234429 const planMarkdown = "# Wake plan\n\n1. Report the background result.";
4424 const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) {
4425 return claudeSdkFrame({
4430 const stamp = (frame: SDKMessage, promptIndex: number) =>
4431 claudeSdkFrame({
44264432 ...frame,
4427 user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId),
4433 user_message_uuid: harness.promptUuid(promptIndex),
44284434 });
4429 });
44304435 const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`);
44314436 const planToolUse = claudeSdkFrame({
44324437 type: "assistant",
......@@ -4479,12 +4484,12 @@ describe("ClaudeAdapterV2 background wake turns", () => {
44794484 runtimePolicy,
44804485 }),
44814486 );
4482 yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt));
4487 yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, 0));
44834488 yield* Queue.offer(
44844489 harness.sdkMessages,
4485 yield* stamp(
4490 stamp(
44864491 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000740", result: "STARTED" }),
4487 firstAttempt,
4492 0,
44884493 ),
44894494 );
44904495 yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal");
......@@ -4541,22 +4546,22 @@ describe("ClaudeAdapterV2 background wake turns", () => {
45414546 // The prompt's own turn follows and echoes its uuid.
45424547 yield* Queue.offer(
45434548 harness.sdkMessages,
4544 yield* stamp(
4549 stamp(
45454550 makeAssistantTextFrame({
45464551 uuid: "00000000-0000-4000-8000-000000000743",
45474552 text: "USER_REPLY",
45484553 }),
4549 userAttempt,
4554 1,
45504555 ),
45514556 );
45524557 yield* Queue.offer(
45534558 harness.sdkMessages,
4554 yield* stamp(
4559 stamp(
45554560 makeResultFrame({
45564561 uuid: "00000000-0000-4000-8000-000000000744",
45574562 result: "USER_REPLY",
45584563 }),
4559 userAttempt,
4564 1,
45604565 ),
45614566 );
45624567 yield* awaitUntil(() => harness.terminalEvents().length === 2, "user turn terminal");
......@@ -4721,12 +4726,11 @@ describe("ClaudeAdapterV2 background wake turns", () => {
47214726 const firstAttempt = RunAttemptId.make("attempt-claude-wake-approval-1");
47224727 const userAttempt = RunAttemptId.make("attempt-claude-wake-approval-2");
47234728 const bashToolUseId = "toolu_01WakeApprovalBash";
4724 const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) {
4725 return claudeSdkFrame({
4729 const stamp = (frame: SDKMessage, promptIndex: number) =>
4730 claudeSdkFrame({
47264731 ...frame,
4727 user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId),
4732 user_message_uuid: harness.promptUuid(promptIndex),
47284733 });
4729 });
47304734
47314735 yield* harness.runtime.startTurn(
47324736 makeClaudeTestTurnInput({
......@@ -4741,16 +4745,16 @@ describe("ClaudeAdapterV2 background wake turns", () => {
47414745 );
47424746 yield* Queue.offer(
47434747 harness.sdkMessages,
4744 yield* stamp(
4748 stamp(
47454749 makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000780", text: "One." }),
4746 firstAttempt,
4750 0,
47474751 ),
47484752 );
47494753 yield* Queue.offer(
47504754 harness.sdkMessages,
4751 yield* stamp(
4755 stamp(
47524756 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000781", result: "One." }),
4753 firstAttempt,
4757 0,
47544758 ),
47554759 );
47564760 yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal");
......@@ -4845,12 +4849,11 @@ describe("ClaudeAdapterV2 background wake turns", () => {
48454849 const now = yield* DateTime.now;
48464850 const firstAttempt = RunAttemptId.make("attempt-claude-echo-debris-1");
48474851 const secondAttempt = RunAttemptId.make("attempt-claude-echo-debris-2");
4848 const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) {
4849 return claudeSdkFrame({
4852 const stamp = (frame: SDKMessage, promptIndex: number) =>
4853 claudeSdkFrame({
48504854 ...frame,
4851 user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId),
4855 user_message_uuid: harness.promptUuid(promptIndex),
48524856 });
4853 });
48544857
48554858 yield* harness.runtime.startTurn(
48564859 makeClaudeTestTurnInput({
......@@ -4865,16 +4868,16 @@ describe("ClaudeAdapterV2 background wake turns", () => {
48654868 // The first turn echoes on its first frame: this process echoes early.
48664869 yield* Queue.offer(
48674870 harness.sdkMessages,
4868 yield* stamp(
4871 stamp(
48694872 makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000760", text: "One." }),
4870 firstAttempt,
4873 0,
48714874 ),
48724875 );
48734876 yield* Queue.offer(
48744877 harness.sdkMessages,
4875 yield* stamp(
4878 stamp(
48764879 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000761", result: "One." }),
4877 firstAttempt,
4880 0,
48784881 ),
48794882 );
48804883 yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal");
......@@ -4899,16 +4902,16 @@ describe("ClaudeAdapterV2 background wake turns", () => {
48994902 yield* Queue.offer(harness.sdkMessages, staleTaskNotificationResult);
49004903 yield* Queue.offer(
49014904 harness.sdkMessages,
4902 yield* stamp(
4905 stamp(
49034906 makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000762", text: "Two." }),
4904 secondAttempt,
4907 1,
49054908 ),
49064909 );
49074910 yield* Queue.offer(
49084911 harness.sdkMessages,
4909 yield* stamp(
4912 stamp(
49104913 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000763", result: "Two." }),
4911 secondAttempt,
4914 1,
49124915 ),
49134916 );
49144917 yield* awaitUntil(() => harness.terminalEvents().length === 2, "second turn terminal");
......@@ -4944,12 +4947,11 @@ describe("ClaudeAdapterV2 background wake turns", () => {
49444947 const now = yield* DateTime.now;
49454948 const firstAttempt = RunAttemptId.make("attempt-claude-echo-1");
49464949 const secondAttempt = RunAttemptId.make("attempt-claude-echo-2");
4947 const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) {
4948 return claudeSdkFrame({
4950 const stamp = (frame: SDKMessage, promptIndex: number) =>
4951 claudeSdkFrame({
49494952 ...frame,
4950 user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId),
4953 user_message_uuid: harness.promptUuid(promptIndex),
49514954 });
4952 });
49534955 const assistantTexts = () =>
49544956 harness.events.flatMap((event) =>
49554957 event.type === "message.updated" && event.message.role === "assistant"
......@@ -4969,22 +4971,18 @@ describe("ClaudeAdapterV2 background wake turns", () => {
49694971 attachments: [],
49704972 }),
49714973 );
4972 assert.equal(
4973 harness.offeredMessages[0]?.uuid,
4974 yield* ClaudeAdapterV2.claudePromptUuid(firstAttempt),
4975 );
49764974 yield* Queue.offer(
49774975 harness.sdkMessages,
4978 yield* stamp(
4976 stamp(
49794977 makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000701", text: "One." }),
4980 firstAttempt,
4978 0,
49814979 ),
49824980 );
49834981 yield* Queue.offer(
49844982 harness.sdkMessages,
4985 yield* stamp(
4983 stamp(
49864984 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000702", result: "One." }),
4987 firstAttempt,
4985 0,
49884986 ),
49894987 );
49904988 yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal");
......@@ -5018,6 +5016,118 @@ describe("ClaudeAdapterV2 background wake turns", () => {
50185016 ),
50195017 );
50205018
5019 it.effect("gives the same run attempt a fresh prompt uuid on every offer", () =>
5020 Effect.scoped(
5021 Effect.gen(function* () {
5022 // Two servers sharing a database copy allocate identical run attempt
5023 // ids and resume the same Claude session. Claude acks a prompt whose
5024 // uuid its transcript already holds without ever running a turn.
5025 const original = yield* makeWakeHarness;
5026 const copy = yield* makeWakeHarness;
5027 const now = yield* DateTime.now;
5028 for (const harness of [original, copy]) {
5029 yield* harness.runtime.startTurn(
5030 makeClaudeTestTurnInput({
5031 threadId: harness.threadId,
5032 providerThread: harness.providerThread,
5033 now,
5034 attemptId: RunAttemptId.make("attempt-claude-shared-copy"),
5035 text: "Continue where you left off.",
5036 attachments: [],
5037 }),
5038 );
5039 }
5040 assert.notEqual(original.promptUuid(0), copy.promptUuid(0));
5041 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
5042 ),
5043 );
5044
5045 it.effect("fails a prompt Claude completes without starting a turn for it", () =>
5046 Effect.scoped(
5047 Effect.gen(function* () {
5048 // Claude answers a prompt whose uuid its transcript already holds with
5049 // a lone completed lifecycle frame and never runs a turn for it.
5050 const harness = yield* makeWakeHarness;
5051 const now = yield* DateTime.now;
5052 yield* harness.runtime.startTurn(
5053 makeClaudeTestTurnInput({
5054 threadId: harness.threadId,
5055 providerThread: harness.providerThread,
5056 now,
5057 attemptId: RunAttemptId.make("attempt-claude-duplicate-prompt"),
5058 text: "Bump this PR to the latest main.",
5059 attachments: [],
5060 }),
5061 );
5062 yield* Queue.offer(
5063 harness.sdkMessages,
5064 claudeSdkFrame({
5065 type: "command_lifecycle",
5066 command_uuid: harness.promptUuid(0),
5067 state: "completed",
5068 uuid: "00000000-0000-4000-8000-000000000790",
5069 session_id: WAKE_NATIVE_SESSION,
5070 }),
5071 );
5072 yield* awaitUntil(() => harness.terminalEvents().length === 1, "turn terminal");
5073 const [terminal] = harness.terminalEvents();
5074 assert.equal(terminal?.status, "failed");
5075 if (terminal?.status !== "failed") return;
5076 assert.equal(terminal.threadDisposition, "reusable");
5077 assert.include(terminal.failure.message, "never started a turn");
5078 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
5079 ),
5080 );
5081
5082 it.effect("keeps a started prompt running until its result", () =>
5083 Effect.scoped(
5084 Effect.gen(function* () {
5085 const harness = yield* makeWakeHarness;
5086 const now = yield* DateTime.now;
5087 yield* harness.runtime.startTurn(
5088 makeClaudeTestTurnInput({
5089 threadId: harness.threadId,
5090 providerThread: harness.providerThread,
5091 now,
5092 attemptId: RunAttemptId.make("attempt-claude-started-prompt"),
5093 text: "Bump this PR to the latest main.",
5094 attachments: [],
5095 }),
5096 );
5097 const lifecycle = (state: string, uuid: string) =>
5098 claudeSdkFrame({
5099 type: "command_lifecycle",
5100 command_uuid: harness.promptUuid(0),
5101 state,
5102 uuid,
5103 session_id: WAKE_NATIVE_SESSION,
5104 });
5105 yield* Queue.offer(
5106 harness.sdkMessages,
5107 lifecycle("queued", "00000000-0000-4000-8000-000000000791"),
5108 );
5109 yield* Queue.offer(
5110 harness.sdkMessages,
5111 lifecycle("started", "00000000-0000-4000-8000-000000000792"),
5112 );
5113 // The CLI can report the prompt completed before its turn's result.
5114 yield* Queue.offer(
5115 harness.sdkMessages,
5116 lifecycle("completed", "00000000-0000-4000-8000-000000000793"),
5117 );
5118 yield* Queue.offer(
5119 harness.sdkMessages,
5120 claudeSdkFrame({
5121 ...makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000794", result: "Done." }),
5122 user_message_uuid: harness.promptUuid(0),
5123 }),
5124 );
5125 yield* awaitUntil(() => harness.terminalEvents().length === 1, "turn terminal");
5126 assert.equal(harness.terminalEvents()[0]?.status, "completed");
5127 }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))),
5128 ),
5129 );
5130
50215131 it.effect("buffers wake output and requests a single continuation run", () =>
50225132 Effect.scoped(
50235133 Effect.gen(function* () {
......@@ -6031,12 +6141,11 @@ describe("ClaudeAdapterV2 background wake turns", () => {
60316141 const userAttempt = RunAttemptId.make("attempt-claude-wake-subagent-2");
60326142 const continuationAttempt = RunAttemptId.make("attempt-claude-wake-subagent-3");
60336143 const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`);
6034 const stamp = Effect.fnUntraced(function* (frame: SDKMessage, attemptId: RunAttemptId) {
6035 return claudeSdkFrame({
6144 const stamp = (frame: SDKMessage, promptIndex: number) =>
6145 claudeSdkFrame({
60366146 ...frame,
6037 user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId),
6147 user_message_uuid: harness.promptUuid(promptIndex),
60386148 });
6039 });
60406149
60416150 yield* harness.runtime.startTurn(
60426151 makeClaudeTestTurnInput({
......@@ -6048,12 +6157,12 @@ describe("ClaudeAdapterV2 background wake turns", () => {
60486157 attachments: [],
60496158 }),
60506159 );
6051 yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt));
6160 yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, 0));
60526161 yield* Queue.offer(
60536162 harness.sdkMessages,
6054 yield* stamp(
6163 stamp(
60556164 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000770", result: "STARTED" }),
6056 firstAttempt,
6165 0,
60576166 ),
60586167 );
60596168 yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal");
......@@ -6120,16 +6229,16 @@ describe("ClaudeAdapterV2 background wake turns", () => {
61206229 session_id: WAKE_NATIVE_SESSION,
61216230 }),
61226231 wakeResult,
6123 yield* stamp(
6232 stamp(
61246233 makeAssistantTextFrame({
61256234 uuid: "00000000-0000-4000-8000-000000000776",
61266235 text: "USER_REPLY",
61276236 }),
6128 userAttempt,
6237 1,
61296238 ),
6130 yield* stamp(
6239 stamp(
61316240 makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000777", result: "USER_REPLY" }),
6132 userAttempt,
6241 1,
61336242 ),
61346243 ];
61356244 for (const frame of wakeFrames) {
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts+5-2
......@@ -18,6 +18,7 @@ import {
1818} from "@t3tools/contracts";
1919import * as Cause from "effect/Cause";
2020import * as Duration from "effect/Duration";
21import * as Crypto from "effect/Crypto";
2122import * as Effect from "effect/Effect";
2223import * as Exit from "effect/Exit";
2324import * as Layer from "effect/Layer";
......@@ -1577,11 +1578,13 @@ async function recordClaudeStreamingQuery(input: {
15771578 };
15781579 try {
15791580 for (const [index, prompt] of input.prompts.entries()) {
1580 // Like the adapter, give each prompt a uuid Claude echoes on its turn.
1581 // Like the adapter, give each prompt a fresh uuid Claude echoes on its turn.
15811582 const message = ClaudeAdapterV2.makeClaudeUserMessage({
15821583 text: prompt,
15831584 uuid: await Effect.runPromise(
1584 ClaudeAdapterV2.claudePromptUuid(`${input.sessionId}:prompt:${index + 1}`).pipe(
1585 Crypto.Crypto.pipe(
1586 Effect.flatMap((crypto) => crypto.randomUUIDv4),
1587 Effect.filterOrFail(ClaudeAdapterV2.isClaudePromptUuid),
15851588 Effect.provide(NodeServices.layer),
15861589 ),
15871590 ),
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+52-24
......@@ -71,7 +71,6 @@ import {
7171import * as Cause from "effect/Cause";
7272import * as Context from "effect/Context";
7373import * as Crypto from "effect/Crypto";
74import * as Hex from "effect/encoding/Hex";
7574import * as DateTime from "effect/DateTime";
7675import * as Deferred from "effect/Deferred";
7776import * as Effect from "effect/Effect";
......@@ -1417,19 +1416,9 @@ const makeClaudeUserMessageWithAttachments = Effect.fnUntraced(function* (input:
14171416 } satisfies SDKUserMessage;
14181417});
14191418
1420// Stable per run attempt, so a replayed prompt offer matches its recording.
1421// Claude echoes it back as user_message_uuid on the turn that answers it.
1422export const claudePromptUuid = Effect.fn("claudePromptUuid")(function* (attemptId: string) {
1423 const crypto = yield* Crypto.Crypto;
1424 const digest = yield* crypto
1425 .digest("SHA-256", new TextEncoder().encode(`t3-claude-prompt:${attemptId}`))
1426 .pipe(Effect.orDie);
1427 const hex = Hex.encode(digest);
1428 const variant = ((Number.parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16);
1429 const uuid: NonNullable<SDKUserMessage["uuid"]> =
1430 `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1431 return uuid;
1432});
1419export function isClaudePromptUuid(value: string): value is NonNullable<SDKUserMessage["uuid"]> {
1420 return /^[^-]+-[^-]+-[^-]+-[^-]+-[^-]+$/.test(value);
1421}
14331422
14341423type ClaudeAssistantContentBlock = SDKAssistantMessage["message"]["content"][number];
14351424type ClaudeToolUseContentBlock = Extract<
......@@ -2477,14 +2466,17 @@ function claudeEchoedPromptUuids(message: SDKMessage): ReadonlyArray<string> {
24772466 return typeof uuid === "string" ? [uuid] : [];
24782467}
24792468
2480// The prompt uuid a command_lifecycle frame (queued, started, completed)
2469// The prompt a command_lifecycle frame (queued, started, completed)
24812470// acknowledges. A CLI that sends them also echoes that uuid on the result of
24822471// the turn answering the prompt; one that does not keeps the old path.
2483function claudeAcknowledgedPromptUuid(message: SDKMessage): string | null {
2472function claudePromptLifecycle(
2473 message: SDKMessage,
2474): { readonly uuid: string; readonly state: string } | null {
24842475 const type: unknown = Reflect.get(message, "type");
24852476 if (type !== "command_lifecycle") return null;
24862477 const uuid: unknown = Reflect.get(message, "command_uuid");
2487 return typeof uuid === "string" ? uuid : null;
2478 const state: unknown = Reflect.get(message, "state");
2479 return typeof uuid === "string" && typeof state === "string" ? { uuid, state } : null;
24882480}
24892481
24902482// A result that answers a turn other than the pending prompt's. An echo
......@@ -2750,6 +2742,8 @@ interface ActiveClaudeTurnContext {
27502742 // prompt's turn echoes this uuid (see handleSdkMessage).
27512743 readonly promptUuid: string | null;
27522744 promptEcho: "pending" | "confirmed";
2745 // Claude acknowledged starting the prompt (command_lifecycle "started").
2746 promptStarted: boolean;
27532747 // Root frames seen before the echo; held only when the CLI echoes early.
27542748 gatedFramesBeforeEcho: number;
27552749 readonly heldRootFrames: Array<SDKMessage>;
......@@ -6801,11 +6795,32 @@ export function makeClaudeAdapterV2(
68016795 yield* handleRoutedSdkMessage(input);
68026796 return;
68036797 }
6804 if (
6805 liveQuery.promptEchoMode === "unknown" &&
6806 claudeAcknowledgedPromptUuid(message) === context.promptUuid
6807 ) {
6808 liveQuery.promptEchoMode = "acknowledged";
6798 const lifecycle = claudePromptLifecycle(message);
6799 if (lifecycle?.uuid === context.promptUuid) {
6800 if (liveQuery.promptEchoMode === "unknown") {
6801 liveQuery.promptEchoMode = "acknowledged";
6802 }
6803 if (lifecycle.state === "started") {
6804 context.promptStarted = true;
6805 }
6806 // Claude finishes a prompt whose uuid its transcript already
6807 // holds without starting it, and no turn will ever answer it.
6808 if (lifecycle.state === "completed" && !context.promptStarted) {
6809 yield* Effect.logWarning("orchestration-v2.claude-prompt-completed-without-turn", {
6810 providerTurnId: context.providerTurnId,
6811 promptUuid: context.promptUuid,
6812 });
6813 yield* finalizeActiveTurn({
6814 context,
6815 status: "failed",
6816 completedAt: yield* DateTime.now,
6817 failure: makeProviderFailure({
6818 class: "provider_error",
6819 message: "Claude acknowledged the message but never started a turn for it.",
6820 }),
6821 });
6822 return;
6823 }
68096824 }
68106825 if (!isClaudePromptEchoGatedFrame(message)) {
68116826 // Frames the held turn produced through its own tool uses (a
......@@ -7442,10 +7457,22 @@ export function makeClaudeAdapterV2(
74427457 const startedAt = yield* DateTime.now;
74437458 const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread);
74447459 const nativeTurnId = `turn:${turnInput.attemptId}`;
7460 // Fresh for every offer: Claude acks a prompt whose uuid its
7461 // transcript already holds without running a turn, and a session
7462 // can outlive this database (or be resumed from a copy of it), so
7463 // a uuid derived from run ids can repeat.
74457464 const promptUuid = isClaudeProviderContinuationTurn(turnInput)
74467465 ? null
7447 : yield* claudePromptUuid(turnInput.attemptId).pipe(
7448 Effect.provideService(Crypto.Crypto, crypto),
7466 : yield* crypto.randomUUIDv4.pipe(
7467 Effect.filterOrFail(isClaudePromptUuid),
7468 Effect.mapError(
7469 (cause) =>
7470 new ProviderAdapter.ProviderAdapterProtocolError({
7471 driver: CLAUDE_PROVIDER,
7472 detail: "Failed to allocate a Claude prompt uuid.",
7473 payload: cause,
7474 }),
7475 ),
74497476 );
74507477 const providerTurnId = idAllocator.derive.providerTurn({
74517478 driver: CLAUDE_PROVIDER,
......@@ -7505,6 +7532,7 @@ export function makeClaudeAdapterV2(
75057532 subagentNodesByTaskId: new Map(),
75067533 promptUuid,
75077534 promptEcho: isClaudeProviderContinuationTurn(turnInput) ? "confirmed" : "pending",
7535 promptStarted: false,
75087536 gatedFramesBeforeEcho: 0,
75097537 heldRootFrames: [],
75107538 };