feat: swarm membership discovery via peer-list gossip

Refs #1

- /v1/ping now advertises the node's known peers (peer_id, addr, name)
- Run sweeps static KNOX_PEERS + persisted discovered peers, enqueueing
  newly-learned addresses for direct sweeps (membership-only relay; no
  observation relay)
- db: ShareablePeers, SwarmPeerAddrs, MergePeer (cursor-preserving
  discovery upsert), MaxHCLForNode
- integration test: a node configured with a single seed discovers and
  pulls from other swarm members without direct configuration
This commit is contained in:
2026-08-29 05:41:52 -07:00
parent 48e9c39d7b
commit 25a7112d8a
6 changed files with 204 additions and 20 deletions
+11 -4
View File
@@ -125,14 +125,16 @@ a single node the rowid remains monotonic, so the current dedup query
### 5.1 Transport
Plain HTTP/JSON on a per-node advertized address (default port `8931`). Nodes
discover peers via a static list in `settings` (M3). mDNS/rendezvous is future
work.
Plain HTTP/JSON on a per-node advertized address (default port `8931`). Peers
are seeded from a static list (`KNOX_PEERS`), then the swarm discovers itself:
each node advertises its known peer addresses in `/v1/ping`, and every sweep
enqueues newly-learned nodes for direct contact (membership gossip — no relay of
observations). mDNS/rendezvous is future work.
Endpoints:
```
GET /v1/ping → { node_id, name, max_hcl }
GET /v1/ping → { node_id, name, max_hcl, peers: [{peer_id, addr, name}] }
GET /v1/log?after=<seq>&node=<id> → { cursor, rows: [observation...] } # pull
POST /v1/obs/batch → body: [observation...]; reply: { accepted n, conflict n } # push
GET /v1/diff → divergence summary (M4)
@@ -142,6 +144,11 @@ GET /v1/diff → divergence summary (M4)
- **Knowledge vector:** each node tracks `peer_id → max_hcl consumed`. Anti-entropy
is a pull: periodically (and on handshake) query each peer's `/v1/log?after=...`.
- **Membership gossip:** `/v1/ping` includes the responding node's known peers
(`peer_id`, `addr`, `name`). The caller merges them into its `peers` table and
enqueues their addresses for direct sweeps. A new node therefore joins the
whole swarm by configuring just one seed peer. Membership flows independently
of data — a node never relays another's observations, only its address.
- **Push:** on a new local observation, best-effort `POST /v1/obs/batch` to known
peers. A node does **not** re-broadcast something it merely received (that peer
already has it and will pull from its origin) — this is the echo/loop