claude-token-saver 3.8.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -263,6 +263,9 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
263
263
 
264
264
  ## Release notes
265
265
 
266
+ ### v3.8.1 (2026-07-31)
267
+ - **Fixed: 1M sessions were judged against a 200k window** — the briefing inferred the window from the largest request seen so far, so a 1M session counted as 200k until it had already grown past 250k. At 160k of input it announced "past 80% of the 200k window" — really 16%. The window now comes from the configured model id, and when `autoCompactWindow` is set that is where the session actually turns over, so the percentage is measured against it (the text says `(autoCompactWindow 기준)`). The observed-size heuristic remains only as the fallback for an unreadable model id.
268
+
266
269
  ### v3.8.0 (2026-07-31)
267
270
  - **New `compact-window` — 1M sessions had no compaction cap** — Claude Code compacts near `min(autoCompactWindow, model max context)`. On a 1M window with that value unset, compaction only fires around 800k, and every request until then re-bills the entire context. A 1M model with the value unset or above 400k now raises `🅷⚠ compact-window?` on the statusline plus a session briefing, and `compact-window set --global|--project` pins 400k. 200k sessions are exempt — the setting cannot change anything for them.
268
271
 
package/README.md CHANGED
@@ -220,6 +220,9 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
220
220
 
221
221
  ## 릴리스 노트
222
222
 
223
+ ### v3.8.1 (2026-07-31)
224
+ - **1M 세션을 200k 창으로 오판하던 브리핑 버그 수정** — 세션 창을 "지금까지 본 가장 큰 요청"으로 추정해서, 1M 세션이라도 25만 토큰을 넘기 전까지는 200k로 취급했습니다. 그래서 입력 160k에서 "200k 창의 80%를 넘었습니다" 경고가 떴습니다(실제로는 16%). 이제 설정된 모델 ID로 창을 판정하고, `autoCompactWindow`가 잡혀 있으면 그 값이 실제로 세션이 넘어가는 지점이므로 그쪽을 기준으로 %를 계산합니다(문구에도 `(autoCompactWindow 기준)` 표기). 모델 ID를 못 읽는 경우에만 기존 관측치 추정으로 되돌아갑니다.
225
+
223
226
  ### v3.8.0 (2026-07-31)
224
227
  - **`compact-window` 추가 — 1M 컨텍스트에서 자동 압축 지점이 방치되던 문제** — Claude Code는 `min(autoCompactWindow, 모델 최대 창)` 근처에서 압축합니다. 1M 창을 쓰면 이 값을 안 잡는 한 80만 토큰까지 커진 뒤에야 압축이 걸리고, 그전까지 모든 요청이 전체 컨텍스트를 재과금합니다. 이제 1M 모델인데 미설정이거나 40만 초과면 statusline `🅷⚠ compact-window?` + 세션 브리핑으로 알리고, `compact-window set --global|--project`로 40만을 고정합니다. 200k 컨텍스트는 설정이 영향을 주지 않으므로 경고 대상에서 제외합니다.
225
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
4
4
  "description": "Save tokens on Claude Code — spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/brief.js CHANGED
@@ -28,6 +28,7 @@
28
28
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
29
29
  import { join } from 'node:path';
30
30
  import { userDataDir } from './paths.js';
31
+ import { resolveModelId, isOneMillionModel, effectiveWindow } from './compact-window.js';
31
32
 
32
33
  // Context tiers as a fraction of the session's context window. Tier 1 warns
33
34
  // (compaction/cost territory ahead), tier 2 urges wrapping up. A session only
@@ -36,7 +37,9 @@ export const CTX_TIERS = [
36
37
  { tier: 1, pct: 0.8 },
37
38
  { tier: 2, pct: 0.95 },
38
39
  ];
39
- // Requests above this input size can only exist on a 1M window.
40
+ // Requests above this input size can only exist on a 1M window. Used as a
41
+ // fallback signal only — a 1M session that has not yet grown past 250k is
42
+ // still a 1M session, so the configured model decides first (see sessionCtx).
40
43
  const WINDOW_1M_MIN_INPUT = 250_000;
41
44
  const PRUNE_MS = 7 * 24 * 60 * 60 * 1000;
42
45
  const TAIL_BYTES = 256 * 1024;
@@ -67,11 +70,40 @@ function saveState(state, now) {
67
70
  writeFileSync(briefStatePath(), JSON.stringify(state) + '\n');
68
71
  }
69
72
 
