Skip to content

fix(codex): only Stop ends a hooked Codex turn, and its question box is a wait - #71

Open
Stener1 wants to merge 1 commit into
cosscom:mainfrom
Stener1:codex-turns
Open

Stener1 wants to merge 1 commit into
cosscom:mainfrom
Stener1:codex-turns

Conversation

@Stener1

@Stener1 Stener1 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Split out of #7, as asked there, and rebased on main. It no longer depends on turn check.

Codex turns showing Done while Codex is still working: three Codex signals Shipyard was missing or misreading, found with a hook probe on Codex CLI 0.160.1.

  1. Codex's notify fires at the end of each assistant message, not the turn. In one turn: notify at 13:58:09, a PreToolUse at 13:58:10, the real end (Stop) at 13:58:15. Both map to agent.finished, so a long Codex turn read as finished after its first message and stayed that way while Codex worked.
  2. Codex has PreToolUse / PostToolUse hooks; integrations install codex didn't write them. The adapter already mapped PostToolUse.
  3. Codex's question box is a tool. Asking the user fires PreToolUse with tool_name: "request_user_input_async" (or request_user_input in Plan mode, which blocks).

Change

  • Installer: also writes Codex PreToolUse and PostToolUse. An install without them reads as outdated in doctor, so an upgrade adds them. If Codex asks, trust the new hooks once with /hooks.
  • Adapter: PreToolUse for request_user_input* → agent.waiting with reason: question (only that it asks, never the questions); any other PreToolUse → agent.started with signal: tool, like PostToolUse. The async question's own PostToolUse comes back at once, before any answer, so it says nothing.
  • Ledger: a Codex notify finish is dropped for a session whose hooks have been heard (SawStart, set by a hook's own prompt start), so Stop alone ends a hooked turn; Codex without hooks still finishes on notify. Such a notify isn't published either, so it doesn't push "finished" to the phone mid-turn.
  • Ledger: a turn that ends while its own async question is the last thing that happened still reads as waiting until you prompt again (Codex's async question returns at once and the turn then stops). The adapter marks it question_box: "async". A question answered within the turn (Plan mode's blocking one, or the new structured Codex driver's) ends with the turn, and so does a turn where the agent worked on after asking.
  • Ledger: a screen-read "working again" doesn't end a question the agent's own hook reported; the question box redraws the screen, and in the live test the screen poller cleared the wait in the same second it was asked. The agent's next tool or prompt ends it.

Rebased on main after #81 (structured Codex driver) and v0.3.20

Rebasing showed a real interaction: TestAStructuredCodexSessionIsDrivenThroughItsAppServer failed with this branch, because the driver's question is answered inside the turn, yet the "ends on its question" rule kept the session waiting. The rule now applies only to the async question box. The test passes, and TestATurnThatEndsOnItsQuestionStillWaits gained the blocking case. go test -race ./internal/box/ ./internal/integrations/... passes, except TestLongPromptsOverTheAPI, which hit a race in the test's own cleanup once (a global reset in longprompt_test.go against tmux lookup) and passes 5 of 5 alone, on this branch and on main. The changelog entry moved to a new Unreleased section above v0.3.20.

Tested

  • New: TestCodexNotifyEndsATurnOnlyWithoutHooks, TestACodexQuestionWaitsUntilItIsAnswered, TestATurnThatEndsOnItsQuestionStillWaits, TestCodexToolHooksAreAddedToAnOlderInstallOnce, TestCodexAsyncQuestionStaysAWait; extended TestTranslate, TestTranslateNeverCopiesContent (questions, a command and a tool response marked secret), TestEveryAgentAdapterMapsItsTurn.
  • On this rebase: go build ./..., go vet, go test -race ./internal/box/ ./internal/integrations/... all pass.
  • Live (before the rebase) on a Linux box with Codex 0.160.1, notify installed as usual: a 34-second turn running three commands stayed running and finished once, at Stop; an async question showed as waiting the moment it was asked (reason: question, from the hook) and stayed waiting through the turn's end, with no moment of finished in between (sampled every 1.5 s).
  • Not covered live: Plan mode's blocking request_user_input (it only exists in Plan mode); the adapter and ledger handle it the same way.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V7UV6sY16AabsdHtcyxyjL

@vercel

vercel Bot commented Oct 12, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Stener1 Stener1 changed the title fix(codex): only Stop ends a hooked Codex turn, and its question box … fix(codex): only Stop ends a hooked Codex turn, and its question box is a wait Oct 12, 2026
@Stener1

Stener1 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

@sean-brydon here's the Codex half of #7 on its own, rebased on main, as you asked. It doesn't depend on turn check; go vet and go test -race ./internal/box/ ./internal/integrations/... pass on the rebase.

#7 now has your three turn-check points addressed too (laptop-only settings with keys only to the default gateway, capped and redacted text, no default model); details are in its description.

@Stener1
Stener1 force-pushed the codex-turns branch 3 times, most recently from d77be54 to 61eeb6b Compare October 14, 2026 21:04
@Stener1
Stener1 marked this pull request as ready for review October 15, 2026 07:54
…is a wait

Codex's notify (agent-turn-complete) comes at the end of each assistant
message, not the turn's, so a long turn read as finished after its first
message. Once a session's hooks have said a prompt started (SawStart), the
ledger ignores a notify finish and the box does not publish it; Stop ends
the turn. A Codex without hooks still finishes with notify.

integrations install codex adds PreToolUse and PostToolUse. PreToolUse for
request_user_input* (Codex's question box) is agent.waiting with reason
question, never with the questions; any other tool use is agent.started
with signal tool. A hooks.json without PreToolUse reads as outdated, so an
upgrade adds them.

Only the async question box (request_user_input_async, marked
question_box: "async") keeps a session waiting after its turn ends: it
returns at once, so the turn can stop before anyone answers. A question
answered within the turn (Plan mode's, or a structured driver's) ends
with the turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant