1import { assert, it } from "@effect/vitest";
2import {
3 MessageId,
4 ProviderThreadId,
5 RunAttemptId,
6 RunId,
7 type OrchestrationV2Run,
8} from "@t3tools/contracts";
9import * as DateTime from "effect/DateTime";
10
11import {
12 cancelledRosterTaskWork,
13 cancelledTurnItemWork,
14 isRestartNoteContinuation,
15 pendingRestartCancelledBackgroundWork,
16 restartCancelledBackgroundWorkNote,
17 restartContinuationNote,
18 mergeRestartCancelledBackgroundWork,
19} from "./RestartBackgroundNote.ts";
20
21const claudeThread = ProviderThreadId.make("provider-thread:claude");
22const codexThread = ProviderThreadId.make("provider-thread:codex");
23const lost = [{ kind: "subagent" as const, label: "Background subagent test" }];
24
25function 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
41const turnFor = (source: OrchestrationV2Run) => ({
42 runAttemptId: source.activeAttemptId,
43 providerThreadId: source.providerThreadId!,
44 status: "completed" as const,
45});
46
47it("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
68it("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
92it("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
108it("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
120it("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
130it("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
162it("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});