| ... | ... | @@ -2229,6 +2229,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2229 | 2229 | processQueues, |
| 2230 | 2230 | offerAndWait, |
| 2231 | 2231 | 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 | }, |
| 2232 | 2238 | permissionModeChanges, |
| 2233 | 2239 | continuationRequests, |
| 2234 | 2240 | events, |
| ... | ... | @@ -4421,12 +4427,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4421 | 4427 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-callback-3"); |
| 4422 | 4428 | const planToolUseId = "toolu_01WakePlanExitPlanMode"; |
| 4423 | 4429 | 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({ |
| 4426 | 4432 | ...frame, |
| 4427 | | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4433 | user_message_uuid: harness.promptUuid(promptIndex), |
| 4428 | 4434 | }); |
| 4429 | | }); |
| 4430 | 4435 | const runOf = (attemptId: RunAttemptId) => RunId.make(`run-${attemptId}`); |
| 4431 | 4436 | const planToolUse = claudeSdkFrame({ |
| 4432 | 4437 | type: "assistant", |
| ... | ... | @@ -4479,12 +4484,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4479 | 4484 | runtimePolicy, |
| 4480 | 4485 | }), |
| 4481 | 4486 | ); |
| 4482 | | yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); |
| 4487 | yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, 0)); |
| 4483 | 4488 | yield* Queue.offer( |
| 4484 | 4489 | harness.sdkMessages, |
| 4485 | | yield* stamp( |
| 4490 | stamp( |
| 4486 | 4491 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000740", result: "STARTED" }), |
| 4487 | | firstAttempt, |
| 4492 | 0, |
| 4488 | 4493 | ), |
| 4489 | 4494 | ); |
| 4490 | 4495 | yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal"); |
| ... | ... | @@ -4541,22 +4546,22 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4541 | 4546 | // The prompt's own turn follows and echoes its uuid. |
| 4542 | 4547 | yield* Queue.offer( |
| 4543 | 4548 | harness.sdkMessages, |
| 4544 | | yield* stamp( |
| 4549 | stamp( |
| 4545 | 4550 | makeAssistantTextFrame({ |
| 4546 | 4551 | uuid: "00000000-0000-4000-8000-000000000743", |
| 4547 | 4552 | text: "USER_REPLY", |
| 4548 | 4553 | }), |
| 4549 | | userAttempt, |
| 4554 | 1, |
| 4550 | 4555 | ), |
| 4551 | 4556 | ); |
| 4552 | 4557 | yield* Queue.offer( |
| 4553 | 4558 | harness.sdkMessages, |
| 4554 | | yield* stamp( |
| 4559 | stamp( |
| 4555 | 4560 | makeResultFrame({ |
| 4556 | 4561 | uuid: "00000000-0000-4000-8000-000000000744", |
| 4557 | 4562 | result: "USER_REPLY", |
| 4558 | 4563 | }), |
| 4559 | | userAttempt, |
| 4564 | 1, |
| 4560 | 4565 | ), |
| 4561 | 4566 | ); |
| 4562 | 4567 | yield* awaitUntil(() => harness.terminalEvents().length === 2, "user turn terminal"); |
| ... | ... | @@ -4721,12 +4726,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4721 | 4726 | const firstAttempt = RunAttemptId.make("attempt-claude-wake-approval-1"); |
| 4722 | 4727 | const userAttempt = RunAttemptId.make("attempt-claude-wake-approval-2"); |
| 4723 | 4728 | 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({ |
| 4726 | 4731 | ...frame, |
| 4727 | | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4732 | user_message_uuid: harness.promptUuid(promptIndex), |
| 4728 | 4733 | }); |
| 4729 | | }); |
| 4730 | 4734 | |
| 4731 | 4735 | yield* harness.runtime.startTurn( |
| 4732 | 4736 | makeClaudeTestTurnInput({ |
| ... | ... | @@ -4741,16 +4745,16 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4741 | 4745 | ); |
| 4742 | 4746 | yield* Queue.offer( |
| 4743 | 4747 | harness.sdkMessages, |
| 4744 | | yield* stamp( |
| 4748 | stamp( |
| 4745 | 4749 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000780", text: "One." }), |
| 4746 | | firstAttempt, |
| 4750 | 0, |
| 4747 | 4751 | ), |
| 4748 | 4752 | ); |
| 4749 | 4753 | yield* Queue.offer( |
| 4750 | 4754 | harness.sdkMessages, |
| 4751 | | yield* stamp( |
| 4755 | stamp( |
| 4752 | 4756 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000781", result: "One." }), |
| 4753 | | firstAttempt, |
| 4757 | 0, |
| 4754 | 4758 | ), |
| 4755 | 4759 | ); |
| 4756 | 4760 | yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal"); |
| ... | ... | @@ -4845,12 +4849,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4845 | 4849 | const now = yield* DateTime.now; |
| 4846 | 4850 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-debris-1"); |
| 4847 | 4851 | 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({ |
| 4850 | 4854 | ...frame, |
| 4851 | | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4855 | user_message_uuid: harness.promptUuid(promptIndex), |
| 4852 | 4856 | }); |
| 4853 | | }); |
| 4854 | 4857 | |
| 4855 | 4858 | yield* harness.runtime.startTurn( |
| 4856 | 4859 | makeClaudeTestTurnInput({ |
| ... | ... | @@ -4865,16 +4868,16 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4865 | 4868 | // The first turn echoes on its first frame: this process echoes early. |
| 4866 | 4869 | yield* Queue.offer( |
| 4867 | 4870 | harness.sdkMessages, |
| 4868 | | yield* stamp( |
| 4871 | stamp( |
| 4869 | 4872 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000760", text: "One." }), |
| 4870 | | firstAttempt, |
| 4873 | 0, |
| 4871 | 4874 | ), |
| 4872 | 4875 | ); |
| 4873 | 4876 | yield* Queue.offer( |
| 4874 | 4877 | harness.sdkMessages, |
| 4875 | | yield* stamp( |
| 4878 | stamp( |
| 4876 | 4879 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000761", result: "One." }), |
| 4877 | | firstAttempt, |
| 4880 | 0, |
| 4878 | 4881 | ), |
| 4879 | 4882 | ); |
| 4880 | 4883 | yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal"); |
| ... | ... | @@ -4899,16 +4902,16 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4899 | 4902 | yield* Queue.offer(harness.sdkMessages, staleTaskNotificationResult); |
| 4900 | 4903 | yield* Queue.offer( |
| 4901 | 4904 | harness.sdkMessages, |
| 4902 | | yield* stamp( |
| 4905 | stamp( |
| 4903 | 4906 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000762", text: "Two." }), |
| 4904 | | secondAttempt, |
| 4907 | 1, |
| 4905 | 4908 | ), |
| 4906 | 4909 | ); |
| 4907 | 4910 | yield* Queue.offer( |
| 4908 | 4911 | harness.sdkMessages, |
| 4909 | | yield* stamp( |
| 4912 | stamp( |
| 4910 | 4913 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000763", result: "Two." }), |
| 4911 | | secondAttempt, |
| 4914 | 1, |
| 4912 | 4915 | ), |
| 4913 | 4916 | ); |
| 4914 | 4917 | yield* awaitUntil(() => harness.terminalEvents().length === 2, "second turn terminal"); |
| ... | ... | @@ -4944,12 +4947,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4944 | 4947 | const now = yield* DateTime.now; |
| 4945 | 4948 | const firstAttempt = RunAttemptId.make("attempt-claude-echo-1"); |
| 4946 | 4949 | 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({ |
| 4949 | 4952 | ...frame, |
| 4950 | | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 4953 | user_message_uuid: harness.promptUuid(promptIndex), |
| 4951 | 4954 | }); |
| 4952 | | }); |
| 4953 | 4955 | const assistantTexts = () => |
| 4954 | 4956 | harness.events.flatMap((event) => |
| 4955 | 4957 | event.type === "message.updated" && event.message.role === "assistant" |
| ... | ... | @@ -4969,22 +4971,18 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4969 | 4971 | attachments: [], |
| 4970 | 4972 | }), |
| 4971 | 4973 | ); |
| 4972 | | assert.equal( |
| 4973 | | harness.offeredMessages[0]?.uuid, |
| 4974 | | yield* ClaudeAdapterV2.claudePromptUuid(firstAttempt), |
| 4975 | | ); |
| 4976 | 4974 | yield* Queue.offer( |
| 4977 | 4975 | harness.sdkMessages, |
| 4978 | | yield* stamp( |
| 4976 | stamp( |
| 4979 | 4977 | makeAssistantTextFrame({ uuid: "00000000-0000-4000-8000-000000000701", text: "One." }), |
| 4980 | | firstAttempt, |
| 4978 | 0, |
| 4981 | 4979 | ), |
| 4982 | 4980 | ); |
| 4983 | 4981 | yield* Queue.offer( |
| 4984 | 4982 | harness.sdkMessages, |
| 4985 | | yield* stamp( |
| 4983 | stamp( |
| 4986 | 4984 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000702", result: "One." }), |
| 4987 | | firstAttempt, |
| 4985 | 0, |
| 4988 | 4986 | ), |
| 4989 | 4987 | ); |
| 4990 | 4988 | yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal"); |
| ... | ... | @@ -5018,6 +5016,118 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 5018 | 5016 | ), |
| 5019 | 5017 | ); |
| 5020 | 5018 | |
| 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 | |
| 5021 | 5131 | it.effect("buffers wake output and requests a single continuation run", () => |
| 5022 | 5132 | Effect.scoped( |
| 5023 | 5133 | Effect.gen(function* () { |
| ... | ... | @@ -6031,12 +6141,11 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 6031 | 6141 | const userAttempt = RunAttemptId.make("attempt-claude-wake-subagent-2"); |
| 6032 | 6142 | const continuationAttempt = RunAttemptId.make("attempt-claude-wake-subagent-3"); |
| 6033 | 6143 | 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({ |
| 6036 | 6146 | ...frame, |
| 6037 | | user_message_uuid: yield* ClaudeAdapterV2.claudePromptUuid(attemptId), |
| 6147 | user_message_uuid: harness.promptUuid(promptIndex), |
| 6038 | 6148 | }); |
| 6039 | | }); |
| 6040 | 6149 | |
| 6041 | 6150 | yield* harness.runtime.startTurn( |
| 6042 | 6151 | makeClaudeTestTurnInput({ |
| ... | ... | @@ -6048,12 +6157,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 6048 | 6157 | attachments: [], |
| 6049 | 6158 | }), |
| 6050 | 6159 | ); |
| 6051 | | yield* Queue.offer(harness.sdkMessages, yield* stamp(wakeTaskStarted, firstAttempt)); |
| 6160 | yield* Queue.offer(harness.sdkMessages, stamp(wakeTaskStarted, 0)); |
| 6052 | 6161 | yield* Queue.offer( |
| 6053 | 6162 | harness.sdkMessages, |
| 6054 | | yield* stamp( |
| 6163 | stamp( |
| 6055 | 6164 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000770", result: "STARTED" }), |
| 6056 | | firstAttempt, |
| 6165 | 0, |
| 6057 | 6166 | ), |
| 6058 | 6167 | ); |
| 6059 | 6168 | yield* awaitUntil(() => harness.terminalEvents().length === 1, "first turn terminal"); |
| ... | ... | @@ -6120,16 +6229,16 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 6120 | 6229 | session_id: WAKE_NATIVE_SESSION, |
| 6121 | 6230 | }), |
| 6122 | 6231 | wakeResult, |
| 6123 | | yield* stamp( |
| 6232 | stamp( |
| 6124 | 6233 | makeAssistantTextFrame({ |
| 6125 | 6234 | uuid: "00000000-0000-4000-8000-000000000776", |
| 6126 | 6235 | text: "USER_REPLY", |
| 6127 | 6236 | }), |
| 6128 | | userAttempt, |
| 6237 | 1, |
| 6129 | 6238 | ), |
| 6130 | | yield* stamp( |
| 6239 | stamp( |
| 6131 | 6240 | makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000777", result: "USER_REPLY" }), |
| 6132 | | userAttempt, |
| 6241 | 1, |
| 6133 | 6242 | ), |
| 6134 | 6243 | ]; |
| 6135 | 6244 | for (const frame of wakeFrames) { |