v1.3.1 — Cold-start hardening
A stability and correctness release. v1.3.0 shipped the cold-start campaign
(snapshot persistence, serve-before-warm staging, embed cache); v1.3.1 is that
campaign hardened by five independent re-audit rounds, each one auditing the
previous round's fixes. 44 commits, all bug-fix / perf / test — no new
features, no breaking changes. Every fix landed on main under full CI
(build · vet · gofmt · golangci-lint · go test -race · Windows smoke · DB
integration on Postgres/MySQL/MariaDB/SQLite), green across all five rounds.
Upgrading from v1.3.0 is a drop-in. Most fixes are in the long-lived ken-mcp
watch/staging paths, so servers that stay up across file changes and restarts
benefit most.
Highlights
- No more staged-boot crash. A
KEN_MCP_STAGED=1server that loaded a
snapshot on its second start could panic during startup (vectors/chunks fell
out of alignment). Fixed, with a defensive path that degrades to BM25 instead
of ever crashing on a misaligned corpus. - Search results are the source, not a synthetic line. A pre-existing
.ken/index.binbuilt before v1.3.0's enrichment-label change could serve a
fabricated# func: …first line to the agent as if it were real code. The
on-disk format version is now bumped so those stale prebuilts are rejected and
rebuilt. - Correct semantic ranking with a database configured. In the narrow
staged-warmup window withKEN_DB_DSNset,find_related/ semantic search
could rank filesystem chunks by database vectors. Now detected and downgraded
to BM25 until the real vectors land. - Snapshots actually re-persist. A staged server that reconciled a snapshot
on boot never re-wrote it, so drift grew every restart until it forced the full
rebuild snapshots exist to avoid. Fixed. - Crash-durable writes. Snapshot, prebuilt-index, rerank-cache, and
downloaded-model writes nowfsyncthe file before rename and the parent
directory after — a power loss can no longer publish a torn file that passes
its in-memory checks but fails to load. - Honest database errors and tool surface.
reindex_dbno longer appears on
filesystem-only servers, and when a DB operation fails the full error is
actually written to the server log the sanitized message tells you to check
(credentials never leak to the model).
By area
Watch / incremental indexing
- Fixed a debounce busy-spin that could peg a core during a slow flush.
- A stray delete of a never-indexed file (an editor
.swp,.orig,.DS_Store)
no longer triggers a full rebuild + snapshot rewrite. - A directory renamed during an in-flight flush now correctly drops its stale
chunks instead of orphaning them. - Recovery from a dropped-event (inotify overflow) storm now reconciles deletions
against the authoritative in-memory corpus, catching files it previously missed. - Enrichment labels no longer double on each restart; the label now uses an
unambiguous# ken:sentinel.
Cold start (v1.3.0 campaign)
- Staged embedding, snapshot reconcile, and the embed cache all hardened per the
fixes above; snapshot config keys now version and cover lazy/staged modes.
MCP server
reindex_dbregistered only when a database is actually configured.- Structural-index background builds are cancelled when their repo is evicted
from the cache (no more parse goroutine walking a deleted temp clone). - Outline/symbols responses signal when results were truncated (
overshot).
CLI / distribution
ken build-indexoutput is world-readable again (0644), so a prebuilt
.ken/index.binbaked into a container image loads under a non-root uid
instead of silently falling back to a full cold build.
Performance
- Index serialization holds ~2× less peak memory on large corpora.
- Token hashing on the watch path is now zero-allocation.
Process note
The residual risk after five rounds isn't in the code — it's that the audit was
performed in a sandbox that couldn't compile the project. That gap is already
closed on main: CI builds and runs the full race-enabled suite (plus a live
database matrix) on every push, and it is green on this release.
Full commit changelog (auto-generated)
Changelog
Fixes
- 1a3cd39: fix(db): bound startup introspection with KEN_DB_STARTUP_TIMEOUT (audit db/mcp §3) (@townsendmerino)
- 7045bcd: fix(db): introspection determinism + cleanup (audit db/mcp §12, §15, §17, §25, §29) (@townsendmerino)
- 1957fd0: fix(db): sampler robustness — pool churn, BLOBs, FK PKs, panics, timeouts (audit db/mcp §10,§13,§14,§26,§27,§28) (@townsendmerino)
- 4b58a10: fix(ken-mcp): Tier-2 DB chunks survive default-repo LRU eviction (audit db/mcp §1) (@townsendmerino)
- 9fc85c4: fix(lint): enable rowserrcheck + sqlclosecheck; fix the 6 leaks they caught (audit db/mcp §20) (@townsendmerino)
- 1479661: fix(mcp): embedded find_related error mode, MCP rerank validation, per-clone byte cap (audit db/mcp §21, §22, §24) (@townsendmerino)
- 656de7c: fix(mcp): single-pass outline + error survives bad output mode (audit db/mcp §16, §23) (@townsendmerino)
- f295e4d: fix(mcp): strip Arm B enrichment label from search results (audit search §10) (@townsendmerino)
- 034fadd: fix(mcp): structural build honors ctx, heartbeats, and retries on failure (audit db/mcp §8) (@townsendmerino)
- f47a850: fix(mcp): tree-diff recently_changed + honor ctx; cap outline/symbols dumps (audit §5, §6) (@townsendmerino)
- e8638e4: fix(pull-forward): db#4 allowlist, §21 vecs peak, §27 fsync, R14 comment (round-3 leftovers) (@townsendmerino)
- f4e210c: fix(regressions): N1 staged panic, N2 orphaned dir-rename chunks, N8 debounce bypass; make the slow-flush test bite (round-3 re-audit) (@townsendmerino)
- f9afc35: fix(regressions): N3 snapshot key/lock, N4 label sentinel (round-3 re-audit) (@townsendmerino)
- 3bcffb9: fix(regressions): N5 zero-alloc token hash, N6 purge retry, N7 structural-build lifecycle (round-3 re-audit) (@townsendmerino)
- 59c714b: fix(regressions): N9 overshoot signal in JSON responses (round-3 re-audit) (@townsendmerino)
- 36f6945: fix(regressions): R1 busy-spin, R2 label doubling, R3 structural never-converges, R4 bogus row count, R5 label strip (2026-07-26 re-audit) (@townsendmerino)
- 388d935: fix(regressions): R10 usage rotation recovery, R11 token-cache hash key (2026-07-26 re-audit) (@townsendmerino)
- 7d35d3e: fix(regressions): R12-R15 + leftover watcher/credential holes (2026-07-26 re-audit) (@townsendmerino)
- ca580ee: fix(regressions): R4-1 staged-boot panic, R4-3 no-op flush, R4-6 resync — round-4 re-audit (@townsendmerino)
- 730d1d9: fix(regressions): R4-2 bump KEN1 serializeFormatVersion to 2 (round-4 re-audit) (@townsendmerino)
- 08f3549: fix(regressions): R4-4 route LRU eviction through reapEntry (round-4 re-audit) (@townsendmerino)
- 72384b5: fix(regressions): R4-5 reindex diagnostic logging, R4-6 remaining fsync paths (round-4 re-audit) (@townsendmerino)
- a93168b: fix(regressions): R5-1 merged-extras vecs/chunks invariant + round-5 test guards (search) (@townsendmerino)
- e9ea01e: fix(regressions): R5-2 guard typed-nil DBIntegration + log full reindex_db error (@townsendmerino)
- 3e06867: fix(regressions): R5-3 gate reconcile-boot snapshot persist on wi.Warming() (@townsendmerino)
- 4aed5e9: fix(regressions): R5-4 restore 0644 on ken build-index output (@townsendmerino)
- d10a28d: fix(regressions): R6 staged-embed vecs misalign, R7 purge repopulates stale bundle (2026-07-26 re-audit) (@townsendmerino)
- 4e49b7f: fix(regressions): R8 event loop no longer blocks on corpusMu, R9 gitignore-gate new dirs (2026-07-26 re-audit) (@townsendmerino)
- ae2e3ee: fix(repo): gitignore ** respects path-component boundaries (audit search §16) (@townsendmerino)
- 60e1370: fix(search): copy-on-write in reconcileCorpusLocked — kill the flush data race (audit §1) (@townsendmerino)
- 9bdecab: fix(search): tombstone over-fetch, no-panic extras, synced lazy reads, misc Lows (audit §11,§23,§24,§25,§26,§29,§30) (@townsendmerino)
- 9aca434: fix(search): unique temp name for rerank-cache atomic write (audit search §27) (@townsendmerino)
- 17d09d2: fix(security): close three credential/topology leaks (audit db/mcp #2, #4, #7) (@townsendmerino)
- 3aa62a2: fix(structural): case-insensitive extension lookup + correct determinism comment (audit search §18, §28) (@townsendmerino)
- 38366ad: fix(usage): local-tz day boundary + cached append handle (audit db/mcp §9, §19) (@townsendmerino)
- 71bd780: fix(watch): deterministic flush order, size re-check, debounce ceiling (audit search §13, §15, §17) (@townsendmerino)
- 9cbacf3: fix(watch): index new/moved-in dirs, re-path renamed dirs (audit §2, §14) (@townsendmerino)
- 42afd84: fix(watch): recover from fsnotify overflow; tolerate watch-registration failures (audit search §3, §4) (@townsendmerino)
Others
- c9f46a7: perf(db): parallelize the Postgres row sampler over a pool (audit db/mcp §11) (@townsendmerino)
- 10ac79e: perf(search): O(1) rerank min + single precompiled definition pattern (audit search §6, §7) (@townsendmerino)
- 6be2ab9: perf(search): incremental + parallel BM25 tokenization on watch flush (audit §5) (@townsendmerino)
- 508361e: perf(search): serialize into one buffer, pack vecs in bulk (audit search §21) (@townsendmerino)
- 43ace00: perf(watch): run flush async with single-flight so the event loop keeps draining (audit search §12) (@townsendmerino)
- 6fd812c: test(search): make the N2 bite test faithful cross-platform (round-4 verify) (@townsendmerino)