1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import {
4 CommandId,
5 EnvironmentId,
6 NodeId,
7 type OrchestrationV2ThreadShell,
8 type ScheduledTask,
9 ScheduledTaskId,
10 ProjectId,
11 ProviderDriverKind,
12 ProviderInstanceId,
13 RunId,
14 ThreadId,
15 type OrchestrationV2ThreadProjection,
16 type ServerProvider,
17} from "@t3tools/contracts";
18import * as DateTime from "effect/DateTime";
19import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";
21import * as Ref from "effect/Ref";
22
23import {
24 DispatchModeLimit,
25 type DispatchModeRefusal,
26} from "../orchestration-v2/DispatchModeLimit.ts";
27import {
28 OrchestratorProjectionError,
29 OrchestratorThreadAboveModeLimitError,
30} from "../orchestration-v2/Orchestrator.ts";
31import * as ProviderAdapterRegistry from "../orchestration-v2/ProviderAdapterRegistry.ts";
32import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
33import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
34import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
35import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
36import { buildUnavailableProviderSnapshot } from "../provider/unavailableProviderSnapshot.ts";
37import * as ProjectService from "../project/ProjectService.ts";
38import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
39import * as ServerSettings from "../serverSettings.ts";
40import * as SecretRequests from "../secrets/SecretRequests.ts";
41import type { McpInvocationScope } from "./McpInvocationContext.ts";
42import { idleThreadProjection, liveThreadShell } from "./McpToolAccess.testkit.ts";
43import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
44import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
45
46const layerOrchestratorService = OrchestratorMcpService.layer.pipe(
47 Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})),
48 Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})),
49);
50
51describe("OrchestratorMcpService", () => {
52 it.effect("retries terminal acknowledgement with a fresh command id", () =>
53 Effect.gen(function* () {
54 const parentThreadId = ThreadId.make("thread:mcp-ack-parent");
55 const childThreadId = ThreadId.make("thread:mcp-ack-child");
56 const childRunId = RunId.make("run:mcp-ack-child");
57 const taskId = NodeId.make("node:mcp-ack-task");
58 const acknowledgementCommandIds = yield* Ref.make<ReadonlyArray<string>>([]);
59 const acknowledgementAttempts = yield* Ref.make(0);
60 const parentProjection = {
61 thread: { id: parentThreadId },
62 runs: [],
63 contextTransfers: [],
64 subagents: [
65 {
66 id: taskId,
67 threadId: parentThreadId,
68 origin: "app_owned",
69 childThreadId,
70 driver: "codex",
71 model: "gpt-5.6-terra",
72 result: "terminal result",
73 completionDelivery: { state: "pending" },
74 },
75 ],
76 } as unknown as OrchestrationV2ThreadProjection;
77 const childProjection = {
78 thread: { id: childThreadId },
79 runs: [
80 {
81 id: childRunId,
82 ordinal: 1,
83 status: "completed",
84 startedAt: DateTime.makeUnsafe("2026-10-03T10:00:00Z"),
85 completedAt: DateTime.makeUnsafe("2026-10-03T10:10:00Z"),
86 },
87 {
88 id: RunId.make("run:mcp-ack-continuation"),
89 ordinal: 2,
90 status: "failed",
91 startedAt: DateTime.makeUnsafe("2026-10-03T10:02:00Z"),
92 completedAt: DateTime.makeUnsafe("2026-10-03T10:05:00Z"),
93 },
94 ],
95 contextTransfers: [],
96 messages: [],
97 subagents: [],
98 providerThreads: [],
99 runtimeRequests: [],
100 } as unknown as OrchestrationV2ThreadProjection;
101 let hasNestedWork = true;
102 const layerDependencies = Layer.mergeAll(
103 NodeServices.layer,
104 Layer.mock(ThreadManagementService.ThreadManagementService)({
105 getThreadRecords: (threadId) =>
106 Effect.succeed(
107 threadId === parentThreadId
108 ? hasNestedWork
109 ? {
110 ...parentProjection,
111 subagents: parentProjection.subagents.map((task) => ({
112 ...task,
113 result: null,
114 status: "running" as const,
115 })),
116 }
117 : parentProjection
118 : hasNestedWork
119 ? {
120 ...childProjection,
121 subagents: [
122 { ...parentProjection.subagents[0]!, status: "running" as const },
123 ],
124 }
125 : childProjection,
126 ),
127 dispatch: (command) =>
128 Ref.update(acknowledgementCommandIds, (commandIds) => [
129 ...commandIds,
130 String(command.commandId),
131 ]).pipe(
132 Effect.andThen(Ref.updateAndGet(acknowledgementAttempts, (count) => count + 1)),
133 Effect.flatMap((attempt) =>
134 attempt === 1
135 ? Effect.fail(new Error("simulated acknowledgement failure") as never)
136 : Effect.succeed({} as never),
137 ),
138 ),
139 }),
140 ServerSettings.layerTest(),
141 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
142 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
143 list: () => Effect.succeed([]),
144 }),
145 Layer.mock(ProjectService.ProjectService)({}),
146 Layer.mock(SecretRequests.SecretRequests)({}),
147 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
148 );
149 const scope: McpInvocationScope = {
150 environmentId: EnvironmentId.make("environment:mcp-ack"),
151 requestNamespace: "provider-session:mcp-ack",
152 thread: {
153 threadId: parentThreadId,
154 providerSessionId: "provider-session:mcp-ack",
155 providerInstanceId: ProviderInstanceId.make("codex"),
156 },
157 client: undefined,
158 capabilities: new Set(["orchestration"]),
159 issuedAt: 1,
160 };
161
162 yield* Effect.gen(function* () {
163 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
164 const pending = yield* service.taskStatus(scope, taskId);
165 assert.equal(pending.status, "running");
166 assert.equal(pending.workState, "waiting_for_children");
167 assert.isNull(pending.summary);
168 assert.equal(yield* Ref.get(acknowledgementAttempts), 0);
169 hasNestedWork = false;
170 const error = yield* service.taskStatus(scope, taskId).pipe(Effect.flip);
171 assert.equal(error.code, "orchestration_error");
172
173 const result = yield* service.taskStatus(scope, taskId);
174 assert.equal(result.status, "completed");
175 assert.equal(result.summary, "terminal result");
176 assert.equal(result.latestTerminalRunId, childRunId);
177 assert.equal(result.latestTerminalStatus, "completed");
178 const commandIds = yield* Ref.get(acknowledgementCommandIds);
179 assert.equal(commandIds.length, 2);
180 assert.notEqual(commandIds[0], commandIds[1]);
181 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
182 }),
183 );
184
185 it.effect("reports a restart-cut child as working until its continuation settles", () =>
186 Effect.gen(function* () {
187 const parentThreadId = ThreadId.make("thread:mcp-restart-parent");
188 const childThreadId = ThreadId.make("thread:mcp-restart-child");
189 const taskId = NodeId.make("node:mcp-restart-task");
190 const dispatched = yield* Ref.make(0);
191 let awaitsRestart = true;
192 let readFails = true;
193 const parentProjection = {
194 thread: { id: parentThreadId },
195 runs: [],
196 contextTransfers: [],
197 subagents: [
198 {
199 id: taskId,
200 threadId: parentThreadId,
201 origin: "app_owned",
202 childThreadId,
203 driver: "codex",
204 model: "gpt-5.6-terra",
205 status: "running",
206 result: null,
207 completionDelivery: { state: "pending" },
208 },
209 ],
210 } as unknown as OrchestrationV2ThreadProjection;
211 const childProjection = {
212 thread: { id: childThreadId },
213 runs: [
214 {
215 id: RunId.make("run:mcp-restart-child"),
216 ordinal: 1,
217 status: "cancelled",
218 delegatedTaskId: taskId,
219 },
220 // An unrelated later run cannot supply this follow-up's result.
221 { id: RunId.make("run:mcp-restart-later"), ordinal: 2, status: "running" },
222 ],
223 contextTransfers: [],
224 messages: [],
225 subagents: [],
226 providerThreads: [],
227 runtimeRequests: [],
228 turnItems: [],
229 } as unknown as OrchestrationV2ThreadProjection;
230 const layerDependencies = Layer.mergeAll(
231 NodeServices.layer,
232 Layer.mock(ThreadManagementService.ThreadManagementService)({
233 getThreadRecords: (threadId) =>
234 Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection),
235 delegatedTaskResultPending: () =>
236 readFails
237 ? Effect.fail(new OrchestratorProjectionError({ threadId: childThreadId }))
238 : Effect.succeed(awaitsRestart),
239 dispatch: () => Ref.update(dispatched, (count) => count + 1).pipe(Effect.as({} as never)),
240 }),
241 ServerSettings.layerTest(),
242 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
243 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
244 list: () => Effect.succeed([]),
245 }),
246 Layer.mock(ProjectService.ProjectService)({}),
247 Layer.mock(SecretRequests.SecretRequests)({}),
248 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
249 );
250 const scope: McpInvocationScope = {
251 environmentId: EnvironmentId.make("environment:mcp-restart"),
252 requestNamespace: "provider-session:mcp-restart",
253 thread: {
254 threadId: parentThreadId,
255 providerSessionId: "provider-session:mcp-restart",
256 providerInstanceId: ProviderInstanceId.make("codex"),
257 },
258 client: undefined,
259 capabilities: new Set(["orchestration"]),
260 issuedAt: 1,
261 };
262
263 yield* Effect.gen(function* () {
264 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
265 const failed = yield* service.taskStatus(scope, taskId).pipe(Effect.flip);
266 assert.equal(failed.code, "orchestration_error");
267 assert.equal(yield* Ref.get(dispatched), 0);
268 readFails = false;
269 const held = yield* service.taskStatus(scope, taskId);
270 assert.equal(held.status, "running");
271 assert.equal(held.workState, "working");
272 assert.isNull(held.summary);
273 // Acknowledging the cut run would suppress the real result's wake.
274 assert.equal(yield* Ref.get(dispatched), 0);
275 awaitsRestart = false;
276 const settled = yield* service.taskStatus(scope, taskId);
277 assert.equal(settled.status, "cancelled");
278 assert.equal(yield* Ref.get(dispatched), 1);
279 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
280 }),
281 );
282
283 it.effect("does not dispose delivery when a nonterminal task has no active child run", () =>
284 Effect.gen(function* () {
285 const parentThreadId = ThreadId.make("thread:mcp-cancel-parent");
286 const childThreadId = ThreadId.make("thread:mcp-cancel-child");
287 const taskId = NodeId.make("node:mcp-cancel-task");
288 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
289 const parentProjection = {
290 thread: { id: parentThreadId },
291 runs: [],
292 contextTransfers: [],
293 subagents: [
294 {
295 id: taskId,
296 threadId: parentThreadId,
297 origin: "app_owned",
298 childThreadId,
299 driver: "codex",
300 model: "gpt-5.6-terra",
301 result: null,
302 completionDelivery: { state: "pending" },
303 },
304 ],
305 } as unknown as OrchestrationV2ThreadProjection;
306 const childProjection = {
307 thread: { id: childThreadId },
308 runs: [],
309 contextTransfers: [],
310 messages: [],
311 subagents: [],
312 providerThreads: [],
313 runtimeRequests: [],
314 } as unknown as OrchestrationV2ThreadProjection;
315 const layerDependencies = Layer.mergeAll(
316 NodeServices.layer,
317 Layer.mock(ThreadManagementService.ThreadManagementService)({
318 getThreadRecords: (threadId) =>
319 Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection),
320 // The child still runs within the parent's modes.
321 getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)),
322 dispatch: (command) =>
323 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
324 Effect.as({} as never),
325 ),
326 }),
327 ServerSettings.layerTest(),
328 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
329 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
330 list: () => Effect.succeed([]),
331 }),
332 Layer.mock(ProjectService.ProjectService)({}),
333 Layer.mock(SecretRequests.SecretRequests)({}),
334 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
335 );
336 const scope: McpInvocationScope = {
337 environmentId: EnvironmentId.make("environment:mcp-cancel"),
338 requestNamespace: "provider-session:mcp-cancel",
339 thread: {
340 threadId: parentThreadId,
341 providerSessionId: "provider-session:mcp-cancel",
342 providerInstanceId: ProviderInstanceId.make("codex"),
343 },
344 client: undefined,
345 capabilities: new Set(["orchestration"]),
346 issuedAt: 1,
347 };
348
349 yield* Effect.gen(function* () {
350 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
351 const error = yield* service
352 .cancelTask(scope, { taskId, clientRequestId: "cancel-unstarted-task" })
353 .pipe(Effect.flip);
354 assert.equal(error.code, "task_not_cancellable");
355 assert.deepEqual(yield* Ref.get(dispatched), []);
356 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
357 }),
358 );
359
360 it.effect("does not dispose delivery when the child stop fails", () =>
361 Effect.gen(function* () {
362 const parentThreadId = ThreadId.make("thread:mcp-cancel-failed-parent");
363 const childThreadId = ThreadId.make("thread:mcp-cancel-failed-child");
364 const childRunId = RunId.make("run:mcp-cancel-failed-child");
365 const taskId = NodeId.make("node:mcp-cancel-failed-task");
366 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
367 const parentProjection = {
368 thread: { id: parentThreadId },
369 runs: [],
370 contextTransfers: [],
371 subagents: [
372 {
373 id: taskId,
374 threadId: parentThreadId,
375 origin: "app_owned",
376 childThreadId,
377 driver: "codex",
378 model: "gpt-5.6-terra",
379 result: null,
380 completionDelivery: { state: "pending" },
381 },
382 ],
383 } as unknown as OrchestrationV2ThreadProjection;
384 const childProjection = {
385 thread: { id: childThreadId },
386 runs: [{ id: childRunId, status: "running" }],
387 contextTransfers: [],
388 messages: [],
389 subagents: [],
390 providerThreads: [],
391 runtimeRequests: [],
392 } as unknown as OrchestrationV2ThreadProjection;
393 const layerDependencies = Layer.mergeAll(
394 NodeServices.layer,
395 Layer.mock(ThreadManagementService.ThreadManagementService)({
396 getThreadRecords: (threadId) =>
397 Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection),
398 // The child still runs within the parent's modes.
399 getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)),
400 dispatch: (command) =>
401 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
402 Effect.andThen(Effect.fail(new Error("simulated stop failure") as never)),
403 ),
404 }),
405 ServerSettings.layerTest(),
406 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
407 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
408 list: () => Effect.succeed([]),
409 }),
410 Layer.mock(ProjectService.ProjectService)({}),
411 Layer.mock(SecretRequests.SecretRequests)({}),
412 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
413 );
414 const scope: McpInvocationScope = {
415 environmentId: EnvironmentId.make("environment:mcp-cancel-failed"),
416 requestNamespace: "provider-session:mcp-cancel-failed",
417 thread: {
418 threadId: parentThreadId,
419 providerSessionId: "provider-session:mcp-cancel-failed",
420 providerInstanceId: ProviderInstanceId.make("codex"),
421 },
422 client: undefined,
423 capabilities: new Set(["orchestration"]),
424 issuedAt: 1,
425 };
426
427 yield* Effect.gen(function* () {
428 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
429 const error = yield* service
430 .cancelTask(scope, { taskId, clientRequestId: "cancel-failed-task" })
431 .pipe(Effect.flip);
432 assert.equal(error.code, "task_not_cancellable");
433 assert.deepEqual(
434 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),
435 ["thread.stop"],
436 );
437 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
438 }),
439 );
440
441 it.effect("returns cancel requested when post-stop disposal fails", () =>
442 Effect.gen(function* () {
443 const parentThreadId = ThreadId.make("thread:mcp-cancel-dispose-failed-parent");
444 const childThreadId = ThreadId.make("thread:mcp-cancel-dispose-failed-child");
445 const childRunId = RunId.make("run:mcp-cancel-dispose-failed-child");
446 const taskId = NodeId.make("node:mcp-cancel-dispose-failed-task");
447 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
448 const parentProjection = {
449 thread: { id: parentThreadId },
450 runs: [],
451 contextTransfers: [],
452 subagents: [
453 {
454 id: taskId,
455 threadId: parentThreadId,
456 origin: "app_owned",
457 childThreadId,
458 driver: "codex",
459 model: "gpt-5.6-terra",
460 result: null,
461 completionDelivery: { state: "pending" },
462 },
463 ],
464 } as unknown as OrchestrationV2ThreadProjection;
465 const childProjection = {
466 thread: { id: childThreadId },
467 runs: [{ id: childRunId, status: "running" }],
468 contextTransfers: [],
469 messages: [],
470 subagents: [],
471 providerThreads: [],
472 runtimeRequests: [],
473 } as unknown as OrchestrationV2ThreadProjection;
474 const layerDependencies = Layer.mergeAll(
475 NodeServices.layer,
476 Layer.mock(ThreadManagementService.ThreadManagementService)({
477 getThreadRecords: (threadId) =>
478 Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection),
479 // The child still runs within the parent's modes.
480 getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)),
481 dispatch: (command) =>
482 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
483 Effect.andThen(
484 command.type === "delegated_task.completion-delivery.dispose"
485 ? Effect.fail(new Error("simulated disposal failure") as never)
486 : Effect.succeed({} as never),
487 ),
488 ),
489 stopDelegatedTasks: () => Effect.void,
490 }),
491 ServerSettings.layerTest(),
492 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
493 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
494 list: () => Effect.succeed([]),
495 }),
496 Layer.mock(ProjectService.ProjectService)({}),
497 Layer.mock(SecretRequests.SecretRequests)({}),
498 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
499 );
500 const scope: McpInvocationScope = {
501 environmentId: EnvironmentId.make("environment:mcp-cancel-dispose-failed"),
502 requestNamespace: "provider-session:mcp-cancel-dispose-failed",
503 thread: {
504 threadId: parentThreadId,
505 providerSessionId: "provider-session:mcp-cancel-dispose-failed",
506 providerInstanceId: ProviderInstanceId.make("codex"),
507 },
508 client: undefined,
509 capabilities: new Set(["orchestration"]),
510 issuedAt: 1,
511 };
512
513 yield* Effect.gen(function* () {
514 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
515 const result = yield* service.cancelTask(scope, {
516 taskId,
517 clientRequestId: "cancel-dispose-failed-task",
518 });
519 assert.equal(result.status, "cancel_requested");
520 assert.deepEqual(
521 (yield* Ref.get(dispatched)).map((command) => (command as { type: string }).type),
522 ["thread.stop", "delegated_task.completion-delivery.dispose"],
523 );
524 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
525 }),
526 );
527
528 it.effect("refuses to cancel a task whose child now runs above the parent's modes", () =>
529 Effect.gen(function* () {
530 const parentThreadId = ThreadId.make("thread:mcp-cancel-dispose-failed-parent");
531 const childThreadId = ThreadId.make("thread:mcp-cancel-dispose-failed-child");
532 const childRunId = RunId.make("run:mcp-cancel-dispose-failed-child");
533 const taskId = NodeId.make("node:mcp-cancel-dispose-failed-task");
534 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
535 // The parent runs Supervised in plan mode.
536 const parentProjection = {
537 thread: { id: parentThreadId, runtimeMode: "approval-required", interactionMode: "plan" },
538 runs: [],
539 contextTransfers: [],
540 subagents: [
541 {
542 id: taskId,
543 threadId: parentThreadId,
544 origin: "app_owned",
545 childThreadId,
546 driver: "codex",
547 model: "gpt-5.6-terra",
548 result: null,
549 completionDelivery: { state: "pending" },
550 },
551 ],
552 } as unknown as OrchestrationV2ThreadProjection;
553 const childProjection = {
554 thread: { id: childThreadId },
555 runs: [{ id: childRunId, status: "running" }],
556 contextTransfers: [],
557 messages: [],
558 subagents: [],
559 providerThreads: [],
560 runtimeRequests: [],
561 } as unknown as OrchestrationV2ThreadProjection;
562 const layerDependencies = Layer.mergeAll(
563 NodeServices.layer,
564 Layer.mock(ThreadManagementService.ThreadManagementService)({
565 getThreadRecords: (threadId) =>
566 Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection),
567 // Its user has since raised the child to full access.
568 getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)),
569 dispatch: (command) =>
570 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
571 Effect.andThen(
572 command.type === "delegated_task.completion-delivery.dispose"
573 ? Effect.fail(new Error("simulated disposal failure") as never)
574 : Effect.succeed({} as never),
575 ),
576 ),
577 stopDelegatedTasks: () => Effect.void,
578 }),
579 ServerSettings.layerTest(),
580 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
581 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
582 list: () => Effect.succeed([]),
583 }),
584 Layer.mock(ProjectService.ProjectService)({}),
585 Layer.mock(SecretRequests.SecretRequests)({}),
586 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
587 );
588 const scope: McpInvocationScope = {
589 environmentId: EnvironmentId.make("environment:mcp-cancel-dispose-failed"),
590 requestNamespace: "provider-session:mcp-cancel-dispose-failed",
591 thread: {
592 threadId: parentThreadId,
593 providerSessionId: "provider-session:mcp-cancel-dispose-failed",
594 providerInstanceId: ProviderInstanceId.make("codex"),
595 },
596 client: undefined,
597 capabilities: new Set(["orchestration"]),
598 issuedAt: 1,
599 };
600
601 yield* Effect.gen(function* () {
602 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
603 const error = yield* service
604 .cancelTask(scope, { taskId, clientRequestId: "cancel-above-modes" })
605 .pipe(Effect.flip);
606 assert.equal(error.code, "runtime_mode_escalation_denied");
607 assert.deepEqual(yield* Ref.get(dispatched), []);
608 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
609 }),
610 );
611
612 /**
613 * A Supervised parent cancels its Supervised child, under which a task
614 * now runs at full access. Reports what the cancel did.
615 */
616 const cancelOverRaisedGrandchild = (child: {
617 readonly deleted: boolean;
618 /** The grandchild passes the check, and its user raises it before the stop reaches it. */
619 readonly raisedDuringStop?: boolean;
620 }) =>
621 Effect.gen(function* () {
622 const parentThreadId = ThreadId.make("thread:mcp-cancel-grandchild-parent");
623 const childThreadId = ThreadId.make("thread:mcp-cancel-grandchild-child");
624 const grandchildThreadId = ThreadId.make("thread:mcp-cancel-grandchild-grandchild");
625 const taskId = NodeId.make("node:mcp-cancel-grandchild-task");
626 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
627 const stoppedBelow = yield* Ref.make(false);
628 const appOwnedTask = (id: string, threadId: ThreadId, childId: ThreadId) => ({
629 id: NodeId.make(id),
630 threadId,
631 origin: "app_owned",
632 childThreadId: childId,
633 driver: "codex",
634 model: "gpt-5.6-terra",
635 result: null,
636 completionDelivery: { state: "pending" },
637 });
638 // A Supervised parent delegated a Supervised child, which delegated a task of its own.
639 const projections = new Map([
640 [
641 parentThreadId,
642 {
643 thread: {
644 id: parentThreadId,
645 runtimeMode: "approval-required",
646 interactionMode: "default",
647 },
648 runs: [],
649 contextTransfers: [],
650 subagents: [appOwnedTask(taskId, parentThreadId, childThreadId)],
651 },
652 ],
653 [
654 childThreadId,
655 {
656 thread: { id: childThreadId },
657 runs: [{ id: RunId.make("run:mcp-cancel-grandchild-child"), status: "running" }],
658 contextTransfers: [],
659 messages: [],
660 subagents: [
661 appOwnedTask("node:mcp-cancel-grandchild-below", childThreadId, grandchildThreadId),
662 ],
663 providerThreads: [],
664 runtimeRequests: [],
665 },
666 ],
667 [
668 grandchildThreadId,
669 {
670 thread: { id: grandchildThreadId },
671 runs: [],
672 contextTransfers: [],
673 messages: [],
674 subagents: [],
675 providerThreads: [],
676 runtimeRequests: [],
677 },
678 ],
679 ]) as unknown as ReadonlyMap<ThreadId, OrchestrationV2ThreadProjection>;
680 const layerDependencies = Layer.mergeAll(
681 NodeServices.layer,
682 Layer.mock(ThreadManagementService.ThreadManagementService)({
683 getThreadRecords: (threadId) => Effect.succeed(projections.get(threadId)!),
684 // The child still runs Supervised; its user has since raised the task under it
685 // to full access.
686 getThreadShell: (threadId) =>
687 Effect.succeed(
688 threadId === grandchildThreadId && child.raisedDuringStop !== true
689 ? liveThreadShell(threadId)
690 : threadId === childThreadId && child.deleted
691 ? null
692 : liveThreadShell(threadId, { runtimeMode: "approval-required" }),
693 ),
694 dispatch: (command) =>
695 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
696 Effect.as({} as never),
697 ),
698 // Stands in for the orchestrator, which finds the grandchild raised
699 // under its lock, when the stop runs under the parent's limit.
700 stopDelegatedTasks: () =>
701 Effect.gen(function* () {
702 const limit = yield* DispatchModeLimit;
703 if (child.raisedDuringStop === true && limit?.refused !== undefined) {
704 const refusal: DispatchModeRefusal = {
705 threadId: grandchildThreadId,
706 mode: "runtime",
707 runtimeMode: "full-access",
708 interactionMode: "default",
709 };
710 yield* Ref.set(limit.refused, refusal);
711 return yield* new OrchestratorThreadAboveModeLimitError({
712 commandId: CommandId.make("stop-grandchild"),
713 threadId: grandchildThreadId,
714 mode: "runtime",
715 runtimeMode: "full-access",
716 interactionMode: "default",
717 });
718 }
719 yield* Ref.set(stoppedBelow, true);
720 }),
721 }),
722 ServerSettings.layerTest(),
723 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
724 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
725 list: () => Effect.succeed([]),
726 }),
727 Layer.mock(ProjectService.ProjectService)({}),
728 Layer.mock(SecretRequests.SecretRequests)({}),
729 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
730 );
731 const scope: McpInvocationScope = {
732 environmentId: EnvironmentId.make("environment:mcp-cancel-grandchild"),
733 requestNamespace: "provider-session:mcp-cancel-grandchild",
734 thread: {
735 threadId: parentThreadId,
736 providerSessionId: "provider-session:mcp-cancel-grandchild",
737 providerInstanceId: ProviderInstanceId.make("codex"),
738 },
739 client: undefined,
740 capabilities: new Set(["orchestration"]),
741 issuedAt: 1,
742 };
743
744 return yield* Effect.gen(function* () {
745 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
746 const error = yield* service
747 .cancelTask(scope, { taskId, clientRequestId: "cancel-grandchild-above-modes" })
748 .pipe(Effect.flip);
749 return {
750 code: error.code,
751 dispatched: yield* Ref.get(dispatched),
752 stoppedBelow: yield* Ref.get(stoppedBelow),
753 };
754 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
755 });
756
757 it.effect("refuses to cancel a task when a task under it now runs above the parent's modes", () =>
758 Effect.gen(function* () {
759 assert.deepEqual(yield* cancelOverRaisedGrandchild({ deleted: false }), {
760 code: "runtime_mode_escalation_denied",
761 dispatched: [],
762 stoppedBelow: false,
763 });
764 }),
765 );
766
767 it.effect("reports a task under the child that its user raises while it is being stopped", () =>
768 Effect.gen(function* () {
769 const outcome = yield* cancelOverRaisedGrandchild({ deleted: false, raisedDuringStop: true });
770 assert.equal(outcome.code, "runtime_mode_escalation_denied");
771 assert.isFalse(outcome.stoppedBelow);
772 }),
773 );
774
775 it.effect("checks the tasks under a deleted child before cancelling it", () =>
776 Effect.gen(function* () {
777 assert.deepEqual(yield* cancelOverRaisedGrandchild({ deleted: true }), {
778 code: "runtime_mode_escalation_denied",
779 dispatched: [],
780 stoppedBelow: false,
781 });
782 }),
783 );
784});
785
786describe("OrchestratorMcpService provider resolution", () => {
787 const parentThreadId = ThreadId.make("thread:mcp-providers-parent");
788 const childThreadId = ThreadId.make("thread:mcp-providers-child");
789 const parentRunId = RunId.make("run:mcp-providers-parent");
790 const parentNodeId = NodeId.make("node:mcp-providers-root");
791 const taskId = NodeId.make("node:mcp-providers-task");
792 const projectId = ProjectId.make("project:mcp-providers");
793 const codexInstanceId = ProviderInstanceId.make("codex");
794 const antigravityInstanceId = ProviderInstanceId.make("antigravity");
795
796 const scope: McpInvocationScope = {
797 environmentId: EnvironmentId.make("environment:mcp-providers"),
798 requestNamespace: "provider-session:mcp-providers",
799 thread: {
800 threadId: parentThreadId,
801 providerSessionId: "provider-session:mcp-providers",
802 providerInstanceId: codexInstanceId,
803 },
804 client: undefined,
805 capabilities: new Set(["orchestration"]),
806 issuedAt: 1,
807 };
808
809 const providerSnapshot = (input: {
810 readonly instanceId: ProviderInstanceId;
811 readonly driver: ProviderDriverKind;
812 readonly model?: string;
813 readonly enabled?: boolean;
814 }): ServerProvider => ({
815 instanceId: input.instanceId,
816 driver: input.driver,
817 enabled: input.enabled ?? true,
818 installed: true,
819 version: "test",
820 status: "ready",
821 auth: { status: "authenticated" },
822 checkedAt: "2026-09-13T00:00:00.000Z",
823 models:
824 input.model === undefined
825 ? []
826 : [{ slug: input.model, name: input.model, isCustom: false, capabilities: null }],
827 slashCommands: [],
828 skills: [],
829 });
830
831 /** A registry whose probes report the same snapshots it already holds. */
832 const providerRegistryLayer = (providers: ReadonlyArray<ServerProvider>) =>
833 Layer.mock(ProviderRegistry.ProviderRegistry)({
834 getProviders: Effect.succeed(providers),
835 refreshInstance: () => Effect.succeed(providers),
836 });
837
838 const adapterRegistryLayer = (instanceIds: ReadonlyArray<ProviderInstanceId>) =>
839 Layer.succeed(
840 ProviderAdapterRegistry.ProviderAdapterRegistryV2,
841 ProviderAdapterRegistry.ProviderAdapterRegistryV2.of({
842 list: () => Effect.succeed(instanceIds),
843 get: (instanceId) =>
844 instanceIds.includes(instanceId)
845 ? Effect.succeed({
846 instanceId,
847 } as unknown as ProviderAdapter.ProviderAdapterV2["Service"])
848 : Effect.fail(
849 new ProviderAdapterRegistry.ProviderAdapterRegistryLookupError({ instanceId }),
850 ),
851 }),
852 );
853
854 const parentProjection = (
855 subagents: ReadonlyArray<unknown>,
856 modelSelection: {
857 readonly instanceId: ProviderInstanceId;
858 readonly model: string;
859 readonly options?: ReadonlyArray<{ readonly id: string; readonly value: unknown }>;
860 } = { instanceId: codexInstanceId, model: "gpt-5.4" },
861 ): OrchestrationV2ThreadProjection =>
862 ({
863 thread: {
864 id: parentThreadId,
865 projectId,
866 title: "MCP parent",
867 createdBy: "user",
868 creationSource: "web",
869 modelSelection,
870 runtimeMode: "full-access",
871 interactionMode: "default",
872 },
873 runs: [
874 {
875 id: parentRunId,
876 ordinal: 1,
877 status: "running",
878 rootNodeId: parentNodeId,
879 providerInstanceId: codexInstanceId,
880 modelSelection,
881 },
882 ],
883 contextTransfers: [],
884 subagents,
885 }) as unknown as OrchestrationV2ThreadProjection;
886
887 const childProjection = {
888 thread: { id: childThreadId },
889 runs: [],
890 contextTransfers: [],
891 messages: [],
892 subagents: [],
893 providerThreads: [],
894 runtimeRequests: [],
895 turnItems: [],
896 } as unknown as OrchestrationV2ThreadProjection;
897
898 it.effect(
899 "advertises orchestration capability from registered adapters rather than a driver allowlist",
900 () =>
901 Effect.gen(function* () {
902 const disabledAntigravityInstanceId = ProviderInstanceId.make("antigravity-alt");
903 const forkOnlyInstanceId = ProviderInstanceId.make("forkOnly");
904 const forkShadow = yield* buildUnavailableProviderSnapshot({
905 driverKind: "forkOnly",
906 instanceId: forkOnlyInstanceId,
907 reason: "Driver 'forkOnly' is not registered in this build.",
908 checkedAt: "2026-09-13T00:00:00.000Z",
909 });
910 let providers: ReadonlyArray<ServerProvider> = [
911 providerSnapshot({
912 instanceId: codexInstanceId,
913 driver: ProviderDriverKind.make("codex"),
914 model: "gpt-5.4",
915 }),
916 providerSnapshot({
917 instanceId: ProviderInstanceId.make("claudeAgent"),
918 driver: ProviderDriverKind.make("claudeAgent"),
919 model: "claude-sonnet-4-6",
920 }),
921 providerSnapshot({
922 instanceId: ProviderInstanceId.make("pi"),
923 driver: ProviderDriverKind.make("pi"),
924 model: "pi-model",
925 }),
926 providerSnapshot({
927 instanceId: ProviderInstanceId.make("acpRegistry"),
928 driver: ProviderDriverKind.make("acpRegistry"),
929 model: "acp-model",
930 }),
931 // Antigravity has a live orchestration adapter through the provider
932 // instance registry even though no adapter driver entry exists in
933 // the static built-in list.
934 providerSnapshot({
935 instanceId: antigravityInstanceId,
936 driver: ProviderDriverKind.make("antigravity"),
937 model: "ant-model",
938 }),
939 // A second Antigravity instance whose adapter resolves but whose
940 // provider state still blocks delegation.
941 providerSnapshot({
942 instanceId: disabledAntigravityInstanceId,
943 driver: ProviderDriverKind.make("antigravity"),
944 model: "ant-model",
945 enabled: false,
946 }),
947 forkShadow,
948 ];
949 const layerDependencies = Layer.mergeAll(
950 NodeServices.layer,
951 Layer.mock(ThreadManagementService.ThreadManagementService)({
952 getThreadRecords: () => Effect.succeed(parentProjection([])),
953 }),
954 ServerSettings.layerTest(),
955 Layer.mock(ProviderRegistry.ProviderRegistry)({
956 getProviders: Effect.sync(() => providers),
957 }),
958 adapterRegistryLayer([
959 codexInstanceId,
960 ProviderInstanceId.make("claudeAgent"),
961 ProviderInstanceId.make("pi"),
962 ProviderInstanceId.make("acpRegistry"),
963 antigravityInstanceId,
964 disabledAntigravityInstanceId,
965 ]),
966 Layer.mock(ProjectService.ProjectService)({}),
967 Layer.mock(SecretRequests.SecretRequests)({}),
968 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
969 );
970
971 yield* Effect.gen(function* () {
972 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
973 const capabilities = yield* service.capabilities(scope);
974 const byId = new Map(
975 capabilities.providers.map((provider) => [provider.providerInstanceId, provider]),
976 );
977 for (const provider of providers) {
978 assert.deepEqual(
979 byId.get(provider.instanceId)?.models.map((model) => model.id),
980 provider.models.map((model) => model.slug),
981 );
982 }
983
984 providers = providers.map((provider) => ({
985 ...provider,
986 models: [
987 ...provider.models,
988 {
989 slug: `${provider.driver}/custom-model-after-refresh`,
990 name: "Custom model",
991 isCustom: true,
992 capabilities: null,
993 },
994 ],
995 }));
996 const refreshed = yield* service.capabilities(scope);
997 for (const provider of providers) {
998 assert.deepEqual(
999 refreshed.providers
1000 .find((entry) => entry.providerInstanceId === provider.instanceId)
1001 ?.models.map((model) => model.id),
1002 provider.models.map((model) => model.slug),
1003 );
1004 }
1005
1006 for (const instanceId of [
1007 codexInstanceId,
1008 ProviderInstanceId.make("claudeAgent"),
1009 ProviderInstanceId.make("pi"),
1010 ProviderInstanceId.make("acpRegistry"),
1011 antigravityInstanceId,
1012 ]) {
1013 const entry = byId.get(instanceId);
1014 assert.isDefined(entry);
1015 assert.isTrue(
1016 entry!.canRunChildTask,
1017 `expected ${instanceId} to advertise canRunChildTask`,
1018 );
1019 assert.isTrue(entry!.canRunCrossProviderChildTask);
1020 assert.deepEqual(entry!.constraints, []);
1021 }
1022
1023 const disabled = byId.get(disabledAntigravityInstanceId);
1024 assert.isDefined(disabled);
1025 assert.isFalse(disabled!.canRunChildTask);
1026 assert.deepEqual(disabled!.constraints, ["Provider instance is disabled."]);
1027
1028 const fork = byId.get(forkOnlyInstanceId);
1029 assert.isDefined(fork);
1030 assert.isFalse(fork!.canRunChildTask);
1031 assert.isTrue(
1032 fork!.constraints.includes("No V2 provider adapter is registered."),
1033 `expected missing-adapter constraint, got ${fork!.constraints.join(" | ")}`,
1034 );
1035 assert.isTrue(
1036 fork!.constraints.includes("Driver 'forkOnly' is not registered in this build."),
1037 );
1038 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1039 }),
1040 );
1041
1042 it.effect("leaves models the user hid out of the capability catalog", () =>
1043 Effect.gen(function* () {
1044 const codex = providerSnapshot({
1045 instanceId: codexInstanceId,
1046 driver: ProviderDriverKind.make("codex"),
1047 model: "gpt-5.4",
1048 });
1049 const layerDependencies = Layer.mergeAll(
1050 NodeServices.layer,
1051 Layer.mock(ThreadManagementService.ThreadManagementService)({
1052 getThreadRecords: () => Effect.succeed(parentProjection([])),
1053 }),
1054 ServerSettings.layerTest({
1055 providerModelPreferences: {
1056 [codexInstanceId]: { hiddenModels: ["gpt-5.4-mini", "my-model"], modelOrder: [] },
1057 },
1058 }),
1059 providerRegistryLayer([
1060 {
1061 ...codex,
1062 models: [
1063 ...codex.models,
1064 { slug: "gpt-5.4-mini", name: "Mini", isCustom: false, capabilities: null },
1065 // Custom models cannot be hidden, matching the client pickers.
1066 { slug: "my-model", name: "Mine", isCustom: true, capabilities: null },
1067 ],
1068 },
1069 ]),
1070 adapterRegistryLayer([codexInstanceId]),
1071 Layer.mock(ProjectService.ProjectService)({}),
1072 Layer.mock(SecretRequests.SecretRequests)({}),
1073 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1074 );
1075
1076 yield* Effect.gen(function* () {
1077 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1078 const capabilities = yield* service.capabilities(scope);
1079 assert.deepEqual(
1080 capabilities.providers[0]?.models.map((model) => model.id),
1081 ["gpt-5.4", "my-model"],
1082 );
1083 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1084 }),
1085 );
1086
1087 it.effect(
1088 "delegates to an Antigravity instance whose adapter resolves through the registry",
1089 () =>
1090 Effect.gen(function* () {
1091 let delegated = false;
1092 const task = {
1093 id: taskId,
1094 threadId: parentThreadId,
1095 runId: parentRunId,
1096 parentNodeId,
1097 origin: "app_owned",
1098 createdBy: "agent",
1099 driver: ProviderDriverKind.make("antigravity"),
1100 providerInstanceId: antigravityInstanceId,
1101 providerThreadId: null,
1102 childThreadId,
1103 nativeTaskRef: null,
1104 prompt: "Summarize the diff.",
1105 title: null,
1106 model: "ant-model",
1107 status: "running",
1108 result: null,
1109 startedAt: null,
1110 completedAt: null,
1111 };
1112 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1113 const layerDependencies = Layer.mergeAll(
1114 NodeServices.layer,
1115 Layer.mock(ThreadManagementService.ThreadManagementService)({
1116 hasReceipt: () => Effect.succeed(false),
1117 assertProviderInstanceEnabled: () => Effect.void,
1118 getThreadRecords: (threadId) =>
1119 Effect.succeed(
1120 threadId === parentThreadId
1121 ? parentProjection(delegated ? [task] : [])
1122 : childProjection,
1123 ),
1124 dispatch: (command) =>
1125 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
1126 Effect.andThen(
1127 Effect.sync(() => {
1128 delegated = true;
1129 }),
1130 ),
1131 Effect.as({
1132 sequence: 1,
1133 storedEvents: [
1134 {
1135 sequence: 1,
1136 commandId: null,
1137 event: { type: "subagent.updated", payload: task },
1138 },
1139 ],
1140 } as never),
1141 ),
1142 }),
1143 ServerSettings.layerTest(),
1144 Layer.mock(ProviderRegistry.ProviderRegistry)({
1145 getProviders: Effect.succeed([
1146 providerSnapshot({
1147 instanceId: codexInstanceId,
1148 driver: ProviderDriverKind.make("codex"),
1149 model: "gpt-5.4",
1150 }),
1151 providerSnapshot({
1152 instanceId: antigravityInstanceId,
1153 driver: ProviderDriverKind.make("antigravity"),
1154 model: "ant-model",
1155 }),
1156 ]),
1157 }),
1158 adapterRegistryLayer([codexInstanceId, antigravityInstanceId]),
1159 Layer.mock(ProjectService.ProjectService)({}),
1160 Layer.mock(SecretRequests.SecretRequests)({}),
1161 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1162 );
1163
1164 yield* Effect.gen(function* () {
1165 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1166 const result = yield* service.delegateTask(scope, {
1167 task: "Summarize the diff.",
1168 target: { providerInstanceId: antigravityInstanceId, model: "ant-model" },
1169 mode: "async",
1170 clientRequestId: "delegate-antigravity-1",
1171 });
1172 assert.equal(result.status, "running");
1173 assert.equal(result.providerInstanceId, antigravityInstanceId);
1174 const commands = yield* Ref.get(dispatched);
1175 assert.equal(commands.length, 1);
1176 const request = commands[0] as {
1177 type: string;
1178 modelSelection: { instanceId: string; model: string };
1179 };
1180 assert.equal(request.type, "delegated_task.request");
1181 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);
1182 assert.equal(request.modelSelection.model, "ant-model");
1183 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1184 }),
1185 );
1186
1187 it.effect("resolves a driverKind target to a capable Antigravity instance", () =>
1188 Effect.gen(function* () {
1189 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1190 const task = {
1191 id: taskId,
1192 threadId: parentThreadId,
1193 runId: parentRunId,
1194 parentNodeId,
1195 origin: "app_owned",
1196 createdBy: "agent",
1197 driver: ProviderDriverKind.make("antigravity"),
1198 providerInstanceId: antigravityInstanceId,
1199 providerThreadId: null,
1200 childThreadId,
1201 nativeTaskRef: null,
1202 prompt: "Summarize the diff.",
1203 title: null,
1204 model: "ant-model",
1205 status: "running",
1206 result: null,
1207 startedAt: null,
1208 completedAt: null,
1209 };
1210 let delegated = false;
1211 const layerDependencies = Layer.mergeAll(
1212 NodeServices.layer,
1213 Layer.mock(ThreadManagementService.ThreadManagementService)({
1214 hasReceipt: () => Effect.succeed(false),
1215 assertProviderInstanceEnabled: () => Effect.void,
1216 isProviderInstanceEnabledForProject: () => Effect.succeed(true),
1217 getThreadRecords: (threadId) =>
1218 Effect.succeed(
1219 threadId === parentThreadId
1220 ? parentProjection(delegated ? [task] : [])
1221 : childProjection,
1222 ),
1223 dispatch: (command) =>
1224 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
1225 Effect.andThen(
1226 Effect.sync(() => {
1227 delegated = true;
1228 }),
1229 ),
1230 Effect.as({
1231 sequence: 1,
1232 storedEvents: [
1233 {
1234 sequence: 1,
1235 commandId: null,
1236 event: { type: "subagent.updated", payload: task },
1237 },
1238 ],
1239 } as never),
1240 ),
1241 }),
1242 ServerSettings.layerTest(),
1243 Layer.mock(ProviderRegistry.ProviderRegistry)({
1244 getProviders: Effect.succeed([
1245 providerSnapshot({
1246 instanceId: codexInstanceId,
1247 driver: ProviderDriverKind.make("codex"),
1248 model: "gpt-5.4",
1249 }),
1250 providerSnapshot({
1251 instanceId: antigravityInstanceId,
1252 driver: ProviderDriverKind.make("antigravity"),
1253 model: "ant-model",
1254 }),
1255 ]),
1256 }),
1257 adapterRegistryLayer([codexInstanceId, antigravityInstanceId]),
1258 Layer.mock(ProjectService.ProjectService)({}),
1259 Layer.mock(SecretRequests.SecretRequests)({}),
1260 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1261 );
1262
1263 yield* Effect.gen(function* () {
1264 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1265 const result = yield* service.delegateTask(scope, {
1266 task: "Summarize the diff.",
1267 target: { driverKind: ProviderDriverKind.make("antigravity") },
1268 mode: "async",
1269 clientRequestId: "delegate-antigravity-driver-1",
1270 });
1271 assert.equal(result.status, "running");
1272 const commands = yield* Ref.get(dispatched);
1273 assert.equal(commands.length, 1);
1274 const request = commands[0] as {
1275 modelSelection: { instanceId: string; model: string };
1276 };
1277 assert.equal(request.modelSelection.instanceId, antigravityInstanceId);
1278 assert.equal(request.modelSelection.model, "ant-model");
1279 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1280 }),
1281 );
1282
1283 it.effect("rejects delegation to a provider without a registered adapter", () =>
1284 Effect.gen(function* () {
1285 const forkOnlyInstanceId = ProviderInstanceId.make("forkOnly");
1286 const forkShadow = yield* buildUnavailableProviderSnapshot({
1287 driverKind: "forkOnly",
1288 instanceId: forkOnlyInstanceId,
1289 reason: "Driver 'forkOnly' is not registered in this build.",
1290 checkedAt: "2026-09-13T00:00:00.000Z",
1291 });
1292 const layerDependencies = Layer.mergeAll(
1293 NodeServices.layer,
1294 Layer.mock(ThreadManagementService.ThreadManagementService)({
1295 hasReceipt: () => Effect.succeed(false),
1296 getThreadRecords: () => Effect.succeed(parentProjection([])),
1297 }),
1298 ServerSettings.layerTest(),
1299 providerRegistryLayer([
1300 providerSnapshot({
1301 instanceId: codexInstanceId,
1302 driver: ProviderDriverKind.make("codex"),
1303 model: "gpt-5.4",
1304 }),
1305 forkShadow,
1306 ]),
1307 adapterRegistryLayer([codexInstanceId]),
1308 Layer.mock(ProjectService.ProjectService)({}),
1309 Layer.mock(SecretRequests.SecretRequests)({}),
1310 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1311 );
1312
1313 yield* Effect.gen(function* () {
1314 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1315 const byInstance = yield* service
1316 .delegateTask(scope, {
1317 task: "Summarize the diff.",
1318 target: { providerInstanceId: forkOnlyInstanceId },
1319 mode: "async",
1320 clientRequestId: "delegate-fork-1",
1321 })
1322 .pipe(Effect.flip);
1323 assert.equal(byInstance.code, "provider_unavailable");
1324 assert.isTrue(byInstance.message.includes("No V2 provider adapter is registered."));
1325
1326 const byDriver = yield* service
1327 .delegateTask(scope, {
1328 task: "Summarize the diff.",
1329 target: { driverKind: ProviderDriverKind.make("forkOnly") },
1330 mode: "async",
1331 clientRequestId: "delegate-fork-2",
1332 })
1333 .pipe(Effect.flip);
1334 assert.equal(byDriver.code, "provider_unavailable");
1335 assert.isTrue(
1336 byDriver.message.includes("No V2 provider adapter is registered for driver forkOnly."),
1337 );
1338 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1339 }),
1340 );
1341
1342 it.effect("refuses delegation to a provider the project turned off", () =>
1343 Effect.gen(function* () {
1344 const layerDependencies = Layer.mergeAll(
1345 NodeServices.layer,
1346 Layer.mock(ThreadManagementService.ThreadManagementService)({
1347 hasReceipt: () => Effect.succeed(false),
1348 getThreadRecords: () => Effect.succeed(parentProjection([])),
1349 // ThreadManagementService.dispatch is the chokepoint now: it is the
1350 // one that refuses, not a separate pre-dispatch assert.
1351 dispatch: () =>
1352 Effect.fail(
1353 new ThreadManagementService.ThreadManagementProviderInstanceDisabledError({
1354 projectId,
1355 instanceId: codexInstanceId,
1356 }),
1357 ),
1358 }),
1359 providerRegistryLayer([
1360 providerSnapshot({
1361 instanceId: codexInstanceId,
1362 driver: ProviderDriverKind.make("codex"),
1363 model: "gpt-5.4",
1364 }),
1365 ]),
1366 adapterRegistryLayer([codexInstanceId]),
1367 Layer.mock(ProjectService.ProjectService)({}),
1368 Layer.mock(SecretRequests.SecretRequests)({}),
1369 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1370 ServerSettings.layerTest(),
1371 );
1372
1373 yield* Effect.gen(function* () {
1374 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1375 const error = yield* service
1376 .delegateTask(scope, {
1377 task: "Summarize the diff.",
1378 target: { providerInstanceId: codexInstanceId },
1379 mode: "async",
1380 clientRequestId: "delegate-disabled-1",
1381 })
1382 .pipe(Effect.flip);
1383 assert.equal(error.code, "invalid_request");
1384 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1385 }),
1386 );
1387
1388 it.effect("driver-only delegation skips an instance the project turned off", () =>
1389 Effect.gen(function* () {
1390 const altInstanceId = ProviderInstanceId.make("codex-alt");
1391 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1392 const task = {
1393 id: taskId,
1394 threadId: parentThreadId,
1395 runId: parentRunId,
1396 parentNodeId,
1397 origin: "app_owned",
1398 createdBy: "agent",
1399 driver: ProviderDriverKind.make("codex"),
1400 providerInstanceId: altInstanceId,
1401 providerThreadId: null,
1402 childThreadId,
1403 nativeTaskRef: null,
1404 prompt: "Summarize the diff.",
1405 title: null,
1406 model: "gpt-5.4",
1407 status: "running",
1408 result: null,
1409 startedAt: null,
1410 completedAt: null,
1411 };
1412 let delegated = false;
1413 const layerDependencies = Layer.mergeAll(
1414 NodeServices.layer,
1415 Layer.mock(ThreadManagementService.ThreadManagementService)({
1416 hasReceipt: () => Effect.succeed(false),
1417 getThreadRecords: (threadId) =>
1418 Effect.succeed(
1419 threadId === parentThreadId
1420 ? parentProjection(delegated ? [task] : [])
1421 : childProjection,
1422 ),
1423 // The parent's own instance is off for this project; the other
1424 // instance of the same driver is on.
1425 isProviderInstanceEnabledForProject: ({ instanceId }) =>
1426 Effect.succeed(instanceId !== codexInstanceId),
1427 assertProviderInstanceEnabled: ({ projectId, instanceId }) =>
1428 instanceId === codexInstanceId
1429 ? Effect.fail(
1430 new ThreadManagementService.ThreadManagementProviderInstanceDisabledError({
1431 projectId,
1432 instanceId,
1433 }),
1434 )
1435 : Effect.void,
1436 dispatch: (command) =>
1437 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
1438 Effect.andThen(
1439 Effect.sync(() => {
1440 delegated = true;
1441 }),
1442 ),
1443 Effect.as({
1444 sequence: 1,
1445 storedEvents: [
1446 {
1447 sequence: 1,
1448 commandId: null,
1449 event: { type: "subagent.updated", payload: task },
1450 },
1451 ],
1452 } as never),
1453 ),
1454 }),
1455 providerRegistryLayer([
1456 providerSnapshot({
1457 instanceId: codexInstanceId,
1458 driver: ProviderDriverKind.make("codex"),
1459 model: "gpt-5.4",
1460 }),
1461 providerSnapshot({
1462 instanceId: altInstanceId,
1463 driver: ProviderDriverKind.make("codex"),
1464 model: "gpt-5.4",
1465 }),
1466 ]),
1467 adapterRegistryLayer([codexInstanceId, altInstanceId]),
1468 Layer.mock(ProjectService.ProjectService)({}),
1469 Layer.mock(SecretRequests.SecretRequests)({}),
1470 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1471 ServerSettings.layerTest(),
1472 );
1473
1474 yield* Effect.gen(function* () {
1475 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1476 const result = yield* service.delegateTask(scope, {
1477 task: "Summarize the diff.",
1478 target: { driverKind: ProviderDriverKind.make("codex") },
1479 mode: "async",
1480 clientRequestId: "delegate-driver-skip-disabled-1",
1481 });
1482 assert.equal(result.status, "running");
1483 const commands = yield* Ref.get(dispatched);
1484 assert.equal(commands.length, 1);
1485 const request = commands[0] as {
1486 modelSelection: { instanceId: string; model: string };
1487 };
1488 assert.equal(request.modelSelection.instanceId, altInstanceId);
1489 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1490 }),
1491 );
1492
1493 it.effect(
1494 "retrying an accepted driver-only delegation replays even after the project disables every candidate",
1495 () =>
1496 Effect.gen(function* () {
1497 const altInstanceId = ProviderInstanceId.make("codex-alt");
1498 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1499 const accepted = yield* Ref.make(false);
1500 const everyCandidateDisabled = yield* Ref.make(false);
1501 const task = {
1502 id: taskId,
1503 threadId: parentThreadId,
1504 runId: parentRunId,
1505 parentNodeId,
1506 origin: "app_owned",
1507 createdBy: "agent",
1508 driver: ProviderDriverKind.make("codex"),
1509 providerInstanceId: codexInstanceId,
1510 providerThreadId: null,
1511 childThreadId,
1512 nativeTaskRef: null,
1513 prompt: "Summarize the diff.",
1514 title: null,
1515 model: "gpt-5.4",
1516 status: "running",
1517 result: null,
1518 startedAt: null,
1519 completedAt: null,
1520 };
1521 const layerDependencies = Layer.mergeAll(
1522 NodeServices.layer,
1523 Layer.mock(ThreadManagementService.ThreadManagementService)({
1524 getThreadRecords: (threadId) =>
1525 Ref.get(accepted).pipe(
1526 Effect.map((isAccepted) =>
1527 threadId === parentThreadId
1528 ? parentProjection(isAccepted ? [task] : [])
1529 : childProjection,
1530 ),
1531 ),
1532 // A command id already accepted once, even if the project later
1533 // disables every candidate: its retry must replay, not re-resolve.
1534 hasReceipt: () => Ref.get(accepted),
1535 // Effective settings once the project turns every instance off,
1536 // ignoring the already-accepted command's replay exemption.
1537 isProviderInstanceEnabledForProject: () =>
1538 Ref.get(everyCandidateDisabled).pipe(Effect.map((disabled) => !disabled)),
1539 assertProviderInstanceEnabled: ({ projectId, instanceId }) =>
1540 Effect.all([Ref.get(accepted), Ref.get(everyCandidateDisabled)]).pipe(
1541 Effect.flatMap(([isAccepted, disabled]) =>
1542 isAccepted || !disabled
1543 ? Effect.void
1544 : Effect.fail(
1545 new ThreadManagementService.ThreadManagementProviderInstanceDisabledError({
1546 projectId,
1547 instanceId,
1548 }),
1549 ),
1550 ),
1551 ),
1552 dispatch: (command) =>
1553 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
1554 Effect.andThen(Ref.set(accepted, true)),
1555 Effect.as({
1556 sequence: 1,
1557 storedEvents: [
1558 {
1559 sequence: 1,
1560 commandId: null,
1561 event: { type: "subagent.updated", payload: task },
1562 },
1563 ],
1564 } as never),
1565 ),
1566 }),
1567 providerRegistryLayer([
1568 providerSnapshot({
1569 instanceId: codexInstanceId,
1570 driver: ProviderDriverKind.make("codex"),
1571 model: "gpt-5.4",
1572 }),
1573 providerSnapshot({
1574 instanceId: altInstanceId,
1575 driver: ProviderDriverKind.make("codex"),
1576 model: "gpt-5.4",
1577 }),
1578 ]),
1579 adapterRegistryLayer([codexInstanceId, altInstanceId]),
1580 Layer.mock(ProjectService.ProjectService)({}),
1581 Layer.mock(SecretRequests.SecretRequests)({}),
1582 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1583 ServerSettings.layerTest(),
1584 );
1585
1586 yield* Effect.gen(function* () {
1587 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1588 const first = yield* service.delegateTask(scope, {
1589 task: "Summarize the diff.",
1590 target: { driverKind: ProviderDriverKind.make("codex") },
1591 mode: "async",
1592 clientRequestId: "delegate-driver-retry-after-disable-1",
1593 });
1594 assert.equal(first.status, "running");
1595
1596 // The project turns off every instance of this driver only after
1597 // the delegation above was already accepted.
1598 yield* Ref.set(everyCandidateDisabled, true);
1599
1600 const retry = yield* service.delegateTask(scope, {
1601 task: "Summarize the diff.",
1602 target: { driverKind: ProviderDriverKind.make("codex") },
1603 mode: "async",
1604 clientRequestId: "delegate-driver-retry-after-disable-1",
1605 });
1606 assert.equal(retry.status, "running");
1607
1608 const commands = yield* Ref.get(dispatched);
1609 assert.equal(commands.length, 2);
1610 const retryCommand = commands[1] as {
1611 modelSelection: { instanceId: string; model: string };
1612 };
1613 assert.equal(retryCommand.modelSelection.instanceId, codexInstanceId);
1614 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1615 }),
1616 );
1617
1618 it.effect("re-probes an unavailable target once before refusing it", () =>
1619 Effect.gen(function* () {
1620 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
1621 const claudeDriver = ProviderDriverKind.make("claudeAgent");
1622 const task = {
1623 id: taskId,
1624 threadId: parentThreadId,
1625 runId: parentRunId,
1626 parentNodeId,
1627 origin: "app_owned",
1628 createdBy: "agent",
1629 driver: claudeDriver,
1630 providerInstanceId: claudeInstanceId,
1631 providerThreadId: null,
1632 childThreadId,
1633 nativeTaskRef: null,
1634 prompt: "Review the diff.",
1635 title: null,
1636 model: "claude-opus-5-5",
1637 status: "running",
1638 result: null,
1639 startedAt: null,
1640 completedAt: null,
1641 };
1642 const healthy = providerSnapshot({
1643 instanceId: claudeInstanceId,
1644 driver: claudeDriver,
1645 model: "claude-opus-5-5",
1646 });
1647 const missingCli: ServerProvider = {
1648 ...healthy,
1649 installed: false,
1650 status: "error",
1651 message: "Claude Agent CLI (`claude`) was not found on PATH.",
1652 };
1653 const codex = providerSnapshot({
1654 instanceId: codexInstanceId,
1655 driver: ProviderDriverKind.make("codex"),
1656 model: "gpt-5.4",
1657 });
1658 // The cache still says the CLI is missing; a probe reports `cliInstalled`.
1659 let cliInstalled = false;
1660 const probes = yield* Ref.make(0);
1661 const dispatched = yield* Ref.make(0);
1662 const layerDependencies = Layer.mergeAll(
1663 NodeServices.layer,
1664 Layer.mock(ThreadManagementService.ThreadManagementService)({
1665 hasReceipt: () => Effect.succeed(false),
1666 assertProviderInstanceEnabled: () => Effect.void,
1667 getThreadRecords: (threadId) =>
1668 Effect.succeed(
1669 threadId === parentThreadId ? parentProjection([task]) : childProjection,
1670 ),
1671 dispatch: () =>
1672 Ref.update(dispatched, (count) => count + 1).pipe(
1673 Effect.as({
1674 sequence: 1,
1675 storedEvents: [
1676 {
1677 sequence: 1,
1678 commandId: null,
1679 event: { type: "subagent.updated", payload: task },
1680 },
1681 ],
1682 } as never),
1683 ),
1684 }),
1685 ServerSettings.layerTest(),
1686 Layer.mock(ProviderRegistry.ProviderRegistry)({
1687 getProviders: Effect.succeed([codex, missingCli]),
1688 refreshInstance: () =>
1689 Ref.update(probes, (count) => count + 1).pipe(
1690 Effect.as([codex, cliInstalled ? healthy : missingCli]),
1691 ),
1692 }),
1693 adapterRegistryLayer([codexInstanceId, claudeInstanceId]),
1694 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1695 Layer.mock(ProjectService.ProjectService)({}),
1696 Layer.mock(SecretRequests.SecretRequests)({}),
1697 );
1698
1699 yield* Effect.gen(function* () {
1700 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1701 const delegate = (clientRequestId: string) =>
1702 service.delegateTask(scope, {
1703 task: "Review the diff.",
1704 target: { providerInstanceId: claudeInstanceId, model: "claude-opus-5-5" },
1705 mode: "async",
1706 clientRequestId,
1707 });
1708
1709 const error = yield* delegate("delegate-recheck-1").pipe(Effect.flip);
1710 assert.equal(error.code, "provider_unavailable");
1711 assert.equal(yield* Ref.get(probes), 1);
1712 assert.equal(yield* Ref.get(dispatched), 0);
1713
1714 cliInstalled = true;
1715 const result = yield* delegate("delegate-recheck-2");
1716 assert.equal(result.providerInstanceId, claudeInstanceId);
1717 assert.equal(yield* Ref.get(probes), 2);
1718 assert.equal(yield* Ref.get(dispatched), 1);
1719 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1720 }),
1721 );
1722
1723 it.effect(
1724 "inherits an available parent instance for driver-only targets and otherwise selects a healthy peer",
1725 () =>
1726 Effect.gen(function* () {
1727 const codexAltInstanceId = ProviderInstanceId.make("codex-alt");
1728 const driver = ProviderDriverKind.make("codex");
1729 const claudeDriver = ProviderDriverKind.make("claudeAgent");
1730 const parentModelSelection = {
1731 instanceId: codexInstanceId,
1732 model: "gpt-5.4",
1733 options: [{ id: "reasoningEffort", value: "high" }],
1734 } as const;
1735 const task = {
1736 id: taskId,
1737 threadId: parentThreadId,
1738 runId: parentRunId,
1739 parentNodeId,
1740 origin: "app_owned",
1741 createdBy: "agent",
1742 driver,
1743 providerInstanceId: codexInstanceId,
1744 providerThreadId: null,
1745 childThreadId,
1746 nativeTaskRef: null,
1747 prompt: "Summarize the diff.",
1748 title: null,
1749 model: "gpt-5.4",
1750 status: "running",
1751 result: null,
1752 startedAt: null,
1753 completedAt: null,
1754 };
1755 const cases = [
1756 {
1757 name: "healthy-inherited",
1758 inheritedEnabled: true,
1759 peerEnabled: true,
1760 explicit: false,
1761 selectedInstanceId: codexInstanceId,
1762 candidateDriver: driver,
1763 },
1764 {
1765 name: "unavailable-inherited-falls-back-to-healthy-peer",
1766 inheritedEnabled: false,
1767 peerEnabled: true,
1768 explicit: false,
1769 selectedInstanceId: codexAltInstanceId,
1770 candidateDriver: driver,
1771 },
1772 {
1773 name: "no-available-peer",
1774 inheritedEnabled: false,
1775 peerEnabled: false,
1776 explicit: false,
1777 selectedInstanceId: null,
1778 candidateDriver: driver,
1779 },
1780 {
1781 name: "cross-driver-no-available-candidate",
1782 inheritedEnabled: false,
1783 peerEnabled: false,
1784 explicit: false,
1785 selectedInstanceId: null,
1786 candidateDriver: claudeDriver,
1787 },
1788 {
1789 name: "explicit-unavailable",
1790 inheritedEnabled: false,
1791 peerEnabled: true,
1792 explicit: true,
1793 selectedInstanceId: null,
1794 candidateDriver: driver,
1795 },
1796 {
1797 name: "explicit-healthy",
1798 inheritedEnabled: true,
1799 peerEnabled: true,
1800 explicit: true,
1801 selectedInstanceId: codexAltInstanceId,
1802 candidateDriver: driver,
1803 },
1804 ] as const;
1805
1806 for (const testCase of cases) {
1807 const dispatched = yield* Ref.make<ReadonlyArray<unknown>>([]);
1808 let delegated = false;
1809 const layerDependencies = Layer.mergeAll(
1810 NodeServices.layer,
1811 Layer.mock(ThreadManagementService.ThreadManagementService)({
1812 hasReceipt: () => Effect.succeed(false),
1813 assertProviderInstanceEnabled: () => Effect.void,
1814 isProviderInstanceEnabledForProject: () => Effect.succeed(true),
1815 getThreadRecords: (threadId) =>
1816 Effect.succeed(
1817 threadId === parentThreadId
1818 ? parentProjection(delegated ? [task] : [], parentModelSelection)
1819 : childProjection,
1820 ),
1821 dispatch: (command) =>
1822 Ref.update(dispatched, (commands) => [...commands, command]).pipe(
1823 Effect.andThen(
1824 Effect.sync(() => {
1825 delegated = true;
1826 }),
1827 ),
1828 Effect.as({
1829 sequence: 1,
1830 storedEvents: [
1831 {
1832 sequence: 1,
1833 commandId: null,
1834 event: { type: "subagent.updated", payload: task },
1835 },
1836 ],
1837 } as never),
1838 ),
1839 }),
1840 ServerSettings.layerTest(),
1841 providerRegistryLayer([
1842 providerSnapshot({
1843 instanceId: codexInstanceId,
1844 driver,
1845 model: "gpt-5.4",
1846 enabled: testCase.inheritedEnabled,
1847 }),
1848 providerSnapshot({
1849 instanceId: codexAltInstanceId,
1850 driver: testCase.candidateDriver,
1851 model: "codex-alt-model",
1852 enabled: testCase.peerEnabled,
1853 }),
1854 ]),
1855 adapterRegistryLayer([codexInstanceId, codexAltInstanceId]),
1856 Layer.mock(ProjectService.ProjectService)({}),
1857 Layer.mock(SecretRequests.SecretRequests)({}),
1858 Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
1859 );
1860
1861 yield* Effect.gen(function* () {
1862 const service = yield* OrchestratorMcpService.OrchestratorMcpService;
1863 const target = testCase.explicit
1864 ? ({
1865 providerInstanceId:
1866 testCase.selectedInstanceId === null
1867 ? codexInstanceId
1868 : testCase.selectedInstanceId,
1869 } as const)
1870 : ({ driverKind: testCase.candidateDriver } as const);
1871 if (testCase.selectedInstanceId === null) {
1872 const error = yield* service
1873 .delegateTask(scope, {
1874 task: "Summarize the diff.",
1875 target,
1876 mode: "async",
1877 clientRequestId: `delegate-select-${testCase.name}`,
1878 })
1879 .pipe(Effect.flip);
1880 assert.equal(error.code, "provider_unavailable", testCase.name);
1881 if (testCase.name === "cross-driver-no-available-candidate") {
1882 assert.isTrue(error.message.includes("driver claudeAgent"), testCase.name);
1883 const threadError = yield* service
1884 .createThreads(scope, {
1885 threads: [{ prompt: "Summarize the diff.", target }],
1886 clientRequestId: `delegate-threads-${testCase.name}`,
1887 })
1888 .pipe(Effect.flip);
1889 assert.equal(
1890 threadError.code,
1891 "provider_unavailable",
1892 `${testCase.name}-createThreads`,
1893 );
1894 assert.isTrue(
1895 threadError.message.includes("driver claudeAgent"),
1896 `${testCase.name}-createThreads`,
1897 );
1898 }
1899 assert.deepEqual(yield* Ref.get(dispatched), [], testCase.name);
1900 return;
1901 }
1902 const result = yield* service.delegateTask(scope, {
1903 task: "Summarize the diff.",
1904 target,
1905 mode: "async",
1906 clientRequestId: `delegate-select-${testCase.name}`,
1907 });
1908 assert.equal(result.status, "running", testCase.name);
1909 const commands = yield* Ref.get(dispatched);
1910 assert.equal(commands.length, 1, testCase.name);
1911 const request = commands[0] as {
1912 type: string;
1913 modelSelection: {
1914 instanceId: string;
1915 model: string;
1916 options?: ReadonlyArray<{ id: string; value: unknown }>;
1917 };
1918 };
1919 assert.equal(request.type, "delegated_task.request", testCase.name);
1920 assert.equal(
1921 request.modelSelection.instanceId,
1922 testCase.selectedInstanceId,
1923 testCase.name,
1924 );
1925 if (testCase.name === "healthy-inherited") {
1926 assert.deepEqual(request.modelSelection, parentModelSelection, testCase.name);
1927 } else {
1928 assert.equal(request.modelSelection.model, "codex-alt-model", testCase.name);
1929 }
1930 }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
1931 }
1932 }),
1933 );
1934
1935 describe("scheduled tasks at modes above the caller's", () => {
1936 const projectId = ProjectId.make("project:scheduled");
1937 const boundThreadId = ThreadId.make("thread:scheduled-bound");
1938 const task = (overrides: Partial<ScheduledTask>): ScheduledTask => ({
1939 id: ScheduledTaskId.make("scheduled-task:webhook"),
1940 title: "On push",
1941 prompt: "Do {{body.instruction}}",
1942 enabled: true,
1943 schedule: { type: "webhook", signature: null },
1944 projectId,
1945 threadId: null,
1946 workspaceStrategy: { type: "root" },
1947 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" },
1948 runtimeMode: "approval-required",
1949 interactionMode: "default",
1950 createdBy: "agent",
1951 creationSource: "mcp",
1952 createdAt: "2026-10-05T00:00:00.000Z",
1953 updatedAt: "2026-10-05T00:00:00.000Z",
1954 nextRunAt: null,
1955 lastRunAt: null,
1956 lastRunStatus: "never",
1957 lastRunError: null,
1958 runCount: 0,
1959 webhook: {
1960 path: "/hooks/scheduled-task/secret",
1961 url: "https://t3.example/hooks/secret",
1962 hasSecret: false,
1963 },
1964 ...overrides,
1965 });
1966 const supervisedClient: McpInvocationScope = {
1967 environmentId: EnvironmentId.make("environment:scheduled"),
1968 requestNamespace: "client:scheduled",
1969 thread: undefined,
1970 client: {
1971 sessionId: "scheduled",
1972 label: "Claude Code",
1973 access: "approval-required",
1974 },
1975 capabilities: new Set(["orchestration"]),
1976 issuedAt: 1,
1977 };
1978 const service = (
1979 tasks: ReadonlyArray<ScheduledTask>,
1980 boundThread: OrchestrationV2ThreadShell | null,
1981 upserted: Ref.Ref<number>,
1982 ) =>
1983 layerOrchestratorService.pipe(
1984 Layer.provide(
1985 Layer.mergeAll(
1986 NodeServices.layer,
1987 Layer.mock(ThreadManagementService.ThreadManagementService)({
1988 getThreadShell: (threadId) =>
1989 Effect.succeed(threadId === boundThreadId ? boundThread : null),
1990 }),
1991 ServerSettings.layerTest(),
1992 Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
1993 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
1994 list: () => Effect.succeed([]),
1995 }),
1996 Layer.mock(ProjectService.ProjectService)({}),
1997 Layer.mock(SecretRequests.SecretRequests)({}),
1998 Layer.mock(ScheduledTaskService.ScheduledTaskService)({
1999 list: () => Effect.succeed({ tasks }),
2000 upsert: () =>
2001 Ref.update(upserted, (count) => count + 1).pipe(Effect.as({ task: tasks[0]! })),
2002 }),
2003 ),
2004 ),
2005 );
2006
2007 it.effect("hides a webhook URL from a caller below the task's modes", () =>
2008 Effect.gen(function* () {
2009 const upserted = yield* Ref.make(0);
2010 const listed = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
2011 Effect.flatMap((mcp) => mcp.listScheduledTasks(supervisedClient, { projectId })),
2012 Effect.provide(
2013 service(
2014 [
2015 task({ runtimeMode: "full-access" }),
2016 task({ id: ScheduledTaskId.make("scheduled-task:supervised") }),
2017 ],
2018 null,
2019 upserted,
2020 ),
2021 ),
2022 );
2023 assert.deepEqual(
2024 listed.tasks.map((summary) => summary.webhookUrl),
2025 [undefined, "https://t3.example/hooks/secret"],
2026 );
2027 }),
2028 );
2029
2030 it.effect("hides a webhook URL from a thread whose turn has ended", () =>
2031 Effect.gen(function* () {
2032 const callerId = ThreadId.make("thread:scheduled-ended");
2033 const shell = liveThreadShell(callerId, { activeRunId: null });
2034 const listed = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
2035 Effect.flatMap((mcp) =>
2036 mcp.listScheduledTasks(
2037 {
2038 ...supervisedClient,
2039 requestNamespace: "provider:scheduled-ended",
2040 thread: {
2041 threadId: callerId,
2042 providerSessionId: "provider:scheduled-ended",
2043 providerInstanceId: shell.providerInstanceId,
2044 },
2045 client: undefined,
2046 },
2047 { projectId },
2048 ),
2049 ),
2050 Effect.provide(
2051 layerOrchestratorService.pipe(
2052 Layer.provide(
2053 Layer.mergeAll(
2054 NodeServices.layer,
2055 Layer.mock(ThreadManagementService.ThreadManagementService)({
2056 getThreadShell: () => Effect.succeed(null),
2057 // Its turn ended: no run is active.
2058 getThreadRecords: () => Effect.succeed(idleThreadProjection(shell)),
2059 }),
2060 ServerSettings.layerTest(),
2061 Layer.mock(ProviderRegistry.ProviderRegistry)({
2062 getProviders: Effect.succeed([]),
2063 }),
2064 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
2065 list: () => Effect.succeed([]),
2066 }),
2067 Layer.mock(ProjectService.ProjectService)({}),
2068 Layer.mock(SecretRequests.SecretRequests)({}),
2069 Layer.mock(ScheduledTaskService.ScheduledTaskService)({
2070 list: () => Effect.succeed({ tasks: [task({})] }),
2071 }),
2072 ),
2073 ),
2074 ),
2075 ),
2076 );
2077 assert.equal(listed.tasks[0]?.webhookUrl, undefined);
2078 }),
2079 );
2080
2081 it.effect("never shows a read-only client a webhook URL", () =>
2082 Effect.gen(function* () {
2083 const upserted = yield* Ref.make(0);
2084 const listed = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
2085 Effect.flatMap((mcp) =>
2086 mcp.listScheduledTasks(
2087 {
2088 ...supervisedClient,
2089 client: { sessionId: "scheduled", label: "Claude Code", access: "read-only" },
2090 },
2091 { projectId },
2092 ),
2093 ),
2094 Effect.provide(service([task({})], null, upserted)),
2095 );
2096 assert.equal(listed.tasks[0]?.webhookUrl, undefined);
2097 }),
2098 );
2099
2100 it.effect("checks a bound task against its thread's current modes", () =>
2101 Effect.gen(function* () {
2102 const upserted = yield* Ref.make(0);
2103 // Scheduled while the thread was Supervised; the thread now runs in full access.
2104 const bound = task({ threadId: boundThreadId });
2105 const layer = service(
2106 [bound],
2107 liveThreadShell(boundThreadId, { runtimeMode: "full-access" }),
2108 upserted,
2109 );
2110 const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
2111 Effect.provide(layer),
2112 );
2113 const listed = yield* mcp.listScheduledTasks(supervisedClient, { projectId });
2114 assert.equal(listed.tasks[0]?.webhookUrl, undefined);
2115 const error = yield* mcp
2116 .updateScheduledTask(supervisedClient, {
2117 scheduledTaskId: bound.id,
2118 prompt: "Something else",
2119 })
2120 .pipe(Effect.flip);
2121 assert.equal(error.code, "runtime_mode_escalation_denied");
2122 assert.equal(yield* Ref.get(upserted), 0);
2123 }),
2124 );
2125
2126 it.effect("reports a saved task even when its bound thread cannot be read", () =>
2127 Effect.gen(function* () {
2128 const upserted = yield* Ref.make(0);
2129 const bound = task({ threadId: boundThreadId });
2130 const lookups = yield* Ref.make(0);
2131 // The edit's own check reads the thread; the read after the save fails.
2132 const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
2133 Effect.provide(
2134 layerOrchestratorService.pipe(
2135 Layer.provide(
2136 Layer.mergeAll(
2137 NodeServices.layer,
2138 Layer.mock(ThreadManagementService.ThreadManagementService)({
2139 getThreadShell: (threadId) =>
2140 Ref.getAndUpdate(lookups, (count) => count + 1).pipe(
2141 Effect.flatMap((count) =>
2142 count === 0
2143 ? Effect.succeed(
2144 liveThreadShell(threadId, { runtimeMode: "approval-required" }),
2145 )
2146 : Effect.fail(new OrchestratorProjectionError({ threadId })),
2147 ),
2148 ),
2149 }),
2150 ServerSettings.layerTest(),
2151 Layer.mock(ProviderRegistry.ProviderRegistry)({
2152 getProviders: Effect.succeed([]),
2153 }),
2154 Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
2155 list: () => Effect.succeed([]),
2156 }),
2157 Layer.mock(ProjectService.ProjectService)({}),
2158 Layer.mock(SecretRequests.SecretRequests)({}),
2159 Layer.mock(ScheduledTaskService.ScheduledTaskService)({
2160 list: () => Effect.succeed({ tasks: [bound] }),
2161 upsert: () =>
2162 Ref.update(upserted, (count) => count + 1).pipe(Effect.as({ task: bound })),
2163 }),
2164 ),
2165 ),
2166 ),
2167 ),
2168 );
2169 const updated = yield* mcp.updateScheduledTask(supervisedClient, {
2170 scheduledTaskId: bound.id,
2171 prompt: "Something else",
2172 });
2173 assert.equal(yield* Ref.get(upserted), 1);
2174 assert.equal(updated.scheduledTaskId, bound.id);
2175 assert.equal(updated.webhookUrl, undefined);
2176 }),
2177 );
2178 });
2179
2180 describe("resolveRuntimeMode", () => {
2181 // Muse offers only these two, and runs any other mode as Supervised.
2182 const muse = {
2183 modelSelection: { instanceId: ProviderInstanceId.make("muse"), model: "muse-model" },
2184 supportedRuntimeModes: ["approval-required", "full-access"] as const,
2185 };
2186
2187 it.effect("refuses an explicit mode the target provider does not offer", () =>
2188 Effect.gen(function* () {
2189 const error = yield* OrchestratorMcpService.resolveRuntimeMode(
2190 "full-access",
2191 "auto-accept-edits",
2192 muse,
2193 ).pipe(Effect.flip);
2194 assert.equal(error.code, "invalid_request");
2195 assert.include(error.message, "approval-required, full-access");
2196 }),
2197 );
2198
2199 it.effect("steps an inherited mode down to the broadest one the provider offers", () =>
2200 Effect.gen(function* () {
2201 assert.equal(
2202 yield* OrchestratorMcpService.resolveRuntimeMode("full-access", "inherit", muse),
2203 "full-access",
2204 );
2205 assert.equal(
2206 yield* OrchestratorMcpService.resolveRuntimeMode("auto", undefined, muse),
2207 "approval-required",
2208 );
2209 }),
2210 );
2211 });
2212});