Skip to content

fix(move): read the host left behind until every contact has followed - #6

Merged
koabula merged 6 commits into
mainfrom
fix/drain-previous-host
Nov 11, 2026
Merged

koabula merged 6 commits into
mainfrom
fix/drain-previous-host

Conversation

@koabula

@koabula koabula commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Why

A move tells each contact the new address in a lane notice inside the session. Until a contact applies that notice, it keeps appending to the old host on the old lane. Nothing read the old host after the move, so whatever contacts wrote there in the meantime was lost:

  • A contact that was offline flushed its queue before it fetched the notice, so all of that queue went to the old host and was lost.
  • A contact that was online lost what it sent while the notice was in flight.
  • A move made during a commit race could not settle. The winner's commit went to the loser's old lane, so the loser never saw it.

What

Engine

  • Previous inbox. On a move, the device keeps the old host's deployment, its cursor there, and each conversation's old inbound lane. Together these are the previous inbox, persisted with the deployment.
  • Reading the old host. Each sync, and a 5-minute poll, reads the old host under a sync key of its own (previous:{device}), so that host has its own cursor, dedup set and quarantine. Records there route to their conversations by the old lanes and go through the ordinary inbound path. They are never acknowledged at that host; they expire there on their own.
  • End of the drain. Reading stops once every conversation's peer has been seen at the new host and the old host has nothing left, or after Δ (30 days).
  • On the contact's side. Applying a lane notice readdresses whatever is still queued on the old lane to the new one, and restarts its retries. A message that could not reach a host already gone then goes to the new one.

Platforms

  • The profile and the CLI driver keep the replaced credential when a credential for another runtime is installed. They pick the credential a request names by its runtime id.
  • The desktop refreshes the kept credential at the old host when it nears expiry.

Ledgers. No new request or event kind: reading the old host reuses GetHead and FetchMessages, and there is no new emission site.

Tests

  • a_move_goes_on_reading_the_host_it_left. Bob reads his old host under its own credential and takes Alice's message on the old lane, with no ack sent there. When Alice applies the notice, her queued envelope moves to the new lane.
  • the_old_host_is_read_until_every_peer_has_followed. The drain ends once the peer has written to the new host and the old host is read to its end.
  • a_lane_moved_during_a_race_is_announced_after_the_rebuild. A move made during a race settles, and the winner follows the loser's new lane. This test was moved out of fix(direct): open items from the client's spec, and the KeyPackage pool count route #5, where it could not pass without this change.

Not verified locally

There is no Rust toolchain on this machine; CI is the first build.

🤖 Generated with Claude Code

koala and others added 2 commits November 11, 2026 20:33
A move tells each contact the new address in a lane notice inside the
session, and until the contact applies it, it keeps appending to the old host
on the old lane. Nothing read that host after the move, so whatever contacts
wrote there in the meantime was lost; a contact that was offline, and so
flushed its queue before it fetched the notice, lost all of it. A move made
during a commit race could not settle either: the loser's rebuild Welcome went
to the old host.

The device now goes on reading the old host. On a move it keeps the old host's
deployment, its cursor and each conversation's old inbound lane as the
previous inbox (persisted with the deployment). Each sync, and a 5-minute
poll, reads that host under a sync key of its own, so it has its own cursor,
dedup set and quarantine; records there route to their conversations by the
old lanes, are processed as usual, and are never acknowledged at that host.
The drain ends once every conversation's peer has been seen at the new host
and the old one has nothing left, or after Δ (30 days), the bound within which
a contact is held to follow.

Reading the old host needs the credential held there, so the profile and the
CLI driver keep the replaced credential when a credential for another runtime
is installed, and pick the one a request names by runtime id. The desktop
refreshes the kept credential at the old host when it nears expiry.

On the contact's side, applying a lane notice now readdresses what is still
queued on the old lane to the new one and restarts its retries, so a message
that could not reach a host already gone goes to the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restores the test moved out of the previous change: with the host left
behind still read, the winner's commit on the loser's old lane is taken, the
race settles, and the rebuild's lane notice reaches the winner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@koabula
koabula enabled auto-merge (squash) November 11, 2026 12:51
koala and others added 4 commits November 11, 2026 20:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Readdressing every queued envelope when a peer's notice arrived also moved
the ones in flight, which then held the new lane until their requests to the
old host came back: one request to a host already gone could hold every later
message for as long as its timeout. A notice now readdresses only what is
idle; an envelope in flight keeps its lane, and if it fails, the flush before
its retry readdresses it to its peer's current lane.

The relocation test now checks that what Alice queued goes to Bob's new host
once she applies the notice, with later messages after it in order. The new
test picks Alice's message by its text, settles its append if it went out,
and reports what Bob held when it does not take the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts inbox key

The batch passed its sync key to the application-plaintext check and to the
conversation it creates on a first-contact Welcome, both of which want the
device itself. For the host a move left behind the key is previous:{device},
so every application record read there was refused as addressed to another
device. Both now get the recipient the records are addressed to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@koabula
koabula merged commit bb0f1b1 into main Nov 11, 2026
7 checks passed
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