Files
buzz/desktop/src-tauri
9128b9389b fix(desktop): preserve failed unread mutations and fence archive session acquisition
Round-2 review found two defects in the previous repair commit.

**The mutation whose ingest failed was dropped.** `enqueueNative`'s recovery
reopened the authoritative snapshot and resolved, so the captured mutation was
never retried or preserved — and the reopened snapshot is the store *without*
that write. A rejected marker un-read the channel; a rejected `removeChannel`
/`clearAll` resurrected rows the user had just cleared, because those delete
their projection optimistically first. The previous test proved the chain
healed, not that the operation survived: it asserted only that the *next*
command landed.

Every captured mutation now gets one retry after `reopen` refreshes sequence
and revision, which is safe because ingest is idempotent — events upsert `DO
NOTHING`, channel latest advances by `MAX`, membership is `INSERT OR IGNORE`,
and a replayed sequence returns a snapshot rather than reapplying. A
`snapshotRequired` response takes the same recovery path instead of being
treated as success. If reopen and retry both fail, native is explicitly
degraded and the equivalent event/marker/latest/clear state is applied to the
JS fallback, so `isNative()` stops reporting healthy. Membership needs no
fallback copy: its renderer-owned sets remain authoritative.

**A superseded archive start could tear down the newer scope's relay session.**
`begin` claimed ownership and released its guards, then `start_archive_sync`
awaited `archive_session`. `ensure_session` shuts down a different scope's
socket and installs its own inside its own lock, and `attach_archive` replaces
the session's archive sender outright — both destructive on entry. So a stale
start B could win `begin`, pause, let newer C claim and install, then resume,
shut C's session down, and attach the archive stream to a task whose token was
already cancelled. B cleared its subscriptions on exit and archive sync stayed
dead until the next lifecycle edge.

Revalidating the mark after the await cannot fix this: by the time B discovers
it lost, C's socket is already gone, and a session is spawned rather than handed
back, so there is nothing to restore it from. The damage is done by the call, so
the fence is around the call. `begin` now returns an `ArchiveOwnership` token
holding both guards, and `archive_session` requires one. The token is
un-constructible outside `archive::sync`, so a stale start cannot reach the call
at all. This is sound because acquisition performs no I/O: both halves await
only mutex acquisitions and the socket connects on a spawned task.

Reverting the token does not compile, so the Rust regression pins the property
its usefulness rests on — while an owner holds the token, no newer start can
claim. That goes red against the mutant this design exists to stop: keeping the
token but releasing the guards inside `begin`, which compiles and restores the
race. The JS regressions assert the originally rejected marker, destructive
clear, and membership delta each survive, and go red when the retry is removed.

Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
2026-08-17 17:05:11 -04:00
..