Commit Graph

9 Commits

Author SHA1 Message Date
david 52c4d1f1e6 fix: derived-state determinism, error visibility, metrics naming, HTTP hardening
- rebuild: fold observations per fingerprint in a total order
  (hcl DESC, node_id DESC, id DESC) so two nodes with identical logs
  rebuild identical entries (was: arbitrary bare-column row, merge-order
  dependent)
- threads: CreateThreadCluster uses INSERT OR IGNORE + existing-id
  fallback — concurrent auto-threaders converge instead of hitting UNIQUE
- errors surfaced instead of swallowed: scanEntries returns rows.Err(),
  Stats() fails fast on query errors, AutoLinkThreadObservations /
  linkTemporalNeighbors / golden-thread linking propagate failures,
  AddThreadNote + LinkObservationToThread write under one tx,
  watch records session upsert failures
- gossip client: push checks HTTP status and reports errors (a broken
  push direction no longer looks like a silent success); gossip diff
  gets a 10s timeout so a dead peer cannot hang the CLI
- ingest: failed source sweeps (obsidian/browser/gitea) are logged,
  and -d's help text now states its file-only scope
- watch --quiet: fatal errors go to stderr instead of io.Discard
- main: cobra SilenceErrors/SilenceUsage (errors print once, usage is
  not dumped on runtime failures); knox mcp exits 0 on SIGINT/SIGTERM
- metrics: drop _total suffix from gauges (knox_observations,
  knox_entries, knox_projects, knox_sessions, knox_peers, knox_threads);
  _total stays on counters per Prometheus convention
- http: ReadHeaderTimeout + IdleTimeout on gossip, metrics, and web servers

tests: concurrent cluster-create idempotency, HCL-order rebuild fold
(both merge orders), push HTTP-error surfacing; full suite + -race pass,
gofmt clean
2026-09-17 02:28:08 -07:00
david bb852faa27 fix: harden gossip, HLC restarts, watcher races, MCP args, pagination (#3)
Implements the top findings from the codebase review, verified with tests and live CLI/MCP checks.

**Gossip integrity**
- Push validation: 4 MiB body cap, 1000-row batch cap; rows claiming the local node id (vector-poisoning), empty node ids, and negative HCLs rejected (internal/watch/gossip.go, internal/db/gossip.go)
- Reconcile-on-pull: Run returns the pulled count, syncGossip rebuilds derived state when > 0 — entry-count comparison could never fire, so synced observations never materialized into searchable entries

**Data-layer safety**
- HLC resumed from MAX(hcl) at Open (hlc.SeekTo): a restart with a regressed wall clock cannot reissue values the (node_id, hcl) locator and pull cursors depend on
- Writer serialization: _txlock=immediate DSN + SetMaxOpenConns(1) + per-KnoxDB mutex around RecordObservation's check-then-insert dedup (closes duplicate-row race)

**Watch daemon**
- Ticker guard flags now atomic.Bool (was a cross-goroutine data race)
- Trailing-edge per-path debounce (timer-based, pruned on fire/delete)
- Recursive watches (startup tree walk + watcher.Add on dir Create); Rename re-ingests, Remove cancels pending ingests

**MCP + CLI**
- Strict arg validation, no silent clamping: thread_id 0 errors instead of renaming thread #1; empty knox_thread_link {} errors instead of false success; thread existence checked before writes; golden-thread tool nil-safe
- --page 0 errors instead of panicking; query/recent pagination actually pages (page x limit)

**Tests** (new internal/hlc and internal/db packages): SeekTo monotonicity, concurrent dedup race, push validation, reopen HCL monotonicity, batch caps, self-spoof rejection, idempotency on observation counts.

Verified: go build, go vet, full suite with -race, live MCP stdio transcripts against a scratch DB.
Reviewed-on: #3
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
2026-09-17 09:06:08 +00:00
david d6d2a24ddc style: gofmt all packages (#4)
Applies gofmt to the 18 files that were already unformatted at HEAD (pre-existing debt — 122 insertions / 122 deletions, whitespace plus import-block reorderings only; `git diff -w` confirms no semantic changes).

Kept on its own branch so the functional change set (see the review-hardening PR) stays reviewable without formatting noise.

Verified: go build, go vet, go test ./... pass on this branch; a merge simulation with the functional branch produces a clean 3-way merge with all tests green.
Reviewed-on: #4
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
2026-09-17 09:05:57 +00:00
david 876d2aa45f feat: Prometheus metrics endpoint for knox nodes
Refs #2

- /metrics served on a dedicated port (KNOX_METRICS_ADDR, default
  localhost:8932) via prometheus/client_golang, with Go runtime +
  process collectors
- DB-derived gauges refreshed per scrape: observations by source,
  last-24h observations, entries, projects, sessions, pending
  reflections, threads by status, peers, observations by origin node,
  knowledge vector (max hcl per node)
- live gossip counters (pulls/pushes, observations pulled/pushed,
  errors) incremented during the anti-entropy sweep; Run accepts an
  optional metrics handle (nil for one-shot CLI)
- knox_node_info{node_id,name} for scrape identification
- internal/metrics package + db MetricsSnapshot; tests for snapshot,
  scrape output, and counter increments
2026-08-29 06:06:49 -07:00
david 25a7112d8a 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
2026-08-29 05:41:52 -07:00
david 48e9c39d7b 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
2026-08-29 05:08:45 -07:00
david 8c054094a1 feat: M3 peer gossip protocol
Refs #1

- peers table (peer_id, addr, cursor, last_handshake)
- watch serves HTTP API: GET /v1/ping (knowledge vector),
  GET /v1/log?node=&after= (cursor-paged pull), POST /v1/obs/batch
- anti-entropy sweep (Run): ping, pull what we lack, push own obs;
  echo suppressed by node_id ownership; reconcile-on-pull
- config via KNOX_PEERS / KNOX_PEER_ADDR; knox gossip status
- db: GossipObservation wire type, PushObservations, ObservationsAfter,
  KnowledgeVector, peer upsert/list
- integration tests: bidirectional convergence + idempotency
2026-08-29 04:53:26 -07:00
david aa0dec68c1 feat: M2 composite locator + idle reconcile
Refs #1

- observations: hcl backfilled from local rowid; (node_id, hcl) UNIQUE
  locator index becomes the gossip merge key
- threads: cluster_key column + partial UNIQUE index; CreateThreadCluster
  is idempotent, threader folds into exact cluster_key before heuristic
- AutoLinkThreadObservations dedup re-expressed on hcl DESC
- new `knox reconcile [--dry-run]` rebuilds entries from the observation
  log and re-links threads idempotently (entry count, thread cluster_key
  verified bit-identical from log-only DB)
2026-08-29 04:47:15 -07:00
david 8eecde18b4 Initial commit: knox knowledge index 2026-08-29 02:52:32 -07:00