import * as plugins from './plugins.js'; import { controllerMaxAttachmentPromptSuffixBytes, controllerMaxDraftTextBytes, controllerInitialMessageBundleLimit, controllerMaxLiveMessageEventBytes, controllerRuntimeIdKey, } from '../ts_interfaces/index.js'; import type { IControllerEvent, IControllerMessage, TControllerModelChoice, TControllerModelOption, IControllerPermission, IControllerQuestion, IControllerQuestionItem, IControllerReasoningPart, IControllerRuntimeId, IControllerSession, IControllerSessionDetail, IControllerSessionMetrics, IControllerSessionReversionInfo, IControllerTodo, IControllerToolCall, IControllerToolExecution, IControllerUsage, TControllerBuiltinCommand, TControllerPermissionReply, TControllerSessionStatus, TControllerTodoStatus, } from '../ts_interfaces/index.js'; import type { IOpenCodeClientOptions, IOpenCodeDirectChildObservation, IOpenCodeEventStreamOptions, IOpenCodeMessagePage, IOpenCodeMessageLifecycle, IOpenCodeNormalizedMessageBundle, IOpenCodeSessionAuthorityObservation, TOpenCodeReasoningUpdate, TOpenCodeTextUpdate, TOpenCodeToolExecution, TOpenCodeSdkClient, } from './interfaces.opencode.js'; import type { IFlexOpenCodeOAuthAuth } from './interfaces.flexipc.js'; import { boundLiveToolExecution } from './functions.livetoolbounding.js'; import { openCodeSessionIdentitySnapshotCapability, openCodeSessionProviderGeneration, type IOpenCodeSessionIdentitySnapshot, type IOpenCodeSessionIdentitySnapshotCapability, } from './classes.sessionidentityintegration.js'; // Every normalized response must fit the transport. TypedSocket allows 16 MiB // frames here; these lower per-response budgets also bound memory and fan-out. const maxIdentifierBytes = 512; const maxTitleBytes = 2048; const maxPromptBytes = controllerMaxDraftTextBytes + controllerMaxAttachmentPromptSuffixBytes; const maxMessageTextBytes = 96 * 1024; const maxToolPayloadBytes = 48 * 1024; const maxLiveMessageSnapshotBytes = controllerMaxLiveMessageEventBytes - (8 * 1024); const maxLiveMessagePartCacheEntries = 512; const maxLiveMessagePartCacheBytes = 4 * 1024 * 1024; const maxLiveMessageEventIds = 4_096; const maxMetadataStringBytes = 16 * 1024; const maxMetadataNodes = 4096; const maxCollectionEntries = 512; const maxMessagesPerRequest = 200; const maxMessagesPerPage = 50; const maxMessagePartsPerResponse = 2048; const maxNormalizedMessageBytes = 512 * 1024; const maxNormalizedPermissionBytes = 128 * 1024; const maxPerMessageReasoningBytes = 128 * 1024; const maxNormalizedQuestionBytes = 128 * 1024; const maxQuestionsPerRequest = 16; const maxOptionsPerQuestion = 64; const maxNormalizedSessionDetailBytes = 576 * 1024; // Read-side memory guards only: the transcript is trimmed to the transfer // budget after parsing, so these just bound transient buffering while a // response streams in. Long-running local sessions legitimately produce // tens of megabytes of raw message history. const maxRawOpenCodeResponseBytes = 64 * 1024 * 1024; const maxRawOpenCodeSseEventBytes = 8 * 1024 * 1024; const defaultSdkRequestTimeoutMs = 30_000; // compact/init run a full model call server-side before responding. const builtinModelCommandTimeoutMs = 10 * 60_000; const normalizedNodeCostBytes = 64; const truncatedSuffix = '\n… [truncated]'; const openCodeHarnessId = 'opencode' as const; const sessionIntelligenceProviderId = 'openai'; const sessionIntelligenceModelId = 'gpt-5.6-luna'; const sessionIntelligenceModel = `${sessionIntelligenceProviderId}/${sessionIntelligenceModelId}`; const sessionIntelligenceTimeoutMs = 5 * 60_000; const maxExhaustiveMessagePages = 400; const sessionMetricsCacheTtlMs = 30_000; const maxSessionMetricsCacheEntries = 512; const maxIntelligenceTranscriptBytes = 256 * 1024; const maxIntelligenceTranscriptMessages = 256; // One persisted cleanup page can contain 500 states with eight exchanges each. const maxSessionIntelligenceCleanupObligations = 4_000; const sessionIntelligenceTitle = 'Session Intelligence'; const openCodeIdentitySnapshotPageEntries = 256; const maxOpenCodeIdentitySnapshotSourceEntries = 65_536; const maxOpenCodeIdentitySnapshotPages = 512; const maxOpenCodeIdentityMetadataProbes = 8_192; const maxOpenCodeIdentityCursorBytes = 16 * 1024; const maxOpenCodeIdentityAggregateCursorBytes = 1024 * 1024; const openCodeIdentityMetadataProbeConcurrency = 8; const maxOpenCodeSearchResults = 512; const eventStreamResubscribeDelayMs = 500; const eventStreamMaxResubscribeDelayMs = 10_000; const openCodeRuntimeId = (nativeIdArg: string): IControllerRuntimeId => ({ harnessId: openCodeHarnessId, nativeId: nativeIdArg, }); const controllerRuntimeIdsEqual = ( leftArg: IControllerRuntimeId, rightArg: IControllerRuntimeId, ): boolean => controllerRuntimeIdKey(leftArg) === controllerRuntimeIdKey(rightArg); interface IOutputBudget { bytesRemaining: number; label: string; } interface IUnknownBudget { nodesRemaining: number; outputBudget: IOutputBudget; } const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value); const isOpenCodeNotFoundBody = (valueArg: unknown): boolean => isRecord(valueArg) && valueArg.name === 'NotFoundError' && isRecord(valueArg.data) && typeof valueArg.data.message === 'string'; const isOpenCodeNotFoundError = (valueArg: unknown): boolean => { if (isOpenCodeNotFoundBody(valueArg)) return true; if (!isRecord(valueArg) || !isRecord(valueArg.cause)) return false; return valueArg.cause.status === 404 && isOpenCodeNotFoundBody(valueArg.cause.body); }; const hasExactKeys = ( valueArg: Record, requiredArg: readonly string[], optionalArg: readonly string[] = [], ): boolean => { const keys = Object.keys(valueArg); const allowed = new Set([...requiredArg, ...optionalArg]); return requiredArg.every((key) => Object.hasOwn(valueArg, key)) && keys.every((key) => allowed.has(key)); }; interface ISessionIntelligenceMarker { controllerId: string; projectId: string; sourceSessionId: IControllerRuntimeId; exchangeId: string; } interface ISessionMetricsCacheEntry { promise: Promise; expiresAt: number; } export class OpenCodeSessionIntelligenceCleanupError extends AggregateError { public readonly temporarySessionId: string; constructor(temporarySessionIdArg: string, isolationErrorArg: Error, cleanupErrorArg: unknown) { super( [isolationErrorArg, cleanupErrorArg], 'OpenCode rejected the Session Intelligence isolation contract and cleanup failed.', ); this.temporarySessionId = temporarySessionIdArg; } } const isSessionIntelligenceMetadata = ( valueArg: unknown, controllerIdArg?: string, expectedMarkerArg?: ISessionIntelligenceMarker, ): boolean => { if (!isRecord(valueArg) || !isRecord(valueArg.modelprofileSessionIntelligence)) return false; const marker = valueArg.modelprofileSessionIntelligence; return hasExactKeys(valueArg, ['modelprofileSessionIntelligence']) && hasExactKeys(marker, [ 'schemaVersion', 'controllerId', 'projectId', 'sourceSessionId', 'exchangeId', ]) && marker.schemaVersion === 1 && typeof marker.controllerId === 'string' && (controllerIdArg === undefined || marker.controllerId === controllerIdArg) && typeof marker.projectId === 'string' && isRecord(marker.sourceSessionId) && hasExactKeys(marker.sourceSessionId, ['harnessId', 'nativeId']) && (marker.sourceSessionId.harnessId === 'opencode' || marker.sourceSessionId.harnessId === 'flex') && typeof marker.sourceSessionId.nativeId === 'string' && typeof marker.exchangeId === 'string' && ( expectedMarkerArg === undefined || ( marker.controllerId === expectedMarkerArg.controllerId && marker.projectId === expectedMarkerArg.projectId && marker.sourceSessionId.harnessId === expectedMarkerArg.sourceSessionId.harnessId && marker.sourceSessionId.nativeId === expectedMarkerArg.sourceSessionId.nativeId && marker.exchangeId === expectedMarkerArg.exchangeId ) ); }; const truncateUtf8 = (value: string, byteLimit: number): string => { if (Buffer.byteLength(value, 'utf8') <= byteLimit) { return value; } const suffixBytes = Buffer.byteLength(truncatedSuffix, 'utf8'); if (byteLimit <= suffixBytes) { return Buffer.from(value, 'utf8') .subarray(0, Math.max(0, byteLimit)) .toString('utf8') .replace(/\uFFFD$/u, ''); } const contentBytes = Math.max(0, byteLimit - suffixBytes); let truncated = Buffer.from(value, 'utf8').subarray(0, contentBytes).toString('utf8'); truncated = truncated.replace(/\uFFFD$/u, ''); return `${truncated}${truncatedSuffix}`; }; const createOutputBudget = (byteLimit: number, label: string): IOutputBudget => ({ bytesRemaining: byteLimit, label, }); const consumeOutputBytes = (budget: IOutputBudget, byteLength: number): void => { budget.bytesRemaining -= byteLength; if (budget.bytesRemaining < 0) { throw new Error(`OpenCode returned ${budget.label} that exceeded its aggregate output budget.`); } }; const consumeOutputString = (budget: IOutputBudget, value: string): string => { consumeOutputBytes(budget, Buffer.byteLength(value, 'utf8')); return value; }; const elidedPayloadNotice = '[elided: this transcript exceeds the transfer budget]'; const elidedHistoryNotice = '[earlier messages elided: this transcript exceeds the transfer budget]'; const createElidedHistoryId = ( messagesArg: readonly IControllerMessage[], ): IControllerRuntimeId => { const occupiedKeys = new Set(messagesArg.map((message) => controllerRuntimeIdKey(message.id))); const transcriptDigest = plugins.crypto .createHash('sha256') .update(JSON.stringify(messagesArg.map((message) => controllerRuntimeIdKey(message.id)))) .digest('hex'); const nativeIdBase = `harness-controller:elided-history:${transcriptDigest}`; let disambiguator = 0; let candidate = openCodeRuntimeId(nativeIdBase); while (occupiedKeys.has(controllerRuntimeIdKey(candidate))) { disambiguator += 1; candidate = openCodeRuntimeId(`${nativeIdBase}:${disambiguator}`); } return candidate; }; /** * Drops the OLDEST transcript entries until the serialized form fits, * leaving a marker so readers can see history was cut. Newest messages are * what a conversation view needs; a transcript must never fail to load over * its size. */ const trimOldestMessagesToFit = ( messages: IControllerMessage[], byteLimit: number, ): IControllerMessage[] => { let current = messages; const markerId = createElidedHistoryId(messages); const markerIdKey = controllerRuntimeIdKey(markerId); while ( current.length > 1 && Buffer.byteLength(JSON.stringify(current), 'utf8') > byteLimit ) { const realMessages = current.filter( (message) => controllerRuntimeIdKey(message.id) !== markerIdKey, ); const dropCount = Math.max(1, Math.ceil(realMessages.length / 10)); const kept = realMessages.slice(dropCount); const marker: IControllerMessage = { id: markerId, role: 'system', text: elidedHistoryNotice, createdAt: kept[0]?.createdAt ?? 0, }; current = [marker, ...kept]; } return current; }; const assertSerializedOutputWithinLimit = ( value: unknown, byteLimit: number, label: string ): void => { const serialized = JSON.stringify(value); if (Buffer.byteLength(serialized, 'utf8') > byteLimit) { throw new Error(`OpenCode returned ${label} that exceeded its aggregate output budget.`); } }; const createBoundedResponse = async ( responseArg: Response, byteLimitArg: number, sseEventModeArg: boolean, releaseArg?: () => void, runtimeAbortSignalArg?: AbortSignal, ): Promise => { const declaredLength = responseArg.headers.get('content-length'); if (!sseEventModeArg && declaredLength !== null) { const parsedLength = Number(declaredLength); if (Number.isFinite(parsedLength) && parsedLength > byteLimitArg) { let cancellation: Promise | undefined; try { cancellation = responseArg.body?.cancel( new Error('OpenCode response exceeded its declared raw byte limit.'), ); } finally { releaseArg?.(); } void cancellation?.catch(() => undefined); throw new Error('OpenCode response exceeded its raw byte limit.'); } } if (!responseArg.body) { releaseArg?.(); return responseArg; } if (sseEventModeArg) releaseArg?.(); let bytesSeen = 0; let eventText = ''; let trailingCarriageReturn = false; const decoder = sseEventModeArg ? new TextDecoder() : undefined; const sourceReader = responseArg.body.getReader(); let released = sseEventModeArg; let sourceLockReleased = false; let runtimeAbortListener: (() => void) | undefined; let streamController: ReadableStreamDefaultController | undefined; const release = (): void => { if (released) return; released = true; releaseArg?.(); }; const removeRuntimeAbortListener = (): void => { if (!runtimeAbortListener) return; runtimeAbortSignalArg?.removeEventListener('abort', runtimeAbortListener); runtimeAbortListener = undefined; }; const releaseSourceLock = (): void => { if (sourceLockReleased) return; sourceLockReleased = true; sourceReader.releaseLock(); }; const fail = (reasonArg: unknown): void => { release(); removeRuntimeAbortListener(); let cancellation: Promise; try { cancellation = sourceReader.cancel(reasonArg); } catch { releaseSourceLock(); return; } try { releaseSourceLock(); } catch { void cancellation.finally(() => releaseSourceLock()).catch(() => undefined); } void cancellation.catch(() => undefined); }; const abortRuntimeResponse = (): void => { const reason = runtimeAbortSignalArg?.reason ?? new DOMException('The OpenCode runtime generation ended.', 'AbortError'); fail(reason); try { streamController?.error(reason); } catch { // A concurrently completed or cancelled stream already owns settlement. } }; const stream = new ReadableStream({ start(controllerArg) { streamController = controllerArg; if (!runtimeAbortSignalArg) return; runtimeAbortListener = abortRuntimeResponse; runtimeAbortSignalArg.addEventListener('abort', runtimeAbortListener, { once: true }); if (runtimeAbortSignalArg.aborted) abortRuntimeResponse(); }, async pull(controllerArg) { try { const result = await sourceReader.read(); if (result.done) { if (sseEventModeArg) { const finalText = `${eventText}${trailingCarriageReturn ? '\r' : ''}${decoder!.decode()}`; if (Buffer.byteLength(finalText, 'utf8') > byteLimitArg) { throw new Error('OpenCode SSE event exceeded its raw byte limit.'); } } release(); removeRuntimeAbortListener(); releaseSourceLock(); controllerArg.close(); return; } const chunkArg = result.value; if (!sseEventModeArg) { bytesSeen += chunkArg.byteLength; if (bytesSeen > byteLimitArg) { throw new Error('OpenCode response exceeded its raw byte limit.'); } } else { let decoded = decoder!.decode(chunkArg, { stream: true }); if (trailingCarriageReturn) { decoded = `\r${decoded}`; trailingCarriageReturn = false; } if (decoded.endsWith('\r')) { decoded = decoded.slice(0, -1); trailingCarriageReturn = true; } const normalized = `${eventText}${decoded}` .replace(/\r\n/gu, '\n') .replace(/\r/gu, '\n'); const lastBoundary = normalized.lastIndexOf('\n\n'); eventText = lastBoundary >= 0 ? normalized.slice(lastBoundary + 2) : normalized; if (Buffer.byteLength(eventText, 'utf8') > byteLimitArg) { throw new Error('OpenCode SSE event exceeded its raw byte limit.'); } } controllerArg.enqueue(chunkArg); } catch (errorArg) { fail(errorArg); throw errorArg; } }, cancel(reasonArg) { fail(reasonArg); }, }); return new Response(stream, { status: responseArg.status, statusText: responseArg.statusText, headers: responseArg.headers, }); }; export const createBoundedOpenCodeFetch = ( expectedBaseUrlArg: string, fetchImplementationArg: typeof globalThis.fetch = globalThis.fetch, acquireRuntimeLeaseArg?: () => () => void, runtimeAbortSignalArg?: AbortSignal, ): typeof globalThis.fetch => { const expectedOrigin = new URL(expectedBaseUrlArg).origin; return (async (inputArg: string | URL | Request, initArg?: RequestInit) => { const requested = new Request(inputArg, { ...initArg, redirect: 'error' }); if (new URL(requested.url).origin !== expectedOrigin) { throw new Error('OpenCode SDK attempted a request outside its loopback server origin.'); } const request = runtimeAbortSignalArg ? new Request(requested, { signal: AbortSignal.any([requested.signal, runtimeAbortSignalArg]), }) : requested; const release = acquireRuntimeLeaseArg?.(); let response: Response; try { response = await fetchImplementationArg(request); } catch (errorArg) { release?.(); throw errorArg; } const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim(); return createBoundedResponse( response, contentType === 'text/event-stream' ? maxRawOpenCodeSseEventBytes : maxRawOpenCodeResponseBytes, contentType === 'text/event-stream', release, runtimeAbortSignalArg, ); }) as typeof globalThis.fetch; }; const runSdkRequest = async ( callerSignal: AbortSignal | undefined, operation: (requestSignal: AbortSignal) => Promise, timeoutMs: number | undefined = defaultSdkRequestTimeoutMs ): Promise => { callerSignal?.throwIfAborted(); const requestAbortController = new AbortController(); const abortFromCaller = (): void => { requestAbortController.abort(callerSignal?.reason); }; callerSignal?.addEventListener('abort', abortFromCaller, { once: true }); if (callerSignal?.aborted) { abortFromCaller(); } const timeout = timeoutMs === undefined ? undefined : setTimeout(() => { requestAbortController.abort( new Error(`OpenCode SDK request exceeded ${timeoutMs}ms.`) ); }, timeoutMs); timeout?.unref(); try { return await operation(requestAbortController.signal); } finally { if (timeout) clearTimeout(timeout); callerSignal?.removeEventListener('abort', abortFromCaller); } }; const requireString = (value: unknown, fieldName: string, byteLimit: number): string => { if (typeof value !== 'string' || value.length === 0) { throw new Error(`OpenCode returned an invalid ${fieldName}.`); } if (Buffer.byteLength(value, 'utf8') > byteLimit) { throw new Error(`OpenCode returned an oversized ${fieldName}.`); } return value; }; const normalizeModelVariants = (value: unknown): string[] => { if (!isRecord(value)) return []; const entries = Object.entries(value); if (entries.length > 16) { throw new Error('OpenCode returned too many variants for one model.'); } const variants: string[] = []; for (const [variantKey, variant] of entries) { if ( variantKey === '__proto__' || variantKey === 'constructor' || variantKey === 'prototype' ) continue; if (isRecord(variant) && variant.disabled === true) continue; variants.push(requireString(variantKey, 'model variant name', 64)); } return variants; }; const requireFiniteNumber = (value: unknown, fieldName: string): number => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { throw new Error(`OpenCode returned an invalid ${fieldName}.`); } return value; }; const optionalFiniteNumber = (value: unknown): number | undefined => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { return undefined; } return value; }; const optionalNonNegativeSafeInteger = (value: unknown): number | undefined => ( typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined ); export interface IOpenCodeSessionSearchResult { sessions: IControllerSession[]; truncated: boolean; } const validateInputString = ( value: unknown, fieldName: string, byteLimit: number, allowEmpty = false ): string => { if (typeof value !== 'string' || (!allowEmpty && value.trim().length === 0)) { throw new Error(`${fieldName} must be a non-empty string.`); } if (Buffer.byteLength(value, 'utf8') > byteLimit) { throw new Error(`${fieldName} exceeds the ${byteLimit}-byte limit.`); } return value; }; const validateOpenCodeModelChoice = ( modelArg: TControllerModelChoice | undefined, identifierByteLimitArg = 256, ): { providerID: string; modelID: string; variant?: string } | undefined => { if (modelArg === undefined) return undefined; if (modelArg.harnessId !== openCodeHarnessId) { throw new Error('OpenCode adapter requires an OpenCode model choice.'); } return { providerID: validateInputString( modelArg.providerID, 'Model provider ID', identifierByteLimitArg, ), modelID: validateInputString(modelArg.modelID, 'Model ID', identifierByteLimitArg), ...(modelArg.variant === undefined ? {} : { variant: validateInputString(modelArg.variant, 'Model variant', 64) }), }; }; const sanitizeUnknown = ( value: unknown, budget: IUnknownBudget = { nodesRemaining: maxMetadataNodes, outputBudget: createOutputBudget(maxNormalizedPermissionBytes, 'metadata'), }, depth = 0, seen = new WeakSet() ): unknown => { budget.nodesRemaining -= 1; if (budget.nodesRemaining < 0) { return consumeOutputString(budget.outputBudget, '[truncated]'); } consumeOutputBytes(budget.outputBudget, normalizedNodeCostBytes); if (value === null || typeof value === 'boolean') { return value; } if (typeof value === 'number') { return Number.isFinite(value) ? value : consumeOutputString(budget.outputBudget, String(value)); } if (typeof value === 'string') { return consumeOutputString( budget.outputBudget, truncateUtf8(value, maxMetadataStringBytes) ); } if (typeof value === 'bigint') { return consumeOutputString(budget.outputBudget, value.toString()); } if (typeof value !== 'object') { return consumeOutputString(budget.outputBudget, `[${typeof value}]`); } if (depth >= 8) { return consumeOutputString(budget.outputBudget, '[maximum depth reached]'); } if (seen.has(value)) { return consumeOutputString(budget.outputBudget, '[circular]'); } seen.add(value); if (Array.isArray(value)) { const result = value .slice(0, maxCollectionEntries) .map((item) => sanitizeUnknown(item, budget, depth + 1, seen)); if (value.length > maxCollectionEntries) { result.push( consumeOutputString( budget.outputBudget, `[${value.length - maxCollectionEntries} items omitted]` ) ); } return result; } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { return consumeOutputString( budget.outputBudget, `[unsupported ${value.constructor?.name || 'object'}]` ); } const recordValue = value as Record; const result: Record = {}; let includedEntryCount = 0; let wasTruncated = false; for (const rawKey in recordValue) { if (!Object.hasOwn(recordValue, rawKey)) { continue; } if (includedEntryCount >= maxCollectionEntries) { wasTruncated = true; break; } if (rawKey === '__proto__' || rawKey === 'constructor' || rawKey === 'prototype') { continue; } const key = truncateUtf8(rawKey, maxIdentifierBytes); consumeOutputString(budget.outputBudget, key); result[key] = sanitizeUnknown(recordValue[rawKey], budget, depth + 1, seen); includedEntryCount += 1; } if (wasTruncated) { result.__truncated = true; } return result; }; const sanitizeRecord = ( value: unknown, fieldName: string, budget?: IUnknownBudget ): Record => { if (!isRecord(value)) { throw new Error(`OpenCode returned invalid ${fieldName}.`); } const sanitized = sanitizeUnknown(value, budget); if (!isRecord(sanitized)) { throw new Error(`OpenCode returned invalid ${fieldName}.`); } return sanitized; }; const normalizeSessionStatus = (value: unknown): TControllerSessionStatus => { if (!isRecord(value)) { throw new Error('OpenCode returned an invalid session status.'); } switch (value.type) { case 'idle': case 'busy': case 'retry': return value.type; default: throw new Error('OpenCode returned an unknown session status.'); } }; export const normalizeOpenCodeSession = ( value: unknown, directory: string, status: TControllerSessionStatus = 'idle' ): IControllerSession => { if (!isRecord(value) || !isRecord(value.time)) { throw new Error('OpenCode returned an invalid session.'); } if (value.directory !== directory) { throw new Error('OpenCode returned a session outside the configured directory.'); } const parentId = value.parentID === undefined ? undefined : openCodeRuntimeId( requireString(value.parentID, 'session parent ID', maxIdentifierBytes), ); // A zero timestamp reads as "not archived" rather than 1970. const archivedAt = optionalFiniteNumber(value.time.archived); return { id: openCodeRuntimeId(requireString(value.id, 'session ID', maxIdentifierBytes)), title: truncateUtf8(requireString(value.title, 'session title', maxMessageTextBytes), maxTitleBytes), ...(parentId ? { parentId } : {}), createdAt: requireFiniteNumber(value.time.created, 'session creation time'), updatedAt: requireFiniteNumber(value.time.updated, 'session update time'), ...(archivedAt ? { archivedAt } : {}), status, }; }; const normalizeUsage = (value: unknown): IControllerUsage | undefined => { if (!isRecord(value)) { return undefined; } const cache = isRecord(value.cache) ? value.cache : undefined; const usage: IControllerUsage = { inputTokens: optionalFiniteNumber(value.input), outputTokens: optionalFiniteNumber(value.output), totalTokens: optionalFiniteNumber(value.total), cacheReadTokens: optionalFiniteNumber(cache?.read), cacheWriteTokens: optionalFiniteNumber(cache?.write), }; return Object.values(usage).some((item) => item !== undefined) ? usage : undefined; }; const normalizeErrorText = (value: unknown): string | undefined => { if (!isRecord(value)) { return undefined; } const data = isRecord(value.data) ? value.data : undefined; if (typeof data?.message === 'string') { return truncateUtf8(data.message, maxToolPayloadBytes); } if (typeof value.message === 'string') { return truncateUtf8(value.message, maxToolPayloadBytes); } if (typeof value.name === 'string') { return truncateUtf8(value.name, maxTitleBytes); } return 'OpenCode reported an unspecified error.'; }; const normalizeReasoningPart = ( value: Record, ): IControllerReasoningPart => { const time = isRecord(value.time) ? value.time : undefined; return { id: openCodeRuntimeId( requireString(value.id, 'reasoning part ID', maxIdentifierBytes), ), text: truncateUtf8( typeof value.text === 'string' ? value.text : '', maxMessageTextBytes ), startedAt: optionalFiniteNumber(time?.start), endedAt: optionalFiniteNumber(time?.end), }; }; const normalizeToolCall = ( value: Record, ): IControllerToolCall => { if (!isRecord(value.state)) { throw new Error('OpenCode returned an invalid tool state.'); } const state = value.state; const status = state.status; if (status !== 'pending' && status !== 'running' && status !== 'completed' && status !== 'error') { throw new Error('OpenCode returned an unknown tool state.'); } const time = isRecord(state.time) ? state.time : undefined; const stateMetadata = isRecord(state.metadata) ? state.metadata : undefined; // Every tool call gets its own payload budget: one call's giant payload // elides itself and can never starve the rest of the transcript. const metadataBudget: IUnknownBudget = { nodesRemaining: maxMetadataNodes, outputBudget: createOutputBudget(maxToolPayloadBytes * 2, 'tool payload'), }; const sanitizePayload = (payload: unknown): unknown => { if (metadataBudget.outputBudget.bytesRemaining <= 0) { return elidedPayloadNotice; } try { return sanitizeUnknown(payload, metadataBudget); } catch { // Structured payloads past the budget degrade like string payloads do. return elidedPayloadNotice; } }; const rawOutput = state.output !== undefined ? state.output : (status === 'running' || status === 'error') && typeof stateMetadata?.output === 'string' ? stateMetadata.output : undefined; const output = rawOutput === undefined ? undefined : typeof rawOutput === 'string' ? truncateUtf8(rawOutput, maxToolPayloadBytes) : sanitizePayload(rawOutput); const exitCode = optionalNonNegativeSafeInteger(stateMetadata?.exit); const childSessionId = typeof stateMetadata?.sessionId === 'string' && stateMetadata.sessionId.length > 0 && Buffer.byteLength(stateMetadata.sessionId, 'utf8') <= maxIdentifierBytes ? openCodeRuntimeId(stateMetadata.sessionId) : undefined; // Subagent (task) calls report the model their child session runs on. const childModel = isRecord(stateMetadata?.model) ? normalizeMessageModel(stateMetadata.model) : undefined; // OpenCode's own one-line description of the call. It is empty for every MCP tool and absent on // error states, and an apply_patch title is genuinely multi-line, so only the first line is kept // and an empty one is dropped rather than shipped as a blank subtitle. const title = typeof state.title === 'string' ? truncateUtf8(state.title.split('\n')[0]!.trim(), maxTitleBytes) : ''; return { id: openCodeRuntimeId(requireString(value.callID, 'tool call ID', maxIdentifierBytes)), name: requireString(value.tool, 'tool name', maxTitleBytes), status, ...(title === '' ? {} : { title }), ...(childSessionId === undefined ? {} : { childSessionId }), ...(childModel === undefined ? {} : { model: childModel }), ...(state.input === undefined ? {} : { input: sanitizePayload(state.input) }), ...(output === undefined ? {} : { output }), ...(exitCode === undefined ? {} : { exitCode }), ...(typeof state.error === 'string' ? { errorText: truncateUtf8(state.error, maxToolPayloadBytes) } : {}), startedAt: optionalFiniteNumber(time?.start), finishedAt: optionalFiniteNumber(time?.end), }; }; /** * "providerID/modelID" for an assistant message, when OpenCode reported * both. Malformed or oversized values are dropped rather than thrown on: * the model label is display metadata and must never fail a transcript. */ const normalizeMessageModel = (info: Record): string | undefined => { const providerId = info.providerID; const modelId = info.modelID; if (typeof providerId !== 'string' || typeof modelId !== 'string') return undefined; if (!providerId || !modelId) return undefined; const combined = `${providerId}/${modelId}`; if (Buffer.byteLength(combined, 'utf8') > maxIdentifierBytes) return undefined; return combined; }; /** * The model variant (reasoning effort) an assistant message ran with, when * OpenCode reported one. Display metadata: malformed values are dropped * rather than thrown on. */ const normalizeMessageEffort = (info: Record): string | undefined => { const variant = info.variant; if (typeof variant !== 'string' || !variant) return undefined; if (Buffer.byteLength(variant, 'utf8') > maxTitleBytes) return undefined; return variant; }; const normalizeOpenCodeMessageBundle = ( bundle: unknown, aggregatePartCount: { value: number }, expectedSessionIdArg?: string, ): IOpenCodeNormalizedMessageBundle => { if (!isRecord(bundle) || !isRecord(bundle.info) || !Array.isArray(bundle.parts)) { throw new Error('OpenCode returned an invalid message bundle.'); } const info = bundle.info; const sourceSessionId = requireString(info.sessionID, 'message session ID', maxIdentifierBytes); if (expectedSessionIdArg !== undefined && sourceSessionId !== expectedSessionIdArg) { throw new Error('OpenCode returned a message for the wrong session.'); } const role = info.role; if (role !== 'user' && role !== 'assistant') { throw new Error('OpenCode returned an unknown message role.'); } if (!isRecord(info.time)) { throw new Error('OpenCode returned invalid message timing.'); } const sourceMessageId = openCodeRuntimeId( requireString(info.id, 'message ID', maxIdentifierBytes), ); const createdAt = requireFiniteNumber(info.time.created, 'message creation time'); const textParts: string[] = []; let textBytes = 0; const reasoningParts: IControllerReasoningPart[] = []; const toolParts: Array<{ part: Record; call: IControllerToolCall }> = []; if (bundle.parts.length > maxCollectionEntries) { throw new Error('OpenCode returned too many message parts.'); } aggregatePartCount.value += bundle.parts.length; if (aggregatePartCount.value > maxMessagePartsPerResponse) { throw new Error('OpenCode returned too many aggregate message parts.'); } for (const part of bundle.parts) { if (!isRecord(part) || typeof part.type !== 'string') { throw new Error('OpenCode returned an invalid message part.'); } if ( requireString(part.sessionID, 'message part session ID', maxIdentifierBytes) !== sourceSessionId || requireString(part.messageID, 'message part message ID', maxIdentifierBytes) !== sourceMessageId.nativeId ) { throw new Error('OpenCode returned a message part with mismatched ownership.'); } if (part.type === 'text') { if (typeof part.text !== 'string') { throw new Error('OpenCode returned an invalid text part.'); } if (part.ignored !== true) { const separatorBytes = textParts.length > 0 ? 2 : 0; const remainingTextBytes = Math.max( 0, maxMessageTextBytes - textBytes - separatorBytes ); if (remainingTextBytes > 0) { const normalizedTextPart = truncateUtf8(part.text, remainingTextBytes); textParts.push(normalizedTextPart); textBytes += separatorBytes + Buffer.byteLength(normalizedTextPart, 'utf8'); } } } else if (part.type === 'reasoning') { reasoningParts.push(normalizeReasoningPart(part)); } else if (part.type === 'tool') { toolParts.push({ part, call: normalizeToolCall(part), }); } } // One message's reasoning is capped as a whole: parts past the cap elide // individually so a marathon trace cannot dominate the transcript. let reasoningBytesRemaining = maxPerMessageReasoningBytes; for (const reasoningPart of reasoningParts) { const partBytes = Buffer.byteLength(reasoningPart.text, 'utf8'); if (partBytes <= reasoningBytesRemaining) { reasoningBytesRemaining -= partBytes; } else { reasoningPart.text = elidedPayloadNotice; } } const completedAt = optionalFiniteNumber(info.time.completed); const errorText = normalizeErrorText(info.error); const usage = normalizeUsage(info.tokens); // Assistant messages carry the model that produced them, so a transcript // shows exactly which model answered — including after a mid-chat switch. const model = normalizeMessageModel(info); const effort = normalizeMessageEffort(info); const messages: IControllerMessage[] = [{ id: sourceMessageId, role, text: textParts.join('\n\n'), createdAt, ...(completedAt === undefined ? {} : { updatedAt: completedAt }), ...(role === 'assistant' ? { streaming: completedAt === undefined } : {}), ...(reasoningParts.length > 0 ? { reasoning: reasoningParts } : {}), ...(usage ? { usage } : {}), ...(errorText ? { error: errorText } : {}), ...(model ? { model } : {}), ...(effort ? { effort } : {}), }]; for (const toolPart of toolParts) { const state = toolPart.part.state; const stateTime = isRecord(state) && isRecord(state.time) ? state.time : undefined; const toolMessageId = openCodeRuntimeId( requireString(toolPart.part.id, 'tool part ID', maxIdentifierBytes), ); messages.push({ id: toolMessageId, role: 'tool', text: '', createdAt: optionalFiniteNumber(stateTime?.start) ?? createdAt, updatedAt: optionalFiniteNumber(stateTime?.end), streaming: toolPart.call.status === 'pending' || toolPart.call.status === 'running', toolCall: toolPart.call, }); } const structuralDigest = plugins.crypto .createHash('sha256') .update(JSON.stringify({ sourceMessageId: controllerRuntimeIdKey(sourceMessageId), entries: messages.map((message) => ({ id: controllerRuntimeIdKey(message.id), type: message.role, })), })) .digest('hex'); return { sourceMessageId, structuralDigest, sourceRole: role, ...(role === 'assistant' && typeof info.parentID === 'string' ? { parentMessageId: openCodeRuntimeId( requireString(info.parentID, 'assistant parent message ID', maxIdentifierBytes), ), } : {}), terminal: role === 'user' || completedAt !== undefined || info.error !== undefined, messages, }; }; const normalizeOpenCodeMessageBundlesWithPartCount = ( value: unknown, expectedSessionIdArg: string | undefined, aggregatePartCount: { value: number }, ): IOpenCodeNormalizedMessageBundle[] => { if (!Array.isArray(value)) { throw new Error('OpenCode returned an invalid message list.'); } if (value.length > maxMessagesPerRequest) { throw new Error('OpenCode returned too many messages in one response.'); } const result: IOpenCodeNormalizedMessageBundle[] = []; for (const bundle of value) { result.push(normalizeOpenCodeMessageBundle(bundle, aggregatePartCount, expectedSessionIdArg)); } return result; }; export const normalizeOpenCodeMessageBundles = ( value: unknown, expectedSessionIdArg?: string, ): IOpenCodeNormalizedMessageBundle[] => normalizeOpenCodeMessageBundlesWithPartCount( value, expectedSessionIdArg, { value: 0 }, ); export const normalizeOpenCodeMessages = ( value: unknown, expectedSessionIdArg?: string, ): IControllerMessage[] => { const result = normalizeOpenCodeMessageBundles(value, expectedSessionIdArg) .flatMap((bundle) => bundle.messages); // A transcript never fails over its size: the oldest entries drop first. return trimOldestMessagesToFit(result, maxNormalizedMessageBytes); }; export const mergeOpenCodeToolExecutionsIntoDetail = < TDetail extends { messages: IControllerMessage[] }, >( detailArg: TDetail, executionsArg: readonly IControllerToolExecution[], ): Omit & { messages: IControllerMessage[] } => { const messages = detailArg.messages.map((messageArg) => ({ ...messageArg })); for (const execution of executionsArg) { const { sessionId: _sessionId, messageId: _messageId, partId, callId, toolName, order, sourceUpdatedAt, revision: _revision, streamEpoch: _streamEpoch, ...state } = execution; const toolCall: IControllerToolCall = { id: callId, name: toolName, ...state, }; const existingIndex = messages.findIndex((messageArg) => ( controllerRuntimeIdsEqual(messageArg.id, partId) )); if (existingIndex >= 0) { const existingCall = messages[existingIndex].toolCall; if (existingCall) { const existingTerminal = existingCall.status === 'completed' || existingCall.status === 'error'; if (existingTerminal) { if ( controllerRuntimeIdsEqual(existingCall.id, execution.callId) && existingCall.name === execution.toolName && existingCall.status === execution.status && ( execution.finishedAt === undefined || ( existingCall.finishedAt !== undefined && existingCall.finishedAt >= execution.finishedAt ) ) ) { messages[existingIndex] = { ...messages[existingIndex], toolCall: { ...existingCall, ...(existingCall.input === undefined && toolCall.input !== undefined ? { input: toolCall.input } : {}), // A bounded payload and its truncation flag are one unit: supplementing the // payload without the flag would claim a complete text the snapshot never had. ...(existingCall.output === undefined && toolCall.output !== undefined ? { output: toolCall.output, ...(toolCall.outputTruncated ? { outputTruncated: true as const } : {}), } : {}), ...(existingCall.errorText === undefined && toolCall.errorText !== undefined ? { errorText: toolCall.errorText, ...(toolCall.errorTextTruncated ? { errorTextTruncated: true as const } : {}), } : {}), ...(existingCall.exitCode === undefined && toolCall.exitCode !== undefined ? { exitCode: toolCall.exitCode } : {}), ...(existingCall.startedAt === undefined && toolCall.startedAt !== undefined ? { startedAt: toolCall.startedAt } : {}), ...(existingCall.finishedAt === undefined && toolCall.finishedAt !== undefined ? { finishedAt: toolCall.finishedAt } : {}), ...(existingCall.childSessionId === undefined && toolCall.childSessionId !== undefined ? { childSessionId: toolCall.childSessionId } : {}), ...(existingCall.model === undefined && toolCall.model !== undefined ? { model: toolCall.model } : {}), ...(existingCall.title === undefined && toolCall.title !== undefined ? { title: toolCall.title } : {}), }, }; } continue; } if (existingCall.status === 'running' && execution.status === 'pending') continue; const existingSourceTime = existingCall.finishedAt ?? existingCall.startedAt ?? 0; if (sourceUpdatedAt < existingSourceTime) continue; } messages[existingIndex] = { ...messages[existingIndex], ...(order === undefined ? {} : { order }), toolCall, }; continue; } messages.push({ id: partId, role: 'tool', text: '', createdAt: execution.startedAt ?? sourceUpdatedAt, ...(order === undefined ? {} : { order }), ...(execution.finishedAt === undefined ? {} : { updatedAt: execution.finishedAt }), toolCall, }); } const detailWithoutMessages = { ...detailArg, messages: [] }; const otherPartsBytes = Buffer.byteLength(JSON.stringify(detailWithoutMessages), 'utf8'); const boundedMessages = trimOldestMessagesToFit( messages, Math.max(64 * 1024, maxNormalizedSessionDetailBytes - otherPartsBytes), ); const result = { ...detailArg, messages: boundedMessages }; assertSerializedOutputWithinLimit( result, maxNormalizedSessionDetailBytes + 64 * 1024, 'session detail with live tool snapshots', ); return result; }; export const createBoundedOpenCodeSelectedMessageSlice = ( messagesArg: readonly IControllerMessage[], ): IControllerMessage[] => { if (messagesArg.length > maxMessagesPerPage) { throw new Error(`An OpenCode message slice cannot exceed ${maxMessagesPerPage} entries.`); } const messages = messagesArg.map((message) => ({ ...message, ...(message.reasoning ? { reasoning: message.reasoning.map((reasoningPart) => ({ ...reasoningPart })) } : {}), ...(message.toolCall ? { toolCall: { ...message.toolCall } } : {}), })); const isWithinBudget = (): boolean => Buffer.byteLength(JSON.stringify(messages), 'utf8') <= maxNormalizedMessageBytes; if (isWithinBudget()) return messages; // Preserve every selected shell and its order. Payloads disappear from the // oldest entries first until the selected slice fits the transfer budget. for (const message of messages) { if (message.toolCall && Object.hasOwn(message.toolCall, 'input')) { delete message.toolCall.input; if (isWithinBudget()) return messages; } if (message.toolCall && Object.hasOwn(message.toolCall, 'output')) { delete message.toolCall.output; if (isWithinBudget()) return messages; } if (message.reasoning) { for (const reasoningPart of message.reasoning) { if (!reasoningPart.text) continue; reasoningPart.text = ''; if (isWithinBudget()) return messages; } } if (message.text) { message.text = ''; if (isWithinBudget()) return messages; } } throw new Error('Selected OpenCode message shells exceeded the aggregate output budget.'); }; export const normalizeOpenCodePermissions = (value: unknown): IControllerPermission[] => { if (!Array.isArray(value) || value.length > maxCollectionEntries) { throw new Error('OpenCode returned an invalid permission list.'); } const outputBudget = createOutputBudget(maxNormalizedPermissionBytes, 'permissions'); const metadataBudget: IUnknownBudget = { nodesRemaining: maxMetadataNodes, outputBudget, }; const result = value.map((permission) => { if (!isRecord(permission) || !Array.isArray(permission.patterns)) { throw new Error('OpenCode returned an invalid permission request.'); } if (permission.patterns.length > maxCollectionEntries) { throw new Error('OpenCode returned too many permission patterns.'); } consumeOutputBytes(outputBudget, normalizedNodeCostBytes); const type = consumeOutputString( outputBudget, requireString(permission.permission, 'permission type', maxTitleBytes) ); const patterns = permission.patterns.map((pattern) => consumeOutputString( outputBudget, requireString(pattern, 'permission pattern', maxMetadataStringBytes) ) ); const id = consumeOutputString( outputBudget, requireString(permission.id, 'permission ID', maxIdentifierBytes) ); const sessionId = consumeOutputString( outputBudget, requireString(permission.sessionID, 'permission session ID', maxIdentifierBytes) ); return { id: openCodeRuntimeId(id), sessionId: openCodeRuntimeId(sessionId), title: type, type, patterns, metadata: sanitizeRecord(permission.metadata, 'permission metadata', metadataBudget), }; }); assertSerializedOutputWithinLimit(result, maxNormalizedPermissionBytes, 'permissions'); return result; }; export const normalizeOpenCodeQuestions = (value: unknown): IControllerQuestion[] => { if (!Array.isArray(value) || value.length > maxCollectionEntries) { throw new Error('OpenCode returned an invalid question list.'); } const outputBudget = createOutputBudget(maxNormalizedQuestionBytes, 'questions'); const result = value.map((request) => { if (!isRecord(request) || !Array.isArray(request.questions)) { throw new Error('OpenCode returned an invalid question request.'); } if (request.questions.length === 0 || request.questions.length > maxQuestionsPerRequest) { throw new Error('OpenCode returned an unsupported question count.'); } consumeOutputBytes(outputBudget, normalizedNodeCostBytes); const questions = request.questions.map((question): IControllerQuestionItem => { if (!isRecord(question) || !Array.isArray(question.options)) { throw new Error('OpenCode returned an invalid question.'); } if (question.options.length > maxOptionsPerQuestion) { throw new Error('OpenCode returned too many question options.'); } consumeOutputBytes(outputBudget, normalizedNodeCostBytes); const options = question.options.map((option) => { if (!isRecord(option)) { throw new Error('OpenCode returned an invalid question option.'); } consumeOutputBytes(outputBudget, normalizedNodeCostBytes); return { label: consumeOutputString( outputBudget, requireString(option.label, 'question option label', maxTitleBytes) ), description: consumeOutputString( outputBudget, truncateUtf8( requireString(option.description, 'question option description', maxMetadataStringBytes), maxTitleBytes ) ), }; }); return { question: consumeOutputString( outputBudget, truncateUtf8( requireString(question.question, 'question text', maxMessageTextBytes), maxMetadataStringBytes ) ), header: consumeOutputString( outputBudget, requireString(question.header, 'question header', maxTitleBytes) ), options, // An explicit false matters downstream: the UI hides free-text input // only when custom answers are explicitly disallowed. ...(typeof question.multiple === 'boolean' ? { multiple: question.multiple } : {}), ...(typeof question.custom === 'boolean' ? { custom: question.custom } : {}), }; }); const toolCallId = isRecord(request.tool) && typeof request.tool.callID === 'string' ? openCodeRuntimeId( consumeOutputString( outputBudget, requireString(request.tool.callID, 'question tool call ID', maxIdentifierBytes) ), ) : undefined; const id = consumeOutputString( outputBudget, requireString(request.id, 'question request ID', maxIdentifierBytes) ); const sessionId = consumeOutputString( outputBudget, requireString(request.sessionID, 'question session ID', maxIdentifierBytes) ); return { id: openCodeRuntimeId(id), sessionId: openCodeRuntimeId(sessionId), questions, ...(toolCallId === undefined ? {} : { toolCallId }), }; }); assertSerializedOutputWithinLimit(result, maxNormalizedQuestionBytes, 'questions'); return result; }; export interface IPtyConnectionCallbacks { onData: (chunk: Buffer) => void; onMeta?: (cursor: number) => void; onClose: () => void; } export interface IPtyConnectionHandle { sendInput: (data: string) => void; close: () => void; } interface IOpenCodePty { /** OpenCode-native PTY ID; this is not a controller terminal ID. */ id: string; title: string; command: string; cwd: string; } export const normalizeOpenCodePtys = (value: unknown): IOpenCodePty[] => { if (!Array.isArray(value) || value.length > maxCollectionEntries) { throw new Error('OpenCode returned an invalid PTY list.'); } return value.map((pty) => { if (!isRecord(pty)) { throw new Error('OpenCode returned an invalid PTY session.'); } return { id: requireString(pty.id, 'PTY ID', maxIdentifierBytes), title: truncateUtf8( requireString(pty.title, 'PTY title', maxMetadataStringBytes), maxTitleBytes ), command: truncateUtf8( requireString(pty.command, 'PTY command', maxMetadataStringBytes), maxTitleBytes ), cwd: requireString(pty.cwd, 'PTY cwd', 4096), }; }); }; export const normalizeOpenCodeTodos = (value: unknown): IControllerTodo[] => { if (!Array.isArray(value) || value.length > maxCollectionEntries) { throw new Error('OpenCode returned an invalid todo list.'); } return value.map((todo) => { if (!isRecord(todo)) { throw new Error('OpenCode returned an invalid todo.'); } const status = todo.status; if ( status !== 'pending' && status !== 'in_progress' && status !== 'completed' && status !== 'cancelled' ) { throw new Error('OpenCode returned an unknown todo status.'); } const id = todo.id === undefined ? undefined : openCodeRuntimeId(requireString(todo.id, 'todo ID', maxIdentifierBytes)); return { ...(id === undefined ? {} : { id }), content: truncateUtf8( requireString(todo.content, 'todo content', maxMessageTextBytes), maxTitleBytes ), status: status as TControllerTodoStatus, }; }); }; const normalizeLiveToolExecution = ( partArg: Record, expectedSessionIdArg: string, eventTimeArg: unknown, ): TOpenCodeToolExecution => { if (partArg.type !== 'tool') { throw new Error('OpenCode returned a non-tool part for live tool normalization.'); } const nativeSessionId = requireString( partArg.sessionID, 'live tool session ID', maxIdentifierBytes, ); if (nativeSessionId !== expectedSessionIdArg) { throw new Error('OpenCode returned a live tool part for the wrong session.'); } const sessionId = openCodeRuntimeId(nativeSessionId); const messageId = openCodeRuntimeId( requireString(partArg.messageID, 'live tool message ID', maxIdentifierBytes), ); const partId = openCodeRuntimeId( requireString(partArg.id, 'live tool part ID', maxIdentifierBytes), ); const call = normalizeToolCall(partArg); const sourceUpdatedAt = optionalFiniteNumber(eventTimeArg) ?? call.finishedAt ?? call.startedAt ?? Date.now(); const { id: callId, name: toolName, ...state } = call; const result: TOpenCodeToolExecution = { sessionId, messageId, partId, callId, toolName, ...state, sourceUpdatedAt, }; // The transcript bound in `normalizeToolCall` above applies to the durable message page and to // this snapshot alike, so a payload it shortened is identical on both sides. Only the live-only // bound below can make the two differ, and it therefore speaks the contract's signals. return boundLiveToolExecution(result, 'OpenCode'); }; type TNormalizedOpenCodeEvent = | { directory: string; event: IControllerEvent } | { directory: string; messageLifecycle: IOpenCodeMessageLifecycle; event: IControllerEvent; } | { directory: string; toolExecution: TOpenCodeToolExecution; timestamp: number; } | { directory: string; toolStreamGap: true }; type TOpenCodeLiveMessageUpdate = | { type: 'reasoning'; update: TOpenCodeReasoningUpdate } | { type: 'text'; update: TOpenCodeTextUpdate }; interface IOpenCodeLiveMessagePartCacheEntry { directory: string; type: TOpenCodeLiveMessageUpdate['type']; update: TOpenCodeLiveMessageUpdate['update']; ignored: boolean; bytes: number; } type TNormalizedOpenCodeMessageStreamEvent = | { directory: string; sessionNativeId: string; update: TOpenCodeLiveMessageUpdate } | { directory: string; sessionNativeId?: string; messageStreamGap: true } | { directory: string; sessionNativeId?: string; sharedStreamGap: true }; const messageStreamEventSessionId = (valueArg: unknown): string | undefined => { if (!isRecord(valueArg) || !isRecord(valueArg.payload) || !isRecord(valueArg.payload.properties)) { return undefined; } const type = valueArg.payload.type; if ( type !== 'message.part.updated' && type !== 'message.part.delta' && type !== 'message.part.removed' && type !== 'message.removed' ) return undefined; const sessionId = valueArg.payload.properties.sessionID; return typeof sessionId === 'string' ? sessionId : undefined; }; const normalizeEvent = ( value: unknown, ): TNormalizedOpenCodeEvent | undefined => { if ( !isRecord(value) || typeof value.directory !== 'string' || value.directory.length === 0 || Buffer.byteLength(value.directory, 'utf8') > 4096 || !isRecord(value.payload) ) { return undefined; } const directory = value.directory; const payload = value.payload; if (typeof payload.type !== 'string') { return undefined; } if (!isRecord(payload.properties)) return undefined; const properties = payload.properties; if (payload.type === 'message.updated' && isRecord(properties.info)) { const info = properties.info; try { const role = info.role; if (role !== 'user' && role !== 'assistant') return undefined; const sessionId = openCodeRuntimeId( requireString(info.sessionID, 'live message session ID', maxIdentifierBytes), ); const messageId = openCodeRuntimeId( requireString(info.id, 'live message ID', maxIdentifierBytes), ); const parentMessageId = role === 'assistant' ? openCodeRuntimeId( requireString(info.parentID, 'live assistant parent message ID', maxIdentifierBytes), ) : undefined; const terminal = role === 'user' || (isRecord(info.time) && optionalFiniteNumber(info.time.completed) !== undefined) || info.error !== undefined; return { directory, messageLifecycle: { sessionId, messageId, role, ...(parentMessageId === undefined ? {} : { parentMessageId }), terminal, }, event: { type: 'session.changed', harnessId: openCodeHarnessId, sessionId, timestamp: Date.now(), }, }; } catch { return undefined; } } if (payload.type === 'message.part.updated') { if (!isRecord(properties.part)) return { directory, toolStreamGap: true }; if (properties.part.type === 'tool') { try { const nativeSessionId = requireString( properties.sessionID, 'live tool event session ID', maxIdentifierBytes, ); const timestamp = optionalFiniteNumber(properties.time) ?? Date.now(); return { directory, toolExecution: normalizeLiveToolExecution( properties.part, nativeSessionId, properties.time, ), timestamp, }; } catch { return { directory, toolStreamGap: true }; } } } const sessionId = typeof properties.sessionID === 'string' && properties.sessionID.length > 0 && Buffer.byteLength(properties.sessionID, 'utf8') <= maxIdentifierBytes ? openCodeRuntimeId(properties.sessionID) : undefined; if (sessionId && payload.type === 'session.status') { return { directory, event: { type: 'session.changed', harnessId: openCodeHarnessId, sessionId, sessionStatus: normalizeSessionStatus(properties.status), timestamp: Date.now(), }, }; } if (sessionId && payload.type === 'session.idle') { return { directory, event: { type: 'session.changed', harnessId: openCodeHarnessId, sessionId, sessionStatus: 'idle', timestamp: Date.now(), }, }; } if (sessionId && payload.type === 'session.error') { return { directory, event: { type: 'session.changed', harnessId: openCodeHarnessId, sessionId, sessionError: true, timestamp: Date.now(), }, }; } if ( sessionId && (payload.type === 'session.created' || payload.type === 'session.updated' || payload.type === 'session.deleted') ) { return { directory, event: { type: 'sessions.changed', harnessId: openCodeHarnessId, sessionId, timestamp: Date.now(), }, }; } if (sessionId && payload.type.startsWith('permission.')) { return { directory, event: { type: 'permissions.changed', harnessId: openCodeHarnessId, sessionId, timestamp: Date.now(), }, }; } if ( sessionId && (payload.type.startsWith('message.') || payload.type.startsWith('session.') || payload.type === 'todo.updated' || payload.type.startsWith('question.') || payload.type === 'command.executed') ) { return { directory, event: { type: 'session.changed', harnessId: openCodeHarnessId, sessionId, timestamp: Date.now(), }, }; } if ( payload.type.startsWith('installation.') || payload.type === 'server.connected' || payload.type === 'global.disposed' || payload.type === 'server.instance.disposed' ) { return { directory, event: { type: 'harness.changed', harnessId: openCodeHarnessId, timestamp: Date.now(), }, }; } return undefined; }; const asError = (value: unknown, message: string): Error => { if (value instanceof Error) { return value; } return new Error(message, { cause: value }); }; const waitForAbortableDelay = async (delayMsArg: number, signalArg: AbortSignal): Promise => { if (signalArg.aborted) return; await new Promise((resolve) => { const finish = () => { clearTimeout(timer); signalArg.removeEventListener('abort', finish); resolve(); }; const timer = setTimeout(finish, delayMsArg); signalArg.addEventListener('abort', finish, { once: true }); }); }; export class OpenCodeClientAdapter implements IOpenCodeSessionIdentitySnapshotCapability { public readonly directory: string; public readonly baseUrl: string; private readonly client: TOpenCodeSdkClient; private readonly controlClient: TOpenCodeSdkClient; private readonly authorization: string; private eventAbortController?: AbortController; private eventTask?: Promise; private readonly runtimeRequestAbortController = new AbortController(); private readonly temporarySessionIds = new Set(); private readonly sessionMetricsCache = new Map(); private readonly liveMessagePartCache = new Map(); private liveMessagePartCacheBytes = 0; private readonly liveMessageEventIds = new Set(); constructor(options: IOpenCodeClientOptions) { this.directory = plugins.fs.realpathSync.native(options.directory); const parsedBaseUrl = new URL(options.baseUrl); if (parsedBaseUrl.protocol !== 'http:' || parsedBaseUrl.hostname !== '127.0.0.1') { throw new Error('OpenCode SDK traffic must use loopback HTTP.'); } parsedBaseUrl.pathname = '/'; parsedBaseUrl.search = ''; parsedBaseUrl.hash = ''; this.baseUrl = parsedBaseUrl.toString().replace(/\/$/u, ''); const username = validateInputString(options.username, 'OpenCode username', 128); const password = validateInputString(options.password, 'OpenCode password', 1024); const authorization = `Basic ${Buffer.from(`${username}:${password}`, 'utf8').toString('base64')}`; this.authorization = authorization; const clientFactory = options.clientFactory ?? plugins.opencodeSdk.createOpencodeClient; const boundedFetch = createBoundedOpenCodeFetch( this.baseUrl, options.fetchImplementation ?? globalThis.fetch, options.acquireRuntimeLease, this.runtimeRequestAbortController.signal, ); const clientOptions = { baseUrl: this.baseUrl, directory: this.directory, headers: { Authorization: authorization, }, responseStyle: 'data' as const, throwOnError: true as const, }; this.client = clientFactory({ ...clientOptions, fetch: boundedFetch, }); this.controlClient = clientFactory({ ...clientOptions, fetch: createBoundedOpenCodeFetch( this.baseUrl, options.fetchImplementation ?? globalThis.fetch, ), }); } public abortRuntimeRequests(reasonArg: unknown): void { if (!this.runtimeRequestAbortController.signal.aborted) { this.runtimeRequestAbortController.abort(reasonArg); } } private requireDirectory(directoryArg: string): string { if (typeof directoryArg !== 'string' || !plugins.path.isAbsolute(directoryArg)) { throw new Error('The OpenCode call requires an absolute project directory.'); } return directoryArg; } public async getStatuses( directoryArg: string, signal?: AbortSignal ): Promise> { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.status( { directory }, { signal: requestSignal } ) ); if (!isRecord(response)) { throw new Error('OpenCode returned an invalid session status map.'); } if (Object.keys(response).length > maxCollectionEntries) { throw new Error('OpenCode returned too many session statuses.'); } const result: Record = {}; for (const [sessionId, status] of Object.entries(response)) { if ( sessionId === '__proto__' || sessionId === 'constructor' || sessionId === 'prototype' ) { continue; } requireString(sessionId, 'session status ID', maxIdentifierBytes); result[sessionId] = normalizeSessionStatus(status); } return result; } public async listSessions( directoryArg: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const [sessions, statuses] = await Promise.all([ this.listNativeSessions(directory, signal), this.getStatuses(directory, signal), ]); return sessions.filter((session) => session.parentId === undefined) .map((session) => ({ ...session, status: statuses[session.id.nativeId] ?? 'idle' })); } public async [openCodeSessionIdentitySnapshotCapability]( directoryArg: string, signalArg?: AbortSignal, ): Promise { const sessions = await this.listNativeSessions(directoryArg, signalArg); return { sessions: sessions.map((session) => ({ nativeId: session.id.nativeId, providerSessionGeneration: openCodeSessionProviderGeneration(session.id.nativeId, session.createdAt), ...(session.parentId ? { parentNativeId: session.parentId.nativeId } : {}), })).sort((left, right) => left.nativeId.localeCompare(right.nativeId)) }; } /** V2 includes archived sessions and offers a lossless cursor for mature folders. */ private async listNativeSessions( directoryArg: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const projectResponse: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.project.current({ directory }, { signal: requestSignal }) )); if (!isRecord(projectResponse)) { throw new Error('OpenCode returned an invalid identity snapshot project.'); } const projectId = requireString( projectResponse.id, 'identity snapshot project ID', maxIdentifierBytes, ); const candidates: Array<{ session: IControllerSession; nativeId: string; metadataProbeRequired: boolean }> = []; const sourceIds = new Set(); const seenCursors = new Set(); let cursor: string | undefined; let sourceEntryCount = 0; let pageCount = 0; let aggregateCursorBytes = 0; while (true) { signalArg?.throwIfAborted(); pageCount += 1; const response: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.v2.session.list({ directory, project: projectId, order: 'asc', limit: openCodeIdentitySnapshotPageEntries, ...(cursor === undefined ? {} : { cursor }), }, { signal: requestSignal }) )); if ( !isRecord(response) || !Array.isArray(response.data) || response.data.length > openCodeIdentitySnapshotPageEntries || !isRecord(response.cursor) ) throw new Error('OpenCode returned an invalid paged identity session snapshot.'); if ( response.cursor.previous !== undefined && response.cursor.previous !== null && ( typeof response.cursor.previous !== 'string' || Buffer.byteLength(response.cursor.previous, 'utf8') > maxOpenCodeIdentityCursorBytes ) ) throw new Error('OpenCode returned an invalid identity snapshot previous cursor.'); const nextCursor = response.cursor.next === undefined || response.cursor.next === null ? undefined : validateInputString( response.cursor.next, 'Identity snapshot next cursor', maxOpenCodeIdentityCursorBytes, ); sourceEntryCount += response.data.length; if (sourceEntryCount > maxOpenCodeIdentitySnapshotSourceEntries) { throw new Error('The OpenCode identity session snapshot exceeds its source entry limit.'); } for (const sessionArg of response.data) { if ( !isRecord(sessionArg) || !isRecord(sessionArg.location) || !isRecord(sessionArg.time) ) throw new Error('OpenCode returned an invalid V2 identity session.'); const nativeId = requireString(sessionArg.id, 'session ID', maxIdentifierBytes); if (sourceIds.has(nativeId)) { throw new Error('OpenCode returned a duplicate paged identity session.'); } sourceIds.add(nativeId); if (sessionArg.projectID !== projectId) { throw new Error('OpenCode returned an identity session for the wrong project.'); } if (sessionArg.location.directory !== directory) { throw new Error('OpenCode returned an identity session outside the configured directory.'); } const title = requireString(sessionArg.title, 'session title', maxMessageTextBytes); candidates.push({ nativeId, session: normalizeOpenCodeSession({ ...sessionArg, directory }, directory), metadataProbeRequired: title === sessionIntelligenceTitle, }); } if (nextCursor === undefined) break; if (sourceEntryCount === maxOpenCodeIdentitySnapshotSourceEntries) { throw new Error('The OpenCode identity session snapshot exceeds its source entry limit.'); } if (pageCount >= maxOpenCodeIdentitySnapshotPages) { throw new Error('The OpenCode identity session snapshot exceeds its page limit.'); } aggregateCursorBytes += Buffer.byteLength(nextCursor, 'utf8'); if (aggregateCursorBytes > maxOpenCodeIdentityAggregateCursorBytes) { throw new Error('The OpenCode identity session snapshot exceeds its cursor budget.'); } if ( response.data.length === 0 || nextCursor === cursor || seenCursors.has(nextCursor) ) throw new Error('OpenCode identity snapshot pagination did not advance.'); seenCursors.add(nextCursor); cursor = nextCursor; } const metadataProbeCandidates = candidates.filter((candidateArg) => ( candidateArg.metadataProbeRequired && !this.temporarySessionIds.has(candidateArg.nativeId) )); if (metadataProbeCandidates.length > maxOpenCodeIdentityMetadataProbes) { throw new Error('The OpenCode identity snapshot metadata probe limit was exceeded.'); } const metadataTemporaryIds = new Set(); for ( let index = 0; index < metadataProbeCandidates.length; index += openCodeIdentityMetadataProbeConcurrency ) { const batchAbortController = new AbortController(); const batchSignal = signalArg === undefined ? batchAbortController.signal : AbortSignal.any([signalArg, batchAbortController.signal]); const probeTasks = metadataProbeCandidates.slice( index, index + openCodeIdentityMetadataProbeConcurrency, ).map(async (candidateArg) => { const response: unknown = await runSdkRequest(batchSignal, (requestSignal) => ( this.controlClient.session.get({ directory, sessionID: candidateArg.nativeId, }, { signal: requestSignal }) )); if (!isRecord(response)) { throw new Error('OpenCode returned an invalid identity metadata probe session.'); } const normalized = normalizeOpenCodeSession(response, directory, 'idle'); if ( normalized.id.nativeId !== candidateArg.nativeId || response.projectID !== projectId || response.title !== sessionIntelligenceTitle ) throw new Error('OpenCode returned a mismatched identity metadata probe session.'); return isSessionIntelligenceMetadata(response.metadata) ? candidateArg.nativeId : undefined; }); let probeResults: Array; try { probeResults = await Promise.all(probeTasks); } catch (errorArg) { batchAbortController.abort(errorArg); await Promise.allSettled(probeTasks); throw errorArg; } for (const nativeId of probeResults) { if (nativeId !== undefined) metadataTemporaryIds.add(nativeId); } } return candidates.filter((candidate) => !this.temporarySessionIds.has(candidate.nativeId) && !metadataTemporaryIds.has(candidate.nativeId)).map((candidate) => candidate.session); } public async getSessionReversionInfo( directoryArg: string, sessionIdArg: string, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); const [session, messages] = await Promise.all([ runSdkRequest(signal, (requestSignal) => this.client.session.get( { sessionID: sessionId, directory }, { signal: requestSignal }, )), this.readRawMessagePage(directory, sessionId, { limit: 1 }, signal), ]); normalizeOpenCodeSession(session, directory); if (!isRecord(session)) throw new Error('OpenCode returned an invalid session.'); const sessionRecord: Record = session; return { undoAvailable: messages.data.length > 0, redoAvailable: isRecord(sessionRecord.revert), groups: [], }; } public async getSession( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const [session, statuses] = await Promise.all([ this.getSessionSnapshot(directory, validatedSessionId, signal), this.getStatuses(directory, signal), ]); return { ...session, status: statuses[validatedSessionId] ?? 'idle' }; } public async getSessionAuthorityObservation( directoryArg: string, sessionIdArg: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); const response: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.session.get({ directory, sessionID: sessionId }, { signal: requestSignal }) )); if (!isRecord(response)) throw new Error('OpenCode returned an invalid session observation.'); const session = normalizeOpenCodeSession(response, directory, 'idle'); if (session.id.nativeId !== sessionId) { throw new Error('OpenCode returned a mismatched session observation.'); } return { session, providerSessionGeneration: openCodeSessionProviderGeneration(sessionId, session.createdAt), }; } public async listDirectChildSessionObservations( directoryArg: string, parentSessionIdArg: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const parentSessionId = validateInputString( parentSessionIdArg, 'Parent session ID', maxIdentifierBytes, ); const [response, statuses] = await Promise.all([ runSdkRequest(signalArg, (requestSignal) => this.controlClient.session.children( { directory, sessionID: parentSessionId }, { signal: requestSignal }, )), this.getStatuses(directory, signalArg), ]); if (!Array.isArray(response) || response.length > maxCollectionEntries) { throw new Error('OpenCode returned an invalid direct-child session collection.'); } const observations: IOpenCodeDirectChildObservation[] = []; const nativeIds = new Set(); for (const candidate of response) { if (!isRecord(candidate)) { throw new Error('OpenCode returned an invalid direct-child session observation.'); } const session = normalizeOpenCodeSession(candidate, directory, 'idle'); const nativeId = session.id.nativeId; if ( session.parentId?.harnessId !== openCodeHarnessId || session.parentId.nativeId !== parentSessionId || nativeIds.has(nativeId) ) { throw new Error('OpenCode returned a mismatched direct-child session observation.'); } nativeIds.add(nativeId); if ( this.temporarySessionIds.has(nativeId) || isSessionIntelligenceMetadata(candidate.metadata) ) continue; observations.push({ parentNativeId: parentSessionId, session: { ...session, status: statuses[nativeId] ?? 'idle' }, providerSessionGeneration: openCodeSessionProviderGeneration(nativeId, session.createdAt), }); } observations.sort((leftArg, rightArg) => ( leftArg.session.id.nativeId.localeCompare(rightArg.session.id.nativeId) )); return observations; } public async getEnrollableSession( directoryArg: string, sessionIdArg: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); if (this.temporarySessionIds.has(sessionId)) return undefined; const projectResponse: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.project.current({ directory }, { signal: requestSignal }) )); if (!isRecord(projectResponse)) throw new Error('OpenCode returned an invalid current project.'); const projectId = requireString(projectResponse.id, 'current project ID', maxIdentifierBytes); let response: unknown; try { response = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.session.get({ directory, sessionID: sessionId }, { signal: requestSignal }) )); } catch (errorArg) { if (isOpenCodeNotFoundError(errorArg)) return undefined; throw errorArg; } if (!isRecord(response)) throw new Error('OpenCode returned an invalid enrollment session.'); const session = normalizeOpenCodeSession(response, directory, 'idle'); if ( session.id.nativeId !== sessionId || response.projectID !== projectId || session.parentId !== undefined || isSessionIntelligenceMetadata(response.metadata) ) return undefined; return { session, providerSessionGeneration: openCodeSessionProviderGeneration(sessionId, session.createdAt), }; } public async getSessionSnapshot( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const response = await runSdkRequest(signal, (requestSignal) => this.client.session.get( { sessionID: validatedSessionId, directory, }, { signal: requestSignal } ) ); return normalizeOpenCodeSession(response, directory, 'idle'); } public async getSessionIfPresent( directoryArg: string, sessionId: string, signal?: AbortSignal, ): Promise { try { return await this.getSessionSnapshot(directoryArg, sessionId, signal); } catch (errorArg) { if (isOpenCodeNotFoundError(errorArg)) return undefined; throw errorArg; } } public async updateSession( directoryArg: string, sessionId: string, patch: { title?: string; archivedAt?: number }, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); if (patch.title === undefined && patch.archivedAt === undefined) { throw new Error('A session update requires a title or an archive timestamp.'); } const validatedTitle = patch.title === undefined ? undefined : validateInputString(patch.title, 'Session title', maxTitleBytes); if ( patch.archivedAt !== undefined && (!Number.isFinite(patch.archivedAt) || patch.archivedAt <= 0) ) { throw new Error('The archive timestamp is invalid.'); } // Archived sessions are admitted only while idle and immediately leave the // active UI. Avoid a fallible supplemental read before irreversible dispatch. const statuses = patch.archivedAt === undefined ? await this.getStatuses(directory, signal) : {}; const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.update( { sessionID: validatedSessionId, directory, ...(validatedTitle === undefined ? {} : { title: validatedTitle }), ...(patch.archivedAt === undefined ? {} : { time: { archived: patch.archivedAt } }), }, { signal: requestSignal } ), patch.archivedAt === undefined ? defaultSdkRequestTimeoutMs : undefined, ); return normalizeOpenCodeSession(response, directory, statuses[validatedSessionId] ?? 'idle'); } public async createSession( directoryArg: string, title?: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedTitle = title === undefined ? undefined : validateInputString(title, 'Session title', maxTitleBytes); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.create( { directory, ...(validatedTitle ? { title: validatedTitle } : {}), }, { signal: requestSignal } ) ); return normalizeOpenCodeSession(response, directory, 'idle'); } public async createSessionWithId( directoryArg: string, sessionIdArg: string, titleArg?: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); const title = titleArg === undefined ? undefined : validateInputString(titleArg, 'Session title', maxTitleBytes); const response: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.v2.session.create({ id: sessionId, location: { directory }, }, { signal: requestSignal }) )); if ( !isRecord(response) || !isRecord(response.data) || !isRecord(response.data.location) || response.data.id !== sessionId || response.data.location.directory !== directory ) throw new Error('OpenCode returned an invalid exact-ID session creation result.'); if (title !== undefined) await this.updateSession(directory, sessionId, { title }, signalArg); const observation = await this.getEnrollableSession(directory, sessionId, signalArg); if (!observation) throw new Error('OpenCode did not expose the exact created session.'); return observation; } public async searchSessions( directoryArg: string, queryArg: string, signalArg?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const query = validateInputString(queryArg, 'Session search query', maxTitleBytes).trim(); if (!query) throw new Error('Session search query must not be empty.'); const projectResponse: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.project.current({ directory }, { signal: requestSignal }) )); if (!isRecord(projectResponse)) throw new Error('OpenCode returned an invalid search project.'); const projectId = requireString(projectResponse.id, 'search project ID', maxIdentifierBytes); const candidates: Array = []; const sourceIds = new Set(); const seenCursors = new Set(); let cursor: string | undefined; let sourceEntryCount = 0; let pageCount = 0; let aggregateCursorBytes = 0; let sourceTruncated = false; while (true) { signalArg?.throwIfAborted(); pageCount += 1; const response: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.v2.session.list({ directory, project: projectId, order: 'desc', search: query, limit: openCodeIdentitySnapshotPageEntries, ...(cursor === undefined ? {} : { cursor }), }, { signal: requestSignal }) )); if ( !isRecord(response) || !Array.isArray(response.data) || response.data.length > openCodeIdentitySnapshotPageEntries || !isRecord(response.cursor) ) throw new Error('OpenCode returned an invalid paged session search.'); const nextCursor = response.cursor.next === undefined || response.cursor.next === null ? undefined : validateInputString( response.cursor.next, 'Session search next cursor', maxOpenCodeIdentityCursorBytes, ); sourceEntryCount += response.data.length; if (sourceEntryCount > maxOpenCodeIdentitySnapshotSourceEntries) { throw new Error('The OpenCode session search exceeds its source entry limit.'); } for (const valueArg of response.data) { if (!isRecord(valueArg) || !isRecord(valueArg.location) || !isRecord(valueArg.time)) { throw new Error('OpenCode returned an invalid V2 search session.'); } const nativeId = requireString(valueArg.id, 'search session ID', maxIdentifierBytes); if (sourceIds.has(nativeId)) throw new Error('OpenCode returned a duplicate search session.'); sourceIds.add(nativeId); if (valueArg.projectID !== projectId || valueArg.location.directory !== directory) { throw new Error('OpenCode returned a search session outside the requested scope.'); } const parentNativeId = valueArg.parentID === undefined ? undefined : requireString(valueArg.parentID, 'search session parent ID', maxIdentifierBytes); const title = truncateUtf8( requireString(valueArg.title, 'search session title', maxMessageTextBytes), maxTitleBytes, ); const createdAt = requireFiniteNumber(valueArg.time.created, 'search session creation time'); const updatedAt = requireFiniteNumber(valueArg.time.updated, 'search session update time'); const archivedAt = optionalFiniteNumber(valueArg.time.archived); if ( parentNativeId === undefined && !this.temporarySessionIds.has(nativeId) && candidates.length <= maxOpenCodeSearchResults ) { candidates.push({ id: openCodeRuntimeId(nativeId), title, createdAt, updatedAt, ...(archivedAt ? { archivedAt } : {}), status: 'idle', metadataProbeRequired: title === sessionIntelligenceTitle, }); } if (candidates.length > maxOpenCodeSearchResults) sourceTruncated = true; } if (sourceTruncated || nextCursor === undefined) break; if (sourceEntryCount === maxOpenCodeIdentitySnapshotSourceEntries) { throw new Error('The OpenCode session search exceeds its source entry limit.'); } if (pageCount >= maxOpenCodeIdentitySnapshotPages) { throw new Error('The OpenCode session search exceeds its page limit.'); } aggregateCursorBytes += Buffer.byteLength(nextCursor, 'utf8'); if (aggregateCursorBytes > maxOpenCodeIdentityAggregateCursorBytes) { throw new Error('The OpenCode session search exceeds its cursor budget.'); } if (response.data.length === 0 || nextCursor === cursor || seenCursors.has(nextCursor)) { throw new Error('OpenCode session search pagination did not advance.'); } seenCursors.add(nextCursor); cursor = nextCursor; } const metadataCandidates = candidates.filter((candidateArg) => candidateArg.metadataProbeRequired); if (metadataCandidates.length > maxOpenCodeIdentityMetadataProbes) { throw new Error('The OpenCode session search metadata probe limit was exceeded.'); } const metadataTemporaryIds = new Set(); for ( let index = 0; index < metadataCandidates.length; index += openCodeIdentityMetadataProbeConcurrency ) { const results = await Promise.all(metadataCandidates.slice( index, index + openCodeIdentityMetadataProbeConcurrency, ).map(async (candidateArg) => { const response: unknown = await runSdkRequest(signalArg, (requestSignal) => ( this.controlClient.session.get({ directory, sessionID: candidateArg.id.nativeId, }, { signal: requestSignal }) )); if (!isRecord(response)) throw new Error('OpenCode returned an invalid search metadata session.'); const normalized = normalizeOpenCodeSession(response, directory, 'idle'); if (normalized.id.nativeId !== candidateArg.id.nativeId || response.projectID !== projectId) { throw new Error('OpenCode returned a mismatched search metadata session.'); } return isSessionIntelligenceMetadata(response.metadata) ? candidateArg.id.nativeId : undefined; })); for (const nativeId of results) { if (nativeId !== undefined) metadataTemporaryIds.add(nativeId); } } const statuses = await this.getStatuses(directory, signalArg); const sessions = candidates .filter((candidateArg) => !metadataTemporaryIds.has(candidateArg.id.nativeId)) .slice(0, maxOpenCodeSearchResults) .map(({ metadataProbeRequired: _metadataProbeRequired, ...candidateArg }) => ({ ...candidateArg, status: statuses[candidateArg.id.nativeId] ?? 'idle' as const, })) .sort((leftArg, rightArg) => rightArg.updatedAt - leftArg.updatedAt || leftArg.id.nativeId.localeCompare(rightArg.id.nativeId)); return { sessions, truncated: sourceTruncated }; } public async deleteSession( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.delete( { sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), directory, }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid session deletion result.'); } if (response) { this.sessionMetricsCache.delete(this.sessionMetricsCacheKey(directory, sessionId)); this.clearLiveMessagePartsForSession(directory, sessionId); } return response; } public async deleteSessionIfPresent( directoryArg: string, sessionIdArg: string, signal?: AbortSignal, ): Promise { try { const deleted = await this.deleteSession(directoryArg, sessionIdArg, signal); if (!deleted) throw new Error('OpenCode did not delete the session.'); } catch (errorArg) { if (!isOpenCodeNotFoundError(errorArg)) throw errorArg; const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); this.sessionMetricsCache.delete(this.sessionMetricsCacheKey(directory, sessionId)); this.clearLiveMessagePartsForSession(directory, sessionId); } } public async sendMessage( directoryArg: string, sessionId: string, text: string, model?: TControllerModelChoice, agent?: string, signal?: AbortSignal, messageIdArg?: string, ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); this.sessionMetricsCache.delete(this.sessionMetricsCacheKey(directory, validatedSessionId)); const validatedChoice = validateOpenCodeModelChoice(model); const validatedModel = validatedChoice === undefined ? undefined : { providerID: validatedChoice.providerID, modelID: validatedChoice.modelID, }; // The variant is a body-level sibling of the model reference in the OpenCode API. const validatedVariant = validatedChoice?.variant; const validatedAgent = agent === undefined ? undefined : validateInputString(agent, 'Agent name', 128); const messageID = messageIdArg === undefined ? undefined : validateInputString(messageIdArg, 'Message ID', maxIdentifierBytes); if (messageID !== undefined && !messageID.startsWith('msg')) { throw new Error('The OpenCode message ID must start with msg.'); } await runSdkRequest(signal, (requestSignal) => this.client.session.promptAsync( { sessionID: validatedSessionId, directory, ...(messageID === undefined ? {} : { messageID }), ...(validatedModel ? { model: validatedModel } : {}), ...(validatedVariant !== undefined ? { variant: validatedVariant } : {}), ...(validatedAgent !== undefined ? { agent: validatedAgent } : {}), parts: [ { type: 'text', text: validateInputString(text, 'Message text', maxPromptBytes), }, ], }, { signal: requestSignal } ) ); } public async sendMessageAndWait( directoryArg: string, sessionId: string, text: string, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); this.sessionMetricsCache.delete(this.sessionMetricsCacheKey(directory, validatedSessionId)); const response: unknown = await runSdkRequest(signal, (requestSignal) => ( this.client.session.prompt( { sessionID: validatedSessionId, directory, parts: [{ type: 'text', text: validateInputString(text, 'Message text', maxPromptBytes), }], }, { signal: requestSignal }, ) )); if (!isRecord(response) || !isRecord(response.info) || !Array.isArray(response.parts)) { throw new Error('OpenCode returned an invalid completed prompt response.'); } } public async sendHarnessControlMessageAndWait( directoryArg: string, sessionId: string, text: string, signal?: AbortSignal, timeoutMsArg = defaultSdkRequestTimeoutMs, ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => ( this.controlClient.session.prompt({ sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), directory, parts: [{ type: 'text', text: validateInputString(text, 'Message text', maxPromptBytes) }], }, { signal: requestSignal }) ), timeoutMsArg); if (!isRecord(response) || !isRecord(response.info) || !Array.isArray(response.parts)) { throw new Error('OpenCode returned an invalid completed control prompt response.'); } } public async sendHarnessControlMessage( directoryArg: string, sessionId: string, text: string, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); await runSdkRequest(signal, (requestSignal) => this.controlClient.session.promptAsync({ sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), directory, parts: [{ type: 'text', text: validateInputString(text, 'Message text', maxPromptBytes) }], }, { signal: requestSignal })); } public async abortSession( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.abort( { sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), directory, }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid abort result.'); } return response; } public async setOpenCodeOAuthAuth( authArg: IFlexOpenCodeOAuthAuth, signal?: AbortSignal, ): Promise { try { const response: unknown = await runSdkRequest(signal, (requestSignal) => ( this.controlClient.auth.set( { providerID: 'openai', auth: { ...authArg } }, { signal: requestSignal }, ) )); if (response !== true) throw new Error('OpenCode returned an invalid auth update result.'); } catch { // SDK errors may retain request options. Never preserve a cause containing auth material. throw new Error('OpenCode rejected the replacement OpenAI authentication.'); } } public async listMessages( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const aggregatePartCount = { value: 0 }; const pages: IOpenCodeNormalizedMessageBundle[][] = []; const seenCursors = new Set(); let before: string | undefined; let sourceMessageCount = 0; for ( let pageIndex = 0; pageIndex < Math.ceil(maxMessagesPerRequest / maxMessagesPerPage); pageIndex += 1 ) { const page = await this.readRawMessagePage( directory, validatedSessionId, { limit: Math.min(maxMessagesPerPage, maxMessagesPerRequest - sourceMessageCount), ...(before === undefined ? {} : { before }), }, signal, ); if (page.data.length === 0 && page.nextCursor !== undefined) { throw new Error('OpenCode returned an empty message page with a continuation cursor.'); } const bundles = normalizeOpenCodeMessageBundlesWithPartCount( page.data, validatedSessionId, aggregatePartCount, ); pages.unshift(bundles); sourceMessageCount += bundles.length; if (page.nextCursor === undefined || sourceMessageCount >= maxMessagesPerRequest) break; if (seenCursors.has(page.nextCursor)) { throw new Error('OpenCode repeated a message page cursor.'); } seenCursors.add(page.nextCursor); before = page.nextCursor; } return trimOldestMessagesToFit( pages.flat(2).flatMap((bundle) => bundle.messages), maxNormalizedMessageBytes, ); } private async readRawMessagePage( directoryArg: string, sessionIdArg: string, optionsArg: { limit: number; before?: string }, signal?: AbortSignal, ): Promise<{ data: unknown[]; nextCursor?: string }> { const fields: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.messages( { sessionID: validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes), directory: this.requireDirectory(directoryArg), limit: optionsArg.limit, ...(optionsArg.before === undefined ? {} : { before: validateInputString(optionsArg.before, 'Message page cursor', 4096), }), }, { signal: requestSignal, responseStyle: 'fields' }, ), ); if (!isRecord(fields) || !(fields.response instanceof Response) || !Array.isArray(fields.data)) { throw new Error('OpenCode returned invalid message page response fields.'); } if (fields.data.length > optionsArg.limit) { throw new Error('OpenCode returned too many messages in one page.'); } const cursorHeader = fields.response.headers.get('x-next-cursor'); const nextCursor = cursorHeader === null ? undefined : requireString(cursorHeader, 'message page cursor', 4096); return { data: fields.data, ...(nextCursor === undefined ? {} : { nextCursor }), }; } public async listMessagePage( directoryArg: string, sessionId: string, options: { limit: number; before?: string }, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > maxMessagesPerPage) { throw new Error(`Message page limit must be an integer from 1 to ${maxMessagesPerPage}.`); } const before = options.before === undefined ? undefined : validateInputString(options.before, 'Message page cursor', 4096); const page = await this.readRawMessagePage( directory, sessionId, { limit: options.limit, ...(before === undefined ? {} : { before }) }, signal, ); const bundles = normalizeOpenCodeMessageBundles(page.data, sessionId); return { bundles, ...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }), }; } public async getSessionMetrics( directoryArg: string, sessionIdArg: string, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); const cacheKey = this.sessionMetricsCacheKey(directory, sessionId); const cached = this.sessionMetricsCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) return cached.promise; const entry: ISessionMetricsCacheEntry = { promise: Promise.resolve({}), expiresAt: Number.MAX_SAFE_INTEGER, }; const calculation = this.calculateSessionMetrics(directory, sessionId, signal); entry.promise = calculation; this.sessionMetricsCache.delete(cacheKey); this.sessionMetricsCache.set(cacheKey, entry); while (this.sessionMetricsCache.size > maxSessionMetricsCacheEntries) { const oldestKey = this.sessionMetricsCache.keys().next().value; if (oldestKey === undefined) break; this.sessionMetricsCache.delete(oldestKey); } calculation.then( () => { if (this.sessionMetricsCache.get(cacheKey) === entry) { entry.expiresAt = Date.now() + sessionMetricsCacheTtlMs; } }, () => { if (this.sessionMetricsCache.get(cacheKey) === entry) { this.sessionMetricsCache.delete(cacheKey); } }, ); return calculation; } private sessionMetricsCacheKey(directoryArg: string, sessionIdArg: string): string { return `${directoryArg}\0${sessionIdArg}`; } private async calculateSessionMetrics( directory: string, sessionId: string, signal?: AbortSignal, ): Promise { let before: string | undefined; const seenCursors = new Set(); let exhaustive = false; let lifetimeUsedTokens = 0; let lifetimeProvable = true; let compactionCount = 0; let latestCompaction: | { createdAt: number; id: string; contextTokens?: number } | undefined; let latestCurrent: | { createdAt: number; id: string; contextTokens: number; providerId: string; modelId: string } | undefined; let latestMessage: { createdAt: number; id: string } | undefined; const tokenFacts = (infoArg: Record): { lifetime: number; context: number; } | undefined => { if (!isRecord(infoArg.tokens) || !isRecord(infoArg.tokens.cache)) return undefined; const values = [ infoArg.tokens.input, infoArg.tokens.output, infoArg.tokens.reasoning, infoArg.tokens.cache.read, infoArg.tokens.cache.write, ]; if (!values.every((value) => Number.isSafeInteger(value) && (value as number) >= 0)) { return undefined; } const [input, output, reasoning, cacheRead, cacheWrite] = values as number[]; const lifetime = input + output + reasoning + cacheRead + cacheWrite; const context = input + cacheRead + cacheWrite; if (!Number.isSafeInteger(lifetime) || !Number.isSafeInteger(context)) return undefined; return { lifetime, context }; }; for (let pageIndex = 0; pageIndex < maxExhaustiveMessagePages; pageIndex += 1) { const page = await this.readRawMessagePage( directory, sessionId, { limit: maxMessagesPerPage, ...(before === undefined ? {} : { before }) }, signal, ); for (const bundle of page.data) { if (!isRecord(bundle) || !isRecord(bundle.info) || !Array.isArray(bundle.parts)) { throw new Error('OpenCode returned an invalid message while calculating metrics.'); } const info = bundle.info; if (info.role !== 'assistant' && info.role !== 'user') continue; const id = requireString(info.id, 'message ID', maxIdentifierBytes); const createdAt = isRecord(info.time) ? requireFiniteNumber(info.time.created, 'message creation time') : (() => { throw new Error('OpenCode returned an invalid message time.'); })(); if ( !latestMessage || createdAt > latestMessage.createdAt || (createdAt === latestMessage.createdAt && id > latestMessage.id) ) { latestMessage = { createdAt, id }; } if (info.role !== 'assistant') continue; const usage = tokenFacts(info); if (!usage) { lifetimeProvable = false; } else if (lifetimeProvable) { lifetimeUsedTokens += usage.lifetime; if (!Number.isSafeInteger(lifetimeUsedTokens)) lifetimeProvable = false; } const completed = typeof info.finish === 'string' && info.finish.length > 0 && info.error === undefined && isRecord(info.time) && optionalFiniteNumber(info.time.completed) !== undefined; if (info.summary === true && completed) { compactionCount += 1; if ( !latestCompaction || createdAt > latestCompaction.createdAt || (createdAt === latestCompaction.createdAt && id > latestCompaction.id) ) { latestCompaction = { createdAt, id, ...(usage === undefined ? {} : { contextTokens: usage.context }), }; } } else if (completed && usage) { const providerId = requireString(info.providerID, 'message provider ID', 256); const modelId = requireString(info.modelID, 'message model ID', 256); if ( !latestCurrent || createdAt > latestCurrent.createdAt || (createdAt === latestCurrent.createdAt && id > latestCurrent.id) ) { latestCurrent = { createdAt, id, contextTokens: usage.context, providerId, modelId }; } } } if (!page.nextCursor) { exhaustive = true; break; } if (seenCursors.has(page.nextCursor)) { throw new Error('OpenCode repeated a message page cursor.'); } seenCursors.add(page.nextCursor); before = page.nextCursor; } if (!exhaustive) return {}; const currentCandidate = latestCurrent; const current = currentCandidate !== undefined && latestMessage !== undefined && currentCandidate.createdAt === latestMessage.createdAt && currentCandidate.id === latestMessage.id && ( latestCompaction === undefined || currentCandidate.createdAt > latestCompaction.createdAt || ( currentCandidate.createdAt === latestCompaction.createdAt && currentCandidate.id > latestCompaction.id ) ) ? currentCandidate : undefined; let maxContextTokens: number | undefined; if (current) { const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.config.providers( { directory }, { signal: requestSignal }, ), ); if (!isRecord(response) || !Array.isArray(response.providers)) { throw new Error('OpenCode returned an invalid provider list.'); } const provider = response.providers.find( (entry) => isRecord(entry) && entry.id === current.providerId, ); const model = isRecord(provider) && isRecord(provider.models) ? provider.models[current.modelId] : undefined; const contextLimit = isRecord(model) && isRecord(model.limit) ? model.limit.context : undefined; if (Number.isSafeInteger(contextLimit) && (contextLimit as number) > 0) { maxContextTokens = contextLimit as number; } } return { ...(lifetimeProvable ? { lifetimeUsedTokens } : {}), compactionCount, ...(latestCompaction?.contextTokens === undefined ? {} : { tokensBeforeLatestCompaction: latestCompaction.contextTokens }), ...(current === undefined ? {} : { currentContextTokens: current.contextTokens }), ...(maxContextTokens === undefined ? {} : { maxContextTokens }), }; } public async buildSessionIntelligenceTranscript( directoryArg: string, sessionIdArg: string, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const sessionId = validateInputString(sessionIdArg, 'Session ID', maxIdentifierBytes); const messages: IControllerMessage[] = []; const seenCursors = new Set(); let before: string | undefined; let earlierOmitted = false; for (let pageIndex = 0; pageIndex < maxExhaustiveMessagePages; pageIndex += 1) { const page = await this.listMessagePage( directory, sessionId, { limit: maxMessagesPerPage, ...(before === undefined ? {} : { before }) }, signal, ); const pageMessages = page.bundles.flatMap((bundle) => bundle.messages); messages.unshift(...pageMessages); while ( messages.length > maxIntelligenceTranscriptMessages || Buffer.byteLength(JSON.stringify(messages), 'utf8') > maxIntelligenceTranscriptBytes ) { messages.shift(); earlierOmitted = true; } if (!page.nextCursor) break; if (seenCursors.has(page.nextCursor)) { throw new Error('OpenCode repeated a message page cursor.'); } seenCursors.add(page.nextCursor); before = page.nextCursor; if (earlierOmitted) break; if (pageIndex === maxExhaustiveMessagePages - 1) earlierOmitted = true; } const transcript = JSON.stringify({ ...(earlierOmitted ? { notice: 'Earlier messages were omitted to keep the analysis transcript bounded.' } : {}), messages: messages.map((message) => ({ role: message.role, createdAt: message.createdAt, ...(message.model === undefined ? {} : { model: message.model }), ...(message.text ? { text: message.text } : {}), ...(message.error === undefined ? {} : { error: message.error }), ...(message.toolCall === undefined ? {} : { toolCall: message.toolCall }), })), }); if (Buffer.byteLength(transcript, 'utf8') > maxIntelligenceTranscriptBytes + 1024) { throw new Error('The bounded Session Intelligence transcript exceeded its output budget.'); } return transcript; } public async getMessageBundle( directoryArg: string, sessionId: string, messageId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedMessageId = validateInputString( messageId, 'Message ID', maxIdentifierBytes ); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.message( { sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), messageID: validatedMessageId, directory, }, { signal: requestSignal } ) ); const bundles = normalizeOpenCodeMessageBundles([response], sessionId); if ( bundles.length !== 1 || !controllerRuntimeIdsEqual( bundles[0].sourceMessageId, openCodeRuntimeId(validatedMessageId), ) ) { throw new Error('OpenCode returned a different message than requested.'); } return bundles[0]; } public async runBuiltinSessionCommand( directoryArg: string, sessionId: string, command: TControllerBuiltinCommand, model?: TControllerModelChoice, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const sessionID = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); this.sessionMetricsCache.delete(this.sessionMetricsCacheKey(directory, sessionID)); const validatedModel = validateOpenCodeModelChoice(model, maxIdentifierBytes); const modelReference = validatedModel ? { providerID: validatedModel.providerID, modelID: validatedModel.modelID, } : {}; switch (command) { case 'compact': // Summarization runs a model call and blocks until done. await runSdkRequest(signal, (requestSignal) => this.client.session.summarize( { sessionID, directory, ...modelReference }, { signal: requestSignal } ), builtinModelCommandTimeoutMs); return; case 'undo': await runSdkRequest(signal, (requestSignal) => this.client.session.revert( { sessionID, directory }, { signal: requestSignal } )); return; case 'redo': await runSdkRequest(signal, (requestSignal) => this.client.session.unrevert( { sessionID, directory }, { signal: requestSignal } )); return; case 'init': // AGENTS.md generation runs a model call and blocks until done. await runSdkRequest(signal, (requestSignal) => this.client.session.init( { sessionID, directory, ...modelReference }, { signal: requestSignal } ), builtinModelCommandTimeoutMs); return; default: { const exhaustive: never = command; throw new Error(`Unsupported builtin command: ${String(exhaustive)}`); } } } public async listPermissions( directoryArg: string, sessionId?: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = sessionId === undefined ? undefined : validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.permission.list( { directory }, { signal: requestSignal } ) ); const permissions = normalizeOpenCodePermissions(response); return validatedSessionId ? permissions.filter((permission) => permission.sessionId.nativeId === validatedSessionId) : permissions; } public async listQuestions( directoryArg: string, sessionId?: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedSessionId = sessionId === undefined ? undefined : validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.question.list( { directory }, { signal: requestSignal } ) ); const questions = normalizeOpenCodeQuestions(response); return validatedSessionId ? questions.filter((question) => question.sessionId.nativeId === validatedSessionId) : questions; } public async replyQuestion( directoryArg: string, requestId: string, answers: string[][], signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); if ( answers.length === 0 || answers.some((answerList) => answerList.length === 0) ) { throw new Error('Question answers are invalid.'); } const validatedAnswers = answers.map((answerList) => answerList.map((answer) => validateInputString(answer, 'Question answer', maxMetadataStringBytes) ) ); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.question.reply( { requestID: validateInputString(requestId, 'Question request ID', maxIdentifierBytes), directory, answers: validatedAnswers, }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid question result.'); } return response; } public async rejectQuestion( directoryArg: string, requestId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.question.reject( { requestID: validateInputString(requestId, 'Question request ID', maxIdentifierBytes), directory, }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid question result.'); } return response; } public async replyPermission( directoryArg: string, requestId: string, reply: TControllerPermissionReply, message?: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); if (reply !== 'once' && reply !== 'reject') { throw new Error('Permission reply is invalid.'); } const validatedMessage = message === undefined ? undefined : validateInputString(message, 'Permission message', maxTitleBytes, true); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.permission.reply( { requestID: validateInputString(requestId, 'Permission request ID', maxIdentifierBytes), directory, reply, ...(validatedMessage === undefined ? {} : { message: validatedMessage }), }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid permission result.'); } return response; } public async listPtys( directoryArg: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.pty.list( { directory }, { signal: requestSignal } ) ); return normalizeOpenCodePtys(response); } public async createPty( directoryArg: string, title?: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedTitle = title === undefined ? undefined : validateInputString(title, 'Terminal title', maxTitleBytes); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.pty.create( { directory, cwd: directory, ...(validatedTitle === undefined ? {} : { title: validatedTitle }), }, { signal: requestSignal } ) ); const normalized = normalizeOpenCodePtys([response]); if (normalized.length !== 1) { throw new Error('OpenCode returned an invalid PTY session.'); } return normalized[0]; } public async updatePty( directoryArg: string, ptyId: string, patch: { title?: string; size?: { rows: number; cols: number } }, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const validatedPtyId = validateInputString(ptyId, 'PTY ID', maxIdentifierBytes); const validatedTitle = patch.title === undefined ? undefined : validateInputString(patch.title, 'Terminal title', maxTitleBytes); if ( patch.size !== undefined && (!Number.isSafeInteger(patch.size.rows) || !Number.isSafeInteger(patch.size.cols) || patch.size.rows < 2 || patch.size.cols < 2 || patch.size.rows > 500 || patch.size.cols > 500) ) { throw new Error('The terminal size is invalid.'); } const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.pty.update( { ptyID: validatedPtyId, directory, ...(validatedTitle === undefined ? {} : { title: validatedTitle }), ...(patch.size === undefined ? {} : { size: { rows: patch.size.rows, cols: patch.size.cols } }), }, { signal: requestSignal } ) ); const normalized = normalizeOpenCodePtys([response]); if (normalized.length !== 1) { throw new Error('OpenCode returned an invalid PTY session.'); } return normalized[0]; } public async removePty( directoryArg: string, ptyId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.pty.remove( { ptyID: validateInputString(ptyId, 'PTY ID', maxIdentifierBytes), directory, }, { signal: requestSignal } ) ); if (typeof response !== 'boolean') { throw new Error('OpenCode returned an invalid PTY removal result.'); } return response; } /** * Opens the PTY relay socket. The ticket flow mirrors the official web * client: mint a short-lived single-use ticket, then upgrade without Basic * auth. cursor 0 requests a full scrollback replay. */ public async connectPty( directoryArg: string, ptyId: string, cursor: number, callbacks: IPtyConnectionCallbacks ): Promise { const directory = this.requireDirectory(directoryArg); const validatedPtyId = validateInputString(ptyId, 'PTY ID', maxIdentifierBytes); const ticketResponse = await fetch( `${this.baseUrl}/pty/${encodeURIComponent(validatedPtyId)}/connect-token` + `?directory=${encodeURIComponent(directory)}`, { method: 'POST', headers: { authorization: this.authorization, 'x-opencode-ticket': '1', }, }, ); if (!ticketResponse.ok) { throw new Error(`OpenCode refused a PTY connect ticket (${ticketResponse.status}).`); } const ticketBody: unknown = await ticketResponse.json(); const ticket = isRecord(ticketBody) && typeof ticketBody.ticket === 'string' ? ticketBody.ticket : undefined; if (!ticket) { throw new Error('OpenCode returned an invalid PTY connect ticket.'); } const wsUrl = `${this.baseUrl.replace(/^http:/u, 'ws:')}` + `/pty/${encodeURIComponent(validatedPtyId)}/connect` + `?cursor=${encodeURIComponent(String(cursor))}` + `&ticket=${encodeURIComponent(ticket)}` + `&directory=${encodeURIComponent(directory)}`; const socket = new WebSocket(wsUrl); socket.binaryType = 'arraybuffer'; let closed = false; const finish = () => { if (closed) return; closed = true; callbacks.onClose(); }; socket.onmessage = (eventArg: MessageEvent) => { const payload: unknown = eventArg.data; if (typeof payload === 'string') { callbacks.onData(Buffer.from(payload, 'utf8')); return; } if (payload instanceof ArrayBuffer) { const bytes = Buffer.from(payload); // A 0x00-prefixed frame is the JSON meta frame carrying the cursor. if (bytes.length > 0 && bytes[0] === 0) { try { const meta: unknown = JSON.parse(bytes.subarray(1).toString('utf8')); if (isRecord(meta) && typeof meta.cursor === 'number') { callbacks.onMeta?.(meta.cursor); } } catch { // A malformed meta frame only loses resume information. } return; } callbacks.onData(bytes); } }; socket.onclose = finish; socket.onerror = finish; await new Promise((resolve, reject) => { const openTimeout = setTimeout(() => { try { socket.close(); } catch { // Never opened. } reject(new Error('The PTY relay connection timed out.')); }, 10_000); socket.onopen = () => { clearTimeout(openTimeout); resolve(); }; socket.addEventListener('close', () => { clearTimeout(openTimeout); reject(new Error('The PTY relay connection closed during setup.')); }, { once: true }); }); return { sendInput: (dataArg: string) => { if (socket.readyState === WebSocket.OPEN) { socket.send(dataArg); } }, close: () => { closed = true; try { socket.close(); } catch { // Already closed. } }, }; } public async listTodos( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.todo( { sessionID: validateInputString(sessionId, 'Session ID', maxIdentifierBytes), directory, }, { signal: requestSignal } ) ); return normalizeOpenCodeTodos(response); } public async listCommands( directoryArg: string, signal?: AbortSignal ): Promise> { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.command.list( { directory }, { signal: requestSignal } ) ); if (!Array.isArray(response)) { throw new Error('OpenCode returned an invalid command list.'); } if (response.length > 256) { throw new Error('OpenCode returned too many commands.'); } return response.map((command) => { if (!isRecord(command)) { throw new Error('OpenCode returned an invalid command.'); } const name = requireString(command.name, 'command name', 128); const template = truncateUtf8( requireString(command.template, 'command template', maxPromptBytes), maxPromptBytes, ); const description = typeof command.description === 'string' && command.description.length > 0 ? truncateUtf8(command.description, 1024) : undefined; const agent = typeof command.agent === 'string' && command.agent.length > 0 ? truncateUtf8(command.agent, 128) : undefined; const model = typeof command.model === 'string' && command.model.length > 0 ? truncateUtf8(command.model, 512) : undefined; return { name, ...(description !== undefined ? { description } : {}), template, ...(agent !== undefined ? { agent } : {}), ...(model !== undefined ? { model } : {}), }; }); } public async getSessionIntelligenceCapability( directoryArg: string, signal?: AbortSignal, ): Promise<{ enabled: boolean; model?: string; contextTokens?: number }> { const directory = this.requireDirectory(directoryArg); const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.config.providers( { directory }, { signal: requestSignal }, ), ); if (!isRecord(response) || !Array.isArray(response.providers)) { throw new Error('OpenCode returned an invalid provider list.'); } const provider = response.providers.find( (entry) => isRecord(entry) && entry.id === sessionIntelligenceProviderId, ); const model = isRecord(provider) && isRecord(provider.models) ? provider.models[sessionIntelligenceModelId] : undefined; if (!isRecord(model)) return { enabled: false }; const contextTokens = isRecord(model.limit) && Number.isSafeInteger(model.limit.context) && (model.limit.context as number) > 0 ? model.limit.context as number : undefined; return { enabled: true, model: sessionIntelligenceModel, ...(contextTokens === undefined ? {} : { contextTokens }), }; } public async createSessionIntelligenceSession( directoryArg: string, markerArg: ISessionIntelligenceMarker, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); if ( markerArg.sourceSessionId.harnessId !== 'opencode' && markerArg.sourceSessionId.harnessId !== 'flex' ) { throw new Error('Session Intelligence requires an OpenCode or Flex source session.'); } const capability = await this.getSessionIntelligenceCapability(directory, signal); if (!capability.enabled || capability.model !== sessionIntelligenceModel) { throw new Error(`${sessionIntelligenceModel} is not connected in OpenCode.`); } const metadata = { modelprofileSessionIntelligence: { schemaVersion: 1, controllerId: validateInputString(markerArg.controllerId, 'Controller ID', 512), projectId: validateInputString(markerArg.projectId, 'Project ID', 64), sourceSessionId: { harnessId: markerArg.sourceSessionId.harnessId, nativeId: validateInputString( markerArg.sourceSessionId.nativeId, 'Source session ID', maxIdentifierBytes, ), }, exchangeId: validateInputString(markerArg.exchangeId, 'Exchange ID', 128), }, }; const permission = [{ permission: '*', pattern: '*', action: 'deny' as const }]; const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.create( { directory, title: sessionIntelligenceTitle, agent: 'plan', model: { providerID: sessionIntelligenceProviderId, id: sessionIntelligenceModelId, }, metadata, permission, }, { signal: requestSignal }, ), ); if (!isRecord(response)) { throw new Error('OpenCode returned an invalid Session Intelligence session.'); } const sessionId = requireString(response.id, 'Session Intelligence session ID', maxIdentifierBytes); const responsePermission = response.permission; if ( response.directory !== directory || response.title !== sessionIntelligenceTitle || response.agent !== 'plan' || !isRecord(response.model) || !hasExactKeys(response.model, ['id', 'providerID']) || response.model.id !== sessionIntelligenceModelId || response.model.providerID !== sessionIntelligenceProviderId || !Array.isArray(responsePermission) || responsePermission.length !== 1 || !isRecord(responsePermission[0]) || !hasExactKeys(responsePermission[0], ['permission', 'pattern', 'action']) || responsePermission[0].permission !== '*' || responsePermission[0].pattern !== '*' || responsePermission[0].action !== 'deny' || !isSessionIntelligenceMetadata(response.metadata, markerArg.controllerId, markerArg) ) { const isolationError = new Error( 'OpenCode did not preserve the Session Intelligence isolation contract.', ); this.rememberTemporarySessionId(sessionId); try { await this.deleteSessionIntelligenceSession( directory, sessionId, AbortSignal.timeout(10_000), ); } catch (cleanupError) { throw new OpenCodeSessionIntelligenceCleanupError( sessionId, isolationError, cleanupError, ); } throw isolationError; } this.rememberTemporarySessionId(sessionId); return sessionId; } public async runSessionIntelligence( directoryArg: string, temporarySessionIdArg: string, promptArg: string, signal?: AbortSignal, ): Promise<{ answer: string; scratchpad: string; model: string }> { const directory = this.requireDirectory(directoryArg); const temporarySessionId = validateInputString( temporarySessionIdArg, 'Session Intelligence session ID', maxIdentifierBytes, ); const prompt = validateInputString(promptArg, 'Session Intelligence prompt', 512 * 1024); try { const response: unknown = await runSdkRequest( signal, (requestSignal) => this.client.session.prompt( { sessionID: temporarySessionId, directory, model: { providerID: sessionIntelligenceProviderId, modelID: sessionIntelligenceModelId, }, agent: 'plan', system: [ 'You are Session Intelligence. Analyze only the supplied session transcript and scratchpad.', 'Do not assume access to tools or files. Answer the question and return a concise updated scratchpad.', ].join(' '), format: { type: 'json_schema', retryCount: 2, schema: { type: 'object', additionalProperties: false, properties: { answer: { type: 'string', maxLength: 32768 }, scratchpad: { type: 'string', maxLength: 32768 }, }, required: ['answer', 'scratchpad'], }, }, parts: [{ type: 'text', text: prompt }], }, { signal: requestSignal }, ), sessionIntelligenceTimeoutMs, ); if (!isRecord(response) || !isRecord(response.info)) { throw new Error('OpenCode returned an invalid Session Intelligence response.'); } const responseInfo = response.info; if ( responseInfo.sessionID !== temporarySessionId || responseInfo.role !== 'assistant' || responseInfo.providerID !== sessionIntelligenceProviderId || responseInfo.modelID !== sessionIntelligenceModelId || responseInfo.agent !== 'plan' ) { throw new Error('OpenCode returned Session Intelligence output from a different execution context.'); } if (responseInfo.error !== undefined) { throw new Error( normalizeErrorText(responseInfo.error) ?? 'Session Intelligence model execution failed.', ); } const structured = responseInfo.structured; if ( !isRecord(structured) || !hasExactKeys(structured, ['answer', 'scratchpad']) || typeof structured.answer !== 'string' || typeof structured.scratchpad !== 'string' || structured.answer.length > 32_768 || structured.scratchpad.length > 32_768 || Buffer.byteLength(structured.answer, 'utf8') > 128 * 1024 || Buffer.byteLength(structured.scratchpad, 'utf8') > 128 * 1024 ) { throw new Error('OpenCode returned invalid structured Session Intelligence output.'); } return { answer: structured.answer, scratchpad: structured.scratchpad, model: sessionIntelligenceModel, }; } catch (errorArg) { await this.abortSession( directory, temporarySessionId, AbortSignal.timeout(10_000), ).catch(() => undefined); throw errorArg; } } public async deleteSessionIntelligenceSession( directoryArg: string, temporarySessionIdArg: string, signal?: AbortSignal, ): Promise { const sessionId = validateInputString( temporarySessionIdArg, 'Session Intelligence session ID', maxIdentifierBytes, ); this.rememberTemporarySessionId(sessionId); try { const deleted = await this.deleteSession(directoryArg, sessionId, signal); if (!deleted) throw new Error('OpenCode did not delete the Session Intelligence session.'); } catch (errorArg) { if (!isOpenCodeNotFoundError(errorArg)) throw errorArg; } this.temporarySessionIds.delete(sessionId); } public rememberSessionIntelligenceTemporarySessionIds(sessionIdsArg: string[]): void { if (sessionIdsArg.length > maxSessionIntelligenceCleanupObligations) { throw new Error('Too many Session Intelligence cleanup obligations were supplied.'); } for (const sessionId of sessionIdsArg) { this.rememberTemporarySessionId(validateInputString( sessionId, 'Session Intelligence session ID', maxIdentifierBytes, )); } } public getRememberedSessionIntelligenceTemporarySessionIds(): string[] { return [...this.temporarySessionIds]; } public async cleanupOrphanedSessionIntelligenceSessions( directoryArg: string, controllerIdArg: string, olderThanArg: number, signal?: AbortSignal, ): Promise { const directory = this.requireDirectory(directoryArg); const controllerId = validateInputString(controllerIdArg, 'Controller ID', 512); if (!Number.isFinite(olderThanArg) || olderThanArg < 0) { throw new Error('The Session Intelligence orphan cutoff is invalid.'); } let exhaustive = false; let start = 0; let deleted = 0; const deletionErrors: unknown[] = []; for (let pageIndex = 0; pageIndex < 16; pageIndex += 1) { const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.session.list( { directory, roots: true, search: sessionIntelligenceTitle, start, limit: maxMessagesPerRequest, }, { signal: requestSignal }, ), ); if (!Array.isArray(response) || response.length > maxMessagesPerRequest) { throw new Error('OpenCode returned an invalid Session Intelligence orphan list.'); } const candidateIds: string[] = []; for (const session of response) { if ( !isRecord(session) || !isRecord(session.time) || !isSessionIntelligenceMetadata(session.metadata, controllerId) || optionalFiniteNumber(session.time.created) === undefined || (session.time.created as number) >= olderThanArg ) continue; candidateIds.push( requireString(session.id, 'Session Intelligence session ID', maxIdentifierBytes), ); } let pageDeleted = 0; for (const candidateId of candidateIds) { this.rememberTemporarySessionId(candidateId); try { await this.deleteSessionIntelligenceSession(directory, candidateId, signal); deleted += 1; pageDeleted += 1; } catch (errorArg) { deletionErrors.push(errorArg); } } if (response.length < maxMessagesPerRequest) { exhaustive = true; break; } // Successful deletions shift later offset pages left. Advance only past // the sessions that remain so no unscanned session is skipped. start += response.length - pageDeleted; } if (deletionErrors.length > 0) { throw new AggregateError( deletionErrors, 'One or more orphaned Session Intelligence sessions could not be deleted.', ); } if (!exhaustive) { throw new Error( `Session Intelligence orphan cleanup deleted ${deleted} session(s) before exceeding its bounded scan.`, ); } return deleted; } private rememberTemporarySessionId(sessionIdArg: string): void { this.temporarySessionIds.delete(sessionIdArg); this.temporarySessionIds.add(sessionIdArg); while ( this.temporarySessionIds.size > maxSessionIntelligenceCleanupObligations ) { const oldestSessionId = this.temporarySessionIds.values().next().value; if (oldestSessionId === undefined) break; this.temporarySessionIds.delete(oldestSessionId); } } public async listModelOptions(signal?: AbortSignal): Promise { const response: unknown = await runSdkRequest(signal, (requestSignal) => this.client.config.providers( { directory: this.directory }, { signal: requestSignal } ) ); if (!isRecord(response) || !Array.isArray(response.providers)) { throw new Error('OpenCode returned an invalid provider list.'); } if (response.providers.length > 64) { throw new Error('OpenCode returned too many providers.'); } const options: TControllerModelOption[] = []; for (const provider of response.providers) { if (!isRecord(provider) || !isRecord(provider.models)) { throw new Error('OpenCode returned an invalid provider.'); } const providerID = requireString(provider.id, 'provider ID', 256); const providerName = typeof provider.name === 'string' && provider.name.length > 0 ? truncateUtf8(provider.name, 256) : providerID; const modelEntries = Object.entries(provider.models); if (modelEntries.length > 256) { throw new Error('OpenCode returned too many models for one provider.'); } for (const [modelKey, model] of modelEntries) { if ( modelKey === '__proto__' || modelKey === 'constructor' || modelKey === 'prototype' ) continue; const modelID = requireString(modelKey, 'model ID', 256); const modelName = isRecord(model) && typeof model.name === 'string' && model.name.length > 0 ? truncateUtf8(model.name, 256) : modelID; const variants = normalizeModelVariants(isRecord(model) ? model.variants : undefined); options.push({ harnessId: openCodeHarnessId, providerID, providerName, modelID, modelName, variants, }); } } if (options.length > 2048) { throw new Error('OpenCode returned too many models.'); } return options; } public async getSessionDetail( directoryArg: string, sessionId: string, signal?: AbortSignal ): Promise & { messagePage: IOpenCodeMessagePage }> { const directory = this.requireDirectory(directoryArg); const validatedSessionId = validateInputString(sessionId, 'Session ID', maxIdentifierBytes); const [session, messagePage, permissions, questions, todos] = await Promise.all([ this.getSession(directory, validatedSessionId, signal), this.listMessagePage( directory, validatedSessionId, { limit: controllerInitialMessageBundleLimit }, signal, ), this.listPermissions(directory, validatedSessionId, signal), this.listQuestions(directory, validatedSessionId, signal), this.listTodos(directory, validatedSessionId, signal), ]); const messages = messagePage.bundles.flatMap((bundle) => bundle.messages); let model: string | undefined; let effort: string | undefined; for (let index = messages.length - 1; index >= 0; index--) { const candidate = messages[index]; if (candidate.role === 'assistant' && candidate.model) { model = candidate.model; effort = candidate.effort; break; } } const detail = { session, messagePage, permissions, questions, todos, ...(model ? { model } : {}), ...(effort ? { effort } : {}), }; return detail; } public startEventStream(options: IOpenCodeEventStreamOptions): Promise { if (this.eventTask) { throw new Error('The OpenCode event stream is already running.'); } const abortController = new AbortController(); this.eventAbortController = abortController; const signal = options.signal ? AbortSignal.any([abortController.signal, options.signal]) : abortController.signal; const task = this.consumeEventStream(options, signal).finally(() => { this.clearLiveMessagePartCache(); }); this.eventTask = task; task.then( () => { if (this.eventTask === task) { this.eventTask = undefined; this.eventAbortController = undefined; } }, () => { if (this.eventTask === task) { this.eventTask = undefined; this.eventAbortController = undefined; } } ); return task; } public async stopEventStream(): Promise { const task = this.eventTask; if (!task) { this.clearLiveMessagePartCache(); return; } const abortController = this.eventAbortController; abortController?.abort(); try { await task; } catch (error) { if (!abortController?.signal.aborted) { throw error; } } finally { this.clearLiveMessagePartCache(); } } private liveMessagePartKey( directoryArg: string, sessionIdArg: string, messageIdArg: string, partIdArg: string, ): string { return JSON.stringify([directoryArg, sessionIdArg, messageIdArg, partIdArg]); } private clearLiveMessagePartCache(): void { this.liveMessagePartCache.clear(); this.liveMessagePartCacheBytes = 0; this.liveMessageEventIds.clear(); } private acceptLiveMessageEventId(payloadArg: Record): boolean { if (typeof payloadArg.id !== 'string' || Buffer.byteLength(payloadArg.id, 'utf8') > 512) { return true; } if (this.liveMessageEventIds.has(payloadArg.id)) return false; this.liveMessageEventIds.add(payloadArg.id); while (this.liveMessageEventIds.size > maxLiveMessageEventIds) { const oldest = this.liveMessageEventIds.values().next().value; if (oldest === undefined) break; this.liveMessageEventIds.delete(oldest); } return true; } private clearLiveMessagePartsForSession(directoryArg: string, sessionIdArg: string): void { for (const [key, entry] of this.liveMessagePartCache) { if ( entry.directory === directoryArg && entry.update.sessionId.nativeId === sessionIdArg ) { this.liveMessagePartCache.delete(key); this.liveMessagePartCacheBytes = Math.max(0, this.liveMessagePartCacheBytes - entry.bytes); } } } private clearLiveMessagePartsForMessage( directoryArg: string, sessionIdArg: string, messageIdArg: string, ): void { for (const [key, entry] of this.liveMessagePartCache) { if ( entry.directory === directoryArg && entry.update.sessionId.nativeId === sessionIdArg && entry.update.messageId.nativeId === messageIdArg ) { this.liveMessagePartCache.delete(key); this.liveMessagePartCacheBytes = Math.max(0, this.liveMessagePartCacheBytes - entry.bytes); } } } private projectLiveMessageUpdate( directoryArg: string, sessionIdArg: string, messageIdArg: string, updateArg: TOpenCodeLiveMessageUpdate, ): TOpenCodeLiveMessageUpdate | undefined { if (updateArg.type === 'reasoning') return updateArg; const textEntries = [...this.liveMessagePartCache.values()].filter((entryArg) => ( entryArg.directory === directoryArg && entryArg.type === 'text' && !entryArg.ignored && entryArg.update.sessionId.nativeId === sessionIdArg && entryArg.update.messageId.nativeId === messageIdArg )); if (textEntries.length === 0) return undefined; const text = textEntries.map((entryArg) => entryArg.update.text).join('\n\n'); if (Buffer.byteLength(text, 'utf8') > maxMessageTextBytes) return undefined; const update: TOpenCodeTextUpdate = { sessionId: openCodeRuntimeId(sessionIdArg), messageId: openCodeRuntimeId(messageIdArg), // OpenCode normalizes every text part into one source-message entry. partId: openCodeRuntimeId(messageIdArg), text, status: textEntries.some((entryArg) => entryArg.update.status === 'running') ? 'running' : 'completed', sourceUpdatedAt: Math.max(...textEntries.map((entryArg) => entryArg.update.sourceUpdatedAt)), }; if (Buffer.byteLength(JSON.stringify(update), 'utf8') > maxLiveMessageSnapshotBytes) { return undefined; } return { type: 'text', update }; } private normalizeLiveMessageStreamEvent( valueArg: unknown, ): TNormalizedOpenCodeMessageStreamEvent | undefined { if ( !isRecord(valueArg) || typeof valueArg.directory !== 'string' || valueArg.directory.length === 0 || Buffer.byteLength(valueArg.directory, 'utf8') > 4096 || !isRecord(valueArg.payload) || !isRecord(valueArg.payload.properties) ) return undefined; const directory = valueArg.directory; const payload = valueArg.payload as Record; const properties = payload.properties as Record; const sessionNativeId = typeof properties.sessionID === 'string' ? properties.sessionID : undefined; const gap = (): TNormalizedOpenCodeMessageStreamEvent => { this.clearLiveMessagePartCache(); return { directory, ...(sessionNativeId === undefined ? {} : { sessionNativeId }), messageStreamGap: true, }; }; if (payload.type === 'message.part.updated') { if (!this.acceptLiveMessageEventId(payload)) return undefined; if (!isRecord(properties.part)) { this.clearLiveMessagePartCache(); return { directory, ...(sessionNativeId === undefined ? {} : { sessionNativeId }), sharedStreamGap: true, }; } const part = properties.part; if (part.type !== 'text' && part.type !== 'reasoning') return undefined; try { const validatedSessionId = requireString( properties.sessionID, 'live message event session ID', maxIdentifierBytes, ); const partSessionId = requireString( part.sessionID, 'live message part session ID', maxIdentifierBytes, ); const messageId = requireString( part.messageID, 'live message part message ID', maxIdentifierBytes, ); const partId = requireString(part.id, 'live message part ID', maxIdentifierBytes); if (partSessionId !== validatedSessionId || typeof part.text !== 'string') return gap(); const sourceUpdatedAt = optionalFiniteNumber(properties.time) ?? Date.now(); const endedAt = isRecord(part.time) ? optionalFiniteNumber(part.time.end) : undefined; const common = { sessionId: openCodeRuntimeId(validatedSessionId), messageId: openCodeRuntimeId(messageId), partId: openCodeRuntimeId(partId), text: part.text, status: endedAt === undefined ? 'running' as const : 'completed' as const, sourceUpdatedAt, }; const update: TOpenCodeLiveMessageUpdate = part.type === 'text' ? { type: 'text', update: common } : { type: 'reasoning', update: common }; const ignored = part.type === 'text' && part.ignored === true; const bytes = Buffer.byteLength(JSON.stringify(update.update), 'utf8'); if (bytes > maxLiveMessageSnapshotBytes) return gap(); const key = this.liveMessagePartKey(directory, validatedSessionId, messageId, partId); const existing = this.liveMessagePartCache.get(key); if (!existing && !ignored && (part.text.length > 0 || endedAt !== undefined)) return gap(); const nextTotalBytes = this.liveMessagePartCacheBytes - (existing?.bytes ?? 0) + bytes; if ( (!existing && this.liveMessagePartCache.size >= maxLiveMessagePartCacheEntries) || nextTotalBytes > maxLiveMessagePartCacheBytes ) return gap(); this.liveMessagePartCache.set(key, { directory, type: update.type, update: update.update, ignored, bytes, }); this.liveMessagePartCacheBytes = nextTotalBytes; if (ignored) return undefined; const projected = this.projectLiveMessageUpdate( directory, validatedSessionId, messageId, update, ); return projected ? { directory, sessionNativeId: validatedSessionId, update: projected } : gap(); } catch { return gap(); } } if (payload.type === 'message.part.delta') { if (!this.acceptLiveMessageEventId(payload)) return undefined; try { const validatedSessionId = requireString( properties.sessionID, 'live message delta session ID', maxIdentifierBytes, ); const messageId = requireString( properties.messageID, 'live message delta message ID', maxIdentifierBytes, ); const partId = requireString( properties.partID, 'live message delta part ID', maxIdentifierBytes, ); if (properties.field !== 'text' || typeof properties.delta !== 'string') return gap(); const key = this.liveMessagePartKey(directory, validatedSessionId, messageId, partId); const existing = this.liveMessagePartCache.get(key); if (!existing) return gap(); const update = { ...existing.update, text: `${existing.update.text}${properties.delta}`, sourceUpdatedAt: Date.now(), }; const bytes = Buffer.byteLength(JSON.stringify(update), 'utf8'); const nextTotalBytes = this.liveMessagePartCacheBytes - existing.bytes + bytes; if (bytes > maxLiveMessageSnapshotBytes || nextTotalBytes > maxLiveMessagePartCacheBytes) { return gap(); } const normalizedUpdate: TOpenCodeLiveMessageUpdate = existing.type === 'text' ? { type: 'text', update: update as TOpenCodeTextUpdate } : { type: 'reasoning', update: update as TOpenCodeReasoningUpdate }; this.liveMessagePartCache.set(key, { directory, type: normalizedUpdate.type, update: normalizedUpdate.update, ignored: existing.ignored, bytes, }); this.liveMessagePartCacheBytes = nextTotalBytes; if (existing.ignored) return undefined; const projected = this.projectLiveMessageUpdate( directory, validatedSessionId, messageId, normalizedUpdate, ); return projected ? { directory, sessionNativeId: validatedSessionId, update: projected } : gap(); } catch { return gap(); } } if (payload.type === 'message.part.removed') { try { const validatedSessionId = requireString( properties.sessionID, 'removed message part session ID', maxIdentifierBytes, ); const messageId = requireString( properties.messageID, 'removed message part message ID', maxIdentifierBytes, ); const partId = requireString( properties.partID, 'removed message part ID', maxIdentifierBytes, ); const key = this.liveMessagePartKey(directory, validatedSessionId, messageId, partId); const existing = this.liveMessagePartCache.get(key); if (existing) { this.liveMessagePartCache.delete(key); this.liveMessagePartCacheBytes = Math.max(0, this.liveMessagePartCacheBytes - existing.bytes); } this.clearLiveMessagePartCache(); return { directory, sessionNativeId: validatedSessionId, sharedStreamGap: true }; } catch { this.clearLiveMessagePartCache(); return { directory, ...(sessionNativeId === undefined ? {} : { sessionNativeId }), sharedStreamGap: true, }; } } if (payload.type === 'message.removed') { try { const validatedSessionId = requireString( properties.sessionID, 'removed message session ID', maxIdentifierBytes, ); const messageId = requireString( properties.messageID, 'removed message ID', maxIdentifierBytes, ); for (const [key, entry] of this.liveMessagePartCache) { if ( entry.directory === directory && controllerRuntimeIdsEqual(entry.update.sessionId, openCodeRuntimeId(validatedSessionId)) && controllerRuntimeIdsEqual(entry.update.messageId, openCodeRuntimeId(messageId)) ) { this.liveMessagePartCache.delete(key); this.liveMessagePartCacheBytes = Math.max(0, this.liveMessagePartCacheBytes - entry.bytes); } } this.clearLiveMessagePartCache(); return { directory, sessionNativeId: validatedSessionId, sharedStreamGap: true }; } catch { this.clearLiveMessagePartCache(); return { directory, ...(sessionNativeId === undefined ? {} : { sessionNativeId }), sharedStreamGap: true, }; } } return undefined; } private async consumeEventStream( options: IOpenCodeEventStreamOptions, signal: AbortSignal ): Promise { let resubscribeDelayMs = eventStreamResubscribeDelayMs; while (!signal.aborted) { let lastStreamError: Error | undefined; try { const subscription = await this.client.global.event({ signal, // One SDK attempt avoids its non-abortable retry sleep. The adapter's // outer loop owns reconnect backoff so stopEventStream stays prompt. sseMaxRetryAttempts: 1, sseDefaultRetryDelay: eventStreamResubscribeDelayMs, sseMaxRetryDelay: eventStreamMaxResubscribeDelayMs, onSseError: (error) => { if (signal.aborted) return; lastStreamError = asError(error, 'The OpenCode event stream failed.'); try { options.onError?.(lastStreamError); } catch { // Diagnostics must not terminate stream recovery. } }, }); for await (const event of subscription.stream) { if (signal.aborted) return; if ( isRecord(event) && isRecord(event.payload) && event.payload.type === 'server.connected' ) { lastStreamError = undefined; resubscribeDelayMs = eventStreamResubscribeDelayMs; this.clearLiveMessagePartCache(); await options.onConnected?.(); continue; } const messageStreamSessionId = messageStreamEventSessionId(event); if ( messageStreamSessionId !== undefined && this.temporarySessionIds.has(messageStreamSessionId) ) continue; const normalizedMessageStream = this.normalizeLiveMessageStreamEvent(event); if (normalizedMessageStream) { if (normalizedMessageStream.sessionNativeId !== undefined) { this.sessionMetricsCache.delete(this.sessionMetricsCacheKey( normalizedMessageStream.directory, normalizedMessageStream.sessionNativeId, )); } if ('sharedStreamGap' in normalizedMessageStream) { if (options.onStreamGap) { await options.onStreamGap(); } else { await options.onToolStreamGap?.(); await options.onMessageStreamGap?.(); } continue; } if ('messageStreamGap' in normalizedMessageStream) { if (options.onMessageStreamGap) { await options.onMessageStreamGap(); continue; } } else if (normalizedMessageStream.update.type === 'text') { if (options.onTextUpdate) { await options.onTextUpdate( normalizedMessageStream.update.update, normalizedMessageStream.directory, ); continue; } } else { if (options.onReasoningUpdate) { await options.onReasoningUpdate( normalizedMessageStream.update.update, normalizedMessageStream.directory, ); continue; } } } const normalized = normalizeEvent(event); if (!normalized) continue; if ('toolStreamGap' in normalized) { await options.onToolStreamGap?.(); continue; } if ('toolExecution' in normalized) { this.sessionMetricsCache.delete(this.sessionMetricsCacheKey( normalized.directory, normalized.toolExecution.sessionId.nativeId, )); if (!this.temporarySessionIds.has(normalized.toolExecution.sessionId.nativeId)) { await options.onToolExecution?.( normalized.toolExecution, normalized.directory, normalized.timestamp, ); } continue; } if ('messageLifecycle' in normalized) { await options.onMessageLifecycle?.( normalized.messageLifecycle, normalized.directory, ); if (normalized.messageLifecycle.terminal) { this.clearLiveMessagePartsForMessage( normalized.directory, normalized.messageLifecycle.sessionId.nativeId, normalized.messageLifecycle.messageId.nativeId, ); } } if (normalized.event.sessionId?.harnessId === 'opencode') { this.sessionMetricsCache.delete(this.sessionMetricsCacheKey( normalized.directory, normalized.event.sessionId.nativeId, )); } if ( normalized.event.sessionId === undefined || !this.temporarySessionIds.has(normalized.event.sessionId.nativeId) ) { await options.onEvent(normalized.event, normalized.directory); } } } catch (error) { if (signal.aborted) return; lastStreamError = asError(error, 'The OpenCode event stream failed.'); try { options.onError?.(lastStreamError); } catch { // Diagnostics must not terminate stream recovery. } } if (signal.aborted) return; if (!lastStreamError) { lastStreamError = new Error('The OpenCode event stream ended unexpectedly.'); try { options.onError?.(lastStreamError); } catch { // Diagnostics must not terminate stream recovery. } } this.clearLiveMessagePartCache(); if (options.onStreamGap) { await options.onStreamGap(); } else { await options.onToolStreamGap?.(); await options.onMessageStreamGap?.(); } await waitForAbortableDelay(resubscribeDelayMs, signal); resubscribeDelayMs = Math.min( resubscribeDelayMs * 2, eventStreamMaxResubscribeDelayMs, ); } } }