| ... | ... | @@ -132,6 +132,12 @@ const CLAUDE_TEST_MODEL_SELECTION = { |
| 132 | 132 | model: "claude-sonnet-4-6", |
| 133 | 133 | options: [{ id: "effort", value: "ultrathink" }], |
| 134 | 134 | } satisfies ModelSelection; |
| 135 | // Thinking is a launch flag, so switching to this selection needs a new process. |
| 136 | const CLAUDE_TEST_RESTART_MODEL_SELECTION = { |
| 137 | ...CLAUDE_TEST_MODEL_SELECTION, |
| 138 | model: "claude-haiku-4-5-20251001", |
| 139 | options: [{ id: "thinking", value: false }], |
| 140 | } satisfies ModelSelection; |
| 135 | 141 | const CLAUDE_TEST_RUNTIME_POLICY = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ |
| 136 | 142 | runtimeMode: "full-access", |
| 137 | 143 | interactionMode: "default", |
| ... | ... | @@ -1027,6 +1033,7 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => { |
| 1027 | 1033 | messages: Stream.never, |
| 1028 | 1034 | offer: () => Effect.void, |
| 1029 | 1035 | setModel: () => Effect.void, |
| 1036 | applyFlagSettings: () => Effect.void, |
| 1030 | 1037 | setPermissionMode: () => Effect.void, |
| 1031 | 1038 | stopTask: () => Effect.void, |
| 1032 | 1039 | interrupt: Effect.void, |
| ... | ... | @@ -1187,6 +1194,7 @@ const captureSdkExecutablePaths = Effect.fn("captureSdkExecutablePaths")(functio |
| 1187 | 1194 | messages: Stream.never, |
| 1188 | 1195 | offer: () => Effect.void, |
| 1189 | 1196 | setModel: () => Effect.void, |
| 1197 | applyFlagSettings: () => Effect.void, |
| 1190 | 1198 | setPermissionMode: () => Effect.void, |
| 1191 | 1199 | stopTask: () => Effect.void, |
| 1192 | 1200 | interrupt: Effect.void, |
| ... | ... | @@ -1307,6 +1315,7 @@ describe("ClaudeAdapterV2 resume compaction", () => { |
| 1307 | 1315 | messages: Stream.never, |
| 1308 | 1316 | offer: () => Effect.void, |
| 1309 | 1317 | setModel: () => Effect.void, |
| 1318 | applyFlagSettings: () => Effect.void, |
| 1310 | 1319 | setPermissionMode: () => Effect.void, |
| 1311 | 1320 | stopTask: () => Effect.void, |
| 1312 | 1321 | interrupt: Effect.void, |
| ... | ... | @@ -1533,6 +1542,7 @@ describe("ClaudeAdapterV2 attachments", () => { |
| 1533 | 1542 | offeredMessages.push(message); |
| 1534 | 1543 | }), |
| 1535 | 1544 | setModel: () => Effect.void, |
| 1545 | applyFlagSettings: () => Effect.void, |
| 1536 | 1546 | setPermissionMode: () => Effect.void, |
| 1537 | 1547 | stopTask: () => Effect.void, |
| 1538 | 1548 | interrupt: Effect.void, |
| ... | ... | @@ -1678,6 +1688,7 @@ describe("ClaudeAdapterV2 attachments", () => { |
| 1678 | 1688 | messages: Stream.never, |
| 1679 | 1689 | offer: () => Effect.void, |
| 1680 | 1690 | setModel: () => Effect.void, |
| 1691 | applyFlagSettings: () => Effect.void, |
| 1681 | 1692 | setPermissionMode: () => Effect.void, |
| 1682 | 1693 | stopTask: () => Effect.void, |
| 1683 | 1694 | interrupt: Effect.void, |
| ... | ... | @@ -1773,6 +1784,7 @@ describe("ClaudeAdapterV2 native fork", () => { |
| 1773 | 1784 | messages: Stream.empty, |
| 1774 | 1785 | offer: () => Effect.void, |
| 1775 | 1786 | setModel: () => Effect.void, |
| 1787 | applyFlagSettings: () => Effect.void, |
| 1776 | 1788 | setPermissionMode: () => Effect.void, |
| 1777 | 1789 | stopTask: () => Effect.void, |
| 1778 | 1790 | interrupt: Effect.void, |
| ... | ... | @@ -2065,6 +2077,7 @@ describe("ClaudeAdapterV2 native session identity", () => { |
| 2065 | 2077 | messages: Stream.empty, |
| 2066 | 2078 | offer: () => Effect.void, |
| 2067 | 2079 | setModel: () => Effect.void, |
| 2080 | applyFlagSettings: () => Effect.void, |
| 2068 | 2081 | setPermissionMode: () => Effect.void, |
| 2069 | 2082 | stopTask: () => Effect.void, |
| 2070 | 2083 | interrupt: Effect.void, |
| ... | ... | @@ -2356,6 +2369,8 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2356 | 2369 | }); |
| 2357 | 2370 | const offeredMessages: Array<SDKUserMessage> = []; |
| 2358 | 2371 | const permissionModeChanges: Array<string> = []; |
| 2372 | // setModel and applyFlagSettings calls on the first process, in order. |
| 2373 | const liveSelectionChanges: Array<unknown> = []; |
| 2359 | 2374 | const continuationRequests: Array<ProviderContinuationRequests.ProviderContinuationRequest> = |
| 2360 | 2375 | []; |
| 2361 | 2376 | const continuationReceipts = |
| ... | ... | @@ -2403,6 +2418,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2403 | 2418 | offeredMessages.push(message); |
| 2404 | 2419 | }), |
| 2405 | 2420 | setModel: () => Effect.void, |
| 2421 | applyFlagSettings: () => Effect.void, |
| 2406 | 2422 | setPermissionMode: () => Effect.void, |
| 2407 | 2423 | stopTask: options?.stopTask ?? (() => Effect.void), |
| 2408 | 2424 | interrupt: Effect.void, |
| ... | ... | @@ -2432,7 +2448,14 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2432 | 2448 | Effect.sync(() => { |
| 2433 | 2449 | offeredMessages.push(message); |
| 2434 | 2450 | }), |
| 2435 | | setModel: () => Effect.void, |
| 2451 | setModel: (model) => |
| 2452 | Effect.sync(() => { |
| 2453 | liveSelectionChanges.push({ model }); |
| 2454 | }), |
| 2455 | applyFlagSettings: (settings) => |
| 2456 | Effect.sync(() => { |
| 2457 | liveSelectionChanges.push(settings); |
| 2458 | }), |
| 2436 | 2459 | setPermissionMode: (mode) => |
| 2437 | 2460 | Effect.sync(() => { |
| 2438 | 2461 | permissionModeChanges.push(mode); |
| ... | ... | @@ -2503,6 +2526,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2503 | 2526 | return uuid; |
| 2504 | 2527 | }, |
| 2505 | 2528 | permissionModeChanges, |
| 2529 | liveSelectionChanges, |
| 2506 | 2530 | continuationRequests, |
| 2507 | 2531 | continuationReceipts, |
| 2508 | 2532 | events, |
| ... | ... | @@ -2627,7 +2651,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2627 | 2651 | ); |
| 2628 | 2652 | |
| 2629 | 2653 | it.effect( |
| 2630 | | "reuses a background shell's query for omitted and explicit Normal, but blocks Fast", |
| 2654 | "applies model, effort and Fast changes to a process running a background shell, but not thinking or a cleared effort", |
| 2631 | 2655 | () => |
| 2632 | 2656 | Effect.scoped( |
| 2633 | 2657 | Effect.gen(function* () { |
| ... | ... | @@ -2648,41 +2672,65 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 2648 | 2672 | providerTurnOrdinal: ordinal, |
| 2649 | 2673 | modelSelection, |
| 2650 | 2674 | }); |
| 2675 | const settle = Effect.gen(function* () { |
| 2676 | yield* harness.offerAndWait(turnOneResult); |
| 2677 | yield* Queue.take(harness.terminalReceipts); |
| 2678 | }); |
| 2651 | 2679 | yield* harness.runtime.startTurn(turn(1, normal)); |
| 2652 | 2680 | const originalOptions = harness.getOpenedOptions(); |
| 2653 | 2681 | yield* harness.offerAndWait(wakeTaskStarted); |
| 2654 | | yield* harness.offerAndWait(turnOneResult); |
| 2655 | | yield* Queue.take(harness.terminalReceipts); |
| 2682 | yield* settle; |
| 2656 | 2683 | assert.isTrue(yield* harness.hasPendingBackgroundWork); |
| 2657 | 2684 | |
| 2685 | // Omitted and explicit Normal are the same selection. |
| 2658 | 2686 | yield* harness.runtime.startTurn( |
| 2659 | | turn(2, { |
| 2687 | turn(2, { ...normal, options: [{ id: "fastMode", value: false }] }), |
| 2688 | ); |
| 2689 | yield* settle; |
| 2690 | assert.deepEqual(harness.liveSelectionChanges, []); |
| 2691 | |
| 2692 | yield* harness.runtime.startTurn( |
| 2693 | turn(3, { |
| 2660 | 2694 | ...normal, |
| 2661 | | options: [{ id: "fastMode", value: false }], |
| 2695 | options: [ |
| 2696 | { id: "effort", value: "ultracode" }, |
| 2697 | { id: "fastMode", value: true }, |
| 2698 | ], |
| 2662 | 2699 | }), |
| 2663 | 2700 | ); |
| 2701 | yield* settle; |
| 2702 | yield* harness.runtime.startTurn(turn(4, { ...normal, model: "claude-sonnet-4-6" })); |
| 2703 | yield* settle; |
| 2704 | assert.deepEqual(harness.liveSelectionChanges, [ |
| 2705 | { model: "claude-opus-5-5" }, |
| 2706 | { effortLevel: "xhigh", fastMode: true, ultracode: true }, |
| 2707 | { model: "claude-sonnet-4-6" }, |
| 2708 | // Sonnet's default effort; it has no Fast mode. |
| 2709 | { effortLevel: "high", fastMode: null, ultracode: null }, |
| 2710 | ]); |
| 2664 | 2711 | assert.strictEqual(harness.getOpenedOptions(), originalOptions); |
| 2665 | | assert.lengthOf(harness.offeredMessages, 2); |
| 2666 | | yield* harness.offerAndWait(turnOneResult); |
| 2667 | | yield* Queue.take(harness.terminalReceipts); |
| 2668 | | |
| 2669 | | const refused = yield* harness.runtime |
| 2670 | | .startTurn( |
| 2671 | | turn(3, { |
| 2672 | | ...normal, |
| 2673 | | options: [{ id: "fastMode", value: true }], |
| 2674 | | }), |
| 2675 | | ) |
| 2676 | | .pipe(Effect.result); |
| 2677 | | assert.equal(refused._tag, "Failure"); |
| 2678 | | if (refused._tag === "Failure") { |
| 2679 | | assert.instanceOf( |
| 2680 | | refused.failure.cause, |
| 2681 | | ClaudeAdapterV2.ClaudeBackgroundWorkBlocksQueryReplacementError, |
| 2682 | | ); |
| 2712 | assert.lengthOf(harness.offeredMessages, 4); |
| 2713 | |
| 2714 | // Thinking is a launch flag. Haiku 4.5 has no effort, and only a new |
| 2715 | // process reads the user's configured effort again. |
| 2716 | for (const [ordinal, selection] of [ |
| 2717 | [5, CLAUDE_TEST_RESTART_MODEL_SELECTION], |
| 2718 | [6, { ...normal, model: "claude-haiku-4-5-20251001" }], |
| 2719 | ] as const) { |
| 2720 | const refused = yield* harness.runtime |
| 2721 | .startTurn(turn(ordinal, selection)) |
| 2722 | .pipe(Effect.result); |
| 2723 | assert.equal(refused._tag, "Failure"); |
| 2724 | if (refused._tag === "Failure") { |
| 2725 | assert.instanceOf( |
| 2726 | refused.failure.cause, |
| 2727 | ClaudeAdapterV2.ClaudeBackgroundWorkBlocksQueryReplacementError, |
| 2728 | ); |
| 2729 | } |
| 2683 | 2730 | } |
| 2684 | 2731 | assert.strictEqual(harness.getOpenedOptions(), originalOptions); |
| 2685 | | assert.lengthOf(harness.offeredMessages, 2); |
| 2732 | assert.lengthOf(harness.offeredMessages, 4); |
| 2733 | assert.lengthOf(harness.liveSelectionChanges, 4); |
| 2686 | 2734 | assert.isTrue(yield* harness.hasPendingBackgroundWork); |
| 2687 | 2735 | }).pipe( |
| 2688 | 2736 | Effect.provide( |
| ... | ... | @@ -4948,6 +4996,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 4948 | 4996 | messages: Stream.fromQueue(sdkMessages), |
| 4949 | 4997 | offer: () => Effect.void, |
| 4950 | 4998 | setModel: () => Effect.void, |
| 4999 | applyFlagSettings: () => Effect.void, |
| 4951 | 5000 | setPermissionMode: () => Effect.void, |
| 4952 | 5001 | stopTask: () => Effect.void, |
| 4953 | 5002 | interrupt: Effect.void, |
| ... | ... | @@ -5208,6 +5257,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 5208 | 5257 | messages: Stream.fromQueue(queue), |
| 5209 | 5258 | offer: () => Effect.void, |
| 5210 | 5259 | setModel: () => Effect.void, |
| 5260 | applyFlagSettings: () => Effect.void, |
| 5211 | 5261 | setPermissionMode: () => Effect.void, |
| 5212 | 5262 | stopTask: () => Effect.void, |
| 5213 | 5263 | interrupt: Effect.void, |
| ... | ... | @@ -10299,6 +10349,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10299 | 10349 | messages: Stream.fromQueue(sdkMessages), |
| 10300 | 10350 | offer: () => Effect.void, |
| 10301 | 10351 | setModel: () => Effect.void, |
| 10352 | applyFlagSettings: () => Effect.void, |
| 10302 | 10353 | setPermissionMode: () => Effect.void, |
| 10303 | 10354 | stopTask: () => Effect.void, |
| 10304 | 10355 | interrupt: Effect.void, |
| ... | ... | @@ -10370,10 +10421,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10370 | 10421 | ); |
| 10371 | 10422 | assert.isFalse(yield* hasPendingBackgroundWork); |
| 10372 | 10423 | |
| 10373 | | const alternateModel = { |
| 10374 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 10375 | | model: "claude-haiku-4-5-20251001", |
| 10376 | | } satisfies ModelSelection; |
| 10424 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 10377 | 10425 | // ProviderTurnStartService marks the thread active before startTurn; |
| 10378 | 10426 | // the process-reset clear must preserve that status. |
| 10379 | 10427 | const activeProviderThread = { |
| ... | ... | @@ -10489,6 +10537,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10489 | 10537 | messages: Stream.fromQueue(sdkMessages), |
| 10490 | 10538 | offer: () => Effect.void, |
| 10491 | 10539 | setModel: () => Effect.void, |
| 10540 | applyFlagSettings: () => Effect.void, |
| 10492 | 10541 | setPermissionMode: () => Effect.void, |
| 10493 | 10542 | stopTask: () => Effect.void, |
| 10494 | 10543 | interrupt: Effect.void, |
| ... | ... | @@ -10556,10 +10605,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10556 | 10605 | |
| 10557 | 10606 | // User turn changes model, replacing the query while the wake buffer |
| 10558 | 10607 | // stays queued for the later provider continuation. |
| 10559 | | const alternateModel = { |
| 10560 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 10561 | | model: "claude-haiku-4-5-20251001", |
| 10562 | | } satisfies ModelSelection; |
| 10608 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 10563 | 10609 | yield* runtime.startTurn( |
| 10564 | 10610 | makeClaudeTestTurnInput({ |
| 10565 | 10611 | threadId, |
| ... | ... | @@ -10731,6 +10777,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10731 | 10777 | messages: Stream.fromQueue(sdkMessages), |
| 10732 | 10778 | offer: () => Effect.void, |
| 10733 | 10779 | setModel: () => Effect.void, |
| 10780 | applyFlagSettings: () => Effect.void, |
| 10734 | 10781 | setPermissionMode: () => Effect.void, |
| 10735 | 10782 | stopTask: () => Effect.void, |
| 10736 | 10783 | interrupt: Effect.void, |
| ... | ... | @@ -10818,10 +10865,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10818 | 10865 | // Model-changing user turn replaces the query while continuation stays |
| 10819 | 10866 | // queued. Process reset must not invent opaque classification for the |
| 10820 | 10867 | // buffered subagent notification. |
| 10821 | | const alternateModel = { |
| 10822 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 10823 | | model: "claude-haiku-4-5-20251001", |
| 10824 | | } satisfies ModelSelection; |
| 10868 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 10825 | 10869 | yield* runtime.startTurn( |
| 10826 | 10870 | makeClaudeTestTurnInput({ |
| 10827 | 10871 | threadId, |
| ... | ... | @@ -10900,7 +10944,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10900 | 10944 | ), |
| 10901 | 10945 | ); |
| 10902 | 10946 | |
| 10903 | | it.effect("refuses a model change that would kill a running background subagent", () => |
| 10947 | it.effect("refuses a thinking change that would kill a running background subagent", () => |
| 10904 | 10948 | Effect.scoped( |
| 10905 | 10949 | Effect.gen(function* () { |
| 10906 | 10950 | const SUBAGENT_TASK_ID = "task-model-change-running-subagent"; |
| ... | ... | @@ -10932,6 +10976,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 10932 | 10976 | messages: Stream.fromQueue(sdkMessages), |
| 10933 | 10977 | offer: () => Effect.void, |
| 10934 | 10978 | setModel: () => Effect.void, |
| 10979 | applyFlagSettings: () => Effect.void, |
| 10935 | 10980 | setPermissionMode: () => Effect.void, |
| 10936 | 10981 | stopTask: () => Effect.void, |
| 10937 | 10982 | interrupt: Effect.void, |
| ... | ... | @@ -11004,12 +11049,9 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 11004 | 11049 | yield* awaitUntil(() => terminals().length === 1, "first turn terminal"); |
| 11005 | 11050 | const settledTurn = terminals()[0]!; |
| 11006 | 11051 | |
| 11007 | | // The subagent runs inside the first CLI process. Another model needs |
| 11008 | | // another process, so the turn must not start and close this one. |
| 11009 | | const alternateModel = { |
| 11010 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 11011 | | model: "claude-haiku-4-5-20251001", |
| 11012 | | } satisfies ModelSelection; |
| 11052 | // The subagent runs inside the first CLI process. A thinking change |
| 11053 | // needs another process, so the turn must not start and close this one. |
| 11054 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 11013 | 11055 | const switchTurn = (attempt: string) => |
| 11014 | 11056 | runtime.startTurn( |
| 11015 | 11057 | makeClaudeTestTurnInput({ |
| ... | ... | @@ -11115,6 +11157,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 11115 | 11157 | messages: Stream.fromQueue(sdkMessages), |
| 11116 | 11158 | offer: () => Effect.void, |
| 11117 | 11159 | setModel: () => Effect.void, |
| 11160 | applyFlagSettings: () => Effect.void, |
| 11118 | 11161 | setPermissionMode: () => Effect.void, |
| 11119 | 11162 | stopTask: () => Effect.void, |
| 11120 | 11163 | interrupt: Effect.void, |
| ... | ... | @@ -11174,10 +11217,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 11174 | 11217 | ); |
| 11175 | 11218 | assert.isTrue(yield* hasPendingBackgroundWork); |
| 11176 | 11219 | |
| 11177 | | const alternateModel = { |
| 11178 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 11179 | | model: "claude-haiku-4-5-20251001", |
| 11180 | | } satisfies ModelSelection; |
| 11220 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 11181 | 11221 | const failedStart = yield* runtime |
| 11182 | 11222 | .startTurn( |
| 11183 | 11223 | makeClaudeTestTurnInput({ |
| ... | ... | @@ -11254,6 +11294,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 11254 | 11294 | messages: Stream.fromQueue(sdkMessages), |
| 11255 | 11295 | offer: () => Effect.void, |
| 11256 | 11296 | setModel: () => Effect.void, |
| 11297 | applyFlagSettings: () => Effect.void, |
| 11257 | 11298 | setPermissionMode: () => Effect.void, |
| 11258 | 11299 | stopTask: () => Effect.void, |
| 11259 | 11300 | interrupt: Effect.void, |
| ... | ... | @@ -11317,10 +11358,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 11317 | 11358 | yield* awaitUntil(() => continuationRequests.length === 1, "first continuation request"); |
| 11318 | 11359 | assert.isTrue(yield* hasPendingBackgroundWork); |
| 11319 | 11360 | |
| 11320 | | const alternateModel = { |
| 11321 | | ...CLAUDE_TEST_MODEL_SELECTION, |
| 11322 | | model: "claude-haiku-4-5-20251001", |
| 11323 | | } satisfies ModelSelection; |
| 11361 | const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; |
| 11324 | 11362 | const failedStart = yield* runtime |
| 11325 | 11363 | .startTurn( |
| 11326 | 11364 | makeClaudeTestTurnInput({ |