73
+ /**
74
+ * The window a tier crossing should be measured against.
75
+ *
76
+ * Two corrections over "infer from the biggest request seen":
77
+ * - The configured model decides the ceiling. A 1M session that has not yet
78
+ * grown past 250k is still a 1M session; judging it against 200k fired the
79
+ * 80% warning at 160k, less than a fifth of the real window.
80
+ * - `autoCompactWindow` lowers that ceiling. Once compaction is pinned at
81
+ * 400k, 400k — not 1M — is where the session actually turns over, so that
82
+ * is the number a "you are at 80%" warning has to mean.
83
+ *
84
+ * `observedMax` stays as a floor: it proves a 1M window even when the model id
85
+ * is unreadable (env override, settings we do not resolve).
86
+ */
87
+ export function ctxWindowFor(observedMax = 0, root = process.cwd()) {
88
+ let window = observedMax > WINDOW_1M_MIN_INPUT ? 1_000_000 : 200_000;
89
+ let compactCapped = false;
90
+ try {
91
+ const { model } = resolveModelId(root);
92
+ if (isOneMillionModel(model)) window = 1_000_000;
93
+ const cap = effectiveWindow(root).value;
94
+ if (cap !== null && cap < window) {
95
+ window = cap;
96
+ compactCapped = true;
97
+ }
98
+ } catch { /* settings unreadable — the observed-size fallback still holds */ }
99
+ return { window, compactCapped };
100
+ }
101
+
70
102
  /**
71
103
  * Last request's input size for THIS session, from the transcript tail.
72
104
  * Reads at most TAIL_BYTES — prompt-submit hooks must stay fast.
73
105
  */
74
- export function sessionCtx(transcriptPath) {
106
+ export function sessionCtx(transcriptPath, { root = process.cwd() } = {}) {
75
107
  let size;
76
108
  try { size = statSync(transcriptPath).size; } catch { return null; }
77
109
  const start = Math.max(0, size - TAIL_BYTES);
@@ -97,8 +129,8 @@ export function sessionCtx(transcriptPath) {
97
129
  if (total > 0) { input = total; maxInput = Math.max(maxInput, total); }
98
130
  }
99
131
  if (input == null) return null;
100
- const window = maxInput > WINDOW_1M_MIN_INPUT ? 1_000_000 : 200_000;
101
- return { input, window, pct: input / window };
132
+ const { window, compactCapped } = ctxWindowFor(maxInput, root);
133
+ return { input, window, compactCapped, pct: input / window };
102
134
  }
103
135
 
104
136
  function ctxTierOf(pct) {
@@ -144,10 +176,14 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
144
176
  if (ctx) {
145
177
  const tier = ctxTierOf(ctx.pct);
146
178
  if (tier > (s.ctxTier || 0)) {
147
- const winLabel = ctx.window >= 1_000_000 ? '1M' : '200k';
179
+ // Name the window the percentage was actually computed against — when
180
+ // autoCompactWindow caps a 1M model at 400k, "1M 창의 80%" would be a
181
+ // number the user cannot reconcile with anything they configured.
182
+ const winLabel = ctx.window >= 1_000_000 ? '1M' : fmtK(ctx.window);
183
+ const capNote = ctx.compactCapped ? ' (autoCompactWindow 기준)' : '';
148
184
  items.push(tier === 2
149
- ? `이 세션의 컨텍스트가 ${winLabel} 창의 95%를 넘었습니다(직전 요청 입력 ${fmtK(ctx.input)}). 곧 자동 압축으로 맥락 손실이 생길 수 있으니, 진행 중인 작업을 일단락하고 새 세션을 시작하는 편이 좋습니다.`
150
- : `이 세션의 컨텍스트가 ${winLabel} 창의 80%를 넘었습니다(직전 요청 입력 ${fmtK(ctx.input)}). 이후 요청은 비용이 커지는 구간입니다 — 작업이 일단락되면 새 세션 시작을 권합니다.`);
185
+ ? `이 세션의 컨텍스트가 ${winLabel} 창${capNote}의 95%를 넘었습니다(직전 요청 입력 ${fmtK(ctx.input)}). 곧 자동 압축으로 맥락 손실이 생길 수 있으니, 진행 중인 작업을 일단락하고 새 세션을 시작하는 편이 좋습니다.`
186
+ : `이 세션의 컨텍스트가 ${winLabel} 창${capNote}의 80%를 넘었습니다(직전 요청 입력 ${fmtK(ctx.input)}). 이후 요청은 비용이 커지는 구간입니다 — 작업이 일단락되면 새 세션 시작을 권합니다.`);
151
187
  s.ctxTier = tier;
152
188
  }
153
189
  }