axagent experiencelivev0.43.18

release v0.40.0

The storage rebuild: SurrealDB out, embedded DuckDB in, 21x faster ingest

v0.40.0 replaces the SurrealDB daemon with an embedded DuckDB cache plus a SQLite judgment sidecar, rebuilds the ingest pipeline around batch writers and SQL models, and makes recall search full turn text.

2026-08-20ax v0.40.032 referenced changes
announcement

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=turn

Parser 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=14

Sessions 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.

referenced changes

32 linked changes

Generated from Release Please and grouped by change type. Each row keeps its issue and commit references.

⚠ BREAKING CHANGES

  1. 01duckdb: swap the bun:ffi client internals for the threaded napi driver (#880) (#883)
  2. 02v2: replace SurrealDB with an embedded DuckDB cache + SQLite judgment sidecar (#849)

Features

  1. 01duckdb: swap the bun:ffi client internals for the threaded napi driver (#880) (#883) (d8c83b4)
  2. 02improve: auto-mint cache-lens proposals behind corroboration/recurrence/materiality/cap guards (#915) (9267e20)
  3. 03ingest: content-hash watermark - SHA-256 two-tier skip + eager backfill (#900) (#901) (ac50f2f)
  4. 04ingest: golden transcript corpus - one sanitized real transcript per provider + replay test (#877) (1ecf8c4)
  5. 05ingest: read Claude's native attribution fields onto turn_token_usage (#867) (#878) (2bd1f0b)
  6. 06ingest: v3 Phase 2 - NDJSON spool + batch writers for JSONL providers (#886) (#887) (0f1adc7)
  7. 07ingest: v3 Phase 3 - SQL model runner v1 + run-evidence as SQL models (#888) (#889) (a10a92d)
  8. 08insights: cache-bust cost attribution lens as a SQL model (#868) (#890) (c8914b2)
  9. 09lib: BlobPointer branded type + read cold-storage snapshots back (#891) (#892) (34ea07a)
  10. 10prototypes: Phase 5 learning-layer prototype - gate PASSED at matched recall (#895) (#896) (3f9be39)
  11. 11recall: index full turn text, not the 500-char excerpt (#922) (3e89ff1)
  12. 12routability: learned judgment classifier DARK behind AX_JUDGMENT_MODEL=learned (#916) (1b4f280)
  13. 13schema: event-layer freeze - layer classification + TableWrite contract + enforcement (#897) (#899) (a656f69)
  14. 14segment: ax segment export/import - COPY TO export, column-intersection loader, wide-window re-derive (#902) (#904) (405a009)
  15. 15v2: replace SurrealDB with an embedded DuckDB cache + SQLite judgment sidecar (#849) (8642afa)

Bug Fixes

  1. 01ci: bench workflow needs bun install - suite imports workspace pkgs (#874) (cbb90ca)
  2. 02ci: exempt golden-corpus harvest/replay from the no-node-fs gate (#924) (ca25415)
  3. 03cost: coverage line printed the attribution share 100x too small (#881) (#882) (3821e06)
  4. 04duckdb: canonicalize the engine path before staging the napi symlink (#884) (#885) (deb092d)
  5. 05ingest: a forked codex rollout took the fork source's identity (#796) (#861) (abc8438)
  6. 06ingest: normalized session upsert no longer nulls columns it does not own (#898) (#905) (532a8a3)
  7. 07install: make `curl | sh` work on Debian and Ubuntu (#797) (#858) (8356f6d)
  8. 08schema: register schema_comment_state in the DuckDB table manifest (#875) (d1557b9)
  9. 09spool: repair unpaired UTF-16 surrogates instead of failing the whole load (#906) (#907) (589bd6d)
  10. 10test: cost-query fixture turns carry full text - the FTS index reads it since #921 (#923) (3681970)
  11. 11test: gate the byte-for-byte clone assertion on filesystem support (#918) (5da7386)
  12. 12test: make #900 content-tier tests deterministic - stop trusting rewrite mtime (#903) (7ef1b21)

Performance

  1. 01fts: skip FTS rebuild when indexed tables are unchanged (#913) (db927f8), closes #909
  2. 02ingest: rewrite invoked-positions backfill as set-based SQL (#912) (ba3f149), closes #910
  3. 03publish: clone-based snapshot publish (APFS clonefile) with logical-copy fallback (#914) (8a53a3d), closes #908
generated by release please
Show generated changelog for v0.40.0commit-level detail from Release Please

0.40.0 (2026-08-20)

⚠ BREAKING CHANGES

  • duckdb: swap the bun:ffi client internals for the threaded napi driver (#880) (#883)
  • v2: replace SurrealDB with an embedded DuckDB cache + SQLite judgment sidecar (#849)

Features

  • duckdb: swap the bun:ffi client internals for the threaded napi driver (#880) (#883) (d8c83b4)
  • improve: auto-mint cache-lens proposals behind corroboration/recurrence/materiality/cap guards (#915) (9267e20)
  • ingest: content-hash watermark - SHA-256 two-tier skip + eager backfill (#900) (#901) (ac50f2f)
  • ingest: golden transcript corpus - one sanitized real transcript per provider + replay test (#877) (1ecf8c4)
  • ingest: read Claude's native attribution fields onto turn_token_usage (#867) (#878) (2bd1f0b)
  • ingest: v3 Phase 2 - NDJSON spool + batch writers for JSONL providers (#886) (#887) (0f1adc7)
  • ingest: v3 Phase 3 - SQL model runner v1 + run-evidence as SQL models (#888) (#889) (a10a92d)
  • insights: cache-bust cost attribution lens as a SQL model (#868) (#890) (c8914b2)
  • lib: BlobPointer branded type + read cold-storage snapshots back (#891) (#892) (34ea07a)
  • prototypes: Phase 5 learning-layer prototype - gate PASSED at matched recall (#895) (#896) (3f9be39)
  • recall: index full turn text, not the 500-char excerpt (#922) (3e89ff1)
  • routability: learned judgment classifier DARK behind AX_JUDGMENT_MODEL=learned (#916) (1b4f280)
  • schema: event-layer freeze - layer classification + TableWrite contract + enforcement (#897) (#899) (a656f69)
  • segment: ax segment export/import - COPY TO export, column-intersection loader, wide-window re-derive (#902) (#904) (405a009)
  • v2: replace SurrealDB with an embedded DuckDB cache + SQLite judgment sidecar (#849) (8642afa)

Bug Fixes

  • ci: bench workflow needs bun install - suite imports workspace pkgs (#874) (cbb90ca)
  • ci: exempt golden-corpus harvest/replay from the no-node-fs gate (#924) (ca25415)
  • cost: coverage line printed the attribution share 100x too small (#881) (#882) (3821e06)
  • duckdb: canonicalize the engine path before staging the napi symlink (#884) (#885) (deb092d)
  • ingest: a forked codex rollout took the fork source's identity (#796) (#861) (abc8438)
  • ingest: normalized session upsert no longer nulls columns it does not own (#898) (#905) (532a8a3)
  • install: make `curl | sh` work on Debian and Ubuntu (#797) (#858) (8356f6d)
  • schema: register schema_comment_state in the DuckDB table manifest (#875) (d1557b9)
  • spool: repair unpaired UTF-16 surrogates instead of failing the whole load (#906) (#907) (589bd6d)
  • test: cost-query fixture turns carry full text - the FTS index reads it since #921 (#923) (3681970)
  • test: gate the byte-for-byte clone assertion on filesystem support (#918) (5da7386)
  • test: make #900 content-tier tests deterministic - stop trusting rewrite mtime (#903) (7ef1b21)

Performance

  • fts: skip FTS rebuild when indexed tables are unchanged (#913) (db927f8), closes #909
  • ingest: rewrite invoked-positions backfill as set-based SQL (#912) (ba3f149), closes #910
  • publish: clone-based snapshot publish (APFS clonefile) with logical-copy fallback (#914) (8a53a3d), closes #908