Skip to content

feat(chat): backfill transcripts after a feed sync - #628

Open
bmc08gt wants to merge 2 commits into
mainfrom
feat/chat-message-backfill
Open

feat(chat): backfill transcripts after a feed sync#628
bmc08gt wants to merge 2 commits into
mainfrom
feat/chat-message-backfill

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Chat history was only ever fetched by opening a chat: loadMessages(for:) had exactly one caller (ConversationScreen, on open) and catchUp fired only for the visible chat or a detected live gap. A fresh login — switching accounts included — starts from a per-owner database with no messages in it, so every conversation in the feed sat empty until it was opened by hand.

What changed

  • ConversationController.loadFeed() now backfills the conversations the feed reported, four at a time, outside the loading flag — the feed is on screen the moment the conversations apply and the transcripts fill behind it.
  • The branch is chosen from the catch-up cursor, which is the only thing that says whether a transcript was ever pulled: a zero cursor takes the newest page, and a seated cursor behind the feed's latest_event_sequence resumes with GetDelta from where it sits. Choosing on "does this chat hold any message" does not work — the feed persists each conversation's last-message preview as a message row before the backfill plans, so that test is true for nearly every chat in a feed the client has otherwise never fetched. Order matters too: a GetDelta(after: 0) would re-pull whole histories and prepend them, knocking an open transcript off the bottom, so delta only runs on a cursor that is already seated.
  • Work is deduped by conversation id (the feed loads per type) and skips any chat whose newest page the open screen is already fetching.
  • Conversation carries the server's latestEventSequence from the feed payload. It is server truth valid only at fetch time, so it is deliberately not cached — a conversation restored from the local database reports 0, meaning "unknown", not "empty". No schema change: the local cursor is already persisted as ConversationTable.catchupCursor.

Tests

Four new cases in ConversationControllerTests cover the empty-conversation backfill, the cached no-op, the lagging-cursor delta path, and the regression above — a chat holding only the feed's preview row still gets its transcript fetched. Two existing cases were updated for the new premise: RESET_REQUIRED… now forgets the page start()'s backfill fetched, and the old "a reconnect with no conversation open fetches no transcript" is now "a reconnect backfills a chat it surfaces" — that behavior is the point of the change.

Chat history was only ever fetched by opening a chat, so a fresh login —
switching accounts included — started from a per-owner database with no
messages in it, and every conversation in the feed showed an empty
transcript until it was opened by hand.

Port Android's `FeedSyncDelegate` pass: after each feed load, bring every
conversation it reported level with the server's head, four at a time.
Branch order matches Android's — a local cursor that lags the feed's
`latest_event_sequence` is streamed forward with `GetDelta` from where it
sits, and only a conversation holding nothing at all falls back to the
newest page. Reversing that would spend a `GetDelta(after: 0)` re-pulling
whole histories and prepending them.

`Conversation` now carries the server's `latestEventSequence`. It is
server truth valid only at fetch time, so it is deliberately not cached:
a conversation restored from the local database reports 0, meaning
"unknown", not "empty".
The feed writes each conversation's last-message preview as a message row
before the backfill plans its work, so "holds any message" was true for
nearly every chat in a feed the client had otherwise never fetched. With a
fresh database the cursor is also 0, which blocks the delta branch — so the
pass queued nothing at all on exactly the fresh-login case it exists for.

Ask instead whether a transcript was ever pulled: only a newest page or an
applied delta seats a catch-up cursor. Zero cursor means fetch the newest
page; a seated cursor behind the server's head means resume via GetDelta.

Verified on the simulator against a wiped database: 47 conversations, 47
newest-page fetches, no duplicates, and the following reconnects queue
nothing once the cursors are seated.
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