Skip to content

fix(codex): log a late reply to a timed-out request instead of showing it in the chat - #23893

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/codex-late-response-diagnostics
Sep 29, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
brennanb2025/codex-late-response-diagnostics

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Sometimes Orca asks Codex for something, gives up waiting, and tells you the request failed. If Codex's answer turned up after that, Orca had nowhere to put it, so it printed it into the chat as a raw line: "codex response:unmatched" plus a block of JSON. You no longer see that raw line for a late Codex reply: it goes to the app log, and the chat only shows what matters to you.

What Changed

The problem. During Stop QA on a Codex chat, a Stop request (turn/interrupt) timed out, and Orca showed its usual timeout line. Codex's reply came later ({"id":7,"result":{}}), and the chat then showed a second, meaningless line: codex response:unmatched with that JSON. This happened because a reply whose request was no longer being waited on was handed to the generic "frame Orca doesn't understand" path. That path writes a visible status line into the chat.

Scope. Codex chats only, local, WSL and SSH hosts alike. This affects only replies to a request Orca already stopped waiting for, or to an id it never sent. Codex notifications, Codex's requests to Orca, unreadable lines, and error frames with no id still take the same path as before.

Before

  • A reply arriving after its request timed out showed as codex response:unmatched plus raw JSON in the chat, just after the timeout line that had already told you the request failed.

After

  • You no longer see a raw codex response:unmatched line for a late Codex reply. The chat shows only the timeout line. The late reply is logged as [codex-app-server] late reply to turn/interrupt after timeout (id 7). Any other reply with no request waiting for it (for example one arriving after the connection already failed its requests) is logged as reply with no waiting request (id N), with the error message if the reply carries one.

Mechanism

  • codex-app-server-record-dispatch.ts: a reply with no waiting request is logged with console.warn instead of being passed to onUnhandledFrame. The dispatcher, which pairs replies with requests, is now the only place that decides about them. Nothing reaches the chat history.
  • When a request times out, the connection now calls timeOutPending. It drops the waiter as before and remembers the request's method, capped at the 64 most recent per connection, so the log can name what the late reply was for.
  • Nothing is saved, nothing changes on the wire, and no chat rows change except the one that no longer appears.

Why

A reply only means something to the request that asked for it. Once that request has given up and reported its own outcome, nobody is left to act on the reply, so it is a transport diagnostic rather than part of the conversation. Logging it at the point that pairs replies with requests keeps one owner for the decision.

Alternatives considered:

  • Hide the frame when the chat is displayed. The line would still be saved into every chat's history, and a second place would have to know it is noise.
  • Keep the timed-out request waiting for its late reply. A timeout exists so the caller can stop waiting and move on; the user has already been told the outcome.

Linked Issue

No issue. Found during Codex Stop QA for #23026.

Visual Proof

N/A: the change removes a status line that appears only when Codex answers after Orca's own timeout. That can't be forced from the app without a rig that delays Codex's reply. It is covered by the connection test below, which drives a real timeout followed by a late reply.

Testing

  • I manually tested these changes locally

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

  • codex-app-server-connection.test.ts:

    • A turn/interrupt times out, then a late {id, result: {}} and an error reply with no waiting request arrive. The test asserts nothing is forwarded to the chat and checks both log lines. It fails on main, where the reply is forwarded as response:unmatched.
    • The existing "unclassified frame" test now also covers an error frame with id: null, such as a parse error. That frame still reaches the chat as before.
  • Removing each change fails its test: putting the forward back fails the "nothing forwarded" check; dropping the timeout record fails the "late reply to turn/interrupt" log check.

  • pnpm tc:node, oxlint, check:code-quality:changed, check:react-doctor:changed and the anti-slop audit pass.

AI Disclosure

Review

One focused review loop: CLEAN. It checked that nothing else reads the removed frame kind, that the remembered timeouts can't collide (ids only grow per connection) and go away with the connection, and that frames the user needs still reach the chat. Two small fixes from its notes: the log no longer calls a reply whose request Orca did send "unknown", and the test's log spy is always restored.

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

  • Same on macOS, Linux, Windows and SSH hosts: the host's own Codex connection pairs its replies.

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)

Author: @BrennanKB5

@brennanb2025
brennanb2025 marked this pull request as ready for review September 29, 2026 15:23
@brennanb2025

brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Status: ready for review

Head 6bd0e12bae5. CI is green: 16 checks passed, none failed or pending.

What it fixes: a Codex reply that arrived after Orca had already timed out its request used to show in the chat as a raw codex response:unmatched line with JSON. Users no longer see that line: the reply now goes to the app log. Everything else the chat showed before still reaches it.

Review: one focused loop, CLEAN (no P0, P1 or P2 findings). The loop confirmed three things:

  • nothing else reads the removed frame kind;
  • the remembered timeouts can't collide, because request ids only grow per connection, and they go away with the connection;
  • frames the user needs still reach the chat: notifications, Codex's requests, error frames with no id, and unreadable lines.

Both of the loop's small notes (P3) are fixed in 6bd0e12bae5.

Validation:

  • codex-app-server-connection.test.ts: 32 passed. The new test fails on main, and removing either part of the fix fails it again.
  • The adapter and cancel suites pass.
  • tc:node, oxlint, check:code-quality:changed, check:react-doctor:changed and the anti-slop audit pass.

Not done:

  • No live Electron check. The row appears only when Codex answers after Orca's own timeout, and that can't be produced from the app without a rig that delays Codex's reply.
  • There is no test of the 64-entry cap on remembered timeouts.

Not merged. It's ready for a maintainer's decision.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: c56d50ab-8e56-40c0-acb6-6139f3dac5ae

📥 Commits

Reviewing files that changed from the base of the PR and between 31012ae and 6bd0e12.

📒 Files selected for processing (3)
  • src/main/codex/codex-app-server-connection.test.ts
  • src/main/codex/codex-app-server-connection.ts
  • src/main/codex/codex-app-server-record-dispatch.ts

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


📝 Walkthrough

Walkthrough

The connection now delegates request timeouts to the dispatcher. The dispatcher records the method for each timed-out request, up to 64 entries, and logs responses that have no pending request. It reports whether a response matches a remembered timeout or has no waiting request. Tests cover late replies, unmatched replies, unclassified frames, and mock cleanup.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 6bd0e

The timeout and late-reply changes appear ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6bd0e

Late replies no longer appear as raw chat frames, but an error message supplied by Codex can now enter application logs. The privacy implications depend on how those logs are handled.

Retained concerns

  • Low · security · inferred: Unmatched replies now send provider-supplied error text to application warnings. If those logs have a wider audience or longer retention than the former frame destination, the change could expose sensitive error details; the log controls are not established.
Security review details

Security Blast Radius

  • inferred — The changed path is reachable through records emitted by a Codex provider connected to this app-server connection. No new external entrypoint or cross-connection sharing of timeout context is shown.

Security Findings and Attack Paths

  • inferred — A provider emitting an unmatched numeric error reply can place its error message in an application warning. Whether that creates a sensitive-data exposure depends on the message content and downstream log controls, neither of which is established.

Trust Boundaries and Controls

  • observed — The record reader passes parsed object records into the dispatcher. The dispatcher distinguishes server requests, notifications, and numeric replies; only a numeric reply without a waiter takes the new warning path.

Resilience and Maintainability Implications

  • observed — Timeout removes the pending waiter before rejection; transport failure rejects remaining waiters. The retained timeout map is bounded, although failPending does not explicitly clear it.

Hardening Proposals

  • proposed — Confirm the warning sink's access, retention, and redaction policy; if provider errors can contain sensitive text, omit or sanitize their messages in unmatched-reply diagnostics.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the primary change: logging late Codex replies instead of displaying them in chat.
Description check ✅ Passed The description is detailed and covers the user impact, mechanism, rationale, testing, scope, alternatives, and checklist items. It notes that no issue was opened and references the related QA issue, …
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Changes how the app handles late replies from a timed-out request.

