Files
orca/src/shared/agent-status-osc.ts
T
Neil a067cccd38 perf(terminal): resume OSC terminator search past the carried frame (#19839)
A single unterminated OSC 9999 marker split across many PTY chunks
re-scanned the whole accumulation for a terminator on every chunk, so
work grew with the square of the frame length.

Carry how much of `pending` already failed the search and resume one
character before it, which is enough for an `ESC \\` straddling the
chunk boundary.
2026-09-09 22:06:31 -07:00

132 lines
4.8 KiB
TypeScript

import type { ParsedAgentStatusPayload } from './agent-status-types'
import { parseAgentStatusPayload } from './agent-status-types'
import { ownRetainedString } from './own-retained-string'
const OSC_AGENT_STATUS_PREFIX = '\x1b]9999;'
/** Return a suffix that can only be the beginning of an OSC 9999 marker. */
function findAgentStatusPrefixCarry(data: string): string {
const lastChar = data.charCodeAt(data.length - 1)
if (lastChar !== 0x1b && lastChar !== 0x5d && lastChar !== 0x39 && lastChar !== 0x3b) {
return ''
}
const maxCarryLength = Math.min(data.length, OSC_AGENT_STATUS_PREFIX.length - 1)
for (let length = maxCarryLength; length > 0; length -= 1) {
const suffix = data.slice(data.length - length)
if (OSC_AGENT_STATUS_PREFIX.startsWith(suffix)) {
return suffix
}
}
return ''
}
export type ProcessedAgentStatusChunk = {
cleanData: string
payloads: ParsedAgentStatusPayload[]
lastPayloadCleanOffset: number | null
}
function findAgentStatusTerminator(
data: string,
searchFrom: number,
next: { belIndex: number; stIndex: number }
): { index: number; length: 1 | 2 } | null {
// Reuse forward matches, including absence, for this chunk's remaining frames.
// Requires `searchFrom` to increase on every call for one `data`; a rewind would
// reuse a match that is no longer the earliest.
if (next.belIndex !== -1 && next.belIndex < searchFrom) {
next.belIndex = data.indexOf('\x07', searchFrom)
}
if (next.stIndex !== -1 && next.stIndex < searchFrom) {
next.stIndex = data.indexOf('\x1b\\', searchFrom)
}
if (next.belIndex === -1 && next.stIndex === -1) {
return null
}
if (next.stIndex === -1 || (next.belIndex !== -1 && next.belIndex < next.stIndex)) {
return { index: next.belIndex, length: 1 }
}
return { index: next.stIndex, length: 2 }
}
/**
* Stateful OSC 9999 parser for PTY streams.
* Why: hidden/model-owned terminal output needs the same agent-status parsing
* as mounted terminal panes, even when no terminal view is rendered.
*/
export function createAgentStatusOscProcessor(): (data: string) => ProcessedAgentStatusChunk {
const MAX_PENDING = 64 * 1024
let pending = ''
// How much of `pending` already failed a terminator search, so a frame split across
// many chunks re-scans only the new bytes instead of the whole accumulation.
let pendingSearched = 0
return (data: string): ProcessedAgentStatusChunk => {
// Ordinary terminal output is by far the common case. Keep it on the
// identity path unless the chunk ends with a split OSC marker; this avoids
// rebuilding a clean-data string for every PTY frame.
if (pending.length === 0 && !data.includes(OSC_AGENT_STATUS_PREFIX)) {
const carry = findAgentStatusPrefixCarry(data)
if (carry.length === 0) {
return { cleanData: data, payloads: [], lastPayloadCleanOffset: null }
}
pending = carry
return {
cleanData: data.slice(0, data.length - carry.length),
payloads: [],
lastPayloadCleanOffset: null
}
}
const combined = pending + data
const resumeFrom = pendingSearched
pending = ''
pendingSearched = 0
const payloads: ParsedAgentStatusPayload[] = []
let lastPayloadCleanOffset: number | null = null
let cleanData = ''
let cursor = 0
const nextTerminator = { belIndex: 0, stIndex: 0 }
while (cursor < combined.length) {
const start = combined.indexOf(OSC_AGENT_STATUS_PREFIX, cursor)
if (start === -1) {
const tail = combined.slice(cursor)
const carry = findAgentStatusPrefixCarry(tail)
if (carry.length > 0) {
cleanData += tail.slice(0, tail.length - carry.length)
pending = carry
} else {
cleanData += tail
}
break
}
cleanData += combined.slice(cursor, start)
const payloadStart = start + OSC_AGENT_STATUS_PREFIX.length
// Minus one so a `\x1b\\` straddling the previous chunk boundary is still found.
const searchFrom =
start === 0 && resumeFrom > 0 ? Math.max(payloadStart, resumeFrom - 1) : payloadStart
const terminator = findAgentStatusTerminator(combined, searchFrom, nextTerminator)
if (terminator === null) {
const candidate = combined.slice(start)
// Own the frame so it stops pinning the consumed chunk it was sliced from.
pending = candidate.length > MAX_PENDING ? '' : ownRetainedString(candidate)
pendingSearched = pending.length
break
}
const parsed = parseAgentStatusPayload(combined.slice(payloadStart, terminator.index))
if (parsed) {
payloads.push(parsed)
lastPayloadCleanOffset = cleanData.length
}
cursor = terminator.index + terminator.length
}
return { cleanData, payloads, lastPayloadCleanOffset }
}
}