mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
fix(terminal): fence prompt retry state
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
AGENT_PROMPT_BRACKETED_PASTE_END,
|
||||
AGENT_PROMPT_SUBMIT_DELAY_MS
|
||||
} from '../../shared/agent-prompt-injection'
|
||||
import { AGENT_PROMPT_EFFECT_TIMEOUT_MS } from './agent-prompt-submission-verification'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { makeStore } from './runtime-rpc-worktree-store-fixtures'
|
||||
|
||||
@@ -132,6 +133,87 @@ describe('agent prompt submission runtime', () => {
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
|
||||
it('prefers a later permission title over an earlier explicit idle status', async () => {
|
||||
vi.useFakeTimers()
|
||||
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('does not retry from a composer snapshot older than observed output', async () => {
|
||||
vi.useFakeTimers()
|
||||
let resolveSnapshot!: (snapshot: {
|
||||
data: string
|
||||
cols: number
|
||||
rows: number
|
||||
seq: number
|
||||
source: 'headless'
|
||||
}) => void
|
||||
let snapshotSequence = 0
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
const writes: string[] = []
|
||||
const serializeProviderBuffer = vi.fn(
|
||||
() =>
|
||||
new Promise<{
|
||||
data: string
|
||||
cols: number
|
||||
rows: number
|
||||
seq: number
|
||||
source: 'headless'
|
||||
}>((resolve) => {
|
||||
snapshotSequence = runtime.getPtyOutputSequence('pty-prompt')
|
||||
resolveSnapshot = resolve
|
||||
})
|
||||
)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-prompt' }),
|
||||
write: (_ptyId, data) => {
|
||||
writes.push(data)
|
||||
if (data.includes(AGENT_PROMPT_BRACKETED_PASTE_END)) {
|
||||
runtime.onPtyData('pty-prompt', '\x1b[2J\x1b[H› review this', Date.now())
|
||||
}
|
||||
return true
|
||||
},
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
serializeProviderBuffer,
|
||||
hasRendererSerializer: () => false
|
||||
})
|
||||
const terminal = await runtime.createTerminal(`path:${WORKTREE_PATH}`, {
|
||||
launchAgent: 'aider'
|
||||
})
|
||||
runtime.synchronizePtyOutputSequenceFromProvider(
|
||||
'pty-prompt',
|
||||
{ value: 900, generation: 'continued' },
|
||||
0
|
||||
)
|
||||
const submission = runtime.sendTerminalAgentPrompt(terminal.handle, 'review this')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(AGENT_PROMPT_EFFECT_TIMEOUT_MS + 500)
|
||||
await vi.waitFor(() => expect(serializeProviderBuffer).toHaveBeenCalledOnce())
|
||||
runtime.onPtyData('pty-prompt', '\x1b[2J\x1b[Hagent accepted', Date.now())
|
||||
resolveSnapshot({
|
||||
data: '\x1b[2J\x1b[H› review this',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
seq: snapshotSequence,
|
||||
source: 'headless'
|
||||
})
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
await expect(submission).resolves.toMatchObject({ accepted: true })
|
||||
expect(writes.filter((data) => data === '\r')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('blocks a retry when permission appears after the first Enter', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { runtime, handle, writes } = await createPromptRuntime((runtime, data) => {
|
||||
|
||||
@@ -158,6 +158,28 @@ describe('agent prompt submission verification', () => {
|
||||
expect(retrySubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports permission reached after the retry as blocked', async () => {
|
||||
vi.useFakeTimers()
|
||||
let current = activity()
|
||||
const retrySubmit = vi.fn(async () => {
|
||||
current = activity({ lifecycleSequence: 5, status: 'permission' })
|
||||
return 'retried' as const
|
||||
})
|
||||
const verification = verifyAgentPromptSubmission({
|
||||
baseline: current,
|
||||
prompt: 'review this',
|
||||
readActivity: () => current,
|
||||
readTextBeforeCursor: async () => '› review this',
|
||||
retrySubmit
|
||||
})
|
||||
const rejected = expect(verification).rejects.toThrow('agent_prompt_blocked')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(AGENT_PROMPT_EFFECT_TIMEOUT_MS + 50)
|
||||
|
||||
await rejected
|
||||
expect(retrySubmit).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('cancels before retrying', async () => {
|
||||
vi.useFakeTimers()
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -72,7 +72,10 @@ export async function verifyAgentPromptSubmission(
|
||||
options.readActivity,
|
||||
options.signal
|
||||
)
|
||||
if (retryEffect === 'working' || retryEffect === 'permission') {
|
||||
if (retryEffect === 'permission') {
|
||||
throw new Error('agent_prompt_blocked')
|
||||
}
|
||||
if (retryEffect === 'working') {
|
||||
return { retried: true }
|
||||
}
|
||||
if (retryEffect === 'none') {
|
||||
|
||||
@@ -18620,7 +18620,10 @@ export class OrcaRuntimeService {
|
||||
if (current.generation !== baseline.generation) {
|
||||
throw new Error('terminal_handle_stale')
|
||||
}
|
||||
return visible?.generation === baseline.generation ? visible.textBeforeCursor : null
|
||||
return visible?.generation === baseline.generation &&
|
||||
visible.sequence >= current.outputSequence
|
||||
? visible.textBeforeCursor
|
||||
: null
|
||||
},
|
||||
retrySubmit: async (expected) => {
|
||||
assertAgentPromptRequestActive(options.signal)
|
||||
@@ -18679,11 +18682,15 @@ export class OrcaRuntimeService {
|
||||
const explicit = this.getFreshExplicitAgentStatusForHandle(handle)?.status
|
||||
const ptyStatus = this.ptysById.get(ptyId)?.lastAgentStatus ?? null
|
||||
const lifecycle = this.agentPromptLifecycleByPtyId.get(ptyId)
|
||||
const status =
|
||||
lifecycle?.status === 'permission'
|
||||
? 'permission'
|
||||
: (explicit ?? ptyStatus ?? lifecycle?.status ?? null)
|
||||
return {
|
||||
generation: this.getPtyLifecycleGeneration(ptyId),
|
||||
lifecycleSequence: lifecycle?.sequence ?? 0,
|
||||
outputSequence: this.getPtyOutputSequence(ptyId),
|
||||
status: explicit ?? ptyStatus ?? lifecycle?.status ?? null
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user