peaks-loop 4.0.26 → 4.0.27

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.27 — 2026-09-01 (auto-compact context probe fix)
4
+
5
+ **1 atomic commit from session 2026-09-01-session-fdd7aa**:
6
+
7
+ - `7f0f4ca2` perf(auto-compact): vendor-neutral context probe + token-based transcript calibration
8
+
9
+ **Highlights**:
10
+
11
+ 1. **auto-compact 根因修复(三级链)** — context 比例探针之前永远返回 `conservative-fallback`(ratio 0),auto-compact 从不触发。三处修复:(a) vendor-neutral:Claude transcript/statusline fallback 收进 claude-code-adapter 的 `IdeCompactProfile.readContextPercentFallback`,通用 reader 不再硬编码 `~/.claude`;(b) session-id:transcript 查找改用 `outerSessionId`(Claude Code 真 UUID)而非 peaks `sessionId`;(c) 校准:`ratio = contextTokens / contextWindowTokens`,contextTokens 从最新 `message.usage`(input + cache_read + cache_creation)读取,contextWindowTokens 模型感知(1M vs 200K 默认),替换错误的 `bytes/256KB`(永远 100%)。
12
+
3
13
  ## 4.0.26 — 2026-09-01 (headroom removal + prompt-cache prefix alignment)
4
14
 
5
15
  **2 atomic commits from session 2026-09-01-session-fdd7aa**:
@@ -15,6 +15,7 @@ import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from
15
15
  import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
16
16
  import { getSkillPresence } from '../../services/skills/skill-presence-service.js';
17
17
  import { probeInFlightBatch } from '../../services/workflow/workflow-inflight-probe.js';
