1import { assert, it } from "@effect/vitest";
2import {
3 CommandId,
4 EventId,
5 MessageId,
6 NodeId,
7 type OrchestrationV2Run,
8 type OrchestrationV2ThreadProjection,
9 ProjectId,
10 ProviderDriverKind,
11 ProviderInstanceId,
12 ProviderSessionId,
13 ProviderThreadId,
14 RunAttemptId,
15 RunId,
16 RuntimeRequestId,
17 ThreadId,
18} from "@t3tools/contracts";
19import * as DateTime from "effect/DateTime";
20import * as Effect from "effect/Effect";
21
22import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
23import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
24import { applyToProjection, emptyProjection } from "./ProjectionStore.ts";
25import { planThreadDeletion } from "./ThreadDeletion.ts";
26
27const threadId = ThreadId.make("thread:delete-plan");
28const providerInstanceId = ProviderInstanceId.make("codex");
29const driver = ProviderDriverKind.make("codex");
30const providerThreadId = ProviderThreadId.make("provider-thread:delete-plan");
31const modelSelection = { instanceId: providerInstanceId, model: "gpt-5.4" };
32const createdAt = DateTime.makeUnsafe("2026-09-01T00:00:00.000Z");
33const deletedAt = DateTime.makeUnsafe("2026-09-04T00:00:00.000Z");
34const command = {
35 type: "thread.delete" as const,
36 commandId: CommandId.make("command:delete-plan"),
37 threadId,
38};
39
40function makeProjection(): OrchestrationV2ThreadProjection {
41 const base = emptyProjection({
42 id: EventId.make("event:delete-plan-created"),
43 type: "thread.created",
44 threadId,
45 occurredAt: createdAt,
46 payload: {
47 id: threadId,
48 createdBy: "user",
49 creationSource: "web",
50 projectId: ProjectId.make("project:delete-plan"),
51 title: "Delete the thread",
52 providerInstanceId,
53 modelSelection,
54 runtimeMode: "full-access",
55 interactionMode: "default",
56 branch: "feature",
57 worktreePath: "/workspace/feature",
58 activeProviderThreadId: null,
59 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
60 forkedFrom: null,
61 createdAt,
62 updatedAt: createdAt,
63 archivedAt: null,
64 settledOverride: null,
65 settledAt: null,
66 lastVisitedAt: null,
67 deletedAt: null,
68 },
69 });
70 const runs: Array<OrchestrationV2Run> = (
71 ["preparing", "queued", "starting", "running", "waiting", "completed"] as const
72 ).map((status, index) => ({
73 id: RunId.make(`run:delete-plan:${status}`),
74 threadId,
75 ordinal: index + 1,
76 providerInstanceId,
77 modelSelection,
78 providerThreadId,
79 userMessageId: MessageId.make(`message:delete-plan:${status}`),
80 rootNodeId: NodeId.make(`node:delete-plan:${status}`),
81 activeAttemptId: RunAttemptId.make(`attempt:delete-plan:${status}`),
82 status,
83 queuePosition: status === "queued" ? 1 : null,
84 requestedAt: createdAt,
85 startedAt: createdAt,
86 completedAt: status === "completed" ? createdAt : null,
87 checkpointId: null,
88 contextHandoffId: null,
89 }));
90 return {
91 ...base,
92 runs,
93 attempts: runs.map((run) => ({
94 id: run.activeAttemptId!,
95 runId: run.id,
96 attemptOrdinal: 1,
97 rootNodeId: run.rootNodeId!,
98 providerInstanceId,
99 providerThreadId,
100 providerTurnId: null,
101 reason: "initial",
102 status: run.status === "completed" ? "completed" : "running",
103 startedAt: createdAt,
104 completedAt: run.completedAt,
105 })),
106 nodes: runs.map((run) => ({
107 id: run.rootNodeId!,
108 threadId,
109 runId: run.id,
110 parentNodeId: null,
111 rootNodeId: run.rootNodeId!,
112 kind: "root_turn",
113 status: run.status === "completed" ? "completed" : "waiting",
114 countsForRun: true,
115 providerThreadId,
116 providerTurnId: null,
117 nativeItemRef: null,
118 runtimeRequestId: null,
119 checkpointScopeId: null,
120 startedAt: createdAt,
121 completedAt: run.completedAt,
122 })),
123 runtimeRequests: ["pending", "resolved"].map((status) => ({
124 id: RuntimeRequestId.make(`request:delete-plan:${status}`),
125 nodeId: runs[3]!.rootNodeId!,
126 providerTurnId: null,
127 nativeRequestRef: null,
128 kind: "user_input",
129 status: status === "pending" ? "pending" : "resolved",
130 responseCapability: { type: "message" },
131 createdAt,
132 resolvedAt: status === "resolved" ? createdAt : null,
133 })),
134 };
135}
136
137it.effect("cancels active work without reviving a run while disposing delegated completion", () =>
138 Effect.gen(function* () {
139 const base = makeProjection();
140 const parentRun = base.runs.find((run) => run.status === "running")!;
141 const queuedRun = base.runs.find((run) => run.status === "queued")!;
142 const taskId = NodeId.make("task:delete-plan");
143 const projection: OrchestrationV2ThreadProjection = {
144 ...base,
145 runs: base.runs.map((run) =>
146 run.id === parentRun.id
147 ? {
148 ...run,
149 delegatedCompletion: {
150 disposition: "open",
151 nextGeneration: 3,
152 delivery: { generation: 2, messageId: queuedRun.userMessageId, taskIds: [taskId] },
153 },
154 }
155 : run,
156 ),
157 subagents: [
158 {
159 id: taskId,
160 threadId,
161 runId: parentRun.id,
162 parentNodeId: parentRun.rootNodeId!,
163 origin: "app_owned",
164 createdBy: "agent",
165 driver,
166 providerInstanceId,
167 providerThreadId: null,
168 childThreadId: null,
169 nativeTaskRef: null,
170 prompt: "Inspect the project",
171 title: null,
172 model: null,
173 completionDelivery: { state: "claimed", observedByRunId: null },
174 status: "completed",
175 result: "done",
176 startedAt: createdAt,
177 completedAt: createdAt,
178 updatedAt: createdAt,
179 },
180 ],
181 };
182 const plan = yield* planThreadDeletion({
183 command,
184 projection,
185 attachmentIds: projection.messages.flatMap((message) =>
186 message.attachments.map((attachment) => attachment.id),
187 ),
188 now: deletedAt,
189 idAllocator: yield* IdAllocator.IdAllocatorV2,
190 });
191 const deleted = plan.events.reduce(applyToProjection, projection);
192 assert.deepEqual(deleted.thread.deletedAt, deletedAt);
193 assert.equal(deleted.thread.worktreePath, "/workspace/feature");
194 assert.isNull(projection.thread.deletedAt);
195 for (const run of deleted.runs) {
196 assert.equal(run.status, run.id.endsWith(":completed") ? "completed" : "cancelled");
197 assert.deepEqual(run.completedAt, run.id.endsWith(":completed") ? createdAt : deletedAt);
198 }
199 for (const attempt of deleted.attempts) {
200 assert.equal(
201 attempt.status,
202 attempt.runId.endsWith(":completed") ? "completed" : "cancelled",
203 );
204 }
205 for (const node of deleted.nodes) {
206 assert.equal(node.status, node.runId?.endsWith(":completed") ? "completed" : "cancelled");
207 }
208 const pending = deleted.runtimeRequests.find((request) => request.id.endsWith(":pending"))!;
209 assert.equal(pending.status, "cancelled");
210 assert.deepEqual(pending.responseCapability, {
211 type: "not_resumable",
212 reason: "The thread was deleted.",
213 });
214 assert.deepEqual(deleted.runtimeRequests[1], projection.runtimeRequests[1]);
215 assert.deepEqual(deleted.runs.find((run) => run.id === parentRun.id)?.delegatedCompletion, {
216 disposition: "disposed",
217 nextGeneration: 3,
218 delivery: null,
219 });
220 assert.equal(deleted.subagents[0]?.completionDelivery?.state, "disposed");
221 const queuedRunUpdates = plan.events.filter(
222 (event) => event.type === "run.updated" && event.payload.id === queuedRun.id,
223 );
224 assert.lengthOf(queuedRunUpdates, 1);
225 }).pipe(Effect.provide(IdAllocator.layer)),
226);
227
228it.effect("queues provider and resource cleanup and preserves an earlier deletion timestamp", () =>
229 Effect.gen(function* () {
230 const base = makeProjection();
231 const projection: OrchestrationV2ThreadProjection = {
232 ...base,
233 thread: { ...base.thread, deletedAt: createdAt },
234 providerSessions: ["running", "stopped", "error"].map((status) => ({
235 id: ProviderSessionId.make(`session:delete-plan:${status}`),
236 driver,
237 providerInstanceId,
238 status: status === "running" ? "running" : status === "stopped" ? "stopped" : "error",
239 cwd: "/workspace/feature",
240 model: null,
241 capabilities: CodexProviderCapabilitiesV2,
242 createdAt,
243 updatedAt: createdAt,
244 lastError: null,
245 })),
246 messages: [0, 1].map((index) => ({
247 id: MessageId.make(`message:attachment:${index}`),
248 threadId,
249 createdBy: "user",
250 creationSource: "web",
251 runId: null,
252 nodeId: null,
253 role: "user",
254 text: "Inspect this file",
255 attachments: [
256 {
257 type: "file",
258 id: "shared_file",
259 name: "input.txt",
260 mimeType: "text/plain",
261 sizeBytes: 10,
262 },
263 ],
264 streaming: false,
265 createdAt,
266 updatedAt: createdAt,
267 })),
268 };
269 const plan = yield* planThreadDeletion({
270 command,
271 projection,
272 attachmentIds: projection.messages.flatMap((message) =>
273 message.attachments.map((attachment) => attachment.id),
274 ),
275 now: deletedAt,
276 idAllocator: yield* IdAllocator.IdAllocatorV2,
277 });
278 const deleted = plan.events.reduce(applyToProjection, projection);
279 assert.deepEqual(deleted.thread.deletedAt, createdAt);
280 assert.deepEqual(
281 deleted.providerSessions.map((session) => session.status),
282 ["stopped", "error"],
283 );
284 assert.deepEqual(
285 plan.effects.map((effect) => effect.request),
286 [
287 {
288 type: "provider-session.detach",
289 providerSessionId: ProviderSessionId.make("session:delete-plan:running"),
290 detail: "Thread deleted.",
291 revokeMcpCredential: true,
292 },
293 { type: "terminal.cleanup" },
294 { type: "preview.cleanup" },
295 { type: "attachment.cleanup", attachmentIds: ["shared_file"] },
296 ],
297 );
298 }).pipe(Effect.provide(IdAllocator.layer)),
299);