fix(terminal): bind settled prompt proof to send

This commit is contained in:
Jinwoo-H
2026-08-20 05:40:01 -04:00
parent 3fc4cf6da6
commit de4463b977
2 changed files with 114 additions and 51 deletions
+84 -29
View File
@@ -18566,6 +18566,7 @@ export class OrcaRuntimeService {
},
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
assertWriteAuthority?: (ptyId: string) => void
reserveWrite?: (ptyId: string) => void
afterWrite?: (ptyId: string) => void | Promise<void>
suffixFailureError?: string
@@ -18620,8 +18621,10 @@ export class OrcaRuntimeService {
prompt: string,
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
assertWriteAuthority?: (ptyId: string) => void
suffixFailureError?: string
signal?: AbortSignal
requireSettledForeground?: boolean
} = {}
): Promise<RuntimeTerminalSend> {
const payload = buildAgentPromptPasteBytes(prompt)
@@ -18638,15 +18641,22 @@ export class OrcaRuntimeService {
async () => {
this.assertLiveTerminalHandleTargetsPty(handle, pty.pty.ptyId)
this.assertAgentPromptGeneration(pty.pty.ptyId, generation)
return await this.writeTerminalAgentPrompt(
handle,
pty.pty.ptyId,
generation,
payload,
options
)
// Why: classify inside the PTY queue so concurrent sends cannot fan out or reuse a stale foreground proof.
const settlementAgent = options.requireSettledForeground
? await this.recognizeSettledPromptAgent(handle, pty.pty.ptyId, options.signal)
: undefined
if (options.requireSettledForeground && !settlementAgent) {
return null
}
return await this.writeTerminalAgentPrompt(handle, pty.pty.ptyId, generation, payload, {
...options,
settlementAgent: settlementAgent ?? undefined
})
}
)
if (submits === null) {
return { handle, accepted: false, bytesWritten: 0 }
}
const bytesWritten = Buffer.byteLength(payload, 'utf8') + submits
return { handle, accepted: true, bytesWritten }
}
@@ -18665,8 +18675,20 @@ export class OrcaRuntimeService {
const submits = await this.serializeAgentPromptSubmission(leaf.ptyId, generation, async () => {
this.assertLiveTerminalHandleTargetsPty(handle, leaf.ptyId!)
this.assertAgentPromptGeneration(leaf.ptyId!, generation)
return await this.writeTerminalAgentPrompt(handle, leaf.ptyId!, generation, payload, options)
const settlementAgent = options.requireSettledForeground
? await this.recognizeSettledPromptAgent(handle, leaf.ptyId!, options.signal)
: undefined
if (options.requireSettledForeground && !settlementAgent) {
return null
}
return await this.writeTerminalAgentPrompt(handle, leaf.ptyId!, generation, payload, {
...options,
settlementAgent: settlementAgent ?? undefined
})
})
if (submits === null) {
return { handle, accepted: false, bytesWritten: 0 }
}
const bytesWritten = Buffer.byteLength(payload, 'utf8') + submits
return { handle, accepted: true, bytesWritten }
}
@@ -19262,6 +19284,7 @@ export class OrcaRuntimeService {
payload: string,
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
assertWriteAuthority?: (ptyId: string) => void
reserveWrite?: (ptyId: string) => void
afterWrite?: (ptyId: string) => void | Promise<void>
suffixFailureError?: string
@@ -19281,6 +19304,7 @@ export class OrcaRuntimeService {
}
try {
await options.beforeWrite?.(ptyId)
options.assertWriteAuthority?.(ptyId)
options.reserveWrite?.(ptyId)
} catch (error) {
if (options.suffixFailureError) {
@@ -19300,6 +19324,7 @@ export class OrcaRuntimeService {
}
await options.beforeWrite?.(ptyId)
options.assertWriteAuthority?.(ptyId)
options.reserveWrite?.(ptyId)
const wrote = this.ptyController?.write(ptyId, payload) ?? false
if (!wrote) {
@@ -19313,6 +19338,7 @@ export class OrcaRuntimeService {
text: string,
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
assertWriteAuthority?: (ptyId: string) => void
reserveWrite?: (ptyId: string) => void
afterWrite?: (ptyId: string) => void | Promise<void>
} = {}
@@ -19321,6 +19347,7 @@ export class OrcaRuntimeService {
let chunk = chunks.next()
while (!chunk.done) {
await options.beforeWrite?.(ptyId)
options.assertWriteAuthority?.(ptyId)
options.reserveWrite?.(ptyId)
const wrote = this.ptyController?.write(ptyId, chunk.value) ?? false
if (!wrote) {
@@ -19341,15 +19368,17 @@ export class OrcaRuntimeService {
pastePayload: string,
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
assertWriteAuthority?: (ptyId: string) => void
suffixFailureError?: string
signal?: AbortSignal
settlementAgent?: TuiAgent
} = {}
): Promise<number> {
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
const permissionBaseline = this.getAgentPromptActivity(handle, ptyId)
this.assertAgentPromptPermissionSafe(permissionBaseline, permissionBaseline)
const renderGate = this.createAgentPromptRenderGate(ptyId)
const renderGate = this.createAgentPromptRenderGate(ptyId, options.settlementAgent)
let wrotePasteBytes = false
let completedPaste = false
try {
@@ -19366,6 +19395,8 @@ export class OrcaRuntimeService {
permissionBaseline,
this.getAgentPromptActivity(handle, ptyId)
)
// Why: awaited guards can yield; ownership must be checked in the same turn as the PTY write.
options.assertWriteAuthority?.(ptyId)
if (nextChunk.done) {
renderGate?.arm()
}
@@ -19415,6 +19446,7 @@ export class OrcaRuntimeService {
this.assertAgentPromptGeneration(ptyId, generation)
const baseline = this.getAgentPromptActivity(handle, ptyId)
this.assertAgentPromptPermissionSafe(permissionBaseline, baseline)
options.assertWriteAuthority?.(ptyId)
const suffixWrote = this.ptyController?.write(ptyId, AGENT_PROMPT_SUBMIT) ?? false
if (!suffixWrote) {
throw new Error(options.suffixFailureError ?? 'terminal_not_writable')
@@ -19500,13 +19532,16 @@ export class OrcaRuntimeService {
}
}
private createAgentPromptRenderGate(ptyId: string): {
private createAgentPromptRenderGate(
ptyId: string,
provenAgent?: TuiAgent
): {
arm: () => void
wait: () => Promise<void>
dispose: () => void
} | null {
const pty = this.ptysById.get(ptyId)
const agent = pty?.foregroundAgent ?? pty?.launchAgent
const agent = provenAgent ?? pty?.launchAgent ?? pty?.foregroundAgent
if (!isTerminalSendSettlementAgent(agent)) {
return null
}
@@ -34634,31 +34669,50 @@ export class OrcaRuntimeService {
}
}
async isTerminalRunningSettledPromptAgent(handle: string): Promise<boolean> {
async isTerminalRunningSettledPromptAgent(
handle: string,
options: { signal?: AbortSignal } = {}
): Promise<boolean> {
try {
const livePty = this.getLivePtyForHandle(handle)
const leaf = livePty ? null : this.getLiveLeafForHandle(handle).leaf
const ptyId = livePty?.pty.ptyId ?? leaf?.ptyId ?? null
const trackedPty = livePty?.pty ?? (ptyId ? this.ptysById.get(ptyId) : null)
if (!ptyId || !trackedPty || !this.ptyController) {
if (!ptyId) {
return false
}
const foregroundProcess = await this.ptyController.getForegroundProcess(ptyId)
const recognized = await this.recognizeForegroundAgentProcess(ptyId, foregroundProcess)
const recognizedAgent = recognized?.agent
if (!isTerminalSendSettlementAgent(recognizedAgent)) {
return false
}
if (!(await this.isTerminalRunningAgent(handle, { retryForegroundWrappers: false }))) {
return false
}
trackedPty.foregroundAgent = recognizedAgent
return true
return (await this.recognizeSettledPromptAgent(handle, ptyId, options.signal)) !== null
} catch {
assertAgentPromptRequestActive(options.signal)
return false
}
}
private async recognizeSettledPromptAgent(
handle: string,
ptyId: string,
signal?: AbortSignal
): Promise<TuiAgent | null> {
if (!this.ptyController) {
return null
}
assertAgentPromptRequestActive(signal)
const foregroundProcess = await this.ptyController.getForegroundProcess(ptyId)
const recognized = await this.recognizeForegroundAgentProcess(ptyId, foregroundProcess, {
signal
})
if (!isTerminalSendSettlementAgent(recognized?.agent)) {
return null
}
assertAgentPromptRequestActive(signal)
if (!(await this.isTerminalRunningAgent(handle, { retryForegroundWrappers: false }))) {
return null
}
assertAgentPromptRequestActive(signal)
const confirmed = recognizeAgentProcess(await this.ptyController.getForegroundProcess(ptyId))
assertAgentPromptRequestActive(signal)
return confirmed?.agent === recognized.agent ? recognized.agent : null
}
private async isPtyRunningAgent(
pty: RuntimePtyWorktreeRecord,
leaf: RuntimeLeafRecord | null = null,
@@ -34737,8 +34791,9 @@ export class OrcaRuntimeService {
private async recognizeForegroundAgentProcess(
ptyId: string,
foregroundProcess: string | null,
options: { suppressClaude?: boolean; retryWrappers?: boolean } = {}
options: { suppressClaude?: boolean; retryWrappers?: boolean; signal?: AbortSignal } = {}
): Promise<RecognizedAgentProcess | null> {
assertAgentPromptRequestActive(options.signal)
const initialRecognition = recognizeAgentProcess(foregroundProcess)
if (initialRecognition !== null) {
return options.suppressClaude === true &&
@@ -34755,10 +34810,10 @@ export class OrcaRuntimeService {
}
const startedAt = Date.now()
while (Date.now() - startedAt < FOREGROUND_AGENT_WRAPPER_RETRY_TIMEOUT_MS) {
await new Promise((resolve) =>
setTimeout(resolve, FOREGROUND_AGENT_WRAPPER_RETRY_INTERVAL_MS)
)
await waitForAgentPromptDelay(FOREGROUND_AGENT_WRAPPER_RETRY_INTERVAL_MS, options.signal)
assertAgentPromptRequestActive(options.signal)
const refreshedProcess = await this.ptyController.getForegroundProcess(ptyId)
assertAgentPromptRequestActive(options.signal)
const refreshedRecognition = recognizeAgentProcess(refreshedProcess)
if (refreshedRecognition !== null) {
return options.suppressClaude === true &&
+30 -22
View File
@@ -1405,20 +1405,22 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
const assertPromptInputAuthority =
params.agentPrompt === true && params.client?.type === 'desktop'
? (ptyId?: string): void => {
if (signal?.aborted) {
throw new Error('request_aborted')
}
assertTerminalSendExactPtyBinding(runtime, params.terminal, ptyId)
if (ptyId && isTerminalInputLockedForClient(runtime, ptyId, params.client)) {
throw new Error('terminal_guard_not_writable')
}
}
: undefined
const beforeWrite = assertSendPreconditions ?? assertPromptInputAuthority
const beforeWrite = assertSendPreconditions
const useSettledAgentPrompt =
params.agentPrompt === true &&
hasText &&
params.enter === true &&
params.interrupt !== true &&
params.client?.type === 'desktop' &&
(await runtime.isTerminalRunningSettledPromptAgent(params.terminal))
params.client?.type === 'desktop'
const reserveWrite =
params.inputKind !== 'query-reply' && leaf?.ptyId && mobileFloorClientId
? (ptyId: string): void => {
@@ -1431,26 +1433,32 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
: undefined
let result
try {
result = useSettledAgentPrompt
? await runtime.sendTerminalAgentPrompt(params.terminal, params.text!, {
if (useSettledAgentPrompt) {
result = await runtime.sendTerminalAgentPrompt(params.terminal, params.text!, {
beforeWrite,
assertWriteAuthority: assertPromptInputAuthority,
signal,
requireSettledForeground: true
})
}
if (!useSettledAgentPrompt || result?.accepted !== true) {
result = await runtime.sendTerminal(
params.terminal,
{
text: params.text,
enter: params.enter === true,
interrupt: params.interrupt === true
},
{
beforeWrite,
signal
})
: await runtime.sendTerminal(
params.terminal,
{
text: params.text,
enter: params.enter === true,
interrupt: params.interrupt === true
},
{
beforeWrite,
...(reserveWrite ? { reserveWrite } : {}),
...(params.inputKind !== 'query-reply' && mobileFloorClientId
? { afterWrite: () => commitMobileInputFloorClaim(mobileFloorClaim) }
: {})
}
)
assertWriteAuthority: assertPromptInputAuthority,
...(reserveWrite ? { reserveWrite } : {}),
...(params.inputKind !== 'query-reply' && mobileFloorClientId
? { afterWrite: () => commitMobileInputFloorClaim(mobileFloorClaim) }
: {})
}
)
}
} catch (error) {
mobileFloorClaim.current?.rollback()
const refusedReason = getTerminalSendGuardRefusedReason(error)