18
+ import { resolveOuterSessionId } from '../../services/session/binding-status-service.js';
18
19
  export function registerCodeRuntimeCommands(code, io) {
19
20
  addJsonOption(code
20
21
  .command('post-compact-detect')
@@ -175,9 +176,12 @@ export function registerCodeRuntimeCommands(code, io) {
175
176
  // missing/malformed decision file is fine — fall back to advisory.
176
177
  }
177
178
  }
179
+ const sessionId = opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown';
180
+ const outerSessionId = resolveOuterSessionId(opts.project, sessionId);
178
181
  const probe = readContextPercent({
179
182
  projectRoot: opts.project,
180
- sessionId: opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown',
183
+ sessionId,
184
+ outerSessionId,
181
185
  env: process.env,
182
186
  promptSizeBytes
183
187
  });
@@ -218,6 +222,8 @@ export function registerCodeRuntimeCommands(code, io) {
218
222
  ide: probe.ide,
219
223
  capacityBytes: probe.capacityBytes,
220
224
  rawBytes: probe.rawBytes ?? null,
225
+ rawTokens: probe.rawTokens ?? null,
226
+ capacityTokens: probe.capacityTokens ?? null,
221
227
  bytesPrompt: promptSizeBytes ?? null,
222
228
  capturedAt: probe.capturedAt
223
229
  }, [], [
@@ -27,6 +27,7 @@ import { noteDispatched, BATCH_LIMIT } from '../../services/dispatch/batch-count
27
27
  import { writeInitialDispatchRecord } from '../../services/dispatch/dispatch-record-writer.js';
28
28
  import { evaluatePromptSize } from '../../services/context/context-guard.js';
29
29
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
30
+ import { resolveOuterSessionId } from '../../services/session/binding-status-service.js';
30
31
  import { buildArtifactMeta, buildContextImpact } from '../../services/context/artifact-meta.js';
31
32
  import { assertSafeArtifactPath } from 'peaks-loop-shared-channel';
32
33
  import { playwrightProfilePaths } from '../../services/worktree/playwright-profile.js';
@@ -382,9 +383,11 @@ export function registerDispatchCommand(parent, io) {
382
383
  let contextProbe = null;
383
384
  try {
384
385
  const { readContextPercent } = await import('../../services/context/auto-compact-reader.js');
386
+ const outerSessionId = resolveOuterSessionId(projectRoot, sid);
385
387
  contextProbe = readContextPercent({
386
388
  projectRoot,
387
389
  sessionId: sid,
390
+ outerSessionId,
388
391
  env: process.env
389
392
  });
390
393
  }
@@ -31,6 +31,7 @@
31
31
  import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
32
32
  import { dirname, join } from 'node:path';
33
33
  import { getSessionIdCanonical } from '../session/session-manager.js';
34
+ import { resolveOuterSessionId } from '../session/binding-status-service.js';
34
35
  import { AUTO_COMPACT_PRE_COMPACT_RATIO, AUTO_COMPACT_THRESHOLD_RATIO } from '../context/auto-compact-types.js';
35
36
  import { describeMode, thresholdFor } from './auto-compact-modes.js';
36
37
  import { read24hState } from '../24h-mode/store.js';
@@ -525,9 +526,11 @@ export async function runAutoCompact(input) {
525
526
  const mode = input.mode ?? resolveAutoCompactMode(input.projectRoot, sessionId);
526
527
  // Lazy import to avoid the AC-1 module depending on the orchestrator.
527
528
  const { readContextPercent } = await import('../context/auto-compact-reader.js');
529
+ const outerSessionId = resolveOuterSessionId(input.projectRoot, sessionId, input.env ?? process.env);
528
530
  const probe = readContextPercent({
529
531
  projectRoot: input.projectRoot,
530
532
  sessionId,
533
+ outerSessionId,
531
534
  env: input.env
532
535
  });
533
536
  const decision = evaluateAutoCompactDecision({
@@ -1,14 +1,50 @@
1
+ /**
2
+ * AC-1 — auto context-percent probe.
3
+ *
4
+ * Reads the current AI CLI context-fill ratio without requiring the
5
+ * LLM to pass `--prompt-size <bytes>` manually. Strategy: ask the
6
+ * registered `IdeAdapter.compact` profile which env-var to read and,
7
+ * when that misses, ask the adapter for a vendor-specific fallback —
8
+ * no hard-coded IDE names. Per-adapter:
9
+ *
10
+ * - claude-code: its adapter-declared env-var (MVP) + a
11
+ * `readContextPercentFallback` that polls the statusline /
12
+ * transcript (see claude-code-adapter.ts).
13
+ * - trae / codex / cursor / qoder / tongyi-lingma / hermes /
14
+ * openclaw / zcode: each adapter fills its own env-var; until
15
+ * L2-dogfood verifies each surface, adapters may omit `compact`
16
+ * and the probe returns `source: 'conservative-fallback'`.
17
+ *
18
+ * Resolution order (user-overridden → env-var → adapter fallback →
19
+ * conservative-fallback):
20
+ * 1. `promptSizeBytes` (P0 `--prompt-size <bytes>` escape hatch) →
21
+ * `source: 'user-overridden'`.
22
+ * 2. `adapter.compact.envVarForContextPercent` env-var →
23
+ * `source: '<ideId>-env'`.
24
+ * 3. `adapter.compact.readContextPercentFallback?.(input)` — the
25
+ * adapter owns any vendor-specific statusline / transcript probe.
26
+ * 4. `ratio: 0` with `source: 'conservative-fallback'` — the
27
+ * orchestrator MUST NOT auto-fire compact on this signal.
28
+ */
1
29
  import type { ContextPercentProbe } from './auto-compact-types.js';
2
30
  export interface ReadContextPercentInput {
3
31
  readonly projectRoot: string;
4
32
  readonly sessionId: string;
33
+ /**
34
+ * Outer (harness / IDE) session id — the id the IDE uses to name its
35
+ * transcript / session files. Resolved by the caller (env signal → bound
36
+ * session meta) and passed through to the adapter's
37
+ * `readContextPercentFallback`. Optional: when unresolved, the adapter
38
+ * fallback returns null → conservative-fallback.
39
+ */
40
+ readonly outerSessionId?: string | undefined;
5
41
  readonly env?: NodeJS.ProcessEnv | undefined;
6
42
  /**
7
43
  * Slice 2026-07-31-rid-002: explicit byte count from `--prompt-size <bytes>`.
8
44
  * When set to a finite non-negative number, short-circuits the entire
9
45
  * env / statusline / transcript chain with `source: 'user-overridden'`.
10
- * Mac escape hatch — Claude Code Mac does NOT inject
11
- * `CLAUDE_CONTEXT_USAGE_PERCENT` into PreToolUse sub-shells, so the
46
+ * Mac escape hatch — some IDEs (e.g. Claude Code on macOS) do NOT
47
+ * inject their context-percent env-var into PreToolUse sub-shells, so the
12
48
  * user (or a hook wrapper) can inject the bytes they observed themselves.
13
49
  * Priority P0 — above everything else.
14
50
  */
@@ -21,60 +57,20 @@ export interface ReadContextPercentInput {
21
57
  * so the function itself has no hard-coded IDE names.
22
58
  */
23
59
  declare function readEnvPercent(env: NodeJS.ProcessEnv, varName: string): number | null;
24
- /**
25
- * Read the IDE-specific statusline state. MVP path is Claude Code's
26
- * `~/.claude/statusline-state.json`; other IDEs are intentionally
27
- * left for future slices (each IDE will expose its own
28
- * `compact.postCompactDetectCommand` to drive this).
29
- */
30
- declare function readClaudeStatuslinePercent(): number | null;
31
- /**
32
- * Recursive search for `<sessionId>.jsonl` under `projectsDir`. Used by
33
- * `readClaudeTranscriptFallback` and exported via `_internal` so unit
34
- * tests can drive it without monkey-patching `os.homedir` (which is
35
- * non-configurable in ESM module namespaces).
36
- *
37
- * The Mac layout encodes the cwd as a single hash directory; on Mac
38
- * Claude Code nests the transcript under that hash with an extra level
39
- * of subdirectory we cannot predict ahead of time. A flat readdir misses
40
- * that branch and returns null — the silent-failure mode that this fix
41
- * closes.
42
- */
43
- declare function findTranscriptJsonl(projectsDir: string, sessionId: string): {
44
- path: string;
45
- bytes: number;
46
- } | null;
47
- /**
48
- * Conservative transcript-size fallback. Recursively searches
49
- * `~/.claude/projects/<hash>/<sid-or-nested>.jsonl` (Mac may nest
50
- * the jsonl under an
51
- * extra directory we cannot predict ahead of time) and estimates
52
- * `ratio = bytesUsed / 256K`. Returns the bytes seen so the
53
- * orchestrator can show "estimated from 124KB of 256KB transcript"
54
- * in the envelope. Tagged `'transcript-estimate'` (v2.14.0) so callers
55
- * know it is a real signal, NOT a hard gate.
56
- */
57
- declare function readClaudeTranscriptFallback(sessionId: string): {
58
- ratio: number;
59
- bytes: number;
60
- } | null;
61
60
  /**
62
61
  * Probe the current AI CLI's context-fill ratio. Adapter-driven:
63
62
  * looks up the registered `IdeAdapter.compact` profile via
64
- * `getIdeAdapter(detectIdeFromEnv(env))` and reads the
65
- * adapter-declared env-var. Falls back to statusline poll +
66
- * transcript estimate ONLY for adapters that opt in
67
- * (`adapter.id === 'claude-code'` for the MVP); other adapters
68
- * without an env-var hit return `source: 'conservative-fallback'`
69
- * with `ratio: 0` so the orchestrator never auto-fires on a
70
- * missing signal.
63
+ * `getAdapter(detectIdeFromEnv(env))` and reads the
64
+ * adapter-declared env-var. When that misses, delegates to the
65
+ * adapter's optional `readContextPercentFallback` (which owns any
66
+ * vendor-specific statusline / transcript probe). Adapters without a
67
+ * fallback (or a fallback that returns null) yield
68
+ * `source: 'conservative-fallback'` with `ratio: 0` so the
69
+ * orchestrator never auto-fires on a missing signal.
71
70
  */
72
71
  export declare function readContextPercent(input: ReadContextPercentInput): ContextPercentProbe;
73
72
  /** Re-export the env-var probe for unit tests. */
74
73
  export declare const _internal: {
75
74
  readEnvPercent: typeof readEnvPercent;
76
- readClaudeStatuslinePercent: typeof readClaudeStatuslinePercent;
77
- readClaudeTranscriptFallback: typeof readClaudeTranscriptFallback;
78
- findTranscriptJsonl: typeof findTranscriptJsonl;
79
75
  };
80
76
  export {};
@@ -1,30 +1,3 @@
1
- /**
2
- * AC-1 — auto context-percent probe.
3
- *
4
- * Reads the current AI CLI context-fill ratio without requiring the
5
- * LLM to pass `--prompt-size <bytes>` manually. Strategy: ask the
6
- * registered `IdeAdapter.compact` profile which env-var to read;
7
- * no hard-coded IDE names. Per-adapter:
8
- *
9
- * - claude-code: `CLAUDE_CONTEXT_USAGE_PERCENT` (MVP)
10
- * - trae / codex / cursor / qoder / tongyi-lingma / hermes /
11
- * openclaw: each adapter fills its own env-var; until L2-dogfood
12
- * verifies each surface, adapters may omit `compact` and the
13
- * probe returns `source: 'conservative-fallback'`.
14
- *
15
- * Fallback chain (when adapter.compact is undefined OR the env-var
16
- * is missing):
17
- * 1. statusline poll (`~/.claude/statusline-state.json` for
18
- * Claude Code MVP; other IDEs register their own poll path
19
- * by exposing `compact.postCompactDetectCommand`).
20
- * 2. Conservative transcript-size estimate
21
- * (`~/.claude/projects/<hash>/<sid>.jsonl` for Claude Code).
22
- * 3. `ratio: 0` with `source: 'conservative-fallback'` — the
23
- * orchestrator MUST NOT auto-fire compact on this signal.
24
- */
25
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
26
- import { homedir } from 'node:os';
27
- import { join } from 'node:path';
28
1
  import { detectIdeFromEnv } from './ide-detect.js';
29
2
  import { getAdapter } from '../ide/ide-registry.js';
30
3
  /**
@@ -44,106 +17,16 @@ function readEnvPercent(env, varName) {
44
17
  return null;
45
18
  return Math.max(0, Math.min(1, parsed));
46
19
  }
47
- /**
48
- * Read the IDE-specific statusline state. MVP path is Claude Code's
49
- * `~/.claude/statusline-state.json`; other IDEs are intentionally
50
- * left for future slices (each IDE will expose its own
51
- * `compact.postCompactDetectCommand` to drive this).
52
- */
53
- function readClaudeStatuslinePercent() {
54
- const path = join(homedir(), '.claude', 'statusline-state.json');
55
- if (!existsSync(path))
56
- return null;
57
- try {
58
- const json = JSON.parse(readFileSync(path, 'utf8'));
59
- const candidates = ['contextPercent', 'context_usage_percent', 'contextPercentUsed'];
60
- for (const key of candidates) {
61
- const raw = json[key];
62
- if (typeof raw === 'number' && Number.isFinite(raw)) {
63
- return raw > 1.5 ? raw / 100 : Math.max(0, Math.min(1, raw));
64
- }
65
- }
66
- }
67
- catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
68
- if (err instanceof ReferenceError)
69
- throw err; // surface module-load bugs
70
- if (err instanceof SyntaxError)
71
- throw err; // surface parse bugs (e.g. broken statusline JSON)
72
- return null; // only swallow IO errors
73
- }
74
- return null;
75
- }
76
- /**
77
- * Recursive search for `<sessionId>.jsonl` under `projectsDir`. Used by
78
- * `readClaudeTranscriptFallback` and exported via `_internal` so unit
79
- * tests can drive it without monkey-patching `os.homedir` (which is
80
- * non-configurable in ESM module namespaces).
81
- *
82
- * The Mac layout encodes the cwd as a single hash directory; on Mac
83
- * Claude Code nests the transcript under that hash with an extra level
84
- * of subdirectory we cannot predict ahead of time. A flat readdir misses
85
- * that branch and returns null — the silent-failure mode that this fix
86
- * closes.
87
- */
88
- function findTranscriptJsonl(projectsDir, sessionId) {
89
- if (!existsSync(projectsDir))
90
- return null;
91
- try {
92
- const stack = [projectsDir];
93
- while (stack.length > 0) {
94
- const dir = stack.pop();
95
- if (dir === undefined)
96
- break;
97
- const entries = readdirSync(dir, { withFileTypes: true });
98
- for (const entry of entries) {
99
- const full = join(dir, entry.name);
100
- if (entry.isDirectory()) {
101
- stack.push(full);
102
- }
103
- else if (entry.isFile() && entry.name === `${sessionId}.jsonl`) {
104
- const bytes = statSync(full).size;
105
- return { path: full, bytes };
106
- }
107
- }
108
- }
109
- }
110
- catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
111
- if (err instanceof ReferenceError)
112
- throw err; // surface module-load bugs
113
- if (err instanceof SyntaxError)
114
- throw err; // surface parse bugs
115
- return null; // only swallow IO errors
116
- }
117
- return null;
118
- }
119
- /**
120
- * Conservative transcript-size fallback. Recursively searches
121
- * `~/.claude/projects/<hash>/<sid-or-nested>.jsonl` (Mac may nest
122
- * the jsonl under an
123
- * extra directory we cannot predict ahead of time) and estimates
124
- * `ratio = bytesUsed / 256K`. Returns the bytes seen so the
125
- * orchestrator can show "estimated from 124KB of 256KB transcript"
126
- * in the envelope. Tagged `'transcript-estimate'` (v2.14.0) so callers
127
- * know it is a real signal, NOT a hard gate.
128
- */
129
- function readClaudeTranscriptFallback(sessionId) {
130
- const projectsDir = join(homedir(), '.claude', 'projects');
131
- const hit = findTranscriptJsonl(projectsDir, sessionId);
132
- if (hit === null)
133
- return null;
134
- const ratio = Math.min(1, hit.bytes / (256 * 1024));
135
- return { ratio, bytes: hit.bytes };
136
- }
137
20
  /**
138
21
  * Probe the current AI CLI's context-fill ratio. Adapter-driven:
139
22
  * looks up the registered `IdeAdapter.compact` profile via
140
- * `getIdeAdapter(detectIdeFromEnv(env))` and reads the
141
- * adapter-declared env-var. Falls back to statusline poll +
142
- * transcript estimate ONLY for adapters that opt in
143
- * (`adapter.id === 'claude-code'` for the MVP); other adapters
144
- * without an env-var hit return `source: 'conservative-fallback'`
145
- * with `ratio: 0` so the orchestrator never auto-fires on a
146
- * missing signal.
23
+ * `getAdapter(detectIdeFromEnv(env))` and reads the
24
+ * adapter-declared env-var. When that misses, delegates to the
25
+ * adapter's optional `readContextPercentFallback` (which owns any
26
+ * vendor-specific statusline / transcript probe). Adapters without a
27
+ * fallback (or a fallback that returns null) yield
28
+ * `source: 'conservative-fallback'` with `ratio: 0` so the
29
+ * orchestrator never auto-fires on a missing signal.
147
30
  */
148
31
  export function readContextPercent(input) {
149
32
  const env = input.env ?? process.env;
@@ -158,7 +41,7 @@ export function readContextPercent(input) {
158
41
  // through `unknown` to IdeId's wider 8-element set.
159
42
  const ideId = (detected === 'unknown' ? 'claude-code' : detected);
160
43
  const adapter = getAdapter(ideId);
161
- // P0 user-overridden takes priority over env / statusline / transcript.
44
+ // P0 user-overridden takes priority over env / fallback / transcript.
162
45
  // Mac escape hatch: when the CLI/helper passes `--prompt-size <bytes>`,
163
46
  // honor that number directly. Do NOT read env, statusline, or transcript
164
47
  // — user intent always wins. Negative / non-finite values are ignored
@@ -176,8 +59,8 @@ export function readContextPercent(input) {
176
59
  capturedAt
177
60
  };
178
61
  }
179
- // Primary: read the adapter-declared env-var (no hard-coded IDE names).
180
62
  if (adapter.compact) {
63
+ // Primary: read the adapter-declared env-var (no hard-coded IDE names).
181
64
  const primary = readEnvPercent(env, adapter.compact.envVarForContextPercent);
182
65
  if (primary !== null) {
183
66
  return {
@@ -188,27 +71,17 @@ export function readContextPercent(input) {
188
71
  capturedAt
189
72
  };
190
73
  }
191
- }
192
- // MVP-only fallback chain: statusline poll + transcript estimate.
193
- // Non-MVP adapters that don't fill `compact` skip this and return
194
- // a conservative-zero probe (the orchestrator will stay in
195
- // 'none' / 'soft-warn' zone and never auto-fire compact).
196
- if (ideId === 'claude-code') {
197
- const statusline = readClaudeStatuslinePercent();
198
- if (statusline !== null) {
199
- return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide: ideId, capturedAt };
200
- }
201
- const fallback = readClaudeTranscriptFallback(input.sessionId);
202
- if (fallback !== null) {
203
- return {
204
- ratio: fallback.ratio,
205
- source: 'transcript-estimate',
206
- rawBytes: fallback.bytes,
207
- capacityBytes,
208
- ide: ideId,
209
- capturedAt
210
- };
211
- }
74
+ // Fallback: the adapter owns any vendor-specific statusline /
75
+ // transcript probe. When it returns a probe, honor it; otherwise
76
+ // fall through to conservative-fallback.
77
+ const fallback = adapter.compact.readContextPercentFallback?.({
78
+ projectRoot: input.projectRoot,
79
+ sessionId: input.sessionId,
80
+ outerSessionId: input.outerSessionId,
81
+ env
82
+ });
83
+ if (fallback)
84
+ return fallback;
212
85
  }
213
86
  // No signal available — return `ratio: 0` so the orchestrator
214
87
  // stays in `none` zone and the LLM can still pass `--prompt-size`
@@ -216,4 +89,4 @@ export function readContextPercent(input) {
216
89
  return { ratio: 0, source: 'conservative-fallback', capacityBytes, ide: ideId, capturedAt };
217
90
  }
218
91
  /** Re-export the env-var probe for unit tests. */
219
- export const _internal = { readEnvPercent, readClaudeStatuslinePercent, readClaudeTranscriptFallback, findTranscriptJsonl };
92
+ export const _internal = { readEnvPercent };
@@ -161,8 +161,10 @@ export interface ContextPercentProbe {
161
161
  * - `transcript-estimate` (v2.14.0 Mac-aware: recursive
162
162
  * readdir under ~/.claude/projects/
163
163
  * finds <sid>.jsonl and reports
164
- * bytes/256K; a real signal, NOT
165
- * a hard gate)
164
+ * contextTokens / contextWindowTokens
165
+ * from the LATEST message.usage entry —
166
+ * token-based + model-aware; a real
167
+ * signal, NOT a hard gate)
166
168
  * - `conservative-fallback` (no signal available — caller
167
169
  * MUST NOT treat as hard gate)
168
170
  * Future IDEs may add per-ide sources; the type accepts any
@@ -171,6 +173,19 @@ export interface ContextPercentProbe {
171
173
  readonly source: string;
172
174
  readonly rawBytes?: number;
173
175
  readonly capacityBytes?: number;
176
+ /**
177
+ * Token-based numerator for `transcript-estimate`: the observed
178
+ * `input_tokens + cache_read_input_tokens + cache_creation_input_tokens`
179
+ * from the LATEST transcript `message.usage` entry. Undefined for byte /
180
+ * percent sources (`user-overridden`, `${ideId}-env`, `statusline-poll`).
181
+ */
182
+ readonly rawTokens?: number;
183
+ /**
184
+ * Token-based denominator for `transcript-estimate`: the model's context
185
+ * window in tokens (`modelContextWindowTokens`). Undefined for byte /
186
+ * percent sources.
187
+ */
188
+ readonly capacityTokens?: number;
174
189
  readonly ide: string;
175
190
  readonly capturedAt: string;
176
191
  }
@@ -1,18 +1,18 @@
1
1
  import type { IdeAdapter } from '../ide-types.js';
2
2
  /**
3
- * Claude Code adapter —— peaks-loop 的"起源 IDE"。
3
+ * Model-aware context-window size in tokens.
4
4
  *
5
- * 该 adapter 从原 `src/services/skills/hooks-settings-service.ts` 提取,保持
6
- * 字节级兼容:用户在 Claude Code 环境下跑 `peaks hooks install` 产出的
7
- * `.claude/settings.json` 与 refactor 前逐字节相同。
5
+ * Detection rule (documented):
6
+ * 1. Empty / unknown model → DEFAULT_CONTEXT_WINDOW_TOKENS (200_000).
7
+ * 2. Suffix heuristic — a model id containing `1m` (case-insensitive) is
8
+ * treated as 1M-context.
9
+ * 3. Explicit allowlist — known 1M Claude model ids
10
+ * (ONE_MILLION_CONTEXT_MODELS).
11
+ * 4. Everything else → 200_000 (the safe default).
8
12
  *
9
- * 字段解释(见 PRD AC-1):
10
- * - dirName = '.claude' : Claude Code 项目根下的 settings 目录
11
- * - settingsFileName = 'settings.json'
12
- * - envVar = 'CLAUDE_PROJECT_DIR' : Claude Code 注入的 env 变量,用于 ${...} 占位
13
- * - hookEvent = 'PreToolUse' : Claude Code hook 数组 key
14
- * - toolMatcher = 'Bash' | 'Task' : PreToolUse 数组元素的 matcher 字段
15
- *
16
- * 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
13
+ * Callers MAY additionally infer ≥1M from the observed token count: if
14
+ * `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
15
+ * 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
17
16
  */
17
+ export declare function modelContextWindowTokens(model: string): number;
18
18
  export declare const CLAUDE_CODE_ADAPTER: IdeAdapter;
@@ -1,3 +1,4 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
1
2
  import { homedir } from 'node:os';
2
3
  import { join, resolve } from 'node:path';
3
4
  import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
@@ -17,6 +18,293 @@ import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatche
17
18
  *
18
19
  * 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
19
20
  */
21
+ /**
22
+ * Read Claude Code's statusline state file
23
+ * (`~/.claude/statusline-state.json`) and parse a context-percent key.
24
+ * Moved from the generic reader in slice
25
+ * 2026-09-02-vendor-neutral-context-probe — Claude-specific paths now live
26
+ * only in the Claude Code adapter.
27
+ */
28
+ function readClaudeStatuslinePercent() {
29
+ const path = join(homedir(), '.claude', 'statusline-state.json');
30
+ if (!existsSync(path))
31
+ return null;
32
+ try {
33
+ const json = JSON.parse(readFileSync(path, 'utf8'));
34
+ const candidates = ['contextPercent', 'context_usage_percent', 'contextPercentUsed'];
35
+ for (const key of candidates) {
36
+ const raw = json[key];
37
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
38
+ return raw > 1.5 ? raw / 100 : Math.max(0, Math.min(1, raw));
39
+ }
40
+ }
41
+ }
42
+ catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
43
+ if (err instanceof ReferenceError)
44
+ throw err; // surface module-load bugs
45
+ if (err instanceof SyntaxError)
46
+ throw err; // surface parse bugs (e.g. broken statusline JSON)
47
+ return null; // only swallow IO errors
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Recursive search for `<outerSessionId>.jsonl` under `projectsDir`. The
53
+ * Mac layout encodes the cwd as a single hash directory; on Mac Claude Code
54
+ * nests the transcript under that hash with an extra level of subdirectory we
55
+ * cannot predict ahead of time. A flat readdir misses that branch and returns
56
+ * null — the silent-failure mode this recursion closes.
57
+ *
58
+ * Moved from the generic reader in slice
59
+ * 2026-09-02-vendor-neutral-context-probe. The lookup key is the OUTER
60
+ * session id (Claude Code names its transcript by the outer session UUID),
61
+ * NOT the peaks session id.
62
+ */
63
+ function findTranscriptJsonl(projectsDir, outerSessionId) {
64
+ if (!existsSync(projectsDir))
65
+ return null;
66
+ try {
67
+ const stack = [projectsDir];
68
+ while (stack.length > 0) {
69
+ const dir = stack.pop();
70
+ if (dir === undefined)
71
+ break;
72
+ const entries = readdirSync(dir, { withFileTypes: true });
73
+ for (const entry of entries) {
74
+ const full = join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ stack.push(full);
77
+ }
78
+ else if (entry.isFile() && entry.name === `${outerSessionId}.jsonl`) {
79
+ return full;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
85
+ if (err instanceof ReferenceError)
86
+ throw err; // surface module-load bugs
87
+ if (err instanceof SyntaxError)
88
+ throw err; // surface parse bugs
89
+ return null; // only swallow IO errors
90
+ }
91
+ return null;
92
+ }
93
+ /** 1M-context window size in tokens (documented single choice: 1,000,000). */
94
+ const ONE_MILLION_CONTEXT_TOKENS = 1_000_000;
95
+ /** Safe-default (non-1M) context window size in tokens. */
96
+ const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
97
+ /** Reverse-scan chunk size in bytes (keeps memory bounded on multi-MB transcripts). */
98
+ const TRANSCRIPT_SCAN_CHUNK_BYTES = 64 * 1024;
99
+ /**
100
+ * Known 1M-context Claude model id prefixes whose ids do NOT carry a `1m`
101
+ * suffix (e.g. `claude-sonnet-4-5-20250929`). The substring match is
102
+ * intentionally generous — every `claude-sonnet-4*` / `claude-opus-4*`
103
+ * variant is 1M-context.
104
+ */
105
+ const ONE_MILLION_CONTEXT_MODELS = ['claude-opus-4', 'claude-sonnet-4'];
106
+ /**
107
+ * Model-aware context-window size in tokens.
108
+ *
109
+ * Detection rule (documented):
110
+ * 1. Empty / unknown model → DEFAULT_CONTEXT_WINDOW_TOKENS (200_000).
111
+ * 2. Suffix heuristic — a model id containing `1m` (case-insensitive) is
112
+ * treated as 1M-context.
113
+ * 3. Explicit allowlist — known 1M Claude model ids
114
+ * (ONE_MILLION_CONTEXT_MODELS).
115
+ * 4. Everything else → 200_000 (the safe default).
116
+ *
117
+ * Callers MAY additionally infer ≥1M from the observed token count: if
118
+ * `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
119
+ * 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
120
+ */
121
+ export function modelContextWindowTokens(model) {
122
+ const m = model.trim().toLowerCase();
123
+ if (m.length === 0)
124
+ return DEFAULT_CONTEXT_WINDOW_TOKENS;
125
+ if (m.includes('1m'))
126
+ return ONE_MILLION_CONTEXT_TOKENS;
127
+ for (const known of ONE_MILLION_CONTEXT_MODELS) {
128
+ if (m.includes(known))
129
+ return ONE_MILLION_CONTEXT_TOKENS;
130
+ }
131
+ return DEFAULT_CONTEXT_WINDOW_TOKENS;
132
+ }
133
+ /** A non-negative finite number, or null when the value is not numeric. */
134
+ function numericTokenCount(value) {
135
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
136
+ return value;
137
+ return null;
138
+ }
139
+ /**
140
+ * Parse a single jsonl line into its token count + model id. Returns null
141
+ * when the line has no `message.usage` object with numeric token fields.
142
+ */
143
+ function parseTranscriptUsageLine(line) {
144
+ if (line.length === 0)
145
+ return null;
146
+ let json;
147
+ try {
148
+ json = JSON.parse(line);
149
+ }
150
+ catch {
151
+ return null; // non-JSON line (blank / corrupt) — skip
152
+ }
153
+ if (typeof json !== 'object' || json === null)
154
+ return null;
155
+ const record = json;
156
+ const message = record.message;
157
+ if (typeof message !== 'object' || message === null)
158
+ return null;
159
+ const msg = message;
160
+ const usage = msg.usage;
161
+ if (typeof usage !== 'object' || usage === null)
162
+ return null;
163
+ const u = usage;
164
+ const inputTokens = numericTokenCount(u.input_tokens);
165
+ const cacheRead = numericTokenCount(u.cache_read_input_tokens);
166
+ const cacheCreation = numericTokenCount(u.cache_creation_input_tokens);
167
+ if (inputTokens === null && cacheRead === null && cacheCreation === null)
168
+ return null;
169
+ const contextTokens = (inputTokens ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0);
170
+ // Model id lives at `message.model`, falling back to a top-level `model`.
171
+ const model = typeof msg.model === 'string'
172
+ ? msg.model
173
+ : typeof record.model === 'string' ? record.model : '';
174
+ return { contextTokens, model };
175
+ }
176
+ /**
177
+ * Reverse-scan the transcript jsonl (from the END) for the LATEST entry that
178
+ * carries a numeric `message.usage`. The file can be many MB; it is read in
179
+ * backward chunks of TRANSCRIPT_SCAN_CHUNK_BYTES — never fully into memory —
180
+ * and stops at the first (newest) usable entry.
181
+ */
182
+ function findLatestTranscriptUsage(filePath) {
183
+ let fd = null;
184
+ try {
185
+ const size = statSync(filePath).size;
186
+ if (size === 0)
187
+ return null;
188
+ fd = openSync(filePath, 'r');
189
+ let position = size;
190
+ let carry = ''; // partial line head carried into the next (older) chunk
191
+ while (position > 0) {
192
+ const readLen = Math.min(TRANSCRIPT_SCAN_CHUNK_BYTES, position);
193
+ position -= readLen;
194
+ const buf = Buffer.alloc(readLen);
195
+ const bytesRead = readSync(fd, buf, 0, readLen, position);
196
+ if (bytesRead <= 0)
197
+ break;
198
+ const lines = (buf.toString('utf8', 0, bytesRead) + carry).split('\n');
199
+ carry = lines[0] ?? '';
200
+ for (let i = lines.length - 1; i >= 1; i--) {
201
+ const line = lines[i];
202
+ if (line === undefined)
203
+ continue;
204
+ const parsed = parseTranscriptUsageLine(line);
205
+ if (parsed !== null)
206
+ return parsed;
207
+ }
208
+ }
209
+ // The final carry is the first line of the file (complete, since it starts at byte 0).
210
+ if (carry.length > 0) {
211
+ const parsed = parseTranscriptUsageLine(carry);
212
+ if (parsed !== null)
213
+ return parsed;
214
+ }
215
+ return null;
216
+ }
217
+ catch (err) {
218
+ // Narrow: surface module-load / parse bugs, swallow IO errors only
219
+ // (mirrors the other adapter read helpers' catch discipline).
220
+ if (err instanceof ReferenceError)
221
+ throw err;
222
+ if (err instanceof SyntaxError)
223
+ throw err;
224
+ return null;
225
+ }
226
+ finally {
227
+ if (fd !== null) {
228
+ try {
229
+ closeSync(fd);
230
+ }
231
+ catch { /* best-effort */ }
232
+ }
233
+ }
234
+ }
235
+ /**
236
+ * Resolve the context window for a transcript usage entry. Prefers the
237
+ * explicit `modelContextWindowTokens(model)` mapping; when the observed token
238
+ * count contradicts it (tokens exceed the mapped window), the model must be
239
+ * ≥1M, so bump to the 1M window.
240
+ */
241
+ function resolveContextWindowTokens(model, contextTokens) {
242
+ const window = modelContextWindowTokens(model);
243
+ return contextTokens > window ? ONE_MILLION_CONTEXT_TOKENS : window;
244
+ }
245
+ /**
246
+ * Conservative transcript-estimate fallback. Recursively searches
247
+ * `~/.claude/projects/<hash>/<outerSessionId-or-nested>.jsonl` (Mac may nest
248
+ * the jsonl under an extra directory we cannot predict ahead of time) and
249
+ * estimates `ratio = contextTokens / contextWindowTokens` from the LATEST
250
+ * `message.usage` entry — token-based + model-aware, NOT the old
251
+ * `bytes / 256KB` (which over-fired because the transcript grows unboundedly).
252
+ * Tagged `'transcript-estimate'` (v2.14.0) so callers know it is a real
253
+ * signal, NOT a hard gate.
254
+ */
255
+ function readClaudeTranscriptEstimate(outerSessionId) {
256
+ const projectsDir = join(homedir(), '.claude', 'projects');
257
+ const path = findTranscriptJsonl(projectsDir, outerSessionId);
258
+ if (path === null)
259
+ return null;
260
+ const latest = findLatestTranscriptUsage(path);
261
+ if (latest === null)
262
+ return null;
263
+ const contextWindowTokens = resolveContextWindowTokens(latest.model, latest.contextTokens);
264
+ const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
265
+ return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
266
+ }
267
+ /**
268
+ * Claude Code's vendor-specific context-percent fallback, exposed as
269
+ * `IdeCompactProfile.readContextPercentFallback`. The generic reader calls
270
+ * this only when the primary env-var probe misses; only the adapter knows the
271
+ * Claude-specific statusline + transcript paths. Resolution order:
272
+ * 1. statusline poll (`~/.claude/statusline-state.json`)
273
+ * 2. transcript estimate — looks up `<outerSessionId>.jsonl` under
274
+ * `~/.claude/projects/<hash>/...` using the OUTER session id (Claude
275
+ * names its transcript by the outer session UUID, not the peaks sid),
276
+ * and estimates `contextTokens / contextWindowTokens` from the LATEST
277
+ * `message.usage` entry (token-based + model-aware).
278
+ * Returns `null` when neither yields a signal → the reader emits
279
+ * `conservative-fallback`.
280
+ */
281
+ function readContextPercentFallback(input) {
282
+ const capturedAt = new Date().toISOString();
283
+ // Byte-based capacity is carried only for the percent path (statusline-poll
284
+ // returns a 0..1 ratio; capacityBytes is metadata there). The
285
+ // transcript-estimate path is token-based and surfaces `capacityTokens`
286
+ // (the model window) instead — see readClaudeTranscriptEstimate.
287
+ const capacityBytes = 256 * 1024;
288
+ const ide = 'claude-code';
289
+ const statusline = readClaudeStatuslinePercent();
290
+ if (statusline !== null) {
291
+ return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide, capturedAt };
292
+ }
293
+ if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
294
+ const estimate = readClaudeTranscriptEstimate(input.outerSessionId);
295
+ if (estimate !== null) {
296
+ return {
297
+ ratio: estimate.ratio,
298
+ source: 'transcript-estimate',
299
+ rawTokens: estimate.contextTokens,
300
+ capacityTokens: estimate.contextWindowTokens,
301
+ ide,
302
+ capturedAt
303
+ };
304
+ }
305
+ }
306
+ return null;
307
+ }
20
308
  export const CLAUDE_CODE_ADAPTER = {
21
309
  id: 'claude-code',
22
310
  displayName: 'Claude Code',
@@ -71,7 +359,8 @@ export const CLAUDE_CODE_ADAPTER = {
71
359
  envVarForContextPercent: 'CLAUDE_CONTEXT_USAGE_PERCENT',
72
360
  compactCommand: 'claude --compact',
73
361
  compactPathway: 'ide-native',
74
- postCompactDetectCommand: 'peaks compact auto --json'
362
+ postCompactDetectCommand: 'peaks compact auto --json',
363
+ readContextPercentFallback
75
364
  },
76
365
  // Slice #011: standards profile. Claude Code reads its constitution at
77
366
  // CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
@@ -12,6 +12,7 @@
12
12
  * 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。
13
13
  */
14
14
  import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js';
15
+ import type { ContextPercentProbe } from '../context/auto-compact-types.js';
15
16
  export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma' | 'hermes' | 'openclaw' | 'zcode';
16
17
  export interface IdeCapabilities {
17
18
  /** peaks gate enforce 是否适用该 IDE(必备) */
@@ -200,6 +201,44 @@ export interface IdeCompactProfile {
200
201
  * the orchestrator polls `envVarForContextPercent` directly.
201
202
  */
202
203
  readonly postCompactDetectCommand?: string;
204
+ /**
205
+ * Optional vendor-specific fallback the generic `readContextPercent`
206
+ * reader calls when the primary env-var probe misses. Returns a
207
+ * completed `ContextPercentProbe` (e.g. `statusline-poll` /
208
+ * `transcript-estimate`) or `null` when the adapter has no signal —
209
+ * the reader then falls through to `conservative-fallback`.
210
+ *
211
+ * Keeps IDE-specific filesystem / env knowledge (Claude Code's
212
+ * `~/.claude/statusline-state.json` + `~/.claude/projects` transcript
213
+ * layout, etc.) inside the adapter, not the generic reader. Adapters
214
+ * that do not opt in simply omit the field; new IDEs are addable
215
+ * without touching the generic reader.
216
+ *
217
+ * Added in slice 2026-09-02-vendor-neutral-context-probe.
218
+ */
219
+ readonly readContextPercentFallback?: (input: ContextPercentFallbackInput) => ContextPercentProbe | null;
220
+ }
221
+ /**
222
+ * Input the generic `readContextPercent` reader passes to an adapter's
223
+ * optional `IdeCompactProfile.readContextPercentFallback` hook. The adapter
224
+ * owns the vendor-specific fallback logic (statusline poll, transcript
225
+ * lookup, etc.); the generic reader stays IDE-agnostic.
226
+ *
227
+ * Added in slice 2026-09-02-vendor-neutral-context-probe.
228
+ */
229
+ export interface ContextPercentFallbackInput {
230
+ /** Project root (the probe's `--project` anchor). */
231
+ readonly projectRoot: string;
232
+ /** Peaks session id (NOT the harness / IDE transcript id). */
233
+ readonly sessionId: string;
234
+ /**
235
+ * Outer (harness / IDE) session id — the id the IDE uses to name its
236
+ * transcript / session files (e.g. a UUID). Optional: when unresolved,
237
+ * adapters whose fallback depends on it should return `null`.
238
+ */
239
+ readonly outerSessionId?: string | undefined;
240
+ /** Injectable env (defaults to process.env in the reader). */
241
+ readonly env?: NodeJS.ProcessEnv | undefined;
203
242
  }
204
243
  /**
205
244
  * Per-IDE standards-file location + format profile. Used by the
@@ -38,6 +38,17 @@ export type BindingStatusView = {
38
38
  * helper that does no filesystem writes; safe to call from any test.
39
39
  */
40
40
  export declare function loadBindingStatus(projectRoot: string): BindingStatusView;
41
+ export declare function readOuterSessionId(): string;
42
+ /**
43
+ * Resolve the outer (harness / IDE) session id for the auto-compact
44
+ * context-percent probe. Order: env signal first (PEAKS_OUTER_SESSION_ID /
45
+ * CLAUDE_CODE_SESSION_ID), then the bound peaks session's recorded
46
+ * `outerSessionId` (`session.json` meta). Returns `undefined` when neither is
47
+ * available — the caller passes `undefined` through and the adapter's fallback
48
+ * returns null → conservative-fallback. Vendor-neutral: this helper has no
49
+ * IDE-specific naming; the env-var read lives alongside the binding layer.
50
+ */
51
+ export declare function resolveOuterSessionId(projectRoot: string, sessionId: string, env?: NodeJS.ProcessEnv): string | undefined;
41
52
  /**
42
53
  * Render the binding as a pipeable ASCII table. The columns are fixed
43
54
  * (no truncation, no wrapping) so a downstream `awk` / `cut` pipeline
@@ -22,6 +22,7 @@
22
22
  import { existsSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
24
  import { readBinding } from './binding-store.js';
25
+ import { getSessionMeta } from './session-manager.js';
25
26
  /**
26
27
  * Read the binding from disk and assemble the read-only view. Pure
27
28
  * helper that does no filesystem writes; safe to call from any test.
@@ -37,14 +38,34 @@ export function loadBindingStatus(projectRoot) {
37
38
  : !Object.values(binding.instances).some((inst) => inst.callerId.startsWith(outerSessionId));
38
39
  return { binding, source, projectRoot, stale, outerSessionId };
39
40
  }
40
- function readOuterSessionId() {
41
- const peaks = process.env.PEAKS_OUTER_SESSION_ID;
41
+ export function readOuterSessionId() {
42
+ return readOuterSessionIdFromEnv(process.env) ?? 'unknown';
43
+ }
44
+ /** Pure env read (no process.env dependency) so callers can inject an env. */
45
+ function readOuterSessionIdFromEnv(env) {
46
+ const peaks = env.PEAKS_OUTER_SESSION_ID;
42
47
  if (typeof peaks === 'string' && peaks.length > 0)
43
48
  return peaks;
44
- const claude = process.env.CLAUDE_CODE_SESSION_ID;
49
+ const claude = env.CLAUDE_CODE_SESSION_ID;
45
50
  if (typeof claude === 'string' && claude.length > 0)
46
51
  return claude;
47
- return 'unknown';
52
+ return undefined;
53
+ }
54
+ /**
55
+ * Resolve the outer (harness / IDE) session id for the auto-compact
56
+ * context-percent probe. Order: env signal first (PEAKS_OUTER_SESSION_ID /
57
+ * CLAUDE_CODE_SESSION_ID), then the bound peaks session's recorded
58
+ * `outerSessionId` (`session.json` meta). Returns `undefined` when neither is
59
+ * available — the caller passes `undefined` through and the adapter's fallback
60
+ * returns null → conservative-fallback. Vendor-neutral: this helper has no
61
+ * IDE-specific naming; the env-var read lives alongside the binding layer.
62
+ */
63
+ export function resolveOuterSessionId(projectRoot, sessionId, env = process.env) {
64
+ const envOuter = readOuterSessionIdFromEnv(env);
65
+ if (envOuter !== undefined)
66
+ return envOuter;
67
+ const recorded = getSessionMeta(projectRoot, sessionId)?.outerSessionId;
68
+ return typeof recorded === 'string' && recorded.length > 0 ? recorded : undefined;
48
69
  }
49
70
  /**
50
71
  * Render the binding as a pipeable ASCII table. The columns are fixed
@@ -32,9 +32,9 @@ export declare const SkillSearchInputSchema: z.ZodObject<{
32
32
  status: "status";
33
33
  resume: "resume";
34
34
  audit: "audit";
35
+ ide: "ide";
35
36
  content: "content";
36
37
  test: "test";
37
- ide: "ide";
38
38
  research: "research";
39
39
  doctor: "doctor";
40
40
  triage: "triage";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.26",
3
+ "version": "4.0.27",
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-internal-runtime": "0.0.11",
105
- "peaks-loop-shared": "0.0.60",
106
- "peaks-loop-shared-channel": "0.0.28",
107
- "peaks-loop-mut": "0.1.24"
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"
108
108
  },
109
109
  "devDependencies": {
110
110
  "@changesets/cli": "2.31.1",