feat(storage): replicate messages + read receipts across the fleet (uuid-keyed incremental sync, interim shared storage)#11
Open
andrei-hasna wants to merge 4 commits into
Open
Conversation
…emental sync (interim fleet shared storage) Conversations was a per-machine SQLite island: messages and message_read_receipts were excluded from the storage sync surface, so a message posted on one machine was invisible everywhere else (fleet comms Phase 0 blocker, todos 1e723ce4 / tracking 843cfb62). - NEW src/lib/message-sync.ts: uuid-keyed incremental append-only replication of messages + message_read_receipts through the shared hub Postgres. Per-machine integer ids collide fleet-wide, so reply_to and receipt message_id translate through the parent message uuid on each side; per-machine cursors live in local never-synced _message_sync_state. - v1 conflict semantics: append-only; on re-sync content wins by newer edited_at, read_at/pinned_at are set-once. Steady-state edits/deletes do not re-send (documented; v2 change-log follow-up). Receipts hold the cursor back until their message replicates. TIMESTAMPTZ params are sent zone-explicit and normalized back to naive-UTC text on pull. - storage push/pull/sync default table set now includes messages + message_read_receipts; --no-messages opts out; storage status shows message-sync cursors. syncPush/syncPull route message tables to the new engine (generic pk-upsert engine still never touches them). - db.ts: idempotent uuid repair on open (backfill NULL/duplicate uuids + unique index) — 627 NULL-uuid rows observed on spark01 from an older migration that added the column without backfill. - docs/FLEET-SHARED-STORAGE-INTERIM.md: on-prem hub architecture, staged satellite runbook (spark02 done; apple01/03/06 staged), rollback. Marked CUTOVER: gate off with the rest of the legacy sync engine (A1).
…storage subpath test machine-independent - Hub ids are assigned at INSERT but visible at COMMIT: with two machines pushing concurrently, a row can appear behind an already-advanced pull cursor and be skipped forever. pullMessages now re-scans a fixed id margin (200) behind the saved cursor each run; the WHERE-gated conflict clause makes the overlap idempotent and free. Regression test simulates the late-visible row. - storage-sync.test.ts asserted getStorageDatabaseUrl() is null, which read the machine's real storage/config.json — fails on any machine with remote storage configured (as fleet machines now are). Force local mode via env in the test; also cover the new message-sync exports.
Contributor
Author
|
Adversarial self-review findings (reconciled in 2ec7e6d) — for the independent reviewer:
Live evidence so far: hub landed exactly 24,403 (spark01) + 11,295 (spark02) = 35,698 messages — zero uuid collisions across machines; 627 legacy NULL uuids repaired on spark01 by the new open-time repair pass. Cross-machine canary read-back running now; results in todos 1e723ce4. |
The legacy pk-upsert engine is append-only, so 'agents remove' on one machine was resurrected by the next storage pull. This silently undid the 2026-07-06 supervised registry purge (579 -> 98 agents rebounded to 583; todos bc244f4d x 1e723ce4, confirmed by both rollout adversaries). - new _sync_agent_tombstones table (SQLite lazy-create + PG migration 3) - removePresence() records a tombstone; renameAgent() tombstones the old name, bumps last_seen_at, and clears a stale tombstone on the new name - storage push uploads tombstones and deletes remote agent_presence rows older than their tombstone; storage pull downloads tombstones and reconciles local rows the same way (including rows the same pull just resurrected). Re-registered/heartbeating agents always outlive tombstones - tombstone timestamps use the legacy engine's raw-naive convention so comparisons stay internally consistent (documented caveat) - runbook: mandatory pre-cutover registry groom per machine + semantics - 13 new tests (full suite 992 pass / 0 fail) Task: 843cfb62 (fleet comms rollout, fixer stream); refs bc244f4d, 1e723ce4
…n-wins guard The presence upsert conflict target was (agent, project_id) but both stores enforce uniqueness on agent alone; a name reused with a different project_id on another machine took the INSERT path, violated idx_agent_presence_agent_unique, and aborted the entire presence table sync (spark02's registry push never converged during the 2026-07-06 cutover, and the re-purge convergence hit the same wall in both directions). - conflict target for agent_presence is now the agent name - new per-table UPDATE_GUARDS: presence updates only apply when excluded.last_seen_at > agent_presence.last_seen_at, so a stale replica can never regress fresher presence on push OR pull - test DDLs mirror production's idx_agent_presence_agent_unique; 2 new tests (994 pass / 0 fail) Task: 843cfb62 (fleet comms rollout, fixer stream); refs bc244f4d, 1e723ce4
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
Fleet comms Phase 0 blocker (todos task 1e723ce4, tracking 843cfb62, strategy wf_09ba3f51-f00): conversations is a per-machine SQLite island.
messagesandmessage_read_receiptssit inSYNC_EXCLUDED, so a message posted on spark01 is invisible on apple01 — every comms design assumption (durable channel record, [FREEZE] visibility, ack receipts) is invalid until they replicate.They were excluded for a real reason: SQLite
messages.idis per-machine AUTOINCREMENT — ids collide fleet-wide, so the generic full-table pk-upsert engine would corrupt them.What
src/lib/message-sync.ts— uuid-keyed incremental append-only replication ofmessages+message_read_receiptsthrough a shared hub Postgres.reply_toand receiptmessage_idtranslate through the parent message's uuid on each side; per-machine cursors live in the local never-synced_message_sync_statetable.docs/FLEET-SHARED-STORAGE-INTERIM.md): append-only; on re-sync content wins by neweredited_at,read_at/pinned_atset-once; receipts hold the cursor back until their message replicates; TIMESTAMPTZ params sent zone-explicit so hub session TZ can never shift instants. Known limitation: steady-state edits/deletes/pins do NOT re-send (no change log yet — v2 follow-up; secret-purge must run per machine until then).storage push/pull/syncnow includes the two tables (--no-messagesopts out);storage statusshows cursors/counts;syncPush/syncPullroute message tables to the new engine.db.tsuuid repair pass on open — long-lived DBs carry NULL uuids from an older migration that added the column without backfill (627 on spark01); uuid is the replication key so it must be total + unique.CUTOVER: gate offwith the rest of the legacy sync engine; the pure-remote flip lane removes it (seedocs/CUTOVER-RUNBOOK.md— unchanged).Verification
bun test: 979 pass / 0 fail (16 new tests: round trip, bidirectional exchange, reply_to remapping across id spaces, edit LWW, receipt translation + holdback, uuid backfill idempotency, TZ normalization).tsc --noEmitclean.hasna_conversations), spark01 + spark02 configured, cross-machine canary read-back — evidence in todos task 1e723ce4 comments.Rollout / rollback
spark01 + spark02 first; apple01/03/06 staged (runbook included). Rollback = delete
storage/config.json(runtime is still local SQLite; hub down never blocks an agent). NO AWS involved (gate 97610c99 closed).