From 66f20258e3f1245fcc69b0f683ae0b4081b6e7b2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 3 Jul 2026 00:35:51 -0400 Subject: [PATCH] Throttle the terminal wait-blocked check off the PTY hot path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-windowed-tail attribution (findings log 2026-07-03): the blocked- reason complex — two full-tail buildTerminalWaitText builds plus toLowerCase and multi-pattern scans per chunk, existing only to stamp waitBlockedAt — consumed ~85% of onPtyData's remaining cost (~700-790ms/s under an agent-TUI flood). The check now runs at a 50ms cadence over coalesced chunks (PTY chunk boundaries are arbitrary, so coalescing preserves semantics), with a trailing-edge timer so burst-final state is always evaluated, and an immediate bypass when the incoming chunk (plus a 31-char split carry) contains a prompt keyword — so actionable-prompt stamping stays per-chunk-immediate while keyword-free flood frames skip the complex entirely. Previous wait text is cached per pty instead of rebuilt, and state is cleared at both pty teardown sites. 1,415 runtime tests green (including the cross-chunk prompt test, which exercises the keyword bypass), typecheck and lint clean. Co-authored-by: Orca --- src/main/runtime/orca-runtime.ts | 107 +++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 14 deletions(-) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index f71f26b1e63..d63cea1f0b4 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -1967,6 +1967,23 @@ export class OrcaRuntimeService { // Why: OSC 9999 status can span PTY chunks. Keeping parser state in the // runtime lets hidden/model-owned terminals observe agent state without a // mounted xterm view. + // Why a throttle: the blocked-reason check builds and scans two full wait + // texts (<=256KB each, lowercased) — measured at ~85% of onPtyData's cost + // under a TUI flood (findings log 2026-07-03). PTY chunk boundaries are + // arbitrary, so running the identical computation over coalesced chunks at + // a bounded cadence (plus a trailing-edge timer so burst-final state is + // always evaluated) preserves semantics while removing it from the hot path. + private waitBlockedCheckStateByPtyId = new Map< + string, + { + lastAt: number + lastWaitText: string + appended: string + keywordCarry: string + timer: ReturnType | null + } + >() + private agentStatusOscProcessorsByPtyId = new Map< string, ReturnType @@ -5088,26 +5105,12 @@ export class OrcaRuntimeService { pty.lastOutputAt = at const normalized = normalizeTerminalChunk(data, pty.tailPendingAnsi) pty.tailPendingAnsi = normalized.pendingAnsi - const previousWaitText = buildTerminalWaitText( - pty.tailBuffer, - pty.tailPartialLine, - pty.preview - ) const nextTail = appendNormalizedToTailBuffer( pty.tailBuffer, pty.tailPartialLine, normalized.text, pty.tailRedrawCursor ) - if ( - nextTailHasNewerBlockedReason( - previousWaitText, - buildTerminalWaitText(nextTail.lines, nextTail.partialLine, pty.preview), - normalized.text - ) - ) { - pty.waitBlockedAt = at - } ptyTailAfter = nextTail pty.tailBuffer = nextTail.lines pty.tailPartialLine = nextTail.partialLine @@ -5115,6 +5118,7 @@ export class OrcaRuntimeService { pty.tailTruncated = pty.tailTruncated || nextTail.truncated pty.tailLinesTotal += nextTail.newCompleteLines pty.preview = buildPreview(pty.tailBuffer, pty.tailPartialLine) + this.scheduleWaitBlockedCheck(ptyId, normalized.text, at) if (oscTitle !== null) { const prevStatus = pty.lastAgentStatus const prevTitle = pty.lastOscTitle @@ -5248,6 +5252,71 @@ export class OrcaRuntimeService { return outputSequence } + private scheduleWaitBlockedCheck(ptyId: string, appendedText: string, at: number): void { + let state = this.waitBlockedCheckStateByPtyId.get(ptyId) + if (!state) { + state = { lastAt: 0, lastWaitText: '', appended: '', keywordCarry: '', timer: null } + this.waitBlockedCheckStateByPtyId.set(ptyId, state) + } + const appendedLower = appendedText.toLowerCase() + const keywordHit = WAIT_BLOCKED_KEYWORD_PATTERN.test(`${state.keywordCarry}${appendedLower}`) + state.keywordCarry = appendedLower.slice(-WAIT_BLOCKED_KEYWORD_CARRY_CHARS) + // Why the cap keeps the tail: the accumulated text only anchors boundary- + // spanning prompt detection; anything past the tail cap has scrolled out + // of the retained tail the check reads anyway. + state.appended = + state.appended.length + appendedText.length > MAX_TAIL_CHARS + ? `${state.appended}${appendedText}`.slice(-MAX_TAIL_CHARS) + : `${state.appended}${appendedText}` + const elapsed = at - state.lastAt + if (keywordHit || elapsed >= WAIT_BLOCKED_CHECK_MIN_INTERVAL_MS || elapsed < 0) { + this.runWaitBlockedCheck(ptyId, state, at) + return + } + if (!state.timer) { + // Why trailing edge: the final chunks of a burst must still be + // evaluated or a prompt arriving right after a flood would go + // unstamped until the next output. + state.timer = setTimeout(() => { + state.timer = null + this.runWaitBlockedCheck(ptyId, state, Date.now()) + }, WAIT_BLOCKED_CHECK_MIN_INTERVAL_MS - elapsed) + } + } + + private runWaitBlockedCheck( + ptyId: string, + state: { + lastAt: number + lastWaitText: string + appended: string + keywordCarry: string + timer: ReturnType | null + }, + at: number + ): void { + const pty = this.ptysById.get(ptyId) + if (!pty) { + state.appended = '' + return + } + const nextWaitText = buildTerminalWaitText(pty.tailBuffer, pty.tailPartialLine, pty.preview) + if (nextTailHasNewerBlockedReason(state.lastWaitText, nextWaitText, state.appended)) { + pty.waitBlockedAt = at + } + state.lastAt = at + state.lastWaitText = nextWaitText + state.appended = '' + } + + private clearWaitBlockedCheckState(ptyId: string): void { + const state = this.waitBlockedCheckStateByPtyId.get(ptyId) + if (state?.timer) { + clearTimeout(state.timer) + } + this.waitBlockedCheckStateByPtyId.delete(ptyId) + } + private processAgentStatusOscForPty(ptyId: string, data: string): ProcessedAgentStatusChunk { let processor = this.agentStatusOscProcessorsByPtyId.get(ptyId) if (!processor) { @@ -6661,6 +6730,7 @@ export class OrcaRuntimeService { this.resizeListeners.delete(ptyId) this.lastRendererSizes.delete(ptyId) this.recentPtyOutputById.delete(ptyId) + this.clearWaitBlockedCheckState(ptyId) this.ptyOutputSequenceById.delete(ptyId) this.agentStatusOscProcessorsByPtyId.delete(ptyId) this.oscTitleScanTailByPtyId.delete(ptyId) @@ -17670,6 +17740,7 @@ export class OrcaRuntimeService { serveSimStateWatcher.unbindPty(ptyId) this.ptysById.delete(ptyId) this.recentPtyOutputById.delete(ptyId) + this.clearWaitBlockedCheckState(ptyId) this.ptyOutputSequenceById.delete(ptyId) this.agentStatusOscProcessorsByPtyId.delete(ptyId) this.oscTitleScanTailByPtyId.delete(ptyId) @@ -22040,6 +22111,14 @@ export class OrcaRuntimeService { } } +const WAIT_BLOCKED_CHECK_MIN_INTERVAL_MS = 50 +// Why: chunks that can complete an actionable prompt bypass the throttle so +// blocked stamps stay per-chunk-immediate; the pattern heads mirror +// findTerminalWaitBlockedSignal. Scanned over the new chunk plus a short +// carry only — never the accumulated window. +const WAIT_BLOCKED_KEYWORD_PATTERN = + /press enter|press t to trust|do you trust|trust this|trusted workspace|update available|choose working directory|codex just got an upgrade|hooks need review/ +const WAIT_BLOCKED_KEYWORD_CARRY_CHARS = 31 const MAX_TAIL_LINES = 2000 const MAX_TAIL_CHARS = 256 * 1024 const MAX_TAIL_PARTIAL_CHARS = 4000