Releases: townsendmerino/ken
Release list
v1.3.1
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) ...
v1.3.0
ken v1.3.0 — cold-start campaign
ken-mcp no longer rebuilds the index from scratch on every launch.
Warm search in ken was always fast (tens of ms). The rough edge was cold
start — every IDE restart re-walked, re-chunked, re-parsed, and re-embedded the
whole repo before serving a single query. This release makes cold start a
one-time cost, not a per-launch tax, and adds opt-in modes that serve a first
query almost immediately on a true-cold index.
Motivated by #61.
The headline (on by default)
Persistent index snapshots + reconcile-on-boot. ken-mcp now persists the
built index to <repo>/.ken/ and, on restart, loads it and drift-scans
(config-key + per-file mtime/size) instead of rebuilding when the repo hasn't
changed. Edit a few files and restart? It reconciles only the changed files
rather than the whole tree.
Time to a servable first query — yii2 PHP corpus (~12k chunks), hybrid, M1 Pro, median of 3:
| scenario | before | after | speedup |
|---|---|---|---|
| restart, repo unchanged | 2.39 s | 573 ms | 4.2× |
| restart after a 1-file edit | 2.39 s | 746 ms | 3.2× |
It's safe by construction: any missing / corrupt / config-mismatched / drifted
snapshot silently falls back to a normal live build, and both on-disk loaders
are fuzzed against hostile input. .ken/ is a plain cache — delete it anytime;
add it to your .gitignore. Turn the whole thing off with KEN_MCP_SNAPSHOT=0.
(This is separate from, and coexists with, the ADR-024 operator prebuilt
.ken/index.bin.)
Serve-before-warm (opt-in)
For the first-ever index of a repo — where there's no snapshot to load yet —
three opt-in knobs move the expensive work off the first query:
| env var | what it does |
|---|---|
KEN_MCP_STAGED=1 |
Serve BM25 lexical results instantly on a cold hybrid build (~4.2× faster first query), then embed to full hybrid in the background. Responses carry "semantic":"warming" until the upgrade lands. |
KEN_MCP_LAZY_ENRICH=1 |
Defer the structural-enrichment tree-sitter parse off the cold path (serve raw, enrich in the background). |
KEN_MCP_EMBED_CACHE=1 |
Persistent sha256(chunk)→vector cache at <repo>/.ken/embed.db, so a full rebuild re-embeds only never-seen text. |
Also new:
ken index --write-snapshot— build once and persist, so a later
ken-mcplaunch on that repo loads instead of rebuilds (CI prewarming).- Lazy structural index —
ken-mcpdefers the symbol index for the
definition/references/callers/outline/symbolstools to first use,
removing a redundant full-corpus parse from every cold start. KEN_ENRICH_FILE_BUDGET_MS— a per-file wall-clock parse budget that
skips (and logs) a pathological template-like file the size cap misses.
Honest notes
- All numbers above are M1 Pro (10-core / 16 GB). The speedups should port,
but absolute times won't hold on a 4-core laptop — so the serve-before-warm
modes ship off by default until we've measured them on that hardware
class. If you run ken on a constrained machine, tryKEN_MCP_STAGED=1and let
us know how it lands. - Why "take the parse off the cold path" rather than "make it faster": the
gotreesitter0.20.5 → 0.47.0bump we shipped in 1.1.1 (a correctness fix)
regressed the PHP tree-sitter parse ~2.7×, and that parse is ~50% of cold
index time. Details + the upstream data point are in the repo docs. - Index throughput and warm-search latency are unchanged — the opt-in modes are
off by default, and the default index path is untouched.
Install / upgrade
brew upgrade ken # Homebrew
scoop update ken # Scoop
go install github.com/townsendmerino/ken/cmd/ken@v1.3.0
go install github.com/townsendmerino/ken/cmd/ken-mcp@v1.3.0No config changes needed — snapshots turn on automatically. Full details in
CHANGELOG.md.
v1.2.1
ken v1.2.1 — memory campaign + security & correctness hardening
The biggest release since 1.0. Three strands came together: a memory campaign that gives you real control over what ken indexes and how much RAM it holds, a full security & correctness review (two process-kill bugs and seven majors, each fixed with a regression test), and bounded, cancellable shutdown.
No breaking changes. Every public 1.0 signature is unchanged; new API and env vars are purely additive. Upgrading is a drop-in — no config edits required.
🔐 Security note: 1.2.1 is a same-day security fast-follow to 1.2.0. It bumps
golang.org/x/textto v0.39.0 to fixGO-2026-5970(an infinite loop on invalid input), whichgovulncheckflagged as reachable via the Postgres connect path and Unicode normalization. The advisory landed after 1.2.0 was tagged, so 1.2.0's binaries shipped the vulnerable version — use 1.2.1. Everything else below is the 1.2.0 feature set, unchanged.
🗂️ .kenignore — control what ken indexes
Your repo commits things you don't want searched: built JS/CSS bundles, generated migrations, vendored code. ken was gitignore-only, so on artifact-heavy monorepos it indexed roughly 2× the files it should have — inflating cold-start time and memory.
Drop a .kenignore (gitignore syntax, nested per-directory) to fix that:
web/assets/ # compiled bundles
vendor/ # third-party code
**/migrations/*.php # generated migrations
*.min.js- Union with
.gitignore— excluded if either matches; evaluated independently so a!negationin one can't re-include what the other dropped. .sembleignorehonored as a fallback — a drop-in for anyone migrating from semble.- Default-on and inert without the file — your existing
.gitignorebehavior is untouched. - Even without one, ken now auto-skips oversized files (
KEN_MAX_FILE_BYTES) and minified/generated files (KEN_MAX_AVG_LINE_BYTES).
On a committed-artifact monorepo this is the single biggest lever on index size, cold start, and RAM.
🧠 Lower, more predictable memory
For the long-lived ken-mcp server: GOGC=50 by default, an opt-in KEN_MEMLIMIT soft cap, and freed pages returned to the OS after builds and watch flushes — so idle RSS settles near the live index size instead of the build high-water. KEN_MAX_FILES rejects a hostile "millions of tiny files" repo rather than letting it OOM. A new scripts/rss_bench.sh measures it the same way external benchmarks do.
🔒 Security & correctness review
Every finding fixed and regression-tested:
top_kcould OOM-crash the whole server (Critical). An unboundedtop_kreached a giant allocation (and a filter path could overflow into a panic). Now clamped.- One source file could fatal-crash the server (Critical). The structural indexer parsed files with none of the size/stop-reason guards its sibling path had, so a pathological file could uncatchably overflow the goroutine stack. Both paths now share one guarded parser.
- DB passwords no longer leak in a malformed-DSN error (MySQL + Postgres), which also fixes passwords containing
/ @ ?. - Symlink-escape closed on the watch path — an out-of-root symlink can no longer be indexed.
- Plus: shutdown cleanup on stdin-EOF, cache eviction moved off the hot lock, watch-path enrichment parity, a lock-free-reader race in the reranker, and a batch of determinism/robustness fixes.
🔐 Verified model downloads
ken download-model and the background auto-fetch now verify each file as it streams — SHA-256 against HuggingFace's git-lfs ETag, and byte count against Content-Length / X-Linked-Size — so a truncated or swapped download is rejected before it lands. Concurrent fetches use a per-process temp file.
🛑 Graceful shutdown that can't hang
In-flight tool calls drain within KEN_MCP_SHUTDOWN_GRACE (default 5s), a second Ctrl-C force-quits, and a large in-flight index build now honors cancellation instead of blocking shutdown until it finishes.
New environment variables
| Variable | Default | Purpose |
|---|---|---|
KEN_MAX_FILE_BYTES |
2MiB |
Per-file size cap for indexing |
KEN_MAX_AVG_LINE_BYTES |
1000 |
Skip minified/generated files (0 disables) |
KEN_MAX_FILES |
1000000 |
Admission cap — reject huge/hostile repos (0 = unlimited) |
KEN_MEMLIMIT |
(unset) | Soft memory limit (overrides GOMEMLIMIT) |
KEN_MCP_SHUTDOWN_GRACE |
5s |
Bounded in-flight drain window on SIGINT/SIGTERM |
Install / upgrade
# Homebrew (macOS/Linux)
brew upgrade --cask ken # new: brew install --cask townsendmerino/tap/ken# Windows (Scoop)
scoop update ken # new: scoop bucket add townsendmerino https://github.com/townsendmerino/scoop-bucket; scoop install ken# Or with Go
go install github.com/townsendmerino/ken/cmd/ken@v1.2.1
go install github.com/townsendmerino/ken/cmd/ken-mcp@v1.2.1Nothing to change on upgrade. If you're on an artifact-heavy repo, adding a .kenignore is the one thing worth doing.
Full changelog: CHANGELOG.md · compare v1.1.1...v1.2.1
v1.2.0
⚠️ ken v1.2.0 — superseded by v1.2.1 (security)
Use v1.2.1 instead.
v1.2.0's binaries shipped with golang.org/x/text v0.37.0, which is affected by GO-2026-5970 — an infinite loop on invalid input, reachable via ken's Postgres-connect and Unicode-normalization paths. The advisory was published to the Go vulnerability database shortly after v1.2.0 was tagged.
v1.2.1 is an identical release with x/text bumped to v0.39.0 — same features, fixed dependency.
The full feature notes for this release (the .kenignore memory campaign, the security & correctness review, verified model downloads, and bounded shutdown) are on the v1.2.1 release page.
v1.1.1
ken v1.1.1 — crypto/tls CVE fix + dependency updates
A patch release. It closes a reachable standard-library TLS vulnerability and folds in routine dependency updates. No API, wire-format, or default changes from 1.1.0 — a drop-in upgrade.
🔒 Security
- Toolchain go 1.26.4 → 1.26.5 fixes
GO-2026-5856— an Encrypted Client Hello privacy leak in the standard library'scrypto/tls. ken'sgovulncheckCI gate (added in 1.1.0) flagged it as reachable, with real call paths through the MySQL introspection TLS handshake and the model-fetch HTTPS client. The fix is a singlego-directive bump ingo.mod, which propagates to every CI job (all readgo-version-file: go.mod);govulncheck ./...now reports 0 affecting vulnerabilities. No source changes.
📦 Dependencies
No ken API changes; the full test suite and govulncheck are green on the combined tree.
github.com/townsendmerino/aikitv1.5.0 → v1.9.0 — the extracted algorithm packages (BM25 / embed / ann / encoder / chunk / fuse / topk).modernc.org/sqlitev1.52.0 → v1.53.0 — pure-Go SQLite driver (Tier-2 DB indexing).github.com/odvcencio/gotreesitterv0.20.2 → v0.20.5 — pure-Go tree-sitter (treesitter chunker + Arm B enrichment).actions/checkoutv6 → v7 (CI).
Install / upgrade
# Homebrew (macOS/Linux) — ken ships as a cask
brew upgrade --cask ken # new install: brew install --cask townsendmerino/tap/ken# Windows (Scoop)
scoop update ken # new install: scoop bucket add townsendmerino https://github.com/townsendmerino/scoop-bucket; scoop install ken# Or with Go
go install github.com/townsendmerino/ken/cmd/ken@v1.1.1
go install github.com/townsendmerino/ken/cmd/ken-mcp@v1.1.1No config changes required. Upgrading is recommended for the TLS CVE fix if you use ken-mcp's remote-clone or MySQL Tier-2 introspection paths.
Full changelog: CHANGELOG.md · compare v1.1.0...v1.1.1
ken v1.1.0 — security hardening + int8 reranker by default
A feature-and-hardening release. The int8 reranker becomes the default now that it's free, output: "json" reaches the last tool, the remote-clone path closes its two known SSRF gaps, and a govulncheck gate joins CI.
No breaking changes. mcp.Run / mcp.NewServer / the chunk.Chunker interface and the MCP wire format are unchanged from 1.0. The int8 default and the new KEN_MAX_CLONE_BYTES knob are both backward-compatible — existing configs keep working.
Highlights
⚡ int8 reranker is now the default
aikit v1.5.0 fixed the q8 path, so int8 now matches f32 reranker latency (50-doc cold: 7.35 s vs 7.75 s on arm64) at ~21× less runtime memory (18 MiB vs 379 MiB) and ¼ the weight footprint (~140 MB resident vs ~547 MB) — with cosine 0.997 vs f32, unchanged ranking quality. KEN_MCP_RERANK_QUANT and --rerank-quant now default to int8; pass f32 for the full-precision path. No re-download needed — LoadQ8 quantizes your existing CodeRankEmbed snapshot in-process.
This reverses the "int8 is slower on Apple Silicon" note from 1.0.1 — that was the pre-fix aikit path.
🧩 All nine MCP tools now speak JSON
recently_changed was the last markdown-only tool; it now accepts output: "json" and returns a typed RecentlyChangedResponse (considered-count + per-commit hash / short_hash / subject / author / RFC3339 timestamp / changed_files), built from the same rows as the markdown render so the two can't drift.
🔒 Security
- Clone SSRF guard hardened + a byte cap.
ken-mcp's remote-repo clone now re-validates the resolved IP at connect time and dials it literally (TLS still verifies the hostname via SNI) — closing the DNS-rebinding TOCTOU the pre-flight check couldn't catch, including redirects to internal hosts. It also byte-caps the clone stream (KEN_MAX_CLONE_BYTES, default 2 GiB) so a hostile server can't stream an unbounded/pathological pack. Both checks honorKEN_ALLOW_PRIVATE_CLONE_TARGETS=1. govulncheckCI gate. Go's symbol-level vulnerability scanner now runs on every push. Bringing it green fixed 9 reachable CVEs (bumpedgolang.org/x/cryptoto v0.52.0 and the toolchain to go 1.26.4). The gate blocks any reachable-CVE regression from landing.
🐛 Fixed
- Data race in the definition-pattern cache.
internal/search'sdefPatternCache(the compiled-regex memo behind thedefinitiontool) was read and written unsynchronized; concurrent queries could race it. Now guarded by anRWMutex, with a race-detector regression test.
📚 Docs & tooling
New top-level CONTRIBUTING.md and SECURITY.md, a Makefile, a "Compared to other agent code-search tools" table in the README, and a "Personally-namespaced dependencies" section in DEVELOPERS.md documenting the gotreesitter dependency delta and the go.work local-aikit workflow.
Install / upgrade
# Homebrew (macOS/Linux) — ken ships as a cask
brew upgrade --cask ken # new install: brew install --cask townsendmerino/tap/ken# Windows (Scoop)
scoop update ken # new install: scoop bucket add townsendmerino https://github.com/townsendmerino/scoop-bucket; scoop install ken# Or with Go
go install github.com/townsendmerino/ken/cmd/ken@v1.1.0
go install github.com/townsendmerino/ken/cmd/ken-mcp@v1.1.0No config changes are required to upgrade. If you run the reranker (KEN_MCP_RERANK=on) and want to keep the full-precision path, set KEN_MCP_RERANK_QUANT=f32.
Full changelog: CHANGELOG.md · compare v1.0.1...v1.1.0
v1.0.1
ken v1.0.1 — faster hybrid search
A patch release: same retrieval quality, ~3× faster hybrid search. No API changes — mcp.Run, mcp.NewServer, mcp.NewCache, FormatResults, the chunk.Chunker interface, and the MCP wire format are unchanged from 1.0.0, so this is a drop-in upgrade.
~3× faster hybrid search (measured)
The headline comes from a aikit bump (v1.0.0 → v1.4.0) that rewrote the semantic-arm cosine scan (ann.Flat.Query) from a scalar-float64 loop to a SIMD-float32 dot kernel with 8-vectors-per-pass streaming. Benchmarked before/after on the same machine:
Flat.Query11.7× faster in isolation (2061 µs → 176 µs over 8 000×256).- End-to-end hybrid
searchp50 −66 % — 4.58 ms → 1.56 ms on a ~13 k-chunk corpus. The scan is O(N) in chunks, so the win grows with corpus size (it's invisible on tiny repos where search is already sub-millisecond, large on real ones). - Indexing time unchanged — the bump didn't touch the index path.
Quality is provably untouched: recall@10 was re-verified identical (0.967 NL / 0.995 symbol), and embedding parity holds (golden cosine ≥ 1 − 1e-5 vs the Python reference). The neural reranker is unchanged too (the encoder vectorization in this aikit range targets the int8 path; ken's default reranker runs f32).
Also in this release
- Deserializer fuzzing.
FuzzDeserializeIndexandFuzzDecodeRerankCachevalidate the hand-written adversarial-input defenses in the KEN1 / KNRC binary parsers — the index parser is real attack surface, sinceken-mcpauto-loads<repo>/.ken/index.binfrom shallow-cloned remote repos. 2.6M executions, zero crashers. - Model-download fix.
ken download-modelno longer mistakes a leftover Git-LFS / HF-hub pointer stub (or a broken symlink / truncated file) for a real model. The "already present" check now validates file size, so a stubbed model gets re-downloaded instead of silently failing to load later. - Robustness from the aikit bump on exactly the paths ken runs: a safetensors mmap-lifetime guardrail and two fuzz-fixed untrusted-tensor crashes in
embed, plusbm25/chunkindexing-pipeline fuzzing. - Docs accuracy sweep. A full pass aligned the documentation with the post-1.0 / post-extraction reality (package locations, the 10-tool MCP surface, 13 languages, the
mcp.Runlibrary example, regenerated third-party licenses, and a current-stateARCHITECTURE.md).
Upgrade
brew upgrade --cask ken # or: scoop update kenOr grab a binary below, or go install github.com/townsendmerino/ken/cmd/ken-mcp@v1.0.1. Full detail in CHANGELOG.md; benchmark methodology in docs/PERF-expectations.md.
v1.0.0
ken v1.0.0 — code search for agents, in one binary
ken is a hybrid code-search engine for AI agents: BM25 lexical + Model2Vec semantic embeddings + reciprocal-rank fusion + a code-aware ranking pipeline, served over MCP. It is a faithful Go port of MinishLab/semble — the retrieval algorithm is ported verbatim and validated against the Python reference — packaged as a single static, pure-Go, no-cgo binary: no Python, no GPU, no vector DB, no API keys, air-gapped friendly.
1.0 means two things: the public API surface is frozen (mcp.Run, mcp.NewServer, mcp.NewCache, FormatResults, the chunk.Chunker interface, and the typed JSON-output structs), and the retrieval quality is measured, reproducible, and at its ceiling for this corpus class — every headline number below has an in-tree reproduction harness.
The numbers (all reproducible — see docs/BENCH.md)
- ~97% recall@10 in the default (hybrid) mode — 0.967 NL / 0.995 symbol on semble's 1,251-query, 63-repo benchmark — at ~46× fewer agent tokens than
grep + Readfor NL queries. grep still wins exhaustive enumeration (~99.9%); ken's job is "find the chunk that answers this," at 1–2 orders of magnitude lower token cost. - NDCG@10 0.842 vs semble's published 0.854 — the verbatim-port validation, with the semantic-only arms matching within 0.003.
- Opt-in neural reranker: +0.165 NDCG@10 on CoIR — a CodeRankEmbed transformer running entirely in pure Go (hand-written ARM64 NEON assembly, optional int8 quantization, disk-persisted embedding cache).
- At 280K-file corpus scale, grep+Read needs ~16M tokens per NL query (past any context window); ken answers in ~1,300.
Highlights
Zero-setup onboarding — new installs land on the good path
ken-mcp now auto-fetches the embedding model (~60 MB) in the background on first run: it serves BM25 immediately, then transparently upgrades to hybrid (~0.83 → ~0.97 recall@10) when the model lands. No flags, no setup step, stderr-only progress — the JSON-RPC stream stays clean. (KEN_MCP_AUTO_FETCH=0 restores warn-and-downgrade.)
Structural navigation — nine MCP tools, 13 languages
Beyond ranked search (search, find_related), ken builds a structural index from its tree-sitter parse and answers structural questions exactly, by lookup: definition, references, callers, outline, symbols — plus status and recently_changed. Honest framing baked into the tool descriptions: name-resolved, not type-resolved (for type-level call hierarchy, use an LSP). The callers edge data ships with a published precision sample (100% on 400 edges across 8 languages). Languages: Python, Go, TypeScript, JavaScript, Java, Rust, C, C++, C#, PHP, Ruby, Kotlin, Dart.
Retrieval quality, measured the hard way
- Structural enrichment (default-on): every chunk from a supported language is prefixed with an AST-derived label (
# func: NAME | calls: A, B | raises: X) before indexing — +0.02 NDCG@10 on leak-free benchmarks, pure Go, no model,KEN_ENRICH=offto disable. - The recall numbers come from a decomposition harness (
internal/search/recall_decomp_test.go) that separates candidate-generation loss from ranking loss — the same harness that caught our own docs misreporting the BM25-only fallback (~0.84) as ken's recall. The corrected, reproducible story is indocs/BENCH.md.
An index that keeps up
- Watch mode by default:
ken indexstays alive and re-publishes an atomic snapshot ~2s after any file change;ken-mcpalways watches, so agents see their own edits. - Pre-built indices: serialize an index into your binary (
mcp.Run+//go:embed) for instant-start embedded corpora;ken-mcpauto-loads<repo>/.ken/index.bin. - Database schema indexing: point ken at Postgres/MySQL/MariaDB/SQLite and search your schema + migration history alongside code (
reindex_db).
Drop-in for semble
Same search / find_related tool schemas, same markdown wire format. If your agent already uses semble, swap the command: path and nothing else changes.
Runs everywhere
macOS, Linux, Windows (amd64 + arm64). brew install --cask townsendmerino/tap/ken, scoop install ken, direct download, or go install. Single static binaries; slim builds ~23 MB (ken) / ~40 MB (ken-mcp).
Try it in 60 seconds
brew install --cask townsendmerino/tap/ken # or scoop / download / go install
claude mcp add ken -s user -- $(which ken-mcp)
# ask your agent: "where do we handle retry backoff?" — ken auto-fetches the model on first runOr try the flagship demo — the Go standard library, pre-indexed, model baked in, zero config: ken-demo-go-stdlib. Every Go developer knows this corpus by heart — verify the answers yourself, then reproduce against your own $GOROOT/src in 30 seconds (demos/go-stdlib/README.md). Supporting demos: kubernetes (90K chunks, scale) and postgres (treesitter on C).
Honest limits
- Exhaustive enumeration belongs to grep. Refactors and pre-rename audits need 100% literal recall; ken optimizes relevance per token, not completeness.
- Structural tools are tree-sitter-grade. Name resolution, ranked candidates — not a compiler. Same-spelled symbols collapse; type-level questions need an LSP.
- The neural reranker is opt-in (
--mode=hybrid-rerank/KEN_MCP_RERANK=on): it buys NDCG at real latency cost on first contact with a repo (the embedding cache persists to disk and is fast thereafter).
Provenance & thanks
ken stands on semble and the potion-code-16M model by MinishLab, and CodeRankEmbed by Nomic AI / CoRNStack for the reranker. Tree-sitter parsing via gotreesitter; algorithm packages live in aikit (pinned at its own 1.0). Everything runs in pure Go — including the transformer.
Full change history: CHANGELOG.md. Benchmarks and methodology: docs/BENCH.md.
v0.10.1
Changelog
Features
- 43e93df: feat(release): publish Homebrew cask + Scoop manifest (brew/scoop install) (@townsendmerino)
v0.10.0
v0.10.0 — C# · automatic model onboarding · Windows
Three headline additions — plus a measurement correction that's the most important thing in this release.
Highlights
- 🪟 Windows binaries.
kenandken-mcpnow ship for Windows (amd64 + arm64) alongside macOS and Linux — pure-Go single static binaries,.ziparchives. - ⬇️ ken-mcp auto-fetches the embedding model on first run. A fresh
ken-mcpwith no model now downloads it in the background (~60 MB, pure-Go, no Python) and upgrades to hybrid automatically — new installs land on the high-recall path with zero setup.KEN_MCP_AUTO_FETCH=0to opt out. (ADR-037) - #️⃣ C# is the 13th language. gotreesitter v0.20.2 bounded the namespace-recovery recursion that used to OOM ken's indexer on real C#, so
.csfiles now get full structural extraction + Arm B enrichment.
The measurement correction (read this one)
Auditing recall for 1.0, we found the widely-quoted "82–91% recall@10" was the BM25-only fallback — the mode ken runs in when no embedding model is installed. The shipped default (hybrid) mode measures ~97% recall@10 (0.967 NL / 0.995 symbol on semble's 1,251-query benchmark), at ~46× fewer agent tokens than grep+Read (4,120 vs 189,773 median on NL queries).
That reframes the product story: ken isn't "82% and hope" — it's near-grep recall at 1–2 orders of magnitude lower token cost, if you're on hybrid. Which is exactly why auto-fetch is in this release — the only users on the 82% path were the ones who hadn't installed the model, and now that's automatic. Every number is backed by a reproducible decomposition harness (internal/search/recall_decomp_test.go), and the README / benchmark docs were corrected throughout.
Also in this release
- Phase 0 structural call-graph substrate — per-call-site records + span fields on every symbol; groundwork for resolved function-level
callers/impact(no tool-surface change yet). - Crash fix — a guard against a gotreesitter stack overflow on oversized (64 KiB+) table-driven files that could take down
ken index. - Docs overhaul — README trimmed 711 → 184 lines with the recall headline up front; working docs moved to
docs/internal/; a newdocs/db-indexing.md.
Install
go install github.com/townsendmerino/ken/cmd/ken@v0.10.0
go install github.com/townsendmerino/ken/cmd/ken-mcp@v0.10.0Or grab a pre-built binary below — .tar.gz for macOS/Linux, .zip for Windows (amd64/arm64). Point your agent at ken-mcp; it fetches the embedding model on first run.
Full notes: CHANGELOG.md · compare v0.9.1...v0.10.0