@mnemom/mnemom 0.17.4-next.3 → 0.17.4-next.4

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.
@@ -116,6 +116,118 @@ export declare function sanitizeAgentName(raw: string): string;
116
116
  * post`) can identify themselves by the same name the claude.ai session list shows.
117
117
  */
118
118
  export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
119
+ /**
120
+ * Claude Code's tool-search switch. Behind an ANTHROPIC_BASE_URL that is not an Anthropic host
121
+ * (the gateway door), Claude Code turns tool search OFF unless this is set, and then inlines
122
+ * every tool schema — MCP included — on every request (EIM: 311 tools, a ~245k-token fixed
123
+ * prefix, a 200k session that starts at "0% left" and compaction that can never get under it).
124
+ * Claude Code 2.1.282 / 2.1.285 log `[ToolSearch:optimistic] disabled: ANTHROPIC_BASE_URL=… is
125
+ * not a first-party Anthropic host. Set ENABLE_TOOL_SEARCH=true (or auto / auto:N) if your proxy
126
+ * forwards tool_reference blocks.` and parse: `force`, `auto`, `auto:N` (0–100), a truthy value
127
+ * (1/true/yes/on) ⇒ tool search on, a falsy one (0/false/no/off) ⇒ off; empty/unset ⇒ off behind
128
+ * a proxy.
129
+ */
130
+ export declare const TOOL_SEARCH_ENV = "ENABLE_TOOL_SEARCH";
131
+ /**
132
+ * Turn Claude Code's tool search on for a gateway launch: sets ENABLE_TOOL_SEARCH=true in the
133
+ * child env unless the user already gave it a value (any non-blank value — `false`, `auto:50`,
134
+ * `force` — is theirs and is kept). A blank value counts as unset: Claude Code reads it as unset
135
+ * too. Mutates and returns `env`.
136
+ */
137
+ export declare function applyToolSearchDefault(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
138
+ /** The value of a passthrough `--model <id>` / `--model=<id>`, else null. */
139
+ export declare function passthroughModel(passthrough: readonly string[]): string | null;
140
+ /** Where a launch's model came from (resolveLaunchModel). */
141
+ export type LaunchModelSource = "flag" | "passthrough" | "env" | "none";
142
+ export interface LaunchModel {
143
+ /** The model this launch runs on as far as the launcher can tell (the ledger records it). */
144
+ model: string | null;
145
+ source: LaunchModelSource;
146
+ }
147
+ /**
148
+ * The model a launch runs on, kept VERBATIM — a `[1m]` suffix included, which is what makes
149
+ * Claude Code send the `context-1m-2025-08-07` beta and size its window at 1M (verified against
150
+ * real claude 2.1.285 behind a local base URL, for `--model` and for ANTHROPIC_MODEL):
151
+ * 1. `--model` (or launcher.toml `model`) → what buildCliArgs injects / the server shape's
152
+ * ANTHROPIC_MODEL;
153
+ * 2. a passthrough `-- --model <id>` → already on the CLI's argv;
154
+ * 3. ANTHROPIC_MODEL in the environment → every launch shape's child inherits it;
155
+ * 4. nothing → Claude Code's own default (on `--resume` Claude Code restores the session's
156
+ * own model, `[1m]` included), recorded as null.
157
+ * Only `--model` is ever injected; the rest is what the ledger records so `mnemom agent
158
+ * sessions` shows the window a session ran with instead of null.
159
+ */
160
+ export declare function resolveLaunchModel(input: {
161
+ explicit?: string;
162
+ passthrough: readonly string[];
163
+ env: Record<string, string | undefined>;
164
+ }): LaunchModel;
165
+ /**
166
+ * Claude Code's own 1M opt-out. Its window code short-circuits on it (`function xD(){return
167
+ * a.CLAUDE_CODE_DISABLE_1M_CONTEXT}`; every `[1m]` path checks `if(xD())return!1` first, and a
168
+ * `[1m]` model switch under it fails with "1M context is turned off (CLAUDE_CODE_DISABLE_1M_CONTEXT
169
+ * is set)"). Set in the environment or in a settings `env` block, it is the user's explicit
170
+ * 200k choice and the launcher then adds no `[1m]`.
171
+ */
172
+ export declare const DISABLE_1M_ENV = "CLAUDE_CODE_DISABLE_1M_CONTEXT";
173
+ /** The window suffix Claude Code reads off a model id (`/\[1m\]/i` → 1M + `context-1m` beta). */
174
+ export declare const ONE_M_SUFFIX = "[1m]";
175
+ /**
176
+ * Whether `[1m]` may be appended to `model`. Claude Code does NOT check this itself: given
177
+ * `--model haiku[1m]` or `default[1m]` it sends the 1M beta (and, for `default`, a model id of
178
+ * `default`) upstream (measured, claude 2.1.282 / 2.1.285). So the launcher allows only the
179
+ * `opus` / `sonnet` aliases and concrete `claude-opus-*` / `claude-sonnet-*` ids, minus the ids
180
+ * Claude Code itself lists as having no 1M window (`function mde(e){return
181
+ * e.includes("claude-3-")||e==="claude-opus-4-0"||e==="claude-opus-4-1"||e==="claude-opus-4-5"||
182
+ * e==="claude-haiku-4-5"}`) — `claude-opus-4` / `-4-0` / `-4-1` / `-4-5`, dated or not. Anything
183
+ * else (haiku, non-Claude router models, unknown families) is left as given.
184
+ */
185
+ export declare function supportsOneM(model: string): boolean;
186
+ /** The parts of Claude Code's settings files the window default reads. */
187
+ export interface CcSettingsView {
188
+ model?: string;
189
+ env?: Record<string, string | undefined>;
190
+ }
191
+ /**
192
+ * Claude Code's settings files, highest precedence first: `<cwd>/.claude/settings.local.json`,
193
+ * `<cwd>/.claude/settings.json`, then the user's `settings.json` under CLAUDE_CONFIG_DIR (else
194
+ * `~/.claude`). Unreadable or malformed files are skipped. Managed (MDM) settings are not read.
195
+ */
196
+ export declare function readCcSettings(opts: {
197
+ cwd: string;
198
+ configDir: string;
199
+ readFile?: (path: string) => string;
200
+ }): CcSettingsView[];
201
+ /** Why the launch model came out the way it did (resolveWindowModel). */
202
+ export type WindowReason = "one-m" | "opt-out" | "passthrough" | "suffixed" | "unsupported" | "resume" | "cc-default";
203
+ export interface WindowModel {
204
+ /** The model to launch with (`--model`, or the server shape's ANTHROPIC_MODEL); undefined = none. */
205
+ model: string | undefined;
206
+ reason: WindowReason;
207
+ /** The model before `[1m]` was appended (reason "one-m" only). */
208
+ from?: string;
209
+ }
210
+ /**
211
+ * The launch model with the 1M window as the default. The model the launch would run on — the
212
+ * `--model` flag (or launcher.toml `model`), else an inherited ANTHROPIC_MODEL, else Claude Code's
213
+ * settings (`env.ANTHROPIC_MODEL`, then `model`) — gets `[1m]` appended when it has no window
214
+ * suffix and supports 1M. Claude Code reads the window off the id: `[1m]` ⇒ a 1M window and the
215
+ * `context-1m-2025-08-07` beta (`function ift(e){return/\[1m\]/i.test(e)}`; `Ob()` returns 1e6
216
+ * when it matches). Without it, claude 2.1.282 runs a bare `opus` / `claude-opus-4-8` at 200k on
217
+ * both auth paths (the EIM thrash window); 2.1.285 already gives those ids 1M, where `[1m]` is
218
+ * harmless.
219
+ *
220
+ * Never overrides an explicit choice: CLAUDE_CODE_DISABLE_1M_CONTEXT (env or settings env), a
221
+ * passthrough `-- --model`, and any model that already names a window are left exactly as
222
+ * given. A resume with no model injects nothing (Claude Code restores the session's model,
223
+ * `[1m]` included), and so does a launch with no model anywhere (Claude Code's default).
224
+ */
225
+ export declare function resolveWindowModel(input: {
226
+ explicit?: string;
227
+ passthrough: readonly string[];
228
+ env: Record<string, string | undefined>;
229
+ settings: () => CcSettingsView[];
230
+ }): WindowModel;
119
231
  /**
120
232
  * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
121
233
  * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
package/dist/lib/agent.js CHANGED
@@ -20,6 +20,8 @@
20
20
  * NAMED), but prod is never selected silently — see `resolveGatewayHost`.
21
21
  */
22
22
  import { createHash, randomUUID } from "node:crypto";
23
+ import { readFileSync } from "node:fs";
24
+ import { join } from "node:path";
23
25
  import { getGatewayUrl } from "./config.js";
24
26
  /** The us-2/prod gateway host. The default door — never any other cell silently. */
25
27
  export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
@@ -117,6 +119,188 @@ export function sanitizeAgentName(raw) {
117
119
  * post`) can identify themselves by the same name the claude.ai session list shows.
