From d9aa09cc8cf5c7fddd87898fca738134fdbbca63 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:38:52 -0700 Subject: [PATCH] fix(runtime): retire terminal snapshot probes before waiter timeout (#16149) * refactor(renderer): use terminal paste sanitizer directly * fix(runtime): order visible probe timeout settlement * fix(runtime): skip unorderable visible probes --- src/main/runtime/orca-runtime.ts | 24 +++++++++++++++---- .../src/lib/active-agent-note-send.ts | 6 ++--- src/renderer/src/lib/agent-paste-draft.ts | 7 +----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index aac73f5e7f4..ab470d78533 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -35600,23 +35600,36 @@ export class OrcaRuntimeService { * later transition. A provider screen that is still working when this fires * resolves through the poll, not here. */ private startTuiIdleVisibleReadProbe(waiter: TerminalWaiter, waiterTimeoutMs: number): void { - const snapshotTimeoutMs = Math.min( - VISIBLE_TERMINAL_SNAPSHOT_TIMEOUT_MS, - Math.max(0, waiterTimeoutMs - 1) + const settleMarginMs = Math.min( + TUI_IDLE_VISIBLE_PROBE_SETTLE_MARGIN_MS, + Math.max(1, Math.floor(waiterTimeoutMs / 3)) ) + const probeTimeoutMs = Math.min( + VISIBLE_TERMINAL_SNAPSHOT_TIMEOUT_MS + settleMarginMs, + Math.max(0, waiterTimeoutMs - settleMarginMs) + ) + const providerTimeoutMs = Math.min( + VISIBLE_TERMINAL_SNAPSHOT_TIMEOUT_MS, + Math.max(0, probeTimeoutMs - settleMarginMs) + ) + // Node clamps sub-millisecond timers to 1ms, so no distinct retirement deadline exists. + if (providerTimeoutMs < 1) { + return + } + // Retire the provider before the detached probe and waiter can settle. void withTimeout( this.readTerminal( waiter.handle, {}, { - timeoutMs: snapshotTimeoutMs, + timeoutMs: providerTimeoutMs, retireOnTimeout: true, // Why: the ready banner stays in scrollback for the whole session, so // classifying history would call a working agent idle (#15569 review). visibleScreenOnly: true } ), - snapshotTimeoutMs, + probeTimeoutMs, null ) .then((read) => { @@ -38641,6 +38654,7 @@ const MAX_TERMINAL_PREVIEW_CHARS = 32 * 1024 export const AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS = 8_000 const VISIBLE_TERMINAL_SNAPSHOT_TIMEOUT_MS = 750 const VISIBLE_TERMINAL_SNAPSHOT_RETRY_MS = 1_000 +const TUI_IDLE_VISIBLE_PROBE_SETTLE_MARGIN_MS = 10 const MAX_PREVIEW_LINES = 6 const MAX_PREVIEW_CHARS = 300 const WORKTREE_STATUS_PRIORITY: Record = { diff --git a/src/renderer/src/lib/active-agent-note-send.ts b/src/renderer/src/lib/active-agent-note-send.ts index 0089773c7d3..97bb2418132 100644 --- a/src/renderer/src/lib/active-agent-note-send.ts +++ b/src/renderer/src/lib/active-agent-note-send.ts @@ -1,4 +1,5 @@ import type { RuntimeTerminalSend, RuntimeTerminalWait } from '../../../shared/runtime-types' +import { sanitizeTerminalPasteText } from '@/components/terminal-pane/terminal-bracketed-paste' import { useAppStore } from '@/store' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' @@ -10,8 +11,7 @@ import { import { BRACKETED_PASTE_BEGIN, BRACKETED_PASTE_END, - POST_PASTE_SUBMIT_DELAY_MS, - sanitizeBracketedPasteContent + POST_PASTE_SUBMIT_DELAY_MS } from './agent-paste-draft' import type { ActiveAgentNotesSendResult } from './active-agent-note-send-result' import { @@ -182,7 +182,7 @@ async function sendPromptWithGuardedPasteAndEnter( return { status: initialAgentStatus.status } } - const pastePayload = `${BRACKETED_PASTE_BEGIN}${sanitizeBracketedPasteContent(prompt)}${BRACKETED_PASTE_END}` + const pastePayload = `${BRACKETED_PASTE_BEGIN}${sanitizeTerminalPasteText(prompt)}${BRACKETED_PASTE_END}` try { const { send } = await callRuntimeRpc<{ send: RuntimeTerminalSend }>( runtimeTarget, diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index daa9fce0da5..f078383333d 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -9,8 +9,7 @@ import { } from '@/runtime/runtime-terminal-inspection' import { BRACKETED_PASTE_END, - BRACKETED_PASTE_START, - sanitizeTerminalPasteText + BRACKETED_PASTE_START } from '@/components/terminal-pane/terminal-bracketed-paste' import { runTerminalPtyInputTransaction } from '@/components/terminal-pane/terminal-pty-input-transaction' import { waitForAgentReady } from './agent-ready-wait' @@ -36,10 +35,6 @@ export const BRACKETED_PASTE_BEGIN = BRACKETED_PASTE_START export { BRACKETED_PASTE_END } export const POST_PASTE_SUBMIT_DELAY_MS = 50 -export function sanitizeBracketedPasteContent(content: string): string { - return sanitizeTerminalPasteText(content) -} - // Why: "the tab has a PTY" and "the agent's composer accepts input" are separate // states with separate failure modes, so they get separate budgets. A PTY that // hasn't appeared in 8s means the launch itself failed — waiting the (longer)