A Stable Prefix at 53 Percent Cache Hit
Reasonix's local HTTP tests separate message-prefix stability from cache ratios, session totals, and rewrite diagnostics.
A cache-hit percentage cannot tell you whether an agent changed its previous messages. In a local Reasonix replay, the second request preserved the full message prefix from the first request and still reported only 53 percent cached input. The fresh suffix accounted for the rest.
This distinction matters when debugging a long-running coding agent. If you react to every low ratio by changing compaction or prompt assembly, you may break a stable request path. Compare the bytes first, then inspect the denominator and the provider's usage fields.
I checked Reasonix revision 4d4700a39af7fcc695581e08930325246bbf0952, from its Go implementation. It contains an agent loop, provider adapters, session state, rewrite diagnostics, and local HTTP test fixtures. The engineering value here is a testable separation between request preservation and usage reporting. I did not reproduce a hosted cache rate or a billing claim.
Three Requests Through a Local Server
The project's cache replay tests start an httptest server and connect the actual agent through its OpenAI-compatible provider adapter. A read-only echo tool supplies two tool rounds. The mock emits streaming tool-call responses and then a final answer.
For each incoming request, the mock decodes messages into raw JSON entries. It compares entries from the start of the list until it finds a difference. It then totals the matching entries' byte lengths. Despite the helper's charsOf name, Go's len on those byte slices measures bytes.
The mock defines its own token accounting:
prompt = total message bytes / 4
hit = matching-prefix message bytes / 4
miss = prompt - hit
These are integer estimates created by the test server. They do not use a model tokenizer, cache blocks, expiry, or a remote cache service. They exclude other request fields such as tool definitions. The test can catch a changed old message; it cannot prove whole-request cache eligibility for a particular provider.
From the pinned checkout, I ran:
go test -count=1 -v ./internal/agent -run 'Test(CacheHit|CaptureShape|CompareShape)'
The module requests Go 1.26.0 and recommends toolchain 1.26.6. My installed Go 1.26.0 downloaded the recommended toolchain before running on Windows. Four selected tests passed. The three-request case produced these usage values:
| Request | Mock prompt tokens | Mock cached tokens | Mock new tokens | Displayed hit rate |
|---|---|---|---|---|
| 1 | 65 | 0 | 65 | 0% |
| 2 | 122 | 65 | 57 | 53% |
| 3 | 179 | 122 | 57 | 68% |
The test asserts that each request's matching-prefix byte count equals the entire previous request's message-byte count. Both transitions passed. The ratio stays below 100 percent because the agent adds new messages, even when every old message survives unchanged.
A separate 14-turn fixture reached a mock rate of 93 percent. Each turn added roughly 69 or 70 estimated tokens while the preserved history grew. That curve follows the fixture's arithmetic. It is not evidence that a production endpoint will reach the same ratio, or that keeping all history is an optimal latency policy.
The Last Turn and the Session Disagree
I ran a second focused group:
go test -count=1 -v ./internal/agent -run 'Test(RunPopulatesCacheDiagnostics|SessionAggregateCacheRate|SetSessionResetsSessionCache|TooSmallWindowReturnsCompactionRequired)'
All four selected tests passed. One printed:
after 8 turns: aggregate(session) = 78% vs single(last turn) = 88%
PASS
ok reasonix/internal/agent 0.366s
The aggregate includes earlier cold requests. The final-turn percentage uses only the last request. Neither value implies that the prefix changed. The session test also verifies that the cumulative hit and miss counters equal the sum of per-turn usage. Another test resets the session and checks that those counters return to zero.
That is a useful UI contract. A label such as cache 88% needs a scope: current request or whole session. Without it, a developer can compare two correct numbers and infer a regression that never happened.
Record the Reason for a Rewrite
Reasonix's prefix-shape code hashes the system prompt and a normalized list of tool schemas. It copies the schema slice before sorting by name, description, and parameter bytes. The normalization test confirms that reordering input tools preserves the diagnostic hashes without mutating the caller's slice.
This hash represents a normalized diagnostic view. It does not prove that every provider adapter serializes tools in that order. The local message-prefix test and the shape test answer different questions.
CompareShape() combines changes to the system hash, tool hash, and session-context digest with explicit content-rewrite reasons. A bare rewrite-version increment does not trigger a change report. Local bookkeeping can advance that version without modifying provider-visible text.
| Change under test | Expected diagnostic behavior |
|---|---|
| Reorder the same tool schemas | Keep normalized hashes stable |
| Advance rewrite version from 5 to 9 with no content reason | Do not flag a prefix change |
Supply the compact_auto content reason | Report that rewrite reason |
| Add a tool between two agent runs | Attach changed-tool diagnostics to usage |
The last row runs through usage-event integration, rather than calling the comparator alone. That distinction makes the test more useful: a correct helper would not help an operator if the runtime forgot to attach its result.
The small-window test also passed. With a 900-token configured window and a tool-heavy fixture, the agent returned ErrCompactionRequired. A consumer still has to handle that failure; passing this test does not establish that the agent can continue indefinitely.
These eight focused tests cover local transport, message preservation, diagnostic attribution, and counter scope. I did not run the full suite, a live provider test, a desktop session, or a cost benchmark. For a cache complaint, collect the same evidence these tests keep separate: old-message equality, the reason for any rewrite, and usage counters with a named scope.