fix(always-on): the turn-1 prefix includes what it READ from cache - #27
Merged
Conversation
computeAlwaysOn sampled `input + cacheWrite5m + cacheWrite1h` from the first main-chain turn. That is the prefix only when the session opened COLD. A session resuming in a project it has run in before finds the whole prefix already cached — it reads ~everything and writes ~nothing — so the observed standing context was smallest exactly where it is largest. Measured over 522 local sessions: turn 1 reads from cache in 97.9% of sessions median prefix, input+cacheWrite 40,272 median prefix, + cacheRead 59,645 (1.48x) 59,645 lands within 1.8% of the same quantity measured independently by differencing `claude -p` runs with one context component removed at a time (60,738). Two methods with no shared code agreeing to 1.8% is the evidence the term belongs. New test locks the warm-prefix case at 54,200 rather than 4,200. The existing spawn-economics fixtures all carry cache_read 0 on turn 1 and are unchanged, which is why nothing caught this. The same defect shipped in promptster-backend costEfficiency.ts:591 and is fixed there in pa-arth/promptster-backend#713. Note pricingDrift.test.ts fails on clean main (our table vs litellm) and is unrelated to this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR corrects the observed turn-1 standing-context measurement by including tokens read from cache, with a regression test for warm-cache sessions.
Confidence Score: 5/5The PR appears safe to merge, with the corrected calculation matching the documented empirical turn-1-prefix contract. The implementation adds the previously omitted cache-read component, and the new regression test covers the common warm-cache shape without revealing a reachable behavioral or security failure.
|
| Filename | Overview |
|---|---|
| src/alwaysOn.ts | Includes cache-read tokens in the empirical first-turn prefix, aligning warm and cold cache representations without changing downstream pricing structure. |
| src/tests/audit.test.ts | Adds focused regression coverage demonstrating that a warm-cache prefix includes input, cache-write, and cache-read tokens. |
| pnpm-lock.yaml | Adds deterministic pnpm dependency resolution; no changed-code-reachable security issue was established. |
Reviews (1): Last reviewed commit: "fix(always-on): the turn-1 prefix includ..." | Re-trigger Greptile
pa-arth
added a commit
that referenced
this pull request
Aug 14, 2026
) * feat(always-on): measure the injected prefix, don't census the disk The always-on breakdown answered "what did you put in your context" by walking files under ~/.claude. The context is not assembled from disk — Claude Code assembles it and records it, itemised, as typed `attachment` rows ahead of the first assistant turn, and none of those were read. Measured over 501 local sessions, `--json --since-days 30`: skill listing 967 tok -> 5,683 tok (skillCount 15 -> 78) hook output no field -> 948 tok auto-memory folded -> 2,990 tok (projectClaudeMd falls to match) fixedPrefix no field -> 49,623 tok (82% of the floor, not yours to cut) WHY THIS IS A CLASS, NOT FOUR BUGS. A disk census has no subscriber to a Claude Code release: built-in skills ship inside the binary and are never on disk, and the agent listing / deferred-tool delta move when the tool updates while nothing in ~/.claude does. So it cannot be wrong once and then fixed — it goes stale every release, silently. It was also NOT stale in one direction, which the plan assumed. It never read `settings.json > skillOverrides`, so six skills the user had switched off were sized and billed every turn; and `readdirSync().isDirectory()` is false for a symlinked directory, so four more were invisible. Net under, per-skill over. "Scale the floor up" would not have fixed it — the membership set was wrong, not just the magnitude. The listing's own `names[]` is now the authority, and `skillCarry[].loaded` says so. The disk census is retained and DEMOTED: it attributes a measured block to the user's files, it no longer decides how big the block is. Plugin and user-command listings appear INSIDE the injected skill listing, so they are slices of it rather than addends to it — the old formula added them twice the moment the listing became measured. Also: - `~/.claude/commands/**` is walked at all now. Six `opsx:*` commands load into every listing and nothing had ever counted them. - `mcpDeferred` is read from `deferred_tools_delta` in the sessions AS THEY RAN, not inferred from `ENABLE_TOOL_SEARCH` in the auditing process. - Unknown is `null` + a named reason, never 0. A session that recorded no attachments does not vote a 0 into any median, and does not vote on deferral either — that conflation was caught by a test mid-implementation. - A negative reconciliation remainder FAILS and is reported per session. Clamping is how this defect class hides: the number stays plausible while the model underneath is wrong. VERIFIED NOT TO MOVE, run against origin/main at the same instant: standingContextTokens (60,457 — already correct as of #27, and the 40,487 in the plan was a pre-#27 figure), pluginSkillTokens (a true zero: the walk runs, the enabled plugin ships no skills/ dir), spend.totalUsd, fluency.score. Fixtures are RECORDED from real transcripts by scripts/record-injected-fixture.mjs, never hand-authored — structure, field names, string lengths and usage numbers preserved, content replaced with same-length filler. A fixture written from the design doc would have passed against a broken parser, because the doc had the kind list and the per-kind content field wrong. All four deliberate mutations of this logic are caught by the tests written for them. Aggregate schemaVersion 9 -> 10. `skillDescriptionTokens` is REMOVED rather than kept beside `skillListingTokens`: a field that keeps its name while changing meaning lets a downstream comparison silently mix two quantities. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(alwaysOn): bound the unmeasured reasons — they now ride into the upload v10 put the first sentence-shaped strings into the uploaded aggregate. Everything before it was a model id or a hash, 25 chars at the longest, so the solo-capture endpoint's 200-char per-string ceiling had no plausible way to bite. These sentences are 82 and 118 chars — inside the bound, with the margin now moving every time someone rewords one. What a breach costs is why this is a test and not a comment: the server screens every string leaf and 400s the WHOLE capture on a breach, and sendCapture() ignores the response by design so telemetry can never break a local run. The failure mode is therefore the entire record silently not arriving, for every user, from the release that lengthened a sentence. Nothing downstream can detect it; the corpus just thins. Extracts the three literals into UNMEASURED_REASONS so they are enumerable, and bounds them. The second test checks the call sites actually use the set — a bound over a constant nobody references is worth nothing. Mutation-checked both ways: lengthening a reason past the ceiling, and writing an inline literal at a call site. MAX_UPLOADED_STRING_CHARS restates the server's constant rather than importing it; cc-audit does not depend on the backend, and a copied number with a test behind it beats a coupling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
pa-arth
added a commit
that referenced
this pull request
Aug 15, 2026
Four merged PRs since 0.7.0 (#27 #28 #29 #30), and two of them change numbers a user has already read, which is what makes this a release rather than a merge. MINOR, not patch: #28 changes what the always-on breakdown MEANS — measured off the transcript instead of censused off disk — and takes the aggregate schemaVersion 9 -> 10, removing `skillDescriptionTokens` rather than keeping a field whose name outlived its meaning. THE ONE TO SHIP FOR: 0.7.0 prices gpt-5.6-luna 5x over and terra 1.25x over on every Codex session it has audited since the vendor repriced (backend f4500a27, 2026-07-31, two days after 0.7.0 went out). That was fixed on main on 2026-08-14 and reaches nobody until this publishes — a merged pricing fix in an unreleased CLI is a fix that does not exist for users. Verified before tagging, not just built: - 414/414 tests, typecheck and lint clean - `npm run build:npm` -> 411.9kb single file, `--version` reports 0.8.0 (so the esbuild --define baked the bump, which is the thing a version bump can silently miss) - ran the BUNDLE against the real local corpus, not just `--help`: schemaVersion 10, standingContextTokens 60,963 — within 0.4% of the 60,738 measured independently by differencing `claude -p` runs — `skillDescriptionTokens` absent from the payload as #28 intended, `unmeasured` empty. Tag v0.8.0 after merge; .github/workflows/publish.yml guards tag == package.json and publishes + attaches the standalone binaries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
computeAlwaysOnsampledinput + cacheWrite5m + cacheWrite1hfrom the first main-chain turn (src/alwaysOn.ts:212-214). That is the prefix only when the session opened cold. A session resuming in a project it has run in before finds the whole prefix already cached — it reads ~everything and writes ~nothing — soobserved standing contextcame out smallest exactly where the standing context is largest.The measurement
522 local sessions, main-chain turn 1,
/subagents/files excluded:input + cacheWrite+ cacheRead59,645 lands within 1.8% of the same quantity measured independently by differencing
claude -p "ok" --output-format jsonruns with one context component removed at a time (60,738). Two methods sharing no code agreeing to 1.8% is what makes this a correction rather than a preference.Why no test caught it
Every existing turn-1 fixture in
audit.test.tscarriescache_read_input_tokens: 0— the spawn-economics block asserts 11,000 and 17,500 on exactly that shape. The suite covered the cold case thoroughly and the warm case not at all, which is the 97.9% case. New test locks a warm prefix at 54,200 and asserts explicitly it is not 4,200, so a future edit dropping the term again lands there.Related
The same defect shipped in the product:
promptster-backend/packages/engine/src/jobs/costEfficiency.ts:591had the identical formula, fixed in pa-arth/promptster-backend#713 (which also needs a rail term — its rows can be OpenAI, where cached tokens sit inside input; cc-audit is Claude-Code-only so it does not).Still open, not addressed here
report.tsprinted 31,365 for this number, where a faithful replication of the pre-fix formula over the same transcripts gives 40,272. So ~9k of the originally-observed gap is something else in the span/merge handling —mergeUsagefolds streamed rows by per-field max, and the main-chain span selection is stricter than my replication. Worth chasing separately; this PR does not explain it.Checks
src/__tests__/audit.test.ts50/50 · full suite 386 passed, 1 failed —pricingDrift.test.ts, which fails identically on cleanmain(our pricing table vs litellm) and is unrelated ·tsc --noEmitclean.🤖 Generated with Claude Code