import { assert, it } from "@effect/vitest"; import { type ModelSelection, NodeId, type OrchestrationV2ProviderThread, type OrchestrationV2ThreadProjection, ProjectId, ProviderDriverKind, ProviderInstanceId, ProviderSessionId, ProviderThreadId, ProviderTurnId, RunAttemptId, RunId, ThreadId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; import { ProjectionStoreV2 } from "./ProjectionStore.ts"; import type { ProviderAdapterV2SessionRuntime } from "./ProviderAdapter.ts"; import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; import { layer as providerTurnControlLayer, ProviderTurnControlServiceV2, } from "./ProviderTurnControlService.ts"; const driver = ProviderDriverKind.make("codex"); const providerInstanceId = ProviderInstanceId.make("codex"); const modelSelection = { instanceId: providerInstanceId, model: "gpt-5.4", } satisfies ModelSelection; function makeProjection(input: { readonly now: DateTime.Utc; readonly threadId: ThreadId; readonly providerThread: OrchestrationV2ProviderThread; readonly providerTurnId: ProviderTurnId; readonly attemptId: RunAttemptId; }): OrchestrationV2ThreadProjection { const runId = RunId.make("run:restart-session"); const nodeId = NodeId.make("node:restart-session"); return { thread: { createdBy: "user", creationSource: "web", id: input.threadId, projectId: ProjectId.make("project:restart-session"), title: "Restart session", providerInstanceId, modelSelection, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: "/workspace", activeProviderThreadId: input.providerThread.id, lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: input.threadId, }, forkedFrom: null, createdAt: input.now, updatedAt: input.now, archivedAt: null, settledOverride: null, settledAt: null, lastVisitedAt: null, deletedAt: null, }, runs: [], attempts: [ { id: input.attemptId, runId, attemptOrdinal: 1, rootNodeId: nodeId, providerInstanceId, providerThreadId: input.providerThread.id, providerTurnId: input.providerTurnId, reason: "initial", status: "superseded", startedAt: input.now, completedAt: input.now, }, ], nodes: [], subagents: [], providerSessions: [], providerThreads: [input.providerThread], providerTurns: [ { id: input.providerTurnId, providerThreadId: input.providerThread.id, nodeId, runAttemptId: input.attemptId, nativeTurnRef: { driver, nativeId: "native-turn:restart-session", strength: "strong", }, ordinal: 1, status: "running", startedAt: input.now, completedAt: null, }, ], runtimeRequests: [], messages: [], plans: [], turnItems: [], checkpointScopes: [], checkpoints: [], contextHandoffs: [], contextTransfers: [], visibleTurnItems: [], updatedAt: input.now, }; } it.effect( "interrupts the historical session only for the exact committed restart replacement", () => Effect.gen(function* () { const now = yield* DateTime.now; const threadId = ThreadId.make("thread:restart-session"); const oldSessionId = ProviderSessionId.make("provider-session:restart-session:old"); const replacementSessionId = ProviderSessionId.make( "provider-session:restart-session:replacement", ); const unrelatedSessionId = ProviderSessionId.make( "provider-session:restart-session:unrelated", ); const providerThreadId = ProviderThreadId.make("provider-thread:restart-session"); const providerTurnId = ProviderTurnId.make("provider-turn:restart-session"); const attemptId = RunAttemptId.make("run-attempt:restart-session"); const providerThread: OrchestrationV2ProviderThread = { id: providerThreadId, driver, providerInstanceId, // The restart command has already projected this replacement binding // before the process-bound restart effect executes. providerSessionId: replacementSessionId, appThreadId: threadId, ownerNodeId: null, nativeThreadRef: { driver, nativeId: "native-thread:restart-session", strength: "strong", }, nativeConversationHeadRef: null, status: "not_loaded", firstRunOrdinal: 1, lastRunOrdinal: 1, handoffIds: [], forkedFrom: null, createdAt: now, updatedAt: now, }; const projection = yield* Ref.make( makeProjection({ now, threadId, providerThread, providerTurnId, attemptId }), ); const interruptedThread = yield* Ref.make(null); const providerSession = { id: oldSessionId, driver, providerInstanceId, status: "running" as const, cwd: "/workspace", model: modelSelection.model, capabilities: CodexProviderCapabilitiesV2, createdAt: now, updatedAt: now, lastError: null, }; const runtime: ProviderAdapterV2SessionRuntime = { instanceId: providerInstanceId, driver, providerSessionId: oldSessionId, providerSession, events: Stream.empty, ensureThread: () => Effect.die("unused ensureThread"), resumeThread: () => Effect.die("unused resumeThread"), startTurn: () => Effect.die("unused startTurn"), steerTurn: () => Effect.die("unused steerTurn"), interruptTurn: ({ providerThread: target }) => Effect.all( [ Ref.set(interruptedThread, target), Ref.update(projection, (current) => ({ ...current, providerTurns: current.providerTurns.map((turn) => turn.id === providerTurnId ? { ...turn, status: "interrupted" as const, completedAt: now } : turn, ), })), ], { discard: true }, ), respondToRuntimeRequest: () => Effect.die("unused respondToRuntimeRequest"), readThreadSnapshot: () => Effect.die("unused readThreadSnapshot"), rollbackThread: () => Effect.die("unused rollbackThread"), forkThread: () => Effect.die("unused forkThread"), }; const projectionLayer = Layer.succeed( ProjectionStoreV2, ProjectionStoreV2.of({ apply: () => Effect.void, getShellSnapshot: () => Effect.die("unused getShellSnapshot"), getThreadShell: () => Effect.die("unused getThreadShell"), getThread: () => Ref.get(projection).pipe(Effect.map((state) => state.thread)), getSettlementCandidates: () => Effect.die("unused getSettlementCandidates"), getThreadProjection: () => Effect.die("control effects must not load transcript"), getRuntimeRequest: () => Effect.die("unused getRuntimeRequest"), getPendingNativeUserInputs: () => Effect.die("unused getPendingNativeUserInputs"), getProviderControlContext: (_threadId, target) => Ref.get(projection).pipe( Effect.map((current) => ({ providerThread: current.providerThreads.find( (thread) => thread.id === target.providerThreadId, ), providerTurn: current.providerTurns.find( (turn) => turn.id === target.providerTurnId, ), attempt: current.attempts.find((attempt) => attempt.id === target.attemptId), message: undefined, run: undefined, })), ), getCheckpointContext: () => Effect.die("not used"), getRecoveryThreadIds: () => Effect.die("unused getRecoveryThreadIds"), getUnreadableThreadIds: () => Effect.die("unused getUnreadableThreadIds"), getThreadSnapshot: () => Effect.die("unused getThreadSnapshot"), getThreadSnapshotWindow: () => Effect.die("unused getThreadSnapshotWindow"), }), ); const sessionManagerLayer = Layer.succeed( ProviderSessionManagerV2, ProviderSessionManagerV2.of({ shutdown: Effect.void, open: () => Effect.die("unused open"), get: (providerSessionId) => Effect.succeed( providerSessionId === oldSessionId ? Option.some(runtime) : Option.none(), ), close: () => Effect.void, release: () => Effect.void, detach: () => Effect.void, }), ); const controlLayer = providerTurnControlLayer.pipe( Layer.provide(Layer.merge(projectionLayer, sessionManagerLayer)), ); const [ordinaryInterrupt, unrelatedRestart] = yield* Effect.gen(function* () { const control = yield* ProviderTurnControlServiceV2; const ordinary = yield* Effect.exit( control.interrupt({ threadId, providerSessionId: oldSessionId, providerThreadId, providerTurnId, }), ); const unrelated = yield* Effect.exit( control.interruptAndAwaitTerminal({ threadId, providerSessionId: oldSessionId, replacementProviderSessionId: unrelatedSessionId, providerThreadId, providerTurnId, interruptedAttemptId: attemptId, }), ); return [ordinary, unrelated] as const; }).pipe(Effect.provide(controlLayer)); assert.isTrue(Exit.isFailure(ordinaryInterrupt)); assert.isTrue(Exit.isFailure(unrelatedRestart)); assert.isNull(yield* Ref.get(interruptedThread)); yield* Effect.gen(function* () { const control = yield* ProviderTurnControlServiceV2; yield* control.interruptAndAwaitTerminal({ threadId, providerSessionId: oldSessionId, replacementProviderSessionId: replacementSessionId, providerThreadId, providerTurnId, interruptedAttemptId: attemptId, }); }).pipe(Effect.provide(controlLayer)); const interrupted = yield* Ref.get(interruptedThread); assert.isNotNull(interrupted); assert.equal(interrupted?.providerSessionId, oldSessionId); assert.equal(interrupted?.id, providerThreadId); assert.equal(interrupted?.nativeThreadRef?.nativeId, "native-thread:restart-session"); }), );