How we got here
Every release before this one ran on a SurrealDB daemon: a server to install, start, keep alive, and debug when a crashed studio left it wedged. The graph itself was rebuildable from transcripts on disk, but the thing holding it was not - and most of the operational pain in the issue tracker traced back to that daemon, not to the data.
The v0.40.0 range (34 commits over three days) is the replacement, executed in phases with receipts at each step. First the engine swap (#849): everything derivable from transcripts, git, and skills moved into a single-file embedded DuckDB cache, and everything that must survive a rebuild - proposals, verdicts, experiments, session labels - moved into a SQLite judgment sidecar. A fresh install now starts nothing; reads open a published snapshot, writes go through an ingest lock on the live file. Then the pipeline rebuild (the v3 phases), because an embedded engine rewards a different write shape than a daemon did. Then the tail work the new shape exposed: snapshot publish and index rebuild costs that a per-row engine had hidden inside its own overhead.
The migration is checked against the retired engine, not just asserted: the old SurrealDB DDL stays in the repo as parity proof, and a new golden transcript corpus pins each provider's parser output byte for byte.
What changed
The engine swap (#849, #883, #885): the cache is one DuckDB file with a statically linked FTS extension, driven by the threaded napi client (embedded in the compiled binary, so `ax mcp` and every query work from a release install with no repo checkout). Decisions live in the SQLite sidecar and survive any cache rebuild. There is no daemon left to manage: `ax studio` binds a port only while a browser is attached, and `ax otlpd` remains the single LaunchAgent because an OTLP exporter needs a durable endpoint.
The ingest pipeline was rebuilt for batch writes (#886, #888, #900, #910): JSONL providers now spool 15 hot tables in memory and land them as one NDJSON upsert per flush (a 7-day claude window dropped 42.5s to 15.9s); derive stages started moving into version-marked SQL models that run entirely inside DuckDB (run-evidence re-derives 10.3x faster); the file watermark gained a SHA-256 content tier, so a touched-but-unchanged transcript skips the parse; and the invoked-positions backfill became one set-based UPDATE instead of ninety thousand row updates.
The run tail died (#908, #909): publishing the snapshot was a ~37-second logical copy of the whole catalog on every ingest; it is now an APFS clonefile behind a four-guard safety protocol (checkpoint on the owning connection, WAL absence check, stat tripwire, sanity-open before an atomic rename), with a logical-copy fallback for filesystems without clone support. The full-text index rebuild now skips entirely when a content digest says the table did not change.
The combined receipts on a real ~12GB store: cold full backfill 5,136s on the old engine to 1,508s (3.4x); warm `ax ingest --since=1` 621s to 29.6s (21x).
Recall searches full turn text now (#921): the turn index covered only the first 500 characters of each turn, so a phrase deeper in a long message was silently unfindable. The index target moved to full `text` (measured on 1.05M turns: rebuild 2.2s to 6.2s, no measurable file growth), and hits now snippet around the match instead of showing an unrelated excerpt.
ax recall "that phrase from the middle of a long turn" --sources=turnParser output is pinned by a golden corpus (#876): one sanitized real transcript per provider - claude, codex, pi, omp, opencode, cursor - is committed with the exact normalized batch each parser must produce from it. Any change to normalized output now shows up as a golden diff in the PR that caused it. Sanitization runs on decoded JSON values and includes a codename map, so private project names never ride along.
Cost got two new attribution lenses (#867, #868): `ax cost attribution` reads Claude Code's native per-event skill/agent stamps as ground truth beside the graph's own inference, and `ax cost cache` derives a cache-bust ledger (as a SQL model) that prices every cache-creation event twice through independent paths, so the numbers corroborate each other instead of one pricer grading itself. Ingest auto-mints the top cache-busting offenders as reviewable proposals behind four guards - corroboration, recurrence, materiality, and an open-proposal cap.
ax cost attribution --days=14
ax cost cache --days=14Sessions move between stores (#902): `ax segment export` writes session-scoped event rows as a plain NDJSON directory off the read-only snapshot; `ax segment import` loads them with a column-intersection upsert that can never clobber local enrichment, then re-derives the derived layer. Enrichment columns are excluded from the export rather than nulled - that distinction is what makes the import safe.
A learned judgment classifier shipped dark (#911): a 12-feature logistic model, trained on LLM-labeled turns, beats the regex judgment guard by +8.3 points precision at matched recall across 20 random splits. It ships seeded but disabled - `AX_JUDGMENT_MODEL=learned` turns it on - because a flip rehearsal showed the routability lens produces identical output either way today. The default is pinned byte-identical by test.
The migration contract
The event-layer write contract (#893) now classifies all 126 cache tables as event, derived, or bookkeeping, and every writer declares its writes; legality is enforced by static tests plus behavioral runs that diff per-table content digests. That contract is what made the rest of the range safe to execute quickly: a stage that writes where it should not fails a test before it corrupts a store.
Upgrading is `curl | sh` (now fixed on Debian and Ubuntu, #797) followed by one full `ax ingest` to rebuild the cache from your transcripts. Decisions in the sidecar carry over untouched.