Skip to content

fix(codex): keep wrapped exec_command results in resumed history - #1395

Merged
Juliusolsson05 merged 3 commits into
mainfrom
fix/codex-exec-wrapped-output
Sep 27, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
fix/codex-exec-wrapped-output

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1321.

Plan: docs/plans/2026-09-27-codex-exec-wrapped-output.md.

Problem

  • The mapper drops every wrapped result. mapCodexRolloutToFeedEntries drops every function_call_output wrapped as Chunk ID … Process exited with code N … Output:. Its stated reason: the correlated exec_command_end event carries the same result.
  • That event is never written. Current Codex never persists it: codex-rs rollout/src/policy.rs lists EventMsg::ExecCommandEnd as transient. rust-v0.107.0 through v0.136.0 did persist it in extended-history mode (an app-server option), which no local rollout uses.
  • Census of 2,541 local rollouts: 85,355 wrapped outputs with an exit line, and 0 exec_command_end events.
  • Effect. The dropped line was the only copy of the result, so every exec_command card in resumed history (Codex through 0.144) showed no output and no exit status. This is the df -h case from Codex transcript silently drops a tool result or message that shares a millisecond with its sibling #1288.
  • A second bug. The predicate searched the whole string, including the command's own output, for the exit words. A still-running chunk whose output contained them was dropped too.

Change

  • Header-only parsing. codexExecWrapperExitCode reads the exit code from the header only (before \nOutput:\n). It returns null for unwrapped output or a still-running chunk.

  • Keep the result. A finished wrapped output becomes a tool result with:

    • the stripped output (possibly empty);
    • is_error from the real exit code;
    • the same exec_command_end metadata the event branch stamps.

    The command adapter therefore reads it as the native transport, which is exit-proven and owns its bytes, and an empty success is still absorbed by the row dispatcher.

  • Review a, round 1:

    • Running chunks are marked. A running chunk ("Process running with session ID N") is stamped exec_command_running, and the command adapter no longer claims a proven exit for it. It shows unknown, not success. Its real exit comes later on a write_stdin result, which this card cannot correlate.
    • The fuller carrier wins (review b). When a rollout has both carriers for a call, the wrapper wins: its output is whole, while the event's is sanitized to 10,000 bytes. An event after its wrapper is dropped in createCodexTranscriptEntryMapper. A wrapper after its event (Codex's own emit order) wins the later-wins result index.
    • The parser requires the LF Output: marker. A Chunk ID: wrapper it cannot parse is marked exec_command_unparsed, kept whole, and shown as unknown, never success (review b).
  • Comments corrected. The stale comments that claimed the wrapper came from, or was duplicated by, exec_command_end are fixed.

Tests

  • execWrappedOutput.test.ts uses the fixture testing/fixtures/codex-exec-output/wrapped-exec-command-output.json: three real 0.132.0 exec_command pairs (exit 0 with output, exit 1, exit 0 empty).
  • They go through the real mapper and the real fromCodexCommandOperation, and assert output, exit code, failure status and the empty-success result.
  • One more case: a still-running chunk whose body contains "Process exited with code 0" keeps its body and gets no exit metadata.
  • All four are red on main. Review a added three more tests: a running chunk shows unknown through the adapter; both carriers give one result; a header without Output: makes no exit claim. The exact codex metadata is now asserted too. Reverting any of the three fixes fails its test. Review b added: an unparsed (CRLF) wrapper shows unknown; with event then wrapper, the card gets the wrapper; with wrapper then event, the event is dropped.
  • Fixture privacy. The command, workdir and output body are replaced by the same number of x characters, with newlines kept. Keys, call ids, timestamps and the wrapper header are verbatim. I checked every leaf string.

Verification

  • npx tsc -b clean (Node 24).
  • At c053f52d: src/providers, src/renderer/src/rendering and features/feed pass (158 files, 1,145 tests at 63553e5c, including shapes.coverage).
  • scripts/audit-rendering-shapes.mts fails to start with ERR_MODULE_NOT_FOUND @renderer/rendering both here and on main. That is an environment issue, not this change.

