Repository navigation
feat(codex): raise codex approval, question and trust dialogs as BLOCKED - #624
Merged
Merged
Conversation
Codex is the one CLI Codeman launches with its approvals on by default, and it declares `hooks: 'none'`, so nothing tells Codeman it stopped to ask. A codex session parked on "Would you like to run the following command?" read as plainly idle, and one parked on a question read as WORKING, because the question's own key hints end in `esc to interrupt`, codex's working line. This adds a generic, optional registry field, `capabilities.workDetect.blockedLine` (plus `blockedLines`, the window), and the session state that reads it: - `isBlockedScreen()` (session-activity.ts, pure) joins the last `blockedLines` non-blank rows of a capture and tests the pattern once, so a pattern can demand several markers and anchor the dialog's footer on the end of the window. - `Session` reads it off the same capture the idle confirmation already takes, emits `blockedChanged`, and exposes `screenBlocked`. It raises on one reading, and while a dialog is up re-reads the screen every 2 s, because answering resumes the turn and a busy pane would not reach the settle path again until the turn ended. It clears only after two readable captures in a row without the dialog, so a capture taken mid-repaint cannot clear and re-raise it; an unreadable capture changes nothing. A blocked screen wins over the working line in the same capture. - The pattern goes through `compileVersionRegex()` in both the schema refine and at use (lazily cached), like every other config regex. Codex's pattern was measured on a live codex-cli 0.162.0 pane on 2026-10-14 (`-a on-request -s read-only`, widths 200 down to 36): it needs a `› N.` option row and then the dialog's key-hint row as the last row, allowing for that row to wrap below about 60 columns. Command, edit and network-escalation approvals, single and multi-question dialogs and the startup trust prompt match; the idle and working views, menus the user opened (`/model`, `/permissions`, `/review`), the composer holding the dialogs' words and a transcript quoting them verbatim do not. `Implement this plan?` is left out on purpose: its footer is the same as `/model`'s and the turn has ended there. The captures are kept as fixtures (trailing whitespace trimmed, the scratch path replaced). Nothing consumes `blockedChanged` yet; the next commit raises the alert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A codex dialog the session now reads off its screen gets exactly what a Claude `permission_prompt` hook gets, so every surface that already handles that event needs no second path: `web/screen-blocked.ts` turns the session's `blockedChanged` edge into the `blocked` wait signal, an Approvals Inbox item, the `hook:permission_prompt` broadcast (with `source: 'screen'`), web push and webhooks, and a run-summary row. That lights the red "needs you" tab alert and desktop notification, the phone overview's NEEDS YOU row and the TUI. When the dialog leaves the screen the item it opened is resolved, and `approval:resolved` clears every client. It is an ALERT and never a keystroke, following #610's shape for DeepSeek: the item is `answerInTerminal`, push carries no `approvalId` (so no Approve/Deny), and the answer route refuses it. The dialog was read from a screen the agent writes most of, so a forged match must cost a false alert and nothing more. The message every surface shows is built from the CLI's registry label, never from the dialog's text. The item gets no `capture` callback, only its frame as context, because the inbox's option parser and staleness sweep are shaped for Claude's dialogs and the session owns this item's lifetime. A CLI whose hooks report blocked gets nothing from the screen, so declaring both could never alert twice. The answer route now checks `answerInTerminal` BEFORE its hooks gate and names the CLI: a codex session has no hooks, and "Session mode cannot have pending approvals" would be false about the item the caller holds. Wait signals: `blocked` is available where hooks report it OR a CLI declares `blockedLine` (`blockedSignalAvailable()`); `stop` stays hooks-only, since nothing on a screen says a turn ended, so `until=stop` on codex is still a 400 (now suggesting `idle, blocked or exit`). A screen-read dialog is a level the server keeps reading, so a `GET .../wait` that asks for `blocked` while one is up answers at once; a caller that asked for `idle` alone keeps getting `idle`. The MCP and `codeman agent` help text say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Document what the two previous commits changed for users and for whoever adds the field to another CLI: - docs/cli-registry.md: the `blockedLine`/`blockedLines` fields, how codex's pattern was measured, and the three rules any declaring CLI keeps (end on chrome only the CLI draws, alert only; raise on one reading, clear on two; hooks win). - docs/architecture-invariants.md: a "Screen-read blocked dialogs (codex)" entry, plus the wait-signal, idle-detection, watching and Approvals Inbox entries it changes. - docs/api-reference.md, docs/extending-codeman.md and the wiki (Agent CLIs, Notifications and Approvals, Hooks, HTTP API, Driving Codeman From An Agent): `blocked` now fires for codex, `stop` still does not, codex approvals are alert-only cards answered in the terminal. - CLAUDE.md: `stop` and `blocked` are no longer one rule, the config-regex list gains `blockedLine`, and a short pointer that a codex dialog match is only ever an alert. - The codeman skill's signal table and verbs note, mirrored into plugins/codeman by scripts/sync-plugin.mjs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
While a codex dialog was up, the session re-read its screen every 2 s with a synchronous `tmux capture-pane`, for as long as the dialog stayed up. That read costs about 8 ms of blocked event loop (measured 2026-10-14), and an approval can sit unanswered overnight: 1,800 execs an hour per waiting session, measured as 300 in 10 quiet minutes in the unit test. A dialog leaves the screen only through a repaint, and tmux sends every repaint to the session's client as PTY output. So while the last reading saw the dialog and nothing has been drawn since, it is still up and the read is skipped; a safety-net read still runs every 30 s (BLOCKED_QUIET_RECHECK_MS). The clear keeps its 2 to 4 s latency, because the answer itself is output. Never skipped once a reading has missed the dialog: tmux updates the pane before it sends the repaint, and the confirming read must not wait for output that may already have arrived. The session-state tests now change the fake screen the way a real pane changes (with output), and two new tests pin the skip and the confirming read (the second fails with the guard removed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`GET .../wait` answers `blocked` at once for a codex pane showing a dialog, because a screen-read dialog is a level the server is still reading. The duplicate branch of `POST .../input` (a tagged retry after a dropped connection, answered from current state) still used the plain `currentSignalFor()`, so a retry with `wait: "blocked"` on a pane the first delivery had already blocked waited out its whole timeout for an edge that had passed. It now uses `currentSignalForWait()` like the GET route; a caller that asked only for `idle` still gets `idle`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…blocks The README (both languages) still said only claude and deepseek sessions emit `blocked` and that codex accepts only idle, working and exit, and the skill's Flow 5 called `blocked` claude-mode only. Codex now raises it from its screen-read dialogs; the recipe says to watch it with `until=blocked,exit` (codex has no `stop`) and to answer in the terminal. The plugin mirror is regenerated with scripts/sync-plugin.mjs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N
pushed a commit
that referenced
this pull request
Oct 16, 2026
On codex-cli 0.162.0, with 0.163.0 released, every launch first shows the
"Update available" prompt, and choosing Skip leads to a variant of the
workspace trust prompt: "2. Back to Agent Command Center" over the key-hint
row "enter continue · esc back". The pattern only accepted cancel,
interrupt, quit, answer or all as the closing word, so a fresh git repo
launched from Codeman sat on that trust prompt reading idle, with no red
alert and no blocked signal (measured live on a throwaway instance).
Adding "back" is safe because the hint row must still START with one of
the anchored hints ("enter continue" here): the popups the user opens at
the keyboard (/model, /permissions, /review, Implement this plan?) end in
"esc back" too, but their row starts with "enter select", so they stay
unmatched, as every existing negative fixture confirms. The new fixture is
the live capture with the scratch path replaced.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N
pushed a commit
that referenced
this pull request
Oct 16, 2026
feat(codex): raise codex approval, question and trust dialogs as BLOCKED Conflict resolved against #634 in src/session.ts _probePaneWorking(): #624's assignment (a dialog on screen is never work) is kept, and #634's trust-dialog verdict block (log the hold once, remember the reading) follows it, before _readWatching(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Codeman shows a session as BLOCKED (a red tab, a desktop notification, web push, webhooks, the phone overview's NEEDS YOU row, an Approvals Inbox card and the
blockedwait signal) when an agent stops to ask the user something. Until now that came only from Claude Code's hooks and the DeepSeek Harness status bridge. Codex has no hooks, yet it is launched with its approvals on by default. So a codex session waiting on "Would you like to run the following command?" looked plainly idle, and one waiting on a question looked WORKING: the question's key hints containesc to interrupt, which is codex's working line. This PR reads codex's dialogs off its screen and raises exactly what a Claudepermission_prompthook raises, as an alert only. Codeman never answers a codex dialog. It does not touch Claude or DeepSeek behavior, the inbox's answer flow, or any other CLI. Codex'sstopsignal stays a 400.How it works
capabilities.workDetect.blockedLine(a regex), plusblockedLines(window size, 1 to 24). It is validated throughcompileVersionRegex()in the schema refine and again when first used. Only codex declares one. Its pattern needs a› N.option row followed by the dialog's own key-hint row as the LAST row of the window (allowing for that row to wrap at narrow widths). It was measured on live codex-cli 0.162.0 at widths 200 to 36, and 31 sanitized captures are checked in as fixtures.isBlockedScreen()(pure,src/session-activity.ts) is tested against the capture the idle confirmation already takes. The state raises on one reading and emitsblockedChanged. A blocked screen wins over the working line in the same capture. While a dialog is up a timer re-reads every 2 s, but skips the read while the pane has drawn nothing since the last reading that saw the dialog. A dialog can only leave through a repaint, and every repaint reaches the session as output. A safety-net read still runs every 30 s. The state clears after 2 readable captures in a row without the dialog, and an unreadable capture changes nothing.src/web/screen-blocked.tsturns the edge into the hook route's outputs:notifySignal('blocked'), ananswerInTerminalinbox item with the frame as context,hook:permission_promptwithsource: 'screen', push and webhook (noapprovalId, so no Approve/Deny), and a run-summary row. The clear resolves only the item it opened. The message every surface shows is built from the registry label, never from screen text.blockedSignalAvailable()(hooks OR blockedLine) lets codex acceptuntil=blocked. Because a screen-read dialog is a level,GET .../waitand a duplicate send-and-wait answerblockedat once for a caller that asked for it. A caller that asked only foridlestill getsidle.Testing
npm testgate: 569 files passed, 1 skipped; 11,328 tests passed, 12 skipped. Typecheck, lint, format, the CLI catalogue check and the plugin mirror check are clean.~/.codex/config.tomlhash unchanged):wait?until=blockedresolved in 7.2 s, and the tab turned red in the browser. Answeringycleared it about 3 s later, the file was created, and nothing re-fired.idle, not working, while the question was up, and it cleared 2.1 s after Enter.blockedafter 4.5 s. Deleting the session resolved the alert assession_ended.Reviewing the diff
About 2,900 added lines, most of them mechanical:
test/fixtures/codex-dialogs/*.txt: about 1,300 lines of captured screens.docs/,docs/wiki/, both READMEs,CLAUDE.md, the skill reference and its plugin mirror, whichscripts/sync-plugin.mjsgenerates).About 300 lines of logic need real review. Start with
isBlockedScreen()and the constants insrc/session-activity.ts, then the_probePaneWorking,_noteBlockedReading,_armBlockedRecheckand_clearScreenBlockedblock insrc/session.ts. Then readsrc/web/screen-blocked.tsand its wiring insrc/web/session-listener-wiring.ts, the wait changes insrc/web/session-wait-registry.tsandsrc/web/routes/session-routes.ts, and the reordered guard insrc/web/routes/approval-routes.ts. The codex regex sits insrc/config/cli-registry/stock.ts, under a comment that records what was measured.Risk
Follow-ups
codeman tuishould hide answer keys for alert-only items (same today for DeepSeek).Implement this plan?, which is excluded on purpose because its footer matches/model.