Repository navigation
fix(codex): a message your Codex hook blocks no longer leaves the chat Working forever - #25049
Draft
brennanb2025 wants to merge 26 commits into
Draft
brennanb2025 wants to merge 26 commits into
brennanb2025 wants to merge 26 commits into
Conversation
…ered once the thread stops running
…de typecheck holds
…ook's reason when its turn completes, not at idle
…x on its answer too
…rson, never re-sent into the same hook
…reason and message, and never blinks out or vanishes on the phone
…ut an emoji right on the cut
Contributor
Author
…son apart, and a blocked card names the chat's agent
…2025/pending-steer-settles-at-idle # Conflicts: # src/shared/structured-agent-session-message-projection.ts # src/shared/structured-agent-session-outbox.ts
…-barrier' into brennanb2025/pending-steer-settles-at-idle # Conflicts: # mobile/src/session/use-mobile-structured-agent-session-undelivered-rows.test.tsx # src/shared/structured-agent-session-failed-start-elsewhere.ts # src/shared/structured-agent-session-message-projection.ts
… idle, not Failed The person's own hook refused it and the reason shows in place: nothing failed, so the turn it was blocked in is the session's verdict. A failed start whose tries ran out still reads Failed.
…2025/pending-steer-settles-at-idle # Conflicts: # src/main/codex/codex-structured-dispatch-echo.ts # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
…o reason shown The person's own hook refused it, so nothing failed to show: every client draws it as a sent message, with no notice and no Retry. The hook's reason stays on the rejection as a diagnostic. The hook sentences, their catalog keys, the quoting helper move and the agent name on returned cards go with the words.
…2025/pending-steer-settles-at-idle
…2025/pending-steer-settles-at-idle
…2025/pending-steer-settles-at-idle # Conflicts: # src/main/codex/codex-structured-dispatch-echo.ts # src/main/codex/codex-structured-turn-end-settlement.ts
…n as sent, so its text no longer also returns to the draft
…2025/pending-steer-settles-at-idle
… settles at the turn's end while the turn runs on
…ivery test typechecks
…ed it leaves as sent, never flashing as not sent
This branch has not been deployed
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.










