Skip to content

Send message history when linking a device (GRYT-1574) - #33

Merged
sivert-io merged 1 commit into
mainfrom
claude/GRYT-1574-history-transfer
Sep 28, 2026
Merged

sivert-io merged 1 commit into
mainfrom
claude/GRYT-1574-history-transfer

Conversation

@sivert-io

Copy link
Copy Markdown
Member

GRYT-1574. Build item 9 in docs/pairing-design.md on crypto main. It builds on #28's addOwnDevice and groupPositions and #29's pairing client.

What happens now

A (approving):

  • At Approve it notes every group's cursor with groupPositions, then walks the archive through HistoryArchive. That's per conversation, a page at a time, merged newest UTC day first across servers.
  • Each day goes through planHistoryChunks, sealHistoryChunk, and a PUT to the relay. A chunk only gets listed once it's uploaded.
  • The envelope's history carries the key and the manifest of what's up after at most 5 seconds (or 10 seconds before the approval deadline). Anything after that goes in sealed {t: "history", chunks: [{n, ...entry}], total, final} messages, 180 entries each.
  • After the adds comes the tail: every noted message with a seq past the snapshot, and before the add or in an older epoch. Then 60 seconds for late old-epoch ones, and a last message with final.
  • A doesn't close the session at the end, because closing deletes the chunks. N closes it.

N (new device):

  • Starts fetching as soon as it has written the envelope, newest chunk first, and keeps going as more pages arrive.
  • Runs openHistoryChunk against each entry. A chunk that fails is refused, one the relay lost is missing, and both are counted. Neither stops the rest.
  • Hands each message to HistorySink once, keyed by scope, conversation and message id.

Both sides get history and subscribeHistory for progress (messages, total, chunks, missing, refused, truncated, complete). Cancel works as before, at any point.

API changes the apps will see

  • OwnDeviceAdder needs groupPositions now. MlsDmDriver already has it, so passing the driver works.
  • New options: history on both sides, lateWindowMs on A.
  • New noteMessage on A. The app calls it for every MLS message it archives while a pairing runs, sent or received, with its seq and epoch. Without it the tail is empty.
  • New phase sending on A, and new end reason history_failed on N for when the sink throws.

Relay
auth#45 has no chunk endpoints. The design has them as auth step 4. I built against the shape in the design, with a fake: PUT/GET/DELETE /sessions/:id/chunks/:n, sealed bytes as base64url in {body}. I picked JSON over raw bytes because React Native's fetch is patchy with binary bodies. That's GRYT-1591 now.

Limits
The relay allows 64 messages a session for both sides together. N uses four and the envelope one. So A lists at most 400 + 40 × 180 = 7,600 snapshot chunks, and stops at 248 MiB. Past either, the oldest stay behind and truncated says so.

Least sure about

  • The late window is 60 seconds. The design says "as long as MLS keeps the old epoch's keys", and I didn't find a number for that.
  • The five-second budget before the envelope. A long history won't fit in it, so most of it goes in later messages, and N waits for the first page until after ready.
  • A broken archive read before the envelope fails the whole pairing as relay_error. It could link without history instead.
  • An N on core 0.15 paired with an A on this waits in linked for up to an hour, since A no longer closes the session. Both apps take the same core release, so this shouldn't come up.

Not in this PR

  • Carrying on after either app restarts within the hour: GRYT-1592.
  • The relay's chunk endpoints: GRYT-1591.

Tests (src/pairing/history.test.ts, fake relay and fake clock)

  • 5,000 messages in three conversations on two servers over about 600 days. Every message arrives once, batches arrive newest first, and there are more than 400 chunks, so the pages get used.
  • A chunk changed on the relay is refused and the rest still arrive.
  • A chunk the relay lost is reported as missing.
  • The tail: messages land at Approve, during the add, and after it in both the old and the new epoch. No gaps, no repeats, and nothing N reads itself.
  • N with no sink finishes on A's last message.
  • N cancelling partway ends A with cancelled_by_other.

I checked the tail and ordering tests fail when the tail, the dedupe, the epoch check or the newest-first pick is broken.

Normal review: nothing here is in a review-required path.

🤖 Generated with Claude Code

Build item 9 in docs/pairing-design.md. When the app passes its archive,
the approving device (A) sends its history to the new one (N).

On Approve, A notes each MLS group's position, then reads the archive
newest day first through HistoryArchive, seals it into chunks with
@gryt/crypto's chunk format and uploads them to the relay. The
envelope's history carries the key and the manifest of whatever is up
after at most five seconds. Later sealed "history" messages list the
rest, a page of 180 entries at a time, each with its relay slot.

After the adds, A sends the tail. That's every message it archived past
the snapshot that N can't read: before N's add, or sent in an older
epoch and landing after it. The app reports those with noteMessage. A
waits a minute for late ones, sends a last message marked final, and
leaves the session open, because closing it deletes the chunks.

N fetches chunks newest first, checks each one against its manifest
entry, and hands each message to HistorySink once. A chunk that fails
the check is refused and counted. A chunk the relay no longer has is
counted as missing. N closes the session once the final message has
come and every chunk is handled.

The relay has no chunk endpoints yet (GRYT-1591), so the client and the
fake follow the design: PUT, GET and DELETE on /sessions/:id/chunks/:n,
with the sealed chunk as base64url in JSON.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sivert-io
sivert-io merged commit 446c63f into main Sep 28, 2026
5 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-1574-history-transfer branch September 28, 2026 23:41
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