feat: M4 gossip ops and UX

Refs #1

- /v1/diff endpoint returns each node's fingerprint set + tombstoned
  auto-thread status
- knox gossip diff <peer-url>: shows peer-only/local-only fingerprints
  (pull/keep preview) and tombstone divergence (resolved-on-peer vs
  would-resurrect)
- knox gossip sync: one-shot anti-entropy sweep + reconcile
- gossip server starts before initial seed so peers can reach a booting
  node; KNOX_PEER_ADDR alone now serves without KNOX_PEERS
- integration tests for diff + tombstone reporting
- e2e verified: two live daemons, diff previewed 1655 peer-only fps,
  sync converged second node to 1655 observations
This commit is contained in:
2026-08-29 05:08:45 -07:00
parent 8c054094a1
commit 48e9c39d7b
6 changed files with 273 additions and 17 deletions
+40
View File
@@ -179,4 +179,44 @@ type Peer struct {
LastHandshake string
Cursor int64
CreatedAt string
}
// DistinctFingerprints returns the set of all observed fingerprints — the
// ground-truth index of what this node knows. Used by gossip diff.
func (k *KnoxDB) DistinctFingerprints() (map[string]bool, error) {
rows, err := k.db.Query(`SELECT DISTINCT fingerprint FROM observations`)
if err != nil {
return nil, err
}
defer rows.Close()
set := make(map[string]bool)
for rows.Next() {
var f string
if err := rows.Scan(&f); err != nil {
return nil, err
}
set[f] = true
}
return set, rows.Err()
}
// ThreadStatusByCluster returns cluster_key → status for auto-threaded threads,
// excluding human-created threads (no cluster key). Diff uses it to show
// tombstoned threads: a thread resolved on one node but active on another.
func (k *KnoxDB) ThreadStatusByCluster() (map[string]string, error) {
rows, err := k.db.Query(
`SELECT cluster_key, status FROM threads WHERE cluster_key IS NOT NULL AND cluster_key<>''`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]string)
for rows.Next() {
var key, status string
if err := rows.Scan(&key, &status); err != nil {
return nil, err
}
out[key] = status
}
return out, rows.Err()
}