Files
orca/src/main/runtime/omp-readiness-transcripts.test.ts
T
44a2ee54bb fix(runtime): make OMP 18.4 workers ready at the composer and keep stale-title clears display-only (#24295)
* fix(runtime): require OMP composer readiness and retain native title evidence

Keep timer-cleared titles in display state so they cannot settle an idle wait.
Require OMP's captured empty composer through the existing screen rules.

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Saurav M Hiremath <sauravhiremath@gmail.com>

* fix(runtime): veto OMP setup by screen and keep stale-title clears display-only

OMP readiness no longer keys on the composer's thinking-effort hint, which
OMP retires after a few uses, hides once a conversation exists and renders
differently per glyph preset. Instead OMP's setup wizard (alternate screen or
a "Setup step N of M" heading) vetoes every ready lane, and OMP's own `π >`
idle title counts for the omp identity only once quiet on a screen read clear
of the wizard. Other OMP titles take the general path again, so the
post-turn `π -` title, 17.x `π:` titles and first-party blocked waits behave
as before. `π >` is no longer explicit idle for every identity.

The stale-working-title clear stays out of runtime records, but display
readers (worktree ps, phone status and titles, terminal list, orchestration
pointer, orphan adoption and reveal titles) now project records through one
display accessor, so they show the cleared title and status again. A title
the clear retired can no longer prove an agent is present; the foreground
process must, which also covers the retained Cursor spinner, so the Cursor
presence exclusion is reverted.

* test(runtime): refuse OMP's setup splash, which has no step heading

* test(runtime): a genuine title retires the stale-working display clear

* test(daemon): record the OMP captures' inherited shrink cursor divergence

* fix(runtime): settle OMP's idle title by its age and veto only its alternate screen

OMP 18.4 re-asserts bracketed paste every second once a terminal answers
its capability probe, which Orca's terminals do, so an idle OMP pane never
went quiet and worker-start still timed out. Its idle title now counts once
it has stood a quiescence window on a screen read clear of the setup wizard.
The setup veto now reads only the alternate screen: the wizard always runs
there, and a 'Setup step N of M' line on the normal screen is an answer's
own text.

* test(runtime): let the grid ingest OMP repaints before the title arrives

* fix(runtime): keep the stale-title clear's process checks and record lifetime

The stale-working-title clear is now an explicit branch instead of an early
return. It still skips the title/status evidence readiness and delivery
read, but runs what main's clear did to re-derive process state: the
foreground-agent re-probe (an exited shell-launched agent loses its
identity) and the completion-race exit check behind a fresh done hook. The
clear now lives on the PTY record, so an SSH relay drop and reattach keeps
the cleared display, and a recreated tracker is seeded from it. Presence
judges a retired title by display ordering, so a renderer pane title older
than the clear cannot prove an exited agent is present.

* docs(runtime): OMP's idle title precedes its setup wizard by its startup tail

* fix(runtime): judge agent presence by the displayed title while a clear stands

A title the stale-working timer cleared used to veto presence outright, so a
live, busy agent whose title cleared to its name and whose process Orca
cannot recognize read as no agent, and the answer depended on whether a
renderer pane was mounted. Presence and the terminal status query now read
the display projection instead, which is what they read before the clear
stopped rewriting records: a cwd spinner still clears to a neutral title, so
the foreground process decides for an agent that exited behind it.

* fix(runtime): compare a cleared title with a lifecycle cleared the same way

The stale-working clear no longer writes the prompt lifecycle, so the
blocked-dialog check compared a cleared title with a still-working
lifecycle and ignored a trust or update dialog painted after the clear:
terminal status said idle, the guarded send accepted, and a mounted pane's
interactive wait went missing. The lifecycle is now projected through the
clear wherever a cleared title is compared with it, as main recorded both
together. That comparison can only report a wait on the user, never idle.

* fix(runtime): verify prompts behind a stale-title clear against main's baseline

