mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
fix(runtime): wait out Codex 0.157's startup screen, and stop at Codex's startup dialogs, before typing a worker brief (#23745)
* fix(runtime): wait for Codex's live chat before typing a worker brief Codex 0.157 draws a provisional startup screen (header reads model: loading) and discards typed input while it starts its shared daemon behind it; a fresh Codex home makes that window seconds long, so worker-start pasted briefs that were truncated or never submitted. Codex 0.158 dropped the header labels Orca matched, so worker-start stopped seeing Codex as ready at all. Readiness now requires Codex's live chat on both layouts: the provisional header vetoes a text match unless the live status row is already painted, and 0.158's greeting layout counts once that status row appears. Codex 0.158's model announcement dialog is reported as a blocked prompt instead of receiving the brief. * fix(runtime): recognise Codex's provisional screen from the text copy and the screen probe Live worker-start on a fresh Codex 0.157 home still typed during the daemon start: Codex leaves its alternate screen for that window, so the live screen showed no header and the screen-based veto never fired. The text copy keeps the provisional header until the live chat paints its status row, so the veto now reads it there. The tui-idle visible-screen probe used the bare text rule on the rendered screen; it now goes through the same body rule. * test(daemon): register the new Codex captures' known serializer divergences The serialize round-trip replay picks up every fixture under runtime/__fixtures__, and the three new Codex 0.157/0.158 captures showed 48/9/8 "new-fail" checkpoints against an expected 0, turning CI red. They are the existing live-pen colour leak on restored cells, the same class as the other Codex and DSH entries; this branch changes no serializer code. * fix(runtime): keep Qoder off the Codex screen probe change; drop an unbacked row filter - The tui-idle visible-screen probe now classified Qoder panes with isQoderComposerReady, which skips the working veto evaluateTuiIdle applies first. Qoder paints its composer mid-turn, so an adopted Qoder pane whose hooks said "working" settled the wait immediately. Only Codex and unknown panes take the body rule there; every other agent keeps its old verdict. - The live status-row check skipped rows containing "waiting for startup", a string Codex 0.157/0.158's TUI never prints. The line-folded text copy keeps a whole screen on one line, so the filter could only ever veto the real status row. It is now a bounded includes() with no split. - Lowercase the wait text once in isKnownReadyPromptBody. - Restore the per-frame "screen never takes a settled header away" check, guarded on the provisional veto, instead of checking the final frame only. * fix(runtime): stop reporting Codex 0.158's model announcement once it is answered The announcement's choices stay in the text copy after the user answers it, and the existing dismissal check needed the model:/directory: labels that 0.158's header lacks, so tui-idle waits and the agent-status query kept reporting codex-model-migration-prompt over a live chat. Codex repaints its whole screen, header included, when a startup dialog closes, so the header after the dialog now marks it answered. Also corrects the live-chat marker comments: the middle dot also comes from the daemon session's agents hint row and the warnings notice, not only the status row. * docs(runtime): note that Codex startup dialogs also draw the live-footer dot * fix(runtime): recognise Codex 0.157/0.158 startup dialogs by the rows they really print Codex 0.157 and 0.158 no longer print `Press enter to continue/confirm` on their startup dialogs; they print key rows instead (`enter continue · esc skip`, `enter confirm · esc skip`, `enter/esc continue · ctrl+c quit`). The update, hooks-review and model-migration matchers still required the old wording, so none of these dialogs was reported as blocked. On 0.157 the dialog's `·` also satisfied the live-footer check, so a tui-idle wait read the update dialog as ready and worker-start would type the brief into it, where Enter picks "Update now" (npm install -g, Codex exits). On 0.158 the wait timed out instead of reporting blocked. The matchers now accept the old wording or the new row, tolerating the spaces the line-folded text copy drops around `·`. Each one matches from the dialog's first `·` (for the update dialog that is its title row, `Update available · 0.157.0 → …`), so the dialog is blocked from the same character that would otherwise make the provisional header read as live. The retired-model notice without choices has a catalog-supplied heading (`GPT-5.4 is no longer available`), so it is matched by its own key row. No new blocked-reason value. The startup-dialog matchers move to startup-dialog-blocked-signals.ts to keep terminal-wait-detection.ts under the line limit. Backed by six real captures (update available, hooks review, retired model without choices, each on 0.157.0 and 0.158.0), replayed frame by frame and through a tui-idle wait; the serializer round-trip replay registers their existing live-pen colour divergences. * fix(runtime): keep reporting Codex's retired-model notice after a relaunch in the same pane The retired-model notice is matched by its key row alone, and the matcher took the first `enter/esc continue ·` in the live window while every other startup-dialog matcher takes the last. Quitting Codex from the notice and relaunching it in the same pane leaves the old copy ahead of the new launch's header, so the header read as having dismissed the new notice: 0.157 then read ready and a worker brief would be typed into the dialog. Take the last key row, and replay each captured dialog quit-and-relaunched to pin all six. * fix(runtime): match Codex startup dialogs by the rows the text copy keeps intact Codex 0.157+ paints each startup dialog over its startup screen by cell diff, so Orca's line-folded text copy can drop letters and spaces from a heading: #23765's 0.157.1 capture reads `Updat available`. The update matcher needed `update available`, so on that capture tier 1 read the dialog's own `·` as the live chat's footer and a tui-idle wait settled ready on the update dialog, whose Enter picks "Update now". Match each dialog from its first `·` by rows Codex prints as fixed literals: `available · <version>` and `enter continue · esc skip` (update), `enter confirm ·` (hooks review), `enter/esc continue|confirm ·` (model notices, which also covers 0.158's new-model announcement, so its choice-text matcher goes). Legacy `Press enter to …` wording still matches. Add the new rows to the blocked-signal prefilter, and replay #23765's 0.157.1 update capture in the dialog suite. * fix(runtime): don't name Codex's mid-session pickers a hooks review Codex's rate-limit reset popup (and its other pickers) end their key row with `enter confirm · esc back`, which the hooks-review row matched now that it no longer needs the heading. Exclude `esc back` instead of requiring `esc skip`, so a half-painted hooks-review row still blocks.
This commit is contained in:
@@ -43,6 +43,17 @@ const KNOWN_PREEXISTING_I2_FAILURES: Record<string, number> = {
|
||||
'codex-0157-effort-override-embedded-warning': 4,
|
||||
'codex-0157-no-daemon-effort-override': 16,
|
||||
'codex-0157-plain-ready': 18,
|
||||
// Fresh-home 0.157/0.158 captures: the live pen's true-colour fg/bg leaks onto restored cells.
|
||||
'codex-0157-fresh-home-daemon-install': 48,
|
||||
'codex-0158-fresh-home-greeting': 9,
|
||||
'codex-0158-model-announcement-dialog': 8,
|
||||
// Codex 0.157/0.158 startup-dialog captures: the same live-pen true-colour leak onto restored cells.
|
||||
'codex-0157-update-available-dialog': 26,
|
||||
'codex-0158-update-available-dialog': 8,
|
||||
'codex-0157-hooks-review-dialog': 24,
|
||||
'codex-0158-hooks-review-dialog': 8,
|
||||
'codex-0157-model-retired-dialog': 22,
|
||||
'codex-0158-model-retired-dialog': 6,
|
||||
// Same extra dim bit on the 0.157/0.158 header row (STA-8834 fixtures).
|
||||
'codex-0-157-1-update-dialog': 16,
|
||||
'codex-0-158-0-approval': 12,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-28T21:45:00.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.157.0, plain launch (no -m/-c) on a fresh CODEX_HOME. The provisional screen reads `model: loading` while Codex installs and starts its shared daemon (`Installing daemon from CLI version 0.157.0`); 0.157 discards typed input during that start. The live header fills in at the end. Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:30:27.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.157.0, plain launch on a fresh CODEX_HOME with a hooks.json of 8 command hooks that were never trusted. Over the provisional screen Codex paints `Hooks need review`, `1. Review hooks` / `2. Trust all and continue` / `3. Continue without trusting (hooks won't run)` and the key row `enter confirm · esc skip`, where older builds wrote `Press enter to confirm`. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[?2004h[>4;0m[>7u[?1004h[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭───────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22mloading[2m │[6;1H╰───────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;41H[2m───────────╮[2;41H │[3;41H │[4;14H [3mloading[4;24H[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;14H [22m/Users/user1/cr/rounds/dlg-h57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[39m[49m[0m[37;3H[?25h[?2026l[?1006l[?1015l[?1003l[?1002l[?1000l[<1u[?1007l[?1049l[<1u[>4;0m[?2004l[?1004l[0 q[?25hInstalling daemon from CLI version 0.157.0 into /Users/user1/cr/rounds/dlg-h57/home/.codex/packages/app-server-daemon...
|
||||
[?2004h[>4;0m[>7u[?1004h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭──────────────────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22m/Users/user1/cr/rounds/dlg-h57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;2H[0m[48;2;30;30;30m[K[2;20H[0m[48;2;30;30;30m[K[3;30H[0m[48;2;30;30;30m[K[4;58H[0m[48;2;30;30;30m[K[5;2H[0m[48;2;30;30;30m[K[6;2H[0m[48;2;30;30;30m[K[8;28H[0m[48;2;30;30;30m[K[9;49H[0m[48;2;30;30;30m[K[10;2H[0m[48;2;30;30;30m[K[36;2H[0m[49m[K[37;2H[0m[49m[K[38;2H[0m[49m[K[40;2H[0m[49m[K[1;1H[39;48;2;30;30;30m [2;1H [1mHooks need review[3;1H[22m [38;5;3;48;2;30;30;30m8 hooks are new or changed.[4;1H[39;48;2;30;30;30m [2mHooks can run outside the sandbox after you trust them.[5;1H[22m [6;1H [7;1H[1m[38;2;0;0;46;48;2;99;168;248m› 1. Review hooks [8;1H[22m[39;48;2;30;30;30m 2. Trust all and continue[9;1H 3. Continue without trusting (hooks won't run)[10;1H [11;3H[1m[38;2;255;255;255;49menter[22m[2m[2m[39;49m confirm · [22m[1m[38;2;255;255;255;49mesc[22m[2m[2m[39;49m skip [36;1H[22m [37;1H [38;1H [39m[49m[0m[?2026l
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:31:06.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.157.0, plain launch on a fresh CODEX_HOME whose config.toml sets model = \"gpt-5.4-mini\", a model missing from 0.157's catalog, so Codex shows its retired-model notice without choices: `GPT-5.4 Mini is no longer available`, `Switch to GPT-6 Luna to continue.` and the key row `enter/esc continue · ctrl+c quit`. Enter or Esc switches the model; the heading comes from the model catalog, not the older `Codex just got an upgrade` wording. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[?2004h[>4;0m[>7u[?1004h[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭───────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22mloading[2m │[6;1H╰───────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;41H[2m───────────╮[2;41H │[3;41H │[4;14H [3mloading[4;24H[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;14H [22m/Users/user1/cr/rounds/dlg-m57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[39m[49m[0m[37;3H[?25h[?2026l[?1006l[?1015l[?1003l[?1002l[?1000l[<1u[?1007l[?1049l[<1u[>4;0m[?2004l[?1004l[0 q[?25hInstalling daemon from CLI version 0.157.0 into /Users/user1/cr/rounds/dlg-m57/home/.codex/packages/app-server-daemon...
|
||||
[?2004h[>4;0m[>7u[?1004h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭──────────────────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22m/Users/user1/cr/rounds/dlg-m57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;2H[0m[48;2;30;30;30m[K[2;38H[0m[48;2;30;30;30m[K[3;2H[0m[48;2;30;30;30m[K[4;88H[0m[48;2;30;30;30m[K[5;35H[0m[48;2;30;30;30m[K[6;2H[0m[48;2;30;30;30m[K[7;2H[0m[49m[K[36;2H[0m[49m[K[37;2H[0m[49m[K[38;2H[0m[49m[K[40;2H[0m[49m[K[1;1H[39;48;2;30;30;30m [2;1H GPT-5.4 Mini is no longer available[3;1H [4;1H Codex now uses GPT-6 Luna in place of GPT-5.4 Mini. Switch to GPT-6 Luna to continue.[5;1H [1m[38;2;255;255;255;48;2;30;30;30menter/esc[22m[2m[2m[39;48;2;30;30;30m continue · [22m[1m[38;2;255;255;255;48;2;30;30;30mctrl+c[22m[2m[2m[39;48;2;30;30;30m quit[6;1H[22m [7;1H[39;49m [36;1H [37;1H [38;1H [39m[49m[0m[?2026l
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:29:52.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.157.0, plain launch on a fresh CODEX_HOME whose config.toml sets check_for_update_on_startup = true, with a version.json cache naming 0.159.0 checked moments earlier (so no network check) and CODEX_MANAGED_BY_NPM=1 so Codex offers an update command. Over the provisional screen Codex paints `Update available · 0.157.0 → 0.159.0`, `1. Update now (runs `npm install -g @openai/codex`)` / `2. Skip` / `3. Skip until next version` and the key row `enter continue · esc skip`, where older builds wrote `Press enter to continue`. Enter picks Update now. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[?2004h[>4;0m[>7u[?1004h[6n]10;?\]11;?\[?u[c[?2026h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭───────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22mloading[2m │[6;1H╰───────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;41H[2m───────────╮[2;41H │[3;41H │[4;14H [3mloading[4;24H[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;14H [22m/Users/user1/cr/rounds/dlg-u57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?1006l[?1015l[?1003l[?1002l[?1000l[<1u[?1007l[?1049l[<1u[>4;0m[?2004l[?1004l[0 q[?25hInstalling daemon from CLI version 0.157.0 into /Users/user1/cr/rounds/dlg-u57/home/.codex/packages/app-server-daemon...
|
||||
[?2004h[>4;0m[>7u[?1004h[?25l[?1049h[>4;0m[>7u[?1007l[?1000h[?1002h[?1006h[?1003h[?25l[1;1H[J[?2026h[?25l[36;2H[0m[48;2;30;30;30m[K[37;27H[0m[48;2;30;30;30m[K[38;2H[0m[48;2;30;30;30m[K[1;1H[2m[39;49m╭──────────────────────────────────────────────────╮[2;1H│ >_ [22m[1mOpenAI Codex[22m[2m[2m (v0.157.0) │[3;1H│ │[4;1H│ model: [3mloading[23m [22m[38;2;99;168;248;49m/model[2m[39;49m to change │[5;1H│ directory: [22m/Users/user1/cr/rounds/dlg-u57/repo[2m │[6;1H│ permissions: [22m[1m[38;5;5;49mYOLO mode[22m[2m[2m[39;49m │[7;1H╰──────────────────────────────────────────────────╯[36;1H[22m[39;48;2;30;30;30m [37;1H[1m›[22m [2mAsk Codex to do anything[38;1H[22m [40;3H[1m[38;2;255;255;255;49m?[22m[38;2;153;153;153;49m for shortcuts[39m[49m[0m[1;1H[0 q[1;1H[2m╭[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[39m[49m[0m[37;3H[?25h[?2026l[?2026h[?25l[1;2H[0m[48;2;30;30;30m[K[2;39H[0m[48;2;30;30;30m[K[3;65H[0m[48;2;30;30;30m[K[4;2H[0m[48;2;30;30;30m[K[6;10H[0m[48;2;30;30;30m[K[7;29H[0m[48;2;30;30;30m[K[8;2H[0m[48;2;30;30;30m[K[9;28H[0m[48;2;30;30;30m[K[10;2H[0m[48;2;30;30;30m[K[36;2H[0m[49m[K[37;2H[0m[49m[K[38;2H[0m[49m[K[40;2H[0m[49m[K[1;1H[39;48;2;30;30;30m [2;1H [1mUpdate available[22m[2m[2m · 0.157.0 → 0.159.0[3;1H[22m [2mRelease notes: [4m]8;;https://github.com/openai/codex/releases/latesthttps://github.com/openai/codex/releases/latest]8;;[4;1H[24m[22m [5;1H[1m[38;2;0;0;46;48;2;99;168;248m› 1. Update now (runs `npm install -g @openai/codex`) [6;1H[22m[39;48;2;30;30;30m 2. Skip[7;1H 3. Skip until next version[8;1H [9;1H [1m[38;2;255;255;255;48;2;30;30;30menter[22m[2m[2m[39;48;2;30;30;30m continue · [22m[1m[38;2;255;255;255;48;2;30;30;30mesc[22m[2m[2m[39;48;2;30;30;30m skip[10;1H[22m [36;1H[39;49m [37;1H [38;1H [39m[49m[0m[?2026l
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-28T21:44:00.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.158.0, plain launch (no -m/-c) on a fresh CODEX_HOME. Greeting layout: an unboxed `>_ OpenAI Codex (v0.158.0)` header with no model:/directory: labels, starting from a provisional screen whose directory reads `loading`; the live status row under the composer appears last. Braille logo animation frames included. Cut 4.6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:30:46.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.158.0, plain launch on a fresh CODEX_HOME with a hooks.json of 8 command hooks that were never trusted. Over the provisional screen Codex paints `Hooks need review`, `1. Review hooks` / `2. Trust all and continue` / `3. Continue without trusting (hooks won't run)` and the key row `enter confirm · esc skip`, where older builds wrote `Press enter to confirm`. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-28T21:44:30.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.158.0, plain launch on a fresh CODEX_HOME whose config.toml sets an older model with an available upgrade. After the provisional screen, Codex paints its model announcement (`Meet GPT-6 Sol`, `1. Try new model` / `2. Use existing model`, `enter/esc confirm`), which owns Enter. Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:31:56.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.158.0, plain launch on a fresh CODEX_HOME whose config.toml sets model_provider = \"amazon-bedrock\" and model = \"openai.gpt-5.4\" (0.158 keeps the notice for a model missing from its catalog only on the OpenAI and Bedrock providers, and the OpenAI provider asks for a sign-in first, which the rig does not have). Codex shows the notice without choices: `GPT-5.4 on Amazon Bedrock is no longer offered in Codex`, `Switch to GPT-6 Sol on Amazon Bedrock to continue.` and the key row `enter/esc continue · ctrl+c quit`. Enter or Esc switches the model. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"capturedAt": "2026-09-29T03:30:10.000Z",
|
||||
"platform": "darwin",
|
||||
"command": ["codex", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "codex-cli 0.158.0, plain launch on a fresh CODEX_HOME whose config.toml sets check_for_update_on_startup = true, with a version.json cache naming 0.159.0 checked moments earlier (so no network check) and CODEX_MANAGED_BY_NPM=1 so Codex offers an update command. Over the provisional screen Codex paints `Update available · 0.158.0 → 0.159.0`, `1. Update now (runs `npm install -g @openai/codex`)` / `2. Skip` / `3. Skip until next version` and the key row `enter continue · esc skip`, where older builds wrote `Press enter to continue`. Enter picks Update now. No key was sent (only automatic terminal-query replies). Cut 6 s after spawn. Home-directory user name redacted by hand, same length.",
|
||||
"exitCode": null
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,11 @@ import {
|
||||
replayTranscript,
|
||||
type TranscriptReplayFrame
|
||||
} from './agent-transcript-replay-test-harness'
|
||||
import { isKnownReadyPromptBody, isKnownReadyPromptPreview } from './terminal-wait-detection'
|
||||
import {
|
||||
isKnownReadyPromptBody,
|
||||
isKnownReadyPromptPreview,
|
||||
isKnownReadyPromptSettled
|
||||
} from './terminal-wait-detection'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: { fromId: vi.fn(() => null) },
|
||||
@@ -19,7 +23,9 @@ const PLAIN = 'codex-0157-plain-ready'
|
||||
const EFFORT_OVERRIDE = 'codex-0157-effort-override-embedded-warning'
|
||||
const CONFIG_OVERRIDE = 'codex-0157-config-override-embedded-warning'
|
||||
const NO_DAEMON = 'codex-0157-no-daemon-effort-override'
|
||||
const ALL_FIXTURES = [PLAIN, EFFORT_OVERRIDE, CONFIG_OVERRIDE, NO_DAEMON]
|
||||
// Fresh CODEX_HOME: the provisional header stays up while Codex installs and starts its daemon.
|
||||
const FRESH_HOME = 'codex-0157-fresh-home-daemon-install'
|
||||
const ALL_FIXTURES = [PLAIN, EFFORT_OVERRIDE, CONFIG_OVERRIDE, NO_DAEMON, FRESH_HOME]
|
||||
|
||||
async function finalFrame(
|
||||
name: string,
|
||||
@@ -41,6 +47,16 @@ function screenShowsLoadingHeader(screenLines: string[]): boolean {
|
||||
return screen.includes('openai codex') && /(?:model|directory):\s+loading/.test(screen)
|
||||
}
|
||||
|
||||
// Codex's default status row opens with `<model> <effort> ·`; only the live chat paints it.
|
||||
const LIVE_STATUS_ROW_RE = /\b(?:default|minimal|low|medium|high|xhigh) ·/
|
||||
|
||||
function screenShowsProvisionalStartup(screenLines: string[]): boolean {
|
||||
return (
|
||||
screenShowsLoadingHeader(screenLines) &&
|
||||
!screenLines.some((line) => LIVE_STATUS_ROW_RE.test(line.toLowerCase()))
|
||||
)
|
||||
}
|
||||
|
||||
describe('Codex 0.157 header readiness from captured bytes', () => {
|
||||
it.each([EFFORT_OVERRIDE, CONFIG_OVERRIDE])(
|
||||
'%s: the line-folded wait text never shows a ready header',
|
||||
@@ -71,6 +87,26 @@ describe('Codex 0.157 header readiness from captured bytes', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Why: 0.157 discards input typed during its daemon start, behind the provisional header.
|
||||
it.each(ALL_FIXTURES)(
|
||||
'%s: never ready while the screen shows the provisional `model: loading` startup screen',
|
||||
async (name) => {
|
||||
let sawTextOnlyReadiness = false
|
||||
for await (const frame of replayTranscript(readRuntimeFixture(name), 120, 40)) {
|
||||
if (screenShowsProvisionalStartup(frame.screenLines)) {
|
||||
sawTextOnlyReadiness ||= isKnownReadyPromptPreview(frame.waitText)
|
||||
expect(isKnownReadyPromptBody(frame.waitText, 'codex', () => frame.screenLines)).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
// Presence precondition for the text-copy fixtures: the text rules alone would say ready here.
|
||||
if (name === PLAIN || name === FRESH_HOME) {
|
||||
expect(sawTextOnlyReadiness).toBe(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Why these sizes: grids out of step with the 120x40 recording garble the header (review of #23475).
|
||||
describe.each([
|
||||
[120, 40],
|
||||
@@ -78,15 +114,21 @@ describe('Codex 0.157 header readiness from captured bytes', () => {
|
||||
[30, 50],
|
||||
[108, 30],
|
||||
[60, 5]
|
||||
])('at %ix%i the screen never takes readiness away from the text rules', (cols, rows) => {
|
||||
])('at %ix%i the screen never takes a settled header away from the text rules', (cols, rows) => {
|
||||
it.each(ALL_FIXTURES)('%s', async (name) => {
|
||||
let settledFrames = 0
|
||||
for await (const frame of replayTranscript(readRuntimeFixture(name), cols, rows)) {
|
||||
if (isKnownReadyPromptPreview(frame.waitText)) {
|
||||
if (isKnownReadyPromptSettled(frame.waitText)) {
|
||||
settledFrames += 1
|
||||
expect(isKnownReadyPromptBody(frame.waitText, 'codex', () => frame.screenLines)).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
// Presence precondition: the text-copy fixtures reach a settled header.
|
||||
if (name === PLAIN || name === FRESH_HOME || name === NO_DAEMON) {
|
||||
expect(settledFrames).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -175,6 +217,58 @@ describe('Codex 0.157 header readiness from captured bytes', () => {
|
||||
15_000
|
||||
)
|
||||
|
||||
it('does not settle while Codex is still installing its daemon behind the provisional screen', async () => {
|
||||
const data = readRuntimeFixture(FRESH_HOME)
|
||||
const install = data.indexOf('Installing daemon')
|
||||
// Presence precondition: the cut keeps the provisional header and stops before the live chat.
|
||||
expect(install).toBeGreaterThan(0)
|
||||
const provisional = data.slice(0, data.indexOf('\n', install) + 1)
|
||||
expect(provisional).toMatch(/model:.*loading/)
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: 'Terminal',
|
||||
foregroundProcess: 'codex',
|
||||
launchAgent: 'codex',
|
||||
data: provisional,
|
||||
size: { cols: 120, rows: 40 }
|
||||
})
|
||||
await expect(
|
||||
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2_500 })
|
||||
).rejects.toThrow(/timeout/)
|
||||
}, 15_000)
|
||||
|
||||
// Why no bytes: worker-start waits on a pane that has printed nothing yet, which is when the
|
||||
// runtime asks for a visible-screen read instead of its own text copy.
|
||||
it('does not settle from a visible-screen read of the provisional screen', async () => {
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: 'Terminal',
|
||||
foregroundProcess: 'codex',
|
||||
launchAgent: 'codex',
|
||||
data: '',
|
||||
size: { cols: 120, rows: 40 }
|
||||
})
|
||||
const readVisibleScreen = vi.spyOn(runtime, 'readTerminal').mockResolvedValue({
|
||||
handle,
|
||||
status: 'running',
|
||||
tail: [
|
||||
'╭──────────────────────────────────────────╮',
|
||||
'│ >_ OpenAI Codex (v0.157.0) │',
|
||||
'│ model: loading /model to change │',
|
||||
'│ directory: ~/repo/app │',
|
||||
'╰──────────────────────────────────────────╯',
|
||||
'› Ask Codex to do anything',
|
||||
' ? for shortcuts'
|
||||
],
|
||||
truncated: false,
|
||||
nextCursor: null,
|
||||
source: 'screen'
|
||||
})
|
||||
await expect(
|
||||
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2_500 })
|
||||
).rejects.toThrow(/timeout/)
|
||||
// Presence precondition: the visible-screen probe actually ran.
|
||||
expect(readVisibleScreen).toHaveBeenCalled()
|
||||
}, 15_000)
|
||||
|
||||
it('keeps timing out on the garbled 80x24 default grid, as before', async () => {
|
||||
const { runtime, handle } = await codexPane(EFFORT_OVERRIDE)
|
||||
await expect(
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-types'
|
||||
import { createTranscriptPane } from './agent-transcript-pane-test-harness'
|
||||
import {
|
||||
readRuntimeFixture,
|
||||
replayTranscript,
|
||||
type TranscriptReplayFrame
|
||||
} from './agent-transcript-replay-test-harness'
|
||||
import { detectTerminalWaitBlockedReason, isKnownReadyPromptBody } from './terminal-wait-detection'
|
||||
|
||||
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') }
|
||||
}))
|
||||
|
||||
type StartupDialog = {
|
||||
name: string
|
||||
reason: RuntimeTerminalWaitBlockedReason
|
||||
heading: string
|
||||
keyRow: string
|
||||
}
|
||||
|
||||
// codex-cli 0.157.0 / 0.158.0 recordings at 120x40 on a fresh CODEX_HOME (see each .meta.json).
|
||||
// Each dialog owns Enter, and none prints the `Press enter to …` wording older builds did.
|
||||
const DIALOGS: StartupDialog[] = [
|
||||
{
|
||||
// Recorded separately on 0.157.1 (STA-8834).
|
||||
name: 'codex-0-157-1-update-dialog',
|
||||
reason: 'agent-update-prompt',
|
||||
heading: 'Update available ·',
|
||||
keyRow: 'enter continue · esc skip'
|
||||
},
|
||||
{
|
||||
name: 'codex-0157-update-available-dialog',
|
||||
reason: 'agent-update-prompt',
|
||||
heading: 'Update available ·',
|
||||
keyRow: 'enter continue · esc skip'
|
||||
},
|
||||
{
|
||||
name: 'codex-0158-update-available-dialog',
|
||||
reason: 'agent-update-prompt',
|
||||
heading: 'Update available ·',
|
||||
keyRow: 'enter continue · esc skip'
|
||||
},
|
||||
{
|
||||
name: 'codex-0157-hooks-review-dialog',
|
||||
reason: 'agent-hooks-review-prompt',
|
||||
heading: 'Hooks need review',
|
||||
keyRow: 'enter confirm · esc skip'
|
||||
},
|
||||
{
|
||||
name: 'codex-0158-hooks-review-dialog',
|
||||
reason: 'agent-hooks-review-prompt',
|
||||
heading: 'Hooks need review',
|
||||
keyRow: 'enter confirm · esc skip'
|
||||
},
|
||||
{
|
||||
name: 'codex-0157-model-retired-dialog',
|
||||
reason: 'codex-model-migration-prompt',
|
||||
heading: 'is no longer available',
|
||||
keyRow: 'enter/esc continue · ctrl+c quit'
|
||||
},
|
||||
{
|
||||
name: 'codex-0158-model-retired-dialog',
|
||||
reason: 'codex-model-migration-prompt',
|
||||
heading: 'is no longer offered',
|
||||
keyRow: 'enter/esc continue · ctrl+c quit'
|
||||
},
|
||||
{
|
||||
name: 'codex-0158-model-announcement-dialog',
|
||||
reason: 'codex-model-migration-prompt',
|
||||
heading: 'Try new model',
|
||||
keyRow: 'enter/esc confirm · ctrl+c quit'
|
||||
}
|
||||
]
|
||||
const DIALOGS_0158 = DIALOGS.filter((dialog) => dialog.name.startsWith('codex-0158-'))
|
||||
const LIVE_CHAT_FIXTURES = [
|
||||
'codex-0157-plain-ready',
|
||||
'codex-0157-effort-override-embedded-warning',
|
||||
'codex-0157-config-override-embedded-warning',
|
||||
'codex-0157-no-daemon-effort-override',
|
||||
'codex-0157-fresh-home-daemon-install',
|
||||
'codex-0158-fresh-home-greeting'
|
||||
]
|
||||
|
||||
function screenText(frame: TranscriptReplayFrame): string {
|
||||
return frame.screenLines.join('\n')
|
||||
}
|
||||
|
||||
async function lastFrame(data: string): Promise<TranscriptReplayFrame | null> {
|
||||
let last: TranscriptReplayFrame | null = null
|
||||
for await (const frame of replayTranscript(data, 120, 40)) {
|
||||
last = frame
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
// Why stitched: no capture spans answering a dialog. Codex repaints every cell once a startup
|
||||
// dialog closes, so the 0.158 greeting capture's paints stand in for that repaint.
|
||||
function answered(name: string): string {
|
||||
const greeting = readRuntimeFixture('codex-0158-fresh-home-greeting')
|
||||
return readRuntimeFixture(name) + greeting.slice(greeting.indexOf('\x1b[?2026h'))
|
||||
}
|
||||
|
||||
describe('Codex 0.157/0.158 startup dialogs from captured bytes', () => {
|
||||
it.each(DIALOGS)(
|
||||
'$name: reports $reason from its key row on, and never reads ready while it is up',
|
||||
async ({ name, reason, heading, keyRow }) => {
|
||||
let headingFrames = 0
|
||||
let keyRowFrames = 0
|
||||
for await (const frame of replayTranscript(readRuntimeFixture(name), 120, 40)) {
|
||||
const screen = screenText(frame)
|
||||
if (screen.includes(heading)) {
|
||||
headingFrames += 1
|
||||
expect(isKnownReadyPromptBody(frame.waitText, 'codex', () => frame.screenLines)).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
if (keyRowFrames > 0 || screen.includes(keyRow)) {
|
||||
keyRowFrames += 1
|
||||
expect(detectTerminalWaitBlockedReason(frame.waitText)).toBe(reason)
|
||||
}
|
||||
}
|
||||
// Presence precondition: the dialog and its key row were painted, not just parsed.
|
||||
expect(headingFrames).toBeGreaterThan(0)
|
||||
expect(keyRowFrames).toBeGreaterThan(0)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(DIALOGS_0158)(
|
||||
'$name: stops reporting once Codex repaints its chat after it',
|
||||
async ({ name, heading }) => {
|
||||
const last = await lastFrame(answered(name))
|
||||
// Presence precondition: the answered dialog is still in the text copy.
|
||||
expect(last?.waitText).toContain(heading.replace(' ·', ''))
|
||||
expect(detectTerminalWaitBlockedReason(last?.waitText ?? '')).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(DIALOGS)(
|
||||
'$name: still reports $reason when Codex is relaunched in the same pane and shows it again',
|
||||
async ({ name, reason }) => {
|
||||
// Why: quitting Codex from a dialog leaves that copy in the text copy ahead of the relaunch.
|
||||
const dialog = readRuntimeFixture(name)
|
||||
const last = await lastFrame(`${dialog}\x1b[?1049l\r\n% codex\r\n${dialog}`)
|
||||
expect(detectTerminalWaitBlockedReason(last?.waitText ?? '')).toBe(reason)
|
||||
expect(
|
||||
isKnownReadyPromptBody(last?.waitText ?? '', 'codex', () => last?.screenLines ?? null)
|
||||
).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(LIVE_CHAT_FIXTURES)('%s: a live chat reports no dialog', async (name) => {
|
||||
const waitText = (await lastFrame(readRuntimeFixture(name)))?.waitText ?? ''
|
||||
expect(detectTerminalWaitBlockedReason(waitText)).toBeNull()
|
||||
// Why these lines: chat can name a dialog, and Codex's own update notice and footer draw `·`.
|
||||
const chat = [
|
||||
'› is there an update available, and do my hooks need review?',
|
||||
'✨ Update available! 0.158.0 -> 0.159.0',
|
||||
'Run npm install -g @openai/codex to update.',
|
||||
' gpt-6-astra default · ~/repo',
|
||||
' ← for agents · ? for shortcuts'
|
||||
].join('\n')
|
||||
expect(detectTerminalWaitBlockedReason(`${waitText}\n${chat}`)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not name a mid-session Codex popup a hooks review', () => {
|
||||
// Why: Codex's rate-limit reset popup (and other pickers) ends `enter confirm · esc back`.
|
||||
const popup = [
|
||||
' Use this reset?',
|
||||
' 1. Yes, use reset Reset your weekly and 5-hour usage limits.',
|
||||
'› 2. No, go back Choose a different reset',
|
||||
' enter confirm · esc back'
|
||||
].join('\n')
|
||||
expect(detectTerminalWaitBlockedReason(popup)).not.toBe('agent-hooks-review-prompt')
|
||||
})
|
||||
|
||||
describe('through the runtime', () => {
|
||||
it.each(DIALOGS)(
|
||||
'$name: stops a tui-idle wait as $reason instead of typing into it',
|
||||
async ({ name, reason }) => {
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: 'Terminal',
|
||||
foregroundProcess: 'codex',
|
||||
launchAgent: 'codex',
|
||||
data: readRuntimeFixture(name),
|
||||
size: { cols: 120, rows: 40 }
|
||||
})
|
||||
await expect(
|
||||
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2_500 })
|
||||
).resolves.toMatchObject({ satisfied: false, blockedReason: reason })
|
||||
},
|
||||
15_000
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -34,6 +34,27 @@ export function findCodexScreenReadyPromptIndex(screen: string): number | null {
|
||||
: null
|
||||
}
|
||||
|
||||
// Why the text copy: 0.157 leaves its alternate screen while it starts its daemon, so the live
|
||||
// screen shows no header then, while the text copy keeps the provisional one until the live chat
|
||||
// paints its footer after it (a later model repaint rewrites only the value, never the label).
|
||||
// Why `·`: every live footer row draws one (status row, `← for agents · ?`, `⚠ N warning · f2`);
|
||||
// startup dialogs draw one too, which is why startup-dialog-blocked-signals.ts matches them first.
|
||||
export function isCodexProvisionalStartupText(normalized: string): boolean {
|
||||
const headerIndex = normalized.lastIndexOf('openai codex')
|
||||
if (headerIndex === -1) {
|
||||
return false
|
||||
}
|
||||
const loading = /model:\s+loading/.exec(normalized.slice(headerIndex))
|
||||
return loading !== null && !normalized.includes('·', headerIndex + loading.index)
|
||||
}
|
||||
|
||||
// Why: Codex repaints its whole screen, header included, once a startup dialog closes, and the
|
||||
// dialog never draws the header; 0.158's header has no labels, so the header alone marks it answered.
|
||||
export function findCodexHeaderIndex(normalized: string): number | null {
|
||||
const index = normalized.lastIndexOf('openai codex (v')
|
||||
return index === -1 ? null : index
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 1b, codex panes only: the empty composer with no busy status row just above it and no
|
||||
* header load. Codex 0.158 dropped `model:`/`directory:`, and a long session scrolls the header
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { withTimeout } from './runtime-async-boundaries'
|
||||
import {
|
||||
detectTerminalWaitBlockedReason,
|
||||
isKnownReadyPromptPreview
|
||||
isKnownReadyPromptSettled
|
||||
} from './terminal-wait-detection'
|
||||
import type {
|
||||
RuntimeTerminalWait,
|
||||
@@ -80,7 +80,7 @@ export class OrcaRuntimeWithStartTuiIdleVisibleReadProbe extends OrcaRuntimeWith
|
||||
const ready =
|
||||
agent === 'antigravity'
|
||||
? isAntigravityReadyPromptSnapshot(snapshotText)
|
||||
: isKnownReadyPromptPreview(snapshotText)
|
||||
: isKnownReadyPromptSettled(snapshotText)
|
||||
if (!blockedReason && !ready) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-types'
|
||||
|
||||
// Why rows, from each dialog's first `·` on: codex-terminal-readiness.ts takes a `·` after the
|
||||
// startup header for the live chat's footer, so each dialog must be matched by the time that `·`
|
||||
// lands. Why not headings: Codex 0.157+ paints them by cell diff over its startup screen, so the
|
||||
// text copy can lose letters and spaces (`updat available`); these rows are fixed literals.
|
||||
// Update: `Update available · 0.157.1 → 0.158.0`, then `enter continue · esc skip`.
|
||||
const CODEX_UPDATE_ROW_RE = /available\s*·\s*\d+\.\d+|enter\s*continue\s*·\s*esc\s*skip/g
|
||||
// Why not `esc back`: Codex's mid-session pickers end `enter confirm · esc back`.
|
||||
const CODEX_HOOKS_REVIEW_KEY_ROW_RE = /enter\s*confirm\s*·(?!\s*esc\s*back)/g
|
||||
// Why both verbs: the retired-model notice says `continue`, the new-model announcement `confirm`.
|
||||
const CODEX_MODEL_MIGRATION_KEY_ROW_RE = /enter\/esc\s*(?:continue|confirm)\s*·/g
|
||||
|
||||
function lastMatchIndex(text: string, row: RegExp): number {
|
||||
let index = -1
|
||||
for (const match of text.matchAll(row)) {
|
||||
index = match.index
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Why the last match, and the legacy wording too: an answered dialog stays in the text copy, and
|
||||
// builds before 0.157 wrote `Press enter to continue/confirm`.
|
||||
function findDialogIndex(
|
||||
normalized: string,
|
||||
legacyHeading: string,
|
||||
legacyKeys: string,
|
||||
row: RegExp
|
||||
): number {
|
||||
const rowIndex = lastMatchIndex(normalized, row)
|
||||
const headingIndex = normalized.lastIndexOf(legacyHeading)
|
||||
const legacyIndex =
|
||||
headingIndex !== -1 && normalized.includes(legacyKeys, headingIndex) ? headingIndex : -1
|
||||
return Math.max(rowIndex, legacyIndex)
|
||||
}
|
||||
|
||||
// Why together: each startup dialog owns Enter before the chat exists, so a brief typed into one
|
||||
// answers it (Codex's update dialog defaults to `Update now`).
|
||||
export function findStartupDialogBlockedSignals(
|
||||
normalized: string
|
||||
): { reason: RuntimeTerminalWaitBlockedReason; index: number }[] {
|
||||
const candidates: { reason: RuntimeTerminalWaitBlockedReason; index: number }[] = []
|
||||
const updateIndex = findDialogIndex(
|
||||
normalized,
|
||||
'update available',
|
||||
'press enter to continue',
|
||||
CODEX_UPDATE_ROW_RE
|
||||
)
|
||||
if (updateIndex !== -1) {
|
||||
candidates.push({ reason: 'agent-update-prompt', index: updateIndex })
|
||||
}
|
||||
const cwdIndex = normalized.lastIndexOf('choose working directory to')
|
||||
if (cwdIndex !== -1 && normalized.includes('press enter to continue', cwdIndex)) {
|
||||
candidates.push({ reason: 'agent-cwd-prompt', index: cwdIndex })
|
||||
}
|
||||
const modelMigrationIndex = findDialogIndex(
|
||||
normalized,
|
||||
'codex just got an upgrade',
|
||||
'press enter to continue',
|
||||
CODEX_MODEL_MIGRATION_KEY_ROW_RE
|
||||
)
|
||||
if (modelMigrationIndex !== -1) {
|
||||
candidates.push({ reason: 'codex-model-migration-prompt', index: modelMigrationIndex })
|
||||
}
|
||||
const hooksIndex = findDialogIndex(
|
||||
normalized,
|
||||
'hooks need review',
|
||||
'press enter to confirm',
|
||||
CODEX_HOOKS_REVIEW_KEY_ROW_RE
|
||||
)
|
||||
if (hooksIndex !== -1) {
|
||||
// Why neutral: this matcher never inspects the agent -- 'hooks need review' is not Codex-only wording.
|
||||
candidates.push({ reason: 'agent-hooks-review-prompt', index: hooksIndex })
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
@@ -9,9 +9,12 @@ import type { RuntimeTerminalWaitBlockedReason } from '../../shared/runtime-type
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { findAntigravityReadyPromptIndex } from './antigravity-terminal-readiness'
|
||||
import {
|
||||
findCodexHeaderIndex,
|
||||
findCodexScreenReadyPromptIndex,
|
||||
isCodexComposerReadyScreen
|
||||
isCodexComposerReadyScreen,
|
||||
isCodexProvisionalStartupText
|
||||
} from './codex-terminal-readiness'
|
||||
import { findStartupDialogBlockedSignals } from './startup-dialog-blocked-signals'
|
||||
import { startOfLastLines, startOfLastNonBlankLines } from './terminal-wait-tail-window'
|
||||
|
||||
const EXPLICIT_IDLE_TITLE_RE = /(^|\s)(ready|idle|done)(\s|$|[.!?])/i
|
||||
@@ -52,6 +55,19 @@ export function isKnownReadyPromptPreview(preview: string): boolean {
|
||||
return isReadyPromptUnblocked(normalized, findKnownReadyPromptIndex(normalized))
|
||||
}
|
||||
|
||||
/**
|
||||
* The ready-prompt text rules for a pane about to take input. Unlike isKnownReadyPromptPreview
|
||||
* (agent presence), Codex's provisional startup header does not count: 0.157 discards input typed
|
||||
* behind it while its daemon starts.
|
||||
*/
|
||||
export function isKnownReadyPromptSettled(preview: string): boolean {
|
||||
const normalized = preview.toLowerCase()
|
||||
return (
|
||||
isReadyPromptUnblocked(normalized, findKnownReadyPromptIndex(normalized)) &&
|
||||
!isCodexProvisionalStartupText(normalized)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 1 body evidence for every tui-idle site. `readScreenLines` yields the live emulator's
|
||||
* visible grid, or null when the runtime has no trustworthy one.
|
||||
@@ -69,7 +85,7 @@ export function isKnownReadyPromptBody(
|
||||
if (agent === 'qoder') {
|
||||
return isQoderComposerReady(readScreenLines())
|
||||
}
|
||||
if (isKnownReadyPromptPreview(waitText)) {
|
||||
if (isKnownReadyPromptSettled(waitText)) {
|
||||
return true
|
||||
}
|
||||
// Why the agent gate: another agent's screen can merely mention "OpenAI Codex".
|
||||
@@ -139,6 +155,7 @@ export function findActionableTerminalWaitBlockedSignal(
|
||||
function findDismissedStartupModalIndex(normalized: string): number | null {
|
||||
const indexes = [
|
||||
findCodexReadyPromptIndex(normalized),
|
||||
findCodexHeaderIndex(normalized),
|
||||
findAntigravityReadyPromptIndex(normalized),
|
||||
findCursorActivePromptIndex(normalized),
|
||||
findMuseReadyPromptIndex(normalized)
|
||||
@@ -199,7 +216,7 @@ function findCodexReadyPromptIndex(normalized: string): number | null {
|
||||
}
|
||||
|
||||
export const TERMINAL_WAIT_BLOCKED_SENTINEL_RE =
|
||||
/update available|choose working directory to|codex just got an upgrade|hooks need review|do you trust|trust this|trusted workspace|press enter to (?:confirm|continue|view|insert)|press t to trust|permission required|requires permission|allow once|allow always|run this command\?/i
|
||||
/update available|choose working directory to|codex just got an upgrade|available\s*·|esc\s*skip|enter\s*confirm\s*·|enter\/esc\s*(?:continue|confirm)|hooks need review|do you trust|trust this|trusted workspace|press enter to (?:confirm|continue|view|insert)|press t to trust|permission required|requires permission|allow once|allow always|run this command\?/i
|
||||
|
||||
// Why text at all: cursor-agent has no approval hook, so the key-bound menu is the only authority.
|
||||
const CURSOR_APPROVAL_CHOICE_MARKERS = [
|
||||
@@ -268,27 +285,7 @@ function findTerminalWaitBlockedSignal(
|
||||
function findBlockedSignalInLiveWindow(
|
||||
normalized: string
|
||||
): { reason: RuntimeTerminalWaitBlockedReason; index: number } | null {
|
||||
const candidates: { reason: RuntimeTerminalWaitBlockedReason; index: number }[] = []
|
||||
const updateIndex = normalized.lastIndexOf('update available')
|
||||
if (updateIndex !== -1 && normalized.includes('press enter to continue', updateIndex)) {
|
||||
candidates.push({ reason: 'agent-update-prompt', index: updateIndex })
|
||||
}
|
||||
const cwdIndex = normalized.lastIndexOf('choose working directory to')
|
||||
if (cwdIndex !== -1 && normalized.includes('press enter to continue', cwdIndex)) {
|
||||
candidates.push({ reason: 'agent-cwd-prompt', index: cwdIndex })
|
||||
}
|
||||
const modelMigrationIndex = normalized.lastIndexOf('codex just got an upgrade')
|
||||
if (
|
||||
modelMigrationIndex !== -1 &&
|
||||
normalized.includes('press enter to continue', modelMigrationIndex)
|
||||
) {
|
||||
candidates.push({ reason: 'codex-model-migration-prompt', index: modelMigrationIndex })
|
||||
}
|
||||
const hooksIndex = normalized.lastIndexOf('hooks need review')
|
||||
if (hooksIndex !== -1 && normalized.includes('press enter to confirm', hooksIndex)) {
|
||||
// Why neutral: this matcher never inspects the agent -- 'hooks need review' is not Codex-only wording.
|
||||
candidates.push({ reason: 'agent-hooks-review-prompt', index: hooksIndex })
|
||||
}
|
||||
const candidates = findStartupDialogBlockedSignals(normalized)
|
||||
const trustIndex = Math.max(
|
||||
normalized.lastIndexOf('do you trust'),
|
||||
normalized.lastIndexOf('trust this'),
|
||||
|
||||
Reference in New Issue
Block a user