Repository navigation
fix(#4985): statusline meter reads 100% where Claude Code auto-compacts, and the full window when auto-compact is off - #5252
Conversation
trek-e
left a comment
There was a problem hiding this comment.
Automated maintainer review of head 87d7efa7b7. Zero-tolerance policy: every finding at any severity must be resolved before approval.
Review PR #5252 — fix(#4985): statusline meter reads 100% where Claude Code auto-compacts, and the full window when auto-compact is off
Head 87d7efa (fresh gh pr view); mergeStateStatus BEHIND, mergeable MERGEABLE, not draft, reviewDecision REVIEW_REQUIRED.
CI: only non-success rollup entries are SKIPPED (auto-merge, inert CI, emitted-baseline publish); no failing checks.
Summary
Carries tanji-dg #4959 (#4844, 7 commits, authorship kept) plus 0xdhx #4985 fix: resolveAutoCompactThreshold / contextMeter / per-model autoCompactWindow merge in hooks/gsd-statusline.js; bridge moved to threshold scale (+used_tokens/threshold_tokens); monitor message quotes tokens. Root cause addressed at the right site (meter block + bridge writer). tanji-dg comment (data): consents to supersede; no effect on verdict.
Acceptance checklist
#4844 (confirmed-bug) AC:
- disabled => used == round(100-remaining) at mid and at 16.5 boundary: MET (isAutoCompactDisabled -> limit=model window; tests added).
- enabled/default unchanged incl. custom-window override: NOT MET by design (default now 52% not 60% at 50%/1M). PR body declares superseded; #4985 scope requires it.
- bridge stays raw: NOT MET by design (option (a)). "Out of scope" in #4844 triage lists the bridge raw contract. #4985 triage allows scaling the monitor thresholds. Option (a) was proposed by author; maintainer never replied (author said "no reply needed"). Judgment: maintainer must confirm.
#4985 (confirmed-bug) AC: - settings-only autoCompactWindow => 100% at real compaction point: MET (650k/1M, 616,927 -> 100%, test asserts threshold_tokens 617000).
- monitor can warn/critical before compaction at any window size: MET (bridge on threshold scale; end-to-end monitor test).
Out of scope respected: token-floor idea excluded.
Scope: two issues, one PR, at maintainer request (trek-e 2026-10-07 on #4959). OK vs RULESET.PR-SCOPE.
Findings
- MAJOR | deterministic | tests/gsd-statusline.test.cjs (new describe blocks, ~L'context meter resolves...') | RULESET.TESTS.property-based-testing: parseAutoCompactWindowEnv, canonicalModelKey, readAutoCompactWindowSetting merge and resolveAutoCompactThreshold (parser + budget/threshold) have no fast-check property. Fix: add property suite (e.g. threshold monotone in window, <= model window - 33k, used+remaining==100, canonicalModelKey idempotent and spelling-invariant, env parse never throws / result in [100k,1M] or null).
- MAJOR | deterministic | same tests | RULESET.TESTS.boundary-coverage / .fixtures: no limit-1/limit/limit+1 for window bounds. Existing cases use 50_000, 1_500_000, 5_000_000; none at 99_999/100_000/100_001 (setting + env) or 999_999/1_000_000/1_000_001 (setting + env); pct override not at 0/1/100/101; threshold>0 edge untested. Fix: add boundary table for isAutoCompactWindowValue and parseAutoCompactWindowEnv, and pct override edges.
- MINOR | judgment | hooks/gsd-statusline.js readSettingsFile / claudeSettingsPaths (project_dir/.claude/settings*.json): hostile-input rule (CONTRIBUTING.md "Security and prompt-injection surfaces", user-controlled files hostile). Settings are read with unbounded fs.readFileSync, no size cap, no lstat/symlink/non-regular-file rejection; a repo shipping .claude/settings.json as a FIFO or huge file stalls/bloats the per-turn statusline. Read is also now done for up to 4 files per render (extra reads vs prior single walk, still bounded in count). Partly inherited from #4959 but this PR now expands the surface (project_dir). Fix: lstat regular-file check + size cap (e.g. 1 MiB) -> treat as absent.
- MINOR | judgment | PR body/maintainer gate: option (a) bridge-scale change (reverses #2451 contract and #4844 AC3) has no explicit maintainer approval on #4985; surface in chat for confirmation before merge.
- NIT | deterministic | docs/context-monitor.md ~L37: one prose line far exceeds the wrapped width of neighbours ("lowered further when CLAUDE_AUTOCOMPACT_PCT_OVERRIDE sets an earlier point. With auto-compact off, it is the model\nwindow." ragged wrap). Reflow.
- NIT | judgment | PR template: "All existing tests pass" unchecked, Platforms Linux-only (Windows path handling for settings paths untested locally; CI matrix covers). Check or explain.
- NIT | deterministic | carried commit ee7ceb1 subject
fix(statusline): ...lacks(#4844)scope formtype(#issue):; carried unchanged by request; later commits conform. PR title conforms.
Not findings (verified): no source-grep tests (bridge read via JSON.parse of SUT-written tmp file; RULESET.TESTS.no-source-grep applies to source paths only), no wall-clock assertions (Date.now only in unique ids), try/finally only inside module-level helper runBoundaryHook.
Blast radius
codegraph index points at the main checkout (stale vs PR; contextMeter not found) so codegraph_impact was not usable for new symbols. By inspection: callers confined to runStatusline in hooks/gsd-statusline.js; bridge file reader is hooks/gsd-context-monitor.js only (tests/security.test.cjs, install-minimal-hooks.test.cjs, perf-317 reference the path). New module.exports: isAutoCompactDisabled, AUTO_COMPACT_DISABLE_ENV_KEYS, AUTO_COMPACT_ENV_TRUTHY, resolveAutoCompactThreshold, contextMeter (additive). Behavior change: bridge used_pct/remaining_percentage scale (consumers: monitor only; stale pre-upgrade bridge files lack token fields and are handled). Monitor thresholds 35/25 now mean runway to compaction. Risk: low-medium, 2 hook files.
Co-change gaps
- Changesets: two present, type Fixed, bold
—format OK, pr: 5252 backfilled. Fine. - docs: Fixed type => docs exempt; PR updated docs/context-monitor.md, FEATURES.md (REQ-HOOK-07 mirrored in docs/features/hook-system.md). Parity OK.
- docs/features/context-window-monitoring.md and docs/ARCHITECTURE.md only mention bridge path; no stale scale claims found. Translated docs (ja/zh/ko/pt) not updated: not required.
- No hooks/dist copies; no workflows/references added => INVENTORY/manifest N/A; no gsd-core/workflows growth => no Emitted-Drift-Ack-Growth trailer needed. CONTEXT.md: no new vocabulary/module.
- Closing keywords: "Fixes #4985 / Fixes #4844" present. Branch fix/4985-... conforms (/NNN-slug).
Behind-by / rebase
1 commit behind origin/next. git merge-tree --write-tree HEAD origin/next produced a clean tree (no conflicts). Fix: git fetch origin && git rebase origin/next, push. BEHIND is a CI condition.
Verdict
Changes requested (no Blockers; 2 Major test-rule violations, 2 Minor, 3 Nit). Substance of the fix is sound.
Memtrace Evidence
Memtrace quota was exhausted for this review run, so the graph-backed checks were NOT executed; this is stated rather than reported as passed:
get_impact: not run (quota). Blast radius above comes from codegraph impact/callers where present.get_symbol_context: not run (quota). Callers/callees were read via codegraph where present.recall_decision: not run (quota). The recorded-decision gate was NOT evaluated; this is not a finding that no decision governs the change.find_code_review_issues: not run (quota). Deterministic AST/YAML/cross-module pass did not execute; every finding above is judgment-sourced unless marked deterministic.
…led is false The context meter always subtracted the ~16.5% auto-compact buffer (or the CLAUDE_CODE_AUTO_COMPACT_WINDOW share) from `remaining` and showed used% against the "usable" range. With `"autoCompactEnabled": false` in settings.json that buffer is ordinary context: raw 40% read as 48%, raw 83.5% pinned the bar at 100%, and the last sixth of the window was invisible. isAutoCompactDisabled(dir) reads, fail-soft and first-definitive-wins, DISABLE_AUTOCOMPACT / CLAUDE_CODE_DISABLE_AUTO_COMPACT, then <project>/.claude/settings.local.json, settings.json, then (CLAUDE_CONFIG_DIR || ~/.claude)/settings.local.json, settings.json for the boolean `autoCompactEnabled`; when disabled the buffer is 0 and the bar shows round(100 - remaining). The bridge file's used_pct was already raw. Tests: autoCompactEnabled:false / env / precedence cases; the open-gsd#2219 and developer's real settings.json cannot steer the expected percentages.
…de actually has Review found both names in this PR were invented, which meant the gate could never fire for a real user — open-gsd#4844's symptom reached another way. - The env vars are `DISABLE_AUTO_COMPACT` and `DISABLE_COMPACT` (the latter switches off manual /compact as well). `DISABLE_AUTOCOMPACT` and `CLAUDE_CODE_DISABLE_AUTO_COMPACT` do not exist: 0 occurrences in the shipped 2.1.282 binary, absent from code.claude.com/docs/en/env-vars. Truthy is `1` / `true` / `yes` / `on`, trimmed and lowercased — Claude Code's own accepted set, which this PR's `/^(1|true|yes)$/i` was missing `on` from. Precedence is unchanged and matches the host's own resolution: either env var wins, and only then does `autoCompactEnabled` decide. - `context_window.total_tokens` is not a statusline payload field; the documented name is `context_window.context_window_size` (code.claude.com/docs/en/statusline). The meter read the absent field, so `totalCtx` always took its 1M fallback in a real session — and the fixtures supplied the invented name, so the dynamic-buffer assertions passed on a code path that could never run. Tests: every fixture now builds its `context_window` through `contextWindow()`, which rejects any field the host does not send, with a positive control proving the guard fires. The gate's unit test pins the env names and the truthy set against the host's own list, asserts the full truthy/falsy matrix for both names, and asserts the invented name is NOT honoured. Both constants are exported so the assertions name one source.
… the docs-guard detector Review nit: two consecutive `/**` lines before the constants block — an editing leftover, now one. CI: lint-docs-guard-registration flagged the test file because its detector reads any `docs/` sitting between two backticks anywhere in the file as a template literal, and the fixture helper's comment cited the statusline reference URL between two inline-code spans. Reworded rather than exempted: the file does not read a repo docs/ path, so an exemption marker would record a fact that is not true.
…eset and REQ-HOOK-07 The code and PR body were corrected in a02af98, but the release note and the feature docs still told users to export DISABLE_AUTOCOMPACT / CLAUDE_CODE_DISABLE_AUTO_COMPACT, neither of which Claude Code reads. Now DISABLE_AUTO_COMPACT / DISABLE_COMPACT; FEATURES.md regenerated.
…comment After rebasing onto next, the comment's own docs/ sat between two backticks again and lint-docs-guard-registration flagged the file.
…hy set; fix the perf-317 fixture's context_window field
…threshold The meter treated the model window (or the env-var window) as 100%, and never read the `autoCompactWindow` setting `/autocompact` saves. With `/autocompact 650k` on a 1M model it read 74% at the moment Claude Code compacted, and the context monitor, gating on the bridge's model-window `remaining_percentage`, could not warn first. - resolveAutoCompactThreshold(): the window from CLAUDE_CODE_AUTO_COMPACT_WINDOW, then settings `autoCompactWindow`, then the model window; capped at the model window; minus 20,000 + 13,000; lowered by CLAUDE_AUTOCOMPACT_PCT_OVERRIDE. Null when isAutoCompactDisabled() (open-gsd#4844) says compaction is off. - contextMeter(): used tokens from current_usage (the /context count) against that threshold, or the model window when compaction is off. Falls back to Claude Code's own percentage when the window size is absent. - The settings-file walk isAutoCompactDisabled() did inline is now readClaudeSetting(), shared by both lookups; project settings come from workspace.project_dir. - Bridge: remaining_percentage / used_pct on the meter's scale, plus used_tokens / threshold_tokens. The monitor's 35% / 25% are unchanged and now measure runway to compaction; its message quotes the token counts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DsvroGuHEwg1cUd6n13fHK
…to the threshold scale open-gsd#4959 switched this file's runStatuslineHook fixture from the invented context_window.total_tokens to context_window_size, so its open-gsd#2451 bridge tests now reach the open-gsd#4985 meter instead of the unscaled fallback, and they asserted the old model-window scale (used_pct 65 for remaining 35). Under the bridge-scale choice on open-gsd#4985 (option a), used_pct and remaining_percentage count down to the auto-compact threshold (650,000 of 967,000 → 67%), and the /context agreement open-gsd#2451 protected moves to the bridge's used_tokens, which must equal what /context reports as used. Same rewrite as the open-gsd#2451 test in gsd-statusline.test.cjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
…mpact saves Claude Code 2.1.292 saves /autocompact's window per model, under modelSettings.<canonical model name>.autoCompactWindow, not as the top-level autoCompactWindow older versions wrote. Reading only the top-level key left the meter on the model window for anyone who ran /autocompact on a current Claude Code — the open-gsd#4985 headline case. The window is now merged the way the binary's lFt()/bv() merge it: settings files lowest precedence first, each file's per-model entries layered over the lower files', a file's top-level autoCompactWindow replacing the default and dropping the lower files' per-model entries, and the session model's own entry winning over the default. The model key is the payload's model.id, canonicalized to match its dated, [1m], Bedrock and Vertex spellings. "auto" (Claude Code's tuned per-model window, not visible to a statusline) falls back to the model window, as does an invalid value or an unmatched model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
…changed when enabled" This PR carries open-gsd#4959's commits and also changes the enabled case (open-gsd#4985), so the fragment's closing "Unchanged when auto-compact is enabled" no longer holds. It now credits the contribution instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
… and the tests isolated Pre-filing review findings on the per-model window: - readClaudeSetting stops at the first settings file that answers again; the shared reader had made isAutoCompactDisabled parse all four files. - canonicalModelKey strips a Bedrock -v<n> revision only after a version or date segment, so anthropic.claude-v1 and claude-v2 no longer share the key "claude". - Tests pin that an invalid top-level autoCompactWindow merges (keeps the lower files' per-model entries) while a top-level "auto" resets, as the binary's lFt() does; both mutants are caught. - The perf-317 open-gsd#2451 helper pins CLAUDE_CONFIG_DIR to a scratch dir and clears the compaction env vars (the statusline helpers clear the pct override too), so a developer's own settings cannot move the threshold. - docs/context-monitor.md and the open-gsd#4985 changeset name the CLAUDE_AUTOCOMPACT_PCT_OVERRIDE lowering; the open-gsd#4844 changeset states the disabled-case meter as the share of the model window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
Without current_usage (before the first response) the statusline recovers the used tokens from Claude Code's percentage, so the count is an estimate rather than the one /context shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
CONTRIBUTING.md bans try/finally inside test bodies. The isAutoCompactDisabled precedence test now registers its cleanup with t.after(), and the perf-317 open-gsd#2451 helper cleans its scratch config dir after the try/catch instead of in a finally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
The open-gsd#4844 fragment was authored on open-gsd#4959, which this PR supersedes. It now carries this PR's number; the credit to @tanji-dg is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K
…le of at most 1 MiB The statusline reads the project's .claude/settings*.json on every render, and that directory is repository content. A FIFO there blocked the hook until it was killed, and a huge file was read whole each turn. readSettingsFile now opens the file non-blocking, checks it through its own descriptor, and treats anything that is not a regular file of at most 1 MiB as absent. A symlink is still followed: a settings file linked in from a dotfiles repo is ordinary, and lstat would have dropped it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…t-1, limit and limit+1 The autoCompactWindow setting (top-level and per model) and the CLAUDE_CODE_AUTO_COMPACT_WINDOW env are now tested at 99,999 / 100,000 / 100,001 and 999,999 / 1,000,000 / 1,000,001, on a 2M model window so an accepted 1M window reads differently from a rejected one. The pct override is tested at -1 / 0 / 1 / 100 / 101, and threshold > 0 at a pct that floors to 0 or 1 and at model windows of 32,999 / 33,000 / 33,001. At pct = 100 the override is always above the threshold it is min'd with, so its upper edge is not observable; the test says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…s own key One pass of the rules left some ids half-canonical: `x[1m][1m]` keyed as `x[1m]`, a date after a Bedrock revision left the revision, and a space before `[1m]` survived the trim. Keying the result again changed it, so a settings entry and a model id could each be canonical and still not match. The rules now repeat until nothing changes. Each one only removes characters, so the loop ends. A fast-check property asserts canonicalModelKey(canonicalModelKey(s)) === canonicalModelKey(s) over arbitrary strings and strings built from the pieces the rules strip; the single-pass version fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…eshold, meter and settings merge TESTING-STANDARDS.md asks parsing and budget/limit logic for a fast-check property. Eight new properties (seed and run count from tests/helpers/fast-check-setup.cjs): - parseAutoCompactWindowEnv never throws and returns null or an integer in 100k-1M; reads back any such integer, plain or digit-grouped; is monotone in a positive value, clamped into 100k-1M. - canonicalModelKey keys dated, [1m], Bedrock (with and without region and date) and Vertex spellings, in either case, as the model. - resolveAutoCompactThreshold is a positive integer at most min(window, model window) - 33,000, or none; never falls as the window or the model window grows; the pct override only ever lowers it. - contextMeter: used + remaining = 100, both integers in 0-100, measured against the threshold or the model window, monotone in usage. - The settings merge, over four generated settings files: the window is one a file set or the model window, and the highest file decides when it speaks - its own model entry under any spelling, else its top-level value, which drops the lower files' model entries. Each was checked against a mutant of the rule it covers: single-pass key, no [1m] strip, Bedrock :0 kept, a 19k reserve, a pct override that raises, an unclamped meter, top-level keeping lower model entries, reversed file order, a non-canonical spelling overriding the canonical one, and an env window not raised to 100k. Each mutant fails. parseAutoCompactWindowEnv and isAutoCompactWindowValue are exported for the properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…action point One line ran past the paragraph's wrap width and left a ragged one-word line after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…ck saw fce0e58 checked the size through the open descriptor and then read it with readFileSync(fd), which reads to EOF. A file that was 2 bytes at the check and grew past 1 MiB before the read was parsed whole, so the cap held only for a file nobody was writing. The read now stops one byte past the checked size, and a file whose length differs from that size is treated as absent - grown or shrunk. The test mocks fstatSync to report a size the file does not have; the readFileSync(fd) version fails its growth case, and a version that accepts a short read fails its shrink case. Found by the cross-AI review of this round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…ngs key 383215b made the key a fixpoint by re-running every rule until nothing changed, stripping one [1m] per pass. That is quadratic: a 524 KB project .claude/settings.json whose model key ends in 131,072 [1m] took 16.9 s to resolve, and the statusline hook was killed before writing its bridge. A key longer than 256 characters is now no model's and keys as '' - no model id is near that length - and a run of [1m] suffixes is stripped in one pass. Lowercasing moves before the loop, because it can lengthen a string (U+0130 lowercases to two characters); inside the loop every rule only removes characters, so the loop still ends. The test renders that file through the hook within its normal timeout; the pre-change key function stalls it. A second test pins the 255 / 256 / 257-character boundary. Found by the cross-AI review of this round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
…el, and the meter's percentage Two properties from cc125e5 passed on wrong code: - The settings-merge property checked only the highest file, then that the window was one some file set, and it decided which spelling belonged to which model with canonicalModelKey itself. A merge that dropped the lower files' entries, and a key function that filed a dated Opus id under Sonnet, both passed. It now compares the window with a reference model of the documented merge, written out in the test with its own spelling-to-model table and its own window check. - The meter property checked used + remaining = 100 and the limit, not the percentage: used = 0 passed. It now requires used to be within 0.5 of usedTokens / limit x 100, and 100 at or past the limit. Each of the four wrong versions now fails. Found by the cross-AI review of this round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF
87d7efa to
d50621d
Compare
|
Thanks for the review. All seven findings are addressed below; the branch is rebased onto Findings → commits
Rebase. 16 carried commits replayed onto One intermediate commit is red at its own tree. Reversion controls. Restoring the old pathname settings reader fails the FIFO and size tests; the earlier descriptor reader without the re-check, and a variant that accepts a short read, fail the grow/shrink test; six off-by-one mutants of the window, pct and threshold bounds each fail the boundary table; the pre- Tests at |
trek-e
left a comment
There was a problem hiding this comment.
Review: fix(#4985) statusline meter at the real auto-compact threshold
Summary
All seven findings from the 2026-10-09 review are resolved at head d50621d0be. CI at this exact SHA is green (newest run of every check is success, none pending, Required tests success).
Classification & gate compliance
Bug-fix track. #4985 and #4844 are confirmed-bug, both have Fixes, the changesets are Fixed with pr: 5252, docs and FEATURES are mirrored. No shipped workflow growth, so no emitted-drift trailer.
Root-cause verdict
The meter now resolves the real threshold. The bridge writes threshold-scale used_pct and remaining_percentage plus used_tokens and threshold_tokens.
Maintainer decisions recorded
- Option (a), the bridge scale change that reverses the #2451 raw-bridge contract and #4844 AC3, is approved. The additive-fields alternative is not needed.
- No history rewrite is needed for
a710ed88e, which is red at its own tree. The merge is a squash. - Following a symlinked settings file is accepted. Reading through the descriptor with a regular-file check and a 1 MiB cap meets the hostile-input goal, which was never to break dotfile-linked settings.
Blast radius
hooks/gsd-statusline.js and hooks/gsd-context-monitor.js plus their tests and docs. Blast radius was derived by diff reading and grep.
Co-change completeness
Docs, FEATURES, hook-system.md REQ-HOOK-07 and changesets are all in the diff.
Findings
None.
Verdict
Approved. The branch is behind next, so it will be brought up to date server-side, re-verified green, and merged.
Memtrace Evidence
The Memtrace MCP server was unavailable for this review (it failed to connect: memtrace not found in PATH). None of the four checks below ran, so none is a clean graph result.
get_impact: not run. Blast radius comes from the diff and grep.get_symbol_context: not run. Callers were checked by grep.recall_decision: not run. This does not show that no decision governs the change. The #2451 and #4844 contract was checked against the PR threads, and the maintainer ruled on it.find_code_review_issues: not run. A manual diff review by an isolated reviewer stood in for it, and CI (lint,Required tests) is green.
Fix PR
Linked Issue
Fixes #4985
Fixes #4844
Supersedes #4959, at a maintainer's request: trek-e, 2026-10-07 asked for one PR that supersedes #4959 and carries the statusline fix.
Credit. The first seven commits are @tanji-dg's #4959 (the #4844 fix), carried with their authorship and rebased onto
next. Their content is unchanged; the first one's subject now takes thefix(#4844):form. Their changeset is kept and credits the contribution. The 17 commits after them are the #4985 fix built on top, which reuses #4959'sisAutoCompactDisabledfor the disabled case, and the changes from the 2026-10-09 review.The review findings on #4959 are addressed in the carried commits. #4959's head
d21d3d1(pushed 2026-10-07 02:41Z) addressed all three findings from the 2026-10-07 01:23Z review:DISABLE_AUTO_COMPACTaccepted values. Read from the Claude Code 2.1.292 binary: the second operand ofBoolean(Le(DISABLE_COMPACT) || a.DISABLE_AUTO_COMPACT)is the env registry'sD.bool()field, which runs the same["1","true","yes","on"]helper. So both keys share one truthy set. The source is stated beside the constants and in the test.tests/perf-317-context-monitor-fs.test.cjs. It sent the inventedcontext_window.total_tokens; it now sendscontext_window_size.round(83.5)), not 83%.Two of #4844's acceptance criteria are superseded here, on purpose. #4844's AC2 (enabled behaviour unchanged) and AC3 (the bridge stays raw) were written with the enabled-case math out of scope. #4985 is that scope, and its fix changes both: the enabled case now measures against the real threshold, and the bridge moves to the threshold scale (option a). #4844's AC1 holds. With auto-compact disabled the meter shows the used share of the model window, which is
round(100 − remaining)with no buffer (raw 40 reads 40; at the old buffer boundary, raw 83.5 reads 84).Maintainer confirmation needed: the bridge scale. Option (a) reverses #2451's raw-bridge contract and #4844's AC3. @tanji-dg agreed to the supersede; that is not a maintainer's agreement to the scale change, and this PR should not merge without one.
What was broken
Two halves of the same meter block in
hooks/gsd-statusline.js:context_window.total_tokens, so the window was always the 1M fallback), ignored theautoCompactWindowsetting/autocompactsaves, and put 100% at the window itself instead of the threshold. With/autocompact 650kon a 1M model it read 74% at the moment of compaction, andgsd-context-monitor.jsstayed silent because the bridge'sremaining_percentagewas on the model-window scale (38% remaining when compaction fired).What this fix does
The meter measures usage against the token count at which Claude Code auto-compacts, resolved the way Claude Code resolves it, so it reaches 100% there (rounded: within half a percent of it). When compaction is off, the denominator is the model window. The bridge counts down to the same point, so the context monitor warns before compaction.
CLAUDE_CODE_AUTO_COMPACT_WINDOW(raised to 100k, capped at 1M; an unparseable or non-positive value is ignored), else theautoCompactWindowsetting, elsecontext_window_size. Always capped at the model window.window − min(maxOutput, 20000) − 13000, lowered tofloor(effective × pct/100)byCLAUDE_AUTOCOMPACT_PCT_OVERRIDE. A statusline cannot seemaxOutput, so the hook takes the 20,000 cap; on a model whose output limit is below 20,000, Claude Code compacts slightly later than the meter's 100%./contextshows the 33k as "Autocompact buffer". The hardcoded 16.5% this replaces is 33,000 / 200,000, so it was right only on a 200K window with nothing configured.current_usage.input_tokens + cache_creation_input_tokens + cache_read_input_tokens, the sum/contextreports, measured against the threshold. This avoids re-scalingremaining_percentage, which is already rounded against the model window and loses up to 5,000 tokens on a 1M model./autocompactnow saves the window undermodelSettings.<canonical model name>.autoCompactWindow, not the top-level key earlier versions wrote. Reading only the top-level key would leave this issue's headline case unfixed on a current Claude Code. The hook mirrors the binary's merge (lFt()/bv()):autoCompactWindowreplaces the default and drops the lower files' per-model entries.model.id, canonicalized across its dated,[1m], Bedrock and Vertex spellings. Canonicalizing is idempotent (a key is its own key), and a key longer than 256 characters once lowercased matches no model, which bounds the work a hostile settings key can cause."auto"falls back to the model window..claude/is repository content, and the statusline reads the settings files on renders that draw the meter (unless an env var already settles it). A file is read only when it is a regular file of at most 1 MiB. It is opened non-blocking and checked through its own descriptor, so a FIFO is skipped instead of blocking the render. The read stops one byte past the checked size, and a file that grew or shrank after the check is treated as absent. A symlink is followed, since a settings file linked in from a dotfiles repo is ordinary.remaining_percentage/used_pctare on the threshold scale, and the bridge gainsused_tokens/threshold_tokens. The monitor gates onremaining_percentage. On the raw scale its 35% / 25% sit at a different distance from compaction for every configured window, and with a window well below the model's (the 650k-on-1M case) compaction fires before the warning does. On the threshold scale they measure runway to compaction whatever the window. The/contextagreement fix(#2315): respect review.default_reviewers in bare convergence invocation #2451 protected moves to the token counts, which the monitor's message now quotes:CONTEXT WARNING: Usage at 65% (401k of 617k usable tokens). The monitor's 35% / 25% are unchanged; they now measure runway to compaction, per the triage scope. The token-floor idea from the issue stays out of this PR.Root cause
Testing
How I verified the fix
The three cases from #4985's repro script, run against this branch's
hooks/:autoCompactWindow: 650000in settings, 1M model, 616,927 usedCONTEXT CRITICAL: Usage at 100% (617k of 617k usable tokens)CLAUDE_CODE_AUTO_COMPACT_WINDOW=150000, 200K model, 100,000 usedCLAUDE_CODE_AUTO_COMPACT_WINDOW=650000, 1M model, 616,927 usedClaude Code 2.1.292's resolution, re-read from the shipped binary (minified names):
YV(e,n)is the threshold:e − 13000, ormin(floor(e × pct/100), e − 13000)under the pct override.U3is the effective window: window −min(maxOutputTokens, 20000).bvresolves the window in this order:byModel[settingsKey], else the default."auto"falls through.YV. The newereLe(a precompute buffer fraction) only feeds the earlier summary-precompute check.Suites at this head (
d50621d0b), run locally on Linux withHOMEsandboxed andCLAUDE_CONFIG_DIRcleared:tests/gsd-statusline.test.cjsandtests/perf-317-context-monitor-fs.test.cjs: 394/394npm run lint:ci), the targeted lane (926/926), the QA ratchet (162/162), and the three unit shards, whose first also runs the integration, security, install and slow suites (44,246 tests, 0 failed, 45 skipped, all platform- or install-gated and none in this PR's files)Regression test added?
The new #4985 tests fail on the pre-fix hooks:
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, and the per-modelmodelSettingsmerge.model.idin the payload. 617k (100%) is due. Onnext's hooks the meter reads 74%; before the per-model fix (13ccef5f8) the window resolved to 967k and the meter read 64%.From the 2026-10-09 review:
canonicalModelKey(idempotent, spelling-invariant), the threshold (never falls as the window grows, never above the window less the 33,000 buffer, only ever lowered by the pct override), the meter (used + remaining = 100, its percentage within 0.5 below the threshold and exactly 100 at or above it) and the settings merge, which is checked against an independent reference model rather than the production helpers.The #2451 assertions in
gsd-statusline.test.cjsandperf-317-context-monitor-fs.test.cjsare rewritten, not dropped. The bridge'sused_tokensmust equal what/contextreports as used, andused_pct + remaining_percentage = 100.One intermediate commit is red at its own tree.
a710ed88emoves the bridge to the threshold scale, and five ofperf-317's #2451 raw-bridge assertions fail there until the next commit,0af4c9fcc, moves them to the new scale. A squash merge makes this moot; I can restructure the history if you'd rather keep the commits.Platforms tested
Run locally on Linux only. Both changed test files are on CI's Windows (
platform-conformance-tier.generated.cjs) and macOS (macos-conformance-tier.generated.cjs) conformance lists, so those legs run them (the FIFO case skips on Windows); their result is CI's, not a local run.Runtimes tested
Checklist
Fixes #NNN— PR will be auto-closed if missingconfirmed-buglabelnpm test).changeset/fragment added if this is a user-facing fix (npm run changeset -- --type Fixed --pr <NNN> --body "...") — orno-changeloglabel appliedBreaking changes
None to an API. A reviewer will see these behaviour changes in the test diff:
used_pct/remaining_percentageare on the threshold scale (option a), and the bridge gainsused_tokens/threshold_tokens. The context monitor is the bridge's only runtime reader in this repo (two test files read it too).current_usagerecovers tokens from the percentage.context_window_sizefalls back to Claude Code's own percentage, unscaled.workspace.project_dir(Claude Code's launch directory), notcurrent_dir.isAutoCompactDisabled's settings walk moved into a shared reader. It now also skips a settings file that is not a regular file, is over 1 MiB, or changes size while it is read.Stated limits. These fall back to the model window because a statusline cannot see them:
--autocompactlaunch flag--settingsfiles"auto"resolves toautoCompactEnabledstored in the legacy global config (~/.claude.json), which 2.1.292 still reads as a fallback source; the disabled check reads settings files onlyDomain census. Settings files are read in this order, highest precedence first:
settings.local.jsonsettings.jsonsettings.local.jsonsettings.jsonThe window merge walks them in reverse. Not reached: managed-policy and
--settingsfiles.