Skip to content

Replace the Loro workspace doc with a wedge-proof row registry (sidebar sync) - #18

Merged
wingleeio merged 3 commits into
mainfrom
registry-sidebar-sync
Aug 5, 2026
Merged

wingleeio merged 3 commits into
mainfrom
registry-sidebar-sync

Conversation

@wingleeio

@wingleeio wingleeio commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the Loro CRDT workspace doc with a server-authoritative row registry: the sidebar index (devices, spaces, chats, session status) is now a plain table in a Durable Object with per-field last-write-wins, a monotonic change cursor, and live row broadcasts. Session docs (transcripts) stay on Loro — this only changes the sidebar plane. Design doc: docs/registry-sync.md.

Why

Three incidents in one week (2026-07-30, 08-04, 08-05) shared one root cause: the CRDT keeps history, and the workspace DO had to replay/re-export that history in wasm under a CPU limit — while loro's snapshot export deterministically RangeErrors on workerd, so the room could never self-compact. All that machinery bought masterless merge that the topology never uses: every write already flows through one DO. ~150 sessions wedged the room; the problem was never the row count, it was the remembered history.

The registry stores current state only. 10,000 sessions ≈ a few MB of rows, bounded forever. DO cold start is a table read — no replay, no wasm, no compaction, no fold budgets. The wedge class is gone by construction.

Live demo — remote engine drives the sidebar through the registry

Headed app (device A) on the left; every change below arrives from a second, headless engine ("demo-vps", device B) through a real RegistryRoom DO: space created, chat created, a mock run (Working → done indicators), two renames, and a space rename.

live sync

🎬 Full-resolution video: docs/media/registry-sync/live-sync.mp4

Remote chat arrives Transcript + auto-title synced Renames + indicators settled
arrives transcript final

Design

engine A ── RegistryDoc (rows + pending ops) ── RegistryClient ─┐
                                                                ├─ RegistryRoom DO (reg1/{org}/{user})
engine B ── RegistryDoc ── RegistryClient ──────────────────────┘   SQLite rows + seq counter
  • edge/src/registry-room.ts — new DO class: applies pushed op batches atomically with per-field HLC LWW (registry-core.ts, pure + unit-tested), bumps one seq per batch, broadcasts merged rows to every socket. Cursor sync: hello {cursor} → only rows with seq > cursor. Tombstone GC (30d) with a gcFloor full-resync guard; nightly seq-monotonic R2 backup; per-device pushOutcomes attribution on /registry/:orgId/stats from day one.
  • crates/doc/src/registry.rs — RegistryDoc: authoritative rows + pending-op overlay (optimistic reads, offline queue), typed API drop-in for WorkspaceDoc, whole-state persistence in DocsStore. The merge fn mirrors the TS one 1:1 with shared test vectors.
  • crates/sync/src/registry.rs — RegistryClient: hello/cursor handshake, push/ack, presence beats (memory-only, replaces %EPH), probe/redial liveness with the same deaf-socket discipline as RoomClient, reconnect backoff, RoomStatsSnapshot-compatible stats so comet sync and SyncStatus render unchanged.
  • WorkspaceHost — same public surface; presence deafness tripwire, relay-probe fallback, and seen-cache all preserved.

Conflict semantics are unchanged in practice: the loro schema resolved same-field conflicts by LWW map sets already; the registry runs the same rule as ~40 auditable lines (HLC = ms-counter-deviceId, total order). update ops never invent rows; deletes only win when causally newer; batches apply atomically (space-delete cascades).

Migration — instant, on first boot

No registry1 snapshot → read the legacy workspace2 loro snapshot → every row becomes a pending upsert with historical HLCs derived from the row's own timestamps (live writes always beat migrated values) → saved immediately. The UI reads the overlay before any server contact — zero visible gap. N devices seeding the same converged doc is idempotent. Old ws4 rooms are simply never joined again (legacy /workspace routes remain for older builds); the legacy snapshot stays on disk for rollback.

Recovery got strictly better: a wiped/reset room is detected by seq regression on the next hello and every client re-seeds it automatically with original per-field clocks — the 2026-08-05 manual repair recipe, built in and lossless. POST /registry/:orgId/reset exists for operators and is safe to use.

Testing

  • TS unit (registry-core.test.ts, 13 cases): field LWW, tie-break by device, null-field deletes, tombstone/revive causality, guard tombstones, re-seed clock preservation, convergence over all causally-valid arrival orders, validation.
  • Rust unit (crates/doc/src/registry/tests.rs, 21 cases): the same merge vectors mirrored, HLC monotonicity across clock regressions, wire-shape compatibility with the TS JSON, overlay/ack lifecycle, cascade delete, seen-marker monotonic guard, persistence round-trip, migration seeding (identical state, live-write precedence, N-device idempotence).
  • Client integration (crates/sync/tests/registry_client.rs, 10 cases, in-process mock server running the same merge fn): live two-client convergence, cursor delta on reconnect, idempotent replay of unacked writes, server-wipe auto-reseed, gcFloor full resync, presence, probe stats, DocsStore restart with offline queue, and a churn regression: 2,001 writes → 2 rows, and a fresh device syncs 2 rows, not 2,001 updates.
  • Real Durable Object (wrangler dev, workerd): registry_edge.rs --ignored — two Rust clients through the actual TS DO (convergence, LWW agreement across the language boundary, presence, probe, 500-write churn bounded); the two-engine --ignored live test; scripts/e2e-smoke.sh passes every registry step (device rows, space/chat rows, cross-device command queue).
  • Full cargo test --workspace green; clippy clean on changed crates; edge tsc + vitest green; wrangler deploy --dry-run validates the DO migration (v2: new_sqlite_classes: [RegistryRoom]).

Known-unrelated: the smoke's final step (session-doc transcript to the viewer device) stalls on this box's local workerd — it fails on main too (earlier, even: the loro workspace room can't sync a single row there), and the transcript path is untouched by this PR. The demo recording above shows transcripts syncing fine in the same environment.

Notes

  • One less wasm dependency in the DO hot path; the registry room has zero timers (daily alarm only) and hibernates cleanly.
  • comet sync, SyncStatus, ProbeSync, and the Devices page work unchanged.
  • Rollback: revert the engine update; the legacy loro snapshot and ws4 rooms are still there.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

wing-anara and others added 3 commits August 5, 2026 05:19
…orkspace doc

Edge: RegistryRoom DO (SQLite rows + seq cursor, per-field HLC LWW in
registry-core.ts), /registry/:orgId routes, wrangler v2 migration.
Rust: comet-doc RegistryDoc (authoritative rows + pending overlay, typed
WorkspaceDoc-compatible API, legacy-doc migration seeding), comet-sync
RegistryClient (hello/cursor, push/ack, presence, probe/redial, backoff)
+ mock server; WorkspaceHost swapped onto the registry with the same
public surface, presence tripwire, and SyncStatus stats.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wingleeio
wingleeio merged commit 92fba3d into main Aug 5, 2026
1 check passed
Gratenes pushed a commit to Gratenes/zeron that referenced this pull request Sep 20, 2026
Merge upstream zeronsh/zeron v0.2.68 (preserving fork changes)
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.

2 participants