Skip to content

fix(history,web): stop user messages rendering as duplicate bubbles - #12

Open
Antisophy wants to merge 3 commits into
CyberShadow:masterfrom
Antisophy:fix/user-message-identity-dedup
Open

fix(history,web): stop user messages rendering as duplicate bubbles#12
Antisophy wants to merge 3 commits into
CyberShadow:masterfrom
Antisophy:fix/user-message-identity-dedup

Conversation

@Antisophy

@Antisophy Antisophy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Three cooperating defects made user messages render as duplicate bubbles after the queue-primary history rework; this PR fixes all three. Live-recorded histories were affected on every message when replayed.

1. The live consumed confirmation names the wrong identity

The live queue tail emitted user_message/consumed with the echo's own uuid as both the identity and native_uuid, discarding the awaited enqueue anchor. Live that happens to work (the nonce correlation upgrades the optimistic bubble, which is the message's display), but the stored record is replay-broken by construction: replay strips the nonce, and no bubble carries the echo's uuid at that point in the stream, so on reload the confirmation was a no-op. The confirmation now carries the awaited anchor as its identity and the echo's uuid as native_uuid, matching the replay pipeline. Histories already recorded with the misnamed shape are recognizable (uuid equals native_uuid), and the queue is FIFO, so the reducer resolves them against the oldest still-pending enqueue-emitted bubble; a correctly named confirmation never reaches that fallback.

2. A replayed echo could not displace an upgraded placeholder

A replayed history delivers a live-sent message as the unconfirmed placeholder plus the canonical echo, with the consumed confirmation between them. The confirmation upgrades the placeholder in place (correct live, where no echo bubble renders at all), but the upgrade clears the pending state, and the echo's displacement only accepted pending placeholders, so the echo appended beside the upgraded bubble. On a replayed echo, a uuid-less user bubble is now displaceable whether or not it is still pending; canonical messages always carry uuids, so they are never displaced, and the widening applies to replay echoes only, so two genuine identical live sends still render as two bubbles. Observed end to end in a headless browser against a real task before and after: the wire delivers each form exactly once, and the fix takes the rendering from two bubbles to one.

3. No identity dedup on append

The same user record can also reach the reducer more than once (overlapping history replays merge without a reset). Assistant content survives this because its blocks are keyed by item id; user messages were appended fresh every time. An arriving user message whose uuid already exists now updates that bubble in place.

Tests

Reducer tests cover: the same record arriving twice, distinct records staying distinct, pending re-delivery not regressing an upgraded bubble, a nonce-correlated confirmation upgrading (not dropping) the optimistic placeholder, the misnamed-confirmation FIFO healing, a correctly named confirmation leaving a still-queued same-text bubble alone, and the full stored sequence (unconfirmed placeholder, consumed, echo) rendering as one bubble. The existing no-reset duplication baseline still passes, pinning that genuine identical sends keep separate bubbles.

nix flake check: full set built per commit; the handful of integration checks that failed under full parallel load passed at low parallelism each time, in different sets per run.

The same user record can reach the reducer more than once. Overlapping
history replays merge without a reset (the pendingHistoryReplies window
keeps existing messages when a second replay begins mid-flight), and a
live echo can already be present when its replayed copy arrives.
Assistant content survives this because its blocks are keyed by item id;
user messages were appended with a fresh id every time, so one prompt
rendered as two full bubbles after switching between tasks.

A user record's uuid is its identity, so an arriving user message whose
uuid already exists in the transcript now updates that bubble in place
(content, seq, source) instead of appending a second copy. The update
never reapplies the pending presentation, so a re-delivered pending copy
cannot regress a bubble that consumption has already upgraded. Records
without a uuid (optimistic placeholders) are untouched, as is the
placeholder displacement that runs before the guard.
@Antisophy Antisophy changed the title fix(web): dedup user messages by record identity in the reducer fix(history,web): stop user messages rendering as duplicate bubbles Aug 5, 2026
The live queue tail emitted user_message/consumed with the echo's own
uuid as both the identity and native_uuid, discarding the awaited
enqueue anchor. Live that happens to work: the correlation nonce
upgrades the optimistic bubble in place, which is the message's display.
But the stored record is replay-broken by construction: replay strips
the nonce, and no bubble ever carries the echo's uuid at that point in
the stream, so on every reload the confirmation was a no-op and the
pipeline-derived provisional survived beside its canonical echo. Every
message rendered twice after any reload.

Name the awaited anchor as the confirmation's identity with the echo's
uuid as native_uuid, exactly as the replay pipeline already does. The
live path is unchanged in effect (the nonce still upgrades the
optimistic bubble; a uuid-less placeholder is never dropped, since no
separate echo bubble renders live), and the stored record now resolves
its provisional on replay.

Histories recorded while the live path was miswired hold the bad
confirmations. Those are recognizable (uuid equals native_uuid, nonce
stripped by replay), and the queue is FIFO, so such a confirmation can
only describe the oldest still-pending enqueue-emitted bubble: drop that
one. A correctly named confirmation resolves directly and never reaches
the fallback, so a genuinely queued later message with identical text is
not touched.
@Antisophy
Antisophy force-pushed the fix/user-message-identity-dedup branch from cbf1c36 to bd3a216 Compare August 5, 2026 22:11
A replayed history delivers a live-sent message in two forms: the
unconfirmed placeholder (nonce-correlated, no uuid) and later the
canonical echo. Between them sits the consumed confirmation, which
upgrades the placeholder in place; that is correct live, where no echo
bubble renders at all. But the upgrade clears the pending state, and the
echo's displacement only accepted pending placeholders, so on replay the
echo appended beside the upgraded bubble and the message rendered twice.

On a replayed echo, also displace a uuid-less user bubble that is no
longer pending: it is still the same message in placeholder form, and
canonical messages always carry uuids so they are never displaced. The
widening applies to replay echoes only; a live no-nonce send keeps the
original pending-only rule, so two genuine identical sends still render
as two bubbles, which is also what keeps the existing no-reset
duplication baseline test passing.

Observed end to end in a headless browser against a real task: the wire
delivers the unconfirmed envelope and the seq-bearing echo exactly once
each, and the previous reducer rendered them as two bubbles.
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