Prompt-submission verification read the native lifecycle while a
stale-working clear stood, so output after a swallowed Enter behind a stuck
spinner counted as delivered, and the first spinner frame after the clear
was not a new turn. Its starting status now goes through the same lifecycle
projection the blocked-dialog check uses, and the first working status
after a clear starts a new turn, as main recorded it. Readiness, delivery
and the mailbox still read only native evidence. The blocked-dialog comment
now says the projected pair reproduces main's verdict.

---------

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: Ahmed Nagy <ahmednagy25t@gmail.com>
Co-authored-by: Saurav M Hiremath <sauravhiremath@gmail.com>
2026-10-02 13:29:47 -07:00

232 lines
8.4 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness'
import { extractLastOscTitle } from '../../shared/osc-title-extraction'
import type { TuiAgent } from '../../shared/tui-agent'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
webContents: { fromId: vi.fn(() => null) },
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
app: { getPath: vi.fn(() => '/tmp') }
}))
function transcript(name: string): string {
return readFileSync(join(__dirname, '__fixtures__', `${name}.txt`), 'utf8')
}
// Synthetic repaint of the composer's bottom row (row 21 of the 120x40 capture): no capture
// exists of a post-turn, resumed or non-default-glyph composer, whose effort hint is gone or
// differs.
const HINTLESS_COMPOSER_ROW = '\x1b[21;1H\x1b[2K╰─'
const ASCII_HINT_COMPOSER_ROW = `\x1b[21;1H\x1b[2K╰─${' '.repeat(82)}Shift+Tab to change thinking effort`
async function waitsReady(
options: {
name: string
title: string
launchAgent?: TuiAgent
repaint?: string
size?: { cols: number; rows: number } | null
busyFirst?: boolean
keepalive?: boolean
},
timeoutMs = 5_000
): Promise<boolean> {
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'Terminal',
foregroundProcess: options.launchAgent ? 'omp' : 'bun',
data: transcript(options.name),
...(options.launchAgent ? { launchAgent: options.launchAgent } : {}),
...(options.size === null ? {} : { size: options.size ?? { cols: 120, rows: 40 } })
})
if (options.busyFirst) {
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b]0;π : capture-cwd\x07', Date.now())
}
if (options.repaint) {
// Why the wait: the grid ingests writes asynchronously, and the repaint precedes the title.
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, options.repaint, Date.now())
await vi.advanceTimersByTimeAsync(50)
}
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, `\x1b]0;${options.title}\x07`, Date.now())
// OMP 18.4.5 re-asserts bracketed paste every second once a terminal answers its DECRQM probe,
// as xterm and main's query authority do; the capture tool answered nothing.
const keepalive = options.keepalive
? setInterval(() => runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b[?2004h', Date.now()), 1_000)
: null
const settled = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs }).then(
(result) => result.satisfied === true,
() => false
)
await vi.advanceTimersByTimeAsync(timeoutMs)
if (keepalive) {
clearInterval(keepalive)
}
return settled
}
describe('OMP 18.4.5 captured readiness', () => {
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it.each([
['omp-18-setup', 120, 40, false],
['omp-18-composer', 120, 40, true],
['omp-18-composer-narrow', 60, 24, true]
] as const)('%s at %sx%s has readiness %s', async (name, cols, rows, ready) => {
const data = transcript(name)
expect(data).toContain('\x1b[')
expect(data).toContain('\r')
const { runtime, handle } = await createTranscriptPane({
paneTitle: extractLastOscTitle(data) ?? 'OMP',
foregroundProcess: 'omp',
data,
launchAgent: 'omp',
size: { cols, rows }
})
const result = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 5_000 })
const assertion = ready
? expect(result).resolves.toMatchObject({ satisfied: true })
: expect(result).rejects.toThrow('timeout')
await Promise.all([assertion, vi.advanceTimersByTimeAsync(5_000)])
})
it('does not accept the composer while its native title still says working', async () => {
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'OMP',
foregroundProcess: 'omp',
data: transcript('omp-18-composer'),
launchAgent: 'omp',
size: { cols: 120, rows: 40 }
})
// Synthetic busy transition isolates the timer while preserving the captured composer grid.
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b]0;π : capture-cwd\x07', Date.now())
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', Date.now())
const result = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 5_000 })
const assertion = expect(result).rejects.toThrow('timeout')
await Promise.all([assertion, vi.advanceTimersByTimeAsync(5_000)])
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b]0;π > capture-cwd\x07', Date.now())
const idle = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 5_000 })
const idleAssertion = expect(idle).resolves.toMatchObject({ satisfied: true })
await Promise.all([idleAssertion, vi.advanceTimersByTimeAsync(5_000)])
})
it.each([
['hintless', HINTLESS_COMPOSER_ROW],
['ascii-glyph', ASCII_HINT_COMPOSER_ROW]
])('accepts a %s composer: the effort hint retires and its glyphs vary', async (_, repaint) => {
expect(
await waitsReady({
name: 'omp-18-composer',
title: 'π > capture-cwd',
launchAgent: 'omp',
repaint
})
).toBe(true)
})
it("accepts the titlebar extension's post-turn title on a hintless composer", async () => {
expect(
await waitsReady({
name: 'omp-18-composer',
title: 'π - capture-cwd',
launchAgent: 'omp',
repaint: HINTLESS_COMPOSER_ROW,
busyFirst: true
})
).toBe(true)
})
it('accepts an OMP 17-style `π:` title through the quiet name-only lane', async () => {
expect(
await waitsReady({ name: 'omp-18-composer', title: 'π: capture-cwd', launchAgent: 'omp' })
).toBe(true)
})
it("refuses the wizard's splash, which has no step heading yet", async () => {
const setup = transcript('omp-18-setup')
const splash = setup.slice(0, setup.indexOf('Setup step'))
expect(splash).toContain('\x1b[?1049h')
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'Terminal',
foregroundProcess: 'omp',
data: splash,
launchAgent: 'omp',
size: { cols: 120, rows: 40 }
})
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, '\x1b]0;π > capture-cwd\x07', Date.now())
const result = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 5_000 })
const assertion = expect(result).rejects.toThrow('timeout')
await Promise.all([assertion, vi.advanceTimersByTimeAsync(5_000)])
})
it.each([
['omp-18-composer', true],
['omp-18-setup', false]
] as const)('%s under the bracketed-paste keepalive has readiness %s', async (name, ready) => {
expect(
await waitsReady(
{ name, title: 'π > capture-cwd', launchAgent: 'omp', keepalive: true },
10_000
)
).toBe(ready)
})
it('accepts a post-turn answer that mentions a setup step on the normal screen', async () => {
expect(
await waitsReady({
name: 'omp-18-composer',
title: 'π - capture-cwd',
launchAgent: 'omp',
repaint: '\x1b[12;1H\x1b[2K Setup step 2 of 4: install the dependencies',
busyFirst: true
})
).toBe(true)
})
it.each(['π - capture-cwd', 'π: capture-cwd'])(
'refuses %s while the setup wizard is on screen',
async (title) => {
expect(await waitsReady({ name: 'omp-18-setup', title, launchAgent: 'omp' })).toBe(false)
}
)
it('refuses `π >` with no trustworthy screen to rule the setup wizard out', async () => {
expect(
await waitsReady({
name: 'omp-18-composer',
title: 'π > capture-cwd',
launchAgent: 'omp',
size: null
})
).toBe(false)
// A post-turn title cannot come from setup, so it needs no screen.
expect(
await waitsReady({
name: 'omp-18-composer',
title: 'π - capture-cwd',
launchAgent: 'omp',
size: null
})
).toBe(true)
})
it.each([['pi' as const], [undefined]])(
'does not make the setup wizard ready at once for agent identity %s',
async (launchAgent) => {
// Identities other than omp keep main's quiet name-only lane, which cannot settle this soon.
expect(
await waitsReady(
{
name: 'omp-18-setup',
title: 'π > capture-cwd',
...(launchAgent ? { launchAgent } : {})
},
1_000
)
).toBe(false)
}
)
})