Skip to content

fix: stop codex app-server transcript re-printing streamed reasoning/messages on idle - #428

Closed
dimavedenyapin wants to merge 1 commit into
mainfrom
fix/codex-app-server-reprint-live-reconcile
Closed

fix: stop codex app-server transcript re-printing streamed reasoning/messages on idle#428
dimavedenyapin wants to merge 1 commit into
mainfrom
fix/codex-app-server-reprint-live-reconcile

Conversation

@dimavedenyapin

Copy link
Copy Markdown
Contributor

Problem

Codex is still re-printing previous messages after the last message in the agent transcript, even after PR #419.

Root cause (incomplete fix, not a regression)

PR #419 made reconcileCompletedTurn() idempotent for the reconcile-vs-reconcile path (bufferedThreadItemIds + computeThreadItemKey) — it stops the same item being re-buffered across idles. But it never covered the live-stream-vs-reconcile path: on every turn/completed the adapter re-lists the whole thread and re-emits items that were already delivered live during the turn. When the reconcile emission lands under a different part id/type than the live emission, the renderer cannot collapse the two and appends a duplicate below the final message.

The only commit touching this file since #419 is #423 (git metadata roots), which is unrelated to reconcile/buffering — so this is an incomplete fix, not a regression. #419's logic and its regression test still pass.

Two concrete, reproduced paths (investigation subtask confirmed both):

  1. Reasoning items — streamed live via item/reasoning/textDelta as part id reasoning-<id> (REASONING). convertThreadItem() had no reasoning branch, so on reconcile the same item routed into the tool branch and was emitted as tool-<id> (TOOL, name reasoning). Different part id and type → the renderer appends a duplicate.
  2. Assistant final message when the persisted thread-item id differs from the streaming itemId — live delta emits agent-<streamId>, reconcile emits agent-<persistedId>, both update:false → appended twice. The hasSeenAssistantTextForTurn guard missed it because streaming deltas never call markAssistantTextForTurn() (only convertThreadItem's item/completed branch did), so the text wasn't registered for the turn when reconcile ran.

Fix

Make the reconcile emission reuse the same part identity as the live path:

  1. Add a reasoning branch to convertThreadItem() that emits reasoning-<itemId> (REASONING, role: assistant), updating in place when the part was already streamed — so it collapses against the live reasoning part instead of appearing as a separate tool call.
  2. Register streamed assistant text for the turn via markAssistantTextForTurn() inside the item/agentMessage/delta handler, so a later reconcile item/completed for the same message (under a different persisted id) is recognized as already-shown and dropped.

Both changes are additive and scoped to the transcript emission path.

Tests

  • Added reconciles a reasoning item as a REASONING part, not a duplicate tool part
  • Added does not re-print a streamed assistant message when reconcile re-lists it under a different item id

Both new tests fail on main (reproduce the bug) and pass with the fix.

  • pnpm typecheck
  • eslint on changed files
  • ✅ Full main project suite: 918 tests passed (24 in the codex adapter, incl. the 2 new)

🤖 Generated with Claude Code

…messages on idle

PR #419 made reconcileCompletedTurn() idempotent for reconcile-vs-reconcile
re-buffering, but the transcript still repeated messages after each turn because
the LIVE-stream-vs-RECONCILE path was uncovered. On every turn/completed the
adapter re-lists the whole thread and re-emits items already delivered live; when
the reconcile emission lands under a different part id/type than the live one, the
renderer cannot collapse them and appends a duplicate below the final message.

Two reproduced paths, both fixed by making the reconcile emission reuse the live
part identity:

1. Reasoning items: streamed live via item/reasoning/textDelta as `reasoning-<id>`
   (REASONING), but convertThreadItem() had no reasoning branch, so the reconcile
   copy fell through to the tool branch and was emitted as `tool-<id>` (TOOL) — a
   distinct part. Add a reasoning branch that emits `reasoning-<id>` (REASONING),
   updating in place when already streamed.

2. Assistant final message when the persisted thread-item id differs from the
   streaming itemId: live delta emits `agent-<streamId>`, reconcile emits
   `agent-<persistedId>`, both update:false, so the final message is appended
   twice. The hasSeenAssistantTextForTurn guard missed it because streaming deltas
   never registered their text for the turn (only convertThreadItem's item/completed
   did). Register streamed assistant text via markAssistantTextForTurn() in the
   delta handler so the reconcile copy is recognized as already-shown and dropped.

Adds regression tests proving each logical item is emitted under a single part
identity across the live stream and the completed-turn reconcile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dimavedenyapin

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #427. #427 contains this live-stream-vs-reconcile app-server fix plus the additional AP-task evidence fix for Codex turn_id/thread-level assistant text dedupe.

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