mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
(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>
161 lines
4.7 KiB
TypeScript
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'))
|
|
}
|