mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
Synthesize salvaged query replies directly instead of replaying into xterm
The 10MB dev bench proved the write-back salvage insufficient: a pending-cap drop always triggers a snapshot restore, whose replay guard swallows xterm auto-replies and whose discardTerminalOutput races away still-queued query writes — the salvaged DSR died both ways and the fence still timed out. Salvage now answers directly on the input path (immune to both): CPR (CSI 6n) from the live buffer via transport.sendInput, DA1 with the renderer's canned response, OSC color probes via the existing direct responder. Rare queries (DECRQM, DA2) keep the best-effort xterm replay. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -346,7 +346,8 @@ function createPane(paneId: number) {
|
||||
type: 'normal' as const,
|
||||
viewportY: 0,
|
||||
baseY: 0,
|
||||
cursorY: 0
|
||||
cursorY: 0,
|
||||
cursorX: 0
|
||||
}
|
||||
return {
|
||||
id: paneId,
|
||||
@@ -6693,6 +6694,7 @@ describe('connectPanePty', () => {
|
||||
|
||||
async function startInFlightRestore(): Promise<{
|
||||
pane: ReturnType<typeof createPane>
|
||||
transport: MockTransport
|
||||
dataCallback: (
|
||||
data: string,
|
||||
meta?: { seq?: number; rawLength?: number; droppedOutput?: boolean }
|
||||
@@ -6707,7 +6709,7 @@ describe('connectPanePty', () => {
|
||||
}> {
|
||||
enableMainAuthority()
|
||||
const deps = createDeps({ isVisibleRef: { current: true } })
|
||||
const { pane, dataCallback } = await connectHiddenPane(deps)
|
||||
const { pane, transport, dataCallback } = await connectHiddenPane(deps)
|
||||
const transportOptions = createdTransportOptions.at(-1) as {
|
||||
onPtySpawn?: (ptyId: string) => void
|
||||
}
|
||||
@@ -6735,6 +6737,7 @@ describe('connectPanePty', () => {
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
return {
|
||||
pane,
|
||||
transport,
|
||||
dataCallback,
|
||||
getMainBufferSnapshot,
|
||||
resolveFirstSnapshot: (snapshot) => firstSnapshot.resolve(snapshot)
|
||||
@@ -6779,11 +6782,12 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
|
||||
it('salvages stateful queries out of an overflowing restore queue', async () => {
|
||||
const { pane, dataCallback } = await startInFlightRestore()
|
||||
const { pane, transport, dataCallback } = await startInFlightRestore()
|
||||
|
||||
// Queue 400KB, then a chunk that overflows the cap and carries a DSR
|
||||
// probe. The content is discarded (snapshot owns it) but the query
|
||||
// must reach xterm so its reply flows.
|
||||
// probe. The content is discarded (snapshot owns it) but the probe's
|
||||
// reply is SYNTHESIZED directly — replaying it into xterm would race
|
||||
// the restore's discard and the replay guard's auto-reply swallow.
|
||||
dataCallback('a'.repeat(400 * 1024), { seq: 400 * 1024, rawLength: 400 * 1024 })
|
||||
dataCallback(`${'b'.repeat(200 * 1024)}\x1b[6n`, {
|
||||
seq: 600 * 1024 + 4,
|
||||
@@ -6791,8 +6795,10 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
await flushAsyncTicks(8)
|
||||
|
||||
const replies = transport.sendInput.mock.calls.map((call) => String(call[0]))
|
||||
// oxlint-disable-next-line no-control-regex -- the ESC byte IS the payload: this matches the CPR reply
|
||||
expect(replies.some((reply) => /^\u001b\[\d+;\d+R$/.test(reply))).toBe(true)
|
||||
const written = writtenFloodData(pane)
|
||||
expect(written).toContain('\x1b[6n')
|
||||
expect(written).not.toContain('aaaa')
|
||||
expect(written).not.toContain('bbbb')
|
||||
})
|
||||
|
||||
@@ -144,6 +144,7 @@ import { createTerminalGitHubPRLinkDetector } from '../../../../shared/terminal-
|
||||
import { scheduleTerminalWebglAtlasRecovery } from './terminal-webgl-atlas-recovery'
|
||||
import {
|
||||
CONPTY_DA1_RESPONSE,
|
||||
DEFAULT_DA1_RESPONSE,
|
||||
createTerminalPixelSizeQueryResponder,
|
||||
installTerminalCapabilityReplyHandlers,
|
||||
sendTerminalOscColorQueryReplies
|
||||
@@ -4591,10 +4592,13 @@ export function connectPanePty(
|
||||
|
||||
// Why: discarding queued flood bytes must never swallow terminal queries —
|
||||
// a lost DSR/CPR (or color/DA) reply hangs the querying program (the bench
|
||||
// DSR timeout). The discarded CONTENT is owned by the snapshot repaint;
|
||||
// the queries are re-played to xterm immediately so replies still flow.
|
||||
// Only called for bytes that are being thrown away, so replies cannot
|
||||
// double-fire against a later queue drain.
|
||||
// DSR timeout). The discarded CONTENT is owned by the snapshot repaint.
|
||||
// Replies are SYNTHESIZED directly (transport.sendInput) instead of
|
||||
// replaying the queries into xterm: a drop always triggers a snapshot
|
||||
// restore, whose replay guard swallows xterm auto-replies and whose
|
||||
// discardTerminalOutput races away queued query writes — both killed the
|
||||
// salvaged reply in practice. Only called for bytes being thrown away, so
|
||||
// replies cannot double-fire against a later queue drain.
|
||||
function salvageRendererQueriesFromDiscardedRestoreData(data: string): void {
|
||||
if (!data || !data.includes('\x1b')) {
|
||||
return
|
||||
@@ -4605,10 +4609,43 @@ export function connectPanePty(
|
||||
transport.sendInput(reply)
|
||||
)
|
||||
}
|
||||
const queryData = extracted.statelessQueryData + extracted.statefulQueryData
|
||||
if (queryData) {
|
||||
writePtyOutputToXterm(queryData, true, { hiddenStartupRendererQuery: true })
|
||||
let unansweredQueryData = ''
|
||||
for (const sequence of splitCsiSequences(
|
||||
extracted.statefulQueryData + extracted.statelessQueryData
|
||||
)) {
|
||||
if (sequence === '\x1b[6n') {
|
||||
// CPR from the live buffer. Position may be mid-repaint stale — in a
|
||||
// drop scenario positional accuracy is already forfeit; liveness is
|
||||
// the contract (a blocked reader must unblock).
|
||||
const buffer = pane.terminal.buffer.active
|
||||
const row = Math.min(buffer.cursorY + 1, pane.terminal.rows)
|
||||
const col = Math.min(buffer.cursorX + 1, pane.terminal.cols)
|
||||
transport.sendInput(`\x1b[${row};${col}R`)
|
||||
} else if (sequence === '\x1b[c' || sequence === '\x1b[0c') {
|
||||
transport.sendInput(DEFAULT_DA1_RESPONSE)
|
||||
} else {
|
||||
unansweredQueryData += sequence
|
||||
}
|
||||
}
|
||||
if (unansweredQueryData) {
|
||||
// Best-effort for the rarer queries (DECRQM, DA2, XTVERSION): replay
|
||||
// into xterm and let its handlers answer when no replay is active.
|
||||
writePtyOutputToXterm(unansweredQueryData, true, { hiddenStartupRendererQuery: true })
|
||||
}
|
||||
}
|
||||
|
||||
function splitCsiSequences(queryData: string): string[] {
|
||||
const sequences: string[] = []
|
||||
let offset = queryData.indexOf('\x1b[')
|
||||
while (offset !== -1) {
|
||||
const finalByteIndex = findCsiFinalByteIndex(queryData, offset + 2)
|
||||
if (finalByteIndex === -1) {
|
||||
break
|
||||
}
|
||||
sequences.push(queryData.slice(offset, finalByteIndex + 1))
|
||||
offset = queryData.indexOf('\x1b[', finalByteIndex + 1)
|
||||
}
|
||||
return sequences
|
||||
}
|
||||
|
||||
function queueLiveChunkDuringRestore(data: string, meta?: PtyDataMeta): void {
|
||||
|
||||
Reference in New Issue
Block a user