Skip to content

Releases

v4.24.0 — Lighter edit context, architecture map, review prompts

Choose a tag to compare

@Mibayy Mibayy released this 15 Oct 11:03

Ideas from two neighbouring tools, rebuilt on Token Savior's own index:
Ponytail (write the smallest
complete change) and graphify
(rationale comments and graph communities as first-class context).

  • get_edit_context is 29 % lighter. Measured on 40 random symbols of
    this repository: 209 644 -> 148 190 characters. The sibling list sent every
    function of the file (up to half of the response: 7 161 of 14 272 characters
    for a method of query_api.py) and its line numbers were all null, because
    the field is lines, not line. It now keeps the 12 nearest siblings, with
    their real line, and siblings_total when it cut.
  • why in get_edit_context. The comment block glued above a definition
    (Python #, JSDoc /** */, //) is returned as why: the intent the
    source alone does not state, such as a counter-intuitive rule nobody should
    "fix". Section banners are ignored; capped at 8 lines.
  • Architecture map in get_project_summary. Two lines, from the graph the
    index already holds and with no model call: the most depended-on symbols
    (hubs) and the largest graph communities, named by their hub and by their
    production directory. About 400 more characters, once per project.
  • Three MCP prompts: review (the change, including breakage outside the
    diff through get_change_impact and detect_breaking_changes), audit (the
    whole project, ranked must / should / nice to have) and lean (a task as the
    smallest complete change). Claude Code lists them as slash commands. Free
    until called; TS_PROMPTS=0 removes them.
  • Opt-in session instructions (TS_DISCIPLINE=1, 977 characters): the
    smallest complete change scoped by get_edit_context, a reuse ladder that
    checks the codebase first (search_codebase with semantic=true), deletion
    before addition, never cutting validation or security, one closing line on
    what was skipped. Off by default because the benchmark below found a cost
    and no gain.
  • Benchmark, old vs new server (tsbench, Sonnet, auto profile, real
    repository): 28/28 sessions succeeded in both arms. With the instructions on,
    cost per session was +3 % on editing (4 comparable sessions) and +4 % on
    reading (7), the price of ~100 more cached tokens per session; code written
    in editing fell 9 %, within noise. The first session of each arm was left
    out: the arm that ran first paid the prompt-cache warm-up alone (11 851
    cache-write tokens against ~2 200), which had produced a fake -18 %. These
    tasks take 2 to 4 turns and barely call get_edit_context, so they cannot
    show its 29 % reduction, measured directly instead.
  • Both SDK generations: tested with MCP 1.27 and 2.3 through a real stdio
    handshake. Options a SDK does not know are not passed.

v4.23.2 — The launch directory is the active project

Choose a tag to compare

@Mibayy Mibayy released this 08 Oct 07:10
  • Fix: a session could edit another project. Started in a project that was
    not the first WORKSPACE_ROOTS entry, the server registered the launch
    directory but kept the first root as the active project. Calls by symbol name
    or relative path then answered from that other tree, and
    replace_symbol_source wrote into it while reporting success. Found by the
    new editing benchmark, whose session in a copy of a repository rewrote the
    original. The launch directory now becomes the active project unless
    CLAUDE_PROJECT_ROOT / CLAUDE_PROJECT_DIR says otherwise; a marker-less
    folder under the home directory still restores its last active project first.
  • Editing benchmark on a real repository (3 tasks in files of 1 900 to
    3 700 lines, graded by running code and the module's tests, 4 passes).
    Every session of both arms succeeded. Cost is too noisy to call a gain
    (passes from -54 % to +16 %). Native Edit only needs a partial Read, so
    Token Savior has little to remove there.
  • edit_lines_in_symbol was tried in the auto profile and taken back out: on
    a rename it led the model to one call per function (23 turns, $0.21) instead
    of one Edit with replace_all (6 turns, $0.04). Its description is
    clearer and it now has its own tests; it stays callable via ts_execute.

v4.23.1 — Smaller session start, reachable tools

Choose a tag to compare

@Mibayy Mibayy released this 07 Oct 21:02
  • SessionStart memory index trimmed by 57 % (2 990 -> 1 295 characters on
    the author's base, 30 -> 12 observations). Only guardrails, warnings and
    decisions; observations younger than 14 days first; automatic traces
    (chmod, restart…) excluded; Continuity and Tool Capture on one short line.
    Tunable: TS_SESSION_INDEX_MAX, TS_SESSION_INDEX_TYPES,
    TS_SESSION_INDEX_RECENT_DAYS, TS_SESSION_INDEX_EXCLUDE, TS_SESSION_LINE_MAX.
  • move_symbol and checkpoint callable from ts_execute. The auto
    profile no longer lists them, and the sandbox allowlist did not have them
    either, so they had become unreachable. A test now checks that every tool
    dropped from the profile stays reachable through ts_execute.
  • Benchmark variance. Three passes of the v4.23.0 measurement: cost
    -45 % ± 7 (range -39 % to -53 %), new tokens -59 % ± 6, wall time +40 %,
    Token Savior used in 23 sessions of 24. On 3 editing tasks (one pass) both
    arms succeed 3/3 with no measurable gain yet.
  • Ops note: the index cache key includes EXCLUDE_EXTRA. Two processes sharing
    a project with different values (a daemon and a Claude Code session, say)
    invalidate each other's cache and rebuild the whole index on every switch.
    Give them the same value.

v4.23.0 — Beat Grep where the model reaches for it

Choose a tag to compare

@Mibayy Mibayy released this 07 Oct 20:08

Same benchmark as v4.22.0 (tsbench --reel, Haiku, 8 tasks, same evening),
three arms:

cost new tokens sessions using TS wall time
no Token Savior 0.234 $ 99 993 — 134 s
v4.22.0 auto 0.202 $ (-14 %) 75 470 5/8 195 s
v4.23.0 auto 0.129 $ (-45 %) 43 660 (-56 %) 8/8 210 s

One pass per task, variance not measured. Wall time is still higher than the
baseline: each benchmark session starts a fresh server that indexes the repo.

  • search_codebase names the symbol containing each hit (in). "Who
    calls X" is answered in one call; Grep cannot do that, and that is where
    the model kept choosing Grep.
  • read_lines names the symbol holding the first line when none covers
    the whole range. "Which function is line 145 in" took three reads: the
    default 60-line window spilled into the next function and no name came out.
  • Descriptions of search_codebase, find_symbol, get_function_source
    and read_lines rewritten
    around what they do better than Grep and Read
    (+37 tokens on the manifest).
  • ts gain and the dashboard stop inflating savings. The native cost of
    a call was a fraction of the whole project (15 % for a search, 30 % for
    get_full_context), hence "98.7 % saved". It is now the real alternative:
    the file holding the symbol, the whole file for structure tools, and as
    much as the answer for a search or a line range. Sessions carry their
    method (naive_method=2); only that method is summed, and the output
    recalls the end-to-end measurement.
  • Memory hooks: no more chmod/restart traces, nothing injected off
    topic.
    Over 7 days, 83 % of memories injected with each prompt were
    automatic traces ("chmod on …", "restart …"), 13.5 % shared a meaningful
    word with the prompt, and none was cited or recalled by the model.
    memory-posttooluse.sh no longer captures systemctl start|restart|… or
    chmod|chown; memory-userprompt.sh injects only non-trace memories that
    share a 5+ letter word with the prompt, and nothing otherwise.
  • Discipline guard matcher now sends get_function_source,
    get_class_source and ts_execute to the guard, so reading a symbol counts
    as context as v4.22.0 intended; a test keeps matcher and rule aligned.

v4.22.0 — What real usage said

Choose a tag to compare

@Mibayy Mibayy released this 07 Oct 15:47

Every change below answers a failure counted in real transcripts (765 sessions
over 30 days, then a second pass), not a hypothesis. Measured on this
project's own A/B benchmark (tsbench --reel, Haiku, same evening): with the
auto profile, 8/8 tasks solved like the baseline, cost -14 %, new tokens
-25 %, wall time +45 %. One pass per task, variance not measured.

Second pass on real usage (07/10/2026)

  • ts_execute results are capped at 60 000 characters
    (TS_EXECUTE_MAX_CHARS). A script that read four whole files returned
    57 545, which Claude Code refused and parked in a file: the script ran for
    nothing. The returned value is cut, logs and counters kept, and the cut is
    reported.
  • README: "alwaysLoad": true in the Claude Code snippet. Without it every
    tool sits behind ToolSearch and the model reaches for Read and Grep.
  • get_function_source / get_class_source return the source again.
    Without an explicit level, a Thompson-sampling bandit picked the detail
    level and scored every non-empty answer a success, a [L2] summary
    included; a re-request with an explicit level recorded nothing, so no
    failure was ever counted. It had drifted to 5 284 successes for [L2]
    against 34 for the full source: over 30 days, 61 of 95 calls without
    level returned a summary or a bare signature instead of the code asked
    for. Full source is now the default; the bandit only runs with
    TS_LEVEL_BANDIT=1.
  • The home directory is a container, never a project. A home that is
    itself a git repository (a VPS under version control) made every
    marker-less folder below it — /root/claude-chat, /root/gmgn-api… —
    resolve to the whole home: a 10 000-file index, capped, answering
    "not found". Such a path now belongs to its first-level subdirectory, and
    a session launched from home no longer registers home as its project.
  • The active project survives a server restart. Each Telegram message
    starts a fresh claude -p, hence a fresh MCP server whose active project
    fell back to the first configured root. The last project made active is
    now remembered per launch directory and restored when nothing more
    reliable (environment hint, launch directory that is a project) chose.
  • The over-exploration hint stops repeating. Past 15 navigation calls in
    a server's life (sub-agents included), ~340 identical characters were
    appended to every answer. It now shows at the threshold, then every 25
    calls.
  • Leaner auto manifest: 18 → 13 tools, ~4 160 → ~3 220 tokens.
    move_symbol (0 calls in 30 days), ts_search (1) and get_git_status
    (11) leave the always-on set; set_project_root and get_functions are
    never promoted (switch_project and get_structure_summary already do
    their job). All stay callable by name and from ts_execute. Hot slots
    10 → 8 (TS_AUTO_HOT_K).
  • test_vector_distance_floor skips when the vector stack is half
    installed (fastembed without sqlite-vec made vec_search_rows return
    [] silently, read as a recall defect).

What 30 days of real calls said (06/10/2026)

Audit of 765 transcripts (3 569 Token Savior calls) and of the discipline
guard's own log. Every change below answers a measured failure, counted in
the commit that fixes it.

  • .mjs and .cjs are indexed. Every get_functions miss and most
    read_lines misses were on ES modules (launchpad/pump.mjs,
    runtime/cycle-reconciler.mjs): the include patterns stopped at .js/.jsx.
    Imports between them resolve, editing works. Index cache version bumped to 4.
  • Constants have a context. find_symbol with default kinds now falls
    back to variables on a miss instead of answering retry_with; the
    TypeScript/JavaScript annotator records top-level const/let/var
    bindings (it recorded none); get_full_context and get_edit_context
    return the binding's source (new get_variable_source, brackets balanced)
    instead of "function 'X' not found". Half of the get_edit_context
    failures were export const tables.
  • read_lines reads files outside the index — absolute, or relative to
    the active root; regular text files under 5 MB, no binaries — and says so.
    17 of its 19 failures were files that existed on disk.
  • search_codebase(max_results=0) is cut at 60 000 characters with a
    _tronque note. Five such calls returned 107-192 k characters, which
    Claude Code refuses.
  • ts_execute accepts code (and body, js) for script. 8 of its 30
    failures. The two homonymous _ARG_ALIASES tables in server.py had
    diverged (schema read one, dispatch the other); merged into one.
  • "Found in project" no longer sticks to successful reads. The footer
    fired whenever the returned text contained "not found in index"; reading
    the handler that emits it was enough, and the footer ended up pasted into
    the source by a replace_symbol_source.
  • An empty .git directory is not a project, and the temp directory never
    is.
    A stray /tmp/.git (empty, 27/09) made /tmp a repository, so every
    scratch clone resolved to /tmp, which a nightly job indexed whole
    (25 MB cache).
  • MCP SDK 2.x and protocol 2026-07-28. Claude Code negotiates the
    2026-07-28 protocol on stdio by default since 2.1.292; a server that does
    not answer it pays a slow connect, then seven days remembered in the old
    mode. The 2.x SDK serves both eras in one loop but dropped the
    list_tools()/call_tool() decorators, so the server crashed at start
    under it. main() now builds the server with the 2.x callbacks when that
    SDK is installed and keeps the 1.x path otherwise; the pin is mcp>=1.25,<3.
    Checked over stdio: 2.x server answers 2026-07-28 to a modern client and
    2025-11-25 to a 1.x one.
  • Discipline guard, recalibrated on its own log (30 days):
    • native Edit/Write on indexed code is now advice, logged as
      conseil, never denied: 303 of 305 refusals were re-issued identically
      within five minutes;
    • native Read is refused only whole-file and over 200 lines
      (TS_GUARD_READ_FREE_LINES): 78 % of refused Reads were re-issued, and
      Claude Code's Edit requires a prior Read;
    • insert_near_symbol no longer needs the anchor's context (26 of 29
      refusals were on an anchor that is not modified);
    • get_function_source, get_class_source and ts_execute scripts
      calling the context tools count as context; a symbol edited once can be
      edited again;
    • a marker at / or in the temp directory no longer makes a project.

Reading by line number

An audit of 1 047 real code reads done in Bash rather than through this server
found that 93,2 % never tried a tool at all (only 1,4 % were repairs after a
failure), and that among the reads the index could have served, the dominant
shape was sed -n '105,150p' path.py. The caller held a line number — from a
traceback, a grep -n hit, a compiler error — and every read tool on the
surface asked for a symbol name.

  • read_lines(file_path, start, end). 1-indexed, inclusive, numbered by
    default (numbers=false to get raw text back). end is optional: with only
    a start, it returns a 60-line window, which is the shape a traceback
    gives you. Capped at 400 lines by default (max_lines) so a huge end
    can't turn back into cat, with a marker naming the start to resume from.
    Names the enclosing function or class after the excerpt, so a range that
    turns out to be the wrong unit routes to get_function_source instead of a
    second blind guess. The engine already had get_lines — tested, reachable
    from the Python API, exposed by no tool. Also whitelisted for ts_execute
    scripts, where it was equally missing.
  • TS_STICKY_ACTIVE now holds for unregistered directories. The freeze
    lived in server_state.noter_racine_active, at the dispatch layer, but
    SlotManager.resolve assigned active_root itself on one branch: the real
    path nobody had registered. That is exactly the /tmp clone, the export
    folder, the scratch file — 23,4 % of those same Bash reads. So the one
    guarantee meant to stop a parallel agent from repointing the shared default
    failed precisely where the need is most common, and the next hint-less call
    silently answered from the wrong repository. Default behaviour is unchanged
    (promotion still happens with the flag off).

One server, many worktrees, no stolen calls

Parallel agents in sibling worktrees shared one server and one mutable
active_root, and the worktree a session actually worked in was often never
even registered. Verified against the hosts, not their folklore: Claude Code
exports CLAUDE_PROJECT_DIR (stable — always the main checkout, never the
worktree); CLAUDE_PROJECT_ROOT is set by no host at all; Codex exports no
path variable and whitelist-filters the MCP server environment, so only the
spawn cwd carries its signal.

  • Per-call routing by absolute path. A call with no project hint but an
    absolute file_path/path/target_file now routes to the tree that owns
    the path — nearest marker root, so a worktree nested in a registered repo
    (repo/.claude/worktrees/wt) wins over the parent checkout that contains
    it. A linked worktree's .git file counts as a marker. The owning root
    registers on the fly, and the shared active_root is never touched.
  • Path hints resolve to their owner. project= hints that are paths go
    through the same nearest-root routing; a subdirectory of a registered
    project no longer gets registered as a project of its own. Removed a
    literally duplicated reverse-containment/register block in resolve().
  • Boot signals, validated and ranked. CLAUDE_PROJECT_ROOT (deliberate,
    ours: may register, always wins) → launch-directory root (the only signal
    that follows worktrees and the only one Codex has; when it is a linked
    worktree it takes active) → CLAUDE_PROJECT_DIR (automatic: promotes amon...
Read more

v4.21.0 — The server stops keeping what it knows to itself

Choose a tag to compare

@Mibayy Mibayy released this 28 Jul 13:21

v4.21.0 — The server stops keeping what it knows to itself (2026-07-28)

Three things the server knew and never told anyone.

It knew which tools only read, and said the opposite. MCP defaults
readOnlyHint to false and destructiveHint to true, so every one of the 69
tools reached the client looking potentially destructive — get_function_source
included. Any consumer wanting a safe subset had to hard-code its own list and
keep it in sync by hand; that list drifts silently the day a tool is added. The
classification now lives in tool_annotations.py, list_tools() ships the four
hints, and read_only_tool_names() is exported so a judge loop derives the safe
set from the server instead of copying it.

A classification is worth what its guard is worth.
test_no_unclassified_writer_slips_through greps for the absence of
classification: any tool whose name carries a write verb and is missing from
MUTATING_TOOLS fails CI, with an explicit derogation list for the three
readers whose names look like writers. Verified by mutation — dropping
replace_symbol_source from the set breaks three tests.

It knew it had cut a result, and stayed quiet. 41 tools accept a bound and
honour it. None said it truncated, so a response holding exactly max_results
items was indistinguishable from a complete one. A caller that counts those
items believes it measured a total and actually measured the bound — two counts
that both saturate the limit read as two equal values when they are two
truncations. The notice is added at the single point where a raw handler result
is wrapped.

It also separates two things the schemas conflate: bounds that govern a number
of elements, comparable to a list length, and bounds that govern a size in
bytes or lines, comparable to nothing. max_symbols_per_file sits with the size
bounds for a third reason — it caps symbols within each file, so comparing it
to the overall total would be wrong in both directions. Multiple bounds in one
call are all considered: get_change_impact carries max_direct and
max_transitive, and keeping only the first would miss the truncation governed
by the other. The coverage test found fourteen bounds a hand inventory had
missed.

This is deliberately not pagination. The handler has already cut by the time we
reach that seam, so total_count and next_offset are not knowable there. It
closes the "I don't know that I don't have everything" class and leaves the
per-handler work for later.

It paid for its own bookkeeping on every call. record_tool_call re-reads
and re-writes the counter file under an inter-process flock; paid synchronously
inside the dispatch path, that cost sat on every single tool call. The obvious
fix — one daemon thread per call — is wrong: a daemon thread is killed abruptly
at interpreter exit and can die mid read-modify-write, leaving truncated JSON.
A single worker now drains the queue and aggregates, so everything enqueued
while the previous write was in flight lands in the same locked write and the
cost stops scaling with the number of calls. atexit flushes what is still
queued — without it, short sessions would be systematically under-counted, which
is a bias, not just a loss. record_tool_call stays synchronous and durable for
its direct callers. Measured on an uncontended path: 0.489 ms per call down to
0.005 ms.

v4.20.0 — What the tools promise, now measured

Choose a tag to compare

@Mibayy Mibayy released this 27 Jul 01:29

Fourteen defects, found by exercising all 69 tools on a real machine rather
than by running the suite again — the suite was green at 2369 tests while
every one of them was live. They shipped as four hurried patch releases the
same night; this single release replaces them, and 4.19.1 through 4.19.3 are
yanked.

Two of them made the product unusable for its own stated purpose.

  • A search_codebase(semantic=True) held a write lock on the shared memory
    database for more than 25 minutes: the reindex runs inside the request,
    embedding is sequential by design, and the commit only came at the end of
    the loop. While it ran, nothing else could write, the WAL grew from 8 to
    15 MB, and a second client could not start at all — run_migrations()
    failed on database is locked. The multi-client story shipped in v4.11
    collapsed the moment a semantic search was running. A longer busy timeout
    fixed nothing, verified at 30 s. Committing per batch does.
  • detect_breaking_changes(ref=...) silently discarded the ref. The
    schema exposes ref, the CLI sends ref, the docs say ref="v1" — the
    handler only read since_ref, which nobody sends. HEAD~1, HEAD~3,
    HEAD~6 and a release tag all analysed HEAD~1. The project's own rule is
    "run it before a commit or PR": every check against a tag was comparing the
    last commit, and answering reassuringly.

Two silent data-integrity defects, both answering ok: true.

  • A project copied after indexing had its edits written into the original.
    cp -r carries .token-savior-cache.json, which holds the original's
    absolute paths; loaded from the copy it matched on the git ref, so
    replace_symbol_source(project=<copy>) modified the original file and
    left the copy untouched. A cache whose recorded root does not match where it
    was loaded from is now ignored.
  • replace_symbol_source deleted decorators. A symbol's indexed range
    starts at the first line of its block, decorators included, while
    get_function_source never shows them — so a caller who reads then replaces
    cannot restore them. The same defect ate a @pytest.fixture and a
    @pytest.mark.parametrize while this release was being written, breaking 73
    tests at once.

Four tools cost more than they saved. The claim this project rests on was
verified nowhere, so it was measured across 43 of the 69 tools:

  • get_function_source on a three-line function returned 131 characters where
    reading the whole file cost 76 — the gap was exactly the
    → get_full_context(...) hint, appended to every response regardless of
    size. Now proportionate: 54 characters.
  • get_edit_context shipped the source twice, in full under source and
    again as location.source_preview. 459 characters against 216 for the chain
    it replaces; now 375.
  • The cache acknowledgement was longer than the source it replaced on small
    symbols — 317 against 232 — and the code knew it, computing a saving of zero
    through a max(0, ...) without drawing the consequence.
  • capture_list returned 29 836 characters by default, twenty-four times
    the entire source of the audited project. Rows are bounded and the default
    limit is 20: 6 860 characters, down 77%.

And six smaller ones: ts_execute discarded every tool result already
served when a script timed out; capture_put with no arguments wrote an empty
capture attributed to "unknown"; capture_search without a query answered
{"count": 0}; a missing required argument returned a raw KeyError; an
abstract picked by the sampling bandit was cached as if it were the body; and
subprocess transports outlived their event loop, killing the process with
SIGSEGV at exit — 8 times out of 8 as soon as a second project was registered.

The discipline guard no longer blocks legitimate work. ls dist/index-*.js
splits into dist/index- and .js; that bare fragment resolved against the
current directory, walked up to the project root, and got the whole command
refused. A path must now have a stem and name a file that exists. The guard
still refuses native reads of indexed code — it is the noise that went, not
the rule. It remains opt-in via TS_DISCIPLINE_GUARD=1, so no install is
affected on upgrade.

Verification is now part of the suite. tests/maison/ holds 320 tests
that exercise all 69 tools the way an agent calls them, on a project built to
contain what the tools claim to find. A coverage test fails if any tool in
TOOL_SCHEMAS is never exercised, and an economy test fails if any tool
starts costing more than reading the file it answers about.

2728 tests, ruff clean, zero warnings.

v4.19.0 — A memory that always answers is not a memory

Choose a tag to compare

@Mibayy Mibayy released this 26 Jul 19:44

v4.19.0 — A memory that always answers is not a memory (2026-07-26)

Vector search had no distance floor. A k-NN always returns k results, however
far they are. Any query — including one with no relation to anything stored —
came back with the least-distant observations, presented as relevant memories
and injected into the model as if it had lived them.

Distances measured on French observations, as returned by sqlite-vec:

relevant neighbours      0.85 – 0.99
unrelated neighbours     0.97 – 1.07

The bands overlap almost entirely. Worse: redemarrer le serveur web ranked
Certificat SSL expire (0.928) ahead of Redemarrer nginx (0.989), and a
nonsense query scored better (0.973) than the correct answer to a legitimate
one. The vector layer only contributes when it is clearly confident; elsewhere
lexical search is the one that knows. The floor is a measured value, and a test
guards it against being relaxed without re-measuring.

Three more defects around the memory store, all found by trying to use it:

  • memory_delete returned True and memory_get still returned the
    observation. Deletion archives, and observation_get did not filter archived
    rows while observation_search did. A "deleted" memory could still reach the
    model. Now excluded by default, include_archived=True to audit or undelete.
  • capture_get(range="10-12") returned the entire capture instead of three
    lines. Only line:10-12 was recognised, and any unrecognised form silently
    meant "everything" — in a tool whose purpose is to save tokens. Natural ranges
    are accepted; an unknown one is refused with the accepted forms.
  • The memory database ignored TOKEN_SAVIOR_DATA_DIR and XDG_DATA_HOME, the
    convention every other data path follows. Moving your data left captures and
    state in the new place and memory in the old one, silently.

And the test suite was writing into the user's real database. 284 test
observations were found in it. conftest.py now redirects the memory DB for the
whole suite: a per-file guarantee does not hold, since one future test forgetting
to patch is enough.

v4.18.1 — Two clients on one repository, now covered by tests

Choose a tag to compare

@Mibayy Mibayy released this 26 Jul 19:12

v4.18.1 — Two clients on one repository, now covered by tests (2026-07-26)

A Claude Code session and a Codex session open on the same repository is not an
exotic setup, and it was never tested. Each client keeps its own in-memory
index: the moment one writes, the other's line ranges are stale, and its next
edit targets lines that have moved. That is precisely the corruption seen
during development.

The pre-edit reindex shipped in v4.15.2 already handles it. This release adds
the tests that prove it and stop it regressing: crossed edits from two
independent clients leave one definition, the neighbours above and below
untouched, and a file that still compiles.

No behaviour change — a verification that was missing.

v4.18.0 — Recall found 4 queries out of 6

Choose a tag to compare

@Mibayy Mibayy released this 26 Jul 19:08

v4.18.0 — Recall found 4 queries out of 6 (2026-07-26)

"Recall" is in the product name and its quality had never been measured, only
its absence of errors. First measurement: 4 out of 6 queries returned the
right observation.

SQLite FTS5 applies an implicit AND across the words of a bare query. A
natural-language sentence — exactly how an agent phrases things — therefore
required every word to appear in the observation, function words included.
supprimer des donnees en prod returned nothing against an observation
containing supprimer, donnees and prod, because it did not contain des.

Three passes now, narrowest first, each attempted only if the previous returned
nothing — so a query that worked before returns byte-identical results:

  1. the query as written (unchanged behaviour);
  2. an OR over the meaningful terms, function words dropped;
  3. prefix matching for word families — FTS5 does not stem French, so nommer
    never joined Nommage, nor branche its plural. Reserved to terms of four
    letters or more; below that a prefix returns half the database.

Malformed queries (stray punctuation, misplaced operators) return a list rather
than raising OperationalError at the caller.

Measured on the same set after the change: 8 out of 8, and a genuinely
absent query still returns nothing. Widening a search must not turn it into a
noise generator, so tests/test_memory_recall_quality.py pins both directions.