1import {
2 CheckpointId,
3 CheckpointRef,
4 CheckpointScopeId,
5 CommandId,
6 EnvironmentId,
7 MessageId,
8 NodeId,
9 ORCHESTRATION_V2_WS_METHODS,
10 PlanId,
11 ProjectId,
12 ProviderInstanceId,
13 ProviderDriverKind,
14 RunId,
15 RuntimeRequestId,
16 ThreadId,
17 TurnItemId,
18 WS_METHODS,
19 type OrchestrationV2Command,
20 type OrchestrationV2ThreadLaunchInput,
21 type OrchestrationV2ThreadProjection,
22 type ProjectMutation,
23} from "@t3tools/contracts";
24import { describe, expect, it } from "@effect/vitest";
25import * as Crypto from "effect/Crypto";
26import * as Effect from "effect/Effect";
27import * as Layer from "effect/Layer";
28import * as Option from "effect/Option";
29import * as SubscriptionRef from "effect/SubscriptionRef";
30
31import {
32 AVAILABLE_CONNECTION_STATE,
33 PrimaryConnectionTarget,
34 type PreparedConnection,
35} from "../connection/model.ts";
36import * as EnvironmentSupervisor from "../connection/supervisor.ts";
37import * as RpcSession from "../rpc/session.ts";
38import type { WsRpcProtocolClient } from "../rpc/protocol.ts";
39import { v2Now, v2Projection, v2ThreadId } from "../state/orchestrationV2TestFixtures.ts";
40import {
41 archiveThread,
42 cancelQueuedRun,
43 createProject,
44 dismissThreadUserInput,
45 editQueuedRun,
46 forkThreadFromRun,
47 interruptThreadTurn,
48 mergeThreadBack,
49 promoteQueuedRun,
50 reorderActiveThread,
51 reorderQueuedRun,
52 revertThreadCheckpoint,
53 settleThread,
54 startThreadTurn,
55 unsettleThread,
56 updateProject,
57 updateThreadMetadata,
58} from "./commands.ts";
59
60const layerTestCrypto = Layer.succeed(
61 Crypto.Crypto,
62 Crypto.make({
63 randomBytes: (size) => new Uint8Array(size),
64 digest: (_algorithm, data) => Effect.succeed(data),
65 }),
66);
67
68const TARGET = new PrimaryConnectionTarget({
69 environmentId: EnvironmentId.make("environment-1"),
70 label: "Test environment",
71 httpBaseUrl: "https://environment.example.test",
72 wsBaseUrl: "wss://environment.example.test",
73});
74
75const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(function* (input: {
76 readonly commands: OrchestrationV2Command[];
77 readonly projects: ProjectMutation[];
78 readonly launches?: OrchestrationV2ThreadLaunchInput[];
79 readonly projection?: OrchestrationV2ThreadProjection;
80 readonly projectionRequests?: ThreadId[];
81 readonly advertiseServerResolvedCommandContext?: boolean;
82}) {
83 const client = {
84 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command: OrchestrationV2Command) =>
85 Effect.sync(() => {
86 input.commands.push(command);
87 return { sequence: input.commands.length };
88 }),
89 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (requestInput: {
90 readonly threadId: ThreadId;
91 }) =>
92 Effect.sync(() => {
93 input.projectionRequests?.push(requestInput.threadId);
94 return input.projection ?? v2Projection;
95 }),
96 [ORCHESTRATION_V2_WS_METHODS.launchThread]: (launchInput: OrchestrationV2ThreadLaunchInput) =>
97 Effect.sync(() => {
98 input.launches?.push(launchInput);
99 return {
100 threadId: launchInput.threadId ?? v2ThreadId,
101 projection: input.projection ?? v2Projection,
102 resumed: false,
103 };
104 }),
105 [WS_METHODS.projectsMutate]: (mutation: ProjectMutation) =>
106 Effect.sync(() => {
107 input.projects.push(mutation);
108 return {
109 id: mutation.projectId,
110 title: mutation.type === "project.create" ? mutation.title : "Project",
111 workspaceRoot:
112 mutation.type === "project.create" ? mutation.workspaceRoot : "/workspace/project",
113 repositoryIdentity: null,
114 faviconPath: null,
115 defaultModelSelection: null,
116 scripts: [],
117 createdAt: "2026-06-06T00:00:00.000Z",
118 updatedAt: "2026-06-06T00:00:00.000Z",
119 deletedAt: null,
120 };
121 }),
122 } as unknown as WsRpcProtocolClient;
123 const session: RpcSession.RpcSession = {
124 client,
125 initialConfig: Effect.succeed({
126 environment: {
127 capabilities: {
128 repositoryIdentity: true,
129 ...(input.advertiseServerResolvedCommandContext === false
130 ? {}
131 : { serverResolvedCommandContext: true }),
132 },
133 },
134 } as never),
135 subscribeServerConfig: (input) => client.subscribeServerConfig(input),
136 ready: Effect.void,
137 probe: Effect.void,
138 closed: Effect.never,
139 };
140 return EnvironmentSupervisor.EnvironmentSupervisor.of({
141 target: TARGET,
142 state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE),
143 session: yield* SubscriptionRef.make(Option.some(session)),
144 prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()),
145 connect: Effect.void,
146 disconnect: Effect.void,
147 retryNow: Effect.void,
148 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
149});
150
151describe("V2 environment commands", () => {
152 it.effect("routes projects through the event-sourced project transport", () =>
153 Effect.gen(function* () {
154 const projects: ProjectMutation[] = [];
155 const supervisor = yield* makeSupervisor({ commands: [], projects });
156
157 yield* createProject({
158 projectId: ProjectId.make("project-1"),
159 title: "Project",
160 workspaceRoot: "/workspace/project",
161 createWorkspaceRootIfMissing: true,
162 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
163
164 expect(projects).toEqual([
165 {
166 type: "project.create",
167 commandId: "00000000-0000-4000-8000-000000000000",
168 projectId: "project-1",
169 title: "Project",
170 workspaceRoot: "/workspace/project",
171 createWorkspaceRootIfMissing: true,
172 },
173 ]);
174 }).pipe(Effect.provide(layerTestCrypto)),
175 );
176
177 it.effect("persists and clears project presentation and environment settings", () =>
178 Effect.gen(function* () {
179 const projects: ProjectMutation[] = [];
180 const supervisor = yield* makeSupervisor({ commands: [], projects });
181 const projectId = ProjectId.make("project-1");
182 const projectIcon = { kind: "emoji", emoji: "🌲" } as const;
183 yield* updateProject({
184 projectId,
185 autoPull: true,
186 projectIcon,
187 faviconPath: "/workspace/project/icon.png",
188 defaultThreadEnvMode: "worktree",
189 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
190 yield* updateProject({
191 projectId,
192 autoPull: false,
193 projectIcon: null,
194 faviconPath: null,
195 defaultThreadEnvMode: null,
196 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
197
198 expect(projects).toEqual([
199 {
200 type: "project.update",
201 commandId: "00000000-0000-4000-8000-000000000000",
202 projectId,
203 autoPull: true,
204 projectIcon,
205 faviconPath: "/workspace/project/icon.png",
206 defaultThreadEnvMode: "worktree",
207 },
208 {
209 type: "project.update",
210 commandId: "00000000-0000-4000-8000-000000000000",
211 projectId,
212 autoPull: false,
213 projectIcon: null,
214 faviconPath: null,
215 defaultThreadEnvMode: null,
216 },
217 ]);
218 }).pipe(Effect.provide(layerTestCrypto)),
219 );
220
221 it.effect("preserves caller command ids for idempotent V2 commands", () =>
222 Effect.gen(function* () {
223 const commands: OrchestrationV2Command[] = [];
224 const supervisor = yield* makeSupervisor({ commands, projects: [] });
225
226 yield* archiveThread({
227 commandId: CommandId.make("queued-command"),
228 threadId: ThreadId.make("thread-1"),
229 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
230
231 expect(commands).toEqual([
232 { type: "thread.archive", commandId: "queued-command", threadId: "thread-1" },
233 ]);
234 }).pipe(Effect.provide(layerTestCrypto)),
235 );
236
237 it.effect("preserves the settled shelf snapshot guard when archiving", () =>
238 Effect.gen(function* () {
239 const commands: OrchestrationV2Command[] = [];
240 const supervisor = yield* makeSupervisor({ commands, projects: [] });
241 yield* archiveThread({
242 commandId: CommandId.make("guarded-archive"),
243 threadId: ThreadId.make("settled-thread"),
244 snapshotSequence: 137,
245 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
246 expect(commands).toEqual([
247 {
248 type: "thread.archive",
249 commandId: "guarded-archive",
250 threadId: "settled-thread",
251 snapshotSequence: 137,
252 },
253 ]);
254 }).pipe(Effect.provide(layerTestCrypto)),
255 );
256
257 it.effect("resolves run ordinal zero to the persisted thread-start checkpoint", () =>
258 Effect.gen(function* () {
259 const scopeId = CheckpointScopeId.make("checkpoint-scope-root");
260 const checkpointId = CheckpointId.make("checkpoint-thread-start");
261 const projection: OrchestrationV2ThreadProjection = {
262 ...v2Projection,
263 checkpoints: [
264 {
265 id: checkpointId,
266 threadId: v2ThreadId,
267 scopeId,
268 runId: null,
269 nodeId: NodeId.make("node-run-1"),
270 parentCheckpointId: null,
271 ordinalWithinScope: 0,
272 appRunOrdinal: null,
273 ref: CheckpointRef.make("refs/t3/thread-start"),
274 status: "ready",
275 files: [],
276 capturedAt: v2Now,
277 },
278 ],
279 };
280 const commands: OrchestrationV2Command[] = [];
281 const supervisor = yield* makeSupervisor({ commands, projects: [], projection });
282
283 yield* revertThreadCheckpoint({
284 commandId: CommandId.make("rollback-thread-start"),
285 threadId: v2ThreadId,
286 turnCount: 0,
287 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
288
289 expect(commands).toEqual([
290 {
291 type: "checkpoint.rollback",
292 commandId: "rollback-thread-start",
293 threadId: v2ThreadId,
294 scopeId,
295 checkpointId,
296 },
297 ]);
298 }).pipe(Effect.provide(layerTestCrypto)),
299 );
300
301 it.effect("preserves plan implementation provenance on V2 runs", () =>
302 Effect.gen(function* () {
303 const commands: OrchestrationV2Command[] = [];
304 const supervisor = yield* makeSupervisor({ commands, projects: [] });
305
306 yield* startThreadTurn({
307 commandId: CommandId.make("implement-plan"),
308 threadId: v2ThreadId,
309 message: {
310 messageId: MessageId.make("message-implementation"),
311 role: "user",
312 text: "Implement the plan",
313 attachments: [],
314 },
315 modelSelection: {
316 instanceId: ProviderInstanceId.make("codex"),
317 model: "gpt-5.4",
318 },
319 runtimeMode: "full-access",
320 interactionMode: "default",
321 titleSeed: "Implement the plan",
322 sourceProposedPlan: {
323 threadId: ThreadId.make("thread-plan"),
324 planId: PlanId.make("plan-1"),
325 },
326 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
327
328 expect(commands).toHaveLength(1);
329 expect(commands[0]).toMatchObject({
330 type: "message.dispatch",
331 commandId: "implement-plan",
332 threadId: v2ThreadId,
333 titleSeed: "Implement the plan",
334 sourcePlanRef: { threadId: "thread-plan", planId: "plan-1" },
335 deliveryIntent: "auto",
336 dispatchMode: { type: "start_immediately" },
337 });
338 }).pipe(Effect.provide(layerTestCrypto)),
339 );
340
341 it.effect("preserves an existing worktree and branch during first-message launch", () =>
342 Effect.gen(function* () {
343 const launches: OrchestrationV2ThreadLaunchInput[] = [];
344 const supervisor = yield* makeSupervisor({ commands: [], projects: [], launches });
345
346 yield* startThreadTurn({
347 commandId: CommandId.make("launch-existing-worktree"),
348 threadId: v2ThreadId,
349 message: {
350 messageId: MessageId.make("message-existing-worktree"),
351 role: "user",
352 text: "Continue here",
353 attachments: [],
354 },
355 runtimeMode: "full-access",
356 interactionMode: "default",
357 titleSeed: "Continue here",
358 bootstrap: {
359 createThread: {
360 projectId: ProjectId.make("project-1"),
361 title: "Thread",
362 modelSelection: v2Projection.thread.modelSelection,
363 runtimeMode: "full-access",
364 interactionMode: "default",
365 branch: "feature",
366 worktreePath: "/workspace/project-worktrees/feature",
367 createdAt: "2026-06-20T00:00:00.000Z",
368 },
369 },
370 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
371
372 expect(launches[0]).toMatchObject({
373 threadId: v2ThreadId,
374 title: "Continue here",
375 generateTitle: true,
376 workspaceStrategy: {
377 type: "existing_worktree",
378 worktreePath: "/workspace/project-worktrees/feature",
379 branch: "feature",
380 },
381 });
382 }).pipe(Effect.provide(layerTestCrypto)),
383 );
384
385 it.effect("provisions an origin-based worktree for an existing empty thread", () =>
386 Effect.gen(function* () {
387 const launches: OrchestrationV2ThreadLaunchInput[] = [];
388 const supervisor = yield* makeSupervisor({ commands: [], projects: [], launches });
389
390 yield* startThreadTurn({
391 commandId: CommandId.make("launch-existing-thread-worktree"),
392 threadId: v2ThreadId,
393 message: {
394 messageId: MessageId.make("message-existing-thread-worktree"),
395 role: "user",
396 text: "Move to a worktree",
397 attachments: [],
398 },
399 runtimeMode: "full-access",
400 interactionMode: "default",
401 bootstrap: {
402 prepareWorktree: {
403 projectCwd: "/workspace/project",
404 baseBranch: "main",
405 branch: "feature",
406 startFromOrigin: true,
407 },
408 },
409 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
410
411 expect(launches[0]).toMatchObject({
412 threadId: v2ThreadId,
413 reuseExistingThread: true,
414 projectId: v2Projection.thread.projectId,
415 workspaceStrategy: {
416 type: "worktree",
417 baseRef: "main",
418 branch: "feature",
419 startFromOrigin: true,
420 },
421 });
422 }).pipe(Effect.provide(layerTestCrypto)),
423 );
424
425 it.effect("uses server-resolved delivery intent without fetching the full projection", () =>
426 Effect.gen(function* () {
427 const commands: OrchestrationV2Command[] = [];
428 const projectionRequests: ThreadId[] = [];
429 const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests });
430
431 for (const [mode, expectedType] of [
432 ["queue", "queue_after_active"],
433 ["auto", "start_immediately"],
434 ["steer", "start_immediately"],
435 ["restart", "start_immediately"],
436 ] as const) {
437 yield* startThreadTurn({
438 commandId: CommandId.make(`command-${mode}`),
439 threadId: v2ThreadId,
440 message: {
441 messageId: MessageId.make(`message-${mode}`),
442 role: "user",
443 text: mode,
444 attachments: [],
445 },
446 runtimeMode: "full-access",
447 interactionMode: "default",
448 dispatchMode: mode,
449 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
450
451 expect(commands.at(-1)).toMatchObject({
452 type: "message.dispatch",
453 dispatchMode: { type: expectedType },
454 ...(mode === "queue" ? {} : { deliveryIntent: mode }),
455 });
456 }
457 expect(projectionRequests).toEqual([]);
458 }).pipe(Effect.provide(layerTestCrypto)),
459 );
460
461 it.effect("retains projection-shaped delivery for servers without command context support", () =>
462 Effect.gen(function* () {
463 const activeRunId = RunId.make("legacy-active-run");
464 const projection: OrchestrationV2ThreadProjection = {
465 ...v2Projection,
466 runs: [
467 {
468 id: activeRunId,
469 threadId: v2ThreadId,
470 ordinal: 1,
471 providerInstanceId: v2Projection.thread.providerInstanceId,
472 modelSelection: v2Projection.thread.modelSelection,
473 providerThreadId: null,
474 userMessageId: MessageId.make("legacy-active-message"),
475 rootNodeId: null,
476 activeAttemptId: null,
477 status: "running",
478 requestedAt: v2Now,
479 startedAt: v2Now,
480 completedAt: null,
481 checkpointId: null,
482 contextHandoffId: null,
483 },
484 ],
485 };
486 const commands: OrchestrationV2Command[] = [];
487 const projectionRequests: ThreadId[] = [];
488 const supervisor = yield* makeSupervisor({
489 commands,
490 projects: [],
491 projection,
492 projectionRequests,
493 advertiseServerResolvedCommandContext: false,
494 });
495
496 for (const [mode, expectedMode] of [
497 ["auto", { type: "queue_after_active" }],
498 ["queue", { type: "queue_after_active" }],
499 ["steer", { type: "steer_active", targetRunId: activeRunId }],
500 ["restart", { type: "restart_active", targetRunId: activeRunId }],
501 ] as const) {
502 yield* startThreadTurn({
503 commandId: CommandId.make(`legacy-command-${mode}`),
504 threadId: v2ThreadId,
505 message: {
506 messageId: MessageId.make(`legacy-message-${mode}`),
507 role: "user",
508 text: mode,
509 attachments: [],
510 },
511 runtimeMode: "full-access",
512 interactionMode: "default",
513 dispatchMode: mode,
514 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
515
516 expect(commands.at(-1)).toMatchObject({
517 type: "message.dispatch",
518 dispatchMode: expectedMode,
519 });
520 expect(commands.at(-1)).not.toHaveProperty("deliveryIntent");
521 }
522 expect(projectionRequests).toEqual([v2ThreadId, v2ThreadId, v2ThreadId, v2ThreadId]);
523 }).pipe(Effect.provide(layerTestCrypto)),
524 );
525
526 it.effect("Stop with no run left ends the thread's pull request watches", () =>
527 Effect.gen(function* () {
528 const link = {
529 host: "github.com",
530 repository: "pingdotgg/t3code",
531 url: "https://github.com/pingdotgg/t3code/pull/7",
532 source: "agent" as const,
533 linkedAt: "2026-10-05T00:00:00.000Z",
534 snapshot: null,
535 stack: null,
536 };
537 const watch = {
538 startedAt: "2026-10-05T00:00:00.000Z",
539 headSha: null,
540 failedChecks: [],
541 passed: false,
542 passedChecks: [],
543 remarksThrough: "2026-10-05T00:00:00.000Z",
544 remarkIds: [],
545 conflicting: false,
546 wakes: 0,
547 };
548 const projection: OrchestrationV2ThreadProjection = {
549 ...v2Projection,
550 thread: {
551 ...v2Projection.thread,
552 pullRequests: [
553 { ...link, number: 7, watch },
554 { ...link, number: 8 },
555 { ...link, number: 9, source: "stack-dismissed", watch },
556 ],
557 },
558 };
559 const commands: OrchestrationV2Command[] = [];
560 const supervisor = yield* makeSupervisor({ commands, projects: [], projection });
561
562 yield* interruptThreadTurn({ threadId: v2ThreadId }).pipe(
563 Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
564 );
565
566 expect(commands).toEqual([
567 {
568 type: "thread.pull-request.watch",
569 commandId: expect.any(String),
570 threadId: v2ThreadId,
571 host: "github.com",
572 repository: "pingdotgg/t3code",
573 number: 7,
574 watching: false,
575 },
576 ]);
577 }).pipe(Effect.provide(layerTestCrypto)),
578 );
579
580 it.effect.each([
581 "waiting",
582 "completed",
583 "failed",
584 "interrupted",
585 "cancelled",
586 "rolled_back",
587 ] as const)("dispatches Stop for %s runs with background commands except rollback", (status) =>
588 Effect.gen(function* () {
589 const waitingRunId = RunId.make("run-waiting");
590 const projection: OrchestrationV2ThreadProjection = {
591 ...v2Projection,
592 runs: [
593 {
594 id: waitingRunId,
595 threadId: v2ThreadId,
596 ordinal: 1,
597 providerInstanceId: v2Projection.thread.providerInstanceId,
598 modelSelection: v2Projection.thread.modelSelection,
599 providerThreadId: null,
600 userMessageId: MessageId.make("message-waiting"),
601 rootNodeId: null,
602 activeAttemptId: null,
603 status,
604 requestedAt: v2Now,
605 startedAt: v2Now,
606 completedAt: null,
607 checkpointId: null,
608 contextHandoffId: null,
609 },
610 ],
611 turnItems: [
612 {
613 id: TurnItemId.make("background-command"),
614 threadId: v2ThreadId,
615 runId: waitingRunId,
616 nodeId: null,
617 providerThreadId: null,
618 providerTurnId: null,
619 nativeItemRef: null,
620 parentItemId: null,
621 ordinal: 1,
622 status: "running",
623 title: null,
624 startedAt: v2Now,
625 completedAt: null,
626 updatedAt: v2Now,
627 type: "command_execution",
628 input: "vp run dev",
629 },
630 ],
631 };
632 const commands: OrchestrationV2Command[] = [];
633 const supervisor = yield* makeSupervisor({ commands, projects: [], projection });
634
635 const result = yield* interruptThreadTurn({ threadId: v2ThreadId }).pipe(
636 Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
637 );
638
639 expect(result).toEqual({ sequence: status === "rolled_back" ? 0 : 1 });
640 expect(commands).toEqual(
641 status === "rolled_back"
642 ? []
643 : [
644 {
645 type: "run.interrupt",
646 commandId: expect.any(String),
647 threadId: v2ThreadId,
648 runId: waitingRunId,
649 holdQueue: true,
650 },
651 ],
652 );
653 }).pipe(Effect.provide(layerTestCrypto)),
654 );
655
656 it.effect(
657 "dispatches V2-native relationship and queue commands without compatibility shaping",
658 () =>
659 Effect.gen(function* () {
660 const commands: OrchestrationV2Command[] = [];
661 const supervisor = yield* makeSupervisor({ commands, projects: [] });
662 const provide = Effect.provideService(
663 EnvironmentSupervisor.EnvironmentSupervisor,
664 supervisor,
665 );
666
667 yield* forkThreadFromRun({
668 commandId: CommandId.make("fork"),
669 sourceThreadId: v2ThreadId,
670 targetThreadId: ThreadId.make("thread-fork"),
671 runId: RunId.make("run-1"),
672 }).pipe(provide);
673 yield* mergeThreadBack({
674 commandId: CommandId.make("merge"),
675 sourceThreadId: ThreadId.make("thread-fork"),
676 targetThreadId: v2ThreadId,
677 runId: RunId.make("run-2"),
678 }).pipe(provide);
679 yield* reorderQueuedRun({
680 commandId: CommandId.make("reorder"),
681 threadId: v2ThreadId,
682 runId: RunId.make("run-3"),
683 beforeRunId: RunId.make("run-4"),
684 }).pipe(provide);
685 yield* promoteQueuedRun({
686 commandId: CommandId.make("promote"),
687 threadId: v2ThreadId,
688 queuedRunId: RunId.make("run-3"),
689 targetRunId: RunId.make("run-active"),
690 }).pipe(provide);
691 yield* cancelQueuedRun({
692 commandId: CommandId.make("cancel"),
693 threadId: v2ThreadId,
694 runId: RunId.make("run-3"),
695 }).pipe(provide);
696 yield* editQueuedRun({
697 commandId: CommandId.make("edit"),
698 threadId: v2ThreadId,
699 runId: RunId.make("run-3"),
700 text: "updated queued text",
701 }).pipe(provide);
702 yield* editQueuedRun({
703 commandId: CommandId.make("edit-attachments"),
704 threadId: v2ThreadId,
705 runId: RunId.make("run-3"),
706 text: "updated queued text with attachments",
707 edit: {
708 messageId: MessageId.make("message-3"),
709 attachments: [
710 {
711 type: "image",
712 id: "attachment-kept",
713 name: "kept.png",
714 mimeType: "image/png",
715 sizeBytes: 64,
716 },
717 ],
718 },
719 }).pipe(provide);
720
721 expect(commands).toMatchObject([
722 { type: "thread.fork", sourcePoint: { type: "run", runId: "run-1" } },
723 { type: "thread.merge_back", sourcePoint: { type: "run", runId: "run-2" } },
724 { type: "queued-run.reorder", runId: "run-3", beforeRunId: "run-4" },
725 {
726 type: "queued-message.promote-to-steer",
727 queuedRunId: "run-3",
728 targetRunId: "run-active",
729 },
730 { type: "queued-run.cancel", runId: "run-3" },
731 { type: "queued-run.edit", runId: "run-3", text: "updated queued text" },
732 {
733 type: "queued-run.edit",
734 runId: "run-3",
735 text: "updated queued text with attachments",
736 attachments: [{ id: "attachment-kept" }],
737 },
738 ]);
739 // A text-only edit must not send an attachments replacement list.
740 expect(commands[5]).not.toHaveProperty("attachments");
741 }).pipe(Effect.provide(layerTestCrypto)),
742 );
743
744 it.effect("delegates model selection to the server without fetching the full projection", () =>
745 Effect.gen(function* () {
746 const commands: OrchestrationV2Command[] = [];
747 const projectionRequests: ThreadId[] = [];
748 const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests });
749
750 yield* updateThreadMetadata({
751 commandId: CommandId.make("same-provider"),
752 threadId: v2ThreadId,
753 modelSelection: {
754 instanceId: ProviderInstanceId.make("codex"),
755 model: "another-model",
756 },
757 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
758
759 yield* updateThreadMetadata({
760 commandId: CommandId.make("switch-provider"),
761 threadId: v2ThreadId,
762 modelSelection: {
763 instanceId: ProviderInstanceId.make("claude"),
764 model: "claude-sonnet-4-6",
765 },
766 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
767
768 expect(commands).toEqual([
769 {
770 type: "thread.model-selection.set",
771 commandId: "same-provider",
772 threadId: v2ThreadId,
773 modelSelection: { instanceId: "codex", model: "another-model" },
774 },
775 {
776 type: "thread.model-selection.set",
777 commandId: "switch-provider",
778 threadId: v2ThreadId,
779 modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" },
780 },
781 ]);
782 expect(projectionRequests).toEqual([]);
783 }).pipe(Effect.provide(layerTestCrypto)),
784 );
785
786 it.effect("retains provider-switch shaping for servers without command context support", () =>
787 Effect.gen(function* () {
788 const commands: OrchestrationV2Command[] = [];
789 const projectionRequests: ThreadId[] = [];
790 const supervisor = yield* makeSupervisor({
791 commands,
792 projects: [],
793 projectionRequests,
794 advertiseServerResolvedCommandContext: false,
795 });
796
797 yield* updateThreadMetadata({
798 commandId: CommandId.make("legacy-switch-provider"),
799 threadId: v2ThreadId,
800 modelSelection: {
801 instanceId: ProviderInstanceId.make("claude"),
802 model: "claude-sonnet-4-6",
803 },
804 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
805
806 expect(projectionRequests).toEqual([v2ThreadId]);
807 expect(commands).toEqual([
808 {
809 type: "provider.switch",
810 commandId: "legacy-switch-provider",
811 threadId: v2ThreadId,
812 modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" },
813 },
814 ]);
815 }).pipe(Effect.provide(layerTestCrypto)),
816 );
817
818 it.effect.each([true, false])(
819 "rolls back an identified checkpoint without fetching the full projection, restoreFiles=%s",
820 (restoreFiles) =>
821 Effect.gen(function* () {
822 const commands: OrchestrationV2Command[] = [];
823 const projectionRequests: ThreadId[] = [];
824 const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests });
825
826 yield* revertThreadCheckpoint({
827 commandId: CommandId.make("rollback-known-checkpoint"),
828 threadId: v2ThreadId,
829 checkpointId: CheckpointId.make("checkpoint-known"),
830 scopeId: CheckpointScopeId.make("scope-known"),
831 restoreFiles,
832 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
833
834 expect(projectionRequests).toEqual([]);
835 expect(commands).toEqual([
836 {
837 type: "checkpoint.rollback",
838 commandId: "rollback-known-checkpoint",
839 threadId: v2ThreadId,
840 checkpointId: "checkpoint-known",
841 scopeId: "scope-known",
842 restoreFiles,
843 },
844 ]);
845 }).pipe(Effect.provide(layerTestCrypto)),
846 );
847
848 it.effect("validates identified checkpoints locally for older servers", () =>
849 Effect.gen(function* () {
850 const checkpointId = CheckpointId.make("legacy-checkpoint");
851 const scopeId = CheckpointScopeId.make("legacy-checkpoint-scope");
852 for (const status of ["ready", "missing", "error", "stale", null] as const) {
853 const commands: OrchestrationV2Command[] = [];
854 const projectionRequests: ThreadId[] = [];
855 const supervisor = yield* makeSupervisor({
856 commands,
857 projects: [],
858 projectionRequests,
859 advertiseServerResolvedCommandContext: false,
860 projection: {
861 ...v2Projection,
862 checkpoints:
863 status === null
864 ? []
865 : [
866 {
867 id: checkpointId,
868 threadId: v2ThreadId,
869 scopeId,
870 runId: null,
871 nodeId: NodeId.make("legacy-checkpoint-node"),
872 parentCheckpointId: null,
873 ordinalWithinScope: 0,
874 appRunOrdinal: null,
875 ref: CheckpointRef.make("refs/t3/legacy-checkpoint"),
876 status,
877 files: [],
878 capturedAt: v2Now,
879 },
880 ],
881 },
882 });
883 const rollback = revertThreadCheckpoint({
884 commandId: CommandId.make(`legacy-rollback-${status}`),
885 threadId: v2ThreadId,
886 checkpointId,
887 scopeId,
888 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
889
890 if (status === "ready") {
891 yield* rollback;
892 expect(commands).toEqual([
893 {
894 type: "checkpoint.rollback",
895 commandId: "legacy-rollback-ready",
896 threadId: v2ThreadId,
897 checkpointId,
898 scopeId,
899 },
900 ]);
901 } else {
902 const error = yield* rollback.pipe(Effect.flip);
903 expect(error._tag).toBe("OrchestrationV2CheckpointUnavailableError");
904 expect(commands).toEqual([]);
905 }
906 expect(projectionRequests).toEqual([v2ThreadId]);
907 }
908 }).pipe(Effect.provide(layerTestCrypto)),
909 );
910
911 it.effect("dispatches settle and unsettle commands without timestamps", () =>
912 Effect.gen(function* () {
913 const dispatched: OrchestrationV2Command[] = [];
914 const supervisor = yield* makeSupervisor({ commands: dispatched, projects: [] });
915
916 yield* settleThread({
917 commandId: CommandId.make("settle-command"),
918 threadId: ThreadId.make("thread-1"),
919 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
920 yield* unsettleThread({
921 commandId: CommandId.make("unsettle-command"),
922 threadId: ThreadId.make("thread-1"),
923 reason: "user",
924 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
925
926 expect(dispatched).toEqual([
927 {
928 type: "thread.settle",
929 commandId: "settle-command",
930 threadId: "thread-1",
931 },
932 {
933 type: "thread.unsettle",
934 commandId: "unsettle-command",
935 threadId: "thread-1",
936 reason: "user",
937 },
938 ]);
939 }).pipe(Effect.provide(layerTestCrypto)),
940 );
941
942 it.effect("sends an active order key without changing activity timestamps", () =>
943 Effect.gen(function* () {
944 const dispatched: OrchestrationV2Command[] = [];
945 const supervisor = yield* makeSupervisor({ commands: dispatched, projects: [] });
946 yield* reorderActiveThread({
947 commandId: CommandId.make("reorder-command"),
948 threadId: ThreadId.make("thread-1"),
949 orderKey: "mf",
950 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
951 expect(dispatched).toEqual([
952 {
953 type: "thread.active.reorder",
954 commandId: "reorder-command",
955 threadId: "thread-1",
956 orderKey: "mf",
957 },
958 ]);
959 }).pipe(Effect.provide(layerTestCrypto)),
960 );
961
962 it.effect("dismisses a pending user-input request", () =>
963 Effect.gen(function* () {
964 const commands: OrchestrationV2Command[] = [];
965 const supervisor = yield* makeSupervisor({ commands, projects: [] });
966
967 yield* dismissThreadUserInput({
968 commandId: CommandId.make("dismiss-command"),
969 threadId: v2ThreadId,
970 requestId: RuntimeRequestId.make("request-1"),
971 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
972
973 expect(commands).toEqual([
974 {
975 type: "thread.user-input.dismiss",
976 commandId: "dismiss-command",
977 threadId: v2ThreadId,
978 requestId: "request-1",
979 },
980 ]);
981 }).pipe(Effect.provide(layerTestCrypto)),
982 );
983
984 it.effect("dispatches an explicit idle start without fetching the full projection", () =>
985 Effect.gen(function* () {
986 const commands: OrchestrationV2Command[] = [];
987 const projectionRequests: ThreadId[] = [];
988 const supervisor = yield* makeSupervisor({
989 commands,
990 projects: [],
991 projectionRequests,
992 });
993
994 yield* startThreadTurn({
995 commandId: CommandId.make("direct-start"),
996 threadId: v2ThreadId,
997 message: {
998 messageId: MessageId.make("direct-start-message"),
999 role: "user",
1000 text: "continue",
1001 attachments: [],
1002 },
1003 runtimeMode: "full-access",
1004 interactionMode: "default",
1005 dispatchMode: "start",
1006 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
1007
1008 expect(projectionRequests).toEqual([]);
1009 expect(commands).toMatchObject([
1010 {
1011 type: "message.dispatch",
1012 threadId: v2ThreadId,
1013 dispatchMode: { type: "start_immediately" },
1014 },
1015 ]);
1016 }).pipe(Effect.provide(layerTestCrypto)),
1017 );
1018
1019 it.effect(
1020 "a native subagent stop targets its owning run rather than holding the parent queue",
1021 () =>
1022 Effect.gen(function* () {
1023 const subagentId = NodeId.make("native-subagent");
1024 const runId = RunId.make("settled-owner-run");
1025 const commands: OrchestrationV2Command[] = [];
1026 const supervisor = yield* makeSupervisor({
1027 commands,
1028 projects: [],
1029 projection: {
1030 ...v2Projection,
1031 subagents: [
1032 {
1033 id: subagentId,
1034 threadId: v2ThreadId,
1035 runId,
1036 parentNodeId: NodeId.make("owner-node"),
1037 origin: "provider_native",
1038 createdBy: "agent",
1039 driver: ProviderDriverKind.make("claudeAgent"),
1040 providerInstanceId: ProviderInstanceId.make("claudeAgent"),
1041 providerThreadId: null,
1042 childThreadId: ThreadId.make("native-child-thread"),
1043 nativeTaskRef: null,
1044 prompt: "Work",
1045 title: null,
1046 model: null,
1047 result: null,
1048 status: "running",
1049 startedAt: v2Now,
1050 completedAt: null,
1051 updatedAt: v2Now,
1052 },
1053 ],
1054 },
1055 });
1056 yield* interruptThreadTurn({
1057 commandId: CommandId.make("native-stop"),
1058 threadId: v2ThreadId,
1059 subagentId,
1060 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
1061 expect(commands).toEqual([
1062 {
1063 type: "subagent.stop",
1064 commandId: "native-stop",
1065 threadId: v2ThreadId,
1066 runId,
1067 subagentId,
1068 },
1069 ]);
1070 }).pipe(Effect.provide(layerTestCrypto)),
1071 );
1072
1073 it.effect("interrupts a known run without fetching the full projection", () =>
1074 Effect.gen(function* () {
1075 const commands: OrchestrationV2Command[] = [];
1076 const projectionRequests: ThreadId[] = [];
1077 const supervisor = yield* makeSupervisor({
1078 commands,
1079 projects: [],
1080 projectionRequests,
1081 });
1082
1083 yield* interruptThreadTurn({
1084 commandId: CommandId.make("direct-interrupt"),
1085 threadId: v2ThreadId,
1086 runId: RunId.make("active-run"),
1087 }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
1088
1089 expect(projectionRequests).toEqual([]);
1090 expect(commands).toEqual([
1091 {
1092 type: "run.interrupt",
1093 commandId: "direct-interrupt",
1094 threadId: v2ThreadId,
1095 runId: "active-run",
1096 holdQueue: true,
1097 },
1098 ]);
1099 }).pipe(Effect.provide(layerTestCrypto)),
1100 );
1101});