mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
refactor(agent-launch): collapse the readiness budget to one flat timeout
The per-signal budget (marker 20s / quiet-window 8s) tied the timeout to how readiness is DETECTED. The budget is really a property of how slowly an agent can boot — a marker, a quiet window, and a process check all wait out the same cold start — so one number covers all three signals. Replaces draftPasteReadyBudgetMs() with DRAFT_PASTE_READY_TIMEOUT_MS: drops a constant, a branch, and two tests, and removes the only reason a delivery path needed to know which signal class it was using. Cost: a launch that never emits DECSET 2004 now surfaces its 'prompt not sent' toast at 20s instead of 8s. That is the failed-launch path only; successful markerless delivery still resolves on the 1.5s quiet window as before.
This commit is contained in:
@@ -418,7 +418,7 @@ import {
|
||||
} from '../../shared/tui-agent-config'
|
||||
import {
|
||||
createDraftPasteReadyScanner,
|
||||
draftPasteReadyBudgetMs
|
||||
DRAFT_PASTE_READY_TIMEOUT_MS
|
||||
} from '../../shared/draft-paste-ready-scanner'
|
||||
import { detectInstalledAgentsWithShellPathHydration, detectRemoteAgents } from '../ipc/preflight'
|
||||
import {
|
||||
@@ -21374,7 +21374,7 @@ export class OrcaRuntimeService {
|
||||
// delivery paths do, instead of silently dropping the prompt (STA-3367).
|
||||
hardTimer = setTimeout(() => {
|
||||
void this.resolveDraftPastePtyIfAgentOwns(ptyId, agent).then(finish)
|
||||
}, draftPasteReadyBudgetMs(readySignal))
|
||||
}, DRAFT_PASTE_READY_TIMEOUT_MS)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,16 @@
|
||||
* scanner, the shared budget, and the fallback are all exercised.
|
||||
*/
|
||||
import { beforeEach, afterEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import { draftPasteReadyBudgetMs } from '../../shared/draft-paste-ready-scanner'
|
||||
import { DRAFT_PASTE_READY_TIMEOUT_MS } from '../../shared/draft-paste-ready-scanner'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
|
||||
const CODEX_COMPOSER_PROMPT_RENDER = '\x1b[1m›\x1b[0m Ask Codex to do anything'
|
||||
const PTY_ID = 'pty-1'
|
||||
const HANDLE = 'term-1'
|
||||
// Why: pull the budgets from the shared policy rather than restating them, so
|
||||
// this fails if the runtime waiter ever stops consuming it. The literal values
|
||||
// are pinned once, in draft-paste-ready-scanner.test.ts.
|
||||
const CODEX_MARKER_BUDGET_MS = draftPasteReadyBudgetMs('codex-composer-prompt')
|
||||
const QUIET_WINDOW_BUDGET_MS = draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste')
|
||||
// Why: the old budget that dropped the prompt, kept only to prove the waiter now
|
||||
// outlasts it. The current value is pinned in draft-paste-ready-scanner.test.ts.
|
||||
const OLD_DROPPED_AT_MS = 8000
|
||||
|
||||
type RuntimeHost = {
|
||||
getLivePtyForHandle: (handle: string) => { pty: { ptyId: string } } | null
|
||||
@@ -80,18 +78,18 @@ describe('waitForStartupDraftReady', () => {
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('waits the full marker budget before giving up on a cold codex composer', async () => {
|
||||
it('waits the full budget before giving up on a cold codex composer', async () => {
|
||||
const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex')
|
||||
await Promise.resolve()
|
||||
observer?.(DECSET_BRACKETED_PASTE)
|
||||
|
||||
// The old markerless-length budget would already have resolved (to null) here.
|
||||
await vi.advanceTimersByTimeAsync(QUIET_WINDOW_BUDGET_MS)
|
||||
// The old budget would already have resolved (to null) here.
|
||||
await vi.advanceTimersByTimeAsync(OLD_DROPPED_AT_MS)
|
||||
expect(getForegroundProcess).not.toHaveBeenCalled()
|
||||
|
||||
// Still inside the marker budget: a late glyph is the real ready proof, and
|
||||
// must win over the ownership fallback.
|
||||
await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS - QUIET_WINDOW_BUDGET_MS - 1)
|
||||
// Still inside the budget: a late glyph is the real ready proof, and must
|
||||
// win over the ownership fallback.
|
||||
await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS - OLD_DROPPED_AT_MS - 1)
|
||||
observer?.(CODEX_COMPOSER_PROMPT_RENDER)
|
||||
await expect(promise).resolves.toBe(PTY_ID)
|
||||
expect(getForegroundProcess).not.toHaveBeenCalled()
|
||||
@@ -105,7 +103,7 @@ describe('waitForStartupDraftReady', () => {
|
||||
const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex')
|
||||
await Promise.resolve()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS)
|
||||
await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS)
|
||||
|
||||
await expect(promise).resolves.toBe(PTY_ID)
|
||||
expect(getForegroundProcess).toHaveBeenCalledWith(PTY_ID)
|
||||
@@ -117,7 +115,7 @@ describe('waitForStartupDraftReady', () => {
|
||||
const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex')
|
||||
await Promise.resolve()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS)
|
||||
await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS)
|
||||
|
||||
await expect(promise).resolves.toBeNull()
|
||||
})
|
||||
@@ -127,7 +125,7 @@ describe('waitForStartupDraftReady', () => {
|
||||
const promise = waitForStartupDraftReady.call(host, HANDLE, 'codex')
|
||||
await Promise.resolve()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(CODEX_MARKER_BUDGET_MS)
|
||||
await vi.advanceTimersByTimeAsync(DRAFT_PASTE_READY_TIMEOUT_MS)
|
||||
|
||||
await expect(promise).resolves.toBeNull()
|
||||
})
|
||||
|
||||
@@ -306,7 +306,7 @@ import { isWslUncPath } from '../../../../shared/wsl-paths'
|
||||
import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config'
|
||||
import {
|
||||
createDraftPasteReadyScanner,
|
||||
draftPasteReadyBudgetMs
|
||||
DRAFT_PASTE_READY_TIMEOUT_MS
|
||||
} from '../../../../shared/draft-paste-ready-scanner'
|
||||
import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content'
|
||||
import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer'
|
||||
@@ -4883,7 +4883,7 @@ export function connectPanePty(
|
||||
startupDraftHardTimer = setTimeout(() => {
|
||||
startupDraftHardTimer = null
|
||||
void deliverStartupDraftIfAgentOwnsPty()
|
||||
}, draftPasteReadyBudgetMs(startupDraftReadySignal))
|
||||
}, DRAFT_PASTE_READY_TIMEOUT_MS)
|
||||
}
|
||||
const armStartupDraftQuietTimer = (): void => {
|
||||
if (!startupDraftReadyScanner || startupDraftPasteSettled) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TuiAgent, GlobalSettings } from '../../../shared/types'
|
||||
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
|
||||
import { draftPasteReadyBudgetMs } from '../../../shared/draft-paste-ready-scanner'
|
||||
import { DRAFT_PASTE_READY_TIMEOUT_MS } from '../../../shared/draft-paste-ready-scanner'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
inspectRuntimeTerminalProcess,
|
||||
@@ -68,8 +68,7 @@ export function getSettingsForAgentTabRuntimeOwner(
|
||||
* Returns true when the paste was issued, false on timeout or missing
|
||||
* PTY. `onTimeout` lets the caller surface a UI hint (e.g. toast) when
|
||||
* the agent doesn't reach a ready state. `timeoutMs` overrides the
|
||||
* per-signal readiness budget only; waiting for the PTY to spawn keeps its
|
||||
* own fixed budget.
|
||||
* readiness budget only; waiting for the PTY to spawn keeps its own budget.
|
||||
*
|
||||
* Readiness combines DECSET 2004 with one agent-specific follow-up signal:
|
||||
* 1. `\x1b[?2004h` (DECSET 2004 — bracketed-paste-enable) on the PTY
|
||||
@@ -110,7 +109,7 @@ export async function pasteDraftWhenAgentReady(args: {
|
||||
const settings = getSettingsForAgentTabRuntimeOwner(tabId)
|
||||
// Why: the readiness budget starts once the PTY exists, so a slow spawn
|
||||
// shortens nothing — a cold composer still gets its full window.
|
||||
const budget = timeoutMs ?? draftPasteReadyBudgetMs(readySignal)
|
||||
const budget = timeoutMs ?? DRAFT_PASTE_READY_TIMEOUT_MS
|
||||
const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings)
|
||||
if (!ready) {
|
||||
// Why: fast-starting TUIs can emit the paste-ready escape sequence before
|
||||
@@ -153,7 +152,7 @@ export async function pasteDraftToAgentPtyWhenReady(args: {
|
||||
|
||||
const settings = getSettingsForAgentTabRuntimeOwner(tabId)
|
||||
const readySignal = agentConfig?.draftPasteReadySignal ?? 'render-quiet-after-bracketed-paste'
|
||||
const budget = timeoutMs ?? draftPasteReadyBudgetMs(readySignal)
|
||||
const budget = timeoutMs ?? DRAFT_PASTE_READY_TIMEOUT_MS
|
||||
const ready = await waitForAgentDraftInputReady(ptyId, budget, readySignal, settings)
|
||||
if (!ready) {
|
||||
const fallbackReady = agentConfig
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createDraftPasteReadyScanner, draftPasteReadyBudgetMs } from './draft-paste-ready-scanner'
|
||||
import {
|
||||
createDraftPasteReadyScanner,
|
||||
DRAFT_PASTE_READY_TIMEOUT_MS
|
||||
} from './draft-paste-ready-scanner'
|
||||
|
||||
const DECSET_BRACKETED_PASTE = '\x1b[?2004h'
|
||||
const SHOW_CURSOR = '\x1b[?25h'
|
||||
@@ -137,28 +140,10 @@ describe('createDraftPasteReadyScanner', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('draftPasteReadyBudgetMs', () => {
|
||||
it('gives marker-gated signals room for a cold boot (STA-3367)', () => {
|
||||
// Why: the marker is a positive proof, so waiting longer can only tolerate a
|
||||
// slow first-run codex — it can never paste into a composer that isn't there.
|
||||
expect(draftPasteReadyBudgetMs('codex-composer-prompt')).toBe(20000)
|
||||
expect(draftPasteReadyBudgetMs('render-cursor-after-bracketed-paste')).toBe(20000)
|
||||
})
|
||||
|
||||
it('keeps the markerless quiet window tight', () => {
|
||||
// Why: silence is a guess, not a proof; a longer budget just delays a wrong answer.
|
||||
expect(draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste')).toBe(8000)
|
||||
})
|
||||
|
||||
it('gives every marker-gated signal a strictly larger budget than the quiet window', () => {
|
||||
// Guards the invariant rather than the numbers: a new marker signal added to
|
||||
// the scanner must not silently inherit the tighter markerless budget.
|
||||
const quiet = draftPasteReadyBudgetMs('render-quiet-after-bracketed-paste')
|
||||
for (const signal of [
|
||||
'codex-composer-prompt',
|
||||
'render-cursor-after-bracketed-paste'
|
||||
] as const) {
|
||||
expect(draftPasteReadyBudgetMs(signal)).toBeGreaterThan(quiet)
|
||||
}
|
||||
describe('DRAFT_PASTE_READY_TIMEOUT_MS', () => {
|
||||
it('leaves room for a cold-boot composer (STA-3367)', () => {
|
||||
// Why: the reported drop was a first-run codex composer mounting past the old
|
||||
// 8s budget. Pinned once here; every delivery path imports rather than restates it.
|
||||
expect(DRAFT_PASTE_READY_TIMEOUT_MS).toBe(20000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,32 +20,21 @@ export type DraftPasteReadyScanResult = {
|
||||
armQuietTimer: boolean
|
||||
}
|
||||
|
||||
// Why: the markerless quiet-window signal guesses readiness from output silence,
|
||||
// so a long budget just delays a wrong answer. Keep it tight.
|
||||
const QUIET_WINDOW_READY_TIMEOUT_MS = 8000
|
||||
// Why: marker-gated signals (Codex '›', opencode show-cursor) are positive
|
||||
// readiness proofs — the paste fires only when the marker actually renders, so a
|
||||
// longer budget can never paste prematurely; it only tolerates a slow cold boot.
|
||||
// First-run codex on a cold app can take >8s to mount its composer, past which
|
||||
// the old budget gave up and dropped the launch prompt (STA-3367).
|
||||
const MARKER_READY_TIMEOUT_MS = 20000
|
||||
|
||||
/**
|
||||
* How long a delivery path should wait for `readySignal` before falling back.
|
||||
* Backstop for every draft-paste delivery path: how long to wait for a ready
|
||||
* signal before falling back to a best-effort, ownership-checked paste.
|
||||
*
|
||||
* Lives beside the scanner because the budget is a property of the signal — only
|
||||
* this module knows which signals are marker-gated — not of the calling path. All
|
||||
* three draft-delivery owners (renderer tab paste, renderer startup paste, main
|
||||
* runtime startup paste) consume this so the policy cannot drift between them.
|
||||
* One budget for all signals. The value is a property of how slowly an agent can
|
||||
* boot, not of how readiness is detected — a marker, a quiet window, and a
|
||||
* process check all wait on the same cold start. First-run codex on a cold app
|
||||
* can take >8s to mount its composer, past which the old budget gave up and
|
||||
* dropped the launch prompt (STA-3367).
|
||||
*
|
||||
* This is a backstop, not a correctness gate: every caller must still attempt a
|
||||
* best-effort delivery when the agent owns the PTY after the budget expires.
|
||||
* Lives beside the scanner so all three delivery owners (renderer tab paste,
|
||||
* renderer startup paste, main runtime startup paste) share one policy and
|
||||
* cannot drift apart.
|
||||
*/
|
||||
export function draftPasteReadyBudgetMs(readySignal: DraftPasteReadySignal): number {
|
||||
return readySignal === 'render-quiet-after-bracketed-paste'
|
||||
? QUIET_WINDOW_READY_TIMEOUT_MS
|
||||
: MARKER_READY_TIMEOUT_MS
|
||||
}
|
||||
export const DRAFT_PASTE_READY_TIMEOUT_MS = 20000
|
||||
|
||||
/**
|
||||
* Pure, incremental scanner shared by the renderer and main-process draft-paste
|
||||
|
||||
Reference in New Issue
Block a user