1import { assert, it } from "@effect/vitest";
2import {
3 type ModelSelection,
4 NodeId,
5 RunId,
6 RunAttemptId,
7 MessageId,
8 EventId,
9 TurnItemId,
10 type OrchestrationV2Run,
11 type OrchestrationV2AppThread,
12 ProjectId,
13 ProviderInstanceId,
14 ProviderThreadId,
15 ThreadId,
16} from "@t3tools/contracts";
17import * as DateTime from "effect/DateTime";
18import * as Effect from "effect/Effect";
19
20import {
21 makeSubagentChildThread,
22 delegatedTaskProgress,
23 subagentResultForRun,
24 makeSubagentConversationArtifacts,
25 endOrphanedNativeSubagent,
26 endRunlessRootTurns,
27} from "@t3tools/provider-core/server/subagentProjection";
28
29import { emptyProjection } from "./ProjectionStore.ts";
30
31const parentThreadId = ThreadId.make("thread:subagent-snoozed-parent");
32const childThreadId = ThreadId.make("thread:subagent-awake-child");
33const parentProviderInstanceId = ProviderInstanceId.make("codex");
34const childProviderInstanceId = ProviderInstanceId.make("claude");
35const parentModelSelection = {
36 instanceId: parentProviderInstanceId,
37 model: "gpt-5.4",
38} satisfies ModelSelection;
39const childModelSelection = {
40 instanceId: childProviderInstanceId,
41 model: "claude-opus-4-1",
42} satisfies ModelSelection;
43const parentCreatedAt = DateTime.makeUnsafe("2026-07-24T09:00:00.000Z");
44const snoozedAt = DateTime.makeUnsafe("2026-07-24T09:05:00.000Z");
45const snoozedUntil = DateTime.makeUnsafe("2026-07-25T09:00:00.000Z");
46const childCreatedAt = DateTime.makeUnsafe("2026-07-24T09:10:00.000Z");
47
48function makeParentThread(): OrchestrationV2AppThread {
49 return {
50 createdBy: "user",
51 creationSource: "web",
52 id: parentThreadId,
53 projectId: ProjectId.make("project:subagent-snooze"),
54 title: "Snoozed parent",
55 providerInstanceId: parentProviderInstanceId,
56 modelSelection: parentModelSelection,
57 runtimeMode: "full-access",
58 interactionMode: "plan",
59 branch: "feature/source",
60 worktreePath: "/tmp/source-worktree",
61 activeProviderThreadId: ProviderThreadId.make("provider-thread:subagent-snoozed-parent"),
62 lineage: {
63 parentThreadId: null,
64 relationshipToParent: null,
65 rootThreadId: parentThreadId,
66 },
67 forkedFrom: null,
68 createdAt: parentCreatedAt,
69 updatedAt: snoozedAt,
70 archivedAt: null,
71 settledOverride: null,
72 settledAt: null,
73 lastVisitedAt: null,
74 snoozedUntil,
75 snoozedAt,
76 deletedAt: null,
77 historyOrigin: "v1_import",
78 };
79}
80
81it("keeps a subagent child awake when its parent thread is snoozed", () => {
82 const parentThread = makeParentThread();
83 const childProviderThreadId = ProviderThreadId.make("provider-thread:subagent-awake-child");
84 const parentNodeId = NodeId.make("node:subagent-parent");
85 const childThread = makeSubagentChildThread({
86 parentThread,
87 childThreadId,
88 parentNodeId,
89 activeProviderThreadId: childProviderThreadId,
90 providerInstanceId: childProviderInstanceId,
91 modelSelection: childModelSelection,
92 title: "Awake child",
93 now: childCreatedAt,
94 createdBy: "agent",
95 creationSource: "provider",
96 });
97
98 assert.isNull(childThread.snoozedUntil);
99 assert.isNull(childThread.snoozedAt);
100 assert.equal(childThread.projectId, parentThread.projectId);
101 assert.equal(childThread.runtimeMode, parentThread.runtimeMode);
102 assert.equal(childThread.interactionMode, parentThread.interactionMode);
103 assert.equal(childThread.branch, parentThread.branch);
104 assert.equal(childThread.worktreePath, parentThread.worktreePath);
105 assert.equal(childThread.providerInstanceId, childProviderInstanceId);
106 assert.deepEqual(childThread.modelSelection, childModelSelection);
107 assert.equal(childThread.activeProviderThreadId, childProviderThreadId);
108 assert.isUndefined(childThread.historyOrigin);
109 assert.deepEqual(childThread.lineage, {
110 parentThreadId,
111 relationshipToParent: "subagent",
112 rootThreadId: parentThreadId,
113 });
114 assert.deepEqual(childThread.forkedFrom, {
115 type: "node",
116 nodeId: parentNodeId,
117 });
118});
119
120it("attributes native subagent prompts to their parent thread", () => {
121 for (const role of ["user", "assistant"] as const) {
122 const artifacts = makeSubagentConversationArtifacts({
123 messageId: MessageId.make(`native-${role}`),
124 turnItemId: TurnItemId.make(`native-${role}`),
125 threadId: childThreadId,
126 senderThreadId: parentThreadId,
127 rootNodeId: NodeId.make("child-root"),
128 providerThreadId: null,
129 providerTurnId: null,
130 nativeItemRef: null,
131 role,
132 text: role === "user" ? "Review the changes" : "Review complete",
133 ordinal: 100,
134 now: childCreatedAt,
135 });
136 assert.equal(artifacts.message.threadId, childThreadId);
137 assert.equal(artifacts.message.senderThreadId, role === "user" ? parentThreadId : undefined);
138 if (artifacts.turnItem.type === "user_message") {
139 assert.equal(artifacts.turnItem.senderThreadId, parentThreadId);
140 }
141 }
142});
143
144function taskFixture() {
145 const projection = emptyProjection({
146 type: "thread.created",
147 id: EventId.make("task-fixture"),
148 threadId: parentThreadId,
149 occurredAt: parentCreatedAt,
150 payload: makeParentThread(),
151 });
152 const run: OrchestrationV2Run = {
153 id: RunId.make("initial"),
154 threadId: parentThreadId,
155 ordinal: 1,
156 providerInstanceId: parentProviderInstanceId,
157 modelSelection: parentModelSelection,
158 providerThreadId: null,
159 userMessageId: MessageId.make("prompt"),
160 rootNodeId: NodeId.make("root"),
161 activeAttemptId: RunAttemptId.make("attempt"),
162 status: "completed",
163 requestedAt: parentCreatedAt,
164 startedAt: parentCreatedAt,
165 completedAt: childCreatedAt,
166 checkpointId: null,
167 contextHandoffId: null,
168 };
169 return { projection: { ...projection, runs: [run] }, run };
170}
171
172it("reports the run that ended last, not the highest ordinal", () => {
173 const { projection, run } = taskFixture();
174 // A restart continuation (ordinal 4) ran ahead of held queued runs 2 and 3.
175 const ended = (ordinal: number, completedAt: string): OrchestrationV2Run => ({
176 ...run,
177 id: RunId.make(`run:${ordinal}`),
178 ordinal,
179 completedAt: DateTime.makeUnsafe(completedAt),
180 });
181 const progress = delegatedTaskProgress({
182 ...projection,
183 runs: [
184 { ...run, status: "cancelled" },
185 ended(4, "2026-07-24T10:00:00.000Z"),
186 ended(2, "2026-07-24T10:05:00.000Z"),
187 ended(3, "2026-07-24T10:10:00.000Z"),
188 ],
189 });
190 assert.equal(progress.state, "result_available");
191 assert.equal(progress.resultRun?.ordinal, 3);
192});
193
194it("waits for nested work and retains the report across monitor acknowledgements", () => {
195 const { projection, run } = taskFixture();
196 assert.equal(
197 delegatedTaskProgress({ ...projection, subagents: [{ status: "running" }] }).state,
198 "waiting_for_children",
199 );
200 assert.equal(
201 delegatedTaskProgress({ ...projection, subagents: [{ status: "completed" }] }).state,
202 "result_available",
203 );
204 assert.equal(
205 delegatedTaskProgress({ ...projection, subagents: [{ status: "idle" }] }).state,
206 "result_available",
207 );
208 for (const state of ["pending", "claimed"] as const) {
209 assert.equal(
210 delegatedTaskProgress({
211 ...projection,
212 subagents: [{ status: "completed", completionDelivery: { state, observedByRunId: null } }],
213 }).state,
214 "waiting_for_children",
215 );
216 }
217 for (const state of ["acknowledged", "delivered", "disposed"] as const) {
218 assert.equal(
219 delegatedTaskProgress({
220 ...projection,
221 subagents: [{ status: "completed", completionDelivery: { state, observedByRunId: null } }],
222 }).state,
223 "result_available",
224 );
225 }
226 assert.equal(
227 delegatedTaskProgress({
228 ...projection,
229 providerThreads: [
230 { pendingBackgroundTasks: [{ taskId: "background-audit", kind: "command" }] },
231 ],
232 }).state,
233 "waiting_for_children",
234 );
235 const pending = {
236 ...run,
237 id: RunId.make("followup"),
238 ordinal: 2,
239 status: "queued" as const,
240 startedAt: null,
241 };
242 assert.equal(delegatedTaskProgress({ ...projection, runs: [run, pending] }).state, "working");
243 // Stop or a restart holds queued wakes for the user; they are not work the task owes.
244 const cancelled = { ...run, status: "cancelled" as const };
245 const held = delegatedTaskProgress({
246 ...projection,
247 runs: [cancelled, { ...pending, queueHeld: true }],
248 });
249 assert.equal(held.state, "result_available");
250 assert.equal(held.resultRun?.id, cancelled.id);
251 const report = { ...pending, status: "completed" as const, startedAt: parentCreatedAt };
252 const monitor = { ...report, id: RunId.make("monitor"), ordinal: 3 };
253 const artifacts = makeSubagentConversationArtifacts({
254 messageId: MessageId.make("monitor-message"),
255 turnItemId: TurnItemId.make("monitor-item"),
256 threadId: parentThreadId,
257 rootNodeId: NodeId.make("root"),
258 providerThreadId: null,
259 providerTurnId: null,
260 nativeItemRef: null,
261 role: "user",
262 text: "Monitor update",
263 ordinal: 3,
264 now: childCreatedAt,
265 });
266 const progress = delegatedTaskProgress({
267 ...projection,
268 runs: [run, report, monitor],
269 messages: [
270 {
271 ...artifacts.message,
272 runId: monitor.id,
273 notification: {
274 source: { kind: "monitor" },
275 outcome: "updated",
276 summary: "Monitor update",
277 },
278 },
279 ],
280 });
281 assert.equal(progress.state, "result_available");
282 assert.equal(progress.resultRun?.id, report.id);
283});
284
285it("exposes the provider failure rather than a progress message from the failed run", () => {
286 const { projection, run } = taskFixture();
287 const failedRun = { ...run, status: "failed" as const };
288 const artifacts = makeSubagentConversationArtifacts({
289 messageId: MessageId.make("progress"),
290 turnItemId: TurnItemId.make("error"),
291 threadId: parentThreadId,
292 rootNodeId: NodeId.make("root"),
293 providerThreadId: null,
294 providerTurnId: null,
295 nativeItemRef: null,
296 role: "assistant",
297 text: "Starting the audit.",
298 ordinal: 1,
299 now: childCreatedAt,
300 });
301 if (artifacts.turnItem.type !== "assistant_message") throw new Error("Expected assistant item");
302 const failure = {
303 class: "unknown" as const,
304 message: "You've hit your usage limit. Try again Sep 14 at 7:20 PM.",
305 code: null,
306 retryable: false,
307 };
308 const result = subagentResultForRun(
309 {
310 ...projection,
311 messages: [{ ...artifacts.message, runId: run.id }],
312 turnItems: [{ ...artifacts.turnItem, type: "error", runId: run.id, failure }],
313 },
314 failedRun,
315 );
316 assert.equal(result.text, failure.message);
317 assert.equal(result.turnItemId, artifacts.turnItem.id);
318 assert.isNull(result.messageId);
319});
320
321it.effect("ends an orphaned native subagent's node when only its record already ended", () =>
322 Effect.gen(function* () {
323 const id = NodeId.make("node:orphaned-native-subagent");
324 const subagent = { id, origin: "provider_native", status: "running", runId: null };
325 const events = yield* endOrphanedNativeSubagent({
326 projection: {
327 subagents: [subagent],
328 nodes: [{ id, status: "running", runId: null }],
329 turnItems: [],
330 } as never,
331 subagentId: id,
332 status: "cancelled",
333 now: yield* DateTime.now,
334 emitted: [
335 { type: "subagent.updated", payload: { ...subagent, status: "cancelled" } },
336 ] as never,
337 allocateEventId: () => Effect.succeed(EventId.make("event:orphaned-native-subagent")),
338 });
339 assert.deepEqual(
340 events.map((event) => [event.type, event.type === "node.updated" && event.payload.status]),
341 [["node.updated", "cancelled"]],
342 );
343 }),
344);
345
346it.effect("ends a runless root turn's items when its node already ended", () =>
347 Effect.gen(function* () {
348 const root = {
349 id: NodeId.make("node:runless-root"),
350 kind: "root_turn",
351 status: "running",
352 runId: null,
353 };
354 const item = (id: string) => ({ id, nodeId: root.id, runId: null, status: "running" });
355 const events = yield* endRunlessRootTurns({
356 threadId: childThreadId,
357 providerInstanceId: parentProviderInstanceId,
358 projection: {
359 nodes: [root],
360 turnItems: [item("turn-item:command"), item("turn-item:reasoning")],
361 } as never,
362 status: "cancelled",
363 now: yield* DateTime.now,
364 emitted: [
365 {
366 type: "turn-item.updated",
367 payload: { ...item("turn-item:command"), status: "cancelled" },
368 },
369 { type: "node.updated", payload: { ...root, status: "cancelled" } },
370 ] as never,
371 allocateEventId: () => Effect.succeed(EventId.make("event:runless-root")),
372 });
373 assert.deepEqual(
374 events.map(
375 (event) => `${event.type} ${event.type === "turn-item.updated" && event.payload.id}`,
376 ),
377 ["turn-item.updated turn-item:reasoning"],
378 );
379 }),
380);