Release 0.5.5 — a cache you can warm, an outline before the read, half-length hashes - #34
Merged
Conversation
A summary is non-contiguous, so a reader told to re-read "specific sections" held no line number it could name. Each kept segment now opens with a `// L<start>-<end>` anchor whose range feeds straight into read(offset, limit).
extract_outline walks a file's definition lines and records where each one lives; render_outline prints them. Pure and I/O-free, so it stays inside the core layer.
A claim is only ever checked against the entry for the path it names, so 64 bits separates two versions of one file with room to spare. hash_matches accepts the wire form or the full digest and nothing between, so a truncated claim buys nothing.
shared_root finds the directory a set of paths agree on and relativize strips it, so a multi-file response spends the common prefix once.
A tool result was going out as both structured content and a text block, paying for the same JSON twice. Middleware now sends one representation; SCMCP_STRUCTURED_CONTENT and SCMCP_PUBLISH_OUTPUT_SCHEMA opt back in for clients that need the schema.
The stored preview was the file's first 200 characters — for source that is the module docstring and imports, which never says why the file matched. The preview now windows around the first query term.
warm indexes a glob so search and grep can see files before any read. read gains an outline mode and stops numbering ranged lines by default. Responses now spend a shared path root once, emit 16-character hashes, and refit to the token budget instead of being cut.
The 2s budget had to cover a macOS spawn plus a cold-cache import of server.tools, which alone measures 1135ms, leaving under a second of headroom on a shared runner. One test in this file already used 5s; the rest now match.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two costs were being paid on every turn regardless of what any tool did, and neither was visible in any per-call measurement. The advertised tool list sits in the prompt prefix of every request — output schemas were 11.5k of this server's 19.8k advertised tokens, paid whether or not a tool was ever called, and the Anthropic Messages API has no field to receive them. Separately, every tool result went out twice: once as a text block and once as
structuredContent, byte for byte identical, measured at 8.8k + 8.9k tokens for a single 584-line file. Both defaults now pick the cheaper shape, andSCMCP_PUBLISH_OUTPUT_SCHEMA/SCMCP_STRUCTURED_CONTENTopt back in for a client that actually consumes structured output. The Pydantic response models stay either way — they remain the declared contract, enforced bytests/test_response_contract.py.The other half of this release is about the read that comes before you know what to read. Search and grep only see files the cache holds, so making a tree searchable meant reading it — paying for content in order to find out which content you wanted.
warmbreaks that: it indexes paths or globs and returns counts only, never a byte. The opening move on an unfamiliar tree becomeswarm, thengreporsearch, thenreadonly what those name.No cache-format change and no migration. A client that ignores the new parameters behaves as it did in 0.5.4.
Added
warm— index files into the cache sogrepandsearchcan see them, returningwarmed,already_current,skippedandtokens_indexedand no content. Takes paths or globs. Every file left out is named with a reason (not_found,not_a_file,binary,too_large,unreadable,timeout), and a cap that stops the walk setstruncatedorincompleterather than a short count that reads as complete. Bounded by a per-file size limit, a total byte budget, and a deadline, so a careless glob cannot run away with the process.read(outline=true)— oneline: signatureper definition instead of the file's text. The cheap first read of a large file: a map of where things are, which is exactly why it reportsfile_hashand not a claimablecontent_hash. Backed byextract_outline/render_outlineincore.text, pure and I/O-free like the rest of that layer.// L<start>-<end>, a 1-based inclusive range that feeds straight intoread(offset, limit). About 6 tokens per segment, charged against the existing marker reserve.Changed
rootand reports paths relative to it, instead of repeating the common prefix on every entry.grephits are"<line>:<text>"strings grouped by file, context lines"<line>-<text>", with overlapping context windows merged so no line is sent twice — 37% fewer tokens than the per-match objects they replace, andglob50%.output="count"turns a 2.6k-token answer into 77.linesregardless;line_numbers=truerestores it.searchpreviews centre on the matching term. The stored preview was the file's first 200 characters — for source, the module docstring and its imports, which never says why the file ranked and is text the follow-upreadreturns anyway.Fixed
batch_readcould return files with their content silently removed. The budgeted refit assumed afileslist was grep-shaped;batch_read's entries carrycontent/statusand nolines, so every entry was rewritten to bare{"path": ...}and the function returned before settingtruncated— handing back a list of paths that the caller had every reason to read as the files it asked for. Covered bytests/test_server_tools.py::TestMinimalPayload::test_batch_read_files_are_not_silently_emptied.read's tool description still promised numbered lines on a ranged read after the default changed — a caller trusting it would misread every line position in an unnumbered window.warmoverwrote one hint with another when both themax_filescap and the byte/time budget fired, losing themax_filesguidance.grep(output="count")rendered every file's lines and walked the character budget for afilesarray it never sends, and could report truncation of a list the caller did not receive.SCMCP_STRUCTURED_CONTENTvalue logged its warning twice.warm's docstring promised every skipped file appears underfailures; the list is capped at 20.docs/architecture.mddiagrams, and the tool count there was still 13.Verification
uv run pytest -q→ 5118 passed, 6 skippeduv run ruff check src/ tests/→ All checks passeduv run ruff format --check src/ tests/→ 101 files already formatteduv run mypy src/→ no issues in 45 source files