import { EventId, type OpenCodeSettings, ProviderDriverKind, ProviderInstanceId, type ProviderRuntimeEvent, type ProviderSendTurnInput, type ProviderSession, RuntimeItemId, RuntimeRequestId, ThreadId, type ToolLifecycleItemType, type TurnTokenUsage, TurnId, type UserInputQuestion,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Fiber from "effect/Fiber";import * as Path from "effect/Path";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as Stream from "effect/Stream";import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2";import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import * as McpProviderSession from "../../mcp/McpProviderSession.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError,} from "../Errors.ts";import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";import { buildOpenCodePermissionRules, OpenCodeRuntime, OpenCodeRuntimeError, openCodeQuestionId, openCodeRuntimeErrorDetail, parseOpenCodeModelSlug, runOpenCodeSdk, toOpenCodeFileParts, toOpenCodePermissionReply, toOpenCodeQuestionAnswers, type OpenCodeServerConnection,} from "../opencodeRuntime.ts";import * as Option from "effect/Option"; const PROVIDER = ProviderDriverKind.make("opencode"); /** * Version tag stamped into the OpenCode resume cursor. Bump if the cursor * shape changes so stale-shaped cursors written by older builds are ignored * rather than misread (mirrors GROK_RESUME_VERSION / CURSOR_RESUME_VERSION). */const OPENCODE_RESUME_VERSION = 1 as const; /** * Decode a persisted resume cursor into the upstream `ses_…` id. Anything * that isn't a current-version cursor with a non-empty id means "no resume" * rather than an error. Re-adopting the session id IS the resume mechanism — * OpenCode scopes a conversation's history by session id. */function parseOpenCodeResume(raw: unknown): { readonly sessionId: string } | undefined { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { return undefined; } const record = raw as Record<string, unknown>; if (record.schemaVersion !== OPENCODE_RESUME_VERSION) { return undefined; } if (typeof record.sessionId !== "string" || record.sessionId.trim().length === 0) { return undefined; } return { sessionId: record.sessionId.trim() };} /** * Whether an error definitively reports a missing session. Only a confirmed * miss may silently start a fresh session; any other failure (the SDK client * is `throwOnError: true`, so `session.get` rejects on every non-2xx) must * propagate, or a transient blip resets a live thread to an empty one — the * #3604 silent context loss. Decides on structured signals only, never free * text: a numeric 404 or the exact `NotFoundError` name, found via a bounded walk * over `cause`/`body`/`error`/`data`. An explicit non-404 status seals its * subtree so a wrapped "NotFound" name can't reclassify a real failure. * Exported for unit testing. */export function isOpenCodeNotFound(cause: unknown): boolean { const seen = new Set<unknown>(); const queue: Array<unknown> = [cause]; for (let steps = 0; queue.length > 0 && steps < 32; steps += 1) { const node = queue.shift(); if (node === null || typeof node !== "object" || seen.has(node)) { continue; } seen.add(node); const record = node as Record<string, unknown>; const response = record.response; const statuses = [ record.status, record.statusCode, response !== null && typeof response === "object" ? (response as { readonly status?: unknown }).status : undefined, ].filter((status): status is number => typeof status === "number"); if (statuses.includes(404)) { return true; } if (statuses.length > 0) { continue; } const name = record.name; if (typeof name === "string" && name.toLowerCase() === "notfounderror") { return true; } for (const key of ["cause", "body", "error", "data"] as const) { if (record[key] !== undefined) { queue.push(record[key]); } } } return false;} /** * Whether two directory spellings name the same location. Raw string * equality misreads a trailing slash, `.`/`..` segment, or symlinked cwd * (macOS `/tmp` → `/private/tmp`) as a cwd change, needlessly forking the * session on every resume. Lexically equal paths short-circuit; otherwise * both sides go through `realPath`, each falling back to its lexical form * on failure (deleted directory, external-server path) — so the probe can * only widen matches, never split them. Takes the services as arguments so * adapter methods stay service-free. Exported for unit testing. */export function isSameOpenCodeDirectory( fileSystem: FileSystem.FileSystem, path: Path.Path, left: string, right: string,): Effect.Effect<boolean> { const lexicalLeft = path.resolve(left); const lexicalRight = path.resolve(right); if (lexicalLeft === lexicalRight) { return Effect.succeed(true); } const canonicalize = (lexical: string) => fileSystem.realPath(lexical).pipe(Effect.orElseSucceed(() => lexical)); return Effect.zipWith( canonicalize(lexicalLeft), canonicalize(lexicalRight), (canonicalLeft, canonicalRight) => canonicalLeft === canonicalRight, );} interface OpenCodeTurnSnapshot { readonly id: TurnId; readonly items: Array<unknown>;} type OpenCodeSubscribedEvent = Awaited<ReturnType<OpencodeClient["event"]["subscribe"]>> extends { readonly stream: AsyncIterable<infer TEvent>; } ? TEvent : never; type OpenCodeSessionStatusEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "session.status" }>; const OpenCodeSessionStatusMap = Schema.Record( Schema.String, Schema.Struct({ type: Schema.String }),);const decodeOpenCodeSessionStatusMap = Schema.decodeUnknownOption(OpenCodeSessionStatusMap); interface OpenCodeCancellation { readonly turnId: TurnId | undefined; readonly acknowledgment: Deferred.Deferred<void>; readonly completion: Deferred.Deferred<void, ProviderAdapterRequestError>; acknowledged?: boolean; turnSettled?: boolean; deferredIdleEvent?: OpenCodeSessionStatusEvent;} interface OpenCodeIdleReconciliation { readonly turnId: TurnId; readonly promptGeneration: number; raw: unknown; warned: boolean; dirty: boolean; fiber?: Fiber.Fiber<void, never>;} interface OpenCodePromptAdmission { readonly generation: number; readonly turnId: TurnId; readonly messageId: string; readonly priorAwaitingBusy: boolean; readonly priorIdle: { readonly turnId: TurnId; readonly raw: unknown } | undefined; idleDuringAdmission: { readonly turnId: TurnId; readonly raw: unknown } | undefined; idleObservedAfterMessage: boolean; messageObserved: boolean; busyObserved: boolean; idleStatusConfirmations: number; accepted: boolean; cancelled: boolean; readonly acceptance: Deferred.Deferred<void>; readonly submissionSettled: Deferred.Deferred<void>; promptFiber?: Fiber.Fiber<void, ProviderAdapterRequestError>; recoveryFiber?: Fiber.Fiber<void, never>; recoveryRaw: unknown;} type OpenCodeTerminalRequestEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "permission.replied" | "question.replied" | "question.rejected"; }>; type OpenCodeAskedRequestEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "permission.asked" | "question.asked" }>; type OpenCodeRoutedRequestEvent = OpenCodeAskedRequestEvent | OpenCodeTerminalRequestEvent; interface OpenCodeRequestRelationRetry { warned: boolean; fiber?: Fiber.Fiber<void, never>;} interface OpenCodePendingRequestRecovery { warned: boolean; rerun: boolean;} function trimText(value: string | undefined | null): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined;} function openCodeEventSessionId(event: OpenCodeSubscribedEvent): string | undefined { const properties = "properties" in event ? event.properties : undefined; if (!properties || typeof properties !== "object") { return undefined; } const sessionID = (properties as { readonly sessionID?: unknown }).sessionID; const sessionIDFromProperties = typeof sessionID === "string" ? sessionID : undefined; if (sessionIDFromProperties) { return sessionIDFromProperties; } const info = (properties as { readonly info?: { readonly id?: unknown } }).info; return info && typeof info.id === "string" ? info.id : undefined;} function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | undefined { if (event.type !== "session.updated") { return undefined; } const title = trimText(event.properties.info.title); // OpenCode mints a placeholder title at session.create when no title was // provided, and re-emits it on every `session.updated`. Mirroring it would // overwrite the thread's real title (openCodeEventSessionTitle feeds the // `thread.metadata.updated` mirror). Ignore OpenCode's auto-generated // placeholders so the thread isn't locked onto them. if (!title || isOpenCodeDefaultTitle(title)) { return undefined; } return title;} function isOpenCodeAbortError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "name" in error && error.name === "MessageAbortedError" );} function isOpenCodeChildRequestEvent(event: OpenCodeSubscribedEvent): boolean { switch (event.type) { case "permission.asked": case "permission.replied": case "question.asked": case "question.replied": case "question.rejected": return true; default: return false; }} const OPENCODE_DEFAULT_TITLE_PATTERN = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; function isOpenCodeDefaultTitle(title: string): boolean { return OPENCODE_DEFAULT_TITLE_PATTERN.test(title);} type OpenCodeTextPart = Extract<Part, { readonly type: "text" | "reasoning" }>; type OpenCodeTextPartState = Pick<OpenCodeTextPart, "id" | "messageID" | "type" | "time"> & { text: string | undefined; emittedText: string | undefined; completed: boolean;}; type OpenCodeStepUsage = Pick<Extract<Part, { readonly type: "step-finish" }>, "id" | "tokens">; interface OpenCodeSessionContext { session: ProviderSession; readonly client: OpencodeClient; readonly server: OpenCodeServerConnection; readonly directory: string; readonly openCodeSessionId: string; readonly relatedSessionIds: Set<string>; readonly resolvedRequestIds: Set<string>; readonly autoRepliedRequestIds: Set<string>; readonly emittedTerminalRequestIds: Set<string>; readonly requestRelationRetries: Map<string, OpenCodeRequestRelationRetry>; readonly pendingPermissions: Map<string, PermissionRequest>; readonly pendingQuestions: Map<string, QuestionRequest>; readonly messageRoleById: Map<string, "user" | "assistant">; // OpenCode permits edits to completed parts. Keep text for snapshot comparison // until native removal or session teardown, but do not retain other part payloads. readonly textPartsByMessageId: Map<string, Map<string, OpenCodeTextPartState>>; turnTokenUsage: OpenCodeTurnTokenUsageAccumulator | undefined; activeTurnId: TurnId | undefined; activeAgent: string | undefined; activeVariant: string | undefined; cancellation: OpenCodeCancellation | undefined; interruptedTurnId: TurnId | undefined; reconcileIdleStatus: boolean; awaitingBusyAfterInterruption: boolean; pendingIdleReconciliation: OpenCodeIdleReconciliation | undefined; pendingRequestRecovery: OpenCodePendingRequestRecovery | undefined; promptGeneration: number; promptAdmission: OpenCodePromptAdmission | undefined; readonly promptSemaphore: Semaphore.Semaphore; readonly firstConnection: Deferred.Deferred<void, ProviderAdapterRequestError>; /** * One-shot guard flipped by `stopOpenCodeContext` / `emitUnexpectedExit`. * The session lifecycle is owned by `sessionScope`; this Ref exists only * so concurrent callers can race the transition safely via `getAndSet`. */ readonly stopped: Ref.Ref<boolean>; /** * Sole lifecycle handle for the session. Closing this scope: * - aborts the `AbortController` registered as a finalizer * (cancels the in-flight `event.subscribe` fetch), * - interrupts the event-pump and server-exit fibers forked * via `Effect.forkIn(sessionScope)`, * - tears down the OpenCode server process for scope-owned servers. */ readonly sessionScope: Scope.Closeable;} interface OpenCodeTurnTokenUsageAccumulator { readonly partIds: Set<string>; readonly promptMessageIds: Set<string>; readonly assistantOwnershipByMessageId: Map<string, "owned" | "other" | "unknown">; // Native removal does not undo usage. Keep unresolved counts until this turn settles. readonly unresolvedStepsByMessageId: Map<string, Map<string, OpenCodeStepUsage>>; inputTokens: number; cachedInputTokens: number; cacheCreationTokens: number; outputTokens: number; reasoningTokens: number; complete: boolean; hasSubagents: boolean;} function makeOpenCodeTurnTokenUsageAccumulator(): OpenCodeTurnTokenUsageAccumulator { return { partIds: new Set(), promptMessageIds: new Set(), assistantOwnershipByMessageId: new Map(), unresolvedStepsByMessageId: new Map(), inputTokens: 0, cachedInputTokens: 0, cacheCreationTokens: 0, outputTokens: 0, reasoningTokens: 0, complete: true, hasSubagents: false, };} function accumulateOpenCodeStepUsage( accumulator: OpenCodeTurnTokenUsageAccumulator, part: OpenCodeStepUsage,): void { if (accumulator.partIds.has(part.id)) return; accumulator.partIds.add(part.id); accumulator.inputTokens += part.tokens.input + part.tokens.cache.read + part.tokens.cache.write; accumulator.cachedInputTokens += part.tokens.cache.read; accumulator.cacheCreationTokens += part.tokens.cache.write; accumulator.outputTokens += part.tokens.output + part.tokens.reasoning; accumulator.reasoningTokens += part.tokens.reasoning;} function takeOpenCodeTurnTokenUsage( context: OpenCodeSessionContext, complete: boolean,): TurnTokenUsage { const usage = context.turnTokenUsage; context.turnTokenUsage = undefined; if (!usage || usage.partIds.size === 0) { return { usageStatus: "unavailable", usageScope: "main_agent", hasSubagents: usage?.hasSubagents ?? false, }; } return { usageStatus: complete && usage.complete && usage.unresolvedStepsByMessageId.size === 0 ? "complete" : "partial", usageScope: "main_agent", inputTokens: usage.inputTokens, cachedInputTokens: usage.cachedInputTokens, cacheCreationTokens: usage.cacheCreationTokens, outputTokens: usage.outputTokens, reasoningTokens: Math.min(usage.outputTokens, usage.reasoningTokens), hasSubagents: usage.hasSubagents, };} export interface OpenCodeAdapterLiveOptions { readonly instanceId?: ProviderInstanceId; readonly environment?: NodeJS.ProcessEnv; readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger;} const nowIso = Effect.map(DateTime.now, DateTime.formatIso); /** * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into * the adapter-boundary `ProviderAdapterRequestError`. SDK-method-level call * sites pipe through this in `Effect.mapError` so they never build the error * shape by hand. */const toRequestError = (cause: OpenCodeRuntimeError): ProviderAdapterRequestError => new ProviderAdapterRequestError({ provider: PROVIDER, method: cause.operation, detail: cause.detail, cause: cause.cause, }); /** * Map a `Cause.squash`-ed failure into a `ProviderAdapterProcessError`. The * typed cause is usually an `OpenCodeRuntimeError` (from {@link runOpenCodeSdk}), * in which case we preserve its `detail`; otherwise we fall back to * {@link openCodeRuntimeErrorDetail} for unknown causes (defects, etc.). */const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProcessError => new ProviderAdapterProcessError({ provider: PROVIDER, threadId, detail: OpenCodeRuntimeError.is(cause) ? cause.detail : openCodeRuntimeErrorDetail(cause), cause, }); type EventBaseInput = { readonly threadId: ThreadId; readonly turnId?: TurnId | undefined; readonly itemId?: string | undefined; readonly requestId?: string | undefined; readonly createdAt?: string | undefined; readonly raw?: unknown;};Lines 1–500 of 3866. Load more
import { EventId, type OpenCodeSettings, ProviderDriverKind, ProviderInstanceId, type ProviderRuntimeEvent, type ProviderSendTurnInput, type ProviderSession, RuntimeItemId, RuntimeRequestId, ThreadId, type ToolLifecycleItemType, type TurnTokenUsage, TurnId, type UserInputQuestion, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; import { ProviderAdapterProcessError, ProviderAdapterRequestError, ProviderAdapterSessionClosedError, ProviderAdapterSessionNotFoundError, ProviderAdapterValidationError, } from "../Errors.ts"; import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; import { buildOpenCodePermissionRules, OpenCodeRuntime, OpenCodeRuntimeError, openCodeQuestionId, openCodeRuntimeErrorDetail, parseOpenCodeModelSlug, runOpenCodeSdk, toOpenCodeFileParts, toOpenCodePermissionReply, toOpenCodeQuestionAnswers, type OpenCodeServerConnection, } from "../opencodeRuntime.ts"; import * as Option from "effect/Option"; const PROVIDER = ProviderDriverKind.make("opencode"); /** * Version tag stamped into the OpenCode resume cursor. Bump if the cursor * shape changes so stale-shaped cursors written by older builds are ignored * rather than misread (mirrors GROK_RESUME_VERSION / CURSOR_RESUME_VERSION). */ const OPENCODE_RESUME_VERSION = 1 as const; /** * Decode a persisted resume cursor into the upstream `ses_…` id. Anything * that isn't a current-version cursor with a non-empty id means "no resume" * rather than an error. Re-adopting the session id IS the resume mechanism — * OpenCode scopes a conversation's history by session id. */ function parseOpenCodeResume(raw: unknown): { readonly sessionId: string } | undefined { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { return undefined; } const record = raw as Record<string, unknown>; if (record.schemaVersion !== OPENCODE_RESUME_VERSION) { return undefined; } if (typeof record.sessionId !== "string" || record.sessionId.trim().length === 0) { return undefined; } return { sessionId: record.sessionId.trim() }; } /** * Whether an error definitively reports a missing session. Only a confirmed * miss may silently start a fresh session; any other failure (the SDK client * is `throwOnError: true`, so `session.get` rejects on every non-2xx) must * propagate, or a transient blip resets a live thread to an empty one — the * #3604 silent context loss. Decides on structured signals only, never free * text: a numeric 404 or the exact `NotFoundError` name, found via a bounded walk * over `cause`/`body`/`error`/`data`. An explicit non-404 status seals its * subtree so a wrapped "NotFound" name can't reclassify a real failure. * Exported for unit testing. */ export function isOpenCodeNotFound(cause: unknown): boolean { const seen = new Set<unknown>(); const queue: Array<unknown> = [cause]; for (let steps = 0; queue.length > 0 && steps < 32; steps += 1) { const node = queue.shift(); if (node === null || typeof node !== "object" || seen.has(node)) { continue; } seen.add(node); const record = node as Record<string, unknown>; const response = record.response; const statuses = [ record.status, record.statusCode, response !== null && typeof response === "object" ? (response as { readonly status?: unknown }).status : undefined, ].filter((status): status is number => typeof status === "number"); if (statuses.includes(404)) { return true; } if (statuses.length > 0) { continue; } const name = record.name; if (typeof name === "string" && name.toLowerCase() === "notfounderror") { return true; } for (const key of ["cause", "body", "error", "data"] as const) { if (record[key] !== undefined) { queue.push(record[key]); } } } return false; } /** * Whether two directory spellings name the same location. Raw string * equality misreads a trailing slash, `.`/`..` segment, or symlinked cwd * (macOS `/tmp` → `/private/tmp`) as a cwd change, needlessly forking the * session on every resume. Lexically equal paths short-circuit; otherwise * both sides go through `realPath`, each falling back to its lexical form * on failure (deleted directory, external-server path) — so the probe can * only widen matches, never split them. Takes the services as arguments so * adapter methods stay service-free. Exported for unit testing. */ export function isSameOpenCodeDirectory( fileSystem: FileSystem.FileSystem, path: Path.Path, left: string, right: string, ): Effect.Effect<boolean> { const lexicalLeft = path.resolve(left); const lexicalRight = path.resolve(right); if (lexicalLeft === lexicalRight) { return Effect.succeed(true); } const canonicalize = (lexical: string) => fileSystem.realPath(lexical).pipe(Effect.orElseSucceed(() => lexical)); return Effect.zipWith( canonicalize(lexicalLeft), canonicalize(lexicalRight), (canonicalLeft, canonicalRight) => canonicalLeft === canonicalRight, ); } interface OpenCodeTurnSnapshot { readonly id: TurnId; readonly items: Array<unknown>; } type OpenCodeSubscribedEvent = Awaited<ReturnType<OpencodeClient["event"]["subscribe"]>> extends { readonly stream: AsyncIterable<infer TEvent>; } ? TEvent : never; type OpenCodeSessionStatusEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "session.status" } >; const OpenCodeSessionStatusMap = Schema.Record( Schema.String, Schema.Struct({ type: Schema.String }), ); const decodeOpenCodeSessionStatusMap = Schema.decodeUnknownOption(OpenCodeSessionStatusMap); interface OpenCodeCancellation { readonly turnId: TurnId | undefined; readonly acknowledgment: Deferred.Deferred<void>; readonly completion: Deferred.Deferred<void, ProviderAdapterRequestError>; acknowledged?: boolean; turnSettled?: boolean; deferredIdleEvent?: OpenCodeSessionStatusEvent; } interface OpenCodeIdleReconciliation { readonly turnId: TurnId; readonly promptGeneration: number; raw: unknown; warned: boolean; dirty: boolean; fiber?: Fiber.Fiber<void, never>; } interface OpenCodePromptAdmission { readonly generation: number; readonly turnId: TurnId; readonly messageId: string; readonly priorAwaitingBusy: boolean; readonly priorIdle: { readonly turnId: TurnId; readonly raw: unknown } | undefined; idleDuringAdmission: { readonly turnId: TurnId; readonly raw: unknown } | undefined; idleObservedAfterMessage: boolean; messageObserved: boolean; busyObserved: boolean; idleStatusConfirmations: number; accepted: boolean; cancelled: boolean; readonly acceptance: Deferred.Deferred<void>; readonly submissionSettled: Deferred.Deferred<void>; promptFiber?: Fiber.Fiber<void, ProviderAdapterRequestError>; recoveryFiber?: Fiber.Fiber<void, never>; recoveryRaw: unknown; } type OpenCodeTerminalRequestEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "permission.replied" | "question.replied" | "question.rejected"; } >; type OpenCodeAskedRequestEvent = Extract< OpenCodeSubscribedEvent, { readonly type: "permission.asked" | "question.asked" } >; type OpenCodeRoutedRequestEvent = OpenCodeAskedRequestEvent | OpenCodeTerminalRequestEvent; interface OpenCodeRequestRelationRetry { warned: boolean; fiber?: Fiber.Fiber<void, never>; } interface OpenCodePendingRequestRecovery { warned: boolean; rerun: boolean; } function trimText(value: string | undefined | null): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined; } function openCodeEventSessionId(event: OpenCodeSubscribedEvent): string | undefined { const properties = "properties" in event ? event.properties : undefined; if (!properties || typeof properties !== "object") { return undefined; } const sessionID = (properties as { readonly sessionID?: unknown }).sessionID; const sessionIDFromProperties = typeof sessionID === "string" ? sessionID : undefined; if (sessionIDFromProperties) { return sessionIDFromProperties; } const info = (properties as { readonly info?: { readonly id?: unknown } }).info; return info && typeof info.id === "string" ? info.id : undefined; } function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | undefined { if (event.type !== "session.updated") { return undefined; } const title = trimText(event.properties.info.title); // OpenCode mints a placeholder title at session.create when no title was // provided, and re-emits it on every `session.updated`. Mirroring it would // overwrite the thread's real title (openCodeEventSessionTitle feeds the // `thread.metadata.updated` mirror). Ignore OpenCode's auto-generated // placeholders so the thread isn't locked onto them. if (!title || isOpenCodeDefaultTitle(title)) { return undefined; } return title; } function isOpenCodeAbortError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "name" in error && error.name === "MessageAbortedError" ); } function isOpenCodeChildRequestEvent(event: OpenCodeSubscribedEvent): boolean { switch (event.type) { case "permission.asked": case "permission.replied": case "question.asked": case "question.replied": case "question.rejected": return true; default: return false; } } const OPENCODE_DEFAULT_TITLE_PATTERN = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; function isOpenCodeDefaultTitle(title: string): boolean { return OPENCODE_DEFAULT_TITLE_PATTERN.test(title); } type OpenCodeTextPart = Extract<Part, { readonly type: "text" | "reasoning" }>; type OpenCodeTextPartState = Pick<OpenCodeTextPart, "id" | "messageID" | "type" | "time"> & { text: string | undefined; emittedText: string | undefined; completed: boolean; }; type OpenCodeStepUsage = Pick<Extract<Part, { readonly type: "step-finish" }>, "id" | "tokens">; interface OpenCodeSessionContext { session: ProviderSession; readonly client: OpencodeClient; readonly server: OpenCodeServerConnection; readonly directory: string; readonly openCodeSessionId: string; readonly relatedSessionIds: Set<string>; readonly resolvedRequestIds: Set<string>; readonly autoRepliedRequestIds: Set<string>; readonly emittedTerminalRequestIds: Set<string>; readonly requestRelationRetries: Map<string, OpenCodeRequestRelationRetry>; readonly pendingPermissions: Map<string, PermissionRequest>; readonly pendingQuestions: Map<string, QuestionRequest>; readonly messageRoleById: Map<string, "user" | "assistant">; // OpenCode permits edits to completed parts. Keep text for snapshot comparison // until native removal or session teardown, but do not retain other part payloads. readonly textPartsByMessageId: Map<string, Map<string, OpenCodeTextPartState>>; turnTokenUsage: OpenCodeTurnTokenUsageAccumulator | undefined; activeTurnId: TurnId | undefined; activeAgent: string | undefined; activeVariant: string | undefined; cancellation: OpenCodeCancellation | undefined; interruptedTurnId: TurnId | undefined; reconcileIdleStatus: boolean; awaitingBusyAfterInterruption: boolean; pendingIdleReconciliation: OpenCodeIdleReconciliation | undefined; pendingRequestRecovery: OpenCodePendingRequestRecovery | undefined; promptGeneration: number; promptAdmission: OpenCodePromptAdmission | undefined; readonly promptSemaphore: Semaphore.Semaphore; readonly firstConnection: Deferred.Deferred<void, ProviderAdapterRequestError>; /** * One-shot guard flipped by `stopOpenCodeContext` / `emitUnexpectedExit`. * The session lifecycle is owned by `sessionScope`; this Ref exists only * so concurrent callers can race the transition safely via `getAndSet`. */ readonly stopped: Ref.Ref<boolean>; /** * Sole lifecycle handle for the session. Closing this scope: * - aborts the `AbortController` registered as a finalizer * (cancels the in-flight `event.subscribe` fetch), * - interrupts the event-pump and server-exit fibers forked * via `Effect.forkIn(sessionScope)`, * - tears down the OpenCode server process for scope-owned servers. */ readonly sessionScope: Scope.Closeable; } interface OpenCodeTurnTokenUsageAccumulator { readonly partIds: Set<string>; readonly promptMessageIds: Set<string>; readonly assistantOwnershipByMessageId: Map<string, "owned" | "other" | "unknown">; // Native removal does not undo usage. Keep unresolved counts until this turn settles. readonly unresolvedStepsByMessageId: Map<string, Map<string, OpenCodeStepUsage>>; inputTokens: number; cachedInputTokens: number; cacheCreationTokens: number; outputTokens: number; reasoningTokens: number; complete: boolean; hasSubagents: boolean; } function makeOpenCodeTurnTokenUsageAccumulator(): OpenCodeTurnTokenUsageAccumulator { return { partIds: new Set(), promptMessageIds: new Set(), assistantOwnershipByMessageId: new Map(), unresolvedStepsByMessageId: new Map(), inputTokens: 0, cachedInputTokens: 0, cacheCreationTokens: 0, outputTokens: 0, reasoningTokens: 0, complete: true, hasSubagents: false, }; } function accumulateOpenCodeStepUsage( accumulator: OpenCodeTurnTokenUsageAccumulator, part: OpenCodeStepUsage, ): void { if (accumulator.partIds.has(part.id)) return; accumulator.partIds.add(part.id); accumulator.inputTokens += part.tokens.input + part.tokens.cache.read + part.tokens.cache.write; accumulator.cachedInputTokens += part.tokens.cache.read; accumulator.cacheCreationTokens += part.tokens.cache.write; accumulator.outputTokens += part.tokens.output + part.tokens.reasoning; accumulator.reasoningTokens += part.tokens.reasoning; } function takeOpenCodeTurnTokenUsage( context: OpenCodeSessionContext, complete: boolean, ): TurnTokenUsage { const usage = context.turnTokenUsage; context.turnTokenUsage = undefined; if (!usage || usage.partIds.size === 0) { return { usageStatus: "unavailable", usageScope: "main_agent", hasSubagents: usage?.hasSubagents ?? false, }; } return { usageStatus: complete && usage.complete && usage.unresolvedStepsByMessageId.size === 0 ? "complete" : "partial", usageScope: "main_agent", inputTokens: usage.inputTokens, cachedInputTokens: usage.cachedInputTokens, cacheCreationTokens: usage.cacheCreationTokens, outputTokens: usage.outputTokens, reasoningTokens: Math.min(usage.outputTokens, usage.reasoningTokens), hasSubagents: usage.hasSubagents, }; } export interface OpenCodeAdapterLiveOptions { readonly instanceId?: ProviderInstanceId; readonly environment?: NodeJS.ProcessEnv; readonly nativeEventLogPath?: string; readonly nativeEventLogger?: EventNdjsonLogger; } const nowIso = Effect.map(DateTime.now, DateTime.formatIso); /** * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into * the adapter-boundary `ProviderAdapterRequestError`. SDK-method-level call * sites pipe through this in `Effect.mapError` so they never build the error * shape by hand. */ const toRequestError = (cause: OpenCodeRuntimeError): ProviderAdapterRequestError => new ProviderAdapterRequestError({ provider: PROVIDER, method: cause.operation, detail: cause.detail, cause: cause.cause, }); /** * Map a `Cause.squash`-ed failure into a `ProviderAdapterProcessError`. The * typed cause is usually an `OpenCodeRuntimeError` (from {@link runOpenCodeSdk}), * in which case we preserve its `detail`; otherwise we fall back to * {@link openCodeRuntimeErrorDetail} for unknown causes (defects, etc.). */ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProcessError => new ProviderAdapterProcessError({ provider: PROVIDER, threadId, detail: OpenCodeRuntimeError.is(cause) ? cause.detail : openCodeRuntimeErrorDetail(cause), cause, }); type EventBaseInput = { readonly threadId: ThreadId; readonly turnId?: TurnId | undefined; readonly itemId?: string | undefined; readonly requestId?: string | undefined; readonly createdAt?: string | undefined; readonly raw?: unknown; };