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
|
@@ -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
|
-
*
|
|
165
|
-
*
|
|
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
|
}
|
|
@@ -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
|
-
*
|
|
10
|
-
* codegraph
|
|
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
|
|
14
|
-
*
|
|
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
|
-
*
|
|
10
|
-
* codegraph
|
|
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
|
|
14
|
-
*
|
|
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
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
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
|
-
*
|
|
115
|
-
*
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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,18 +1,25 @@
|
|
|
1
1
|
import type { IdeAdapter } from '../ide-types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Claude Code
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
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;
|
|
9
|
+
/**
|
|
10
|
+
* Model-aware context-window size in tokens.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Detection rule (documented):
|
|
13
|
+
* 1. Empty / unknown model → DEFAULT_CONTEXT_WINDOW_TOKENS (200_000).
|
|
14
|
+
* 2. Suffix heuristic — a model id containing `1m` (case-insensitive) is
|
|
15
|
+
* treated as 1M-context.
|
|
16
|
+
* 3. Explicit allowlist — known 1M Claude model ids
|
|
17
|
+
* (ONE_MILLION_CONTEXT_MODELS).
|
|
18
|
+
* 4. Everything else → 200_000 (the safe default).
|
|
15
19
|
*
|
|
16
|
-
*
|
|
20
|
+
* Callers MAY additionally infer ≥1M from the observed token count: if
|
|
21
|
+
* `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
|
|
22
|
+
* 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
|
|
17
23
|
*/
|
|
24
|
+
export declare function modelContextWindowTokens(model: string): number;
|
|
18
25
|
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,346 @@ 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
|
+
* 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
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Model-aware context-window size in tokens.
|
|
152
|
+
*
|
|
153
|
+
* Detection rule (documented):
|
|
154
|
+
* 1. Empty / unknown model → DEFAULT_CONTEXT_WINDOW_TOKENS (200_000).
|
|
155
|
+
* 2. Suffix heuristic — a model id containing `1m` (case-insensitive) is
|
|
156
|
+
* treated as 1M-context.
|
|
157
|
+
* 3. Explicit allowlist — known 1M Claude model ids
|
|
158
|
+
* (ONE_MILLION_CONTEXT_MODELS).
|
|
159
|
+
* 4. Everything else → 200_000 (the safe default).
|
|
160
|
+
*
|
|
161
|
+
* Callers MAY additionally infer ≥1M from the observed token count: if
|
|
162
|
+
* `contextTokens > DEFAULT_CONTEXT_WINDOW_TOKENS`, the model cannot be a
|
|
163
|
+
* 200K model and must be ≥1M (see `readClaudeTranscriptEstimate`).
|
|
164
|
+
*/
|
|
165
|
+
export function modelContextWindowTokens(model) {
|
|
166
|
+
const m = model.trim().toLowerCase();
|
|
167
|
+
if (m.length === 0)
|
|
168
|
+
return DEFAULT_CONTEXT_WINDOW_TOKENS;
|
|
169
|
+
if (m.includes('1m'))
|
|
170
|
+
return ONE_MILLION_CONTEXT_TOKENS;
|
|
171
|
+
for (const known of ONE_MILLION_CONTEXT_MODELS) {
|
|
172
|
+
if (m.includes(known))
|
|
173
|
+
return ONE_MILLION_CONTEXT_TOKENS;
|
|
174
|
+
}
|
|
175
|
+
return DEFAULT_CONTEXT_WINDOW_TOKENS;
|
|
176
|
+
}
|
|
177
|
+
/** A non-negative finite number, or null when the value is not numeric. */
|
|
178
|
+
function numericTokenCount(value) {
|
|
179
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
180
|
+
return value;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Parse a single jsonl line into its token count + model id. Returns null
|
|
185
|
+
* when the line has no `message.usage` object with numeric token fields.
|
|
186
|
+
*/
|
|
187
|
+
function parseTranscriptUsageLine(line) {
|
|
188
|
+
if (line.length === 0)
|
|
189
|
+
return null;
|
|
190
|
+
let json;
|
|
191
|
+
try {
|
|
192
|
+
json = JSON.parse(line);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null; // non-JSON line (blank / corrupt) — skip
|
|
196
|
+
}
|
|
197
|
+
if (typeof json !== 'object' || json === null)
|
|
198
|
+
return null;
|
|
199
|
+
const record = json;
|
|
200
|
+
const message = record.message;
|
|
201
|
+
if (typeof message !== 'object' || message === null)
|
|
202
|
+
return null;
|
|
203
|
+
const msg = message;
|
|
204
|
+
const usage = msg.usage;
|
|
205
|
+
if (typeof usage !== 'object' || usage === null)
|
|
206
|
+
return null;
|
|
207
|
+
const u = usage;
|
|
208
|
+
const inputTokens = numericTokenCount(u.input_tokens);
|
|
209
|
+
const cacheRead = numericTokenCount(u.cache_read_input_tokens);
|
|
210
|
+
const cacheCreation = numericTokenCount(u.cache_creation_input_tokens);
|
|
211
|
+
if (inputTokens === null && cacheRead === null && cacheCreation === null)
|
|
212
|
+
return null;
|
|
213
|
+
const contextTokens = (inputTokens ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0);
|
|
214
|
+
// Model id lives at `message.model`, falling back to a top-level `model`.
|
|
215
|
+
const model = typeof msg.model === 'string'
|
|
216
|
+
? msg.model
|
|
217
|
+
: typeof record.model === 'string' ? record.model : '';
|
|
218
|
+
return { contextTokens, model };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Reverse-scan the transcript jsonl (from the END) for the LATEST entry that
|
|
222
|
+
* carries a numeric `message.usage`. The file can be many MB; it is read in
|
|
223
|
+
* backward chunks of TRANSCRIPT_SCAN_CHUNK_BYTES — never fully into memory —
|
|
224
|
+
* and stops at the first (newest) usable entry.
|
|
225
|
+
*/
|
|
226
|
+
function findLatestTranscriptUsage(filePath) {
|
|
227
|
+
let fd = null;
|
|
228
|
+
try {
|
|
229
|
+
const size = statSync(filePath).size;
|
|
230
|
+
if (size === 0)
|
|
231
|
+
return null;
|
|
232
|
+
fd = openSync(filePath, 'r');
|
|
233
|
+
let position = size;
|
|
234
|
+
let carry = ''; // partial line head carried into the next (older) chunk
|
|
235
|
+
while (position > 0) {
|
|
236
|
+
const readLen = Math.min(TRANSCRIPT_SCAN_CHUNK_BYTES, position);
|
|
237
|
+
position -= readLen;
|
|
238
|
+
const buf = Buffer.alloc(readLen);
|
|
239
|
+
const bytesRead = readSync(fd, buf, 0, readLen, position);
|
|
240
|
+
if (bytesRead <= 0)
|
|
241
|
+
break;
|
|
242
|
+
const lines = (buf.toString('utf8', 0, bytesRead) + carry).split('\n');
|
|
243
|
+
carry = lines[0] ?? '';
|
|
244
|
+
for (let i = lines.length - 1; i >= 1; i--) {
|
|
245
|
+
const line = lines[i];
|
|
246
|
+
if (line === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
const parsed = parseTranscriptUsageLine(line);
|
|
249
|
+
if (parsed !== null)
|
|
250
|
+
return parsed;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// The final carry is the first line of the file (complete, since it starts at byte 0).
|
|
254
|
+
if (carry.length > 0) {
|
|
255
|
+
const parsed = parseTranscriptUsageLine(carry);
|
|
256
|
+
if (parsed !== null)
|
|
257
|
+
return parsed;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
// Narrow: surface module-load / parse bugs, swallow IO errors only
|
|
263
|
+
// (mirrors the other adapter read helpers' catch discipline).
|
|
264
|
+
if (err instanceof ReferenceError)
|
|
265
|
+
throw err;
|
|
266
|
+
if (err instanceof SyntaxError)
|
|
267
|
+
throw err;
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
if (fd !== null) {
|
|
272
|
+
try {
|
|
273
|
+
closeSync(fd);
|
|
274
|
+
}
|
|
275
|
+
catch { /* best-effort */ }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Resolve the context window for a transcript usage entry. Prefers the
|
|
281
|
+
* explicit `modelContextWindowTokens(model)` mapping; when the observed token
|
|
282
|
+
* count contradicts it (tokens exceed the mapped window), the model must be
|
|
283
|
+
* ≥1M, so bump to the 1M window.
|
|
284
|
+
*/
|
|
285
|
+
function resolveContextWindowTokens(model, contextTokens) {
|
|
286
|
+
const window = modelContextWindowTokens(model);
|
|
287
|
+
return contextTokens > window ? ONE_MILLION_CONTEXT_TOKENS : window;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Conservative transcript-estimate fallback. Recursively searches
|
|
291
|
+
* `~/.claude/projects/<hash>/<outerSessionId-or-nested>.jsonl` (Mac may nest
|
|
292
|
+
* the jsonl under an extra directory we cannot predict ahead of time) and
|
|
293
|
+
* estimates `ratio = contextTokens / contextWindowTokens` from the LATEST
|
|
294
|
+
* `message.usage` entry — token-based + model-aware, NOT the old
|
|
295
|
+
* `bytes / 256KB` (which over-fired because the transcript grows unboundedly).
|
|
296
|
+
* Tagged `'transcript-estimate'` (v2.14.0) so callers know it is a real
|
|
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.
|
|
302
|
+
*/
|
|
303
|
+
function readClaudeTranscriptEstimate(outerSessionId, envModel) {
|
|
304
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
305
|
+
const path = findTranscriptJsonl(projectsDir, outerSessionId);
|
|
306
|
+
if (path === null)
|
|
307
|
+
return null;
|
|
308
|
+
const latest = findLatestTranscriptUsage(path);
|
|
309
|
+
if (latest === null)
|
|
310
|
+
return null;
|
|
311
|
+
const model = envModel !== undefined && envModel.trim().length > 0 ? envModel : latest.model;
|
|
312
|
+
const contextWindowTokens = resolveContextWindowTokens(model, latest.contextTokens);
|
|
313
|
+
const ratio = Math.min(1, latest.contextTokens / contextWindowTokens);
|
|
314
|
+
return { ratio, contextTokens: latest.contextTokens, contextWindowTokens };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Claude Code's vendor-specific context-percent fallback, exposed as
|
|
318
|
+
* `IdeCompactProfile.readContextPercentFallback`. The generic reader calls
|
|
319
|
+
* this only when the primary env-var probe misses; only the adapter knows the
|
|
320
|
+
* Claude-specific statusline + transcript paths. Resolution order:
|
|
321
|
+
* 1. statusline poll (`~/.claude/statusline-state.json`)
|
|
322
|
+
* 2. transcript estimate — looks up `<outerSessionId>.jsonl` under
|
|
323
|
+
* `~/.claude/projects/<hash>/...` using the OUTER session id (Claude
|
|
324
|
+
* names its transcript by the outer session UUID, not the peaks sid),
|
|
325
|
+
* and estimates `contextTokens / contextWindowTokens` from the LATEST
|
|
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.
|
|
330
|
+
* Returns `null` when neither yields a signal → the reader emits
|
|
331
|
+
* `conservative-fallback`.
|
|
332
|
+
*/
|
|
333
|
+
function readContextPercentFallback(input) {
|
|
334
|
+
const capturedAt = new Date().toISOString();
|
|
335
|
+
// Byte-based capacity is carried only for the percent path (statusline-poll
|
|
336
|
+
// returns a 0..1 ratio; capacityBytes is metadata there). The
|
|
337
|
+
// transcript-estimate path is token-based and surfaces `capacityTokens`
|
|
338
|
+
// (the model window) instead — see readClaudeTranscriptEstimate.
|
|
339
|
+
const capacityBytes = 256 * 1024;
|
|
340
|
+
const ide = 'claude-code';
|
|
341
|
+
const statusline = readClaudeStatuslinePercent();
|
|
342
|
+
if (statusline !== null) {
|
|
343
|
+
return { ratio: statusline, source: 'statusline-poll', capacityBytes, ide, capturedAt };
|
|
344
|
+
}
|
|
345
|
+
if (typeof input.outerSessionId === 'string' && input.outerSessionId.length > 0) {
|
|
346
|
+
const envModel = resolveClaudeModelFromEnv(input.env);
|
|
347
|
+
const estimate = readClaudeTranscriptEstimate(input.outerSessionId, envModel);
|
|
348
|
+
if (estimate !== null) {
|
|
349
|
+
return {
|
|
350
|
+
ratio: estimate.ratio,
|
|
351
|
+
source: 'transcript-estimate',
|
|
352
|
+
rawTokens: estimate.contextTokens,
|
|
353
|
+
capacityTokens: estimate.contextWindowTokens,
|
|
354
|
+
ide,
|
|
355
|
+
capturedAt
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
20
361
|
export const CLAUDE_CODE_ADAPTER = {
|
|
21
362
|
id: 'claude-code',
|
|
22
363
|
displayName: 'Claude Code',
|
|
@@ -71,7 +412,8 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
71
412
|
envVarForContextPercent: 'CLAUDE_CONTEXT_USAGE_PERCENT',
|
|
72
413
|
compactCommand: 'claude --compact',
|
|
73
414
|
compactPathway: 'ide-native',
|
|
74
|
-
postCompactDetectCommand: 'peaks compact auto --json'
|
|
415
|
+
postCompactDetectCommand: 'peaks compact auto --json',
|
|
416
|
+
readContextPercentFallback
|
|
75
417
|
},
|
|
76
418
|
// Slice #011: standards profile. Claude Code reads its constitution at
|
|
77
419
|
// 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
|