Skip to content

fix: stop Codex app-server transcript repeating older messages after idle - #419

Merged
dimavedenyapin merged 1 commit into
mainfrom
fix/codex-app-server-transcript-repeat-on-idle
Jul 15, 2026
Merged

fix: stop Codex app-server transcript repeating older messages after idle#419
dimavedenyapin merged 1 commit into
mainfrom
fix/codex-app-server-transcript-repeat-on-idle

Conversation

@dimavedenyapin

Copy link
Copy Markdown
Contributor

Problem

In the Codex agent transcript, older messages get repeated after the last ongoing idle — every time a Codex turn completes and the thread goes idle, previously-shown messages are re-appended to the transcript, so the conversation keeps growing with duplicates.

Root cause

CodexAppServerAdapter.reconcileCompletedTurn() (added in #412) re-lists the entire thread via thread/items/list on every turn/completed, to make sure the final assistant message isn't lost. Re-buffering the whole history is only safe if it's idempotent — and it wasn't:

  • The dedup guard bufferedThreadItemIds only registered an item when asString(item.id) was truthy.
  • extractItemId() fell back to `item-${Date.now()}` when no id was found.

But many Codex thread item types have no top-level id — only a call_id, or nothing at all. Verified against real Codex rollout logs (~/.codex/sessions/.../rollout-*.jsonl) for the referenced task pf-web: analyse slow mongodb queries (thread 019f4a0d-e8c7-73f3-8592-6b5f312d1b5b, 12 turns):

item type has id
function_call_output (168×) ❌ (only call_id)
custom_tool_call_output
tool_search_output
user/developer input message

So on every reconcile those id-less items bypassed the dedup, got re-buffered, and were re-emitted to the renderer with a fresh item-${Date.now()} part id that neither seenPartIds nor the renderer's id-keyed dedup could collapse → the transcript repeated older messages after each idle.

Fix

Make reconcile buffering idempotent for every item:

  • deriveItemIdentity() derives a stable identity from id / itemId / call_id.
  • computeThreadItemKey() falls back to a deterministic content hash (turn + type + role + text) when there's no id-like field.
  • bufferReconciledThreadItem() (shared by bufferThreadItems and bufferAllThreadTurns) skips already-buffered items by that key and forwards it as itemId, so the derived part id stays stable across passes.
  • extractItemId() no longer uses Date.now() — it uses the same stable identity, then a deterministic content hash.

Reproduction / test

Added a regression test that fires three turn/completed reconciles returning the same id-less tool output and asserts it is buffered exactly once (was 3× before the fix).

  • New + existing codex-app-server-adapter tests: 19 passing
  • Full pre-commit suite: 1533 passing

🤖 Generated with Claude Code

reconcileCompletedTurn() re-lists the entire Codex thread on every
turn/completed. The dedup guard (bufferedThreadItemIds) only registered
items that had a top-level `id`, and extractItemId fell back to
`item-${Date.now()}`. But many Codex item types have no `id` — only a
`call_id` or nothing (function_call_output, custom_tool_call_output,
tool_search_output, user/developer input messages). Those id-less items
were therefore re-buffered and re-emitted with a brand-new part id on
every idle, so the transcript repeated older messages after each turn.

Make reconcile buffering idempotent: derive a STABLE key for every item
(id/itemId/call_id, else a deterministic content hash), skip already
buffered items using that key, and forward it as `itemId` so the derived
part id stays stable across passes. Also drop the Date.now() fallback in
extractItemId for a deterministic content-based id.

Adds a regression test proving an id-less tool output is buffered exactly
once across multiple completed turns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dimavedenyapin
dimavedenyapin merged commit ade8939 into main Jul 15, 2026
1 check 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