Conversation
…'s ids onto stale zombies CDP-attach to the live app (2026-07-15) caught the last unexplained double- paint mechanism in the act. A long session held a STALE optimistic assistant row (its completion frame severed by an earlier backend restart). When the NEXT turn completed, markTurnComplete stamped the frame's [user_id, assistant_id] with a top-down, role-blind walk over non-committed rows: user_id landed on the stale ASSISTANT zombie and assistant_id on the fresh USER row. The zombie now wore a committed id — invisible to the #361 sweep (which only targets optimistic ids) — so its stale text painted as a permanent duplicate, and the actually-fresh rows stayed un-stamped. Always 'the last message', only in restart-scarred sessions: exactly the reported symptom. stampOptimisticTranscriptRows now assigns role-aware and tail-first: with the [user, assistant] two-id contract, each id claims the LAST unclaimed stampable row of its role; a lone id prefers the assistant streamed row. Stale zombies stay optimistic — and therefore remain sweepable by #361 when their committed twins arrive via the poll. Tests: 4 new (mis-stamp guard, end-to-end zombie sweep after correct stamp, normal-path unchanged, lone-id preference). RED-proven: restoring the old top-down walk fails 3. Suite 1334/1334, tsc clean.
| if (committedIds.length === 2) { | ||
| claim(committedIds[0], 'user') | ||
| claim(committedIds[1], 'assistant') | ||
| } else { | ||
| // Lone id: the [user, assistant] positional contract can't disambiguate a | ||
| // single survivor. Prefer the streamed assistant row (the dup-prone one — | ||
| // the poll always re-fetches the final text bubble), fall back to any. | ||
| for (const id of committedIds) { | ||
| if (!claim(id, 'assistant')) { | ||
| claim(id, null) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The
else branch handles every committedIds.length !== 2 case — including lengths of 3 or more — but its logic and comment only make sense for exactly one ID. extractCommittedMessageIds can return 3+ IDs when both an array field (message_ids) and a scalar field (message_id) are non-empty in the same payload. In that situation the for loop calls claim(id, 'assistant') for every ID, including committedIds[0] which is the positional user ID — repeating the exact cross-role mis-stamp this PR fixes, just for the >2 case. Tightening the condition to === 1 keeps the assistant-preference logic scoped to its documented intent.
| if (committedIds.length === 2) { | |
| claim(committedIds[0], 'user') | |
| claim(committedIds[1], 'assistant') | |
| } else { | |
| // Lone id: the [user, assistant] positional contract can't disambiguate a | |
| // single survivor. Prefer the streamed assistant row (the dup-prone one — | |
| // the poll always re-fetches the final text bubble), fall back to any. | |
| for (const id of committedIds) { | |
| if (!claim(id, 'assistant')) { | |
| claim(id, null) | |
| } | |
| } | |
| } | |
| if (committedIds.length === 2) { | |
| claim(committedIds[0], 'user') | |
| claim(committedIds[1], 'assistant') | |
| } else if (committedIds.length === 1) { | |
| // Lone id: the [user, assistant] positional contract can't disambiguate a | |
| // single survivor. Prefer the streamed assistant row (the dup-prone one — | |
| // the poll always re-fetches the final text bubble), fall back to any. | |
| if (!claim(committedIds[0], 'assistant')) { | |
| claim(committedIds[0], null) | |
| } | |
| } else { | |
| // Unexpected count (>2 or 0 after the early-exit guard): fall back to | |
| // role-blind tail-first assignment so at least the tail rows get stamped. | |
| for (const id of committedIds) { | |
| claim(id, null) | |
| } | |
| } |
| if (committedIds.length === 2) { | ||
| claim(committedIds[0], 'user') | ||
| claim(committedIds[1], 'assistant') |
There was a problem hiding this comment.
Silent drop when no user-role stampable row exists
In the two-id branch, claim(committedIds[0], 'user') returns false when there are no unclaimed user-role rows in stampable — and the return value is silently discarded. Unlike the lone-id path (which falls back to claim(id, null)), the user ID is simply lost. This is fine if the user row was already committed before this stamp fires, but it could silently drop a valid user ID in any edge case where the optimistic user row was cleared without getting a committed integer ID. Adding a fallback claim(committedIds[0], null) when the role-specific claim fails would mirror the lone-id path's defensive behaviour.
|
Superseded by #397 — a re-land onto current main (this branch is 705 commits behind and conflicts with the livesync Phase-1 hook changes). Both LIVE Greptile findings are adjudicated there: the no-user-row silent drop is deliberate (documented + test-pinned — cross-role fallback would re-create the mis-stamp), and 3+-id frames are handled + test-pinned. RED-proven against the old impl. Authorship credited via Co-authored-by. |
… ids onto stale zombies (re-land of #364) Re-lands #364 (author: Kyzcreig; branch was 705 commits behind) onto current main. CDP-attach caught the mechanism live 2026-07-15: a stale optimistic assistant row (completion frame severed by a backend restart) absorbed the fresh turn user_id via the old top-down role-blind walk — the zombie wore a committed id (invisible to the #361 sweep) and painted as a permanent duplicate. stampOptimisticTranscriptRows now assigns role-aware and tail-first. Both LIVE Greptile #364 findings adjudicated: (1) silent user-id drop when no user-role stampable row exists is DELIBERATE and now documented + test-pinned — cross-role fallback would re-create the exact mis-stamp this fixes; the poll reconciles the committed row safely. (2) 3+-id frames (array + scalar fields both populated) now documented + test-pinned: assistant-preferring tail-first walk, extras left for the poll, no crash. RED-proven: swapping back the fork/main top-down impl fails 5/36 (the 3 zombie vectors + both edge-frame tests). Hook suite 36 green; full desktop suite delta vs clean fork/main baseline = zero new failures (17 pre-existing reds on both, 1 local-env flake passes 3/3 in isolation); tsc errors identical to baseline (7, all pre-existing). Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
* fix(desktop): role-aware tail-first stamping — stop stamping the turn ids onto stale zombies (re-land of #364) Re-lands #364 (author: Kyzcreig; branch was 705 commits behind) onto current main. CDP-attach caught the mechanism live 2026-07-15: a stale optimistic assistant row (completion frame severed by a backend restart) absorbed the fresh turn user_id via the old top-down role-blind walk — the zombie wore a committed id (invisible to the #361 sweep) and painted as a permanent duplicate. stampOptimisticTranscriptRows now assigns role-aware and tail-first. Both LIVE Greptile #364 findings adjudicated: (1) silent user-id drop when no user-role stampable row exists is DELIBERATE and now documented + test-pinned — cross-role fallback would re-create the exact mis-stamp this fixes; the poll reconciles the committed row safely. (2) 3+-id frames (array + scalar fields both populated) now documented + test-pinned: assistant-preferring tail-first walk, extras left for the poll, no crash. RED-proven: swapping back the fork/main top-down impl fails 5/36 (the 3 zombie vectors + both edge-frame tests). Hook suite 36 green; full desktop suite delta vs clean fork/main baseline = zero new failures (17 pre-existing reds on both, 1 local-env flake passes 3/3 in isolation); tsc errors identical to baseline (7, all pre-existing). Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com> * fix(desktop): 3+-id frames keep the positional [user, assistant] pair role-aware (Greptile #398) --------- Co-authored-by: Apollo <apollo@kyzcreig.local> Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Symptom (last unexplained double-paint mechanism)
In ONE long, restart-scarred session: the newest assistant reply painted twice, DB and render cache both verifiably clean, fresh sessions unaffected. Survived #352/#357/#361/#362.
Root cause — caught live via CDP-attach to the running app
The session held a stale optimistic assistant row (completion frame severed by an earlier backend restart). On the next turn's
message.complete, the stamp walked non-committed rows top-down and role-blind: the frame'suser_idlanded on the stale ASSISTANT zombie,assistant_idon the fresh USER row. The zombie now wore a committed id — invisible to the #361 sweep, which only targets optimistic ids — so its stale text painted as a permanent duplicate and the fresh rows stayed un-stamped. Deterministically 'the last message'.Fix
Role-aware, tail-first assignment in
stampOptimisticTranscriptRows: with the[user_id, assistant_id]contract each id claims the last unclaimed stampable row of its role; a lone id prefers the assistant streamed row. Stale zombies keep optimistic ids → remain sweepable by #361 when their committed twins arrive via the poll (covered end-to-end in the new tests).Tests
4 new cases; RED-proof: restoring the old top-down walk fails 3 of them. Full desktop suite 1334/1334, tsc clean.