Skip to content

0.16.0

Released 2026-09-01.

The release that keeps its promises at scale. 0.15.0 made the answers reachable; this release makes them hold when the estate is large, when the reader has to decide whether to trust them, and when the deployment is a cluster. Nothing here is a new promise — each change is an existing promise caught failing in the field and repaired: a flagship read that cost seconds at fleet scale, answers that could not state their own reliability, an operator overlay that quietly stopped at one node, and three ceilings that were hard-coded constants until someone hit them.

Contract delta is additive (one graph object on the read tools). No data migration: annotation rows written under the old keying migrate on first touch, and untagged time-index entries age out through retention on their own.

The incident window now answers in milliseconds

"What changed in the last hour" at 10,000 hosts took 7.5 seconds — on recent_changes and graph_diff alike, the very reads the MCP instructions route incident investigation to. The cost was not the window seek and not the graph size: classifying each event as a heartbeat cost one random lookup plus a proto decode, and a fleet-scale window holds millions of heartbeats that exist only to be dropped. The read paid for its noise, not for its answer.

Each time-index entry now carries a one-byte tag — the change type plus the relation's structural flag — so readers that exclude heartbeats, or filter by kind or change type, skip an entry from the tag alone and touch the primary record only for events they keep.

Measured on the same 311,750-entity estate: a window holding 380,000 heartbeats and nothing else answers in 21 ms on recent_changes (7,612 ms before, with heartbeats_excluded still reported exactly) and 17 ms on graph_diff (7,562 ms before). A window of 611,750 real changes still costs seconds — that is the answer being large, and that is the principle: cost follows the answer.

Every answer declares its own scope and freshness

Three operator agents, surveyed independently, converged on the same blocker before any of them would lean on an answer: their work is arbitrating between sources that disagree, and a source that does not state its own coverage and freshness cannot enter the arbitration — it gets hand-verified once, then bypassed, however correct it is.

Every MCP read answer now carries a graph block:

  • how many entities and relations the answering graph holds,
  • the newest event time in the tenant's log,
  • the oldest instant still answerable (the prune horizon),
  • and, when the answer reads a past instant, the as_of it describes — so a time-travel view is never mistaken for the present.

Freshness dates the log, not the projection, deliberately: a live graph over a stale log means the producers stopped talking, and that is a finding the reader must see, not a detail to smooth over. An as_of fold reports the folded counts, not the present's. And the block matters most on empty answers — absence is not evidence of absence, and the block is where an answer says how much absence it can even speak for.

The operator overlay is now cluster-real

Found while verifying a rolling upgrade: the annotation reading "never reboot both nodes in the same maintenance pass" — the instruction that prevents a total outage — was readable on one of the two nodes it protects, and had been since June. Two defects compounded:

  • Keying. Annotations were keyed by the entity's logical id, which each replica mints for itself — a row copied between nodes would attach to nothing, or to something else, and a re-minted id after the resurrection window silently orphaned the note. They now key on the identity fingerprint (ADR 0017), which every node computes identically. Old rows migrate on first touch, write-before-delete, so a crash leaves a resolvable duplicate rather than a lost operator note.
  • Transport. When log shipping is enabled, each maintenance cycle now also reconciles every tenant's annotations with the object store under annotations/<tenant>, both directions, last-writer-wins by UpdatedAt. Zero new configuration and zero node-to-node coupling: the nodes never talk, they meet at the store. Deletions travel because a removal is now a write (a tombstone) — otherwise a deleted note is indistinguishable from one that never existed, and resurrects on the next pull. A single-node deployment simply gains an annotation backup.

Constants become configuration

Three ceilings were compiled in, and each was rediscovered in the field as a suspected defect before it was recognized as a setting:

  • resurrection_grace — the 15-minute window during which a deleted identity keeps its logical id. Zero or absent keeps the default (the zero-config posture is byte-identical); negative disables the time bound. Applied uniformly to every tenant stack, pinned by tests.
  • tenant_retention_max_age — retention per tenant, as tenant:duration pairs. An unlisted tenant keeps the global bound; a listed one is pruned to its own, even when the global is unlimited. A malformed or zero pair refuses to boot rather than pruning someone's history to the wrong bound. Retention is what a multi-tenant offer prices, and the product can now express it.
  • otlp_max_recv_bytes — the 4 MiB gRPC receive cap that no document mentioned while the producer contract urged aggressive batching; a 5,000-host inventory sent as one export was refused whole. The cap is now configurable and documented with its failure mode, so a producer learns where batching stops from the docs rather than from ResourceExhausted.

Honest performance docs, and a gate to keep them honest

performance.md claimed "10k hosts" for a benchmark whose fixture is ~270 real hosts at the fleet's measured 37-entities-per-host ratio. The row now says what was measured, and a fleet-scale section records the campaign numbers — noting that a reading taken during a bulk load is a GC high-water mark, not a sizing figure.

The new CI bench gate makes the fast paths contractual: every PR touching Go runs four guarded benchmarks on merge-base and head on the same runner, and a statistically significant regression above threshold fails. The guarded set is small and named — the durable append and the reads the promises above rest on, including one that degrades by orders of magnitude if per-event resolution ever returns to the windowed change read.

Upgrade notes

Drop-in binary swap. New configuration keys are all optional; omitting them keeps 0.15.0 behavior exactly. Changing resurrection_grace or the retention pairs requires a restart, and the configuration reference says so. Annotation sync activates only where log shipping is already enabled.