## 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. -->
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





