peaks-loop 4.0.27 → 4.0.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.28 — 2026-09-03 (codegraph 目录回根 + env-first 1M 窗口识别)
4
+
5
+ **3 atomic commits from session 2026-09-03-session-d49394** (user feedback batch):
6
+
7
+ - `c0721d3a` fix(codegraph): revert data dir to root `.codegraph/` — drop `.peaks/.codegraph` handling
8
+ - `7011e64c` fix(context): env-first model window resolution — `[1M]`-suffixed model → 1M context
9
+ - `5289a9b3` docs(memory): sediment codegraph root revert + env-first 1M window fix
10
+
11
+ **Highlights**:
12
+
13
+ 1. **codegraph 数据目录完全回根** — rid-CG-003(4.0.20)引入的 `.peaks/.codegraph/` preferred 位置被移除,数据目录回到根 `.codegraph/`(上游默认语义)。删除 `PREFERRED_CODEGRAPH_DIR` + preferred/legacy/fresh-preferred 三态 union;`resolveCodegraphProjectRoot` 纯路径数学返回 root;doctor messaging / `.gitignore` / 3 测试同步。用户明确"完全回根,去掉 .peaks 路径"。
14
+
15
+ 2. **env-first 模型上下文窗口识别(1M vs 200K)** — 之前只从 transcript `message.model` 读模型 id,而 Claude Code / deepseek 等的 transcript 记录**裸模型名**(`deepseek-v4-flash`,不带 `[1M]`),导致 1M 上下文模型被误判为 200K 并在 ~50% 处 false soft-warn。新增 `resolveClaudeModelFromEnv` 优先读 env 模型标记(`ANTHROPIC_MODEL` → `ANTHROPIC_DEFAULT_OPUS/SONNET/HAIKU/FABLE_MODEL` → `CLAUDE_CODE_SUBAGENT_MODEL`,`[1M]`/`[1m]` 后缀即 1M),transcript 兜底。实测修复后 `context-now` 从 `capacityTokens: 200000, ratio 0.88` 变为 `capacityTokens: 1000000, ratio 0.19`。
16
+
3
17
  ## 4.0.27 — 2026-09-01 (auto-compact context probe fix)
4
18
 
5
19
  **1 atomic commit from session 2026-09-01-session-fdd7aa**:
@@ -41,17 +41,13 @@ export type CodegraphProcessRunner = (invocation: CodegraphInvocation) => Promis
41
41
  export declare function createCodegraphInvocation(options: CodegraphInvocationOptions): CodegraphInvocation;
42
42
  export declare function executeCodegraphInvocation(invocation: CodegraphInvocation, runner?: CodegraphProcessRunner): Promise<CodegraphExecutionResult>;
43
43
  /**
44
- * Slice rid-CG-006 — root `.codegraph/` (legacy). peaks-loop still
45
- * reads from this location for back-compat with downstream consumers
46
- * whose projects pre-date the slice rid-CG-003 preferred-path move.
44
+ * Single managed codegraph data directory name, relative to the
45
+ * project root. peaks-loop always reads/writes
46
+ * `<projectRoot>/.codegraph/` and spawns the upstream binary with
47
+ * `cwd = <projectRoot>` so its default discovery lands on the same
48
+ * directory.
47
49
  */
48
50
  export declare const CODEGRAPH_DIR_NAME = ".codegraph";
49
- /**
50
- * Slice rid-CG-003 — preferred managed location. Going forward
51
- * peaks-loop writes here; legacy root `.codegraph/` is only used
52
- * when it pre-exists AND `.peaks/.codegraph/` does not.
53
- */
54
- export declare const PREFERRED_CODEGRAPH_DIR = ".peaks/.codegraph";
55
51
  /**
56
52
  * Marker file peaks-loop writes inside the resolved codegraph dir
57
53
  * after a successful upstream init. Its presence distinguishes
@@ -61,37 +57,18 @@ export declare const PREFERRED_CODEGRAPH_DIR = ".peaks/.codegraph";
61
57
  export declare const CODEGRAPH_MARKER_NAME = ".peaks-loop-marker";
62
58
  export declare const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
63
59
  export type ResolvedCodegraphLocation = {
64
- readonly source: 'preferred';
65
- readonly cwd: string;
66
- readonly codegraphDir: string;
67
- } | {
68
- readonly source: 'legacy';
69
- readonly cwd: string;
70
- readonly codegraphDir: string;
71
- } | {
72
- readonly source: 'fresh-preferred';
60
+ readonly source: 'root';
61
+ /** Absolute path the upstream binary should be spawned with. */
73
62
  readonly cwd: string;
63
+ /** Absolute path to the data directory codegraph reads/writes. */
74
64
  readonly codegraphDir: string;
75
65
  };
76
66
  /**
77
- * Slice rid-CG-003 — pure resolver that returns the cwd the
78
- * upstream codegraph binary should be spawned with and the
79
- * absolute path to the data directory it should read/write.
80
- *
81
- * Precedence:
82
- * 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
83
- * (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
84
- * 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
85
- * (cwd = `<projectRoot>`, codegraphDir = legacy path)
86
- * 3. neither exists → default to the preferred path so the next
87
- * `peaks codegraph init` lands in `.peaks/.codegraph/`
88
- * (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
89
- * `source: 'fresh-preferred'`)
90
- *
91
- * The cwd is the directory the upstream binary treats as "project
92
- * root" — the binary's default codegraph discovery reads
93
- * `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
94
- * resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
67
+ * Root-only resolver: peaks-loop always manages
68
+ * `<projectRoot>/.codegraph/`. Returns the cwd the upstream codegraph
69
+ * binary should be spawned with (= project root, so the binary's
70
+ * default discovery reads `<projectRoot>/.codegraph/`) and the
71
+ * absolute data-dir path. Pure path computation; no fs IO.
95
72
  */
96
73
  export declare function resolveCodegraphProjectRoot(projectRoot: string): ResolvedCodegraphLocation;
97
74
  export type CodegraphInitGuardResult = {
@@ -112,12 +89,9 @@ export declare class CodegraphInitConflictError extends Error {
112
89
  }
113
90
  export type CodegraphInitGuard = (projectRoot: string) => CodegraphInitGuardResult;
114
91
  /**
115
- * Slice rid-CG-003 — preferred-path-first init guard.
116
- *
117
- * Looks at `.peaks/.codegraph/` first; falls back to `.codegraph/`
118
- * when the preferred path is absent. The 'fresh' case returns the
119
- * preferred path so the next `peaks codegraph init` lands inside
120
- * `.peaks/` instead of polluting the project root.
92
+ * Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
93
+ * is returned when the directory does not exist yet, so the next
94
+ * `peaks codegraph init` creates the root `.codegraph/` directory.
121
95
  */
122
96
  export declare function defaultCodegraphInitGuard(projectRoot: string): CodegraphInitGuardResult;
123
97
  /**
@@ -143,10 +143,8 @@ function buildCommandArgs(options, projectRoot) {
143
143
  export function createCodegraphInvocation(options) {
144
144
  assertSupportedSubcommand(options.subcommand);
145
145
  const projectRoot = resolveProjectRoot(options.project);
146
- // Slice rid-CG-003 — spawn the upstream binary inside the
147
- // resolved codegraph dir's PARENT so its default `.codegraph/`
148
- // discovery lands on `.peaks/.codegraph/` (preferred) or
149
- // `.codegraph/` (legacy fallback).
146
+ // Spawn the upstream binary with the project root as cwd so its
147
+ // default `.codegraph/` discovery reads `<projectRoot>/.codegraph/`.
150
148
  const location = resolveCodegraphProjectRoot(projectRoot);
151
149
  assertSupportedOptions(options);
152
150
  assertRequiredOptions(options);
@@ -165,21 +163,16 @@ export async function executeCodegraphInvocation(invocation, runner = defaultCod
165
163
  return runner(invocation);
166
164
  }
167
165
  /* ──────────────────────────────────────────────────────────────────────
168
- * Slice rid-CG-003 — preferred `.peaks/.codegraph/` lookup + legacy
169
- * root `.codegraph/` fallback (spike follow-up #1).
166
+ * codegraph data-directory resolution — root `.codegraph/` only.
170
167
  * ────────────────────────────────────────────────────────────────────── */
171
168
  /**
172
- * Slice rid-CG-006 — root `.codegraph/` (legacy). peaks-loop still
173
- * reads from this location for back-compat with downstream consumers
174
- * whose projects pre-date the slice rid-CG-003 preferred-path move.
169
+ * Single managed codegraph data directory name, relative to the
170
+ * project root. peaks-loop always reads/writes
171
+ * `<projectRoot>/.codegraph/` and spawns the upstream binary with
172
+ * `cwd = <projectRoot>` so its default discovery lands on the same
173
+ * directory.
175
174
  */
176
175
  export const CODEGRAPH_DIR_NAME = '.codegraph';
177
- /**
178
- * Slice rid-CG-003 — preferred managed location. Going forward
179
- * peaks-loop writes here; legacy root `.codegraph/` is only used
180
- * when it pre-exists AND `.peaks/.codegraph/` does not.
181
- */
182
- export const PREFERRED_CODEGRAPH_DIR = '.peaks/.codegraph';
183
176
  /**
184
177
  * Marker file peaks-loop writes inside the resolved codegraph dir
185
178
  * after a successful upstream init. Its presence distinguishes
@@ -189,46 +182,17 @@ export const PREFERRED_CODEGRAPH_DIR = '.peaks/.codegraph';
189
182
  export const CODEGRAPH_MARKER_NAME = '.peaks-loop-marker';
190
183
  export const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
191
184
  /**
192
- * Slice rid-CG-003 — pure resolver that returns the cwd the
193
- * upstream codegraph binary should be spawned with and the
194
- * absolute path to the data directory it should read/write.
195
- *
196
- * Precedence:
197
- * 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
198
- * (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
199
- * 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
200
- * (cwd = `<projectRoot>`, codegraphDir = legacy path)
201
- * 3. neither exists → default to the preferred path so the next
202
- * `peaks codegraph init` lands in `.peaks/.codegraph/`
203
- * (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
204
- * `source: 'fresh-preferred'`)
205
- *
206
- * The cwd is the directory the upstream binary treats as "project
207
- * root" — the binary's default codegraph discovery reads
208
- * `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
209
- * resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
185
+ * Root-only resolver: peaks-loop always manages
186
+ * `<projectRoot>/.codegraph/`. Returns the cwd the upstream codegraph
187
+ * binary should be spawned with (= project root, so the binary's
188
+ * default discovery reads `<projectRoot>/.codegraph/`) and the
189
+ * absolute data-dir path. Pure path computation; no fs IO.
210
190
  */
211
191
  export function resolveCodegraphProjectRoot(projectRoot) {
212
- const preferredDir = join(projectRoot, PREFERRED_CODEGRAPH_DIR);
213
- const legacyDir = join(projectRoot, CODEGRAPH_DIR_NAME);
214
- if (existsSync(preferredDir)) {
215
- return {
216
- source: 'preferred',
217
- cwd: join(projectRoot, '.peaks'),
218
- codegraphDir: preferredDir
219
- };
220
- }
221
- if (existsSync(legacyDir)) {
222
- return {
223
- source: 'legacy',
224
- cwd: projectRoot,
225
- codegraphDir: legacyDir
226
- };
227
- }
228
192
  return {
229
- source: 'fresh-preferred',
230
- cwd: join(projectRoot, '.peaks'),
231
- codegraphDir: preferredDir
193
+ source: 'root',
194
+ cwd: projectRoot,
195
+ codegraphDir: join(projectRoot, CODEGRAPH_DIR_NAME)
232
196
  };
233
197
  }
234
198
  export class CodegraphInitConflictError extends Error {
@@ -264,23 +228,16 @@ function inspectCandidateCodegraphDir(codegraphDir) {
264
228
  return { status: 'conflict-foreign-schema', codegraphDir };
265
229
  }
266
230
  /**
267
- * Slice rid-CG-003 — preferred-path-first init guard.
268
- *
269
- * Looks at `.peaks/.codegraph/` first; falls back to `.codegraph/`
270
- * when the preferred path is absent. The 'fresh' case returns the
271
- * preferred path so the next `peaks codegraph init` lands inside
272
- * `.peaks/` instead of polluting the project root.
231
+ * Root-only init guard: probes `<projectRoot>/.codegraph/`. 'fresh'
232
+ * is returned when the directory does not exist yet, so the next
233
+ * `peaks codegraph init` creates the root `.codegraph/` directory.
273
234
  */
274
235
  export function defaultCodegraphInitGuard(projectRoot) {
275
- const preferredDir = join(projectRoot, PREFERRED_CODEGRAPH_DIR);
276
- const legacyDir = join(projectRoot, CODEGRAPH_DIR_NAME);
277
- if (existsSync(preferredDir)) {
278
- return inspectCandidateCodegraphDir(preferredDir);
279
- }
280
- if (existsSync(legacyDir)) {
281
- return inspectCandidateCodegraphDir(legacyDir);
236
+ const codegraphDir = join(projectRoot, CODEGRAPH_DIR_NAME);
237
+ if (!existsSync(codegraphDir)) {
238
+ return { status: 'fresh', codegraphDir };
282
239
  }
283
- return { status: 'fresh', codegraphDir: preferredDir };
240
+ return inspectCandidateCodegraphDir(codegraphDir);
284
241
  }
285
242
  /**
286
243
  * Pure-fs helper that stamps the peaks-loop marker AFTER a
@@ -6,12 +6,11 @@
6
6
  * Fails when the version drifts, when the binary is missing, or
7
7
  * when the package is not resolvable at all.
8
8
  *
9
- * Slice rid-CG-003 (spike follow-up #3): also detects which managed
10
- * codegraph directory is in use — `.peaks/.codegraph/` (preferred)
11
- * or root `.codegraph/` (legacy) — via the injected `managedPathProbe`.
9
+ * The check also reports the managed codegraph data directory in use —
10
+ * always the root `.codegraph/` — via the injected `managedPathProbe`.
12
11
  * The CG-007 yarn-pnp fallback is preserved as the package-resolution
13
- * default; the managed-path probe defaults to the same fs-walk-style
14
- * helper and is independently injectable for tests.
12
+ * default; the managed-path probe defaults to the same root-only
13
+ * resolver and is independently injectable for tests.
15
14
  *
16
15
  * The probe is injected so tests do not depend on the real
17
16
  * `node_modules` resolution; the default probe uses
@@ -6,12 +6,11 @@
6
6
  * Fails when the version drifts, when the binary is missing, or
7
7
  * when the package is not resolvable at all.
8
8
  *
9
- * Slice rid-CG-003 (spike follow-up #3): also detects which managed
10
- * codegraph directory is in use — `.peaks/.codegraph/` (preferred)
11
- * or root `.codegraph/` (legacy) — via the injected `managedPathProbe`.
9
+ * The check also reports the managed codegraph data directory in use —
10
+ * always the root `.codegraph/` — via the injected `managedPathProbe`.
12
11
  * The CG-007 yarn-pnp fallback is preserved as the package-resolution
13
- * default; the managed-path probe defaults to the same fs-walk-style
14
- * helper and is independently injectable for tests.
12
+ * default; the managed-path probe defaults to the same root-only
13
+ * resolver and is independently injectable for tests.
15
14
  *
16
15
  * The probe is injected so tests do not depend on the real
17
16
  * `node_modules` resolution; the default probe uses
@@ -100,45 +99,30 @@ function defaultCodegraphProbe() {
100
99
  version,
101
100
  binaryPath,
102
101
  binaryExists,
103
- // Slice rid-CG-003 — resolve the managed codegraph directory
104
- // relative to the cwd the doctor itself was invoked from.
105
- // Best-effort: when the operator ran `peaks doctor` outside a
106
- // project (cwd has neither `.peaks/.codegraph/` nor
107
- // `.codegraph/`) we still report `fresh-preferred` so the check
108
- // message names the canonical future location.
102
+ // Resolve the managed codegraph directory relative to the cwd the
103
+ // doctor itself was invoked from. Root-only: always names
104
+ // `<cwd>/.codegraph/`.
109
105
  managedPath: detectManagedCodegraphPath(process.cwd())
110
106
  };
111
107
  return result;
112
108
  }
113
109
  /**
114
- * Slice rid-CG-003 — pure wrapper over `resolveCodegraphProjectRoot`
115
- * that returns a probe-shaped managed-path payload (or null when
116
- * the cwd does not look like a peaks project root).
110
+ * Pure wrapper over `resolveCodegraphProjectRoot` that returns a
111
+ * probe-shaped managed-path payload for the root `.codegraph/`.
117
112
  */
118
113
  function detectManagedCodegraphPath(cwd) {
119
- try {
120
- const location = resolveCodegraphProjectRoot(cwd);
121
- return {
122
- source: location.source,
123
- codegraphDir: location.codegraphDir,
124
- cwd: location.cwd
125
- };
126
- }
127
- catch {
128
- return null;
129
- }
114
+ const location = resolveCodegraphProjectRoot(cwd);
115
+ return {
116
+ source: location.source,
117
+ codegraphDir: location.codegraphDir,
118
+ cwd: location.cwd
119
+ };
130
120
  }
131
121
  function renderManagedPathSuffix(managedPath) {
132
122
  if (!managedPath) {
133
123
  return '';
134
124
  }
135
- if (managedPath.source === 'preferred') {
136
- return `; managed path: ${managedPath.codegraphDir} (preferred .peaks/.codegraph/)`;
137
- }
138
- if (managedPath.source === 'legacy') {
139
- return `; managed path: ${managedPath.codegraphDir} (legacy root .codegraph/ — consider moving to .peaks/.codegraph/)`;
140
- }
141
- return `; managed path: ${managedPath.codegraphDir} (fresh-preferred — no codegraph dir yet; next init will create it)`;
125
+ return `; managed path: ${managedPath.codegraphDir}`;
142
126
  }
143
127
  function runCheck(probe, managedPathProbe) {
144
128
  try {
@@ -57,7 +57,7 @@ export type DoctorReport = {
57
57
  };
58
58
  };
59
59
  export type CodegraphManagedPathInfo = {
60
- source: 'preferred' | 'legacy' | 'fresh-preferred';
60
+ source: 'root';
61
61
  codegraphDir: string;
62
62
  cwd: string;
63
63
  };
@@ -67,14 +67,11 @@ export type CodegraphCapabilityProbe = {
67
67
  binaryPath: string;
68
68
  binaryExists: boolean;
69
69
  /**
70
- * Slice rid-CG-003 — preferred-path resolution result. When the
71
- * probe can detect `.peaks/.codegraph/` or `.codegraph/` inside
72
- * `process.cwd()` it sets `managedPath`; otherwise null (e.g.
73
- * when the operator invoked `peaks doctor` outside a project).
74
- *
75
- * `source: 'preferred'` — `.peaks/.codegraph/` exists
76
- * `source: 'legacy'` — only `.codegraph/` exists
77
- * `source: 'fresh-preferred'` — neither exists; defaults to preferred
70
+ * Root-only managed-path resolution result. The check resolves the
71
+ * single codegraph data directory, `<cwd>/.codegraph/`, relative to
72
+ * the cwd the doctor was invoked from; null only when resolution is
73
+ * unavailable (e.g. the operator invoked `peaks doctor` outside a
74
+ * resolvable directory).
78
75
  */
79
76
  managedPath: CodegraphManagedPathInfo | null;
80
77
  };
@@ -207,12 +204,11 @@ export type DoctorOptions = {
207
204
  skillsBaseDir?: string;
208
205
  codegraphProbe?: () => CodegraphCapabilityProbe;
209
206
  /**
210
- * Slice rid-CG-003 — optional override for the managed-codegraph
211
- * path detection inside the `capability:codegraph` check. When
212
- * omitted, the check uses the default fs helper
213
- * (`resolveCodegraphProjectRoot(process.cwd())`). Tests inject a
214
- * custom probe to drive preferred / legacy / fresh-preferred
215
- * outcomes without monkey-patching `process.cwd()`.
207
+ * Optional override for the managed-codegraph path detection inside
208
+ * the `capability:codegraph` check. When omitted, the check uses
209
+ * the default resolver (`resolveCodegraphProjectRoot(process.cwd())`).
210
+ * Tests inject a custom probe without monkey-patching
211
+ * `process.cwd()`.
216
212
  */
217
213
  codegraphManagedPathProbe?: () => CodegraphManagedPathInfo | null;
218
214
  skillPresenceProbe?: () => DoctorSkillPresence | null;
@@ -1,4 +1,11 @@
1
1
  import type { IdeAdapter } from '../ide-types.js';
2
+ /**
3
+ * Resolve the currently-active Claude Code model id from a runtime env map.
4
+ * Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
5
+ * `undefined` when none is present. Pure + exported for tests; the caller falls
6
+ * back to the transcript `message.model` when this returns undefined.
7
+ */
8
+ export declare function resolveClaudeModelFromEnv(env: NodeJS.ProcessEnv | undefined): string | undefined;
2
9
  /**
3
10
  * Model-aware context-window size in tokens.
4
11
  *
@@ -103,6 +103,50 @@ const TRANSCRIPT_SCAN_CHUNK_BYTES = 64 * 1024;
103
103
  * variant is 1M-context.
104
104
  */
105
105
  const ONE_MILLION_CONTEXT_MODELS = ['claude-opus-4', 'claude-sonnet-4'];
106
+ /**
107
+ * Claude Code runtime env vars that may carry the currently-active model id,
108
+ * in documented precedence order (first non-empty value wins):
109
+ * 1. ANTHROPIC_MODEL — explicit per-run model override
110
+ * 2. ANTHROPIC_DEFAULT_OPUS_MODEL — default Opus fallback
111
+ * 3. ANTHROPIC_DEFAULT_SONNET_MODEL — default Sonnet fallback
112
+ * 4. ANTHROPIC_DEFAULT_HAIKU_MODEL — default Haiku fallback
113
+ * 5. ANTHROPIC_DEFAULT_FABLE_MODEL — default Fable fallback
114
+ * 6. CLAUDE_CODE_SUBAGENT_MODEL — sub-agent model (used when the others
115
+ * are absent, e.g. a sub-agent-only env)
116
+ *
117
+ * Why env-first: the transcript's `message.model` often drops Claude Code's
118
+ * `[1M]` / `[200K]` suffix marker (observed: `deepseek-v4-flash`), while the
119
+ * runtime env vars above carry it (`deepseek-v4-flash[1M]`). Reading them
120
+ * first lets the window resolver see the true context window. This family is
121
+ * vendor-specific, so it lives ONLY in the claude-code adapter.
122
+ */
123
+ const CLAUDE_CODE_MODEL_ENV_VARS = [
124
+ 'ANTHROPIC_MODEL',
125
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
126
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
127
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
128
+ 'ANTHROPIC_DEFAULT_FABLE_MODEL',
129
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
130
+ ];
131
+ /**
132
+ * Resolve the currently-active Claude Code model id from a runtime env map.
133
+ * Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
134
+ * `undefined` when none is present. Pure + exported for tests; the caller falls
135
+ * back to the transcript `message.model` when this returns undefined.
136
+ */
137
+ export function resolveClaudeModelFromEnv(env) {
138
+ if (!env)
139
+ return undefined;
140
+ for (const name of CLAUDE_CODE_MODEL_ENV_VARS) {
141
+ const value = env[name];
142
+ if (typeof value === 'string') {
143
+ const trimmed = value.trim();
144
+ if (trimmed.length > 0)
145
+ return trimmed;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
106
150
  /**
107
151
  * Model-aware context-window size in tokens.
108
152
  *
@@ -251,8 +295,12 @@ function resolveContextWindowTokens(model, contextTokens) {
251
295
  * `bytes / 256KB` (which over-fired because the transcript grows unboundedly).
252
296
  * Tagged `'transcript-estimate'` (v2.14.0) so callers know it is a real
253
297
  * signal, NOT a hard gate.
298
+ *
299
+ * Window model resolution is env-first: when `envModel` is present, its id
300
+ * (which Claude Code stamps with the `[1M]` / `[200K]` suffix) drives the
301
+ * window; otherwise the transcript `message.model` is used.
254
302
  */
255
- function readClaudeTranscriptEstimate(outerSessionId) {
303
+ function readClaudeTranscriptEstimate(outerSessionId, envModel) {
256
304
  const projectsDir = join(homedir(), '.claude', 'projects');
257
305
  const path = findTranscriptJsonl(projectsDir, outerSessionId);
258
306
  if (path === null)
@@ -260,7 +308,8 @@ function readClaudeTranscriptEstimate(outerSessionId) {
260
308
  const latest = findLatestTranscriptUsage(path);
261
309
  if (latest === null)
262
310
  return null;
263
- const contextWindowTokens = resolveContextWindowTokens(latest.model, latest.contextTokens);
311
+ const model = envModel !== undefined && envModel.trim().length > 0 ? envModel : latest.model;
312
+ const contextWindowTokens = resolveContextWindowTokens(model, latest.contextTokens);
264
313
  const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
265
314
  return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
266
315
  }
@@ -274,7 +323,10 @@ function readClaudeTranscriptEstimate(outerSessionId) {
274
323
  * `~/.claude/projects/<hash>/...` using the OUTER session id (Claude
275
324
  * names its transcript by the outer session UUID, not the peaks sid),
276
325
  * and estimates `contextTokens / contextWindowTokens` from the LATEST
277
- * `message.usage` entry (token-based + model-aware).
326
+ * `message.usage` entry (token-based + model-aware). The window model
327
+ * resolves env-first via `resolveClaudeModelFromEnv(input.env)` (which
328
+ * carries the `[1M]` suffix the transcript often drops), falling back to
329
+ * the transcript `message.model` when env is empty.
278
330
  * Returns `null` when neither yields a signal → the reader emits
279
331
  * `conservative-fallback`.
280
332
  */
@@ -291,7 +343,8 @@ function readContextPercentFallback(input) {
291
343
  return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide, capturedAt };
292
344
  }
293
345
  if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
294
- const estimate = readClaudeTranscriptEstimate(input.outerSessionId);
346
+ const envModel = resolveClaudeModelFromEnv(input.env);
347
+ const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel);
295
348
  if (estimate !== null) {
296
349
  return {
297
350
  ratio: estimate.ratio,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.27",
3
+ "version": "4.0.28",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,10 +101,10 @@
101
101
  "fzf": "^0.5.2",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^4.4.3",
104
- "peaks-loop-mut": "0.1.25",
105
- "peaks-loop-internal-runtime": "0.0.12",
106
- "peaks-loop-shared": "0.0.61",
107
- "peaks-loop-shared-channel": "0.0.29"
104
+ "peaks-loop-internal-runtime": "0.0.13",
105
+ "peaks-loop-mut": "0.1.26",
106
+ "peaks-loop-shared": "0.0.62",
107
+ "peaks-loop-shared-channel": "0.0.30"
108
108
  },
109
109
  "devDependencies": {
110
110
  "@changesets/cli": "2.31.1",