Stacked on #25028 (base branch
brennanb2025/in-doubt-message-no-barrier, itself stacked on #24340). Retarget tomainonce those land, and before this is marked ready.This PR changes 12 code files, and tests:
src/main/codex/:codex-structured-prompt-block.ts(new)codex-structured-dispatch-echo.tscodex-structured-turn-end-settlement.tscodex-structured-session-adapter.tssrc/shared/:agent-session-failure.tsagent-session-failure-words.tsstructured-agent-session-dispatch-rejection.tsstructured-agent-session-latest-request.tsstructured-agent-session-message-projection.tsstructured-agent-session-send-disposition.tsmobile/src/session/:mobile-structured-send-delivery.tsELI5
You send a message to Codex, and a Codex hook blocks it. That might be a hook in your Codex settings, the repository's, a plugin's, or one your organization manages, for example one that refuses prompts containing a secret. Codex drops the message and finishes the turn. Orca never noticed, so the chat showed Working, with a Stop button, for as long as that Codex process lived.
After this change, the chat goes idle when that turn finishes, and the next message goes out normally. The blocked message stays in the chat as a sent message: your hook refused it, so nothing failed.
What Changed
The problem.
pendingwhen it hands it to Codex. Only Codex's echo of the message (the user message carrying Orca's id for it) marks it delivered.completedwithout that echo, Orca leaves the messagepending(codex-structured-turn-end-settlement.ts). Codex echoes every message it records before the turn completes, so normally nothing is lost.pendingmessage counts as work still owed. The chat, the sidebar and every session list therefore read Working until the Codex process ends or the chat is reopened.turn/completed, it sends ahook/completedframe:eventNameisuserPromptSubmitand itsstatusisblocked(orstopped);warningentry;feedback(orstop) entry.What you see now.
The mechanism. It applies to Codex only and persists nothing beyond the message's own record.
readCodexPromptBlock(newcodex-structured-prompt-block.ts) reads ahook/completedframe for the primary thread whose run isuserPromptSubmitwith statusblockedorstopped.warningtext, then the firstfeedbackorstoptext.blockPrompt).takePromptBlock). It is bounded like the registry's other per-turn records and cleared with the session.completedend, the existing turn-end settlement settles every send bound to the turn that Codex never echoed. Each isrejectedwith a new failure kind,hookBlocked, carrying the hook's reason as a diagnostic on the fact. No client shows it.rejectionDrawnAsSent(ahookBlockedrejection that isn't a queued card's), draws the message as sent: fix(native-chat): show a message Orca accepted and then failed to deliver as "Not sent" in the chat #24710's in-place drawing skips it on the desktop, and the shared projection keeps its row as a plain message on both the desktop and the phone. With no in-place row, no notice or Retry is attached.disposeStructuredAgentSessionSendResultlets the copy go as it does for a delivered one, so it never shows as not sent while the row loads. Both clients use the same shared rule,rejectionDrawnAsSent.mobileStructuredSendDeliveryreports it as sent, so it isn't shown twice, once as a sent message and once as returned draft text.structured-agent-session-latest-request.ts), so the turn it was blocked in is the verdict.failure, so a queued card it blocked is returned rather than re-queued.Why
pendinguntil its echo and is delivered.Differences from the common pattern
Linked Issue
No issue. Found while investigating a chat stuck on Working after a Stop, during the work on #24864 and #25028.
Visual Proof
After (this PR, head 4279084): a blocked message is drawn as a plain sent message, the chat goes idle, and the sidebar doesn't read Failed. These come from live QA on 2026-10-05: a hidden dev build on a second Mac with an isolated profile, plus the Orca Mobile dev client on an iOS simulator paired to it over LAN. Grok drove the desktop. The phone was driven through Orca's emulator tooling. Codex was a stand-in whose UserPromptSubmit hook blocks any prompt containing "BLOCKME", sending Codex's frames in Codex's real event order: the hook reports blocked, no user message is recorded, and the turn ends.
Phone: the opening message "Please BLOCKME now" is drawn as a plain sent message, with no reason sentence, no "Not sent" and no Retry. The chat is idle, and the blocked text did not come back into the composer.
Phone: a later message goes out and is answered.
Desktop, same chat: plain and idle. The sidebar row is green, not Failed.
Desktop: a follow-up steered into a running turn and blocked by the hook ends that turn at once ("Worked for 1s"). It is drawn after the turn, as a plain message that never gets an answer, because nothing answered it. The chat goes idle and the sidebar row stays not Failed. A 50 ms screen sampler saw no reason sentence, "not sent", Retry or duplicate at any sample.
Phone: the blocked steer appears in the same place.
Desktop: the next message goes out and is answered.
Unrelated to this PR: the desktop shots show main's notice "Orca now runs Codex without its shared server…".
Before (
main, at 182c1a4, before #24710 was merged; not re-run on currentmain): stuck on Working.main: the blocked prompt leaves the chat on Working, with Stop.main: the next message goes out, but the chat stays on Working.main: a blocked follow-up looks sent, and the chat stays on Working.Testing
New host-level tests in
src/main/runtime/structured-agent-session-codex-hook-blocked-send.test.ts. They use the shipped runtime, host, journal and Codex adapter; only the Codex child is a fake.A hook-blocked steer, with a reason and no echo, in a turn that completes:
hookBlocked, the hook's reason kept on the fact;A blocked steer and an accepted steer in one batch: the turn runs on, the accepted steer is delivered and answered, and the blocked one stays
pendinguntil the turn's end. Then it settleshookBlocked, drawn as sent with no notice or Retry, the sidebar not Failed and the chat idle.A hook-blocked message that opened its turn: the same.
A block with no reason (
stopped, no entries): no reason kept.A long reason with markup, control characters, bidi characters, U+061C and an emoji right at the cut: kept plain, one line, at most 300 units, no half emoji, ending with "…".
Codex's own example (a
warning"go-workflow must start from PlanMode" and astop"prompt blocked"): both kept, warning first.Three hooks blocking in one turn (a reason, another reason, none): the first reason stands.
Two hooks in one turn, the first only speaking to the person ("Heads up") and the second blocking with a reason: both kept.
No block report (a hook that let the prompt through, and a block in another turn) with a completed turn that never echoed the send: it stays
pending, as today.A send whose steer is refused and that falls through to
turn/start, with the previous turn's end and the thread idle arriving before the new turn opens: it stayspendingand is delivered by its echo.An echoed steer: delivered, unchanged.
The host's records, read by the desktop and the phone:
On the desktop, a hook-blocked message is drawn as sent with no notice and no Retry, whether another device sent it or this desktop still holds its copy (
structured-agent-session-hook-blocked-message.test.ts).The sidebar reads the turn, not Failed, for a blocked steer and a blocked first message; a failed start whose tries ran out still reads Failed (
structured-agent-session-latest-request.test.ts).On the desktop, a send whose answer says its hook blocked it leaves no not-sent copy and no notice before its row loads, and is drawn once as sent after (
src/shared/structured-agent-session-send-disposition.test.ts).The phone reports a send whose answer says its hook blocked it as sent, so its text doesn't also come back to the composer (
mobile/src/session/mobile-structured-send-delivery.test.ts).The phone's own chat hook draws a blocked message as sent and still hides other rejected messages (
mobile/src/session/use-mobile-structured-agent-session-rejected-rows.test.tsx).Ablations, each production change reverted once:
The echoed-steer test is a guard and passes either way.
Desktop and host, after merging fix(native-chat): a message whose delivery is in doubt no longer holds every later send #25028's current head: 813 test files (8,735 tests) pass, covering this PR's tests, fix(native-chat): a message whose delivery is in doubt no longer holds every later send #25028's and fix(native-chat): a failed start is shown on its message and retried, and later messages go ahead #24340's, every Codex adapter and runtime test, and main's native-chat, journal, runtime and shared tests.
Phone: 218 test files (2,079 tests) pass.
Static checks:
tscfor node, web and the phone app, and the phone's test typecheck ratchet;Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
hookBlockedis a new failure kind in a persisted row. Every client also gets the host's generic sentence beside it ("The provider did not accept this message."), and that is what an older client shows.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)