fix: allow concurrent and unrestricted Runtime sessions
Local embedded OpenCode serialized otherwise independent conversations, and GoodBuddy could stop long Runtime tasks after fixed tool, activity, model-call, or accumulated display-output limits. Different conversations can now run concurrently while requests in the same conversation stay ordered. Dynamic MCP tools remain isolated to the current request. OpenCode, Continue, managed ACP, and the Agent model gateway no longer stop native execution at GoodBuddy-defined call counts or prompt-wide output totals; display copies truncate and transport queues apply backpressure instead. Prompt-wide quota fields are removed from the current contracts. Per-call model settings, provider request timeouts, cancellation, and same-conversation ordering remain unchanged. Release note: 修复本机 OpenCode 不同会话互相等待的问题,并移除 GoodBuddy 对单次 Runtime 工具、活动和模型调用次数的人为上限;长输出现在只截断展示或施加传输背压,不再取消原生任务。
This commit is contained in:
@@ -36,8 +36,6 @@ const profile: AgentPromptModelProfile = {
|
||||
capabilities: { imageInput: false },
|
||||
limits: {
|
||||
maximumOutputTokens: 4_096,
|
||||
maximumModelCalls: 8,
|
||||
maximumTotalOutputTokens: 32_768,
|
||||
requestTimeoutMilliseconds: 5_000
|
||||
}
|
||||
}
|
||||
@@ -124,6 +122,34 @@ describe('AgentModelGateway', () => {
|
||||
ledger.close()
|
||||
})
|
||||
|
||||
it('does not impose prompt-wide model-call or output-token quotas', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>(async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
id: 'response',
|
||||
usage: { output_tokens: 4_096 }
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
}
|
||||
)
|
||||
)
|
||||
const { gateway, ledger } = setup(fetcher)
|
||||
|
||||
for (let roundIndex = 0; roundIndex < 101; roundIndex += 1) {
|
||||
const result = await gateway.dispatch(
|
||||
{ ...context, roundIndex },
|
||||
request,
|
||||
new AbortController().signal
|
||||
)
|
||||
await result.acknowledgeDelivery()
|
||||
}
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(101)
|
||||
ledger.close()
|
||||
})
|
||||
|
||||
it('caps OpenCode chat-completions max_tokens before dispatch', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>(async (_url, init) => {
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({
|
||||
|
||||
@@ -313,7 +313,6 @@ export type AgentModelGatewayContext = {
|
||||
export class AgentModelGateway {
|
||||
readonly #ledger: AgentModelCallLedger
|
||||
readonly #fetch: typeof fetch
|
||||
readonly #promptOutputTokens = new Map<string, number>()
|
||||
|
||||
constructor(options: {
|
||||
ledger: AgentModelCallLedger
|
||||
@@ -334,24 +333,10 @@ export class AgentModelGateway {
|
||||
}> {
|
||||
signal.throwIfAborted()
|
||||
const request = remoteModelGatewayRequestSchema.parse(requestInput)
|
||||
const promptKey = `${context.bindingId}\0${context.operationId}`
|
||||
const usedOutputTokens = this.#promptOutputTokens.get(promptKey) ?? 0
|
||||
const remainingOutputTokens =
|
||||
context.profile.limits.maximumTotalOutputTokens - usedOutputTokens
|
||||
if (remainingOutputTokens < 1) {
|
||||
throw new AgentModelGatewayError(
|
||||
'policy',
|
||||
'Prompt total output token limit is exhausted'
|
||||
)
|
||||
}
|
||||
const reservedOutputTokens = Math.min(
|
||||
context.profile.limits.maximumOutputTokens,
|
||||
remainingOutputTokens
|
||||
)
|
||||
const prepared = prepareProviderRequest(
|
||||
context.profile,
|
||||
request,
|
||||
reservedOutputTokens
|
||||
context.profile.limits.maximumOutputTokens
|
||||
)
|
||||
const requestDigest = sha256(prepared.canonicalRequest)
|
||||
const callId = createAgentModelCallId(context)
|
||||
@@ -444,14 +429,6 @@ export class AgentModelGateway {
|
||||
headers,
|
||||
bodyBase64: Buffer.from(bytes).toString('base64')
|
||||
})
|
||||
this.#promptOutputTokens.set(
|
||||
promptKey,
|
||||
usedOutputTokens +
|
||||
Math.min(
|
||||
reservedOutputTokens,
|
||||
responseOutputTokens(bytes) ?? reservedOutputTokens
|
||||
)
|
||||
)
|
||||
this.#ledger.complete(callId)
|
||||
let finalized = false
|
||||
return {
|
||||
@@ -474,9 +451,6 @@ export class AgentModelGateway {
|
||||
}
|
||||
}
|
||||
|
||||
finalizePrompt(bindingId: string, operationId: string): void {
|
||||
this.#promptOutputTokens.delete(`${bindingId}\0${operationId}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function createAgentModelCallId(
|
||||
@@ -598,29 +572,6 @@ function prepareProviderRequest(
|
||||
}
|
||||
}
|
||||
|
||||
function responseOutputTokens(body: Uint8Array): number | undefined {
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(new TextDecoder().decode(body))
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return undefined
|
||||
}
|
||||
const usage = (value as { usage?: unknown }).usage
|
||||
if (usage === null || typeof usage !== 'object' || Array.isArray(usage)) {
|
||||
return undefined
|
||||
}
|
||||
const record = usage as Record<string, unknown>
|
||||
const tokens = record.output_tokens ?? record.completion_tokens
|
||||
return typeof tokens === 'number' &&
|
||||
Number.isSafeInteger(tokens) &&
|
||||
tokens >= 0
|
||||
? tokens
|
||||
: undefined
|
||||
}
|
||||
|
||||
function providerUrl(profile: AgentPromptModelProfile): URL {
|
||||
const url = new URL(profile.baseUrl)
|
||||
const path = url.pathname.replace(/\/+$/u, '')
|
||||
|
||||
@@ -254,6 +254,56 @@ describe('direct Linux stdio Runtime ownership', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('uses stdout backpressure instead of stopping when output queues fill', async () => {
|
||||
const registry = createRegistry()
|
||||
const child = fakeChild()
|
||||
const launched = identity()
|
||||
const sendSignal = vi.fn<typeof process.kill>(() => true)
|
||||
let releaseFirst!: () => void
|
||||
const firstConsumed = new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve
|
||||
})
|
||||
const owner = await launchDirectLinuxStdioProcessOwner({
|
||||
manifest: manifest(),
|
||||
profile: profile(),
|
||||
identity: { launchId: 'launch-output', processId: 'process-output' },
|
||||
installationId: 'installation-1',
|
||||
registry,
|
||||
deadlineAt: UNBOUNDED_REMOTE_PROMPT_DEADLINE,
|
||||
maximumInputBytes: 1024,
|
||||
maximumOutputQueueChunks: 1,
|
||||
platform: 'linux',
|
||||
spawn: () => {
|
||||
queueMicrotask(() => child.emit('spawn'))
|
||||
return child
|
||||
},
|
||||
randomOwnerToken: () => 'a'.repeat(32),
|
||||
readProcessIdentity: async () => launched,
|
||||
sendSignal
|
||||
})
|
||||
const pause = vi.spyOn(child.stdout, 'pause')
|
||||
const resume = vi.spyOn(child.stdout, 'resume')
|
||||
const received: string[] = []
|
||||
owner.subscribeOutput(async ({ data }) => {
|
||||
received.push(Buffer.from(data).toString())
|
||||
if (received.length === 1) {
|
||||
await firstConsumed
|
||||
}
|
||||
})
|
||||
|
||||
child.stdout.write('one')
|
||||
await vi.waitFor(() => expect(pause).toHaveBeenCalled())
|
||||
child.stdout.write('two')
|
||||
releaseFirst()
|
||||
await vi.waitFor(() => expect(received).toEqual(['one', 'two']))
|
||||
|
||||
expect(resume).toHaveBeenCalled()
|
||||
expect(sendSignal).not.toHaveBeenCalled()
|
||||
child.exitCode = 0
|
||||
child.emit('close', 0, null)
|
||||
registry.close()
|
||||
})
|
||||
|
||||
it('startup reconciles only owners from the current installation', async () => {
|
||||
const registry = createRegistry()
|
||||
const reservation = {
|
||||
|
||||
@@ -170,13 +170,13 @@ export class DirectLinuxStdioProcessOwner {
|
||||
#maximumPendingStdinBytes: number
|
||||
#inputBytes = 0
|
||||
#pendingInputBytes = 0
|
||||
#outputBytes = 0
|
||||
#queuedOutputBytes = 0
|
||||
#writeTail: Promise<void> = Promise.resolve()
|
||||
#deadlineTimer?: NodeJS.Timeout
|
||||
#promptGeneration = 1
|
||||
#promptActive = true
|
||||
#draining = false
|
||||
#outputPaused = false
|
||||
#closed = false
|
||||
#stdinClosed = false
|
||||
#stopPromise?: Promise<void>
|
||||
@@ -280,7 +280,6 @@ export class DirectLinuxStdioProcessOwner {
|
||||
this.#maximumPendingStdinBytes = this.#maximumInputBytes
|
||||
this.#inputBytes = 0
|
||||
this.#pendingInputBytes = 0
|
||||
this.#outputBytes = 0
|
||||
this.#promptActive = true
|
||||
this.#promptGeneration += 1
|
||||
this.#scheduleDeadline(
|
||||
@@ -449,24 +448,17 @@ export class DirectLinuxStdioProcessOwner {
|
||||
|
||||
#acceptOutput(chunk: Buffer | Uint8Array): void {
|
||||
if (this.#closed || chunk.byteLength === 0) return
|
||||
if (
|
||||
this.#outputBytes + chunk.byteLength > this.#maximumOutputBytes ||
|
||||
this.#outputQueue.length >= this.#maximumOutputQueueChunks ||
|
||||
this.#queuedOutputBytes + chunk.byteLength > this.#maximumOutputBytes
|
||||
) {
|
||||
this.#child.stdout.pause()
|
||||
this.#outputQueue.splice(0)
|
||||
this.#queuedOutputBytes = 0
|
||||
void this.stop({
|
||||
reason: 'output-quota',
|
||||
deadlineAt: new Date(this.#now() + DEFAULT_STOP_TIMEOUT_MS).toISOString()
|
||||
}).catch(() => undefined)
|
||||
return
|
||||
}
|
||||
const copy = Uint8Array.from(chunk)
|
||||
this.#outputBytes += copy.byteLength
|
||||
this.#queuedOutputBytes += copy.byteLength
|
||||
this.#outputQueue.push(copy)
|
||||
if (
|
||||
this.#outputQueue.length >= this.#maximumOutputQueueChunks ||
|
||||
(this.#maximumOutputBytes > 0 &&
|
||||
this.#queuedOutputBytes >= this.#maximumOutputBytes)
|
||||
) {
|
||||
this.#child.stdout.pause()
|
||||
this.#outputPaused = true
|
||||
}
|
||||
this.#scheduleDrain()
|
||||
}
|
||||
|
||||
@@ -484,6 +476,15 @@ export class DirectLinuxStdioProcessOwner {
|
||||
for (const listener of [...this.#outputListeners]) {
|
||||
await listener({ stream: 'stdout', data: data.slice() })
|
||||
}
|
||||
if (
|
||||
this.#outputPaused &&
|
||||
this.#outputQueue.length < this.#maximumOutputQueueChunks &&
|
||||
(this.#maximumOutputBytes === 0 ||
|
||||
this.#queuedOutputBytes < this.#maximumOutputBytes)
|
||||
) {
|
||||
this.#outputPaused = false
|
||||
this.#child.stdout.resume()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
void this.stop({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type { RemoteRuntimeBundleManifest } from '../shared/remote-runtime-launch-contracts'
|
||||
import type { ControllerLease } from './controller-registry'
|
||||
import { AgentModelGatewayError } from './agent-model-gateway'
|
||||
import { EventJournalCapacityError } from './event-journal'
|
||||
import type { ProtocolMethodContext } from './protocol-server'
|
||||
import type {
|
||||
ModelBridgeBrokerDispatch,
|
||||
@@ -43,8 +44,16 @@ class MemoryAcpJournal implements RuntimeAcpJournal {
|
||||
}> = []
|
||||
mainAckSequence = '0'
|
||||
failAppend = false
|
||||
capacityBlocked = false
|
||||
|
||||
appendAcpFrame(input: (typeof this.frames)[number]): { created: boolean } {
|
||||
if (this.capacityBlocked) {
|
||||
throw new EventJournalCapacityError(
|
||||
'journal capacity',
|
||||
'stream',
|
||||
false
|
||||
)
|
||||
}
|
||||
if (this.failAppend) {
|
||||
throw new Error('journal capacity')
|
||||
}
|
||||
@@ -415,8 +424,6 @@ function harness(input: {
|
||||
capabilities: { imageInput: false },
|
||||
limits: {
|
||||
maximumOutputTokens: 4_096,
|
||||
maximumModelCalls: 100,
|
||||
maximumTotalOutputTokens: 409_600,
|
||||
requestTimeoutMilliseconds: 60_000
|
||||
}
|
||||
},
|
||||
@@ -541,6 +548,52 @@ describe('RuntimeAcpBackend', () => {
|
||||
expect(fixture.blobSink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows more than 100 Agent-local model calls in one prompt', async () => {
|
||||
const dispatch = vi.fn(async () => ({
|
||||
response: {
|
||||
status: 200,
|
||||
headers: {},
|
||||
bodyBase64: Buffer.from('{}').toString('base64')
|
||||
},
|
||||
acknowledgeDelivery: vi.fn(async () => undefined),
|
||||
failDelivery: vi.fn()
|
||||
}))
|
||||
const fixture = harness({
|
||||
agentOwned: true,
|
||||
modelGateway: {
|
||||
dispatch,
|
||||
finalizePrompt: vi.fn()
|
||||
}
|
||||
})
|
||||
await open(fixture)
|
||||
await invoke(
|
||||
fixture,
|
||||
'runtime/preparePrompt',
|
||||
fixture.preparation()
|
||||
)
|
||||
|
||||
for (let index = 0; index < 101; index += 1) {
|
||||
const exchange = await fixture.bridgeDispatch!(
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/v1/responses',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
bodyBase64: Buffer.from('{}').toString('base64')
|
||||
},
|
||||
{
|
||||
requestId: `provider-request-${index}`,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
)
|
||||
if (!('acknowledgeDelivery' in exchange)) {
|
||||
throw new Error('Expected an acknowledged model exchange')
|
||||
}
|
||||
await exchange.acknowledgeDelivery()
|
||||
}
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(101)
|
||||
})
|
||||
|
||||
it('runs open, prepare, ACP delivery, durable output, cursors, and close', async () => {
|
||||
const fixture = harness()
|
||||
const opened = await invoke(
|
||||
@@ -1162,7 +1215,7 @@ describe('RuntimeAcpBackend', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('stops the process when its output quota is exceeded', async () => {
|
||||
it('does not stop the process when cumulative output exceeds the display budget', async () => {
|
||||
const fixture = harness({ maximumOutputBytes: 3 })
|
||||
await open(fixture)
|
||||
await invoke(
|
||||
@@ -1172,9 +1225,30 @@ describe('RuntimeAcpBackend', () => {
|
||||
)
|
||||
await expect(
|
||||
fixture.process.emit('stdout', 'four')
|
||||
).rejects.toMatchObject({ code: 'output-quota' })
|
||||
expect(fixture.process.stops).toEqual(['output-quota'])
|
||||
).resolves.toBeUndefined()
|
||||
expect(fixture.process.stops).toEqual([])
|
||||
expect(fixture.outputFrames).toEqual(['four'])
|
||||
})
|
||||
|
||||
it('waits for durable journal capacity instead of stopping Runtime output', async () => {
|
||||
const fixture = harness()
|
||||
await open(fixture)
|
||||
await invoke(
|
||||
fixture,
|
||||
'runtime/preparePrompt',
|
||||
fixture.preparation()
|
||||
)
|
||||
fixture.journal.capacityBlocked = true
|
||||
const output = fixture.process.emit('stdout', 'later')
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 10))
|
||||
expect(fixture.process.stops).toEqual([])
|
||||
expect(fixture.outputFrames).toEqual([])
|
||||
|
||||
fixture.journal.capacityBlocked = false
|
||||
await output
|
||||
|
||||
expect(fixture.process.stops).toEqual([])
|
||||
expect(fixture.outputFrames).toEqual(['later'])
|
||||
})
|
||||
|
||||
it('stops with output quota when the detached durable journal is exhausted', async () => {
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
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
|
||||
@@ -82,6 +83,7 @@ const DEFAULT_MAXIMUM_OPERATIONS_PER_BINDING = 1_000
|
||||
const DEFAULT_DISPOSE_TIMEOUT_MS = 10_000
|
||||
const OWNED_PROMPT_CANCEL_GRACE_MS = 1_000
|
||||
const OWNED_PROMPT_START_TIMEOUT_MS = 2 * 60_000
|
||||
const ACP_JOURNAL_RETRY_MILLISECONDS = 25
|
||||
|
||||
export type RuntimeAcpBackendErrorCode =
|
||||
| 'capacity'
|
||||
@@ -317,8 +319,6 @@ type BindingState = {
|
||||
activeOperationId?: string
|
||||
terminalState?: 'completed' | 'failed' | 'cancelled' | 'interrupted'
|
||||
inputBytes: number
|
||||
outputBytes: number
|
||||
maximumOutputBytes?: number
|
||||
nextOutputSequence: bigint
|
||||
pendingInput: number
|
||||
pendingOutput: number
|
||||
@@ -721,8 +721,6 @@ export class RuntimeAcpBackend {
|
||||
existing.stopRequested = false
|
||||
existing.terminalState = undefined
|
||||
existing.inputBytes = 0
|
||||
existing.outputBytes = 0
|
||||
existing.maximumOutputBytes = undefined
|
||||
existing.nextOutputSequence = 1n
|
||||
existing.pendingInput = 0
|
||||
existing.pendingOutput = 0
|
||||
@@ -774,7 +772,6 @@ export class RuntimeAcpBackend {
|
||||
stopRequested: false,
|
||||
operations: new Map(),
|
||||
inputBytes: 0,
|
||||
outputBytes: 0,
|
||||
nextOutputSequence: 1n,
|
||||
pendingInput: 0,
|
||||
pendingOutput: 0,
|
||||
@@ -1234,11 +1231,6 @@ export class RuntimeAcpBackend {
|
||||
}
|
||||
}
|
||||
binding.inputBytes = 0
|
||||
binding.outputBytes = 0
|
||||
binding.maximumOutputBytes = Math.min(
|
||||
preparation.budget.maximumOutputBytes,
|
||||
verified.manifest.limits.maximumPromptOutputBytes
|
||||
)
|
||||
if (!binding.operations.has(preparation.operationId)) {
|
||||
binding.operations.set(preparation.operationId, {
|
||||
preparationDigest,
|
||||
@@ -1308,10 +1300,6 @@ export class RuntimeAcpBackend {
|
||||
)
|
||||
}
|
||||
await this.#closeModelBridge(binding, true)
|
||||
this.#options.modelGateway?.finalizePrompt(
|
||||
binding.request.bindingId,
|
||||
operation.operationId
|
||||
)
|
||||
if (binding.poisoned) {
|
||||
throw new RuntimeAcpBackendError(
|
||||
'Model response delivery was not proven before prompt completion',
|
||||
@@ -1336,8 +1324,6 @@ export class RuntimeAcpBackend {
|
||||
prepared.completion = completion
|
||||
binding.activeOperationId = undefined
|
||||
binding.inputBytes = 0
|
||||
binding.outputBytes = 0
|
||||
binding.maximumOutputBytes = undefined
|
||||
if (binding.deadlineTimer !== undefined) {
|
||||
clearTimeout(binding.deadlineTimer)
|
||||
binding.deadlineTimer = undefined
|
||||
@@ -1527,10 +1513,6 @@ export class RuntimeAcpBackend {
|
||||
return
|
||||
}
|
||||
await this.#closeModelBridge(binding, true)
|
||||
this.#options.modelGateway?.finalizePrompt(
|
||||
binding.request.bindingId,
|
||||
operationId
|
||||
)
|
||||
try {
|
||||
await binding.process.completePrompt()
|
||||
} catch {
|
||||
@@ -1568,8 +1550,6 @@ export class RuntimeAcpBackend {
|
||||
prepared.modelProfile = undefined
|
||||
binding.activeOperationId = undefined
|
||||
binding.inputBytes = 0
|
||||
binding.outputBytes = 0
|
||||
binding.maximumOutputBytes = undefined
|
||||
if (binding.deadlineTimer !== undefined) {
|
||||
clearTimeout(binding.deadlineTimer)
|
||||
binding.deadlineTimer = undefined
|
||||
@@ -1673,12 +1653,6 @@ export class RuntimeAcpBackend {
|
||||
'closed'
|
||||
)
|
||||
}
|
||||
if (binding.nextModelRound >= profile.limits.maximumModelCalls) {
|
||||
throw new RuntimeAcpBackendError(
|
||||
'Prompt model call limit reached',
|
||||
'capacity'
|
||||
)
|
||||
}
|
||||
const roundIndex = binding.nextModelRound
|
||||
let exchanged
|
||||
try {
|
||||
@@ -1990,21 +1964,6 @@ export class RuntimeAcpBackend {
|
||||
)
|
||||
}
|
||||
this.#assertBeforeDeadline(binding)
|
||||
const budget = this.#activeBudget(binding)
|
||||
const maximumOutputBytes = Math.min(
|
||||
budget.maximumOutputBytes,
|
||||
binding.maximumOutputBytes ?? budget.maximumOutputBytes
|
||||
)
|
||||
if (binding.outputBytes + output.data.byteLength > maximumOutputBytes) {
|
||||
await this.#stopAndReconcile(binding, 'output-quota').catch(
|
||||
() => undefined
|
||||
)
|
||||
throw new RuntimeAcpBackendError(
|
||||
'Runtime output quota reached',
|
||||
'output-quota'
|
||||
)
|
||||
}
|
||||
binding.outputBytes += output.data.byteLength
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < output.data.byteLength;
|
||||
@@ -2017,14 +1976,31 @@ export class RuntimeAcpBackend {
|
||||
try {
|
||||
await this.#enqueueOutput(binding, payload, async () => {
|
||||
const sequence = binding.nextOutputSequence.toString()
|
||||
this.#options.journal.appendAcpFrame({
|
||||
controllerId: binding.controllerId,
|
||||
bindingId: binding.request.bindingId,
|
||||
channelEpoch: binding.openResult.channelEpoch,
|
||||
direction: 'runtime-to-main',
|
||||
sequence,
|
||||
payload
|
||||
})
|
||||
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(ACP_JOURNAL_RETRY_MILLISECONDS)
|
||||
}
|
||||
}
|
||||
binding.nextOutputSequence += 1n
|
||||
if (binding.transportState === 'live') {
|
||||
try {
|
||||
@@ -2287,12 +2263,6 @@ export class RuntimeAcpBackend {
|
||||
}
|
||||
|
||||
#finishProcess(binding: BindingState): void {
|
||||
if (binding.activeOperationId !== undefined) {
|
||||
this.#options.modelGateway?.finalizePrompt(
|
||||
binding.request.bindingId,
|
||||
binding.activeOperationId
|
||||
)
|
||||
}
|
||||
if (binding.process !== undefined) {
|
||||
this.#processIdentityBindings.delete(
|
||||
processIdentityKey(binding.process.identity)
|
||||
@@ -2750,6 +2720,13 @@ function processIdentityKey(identity: RuntimeAcpProcessIdentity): string {
|
||||
].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 {
|
||||
|
||||
Reference in New Issue
Block a user