Files
twenty/packages
Félix Malfait 77529191f8 fix(ai): apply stream chunks in exact server seq order via a client-side sequencer (#22484)
## 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. -->
2026-07-02 21:17:11 +02:00
..