Skip to content

Seed legacy reactions in migration and serve them from the v2 read path (rebuild RR-2)#139

Merged
MaxGhenis merged 1 commit into
mainfrom
reactions-rr2-seed
Jul 18, 2026
Merged

Seed legacy reactions in migration and serve them from the v2 read path (rebuild RR-2)#139
MaxGhenis merged 1 commit into
mainfrom
reactions-rr2-seed

Conversation

@MaxGhenis

Copy link
Copy Markdown
Owner

Rebuild RR-2 — seed legacy reactions in migration + serve from v2read

Completes the reactions read model behind cutover. With RR-1 (#138) writing live reactions and this PR seeding + serving historical ones, the top behavioral gap on the cutover list is closed: 9,441 reaction rows from 5,455 reaction-bearing messages on the real store survive migration and render in v2-primary.

What's in it

  • Seeding: legacy messages.reactions JSON ([{"emoji","count","actors"}] — ground-truthed against all three platform writers + the UI parser) seeds through RR-1's first-wins SeedReaction contract. Seeds lose to any live write; no fence rows (first live Google snapshot establishes each fence).
  • Actor-key convergence (the review's blocking finding, fixed): seeded keys are derived exactly as live ingest derives them — WhatsApp JID→E164 mirroring the live decoder, Signal resolved-E164 vs raw-ACI matching live's resolve-or-raw capture, and self-detection against per-platform own numbers derived from is_from_me rows → self/is_self=1/"me", field-identical to the live worker. Without this, every seeded WhatsApp reaction (2,289 messages on the real store, incl. 84 self) would duplicate or go stale on the first post-cutover delta.
  • Accounting: row-grained reconciliation (plannable vs seeded) with explicit collapse/surplus/dedup/dropped-with-parent counters; validation enforces messages_with_reactions == seeded + malformed + unmappable + dropped_with_parent exactly.
  • Serving: one batched ReactionsForMessages per page (no N+1), active rows only, byte-compatible with the legacy read path including insertion order; conversation page, search, and message lists all covered.

Review

Opus adversarial review: FIX-FIRST → all six findings fixed. The convergence discriminators now drive the actual platform decoder → ingest worker (not hand-built entries) for WhatsApp and Signal, migrate-then-live-apply, asserting exactly one row with live-shaped fields; each fails on the pre-fix key derivation (e.g. WA contact 24ea023f…b8222756…, WA self f6d6c1ec…self). Served-JSON byte-parity pins non-lexicographic insertion order and the documented collapse/surplus degradations.

Real-store rehearsal (third run, this exact build)

  • 9,441/9,441 rows reconciled at ratio 1; integrity gates green; source untouched.
  • 84 self-reactions seed as self (was the proven duplicate/stuck-reaction bug — those rows previously seeded as ordinary identities keyed by Max's own JID).
  • 13 reactions-on-dropped-messages attributed exactly via the new counter; I5 equation holds: 5,442 + 0 + 0 + 13 = 5,455.
  • Fences 0; 5 actorless-collapse units counted (matches probe: 2 entries, both SMS).

Verification

Independent full GOWORK=off go test -race ./... outside the sandbox: 31/31 packages, exit 0 (three runs: post-build, post-fixes, post-discriminators).

🤖 Generated with Claude Code

…th (rebuild RR-2)

Completes the reactions read model: the legacy messages.reactions JSON now
survives cutover (9,441 rows from 5,455 reaction-bearing messages on the
operator's real store) and v2-primary serves reactions in the same JSON the
web UI already parses.

Seeding (migration transform):
- Parses the legacy per-message reactions JSON
  ([{"emoji","count","actors"}]) and seeds rows through RR-1's first-wins
  SeedReaction contract: seeds lose to any live write and never touch
  reaction_snapshot_fences (the first live Google snapshot establishes each
  fence).
- Actor tokens normalize to EXACTLY the identity forms live ingest derives,
  so a post-cutover frame lands on the seeded primary key instead of
  minting a duplicate: WhatsApp canonical JIDs normalize to E164 the same
  way the live decoder does; Signal actors split resolved-E164 vs raw-ACI,
  matching live's resolve-or-raw capture behavior; self-reactions are
  detected against per-platform own numbers derived from is_from_me rows
  (guarding blank and non-E164 garbage senders) and seed as
  key "self" / is_self=1 / label "me", byte-identical to the live worker's
  convention. Identity-resolved rows carry the live label convention.
  Signal self-reactions whose actor is an ACI are an accepted, documented
  corner: the legacy store cannot resolve own-ACI, and delta platforms only
  re-deliver on new activity for that (message, reactor).
- Convergence is pinned end-to-end per platform: migrate a fixture with
  real actor forms, then deliver a live reaction frame through the actual
  decoder and ingest worker, asserting exactly one row with live-shaped
  fields.

Accounting (drop-with-count discipline):
- Row-grained reconciliation: plannable rows vs seeded rows, with explicit
  counters for actorless-count collapse, actor-count surplus, in-message
  duplicates, and reactions on dropped parent messages. Validation enforces
  messages_with_reactions == seeded + malformed + unmappable +
  dropped_with_parent exactly, and reactions row count == rows seeded.
- Malformed reaction JSON degrades per message without blocking migration.

Serving (v2read):
- Message DTOs populate Reactions from one batched ReactionsForMessages
  call per page (no N+1), active rows only, serialized byte-compatibly with
  the legacy read path including legacy insertion order (seeded rows keep
  order via per-entry timestamp offsets; first-wins semantics are
  repo-level, so the offsets cannot outrank live writes). Empty set serves
  the legacy empty representation.

Real-store rehearsal (third run): 9,441/9,441 rows reconciled at ratio 1,
84 self-reactions seeded as self, 13 dropped-with-parent attributed
exactly, fences 0, integrity gates green, source untouched.
@MaxGhenis
MaxGhenis merged commit f5bff75 into main Jul 18, 2026
5 checks passed
@MaxGhenis
MaxGhenis deleted the reactions-rr2-seed branch July 18, 2026 18:00
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