fix(terminal): verify agent prompt submission (#14962)

This commit is contained in:
Jinwoo Hong
2026-08-16 23:34:18 -07:00
committed by GitHub
parent cc74436b3c
commit 77ef6bb9ee
13 changed files with 1557 additions and 59 deletions
@@ -0,0 +1,683 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AGENT_PROMPT_BRACKETED_PASTE_END } from '../../shared/agent-prompt-injection'
import { OrcaRuntimeService } from './orca-runtime'
import { makeStore } from './runtime-rpc-worktree-store-fixtures'
const WORKTREE_PATH = '/tmp/worktree-a'
vi.mock('../git/worktree', () => ({
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/tmp/worktree-a',
head: 'abc',
branch: 'feature/prompt-verification',
isBare: false,
isMainWorktree: false
}
]),
listWorktreesStrict: vi.fn().mockResolvedValue([
{
path: '/tmp/worktree-a',
head: 'abc',
branch: 'feature/prompt-verification',
isBare: false,
isMainWorktree: false
}
])
}))
async function createPromptRuntime(
onWrite: (runtime: OrcaRuntimeService, data: string, writeIndex: number) => void
): Promise<{ runtime: OrcaRuntimeService; handle: string; writes: string[] }> {
const runtime = new OrcaRuntimeService(makeStore() as never)
const writes: string[] = []
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-prompt' }),
write: (_ptyId, data) => {
writes.push(data)
onWrite(runtime, data, writes.length)
return true
},
kill: () => true,
getForegroundProcess: async () => null
})
const terminal = await runtime.createTerminal(`path:${WORKTREE_PATH}`, {
launchAgent: 'aider'
})
return { runtime, handle: terminal.handle, writes }
}
describe('agent prompt submission runtime', () => {
afterEach(() => vi.useRealTimers())
it('submits exactly once after an observed lifecycle transition', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('accepts a working-to-idle cycle completed before the first poll', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('reports redraw-only activity as stalled without retrying Enter', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data.includes(AGENT_PROMPT_BRACKETED_PASTE_END)) {
runtime.onPtyData('pty-prompt', '\x1b[2J\x1b[H review this', Date.now())
} else if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b[2J\x1b[H review this', Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_stalled')
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('reports a neutral title transition as stalled without retrying Enter', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;plain shell\x07', Date.now())
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
}
})
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_stalled')
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('does not send Enter after a permission state appears', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data.includes(AGENT_PROMPT_BRACKETED_PASTE_END)) {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_blocked')
await vi.runAllTimersAsync()
await rejected
expect(writes).not.toContain('\r')
})
it('does not paste into an existing permission prompt', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('does not paste into an output-only permission prompt', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
vi.setSystemTime(2_000)
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('does not paste into a coalesced live permission title', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"working","agentType":"aider"}\x07' +
'\x1b]0;Codex waiting for permission\x07',
Date.now()
)
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('does not paste when split status stripping completes a permission title', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
runtime.onPtyData(
'pty-prompt',
'\x1b]0;Codex waiting for permission\x1b]9999;{"state":"working","agentType":"aider"',
Date.now()
)
runtime.onPtyData('pty-prompt', '}\x07\x07', Date.now())
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_blocked')
await vi.runAllTimersAsync()
await rejected
expect(writes).toEqual([])
})
it('preserves hook permission after an earlier live idle title', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"waiting","agentType":"aider"}\x07',
Date.now()
)
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('does not block on permission text restored only as history', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
runtime.seedTerminalRestoreTail('pty-prompt', {
text: 'Permission required\r\nAllow once\r\nAllow always\r\nReject\r\n'
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('does not send Enter after output-only permission appears during settlement', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data.includes(AGENT_PROMPT_BRACKETED_PASTE_END)) {
vi.setSystemTime(2_000)
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
}
})
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_blocked')
await vi.runAllTimersAsync()
await rejected
expect(writes).not.toContain('\r')
})
it.each([
'\x1b]0;Codex waiting for permission\x07\x1b]0;Codex idle\x07',
'\x1b]9999;{"state":"working","agentType":"aider"}\x07' +
'\x1b]0;Codex waiting for permission\x07',
'\x1b]0;Codex waiting for permission\x07' +
'\x1b]9999;{"state":"working","agentType":"aider"}\x07'
])('does not send Enter after coalesced permission activity', async (output) => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data.includes(AGENT_PROMPT_BRACKETED_PASTE_END)) {
runtime.onPtyData('pty-prompt', output, Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_blocked')
await vi.runAllTimersAsync()
await rejected
expect(writes).not.toContain('\r')
})
it('stops a chunked paste when permission appears between chunks', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
let writeChecks = 0
const submission = runtime.sendTerminalAgentPrompt(handle, 'x'.repeat(20_000), {
beforeWrite: () => {
writeChecks += 1
if (writeChecks === 2) {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
}
}
})
await expect(submission).rejects.toThrow('agent_prompt_blocked')
expect(writes).toHaveLength(2)
expect(writes[1]).toBe(AGENT_PROMPT_BRACKETED_PASTE_END)
expect(writes).not.toContain('\r')
})
it('stops a chunked paste after transient output-only permission', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData('pty-prompt', 'initial output\n', Date.now())
let writeChecks = 0
const submission = runtime.sendTerminalAgentPrompt(handle, 'x'.repeat(20_000), {
beforeWrite: () => {
writeChecks += 1
if (writeChecks === 2) {
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
}
})
await expect(submission).rejects.toThrow('agent_prompt_blocked')
expect(writes).toHaveLength(2)
expect(writes[1]).toBe(AGENT_PROMPT_BRACKETED_PASTE_END)
expect(writes).not.toContain('\r')
})
it('prefers a later permission title over an earlier explicit idle status', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"done","agentType":"aider"}\x07',
Date.now()
)
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('prefers later explicit idle evidence over stale permission output', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
let handle = ''
const writes: string[] = []
const runtime = new OrcaRuntimeService(makeStore() as never, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey: 'prompt-pane',
terminalHandle: handle,
state: 'done',
prompt: '',
agentType: 'aider',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now()
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-prompt' }),
write: (_ptyId, data) => {
writes.push(data)
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
return true
},
kill: () => true,
getForegroundProcess: async () => null
})
handle = (await runtime.createTerminal(`path:${WORKTREE_PATH}`, { launchAgent: 'aider' }))
.handle
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n' +
'\x1b]0;Codex waiting for permission\x07',
Date.now()
)
vi.setSystemTime(2_000)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('prefers a later working title over an earlier explicit idle status', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"done","agentType":"aider"}\x07',
Date.now()
)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('does not treat an unchanged newer working status as submission evidence', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"working","agentType":"aider"}\x07',
Date.now()
)
}
})
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
vi.setSystemTime(2_000)
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"working","agentType":"aider"}\x07',
Date.now()
)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_stalled')
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('does not write Enter after the PTY generation changes during settlement', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('terminal_handle_stale')
await vi.advanceTimersByTimeAsync(0)
expect(writes.some((data) => data.includes(AGENT_PROMPT_BRACKETED_PASTE_END))).toBe(true)
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
runtime.getPtyOutputSequence('pty-prompt')
)
await vi.runAllTimersAsync()
await rejected
expect(writes).not.toContain('\r')
})
it('does not reuse explicit permission status across a provider generation reset', async () => {
vi.useFakeTimers()
const controller = new AbortController()
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'continued' },
0
)
runtime.onPtyData(
'pty-prompt',
'\x1b]9999;{"state":"waiting","agentType":"aider"}\x07',
Date.now()
)
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
0
)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this', {
signal: controller.signal
})
const rejected = expect(submission).rejects.toThrow('request_aborted')
await vi.advanceTimersByTimeAsync(0)
expect(writes.some((data) => data.includes(AGENT_PROMPT_BRACKETED_PASTE_END))).toBe(true)
controller.abort()
await vi.runAllTimersAsync()
await rejected
})
it('does not reuse output-only permission across a provider generation reset', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'continued' },
0
)
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
const sequenceAtSpawnStart = runtime.getPtyOutputSequence('pty-prompt')
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
sequenceAtSpawnStart
)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
await vi.runAllTimersAsync()
await expect(submission).resolves.toMatchObject({ accepted: true })
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('fails closed when new bytes race a reset after old permission output', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'continued' },
0
)
runtime.onPtyData(
'pty-prompt',
'Permission required\nAllow once\nAllow always\nReject\n',
Date.now()
)
const sequenceAtSpawnStart = runtime.getPtyOutputSequence('pty-prompt')
runtime.onPtyData('pty-prompt', 'replacement startup output\n', Date.now())
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
sequenceAtSpawnStart
)
await expect(runtime.sendTerminalAgentPrompt(handle, 'review this')).rejects.toThrow(
'agent_prompt_blocked'
)
expect(writes).toEqual([])
})
it('reports permission reached after the first Enter as blocked', async () => {
vi.useFakeTimers()
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex waiting for permission\x07', Date.now())
}
})
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this')
const rejected = expect(submission).rejects.toThrow('agent_prompt_blocked')
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
it('serializes concurrent prompt submissions within one PTY generation', async () => {
vi.useFakeTimers()
let enterCount = 0
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
enterCount += 1
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
runtime.onPtyData('pty-prompt', '\x1b]0;Codex idle\x07', Date.now())
}
})
const first = runtime.sendTerminalAgentPrompt(handle, 'first prompt')
const second = runtime.sendTerminalAgentPrompt(handle, 'second prompt')
await vi.runAllTimersAsync()
await Promise.all([first, second])
const firstPaste = writes.findIndex((data) => data.includes('first prompt'))
const firstEnter = writes.indexOf('\r', firstPaste + 1)
const secondPaste = writes.findIndex((data) => data.includes('second prompt'))
const secondEnter = writes.indexOf('\r', secondPaste + 1)
expect(firstPaste).toBeGreaterThanOrEqual(0)
expect(firstEnter).toBeGreaterThan(firstPaste)
expect(secondPaste).toBeGreaterThan(firstEnter)
expect(secondEnter).toBeGreaterThan(secondPaste)
expect(enterCount).toBe(2)
})
it('does not queue a replacement generation behind an obsolete submission', async () => {
vi.useFakeTimers()
let releaseFirst!: () => void
let firstWriteReached!: () => void
const firstWrite = new Promise<void>((resolve) => {
firstWriteReached = resolve
})
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve
})
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
if (data === '\r') {
runtime.onPtyData('pty-prompt', '\x1b]0;Codex working\x07', Date.now())
}
})
const first = runtime.sendTerminalAgentPrompt(handle, 'obsolete prompt', {
beforeWrite: async () => {
firstWriteReached()
await firstGate
}
})
await firstWrite
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
0
)
const replacement = runtime.sendTerminalAgentPrompt(handle, 'replacement prompt')
await vi.runAllTimersAsync()
await expect(replacement).resolves.toMatchObject({ accepted: true })
expect(writes.some((data) => data.includes('replacement prompt'))).toBe(true)
releaseFirst()
await expect(first).rejects.toThrow('terminal_handle_stale')
})
it('does not close a partial paste after the PTY generation changes', async () => {
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
let writeChecks = 0
const submission = runtime.sendTerminalAgentPrompt(handle, 'x'.repeat(20_000), {
beforeWrite: () => {
writeChecks += 1
if (writeChecks === 2) {
runtime.synchronizePtyOutputSequenceFromProvider(
'pty-prompt',
{ value: 0, generation: 'reset' },
runtime.getPtyOutputSequence('pty-prompt')
)
}
}
})
await expect(submission).rejects.toThrow('terminal_handle_stale')
expect(writes).toHaveLength(1)
expect(writes[0]).not.toContain(AGENT_PROMPT_BRACKETED_PASTE_END)
})
it('does not send delayed Enter after cancellation during settlement', async () => {
vi.useFakeTimers()
const controller = new AbortController()
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this', {
signal: controller.signal
})
const rejected = expect(submission).rejects.toThrow('request_aborted')
await vi.advanceTimersByTimeAsync(0)
controller.abort()
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(0)
})
it('does not send another Enter after cancellation during verification', async () => {
vi.useFakeTimers()
const controller = new AbortController()
const { runtime, handle, writes } = await createPromptRuntime(() => undefined)
const submission = runtime.sendTerminalAgentPrompt(handle, 'review this', {
signal: controller.signal
})
const rejected = expect(submission).rejects.toThrow('request_aborted')
await vi.advanceTimersByTimeAsync(500)
controller.abort()
await vi.runAllTimersAsync()
await rejected
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
})
})
@@ -0,0 +1,155 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
AGENT_PROMPT_EFFECT_TIMEOUT_MS,
type AgentPromptActivity,
verifyAgentPromptSubmission
} from './agent-prompt-submission-verification'
function activity(overrides: Partial<AgentPromptActivity> = {}): AgentPromptActivity {
return {
generation: 1,
permissionSequence: 2,
workingSequence: 4,
status: 'idle',
...overrides
}
}
describe('agent prompt submission verification', () => {
afterEach(() => vi.useRealTimers())
it('accepts an observed working transition', async () => {
vi.useFakeTimers()
let current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
current = activity({ workingSequence: 5, status: 'working' })
await vi.advanceTimersByTimeAsync(50)
await expect(verification).resolves.toBeUndefined()
})
it('accepts a completed lifecycle transition between polls', async () => {
vi.useFakeTimers()
let current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
current = activity({ workingSequence: 5 })
await vi.advanceTimersByTimeAsync(50)
await expect(verification).resolves.toBeUndefined()
})
it('does not accept an unrelated transition to a neutral title', async () => {
vi.useFakeTimers()
let current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
const rejected = expect(verification).rejects.toThrow('agent_prompt_stalled')
current = activity({ status: null })
await vi.advanceTimersByTimeAsync(AGENT_PROMPT_EFFECT_TIMEOUT_MS)
await rejected
})
it('reports stalled when no lifecycle transition occurs', async () => {
vi.useFakeTimers()
const current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
const rejected = expect(verification).rejects.toThrow('agent_prompt_stalled')
await vi.advanceTimersByTimeAsync(AGENT_PROMPT_EFFECT_TIMEOUT_MS)
await rejected
})
it('blocks when permission appears after submit', async () => {
vi.useFakeTimers()
let current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
const rejected = expect(verification).rejects.toThrow('agent_prompt_blocked')
current = activity({ status: 'permission' })
await vi.advanceTimersByTimeAsync(50)
await rejected
})
it('blocks when permission appears and clears between polls', async () => {
vi.useFakeTimers()
let current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
const rejected = expect(verification).rejects.toThrow('agent_prompt_blocked')
current = activity({ permissionSequence: 3 })
await vi.advanceTimersByTimeAsync(50)
await rejected
})
it('rejects an existing permission state', async () => {
const current = activity({ status: 'permission' })
await expect(
verifyAgentPromptSubmission({ baseline: current, readActivity: () => current })
).rejects.toThrow('agent_prompt_blocked')
})
it('does not accept an unchanged working baseline', async () => {
vi.useFakeTimers()
const current = activity({ status: 'working' })
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current
})
const rejected = expect(verification).rejects.toThrow('agent_prompt_stalled')
await vi.advanceTimersByTimeAsync(AGENT_PROMPT_EFFECT_TIMEOUT_MS)
await rejected
})
it('rejects a replaced terminal generation', async () => {
const baseline = activity()
await expect(
verifyAgentPromptSubmission({
baseline,
readActivity: () => activity({ generation: 2 })
})
).rejects.toThrow('terminal_handle_stale')
})
it('cancels while waiting for activity', async () => {
vi.useFakeTimers()
const controller = new AbortController()
const current = activity()
const verification = verifyAgentPromptSubmission({
baseline: current,
readActivity: () => current,
signal: controller.signal
})
controller.abort()
await expect(verification).rejects.toThrow('request_aborted')
})
})
@@ -0,0 +1,90 @@
export const AGENT_PROMPT_EFFECT_TIMEOUT_MS = 5_000
const AGENT_PROMPT_EFFECT_POLL_MS = 50
export type AgentPromptActivity = Readonly<{
generation: number
permissionSequence: number
workingSequence: number
status: 'working' | 'permission' | 'idle' | null
}>
type AgentPromptVerificationOptions = {
baseline: AgentPromptActivity
readActivity: () => AgentPromptActivity
signal?: AbortSignal
}
export async function verifyAgentPromptSubmission(
options: AgentPromptVerificationOptions
): Promise<void> {
throwIfAgentPromptAborted(options.signal)
assertPromptNotBlocked(options.baseline, options.baseline)
const deadline = Date.now() + AGENT_PROMPT_EFFECT_TIMEOUT_MS
while (Date.now() < deadline) {
const current = options.readActivity()
assertSamePromptGeneration(options.baseline, current)
assertPromptNotBlocked(options.baseline, current)
if (agentPromptLifecycleChanged(options.baseline, current)) {
return
}
await waitForAgentPromptPoll(options.signal)
}
const current = options.readActivity()
assertSamePromptGeneration(options.baseline, current)
assertPromptNotBlocked(options.baseline, current)
if (agentPromptLifecycleChanged(options.baseline, current)) {
return
}
throw new Error('agent_prompt_stalled')
}
function agentPromptLifecycleChanged(
baseline: AgentPromptActivity,
current: AgentPromptActivity
): boolean {
return current.workingSequence > baseline.workingSequence
}
function assertSamePromptGeneration(
baseline: AgentPromptActivity,
current: AgentPromptActivity
): void {
if (current.generation !== baseline.generation) {
throw new Error('terminal_handle_stale')
}
}
function assertPromptNotBlocked(baseline: AgentPromptActivity, current: AgentPromptActivity): void {
if (current.status === 'permission' || current.permissionSequence > baseline.permissionSequence) {
throw new Error('agent_prompt_blocked')
}
}
function throwIfAgentPromptAborted(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error('request_aborted')
}
}
async function waitForAgentPromptPoll(signal?: AbortSignal): Promise<void> {
if (!signal) {
await new Promise((resolve) => setTimeout(resolve, AGENT_PROMPT_EFFECT_POLL_MS))
return
}
await new Promise<void>((resolve, reject) => {
const onAbort = (): void => {
clearTimeout(timer)
reject(new Error('request_aborted'))
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, AGENT_PROMPT_EFFECT_POLL_MS)
signal.addEventListener('abort', onAbort, { once: true })
if (signal.aborted) {
onAbort()
}
})
}
+18
View File
@@ -166,6 +166,16 @@ function resetPlatform(): void {
}
}
function acknowledgeAgentPromptSubmit(
runtime: OrcaRuntimeService,
ptyId: string,
data: string
): void {
if (data === '\r') {
runtime.onPtyData(ptyId, '\x1b]0;Codex working\x07', Date.now())
}
}
const electronMocks = vi.hoisted(() => {
type Listener = (...args: unknown[]) => void
const listeners = new Map<string, Set<Listener>>()
@@ -16762,6 +16772,7 @@ describe('OrcaRuntimeService', () => {
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
write: (_ptyId, data) => {
writes.push(data)
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -16828,6 +16839,7 @@ describe('OrcaRuntimeService', () => {
} else {
prematureEnters += 1
}
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
}
return true
},
@@ -16876,6 +16888,7 @@ describe('OrcaRuntimeService', () => {
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
write: (_ptyId, data) => {
writes.push(data)
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -16913,6 +16926,7 @@ describe('OrcaRuntimeService', () => {
runtime.onPtyData('pty-bg', '\x1b[?25hcomposer rendered', Date.now())
}, 1_200)
}
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -16945,6 +16959,7 @@ describe('OrcaRuntimeService', () => {
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
write: (_ptyId, data) => {
writes.push(data)
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -16983,6 +16998,7 @@ describe('OrcaRuntimeService', () => {
runtime.onPtyData('pty-bg', 'final slow composer frame', Date.now())
}, 8_100)
}
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -17024,6 +17040,7 @@ describe('OrcaRuntimeService', () => {
)
}
}
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
@@ -17054,6 +17071,7 @@ describe('OrcaRuntimeService', () => {
spawn: vi.fn().mockResolvedValue({ id: 'pty-bg' }),
write: (_ptyId, data) => {
writes.push(data)
acknowledgeAgentPromptSubmit(runtime, 'pty-bg', data)
return true
},
kill: () => true,
+374 -30
View File
@@ -3,6 +3,7 @@
/* eslint-disable no-control-regex -- Why: terminal normalization must strip ANSI and OSC control sequences from PTY output before returning bounded text to agents. */
import {
detectAgentStatusFromTitle,
extractLastOscTitle,
isClaudeManagementTitle,
isCursorNativeAgentTitle,
isOpenCodeNativeTitle,
@@ -100,6 +101,10 @@ import {
AGENT_PROMPT_SUBMIT_DELAY_MS,
buildAgentPromptPasteBytes
} from '../../shared/agent-prompt-injection'
import {
type AgentPromptActivity,
verifyAgentPromptSubmission
} from './agent-prompt-submission-verification'
import {
awaitWindowsHostGitEnvironmentReady,
gitExecFileAsync,
@@ -1483,6 +1488,14 @@ type RuntimePtyWorktreeRecord = {
tailWaitState?: TerminalTailWaitState
}
type TerminalAgentStatusSnapshot = {
waitText: string
waitBlockedAt: number | null
title: string | null
titleStatus: AgentStatus | null
titleStatusIsLive: boolean
}
type TerminalCreateOptions = {
command?: string
claudeAgentTeamsSourceCommand?: string
@@ -1962,6 +1975,67 @@ const AGENT_PROMPT_RENDER_TIMEOUT_MS = 8000
const AGENT_PROMPT_RENDER_QUIET_MS = 1500
// Why: Claude and Codex emit show-cursor after accepting bracketed paste.
const AGENT_PROMPT_RENDER_MARKER = '\x1b[?25h'
function assertAgentPromptRequestActive(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error('request_aborted')
}
}
async function waitForAgentPromptPromise<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) {
return await promise
}
assertAgentPromptRequestActive(signal)
return await new Promise<T>((resolve, reject) => {
let settled = false
const finish = (result: { value: T } | { error: unknown }): void => {
if (settled) {
return
}
settled = true
signal.removeEventListener('abort', onAbort)
if ('error' in result) {
reject(result.error)
} else {
resolve(result.value)
}
}
const onAbort = (): void => finish({ error: new Error('request_aborted') })
signal.addEventListener('abort', onAbort, { once: true })
if (signal.aborted) {
onAbort()
return
}
promise.then(
(value) => finish({ value }),
(error: unknown) => finish({ error })
)
})
}
async function waitForAgentPromptDelay(delayMs: number, signal?: AbortSignal): Promise<void> {
if (!signal) {
await new Promise((resolve) => setTimeout(resolve, delayMs))
return
}
assertAgentPromptRequestActive(signal)
await new Promise<void>((resolve, reject) => {
const onAbort = (): void => {
clearTimeout(timer)
reject(new Error('request_aborted'))
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, delayMs)
signal.addEventListener('abort', onAbort, { once: true })
if (signal.aborted) {
onAbort()
}
})
}
const MOBILE_TERMINAL_SURFACE_TIMEOUT_MS = 10_000
// Why: the split already failed; the caller waits on this teardown only to learn whether the
// fallback kill is needed, so keep it short — an unreachable host must not stall the rejection.
@@ -3136,6 +3210,13 @@ export class OrcaRuntimeService {
private titleObservationSequence = 0
private headlessTerminals = new Map<string, RuntimeHeadlessTerminal>()
private ptyOutputSequenceById = new Map<string, number>()
private agentPromptLifecycleByPtyId = new Map<
string,
{ status: AgentStatus | null; workingSequence: number; updatedAt: number }
>()
private agentPromptPermissionSequenceByPtyId = new Map<string, number>()
private agentPromptExplicitStatusFloorByPtyId = new Map<string, number>()
private agentPromptSubmissionTailByPtyId = new Map<string, Promise<void>>()
private providerSequenceInitializedPtys = new Set<string>()
private providerSequenceOffsetByPtyId = new Map<string, number>()
private providerSnapshotPreferredPtys = new Set<string>()
@@ -10957,6 +11038,11 @@ export class OrcaRuntimeService {
// bell — the chunk's agentStatus:set events must reach the renderer
// before its pty:sideEffect batch.
retainedAgentStatusChanged = this.emitTerminalAgentStatusEvents(ptyId, agentStatusChunk)
const lastPayloadTitleOffset =
agentStatusChunk.lastPayloadCleanOffset === null
? null
: (previousTitleScanTail?.length ?? 0) + agentStatusChunk.lastPayloadCleanOffset
this.restoreAgentPromptLifecycleByteOrder(ptyId, titleInput, lastPayloadTitleOffset)
} finally {
// Why: flushed in the finally so a throwing tracker callback cannot
// strand this chunk's facts to be emitted under the next chunk's seq.
@@ -11052,6 +11138,7 @@ export class OrcaRuntimeService {
}
if (tailGainedNewerBlockedReason(previousWaitState, nextWaitState, state.appended)) {
pty.waitBlockedAt = at
this.recordAgentPromptPermissionObservation(ptyId)
}
state.lastAt = at
state.lastWaitState = nextWaitState
@@ -11518,6 +11605,7 @@ export class OrcaRuntimeService {
const identityOnlyTitle = this.isLiveCursorNativeTitle(rawTitle, meta)
const recordedTitle = identityOnlyTitle ? null : normalizedTitle
const agentStatus = identityOnlyTitle ? null : detectAgentStatusFromTitle(rawTitle)
this.recordAgentPromptLifecycleState(ptyId, agentStatus)
let ptyRecordChanged = false
const pty = this.ptysById.get(ptyId)
if (pty) {
@@ -11633,6 +11721,9 @@ export class OrcaRuntimeService {
this.oscTitleScanTailByPtyId.delete(ptyId)
this.osc7ScanTailByPtyId.delete(ptyId)
this.agentStatusOscProcessorsByPtyId.delete(ptyId)
this.agentPromptLifecycleByPtyId.delete(ptyId)
this.agentPromptPermissionSequenceByPtyId.delete(ptyId)
this.clearWaitBlockedCheckState(ptyId)
const pty = this.ptysById.get(ptyId)
if (pty) {
pty.lastOscTitle = null
@@ -11646,13 +11737,18 @@ export class OrcaRuntimeService {
pty.lastAgentStatusRichInvalidatedAtEpochMs = Date.now()
pty.managementTitle = null
pty.managementTitleAt = null
pty.waitBlockedAt = null
pty.tailWaitState = undefined
}
for (const leaf of this.getLeavesForPty(ptyId)) {
leaf.lastOscTitle = null
leaf.lastOscTitleAt = null
leaf.lastAgentStatus = null
leaf.lastAgentStatusObservedLive = false
leaf.waitBlockedAt = null
leaf.tailWaitState = undefined
}
this.primeWaitBlockedBaselineFromSeededTail(ptyId)
this.clearAgentRowSnapshotsForPty(ptyId)
}
@@ -11791,6 +11887,10 @@ export class OrcaRuntimeService {
}
let retainedChanged = false
for (const payload of chunk.payloads) {
this.recordAgentPromptLifecycleState(
ptyId,
mapExplicitAgentStateToRuntimeTerminalStatus(payload.state)
)
for (const target of targets.values()) {
retainedChanged =
this.retainAgentRowSnapshot(
@@ -11876,6 +11976,64 @@ export class OrcaRuntimeService {
return this.ptyOutputSequenceById.get(ptyId) ?? 0
}
private recordAgentPromptLifecycleState(ptyId: string, status: AgentStatus | null): void {
if (status === 'permission') {
this.recordAgentPromptPermissionObservation(ptyId)
}
const current = this.agentPromptLifecycleByPtyId.get(ptyId)
const updatedAt = Date.now()
if (!current) {
this.agentPromptLifecycleByPtyId.set(ptyId, {
status,
workingSequence: status === 'working' ? 1 : 0,
updatedAt
})
return
}
this.agentPromptLifecycleByPtyId.set(ptyId, {
status,
workingSequence:
current.workingSequence + (status === 'working' && current.status !== 'working' ? 1 : 0),
updatedAt
})
}
private recordAgentPromptPermissionObservation(ptyId: string): void {
this.agentPromptPermissionSequenceByPtyId.set(
ptyId,
(this.agentPromptPermissionSequenceByPtyId.get(ptyId) ?? 0) + 1
)
}
private restoreAgentPromptLifecycleByteOrder(
ptyId: string,
titleInput: string,
lastPayloadTitleOffset: number | null
): void {
if (lastPayloadTitleOffset === null) {
return
}
const titleRange = findLastCompleteOscTitleRange(titleInput)
if (!titleRange || titleRange.end <= lastPayloadTitleOffset) {
return
}
const title = extractLastOscTitle(titleInput)
if (title === null) {
return
}
const status = detectAgentStatusFromTitle(title)
const current = this.agentPromptLifecycleByPtyId.get(ptyId)
if (!current || current.status === status) {
return
}
this.agentPromptLifecycleByPtyId.set(ptyId, {
status,
workingSequence:
current.workingSequence + (status === 'working' && current.status !== 'working' ? 1 : 0),
updatedAt: Date.now()
})
}
private getPtyLifecycleGeneration(ptyId: string): number {
const existing = this.ptyLifecycleGenerationById.get(ptyId)
if (existing !== undefined) {
@@ -11888,6 +12046,9 @@ export class OrcaRuntimeService {
private advancePtyLifecycleGeneration(ptyId: string): void {
this.ptyLifecycleGenerationById.set(ptyId, this.nextPtyLifecycleGeneration++)
this.agentPromptLifecycleByPtyId.delete(ptyId)
this.agentPromptPermissionSequenceByPtyId.delete(ptyId)
this.agentPromptExplicitStatusFloorByPtyId.set(ptyId, Date.now())
this.legacyWorkerRecoveredPtys.delete(ptyId)
// Why: a respawn under the same session id needs its own subscriber-driven attach.
this.subscriberDrivenProviderAttachesByPtyId.delete(ptyId)
@@ -11940,6 +12101,7 @@ export class OrcaRuntimeService {
if (pty) {
pty.wslDistro = null
}
// Why: raced post-spawn bytes may already contain the replacement's permission state.
if (replacesExistingRuntimeGeneration && postSpawnSequence === 0) {
this.resetTrackedTerminalStateForProviderGeneration(ptyId)
}
@@ -14819,6 +14981,7 @@ export class OrcaRuntimeService {
this.providerBufferAcquisitionsByPtyId.delete(ptyId)
this.providerVisibleStateByPtyId.delete(ptyId)
this.providerVisibleRetryAtByPtyId.delete(ptyId)
this.agentPromptExplicitStatusFloorByPtyId.delete(ptyId)
this.agentStatusOscProcessorsByPtyId.delete(ptyId)
this.terminalSpawnCommandsByPtyId.delete(ptyId)
this.disposePtyTitleTracker(ptyId)
@@ -17950,17 +18113,33 @@ export class OrcaRuntimeService {
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
suffixFailureError?: string
signal?: AbortSignal
} = {}
): Promise<RuntimeTerminalSend> {
const payload = buildAgentPromptPasteBytes(prompt)
const bytesWritten = Buffer.byteLength(`${payload}${AGENT_PROMPT_SUBMIT}`, 'utf8')
const pty = this.getLivePtyForHandle(handle)
if (pty) {
if (!pty.pty.connected) {
throw new Error('terminal_not_writable')
}
await assertTerminalInputWithinLimitWithYield(payload)
await this.writeTerminalAgentPrompt(pty.pty.ptyId, payload, options)
const generation = this.getPtyLifecycleGeneration(pty.pty.ptyId)
const submits = await this.serializeAgentPromptSubmission(
pty.pty.ptyId,
generation,
async () => {
this.assertLiveTerminalHandleTargetsPty(handle, pty.pty.ptyId)
this.assertAgentPromptGeneration(pty.pty.ptyId, generation)
return await this.writeTerminalAgentPrompt(
handle,
pty.pty.ptyId,
generation,
payload,
options
)
}
)
const bytesWritten = Buffer.byteLength(payload, 'utf8') + submits
return { handle, accepted: true, bytesWritten }
}
@@ -17974,7 +18153,13 @@ export class OrcaRuntimeService {
if (await this.isLeafPtyProvenAbsent(leaf.ptyId)) {
throw new Error('terminal_not_writable')
}
await this.writeTerminalAgentPrompt(leaf.ptyId, payload, options)
const generation = this.getPtyLifecycleGeneration(leaf.ptyId)
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 bytesWritten = Buffer.byteLength(payload, 'utf8') + submits
return { handle, accepted: true, bytesWritten }
}
@@ -17982,21 +18167,10 @@ export class OrcaRuntimeService {
const ptyId = this.getTerminalAgentStatusPtyId(handle)
const terminal = this.getTerminalAgentStatusSnapshot(handle, ptyId)
const explicitStatus = this.getFreshExplicitAgentStatusForHandle(handle)
const blockedByWaitText = detectTerminalWaitBlockedReason(terminal.waitText)
const liveTitleClearsBlockedText =
terminal.titleStatusIsLive &&
terminal.titleStatus !== null &&
terminal.titleStatus !== 'permission' &&
!isOpenCodeNativeTitle(terminal.title)
if (terminal.titleStatus === 'permission' && terminal.titleStatusIsLive) {
return { handle, isRunningAgent: true, status: 'permission' }
}
const lifecycle = this.agentPromptLifecycleByPtyId.get(ptyId)
if (
blockedByWaitText &&
!liveTitleClearsBlockedText &&
(!explicitStatus ||
explicitStatus.status === 'permission' ||
(terminal.waitBlockedAt !== null && terminal.waitBlockedAt >= explicitStatus.updatedAt))
(terminal.titleStatus === 'permission' && terminal.titleStatusIsLive) ||
this.hasAuthoritativeTerminalWaitPermission(terminal, explicitStatus, lifecycle)
) {
return { handle, isRunningAgent: true, status: 'permission' }
}
@@ -18066,13 +18240,7 @@ export class OrcaRuntimeService {
private getTerminalAgentStatusSnapshot(
handle: string,
expectedPtyId: string
): {
waitText: string
waitBlockedAt: number | null
title: string | null
titleStatus: AgentStatus | null
titleStatusIsLive: boolean
} {
): TerminalAgentStatusSnapshot {
const pty = this.getLivePtyForHandle(handle)
if (pty) {
if (!pty.pty.connected || pty.pty.ptyId !== expectedPtyId) {
@@ -18131,6 +18299,36 @@ export class OrcaRuntimeService {
}
}
private hasAuthoritativeTerminalWaitPermission(
terminal: TerminalAgentStatusSnapshot,
explicitStatus: { status: AgentStatus; updatedAt: number } | null,
lifecycle: { status: AgentStatus | null; updatedAt: number } | null | undefined
): boolean {
const blockedByWaitText = detectTerminalWaitBlockedReason(terminal.waitText)
if (!blockedByWaitText) {
return false
}
const liveTitleClearsBlockedText =
terminal.titleStatusIsLive &&
terminal.titleStatus !== null &&
terminal.titleStatus !== 'permission' &&
!isOpenCodeNativeTitle(terminal.title)
if (liveTitleClearsBlockedText && lifecycle?.status !== terminal.titleStatus) {
return false
}
const newestPermissionAt = Math.max(
explicitStatus?.status === 'permission' ? explicitStatus.updatedAt : -1,
lifecycle?.status === 'permission' ? lifecycle.updatedAt : -1,
terminal.waitBlockedAt ?? -1
)
const newestClearAt = Math.max(
explicitStatus && explicitStatus.status !== 'permission' ? explicitStatus.updatedAt : -1,
lifecycle?.status && lifecycle.status !== 'permission' ? lifecycle.updatedAt : -1
)
// Equal wall-clock observations fail closed because their raw intra-chunk order is unknown.
return newestPermissionAt >= 0 && newestPermissionAt >= newestClearAt
}
private async terminalHasShellForegroundProcess(handle: string, ptyId: string): Promise<boolean> {
if (!this.ptyController) {
return false
@@ -18530,13 +18728,20 @@ export class OrcaRuntimeService {
}
private async writeTerminalAgentPrompt(
handle: string,
ptyId: string,
generation: number,
pastePayload: string,
options: {
beforeWrite?: (ptyId: string) => void | Promise<void>
suffixFailureError?: string
signal?: AbortSignal
} = {}
): Promise<void> {
): Promise<number> {
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
const permissionBaseline = this.getAgentPromptActivity(handle, ptyId)
this.assertAgentPromptPermissionSafe(permissionBaseline, permissionBaseline)
const renderGate = this.createAgentPromptRenderGate(ptyId)
let wrotePasteBytes = false
let completedPaste = false
@@ -18545,7 +18750,15 @@ export class OrcaRuntimeService {
let chunk = chunks.next()
while (!chunk.done) {
const nextChunk = chunks.next()
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
await options.beforeWrite?.(ptyId)
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
this.assertAgentPromptPermissionSafe(
permissionBaseline,
this.getAgentPromptActivity(handle, ptyId)
)
if (nextChunk.done) {
renderGate?.arm()
}
@@ -18561,7 +18774,11 @@ export class OrcaRuntimeService {
}
completedPaste = true
} catch (error) {
if (wrotePasteBytes && !completedPaste) {
if (
wrotePasteBytes &&
!completedPaste &&
this.getPtyLifecycleGeneration(ptyId) === generation
) {
this.ptyController?.write(ptyId, AGENT_PROMPT_BRACKETED_PASTE_END)
}
renderGate?.dispose()
@@ -18569,11 +18786,16 @@ export class OrcaRuntimeService {
}
if (renderGate) {
await renderGate.wait()
renderGate.dispose()
try {
await waitForAgentPromptPromise(renderGate.wait(), options.signal)
} finally {
renderGate.dispose()
}
} else {
await new Promise((resolve) => setTimeout(resolve, AGENT_PROMPT_SUBMIT_DELAY_MS))
await waitForAgentPromptDelay(AGENT_PROMPT_SUBMIT_DELAY_MS, options.signal)
}
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
try {
await options.beforeWrite?.(ptyId)
} catch (error) {
@@ -18582,10 +18804,93 @@ export class OrcaRuntimeService {
}
throw error
}
assertAgentPromptRequestActive(options.signal)
this.assertAgentPromptGeneration(ptyId, generation)
const baseline = this.getAgentPromptActivity(handle, ptyId)
this.assertAgentPromptPermissionSafe(permissionBaseline, baseline)
const suffixWrote = this.ptyController?.write(ptyId, AGENT_PROMPT_SUBMIT) ?? false
if (!suffixWrote) {
throw new Error(options.suffixFailureError ?? 'terminal_not_writable')
}
await verifyAgentPromptSubmission({
baseline,
readActivity: () => this.getAgentPromptActivity(handle, ptyId),
signal: options.signal
})
return 1
}
private async serializeAgentPromptSubmission<T>(
ptyId: string,
generation: number,
submit: () => Promise<T>
): Promise<T> {
const queueKey = `${ptyId}\u0000${generation}`
const previous = this.agentPromptSubmissionTailByPtyId.get(queueKey) ?? Promise.resolve()
const submission = previous.catch(() => undefined).then(submit)
const tail = submission.then(
() => undefined,
() => undefined
)
this.agentPromptSubmissionTailByPtyId.set(queueKey, tail)
try {
return await submission
} finally {
if (this.agentPromptSubmissionTailByPtyId.get(queueKey) === tail) {
this.agentPromptSubmissionTailByPtyId.delete(queueKey)
}
}
}
private getAgentPromptActivity(handle: string, ptyId: string): AgentPromptActivity {
this.assertLiveTerminalHandleTargetsPty(handle, ptyId)
const explicitCandidate = this.getFreshExplicitAgentStatusForHandle(handle)
const explicitFloor = this.agentPromptExplicitStatusFloorByPtyId.get(ptyId)
const explicit =
explicitCandidate &&
(explicitFloor === undefined || explicitCandidate.updatedAt > explicitFloor)
? explicitCandidate
: null
const lifecycle = this.agentPromptLifecycleByPtyId.get(ptyId)
const ptyStatus =
lifecycle || explicitFloor === undefined
? (this.ptysById.get(ptyId)?.lastAgentStatus ?? null)
: null
const lifecycleIsNewer =
lifecycle &&
(!explicit ||
lifecycle.updatedAt > explicit.updatedAt ||
(lifecycle.updatedAt === explicit.updatedAt && lifecycle.status === 'permission'))
const terminal = this.getTerminalAgentStatusSnapshot(handle, ptyId)
const status = this.hasAuthoritativeTerminalWaitPermission(terminal, explicit, lifecycle)
? 'permission'
: lifecycleIsNewer
? lifecycle.status
: (explicit?.status ?? ptyStatus ?? null)
return {
generation: this.getPtyLifecycleGeneration(ptyId),
permissionSequence: this.agentPromptPermissionSequenceByPtyId.get(ptyId) ?? 0,
workingSequence: lifecycle?.workingSequence ?? 0,
status
}
}
private assertAgentPromptPermissionSafe(
baseline: AgentPromptActivity,
current: AgentPromptActivity
): void {
if (
current.status === 'permission' ||
current.permissionSequence > baseline.permissionSequence
) {
throw new Error('agent_prompt_blocked')
}
}
private assertAgentPromptGeneration(ptyId: string, expected: number): void {
if (this.getPtyLifecycleGeneration(ptyId) !== expected) {
throw new Error('terminal_handle_stale')
}
}
private createAgentPromptRenderGate(ptyId: string): {
@@ -37277,7 +37582,7 @@ export class OrcaRuntimeService {
const WAIT_BLOCKED_CHECK_MIN_INTERVAL_MS = 50
// Why: chunks that could complete an actionable prompt bypass the throttle so blocked stamps stay immediate; scanned over the new chunk + short carry, never the whole window.
const WAIT_BLOCKED_KEYWORD_PATTERN =
/press enter|press t to trust|do you trust|trust this|trusted workspace|update available|choose working directory|codex just got an upgrade|hooks need review/
/press enter|press t to trust|do you trust|trust this|trusted workspace|permission required|requires permission|allow once|allow always|update available|choose working directory|codex just got an upgrade|hooks need review/
const WAIT_BLOCKED_KEYWORD_CARRY_CHARS = 31
const MAX_TAIL_LINES = 2000
const MAX_TAIL_CHARS = 256 * 1024
@@ -39360,6 +39665,45 @@ function isTerminalSendSettlementAgent(
return agent === 'claude' || agent === 'codex'
}
function findLastCompleteOscTitleRange(data: string): { start: number; end: number } | null {
// Why: one forward cursor keeps hostile unterminated OSC output linear-time.
let last: { start: number; end: number } | null = null
let searchFrom = 0
while (searchFrom < data.length) {
const start = data.indexOf('\x1b]', searchFrom)
if (start === -1) {
break
}
const command = data[start + 2]
if ((command !== '0' && command !== '1' && command !== '2') || data[start + 3] !== ';') {
searchFrom = start + 2
continue
}
let cursor = start + 4
for (; cursor < data.length; cursor += 1) {
if (data[cursor] === '\x07') {
last = { start, end: cursor + 1 }
searchFrom = cursor + 1
break
}
if (data[cursor] !== '\x1b') {
continue
}
if (data[cursor + 1] === '\\') {
last = { start, end: cursor + 2 }
searchFrom = cursor + 2
} else {
searchFrom = cursor
}
break
}
if (cursor === data.length) {
break
}
}
return last
}
function terminalTitleBlocksExplicitAgentStatus(title: string | null): boolean {
if (!title) {
return false
+10
View File
@@ -45,6 +45,16 @@ describe('mapRuntimeError', () => {
}
)
it.each(['agent_prompt_blocked', 'agent_prompt_stalled', 'request_aborted'])(
'preserves the agent prompt failure %s',
(code) => {
expect(mapRuntimeError('req_1', { runtimeId: 'runtime-1' }, new Error(code))).toMatchObject({
ok: false,
error: { code, message: code }
})
}
)
it.each([
'remote_update_manual_required',
'remote_update_not_available',
+3
View File
@@ -61,10 +61,13 @@ const RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'terminal_tab_close_timeout',
'terminal_tab_not_found',
'terminal_tab_pinned',
'agent_prompt_blocked',
'agent_prompt_stalled',
'no_active_terminal',
'repo_not_found',
'timeout',
'invalid_limit',
'request_aborted',
'remote_update_manual_required',
'remote_update_not_available',
'remote_update_not_downloaded',
+5 -2
View File
@@ -1226,7 +1226,7 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
defineMethod({
name: 'terminal.send',
params: TerminalSend,
handler: async (params, { runtime, clientId }) => {
handler: async (params, { runtime, clientId, signal }) => {
await assertTerminalSendTextWithinLimit(params.text)
await assertTerminalSendTextWithinLimit(params.resolvedLaunchDraft?.text)
const queryReplyClientId = clientId ?? params.client?.id
@@ -1374,7 +1374,10 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
let result
try {
result = useSettledAgentPrompt
? await runtime.sendTerminalAgentPrompt(params.terminal, params.text!, { beforeWrite })
? await runtime.sendTerminalAgentPrompt(params.terminal, params.text!, {
beforeWrite,
signal
})
: await runtime.sendTerminal(
params.terminal,
{
@@ -45,7 +45,8 @@ describe('terminal agent prompt send RPC', () => {
expect(response.ok).toBe(true)
expect(runtime.isTerminalRunningSettledPromptAgent).toHaveBeenCalledWith('terminal-1')
expect(sendTerminalAgentPrompt).toHaveBeenCalledWith('terminal-1', 'review this change', {
beforeWrite: undefined
beforeWrite: undefined,
signal: undefined
})
expect(sendTerminal).not.toHaveBeenCalled()
})
+2
View File
@@ -17,6 +17,7 @@ describe('createAgentStatusOscProcessor', () => {
agentType: 'codex'
}
])
expect(result.lastPayloadCleanOffset).toBe('before'.length)
})
it('preserves parser state across split OSC 9999 chunks', () => {
@@ -32,5 +33,6 @@ describe('createAgentStatusOscProcessor', () => {
prompt: 'ok'
}
])
expect(result.lastPayloadCleanOffset).toBe(0)
})
})
+4 -1
View File
@@ -6,6 +6,7 @@ const OSC_AGENT_STATUS_PREFIX = '\x1b]9999;'
export type ProcessedAgentStatusChunk = {
cleanData: string
payloads: ParsedAgentStatusPayload[]
lastPayloadCleanOffset: number | null
}
function findAgentStatusTerminator(
@@ -40,6 +41,7 @@ export function createAgentStatusOscProcessor(): (data: string) => ProcessedAgen
pending = ''
const payloads: ParsedAgentStatusPayload[] = []
let lastPayloadCleanOffset: number | null = null
let cleanData = ''
let cursor = 0
@@ -77,10 +79,11 @@ export function createAgentStatusOscProcessor(): (data: string) => ProcessedAgen
const parsed = parseAgentStatusPayload(combined.slice(payloadStart, terminator.index))
if (parsed) {
payloads.push(parsed)
lastPayloadCleanOffset = cleanData.length
}
cursor = terminator.index + terminator.length
}
return { cleanData, payloads }
return { cleanData, payloads, lastPayloadCleanOffset }
}
}
@@ -18,8 +18,8 @@ mkdirSync(fixtureBin)
writeFileSync(
fakeCodex,
process.platform === 'win32'
? `@echo off\r\n"${process.execPath}" "${fixtureScript}" --fake-agent --report "%ORCA_FAKE_AGENT_REPORT%" --marker "%ORCA_FAKE_AGENT_MARKER%" --allow-unframed-paste\r\n`
: `#!/usr/bin/env sh\nexec "${process.execPath}" "${fixtureScript}" --fake-agent --report "$ORCA_FAKE_AGENT_REPORT" --marker "$ORCA_FAKE_AGENT_MARKER"\n`,
? `@echo off\r\n"${process.execPath}" "${fixtureScript}" --fake-agent --report "%ORCA_FAKE_AGENT_REPORT%" --marker "%ORCA_FAKE_AGENT_MARKER%" --allow-unframed-paste %*\r\n`
: `#!/usr/bin/env sh\nexec "${process.execPath}" "${fixtureScript}" --fake-agent --report "$ORCA_FAKE_AGENT_REPORT" --marker "$ORCA_FAKE_AGENT_MARKER" "$@"\n`,
'utf8'
)
if (process.platform !== 'win32') {
@@ -27,6 +27,7 @@ if (process.platform !== 'win32') {
}
test.use({
seedTestRepo: false,
orcaAppExtraEnv: {
PATH: `${fixtureBin}${path.delimiter}${process.env.PATH ?? ''}`,
ORCA_FAKE_AGENT_REPORT: fixtureReport,
@@ -87,3 +88,104 @@ test('CLI text plus Enter waits for a slow agent composer before submitting', as
markerReceived: true
})
})
test('CLI reports a swallowed Enter without submitting a second Enter', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
test.setTimeout(90_000)
await waitForSessionReady(orcaPage)
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
const repoRoot = process.cwd()
let stdout = ''
try {
const result = await execFileAsync(
process.execPath,
[
path.join(repoRoot, 'tests', 'tools', 'repro-terminal-send-submit.mjs'),
'--cli',
path.join(repoRoot, 'config', 'scripts', 'orca-dev.mjs'),
'--worktree',
testRepoPath,
'--agent-command',
'codex --swallow-first-enter',
'--expect-stalled',
'--report',
fixtureReport,
'--marker',
fixtureMarker,
'--discard-report'
],
{
cwd: repoRoot,
env: { ...process.env, ORCA_DEV_USER_DATA_PATH: userDataDir },
timeout: 60_000
}
)
stdout = result.stdout
} catch (error) {
const failed = error as Error & { stdout?: string; stderr?: string }
throw new Error([failed.message, failed.stdout, failed.stderr].filter(Boolean).join('\n'))
}
expect(JSON.parse(stdout)).toMatchObject({
rescueSent: false,
sendErrorCode: 'agent_prompt_stalled',
contractOk: true,
submitted: false,
prematureEnters: 0,
receivedEnters: 1,
swallowedEnters: 1,
markerReceived: true
})
})
test('CLI does not write prompt bytes into an active permission dialog', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
test.setTimeout(90_000)
await waitForSessionReady(orcaPage)
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
const repoRoot = process.cwd()
let stdout = ''
try {
const result = await execFileAsync(
process.execPath,
[
path.join(repoRoot, 'tests', 'tools', 'repro-terminal-send-submit.mjs'),
'--cli',
path.join(repoRoot, 'config', 'scripts', 'orca-dev.mjs'),
'--worktree',
testRepoPath,
'--expect-blocked',
'--report',
fixtureReport,
'--marker',
fixtureMarker,
'--discard-report'
],
{
cwd: repoRoot,
env: { ...process.env, ORCA_DEV_USER_DATA_PATH: userDataDir },
timeout: 60_000
}
)
stdout = result.stdout
} catch (error) {
const failed = error as Error & { stdout?: string; stderr?: string }
throw new Error([failed.message, failed.stdout, failed.stderr].filter(Boolean).join('\n'))
}
expect(JSON.parse(stdout)).toMatchObject({
rescueSent: false,
sendErrorCode: 'agent_prompt_blocked',
contractOk: true,
submitted: false,
receivedBytes: 0,
receivedEnters: 0,
markerReceived: false
})
})
+107 -23
View File
@@ -66,7 +66,7 @@ function runCommand(command, args, options = {}) {
reject(
Object.assign(
new Error(`${command} ${args.join(' ')} exited ${code}${detail ? `: ${detail}` : ''}`),
{ stderr }
{ stdout, stderr }
)
)
})
@@ -76,10 +76,24 @@ function runCommand(command, args, options = {}) {
async function callOrca(cli, args, cwd) {
const command = cli.endsWith('.mjs') ? process.execPath : cli
const prefixArgs = cli.endsWith('.mjs') ? [cli] : []
const { stdout } = await runCommand(command, [...prefixArgs, ...args, '--json'], { cwd })
let stdout
try {
const result = await runCommand(command, [...prefixArgs, ...args, '--json'], { cwd })
stdout = result.stdout
} catch (error) {
const parsed = JSON.parse(error?.stdout?.trim() ?? 'null')
if (parsed?.ok === false) {
throw Object.assign(new Error(parsed.error?.message ?? JSON.stringify(parsed.error)), {
code: parsed.error?.code
})
}
throw error
}
const parsed = JSON.parse(stdout.trim())
if (parsed.ok === false) {
throw new Error(parsed.error?.message ?? JSON.stringify(parsed.error))
throw Object.assign(new Error(parsed.error?.message ?? JSON.stringify(parsed.error)), {
code: parsed.error?.code
})
}
return parsed.result ?? parsed
}
@@ -146,7 +160,10 @@ async function parentMain() {
const reportPath = path.resolve(argValue('report', path.join(tempDir, 'report.json')))
const marker = argValue('marker', `ORCA_TERMINAL_SEND_${process.pid}_${Date.now()}`)
const prompt = `${marker} ${'slow composer payload '.repeat(24)}`
const expectStalled = hasFlag('expect-stalled')
const expectBlocked = hasFlag('expect-blocked')
await mkdir(tempDir, { recursive: true })
await rm(reportPath, { force: true })
const command =
argValue('agent-command') ??
@@ -158,6 +175,7 @@ async function parentMain() {
shellQuote(marker),
'--timeout-ms',
String(timeoutMs),
...(expectBlocked ? ['--permission-before-send'] : []),
...(process.platform === 'win32' ? ['--allow-unframed-paste'] : [])
]))
const added = await callOrca(cli, ['repo', 'add', '--path', cwd], cwd)
@@ -186,19 +204,37 @@ async function parentMain() {
}
try {
await callOrca(
cli,
['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', '10000'],
cwd
)
await callOrca(
cli,
['terminal', 'send', '--terminal', handle, '--text', prompt, '--enter'],
cwd
)
if (expectBlocked) {
const setupReport = await readReport(reportPath, 10_000)
if (!setupReport) {
throw new Error('terminal permission prompt did not materialize')
}
} else {
await callOrca(
cli,
['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', '10000'],
cwd
)
}
let sendErrorCode = null
try {
await callOrca(
cli,
['terminal', 'send', '--terminal', handle, '--text', prompt, '--enter'],
cwd
)
} catch (error) {
const expectedError =
(expectStalled && error?.code === 'agent_prompt_stalled') ||
(expectBlocked && error?.code === 'agent_prompt_blocked')
if (!expectedError) {
throw error
}
sendErrorCode = error.code
}
let report = await readReport(reportPath, 1_000)
let rescueSent = false
if (!report) {
if (!report && !expectStalled && !expectBlocked) {
rescueSent = true
await callOrca(cli, ['terminal', 'send', '--terminal', handle, '--enter'], cwd)
report = await readReport(reportPath, timeoutMs)
@@ -210,10 +246,25 @@ async function parentMain() {
handle,
promptBytes: Buffer.byteLength(prompt, 'utf8'),
rescueSent,
sendErrorCode,
...report
}
console.log(JSON.stringify(summary, null, 2))
if (!report.contractOk || rescueSent) {
const expectedStallObserved =
sendErrorCode === 'agent_prompt_stalled' &&
report.submitted === false &&
report.receivedEnters === 1 &&
report.swallowedEnters === 1
const expectedBlockObserved =
sendErrorCode === 'agent_prompt_blocked' &&
report.receivedBytes === 0 &&
report.receivedEnters === 0
if (
!report.contractOk ||
rescueSent ||
(expectStalled && !expectedStallObserved) ||
(expectBlocked && !expectedBlockObserved)
) {
process.exitCode = 1
}
} finally {
@@ -231,6 +282,8 @@ async function fakeAgentMain() {
const marker = argValue('marker')
const timeoutMs = parsePositiveInteger('timeout-ms', DEFAULT_TIMEOUT_MS)
const pasteFramingRequired = !hasFlag('allow-unframed-paste')
const swallowFirstEnter = hasFlag('swallow-first-enter')
const permissionBeforeSend = hasFlag('permission-before-send')
if (!reportPath || !marker) {
throw new Error('--fake-agent requires --report and --marker')
}
@@ -239,49 +292,80 @@ async function fakeAgentMain() {
}
process.stdin.resume()
process.stdout.write('OpenAI Codex\nmodel: fake\ndirectory: fixture\n> ')
if (permissionBeforeSend) {
process.stdout.write('\nPermission required\nAllow once\nAllow always\nReject\n')
}
let input = ''
let countedCarriages = 0
let prematureEnters = 0
let receivedEnters = 0
let swallowedEnters = 0
let composerReady = false
let renderScheduled = false
let finished = false
const finish = async () => {
if (finished) {
return
}
finished = true
const writeReport = async (submitted) => {
const hasBracketedPasteFrame = input.includes(BEGIN) && input.includes(END)
const report = {
contractOk: prematureEnters === 0 && (!pasteFramingRequired || hasBracketedPasteFrame),
submitted: true,
contractOk:
prematureEnters === 0 &&
(permissionBeforeSend || !pasteFramingRequired || hasBracketedPasteFrame),
submitted,
prematureEnters,
receivedEnters,
swallowedEnters,
pasteFramingRequired,
hasBracketedPasteFrame,
markerReceived: input.includes(marker),
receivedBytes: Buffer.byteLength(input, 'utf8')
}
await writeFile(reportPath, JSON.stringify(report, null, 2))
return report
}
const finish = async () => {
if (finished) {
return
}
finished = true
const report = await writeReport(true)
process.stdout.write(`\nORCA_TERMINAL_SEND_REPORT ${report.contractOk ? 'ok' : 'rescued'}\n`)
process.exit(report.contractOk ? 0 : 7)
}
const timeout = setTimeout(() => process.exit(8), timeoutMs)
if (permissionBeforeSend) {
setTimeout(() => void writeReport(false), 250)
}
process.stdin.on('data', (chunk) => {
input += chunk.toString('utf8')
if (!renderScheduled && input.includes(marker)) {
renderScheduled = true
setTimeout(() => {
composerReady = true
process.stdout.write('\x1b[?25hcomposer rendered')
const pasteStart = input.indexOf(BEGIN)
const pasteEnd = input.indexOf(END, pasteStart + BEGIN.length)
const composer =
pasteStart !== -1 && pasteEnd !== -1
? input.slice(pasteStart + BEGIN.length, pasteEnd)
: input
process.stdout.write(`\x1b[?25h\x1b[2J\x1b[H ${composer}`)
}, COMPOSER_RENDER_MS)
}
let nextCarriage = input.indexOf('\r', countedCarriages)
while (nextCarriage !== -1) {
countedCarriages = nextCarriage + 1
if (composerReady) {
receivedEnters += 1
if (swallowFirstEnter && swallowedEnters === 0) {
swallowedEnters += 1
void writeReport(false)
nextCarriage = input.indexOf('\r', countedCarriages)
continue
}
clearTimeout(timeout)
process.stdout.write('\x1b]0;Codex working\x07')
void finish()
return
}