SPEC-003: realtime collaborative editing via p2p CRDTs #3

Closed
hugooconnor wants to merge 0 commits from refs/pull/3/head into main AGit
hugooconnor commented 2026-06-19 00:46:54 +00:00 (Migrated from codeberg.org)

Spec (v1.1.0) + implementation of multiplayer editing: Loro CRDT edit document, iroh transport + blob-integrity sync, serverless pkarr/Mainline-DHT discovery, SPAKE2 pairing over iroh, and a session daemon. 38 tests passing. Details in plans/IMPL-003-STATUS.md. Intentional gates: SPAKE2 crypto review (ADR-009), real-DHT round-trip (OQ-7), daemon lifecycle/remote-sync wiring (OQ-8).

Spec (v1.1.0) + implementation of multiplayer editing: Loro CRDT edit document, iroh transport + blob-integrity sync, serverless pkarr/Mainline-DHT discovery, SPAKE2 pairing over iroh, and a session daemon. 38 tests passing. Details in plans/IMPL-003-STATUS.md. Intentional gates: SPAKE2 crypto review (ADR-009), real-DHT round-trip (OQ-7), daemon lifecycle/remote-sync wiring (OQ-8).
hugooconnor commented 2026-06-19 01:42:39 +00:00 (Migrated from codeberg.org)

Review issues addressed — 9ae2ec1

All five points fixed, each with a regression test. Full transport-feature suite (34 tests) passes under rustc 1.93; vault clean.

# Issue Fix Test
P1 Note loss after snapshot import CollabDoc::import() now advances note_counter past this actor's existing note ids (sync_note_counter), so a reloaded doc (same ActorId) never reuses actor:00000000 and overwrites a note notes_survive_reload_same_actor
P1 Blobs > 512 MiB rejected fetch_blob streams chunks and hashes incrementally (BLAKE3 verified streaming) instead of read_to_end(512 MiB) — multi-GB sources transfer blob test now uses a 3 MiB (multi-chunk) blob
P1 Default binary fails to build on Windows the daemon command body is #[cfg(unix)] and returns an unsupported-platform error on non-unix; shell::daemon stays Unix-gated cargo check -p ar-edit
P2 Relay URL dropped on discovery parse_record restores the relay= attribute via with_relay_url, so a relay-only (WAN/NAT) host stays dialable record_preserves_relay_url
P2 CRDT deltas beyond the frame cap CON-015 sync envelopes use a dedicated MAX_SYNC_FRAME (64 MiB) instead of the 64 KiB control-frame cap; real snapshots/offline batches parse sync_envelope_allows_large_payload (128 KiB)

Spec updated to match (CON-015 cap, REQ-075 streaming). Two iroh-API corrections surfaced during the build and were fixed: RecvStream::read_chunk returns Option<Bytes> (not a Chunk), and SecretKey::generate() takes no args.

## Review issues addressed — `9ae2ec1` All five points fixed, each with a regression test. Full transport-feature suite (34 tests) passes under rustc 1.93; vault clean. | # | Issue | Fix | Test | |---|-------|-----|------| | **P1** | Note loss after snapshot import | `CollabDoc::import()` now advances `note_counter` past this actor's existing note ids (`sync_note_counter`), so a reloaded doc (same `ActorId`) never reuses `actor:00000000` and overwrites a note | `notes_survive_reload_same_actor` | | **P1** | Blobs > 512 MiB rejected | `fetch_blob` streams chunks and hashes incrementally (BLAKE3 verified streaming) instead of `read_to_end(512 MiB)` — multi-GB sources transfer | blob test now uses a 3 MiB (multi-chunk) blob | | **P1** | Default binary fails to build on Windows | the `daemon` command body is `#[cfg(unix)]` and returns an unsupported-platform error on non-unix; `shell::daemon` stays Unix-gated | `cargo check -p ar-edit` | | **P2** | Relay URL dropped on discovery | `parse_record` restores the `relay=` attribute via `with_relay_url`, so a relay-only (WAN/NAT) host stays dialable | `record_preserves_relay_url` | | **P2** | CRDT deltas beyond the frame cap | CON-015 sync envelopes use a dedicated `MAX_SYNC_FRAME` (64 MiB) instead of the 64 KiB control-frame cap; real snapshots/offline batches parse | `sync_envelope_allows_large_payload` (128 KiB) | Spec updated to match (CON-015 cap, REQ-075 streaming). Two iroh-API corrections surfaced during the build and were fixed: `RecvStream::read_chunk` returns `Option<Bytes>` (not a `Chunk`), and `SecretKey::generate()` takes no args.
hugooconnor commented 2026-06-19 03:06:18 +00:00 (Migrated from codeberg.org)

Review round 2 addressed — 6935489

All five points fixed with regression tests. Full collab matrix: 43 tests pass (rustc 1.93).

# Issue Fix Test
P1 Concurrent inserts collide on duplicate shot ids add_new_shot mints an actor-scoped id for live inserts (no cross-replica collision); add_shot disambiguates a local duplicate instead of overwriting; shot_counter restored on import generated_shot_ids_avoid_collision_across_replicas, add_shot_disambiguates_local_duplicate_id
P2 Migrated docs hard-code actor 1 from_event_sourced(ed, actor) migrates under a fixed peer id (idempotent across independent migrations) then re-keys to the caller's node actor for later edits (rekey_actor) independent_migrations_converge_without_duplication
P2 New daemon unlinks a live socket bind() probes first and refuses (AlreadyRunning) if a live daemon answers; removes only a proven-stale socket daemon_refuses_when_already_running
P2 Rendezvous waiter never dropped on disconnect waiting branch selects partner-arrival vs the waiter's own EOF — frees the channel on disconnect and buffers early frames to relay once paired waiter_freed_on_disconnect

Spec updated to match (REQ-080 generated ids, REQ-088 fixed-actor migrate + re-key, REQ-089 refuse-if-running, CON-014 free/buffer).

## Review round 2 addressed — `6935489` All five points fixed with regression tests. Full collab matrix: **43 tests pass** (rustc 1.93). | # | Issue | Fix | Test | |---|-------|-----|------| | **P1** | Concurrent inserts collide on duplicate shot ids | `add_new_shot` mints an **actor-scoped id** for live inserts (no cross-replica collision); `add_shot` disambiguates a local duplicate instead of overwriting; `shot_counter` restored on import | `generated_shot_ids_avoid_collision_across_replicas`, `add_shot_disambiguates_local_duplicate_id` | | **P2** | Migrated docs hard-code actor 1 | `from_event_sourced(ed, actor)` migrates under a **fixed peer id** (idempotent across independent migrations) then **re-keys to the caller's node actor** for later edits (`rekey_actor`) | `independent_migrations_converge_without_duplication` | | **P2** | New daemon unlinks a live socket | `bind()` probes first and **refuses** (`AlreadyRunning`) if a live daemon answers; removes only a proven-stale socket | `daemon_refuses_when_already_running` | | **P2** | Rendezvous waiter never dropped on disconnect | waiting branch selects partner-arrival vs the waiter's own EOF — **frees the channel on disconnect** and **buffers early frames** to relay once paired | `waiter_freed_on_disconnect` | Spec updated to match (REQ-080 generated ids, REQ-088 fixed-actor migrate + re-key, REQ-089 refuse-if-running, CON-014 free/buffer).

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
anuna-research/ar-edit!3
No description provided.