Vectr v1.0.0: The Release Gate, the Bugs It Caught, and the Numbers I Didn't Round Up

A tag went out quietly at 1:51am. This is the gate it had to clear, the three bugs that gate caught, and the cost numbers — wins and losses — that shipped next to each other in the README.

At 1:51am on July 8th I ran git tag v1.0.0 and pushed it. No fanfare, no launch thread queued up in advance — I'd been saying "soon" about this for weeks and I wanted the tag to exist before I let myself write about it.

The release is the same tool the last four posts on this blog have been about: the indexer from Part 1, the working memory from Part 2, the benchmark methodology from Part 3, the hook pipeline and the four families of context relief from the two posts after that. None of those posts were about shipping. This one is.

"1.0" is doing real work as a label here, so let me be precise about what it means: not "feature complete," not "no bugs," but this is the version I'm willing to stand behind with the honest numbers printed next to the flattering ones. That standard is what the last week of work before the tag was actually about, and it's the part that doesn't show up in a changelog diff — so this post is about the gate the release had to clear, the bugs that gate caught, and the numbers that made it into the README without getting rounded in vectr's favor.

Part 01
The Release
01

What 1.0 Actually Is, Briefly

If you've read the earlier posts you know the shape of this already, so I'll keep the recap short. Vectr is two things wired into one MCP server, served locally, no API key, MIT-licensed.

Semantic codebase search

Hybrid retrieval combining a local dense embedding model (ibm-granite/granite-embedding-english-r2, swapped in after earlier posts referenced Snowflake's arctic-embed) with BM25, weighted by how large and unfamiliar the codebase looks. Underneath that, dual-vector indexing: every symbol gets a body-stripped "purpose" vector alongside its full-body vector, closing a dilution gap where a symbol's own doc paraphrase of your query didn't surface it in the dense pool. A symbol graph backs vectr_locate (five fallback strategies, unchanged since Part 1) and vectr_trace for callers and callees. AST-aware chunking now covers 9 languages — Python, JavaScript, TypeScript, Go, Rust, Java, Zig, C, and C++ — with a windowed fallback for everything else.

Persistent working memory

The layer Part 2 was about. Five note kinds now carry genuinely distinct injection semantics instead of being flavors of the same thing: directive fires unconditionally every session, task carries current-work state, gotcha resurfaces when its anchored file is touched, finding is relevance-ranked, reference is a pointer. Recall is two-tier — a token-bounded index by default, full bodies only on request. Boot injection puts directives and high-priority tasks in front of the model before turn one. Snapshots checkpoint a whole working-memory state under a label.

14 MCP tools total. Everything runs on your machine; nothing about a search or a recall call leaves it.

Part 02
The Gate
02

The Release Gate

Here's the part that isn't in the README. Before the tag went out, the three families of adoption behavior that make working memory actually get used — not just exist — went through what I've started calling a dogfood gate: a fresh reviewer, with no memory of building any of this, driving the release candidate through the real MCP tool surface, call by call, on real tasks. Not reading the code and nodding at it. Actually calling vectr_remember, actually calling vectr_recall, actually watching what came back, exactly the way a caller model would.

The three families under review map neatly onto the four families of context relief I wrote about a few weeks ago: subagent working memory (families 2 and 4 combined — offload-and-recall serving as the shared bus between an orchestrator and its subagents), token eviction (family 1, vectr's vectr_evict_hint and vectr_fetch), and hook-injected recall (the delivery mechanism from the hooks post). Two of the three passed clean: subagent memory and hook injection held up under adversarial first-person use. Eviction came back with a verdict of REWORK and two release-blocking defects.

Why a gate, not a review

The value of a dogfood gate over a design review is that it only trusts what actually happened in a real tool call. Reading eviction_advisor.py and reasoning about whether the recency ordering is correct is a code review. Calling vectr_search eight times, re-running the same query on purpose, and reading what the hint actually rendered is the only way either of the next two bugs gets found — they're both invisible from the diff.

That "re-running the same query on purpose" is the whole trick — it's the seam all three bugs hid in. A caller that has already seen a result will ask for it again — it re-checks a file it just touched, it re-fetches a chunk it saw ten turns ago. The first call to any of these paths worked fine. It was the second identical call that misbehaved, and a code review reading the function top-to-bottom has no reason to imagine that second call. You have to actually make it.

03

Bug One: Re-Retrieving a Chunk Made It Look Stale

vectr_evict_hint orders the files it lists by recency — most-recently-retrieved first, so the caller sees what it just touched at the top. The recency dedup guard broke this in the single most common case: retrieving the same chunk twice. record() deduped a repeat retrieval by returning early — correct, in that it doesn't double-count tokens or store a second copy — but that early return also meant it never moved the chunk's position in the recency-ordered list. Run the same search again, get the same top hit back (which is exactly what happens when a caller re-checks something), and the file you'd just re-retrieved rendered dead last, as if it were the oldest thing in the session.

eviction_advisor.py · record()
# before — dedup guard returns early, recency position never refreshes
def record(self, file_path, lines, ...):
    key = f"{file_path}:{lines}"
    if any(f"{c.file_path}:{c.lines}" == key for c in self._chunks):
        return
    self._chunks.append(RetrievedChunk(...))

# after — dedup hit moves the existing entry to the end instead of a no-op
def record(self, file_path, lines, ...):
    key = f"{file_path}:{lines}"
    for i, c in enumerate(self._chunks):
        if f"{c.file_path}:{c.lines}" == key:
            self._chunks.append(self._chunks.pop(i))  # fresh recency, no dup, no counter bump
            return
    self._chunks.append(RetrievedChunk(...))

The fix is three lines and reads as obvious once you've seen the failure. The point is that you don't see the failure by reading the "before" version — the early return looks like a correct, even careful, dedup. You see it by retrieving a.py, then b.py, then a.py again, and noticing that a.py — the thing you touched last — is sitting at the bottom of the list labeled as the stalest chunk in the session. The demo below is exactly that sequence. Toggle between the shipped fix and the bug and watch where a.py lands on the re-retrieve.

Interactive · Demo 01

Watch the Recency Bug Happen

This is what the dogfood reviewer saw. Retrieve a few chunks, then re-retrieve one, and read what the eviction hint renders — a recency-ordered file list and the "re-fetch keys" line beneath it. Flip between the shipped fix and the two bugs to see how the same call sequence produces a list that lies about what you touched last.

Eviction hint — files, most recently retrieved first
  • Retrieve a chunk to start the session.
Re-fetch keys (escalated banner, max 3 ids)
The list and the key line are rendered by the same ordering logic the tool uses. In "before" mode, re-retrieving a chunk is a no-op that never refreshes its position (bug 1), and the re-fetch keys slice the front of an oldest-first list so they pin the session's first chunks forever (bug 3).
04

Bug Two: The "Byte-Verbatim" Promise Had a Silent Exception

vectr_fetch exists to restore a chunk you saw earlier, exactly as it was, no re-search and no file re-read required. But large classes and methods get capped at index time — roughly 2000 characters per stored chunk — and _format_fetch_results() rendered whatever content was stored, with no signal when that content was itself incomplete.

_format_search_results() already detected this by comparing the rendered lines against the symbol's recorded start/end span and appending a Read(...) fallback; vectr_fetch never ran that check. The result: re-fetching a storage-capped chunk returned it silently truncated, directly contradicting the one guarantee vectr_fetch exists to make. A caller that trusted the re-fetch — the entire reason the tool exists is so it can trust the re-fetch — would work from a class body missing its last third and never know.

The fix pulled the detection into a shared _storage_cap_truncation_warning() helper so both surfaces — search's first showing and fetch's later re-showing — flag a capped chunk identically, and applied it to the CLI's vectr fetch renderer as well, which had the same gap on its own code path.

The gap between two showings

Both of the first two bugs are the same category: the first time a chunk was shown, everything was correct — search flagged the truncation, the file list ordered fine. The bug lived in the re-showing. Search and fetch had drifted into two separate rendering paths, and the safety check lived on only one of them. This is why re-running the same action is the load-bearing move in the gate: a single showing exercises one path and passes.

Both fixes shipped the same day, each with its own test — a dedup-then-recency-position test for the first, a re-fetch-of-a-capped-chunk test for the second — before the tag went anywhere near being cut.

05

Bug Three, Found by Asking a Different Question

A separate pass — not the adoption gate, but a measurement pass checking whether eviction was actually efficient, turn over turn — turned up a third defect the gate hadn't been looking for. The escalated eviction banner (the "action required, you have unsaved findings" version that only fires once both a chunk-count and a token-count threshold are crossed) lists a Re-fetch keys line telling the caller exactly which ids it can pull back instead of re-reading. That list was byte-identical across an entire session, no matter what had been retrieved since.

The cause: it sliced the front of an oldest-first list, so it pinned the session's very first chunks in that slot permanently.

eviction_advisor.py · escalated banner
# before — prefix slice over an oldest-first list pins the OLDEST ids forever
fetch_ids = [c.chunk_id for c in self._chunks if c.chunk_id][:EVICTION_HINT_MAX_IDS]

# after — suffix slice + reverse: newest retrieved first, matching the file list above it
fetch_ids = [c.chunk_id for c in self._chunks if c.chunk_id][-EVICTION_HINT_MAX_IDS:][::-1]

A one-line fix, caught only because someone asked "does this banner's content actually track the session, or does it just look plausible once?" — the kind of question a code review doesn't naturally ask and a live measurement pass does. It was fixed and tested the same day, before the README got its final pass and well before the tag. (It's the second half of the demo above: in "before" mode, the re-fetch keys line never changes no matter how many chunks you retrieve after the first three.)

The rule in force for all three: a defect found while dogfooding the release candidate gets fixed before release. Not logged as a follow-up, not shipped with an asterisk. If it's bad enough to find in a gate, it's bad enough to hold the tag for a few hours.

Part 03
The Numbers
06

The Honest Numbers

This is the section I care most about getting right, because it's the one most launch posts skip. Most "we shipped v1.0" posts for AI dev tools print the number that makes the tool look good and stop there. Vectr's README has a section literally titled "Measured costs, honestly", and the discipline behind that title is the actual point of this post, not a footnote to it.

The clean win

Recalling 3 stored notes with vectr_recall costs 360 tokens in one tool call. Re-deriving the same three facts with grep and Read, on the same 182-file Python repo, costs roughly 2,060 tokens across six tool calls — about 5.7× fewer tokens, 6× fewer calls, and it comes back in under 50 milliseconds. That's the /compact-and-cross-session story from Part 2, with an actual token count attached instead of an intuition.

The sprint-scale win, with its floor shown

Across the 6-task CPython sprint from Part 3, recall discipline cut re-discovery by 39% overall — but the per-task spread runs the full range from 0% to 85%, and the 0% task isn't hidden in a footnote. It's debug_descriptor_priority: a bug in Python's descriptor protocol, a part of the language the model already knows cold from training. Vanilla and vectr took the same 6 re-discovery calls. The honest read is the same one Part 3 landed on — vectr's advantage is proportional to how unfamiliar the code actually is to the model, and printing the 0% case next to the 85% case is what makes the 39% mean something.

The other side of the ledger

For a single pointed lookup on a small, already-familiar repo, grep is cheaper — vectr's median cost across 5 single-fact tasks ran +60% more tokens than grep — and grep is faster: a vectr_search round trip takes 1.7–3.6 seconds against roughly 28 milliseconds for a local grep. And the automatic reminder banners riding on tool responses aren't free either: the always-on re-fetch footer costs about 27 tokens, a light nudge about 89, and the escalated action-required banner (the one bug three lives in) scales from roughly 480 to 535 tokens before it plateaus.

Interactive · Demo 02

The Token Ledger, Both Directions

Switch between the two cases vectr was built to win and the one it was built to lose. Each shows the measured token cost of vectr against grep-and-read for the same job, plus the call count and latency — so you can see the trade-off as a shape, not a slogan.

Every figure here is a measured number from the shipped README, not a projection: 360 vs ~2,060 tokens on recall, −39% re-discovery on the sprint (0%–85% spread), +60% tokens and 1.7–3.6s vs ~28ms on the single lookup.
Read the number where it loses

None of those costs are hidden or estimated after the fact — they're the same numbers in the shipped README, next to the wins, not in a separate "limitations" section three scrolls down. If a tool's marketing only shows you the number where it wins, ask what the number looks like on the case it was built to lose.

The shape of the trade-off, stated plainly: vectr pays off on unfamiliar or large codebases, on work you come back to — later in the same session, after /compact, or in a new session entirely — and on long sessions with many turns. It does not pay off on a one-off grep against code you already know cold. That second sentence is not a caveat I'm burying. It's half of the value proposition. A tool whose docs only describe the conditions under which it wins is a tool whose docs you can't actually trust when it's your codebase on the line.

Part 04
The Last Mile
07

What's New in the Last Mile

A handful of changes landed in the final pre-release wave, mostly because dogfooding the tool surfaces exactly the kind of gap that only shows up once you're relying on it for real work.

  • Per-hook-kind injection counters, visible in vectr status. Before this, a working memory system that was silently failing to inject anything and one that was injecting correctly looked identical from the outside — both just sat there. Now you can see, per hook kind, how many injections actually fired.
  • Gotcha injection extended to file reads, not just edits. A gotcha ("this file's config is regenerated, edit the source instead") used to fire only on Edit/Write. It now fires on Read too — the moment you open a file is often the moment you'd want the warning, not just the moment you're about to change it.
  • Task-note recency, newest-first. This one came from my own resume-workflow pain: task notes used to sort by a trust/decay score that could put an old checkpoint ahead of the one I'd just written. They now sort newest-first, full stop.
  • Explicit subagent attribution and the handoff pattern. vectr_remember(..., agent="coder-2") tags a note to the subagent that wrote it — never inferred, always explicit — and the orchestrator/subagent pattern this enables is durable handoff: a subagent finishes its investigation, writes its findings as notes, and the orchestrator recalls them instead of re-reading the subagent's entire transcript.
  • Watcher burst governor and an RSS self-limit, so a large or rapid burst of file changes (a branch switch, a big refactor landing) doesn't let the file watcher run away with CPU or memory.
  • --memory-only mode. Working memory and hooks, with no code index and no file watcher — for projects where you want the memory half without paying indexing cost at all.
08

The Quality Bar at Tag Time

2,275 tests passing, and the full suite runs independently before every push to main — not sampled, not "the tests relevant to the diff," the whole thing. That's the mechanical backstop underneath the dogfood gate: the gate catches the class of bug that only shows up in real usage of a real tool call sequence; the test suite catches everything else, every time, before anything merges.

The two do different jobs. A test asserts that a behavior you already know about stays fixed. The gate goes hunting for behavior nobody thought to assert yet — which is exactly why all three bugs here were caught by a human making a second identical call, not by a red test. Each one became a test the moment it was found. Gate discovers, suite pins.

09

What's Deliberately Not in 1.0

A few things are missing on purpose, not by oversight, and I'd rather say so than let the README's silence on them read as an accident.

Codex CLI support. The host matrix in the README marks it "Planned (post-v1)" — every other editor listed there is either verified or experimental, meaning the MCP config at minimum works. Codex CLI gets neither yet. It's next, not skipped.

A search-relevance floor I haven't shipped. The efficiency measurements above point at an obvious-looking fix: vectr_search returns five results by default with no minimum relevance score, so the low-scoring tail — chunks matching at 0.15 that have nothing to do with your question — gets returned and paid for anyway. Cutting that tail is the single biggest token lever the measurement pass found. It's also exactly the kind of change I refuse to rush: a score floor that trims noise on easy queries can silently drop the correct answer on hard ones, where the right symbol legitimately scores low. Shipping a ranking change on hope instead of evidence is exactly the kind of thing this whole post is arguing against, so it waits for a proper evidence gate against the full acceptance corpus.

PyPI. Install today is pip install git+https://github.com/swapnanil/vectr. A proper PyPI package is coming; it wasn't worth holding the tag for.

10

What's Next

The immediate queue is the three items above, roughly in that order: Codex CLI parity, then the relevance floor once it clears its own evidence gate, then packaging. Longer-term, the interesting open question is the one Part 3 ended on and I still don't have a better answer to: controlled benchmarks tell you the tool isn't broken, but they don't tell you whether it helps you, on your codebase, with your tasks.

The benchmarks and the honest numbers in this post are the evidence that the approach works in principle. Whether it's worth the five minutes of setup for your specific situation is something only running it will tell you — and now there's a tagged, gated, honestly-documented 1.0 to run.

Summary

A tag is a two-second command. What made this one a 1.0 was the week in front of it — the gate, the three bugs it flushed out, and the decision to print the numbers where vectr loses in the same breath as the numbers where it wins. If you take one thing from this post, take that last one: a launch that only shows you its good number is asking you to trust it on faith, and the whole point of a gate is to not do that.

  • v1.0.0 tagged 2026-07-08github.com/swapnanil/vectr. 14 MCP tools, local-only, no API key, MIT.
  • A first-person dogfood gate, not a design review, ran the release candidate through the real MCP surface before the tag. Two of three adoption families passed clean; eviction came back REWORK with two release-blocking defects, both fixed the same day with dedicated tests.
  • A third bug — a stale re-fetch key list in the escalated eviction banner — turned up in a separate efficiency measurement pass and was fixed pre-tag under the same rule: found while dogfooding, fixed before release, not logged for later.
  • The honest numbers shipped next to each other: 5.7× fewer tokens and 6× fewer calls on recall vs. re-derivation; −39% re-discovery overall on the CPython sprint with the 0% task shown, not hidden; and the reverse case — grep cheaper by ~60% and faster by two orders of magnitude on a single familiar-repo lookup — printed in the same README section, not a buried caveat.
  • 2,275 tests passing, full suite run independently before every push.
  • Deliberately absent from 1.0: Codex CLI parity, an evidence-gated search-relevance floor, and a PyPI package — each named as a gap, not smoothed over.

This is the finale of the vectr series. If you're arriving here first, the build starts with semantic code search and working memory, the evidence is in the benchmarks, and the delivery mechanism is in the hooks post and the four families of context relief.

↑ Back to top