Repository navigation
fix(codex): release chats after a hook refuses a prompt - #28630
brennanb2025 wants to merge 5 commits into
Conversation
|
A Codex prompt hook could block a message without recording it, then finish the turn. Orca kept waiting for that message, leaving the chat Working and blocking the next send. At Three review rounds, architecture checks, readiness and a separate resource audit are complete. The final trim passed 76 targeted tests, and all required CI checks passed. A test expecting acknowledgment after completion was corrected to Codex's actual ordered events. One opening-status concern assumed an order the real host does not produce; we removed that extra code and artificial test. No code finding remains open. Hidden M4Air Electron QA used the real composer and an isolated deterministic Codex stand-in. The final head showed the blocked opening as Failed, accepted the next message as Done, and kept an accepted turn Done when its steered message was refused. The full-window screenshots below were inspected. Native chat was explicitly enabled for QA; real Codex, setup-script parity, other operating systems, SSH interruptions and phone interaction remain coverage gaps. Existing fresh-profile notices differ between screenshots, including the macOS notification-permission notice after refusal; no notice code changed. Local checks still report the documented old-base clock argument, stale Spanish entries and local Bun runner mismatch. The PR remains draft; main has not been synchronized. The remaining step is the agreed single main sync and its final checks before merge. Main: completed blocked turn remains Working. Final head: policy refusal and Failed status. Final head: the following accepted message finishes normally. Final head: refused steer with the accepted turn still Done. |
A UserPromptSubmit hook's verdict carries no client submission id, so its reason is shown only when the turn refuses exactly one unconfirmed send; otherwise the generic refusal stands. The reason falls back from feedback/stop to error, warning, then the run's status message. The hook-refused queued card returns with that reason and holds the card behind it.
|
Closing this in favor of a root-cause refactor instead of another handler. Why. This PR stops a chat from staying on Working after a Codex hook blocks a prompt. It does that by adding one more turn-end rule to the code that matches Codex's answers and echoes in memory. That is the latest of several fixes with the same shape: a chat stays Working because a message's delivery was never settled. Each fix closed one more way a send can stay pending: a completed turn with no echo, an answer naming no turn, an idle signal arriving before a timeout was recorded, a lost write, and an evicted cache entry. The shared cause is that Working depends on whether a message was delivered, and delivery outcomes are set by event handlers instead of derived from saved facts. What replaces it. A planned refactor makes Working come only from the running turn and the messages the agent still holds, and derives each message's outcome from the facts Codex itself reported. This PR's turn-end rule and its refused-steer display fix carry over into that work. The commits stay on this branch for reference. |




ELI5
Codex lets people add prompt hooks: small scripts that check each message before Codex accepts it. If a hook blocked a message, the Orca chat stayed on "Working" forever, and any messages queued behind it never went out. The only way out was to stop or restart the chat.
After this change, the blocked message shows as not sent as soon as Codex finishes that turn, and the chat goes idle. When the hook gave a reason and Orca can tell which message it was about, the reason is shown on that message. A queued message the hook blocked comes back as a returned card, and the cards behind it wait until you act on it, the same as any other refused card.
What Changed
The problem. Orca counts a Codex message as sent only when Codex reports that it recorded the message in the conversation history. That report carries the message's id. A blocking hook runs before Codex records anything, so that report never arrives. Codex still finishes the turn normally. Before this PR, Orca kept a message from a normally finished turn pending in case the report came late. Because nothing ever cleared it, the chat kept owing work and read "Working" for good.
User-facing change.
Mechanism. Codex records accepted input, and sends the report that names it, before it reports the turn finished. A failed turn does this after its error and before its end. This order holds in Codex 0.136 (Orca's minimum) and in later releases through current. So when a turn ends, every message bound to it with no report is settled:
When Codex answers a send after the turn it names has already ended, the send gets the same refusal, in the generic wording. A message Codex already reported recording stays sent. A failed turn that reports an error, and then records its accepted input before it ends, still marks that input sent: the error alone does not end a turn Codex opened. Both cases have tests.
The hook reason is kept per turn in the existing Codex dispatch bookkeeping and dropped when that turn ends. The list is capped at 64 entries, like the turn ends kept next to it.
Why
A turn that has ended can't still owe a message it never recorded. Settling that at the existing turn boundary removes the stuck state without a timer, a retry, a durable flag or a client-specific rule. The missing receipt decides the refusal, so a hook that gives no reason is handled too. The hook text is only used to explain the refusal. It never decides whether a message was delivered.
Considered and rejected: counting a message as sent when Codex answers the send request. Stop clears messages Codex accepted but had not yet recorded, so under that rule a message lost to a Stop would show as sent.
Linked Issue
Refs #27658 (closed; reproduced and verified here).
Visual Proof
N/A: no UI changes. The refused message and the returned card use the existing refusal display. The new regression test runs the real host, journal, queue and Codex adapter; only the Codex process is faked.
Testing
orca-ci-checkson Linux passes oxlint, both code-quality audits, the changed-code gates, the ratchets and the other static gates; formatting is clean. Thetc:nodetypecheck shows one error:structured-agent-session-message-options.ts:30(missingnow). That file isn't touched here. The error comes from this branch's main base (294927e0978) and is already fixed on main by fix(i18n,test): drop setup-only keys and fix the default-tab test #28543 left red #28603. The Bun-based typecheck runner and the localization check couldn't run there (no Bun installed); this PR touches neither.Review
Self-reviewed for: settlement order, thread and turn isolation, messages that were already sent, late answers, the failed-turn path, hook-reason attribution when one turn refuses several messages, and the cap on the hook-reason list.
Agent skill upstream boundary
Notes
Differences from the common pattern
Mixed versions. An older client on a newer host gets the existing
providerRejectedrefusal record and its full sentence. A newer client on an older host keeps the old behavior until that host updates. This adds no new field, operation or capability. The phone reads the same refusal record.SSH and folder workspaces. Only frames the execution host's Codex reports can settle a send. Losing the connection is never treated as a turn ending. Folder workspaces use the same host path. There is no platform-specific logic.
Author: Brennan Benson (X: @BrennanKB5)
Checklist