Files
goodbuddy/src/agent-daemon/runtime-acp-backend.ts
T
mesalogo bbf24c282a fix: resolve runtime, review, channel and chat state failures
Keep remote startup cancellable without blocking other sessions and surface local OpenCode submission failures promptly. Preserve supervisor project isolation, pause behavior and valid story destinations.

Restore generated-image channel delivery, Telegram reconnection on unchanged Save and DingTalk business-error retries. Preserve virtualized message disclosures using sparse state and release stale supervisor UI requests.

Add production-path regressions, Electron checks, isolated SQLite validation and audit evidence. Fix the SDK cleanup test fixture lifetime under garbage collection without weakening its assertions.

Validation: 6136 tests passed, 87 skipped; typecheck, lint and diff checks passed. Seven real text-model calls, current-source Linux x64 Host scenarios and large-history Electron comparisons completed.

Release note: Fix stuck runtime requests, inconsistent review results and channel delivery failures, and retain expanded details while browsing long conversations. Remote runtime fixes require an Agent build containing these changes.
2026-10-05 10:25:43 +08:00

3054 lines
99 KiB
TypeScript

import { z } from 'zod'
import { AgentImageToolMcp } from './image-tool-mcp'
import { remoteImageToolMcpName } from '../shared/remote-image-tool-node'
import { remoteQuestionResponseSchema } from '../shared/remote-question-contracts'
import { createHash } from 'node:crypto'
import {
AGENT_PROTOCOL_LIMITS,
AGENT_PROTOCOL_VERSION,
acpCompletePromptRequestSchema as promptCompletionSchema,
acpCompletePromptResultSchema,
acpEscalateCancellationRequestSchema as cancellationEscalationSchema,
acpGetCursorsRequestSchema as getCursorsRequestSchema,
acpReplayChannelRequestSchema,
acpReplayChannelResultSchema,
acpResumeChannelRequestSchema,
acpResumeChannelResultSchema,
acpCloseChannelRequestSchema,
acpCloseChannelResultSchema,
acpOpenChannelRequestSchema,
acpOpenChannelResultSchema,
acpReconcilePromptRequestSchema as promptReconciliationSchema,
agentIdentifierSchema,
positiveAgentSequenceSchema,
sha256DigestSchema,
type AgentFrame
} from '../shared/agent-protocol'
import { canonicalJson } from '../shared/agent-protocol/canonical'
import type {
AgentDiagnosticLog,
AgentDiagnosticRecord
} from './diagnostic-log'
import {
acpJournalCursorSchema,
remoteOwnedPromptAttachRequestSchema,
remoteOwnedPromptStartRequestSchema,
remoteSemanticTranscriptAckRequestSchema,
remoteSemanticTranscriptPageRequestSchema,
remotePromptOperationAcceptanceSchema,
remotePromptOperationPreparationSchema,
UNBOUNDED_REMOTE_PROMPT_DEADLINE,
type AcpJournalCursor,
type RemotePromptOperationAcceptance,
type RemotePromptOperationPreparation
} from '../shared/remote-agent-contracts'
import type { RemoteRuntimeBundleManifest } from '../shared/remote-runtime-launch-contracts'
import {
modelBridgePolicySchema,
type AgentPromptModelProfile,
type ModelBridgePolicy
} from '../shared/model-bridge-contracts'
import {
ModelBridgeBlobClient,
ModelBridgeClientError
} from './model-bridge-client'
import {
ModelBridgeBrokerServer,
type ModelBridgeBrokerDispatch
} from './model-bridge-broker'
import type {
ProtocolMethodContext,
ProtocolMethodHandler
} from './protocol-server'
import { AgentOwnedAcpPrompt } from './agent-owned-acp-prompt'
import { AgentAcpConnection } from './agent-acp-connection'
import type { SemanticPromptStore } from './semantic-prompt-store'
import {
AgentModelGatewayError,
type AgentModelGateway
} from './agent-model-gateway'
import { openCodeModelBridgeModelId } from './model-bridge-helper'
import { EventJournalCapacityError } from './event-journal'
const ZERO_CURSOR = '0'
const DEFAULT_MAXIMUM_BINDINGS = 128
const DEFAULT_MAXIMUM_CONTROLLER_BINDINGS = 32
const DEFAULT_MAXIMUM_LIFETIME_BINDINGS = 4_096
const DEFAULT_MAXIMUM_PENDING_CONTROL = 256
const DEFAULT_MAXIMUM_PENDING_INPUT_PER_BINDING = 128
const DEFAULT_MAXIMUM_PENDING_INPUT_GLOBAL = 1_024
const DEFAULT_MAXIMUM_PENDING_OUTPUT_PER_BINDING = 128
const DEFAULT_MAXIMUM_PENDING_OUTPUT_GLOBAL = 1_024
const DEFAULT_MAXIMUM_PENDING_OUTPUT_BYTES_PER_BINDING =
AGENT_PROTOCOL_LIMITS.runEventJournalBytes
const DEFAULT_MAXIMUM_PENDING_OUTPUT_BYTES_GLOBAL =
AGENT_PROTOCOL_LIMITS.maximumBufferedProtocolBytes
const DEFAULT_MAXIMUM_OPERATIONS_PER_BINDING = 1_000
const DEFAULT_DISPOSE_TIMEOUT_MS = 10_000
const OWNED_PROMPT_CANCEL_GRACE_MS = 1_000
const SHARED_PROCESS_IDLE_MS = 0
const OWNED_PROMPT_START_TIMEOUT_MS = 2 * 60_000
const ACP_JOURNAL_RETRY_INITIAL_MILLISECONDS = 25
const ACP_JOURNAL_RETRY_MAXIMUM_MILLISECONDS = 1_000
export type RuntimeAcpBackendErrorCode =
| 'capacity'
| 'closed'
| 'conflict'
| 'deadline'
| 'identity'
| 'not-found'
| 'output-quota'
| 'process'
| 'stale-controller'
| 'untrusted-runtime'
| 'workspace'
export class RuntimeAcpBackendError extends Error {
constructor(
message: string,
readonly code: RuntimeAcpBackendErrorCode
) {
super(message)
this.name = 'RuntimeAcpBackendError'
}
}
export type RuntimeAcpJournal = {
appendAcpFrame(input: {
controllerId: string
bindingId: string
channelEpoch: string
direction: 'main-to-runtime' | 'runtime-to-main'
sequence: string
payload: Uint8Array
}): { created: boolean }
getAcpCursor(
bindingId: string,
channelEpoch: string,
direction: 'main-to-runtime' | 'runtime-to-main'
): AcpJournalCursor | undefined
acknowledgeAcpFromMain?(input: {
bindingId: string
channelEpoch: string
sequence: string
}): void
retireAcpBinding?(input: {
controllerId: string
bindingId: string
channelEpoch: string
}): void
replayAcpFrames?(input: {
bindingId: string
channelEpoch: string
direction: 'runtime-to-main'
afterSequence: string
limit?: number
}): Array<{ sequence: string; payload: Uint8Array; createdAt: number }>
}
export type RuntimeAcpResolvedBundle = {
entry: {
runtimeId: string
bundleDigest: string
acpCapabilitiesDigest: string
manifestDigest?: string
}
bundleDirectory: string
}
export type RuntimeAcpVerifiedBundle = {
bundleDirectory: string
executablePath: string
manifest: RemoteRuntimeBundleManifest
manifestDigest: string
}
export type RuntimeAcpWorkspace = {
workspaceIdentity: string
workspaceDirectory: string
scratchDirectory: string
bridgeDirectory?: string
}
export type RuntimeAcpProcessIdentity = {
launchId: string
processId: string
supervisorIdentityDigest: string
}
export type RuntimeAcpProcessOutput = {
stream: 'stdout' | 'stderr'
data: Uint8Array
}
export type RuntimeAcpProcessReconciliation = {
identity: RuntimeAcpProcessIdentity
state:
| 'starting'
| 'running'
| 'completed'
| 'failed'
| 'cancelled'
| 'interrupted'
| 'outcome-unknown'
processTree: 'running' | 'empty' | 'unknown'
}
export type RuntimeAcpProcessStopReason =
| 'binding-closed'
| 'controller-disconnected'
| 'deadline-exceeded'
| 'identity-conflict'
| 'output-quota'
| 'user-cancelled'
export type RuntimeAcpProcessOwner = {
identity: RuntimeAcpProcessIdentity
beginPrompt(input: {
deadlineAt: string
maximumInputBytes: number
}): void | Promise<void>
completePrompt(): void | Promise<void>
writeStdin(payload: Uint8Array): void | Promise<void>
stop(input: {
reason: RuntimeAcpProcessStopReason
deadlineAt: string
}): void | Promise<void>
reconcile(): RuntimeAcpProcessReconciliation | Promise<RuntimeAcpProcessReconciliation>
subscribeOutput(
listener: (output: RuntimeAcpProcessOutput) => void | Promise<void>
): void | (() => void)
subscribeExit?(
listener: () => void | Promise<void>
): void | (() => void)
}
export type RuntimeAcpProcessLaunch = {
manifest: RemoteRuntimeBundleManifest
bundle: RuntimeAcpVerifiedBundle
workspace: RuntimeAcpWorkspace
scratch: string
deadlineAt: string
budget: RemotePromptOperationPreparation['budget']
sharedSessions?: boolean
modelBridge?: {
socketPath: string
policy: ModelBridgePolicy
}
}
export type RuntimeAcpBackendLimits = {
maximumBindings: number
maximumControllerBindings: number
maximumLifetimeBindings: number
maximumPendingControl: number
maximumPendingInputPerBinding: number
maximumPendingInputGlobal: number
maximumPendingOutputPerBinding: number
maximumPendingOutputGlobal: number
maximumPendingOutputBytesPerBinding: number
maximumPendingOutputBytesGlobal: number
maximumOperationsPerBinding: number
}
export type RuntimeAcpBackendOptions = {
shareOwnedProcesses?: boolean
sharedProcessIdleMs?: number
journal: RuntimeAcpJournal
resolveRuntimeBundle(
runtimeId: string,
runtimeBundleDigest: string
): RuntimeAcpResolvedBundle | Promise<RuntimeAcpResolvedBundle>
loadRegisteredRuntimeBundle(
resolved: RuntimeAcpResolvedBundle,
preparation: RemotePromptOperationPreparation
): RuntimeAcpVerifiedBundle | Promise<RuntimeAcpVerifiedBundle>
resolveWorkspace(
preparation: RemotePromptOperationPreparation,
context: ProtocolMethodContext
): RuntimeAcpWorkspace | Promise<RuntimeAcpWorkspace>
launchProcess(
launch: RuntimeAcpProcessLaunch
): RuntimeAcpProcessOwner | Promise<RuntimeAcpProcessOwner>
outputSink(
frame: AgentFrame,
context: {
bindingId: string
controllerId: string
controllerGeneration: number
}
): void | Promise<void>
blobSink?: (
frame: AgentFrame,
context: {
bindingId: string
controllerId: string
controllerGeneration: number
}
) => void | Promise<void>
createModelBridgeBroker?: (options: {
bridgeDirectory: string
dispatch: ModelBridgeBrokerDispatch
}) => ModelBridgeBrokerServer
semanticPrompts?: SemanticPromptStore
modelGateway?: AgentModelGateway
diagnostics?: Pick<AgentDiagnosticLog, 'tryRecord'>
now?: () => number
limits?: Partial<RuntimeAcpBackendLimits>
}
type PreparedOperation = {
preparationDigest: string
acceptance: RemotePromptOperationAcceptance
budget: RemotePromptOperationPreparation['budget']
completion?: z.infer<typeof acpCompletePromptResultSchema>
terminalState?: 'completed' | 'failed' | 'cancelled' | 'interrupted' | 'outcome-unknown'
processTree?: 'running' | 'empty'
modelProfile?: AgentPromptModelProfile
promptSequence: number
ownedPromptStarted?: boolean
terminalAcknowledged?: boolean
}
type BindingState = {
request: z.infer<typeof acpOpenChannelRequestSchema>
openResult: z.infer<typeof acpOpenChannelResultSchema>
controllerId: string
controllerGeneration: number
connectionId: string
transportState: 'live' | 'detached' | 'replaying'
resolvedBundle: RuntimeAcpResolvedBundle
state: 'open' | 'running' | 'stopping' | 'closed' | 'outcome-unknown'
process?: RuntimeAcpProcessOwner
sharedProcess?: SharedProcess
unsubscribeOutput?: () => void
unsubscribeExit?: () => void
stopRequested: boolean
operations: Map<string, PreparedOperation>
activeOperationId?: string
terminalState?: 'completed' | 'failed' | 'cancelled' | 'interrupted'
inputBytes: number
nextOutputSequence: bigint
pendingInput: number
pendingOutput: number
pendingOutputBytes: number
inputTail: Promise<void>
outputTail: Promise<void>
deadlineTimer?: NodeJS.Timeout
ownedPromptStartTimer?: NodeJS.Timeout
modelBridgePolicy?: ModelBridgePolicy
modelBridgeClient?: ModelBridgeBlobClient
imageTool?: AgentImageToolMcp
modelBridgeBroker?: ModelBridgeBrokerServer
ownedAcp?: AgentOwnedAcpPrompt
workspaceDirectory?: string
nextModelRound: number
poisoned: boolean
}
type SharedProcess = {
key: string
process: RuntimeAcpProcessOwner
transport: AgentAcpConnection
bindings: Set<BindingState>
unsubscribeExit?: () => void
idleTimer?: NodeJS.Timeout
exited: boolean
}
export class RuntimeAcpBackend {
readonly methods: Readonly<Record<string, ProtocolMethodHandler>>
readonly #options: RuntimeAcpBackendOptions
readonly #limits: RuntimeAcpBackendLimits
readonly #bindings = new Map<string, BindingState>()
readonly #processIdentityBindings = new Map<string, string>()
readonly #sharedProcesses = new Map<string, SharedProcess>()
readonly #boundConnectionSignals = new WeakSet<AbortSignal>()
#nextChannelEpoch: bigint
#lifetimeBindings = 0
#pendingControl = 0
#pendingInputGlobal = 0
#pendingOutputGlobal = 0
#pendingOutputBytesGlobal = 0
#controlTail: Promise<void> = Promise.resolve()
#disposed = false
#draining = false
#disposePromise?: Promise<void>
constructor(options: RuntimeAcpBackendOptions) {
this.#options = options
this.#limits = {
maximumBindings: boundedLimit(
options.limits?.maximumBindings,
DEFAULT_MAXIMUM_BINDINGS
),
maximumControllerBindings: boundedLimit(
options.limits?.maximumControllerBindings,
DEFAULT_MAXIMUM_CONTROLLER_BINDINGS
),
maximumLifetimeBindings: boundedLimit(
options.limits?.maximumLifetimeBindings,
DEFAULT_MAXIMUM_LIFETIME_BINDINGS
),
maximumPendingControl: boundedLimit(
options.limits?.maximumPendingControl,
DEFAULT_MAXIMUM_PENDING_CONTROL
),
maximumPendingInputPerBinding: boundedLimit(
options.limits?.maximumPendingInputPerBinding,
DEFAULT_MAXIMUM_PENDING_INPUT_PER_BINDING
),
maximumPendingInputGlobal: boundedLimit(
options.limits?.maximumPendingInputGlobal,
DEFAULT_MAXIMUM_PENDING_INPUT_GLOBAL
),
maximumPendingOutputPerBinding: boundedLimit(
options.limits?.maximumPendingOutputPerBinding,
DEFAULT_MAXIMUM_PENDING_OUTPUT_PER_BINDING
),
maximumPendingOutputGlobal: boundedLimit(
options.limits?.maximumPendingOutputGlobal,
DEFAULT_MAXIMUM_PENDING_OUTPUT_GLOBAL
),
maximumPendingOutputBytesPerBinding: boundedLimit(
options.limits?.maximumPendingOutputBytesPerBinding,
DEFAULT_MAXIMUM_PENDING_OUTPUT_BYTES_PER_BINDING
),
maximumPendingOutputBytesGlobal: boundedLimit(
options.limits?.maximumPendingOutputBytesGlobal,
DEFAULT_MAXIMUM_PENDING_OUTPUT_BYTES_GLOBAL
),
maximumOperationsPerBinding: boundedLimit(
options.limits?.maximumOperationsPerBinding,
DEFAULT_MAXIMUM_OPERATIONS_PER_BINDING
)
}
const initialEpoch = Math.max(1, Math.trunc(options.now?.() ?? Date.now()))
this.#nextChannelEpoch = BigInt(
Math.min(initialEpoch, Number.MAX_SAFE_INTEGER - 1)
)
this.methods = {
'runtime/openAcpChannel': (params, context) =>
this.#enqueuePublicControl(() => this.#open(params, context)),
'runtime/closeAcpChannel': (params, context) =>
this.#enqueuePublicControl(() => this.#close(params, context)),
'runtime/resumeAcpChannel': (params, context) =>
this.#enqueuePublicControl(() => this.#resume(params, context)),
'runtime/replayAcpChannel': (params, context) =>
this.#enqueuePublicControl(() => this.#replay(params, context)),
'runtime/preparePrompt': (params, context) =>
this.#enqueuePublicControl(() => this.#prepare(params, context)),
'runtime/startPrompt': (params, context) =>
this.#enqueuePublicControl(() => this.#startOwnedPrompt(params, context))
.then(({ started }) => started),
'runtime/attachPrompt': (params, context) =>
this.#enqueuePublicControl(() => this.#attachOwnedPrompt(params, context)),
'runtime/respondToQuestion': (params, context) =>
this.#enqueuePublicControl(async () => {
const request = remoteQuestionResponseSchema.parse(params)
this.#assertControllerLive(context)
const binding = this.#bindings.get(request.bindingId)
if (!binding?.ownedAcp || binding.activeOperationId !== request.operationId ||
binding.controllerId !== context.controller.controllerId ||
binding.controllerGeneration !== context.controller.generation ||
binding.connectionId !== context.controller.connectionId) {
throw new RuntimeAcpBackendError('Question reply identity does not match', 'identity')
}
await binding.ownedAcp.respondToQuestion(request)
return {}
}),
'runtime/pagePromptTranscript': (params, context) =>
this.#enqueuePublicControl(() => this.#pageOwnedPrompt(params, context)),
'runtime/ackPromptTranscript': (params, context) =>
this.#enqueuePublicControl(() => this.#ackOwnedPrompt(params, context)),
'runtime/completePrompt': (params, context) =>
this.#enqueuePublicControl(() => this.#complete(params, context)),
'runtime/getAcpCursors': (params, context) =>
this.#enqueuePublicControl(() => this.#getCursors(params, context)),
'runtime/escalateCancellation': (params, context) =>
this.#enqueuePublicControl(() => this.#escalate(params, context)),
'runtime/reconcilePrompt': (params, context) =>
this.#enqueuePublicControl(() => this.#reconcile(params, context))
}
}
readonly onAcpFrame = async (
frame: AgentFrame,
context: ProtocolMethodContext
): Promise<void> => {
this.#assertNotDisposed()
if (
frame.header.kind !== 'acp' ||
frame.header.direction !== 'main-to-agent'
) {
throw new RuntimeAcpBackendError(
'Invalid ACP frame direction or kind',
'identity'
)
}
const binding = this.#bindingForContext(frame.header.channelId, context)
if (
context.channelId !== binding.request.bindingId ||
frame.header.channelEpoch !== binding.openResult.channelEpoch ||
frame.header.connectionId !== binding.connectionId ||
frame.header.generation !== binding.controllerGeneration
) {
throw new RuntimeAcpBackendError(
'ACP frame identity does not match its binding',
'identity'
)
}
if (
binding.process === undefined ||
binding.state !== 'running' ||
binding.transportState !== 'live'
) {
throw new RuntimeAcpBackendError(
'ACP Runtime process is not running',
'closed'
)
}
this.#assertBeforeDeadline(binding)
const cursor = this.#cursor(binding, 'main-to-runtime')
if (
cursor !== undefined &&
BigInt(cursor.deliveredSequence) >= BigInt(frame.header.sequence)
) {
return
}
if (
cursor === undefined ||
BigInt(cursor.journaledSequence) < BigInt(frame.header.sequence)
) {
throw new RuntimeAcpBackendError(
'ACP input was not durably journaled before delivery',
'identity'
)
}
if (
binding.inputBytes + frame.payload.byteLength >
this.#activeBudget(binding).maximumInputBytes
) {
throw new RuntimeAcpBackendError(
'ACP input quota reached',
'capacity'
)
}
await this.#enqueueInput(binding, async () => {
try {
await binding.process!.writeStdin(frame.payload)
} catch {
throw new RuntimeAcpBackendError(
'Runtime input delivery failed',
'process'
)
}
binding.inputBytes += frame.payload.byteLength
})
}
readonly onBlobFrame = async (
frame: AgentFrame,
context: ProtocolMethodContext
): Promise<void> => {
this.#assertNotDisposed()
if (
frame.header.kind !== 'blob' ||
frame.header.direction !== 'main-to-agent'
) {
throw new RuntimeAcpBackendError(
'Invalid model bridge frame direction or kind',
'identity'
)
}
const imageBinding = [...this.#bindings.values()].find(binding =>
binding.controllerId === context.controller.controllerId &&
binding.controllerGeneration === context.controller.generation &&
binding.connectionId === context.controller.connectionId &&
binding.activeOperationId !== undefined &&
binding.imageTool?.descriptor.channelId === frame.header.channelId &&
binding.imageTool.descriptor.channelEpoch === frame.header.channelEpoch &&
context.channelId === frame.header.channelId)
if (imageBinding) {
imageBinding.imageTool!.onReply(frame.payload)
return
}
const matches = [...this.#bindings.values()].filter(
(binding) =>
binding.controllerId === context.controller.controllerId &&
binding.controllerGeneration === context.controller.generation &&
binding.connectionId === context.controller.connectionId &&
binding.modelBridgeClient?.binding.channelId ===
frame.header.channelId
)
if (matches.length !== 1) {
throw new RuntimeAcpBackendError(
'Blob channel has no exact active model bridge authority',
'identity'
)
}
const binding = matches[0]!
if (
binding.poisoned ||
binding.activeOperationId === undefined ||
context.channelId !== frame.header.channelId
) {
throw new RuntimeAcpBackendError(
'Blob frame is not authorized for an active prompt',
'identity'
)
}
try {
await binding.modelBridgeClient!.onBlobFrame(frame)
} catch (error) {
if (
error instanceof ModelBridgeClientError &&
(error.poisoned || error.outcomeUnknown)
) {
await this.#poisonBinding(binding)
}
throw error
}
}
readonly authorizeBlobFrame = (
frame: AgentFrame,
controller: {
controllerId: string
connectionId: string
generation: number
}
): boolean =>
frame.header.kind === 'blob' &&
frame.header.direction === 'agent-to-main' &&
[...this.#bindings.values()].some(
(binding) =>
!binding.poisoned &&
binding.activeOperationId !== undefined &&
binding.controllerId === controller.controllerId &&
binding.controllerGeneration === controller.generation &&
binding.connectionId === controller.connectionId &&
((binding.modelBridgeClient?.binding.channelId === frame.header.channelId &&
binding.modelBridgeClient.binding.channelEpoch === frame.header.channelEpoch) ||
(binding.imageTool?.descriptor.channelId === frame.header.channelId &&
binding.imageTool.descriptor.channelEpoch === frame.header.channelEpoch)) &&
frame.header.connectionId === binding.connectionId &&
frame.header.generation === binding.controllerGeneration
)
async drain(): Promise<boolean> {
this.#draining = true
return this.#enqueueControl(() =>
![...this.#bindings.values()].some(binding =>
binding.activeOperationId !== undefined ||
(binding.transportState !== 'detached' && [...binding.operations.values()].some(operation =>
operation.ownedPromptStarted && !operation.terminalAcknowledged
))
)
)
}
dispose(timeoutMs = DEFAULT_DISPOSE_TIMEOUT_MS): Promise<void> {
if (this.#disposePromise !== undefined) {
return this.#disposePromise
}
if (
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1 ||
timeoutMs > 60_000
) {
return Promise.reject(
new RangeError('Invalid Runtime backend disposal timeout')
)
}
this.#disposed = true
const cleanup = this.#controlTail
.catch(() => undefined)
.then(async () => {
const results = await Promise.allSettled(
[...this.#bindings.values()]
.filter((binding) => binding.state !== 'closed')
.map(async (binding) =>
await this.#stopAndReconcile(
binding,
'binding-closed'
)
)
)
const failures = results
.filter(
(result): result is PromiseRejectedResult =>
result.status === 'rejected'
)
.map((result) => result.reason)
const sharedResults = await Promise.allSettled(
[...this.#sharedProcesses.values()].map(group =>
this.#stopSharedProcess(group, 'binding-closed')
)
)
for (const result of sharedResults) {
if (result.status === 'rejected') failures.push(result.reason)
}
if (failures.length > 0) {
throw new AggregateError(
failures,
'Runtime process shutdown did not reconcile every process tree'
)
}
})
this.#controlTail = cleanup
this.#disposePromise = rejectAfter(
cleanup,
timeoutMs,
'Runtime backend shutdown timed out'
)
return this.#disposePromise
}
async #open(
params: unknown,
context: ProtocolMethodContext
): Promise<z.infer<typeof acpOpenChannelResultSchema>> {
const request = acpOpenChannelRequestSchema.parse(params)
this.#assertControllerLive(context)
const existing = this.#bindings.get(request.bindingId)
if (existing !== undefined) {
if (existing.poisoned) {
throw new RuntimeAcpBackendError(
'ACP binding was poisoned by uncertain model delivery',
'process'
)
}
if (canonicalJson(existing.request) !== canonicalJson(request)) {
throw new RuntimeAcpBackendError(
'ACP binding identity conflicts with an existing binding',
'conflict'
)
}
if (existing.controllerId !== context.controller.controllerId) {
throw new RuntimeAcpBackendError(
'ACP binding belongs to another controller',
'stale-controller'
)
}
if (existing.state !== 'closed' || existing.process !== undefined) {
const ownedByCurrentConnection =
existing.controllerGeneration ===
context.controller.generation &&
existing.connectionId ===
context.controller.connectionId
if (ownedByCurrentConnection) {
return acpOpenChannelResultSchema.parse(existing.openResult)
}
const canReplaceDetachedIdleBinding =
context.controllerTakeoverProven === true &&
context.controller.generation >
existing.controllerGeneration &&
existing.transportState === 'detached' &&
existing.activeOperationId === undefined &&
(existing.state === 'open' ||
existing.state === 'running')
const canReattachDetachedOwnedPrompt =
context.controllerTakeoverProven === true &&
context.controller.generation >
existing.controllerGeneration &&
existing.transportState === 'detached' &&
existing.activeOperationId !== undefined &&
this.#options.semanticPrompts !== undefined &&
existing.operations.get(existing.activeOperationId)
?.modelProfile !== undefined &&
existing.state === 'running'
if (canReattachDetachedOwnedPrompt) {
existing.controllerGeneration = context.controller.generation
existing.connectionId = context.controller.connectionId
existing.transportState = 'live'
this.#bindConnectionLifetime(existing, context.signal)
return acpOpenChannelResultSchema.parse(existing.openResult)
}
if (!canReplaceDetachedIdleBinding) {
this.#assertBindingOwner(existing, context)
}
await Promise.all([
existing.inputTail,
existing.outputTail
])
await this.#stopAndReconcile(existing, 'binding-closed')
}
this.#assertOpenCapacity(context.controller.controllerId)
const channelEpoch = this.#allocateChannelEpoch()
existing.openResult = acpOpenChannelResultSchema.parse({
bindingId: request.bindingId,
channelId: request.bindingId,
channelEpoch,
acpCapabilitiesDigest:
existing.resolvedBundle.entry.acpCapabilitiesDigest
})
existing.controllerGeneration = context.controller.generation
existing.connectionId = context.controller.connectionId
existing.transportState = 'live'
existing.state = 'open'
existing.stopRequested = false
existing.terminalState = undefined
existing.inputBytes = 0
existing.nextOutputSequence = 1n
existing.pendingInput = 0
existing.pendingOutput = 0
existing.pendingOutputBytes = 0
existing.inputTail = Promise.resolve()
existing.outputTail = Promise.resolve()
existing.nextModelRound = 0
this.#bindConnectionLifetime(existing, context.signal)
return existing.openResult
}
this.#assertOpenCapacity(context.controller.controllerId)
let resolved: RuntimeAcpResolvedBundle
try {
resolved = await this.#options.resolveRuntimeBundle(
request.runtimeId,
request.runtimeBundleDigest
)
} catch {
throw new RuntimeAcpBackendError(
'Runtime bundle is not trusted',
'untrusted-runtime'
)
}
if (
resolved.entry.runtimeId !== request.runtimeId ||
resolved.entry.bundleDigest !== request.runtimeBundleDigest
) {
throw new RuntimeAcpBackendError(
'Resolved Runtime bundle identity does not match',
'untrusted-runtime'
)
}
const channelEpoch = this.#allocateChannelEpoch()
const openResult = acpOpenChannelResultSchema.parse({
bindingId: request.bindingId,
channelId: request.bindingId,
channelEpoch,
acpCapabilitiesDigest: resolved.entry.acpCapabilitiesDigest
})
const binding: BindingState = {
request,
openResult,
controllerId: context.controller.controllerId,
controllerGeneration: context.controller.generation,
connectionId: context.controller.connectionId,
transportState: 'live',
resolvedBundle: resolved,
state: 'open',
stopRequested: false,
operations: new Map(),
inputBytes: 0,
nextOutputSequence: 1n,
pendingInput: 0,
pendingOutput: 0,
pendingOutputBytes: 0,
inputTail: Promise.resolve(),
outputTail: Promise.resolve(),
nextModelRound: 0,
poisoned: false
}
this.#bindings.set(request.bindingId, binding)
this.#lifetimeBindings += 1
this.#bindConnectionLifetime(binding, context.signal)
return openResult
}
async #close(
params: unknown,
context: ProtocolMethodContext
): Promise<z.infer<typeof acpCloseChannelResultSchema>> {
const request = acpCloseChannelRequestSchema.parse(params)
const binding = this.#bindingForContext(request.bindingId, context)
if (
request.channelId !== binding.openResult.channelId ||
request.channelEpoch !== binding.openResult.channelEpoch
) {
throw new RuntimeAcpBackendError(
'ACP close identity does not match its binding',
'identity'
)
}
if (binding.state !== 'closed') {
await this.#stopAndReconcile(
binding,
request.reason === 'cancelled'
? 'user-cancelled'
: 'binding-closed'
)
}
return acpCloseChannelResultSchema.parse({
bindingId: request.bindingId,
channelEpoch: request.channelEpoch,
closed: true
})
}
async #resume(
params: unknown,
context: ProtocolMethodContext
): Promise<z.infer<typeof acpResumeChannelResultSchema>> {
const request = acpResumeChannelRequestSchema.parse(params)
this.#assertControllerLive(context)
const binding = this.#bindings.get(request.bindingId)
if (binding === undefined) {
throw new RuntimeAcpBackendError(
'ACP binding does not exist',
'not-found'
)
}
if (
binding.controllerId !== context.controller.controllerId ||
context.controller.generation <= binding.controllerGeneration ||
request.channelId !== binding.openResult.channelId ||
request.channelEpoch !== binding.openResult.channelEpoch
) {
throw new RuntimeAcpBackendError(
'ACP resume identity does not exactly match its binding',
'identity'
)
}
if (
binding.transportState !== 'detached' ||
binding.process === undefined ||
binding.state !== 'running' ||
binding.poisoned
) {
throw new RuntimeAcpBackendError(
'ACP binding is not detached and resumable',
'conflict'
)
}
this.#assertBeforeDeadline(binding)
await binding.outputTail.catch(() => undefined)
let reconciliation: RuntimeAcpProcessReconciliation
try {
reconciliation = await binding.process.reconcile()
} catch {
throw new RuntimeAcpBackendError(
'Runtime process reconciliation failed',
'process'
)
}
if (
!sameProcessIdentity(binding.process.identity, reconciliation.identity) ||
reconciliation.processTree !== 'running' ||
reconciliation.state !== 'running'
) {
throw new RuntimeAcpBackendError(
'Detached Runtime process identity or state cannot be proven',
'identity'
)
}
binding.controllerGeneration = context.controller.generation
binding.connectionId = context.controller.connectionId
binding.transportState = 'replaying'
this.#bindConnectionLifetime(binding, context.signal)
const acceptance = this.#activeAcceptance(binding)!
return acpResumeChannelResultSchema.parse({
bindingId: binding.request.bindingId,
channelId: binding.openResult.channelId,
channelEpoch: binding.openResult.channelEpoch,
deadlineAt: acceptance.deadlineAt,
cursors: this.#bindingCursors(binding)
})
}
async #replay(
params: unknown,
context: ProtocolMethodContext
): Promise<z.infer<typeof acpReplayChannelResultSchema>> {
const request = acpReplayChannelRequestSchema.parse(params)
const binding = this.#bindingForContext(request.bindingId, context)
if (
request.channelId !== binding.openResult.channelId ||
request.channelEpoch !== binding.openResult.channelEpoch ||
binding.transportState !== 'replaying'
) {
throw new RuntimeAcpBackendError(
'ACP replay identity or phase does not match its binding',
'identity'
)
}
this.#assertBeforeDeadline(binding)
const initialCursor = this.#cursor(binding, 'runtime-to-main')
if (
BigInt(request.acknowledgedSequence) >
BigInt(initialCursor?.journaledSequence ?? ZERO_CURSOR)
) {
throw new RuntimeAcpBackendError(
'ACP replay ACK exceeds durable output',
'identity'
)
}
// Agent-owned prompts may have semantic events but no binary journal.
// In that case the validated zero cursor has nothing to acknowledge.
if (initialCursor !== undefined) {
this.#options.journal.acknowledgeAcpFromMain?.({
bindingId: binding.request.bindingId,
channelEpoch: binding.openResult.channelEpoch,
sequence: request.acknowledgedSequence
})
}
if (this.#options.journal.replayAcpFrames === undefined) {
throw new RuntimeAcpBackendError(
'Durable ACP replay is unavailable',
'process'
)
}
let replayedThrough = request.acknowledgedSequence
for (;;) {
const frames = this.#options.journal.replayAcpFrames({
bindingId: binding.request.bindingId,
channelEpoch: binding.openResult.channelEpoch,
direction: 'runtime-to-main',
afterSequence: replayedThrough,
limit: AGENT_PROTOCOL_LIMITS.runPendingEvents
})
for (const replay of frames) {
await this.#deliverOutput(binding, replay.sequence, replay.payload)
replayedThrough = replay.sequence
}
const cursor = this.#cursor(binding, 'runtime-to-main')
if (
frames.length === 0 ||
BigInt(replayedThrough) >=
BigInt(cursor?.journaledSequence ?? ZERO_CURSOR)
) {
break
}
}
binding.transportState = 'live'
return acpReplayChannelResultSchema.parse({
bindingId: binding.request.bindingId,
channelId: binding.openResult.channelId,
channelEpoch: binding.openResult.channelEpoch,
replayedThroughSequence: replayedThrough,
live: true
})
}
async #prepare(
params: unknown,
context: ProtocolMethodContext
): Promise<RemotePromptOperationAcceptance> {
const preparation = remotePromptOperationPreparationSchema.parse(params)
const binding = this.#bindingForContext(preparation.bindingId, context)
this.#assertPreparationIdentity(binding, preparation, context)
this.#assertBeforeDeadline(binding, preparation.deadlineAt)
const preparationDigest = digestSecretBearingPreparation(preparation)
const existing = binding.operations.get(preparation.operationId)
if (existing !== undefined) {
if (existing.preparationDigest !== preparationDigest) {
throw new RuntimeAcpBackendError(
'Prompt operation identity is already bound to different content',
'conflict'
)
}
return remotePromptOperationAcceptanceSchema.parse(
existing.acceptance
)
}
if (this.#draining) {
throw new RuntimeAcpBackendError('Agent installation is draining after update', 'closed')
}
if (binding.activeOperationId !== undefined) {
throw new RuntimeAcpBackendError(
'Another prompt operation is still active',
'conflict'
)
}
if (
binding.operations.size >=
this.#limits.maximumOperationsPerBinding
) {
throw new RuntimeAcpBackendError(
'ACP binding operation lifetime is exhausted',
'capacity'
)
}
let verified: RuntimeAcpVerifiedBundle
try {
verified = await this.#options.loadRegisteredRuntimeBundle(
binding.resolvedBundle,
preparation
)
} catch {
throw new RuntimeAcpBackendError(
'Runtime verification failed',
'untrusted-runtime'
)
}
let workspace: RuntimeAcpWorkspace
try {
workspace = await this.#options.resolveWorkspace(
preparation,
context
)
} catch {
throw new RuntimeAcpBackendError(
'Workspace verification failed',
'workspace'
)
}
this.#assertVerifiedIdentity(binding, preparation, verified)
if (workspace.workspaceIdentity !== preparation.workspaceIdentity) {
throw new RuntimeAcpBackendError(
'Workspace identity does not match its binding',
'workspace'
)
}
if (binding.poisoned) {
throw new RuntimeAcpBackendError(
'ACP binding was poisoned by uncertain model delivery',
'process'
)
}
const agentGatewayPolicy =
this.#options.modelGateway !== undefined &&
preparation.modelProfile !== undefined
? modelBridgePolicySchema.parse({
protocol: preparation.modelProfile.protocol,
model: preparation.modelProfile.model,
modelProfileDigest:
preparation.modelProfile.modelProfileDigest,
supportsImageInput:
preparation.modelProfile.capabilities.imageInput
})
: undefined
if (
agentGatewayPolicy !== undefined &&
preparation.modelBridge !== undefined &&
canonicalJson(agentGatewayPolicy) !==
canonicalJson(preparation.modelBridge.policy)
) {
throw new RuntimeAcpBackendError(
'Prompt model profile does not match its persisted policy',
'identity'
)
}
const requestedModelBridgePolicy =
agentGatewayPolicy ?? preparation.modelBridge?.policy
if (
requestedModelBridgePolicy !== undefined &&
binding.modelBridgePolicy !== undefined &&
canonicalJson(binding.modelBridgePolicy) !==
canonicalJson(requestedModelBridgePolicy)
) {
throw new RuntimeAcpBackendError(
'Model bridge policy cannot change within a Runtime binding',
'conflict'
)
}
if (
preparation.modelBridge !== undefined &&
agentGatewayPolicy === undefined
) {
for (const candidate of this.#bindings.values()) {
if (
candidate !== binding &&
candidate.modelBridgeClient?.binding.channelId ===
preparation.modelBridge.channelId
) {
throw new RuntimeAcpBackendError(
'Model bridge channel is already bound',
'conflict'
)
}
}
}
if (preparation.imageTool && !this.#options.blobSink) {
throw new RuntimeAcpBackendError('Image tools require a desktop transport', 'identity')
}
if (requestedModelBridgePolicy !== undefined) {
await this.#startModelBridge(binding, preparation, workspace)
}
const acceptedAt = new Date(this.#now()).toISOString()
binding.imageTool?.close()
binding.imageTool = undefined
if (preparation.imageTool) {
let sequence = 0n
const imageTool = new AgentImageToolMcp(preparation.imageTool, async payload => {
if (binding.activeOperationId !== preparation.operationId || binding.transportState === 'detached') {
throw new Error('Image tool prompt is no longer connected')
}
await this.#options.blobSink!({
header: {
protocolMajor: AGENT_PROTOCOL_VERSION.major, protocolMinor: AGENT_PROTOCOL_VERSION.minor,
kind: 'blob', direction: 'agent-to-main',
connectionId: binding.connectionId, generation: binding.controllerGeneration,
channelId: preparation.imageTool!.channelId, channelEpoch: preparation.imageTool!.channelEpoch,
sequence: String(++sequence), payloadLength: payload.byteLength
}, payload
}, { bindingId: binding.request.bindingId, controllerId: binding.controllerId,
controllerGeneration: binding.controllerGeneration })
})
binding.imageTool = imageTool
await imageTool.start()
}
const acceptance = remotePromptOperationAcceptanceSchema.parse({
...(binding.imageTool?.url ? { imageToolUrl: binding.imageTool.url } : {}),
bindingId: preparation.bindingId,
operationId: preparation.operationId,
requestId: preparation.requestId,
deadlineAt: preparation.deadlineAt,
acceptedAt
})
if (binding.process === undefined) {
let process: RuntimeAcpProcessOwner
const sharedKey =
this.#options.shareOwnedProcesses &&
preparation.modelProfile && this.#options.modelGateway &&
this.#options.semanticPrompts && binding.modelBridgePolicy
? canonicalJson([
verified.manifest.bundleDigest,
verified.manifest.adapterDigest,
binding.modelBridgePolicy
])
: undefined
this.#options.diagnostics?.tryRecord('runtime.starting', {
runtimeId: verified.manifest.runtimeId
})
try {
const launch: RuntimeAcpProcessLaunch = {
manifest: verified.manifest,
bundle: verified,
workspace,
scratch: workspace.scratchDirectory,
deadlineAt: preparation.deadlineAt,
budget: preparation.budget,
...(sharedKey === undefined ? {} : { sharedSessions: true }),
...(binding.modelBridgePolicy === undefined ||
binding.modelBridgeBroker === undefined
? {}
: {
modelBridge: {
socketPath: binding.modelBridgeBroker.socketPath,
policy: binding.modelBridgePolicy
}
})
}
if (sharedKey !== undefined) {
const group = await this.#acquireSharedProcess(sharedKey, launch)
binding.sharedProcess = group
group.bindings.add(binding)
process = group.process
} else {
process = await this.#options.launchProcess(launch)
}
assertProcessIdentity(process.identity)
} catch (error) {
this.#options.diagnostics?.tryRecord('runtime.start.failed', {
runtimeId: verified.manifest.runtimeId,
error
})
await this.#closeModelBridge(binding, false).catch(
() => undefined
)
throw new RuntimeAcpBackendError(
'Runtime process launch failed',
'process'
)
}
const processKey = processIdentityKey(process.identity)
const identityOwner = this.#processIdentityBindings.get(processKey)
if (
identityOwner !== undefined &&
identityOwner !== (binding.sharedProcess?.key ?? binding.request.bindingId)
) {
await Promise.resolve(
process.stop({
reason: 'identity-conflict',
deadlineAt: new Date(this.#now() + 10_000).toISOString()
})
).catch(() => undefined)
await Promise.resolve(process.reconcile()).catch(() => undefined)
throw new RuntimeAcpBackendError(
'Supervised process identity is already bound',
'identity'
)
}
this.#processIdentityBindings.set(
processKey,
binding.sharedProcess?.key ?? binding.request.bindingId
)
binding.process = process
binding.workspaceDirectory = workspace.workspaceDirectory
binding.state = 'running'
binding.operations.set(preparation.operationId, {
preparationDigest,
acceptance,
budget: preparation.budget,
modelProfile: preparation.modelProfile,
promptSequence: preparation.promptSequence
})
try {
if (
preparation.modelProfile === undefined ||
this.#options.modelGateway === undefined ||
this.#options.semanticPrompts === undefined
) {
const unsubscribe = process.subscribeOutput((output) =>
this.#acceptProcessOutput(binding, output)
)
if (unsubscribe !== undefined) {
binding.unsubscribeOutput = unsubscribe
}
}
const unsubscribeExit = binding.sharedProcess ? undefined : process.subscribeExit?.(() => {
void this.#enqueueControl(async () => {
await this.#handleProcessExit(binding)
}).catch(() => undefined)
})
if (unsubscribeExit !== undefined) {
binding.unsubscribeExit = unsubscribeExit
}
} catch {
binding.operations.delete(preparation.operationId)
await this.#stopAndReconcile(binding, 'identity-conflict').catch(
() => undefined
)
throw new RuntimeAcpBackendError(
'Runtime output subscription failed',
'process'
)
}
this.#options.diagnostics?.tryRecord('runtime.started', {
runtimeId: verified.manifest.runtimeId
})
} else if (!binding.sharedProcess) {
try {
await binding.process.beginPrompt({
deadlineAt: preparation.deadlineAt,
maximumInputBytes: preparation.budget.maximumInputBytes
})
} catch {
await this.#closeModelBridge(binding, false).catch(
() => undefined
)
throw new RuntimeAcpBackendError(
'Runtime prompt budget could not be renewed',
'process'
)
}
}
binding.inputBytes = 0
if (!binding.operations.has(preparation.operationId)) {
binding.operations.set(preparation.operationId, {
preparationDigest,
acceptance,
budget: preparation.budget,
modelProfile: preparation.modelProfile,
promptSequence: preparation.promptSequence
})
}
this.#options.semanticPrompts?.prepare({
bindingId: preparation.bindingId,
operationId: preparation.operationId,
requestId: preparation.requestId,
controllerId: binding.controllerId,
preparationDigest,
promptSequence: preparation.promptSequence
})
binding.activeOperationId = preparation.operationId
binding.nextModelRound = 0
if (
preparation.modelProfile !== undefined &&
this.#options.modelGateway !== undefined &&
this.#options.semanticPrompts !== undefined
) {
this.#scheduleOwnedPromptStart(
binding,
preparation.operationId
)
}
this.#scheduleDeadline(binding, preparation.deadlineAt)
return acceptance
}
async #complete(
params: unknown,
context: ProtocolMethodContext
): Promise<z.infer<typeof acpCompletePromptResultSchema>> {
const operation = promptCompletionSchema.parse(params)
const binding = this.#bindingForContext(operation.bindingId, context)
if (
operation.operationId !== operation.requestId ||
!binding.operations.has(operation.operationId)
) {
throw new RuntimeAcpBackendError(
'Prompt completion identity does not match a prepared operation',
'identity'
)
}
const prepared = binding.operations.get(operation.operationId)!
if (prepared.completion !== undefined) {
return acpCompletePromptResultSchema.parse(prepared.completion)
}
if (
binding.activeOperationId !== operation.operationId ||
binding.process === undefined ||
binding.state !== 'running'
) {
throw new RuntimeAcpBackendError(
'Prompt completion conflicts with the active operation',
'conflict'
)
}
if (binding.ownedAcp !== undefined) {
throw new RuntimeAcpBackendError(
'Agent-owned prompt completion is independent of Desktop ACK',
'conflict'
)
}
await this.#closeModelBridge(binding, true)
if (binding.poisoned) {
throw new RuntimeAcpBackendError(
'Model response delivery was not proven before prompt completion',
'process'
)
}
try {
await binding.process.completePrompt()
} catch {
throw new RuntimeAcpBackendError(
'Runtime prompt completion failed',
'process'
)
}
const completion = acpCompletePromptResultSchema.parse({
bindingId: operation.bindingId,
operationId: operation.operationId,
requestId: operation.requestId,
status: 'completed',
processTree: 'running'
})
prepared.completion = completion
binding.activeOperationId = undefined
binding.inputBytes = 0
if (binding.deadlineTimer !== undefined) {
clearTimeout(binding.deadlineTimer)
binding.deadlineTimer = undefined
}
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
binding.ownedPromptStartTimer = undefined
}
return completion
}
#startOwnedPrompt(
params: unknown,
context: ProtocolMethodContext
) {
const request = remoteOwnedPromptStartRequestSchema.parse(params)
const binding = this.#bindingForContext(request.bindingId, context)
const prepared = binding.operations.get(request.operationId)
const existing = this.#options.semanticPrompts?.findStarted({
bindingId: request.bindingId,
operationId: request.operationId,
controllerId: context.controller.controllerId,
startDigest: `sha256:${createHash('sha256')
.update(canonicalJson(request))
.digest('hex')}`
})
if (existing !== undefined) {
if (prepared !== undefined) {
prepared.ownedPromptStarted = true
}
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
binding.ownedPromptStartTimer = undefined
}
return { started: Promise.resolve(existing) }
}
if (
this.#options.semanticPrompts === undefined ||
prepared === undefined ||
binding.activeOperationId !== request.operationId ||
binding.process === undefined ||
binding.workspaceDirectory === undefined ||
binding.state !== 'running'
) {
throw new RuntimeAcpBackendError(
'Agent-owned prompt was not prepared',
'conflict'
)
}
if (Buffer.byteLength(canonicalJson(request.prompt)) > prepared.budget.maximumInputBytes) {
throw new RuntimeAcpBackendError('Prompt input quota reached', 'capacity')
}
if (binding.ownedAcp === undefined) {
binding.ownedAcp = new AgentOwnedAcpPrompt({
bindingId: binding.request.bindingId,
controllerId: binding.controllerId,
workspaceDirectory: binding.workspaceDirectory,
mcpServers: () => binding.imageTool?.url ? [{
type: 'http', name: remoteImageToolMcpName(binding.request.bindingId), url: binding.imageTool.url, headers: []
}] : [],
...(binding.modelBridgePolicy === undefined
? {}
: {
expectedModel: openCodeModelBridgeModelId(
binding.modelBridgePolicy.protocol,
binding.modelBridgePolicy.model
)
}),
process: binding.process,
...(binding.sharedProcess ? {
transport: binding.sharedProcess.transport,
prepareSession: async (sessionId: string, operationId: string) => {
await binding.sharedProcess!.transport.setModelRoute(
sessionId, operationId, binding.modelBridgeBroker!.socketPath,
binding.imageTool ? remoteImageToolMcpName(binding.request.bindingId) : undefined
)
}
} : {}),
transcript: this.#options.semanticPrompts,
completePrompt: async (operationId, status, _response, commitTerminal) => {
await this.#enqueueControl(async () => {
await this.#terminalizeOwnedPrompt(
binding,
operationId,
status,
commitTerminal
)
})
},
resolveTerminalState: (status) =>
binding.poisoned ? 'outcome-unknown' : status
})
}
const owner = binding.ownedAcp
// Native initialization must not hold the global control queue: cancellation,
// watchdogs and other bindings still need to make progress while it waits.
const started = owner.start(request).then(result => {
if (binding.ownedAcp === owner && binding.activeOperationId === request.operationId) {
prepared.ownedPromptStarted = true
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
binding.ownedPromptStartTimer = undefined
}
}
return result
})
return { started }
}
#attachOwnedPrompt(
params: unknown,
context: ProtocolMethodContext
) {
const request = remoteOwnedPromptAttachRequestSchema.parse(params)
this.#assertControllerLive(context)
if (request.operationId !== request.requestId) {
throw new RuntimeAcpBackendError(
'Prompt attach identity does not match',
'identity'
)
}
if (this.#options.semanticPrompts === undefined) {
throw new RuntimeAcpBackendError(
'Semantic prompt recovery is unavailable',
'process'
)
}
const binding = this.#bindings.get(request.bindingId)
if (
binding === undefined ||
binding.controllerId !== context.controller.controllerId
) {
throw new RuntimeAcpBackendError(
'Prompt attach binding does not exist for this controller',
'not-found'
)
}
if (
binding.controllerGeneration !== context.controller.generation ||
binding.connectionId !== context.controller.connectionId
) {
if (
context.controllerTakeoverProven !== true ||
binding.transportState !== 'detached' ||
context.controller.generation <= binding.controllerGeneration
) {
throw new RuntimeAcpBackendError(
'Prompt attach cannot take over this binding generation',
'stale-controller'
)
}
binding.controllerGeneration = context.controller.generation
binding.connectionId = context.controller.connectionId
}
return this.#options.semanticPrompts.attach(
request.bindingId,
request.operationId,
context.controller.controllerId
)
}
#pageOwnedPrompt(
params: unknown,
context: ProtocolMethodContext
) {
const request = remoteSemanticTranscriptPageRequestSchema.parse(params)
this.#assertControllerLive(context)
if (this.#options.semanticPrompts === undefined) {
throw new RuntimeAcpBackendError(
'Semantic prompt recovery is unavailable',
'process'
)
}
const page = this.#options.semanticPrompts.page({
...request,
controllerId: context.controller.controllerId
})
const binding = this.#bindings.get(request.bindingId)
return { ...page, pendingQuestions: binding?.ownedAcp?.pendingQuestions(request.operationId) ?? [] }
}
#ackOwnedPrompt(
params: unknown,
context: ProtocolMethodContext
) {
const request = remoteSemanticTranscriptAckRequestSchema.parse(params)
this.#assertControllerLive(context)
if (this.#options.semanticPrompts === undefined) {
throw new RuntimeAcpBackendError(
'Semantic prompt recovery is unavailable',
'process'
)
}
const result = this.#options.semanticPrompts.acknowledge({
...request,
controllerId: context.controller.controllerId
})
const operation = this.#bindings.get(request.bindingId)?.operations.get(request.operationId)
if (operation?.ownedPromptStarted) {
const terminal = this.#options.semanticPrompts.attach(
request.bindingId, request.operationId, context.controller.controllerId
)
operation.terminalAcknowledged = terminal.state !== 'starting' && terminal.state !== 'running' &&
BigInt(result.acknowledgedSequence) >= BigInt(terminal.latestSemanticSequence)
}
return result
}
async #terminalizeOwnedPrompt(
binding: BindingState,
operationId: string,
status: 'completed' | 'failed' | 'cancelled' | 'outcome-unknown',
commitTerminal: () => void
): Promise<void> {
const prepared = binding.operations.get(operationId)
if (
prepared === undefined ||
binding.activeOperationId !== operationId ||
binding.process === undefined
) {
return
}
try {
await this.#closeModelBridge(binding, true, false, true)
if (!binding.sharedProcess) await binding.process.completePrompt()
} catch (error) {
binding.poisoned = true
binding.state = 'outcome-unknown'
prepared.modelProfile = undefined
await this.#stopAndReconcile(binding, 'identity-conflict').catch(() => undefined)
throw error
}
if (binding.poisoned || status === 'outcome-unknown') {
binding.state = 'outcome-unknown'
prepared.modelProfile = undefined
await this.#stopAndReconcile(
binding,
'identity-conflict'
).catch(() => undefined)
return
}
// Persist the terminal while control is serialized, before exposing an idle binding.
commitTerminal()
prepared.terminalState =
status === 'cancelled'
? 'cancelled'
: status === 'failed'
? 'failed'
: 'completed'
prepared.processTree = 'running'
if (status === 'completed') {
prepared.completion = acpCompletePromptResultSchema.parse({
bindingId: binding.request.bindingId,
operationId,
requestId: prepared.acceptance.requestId,
status: 'completed',
processTree: 'running'
})
}
prepared.modelProfile = undefined
binding.activeOperationId = undefined
binding.inputBytes = 0
if (binding.deadlineTimer !== undefined) {
clearTimeout(binding.deadlineTimer)
binding.deadlineTimer = undefined
}
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
binding.ownedPromptStartTimer = undefined
}
try {
await this.#releaseIdleProcess(binding)
} catch (error) {
this.#options.diagnostics?.tryRecord('daemon.stop.failed', {
runtimeId: binding.request.runtimeId,
reason: 'idle-runtime-cleanup',
error
})
}
}
async #startModelBridge(
binding: BindingState,
preparation: RemotePromptOperationPreparation,
workspace: RuntimeAcpWorkspace
): Promise<void> {
const bridge = preparation.modelBridge
const useAgentGateway =
this.#options.modelGateway !== undefined &&
preparation.modelProfile !== undefined
const policy = useAgentGateway
? modelBridgePolicySchema.parse({
protocol: preparation.modelProfile!.protocol,
model: preparation.modelProfile!.model,
modelProfileDigest:
preparation.modelProfile!.modelProfileDigest,
supportsImageInput:
preparation.modelProfile!.capabilities.imageInput
})
: bridge?.policy
if (
workspace.bridgeDirectory === undefined ||
policy === undefined ||
(
!useAgentGateway &&
(
bridge === undefined ||
bridge.channelId === binding.request.bindingId
)
)
) {
throw new RuntimeAcpBackendError(
'Managed model bridge composition is unavailable or invalid',
'identity'
)
}
if (
binding.modelBridgeClient !== undefined ||
binding.modelBridgeBroker !== undefined
) {
throw new RuntimeAcpBackendError(
'Another model bridge is still active',
'conflict'
)
}
const client = useAgentGateway ? undefined : new ModelBridgeBlobClient({
binding: {
bindingId: binding.request.bindingId,
promptOperationId: preparation.operationId,
controllerId: binding.controllerId,
controllerGeneration: binding.controllerGeneration,
connectionId: binding.connectionId,
acpChannelEpoch: binding.openResult.channelEpoch,
channelId: bridge!.channelId,
channelEpoch: bridge!.channelEpoch,
runtimeId: binding.request.runtimeId,
workspaceIdentity: binding.request.workspaceIdentity,
policy,
deadlineAt: preparation.deadlineAt
},
sendBlobFrame: async (frame) =>
await this.#options.blobSink!(frame, {
bindingId: binding.request.bindingId,
controllerId: binding.controllerId,
controllerGeneration: binding.controllerGeneration
}),
onPoison: () => {
binding.poisoned = true
binding.state = 'outcome-unknown'
void this.#enqueueControl(async () => {
await this.#stopAndReconcile(
binding,
'identity-conflict'
)
}).catch(() => undefined)
},
now: this.#options.now
})
const dispatch: ModelBridgeBrokerDispatch = useAgentGateway
? async (request, context) => {
const operation = binding.operations.get(
preparation.operationId
)
const profile =
operation?.modelProfile ?? preparation.modelProfile
if (
profile === undefined ||
binding.activeOperationId !== preparation.operationId
) {
throw new RuntimeAcpBackendError(
'Prompt model credential is no longer active',
'closed'
)
}
const roundIndex = binding.nextModelRound
let exchanged
try {
exchanged = await this.#options.modelGateway!.dispatch(
{
bindingId: binding.request.bindingId,
operationId: preparation.operationId,
promptSequence: preparation.promptSequence,
roundIndex,
profileDigest: policy.modelProfileDigest,
profile
},
request,
context.signal
)
} catch (error) {
if (
error instanceof AgentModelGatewayError &&
['cancelled', 'outcome-unknown', 'response-too-large', 'timeout']
.includes(error.code)
) {
binding.poisoned = true
binding.state = 'outcome-unknown'
void this.#enqueueControl(async () => {
await this.#stopAndReconcile(
binding,
'identity-conflict'
)
}).catch(() => undefined)
}
throw error
}
return {
response: exchanged.response,
acknowledgeDelivery: async () => {
try {
await exchanged.acknowledgeDelivery()
binding.nextModelRound += 1
} catch (error) {
binding.poisoned = true
binding.state = 'outcome-unknown'
throw error
}
},
failDelivery: () => {
binding.poisoned = true
binding.state = 'outcome-unknown'
exchanged.failDelivery()
}
}
}
: async (request, context) =>
await client!.exchange(request, context)
const broker =
this.#options.createModelBridgeBroker?.({
bridgeDirectory: workspace.bridgeDirectory,
dispatch
}) ??
new ModelBridgeBrokerServer({
scratchDirectory: workspace.bridgeDirectory,
dispatch
})
binding.modelBridgeClient = client
binding.modelBridgeBroker = broker
binding.modelBridgePolicy = policy
try {
await broker.listen()
} catch {
binding.modelBridgeClient = undefined
binding.modelBridgeBroker = undefined
await client?.close({ poisonIfActive: false })
throw new RuntimeAcpBackendError(
'Managed model bridge broker could not start',
'process'
)
}
}
async #closeModelBridge(
binding: BindingState,
poisonIfActive: boolean,
preserveRuntime = false,
waitForDelivery = false
): Promise<void> {
binding.imageTool?.close()
binding.imageTool = undefined
const broker = binding.modelBridgeBroker
const client = binding.modelBridgeClient
binding.modelBridgeBroker = undefined
binding.modelBridgeClient = undefined
let brokerError: unknown
if (binding.sharedProcess && binding.ownedAcp?.sessionId && binding.activeOperationId) {
await binding.sharedProcess.transport.setModelRoute(
binding.ownedAcp.sessionId, binding.activeOperationId
).catch(() => undefined)
}
try {
await broker?.close({ waitForDelivery })
} catch (error) {
brokerError = error
}
if (preserveRuntime && client?.active === true) {
binding.poisoned = true
}
await client?.close({
poisonIfActive: preserveRuntime ? false : poisonIfActive
})
if (brokerError !== undefined) {
throw brokerError
}
}
async #poisonBinding(binding: BindingState): Promise<void> {
binding.poisoned = true
binding.state = 'outcome-unknown'
await this.#closeModelBridge(binding, true).catch(
() => undefined
)
await this.#stopAndReconcile(
binding,
'identity-conflict'
).catch(() => undefined)
}
#getCursors(
params: unknown,
context: ProtocolMethodContext
): {
lastOutboundJournaledSequence: string
lastOutboundDeliveredSequence: string
lastInboundJournaledSequence: string
lastMainAckSequence: string
} {
const request = getCursorsRequestSchema.parse(params)
const binding = this.#bindingForContext(request.bindingId, context)
return this.#bindingCursors(binding)
}
#bindingCursors(binding: BindingState): {
lastOutboundJournaledSequence: string
lastOutboundDeliveredSequence: string
lastInboundJournaledSequence: string
lastMainAckSequence: string
} {
const outbound = this.#cursor(binding, 'main-to-runtime')
const inbound = this.#cursor(binding, 'runtime-to-main')
return {
lastOutboundJournaledSequence:
outbound?.journaledSequence ?? ZERO_CURSOR,
lastOutboundDeliveredSequence:
outbound?.deliveredSequence ?? ZERO_CURSOR,
lastInboundJournaledSequence:
inbound?.journaledSequence ?? ZERO_CURSOR,
lastMainAckSequence: inbound?.mainAckSequence ?? ZERO_CURSOR
}
}
async #escalate(
params: unknown,
context: ProtocolMethodContext
): Promise<{ bindingId: string; stopped: true }> {
const escalation = cancellationEscalationSchema.parse(params)
const binding = this.#bindingForContext(escalation.bindingId, context)
if (
binding.activeOperationId !== undefined &&
(binding.activeOperationId !== escalation.operationId ||
escalation.operationId !== escalation.requestId)
) {
throw new RuntimeAcpBackendError(
'Cancellation operation identity does not match',
'identity'
)
}
if (
binding.activeOperationId !== undefined &&
this.#options.semanticPrompts !== undefined
) {
if (binding.ownedAcp !== undefined) {
await rejectAfter(
binding.ownedAcp.cancel(),
OWNED_PROMPT_CANCEL_GRACE_MS,
'Runtime prompt cancellation timed out'
).catch(() => undefined)
}
this.#appendAbnormalOwnedPromptTerminal(binding, {
status: 'cancelled',
name: 'PromptCancelled',
message: 'Prompt execution was cancelled'
})
}
await this.#stopAndReconcile(binding, 'user-cancelled')
return { bindingId: escalation.bindingId, stopped: true }
}
async #reconcile(
params: unknown,
context: ProtocolMethodContext
): Promise<
| {
status: 'terminal'
terminalState:
| 'completed'
| 'failed'
| 'cancelled'
| 'interrupted'
processTree: 'running' | 'empty'
}
| {
status: 'running' | 'outcome-unknown'
processTree: 'running' | 'unknown'
}
> {
const operation = promptReconciliationSchema.parse(params)
const binding = this.#bindingForContext(operation.bindingId, context)
if (operation.operationId !== operation.requestId) {
throw new RuntimeAcpBackendError(
'Reconciliation operation identity does not match',
'identity'
)
}
const prepared = binding.operations.get(operation.operationId)
if (prepared === undefined) {
throw new RuntimeAcpBackendError(
'Reconciliation operation was not prepared',
'identity'
)
}
if (prepared.completion !== undefined) {
return {
status: 'terminal',
terminalState: 'completed',
processTree: prepared.completion.processTree
}
}
if (prepared.terminalState === 'outcome-unknown') {
return { status: 'outcome-unknown', processTree: 'unknown' }
}
if (prepared.terminalState !== undefined) {
return {
status: 'terminal',
terminalState: prepared.terminalState,
processTree:
prepared.processTree ?? (prepared.terminalState === 'completed' ? 'running' : 'empty')
}
}
if (binding.activeOperationId !== operation.operationId) {
return { status: 'outcome-unknown', processTree: 'unknown' }
}
if (binding.process === undefined) {
if (binding.state === 'closed') {
return {
status: 'terminal',
terminalState: binding.terminalState ?? 'interrupted',
processTree: 'empty'
}
}
return { status: 'outcome-unknown', processTree: 'unknown' }
}
let reconciliation: RuntimeAcpProcessReconciliation
try {
reconciliation = await binding.process.reconcile()
} catch {
binding.state = 'outcome-unknown'
return { status: 'outcome-unknown', processTree: 'unknown' }
}
if (!sameProcessIdentity(binding.process.identity, reconciliation.identity)) {
binding.state = 'outcome-unknown'
await Promise.resolve(
binding.process.stop({
reason: 'identity-conflict',
deadlineAt: new Date(this.#now() + 10_000).toISOString()
})
).catch(() => undefined)
throw new RuntimeAcpBackendError(
'Supervised process identity conflicts with its binding',
'identity'
)
}
if (reconciliation.processTree !== 'empty') {
binding.state =
reconciliation.processTree === 'running'
? 'running'
: 'outcome-unknown'
return reconciliation.processTree === 'running'
? { status: 'running', processTree: 'running' }
: { status: 'outcome-unknown', processTree: 'unknown' }
}
const completedState = terminalState(reconciliation.state)
await this.#closeModelBridge(binding, true)
this.#finishProcess(binding)
binding.state = 'open'
binding.terminalState = completedState
return {
status: 'terminal',
terminalState: completedState,
processTree: 'empty'
}
}
async #acceptProcessOutput(
binding: BindingState,
output: RuntimeAcpProcessOutput
): Promise<void> {
if (output.stream === 'stderr' || output.data.byteLength === 0) {
return
}
if (binding.state !== 'running') {
throw new RuntimeAcpBackendError(
'Runtime output arrived after the binding stopped',
'closed'
)
}
this.#assertBeforeDeadline(binding)
for (
let offset = 0;
offset < output.data.byteLength;
offset += AGENT_PROTOCOL_LIMITS.maximumAcpFrameBytes
) {
const payload = output.data.slice(
offset,
offset + AGENT_PROTOCOL_LIMITS.maximumAcpFrameBytes
)
try {
await this.#enqueueOutput(binding, payload, async () => {
const sequence = binding.nextOutputSequence.toString()
let retryDelay = ACP_JOURNAL_RETRY_INITIAL_MILLISECONDS
while (true) {
try {
this.#options.journal.appendAcpFrame({
controllerId: binding.controllerId,
bindingId: binding.request.bindingId,
channelEpoch: binding.openResult.channelEpoch,
direction: 'runtime-to-main',
sequence,
payload
})
break
} catch (error) {
if (!(error instanceof EventJournalCapacityError)) {
throw error
}
if (binding.state !== 'running') {
throw new RuntimeAcpBackendError(
'Runtime output arrived after the binding stopped',
'closed'
)
}
this.#assertBeforeDeadline(binding)
await delay(retryDelay)
retryDelay = Math.min(
ACP_JOURNAL_RETRY_MAXIMUM_MILLISECONDS,
retryDelay * 2
)
}
}
binding.nextOutputSequence += 1n
if (binding.transportState === 'live') {
try {
await this.#deliverOutput(binding, sequence, payload)
} catch {
await this.#detachBinding(binding)
}
}
})
} catch (error) {
await this.#stopAndReconcile(binding, 'output-quota').catch(
() => undefined
)
if (
error instanceof RuntimeAcpBackendError &&
error.code === 'output-quota'
) {
throw error
}
throw new RuntimeAcpBackendError(
'Runtime durable output journal quota reached',
'output-quota'
)
}
}
}
async #deliverOutput(
binding: BindingState,
sequence: string,
payload: Uint8Array
): Promise<void> {
const frame: AgentFrame = {
header: {
protocolMajor: AGENT_PROTOCOL_VERSION.major,
protocolMinor: AGENT_PROTOCOL_VERSION.minor,
connectionId: binding.connectionId,
generation: binding.controllerGeneration,
channelId: binding.request.bindingId,
channelEpoch: binding.openResult.channelEpoch,
direction: 'agent-to-main',
sequence,
kind: 'acp',
payloadLength: payload.byteLength
},
payload
}
await this.#options.outputSink(frame, {
bindingId: binding.request.bindingId,
controllerId: binding.controllerId,
controllerGeneration: binding.controllerGeneration
})
}
async #detachBinding(binding: BindingState): Promise<void> {
if (binding.transportState === 'detached') {
return
}
binding.transportState = 'detached'
binding.imageTool?.close()
binding.imageTool = undefined
if (binding.modelBridgeClient !== undefined) {
await this.#closeModelBridge(binding, false, true).catch(
() => undefined
)
}
await this.#releaseIdleProcess(binding)
}
async #releaseIdleProcess(binding: BindingState): Promise<void> {
if (binding.activeOperationId !== undefined || !binding.process) return
const group = binding.sharedProcess
if (group) {
if ([...group.bindings].some(peer => peer.activeOperationId !== undefined)) return
await this.#stopSharedProcess(group, 'binding-closed')
} else {
await binding.process.stop({
reason: 'binding-closed', deadlineAt: new Date(this.#now() + 10_000).toISOString()
})
const result = await binding.process.reconcile()
if (!sameProcessIdentity(result.identity, binding.process.identity) || result.processTree !== 'empty') {
throw new RuntimeAcpBackendError('Idle Runtime process stop did not reconcile', 'process')
}
await this.#handleProcessExit(binding)
}
}
async #stopAndReconcile(
binding: BindingState,
reason: RuntimeAcpProcessStopReason
): Promise<void> {
if (binding.state === 'closed') {
return
}
this.#appendAbnormalOwnedPromptTerminal(
binding,
reason === 'user-cancelled'
? {
status: 'cancelled',
name: 'PromptCancelled',
message: 'Prompt execution was cancelled'
}
: {
status: this.#abnormalOwnedPromptState(binding),
name:
reason === 'deadline-exceeded'
? 'PromptDeadlineExceeded'
: 'RuntimeStopped',
message:
reason === 'deadline-exceeded'
? 'Prompt execution exceeded its Runtime deadline'
: 'Runtime stopped before the prompt reached a terminal state'
}
)
await this.#closeModelBridge(binding, true)
if (binding.process === undefined) {
this.#finishBinding(binding, 'interrupted')
return
}
if (binding.sharedProcess) {
const group = binding.sharedProcess
const prepared = binding.activeOperationId
? binding.operations.get(binding.activeOperationId) : undefined
try {
await rejectAfter(
binding.ownedAcp?.releaseSession() ?? Promise.resolve(),
OWNED_PROMPT_CANCEL_GRACE_MS * 3,
'Shared Runtime Session release timed out'
)
if (prepared) prepared.processTree = group.exited ? 'empty' : 'running'
this.#finishBinding(binding, reason === 'user-cancelled' ? 'cancelled' : 'interrupted')
} catch {
// A non-responsive native control plane affects the whole process.
// Ordinary Session cancellation never reaches this fallback.
await this.#stopSharedProcess(group, reason)
}
return
}
binding.state = 'stopping'
if (!binding.stopRequested) {
binding.stopRequested = true
try {
await binding.process.stop({
reason,
deadlineAt: new Date(this.#now() + 10_000).toISOString()
})
} catch {
binding.state = 'outcome-unknown'
throw new RuntimeAcpBackendError(
'Runtime process stop failed',
'process'
)
}
}
let reconciliation: RuntimeAcpProcessReconciliation
try {
reconciliation = await binding.process.reconcile()
} catch {
binding.state = 'outcome-unknown'
throw new RuntimeAcpBackendError(
'Runtime process reconciliation failed',
'process'
)
}
if (!sameProcessIdentity(binding.process.identity, reconciliation.identity)) {
binding.state = 'outcome-unknown'
throw new RuntimeAcpBackendError(
'Supervised process identity conflicts with its binding',
'identity'
)
}
if (reconciliation.processTree !== 'empty') {
binding.state = 'outcome-unknown'
throw new RuntimeAcpBackendError(
'Runtime process tree is not empty',
'process'
)
}
this.#finishBinding(binding, terminalState(reconciliation.state))
}
async #handleProcessExit(binding: BindingState): Promise<void> {
if (binding.process === undefined) {
return
}
if (binding.activeOperationId === undefined) {
for (const operation of binding.operations.values()) {
operation.processTree = 'empty'
if (operation.completion) operation.completion.processTree = 'empty'
}
this.#finishProcess(binding)
binding.state = 'open'
return
}
this.#appendAbnormalOwnedPromptTerminal(binding, {
status: this.#abnormalOwnedPromptState(binding),
name: 'RuntimeExited',
message: 'Runtime exited before the prompt reached a terminal state'
})
await this.#closeModelBridge(binding, true).catch(
() => undefined
)
let state: 'completed' | 'failed' | 'cancelled' | 'interrupted' =
'interrupted'
let diagnosticOutcome: AgentDiagnosticRecord['outcome'] =
'interrupted'
try {
const reconciliation = await binding.process.reconcile()
if (
sameProcessIdentity(
binding.process.identity,
reconciliation.identity
) &&
reconciliation.processTree === 'empty'
) {
state = terminalState(reconciliation.state)
diagnosticOutcome = state
}
} catch (error) {
binding.state = 'outcome-unknown'
diagnosticOutcome = 'outcome-unknown'
this.#options.diagnostics?.tryRecord('runtime.exited', {
runtimeId: binding.resolvedBundle.entry.runtimeId,
outcome: diagnosticOutcome,
error
})
this.#finishBinding(binding, state)
return
}
this.#options.diagnostics?.tryRecord('runtime.exited', {
runtimeId: binding.resolvedBundle.entry.runtimeId,
outcome: diagnosticOutcome
})
this.#finishBinding(binding, state)
}
#abnormalOwnedPromptState(
binding: BindingState
): 'failed' | 'outcome-unknown' {
return binding.poisoned || binding.nextModelRound > 0
? 'outcome-unknown'
: 'failed'
}
#appendAbnormalOwnedPromptTerminal(
binding: BindingState,
terminal: {
status: 'failed' | 'cancelled' | 'outcome-unknown'
name: string
message: string
}
): void {
const operationId = binding.activeOperationId
const prepared =
operationId === undefined
? undefined
: binding.operations.get(operationId)
if (
operationId === undefined ||
prepared === undefined ||
this.#options.semanticPrompts === undefined ||
(
prepared.modelProfile === undefined &&
binding.ownedAcp === undefined
)
) {
return
}
try {
this.#options.semanticPrompts.append({
bindingId: binding.request.bindingId,
operationId,
kind: 'prompt-terminal',
payload: {
status: terminal.status,
error: {
name: terminal.name,
message: terminal.message
}
},
terminalState: terminal.status
})
prepared.terminalState = terminal.status
} catch {
// A concurrent Agent-owned terminal transition already won.
}
}
#finishBinding(
binding: BindingState,
state: 'completed' | 'failed' | 'cancelled' | 'interrupted'
): void {
this.#finishProcess(binding)
this.#options.journal.retireAcpBinding?.({
controllerId: binding.controllerId,
bindingId: binding.request.bindingId,
channelEpoch: binding.openResult.channelEpoch
})
binding.state = 'closed'
binding.terminalState = state
}
#finishProcess(binding: BindingState): void {
const group = binding.sharedProcess
if (group) {
group.bindings.delete(binding)
binding.sharedProcess = undefined
this.#scheduleSharedIdle(group)
} else if (binding.process !== undefined) {
this.#processIdentityBindings.delete(
processIdentityKey(binding.process.identity)
)
}
binding.unsubscribeOutput?.()
binding.unsubscribeOutput = undefined
binding.unsubscribeExit?.()
binding.unsubscribeExit = undefined
binding.process = undefined
binding.ownedAcp?.close()
binding.ownedAcp = undefined
binding.workspaceDirectory = undefined
for (const operation of binding.operations.values()) {
operation.modelProfile = undefined
}
binding.stopRequested = false
binding.activeOperationId = undefined
if (binding.deadlineTimer !== undefined) {
clearTimeout(binding.deadlineTimer)
binding.deadlineTimer = undefined
}
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
binding.ownedPromptStartTimer = undefined
}
}
async #acquireSharedProcess(key: string, launch: RuntimeAcpProcessLaunch): Promise<SharedProcess> {
const existing = this.#sharedProcesses.get(key)
if (existing && !existing.exited) {
if (existing.idleTimer) clearTimeout(existing.idleTimer)
existing.idleTimer = undefined
return existing
}
const process = await this.#options.launchProcess(launch)
assertProcessIdentity(process.identity)
const group: SharedProcess = {
key, process, transport: new AgentAcpConnection(process),
bindings: new Set(), exited: false
}
group.unsubscribeExit = process.subscribeExit?.(() => {
void this.#enqueueControl(() => this.#handleSharedExit(group)).catch(() => undefined)
}) ?? undefined
this.#sharedProcesses.set(key, group)
return group
}
#scheduleSharedIdle(group: SharedProcess): void {
if ([...group.bindings].some(binding => binding.activeOperationId !== undefined) || group.exited || this.#disposed || group.idleTimer) return
group.idleTimer = setTimeout(() => {
group.idleTimer = undefined
void this.#enqueueControl(async () => {
if (![...group.bindings].some(binding => binding.activeOperationId !== undefined)) {
await this.#stopSharedProcess(group, 'binding-closed')
}
}).catch(() => undefined)
}, this.#options.sharedProcessIdleMs ?? SHARED_PROCESS_IDLE_MS)
group.idleTimer.unref?.()
}
async #stopSharedProcess(group: SharedProcess, reason: RuntimeAcpProcessStopReason): Promise<void> {
if (group.exited) return
if (group.idleTimer) clearTimeout(group.idleTimer)
group.idleTimer = undefined
await group.process.stop({
reason, deadlineAt: new Date(this.#now() + 10_000).toISOString()
})
const result = await group.process.reconcile()
if (!sameProcessIdentity(result.identity, group.process.identity) || result.processTree !== 'empty') {
throw new RuntimeAcpBackendError('Shared Runtime process stop did not reconcile', 'process')
}
await this.#handleSharedExit(group)
}
async #handleSharedExit(group: SharedProcess): Promise<void> {
if (group.exited) return
group.exited = true
if (group.idleTimer) clearTimeout(group.idleTimer)
group.unsubscribeExit?.()
group.transport.dispose()
if (this.#sharedProcesses.get(group.key) === group) this.#sharedProcesses.delete(group.key)
this.#processIdentityBindings.delete(processIdentityKey(group.process.identity))
for (const binding of [...group.bindings]) {
if (binding.activeOperationId) {
const prepared = binding.operations.get(binding.activeOperationId)
if (prepared) prepared.processTree = 'empty'
}
await this.#handleProcessExit(binding)
}
}
#scheduleDeadline(binding: BindingState, deadlineAt: string): void {
if (binding.deadlineTimer !== undefined) {
clearTimeout(binding.deadlineTimer)
binding.deadlineTimer = undefined
}
if (deadlineAt === UNBOUNDED_REMOTE_PROMPT_DEADLINE) {
return
}
const delay = Math.max(
1,
Math.min(Date.parse(deadlineAt) - this.#now(), 0x7fff_ffff)
)
binding.deadlineTimer = setTimeout(() => {
void this.#enqueueControl(() =>
this.#stopAndReconcile(binding, 'deadline-exceeded')
).catch(() => undefined)
}, delay)
binding.deadlineTimer.unref?.()
}
#scheduleOwnedPromptStart(
binding: BindingState,
operationId: string
): void {
if (binding.ownedPromptStartTimer !== undefined) {
clearTimeout(binding.ownedPromptStartTimer)
}
binding.ownedPromptStartTimer = setTimeout(() => {
void this.#enqueueControl(async () => {
const operation = binding.operations.get(operationId)
if (
binding.activeOperationId !== operationId ||
operation?.ownedPromptStarted === true
) {
return
}
binding.poisoned = true
binding.state = 'outcome-unknown'
try {
this.#options.semanticPrompts?.append({
bindingId: binding.request.bindingId,
operationId,
kind: 'prompt-terminal',
payload: {
status: 'outcome-unknown',
error: {
name: 'PromptStartTimeout',
message:
'Desktop disconnected after preparation but before prompt start'
}
},
terminalState: 'outcome-unknown'
})
} catch {
// Another terminal transition won the race.
}
await this.#stopAndReconcile(
binding,
'identity-conflict'
).catch(() => undefined)
}).catch(() => undefined)
}, OWNED_PROMPT_START_TIMEOUT_MS)
binding.ownedPromptStartTimer.unref?.()
}
#bindConnectionLifetime(
binding: BindingState,
signal: AbortSignal | undefined
): void {
if (signal === undefined || this.#boundConnectionSignals.has(signal)) {
return
}
this.#boundConnectionSignals.add(signal)
const disconnect = (): void => {
for (const candidate of this.#bindings.values()) {
if (
candidate.controllerId === binding.controllerId &&
candidate.controllerGeneration === binding.controllerGeneration &&
candidate.connectionId === binding.connectionId &&
candidate.state !== 'closed'
) {
void this.#enqueueControl(() =>
this.#detachBinding(candidate)
).catch(() => undefined)
}
}
}
if (signal.aborted) {
disconnect()
} else {
signal.addEventListener('abort', disconnect, { once: true })
}
}
#assertPreparationIdentity(
binding: BindingState,
preparation: RemotePromptOperationPreparation,
context: ProtocolMethodContext
): void {
if (
preparation.controllerId !== binding.controllerId ||
preparation.controllerGeneration !== binding.controllerGeneration ||
preparation.connectionGeneration !== binding.controllerGeneration ||
preparation.channelEpoch !== binding.openResult.channelEpoch ||
preparation.bindingId !== binding.request.bindingId ||
preparation.runtimeId !== binding.request.runtimeId ||
preparation.runtimeBundleDigest !==
binding.request.runtimeBundleDigest ||
preparation.workspaceIdentity !== binding.request.workspaceIdentity ||
context.controller.connectionId !== binding.connectionId
) {
throw new RuntimeAcpBackendError(
'Prompt preparation identity does not match its ACP binding',
'identity'
)
}
if (preparation.operationId !== preparation.requestId) {
throw new RuntimeAcpBackendError(
'Prompt operation and request identities must match',
'identity'
)
}
}
#assertVerifiedIdentity(
binding: BindingState,
preparation: RemotePromptOperationPreparation,
verified: RuntimeAcpVerifiedBundle
): void {
const manifest = verified.manifest
if (
(manifest.provider !== 'opencode' && manifest.provider !== 'continue') ||
manifest.runtimeId !== preparation.runtimeId ||
manifest.bundleDigest !== preparation.runtimeBundleDigest ||
manifest.adapterDigest !== preparation.runtimeAdapterDigest ||
manifest.acpCapabilitiesDigest !==
binding.openResult.acpCapabilitiesDigest ||
(binding.resolvedBundle.entry.manifestDigest !== undefined &&
binding.resolvedBundle.entry.manifestDigest !==
verified.manifestDigest) ||
verified.bundleDirectory !== binding.resolvedBundle.bundleDirectory
) {
throw new RuntimeAcpBackendError(
'Reverified Runtime identity does not match the prompt binding',
'untrusted-runtime'
)
}
}
#bindingForContext(
bindingId: string,
context: ProtocolMethodContext
): BindingState {
this.#assertControllerLive(context)
const binding = this.#bindings.get(bindingId)
if (binding === undefined) {
throw new RuntimeAcpBackendError(
'ACP binding does not exist',
'not-found'
)
}
this.#assertBindingOwner(binding, context)
return binding
}
#assertBindingOwner(
binding: BindingState,
context: ProtocolMethodContext
): void {
if (
binding.controllerId !== context.controller.controllerId ||
binding.controllerGeneration !== context.controller.generation ||
binding.connectionId !== context.controller.connectionId
) {
throw new RuntimeAcpBackendError(
'ACP binding belongs to another controller generation',
'stale-controller'
)
}
}
#assertControllerLive(context: ProtocolMethodContext): void {
if (
context.signal?.aborted === true ||
context.controller.leaseExpiresAt <= this.#now()
) {
throw new RuntimeAcpBackendError(
'Controller connection is no longer current',
'stale-controller'
)
}
}
#assertOpenCapacity(controllerId: string): void {
const active = [...this.#bindings.values()].filter(
(binding) => binding.state !== 'closed'
)
if (
active.length >= this.#limits.maximumBindings ||
active.filter((binding) => binding.controllerId === controllerId)
.length >= this.#limits.maximumControllerBindings ||
this.#lifetimeBindings >= this.#limits.maximumLifetimeBindings
) {
throw new RuntimeAcpBackendError(
'ACP binding capacity reached',
'capacity'
)
}
}
#assertBeforeDeadline(
binding: BindingState,
deadlineAt = this.#activeAcceptance(binding)?.deadlineAt
): void {
if (
deadlineAt === undefined ||
!Number.isFinite(Date.parse(deadlineAt)) ||
Date.parse(deadlineAt) <= this.#now()
) {
throw new RuntimeAcpBackendError(
'Prompt operation deadline has expired',
'deadline'
)
}
}
#activeAcceptance(
binding: BindingState
): RemotePromptOperationAcceptance | undefined {
if (binding.activeOperationId === undefined) {
return undefined
}
return binding.operations.get(binding.activeOperationId)?.acceptance
}
#activeBudget(
binding: BindingState
): RemotePromptOperationPreparation['budget'] {
const acceptance = this.#activeAcceptance(binding)
if (acceptance === undefined) {
throw new RuntimeAcpBackendError(
'Prompt operation is not prepared',
'identity'
)
}
const operation = binding.operations.get(binding.activeOperationId!)
if (operation === undefined) {
throw new RuntimeAcpBackendError(
'Prompt operation is not active',
'identity'
)
}
return operation.budget
}
#cursor(
binding: BindingState,
direction: 'main-to-runtime' | 'runtime-to-main'
): AcpJournalCursor | undefined {
const cursor = this.#options.journal.getAcpCursor(
binding.request.bindingId,
binding.openResult.channelEpoch,
direction
)
return cursor === undefined
? undefined
: acpJournalCursorSchema.parse(cursor)
}
async #enqueueControl<T>(action: () => Promise<T> | T): Promise<T> {
if (this.#pendingControl >= this.#limits.maximumPendingControl) {
throw new RuntimeAcpBackendError(
'ACP global control queue is full',
'capacity'
)
}
this.#pendingControl += 1
const previous = this.#controlTail
let release!: () => void
this.#controlTail = new Promise<void>((resolve) => {
release = resolve
})
await previous
try {
return await action()
} finally {
this.#pendingControl -= 1
release()
}
}
#enqueuePublicControl<T>(
action: () => Promise<T> | T
): Promise<T> {
this.#assertNotDisposed()
return this.#enqueueControl(action)
}
#assertNotDisposed(): void {
if (this.#disposed) {
throw new RuntimeAcpBackendError(
'Runtime backend is closed',
'closed'
)
}
}
async #enqueueInput(
binding: BindingState,
action: () => Promise<void>
): Promise<void> {
if (
binding.pendingInput >=
this.#limits.maximumPendingInputPerBinding ||
this.#pendingInputGlobal >=
this.#limits.maximumPendingInputGlobal
) {
throw new RuntimeAcpBackendError(
'ACP input queue is full',
'capacity'
)
}
binding.pendingInput += 1
this.#pendingInputGlobal += 1
const previous = binding.inputTail
let release!: () => void
binding.inputTail = new Promise<void>((resolve) => {
release = resolve
})
await previous
try {
await action()
} finally {
binding.pendingInput -= 1
this.#pendingInputGlobal -= 1
release()
}
}
async #enqueueOutput(
binding: BindingState,
payload: Uint8Array,
action: () => Promise<void>
): Promise<void> {
if (
binding.pendingOutput >=
this.#limits.maximumPendingOutputPerBinding ||
this.#pendingOutputGlobal >=
this.#limits.maximumPendingOutputGlobal ||
binding.pendingOutputBytes + payload.byteLength >
this.#limits.maximumPendingOutputBytesPerBinding ||
this.#pendingOutputBytesGlobal + payload.byteLength >
this.#limits.maximumPendingOutputBytesGlobal
) {
throw new RuntimeAcpBackendError(
'ACP output queue is full',
'capacity'
)
}
binding.pendingOutput += 1
binding.pendingOutputBytes += payload.byteLength
this.#pendingOutputGlobal += 1
this.#pendingOutputBytesGlobal += payload.byteLength
const previous = binding.outputTail
let release!: () => void
binding.outputTail = new Promise<void>((resolve) => {
release = resolve
})
await previous
try {
await action()
} finally {
binding.pendingOutput -= 1
binding.pendingOutputBytes -= payload.byteLength
this.#pendingOutputGlobal -= 1
this.#pendingOutputBytesGlobal -= payload.byteLength
release()
}
}
#allocateChannelEpoch(): string {
if (this.#nextChannelEpoch > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new RuntimeAcpBackendError(
'ACP channel epoch lifetime is exhausted',
'capacity'
)
}
const epoch = positiveAgentSequenceSchema.parse(
this.#nextChannelEpoch.toString()
)
this.#nextChannelEpoch += 1n
return epoch
}
#now(): number {
return this.#options.now?.() ?? Date.now()
}
}
function assertProcessIdentity(identity: RuntimeAcpProcessIdentity): void {
agentIdentifierSchema.parse(identity.launchId)
agentIdentifierSchema.parse(identity.processId)
sha256DigestSchema.parse(identity.supervisorIdentityDigest)
}
function sameProcessIdentity(
left: RuntimeAcpProcessIdentity,
right: RuntimeAcpProcessIdentity
): boolean {
try {
assertProcessIdentity(right)
} catch {
return false
}
return (
left.launchId === right.launchId &&
left.processId === right.processId &&
left.supervisorIdentityDigest === right.supervisorIdentityDigest
)
}
function processIdentityKey(identity: RuntimeAcpProcessIdentity): string {
return [
identity.launchId,
identity.processId,
identity.supervisorIdentityDigest
].join('\0')
}
async function delay(milliseconds: number): Promise<void> {
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, milliseconds)
timer.unref?.()
})
}
function digestSecretBearingPreparation(
preparation: RemotePromptOperationPreparation
): string {
return `sha256:${createHash('sha256')
.update(canonicalJson(preparation))
.digest('hex')}`
}
function terminalState(
state: RuntimeAcpProcessReconciliation['state']
): 'completed' | 'failed' | 'cancelled' | 'interrupted' {
switch (state) {
case 'completed':
return 'completed'
case 'failed':
return 'failed'
case 'cancelled':
return 'cancelled'
default:
return 'interrupted'
}
}
function rejectAfter<T>(
promise: Promise<T>,
timeoutMs: number,
message: string
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error(message)), timeoutMs)
timeout.unref?.()
promise.then(
(value) => {
clearTimeout(timeout)
resolve(value)
},
(error: unknown) => {
clearTimeout(timeout)
reject(error)
}
)
})
}
function boundedLimit(value: number | undefined, fallback: number): number {
const candidate = value ?? fallback
if (
!Number.isSafeInteger(candidate) ||
candidate < 1 ||
candidate > Number.MAX_SAFE_INTEGER
) {
throw new RangeError('Invalid Runtime ACP backend limit')
}
return candidate
}