Skip to content

🤖 fix: avoid replay/live delta duplication on reconnect#2165

Open
ethanndickson wants to merge 5 commits intomainfrom
fix/replay-live-delta-dedupe
Open

🤖 fix: avoid replay/live delta duplication on reconnect#2165
ethanndickson wants to merge 5 commits intomainfrom
fix/replay-live-delta-dedupe

Conversation

@ethanndickson
Copy link
Member

@ethanndickson ethanndickson commented Feb 5, 2026

Summary

Prevent duplicated streamed deltas (including reasoning) on renderer reconnects, avoiding repeated-prefix UI artifacts (e.g. EvaluEvaluEvalu...) and double token counting.

Background

workspace.onChat subscribes to live session events before awaiting replayHistory(). If a renderer reconnects mid-stream, live stream-delta / reasoning-delta events can arrive while replay is in progress and then be emitted again by replayStream() with replay: true. The frontend aggregator is append-only, so duplicated delivery becomes repeated prefixes.

Implementation

  • Backend (src/node/orpc/router.ts)

    • Keep “subscribe before replay” semantics (so stream replay + init replay aren’t missed).
    • During replay, buffer live stream events and then flush after caught-up, skipping any deltas already delivered by replay (dedupe key: type + messageId + timestamp + delta).
    • Buffer terminal stream events (stream-end / stream-abort / stream-error) alongside deltas to preserve causal ordering.
    • Track replayed delta keys with a counted map (not a Set), so we only drop as many buffered duplicates as were actually replayed.
    • Extracted the replay buffering/dedupe logic into src/node/orpc/replayBufferedStreamMessageRelay.ts to keep the onChat handler minimal.
  • Backend (src/node/services/streamManager.ts)

    • Emit a part before making it visible to replayStream() (avoids replay snapshotting an “in-flight” part and then also receiving the later live emit).
    • Make per-stream part timestamps strictly monotonic (so same-millisecond identical chunks don’t collide during replay dedupe).

Validation

  • make static-check

Risks

  • During the replay window, live stream events are temporarily buffered (not dropped). In the unlikely case of a legitimate duplicate delta with identical (type, messageId, timestamp, delta), it will be skipped; monotonic timestamps reduce the chance of false positives.

Generated with mux • Model: openai:gpt-5.2 • Thinking: medium • Cost: $20.78

Buffer live stream/reasoning deltas during workspace.onChat replay and flush after caught-up, skipping deltas already emitted by replayStream().\n\nAlso emit parts before adding them to streamInfo.parts to avoid replay snapshotting in-flight parts.

---\n\n_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: `2.69`_\n\n<!-- mux-attribution: model=openai:gpt-5.2 thinking=xhigh costs=12.69 -->
@github-actions github-actions bot added the bug label Feb 5, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad84e0c995

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Buffer stream-end/stream-abort/stream-error alongside deltas during replay so causal ordering is preserved.
- Use counted replay keys and strictly monotonic per-stream timestamps to avoid delta identity collisions.

---

_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: `$16.50`_

<!-- mux-attribution: model=openai:gpt-5.2 thinking=xhigh costs=16.50 -->
@ethanndickson
Copy link
Member Author

@codex review

Addressed P1/P2:

  • Buffer stream-end/stream-abort/stream-error alongside deltas during replay so terminal events can't overtake buffered deltas.
  • Use counted replay keys (Map) + strictly monotonic per-stream part timestamps to avoid dedupe key collisions.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ed764b95d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Codex follow-up: replay dedupe keys include delta text and don't need to be retained after replay
finishes. Clear them to avoid per-connection memory growth.

---

_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: `$16.50`_

<!-- mux-attribution: model=openai:gpt-5.2 thinking=xhigh costs=16.50 -->
@ethanndickson
Copy link
Member Author

@codex review

Follow-up: cleared replay dedupe state after flush to avoid retaining delta text for the lifetime of the subscription.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

No behavior change; just moves the replay buffering/dedupe logic into a small helper so the
workspace.onChat handler stays linear/readable.

---

_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: `$18.48`_

<!-- mux-attribution: model=openai:gpt-5.2 thinking=xhigh costs=18.48 -->
@ethanndickson
Copy link
Member Author

@codex review

Refactor-only: extracted the onChat replay buffering/dedupe logic into a helper in router.ts to keep the handler linear/readable.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Extract createReplayBufferedStreamMessageRelay into src/node/orpc/replayBufferedStreamMessageRelay.ts to keep router.onChat minimal.

---

_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `medium` • Cost: `$20.58`_

<!-- mux-attribution: model=openai:gpt-5.2 thinking=medium costs=20.58 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant