fix: stop codex app-server transcript re-printing streamed reasoning/messages on idle - #428
Closed
dimavedenyapin wants to merge 1 commit into
Closed
fix: stop codex app-server transcript re-printing streamed reasoning/messages on idle#428dimavedenyapin wants to merge 1 commit into
dimavedenyapin wants to merge 1 commit into
Conversation
…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>
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 everyturn/completedthe 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):
item/reasoning/textDeltaas part idreasoning-<id>(REASONING).convertThreadItem()had no reasoning branch, so on reconcile the same item routed into the tool branch and was emitted astool-<id>(TOOL, namereasoning). Different part id and type → the renderer appends a duplicate.itemId— live delta emitsagent-<streamId>, reconcile emitsagent-<persistedId>, bothupdate:false→ appended twice. ThehasSeenAssistantTextForTurnguard missed it because streaming deltas never callmarkAssistantTextForTurn()(onlyconvertThreadItem'sitem/completedbranch 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:
convertThreadItem()that emitsreasoning-<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.markAssistantTextForTurn()inside theitem/agentMessage/deltahandler, so a later reconcileitem/completedfor 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
reconciles a reasoning item as a REASONING part, not a duplicate tool partdoes not re-print a streamed assistant message when reconcile re-lists it under a different item idBoth new tests fail on
main(reproduce the bug) and pass with the fix.pnpm typecheckeslinton changed files🤖 Generated with Claude Code