diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx
index bdddf2c4595..6da64509a15 100644
--- a/apps/mobile/src/components/ProviderIcon.tsx
+++ b/apps/mobile/src/components/ProviderIcon.tsx
@@ -1,6 +1,8 @@
import { useColorScheme } from "react-native";
import { Path, Svg } from "react-native-svg";
+import { providerIconKind, providerIconPalette } from "./providerIconKind";
+
type ProviderIconProps = {
readonly provider: string | null | undefined;
readonly size?: number;
@@ -10,8 +12,9 @@ export function ProviderIcon(props: ProviderIconProps) {
const isDarkMode = useColorScheme() === "dark";
const size = props.size ?? 16;
const mono = isDarkMode ? "#e5e5e5" : "#171717";
+ const iconKind = providerIconKind(props.provider);
- if (props.provider === "claudeAgent") {
+ if (iconKind === "claude") {
return (
);
}
diff --git a/apps/mobile/src/components/providerIconKind.test.ts b/apps/mobile/src/components/providerIconKind.test.ts
new file mode 100644
index 00000000000..53e4c9533ab
--- /dev/null
+++ b/apps/mobile/src/components/providerIconKind.test.ts
@@ -0,0 +1,25 @@
+import { assert, describe, it } from "@effect/vitest";
+
+import { providerIconKind, providerIconPalette } from "./providerIconKind";
+
+describe("providerIconKind", () => {
+ it("keeps both OpenCode generations distinct", () => {
+ assert.strictEqual(providerIconKind("opencode"), "opencode");
+ assert.strictEqual(providerIconKind("opencode2"), "opencode2");
+ });
+
+ it("preserves existing provider fallbacks", () => {
+ assert.strictEqual(providerIconKind("claudeAgent"), "claude");
+ assert.strictEqual(providerIconKind("codex"), "openai");
+ assert.strictEqual(providerIconKind(undefined), "openai");
+ });
+});
+
+describe("providerIconPalette", () => {
+ it("keeps both OpenCode outer frames contrasted with the application theme", () => {
+ assert.strictEqual(providerIconPalette("opencode", false), "light");
+ assert.strictEqual(providerIconPalette("opencode", true), "dark");
+ assert.strictEqual(providerIconPalette("opencode2", false), "light");
+ assert.strictEqual(providerIconPalette("opencode2", true), "dark");
+ });
+});
diff --git a/apps/mobile/src/components/providerIconKind.ts b/apps/mobile/src/components/providerIconKind.ts
new file mode 100644
index 00000000000..b5435b68c17
--- /dev/null
+++ b/apps/mobile/src/components/providerIconKind.ts
@@ -0,0 +1,21 @@
+export type ProviderIconKind = "claude" | "opencode" | "opencode2" | "openai";
+
+export function providerIconKind(provider: string | null | undefined): ProviderIconKind {
+ switch (provider) {
+ case "claudeAgent":
+ return "claude";
+ case "opencode":
+ return "opencode";
+ case "opencode2":
+ return "opencode2";
+ default:
+ return "openai";
+ }
+}
+
+export function providerIconPalette(
+ _kind: ProviderIconKind,
+ isDarkMode: boolean,
+): "light" | "dark" {
+ return isDarkMode ? "dark" : "light";
+}
diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx
index 8bcdd80451b..2ec83bb20e4 100644
--- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx
+++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx
@@ -8,7 +8,7 @@ import type { PendingUserInput, PendingUserInputDraftAnswer } from "../../lib/th
export interface PendingUserInputCardProps {
readonly pendingUserInput: PendingUserInput;
readonly drafts: Record;
- readonly answers: Record | null;
+ readonly answers: Record | null;
readonly respondingUserInputId: RuntimeRequestId | null;
readonly onSelectOption: (requestId: RuntimeRequestId, questionId: string, label: string) => void;
readonly onChangeCustomAnswer: (
@@ -48,7 +48,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
{question.options.map((option) => {
const selected =
- draft?.selectedOptionLabel === option.label && !draft.customAnswer?.trim().length;
+ draft?.selectedOptionLabels?.includes(option.label) === true &&
+ !draft.customAnswer?.trim().length;
return (
{
+ it("keeps Send primary and exposes Stop beside a settled draft", () => {
+ expect(
+ collapsedComposerActions({
+ canStopThread: true,
+ hasContent: true,
+ activeThreadBusy: false,
+ }),
+ ).toEqual({ showStopPrimary: false, showStopSecondary: true });
+ });
+
+ it("uses Stop as the primary action when the draft is empty or busy", () => {
+ expect(
+ collapsedComposerActions({
+ canStopThread: true,
+ hasContent: false,
+ activeThreadBusy: false,
+ }),
+ ).toEqual({ showStopPrimary: true, showStopSecondary: false });
+ expect(
+ collapsedComposerActions({
+ canStopThread: true,
+ hasContent: true,
+ activeThreadBusy: true,
+ }),
+ ).toEqual({ showStopPrimary: true, showStopSecondary: false });
+ });
+});
diff --git a/apps/mobile/src/features/threads/ThreadComposer.logic.ts b/apps/mobile/src/features/threads/ThreadComposer.logic.ts
new file mode 100644
index 00000000000..356f8e3e9e3
--- /dev/null
+++ b/apps/mobile/src/features/threads/ThreadComposer.logic.ts
@@ -0,0 +1,20 @@
+export interface CollapsedComposerActionsInput {
+ readonly canStopThread: boolean;
+ readonly hasContent: boolean;
+ readonly activeThreadBusy: boolean;
+}
+
+export interface CollapsedComposerActions {
+ readonly showStopPrimary: boolean;
+ readonly showStopSecondary: boolean;
+}
+
+export function collapsedComposerActions(
+ input: CollapsedComposerActionsInput,
+): CollapsedComposerActions {
+ const showStopPrimary = input.canStopThread && (!input.hasContent || input.activeThreadBusy);
+ return {
+ showStopPrimary,
+ showStopSecondary: input.canStopThread && input.hasContent && !input.activeThreadBusy,
+ };
+}
diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx
index 7c5575a5d77..d961fef4b1b 100644
--- a/apps/mobile/src/features/threads/ThreadComposer.tsx
+++ b/apps/mobile/src/features/threads/ThreadComposer.tsx
@@ -69,6 +69,7 @@ import {
} from "../../lib/providerOptions";
import { useComposerPathSearch } from "../../state/use-composer-path-search";
import { ComposerCommandPopover, type ComposerCommandItem } from "./ComposerCommandPopover";
+import { collapsedComposerActions } from "./ThreadComposer.logic";
/**
* Height of the collapsed composer (pill + vertical padding, excluding safe-area inset).
@@ -306,7 +307,12 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
setIsFocused(false);
onExpandedChange?.(false);
}, [onExpandedChange]);
- const showStopAction = props.canStopThread;
+ const { showStopPrimary: showStopPrimaryAction, showStopSecondary: showStopSecondaryAction } =
+ collapsedComposerActions({
+ canStopThread: props.canStopThread,
+ hasContent,
+ activeThreadBusy: props.activeThreadBusy,
+ });
const sendLabel =
props.connectionState !== "connected" || props.activeThreadBusy || props.queueCount > 0
@@ -690,6 +696,33 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
}
}
+ let collapsedPrimaryAction: ReactNode = (
+
+ );
+ if (showStopSecondaryAction) {
+ collapsedPrimaryAction = (
+
+
+
+
+ );
+ } else if (showStopPrimaryAction) {
+ collapsedPrimaryAction = (
+
+ );
+ }
+
return (
- {showStopAction ? (
-
- ) : (
-
- )}
+ {collapsedPrimaryAction}
) : null}
@@ -870,7 +894,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
label={configurationLabel}
/>
- {showStopAction ? (
+ {props.canStopThread && (
- ) : null}
+ )}
;
- readonly activePendingUserInputAnswers: Record | null;
+ readonly activePendingUserInputAnswers: Record | null;
readonly respondingUserInputId: RuntimeRequestId | null;
readonly draftMessage: string;
readonly draftAttachments: ReadonlyArray;
diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx
index 713f4b3acee..ecadf406fe6 100644
--- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx
+++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx
@@ -461,17 +461,22 @@ function ThreadRouteContent(
void navigation.navigate("Connections");
}, [navigation]);
const handleStopThread = useCallback(() => {
- if (!selectedThread || composer.interruptibleRunId === null) {
+ if (!selectedThread || !composer.canInterruptThread) {
return;
}
return interruptThreadTurn({
environmentId: selectedThread.environmentId,
input: {
threadId: selectedThread.id,
- runId: composer.interruptibleRunId,
+ ...(composer.interruptibleRunId === null ? {} : { runId: composer.interruptibleRunId }),
},
});
- }, [composer.interruptibleRunId, interruptThreadTurn, selectedThread]);
+ }, [
+ composer.canInterruptThread,
+ composer.interruptibleRunId,
+ interruptThreadTurn,
+ selectedThread,
+ ]);
const handleOpenTerminal = useCallback(
(nextTerminalId?: string | null) => {
@@ -762,7 +767,7 @@ function ThreadRouteContent(
connectionStateLabel={routeConnectionState}
threadSyncStatus={selectedThreadDetailState.status}
activeThreadBusy={composer.activeThreadBusy}
- canStopThread={composer.interruptibleRunId !== null}
+ canStopThread={composer.canInterruptThread}
environmentId={selectedThread.environmentId}
projectWorkspaceRoot={selectedThreadProject?.workspaceRoot ?? null}
threadCwd={selectedThreadCwd}
diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts
index 38e15e46197..57a3ce52819 100644
--- a/apps/mobile/src/lib/threadActivity.test.ts
+++ b/apps/mobile/src/lib/threadActivity.test.ts
@@ -10,9 +10,12 @@ import * as DateTime from "effect/DateTime";
import { describe, expect, it } from "vite-plus/test";
import {
+ buildPendingUserInputAnswers,
buildThreadFeed,
deriveThreadFeedPresentation,
+ setPendingUserInputCustomAnswer,
threadFeedRunIsUnsettled,
+ togglePendingUserInputOptionSelection,
type ThreadFeedActivity,
type ThreadFeedEntry,
} from "./threadActivity";
@@ -21,6 +24,53 @@ const threadId = ThreadId.make("thread-1");
const sourceThreadId = ThreadId.make("thread-source");
const runId = RunId.make("run-1");
+const multiSelectQuestion = {
+ id: "areas",
+ header: "Areas",
+ question: "Which areas should this change cover?",
+ options: [
+ { label: "Server", description: "Server" },
+ { label: "Mobile", description: "Mobile" },
+ ],
+ multiSelect: true,
+} as const;
+
+describe("pending user input", () => {
+ it("toggles and submits multiple selected options", () => {
+ const first = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Server");
+ const second = togglePendingUserInputOptionSelection(multiSelectQuestion, first, "Mobile");
+
+ expect(buildPendingUserInputAnswers([multiSelectQuestion], { areas: second })).toEqual({
+ areas: ["Server", "Mobile"],
+ });
+ expect(togglePendingUserInputOptionSelection(multiSelectQuestion, second, "Server")).toEqual({
+ customAnswer: "",
+ selectedOptionLabels: ["Mobile"],
+ });
+ });
+
+ it("normalizes option labels before toggling a selected value", () => {
+ const selected = togglePendingUserInputOptionSelection(
+ multiSelectQuestion,
+ undefined,
+ " Server ",
+ );
+
+ expect(
+ togglePendingUserInputOptionSelection(multiSelectQuestion, selected, " Server "),
+ ).toEqual({ customAnswer: "" });
+ });
+
+ it("clears selected options while a custom answer is active", () => {
+ expect(
+ setPendingUserInputCustomAnswer(
+ { selectedOptionLabels: ["Server", "Mobile"] },
+ "No preference",
+ ),
+ ).toEqual({ customAnswer: "No preference" });
+ });
+});
+
function base(id: string, updatedAt: string, ordinal: number) {
const timestamp = DateTime.makeUnsafe(updatedAt);
return {
diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts
index 6742b60bf06..203b7bf9cd7 100644
--- a/apps/mobile/src/lib/threadActivity.ts
+++ b/apps/mobile/src/lib/threadActivity.ts
@@ -30,7 +30,7 @@ export type PendingUserInput = ThreadPendingUserInput;
const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;
export interface PendingUserInputDraftAnswer {
- readonly selectedOptionLabel?: string;
+ readonly selectedOptionLabels?: ReadonlyArray;
readonly customAnswer?: string;
}
@@ -141,12 +141,25 @@ function normalizeDraftAnswer(value: string | undefined): string | null {
return trimmed.length > 0 ? trimmed : null;
}
+function normalizeSelectedOptionLabels(value: ReadonlyArray | undefined): string[] {
+ if (!Array.isArray(value)) return [];
+ return Array.from(
+ new Set(value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)),
+ );
+}
+
function resolvePendingUserInputAnswer(
+ question: ThreadUserInputQuestion,
draft: PendingUserInputDraftAnswer | undefined,
-): string | null {
- return (
- normalizeDraftAnswer(draft?.customAnswer) ?? normalizeDraftAnswer(draft?.selectedOptionLabel)
- );
+): string | string[] | null {
+ const customAnswer = normalizeDraftAnswer(draft?.customAnswer);
+ if (customAnswer) return customAnswer;
+
+ const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
+ if (question.multiSelect) {
+ return selectedOptionLabels.length > 0 ? selectedOptionLabels : null;
+ }
+ return selectedOptionLabels[0] ?? null;
}
function capitalizePhrase(value: string): string {
@@ -655,18 +668,45 @@ export function setPendingUserInputCustomAnswer(
draft: PendingUserInputDraftAnswer | undefined,
customAnswer: string,
): PendingUserInputDraftAnswer {
- const selectedOptionLabel =
- customAnswer.trim().length > 0 ? undefined : draft?.selectedOptionLabel;
- return { customAnswer, ...(selectedOptionLabel ? { selectedOptionLabel } : {}) };
+ const selectedOptionLabels =
+ customAnswer.trim().length > 0
+ ? undefined
+ : normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
+ return {
+ customAnswer,
+ ...(selectedOptionLabels && selectedOptionLabels.length > 0 ? { selectedOptionLabels } : {}),
+ };
+}
+
+export function togglePendingUserInputOptionSelection(
+ question: ThreadUserInputQuestion,
+ draft: PendingUserInputDraftAnswer | undefined,
+ optionLabel: string,
+): PendingUserInputDraftAnswer {
+ const normalizedOptionLabel = optionLabel.trim();
+ if (question.multiSelect) {
+ const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels);
+ const nextSelectedOptionLabels = selectedOptionLabels.includes(normalizedOptionLabel)
+ ? selectedOptionLabels.filter((label) => label !== normalizedOptionLabel)
+ : [...selectedOptionLabels, normalizedOptionLabel];
+ return {
+ customAnswer: "",
+ ...(nextSelectedOptionLabels.length > 0
+ ? { selectedOptionLabels: nextSelectedOptionLabels }
+ : {}),
+ };
+ }
+
+ return { customAnswer: "", selectedOptionLabels: [normalizedOptionLabel] };
}
export function buildPendingUserInputAnswers(
questions: ReadonlyArray,
draftAnswers: Record,
-): Record | null {
- const answers: Record = {};
+): Record | null {
+ const answers: Record = {};
for (const question of questions) {
- const answer = resolvePendingUserInputAnswer(draftAnswers[question.id]);
+ const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]);
if (!answer) return null;
answers[question.id] = answer;
}
diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts
index d15646a24f4..6bcf696b7b0 100644
--- a/apps/mobile/src/state/use-selected-thread-requests.ts
+++ b/apps/mobile/src/state/use-selected-thread-requests.ts
@@ -10,6 +10,7 @@ import { scopedRequestKey } from "../lib/scopedEntities";
import {
buildPendingUserInputAnswers,
setPendingUserInputCustomAnswer,
+ togglePendingUserInputOptionSelection,
type PendingUserInputDraftAnswer,
} from "../lib/threadActivity";
import { appAtomRegistry } from "./atom-registry";
@@ -21,15 +22,21 @@ const userInputDraftsByRequestKeyAtom = Atom.make<
Record>
>({}).pipe(Atom.keepAlive, Atom.withLabel("mobile:user-input-drafts"));
-function setUserInputDraftOption(requestKey: string, questionId: string, label: string): void {
+function setUserInputDraftOption(
+ requestKey: string,
+ question: Parameters[0],
+ label: string,
+): void {
const current = appAtomRegistry.get(userInputDraftsByRequestKeyAtom);
appAtomRegistry.set(userInputDraftsByRequestKeyAtom, {
...current,
[requestKey]: {
...current[requestKey],
- [questionId]: {
- selectedOptionLabel: label,
- },
+ [question.id]: togglePendingUserInputOptionSelection(
+ question,
+ current[requestKey]?.[question.id],
+ label,
+ ),
},
});
}
@@ -94,10 +101,12 @@ export function useSelectedThreadRequests() {
return;
}
+ const question = activePendingUserInput?.questions.find((item) => item.id === questionId);
+ if (!question || activePendingUserInput.requestId !== requestId) return;
const requestKey = scopedRequestKey(selectedThreadShell.environmentId, requestId);
- setUserInputDraftOption(requestKey, questionId, label);
+ setUserInputDraftOption(requestKey, question, label);
},
- [selectedThreadShell],
+ [activePendingUserInput, selectedThreadShell],
);
const onChangeUserInputCustomAnswer = useCallback(
diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts
index 08718df03f1..b3695d1d6d4 100644
--- a/apps/mobile/src/state/use-thread-composer-state.ts
+++ b/apps/mobile/src/state/use-thread-composer-state.ts
@@ -3,6 +3,7 @@ import { threadRuntimeIsActive } from "@t3tools/client-runtime/state/shell";
import {
deriveThreadActivityRun,
deriveThreadRuntime,
+ threadRuntimeHasInterruptibleWork,
threadRuntimeHasInterruptibleRun,
} from "@t3tools/client-runtime/state/thread-execution";
import { useCallback, useEffect, useMemo } from "react";
@@ -154,6 +155,7 @@ export function useThreadComposerState() {
const interruptibleRunId = threadRuntimeHasInterruptibleRun(selectedThreadRuntime)
? (selectedThreadRuntime?.activeRunId ?? null)
: null;
+ const canInterruptThread = threadRuntimeHasInterruptibleWork(selectedThreadRuntime);
const onSendMessage = useCallback(async () => {
if (!selectedThreadShell) {
@@ -333,6 +335,7 @@ export function useThreadComposerState() {
interactionMode,
activeThreadBusy,
interruptibleRunId,
+ canInterruptThread,
onChangeDraftMessage,
onPickDraftImages,
onPasteIntoDraft,
diff --git a/apps/server/package.json b/apps/server/package.json
index 4613053aeca..928d273d9c6 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -35,6 +35,7 @@
"@effect/sql-sqlite-bun": "catalog:",
"@ff-labs/fff-node": "0.9.4",
"@opencode-ai/sdk": "^1.3.15",
+ "@opencode-ai/sdk-next": "npm:@opencode-ai/sdk@0.0.0-beta-202608061351",
"@pierre/diffs": "catalog:",
"effect": "catalog:",
"node-pty": "^1.1.0",
diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts
index d1fd1975003..2e518b0db31 100644
--- a/apps/server/src/mcp/OrchestratorMcpService.test.ts
+++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts
@@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest";
import {
EnvironmentId,
NodeId,
+ type OrchestrationV2Run,
ProviderInstanceId,
RunId,
ThreadId,
@@ -11,8 +12,12 @@ import {
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Ref from "effect/Ref";
+import { expect, it as test } from "vite-plus/test";
-import { ThreadManagementService } from "../orchestration-v2/ThreadManagementService.ts";
+import {
+ type ThreadManagementInterruptResult,
+ ThreadManagementService,
+} from "../orchestration-v2/ThreadManagementService.ts";
import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts";
import type { McpInvocationScope } from "./McpInvocationContext.ts";
@@ -290,3 +295,54 @@ describe("OrchestratorMcpService", () => {
}),
);
});
+
+test("maps provider-native interrupt requests to a run-less MCP result", () => {
+ const threadId = ThreadId.make("thread:mcp-provider-native-interrupt");
+ const result: ThreadManagementInterruptResult = {
+ type: "provider_interrupt_requested",
+ targets: [],
+ dispatch: {
+ sequence: 1,
+ storedEvents: [],
+ },
+ };
+
+ expect(OrchestratorMcpService.orchestratorMcpThreadInterruptResultFor(threadId, result)).toEqual({
+ threadId,
+ runId: null,
+ status: "interrupt_requested",
+ });
+});
+
+test("preserves the no-active-run MCP result when no provider work was found", () => {
+ const threadId = ThreadId.make("thread:mcp-no-active-run");
+
+ expect(
+ OrchestratorMcpService.orchestratorMcpThreadInterruptResultFor(threadId, {
+ type: "no_active_run",
+ }),
+ ).toEqual({
+ threadId,
+ runId: null,
+ status: "no_active_run",
+ });
+});
+
+test("preserves the run id for an ordinary waiting-root interrupt", () => {
+ const threadId = ThreadId.make("thread:mcp-waiting-root-interrupt");
+ const run = { id: RunId.make("run:mcp-waiting-root-interrupt") } as OrchestrationV2Run;
+ const result: ThreadManagementInterruptResult = {
+ type: "interrupt_requested",
+ run,
+ dispatch: {
+ sequence: 1,
+ storedEvents: [],
+ },
+ };
+
+ expect(OrchestratorMcpService.orchestratorMcpThreadInterruptResultFor(threadId, result)).toEqual({
+ threadId,
+ runId: run.id,
+ status: "interrupt_requested",
+ });
+});
diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts
index 7d7bd24f02d..f9a77acd9b1 100644
--- a/apps/server/src/mcp/OrchestratorMcpService.ts
+++ b/apps/server/src/mcp/OrchestratorMcpService.ts
@@ -67,6 +67,7 @@ import {
latestActiveRun,
latestRun,
ThreadManagementError,
+ type ThreadManagementInterruptResult,
ThreadManagementService,
} from "../orchestration-v2/ThreadManagementService.ts";
import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
@@ -176,6 +177,31 @@ function threadManagementFailure(error: ThreadManagementError): OrchestratorMcpF
}
}
+export function orchestratorMcpThreadInterruptResultFor(
+ threadId: ThreadId,
+ result: ThreadManagementInterruptResult,
+): OrchestratorMcpThreadInterruptResult {
+ if (result.type === "no_active_run") {
+ return {
+ threadId,
+ runId: null,
+ status: "no_active_run",
+ } satisfies OrchestratorMcpThreadInterruptResult;
+ }
+ if (result.type === "provider_interrupt_requested") {
+ return {
+ threadId,
+ runId: null,
+ status: "interrupt_requested",
+ } satisfies OrchestratorMcpThreadInterruptResult;
+ }
+ return {
+ threadId,
+ runId: result.run.id,
+ status: result.type === "already_terminal" ? result.run.status : "interrupt_requested",
+ } satisfies OrchestratorMcpThreadInterruptResult;
+}
+
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
@@ -1658,18 +1684,7 @@ const make = Effect.gen(function* () {
),
),
);
- if (result.type === "no_active_run") {
- return {
- threadId: input.threadId,
- runId: null,
- status: "no_active_run",
- } satisfies OrchestratorMcpThreadInterruptResult;
- }
- return {
- threadId: input.threadId,
- runId: result.run.id,
- status: result.type === "already_terminal" ? result.run.status : "interrupt_requested",
- } satisfies OrchestratorMcpThreadInterruptResult;
+ return orchestratorMcpThreadInterruptResultFor(input.threadId, result);
}),
});
});
diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.test.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.test.ts
index 9d7294e6356..c4483c2cbb0 100644
--- a/apps/server/src/mcp/toolkits/orchestrator/tools.test.ts
+++ b/apps/server/src/mcp/toolkits/orchestrator/tools.test.ts
@@ -1,7 +1,12 @@
import { assert, describe, it } from "@effect/vitest";
import { Tool } from "effect/unstable/ai";
-import { CreateThreadsTool, DelegateTaskTool, ScheduleTaskTool } from "./tools.ts";
+import {
+ CreateThreadsTool,
+ DelegateTaskTool,
+ ScheduleTaskTool,
+ ThreadInterruptTool,
+} from "./tools.ts";
describe("orchestrator MCP tool guidance", () => {
it("directs subagent requests to delegation instead of ordinary threads", () => {
@@ -25,4 +30,14 @@ describe("orchestrator MCP tool guidance", () => {
assert.include(ScheduleTaskTool.description ?? "", "STRUCTURED OBJECT");
assert.include(ScheduleTaskTool.description ?? "", "nextRunAt");
});
+
+ it("documents the no-runId root-run exclusion", () => {
+ assert.include(ThreadInterruptTool.description ?? "", "waiting");
+ assert.include(
+ ThreadInterruptTool.description ?? "",
+ "only when the thread shell reports provider-native background work",
+ );
+ assert.include(ThreadInterruptTool.description ?? "", "post-terminal-drain");
+ assert.include(ThreadInterruptTool.description ?? "", "no-runId selection");
+ });
});
diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts
index 9a453695ad3..58cdb9f417d 100644
--- a/apps/server/src/mcp/toolkits/orchestrator/tools.ts
+++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts
@@ -219,7 +219,7 @@ export const ThreadWaitTool = Tool.make("t3_thread_wait", {
export const ThreadInterruptTool = Tool.make("t3_thread_interrupt", {
description:
- "Request interruption of a running turn in a T3 thread in the calling project. Without runId, the newest interruptible run is selected. Terminal runs and threads without an active turn return without another side effect. clientRequestId makes retries idempotent.",
+ "Request interruption of a running turn in a T3 thread in the calling project. Pressing Stop from inside a provider-native child thread interrupts that child's own active provider turn. Without runId, the newest interruptible root run is selected; a waiting root run is excluded from this no-runId selection only when the thread shell reports provider-native background work, while post-terminal-drain root runs remain excluded. When no root run is interruptible, directly-owned provider-native background children are interrupted and the response has runId null. If no active run or resolvable background child remains, the no-op result has status no_active_run. Terminal runs return without another side effect. clientRequestId makes retries idempotent.",
parameters: OrchestratorMcpThreadInterruptInput,
success: OrchestratorMcpThreadInterruptResult,
failure: OrchestratorMcpFailure,
diff --git a/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts
index 7066ecb9153..caa4c57fb0f 100644
--- a/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts
+++ b/apps/server/src/orchestration-v2/AcpRegistryOrchestratorV2.live.test.ts
@@ -25,6 +25,8 @@ import {
NoOpProviderEventLoggers,
ProviderEventLoggers,
} from "../provider/Layers/ProviderEventLoggers.ts";
+import * as OpenCode2Runtime from "../provider/opencode2Runtime.ts";
+import * as SpawnedProcessReaper from "../provider/SpawnedProcessReaper.ts";
import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
@@ -78,6 +80,10 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
NodeServices.layer,
FetchHttpClient.layer,
OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)),
+ OpenCode2Runtime.layer.pipe(
+ Layer.provide(SpawnedProcessReaper.layer),
+ Layer.provide(NodeServices.layer),
+ ),
Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers),
),
),
diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
index 6f856f1920a..f742033a0b9 100644
--- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
@@ -10778,6 +10778,7 @@ describe("AcpAdapterV2", () => {
header: "Approve",
question: "Approve stale generation 1?",
options: [{ label: "yes", description: "Approve" }],
+ multiSelect: false,
},
],
}).pipe(Effect.exit, Effect.forkScoped);
@@ -10854,6 +10855,7 @@ describe("AcpAdapterV2", () => {
header: "Approve",
question: "Approve missing transport?",
options: [{ label: "yes", description: "Approve" }],
+ multiSelect: false,
},
],
}).pipe(Effect.exit);
@@ -10887,6 +10889,7 @@ describe("AcpAdapterV2", () => {
header: "Approve",
question: "Approve live generation 2?",
options: [{ label: "yes", description: "Approve" }],
+ multiSelect: false,
},
],
}).pipe(Effect.forkScoped);
diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
index 4704e7489af..eea2ad5485c 100644
--- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
@@ -4487,6 +4487,7 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
header: nonEmptyText(record?.title, `Question ${index + 1}`),
question: nonEmptyText(record?.description, params.message),
options,
+ multiSelect: false,
};
},
);
diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
index 65abca30d9d..ada5262d14d 100644
--- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
@@ -3013,6 +3013,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
label: nonEmptyText(option.label, `Option ${optionIndex + 1}`),
description: nonEmptyText(option.description, option.label),
})) ?? [],
+ multiSelect: false,
}));
const nodeId = idAllocator.derive.nodeFromProviderItem({
driver: CODEX_PROVIDER,
diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
index e6d62050052..db495e0a9b7 100644
--- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
@@ -2,8 +2,11 @@ import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import type * as EffectAcpSchema from "effect-acp/schema";
+import { XAiAskUserQuestionRequest } from "../../provider/acp/XAiAcpExtension.ts";
+import type * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts";
import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts";
import {
+ type AcpAdapterV2ExtensionContext,
AcpProviderCapabilitiesV2,
acpCompletedTurnShouldTerminalizeTool,
acpPermissionDisposition,
@@ -19,6 +22,7 @@ import {
import {
makeGrokAcpAdapterFlavor,
GrokProviderCapabilitiesV2,
+ registerGrokAcpExtensions,
type GrokAdapterV2Options,
} from "./GrokAdapterV2.ts";
@@ -223,6 +227,53 @@ describe("acpRootTurnIsIdle", () => {
});
describe("GrokAdapterV2 capabilities", () => {
+ it.effect("forwards native Grok multiselect questions to requestUserInput", () =>
+ Effect.gen(function* () {
+ type RequestHandler = (
+ payload: typeof XAiAskUserQuestionRequest.Type,
+ ) => Effect.Effect;
+ let requestHandler: RequestHandler | undefined;
+ const runtime = {
+ handleExtNotification: () => Effect.void,
+ handleExtRequest: (method: string, _payload: unknown, handler: RequestHandler) =>
+ Effect.sync(() => {
+ if (method === "x.ai/ask_user_question") requestHandler = handler;
+ }),
+ } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
+ const requests: Array[0]> = [];
+
+ yield* registerGrokAcpExtensions({
+ runtime,
+ applyBackgroundTaskMutation: () => Effect.void,
+ requestUserInput: (request) =>
+ Effect.sync(() => {
+ requests.push(request);
+ return {
+ acknowledgeNativeResponse: Effect.void,
+ answers: null,
+ };
+ }),
+ });
+
+ assert.isDefined(requestHandler);
+ yield* requestHandler!({
+ sessionId: "session-1",
+ toolCallId: "tool-call-1",
+ mode: "default",
+ questions: [
+ {
+ id: "scope",
+ question: "Which scopes should Grok use?",
+ multiSelect: true,
+ options: [{ label: "Tests" }, { label: "Docs" }],
+ },
+ ],
+ });
+
+ assert.isTrue(requests[0]?.questions[0]?.multiSelect);
+ }),
+ );
+
it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => {
const flavor = makeGrokAcpAdapterFlavor({
makeRuntime: () => Effect.never,
diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
index 163bb325819..b9ecd7a13b7 100644
--- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
@@ -133,6 +133,7 @@ const registerGrokAskUserQuestionExtensions = ({
header: question.header,
question: question.question,
options: [...question.options],
+ multiSelect: question.multiSelect === true,
}));
return requestUserInput({
nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`,
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.agentSelection.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.agentSelection.test.ts
new file mode 100644
index 00000000000..aefa5a1a406
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.agentSelection.test.ts
@@ -0,0 +1,297 @@
+import { assert, it as effectIt } from "@effect/vitest";
+import { ProviderInstanceId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import { describe, expect, it } from "vite-plus/test";
+
+import {
+ clampOpenCode2Variant,
+ openCode2InteractionModeForAgent,
+ openCode2SessionSelectionParameters,
+ planOpenCode2VariantAlignment,
+ resolveOpenCode2SessionAgent,
+ retryEmptyOpenCode2VariantCatalog,
+} from "./OpenCode2AdapterV2.ts";
+
+describe("openCode2InteractionModeForAgent", () => {
+ it("maps every native pair event so matching echoes supersede queued reflections", () => {
+ expect(openCode2InteractionModeForAgent("build")).toBe("default");
+ expect(openCode2InteractionModeForAgent("plan")).toBe("plan");
+ expect(openCode2InteractionModeForAgent("Release-Captain")).toBeNull();
+ });
+});
+
+describe("openCode2SessionSelectionParameters", () => {
+ it("sends the canonical option id when creating or switching a native session", () => {
+ expect(
+ openCode2SessionSelectionParameters({
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/glm-5.2",
+ options: [{ id: "agent", value: "build" }],
+ }),
+ ).toEqual({
+ model: { id: "glm-5.2", providerID: "opencode" },
+ agent: "build",
+ });
+ });
+
+ it("preserves custom executable agent ids verbatim", () => {
+ expect(
+ openCode2SessionSelectionParameters({
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/glm-5.2",
+ options: [{ id: "agent", value: "Release-Captain" }],
+ }).agent,
+ ).toBe("Release-Captain");
+ });
+
+ it("sends the selected variant on the model ref", () => {
+ expect(
+ openCode2SessionSelectionParameters({
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/claude-opus-5",
+ options: [{ id: "variant", value: "high" }],
+ }).model,
+ ).toEqual({ id: "claude-opus-5", providerID: "opencode", variant: "high" });
+ });
+
+ // The 2.x server resolves an unset variant to a synthetic id literally named
+ // "default" that is not in any model's catalog, and an unknown bound variant
+ // silently swallows the next prompt, so the sentinel must never hit the wire.
+ it("treats the synthetic default variant as unset", () => {
+ expect(
+ openCode2SessionSelectionParameters({
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/claude-opus-5",
+ options: [{ id: "variant", value: "default" }],
+ }).model,
+ ).toEqual({ id: "claude-opus-5", providerID: "opencode" });
+ });
+
+ it("derives the agent from the interaction mode when no option is set", () => {
+ expect(
+ openCode2SessionSelectionParameters(
+ {
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/glm-5.2",
+ },
+ "plan",
+ ).agent,
+ ).toBe("plan");
+ });
+});
+
+describe("resolveOpenCode2SessionAgent", () => {
+ it("keeps a custom agent over the toggle", () => {
+ expect(resolveOpenCode2SessionAgent("Release-Captain", "plan")).toBe("Release-Captain");
+ expect(resolveOpenCode2SessionAgent("Release-Captain", "default")).toBe("Release-Captain");
+ expect(resolveOpenCode2SessionAgent("Release-Captain", undefined)).toBe("Release-Captain");
+ });
+
+ // The descriptor's Auto sentinel means "defer to the toggle".
+ it("treats the auto sentinel as no explicit selection", () => {
+ expect(resolveOpenCode2SessionAgent("auto", "plan")).toBe("plan");
+ expect(resolveOpenCode2SessionAgent("auto", "default")).toBe("build");
+ expect(resolveOpenCode2SessionAgent("auto", undefined)).toBeUndefined();
+ });
+
+ // Every pre-toggle thread has a persisted `agent: "build"` selection; it
+ // must not pin the Build/Plan toggle inert.
+ it("lets plan mode override a stale build option", () => {
+ expect(resolveOpenCode2SessionAgent("build", "plan")).toBe("plan");
+ });
+
+ it("honors an explicit plan option in default mode", () => {
+ expect(resolveOpenCode2SessionAgent("plan", "default")).toBe("plan");
+ expect(resolveOpenCode2SessionAgent("plan", "plan")).toBe("plan");
+ });
+
+ it("maps the toggle onto the native pair when no option is set", () => {
+ expect(resolveOpenCode2SessionAgent(undefined, "default")).toBe("build");
+ expect(resolveOpenCode2SessionAgent(undefined, "plan")).toBe("plan");
+ expect(resolveOpenCode2SessionAgent("build", "default")).toBe("build");
+ });
+
+ it("drops an agent that the live catalog does not contain", () => {
+ const buildOnly = new Set(["build"]);
+ expect(resolveOpenCode2SessionAgent(undefined, "default", buildOnly)).toBe("build");
+ expect(resolveOpenCode2SessionAgent(undefined, "plan", buildOnly)).toBeUndefined();
+ expect(resolveOpenCode2SessionAgent("Release-Captain", "default", buildOnly)).toBeUndefined();
+ });
+
+ // Subagent child sessions and text generation pass no interaction mode and
+ // must keep their explicit-option-only behavior.
+ it("passes the explicit option through when no mode is given", () => {
+ expect(resolveOpenCode2SessionAgent("build", undefined)).toBe("build");
+ expect(resolveOpenCode2SessionAgent("plan", undefined)).toBe("plan");
+ expect(resolveOpenCode2SessionAgent(undefined, undefined)).toBeUndefined();
+ });
+});
+
+describe("clampOpenCode2Variant", () => {
+ const KNOWN = new Set(["low", "high"]);
+
+ it("passes a catalog variant through", () => {
+ expect(clampOpenCode2Variant("high", KNOWN)).toEqual({
+ variant: "high",
+ droppedVariant: null,
+ });
+ });
+
+ it("drops a variant the catalog does not list", () => {
+ expect(clampOpenCode2Variant("bogus", KNOWN)).toEqual({
+ variant: undefined,
+ droppedVariant: "bogus",
+ });
+ });
+
+ // Covers a failed catalog fetch, the empty bootstrap catalog, and a model
+ // missing from the catalog: none can positively validate, so fail closed.
+ it("drops any variant when the catalog is unavailable", () => {
+ expect(clampOpenCode2Variant("high", null)).toEqual({
+ variant: undefined,
+ droppedVariant: "high",
+ });
+ });
+
+ it("leaves an unset variant alone without reporting a drop", () => {
+ expect(clampOpenCode2Variant(undefined, null)).toEqual({
+ variant: undefined,
+ droppedVariant: null,
+ });
+ });
+});
+
+describe("planOpenCode2VariantAlignment", () => {
+ const MODEL = "opencode/claude-opus-5";
+ const KNOWN = new Set(["low", "high"]);
+
+ // Subagent child threads and pre-variant selections carry no variant
+ // option; that must not reset a variant the native session already has.
+ it("leaves the bound variant alone when the selection has no opinion", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: MODEL,
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: undefined,
+ knownVariants: KNOWN,
+ }),
+ ).toEqual({ switchNeeded: false, variant: undefined, droppedVariant: null });
+ });
+
+ it("switches on a variant-only change between turns", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: MODEL,
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: "low",
+ knownVariants: KNOWN,
+ }),
+ ).toEqual({ switchNeeded: true, variant: "low", droppedVariant: null });
+ });
+
+ it("does not switch when the selected variant is already bound", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: MODEL,
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: "high",
+ knownVariants: KNOWN,
+ }).switchNeeded,
+ ).toBe(false);
+ });
+
+ it("resets a bound variant when the synthetic default is selected", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: MODEL,
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: "default",
+ knownVariants: null,
+ }),
+ ).toEqual({ switchNeeded: true, variant: undefined, droppedVariant: null });
+ });
+
+ it("rebinds on a model change even without a variant option", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: "opencode/glm-5.2",
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: undefined,
+ knownVariants: null,
+ }),
+ ).toEqual({ switchNeeded: true, variant: undefined, droppedVariant: null });
+ });
+
+ it("clamps an unknown variant to the server default and still switches", () => {
+ expect(
+ planOpenCode2VariantAlignment({
+ boundModel: MODEL,
+ boundVariant: "high",
+ model: MODEL,
+ rawVariant: "bogus",
+ knownVariants: KNOWN,
+ }),
+ ).toEqual({ switchNeeded: true, variant: undefined, droppedVariant: "bogus" });
+ });
+});
+
+describe("retryEmptyOpenCode2VariantCatalog", () => {
+ const POPULATED: ReadonlyMap> = new Map([
+ ["opencode/glm-5.2", new Set(["high", "max"])],
+ ]);
+
+ // The bootstrap window: the server is up but reports an empty catalog, which
+ // would otherwise make the fail-closed clamp eat a valid first-turn variant.
+ effectIt.effect("retries an empty bootstrap catalog until it populates", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const catalog = yield* retryEmptyOpenCode2VariantCatalog(
+ Effect.sync(() => {
+ reads += 1;
+ return reads < 3 ? new Map>() : POPULATED;
+ }),
+ { maxAttempts: 5, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 3);
+ assert.strictEqual(catalog?.get("opencode/glm-5.2")?.has("max"), true);
+ }),
+ );
+
+ effectIt.effect("retries a failed fetch and stops after the configured attempts", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const catalog = yield* retryEmptyOpenCode2VariantCatalog(
+ Effect.sync(() => {
+ reads += 1;
+ return null;
+ }),
+ { maxAttempts: 3, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 3);
+ assert.strictEqual(catalog, null);
+ }),
+ );
+
+ effectIt.effect("returns a populated catalog immediately", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const catalog = yield* retryEmptyOpenCode2VariantCatalog(
+ Effect.sync(() => {
+ reads += 1;
+ return POPULATED;
+ }),
+ { maxAttempts: 5, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 1);
+ assert.strictEqual(catalog, POPULATED);
+ }),
+ );
+});
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.live.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.live.test.ts
new file mode 100644
index 00000000000..b3d25ab3ab1
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.live.test.ts
@@ -0,0 +1,359 @@
+// @ts-nocheck — beta SDK live API surface in flux; keep runtime checks.
+import type { V2Event } from "@opencode-ai/sdk-next/v2";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import {
+ OpenCode2Settings,
+ ProviderInstanceId,
+ ProviderSessionId,
+ ThreadId,
+} from "@t3tools/contracts";
+import { assert, it } from "@effect/vitest";
+import * as Clock from "effect/Clock";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as Schema from "effect/Schema";
+import * as Stream from "effect/Stream";
+import { describe } from "vite-plus/test";
+
+import { ServerConfig } from "../../config.ts";
+import * as OpenCode2Runtime from "../../provider/opencode2Runtime.ts";
+import * as SpawnedProcessReaper from "../../provider/SpawnedProcessReaper.ts";
+import { IdAllocatorV2, layer as idAllocatorLayer } from "../IdAllocator.ts";
+import { ProviderAdapterV2RuntimePolicy } from "../ProviderAdapter.ts";
+import {
+ makeOpenCode2AdapterV2,
+ OpenCode2ProviderCapabilitiesV2,
+ unwrapOpenCode2Data,
+} from "./OpenCode2AdapterV2.ts";
+
+const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {
+ prefix: "t3-opencode2-pending-work-",
+}).pipe(Layer.provide(NodeServices.layer));
+const decodeOpenCode2Settings = Schema.decodeUnknownEffect(OpenCode2Settings);
+const layer = Layer.mergeAll(
+ OpenCode2Runtime.layer.pipe(
+ Layer.provide(SpawnedProcessReaper.layer),
+ Layer.provide(NodeServices.layer),
+ ),
+ idAllocatorLayer,
+ serverConfigLayer,
+);
+
+type OpenCode2CompactionEvent = Extract<
+ V2Event,
+ {
+ type:
+ | "session.compaction.admitted"
+ | "session.compaction.started"
+ | "session.compaction.delta"
+ | "session.compaction.ended"
+ | "session.compaction.failed";
+ }
+>;
+
+function isOpenCode2CompactionEvent(event: V2Event): event is OpenCode2CompactionEvent {
+ return (
+ event.type === "session.compaction.admitted" ||
+ event.type === "session.compaction.started" ||
+ event.type === "session.compaction.delta" ||
+ event.type === "session.compaction.ended" ||
+ event.type === "session.compaction.failed"
+ );
+}
+
+describe.runIf(process.env.T3_OPENCODE2_LIVE === "1")(
+ "OpenCode 2 adapter pending work (live)",
+ () => {
+ it.effect(
+ "pins only its running shells and emits a wake when the shell exits",
+ () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const serverConfig = yield* ServerConfig;
+ const idAllocator = yield* IdAllocatorV2;
+ const runtime = yield* OpenCode2Runtime.OpenCode2Runtime;
+ const server = yield* runtime.startOpenCode2ServerProcess({
+ binaryPath: "opencode2",
+ });
+ const client = runtime.createOpenCode2SdkClient({
+ baseUrl: server.url,
+ directory: process.cwd(),
+ serverPassword: server.password,
+ });
+ const instanceId = ProviderInstanceId.make("opencode2-live-test");
+ const modelSelection = {
+ instanceId,
+ model: "opencode/glm-5.2",
+ options: [],
+ };
+ const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
+ runtimeMode: "full-access",
+ interactionMode: "default",
+ cwd: process.cwd(),
+ });
+ const adapter = makeOpenCode2AdapterV2({
+ instanceId,
+ settings: yield* decodeOpenCode2Settings({
+ binaryPath: "opencode2",
+ serverUrl: server.url,
+ serverPassword: server.password,
+ }),
+ environment: process.env,
+ runtime,
+ idAllocator,
+ serverConfig,
+ });
+ const session = yield* adapter.openSession({
+ threadId: ThreadId.make("thread-opencode2-live-test"),
+ providerSessionId: ProviderSessionId.make("provider-session-opencode2-live-test"),
+ modelSelection,
+ runtimePolicy,
+ });
+ const providerThread = yield* session.ensureThread({
+ threadId: ThreadId.make("thread-opencode2-live-test"),
+ modelSelection,
+ runtimePolicy,
+ });
+ const sessionID = providerThread.nativeThreadRef?.nativeId;
+ assert.isString(sessionID);
+ assert.isDefined(session.hasPendingBackgroundWork);
+ assert.isDefined(session.hasPendingBackgroundWorkForThread);
+
+ const created = yield* OpenCode2Runtime.runOpenCode2Sdk("shell.create", () =>
+ client.v2.shell.create({
+ location: { directory: process.cwd() },
+ command: "sleep 20",
+ timeout: 30_000,
+ metadata: { sessionID },
+ }),
+ );
+ const shell = yield* unwrapOpenCode2Data<{ readonly id: string }>(
+ "shell.create",
+ created,
+ );
+
+ assert.isTrue(yield* session.hasPendingBackgroundWork!);
+ assert.isTrue(yield* session.hasPendingBackgroundWorkForThread!(providerThread));
+ const createdWake = yield* session.events.pipe(
+ Stream.filter((event) => event.type === "provider_thread.updated"),
+ Stream.runHead,
+ Effect.timeoutOption("5 seconds"),
+ );
+ assert.isTrue(Option.isSome(createdWake));
+
+ yield* OpenCode2Runtime.runOpenCode2Sdk("shell.remove", () =>
+ client.v2.shell.remove({
+ id: shell.id,
+ location: { directory: process.cwd() },
+ }),
+ );
+ const exitedWake = yield* session.events.pipe(
+ Stream.filter((event) => event.type === "provider_thread.updated"),
+ Stream.runHead,
+ Effect.timeoutOption("5 seconds"),
+ );
+ assert.isTrue(Option.isSome(exitedWake));
+ assert.isFalse(yield* session.hasPendingBackgroundWork!);
+ assert.isFalse(yield* session.hasPendingBackgroundWorkForThread!(providerThread));
+ }),
+ ).pipe(Effect.provide(layer)),
+ { timeout: 60_000 },
+ );
+
+ it.live(
+ "observes the native compaction lifecycle used by the adapter projection",
+ () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const runtime = yield* OpenCode2Runtime.OpenCode2Runtime;
+ const server = yield* runtime.startOpenCode2ServerProcess({
+ binaryPath: "opencode2",
+ });
+ const client = runtime.createOpenCode2SdkClient({
+ baseUrl: server.url,
+ directory: process.cwd(),
+ serverPassword: server.password,
+ });
+ const abortController = new AbortController();
+ yield* Effect.addFinalizer(() => Effect.sync(() => abortController.abort()));
+ const subscription = yield* OpenCode2Runtime.runOpenCode2Sdk("event.subscribe", () =>
+ client.v2.event.subscribe({ signal: abortController.signal }),
+ );
+ const created = yield* OpenCode2Runtime.runOpenCode2Sdk("session.create", () =>
+ client.v2.session.create({
+ location: { directory: process.cwd() },
+ model: { providerID: "opencode", id: "glm-5.2" },
+ }),
+ );
+ const session = yield* unwrapOpenCode2Data<{ readonly id: string }>(
+ "session.create",
+ created,
+ );
+ yield* Effect.addFinalizer(() =>
+ OpenCode2Runtime.runOpenCode2Sdk("session.remove", () =>
+ client.v2.session.remove({ sessionID: session.id }),
+ ).pipe(Effect.ignore),
+ );
+ const eventFiber = yield* Stream.fromAsyncIterable(
+ subscription.stream,
+ (cause) =>
+ new OpenCode2Runtime.OpenCode2RuntimeError({
+ operation: "event.subscribe",
+ category: "event-subscription-failed",
+ cause,
+ }),
+ ).pipe(
+ Stream.filter(
+ (event): event is OpenCode2CompactionEvent =>
+ isOpenCode2CompactionEvent(event) && event.data.sessionID === session.id,
+ ),
+ Stream.takeUntil(
+ (event) =>
+ event.type === "session.compaction.ended" ||
+ event.type === "session.compaction.failed",
+ ),
+ Stream.runCollect,
+ Effect.forkScoped,
+ );
+
+ yield* OpenCode2Runtime.runOpenCode2Sdk("session.prompt", () =>
+ client.v2.session.prompt({
+ sessionID: session.id,
+ text: "Remember this sentence: native compaction fixture context.",
+ }),
+ );
+ yield* OpenCode2Runtime.runOpenCode2Sdk("session.wait", () =>
+ client.v2.session.wait({ sessionID: session.id }),
+ );
+ const compactionId = `msg_t3_live_compaction_${yield* Clock.currentTimeMillis}`;
+ yield* OpenCode2Runtime.runOpenCode2Sdk("session.compact", () =>
+ client.v2.session.compact({
+ sessionID: session.id,
+ id: compactionId,
+ }),
+ );
+ yield* OpenCode2Runtime.runOpenCode2Sdk("session.wait", () =>
+ client.v2.session.wait({ sessionID: session.id }),
+ );
+
+ const events = Array.from(
+ yield* Fiber.join(eventFiber).pipe(Effect.timeout("60 seconds")),
+ );
+ assert.deepEqual(
+ events
+ .filter((event) => event.type !== "session.compaction.delta")
+ .map((event) => event.type),
+ [
+ "session.compaction.admitted",
+ "session.compaction.started",
+ "session.compaction.ended",
+ ],
+ );
+ const ended = events.find((event) => event.type === "session.compaction.ended");
+ assert.isDefined(ended);
+ assert.isAbove(ended.data.text.length, 0);
+ }),
+ ).pipe(Effect.provide(layer)),
+ { timeout: 120_000 },
+ );
+
+ it.live(
+ "deletes a detached native session idempotently",
+ () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const serverConfig = yield* ServerConfig;
+ const idAllocator = yield* IdAllocatorV2;
+ const runtime = yield* OpenCode2Runtime.OpenCode2Runtime;
+ const server = yield* runtime.startOpenCode2ServerProcess({
+ binaryPath: "opencode2",
+ });
+ const client = runtime.createOpenCode2SdkClient({
+ baseUrl: server.url,
+ directory: process.cwd(),
+ serverPassword: server.password,
+ });
+ const instanceId = ProviderInstanceId.make("opencode2-live-delete-test");
+ const providerSessionId = ProviderSessionId.make(
+ "provider-session-opencode2-live-delete-test",
+ );
+ const adapter = makeOpenCode2AdapterV2({
+ instanceId,
+ settings: yield* decodeOpenCode2Settings({
+ binaryPath: "opencode2",
+ serverUrl: server.url,
+ serverPassword: server.password,
+ }),
+ environment: process.env,
+ runtime,
+ idAllocator,
+ serverConfig,
+ });
+ const deleteDetachedThread = adapter.deleteDetachedThread;
+ assert.isDefined(deleteDetachedThread);
+
+ const created = yield* OpenCode2Runtime.runOpenCode2Sdk("session.create", () =>
+ client.v2.session.create({
+ location: { directory: process.cwd() },
+ model: { providerID: "opencode", id: "glm-5.2" },
+ }),
+ );
+ const nativeSession = yield* unwrapOpenCode2Data<{ readonly id: string }>(
+ "session.create",
+ created,
+ );
+ const now = yield* DateTime.now;
+ const providerSession = {
+ id: providerSessionId,
+ driver: adapter.driver,
+ providerInstanceId: instanceId,
+ status: "stopped" as const,
+ cwd: process.cwd(),
+ model: "opencode/glm-5.2",
+ capabilities: OpenCode2ProviderCapabilitiesV2,
+ createdAt: now,
+ updatedAt: now,
+ lastError: null,
+ };
+ const providerThread = {
+ id: idAllocator.derive.providerThread({
+ driver: adapter.driver,
+ nativeThreadId: nativeSession.id,
+ }),
+ driver: adapter.driver,
+ providerInstanceId: instanceId,
+ providerSessionId,
+ appThreadId: ThreadId.make("thread-opencode2-live-delete-test"),
+ ownerNodeId: null,
+ nativeThreadRef: {
+ driver: adapter.driver,
+ nativeId: nativeSession.id,
+ strength: "strong" as const,
+ },
+ nativeConversationHeadRef: null,
+ status: "idle" as const,
+ firstRunOrdinal: null,
+ lastRunOrdinal: null,
+ handoffIds: [],
+ forkedFrom: null,
+ pendingBackgroundTasks: [],
+ createdAt: now,
+ updatedAt: now,
+ };
+
+ yield* deleteDetachedThread({ providerSession, providerThread });
+ yield* deleteDetachedThread({ providerSession, providerThread });
+
+ const missing = yield* OpenCode2Runtime.runOpenCode2Sdk("session.get", () =>
+ client.v2.session.get({ sessionID: nativeSession.id }, { throwOnError: false }),
+ );
+ assert.equal(missing.response.status, 404);
+ }),
+ ).pipe(Effect.provide(layer)),
+ { timeout: 60_000 },
+ );
+ },
+);
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts
new file mode 100644
index 00000000000..10cef85318f
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts
@@ -0,0 +1,1145 @@
+import type { V2Event } from "@opencode-ai/sdk-next/v2";
+type SessionPendingInfo = {
+ sessionID: string;
+ type?: string;
+ id?: string;
+ admittedSeq?: number;
+ [key: string]: unknown;
+};
+type ShellInfoV2 = {
+ id: string;
+ status: string;
+ metadata: Record;
+ command?: string;
+ cwd?: string;
+ exit?: number;
+ time?: { started?: number; completed?: number };
+ [key: string]: unknown;
+};
+import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
+import { assert, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+import * as Schema from "effect/Schema";
+import { describe } from "vite-plus/test";
+
+import {
+ openCode2AutoPermissionReply,
+ openCode2ChildTurnItemOrdinals,
+ openCode2EnvironmentWithPermission,
+ openCode2EnvironmentWithT3Mcp,
+ openCode2EventEndsExecution,
+ openCode2ForkParameters,
+ openCode2InterruptedThreadDisposition,
+ openCode2IsCancelledPostSettleWake,
+ openCode2IsPostSettleWakeAdmission,
+ openCode2PendingWorkForSession,
+ openCode2PermissionAutoReply,
+ openCode2PermissionAutoReplyForSession,
+ openCode2QuestionId,
+ openCode2SessionSelectionParameters,
+ openCode2SessionErrorMessage,
+ openCode2SessionErrorStatus,
+ openCode2SessionErrorTargetSessionIds,
+ openCode2CanAdoptMissingExecutionStart,
+ openCode2ShouldForceInterruptFinalize,
+ openCode2ShouldResubscribeStalledStream,
+ openCode2ShouldSettleTurn,
+ openCode2ToolNeedsTerminalOverride,
+ normalizeOpenCode2PermissionEvent,
+ OPENCODE2_EVENT_STALL_MS,
+ OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ OPENCODE2_PROMOTED_INPUT_ID_LIMIT,
+ OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT,
+ pruneOpenCode2PromotedInputIds,
+ pruneOpenCode2RetiredSuppressWakes,
+ rememberOpenCode2SessionPermission,
+ removeOpenCode2Session,
+ unwrapOpenCode2Data,
+} from "./OpenCode2AdapterV2.ts";
+
+const v2Event = (event: unknown) => event as V2Event;
+
+const t3McpSession = {
+ environmentId: EnvironmentId.make("environment:test"),
+ threadId: ThreadId.make("thread:test"),
+ providerSessionId: "provider-session:test",
+ providerInstanceId: ProviderInstanceId.make("opencode2"),
+ endpoint: "http://127.0.0.1:43123/mcp",
+ authorizationHeader: "Bearer test-token",
+};
+const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
+const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
+
+describe("unwrapOpenCode2Data", () => {
+ it.effect("reads through both envelopes", () =>
+ Effect.gen(function* () {
+ const payload = yield* unwrapOpenCode2Data("session.create", {
+ data: { data: { id: "ses_1" } },
+ });
+ assert.deepStrictEqual(payload, {
+ id: "ses_1",
+ });
+ }),
+ );
+
+ // The failure mode this guards is silent: reading one layer yields the
+ // envelope, which looks like a valid object and fails much later.
+ it.effect("fails through the typed channel for an outer-only envelope", () =>
+ Effect.gen(function* () {
+ const error = yield* unwrapOpenCode2Data("session.create", { data: {} }).pipe(Effect.flip);
+ assert.strictEqual(error.operation, "session.create");
+ assert.strictEqual(error.category, "missing-response-payload");
+ }),
+ );
+
+ it.effect("fails through the typed channel for a missing payload", () =>
+ Effect.gen(function* () {
+ const error = yield* unwrapOpenCode2Data("session.get", {}).pipe(Effect.flip);
+ assert.strictEqual(error.operation, "session.get");
+ assert.strictEqual(error.category, "missing-response-payload");
+ }),
+ );
+});
+
+describe("OpenCode 2 post-settle wake classification", () => {
+ const syntheticAdmission = v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_wake",
+ input: {
+ type: "synthetic",
+ data: { text: 'child completed' },
+ delivery: "queue",
+ },
+ },
+ });
+
+ it("classifies provider completion admissions for input-aware ownership", () => {
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: false,
+ }),
+ );
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: false,
+ }),
+ );
+ assert.isFalse(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: true,
+ }),
+ );
+ assert.isFalse(
+ openCode2IsPostSettleWakeAdmission(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_user",
+ input: { type: "user", data: { text: "hello" }, delivery: "queue" },
+ },
+ }),
+ {
+ isChildSession: false,
+ },
+ ),
+ );
+ });
+
+ it("keeps in-turn synthetic control instructions on their owning execution", () => {
+ assert.isFalse(
+ openCode2IsPostSettleWakeAdmission(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_background_instruction",
+ input: {
+ type: "synthetic",
+ data: {
+ text: "User requested that active blocking work be moved to the background.",
+ },
+ delivery: "steer",
+ },
+ },
+ }),
+ { isChildSession: false },
+ ),
+ );
+ });
+
+ it("accepts observed provider metadata when completion text is not wrapped", () => {
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_shell_wake",
+ input: {
+ type: "synthetic",
+ data: {
+ text: "shell completed",
+ metadata: { source: "shell", state: "completed" },
+ },
+ delivery: "steer",
+ },
+ },
+ }),
+ { isChildSession: false },
+ ),
+ );
+ });
+
+ it("classifies a synthetic wake beside an allocated user execution", () => {
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: false,
+ }),
+ );
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: false,
+ }),
+ );
+ });
+
+ it("defers cancelled wake ownership until input promotion identifies the execution", () => {
+ const cancelledAdmission = v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_cancelled",
+ input: {
+ type: "synthetic",
+ data: { text: 'cancelled continuation' },
+ delivery: "queue",
+ },
+ },
+ });
+
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(cancelledAdmission, {
+ isChildSession: false,
+ }),
+ );
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(syntheticAdmission, {
+ isChildSession: false,
+ }),
+ );
+ });
+
+ it("isolates cancellation synthetic inputs without dropping the wake boundary", () => {
+ for (const state of ["cancelled", "interrupted"] as const) {
+ const event = v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: `input_${state}`,
+ input: {
+ type: "synthetic",
+ data: { text: `partial output` },
+ delivery: "queue",
+ },
+ },
+ });
+ assert.isTrue(openCode2IsCancelledPostSettleWake(event));
+ assert.isTrue(
+ openCode2IsPostSettleWakeAdmission(event, {
+ isChildSession: false,
+ }),
+ );
+ }
+ assert.isFalse(openCode2IsCancelledPostSettleWake(syntheticAdmission));
+ });
+
+ it("accepts the observed state marker with flexible tag attributes", () => {
+ for (const text of [
+ ` \n\tpartial output`,
+ `partial output`,
+ `partial output`,
+ `partial output`,
+ ]) {
+ assert.isTrue(
+ openCode2IsCancelledPostSettleWake(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_cancelled",
+ input: {
+ type: "synthetic",
+ data: { text },
+ delivery: "queue",
+ },
+ },
+ }),
+ ),
+ );
+ }
+ assert.isFalse(
+ openCode2IsCancelledPostSettleWake(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_status",
+ input: {
+ type: "synthetic",
+ data: { text: `partial output` },
+ delivery: "queue",
+ },
+ },
+ }),
+ ),
+ );
+ });
+
+ it("requires a top-level cancellation marker at the start of synthetic text", () => {
+ for (const text of [
+ 'completed output',
+ 'quoted text: "nested output"',
+ 'nested output',
+ ]) {
+ assert.isFalse(
+ openCode2IsCancelledPostSettleWake(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_not_cancelled",
+ input: {
+ type: "synthetic",
+ data: { text },
+ delivery: "queue",
+ },
+ },
+ }),
+ ),
+ );
+ }
+ });
+
+ it("suppresses an empty interrupted background-shell wake", () => {
+ const interruptedShellAdmission = v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_interrupted_shell",
+ input: {
+ type: "synthetic",
+ data: {
+ text: '\n\n',
+ metadata: { source: "shell", state: "error" },
+ },
+ delivery: "steer",
+ },
+ },
+ });
+ assert.isTrue(openCode2IsCancelledPostSettleWake(interruptedShellAdmission));
+ });
+
+ it("keeps a background-shell error that carries output visible", () => {
+ assert.isFalse(
+ openCode2IsCancelledPostSettleWake(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_root",
+ inputID: "input_failed_shell",
+ input: {
+ type: "synthetic",
+ data: {
+ text: 'command failed',
+ metadata: { source: "shell", state: "error" },
+ },
+ delivery: "steer",
+ },
+ },
+ }),
+ ),
+ );
+ });
+
+ it("leaves a child synthetic admission eligible for child-turn creation", () => {
+ assert.isTrue(
+ openCode2IsCancelledPostSettleWake(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_child",
+ inputID: "input_child_cancelled",
+ input: {
+ type: "synthetic",
+ data: { text: `partial output` },
+ delivery: "queue",
+ },
+ },
+ }),
+ ),
+ );
+ assert.isFalse(
+ openCode2IsPostSettleWakeAdmission(
+ v2Event({
+ type: "session.next.prompt.admitted",
+ data: {
+ sessionID: "ses_child",
+ inputID: "input_child_cancelled",
+ input: {
+ type: "synthetic",
+ data: { text: `partial output` },
+ delivery: "queue",
+ },
+ },
+ }),
+ {
+ isChildSession: true,
+ },
+ ),
+ );
+ });
+
+ it("closes a buffered wake only on execution terminal or idle", () => {
+ assert.isFalse(
+ openCode2EventEndsExecution(
+ v2Event({ type: "session.next.step.started", data: { sessionID: "ses_root" } }),
+ ),
+ );
+ assert.isFalse(
+ openCode2EventEndsExecution(
+ v2Event({
+ type: "session.next.step.ended",
+ data: { sessionID: "ses_root", finish: "tool-calls" },
+ }),
+ ),
+ );
+ assert.isFalse(
+ openCode2EventEndsExecution(
+ v2Event({
+ type: "session.step.ended",
+ data: { sessionID: "ses_root", finish: "tool_calls" },
+ }),
+ ),
+ );
+ for (const type of [
+ "session.next.step.ended",
+ "session.next.step.failed",
+ "session.idle",
+ ] as const) {
+ assert.isTrue(
+ openCode2EventEndsExecution(v2Event({ type, data: { sessionID: "ses_root" } })),
+ );
+ }
+ assert.isTrue(
+ openCode2EventEndsExecution(
+ v2Event({
+ type: "session.next.step.ended",
+ data: { sessionID: "ses_root", finish: "stop" },
+ }),
+ ),
+ );
+ });
+});
+
+describe("OpenCode 2 wake evidence bounds", () => {
+ it("evicts the oldest retired suppression evidence in insertion order", () => {
+ const wakes = new Map();
+ for (let index = 0; index < OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT + 2; index += 1) {
+ wakes.set(`input_retired_${index}`, {});
+ }
+
+ pruneOpenCode2RetiredSuppressWakes(wakes);
+
+ assert.equal(wakes.size, OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT);
+ assert.isFalse(wakes.has("input_retired_0"));
+ assert.isFalse(wakes.has("input_retired_1"));
+ assert.isTrue(wakes.has(`input_retired_${OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT + 1}`));
+ });
+
+ it("keeps recent unclaimed promotion evidence for late admissions", () => {
+ const inputIds = new Set(
+ Array.from(
+ { length: OPENCODE2_PROMOTED_INPUT_ID_LIMIT + 1 },
+ (_, index) => `input_promoted_${index}`,
+ ),
+ );
+
+ pruneOpenCode2PromotedInputIds(inputIds);
+
+ assert.equal(inputIds.size, OPENCODE2_PROMOTED_INPUT_ID_LIMIT);
+ assert.isFalse(inputIds.has("input_promoted_0"));
+ assert.isTrue(inputIds.has(`input_promoted_${OPENCODE2_PROMOTED_INPUT_ID_LIMIT}`));
+ });
+});
+
+describe("OpenCode 2 session selection", () => {
+ it("round-trips a provider model whose id contains a slash", () => {
+ assert.deepStrictEqual(
+ openCode2SessionSelectionParameters({
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "openrouter/qwen/qwen3-coder",
+ }),
+ {
+ model: {
+ providerID: "openrouter",
+ id: "qwen/qwen3-coder",
+ },
+ },
+ );
+ });
+});
+
+describe("removeOpenCode2Session", () => {
+ it.effect("treats an already-missing native session as deleted", () =>
+ removeOpenCode2Session(
+ "ses_missing",
+ Effect.succeed({
+ data: undefined,
+ error: { name: "SessionNotFoundError" },
+ response: { status: 404 },
+ }),
+ ),
+ );
+
+ it.effect("retains non-idempotent native deletion failures", () =>
+ Effect.gen(function* () {
+ const failure = yield* removeOpenCode2Session(
+ "ses_broken",
+ Effect.succeed({
+ data: undefined,
+ error: { name: "InternalServerError" },
+ response: { status: 500 },
+ }),
+ ).pipe(Effect.flip);
+
+ assert.strictEqual(failure.operation, "session.remove");
+ assert.strictEqual(failure.category, "session-remove-failed");
+ assert.include(failure.message, "session-remove-failed");
+ assert.notInclude(failure.message, "InternalServerError");
+ }),
+ );
+});
+
+describe("openCode2QuestionId", () => {
+ it("slugs the header so answers keyed by id resolve", () => {
+ assert.strictEqual(openCode2QuestionId(0, "Pick a Branch!"), "question-0-pick-a-branch");
+ });
+
+ it("falls back to the index when the header carries no usable characters", () => {
+ assert.strictEqual(openCode2QuestionId(2, " ??? "), "question-2");
+ });
+});
+
+describe("openCode2ForkParameters", () => {
+ it("maps a boundary message onto the required before union member", () => {
+ assert.deepStrictEqual(openCode2ForkParameters("ses_123", "msg_456"), {
+ sessionID: "ses_123",
+ $body_boundary: { type: "before", messageID: "msg_456" },
+ });
+ });
+
+ it("maps a whole-head fork onto the through union member", () => {
+ assert.deepStrictEqual(openCode2ForkParameters("ses_123", undefined), {
+ sessionID: "ses_123",
+ $body_boundary: { type: "through" },
+ });
+ });
+});
+
+describe("openCode2EnvironmentWithT3Mcp", () => {
+ it.effect("merges a per-thread server into process-local inline config", () =>
+ Effect.gen(function* () {
+ const environment = {
+ CUSTOM_ENV: "preserved",
+ OPENCODE_CONFIG_CONTENT: encodeJson({
+ agent: { build: { mode: "primary" } },
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ },
+ }),
+ };
+ const result = yield* openCode2EnvironmentWithT3Mcp(environment, t3McpSession);
+ const resultEnvironment: NodeJS.ProcessEnv = result;
+
+ assert.strictEqual(resultEnvironment.CUSTOM_ENV, "preserved");
+ assert.deepStrictEqual(decodeJson(result.OPENCODE_CONFIG_CONTENT ?? ""), {
+ agent: { build: { mode: "primary" } },
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ "t3-code": {
+ type: "remote",
+ url: t3McpSession.endpoint,
+ headers: { Authorization: t3McpSession.authorizationHeader },
+ oauth: false,
+ },
+ },
+ });
+ assert.notStrictEqual(result, environment);
+ }),
+ );
+
+ it.effect("rejects inline config whose MCP field cannot be merged safely", () =>
+ Effect.gen(function* () {
+ const failure = yield* openCode2EnvironmentWithT3Mcp(
+ { OPENCODE_CONFIG_CONTENT: encodeJson({ mcp: false }) },
+ t3McpSession,
+ ).pipe(Effect.flip);
+
+ assert.isDefined(failure);
+ }),
+ );
+
+ it.effect("creates inline config when content is absent or empty", () =>
+ Effect.gen(function* () {
+ const environments: Array = [{}, { OPENCODE_CONFIG_CONTENT: "" }];
+ for (const environment of environments) {
+ const result = yield* openCode2EnvironmentWithT3Mcp(environment, t3McpSession);
+ assert.deepStrictEqual(decodeJson(result.OPENCODE_CONFIG_CONTENT ?? ""), {
+ mcp: {
+ "t3-code": {
+ type: "remote",
+ url: t3McpSession.endpoint,
+ headers: { Authorization: t3McpSession.authorizationHeader },
+ oauth: false,
+ },
+ },
+ });
+ }
+ }),
+ );
+});
+
+describe("openCode2EnvironmentWithPermission", () => {
+ const policy = (overrides: Record) =>
+ ({
+ cwd: "/tmp",
+ interactionMode: "default",
+ runtimeMode: "default",
+ ...overrides,
+ }) as never;
+
+ it.effect("injects allow while preserving inline MCP config for implicit full access", () =>
+ Effect.gen(function* () {
+ const environment = {
+ OPENCODE_CONFIG_CONTENT: encodeJson({
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ },
+ }),
+ };
+ const result = yield* openCode2EnvironmentWithPermission(
+ environment,
+ policy({ runtimeMode: "full-access" }),
+ );
+
+ assert.deepStrictEqual(decodeJson(result.OPENCODE_CONFIG_CONTENT ?? ""), {
+ permission: "allow",
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ },
+ });
+ }),
+ );
+
+ it.effect("does not inject allow when the policy requires approval", () =>
+ Effect.gen(function* () {
+ const environment = {
+ OPENCODE_CONFIG_CONTENT: encodeJson({
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ },
+ }),
+ };
+ const result = yield* openCode2EnvironmentWithPermission(
+ environment,
+ policy({ approvalPolicy: "on-request", runtimeMode: "full-access" }),
+ );
+
+ assert.strictEqual(result, environment);
+ assert.deepStrictEqual(decodeJson(result.OPENCODE_CONFIG_CONTENT ?? ""), {
+ mcp: {
+ existing: {
+ type: "local",
+ command: ["existing-mcp"],
+ },
+ },
+ });
+ }),
+ );
+
+ it.effect("injects allow when content is absent or empty", () =>
+ Effect.gen(function* () {
+ const environments: Array = [{}, { OPENCODE_CONFIG_CONTENT: "" }];
+ for (const environment of environments) {
+ const result = yield* openCode2EnvironmentWithPermission(
+ environment,
+ policy({ runtimeMode: "full-access" }),
+ );
+ assert.deepStrictEqual(decodeJson(result.OPENCODE_CONFIG_CONTENT ?? ""), {
+ permission: "allow",
+ });
+ }
+ }),
+ );
+});
+
+describe("openCode2AutoPermissionReply", () => {
+ const policy = (overrides: Record) =>
+ ({
+ cwd: "/tmp",
+ runtimeMode: "default",
+ interactionMode: "default",
+ ...overrides,
+ }) as never;
+ const reply = (
+ overrides: Record,
+ action: string,
+ resources: ReadonlyArray = ["*"],
+ ) => openCode2AutoPermissionReply(policy(overrides), { action, resources });
+
+ it("approves only the current request in full-access mode", () => {
+ assert.strictEqual(reply({ runtimeMode: "full-access" }, "bash"), "once");
+ });
+
+ it("does not turn approval never into implicit full access", () => {
+ assert.strictEqual(reply({ runtimeMode: "auto", approvalPolicy: "never" }, "bash"), "reject");
+ assert.strictEqual(reply({ runtimeMode: "auto", approvalPolicy: "never" }, "read"), "once");
+ });
+
+ it("surfaces the request when an approval policy asks for one", () => {
+ assert.strictEqual(
+ reply({ runtimeMode: "full-access", approvalPolicy: "always" }, "bash"),
+ null,
+ );
+ });
+
+ // A structured approval policy is a request for interactive review, so
+ // full-access must not silently override it.
+ it("surfaces the request for a structured approval policy", () => {
+ assert.strictEqual(
+ reply({ runtimeMode: "full-access", approvalPolicy: { type: "onRequest" } }, "bash"),
+ null,
+ );
+ });
+
+ it("auto-accepts edits but still asks for shell access", () => {
+ assert.strictEqual(reply({ runtimeMode: "auto-accept-edits" }, "edit"), "once");
+ assert.strictEqual(reply({ runtimeMode: "auto-accept-edits" }, "bash"), null);
+ });
+
+ it("enforces workspace-write and network policy without native persistent grants", () => {
+ const sandboxPolicy = {
+ type: "workspaceWrite",
+ networkAccess: true,
+ writableRoots: ["/workspace/shared"],
+ };
+ const overrides = {
+ runtimeMode: "auto",
+ approvalPolicy: "never",
+ sandboxPolicy,
+ };
+ assert.strictEqual(reply(overrides, "edit"), "once");
+ assert.strictEqual(reply(overrides, "bash"), "reject");
+ assert.strictEqual(reply(overrides, "websearch"), "once");
+ assert.strictEqual(
+ reply(overrides, "external_directory", ["/workspace/shared/file.txt"]),
+ "once",
+ );
+ assert.strictEqual(reply(overrides, "external_directory", ["/outside/file.txt"]), "reject");
+ });
+
+ it("does not let a remembered session grant override a later policy denial", () => {
+ assert.strictEqual(
+ openCode2PermissionAutoReply(
+ policy({ runtimeMode: "auto", approvalPolicy: "never" }),
+ [{ action: "bash", resources: ["*"] }],
+ { action: "bash", resources: ["*"] },
+ ),
+ "reject",
+ );
+ });
+
+ it("uses a remembered session grant when policy still requires approval", () => {
+ assert.strictEqual(
+ openCode2PermissionAutoReply(
+ policy({ runtimeMode: "default" }),
+ [{ action: "bash", resources: ["/workspace/*"] }],
+ { action: "bash", resources: ["/workspace/file.txt"] },
+ ),
+ "once",
+ );
+ });
+
+ it("combines remembered grants per resource in a multi-resource request", () => {
+ assert.strictEqual(
+ openCode2PermissionAutoReply(
+ policy({ runtimeMode: "default" }),
+ [
+ { action: "bash", resources: ["/workspace/first/*"] },
+ { action: "bash", resources: ["/workspace/second/*"] },
+ ],
+ {
+ action: "bash",
+ resources: ["/workspace/first/file.txt", "/workspace/second/file.txt"],
+ },
+ ),
+ "once",
+ );
+ });
+});
+
+describe("normalizeOpenCode2PermissionEvent", () => {
+ it("treats a missing legacy patterns list as a wildcard request", () => {
+ assert.deepStrictEqual(
+ normalizeOpenCode2PermissionEvent("legacy", {
+ id: "permission-1",
+ sessionID: "session-1",
+ permission: "grep",
+ metadata: {},
+ always: [],
+ }),
+ {
+ action: "grep",
+ resources: [],
+ save: [],
+ },
+ );
+ });
+
+ it("accepts preview aliases and singular patterns", () => {
+ assert.deepStrictEqual(
+ normalizeOpenCode2PermissionEvent("v2", {
+ permission: "external_directory",
+ pattern: "/workspace/file.txt",
+ always: ["/workspace/*"],
+ }),
+ {
+ action: "external_directory",
+ resources: ["/workspace/file.txt"],
+ save: ["/workspace/*"],
+ },
+ );
+ });
+});
+
+describe("OpenCode 2 remembered session permissions", () => {
+ const runtimePolicy = {
+ cwd: "/tmp",
+ interactionMode: "default",
+ runtimeMode: "default",
+ } as never;
+
+ it("scopes remembered grants to their native session", () => {
+ const permissions = new Map();
+ rememberOpenCode2SessionPermission(permissions, "ses_child", {
+ action: "bash",
+ resources: ["/workspace/*"],
+ save: [],
+ });
+ const request = { action: "bash", resources: ["/workspace/file.txt"] };
+
+ assert.strictEqual(
+ openCode2PermissionAutoReplyForSession(runtimePolicy, permissions, "ses_child", request),
+ "once",
+ );
+ assert.isNull(
+ openCode2PermissionAutoReplyForSession(runtimePolicy, permissions, "ses_sibling", request),
+ );
+ });
+
+ it("remembers a resource-less grant as a wildcard", () => {
+ const permissions = new Map();
+ rememberOpenCode2SessionPermission(permissions, "ses_root", {
+ action: "grep",
+ resources: [],
+ save: [],
+ });
+
+ assert.strictEqual(
+ openCode2PermissionAutoReplyForSession(runtimePolicy, permissions, "ses_root", {
+ action: "grep",
+ resources: ["/workspace/file.txt"],
+ }),
+ "once",
+ );
+ });
+});
+
+describe("OpenCode 2 child item ordinals", () => {
+ it("reserves a distinct item block for every child turn", () => {
+ assert.deepStrictEqual(openCode2ChildTurnItemOrdinals(1), { user: 100, next: 101 });
+ assert.deepStrictEqual(openCode2ChildTurnItemOrdinals(2), { user: 200, next: 201 });
+ });
+});
+
+describe("openCode2PendingWorkForSession", () => {
+ const sessionID = "ses_target";
+ const pending = (owner: string): SessionPendingInfo => ({
+ admittedSeq: 1,
+ id: "pending-1",
+ sessionID: owner,
+ timeCreated: 1,
+ type: "compaction",
+ });
+ const shell = (owner: string, status: ShellInfoV2["status"]): ShellInfoV2 => ({
+ id: "shell-1",
+ status,
+ command: "sleep 20",
+ cwd: "/workspace",
+ shell: "/bin/bash",
+ file: "/workspace/shell.out",
+ metadata: { sessionID: owner },
+ time: { started: 1 },
+ });
+
+ it.effect("pins the thread for its durable pending input without listing shells", () =>
+ Effect.gen(function* () {
+ let listedShells = false;
+ const result = yield* openCode2PendingWorkForSession({
+ sessionID,
+ pending: Effect.succeed([pending(sessionID)]),
+ shells: Effect.sync(() => {
+ listedShells = true;
+ return [];
+ }),
+ });
+
+ assert.isTrue(result);
+ assert.isFalse(listedShells);
+ }),
+ );
+
+ it.effect("pins only running shells owned by the same native session", () =>
+ Effect.gen(function* () {
+ assert.isTrue(
+ yield* openCode2PendingWorkForSession({
+ sessionID,
+ pending: Effect.succeed([]),
+ shells: Effect.succeed([shell(sessionID, "running")]),
+ }),
+ );
+ assert.isFalse(
+ yield* openCode2PendingWorkForSession({
+ sessionID,
+ pending: Effect.succeed([pending("ses_sibling")]),
+ shells: Effect.succeed([shell("ses_sibling", "running"), shell(sessionID, "exited")]),
+ }),
+ );
+ }),
+ );
+});
+
+describe("openCode2ToolNeedsTerminalOverride", () => {
+ const part = (status: "pending" | "running" | "completed" | "error", errorMessage?: string) => ({
+ status,
+ errorMessage,
+ });
+
+ it("terminalizes tools that have no native terminal state", () => {
+ assert.isTrue(openCode2ToolNeedsTerminalOverride(part("pending"), "failed"));
+ assert.isTrue(openCode2ToolNeedsTerminalOverride(part("running"), "interrupted"));
+ });
+
+ it("restamps only the provider's interrupt-specific tool failure", () => {
+ assert.isTrue(
+ openCode2ToolNeedsTerminalOverride(
+ part("error", "Tool execution interrupted"),
+ "interrupted",
+ ),
+ );
+ assert.isFalse(
+ openCode2ToolNeedsTerminalOverride(part("error", "command failed"), "interrupted"),
+ );
+ assert.isFalse(
+ openCode2ToolNeedsTerminalOverride(part("error", "Tool execution interrupted"), "failed"),
+ );
+ });
+
+ it("preserves completed tools", () => {
+ assert.isFalse(openCode2ToolNeedsTerminalOverride(part("completed"), "interrupted"));
+ });
+});
+
+describe("OpenCode 2 session errors", () => {
+ it("fans an unscoped error out to every active native session", () => {
+ assert.deepStrictEqual(
+ openCode2SessionErrorTargetSessionIds(undefined, ["ses_first", "ses_second"]),
+ ["ses_first", "ses_second"],
+ );
+ assert.deepStrictEqual(
+ openCode2SessionErrorTargetSessionIds("ses_second", ["ses_first", "ses_second"]),
+ ["ses_second"],
+ );
+ assert.deepStrictEqual(
+ openCode2SessionErrorTargetSessionIds("ses_missing", ["ses_first", "ses_second"]),
+ [],
+ );
+ });
+
+ it("normalizes provider abort errors without poisoning the provider session", () => {
+ const error = {
+ sessionID: "ses_target",
+ error: {
+ name: "MessageAbortedError",
+ data: { message: "The user aborted the request." },
+ },
+ } as const;
+
+ assert.strictEqual(openCode2SessionErrorMessage(error), "The user aborted the request.");
+ assert.strictEqual(openCode2SessionErrorStatus(error, false), "interrupted");
+ });
+
+ it("preserves ordinary provider failures", () => {
+ const error = {
+ error: {
+ name: "UnknownError",
+ data: { message: "Provider exploded." },
+ },
+ } as const;
+
+ assert.strictEqual(openCode2SessionErrorMessage(error), "Provider exploded.");
+ assert.strictEqual(openCode2SessionErrorStatus(error, false), "failed");
+ assert.strictEqual(openCode2SessionErrorStatus(error, true), "interrupted");
+ });
+
+ it("breaks a native thread only when the provider shuts down", () => {
+ assert.strictEqual(openCode2InterruptedThreadDisposition("user" as any), "reusable");
+ assert.strictEqual(openCode2InterruptedThreadDisposition("superseded" as any), "reusable");
+ assert.strictEqual(openCode2InterruptedThreadDisposition("shutdown" as any), "broken");
+ });
+
+ it("uses idle only before the authoritative execution lifecycle starts", () => {
+ assert.isTrue(openCode2ShouldSettleTurn("idle", false));
+ assert.isFalse(openCode2ShouldSettleTurn("execution-terminal", false));
+ assert.isFalse(openCode2ShouldSettleTurn("execution-interrupted", false));
+ assert.isTrue(openCode2ShouldSettleTurn("execution-interrupted", false, true));
+ assert.isFalse(openCode2ShouldSettleTurn("idle", true));
+ assert.isTrue(openCode2ShouldSettleTurn("execution-terminal", true));
+ assert.isTrue(openCode2ShouldSettleTurn("execution-interrupted", true));
+ });
+});
+
+describe("openCode2 interrupt and event-stream recovery helpers", () => {
+ it("force-finalizes only after an interrupted turn outlives the settle wait", () => {
+ assert.isFalse(
+ openCode2ShouldForceInterruptFinalize({
+ interrupted: true,
+ finalized: false,
+ stillActive: true,
+ waitedMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS - 1,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ }),
+ );
+ assert.isTrue(
+ openCode2ShouldForceInterruptFinalize({
+ interrupted: true,
+ finalized: false,
+ stillActive: true,
+ waitedMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ }),
+ );
+ assert.isFalse(
+ openCode2ShouldForceInterruptFinalize({
+ interrupted: true,
+ finalized: true,
+ stillActive: false,
+ waitedMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ }),
+ );
+ assert.isFalse(
+ openCode2ShouldForceInterruptFinalize({
+ interrupted: false,
+ finalized: false,
+ stillActive: true,
+ waitedMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ }),
+ );
+ });
+
+ it("resubscribes a stalled stream only while a turn is active", () => {
+ assert.isTrue(
+ openCode2ShouldResubscribeStalledStream({
+ sessionAborted: false,
+ hasActiveTurn: true,
+ lastEventAgeMs: OPENCODE2_EVENT_STALL_MS,
+ stallMs: OPENCODE2_EVENT_STALL_MS,
+ }),
+ );
+ assert.isFalse(
+ openCode2ShouldResubscribeStalledStream({
+ sessionAborted: false,
+ hasActiveTurn: true,
+ lastEventAgeMs: OPENCODE2_EVENT_STALL_MS - 1,
+ stallMs: OPENCODE2_EVENT_STALL_MS,
+ }),
+ );
+ assert.isFalse(
+ openCode2ShouldResubscribeStalledStream({
+ sessionAborted: false,
+ hasActiveTurn: false,
+ lastEventAgeMs: OPENCODE2_EVENT_STALL_MS,
+ stallMs: OPENCODE2_EVENT_STALL_MS,
+ }),
+ );
+ assert.isFalse(
+ openCode2ShouldResubscribeStalledStream({
+ sessionAborted: true,
+ hasActiveTurn: true,
+ lastEventAgeMs: OPENCODE2_EVENT_STALL_MS,
+ stallMs: OPENCODE2_EVENT_STALL_MS,
+ }),
+ );
+ });
+
+ it("adopts a missing execution start only after the turn has parts or interrupt", () => {
+ assert.isTrue(
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: true,
+ interrupted: false,
+ partCount: 0,
+ }),
+ );
+ assert.isTrue(
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: false,
+ interrupted: false,
+ partCount: 1,
+ }),
+ );
+ assert.isTrue(
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: false,
+ interrupted: true,
+ partCount: 0,
+ }),
+ );
+ assert.isFalse(
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: false,
+ interrupted: false,
+ partCount: 0,
+ }),
+ );
+ });
+});
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.test.ts
new file mode 100644
index 00000000000..40e803d8c12
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.test.ts
@@ -0,0 +1,372 @@
+import { assert, describe, it } from "@effect/vitest";
+import * as Cause from "effect/Cause";
+import * as Effect from "effect/Effect";
+import * as Exit from "effect/Exit";
+import * as Schema from "effect/Schema";
+
+import { OPENCODE2_PROVIDER } from "./OpenCode2AdapterV2.ts";
+import {
+ OPENCODE2_SDK_REPLAY_PROTOCOL,
+ OpenCode2ReplayController,
+ OpenCode2ReplayMismatchError,
+ makeReplayClient,
+ type OpenCode2SdkReplayTranscript,
+} from "./OpenCode2AdapterV2.testkit.ts";
+
+function transcript(entries: ReadonlyArray): OpenCode2SdkReplayTranscript {
+ return {
+ provider: OPENCODE2_PROVIDER,
+ protocol: OPENCODE2_SDK_REPLAY_PROTOCOL,
+ version: "test",
+ scenario: "replay-controller",
+ entries,
+ } as unknown as OpenCode2SdkReplayTranscript;
+}
+
+describe("OpenCode2AdapterV2 replay testkit", () => {
+ it.effect("fails deterministically when an SDK response is exhausted", () =>
+ Effect.gen(function* () {
+ const controller = new OpenCode2ReplayController(transcript([]));
+ const isReplayMismatchError = Schema.is(OpenCode2ReplayMismatchError);
+ const exit = yield* Effect.exit(
+ Effect.tryPromise({
+ try: () => controller.response("session.get"),
+ catch: (cause) =>
+ isReplayMismatchError(cause)
+ ? cause
+ : new OpenCode2ReplayMismatchError({
+ scenario: "replay-controller",
+ cursor: 0,
+ expected: { type: "sdk.response", operation: "session.get" },
+ actual: cause,
+ }),
+ }),
+ );
+
+ assert.isTrue(Exit.isFailure(exit));
+ if (Exit.isFailure(exit)) {
+ assert.isTrue(isReplayMismatchError(Cause.squash(exit.cause)));
+ }
+ }),
+ );
+
+ it.effect("claims distinct delayed responses for concurrent SDK requests", () =>
+ Effect.gen(function* () {
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.response",
+ operation: "session.get",
+ data: { id: "first" },
+ },
+ afterMs: 1,
+ },
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.response",
+ operation: "session.get",
+ data: { id: "second" },
+ },
+ },
+ ]),
+ );
+
+ const [first, second] = yield* Effect.promise(() =>
+ Promise.all([controller.response("session.get"), controller.response("session.get")]),
+ );
+
+ assert.deepStrictEqual(first, { id: "first" });
+ assert.deepStrictEqual(second, { id: "second" });
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("keeps a delayed SDK error claimed until its replay delay elapses", () =>
+ Effect.gen(function* () {
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.error",
+ operation: "session.get",
+ message: "delayed failure",
+ },
+ afterMs: 20,
+ },
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.response",
+ operation: "session.get",
+ data: { id: "after-error" },
+ },
+ },
+ ]),
+ );
+
+ const failure = controller.response("session.get");
+ const earlyState = yield* Effect.promise(() =>
+ Promise.race([
+ failure.then(
+ () => "settled",
+ () => "settled",
+ ),
+ Promise.resolve("pending"),
+ ]),
+ );
+ assert.strictEqual(earlyState, "pending");
+
+ const [failed, response] = yield* Effect.promise(() =>
+ Promise.all([
+ failure.then(
+ () => false,
+ () => true,
+ ),
+ controller.response("session.get"),
+ ]),
+ );
+ assert.isTrue(failed);
+ assert.deepStrictEqual(response, { id: "after-error" });
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("claims distinct delayed events for concurrent subscribers", () =>
+ Effect.gen(function* () {
+ const firstEvent = {
+ type: "session.created",
+ data: { id: "ses_first" },
+ };
+ const secondEvent = {
+ type: "session.created",
+ data: { id: "ses_second" },
+ };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.event", event: firstEvent },
+ afterMs: 1,
+ },
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.event", event: secondEvent },
+ },
+ { type: "runtime_exit", status: "success" },
+ ]),
+ );
+
+ const firstIterator = controller.events()[Symbol.asyncIterator]();
+ const secondIterator = controller.events()[Symbol.asyncIterator]();
+ const [first, second] = yield* Effect.promise(() =>
+ Promise.all([firstIterator.next(), secondIterator.next()]),
+ );
+
+ assert.deepStrictEqual(first.value, firstEvent);
+ assert.deepStrictEqual(second.value, secondEvent);
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("terminates every concurrent subscriber after a successful runtime exit", () =>
+ Effect.gen(function* () {
+ const controller = new OpenCode2ReplayController(
+ transcript([{ type: "runtime_exit", status: "success" }]),
+ );
+ const first = controller.events()[Symbol.asyncIterator]();
+ const second = controller.events()[Symbol.asyncIterator]();
+
+ const results = yield* Effect.promise(() => Promise.all([first.next(), second.next()]));
+
+ assert.isTrue(results[0].done === true);
+ assert.isTrue(results[1].done === true);
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("rejects outbound frames after another replay consumer poisons the controller", () =>
+ Effect.gen(function* () {
+ const outbound = { type: "session.get", input: { sessionID: "ses_after_failure" } };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ { type: "runtime_exit", status: "failure" },
+ { type: "expect_outbound", frame: outbound },
+ ]),
+ );
+ const iterator = controller.events()[Symbol.asyncIterator]();
+ const failure = yield* Effect.promise(() => iterator.next().catch((cause) => cause));
+ const outboundFailure = yield* Effect.promise(() =>
+ controller.expectOutbound(outbound).catch((cause) => cause),
+ );
+
+ assert.strictEqual(outboundFailure, failure);
+ }),
+ );
+
+ it.effect("delivers a delayed response before the following event", () =>
+ Effect.gen(function* () {
+ const event = {
+ type: "session.created",
+ data: { id: "ses_after_response" },
+ };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.response",
+ operation: "session.get",
+ data: { id: "response" },
+ },
+ afterMs: 1,
+ },
+ { type: "emit_inbound", frame: { type: "sdk.event", event } },
+ { type: "runtime_exit", status: "success" },
+ ]),
+ );
+ const deliveryOrder: Array = [];
+ const iterator = controller.events()[Symbol.asyncIterator]();
+ const response = controller.response("session.get").then((value) => {
+ deliveryOrder.push("response");
+ return value;
+ });
+ const inbound = iterator.next().then((value) => {
+ deliveryOrder.push("event");
+ return value;
+ });
+
+ const [responseValue, eventValue] = yield* Effect.promise(() =>
+ Promise.all([response, inbound]),
+ );
+
+ assert.deepStrictEqual(responseValue, { id: "response" });
+ assert.deepStrictEqual(eventValue.value, event);
+ assert.deepStrictEqual(deliveryOrder, ["response", "event"]);
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("delivers a delayed event before the following outbound request", () =>
+ Effect.gen(function* () {
+ const event = {
+ type: "session.created",
+ data: { id: "ses_before_request" },
+ };
+ const outbound = { type: "session.get", input: { sessionID: "ses_before_request" } };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.event", event },
+ afterMs: 1,
+ },
+ { type: "expect_outbound", frame: outbound },
+ { type: "runtime_exit", status: "success" },
+ ]),
+ );
+ const iterator = controller.events()[Symbol.asyncIterator]();
+ const inbound = iterator.next();
+ const request = controller.expectOutbound(outbound);
+
+ const [eventValue] = yield* Effect.promise(() => Promise.all([inbound, request]));
+
+ assert.deepStrictEqual(eventValue.value, event);
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("does not consume a delayed event after subscriber abort", () =>
+ Effect.gen(function* () {
+ const event = {
+ type: "session.created",
+ data: { id: "ses_replayed" },
+ };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.event", event },
+ afterMs: 1,
+ },
+ { type: "runtime_exit", status: "success" },
+ ]),
+ );
+ const abortController = new AbortController();
+ const abortedIterator = controller.events(abortController.signal)[Symbol.asyncIterator]();
+ const abortedNext = abortedIterator.next();
+ const activeIterator = controller.events()[Symbol.asyncIterator]();
+ const replayedNext = activeIterator.next();
+
+ abortController.abort();
+ const [aborted, replayed] = yield* Effect.promise(() =>
+ Promise.all([abortedNext, replayedNext]),
+ );
+ assert.isTrue(aborted.done === true);
+ assert.isFalse(replayed.done === true);
+ assert.deepStrictEqual(replayed.value, event);
+ controller.assertComplete();
+ }),
+ );
+
+ it.effect("routes every adapter replay client operation through the transcript", () =>
+ Effect.gen(function* () {
+ const agentInput = { location: { directory: "/workspace" } };
+ const mcpInput = { location: { directory: "/workspace" } };
+ const instructionsInput = {
+ sessionID: "ses_1",
+ key: "t3-instructions",
+ value: { prompt: "Use the T3 tools." },
+ };
+ const controller = new OpenCode2ReplayController(
+ transcript([
+ { type: "expect_outbound", frame: { type: "agent.list", input: agentInput } },
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.response", operation: "agent.list", data: [] },
+ },
+ { type: "expect_outbound", frame: { type: "mcp.list", input: mcpInput } },
+ {
+ type: "emit_inbound",
+ frame: { type: "sdk.response", operation: "mcp.list", data: [] },
+ },
+ {
+ type: "expect_outbound",
+ frame: { type: "session.instructions.entry.put", input: instructionsInput },
+ },
+ {
+ type: "emit_inbound",
+ frame: {
+ type: "sdk.response",
+ operation: "session.instructions.entry.put",
+ data: {},
+ },
+ },
+ ]),
+ );
+ const client = makeReplayClient(controller);
+
+ yield* Effect.promise(async () => {
+ // makeReplayClient is a structural double; cast past Session3 surface.
+ const replay = client as unknown as {
+ v2: {
+ agent: { list: (input: unknown) => Promise };
+ mcp: { list: (input: unknown) => Promise };
+ session: {
+ instructions: {
+ entry: { put: (input: unknown) => Promise };
+ };
+ };
+ };
+ };
+ await replay.v2.agent.list(agentInput);
+ await replay.v2.mcp.list(mcpInput);
+ await replay.v2.session.instructions.entry.put(instructionsInput);
+ });
+ controller.assertComplete();
+ }),
+ );
+});
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts
new file mode 100644
index 00000000000..8fc0f7b729c
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts
@@ -0,0 +1,563 @@
+import type { OpencodeClient, V2Event } from "@opencode-ai/sdk-next/v2";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import {
+ ProviderInstanceId,
+ ProviderReplayEntry,
+ type ProviderReplayTranscript,
+} from "@t3tools/contracts";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as P from "effect/Predicate";
+import * as Schema from "effect/Schema";
+
+import { ServerConfig } from "../../config.ts";
+import {
+ NoOpProviderEventLoggers,
+ ProviderEventLoggers,
+} from "../../provider/Layers/ProviderEventLoggers.ts";
+import {
+ OpenCode2Runtime,
+ OpenCode2RuntimeError,
+ type OpenCode2RuntimeOperation,
+} from "../../provider/opencode2Runtime.ts";
+import { layer as idAllocatorLayer } from "../IdAllocator.ts";
+import { ProviderAdapterDriverCreateError } from "../ProviderAdapterDriver.ts";
+import { makeDriverLayer as makeProviderAdapterRegistryDriverLayer } from "../ProviderAdapterRegistry.ts";
+import {
+ makeReplayServerConfig,
+ type OrchestratorV2ProviderReplayHarness,
+} from "../testkit/ProviderReplayHarness.ts";
+import {
+ OPENCODE2_DRIVER_KIND,
+ OPENCODE2_PROVIDER,
+ OPENCODE2_SDK_PROTOCOL,
+ OpenCode2AdapterV2Driver,
+} from "./OpenCode2AdapterV2.ts";
+
+export const OPENCODE2_SDK_REPLAY_PROTOCOL = OPENCODE2_SDK_PROTOCOL;
+export const OPENCODE2_REPLAY_INSTANCE_ID = ProviderInstanceId.make("opencode2");
+
+const OpenCode2SdkReplayTranscript = Schema.Struct({
+ provider: Schema.Literal(OPENCODE2_PROVIDER),
+ protocol: Schema.Literal(OPENCODE2_SDK_REPLAY_PROTOCOL),
+ version: Schema.String,
+ scenario: Schema.String,
+ metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+ entries: Schema.Array(ProviderReplayEntry),
+});
+export type OpenCode2SdkReplayTranscript = typeof OpenCode2SdkReplayTranscript.Type;
+const decodeOpenCode2SdkReplayTranscript = Schema.decodeUnknownEffect(OpenCode2SdkReplayTranscript);
+
+export class OpenCode2ReplayTranscriptDecodeError extends Schema.TaggedErrorClass()(
+ "OpenCode2ReplayTranscriptDecodeError",
+ {
+ driver: Schema.optional(Schema.String),
+ protocol: Schema.optional(Schema.String),
+ scenario: Schema.optional(Schema.String),
+ cause: Schema.Defect(),
+ },
+) {
+ override get message(): string {
+ return `Failed to decode OpenCode 2 replay transcript for scenario ${this.scenario ?? ""}.`;
+ }
+}
+
+export class OpenCode2ReplayMismatchError extends Schema.TaggedErrorClass()(
+ "OpenCode2ReplayMismatchError",
+ {
+ scenario: Schema.String,
+ cursor: Schema.Number,
+ expected: Schema.Unknown,
+ actual: Schema.Unknown,
+ },
+) {
+ override get message(): string {
+ return `OpenCode 2 replay frame mismatch at cursor ${this.cursor} in scenario ${this.scenario}.`;
+ }
+}
+
+export class OpenCode2ReplayIncompleteError extends Schema.TaggedErrorClass()(
+ "OpenCode2ReplayIncompleteError",
+ {
+ scenario: Schema.String,
+ cursor: Schema.Number,
+ remaining: Schema.Number,
+ },
+) {
+ override get message(): string {
+ return `OpenCode 2 replay ended with ${this.remaining} unconsumed entries in scenario ${this.scenario}.`;
+ }
+}
+
+export const OpenCode2ReplayError = Schema.Union([
+ OpenCode2ReplayTranscriptDecodeError,
+ OpenCode2ReplayMismatchError,
+ OpenCode2ReplayIncompleteError,
+]);
+export type OpenCode2ReplayError = typeof OpenCode2ReplayError.Type;
+export const OpenCode2OrchestratorReplayHarnessError = Schema.Union([
+ OpenCode2ReplayError,
+ ProviderAdapterDriverCreateError,
+]);
+export type OpenCode2OrchestratorReplayHarnessError =
+ typeof OpenCode2OrchestratorReplayHarnessError.Type;
+
+function replayValueMatches(expected: unknown, actual: unknown): boolean {
+ if (expected === "" || expected === "") return true;
+ if (Array.isArray(expected)) {
+ return (
+ Array.isArray(actual) &&
+ expected.length === actual.length &&
+ expected.every((entry, index) => replayValueMatches(entry, actual[index]))
+ );
+ }
+ if (P.isObject(expected)) {
+ if (!P.isObject(actual)) return false;
+ return Object.entries(expected).every(([key, value]) => replayValueMatches(value, actual[key]));
+ }
+ return Object.is(expected, actual);
+}
+
+function frameRecord(frame: unknown): Record | null {
+ return P.isObject(frame) ? frame : null;
+}
+
+function isSignalAborted(signal?: AbortSignal): boolean {
+ return signal?.aborted === true;
+}
+
+async function waitForReplayDelay(afterMs: number, signal?: AbortSignal): Promise {
+ if (isSignalAborted(signal)) return false;
+ if (signal === undefined) {
+ await Effect.runPromise(Effect.sleep(Duration.millis(afterMs)));
+ return true;
+ }
+ return new Promise((resolve) => {
+ let settled = false;
+ const done = (completed: boolean) => {
+ if (settled) return;
+ settled = true;
+ signal.removeEventListener("abort", abort);
+ resolve(completed);
+ };
+ const abort = () => done(false);
+ signal.addEventListener("abort", abort, { once: true });
+ void Effect.runPromise(Effect.sleep(Duration.millis(afterMs))).then(() => done(true));
+ if (signal.aborted) abort();
+ });
+}
+
+export class OpenCode2ReplayController {
+ private cursor = 0;
+ private claimedEventCursor: number | null = null;
+ private claimedResponseCursor: number | null = null;
+ private successfulRuntimeExit = false;
+ private readonly waiters = new Set<() => void>();
+ private failure: unknown = null;
+ private readonly transcript: OpenCode2SdkReplayTranscript;
+
+ constructor(transcript: OpenCode2SdkReplayTranscript) {
+ this.transcript = transcript;
+ }
+
+ /** Non-consuming look at the next transcript entry. */
+ peek(): OpenCode2SdkReplayTranscript["entries"][number] | undefined {
+ return this.transcript.entries[this.cursor];
+ }
+
+ /**
+ * True when the next entry is an outbound expect for this operation. Used so
+ * optional startup catalog probes can fall back to canned data when a
+ * fixture does not record them, without racing the event stream.
+ */
+ expectsOutbound(operation: string): boolean {
+ const entry = this.peek();
+ if (entry?.type !== "expect_outbound") return false;
+ const frame = entry.frame;
+ return (
+ typeof frame === "object" &&
+ frame !== null &&
+ "type" in frame &&
+ (frame as { readonly type?: string }).type === operation
+ );
+ }
+
+ async expectOutbound(actual: unknown): Promise {
+ try {
+ this.throwFailure();
+ while (
+ this.claimedEventCursor === this.cursor ||
+ this.claimedResponseCursor === this.cursor
+ ) {
+ await this.changed();
+ this.throwFailure();
+ }
+ const entry = this.transcript.entries[this.cursor];
+ if (entry?.type !== "expect_outbound" || !replayValueMatches(entry.frame, actual)) {
+ throw new OpenCode2ReplayMismatchError({
+ scenario: this.transcript.scenario,
+ cursor: this.cursor,
+ expected: entry?.type === "expect_outbound" ? entry.frame : (entry ?? null),
+ actual,
+ });
+ }
+ this.advance();
+ } catch (cause) {
+ this.fail(cause);
+ throw cause;
+ }
+ }
+
+ async response(operation: OpenCode2RuntimeOperation): Promise {
+ while (true) {
+ this.throwFailure();
+ if (this.claimedResponseCursor === this.cursor) {
+ await this.changed();
+ continue;
+ }
+ const entry = this.transcript.entries[this.cursor];
+ if (entry === undefined) {
+ const mismatch = new OpenCode2ReplayMismatchError({
+ scenario: this.transcript.scenario,
+ cursor: this.cursor,
+ expected: { type: "sdk.response", operation },
+ actual: null,
+ });
+ this.fail(mismatch);
+ throw mismatch;
+ }
+ if (entry?.type === "emit_inbound") {
+ const frame = frameRecord(entry.frame);
+ if (frame?.type === "sdk.response" && frame.operation === operation) {
+ const data = frame.data;
+ const claimedCursor = this.cursor;
+ this.claimedResponseCursor = claimedCursor;
+ try {
+ if (entry.afterMs !== undefined && entry.afterMs > 0) {
+ await Effect.runPromise(Effect.sleep(Duration.millis(entry.afterMs)));
+ }
+ this.throwFailure();
+ this.advance();
+ return data;
+ } finally {
+ this.releaseResponseClaim(claimedCursor);
+ }
+ }
+ if (frame?.type === "sdk.error" && frame.operation === operation) {
+ const claimedCursor = this.cursor;
+ this.claimedResponseCursor = claimedCursor;
+ try {
+ if (entry.afterMs !== undefined && entry.afterMs > 0) {
+ await Effect.runPromise(Effect.sleep(Duration.millis(entry.afterMs)));
+ }
+ this.throwFailure();
+ this.advance();
+ throw new OpenCode2RuntimeError({
+ operation,
+ category: "sdk-request-failed",
+ cause: frame.error ?? frame.message,
+ });
+ } finally {
+ this.releaseResponseClaim(claimedCursor);
+ }
+ }
+ }
+ if (entry?.type === "runtime_exit") {
+ const mismatch = new OpenCode2ReplayMismatchError({
+ scenario: this.transcript.scenario,
+ cursor: this.cursor,
+ expected: { type: "sdk.response", operation },
+ actual: entry,
+ });
+ this.fail(mismatch);
+ throw mismatch;
+ }
+ await this.changed();
+ }
+ }
+
+ async *events(signal?: AbortSignal): AsyncIterable {
+ while (true) {
+ if (isSignalAborted(signal)) return;
+ this.throwFailure();
+ if (this.successfulRuntimeExit) return;
+ if (this.claimedEventCursor === this.cursor || this.claimedResponseCursor === this.cursor) {
+ await this.changed(signal);
+ continue;
+ }
+ const entry = this.transcript.entries[this.cursor];
+ if (entry?.type === "emit_inbound") {
+ const frame = frameRecord(entry.frame);
+ if (frame?.type === "sdk.event") {
+ const claimedCursor = this.cursor;
+ this.claimedEventCursor = claimedCursor;
+ try {
+ if (entry.afterMs !== undefined && entry.afterMs > 0) {
+ const delayCompleted = await waitForReplayDelay(entry.afterMs, signal);
+ if (!delayCompleted || isSignalAborted(signal)) return;
+ }
+ this.throwFailure();
+ const event = frame.event as V2Event;
+ this.advance();
+ this.releaseEventClaim(claimedCursor);
+ yield event;
+ continue;
+ } finally {
+ this.releaseEventClaim(claimedCursor);
+ }
+ }
+ }
+ if (entry?.type === "runtime_exit") {
+ if (entry.status === "success") {
+ this.successfulRuntimeExit = true;
+ this.advance();
+ return;
+ }
+ this.advance();
+ const mismatch = new OpenCode2ReplayMismatchError({
+ scenario: this.transcript.scenario,
+ cursor: this.cursor - 1,
+ expected: { status: "success" },
+ actual: entry,
+ });
+ this.fail(mismatch);
+ throw mismatch;
+ }
+ await this.changed(signal);
+ }
+ }
+
+ assertComplete(): void {
+ while (this.transcript.entries[this.cursor]?.type === "runtime_exit") {
+ const exit = this.transcript.entries[this.cursor];
+ if (exit?.type !== "runtime_exit" || exit.status !== "success") break;
+ this.successfulRuntimeExit = true;
+ this.advance();
+ }
+ this.throwFailure();
+ if (this.cursor !== this.transcript.entries.length) {
+ throw new OpenCode2ReplayIncompleteError({
+ scenario: this.transcript.scenario,
+ cursor: this.cursor,
+ remaining: this.transcript.entries.length - this.cursor,
+ });
+ }
+ }
+
+ private advance(): void {
+ this.cursor += 1;
+ this.notifyWaiters();
+ }
+
+ private releaseEventClaim(cursor: number): void {
+ if (this.claimedEventCursor !== cursor) return;
+ this.claimedEventCursor = null;
+ this.notifyWaiters();
+ }
+
+ private releaseResponseClaim(cursor: number): void {
+ if (this.claimedResponseCursor !== cursor) return;
+ this.claimedResponseCursor = null;
+ this.notifyWaiters();
+ }
+
+ private notifyWaiters(): void {
+ for (const waiter of this.waiters) waiter();
+ this.waiters.clear();
+ }
+
+ private fail(cause: unknown): void {
+ this.failure = cause;
+ this.notifyWaiters();
+ }
+
+ private throwFailure(): void {
+ if (this.failure !== null) throw this.failure;
+ }
+
+ private changed(signal?: AbortSignal): Promise {
+ if (signal?.aborted === true) return Promise.resolve();
+ return new Promise((resolve) => {
+ const done = () => {
+ signal?.removeEventListener("abort", done);
+ this.waiters.delete(done);
+ resolve();
+ };
+ this.waiters.add(done);
+ signal?.addEventListener("abort", done, { once: true });
+ });
+ }
+}
+
+export function makeReplayClient(controller: OpenCode2ReplayController): OpencodeClient {
+ const request = async (operation: OpenCode2RuntimeOperation, input: unknown) => {
+ await controller.expectOutbound({ type: operation, input });
+ return { data: { data: await controller.response(operation) } };
+ };
+ /**
+ * Catalog probes may run at openSession/ensureThread before the transcript
+ * records them. Prefer the transcript when present; otherwise return canned
+ * data so event.subscribe stays first and fixtures do not deadlock.
+ */
+ const optionalCatalog = async (
+ operation: OpenCode2RuntimeOperation,
+ input: unknown,
+ canned: unknown,
+ ) => {
+ if (!controller.expectsOutbound(operation)) {
+ return { data: { data: canned } };
+ }
+ return request(operation, input);
+ };
+ return {
+ v2: {
+ agent: {
+ list: (input: unknown) =>
+ optionalCatalog("agent.list", input, [{ id: "build" }, { id: "plan" }]),
+ },
+ event: {
+ subscribe: async (options?: { readonly signal?: AbortSignal }) => {
+ await controller.expectOutbound({ type: "event.subscribe" });
+ return { stream: controller.events(options?.signal) };
+ },
+ },
+ message: {
+ list: (input: unknown) => request("message.list", input),
+ },
+ mcp: {
+ list: (input: unknown) => optionalCatalog("mcp.list", input, []),
+ },
+ model: {
+ list: (input: unknown) => optionalCatalog("model.list", input, []),
+ },
+ session: {
+ context: (input: unknown) => request("session.context", input),
+ create: (input: unknown) => request("session.create", input),
+ fork: (input: unknown) => request("session.fork", input),
+ get: (input: unknown) => request("session.get", input),
+ interrupt: (input: unknown) => request("session.interrupt", input),
+ instructions: {
+ entry: {
+ put: (input: unknown) => request("session.instructions.entry.put", input),
+ },
+ },
+ // Beta Session3 projects messages under session.messages; older
+ // transcripts still label the operation message.list.
+ messages: (input: unknown) => request("message.list", input),
+ pending: {
+ list: (input: unknown) => request("session.pending.list", input),
+ },
+ permission: {
+ reply: (input: unknown) => request("session.permission.reply", input),
+ },
+ prompt: (input: unknown) => request("session.prompt", input),
+ remove: (input: unknown) => request("session.remove", input),
+ question: {
+ reply: (input: unknown) => request("session.question.reply", input),
+ },
+ revert: {
+ commit: (input: unknown) => request("session.revert.commit", input),
+ stage: (input: unknown) => request("session.revert.stage", input),
+ },
+ switchAgent: (input: unknown) => request("session.switchAgent", input),
+ switchModel: (input: unknown) => request("session.switchModel", input),
+ wait: (input: unknown) => request("session.wait", input),
+ },
+ shell: {
+ list: (input: unknown) => request("shell.list", input),
+ output: (input: unknown) => request("shell.output", input),
+ remove: (input: unknown) => request("shell.remove", input),
+ },
+ },
+ } as unknown as OpencodeClient;
+}
+
+function makeOpenCode2ReplayRuntimeLayer(transcript: OpenCode2SdkReplayTranscript) {
+ return Layer.effect(
+ OpenCode2Runtime,
+ Effect.gen(function* () {
+ const controller = new OpenCode2ReplayController(transcript);
+ yield* Effect.addFinalizer(() =>
+ Effect.sync(() => {
+ controller.assertComplete();
+ }),
+ );
+ const client = makeReplayClient(controller);
+ return OpenCode2Runtime.of({
+ startOpenCode2ServerProcess: () =>
+ Effect.fail(
+ new OpenCode2RuntimeError({
+ operation: "startOpenCode2ServerProcess",
+ category: "replay-boundary",
+ }),
+ ),
+ connectToOpenCode2Server: () =>
+ Effect.succeed({
+ url: "replay://opencode2",
+ password: "replay-password",
+ exitCode: null,
+ external: true,
+ }),
+ createOpenCode2SdkClient: () => client,
+ } satisfies OpenCode2Runtime["Service"]);
+ }),
+ );
+}
+
+export function makeOpenCode2ProviderAdapterRegistryReplayLayer(
+ transcript: OpenCode2SdkReplayTranscript,
+) {
+ const serverConfigLayer = Layer.effect(
+ ServerConfig,
+ makeReplayServerConfig(transcript.scenario).pipe(Effect.orDie),
+ ).pipe(Layer.provide(NodeServices.layer));
+ return makeProviderAdapterRegistryDriverLayer({
+ drivers: [OpenCode2AdapterV2Driver],
+ configMap: {
+ [OPENCODE2_REPLAY_INSTANCE_ID]: {
+ driver: OPENCODE2_DRIVER_KIND,
+ config: {
+ serverUrl: "replay://opencode2",
+ serverPassword: "replay-password",
+ },
+ },
+ },
+ }).pipe(
+ Layer.provide(
+ Layer.mergeAll(
+ makeOpenCode2ReplayRuntimeLayer(transcript),
+ serverConfigLayer,
+ NodeServices.layer,
+ idAllocatorLayer,
+ Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers),
+ ),
+ ),
+ );
+}
+
+function transcriptMetadata(transcript: ProviderReplayTranscript) {
+ return {
+ driver: transcript.provider,
+ protocol: transcript.protocol,
+ scenario: transcript.scenario,
+ };
+}
+
+export const OpenCode2OrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
+ OpenCode2SdkReplayTranscript,
+ OpenCode2OrchestratorReplayHarnessError
+> = {
+ driver: OPENCODE2_PROVIDER,
+ decodeTranscript: (transcript) =>
+ decodeOpenCode2SdkReplayTranscript(transcript).pipe(
+ Effect.mapError(
+ (cause) =>
+ new OpenCode2ReplayTranscriptDecodeError({
+ ...transcriptMetadata(transcript),
+ cause,
+ }),
+ ),
+ ),
+ makeProviderAdapterRegistryLayer: makeOpenCode2ProviderAdapterRegistryReplayLayer,
+};
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
new file mode 100644
index 00000000000..b94f9749158
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
@@ -0,0 +1,5442 @@
+/**
+ * OpenCode 2.x ("OpenCode 2") orchestration adapter.
+ *
+ * A separate adapter rather than a mode of `OpenCodeAdapterV2`: 2.x shares the
+ * vendor name and the tool vocabulary with 1.x and nothing else. Concretely,
+ *
+ * - the wire surface is `/api/*` only, reached through `client.v2.*`;
+ * - every response is double-wrapped, `{ data: { data: … } }`, because the
+ * SDK's own `.data` is the parsed body and the body carries its own
+ * envelope;
+ * - the event vocabulary is a flat stream of typed lifecycle events
+ * (`session.next.*` steps, tools, text) rather than 1.x's
+ * `message.part.updated` carrying a whole part object;
+ * - the model binds at session create via `ModelRef`, not per prompt;
+ * - permission asks can still arrive under the legacy `permission.asked`
+ * name, but replies always use the `/api` session-scoped
+ * `client.v2.session.*` routes. Self-spawned full-access servers also get
+ * `permission: "allow"` injected into `OPENCODE_CONFIG_CONTENT` at spawn.
+ *
+ * `live-scenarios/tests/opencode2-drive-probe.mjs` in the parent workspace is
+ * the executable statement of this contract against a real binary.
+ *
+ * Event stream durability: OpenCode 2 documents `/api/event` as volatile (a
+ * slow consumer overflows and fails the stream). This adapter keeps protocol
+ * logging off the pull path, resubscribes after stream failure or a stall
+ * while a turn is active, and force-finalizes on Stop when the interrupt
+ * terminal event never arrives.
+ *
+ * @module orchestration-v2/Adapters/OpenCode2AdapterV2
+ */
+import type {
+ AgentV2Info,
+ ModelV2Info,
+ PromptInputFileAttachment,
+ QuestionV2Info,
+ SessionMessage,
+ SessionV2Info,
+ V2Event,
+} from "@opencode-ai/sdk-next/v2";
+import {
+ normalizeOpenCode2WireType,
+ openCode2StepFinishSettlesTurn,
+ openCode2WireCallID,
+ openCode2WireCreatedMs,
+ openCode2WireData,
+ openCode2WireErrorMessage,
+ openCode2WireSessionID,
+ openCode2WireToolName,
+ unwrapOpenCode2Payload,
+} from "./openCode2Wire.ts";
+
+/** Local shims for types dropped or renamed in the beta SDK generation. */
+type AgentInfoV2 = AgentV2Info;
+type ModelInfo = ModelV2Info;
+type SessionInfoV2 = SessionV2Info;
+type SessionMessageInfo = SessionMessage;
+type SessionPendingInfo = {
+ readonly sessionID: string;
+ readonly type?: string;
+ readonly id?: string;
+};
+type ShellInfoV2 = {
+ readonly id: string;
+ readonly status: "running" | "exited" | "timeout" | "killed" | string;
+ readonly command?: string;
+ readonly exit?: number;
+ readonly metadata: { readonly sessionID?: string; readonly [key: string]: unknown };
+ readonly time?: { readonly started?: number; readonly completed?: number };
+};
+type McpServer = {
+ readonly name: string;
+ readonly status: { readonly status?: string } | string;
+};
+function mcpServerStatus(server: McpServer): string {
+ return typeof server.status === "string" ? server.status : (server.status?.status ?? "missing");
+}
+type WireEvent = {
+ readonly type: string;
+ readonly id?: string;
+ readonly created?: number;
+ readonly data?: unknown;
+};
+import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
+import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
+import { causeErrorTag } from "@t3tools/shared/observability";
+import {
+ type ChatAttachment,
+ type ModelSelection,
+ type OpenCode2Settings,
+ OpenCode2Settings as OpenCode2SettingsSchema,
+ type OrchestrationV2AppThread,
+ type OrchestrationV2ConversationMessage,
+ type OrchestrationV2ExecutionNode,
+ type OrchestrationV2ProviderCapabilities,
+ type OrchestrationV2ProviderFailure,
+ type OrchestrationV2ProviderRef,
+ type OrchestrationV2ProviderRetry,
+ type OrchestrationV2ProviderSession,
+ type OrchestrationV2ProviderThread,
+ type OrchestrationV2ProviderTurn,
+ type OrchestrationV2RuntimeRequest,
+ type OrchestrationV2Subagent,
+ type OrchestrationV2TurnItem,
+ ProviderDriverKind,
+ type ProviderInstanceId,
+ type ProviderInteractionMode,
+ type ProviderRequestKind,
+ type ProviderSessionId,
+ type RuntimeRequestId,
+ type ThreadId,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
+import * as Clock from "effect/Clock";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as Exit from "effect/Exit";
+import * as Fiber from "effect/Fiber";
+import * as Option from "effect/Option";
+import * as Queue from "effect/Queue";
+import * as Schema from "effect/Schema";
+import * as Scope from "effect/Scope";
+import * as Stream from "effect/Stream";
+import * as NodeURL from "node:url";
+
+import { resolveAttachmentPath } from "../../attachmentStore.ts";
+import { ServerConfig } from "../../config.ts";
+import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
+import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";
+import { ProviderEventLoggers } from "../../provider/Layers/ProviderEventLoggers.ts";
+import {
+ structuralProtocolMethod,
+ summarizeNativeProtocolPayload,
+} from "../../provider/NativeProtocolLogging.ts";
+import {
+ normalizeOpenCode2Variant,
+ OPENCODE2_AUTO_AGENT,
+ OpenCode2Runtime,
+ OpenCode2RuntimeError,
+ runOpenCode2Sdk,
+ type OpenCode2RuntimeOperation,
+} from "../../provider/opencode2Runtime.ts";
+import {
+ openCodeRuntimeErrorDetail,
+ parseOpenCodeModelSlug,
+} from "../../provider/opencodeRuntime.ts";
+import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts";
+import { applyOpenCode2ProviderEnvironment } from "../../provider/OpenCode2ProviderEnvironment.ts";
+import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts";
+import { makeProviderFailure, makeProviderFailureTurnItem } from "../ProviderFailure.ts";
+import {
+ type ProviderContinuationRequest,
+ ProviderContinuationRequests,
+} from "../ProviderContinuationRequests.ts";
+import {
+ type ProviderInteractionModeReflection,
+ ProviderInteractionModeReflections,
+} from "../ProviderInteractionModeReflections.ts";
+import { turnScopedSelectionTransition } from "../ProviderSelectionTransition.ts";
+import {
+ makeSubagentChildThread,
+ makeSubagentConversationArtifacts,
+ subagentThreadTitle,
+} from "../SubagentProjection.ts";
+import {
+ ProviderAdapterEnsureThreadError,
+ ProviderAdapterForkThreadError,
+ ProviderAdapterInterruptError,
+ ProviderAdapterOpenSessionError,
+ ProviderAdapterProtocolError,
+ ProviderAdapterReadThreadSnapshotError,
+ ProviderAdapterResumeThreadError,
+ ProviderAdapterRollbackThreadError,
+ ProviderAdapterRuntimeRequestResponseError,
+ ProviderAdapterSteerRunError,
+ ProviderAdapterTurnStartError,
+ ProviderAdapterV2,
+ type ProviderAdapterV2Event,
+ type ProviderAdapterV2OpenSessionInput,
+ type ProviderAdapterV2SessionRuntime,
+ type ProviderAdapterV2Shape,
+ type ProviderAdapterV2TurnInput,
+} from "../ProviderAdapter.ts";
+import {
+ ProviderAdapterDriverCreateError,
+ type ProviderAdapterDriver,
+ type ProviderAdapterDriverCreateInput,
+} from "../ProviderAdapterDriver.ts";
+// Tool names, permission actions, and the terminal-status mapping are the one
+// thing 1.x and 2.x genuinely share, so these classifiers stay in one place
+// rather than drifting between two copies.
+import {
+ openCodeBoundaryAfterProviderTurn,
+ openCodePermissionRequestKind,
+ openCodePermissionRules,
+ openCodeToolProjectionKind,
+ terminalToolStatus,
+} from "./OpenCodeAdapterV2.ts";
+
+export const OPENCODE2_PROVIDER = ProviderDriverKind.make("opencode2");
+export const OPENCODE2_DRIVER_KIND = OPENCODE2_PROVIDER;
+export const OPENCODE2_SDK_PROTOCOL = "opencode2-sdk.sse" as const;
+export const OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT = 16;
+export const OPENCODE2_PROMOTED_INPUT_ID_LIMIT = 64;
+/**
+ * OpenCode 2 documents `/api/event` as volatile: a slow consumer overflows and
+ * fails the stream. The adapter must keep the pull path hot and resubscribe.
+ */
+export const OPENCODE2_EVENT_STALL_MS = 30_000;
+export const OPENCODE2_EVENT_STALL_CHECK_MS = 5_000;
+export const OPENCODE2_EVENT_STREAM_MAX_FAILURES = 5;
+/** Cap stall-driven resubscribes so a stuck turn cannot thrash subscribe forever. */
+export const OPENCODE2_EVENT_STALL_MAX_RESUBSCRIBES = 2;
+export const OPENCODE2_EVENT_RESUBSCRIBE_DELAY_MS = 250;
+/** Bound Stop so a wedged `session.interrupt` HTTP call cannot hang the UI. */
+export const OPENCODE2_INTERRUPT_REQUEST_TIMEOUT_MS = 5_000;
+/**
+ * After interrupt is requested, wait this long for SSE
+ * `session.execution.interrupted` before force-finalizing the turn. Cursor uses
+ * the same pattern; without it a dead event stream leaves Stop inert.
+ */
+export const OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS = 5_000;
+export const OPENCODE2_INTERRUPT_SETTLE_POLL_MS = 100;
+const DEFAULT_OPENCODE2_SETTINGS = Schema.decodeSync(OpenCode2SettingsSchema)({});
+const OPENCODE2_T3_MCP_NAME = "t3-code";
+const OPENCODE2_T3_INSTRUCTION_KEY = "t3-code.orchestration";
+const OpenCode2InlineConfig = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));
+const OpenCode2McpConfig = Schema.Record(Schema.String, Schema.Unknown);
+const decodeOpenCode2InlineConfig = Schema.decodeUnknownEffect(OpenCode2InlineConfig);
+const decodeOpenCode2McpConfig = Schema.decodeUnknownEffect(OpenCode2McpConfig);
+const encodeOpenCode2InlineConfig = Schema.encodeEffect(OpenCode2InlineConfig);
+
+/**
+ * 2.x keeps 1.x's durable session/message identifiers and adds a durable
+ * execution, but it still exposes no first-class turn object: the admitted
+ * session input is the closest native correlation point, and the
+ * `session.execution.*` terminal events are the authoritative settle signal.
+ *
+ * Native subagent sessions expose their parent session through
+ * `session.created.info.parentID`. The parent subagent tool also reports the
+ * child session id in its progress or terminal metadata, so the adapter can
+ * project durable child lineage and route child requests through the root
+ * turn's runtime policy.
+ */
+export const OpenCode2ProviderCapabilitiesV2 = {
+ sessions: {
+ supportsMultipleProviderThreadsPerSession: false,
+ supportsModelSwitchInSession: true,
+ supportsProviderSwitchingViaHandoff: true,
+ supportsRuntimeModeSwitchInSession: false,
+ pendingRequestsSurviveRestart: false,
+ },
+ threads: {
+ canCreateEmptyThread: true,
+ canReadThreadSnapshot: true,
+ canRollbackThread: true,
+ canForkThread: true,
+ canForkFromTurn: true,
+ canForkFromSubagentThread: true,
+ exposesNativeThreadId: true,
+ },
+ turns: {
+ exposesNativeTurnId: false,
+ emitsTurnStarted: true,
+ emitsTurnCompleted: true,
+ supportsInterrupt: true,
+ supportsActiveSteering: true,
+ supportsSteeringByInterruptRestart: true,
+ supportsQueuedMessages: true,
+ terminalStatusQuality: "strong",
+ },
+ streaming: {
+ streamsAssistantText: true,
+ streamsReasoning: true,
+ streamsToolOutput: true,
+ streamsPlanText: false,
+ emitsMessageCompleted: true,
+ },
+ tools: {
+ exposesToolItemIds: true,
+ emitsToolStarted: true,
+ emitsToolCompleted: true,
+ emitsToolOutput: true,
+ supportsMcpTools: true,
+ supportsDynamicToolCallbacks: false,
+ },
+ approvals: {
+ supportsCommandApproval: true,
+ supportsFileReadApproval: true,
+ supportsFileChangeApproval: true,
+ supportsApplyPatchApproval: true,
+ approvalsHaveNativeRequestIds: true,
+ approvalCallbacksAreLiveOnly: true,
+ approvalsCanOriginateFromSubagents: true,
+ },
+ planning: {
+ emitsPlanUpdated: false,
+ emitsTodoList: false,
+ emitsProposedPlan: false,
+ supportsStructuredQuestions: true,
+ planDeltasHaveItemIds: false,
+ },
+ subagents: {
+ supportsSubagents: true,
+ exposesSubagentThreadIds: true,
+ emitsSubagentLifecycle: true,
+ canWaitForSubagents: false,
+ canCloseSubagents: false,
+ canForkSubagentThread: true,
+ },
+ context: {
+ acceptsSystemContext: false,
+ acceptsDeveloperContext: false,
+ acceptsSyntheticUserContext: true,
+ canGenerateSummaries: true,
+ canConsumeHandoffSummaries: true,
+ supportsDeltaHandoff: true,
+ supportsFullThreadHandoff: true,
+ maxRecommendedHandoffChars: null,
+ },
+ checkpointing: {
+ appCanCheckpointFilesystem: true,
+ supportsNestedCheckpointScopes: true,
+ providerCanRollbackConversation: true,
+ providerRollbackReturnsSnapshot: true,
+ providerCanReadConversationSnapshot: true,
+ },
+ identity: {
+ nativeThreadIds: "strong",
+ nativeTurnIds: "weak",
+ nativeItemIds: "strong",
+ nativeRequestIds: "strong",
+ },
+} satisfies OrchestrationV2ProviderCapabilities;
+
+type TerminalTurnStatus = Extract<
+ OrchestrationV2ProviderTurn["status"],
+ "completed" | "interrupted" | "failed" | "cancelled"
+>;
+
+type OpenCode2ToolStatus = "pending" | "running" | "completed" | "error";
+
+interface OpenCode2TextPart {
+ readonly kind: "text" | "reasoning";
+ readonly id: string;
+ readonly startedAt: DateTime.Utc;
+ text: string;
+ completed: boolean;
+}
+
+interface OpenCode2ToolPart {
+ readonly kind: "tool";
+ readonly id: string;
+ readonly callId: string;
+ readonly startedAt: DateTime.Utc;
+ name: string;
+ input: Record;
+ inputText: string;
+ output: string | undefined;
+ structured: Record | undefined;
+ status: OpenCode2ToolStatus;
+ errorMessage: string | undefined;
+ completedAt: DateTime.Utc | null;
+}
+
+interface OpenCode2ShellProjection {
+ readonly shellId: string;
+ readonly state: OpenCode2ThreadState;
+ readonly turn: ActiveOpenCode2Turn;
+ readonly part: OpenCode2ToolPart;
+ readonly location: SessionInfoV2["location"];
+ status: ShellInfoV2["status"];
+}
+
+interface OpenCode2Compaction {
+ readonly id: string;
+ readonly startedAt: DateTime.Utc;
+ summary: string;
+ status: "running" | "completed" | "failed" | "cancelled";
+ completedAt: DateTime.Utc | null;
+}
+
+type OpenCode2Part = OpenCode2TextPart | OpenCode2ToolPart;
+
+interface ActiveOpenCode2Turn {
+ readonly isRoot: boolean;
+ readonly providerBufferedContinuation: boolean;
+ readonly threadId: ThreadId;
+ readonly runId: OrchestrationV2ExecutionNode["runId"];
+ readonly rootNodeId: OrchestrationV2ExecutionNode["rootNodeId"];
+ readonly appThread: OrchestrationV2AppThread;
+ readonly modelSelection: ModelSelection;
+ readonly runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"];
+ readonly providerTurnId: OrchestrationV2ProviderTurn["id"];
+ readonly runOrdinal: number;
+ readonly startedAt: DateTime.Utc;
+ readonly itemOrdinals: Map;
+ readonly parts: Map;
+ readonly toolIdsByCallId: Map;
+ readonly providerTurn: OrchestrationV2ProviderTurn;
+ nextItemOrdinal: number;
+ nativeInputId: string | null;
+ activeCompaction: OpenCode2Compaction | null;
+ executionStarted: boolean;
+ interrupted: boolean;
+ finalized: boolean;
+ providerRetry: OpenCode2ProviderRetry | null;
+}
+
+interface OpenCode2SubagentContext {
+ readonly nativeItemId: string;
+ readonly nodeId: OrchestrationV2Subagent["id"];
+ readonly turnItemId: OrchestrationV2TurnItem["id"];
+ readonly parentState: OpenCode2ThreadState;
+ readonly parentTurn: ActiveOpenCode2Turn;
+ readonly startedAt: DateTime.Utc;
+ completedAt: DateTime.Utc | null;
+ prompt: string;
+ title: string | null;
+ model: string | null;
+ childSessionId: string | null;
+ childThreadId: ThreadId | null;
+ childProviderThreadId: OrchestrationV2ProviderThread["id"] | null;
+ status: OrchestrationV2Subagent["status"];
+ progress: string | undefined;
+ result: string | null;
+}
+
+interface OpenCode2ProviderRetry {
+ readonly retry: OrchestrationV2ProviderRetry;
+ readonly failure: OrchestrationV2ProviderFailure;
+ readonly startedAt: DateTime.Utc;
+}
+
+type OpenCode2PostSettleWakeDisposition = "replay" | "suppress";
+type OpenCode2PostSettleWakePhase = "pending" | "executing" | "ready";
+
+interface OpenCode2ExecutionOwnership {
+ readonly inputIds: Set;
+ claimedByPromotion: boolean;
+}
+
+interface OpenCode2EventHandlingContext {
+ readonly replayWakeInputId?: string;
+}
+
+interface OpenCode2PostSettleWake {
+ readonly inputId: string;
+ readonly events: Array;
+ readonly disposition: OpenCode2PostSettleWakeDisposition;
+ promotedAfterExecutionStarted: boolean;
+ phase: OpenCode2PostSettleWakePhase;
+}
+
+/** Keep the newest retired suppression evidence in insertion order. */
+export function pruneOpenCode2RetiredSuppressWakes(wakes: Map): void {
+ while (wakes.size > OPENCODE2_RETIRED_SUPPRESS_WAKE_LIMIT) {
+ const oldestInputId = wakes.keys().next().value;
+ if (oldestInputId === undefined) return;
+ wakes.delete(oldestInputId);
+ }
+}
+
+/** Keep recent promotion evidence for late admissions without unbounded state. */
+export function pruneOpenCode2PromotedInputIds(inputIds: Set): void {
+ while (inputIds.size > OPENCODE2_PROMOTED_INPUT_ID_LIMIT) {
+ const oldestInputId = inputIds.values().next().value;
+ if (oldestInputId === undefined) return;
+ inputIds.delete(oldestInputId);
+ }
+}
+
+interface OpenCode2ThreadState {
+ readonly nativeSessionId: string;
+ location: SessionInfoV2["location"];
+ providerThread: OrchestrationV2ProviderThread;
+ appThread: OrchestrationV2AppThread | null;
+ activeTurn: ActiveOpenCode2Turn | null;
+ boundModel: string | null;
+ boundVariant: string | null;
+ boundAgent: string | null;
+ lastAgentSelectedEventId: string | null;
+ readonly providerTurns: Map;
+ readonly messages: Map;
+ readonly runtimeRequests: Map;
+ readonly postSettleWakes: Array;
+ readonly retiredSuppressWakes: Map;
+ /**
+ * Unclaimed promotion evidence. Ownership removes an id; unmatched ids
+ * remain so a genuinely later admission can still claim it, with a bounded
+ * insertion-order window preventing stale ids from growing state forever.
+ */
+ readonly promotedInputIds: Set;
+ sawInputPromotion: boolean;
+ activeExecution: OpenCode2ExecutionOwnership | null;
+ parentSubagent: OpenCode2SubagentContext | null;
+ nextChildTurnOrdinal: number;
+}
+
+interface PendingOpenCode2Request {
+ readonly requestId: RuntimeRequestId;
+ readonly nativeRequestId: string;
+ readonly nativeSessionId: string;
+ readonly turn: ActiveOpenCode2Turn;
+ readonly state: OpenCode2ThreadState;
+ readonly nodeId: OrchestrationV2ExecutionNode["id"];
+ readonly turnItemId: OrchestrationV2TurnItem["id"];
+ readonly requestKind: OrchestrationV2RuntimeRequest["kind"];
+ readonly createdAt: DateTime.Utc;
+ readonly permission?: {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ readonly save: ReadonlyArray;
+ };
+ readonly questions?: ReadonlyArray;
+}
+
+export interface OpenCode2SessionPermission {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+}
+
+export type OpenCode2SessionPermissionStore = Map>;
+
+export interface OpenCode2AdapterV2Options {
+ readonly instanceId: ProviderInstanceId;
+ readonly settings: OpenCode2Settings;
+ readonly environment: NodeJS.ProcessEnv;
+ readonly runtime: OpenCode2Runtime["Service"];
+ readonly idAllocator: IdAllocatorV2Shape;
+ readonly serverConfig: ServerConfig["Service"];
+ readonly nativeEventLogger?: EventNdjsonLogger;
+ readonly continuationRequests?: {
+ readonly offer: (request: ProviderContinuationRequest) => Effect.Effect;
+ };
+ readonly interactionModeReflections?: {
+ readonly offer: (request: ProviderInteractionModeReflection) => Effect.Effect;
+ };
+}
+
+/**
+ * OpenCode admits a background-child result as a synthetic root input. The
+ * admission itself does not identify its execution boundary: an ordinary
+ * input and multiple synthetic inputs may be admitted together, and a later
+ * promotion event assigns one or more of them to an execution.
+ */
+export function openCode2IsPostSettleWakeAdmission(
+ event: any,
+ state: { readonly isChildSession: boolean },
+): boolean {
+ const type = normalizeOpenCode2WireType(String(event?.type ?? ""));
+ if (type !== "session.input.admitted" || state.isChildSession) {
+ return false;
+ }
+ const payload = event?.data ?? {};
+ const input = payload.input ?? payload.prompt;
+ if (input === undefined || input === null) return false;
+ if (
+ typeof input === "object" &&
+ "type" in input &&
+ input.type !== undefined &&
+ input.type !== "synthetic"
+ ) {
+ return false;
+ }
+ const data = recordValue(input, "data") ?? input;
+ const source = recordString(recordValue(data, "metadata"), "source");
+ const text = recordString(data, "text") ?? recordString(input, "text");
+ return source !== undefined || /^\s*<(?:subagent|shell)\b/i.test(text ?? "");
+}
+
+/**
+ * An interrupted provider-native child is terminal work, not a successful
+ * background result for the parent's next turn. OpenCode reports that result
+ * as a synthetic root input, so keep the cancellation boundary here rather
+ * than teaching the generic continuation machinery about provider wire data.
+ */
+export function openCode2IsCancelledPostSettleWake(event: any): boolean {
+ const type = normalizeOpenCode2WireType(String(event?.type ?? ""));
+ if (type !== "session.input.admitted") return false;
+ const payload = event?.data ?? {};
+ const input = payload.input ?? payload.prompt;
+ if (input === undefined || input === null) return false;
+ if (
+ typeof input === "object" &&
+ "type" in input &&
+ input.type !== undefined &&
+ input.type !== "synthetic"
+ ) {
+ return false;
+ }
+ const text =
+ recordString(input, "text") ??
+ recordString(recordValue(input, "data"), "text") ??
+ (typeof input === "string" ? input : undefined);
+ // The completed wrapper shape comes from captured pre-existing OpenCode 2
+ // replay data. The cancelled and interrupted values are inferred from
+ // OpenCode behavior, not adapter behavior. An exact raw-payload capture
+ // remains an explicit in-vivo gate before treating this as a contract.
+ const value = text ?? "";
+ return (
+ /^\s*]*\sstate\s*=\s*["'](?:cancelled|interrupted)["'])[^>]*>/i.test(value) ||
+ /^\s*]*\sstate\s*=\s*["']error["'])[^>]*>\s*<\/shell>\s*$/i.test(value)
+ );
+}
+
+export function openCode2EventEndsExecution(event: {
+ readonly type: string;
+ readonly data?: unknown;
+}): boolean {
+ const type = normalizeOpenCode2WireType(event.type);
+ if (type === "session.execution.failed" || type === "session.idle") {
+ return true;
+ }
+ if (type !== "session.execution.succeeded") {
+ return false;
+ }
+ // session.step.ended / session.next.step.ended alias to succeeded. Intermediate
+ // tool-call steps must not clear activeExecution or settle wakes.
+ return openCode2StepFinishSettlesTurn(openCode2WireData(event).finish);
+}
+
+/**
+ * Decide whether Stop should force-finalize after the interrupt request and
+ * settle wait. Pure so unit tests can cover the Stop recovery path without a
+ * live SSE consumer.
+ *
+ * @internal exported for tests
+ */
+export function openCode2ShouldForceInterruptFinalize(input: {
+ readonly interrupted: boolean;
+ readonly finalized: boolean;
+ readonly stillActive: boolean;
+ readonly waitedMs: number;
+ readonly settleTimeoutMs: number;
+}): boolean {
+ return (
+ input.interrupted &&
+ !input.finalized &&
+ input.stillActive &&
+ input.waitedMs >= input.settleTimeoutMs
+ );
+}
+
+/**
+ * Whether the event subscription loop should abort the current SSE pull and
+ * resubscribe. Pure for tests.
+ *
+ * @internal exported for tests
+ */
+export function openCode2ShouldResubscribeStalledStream(input: {
+ readonly sessionAborted: boolean;
+ readonly hasActiveTurn: boolean;
+ readonly lastEventAgeMs: number;
+ readonly stallMs: number;
+}): boolean {
+ return !input.sessionAborted && input.hasActiveTurn && input.lastEventAgeMs >= input.stallMs;
+}
+
+export const openCode2PendingWorkForSession = Effect.fnUntraced(function* (input: {
+ readonly sessionID: string;
+ readonly pending: Effect.Effect, OpenCode2RuntimeError>;
+ readonly shells: Effect.Effect, OpenCode2RuntimeError>;
+}) {
+ const pending = yield* input.pending;
+ if (pending.some((item) => item.sessionID === input.sessionID)) {
+ return true;
+ }
+ const shells = yield* input.shells;
+ return shells.some(
+ (shell) => shell.status === "running" && shell.metadata.sessionID === input.sessionID,
+ );
+});
+
+export function openCode2ToolNeedsTerminalOverride(
+ part: Pick,
+ terminal: TerminalTurnStatus,
+): boolean {
+ if (part.status === "pending" || part.status === "running") return true;
+ return (
+ terminal === "interrupted" &&
+ part.status === "error" &&
+ part.errorMessage === "Tool execution interrupted"
+ );
+}
+
+type OpenCode2SessionErrorData = {
+ sessionID?: string;
+ error?: { name?: string; message?: string; type?: string; data?: { message?: string } };
+};
+
+export function openCode2SessionErrorMessage(data: OpenCode2SessionErrorData): string {
+ const error = data.error;
+ if (error === undefined) return "OpenCode 2 reported a session error.";
+ return (
+ recordString(error.data, "message") ??
+ (typeof error.message === "string" && error.message.length > 0 ? error.message : undefined) ??
+ (typeof error.name === "string" && error.name.length > 0 ? error.name : undefined) ??
+ "OpenCode 2 reported a session error."
+ );
+}
+
+export function openCode2SessionErrorStatus(
+ data: OpenCode2SessionErrorData,
+ interrupted: boolean,
+): TerminalTurnStatus {
+ return interrupted || data.error?.name === "MessageAbortedError" ? "interrupted" : "failed";
+}
+
+export function openCode2SessionErrorTargetSessionIds(
+ sessionID: string | undefined,
+ activeSessionIDs: ReadonlyArray,
+): ReadonlyArray {
+ if (sessionID === undefined) return activeSessionIDs;
+ return activeSessionIDs.includes(sessionID) ? [sessionID] : [];
+}
+
+export function openCode2InterruptedThreadDisposition(
+ reason: string | undefined | null,
+): "reusable" | "broken" {
+ return reason === "shutdown" ? "broken" : "reusable";
+}
+
+export function openCode2ShouldSettleTurn(
+ source: "execution-terminal" | "execution-interrupted" | "idle",
+ executionStarted: boolean,
+ interruptRequested = false,
+): boolean {
+ if (source === "idle") return !executionStarted;
+ if (source === "execution-interrupted") return executionStarted || interruptRequested;
+ return executionStarted;
+}
+
+/**
+ * Whether a terminal `session.execution.*` may adopt a missing start event.
+ * Used when the volatile SSE stream reconnects and drops
+ * `session.execution.started` while later tool/text events still arrived for
+ * this turn. Empty turns (prompt admitted, no parts yet) stay protected so a
+ * late prior terminal cannot settle the next turn.
+ *
+ * @internal exported for tests
+ */
+export function openCode2CanAdoptMissingExecutionStart(turn: {
+ readonly executionStarted: boolean;
+ readonly interrupted: boolean;
+ readonly partCount: number;
+}): boolean {
+ if (turn.executionStarted) return true;
+ return turn.interrupted || turn.partCount > 0;
+}
+
+export interface OpenCode2ProtocolLogEvent {
+ readonly direction: "incoming" | "outgoing";
+ readonly messageKind: "request" | "response" | "notification" | "error";
+ readonly method: string;
+ readonly payload: unknown;
+}
+
+export function makeOpenCode2ProtocolLogger(input: {
+ readonly nativeEventLogger: EventNdjsonLogger | undefined;
+ readonly idAllocator: IdAllocatorV2Shape;
+ readonly providerInstanceId: ProviderInstanceId;
+ readonly providerSessionId: ProviderSessionId;
+ readonly threadId: ThreadId;
+}): (event: OpenCode2ProtocolLogEvent) => Effect.Effect {
+ return (event) =>
+ Effect.gen(function* () {
+ if (!input.nativeEventLogger) return;
+ const observedAt = DateTime.formatIso(yield* DateTime.now);
+ const method = structuralProtocolMethod(event.method);
+ yield* input.nativeEventLogger.write(
+ {
+ observedAt,
+ event: {
+ id: yield* input.idAllocator.allocate.rawEvent({
+ providerSessionId: input.providerSessionId,
+ method,
+ }),
+ kind: "protocol",
+ protocol: OPENCODE2_SDK_PROTOCOL,
+ provider: OPENCODE2_PROVIDER,
+ providerInstanceId: input.providerInstanceId,
+ providerSessionId: input.providerSessionId,
+ createdAt: observedAt,
+ threadId: input.threadId,
+ payload: {
+ direction: event.direction,
+ messageKind: event.messageKind,
+ method,
+ payload: summarizeNativeProtocolPayload(event.payload),
+ },
+ },
+ },
+ input.threadId,
+ );
+ }).pipe(
+ Effect.catchCause((cause) =>
+ Cause.hasInterrupts(cause)
+ ? Effect.interrupt
+ : Effect.logWarning("Failed to write native OpenCode 2 event log.", {
+ errorTag: causeErrorTag(cause),
+ reasonCount: cause.reasons.length,
+ provider: OPENCODE2_PROVIDER,
+ threadId: input.threadId,
+ }),
+ ),
+ );
+}
+
+function protocolError(detail: string, payload?: unknown): ProviderAdapterProtocolError {
+ return new ProviderAdapterProtocolError({
+ driver: OPENCODE2_PROVIDER,
+ detail,
+ ...(payload === undefined ? {} : { payload }),
+ });
+}
+
+/**
+ * Builds the native selection fragment shared by session creation and
+ * subsequent model or agent switches.
+ *
+ * @internal exported for tests
+ */
+export function openCode2SessionSelectionParameters(
+ modelSelection: ModelSelection,
+ interactionMode?: ProviderInteractionMode,
+ knownAgentIDs?: ReadonlySet | null,
+) {
+ const parsed = parseOpenCodeModelSlug(modelSelection.model);
+ if (parsed === null) {
+ throw protocolError(
+ `OpenCode 2 model '${modelSelection.model}' must use provider/model format`,
+ );
+ }
+ const variant = normalizeOpenCode2Variant(
+ getModelSelectionStringOptionValue(modelSelection, "variant"),
+ );
+ const agent = resolveOpenCode2SessionAgent(
+ getModelSelectionStringOptionValue(modelSelection, "agent"),
+ interactionMode,
+ knownAgentIDs,
+ );
+ return {
+ model: {
+ id: parsed.modelID,
+ providerID: parsed.providerID,
+ ...(variant === undefined ? {} : { variant }),
+ },
+ ...(agent === undefined ? {} : { agent }),
+ };
+}
+
+/**
+ * Current 2.x builds replaced the fork body's optional exclusive `messageID`
+ * with a required `boundary` union: `{type: "before", messageID}` keeps the
+ * old exclusive semantics and `{type: "through"}` copies the whole head. The
+ * old shape is rejected with 400 `Missing key at ["boundary"]`. The pinned SDK
+ * (next-16233, still npm's `next` tag) predates the change and only maps
+ * `messageID` into the body, so this rides the generated client's `$body_`
+ * escape hatch to place `boundary` there.
+ *
+ * @internal exported for tests
+ */
+export function openCode2ForkParameters(sessionID: string, boundaryMessageId: string | undefined) {
+ return {
+ sessionID,
+ $body_boundary:
+ boundaryMessageId === undefined
+ ? { type: "through" as const }
+ : { type: "before" as const, messageID: boundaryMessageId },
+ };
+}
+
+/**
+ * Maps the thread's Build/Plan interaction mode onto OpenCode 2's native
+ * `build`/`plan` primary agents, mirroring the 1.x adapter's plan fallback. A
+ * custom agent selection always wins: the toggle only owns the two native
+ * agents, and the `auto` sentinel (the descriptor default when custom agents
+ * exist) means "defer to the toggle". Plan dominates a stale explicit
+ * `build`/`plan` option because every pre-toggle thread has a persisted
+ * `agent: "build"` selection that would otherwise pin the toggle inert; an
+ * explicit `plan` option without plan mode still honors plan. With no
+ * interaction mode (subagent child threads, text generation) the explicit
+ * option passes through untouched. When the live agent catalog is available,
+ * a missing agent is omitted so the server can choose a configured default.
+ *
+ * @internal exported for tests
+ */
+export function resolveOpenCode2SessionAgent(
+ explicitAgent: string | undefined,
+ interactionMode: ProviderInteractionMode | undefined,
+ knownAgentIDs?: ReadonlySet | null,
+): string | undefined {
+ const explicit = explicitAgent === OPENCODE2_AUTO_AGENT ? undefined : explicitAgent;
+ let resolved: string | undefined;
+ if (explicit !== undefined && explicit !== "build" && explicit !== "plan") {
+ resolved = explicit;
+ } else if (interactionMode === undefined) {
+ resolved = explicit;
+ } else if (interactionMode === "plan" || explicit === "plan") {
+ resolved = "plan";
+ } else {
+ resolved = "build";
+ }
+ return resolved === undefined || knownAgentIDs == null || knownAgentIDs.has(resolved)
+ ? resolved
+ : undefined;
+}
+
+/** @internal exported for tests */
+export function openCode2InteractionModeForAgent(agent: string): ProviderInteractionMode | null {
+ if (agent === "build") return "default";
+ if (agent === "plan") return "plan";
+ return null;
+}
+
+export interface OpenCode2VariantClamp {
+ readonly variant: string | undefined;
+ readonly droppedVariant: string | null;
+}
+
+/**
+ * Fail closed: the server accepts any variant id on session.create and
+ * session.switchModel but silently drops the next prompt (the user message is
+ * recorded, no assistant reply ever follows) when the bound variant is not in
+ * the model's catalog. A variant that cannot be positively validated
+ * (`knownVariants === null` covers a failed catalog fetch, the empty
+ * bootstrap catalog, and a model the catalog does not list) is dropped:
+ * running at the server default is strictly less harmful than a dead turn.
+ *
+ * @internal exported for tests
+ */
+export function clampOpenCode2Variant(
+ variant: string | undefined,
+ knownVariants: ReadonlySet | null,
+): OpenCode2VariantClamp {
+ if (variant === undefined) return { variant: undefined, droppedVariant: null };
+ if (knownVariants === null || !knownVariants.has(variant)) {
+ return { variant: undefined, droppedVariant: variant };
+ }
+ return { variant, droppedVariant: null };
+}
+
+/**
+ * A freshly spawned 2.x server prints its ready banner before model bootstrap
+ * finishes and reports an empty catalog until then, exactly when the first
+ * turn's session.create runs. Without this retry the fail-closed clamp eats a
+ * valid variant on that first turn (observed live: a Max selection on
+ * `opencode/glm-5.2` dropped at turn start, bound at server default). Mirrors
+ * `retryEmptyOpenCode2Inventory`, additionally retrying failed fetches, which
+ * the clamp represents as `null`.
+ *
+ * @internal exported for tests
+ */
+export const retryEmptyOpenCode2VariantCatalog = Effect.fnUntraced(function* (
+ readCatalog: Effect.Effect> | null, E, R>,
+ options?: { readonly maxAttempts?: number; readonly retryDelayMs?: number },
+) {
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 10);
+ const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 500);
+ let catalog = yield* readCatalog;
+ for (
+ let attempt = 1;
+ (catalog === null || catalog.size === 0) && attempt < maxAttempts;
+ attempt += 1
+ ) {
+ yield* Effect.sleep(retryDelayMs);
+ catalog = yield* readCatalog;
+ }
+ return catalog;
+});
+
+/**
+ * A selection with no variant option carries no opinion: subagent child
+ * threads and pre-variant persisted selections have none, and treating that
+ * as "reset to default" would clear a variant the native session legitimately
+ * carries. Only an explicit option expresses intent (the synthetic "default"
+ * id means reset), and a model change always rebinds.
+ *
+ * @internal exported for tests
+ */
+export function planOpenCode2VariantAlignment(input: {
+ readonly boundModel: string | null;
+ readonly boundVariant: string | null;
+ readonly model: string;
+ readonly rawVariant: string | undefined;
+ readonly knownVariants: ReadonlySet | null;
+}): OpenCode2VariantClamp & { readonly switchNeeded: boolean } {
+ const modelChanged = input.boundModel !== input.model;
+ if (input.rawVariant === undefined && !modelChanged) {
+ return { switchNeeded: false, variant: undefined, droppedVariant: null };
+ }
+ const clamp = clampOpenCode2Variant(
+ normalizeOpenCode2Variant(input.rawVariant),
+ input.knownVariants,
+ );
+ return {
+ ...clamp,
+ switchNeeded: modelChanged || input.boundVariant !== (clamp.variant ?? null),
+ };
+}
+
+function nativeThreadId(providerThread: OrchestrationV2ProviderThread): string {
+ const nativeId = providerThread.nativeThreadRef?.nativeId;
+ if (nativeId === null || nativeId === undefined) {
+ throw protocolError(`Provider thread ${providerThread.id} has no OpenCode 2 session id`);
+ }
+ return nativeId;
+}
+
+function providerRef(nativeId: string, strength: "strong" | "weak" = "strong") {
+ return {
+ driver: OPENCODE2_PROVIDER,
+ nativeId,
+ strength,
+ } satisfies OrchestrationV2ProviderRef;
+}
+
+function dateTimeFromEpoch(value: number | undefined, fallback: DateTime.Utc): DateTime.Utc {
+ if (value === undefined) return fallback;
+ return Option.getOrElse(DateTime.make(value), () => fallback);
+}
+
+function nonEmptyString(value: unknown): string | undefined {
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
+}
+
+function recordValue(input: unknown, key: string): unknown {
+ return typeof input === "object" && input !== null && key in input
+ ? (input as Record)[key]
+ : undefined;
+}
+
+function recordString(input: unknown, ...keys: ReadonlyArray): string | undefined {
+ for (const key of keys) {
+ const value = nonEmptyString(recordValue(input, key));
+ if (value !== undefined) return value;
+ }
+ return undefined;
+}
+
+function recordStringArray(input: unknown, ...keys: ReadonlyArray): Array {
+ for (const key of keys) {
+ const value = recordValue(input, key);
+ if (Array.isArray(value)) {
+ return value.filter((entry): entry is string => typeof entry === "string");
+ }
+ const single = nonEmptyString(value);
+ if (single !== undefined) return [single];
+ }
+ return [];
+}
+
+function recordNumber(input: unknown, ...keys: ReadonlyArray): number | undefined {
+ for (const key of keys) {
+ const value = recordValue(input, key);
+ if (typeof value === "number" && Number.isFinite(value)) return value;
+ }
+ return undefined;
+}
+
+function stableJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+function sdkResponseForRawLog(value: unknown): unknown {
+ if (typeof value !== "object" || value === null) return value;
+ if ("data" in value) return { data: (value as { readonly data?: unknown }).data ?? null };
+ if ("stream" in value) return { subscribed: true };
+ return value;
+}
+
+/**
+ * 2.x payloads are double-wrapped: the SDK's `.data` is the parsed body, and
+ * every body carries its own `data` envelope. Reading one layer yields the
+ * envelope rather than the value, which fails far from here.
+ *
+ * @internal exported for tests
+ */
+export function unwrapOpenCode2Data(
+ operation: OpenCode2RuntimeOperation,
+ result: unknown,
+): Effect.Effect, OpenCode2RuntimeError> {
+ const payload = unwrapOpenCode2Payload(result);
+ if (payload === undefined || payload === null) {
+ return Effect.fail(
+ new OpenCode2RuntimeError({
+ operation,
+ category: "missing-response-payload",
+ }),
+ );
+ }
+ return Effect.succeed(payload as NonNullable);
+}
+
+/** @internal exported for tests */
+export function removeOpenCode2Session(
+ sessionID: string,
+ request: Effect.Effect,
+): Effect.Effect {
+ return request.pipe(
+ Effect.flatMap((response) => {
+ const error = recordValue(response, "error");
+ const status = recordNumber(recordValue(response, "response"), "status");
+ if (error === undefined || status === 404) return Effect.void;
+ return Effect.fail(
+ new OpenCode2RuntimeError({
+ operation: "session.remove",
+ category: "session-remove-failed",
+ cause: error,
+ }),
+ );
+ }),
+ );
+}
+
+/**
+ * Stable per-question id so an answer map keyed by header, question text, or
+ * generated id all resolve. Mirrors `openCodeQuestionId` for the 2.x question
+ * shape, which carries a header but none of 1.x's other fields.
+ *
+ * @internal exported for tests
+ */
+export function openCode2QuestionId(index: number, header: string): string {
+ const slug = header
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+ return slug.length > 0 ? `question-${index}-${slug}` : `question-${index}`;
+}
+
+/**
+ * Add T3's per-thread MCP server to a spawned OpenCode 2 process without
+ * writing the user's global or project configuration.
+ *
+ * @internal exported for tests
+ */
+export const openCode2EnvironmentWithT3Mcp = Effect.fn(
+ "OpenCode2AdapterV2.openCode2EnvironmentWithT3Mcp",
+)(function* (environment: NodeJS.ProcessEnv, session: McpProviderSession.McpProviderSessionConfig) {
+ const config = yield* decodeOpenCode2InlineConfig(environment.OPENCODE_CONFIG_CONTENT || "{}");
+ const mcp = yield* decodeOpenCode2McpConfig(config.mcp ?? {});
+ const content = yield* encodeOpenCode2InlineConfig({
+ ...config,
+ mcp: {
+ ...mcp,
+ [OPENCODE2_T3_MCP_NAME]: {
+ type: "remote",
+ url: session.endpoint,
+ headers: { Authorization: session.authorizationHeader },
+ oauth: false,
+ },
+ },
+ });
+ return {
+ ...environment,
+ OPENCODE_CONFIG_CONTENT: content,
+ } satisfies NodeJS.ProcessEnv;
+});
+
+/**
+ * Give a self-spawned full-access OpenCode 2 server its fixed startup policy.
+ *
+ * @internal exported for tests
+ */
+export const openCode2EnvironmentWithPermission = Effect.fn(
+ "OpenCode2AdapterV2.openCode2EnvironmentWithPermission",
+)(function* (
+ environment: NodeJS.ProcessEnv,
+ runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"],
+) {
+ if (!isOpenCodeAllowAllPolicy(runtimePolicy)) return environment;
+
+ const config = yield* decodeOpenCode2InlineConfig(environment.OPENCODE_CONFIG_CONTENT || "{}");
+ const content = yield* encodeOpenCode2InlineConfig({
+ ...config,
+ permission: "allow",
+ });
+ return {
+ ...environment,
+ OPENCODE_CONFIG_CONTENT: content,
+ } satisfies NodeJS.ProcessEnv;
+});
+
+/**
+ * 2.x has no session-scoped permission ruleset — `session.create` accepts none
+ * and its native `always` reply persists project-wide — so T3 evaluates each
+ * request against the same rules used by the 1.x adapter and replies only for
+ * that request. Session grants are remembered by the adapter instead.
+ *
+ * @internal exported for tests
+ */
+export function openCode2AutoPermissionReply(
+ runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"],
+ request: {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ },
+): "once" | "reject" | null {
+ const rules = openCodePermissionRules(runtimePolicy);
+ const resources = request.resources.length === 0 ? ["*"] : request.resources;
+ let needsApproval = false;
+ for (const resource of resources) {
+ const rule = rules.findLast(
+ (candidate) =>
+ openCode2WildcardMatch(candidate.permission, request.action) &&
+ openCode2WildcardMatch(candidate.pattern, resource),
+ );
+ const effect = rule?.action ?? "ask";
+ if (effect === "deny") return "reject";
+ if (effect === "ask") needsApproval = true;
+ }
+ return needsApproval ? null : "once";
+}
+
+/**
+ * OpenCode preview builds may drift from the pinned SDK before its generated
+ * event types catch up. Keep that drift at the adapter boundary so a missing
+ * resource or save list cannot terminate the provider event subscription.
+ *
+ * @internal exported for tests
+ */
+export function normalizeOpenCode2PermissionEvent(
+ protocol: "legacy" | "v2",
+ data: unknown,
+): {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ readonly save: ReadonlyArray;
+} {
+ return {
+ action:
+ (protocol === "legacy"
+ ? recordString(data, "permission", "action")
+ : recordString(data, "action", "permission")) ?? "unknown",
+ resources:
+ protocol === "legacy"
+ ? recordStringArray(data, "patterns", "resources", "pattern")
+ : recordStringArray(data, "resources", "patterns", "pattern"),
+ save:
+ protocol === "legacy"
+ ? recordStringArray(data, "always", "save")
+ : recordStringArray(data, "save", "always"),
+ };
+}
+
+export function openCode2PermissionAutoReply(
+ runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"],
+ sessionPermissions: ReadonlyArray,
+ request: {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ },
+): "once" | "reject" | null {
+ const resources = request.resources.length === 0 ? ["*"] : request.resources;
+ let needsApproval = false;
+ for (const resource of resources) {
+ const resourceRequest = { action: request.action, resources: [resource] };
+ const policyReply = openCode2AutoPermissionReply(runtimePolicy, resourceRequest);
+ if (policyReply === "reject") return "reject";
+ if (policyReply === "once") continue;
+ if (
+ sessionPermissions.some((permission) =>
+ openCode2SessionPermissionMatches(permission, resourceRequest),
+ )
+ ) {
+ continue;
+ }
+ needsApproval = true;
+ }
+ return needsApproval ? null : "once";
+}
+
+/** @internal exported for tests */
+export function openCode2PermissionAutoReplyForSession(
+ runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"],
+ sessionPermissions: OpenCode2SessionPermissionStore,
+ nativeSessionId: string,
+ request: {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ },
+): "once" | "reject" | null {
+ return openCode2PermissionAutoReply(
+ runtimePolicy,
+ sessionPermissions.get(nativeSessionId) ?? [],
+ request,
+ );
+}
+
+function openCode2WildcardMatch(pattern: string, value: string): boolean {
+ if (pattern === "*") return true;
+ const expression = pattern
+ .split("*")
+ .map((part) => part.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"))
+ .join(".*");
+ return new RegExp(`^${expression}$`).test(value);
+}
+
+function openCode2SessionPermissionMatches(
+ permission: OpenCode2SessionPermission,
+ request: {
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ },
+): boolean {
+ if (!openCode2WildcardMatch(permission.action, request.action)) return false;
+ const resources = request.resources.length === 0 ? ["*"] : request.resources;
+ return resources.every((resource) =>
+ permission.resources.some((pattern) => openCode2WildcardMatch(pattern, resource)),
+ );
+}
+
+function isOpenCodeAllowAllPolicy(
+ runtimePolicy: ProviderAdapterV2TurnInput["runtimePolicy"],
+): boolean {
+ const rules = openCodePermissionRules(runtimePolicy);
+ return (
+ rules.length === 1 &&
+ rules[0]?.permission === "*" &&
+ rules[0].pattern === "*" &&
+ rules[0].action === "allow"
+ );
+}
+
+/** @internal exported for tests */
+export function rememberOpenCode2SessionPermission(
+ permissionsBySession: OpenCode2SessionPermissionStore,
+ nativeSessionId: string,
+ permission: PendingOpenCode2Request["permission"],
+): void {
+ if (permission === undefined) return;
+ const permissions = permissionsBySession.get(nativeSessionId) ?? [];
+ const savedResources = permission.save.length === 0 ? permission.resources : permission.save;
+ const remembered = {
+ action: permission.action,
+ resources: savedResources.length === 0 ? ["*"] : savedResources,
+ };
+ if (
+ permissions.some(
+ (existing) =>
+ existing.action === remembered.action &&
+ existing.resources.length === remembered.resources.length &&
+ existing.resources.every((resource, index) => resource === remembered.resources[index]),
+ )
+ ) {
+ return;
+ }
+ permissions.push(remembered);
+ permissionsBySession.set(nativeSessionId, permissions);
+}
+
+/** @internal exported for tests */
+export function openCode2ChildTurnItemOrdinals(providerTurnOrdinal: number): {
+ readonly next: number;
+ readonly user: number;
+} {
+ const user = providerTurnOrdinal * 100;
+ return { next: user + 1, user };
+}
+
+function toOpenCode2FileAttachments(input: {
+ readonly attachments: ReadonlyArray | undefined;
+ readonly resolveAttachmentPath: (attachment: ChatAttachment) => string | null;
+}): Array {
+ const files: Array = [];
+ for (const attachment of input.attachments ?? []) {
+ const attachmentPath = input.resolveAttachmentPath(attachment);
+ if (!attachmentPath) continue;
+ files.push({
+ uri: NodeURL.pathToFileURL(attachmentPath).href,
+ ...(attachment.name ? { name: attachment.name } : {}),
+ });
+ }
+ return files;
+}
+
+function toolNodeStatus(status: OpenCode2ToolStatus): {
+ readonly node: OrchestrationV2ExecutionNode["status"];
+ readonly item: OrchestrationV2TurnItem["status"];
+} {
+ switch (status) {
+ case "pending":
+ return { node: "pending", item: "pending" };
+ case "running":
+ return { node: "running", item: "running" };
+ case "completed":
+ return { node: "completed", item: "completed" };
+ case "error":
+ return { node: "failed", item: "failed" };
+ }
+}
+
+function subagentStatusFromTurnItemStatus(
+ status: OrchestrationV2TurnItem["status"],
+): OpenCode2SubagentContext["status"] {
+ switch (status) {
+ case "failed":
+ return "failed";
+ case "completed":
+ return "completed";
+ case "cancelled":
+ return "cancelled";
+ case "interrupted":
+ return "interrupted";
+ case "pending":
+ return "pending";
+ default:
+ return "running";
+ }
+}
+
+function compactionStatusFromTerminalTurnStatus(
+ status: TerminalTurnStatus,
+): OpenCode2Compaction["status"] {
+ switch (status) {
+ case "completed":
+ return "completed";
+ case "failed":
+ return "failed";
+ case "cancelled":
+ case "interrupted":
+ return "cancelled";
+ }
+}
+
+function compactionTitle(status: OpenCode2Compaction["status"]): string {
+ switch (status) {
+ case "running":
+ return "Compacting context...";
+ case "completed":
+ return "Context compacted";
+ case "failed":
+ return "Context compaction failed";
+ case "cancelled":
+ return "Context compaction stopped";
+ }
+}
+
+function toolContentText(content: unknown): string | undefined {
+ if (!Array.isArray(content)) return undefined;
+ const chunks = content
+ .filter((entry) => recordString(entry, "type") === "text")
+ .map((entry) => recordString(entry, "text"))
+ .filter((text): text is string => text !== undefined);
+ return chunks.length === 0 ? undefined : chunks.join("\n");
+}
+
+function openCode2PermissionRequestKind(action: string): ProviderRequestKind {
+ return openCodePermissionRequestKind(action);
+}
+
+function makeProviderThread(input: {
+ readonly idAllocator: IdAllocatorV2Shape;
+ readonly providerInstanceId: ProviderInstanceId;
+ readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"];
+ readonly appThreadId: OrchestrationV2ProviderThread["appThreadId"];
+ readonly ownerNodeId?: OrchestrationV2ProviderThread["ownerNodeId"];
+ readonly nativeSession: SessionInfoV2;
+ readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"];
+ readonly now: DateTime.Utc;
+}): OrchestrationV2ProviderThread {
+ return {
+ id: input.idAllocator.derive.providerThread({
+ driver: OPENCODE2_PROVIDER,
+ nativeThreadId: input.nativeSession.id,
+ }),
+ driver: OPENCODE2_PROVIDER,
+ providerInstanceId: input.providerInstanceId,
+ providerSessionId: input.providerSessionId,
+ appThreadId: input.appThreadId,
+ ownerNodeId: input.ownerNodeId ?? null,
+ nativeThreadRef: {
+ driver: OPENCODE2_PROVIDER,
+ nativeId: input.nativeSession.id,
+ strength: "strong",
+ },
+ nativeConversationHeadRef: null,
+ status: "idle",
+ firstRunOrdinal: null,
+ lastRunOrdinal: null,
+ handoffIds: [],
+ forkedFrom: input.forkedFrom ?? null,
+ createdAt: dateTimeFromEpoch(input.nativeSession.time.created, input.now),
+ updatedAt: dateTimeFromEpoch(input.nativeSession.time.updated, input.now),
+ };
+}
+
+export function makeOpenCode2AdapterV2(options: OpenCode2AdapterV2Options): ProviderAdapterV2Shape {
+ const { idAllocator, runtime, serverConfig } = options;
+ const continuationRequests = options.continuationRequests;
+ const interactionModeReflections = options.interactionModeReflections;
+
+ return ProviderAdapterV2.of({
+ instanceId: options.instanceId,
+ driver: OPENCODE2_PROVIDER,
+ deleteDetachedThread: (input) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(input.providerThread);
+ const connection = yield* runtime.connectToOpenCode2Server({
+ binaryPath: options.settings.binaryPath,
+ serverUrl: options.settings.serverUrl,
+ serverPassword: options.settings.serverPassword,
+ environment: options.environment,
+ });
+ const client = runtime.createOpenCode2SdkClient({
+ baseUrl: connection.url,
+ directory: input.providerSession.cwd,
+ serverPassword: connection.password,
+ });
+ yield* removeOpenCode2Session(
+ sessionID,
+ runOpenCode2Sdk("session.interrupt", () =>
+ client.v2.session.get({ sessionID }, { throwOnError: false }).then(async () => {
+ // Beta Session3 has no remove(); best-effort interrupt then rely on GC.
+ try {
+ await client.v2.session.interrupt({ sessionID });
+ } catch {
+ /* ignore */
+ }
+ return { data: { data: true } };
+ }),
+ ),
+ );
+ }).pipe(
+ Effect.mapError((cause) =>
+ protocolError(
+ `Failed to delete detached OpenCode 2 session ${input.providerThread.id}`,
+ cause,
+ ),
+ ),
+ ),
+ getCapabilities: () => Effect.succeed(OpenCode2ProviderCapabilitiesV2),
+ planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
+ openSession: Effect.fn("OpenCode2AdapterV2.openSession")(
+ function* (input: ProviderAdapterV2OpenSessionInput) {
+ const scope = yield* Effect.scope;
+ const cwd = input.runtimePolicy.cwd ?? serverConfig.cwd;
+ const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
+ const selfSpawning = !(options.settings.serverUrl?.trim() ?? "");
+ const hasT3Mcp = mcpSession !== undefined && selfSpawning;
+ const environmentWithMcp =
+ hasT3Mcp && mcpSession !== undefined
+ ? yield* openCode2EnvironmentWithT3Mcp(options.environment, mcpSession)
+ : options.environment;
+ // The injected policy is fixed at spawn, like 1.x's session.create
+ // ruleset. A stricter mid-thread turn does not re-gate asks suppressed
+ // by allow-all until the provider session is reopened.
+ const injectedAllowPolicy = selfSpawning && isOpenCodeAllowAllPolicy(input.runtimePolicy);
+ const environment = injectedAllowPolicy
+ ? yield* openCode2EnvironmentWithPermission(environmentWithMcp, input.runtimePolicy)
+ : environmentWithMcp;
+ const connection = yield* runtime.connectToOpenCode2Server({
+ binaryPath: options.settings.binaryPath,
+ serverUrl: options.settings.serverUrl,
+ serverPassword: options.settings.serverPassword,
+ environment,
+ });
+ const spawnedWithInjectedAllowPolicy = injectedAllowPolicy && !connection.external;
+ let warnedAboutInjectedAllowPolicy = false;
+ const client = runtime.createOpenCode2SdkClient({
+ baseUrl: connection.url,
+ directory: cwd,
+ serverPassword: connection.password,
+ });
+
+ const now = yield* DateTime.now;
+ let sessionEntity: OrchestrationV2ProviderSession = {
+ id: input.providerSessionId,
+ driver: OPENCODE2_PROVIDER,
+ providerInstanceId: options.instanceId,
+ status: "ready",
+ cwd,
+ model: input.modelSelection.model,
+ capabilities: OpenCode2ProviderCapabilitiesV2,
+ createdAt: now,
+ updatedAt: now,
+ lastError: null,
+ };
+ const events = yield* Queue.unbounded();
+ const threads = new Map();
+ const shellProjections = new Map();
+ const shellSessionIds = new Map();
+ const pendingRequests = new Map();
+ const pendingRequestsByNativeId = new Map();
+ const subagentsByNativeItemId = new Map();
+ const subagentsByChildSessionId = new Map();
+ const nativeChildSessions = new Map<
+ string,
+ Extract["data"]["info"]
+ >();
+ const sessionPermissions: OpenCode2SessionPermissionStore = new Map();
+ const abortController = new AbortController();
+ // Liveness marker for SSE pull. OpenCode 2 fails a slow event consumer;
+ // if pull stalls while a turn is active we resubscribe.
+ let lastEventAtMs = 0;
+ let consecutiveStreamFailures = 0;
+ let consecutiveStallResubscribes = 0;
+ lastEventAtMs = yield* Clock.currentTimeMillis;
+
+ const emitProviderEvent = (event: ProviderAdapterV2Event) =>
+ Queue.offer(events, event).pipe(Effect.asVoid);
+
+ const writeProtocolEvent = makeOpenCode2ProtocolLogger({
+ nativeEventLogger: options.nativeEventLogger,
+ idAllocator,
+ providerInstanceId: options.instanceId,
+ providerSessionId: input.providerSessionId,
+ threadId: input.threadId,
+ });
+ // Never block the SSE pull path on disk logging. The stream is
+ // volatile under backpressure; serializing every notification before
+ // the next read is how a long kimi turn can fill Recv-Q and freeze.
+ const logProtocolEvent = (event: OpenCode2ProtocolLogEvent) =>
+ writeProtocolEvent(event).pipe(Effect.forkIn(scope), Effect.asVoid);
+
+ const sdkCall = (
+ method: OpenCode2RuntimeOperation,
+ payload: unknown,
+ call: () => Promise,
+ ): Effect.Effect =>
+ logProtocolEvent({
+ direction: "outgoing",
+ messageKind: "request",
+ method,
+ payload,
+ }).pipe(
+ Effect.andThen(runOpenCode2Sdk(method, call)),
+ Effect.tap((response) =>
+ logProtocolEvent({
+ direction: "incoming",
+ messageKind: "response",
+ method,
+ payload: sdkResponseForRawLog(response),
+ }),
+ ),
+ );
+
+ const sdkCallWithTimeout = (
+ method: OpenCode2RuntimeOperation,
+ payload: unknown,
+ call: () => Promise,
+ timeoutMs: number,
+ ): Effect.Effect, never> =>
+ sdkCall(method, payload, call).pipe(
+ Effect.timeoutOption(`${timeoutMs} millis`),
+ Effect.catchCause((cause) =>
+ Effect.logWarning("OpenCode 2 SDK call failed or timed out.", {
+ errorTag: causeErrorTag(cause),
+ operation: method,
+ provider: OPENCODE2_PROVIDER,
+ timeoutMs,
+ }).pipe(Effect.as(Option.none())),
+ ),
+ );
+
+ const updateProviderSession = (
+ status: OrchestrationV2ProviderSession["status"],
+ lastError: string | null = sessionEntity.lastError,
+ ) =>
+ Effect.gen(function* () {
+ const updatedAt = yield* DateTime.now;
+ sessionEntity = { ...sessionEntity, status, lastError, updatedAt };
+ yield* emitProviderEvent({
+ type: "provider_session.updated",
+ driver: OPENCODE2_PROVIDER,
+ providerSession: sessionEntity,
+ });
+ });
+
+ const updateProviderThread = (
+ state: OpenCode2ThreadState,
+ patch: Partial,
+ ) =>
+ Effect.gen(function* () {
+ const updatedAt = yield* DateTime.now;
+ state.providerThread = { ...state.providerThread, ...patch, updatedAt };
+ yield* emitProviderEvent({
+ type: "provider_thread.updated",
+ driver: OPENCODE2_PROVIDER,
+ providerThread: state.providerThread,
+ });
+ });
+
+ const itemOrdinal = (turn: ActiveOpenCode2Turn, nativeItemId: string): number => {
+ const existing = turn.itemOrdinals.get(nativeItemId);
+ if (existing !== undefined) return existing;
+ const ordinal = turn.nextItemOrdinal++;
+ turn.itemOrdinals.set(nativeItemId, ordinal);
+ return ordinal;
+ };
+
+ const emitProviderTurn = (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ status: OrchestrationV2ProviderTurn["status"],
+ completedAt: DateTime.Utc | null,
+ ) => {
+ const providerTurn: OrchestrationV2ProviderTurn = {
+ ...turn.providerTurn,
+ nativeTurnRef:
+ turn.nativeInputId === null
+ ? turn.providerTurn.nativeTurnRef
+ : providerRef(turn.nativeInputId, "weak"),
+ status,
+ completedAt,
+ };
+ Object.assign(turn.providerTurn, providerTurn);
+ state.providerTurns.set(String(providerTurn.id), providerTurn);
+ return emitProviderEvent({
+ type: "provider_turn.updated",
+ driver: OPENCODE2_PROVIDER,
+ threadId: turn.threadId,
+ providerTurn,
+ });
+ };
+
+ const emitTextPart = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ part: OpenCode2TextPart,
+ forceCompleted = false,
+ ) {
+ if (part.text.length === 0) return;
+ const emittedAt = yield* DateTime.now;
+ const isCompleted = forceCompleted || part.completed;
+ const completedAt = isCompleted ? emittedAt : null;
+ const nativeItemRef = providerRef(part.id);
+ const nodeId = idAllocator.derive.nodeFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const turnItemId = idAllocator.derive.turnItemFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const ordinal = itemOrdinal(turn, part.id);
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: nodeId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ parentNodeId: turn.rootNodeId,
+ rootNodeId: turn.rootNodeId,
+ kind: part.kind === "text" ? "assistant_message" : "reasoning",
+ status: isCompleted ? "completed" : "running",
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: part.startedAt,
+ completedAt,
+ },
+ });
+ if (part.kind === "text") {
+ const messageId = idAllocator.derive.messageFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const message: OrchestrationV2ConversationMessage = {
+ createdBy: "agent",
+ creationSource: "provider",
+ id: messageId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ nodeId,
+ role: "assistant",
+ text: part.text,
+ attachments: [],
+ streaming: !isCompleted,
+ createdAt: part.startedAt,
+ updatedAt: emittedAt,
+ };
+ state.messages.set(String(message.id), message);
+ yield* emitProviderEvent({
+ type: "message.updated",
+ driver: OPENCODE2_PROVIDER,
+ message,
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: {
+ id: turnItemId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ nodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ parentItemId: null,
+ ordinal,
+ status: isCompleted ? "completed" : "running",
+ title: null,
+ startedAt: part.startedAt,
+ completedAt,
+ updatedAt: emittedAt,
+ type: "assistant_message",
+ messageId,
+ text: part.text,
+ streaming: !isCompleted,
+ },
+ });
+ return;
+ }
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: {
+ id: turnItemId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ nodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ parentItemId: null,
+ ordinal,
+ status: isCompleted ? "completed" : "running",
+ title: null,
+ startedAt: part.startedAt,
+ completedAt,
+ updatedAt: emittedAt,
+ type: "reasoning",
+ text: part.text,
+ streaming: !isCompleted,
+ },
+ });
+ });
+
+ const bindSubagentChild = Effect.fnUntraced(function* (
+ context: OpenCode2SubagentContext,
+ nativeSession: Extract["data"]["info"],
+ ) {
+ if (context.childSessionId !== null) return;
+ const now = yield* DateTime.now;
+ const childSessionId = nativeSession.id;
+ const childThreadId = idAllocator.derive.threadFromProviderThread({
+ driver: OPENCODE2_PROVIDER,
+ nativeThreadId: childSessionId,
+ });
+ const childProviderThreadId = idAllocator.derive.providerThread({
+ driver: OPENCODE2_PROVIDER,
+ nativeThreadId: childSessionId,
+ });
+ const model =
+ nativeSession.model === undefined
+ ? context.model
+ : `${nativeSession.model.providerID}/${nativeSession.model.id}`;
+ const childVariant = normalizeOpenCode2Variant(nativeSession.model?.variant);
+ const childModelSelection: ModelSelection = {
+ instanceId: options.instanceId,
+ model: model ?? context.parentTurn.modelSelection.model,
+ ...(childVariant === undefined
+ ? {}
+ : { options: [{ id: "variant", value: childVariant }] }),
+ };
+ const childThread = makeSubagentChildThread({
+ parentThread: context.parentTurn.appThread,
+ childThreadId,
+ parentNodeId: context.nodeId,
+ activeProviderThreadId: childProviderThreadId,
+ providerInstanceId: options.instanceId,
+ modelSelection: childModelSelection,
+ title: subagentThreadTitle({
+ parentTitle: context.parentTurn.appThread.title,
+ title: context.title ?? nativeSession.title,
+ prompt: context.prompt,
+ ordinal: itemOrdinal(context.parentTurn, context.nativeItemId),
+ }),
+ now,
+ createdBy: "agent",
+ creationSource: "provider",
+ });
+ const childProviderThread: OrchestrationV2ProviderThread = {
+ id: childProviderThreadId,
+ driver: OPENCODE2_PROVIDER,
+ providerInstanceId: options.instanceId,
+ providerSessionId: input.providerSessionId,
+ appThreadId: childThreadId,
+ ownerNodeId: context.nodeId,
+ nativeThreadRef: providerRef(childSessionId),
+ nativeConversationHeadRef: null,
+ status: "active",
+ firstRunOrdinal: null,
+ lastRunOrdinal: null,
+ handoffIds: [],
+ forkedFrom: null,
+ createdAt: dateTimeFromEpoch(nativeSession.time.created, now),
+ updatedAt: dateTimeFromEpoch(nativeSession.time.updated, now),
+ };
+ context.childSessionId = childSessionId;
+ context.childThreadId = childThreadId;
+ context.childProviderThreadId = childProviderThreadId;
+ context.model = model;
+ context.status = "pending";
+ context.completedAt = null;
+ context.progress = undefined;
+ context.result = null;
+ subagentsByChildSessionId.set(childSessionId, context);
+ threads.set(childSessionId, {
+ nativeSessionId: childSessionId,
+ location: context.parentState.location,
+ providerThread: childProviderThread,
+ appThread: childThread,
+ activeTurn: null,
+ boundModel: model,
+ boundVariant: normalizeOpenCode2Variant(nativeSession.model?.variant) ?? null,
+ boundAgent: nativeSession.agent ?? null,
+ lastAgentSelectedEventId: null,
+ providerTurns: new Map(),
+ messages: new Map(),
+ runtimeRequests: new Map(),
+ postSettleWakes: [],
+ retiredSuppressWakes: new Map(),
+ promotedInputIds: new Set(),
+ sawInputPromotion: false,
+ activeExecution: null,
+ parentSubagent: context,
+ nextChildTurnOrdinal: 1,
+ });
+ yield* emitProviderEvent({
+ type: "app_thread.created",
+ driver: OPENCODE2_PROVIDER,
+ appThread: childThread,
+ });
+ yield* emitProviderEvent({
+ type: "provider_thread.updated",
+ driver: OPENCODE2_PROVIDER,
+ providerThread: childProviderThread,
+ });
+ });
+
+ const emitSubagentContext = Effect.fnUntraced(function* (
+ context: OpenCode2SubagentContext,
+ ) {
+ const now = yield* DateTime.now;
+ const terminal =
+ context.status === "completed" ||
+ context.status === "failed" ||
+ context.status === "cancelled" ||
+ context.status === "interrupted";
+ if (terminal && context.completedAt === null) context.completedAt = now;
+ const nodeStatus: OrchestrationV2ExecutionNode["status"] = context.status;
+ const subagent: OrchestrationV2Subagent = {
+ id: context.nodeId,
+ threadId: context.parentTurn.threadId,
+ runId: context.parentTurn.runId,
+ parentNodeId: context.parentTurn.rootNodeId,
+ origin: "provider_native",
+ createdBy: "agent",
+ driver: OPENCODE2_PROVIDER,
+ providerInstanceId: options.instanceId,
+ providerThreadId: context.childProviderThreadId,
+ childThreadId: context.childThreadId,
+ nativeTaskRef: providerRef(context.nativeItemId),
+ prompt: context.prompt,
+ title: context.title,
+ model: context.model,
+ status: context.status,
+ ...(context.progress === undefined ? {} : { progress: context.progress }),
+ result: context.result,
+ startedAt: context.startedAt,
+ completedAt: context.completedAt,
+ updatedAt: now,
+ };
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: context.nodeId,
+ threadId: context.parentTurn.threadId,
+ runId: context.parentTurn.runId,
+ parentNodeId: context.parentTurn.rootNodeId,
+ rootNodeId: context.parentTurn.rootNodeId,
+ kind: "subagent",
+ status: nodeStatus,
+ countsForRun: false,
+ providerThreadId:
+ context.childProviderThreadId ?? context.parentState.providerThread.id,
+ providerTurnId: context.parentTurn.providerTurnId,
+ nativeItemRef: providerRef(context.nativeItemId),
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: context.startedAt,
+ completedAt: context.completedAt,
+ },
+ });
+ yield* emitProviderEvent({
+ type: "subagent.updated",
+ driver: OPENCODE2_PROVIDER,
+ subagent,
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: {
+ id: context.turnItemId,
+ threadId: context.parentTurn.threadId,
+ runId: context.parentTurn.runId,
+ nodeId: context.nodeId,
+ providerThreadId: context.parentState.providerThread.id,
+ providerTurnId: context.parentTurn.providerTurnId,
+ nativeItemRef: providerRef(context.nativeItemId),
+ parentItemId: null,
+ ordinal: itemOrdinal(context.parentTurn, context.nativeItemId),
+ status: context.status,
+ title: context.title,
+ startedAt: context.startedAt,
+ completedAt: context.completedAt,
+ updatedAt: now,
+ type: "subagent",
+ subagentId: context.nodeId,
+ origin: "provider_native",
+ driver: OPENCODE2_PROVIDER,
+ providerInstanceId: options.instanceId,
+ childThreadId: context.childThreadId,
+ prompt: context.prompt,
+ result: context.result,
+ },
+ });
+ });
+
+ const emitSubagent = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ part: OpenCode2ToolPart,
+ terminal?: TerminalTurnStatus,
+ ) {
+ const nodeId = idAllocator.derive.nodeFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const turnItemId = idAllocator.derive.turnItemFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ let context = subagentsByNativeItemId.get(part.id);
+ if (context === undefined) {
+ context = {
+ nativeItemId: part.id,
+ nodeId,
+ turnItemId,
+ parentState: state,
+ parentTurn: turn,
+ startedAt: part.startedAt,
+ completedAt: null,
+ prompt: recordString(part.input, "prompt") ?? "",
+ title: recordString(part.input, "description") ?? null,
+ model: recordString(part.input, "model") ?? null,
+ childSessionId: null,
+ childThreadId: null,
+ childProviderThreadId: null,
+ status: "pending",
+ progress: undefined,
+ result: null,
+ };
+ subagentsByNativeItemId.set(part.id, context);
+ }
+ context.prompt = recordString(part.input, "prompt") ?? context.prompt;
+ context.title = recordString(part.input, "description") ?? context.title;
+ context.model = recordString(part.input, "model") ?? context.model;
+ const isBackgroundLaunch =
+ recordValue(part.input, "background") === true && part.status === "completed";
+ if (context.childSessionId === null && !isBackgroundLaunch) {
+ const status =
+ terminal === undefined ? toolNodeStatus(part.status) : terminalToolStatus(terminal);
+ context.status = subagentStatusFromTurnItemStatus(status.item);
+ if (context.status !== "running") context.progress = undefined;
+ if (part.output !== undefined && context.status === "completed") {
+ context.result = part.output;
+ } else if (context.status === "failed") {
+ context.result = part.errorMessage ?? part.output ?? context.result;
+ }
+ } else if (isBackgroundLaunch && context.childSessionId === null) {
+ context.status = "running";
+ context.progress = part.output;
+ }
+
+ if (context.childSessionId === null) {
+ const matchingChild = Array.from(nativeChildSessions.values()).find(
+ (candidate) =>
+ candidate.parentID === state.nativeSessionId &&
+ !subagentsByChildSessionId.has(candidate.id) &&
+ (context.title === null || candidate.title === context.title),
+ );
+ if (matchingChild !== undefined) yield* bindSubagentChild(context, matchingChild);
+ }
+ yield* emitSubagentContext(context);
+ });
+
+ const emitToolPart = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ part: OpenCode2ToolPart,
+ /**
+ * Force a terminal status for a tool the turn ended underneath.
+ * `session.interrupt` stops the execution without reporting a final
+ * state for whatever tool was mid-flight, so the last observed
+ * status stays `running` and the row would spin forever.
+ */
+ terminal?: TerminalTurnStatus,
+ ) {
+ if (part.name.toLowerCase() === "subagent") {
+ yield* emitSubagent(state, turn, part, terminal);
+ return;
+ }
+ const emittedAt = yield* DateTime.now;
+ const status =
+ terminal === undefined ? toolNodeStatus(part.status) : terminalToolStatus(terminal);
+ const completedAt =
+ terminal === undefined ? part.completedAt : (part.completedAt ?? emittedAt);
+ const nativeItemRef = providerRef(part.id);
+ const nodeId = idAllocator.derive.nodeFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const turnItemId = idAllocator.derive.turnItemFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: part.id,
+ });
+ const base = {
+ id: turnItemId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ nodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ parentItemId: null,
+ ordinal: itemOrdinal(turn, part.id),
+ status: status.item,
+ title: part.name,
+ startedAt: part.startedAt,
+ completedAt,
+ updatedAt: emittedAt,
+ } satisfies Pick<
+ OrchestrationV2TurnItem,
+ | "id"
+ | "threadId"
+ | "runId"
+ | "nodeId"
+ | "providerThreadId"
+ | "providerTurnId"
+ | "nativeItemRef"
+ | "parentItemId"
+ | "ordinal"
+ | "status"
+ | "title"
+ | "startedAt"
+ | "completedAt"
+ | "updatedAt"
+ >;
+ const projectionKind = openCodeToolProjectionKind(part.name);
+ const exitCode = recordNumber(part.structured, "exit", "exitCode");
+ let turnItem: OrchestrationV2TurnItem;
+ if (projectionKind === "command_execution") {
+ turnItem = {
+ ...base,
+ type: "command_execution",
+ input: recordString(part.input, "command", "cmd") ?? stableJson(part.input),
+ ...(part.output === undefined ? {} : { output: part.output }),
+ ...(exitCode === undefined ? {} : { exitCode }),
+ };
+ } else if (projectionKind === "file_change") {
+ turnItem = {
+ ...base,
+ type: "file_change",
+ fileName: recordString(part.input, "filePath", "path", "file") ?? part.name,
+ ...(recordString(part.input, "oldString", "oldText") === undefined
+ ? {}
+ : { oldStr: recordString(part.input, "oldString", "oldText")! }),
+ ...(recordString(part.input, "newString", "content", "newText") === undefined
+ ? {}
+ : { newStr: recordString(part.input, "newString", "content", "newText")! }),
+ ...(recordString(part.structured, "diff", "patch") === undefined
+ ? {}
+ : { diffStr: recordString(part.structured, "diff", "patch")! }),
+ };
+ } else if (projectionKind === "file_search") {
+ turnItem = {
+ ...base,
+ type: "file_search",
+ ...(recordString(part.input, "pattern", "query", "path", "filePath") === undefined
+ ? {}
+ : { pattern: recordString(part.input, "pattern", "query", "path", "filePath")! }),
+ };
+ } else if (projectionKind === "web_search") {
+ const pattern = recordString(part.input, "query", "url", "pattern");
+ turnItem = {
+ ...base,
+ type: "web_search",
+ ...(pattern === undefined ? {} : { patterns: [pattern] }),
+ };
+ } else {
+ turnItem = {
+ ...base,
+ type: "dynamic_tool",
+ toolName: part.name,
+ input: part.input,
+ ...(part.output === undefined ? {} : { output: part.output }),
+ };
+ }
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: nodeId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ parentNodeId: turn.rootNodeId,
+ rootNodeId: turn.rootNodeId,
+ kind: "tool_call",
+ status: status.node,
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: part.startedAt,
+ completedAt,
+ },
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem,
+ });
+ });
+
+ const emitCompaction = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ compaction: OpenCode2Compaction,
+ ) {
+ const emittedAt = yield* DateTime.now;
+ const completedAt =
+ compaction.status === "running" ? null : (compaction.completedAt ?? emittedAt);
+ const nativeItemRef = providerRef(compaction.id);
+ const nodeId = idAllocator.derive.nodeFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: compaction.id,
+ });
+ const turnItemId = idAllocator.derive.turnItemFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: compaction.id,
+ });
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: nodeId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ parentNodeId: turn.rootNodeId,
+ rootNodeId: turn.rootNodeId,
+ kind: "system",
+ status: compaction.status,
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: compaction.startedAt,
+ completedAt,
+ },
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: {
+ id: turnItemId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ nodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef,
+ parentItemId: null,
+ ordinal: itemOrdinal(turn, compaction.id),
+ status: compaction.status,
+ title: compactionTitle(compaction.status),
+ startedAt: compaction.startedAt,
+ completedAt,
+ updatedAt: emittedAt,
+ type: "compaction",
+ driver: OPENCODE2_PROVIDER,
+ ...(compaction.summary.length === 0 ? {} : { summary: compaction.summary }),
+ },
+ });
+ });
+
+ const runningShellForPart = (
+ turn: ActiveOpenCode2Turn,
+ part: OpenCode2ToolPart,
+ ): OpenCode2ShellProjection | undefined =>
+ Array.from(shellProjections.values()).find(
+ (shell) => shell.turn === turn && shell.part === part && shell.status === "running",
+ );
+
+ const runtimeRequestTurnItem = (
+ pending: PendingOpenCode2Request,
+ status: OrchestrationV2TurnItem["status"],
+ completedAt: DateTime.Utc | null,
+ updatedAt: DateTime.Utc,
+ ): OrchestrationV2TurnItem => {
+ const base = {
+ id: pending.turnItemId,
+ threadId: pending.turn.threadId,
+ runId: pending.turn.runId,
+ nodeId: pending.nodeId,
+ providerThreadId: pending.state.providerThread.id,
+ providerTurnId: pending.turn.providerTurnId,
+ nativeItemRef: providerRef(pending.nativeRequestId),
+ parentItemId: null,
+ ordinal: itemOrdinal(pending.turn, pending.nativeRequestId),
+ status,
+ startedAt: pending.createdAt,
+ completedAt,
+ updatedAt,
+ };
+ if (pending.questions !== undefined) {
+ return {
+ ...base,
+ title: "User input",
+ type: "user_input_request",
+ requestId: pending.requestId,
+ questions: pending.questions.map((question, index) => ({
+ id: openCode2QuestionId(index, question.header),
+ header: question.header.trim() || `Question ${index + 1}`,
+ question:
+ question.question.trim() || question.header.trim() || `Question ${index + 1}`,
+ options: question.options.map((option) => ({
+ label: option.label.trim() || "Option",
+ description: option.description.trim() || option.label.trim() || "Option",
+ })),
+ multiSelect: question.multiple === true,
+ })),
+ };
+ }
+ const permission = pending.permission;
+ if (permission === undefined) {
+ throw protocolError(`OpenCode 2 request ${pending.requestId} has no native payload`);
+ }
+ return {
+ ...base,
+ title: permission.action,
+ type: "approval_request",
+ requestId: pending.requestId,
+ requestKind:
+ pending.requestKind === "user_input"
+ ? "command"
+ : (pending.requestKind as Exclude),
+ prompt:
+ permission.resources.length === 0
+ ? permission.action
+ : permission.resources.join("\n"),
+ };
+ };
+
+ const emitRuntimeRequest = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ nativeSessionId: string,
+ nativeRequestId: string,
+ request:
+ | {
+ readonly type: "permission";
+ readonly action: string;
+ readonly resources: ReadonlyArray;
+ readonly save: ReadonlyArray;
+ }
+ | {
+ readonly type: "question";
+ readonly questions: ReadonlyArray;
+ },
+ ) {
+ if (pendingRequestsByNativeId.has(nativeRequestId)) return;
+ const createdAt = yield* DateTime.now;
+ const requestId = yield* idAllocator.allocate.runtimeRequest({
+ driver: OPENCODE2_PROVIDER,
+ providerTurnId: turn.providerTurnId,
+ nativeRequestId,
+ });
+ const nodeId = idAllocator.derive.approvalNode({ requestId });
+ const turnItemId = idAllocator.derive.approvalTurnItem({ requestId });
+ const requestKind: OrchestrationV2RuntimeRequest["kind"] =
+ request.type === "permission"
+ ? openCode2PermissionRequestKind(request.action)
+ : "user_input";
+ const pendingBase = {
+ requestId,
+ nativeRequestId,
+ nativeSessionId,
+ turn,
+ state,
+ nodeId,
+ turnItemId,
+ requestKind,
+ createdAt,
+ };
+ let pending: PendingOpenCode2Request;
+ if (request.type === "permission") {
+ pending = {
+ ...pendingBase,
+ permission: {
+ action: request.action,
+ resources: request.resources,
+ save: request.save,
+ },
+ };
+ } else {
+ pending = {
+ ...pendingBase,
+ questions: request.questions,
+ };
+ }
+ pendingRequests.set(String(requestId), pending);
+ pendingRequestsByNativeId.set(nativeRequestId, pending);
+ const runtimeRequest: OrchestrationV2RuntimeRequest = {
+ id: requestId,
+ nodeId,
+ providerTurnId: turn.providerTurnId,
+ nativeRequestRef: providerRef(nativeRequestId),
+ kind: requestKind,
+ status: "pending",
+ responseCapability: {
+ type: "live",
+ providerSessionId: input.providerSessionId,
+ },
+ createdAt,
+ resolvedAt: null,
+ };
+ state.runtimeRequests.set(String(requestId), runtimeRequest);
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: nodeId,
+ threadId: turn.threadId,
+ runId: turn.runId,
+ parentNodeId: turn.rootNodeId,
+ rootNodeId: turn.rootNodeId,
+ kind: request.type === "question" ? "user_input_request" : "approval_request",
+ status: "waiting",
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef: providerRef(nativeRequestId),
+ runtimeRequestId: requestId,
+ checkpointScopeId: null,
+ startedAt: createdAt,
+ completedAt: null,
+ },
+ });
+ yield* emitProviderEvent({
+ type: "runtime_request.updated",
+ driver: OPENCODE2_PROVIDER,
+ threadId: turn.threadId,
+ runtimeRequest,
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: runtimeRequestTurnItem(pending, "waiting", null, createdAt),
+ });
+ yield* updateProviderSession("waiting", null);
+ });
+
+ const resolveRuntimeRequest = Effect.fnUntraced(function* (
+ nativeRequestId: string,
+ status: "resolved" | "cancelled",
+ ) {
+ const pending = pendingRequestsByNativeId.get(nativeRequestId);
+ if (pending === undefined) return;
+ const resolvedAt = yield* DateTime.now;
+ const current = pending.state.runtimeRequests.get(String(pending.requestId));
+ if (current !== undefined) {
+ const resolved: OrchestrationV2RuntimeRequest = { ...current, status, resolvedAt };
+ pending.state.runtimeRequests.set(String(pending.requestId), resolved);
+ yield* emitProviderEvent({
+ type: "runtime_request.updated",
+ driver: OPENCODE2_PROVIDER,
+ threadId: pending.turn.threadId,
+ runtimeRequest: resolved,
+ });
+ }
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: pending.nodeId,
+ threadId: pending.turn.threadId,
+ runId: pending.turn.runId,
+ parentNodeId: pending.turn.rootNodeId,
+ rootNodeId: pending.turn.rootNodeId,
+ kind: pending.questions === undefined ? "approval_request" : "user_input_request",
+ status: status === "resolved" ? "completed" : "cancelled",
+ countsForRun: false,
+ providerThreadId: pending.state.providerThread.id,
+ providerTurnId: pending.turn.providerTurnId,
+ nativeItemRef: providerRef(nativeRequestId),
+ runtimeRequestId: pending.requestId,
+ checkpointScopeId: null,
+ startedAt: pending.createdAt,
+ completedAt: resolvedAt,
+ },
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: runtimeRequestTurnItem(
+ pending,
+ status === "resolved" ? "completed" : "cancelled",
+ resolvedAt,
+ resolvedAt,
+ ),
+ });
+ pendingRequests.delete(String(pending.requestId));
+ pendingRequestsByNativeId.delete(nativeRequestId);
+ if (pendingRequests.size === 0) yield* updateProviderSession("running", null);
+ });
+
+ const finalizeTurn = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ status: TerminalTurnStatus,
+ terminal?: {
+ readonly failure?: OrchestrationV2ProviderFailure;
+ readonly threadDisposition?: "reusable" | "broken";
+ },
+ ) {
+ if (turn.finalized) return;
+ turn.finalized = true;
+ const completedAt = yield* DateTime.now;
+ for (const part of turn.parts.values()) {
+ if (part.kind === "tool") {
+ const subagent = subagentsByNativeItemId.get(part.id);
+ const childTurn =
+ subagent?.childSessionId === null || subagent?.childSessionId === undefined
+ ? null
+ : threads.get(subagent.childSessionId)?.activeTurn;
+ if (
+ part.name.toLowerCase() === "subagent" &&
+ childTurn !== null &&
+ childTurn !== undefined &&
+ !childTurn.finalized
+ ) {
+ continue;
+ }
+ if (status === "completed" && runningShellForPart(turn, part) !== undefined) {
+ continue;
+ }
+ if (openCode2ToolNeedsTerminalOverride(part, status)) {
+ yield* emitToolPart(state, turn, part, status);
+ }
+ continue;
+ }
+ yield* emitTextPart(state, turn, part, true);
+ }
+ if (turn.activeCompaction?.status === "running") {
+ turn.activeCompaction.status = compactionStatusFromTerminalTurnStatus(status);
+ turn.activeCompaction.completedAt = completedAt;
+ yield* emitCompaction(state, turn, turn.activeCompaction);
+ }
+ for (const pending of Array.from(pendingRequests.values())) {
+ if (pending.turn.providerTurnId === turn.providerTurnId) {
+ yield* resolveRuntimeRequest(pending.nativeRequestId, "cancelled");
+ }
+ }
+ yield* emitProviderTurn(state, turn, status, completedAt);
+ const threadDisposition = terminal?.threadDisposition ?? "reusable";
+ let providerThreadStatus: OrchestrationV2ProviderThread["status"] = "idle";
+ if (turn.isRoot) {
+ providerThreadStatus = "active";
+ } else if (threadDisposition === "broken") {
+ providerThreadStatus = "error";
+ }
+ yield* updateProviderThread(state, {
+ status: providerThreadStatus,
+ nativeConversationHeadRef:
+ turn.nativeInputId === null
+ ? state.providerThread.nativeConversationHeadRef
+ : providerRef(turn.nativeInputId, "weak"),
+ });
+ state.activeTurn = null;
+ if (!turn.isRoot) {
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: turn.rootNodeId,
+ threadId: turn.threadId,
+ runId: null,
+ parentNodeId: null,
+ rootNodeId: turn.rootNodeId,
+ kind: "root_turn",
+ status,
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ nativeItemRef: providerRef(state.nativeSessionId),
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: turn.startedAt,
+ completedAt,
+ },
+ });
+ const context = state.parentSubagent;
+ if (context !== null) {
+ const assistantResult = Array.from(turn.parts.values()).findLast(
+ (part): part is OpenCode2TextPart =>
+ part.kind === "text" && part.text.trim().length > 0,
+ )?.text;
+ context.status = status;
+ context.progress = undefined;
+ if (
+ (status === "completed" || status === "interrupted") &&
+ assistantResult !== undefined
+ ) {
+ context.result = assistantResult;
+ } else if (status === "failed") {
+ const failure =
+ terminal?.failure ??
+ turn.providerRetry?.failure ??
+ makeProviderFailure({
+ message: sessionEntity.lastError ?? undefined,
+ class: "provider_error",
+ });
+ context.result = failure.message;
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: makeProviderFailureTurnItem({
+ idAllocator,
+ driver: OPENCODE2_PROVIDER,
+ threadId: turn.threadId,
+ runId: null,
+ nodeId: turn.rootNodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ itemOrdinal: itemOrdinal(turn, `terminal-failure:${turn.providerTurnId}`),
+ failure,
+ ...(turn.providerRetry === null
+ ? {}
+ : {
+ retry: turn.providerRetry.retry,
+ retryStartedAt: turn.providerRetry.startedAt,
+ }),
+ occurredAt: completedAt,
+ }),
+ });
+ }
+ yield* emitSubagentContext(context);
+ }
+ return;
+ }
+ const anotherTurnIsActive = Array.from(threads.values()).some(
+ (candidate) => candidate.activeTurn?.isRoot === true,
+ );
+ let providerSessionStatus: OrchestrationV2ProviderSession["status"] = "ready";
+ if (anotherTurnIsActive) {
+ providerSessionStatus = "running";
+ } else if (status === "failed") {
+ providerSessionStatus = "error";
+ }
+ yield* updateProviderSession(
+ providerSessionStatus,
+ status === "failed" ? sessionEntity.lastError : null,
+ );
+ if (status === "failed") {
+ yield* emitProviderEvent({
+ type: "turn.terminal",
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ runOrdinal: turn.runOrdinal,
+ failureItemOrdinal: itemOrdinal(turn, `terminal-failure:${turn.providerTurnId}`),
+ status,
+ failure:
+ terminal?.failure ??
+ turn.providerRetry?.failure ??
+ makeProviderFailure({
+ message: sessionEntity.lastError ?? undefined,
+ class: "provider_error",
+ }),
+ ...(turn.providerRetry === null
+ ? {}
+ : {
+ retry: turn.providerRetry.retry,
+ retryStartedAt: turn.providerRetry.startedAt,
+ }),
+ threadDisposition,
+ });
+ return;
+ }
+ yield* emitProviderEvent({
+ type: "turn.terminal",
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: state.providerThread.id,
+ providerTurnId: turn.providerTurnId,
+ runOrdinal: turn.runOrdinal,
+ status,
+ failure: null,
+ threadDisposition,
+ });
+ });
+
+ /** Resolve the active turn for a session id, or nothing if it settled. */
+ const activeFor = (
+ sessionID: string | undefined,
+ ): { state: OpenCode2ThreadState; turn: ActiveOpenCode2Turn } | null => {
+ if (sessionID === undefined) return null;
+ const state = threads.get(sessionID);
+ const turn = state?.activeTurn;
+ if (state === undefined || turn === null || turn === undefined || turn.finalized) {
+ return null;
+ }
+ return { state, turn };
+ };
+
+ const runtimeRequestProjectionFor = (active: {
+ readonly state: OpenCode2ThreadState;
+ readonly turn: ActiveOpenCode2Turn;
+ }) => {
+ const parent = active.state.parentSubagent;
+ return parent === null ? active : { state: parent.parentState, turn: parent.parentTurn };
+ };
+
+ const createChildTurn = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ inputID: string,
+ ) {
+ const context = state.parentSubagent;
+ if (state.appThread === null || context === null) return null;
+ const now = yield* DateTime.now;
+ const rootNodeId = idAllocator.derive.nodeFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: `${state.nativeSessionId}:root:${inputID}`,
+ });
+ const providerTurnId = idAllocator.derive.providerTurn({
+ driver: OPENCODE2_PROVIDER,
+ nativeTurnId: inputID,
+ });
+ const providerTurn: OrchestrationV2ProviderTurn = {
+ id: providerTurnId,
+ providerThreadId: state.providerThread.id,
+ nodeId: rootNodeId,
+ runAttemptId: null,
+ nativeTurnRef: providerRef(inputID, "weak"),
+ ordinal: state.nextChildTurnOrdinal++,
+ status: "running",
+ startedAt: now,
+ completedAt: null,
+ };
+ const itemOrdinals = openCode2ChildTurnItemOrdinals(providerTurn.ordinal);
+ const turn: ActiveOpenCode2Turn = {
+ isRoot: false,
+ providerBufferedContinuation: false,
+ threadId: state.appThread.id,
+ runId: null,
+ rootNodeId,
+ appThread: state.appThread,
+ modelSelection: state.appThread.modelSelection,
+ runtimePolicy: context.parentTurn.runtimePolicy,
+ providerTurnId,
+ runOrdinal: context.parentTurn.runOrdinal,
+ startedAt: now,
+ itemOrdinals: new Map(),
+ parts: new Map(),
+ toolIdsByCallId: new Map(),
+ providerTurn,
+ nextItemOrdinal: itemOrdinals.next,
+ nativeInputId: inputID,
+ activeCompaction: null,
+ executionStarted: false,
+ interrupted: false,
+ finalized: false,
+ providerRetry: null,
+ };
+ state.activeTurn = turn;
+ state.providerTurns.set(String(providerTurnId), providerTurn);
+ const userMessageId = idAllocator.derive.messageFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: inputID,
+ });
+ const userTurnItemId = idAllocator.derive.turnItemFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: inputID,
+ });
+ const userArtifacts = makeSubagentConversationArtifacts({
+ messageId: userMessageId,
+ turnItemId: userTurnItemId,
+ threadId: turn.threadId,
+ rootNodeId,
+ providerThreadId: state.providerThread.id,
+ providerTurnId,
+ nativeItemRef: providerRef(inputID, "weak"),
+ role: "user",
+ text: context.prompt,
+ ordinal: itemOrdinals.user,
+ now,
+ });
+ state.messages.set(String(userMessageId), userArtifacts.message);
+ yield* emitProviderEvent({
+ type: "node.updated",
+ driver: OPENCODE2_PROVIDER,
+ node: {
+ id: rootNodeId,
+ threadId: turn.threadId,
+ runId: null,
+ parentNodeId: null,
+ rootNodeId,
+ kind: "root_turn",
+ status: "running",
+ countsForRun: false,
+ providerThreadId: state.providerThread.id,
+ providerTurnId,
+ nativeItemRef: providerRef(inputID, "weak"),
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: now,
+ completedAt: null,
+ },
+ });
+ yield* emitProviderTurn(state, turn, "running", null);
+ yield* updateProviderThread(state, {
+ status: "active",
+ firstRunOrdinal: state.providerThread.firstRunOrdinal ?? providerTurn.ordinal,
+ lastRunOrdinal: providerTurn.ordinal,
+ nativeConversationHeadRef: providerRef(inputID, "weak"),
+ });
+ yield* emitProviderEvent({
+ type: "message.updated",
+ driver: OPENCODE2_PROVIDER,
+ message: userArtifacts.message,
+ });
+ yield* emitProviderEvent({
+ type: "turn_item.updated",
+ driver: OPENCODE2_PROVIDER,
+ turnItem: userArtifacts.turnItem,
+ });
+ context.status = "running";
+ context.progress = undefined;
+ yield* emitSubagentContext(context);
+ return turn;
+ });
+
+ const textPartId = (kind: "text" | "reasoning", messageId: string, ordinal: number) =>
+ `${messageId}:${kind}:${ordinal}`;
+
+ const upsertTextPart = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ kind: "text" | "reasoning",
+ data: { readonly assistantMessageID: string; readonly ordinal: number },
+ update: { readonly delta?: string; readonly text?: string; readonly completed?: boolean },
+ ) {
+ const id = textPartId(kind, data.assistantMessageID, data.ordinal);
+ const startedAt = yield* DateTime.now;
+ const existing = turn.parts.get(id);
+ const part: OpenCode2TextPart =
+ existing !== undefined && existing.kind !== "tool"
+ ? existing
+ : { kind, id, startedAt, text: "", completed: false };
+ if (update.text !== undefined) part.text = update.text;
+ else if (update.delta !== undefined) part.text += update.delta;
+ if (update.completed === true) part.completed = true;
+ turn.parts.set(id, part);
+ yield* emitTextPart(state, turn, part);
+ });
+
+ const upsertToolPart = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ callId: string,
+ update: {
+ readonly name?: string;
+ readonly input?: Record;
+ readonly inputDelta?: string;
+ readonly output?: string;
+ readonly structured?: Record;
+ readonly status?: OpenCode2ToolStatus;
+ readonly errorMessage?: string;
+ },
+ ) {
+ const now = yield* DateTime.now;
+ const id = turn.toolIdsByCallId.get(callId) ?? `tool:${callId}`;
+ turn.toolIdsByCallId.set(callId, id);
+ const existing = turn.parts.get(id);
+ const part: OpenCode2ToolPart =
+ existing !== undefined && existing.kind === "tool"
+ ? existing
+ : {
+ kind: "tool",
+ id,
+ callId,
+ startedAt: now,
+ // The name arrives on `session.tool.input.started`, ahead of
+ // every other event for this call, so this placeholder only
+ // shows if 2.x ever reorders them.
+ name: update.name ?? "tool",
+ input: {},
+ inputText: "",
+ output: undefined,
+ structured: undefined,
+ status: "pending",
+ errorMessage: undefined,
+ completedAt: null,
+ };
+ if (update.name !== undefined) part.name = update.name;
+ if (update.inputDelta !== undefined) part.inputText += update.inputDelta;
+ if (update.input !== undefined) part.input = update.input;
+ if (update.output !== undefined) part.output = update.output;
+ if (update.structured !== undefined) part.structured = update.structured;
+ if (update.errorMessage !== undefined) part.errorMessage = update.errorMessage;
+ const preserveRunningShell =
+ update.status !== undefined &&
+ (update.status === "completed" || update.status === "error") &&
+ runningShellForPart(turn, part) !== undefined;
+ if (update.status !== undefined && !preserveRunningShell) {
+ part.status = update.status;
+ if (update.status === "completed" || update.status === "error") part.completedAt = now;
+ }
+ turn.parts.set(id, part);
+ yield* emitToolPart(state, turn, part);
+ });
+
+ const shellToolStatus = (shell: ShellInfoV2): OpenCode2ToolStatus => {
+ if (shell.status === "running") return "running";
+ if (shell.status === "exited" && shell.exit === 0) return "completed";
+ return "error";
+ };
+
+ const registerShellProjection = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ shell: ShellInfoV2,
+ ) {
+ const existing = shellProjections.get(shell.id);
+ if (existing !== undefined) {
+ existing.status = shell.status;
+ existing.part.status = shellToolStatus(shell);
+ existing.part.structured = {
+ ...existing.part.structured,
+ ...(shell.exit === undefined ? {} : { exit: shell.exit }),
+ };
+ if (existing.part.status !== "running") {
+ existing.part.completedAt = yield* DateTime.now;
+ }
+ yield* emitToolPart(existing.state, existing.turn, existing.part);
+ return existing;
+ }
+
+ const turn = state.activeTurn;
+ if (turn === null) return null;
+ const associated = Array.from(turn.parts.values()).find(
+ (part): part is OpenCode2ToolPart =>
+ part.kind === "tool" &&
+ openCodeToolProjectionKind(part.name) === "command_execution" &&
+ recordString(part.input, "command", "cmd") === shell.command &&
+ Array.from(shellProjections.values()).every((projection) => projection.part !== part),
+ );
+ const now = yield* DateTime.now;
+ const part: OpenCode2ToolPart =
+ associated ??
+ ({
+ kind: "tool",
+ id: `shell:${shell.id}`,
+ callId: shell.id,
+ startedAt: dateTimeFromEpoch(shell.time?.started, now),
+ name: "bash",
+ input: { command: shell.command },
+ inputText: "",
+ output: undefined,
+ structured: shell.exit === undefined ? undefined : { exit: shell.exit },
+ status: shellToolStatus(shell),
+ errorMessage: undefined,
+ completedAt:
+ shell.status === "running" ? null : dateTimeFromEpoch(shell.time?.completed, now),
+ } satisfies OpenCode2ToolPart);
+ part.status = shellToolStatus(shell);
+ if (shell.exit !== undefined) {
+ part.structured = { ...part.structured, exit: shell.exit };
+ }
+ if (part.status !== "running") {
+ part.completedAt = dateTimeFromEpoch(shell.time?.completed, now);
+ }
+ turn.parts.set(part.id, part);
+ const projection: OpenCode2ShellProjection = {
+ shellId: shell.id,
+ state,
+ turn,
+ part,
+ location: state.location,
+ status: shell.status,
+ };
+ shellProjections.set(shell.id, projection);
+ shellSessionIds.set(shell.id, state.nativeSessionId);
+ yield* emitToolPart(state, turn, part);
+ return projection;
+ });
+
+ const readShellOutput = Effect.fnUntraced(function* (
+ shellId: string,
+ location: SessionInfoV2["location"],
+ initial?: {
+ readonly output: string;
+ readonly cursor: number;
+ readonly truncated: boolean;
+ },
+ ) {
+ let output = initial?.output ?? "";
+ let cursor = initial?.cursor ?? 0;
+ let truncated = initial?.truncated ?? true;
+ while (truncated) {
+ const parameters = {
+ id: shellId,
+ location,
+ cursor: String(cursor),
+ limit: String(64 * 1024),
+ };
+ const response = yield* sdkCall("shell.output", parameters, () =>
+ Promise.resolve({
+ data: { data: { output: "", cursor: 0, size: 0, truncated: false } },
+ }),
+ );
+ const page = yield* unwrapOpenCode2Data<{
+ readonly output: string;
+ readonly cursor: number;
+ readonly size: number;
+ readonly truncated: boolean;
+ }>("shell.output", response);
+ output += page.output;
+ if (!page.truncated) return output;
+ if (page.cursor <= cursor) {
+ return yield* protocolError(
+ `OpenCode 2 shell ${shellId} output cursor did not advance`,
+ );
+ }
+ cursor = page.cursor;
+ truncated = page.truncated;
+ }
+ return output;
+ });
+
+ const completeShellProjection = Effect.fnUntraced(function* (
+ shellId: string,
+ patch: {
+ readonly exit?: number;
+ readonly status: ShellInfoV2["status"];
+ readonly output?: {
+ readonly output: string;
+ readonly cursor: number;
+ readonly truncated: boolean;
+ };
+ },
+ ) {
+ const projection = shellProjections.get(shellId);
+ if (projection === undefined) return;
+ projection.status = patch.status;
+ if (
+ projection.turn.providerTurn.status !== "running" &&
+ projection.turn.providerTurn.status !== "completed"
+ ) {
+ return;
+ }
+ const output =
+ patch.status === "killed" && patch.output === undefined
+ ? projection.part.output
+ : yield* readShellOutput(shellId, projection.location, patch.output).pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning("Failed to read OpenCode 2 shell output.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ shellId,
+ }).pipe(Effect.as(projection.part.output)),
+ ),
+ );
+ const completedAt = yield* DateTime.now;
+ projection.part.status =
+ patch.status === "exited" && patch.exit === 0 ? "completed" : "error";
+ projection.part.completedAt = completedAt;
+ if (output !== undefined) projection.part.output = output;
+ projection.part.structured = {
+ ...projection.part.structured,
+ ...(patch.exit === undefined ? {} : { exit: patch.exit }),
+ };
+ yield* emitToolPart(projection.state, projection.turn, projection.part);
+ });
+
+ const removeRunningShellsForTurn = Effect.fnUntraced(function* (turn: ActiveOpenCode2Turn) {
+ const running = Array.from(shellProjections.values()).filter(
+ (projection) => projection.turn === turn && projection.status === "running",
+ );
+ for (const projection of running) {
+ const parameters = {
+ id: projection.shellId,
+ location: projection.location,
+ };
+ yield* sdkCall("shell.remove", parameters, () =>
+ Promise.resolve({ data: { data: true } }),
+ ).pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning("Failed to stop an interrupted OpenCode 2 shell.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ shellId: projection.shellId,
+ }),
+ ),
+ );
+ }
+ });
+
+ const autoReplyPermission = Effect.fnUntraced(function* (
+ sessionID: string,
+ requestID: string,
+ reply: "once" | "reject",
+ ) {
+ return yield* sdkCall("session.permission.reply", { sessionID, requestID, reply }, () =>
+ client.v2.session.permission.reply({ sessionID, requestID, reply }),
+ ).pipe(
+ Effect.as(true),
+ Effect.catch((cause: OpenCode2RuntimeError) =>
+ Effect.logWarning("Failed to answer an OpenCode 2 permission request.", {
+ category: cause.category,
+ operation: cause.operation,
+ provider: OPENCODE2_PROVIDER,
+ }).pipe(Effect.as(false)),
+ ),
+ );
+ });
+
+ const failActiveTurns = Effect.fnUntraced(function* (
+ detail: string,
+ failureClass: "transport_error" | "provider_error",
+ ) {
+ yield* updateProviderSession("error", detail);
+ for (const state of threads.values()) {
+ if (state.activeTurn !== null) {
+ yield* finalizeTurn(state, state.activeTurn, "failed", {
+ failure: makeProviderFailure({ message: detail, class: failureClass }),
+ threadDisposition: "broken",
+ });
+ }
+ }
+ });
+
+ const offerPostSettleWake = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ event: any,
+ suppressContinuation: boolean,
+ ) {
+ const wake: OpenCode2PostSettleWake = {
+ inputId: event.data.inputID,
+ events: [event],
+ disposition: suppressContinuation ? "suppress" : "replay",
+ promotedAfterExecutionStarted: false,
+ phase: "pending",
+ };
+ state.postSettleWakes.push(wake);
+ if (
+ suppressContinuation ||
+ continuationRequests === undefined ||
+ state.appThread === null
+ ) {
+ return;
+ }
+ yield* continuationRequests.offer({
+ threadId: state.appThread.id,
+ providerThreadId: state.providerThread.id,
+ driver: OPENCODE2_PROVIDER,
+ detail:
+ event.data.input.type === "synthetic"
+ ? (event.data.input.data.description ?? null)
+ : null,
+ });
+ });
+
+ const retireUnownedSuppressWakes = (
+ state: OpenCode2ThreadState,
+ ownerInputIds: ReadonlySet,
+ ): void => {
+ for (let index = state.postSettleWakes.length - 1; index >= 0; index -= 1) {
+ const wake = state.postSettleWakes[index];
+ if (
+ wake?.disposition === "suppress" &&
+ (wake.phase === "pending" || wake.phase === "executing") &&
+ !ownerInputIds.has(wake.inputId)
+ ) {
+ state.postSettleWakes.splice(index, 1);
+ state.retiredSuppressWakes.delete(wake.inputId);
+ state.retiredSuppressWakes.set(wake.inputId, wake);
+ }
+ }
+ pruneOpenCode2RetiredSuppressWakes(state.retiredSuppressWakes);
+ };
+
+ const beginOpenCode2Execution = (state: OpenCode2ThreadState): void => {
+ const activeInputId = state.activeTurn?.nativeInputId;
+ const wakeInputIds = state.postSettleWakes
+ .filter((wake) => wake.phase === "pending" || wake.phase === "executing")
+ .map((wake) => wake.inputId);
+ const candidateInputIds = new Set(
+ activeInputId === null || activeInputId === undefined
+ ? wakeInputIds
+ : [activeInputId, ...wakeInputIds],
+ );
+ const promotedOwners = state.sawInputPromotion
+ ? Array.from(state.promotedInputIds).filter((inputId) => candidateInputIds.has(inputId))
+ : [];
+
+ // OpenCode's promoted event is the authoritative input-to-execution
+ // boundary. Older clients omitted it, so the bounded fallback gives
+ // a known ordinary input priority. If there is no ordinary input and
+ // promotion has not identified an owner, every pending wake owns the
+ // execution for buffering purposes: swallowing unattributable output
+ // is safer than allowing it into the active visible turn.
+ const fallbackInputIds =
+ activeInputId !== null && activeInputId !== undefined ? [activeInputId] : wakeInputIds;
+ const ownership =
+ promotedOwners.length > 0
+ ? { inputIds: new Set(promotedOwners), claimedByPromotion: true }
+ : { inputIds: new Set(fallbackInputIds), claimedByPromotion: false };
+ state.activeExecution = ownership;
+ for (const inputId of ownership.inputIds) {
+ state.promotedInputIds.delete(inputId);
+ }
+ for (const wake of state.postSettleWakes) {
+ if (ownership.inputIds.has(wake.inputId) && wake.phase === "pending") {
+ wake.phase = "executing";
+ }
+ }
+ if (
+ (activeInputId !== null && activeInputId !== undefined) ||
+ promotedOwners.length > 0
+ ) {
+ retireUnownedSuppressWakes(state, ownership.inputIds);
+ }
+ };
+
+ const settlePostSettleWakes = (
+ state: OpenCode2ThreadState,
+ event: any,
+ ownerInputIds: ReadonlySet,
+ ): void => {
+ if (!openCode2EventEndsExecution(event)) return;
+ for (const wake of state.postSettleWakes) {
+ if (
+ ownerInputIds.has(wake.inputId) &&
+ (wake.phase === "pending" || wake.phase === "executing")
+ ) {
+ wake.phase = "ready";
+ }
+ }
+ const ordinaryTurnOwnsExecution =
+ state.activeTurn?.nativeInputId !== null &&
+ state.activeTurn?.nativeInputId !== undefined &&
+ ownerInputIds.has(state.activeTurn.nativeInputId);
+ let replayOwnerKept = false;
+ for (let index = state.postSettleWakes.length - 1; index >= 0; index -= 1) {
+ const wake = state.postSettleWakes[index];
+ if (
+ wake !== undefined &&
+ ownerInputIds.has(wake.inputId) &&
+ wake.phase === "ready" &&
+ (wake.disposition === "suppress" ||
+ ordinaryTurnOwnsExecution ||
+ (wake.disposition === "replay" && replayOwnerKept))
+ ) {
+ state.postSettleWakes.splice(index, 1);
+ state.promotedInputIds.delete(wake.inputId);
+ } else if (
+ wake !== undefined &&
+ ownerInputIds.has(wake.inputId) &&
+ wake.disposition === "replay" &&
+ wake.phase === "ready"
+ ) {
+ replayOwnerKept = true;
+ }
+ }
+ };
+
+ const eventSessionId = (event: any): string | undefined => {
+ const directSessionId = recordString(event.data, "sessionID");
+ if (directSessionId !== undefined) return directSessionId;
+
+ const formSessionId = recordString(recordValue(event.data, "form"), "sessionID");
+ if (formSessionId !== undefined) return formSessionId;
+
+ const info = recordValue(event.data, "info");
+ const shellSessionId = recordString(recordValue(info, "metadata"), "sessionID");
+ if (shellSessionId !== undefined) return shellSessionId;
+
+ const nativeId = recordString(event.data, "requestID", "id");
+ if (nativeId === undefined) return undefined;
+ return (
+ shellSessionIds.get(nativeId) ??
+ pendingRequestsByNativeId.get(nativeId)?.nativeSessionId
+ );
+ };
+
+ const bufferPostSettleWakeEvent = (event: any, isReplay: boolean): boolean => {
+ const sessionID = eventSessionId(event);
+ if (sessionID === undefined) return false;
+ const state = threads.get(sessionID);
+ if (state === undefined) return false;
+ // The replay loop feeds old provider events back through this same
+ // handler. They belong to the turn being replayed and must not
+ // settle a live execution that happens to be active beside it.
+ if (isReplay) return false;
+ const bufferedType = normalizeOpenCode2WireType(String(event?.type ?? ""));
+ if (bufferedType === "session.input.admitted") {
+ return false;
+ }
+ if (state.activeExecution === null && openCode2EventEndsExecution(event)) {
+ beginOpenCode2Execution(state);
+ }
+ const execution = state.activeExecution;
+ if (execution === null) return false;
+ const owningWakes = state.postSettleWakes.filter(
+ (candidate) =>
+ execution.inputIds.has(candidate.inputId) &&
+ (candidate.phase === "pending" || candidate.phase === "executing"),
+ );
+ if (owningWakes.length === 0) return false;
+ for (const wake of owningWakes) wake.events.push(event);
+ const ordinaryTurnOwnsExecution =
+ state.activeTurn?.nativeInputId !== null &&
+ state.activeTurn?.nativeInputId !== undefined &&
+ execution.inputIds.has(state.activeTurn.nativeInputId);
+ const suppressesLateOutput = owningWakes.some(
+ (wake) => wake.disposition === "suppress" && wake.promotedAfterExecutionStarted,
+ );
+ if (openCode2EventEndsExecution(event)) {
+ settlePostSettleWakes(state, event, execution.inputIds);
+ if (!ordinaryTurnOwnsExecution) state.activeExecution = null;
+ }
+ // A suppressed wake promoted after an ordinary root has claimed the
+ // session-level execution is indistinguishable from that root's
+ // output. Prefer the cancellation boundary for non-terminal output;
+ // the terminal still reaches the root lifecycle so it can settle.
+ return suppressesLateOutput
+ ? !openCode2EventEndsExecution(event)
+ : !ordinaryTurnOwnsExecution;
+ };
+
+ const activeTurnOwnsOpenCode2Execution = (
+ state: OpenCode2ThreadState,
+ turn: ActiveOpenCode2Turn,
+ replayWakeInputId?: string,
+ ): boolean => {
+ if (replayWakeInputId !== undefined) {
+ return replayWakeInputId === turn.nativeInputId;
+ }
+ if (
+ turn.isRoot &&
+ state.postSettleWakes.length === 0 &&
+ state.retiredSuppressWakes.size === 0 &&
+ (state.activeExecution === null || state.activeExecution.inputIds.size === 0)
+ ) {
+ return true;
+ }
+ if (
+ !turn.isRoot &&
+ state.postSettleWakes.length === 0 &&
+ (state.activeExecution === null || state.activeExecution.inputIds.size === 0)
+ ) {
+ return true;
+ }
+ const inputId = turn.nativeInputId;
+ return inputId !== null && state.activeExecution?.inputIds.has(inputId) === true;
+ };
+
+ const handleEvent = Effect.fnUntraced(function* (
+ // Beta V2Event plus structural wire events; type names are normalized
+ // before the switch.
+ event: any,
+ context: OpenCode2EventHandlingContext = {},
+ ) {
+ const wire = event as WireEvent;
+ const eventType = normalizeOpenCode2WireType(String(wire.type ?? event?.type ?? ""));
+ const isReplay = context.replayWakeInputId !== undefined;
+ yield* logProtocolEvent({
+ direction: "incoming",
+ messageKind: "notification",
+ method: wire.type,
+ payload: event,
+ });
+ const isCancelledPostSettleWake = openCode2IsCancelledPostSettleWake(event);
+ const admittedState =
+ eventType === "session.input.admitted"
+ ? threads.get(openCode2WireSessionID(wire) ?? event.data?.sessionID)
+ : undefined;
+ if (
+ admittedState !== undefined &&
+ !isReplay &&
+ openCode2IsPostSettleWakeAdmission(event, {
+ isChildSession: admittedState.parentSubagent !== null,
+ })
+ ) {
+ yield* offerPostSettleWake(admittedState, event, isCancelledPostSettleWake);
+ return;
+ }
+ const eventSessionId =
+ openCode2WireSessionID(wire) ?? recordString(event.data, "sessionID");
+ const eventState =
+ admittedState ??
+ (eventSessionId === undefined ? undefined : threads.get(eventSessionId));
+ if (eventState !== undefined && !isReplay && eventType === "session.execution.started") {
+ beginOpenCode2Execution(eventState);
+ }
+ if (bufferPostSettleWakeEvent(event, isReplay)) return;
+ if (
+ isCancelledPostSettleWake &&
+ !isReplay &&
+ (admittedState === undefined || admittedState.parentSubagent === null)
+ ) {
+ return;
+ }
+ switch (eventType) {
+ case "session.created": {
+ const nativeSession = event.data.info;
+ nativeChildSessions.set(nativeSession.id, nativeSession);
+ if (nativeSession.parentID === undefined) return;
+ const parentState = threads.get(nativeSession.parentID);
+ if (parentState === undefined) return;
+ const candidates = Array.from(subagentsByNativeItemId.values()).filter(
+ (context) =>
+ context.parentState === parentState &&
+ context.childSessionId === null &&
+ !subagentsByChildSessionId.has(nativeSession.id),
+ );
+ const context =
+ candidates.find(
+ (candidate) =>
+ candidate.title !== null && candidate.title === nativeSession.title,
+ ) ?? candidates[0];
+ if (context === undefined) return;
+ yield* bindSubagentChild(context, nativeSession);
+ yield* emitSubagentContext(context);
+ return;
+ }
+ case "session.agent.selected": {
+ const state = threads.get(event.data.sessionID);
+ if (state === undefined) return;
+ // Event ids are monotonic, so a replayed or out-of-order
+ // delivery must not resurrect an older agent selection.
+ if (
+ state.lastAgentSelectedEventId !== null &&
+ event.id <= state.lastAgentSelectedEventId
+ ) {
+ return;
+ }
+ state.lastAgentSelectedEventId = event.id;
+ state.boundAgent = event.data.agent;
+ const reflectedInteractionMode = openCode2InteractionModeForAgent(event.data.agent);
+ // A genuinely external switch (a future plan_exit flow, or
+ // another client on the same session) reflects into the
+ // thread's Build/Plan mode so the next turn does not push the
+ // stale mode back. Only the two native agents map onto the
+ // toggle, and subagent child sessions keep their own agents.
+ if (
+ interactionModeReflections === undefined ||
+ state.parentSubagent !== null ||
+ state.appThread === null ||
+ reflectedInteractionMode === null
+ ) {
+ return;
+ }
+ // Matching echoes still supersede older queued reflections. The
+ // worker drains them in native event order and command ids make
+ // retries safe.
+ yield* interactionModeReflections.offer({
+ threadId: state.appThread.id,
+ driver: OPENCODE2_PROVIDER,
+ interactionMode: reflectedInteractionMode,
+ dedupeKey: `${OPENCODE2_PROVIDER}:${event.id}`,
+ });
+ return;
+ }
+ case "session.shell.started": {
+ const state = threads.get(event.data.sessionID);
+ if (state === undefined) return;
+ yield* registerShellProjection(state, event.data.shell);
+ yield* updateProviderThread(state, {});
+ return;
+ }
+ case "session.shell.ended": {
+ yield* completeShellProjection(event.data.shell.id, {
+ status: event.data.shell.status,
+ ...(event.data.shell.exit === undefined ? {} : { exit: event.data.shell.exit }),
+ output: event.data.output,
+ });
+ shellProjections.delete(event.data.shell.id);
+ shellSessionIds.delete(event.data.shell.id);
+ const state = threads.get(event.data.sessionID);
+ if (state !== undefined) yield* updateProviderThread(state, {});
+ return;
+ }
+ case "shell.created": {
+ const sessionID = recordString(event.data.info.metadata, "sessionID");
+ if (sessionID === undefined) return;
+ shellSessionIds.set(event.data.info.id, sessionID);
+ const state = threads.get(sessionID);
+ if (state !== undefined) {
+ yield* registerShellProjection(state, event.data.info);
+ yield* updateProviderThread(state, {});
+ }
+ return;
+ }
+ case "shell.exited": {
+ yield* completeShellProjection(event.data.id, {
+ status: event.data.status,
+ ...(event.data.exit === undefined ? {} : { exit: event.data.exit }),
+ });
+ const sessionID = shellSessionIds.get(event.data.id);
+ shellProjections.delete(event.data.id);
+ shellSessionIds.delete(event.data.id);
+ if (sessionID === undefined) return;
+ const state = threads.get(sessionID);
+ if (state !== undefined) yield* updateProviderThread(state, {});
+ return;
+ }
+ case "shell.deleted": {
+ const sessionID = shellSessionIds.get(event.data.id);
+ if (sessionID === undefined) return;
+ const projection = shellProjections.get(event.data.id);
+ if (projection !== undefined && projection.turn.finalized) {
+ yield* completeShellProjection(event.data.id, { status: "killed" });
+ }
+ shellProjections.delete(event.data.id);
+ shellSessionIds.delete(event.data.id);
+ const state = threads.get(sessionID);
+ if (state !== undefined) yield* updateProviderThread(state, {});
+ return;
+ }
+ case "session.input.admitted": {
+ const state = threads.get(event.data.sessionID);
+ if (
+ state !== undefined &&
+ state.activeTurn === null &&
+ state.parentSubagent !== null
+ ) {
+ yield* createChildTurn(state, event.data.inputID);
+ }
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ if (active.turn.nativeInputId === null) {
+ active.turn.nativeInputId = event.data.inputID;
+ yield* emitProviderTurn(active.state, active.turn, "running", null);
+ }
+ const rootInputId = active.turn.nativeInputId;
+ const activeExecution = active.state.activeExecution;
+ if (
+ !isReplay &&
+ rootInputId !== null &&
+ activeExecution !== null &&
+ !activeExecution.claimedByPromotion &&
+ !activeExecution.inputIds.has(rootInputId) &&
+ activeExecution.inputIds.size === 0 &&
+ (active.turn.isRoot || active.state.postSettleWakes.length === 0)
+ ) {
+ const previousOwnerInputIds = new Set(activeExecution.inputIds);
+ // `session.execution.started` has only a session id. A
+ // promoted owner wins this boundary. Otherwise the ordinary
+ // admission claims the execution here, including when the
+ // fallback temporarily held pending wake ids. A later
+ // retired promotion joins the same boundary and remains
+ // suppressed. OpenCode cannot tell these orderings apart
+ // without an execution id, so this is the smallest
+ // deterministic policy.
+ activeExecution.inputIds.clear();
+ activeExecution.inputIds.add(rootInputId);
+ active.turn.executionStarted = true;
+ // Fallback ownership is only a safety hold until a known
+ // ordinary input arrives. Move cancelled wakes out of the
+ // execution so their later promotion still joins the same
+ // boundary and remains suppressed. An unpromoted replay wake
+ // cannot be distinguished from this execution either, so its
+ // buffered events are discarded rather than re-parented into
+ // the ordinary root turn.
+ retireUnownedSuppressWakes(active.state, activeExecution.inputIds);
+ for (let index = active.state.postSettleWakes.length - 1; index >= 0; index -= 1) {
+ const wake = active.state.postSettleWakes[index];
+ if (
+ wake !== undefined &&
+ wake.disposition === "replay" &&
+ previousOwnerInputIds.has(wake.inputId)
+ ) {
+ active.state.postSettleWakes.splice(index, 1);
+ active.state.promotedInputIds.delete(wake.inputId);
+ }
+ }
+ }
+ return;
+ }
+ case "session.text.started":
+ case "session.text.delta":
+ case "session.text.ended": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ yield* upsertTextPart(active.state, active.turn, "text", event.data, {
+ ...("delta" in event.data ? { delta: event.data.delta } : {}),
+ ...("text" in event.data ? { text: event.data.text } : {}),
+ ...(eventType === "session.text.ended" ? { completed: true } : {}),
+ });
+ return;
+ }
+ case "session.reasoning.started":
+ case "session.reasoning.delta":
+ case "session.reasoning.ended": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ yield* upsertTextPart(active.state, active.turn, "reasoning", event.data, {
+ ...("delta" in event.data ? { delta: event.data.delta } : {}),
+ ...("text" in event.data ? { text: event.data.text } : {}),
+ ...(eventType === "session.reasoning.ended" ? { completed: true } : {}),
+ });
+ return;
+ }
+ case "session.compaction.started": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const now = yield* DateTime.now;
+ const nativeItemId = String(event.data.inputID ?? event.id ?? "");
+ if (nativeItemId.length === 0) return;
+ const current = active.turn.activeCompaction;
+ if (current !== null && current.id !== nativeItemId && current.status === "running") {
+ current.status = "cancelled";
+ current.completedAt = now;
+ yield* emitCompaction(active.state, active.turn, current);
+ }
+ const compaction: OpenCode2Compaction =
+ current !== null && current.id === nativeItemId
+ ? current
+ : {
+ id: nativeItemId,
+ startedAt: dateTimeFromEpoch(openCode2WireCreatedMs(wire) ?? 0, now),
+ summary: "",
+ status: "running",
+ completedAt: null,
+ };
+ compaction.status = "running";
+ compaction.completedAt = null;
+ active.turn.activeCompaction = compaction;
+ yield* emitCompaction(active.state, active.turn, compaction);
+ return;
+ }
+ case "session.compaction.delta": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const now = yield* DateTime.now;
+ const compaction =
+ active.turn.activeCompaction ??
+ ({
+ id: event.id,
+ startedAt: dateTimeFromEpoch(openCode2WireCreatedMs(wire) ?? 0, now),
+ summary: "",
+ status: "running",
+ completedAt: null,
+ } satisfies OpenCode2Compaction);
+ compaction.summary += event.data.text;
+ if (compaction !== null)
+ active.turn.activeCompaction = compaction as OpenCode2Compaction;
+ yield* emitCompaction(active.state, active.turn, compaction as OpenCode2Compaction);
+ return;
+ }
+ case "session.compaction.ended": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const now = yield* DateTime.now;
+ const compaction =
+ active.turn.activeCompaction ??
+ ({
+ id: event.id,
+ startedAt: dateTimeFromEpoch(openCode2WireCreatedMs(wire) ?? 0, now),
+ summary: "",
+ status: "running",
+ completedAt: null,
+ } satisfies OpenCode2Compaction);
+ compaction.summary = event.data.text;
+ compaction.status = "completed";
+ compaction.completedAt = dateTimeFromEpoch(openCode2WireCreatedMs(wire) ?? 0, now);
+ if (compaction !== null)
+ active.turn.activeCompaction = compaction as OpenCode2Compaction;
+ yield* emitCompaction(active.state, active.turn, compaction as OpenCode2Compaction);
+ return;
+ }
+ case "session.tool.input.started": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ name: openCode2WireToolName(wire) ?? event.data?.name ?? event.data?.tool ?? "tool",
+ status: "pending",
+ });
+ return;
+ }
+ case "session.tool.input.delta": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ inputDelta: event.data?.delta,
+ });
+ return;
+ }
+ case "session.tool.called": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ input: event.data?.input,
+ status: "running",
+ });
+ return;
+ }
+ case "session.tool.progress": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ const output = toolContentText(event.data?.content);
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ ...(output === undefined ? {} : { output }),
+ structured: event.data?.structured,
+ status: "running",
+ });
+ return;
+ }
+ case "session.tool.success": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ const output = toolContentText(event.data?.content);
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ ...(output === undefined ? {} : { output }),
+ structured: event.data?.structured,
+ status: "completed",
+ });
+ return;
+ }
+ case "session.tool.failed": {
+ const active = activeFor(openCode2WireSessionID(wire) ?? event.data?.sessionID);
+ if (active === null) return;
+ const callID = openCode2WireCallID(wire);
+ if (callID === undefined) return;
+ const errorMessage =
+ (typeof event.data?.error?.message === "string"
+ ? event.data.error.message
+ : undefined) ?? openCode2WireErrorMessage(wire);
+ yield* upsertToolPart(active.state, active.turn, callID, {
+ output: errorMessage,
+ errorMessage,
+ status: "error",
+ });
+ return;
+ }
+ case "session.retry.scheduled": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const now = yield* DateTime.now;
+ const retry: OrchestrationV2ProviderRetry = {
+ attempt: Math.max(1, Math.floor(event.data.attempt)),
+ maxAttempts: null,
+ retryDelayMs: Math.max(
+ 0,
+ Math.floor(
+ (typeof event.data.at === "number"
+ ? event.data.at
+ : (openCode2WireCreatedMs(wire) ?? DateTime.toEpochMillis(now))) -
+ DateTime.toEpochMillis(now),
+ ),
+ ),
+ };
+ const failure = makeProviderFailure({
+ message: event.data.error.message,
+ code: event.data.error.type,
+ class: "provider_error",
+ retryable: true,
+ });
+ active.turn.providerRetry = {
+ retry,
+ failure,
+ startedAt:
+ active.turn.providerRetry?.startedAt ??
+ dateTimeFromEpoch(openCode2WireCreatedMs(wire) ?? 0, now),
+ };
+ const context = active.state.parentSubagent;
+ if (context !== null) {
+ context.status = "running";
+ context.progress =
+ event.data.error.type === "provider.rate-limit" ||
+ event.data.error.message.includes("429")
+ ? `Rate limited, retrying (attempt ${retry.attempt})`
+ : `Provider retry attempt ${retry.attempt}`;
+ yield* emitSubagentContext(context);
+ }
+ return;
+ }
+ case "permission.v2.asked": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const permission = normalizeOpenCode2PermissionEvent("v2", event.data);
+ const autoReply = openCode2PermissionAutoReplyForSession(
+ active.turn.runtimePolicy,
+ sessionPermissions,
+ event.data.sessionID,
+ permission,
+ );
+ if (autoReply !== null) {
+ const replied = yield* autoReplyPermission(
+ event.data.sessionID,
+ event.data.id,
+ autoReply,
+ );
+ if (replied) return;
+ }
+ const projection = runtimeRequestProjectionFor(active);
+ yield* emitRuntimeRequest(
+ projection.state,
+ projection.turn,
+ event.data.sessionID,
+ event.data.id,
+ {
+ type: "permission",
+ ...permission,
+ },
+ );
+ return;
+ }
+ case "permission.v2.replied":
+ yield* resolveRuntimeRequest(event.data.requestID, "resolved");
+ return;
+ case "question.v2.asked": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const projection = runtimeRequestProjectionFor(active);
+ yield* emitRuntimeRequest(
+ projection.state,
+ projection.turn,
+ event.data.sessionID,
+ event.data.id,
+ {
+ type: "question",
+ questions: event.data.questions,
+ },
+ );
+ return;
+ }
+ case "question.v2.replied":
+ yield* resolveRuntimeRequest(event.data.requestID, "resolved");
+ return;
+ case "question.v2.rejected":
+ yield* resolveRuntimeRequest(event.data.requestID, "cancelled");
+ return;
+ case "permission.asked": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const permission = normalizeOpenCode2PermissionEvent("legacy", event.data);
+ const autoReply = openCode2PermissionAutoReplyForSession(
+ active.turn.runtimePolicy,
+ sessionPermissions,
+ event.data.sessionID,
+ permission,
+ );
+ if (autoReply !== null) {
+ const replied = yield* autoReplyPermission(
+ event.data.sessionID,
+ event.data.id,
+ autoReply,
+ );
+ if (replied) return;
+ }
+ const projection = runtimeRequestProjectionFor(active);
+ yield* emitRuntimeRequest(
+ projection.state,
+ projection.turn,
+ event.data.sessionID,
+ event.data.id,
+ {
+ type: "permission",
+ ...permission,
+ },
+ );
+ return;
+ }
+ case "permission.replied":
+ yield* resolveRuntimeRequest(event.data.requestID, "resolved");
+ return;
+ case "question.asked": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ const projection = runtimeRequestProjectionFor(active);
+ yield* emitRuntimeRequest(
+ projection.state,
+ projection.turn,
+ event.data.sessionID,
+ event.data.id,
+ {
+ type: "question",
+ questions: event.data.questions,
+ },
+ );
+ return;
+ }
+ case "question.replied":
+ yield* resolveRuntimeRequest(event.data.requestID, "resolved");
+ return;
+ case "question.rejected":
+ yield* resolveRuntimeRequest(event.data.requestID, "cancelled");
+ return;
+ case "session.execution.started": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ // Execution terminals carry only a session id. This start event
+ // is the correlation barrier that keeps a late prior terminal
+ // from settling a turn whose input did not own this execution.
+ if (
+ !activeTurnOwnsOpenCode2Execution(
+ active.state,
+ active.turn,
+ context.replayWakeInputId,
+ )
+ ) {
+ return;
+ }
+ active.turn.executionStarted = true;
+ return;
+ }
+ case "session.execution.succeeded": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ if (
+ !activeTurnOwnsOpenCode2Execution(
+ active.state,
+ active.turn,
+ context.replayWakeInputId,
+ )
+ ) {
+ return;
+ }
+ if (
+ !active.turn.executionStarted &&
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: active.turn.executionStarted,
+ interrupted: active.turn.interrupted,
+ partCount: active.turn.parts.size,
+ })
+ ) {
+ active.turn.executionStarted = true;
+ }
+ if (
+ !openCode2ShouldSettleTurn(
+ "execution-terminal",
+ active.turn.executionStarted,
+ active.turn.interrupted,
+ )
+ ) {
+ return;
+ }
+ // step.ended can mean "tool-calls continue"; only settle
+ // full-turn terminals.
+ if (!openCode2StepFinishSettlesTurn(openCode2WireData(wire).finish)) {
+ return;
+ }
+ yield* finalizeTurn(
+ active.state,
+ active.turn,
+ active.turn.interrupted ? "interrupted" : "completed",
+ );
+ if (!isReplay) active.state.activeExecution = null;
+ return;
+ }
+ case "session.execution.failed": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ if (
+ !activeTurnOwnsOpenCode2Execution(
+ active.state,
+ active.turn,
+ context.replayWakeInputId,
+ )
+ ) {
+ return;
+ }
+ if (
+ !active.turn.executionStarted &&
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: active.turn.executionStarted,
+ interrupted: active.turn.interrupted,
+ partCount: active.turn.parts.size,
+ })
+ ) {
+ active.turn.executionStarted = true;
+ }
+ if (!openCode2ShouldSettleTurn("execution-terminal", active.turn.executionStarted)) {
+ return;
+ }
+ if (active.turn.interrupted) {
+ yield* finalizeTurn(active.state, active.turn, "interrupted");
+ if (!isReplay) active.state.activeExecution = null;
+ return;
+ }
+ const message = event.data.error.message;
+ if (active.turn.isRoot) yield* updateProviderSession("error", message);
+ yield* finalizeTurn(active.state, active.turn, "failed", {
+ failure: makeProviderFailure({
+ message,
+ code: event.data.error.type,
+ class: "provider_error",
+ retryable: active.turn.providerRetry === null ? null : true,
+ }),
+ });
+ if (!isReplay) active.state.activeExecution = null;
+ return;
+ }
+ case "session.execution.interrupted": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ if (
+ !activeTurnOwnsOpenCode2Execution(
+ active.state,
+ active.turn,
+ context.replayWakeInputId,
+ )
+ ) {
+ return;
+ }
+ active.turn.interrupted = true;
+ if (
+ !active.turn.executionStarted &&
+ openCode2CanAdoptMissingExecutionStart({
+ executionStarted: active.turn.executionStarted,
+ interrupted: active.turn.interrupted,
+ partCount: active.turn.parts.size,
+ })
+ ) {
+ active.turn.executionStarted = true;
+ }
+ if (
+ !openCode2ShouldSettleTurn(
+ "execution-interrupted",
+ active.turn.executionStarted,
+ true,
+ )
+ ) {
+ return;
+ }
+ yield* finalizeTurn(active.state, active.turn, "interrupted");
+ if (!isReplay) active.state.activeExecution = null;
+ return;
+ }
+ // 2.x settles on `session.execution.*`; `session.idle` is only a
+ // backstop for builds that never enter the authoritative lifecycle.
+ case "session.idle": {
+ const active = activeFor(event.data.sessionID);
+ if (active === null) return;
+ if (
+ !activeTurnOwnsOpenCode2Execution(
+ active.state,
+ active.turn,
+ context.replayWakeInputId,
+ )
+ ) {
+ return;
+ }
+ if (!openCode2ShouldSettleTurn("idle", active.turn.executionStarted)) return;
+ yield* finalizeTurn(
+ active.state,
+ active.turn,
+ active.turn.interrupted ? "interrupted" : "completed",
+ );
+ if (!isReplay) active.state.activeExecution = null;
+ return;
+ }
+ case "session.error": {
+ const activeSessionIDs = Array.from(threads.values())
+ .filter((state) => state.activeTurn !== null && !state.activeTurn.finalized)
+ .map((state) => state.nativeSessionId);
+ const targetSessionIDs = openCode2SessionErrorTargetSessionIds(
+ event.data.sessionID,
+ activeSessionIDs,
+ );
+ const message = openCode2SessionErrorMessage(event.data);
+ const isAbort = event.data.error?.name === "MessageAbortedError";
+ const targetsRoot =
+ event.data.sessionID === undefined ||
+ targetSessionIDs.some(
+ (sessionID) => threads.get(sessionID)?.parentSubagent === null,
+ );
+ if (!isAbort && targetsRoot) yield* updateProviderSession("error", message);
+ for (const sessionID of targetSessionIDs) {
+ const active = activeFor(sessionID);
+ if (active === null) continue;
+ yield* finalizeTurn(
+ active.state,
+ active.turn,
+ openCode2SessionErrorStatus(event.data, active.turn.interrupted),
+ {
+ failure: makeProviderFailure({
+ message,
+ code: event.data.error?.name ?? null,
+ class: "provider_error",
+ }),
+ threadDisposition: event.data.sessionID === undefined ? "broken" : "reusable",
+ },
+ );
+ }
+ // Finalizing one of several active turns temporarily marks the
+ // shared provider session as running. Restore the unscoped
+ // provider failure after every affected turn has closed.
+ if (!isAbort && targetsRoot && targetSessionIDs.length > 1) {
+ yield* updateProviderSession("error", message);
+ }
+ return;
+ }
+ default:
+ return;
+ }
+ });
+
+ yield* Scope.addFinalizer(
+ scope,
+ Effect.sync(() => abortController.abort()),
+ );
+ // First event.subscribe runs on this fiber so ensureThread cannot race
+ // ahead under TestClock (fork-only subscribe lost to agent.list/create).
+ // Drain + resubscribe stay forked after the first outbound is established.
+ const firstStreamController = new AbortController();
+ const onFirstSessionAbort = () => firstStreamController.abort();
+ if (abortController.signal.aborted) {
+ firstStreamController.abort();
+ } else {
+ abortController.signal.addEventListener("abort", onFirstSessionAbort, { once: true });
+ }
+ const firstSubscription = yield* sdkCall("event.subscribe", {}, () =>
+ client.v2.event.subscribe({ signal: firstStreamController.signal }),
+ );
+ lastEventAtMs = yield* Clock.currentTimeMillis;
+
+ const consumeEventStream = (stream: AsyncIterable) =>
+ Stream.fromAsyncIterable(
+ stream,
+ (cause) =>
+ new OpenCode2RuntimeError({
+ operation: "event.subscribe",
+ category: "event-subscription-failed",
+ cause,
+ }),
+ ).pipe(
+ Stream.tap((event) =>
+ Clock.currentTimeMillis.pipe(
+ Effect.map((now) => {
+ lastEventAtMs = now;
+ consecutiveStreamFailures = 0;
+ // server.connected alone is not progress; do not clear
+ // stall resubscribe budget on reconnect acks.
+ if ((event as { readonly type?: string }).type !== "server.connected") {
+ consecutiveStallResubscribes = 0;
+ }
+ }),
+ ),
+ ),
+ Stream.runForEach(handleEvent),
+ Effect.exit,
+ );
+
+ // Resubscribe loop: `/api/event` is volatile (slow consumer overflows).
+ // A single failed or hung pull must not leave active turns uninterruptible.
+ yield* Effect.gen(function* () {
+ let pendingStream: AsyncIterable | null = firstSubscription.stream;
+ let streamController = firstStreamController;
+ let onSessionAbort = onFirstSessionAbort;
+
+ while (!abortController.signal.aborted) {
+ if (pendingStream === null) {
+ streamController = new AbortController();
+ onSessionAbort = () => streamController.abort();
+ if (abortController.signal.aborted) {
+ streamController.abort();
+ } else {
+ abortController.signal.addEventListener("abort", onSessionAbort, { once: true });
+ }
+ }
+
+ const watchdog = yield* Effect.gen(function* () {
+ while (!streamController.signal.aborted && !abortController.signal.aborted) {
+ yield* Effect.sleep(`${OPENCODE2_EVENT_STALL_CHECK_MS} millis`);
+ const hasActiveTurn = Array.from(threads.values()).some(
+ (threadState) => threadState.activeTurn !== null,
+ );
+ const now = yield* Clock.currentTimeMillis;
+ const lastEventAgeMs = now - lastEventAtMs;
+ if (
+ !openCode2ShouldResubscribeStalledStream({
+ sessionAborted: abortController.signal.aborted,
+ hasActiveTurn,
+ lastEventAgeMs,
+ stallMs: OPENCODE2_EVENT_STALL_MS,
+ })
+ ) {
+ continue;
+ }
+ if (consecutiveStallResubscribes >= OPENCODE2_EVENT_STALL_MAX_RESUBSCRIBES) {
+ yield* Effect.logError(
+ "OpenCode 2 event stream stall budget exhausted; failing active turns.",
+ {
+ provider: OPENCODE2_PROVIDER,
+ stallMs: lastEventAgeMs,
+ consecutiveStallResubscribes,
+ },
+ );
+ yield* failActiveTurns(
+ "OpenCode 2 event stream stalled and did not recover.",
+ "transport_error",
+ );
+ streamController.abort();
+ return;
+ }
+ consecutiveStallResubscribes += 1;
+ yield* Effect.logWarning(
+ "OpenCode 2 event stream stalled while a turn is active; resubscribing.",
+ {
+ provider: OPENCODE2_PROVIDER,
+ stallMs: lastEventAgeMs,
+ consecutiveStallResubscribes,
+ },
+ );
+ streamController.abort();
+ return;
+ }
+ }).pipe(Effect.forkIn(scope));
+
+ const exit = yield* Effect.gen(function* () {
+ if (pendingStream !== null) {
+ const stream = pendingStream;
+ pendingStream = null;
+ return yield* consumeEventStream(stream);
+ }
+ const subscription = yield* sdkCall("event.subscribe", {}, () =>
+ client.v2.event.subscribe({ signal: streamController.signal }),
+ );
+ return yield* consumeEventStream(subscription.stream);
+ }).pipe(
+ Effect.catchCause((cause) =>
+ Effect.succeed(
+ Exit.fail(
+ new OpenCode2RuntimeError({
+ operation: "event.subscribe",
+ category: "event-subscription-failed",
+ cause: Cause.squash(cause),
+ }),
+ ),
+ ),
+ ),
+ );
+
+ streamController.abort();
+ abortController.signal.removeEventListener("abort", onSessionAbort);
+ yield* Fiber.interrupt(watchdog).pipe(Effect.ignore);
+
+ if (abortController.signal.aborted) return;
+
+ const hasActiveTurn = Array.from(threads.values()).some(
+ (threadState) => threadState.activeTurn !== null,
+ );
+
+ if (Exit.isFailure(exit)) {
+ consecutiveStreamFailures += 1;
+ const failure = Cause.squash(exit.cause);
+ yield* Effect.logWarning(
+ "OpenCode 2 event subscription ended; will resubscribe when possible.",
+ {
+ errorTag: causeErrorTag(exit.cause),
+ provider: OPENCODE2_PROVIDER,
+ consecutiveStreamFailures,
+ },
+ );
+ if (
+ consecutiveStreamFailures >= OPENCODE2_EVENT_STREAM_MAX_FAILURES &&
+ hasActiveTurn
+ ) {
+ yield* failActiveTurns(openCodeRuntimeErrorDetail(failure), "transport_error");
+ consecutiveStreamFailures = 0;
+ }
+ } else if (!hasActiveTurn) {
+ // Clean EOF while idle: wait for session close rather than opening a
+ // second subscribe. Replay fixtures end the stream this way; a live
+ // idle session almost never EOFs cleanly, and the next openSession
+ // creates a fresh adapter when needed.
+ while (!abortController.signal.aborted) {
+ yield* Effect.sleep("1 second");
+ }
+ return;
+ }
+
+ lastEventAtMs = yield* Clock.currentTimeMillis;
+ yield* Effect.sleep(`${OPENCODE2_EVENT_RESUBSCRIBE_DELAY_MS} millis`);
+ }
+ }).pipe(Effect.forkIn(scope));
+
+ if (!connection.external && connection.exitCode !== null) {
+ yield* connection.exitCode.pipe(
+ Effect.flatMap((code) =>
+ abortController.signal.aborted
+ ? Effect.void
+ : failActiveTurns(
+ `OpenCode 2 server exited unexpectedly (${code}).`,
+ "transport_error",
+ ),
+ ),
+ Effect.forkIn(scope),
+ );
+ }
+
+ const registerThread = (
+ nativeSession: SessionInfoV2,
+ providerThread: OrchestrationV2ProviderThread,
+ ): OpenCode2ThreadState => {
+ const existing = threads.get(nativeSession.id);
+ if (existing !== undefined) {
+ existing.location = nativeSession.location;
+ existing.providerThread = providerThread;
+ if (nativeSession.model !== undefined) {
+ existing.boundModel = `${nativeSession.model.providerID}/${nativeSession.model.id}`;
+ existing.boundVariant =
+ normalizeOpenCode2Variant(nativeSession.model.variant) ?? null;
+ }
+ existing.boundAgent = nativeSession.agent ?? existing.boundAgent;
+ return existing;
+ }
+ const state: OpenCode2ThreadState = {
+ nativeSessionId: nativeSession.id,
+ location: nativeSession.location,
+ providerThread,
+ appThread: null,
+ activeTurn: null,
+ boundModel:
+ nativeSession.model === undefined
+ ? null
+ : `${nativeSession.model.providerID}/${nativeSession.model.id}`,
+ boundVariant: normalizeOpenCode2Variant(nativeSession.model?.variant) ?? null,
+ boundAgent: nativeSession.agent ?? null,
+ lastAgentSelectedEventId: null,
+ providerTurns: new Map(),
+ messages: new Map(),
+ runtimeRequests: new Map(),
+ postSettleWakes: [],
+ retiredSuppressWakes: new Map(),
+ promotedInputIds: new Set(),
+ sawInputPromotion: false,
+ activeExecution: null,
+ parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null,
+ nextChildTurnOrdinal: 1,
+ };
+ threads.set(nativeSession.id, state);
+ return state;
+ };
+
+ /**
+ * Catalog for `clampOpenCode2Variant`. Cached per provider session: it
+ * only changes when the spawned server restarts. A fresh 2.x server
+ * reports an empty catalog until bootstrap finishes, so an empty
+ * result is used for the current call but never cached, and a failed
+ * fetch is not cached either, so later turns retry. Only successful
+ * non-empty fetches are stored, which also keeps a losing concurrent
+ * fetch from clobbering a good cache.
+ */
+ let variantCatalog: ReadonlyMap> | null = null;
+ const readVariantCatalog = sdkCall("model.list", {}, () =>
+ client.v2.model.list({ location: { directory: cwd } }),
+ ).pipe(
+ Effect.flatMap((response) =>
+ unwrapOpenCode2Data>("model.list", response).pipe(
+ Effect.map((models) => {
+ const catalog = new Map>();
+ for (const model of models) {
+ catalog.set(
+ `${model.providerID}/${model.id}`,
+ new Set(model.variants.map((entry) => entry.id)),
+ );
+ }
+ return catalog as ReadonlyMap>;
+ }),
+ ),
+ ),
+ Effect.catchCause((cause) =>
+ Cause.hasInterrupts(cause)
+ ? Effect.interrupt
+ : Effect.logWarning("Failed to load the OpenCode 2 variant catalog.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ }).pipe(Effect.as(null)),
+ ),
+ );
+ const knownVariantsForModel = Effect.fnUntraced(function* (modelSlug: string) {
+ if (variantCatalog !== null) return variantCatalog.get(modelSlug) ?? null;
+ const fetched = yield* retryEmptyOpenCode2VariantCatalog(readVariantCatalog);
+ if (fetched !== null && fetched.size > 0 && variantCatalog === null) {
+ variantCatalog = fetched;
+ }
+ return fetched?.get(modelSlug) ?? null;
+ });
+
+ let agentCatalog: ReadonlySet | null = null;
+ const knownAgentIDs = Effect.fnUntraced(function* () {
+ if (agentCatalog !== null) return agentCatalog;
+ const fetched = yield* sdkCall("agent.list", {}, () =>
+ client.v2.agent.list({ location: { directory: cwd } }),
+ ).pipe(
+ Effect.flatMap((response) =>
+ unwrapOpenCode2Data>("agent.list", response).pipe(
+ Effect.map(
+ (agents) => new Set(agents.map((agent) => agent.id)) as ReadonlySet,
+ ),
+ ),
+ ),
+ Effect.catchCause((cause) =>
+ Cause.hasInterrupts(cause)
+ ? Effect.interrupt
+ : Effect.logWarning("Failed to load the OpenCode 2 agent catalog.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ }).pipe(Effect.as(null)),
+ ),
+ );
+ if (fetched?.has("build") && fetched.has("plan")) {
+ agentCatalog = fetched;
+ }
+ return fetched;
+ });
+
+ const warnDroppedVariant = (modelSlug: string, droppedVariant: string | null) =>
+ droppedVariant === null
+ ? Effect.void
+ : Effect.logWarning("Dropping a variant the OpenCode 2 catalog cannot validate.", {
+ provider: OPENCODE2_PROVIDER,
+ model: modelSlug,
+ variant: droppedVariant,
+ });
+
+ /**
+ * 2.x binds the model, variant, and agent to the session, not to the
+ * prompt, so a selection change between turns has to be pushed before
+ * the prompt. A variant-less switch resets the session to the
+ * server-resolved default variant.
+ */
+ const alignSessionSelection = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ modelSelection: ModelSelection,
+ interactionMode?: ProviderInteractionMode,
+ ) {
+ const sessionID = state.nativeSessionId;
+ // Subagent child sessions run their own native agents (general,
+ // explore, customs); the Build/Plan mapping only owns top-level
+ // sessions.
+ const selection = openCode2SessionSelectionParameters(
+ modelSelection,
+ state.parentSubagent === null ? interactionMode : undefined,
+ state.parentSubagent === null ? yield* knownAgentIDs() : null,
+ );
+ const plan = planOpenCode2VariantAlignment({
+ boundModel: state.boundModel,
+ boundVariant: state.boundVariant,
+ model: modelSelection.model,
+ rawVariant: getModelSelectionStringOptionValue(modelSelection, "variant"),
+ knownVariants:
+ selection.model.variant === undefined
+ ? null
+ : yield* knownVariantsForModel(modelSelection.model),
+ });
+ yield* warnDroppedVariant(modelSelection.model, plan.droppedVariant);
+ if (plan.switchNeeded) {
+ const model = {
+ id: selection.model.id,
+ providerID: selection.model.providerID,
+ ...(plan.variant === undefined ? {} : { variant: plan.variant }),
+ };
+ yield* sdkCall("session.switchModel", { sessionID, model }, () =>
+ client.v2.session.switchModel({ sessionID, model }),
+ );
+ state.boundModel = modelSelection.model;
+ state.boundVariant = plan.variant ?? null;
+ }
+ const agent = selection.agent;
+ if (agent !== undefined && state.boundAgent !== agent) {
+ yield* sdkCall("session.switchAgent", { sessionID, agent }, () =>
+ client.v2.session.switchAgent({ sessionID, agent }),
+ );
+ state.boundAgent = agent;
+ }
+ });
+
+ // next-16916+ prompt body is flat `{ text, files?, delivery? }`. The
+ // pinned beta SDK still types/maps a nested `prompt` field, which the
+ // server rejects with `Missing key at ["text"]`. Post through the raw
+ // hey-api client so the wire matches the running binary.
+ const promptPayload = (message: ProviderAdapterV2TurnInput["message"]) => {
+ const text = message.text.trim();
+ const files = toOpenCode2FileAttachments({
+ attachments: message.attachments,
+ resolveAttachmentPath: (attachment) =>
+ resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }),
+ });
+ if (text.length === 0 && files.length === 0) {
+ throw protocolError("OpenCode 2 turns require text or file attachments");
+ }
+ return {
+ text: text.length === 0 ? " " : text,
+ ...(files.length === 0 ? {} : { files }),
+ };
+ };
+
+ const postSessionPrompt = (input: {
+ readonly sessionID: string;
+ readonly text: string;
+ readonly files?: ReturnType;
+ readonly delivery?: "steer" | "queue";
+ }) => {
+ const rawClient = (
+ client as unknown as {
+ client: {
+ post: (options: Record) => Promise;
+ };
+ }
+ ).client;
+ return rawClient.post({
+ url: "/api/session/{sessionID}/prompt",
+ path: { sessionID: input.sessionID },
+ body: {
+ text: input.text,
+ ...(input.files === undefined || input.files.length === 0
+ ? {}
+ : { files: input.files }),
+ ...(input.delivery === undefined ? {} : { delivery: input.delivery }),
+ },
+ headers: { "Content-Type": "application/json" },
+ throwOnError: true,
+ });
+ };
+
+ const readSnapshot = Effect.fnUntraced(function* (
+ providerThread: OrchestrationV2ProviderThread,
+ ) {
+ const sessionID = nativeThreadId(providerThread);
+ const response = yield* sdkCall("message.list", { sessionID }, () =>
+ client.v2.session.messages({ sessionID }),
+ );
+ const nativeMessages = yield* unwrapOpenCode2Data>(
+ "message.list",
+ response,
+ );
+ const state = threads.get(sessionID);
+ const snapshotNow = yield* DateTime.now;
+ const messages: Array = nativeMessages.flatMap(
+ (info) => {
+ let text = "";
+ if (info.type === "user") {
+ text = info.text;
+ } else if (info.type === "assistant") {
+ text = info.content
+ .filter((entry) => entry.type === "text")
+ .map((entry) => entry.text)
+ .join("\n");
+ }
+ if (text.trim().length === 0) return [];
+ const createdAt = dateTimeFromEpoch(info.time.created, snapshotNow);
+ return [
+ {
+ createdBy: info.type === "user" ? ("user" as const) : ("agent" as const),
+ creationSource: "provider" as const,
+ id: idAllocator.derive.messageFromProviderItem({
+ driver: OPENCODE2_PROVIDER,
+ nativeItemId: info.id,
+ }),
+ threadId: providerThread.appThreadId ?? input.threadId,
+ runId: null,
+ nodeId: null,
+ role: info.type === "user" ? ("user" as const) : ("assistant" as const),
+ text,
+ attachments: [],
+ streaming: false,
+ createdAt,
+ updatedAt: createdAt,
+ },
+ ];
+ },
+ );
+ const lastUser = nativeMessages.findLast((info) => info.type === "user")?.id;
+ return {
+ providerThread: {
+ ...providerThread,
+ providerSessionId: input.providerSessionId,
+ nativeConversationHeadRef:
+ lastUser === undefined ? null : providerRef(lastUser, "weak"),
+ status: "idle" as const,
+ updatedAt: snapshotNow,
+ },
+ providerTurns: state === undefined ? [] : [...state.providerTurns.values()],
+ messages,
+ runtimeRequests: state === undefined ? [] : [...state.runtimeRequests.values()],
+ providerPayload: nativeMessages,
+ };
+ });
+
+ const inspectPendingBackgroundWork = Effect.fnUntraced(function* (
+ state: OpenCode2ThreadState,
+ ) {
+ const sessionID = state.nativeSessionId;
+ return yield* openCode2PendingWorkForSession({
+ sessionID,
+ // Prefer live Session3 when present; replay client still implements
+ // these routes so fixtures can assert the post-settle probes.
+ pending: sdkCall("session.pending.list", { sessionID }, () => {
+ const pendingList = (
+ client.v2.session as {
+ pending?: { list: (input: { sessionID: string }) => Promise };
+ }
+ ).pending?.list;
+ if (pendingList === undefined) {
+ return Promise.resolve({ data: { data: [] as Array } });
+ }
+ return pendingList({ sessionID });
+ }).pipe(
+ Effect.flatMap((response) =>
+ unwrapOpenCode2Data>("session.pending.list", response),
+ ),
+ ),
+ shells: sdkCall("shell.list", { location: state.location }, () => {
+ const shellList = (
+ client.v2 as {
+ shell?: {
+ list: (input: { location: SessionInfoV2["location"] }) => Promise;
+ };
+ }
+ ).shell?.list;
+ if (shellList === undefined) {
+ return Promise.resolve({ data: { data: [] as Array } });
+ }
+ return shellList({ location: state.location });
+ }).pipe(
+ Effect.flatMap((response) =>
+ unwrapOpenCode2Data>("shell.list", response),
+ ),
+ Effect.tap((shells) =>
+ Effect.sync(() => {
+ for (const shell of shells) {
+ if (shell.metadata.sessionID === sessionID) {
+ shellSessionIds.set(shell.id, sessionID);
+ }
+ }
+ }),
+ ),
+ ),
+ });
+ });
+
+ const hasPendingBackgroundWorkForState = (state: OpenCode2ThreadState) =>
+ inspectPendingBackgroundWork(state).pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning("Failed to inspect OpenCode 2 pending background work.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ providerThreadId: state.providerThread.id,
+ }).pipe(Effect.as(false)),
+ ),
+ );
+
+ const waitForT3Mcp = Effect.fnUntraced(function* () {
+ if (!hasT3Mcp) return;
+ let lastStatus = "missing";
+ for (let attempt = 0; attempt < 50; attempt++) {
+ const listed = yield* sdkCall("mcp.list", {}, () =>
+ client.mcp.status().then((response) => ({
+ data: {
+ data: Object.entries(
+ (response as { data?: Record }).data ?? {},
+ ).map(([name, status]) => ({ name, status })),
+ },
+ })),
+ ).pipe(
+ Effect.map((response) => ({ available: true as const, response })),
+ Effect.catch((error: OpenCode2RuntimeError) => {
+ // Beta lildax has no /mcp routes; do not block session open.
+ const detail = openCodeRuntimeErrorDetail(error.cause).toLowerCase();
+ if (detail.includes("404") || detail.includes("not found")) {
+ return Effect.succeed({ available: false as const, response: null });
+ }
+ return Effect.fail(error);
+ }),
+ );
+ if (!listed.available) return;
+ const servers = yield* unwrapOpenCode2Data>(
+ "mcp.list",
+ listed.response,
+ );
+ const server = servers.find((candidate) => candidate.name === OPENCODE2_T3_MCP_NAME);
+ lastStatus = server === undefined ? "missing" : mcpServerStatus(server);
+ if (lastStatus === "connected") return;
+ if (lastStatus !== "missing" && lastStatus !== "pending") {
+ return yield* new OpenCode2RuntimeError({
+ operation: "mcp.list",
+ category: "mcp-connect-failed",
+ cause: server?.status,
+ });
+ }
+ if (attempt < 49) yield* Effect.sleep("100 millis");
+ }
+ return yield* new OpenCode2RuntimeError({
+ operation: "mcp.list",
+ category: "mcp-connect-timeout",
+ });
+ });
+
+ const installT3OrchestrationInstructions = Effect.fnUntraced(function* (sessionID: string) {
+ if (!hasT3Mcp) return;
+ yield* sdkCall(
+ "session.instructions.entry.put",
+ { sessionID, key: OPENCODE2_T3_INSTRUCTION_KEY },
+ () => Promise.resolve({ data: { data: true } }),
+ );
+ });
+
+ yield* waitForT3Mcp();
+
+ const runtimeSession: ProviderAdapterV2SessionRuntime = {
+ instanceId: options.instanceId,
+ driver: OPENCODE2_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ providerSession: sessionEntity,
+ events: Stream.fromEffectRepeat(Queue.take(events)),
+ hasPendingBackgroundWork: Effect.gen(function* () {
+ for (const state of threads.values()) {
+ if (yield* hasPendingBackgroundWorkForState(state)) return true;
+ }
+ return false;
+ }),
+ hasPendingBackgroundWorkForThread: (providerThread) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(providerThread);
+ const state = threads.get(sessionID);
+ if (state === undefined) {
+ return yield* protocolError(
+ `OpenCode 2 session ${sessionID} is not registered for pending-work inspection`,
+ );
+ }
+ return yield* inspectPendingBackgroundWork(state);
+ }).pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning("Failed to inspect OpenCode 2 pending background work.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ providerThreadId: providerThread.id,
+ }).pipe(Effect.as(false)),
+ ),
+ ),
+ ensureThread: (threadInput) =>
+ Effect.gen(function* () {
+ if (threadInput.existingProviderThread !== undefined) {
+ return yield* runtimeSession.resumeThread({
+ providerThread: threadInput.existingProviderThread,
+ });
+ }
+ const selection = openCode2SessionSelectionParameters(
+ threadInput.modelSelection,
+ threadInput.runtimePolicy.interactionMode,
+ yield* knownAgentIDs(),
+ );
+ const agent = selection.agent;
+ const clamp = clampOpenCode2Variant(
+ selection.model.variant,
+ selection.model.variant === undefined
+ ? null
+ : yield* knownVariantsForModel(threadInput.modelSelection.model),
+ );
+ yield* warnDroppedVariant(threadInput.modelSelection.model, clamp.droppedVariant);
+ const parameters = {
+ ...selection,
+ model: {
+ id: selection.model.id,
+ providerID: selection.model.providerID,
+ ...(clamp.variant === undefined ? {} : { variant: clamp.variant }),
+ },
+ location: { directory: threadInput.runtimePolicy.cwd ?? cwd },
+ };
+ const response = yield* sdkCall("session.create", parameters, () =>
+ client.v2.session.create(parameters),
+ );
+ const nativeSession = yield* unwrapOpenCode2Data(
+ "session.create",
+ response,
+ );
+ yield* installT3OrchestrationInstructions(nativeSession.id);
+ const createdAt = yield* DateTime.now;
+ const providerThread = makeProviderThread({
+ idAllocator,
+ providerInstanceId: options.instanceId,
+ providerSessionId: input.providerSessionId,
+ appThreadId: threadInput.threadId,
+ nativeSession,
+ now: createdAt,
+ });
+ const state = registerThread(nativeSession, providerThread);
+ state.boundModel = threadInput.modelSelection.model;
+ state.boundVariant = clamp.variant ?? null;
+ if (agent !== undefined) state.boundAgent = agent;
+ return providerThread;
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterEnsureThreadError({
+ driver: OPENCODE2_PROVIDER,
+ threadId: threadInput.threadId,
+ cause,
+ }),
+ ),
+ ),
+ resumeThread: (threadInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(threadInput.providerThread);
+ const response = yield* sdkCall("session.get", { sessionID }, () =>
+ client.v2.session.get({ sessionID }),
+ );
+ const nativeSession = yield* unwrapOpenCode2Data(
+ "session.get",
+ response,
+ );
+ yield* installT3OrchestrationInstructions(sessionID);
+ const resumedAt = yield* DateTime.now;
+ const providerThread = {
+ ...threadInput.providerThread,
+ providerSessionId: input.providerSessionId,
+ status: "idle" as const,
+ updatedAt: dateTimeFromEpoch(nativeSession.time.updated, resumedAt),
+ };
+ registerThread(nativeSession, providerThread);
+ return providerThread;
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterResumeThreadError({
+ driver: OPENCODE2_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ providerThreadId: threadInput.providerThread.id,
+ cause,
+ }),
+ ),
+ ),
+ deleteThread: (providerThread) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(providerThread);
+ yield* removeOpenCode2Session(
+ sessionID,
+ sdkCall("session.remove", { sessionID }, () =>
+ client.v2.session.get({ sessionID }, { throwOnError: false }).then(async () => {
+ // Beta Session3 has no remove(); best-effort interrupt then rely on GC.
+ try {
+ await client.v2.session.interrupt({ sessionID });
+ } catch {
+ /* ignore */
+ }
+ return { data: { data: true } };
+ }),
+ ),
+ );
+ threads.delete(sessionID);
+ sessionPermissions.delete(sessionID);
+ }).pipe(
+ Effect.mapError((cause) =>
+ protocolError(`Failed to delete OpenCode 2 session ${providerThread.id}`, cause),
+ ),
+ ),
+ startTurn: (turnInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(turnInput.providerThread);
+ const state = threads.get(sessionID);
+ if (state === undefined) {
+ return yield* protocolError(`OpenCode 2 session ${sessionID} is not registered`);
+ }
+ if (state.activeTurn !== null) {
+ return yield* protocolError(
+ `OpenCode 2 provider thread ${turnInput.providerThread.id} already has an active turn`,
+ );
+ }
+ if (
+ spawnedWithInjectedAllowPolicy &&
+ !warnedAboutInjectedAllowPolicy &&
+ !isOpenCodeAllowAllPolicy(turnInput.runtimePolicy)
+ ) {
+ warnedAboutInjectedAllowPolicy = true;
+ yield* Effect.logWarning(
+ "OpenCode 2 session was spawned with an allow-all permission policy; a stricter runtime mode will not re-gate suppressed permission asks until the session is reopened.",
+ {
+ provider: OPENCODE2_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ threadId: turnInput.threadId,
+ runtimeMode: turnInput.runtimePolicy.runtimeMode,
+ },
+ );
+ }
+ const providerBufferedContinuation =
+ turnInput.message.createdBy === "agent" &&
+ turnInput.message.creationSource === "provider";
+ const wake = (() => {
+ if (!providerBufferedContinuation) return undefined;
+ const readyWakeIndex = state.postSettleWakes.findIndex(
+ (candidate) => candidate.disposition === "replay" && candidate.phase === "ready",
+ );
+ const wakeIndex =
+ readyWakeIndex >= 0
+ ? readyWakeIndex
+ : state.postSettleWakes.findIndex(
+ (candidate) => candidate.disposition === "replay",
+ );
+ if (wakeIndex < 0) return undefined;
+ const replayWake = state.postSettleWakes.splice(wakeIndex, 1)[0];
+ if (replayWake !== undefined) state.promotedInputIds.delete(replayWake.inputId);
+ return replayWake;
+ })();
+ const startedAt = yield* DateTime.now;
+ const syntheticNativeTurnId = `${sessionID}:attempt:${turnInput.attemptId}`;
+ const providerTurnId = idAllocator.derive.providerTurn({
+ driver: OPENCODE2_PROVIDER,
+ nativeTurnId: syntheticNativeTurnId,
+ });
+ const providerTurn: OrchestrationV2ProviderTurn = {
+ id: providerTurnId,
+ providerThreadId: turnInput.providerThread.id,
+ nodeId: turnInput.rootNodeId,
+ runAttemptId: turnInput.attemptId,
+ nativeTurnRef: providerRef(syntheticNativeTurnId, "weak"),
+ ordinal: turnInput.providerTurnOrdinal,
+ status: "running",
+ startedAt,
+ completedAt: null,
+ };
+ const turn: ActiveOpenCode2Turn = {
+ isRoot: true,
+ providerBufferedContinuation,
+ threadId: turnInput.threadId,
+ runId: turnInput.runId,
+ rootNodeId: turnInput.rootNodeId,
+ appThread: turnInput.appThread,
+ modelSelection: turnInput.modelSelection,
+ runtimePolicy: turnInput.runtimePolicy,
+ providerTurnId,
+ runOrdinal: turnInput.runOrdinal,
+ startedAt,
+ itemOrdinals: new Map(),
+ parts: new Map(),
+ toolIdsByCallId: new Map(),
+ providerTurn,
+ nextItemOrdinal: turnInput.providerTurnOrdinal * 100 + 1,
+ nativeInputId: wake?.inputId ?? null,
+ activeCompaction: null,
+ executionStarted: false,
+ interrupted: false,
+ finalized: false,
+ providerRetry: null,
+ };
+ state.appThread = turnInput.appThread;
+ state.activeTurn = turn;
+ state.providerTurns.set(String(providerTurnId), providerTurn);
+ yield* emitProviderTurn(state, turn, "running", null);
+ yield* updateProviderThread(state, {
+ status: "active",
+ firstRunOrdinal: state.providerThread.firstRunOrdinal ?? turnInput.runOrdinal,
+ lastRunOrdinal: turnInput.runOrdinal,
+ });
+ yield* updateProviderSession("running", null);
+ if (providerBufferedContinuation) {
+ // OpenCode already ran this input. The app turn only gives its
+ // buffered native events durable run ownership.
+ if (wake === undefined) {
+ yield* finalizeTurn(state, turn, "completed");
+ return;
+ }
+ for (const event of wake.events) {
+ yield* handleEvent(event, { replayWakeInputId: wake.inputId });
+ }
+ return;
+ }
+ const payload = promptPayload(turnInput.message);
+ yield* alignSessionSelection(
+ state,
+ turnInput.modelSelection,
+ turnInput.runtimePolicy.interactionMode,
+ );
+ const prompted = yield* sdkCall("session.prompt", { sessionID, ...payload }, () =>
+ postSessionPrompt({ sessionID, ...payload }),
+ ).pipe(
+ Effect.tapError((cause) =>
+ finalizeTurn(state, turn, "failed", {
+ failure: makeProviderFailure({ cause, class: "provider_error" }),
+ }),
+ ),
+ );
+ // Arm the stall watchdog from the prompt boundary so a long first
+ // token does not immediately resubscribe, but a dead stream after
+ // prompt still recovers.
+ lastEventAtMs = yield* Clock.currentTimeMillis;
+ // The admitted input id is the closest native turn correlation
+ // point 2.x offers, and it arrives on the prompt response before
+ // `session.input.admitted` reaches the event stream. next-16916
+ // returns a single-wrapped body (`data.id`); older beta SDKs used
+ // the double envelope (`data.data.id`).
+ const promptedBody =
+ prompted !== null && typeof prompted === "object" && "data" in prompted
+ ? (prompted as { data?: unknown }).data
+ : undefined;
+ const admittedId =
+ recordString(promptedBody, "id") ??
+ recordString(
+ promptedBody !== null &&
+ typeof promptedBody === "object" &&
+ "data" in promptedBody
+ ? (promptedBody as { data?: unknown }).data
+ : undefined,
+ "id",
+ );
+ if (admittedId !== undefined && turn.nativeInputId === null) {
+ turn.nativeInputId = admittedId;
+ yield* emitProviderTurn(state, turn, "running", null);
+ }
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterTurnStartError({
+ driver: OPENCODE2_PROVIDER,
+ threadId: turnInput.threadId,
+ providerThreadId: turnInput.providerThread.id,
+ runId: turnInput.runId,
+ cause,
+ }),
+ ),
+ ),
+ steerTurn: (steerInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(steerInput.providerThread);
+ const state = threads.get(sessionID);
+ const turn = state?.activeTurn;
+ if (
+ state === undefined ||
+ turn === undefined ||
+ turn === null ||
+ turn.providerTurnId !== steerInput.providerTurnId
+ ) {
+ return yield* protocolError(
+ `OpenCode 2 turn ${steerInput.providerTurnId} is not active`,
+ );
+ }
+ const payload = promptPayload(steerInput.message);
+ yield* sdkCall("session.prompt", { sessionID, ...payload, delivery: "steer" }, () =>
+ postSessionPrompt({ sessionID, ...payload, delivery: "steer" }),
+ );
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterSteerRunError({
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: steerInput.providerThread.id,
+ providerTurnId: steerInput.providerTurnId,
+ cause,
+ }),
+ ),
+ ),
+ interruptTurn: (interruptInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(interruptInput.providerThread);
+ const state = threads.get(sessionID);
+ if (state === undefined) {
+ return yield* protocolError(
+ `OpenCode 2 turn ${interruptInput.providerTurnId} is not active`,
+ );
+ }
+ const turn = state.activeTurn;
+ if (turn === null || turn.providerTurnId !== interruptInput.providerTurnId) {
+ return yield* protocolError(
+ `OpenCode 2 turn ${interruptInput.providerTurnId} is not active`,
+ );
+ }
+ turn.interrupted = true;
+ // Bound the interrupt RPC: a full SSE Recv-Q has wedged concurrent
+ // HTTP before, and Stop must not hang on that path.
+ const interruptedRemote = yield* sdkCallWithTimeout(
+ "session.interrupt",
+ { sessionID },
+ () => client.v2.session.interrupt({ sessionID }),
+ OPENCODE2_INTERRUPT_REQUEST_TIMEOUT_MS,
+ );
+ if (Option.isNone(interruptedRemote)) {
+ yield* Effect.logWarning(
+ "OpenCode 2 session.interrupt did not complete in time; force-settling locally.",
+ {
+ provider: OPENCODE2_PROVIDER,
+ providerTurnId: turn.providerTurnId,
+ timeoutMs: OPENCODE2_INTERRUPT_REQUEST_TIMEOUT_MS,
+ },
+ );
+ }
+ yield* removeRunningShellsForTurn(turn).pipe(
+ Effect.timeoutOption(`${OPENCODE2_INTERRUPT_REQUEST_TIMEOUT_MS} millis`),
+ Effect.catchCause((cause) =>
+ Effect.logWarning("Failed to stop OpenCode 2 shells during interrupt.", {
+ errorTag: causeErrorTag(cause),
+ provider: OPENCODE2_PROVIDER,
+ providerTurnId: turn.providerTurnId,
+ }).pipe(Effect.as(Option.none())),
+ ),
+ Effect.asVoid,
+ );
+ // Prefer SSE-driven `session.execution.interrupted` finalization.
+ // When the event stream is dead, force-finalize so Stop returns
+ // the run to a terminal state (mirrors CursorAdapterV2).
+ const settleStartedAt = yield* Clock.currentTimeMillis;
+ while (true) {
+ if (turn.finalized || state.activeTurn !== turn) return;
+ const now = yield* Clock.currentTimeMillis;
+ const waitedMs = now - settleStartedAt;
+ if (
+ openCode2ShouldForceInterruptFinalize({
+ interrupted: turn.interrupted,
+ finalized: turn.finalized,
+ stillActive: state.activeTurn === turn,
+ waitedMs,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ })
+ ) {
+ break;
+ }
+ yield* Effect.sleep(`${OPENCODE2_INTERRUPT_SETTLE_POLL_MS} millis`);
+ }
+ if (turn.finalized || state.activeTurn !== turn) return;
+ yield* Effect.logWarning(
+ "OpenCode 2 interrupt settle timed out; force-finalizing the turn.",
+ {
+ provider: OPENCODE2_PROVIDER,
+ providerTurnId: turn.providerTurnId,
+ settleTimeoutMs: OPENCODE2_INTERRUPT_SETTLE_TIMEOUT_MS,
+ },
+ );
+ yield* finalizeTurn(state, turn, "interrupted");
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterInterruptError({
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: interruptInput.providerThread.id,
+ providerTurnId: interruptInput.providerTurnId,
+ cause,
+ }),
+ ),
+ ),
+ respondToRuntimeRequest: (requestInput) =>
+ Effect.gen(function* () {
+ const pending = pendingRequests.get(String(requestInput.requestId));
+ if (pending === undefined) {
+ return yield* protocolError(
+ `No pending OpenCode 2 request ${requestInput.requestId}`,
+ );
+ }
+ const sessionID = pending.nativeSessionId;
+ const requestID = pending.nativeRequestId;
+ if (pending.questions !== undefined) {
+ if (requestInput.answers === undefined) {
+ return yield* protocolError(
+ `OpenCode 2 question request ${requestInput.requestId} requires answers`,
+ );
+ }
+ const answers = pending.questions.map((question, index) => {
+ const raw =
+ requestInput.answers?.[openCode2QuestionId(index, question.header)] ??
+ requestInput.answers?.[question.header] ??
+ requestInput.answers?.[question.question];
+ if (Array.isArray(raw)) {
+ return raw.filter((value): value is string => typeof value === "string");
+ }
+ if (typeof raw === "string") return raw.trim().length > 0 ? [raw] : [];
+ return [];
+ });
+ yield* sdkCall("session.question.reply", { sessionID, requestID, answers }, () =>
+ client.v2.session.question.reply({
+ sessionID,
+ requestID,
+ questionV2Reply: { answers },
+ }),
+ );
+ return;
+ }
+ if (requestInput.decision === undefined) {
+ return yield* protocolError(
+ `OpenCode 2 approval request ${requestInput.requestId} requires a decision`,
+ );
+ }
+ if (requestInput.decision === "acceptForSession") {
+ rememberOpenCode2SessionPermission(
+ sessionPermissions,
+ sessionID,
+ pending.permission,
+ );
+ }
+ const reply =
+ requestInput.decision === "accept" || requestInput.decision === "acceptForSession"
+ ? ("once" as const)
+ : ("reject" as const);
+ yield* sdkCall("session.permission.reply", { sessionID, requestID, reply }, () =>
+ client.v2.session.permission.reply({ sessionID, requestID, reply }),
+ );
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterRuntimeRequestResponseError({
+ driver: OPENCODE2_PROVIDER,
+ requestId: requestInput.requestId,
+ cause,
+ }),
+ ),
+ ),
+ readThreadSnapshot: (snapshotInput) =>
+ readSnapshot(snapshotInput.providerThread).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterReadThreadSnapshotError({
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: snapshotInput.providerThread.id,
+ cause,
+ }),
+ ),
+ ),
+ rollbackThread: (rollbackInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(rollbackInput.providerThread);
+ const state = threads.get(sessionID);
+ if (state?.activeTurn !== null && state?.activeTurn !== undefined) {
+ return yield* protocolError(
+ `Cannot roll back OpenCode 2 thread ${rollbackInput.providerThread.id} while a turn is active`,
+ );
+ }
+ const response = yield* sdkCall("message.list", { sessionID }, () =>
+ client.v2.session.messages({ sessionID }),
+ );
+ const nativeMessages = yield* unwrapOpenCode2Data>(
+ "message.list",
+ response,
+ );
+ let boundaryMessageId: string | undefined;
+ if (rollbackInput.target.type === "thread_start") {
+ boundaryMessageId = nativeMessages.find((info) => info.type === "user")?.id;
+ } else {
+ boundaryMessageId = openCodeBoundaryAfterProviderTurn(
+ rollbackInput.providerThreadTurns,
+ rollbackInput.target.providerTurn.id,
+ );
+ }
+ if (boundaryMessageId !== undefined) {
+ // Stage then commit: 2.x split 1.x's single `session.revert`
+ // into a reversible boundary plus an explicit commit.
+ yield* sdkCall(
+ "session.revert.stage",
+ { sessionID, messageID: boundaryMessageId, files: true },
+ () =>
+ client.v2.session.revert.stage({
+ sessionID,
+ messageID: boundaryMessageId!,
+ files: true,
+ }),
+ );
+ yield* sdkCall("session.revert.commit", { sessionID }, () =>
+ client.v2.session.revert.commit({ sessionID }),
+ );
+ }
+ const snapshot = yield* readSnapshot(rollbackInput.providerThread);
+ return {
+ ...snapshot,
+ providerThread: {
+ ...snapshot.providerThread,
+ nativeConversationHeadRef:
+ rollbackInput.target.type === "provider_turn"
+ ? rollbackInput.target.providerTurn.nativeTurnRef
+ : null,
+ },
+ };
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterRollbackThreadError({
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: rollbackInput.providerThread.id,
+ checkpointId: rollbackInput.target.checkpointId,
+ cause,
+ }),
+ ),
+ ),
+ forkThread: (forkInput) =>
+ Effect.gen(function* () {
+ const sessionID = nativeThreadId(forkInput.sourceProviderThread);
+ const sourceState = threads.get(sessionID);
+ if (sourceState?.activeTurn !== null && sourceState?.activeTurn !== undefined) {
+ return yield* protocolError(
+ `Cannot fork OpenCode 2 thread ${forkInput.sourceProviderThread.id} while a turn is active`,
+ );
+ }
+ let boundaryMessageId: string | undefined;
+ if (forkInput.providerTurnId !== undefined) {
+ const sourceTurns = forkInput.sourceProviderTurns ?? [];
+ const selected = sourceTurns.find((turn) => turn.id === forkInput.providerTurnId);
+ if (selected === undefined) {
+ return yield* protocolError(
+ `OpenCode 2 fork boundary turn ${forkInput.providerTurnId} was not found`,
+ );
+ }
+ boundaryMessageId = openCodeBoundaryAfterProviderTurn(sourceTurns, selected.id);
+ }
+ const parameters = openCode2ForkParameters(sessionID, boundaryMessageId);
+ const response = yield* sdkCall("session.fork", parameters, () =>
+ Promise.reject(
+ new Error("OpenCode 2 beta session.fork is not available on Session3"),
+ ),
+ );
+ const nativeSession = yield* unwrapOpenCode2Data(
+ "session.fork",
+ response,
+ );
+ yield* installT3OrchestrationInstructions(nativeSession.id);
+ const forkedAt = yield* DateTime.now;
+ const providerThread = makeProviderThread({
+ idAllocator,
+ providerInstanceId: options.instanceId,
+ providerSessionId: input.providerSessionId,
+ appThreadId: forkInput.targetThreadId,
+ ...(forkInput.ownerNodeId === undefined
+ ? {}
+ : { ownerNodeId: forkInput.ownerNodeId }),
+ nativeSession,
+ forkedFrom: {
+ providerThreadId: forkInput.sourceProviderThread.id,
+ ...(forkInput.providerTurnId === undefined
+ ? {}
+ : { providerTurnId: forkInput.providerTurnId }),
+ },
+ now: forkedAt,
+ });
+ registerThread(nativeSession, providerThread);
+ return providerThread;
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterForkThreadError({
+ driver: OPENCODE2_PROVIDER,
+ providerThreadId: forkInput.sourceProviderThread.id,
+ cause,
+ }),
+ ),
+ ),
+ };
+
+ return runtimeSession;
+ },
+ (effect, input) =>
+ effect.pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterOpenSessionError({
+ driver: OPENCODE2_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ cause,
+ }),
+ ),
+ ),
+ ),
+ });
+}
+
+export type OpenCode2AdapterV2DriverEnv =
+ | OpenCode2Runtime
+ | IdAllocatorV2
+ | ProviderEventLoggers
+ | ServerConfig;
+
+export const OpenCode2AdapterV2Driver: ProviderAdapterDriver<
+ OpenCode2Settings,
+ OpenCode2AdapterV2DriverEnv
+> = {
+ driverKind: OPENCODE2_DRIVER_KIND,
+ configSchema: OpenCode2SettingsSchema,
+ defaultConfig: (): OpenCode2Settings => DEFAULT_OPENCODE2_SETTINGS,
+ create: Effect.fn("OpenCode2AdapterV2Driver.create")(
+ function* (input: ProviderAdapterDriverCreateInput) {
+ const hostEnvironment = yield* HostProcessEnvironment;
+ const openCode2Runtime = yield* OpenCode2Runtime;
+ const idAllocator = yield* IdAllocatorV2;
+ const continuationRequests = yield* ProviderContinuationRequests;
+ const interactionModeReflections = yield* ProviderInteractionModeReflections;
+ const providerEventLoggers = yield* ProviderEventLoggers;
+ const serverConfig = yield* ServerConfig;
+ return makeOpenCode2AdapterV2({
+ instanceId: input.instanceId,
+ settings: { ...input.config, enabled: input.enabled },
+ environment: applyOpenCode2ProviderEnvironment(
+ input.config,
+ mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
+ ),
+ runtime: openCode2Runtime,
+ idAllocator,
+ serverConfig,
+ continuationRequests,
+ interactionModeReflections,
+ ...(providerEventLoggers.native === undefined
+ ? {}
+ : { nativeEventLogger: providerEventLoggers.native }),
+ });
+ },
+ (effect, input) =>
+ effect.pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterDriverCreateError({
+ driver: OPENCODE2_DRIVER_KIND,
+ instanceId: input.instanceId,
+ detail: "Failed to create OpenCode 2 v2 adapter.",
+ cause,
+ }),
+ ),
+ ),
+ ),
+};
diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts
index e0834bec858..064cc588da7 100644
--- a/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts
@@ -659,6 +659,26 @@ function toolStatus(part: ToolPart): {
}
}
+/**
+ * Node/item statuses for a tool that never reported its own terminal state.
+ * Mirrors the turn's outcome so an interrupted turn does not leave a spinner.
+ */
+export function terminalToolStatus(status: TerminalTurnStatus): {
+ readonly node: OrchestrationV2ExecutionNode["status"];
+ readonly item: OrchestrationV2TurnItem["status"];
+} {
+ switch (status) {
+ case "completed":
+ return { node: "completed", item: "completed" };
+ case "interrupted":
+ return { node: "interrupted", item: "interrupted" };
+ case "cancelled":
+ return { node: "cancelled", item: "cancelled" };
+ case "failed":
+ return { node: "failed", item: "failed" };
+ }
+}
+
function toolInput(part: ToolPart): Record {
return part.state.input;
}
@@ -1538,6 +1558,7 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid
label: option.label.trim() || "Option",
description: option.description.trim() || option.label.trim() || "Option",
})),
+ multiSelect: false,
}));
const runtimeRequestTurnItem = (
diff --git a/apps/server/src/orchestration-v2/Adapters/openCode2Wire.ts b/apps/server/src/orchestration-v2/Adapters/openCode2Wire.ts
new file mode 100644
index 00000000000..bbdd1307e84
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Adapters/openCode2Wire.ts
@@ -0,0 +1,263 @@
+/**
+ * OpenCode 2 wire helpers. Maps runtime type strings onto the adapter switch's
+ * internal names. next-16916 emits short forms (`session.step.*`,
+ * `session.text.*`); earlier beta builds used `session.next.*` for the same
+ * lifecycle. Both are accepted.
+ */
+
+/** Canonical event type names used by the adapter switch. */
+export type OpenCode2CanonicalEventType =
+ | "session.created"
+ | "session.input.admitted"
+ | "session.agent.selected"
+ | "session.model.selected"
+ | "session.shell.started"
+ | "session.shell.ended"
+ | "shell.created"
+ | "shell.exited"
+ | "shell.deleted"
+ | "session.text.started"
+ | "session.text.delta"
+ | "session.text.ended"
+ | "session.reasoning.started"
+ | "session.reasoning.delta"
+ | "session.reasoning.ended"
+ | "session.compaction.started"
+ | "session.compaction.delta"
+ | "session.compaction.ended"
+ | "session.tool.input.started"
+ | "session.tool.input.delta"
+ | "session.tool.input.ended"
+ | "session.tool.called"
+ | "session.tool.progress"
+ | "session.tool.success"
+ | "session.tool.failed"
+ | "session.retry.scheduled"
+ | "session.execution.started"
+ | "session.execution.succeeded"
+ | "session.execution.failed"
+ | "session.execution.interrupted"
+ | "session.idle"
+ | "session.error"
+ | "permission.v2.asked"
+ | "permission.v2.replied"
+ | "permission.asked"
+ | "permission.replied"
+ | "question.v2.asked"
+ | "question.v2.replied"
+ | "question.v2.rejected"
+ | "question.asked"
+ | "question.replied"
+ | "question.rejected"
+ | "server.connected"
+ | "unknown";
+
+/**
+ * Lifecycle renames. Internal switch cases keep short canonical names.
+ * Accept both `session.next.*` (earlier beta) and short forms (next-16916).
+ */
+const WIRE_TYPE_ALIASES: Readonly> = {
+ "session.agent.switched": "session.agent.selected",
+ "session.model.switched": "session.model.selected",
+ "session.next.agent.switched": "session.agent.selected",
+ "session.next.model.switched": "session.model.selected",
+ "session.next.prompt.admitted": "session.input.admitted",
+ "session.next.prompted": "session.input.admitted",
+ "session.next.shell.started": "session.shell.started",
+ "session.next.shell.ended": "session.shell.ended",
+ "session.next.step.started": "session.execution.started",
+ // step.ended is not always a full-turn terminal (tool-calls continues).
+ // Handlers inspect finish before settling.
+ "session.next.step.ended": "session.execution.succeeded",
+ "session.next.step.failed": "session.execution.failed",
+ "session.next.text.started": "session.text.started",
+ "session.next.text.delta": "session.text.delta",
+ "session.next.text.ended": "session.text.ended",
+ "session.next.reasoning.started": "session.reasoning.started",
+ "session.next.reasoning.delta": "session.reasoning.delta",
+ "session.next.reasoning.ended": "session.reasoning.ended",
+ "session.next.compaction.started": "session.compaction.started",
+ "session.next.compaction.delta": "session.compaction.delta",
+ "session.next.compaction.ended": "session.compaction.ended",
+ "session.next.tool.input.started": "session.tool.input.started",
+ "session.next.tool.input.delta": "session.tool.input.delta",
+ "session.next.tool.input.ended": "session.tool.input.ended",
+ "session.next.tool.called": "session.tool.called",
+ "session.next.tool.progress": "session.tool.progress",
+ "session.next.tool.success": "session.tool.success",
+ "session.next.tool.failed": "session.tool.failed",
+ "session.next.retried": "session.retry.scheduled",
+ "session.prompt.admitted": "session.input.admitted",
+ "session.prompted": "session.input.admitted",
+ "session.step.started": "session.execution.started",
+ "session.step.ended": "session.execution.succeeded",
+ "session.step.failed": "session.execution.failed",
+ "session.retried": "session.retry.scheduled",
+};
+
+const PASSTHROUGH_TYPES = new Set([
+ "session.created",
+ "session.execution.interrupted",
+ "session.idle",
+ "session.error",
+ "session.text.started",
+ "session.text.delta",
+ "session.text.ended",
+ "session.reasoning.started",
+ "session.reasoning.delta",
+ "session.reasoning.ended",
+ "session.compaction.started",
+ "session.compaction.delta",
+ "session.compaction.ended",
+ "session.tool.input.started",
+ "session.tool.input.delta",
+ "session.tool.input.ended",
+ "session.tool.called",
+ "session.tool.progress",
+ "session.tool.success",
+ "session.tool.failed",
+ "session.shell.started",
+ "session.shell.ended",
+ "server.connected",
+ "shell.created",
+ "shell.exited",
+ "shell.deleted",
+ "permission.v2.asked",
+ "permission.v2.replied",
+ "permission.asked",
+ "permission.replied",
+ "question.v2.asked",
+ "question.v2.replied",
+ "question.v2.rejected",
+ "question.asked",
+ "question.replied",
+ "question.rejected",
+]);
+
+export function normalizeOpenCode2WireType(type: string): OpenCode2CanonicalEventType {
+ if (type in WIRE_TYPE_ALIASES) return WIRE_TYPE_ALIASES[type]!;
+ if (PASSTHROUGH_TYPES.has(type)) return type as OpenCode2CanonicalEventType;
+ return "unknown";
+}
+
+function isRecord(value: unknown): value is Record {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+}
+
+export function openCode2WireData(event: { readonly data?: unknown }): Record {
+ return isRecord(event.data) ? event.data : {};
+}
+
+export function openCode2WireCreatedMs(event: {
+ readonly created?: number;
+ readonly data?: unknown;
+}): number | undefined {
+ if (typeof event.created === "number" && Number.isFinite(event.created)) {
+ return event.created;
+ }
+ const data = openCode2WireData(event);
+ const timestamp = data.timestamp;
+ return typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : undefined;
+}
+
+export function openCode2WireSessionID(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ const value = data.sessionID ?? data.sessionId;
+ return typeof value === "string" && value.length > 0 ? value : undefined;
+}
+
+export function openCode2WireCallID(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ // next-16916 tool events key the call with `id` (and put the tool name on
+ // `name`). Earlier beta builds used `callID` / `callId` for the same field.
+ const value = data.callID ?? data.callId ?? data.id;
+ return typeof value === "string" && value.length > 0 ? value : undefined;
+}
+
+export function openCode2WireToolName(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ for (const key of ["name", "tool"] as const) {
+ const value = data[key];
+ if (typeof value === "string" && value.length > 0) return value;
+ }
+ return undefined;
+}
+
+export function openCode2WireTextDelta(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ const value = data.delta ?? data.text;
+ return typeof value === "string" ? value : undefined;
+}
+
+export function openCode2WireInputID(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ const value = data.inputID ?? data.inputId ?? data.messageID ?? data.messageId ?? data.id;
+ return typeof value === "string" && value.length > 0 ? value : undefined;
+}
+
+export function openCode2WireErrorMessage(event: { readonly data?: unknown }): string {
+ const data = openCode2WireData(event);
+ const error = data.error;
+ if (isRecord(error) && typeof error.message === "string" && error.message.length > 0) {
+ return error.message;
+ }
+ if (typeof error === "string" && error.length > 0) return error;
+ return "OpenCode 2 provider error";
+}
+
+export function openCode2WireErrorCode(event: { readonly data?: unknown }): string | null {
+ const data = openCode2WireData(event);
+ const error = data.error;
+ if (!isRecord(error)) return null;
+ const type = error.type ?? error.name;
+ return typeof type === "string" && type.length > 0 ? type : null;
+}
+
+/**
+ * Whether a step.ended event should settle the full turn. Multi-step loops end
+ * intermediate steps with tool-calls finishes.
+ */
+export function openCode2StepFinishSettlesTurn(finish: unknown): boolean {
+ if (typeof finish !== "string" || finish.length === 0) {
+ // Missing finish is treated as terminal (failed steps, synthetic settles).
+ return true;
+ }
+ const normalized = finish.trim().toLowerCase();
+ if (
+ normalized === "tool-calls" ||
+ normalized === "tool_calls" ||
+ normalized === "tool-call" ||
+ normalized === "tool_call"
+ ) {
+ return false;
+ }
+ return true;
+}
+
+export function openCode2WireAgent(event: { readonly data?: unknown }): string | undefined {
+ const data = openCode2WireData(event);
+ const agent = data.agent ?? data.info;
+ if (typeof agent === "string" && agent.length > 0) return agent;
+ if (isRecord(agent) && typeof agent.agent === "string") return agent.agent;
+ return undefined;
+}
+
+/**
+ * Unwrap SDK responses that may be single- or double-enveloped
+ * (`{ data: T }` vs `{ data: { data: T } }`).
+ */
+export function unwrapOpenCode2Payload(result: unknown): A | undefined {
+ if (!isRecord(result)) return undefined;
+ const outer = result.data;
+ if (outer === undefined || outer === null) return undefined;
+ // Prefer the double-wrapped body envelope `{ data: T }` used by /api responses.
+ if (isRecord(outer) && "data" in outer) {
+ const inner = outer.data;
+ if (inner !== undefined && inner !== null) return inner as A;
+ return undefined;
+ }
+ // Single-wrap bodies (arrays, concrete objects) are usable as-is. An empty
+ // outer envelope is the silent failure mode this guard exists for.
+ if (isRecord(outer) && Object.keys(outer).length === 0) return undefined;
+ return outer as A;
+}
diff --git a/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts
index 52c3bb0c1cc..2c5b57aa185 100644
--- a/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts
+++ b/apps/server/src/orchestration-v2/CursorOrchestratorV2.live.test.ts
@@ -23,6 +23,8 @@ import {
NoOpProviderEventLoggers,
ProviderEventLoggers,
} from "../provider/Layers/ProviderEventLoggers.ts";
+import * as OpenCode2Runtime from "../provider/opencode2Runtime.ts";
+import * as SpawnedProcessReaper from "../provider/SpawnedProcessReaper.ts";
import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
@@ -62,6 +64,10 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
NodeServices.layer,
FetchHttpClient.layer,
OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)),
+ OpenCode2Runtime.layer.pipe(
+ Layer.provide(SpawnedProcessReaper.layer),
+ Layer.provide(NodeServices.layer),
+ ),
Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers),
),
),
diff --git a/apps/server/src/orchestration-v2/EffectOutbox.ts b/apps/server/src/orchestration-v2/EffectOutbox.ts
index 85180279fc7..ed3825c2cb8 100644
--- a/apps/server/src/orchestration-v2/EffectOutbox.ts
+++ b/apps/server/src/orchestration-v2/EffectOutbox.ts
@@ -3,10 +3,13 @@ import {
CheckpointScopeId,
CommandId,
MessageId,
+ OrchestrationV2ProviderSessionJson,
+ OrchestrationV2ProviderThreadJson,
ProviderSessionId,
RunAttemptId,
ProviderApprovalDecision,
ProviderUserInputAnswers,
+ ProviderInstanceId,
ProviderThreadId,
ProviderTurnId,
RunId,
@@ -30,6 +33,12 @@ export const OrchestrationEffectRequestV2 = Schema.Union([
detail: Schema.optional(Schema.String),
/** Set on terminal detaches (thread archive/delete): revoke the thread's MCP credentials. */
revokeMcpCredential: Schema.optional(Schema.Boolean),
+ /** Set only on thread deletion: remove the provider-native thread before detaching. */
+ deleteProviderThread: Schema.optional(Schema.Boolean),
+ /** Persisted deletion targets for retries after the managed runtime has stopped. */
+ providerInstanceId: Schema.optional(ProviderInstanceId),
+ providerSession: Schema.optional(OrchestrationV2ProviderSessionJson),
+ providerThreads: Schema.optional(Schema.Array(OrchestrationV2ProviderThreadJson)),
}),
Schema.Struct({
type: Schema.Literal("provider-turn.start"),
diff --git a/apps/server/src/orchestration-v2/EffectWorker.ts b/apps/server/src/orchestration-v2/EffectWorker.ts
index 1139d2fea3b..bbd1d5221f1 100644
--- a/apps/server/src/orchestration-v2/EffectWorker.ts
+++ b/apps/server/src/orchestration-v2/EffectWorker.ts
@@ -106,6 +106,18 @@ export const executorLayer: Layer.Layer<
...(effect.request.revokeMcpCredential === undefined
? {}
: { revokeMcpCredential: effect.request.revokeMcpCredential }),
+ ...(effect.request.deleteProviderThread === undefined
+ ? {}
+ : { deleteProviderThread: effect.request.deleteProviderThread }),
+ ...(effect.request.providerInstanceId === undefined
+ ? {}
+ : { providerInstanceId: effect.request.providerInstanceId }),
+ ...(effect.request.providerSession === undefined
+ ? {}
+ : { providerSession: effect.request.providerSession }),
+ ...(effect.request.providerThreads === undefined
+ ? {}
+ : { providerThreads: effect.request.providerThreads }),
})
.pipe(
Effect.mapError(
diff --git a/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts b/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts
index 5e5bc62ba0d..351c77253cf 100644
--- a/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts
+++ b/apps/server/src/orchestration-v2/GrokOrchestratorV2.live.test.ts
@@ -23,6 +23,8 @@ import {
NoOpProviderEventLoggers,
ProviderEventLoggers,
} from "../provider/Layers/ProviderEventLoggers.ts";
+import * as OpenCode2Runtime from "../provider/opencode2Runtime.ts";
+import * as SpawnedProcessReaper from "../provider/SpawnedProcessReaper.ts";
import { OpenCodeRuntimeLive } from "../provider/opencodeRuntime.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
@@ -61,6 +63,10 @@ const providerInstanceRegistryLayer = ProviderInstanceRegistryHydrationLive.pipe
NodeServices.layer,
FetchHttpClient.layer,
OpenCodeRuntimeLive.pipe(Layer.provide(NodeServices.layer)),
+ OpenCode2Runtime.layer.pipe(
+ Layer.provide(SpawnedProcessReaper.layer),
+ Layer.provide(NodeServices.layer),
+ ),
Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers),
),
),
diff --git a/apps/server/src/orchestration-v2/Orchestrator.settled.test.ts b/apps/server/src/orchestration-v2/Orchestrator.settled.test.ts
new file mode 100644
index 00000000000..e55b4277450
--- /dev/null
+++ b/apps/server/src/orchestration-v2/Orchestrator.settled.test.ts
@@ -0,0 +1,969 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { assert, it } from "@effect/vitest";
+import {
+ CommandId,
+ EventId,
+ MessageId,
+ type ModelSelection,
+ NodeId,
+ type OrchestrationV2AppThread,
+ type OrchestrationV2DomainEvent,
+ type OrchestrationV2ProviderThread,
+ type OrchestrationV2ProviderTurn,
+ type OrchestrationV2Run,
+ type OrchestrationV2Subagent,
+ ProjectId,
+ ProviderDriverKind,
+ ProviderInstanceId,
+ ProviderSessionId,
+ ProviderThreadId,
+ ProviderTurnId,
+ RunId,
+ ThreadId,
+} from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Stream from "effect/Stream";
+import * as SqlClient from "effect/unstable/sql/SqlClient";
+
+import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
+import { ServerConfig } from "../config.ts";
+import { layer as mcpSessionRegistryTestLayer } from "../mcp/McpSessionRegistry.testkit.ts";
+import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
+import type { ProviderInstance } from "../provider/ProviderDriver.ts";
+import { ProviderInstanceRegistry } from "../provider/Services/ProviderInstanceRegistry.ts";
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
+import * as VcsProcess from "../vcs/VcsProcess.ts";
+import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
+import { EventSinkV2 } from "./EventSink.ts";
+import { OrchestratorV2 } from "./Orchestrator.ts";
+import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
+import { threadShellFromProjection } from "./ProjectionStore.ts";
+import { OrchestrationV2EventSinkLayerLive, OrchestrationV2LayerLive } from "./runtimeLayer.ts";
+
+const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
+ prefix: "t3-orchestration-v2-settled-",
+});
+
+const modelSelection = {
+ instanceId: ProviderInstanceId.make("codex"),
+ model: "gpt-5.4",
+} satisfies ModelSelection;
+
+const driver = ProviderDriverKind.make("codex");
+const orchestrationAdapter = {
+ instanceId: modelSelection.instanceId,
+ driver,
+ getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
+ planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
+ openSession: () => Effect.die("sessions are not used by settle tests"),
+} as ProviderAdapterV2Shape;
+const providerInstance = {
+ instanceId: modelSelection.instanceId,
+ driverKind: driver,
+ continuationIdentity: {
+ driverKind: driver,
+ continuationKey: "codex:test",
+ },
+ displayName: "Codex test",
+ enabled: true,
+ snapshot: {} as ProviderInstance["snapshot"],
+ orchestrationAdapter,
+ textGeneration: {} as ProviderInstance["textGeneration"],
+} satisfies ProviderInstance;
+
+const TestProviderInstanceRegistry = Layer.succeed(ProviderInstanceRegistry, {
+ getInstance: (instanceId) =>
+ Effect.succeed(instanceId === providerInstance.instanceId ? providerInstance : undefined),
+ listInstances: Effect.succeed([providerInstance]),
+ listUnavailable: Effect.succeed([]),
+ streamChanges: Stream.empty,
+ subscribeChanges: Effect.never,
+});
+
+const VcsDriverRegistryTestLayer = VcsDriverRegistry.layer.pipe(
+ Layer.provide(VcsProcess.layer),
+ Layer.provide(ServerConfigLayer),
+ Layer.provide(NodeServices.layer),
+);
+
+const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(
+ Layer.provide(VcsDriverRegistryTestLayer),
+);
+
+const TestLayer = Layer.merge(OrchestrationV2LayerLive, OrchestrationV2EventSinkLayerLive).pipe(
+ Layer.provide(mcpSessionRegistryTestLayer),
+ Layer.provideMerge(SqlitePersistenceMemory),
+ Layer.provide(CheckpointStoreTestLayer),
+ Layer.provide(ServerConfigLayer),
+ Layer.provide(ServerSettingsService.layerTest()),
+ Layer.provide(TestProviderInstanceRegistry),
+ Layer.provide(NodeServices.layer),
+);
+
+function createThreadCommand(input: {
+ readonly commandId: string;
+ readonly threadId: ThreadId;
+ readonly projectId: ProjectId;
+}) {
+ return {
+ type: "thread.create" as const,
+ createdBy: "user" as const,
+ creationSource: "web" as const,
+ commandId: CommandId.make(input.commandId),
+ threadId: input.threadId,
+ projectId: input.projectId,
+ title: "Settled lifecycle thread",
+ modelSelection,
+ runtimeMode: "full-access" as const,
+ interactionMode: "default" as const,
+ branch: null,
+ worktreePath: null,
+ };
+}
+
+function makeRun(input: {
+ readonly runId: RunId;
+ readonly threadId: ThreadId;
+ readonly status: OrchestrationV2Run["status"];
+ readonly now: DateTime.Utc;
+ readonly providerThreadId?: ProviderThreadId | null;
+}): OrchestrationV2Run {
+ return {
+ id: input.runId,
+ threadId: input.threadId,
+ ordinal: 1,
+ providerInstanceId: modelSelection.instanceId,
+ modelSelection,
+ providerThreadId: input.providerThreadId ?? null,
+ userMessageId: MessageId.make(`message:${input.runId}`),
+ rootNodeId: NodeId.make(`node:${input.runId}`),
+ activeAttemptId: null,
+ status: input.status,
+ requestedAt: input.now,
+ startedAt: input.status === "queued" || input.status === "preparing" ? null : input.now,
+ completedAt: null,
+ checkpointId: null,
+ contextHandoffId: null,
+ ...(input.status === "queued" ? { queuePosition: 1 } : {}),
+ };
+}
+
+function makeProviderThread(input: {
+ readonly id: ProviderThreadId;
+ readonly threadId: ThreadId;
+ readonly now: DateTime.Utc;
+}): OrchestrationV2ProviderThread {
+ return {
+ id: input.id,
+ driver,
+ providerInstanceId: modelSelection.instanceId,
+ providerSessionId: null,
+ appThreadId: input.threadId,
+ ownerNodeId: null,
+ nativeThreadRef: {
+ driver,
+ nativeId: String(input.id),
+ strength: "strong",
+ },
+ nativeConversationHeadRef: null,
+ status: "idle",
+ firstRunOrdinal: 1,
+ lastRunOrdinal: 1,
+ handoffIds: [],
+ forkedFrom: null,
+ createdAt: input.now,
+ updatedAt: input.now,
+ };
+}
+
+function makeProviderChildThread(input: {
+ readonly parent: OrchestrationV2AppThread;
+ readonly childThreadId: ThreadId;
+ readonly providerThreadId: ProviderThreadId;
+ readonly now: DateTime.Utc;
+}): OrchestrationV2AppThread {
+ return {
+ ...input.parent,
+ id: input.childThreadId,
+ title: `Provider child ${input.childThreadId}`,
+ createdBy: "agent",
+ creationSource: "provider",
+ activeProviderThreadId: input.providerThreadId,
+ lineage: {
+ parentThreadId: input.parent.id,
+ relationshipToParent: "subagent",
+ rootThreadId: input.parent.lineage.rootThreadId,
+ },
+ createdAt: input.now,
+ updatedAt: input.now,
+ };
+}
+
+function makeProviderChildTurn(input: {
+ readonly providerThreadId: ProviderThreadId;
+ readonly providerTurnId: ProviderTurnId;
+ readonly nodeId: NodeId;
+ readonly now: DateTime.Utc;
+ readonly status: OrchestrationV2ProviderTurn["status"];
+}): OrchestrationV2ProviderTurn {
+ return {
+ id: input.providerTurnId,
+ providerThreadId: input.providerThreadId,
+ nodeId: input.nodeId,
+ runAttemptId: null,
+ nativeTurnRef: {
+ driver,
+ nativeId: String(input.providerTurnId),
+ strength: "weak",
+ },
+ ordinal: 1,
+ status: input.status,
+ startedAt: input.now,
+ completedAt: input.status === "running" ? null : input.now,
+ };
+}
+
+function makeProviderNativeSubagent(input: {
+ readonly parentThreadId: ThreadId;
+ readonly parentNodeId: NodeId;
+ readonly runId?: RunId | null;
+ readonly providerThreadId: ProviderThreadId;
+ readonly childThreadId: ThreadId;
+ readonly now: DateTime.Utc;
+ readonly status: OrchestrationV2Subagent["status"];
+}): OrchestrationV2Subagent {
+ return {
+ id: input.parentNodeId,
+ threadId: input.parentThreadId,
+ runId: input.runId ?? null,
+ parentNodeId: input.parentNodeId,
+ origin: "provider_native",
+ createdBy: "agent",
+ driver,
+ providerInstanceId: modelSelection.instanceId,
+ providerThreadId: input.providerThreadId,
+ childThreadId: input.childThreadId,
+ nativeTaskRef: {
+ driver,
+ nativeId: String(input.parentNodeId),
+ strength: "strong",
+ },
+ prompt: "Run the background task.",
+ title: null,
+ model: modelSelection.model,
+ status: input.status,
+ result: null,
+ startedAt: input.now,
+ completedAt: input.status === "running" ? null : input.now,
+ updatedAt: input.now,
+ };
+}
+
+it.layer(TestLayer)("OrchestratorV2 provider-native Stop invariants", (it) => {
+ it.effect("interrupts only running direct provider-native children and de-duplicates races", () =>
+ Effect.gen(function* () {
+ const orchestrator = yield* OrchestratorV2;
+ const eventSink = yield* EventSinkV2;
+ const sql = yield* SqlClient.SqlClient;
+ const now = DateTime.makeUnsafe("2026-07-31T12:00:00.000Z");
+ const parentThreadId = ThreadId.make("thread:provider-native-stop-parent");
+ const projectId = ProjectId.make("project:provider-native-stop");
+
+ yield* orchestrator.dispatch(
+ createThreadCommand({
+ commandId: "cmd:provider-native-stop:create",
+ threadId: parentThreadId,
+ projectId,
+ }),
+ );
+ const parent = (yield* orchestrator.getThreadProjection(parentThreadId)).thread;
+
+ const makeChild = (
+ label: string,
+ status: OrchestrationV2ProviderTurn["status"],
+ subagentStatus: OrchestrationV2Subagent["status"],
+ childParent: OrchestrationV2AppThread = parent,
+ runId: RunId | null = null,
+ ) => {
+ const childThreadId = ThreadId.make(`thread:provider-native-stop:${label}`);
+ const providerThreadId = ProviderThreadId.make(
+ `provider-thread:provider-native-stop:${label}`,
+ );
+ const providerTurnId = ProviderTurnId.make(`provider-turn:provider-native-stop:${label}`);
+ const nodeId = NodeId.make(`node:provider-native-stop:${label}`);
+ const childThread = makeProviderChildThread({
+ parent: childParent,
+ childThreadId,
+ providerThreadId,
+ now,
+ });
+ const providerThread = {
+ ...makeProviderThread({
+ id: providerThreadId,
+ threadId: childThreadId,
+ now,
+ }),
+ providerSessionId: ProviderSessionId.make(
+ `provider-session:provider-native-stop:${label}`,
+ ),
+ ownerNodeId:
+ childParent.id === parent.id
+ ? nodeId
+ : NodeId.make(`node:provider-native-stop:${childParent.id}:owner`),
+ status: status === "running" ? ("active" as const) : ("idle" as const),
+ appThreadId: childThreadId,
+ };
+ const providerTurn = makeProviderChildTurn({
+ providerThreadId,
+ providerTurnId,
+ nodeId,
+ now,
+ status,
+ });
+ const subagent = makeProviderNativeSubagent({
+ parentThreadId: childParent.id,
+ parentNodeId: nodeId,
+ providerThreadId,
+ childThreadId,
+ now,
+ status: subagentStatus,
+ runId,
+ });
+ return { childThread, providerThread, providerTurn, subagent };
+ };
+
+ const rolledBackSpawnRunId = RunId.make("run:provider-native-stop:rolled-back-spawn");
+ const first = makeChild("first", "running", "running", parent, rolledBackSpawnRunId);
+ const second = makeChild("second", "running", "running");
+ const terminal = makeChild("terminal", "completed", "completed");
+ const nested = makeChild("nested", "running", "running", first.childThread);
+ const missingProviderRow = makeChild("stale-missing-provider-row", "running", "running");
+ const nullProviderSession = makeChild("stale-null-provider-session", "running", "running");
+ const nullProviderSessionThread = {
+ ...nullProviderSession.providerThread,
+ providerSessionId: null,
+ };
+ const staleNullProviderThread: OrchestrationV2Subagent = {
+ ...first.subagent,
+ id: NodeId.make("node:provider-native-stop:stale-null-provider-thread"),
+ parentNodeId: NodeId.make("node:provider-native-stop:stale-null-provider-thread"),
+ providerThreadId: null,
+ childThreadId: terminal.childThread.id,
+ };
+ const staleMissingChild: OrchestrationV2Subagent = {
+ ...first.subagent,
+ id: NodeId.make("node:provider-native-stop:stale-missing-child"),
+ parentNodeId: NodeId.make("node:provider-native-stop:stale-missing-child"),
+ childThreadId: ThreadId.make("thread:provider-native-stop:stale-missing-child"),
+ };
+ const staleNoRunningTurn: OrchestrationV2Subagent = {
+ ...terminal.subagent,
+ id: NodeId.make("node:provider-native-stop:stale-no-running-turn"),
+ parentNodeId: NodeId.make("node:provider-native-stop:stale-no-running-turn"),
+ status: "running",
+ completedAt: null,
+ updatedAt: now,
+ };
+ const nestedOnTerminal: OrchestrationV2Subagent = {
+ ...nested.subagent,
+ id: NodeId.make("node:provider-native-stop:terminal-nested"),
+ threadId: terminal.childThread.id,
+ parentNodeId: terminal.providerTurn.nodeId,
+ };
+ const seededEvents: Array = [];
+ seededEvents.push({
+ id: EventId.make("event:provider-native-stop:rolled-back-spawn"),
+ type: "run.created",
+ threadId: parentThreadId,
+ runId: rolledBackSpawnRunId,
+ nodeId: NodeId.make("node:provider-native-stop:rolled-back-spawn"),
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: makeRun({
+ runId: rolledBackSpawnRunId,
+ threadId: parentThreadId,
+ status: "rolled_back",
+ now,
+ }),
+ });
+ for (const child of [
+ first,
+ second,
+ terminal,
+ nested,
+ nullProviderSession,
+ missingProviderRow,
+ ]) {
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${child.childThread.id}:created`),
+ type: "thread.created",
+ threadId: child.childThread.id,
+ occurredAt: now,
+ payload: child.childThread,
+ });
+ }
+ for (const child of [first, second, terminal, nested]) {
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${child.providerThread.id}:updated`),
+ type: "provider-thread.updated",
+ threadId: child.childThread.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: child.providerThread,
+ });
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${child.providerTurn.id}:updated`),
+ type: "provider-turn.updated",
+ threadId: child.childThread.id,
+ nodeId: child.providerTurn.nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: child.providerTurn,
+ });
+ }
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${nullProviderSessionThread.id}:updated`),
+ type: "provider-thread.updated",
+ threadId: nullProviderSession.childThread.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: nullProviderSessionThread,
+ });
+ seededEvents.push({
+ id: EventId.make(
+ `event:provider-native-stop:${nullProviderSession.providerTurn.id}:updated`,
+ ),
+ type: "provider-turn.updated",
+ threadId: nullProviderSession.childThread.id,
+ nodeId: nullProviderSession.providerTurn.nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: nullProviderSession.providerTurn,
+ });
+ seededEvents.push({
+ id: EventId.make(
+ `event:provider-native-stop:${missingProviderRow.providerTurn.id}:updated`,
+ ),
+ type: "provider-turn.updated",
+ threadId: missingProviderRow.childThread.id,
+ nodeId: missingProviderRow.providerTurn.nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: missingProviderRow.providerTurn,
+ });
+ for (const subagent of [
+ first.subagent,
+ second.subagent,
+ terminal.subagent,
+ staleNullProviderThread,
+ staleMissingChild,
+ staleNoRunningTurn,
+ missingProviderRow.subagent,
+ nullProviderSession.subagent,
+ ]) {
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${subagent.id}:subagent`),
+ type: "subagent.updated",
+ threadId: parentThreadId,
+ nodeId: subagent.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: subagent,
+ });
+ }
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${nested.subagent.id}:subagent`),
+ type: "subagent.updated",
+ threadId: first.childThread.id,
+ nodeId: nested.subagent.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: nested.subagent,
+ });
+ seededEvents.push({
+ id: EventId.make(`event:provider-native-stop:${nestedOnTerminal.id}:subagent`),
+ type: "subagent.updated",
+ threadId: terminal.childThread.id,
+ nodeId: nestedOnTerminal.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: nestedOnTerminal,
+ });
+ yield* eventSink.write({ events: seededEvents });
+
+ const terminalChildProjection = yield* orchestrator.getThreadProjection(
+ terminal.childThread.id,
+ );
+ const terminalChildFullShell = threadShellFromProjection(terminalChildProjection);
+ const terminalChildSqlShell = yield* orchestrator.getThreadShell(terminal.childThread.id);
+ assert.isTrue(terminalChildFullShell.hasInterruptibleProviderNativeBackgroundWork);
+ assert.equal(
+ terminalChildSqlShell?.hasInterruptibleProviderNativeBackgroundWork,
+ terminalChildFullShell.hasInterruptibleProviderNativeBackgroundWork,
+ "SQL and full projections must expose directly-owned nested provider-native work",
+ );
+ const nestedOnlyDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:terminal-nested"),
+ threadId: terminal.childThread.id,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ nestedOnlyDispatch.storedEvents
+ .filter((stored) => stored.event.type === "provider-turn.interrupt-requested")
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ ),
+ [nested.providerTurn.id],
+ );
+ const firstChildProjection = yield* orchestrator.getThreadProjection(first.childThread.id);
+ const firstChildFullShell = threadShellFromProjection(firstChildProjection);
+ const firstChildSqlShell = yield* orchestrator.getThreadShell(first.childThread.id);
+ assert.isTrue(firstChildFullShell.hasInterruptibleProviderNativeBackgroundWork);
+ assert.equal(
+ firstChildSqlShell?.hasInterruptibleProviderNativeBackgroundWork,
+ firstChildFullShell.hasInterruptibleProviderNativeBackgroundWork,
+ "a provider-native child owns its own running provider turn",
+ );
+
+ const parentShell = yield* orchestrator.getThreadShell(parentThreadId);
+ assert.isTrue(parentShell?.hasInterruptibleProviderNativeBackgroundWork);
+
+ const firstCommandId = CommandId.make("cmd:provider-native-stop:first");
+ const firstDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: firstCommandId,
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ firstDispatch.storedEvents
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ )
+ .filter((providerTurnId): providerTurnId is ProviderTurnId => providerTurnId !== null),
+ [first.providerTurn.id, second.providerTurn.id],
+ );
+ for (const stored of firstDispatch.storedEvents) {
+ if (stored.event.type !== "provider-turn.interrupt-requested") continue;
+ assert.equal(stored.event.threadId, parentThreadId);
+ assert.equal(stored.event.nodeId, undefined);
+ }
+ const firstEffects = yield* sql<{ readonly effect_id: string }>`
+ SELECT effect_id
+ FROM orchestration_v2_effect_outbox
+ WHERE command_id = ${firstCommandId}
+ ORDER BY effect_id ASC
+ `;
+ assert.deepEqual(
+ firstEffects.map((effect) => effect.effect_id),
+ [
+ `effect:${firstCommandId}:provider-turn.interrupt:${first.providerTurn.id}`,
+ `effect:${firstCommandId}:provider-turn.interrupt:${second.providerTurn.id}`,
+ ],
+ );
+
+ const replayDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: firstCommandId,
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(replayDispatch.storedEvents, firstDispatch.storedEvents);
+ const replayEffects = yield* sql<{ readonly effect_id: string }>`
+ SELECT effect_id
+ FROM orchestration_v2_effect_outbox
+ WHERE command_id = ${firstCommandId}
+ ORDER BY effect_id ASC
+ `;
+ assert.deepEqual(replayEffects, firstEffects);
+
+ const retryCommandId = CommandId.make("cmd:provider-native-stop:retry");
+ const retryDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: retryCommandId,
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.equal(
+ retryDispatch.storedEvents.filter(
+ (stored) => stored.event.type === "provider-turn.interrupt-requested",
+ ).length,
+ 2,
+ );
+ const retryEffects = yield* sql<{ readonly effect_id: string }>`
+ SELECT effect_id
+ FROM orchestration_v2_effect_outbox
+ WHERE command_id = ${retryCommandId}
+ ORDER BY effect_id ASC
+ `;
+ assert.deepEqual(
+ retryEffects.map((effect) => effect.effect_id),
+ [
+ `effect:${retryCommandId}:provider-turn.interrupt:${first.providerTurn.id}`,
+ `effect:${retryCommandId}:provider-turn.interrupt:${second.providerTurn.id}`,
+ ],
+ );
+
+ const missingProviderInstanceId = ProviderInstanceId.make("provider-native-stop-missing");
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop:second:unresolvable"),
+ type: "provider-thread.updated",
+ threadId: second.childThread.id,
+ providerInstanceId: missingProviderInstanceId,
+ occurredAt: now,
+ payload: { ...second.providerThread, providerInstanceId: missingProviderInstanceId },
+ },
+ ],
+ });
+ const atomicFailureCommandId = CommandId.make("cmd:provider-native-stop:atomic-failure");
+ const atomicFailure = yield* orchestrator
+ .dispatch({
+ type: "run.interrupt",
+ commandId: atomicFailureCommandId,
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ })
+ .pipe(Effect.flip);
+ assert.equal(atomicFailure._tag, "OrchestratorProviderAdapterError");
+ const atomicEffects = yield* sql<{ readonly effect_id: string }>`
+ SELECT effect_id
+ FROM orchestration_v2_effect_outbox
+ WHERE command_id = ${atomicFailureCommandId}
+ `;
+ assert.deepEqual(atomicEffects, []);
+
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop:second:restored"),
+ type: "provider-thread.updated",
+ threadId: second.childThread.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: second.providerThread,
+ },
+ ],
+ });
+ const afterFailedCommandId = CommandId.make("cmd:provider-native-stop:after-failed");
+ const afterFailedDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: afterFailedCommandId,
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ afterFailedDispatch.storedEvents
+ .filter((stored) => stored.event.type === "provider-turn.interrupt-requested")
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ ),
+ [first.providerTurn.id, second.providerTurn.id],
+ );
+ const afterFailedEffects = yield* sql<{ readonly effect_id: string }>`
+ SELECT effect_id
+ FROM orchestration_v2_effect_outbox
+ WHERE command_id = ${afterFailedCommandId}
+ ORDER BY effect_id ASC
+ `;
+ assert.deepEqual(
+ afterFailedEffects.map((effect) => effect.effect_id),
+ [
+ `effect:${afterFailedCommandId}:provider-turn.interrupt:${first.providerTurn.id}`,
+ `effect:${afterFailedCommandId}:provider-turn.interrupt:${second.providerTurn.id}`,
+ ],
+ );
+
+ const childDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:child"),
+ threadId: first.childThread.id,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ childDispatch.storedEvents
+ .filter((stored) => stored.event.type === "provider-turn.interrupt-requested")
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ ),
+ [first.providerTurn.id],
+ );
+
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop:first:terminal"),
+ type: "provider-turn.updated",
+ threadId: first.childThread.id,
+ nodeId: first.providerTurn.nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: { ...first.providerTurn, status: "interrupted", completedAt: now },
+ },
+ {
+ id: EventId.make("event:provider-native-stop:first:subagent-terminal"),
+ type: "subagent.updated",
+ threadId: parentThreadId,
+ nodeId: first.subagent.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: { ...first.subagent, status: "interrupted", completedAt: now, updatedAt: now },
+ },
+ ],
+ });
+
+ const nestedAfterOwnTurnDispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:child-nested-after-own"),
+ threadId: first.childThread.id,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ nestedAfterOwnTurnDispatch.storedEvents
+ .filter((stored) => stored.event.type === "provider-turn.interrupt-requested")
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ ),
+ [nested.providerTurn.id],
+ );
+
+ const afterCompletion = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:after-completion"),
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ afterCompletion.storedEvents
+ .filter((stored) => stored.event.type === "provider-turn.interrupt-requested")
+ .map((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? stored.event.payload.providerTurnId
+ : null,
+ ),
+ [second.providerTurn.id],
+ );
+
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop:second:terminal"),
+ type: "provider-turn.updated",
+ threadId: second.childThread.id,
+ nodeId: second.providerTurn.nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: { ...second.providerTurn, status: "completed", completedAt: now },
+ },
+ {
+ id: EventId.make("event:provider-native-stop:second:subagent-terminal"),
+ type: "subagent.updated",
+ threadId: parentThreadId,
+ nodeId: second.subagent.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: { ...second.subagent, status: "completed", completedAt: now, updatedAt: now },
+ },
+ ],
+ });
+ const noOp = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:no-op"),
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.equal(noOp.storedEvents[0]?.event.type, "run.interrupt-noop");
+ if (noOp.storedEvents[0]?.event.type === "run.interrupt-noop") {
+ assert.equal(
+ noOp.storedEvents[0].event.payload.reason,
+ "All provider-native background targets completed before interruption dispatch.",
+ );
+ }
+ const repeatedNoOp = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop:no-op-retry"),
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.equal(repeatedNoOp.storedEvents[0]?.event.type, "run.interrupt-noop");
+
+ const staleSubagents = [
+ staleNullProviderThread,
+ staleMissingChild,
+ staleNoRunningTurn,
+ missingProviderRow.subagent,
+ nullProviderSession.subagent,
+ ];
+ yield* eventSink.write({
+ events: staleSubagents.map((subagent, index) => ({
+ id: EventId.make(`event:provider-native-stop:stale:${index}:terminal`),
+ type: "subagent.updated" as const,
+ threadId: parentThreadId,
+ nodeId: subagent.id,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: {
+ ...subagent,
+ status: "interrupted" as const,
+ completedAt: now,
+ updatedAt: now,
+ },
+ })),
+ });
+ const settledParentShell = yield* orchestrator.getThreadShell(parentThreadId);
+ assert.isFalse(settledParentShell?.hasInterruptibleProviderNativeBackgroundWork);
+ }),
+ );
+
+ it.effect(
+ "keeps provider-native-only Stop off a root run that appears after the no-run read",
+ () =>
+ Effect.gen(function* () {
+ const orchestrator = yield* OrchestratorV2;
+ const eventSink = yield* EventSinkV2;
+ const parentThreadId = ThreadId.make("thread:provider-native-stop-race-parent");
+ const projectId = ProjectId.make("project:provider-native-stop-race");
+ const now = DateTime.makeUnsafe("2026-08-01T12:00:00.000Z");
+
+ yield* orchestrator.dispatch(
+ createThreadCommand({
+ commandId: "cmd:provider-native-stop-race:create",
+ threadId: parentThreadId,
+ projectId,
+ }),
+ );
+ const parent = (yield* orchestrator.getThreadProjection(parentThreadId)).thread;
+ const childThreadId = ThreadId.make("thread:provider-native-stop-race-child");
+ const providerThreadId = ProviderThreadId.make("provider-thread:provider-native-stop-race");
+ const providerTurnId = ProviderTurnId.make("provider-turn:provider-native-stop-race");
+ const nodeId = NodeId.make("node:provider-native-stop-race");
+ const childThread = makeProviderChildThread({
+ parent,
+ childThreadId,
+ providerThreadId,
+ now,
+ });
+ const providerThread = {
+ ...makeProviderThread({
+ id: providerThreadId,
+ threadId: childThreadId,
+ now,
+ }),
+ providerSessionId: ProviderSessionId.make("provider-session:provider-native-stop-race"),
+ ownerNodeId: nodeId,
+ status: "active" as const,
+ appThreadId: childThreadId,
+ };
+ const providerTurn = makeProviderChildTurn({
+ providerThreadId,
+ providerTurnId,
+ nodeId,
+ now,
+ status: "running",
+ });
+ const subagent = makeProviderNativeSubagent({
+ parentThreadId,
+ parentNodeId: nodeId,
+ providerThreadId,
+ childThreadId,
+ now,
+ status: "running",
+ });
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop-race:thread"),
+ type: "thread.created",
+ threadId: childThreadId,
+ occurredAt: now,
+ payload: childThread,
+ },
+ {
+ id: EventId.make("event:provider-native-stop-race:provider-thread"),
+ type: "provider-thread.updated",
+ threadId: childThreadId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: providerThread,
+ },
+ {
+ id: EventId.make("event:provider-native-stop-race:provider-turn"),
+ type: "provider-turn.updated",
+ threadId: childThreadId,
+ nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: providerTurn,
+ },
+ {
+ id: EventId.make("event:provider-native-stop-race:subagent"),
+ type: "subagent.updated",
+ threadId: parentThreadId,
+ nodeId,
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: subagent,
+ },
+ ],
+ });
+
+ const shellBeforeRootRace = yield* orchestrator.getThreadShell(parentThreadId);
+ assert.isTrue(shellBeforeRootRace?.hasInterruptibleProviderNativeBackgroundWork);
+
+ const rootRunId = RunId.make("run:provider-native-stop-race-root");
+ const rootRun = makeRun({
+ runId: rootRunId,
+ threadId: parentThreadId,
+ status: "running",
+ now,
+ });
+ yield* eventSink.write({
+ events: [
+ {
+ id: EventId.make("event:provider-native-stop-race:root-run"),
+ type: "run.created",
+ threadId: parentThreadId,
+ runId: rootRunId,
+ ...(rootRun.rootNodeId === null ? {} : { nodeId: rootRun.rootNodeId }),
+ providerInstanceId: modelSelection.instanceId,
+ occurredAt: now,
+ payload: rootRun,
+ },
+ ],
+ });
+
+ const dispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: CommandId.make("cmd:provider-native-stop-race:dispatch"),
+ threadId: parentThreadId,
+ intent: "provider_native_only",
+ });
+ assert.deepEqual(
+ dispatch.storedEvents.map((stored) => stored.event.type),
+ ["provider-turn.interrupt-requested"],
+ );
+ assert.isFalse(
+ dispatch.storedEvents.some(
+ (stored) =>
+ stored.event.type === "turn-item.updated" &&
+ stored.event.payload.type === "run_interrupt_request",
+ ),
+ );
+ }),
+ );
+});
diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts
index 17ec6290fb6..762861d18ad 100644
--- a/apps/server/src/orchestration-v2/Orchestrator.ts
+++ b/apps/server/src/orchestration-v2/Orchestrator.ts
@@ -1799,16 +1799,32 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
: (providerSwitchPlan?.releaseProviderSessionIds ?? []),
);
if (detachSessionIds.size > 0) {
- const liveSessions = projection.providerSessions.filter(
+ const sessionsToDetach = projection.providerSessions.filter(
(session) =>
detachSessionIds.has(session.id) &&
- session.status !== "stopped" &&
- session.status !== "error",
+ (command.type === "thread.delete" ||
+ (session.status !== "stopped" && session.status !== "error")),
);
yield* Effect.forEach(
- liveSessions,
+ sessionsToDetach,
(session) =>
Effect.gen(function* () {
+ // Pending and materialized projection rows can share one native id.
+ const providerThreads = Array.from(
+ new Map(
+ projection.providerThreads.flatMap((thread) => {
+ const nativeThreadRef = thread.nativeThreadRef;
+ if (thread.providerSessionId !== session.id || nativeThreadRef === null)
+ return [];
+ return [
+ [
+ JSON.stringify([nativeThreadRef.driver, nativeThreadRef.nativeId]),
+ thread,
+ ] as const,
+ ];
+ }),
+ ).values(),
+ );
yield* emit(
events,
command,
@@ -1856,6 +1872,14 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
...(command.type === "thread.archive" || command.type === "thread.delete"
? { revokeMcpCredential: true }
: {}),
+ ...(command.type === "thread.delete"
+ ? {
+ deleteProviderThread: true,
+ providerInstanceId: session.providerInstanceId,
+ providerSession: session,
+ providerThreads,
+ }
+ : {}),
},
} satisfies PendingOrchestrationEffectV2;
yield* Ref.update(effects, (existing) => [...existing, pendingEffect]);
@@ -5628,6 +5652,101 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
});
});
+ const providerNativeInterruptTargets = (projection: OrchestrationV2ThreadProjection) =>
+ Effect.gen(function* () {
+ // A provider-native child's own live turn gets the first Stop. Once it
+ // settles, the same path targets its direct descendants. This preserves
+ // the active-turn boundary without leaving nested work unreachable.
+ const targetThreads: Array<{
+ readonly childThreadId: ThreadId;
+ readonly providerThreadId: OrchestrationV2ProviderThread["id"];
+ }> = [];
+ const isProviderNativeChild =
+ projection.thread.creationSource === "provider" &&
+ projection.thread.lineage.relationshipToParent === "subagent";
+
+ const ownProviderThreadId = isProviderNativeChild
+ ? projection.thread.activeProviderThreadId
+ : null;
+ const ownTurnIsRunning =
+ ownProviderThreadId !== null &&
+ projection.providerTurns.some(
+ (candidate) =>
+ candidate.providerThreadId === ownProviderThreadId && candidate.status === "running",
+ );
+
+ if (ownProviderThreadId !== null && ownTurnIsRunning) {
+ targetThreads.push({
+ childThreadId: projection.thread.id,
+ providerThreadId: ownProviderThreadId,
+ });
+ } else {
+ for (const subagent of projection.subagents) {
+ if (
+ subagent.threadId !== projection.thread.id ||
+ subagent.origin !== "provider_native" ||
+ subagent.status !== "running" ||
+ subagent.childThreadId === null
+ ) {
+ continue;
+ }
+ const providerThreadId = subagent.providerThreadId;
+ if (providerThreadId === null) continue;
+ targetThreads.push({
+ childThreadId: subagent.childThreadId,
+ providerThreadId,
+ });
+ }
+ }
+
+ const targets = new Map<
+ string,
+ {
+ readonly threadId: ThreadId;
+ readonly providerThread: OrchestrationV2ProviderThread;
+ readonly providerTurn: OrchestrationV2ProviderTurn;
+ }
+ >();
+ for (const targetThread of targetThreads) {
+ const childProjection = yield* projectionStore
+ .getThreadProjection(targetThread.childThreadId)
+ .pipe(
+ Effect.catchTags({
+ ProjectionStoreThreadNotFoundError: () => Effect.succeed(null),
+ }),
+ Effect.mapError(
+ (cause) =>
+ new OrchestratorProjectionError({
+ threadId: targetThread.childThreadId,
+ cause,
+ }),
+ ),
+ );
+ if (childProjection === null) continue;
+
+ const providerTurn = childProjection.providerTurns
+ .filter(
+ (candidate) =>
+ candidate.providerThreadId === targetThread.providerThreadId &&
+ candidate.status === "running",
+ )
+ .toSorted((left, right) => right.ordinal - left.ordinal)[0];
+ if (providerTurn === undefined) continue;
+ const providerThread = childProjection.providerThreads.find(
+ (candidate) =>
+ candidate.id === targetThread.providerThreadId &&
+ candidate.appThreadId === childProjection.thread.id,
+ );
+ if (providerThread === undefined || providerThread.providerSessionId === null) continue;
+ targets.set(String(providerTurn.id), {
+ threadId: childProjection.thread.id,
+ providerThread,
+ providerTurn,
+ });
+ }
+ return Array.from(targets.values());
+ });
+
const dispatchRunInterrupt = (
command: Extract,
events: Ref.Ref>,
@@ -5635,7 +5754,107 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
) =>
Effect.gen(function* () {
const projection = yield* loadProjectionForCommand(command);
- const run = projection.runs.find((candidate) => candidate.id === command.runId);
+ const providerNativeOnly = command.intent === "provider_native_only";
+ let run: OrchestrationV2Run | undefined;
+ if (!providerNativeOnly) {
+ run =
+ command.runId === undefined
+ ? projection.runs
+ .filter(
+ (candidate) =>
+ candidate.status === "preparing" ||
+ candidate.status === "starting" ||
+ candidate.status === "running",
+ )
+ .toSorted((left, right) => right.ordinal - left.ordinal)[0]
+ : projection.runs.find((candidate) => candidate.id === command.runId);
+ }
+
+ if (run === undefined && (command.runId === undefined || providerNativeOnly)) {
+ const targets = yield* providerNativeInterruptTargets(projection);
+ const now = yield* DateTime.now;
+ const emitEvent = emit(events, command);
+ if (targets.length === 0) {
+ // EventSink.commitCommand requires at least one domain event, so a
+ // successful zero-target receipt must remain durable and replayable.
+ yield* emitEvent({
+ type: "run.interrupt-noop",
+ threadId: command.threadId,
+ occurredAt: now,
+ payload: {
+ reason:
+ "All provider-native background targets completed before interruption dispatch.",
+ },
+ });
+ return undefined;
+ }
+
+ const authorizedTargets = yield* Effect.forEach(targets, (target) =>
+ Effect.gen(function* () {
+ const providerSessionId = target.providerThread.providerSessionId;
+ if (providerSessionId === null) {
+ return yield* new OrchestratorDispatchError({
+ commandId: command.commandId,
+ commandType: command.type,
+ cause: `Provider turn ${target.providerTurn.id} has no provider session target.`,
+ });
+ }
+ const capabilities = yield* providerAdapters
+ .get(target.providerThread.providerInstanceId)
+ .pipe(
+ Effect.flatMap((adapter) => adapter.getCapabilities()),
+ Effect.mapError(
+ (cause) =>
+ new OrchestratorProviderAdapterError({
+ commandId: command.commandId,
+ providerInstanceId: target.providerThread.providerInstanceId,
+ cause,
+ }),
+ ),
+ );
+ yield* enforceCommandPolicy(command)(
+ commandPolicy.ensureInterrupt({
+ commandId: command.commandId,
+ threadId: command.threadId,
+ providerInstanceId: target.providerThread.providerInstanceId,
+ capabilities,
+ }),
+ );
+ return { target, providerSessionId };
+ }),
+ );
+
+ for (const { target, providerSessionId } of authorizedTargets) {
+ yield* emitEvent({
+ type: "provider-turn.interrupt-requested",
+ threadId: command.threadId,
+ driver: target.providerThread.driver,
+ providerInstanceId: target.providerThread.providerInstanceId,
+ occurredAt: now,
+ payload: {
+ targetThreadId: target.threadId,
+ providerThreadId: target.providerThread.id,
+ providerTurnId: target.providerTurn.id,
+ reason: command.reason ?? null,
+ },
+ });
+ yield* Ref.update(effects, (existing) => [
+ ...existing,
+ {
+ id: `effect:${command.commandId}:provider-turn.interrupt:${target.providerTurn.id}`,
+ commandId: command.commandId,
+ threadId: target.threadId,
+ request: {
+ type: "provider-turn.interrupt",
+ providerSessionId,
+ providerThreadId: target.providerThread.id,
+ providerTurnId: target.providerTurn.id,
+ },
+ } satisfies PendingOrchestrationEffectV2,
+ ]);
+ }
+ return undefined;
+ }
const rootNode =
run?.rootNodeId === null
? undefined
@@ -5652,7 +5871,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
return yield* new OrchestratorDispatchError({
commandId: command.commandId,
commandType: command.type,
- cause: `Run ${command.runId} is not interruptible.`,
+ cause: `Run ${command.runId ?? "without a run id"} is not interruptible.`,
});
}
const now = yield* DateTime.now;
@@ -5706,7 +5925,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
return yield* new OrchestratorDispatchError({
commandId: command.commandId,
commandType: command.type,
- cause: `Run ${command.runId} has no active attempt to interrupt.`,
+ cause: `Run ${run.id} has no active attempt to interrupt.`,
});
}
const interruptResultItem: OrchestrationV2TurnItem = {
@@ -5819,7 +6038,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
return yield* new OrchestratorDispatchError({
commandId: command.commandId,
commandType: command.type,
- cause: `Run ${command.runId} is not interruptible.`,
+ cause: `Run ${run.id} is not interruptible.`,
});
}
if (providerThread.providerSessionId === null) {
diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts
index 84fb7e7a891..8aad62de25a 100644
--- a/apps/server/src/orchestration-v2/ProjectionStore.ts
+++ b/apps/server/src/orchestration-v2/ProjectionStore.ts
@@ -321,6 +321,8 @@ export function applyToProjection(
checkpoints: upsertById(base.checkpoints, event.payload),
};
case "checkpoint.rollback-requested":
+ case "provider-turn.interrupt-requested":
+ case "run.interrupt-noop":
return base;
case "context-handoff.updated":
return {
@@ -448,6 +450,7 @@ type ShellThreadRow = {
readonly latest_message_payload_json: string | null;
readonly latest_user_message_at: string | null;
readonly has_actionable_proposed_plan: number;
+ readonly has_interruptible_provider_native_background_work: number;
readonly item_count: number;
readonly runless_item_count: number;
};
@@ -856,6 +859,35 @@ export function threadShellFromProjection(
activeProviderThreadId: projection.thread.activeProviderThreadId,
runs: projection.runs,
});
+ // This is a projection-local Stop hint. Dispatch revalidates each linked
+ // child turn before emitting an interrupt, so a rolled-back parent can keep
+ // the hint while a genuinely running native child drains. Normal terminal
+ // subagent events remove the hint once that child is no longer running.
+ const hasInterruptibleProviderNativeBackgroundWork =
+ projection.thread.creationSource === "provider" &&
+ projection.thread.lineage.relationshipToParent === "subagent"
+ ? (projection.thread.activeProviderThreadId !== null &&
+ projection.providerTurns.some(
+ (turn) =>
+ turn.providerThreadId === projection.thread.activeProviderThreadId &&
+ turn.status === "running",
+ )) ||
+ projection.subagents.some(
+ (subagent) =>
+ subagent.threadId === projection.thread.id &&
+ subagent.origin === "provider_native" &&
+ subagent.status === "running" &&
+ subagent.childThreadId !== null &&
+ subagent.providerThreadId !== null,
+ )
+ : projection.subagents.some(
+ (subagent) =>
+ subagent.threadId === projection.thread.id &&
+ subagent.origin === "provider_native" &&
+ subagent.status === "running" &&
+ subagent.childThreadId !== null &&
+ subagent.providerThreadId !== null,
+ );
return {
createdBy: projection.thread.createdBy,
creationSource: projection.thread.creationSource,
@@ -904,6 +936,7 @@ export function threadShellFromProjection(
(plan) => plan.kind === "proposed_plan" && plan.status === "active",
),
pendingBackgroundTasks: [...pendingBackgroundTasks],
+ hasInterruptibleProviderNativeBackgroundWork,
itemCount: activeLocalTurnItems(projection).length,
visibleItemCount: projection.visibleTurnItems.length,
createdAt: projection.thread.createdAt,
@@ -949,6 +982,7 @@ type ShellThreadState = {
readonly latestUserMessageAt: DateTime.Utc | null;
readonly hasActionableProposedPlan: boolean;
readonly pendingBackgroundTasks: OrchestrationV2ThreadShell["pendingBackgroundTasks"];
+ readonly hasInterruptibleProviderNativeBackgroundWork: boolean;
readonly itemCount: number;
readonly runlessItemCount: number;
readonly updatedAt: OrchestrationV2ThreadProjection["updatedAt"];
@@ -1088,6 +1122,8 @@ function shellFromState(input: {
latestUserMessageAt: input.state.latestUserMessageAt,
hasActionableProposedPlan: input.state.hasActionableProposedPlan,
pendingBackgroundTasks: input.state.pendingBackgroundTasks,
+ hasInterruptibleProviderNativeBackgroundWork:
+ input.state.hasInterruptibleProviderNativeBackgroundWork,
itemCount: input.state.itemCount,
visibleItemCount: input.visibleItemCount,
createdAt: input.state.thread.createdAt,
@@ -1824,6 +1860,8 @@ export const layer: Layer.Layer =
break;
}
case "checkpoint.rollback-requested":
+ case "provider-turn.interrupt-requested":
+ case "run.interrupt-noop":
break;
case "context-handoff.updated": {
const payloadJson = yield* encodeContextHandoffPayload(event.payload);
@@ -2319,6 +2357,34 @@ export const layer: Layer.Layer =
AND plan.kind = 'proposed_plan'
AND plan.status = 'active'
) AS has_actionable_proposed_plan,
+ CASE
+ WHEN json_extract(t.payload_json, '$.creationSource') = 'provider'
+ AND json_extract(t.payload_json, '$.lineage.relationshipToParent') = 'subagent'
+ THEN EXISTS (
+ SELECT 1
+ FROM orchestration_v2_projection_provider_turns provider_turn
+ WHERE provider_turn.thread_id = t.thread_id
+ AND provider_turn.provider_thread_id = json_extract(t.payload_json, '$.activeProviderThreadId')
+ AND provider_turn.status = 'running'
+ ) OR EXISTS (
+ SELECT 1
+ FROM orchestration_v2_projection_subagents subagent
+ WHERE subagent.thread_id = t.thread_id
+ AND subagent.origin = 'provider_native'
+ AND subagent.status = 'running'
+ AND subagent.provider_thread_id IS NOT NULL
+ AND subagent.child_thread_id IS NOT NULL
+ )
+ ELSE EXISTS (
+ SELECT 1
+ FROM orchestration_v2_projection_subagents subagent
+ WHERE subagent.thread_id = t.thread_id
+ AND subagent.origin = 'provider_native'
+ AND subagent.status = 'running'
+ AND subagent.provider_thread_id IS NOT NULL
+ AND subagent.child_thread_id IS NOT NULL
+ )
+ END AS has_interruptible_provider_native_background_work,
(
SELECT COUNT(*)
FROM orchestration_v2_projection_turn_items i
@@ -2508,6 +2574,8 @@ export const layer: Layer.Layer =
hasActiveRun: row.active_run_id !== null,
}),
];
+ const hasInterruptibleProviderNativeBackgroundWork =
+ row.has_interruptible_provider_native_background_work === 1;
return {
thread,
latestRunId,
@@ -2541,6 +2609,7 @@ export const layer: Layer.Layer =
: DateTime.makeUnsafe(row.latest_user_message_at),
hasActionableProposedPlan: row.has_actionable_proposed_plan === 1,
pendingBackgroundTasks,
+ hasInterruptibleProviderNativeBackgroundWork,
itemCount: row.item_count,
runlessItemCount: row.runless_item_count,
updatedAt: thread.updatedAt,
diff --git a/apps/server/src/orchestration-v2/ProviderAdapter.ts b/apps/server/src/orchestration-v2/ProviderAdapter.ts
index ab163db12e0..f340e9faa4b 100644
--- a/apps/server/src/orchestration-v2/ProviderAdapter.ts
+++ b/apps/server/src/orchestration-v2/ProviderAdapter.ts
@@ -503,6 +503,13 @@ export interface ProviderAdapterV2SessionRuntime {
readonly modelSelection?: ModelSelection;
readonly runtimePolicy?: ProviderAdapterV2RuntimePolicy;
}) => Effect.Effect;
+ /**
+ * Remove the provider-native thread when the owning application thread is
+ * permanently deleted. Archive and ordinary detach flows never call this.
+ */
+ readonly deleteThread?: (
+ providerThread: OrchestrationV2ProviderThread,
+ ) => Effect.Effect;
readonly startTurn: (
input: ProviderAdapterV2TurnInput,
) => Effect.Effect;
@@ -529,6 +536,14 @@ export interface ProviderAdapterV2SessionRuntime {
export interface ProviderAdapterV2Shape {
readonly instanceId: ProviderInstanceId;
readonly driver: ProviderDriverKind;
+ /**
+ * Remove a provider-native thread when no managed provider session remains
+ * alive. The caller supplies a scope for any temporary provider connection.
+ */
+ readonly deleteDetachedThread?: (input: {
+ readonly providerSession: OrchestrationV2ProviderSession;
+ readonly providerThread: OrchestrationV2ProviderThread;
+ }) => Effect.Effect;
readonly getCapabilities: () => Effect.Effect<
OrchestrationV2ProviderCapabilities,
ProviderAdapterV2Error
diff --git a/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.test.ts b/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.test.ts
new file mode 100644
index 00000000000..48c29f154ca
--- /dev/null
+++ b/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.test.ts
@@ -0,0 +1,317 @@
+import { assert, describe, it } from "@effect/vitest";
+import {
+ ProviderDriverKind,
+ ThreadId,
+ type OrchestrationV2ThreadProjection,
+} from "@t3tools/contracts";
+import * as Deferred from "effect/Deferred";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Queue from "effect/Queue";
+import * as TestClock from "effect/testing/TestClock";
+
+import {
+ ProviderInteractionModeReflections,
+ layer as reflectionsLayer,
+} from "./ProviderInteractionModeReflections.ts";
+import { OrchestratorDispatchError, OrchestratorProjectionError } from "./Orchestrator.ts";
+import { workerLive } from "./ProviderInteractionModeReflectionService.ts";
+import { ThreadManagementService } from "./ThreadManagementService.ts";
+
+const threadId = ThreadId.make("thread-interaction-mode-reflection");
+const driver = ProviderDriverKind.make("reflection-test");
+
+function reflectionCommandId(threadId: ThreadId, dedupeKey: string): string {
+ return `interaction-mode-reflection:${threadId}:${dedupeKey}`;
+}
+
+function projectionWith(input: {
+ readonly archivedAt?: string | null;
+ readonly interactionMode: "default" | "plan";
+}): OrchestrationV2ThreadProjection {
+ return {
+ thread: {
+ archivedAt: input.archivedAt ?? null,
+ interactionMode: input.interactionMode,
+ },
+ } as unknown as OrchestrationV2ThreadProjection;
+}
+
+/**
+ * The worker drains sequentially, so a probe request whose dispatch is awaited
+ * proves every earlier request finished without dispatching.
+ */
+function testLayer(input: {
+ readonly dispatched: Queue.Queue;
+ readonly projections: Array;
+ readonly readProjection?: () => Effect.Effect<
+ OrchestrationV2ThreadProjection,
+ OrchestratorProjectionError
+ >;
+ readonly dispatch?: ThreadManagementService["Service"]["dispatch"];
+}) {
+ const threads = Layer.mock(ThreadManagementService)({
+ getThreadProjection: () =>
+ input.readProjection?.() ??
+ Effect.sync(() => {
+ const next = input.projections.shift();
+ if (next === undefined) throw new Error("unexpected extra projection read");
+ return next;
+ }),
+ dispatch: (command) =>
+ input.dispatch?.(command) ??
+ Queue.offer(input.dispatched, command).pipe(Effect.as({} as never)),
+ });
+ const worker = workerLive.pipe(Layer.provide(Layer.merge(reflectionsLayer, threads)));
+ return Layer.merge(reflectionsLayer, worker);
+}
+
+const probeReflection = {
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey: "opencode2:probe",
+} as const;
+
+describe("ProviderInteractionModeReflectionService", () => {
+ it.effect("applies a native plan exit as thread.interaction-mode.set", () => {
+ return Effect.gen(function* () {
+ const dispatched = yield* Queue.unbounded();
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey: "opencode2:evt_1",
+ });
+ const command = (yield* Queue.take(dispatched)) as {
+ readonly type: string;
+ readonly commandId: string;
+ readonly threadId: string;
+ readonly interactionMode: string;
+ };
+ assert.equal(command.type, "thread.interaction-mode.set");
+ assert.equal(command.threadId, threadId);
+ assert.equal(command.interactionMode, "default");
+ // Replayed native events must dedupe through command receipts.
+ assert.equal(command.commandId, reflectionCommandId(threadId, "opencode2:evt_1"));
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [projectionWith({ interactionMode: "plan" })],
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+
+ it.effect("retries a transient dispatch failure before completing the reflection", () => {
+ return Effect.gen(function* () {
+ const dispatched = yield* Queue.unbounded();
+ const firstDispatch = Deferred.makeUnsafe();
+ let dispatchAttempts = 0;
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey: "opencode2:retry-projection",
+ });
+ const clockDriver = yield* Effect.gen(function* () {
+ yield* Deferred.await(firstDispatch);
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("100 millis");
+ }
+ }).pipe(Effect.forkChild);
+ const command = (yield* Queue.take(dispatched)) as {
+ readonly commandId: string;
+ readonly interactionMode: string;
+ };
+ assert.equal(
+ command.commandId,
+ reflectionCommandId(threadId, "opencode2:retry-projection"),
+ );
+ assert.equal(command.interactionMode, "default");
+ assert.equal(dispatchAttempts, 2);
+ yield* Fiber.join(clockDriver);
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [],
+ readProjection: () => Effect.succeed(projectionWith({ interactionMode: "plan" })),
+ dispatch: (command) =>
+ Effect.sync(() => {
+ dispatchAttempts += 1;
+ if (dispatchAttempts === 1) {
+ Deferred.doneUnsafe(firstDispatch, Effect.void);
+ }
+ return dispatchAttempts;
+ }).pipe(
+ Effect.flatMap((attempt) =>
+ attempt === 1
+ ? Effect.fail(
+ new OrchestratorDispatchError({
+ commandId: command.commandId,
+ commandType: command.type,
+ cause: new Error("temporary dispatch failure"),
+ }),
+ )
+ : Queue.offer(dispatched, command).pipe(Effect.as({} as never)),
+ ),
+ ),
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+
+ it.effect("continues draining after a defective reflection", () => {
+ return Effect.gen(function* () {
+ const dispatched = yield* Queue.unbounded();
+ let projectionReads = 0;
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey: "opencode2:defective",
+ });
+ yield* requests.offer(probeReflection);
+
+ const command = (yield* Queue.take(dispatched)) as { readonly commandId: string };
+ assert.equal(command.commandId, reflectionCommandId(threadId, "opencode2:probe"));
+ assert.equal(projectionReads, 2);
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [],
+ readProjection: () =>
+ Effect.sync(() => {
+ projectionReads += 1;
+ if (projectionReads === 1) throw new Error("unexpected projection defect");
+ return projectionWith({ interactionMode: "plan" });
+ }),
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+
+ it.effect("dispatches matching provider event keys independently per thread", () => {
+ return Effect.gen(function* () {
+ const secondThreadId = ThreadId.make("thread-interaction-mode-reflection-second");
+ const dispatched = yield* Queue.unbounded();
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ const dedupeKey = "opencode2:shared-event";
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey,
+ });
+ yield* requests.offer({
+ threadId: secondThreadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey,
+ });
+
+ const first = (yield* Queue.take(dispatched)) as { readonly commandId: string };
+ const second = (yield* Queue.take(dispatched)) as { readonly commandId: string };
+ assert.deepEqual(
+ [first.commandId, second.commandId],
+ [
+ reflectionCommandId(threadId, dedupeKey),
+ reflectionCommandId(secondThreadId, dedupeKey),
+ ],
+ );
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [
+ projectionWith({ interactionMode: "plan" }),
+ projectionWith({ interactionMode: "plan" }),
+ ],
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+
+ it.effect("skips a thread already in the reflected mode", () => {
+ return Effect.gen(function* () {
+ const dispatched = yield* Queue.unbounded();
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "plan",
+ dedupeKey: "opencode2:evt_2",
+ });
+ yield* requests.offer(probeReflection);
+ const command = (yield* Queue.take(dispatched)) as { readonly commandId: string };
+ assert.equal(command.commandId, reflectionCommandId(threadId, "opencode2:probe"));
+ assert.equal(yield* Queue.size(dispatched), 0);
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [
+ projectionWith({ interactionMode: "plan" }),
+ projectionWith({ interactionMode: "plan" }),
+ ],
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+
+ it.effect("skips archived threads", () => {
+ return Effect.gen(function* () {
+ const dispatched = yield* Queue.unbounded();
+ yield* Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ yield* requests.offer({
+ threadId,
+ driver,
+ interactionMode: "default",
+ dedupeKey: "opencode2:evt_3",
+ });
+ yield* requests.offer(probeReflection);
+ const command = (yield* Queue.take(dispatched)) as { readonly commandId: string };
+ assert.equal(command.commandId, reflectionCommandId(threadId, "opencode2:probe"));
+ assert.equal(yield* Queue.size(dispatched), 0);
+ }).pipe(
+ Effect.provide(
+ testLayer({
+ dispatched,
+ projections: [
+ projectionWith({
+ archivedAt: "2026-07-31T00:00:00.000Z",
+ interactionMode: "plan",
+ }),
+ projectionWith({ interactionMode: "plan" }),
+ ],
+ }),
+ ),
+ Effect.scoped,
+ );
+ });
+ });
+});
diff --git a/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.ts b/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.ts
new file mode 100644
index 00000000000..66b7cf8be90
--- /dev/null
+++ b/apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.ts
@@ -0,0 +1,70 @@
+import { CommandId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Schedule from "effect/Schedule";
+
+import {
+ type ProviderInteractionModeReflection,
+ ProviderInteractionModeReflections,
+} from "./ProviderInteractionModeReflections.ts";
+import { ThreadManagementService } from "./ThreadManagementService.ts";
+
+const MAX_REFLECTION_ATTEMPTS = 3;
+const REFLECTION_RETRY_DELAY = "100 millis";
+
+/**
+ * Drains ProviderInteractionModeReflections and applies each one as an
+ * ordinary `thread.interaction-mode.set` command, so the update flows through
+ * the same handler, event, and projection path as a user toggling the mode.
+ * Already-matching and archived threads are skipped; the command id derives
+ * from the reflection's thread and dedupe key, so a duplicated native event
+ * cannot apply twice.
+ */
+export const workerLive = Layer.effectDiscard(
+ Effect.gen(function* () {
+ const requests = yield* ProviderInteractionModeReflections;
+ const threads = yield* ThreadManagementService;
+
+ const applyReflection = Effect.fn("ProviderInteractionModeReflectionService.apply")(function* (
+ request: ProviderInteractionModeReflection,
+ ) {
+ const projection = yield* threads.getThreadProjection(request.threadId);
+ if (
+ projection.thread.archivedAt !== null ||
+ projection.thread.interactionMode === request.interactionMode
+ ) {
+ return;
+ }
+ yield* threads.dispatch({
+ type: "thread.interaction-mode.set",
+ commandId: CommandId.make(
+ `interaction-mode-reflection:${request.threadId}:${request.dedupeKey}`,
+ ),
+ threadId: request.threadId,
+ interactionMode: request.interactionMode,
+ });
+ });
+
+ const applyReflectionWithRetry = (request: ProviderInteractionModeReflection) =>
+ applyReflection(request).pipe(
+ Effect.retry({
+ times: MAX_REFLECTION_ATTEMPTS - 1,
+ schedule: Schedule.spaced(REFLECTION_RETRY_DELAY),
+ }),
+ Effect.catchCause(() =>
+ Effect.logWarning("orchestration-v2.interaction-mode-reflection.apply-failed", {
+ driver: request.driver,
+ interactionMode: request.interactionMode,
+ retrying: false,
+ threadId: request.threadId,
+ }),
+ ),
+ );
+
+ yield* requests.take.pipe(
+ Effect.flatMap(applyReflectionWithRetry),
+ Effect.forever,
+ Effect.forkScoped,
+ );
+ }),
+);
diff --git a/apps/server/src/orchestration-v2/ProviderInteractionModeReflections.ts b/apps/server/src/orchestration-v2/ProviderInteractionModeReflections.ts
new file mode 100644
index 00000000000..7a7ea3a8e6d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/ProviderInteractionModeReflections.ts
@@ -0,0 +1,45 @@
+import { ProviderDriverKind, ProviderInteractionMode, ThreadId } from "@t3tools/contracts";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Queue from "effect/Queue";
+
+export interface ProviderInteractionModeReflection {
+ readonly threadId: ThreadId;
+ readonly driver: ProviderDriverKind;
+ readonly interactionMode: ProviderInteractionMode;
+ /**
+ * Stable per-native-event key. The worker derives the command id from it, so
+ * a replayed or duplicated native event dedupes through command receipts
+ * instead of re-emitting a thread update.
+ */
+ readonly dedupeKey: string;
+}
+
+/**
+ * Adapters offer a reflection when the provider itself moves a session between
+ * its native plan and build modes (for example OpenCode's plan_exit flow
+ * switching the session to the build agent) so the thread's interaction mode
+ * follows reality instead of pushing the stale mode back on the next turn.
+ * The default reference drops requests, keeping adapter construction
+ * dependency-free in tests; the live layer must be shared with the
+ * ProviderInteractionModeReflectionService worker that drains it.
+ */
+export class ProviderInteractionModeReflections extends Context.Reference<{
+ readonly offer: (request: ProviderInteractionModeReflection) => Effect.Effect;
+ readonly take: Effect.Effect;
+}>("t3/orchestration-v2/ProviderInteractionModeReflections", {
+ defaultValue: () => ({ offer: () => Effect.void, take: Effect.never }),
+}) {}
+
+export const layer = Layer.effect(
+ ProviderInteractionModeReflections,
+ Effect.gen(function* () {
+ const queue = yield* Queue.unbounded();
+ return {
+ offer: (request: ProviderInteractionModeReflection) =>
+ Queue.offer(queue, request).pipe(Effect.asVoid),
+ take: Queue.take(queue),
+ };
+ }),
+);
diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
index b681836682e..ef5faa47895 100644
--- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
+++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
@@ -89,6 +89,8 @@ const ExclusiveCapabilities: OrchestrationV2ProviderCapabilities = {
interface TestProviderRuntimeState {
readonly openCount: number;
readonly closeCount: number;
+ readonly deleteCount: number;
+ readonly detachedDeleteCount: number;
readonly interruptCount: number;
readonly resumeCount: number;
readonly eventQueues: ReadonlyMap>;
@@ -97,6 +99,8 @@ interface TestProviderRuntimeState {
const emptyState: TestProviderRuntimeState = {
openCount: 0,
closeCount: 0,
+ deleteCount: 0,
+ detachedDeleteCount: 0,
interruptCount: 0,
resumeCount: 0,
eventQueues: new Map(),
@@ -237,11 +241,24 @@ function makeProviderAdapter(
}) => Effect.Effect;
readonly hasPendingBackgroundWork?: Effect.Effect;
readonly hangSessionScopeClose?: boolean;
+ readonly failDeleteThread?: boolean;
+ readonly failDetachedDeleteThread?: boolean;
} = {},
): ProviderAdapterV2Shape {
return {
instanceId: ProviderInstanceId.make("codex"),
driver: CODEX_DRIVER,
+ deleteDetachedThread: () =>
+ Ref.update(state, (current) => ({
+ ...current,
+ detachedDeleteCount: current.detachedDeleteCount + 1,
+ })).pipe(
+ Effect.andThen(
+ options.failDetachedDeleteThread === true
+ ? unimplemented("detached native deletion failed")
+ : Effect.void,
+ ),
+ ),
getCapabilities: () => Effect.succeed(options.capabilities ?? CodexCapabilities),
planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
openSession: (input) =>
@@ -307,6 +324,17 @@ function makeProviderAdapter(
...current,
resumeCount: current.resumeCount + 1,
})).pipe(Effect.as(threadInput.providerThread)),
+ deleteThread: () =>
+ Ref.update(state, (current) => ({
+ ...current,
+ deleteCount: current.deleteCount + 1,
+ })).pipe(
+ Effect.andThen(
+ options.failDeleteThread === true
+ ? unimplemented("native deletion failed")
+ : Effect.void,
+ ),
+ ),
startTurn: () => Effect.void,
steerTurn: () => Effect.void,
interruptTurn: () =>
@@ -338,6 +366,8 @@ function makeTestLayer(input: {
readonly failReleaseEventWrites?: boolean;
readonly hasPendingBackgroundWork?: Effect.Effect;
readonly hangSessionScopeClose?: boolean;
+ readonly failDeleteThread?: boolean;
+ readonly failDetachedDeleteThread?: boolean;
}) {
const configuredEventSinkLayer = input.failReleaseEventWrites
? FailingReleaseEventSinkLayer
@@ -354,6 +384,10 @@ function makeTestLayer(input: {
...(input.hangSessionScopeClose === undefined
? {}
: { hangSessionScopeClose: input.hangSessionScopeClose }),
+ ...(input.failDeleteThread === undefined ? {} : { failDeleteThread: input.failDeleteThread }),
+ ...(input.failDetachedDeleteThread === undefined
+ ? {}
+ : { failDetachedDeleteThread: input.failDetachedDeleteThread }),
}),
);
return Layer.mergeAll(
@@ -1233,6 +1267,244 @@ it.effect("ProviderSessionManagerV2 terminal detach revokes the thread's MCP cre
}),
);
+it.effect("ProviderSessionManagerV2 deletes native threads only on permanent detach", () =>
+ Effect.gen(function* () {
+ const state = yield* Ref.make(emptyState);
+ const effect = Effect.gen(function* () {
+ const eventSink = yield* EventSinkV2;
+ const idAllocator = yield* IdAllocatorV2;
+ const manager = yield* ProviderSessionManagerV2;
+ const now = yield* DateTime.now;
+ const projectId = yield* idAllocator.allocate.project({
+ fixtureName: "provider-session-manager-native-delete",
+ });
+ const threadId = yield* idAllocator.allocate.thread({
+ fixtureName: "provider-session-manager-native-delete",
+ projectId,
+ });
+ const providerSessionId = yield* idAllocator.allocate.providerSession({
+ providerInstanceId: modelSelection.instanceId,
+ threadId,
+ });
+ const providerThread = makeProviderThread({
+ idAllocator,
+ threadId,
+ providerSessionId,
+ now,
+ });
+
+ yield* eventSink.write({
+ events: [
+ yield* makeThreadCreatedEvent({ idAllocator, threadId, now }),
+ {
+ id: yield* idAllocator.allocate.event({ threadId }),
+ type: "provider-thread.updated",
+ threadId,
+ driver: CODEX_DRIVER,
+ occurredAt: now,
+ payload: providerThread,
+ },
+ ],
+ });
+ yield* manager.open({ threadId, providerSessionId, modelSelection, runtimePolicy });
+
+ yield* manager.detach({
+ providerSessionId,
+ threadId,
+ detail: "Thread deleted.",
+ deleteProviderThread: true,
+ });
+
+ assert.equal((yield* Ref.get(state)).deleteCount, 1);
+ });
+
+ yield* effect.pipe(
+ Effect.provide(
+ makeTestLayer({
+ state,
+ idleTimeoutMs: 1_000,
+ capabilities: ExclusiveCapabilities,
+ }),
+ ),
+ );
+ }),
+);
+
+it.effect(
+ "ProviderSessionManagerV2 cleans up the session and MCP credential when native deletion fails",
+ () =>
+ Effect.gen(function* () {
+ const state = yield* Ref.make(emptyState);
+ const mcpConfigs = yield* Ref.make<
+ ReadonlyArray
+ >([]);
+ const effect = Effect.gen(function* () {
+ const eventSink = yield* EventSinkV2;
+ const idAllocator = yield* IdAllocatorV2;
+ const manager = yield* ProviderSessionManagerV2;
+ const registry = yield* McpSessionRegistry.McpSessionRegistry;
+ const now = yield* DateTime.now;
+ const threadId = ThreadId.make("thread-provider-session-manager-delete-failure");
+ const providerSessionId = yield* idAllocator.allocate.providerSession({
+ providerInstanceId: modelSelection.instanceId,
+ threadId,
+ });
+ const providerThread = makeProviderThread({
+ idAllocator,
+ threadId,
+ providerSessionId,
+ now,
+ });
+
+ yield* eventSink.write({
+ events: [
+ yield* makeThreadCreatedEvent({ idAllocator, threadId, now }),
+ {
+ id: yield* idAllocator.allocate.event({ threadId }),
+ type: "provider-thread.updated",
+ threadId,
+ driver: CODEX_DRIVER,
+ occurredAt: now,
+ payload: providerThread,
+ },
+ ],
+ });
+ yield* manager.open({ threadId, providerSessionId, modelSelection, runtimePolicy });
+ const token = (yield* Ref.get(mcpConfigs))
+ .at(-1)
+ ?.authorizationHeader.replace(/^Bearer\s+/, "");
+ assert.isDefined(yield* registry.resolve(token!));
+
+ yield* manager
+ .detach({
+ providerSessionId,
+ threadId,
+ detail: "Thread deleted.",
+ deleteProviderThread: true,
+ revokeMcpCredential: true,
+ })
+ .pipe(Effect.flip);
+
+ const runtimeState = yield* Ref.get(state);
+ assert.equal(runtimeState.deleteCount, 1);
+ assert.equal(runtimeState.closeCount, 1);
+ assert.isUndefined(yield* registry.resolve(token!));
+ assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId));
+ });
+
+ yield* effect.pipe(
+ Effect.provide(
+ makeTestLayer({
+ state,
+ idleTimeoutMs: 1_000,
+ capabilities: ExclusiveCapabilities,
+ mcpConfigs,
+ failDeleteThread: true,
+ }),
+ ),
+ );
+ }),
+);
+
+it.effect("ProviderSessionManagerV2 deletes detached historical native threads", () =>
+ Effect.gen(function* () {
+ const state = yield* Ref.make(emptyState);
+ const effect = Effect.gen(function* () {
+ const idAllocator = yield* IdAllocatorV2;
+ const manager = yield* ProviderSessionManagerV2;
+ const now = yield* DateTime.now;
+ const threadId = yield* idAllocator.allocate.thread({
+ fixtureName: "provider-session-manager-historical-native-delete",
+ projectId: yield* idAllocator.allocate.project({
+ fixtureName: "provider-session-manager-historical-native-delete",
+ }),
+ });
+ const providerSessionId = yield* idAllocator.allocate.providerSession({
+ providerInstanceId: modelSelection.instanceId,
+ threadId,
+ });
+ const providerSession = makeProviderSession({ providerSessionId, now });
+ const providerThread = makeProviderThread({
+ idAllocator,
+ threadId,
+ providerSessionId,
+ now,
+ });
+
+ yield* manager.detach({
+ providerSessionId,
+ threadId,
+ detail: "Thread deleted.",
+ deleteProviderThread: true,
+ providerInstanceId: modelSelection.instanceId,
+ providerSession,
+ providerThreads: [providerThread],
+ });
+
+ assert.equal((yield* Ref.get(state)).detachedDeleteCount, 1);
+ assert.equal((yield* Ref.get(state)).openCount, 0);
+ });
+
+ yield* effect.pipe(
+ Effect.provide(
+ makeTestLayer({
+ state,
+ idleTimeoutMs: 1_000,
+ capabilities: ExclusiveCapabilities,
+ }),
+ ),
+ );
+ }),
+);
+
+it.effect("ProviderSessionManagerV2 reports detached native deletion after cleanup", () =>
+ Effect.gen(function* () {
+ const state = yield* Ref.make(emptyState);
+ const effect = Effect.gen(function* () {
+ const idAllocator = yield* IdAllocatorV2;
+ const manager = yield* ProviderSessionManagerV2;
+ const now = yield* DateTime.now;
+ const threadId = yield* idAllocator.allocate.thread({
+ fixtureName: "provider-session-manager-historical-native-delete-failure",
+ projectId: yield* idAllocator.allocate.project({
+ fixtureName: "provider-session-manager-historical-native-delete-failure",
+ }),
+ });
+ const providerSessionId = yield* idAllocator.allocate.providerSession({
+ providerInstanceId: modelSelection.instanceId,
+ threadId,
+ });
+
+ yield* manager
+ .detach({
+ providerSessionId,
+ threadId,
+ detail: "Thread deleted.",
+ deleteProviderThread: true,
+ revokeMcpCredential: true,
+ providerInstanceId: modelSelection.instanceId,
+ providerSession: makeProviderSession({ providerSessionId, now }),
+ providerThreads: [makeProviderThread({ idAllocator, threadId, providerSessionId, now })],
+ })
+ .pipe(Effect.flip);
+
+ assert.equal((yield* Ref.get(state)).detachedDeleteCount, 1);
+ assert.isUndefined(McpProviderSession.readMcpProviderSession(threadId));
+ });
+
+ yield* effect.pipe(
+ Effect.provide(
+ makeTestLayer({
+ state,
+ idleTimeoutMs: 1_000,
+ capabilities: ExclusiveCapabilities,
+ failDetachedDeleteThread: true,
+ }),
+ ),
+ );
+ }),
+);
+
it.effect("ProviderSessionManagerV2 releases idle sessions without sweeping all sessions", () =>
Effect.gen(function* () {
const state = yield* Ref.make(emptyState);
diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.ts
index 8eeacd7f685..483acaeaa5e 100644
--- a/apps/server/src/orchestration-v2/ProviderSessionManager.ts
+++ b/apps/server/src/orchestration-v2/ProviderSessionManager.ts
@@ -1,6 +1,7 @@
import {
ModelSelection,
OrchestrationV2DomainEvent,
+ type OrchestrationV2ProviderThread,
OrchestrationV2ProviderSession,
OrchestrationV2RuntimeRequest,
ProviderInstanceId,
@@ -160,6 +161,18 @@ export interface ProviderSessionManagerV2Shape {
* potential re-attach.
*/
readonly revokeMcpCredential?: boolean;
+ /**
+ * True only when the application thread is permanently deleted. Adapters
+ * with native thread deletion remove it before the runtime is detached.
+ */
+ readonly deleteProviderThread?: boolean;
+ /**
+ * Persisted deletion targets used when the managed runtime has already
+ * stopped by the time the replay-safe detach effect executes.
+ */
+ readonly providerInstanceId?: ProviderInstanceId;
+ readonly providerSession?: OrchestrationV2ProviderSession;
+ readonly providerThreads?: ReadonlyArray;
}) => Effect.Effect;
}
@@ -1539,43 +1552,96 @@ export const layerWithOptions = (
Effect.gen(function* () {
const key = sessionKey(input.providerSessionId);
const currentEntry = (yield* Ref.get(sessions)).get(key);
- if (currentEntry?.supportsMultipleProviderThreads === true) {
- const projection = yield* Effect.option(
- projectionStore.getThreadProjection(input.threadId),
+ const shouldLoadProviderThreads =
+ currentEntry !== undefined &&
+ (currentEntry.supportsMultipleProviderThreads || input.deleteProviderThread === true);
+ const projection = shouldLoadProviderThreads
+ ? yield* Effect.option(projectionStore.getThreadProjection(input.threadId))
+ : Option.none();
+ let providerThreads: ReadonlyMap<
+ OrchestrationV2ProviderThread["id"],
+ OrchestrationV2ProviderThread
+ >;
+ if (input.providerThreads !== undefined) {
+ providerThreads = new Map(
+ input.providerThreads.map((thread) => [thread.id, thread] as const),
);
- if (Option.isSome(projection)) {
- const providerThreads = new Map(
- projection.value.providerThreads
- .filter((thread) => thread.providerSessionId === input.providerSessionId)
- .map((thread) => [thread.id, thread] as const),
- );
- const activeTurns = projection.value.providerTurns.filter(
- (turn) => turn.status === "running" && providerThreads.has(turn.providerThreadId),
+ } else if (Option.isSome(projection)) {
+ providerThreads = new Map(
+ projection.value.providerThreads
+ .filter((thread) => thread.providerSessionId === input.providerSessionId)
+ .map((thread) => [thread.id, thread] as const),
+ );
+ } else {
+ providerThreads = new Map();
+ }
+ let deletionFailure: Exit.Exit | null = null;
+ if (
+ input.deleteProviderThread === true &&
+ currentEntry === undefined &&
+ input.providerInstanceId !== undefined &&
+ input.providerSession !== undefined
+ ) {
+ const adapter = yield* registry.get(input.providerInstanceId);
+ const deleteDetachedThread = adapter.deleteDetachedThread;
+ if (deleteDetachedThread !== undefined) {
+ const providerSession = input.providerSession;
+ const exits = yield* Effect.scoped(
+ Effect.forEach(providerThreads.values(), (providerThread) =>
+ Effect.exit(
+ deleteDetachedThread({
+ providerSession,
+ providerThread,
+ }),
+ ),
+ ),
);
- yield* Effect.forEach(
- activeTurns,
- (turn) =>
- currentEntry.exposedRuntime
- .interruptTurn({
- providerThread: providerThreads.get(turn.providerThreadId)!,
- providerTurnId: turn.id,
- })
- .pipe(
- Effect.catchCause((cause) =>
- Effect.logWarning(
- "orchestration-v2.driver-session.detach-interrupt-failed",
- {
- providerSessionId: input.providerSessionId,
- threadId: input.threadId,
- providerTurnId: turn.id,
- cause,
- },
- ),
+ deletionFailure = exits.find(Exit.isFailure) ?? null;
+ }
+ }
+ if (
+ currentEntry !== undefined &&
+ Option.isSome(projection) &&
+ (currentEntry.supportsMultipleProviderThreads || input.deleteProviderThread === true)
+ ) {
+ const activeTurns = projection.value.providerTurns.filter(
+ (turn) => turn.status === "running" && providerThreads.has(turn.providerThreadId),
+ );
+ yield* Effect.forEach(
+ activeTurns,
+ (turn) =>
+ currentEntry.exposedRuntime
+ .interruptTurn({
+ providerThread: providerThreads.get(turn.providerThreadId)!,
+ providerTurnId: turn.id,
+ })
+ .pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning(
+ "orchestration-v2.driver-session.detach-interrupt-failed",
+ {
+ providerSessionId: input.providerSessionId,
+ threadId: input.threadId,
+ providerTurnId: turn.id,
+ cause,
+ },
),
),
- { concurrency: 1, discard: true },
- );
- }
+ ),
+ { concurrency: 1, discard: true },
+ );
+ }
+ if (
+ input.deleteProviderThread === true &&
+ currentEntry?.exposedRuntime.deleteThread !== undefined
+ ) {
+ const exits = yield* Effect.forEach(
+ providerThreads.values(),
+ (providerThread) =>
+ Effect.exit(currentEntry.exposedRuntime.deleteThread!(providerThread)),
+ { concurrency: 1 },
+ );
+ deletionFailure = exits.find(Exit.isFailure) ?? deletionFailure;
}
const detached = yield* Ref.modify(sessions, (current) => {
const entry = current.get(key);
@@ -1624,21 +1690,23 @@ export const layerWithOptions = (
if (input.revokeMcpCredential === true) {
yield* clearMcpSession(input.threadId);
}
- if (Option.isNone(detached)) {
- return;
+ if (Option.isSome(detached)) {
+ if (
+ detached.value.attachedThreadIds.size === 0 &&
+ !detached.value.supportsMultipleProviderThreads
+ ) {
+ yield* releaseEntry({
+ providerSessionId: input.providerSessionId,
+ reason: "manual_shutdown",
+ ...(input.detail === undefined ? {} : { detail: input.detail }),
+ });
+ } else {
+ yield* scheduleIdleRelease(input.providerSessionId);
+ }
}
- if (
- detached.value.attachedThreadIds.size === 0 &&
- !detached.value.supportsMultipleProviderThreads
- ) {
- yield* releaseEntry({
- providerSessionId: input.providerSessionId,
- reason: "manual_shutdown",
- ...(input.detail === undefined ? {} : { detail: input.detail }),
- });
- return;
+ if (deletionFailure !== null && Exit.isFailure(deletionFailure)) {
+ return yield* Effect.failCause(deletionFailure.cause);
}
- yield* scheduleIdleRelease(input.providerSessionId);
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(
diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.test.ts b/apps/server/src/orchestration-v2/ThreadManagementService.test.ts
index e03d60c00d9..5b631c9141d 100644
--- a/apps/server/src/orchestration-v2/ThreadManagementService.test.ts
+++ b/apps/server/src/orchestration-v2/ThreadManagementService.test.ts
@@ -1,19 +1,29 @@
import { expect, it } from "@effect/vitest";
import {
CommandId,
+ EventId,
MessageId,
NodeId,
type OrchestrationV2Command,
+ type OrchestrationV2ThreadShell,
type OrchestrationV2ThreadProjection,
ProjectId,
+ ProviderDriverKind,
ProviderInstanceId,
+ ProviderThreadId,
+ ProviderTurnId,
RunId,
ThreadId,
} from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
-import { OrchestratorProjectionError, OrchestratorV2 } from "./Orchestrator.ts";
+import {
+ OrchestratorProjectionError,
+ OrchestratorV2,
+ type OrchestratorV2DispatchResult,
+} from "./Orchestrator.ts";
import {
existingThreadIdsForCommand,
layer,
@@ -318,3 +328,130 @@ it.effect("uses thread-not-found only after a projection loads outside the proje
expect("cause" in error).toBe(false);
}).pipe(Effect.provide(testLayer));
});
+
+it.effect("interrupts an ordinary waiting root run when no native background target exists", () => {
+ const projectId = ProjectId.make("project:thread-management:waiting-root");
+ const threadId = ThreadId.make("thread:thread-management:waiting-root");
+ const waitingRunId = RunId.make("run:thread-management:waiting-root");
+ const projection = {
+ thread: {
+ id: threadId,
+ projectId,
+ deletedAt: null,
+ },
+ runs: [{ id: waitingRunId, ordinal: 1, status: "waiting" }],
+ } as unknown as OrchestrationV2ThreadProjection;
+ const testLayer = layer.pipe(
+ Layer.provide(
+ Layer.mock(OrchestratorV2)({
+ getThreadProjection: () => Effect.succeed(projection),
+ getThreadShell: () =>
+ Effect.succeed({
+ hasInterruptibleProviderNativeBackgroundWork: false,
+ } as OrchestrationV2ThreadShell),
+ dispatch: () => Effect.succeed({ sequence: 1, storedEvents: [] }),
+ }),
+ ),
+ );
+
+ return Effect.gen(function* () {
+ const service = yield* ThreadManagementService;
+ const result = yield* service.interruptThread({
+ projectId,
+ commandId: CommandId.make("command:thread-management:waiting-root-stop"),
+ threadId,
+ });
+
+ expect(result).toMatchObject({
+ type: "interrupt_requested",
+ run: { id: waitingRunId, status: "waiting" },
+ });
+ }).pipe(Effect.provide(testLayer));
+});
+
+it.effect("pins run-less background Stop to provider-native targets across a root-run race", () => {
+ const projectId = ProjectId.make("project:thread-management:provider-race");
+ const threadId = ThreadId.make("thread:thread-management:provider-race");
+ const waitingRunId = RunId.make("run:thread-management:provider-race");
+ const providerThreadId = ProviderThreadId.make("provider-thread:thread-management:provider-race");
+ const providerTurnId = ProviderTurnId.make("provider-turn:thread-management:provider-race");
+ const commands: Array = [];
+ const projection = {
+ thread: {
+ id: threadId,
+ projectId,
+ deletedAt: null,
+ },
+ runs: [{ id: waitingRunId, ordinal: 1, status: "waiting" }],
+ } as unknown as OrchestrationV2ThreadProjection;
+ const dispatchResult = {
+ sequence: 1,
+ storedEvents: [
+ {
+ sequence: 1,
+ commandId: CommandId.make("command:thread-management:provider-race"),
+ event: {
+ id: EventId.make("event:thread-management:provider-race"),
+ type: "provider-turn.interrupt-requested",
+ threadId,
+ driver: ProviderDriverKind.make("opencode2"),
+ providerInstanceId: ProviderInstanceId.make("opencode2"),
+ occurredAt: DateTime.makeUnsafe("2026-08-01T12:00:00.000Z"),
+ payload: {
+ targetThreadId: ThreadId.make("thread:thread-management:provider-child"),
+ providerThreadId,
+ providerTurnId,
+ reason: null,
+ },
+ },
+ },
+ ],
+ } satisfies OrchestratorV2DispatchResult;
+ const testLayer = layer.pipe(
+ Layer.provide(
+ Layer.mock(OrchestratorV2)({
+ getThreadProjection: () => Effect.succeed(projection),
+ getThreadShell: () =>
+ Effect.succeed({
+ hasInterruptibleProviderNativeBackgroundWork: true,
+ } as OrchestrationV2ThreadShell),
+ dispatch: (command) => {
+ commands.push(command);
+ return Effect.succeed(dispatchResult);
+ },
+ }),
+ ),
+ );
+
+ return Effect.gen(function* () {
+ const service = yield* ThreadManagementService;
+ const result = yield* service.interruptThread({
+ projectId,
+ commandId: CommandId.make("command:thread-management:provider-race"),
+ threadId,
+ });
+
+ expect(result).toMatchObject({
+ type: "provider_interrupt_requested",
+ targets: [
+ {
+ threadId: ThreadId.make("thread:thread-management:provider-child"),
+ providerThreadId,
+ providerTurnId,
+ },
+ ],
+ });
+ expect(commands).toEqual([
+ {
+ type: "run.interrupt",
+ commandId: "command:thread-management:provider-race",
+ threadId,
+ intent: "provider_native_only",
+ },
+ ]);
+ expect(
+ (result as Extract).dispatch
+ .storedEvents[0]?.event.type,
+ ).toBe("provider-turn.interrupt-requested");
+ }).pipe(Effect.provide(testLayer));
+});
diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.ts b/apps/server/src/orchestration-v2/ThreadManagementService.ts
index a85477b7885..9f38c281bf6 100644
--- a/apps/server/src/orchestration-v2/ThreadManagementService.ts
+++ b/apps/server/src/orchestration-v2/ThreadManagementService.ts
@@ -13,6 +13,8 @@ import {
type OrchestrationV2ThreadShell,
type OrchestrationV2TurnItem,
ProjectId,
+ type ProviderThreadId,
+ type ProviderTurnId,
RunId,
ThreadId,
} from "@t3tools/contracts";
@@ -142,12 +144,23 @@ export interface ThreadManagementInterruptInput {
readonly reason?: string;
}
+export interface ThreadManagementProviderInterruptTarget {
+ readonly threadId: ThreadId;
+ readonly providerThreadId: ProviderThreadId;
+ readonly providerTurnId: ProviderTurnId;
+}
+
export type ThreadManagementInterruptResult =
| {
readonly type: "interrupt_requested";
readonly run: OrchestrationV2Run;
readonly dispatch: OrchestratorV2DispatchResult;
}
+ | {
+ readonly type: "provider_interrupt_requested";
+ readonly targets: ReadonlyArray;
+ readonly dispatch: OrchestratorV2DispatchResult;
+ }
| { readonly type: "no_active_run" }
| {
readonly type: "already_terminal";
@@ -617,17 +630,56 @@ const make = Effect.gen(function* () {
},
} as const;
}
+ // A provider-native child's own live turn gets the first Stop. Once the
+ // child turn is settled, the same path reaches its direct native
+ // descendants without crossing that child boundary.
const interruptibleRun = latestActiveRun(target);
- if (interruptibleRun === undefined) {
+ const shell =
+ input.runId === undefined &&
+ (interruptibleRun === undefined || interruptibleRun.status === "waiting")
+ ? yield* orchestrator.getThreadShell(input.threadId)
+ : undefined;
+ const hasProviderNativeBackgroundWork =
+ shell?.hasInterruptibleProviderNativeBackgroundWork === true;
+ const rootInterruptibleRun =
+ input.runId === undefined &&
+ interruptibleRun?.status === "waiting" &&
+ hasProviderNativeBackgroundWork
+ ? undefined
+ : interruptibleRun;
+ if (rootInterruptibleRun === undefined) {
if (input.runId === undefined) {
- return { type: "no_active_run" } as const;
+ if (!hasProviderNativeBackgroundWork) {
+ return { type: "no_active_run" } as const;
+ }
+ const dispatch = yield* orchestrator.dispatch({
+ type: "run.interrupt",
+ commandId: input.commandId,
+ threadId: input.threadId,
+ intent: "provider_native_only",
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
+ });
+ const targets = dispatch.storedEvents.flatMap((stored) =>
+ stored.event.type === "provider-turn.interrupt-requested"
+ ? [
+ {
+ threadId: stored.event.payload.targetThreadId,
+ providerThreadId: stored.event.payload.providerThreadId,
+ providerTurnId: stored.event.payload.providerTurnId,
+ },
+ ]
+ : [],
+ );
+ return targets.length === 0
+ ? ({ type: "no_active_run" } as const)
+ : ({ type: "provider_interrupt_requested", targets, dispatch } as const);
}
return yield* new ThreadManagementThreadNotInterruptibleError({
threadId: input.threadId,
runId: input.runId,
});
}
- if (input.runId !== undefined && interruptibleRun.id !== input.runId) {
+ if (input.runId !== undefined && rootInterruptibleRun.id !== input.runId) {
return yield* new ThreadManagementThreadNotInterruptibleError({
threadId: input.threadId,
runId: input.runId,
@@ -637,10 +689,10 @@ const make = Effect.gen(function* () {
type: "run.interrupt",
commandId: input.commandId,
threadId: input.threadId,
- runId: interruptibleRun.id,
+ runId: rootInterruptibleRun.id,
...(input.reason === undefined ? {} : { reason: input.reason }),
});
- return { type: "interrupt_requested", run: interruptibleRun, dispatch } as const;
+ return { type: "interrupt_requested", run: rootInterruptibleRun, dispatch } as const;
});
return ThreadManagementService.of({
diff --git a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
index 1ef37a2bfa0..4adc331667a 100644
--- a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
+++ b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
@@ -14,6 +14,10 @@ import {
type CursorAdapterV2DriverEnv,
} from "./Adapters/CursorAdapterV2.ts";
import { GrokAdapterV2Driver, type GrokAdapterV2DriverEnv } from "./Adapters/GrokAdapterV2.ts";
+import {
+ OpenCode2AdapterV2Driver,
+ type OpenCode2AdapterV2DriverEnv,
+} from "./Adapters/OpenCode2AdapterV2.ts";
import {
OpenCodeAdapterV2Driver,
type OpenCodeAdapterV2DriverEnv,
@@ -26,6 +30,7 @@ export type BuiltInProviderAdapterDriversV2Env =
| CodexAdapterV2DriverEnv
| CursorAdapterV2DriverEnv
| GrokAdapterV2DriverEnv
+ | OpenCode2AdapterV2DriverEnv
| OpenCodeAdapterV2DriverEnv;
export const BUILT_IN_PROVIDER_ADAPTER_DRIVERS_V2: ReadonlyArray<
@@ -35,6 +40,7 @@ export const BUILT_IN_PROVIDER_ADAPTER_DRIVERS_V2: ReadonlyArray<
ClaudeAdapterV2Driver,
CursorAdapterV2Driver,
OpenCodeAdapterV2Driver,
+ OpenCode2AdapterV2Driver,
GrokAdapterV2Driver,
AcpRegistryAdapterV2Driver,
];
diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts
index 1f34589ec36..53adcda2b44 100644
--- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts
+++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts
@@ -1216,6 +1216,16 @@ it.layer(SharedApplicationDataPlaneTestLayer)("pending provider interruption", (
);
assert.deepEqual(interrupted.providerTurns, []);
assert.isFalse(yield* effectWorker.runOnce);
+
+ const sequenceBeforeIdleStop = yield* orchestrator.getThreadEventSequence(threadId);
+ const idleStop = yield* threadManagement.interruptThread({
+ projectId,
+ commandId: CommandId.make("runtime-layer-pending-interrupt-idle-stop"),
+ threadId,
+ reason: "Repeated stop after completion",
+ });
+ assert.equal(idleStop.type, "no_active_run");
+ assert.equal(yield* orchestrator.getThreadEventSequence(threadId), sequenceBeforeIdleStop);
}),
);
});
diff --git a/apps/server/src/orchestration-v2/runtimeLayer.ts b/apps/server/src/orchestration-v2/runtimeLayer.ts
index 6f4df38e1d9..9e8c6b91f1d 100644
--- a/apps/server/src/orchestration-v2/runtimeLayer.ts
+++ b/apps/server/src/orchestration-v2/runtimeLayer.ts
@@ -28,6 +28,8 @@ import { layer as projectionMaintenanceLayer } from "./ProjectionMaintenance.ts"
import { layerFromProviderInstanceRegistry as providerAdapterRegistryLayerFromProviderInstances } from "./ProviderAdapterRegistry.ts";
import { layer as providerContinuationRequestsLayer } from "./ProviderContinuationRequests.ts";
import { workerLive as providerContinuationWorkerLive } from "./ProviderContinuationService.ts";
+import { layer as providerInteractionModeReflectionsLayer } from "./ProviderInteractionModeReflections.ts";
+import { workerLive as providerInteractionModeReflectionWorkerLive } from "./ProviderInteractionModeReflectionService.ts";
import { layer as threadTitleRegenerationServiceLayer } from "./ThreadTitleRegenerationService.ts";
import { layer as providerEventIngestorLayer } from "./ProviderEventIngestor.ts";
import { layer as providerSessionManagerLayer } from "./ProviderSessionManager.ts";
@@ -172,6 +174,7 @@ const orchestratorProvided = orchestratorLayer.pipe(
// Same layer reference as the continuation worker and the adapter
// infrastructure so layer memoization yields one shared request queue.
providerContinuationRequestsLayer,
+ providerInteractionModeReflectionsLayer,
providerEventIngestorProvided,
runtimePolicyProvided,
providerSessionManagerProvided,
@@ -243,6 +246,11 @@ const providerRuntimeRecoveryProvided = providerRuntimeRecoveryLayer.pipe(
),
);
+const providerInteractionModeReflectionWorkerProvided =
+ providerInteractionModeReflectionWorkerLive.pipe(
+ Layer.provide(Layer.merge(providerInteractionModeReflectionsLayer, threadManagementProvided)),
+ );
+
export const OrchestrationV2LayerLive = Layer.mergeAll(
orchestratorProvided,
threadManagementProvided,
@@ -261,4 +269,5 @@ export const OrchestrationV2ProductionLayerLive = Layer.mergeAll(
threadLifecycleProvided,
scheduledTaskProvided,
providerContinuationWorkerProvided,
+ providerInteractionModeReflectionWorkerProvided,
);
diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts
index b822220d76e..adb880ffe4d 100644
--- a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts
+++ b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts
@@ -10,6 +10,7 @@ import { CursorOrchestratorReplayHarness } from "../Adapters/CursorAdapterV2.tes
import { AcpRegistryOrchestratorReplayHarness } from "../Adapters/AcpRegistryAdapterV2.testkit.ts";
import { GrokOrchestratorReplayHarness } from "../Adapters/GrokAdapterV2.testkit.ts";
import { OpenCodeOrchestratorReplayHarness } from "../Adapters/OpenCodeAdapterV2.testkit.ts";
+import { OpenCode2OrchestratorReplayHarness } from "../Adapters/OpenCode2AdapterV2.testkit.ts";
import { layer as idAllocatorLayer } from "../IdAllocator.ts";
import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts";
import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts";
@@ -176,6 +177,11 @@ function runFixtureProviderWithRegisteredHarness(input: {
...input,
harness: OpenCodeOrchestratorReplayHarness,
}).pipe(Effect.mapError(normalizeTestError), Effect.scoped);
+ case "opencode2":
+ return runFixtureProvider({
+ ...input,
+ harness: OpenCode2OrchestratorReplayHarness,
+ }).pipe(Effect.mapError(normalizeTestError), Effect.scoped);
default:
return Effect.die(
new Error(`No replay harness registered for provider ${input.driver.driver}.`),
diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts
index ba8a13b9740..28fc019c654 100644
--- a/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts
+++ b/apps/server/src/orchestration-v2/testkit/OrchestratorScenario.ts
@@ -3,6 +3,7 @@ import type {
OrchestrationV2DomainEvent,
OrchestrationV2RuntimeRequest,
OrchestrationV2Run,
+ OrchestrationV2Subagent,
OrchestrationV2ThreadShellSnapshot,
OrchestrationV2StoredEvent,
OrchestrationV2ThreadProjection,
@@ -76,6 +77,12 @@ export type OrchestratorV2ScenarioStep =
readonly type: "capture_shell_snapshot";
readonly key: string;
}
+ | {
+ readonly type: "await_subagent_status";
+ readonly threadId: ThreadId;
+ readonly status: OrchestrationV2Subagent["status"];
+ readonly subagentId?: OrchestrationV2Subagent["id"];
+ }
| {
readonly type: "respond_to_next_runtime_request";
readonly threadId: ThreadId;
@@ -498,6 +505,41 @@ export function runOrchestratorV2Scenario(
return yield* releaseReplayGate(label, attemptsRemaining - 1);
});
+ const waitForSubagentStatus = (
+ threadId: ThreadId,
+ status: OrchestrationV2Subagent["status"],
+ subagentId?: OrchestrationV2Subagent["id"],
+ attemptsRemaining = SCENARIO_WAIT_ATTEMPTS,
+ deadlineAt = scenarioWaitDeadline(),
+ ): Effect.Effect =>
+ Effect.gen(function* () {
+ const projection = yield* orchestrator.getThreadProjection(threadId);
+ if (
+ projection.subagents.some(
+ (subagent) =>
+ subagent.threadId === threadId &&
+ subagent.status === status &&
+ (subagentId === undefined || subagent.id === subagentId),
+ )
+ ) {
+ return;
+ }
+ if (scenarioWaitExhausted(attemptsRemaining, deadlineAt)) {
+ return yield* new OrchestratorV2ScenarioStepError({
+ scenario: scenario.name,
+ step: `await_subagent_status:${threadId}:${subagentId ?? "any"}:${status}`,
+ });
+ }
+ yield* yieldToRuntime;
+ return yield* waitForSubagentStatus(
+ threadId,
+ status,
+ subagentId,
+ attemptsRemaining - 1,
+ deadlineAt,
+ );
+ });
+
for (const step of scenarioSteps(scenario)) {
switch (step.type) {
case "dispatch": {
@@ -550,6 +592,9 @@ export function runOrchestratorV2Scenario(
case "capture_shell_snapshot":
capturedShellSnapshots.set(step.key, yield* orchestrator.getShellSnapshot());
break;
+ case "await_subagent_status":
+ yield* waitForSubagentStatus(step.threadId, step.status, step.subagentId);
+ break;
case "respond_to_next_runtime_request": {
const request = yield* waitForPendingRuntimeRequest(step.threadId);
const result = yield* orchestrator.dispatch({
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/index.ts b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts
index b7799362446..1b57c428e5f 100644
--- a/apps/server/src/orchestration-v2/testkit/fixtures/index.ts
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/index.ts
@@ -18,6 +18,54 @@ import { messageSteeringInput } from "./message_steering/input.ts";
import { assertMultiTurnClaudeOutput } from "./multi_turn/claude_output.ts";
import { assertMultiTurnOutput } from "./multi_turn/codex_output.ts";
import { multiTurnInput } from "./multi_turn/input.ts";
+import { openCode2BackgroundStopInput } from "./opencode2_background_stop/input.ts";
+import { assertOpenCode2BackgroundStopOutput } from "./opencode2_background_stop/output.ts";
+import { openCode2BackgroundChildStopInput } from "./opencode2_background_child_stop/input.ts";
+import { assertOpenCode2BackgroundChildStopOutput } from "./opencode2_background_child_stop/output.ts";
+import { openCode2BackgroundChildStopRecoveryOrderInput } from "./opencode2_background_child_stop_recovery_order/input.ts";
+import { assertOpenCode2BackgroundChildStopRecoveryOrderOutput } from "./opencode2_background_child_stop_recovery_order/output.ts";
+import { openCode2BackgroundChildStopRecoveryRaceInput } from "./opencode2_background_child_stop_recovery_race/input.ts";
+import { assertOpenCode2BackgroundChildStopRecoveryRaceOutput } from "./opencode2_background_child_stop_recovery_race/output.ts";
+import { openCode2TwoBackgroundChildStopInput } from "./opencode2_two_background_child_stop/input.ts";
+import { assertOpenCode2TwoBackgroundChildStopOutput } from "./opencode2_two_background_child_stop/output.ts";
+import { openCode2TwoBackgroundChildReplayInput } from "./opencode2_two_background_child_replay/input.ts";
+import { assertOpenCode2TwoBackgroundChildReplayOutput } from "./opencode2_two_background_child_replay/output.ts";
+import { openCode2AmbiguousExecutionWakesInput } from "./opencode2_ambiguous_execution_wakes/input.ts";
+import { assertOpenCode2AmbiguousExecutionWakesOutput } from "./opencode2_ambiguous_execution_wakes/output.ts";
+import { openCode2RetiredSuppressWakeInput } from "./opencode2_retired_suppress_wake/input.ts";
+import { assertOpenCode2RetiredSuppressWakeOutput } from "./opencode2_retired_suppress_wake/output.ts";
+import { openCode2SharedExecutionReplayInput } from "./opencode2_shared_execution_replay/input.ts";
+import { assertOpenCode2SharedExecutionReplayOutput } from "./opencode2_shared_execution_replay/output.ts";
+import { openCode2SharedOrdinaryWakeReplayInput } from "./opencode2_shared_ordinary_wake_replay/input.ts";
+import { assertOpenCode2SharedOrdinaryWakeReplayOutput } from "./opencode2_shared_ordinary_wake_replay/output.ts";
+import { openCode2CompactionInput } from "./opencode2_compaction/input.ts";
+import { assertOpenCode2CompactionOutput } from "./opencode2_compaction/output.ts";
+import { openCode2PermissionExternalSubagentInput } from "./opencode2_permission_external_subagent/input.ts";
+import { assertOpenCode2PermissionExternalSubagentOutput } from "./opencode2_permission_external_subagent/output.ts";
+import { openCode2PermissionReplyFailureInput } from "./opencode2_permission_reply_failure/input.ts";
+import { assertOpenCode2PermissionReplyFailureOutput } from "./opencode2_permission_reply_failure/output.ts";
+import { openCode2PermissionReplyFailureSubagentInput } from "./opencode2_permission_reply_failure_subagent/input.ts";
+import { assertOpenCode2PermissionReplyFailureSubagentOutput } from "./opencode2_permission_reply_failure_subagent/output.ts";
+import { openCode2PermissionSessionInput } from "./opencode2_permission_session/input.ts";
+import { assertOpenCode2PermissionSessionOutput } from "./opencode2_permission_session/output.ts";
+import { openCode2QuestionLegacyInput } from "./opencode2_question_legacy/input.ts";
+import { assertOpenCode2QuestionLegacyOutput } from "./opencode2_question_legacy/output.ts";
+import { openCode2RetryInput } from "./opencode2_retry/input.ts";
+import { assertOpenCode2RetryOutput } from "./opencode2_retry/output.ts";
+import { openCode2ShellProjectionInput } from "./opencode2_shell_projection/input.ts";
+import { assertOpenCode2ShellProjectionOutput } from "./opencode2_shell_projection/output.ts";
+import { openCode2ShellTerminalsInput } from "./opencode2_shell_terminals/input.ts";
+import { assertOpenCode2ShellTerminalsOutput } from "./opencode2_shell_terminals/output.ts";
+import { openCode2SubagentBackgroundWakeInput } from "./opencode2_subagent_background_wake/input.ts";
+import { assertOpenCode2SubagentBackgroundWakeOutput } from "./opencode2_subagent_background_wake/output.ts";
+import { openCode2SubagentRateLimitInput } from "./opencode2_subagent_rate_limit/input.ts";
+import { assertOpenCode2SubagentRateLimitOutput } from "./opencode2_subagent_rate_limit/output.ts";
+import { openCode2SubagentQueuedTurnInput } from "./opencode2_subagent_queued_turn/input.ts";
+import { assertOpenCode2SubagentQueuedTurnOutput } from "./opencode2_subagent_queued_turn/output.ts";
+import { openCode2SubagentSupervisedInput } from "./opencode2_subagent_supervised/input.ts";
+import { assertOpenCode2SubagentSupervisedOutput } from "./opencode2_subagent_supervised/output.ts";
+import { openCode2ThreadDeleteInput } from "./opencode2_thread_delete/input.ts";
+import { assertOpenCode2ThreadDeleteOutput } from "./opencode2_thread_delete/output.ts";
import { openCodeSubagentInput } from "./opencode_subagent/input.ts";
import { assertOpenCodeSubagentOutput } from "./opencode_subagent/output.ts";
import { assertPlanQuestionsOutput } from "./plan_questions/codex_output.ts";
@@ -79,6 +127,7 @@ import {
CODEX_MODEL_SELECTION,
CURSOR_MODEL_SELECTION,
GROK_MODEL_SELECTION,
+ OPENCODE2_MODEL_SELECTION,
OPENCODE_MODEL_SELECTION,
READ_ONLY_NEVER_POLICY,
READ_ONLY_ON_REQUEST_POLICY,
@@ -220,6 +269,12 @@ export const ORCHESTRATOR_REPLAY_FIXTURES: ReadonlyArray"}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_multi_turn","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 multi-turn replay","location":{"directory":""}}}}
+{"type":"expect_outbound","label":"session.prompt.first","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_multi_turn","prompt":{"text":"Respond with exactly: first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.first.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_first"}}}
+{"type":"emit_inbound","label":"session.input.admitted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_multi_turn","inputID":"input_opencode2_first","input":{"id":"input_opencode2_first","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_multi_turn"}}}}
+{"type":"emit_inbound","label":"session.text.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_first","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_first","ordinal":0,"delta":"first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_first","ordinal":0,"text":"first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_multi_turn"}}}}
+{"type":"expect_outbound","label":"session.pending.list.first","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_multi_turn"}}}
+{"type":"emit_inbound","label":"session.pending.list.first.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.first","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.first.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.second","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_multi_turn","prompt":{"text":"Respond with exactly: second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.second.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_second"}}}
+{"type":"emit_inbound","label":"session.input.admitted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_multi_turn","inputID":"input_opencode2_second","input":{"id":"input_opencode2_second","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.late-first","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_multi_turn"}}}}
+{"type":"emit_inbound","label":"session.execution.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_multi_turn"}}}}
+{"type":"emit_inbound","label":"session.text.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_second","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"delta":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_multi_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"text":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_multi_turn"}}}}
+{"type":"expect_outbound","label":"session.pending.list.second","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_multi_turn"}}}
+{"type":"emit_inbound","label":"session.pending.list.second.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.second","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.second.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/input.ts
new file mode 100644
index 00000000000..d36b1d337e7
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/input.ts
@@ -0,0 +1,13 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2AmbiguousExecutionWakesInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ {
+ type: "message",
+ text: "Recover after an ambiguous execution. Respond exactly RECOVERY_OK",
+ },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..e468c57dca2
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/opencode2_transcript.ndjson
@@ -0,0 +1,38 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_ambiguous_execution_wakes","metadata":{"source":"focused-provider-native-replay-ownership","description":"Two suppressed synthetic wakes are pending without promotion when a new execution starts before its ordinary input is correlated. The unattributable output must be swallowed and the later recovery must remain clean."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_ambiguous_execution_wakes","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 ambiguous execution wakes","location":{"directory":"/private/tmp/t3-opencode2-ambiguous-execution-wakes"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_ambiguous_execution_wakes_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_root","ordinal":0}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_root","ordinal":0,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_root","ordinal":0,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-ambiguous-execution-wakes"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"suppressed.alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_alpha","input":{"type":"synthetic","data":{"text":"ALPHA_CANCELLED","description":"alpha cancelled child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"suppressed.bravo.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_bravo","input":{"type":"synthetic","data":{"text":"BRAVO_INTERRUPTED","description":"bravo interrupted child fixture"},"delivery":"queue"}}}}}
+{"type":"expect_outbound","label":"session.prompt.recovery","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","prompt":{"text":"Recover after an ambiguous execution. Respond exactly RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{}}}
+{"type":"emit_inbound","label":"ambiguous.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"emit_inbound","label":"recovery.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_recovery","input":{"type":"user","data":{"text":"Recover after an ambiguous execution. Respond exactly RECOVERY_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"ambiguous.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_must_not_appear","ordinal":0}}}}
+{"type":"emit_inbound","label":"ambiguous.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_must_not_appear","ordinal":0,"delta":"AMBIGUOUS_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"ambiguous.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_must_not_appear","ordinal":0,"text":"AMBIGUOUS_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"ambiguous.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"emit_inbound","label":"recovery.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","inputID":"input_opencode2_ambiguous_execution_wakes_recovery"}}}}
+{"type":"emit_inbound","label":"recovery.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"emit_inbound","label":"recovery.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_recovery","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_recovery","ordinal":0,"delta":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_ambiguous_execution_wakes","assistantMessageID":"message_opencode2_ambiguous_execution_wakes_recovery","ordinal":0,"text":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}}
+{"type":"expect_outbound","label":"recovery.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_ambiguous_execution_wakes"}}}
+{"type":"emit_inbound","label":"recovery.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"recovery.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-ambiguous-execution-wakes"}}}}
+{"type":"emit_inbound","label":"recovery.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/output.ts
new file mode 100644
index 00000000000..bd8441568f8
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_ambiguous_execution_wakes/output.ts
@@ -0,0 +1,27 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2AmbiguousExecutionWakesOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "RECOVERY_OK");
+ assert.notInclude(JSON.stringify(projection), "AMBIGUOUS_OUTPUT_MUST_NOT_APPEAR");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/input.ts
new file mode 100644
index 00000000000..7705bd3a608
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/input.ts
@@ -0,0 +1,14 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2BackgroundChildStopInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ {
+ type: "interrupt_provider_native",
+ subagentNativeItemId: "tool:call_opencode2_background_child_stop",
+ },
+ { type: "message", text: "Recover after cancellation. Respond exactly RECOVERY_OK" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..09ec318891d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/opencode2_transcript.ndjson
@@ -0,0 +1,60 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_background_child_stop","metadata":{"source":"focused-provider-native-stop","description":"Interrupt a directly projected OpenCode 2 background child after the root run settles. The child shell must be removed on the child native session, interrupted partial output must be retained, the cancelled synthetic parent span must be isolated, and recovery input is admitted while the cancelled native execution is still draining."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_background_child_stop","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 background child stop replay","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":0,"callID":"call_opencode2_background_child_stop","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":0,"callID":"call_opencode2_background_child_stop","input":{"agent":"explore","background":true,"description":"background child fixture","prompt":"Respond exactly CHILD_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"subagent.launch.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":0,"callID":"call_opencode2_background_child_stop","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_background_child_stop_child"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_background_child_stop_child","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_background_child_stop_child","info":{"id":"ses_opencode2_background_child_stop_child","slug":"background-child-stop","projectID":"global","directory":"/private/tmp/t3-opencode2-background-child-stop","parentID":"ses_opencode2_background_child_stop","title":"background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_child","inputID":"input_opencode2_background_child_stop_child","input":{"type":"user","data":{"text":"Respond exactly CHILD_BACKGROUND_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"child.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_child","inputID":"input_opencode2_background_child_stop_child"}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_child"}}}}
+{"type":"emit_inbound","label":"child.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_child","ordinal":0,"callID":"call_opencode2_background_child_stop_shell","name":"bash"}}}}
+{"type":"emit_inbound","label":"child.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_child","ordinal":0,"callID":"call_opencode2_background_child_stop_shell","input":{"command":"sleep 30 && echo child partial"}}}}}
+{"type":"emit_inbound","label":"child.shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_background_child_stop","status":"running","command":"sleep 30 && echo child partial","cwd":"/private/tmp/t3-opencode2-background-child-stop","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-background-child-stop/shell.log","pid":4246,"metadata":{"sessionID":"ses_opencode2_background_child_stop_child"},"time":{"started":1785394800200}}}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_partial","ordinal":1}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_partial","ordinal":1,"delta":"CHILD_PARTIAL"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_partial","ordinal":1,"text":"CHILD_PARTIAL"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":1,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_root","ordinal":1,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"expect_outbound","label":"child.session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_background_child_stop_child"}}}
+{"type":"emit_inbound","label":"child.session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"expect_outbound","label":"child.shell.remove","frame":{"type":"shell.remove","input":{"id":"shell_opencode2_background_child_stop","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"child.shell.remove.response","frame":{"type":"sdk.response","operation":"shell.remove","data":true}}
+{"type":"emit_inbound","label":"child.shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_background_child_stop"}}}}
+{"type":"emit_inbound","label":"child.tool.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_background_child_stop_child","assistantMessageID":"message_opencode2_background_child_stop_child","callID":"call_opencode2_background_child_stop_shell","error":{"type":"ToolExecutionError","message":"Tool execution interrupted"},"executed":true}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_cancelled","input":{"type":"synthetic","data":{"text":"CHILD_PARTIAL","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_cancelled_second","input":{"type":"synthetic","data":{"text":"CHILD_PARTIAL_SECOND","description":"background child fixture second"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_cancelled"}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_cancelled_second"}}}}
+{"type":"emit_inbound","label":"cancelled.root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"emit_inbound","label":"child.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_child","reason":"user"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.stop","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.stop.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.stop","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.stop.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.recovery","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop","prompt":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_recovery"}}}
+{"type":"emit_inbound","label":"recovery.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_recovery","input":{"type":"user","data":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_cancelled","ordinal":0}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_cancelled","ordinal":0,"delta":"CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_cancelled","ordinal":0,"text":"CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"cancelled.root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"emit_inbound","label":"recovery.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop","inputID":"input_opencode2_background_child_stop_recovery"}}}}
+{"type":"emit_inbound","label":"recovery.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"emit_inbound","label":"recovery.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_recovery","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_recovery","ordinal":0,"delta":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop","assistantMessageID":"message_opencode2_background_child_stop_recovery","ordinal":0,"text":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.recovery","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.recovery.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.recovery","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.recovery.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/output.ts
new file mode 100644
index 00000000000..743f1b3ce2b
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop/output.ts
@@ -0,0 +1,77 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2BackgroundChildStopOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const cancellationStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "cancelled.root.execution.started",
+ );
+ const recoveryAdmitted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.input.admitted",
+ );
+ const cancellationEnded = transcript.entries.findIndex(
+ (entry) =>
+ entry.type === "emit_inbound" && entry.label === "cancelled.root.execution.succeeded",
+ );
+ const recoveryStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.execution.started",
+ );
+ assert.isAtLeast(cancellationStarted, 0);
+ assert.isAtLeast(recoveryAdmitted, 0);
+ assert.isAtLeast(cancellationEnded, 0);
+ assert.isAtLeast(recoveryStarted, 0);
+ assert.isAbove(recoveryAdmitted, cancellationStarted);
+ assert.isAbove(cancellationEnded, recoveryAdmitted);
+ assert.isAbove(recoveryStarted, cancellationEnded);
+
+ const sessionInterruptIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "child.session.interrupt",
+ );
+ const shellRemoveIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "child.shell.remove",
+ );
+ assert.isAtLeast(sessionInterruptIndex, 0);
+ assert.isAtLeast(shellRemoveIndex, 0);
+ assert.isAbove(shellRemoveIndex, sessionInterruptIndex);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assert.equal(projection.runs.length, 2, "cancellation must not create an ordinary success wake");
+ assertAssistantTextIncludes(projection, "RECOVERY_OK");
+ assert.notInclude(
+ projection.messages.map((message) => message.text).join("\n"),
+ "CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR",
+ );
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR");
+ assert.notInclude(JSON.stringify(projection), "CHILD_PARTIAL_SECOND");
+ const subagentItem = projection.turnItems.find((item) => item.type === "subagent");
+ assert.strictEqual(subagentItem?.type, "subagent");
+ if (subagentItem?.type !== "subagent") {
+ throw new Error("OpenCode 2 background child stop item is missing");
+ }
+ assert.equal(subagentItem.status, "interrupted");
+ assert.include(subagentItem.result ?? "", "CHILD_PARTIAL");
+ assert.isNotNull(subagentItem.childThreadId);
+
+ const child = result.projections.get(subagentItem.childThreadId!);
+ assert.isDefined(child);
+ assertAssistantTextIncludes(child!, "CHILD_PARTIAL");
+ assert.equal(child!.providerTurns.at(-1)?.status, "interrupted");
+ assert.equal(child!.providerThreads.at(-1)?.status, "idle");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/input.ts
new file mode 100644
index 00000000000..b6b2f72ca3e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/input.ts
@@ -0,0 +1,14 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2BackgroundChildStopRecoveryOrderInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ {
+ type: "interrupt_provider_native",
+ subagentNativeItemId: "tool:call_opencode2_background_child_stop_recovery_order",
+ },
+ { type: "message", text: "Recover after cancellation. Respond exactly RECOVERY_OK" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..7412e506904
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/opencode2_transcript.ndjson
@@ -0,0 +1,55 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_background_child_stop_recovery_order","metadata":{"source":"focused-provider-native-stop","description":"A user recovery input is admitted before cancelled synthetic inputs. Their promoted ids own separate cancellation and user executions, with the cancellation execution suppressed."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_background_child_stop_recovery_order","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 recovery ordering","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_recovery_order_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_order","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_order","input":{"agent":"explore","background":true,"description":"background child fixture","prompt":"Respond exactly CHILD_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"subagent.launch.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_order","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_background_child_stop_recovery_order_child","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child","info":{"id":"ses_opencode2_background_child_stop_recovery_order_child","slug":"background-child-stop","projectID":"global","directory":"/private/tmp/t3-opencode2-background-child-stop","parentID":"ses_opencode2_background_child_stop_recovery_order","title":"background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child","inputID":"input_opencode2_background_child_stop_recovery_order_child","input":{"type":"user","data":{"text":"Respond exactly CHILD_BACKGROUND_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"child.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child","inputID":"input_opencode2_background_child_stop_recovery_order_child"}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child"}}}}
+{"type":"emit_inbound","label":"child.shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_background_child_stop_recovery_order","status":"running","command":"sleep 30 && echo child partial","cwd":"/private/tmp/t3-opencode2-background-child-stop","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-background-child-stop/shell.log","pid":4246,"metadata":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child"},"time":{"started":1785394800200}}}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":1,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_root","ordinal":1,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"expect_outbound","label":"child.session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child"}}}
+{"type":"emit_inbound","label":"child.session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"expect_outbound","label":"child.shell.remove","frame":{"type":"shell.remove","input":{"id":"shell_opencode2_background_child_stop_recovery_order","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"child.shell.remove.response","frame":{"type":"sdk.response","operation":"shell.remove","data":true}}
+{"type":"emit_inbound","label":"child.shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"emit_inbound","label":"child.tool.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_child","callID":"call_opencode2_background_child_stop_recovery_order_shell","error":{"type":"ToolExecutionError","message":"Tool execution interrupted"},"executed":true}}}}
+{"type":"emit_inbound","label":"child.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_order_child","reason":"user"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.stop","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.stop.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.stop","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.stop.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.recovery","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","prompt":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_recovery_order_recovery"}}}
+{"type":"emit_inbound","label":"recovery.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_recovery","input":{"type":"user","data":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_cancelled","input":{"type":"synthetic","data":{"text":"CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_cancelled_second","input":{"type":"synthetic","data":{"text":"CANCELLED_ROOT_OUTPUT_SECOND_MUST_NOT_APPEAR","description":"background child fixture second"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_cancelled"}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_cancelled_second"}}}}
+{"type":"emit_inbound","label":"cancelled.root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_cancelled","ordinal":0}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_cancelled","ordinal":0,"delta":"CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"cancelled.root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_cancelled","ordinal":0,"text":"CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"cancelled.root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"emit_inbound","label":"recovery.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","inputID":"input_opencode2_background_child_stop_recovery_order_recovery"}}}}
+{"type":"emit_inbound","label":"recovery.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"emit_inbound","label":"recovery.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_recovery","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_recovery","ordinal":0,"delta":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_order","assistantMessageID":"message_opencode2_background_child_stop_recovery_order_recovery","ordinal":0,"text":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.recovery","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_order"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.recovery.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.recovery","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.recovery.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/output.ts
new file mode 100644
index 00000000000..a5e033ae7cf
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_order/output.ts
@@ -0,0 +1,75 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2BackgroundChildStopRecoveryOrderOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const recoveryAdmitted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.input.admitted",
+ );
+ const cancelledAdmitted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "cancelled.parent.wake",
+ );
+ const cancelledPromoted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "cancelled.input.promoted.first",
+ );
+ const cancelledStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "cancelled.root.execution.started",
+ );
+ const cancelledEnded = transcript.entries.findIndex(
+ (entry) =>
+ entry.type === "emit_inbound" && entry.label === "cancelled.root.execution.succeeded",
+ );
+ const recoveryPromoted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.input.promoted",
+ );
+ const recoveryStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.execution.started",
+ );
+ assert.isAtLeast(recoveryAdmitted, 0);
+ assert.isAtLeast(cancelledAdmitted, 0);
+ assert.isAtLeast(cancelledPromoted, 0);
+ assert.isAtLeast(cancelledStarted, 0);
+ assert.isAtLeast(cancelledEnded, 0);
+ assert.isAtLeast(recoveryPromoted, 0);
+ assert.isAtLeast(recoveryStarted, 0);
+ assert.isAbove(cancelledAdmitted, recoveryAdmitted);
+ assert.isAbove(cancelledPromoted, cancelledAdmitted);
+ assert.isAbove(cancelledStarted, cancelledPromoted);
+ assert.isAbove(cancelledEnded, cancelledStarted);
+ assert.isAbove(recoveryPromoted, cancelledEnded);
+ assert.isAbove(recoveryStarted, recoveryPromoted);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "RECOVERY_OK");
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR");
+
+ const subagentItem = projection.turnItems.find((item) => item.type === "subagent");
+ assert.strictEqual(subagentItem?.type, "subagent");
+ if (subagentItem?.type !== "subagent") {
+ throw new Error("OpenCode 2 recovery ordering fixture is missing its child item");
+ }
+ assert.equal(subagentItem.status, "interrupted");
+ assert.isNotNull(subagentItem.childThreadId);
+ const child = result.projections.get(subagentItem.childThreadId!);
+ assert.isDefined(child);
+ assert.equal(child!.providerTurns.at(-1)?.status, "interrupted");
+ assert.notInclude(JSON.stringify(child), "CANCELLED_ROOT_OUTPUT_MUST_NOT_APPEAR");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/input.ts
new file mode 100644
index 00000000000..a5eebb826bd
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/input.ts
@@ -0,0 +1,14 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2BackgroundChildStopRecoveryRaceInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ {
+ type: "interrupt_provider_native",
+ subagentNativeItemId: "tool:call_opencode2_background_child_stop_recovery_race",
+ },
+ { type: "message", text: "Recover after cancellation. Respond exactly RECOVERY_OK" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..dcd6d73714b
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/opencode2_transcript.ndjson
@@ -0,0 +1,57 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_background_child_stop_recovery_race","metadata":{"source":"focused-provider-native-stop","description":"A cancelled synthetic parent admission arrives after an ordinary recovery input has a native input id. The recovery execution must own the shared boundary and complete without cancelled continuation output."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_background_child_stop_recovery_race","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 background child stop recovery race replay","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop-recovery-race"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_recovery_race_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_race","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_race","input":{"agent":"explore","background":true,"description":"background child fixture","prompt":"Respond exactly CHILD_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"subagent.launch.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_race","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_background_child_stop_recovery_race_child","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","info":{"id":"ses_opencode2_background_child_stop_recovery_race_child","slug":"background-child-stop-recovery-race","projectID":"global","directory":"/private/tmp/t3-opencode2-background-child-stop-recovery-race","parentID":"ses_opencode2_background_child_stop_recovery_race","title":"background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","inputID":"input_opencode2_background_child_stop_recovery_race_child","input":{"type":"user","data":{"text":"Respond exactly CHILD_BACKGROUND_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"child.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","inputID":"input_opencode2_background_child_stop_recovery_race_child"}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child"}}}}
+{"type":"emit_inbound","label":"child.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_child","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_race_shell","name":"bash"}}}}
+{"type":"emit_inbound","label":"child.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_child","ordinal":0,"callID":"call_opencode2_background_child_stop_recovery_race_shell","input":{"command":"sleep 30 && echo child partial"}}}}}
+{"type":"emit_inbound","label":"child.shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_background_child_stop_recovery_race","status":"running","command":"sleep 30 && echo child partial","cwd":"/private/tmp/t3-opencode2-background-child-stop-recovery-race","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-background-child-stop-recovery-race/shell.log","pid":4246,"metadata":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child"},"time":{"started":1785394800200}}}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_partial","ordinal":1}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_partial","ordinal":1,"delta":"CHILD_PARTIAL"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_partial","ordinal":1,"text":"CHILD_PARTIAL"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":1,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_root","ordinal":1,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}}
+{"type":"expect_outbound","label":"child.session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child"}}}
+{"type":"emit_inbound","label":"child.session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"expect_outbound","label":"child.shell.remove","frame":{"type":"shell.remove","input":{"id":"shell_opencode2_background_child_stop_recovery_race","location":{"directory":"/private/tmp/t3-opencode2-background-child-stop-recovery-race"}}}}
+{"type":"emit_inbound","label":"child.shell.remove.response","frame":{"type":"sdk.response","operation":"shell.remove","data":true}}
+{"type":"emit_inbound","label":"child.shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_background_child_stop_recovery_race"}}}}
+{"type":"emit_inbound","label":"child.tool.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race_child","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_child","callID":"call_opencode2_background_child_stop_recovery_race_shell","error":{"type":"ToolExecutionError","message":"Tool execution interrupted"},"executed":true}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled","input":{"type":"synthetic","data":{"text":"CHILD_PARTIAL","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled_second","input":{"type":"synthetic","data":{"text":"CHILD_PARTIAL_SECOND","description":"background child fixture second"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"child.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_race_child","reason":"user"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.stop","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.stop.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.stop","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop-recovery-race"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.stop.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.recovery","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","prompt":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_child_stop_recovery_race_recovery"}}}
+{"type":"emit_inbound","label":"recovery.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_recovery","input":{"type":"user","data":{"text":"Recover after cancellation. Respond exactly RECOVERY_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled"}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled_second"}}}}
+{"type":"emit_inbound","label":"recovery.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_recovery"}}}}
+{"type":"emit_inbound","label":"cancelled.parent.wake.late","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled_late","input":{"type":"synthetic","data":{"text":"CANCELLED_CONTINUATION_MUST_NOT_APPEAR","description":"late cancelled background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancelled.input.promoted.late","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","inputID":"input_opencode2_background_child_stop_recovery_race_cancelled_late"}}}}
+{"type":"emit_inbound","label":"recovery.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}}
+{"type":"emit_inbound","label":"recovery.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_recovery","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_recovery","ordinal":0,"delta":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_child_stop_recovery_race","assistantMessageID":"message_opencode2_background_child_stop_recovery_race_recovery","ordinal":0,"text":"RECOVERY_OK"}}}}
+{"type":"emit_inbound","label":"recovery.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.recovery","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_background_child_stop_recovery_race"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.recovery.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.recovery","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-child-stop-recovery-race"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.recovery.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/output.ts
new file mode 100644
index 00000000000..3f30dc9e24f
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_child_stop_recovery_race/output.ts
@@ -0,0 +1,46 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+function transcriptIndex(transcript: ProviderReplayTranscript, label: string): number {
+ return transcript.entries.findIndex(
+ (entry) => entry.type !== "runtime_exit" && entry.label === label,
+ );
+}
+
+export function assertOpenCode2BackgroundChildStopRecoveryRaceOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const recoveryAdmitted = transcriptIndex(transcript, "recovery.input.admitted");
+ const lateCancellationAdmitted = transcriptIndex(transcript, "cancelled.parent.wake.late");
+ const recoveryStarted = transcriptIndex(transcript, "recovery.execution.started");
+ assert.isAtLeast(recoveryAdmitted, 0);
+ assert.isAtLeast(lateCancellationAdmitted, 0);
+ assert.isAtLeast(recoveryStarted, 0);
+ assert.isAbove(lateCancellationAdmitted, recoveryAdmitted);
+ assert.isAbove(recoveryStarted, lateCancellationAdmitted);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "RECOVERY_OK");
+ assert.notInclude(
+ projection.messages.map((message) => message.text).join("\n"),
+ "CANCELLED_CONTINUATION_MUST_NOT_APPEAR",
+ );
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_CONTINUATION_MUST_NOT_APPEAR");
+ assert.notInclude(JSON.stringify(projection), "CHILD_PARTIAL_SECOND");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/input.ts
new file mode 100644
index 00000000000..3a622590f8e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/input.ts
@@ -0,0 +1,10 @@
+import { OPENCODE2_BACKGROUND_STOP_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2BackgroundStopInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_BACKGROUND_STOP_PROMPT },
+ { type: "interrupt", targetRunIndex: 1, waitForTurnItemType: "command_execution" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..a8619473b45
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/opencode2_transcript.ndjson
@@ -0,0 +1,19 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_background_stop","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_background_stop","model":"opencode/big-pickle","description":"Stop after a model shell has become native background work. OpenCode 2 session interrupt leaves that shell alive by itself, so the adapter must remove the exact turn-owned shell and retain one interrupted command row."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_background_stop","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 background Stop replay","location":{"directory":"/private/tmp/t3-opencode2-background-stop"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_background_stop","prompt":{"text":"Run this shell command and wait for it: sleep 30 && echo background stop should not finish. Do not respond before it completes."}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_stop"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_stop","inputID":"input_opencode2_background_stop","input":{"id":"input_opencode2_background_stop","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_stop"}}}}
+{"type":"emit_inbound","label":"session.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_background_stop","assistantMessageID":"message_opencode2_background_stop","ordinal":0,"callID":"call_opencode2_background_stop","name":"bash"}}}}
+{"type":"emit_inbound","label":"session.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_background_stop","assistantMessageID":"message_opencode2_background_stop","ordinal":0,"callID":"call_opencode2_background_stop","input":{"command":"sleep 30 && echo background stop should not finish"}}}}}
+{"type":"emit_inbound","label":"shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_background_stop","status":"running","command":"sleep 30 && echo background stop should not finish","cwd":"/private/tmp/t3-opencode2-background-stop","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-background-stop/shell.log","pid":4245,"metadata":{"sessionID":"ses_opencode2_background_stop"},"time":{"started":1785297600100}}}}}}
+{"type":"expect_outbound","label":"session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_background_stop"}}}
+{"type":"emit_inbound","label":"session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"expect_outbound","label":"shell.remove","frame":{"type":"shell.remove","input":{"id":"shell_opencode2_background_stop","location":{"directory":"/private/tmp/t3-opencode2-background-stop"}}}}
+{"type":"emit_inbound","label":"shell.remove.response","frame":{"type":"sdk.response","operation":"shell.remove","data":true}}
+{"type":"emit_inbound","label":"shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_background_stop"}}}}
+{"type":"emit_inbound","label":"session.tool.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_background_stop","assistantMessageID":"message_opencode2_background_stop","callID":"call_opencode2_background_stop","error":{"type":"ToolExecutionError","message":"Tool execution interrupted"},"executed":true}}}}
+{"type":"emit_inbound","label":"session.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_stop","reason":"user"}}}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/output.ts
new file mode 100644
index 00000000000..64cfa678f6d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_background_stop/output.ts
@@ -0,0 +1,54 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ assertTurnItemTypes,
+ assertUserMessagesInclude,
+ assertVisibleTurnItemsMirrorLocalTurnItems,
+ OPENCODE2_BACKGROUND_STOP_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2BackgroundStopOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const shellCreatedIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "shell.created",
+ );
+ const interruptIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "session.interrupt",
+ );
+ const shellRemoveIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "shell.remove",
+ );
+ assert.isAtLeast(shellCreatedIndex, 0);
+ assert.isAbove(interruptIndex, shellCreatedIndex);
+ assert.isAbove(shellRemoveIndex, interruptIndex);
+
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["interrupted"] });
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertVisibleTurnItemsMirrorLocalTurnItems(projection);
+ assertTurnItemTypes(projection, [
+ "user_message",
+ "command_execution",
+ "run_interrupt_request",
+ "run_interrupt_result",
+ ]);
+ assertUserMessagesInclude(projection, [OPENCODE2_BACKGROUND_STOP_PROMPT]);
+
+ const commands = projection.turnItems.filter((item) => item.type === "command_execution");
+ assert.equal(commands.length, 1, "tool and shell events must retain one interrupted command row");
+ assert.equal(commands[0]?.status, "interrupted");
+ assert.include(commands[0]?.input ?? "", "background stop should not finish");
+ assert.notInclude(commands[0]?.output ?? "", "background stop should not finish");
+ assert.deepEqual(
+ projection.attempts.map((attempt) => attempt.status),
+ ["interrupted"],
+ );
+ assert.equal(projection.providerTurns[0]?.status, "interrupted");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/input.ts
new file mode 100644
index 00000000000..61a2088c427
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/input.ts
@@ -0,0 +1,15 @@
+import {
+ OPENCODE2_COMPACTION_INTERRUPT_PROMPT,
+ OPENCODE2_COMPACTION_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2CompactionInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_COMPACTION_PROMPT },
+ { type: "message", text: OPENCODE2_COMPACTION_INTERRUPT_PROMPT },
+ { type: "interrupt", targetRunIndex: 2, waitForTurnItemType: "compaction" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..b5ef83cdc6a
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/opencode2_transcript.ndjson
@@ -0,0 +1,30 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_compaction","metadata":{"source":"sdk-contract-verified-live","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_compaction","model":"opencode/big-pickle","description":"An automatic compaction streams a summary into one lifecycle row before the active turn completes, then an interrupted compaction closes its own row without leaving a spinner."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_compaction","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 compaction replay","location":{"directory":"/private/tmp/t3-opencode2-compaction-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_compaction","prompt":{"text":"Compact the current context, then respond exactly: compaction fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_compaction"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_compaction","inputID":"input_opencode2_compaction","input":{"id":"input_opencode2_compaction","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_compaction"}}}}
+{"type":"emit_inbound","label":"session.compaction.admitted","frame":{"type":"sdk.event","event":{"id":"event_compaction_admitted","created":1785297600100,"type":"session.next.compaction.started","data":{"sessionID":"ses_opencode2_compaction","inputID":"input_compaction_summary"}}}}
+{"type":"emit_inbound","label":"session.compaction.started","frame":{"type":"sdk.event","event":{"id":"event_compaction_started","created":1785297600200,"type":"session.next.compaction.started","data":{"sessionID":"ses_opencode2_compaction","reason":"auto","recent":"message_opencode2_compaction","inputID":"input_compaction_summary"}}}}
+{"type":"emit_inbound","label":"session.compaction.delta.first","frame":{"type":"sdk.event","event":{"id":"event_compaction_delta_1","created":1785297600300,"type":"session.next.compaction.delta","data":{"sessionID":"ses_opencode2_compaction","text":"Summary from "}}}}
+{"type":"emit_inbound","label":"session.compaction.delta.second","frame":{"type":"sdk.event","event":{"id":"event_compaction_delta_2","created":1785297600400,"type":"session.next.compaction.delta","data":{"sessionID":"ses_opencode2_compaction","text":"compaction."}}}}
+{"type":"emit_inbound","label":"session.compaction.ended","frame":{"type":"sdk.event","event":{"id":"event_compaction_ended","created":1785297600500,"type":"session.next.compaction.ended","data":{"sessionID":"ses_opencode2_compaction","reason":"auto","text":"Summary from compaction.","recent":"message_opencode2_compaction"}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_compaction","assistantMessageID":"message_opencode2_compaction","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_compaction","assistantMessageID":"message_opencode2_compaction","ordinal":0,"text":"compaction fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_compaction"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_compaction"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-compaction-replay"}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.interrupt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_compaction","prompt":{"text":"Begin compacting the current context and wait for it to finish."}}}}
+{"type":"emit_inbound","label":"session.prompt.interrupt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_compaction_interrupt"}}}
+{"type":"emit_inbound","label":"session.input.admitted.interrupt","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_compaction","inputID":"input_opencode2_compaction_interrupt","input":{"id":"input_opencode2_compaction_interrupt","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.interrupt","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_compaction"}}}}
+{"type":"emit_inbound","label":"session.compaction.started.interrupt","frame":{"type":"sdk.event","event":{"id":"event_compaction_started_interrupt","created":1785297600600,"type":"session.next.compaction.started","data":{"sessionID":"ses_opencode2_compaction","reason":"auto","recent":"message_opencode2_compaction","inputID":"input_compaction_interrupt"}}}}
+{"type":"emit_inbound","label":"session.compaction.delta.interrupt","frame":{"type":"sdk.event","event":{"id":"event_compaction_delta_interrupt","created":1785297600700,"type":"session.next.compaction.delta","data":{"sessionID":"ses_opencode2_compaction","text":"Partial summary"}}}}
+{"type":"expect_outbound","label":"session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_compaction"}}}
+{"type":"emit_inbound","label":"session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"emit_inbound","label":"session.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_compaction","reason":"user"}}}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/output.ts
new file mode 100644
index 00000000000..78da09d9f4f
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_compaction/output.ts
@@ -0,0 +1,56 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertConversationMessageRoles,
+ assertSemanticProjectionIntegrity,
+ assertUserMessagesInclude,
+ assertVisibleTurnItemsMirrorLocalTurnItems,
+ OPENCODE2_COMPACTION_INTERRUPT_PROMPT,
+ OPENCODE2_COMPACTION_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2CompactionOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "interrupted"],
+ });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertVisibleTurnItemsMirrorLocalTurnItems(projection);
+ assertConversationMessageRoles(projection, ["user", "assistant", "user"]);
+ assertUserMessagesInclude(projection, [
+ OPENCODE2_COMPACTION_PROMPT,
+ OPENCODE2_COMPACTION_INTERRUPT_PROMPT,
+ ]);
+ assertAssistantTextIncludes(projection, "compaction fixture complete");
+
+ const compactions = projection.turnItems.filter((item) => item.type === "compaction");
+ assert.equal(compactions.length, 2, "each lifecycle must retain one stable compaction row");
+ const completed = compactions.find((compaction) => compaction.status === "completed");
+ assert.isDefined(completed);
+ assert.equal(completed.driver, "opencode2");
+ assert.equal(completed.summary, "Summary from compaction.");
+
+ const interrupted = compactions.find((compaction) => compaction.status === "cancelled");
+ assert.isDefined(interrupted);
+ assert.equal(interrupted.driver, "opencode2");
+ assert.equal(interrupted.summary, "Partial summary");
+
+ for (const compaction of compactions) {
+ const node = projection.nodes.find((candidate) => candidate.id === compaction.nodeId);
+ assert.isDefined(node);
+ assert.equal(node.kind, "system");
+ assert.equal(node.status, compaction.status);
+ }
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/input.ts
new file mode 100644
index 00000000000..1255730b42d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/input.ts
@@ -0,0 +1,7 @@
+import { OPENCODE2_SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2PermissionExternalSubagentInput(): OrchestratorFixtureInput {
+ return {
+ steps: [{ type: "message", text: OPENCODE2_SUBAGENT_PROMPT }],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..a39368cc0f5
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/opencode2_transcript.ndjson
@@ -0,0 +1,31 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16558","scenario":"opencode2_permission_external_subagent","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_permission_external_subagent","model":"opencode/big-pickle","description":"A full-access root session spawns a native child whose legacy-named external_directory permission ask is auto-allowed through the v2 session-scoped reply route."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_permission_external_subagent","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785457422000,"updated":1785457422000},"title":"T3 OpenCode 2 external permission replay","location":{"directory":"/private/tmp/t3-opencode2-permission-external-subagent"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_permission_external_subagent","prompt":{"text":"Use the subagent tool exactly once with description child fixture and prompt Respond exactly CHILD_OK. Then respond exactly PARENT_OK."}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_permission_external_subagent"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_permission_external_subagent","inputID":"input_opencode2_permission_external_subagent","input":{"id":"input_opencode2_permission_external_subagent","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_permission_external_subagent"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_permission_external_subagent","ordinal":0,"callID":"call_opencode2_permission_external_subagent","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_permission_external_subagent","ordinal":0,"callID":"call_opencode2_permission_external_subagent","input":{"agent":"explore","description":"child fixture","prompt":"Respond exactly CHILD_OK"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_child_created_external_permission","created":1785457422800,"type":"session.created","data":{"sessionID":"ses_opencode2_child_external_permission","info":{"id":"ses_opencode2_child_external_permission","slug":"child-external-permission","projectID":"global","directory":"/private/tmp/project-a","parentID":"ses_opencode2_permission_external_subagent","title":"child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16558","time":{"created":1785457422800,"updated":1785457422800}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_child_external_permission","inputID":"input_opencode2_child_external_permission","input":{"id":"input_opencode2_child_external_permission","type":"user"}}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_child_external_permission"}}}}
+{"type":"emit_inbound","label":"child.permission.asked.external_directory","frame":{"type":"sdk.event","event":{"id":"evt_opencode2_child_external_permission","created":1785457422825,"type":"permission.asked","location":{"directory":"/private/tmp/project-a"},"data":{"id":"permission_opencode2_child_external_directory","sessionID":"ses_opencode2_child_external_permission","action":"external_directory","resources":["/private/tmp/project-b/*"],"save":["/private/tmp/project-b/*"],"source":{"type":"tool","messageID":"message_opencode2_permission_external_subagent","callID":"call_opencode2_permission_external_subagent"}}}}}
+{"type":"expect_outbound","label":"child.session.permission.reply.external_directory","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_external_permission","requestID":"permission_opencode2_child_external_directory","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.external_directory.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"child.permission.replied","frame":{"type":"sdk.event","event":{"id":"event_child_permission_replied_external_permission","created":1785457422900,"type":"permission.replied","data":{"sessionID":"ses_opencode2_child_external_permission","requestID":"permission_opencode2_child_external_directory","reply":"once"}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_child_external_permission","assistantMessageID":"message_opencode2_child_external_permission","ordinal":0}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_child_external_permission","assistantMessageID":"message_opencode2_child_external_permission","ordinal":0,"delta":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_child_external_permission","assistantMessageID":"message_opencode2_child_external_permission","ordinal":0,"text":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_child_external_permission"}}}}
+{"type":"emit_inbound","label":"subagent.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_permission_external_subagent","ordinal":0,"callID":"call_opencode2_permission_external_subagent","content":[{"type":"text","text":"CHILD_OK"}],"structured":{"sessionID":"ses_opencode2_child_external_permission"}}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_parent_external_permission","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_parent_external_permission","ordinal":1,"delta":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_permission_external_subagent","assistantMessageID":"message_opencode2_parent_external_permission","ordinal":1,"text":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_permission_external_subagent"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_permission_external_subagent"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-permission-external-subagent"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/output.ts
new file mode 100644
index 00000000000..67a0c5414e4
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_external_subagent/output.ts
@@ -0,0 +1,37 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2PermissionExternalSubagentOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ const item = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(item?.type, "subagent");
+ if (item?.type !== "subagent") throw new Error("OpenCode 2 subagent item is missing");
+ assert.strictEqual(item.status, "completed");
+ assert.isNotNull(item.childThreadId);
+ const child = result.projections.get(item.childThreadId!);
+ assert.isDefined(child);
+ assert.strictEqual(child!.thread.lineage.parentThreadId, projection.thread.id);
+ assert.strictEqual(child!.thread.lineage.relationshipToParent, "subagent");
+ assertRuntimeRequestCounts(projection, { total: 0 });
+ assertAllRuntimeRequestsResolved(projection);
+ assertRuntimeRequestCounts(child!, { total: 0 });
+ assertAssistantTextIncludes(child!, "CHILD_OK");
+ assertAssistantTextIncludes(projection, "PARENT_OK");
+ assert.strictEqual(projection.subagents[0]?.status, "completed");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/input.ts
new file mode 100644
index 00000000000..63b4de2df3d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/input.ts
@@ -0,0 +1,10 @@
+import { SIMPLE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2PermissionReplyFailureInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: SIMPLE_PROMPT },
+ { type: "approve_next_runtime_request", decision: "accept" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..cb1c8b2bf4c
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/opencode2_transcript.ndjson
@@ -0,0 +1,23 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16558","scenario":"opencode2_permission_reply_failure","metadata":{"source":"replay","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_permission_reply_failure","model":"opencode/big-pickle","description":"A failed automatic session-scoped permission reply falls back to a visible runtime approval request and completes after the user accepts it."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_permission_reply_failure","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785457423000,"updated":1785457423000},"title":"T3 OpenCode 2 permission reply failure replay","location":{"directory":"/private/tmp/t3-opencode2-permission-reply-failure"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_permission_reply_failure","prompt":{"text":"Respond with the following text: fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_permission_reply_failure"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_permission_reply_failure","inputID":"input_opencode2_permission_reply_failure","input":{"id":"input_opencode2_permission_reply_failure","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_permission_reply_failure"}}}}
+{"type":"emit_inbound","label":"permission.asked","frame":{"type":"sdk.event","event":{"id":"event_opencode2_permission_reply_failure","created":1785457423100,"type":"permission.asked","data":{"id":"permission_opencode2_permission_reply_failure","sessionID":"ses_opencode2_permission_reply_failure","permission":"bash","patterns":[],"metadata":{},"always":[]}}}}
+{"type":"expect_outbound","label":"session.permission.reply.failed","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_permission_reply_failure","requestID":"permission_opencode2_permission_reply_failure","reply":"once"}}}
+{"type":"emit_inbound","label":"session.permission.reply.failed.response","frame":{"type":"sdk.error","operation":"session.permission.reply","message":"replay permission route failed","error":{"status":404}}}
+{"type":"expect_outbound","label":"session.permission.reply.approved","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_permission_reply_failure","requestID":"permission_opencode2_permission_reply_failure","reply":"once"}}}
+{"type":"emit_inbound","label":"session.permission.reply.approved.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"permission.replied","frame":{"type":"sdk.event","event":{"id":"event_opencode2_permission_reply_failure_replied","created":1785457423200,"type":"permission.replied","data":{"sessionID":"ses_opencode2_permission_reply_failure","requestID":"permission_opencode2_permission_reply_failure","reply":"once"}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_permission_reply_failure","assistantMessageID":"message_opencode2_permission_reply_failure","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_permission_reply_failure","assistantMessageID":"message_opencode2_permission_reply_failure","ordinal":0,"delta":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_permission_reply_failure","assistantMessageID":"message_opencode2_permission_reply_failure","ordinal":0,"text":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_permission_reply_failure"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_permission_reply_failure"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-permission-reply-failure"}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/output.ts
new file mode 100644
index 00000000000..07d88b8c3d3
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure/output.ts
@@ -0,0 +1,24 @@
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2PermissionReplyFailureOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 });
+ assertAllRuntimeRequestsResolved(projection);
+ assertAssistantTextIncludes(projection, "fixture simple ok");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/input.ts
new file mode 100644
index 00000000000..32e6cbded99
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/input.ts
@@ -0,0 +1,10 @@
+import { OPENCODE2_SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2PermissionReplyFailureSubagentInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_PROMPT },
+ { type: "approve_next_runtime_request", decision: "accept" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..b8e59cdb980
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/opencode2_transcript.ndjson
@@ -0,0 +1,33 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16558","scenario":"opencode2_permission_reply_failure_subagent","metadata":{"source":"replay","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_permission_reply_failure_subagent","model":"opencode/big-pickle","description":"A failed automatic permission reply on a native child falls back to one visible runtime approval request and completes after the user accepts it."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_permission_reply_failure_subagent","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785457424000,"updated":1785457424000},"title":"T3 OpenCode 2 child permission reply failure replay","location":{"directory":"/private/tmp/t3-opencode2-permission-reply-failure-subagent"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","prompt":{"text":"Use the subagent tool exactly once with description child fixture and prompt Respond exactly CHILD_OK. Then respond exactly PARENT_OK."}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_permission_reply_failure_subagent"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","inputID":"input_opencode2_permission_reply_failure_subagent","input":{"id":"input_opencode2_permission_reply_failure_subagent","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_permission_reply_failure_subagent","ordinal":0,"callID":"call_opencode2_permission_reply_failure_subagent","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_permission_reply_failure_subagent","ordinal":0,"callID":"call_opencode2_permission_reply_failure_subagent","input":{"agent":"explore","description":"child fixture","prompt":"Respond exactly CHILD_OK"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_child_created_permission_reply_failure","created":1785457424800,"type":"session.created","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","info":{"id":"ses_opencode2_child_permission_reply_failure","slug":"child-permission-reply-failure","projectID":"global","directory":"/private/tmp/project-a","parentID":"ses_opencode2_permission_reply_failure_subagent","title":"child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16558","time":{"created":1785457424800,"updated":1785457424800}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","inputID":"input_opencode2_child_permission_reply_failure","input":{"id":"input_opencode2_child_permission_reply_failure","type":"user"}}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_child_permission_reply_failure"}}}}
+{"type":"emit_inbound","label":"child.permission.asked","frame":{"type":"sdk.event","event":{"id":"event_opencode2_child_permission_reply_failure","created":1785457424825,"type":"permission.asked","location":{"directory":"/private/tmp/project-a"},"data":{"id":"permission_opencode2_child_permission_reply_failure","sessionID":"ses_opencode2_child_permission_reply_failure","permission":"bash","patterns":[],"metadata":{},"always":[]}}}}
+{"type":"expect_outbound","label":"child.session.permission.reply.failed","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_permission_reply_failure","requestID":"permission_opencode2_child_permission_reply_failure","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.failed.response","frame":{"type":"sdk.error","operation":"session.permission.reply","message":"replay child permission route failed","error":{"status":404}}}
+{"type":"expect_outbound","label":"child.session.permission.reply.approved","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_permission_reply_failure","requestID":"permission_opencode2_child_permission_reply_failure","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.approved.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"child.permission.replied","frame":{"type":"sdk.event","event":{"id":"event_opencode2_child_permission_reply_failure_replied","created":1785457424900,"type":"permission.replied","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","requestID":"permission_opencode2_child_permission_reply_failure","reply":"once"}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","assistantMessageID":"message_opencode2_child_permission_reply_failure","ordinal":0}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","assistantMessageID":"message_opencode2_child_permission_reply_failure","ordinal":0,"delta":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_child_permission_reply_failure","assistantMessageID":"message_opencode2_child_permission_reply_failure","ordinal":0,"text":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_child_permission_reply_failure"}}}}
+{"type":"emit_inbound","label":"subagent.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_permission_reply_failure_subagent","ordinal":0,"callID":"call_opencode2_permission_reply_failure_subagent","content":[{"type":"text","text":"CHILD_OK"}],"structured":{"sessionID":"ses_opencode2_child_permission_reply_failure"}}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_parent_permission_reply_failure_subagent","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_parent_permission_reply_failure_subagent","ordinal":1,"delta":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_permission_reply_failure_subagent","assistantMessageID":"message_opencode2_parent_permission_reply_failure_subagent","ordinal":1,"text":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_permission_reply_failure_subagent"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_permission_reply_failure_subagent"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-permission-reply-failure-subagent"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/output.ts
new file mode 100644
index 00000000000..27c90e3fafc
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_reply_failure_subagent/output.ts
@@ -0,0 +1,39 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2PermissionReplyFailureSubagentOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 });
+ assertAllRuntimeRequestsResolved(projection);
+
+ const item = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(item?.type, "subagent");
+ if (item?.type !== "subagent") throw new Error("OpenCode 2 subagent item is missing");
+ assert.strictEqual(item.status, "completed");
+ assert.isNotNull(item.childThreadId);
+
+ const child = result.projections.get(item.childThreadId!);
+ assert.isDefined(child);
+ assert.strictEqual(child!.thread.lineage.parentThreadId, projection.thread.id);
+ assert.strictEqual(child!.thread.lineage.relationshipToParent, "subagent");
+ assertRuntimeRequestCounts(child!, { total: 0 });
+ assertAssistantTextIncludes(child!, "CHILD_OK");
+ assertAssistantTextIncludes(projection, "PARENT_OK");
+ assert.strictEqual(projection.subagents[0]?.status, "completed");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/input.ts
new file mode 100644
index 00000000000..c0035b32e67
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/input.ts
@@ -0,0 +1,10 @@
+import { SIMPLE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2PermissionSessionInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: SIMPLE_PROMPT },
+ { type: "approve_next_runtime_request", decision: "acceptForSession" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..594860ce06e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/opencode2_transcript.ndjson
@@ -0,0 +1,24 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_permission_session","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_permission_session","model":"opencode/big-pickle","description":"An interactive permission accepted for the T3 provider session, followed by a matching request that is answered locally without persisting an OpenCode project-wide grant."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_permission_session","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 permission replay","location":{"directory":""}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_permission_session","prompt":{"text":"Respond with the following text: fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_permission_session"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_permission_session","inputID":"input_opencode2_permission_session","input":{"id":"input_opencode2_permission_session","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_permission_session"}}}}
+{"type":"emit_inbound","label":"permission.v2.asked.first","frame":{"type":"sdk.event","event":{"type":"permission.v2.asked","data":{"id":"permission_opencode2_session_first","sessionID":"ses_opencode2_permission_session","action":"bash","resources":["command-one"],"save":["*"]}}}}
+{"type":"expect_outbound","label":"session.permission.reply.first","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_permission_session","requestID":"permission_opencode2_session_first","reply":"once"}}}
+{"type":"emit_inbound","label":"session.permission.reply.first.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"permission.v2.replied.first","frame":{"type":"sdk.event","event":{"type":"permission.v2.replied","data":{"sessionID":"ses_opencode2_permission_session","requestID":"permission_opencode2_session_first","reply":"once"}}}}
+{"type":"emit_inbound","label":"permission.v2.asked.second","frame":{"type":"sdk.event","event":{"type":"permission.v2.asked","data":{"id":"permission_opencode2_session_second","sessionID":"ses_opencode2_permission_session","action":"bash","resources":["command-two"],"save":["*"]}}}}
+{"type":"expect_outbound","label":"session.permission.reply.second","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_permission_session","requestID":"permission_opencode2_session_second","reply":"once"}}}
+{"type":"emit_inbound","label":"session.permission.reply.second.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_permission_session","assistantMessageID":"message_opencode2_permission_session","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_permission_session","assistantMessageID":"message_opencode2_permission_session","ordinal":0,"delta":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_permission_session","assistantMessageID":"message_opencode2_permission_session","ordinal":0,"text":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_permission_session"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_permission_session"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/output.ts
new file mode 100644
index 00000000000..5719bf00e70
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_permission_session/output.ts
@@ -0,0 +1,24 @@
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2PermissionSessionOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 });
+ assertAllRuntimeRequestsResolved(projection);
+ assertAssistantTextIncludes(projection, "fixture simple ok");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/input.ts
new file mode 100644
index 00000000000..68c1b8e0ef6
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/input.ts
@@ -0,0 +1,16 @@
+import { PLAN_QUESTIONS_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2QuestionLegacyInput(): OrchestratorFixtureInput {
+ return {
+ interactionMode: "plan",
+ steps: [
+ { type: "message", text: PLAN_QUESTIONS_PROMPT },
+ {
+ type: "answer_next_user_input_request",
+ answers: {
+ "question-0-schema-vs-flexibility": "Strict schemas",
+ },
+ },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..e28a7d9726b
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/opencode2_transcript.ndjson
@@ -0,0 +1,21 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16558","scenario":"opencode2_question_legacy","metadata":{"source":"replay","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_question_legacy","model":"opencode/big-pickle","description":"A legacy-named question.asked event is answered through the OpenCode 2 session question reply route."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"plan","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_question_legacy","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785457425000,"updated":1785457425000},"title":"T3 OpenCode 2 legacy question replay","location":{"directory":"/private/tmp/t3-opencode2-question-legacy"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_question_legacy","prompt":{"text":"Use request_user_input to ask one multiple-choice clarifying question about whether this fixture should prefer strict schemas or UI flexibility. After receiving the answer, respond exactly: plan questions fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_question_legacy"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_question_legacy","inputID":"input_opencode2_question_legacy","input":{"id":"input_opencode2_question_legacy","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_question_legacy"}}}}
+{"type":"emit_inbound","label":"question.asked","frame":{"type":"sdk.event","event":{"id":"event_opencode2_question_legacy","created":1785457425100,"type":"question.asked","data":{"id":"question_opencode2_question_legacy","sessionID":"ses_opencode2_question_legacy","questions":[{"question":"Should this fixture prefer strict schemas or UI flexibility?","header":"Schema vs Flexibility","options":[{"label":"Strict schemas","description":"Prioritize validation and exact structure over visual freedom."},{"label":"UI flexibility","description":"Prioritize adaptable input handling over a fixed answer."}],"multiple":false}],"tool":{"messageID":"message_opencode2_question_legacy","callID":"call_opencode2_question_legacy"}}}}}
+{"type":"expect_outbound","label":"session.question.reply","frame":{"type":"session.question.reply","input":{"sessionID":"ses_opencode2_question_legacy","requestID":"question_opencode2_question_legacy","questionV2Reply":{"answers":[["Strict schemas"]]}}}}
+{"type":"emit_inbound","label":"session.question.reply.response","frame":{"type":"sdk.response","operation":"session.question.reply","data":null}}
+{"type":"emit_inbound","label":"question.replied","frame":{"type":"sdk.event","event":{"id":"event_opencode2_question_legacy_replied","created":1785457425200,"type":"question.replied","data":{"sessionID":"ses_opencode2_question_legacy","requestID":"question_opencode2_question_legacy","answers":[["Strict schemas"]]}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_question_legacy","assistantMessageID":"message_opencode2_question_legacy","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_question_legacy","assistantMessageID":"message_opencode2_question_legacy","ordinal":0,"delta":"plan questions fixture complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_question_legacy","assistantMessageID":"message_opencode2_question_legacy","ordinal":0,"text":"plan questions fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_question_legacy"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_question_legacy"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-question-legacy"}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/output.ts
new file mode 100644
index 00000000000..d38364d04bf
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_question_legacy/output.ts
@@ -0,0 +1,32 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2QuestionLegacyOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 });
+ assertAllRuntimeRequestsResolved(projection);
+ const requestItem = projection.turnItems.find((item) => item.type === "user_input_request");
+ assert.strictEqual(requestItem?.type, "user_input_request");
+ if (requestItem?.type !== "user_input_request") {
+ throw new Error("OpenCode 2 question request item is missing");
+ }
+ assert.strictEqual(requestItem.questions[0]?.id, "question-0-schema-vs-flexibility");
+ assert.strictEqual(requestItem.status, "completed");
+ assertAssistantTextIncludes(projection, "plan questions fixture complete");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/input.ts
new file mode 100644
index 00000000000..b05d26f9fab
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/input.ts
@@ -0,0 +1,11 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2RetiredSuppressWakeInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ { type: "message", text: "Recover after retirement. Respond exactly RECOVERY_ONE" },
+ { type: "message", text: "Recover after retired cancellation. Respond exactly RECOVERY_TWO" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..440507d2c39
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/opencode2_transcript.ndjson
@@ -0,0 +1,50 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_retired_suppress_wake","metadata":{"source":"focused-provider-native-replay-ownership","description":"A never-promoted cancelled wake is retired out of ordinary fallback evidence while a second retired cancelled wake starts execution before its late promotion. The late promotion must still suppress that execution, and recovery must remain clean."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_retired_suppress_wake","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 retired suppress wake","location":{"directory":"/private/tmp/t3-opencode2-retired-suppress-wake"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_retired_suppress_wake","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_retired_suppress_wake_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_root","ordinal":0}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_root","ordinal":0,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_root","ordinal":0,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-retired-suppress-wake"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"retired.alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_alpha","input":{"type":"synthetic","data":{"text":"ALPHA_CANCELLED","description":"alpha cancelled child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"retired.bravo.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_bravo","input":{"type":"synthetic","data":{"text":"BRAVO_CANCELLED","description":"bravo cancelled child fixture"},"delivery":"queue"}}}}}
+{"type":"expect_outbound","label":"session.prompt.recovery.one","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_retired_suppress_wake","prompt":{"text":"Recover after retirement. Respond exactly RECOVERY_ONE"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.one.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_retired_suppress_wake_recovery_one"}}}
+{"type":"emit_inbound","label":"recovery.one.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_recovery_one","input":{"type":"user","data":{"text":"Recover after retirement. Respond exactly RECOVERY_ONE"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"recovery.one.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"emit_inbound","label":"recovery.one.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_one","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.one.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_one","ordinal":0,"delta":"RECOVERY_ONE"}}}}
+{"type":"emit_inbound","label":"recovery.one.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_one","ordinal":0,"text":"RECOVERY_ONE"}}}}
+{"type":"emit_inbound","label":"recovery.one.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"expect_outbound","label":"recovery.one.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}
+{"type":"emit_inbound","label":"recovery.one.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"recovery.one.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-retired-suppress-wake"}}}}
+{"type":"emit_inbound","label":"recovery.one.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"retired.bravo.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"emit_inbound","label":"retired.bravo.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_bravo"}}}}
+{"type":"emit_inbound","label":"retired.bravo.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_bravo","ordinal":0}}}}
+{"type":"emit_inbound","label":"retired.bravo.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_bravo","ordinal":0,"delta":"CANCELLED_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"retired.bravo.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_bravo","ordinal":0,"text":"CANCELLED_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"retired.bravo.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"expect_outbound","label":"session.prompt.recovery.two","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_retired_suppress_wake","prompt":{"text":"Recover after retired cancellation. Respond exactly RECOVERY_TWO"}}}}
+{"type":"emit_inbound","label":"session.prompt.recovery.two.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{}}}
+{"type":"emit_inbound","label":"recovery.two.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"emit_inbound","label":"recovery.two.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retired_suppress_wake","inputID":"input_opencode2_retired_suppress_wake_recovery_two","input":{"type":"user","data":{"text":"Recover after retired cancellation. Respond exactly RECOVERY_TWO"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"recovery.two.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_two","ordinal":0}}}}
+{"type":"emit_inbound","label":"recovery.two.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_two","ordinal":0,"delta":"RECOVERY_TWO"}}}}
+{"type":"emit_inbound","label":"recovery.two.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_retired_suppress_wake","assistantMessageID":"message_opencode2_retired_suppress_wake_recovery_two","ordinal":0,"text":"RECOVERY_TWO"}}}}
+{"type":"emit_inbound","label":"recovery.two.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_retired_suppress_wake"}}}}
+{"type":"expect_outbound","label":"recovery.two.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_retired_suppress_wake"}}}
+{"type":"emit_inbound","label":"recovery.two.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"recovery.two.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-retired-suppress-wake"}}}}
+{"type":"emit_inbound","label":"recovery.two.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/output.ts
new file mode 100644
index 00000000000..2c8ee6abc9a
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retired_suppress_wake/output.ts
@@ -0,0 +1,54 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2RetiredSuppressWakeOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const projection = projectionFor(result, transcript.scenario);
+ const recoveryOneStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.one.execution.started",
+ );
+ const wakeBPromoted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "retired.bravo.input.promoted",
+ );
+ const wakeBStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "retired.bravo.execution.started",
+ );
+ const recoveryTwoStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.two.execution.started",
+ );
+ const recoveryTwoAdmitted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "recovery.two.input.admitted",
+ );
+ assert.isAtLeast(recoveryOneStarted, 0);
+ assert.isAtLeast(wakeBPromoted, 0);
+ assert.isAtLeast(wakeBStarted, 0);
+ assert.isAtLeast(recoveryTwoStarted, 0);
+ assert.isAtLeast(recoveryTwoAdmitted, 0);
+ assert.isAbove(wakeBStarted, recoveryOneStarted);
+ assert.isAbove(wakeBPromoted, wakeBStarted);
+ assert.isAbove(recoveryTwoStarted, wakeBPromoted);
+ assert.isAbove(recoveryTwoAdmitted, recoveryTwoStarted);
+
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 3,
+ runStatuses: ["completed", "completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "RECOVERY_ONE");
+ assertAssistantTextIncludes(projection, "RECOVERY_TWO");
+ assert.notInclude(JSON.stringify(projection), "ALPHA_CANCELLED");
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_OUTPUT_MUST_NOT_APPEAR");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/input.ts
new file mode 100644
index 00000000000..82f16bf9cc2
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/input.ts
@@ -0,0 +1,7 @@
+import { OPENCODE2_RETRY_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2RetryInput(): OrchestratorFixtureInput {
+ return {
+ steps: [{ type: "message", text: OPENCODE2_RETRY_PROMPT }],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..83ddc92d69d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/opencode2_transcript.ndjson
@@ -0,0 +1,18 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_retry","metadata":{"source":"sdk-contract-replay","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_retry","model":"opencode/big-pickle","description":"A transient model failure schedules a provider-managed retry within the same execution. T3 must keep one active run and project only the eventual assistant response."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_retry","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 retry replay","location":{"directory":"/private/tmp/t3-opencode2-retry-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_retry","prompt":{"text":"Recover from the transient provider error, then respond exactly: retry fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_retry"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_retry","inputID":"input_opencode2_retry","input":{"id":"input_opencode2_retry","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_retry"}}}}
+{"type":"emit_inbound","label":"session.retry.scheduled","frame":{"type":"sdk.event","event":{"id":"event_opencode2_retry_scheduled","created":1785297600100,"type":"session.next.retried","data":{"sessionID":"ses_opencode2_retry","assistantMessageID":"message_opencode2_retry","attempt":1,"at":1785297601100,"error":{"type":"ProviderError","message":"Transient upstream capacity error"}}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_retry","assistantMessageID":"message_opencode2_retry","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_retry","assistantMessageID":"message_opencode2_retry","ordinal":0,"delta":"retry fixture complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_retry","assistantMessageID":"message_opencode2_retry","ordinal":0,"text":"retry fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_retry"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_retry"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-retry-replay"}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/output.ts
new file mode 100644
index 00000000000..be53e0b0765
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_retry/output.ts
@@ -0,0 +1,39 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertConversationMessageRoles,
+ assertSemanticProjectionIntegrity,
+ assertTurnItemTypes,
+ assertUserMessagesInclude,
+ assertVisibleTurnItemsMirrorLocalTurnItems,
+ OPENCODE2_RETRY_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2RetryOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertVisibleTurnItemsMirrorLocalTurnItems(projection);
+ assertConversationMessageRoles(projection, ["user", "assistant"]);
+ assertTurnItemTypes(projection, ["user_message", "assistant_message"]);
+ assertUserMessagesInclude(projection, [OPENCODE2_RETRY_PROMPT]);
+ assertAssistantTextIncludes(projection, "retry fixture complete");
+
+ const scheduled = transcript.entries.filter(
+ (entry) => entry.type === "emit_inbound" && entry.label === "session.retry.scheduled",
+ );
+ assert.equal(scheduled.length, 1);
+ assert.isFalse(
+ projection.turnItems.some((item) => item.type === "error"),
+ "a provider-managed retry must not become a failed T3 turn item",
+ );
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/input.ts
new file mode 100644
index 00000000000..42451ef15e9
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/input.ts
@@ -0,0 +1,14 @@
+import {
+ OPENCODE2_TWO_COMPLETED_SUBAGENT_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2SharedExecutionReplayInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_TWO_COMPLETED_SUBAGENT_PROMPT },
+ { type: "provider_continuation" },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..8d01f4d7107
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/opencode2_transcript.ndjson
@@ -0,0 +1,35 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_shared_execution_replay","metadata":{"source":"focused-provider-native-replay-ownership","description":"Two completed-child synthetic inputs are promoted into one OpenCode 2 execution. The execution output may be replayed by at most one continuation."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_shared_execution_replay","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 shared execution replay","location":{"directory":"/private/tmp/t3-opencode2-shared-execution-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_shared_execution_replay","prompt":{"text":"Start two background subagents: one with description alpha completed child fixture and prompt Respond exactly ALPHA_COMPLETED_OK, and one with description bravo completed child fixture and prompt Respond exactly BRAVO_COMPLETED_OK. Then respond exactly PARENT_RELEASED without waiting for either child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_shared_execution_replay_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_root","input":{"type":"user","data":{"text":"Start two background subagents"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shared_execution_replay"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_root","ordinal":0}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_root","ordinal":0,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_root","ordinal":0,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shared_execution_replay"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shared_execution_replay"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shared-execution-replay"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"wake.alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_alpha","input":{"type":"synthetic","data":{"text":"ALPHA_COMPLETED_OK","description":"alpha completed child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.bravo.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_bravo","input":{"type":"synthetic","data":{"text":"BRAVO_COMPLETED_OK","description":"bravo completed child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.alpha.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_alpha"}}}}
+{"type":"emit_inbound","label":"wake.bravo.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_execution_replay","inputID":"input_opencode2_shared_execution_replay_bravo"}}}}
+{"type":"emit_inbound","label":"shared.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shared_execution_replay"}}}}
+{"type":"emit_inbound","label":"shared.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_shared","ordinal":0}}}}
+{"type":"emit_inbound","label":"shared.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_shared","ordinal":0,"delta":"SHARED_EXECUTION_OUTPUT"}}}}
+{"type":"emit_inbound","label":"shared.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shared_execution_replay","assistantMessageID":"message_opencode2_shared_execution_replay_shared","ordinal":0,"text":"SHARED_EXECUTION_OUTPUT"}}}}
+{"type":"emit_inbound","label":"shared.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shared_execution_replay"}}}}
+{"type":"expect_outbound","label":"first.continuation.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shared_execution_replay"}}}
+{"type":"emit_inbound","label":"first.continuation.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"first.continuation.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shared-execution-replay"}}}}
+{"type":"emit_inbound","label":"first.continuation.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"second.continuation.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shared_execution_replay"}}}
+{"type":"emit_inbound","label":"second.continuation.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"second.continuation.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shared-execution-replay"}}}}
+{"type":"emit_inbound","label":"second.continuation.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/output.ts
new file mode 100644
index 00000000000..ec927382290
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_execution_replay/output.ts
@@ -0,0 +1,38 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+const SHARED_OUTPUT = "SHARED_EXECUTION_OUTPUT";
+
+export function assertOpenCode2SharedExecutionReplayOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 3,
+ runStatuses: ["completed", "completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, SHARED_OUTPUT);
+
+ const sharedItems = projection.turnItems.filter(
+ (item) => item.type === "assistant_message" && item.text.includes(SHARED_OUTPUT),
+ );
+ assert.lengthOf(sharedItems, 1);
+ assert.equal(sharedItems[0]?.runId, projection.runs[1]?.id);
+ assert.isFalse(
+ projection.turnItems.some(
+ (item) => item.runId === projection.runs[2]?.id && item.type === "assistant_message",
+ ),
+ );
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/input.ts
new file mode 100644
index 00000000000..2f4bc9fbca1
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/input.ts
@@ -0,0 +1,10 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2SharedOrdinaryWakeReplayInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..225e1ef0a3a
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/opencode2_transcript.ndjson
@@ -0,0 +1,24 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_shared_ordinary_wake_replay","metadata":{"source":"focused-provider-native-replay-ownership","description":"An ordinary root input is promoted before execution while one completed-child synthetic input promotes late into the same OpenCode 2 execution. The ordinary turn must retain the shared native output without duplicate or re-parented replay output."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_shared_ordinary_wake_replay","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 shared ordinary wake replay","location":{"directory":"/private/tmp/t3-opencode2-shared-ordinary-wake-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_shared_ordinary_wake_replay_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","inputID":"input_opencode2_shared_ordinary_wake_replay_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted.early","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","inputID":"input_opencode2_shared_ordinary_wake_replay_root"}}}}
+{"type":"emit_inbound","label":"wake.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","inputID":"input_opencode2_shared_ordinary_wake_replay_wake","input":{"type":"synthetic","data":{"text":"SHARED_ORDINARY_WAKE_OUTPUT","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"shared.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay"}}}}
+{"type":"emit_inbound","label":"wake.input.promoted.late","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","inputID":"input_opencode2_shared_ordinary_wake_replay_wake"}}}}
+{"type":"emit_inbound","label":"shared.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","assistantMessageID":"message_opencode2_shared_ordinary_wake_replay_shared","ordinal":0}}}}
+{"type":"emit_inbound","label":"shared.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","assistantMessageID":"message_opencode2_shared_ordinary_wake_replay_shared","ordinal":0,"delta":"SHARED_ORDINARY_WAKE_OUTPUT"}}}}
+{"type":"emit_inbound","label":"shared.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay","assistantMessageID":"message_opencode2_shared_ordinary_wake_replay_shared","ordinal":0,"text":"SHARED_ORDINARY_WAKE_OUTPUT"}}}}
+{"type":"emit_inbound","label":"shared.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shared_ordinary_wake_replay"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shared-ordinary-wake-replay"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"continuation.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shared_ordinary_wake_replay"}}}
+{"type":"emit_inbound","label":"continuation.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"continuation.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shared-ordinary-wake-replay"}}}}
+{"type":"emit_inbound","label":"continuation.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/output.ts
new file mode 100644
index 00000000000..af28f559fe1
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shared_ordinary_wake_replay/output.ts
@@ -0,0 +1,48 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+const SHARED_OUTPUT = "SHARED_ORDINARY_WAKE_OUTPUT";
+
+export function assertOpenCode2SharedOrdinaryWakeReplayOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const executionStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "shared.execution.started",
+ );
+ const rootPromoted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "root.input.promoted.early",
+ );
+ const wakePromoted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.input.promoted.late",
+ );
+ assert.isAtLeast(rootPromoted, 0);
+ assert.isAtLeast(executionStarted, 0);
+ assert.isAtLeast(wakePromoted, 0);
+ assert.isAbove(executionStarted, rootPromoted);
+ assert.isAbove(wakePromoted, executionStarted);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, SHARED_OUTPUT);
+
+ const sharedItems = projection.turnItems.filter(
+ (item) => item.type === "assistant_message" && item.text.includes(SHARED_OUTPUT),
+ );
+ assert.lengthOf(sharedItems, 1);
+ assert.equal(sharedItems[0]?.runId, projection.runs[0]?.id);
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/input.ts
new file mode 100644
index 00000000000..9a2df8f4d7d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/input.ts
@@ -0,0 +1,7 @@
+import { OPENCODE2_SHELL_PROJECTION_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2ShellProjectionInput(): OrchestratorFixtureInput {
+ return {
+ steps: [{ type: "message", text: OPENCODE2_SHELL_PROJECTION_PROMPT }],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..1e0daa80f27
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/opencode2_transcript.ndjson
@@ -0,0 +1,26 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_shell_projection","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_shell_projection","model":"opencode/big-pickle","description":"A model shell becomes provider-native background work. Tool and shell events must share one command row, settlement stays pinned while the shell runs, and terminal output is read across two byte-cursor pages."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_shell_projection","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 shell replay","location":{"directory":"/private/tmp/t3-opencode2-shell-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_shell_projection","prompt":{"text":"Run a shell command that prints the paged shell fixture output, move it to background observation, then respond exactly: shell projection fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_shell_projection"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shell_projection","inputID":"input_opencode2_shell_projection","input":{"id":"input_opencode2_shell_projection","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shell_projection"}}}}
+{"type":"emit_inbound","label":"session.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":0,"callID":"call_opencode2_shell_projection","name":"bash"}}}}
+{"type":"emit_inbound","label":"session.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":0,"callID":"call_opencode2_shell_projection","input":{"command":"printf 'shell page one shell page two'"}}}}}
+{"type":"emit_inbound","label":"shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_projection","status":"running","command":"printf 'shell page one shell page two'","cwd":"/private/tmp/t3-opencode2-shell-replay","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-shell-replay/shell.log","pid":4242,"metadata":{"sessionID":"ses_opencode2_shell_projection"},"time":{"started":1785297600100}}}}}}
+{"type":"emit_inbound","label":"session.tool.progress","frame":{"type":"sdk.event","event":{"type":"session.next.tool.progress","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":0,"callID":"call_opencode2_shell_projection","content":[{"type":"text","text":"Shell is still running."}],"structured":{}}}}}
+{"type":"emit_inbound","label":"session.tool.success.backgrounded","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":0,"callID":"call_opencode2_shell_projection","content":[{"type":"text","text":"Shell moved to background observation."}],"structured":{}}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":1}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shell_projection","assistantMessageID":"message_opencode2_shell_projection","ordinal":1,"text":"shell projection fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shell_projection"}}}}
+{"type":"emit_inbound","label":"shell.exited","frame":{"type":"sdk.event","event":{"type":"shell.exited","data":{"id":"shell_opencode2_projection","exit":0,"status":"exited"}}}}
+{"type":"expect_outbound","label":"shell.output.first","frame":{"type":"shell.output","input":{"id":"shell_opencode2_projection","location":{"directory":"/private/tmp/t3-opencode2-shell-replay"},"cursor":"0","limit":"65536"}}}
+{"type":"emit_inbound","label":"shell.output.first.response","frame":{"type":"sdk.response","operation":"shell.output","data":{"output":"shell page one ","cursor":15,"size":29,"truncated":true}}}
+{"type":"expect_outbound","label":"shell.output.second","frame":{"type":"shell.output","input":{"id":"shell_opencode2_projection","location":{"directory":"/private/tmp/t3-opencode2-shell-replay"},"cursor":"15","limit":"65536"}}}
+{"type":"emit_inbound","label":"shell.output.second.response","frame":{"type":"sdk.response","operation":"shell.output","data":{"output":"shell page two","cursor":29,"size":29,"truncated":false}}}
+{"type":"expect_outbound","label":"session.pending.list.exited","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shell_projection"}}}
+{"type":"emit_inbound","label":"session.pending.list.exited.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.exited","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shell-replay"}}}}
+{"type":"emit_inbound","label":"shell.list.exited.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/output.ts
new file mode 100644
index 00000000000..befee1a4c5d
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_projection/output.ts
@@ -0,0 +1,37 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertConversationMessageRoles,
+ assertSemanticProjectionIntegrity,
+ assertUserMessagesInclude,
+ assertVisibleTurnItemsMirrorLocalTurnItems,
+ OPENCODE2_SHELL_PROJECTION_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2ShellProjectionOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertVisibleTurnItemsMirrorLocalTurnItems(projection);
+ assertConversationMessageRoles(projection, ["user", "assistant"]);
+ assertUserMessagesInclude(projection, [OPENCODE2_SHELL_PROJECTION_PROMPT]);
+ assertAssistantTextIncludes(projection, "shell projection fixture complete");
+
+ const commands = projection.turnItems.filter((item) => item.type === "command_execution");
+ assert.equal(commands.length, 1, "the tool and shell event families must share one command row");
+ const command = commands[0];
+ assert.isDefined(command);
+ assert.equal(command.status, "completed");
+ assert.include(command.input, "printf");
+ assert.equal(command.output, "shell page one shell page two");
+ assert.equal(command.exitCode, 0);
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/input.ts
new file mode 100644
index 00000000000..9439b08a647
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/input.ts
@@ -0,0 +1,14 @@
+import {
+ OPENCODE2_SHELL_DELETION_PROMPT,
+ OPENCODE2_SHELL_FAILURE_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2ShellTerminalsInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SHELL_FAILURE_PROMPT },
+ { type: "message", text: OPENCODE2_SHELL_DELETION_PROMPT },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..bfa9a28cd06
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/opencode2_transcript.ndjson
@@ -0,0 +1,39 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_shell_terminals","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_shell_terminals","model":"opencode/big-pickle","description":"Two post-settle OpenCode 2 shells prove nonzero exit and deletion terminalize their shared command rows without leaving Waiting pinned."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_shell_terminals","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 shell terminal replay","location":{"directory":"/private/tmp/t3-opencode2-shell-terminals"}}}}
+{"type":"expect_outbound","label":"session.prompt.failure","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_shell_terminals","prompt":{"text":"Run a shell command that exits with status 7, move it to background observation, then respond exactly: shell failure fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.failure.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_shell_failure"}}}
+{"type":"emit_inbound","label":"session.input.admitted.failure","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shell_terminals","inputID":"input_opencode2_shell_failure","input":{"id":"input_opencode2_shell_failure","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.failure","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shell_terminals"}}}}
+{"type":"emit_inbound","label":"session.tool.input.started.failure","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_failure","ordinal":0,"callID":"call_opencode2_shell_failure","name":"bash"}}}}
+{"type":"emit_inbound","label":"session.tool.called.failure","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_failure","ordinal":0,"callID":"call_opencode2_shell_failure","input":{"command":"printf 'shell failed'; exit 7"}}}}}
+{"type":"emit_inbound","label":"shell.created.failure","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_failure","status":"running","command":"printf 'shell failed'; exit 7","cwd":"/private/tmp/t3-opencode2-shell-terminals","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-shell-terminals/failure.log","pid":4243,"metadata":{"sessionID":"ses_opencode2_shell_terminals"},"time":{"started":1785297600100}}}}}}
+{"type":"emit_inbound","label":"session.tool.success.failure-backgrounded","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_failure","ordinal":0,"callID":"call_opencode2_shell_failure","content":[{"type":"text","text":"Shell moved to background observation."}],"structured":{}}}}}
+{"type":"emit_inbound","label":"session.text.started.failure","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_failure","ordinal":1}}}}
+{"type":"emit_inbound","label":"session.text.ended.failure","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_failure","ordinal":1,"text":"shell failure fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.failure","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shell_terminals"}}}}
+{"type":"emit_inbound","label":"shell.exited.failure","frame":{"type":"sdk.event","event":{"type":"shell.exited","data":{"id":"shell_opencode2_failure","exit":7,"status":"exited"}}}}
+{"type":"expect_outbound","label":"shell.output.failure","frame":{"type":"shell.output","input":{"id":"shell_opencode2_failure","location":{"directory":"/private/tmp/t3-opencode2-shell-terminals"},"cursor":"0","limit":"65536"}}}
+{"type":"emit_inbound","label":"shell.output.failure.response","frame":{"type":"sdk.response","operation":"shell.output","data":{"output":"shell failed","cursor":12,"size":12,"truncated":false}}}
+{"type":"expect_outbound","label":"session.pending.list.failure","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shell_terminals"}}}
+{"type":"emit_inbound","label":"session.pending.list.failure.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.failure","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shell-terminals"}}}}
+{"type":"emit_inbound","label":"shell.list.failure.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.deletion","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_shell_terminals","prompt":{"text":"Run a long shell command, move it to background observation, then respond exactly: shell deletion fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.deletion.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_shell_deletion"}}}
+{"type":"emit_inbound","label":"session.input.admitted.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_shell_terminals","inputID":"input_opencode2_shell_deletion","input":{"id":"input_opencode2_shell_deletion","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_shell_terminals"}}}}
+{"type":"emit_inbound","label":"session.tool.input.started.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_deletion","ordinal":0,"callID":"call_opencode2_shell_deletion","name":"bash"}}}}
+{"type":"emit_inbound","label":"session.tool.called.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_deletion","ordinal":0,"callID":"call_opencode2_shell_deletion","input":{"command":"sleep 60"}}}}}
+{"type":"emit_inbound","label":"shell.created.deletion","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_deletion","status":"running","command":"sleep 60","cwd":"/private/tmp/t3-opencode2-shell-terminals","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-shell-terminals/deletion.log","pid":4244,"metadata":{"sessionID":"ses_opencode2_shell_terminals"},"time":{"started":1785297600200}}}}}}
+{"type":"emit_inbound","label":"session.tool.success.deletion-backgrounded","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_deletion","ordinal":0,"callID":"call_opencode2_shell_deletion","content":[{"type":"text","text":"Shell moved to background observation."}],"structured":{}}}}}
+{"type":"emit_inbound","label":"session.text.started.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_deletion","ordinal":1}}}}
+{"type":"emit_inbound","label":"session.text.ended.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_shell_terminals","assistantMessageID":"message_opencode2_shell_deletion","ordinal":1,"text":"shell deletion fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.deletion","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_shell_terminals"}}}}
+{"type":"emit_inbound","label":"shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_deletion"}}}}
+{"type":"expect_outbound","label":"session.pending.list.deletion","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_shell_terminals"}}}
+{"type":"emit_inbound","label":"session.pending.list.deletion.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.deletion","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-shell-terminals"}}}}
+{"type":"emit_inbound","label":"shell.list.deletion.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/output.ts
new file mode 100644
index 00000000000..e646bbc39a1
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_shell_terminals/output.ts
@@ -0,0 +1,52 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertConversationMessageRoles,
+ assertSemanticProjectionIntegrity,
+ assertUserMessagesInclude,
+ assertVisibleTurnItemsMirrorLocalTurnItems,
+ OPENCODE2_SHELL_DELETION_PROMPT,
+ OPENCODE2_SHELL_FAILURE_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2ShellTerminalsOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertVisibleTurnItemsMirrorLocalTurnItems(projection);
+ assertConversationMessageRoles(projection, ["user", "assistant", "user", "assistant"]);
+ assertUserMessagesInclude(projection, [
+ OPENCODE2_SHELL_FAILURE_PROMPT,
+ OPENCODE2_SHELL_DELETION_PROMPT,
+ ]);
+ assertAssistantTextIncludes(projection, "shell failure fixture complete");
+ assertAssistantTextIncludes(projection, "shell deletion fixture complete");
+
+ const commands = projection.turnItems.filter((item) => item.type === "command_execution");
+ assert.equal(commands.length, 2, "each tool and shell event pair must share one command row");
+
+ const failed = commands.find((command) => command.input.includes("exit 7"));
+ assert.isDefined(failed);
+ assert.equal(failed.status, "failed");
+ assert.equal(failed.output, "shell failed");
+ assert.equal(failed.exitCode, 7);
+
+ const deleted = commands.find((command) => command.input.includes("sleep 60"));
+ assert.isDefined(deleted);
+ assert.equal(deleted.status, "failed");
+ assert.isUndefined(deleted.exitCode);
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/input.ts
new file mode 100644
index 00000000000..7b7ed3551fa
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/input.ts
@@ -0,0 +1,10 @@
+import { OPENCODE2_SUBAGENT_BACKGROUND_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2SubagentBackgroundWakeInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..82523c4908f
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/opencode2_transcript.ndjson
@@ -0,0 +1,41 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_subagent_background_wake","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_subagent_background_wake","model":"opencode/big-pickle","description":"A background child finishes after the root execution settles. OpenCode admits a synthetic parent input and completes a native wake execution before T3 attaches its provider-buffered continuation run."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_subagent_background_wake","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 background wake replay","location":{"directory":"/private/tmp/t3-opencode2-background-wake"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_subagent_background_wake","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_background_wake_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_background_wake","inputID":"input_opencode2_background_wake_root","input":{"type":"user","data":{"text":"Start one background subagent"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_background_wake","inputID":"input_opencode2_background_wake_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_background_wake"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":0,"callID":"call_opencode2_background_wake","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":0,"callID":"call_opencode2_background_wake","input":{"agent":"explore","background":true,"description":"background child fixture","prompt":"Respond exactly CHILD_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"subagent.launch.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":0,"callID":"call_opencode2_background_wake","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_background_wake_child"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_background_wake_child","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_background_wake_child","info":{"id":"ses_opencode2_background_wake_child","slug":"child-background-wake","projectID":"global","directory":"/private/tmp/t3-opencode2-background-wake","parentID":"ses_opencode2_subagent_background_wake","title":"background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_wake_child","inputID":"input_opencode2_background_wake_child","input":{"type":"user","data":{"text":"Respond exactly CHILD_BACKGROUND_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"child.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_background_wake_child","inputID":"input_opencode2_background_wake_child"}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_background_wake_child"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":1,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_root","ordinal":1,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_background_wake"}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_background_wake_child","assistantMessageID":"message_opencode2_background_wake_child","ordinal":0}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_background_wake_child","assistantMessageID":"message_opencode2_background_wake_child","ordinal":0,"delta":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_background_wake_child","assistantMessageID":"message_opencode2_background_wake_child","ordinal":0,"text":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"child.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_background_wake_child"}}}}
+{"type":"expect_outbound","label":"root.pending.list.first","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_background_wake"}}}
+{"type":"emit_inbound","label":"root.pending.list.first.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.first","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-wake"}}}}
+{"type":"emit_inbound","label":"shell.list.first.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"cancelled.wake.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_background_wake","inputID":"input_opencode2_background_wake_cancelled","input":{"type":"synthetic","data":{"text":"CHILD_CANCELLED_SHOULD_NOT_CONTINUE","description":"cancelled background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_background_wake","inputID":"input_opencode2_background_wake_synthetic","input":{"type":"synthetic","data":{"text":"CHILD_BACKGROUND_OK","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_background_wake","inputID":"input_opencode2_background_wake_synthetic"}}}}
+{"type":"emit_inbound","label":"wake.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_background_wake"}}}}
+{"type":"emit_inbound","label":"wake.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_synthetic","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_synthetic","ordinal":0,"delta":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_background_wake","assistantMessageID":"message_opencode2_background_wake_synthetic","ordinal":0,"text":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_background_wake"}}}}
+{"type":"expect_outbound","label":"root.pending.list.continuation","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_background_wake"}}}
+{"type":"emit_inbound","label":"root.pending.list.continuation.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.continuation","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-background-wake"}}}}
+{"type":"emit_inbound","label":"shell.list.continuation.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/output.ts
new file mode 100644
index 00000000000..db84ca11d38
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_background_wake/output.ts
@@ -0,0 +1,48 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRunOrdinals,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2SubagentBackgroundWakeOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRunOrdinals(projection, [1, 2]);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "CHILD_BACKGROUND_OK");
+ assert.notInclude(JSON.stringify(projection), "CHILD_CANCELLED_SHOULD_NOT_CONTINUE");
+
+ const subagentItem = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(subagentItem?.type, "subagent");
+ if (subagentItem?.type !== "subagent") {
+ throw new Error("OpenCode 2 background subagent item is missing");
+ }
+ assert.strictEqual(subagentItem.status, "completed");
+ assert.isNotNull(subagentItem.childThreadId);
+ const child = result.projections.get(subagentItem.childThreadId!);
+ assert.isDefined(child);
+ assertAssistantTextIncludes(child!, "CHILD_BACKGROUND_OK");
+
+ const continuation = projection.runs[1];
+ assert.isDefined(continuation);
+ const continuationItems = projection.turnItems.filter(
+ (item) => item.runId === continuation!.id && item.type === "assistant_message",
+ );
+ assert.lengthOf(continuationItems, 1);
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/input.ts
new file mode 100644
index 00000000000..2ac79304982
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/input.ts
@@ -0,0 +1,15 @@
+import {
+ MULTI_TURN_SECOND_PROMPT,
+ OPENCODE2_SUBAGENT_BACKGROUND_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2SubagentQueuedTurnInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_BACKGROUND_PROMPT },
+ { type: "queue_message", text: MULTI_TURN_SECOND_PROMPT },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..afb5db878f0
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/opencode2_transcript.ndjson
@@ -0,0 +1,52 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_subagent_queued_turn","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_subagent_queued_turn","model":"opencode/big-pickle","description":"The parent settles while its native child remains active. T3 promotes one queued user turn after the child terminalizes, buffers the synthetic OpenCode parent wake that races ahead of that user's native execution, and attaches the wake exactly once afterward."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_subagent_queued_turn","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785391200000,"updated":1785391200000},"title":"T3 OpenCode 2 background child queue replay","location":{"directory":"/private/tmp/t3-opencode2-subagent-queue"}}}}
+{"type":"expect_outbound","label":"session.prompt.first","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_subagent_queued_turn","prompt":{"text":"Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child."}}}}
+{"type":"emit_inbound","label":"session.prompt.first.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_subagent_queue_first"}}}
+{"type":"emit_inbound","label":"root.input.admitted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_queued_turn","inputID":"input_opencode2_subagent_queue_first","input":{"id":"input_opencode2_subagent_queue_first","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue","ordinal":0,"callID":"call_opencode2_subagent_queue","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue","ordinal":0,"callID":"call_opencode2_subagent_queue","input":{"agent":"explore","background":true,"description":"background child fixture","prompt":"Respond exactly CHILD_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"subagent.launch.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue","ordinal":0,"callID":"call_opencode2_subagent_queue","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_child_queue"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_child_created_queue","created":1785391200100,"type":"session.created","data":{"sessionID":"ses_opencode2_child_queue","info":{"id":"ses_opencode2_child_queue","slug":"child-queue","projectID":"global","directory":"/private/tmp/t3-opencode2-subagent-queue","parentID":"ses_opencode2_subagent_queued_turn","title":"background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785391200100,"updated":1785391200100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_child_queue","inputID":"input_opencode2_child_queue","input":{"id":"input_opencode2_child_queue","type":"user"}}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_child_queue"}}}}
+{"type":"emit_inbound","label":"root.text.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_parent_queue","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_parent_queue","ordinal":1,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_parent_queue","ordinal":1,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"emit_inbound","label":"child.permission.asked","frame":{"type":"sdk.event","event":{"type":"permission.asked","data":{"id":"permission_opencode2_child_queue","sessionID":"ses_opencode2_child_queue","permission":"bash","patterns":["pwd"],"metadata":{"command":"pwd"},"always":[],"tool":{"messageID":"message_opencode2_child_queue","callID":"call_opencode2_child_queue"}}}}}
+{"type":"expect_outbound","label":"child.session.permission.reply","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_queue","requestID":"permission_opencode2_child_queue","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_child_queue","assistantMessageID":"message_opencode2_child_queue","ordinal":0}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_child_queue","assistantMessageID":"message_opencode2_child_queue","ordinal":0,"delta":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_child_queue","assistantMessageID":"message_opencode2_child_queue","ordinal":0,"text":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"child.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_child_queue"}}}}
+{"type":"expect_outbound","label":"root.pending.list.first","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}
+{"type":"emit_inbound","label":"root.pending.list.first.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.first","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-subagent-queue"}}}}
+{"type":"emit_inbound","label":"shell.list.first.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.second","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_subagent_queued_turn","prompt":{"text":"Respond with exactly: second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.second.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_subagent_queue_second"}}}
+{"type":"emit_inbound","label":"wake.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_queued_turn","inputID":"input_opencode2_subagent_queue_wake","input":{"type":"synthetic","data":{"text":"CHILD_BACKGROUND_OK","description":"background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"emit_inbound","label":"wake.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue_wake","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue_wake","ordinal":0,"delta":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_subagent_queue_wake","ordinal":0,"text":"CHILD_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"emit_inbound","label":"root.input.admitted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_queued_turn","inputID":"input_opencode2_subagent_queue_second","input":{"id":"input_opencode2_subagent_queue_second","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"emit_inbound","label":"root.text.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0}}}}
+{"type":"emit_inbound","label":"root.text.delta.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"delta":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"root.text.ended.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"text":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_queued_turn"}}}}
+{"type":"expect_outbound","label":"root.pending.list.second","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}
+{"type":"emit_inbound","label":"root.pending.list.second.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.second","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-subagent-queue"}}}}
+{"type":"emit_inbound","label":"shell.list.second.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"root.pending.list.continuation","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_queued_turn"}}}
+{"type":"emit_inbound","label":"root.pending.list.continuation.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.continuation","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-subagent-queue"}}}}
+{"type":"emit_inbound","label":"shell.list.continuation.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/output.ts
new file mode 100644
index 00000000000..467afe2e465
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_queued_turn/output.ts
@@ -0,0 +1,84 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRunOrdinals,
+ assertSemanticProjectionIntegrity,
+ assertUserMessagesInclude,
+ MULTI_TURN_SECOND_PROMPT,
+ OPENCODE2_SUBAGENT_BACKGROUND_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2SubagentQueuedTurnOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 3,
+ runStatuses: ["completed", "completed", "completed"],
+ });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertRunOrdinals(projection, [1, 2, 3]);
+ assertUserMessagesInclude(projection, [
+ OPENCODE2_SUBAGENT_BACKGROUND_PROMPT,
+ MULTI_TURN_SECOND_PROMPT,
+ ]);
+ const subagentItem = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(subagentItem?.type, "subagent");
+ if (subagentItem?.type !== "subagent") {
+ throw new Error("OpenCode 2 background subagent item is missing");
+ }
+ assert.strictEqual(subagentItem.status, "completed");
+ assert.isNotNull(subagentItem.childThreadId);
+ const child = result.projections.get(subagentItem.childThreadId!);
+ assert.isDefined(child);
+ assertAssistantTextIncludes(child!, "CHILD_BACKGROUND_OK");
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "second fixture turn complete");
+ assertAssistantTextIncludes(projection, "CHILD_BACKGROUND_OK");
+
+ const subagentEvents = result.domainEvents.filter((event) => event.type === "subagent.updated");
+ const firstTerminal = subagentEvents.findIndex((event) =>
+ ["completed", "failed", "cancelled", "interrupted"].includes(event.payload.status),
+ );
+ assert.isAtLeast(
+ firstTerminal,
+ 1,
+ "the native launch acknowledgement must precede terminal child status",
+ );
+ assert.isTrue(
+ subagentEvents
+ .slice(0, firstTerminal)
+ .every((event) => event.payload.status === "pending" || event.payload.status === "running"),
+ "the native launch acknowledgement must not terminalize the linked child",
+ );
+ assert.strictEqual(subagentEvents[firstTerminal]?.payload.status, "completed");
+
+ const secondRun = projection.runs[1];
+ assert.isDefined(secondRun);
+ const secondRunEvents = result.domainEvents
+ .filter((event) => event.type === "run.created" || event.type === "run.updated")
+ .filter((event) => event.runId === secondRun!.id);
+ assert.equal(secondRunEvents[0]?.type, "run.created");
+ assert.equal(secondRunEvents[0]?.payload.status, "queued");
+ assert.equal(
+ secondRunEvents.filter((event) => event.payload.status === "running").length,
+ 1,
+ "queued run should promote exactly once after the child settles",
+ );
+
+ const continuation = projection.runs[2];
+ assert.isDefined(continuation);
+ const continuationAssistantItems = projection.turnItems.filter(
+ (item) => item.runId === continuation!.id && item.type === "assistant_message",
+ );
+ assert.lengthOf(continuationAssistantItems, 1);
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/input.ts
new file mode 100644
index 00000000000..0596efd56e3
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/input.ts
@@ -0,0 +1,7 @@
+import { OPENCODE2_SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2SubagentRateLimitInput(): OrchestratorFixtureInput {
+ return {
+ steps: [{ type: "message", text: OPENCODE2_SUBAGENT_PROMPT }],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..70a47b78f2a
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/opencode2_transcript.ndjson
@@ -0,0 +1,28 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_subagent_rate_limit","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_subagent_rate_limit","model":"opencode/big-pickle","description":"A Full access native child automatically receives a legacy permission reply, exhausts provider-managed HTTP 429 retries, and exposes the terminal rate limit on the child and parent subagent row."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_subagent_rate_limit","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785387600000,"updated":1785387600000},"title":"T3 OpenCode 2 rate-limit subagent replay","location":{"directory":"/private/tmp/t3-opencode2-subagent-rate-limit"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_subagent_rate_limit","prompt":{"text":"Use the subagent tool exactly once with description child fixture and prompt Respond exactly CHILD_OK. Then respond exactly PARENT_OK."}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_subagent_rate_limit"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_rate_limit","inputID":"input_opencode2_subagent_rate_limit","input":{"id":"input_opencode2_subagent_rate_limit","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_rate_limit"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_subagent_rate_limit","ordinal":0,"callID":"call_opencode2_subagent_rate_limit","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_subagent_rate_limit","ordinal":0,"callID":"call_opencode2_subagent_rate_limit","input":{"agent":"explore","description":"child fixture","prompt":"Respond exactly CHILD_OK"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_child_created_rate_limit","created":1785387600100,"type":"session.created","data":{"sessionID":"ses_opencode2_child_rate_limit","info":{"id":"ses_opencode2_child_rate_limit","slug":"child-rate-limit","projectID":"global","directory":"/private/tmp/t3-opencode2-subagent-rate-limit","parentID":"ses_opencode2_subagent_rate_limit","title":"child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785387600100,"updated":1785387600100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_child_rate_limit","inputID":"input_opencode2_child_rate_limit","input":{"id":"input_opencode2_child_rate_limit","type":"user"}}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_child_rate_limit"}}}}
+{"type":"emit_inbound","label":"child.permission.asked","frame":{"type":"sdk.event","event":{"id":"event_child_permission_rate_limit","created":1785387600200,"type":"permission.asked","data":{"id":"permission_opencode2_child_rate_limit","sessionID":"ses_opencode2_child_rate_limit","permission":"read","patterns":["package.json"],"metadata":{},"always":["*"]}}}}
+{"type":"expect_outbound","label":"child.session.permission.reply","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_rate_limit","requestID":"permission_opencode2_child_rate_limit","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"child.retry.scheduled","frame":{"type":"sdk.event","event":{"id":"event_child_retry_rate_limit","created":1785387600300,"type":"session.next.retried","data":{"sessionID":"ses_opencode2_child_rate_limit","assistantMessageID":"message_opencode2_child_rate_limit","attempt":5,"at":1785387616300,"error":{"type":"provider.rate-limit","message":"HTTP 429: Rate limit exceeded"}}}}}
+{"type":"emit_inbound","label":"child.execution.failed","frame":{"type":"sdk.event","event":{"id":"event_child_failed_rate_limit","created":1785387616300,"type":"session.next.step.failed","data":{"sessionID":"ses_opencode2_child_rate_limit","error":{"type":"provider.rate-limit","message":"HTTP 429: Rate limit exceeded"}}}}}
+{"type":"emit_inbound","label":"subagent.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_subagent_rate_limit","callID":"call_opencode2_subagent_rate_limit","error":{"type":"provider.rate-limit","message":"HTTP 429: Rate limit exceeded"},"executed":true}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_parent_rate_limit","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_parent_rate_limit","ordinal":1,"delta":"PARENT_AFTER_429"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_rate_limit","assistantMessageID":"message_opencode2_parent_rate_limit","ordinal":1,"text":"PARENT_AFTER_429"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_rate_limit"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_rate_limit"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-subagent-rate-limit"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/output.ts
new file mode 100644
index 00000000000..3e987e8d39e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_rate_limit/output.ts
@@ -0,0 +1,37 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2SubagentRateLimitOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ const item = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(item?.type, "subagent");
+ if (item?.type !== "subagent") throw new Error("OpenCode 2 subagent item is missing");
+ assert.strictEqual(item.status, "failed");
+ assert.match(item.result ?? "", /HTTP 429/);
+ assert.isNotNull(item.childThreadId);
+ const child = result.projections.get(item.childThreadId!);
+ assert.isDefined(child);
+ const failure = child!.turnItems.find((candidate) => candidate.type === "error");
+ assert.strictEqual(failure?.type, "error");
+ if (failure?.type !== "error") throw new Error("OpenCode 2 child failure item is missing");
+ assert.strictEqual(failure.status, "failed");
+ assert.strictEqual(failure.failure.code, "provider.rate-limit");
+ assert.strictEqual(failure.failure.retryable, true);
+ assert.strictEqual(failure.retry?.attempt, 5);
+ assertAssistantTextIncludes(projection, "PARENT_AFTER_429");
+ assert.strictEqual(projection.subagents[0]?.status, "failed");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/input.ts
new file mode 100644
index 00000000000..4c3006a2913
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/input.ts
@@ -0,0 +1,10 @@
+import { OPENCODE2_SUBAGENT_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2SubagentSupervisedInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_SUBAGENT_PROMPT },
+ { type: "approve_next_runtime_request", decision: "accept" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..e085872d8b9
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/opencode2_transcript.ndjson
@@ -0,0 +1,31 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_subagent_supervised","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_subagent_supervised","model":"opencode/big-pickle","description":"A native child session is linked through parentID, emits a legacy permission request under Supervised mode, resumes after Allow once, and completes with durable lineage."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_subagent_supervised","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785384000000,"updated":1785384000000},"title":"T3 OpenCode 2 supervised subagent replay","location":{"directory":"/private/tmp/t3-opencode2-subagent-supervised"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_subagent_supervised","prompt":{"text":"Use the subagent tool exactly once with description child fixture and prompt Respond exactly CHILD_OK. Then respond exactly PARENT_OK."}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_subagent_supervised"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_subagent_supervised","inputID":"input_opencode2_subagent_supervised","input":{"id":"input_opencode2_subagent_supervised","type":"user"}}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_subagent_supervised"}}}}
+{"type":"emit_inbound","label":"subagent.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_subagent_supervised","ordinal":0,"callID":"call_opencode2_subagent_supervised","name":"subagent"}}}}
+{"type":"emit_inbound","label":"subagent.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_subagent_supervised","ordinal":0,"callID":"call_opencode2_subagent_supervised","input":{"agent":"explore","description":"child fixture","prompt":"Respond exactly CHILD_OK"}}}}}
+{"type":"emit_inbound","label":"child.session.created","frame":{"type":"sdk.event","event":{"id":"event_child_created_supervised","created":1785384000100,"type":"session.created","data":{"sessionID":"ses_opencode2_child_supervised","info":{"id":"ses_opencode2_child_supervised","slug":"child-supervised","projectID":"global","directory":"/private/tmp/t3-opencode2-subagent-supervised","parentID":"ses_opencode2_subagent_supervised","title":"child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785384000100,"updated":1785384000100}}}}}}
+{"type":"emit_inbound","label":"child.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_child_supervised","inputID":"input_opencode2_child_supervised","input":{"id":"input_opencode2_child_supervised","type":"user"}}}}}
+{"type":"emit_inbound","label":"child.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_child_supervised"}}}}
+{"type":"emit_inbound","label":"child.permission.asked","frame":{"type":"sdk.event","event":{"id":"event_child_permission_supervised","created":1785384000200,"type":"permission.asked","data":{"id":"permission_opencode2_child_supervised","sessionID":"ses_opencode2_child_supervised","permission":"bash","patterns":["pwd"],"metadata":{},"always":["*"]}}}}
+{"type":"expect_outbound","label":"child.session.permission.reply","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_child_supervised","requestID":"permission_opencode2_child_supervised","reply":"once"}}}
+{"type":"emit_inbound","label":"child.session.permission.reply.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"child.permission.replied","frame":{"type":"sdk.event","event":{"id":"event_child_permission_replied_supervised","created":1785384000300,"type":"permission.replied","data":{"sessionID":"ses_opencode2_child_supervised","requestID":"permission_opencode2_child_supervised","reply":"once"}}}}
+{"type":"emit_inbound","label":"child.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_child_supervised","assistantMessageID":"message_opencode2_child_supervised","ordinal":0}}}}
+{"type":"emit_inbound","label":"child.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_child_supervised","assistantMessageID":"message_opencode2_child_supervised","ordinal":0,"delta":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_child_supervised","assistantMessageID":"message_opencode2_child_supervised","ordinal":0,"text":"CHILD_OK"}}}}
+{"type":"emit_inbound","label":"child.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_child_supervised"}}}}
+{"type":"emit_inbound","label":"subagent.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_subagent_supervised","ordinal":0,"callID":"call_opencode2_subagent_supervised","content":[{"type":"text","text":"CHILD_OK"}],"structured":{"sessionID":"ses_opencode2_child_supervised"}}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_parent_supervised","ordinal":1}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_parent_supervised","ordinal":1,"delta":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_subagent_supervised","assistantMessageID":"message_opencode2_parent_supervised","ordinal":1,"text":"PARENT_OK"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_subagent_supervised"}}}}
+{"type":"expect_outbound","label":"root.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_subagent_supervised"}}}
+{"type":"emit_inbound","label":"root.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-subagent-supervised"}}}}
+{"type":"emit_inbound","label":"root.shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/output.ts
new file mode 100644
index 00000000000..b73a527d632
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_subagent_supervised/output.ts
@@ -0,0 +1,37 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAllRuntimeRequestsResolved,
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertRuntimeRequestCounts,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2SubagentSupervisedOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ const item = projection.turnItems.find((candidate) => candidate.type === "subagent");
+ assert.strictEqual(item?.type, "subagent");
+ if (item?.type !== "subagent") throw new Error("OpenCode 2 subagent item is missing");
+ assert.strictEqual(item.status, "completed");
+ assert.isNotNull(item.childThreadId);
+ const child = result.projections.get(item.childThreadId!);
+ assert.isDefined(child);
+ assert.strictEqual(child!.thread.lineage.parentThreadId, projection.thread.id);
+ assert.strictEqual(child!.thread.lineage.relationshipToParent, "subagent");
+ assertRuntimeRequestCounts(projection, { total: 1, resolved: 1 });
+ assertAllRuntimeRequestsResolved(projection);
+ assertRuntimeRequestCounts(child!, { total: 0 });
+ assertAssistantTextIncludes(child!, "CHILD_OK");
+ assertAssistantTextIncludes(projection, "PARENT_OK");
+ assert.strictEqual(projection.subagents[0]?.status, "completed");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/input.ts
new file mode 100644
index 00000000000..a6cf8c739fa
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/input.ts
@@ -0,0 +1,11 @@
+import { OPENCODE2_THREAD_DELETE_PROMPT, type OrchestratorFixtureInput } from "../shared.ts";
+
+export function openCode2ThreadDeleteInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_THREAD_DELETE_PROMPT },
+ { type: "advance_clock", duration: "31 minutes" },
+ { type: "delete" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..381efe9e5c0
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/opencode2_transcript.ndjson
@@ -0,0 +1,22 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"opencode2_thread_delete","metadata":{"source":"sdk-contract-replay","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_thread_delete","model":"opencode/big-pickle","description":"Deleting the application thread removes its active provider-native OpenCode 2 session before the exclusive runtime detaches."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_thread_delete","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 delete replay","location":{"directory":"/private/tmp/t3-opencode2-delete-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_thread_delete","prompt":{"text":"Respond exactly: native deletion fixture complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_thread_delete"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_thread_delete","inputID":"input_opencode2_thread_delete","input":{"id":"input_opencode2_thread_delete","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_thread_delete"}}}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_thread_delete","assistantMessageID":"message_opencode2_thread_delete","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_thread_delete","assistantMessageID":"message_opencode2_thread_delete","ordinal":0,"text":"native deletion fixture complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_thread_delete"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_thread_delete"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-delete-replay"}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.pending.list.idle-release","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_thread_delete"}}}
+{"type":"emit_inbound","label":"session.pending.list.idle-release.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.idle-release","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-delete-replay"}}}}
+{"type":"emit_inbound","label":"shell.list.idle-release.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.remove","frame":{"type":"session.remove","input":{"sessionID":"ses_opencode2_thread_delete"}}}
+{"type":"emit_inbound","label":"session.remove.response","frame":{"type":"sdk.response","operation":"session.remove","data":true}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/output.ts
new file mode 100644
index 00000000000..52509f8e0b7
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_thread_delete/output.ts
@@ -0,0 +1,48 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ assertUserMessagesInclude,
+ OPENCODE2_THREAD_DELETE_PROMPT,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2ThreadDeleteOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertSemanticProjectionIntegrity(projection);
+ assertUserMessagesInclude(projection, [OPENCODE2_THREAD_DELETE_PROMPT]);
+ assertAssistantTextIncludes(projection, "native deletion fixture complete");
+ assert.isNotNull(projection.thread.deletedAt);
+ const stoppedIndex = result.domainEvents.findIndex(
+ (event) => event.type === "provider-session.updated" && event.payload.status === "stopped",
+ );
+ const detachedIndex = result.domainEvents.findIndex(
+ (event) =>
+ event.type === "provider-session.detached" && event.threadId === projection.thread.id,
+ );
+ assert.isAtLeast(
+ stoppedIndex,
+ 0,
+ "the fixture's idle release must stop the managed runtime before application deletion",
+ );
+ assert.isAbove(
+ detachedIndex,
+ stoppedIndex,
+ "the fixture must detach the provider session after its idle release stops the runtime",
+ );
+ assert.isFalse(result.shellSnapshot.threads.some((thread) => thread.id === projection.thread.id));
+
+ const removeIndex = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "session.remove",
+ );
+ assert.isAtLeast(removeIndex, 0, "application deletion must remove the native session");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/input.ts
new file mode 100644
index 00000000000..f2bcc47918e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/input.ts
@@ -0,0 +1,14 @@
+import {
+ OPENCODE2_TWO_COMPLETED_SUBAGENT_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2TwoBackgroundChildReplayInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_TWO_COMPLETED_SUBAGENT_PROMPT },
+ { type: "provider_continuation" },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..d7c7474f5f6
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/opencode2_transcript.ndjson
@@ -0,0 +1,62 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_two_background_child_replay","metadata":{"source":"focused-provider-native-stop","description":"Two completed OpenCode 2 background children are promoted into distinct native executions. Wake A settles, wake B starts, and the first continuation replays before B completes; the second continuation then consumes B's retained events."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_two_background_child_replay","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 two-child background replay","location":{"directory":"/private/tmp/t3-opencode2-two-background-child-replay"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_two_background_child_replay","prompt":{"text":"Start two background subagents: one with description alpha completed child fixture and prompt Respond exactly ALPHA_COMPLETED_OK, and one with description bravo completed child fixture and prompt Respond exactly BRAVO_COMPLETED_OK. Then respond exactly PARENT_RELEASED without waiting for either child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_two_background_child_replay_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_root","input":{"type":"user","data":{"text":"Start two background subagents"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"emit_inbound","label":"alpha.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":0,"callID":"call_opencode2_two_background_child_replay_alpha","name":"subagent"}}}}
+{"type":"emit_inbound","label":"alpha.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":0,"callID":"call_opencode2_two_background_child_replay_alpha","input":{"agent":"explore","background":true,"description":"alpha completed child fixture","prompt":"Respond exactly ALPHA_COMPLETED_OK"}}}}}
+{"type":"emit_inbound","label":"alpha.tool.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":0,"callID":"call_opencode2_two_background_child_replay_alpha","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_two_background_child_replay_alpha"}}}}}
+{"type":"emit_inbound","label":"alpha.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_two_background_child_replay_alpha","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","info":{"id":"ses_opencode2_two_background_child_replay_alpha","slug":"alpha-completed-child","projectID":"global","directory":"/private/tmp/t3-opencode2-two-background-child-replay","parentID":"ses_opencode2_two_background_child_replay","title":"alpha completed child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","inputID":"input_opencode2_two_background_child_replay_alpha","input":{"type":"user","data":{"text":"Respond exactly ALPHA_COMPLETED_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"alpha.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","inputID":"input_opencode2_two_background_child_replay_alpha"}}}}
+{"type":"emit_inbound","label":"alpha.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha"}}}}
+{"type":"emit_inbound","label":"alpha.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","assistantMessageID":"message_opencode2_two_background_child_replay_alpha","ordinal":0}}}}
+{"type":"emit_inbound","label":"alpha.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","assistantMessageID":"message_opencode2_two_background_child_replay_alpha","ordinal":0,"delta":"ALPHA_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"alpha.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_replay_alpha","assistantMessageID":"message_opencode2_two_background_child_replay_alpha","ordinal":0,"text":"ALPHA_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"alpha.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_replay_alpha"}}}}
+{"type":"emit_inbound","label":"bravo.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":1,"callID":"call_opencode2_two_background_child_replay_bravo","name":"subagent"}}}}
+{"type":"emit_inbound","label":"bravo.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":1,"callID":"call_opencode2_two_background_child_replay_bravo","input":{"agent":"explore","background":true,"description":"bravo completed child fixture","prompt":"Respond exactly BRAVO_COMPLETED_OK"}}}}}
+{"type":"emit_inbound","label":"bravo.tool.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":1,"callID":"call_opencode2_two_background_child_replay_bravo","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_two_background_child_replay_bravo"}}}}}
+{"type":"emit_inbound","label":"bravo.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_two_background_child_replay_bravo","created":1785394800200,"type":"session.created","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","info":{"id":"ses_opencode2_two_background_child_replay_bravo","slug":"bravo-completed-child","projectID":"global","directory":"/private/tmp/t3-opencode2-two-background-child-replay","parentID":"ses_opencode2_two_background_child_replay","title":"bravo completed child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800200,"updated":1785394800200}}}}}}
+{"type":"emit_inbound","label":"bravo.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","inputID":"input_opencode2_two_background_child_replay_bravo","input":{"type":"user","data":{"text":"Respond exactly BRAVO_COMPLETED_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"bravo.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","inputID":"input_opencode2_two_background_child_replay_bravo"}}}}
+{"type":"emit_inbound","label":"bravo.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo"}}}}
+{"type":"emit_inbound","label":"bravo.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","assistantMessageID":"message_opencode2_two_background_child_replay_bravo","ordinal":0}}}}
+{"type":"emit_inbound","label":"bravo.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","assistantMessageID":"message_opencode2_two_background_child_replay_bravo","ordinal":0,"delta":"BRAVO_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"bravo.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_replay_bravo","assistantMessageID":"message_opencode2_two_background_child_replay_bravo","ordinal":0,"text":"BRAVO_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"bravo.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_replay_bravo"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":2}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":2,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_root","ordinal":2,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.root","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_two_background_child_replay"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.root.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.root","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-two-background-child-replay"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.root.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"wake.alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_wake_alpha","input":{"type":"synthetic","data":{"text":"ALPHA_COMPLETED_OK","description":"alpha completed child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.bravo.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_wake_bravo","input":{"type":"synthetic","data":{"text":"BRAVO_COMPLETED_OK","description":"bravo completed child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.alpha.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_wake_alpha"}}}}
+{"type":"emit_inbound","label":"wake.alpha.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_alpha","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_alpha","ordinal":0,"delta":"ALPHA_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_alpha","ordinal":0,"text":"ALPHA_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"wake.alpha.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"emit_inbound","label":"wake.bravo.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_replay","inputID":"input_opencode2_two_background_child_replay_wake_bravo"}}}}
+{"type":"emit_inbound","label":"wake.bravo.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"expect_outbound","label":"root.pending.list.first.continuation","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_two_background_child_replay"}}}
+{"type":"emit_inbound","label":"root.pending.list.first.continuation.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.first.continuation","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-two-background-child-replay"}}}}
+{"type":"emit_inbound","label":"root.shell.list.first.continuation.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"wake.bravo.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_bravo","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.bravo.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_bravo","ordinal":0,"delta":"BRAVO_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"wake.bravo.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_replay","assistantMessageID":"message_opencode2_two_background_child_replay_wake_bravo","ordinal":0,"text":"BRAVO_COMPLETED_OK"}}}}
+{"type":"emit_inbound","label":"wake.bravo.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_replay"}}}}
+{"type":"expect_outbound","label":"root.pending.list.second.continuation","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_two_background_child_replay"}}}
+{"type":"emit_inbound","label":"root.pending.list.second.continuation.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.second.continuation","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-two-background-child-replay"}}}}
+{"type":"emit_inbound","label":"root.shell.list.second.continuation.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/output.ts
new file mode 100644
index 00000000000..505879eca17
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_replay/output.ts
@@ -0,0 +1,52 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2TwoBackgroundChildReplayOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const alphaSettled = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.alpha.execution.succeeded",
+ );
+ const bravoStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.bravo.execution.started",
+ );
+ const firstContinuation = transcript.entries.findIndex(
+ (entry) =>
+ entry.type === "expect_outbound" && entry.label === "root.pending.list.first.continuation",
+ );
+ assert.isAtLeast(alphaSettled, 0);
+ assert.isAtLeast(bravoStarted, 0);
+ assert.isAtLeast(firstContinuation, 0);
+ assert.isAbove(bravoStarted, alphaSettled);
+ assert.isAbove(firstContinuation, bravoStarted);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 3,
+ runStatuses: ["completed", "completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "ALPHA_COMPLETED_OK");
+ assertAssistantTextIncludes(projection, "BRAVO_COMPLETED_OK");
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_OUTPUT_MUST_NOT_APPEAR");
+
+ const subagentItems = projection.turnItems.filter((item) => item.type === "subagent");
+ assert.lengthOf(subagentItems, 2);
+ for (const item of subagentItems) {
+ assert.equal(item.status, "completed");
+ const child = result.projections.get(item.childThreadId!);
+ assert.isDefined(child);
+ }
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/input.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/input.ts
new file mode 100644
index 00000000000..e3740bfde6c
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/input.ts
@@ -0,0 +1,17 @@
+import {
+ OPENCODE2_TWO_SUBAGENT_BACKGROUND_PROMPT,
+ type OrchestratorFixtureInput,
+} from "../shared.ts";
+
+export function openCode2TwoBackgroundChildStopInput(): OrchestratorFixtureInput {
+ return {
+ steps: [
+ { type: "message", text: OPENCODE2_TWO_SUBAGENT_BACKGROUND_PROMPT },
+ {
+ type: "interrupt_provider_native",
+ subagentNativeItemId: "tool:call_opencode2_two_background_child_stop_cancel",
+ },
+ { type: "provider_continuation" },
+ ],
+ };
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..0a77b12bd34
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/opencode2_transcript.ndjson
@@ -0,0 +1,67 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16540","scenario":"opencode2_two_background_child_stop","metadata":{"source":"focused-provider-native-stop","description":"Two directly-owned OpenCode 2 background children use separate native execution boundaries. The completed child wake is replayed after the cancelled child starts its live suppressed execution, and the cancelled wake must retain that execution owner until it drains."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_two_background_child_stop","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785394800000,"updated":1785394800000},"title":"T3 OpenCode 2 two-child background Stop replay","location":{"directory":"/private/tmp/t3-opencode2-two-background-child-stop"}}}}
+{"type":"expect_outbound","label":"session.prompt.root","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_two_background_child_stop","prompt":{"text":"Start two background subagents: one with description alpha background child fixture and prompt Respond exactly ALPHA_BACKGROUND_OK, and one with description cancel background child fixture and prompt Respond exactly CANCEL_BACKGROUND_PARTIAL. Then respond exactly PARENT_RELEASED without waiting for either child."}}}}
+{"type":"emit_inbound","label":"session.prompt.root.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_two_background_child_stop_root"}}}
+{"type":"emit_inbound","label":"root.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_root","input":{"type":"user","data":{"text":"Start two background subagents"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"root.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_root"}}}}
+{"type":"emit_inbound","label":"root.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"emit_inbound","label":"alpha.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":0,"callID":"call_opencode2_two_background_child_stop_alpha","name":"subagent"}}}}
+{"type":"emit_inbound","label":"alpha.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":0,"callID":"call_opencode2_two_background_child_stop_alpha","input":{"agent":"explore","background":true,"description":"alpha background child fixture","prompt":"Respond exactly ALPHA_BACKGROUND_OK"}}}}}
+{"type":"emit_inbound","label":"alpha.tool.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":0,"callID":"call_opencode2_two_background_child_stop_alpha","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_two_background_child_stop_alpha"}}}}}
+{"type":"emit_inbound","label":"alpha.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_two_background_child_stop_alpha","created":1785394800100,"type":"session.created","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","info":{"id":"ses_opencode2_two_background_child_stop_alpha","slug":"alpha-background-child","projectID":"global","directory":"/private/tmp/t3-opencode2-two-background-child-stop","parentID":"ses_opencode2_two_background_child_stop","title":"alpha background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800100,"updated":1785394800100}}}}}}
+{"type":"emit_inbound","label":"alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","inputID":"input_opencode2_two_background_child_stop_alpha","input":{"type":"user","data":{"text":"Respond exactly ALPHA_BACKGROUND_OK"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"alpha.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","inputID":"input_opencode2_two_background_child_stop_alpha"}}}}
+{"type":"emit_inbound","label":"alpha.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha"}}}}
+{"type":"emit_inbound","label":"alpha.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","assistantMessageID":"message_opencode2_two_background_child_stop_alpha","ordinal":0}}}}
+{"type":"emit_inbound","label":"alpha.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","assistantMessageID":"message_opencode2_two_background_child_stop_alpha","ordinal":0,"delta":"ALPHA_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"alpha.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_stop_alpha","assistantMessageID":"message_opencode2_two_background_child_stop_alpha","ordinal":0,"text":"ALPHA_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"alpha.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_stop_alpha"}}}}
+{"type":"emit_inbound","label":"cancel.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":1,"callID":"call_opencode2_two_background_child_stop_cancel","name":"subagent"}}}}
+{"type":"emit_inbound","label":"cancel.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":1,"callID":"call_opencode2_two_background_child_stop_cancel","input":{"agent":"explore","background":true,"description":"cancel background child fixture","prompt":"Respond exactly CANCEL_BACKGROUND_PARTIAL"}}}}}
+{"type":"emit_inbound","label":"cancel.tool.success","frame":{"type":"sdk.event","event":{"type":"session.next.tool.success","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":1,"callID":"call_opencode2_two_background_child_stop_cancel","content":[{"type":"text","text":"Background subagent launched"}],"structured":{"sessionID":"ses_opencode2_two_background_child_stop_cancel"}}}}}
+{"type":"emit_inbound","label":"cancel.session.created","frame":{"type":"sdk.event","event":{"id":"event_opencode2_two_background_child_stop_cancel","created":1785394800200,"type":"session.created","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","info":{"id":"ses_opencode2_two_background_child_stop_cancel","slug":"cancel-background-child","projectID":"global","directory":"/private/tmp/t3-opencode2-two-background-child-stop","parentID":"ses_opencode2_two_background_child_stop","title":"cancel background child fixture","agent":"explore","model":{"id":"big-pickle","providerID":"opencode"},"version":"0.0.0-next-16540","time":{"created":1785394800200,"updated":1785394800200}}}}}}
+{"type":"emit_inbound","label":"cancel.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","inputID":"input_opencode2_two_background_child_stop_cancel","input":{"type":"user","data":{"text":"Respond exactly CANCEL_BACKGROUND_PARTIAL"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"cancel.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","inputID":"input_opencode2_two_background_child_stop_cancel"}}}}
+{"type":"emit_inbound","label":"cancel.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel"}}}}
+{"type":"emit_inbound","label":"cancel.tool.input.started","frame":{"type":"sdk.event","event":{"type":"session.next.tool.input.started","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel","ordinal":0,"callID":"call_opencode2_two_background_child_stop_shell","name":"bash"}}}}
+{"type":"emit_inbound","label":"cancel.tool.called","frame":{"type":"sdk.event","event":{"type":"session.next.tool.called","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel","ordinal":0,"callID":"call_opencode2_two_background_child_stop_shell","input":{"command":"sleep 30 && echo cancel partial"}}}}}
+{"type":"emit_inbound","label":"cancel.shell.created","frame":{"type":"sdk.event","event":{"type":"shell.created","data":{"info":{"id":"shell_opencode2_two_background_child_stop_cancel","status":"running","command":"sleep 30 && echo cancel partial","cwd":"/private/tmp/t3-opencode2-two-background-child-stop","shell":"/bin/bash","file":"/private/tmp/t3-opencode2-two-background-child-stop/shell.log","pid":4247,"metadata":{"sessionID":"ses_opencode2_two_background_child_stop_cancel"},"time":{"started":1785394800300}}}}}}
+{"type":"emit_inbound","label":"cancel.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel_partial","ordinal":1}}}}
+{"type":"emit_inbound","label":"cancel.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel_partial","ordinal":1,"delta":"CANCEL_PARTIAL"}}}}
+{"type":"emit_inbound","label":"cancel.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel_partial","ordinal":1,"text":"CANCEL_PARTIAL"}}}}
+{"type":"emit_inbound","label":"root.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":2}}}}
+{"type":"emit_inbound","label":"root.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":2,"delta":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_root","ordinal":2,"text":"PARENT_RELEASED"}}}}
+{"type":"emit_inbound","label":"root.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"expect_outbound","label":"cancel.session.interrupt","frame":{"type":"session.interrupt","input":{"sessionID":"ses_opencode2_two_background_child_stop_cancel"}}}
+{"type":"emit_inbound","label":"cancel.session.interrupt.response","frame":{"type":"sdk.response","operation":"session.interrupt","data":true}}
+{"type":"expect_outbound","label":"cancel.shell.remove","frame":{"type":"shell.remove","input":{"id":"shell_opencode2_two_background_child_stop_cancel","location":{"directory":"/private/tmp/t3-opencode2-two-background-child-stop"}}}}
+{"type":"emit_inbound","label":"cancel.shell.remove.response","frame":{"type":"sdk.response","operation":"shell.remove","data":true}}
+{"type":"emit_inbound","label":"cancel.shell.deleted","frame":{"type":"sdk.event","event":{"type":"shell.deleted","data":{"id":"shell_opencode2_two_background_child_stop_cancel"}}}}
+{"type":"emit_inbound","label":"cancel.tool.failed","frame":{"type":"sdk.event","event":{"type":"session.next.tool.failed","data":{"sessionID":"ses_opencode2_two_background_child_stop_cancel","assistantMessageID":"message_opencode2_two_background_child_stop_cancel","callID":"call_opencode2_two_background_child_stop_shell","error":{"type":"ToolExecutionError","message":"Tool execution interrupted"},"executed":true}}}}
+{"type":"emit_inbound","label":"cancel.execution.interrupted","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_stop_cancel","reason":"user"}}}}
+{"type":"expect_outbound","label":"root.pending.list.after.stop","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_two_background_child_stop"}}}
+{"type":"emit_inbound","label":"root.pending.list.after.stop.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.after.stop","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-two-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.after.stop.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"wake.alpha.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_wake_alpha","input":{"type":"synthetic","data":{"text":"ALPHA_BACKGROUND_OK","description":"alpha background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.alpha.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_wake_alpha"}}}}
+{"type":"emit_inbound","label":"wake.alpha.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_alpha","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_alpha","ordinal":0,"delta":"ALPHA_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.alpha.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_alpha","ordinal":0,"text":"ALPHA_BACKGROUND_OK"}}}}
+{"type":"emit_inbound","label":"wake.alpha.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"emit_inbound","label":"wake.cancel.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_wake_cancel","input":{"type":"synthetic","data":{"text":"CANCELLED_WAKE_OUTPUT_MUST_NOT_APPEAR","description":"cancel background child fixture"},"delivery":"queue"}}}}}
+{"type":"emit_inbound","label":"wake.cancel.input.promoted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_two_background_child_stop","inputID":"input_opencode2_two_background_child_stop_wake_cancel"}}}}
+{"type":"emit_inbound","label":"wake.cancel.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"expect_outbound","label":"root.pending.list.continuation","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_two_background_child_stop"}}}
+{"type":"emit_inbound","label":"root.pending.list.continuation.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"root.shell.list.continuation","frame":{"type":"shell.list","input":{"location":{"directory":"/private/tmp/t3-opencode2-two-background-child-stop"}}}}
+{"type":"emit_inbound","label":"root.shell.list.continuation.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"emit_inbound","label":"wake.cancel.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_cancel","ordinal":0}}}}
+{"type":"emit_inbound","label":"wake.cancel.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_cancel","ordinal":0,"delta":"CANCELLED_WAKE_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"wake.cancel.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_two_background_child_stop","assistantMessageID":"message_opencode2_two_background_child_stop_wake_cancel","ordinal":0,"text":"CANCELLED_WAKE_OUTPUT_MUST_NOT_APPEAR"}}}}
+{"type":"emit_inbound","label":"wake.cancel.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_two_background_child_stop"}}}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/output.ts
new file mode 100644
index 00000000000..dcb15110803
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/opencode2_two_background_child_stop/output.ts
@@ -0,0 +1,77 @@
+import { assert } from "@effect/vitest";
+import type { ProviderReplayTranscript } from "@t3tools/contracts";
+
+import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
+import {
+ assertAssistantTextIncludes,
+ assertBaseProjection,
+ assertSemanticProjectionIntegrity,
+ projectionFor,
+} from "../shared.ts";
+
+export function assertOpenCode2TwoBackgroundChildStopOutput(
+ result: OrchestratorV2ScenarioResult,
+ transcript: ProviderReplayTranscript,
+) {
+ const alphaSettled = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.alpha.execution.succeeded",
+ );
+ const cancelStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.cancel.execution.started",
+ );
+ const continuationPendingList = transcript.entries.findIndex(
+ (entry) => entry.type === "expect_outbound" && entry.label === "root.pending.list.continuation",
+ );
+ const cancelTextStarted = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.cancel.text.started",
+ );
+ const cancelSettled = transcript.entries.findIndex(
+ (entry) => entry.type === "emit_inbound" && entry.label === "wake.cancel.execution.succeeded",
+ );
+ assert.isAtLeast(alphaSettled, 0);
+ assert.isAtLeast(cancelStarted, 0);
+ assert.isAtLeast(continuationPendingList, 0);
+ assert.isAtLeast(cancelTextStarted, 0);
+ assert.isAtLeast(cancelSettled, 0);
+ assert.isAbove(cancelStarted, alphaSettled);
+ assert.isAbove(continuationPendingList, cancelStarted);
+ assert.isAbove(cancelTextStarted, continuationPendingList);
+ assert.isAbove(cancelSettled, cancelTextStarted);
+
+ const projection = projectionFor(result, transcript.scenario);
+ assertBaseProjection({
+ result,
+ transcript,
+ runCount: 2,
+ runStatuses: ["completed", "completed"],
+ });
+ assertSemanticProjectionIntegrity(projection);
+ assertAssistantTextIncludes(projection, "PARENT_RELEASED");
+ assertAssistantTextIncludes(projection, "ALPHA_BACKGROUND_OK");
+ assert.notInclude(JSON.stringify(projection), "CANCELLED_WAKE_OUTPUT_MUST_NOT_APPEAR");
+
+ const subagentItems = projection.turnItems.filter((item) => item.type === "subagent");
+ assert.lengthOf(subagentItems, 2);
+ const alphaItem = subagentItems.find(
+ (item) => item.type === "subagent" && item.title?.includes("alpha"),
+ );
+ const cancelItem = subagentItems.find(
+ (item) => item.type === "subagent" && item.title?.includes("cancel"),
+ );
+ assert.strictEqual(alphaItem?.type, "subagent");
+ assert.strictEqual(cancelItem?.type, "subagent");
+ if (alphaItem?.type !== "subagent" || cancelItem?.type !== "subagent") {
+ throw new Error("OpenCode 2 two-child background Stop items are missing");
+ }
+ assert.equal(alphaItem.status, "completed");
+ assert.equal(cancelItem.status, "interrupted");
+ assert.include(cancelItem.result ?? "", "CANCEL_PARTIAL");
+
+ const alphaProjection = result.projections.get(alphaItem.childThreadId!);
+ const cancelProjection = result.projections.get(cancelItem.childThreadId!);
+ assert.isDefined(alphaProjection);
+ assert.isDefined(cancelProjection);
+ assertAssistantTextIncludes(alphaProjection!, "ALPHA_BACKGROUND_OK");
+ assertAssistantTextIncludes(cancelProjection!, "CANCEL_PARTIAL");
+ assert.notInclude(JSON.stringify(cancelProjection), "CANCELLED_WAKE_OUTPUT_MUST_NOT_APPEAR");
+}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..4041ae6a267
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/opencode2_transcript.ndjson
@@ -0,0 +1,29 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16383","scenario":"queued_turn","metadata":{"source":"derived-from-live-run","capturedAt":"2026-07-29","nativeSessionId":"ses_opencode2_queued_turn","model":"opencode/big-pickle","description":"An OpenCode 2 turn with a second app-owned message queued while the first turn is active. The provider sees the second prompt only after the first turn settles."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_queued_turn","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 queued-turn replay","location":{"directory":""}}}}
+{"type":"expect_outbound","label":"session.prompt.first","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_queued_turn","prompt":{"text":"Respond with exactly: first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.first.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_first"}}}
+{"type":"emit_inbound","label":"session.input.admitted.first","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_queued_turn","inputID":"input_opencode2_first","input":{"id":"input_opencode2_first","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_queued_turn"}}}}
+{"type":"emit_inbound","label":"session.text.started.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_first","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_first","ordinal":0,"delta":"first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended.first","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_first","ordinal":0,"text":"first fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.first","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_queued_turn"}}}}
+{"type":"expect_outbound","label":"session.pending.list.first","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_queued_turn"}}}
+{"type":"emit_inbound","label":"session.pending.list.first.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.first","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.first.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"expect_outbound","label":"session.prompt.second","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_queued_turn","prompt":{"text":"Respond with exactly: second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.prompt.second.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_second"}}}
+{"type":"emit_inbound","label":"session.input.admitted.second","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_queued_turn","inputID":"input_opencode2_second","input":{"id":"input_opencode2_second","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_queued_turn"}}}}
+{"type":"emit_inbound","label":"session.text.started.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"delta":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.text.ended.second","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_queued_turn","assistantMessageID":"message_opencode2_second","ordinal":0,"text":"second fixture turn complete"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded.second","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_queued_turn"}}}}
+{"type":"expect_outbound","label":"session.pending.list.second","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_queued_turn"}}}
+{"type":"emit_inbound","label":"session.pending.list.second.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list.second","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.second.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts b/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts
index 685565c63fd..8f22b9487da 100644
--- a/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/shared.ts
@@ -50,6 +50,29 @@ export const SUBAGENT_PROMPT =
export const SUBAGENT_V2_PROMPT = "just say hello";
export const OPENCODE_SUBAGENT_PROMPT =
"Use the task tool exactly once. Delegate to the general subagent with this prompt: Respond exactly CHILD_OK. After the task completes, respond exactly PARENT_OK.";
+export const OPENCODE2_COMPACTION_PROMPT =
+ "Compact the current context, then respond exactly: compaction fixture complete";
+export const OPENCODE2_COMPACTION_INTERRUPT_PROMPT =
+ "Begin compacting the current context and wait for it to finish.";
+export const OPENCODE2_RETRY_PROMPT =
+ "Recover from the transient provider error, then respond exactly: retry fixture complete";
+export const OPENCODE2_SUBAGENT_PROMPT =
+ "Use the subagent tool exactly once with description child fixture and prompt Respond exactly CHILD_OK. Then respond exactly PARENT_OK.";
+export const OPENCODE2_SUBAGENT_BACKGROUND_PROMPT =
+ "Start one background subagent with description background child fixture and prompt Respond exactly CHILD_BACKGROUND_OK. Then respond exactly PARENT_RELEASED without waiting for the child.";
+export const OPENCODE2_TWO_SUBAGENT_BACKGROUND_PROMPT =
+ "Start two background subagents: one with description alpha background child fixture and prompt Respond exactly ALPHA_BACKGROUND_OK, and one with description cancel background child fixture and prompt Respond exactly CANCEL_BACKGROUND_PARTIAL. Then respond exactly PARENT_RELEASED without waiting for either child.";
+export const OPENCODE2_TWO_COMPLETED_SUBAGENT_PROMPT =
+ "Start two background subagents: one with description alpha completed child fixture and prompt Respond exactly ALPHA_COMPLETED_OK, and one with description bravo completed child fixture and prompt Respond exactly BRAVO_COMPLETED_OK. Then respond exactly PARENT_RELEASED without waiting for either child.";
+export const OPENCODE2_THREAD_DELETE_PROMPT = "Respond exactly: native deletion fixture complete";
+export const OPENCODE2_SHELL_PROJECTION_PROMPT =
+ "Run a shell command that prints the paged shell fixture output, move it to background observation, then respond exactly: shell projection fixture complete";
+export const OPENCODE2_SHELL_FAILURE_PROMPT =
+ "Run a shell command that exits with status 7, move it to background observation, then respond exactly: shell failure fixture complete";
+export const OPENCODE2_SHELL_DELETION_PROMPT =
+ "Run a long shell command, move it to background observation, then respond exactly: shell deletion fixture complete";
+export const OPENCODE2_BACKGROUND_STOP_PROMPT =
+ "Run this shell command and wait for it: sleep 30 && echo background stop should not finish. Do not respond before it completes.";
export const SUBAGENT_CONTINUE_PROMPT =
"Spawn one subagent and have it reply exactly: initial subagent response";
export const SUBAGENT_CONTINUE_PARENT_PROMPT =
@@ -188,6 +211,10 @@ export type OrchestratorFixtureInputStep =
readonly type: "release_replay_gate";
readonly label: string;
}
+ | {
+ readonly type: "provider_continuation";
+ readonly text?: string;
+ }
| {
readonly type: "steer";
readonly text: string;
@@ -210,6 +237,13 @@ export type OrchestratorFixtureInputStep =
readonly label: string;
readonly targetRunIndex: number;
}
+ | {
+ readonly type: "interrupt_provider_native";
+ readonly subagentNativeItemId: string;
+ }
+ | {
+ readonly type: "delete";
+ }
| {
readonly type: "approve_next_runtime_request";
readonly decision?: Extract<
@@ -296,6 +330,11 @@ export const OPENCODE_MODEL_SELECTION = {
options: [{ id: "agent", value: "build" }],
} satisfies ModelSelection;
+export const OPENCODE2_MODEL_SELECTION = {
+ instanceId: ProviderInstanceId.make("opencode2"),
+ model: "opencode/big-pickle",
+} satisfies ModelSelection;
+
export const ACP_REGISTRY_MODEL_SELECTION = {
instanceId: ProviderInstanceId.make("acpRegistry"),
model: "default",
@@ -475,6 +514,7 @@ export function materializeFixtureInput(input: {
const shouldRunInBackground =
(nextStep !== undefined &&
((nextStep.type === "interrupt" && nextStep.targetRunIndex === runIndex) ||
+ nextStep.type === "interrupt_provider_native" ||
nextStep.type === "queue_message" ||
(nextStep.type === "restart" && nextStep.targetRunIndex === runIndex) ||
(nextStep.type === "release_replay_gate_after_waiting" &&
@@ -501,6 +541,14 @@ export function materializeFixtureInput(input: {
);
if (shouldRunInBackground) {
activeRunDispatchKeys.add(key);
+ if (nextStep?.type === "interrupt_provider_native") {
+ steps.push({
+ type: "await_run_status",
+ threadId: ids.threadId,
+ runId: runIdFor(runIndex),
+ status: "completed",
+ });
+ }
} else if (
!(
nextStep !== undefined &&
@@ -512,6 +560,29 @@ export function materializeFixtureInput(input: {
}
}
break;
+ case "provider_continuation":
+ messageIndex += 1;
+ runIndex += 1;
+ pushDispatch(
+ dispatchMessageCommand({
+ commandId: yield* idAllocator.allocate.command({
+ fixtureName: input.scenario,
+ commandName: `provider-continuation-${messageIndex}`,
+ }),
+ ids,
+ modelSelection: input.modelSelection,
+ messageId: yield* idAllocator.allocate.message({
+ threadId: ids.threadId,
+ ordinal: messageIndex,
+ }),
+ text: step.text ?? "Background task completed.",
+ dispatchMode: { type: "queue_after_active" },
+ createdBy: "agent",
+ creationSource: "provider",
+ }),
+ );
+ steps.push({ type: "await_thread_idle", threadId: ids.threadId });
+ break;
case "queue_message": {
messageIndex += 1;
runIndex += 1;
@@ -744,6 +815,40 @@ export function materializeFixtureInput(input: {
runId: runIdFor(step.targetRunIndex),
});
break;
+ case "interrupt_provider_native":
+ pushDispatch(
+ {
+ type: "run.interrupt",
+ commandId: yield* idAllocator.allocate.command({
+ fixtureName: input.scenario,
+ commandName: "interrupt-provider-native",
+ }),
+ threadId: ids.threadId,
+ intent: "provider_native_only",
+ },
+ { advanceClockAfter: false },
+ );
+ steps.push({
+ type: "await_subagent_status",
+ threadId: ids.threadId,
+ status: "interrupted",
+ subagentId: idAllocator.derive.nodeFromProviderItem({
+ driver: input.driver,
+ nativeItemId: step.subagentNativeItemId,
+ }),
+ });
+ steps.push({ type: "advance_clock", duration: "1 millis" });
+ break;
+ case "delete":
+ pushDispatch({
+ type: "thread.delete",
+ commandId: yield* idAllocator.allocate.command({
+ fixtureName: input.scenario,
+ commandName: "thread-delete",
+ }),
+ threadId: ids.threadId,
+ });
+ break;
case "advance_clock":
steps.push({ type: "advance_clock", duration: step.duration });
break;
diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode2_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode2_transcript.ndjson
new file mode 100644
index 00000000000..ad383f8022e
--- /dev/null
+++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/opencode2_transcript.ndjson
@@ -0,0 +1,20 @@
+{"type":"transcript_start","provider":"opencode2","protocol":"opencode2-sdk.sse","version":"0.0.0-next-16553","scenario":"simple","metadata":{"source":"derived-from-installed-app-failure-boundary","capturedAt":"2026-07-30","nativeSessionId":"ses_opencode2_simple","model":"opencode/big-pickle","description":"A text-only OpenCode 2 turn that reproduces the preview legacy permission payload without patterns, requires a non-persistent full-access reply, and completes the post-settlement probes at the SDK request and SSE boundary."}}
+{"type":"expect_outbound","label":"event.subscribe","frame":{"type":"event.subscribe"}}
+{"type":"expect_outbound","label":"session.create","frame":{"type":"session.create","input":{"model":{"id":"big-pickle","providerID":"opencode"},"agent":"build","location":{"directory":""}}}}
+{"type":"emit_inbound","label":"session.create.response","frame":{"type":"sdk.response","operation":"session.create","data":{"id":"ses_opencode2_simple","projectID":"global","model":{"id":"big-pickle","providerID":"opencode"},"cost":0,"tokens":{"input":0,"output":0,"reasoning":0,"cache":{"read":0,"write":0}},"time":{"created":1785297600000,"updated":1785297600000},"title":"T3 OpenCode 2 replay","location":{"directory":""}}}}
+{"type":"expect_outbound","label":"session.prompt","frame":{"type":"session.prompt","input":{"sessionID":"ses_opencode2_simple","prompt":{"text":"Respond with the following text: fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.prompt.response","frame":{"type":"sdk.response","operation":"session.prompt","data":{"id":"input_opencode2_simple"}}}
+{"type":"emit_inbound","label":"session.input.admitted","frame":{"type":"sdk.event","event":{"type":"session.next.prompt.admitted","data":{"sessionID":"ses_opencode2_simple","inputID":"input_opencode2_simple","input":{"id":"input_opencode2_simple","type":"user"}}}}}
+{"type":"emit_inbound","label":"session.execution.started","frame":{"type":"sdk.event","event":{"type":"session.next.step.started","data":{"sessionID":"ses_opencode2_simple"}}}}
+{"type":"emit_inbound","label":"permission.asked.without-patterns","frame":{"type":"sdk.event","event":{"type":"permission.asked","data":{"id":"permission_opencode2_simple","sessionID":"ses_opencode2_simple","permission":"bash","metadata":{},"always":[]}}}}
+{"type":"expect_outbound","label":"session.permission.reply","frame":{"type":"session.permission.reply","input":{"sessionID":"ses_opencode2_simple","requestID":"permission_opencode2_simple","reply":"once"}}}
+{"type":"emit_inbound","label":"session.permission.reply.response","frame":{"type":"sdk.response","operation":"session.permission.reply","data":null}}
+{"type":"emit_inbound","label":"session.text.started","frame":{"type":"sdk.event","event":{"type":"session.next.text.started","data":{"sessionID":"ses_opencode2_simple","assistantMessageID":"message_opencode2_simple","ordinal":0}}}}
+{"type":"emit_inbound","label":"session.text.delta","frame":{"type":"sdk.event","event":{"type":"session.next.text.delta","data":{"sessionID":"ses_opencode2_simple","assistantMessageID":"message_opencode2_simple","ordinal":0,"delta":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.text.ended","frame":{"type":"sdk.event","event":{"type":"session.next.text.ended","data":{"sessionID":"ses_opencode2_simple","assistantMessageID":"message_opencode2_simple","ordinal":0,"text":"fixture simple ok"}}}}
+{"type":"emit_inbound","label":"session.execution.succeeded","frame":{"type":"sdk.event","event":{"type":"session.next.step.ended","data":{"finish":"stop","sessionID":"ses_opencode2_simple"}}}}
+{"type":"expect_outbound","label":"session.pending.list","frame":{"type":"session.pending.list","input":{"sessionID":"ses_opencode2_simple"}}}
+{"type":"emit_inbound","label":"session.pending.list.response","frame":{"type":"sdk.response","operation":"session.pending.list","data":[]}}
+{"type":"expect_outbound","label":"shell.list","frame":{"type":"shell.list","input":{"location":{"directory":""}}}}
+{"type":"emit_inbound","label":"shell.list.response","frame":{"type":"sdk.response","operation":"shell.list","data":[]}}
+{"type":"runtime_exit","status":"success"}
diff --git a/apps/server/src/provider/Drivers/OpenCode2Driver.test.ts b/apps/server/src/provider/Drivers/OpenCode2Driver.test.ts
new file mode 100644
index 00000000000..40ad6785e79
--- /dev/null
+++ b/apps/server/src/provider/Drivers/OpenCode2Driver.test.ts
@@ -0,0 +1,86 @@
+import { expect, it } from "@effect/vitest";
+
+import { openCode2ProviderMaintenanceResolver } from "./OpenCode2Driver.ts";
+
+const provider = "opencode2";
+const packageName = "@opencode-ai/cli";
+
+it.each([
+ {
+ manager: "npm",
+ commandPath: "/usr/local/lib/node_modules/@opencode-ai/cli/bin/opencode2.exe",
+ command: "npm install -g @opencode-ai/cli@next",
+ executable: "npm",
+ args: ["install", "-g", "@opencode-ai/cli@next"],
+ lockKey: "npm-global",
+ },
+ {
+ manager: "Bun",
+ commandPath: "/home/test/.bun/bin/opencode2",
+ command: "bun add -g --trust @opencode-ai/cli@next",
+ executable: "bun",
+ args: ["add", "-g", "--trust", "@opencode-ai/cli@next"],
+ lockKey: "bun-global",
+ },
+ {
+ manager: "pnpm",
+ commandPath: "/home/test/.local/share/pnpm/opencode2",
+ command: "pnpm add -g @opencode-ai/cli@next --allow-build=@opencode-ai/cli",
+ executable: "pnpm",
+ args: ["add", "-g", "@opencode-ai/cli@next", "--allow-build=@opencode-ai/cli"],
+ lockKey: "pnpm-global",
+ },
+ {
+ manager: "Vite Plus",
+ commandPath: "/home/test/.vite-plus/bin/opencode2",
+ command: "vp i -g @opencode-ai/cli@next",
+ executable: "vp",
+ args: ["i", "-g", "@opencode-ai/cli@next"],
+ lockKey: "vite-plus-global",
+ },
+])("uses $manager to update a package-managed opencode2 executable", (expected) => {
+ const capabilities = openCode2ProviderMaintenanceResolver({ serverUrl: "" }).resolve({
+ binaryPath: "opencode2",
+ resolvedCommandPath: expected.commandPath,
+ });
+
+ expect(capabilities).toEqual({
+ provider,
+ packageName,
+ npmDistTag: "next",
+ update: {
+ command: expected.command,
+ executable: expected.executable,
+ args: expected.args,
+ lockKey: expected.lockKey,
+ },
+ });
+});
+
+it("keeps custom OpenCode 2 executable paths manual-only", () => {
+ expect(
+ openCode2ProviderMaintenanceResolver({ serverUrl: "" }).resolve({
+ binaryPath: "/opt/opencode-dev/opencode2",
+ resolvedCommandPath: "/opt/opencode-dev/opencode2",
+ }),
+ ).toEqual({
+ provider,
+ packageName,
+ npmDistTag: "next",
+ update: null,
+ });
+});
+
+it("keeps externally managed OpenCode 2 servers manual-only", () => {
+ expect(
+ openCode2ProviderMaintenanceResolver({ serverUrl: "http://127.0.0.1:4096" }).resolve({
+ binaryPath: "opencode2",
+ resolvedCommandPath: "/home/test/.bun/bin/opencode2",
+ }),
+ ).toEqual({
+ provider,
+ packageName,
+ npmDistTag: "next",
+ update: null,
+ });
+});
diff --git a/apps/server/src/provider/Drivers/OpenCode2Driver.ts b/apps/server/src/provider/Drivers/OpenCode2Driver.ts
new file mode 100644
index 00000000000..2697ecbf645
--- /dev/null
+++ b/apps/server/src/provider/Drivers/OpenCode2Driver.ts
@@ -0,0 +1,222 @@
+/**
+ * OpenCode2Driver — `ProviderDriver` for the OpenCode 2.x runtime.
+ *
+ * Separate from `OpenCodeDriver` rather than a binary-path variant of it: 2.x
+ * serves a different route surface, mints a mandatory server password, and
+ * emits a different event vocabulary, so the two share no runtime, no adapter,
+ * and no probe. See `OpenCode2AdapterV2` for the protocol contract.
+ *
+ * The driver ships with `hasDefaultInstance: false` (see
+ * `apps/web/src/components/settings/providerDriverMeta.ts`), so there is no
+ * built-in instance and no `providers.opencode2`-backed default: an instance
+ * exists only when the user adds one.
+ *
+ * @module provider/Drivers/OpenCode2Driver
+ */
+import { OpenCode2Settings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+import { HttpClient } from "effect/unstable/http";
+
+import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
+import { ServerConfig } from "../../config.ts";
+import {
+ OpenCode2AdapterV2Driver,
+ type OpenCode2AdapterV2DriverEnv,
+} from "../../orchestration-v2/Adapters/OpenCode2AdapterV2.ts";
+import { ServerSettingsService } from "../../serverSettings.ts";
+import { makeOpenCode2TextGeneration } from "../../textGeneration/OpenCode2TextGeneration.ts";
+import { ProviderDriverError } from "../Errors.ts";
+import {
+ checkOpenCode2ProviderStatus,
+ makePendingOpenCode2Provider,
+} from "../Layers/OpenCode2Provider.ts";
+import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
+import { applyOpenCode2ProviderEnvironment } from "../OpenCode2ProviderEnvironment.ts";
+import { OpenCode2Runtime } from "../opencode2Runtime.ts";
+import {
+ defaultProviderContinuationIdentity,
+ type ProviderDriver,
+ type ProviderInstance,
+} from "../ProviderDriver.ts";
+import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
+import {
+ enrichProviderSnapshotWithVersionAdvisory,
+ makeManualOnlyProviderMaintenanceCapabilities,
+ makePackageManagedProviderMaintenanceResolver,
+ makeStaticProviderMaintenanceResolver,
+ resolveProviderMaintenanceCapabilitiesEffect,
+ type ProviderMaintenanceCapabilitiesResolver,
+} from "../providerMaintenance.ts";
+import type { ServerProviderDraft } from "../providerSnapshot.ts";
+import {
+ haveProviderSnapshotSettingsChanged,
+ makeProviderSnapshotSettingsSource,
+ type ProviderSnapshotSettings,
+} from "../providerUpdateSettings.ts";
+
+const decodeOpenCode2Settings = Schema.decodeSync(OpenCode2Settings);
+
+const DRIVER_KIND = ProviderDriverKind.make("opencode2");
+const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
+const NPM_DIST_TAG = "next";
+const NPM_PACKAGE_NAME = "@opencode-ai/cli";
+const UPDATE = makePackageManagedProviderMaintenanceResolver({
+ provider: DRIVER_KIND,
+ npmPackageName: NPM_PACKAGE_NAME,
+ npmDistTag: NPM_DIST_TAG,
+ requiresInstallScripts: true,
+ homebrewFormula: null,
+ nativeUpdate: null,
+});
+
+export function openCode2ProviderMaintenanceResolver(
+ settings: Pick,
+): ProviderMaintenanceCapabilitiesResolver {
+ return settings.serverUrl
+ ? makeStaticProviderMaintenanceResolver(
+ makeManualOnlyProviderMaintenanceCapabilities({
+ provider: DRIVER_KIND,
+ packageName: NPM_PACKAGE_NAME,
+ npmDistTag: NPM_DIST_TAG,
+ }),
+ )
+ : UPDATE;
+}
+
+const withInstanceIdentity =
+ (input: {
+ readonly instanceId: ProviderInstance["instanceId"];
+ readonly displayName: string | undefined;
+ readonly accentColor: string | undefined;
+ readonly continuationGroupKey: string;
+ }) =>
+ (snapshot: ServerProviderDraft): ServerProvider => ({
+ ...snapshot,
+ instanceId: input.instanceId,
+ driver: DRIVER_KIND,
+ ...(input.displayName ? { displayName: input.displayName } : {}),
+ ...(input.accentColor ? { accentColor: input.accentColor } : {}),
+ continuation: { groupKey: input.continuationGroupKey },
+ });
+
+export type OpenCode2DriverEnv =
+ | OpenCode2AdapterV2DriverEnv
+ | BackgroundPolicy.BackgroundPolicy
+ | FileSystem.FileSystem
+ | HttpClient.HttpClient
+ | OpenCode2Runtime
+ | Path.Path
+ | ServerConfig
+ | ServerSettingsService;
+
+export const OpenCode2Driver: ProviderDriver = {
+ driverKind: DRIVER_KIND,
+ metadata: {
+ displayName: "OpenCode 2",
+ supportsMultipleInstances: true,
+ },
+ configSchema: OpenCode2Settings,
+ defaultConfig: (): OpenCode2Settings => decodeOpenCode2Settings({}),
+ create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
+ Effect.gen(function* () {
+ const openCode2Runtime = yield* OpenCode2Runtime;
+ const serverConfig = yield* ServerConfig;
+ const httpClient = yield* HttpClient.HttpClient;
+ const serverSettings = yield* ServerSettingsService;
+ const effectiveConfig = { ...config, enabled } satisfies OpenCode2Settings;
+ const processEnv = applyOpenCode2ProviderEnvironment(
+ effectiveConfig,
+ mergeProviderInstanceEnvironment(environment),
+ );
+ const continuationIdentity = defaultProviderContinuationIdentity({
+ driverKind: DRIVER_KIND,
+ instanceId,
+ });
+ const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(
+ openCode2ProviderMaintenanceResolver(effectiveConfig),
+ {
+ binaryPath: effectiveConfig.binaryPath,
+ env: processEnv,
+ },
+ );
+ const stampIdentity = withInstanceIdentity({
+ instanceId,
+ displayName,
+ accentColor,
+ continuationGroupKey: continuationIdentity.continuationKey,
+ });
+
+ const orchestrationAdapter = yield* OpenCode2AdapterV2Driver.create({
+ instanceId,
+ displayName,
+ accentColor,
+ environment,
+ enabled,
+ config,
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderDriverError({
+ driver: DRIVER_KIND,
+ instanceId,
+ detail: "Failed to build OpenCode 2 orchestration adapter.",
+ cause,
+ }),
+ ),
+ );
+ const textGeneration = yield* makeOpenCode2TextGeneration(effectiveConfig, processEnv);
+
+ const checkProvider = checkOpenCode2ProviderStatus(
+ effectiveConfig,
+ serverConfig.cwd,
+ processEnv,
+ ).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCode2Runtime, openCode2Runtime));
+
+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
+ const snapshot = yield* makeManagedServerProvider<
+ ProviderSnapshotSettings
+ >({
+ maintenanceCapabilities,
+ getSettings: snapshotSettings.getSettings,
+ streamSettings: snapshotSettings.streamSettings,
+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
+ initialSnapshot: (settings) =>
+ makePendingOpenCode2Provider(settings.provider).pipe(Effect.map(stampIdentity)),
+ checkProvider,
+ enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
+ enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
+ }).pipe(
+ Effect.provideService(HttpClient.HttpClient, httpClient),
+ Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
+ ),
+ refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderDriverError({
+ driver: DRIVER_KIND,
+ instanceId,
+ detail: "Failed to build OpenCode 2 snapshot.",
+ cause,
+ }),
+ ),
+ );
+
+ return {
+ instanceId,
+ driverKind: DRIVER_KIND,
+ continuationIdentity,
+ displayName,
+ accentColor,
+ enabled,
+ snapshot,
+ orchestrationAdapter,
+ textGeneration,
+ } satisfies ProviderInstance;
+ }),
+};
diff --git a/apps/server/src/provider/Layers/OpenCode2Provider.agentSelection.test.ts b/apps/server/src/provider/Layers/OpenCode2Provider.agentSelection.test.ts
new file mode 100644
index 00000000000..33fa142d827
--- /dev/null
+++ b/apps/server/src/provider/Layers/OpenCode2Provider.agentSelection.test.ts
@@ -0,0 +1,111 @@
+// @ts-nocheck — inventory fixtures predate ModelV2Info/AgentV2Info shape.
+/* eslint-disable */
+// Model/agent fixtures are structural for inventory tests across SDK generations.
+import { describe, expect, it } from "vite-plus/test";
+
+import { flattenOpenCode2Models } from "./OpenCode2Provider.ts";
+
+const MODEL = {
+ id: "glm-5.2",
+ modelID: "glm-5.2",
+ providerID: "opencode",
+ name: "GLM-5.2",
+ capabilities: {
+ tools: true,
+ input: ["text"],
+ output: ["text"],
+ },
+ variants: [],
+ time: { released: 0 },
+ cost: [],
+ status: "active",
+ enabled: true,
+ limit: {
+ context: 128_000,
+ output: 16_384,
+ },
+} satisfies any;
+
+const BUILD_AGENT = {
+ id: "build",
+ name: "Build",
+ request: { settings: {}, headers: {}, body: {} },
+ mode: "primary",
+ hidden: false,
+ permissions: [],
+} satisfies any;
+
+const PLAN_AGENT = { ...BUILD_AGENT, id: "plan", name: "Plan" } satisfies any;
+
+describe("OpenCode 2 agent inventory", () => {
+ // The Build/Plan interaction-mode toggle owns the native pair, so no Agent
+ // descriptor appears unless custom primary agents exist.
+ it("suppresses the agent descriptor when only build and plan exist", () => {
+ const [model] = flattenOpenCode2Models({
+ models: [MODEL],
+ agents: [BUILD_AGENT, PLAN_AGENT],
+ });
+
+ expect(
+ model?.capabilities?.optionDescriptors?.some((candidate) => candidate.id === "agent"),
+ ).toBe(false);
+ });
+
+ it("suppresses the agent descriptor when the native pair is incomplete", () => {
+ const customAgent = {
+ ...BUILD_AGENT,
+ id: "release-captain",
+ name: "Release Captain",
+ } satisfies any;
+ const [model] = flattenOpenCode2Models({
+ models: [MODEL],
+ agents: [BUILD_AGENT, customAgent],
+ });
+ expect(
+ model?.capabilities?.optionDescriptors?.some((candidate) => candidate.id === "agent"),
+ ).toBe(false);
+ });
+
+ it("suppresses the agent descriptor when only plan and a custom agent exist", () => {
+ const customAgent = {
+ ...BUILD_AGENT,
+ id: "release-captain",
+ name: "Release Captain",
+ } satisfies any;
+ const [model] = flattenOpenCode2Models({
+ models: [MODEL],
+ agents: [PLAN_AGENT, customAgent],
+ });
+ expect(
+ model?.capabilities?.optionDescriptors?.some((candidate) => candidate.id === "agent"),
+ ).toBe(false);
+ });
+
+ it("keeps executable agent ids separate from title-cased labels", () => {
+ const customAgent = {
+ ...BUILD_AGENT,
+ id: "Release-Captain",
+ name: "Release Captain",
+ } satisfies any;
+ const [model] = flattenOpenCode2Models({
+ models: [MODEL],
+ agents: [customAgent, BUILD_AGENT, PLAN_AGENT],
+ });
+ const descriptor = model?.capabilities?.optionDescriptors?.find(
+ (candidate) => candidate.id === "agent",
+ );
+
+ // Custom agents ride behind the Auto sentinel; build/plan belong to the
+ // Build/Plan toggle and leave the submenu entirely.
+ expect(descriptor).toEqual({
+ id: "agent",
+ label: "Agent",
+ type: "select",
+ currentValue: "auto",
+ options: [
+ { id: "auto", label: "Auto (Build/Plan)" },
+ { id: "Release-Captain", label: "Release Captain" },
+ ],
+ });
+ });
+});
diff --git a/apps/server/src/provider/Layers/OpenCode2Provider.test.ts b/apps/server/src/provider/Layers/OpenCode2Provider.test.ts
new file mode 100644
index 00000000000..058ab2afc3c
--- /dev/null
+++ b/apps/server/src/provider/Layers/OpenCode2Provider.test.ts
@@ -0,0 +1,629 @@
+// @ts-nocheck — inventory fixtures predate ModelV2Info/AgentV2Info shape.
+/* eslint-disable */
+// Model/agent fixtures are structural for inventory tests across SDK generations.
+import { assert, it } from "@effect/vitest";
+import { OpenCode2Settings } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
+import * as TestClock from "effect/testing/TestClock";
+import { describe } from "vite-plus/test";
+
+import * as OpenCode2Runtime from "../opencode2Runtime.ts";
+import { parseGenericCliVersion } from "../providerSnapshot.ts";
+import {
+ checkOpenCode2ProviderStatus,
+ flattenOpenCode2Models,
+ isOpenCode2InventorySettlementError,
+ openCode2NextBuild,
+ parseOpenCode2Version,
+ settleOpenCode2Inventory,
+} from "./OpenCode2Provider.ts";
+
+const OPENCODE2_BANNER = "opencode2 v0.0.0-next-16339\n";
+const BIG_PICKLE_MODEL = {
+ id: "big-pickle",
+ modelID: "big-pickle",
+ providerID: "opencode",
+ name: "Big Pickle",
+ capabilities: {
+ tools: true,
+ input: ["text"],
+ output: ["text"],
+ },
+ variants: [],
+ time: {
+ released: 0,
+ },
+ cost: [],
+ status: "active",
+ enabled: true,
+ limit: {
+ context: 128_000,
+ output: 16_384,
+ },
+} satisfies any;
+const BIG_PICKLE_FAST_MODEL = {
+ ...BIG_PICKLE_MODEL,
+ id: "big-pickle-fast",
+ name: "Big Pickle Fast",
+} satisfies any;
+const OPENAI_MODEL = {
+ ...BIG_PICKLE_MODEL,
+ id: "gpt-test",
+ modelID: "gpt-test",
+ providerID: "openai",
+ name: "GPT Test",
+} satisfies any;
+const OPENCODE2_TEST_SETTINGS = Schema.decodeSync(OpenCode2Settings)({
+ binaryPath: "fake-opencode2",
+});
+const OPENCODE2_EXTERNAL_TEST_SETTINGS = Schema.decodeSync(OpenCode2Settings)({
+ binaryPath: "fake-opencode2",
+ serverPassword: "external-secret",
+ serverUrl: "http://127.0.0.1:9998",
+});
+
+function failingOpenCode2Runtime(
+ category: OpenCode2Runtime.OpenCode2RuntimeErrorCategory,
+ cause?: unknown,
+): OpenCode2Runtime.OpenCode2Runtime["Service"] {
+ const failure = new OpenCode2Runtime.OpenCode2RuntimeError({
+ operation: "startOpenCode2ServerProcess",
+ category,
+ cause,
+ });
+ return OpenCode2Runtime.OpenCode2Runtime.of({
+ startOpenCode2ServerProcess: () => Effect.fail(failure),
+ connectToOpenCode2Server: () => Effect.fail(failure),
+ createOpenCode2SdkClient: () => {
+ throw new Error("unexpected SDK client creation");
+ },
+ });
+}
+
+function openCode2RuntimeWithHealthVersion(
+ version: string,
+ models: () => Array = () => [BIG_PICKLE_MODEL],
+): OpenCode2Runtime.OpenCode2Runtime["Service"] {
+ const client = {
+ global: {
+ health: async () => ({ data: { healthy: true as const, version } }),
+ },
+ v2: {
+ agent: {
+ list: async () => ({ data: { data: [BUILD_AGENT] } }),
+ },
+ health: {
+ get: async () => ({ data: { healthy: true as const, version } }),
+ },
+ integration: {
+ list: async () => ({
+ data: {
+ data: [
+ {
+ id: "opencode",
+ name: "OpenCode",
+ methods: [],
+ connections: [{ type: "env", name: "OPENCODE_TEST_KEY" }],
+ } satisfies IntegrationInfo,
+ ],
+ },
+ }),
+ },
+ model: {
+ list: async () => ({ data: { data: models() } }),
+ },
+ },
+ } as unknown as OpencodeClient;
+
+ return OpenCode2Runtime.OpenCode2Runtime.of({
+ startOpenCode2ServerProcess: () => Effect.die("unexpected server process start"),
+ connectToOpenCode2Server: () =>
+ Effect.succeed({
+ exitCode: null,
+ external: false,
+ password: "test-password",
+ url: "http://127.0.0.1:1234",
+ }),
+ createOpenCode2SdkClient: () => client,
+ });
+}
+
+const BUILD_AGENT = {
+ id: "build",
+ name: "Build",
+ request: { settings: {}, headers: {}, body: {} },
+ mode: "primary",
+ hidden: false,
+ permissions: [],
+} satisfies any;
+
+describe("parseOpenCode2Version", () => {
+ // The reason this parser exists: the generic one anchors on `\b`, and the
+ // `v` prefix kills the word boundary before the leading digit.
+ it("parses the banner the generic CLI parser returns null for", () => {
+ assert.strictEqual(parseGenericCliVersion(OPENCODE2_BANNER), null);
+ assert.strictEqual(parseOpenCode2Version(OPENCODE2_BANNER), "0.0.0-next-16339");
+ });
+
+ it("parses a plain release version", () => {
+ assert.strictEqual(parseOpenCode2Version("opencode2 2.1.4\n"), "2.1.4");
+ });
+
+ it("returns null when there is no version at all", () => {
+ assert.strictEqual(
+ parseOpenCode2Version("Error: @opencode-ai/cli's postinstall script was not run."),
+ null,
+ );
+ });
+});
+
+describe("openCode2NextBuild", () => {
+ it("reads the build number off the next line", () => {
+ assert.strictEqual(openCode2NextBuild("0.0.0-next-16339"), 16339);
+ });
+
+ // A stable 2.x is not on the preview line, so the build gate must not apply
+ // to it rather than rejecting it for lacking a build number.
+ it("returns null for a version that is not on the next line", () => {
+ assert.strictEqual(openCode2NextBuild("2.1.4"), null);
+ assert.strictEqual(openCode2NextBuild("2.1.4-rc.1"), null);
+ });
+});
+
+describe("checkOpenCode2ProviderStatus", () => {
+ it.effect("rejects local next builds below the verified floor with install guidance", () =>
+ Effect.gen(function* () {
+ const providerFiber = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ openCode2RuntimeWithHealthVersion("0.0.0-next-10000"),
+ ),
+ Effect.forkChild,
+ );
+
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("500 millis");
+ const provider = yield* Fiber.join(providerFiber);
+
+ assert.strictEqual(provider.status, "error");
+ assert.include(provider.message ?? "", "next-16339");
+ assert.include(provider.message ?? "", "npm install");
+ }),
+ );
+
+ it.effect("rejects external next builds below the verified floor with server guidance", () =>
+ Effect.gen(function* () {
+ const providerFiber = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_EXTERNAL_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ openCode2RuntimeWithHealthVersion("0.0.0-next-10000"),
+ ),
+ Effect.forkChild,
+ );
+
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("500 millis");
+ const provider = yield* Fiber.join(providerFiber);
+
+ assert.strictEqual(provider.status, "error");
+ assert.include(provider.message ?? "", "configured OpenCode 2 server");
+ assert.include(provider.message ?? "", "next-16339");
+ assert.notInclude(provider.message ?? "", "npm install");
+ }),
+ );
+
+ it.effect("does not include a minted startup password in provider status", () =>
+ Effect.gen(function* () {
+ const password = "MINTED_PROVIDER_STATUS_PASSWORD";
+ const runtime = failingOpenCode2Runtime("startup-failed", new Error(password));
+ const provider = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(Effect.provideService(OpenCode2Runtime.OpenCode2Runtime, runtime));
+
+ assert.notInclude(provider.message ?? "", password);
+ assert.include(provider.message ?? "", "startup-failed");
+ }),
+ );
+
+ it.effect("preserves safe package and executable diagnostics", () =>
+ Effect.gen(function* () {
+ const placeholder = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ failingOpenCode2Runtime("placeholder-binary"),
+ ),
+ );
+ const missing = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ failingOpenCode2Runtime("binary-not-found"),
+ ),
+ );
+
+ assert.isFalse(placeholder.installed);
+ assert.include(placeholder.message ?? "", "postinstall script never ran");
+ assert.isFalse(missing.installed);
+ assert.include(missing.message ?? "", "not installed or not on PATH");
+ }),
+ );
+
+ it.effect("preserves generic health version diagnostics", () =>
+ Effect.gen(function* () {
+ const providerFiber = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ openCode2RuntimeWithHealthVersion("not-a-version"),
+ ),
+ Effect.forkChild,
+ );
+
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("500 millis");
+ const provider = yield* Fiber.join(providerFiber);
+
+ assert.include(
+ provider.message ?? "",
+ "Unable to determine OpenCode 2 version from `/global/health` or `/api/health`.",
+ );
+ }),
+ );
+
+ it.effect("reports inventory instability without blaming the health check", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const providerFiber = yield* checkOpenCode2ProviderStatus(
+ OPENCODE2_TEST_SETTINGS,
+ "/workspace",
+ {},
+ ).pipe(
+ Effect.provideService(
+ OpenCode2Runtime.OpenCode2Runtime,
+ openCode2RuntimeWithHealthVersion("0.0.0-next-16339", () => {
+ reads += 1;
+ return [{ ...BIG_PICKLE_MODEL, id: `big-pickle-${reads}` }];
+ }),
+ ),
+ Effect.forkChild,
+ );
+
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("6 seconds");
+ const provider = yield* Fiber.join(providerFiber);
+
+ assert.strictEqual(provider.status, "error");
+ assert.include(provider.message ?? "", "inventory did not stabilize");
+ assert.notInclude(provider.message ?? "", "health check");
+ }),
+ );
+});
+
+describe("settleOpenCode2Inventory", () => {
+ it.effect("uses a 500ms healthy-path floor by default", () =>
+ Effect.gen(function* () {
+ const reads = yield* Ref.make(0);
+ const settlement = yield* settleOpenCode2Inventory(
+ Ref.update(reads, (count) => count + 1).pipe(
+ Effect.as({
+ models: [BIG_PICKLE_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: ["opencode"],
+ }),
+ ),
+ ).pipe(Effect.forkChild);
+
+ yield* Effect.yieldNow;
+ yield* TestClock.adjust("499 millis");
+ assert.strictEqual(yield* Ref.get(reads), 5);
+
+ yield* TestClock.adjust("1 millis");
+ yield* Fiber.join(settlement);
+ assert.strictEqual(yield* Ref.get(reads), 6);
+ }),
+ );
+
+ it.effect("waits through a non-empty baseline until connected integrations settle", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return reads < 4
+ ? { models: [BIG_PICKLE_MODEL], agents: [BUILD_AGENT], connectedIntegrationIDs: [] }
+ : {
+ models: [BIG_PICKLE_MODEL, OPENAI_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: ["openai", "opencode"],
+ };
+ }),
+ { maxAttempts: 6, minimumAttempts: 4, quietAttempts: 2, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 5);
+ assert.deepStrictEqual(inventory.connectedIntegrationIDs, ["openai", "opencode"]);
+ assert.deepStrictEqual(
+ inventory.models.map((model) => model.providerID),
+ ["opencode", "openai"],
+ );
+ }),
+ );
+
+ it.effect("returns a logged-out free catalog only at the bounded deadline", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return {
+ models: [BIG_PICKLE_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: [],
+ };
+ }),
+ { maxAttempts: 6, minimumAttempts: 4, quietAttempts: 2, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 6);
+ assert.deepStrictEqual(inventory.models, [BIG_PICKLE_MODEL]);
+ assert.deepStrictEqual(inventory.connectedIntegrationIDs, []);
+ }),
+ );
+
+ it.effect("keeps the last settled catalog when the final attempt changes", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return reads < 6
+ ? { models: [BIG_PICKLE_MODEL], agents: [BUILD_AGENT], connectedIntegrationIDs: [] }
+ : {
+ models: [BIG_PICKLE_MODEL, OPENAI_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: ["openai", "opencode"],
+ };
+ }),
+ { maxAttempts: 6, minimumAttempts: 4, quietAttempts: 2, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 6);
+ assert.deepStrictEqual(inventory.connectedIntegrationIDs, []);
+ assert.deepStrictEqual(
+ inventory.models.map((model) => model.providerID),
+ ["opencode"],
+ );
+ }),
+ );
+
+ it.effect("fails when no catalog fingerprint stabilizes", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const error = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return {
+ models: [{ ...BIG_PICKLE_MODEL, id: `big-pickle-${reads}` }],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: [],
+ };
+ }),
+ { maxAttempts: 6, minimumAttempts: 4, quietAttempts: 2, retryDelayMs: 0 },
+ ).pipe(Effect.flip);
+
+ assert.strictEqual(reads, 6);
+ assert.ok(isOpenCode2InventorySettlementError(error));
+ assert.strictEqual(error.attempts, 6);
+ assert.strictEqual(
+ error.message,
+ "OpenCode 2 inventory did not stabilize before the retry limit.",
+ );
+ }),
+ );
+
+ it.effect("ignores a non-model integration when another connection supplies models", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return {
+ models: [BIG_PICKLE_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: ["openai", "opencode"],
+ };
+ }),
+ { maxAttempts: 3, minimumAttempts: 2, quietAttempts: 2, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 2);
+ assert.deepStrictEqual(inventory.connectedIntegrationIDs, ["openai", "opencode"]);
+ }),
+ );
+
+ it.effect("stops at the deadline when no connected integration supplies models", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return {
+ models: [BIG_PICKLE_MODEL],
+ agents: [BUILD_AGENT],
+ connectedIntegrationIDs: ["openai"],
+ };
+ }),
+ { maxAttempts: 3, minimumAttempts: 2, quietAttempts: 2, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 3);
+ assert.deepStrictEqual(inventory.connectedIntegrationIDs, ["openai"]);
+ }),
+ );
+
+ it.effect("stops at the deadline when the catalog stays empty", () =>
+ Effect.gen(function* () {
+ let reads = 0;
+ const inventory = yield* settleOpenCode2Inventory(
+ Effect.sync(() => {
+ reads += 1;
+ return { models: [], agents: [], connectedIntegrationIDs: [] };
+ }),
+ { maxAttempts: 3, retryDelayMs: 0 },
+ );
+
+ assert.strictEqual(reads, 3);
+ assert.deepStrictEqual(inventory, {
+ models: [],
+ agents: [],
+ connectedIntegrationIDs: [],
+ });
+ }),
+ );
+});
+
+describe("flattenOpenCode2Models", () => {
+ it("uses a readable upstream provider label", () => {
+ assert.deepStrictEqual(flattenOpenCode2Models({ models: [BIG_PICKLE_MODEL], agents: [] }), [
+ {
+ slug: "opencode/big-pickle",
+ name: "Big Pickle",
+ subProvider: "OpenCode",
+ isCustom: false,
+ capabilities: {
+ optionDescriptors: [],
+ },
+ },
+ ]);
+ });
+
+ it("uses the selectable model ref id when models share an underlying model id", () => {
+ assert.deepStrictEqual(
+ flattenOpenCode2Models({
+ models: [BIG_PICKLE_MODEL, BIG_PICKLE_FAST_MODEL],
+ agents: [],
+ }).map((model) => model.slug),
+ ["opencode/big-pickle", "opencode/big-pickle-fast"],
+ );
+ });
+
+ it("keeps a structured model whose id contains a slash", () => {
+ const slashModel = {
+ ...BIG_PICKLE_MODEL,
+ id: "qwen/qwen3-coder",
+ modelID: "qwen/qwen3-coder",
+ providerID: "openrouter",
+ name: "qwen3-coder",
+ } satisfies any;
+
+ // Unlike the 1.x text parser fixed by #5072 opencode-model-slug-misclassification,
+ // 2.x receives a structured SDK model and constructs the selectable
+ // provider/model ref directly.
+ assert.deepStrictEqual(
+ flattenOpenCode2Models({ models: [slashModel], agents: [] }).map((model) => model.slug),
+ ["openrouter/qwen/qwen3-coder"],
+ );
+ });
+
+ it("marks the inferred reasoning default without a synthetic Default option", () => {
+ const [model] = flattenOpenCode2Models({
+ models: [
+ {
+ ...BIG_PICKLE_MODEL,
+ variants: [
+ { id: "low" },
+ { id: "medium" },
+ { id: "high" },
+ { id: "xhigh" },
+ { id: "max" },
+ ],
+ },
+ ],
+ agents: [],
+ });
+
+ assert.deepStrictEqual(model?.capabilities?.optionDescriptors, [
+ {
+ id: "variant",
+ label: "Reasoning",
+ type: "select",
+ currentValue: "medium",
+ options: [
+ { id: "low", label: "Low" },
+ { id: "medium", label: "Medium", isDefault: true },
+ { id: "high", label: "High" },
+ { id: "xhigh", label: "Extra High" },
+ { id: "max", label: "Max" },
+ ],
+ },
+ ]);
+ });
+
+ it("hides a catalog-supplied Default sentinel", () => {
+ const [model] = flattenOpenCode2Models({
+ models: [
+ {
+ ...BIG_PICKLE_MODEL,
+ variants: [{ id: "default" }, { id: "high" }],
+ },
+ ],
+ agents: [],
+ });
+ const descriptor = model?.capabilities?.optionDescriptors?.find(
+ (candidate) => candidate.id === "variant",
+ );
+
+ assert.deepStrictEqual(descriptor?.type === "select" ? descriptor.options : [], [
+ { id: "high", label: "High", isDefault: true },
+ ]);
+ });
+
+ it("chooses a concrete fallback default for a thinking toggle", () => {
+ const [model] = flattenOpenCode2Models({
+ models: [
+ {
+ ...BIG_PICKLE_MODEL,
+ variants: [{ id: "none" }, { id: "thinking" }],
+ },
+ ],
+ agents: [],
+ });
+ const descriptor = model?.capabilities?.optionDescriptors?.find(
+ (candidate) => candidate.id === "variant",
+ );
+
+ assert.deepStrictEqual(descriptor, {
+ id: "variant",
+ label: "Reasoning",
+ type: "select",
+ currentValue: "thinking",
+ options: [
+ { id: "none", label: "None" },
+ { id: "thinking", label: "Thinking", isDefault: true },
+ ],
+ });
+ });
+});
diff --git a/apps/server/src/provider/Layers/OpenCode2Provider.ts b/apps/server/src/provider/Layers/OpenCode2Provider.ts
new file mode 100644
index 00000000000..1e9a0f0f77a
--- /dev/null
+++ b/apps/server/src/provider/Layers/OpenCode2Provider.ts
@@ -0,0 +1,642 @@
+/**
+ * Provider status probe for OpenCode 2.x.
+ *
+ * 1.x's probe cannot be reused. It parses ` --version` with
+ * `parseGenericCliVersion` and enumerates models with ` models
+ * --verbose` / ` agent list`, and 2.x breaks both:
+ *
+ * - `/api/health` reports the running server's version. Reading it beside
+ * inventory avoids launching the binary a second time for `--version`.
+ * - the inventory subcommands are gone. 2.x's default handler treats the
+ * first argument as a directory to `chdir` into, logs `ENOENT`, and exits
+ * 0, so the probe cannot even detect its own failure by exit code.
+ * Inventory moved to `/api/model` and `/api/agent`.
+ *
+ * @module provider/Layers/OpenCode2Provider
+ */
+import type { AgentV2Info, ModelV2Info } from "@opencode-ai/sdk-next/v2";
+
+type IntegrationInfo = {
+ readonly id: string;
+ readonly connections: ReadonlyArray;
+ readonly [key: string]: unknown;
+};
+
+import {
+ type ModelCapabilities,
+ type OpenCode2Settings,
+ type ServerProviderModel,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as Schema from "effect/Schema";
+
+import { createModelCapabilities } from "@t3tools/shared/model";
+import {
+ OPENCODE2_AUTO_AGENT,
+ OPENCODE2_DEFAULT_VARIANT,
+ isOpenCode2RuntimeError,
+ OpenCode2Runtime,
+ OpenCode2RuntimeError,
+ runOpenCode2Sdk,
+} from "../opencode2Runtime.ts";
+import {
+ buildServerProvider,
+ inferOpenCodeDefaultVariant,
+ nonEmptyTrimmed,
+ providerModelsFromSettings,
+ type ServerProviderDraft,
+} from "../providerSnapshot.ts";
+
+// The Build/Plan toggle maps onto opencode2's native `build`/`plan` primary
+// agents (resolveOpenCode2SessionAgent in the adapter), so the toggle shows
+// and the Agent descriptor is suppressed unless custom primary agents exist.
+const OPENCODE2_PRESENTATION = {
+ displayName: "OpenCode 2",
+ showInteractionModeToggle: true,
+} as const;
+
+/**
+ * The `next` build this driver's runtime, event mapping, and route usage were
+ * verified against. 2.x has no meaningful semver axis yet — every build on the
+ * line is `0.0.0-next-` — so the build number is the only ordering that
+ * carries information, and it is only compared when the version still carries
+ * a `next` tag. A future stable 2.x is accepted as-is rather than rejected by
+ * a rule written for the preview line.
+ */
+const MINIMUM_OPENCODE2_NEXT_BUILD = 16339;
+
+/**
+ * Accepts both the plain `/api/health` value and the `v` prefix emitted by the
+ * CLI, which `parseGenericCliVersion` chokes on.
+ *
+ * @internal exported for tests
+ */
+export function parseOpenCode2Version(output: string): string | null {
+ return output.match(/v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?)/)?.[1] ?? null;
+}
+
+/**
+ * Build number out of a `0.0.0-next-16339` style version, or `null` when the
+ * version is not on the `next` line and the build gate does not apply.
+ *
+ * @internal exported for tests
+ */
+export function openCode2NextBuild(version: string): number | null {
+ const match = version.match(/-next[.-](\d+)/);
+ if (!match) return null;
+ const build = Number(match[1]);
+ return Number.isFinite(build) ? build : null;
+}
+
+function normalizeProbeMessage(message: string): string | undefined {
+ const trimmed = message.trim();
+ if (trimmed.length === 0) return undefined;
+ if (
+ trimmed === "An error occurred in Effect.tryPromise" ||
+ trimmed === "An error occurred in Effect.try"
+ ) {
+ return undefined;
+ }
+ return trimmed;
+}
+
+function normalizedErrorMessage(cause: unknown): string | undefined {
+ if (!(cause instanceof Error)) return undefined;
+ return normalizeProbeMessage(cause.message);
+}
+
+function formatOpenCode2ProbeError(input: {
+ readonly cause: unknown;
+ readonly isExternalServer: boolean;
+ readonly serverUrl: string;
+}): { readonly installed: boolean; readonly message: string } {
+ if (isOpenCode2InventorySettlementError(input.cause)) {
+ return {
+ installed: true,
+ message: "OpenCode 2 inventory did not stabilize before the retry limit.",
+ };
+ }
+ const detail = normalizedErrorMessage(input.cause);
+ const lower = detail?.toLowerCase() ?? "";
+ const category = isOpenCode2RuntimeError(input.cause) ? input.cause.category : null;
+
+ if (input.isExternalServer) {
+ if (category === "external-server-password-required") {
+ return {
+ installed: true,
+ message:
+ "The configured OpenCode 2 server requires a password. OpenCode 2 has no unauthenticated mode.",
+ };
+ }
+ if (
+ category === "authentication-failed" ||
+ lower.includes("401") ||
+ lower.includes("403") ||
+ lower.includes("unauthorized") ||
+ lower.includes("forbidden")
+ ) {
+ return {
+ installed: true,
+ message:
+ "OpenCode 2 server rejected authentication. Check the server URL and password. OpenCode 2 has no unauthenticated mode.",
+ };
+ }
+ if (
+ category === "network-failed" ||
+ lower.includes("econnrefused") ||
+ lower.includes("enotfound") ||
+ lower.includes("fetch failed") ||
+ lower.includes("networkerror") ||
+ lower.includes("timed out") ||
+ lower.includes("timeout") ||
+ lower.includes("socket hang up")
+ ) {
+ return {
+ installed: true,
+ message: `Couldn't reach the configured OpenCode 2 server at ${input.serverUrl}. Check that the server is running and the URL is correct.`,
+ };
+ }
+ return {
+ installed: true,
+ message: detail ?? "Failed to connect to the configured OpenCode 2 server.",
+ };
+ }
+
+ if (category === "binary-not-found" || lower.includes("enoent") || lower.includes("notfound")) {
+ return {
+ installed: false,
+ message: "OpenCode 2 CLI (`opencode2`) is not installed or not on PATH.",
+ };
+ }
+ if (category === "placeholder-binary" || lower.includes("postinstall")) {
+ return {
+ installed: false,
+ message:
+ "The `@opencode-ai/cli` package shipped its placeholder binary: its postinstall script never ran. Reinstall with dependency build scripts enabled.",
+ };
+ }
+ if (category === "quarantined-binary" || lower.includes("quarantine")) {
+ return {
+ installed: true,
+ message:
+ "macOS is blocking the OpenCode 2 binary (quarantine). Run `xattr -d com.apple.quarantine $(which opencode2)` to fix this.",
+ };
+ }
+ return {
+ installed: true,
+ message: detail
+ ? `Failed to execute OpenCode 2 CLI health check: ${detail}`
+ : "Failed to execute OpenCode 2 CLI health check.",
+ };
+}
+
+function titleCaseSlug(value: string): string {
+ if (value === "opencode") return "OpenCode";
+ if (value === "openai") return "OpenAI";
+ if (value === "xai") return "xAI";
+ const segments: Array = [];
+ for (const segment of value.split(/[-_/]+/)) {
+ if (segment.length > 0) segments.push(segment.charAt(0).toUpperCase() + segment.slice(1));
+ }
+ return segments.join(" ");
+}
+
+const DEFAULT_OPENCODE2_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({
+ optionDescriptors: [],
+});
+
+export interface OpenCode2Inventory {
+ readonly models: ReadonlyArray;
+ readonly agents: ReadonlyArray;
+}
+
+export interface OpenCode2InventorySnapshot extends OpenCode2Inventory {
+ readonly connectedIntegrationIDs: ReadonlyArray;
+}
+
+interface OpenCode2InventoryResult {
+ readonly inventory: OpenCode2InventorySnapshot;
+ readonly version: string | null;
+}
+
+interface OpenCode2InventorySettlementOptions {
+ readonly maxAttempts?: number;
+ readonly minimumAttempts?: number;
+ readonly quietAttempts?: number;
+ readonly retryDelayMs?: number;
+}
+
+function openCode2InventoryFingerprint(inventory: OpenCode2InventorySnapshot): string {
+ return JSON.stringify({
+ agents: inventory.agents
+ .map((agent) => [agent.id, agent.mode, agent.hidden] as const)
+ .toSorted(([left], [right]) => String(left).localeCompare(String(right))),
+ connectedIntegrationIDs: inventory.connectedIntegrationIDs.toSorted(),
+ models: inventory.models
+ .map(
+ (model) =>
+ [
+ model.providerID,
+ model.id,
+ model.name,
+ model.enabled,
+ (model.variants ?? [])
+ .map((variant) => (typeof variant === "string" ? variant : String(variant?.id ?? "")))
+ .toSorted(),
+ ] as const,
+ )
+ .toSorted(([leftProvider, leftModel], [rightProvider, rightModel]) =>
+ String(leftProvider) === String(rightProvider)
+ ? String(leftModel).localeCompare(String(rightModel))
+ : String(leftProvider).localeCompare(String(rightProvider)),
+ ),
+ });
+}
+
+function openCode2InventoryIsUsable(inventory: OpenCode2InventorySnapshot): boolean {
+ const enabledModels = inventory.models.filter((model) => model.enabled);
+ if (enabledModels.length === 0) return false;
+ if (inventory.connectedIntegrationIDs.length === 0) return false;
+ const modelProviders = new Set(enabledModels.map((model) => model.providerID));
+ return inventory.connectedIntegrationIDs.some((integrationID) =>
+ modelProviders.has(integrationID),
+ );
+}
+
+export class OpenCode2InventorySettlementError extends Schema.TaggedErrorClass()(
+ "OpenCode2InventorySettlementError",
+ { attempts: Schema.Int },
+) {
+ override get message(): string {
+ return "OpenCode 2 inventory did not stabilize before the retry limit.";
+ }
+}
+
+export const isOpenCode2InventorySettlementError = Schema.is(OpenCode2InventorySettlementError);
+
+/**
+ * A newly spawned 2.x server prints its ready banner before plugin settlement.
+ * The first non-empty model snapshot may therefore contain only baseline free
+ * models while authenticated integrations are still loading. Observe several
+ * snapshots, require a quiet interval after the minimum observation window,
+ * and keep waiting until at least one model-bearing connected integration is
+ * visible. The attempt cap is the only logged-out completion signal and also
+ * bounds broken-plugin and local-only cases.
+ *
+ * @internal exported for tests
+ */
+export const settleOpenCode2Inventory = Effect.fn("settleOpenCode2Inventory")(function* (
+ readInventory: Effect.Effect,
+ options?: OpenCode2InventorySettlementOptions,
+): Effect.fn.Return {
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 51);
+ const minimumAttempts = Math.min(maxAttempts, Math.max(1, options?.minimumAttempts ?? 6));
+ const quietAttempts = Math.max(1, options?.quietAttempts ?? 2);
+ const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 100);
+ let inventory = yield* readInventory;
+ let fingerprint = openCode2InventoryFingerprint(inventory);
+ let consecutiveMatches = 1;
+ let settledInventory: OpenCode2InventorySnapshot | null = null;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
+ if (attempt >= minimumAttempts && consecutiveMatches >= quietAttempts) {
+ settledInventory = inventory;
+ if (openCode2InventoryIsUsable(inventory)) return inventory;
+ }
+ if (attempt === maxAttempts) break;
+ yield* Effect.sleep(retryDelayMs);
+ inventory = yield* readInventory;
+ const nextFingerprint = openCode2InventoryFingerprint(inventory);
+ consecutiveMatches = nextFingerprint === fingerprint ? consecutiveMatches + 1 : 1;
+ fingerprint = nextFingerprint;
+ }
+
+ if (settledInventory !== null) return settledInventory;
+ return yield* new OpenCode2InventorySettlementError({ attempts: maxAttempts });
+});
+
+const OPENCODE2_VARIANT_LABELS: Record = {
+ xhigh: "Extra High",
+};
+
+function inferOpenCode2DefaultVariant(
+ providerID: string,
+ variants: ReadonlyArray,
+): string | undefined {
+ return (
+ inferOpenCodeDefaultVariant(providerID, variants) ??
+ variants.find((variant) => variant === "medium") ??
+ variants.find((variant) => variant === "high") ??
+ variants.find((variant) => variant !== "none") ??
+ variants[0]
+ );
+}
+
+/**
+ * Variants are opencode2's reasoning axis (effort ladders, thinking toggles,
+ * budget tiers), synthesized per model from models.dev capability flags.
+ */
+function openCode2CapabilitiesForModel(input: {
+ readonly model: ModelV2Info;
+ readonly agents: ReadonlyArray;
+}): ModelCapabilities {
+ const variantValues = (input.model.variants ?? [])
+ .map((variant) => variant.id)
+ .filter((variantId): variantId is string => typeof variantId === "string")
+ .filter((variant) => variant !== OPENCODE2_DEFAULT_VARIANT);
+ const defaultVariant = inferOpenCode2DefaultVariant(input.model.providerID, variantValues);
+ const variantOptions = variantValues.map((variant) => {
+ const option = {
+ id: variant,
+ label: OPENCODE2_VARIANT_LABELS[variant] ?? titleCaseSlug(variant),
+ };
+ return variant === defaultVariant ? { ...option, isDefault: true as const } : option;
+ });
+ const primaryAgents = input.agents.filter(
+ (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"),
+ );
+ // The standalone Build/Plan interaction-mode toggle owns both native agents.
+ // Never expose either one through model options, including while startup has
+ // reported only half of the pair. Custom agents remain available behind an
+ // Auto sentinel that defers to the toggle.
+ const customAgents = primaryAgents.filter(
+ (agent) => agent.id !== "build" && agent.id !== "plan" && agent.id !== OPENCODE2_AUTO_AGENT,
+ );
+ const hasNativeAgentPair =
+ primaryAgents.some((agent) => agent.id === "build") &&
+ primaryAgents.some((agent) => agent.id === "plan");
+ const agentOptions =
+ !hasNativeAgentPair || customAgents.length === 0
+ ? []
+ : [
+ { id: OPENCODE2_AUTO_AGENT, label: "Auto (Build/Plan)" },
+ ...customAgents.map((agent) => ({
+ id: agent.id,
+ label: titleCaseSlug(agent.id),
+ })),
+ ];
+ const defaultVariantSelection = defaultVariant ? { currentValue: defaultVariant } : {};
+ return createModelCapabilities({
+ optionDescriptors: [
+ ...(variantOptions.length > 0
+ ? [
+ {
+ id: "variant",
+ label: "Reasoning",
+ type: "select" as const,
+ options: variantOptions,
+ ...defaultVariantSelection,
+ },
+ ]
+ : []),
+ ...(agentOptions.length > 0
+ ? [
+ {
+ id: "agent",
+ label: "Agent",
+ type: "select" as const,
+ options: agentOptions,
+ currentValue: OPENCODE2_AUTO_AGENT,
+ },
+ ]
+ : []),
+ ],
+ });
+}
+
+export function flattenOpenCode2Models(
+ inventory: OpenCode2Inventory,
+): ReadonlyArray {
+ const models: Array = [];
+ for (const model of inventory.models) {
+ if (!model.enabled) continue;
+ const name = nonEmptyTrimmed(model.name);
+ const providerID = nonEmptyTrimmed(model.providerID);
+ const modelID = nonEmptyTrimmed(model.id);
+ if (!name || !providerID || !modelID) continue;
+ models.push({
+ slug: `${providerID}/${modelID}`,
+ name,
+ subProvider: titleCaseSlug(providerID),
+ isCustom: false,
+ capabilities: openCode2CapabilitiesForModel({ model, agents: inventory.agents }),
+ });
+ }
+ return models.toSorted((left, right) => left.name.localeCompare(right.name));
+}
+
+/**
+ * Reads the 2.x inventory and version over HTTP, spawning a server when none
+ * is configured. `/api/model` and `/api/agent` replaced the inventory CLI
+ * subcommands. Version comes from `/global/health` (beta) with a fallback to
+ * `/api/health` for older next builds that still stamp version there.
+ */
+const loadOpenCode2Inventory = (input: {
+ readonly runtime: OpenCode2Runtime["Service"];
+ readonly settings: OpenCode2Settings;
+ readonly cwd: string;
+ readonly environment: NodeJS.ProcessEnv;
+}): Effect.Effect<
+ OpenCode2InventoryResult,
+ OpenCode2InventorySettlementError | OpenCode2RuntimeError
+> =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const server = yield* input.runtime.connectToOpenCode2Server({
+ binaryPath: input.settings.binaryPath,
+ serverUrl: input.settings.serverUrl,
+ serverPassword: input.settings.serverPassword,
+ environment: input.environment,
+ });
+ const client = input.runtime.createOpenCode2SdkClient({
+ baseUrl: server.url,
+ directory: input.cwd,
+ serverPassword: server.password,
+ });
+ const location = { directory: input.cwd };
+ const [inventory, healthResponse] = yield* Effect.all(
+ [
+ settleOpenCode2Inventory(
+ Effect.gen(function* () {
+ const [modelResponse, agentResponse, integrationResponse] = yield* Effect.all(
+ [
+ runOpenCode2Sdk("model.list", () => client.v2.model.list({ location })),
+ runOpenCode2Sdk("agent.list", () => client.v2.agent.list({ location })),
+ runOpenCode2Sdk("integration.list", () =>
+ client.v2.integration.list({ location }),
+ ),
+ ],
+ { concurrency: "unbounded" },
+ );
+ return {
+ models: modelResponse.data?.data ?? [],
+ agents: agentResponse.data?.data ?? [],
+ connectedIntegrationIDs: (integrationResponse.data?.data ?? [])
+ .filter((integration: IntegrationInfo) => integration.connections.length > 0)
+ .map((integration: IntegrationInfo) => integration.id),
+ } satisfies OpenCode2InventorySnapshot;
+ }),
+ ),
+ runOpenCode2Sdk("health.get", () =>
+ client.global.health().catch(() => client.v2.health.get()),
+ ),
+ ],
+ { concurrency: "unbounded" },
+ );
+ const healthBody = healthResponse.data as
+ | { readonly version?: string; readonly data?: { readonly version?: string } }
+ | undefined;
+ const versionRaw = healthBody?.version ?? healthBody?.data?.version ?? "";
+ return {
+ inventory,
+ version: parseOpenCode2Version(String(versionRaw)),
+ };
+ }),
+ );
+
+export const makePendingOpenCode2Provider = (
+ settings: OpenCode2Settings,
+): Effect.Effect =>
+ Effect.gen(function* () {
+ const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
+ const models = providerModelsFromSettings(
+ [],
+ settings.customModels,
+ DEFAULT_OPENCODE2_MODEL_CAPABILITIES,
+ );
+ return buildServerProvider({
+ presentation: OPENCODE2_PRESENTATION,
+ enabled: settings.enabled,
+ checkedAt,
+ models,
+ probe: {
+ installed: false,
+ version: null,
+ status: "warning",
+ auth: { status: "unknown" },
+ message: settings.enabled
+ ? "OpenCode 2 provider status has not been checked in this session yet."
+ : "OpenCode 2 is disabled in T3 Code settings.",
+ },
+ });
+ });
+
+export const checkOpenCode2ProviderStatus = Effect.fn("checkOpenCode2ProviderStatus")(function* (
+ settings: OpenCode2Settings,
+ cwd: string,
+ environment?: NodeJS.ProcessEnv,
+): Effect.fn.Return