| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { |
| 3 | MessageId, |
| 4 | ProviderThreadId, |
| 5 | RunAttemptId, |
| 6 | RunId, |
| 7 | type OrchestrationV2Run, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as DateTime from "effect/DateTime"; |
| 10 | |
| 11 | import { |
| 12 | cancelledRosterTaskWork, |
| 13 | cancelledTurnItemWork, |
| 14 | isRestartNoteContinuation, |
| 15 | pendingRestartCancelledBackgroundWork, |
| 16 | restartCancelledBackgroundWorkNote, |
| 17 | restartContinuationNote, |
| 18 | mergeRestartCancelledBackgroundWork, |
| 19 | } from "./RestartBackgroundNote.ts"; |
| 20 | |
| 21 | const claudeThread = ProviderThreadId.make("provider-thread:claude"); |
| 22 | const codexThread = ProviderThreadId.make("provider-thread:codex"); |
| 23 | const lost = [{ kind: "subagent" as const, label: "Background subagent test" }]; |
| 24 | |
| 25 | function run( |
| 26 | ordinal: number, |
| 27 | providerThreadId: ProviderThreadId, |
| 28 | extra: Partial<OrchestrationV2Run> = {}, |
| 29 | ): OrchestrationV2Run { |
| 30 | return { |
| 31 | id: RunId.make(`run:${ordinal}`), |
| 32 | ordinal, |
| 33 | providerThreadId, |
| 34 | userMessageId: MessageId.make(`message:${ordinal}`), |
| 35 | activeAttemptId: RunAttemptId.make(`attempt:${ordinal}`), |
| 36 | status: "completed", |
| 37 | ...extra, |
| 38 | } as OrchestrationV2Run; |
| 39 | } |
| 40 | |
| 41 | const turnFor = (source: OrchestrationV2Run) => ({ |
| 42 | runAttemptId: source.activeAttemptId, |
| 43 | providerThreadId: source.providerThreadId!, |
| 44 | status: "completed" as const, |
| 45 | }); |
| 46 | |
| 47 | it("keeps the note for the provider thread that lost the work across a provider switch", () => { |
| 48 | const root = run(1, claudeThread, { restartCancelledBackgroundWork: lost }); |
| 49 | const onCodex = run(2, codexThread); |
| 50 | const backOnClaude = run(3, claudeThread); |
| 51 | const pending = (target: OrchestrationV2Run, runs: ReadonlyArray<OrchestrationV2Run>) => |
| 52 | pendingRestartCancelledBackgroundWork({ |
| 53 | runs, |
| 54 | providerTurns: runs.filter((source) => source.id !== target.id).map(turnFor), |
| 55 | compactionMessageIds: new Set(), |
| 56 | run: target, |
| 57 | attempts: [{ id: target.activeAttemptId!, runId: target.id }], |
| 58 | }); |
| 59 | |
| 60 | // Codex never lost the work, so it neither receives nor consumes the note. |
| 61 | assert.deepEqual(pending(onCodex, [root, onCodex]), []); |
| 62 | assert.deepEqual(pending(backOnClaude, [root, onCodex, backOnClaude]), lost); |
| 63 | // Once Claude was told, later Claude turns are not. |
| 64 | const later = run(4, claudeThread); |
| 65 | assert.deepEqual(pending(later, [root, onCodex, backOnClaude, later]), []); |
| 66 | }); |
| 67 | |
| 68 | it("delivers a resumed queued run's note even after a higher-ordinal run", () => { |
| 69 | // Run 3 ran ahead of held run 2; run 2 then resumed, lost background work in |
| 70 | // a second restart, and its continuation was superseded by a new message. |
| 71 | const ranFirst = run(3, claudeThread, { |
| 72 | completedAt: DateTime.makeUnsafe("2026-10-03T10:00:00.000Z"), |
| 73 | }); |
| 74 | const resumed = run(2, claudeThread, { |
| 75 | completedAt: DateTime.makeUnsafe("2026-10-03T10:05:00.000Z"), |
| 76 | restartCancelledBackgroundWork: lost, |
| 77 | }); |
| 78 | const next = run(4, claudeThread); |
| 79 | const runs = [resumed, ranFirst, next]; |
| 80 | assert.deepEqual( |
| 81 | pendingRestartCancelledBackgroundWork({ |
| 82 | runs, |
| 83 | providerTurns: [resumed, ranFirst].map(turnFor), |
| 84 | compactionMessageIds: new Set(), |
| 85 | run: next, |
| 86 | attempts: [{ id: next.activeAttemptId!, runId: next.id }], |
| 87 | }), |
| 88 | lost, |
| 89 | ); |
| 90 | }); |
| 91 | |
| 92 | it("bounds the note so it cannot crowd out the turn's context", () => { |
| 93 | const work = Array.from({ length: 25 }, (_, index) => ({ |
| 94 | kind: "shell" as const, |
| 95 | label: `sleep ${index}`, |
| 96 | })); |
| 97 | const note = restartCancelledBackgroundWorkNote(work); |
| 98 | assert.lengthOf(note.split("\n"), 12); |
| 99 | assert.isTrue(note.endsWith("- and 15 more")); |
| 100 | const command = cancelledTurnItemWork({ |
| 101 | type: "command_execution", |
| 102 | input: "x".repeat(10_000), |
| 103 | title: null, |
| 104 | } as never); |
| 105 | assert.isAtMost(command?.label.length ?? 0, 160); |
| 106 | }); |
| 107 | |
| 108 | it("bounds roster task labels including a long task id", () => { |
| 109 | const work = cancelledRosterTaskWork({ |
| 110 | kind: "command", |
| 111 | taskId: "t".repeat(400), |
| 112 | description: "d".repeat(400), |
| 113 | }); |
| 114 | assert.equal(work.kind, "shell"); |
| 115 | assert.lengthOf(work.label, 160); |
| 116 | const idOnly = cancelledRosterTaskWork({ kind: "background_task", taskId: "t".repeat(400) }); |
| 117 | assert.lengthOf(idOnly.label, 160); |
| 118 | }); |
| 119 | |
| 120 | it("keeps separate cancelled tasks that share a kind and label", () => { |
| 121 | const first = { kind: "shell" as const, label: "sleep 20", id: "item-1" }; |
| 122 | const second = { kind: "shell" as const, label: "sleep 20", id: "item-2" }; |
| 123 | const merged = mergeRestartCancelledBackgroundWork([first], [second, first]); |
| 124 | assert.deepEqual(merged, [first, second]); |
| 125 | // Rows recorded before ids existed still collapse by kind + label. |
| 126 | const legacy = { kind: "shell" as const, label: "sleep 20" }; |
| 127 | assert.lengthOf(mergeRestartCancelledBackgroundWork([legacy], [legacy]), 1); |
| 128 | }); |
| 129 | |
| 130 | it("does not repeat the note when a steer restarts the run on a new attempt", () => { |
| 131 | const root = run(1, claudeThread, { restartCancelledBackgroundWork: lost }); |
| 132 | const steered = run(2, claudeThread, { |
| 133 | activeAttemptId: RunAttemptId.make("attempt:2b"), |
| 134 | }); |
| 135 | const firstAttempt = RunAttemptId.make("attempt:2a"); |
| 136 | const pending = (runAttemptIds: ReadonlyArray<RunAttemptId>, delivered: boolean) => |
| 137 | pendingRestartCancelledBackgroundWork({ |
| 138 | runs: [root, steered], |
| 139 | providerTurns: [ |
| 140 | turnFor(root), |
| 141 | ...(delivered |
| 142 | ? [ |
| 143 | { |
| 144 | runAttemptId: firstAttempt, |
| 145 | providerThreadId: claudeThread, |
| 146 | status: "completed" as const, |
| 147 | }, |
| 148 | ] |
| 149 | : []), |
| 150 | ], |
| 151 | compactionMessageIds: new Set(), |
| 152 | run: steered, |
| 153 | attempts: runAttemptIds.map((id) => ({ id, runId: steered.id })), |
| 154 | }); |
| 155 | |
| 156 | // The first attempt reached the provider with the note; its replacement must not repeat it. |
| 157 | assert.deepEqual(pending([firstAttempt, RunAttemptId.make("attempt:2b")], true), []); |
| 158 | // A first attempt that never reached the provider did not deliver it. |
| 159 | assert.deepEqual(pending([firstAttempt, RunAttemptId.make("attempt:2b")], false), lost); |
| 160 | }); |
| 161 | |
| 162 | it("counts a prompted mid-turn or chained continuation as delivering the note", () => { |
| 163 | const cut = run(1, claudeThread, { status: "cancelled", restartCancelledBackgroundWork: lost }); |
| 164 | const cutTurn = { ...turnFor(cut), status: "cancelled" as const }; |
| 165 | const continuation = run(2, claudeThread, { restartContinuationOfRunId: cut.id }); |
| 166 | // A turn cut mid-way that lost work is prompted with the note, not resumed natively. |
| 167 | assert.isTrue(isRestartNoteContinuation(continuation, [cut, continuation], [cutTurn], [])); |
| 168 | assert.isFalse( |
| 169 | isRestartNoteContinuation( |
| 170 | continuation, |
| 171 | [{ ...cut, restartCancelledBackgroundWork: [] }, continuation], |
| 172 | [cutTurn], |
| 173 | [], |
| 174 | ), |
| 175 | ); |
| 176 | const later = run(3, claudeThread); |
| 177 | const pending = (delivered: boolean) => |
| 178 | pendingRestartCancelledBackgroundWork({ |
| 179 | runs: [cut, continuation, later], |
| 180 | providerTurns: [cutTurn, ...(delivered ? [turnFor(continuation)] : [])], |
| 181 | compactionMessageIds: new Set(), |
| 182 | run: later, |
| 183 | attempts: [{ id: later.activeAttemptId!, runId: later.id }], |
| 184 | }); |
| 185 | assert.deepEqual(pending(true), []); |
| 186 | // A continuation that never reached the provider delivered nothing. |
| 187 | assert.deepEqual(pending(false), lost); |
| 188 | |
| 189 | // Its own continuation carries the original note forward. |
| 190 | const unstarted = { ...continuation, status: "cancelled" as const }; |
| 191 | const chained = run(3, claudeThread, { restartContinuationOfRunId: unstarted.id }); |
| 192 | assert.deepEqual(restartContinuationNote(unstarted, [cut, unstarted], [cutTurn], []), { |
| 193 | work: lost, |
| 194 | settled: false, |
| 195 | }); |
| 196 | assert.isTrue(isRestartNoteContinuation(chained, [cut, unstarted, chained], [cutTurn], [])); |
| 197 | // Claude announces running before accepting the prompt. A second restart |
| 198 | // can cancel that turn without delivering anything to the provider. |
| 199 | for (const status of ["running", "cancelled"] as const) { |
| 200 | const turns = [cutTurn, { ...turnFor(unstarted), status }]; |
| 201 | assert.deepEqual(restartContinuationNote(unstarted, [cut, unstarted], turns, []), { |
| 202 | work: lost, |
| 203 | settled: false, |
| 204 | }); |
| 205 | assert.deepEqual( |
| 206 | pendingRestartCancelledBackgroundWork({ |
| 207 | runs: [cut, unstarted, later], |
| 208 | providerTurns: turns, |
| 209 | compactionMessageIds: new Set(), |
| 210 | run: later, |
| 211 | attempts: [{ id: later.activeAttemptId!, runId: later.id }], |
| 212 | }), |
| 213 | lost, |
| 214 | ); |
| 215 | } |
| 216 | |
| 217 | // A steer replaced the attempt after its completed turn delivered the note. |
| 218 | const steered = { ...unstarted, activeAttemptId: RunAttemptId.make("attempt:replacement") }; |
| 219 | const attempts = [{ id: unstarted.activeAttemptId!, runId: steered.id }]; |
| 220 | const turns = [ |
| 221 | cutTurn, |
| 222 | turnFor(unstarted), |
| 223 | { ...turnFor(steered), status: "cancelled" as const }, |
| 224 | ]; |
| 225 | assert.deepEqual(restartContinuationNote(steered, [cut, steered], turns, attempts).work, []); |
| 226 | assert.isFalse(isRestartNoteContinuation(chained, [cut, steered, chained], turns, attempts)); |
| 227 | assert.deepEqual( |
| 228 | pendingRestartCancelledBackgroundWork({ |
| 229 | runs: [cut, steered, later], |
| 230 | providerTurns: turns, |
| 231 | compactionMessageIds: new Set(), |
| 232 | run: later, |
| 233 | attempts, |
| 234 | }), |
| 235 | [], |
| 236 | ); |
| 237 | }); |