import { assert, describe, it } from "@effect/vitest";import { CommandId, MessageId, type ModelSelection, type OrchestrationV2Command, type OrchestrationV2ProviderCapabilities, type OrchestrationV2ProviderSession, type OrchestrationV2ProviderThread, ProjectId, ProviderInstanceId, ProviderThreadId, ProviderTurnId, ThreadId, TurnItemId, ProviderDriverKind,} from "@t3tools/contracts";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient"; import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts";import { CodexProviderCapabilitiesV2 } from "../Adapters/CodexAdapterV2.ts";import { CursorProviderCapabilitiesV2 } from "../Adapters/CursorAdapterV2.ts";import { layer as eventSinkLayer } from "../EventSink.ts";import { layer as eventStoreLayer } from "../EventStore.ts";import { LegacyV1ThreadImporter, layer as legacyV1ThreadImporterLayer,} from "../LegacyV1ThreadImporter.ts";import { OrchestratorV2 } from "../Orchestrator.ts";import { ProjectionMaintenanceV2, layer as projectionMaintenanceLayer,} from "../ProjectionMaintenance.ts";import { layer as projectionStoreLayer } from "../ProjectionStore.ts";import { type ProviderAdapterV2Event, ProviderAdapterProtocolError, type ProviderAdapterV2Shape,} from "../ProviderAdapter.ts";import { makeLayer as makeProviderAdapterRegistryLayer } from "../ProviderAdapterRegistry.ts";import { makeProviderFailure } from "../ProviderFailure.ts";import { CLAUDE_MODEL_SELECTION, CODEX_MODEL_SELECTION, CURSOR_MODEL_SELECTION,} from "./fixtures/shared.ts";import { makeOrchestratorV2ReplayLayerWithRegistry } from "./ProviderReplayHarness.ts";import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; const threadId = ThreadId.make("thread:provider-switch");const projectId = ProjectId.make("project:provider-switch");const firstPrompt = "Respond with exactly: codex before switch";const claudePrompt = "Respond with exactly: claude switched response";const returnPrompt = "Respond with exactly: codex after return";const CODEX_DRIVER = ProviderDriverKind.make("codex");const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent");const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); interface CapturedTurn { readonly driver: ProviderDriverKind; readonly threadId: ThreadId; readonly providerThreadId: ProviderThreadId; readonly text: string;} function unimplemented(driver: ProviderDriverKind, detail: string) { return Effect.fail(new ProviderAdapterProtocolError({ driver, detail }));} function makeTestAdapter(input: { readonly instanceId: ProviderInstanceId; readonly driver: ProviderDriverKind; readonly capabilities: OrchestrationV2ProviderCapabilities; readonly modelSelection: ModelSelection; readonly responseByRunOrdinal: Readonly<Record<number, string>>; readonly responseByThreadId?: Readonly<Record<string, Readonly<Record<number, string>>>>; readonly capturedTurns: Ref.Ref<ReadonlyArray<CapturedTurn>>; readonly failResume?: boolean; readonly failedRunOrdinals?: ReadonlySet<number>;}): ProviderAdapterV2Shape { return { instanceId: input.instanceId, driver: input.driver, getCapabilities: () => Effect.succeed(input.capabilities), planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }), openSession: (sessionInput) => Effect.gen(function* () { const events = yield* PubSub.unbounded<ProviderAdapterV2Event>(); const now = yield* DateTime.now; const providerSession: OrchestrationV2ProviderSession = { id: sessionInput.providerSessionId, driver: input.driver, providerInstanceId: input.instanceId, status: "ready", cwd: sessionInput.runtimePolicy.cwd ?? process.cwd(), model: input.modelSelection.model, capabilities: input.capabilities, createdAt: now, updatedAt: now, lastError: null, }; return { instanceId: input.instanceId, driver: input.driver, providerSessionId: sessionInput.providerSessionId, providerSession, events: Stream.fromPubSub(events), ensureThread: (threadInput) => Effect.gen(function* () { const createdAt = yield* DateTime.now; const nativeThreadId = `${input.driver}:${threadInput.threadId}`; return { id: ProviderThreadId.make(`provider-thread:${nativeThreadId}`), driver: input.driver, providerInstanceId: input.instanceId, providerSessionId: sessionInput.providerSessionId, appThreadId: threadInput.threadId, ownerNodeId: null, nativeThreadRef: { driver: input.driver, nativeId: nativeThreadId, strength: "strong", }, nativeConversationHeadRef: null, status: "idle", firstRunOrdinal: null, lastRunOrdinal: null, handoffIds: [], forkedFrom: null, createdAt, updatedAt: createdAt, } satisfies OrchestrationV2ProviderThread; }), resumeThread: ({ providerThread }) => input.failResume ? unimplemented(input.driver, "simulated native resume failure") : Effect.succeed(providerThread), startTurn: (turnInput) => Effect.gen(function* () { yield* Effect.yieldNow; yield* Ref.update(input.capturedTurns, (turns) => [ ...turns, { driver: input.driver, threadId: turnInput.threadId, providerThreadId: turnInput.providerThread.id, text: turnInput.message.text, }, ]); const eventTime = yield* DateTime.now; const providerTurnId = ProviderTurnId.make( `provider-turn:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}`, ); if (input.failedRunOrdinals?.has(turnInput.runOrdinal) === true) { yield* PubSub.publish(events, { type: "provider_turn.updated", driver: input.driver, providerTurn: { id: providerTurnId, providerThreadId: turnInput.providerThread.id, nodeId: turnInput.rootNodeId, runAttemptId: turnInput.attemptId, nativeTurnRef: { driver: input.driver, nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, strength: "strong", }, ordinal: turnInput.runOrdinal, status: "failed", startedAt: eventTime, completedAt: eventTime, }, }); yield* PubSub.publish(events, { type: "turn.terminal", driver: input.driver, providerThreadId: turnInput.providerThread.id, providerTurnId, runOrdinal: turnInput.runOrdinal, failureItemOrdinal: turnInput.runOrdinal * 100 + 1, status: "failed", failure: makeProviderFailure({ message: "Simulated provider failure.", code: "simulated_failure", class: "provider_error", }), threadDisposition: "reusable", }); return; } const response = input.responseByThreadId?.[turnInput.threadId]?.[turnInput.runOrdinal] ?? input.responseByRunOrdinal[turnInput.runOrdinal] ?? `${input.driver} response for run ${turnInput.runOrdinal}`; const providerEvents: ReadonlyArray<ProviderAdapterV2Event> = [ { type: "provider_turn.updated", driver: input.driver, providerTurn: { id: providerTurnId, providerThreadId: turnInput.providerThread.id, nodeId: turnInput.rootNodeId, runAttemptId: turnInput.attemptId, nativeTurnRef: { driver: input.driver, nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, strength: "strong", }, ordinal: turnInput.runOrdinal, status: "completed", startedAt: eventTime, completedAt: eventTime, }, }, { type: "turn_item.updated", driver: input.driver, turnItem: { id: TurnItemId.make( `turn-item:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, ), threadId: turnInput.threadId, runId: turnInput.runId, nodeId: turnInput.rootNodeId, providerThreadId: turnInput.providerThread.id, providerTurnId, nativeItemRef: null, parentItemId: null, ordinal: turnInput.runOrdinal * 100 + 1, status: "completed", title: null, startedAt: eventTime, completedAt: eventTime, updatedAt: eventTime, type: "assistant_message", messageId: MessageId.make( `message:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, ), text: response, streaming: false, }, }, { type: "turn.terminal", driver: input.driver, providerThreadId: turnInput.providerThread.id, providerTurnId, runOrdinal: turnInput.runOrdinal, status: "completed", failure: null, threadDisposition: "reusable", }, ]; for (const event of providerEvents) { yield* PubSub.publish(events, event); } }), steerTurn: () => Effect.void, interruptTurn: () => Effect.void, respondToRuntimeRequest: () => Effect.void, readThreadSnapshot: () => unimplemented(input.driver, "readThreadSnapshot unused in provider switch test"), rollbackThread: () => unimplemented(input.driver, "rollbackThread unused in provider switch test"), forkThread: () => unimplemented(input.driver, "forkThread unused in provider switch test"), }; }), };} const waitForIdle = Effect.fn("ProviderSwitchTest.waitForIdle")(function* ( targetThreadId: ThreadId,) { const orchestrator = yield* OrchestratorV2; for (let attempt = 0; attempt < 1_000; attempt += 1) { const projection = yield* orchestrator.getThreadProjection(targetThreadId); if ( projection.runs.every( (run) => !["queued", "starting", "running", "waiting"].includes(run.status), ) ) { return projection; } yield* Effect.sleep("5 millis"); } return yield* Effect.die(new Error("Provider switch test timed out waiting for idle"));}); describe("orchestration v2 provider switching", () => { it.live("reissues imported v1 context when switching after the first provider fails", () => Effect.scoped( Effect.gen(function* () { const importedThreadId = ThreadId.make("thread:provider-switch:legacy-import"); const importedProjectId = ProjectId.make("project:provider-switch:legacy-import"); const failedPrompt = "This first provider attempt should fail."; const recoveryPrompt = "What was the imported release marker?"; const cwd = yield* checkpointWorkspace("provider-switch-legacy-import"); const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]); const registryLayer = makeProviderAdapterRegistryLayer([ makeTestAdapter({ instanceId: ProviderInstanceId.make("codex"), driver: CODEX_DRIVER, capabilities: CodexProviderCapabilitiesV2, modelSelection: CODEX_MODEL_SELECTION, responseByRunOrdinal: {}, capturedTurns, failedRunOrdinals: new Set([1]), }), makeTestAdapter({ instanceId: ProviderInstanceId.make("claudeAgent"), driver: CLAUDE_DRIVER, capabilities: ClaudeProviderCapabilitiesV2, modelSelection: CLAUDE_MODEL_SELECTION, responseByRunOrdinal: { 2: "The imported release marker is violet." }, capturedTurns, }), ]); const databaseLayer = SqlitePersistenceMemory; const eventStoreProvided = eventStoreLayer.pipe(Layer.provideMerge(databaseLayer)); const projectionStoreProvided = projectionStoreLayer.pipe( Layer.provideMerge(databaseLayer), ); const storesProvided = Layer.mergeAll( databaseLayer, eventStoreProvided, projectionStoreProvided, ); const eventSinkProvided = eventSinkLayer.pipe(Layer.provide(storesProvided)); const importerProvided = legacyV1ThreadImporterLayer.pipe( Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)), ); const maintenanceProvided = projectionMaintenanceLayer.pipe(Layer.provide(storesProvided)); const orchestratorProvided = makeOrchestratorV2ReplayLayerWithRegistry( { name: "provider-switch-legacy-import", runtimePolicyOverride: { cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false, }, }, }, registryLayer, { databaseLayer }, ); const testLayer = Layer.mergeAll( storesProvided, importerProvided, maintenanceProvided, orchestratorProvided, ); const projection = yield* Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const importer = yield* LegacyV1ThreadImporter; const maintenance = yield* ProjectionMaintenanceV2; const orchestrator = yield* OrchestratorV2; yield* sql` INSERT INTO projection_projects ( project_id, title, workspace_root, default_model_selection_json, scripts_json, created_at, updated_at, deleted_at ) VALUES ( ${importedProjectId}, 'Imported provider switch project', ${cwd}, '{"instanceId":"codex","model":"gpt-5.4"}', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL ) `; yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, branch, worktree_path, latest_turn_id, created_at, updated_at, archived_at, settled_override, settled_at, deleted_at ) VALUES ( ${importedThreadId}, ${importedProjectId}, 'Imported provider switch thread', '{"instanceId":"codex","model":"gpt-5.4"}', 'full-access', 'default', 'main', ${cwd}, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, NULL, NULL, NULL ) `; yield* sql` INSERT INTO projection_thread_messages ( message_id, thread_id, turn_id, role, text, attachments_json, is_streaming, created_at, updated_at ) VALUES ( 'message:provider-switch:legacy-import:user', ${importedThreadId}, NULL, 'user', 'Remember that the imported release marker is violet.', '[]', 0, '2026-01-01T01:00:00.000Z', '2026-01-01T01:00:00.000Z' ), ( 'message:provider-switch:legacy-import:assistant', ${importedThreadId}, NULL, 'assistant', 'I will remember violet.', '[]', 0, '2026-01-01T01:01:00.000Z', '2026-01-01T01:01:00.000Z' ) `; yield* importer.reconcileShells; yield* maintenance.rebuild; yield* importer.ensureTranscript(importedThreadId); yield* orchestrator.dispatch({ type: "message.dispatch", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:provider-switch:legacy-import:failed"), threadId: importedThreadId, messageId: MessageId.make("message:provider-switch:legacy-import:failed"), text: failedPrompt, attachments: [], modelSelection: CODEX_MODEL_SELECTION, dispatchMode: { type: "start_immediately" }, }); yield* waitForIdle(importedThreadId); yield* orchestrator.dispatch({ type: "message.dispatch", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:provider-switch:legacy-import:recovery"), threadId: importedThreadId, messageId: MessageId.make("message:provider-switch:legacy-import:recovery"), text: recoveryPrompt, attachments: [], modelSelection: CLAUDE_MODEL_SELECTION, dispatchMode: { type: "start_immediately" }, }); return yield* waitForIdle(importedThreadId); }).pipe(Effect.provide(testLayer)); const turns = yield* Ref.get(capturedTurns); assert.deepEqual( projection.runs.map((run) => [run.providerInstanceId, run.status]), [ ["codex", "failed"], ["claudeAgent", "completed"],Lines 1–500 of 1251. Load more
import { assert, describe, it } from "@effect/vitest"; import { CommandId, MessageId, type ModelSelection, type OrchestrationV2Command, type OrchestrationV2ProviderCapabilities, type OrchestrationV2ProviderSession, type OrchestrationV2ProviderThread, ProjectId, ProviderInstanceId, ProviderThreadId, ProviderTurnId, ThreadId, TurnItemId, ProviderDriverKind, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; import { ClaudeProviderCapabilitiesV2 } from "../Adapters/ClaudeAdapterV2.ts"; import { CodexProviderCapabilitiesV2 } from "../Adapters/CodexAdapterV2.ts"; import { CursorProviderCapabilitiesV2 } from "../Adapters/CursorAdapterV2.ts"; import { layer as eventSinkLayer } from "../EventSink.ts"; import { layer as eventStoreLayer } from "../EventStore.ts"; import { LegacyV1ThreadImporter, layer as legacyV1ThreadImporterLayer, } from "../LegacyV1ThreadImporter.ts"; import { OrchestratorV2 } from "../Orchestrator.ts"; import { ProjectionMaintenanceV2, layer as projectionMaintenanceLayer, } from "../ProjectionMaintenance.ts"; import { layer as projectionStoreLayer } from "../ProjectionStore.ts"; import { type ProviderAdapterV2Event, ProviderAdapterProtocolError, type ProviderAdapterV2Shape, } from "../ProviderAdapter.ts"; import { makeLayer as makeProviderAdapterRegistryLayer } from "../ProviderAdapterRegistry.ts"; import { makeProviderFailure } from "../ProviderFailure.ts"; import { CLAUDE_MODEL_SELECTION, CODEX_MODEL_SELECTION, CURSOR_MODEL_SELECTION, } from "./fixtures/shared.ts"; import { makeOrchestratorV2ReplayLayerWithRegistry } from "./ProviderReplayHarness.ts"; import { checkpointWorkspace } from "./ReplayFixtureWorkspace.ts"; const threadId = ThreadId.make("thread:provider-switch"); const projectId = ProjectId.make("project:provider-switch"); const firstPrompt = "Respond with exactly: codex before switch"; const claudePrompt = "Respond with exactly: claude switched response"; const returnPrompt = "Respond with exactly: codex after return"; const CODEX_DRIVER = ProviderDriverKind.make("codex"); const CLAUDE_DRIVER = ProviderDriverKind.make("claudeAgent"); const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); interface CapturedTurn { readonly driver: ProviderDriverKind; readonly threadId: ThreadId; readonly providerThreadId: ProviderThreadId; readonly text: string; } function unimplemented(driver: ProviderDriverKind, detail: string) { return Effect.fail(new ProviderAdapterProtocolError({ driver, detail })); } function makeTestAdapter(input: { readonly instanceId: ProviderInstanceId; readonly driver: ProviderDriverKind; readonly capabilities: OrchestrationV2ProviderCapabilities; readonly modelSelection: ModelSelection; readonly responseByRunOrdinal: Readonly<Record<number, string>>; readonly responseByThreadId?: Readonly<Record<string, Readonly<Record<number, string>>>>; readonly capturedTurns: Ref.Ref<ReadonlyArray<CapturedTurn>>; readonly failResume?: boolean; readonly failedRunOrdinals?: ReadonlySet<number>; }): ProviderAdapterV2Shape { return { instanceId: input.instanceId, driver: input.driver, getCapabilities: () => Effect.succeed(input.capabilities), planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }), openSession: (sessionInput) => Effect.gen(function* () { const events = yield* PubSub.unbounded<ProviderAdapterV2Event>(); const now = yield* DateTime.now; const providerSession: OrchestrationV2ProviderSession = { id: sessionInput.providerSessionId, driver: input.driver, providerInstanceId: input.instanceId, status: "ready", cwd: sessionInput.runtimePolicy.cwd ?? process.cwd(), model: input.modelSelection.model, capabilities: input.capabilities, createdAt: now, updatedAt: now, lastError: null, }; return { instanceId: input.instanceId, driver: input.driver, providerSessionId: sessionInput.providerSessionId, providerSession, events: Stream.fromPubSub(events), ensureThread: (threadInput) => Effect.gen(function* () { const createdAt = yield* DateTime.now; const nativeThreadId = `${input.driver}:${threadInput.threadId}`; return { id: ProviderThreadId.make(`provider-thread:${nativeThreadId}`), driver: input.driver, providerInstanceId: input.instanceId, providerSessionId: sessionInput.providerSessionId, appThreadId: threadInput.threadId, ownerNodeId: null, nativeThreadRef: { driver: input.driver, nativeId: nativeThreadId, strength: "strong", }, nativeConversationHeadRef: null, status: "idle", firstRunOrdinal: null, lastRunOrdinal: null, handoffIds: [], forkedFrom: null, createdAt, updatedAt: createdAt, } satisfies OrchestrationV2ProviderThread; }), resumeThread: ({ providerThread }) => input.failResume ? unimplemented(input.driver, "simulated native resume failure") : Effect.succeed(providerThread), startTurn: (turnInput) => Effect.gen(function* () { yield* Effect.yieldNow; yield* Ref.update(input.capturedTurns, (turns) => [ ...turns, { driver: input.driver, threadId: turnInput.threadId, providerThreadId: turnInput.providerThread.id, text: turnInput.message.text, }, ]); const eventTime = yield* DateTime.now; const providerTurnId = ProviderTurnId.make( `provider-turn:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}`, ); if (input.failedRunOrdinals?.has(turnInput.runOrdinal) === true) { yield* PubSub.publish(events, { type: "provider_turn.updated", driver: input.driver, providerTurn: { id: providerTurnId, providerThreadId: turnInput.providerThread.id, nodeId: turnInput.rootNodeId, runAttemptId: turnInput.attemptId, nativeTurnRef: { driver: input.driver, nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, strength: "strong", }, ordinal: turnInput.runOrdinal, status: "failed", startedAt: eventTime, completedAt: eventTime, }, }); yield* PubSub.publish(events, { type: "turn.terminal", driver: input.driver, providerThreadId: turnInput.providerThread.id, providerTurnId, runOrdinal: turnInput.runOrdinal, failureItemOrdinal: turnInput.runOrdinal * 100 + 1, status: "failed", failure: makeProviderFailure({ message: "Simulated provider failure.", code: "simulated_failure", class: "provider_error", }), threadDisposition: "reusable", }); return; } const response = input.responseByThreadId?.[turnInput.threadId]?.[turnInput.runOrdinal] ?? input.responseByRunOrdinal[turnInput.runOrdinal] ?? `${input.driver} response for run ${turnInput.runOrdinal}`; const providerEvents: ReadonlyArray<ProviderAdapterV2Event> = [ { type: "provider_turn.updated", driver: input.driver, providerTurn: { id: providerTurnId, providerThreadId: turnInput.providerThread.id, nodeId: turnInput.rootNodeId, runAttemptId: turnInput.attemptId, nativeTurnRef: { driver: input.driver, nativeId: `native-turn:${turnInput.threadId}:${turnInput.runOrdinal}`, strength: "strong", }, ordinal: turnInput.runOrdinal, status: "completed", startedAt: eventTime, completedAt: eventTime, }, }, { type: "turn_item.updated", driver: input.driver, turnItem: { id: TurnItemId.make( `turn-item:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, ), threadId: turnInput.threadId, runId: turnInput.runId, nodeId: turnInput.rootNodeId, providerThreadId: turnInput.providerThread.id, providerTurnId, nativeItemRef: null, parentItemId: null, ordinal: turnInput.runOrdinal * 100 + 1, status: "completed", title: null, startedAt: eventTime, completedAt: eventTime, updatedAt: eventTime, type: "assistant_message", messageId: MessageId.make( `message:${input.driver}:${turnInput.threadId}:${turnInput.runOrdinal}:assistant`, ), text: response, streaming: false, }, }, { type: "turn.terminal", driver: input.driver, providerThreadId: turnInput.providerThread.id, providerTurnId, runOrdinal: turnInput.runOrdinal, status: "completed", failure: null, threadDisposition: "reusable", }, ]; for (const event of providerEvents) { yield* PubSub.publish(events, event); } }), steerTurn: () => Effect.void, interruptTurn: () => Effect.void, respondToRuntimeRequest: () => Effect.void, readThreadSnapshot: () => unimplemented(input.driver, "readThreadSnapshot unused in provider switch test"), rollbackThread: () => unimplemented(input.driver, "rollbackThread unused in provider switch test"), forkThread: () => unimplemented(input.driver, "forkThread unused in provider switch test"), }; }), }; } const waitForIdle = Effect.fn("ProviderSwitchTest.waitForIdle")(function* ( targetThreadId: ThreadId, ) { const orchestrator = yield* OrchestratorV2; for (let attempt = 0; attempt < 1_000; attempt += 1) { const projection = yield* orchestrator.getThreadProjection(targetThreadId); if ( projection.runs.every( (run) => !["queued", "starting", "running", "waiting"].includes(run.status), ) ) { return projection; } yield* Effect.sleep("5 millis"); } return yield* Effect.die(new Error("Provider switch test timed out waiting for idle")); }); describe("orchestration v2 provider switching", () => { it.live("reissues imported v1 context when switching after the first provider fails", () => Effect.scoped( Effect.gen(function* () { const importedThreadId = ThreadId.make("thread:provider-switch:legacy-import"); const importedProjectId = ProjectId.make("project:provider-switch:legacy-import"); const failedPrompt = "This first provider attempt should fail."; const recoveryPrompt = "What was the imported release marker?"; const cwd = yield* checkpointWorkspace("provider-switch-legacy-import"); const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]); const registryLayer = makeProviderAdapterRegistryLayer([ makeTestAdapter({ instanceId: ProviderInstanceId.make("codex"), driver: CODEX_DRIVER, capabilities: CodexProviderCapabilitiesV2, modelSelection: CODEX_MODEL_SELECTION, responseByRunOrdinal: {}, capturedTurns, failedRunOrdinals: new Set([1]), }), makeTestAdapter({ instanceId: ProviderInstanceId.make("claudeAgent"), driver: CLAUDE_DRIVER, capabilities: ClaudeProviderCapabilitiesV2, modelSelection: CLAUDE_MODEL_SELECTION, responseByRunOrdinal: { 2: "The imported release marker is violet." }, capturedTurns, }), ]); const databaseLayer = SqlitePersistenceMemory; const eventStoreProvided = eventStoreLayer.pipe(Layer.provideMerge(databaseLayer)); const projectionStoreProvided = projectionStoreLayer.pipe( Layer.provideMerge(databaseLayer), ); const storesProvided = Layer.mergeAll( databaseLayer, eventStoreProvided, projectionStoreProvided, ); const eventSinkProvided = eventSinkLayer.pipe(Layer.provide(storesProvided)); const importerProvided = legacyV1ThreadImporterLayer.pipe( Layer.provide(Layer.mergeAll(storesProvided, eventSinkProvided)), ); const maintenanceProvided = projectionMaintenanceLayer.pipe(Layer.provide(storesProvided)); const orchestratorProvided = makeOrchestratorV2ReplayLayerWithRegistry( { name: "provider-switch-legacy-import", runtimePolicyOverride: { cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false, }, }, }, registryLayer, { databaseLayer }, ); const testLayer = Layer.mergeAll( storesProvided, importerProvided, maintenanceProvided, orchestratorProvided, ); const projection = yield* Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const importer = yield* LegacyV1ThreadImporter; const maintenance = yield* ProjectionMaintenanceV2; const orchestrator = yield* OrchestratorV2; yield* sql` INSERT INTO projection_projects ( project_id, title, workspace_root, default_model_selection_json, scripts_json, created_at, updated_at, deleted_at ) VALUES ( ${importedProjectId}, 'Imported provider switch project', ${cwd}, '{"instanceId":"codex","model":"gpt-5.4"}', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL ) `; yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, branch, worktree_path, latest_turn_id, created_at, updated_at, archived_at, settled_override, settled_at, deleted_at ) VALUES ( ${importedThreadId}, ${importedProjectId}, 'Imported provider switch thread', '{"instanceId":"codex","model":"gpt-5.4"}', 'full-access', 'default', 'main', ${cwd}, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, NULL, NULL, NULL ) `; yield* sql` INSERT INTO projection_thread_messages ( message_id, thread_id, turn_id, role, text, attachments_json, is_streaming, created_at, updated_at ) VALUES ( 'message:provider-switch:legacy-import:user', ${importedThreadId}, NULL, 'user', 'Remember that the imported release marker is violet.', '[]', 0, '2026-01-01T01:00:00.000Z', '2026-01-01T01:00:00.000Z' ), ( 'message:provider-switch:legacy-import:assistant', ${importedThreadId}, NULL, 'assistant', 'I will remember violet.', '[]', 0, '2026-01-01T01:01:00.000Z', '2026-01-01T01:01:00.000Z' ) `; yield* importer.reconcileShells; yield* maintenance.rebuild; yield* importer.ensureTranscript(importedThreadId); yield* orchestrator.dispatch({ type: "message.dispatch", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:provider-switch:legacy-import:failed"), threadId: importedThreadId, messageId: MessageId.make("message:provider-switch:legacy-import:failed"), text: failedPrompt, attachments: [], modelSelection: CODEX_MODEL_SELECTION, dispatchMode: { type: "start_immediately" }, }); yield* waitForIdle(importedThreadId); yield* orchestrator.dispatch({ type: "message.dispatch", createdBy: "user", creationSource: "web", commandId: CommandId.make("command:provider-switch:legacy-import:recovery"), threadId: importedThreadId, messageId: MessageId.make("message:provider-switch:legacy-import:recovery"), text: recoveryPrompt, attachments: [], modelSelection: CLAUDE_MODEL_SELECTION, dispatchMode: { type: "start_immediately" }, }); return yield* waitForIdle(importedThreadId); }).pipe(Effect.provide(testLayer)); const turns = yield* Ref.get(capturedTurns); assert.deepEqual( projection.runs.map((run) => [run.providerInstanceId, run.status]), [ ["codex", "failed"], ["claudeAgent", "completed"],