🤖 Generated with Claude Code

Codex never persists exec_command_end (codex-rs rollout policy: transient;
0 in 2,541 local rollouts), so dropping the wrapped function_call_output
removed the only copy of every exec_command result and exit status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 added provider:codex Affects Codex integration type:bug Something works wrong sev:P3 Minor labels Sep 27, 2026
…carrier per call (review a)

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

Copy link
Copy Markdown
Owner Author

Disposition for review a (head c053f52)

  • P1, fixed. A running chunk now maps to a result marked exec_command_running, and the native adapter claims no proven exit for it, so the card shows unknown.
    • Test: the fixture's real call plus a running chunk, through fromCodexCommandOperation, gives unknown with a null exit code. It fails when the adapter change is reverted.
    • A first attempt ("no exit code means unproven") also flipped two Git-formatter tests whose synthetic Codex results carry no metadata. The explicit mark limits the change to the shape you found.
    • Correlating the later write_stdin exit back to the command card by session id is a separate feature, not done here.
  • P2, fixed. createCodexTranscriptEntryMapper keeps the first exec terminal result per call_id, remembering the last 512 per stream.
    • Test: call, event, then wrapper through one mapper gives one result, the event's. It fails without the filter.
    • I corrected the "never persisted" claims in the code comments, plan and body to name rust-v0.107.0 through v0.131.0 extended-history mode.
    • A pair split across a history page boundary is not caught. That is stated in the mapper comment.
  • P3, fixed. codexExecWrapperExitCode requires the LF \nOutput:\n marker. Test: a header without it makes no exit claim.
  • Mutation survivors, fixed. The exact codex metadata (kind, parsedCmd, command, cwd, exitCode) is now asserted.

…er a persisted event (review b)

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

Copy link
Copy Markdown
Owner Author

Disposition for review b (head 63553e5c)

  • P2: a wrapper without the LF Output: marker painted as success. Fixed.
    • Any Chunk ID: wrapper the parser cannot read (no marker, a CRLF header, an unknown status line) is marked exec_command_unparsed and kept whole.
    • The native adapter proves no exit for it, as for exec_command_running, so the card shows unknown.
    • Test: a CRLF wrapper, through fromCodexCommandOperation, gives unknown with a null exit. Removing either the mapper branch or the adapter clause fails it.
  • P2: first-carrier dedupe kept the truncated event. Fixed: the wrapper wins.
    • The persisted event's aggregated_output is sanitized to 10,000 bytes, and extended mode ran through v0.136.0. I corrected every "v0.131.0" to v0.136.0.
    • An event that follows its wrapper is dropped. A wrapper that follows its event is kept, and later-wins buildToolResultIndex hands the card the wrapper. Codex's emit order is event, then function result.
    • Tests cover both orders. Preferring the event, or not dropping the late event, fails them.
  • P2: page and burst boundaries bypass the per-mapper memory. Accepted and documented in the mapper WHY comment.
    • Across a boundary both results are kept and later-wins decides, which again favours the wrapper in Codex's emit order.
    • A boundary-proof de-duplication would need the index, not the mapper, to own carrier provenance.
    • No local rollout (0 of 2,555) has both carriers.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

READY (W1). Head 63553e5c. Exact-head CI is green. Every reviewer is MERGE-READY: a (verify-report-a), b (verify-report-b), c (report-c). Review c's one low finding is a test-strength gap: negative exit codes parse correctly but are untested. It is noted, not blocking. Dispositions for reviews a and b are posted above. merge-gate.sh --member: GATE PASS. Mergeable and clean.

@Juliusolsson05
Juliusolsson05 merged commit e246623 into main Sep 27, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/codex-exec-wrapped-output branch September 27, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

provider:codex Affects Codex integration sev:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(codex): exec-wrapped tool outputs are dropped, so exec_command cards can show no result

1 participant