# Changelog All notable changes to this project are documented in this file. Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) --- ## [Unreleased] --- ## [0.20.1] - 2026-10-09 ### Fixed - **Docker images download v0.20.0 instead of v0.15.0.** Download errors stop the build before extraction. Native amd64 and arm64 CI verifies the release version and all seven MCP tools in direct and daemon modes. - **The CLI toolchain pin, CI, and release builds use Go 1.26.9.** This fixes the standard-library vulnerabilities reported by `govulncheck` for Go 1.26.7, including [GO-2026-6617](https://pkg.go.dev/vuln/GO-2026-6617). The existing v0.20.0 download archives are unchanged; newly compiled binaries built with the patched toolchain receive these fixes. - **Homebrew, Scoop and Nix manifests are preserved for publication with signed maintainer commits.** Release and snapshot checks require all three generated manifests; an unavailable Nix hash tool can no longer silently omit that channel. Release retries validate the existing CLI module tag instead of creating an unsigned fallback tag. - **`memory_reflect` remains discoverable in Claude Code deployments that reject root input-schema combinators** ([#139](https://github.com/angelnicolasc/graymatter/issues/139)). Its input is now a flat object requiring `action`; an explicit valid `agent_id` or deprecated `agent` alias remains mandatory at runtime. Canonical-only and alias-only calls keep working, and `agent_id` wins when both are valid. Every supplied identity is validated before any backend call: null, wrong types, empty strings, and whitespace-only strings are rejected even if the other spelling is valid. Valid namespace values remain unchanged. All six actions and their result contracts are preserved. --- ## [0.20.0] - 2026-10-02 ### Added - **Memory workbench with six terminal views.** Memory, Recall, Activity, Graph, Usage and Status share a command palette, responsive list/inspector layouts, explicit focus, dark/light/terminal themes and optional mouse navigation. Recall inspection leaves access counts, learned aliases and recall hooks unchanged. Exact-ID curation rejects stale selections; revision commits the replacement and source retirement together. Read-only sessions block mutations at the store boundary and do not start a daemon. - **Usage observations with explicit provenance.** Subscription windows, API billing, request events and session context have separate representations. Configured subscription and administrative billing sources can be refreshed explicitly; snapshot and statusline imports support other clients. Unknown values remain unknown, failed sources retain stale data, and currencies or overlapping estimates are never summed. Harness requests record disjoint token categories; dated user-supplied price books can produce labeled estimates. See [Usage setup](docs/usage.md). - **Offline workbench demo.** The demo uses one isolated keyword-only store, deterministic graph extraction and clearly labeled synthetic Usage observations, regardless of inherited model-provider environment settings. - **Opt-in confidence writes and retrieval** (Refs [#126](https://github.com/angelnicolasc/graymatter/issues/126)). MCP add and reflect add/update accept validated `confidence`; search and batch accept `min_confidence` and finite `confidence_weight` in `[0, 0.5]`. CLI remember/revise and recall expose equivalent flags, including shared/all/batch scopes. Go methods add narrow optional capabilities without changing `AdvancedStore` or `VectorStore`. Text and metadata commit on the exact fact; new RPC methods negotiate versioned capabilities and reject unsupported requests with an update/restart error. - **Reconstructible confidence ranking receipts.** Filtering precedes IDF, signal ranks, recency, relevance floor, deduplication and top-k. Preference multiplies base RRF by `1 + weight * c` (`+1` verified, `0` inferred/legacy, `-1` unverified/unknown). Explain keeps `ranks.fused_score` as the base score and adds final score, factor and policy; text and JSON show effective retrieval options even for empty results. Explicit filters suppress graph labels without confidence receipts. The v3 compact index stores effective confidence and rebuilds from canonical facts; read-only old indexes fall back to scan. Native vectors intersect eligible IDs before truncation; custom filtered backends must prove completeness through an optional capability. - **Normal `graymatter init` preserves customized GrayMatter MCP entries.** The seven auto-wired clients now retain existing custom commands, arguments, environments, remote endpoints and disabled entries by default. `--only CLIENT --replace-mcp` deliberately replaces only selected GrayMatter nodes. Config edits preserve other nodes and require valid, editable documents; unsafe leaves and unsupported metadata fail without truncation. On Windows, any edit to an existing setup file, including instructions, requires permission to read its audit SACL; otherwise preflight rejects it. Edit the file manually or omit the blocked action (for example, `--skip-instructions`). Explicit `--dir` persists an absolute destination in newly installed MCP and hook commands; omitted `--dir` remains dynamic. - **Init reports a versioned setup result with `--json`.** The command validates every requested action before applying any, reports per-action status and `runtime_verified:false`, and exits nonzero on incomplete setup by default. `--best-effort` permits exit 0 for a known partial apply with a successful required action; validation, total failure, output errors and uncertain effects still fail. Successful earlier files remain in place for retry. Interactive Windows setup now asks separately for PATH with No as the default, and `--no-path` prevents the update. - **`graymatter init --store-only` prepares a project's memory directory without client setup** ([#81](https://github.com/angelnicolasc/graymatter/issues/81)). It creates `.graymatter/MEMORY.md` only when neither a regular marker nor `gray.db` is present; an existing database remains untouched. The repeatable operation does not write MCP configs, instructions or hooks, change `PATH`, or open the runtime. `--dir` selects the store, `--quiet` suppresses success text, and `--json` reports `created` or `already_prepared` with the selected path and marker. Versioned Bash and PowerShell launchers in [`examples/claude-global/`](examples/claude-global/README.md) prepare the Git checkout or worktree root before starting Claude Code. The command does not verify database health or client routing, and a user-scoped MCP server can still create `gray.db` when it starts. - **`graymatter mcp serve --no-create` requires a prepared store before MCP startup** ([#81](https://github.com/angelnicolasc/graymatter/issues/81)). This opt-in flag admits an existing regular `gray.db`, or a regular `MEMORY.md` when the database is absent; it does not make the runtime read-only or certify database health. An unprepared stdio or HTTP server exits with an actionable error before creating a token, starting a daemon, or serving tools. Prepare with `init --store-only`, then reconnect. Existing MCP registrations are not silently changed. Before downgrading to a binary without this flag, remove it from opted-in registrations; an unknown flag fails visibly. - **`graymatter doctor` shows prompt-packet policies configured in project and global settings.** The read-only checks identify explicit choices, inherited defaults, and invalid configurations, and show how to enable experimental lexical selection. - **Optional lexical selection puts more useful memory in the initial prompt context.** `graymatter hooks install --packet-policy lexical` selects up to three whole facts from 32 candidates within 832 UTF-8 payload bytes per namespace. It runs locally in Go without an additional model or download; `native` remains the default and is available as an explicit return path. In an exploratory keyword-only pilot of 24 paired tasks, follow-up memory searches fell from 43 to 23, with similar functional results in that sample. The updated hook benchmark measured warm process medians of 51.1 ms native and 56.3 ms lexical at 10,000 facts on the local test machine, with 12 samples per policy. These results are scoped measurements, not general quality or latency guarantees. See [lexical hook selection](docs/lexical-hook-packets.md). - **`checkpoint_resume` can report a missing checkpoint as a successful, machine-readable result.** A new optional `on_missing` parameter (`"error"` — the default — or `"empty"`) lets callers ask for `{"found": false, "agent_id"}` with `isError` unset instead of the historical not-found tool error, so a session-start check can tell "nothing saved yet" from a broken store without parsing prose. The output schema declares both result shapes under `oneOf`; storage and daemon failures stay prose-only errors in both modes. See [ADR-015](docs/decisions/015-checkpoint-resume-empty-result.md). ### Changed - **Omitted confidence on revised or summarized text is conservative.** The replacement uses the minimum of inferred and the effective confidence of its validated live sources. LLM-extracted facts are unverified; original-text fallback keeps legacy storage. Pin, decay, access and retirement use narrow current-state mutations that preserve unrelated labels. Revision writes and later retirements remain separate phases; errors report a known committed replacement rather than claiming full workflow atomicity. No pin is inherited. - **MCP stdio and Claude hooks now choose one project root and store per invocation.** `CLAUDE_PROJECT_DIR` takes priority, followed by the hook payload `cwd` for hooks, then the process working directory. An omitted `--dir` uses `/.graymatter`; an explicit relative `--dir` still uses the process working directory. For example, a hook launched in `/work/repo/subdir` with `CLAUDE_PROJECT_DIR=/work/repo` now uses store `/work/repo/.graymatter` and namespace `repo`, where the prior process-directory path could use `/work/repo/subdir/.graymatter` and `subdir`. A different legacy store containing data causes an explicit routing conflict; no facts are moved. Select the intended store with an absolute `--dir`, and inspect old namespace IDs before any manual migration. HTTP keeps its service-root routing, authentication policy, and explicit `agent_id` requirement. See [project routing and migration](docs/runtime-project-routing.md). - **Hooks and MCP now reject a nonregular `gray.db` before opening a store.** A symbolic link, reparse point, directory, device, or inspection error blocks both eager and guarded startup, even beside a regular `MEMORY.md`; an absent DB remains allowed for default eager startup. Valid aliases of the data directory itself remain supported. This leaf check does not detect hardlinks to a regular DB or protect against filesystem replacement after inspection. HTTP still uses bearer authentication by default, with `--no-auth` limited to loopback. ### Fixed - **Release checks now reject mismatched versions before publishing.** Preflight validates release metadata and the CLI's pinned core dependency, and the standalone install smoke checks both the binary version and linked library against the tag without local replacements. CI jobs that test source checkouts apply the local core replacement before Go commands, including scheduled fuzz, mutation, coverage and release snapshot checks. - **Standard `graymatter doctor` no longer writes or removes a fixed-name probe.** Setup diagnostics open `gray.db` read-only with a private inert vector backend, never run the executable found on PATH, and report incomplete store or graph observations instead of presenting them as empty. The additive JSON fields `status`, `diagnostic_mode`, `readiness`, and `data_dir_writability` distinguish observed checks from client readiness. Existing `ok` and check fields remain. A missing data directory still exits successfully with a warning; filesystem and database failures are surfaced. The special doctor modes keep their separate runtime or output-file contracts. - **The CLI now requires `golang.org/x/text v0.39.0`, which fixes [GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970).** This dependency update applies independently of the selected hook policy; lexical selection also rejects malformed UTF-8 before normalization. The module graph advances `golang.org/x/sync` to v0.21.0 without raising the CLI's minimum Go version. ### Notes - **Confidence preference remains disabled by default.** The new options are available in v0.20.0 and absent from v0.19.1. This release keeps product and low-level weight zero, preserves old RPC/REST ranking and leaves #126 open. A positive product default requires explicit notice in a published minor release and promotion in a later minor release after frozen calibration and validation gates. This release does not announce a positive default. Explicit zero remains the legacy ranking opt-out and never disables a requested filter. - **Lexical hook selection remains experimental while conflict coverage and everyday use are evaluated for a future default change.** It can omit a supporting fact or one side of a disagreement, and recalling the larger pool updates access metadata even for candidates omitted from the packet. Existing explicit policy choices are preserved. - **Advance notice: a future `checkpoint_resume` default of `on_missing: "empty"` is conditional on real-client validation.** v0.20.0 keeps `"error"` as the default. A change in v0.21.0 may proceed only after a successful OpenChamber/OpenCode smoke and publication of this prior-minor notice and the matching notice in `docs/api-stability.md`. If the client rejects the union schema, apply and validate the documented fallback before publishing v0.20.0, keep the default as `"error"`, and defer the default change. Callers that require absence to remain an error should pass `on_missing: "error"` explicitly; that value remains accepted throughout v0.x. --- ## [0.19.1] - 2026-09-07 ### Fixed - **`checkpoint_resume` errors now reach strict MCP clients.** The invalid structured error payload made clients reject the entire response, leaving callers without a checkpoint or an explanation. Errors now carry text with `isError`, while successful results retain their schema and prose ([#117](https://github.com/angelnicolasc/graymatter/issues/117)). - **Checkpoint resume distinguishes absence from storage and daemon failures.** Only an empty checkpoint history reports not-found, including through the daemon; unreadable checkpoint records now fail explicitly instead of silently resuming older state or appearing absent ([#118](https://github.com/angelnicolasc/graymatter/issues/118)). - **Generated memory guidance now favors durable, actionable facts and explains pin/unpin correctly.** Unneeded facts no longer appear free, permanence comes from the user or project policy, and corrections use update/forget ([#113](https://github.com/angelnicolasc/graymatter/issues/113); thanks to [@bashrusakh](https://github.com/bashrusakh)). ## [0.19.0] - 2026-09-06 ### Added - **`graymatter revise` and `graymatter forget` — a correction stops competing with the value it corrects.** Until now an agent that learned a value had changed could only write a second fact, leaving both live and letting recall rank them against each other; the caller got whichever scored higher, which is a coin flip on wording. `revise ` retires what it replaces and keeps the receipt, `forget ` retires with no replacement, and both go through the same supersede machinery the MCP tools use. Measured over 35 revision families at 600 facts on a compound endpoint — the current value must outrank every retired sibling **and** land in the injected top-8: recording the revision settles currency on **35 of 35** probes and shows the caller **zero** retired facts, against 11/35 and 35 for the same history written flat (McNemar exact, paired, p = 1.5e-5). What it does not move is retrieval, and that is reported rather than hidden: the ten probes still missed are ones where the corrected value never reaches the top-8 at all, every one of them with keyword rank 0 because the correction shares no content word with the question. Currency is solved; retrieval is not, and the two are now separately measured. Numbers and the committed predictions: `benchmarks/revision_currency/`. - **`graymatter alias` and `alias list` — the store learns that two vocabularies mean the same thing.** An alias fact maps a caller's word to the store's, and is content-free by construction: the recall pipeline routes it to query expansion instead of the ranking corpus, so it never appears in a result, never contributes a document frequency and never occupies a top-k slot. `alias list ` prints them with their source, so a store's learned vocabulary is auditable rather than implicit. - **Weak-match feedback.** When a query barely overlaps the store's vocabulary, `RecallDetailed` returns a short block naming the store's nearby vocabulary and one action the caller can actually take. It is additive text: the facts and their order are identical with the block on or off, pinned by test. - **`Store.BatchRecall`** answers several queries in one call with bounded concurrency, for callers that would otherwise pay a round trip per question. - **A revision-currency benchmark** with its predictions committed before the first run (`benchmarks/revision_currency/PREDICTIONS.md`), and pre-registered scale gates for retrieval latency and for stemming. ### Changed - **Recall stops paying for the whole corpus — `CandidateRetrieval` is on by default.** Every recall used to load and re-tokenise every fact the agent owned before scoring anything, which measured 59–65% of a top-8 recall and put the curve on a straight line: at 30 000 facts the scan measures p50 235 ms / p99 287 ms. Recall now answers from an inverted index and a recency spine, and at 30 000 facts measures **p50 3.3 ms / p99 20.5 ms — 70x and 14x**. The ranking is identical by construction and by test: a pre-registered gate runs both paths over one store and compares fact IDs, order, fused scores, per-signal ranks, the feedback block and lineage. A store written before this default keeps answering — the first recall rebuilds the index and the ranking does not change, verified through the binary on a pre-flip store. The cost is on the write side, ~1.5 ms per `Put`, ratified against a 3 ms bar; `GRAYMATTER_CANDIDATE_RETRIEVAL=0` opts out. - **Keyword matching folds morphology — `StemKeywords` is on by default.** Porter stemming on the keyword signal, so a query's `renewed` reaches a stored `renewal`. Worth +4 of 35 out of sample at both 10 000 and 30 000 facts, and the gate pins the **strict-subset property** — four families won, none lost — rather than the count, because a default that trades wins for losses is a different decision needing different evidence. `GRAYMATTER_STEM_KEYWORDS=0` opts out. ### Fixed - **The briefing generated by `graymatter init` now teaches canonical `agent_id`, the deprecated `memory_reflect` alias and its precedence, and lists all seven tools.** It previously discouraged the canonical parameter and claimed it failed validation ([#111](https://github.com/angelnicolasc/graymatter/issues/111)). - **New hook sessions no longer inherit another window's injection throttle.** Identical memory blocks remain suppressed within one identified session, while a different or unidentified session receives its own context; the best-effort cache is bounded to its most recently recorded sessions. - **`Store.Revise` now binds replacement lineage to the identity returned by its committed write.** The returned ID and every retired fact's `SupersededBy` reference stay anchored to that exact replacement, including when another write commits concurrently in the same namespace. - **Consolidation applies the same write-derived identity guarantee to summaries.** Consumed facts reference the exact summary created by their consolidation cycle, including when another writer commits identical summary text concurrently. - **`memory_reflect update` carries the guarantee across direct and daemon-backed stores.** The corrected fact's tombstone receives the exact ID returned by its replacement write, preserving revision lineage through overlapping writes in the same `agent_id`. - **`memory_reflect` now applies `forget`, `update`, `pin` and `unpin` to every eligible exact-text match in the selected namespace.** Forgetting a fact stored twice previously reported success while leaving a copy recallable; retired revision receipts now stay intact. - **Claude Code hooks now launch GrayMatter with structured `command` + `args`.** Installed hooks no longer pass executable paths through a shell, so paths containing spaces or shell metacharacters work unchanged on every platform with Claude Code 2.1.139 or later. Reinstalling migrates both the prior string form and the pre-marker legacy form; uninstall and drift detection continue to recognize them. - **Hook scope management and diagnostics now match their public contract.** `hooks uninstall --all` and `hooks doctor --all` cover project and global settings, while the unsafe `hooks install --all` form is rejected to prevent duplicate hook execution. `hooks doctor` reports an uninitialised store without creating it, including when settings are missing or malformed. - **`run --resume ` now resumes the checkpoint belonging to that exact session.** The flag previously treated every non-empty value as `latest` and silently loaded the newest checkpoint for the agent, even when the requested ID was historical or nonexistent. Concrete IDs are now resolved through the session registry, unknown IDs fail with the requested session named, and `latest` retains its existing newest-checkpoint behavior. - **Installed Claude Code hooks now invoke the runnable `hooks run` path, and global hooks no longer create stores in unrelated working directories** ([#83](https://github.com/angelnicolasc/graymatter/issues/83)). Global commands persist a silent `--no-create` guard while project installs retain first-use creation; `doctor` identifies older global installs, and reinstalling replaces legacy malformed commands. - **`bench --hooks` now documents and enforces one CLI contract in human and JSON modes.** Help names the two 200 ms median-delta gates and the normalized-scaling gate, and states that pre-compact is an ungated baseline. `--json` suppresses the human report so stdout is one valid JSON document, while failed gates now exit non-zero in both modes. - **Hooks and MCP no longer duplicate matching startup recall, and `init --global` now says exactly what it scopes** ([#81](https://github.com/angelnicolasc/graymatter/issues/81)). A non-empty Claude Code hook block names the namespace it actually queried; before the first reply, same-ID non-empty sections replace matching MCP startup searches. A different ID reruns both project and `__shared__` so cross-namespace dedup cannot hide shared facts, while missing sections safely fall back. Focused/batch searches, writes, corrections, aliases, and checkpoints remain available. `init --global` still initializes the current project and additionally installs home-scoped agent instructions — it does not globalize project-scoped MCP configs (Codex's config is already home-scoped). - **`graymatter bench --hooks` no longer reaches an embedding or LLM provider, whatever the environment holds.** The command seeds a 10 000-fact store and fires the hook runners as real processes. Both of its stores were built from `DefaultConfig()` and both child processes inherited the ambient environment, so embedding auto-detection selected a reachable local Ollama first — no credential required — then OpenAI or Voyage when their keys were set, while `session-end` consolidation reached the Anthropic API whenever `ANTHROPIC_API_KEY` was present. A user asking for a local measurement was billed for one, had the synthetic corpus leave the machine, and got a figure that varied with their credentials rather than their hardware — while the command's own help promised a deterministic keyword path and methodology identical to `benchmarks/hook_latency`, which had already been pinned separately. Both stores now select the keyword embedder explicitly; the hook and `daemon stop` children run with the three provider keys cleared and Ollama auto-detection aimed at an unroutable scheme, so detection fails before it dials; and the report names the embedder it measured with, pinned by test. Verified with `OPENAI_API_KEY` present in the environment: `all hook gates hold`, exit 0, 64 s, internal p99 of 40.0 ms / 32.0 ms / 39.0 ms for `user-prompt` / `pre-compact` / `session-end`. The published budgets are unchanged. - **Background agent runs now persist the exact session ID, PID, and log path announced by the parent process.** `sessions list`, `sessions logs`, and `sessions kill` therefore address the same detached run instead of an unreachable duplicate session record. - **`sessions kill` now verifies a background process's exact process-start identity before terminating it.** A stale running-session record and PID file could previously match an unrelated process after the operating system recycled the PID; kills now require both the recorded PID and its platform-derived start identity (including the Linux boot ID) to match, and older unverifiable records fail closed with a manual-stop instruction. ### Notes - **Usage-alias learning ships off.** The store can promote its own vocabulary from observed reformulation pairs, and the mechanism works, but the effect that justified it came from an oracle rather than from agent behaviour; on real runs the net gain is small and a share of promoted pairs is noise that would accumulate. It stays in the tree, documented, auditable through `alias list`, and off until a second corpus says otherwise. - **MaxScore-style pruning is deliberately absent.** The fusion reads keyword *ranks*, so skipping a term whose idf is negligible changes which facts have a keyword rank at all and renumbers everything below it — a re-specification rather than an optimisation. The reasoning is recorded in `recall_indexed.go` at the place someone will be tempted. --- ## [0.18.0] - 2026-08-28 ### Added - **`remember shared: ` — the instant-save prefix reaches the project-wide namespace.** `remember:` has always written to the cwd agent, so nothing typed in a prompt could reach `__shared__`: Claude Code could read project conventions but never create one, and the only way to file one was an MCP client that chose the namespace explicitly. The new prefix is checked before `remember:` (which it cannot collide with — `remember ` carries a space where `remember:` carries a colon), matches case-insensitively like its sibling, and answers `Saved to shared memory: `. `remember:` is unchanged. ### Fixed - **The hooks never injected `__shared__` — the documented path for project-wide conventions led to a silent hole.** `AGENTS.md` tells every agent that project conventions belong in the reserved `__shared__` namespace, and both automatic hooks ignored it: `SessionStart` and the `UserPromptSubmit` recall called `Recall` with the cwd agent only, so a convention archived there was invisible to Claude Code at session open and on every turn — with zero errors, zero warnings, empty stdout, the exact failure mode that reads as "memory is broken". Both runners now recall the shared namespace under its own budget (session-start top-5 agent + top-3 shared, per-turn top-3 + top-3) and render it as a labeled `## Shared memory (project-wide)` section — separate budgets, because a merged ranking on session-start (where recency is the only ranked signal) would let old conventions displace the agent's freshest facts. The failure contract holds per namespace: one recall failing never costs the other's facts, the degraded injection still goes out, and the error lands in `hooks.log`. The identical-block throttle keeps working on merged blocks, and `graymatter status`'s INJECTION line now estimates the block the hooks actually inject instead of a generic top-8. --- ## [0.17.0] - 2026-08-27 ### What users get - **`graymatter hooks` — automatic memory for Claude Code, no tool calls required.** The tool-based story had a structural gap: the model calls `memory_search` only when it decides to, which is almost never, so memory was only half the product. `hooks install` (also `init --hooks`) writes the hook block into `.claude/settings.json` — merge-never-overwrite like every other writer, foreign hooks preserved byte-for-byte in meaning, `.bak` on every rewrite, hand-edit drift detected and reported, unparseable settings never touched — and from the next session memory runs itself: `SessionStart` injects the freshest live facts and re-injects after `/compact` (memory survives compaction), `UserPromptSubmit` injects a short top-3 recall that is suppressed when identical to the previous turn, `remember: ` in any prompt is a deterministic instant save, `PreCompact` checkpoints, and `SessionEnd` checkpoints and spawns consolidation detached so it survives the editor closing. The failure contract is absolute: every hook error exits 0 with empty stdout and a receipt in `/hooks.log` — a broken memory degrades silently, never breaks the session. `hooks doctor` verifies registration, the recorded absolute binary path, and store latency; `hooks uninstall` removes exactly our entries and nothing else. All four runners, the merge writer and the drift detection are contract-tested, and the command records absolute binary paths so hooks resolve from any cwd. - **`benchmarks/hook_latency` — the hooks' hot path is machine-checked against what the code controls, never against shared-CI hardware** (every published number must be). The gate builds the real binary, seeds a 10k-fact store, fires the hook as a fresh OS process per sample through the daemon (the shape Claude Code uses). The gates are hardware-relative by design: the recall's marginal cost (user-prompt p99 minus this run's pre-compact baseline ≤ 200 ms), the detached-spawn delta (session-end ≤ baseline + 200 ms), and in-process scaling (Recall(10k) ≤ 2.5× of linear) — because absolute wall budgets proved a hardware lottery: the identical tree measured 121 ms p99 on the reference machine and 284-369 ms on Windows CI runners, with the spawn+connect baseline alone reaching 192-248 ms. Absolute numbers are still reported as reference data. Verified injections are counted so the benchmark cannot silently measure the throttle instead of recall, and `graymatter bench --hooks` audits the same gates against this binary itself — the CLI is the hook binary, nothing compiles. Getting under the gates took three behaviour-preserving optimizations to the recall path: `keywordScore` now tokenizes each fact once instead of twice (byte-identical scores), the pipeline decodes through a lite fact decoder that skips the fields ranking never reads (`unmarshalFactLite`, pinned against the full decoder by a 500-case agreement test), and recency ranks are read straight from the newest-first list order instead of being re-sorted — recency is strictly monotonic in `CreatedAt`, so the rank order was already there, and equal-`CreatedAt` facts (Windows clocks tick at ~15.6 ms) now resolve by ID deterministically instead of by `sort.Slice`'s unspecified tie order. - **`recall --explain` — receipts per fact, "receipts, not vibes" made literal.** Provenance is the layer nobody in the space has shipped per query, and GrayMatter already computed everything needed internally — this is surface, not architecture. `--explain` returns the same ranking as a plain recall (one pipeline, identical order, dedup and access-metadata side effects; pinned by tests) with one receipt per fact: per-signal RRF ranks (`vector_rank`/`keyword_rank`/`recency_rank`, 0 = signal absent), the fused score that reproduces from those ranks and `k` using the documented arithmetic, stored weight, age in days, and provenance (`fact_id`, `written_at`, tombstone state, confidence, pin). The receipt shape (`memory.RecallReceipt` and friends, added in v0.17.0) is documented in `docs/api-stability.md`, byte-deterministic on the same store (golden test), and carried end-to-end through the daemon RPC protocol and the MCP server: `memory_search` gains an optional `explain` boolean whose `structuredContent` carries the same receipts under `explained` (schema additive, bare shape unchanged, text fallback updated). - **`graymatter kg render` — the graph as a self-contained artifact.** The KG was the claim most shown in its least dynamic form (a static screenshot in Obsidian). `kg render --out graph.html` writes one offline-first page — inline force-directed SVG, no CDN, no fonts, no scripts; node colour by entity type, size by weight, edge thickness by weight, tooltips carrying the edge's fact-ID receipts — deterministic bytes for the same graph (sorted inputs, fixed-seed layout). `--out graph.dot` emits Graphviz for people with their own layout; labels and IDs derived from stored text cannot break out of the embedded JSON (HTML-escaped by construction). `doctor --graph --html` attaches the same render to the analytics report. - **`graymatter demo` — install-to-visible in one command.** Reddit's "show me" culture makes every setup step a lost star. `demo` seeds a scratch store (`.graymatter-demo`, `--tmp` for a temp dir) with the four-agent sales/support/infra corpus, writes the KG sentinel, runs two consolidation cycles, and opens the TUI — no API keys, no Ollama (the keyword embedder is the default path). Idempotent by construction (append-only store, re-runs never duplicate), `--fresh` rebuilds and refuses to delete anything that does not look like a GrayMatter data dir, `--script` emits the exact equivalent shell script because there is no magic to hide. - **`docs/integrations.md` — per-client verified configs for 25 MCP clients.** "Every agent wants a different shape and fails silently" is measured pain: the page documents each client's real config shape (VS Code's root `servers` key, Codex TOML, OpenCode/Kilo's array `command`, Goose's extensions, Zed's `context_servers`), marks the seven `init` auto-wires as such, and accepts one-PR-per-client community verification for the rest. - **`scripts/kg-timelapse.sh` — watch the graph build itself, reproducibly.** The KG was the claim most shown in its least dynamic form (a static screenshot). The script replays a deterministic working-week corpus session by session, renders the graph per frame through `kg render` (its only GrayMatter primitive), rasterises with Graphviz and assembles a GIF with ImageMagick or ffmpeg — degrading with exact instructions when an assembler is missing, never failing. Frames are byte-identical across runs (sorted inputs, fixed-seed layout) and the whole pipeline reproduces on any machine via `scripts/Dockerfile.kg-timelapse` (Go + graphviz + ImageMagick, source and output bind-mounted, nothing baked in). ### Changed - **`touchFacts` writes back read-modify-write instead of the recall-time snapshot.** The old shape marshalled its snapshot over the stored fact whole: correct only while every mutation was serialised by luck, and it silently stomped concurrent weight/tombstone/pin changes that landed between `List` and the write-back. It now re-reads the current fact inside the transaction, carries the tombstone-never-resurrect guard forward, and applies only the access bump. `TestRecallExplain_SameStateAsRecall` pins that an explained recall and a plain recall leave identical access footprints. - **`graymatter consolidate ` is a CLI command.** It was the missing entry point the session-end hook spawns detached (consolidation may proxy an LLM round-trip, so it runs outside the hook's time budget); as a normal command it routes through the daemon's policy and is directly usable. - **`init` gained `--hooks`.** Same merge semantics as `hooks install`; runs last so a failure is reported after everything else is wired, and drift warnings print even under `--quiet` — overwriting a hand-edited file silently is the one thing this step must never do. ### Fixed - **The vector signal was silently dead on young stores.** chromem-go v0.7.0 returns an *error* when asked for more results than the collection holds, and every recall asks for 2×topK — so any store with fewer facts than that (every store for its first days) lost the entire vector signal with the error swallowed by design. The wrapper now returns "at most n" the way the interface always promised. Found by the hardening round's vector-path receipt tests, which assert vector ranks exist. - **Tied vector similarities made recall nondeterministic.** chromem selects the top-n over an internal map, so with equal similarities even *which* documents came back differed call to call — violating the documented "same query, same store, same order" guarantee. Selection is now the same total order the ranking imposes everywhere else (similarity descending, then ID ascending), identical to chromem's pick when similarities are distinct and deterministic when they tie. - **A UTF-8 BOM on stdin broke hook payload parsing.** Nothing sane sends one, but a payload routed through a Windows text pipeline may carry it, and refusing to parse over three bytes silently disabled every hook. The parser strips it. --- ## [0.16.4] - 2026-08-27 > Identical contents to 0.16.1–0.16.3. 0.16.3 was deleted and re-created while debugging the release pipeline, which poisoned the module proxy's index for that name (a 404 that outlives the fix), so this re-release uses a fresh version name. Lesson recorded in the technical-debt log: published tags are immutable — delete-and-recreate breaks every proxy that already crawled the old revision. 0.16.4 is the first version published with binaries and a clean proxy index. - **`Recall` deduplicates identical stored text** — the documented contract ("deduplicated by text") is now enforced: storing the same sentence across sessions yields it exactly once per result set. Found by the agent-lifecycle simulation, where repeated session templates stored identical facts. - **Tombstones survive concurrent consolidation writebacks** ([#78](https://github.com/angelnicolasc/graymatter/issues/78)). A consolidation cycle's decay pass wrote back its batch snapshot, silently overwriting a tombstone that landed mid-cycle — superseded facts resurrected with `SupersededBy=""`. `UpdateFact` and `touchFacts` now enforce update-never-resurrect: when the stored fact is tombstoned and the incoming snapshot is not, the tombstone wins. Found by the new agent-lifecycle simulation; fixed the same day, with the deterministic reproduction (`update_never_resurrect_test.go`) and the full 100-session re-run reporting 0 superseded facts returned. - **`benchmarks/agent_lifecycle`** — an end-to-end simulation of one hundred real working sessions: a fresh `graymatter mcp serve` process per session over JSON-RPC stdio (process death between sessions is the durability claim under test), a realistic adversarial corpus with distractor families, a supersede pair, paraphrase and shared-namespace probes, and pre-registered pass/fail bands. Plus `benchmarks/decay_probe`, which compresses the 30-day half-life to seconds and verifies decay precision, pin exemption, touch protection and the prune floor. Plus `benchmarks/agent_lifecycle/tombstone_dump`, a forensic store dumper. - **Registry metadata.** `server.json` (official MCP registry) and `smithery.yaml` (Smithery) let GrayMatter be published and listed across MCP registries. - **MCP tool definitions engineered for agent-facing quality (TDQS), pinned by contract tests.** Every tool now carries a meaningful title, an opening-verb description with explicit when-to-use guidance and named sibling alternatives, verified return/error disclosure, and fully documented parameters (`top_k` declares its default; the `action` enum is pinned). `tdqs_contract_test.go` locks the client-visible `tools/list` shape so description drift becomes a CI failure. `docs/AGENTS.md` return-shape claims corrected against the real handlers (`memory_search` returns a numbered list, not newline-separated text; `checkpoint_resume` errors when no checkpoint exists). See [ADR-012](docs/decisions/012-tool-definition-quality.md). - **`go install github.com/angelnicolasc/graymatter/cmd/graymatter@version` works again.** The CLI module carried a `replace` directive, which makes every tagged version uninstallable via `go install` ([#75](https://github.com/angelnicolasc/graymatter/issues/75)); the workspace at the repo root already provides the checkout linkage for in-repo builds. The released CLI now resolves the library at the pinned `v0.15.0`, and the release workflow gained an install-smoke job that fails the release if the exact published tag cannot be installed and run. - **MCP tools return structuredContent twins with declared output schemas** ([#76](https://github.com/angelnicolasc/graymatter/issues/76)). Every tool publishes an `outputSchema` generated from typed result payloads; success results carry machine-readable `structuredContent` alongside the unchanged text content. `checkpoint_resume` returns a typed `{error: "not_found", agent_id}` payload with `isError` instead of prose-only errors. Payload↔schema agreement is pinned by `structured_contract_test.go`. Text-parsing clients are unaffected — every text string is byte-identical. See [ADR-013](docs/decisions/013-structured-tool-results.md). - **`memory_reflect` declares `agent_id` in its schema** ([#77](https://github.com/angelnicolasc/graymatter/issues/77)). The alias was previously a runtime-only accommodation invisible to clients reading `tools/list`; it is now a documented schema property, with `agent` remaining the required canonical and winning when both are set. Precedence and alias-driven updates are pinned by `reflect_schema_test.go`; the canonical flip to `agent_id` is deferred to a dedicated release. - **`agent_id` is now the canonical agent parameter on `memory_reflect`** ([#77](https://github.com/angelnicolasc/graymatter/issues/77) step 3, [ADR-014](docs/decisions/014-agent-id-canonical.md)). The schema expresses the real contract as an `anyOf` — exactly one of `agent_id` (canonical) or `agent` (deprecated alias) is required — so callers of either spelling remain schema-valid; `agent_id` wins when both arrive. Both spellings keep working at runtime. Callers sending both spellings with *different* agents observe the precedence change; that input is pathological and the new behaviour is the documented one. - **Registration-time output-schema generation fails fast** ([TD-002](https://github.com/angelnicolasc/graymatter/issues/76)). mcp-go's `WithOutputSchema` swallows generation errors to stderr and would publish a tool without its declared contract; the new `outputSchemaOf[T]` helper panics at registration with the offending type, and generates the schema exactly once per tool. - **The MCP wire contract documents concurrent tool-call execution.** The stdio server runs a worker pool (default 5), so two pipelined `tools/call` requests may execute out of order; a response guarantees only its own effects. Documented in `docs/api-stability.md` rather than "fixed": the concurrency is spec-compliant, benefits real hosts issuing parallel calls, and serialising it would trade genuine throughput for a guarantee the protocol never made ([TD-001](https://github.com/angelnicolasc/graymatter/issues/76)). --- ## [0.15.0] - 2026-08-26 ### What users get - **Embedding provider switches self-heal.** Change providers and the store re-indexes itself under the new provider before serving a single query - stale vectors can no longer poison search, and `doctor --embeddings` shows the whole channel's state: coverage, degraded writes, retry backlog. - **The write path is production-grade.** Recall bookkeeping dropped from eight write transactions per query to one; confident writes are O(1), race-free, and embed exactly once. - **Retrieval speaks every script.** Unicode-native tokenization with plural folding and access-anchored recency - measured across three corpora, headline numbers unchanged, remaining gaps quantified as decision records rather than folklore. - **The knowledge graph lives and dies with memory.** Entity IDs are type-scoped (stores migrate automatically on open), and graph weights decay through the same idempotent lifecycle rule as facts. - **LLM extraction is opt-in and failure-proof.** `GRAYMATTER_KG_LLM_EXTRACT=1` enables Anthropic-backed entity extraction; every failure mode degrades to the deterministic regex extractor, never to silence. ### Changed - **Retrieval vocabulary is now Unicode-native.** Tokenization segments words in any script (accented Spanish stays whole instead of shattering at every accent; CJK text yields tokens where it previously produced none) with an ASCII fast path for the common case, and folds EN/ES plurals so "Rollbacks" meets "rollback". Recency anchors on last access, not creation - the access-tracking machinery finally influences live ranking. Measured honestly against all three corpora: headline numbers unchanged, because the remaining misses are word-formation and compound-word problems, not segmentation - both now precisely quantified in RESULTS-corpora.md as the decision record for fuller stemming and adjacency indexing. - **The production write path stops fighting itself.** Recall access tracking used to spawn one goroutine holding one bbolt write transaction per returned fact - up to eight write transactions and eight goroutines per recall contending with real writers for bookkeeping; it now flushes as one batched transaction, update-never-create, so a fact deleted mid-recall cannot be resurrected by its own bump. `PutConfident` no longer re-lists the entire agent namespace and text-scans for the fact it just wrote: the write path hands back exactly what landed, making confident writes O(1), race-free, and guaranteed to embed once. - **Embedding provider switches self-heal.** Changing providers used to leave every old vector in place behind a log warning - wrong-dimension embeddings silently poisoning similarity search until someone rebuilt by hand. Open now detects the dimension change, queues all live facts, adopts the new dimension durably, and re-embeds synchronously before serving a single query. The reconciler itself learned the same trick: any queued fact whose stored vector length disagrees with the active provider is re-embedded fresh and persisted, tombstones never reach the index, and a mid-reindex failure keeps its retry marker instead of vanishing. - **Two new retrieval-quality corpora extend measurement beyond the frozen English set.** `multilingual-es` (126 Spanish facts, 15 queries in two declared classes: ASCII-anchor reachable vs accent-dependent) and `long-horizon` (421 facts across 50 sessions with genuinely tombstoned late variants). Both ship with deterministic generators. First measured baselines: Spanish overall 60% [36–80] for GrayMatter fixed-K against a 27% window; long-horizon **100% [68–100] HitRate at 0% Dead and 145 tokens/q** while the window and recency-only scoring both return none of the early decisions. Every table now prints its Wilson 95% interval — six-query point estimates are not laws of nature. - **The knowledge graph now decays with memory.** Node and edge weights only ever grew (upsert took max), so a hub entity from a finished project stayed prominent forever, and the graph's old decay helper was dead code carrying the multiplicative bug the fact side had already fixed. Consolidation gained a Step 5 behind an optional `GraphDecayer` capability: weights recompute from staleness (`min(w, exp(-λ·staleness))`, idempotent - running cycles more often never forget faster) and sub-floor entries prune. The shipped `kg.GraphAdapter` implements it; custom `GraphAccessor`s without the capability are untouched. - **Entity IDs are type-scoped.** `canonicalID` now yields `: