mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 08:03:13 +00:00
fix(terminal): require quiet parser before recovery
This commit is contained in:
@@ -4803,6 +4803,30 @@ describe('connectPanePty', () => {
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('does not arm the wedge probe when acknowledged input is rejected', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const { WRITE_PIPELINE_STALL_CHECK_MS } =
|
||||
await import('@/lib/pane-manager/terminal-write-pipeline-health')
|
||||
const transport = createMockTransport('pty-rejected')
|
||||
transport.sendInputAccepted = vi.fn().mockResolvedValue(false)
|
||||
transportFactoryQueue.push(transport)
|
||||
const pane = createPane(1)
|
||||
const binding = connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
await flushAsyncTicks()
|
||||
pane.terminal.write.mockClear()
|
||||
pane.terminal.write.mockImplementation(() => {})
|
||||
|
||||
sendTerminalInputThroughPane(pane, '\x03')
|
||||
await flushAsyncTicks()
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS * 2)
|
||||
await flushAsyncTicks()
|
||||
|
||||
expect(transport.sendInputAccepted).toHaveBeenCalledWith('\x03')
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith('', expect.any(Function))
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('blocks input when tab-level ptyId is stale even if panePtyId is null', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
|
||||
|
||||
@@ -3731,12 +3731,12 @@ export function connectPanePty(
|
||||
cancelSuspendedShellCommandInference()
|
||||
}
|
||||
clearPendingTerminalInputIntent()
|
||||
markTerminalInputSent()
|
||||
const writePromise = transport
|
||||
.sendInputAccepted(data)
|
||||
.then((accepted) => {
|
||||
if (accepted) {
|
||||
recordTerminalInputForHibernationFallback()
|
||||
// Why: rejected writes use transport recovery and must not arm a parser probe.
|
||||
markAcceptedTerminalInputSent()
|
||||
observeAcceptedShellCommandInput(data)
|
||||
observeAcceptedTerminalInput(data, acknowledgedIntent)
|
||||
interruptInference.observeInputIntent(acknowledgedIntent)
|
||||
|
||||
@@ -236,6 +236,32 @@ describe('terminal write pipeline health', () => {
|
||||
_resetWritePipelineHealthForTests(terminal)
|
||||
})
|
||||
|
||||
it('keeps a requested probe alive while the pipeline makes parse progress', () => {
|
||||
vi.useFakeTimers()
|
||||
const terminal = makeTerminal()
|
||||
terminal.dropCallbacks = true
|
||||
const handler = vi.fn()
|
||||
registerUndeliverableWriteHandler(terminal, handler)
|
||||
|
||||
requestTerminalWritePipelineProbe(terminal)
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS)
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS / 2)
|
||||
settleTerminalWriteStallWatch(terminal)
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS / 2)
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS / 2)
|
||||
settleTerminalWriteStallWatch(terminal)
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS / 2)
|
||||
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(isTerminalWritePipelineCertifiedDead(terminal)).toBe(false)
|
||||
|
||||
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS)
|
||||
|
||||
expect(handler).toHaveBeenCalledWith('write-stalled')
|
||||
expect(isTerminalWritePipelineCertifiedDead(terminal)).toBe(true)
|
||||
_resetWritePipelineHealthForTests(terminal)
|
||||
})
|
||||
|
||||
it('notifies at most once per terminal instance', () => {
|
||||
const terminal = makeTerminal()
|
||||
const handler = vi.fn()
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// delivery ack credits leak, and the pane becomes a fossil the user can only
|
||||
// cure by reloading the window. Detection here is probe-certified (mirroring
|
||||
// replay-guard.ts): a stalled completion triggers an empty probe write; xterm
|
||||
// parses in FIFO order, so a probe that also never completes proves the
|
||||
// pipeline is dead rather than slow. Certification notifies a per-terminal
|
||||
// handler (registered by the pane's PTY connection) that requests pane
|
||||
// recovery — a remount that rebuilds the xterm and reattaches the live PTY.
|
||||
// parses in FIFO order, so a completing probe proves preceding work drained. A
|
||||
// silent probe is only certified dead after a full interval without any parse
|
||||
// progress. Certification notifies a per-terminal handler (registered by the
|
||||
// pane's PTY connection) that requests pane recovery — a remount that rebuilds
|
||||
// the xterm and reattaches the live PTY.
|
||||
|
||||
type WriteTarget = {
|
||||
write(data: string, callback?: () => void): void
|
||||
@@ -109,9 +110,9 @@ export function isTerminalWritePipelineCertifiedDead(terminal: object): boolean
|
||||
* Arm (or keep armed) the stall watch for a terminal that just had a write
|
||||
* issued. Cleared by settleTerminalWriteStallWatch from the write-completion
|
||||
* callback. If the completion never arrives, an empty probe write certifies
|
||||
* dead-vs-slow exactly like replay-guard.ts: probe completes → pipeline is
|
||||
* alive (slow parse), re-arm and keep waiting; probe silent for another
|
||||
* interval → dead, notify.
|
||||
* dead-vs-slow conservatively like replay-guard.ts: probe completes → pipeline
|
||||
* is alive (slow parse), re-arm and keep waiting; probe silent for another
|
||||
* interval without any parse progress → dead, notify.
|
||||
*/
|
||||
function armTerminalWritePipelineWatch(
|
||||
terminal: WriteTarget,
|
||||
@@ -138,14 +139,25 @@ function armTerminalWritePipelineWatch(
|
||||
timer: setTimeout(probeForStall, stallCheckMs)
|
||||
}
|
||||
const certifyDead = (): void => certifyTerminalWritePipelineDead(terminal, watch)
|
||||
function armProbeQuietWindow(quietSinceGeneration: number): void {
|
||||
watch.timer = setTimeout(() => {
|
||||
if (stallWatchByTerminal.get(terminal) !== watch) {
|
||||
return
|
||||
}
|
||||
if (hasTerminalParseProgressSince(terminal, quietSinceGeneration)) {
|
||||
armProbeQuietWindow(captureTerminalParseProgressGeneration(terminal))
|
||||
return
|
||||
}
|
||||
certifyDead()
|
||||
}, stallCheckMs)
|
||||
}
|
||||
function probeForStall(): void {
|
||||
if (stallWatchByTerminal.get(terminal) !== watch) {
|
||||
return
|
||||
}
|
||||
let probeParsed = false
|
||||
const probeQueuedAtGeneration = captureTerminalParseProgressGeneration(terminal)
|
||||
try {
|
||||
terminal.write('', () => {
|
||||
probeParsed = true
|
||||
// Why: replay guards share this terminal-scoped generation; even an
|
||||
// auxiliary FIFO probe proves the parser is alive and making progress.
|
||||
recordTerminalParseProgress(terminal)
|
||||
@@ -161,11 +173,9 @@ function armTerminalWritePipelineWatch(
|
||||
certifyDead()
|
||||
return
|
||||
}
|
||||
watch.timer = setTimeout(() => {
|
||||
if (!probeParsed) {
|
||||
certifyDead()
|
||||
}
|
||||
}, stallCheckMs)
|
||||
if (stallWatchByTerminal.get(terminal) === watch) {
|
||||
armProbeQuietWindow(probeQueuedAtGeneration)
|
||||
}
|
||||
}
|
||||
stallWatchByTerminal.set(terminal, watch)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user