@@ -5077,6 +5077,138 @@ describe("ClaudeAdapterV2 background wake turns", () => {
50775077 ),
50785078 );
50795079
5080+ it.effect("an interrupted turn after a rollback stops resuming at the rolled-back head", () =>
5081+ Effect.gen(function* () {
5082+ const harness = yield* makeWakeHarnessWithOptions({
5083+ close: (sdkMessages) => Queue.shutdown(sdkMessages),
5084+ freshQueueOnReopen: true,
5085+ });
5086+ const now = yield* DateTime.now;
5087+ const pinnedHead = "00000000-0000-4000-8000-000000025510";
5088+ const pinnedThread = {
5089+ ...harness.providerThread,
5090+ nativeConversationHeadRef: {
5091+ driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
5092+ nativeId: pinnedHead,
5093+ strength: "weak" as const,
5094+ },
5095+ };
5096+ const interruptedInput = makeClaudeTestTurnInput({
5097+ threadId: harness.threadId,
5098+ providerThread: pinnedThread,
5099+ now,
5100+ attemptId: RunAttemptId.make("attempt-claude-interrupt-after-rollback"),
5101+ text: "The secret word is apricot-25510. Then run sleep 60.",
5102+ attachments: [],
5103+ providerTurnOrdinal: 2,
5104+ nativeThreadHasTurns: true,
5105+ });
5106+ yield* harness.runtime.startTurn(interruptedInput);
5107+ assert.equal(harness.getOpenedOptions()?.resumeSessionAt, pinnedHead);
5108+ yield* harness.offerAndWait(
5109+ makeAssistantTextFrame({
5110+ uuid: "00000000-0000-4000-8000-000000025511",
5111+ text: "Noted apricot-25510. Running sleep.",
5112+ }),
5113+ );
5114+ yield* harness.runtime.interruptTurn({
5115+ providerThread: pinnedThread,
5116+ providerTurnId: (yield* IdAllocator.IdAllocatorV2).derive.providerTurn({
5117+ driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
5118+ nativeTurnId: `turn:${interruptedInput.attemptId}`,
5119+ }),
5120+ });
5121+ assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "interrupted");
5122+ const afterInterrupt = harness.events.findLast(
5123+ (event) => event.type === "provider_thread.updated",
5124+ )?.providerThread;
5125+ assert.isDefined(afterInterrupt);
5126+ assert.isNull(afterInterrupt.nativeConversationHeadRef);
5127+
5128+ yield* harness.runtime.startTurn(
5129+ makeClaudeTestTurnInput({
5130+ threadId: harness.threadId,
5131+ providerThread: afterInterrupt,
5132+ now,
5133+ attemptId: RunAttemptId.make("attempt-claude-after-interrupt"),
5134+ text: "What is the secret word?",
5135+ attachments: [],
5136+ providerTurnOrdinal: 3,
5137+ nativeThreadHasTurns: true,
5138+ }),
5139+ );
5140+ assert.equal(harness.processQueues.length, 2);
5141+ assert.isUndefined(harness.getOpenedOptions()?.resumeSessionAt);
5142+ }).pipe(
5143+ Effect.provide(
5144+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
5145+ ),
5146+ ),
5147+ );
5148+
5149+ it.effect("an interrupted turn with no assistant output keeps the rolled-back head", () =>
5150+ Effect.gen(function* () {
5151+ const harness = yield* makeWakeHarnessWithOptions({
5152+ close: (sdkMessages) => Queue.shutdown(sdkMessages),
5153+ freshQueueOnReopen: true,
5154+ });
5155+ const now = yield* DateTime.now;
5156+ const pinnedHead = "00000000-0000-4000-8000-000000025510";
5157+ const pinnedThread = {
5158+ ...harness.providerThread,
5159+ nativeConversationHeadRef: {
5160+ driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
5161+ nativeId: pinnedHead,
5162+ strength: "weak" as const,
5163+ },
5164+ };
5165+ const interruptedInput = makeClaudeTestTurnInput({
5166+ threadId: harness.threadId,
5167+ providerThread: pinnedThread,
5168+ now,
5169+ attemptId: RunAttemptId.make("attempt-claude-silent-interrupt-after-rollback"),
5170+ text: "The secret word is apricot-25510. Then run sleep 60.",
5171+ attachments: [],
5172+ providerTurnOrdinal: 2,
5173+ nativeThreadHasTurns: true,
5174+ });
5175+ yield* harness.runtime.startTurn(interruptedInput);
5176+ assert.equal(harness.getOpenedOptions()?.resumeSessionAt, pinnedHead);
5177+ yield* harness.runtime.interruptTurn({
5178+ providerThread: pinnedThread,
5179+ providerTurnId: (yield* IdAllocator.IdAllocatorV2).derive.providerTurn({
5180+ driver: ClaudeAdapterV2.CLAUDE_PROVIDER,
5181+ nativeTurnId: `turn:${interruptedInput.attemptId}`,
5182+ }),
5183+ });
5184+ assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "interrupted");
5185+ const afterInterrupt = harness.events.findLast(
5186+ (event) => event.type === "provider_thread.updated",
5187+ )?.providerThread;
5188+ assert.isDefined(afterInterrupt);
5189+ assert.equal(afterInterrupt.nativeConversationHeadRef?.nativeId, pinnedHead);
5190+
5191+ yield* harness.runtime.startTurn(
5192+ makeClaudeTestTurnInput({
5193+ threadId: harness.threadId,
5194+ providerThread: afterInterrupt,
5195+ now,
5196+ attemptId: RunAttemptId.make("attempt-claude-after-silent-interrupt"),
5197+ text: "What is the secret word?",
5198+ attachments: [],
5199+ providerTurnOrdinal: 3,
5200+ nativeThreadHasTurns: true,
5201+ }),
5202+ );
5203+ assert.equal(harness.processQueues.length, 2);
5204+ assert.equal(harness.getOpenedOptions()?.resumeSessionAt, pinnedHead);
5205+ }).pipe(
5206+ Effect.provide(
5207+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
5208+ ),
5209+ ),
5210+ );
5211+
50805212 it.effect("never records a subagent uuid for a turn with no main-thread assistant message", () =>
50815213 Effect.gen(function* () {
50825214 const harness = yield* makeWakeHarnessWithOptions({
0 commit comments