peaks-loop 4.0.26 → 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 +24 -0
- package/dist/cli/commands/code-runtime-commands.js +7 -1
- package/dist/cli/commands/dispatch-commands.js +3 -0
- package/dist/services/code/auto-compact-orchestrator.js +3 -0
- package/dist/services/codegraph/codegraph-service.d.ts +16 -42
- package/dist/services/codegraph/codegraph-service.js +23 -66
- package/dist/services/context/auto-compact-reader.d.ts +45 -49
- package/dist/services/context/auto-compact-reader.js +21 -148
- package/dist/services/context/auto-compact-types.d.ts +17 -2
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.d.ts +4 -5
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.js +16 -32
- package/dist/services/doctor/doctor-service/types.d.ts +11 -15
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +19 -12
- package/dist/services/ide/adapters/claude-code-adapter.js +343 -1
- package/dist/services/ide/ide-types.d.ts +39 -0
- package/dist/services/session/binding-status-service.d.ts +11 -0
- package/dist/services/session/binding-status-service.js +25 -4
- package/dist/services/skill/skill-search-service.d.ts +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
17
|
+
## 4.0.27 — 2026-09-01 (auto-compact context probe fix)
|
|
18
|
+
|
|
19
|
+
**1 atomic commit from session 2026-09-01-session-fdd7aa**:
|
|
20
|
+
|
|
21
|
+
- `7f0f4ca2` perf(auto-compact): vendor-neutral context probe + token-based transcript calibration
|
|
22
|
+
|
|
23
|
+
**Highlights**:
|
|
24
|
+
|
|
25
|
+
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%)。
|
|
26
|
+
|
|
3
27
|
## 4.0.26 — 2026-09-01 (headroom removal + prompt-cache prefix alignment)
|
|
4
28
|
|
|
5
29
|
**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
|
|
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({
|
|
@@ -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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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: '
|
|
65
|
-
|
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
//
|
|
147
|
-
//
|
|
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
|
-
*
|
|
169
|
-
* root `.codegraph/` fallback (spike follow-up #1).
|
|
166
|
+
* codegraph data-directory resolution — root `.codegraph/` only.
|
|
170
167
|
* ────────────────────────────────────────────────────────────────────── */
|
|
171
168
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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: '
|
|
230
|
-
cwd:
|
|
231
|
-
codegraphDir:
|
|
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
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
240
|
+
return inspectCandidateCodegraphDir(codegraphDir);
|
|
284
241
|
}
|
|
285
242
|
/**
|
|
286
243
|
* Pure-fs helper that stamps the peaks-loop marker AFTER a
|
|
@@ -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
|
|
11
|
-
*
|
|
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
|
-
* `
|
|
65
|
-
* adapter-declared env-var.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* with `ratio: 0` so the
|
|
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
|
-
* `
|
|
141
|
-
* adapter-declared env-var.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* with `ratio: 0` so the
|
|
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 /
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
92
|
+
export const _internal = { readEnvPercent };
|