mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 16:02:41 +00:00
* fix(agents): deliver grok launch drafts on its composer frame
Grok has no --prefill-style flag, so a launch draft (e.g. the issue URL of
a worktree created from a GitHub issue) always goes through Orca's
paste-after-ready path. That path used the default readiness signal: DECSET
2004 plus 1.5s of PTY silence. Grok shimmers its startup logo at ~12fps
until the session opens, so the quiet window never settled and the draft
fell through to the 8s hard timeout before it appeared in the composer.
Gate grok on its own composer glyph instead, anchored on the alternate-screen
switch rather than DECSET 2004: the shell that runs the launch command emits
2004 too, and its prompt may itself be the same glyph (starship, pure), so a
Codex-style anchor could paste into the shell. Grok keeps the quiet window
armed as a fallback because it renders differentially and paints the glyph
once, so a late-attaching scanner would otherwise wait out the hard timeout.
Measured against grok 1.0.0 driving the real scanner over a zsh -> grok PTY:
draft delivery moves from 8003ms to 689ms, with the URL landing unsubmitted
in the composer exactly as before.
* fix(agents): keep grok's quiet-window floor on DECSET 2004
The composer-glyph marker is anchored on the alternate-screen switch, but grok
can render inline (`--no-alt-screen`, `--minimal`, `[ui] screen_mode =
"minimal"`), where 1049h never arrives. Anchoring the quiet-window fallback
there too left those launches with no delivery path at all: readiness never
resolved, and the main-process caller drops the draft when it resolves null —
so the issue URL vanished instead of arriving late.
Give the signal two independent anchors: the marker still waits for the
alt-screen switch (so a starship/pure shell prompt can't trip it), while the
quiet window arms off DECSET 2004 exactly as the default signal does. Inline and
legacy-Windows-console launches keep their pre-existing timing; alt-screen
launches keep the fast marker path.
Verified on grok 1.0.0 over a real zsh -> grok PTY: alt-screen delivers at 687ms
via the marker, inline at 1949ms via the quiet window (the default signal
measures 1861ms on the same launch), URL landing unsubmitted in both. Adds a
recorded inline-mode trace fixture so the no-1049h path stays covered.
* fix(agents): revoke grok's alt-screen anchor when the screen is handed back
The composer-glyph anchor latched forever: once \x1b[?1049h had been seen, any
later `❯` counted as grok's composer. Two ways that pastes the launch draft into
the user's shell instead of into grok:
- grok enters the alternate screen and then dies before painting a composer;
the shell prompt that follows is `❯` under starship or pure.
- a pager or editor started from the user's shell rc enters and leaves the
alternate screen before grok is ever launched, arming the anchor against the
shell's own prompt.
Track the anchor in stream order instead of as a latch: \x1b[?1049l revokes it,
re-entering re-arms it, and a marker only counts inside a segment where the
anchor is actually held. The chunk is walked segment by segment so ordering
within a single PTY packet is honored, with a 7-char carry — one short of the
escape sequence — so a split sequence rejoins without re-walking scanned output
into a second transition. Signals with no `markerAnchorEnd` (codex, opencode,
the default) keep their existing latch semantics untouched.
Also makes the trace-replay test model the hard timeout: the real waiters settle
at 8s, so a marker landing after that is not a delivery time.
120 lines
5.5 KiB
TypeScript
120 lines
5.5 KiB
TypeScript
// Replays recorded grok startup PTY streams through the readiness scanner and
|
||
// asserts when each signal would have delivered the launch draft.
|
||
//
|
||
// The bug this covers: grok shimmers its welcome logo at ~12fps from startup
|
||
// until the session opens, so the default quiet window (1.5s of silence after
|
||
// DECSET 2004) never settles. Draft delivery fell through to the caller's 8s
|
||
// hard timeout, which is why pasting a GitHub issue URL into a fresh grok
|
||
// worktree felt frozen next to Claude's argv prefill.
|
||
//
|
||
// Two traces, because grok has two rendering modes and the fix must not trade
|
||
// one for the other: the default alternate-screen mode (fast marker path) and
|
||
// the inline mode that emits no alt-screen switch (quiet-window floor).
|
||
import { describe, expect, it } from 'vitest'
|
||
import { createDraftPasteReadyScanner } from './draft-paste-ready-scanner'
|
||
import type { DraftPasteReadySignal } from './tui-agent-config'
|
||
import {
|
||
GROK_STARTUP_PTY_TRACE,
|
||
type GrokStartupTraceChunk
|
||
} from './__fixtures__/grok-startup-pty-trace'
|
||
import { GROK_INLINE_STARTUP_PTY_TRACE } from './__fixtures__/grok-inline-startup-pty-trace'
|
||
|
||
const QUIET_WINDOW_MS = 1500
|
||
const HARD_TIMEOUT_MS = 8000
|
||
|
||
function chunkData(chunk: GrokStartupTraceChunk): string {
|
||
return chunk.data ?? 'x'.repeat(chunk.bytes ?? 0)
|
||
}
|
||
|
||
/**
|
||
* Replay `trace` against `signal` and return the ms offset at which the caller
|
||
* would have pasted — the marker frame, or the first quiet window that elapses
|
||
* without another chunk. `null` means the caller's hard timeout wins, which for
|
||
* the main-process path means the draft is dropped entirely.
|
||
*
|
||
* Mirrors the callers in agent-draft-readiness.ts and orca-runtime.ts: the quiet
|
||
* timer is re-armed on every chunk that asks for it, a chunk arriving before the
|
||
* deadline cancels it, and the hard timeout outranks both — a marker that lands
|
||
* after it is too late for the real waiters, which have already settled.
|
||
*/
|
||
function replayReadyAtMs(
|
||
signal: DraftPasteReadySignal,
|
||
trace: GrokStartupTraceChunk[]
|
||
): number | null {
|
||
const scanner = createDraftPasteReadyScanner(signal)
|
||
let quietDeadline: number | null = null
|
||
for (const [index, chunk] of trace.entries()) {
|
||
const settledAt =
|
||
quietDeadline !== null ? Math.min(quietDeadline, HARD_TIMEOUT_MS) : HARD_TIMEOUT_MS
|
||
if (chunk.t >= settledAt) {
|
||
return quietDeadline !== null && quietDeadline <= HARD_TIMEOUT_MS ? quietDeadline : null
|
||
}
|
||
const scanned = scanner.observe(chunkData(chunk))
|
||
if (scanned.ready) {
|
||
return chunk.t
|
||
}
|
||
quietDeadline = scanned.armQuietTimer ? chunk.t + QUIET_WINDOW_MS : quietDeadline
|
||
if (index === trace.length - 1 && quietDeadline !== null && quietDeadline < HARD_TIMEOUT_MS) {
|
||
return quietDeadline
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
describe('grok startup trace replay (alternate-screen mode)', () => {
|
||
it('delivers on the composer frame instead of waiting out the hard timeout', () => {
|
||
const readyAt = replayReadyAtMs('grok-composer-prompt', GROK_STARTUP_PTY_TRACE)
|
||
expect(readyAt).not.toBeNull()
|
||
expect(readyAt).toBeLessThan(1000)
|
||
})
|
||
|
||
it('never settles the quiet window under the shimmering logo (the old behavior)', () => {
|
||
// The recording runs 10s past launch; the default signal reaches the end
|
||
// still waiting, so delivery only happened at the caller's 8s hard timeout.
|
||
expect(replayReadyAtMs('render-quiet-after-bracketed-paste', GROK_STARTUP_PTY_TRACE)).toBeNull()
|
||
})
|
||
|
||
it('fires on the same frame that paints the composer box', () => {
|
||
const composerFrame = GROK_STARTUP_PTY_TRACE.find((chunk) => chunk.data?.includes('❯'))
|
||
expect(composerFrame).toBeDefined()
|
||
expect(replayReadyAtMs('grok-composer-prompt', GROK_STARTUP_PTY_TRACE)).toBe(composerFrame?.t)
|
||
})
|
||
})
|
||
|
||
describe('grok startup trace replay (inline mode, no alternate screen)', () => {
|
||
it('still delivers through the quiet window when the marker never anchors', () => {
|
||
// Why: inline grok emits no \x1b[?1049h, so the composer glyph never counts.
|
||
// Anchoring the quiet window on the alt-screen switch too would leave this
|
||
// launch with NO delivery path — orca-runtime drops the draft when readiness
|
||
// resolves null, so the issue URL would vanish instead of arriving late.
|
||
const readyAt = replayReadyAtMs('grok-composer-prompt', GROK_INLINE_STARTUP_PTY_TRACE)
|
||
expect(readyAt).not.toBeNull()
|
||
expect(readyAt).toBeLessThan(HARD_TIMEOUT_MS)
|
||
})
|
||
|
||
it('matches the default signal exactly, so inline launches keep their old timing', () => {
|
||
expect(replayReadyAtMs('grok-composer-prompt', GROK_INLINE_STARTUP_PTY_TRACE)).toBe(
|
||
replayReadyAtMs('render-quiet-after-bracketed-paste', GROK_INLINE_STARTUP_PTY_TRACE)
|
||
)
|
||
})
|
||
|
||
it('reports the hard timeout, not a late marker, once the waiters have settled', () => {
|
||
// Guards the model above: the real waiters resolve at 8s, so a marker landing
|
||
// after that must not be reported as a delivery time.
|
||
const lateMarker: GrokStartupTraceChunk[] = [
|
||
{ t: 0, data: '\x1b[?1049h' },
|
||
{ t: 8500, data: '\x1b[38;2;200;200;200m❯ ' }
|
||
]
|
||
expect(replayReadyAtMs('grok-composer-prompt', lateMarker)).toBeNull()
|
||
})
|
||
|
||
it('records a startup with no alternate-screen switch', () => {
|
||
// Guards the fixture itself: if a future re-recording captures alt-screen
|
||
// output, the inline assertions above would silently stop testing inline.
|
||
expect(GROK_INLINE_STARTUP_PTY_TRACE.some((chunk) => chunk.data?.includes('\x1b[?1049h'))).toBe(
|
||
false
|
||
)
|
||
expect(GROK_INLINE_STARTUP_PTY_TRACE.some((chunk) => chunk.data?.includes('❯'))).toBe(true)
|
||
})
|
||
})
|