77529191f8
## Rationale Stream chunks reach the client on two unsynchronized paths: live SSE events and the catchup replay (fired on reload, refetch, SSE reconnect, and keep-alive recovery). The server already stamps every chunk with an authoritative `seq` (Redis `RPUSH` length), but the client applies chunks in **arrival order**. Reload mid-stream and the two paths interleave: duplicated text deltas, or lower-seq catchup chunks applied after higher-seq live ones — the streaming answer visibly garbles until the persist-refetch repaints it. Main's existing guard (`seq < firstLiveSeq` bound on catchup) only prevents duplication in one direction (live-before-catchup); it does nothing for catchup-during-live overlap, and it *creates* a dropped-chunk window when chunks land between the catchup snapshot and the first live event. ## Why this is the root cause, not a symptom patch The defect is a joining problem between two ordered sources, and the join point is the client — the server can't fix it without a protocol change (per-subscriber cursor resume), because Redis pub/sub fan-out has no per-subscriber replay. Given the transport, the correct fix is to make the reducer's input **seq-exact**: apply strictly in server order, dedup anything already applied, buffer early arrivals until the gap fills. Escalation is bounded and degrades gracefully: a stalled gap triggers one refetch (the full-list catchup replay doubles as gap-fill, no new endpoint), a second stall flushes the buffer in order — so even an expired chunk list degrades to slightly-lossy instead of wedging. The catchup path now replays the full list (the sequencer dedups overlap), which also closes the dropped-chunk window. Server-side cursor resume remains the nicer long-term protocol (would simplify this client), but it's a subscription protocol change; this fixes the user-facing defect with zero server change and is forward-compatible with it. ## User impact Reloading (or losing the connection) mid-answer currently scrambles or duplicates the streaming text until the turn completes. With this, the answer renders identically no matter when you reload or how the two delivery paths race. ## Test plan - [x] Sequencer unit suite (fake timers): in-order apply, out-of-order buffering, catchup/live overlap dedup, gap-fill via replay, stall→refetch escalation, second-stall in-order flush, high-water-mark continuation, reset - [ ] CI green - [ ] Manual: reload mid-stream repeatedly; text never reorders https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38 --- _Generated by [Claude Code](https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22484?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->