Skip to content

v4.22.0 — What real usage said

Choose a tag to compare

@Mibayy Mibayy released this 07 Oct 15:47
· 11 commits to main since this release

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 among
    registered roots, never registers — pytest under Claude Code would adopt
    the developer's repo at import otherwise). A candidate counts only if it
    exists and carries a project marker. The launch directory now always gets
    a slot even when WORKSPACE_ROOTS pinned the registry.
  • TS_STICKY_ACTIVE=1 freezes active_root for multi-agent sessions:
    explicit hints and path arguments still route every call, but no call
    repoints the default the other agents fall back to.
  • Hooks and the standalone CLI now fall back CLAUDE_PROJECT_ROOT →
    CLAUDE_PROJECT_DIR → $PWD; the session-start memory hook previously
    read only the first, which no host sets, and ran with an empty project.