118
120
  */
119
121
  export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
122
+ /**
123
+ * Claude Code's tool-search switch. Behind an ANTHROPIC_BASE_URL that is not an Anthropic host
124
+ * (the gateway door), Claude Code turns tool search OFF unless this is set, and then inlines
125
+ * every tool schema — MCP included — on every request (EIM: 311 tools, a ~245k-token fixed
126
+ * prefix, a 200k session that starts at "0% left" and compaction that can never get under it).
127
+ * Claude Code 2.1.282 / 2.1.285 log `[ToolSearch:optimistic] disabled: ANTHROPIC_BASE_URL=… is
128
+ * not a first-party Anthropic host. Set ENABLE_TOOL_SEARCH=true (or auto / auto:N) if your proxy
129
+ * forwards tool_reference blocks.` and parse: `force`, `auto`, `auto:N` (0–100), a truthy value
130
+ * (1/true/yes/on) ⇒ tool search on, a falsy one (0/false/no/off) ⇒ off; empty/unset ⇒ off behind
131
+ * a proxy.
132
+ */
133
+ export const TOOL_SEARCH_ENV = "ENABLE_TOOL_SEARCH";
134
+ /**
135
+ * Turn Claude Code's tool search on for a gateway launch: sets ENABLE_TOOL_SEARCH=true in the
136
+ * child env unless the user already gave it a value (any non-blank value — `false`, `auto:50`,
137
+ * `force` — is theirs and is kept). A blank value counts as unset: Claude Code reads it as unset
138
+ * too. Mutates and returns `env`.
139
+ */
140
+ export function applyToolSearchDefault(env) {
141
+ if ((env[TOOL_SEARCH_ENV] ?? "").trim() === "")
142
+ env[TOOL_SEARCH_ENV] = "true";
143
+ return env;
144
+ }
145
+ /** The value of a passthrough `--model <id>` / `--model=<id>`, else null. */
146
+ export function passthroughModel(passthrough) {
147
+ for (let i = 0; i < passthrough.length; i++) {
148
+ const a = passthrough[i];
149
+ if (a === "--model") {
150
+ const next = (passthrough[i + 1] ?? "").trim();
151
+ return next && !next.startsWith("-") ? next : null;
152
+ }
153
+ if (a.startsWith("--model="))
154
+ return a.slice("--model=".length).trim() || null;
155
+ }
156
+ return null;
157
+ }
158
+ /**
159
+ * The model a launch runs on, kept VERBATIM — a `[1m]` suffix included, which is what makes
160
+ * Claude Code send the `context-1m-2025-08-07` beta and size its window at 1M (verified against
161
+ * real claude 2.1.285 behind a local base URL, for `--model` and for ANTHROPIC_MODEL):
162
+ * 1. `--model` (or launcher.toml `model`) → what buildCliArgs injects / the server shape's
163
+ * ANTHROPIC_MODEL;
164
+ * 2. a passthrough `-- --model <id>` → already on the CLI's argv;
165
+ * 3. ANTHROPIC_MODEL in the environment → every launch shape's child inherits it;
166
+ * 4. nothing → Claude Code's own default (on `--resume` Claude Code restores the session's
167
+ * own model, `[1m]` included), recorded as null.
168
+ * Only `--model` is ever injected; the rest is what the ledger records so `mnemom agent
169
+ * sessions` shows the window a session ran with instead of null.
170
+ */
171
+ export function resolveLaunchModel(input) {
172
+ const explicit = (input.explicit ?? "").trim();
173
+ if (explicit)
174
+ return { model: explicit, source: "flag" };
175
+ const fromArgv = passthroughModel(input.passthrough);
176
+ if (fromArgv)
177
+ return { model: fromArgv, source: "passthrough" };
178
+ const fromEnv = (input.env.ANTHROPIC_MODEL ?? "").trim();
179
+ if (fromEnv)
180
+ return { model: fromEnv, source: "env" };
181
+ return { model: null, source: "none" };
182
+ }
183
+ /**
184
+ * Claude Code's own 1M opt-out. Its window code short-circuits on it (`function xD(){return
185
+ * a.CLAUDE_CODE_DISABLE_1M_CONTEXT}`; every `[1m]` path checks `if(xD())return!1` first, and a
186
+ * `[1m]` model switch under it fails with "1M context is turned off (CLAUDE_CODE_DISABLE_1M_CONTEXT
187
+ * is set)"). Set in the environment or in a settings `env` block, it is the user's explicit
188
+ * 200k choice and the launcher then adds no `[1m]`.
189
+ */
190
+ export const DISABLE_1M_ENV = "CLAUDE_CODE_DISABLE_1M_CONTEXT";
191
+ /** The window suffix Claude Code reads off a model id (`/\[1m\]/i` → 1M + `context-1m` beta). */
192
+ export const ONE_M_SUFFIX = "[1m]";
193
+ const FALSY = new Set(["0", "false", "no", "off"]);
194
+ /** True when an env-style value turns a switch on: non-blank and not 0/false/no/off. */
195
+ function envOn(v) {
196
+ const t = (v ?? "").trim().toLowerCase();
197
+ return t !== "" && !FALSY.has(t);
198
+ }
199
+ /**
200
+ * Whether `[1m]` may be appended to `model`. Claude Code does NOT check this itself: given
201
+ * `--model haiku[1m]` or `default[1m]` it sends the 1M beta (and, for `default`, a model id of
202
+ * `default`) upstream (measured, claude 2.1.282 / 2.1.285). So the launcher allows only the
203
+ * `opus` / `sonnet` aliases and concrete `claude-opus-*` / `claude-sonnet-*` ids, minus the ids
204
+ * Claude Code itself lists as having no 1M window (`function mde(e){return
205
+ * e.includes("claude-3-")||e==="claude-opus-4-0"||e==="claude-opus-4-1"||e==="claude-opus-4-5"||
206
+ * e==="claude-haiku-4-5"}`) — `claude-opus-4` / `-4-0` / `-4-1` / `-4-5`, dated or not. Anything
207
+ * else (haiku, non-Claude router models, unknown families) is left as given.
208
+ */
209
+ export function supportsOneM(model) {
210
+ const id = model.trim().toLowerCase();
211
+ if (id === "opus" || id === "sonnet")
212
+ return true;
213
+ if (!/^claude-(opus|sonnet)-/.test(id))
214
+ return false;
215
+ return !/^claude-opus-4(-[015])?(-\d{8})?$/.test(id);
216
+ }
217
+ /** True when `model` already names a window (`[1m]`, `[2m]`, any `[…]` suffix): left as given. */
218
+ function hasWindowSuffix(model) {
219
+ return /\[[^\]]*\]\s*$/.test(model);
220
+ }
221
+ /** Passthrough `--resume` / `-r` / `--continue` / `-c`: Claude Code restores the session's model. */
222
+ function resumesSession(passthrough) {
223
+ return passthrough.some((a) => a === "--resume" ||
224
+ a === "-r" ||
225
+ a === "--continue" ||
226
+ a === "-c" ||
227
+ a.startsWith("--resume="));
228
+ }
229
+ /**
230
+ * Claude Code's settings files, highest precedence first: `<cwd>/.claude/settings.local.json`,
231
+ * `<cwd>/.claude/settings.json`, then the user's `settings.json` under CLAUDE_CONFIG_DIR (else
232
+ * `~/.claude`). Unreadable or malformed files are skipped. Managed (MDM) settings are not read.
233
+ */
234
+ export function readCcSettings(opts) {
235
+ const read = opts.readFile ?? ((p) => readFileSync(p, "utf8"));
236
+ const out = [];
237
+ for (const p of [
238
+ join(opts.cwd, ".claude", "settings.local.json"),
239
+ join(opts.cwd, ".claude", "settings.json"),
240
+ join(opts.configDir, "settings.json"),
241
+ ]) {
242
+ try {
243
+ const raw = JSON.parse(read(p));
244
+ if (!raw || typeof raw !== "object")
245
+ continue;
246
+ const o = raw;
247
+ const view = {};
248
+ if (typeof o.model === "string")
249
+ view.model = o.model;
250
+ if (o.env && typeof o.env === "object") {
251
+ view.env = Object.fromEntries(Object.entries(o.env).filter((e) => typeof e[1] === "string"));
252
+ }
253
+ out.push(view);
254
+ }
255
+ catch {
256
+ // absent / unreadable / not JSON: Claude Code ignores it too
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+ /**
262
+ * The launch model with the 1M window as the default. The model the launch would run on — the
263
+ * `--model` flag (or launcher.toml `model`), else an inherited ANTHROPIC_MODEL, else Claude Code's
264
+ * settings (`env.ANTHROPIC_MODEL`, then `model`) — gets `[1m]` appended when it has no window
265
+ * suffix and supports 1M. Claude Code reads the window off the id: `[1m]` ⇒ a 1M window and the
266
+ * `context-1m-2025-08-07` beta (`function ift(e){return/\[1m\]/i.test(e)}`; `Ob()` returns 1e6
267
+ * when it matches). Without it, claude 2.1.282 runs a bare `opus` / `claude-opus-4-8` at 200k on
268
+ * both auth paths (the EIM thrash window); 2.1.285 already gives those ids 1M, where `[1m]` is
269
+ * harmless.
270
+ *
271
+ * Never overrides an explicit choice: CLAUDE_CODE_DISABLE_1M_CONTEXT (env or settings env), a
272
+ * passthrough `-- --model`, and any model that already names a window are left exactly as
273
+ * given. A resume with no model injects nothing (Claude Code restores the session's model,
274
+ * `[1m]` included), and so does a launch with no model anywhere (Claude Code's default).
275
+ */
276
+ export function resolveWindowModel(input) {
277
+ const explicit = (input.explicit ?? "").trim() || undefined;
278
+ const given = (reason) => ({ model: explicit, reason });
279
+ if (passthroughModel(input.passthrough) !== null)
280
+ return given("passthrough");
281
+ let settings;
282
+ const loadSettings = () => (settings ??= input.settings());
283
+ if (envOn(input.env[DISABLE_1M_ENV]) ||
284
+ loadSettings().some((s) => envOn(s.env?.[DISABLE_1M_ENV]))) {
285
+ return given("opt-out");
286
+ }
287
+ let base = explicit ?? ((input.env.ANTHROPIC_MODEL ?? "").trim() || undefined);
288
+ if (base === undefined) {
289
+ if (resumesSession(input.passthrough))
290
+ return given("resume");
291
+ const s = loadSettings();
292
+ base =
293
+ s.map((v) => (v.env?.ANTHROPIC_MODEL ?? "").trim()).find((m) => m !== "") ??
294
+ s.map((v) => (v.model ?? "").trim()).find((m) => m !== "");
295
+ if (base === undefined)
296
+ return given("cc-default");
297
+ }
298
+ if (hasWindowSuffix(base))
299
+ return { model: explicit, reason: "suffixed" };
300
+ if (!supportsOneM(base))
301
+ return { model: explicit, reason: "unsupported" };
302
+ return { model: `${base}${ONE_M_SUFFIX}`, reason: "one-m", from: base };
303
+ }
120
304
  /**
121
305
  * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
122
306
  * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The first-run cost backtest: price each request in a Claude Code transcript at API list
3
+ * price, and estimate what the same request would have cost through Mnemom from the
4
+ * EMPIRICAL saving curve measured on paired stock-vs-Mnemom runs.
5
+ *
6
+ * Read-only; no network. Only token counts, model ids and timestamps are read from a
7
+ * transcript line — never message content.
8
+ *
9
+ * The curve (lower 95% bootstrap bound of the size-matched per-request saving, rounded
10
+ * DOWN to 0.1 pt) comes from the S43 / S44 chain45 and d240p1 marathon paired arms:
11
+ * mnemom-adw scratchpad/wow-first-run/CURVE-REPORT.md, curve/curve.json. The strategy is
12
+ * chosen PER REQUEST from that request's original context (operator policy 2026-09-30):
13
+ *
14
+ * < 200k none (no fold) → 0 on screen (the measured balanced rate there is
15
+ * slightly negative; floored, raw kept for the audit)
16
+ * 200k – 700k aggressive → the measured lower bound of its bin
17
+ * 700k – 1M aggressive → the 400–700k bound, EXTRAPOLATED (no measured pair)
18
+ * ≥ 1M balanced → 0 (live sessions only; a stock history cannot get
19
+ * there because Claude Code compacts first — counted)
20
+ */
21
+ /** One billed request, deduped by message id. `t` is epoch seconds. */
22
+ export interface Row {
23
+ t: number;
24
+ model: string;
25
+ inp: number;
26
+ cr: number;
27
+ cw5: number;
28
+ cw1h: number;
29
+ out: number;
30
+ /** false for a sidechain (Task / subagent) request inside a top-level transcript. */
31
+ mainThread: boolean;
32
+ /** true when the row came from a subagents/ transcript. */
33
+ subagent: boolean;
34
+ }
35
+ /**
36
+ * List prices, USD per MTok: [input, output, cache read, cache write 5m, cache write 1h].
37
+ * Source (verified 2026-09-30): Claude Code 2.1.285's own model registry + pricing table
38
+ * read out of the installed binary (models[].pricing → tier_*). Longest prefix wins; an
39
+ * unknown model is priced as DEFAULT_PRICE.
40
+ */
41
+ export declare const PRICES: Readonly<Record<string, readonly [number, number, number, number, number]>>;
42
+ export declare function price(model: string): readonly [number, number, number, number, number];
43
+ /** List-price cost of one request, USD. */
44
+ export declare function rowCost(r: Row): number;
45
+ export declare function actualBill(rows: readonly Row[]): number;
46
+ /** An ungoverned request's original context = its billed prompt. */
47
+ export declare function origContext(r: Row): number;
48
+ /**
49
+ * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
50
+ * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
51
+ * session copies messages) is skipped, so each request is billed once, to its origin.
52
+ */
53
+ export declare function loadTranscript(path: string, seen?: Set<string>): Row[];
54
+ /** Original-context bin edges (tokens). */
55
+ export declare const CURVE_EDGES: readonly [0, 100000, 200000, 300000, 400000, 700000, 1000000];
56
+ /**
57
+ * Lower 95% bound per bin, rounded down to 0.1 pt (curve/curve.json `strategies`).
58
+ * aggressive: no measured pair below 100k or at/above 700k.
59
+ */
60
+ export declare const CURVE_LOWER: Readonly<Record<"aggressive" | "balanced", Record<number, number>>>;
61
+ export declare const NO_FOLD_BELOW = 200000;
62
+ export declare const EXTRAPOLATE_FROM = 700000;
63
+ export declare const BALANCED_FROM = 1000000;
64
+ export declare const POLICY = "none <200k / aggressive 200k-1M / balanced 1M+ (1M+ applies only to live sessions)";
65
+ export declare const ESTIMATOR_METHOD: string;
66
+ export type Strategy = "none" | "aggressive" | "balanced_1M+";
67
+ export type RateNote = "floored_at_0" | "extrapolated" | "exception_1M_plus" | null;
68
+ /** The curve bin of an original context (0 = below 100k; the top bin is open-ended). */
69
+ export declare function binOf(x: number): number;
70
+ /** The screen never claims a negative saving: a negative measured rate shows as 0. */
71
+ export declare function floorRate(raw: number): {
72
+ screen: number;
73
+ note: "floored_at_0" | null;
74
+ };
75
+ /** The per-request rate the screen uses (`screen`) and the honest measured one (`raw`). */
76
+ export declare function ruleRate(x: number): {
77
+ strategy: Strategy;
78
+ raw: number;
79
+ screen: number;
80
+ note: RateNote;
81
+ };
82
+ export interface Estimate {
83
+ actual: number;
84
+ saved: number;
85
+ /** The saving with the < 200k band NOT floored (audit only; never shown). */
86
+ rawSaved: number;
87
+ peak: number;
88
+ requests: number;
89
+ notes: Partial<Record<Exclude<RateNote, null>, number>>;
90
+ }
91
+ /** The one swap point: rows → list-price actual and the conservative saving. */
92
+ export declare function estimate(rows: readonly Row[]): Estimate;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The first-run cost backtest: price each request in a Claude Code transcript at API list
3
+ * price, and estimate what the same request would have cost through Mnemom from the
4
+ * EMPIRICAL saving curve measured on paired stock-vs-Mnemom runs.
5
+ *
6
+ * Read-only; no network. Only token counts, model ids and timestamps are read from a
7
+ * transcript line — never message content.
8
+ *
9
+ * The curve (lower 95% bootstrap bound of the size-matched per-request saving, rounded
10
+ * DOWN to 0.1 pt) comes from the S43 / S44 chain45 and d240p1 marathon paired arms:
11
+ * mnemom-adw scratchpad/wow-first-run/CURVE-REPORT.md, curve/curve.json. The strategy is
12
+ * chosen PER REQUEST from that request's original context (operator policy 2026-09-30):
13
+ *
14
+ * < 200k none (no fold) → 0 on screen (the measured balanced rate there is
15
+ * slightly negative; floored, raw kept for the audit)
16
+ * 200k – 700k aggressive → the measured lower bound of its bin
17
+ * 700k – 1M aggressive → the 400–700k bound, EXTRAPOLATED (no measured pair)
18
+ * ≥ 1M balanced → 0 (live sessions only; a stock history cannot get
19
+ * there because Claude Code compacts first — counted)
20
+ */
21
+ import { forEachLine, isoSeconds, parseJson } from "./lines.js";
22
+ /**
23
+ * List prices, USD per MTok: [input, output, cache read, cache write 5m, cache write 1h].
24
+ * Source (verified 2026-09-30): Claude Code 2.1.285's own model registry + pricing table
25
+ * read out of the installed binary (models[].pricing → tier_*). Longest prefix wins; an
26
+ * unknown model is priced as DEFAULT_PRICE.
27
+ */
28
+ export const PRICES = {
29
+ "claude-opus-5-5": [4.0, 20.0, 0.2, 5.0, 8.0],
30
+ "claude-opus-5": [5.0, 25.0, 0.5, 6.25, 10.0],
31
+ "claude-opus-4": [5.0, 25.0, 0.5, 6.25, 10.0],
32
+ "claude-opus-4-0": [15.0, 75.0, 1.5, 18.75, 30.0],
33
+ "claude-opus-4-1": [15.0, 75.0, 1.5, 18.75, 30.0],
34
+ "claude-fable-5-1": [10.0, 50.0, 0.25, 12.5, 20.0],
35
+ "claude-fable-5": [10.0, 50.0, 1.0, 12.5, 20.0],
36
+ "claude-mythos": [10.0, 50.0, 0.25, 12.5, 20.0],
37
+ "claude-sonnet-5": [2.0, 10.0, 0.2, 2.5, 4.0],
38
+ "claude-sonnet-4": [3.0, 15.0, 0.3, 3.75, 6.0],
39
+ "claude-haiku-4-5": [1.0, 5.0, 0.1, 1.25, 2.0],
40
+ "claude-3": [3.0, 15.0, 0.3, 3.75, 6.0],
41
+ "claude-3-5-haiku": [0.8, 4.0, 0.08, 1.0, 1.6],
42
+ };
43
+ const DEFAULT_PRICE = "claude-opus-5";
44
+ export function price(model) {
45
+ const m = model.replace(/\[[^\]]*\]$/, "");
46
+ let best = null;
47
+ for (const k of Object.keys(PRICES)) {
48
+ if (m.startsWith(k) && (best === null || k.length > best.length))
49
+ best = k;
50
+ }
51
+ return PRICES[best ?? DEFAULT_PRICE];
52
+ }
53
+ /** List-price cost of one request, USD. */
54
+ export function rowCost(r) {
55
+ const [pi, po, pr, w5, w1] = price(r.model);
56
+ return (r.inp * pi + r.out * po + r.cr * pr + r.cw5 * w5 + r.cw1h * w1) / 1e6;
57
+ }
58
+ export function actualBill(rows) {
59
+ let s = 0;
60
+ for (const r of rows)
61
+ s += rowCost(r);
62
+ return s;
63
+ }
64
+ /** An ungoverned request's original context = its billed prompt. */
65
+ export function origContext(r) {
66
+ return r.inp + r.cr + r.cw5 + r.cw1h;
67
+ }
68
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
69
+ const USAGE = Buffer.from('"usage"');
70
+ const ASSISTANT = Buffer.from('"assistant"');
71
+ /**
72
+ * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
73
+ * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
74
+ * session copies messages) is skipped, so each request is billed once, to its origin.
75
+ */
76
+ export function loadTranscript(path, seen) {
77
+ const byId = new Map();
78
+ const subagent = path.includes("/subagents/");
79
+ let anon = 0;
80
+ forEachLine(path, (line) => {
81
+ if (line.indexOf(USAGE) === -1 || line.indexOf(ASSISTANT) === -1)
82
+ return;
83
+ const d = parseJson(line);
84
+ if (!d || d.type !== "assistant")
85
+ return;
86
+ const m = d.message;
87
+ if (!m || typeof m !== "object")
88
+ return;
89
+ const u = m.usage;
90
+ if (!u || typeof u !== "object")
91
+ return;
92
+ const model = typeof m.model === "string" ? m.model : "";
93
+ if (!model || model.startsWith("<"))
94
+ return;
95
+ const t = isoSeconds(d.timestamp);
96
+ if (t === null)
97
+ return;
98
+ const cc = u.cache_creation ?? {};
99
+ const cw = num(u.cache_creation_input_tokens);
100
+ const cw1h = num(cc.ephemeral_1h_input_tokens);
101
+ // Older Claude Code versions omit the 5m split (or the whole cache_creation object):
102
+ // derive 5m as total − 1h, and never let a reported 5m share leave writes unbilled.
103
+ const cw5 = Math.max(num(cc.ephemeral_5m_input_tokens), cw - cw1h);
104
+ const id = (typeof m.id === "string" && m.id) ||
105
+ (typeof d.uuid === "string" && d.uuid) ||
106
+ `anon-${path}-${anon++}`;
107
+ byId.set(id, {
108
+ t,
109
+ model,
110
+ inp: num(u.input_tokens),
111
+ cr: num(u.cache_read_input_tokens),
112
+ cw5: Math.max(cw5, 0),
113
+ cw1h,
114
+ out: num(u.output_tokens),
115
+ mainThread: d.isSidechain !== true,
116
+ subagent,
117
+ });
118
+ });
119
+ const out = [];
120
+ for (const [id, row] of byId) {
121
+ if (seen) {
122
+ if (seen.has(id))
123
+ continue;
124
+ seen.add(id);
125
+ }
126
+ out.push(row);
127
+ }
128
+ return out;
129
+ }
130
+ // ── the empirical curve ──────────────────────────────────────────────────────
131
+ /** Original-context bin edges (tokens). */
132
+ export const CURVE_EDGES = [0, 100_000, 200_000, 300_000, 400_000, 700_000, 1_000_000];
133
+ /**
134
+ * Lower 95% bound per bin, rounded down to 0.1 pt (curve/curve.json `strategies`).
135
+ * aggressive: no measured pair below 100k or at/above 700k.
136
+ */
137
+ export const CURVE_LOWER = {
138
+ // bin 1 (100-200k) is measured at -0.059 but never used: < 200k runs no fold.
139
+ aggressive: { 2: 0.137, 3: 0.282, 4: 0.342 },
140
+ balanced: { 0: -0.222, 1: -0.028, 2: 0.055, 3: 0.081, 4: 0.163, 5: 0.43 },
141
+ };
142
+ export const NO_FOLD_BELOW = 200_000;
143
+ export const EXTRAPOLATE_FROM = 700_000;
144
+ export const BALANCED_FROM = 1_000_000;
145
+ export const POLICY = "none <200k / aggressive 200k-1M / balanced 1M+ (1M+ applies only to live sessions)";
146
+ export const ESTIMATOR_METHOD = "empirical curve (lower 95% bound, rounded down), strategy per request by its original context, " +
147
+ `policy ${POLICY}: < 200k the balanced arm's measured rate floored at 0, 200k-1M aggressive by bin ` +
148
+ "(>= 700k: the 400-700k rate, extrapolated); request count and output held at stock";
149
+ /** The curve bin of an original context (0 = below 100k; the top bin is open-ended). */
150
+ export function binOf(x) {
151
+ let i = 0;
152
+ while (i < CURVE_EDGES.length - 2 && x >= CURVE_EDGES[i + 1])
153
+ i++;
154
+ return i;
155
+ }
156
+ /** The screen never claims a negative saving: a negative measured rate shows as 0. */
157
+ export function floorRate(raw) {
158
+ return raw < 0 ? { screen: 0, note: "floored_at_0" } : { screen: raw, note: null };
159
+ }
160
+ /** The per-request rate the screen uses (`screen`) and the honest measured one (`raw`). */
161
+ export function ruleRate(x) {
162
+ if (x >= BALANCED_FROM) {
163
+ return { strategy: "balanced_1M+", raw: 0, screen: 0, note: "exception_1M_plus" };
164
+ }
165
+ if (x < NO_FOLD_BELOW) {
166
+ const raw = CURVE_LOWER.balanced[binOf(x)];
167
+ return { strategy: "none", raw, ...floorRate(raw) };
168
+ }
169
+ if (x >= EXTRAPOLATE_FROM) {
170
+ const r = CURVE_LOWER.aggressive[binOf(400_000)];
171
+ return { strategy: "aggressive", raw: r, screen: r, note: "extrapolated" };
172
+ }
173
+ const r = CURVE_LOWER.aggressive[binOf(x)];
174
+ return { strategy: "aggressive", raw: r, screen: r, note: null };
175
+ }
176
+ /** The one swap point: rows → list-price actual and the conservative saving. */
177
+ export function estimate(rows) {
178
+ let actual = 0;
179
+ let saved = 0;
180
+ let rawSaved = 0;
181
+ let peak = 0;
182
+ const notes = {};
183
+ for (const r of rows) {
184
+ const x = origContext(r);
185
+ if (x > peak)
186
+ peak = x;
187
+ const rate = ruleRate(x);
188
+ const c = rowCost(r);
189
+ actual += c;
190
+ saved += rate.screen * c;
191
+ rawSaved += rate.raw * c;
192
+ if (rate.note)
193
+ notes[rate.note] = (notes[rate.note] ?? 0) + 1;
194
+ }
195
+ return { actual, saved, rawSaved, peak, requests: rows.length, notes };
196
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Did a Claude Code transcript already run through Mnemom? A session is GOVERNED — and
3
+ * left out of the first-run look — if ANY signal fires. When in doubt, exclude.
4
+ *
5
+ * registry_cc_id the transcript's session id is a `cc_session_id` in the launch
6
+ * ledger (~/.mnemom/sessions.jsonl, lib/agent-ledger.ts)
7
+ * registry_conv_id … or a ledger `conversation_id`
8
+ * registry_window its first event is within [launch-60s, launch+10min] of a ledger
9
+ * launch that recorded no cc_session_id (--server, older rows)
10
+ * rc_proxy_window the same window match against a Remote Control proxy log
11
+ * text_marker the bytes carry a gateway marker ('[mnemom', 'x-mnemom', 'covers=',
12
+ * …). Also fires on sessions that merely DISCUSS Mnemom: over-excludes,
13
+ * the safe direction
14
+ * settings_env a settings file (global or the session cwd's) or the transcript
15
+ * text points ANTHROPIC_BASE_URL / HTTPS_PROXY at a mnemom host or a
16
+ * localhost proxy
17
+ * agent_sdk_host entrypoint sdk-py / sdk-ts: an Agent-SDK host can route through
18
+ * the gateway with env no transcript records
19
+ * (a session is governed if its top transcript OR any subagent transcript fires)
20
+ *
21
+ * Read-only on ~/.claude and ~/.mnemom. No network.
22
+ */
23
+ export declare const AGENT_SDK_ENTRYPOINTS: Set<string>;
24
+ /** Lowercase byte markers. Candidate lines are found by the case-variant probes below. */
25
+ export declare const TEXT_MARKERS: string[];
26
+ export declare function lineHasMarker(line: Buffer): boolean;
27
+ export declare function lineHasEnv(line: Buffer): boolean;
28
+ export interface FileHead {
29
+ firstTs: number | null;
30
+ cwd: string | null;
31
+ sessionId: string | null;
32
+ entrypoint: string | null;
33
+ }
34
+ export interface FileResult extends FileHead {
35
+ signals: string[];
36
+ }
37
+ export interface GovernanceInputs {
38
+ home?: string;
39
+ /** Directory holding the launch ledger (MNEMOM_DIR). */
40
+ mnemomDir?: string;
41
+ tmp?: string;
42
+ }
43
+ /** Session id of a transcript path: its own uuid, or the parent's for a subagent file. */
44
+ export declare function sessionIdFor(path: string): string;
45
+ export declare class Governance {
46
+ readonly ccIds: Set<string>;
47
+ readonly convIds: Set<string>;
48
+ readonly windows: Array<["registry_window" | "rc_proxy_window", number]>;
49
+ readonly globalSettingsEnv: boolean;
50
+ registryRows: number;
51
+ private readonly cwdCache;
52
+ constructor(inp?: GovernanceInputs);
53
+ private cwdEnv;
54
+ /** The first records of a file: first timestamp, cwd, session id, entrypoint. */
55
+ head(path: string, maxBytes?: number): FileHead;
56
+ /** Signals that follow from a file's head records alone. */
57
+ private headSignals;
58
+ /**
59
+ * Classify one transcript. The cheap signals (ledger, head records, settings, windows)
60
+ * are checked first; the full byte scan for markers runs only when none fired (the
61
+ * session is excluded either way, so the scan would change nothing). The full scan
62
+ * also completes head fields that a large first record pushed past the head read.
63
+ */
64
+ classify(path: string): FileResult;
65
+ }