Repository navigation
Conversation
…of quarantining them A live conversation snapshot with a roster entry that has no number, or one v2keys can't key, failed IdentityKey in refreshConversation and quarantined the whole frame, so the thread missed that update (kind, title, roster). A group v2 first saw through an incoming message stayed a direct thread with the sender as its peer, and catch-up history skipped every fetched message of it as contradicting that thread (third review of #200, finding F1). refreshConversation now drops such entries before anything reads the roster, so the roster write and #176's rebinding (googleConversationEventTarget, through resolveEventPeers) judge the same entries. A snapshot that left entries off keeps each stored member it doesn't name that a left-off entry may be: any member when one of them isn't flagged as the account, only the account's own rows when all are. A complete snapshot still replaces the roster. Live inbox (9,155 Google frames, 2026-07-19 to 10-09): 4 snapshots hit this, all 1:1 threads listing the account itself several times without a number. Replayed into an empty store, quarantines drop from 66 to 62 and the final state is identical. The runbook now diagnoses the recurring quarantine alerts and explains why re-applying quarantined frames is unsafe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Finding F1 of the third review of #200. On the live path,
refreshConversationkeyed every roster entry of a Google conversation snapshot. One entry with no number (libgm delivers these) or a number v2keys rejects madeIdentityKeyfail, and the whole frame was quarantined. The thread then missed that update: its kind, title and roster.The review's worst case is a group that v2 first sees through an incoming message:
history_skipped).The live inbox has 4 such frames; details under "Live inbox evidence" below.
What
refreshConversationdrops roster entries without a usable address (addressable: a non-empty value thatv2keys.IdentityKeyaccepts) before anything reads the roster. The roster it writes and Route Google frames by participant identity across device id-space resets #176's rebinding (googleConversationEventTarget) therefore judge the same entries.resolveEventPeersuses the same check.A snapshot that left entries off removes only members it can rule out. A stored member the snapshot doesn't name stays when a left-off entry could be that member:
Without this rule, a 1:1 snapshot whose peer has no number would wipe the thread's peer. That would also disable Route Google frames by participant identity across device id-space resets #176's sender check for the thread.
One Info log per such snapshot:
Left conversation snapshot participants without a usable address off the roster(unaddressable,unaddressable_self).Runbook:
Invariants
These are executed by
TestLiveRosterPropertyUnaddressableEntriesChangeNothingElse(testing/quick, 120 cases). Each case compares two stores given the same setup: one gets the snapshot, the other the same snapshot without its unaddressable entries.The generated rosters mix several kinds of entry:
+, or a blank number, flagged as the account or not.They are applied to a wire v2 didn't know, or knew from an incoming message, an outgoing message, a 1:1 or a group, with or without other threads to rebind to.
remote_rebindsare identical to the clean snapshot's.A coverage check fails the test if any of 6 case classes goes unsampled. The classes include: self-only drop removes an unnamed peer, peer drop keeps one, rebound, and direct corrected to group.
Tests
TestLiveGroupSnapshotWithANumberlessMemberCorrectsAMessageMintedThread, the review's scenario through the real decoder:lr-gas direct.history_skipped=2).remote_rebinds=0,quarantined=0.TestLiveSnapshotListingSelfWithoutANumberAppliesLikeTheCleanOne: the observed shape (self listed 5 times, 4 without a number). Every table matches the clean snapshot's.TestLiveSnapshotWithUnaddressableEntriesKeepsTheMembersTheyMayBe, four subtests:TestLiveRebindJudgesTheAddressableRoster: a stale binding plus a+entry and an entry with no number moves exactly as the clean snapshot does. It is a differential against a twin store, so it holds under Keep same-roster Google threads distinct; re-key only threads whose id is dead #201's rebinding rules too.Mutants (each through
go test -overlay; generator in the evidence folder):Killed: the old code (fails all 5 tests) and 13 mutants:
The property test alone kills each mutant of the keep rule.
Equivalent: rebinding given the unfiltered event while
resolveEventPeerschecksaddressable(it reads the roster only through that function).Not killed:
resolveEventPeersreverted to the empty-only check. The live path filters first, so it is unreachable there. On the history path it only decides whether an unbound snapshot with a+entry mints or is skipped, and that is unreachable once History stale-binding check: ignore unkeyable roster entries, and pin its controls #207's filter lands.go test ./internal/ingest/andgo vetpass.Live inbox evidence
These figures come from an online backup of the live
v2/store.sqlite3(9,155 Google frames, 2026-07-19 to 10-09).IsMe), all but one copy without a number: wires 1334 (08-08), 2923 (09-11), 2411 (09-21) and 2981 (10-09 05:50 EDT). No group was affected, and no+entry was seen.mainquarantines 66 frames: 59updated_at_ms >= created_at_msCHECK failures (Keep same-roster Google threads distinct; re-key only threads whose id is dead #201 changes this), 4identity value is empty, and 3message_idUNIQUE failures. With this branch it quarantines 62.quarantined: 1on 10/9 is consistent with the 2981 frame, the only replayed quarantine after 10/2.Frames already quarantined: no replay
No command re-applies a processed frame. The only mechanism is to clear
processed_at_mswith the daemon down;Worker.Runthen drains every unprocessed row. I tried it on a copy of the live store with this branch's worker:updated_at_ms. Later clean snapshots had already given those threads the same state.remote_rebinds=1). An outgoing message on 1333 then started a new thread.So nothing needs repairing on this install, and a blind re-apply can split a live thread. The runbook says to wait for the thread's next live snapshot instead, then run a window backfill. It also lists the precautions if a re-apply is ever needed:
remote_rebindsmust stay 0.This matters for the planned re-drive of processed-but-unapplied frames (#196 follow-up).
Known limits
googleConversationEventTargetkeeps the stored binding (no peers to judge it by), and the snapshot's title and kind are written to that thread.mainalready does the same for a snapshot that lists only the account. Before, such a snapshot was quarantined. After a phone swap that left the wire stale-bound, the title would land on the old thread until that thread's own snapshot arrives. This has never been seen live.+, or a reaction from one, still quarantines its frame; a sender with no number already becomes a null sender. That is History stale-binding check: ignore unkeyable roster entries, and pin its controls #207's review finding 4, and it is filed as a separate task.Coordination
applyHistoryConversation's filter toaddressableParticipants(same rule).refreshConversationhunks sit next to this one; the conflict is textual only. The rebind test and the property compare against a twin store, so they don't depend on Route Google frames by participant identity across device id-space resets #176's specific rules.Evidence, harnesses and the store snapshot:
~/reviews/openmessage-live-roster-2026-10-09/.🤖 Generated with Claude Code