Skip to content

fix(codex): same-millisecond rollout items are no longer dropped (#1288) - #1298

Merged
Juliusolsson05 merged 5 commits into
mainfrom
fix/codex-rollout-identity
Sep 25, 2026
Merged

Juliusolsson05 merged 5 commits into
mainfrom
fix/codex-rollout-identity

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1288.

Found by the Stage 3 C8 hunt. Plan: docs/plans/2026-09-25-codex-rollout-identity.md (first commit).

Why

Codex rollout lines have no uuid. The renderer's entry uuid, the renderer's marker and main's history marker were three copies of ${timestamp}:${id ?? call_id ?? type}. That rule is not unique:

  • A call and its output in the same millisecond share ts:call_id.
  • Two outputs of one call in the same millisecond share it too.
  • Id-less items (message) in the same millisecond share ts:message.

Committed-record admission dedupes by uuid, so the second item was dropped on every path (live, initial history, older pages, remote client, rendering replay), permanently and silently. The result is a tool card with no result, or a message that never rendered. A third reviewer's recount of the owner's corpus under the old rule found 4,976 dropped items in 1,300 of 2,231 rollouts; 4,767 of them had content that differed from the item kept.

What merges

One shared src/shared/codex/rolloutIdentity.ts, used by all three sites:

  • A payload id stays ts:id.
  • A call keeps ts:call_id, and its *_output becomes ts:call_id:output:<fnv1a(payload)>, so two different outputs of one call differ.
  • A type-only item becomes ts:type:<fnv1a(payload)>, so different items differ and a re-read still dedupes.
  • A missing timestamp is '', not Date.now().

Main's extractCodexHistoryMarker is exported so a test can hold it equal to the renderer's codexHistoryMarker.

The isCodexExecWrapperOutput comment in entries.ts is corrected. The predicate matches any exec-wrapped output, not "only the wrapper". The predicate itself is unchanged.

Markers aren't persisted, and older pages anchor by byte offset, so no migration is needed.

Tests

rolloutIdentity.test.ts has 6 tests. They drive recorded rollout lines (testing/fixtures/codex-identity/same-millisecond-2026-04.json) through the real mapper and admitMappedEntries. Only paths, strings and ids are redacted. The recorded lines are:

  • an Edit call and its <tool_use_error> output in the same millisecond;
  • two different custom_tool_call_output records of one call in the same millisecond;
  • a user message and an assistant message in the same millisecond.

The tests also cover stable identity on re-read and without a timestamp, and marker parity between main and the renderer on every fixture line.

npx tsc -b is clean.

Residual

  • Not fixed here: isCodexExecWrapperOutput still drops exec-wrapped function_call_output results. In the owner's corpus no exec_command_end event replaces them, so exec_command cards (including Codex transcript silently drops a tool result or message that shares a millisecond with its sibling #1288's df -h example) can still show no result. That is a separate, older cause, and it needs its own issue.
  • Admission is tested only in the 'live' mode. historyLoader.test.ts still uses the old marker rule as its oracle.
  • Byte-identical lines in the same millisecond still share an identity, which is the intended dedupe.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 25, 2026 06:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…items are not dropped (#1288)

The entry uuid, the renderer marker and main's marker were three copies of
ts:id|call_id|type. A call and its output in one millisecond shared a uuid,
as did id-less items (message), and admission dropped the second for good.
One shared codexRolloutIdentity now keeps ids as-is, suffixes call outputs,
and disambiguates type-only items by a content hash (a re-read still
dedupes). A missing timestamp no longer mints a new identity per read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
/#1297/#1298 opened

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 3 commits September 25, 2026 07:17
…ithful fixture, marker parity pinned

- A: one call can record two different outputs in the same millisecond (a
  real rollout: session_id 88325 then 39977); outputs now carry a content
  hash too, so both are admitted while a re-read still dedupes.
- B: the call/output fixture had its exec wrapper 'redacted' away, which
  changed what the mapper does. It is now the recorded kept pair (an Edit
  call and its error output), plus the recorded two-output pair.
- B: nothing pinned main's marker to the renderer's; a re-inlined old rule
  passed CI. A parity test now compares them on every recorded shape.
- The exec-wrapper predicate's comment said 'only the wrapper'; it drops
  any wrapped output because exec_command_end carries the result.

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

Copy link
Copy Markdown
Owner Author

Final disposition (pre-merge), head 1739a92. Three independent reviewers:

  • A (Codex identity consumers, real corpus) and B (tests, fixtures): two rounds each; round 2 was at 7b2fb1a.
  • C (third reviewer): one round, on the final head.

Every commit after 7b2fb1a is a merge from main, and none of them touches this PR's files.

Reviewer Round 1 Round 2
A (identity consumers, corpus) FIX-BEFORE-MERGE MERGE-READY
B (tests, fixtures) FIX-BEFORE-MERGE MERGE-READY
C (third reviewer) MERGE-READY (at 1739a92) —

Findings and dispositions:

Round 1 (majors fixed in 7b2fb1a)

  • A, major: two different outputs of one call in the same millisecond shared ts:call_id:output, so the second was dropped. The real pair is from the 2026-07-09 rollout. Fixed: output identity is now ts:call_id:output:<fnv1a(payload)>. A's round-2 run of the real mapper over 2,218 files found 0 collisions between distinct payloads and 0 identity changes on re-read.
  • B1, major: the callAndOutput fixture had its exec wrapper edited out, which changed which mapper branch ran. Fixed. It is replaced by real recorded pairs:
    • an Edit call with a <tool_use_error> output;
    • the two-output pair.
      B and C each checked them against the corpus.
  • B2, major: no test tied main's history marker and the renderer's marker to the shared function. Fixed: extractCodexHistoryMarker is exported, and a parity test compares the two markers on every fixture line. Both re-inlining mutants are now killed.
  • B3, minor: the admission test runs only the 'live' mode. Not fixed. Low risk, because admitMappedEntries is shared.
  • B4, minor: historyLoader.test.ts still carries the old marker rule as its oracle. Not fixed. The parity test now guards the real drift.
  • A, test gap: the endsWith('_output')→=== 'function_call_output' mutant survived. It is now killed by the two-output test, whose fixture uses custom_tool_call_output (C confirmed).

Round 2

  • B1, minor: the rewritten isCodexExecWrapperOutput comment (entries.ts) says the correlated exec_command_end event carries the result. The owner's corpus has no exec_command_* events in 2,217 rollouts, while 85,355 wrapped outputs are dropped. Not fixed. Only the comment changed; the predicate is byte-identical to main.
  • B2 and B3, minor: carried over from round 1, not fixed.
  • A, test gap: no test catches hashing payload.output instead of the whole payload. Not fixed. The code hashes the full payload, and the corpus has no collision that would expose the difference.

Round 1, reviewer C (final head; all minor)

  • C1, minor: the PR body was stale (it still said ts:call_id:output and "3 of 4 fail"). Fixed in the replacement body.
  • C2, minor: two collision classes remain by design:
    • byte-identical lines in the same millisecond. That is the dedupe the PR wants.
    • a call and its exec_command_end event in the same millisecond. The corpus has none, and main collided on it the same way.
      No change needed.
  • C3, minor: the issue's figure of "1,232 collisions in 270 rollouts" is not reproducible. C counts 4,976 dropped items in 1,300 files under the old rule, so the problem is understated, not overstated. No code impact.
  • C's three surviving mutants behave the same as the shipped code, so they are cosmetic.

Residuals:

  • Exec-wrapped function_call_output results are still dropped by isCodexExecWrapperOutput. No exec_command_end event replaces them in the owner's corpus, so exec_command cards can still show no result. That includes the df -h example cited in Codex transcript silently drops a tool result or message that shares a millisecond with its sibling #1288, whose identity collision this PR fixes. This predates the PR and is outside its diff. No follow-up issue is filed yet.
  • The B2/B3 test gaps, and the exec-wrapper comment that states a justification the corpus contradicts.
  • Repeated keys with identical payloads are still deduped on purpose.

Merges through batch PR #1318 once every member passes its gate.

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit b72c5f0 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/codex-rollout-identity branch September 25, 2026 18:02
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.

Codex transcript silently drops a tool result or message that shares a millisecond with its sibling

1 participant