Files
orca/src/shared/terminal-reply-query-extraction.ts
T
Jinwoo-HandOrca 09b68cbb12 Cut the hidden-restore flood feedback loop (A) + query carve-out on drops (B)
(A) Under a foreground flood, the hidden-output-restore loop re-fetched
snapshots endlessly: each synchronous applyMainBufferSnapshot starved ACK
processing, main pinned at the in-flight cap, dropped at the pending cap,
and every droppedOutput/modelRestoreNeeded marker re-armed another
restore until the flood ended (rc.7.perf DSR timeouts).
- Restore loop: a foreground live-chunk queue overflow now abandons the
  restore immediately (the stream is outrunning snapshot fetch+replay),
  with a 3-iteration hard cap + lifecycle warn as backstop.
- Re-arm gate: drop markers/sentinels and reconcile seq-gaps on a visible
  pane during its own in-flight/just-abandoned restore no longer re-arm;
  live bytes write through and ONE deferred repaint (2s after the last
  backpressure signal) heals the gap. Hidden-pane gate semantics are
  unchanged.
- Query salvage: discarding queued restore bytes (overflow/refetch) now
  extracts DSR/CPR/DA/OSC-color queries and replays them to xterm so
  replies still flow.

(B) Main-side: dropOversizedPendingPtyData carves reply-eliciting query
sequences out of the dropped buffer (and out of post-drop latched data,
bounded) and ships them on the droppedOutput sentinel, so DSR probes
survive bulk drops. Query scanning moved to
src/shared/terminal-reply-query-extraction.ts, shared verbatim with the
renderer's hidden-startup query extraction.

Co-authored-by: Orca <help@stably.ai>
2026-07-03 22:45:02 -04:00

161 lines
4.7 KiB
TypeScript

// Terminal-output scanning for reply-eliciting query sequences (DSR/CPR,
// DA1/DA2, DECRQM, XTGETTCAP-adjacent CSI queries, OSC 10/11 color probes).
// Shared because both sides must salvage queries out of bytes they are about
// to drop: the renderer's hidden-output restore queue and main's pending-cap
// bulk drop. A swallowed query means the program that sent it waits forever
// for a reply (the bench DSR timeout).
import { parseTerminalOscColorQuery } from './terminal-osc-color-reply'
export const HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS = 64
export type ExtractedRendererQueryData = {
statelessQueryData: string
statefulQueryData: string
oscColorQueryData: string
pending: string
}
export function extractHiddenStartupRendererQueryData(
data: string,
pending: string
): ExtractedRendererQueryData {
const input = pending + data
let statelessQueryData = ''
let statefulQueryData = ''
let oscColorQueryData = ''
let offset = 0
while (offset < input.length) {
const candidateIndex = input.indexOf('\x1b', offset)
if (candidateIndex === -1) {
break
}
if (candidateIndex + 1 >= input.length) {
return {
statelessQueryData,
statefulQueryData,
oscColorQueryData,
pending: input.slice(candidateIndex)
}
}
if (input.startsWith('\x1b[', candidateIndex)) {
const finalByteIndex = findCsiFinalByteIndex(input, candidateIndex + 2)
if (finalByteIndex === -1) {
return {
statelessQueryData,
statefulQueryData,
oscColorQueryData,
pending: input.slice(
candidateIndex,
candidateIndex + HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS
)
}
}
const sequence = input.slice(candidateIndex, finalByteIndex + 1)
if (isStatelessRendererReplyCsiQuery(sequence)) {
statelessQueryData += sequence
} else if (isStatefulRendererReplyCsiQuery(sequence)) {
statefulQueryData += sequence
}
offset = finalByteIndex + 1
continue
}
if (input.startsWith('\x1b]', candidateIndex)) {
const query = parseTerminalOscColorQuery(input, candidateIndex)
if (query.kind === 'partial') {
return {
statelessQueryData,
statefulQueryData,
oscColorQueryData,
pending: input.slice(
candidateIndex,
candidateIndex + HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS
)
}
}
if (query.kind === 'none') {
offset = candidateIndex + 2
continue
}
oscColorQueryData += input.slice(candidateIndex, query.endIndex)
offset = query.endIndex
continue
}
if (parseTerminalOscColorQuery(input, candidateIndex).kind === 'partial') {
return {
statelessQueryData,
statefulQueryData,
oscColorQueryData,
pending: input.slice(candidateIndex)
}
}
{
offset = candidateIndex + 1
continue
}
}
return { statelessQueryData, statefulQueryData, oscColorQueryData, pending: '' }
}
export function containsCsiRendererQuery(data: string): boolean {
let offset = data.indexOf('\x1b[')
while (offset !== -1) {
const finalByteIndex = findCsiFinalByteIndex(data, offset + 2)
if (finalByteIndex === -1) {
return false
}
const sequence = data.slice(offset, finalByteIndex + 1)
if (isStatelessRendererReplyCsiQuery(sequence) || isStatefulRendererReplyCsiQuery(sequence)) {
return true
}
offset = data.indexOf('\x1b[', finalByteIndex + 1)
}
return false
}
export function containsStatefulRendererQuery(data: string): boolean {
let offset = data.indexOf('\x1b[')
while (offset !== -1) {
const finalByteIndex = findCsiFinalByteIndex(data, offset + 2)
if (finalByteIndex === -1) {
return false
}
const sequence = data.slice(offset, finalByteIndex + 1)
if (isStatefulRendererReplyCsiQuery(sequence)) {
return true
}
offset = data.indexOf('\x1b[', finalByteIndex + 1)
}
return false
}
export function findCsiFinalByteIndex(data: string, offset: number): number {
for (let index = offset; index < data.length; index++) {
const code = data.charCodeAt(index)
if (code >= 0x40 && code <= 0x7e) {
return index
}
}
return -1
}
export function isStatelessRendererReplyCsiQuery(sequence: string): boolean {
if (sequence.endsWith('c')) {
return true
}
return (
sequence === '\x1b[5n' ||
sequence === '\x1b[>q' ||
sequence === '\x1b[14t' ||
sequence === '\x1b[16t'
)
}
export function isStatefulRendererReplyCsiQuery(sequence: string): boolean {
return sequence === '\x1b[6n' || (sequence.startsWith('\x1b[?') && sequence.endsWith('$p'))
}