This PR should not merge until error-bearing late replies retain a user-visible diagnostic.

Findings

  1. P1 Late errors disappear from chat ▶
  2. P2 Unmatched replies can flood logs ▶

Summary

The PR removes unmatched Codex replies from the chat, remembers recently timed-out request methods, and logs late replies instead.

  • The new timeout test verifies that late replies do not reach the unhandled-frame callback.
  • Error-bearing late replies also lose their previously visible diagnostic, and the new warning path has no output bound.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Request deadline expires] --> B[Remove waiter and remember method]
  B --> C[Late reply arrives]
  C --> D{Reply has provider error?}
  D -->|No| E[Log transport diagnostic]
  D -->|Yes| F[Currently log error only; user loses diagnostic]
Loading

Reviews (1) · Last reviewed commit: "fix(codex): say a reply had no waiting r..."

Comment on lines +95 to 102
const error = isAppServerRecord(message.error) ? message.error.message : undefined
console.warn(
timedOutMethod
? `[codex-app-server] late reply to ${timedOutMethod} after timeout (id ${message.id})`
: `[codex-app-server] reply with no waiting request (id ${message.id})`,
...(typeof error === 'string' ? [error] : [])
)
return

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 Late errors disappear from chat

When a turn/start request times out and Codex later replies with an error, this branch writes that error only to the log. The timeout gives the user no provider explanation, while the previous path showed the late error in the chat. The user therefore loses the reason Codex gave for the failure. Please keep error-bearing late replies visible while suppressing routine late results.

Knowledge Base Used: Restore Native Chat Session Admission

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +95 to +101
const error = isAppServerRecord(message.error) ? message.error.message : undefined
console.warn(
timedOutMethod
? `[codex-app-server] late reply to ${timedOutMethod} after timeout (id ${message.id})`
: `[codex-app-server] reply with no waiting request (id ${message.id})`,
...(typeof error === 'string' ? [error] : [])
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unmatched replies can flood logs

Every unmatched reply now produces a warning, including its full error message. The reader accepts lines of unlimited length, and the 64-entry limit only caps remembered request methods—not warnings. Repeated replies or a very large error can therefore produce excessive host log output. Consider bounding or coalescing these diagnostics.

@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.

✅ No new issues found.

Reviewed changes

A Codex app-server reply whose id has no pending request is no longer surfaced to the chat; it is logged at the point that pairs replies with requests.

  • Timed-out replies are logged, not rendered. codex-app-server-connection.ts now calls dispatcher.timeOutPending(id) on the per-request deadline; the dispatcher remembers the request's method (bounded at 64 per connection) and, when the reply finally arrives with no waiter, console.warns late reply to <method> after timeout (id N) (or reply with no waiting request (id N), with the reply's error message) instead of calling onUnhandledFrame('response:unmatched', …). Nothing reaches the journal or chat.
  • Coverage moved, not lost. The new connection test drives a real timeout followed by a late result and a late error reply, asserting no frame is forwarded and both log lines land; the existing unclassified-frame test now uses id: null to keep pinning the frame:unclassified path. The test fails on main, where both replies surfaced as response:unmatched.
  • Scope is contained. Notifications, server requests, non-numeric-id frames, invalid/oversized frames, and error frames with no id all keep their previous behavior. The remembered-timeout map is per connection and capped, so it dies with the connection and cannot grow.

I verified the new test passes (32/32), check:code-quality:changed is clean, and no producer or consumer of the response:unmatched string remains anywhere in the repo. The console.warn prefix follows the established [codex-…] convention in this module.

One deliberate tradeoff is worth noting but not blocking: an unmatched reply carrying error used to be classified error-surface and shown as a visible error row. It is now only logged. Since no pending request remains to attribute it to (it timed out or was never sent), and the PR description documents this, the choice is reasonable.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit ff59e2c into main Sep 29, 2026
58 checks passed
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