Skip to content

fix(codex): show Codex 0.158 async questions as answerable question cards - #24169

Closed
OrcaWin wants to merge 3 commits into
mainfrom
fix/codex-async-user-input-question
Closed

OrcaWin wants to merge 3 commits into
mainfrom
fix/codex-async-user-input-question

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​366 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​366
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​347 $\color{#cf222e}{\Huge{\mathbf{−}}}$​15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​332

ELI5

Newer Codex (0.158) asks you a question in a new way: it writes the question as a normal chat message with a hidden tag saying "this is a question, here are the choices", then waits. Orca only knew the old way, where Codex asks through a separate "please answer this" request. So Orca showed the question as plain text, no answer buttons appeared, and the chat just looked stuck while Codex waited. This PR teaches Orca to spot the new-style question and turn it into the same answer card it already shows for old-style questions. When you pick an answer, Orca sends it to Codex as your next chat message, which is how Codex expects answers to these questions.

What Changed

Before (Codex 0.158): Codex asks "Which color do you prefer: red or blue?". Orca shows it as text, no question card appears, and the session stays "working" instead of "needs attention". Codex quietly sends sleep steps in the background while it waits, so from the user's side the chat looks frozen.

After: the same question shows up as a normal pending question card (options Red / Blue, plus free text), and the session goes to "attention". Picking an option or typing a reply answers it, and Codex carries on.

Mechanism. It reuses the existing question-card system instead of adding a second one.

  • New src/main/codex/codex-async-user-input.ts: recognises a completed Codex agentMessage with delivery: 'async' and a questions list, and reshapes it into the same parameters an old-style blocking item/tool/requestUserInput carries. The question title becomes the question id. Each string option becomes {label}, and an "Other" (free text) choice is added, because any message the user types answers an async question.
  • codex-structured-provider-events.ts: registers the async question in the existing CodexPromptRegistry (marked delivery: 'async') and emits the normal prompt event. The existing question-card builder, pending-question tracking and status projection then do the rest.
  • Answering from the card (codex-structured-prompt-ownership.ts, codex-structured-prompt-replies.ts, new steerCodexTurn in codex-structured-turn-start.ts): the answer is written to the journal first, as before. For an async question, Orca then sends it as a user message (turn/start on the question's thread) instead of a JSON-RPC reply, since there is no server request to reply to. A multi-question ask sends one message ("Title: answer" per line) once all cards are answered.
  • Typing instead (codex-structured-journal-prompts.ts, new answerAsync): a live user message on the thread resolves every open async card there, recording the typed text as the free-text answer. Any card answers already given on a partly answered ask are still delivered.
  • Turn end / Stop: cards are cancelled by the existing turn-end handling. Answers already given on a partly answered ask are still sent to Codex when Codex ends the turn on its own. Stop does not restart work to deliver them.
  • Older Codex: the blocking requestUserInput path is unchanged and covered by a test.

Why

Found in a crash-report scan: a user note on Slack report ts 1790733136.887819 (Orca 1.4.217, Codex CLI 0.158) said interactive questions from Codex still didn't work. I reproduced it against the real codex app-server 0.158.0 and captured its frames.

Alternatives considered:

  • A separate "async question" UI or row type. Rejected: it would need a new wire field, and older clients and SSH-connected clients would not understand it. Reusing the existing question row means no wire change, and everything runs on the execution host (adapter and translator). Remote and older clients answer through the same respondToPrompt path.
  • Hiding the question's chat message and showing only the card. Rejected: rebuilding a session from Codex history replays that message under its Codex id, so hiding it only in the live path would make it reappear after reload.

CLEAR UNDENIABLE fix evidence

Repro command:

pnpm test src/main/codex/codex-structured-async-question.test.ts

The test replays frames captured from the real Codex 0.158.0 app-server: an item/completed agentMessage with delivery: 'async' and questions: [{title:'Which color do you prefer: red or blue?', options:['Red','Blue']}], followed by sleep items.

Real 0.158.0 frames (captured by driving codex app-server directly):

item/completed {"type":"agentMessage","id":"call_bKNG2cV43RuJnV9hie9lVx8S","text":"Do you prefer red or blue?\n- Red\n- Blue","phase":"final_answer","delivery":"async","questions":[...]}
item/started {"type":"sleep","id":"call_GjkJTaLdVbJ0W4aSOX6FDnxy","durationMs":30000}

Before (main source files restored, new module removed, first version of the test file):

× shows a pending question card and puts the session in attention
× sends the chosen option as a steering user message, not a server-request reply
× resolves the card with the typed reply when the user writes instead
× cancels the card when the turn ends unanswered
AssertionError: expected 'working' to be 'attention' // Object.is equality
Expected: "attention"
Received: "working"
AssertionError: expected [] to deeply equal [ Array(1) ]
- [ "Which color do you prefer: red or blue?", ]
+ []
TypeError: Cannot read properties of undefined (reading 'body')   (no question row to answer)
Tests  4 failed | 1 passed (5)

The one passing test covers the older blocking requestUserInput path, which should pass both before and after. With the final 9-test file against main's source, the adversarial reviewer saw 8 of 9 fail (only the blocking-path test passes).

After (this branch, rebased on latest main):

✓ shows a pending question card and puts the session in attention
✓ sends the chosen option as a steering user message, not a server-request reply
✓ resolves the card with the typed reply when the user writes instead
✓ cancels the card when the turn ends unanswered
✓ steers one message naming each question once every card of a multi-question ask is answered
✓ still delivers a card answer when a typed reply closes a partly answered ask
✓ still delivers a card answer when Codex ends the turn on a partly answered ask
✓ does not restart work a Stop ended to deliver a partly answered ask
✓ keeps the blocking requestUserInput path for older Codex builds
Test Files  1 passed (1)
     Tests  9 passed (9)

Live end to end with real codex app-server 0.158.0 through Orca's real host, journal and Codex adapter (a scratch harness, not committed):

QUESTION ROW {"kind":"question","question":"Color preference","options":[{"id":"Color%20preference:Red","label":"Red"},{"id":"Color%20preference:Blue","label":"Blue"}],"freeTextQuestionId":"Color preference","resolution":{"state":"pending",...}}
STATUS while pending attention
ANSWER {"ok":true,...,"resolution":{"state":"resolved",...}}
FINAL STATUS idle
FINAL ROWS [user prompt, turn, assistant "Color preference\n- Red\n- Blue", question resolved (Red), assistant "Red"]

Trade-offs (negative user-facing changes)

  • The question text appears twice. The async message still renders as a normal assistant message, with the question card right below it. This is deliberate (see Why): hiding it live would make it reappear on reload.
  • Cards close when the turn ends. If Codex ends the turn instead of waiting (it does this after about 110s unanswered, or sometimes right away), the card is cancelled. The user then answers by typing, which starts a new turn.
  • Any user message counts as the answer. A live user message on the thread resolves all open async cards on that thread with its text, the same way Codex treats the next user message as the answer. In a rare race, a message sent just before the question but echoed after it would resolve the card with unrelated text.
  • No chat bubble for a card answer. Answering from a card creates no separate user-message row; the resolved card shows the answer. Typed replies still show as normal messages.
  • Multi-question answers are held. For an ask with several questions, the message to Codex goes out once every card is answered, or earlier if the user types or Codex ends the turn.
  • Oversized questions stay as text. An async ask too large for the prompt registry limits stays as plain message text with no card, rather than failing.

Adversarial review

3 loops, run until clean. The final loop came back clean.

  • Loop 1 — 1 defect found, fixed. In a multi-question ask, if the user answered one card and then typed a reply instead of answering the rest, the card answer showed as resolved but was never sent to Codex. Fixed in 76dcbbcbce, with a regression test.
  • Loop 2 — 1 defect found, fixed. The same kind of bug, triggered when Codex ends the turn on its own (the reviewer confirmed on real 0.158.0 that Codex sleeps in 30s steps for about 110s, then ends the turn). Partial card answers were dropped. Fixed in 80890da257, with a regression test and a test that Stop does not restart work. The reviewer also checked two things that are not defects: the steered echo creates no stray user row, and a second ask in the same turn is resolved by the next user message, which matches the design.
  • Loop 3 — clean. Checks: retries of the notification replay the same journal key, which is harmless; restored threads create no stray cards; Stop never sends a false JSON-RPC reply for the async prompt's id; free-text "Other" works through the existing code; long titles stay within registry limits; the fixtures match the real captured frames. The reviewer found no wire-compatibility, SSH, older-Codex, Windows, folder-workspace or AGENTS.md problems.

Linked Issue

None. Found by the crash-report scan (user note on Slack report ts 1790733136.887819).

Visual Proof

N/A for screenshots: this is a main-process protocol change that makes the existing question card appear. The live run against real Codex 0.158.0 above shows the pending card row, the 'attention' status and the resolved answer.

Testing

Checks rerun after rebasing on latest origin/main:

  • pnpm test src/main/codex/codex-structured-async-question.test.ts: 9/9 pass.

  • pnpm test src/main/codex src/main/native-chat: Test Files 447 passed, 3 skipped (450); Tests 4314 passed, 13 skipped.

  • pnpm tc:node: clean.

  • oxlint on all 11 changed files: exit 0.

  • check:code-quality:changed: passed (0 findings) on the original fix commit.

  • Live run against real codex app-server 0.158.0 on macOS (above).

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: no new inputs beyond Codex's own message fields, which are held to the existing prompt-registry size limits.
  • Cross-platform: no platform-specific code.
  • SSH / remote and mobile: the logic runs on the execution host, and the rows use the existing question shape. No wire change, so older clients work.
  • Backwards compatibility: older Codex builds use the unchanged blocking path.
  • Performance: one extra check per completed agent message.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

m4air added 3 commits September 30, 2026 07:29
…n cards

Codex 0.158 asks with request_user_input_async: an async agentMessage carrying
questions, no server request, then sleep items until a user message steers the
turn. Orca rendered it as plain text, so no card appeared and the session never
went to attention.

Register the async ask in the existing prompt registry and journal it through
the existing question path. Answering a card steers the answer into the turn via
turn/start instead of connection.respond; a typed user message resolves the open
cards with its text; turn end cancels them as before. Blocking requestUserInput
is unchanged.
…uestion async ask

A typed reply closes every async ask on the thread and dropped answers already
given on its cards, though the journal showed them resolved. Steer those
collected answers after the reply so Codex receives what the cards show.
…ync ask

Codex 0.158 ends a turn on its own while an async ask waits. The turn end
forgot the prompt, so answers already given on its cards were never sent
though the journal showed them resolved. Steer them once the turn completes;
a Stop or failed turn is not restarted for them.
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds async question handling for Codex 0.158 protocol.

The PR should not merge until a failed final card-answer steer leaves the answer recoverable.

Findings

  1. P1 Card answer becomes unrecoverable ▶

Summary

The PR converts Codex 0.158 async question messages into existing question cards and sends card answers through turn/start.

  • It also resolves open cards when the user types and handles partially answered asks at turn end.
  • Final card-answer delivery can fail after the card is irreversibly resolved.
Diagram
sequenceDiagram
  participant Codex
  participant Orca
  participant Journal
  participant User
  Codex->>Orca: Completed async agentMessage with questions
  Orca->>Journal: Add pending question cards
  User->>Orca: Answer final card
  Orca->>Journal: Resolve card
  Orca->>Orca: Forget pending prompt
  Orca->>Codex: turn/start with answer
Loading

Reviews (1) · Last reviewed commit: "fix(codex): deliver card answers when Co..."

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds handling for Codex async user-input questions. It parses completed async question items into prompts, records card and typed-message answers in the journal, and sends applicable answers through Codex turn steering. Integration tests cover single- and multi-question flows, turn completion and interruption, and the blocking server-request path.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 80890

Partially answered async question cards can occasionally have their answers lost or delivered at the wrong time when several threads or turns are active. The blocking question path is unchanged. Scoping the drain to the completed turn before merge would remove the issue.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 80890

Answers remain scoped to their conversation, with existing ownership checks preserved. However, automatically resumed work may briefly lack the tracking needed for Stop to cancel it before the new turn is announced.

Retained concerns

  • Medium · security · inferred: Completion recovery can start agent work without preserving its pre-open cancellation identity. Partial answers trigger turn/start after the previous turn completes, but steerCodexTurn discards the returned turn ID and does not register dispatch correlation. Before turn/started supplies an active turn, conversation cancellation can find neither a live turn nor an answered-but-unopened dispatch and return cancelled:false. The guard against resuming interrupted turns does not cover this newly created interval, weakening operator cancellation as a containment control within the affected conversation.
Security review details

Security Blast Radius

  • inferred — The identified cancellation gap is bounded to work initiated in the affected Codex conversation. Steering uses the prompt's stored thread and existing connection; the inspected route does not grant new cross-session routing or permission-setting authority.

Security Findings and Attack Paths

  • inferred — An async question with a recorded partial answer can lead to automatic turn creation when its original turn completes. A Stop arriving before the replacement turn is announced may not find that work to interrupt. This is a potential containment failure, not evidence that question content bypasses tool approvals or changes execution privileges.

Trust Boundaries and Controls

  • observed — Provider-supplied question metadata becomes a question prompt only through the completed async agentMessage parser. User answers must name a question belonging to the claimed prompt and fit the answer-byte bound before the existing ownership checks permit provider steering.

Resilience and Maintainability Implications

  • observed — Partial-answer recovery consumes registry state before steering and logs delivery failures without restoring that state in the inspected path. Direct replies also forget prompts before transport, as the legacy blocking path already did; that shared behavior is not treated as a newly introduced vulnerability.

Hardening Proposals

  • proposed — Track answer-created turns through pending initiation and returned-turn identity so Stop can wait for and interrupt them before turn/started. Preserve the existing rule that interrupted or failed turns are not automatically resumed.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 11 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: support for answerable Codex 0.158 async question cards.
Description check ✅ Passed The description is detailed and covers the user impact, implementation, rationale, testing, trade-offs, compatibility, and checklist items. It leaves the linked issue as "None" and does not confirm al…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 5578a01c-2d8f-456f-b9dd-4eb53f7489cb

📥 Commits

Reviewing files that changed from the base of the PR and between d74388f and 80890da.

📒 Files selected for processing (11)
  • src/main/codex/codex-async-user-input.ts
  • src/main/codex/codex-prompt-registry.ts
  • src/main/codex/codex-structured-async-question.test.ts
  • src/main/codex/codex-structured-journal-prompts.ts
  • src/main/codex/codex-structured-journal-translation.ts
  • src/main/codex/codex-structured-prompt-ownership.ts
  • src/main/codex/codex-structured-prompt-replies.ts
  • src/main/codex/codex-structured-provider-events.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/codex/codex-structured-turn-start.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment on lines +54 to +60
const abandoned = session.prompts.takeAbandonedAsyncAnswers()
// Codex 0.158 ends a turn on its own while an async ask waits; a Stop or failure is not resumed.
if (readCodexTurnEnd(method, params)?.status === 'completed') {
for (const answered of abandoned) {
steerCodexPartialAnswers(session, answered, input.requestTimeoutMs)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C4 'clearPromptTurn' src/main/codex

Repository: stablyai/orca

Length of output: 12707


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files 'src/main/codex/*' | rg 'codex-(structured-provider-events|structured-journal-settlement|structured-journal-translation-turn-boundaries|structured-journal-translation-turn-lifecycle|structured-session-acquire|structured-prompts|prompt)'
printf '%s\n' '--- clearTurn definitions and calls ---'
rg -n -C8 'clearTurn|clearPromptTurn|turn/completed|turnId|threadId' src/main/codex/codex-structured-prompt* src/main/codex/codex-structured-journal-settlement.ts src/main/codex/codex-structured-journal-translation-turn-boundaries.ts src/main/codex/codex-structured-provider-events.ts src/main/codex/codex-structured-session-acquire.ts 2>/dev/null
printf '%s\n' '--- changed provider-events file ---'
cat -n src/main/codex/codex-structured-provider-events.ts
printf '%s\n' '--- relevant diff ---'
git diff --unified=25 d74388f8a2dad2bd4bbfe3b937aba66e6648258b 80890da2573dce387649bb4332a8128b66e511ec -- src/main/codex/codex-structured-provider-events.ts src/main/codex/codex-structured-journal-settlement.ts src/main/codex/codex-structured-journal-translation-turn-boundaries.ts src/main/codex/codex-structured-session-acquire.ts

Repository: stablyai/orca

Length of output: 43213


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- prompt registry ---'
cat -n src/main/codex/codex-prompt-registry.ts
printf '%s\n' '--- settlement path ---'
sed -n '1,230p' src/main/codex/codex-structured-journal-settlement.ts
printf '%s\n' '--- turn boundary path ---'
sed -n '1,240p' src/main/codex/codex-structured-journal-translation-turn-boundaries.ts
printf '%s\n' '--- relevant registry tests ---'
rg -n -C8 'clearTurn|abandoned|takeAbandoned|thread|turn' src/main/codex/codex-prompt-registry-retention.test.ts src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts

Repository: stablyai/orca

Length of output: 43091


Scope abandoned async answers to the completed turn that created them.

clearTurn matches prompts by thread and turn, but it appends answered async prompts to a session-wide queue. Every accepted notification drains that queue before checking its own turn status. A non-completed notification can therefore discard answers, and a completed notification from another thread can steer them based on the wrong turn. Drain only answers belonging to the current thread and turn after matching a completed turn notification.

return null
}
// Forget first, as for a blocking reply: a second answer must not steer twice.
registry.forget(prompt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Card answer becomes unrecoverable

When the user answers the final card, the code removes the pending prompt before Codex confirms the turn/start request. If that request is rejected or times out, the card is already resolved and cannot be answered again. Orca shows an “answer unconfirmed” status, but Codex may still be waiting without a way to retry the answer. Keep the answer recoverable until delivery is confirmed.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two delivery gaps in the new async-answer path: a machine-generated steer can resolve an unrelated open card with the wrong text, and a failed turn end can silently drop an already-committed card answer. Neither is covered by the added tests.

Reviewed changes

  • Async question recognition (codex-async-user-input.ts): a completed agentMessage with delivery: 'async' and a questions list is reshaped into the existing requestUserInput params (title doubles as question id, an isOther free-text option is added), so the existing card/status machinery renders it. Also adds readCodexUserMessageReply, codexAsyncAnswerText, and codexAsyncPartialAnswerText.
  • Registry (codex-prompt-registry.ts): register takes a delivery: 'async' marker; forgetAsync returns/forgets every async prompt on a thread; clearTurn collects partly answered async prompts into abandonedAsyncAnswers for takeAbandonedAsyncAnswers.
  • Answer routing (codex-structured-prompt-ownership.ts, -replies.ts, codex-structured-turn-start.ts): an async card answer is recorded, then delivered by steering a turn/start user message (steerCodexTurn, no clientUserMessageId) instead of a JSON-RPC reply.
  • Typed replies and turn end (codex-structured-journal-translation.ts, codex-structured-journal-prompts.ts, codex-structured-provider-events.ts): a live user message resolves open async cards with its text, and partly answered asks are steered when a typed reply or a completed turn end closes them.
  • Test (codex-structured-async-question.test.ts): 9 replayed-0.158-frame cases covering the card, both answer paths, multi-question partial delivery, turn-end cancellation, and the unchanged blocking path.

ℹ️ Nitpicks

  • steerCodexPartialAnswers is fire-and-forget (console.warn on failure) while the full-answer path awaits steerCodexTurn and lets the host record answerUnconfirmed; a transient failure on the partial path leaves the card resolved with no marker. Worth aligning or at least documenting.
  • register's byte-cap eviction and trim() delete async prompts without pushing them to abandonedAsyncAnswers, so recorded partial answers can be dropped under registry pressure — same resolved-but-undelivered shape, lower severity.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment on lines +78 to +90
export function readCodexUserMessageReply(
method: string,
params: unknown
): { text: string | null } | null {
if (method !== 'item/started' && method !== 'item/completed') {
return null
}
const item = readCodexThreadItem(readRecord(params).item)
if (!item || item.type !== 'userMessage') {
return null
}
return { text: readTextContent(item, 'content') ?? readString(item, 'text') }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readCodexUserMessageReply accepts any userMessage item, which includes the echo of Orca's own steerCodexTurn message (it deliberately sends no clientUserMessageId). If a second async ask is still open on the thread when that steer echo lands, answerAsync resolves its card with the first ask's synthesized text and forgetAsync drops it, so the card shows an answer the user never gave and can no longer be answered. The "no stray user row" live check does not clear this: CodexJournalItems.handle suppresses live userMessage rows regardless of arming, so the reply path fires even with no visible row.

Technical details
# Steered answer echo resolves an unrelated async card

## Affected sites
- `src/main/codex/codex-async-user-input.ts:78` — matches any `userMessage`, not only a real send.
- `src/main/codex/codex-structured-journal-translation.ts:244` — calls `prompts.answerAsync(event.threadId, reply.text)` for every `item/started`/`item/completed`.
- `src/main/codex/codex-structured-provider-events.ts:49` — `forgetAsync(threadId)` then drops the other ask.
- `src/main/codex/codex-structured-turn-start.ts:127` — `steerCodexTurn` omits `clientUserMessageId`.

## Reachability
Precondition: two async asks open on the same thread (the design explicitly supports this; `answerAsync`/`forgetAsync` iterate all of them). Answer ask A's card → `steerCodexTurn` → Codex coalesces that input into the open turn and surfaces it as a `userMessage`. The echo is not distinguished from a real user reply, so `answerAsync` rewrites ask B's card to `resolved` with A's answer and `forgetAsync` forgets B.

## Required outcome
A machine-generated steer must not count as a live user reply for answering other async cards. Either correlate the reply to an armed submit (only real sends have a matching dispatch echo / client id) or mark steered inputs so the reply path ignores them.

## Open questions for the human
- Does 0.158 echo a `turn/start` that carries no `clientUserMessageId` as a `userMessage` item? No repo fixture captures the steer echo; the "no stray row" observation is explained by `CodexJournalItems.handle` suppressing live userMessage rows even while it routes them.

Comment on lines +54 to +60
const abandoned = session.prompts.takeAbandonedAsyncAnswers()
// Codex 0.158 ends a turn on its own while an async ask waits; a Stop or failure is not resumed.
if (readCodexTurnEnd(method, params)?.status === 'completed') {
for (const answered of abandoned) {
steerCodexPartialAnswers(session, answered, input.requestTimeoutMs)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

takeAbandonedAsyncAnswers() drains on every notification, but the answers are only steered when the end is completed. For interrupted that is the intended Stop behavior, but for a failed end a card the user already answered stays resolved in the journal while Codex never receives it, and unlike the awaited full-answer path there is no "unconfirmed" marker. The added regression test only covers interrupted.

Technical details
# Failed turn end drops a committed async card answer

## Affected sites
- `src/main/codex/codex-structured-provider-events.ts:54` — drains `takeAbandonedAsyncAnswers()` unconditionally.
- `src/main/codex/codex-structured-provider-events.ts:56` — steers only when `readCodexTurnEnd(...).status === 'completed'`.
- `src/main/codex/codex-prompt-registry.ts:250` — `clearTurn` abandons partly answered async prompts on any turn end.
- Host commit path — a clicked card is committed `resolved` immediately, before the ask is complete.

## Reachability
1. Multi-question async ask; user answers one card (committed `resolved`, held until the ask completes).
2. Codex ends the turn with `status: 'failed'` (not a user Stop).
3. `clearTurn` abandons the prompt, `takeAbandonedAsyncAnswers` drains it, the `completed` gate is false → no steer and no `answerUnconfirmed` marker.
4. Card stays `resolved`; Codex never receives the answer.

## Required outcome
On a `failed` end, either deliver the held answers (as for `completed`) or leave/cancel the card so the UI does not present an undelivered answer as accepted. `interrupted` (Stop) is a documented exception; `failed` is not.

## Open questions for the human
- Was `failed` folded into "not resumed" deliberately? It is untested; the regression test covers only `interrupted`.

@miguel93041

Copy link
Copy Markdown

@OrcaWin I've opened #30062 with an overlapping fix for structured Codex async questions, plus desktop/mobile support for chat backed by a terminal. This PR is linked in its description.

One deliberate difference is the lifecycle: async questions stay separate from blocking approvals and survive turn completion or Stop. The host derives them from history and exposes them through the current-request transport merged in #29121. Answers use the existing immediate message path, and failed or unconfirmed delivery does not resolve the shared pending set.

384 focused tests and the build pass; fork CI approval and physical-device/live SSH checks remain pending. I'd appreciate a comparison against the real app-server behavior you captured, particularly delivery failures and turn-end handling.

@nwparker

Copy link
Copy Markdown
Contributor

Thank you for this PR. Codex async questions now show as answerable cards on main via #31652, which covers what this PR targeted. Since this branch is conflicting, we are closing it as superseded. We appreciate the work.

@nwparker nwparker closed this Oct 24, 2026
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.

3 participants