@mnemom/mnemom 0.17.8-next.0 → 0.17.8

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.
@@ -13,7 +13,8 @@
13
13
  * original context is at or above the aggressive profile's fold point (its 60k tail
14
14
  * budget); smaller requests never fold, so they save 0. A flat rate over-counts mid-size
15
15
  * contexts and under-counts large ones; in aggregate that evens out. Per-size rates come
16
- * back once there are enough measured pairs.
16
+ * back once there are enough measured pairs. The token saving uses the same rule on the
17
+ * prompt (operator 2026-10-02: one rule for dollars and tokens).
17
18
  */
18
19
  /** One billed request, deduped by message id. `t` is epoch seconds. */
19
20
  export interface Row {
@@ -50,12 +51,24 @@ export declare function parseUsageLine(line: Buffer, subagent: boolean): {
50
51
  id: string | null;
51
52
  row: Row;
52
53
  } | null;
54
+ /** One Claude Code compaction (`type:"system", subtype:"compact_boundary"`). */
55
+ export interface Compaction {
56
+ /** Epoch seconds. */
57
+ t: number;
58
+ /** compactMetadata.preTokens / postTokens (0 when absent). */
59
+ pre: number;
60
+ post: number;
61
+ }
62
+ /** One transcript line → its compaction record, or null. Reads only the metadata. */
63
+ export declare function parseCompactLine(line: Buffer): Compaction | null;
53
64
  /**
54
65
  * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
55
66
  * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
56
67
  * session copies messages) is skipped, so each request is billed once, to its origin.
68
+ * With `compactions`, the transcript's compaction records are appended to it in the same
69
+ * pass (no second read of the file).
57
70
  */
58
- export declare function loadTranscript(path: string, seen?: Set<string>): Row[];
71
+ export declare function loadTranscript(path: string, seen?: Set<string>, compactions?: Compaction[]): Row[];
59
72
  /** S46 C2n vs stock, median paired total-bill saving (n = 13). */
60
73
  export declare const S46_MEDIAN_SAVING = 0.238;
61
74
  /** The aggressive profile's tail budget (cli/src/lib/agent.ts): below it nothing folds. */
@@ -73,11 +86,18 @@ export declare function ruleRate(x: number): {
73
86
  export interface Estimate {
74
87
  actual: number;
75
88
  saved: number;
89
+ /** Tokens used: prompt + output over every request. */
90
+ tokens: number;
91
+ /** The token saving: the same flat rule on the prompt, ruleRate(prompt) x prompt. */
92
+ savedTokens: number;
76
93
  /** Same as `saved` now that no rate is floored (kept for the audit record). */
77
94
  rawSaved: number;
78
95
  peak: number;
79
96
  requests: number;
80
97
  notes: Partial<Record<Exclude<RateNote, null>, number>>;
81
98
  }
82
- /** The one swap point: rows → list-price actual and the conservative saving. */
99
+ /**
100
+ * The one swap point for a session that did NOT run through Mnemom: rows → list-price
101
+ * actual, the conservative dollar saving (the cost curve) and the token saving.
102
+ */
83
103
  export declare function estimate(rows: readonly Row[]): Estimate;
@@ -13,7 +13,8 @@
13
13
  * original context is at or above the aggressive profile's fold point (its 60k tail
14
14
  * budget); smaller requests never fold, so they save 0. A flat rate over-counts mid-size
15
15
  * contexts and under-counts large ones; in aggregate that evens out. Per-size rates come
16
- * back once there are enough measured pairs.
16
+ * back once there are enough measured pairs. The token saving uses the same rule on the
17
+ * prompt (operator 2026-10-02: one rule for dollars and tokens).
17
18
  */
18
19
  import { forEachLine, isoSeconds, parseJson } from "./lines.js";
19
20
  /**
@@ -109,16 +110,39 @@ export function parseUsageLine(line, subagent) {
109
110
  },
110
111
  };
111
112
  }
113
+ const COMPACT = Buffer.from('"compact_boundary"');
114
+ /** One transcript line → its compaction record, or null. Reads only the metadata. */
115
+ export function parseCompactLine(line) {
116
+ if (line.indexOf(COMPACT) === -1)
117
+ return null;
118
+ const d = parseJson(line);
119
+ if (!d || d.type !== "system" || d.subtype !== "compact_boundary")
120
+ return null;
121
+ const t = isoSeconds(d.timestamp);
122
+ if (t === null)
123
+ return null;
124
+ const cm = d.compactMetadata ?? {};
125
+ return { t, pre: num(cm.preTokens), post: num(cm.postTokens) };
126
+ }
112
127
  /**
113
128
  * Billed requests of one transcript, deduped by message id (last copy wins, first-seen
114
129
  * order). With `seen`, an id already billed in an earlier transcript (a resumed / forked
115
130
  * session copies messages) is skipped, so each request is billed once, to its origin.
131
+ * With `compactions`, the transcript's compaction records are appended to it in the same
132
+ * pass (no second read of the file).
116
133
  */
117
- export function loadTranscript(path, seen) {
134
+ export function loadTranscript(path, seen, compactions) {
118
135
  const byId = new Map();
119
136
  const subagent = path.includes("/subagents/");
120
137
  let anon = 0;
121
138
  forEachLine(path, (line) => {
139
+ if (compactions) {
140
+ const c = parseCompactLine(line);
141
+ if (c) {
142
+ compactions.push(c);
143
+ return;
144
+ }
145
+ }
122
146
  const r = parseUsageLine(line, subagent);
123
147
  if (r)
124
148
  byId.set(r.id ?? `anon-${path}-${anon++}`, r.row);
@@ -148,11 +172,16 @@ export function ruleRate(x) {
148
172
  }
149
173
  return { strategy: "aggressive", raw: S46_MEDIAN_SAVING, screen: S46_MEDIAN_SAVING, note: null };
150
174
  }
151
- /** The one swap point: rows → list-price actual and the conservative saving. */
175
+ /**
176
+ * The one swap point for a session that did NOT run through Mnemom: rows → list-price
177
+ * actual, the conservative dollar saving (the cost curve) and the token saving.
178
+ */
152
179
  export function estimate(rows) {
153
180
  let actual = 0;
154
181
  let saved = 0;
155
182
  let rawSaved = 0;
183
+ let tokens = 0;
184
+ let savedTokens = 0;
156
185
  let peak = 0;
157
186
  const notes = {};
158
187
  for (const r of rows) {
@@ -164,8 +193,10 @@ export function estimate(rows) {
164
193
  actual += c;
165
194
  saved += rate.screen * c;
166
195
  rawSaved += rate.raw * c;
196
+ tokens += x + r.out;
197
+ savedTokens += rate.screen * x;
167
198
  if (rate.note)
168
199
  notes[rate.note] = (notes[rate.note] ?? 0) + 1;
169
200
  }
170
- return { actual, saved, rawSaved, peak, requests: rows.length, notes };
201
+ return { actual, saved, tokens, savedTokens, rawSaved, peak, requests: rows.length, notes };
171
202
  }
@@ -3,21 +3,30 @@
3
3
  * backtest the user's last 30 days of Claude Code sessions on this machine and show what
4
4
  * Mnemom would have saved. `mnemom agent --first-run` shows it again at any time.
5
5
  *
6
- * Every session in the window is priced (operator 2026-10-02: "count all sessions"),
7
- * including ones that already ran through Mnemom: their transcripts hold the full,
8
- * unfolded context the client sent. Only copies of one session are dropped. The governed
9
- * classification is still run, for the audit counts only.
6
+ * Every session in the window is priced (operator 2026-10-02: "count all sessions"), in
7
+ * one of two buckets (throughEvidence):
8
+ * THROUGH Mnemom a launch-ledger signal, a window / env signal, or fold evidence on
9
+ * the main thread. Its transcript does NOT hold the unfolded context:
10
+ * through the gateway the top-level `usage` is the folded, billed size
11
+ * (`usage.iterations` matches it on every one of 365k rows measured),
12
+ * so the curve cannot be applied to it. Its saving is REALIZED: a
13
+ * replay of stock Claude Code at its default window (replay.ts).
14
+ * NOT through everything else, headless, Agent-SDK and marker-only sessions
15
+ * included: the POTENTIAL saving (backtest.estimate()).
16
+ * Only copies of one session are dropped.
10
17
  *
11
- * Privacy: READ-ONLY on ~/.claude and every transcript; no network call anywhere in this
12
- * module tree; only token counts, model ids, timestamps and Claude Code's own limit
13
- * records are read. Writes only the offer state file and a local audit (both 0600,
14
- * under ~/.mnemom).
18
+ * Privacy: READ-ONLY on ~/.claude and every transcript, one pass per file; no network
19
+ * call anywhere in this module tree; only token counts, model ids, timestamps, Claude
20
+ * Code's own limit records and its compaction metadata are read.
21
+ *
22
+ * Writes only the offer state file and a local audit (both 0600, under ~/.mnemom).
15
23
  *
16
24
  * State file (~/.mnemom/first-run.json): { offers, last_offer, last_answer, accepted_at?,
17
25
  * never? }. The offer is made while neither `accepted_at` nor `never` is set:
18
26
  * Enter / y shows the screen and records acceptance; n skips until the next launch;
19
27
  * never stops asking. A non-interactive launch is never asked and writes nothing.
20
28
  */
29
+ import { type Compaction, type Row } from "./backtest.js";
21
30
  import { type ScopeResult, type Session } from "./scope.js";
22
31
  import { type Audit, type Variation } from "./screen.js";
23
32
  export declare const STATE_FILE = "first-run.json";
@@ -59,18 +68,30 @@ export interface FirstRunResult {
59
68
  export declare function computeFirstRun(opts?: ScanOptions): FirstRunResult;
60
69
  /**
61
70
  * The sessions the screen prices: every session in the window, one per session id (an
62
- * ungoverned copy first), none without a parent transcript.
71
+ * ungoverned copy first), none without a parent transcript, with its governed signals.
63
72
  */
64
73
  export declare function pricedSessions(scope: ScopeResult): Array<{
65
74
  s: Session;
66
- viaMnemom: boolean;
75
+ signals: string[];
67
76
  }>;
77
+ /** `mnemom agent` recorded the session in the launch ledger. */
78
+ export declare const LEDGER_SIGNALS: Set<string>;
68
79
  /**
69
- * Proof a session ran through Mnemom: `mnemom agent` recorded it in the launch ledger.
70
- * Every other governed signal (an Agent-SDK host, a text marker, a time window) only
71
- * means it may have, so those sessions are not said to have been saved on.
80
+ * A launch / Remote Control window match, or a settings env pointing at the gateway.
81
+ * These put a session in the through bucket (operator sign-off 2026-10-02: "launch
82
+ * ledger, OR inside a Mnemom launch window, OR shows a fold"), but they never make a
83
+ * saving claim: the realized saving comes only from fold drops the replay detects
84
+ * (replayStock caps it at 0 without one), so a window- or env-only session with no
85
+ * fold adds its usage to "Through Mnemom" and nothing to "That saved".
72
86
  */
73
- export declare const LEDGER_SIGNALS: Set<string>;
87
+ export declare const WINDOW_SIGNALS: Set<string>;
88
+ /**
89
+ * Did the session run through Mnemom, and on what evidence? A ledger or window / env
90
+ * signal, else fold evidence on its main thread (replay.hasFoldEvidence). A headless,
91
+ * Agent-SDK or marker-only session without fold evidence is NOT through: those signals
92
+ * only mean it may have been.
93
+ */
94
+ export declare function throughEvidence(signals: readonly string[], rows: readonly Row[], comps: readonly Compaction[]): "ledger" | "window" | "folds" | null;
74
95
  /** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
75
96
  export declare function assertScreenBudget(lines: readonly string[]): void;
76
97
  /** A transcript's mtime (ms), 0 when it vanished since discovery (it then sorts first). */
@@ -3,15 +3,23 @@
3
3
  * backtest the user's last 30 days of Claude Code sessions on this machine and show what
4
4
  * Mnemom would have saved. `mnemom agent --first-run` shows it again at any time.
5
5
  *
6
- * Every session in the window is priced (operator 2026-10-02: "count all sessions"),
7
- * including ones that already ran through Mnemom: their transcripts hold the full,
8
- * unfolded context the client sent. Only copies of one session are dropped. The governed
9
- * classification is still run, for the audit counts only.
6
+ * Every session in the window is priced (operator 2026-10-02: "count all sessions"), in
7
+ * one of two buckets (throughEvidence):
8
+ * THROUGH Mnemom a launch-ledger signal, a window / env signal, or fold evidence on
9
+ * the main thread. Its transcript does NOT hold the unfolded context:
10
+ * through the gateway the top-level `usage` is the folded, billed size
11
+ * (`usage.iterations` matches it on every one of 365k rows measured),
12
+ * so the curve cannot be applied to it. Its saving is REALIZED: a
13
+ * replay of stock Claude Code at its default window (replay.ts).
14
+ * NOT through everything else, headless, Agent-SDK and marker-only sessions
15
+ * included: the POTENTIAL saving (backtest.estimate()).
16
+ * Only copies of one session are dropped.
10
17
  *
11
- * Privacy: READ-ONLY on ~/.claude and every transcript; no network call anywhere in this
12
- * module tree; only token counts, model ids, timestamps and Claude Code's own limit
13
- * records are read. Writes only the offer state file and a local audit (both 0600,
14
- * under ~/.mnemom).
18
+ * Privacy: READ-ONLY on ~/.claude and every transcript, one pass per file; no network
19
+ * call anywhere in this module tree; only token counts, model ids, timestamps, Claude
20
+ * Code's own limit records and its compaction metadata are read.
21
+ *
22
+ * Writes only the offer state file and a local audit (both 0600, under ~/.mnemom).
15
23
  *
16
24
  * State file (~/.mnemom/first-run.json): { offers, last_offer, last_answer, accepted_at?,
17
25
  * never? }. The offer is made while neither `accepted_at` nor `never` is set:
@@ -21,7 +29,8 @@
21
29
  import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
22
30
  import { homedir } from "node:os";
23
31
  import { dirname, join } from "node:path";
24
- import { estimate, loadTranscript, origContext, ruleRate } from "./backtest.js";
32
+ import { estimate, loadTranscript, origContext, ruleRate, } from "./backtest.js";
33
+ import { defaultWindow, dominantModel, hasFoldEvidence, replayStock } from "./replay.js";
25
34
  import { analyzeSeat } from "./seat.js";
26
35
  import { ORG_SEAT_TYPES, authShape, billingClass, buildScope, configDirOf, prettyProject, } from "./scope.js";
27
36
  import { DAYS, MAX_LINES, WIDTH, buildScreen, labelFor, } from "./screen.js";
@@ -117,17 +126,22 @@ export function computeFirstRun(opts = {}) {
117
126
  const seen = new Set();
118
127
  const shapes = new Map();
119
128
  const sessions = [];
120
- const subSessions = [];
121
129
  // A Team / Enterprise subscriber: a subscription-billed session, or a Team / Enterprise
122
130
  // login in any kept session's config dir. Only these plans can run through Mnemom on
123
131
  // their subscription (lib/subscription-login.ts), so only they get the usage sentence.
124
- let orgLogin = false;
132
+ let subscriber = false;
133
+ let extraUsage = false;
125
134
  const seatInputs = new Map();
126
- ordered.forEach(({ s, viaMnemom }, i) => {
135
+ ordered.forEach(({ s, signals }, i) => {
127
136
  if (i % 25 === 0)
128
137
  opts.progress?.(`pricing sessions ${i + 1}/${ordered.length}`);
129
138
  const paths = [s.top, ...[...s.subs].sort()];
130
- const rows = paths.flatMap((p) => loadTranscript(p, seen));
139
+ // Compactions are the main thread's: read from the top transcript only.
140
+ const comps = [];
141
+ const rows = [
142
+ ...loadTranscript(paths[0], seen, comps),
143
+ ...paths.slice(1).flatMap((p) => loadTranscript(p, seen)),
144
+ ];
131
145
  if (!rows.length) {
132
146
  counts.no_usage = (counts.no_usage ?? 0) + 1;
133
147
  return;
@@ -152,30 +166,44 @@ export function computeFirstRun(opts = {}) {
152
166
  const rowsIn = rows.filter((r) => r.t >= cut);
153
167
  if (!rowsIn.length)
154
168
  return;
155
- // Every class is priced at API list prices; the screen says so.
169
+ const compsIn = comps.filter((c) => c.t >= cut);
170
+ // Every bucket is priced at API list prices; the screen says so.
156
171
  const e = estimate(rowsIn);
157
172
  const end = rowsIn.reduce((m, r) => Math.max(m, r.t), -Infinity);
158
- const sc = {
173
+ const via = throughEvidence(signals, rowsIn, compsIn);
174
+ let saved = e.saved;
175
+ let savedTokens = e.savedTokens;
176
+ if (via) {
177
+ counts[`through_${via}`] = (counts[`through_${via}`] ?? 0) + 1;
178
+ const W = defaultWindow(dominantModel(rowsIn));
179
+ const rp = replayStock(rowsIn, compsIn, W);
180
+ counts[`through_window_${W}`] = (counts[`through_window_${W}`] ?? 0) + 1;
181
+ counts.stock_compactions = (counts.stock_compactions ?? 0) + rp.stockCompactions;
182
+ counts.actual_compactions = (counts.actual_compactions ?? 0) + rp.actualCompactions;
183
+ if (rp.savedUsd < 0)
184
+ counts.through_negative = (counts.through_negative ?? 0) + 1;
185
+ saved = rp.savedUsd;
186
+ savedTokens = rp.savedTokens;
187
+ }
188
+ else {
189
+ counts.not_through = (counts.not_through ?? 0) + 1;
190
+ }
191
+ sessions.push({
159
192
  label: labelFor(prettyProject(s.pdir)),
160
193
  end,
161
194
  actual: e.actual,
162
- saved: e.saved,
163
- viaMnemom,
164
- };
165
- sessions.push(sc);
166
- if (cls === "subscription_org")
167
- subSessions.push(sc);
168
- orgLogin ||= subLogin;
195
+ tokens: e.tokens,
196
+ saved,
197
+ savedTokens,
198
+ through: via !== null,
199
+ });
200
+ if (cls === "subscription_org" || subLogin) {
201
+ subscriber = true;
202
+ extraUsage ||= shape.extraUsage === true;
203
+ }
169
204
  });
170
205
  const seat = [...seatInputs.values()].map((a) => analyzeSeat(a.ss, a.extra, now));
171
- const use = subSessions.length ? subSessions : sessions;
172
- const sub = subSessions.length || orgLogin
173
- ? {
174
- sessions: use.length,
175
- actual: use.reduce((t, r) => t + r.actual, 0),
176
- saved: use.reduce((t, r) => t + r.saved, 0),
177
- }
178
- : null;
206
+ const sub = subscriber ? { extraUsage } : null;
179
207
  const { lines, audit, variation } = buildScreen({ sessions, sub });
180
208
  assertScreenBudget(lines);
181
209
  audit.seat = {
@@ -187,7 +215,7 @@ export function computeFirstRun(opts = {}) {
187
215
  }
188
216
  /**
189
217
  * The sessions the screen prices: every session in the window, one per session id (an
190
- * ungoverned copy first), none without a parent transcript.
218
+ * ungoverned copy first), none without a parent transcript, with its governed signals.
191
219
  */
192
220
  export function pricedSessions(scope) {
193
221
  const out = new Map();
@@ -195,17 +223,34 @@ export function pricedSessions(scope) {
195
223
  const s = scope.sessions.get(key);
196
224
  if (!s.top || out.has(s.sid))
197
225
  continue;
198
- const sigs = scope.excluded.get(key) ?? [];
199
- out.set(s.sid, { s, viaMnemom: sigs.some((x) => LEDGER_SIGNALS.has(x)) });
226
+ out.set(s.sid, { s, signals: scope.excluded.get(key) ?? [] });
200
227
  }
201
228
  return [...out.values()];
202
229
  }
230
+ /** `mnemom agent` recorded the session in the launch ledger. */
231
+ export const LEDGER_SIGNALS = new Set(["registry_cc_id", "registry_conv_id"]);
203
232
  /**
204
- * Proof a session ran through Mnemom: `mnemom agent` recorded it in the launch ledger.
205
- * Every other governed signal (an Agent-SDK host, a text marker, a time window) only
206
- * means it may have, so those sessions are not said to have been saved on.
233
+ * A launch / Remote Control window match, or a settings env pointing at the gateway.
234
+ * These put a session in the through bucket (operator sign-off 2026-10-02: "launch
235
+ * ledger, OR inside a Mnemom launch window, OR shows a fold"), but they never make a
236
+ * saving claim: the realized saving comes only from fold drops the replay detects
237
+ * (replayStock caps it at 0 without one), so a window- or env-only session with no
238
+ * fold adds its usage to "Through Mnemom" and nothing to "That saved".
207
239
  */
208
- export const LEDGER_SIGNALS = new Set(["registry_cc_id", "registry_conv_id"]);
240
+ export const WINDOW_SIGNALS = new Set(["registry_window", "rc_proxy_window", "settings_env"]);
241
+ /**
242
+ * Did the session run through Mnemom, and on what evidence? A ledger or window / env
243
+ * signal, else fold evidence on its main thread (replay.hasFoldEvidence). A headless,
244
+ * Agent-SDK or marker-only session without fold evidence is NOT through: those signals
245
+ * only mean it may have been.
246
+ */
247
+ export function throughEvidence(signals, rows, comps) {
248
+ if (signals.some((x) => LEDGER_SIGNALS.has(x)))
249
+ return "ledger";
250
+ if (signals.some((x) => WINDOW_SIGNALS.has(x)))
251
+ return "window";
252
+ return hasFoldEvidence(rows, comps) ? "folds" : null;
253
+ }
209
254
  /** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
210
255
  export function assertScreenBudget(lines) {
211
256
  if (lines.length > MAX_LINES || lines.some((l) => l.length > WIDTH)) {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The realized saving of a session that ran through Mnemom: a replay of what stock Claude
3
+ * Code would have sent.
4
+ *
5
+ * Through the gateway, a transcript's top-level `usage` is the FOLDED, billed size (over
6
+ * 365k rows, `usage.iterations` matches the top-level fields, so no pre-fold size is ever
7
+ * recorded). So the client-side size is rebuilt from the fold drops themselves, and stock
8
+ * Claude Code is replayed on it at its DEFAULT window for the session's model, compacting
9
+ * whenever it would have. The saving is what stock would have billed minus what was
10
+ * billed. It can be negative for a session, and is kept negative.
11
+ *
12
+ * Pure: rows and compaction records in, numbers out. No I/O.
13
+ */
14
+ import { type Compaction, type Row } from "./backtest.js";
15
+ /** Main-thread requests with a smaller prompt are side calls (title, summary): ignored. */
16
+ export declare const SIDE_CALL_BELOW = 10000;
17
+ /** A fold: prompt(a) - prompt(b) > this … */
18
+ export declare const FOLD_MIN_DROP = 20000;
19
+ /** … and prompt(b) < this x prompt(a), with no Claude Code compaction in between. */
20
+ export declare const FOLD_MAX_RATIO = 0.7;
21
+ /** Median post-compaction size over 1,460 observed Claude Code compactions. */
22
+ export declare const POST_COMPACT = 17181;
23
+ /** Output tokens of a stock compaction's summary call. */
24
+ export declare const SUMMARY_OUT = 6000;
25
+ export declare const WINDOW_200K = 200000;
26
+ export declare const WINDOW_1M = 1000000;
27
+ /**
28
+ * Models whose Claude Code default context window is 1M (read out of Claude Code
29
+ * 2.1.285's binary). Everything else, unknown models included, is 200k. The user's own
30
+ * autoCompactWindow is deliberately NOT read: the baseline is stock Claude Code.
31
+ */
32
+ export declare const ONE_M_DEFAULT: RegExp;
33
+ /** Claude Code's default window for a model id (a trailing `[...]` is stripped). */
34
+ export declare function defaultWindow(model: string): number;
35
+ /** The most frequent main-thread model (all rows when none is main); first seen wins a tie. */
36
+ export declare function dominantModel(rows: readonly Row[]): string;
37
+ /** The main-thread requests the replay walks: prompt >= 10k, in timestamp order. */
38
+ export declare function replayRows(rows: readonly Row[]): Row[];
39
+ /** A Claude Code compaction with a timestamp in [ta, tb]. */
40
+ export declare function compactedBetween(ct: readonly number[], ta: number, tb: number): boolean;
41
+ /** The size drop from a to b looks like a fold (the compaction check is separate). */
42
+ export declare function isFoldDrop(a: number, b: number): boolean;
43
+ /** Fold evidence: two consecutive main-thread requests with a fold drop and no compaction. */
44
+ export declare function hasFoldEvidence(rows: readonly Row[], comps: readonly Compaction[]): boolean;
45
+ export interface Replay {
46
+ /** The baseline window. */
47
+ window: number;
48
+ /** Σ (stock size - billed prompt) over the replayed requests. */
49
+ tokenDiff: number;
50
+ /** The same, priced at each request's cache-read rate. */
51
+ usdDiff: number;
52
+ stockCompactions: number;
53
+ stockCompactionUsd: number;
54
+ /** Claude Code compactions recorded in the session. */
55
+ actualCompactions: number;
56
+ /** Fold drops the replay found along the main thread. */
57
+ folds: number;
58
+ savedTokens: number;
59
+ savedUsd: number;
60
+ }
61
+ /**
62
+ * Replay stock Claude Code at window W along the session's main thread.
63
+ *
64
+ * client = billed prompt + what the folds removed so far (reset by a real compaction;
65
+ * growth during a fold request counts as 0, the conservative direction)
66
+ * stock = min(client, W) at the first request and after a reset; otherwise it grows
67
+ * with client and compacts above W: count it, charge (W x cache read + 6000 x
68
+ * output + 17181 x cache write 5m) at the request's model price, and continue
69
+ * from 17181 + the overshoot
70
+ * saved = Σ (stock - billed) at cache read + stock compaction cost - the session's own
71
+ * compactions x the mean simulated compaction cost (0 if none was simulated)
72
+ *
73
+ * The last term is deliberately CONSERVATIVE (operator 2026-10-02). At a real compaction
74
+ * the replay resets both worlds to the same size, so that compaction's cost is paid on
75
+ * both sides and cancels. Subtracting it then charges each real compaction against
76
+ * Mnemom a second time, at the mean simulated stock cost, which is 0 when the replay
77
+ * simulated none. It can only lower the saving, never raise it, and with more real
78
+ * compactions than simulated ones it can take the saving below 0.
79
+ *
80
+ * A REALIZED saving comes only from detected folds. With none, savedTokens and savedUsd
81
+ * are capped at 0: a session put through Mnemom by a ledger, window or env signal alone
82
+ * can never add a saving (it adds its usage to the through bucket and 0 to "That saved"),
83
+ * but a replay that comes out NEGATIVE (billed above stock's window) still counts against
84
+ * it, so the cap only ever lowers the bucket total. tokenDiff / usdDiff keep the raw values.
85
+ */
86
+ export declare function replayStock(rows: readonly Row[], comps: readonly Compaction[], W: number): Replay;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The realized saving of a session that ran through Mnemom: a replay of what stock Claude
3
+ * Code would have sent.
4
+ *
5
+ * Through the gateway, a transcript's top-level `usage` is the FOLDED, billed size (over
6
+ * 365k rows, `usage.iterations` matches the top-level fields, so no pre-fold size is ever
7
+ * recorded). So the client-side size is rebuilt from the fold drops themselves, and stock
8
+ * Claude Code is replayed on it at its DEFAULT window for the session's model, compacting
9
+ * whenever it would have. The saving is what stock would have billed minus what was
10
+ * billed. It can be negative for a session, and is kept negative.
11
+ *
12
+ * Pure: rows and compaction records in, numbers out. No I/O.
13
+ */
14
+ import { origContext, price } from "./backtest.js";
15
+ /** Main-thread requests with a smaller prompt are side calls (title, summary): ignored. */
16
+ export const SIDE_CALL_BELOW = 10_000;
17
+ /** A fold: prompt(a) - prompt(b) > this … */
18
+ export const FOLD_MIN_DROP = 20_000;
19
+ /** … and prompt(b) < this x prompt(a), with no Claude Code compaction in between. */
20
+ export const FOLD_MAX_RATIO = 0.7;
21
+ /** Median post-compaction size over 1,460 observed Claude Code compactions. */
22
+ export const POST_COMPACT = 17_181;
23
+ /** Output tokens of a stock compaction's summary call. */
24
+ export const SUMMARY_OUT = 6_000;
25
+ export const WINDOW_200K = 200_000;
26
+ export const WINDOW_1M = 1_000_000;
27
+ /**
28
+ * Models whose Claude Code default context window is 1M (read out of Claude Code
29
+ * 2.1.285's binary). Everything else, unknown models included, is 200k. The user's own
30
+ * autoCompactWindow is deliberately NOT read: the baseline is stock Claude Code.
31
+ */
32
+ export const ONE_M_DEFAULT = /^claude-(fable-5|opus-4-[6-9]|opus-[5-9]|sonnet-4-[6-9]|sonnet-[5-9])/;
33
+ /** Claude Code's default window for a model id (a trailing `[...]` is stripped). */
34
+ export function defaultWindow(model) {
35
+ return ONE_M_DEFAULT.test(model.replace(/\[[^\]]*\]$/, "")) ? WINDOW_1M : WINDOW_200K;
36
+ }
37
+ const isMain = (r) => r.mainThread && !r.subagent;
38
+ /** The most frequent main-thread model (all rows when none is main); first seen wins a tie. */
39
+ export function dominantModel(rows) {
40
+ const main = rows.filter(isMain);
41
+ const counts = new Map();
42
+ let best = "";
43
+ let bestN = 0;
44
+ for (const r of main.length ? main : rows) {
45
+ const n = (counts.get(r.model) ?? 0) + 1;
46
+ counts.set(r.model, n);
47
+ if (n > bestN) {
48
+ best = r.model;
49
+ bestN = n;
50
+ }
51
+ }
52
+ return best;
53
+ }
54
+ /** The main-thread requests the replay walks: prompt >= 10k, in timestamp order. */
55
+ export function replayRows(rows) {
56
+ return rows
57
+ .filter((r) => isMain(r) && origContext(r) >= SIDE_CALL_BELOW)
58
+ .sort((a, b) => a.t - b.t);
59
+ }
60
+ /** A Claude Code compaction with a timestamp in [ta, tb]. */
61
+ export function compactedBetween(ct, ta, tb) {
62
+ return ct.some((t) => t >= ta && t <= tb);
63
+ }
64
+ /** The size drop from a to b looks like a fold (the compaction check is separate). */
65
+ export function isFoldDrop(a, b) {
66
+ return a - b > FOLD_MIN_DROP && b < FOLD_MAX_RATIO * a;
67
+ }
68
+ /** Fold evidence: two consecutive main-thread requests with a fold drop and no compaction. */
69
+ export function hasFoldEvidence(rows, comps) {
70
+ const m = replayRows(rows);
71
+ const ct = comps.map((c) => c.t);
72
+ for (let i = 1; i < m.length; i++) {
73
+ const a = m[i - 1];
74
+ const b = m[i];
75
+ if (isFoldDrop(origContext(a), origContext(b)) && !compactedBetween(ct, a.t, b.t))
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ /**
81
+ * Replay stock Claude Code at window W along the session's main thread.
82
+ *
83
+ * client = billed prompt + what the folds removed so far (reset by a real compaction;
84
+ * growth during a fold request counts as 0, the conservative direction)
85
+ * stock = min(client, W) at the first request and after a reset; otherwise it grows
86
+ * with client and compacts above W: count it, charge (W x cache read + 6000 x
87
+ * output + 17181 x cache write 5m) at the request's model price, and continue
88
+ * from 17181 + the overshoot
89
+ * saved = Σ (stock - billed) at cache read + stock compaction cost - the session's own
90
+ * compactions x the mean simulated compaction cost (0 if none was simulated)
91
+ *
92
+ * The last term is deliberately CONSERVATIVE (operator 2026-10-02). At a real compaction
93
+ * the replay resets both worlds to the same size, so that compaction's cost is paid on
94
+ * both sides and cancels. Subtracting it then charges each real compaction against
95
+ * Mnemom a second time, at the mean simulated stock cost, which is 0 when the replay
96
+ * simulated none. It can only lower the saving, never raise it, and with more real
97
+ * compactions than simulated ones it can take the saving below 0.
98
+ *
99
+ * A REALIZED saving comes only from detected folds. With none, savedTokens and savedUsd
100
+ * are capped at 0: a session put through Mnemom by a ledger, window or env signal alone
101
+ * can never add a saving (it adds its usage to the through bucket and 0 to "That saved"),
102
+ * but a replay that comes out NEGATIVE (billed above stock's window) still counts against
103
+ * it, so the cap only ever lowers the bucket total. tokenDiff / usdDiff keep the raw values.
104
+ */
105
+ export function replayStock(rows, comps, W) {
106
+ const m = replayRows(rows);
107
+ const ct = comps.map((c) => c.t);
108
+ let removed = 0;
109
+ let prev = null;
110
+ let clientPrev = 0;
111
+ let st = 0;
112
+ let tokenDiff = 0;
113
+ let usdDiff = 0;
114
+ let n = 0;
115
+ let ccost = 0;
116
+ let folds = 0;
117
+ for (const r of m) {
118
+ const p = origContext(r);
119
+ const [, po, pr, w5] = price(r.model);
120
+ let reset = prev === null;
121
+ if (prev !== null) {
122
+ if (compactedBetween(ct, prev.t, r.t)) {
123
+ removed = 0;
124
+ reset = true;
125
+ }
126
+ else if (isFoldDrop(origContext(prev), p)) {
127
+ removed += origContext(prev) - p;
128
+ folds++;
129
+ }
130
+ }
131
+ const client = p + removed;
132
+ if (reset) {
133
+ st = Math.min(client, W);
134
+ }
135
+ else {
136
+ st += Math.max(client - clientPrev, 0);
137
+ while (st > W) {
138
+ n++;
139
+ ccost += (W * pr + SUMMARY_OUT * po + POST_COMPACT * w5) / 1e6;
140
+ st = POST_COMPACT + (st - W);
141
+ }
142
+ }
143
+ tokenDiff += st - p;
144
+ usdDiff += ((st - p) * pr) / 1e6;
145
+ prev = r;
146
+ clientPrev = client;
147
+ }
148
+ // Conservative: see the doc comment. Never raises savedUsd (each >= 0).
149
+ const each = n ? ccost / n : 0;
150
+ const usd = usdDiff + ccost - comps.length * each;
151
+ return {
152
+ window: W,
153
+ tokenDiff,
154
+ usdDiff,
155
+ stockCompactions: n,
156
+ stockCompactionUsd: ccost,
157
+ actualCompactions: comps.length,
158
+ folds,
159
+ savedTokens: folds ? tokenDiff : Math.min(tokenDiff, 0),
160
+ savedUsd: folds ? usd : Math.min(usd, 0),
161
+ };
162
+ }
@@ -1,34 +1,52 @@
1
1
  /**
2
2
  * The first-run screen: one plain terminal screen (at most 22 lines of at most 78
3
- * columns, ASCII) built from the backtest numbers. Pure — no I/O — so every layout is
3
+ * columns, ASCII; with the command's blank line above and below, 24 rows) built from
4
+ * the backtest numbers. Pure — no I/O — so every layout is
4
5
  * unit-tested. Every number on it is recorded in the audit with its raw value and
5
6
  * method.
6
7
  *
7
- * Exactly three variations (operator v1, 2026-10-01); every number has one source and no
8
- * two lines make the same claim:
9
- * A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
10
- * the saving, the biggest session; plus ONE subscription sentence for a Team /
11
- * Enterprise subscriber whose usage cut rounds to >= 5%. Where sessions ran
12
- * through Mnemom (launch ledger), that part is said to be SAVED: what it
13
- * saved you, and what it would have saved on the rest (operator 2026-10-02).
14
- * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
15
- * "wouldn't have changed that much" line, and the forward line.
16
- * C none too little history: the forward line only.
17
- * All three end with the price line and the live lines.
8
+ * Sessions come in two buckets (index.ts): THROUGH Mnemom (launch ledger, a window / env
9
+ * signal, or fold evidence), whose saving is REALIZED (replay.ts: stock Claude Code
10
+ * replayed at its default window, can be negative), and NOT through Mnemom, whose saving
11
+ * is the POTENTIAL one (backtest.ts: the flat S46 rule, 23.8% of every request at or
12
+ * above 60k, for dollars and for tokens alike). Tokens and dollars are both shown
13
+ * (operator 2026-10-02). Percentages drop their decimals (rounded DOWN).
18
14
  *
19
- * Operator rulings (2026-09-30 / 2026-10-01), binding:
20
- * - Cost only. Nothing about conduct ("habits", "caught", "checked").
21
- * - Round DOWN, never overstate; frame the number around the biggest session.
22
- * - Every kept session is priced at API list prices whatever it was billed on, and the
23
- * screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
24
- * overage dollar figure or a limit-hit count.
15
+ * Variations; every number has one source and no two lines make the same claim:
16
+ * A no through-Mnemom usage, and the potential saving qualifies: the usage in
17
+ * tokens and at API prices, the cut with Mnemom, the biggest session.
18
+ * mixed both buckets have usage: one block per bucket, each with its saving line
19
+ * only if that BUCKET's saving qualifies, then the largest dollar saving
20
+ * across the shown buckets, worded by its bucket ("biggest saving ... of"
21
+ * through, "biggest session ... less with Mnemom" not through).
22
+ * through only through-Mnemom usage: its block alone, then its biggest saving.
23
+ * B >= $5 of usage but no saving line qualifies: the usage, the honest "wouldn't
24
+ * have changed that much" line (no through usage) or the bucket usage lines,
25
+ * and the forward line.
26
+ * C too little history: the forward line only.
27
+ * A saving qualifies iff it rounds DOWN to >= $5 and its token percent to >= 5%. A
28
+ * Team / Enterprise subscriber also gets the subscription line whenever a saving line is
29
+ * shown; its overage clause only when the account has extra usage enabled. All end with
30
+ * the price line and the live lines.
31
+ *
32
+ * Layout (operator 2026-10-02): a blank line between every block (each bucket's usage
33
+ * and saving lines, the biggest line, the subscriber lines, the price line, the live
34
+ * lines); the saving lines are not indented.
35
+ *
36
+ * Operator rulings (2026-09-30 .. 2026-10-02), binding:
37
+ * - Cost and usage only. Nothing about conduct ("habits", "caught", "checked").
38
+ * - Round DOWN, never overstate. A through session's saving may be negative: the
39
+ * bucket total is the honest sum, never floored per session.
40
+ * - Every session is priced at API list prices whatever it was billed on, and the
41
+ * screen says "at API prices". A subscriber never gets a seat bill, an overage dollar
42
+ * figure or a limit-hit count.
25
43
  * - No live counter is promised: `mnemom agent` has no status bar today.
26
44
  */
27
45
  export declare const WIDTH = 78;
28
46
  export declare const MAX_LINES = 22;
29
47
  /** Less history than this (API prices, in the window) is too little to backtest. */
30
48
  export declare const MIN_HISTORY_USD = 5;
31
- /** Variation A iff the shown saving is at least this many dollars and this many percent. */
49
+ /** A saving line is shown iff its saving rounds to at least this many dollars and percent. */
32
50
  export declare const STRONG_MIN_USD = 5;
33
51
  export declare const STRONG_MIN_PCT = 5;
34
52
  export declare const DAYS = 30;
@@ -42,6 +60,14 @@ export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the mo
42
60
  /** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
43
61
  export declare const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
44
62
  /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
63
+ /**
64
+ * The subscriber lines, fixed like the live lines: the operator's break, so the overage
65
+ * form reads as two lines and never as one 78-column line.
66
+ */
67
+ export declare const SUB_LINES: {
68
+ limit: string[];
69
+ overage: string[];
70
+ };
45
71
  export declare const LIVE_LINES: string[];
46
72
  /** 2 significant figures, rounded DOWN; 0 below $1. */
47
73
  export declare function sig2(x: number): number;
@@ -56,31 +82,41 @@ export declare function pair(total: number, saved: number): [number, number, num
56
82
  export declare function pctDown(part: number, whole: number): number;
57
83
  export declare function day(t: number): string;
58
84
  export declare function usd(n: number): string;
85
+ /** A token count rounded DOWN to 2 significant figures (the number it stands for). */
86
+ export declare function tokShown(n: number): number;
87
+ /**
88
+ * A token count in words, 2 significant figures DOWN: "9.0 billion", "39 billion",
89
+ * "450 million", "12 thousand"; below a thousand, the plain number.
90
+ */
91
+ export declare function tokens(n: number): string;
59
92
  export declare function clean(s: string): string;
60
93
  /** A short, safe project label from Claude Code's encoded project dir, or null. */
61
94
  export declare function labelFor(pretty: string): string | null;
62
95
  export declare function wrap(text: string, width?: number): string[];
63
- /** One kept session's requests in the window, priced at API list prices. */
96
+ /** One session's requests in the window, priced at API list prices. */
64
97
  export interface SessionCost {
65
98
  label: string | null;
66
99
  end: number;
100
+ /** What it used, at API prices (through Mnemom: the folded, billed size). */
67
101
  actual: number;
102
+ /** Tokens it used: prompt + output. */
103
+ tokens: number;
104
+ /** Through: the realized saving vs stock Claude Code (may be negative). Not: potential. */
68
105
  saved: number;
69
- /** The launch ledger proves it ran through Mnemom: its saving is said to be SAVED. */
70
- viaMnemom?: boolean;
106
+ savedTokens: number;
107
+ /** It ran through Mnemom (index.ts throughEvidence). */
108
+ through: boolean;
71
109
  }
72
- /** A Team / Enterprise subscriber's usage, at API prices (local evidence only). */
73
- export interface SubUsage {
74
- /** Subscription-billed sessions in the window (all sessions when none is tagged). */
75
- sessions: number;
76
- actual: number;
77
- saved: number;
110
+ /** A Team / Enterprise subscriber (local evidence only). */
111
+ export interface SubInfo {
112
+ /** The account has extra usage (overage) enabled: `hasExtraUsageEnabled`. */
113
+ extraUsage: boolean;
78
114
  }
79
115
  export interface ScreenInput {
80
- /** Every kept session with requests in the window. */
116
+ /** Every session with requests in the window. */
81
117
  sessions: SessionCost[];
82
118
  /** null: not a Team / Enterprise subscriber. */
83
- sub: SubUsage | null;
119
+ sub: SubInfo | null;
84
120
  }
85
121
  export interface AuditEntry {
86
122
  shown: unknown;
@@ -88,7 +124,7 @@ export interface AuditEntry {
88
124
  method: string;
89
125
  }
90
126
  export type Audit = Record<string, AuditEntry>;
91
- export type Variation = "A" | "B" | "C";
127
+ export type Variation = "A" | "mixed" | "through" | "B" | "C";
92
128
  /** -> the screen lines, the variation and the audit of every number on them. */
93
129
  export declare function buildScreen(inp: ScreenInput): {
94
130
  lines: string[];
@@ -1,27 +1,45 @@
1
1
  /**
2
2
  * The first-run screen: one plain terminal screen (at most 22 lines of at most 78
3
- * columns, ASCII) built from the backtest numbers. Pure — no I/O — so every layout is
3
+ * columns, ASCII; with the command's blank line above and below, 24 rows) built from
4
+ * the backtest numbers. Pure — no I/O — so every layout is
4
5
  * unit-tested. Every number on it is recorded in the audit with its raw value and
5
6
  * method.
6
7
  *
7
- * Exactly three variations (operator v1, 2026-10-01); every number has one source and no
8
- * two lines make the same claim:
9
- * A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
10
- * the saving, the biggest session; plus ONE subscription sentence for a Team /
11
- * Enterprise subscriber whose usage cut rounds to >= 5%. Where sessions ran
12
- * through Mnemom (launch ledger), that part is said to be SAVED: what it
13
- * saved you, and what it would have saved on the rest (operator 2026-10-02).
14
- * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
15
- * "wouldn't have changed that much" line, and the forward line.
16
- * C none too little history: the forward line only.
17
- * All three end with the price line and the live lines.
8
+ * Sessions come in two buckets (index.ts): THROUGH Mnemom (launch ledger, a window / env
9
+ * signal, or fold evidence), whose saving is REALIZED (replay.ts: stock Claude Code
10
+ * replayed at its default window, can be negative), and NOT through Mnemom, whose saving
11
+ * is the POTENTIAL one (backtest.ts: the flat S46 rule, 23.8% of every request at or
12
+ * above 60k, for dollars and for tokens alike). Tokens and dollars are both shown
13
+ * (operator 2026-10-02). Percentages drop their decimals (rounded DOWN).
18
14
  *
19
- * Operator rulings (2026-09-30 / 2026-10-01), binding:
20
- * - Cost only. Nothing about conduct ("habits", "caught", "checked").
21
- * - Round DOWN, never overstate; frame the number around the biggest session.
22
- * - Every kept session is priced at API list prices whatever it was billed on, and the
23
- * screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
24
- * overage dollar figure or a limit-hit count.
15
+ * Variations; every number has one source and no two lines make the same claim:
16
+ * A no through-Mnemom usage, and the potential saving qualifies: the usage in
17
+ * tokens and at API prices, the cut with Mnemom, the biggest session.
18
+ * mixed both buckets have usage: one block per bucket, each with its saving line
19
+ * only if that BUCKET's saving qualifies, then the largest dollar saving
20
+ * across the shown buckets, worded by its bucket ("biggest saving ... of"
21
+ * through, "biggest session ... less with Mnemom" not through).
22
+ * through only through-Mnemom usage: its block alone, then its biggest saving.
23
+ * B >= $5 of usage but no saving line qualifies: the usage, the honest "wouldn't
24
+ * have changed that much" line (no through usage) or the bucket usage lines,
25
+ * and the forward line.
26
+ * C too little history: the forward line only.
27
+ * A saving qualifies iff it rounds DOWN to >= $5 and its token percent to >= 5%. A
28
+ * Team / Enterprise subscriber also gets the subscription line whenever a saving line is
29
+ * shown; its overage clause only when the account has extra usage enabled. All end with
30
+ * the price line and the live lines.
31
+ *
32
+ * Layout (operator 2026-10-02): a blank line between every block (each bucket's usage
33
+ * and saving lines, the biggest line, the subscriber lines, the price line, the live
34
+ * lines); the saving lines are not indented.
35
+ *
36
+ * Operator rulings (2026-09-30 .. 2026-10-02), binding:
37
+ * - Cost and usage only. Nothing about conduct ("habits", "caught", "checked").
38
+ * - Round DOWN, never overstate. A through session's saving may be negative: the
39
+ * bucket total is the honest sum, never floored per session.
40
+ * - Every session is priced at API list prices whatever it was billed on, and the
41
+ * screen says "at API prices". A subscriber never gets a seat bill, an overage dollar
42
+ * figure or a limit-hit count.
25
43
  * - No live counter is promised: `mnemom agent` has no status bar today.
26
44
  */
27
45
  import { ESTIMATOR_METHOD } from "./backtest.js";
@@ -29,7 +47,7 @@ export const WIDTH = 78;
29
47
  export const MAX_LINES = 22;
30
48
  /** Less history than this (API prices, in the window) is too little to backtest. */
31
49
  export const MIN_HISTORY_USD = 5;
32
- /** Variation A iff the shown saving is at least this many dollars and this many percent. */
50
+ /** A saving line is shown iff its saving rounds to at least this many dollars and percent. */
33
51
  export const STRONG_MIN_USD = 5;
34
52
  export const STRONG_MIN_PCT = 5;
35
53
  export const DAYS = 30;
@@ -45,6 +63,17 @@ const FORWARD_SOURCE = "operator 2026-10-02: S46 C2n total bill -20.9% [-25.6, -
45
63
  /** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
46
64
  export const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
47
65
  /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
66
+ /**
67
+ * The subscriber lines, fixed like the live lines: the operator's break, so the overage
68
+ * form reads as two lines and never as one 78-column line.
69
+ */
70
+ export const SUB_LINES = {
71
+ limit: ["On your subscription, fewer tokens means more work before you hit a limit."],
72
+ overage: [
73
+ "On your subscription, fewer tokens means more work before you hit a",
74
+ "limit, and less overage when you go past it.",
75
+ ],
76
+ };
48
77
  export const LIVE_LINES = [
49
78
  "From now on it's live: your sessions run through Mnemom, which keeps long",
50
79
  "conversations lean and puts each tool call your agent makes on the record.",
@@ -87,6 +116,26 @@ export function day(t) {
87
116
  export function usd(n) {
88
117
  return `$${Math.floor(n).toLocaleString("en-US")}`;
89
118
  }
119
+ /** A token count rounded DOWN to 2 significant figures (the number it stands for). */
120
+ export function tokShown(n) {
121
+ return n >= 1 ? sig2(n) : 0;
122
+ }
123
+ /**
124
+ * A token count in words, 2 significant figures DOWN: "9.0 billion", "39 billion",
125
+ * "450 million", "12 thousand"; below a thousand, the plain number.
126
+ */
127
+ export function tokens(n) {
128
+ const v = tokShown(n);
129
+ for (const [u, w] of [
130
+ [1e9, "billion"],
131
+ [1e6, "million"],
132
+ [1e3, "thousand"],
133
+ ]) {
134
+ if (v >= u)
135
+ return `${v >= 10 * u ? String(v / u) : (v / u).toFixed(1)} ${w}`;
136
+ }
137
+ return String(v);
138
+ }
90
139
  // ── output hygiene ───────────────────────────────────────────────────────────
91
140
  const SECRETS = /(sk-[A-Za-z0-9_-]{6,}|ghp_[A-Za-z0-9]{6,}|mnm_[A-Za-z0-9_-]{6,}|op:\/\/\S+|Bearer\s+\S+|\b[0-9a-f]{40,}\b|\b[A-Za-z0-9+/]{48,}={0,2})/g;
92
141
  const PATHISH = /(\/Users\/\S+|\/home\/\S+|\/private\/\S+|~\/\S+)/g;
@@ -119,27 +168,45 @@ export function wrap(text, width = WIDTH) {
119
168
  out.push(cur);
120
169
  return out;
121
170
  }
122
- const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id, every kept " +
123
- "session's requests in the last 30 days, priced at API list prices whatever they were billed " +
124
- "on; 3 sig figs DOWN; a lower bound (side calls not in transcripts are missing)";
171
+ const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id, the bucket's " +
172
+ "requests in the last 30 days, priced at API list prices whatever they were billed on; 3 sig " +
173
+ "figs DOWN; a lower bound (side calls not in transcripts are missing)";
174
+ const TOKENS_METHOD = "Σ (input + cache read + cache write 5m + cache write 1h + output) over the same requests; " +
175
+ "2 sig figs DOWN";
125
176
  const SAVED_METHOD = `backtest.estimate(): ${ESTIMATOR_METHOD}; 2 sig figs DOWN`;
177
+ const SAVED_TOKENS_METHOD = "backtest.estimate(): ruleRate(prompt) x prompt per request (the same flat rule as the " +
178
+ "dollars); 2 sig figs DOWN";
179
+ const REALIZED_METHOD = "replay.replayStock(): stock Claude Code at its default window for the session's dominant " +
180
+ "main-thread model, replayed on the client size rebuilt from the fold drops; Σ per session, " +
181
+ "negative sessions kept; 2 sig figs DOWN";
182
+ function sum(ses) {
183
+ const b = { n: ses.length, usd: 0, tokens: 0, saved: 0, savedTokens: 0 };
184
+ for (const r of ses) {
185
+ b.usd += r.actual;
186
+ b.tokens += r.tokens;
187
+ b.saved += r.saved;
188
+ b.savedTokens += r.savedTokens;
189
+ }
190
+ return b;
191
+ }
192
+ /** The shown saving of a bucket, or null when it does not qualify. */
193
+ function qualifying(saved, savedTokens, base) {
194
+ const u = sig2(Math.max(saved, 0));
195
+ const pct = pctDown(savedTokens, base);
196
+ if (u < STRONG_MIN_USD || pct < STRONG_MIN_PCT)
197
+ return null;
198
+ // pct >= 5 implies base > 0.
199
+ return { usd: u, tok: tokShown(savedTokens), pct, rawPct: (100 * savedTokens) / base };
200
+ }
126
201
  function subLines(sub, A) {
127
202
  if (!sub)
128
203
  return [];
129
- const p = pctDown(sub.saved, sub.actual);
130
- const raw = sub.actual > 0 ? (100 * sub.saved) / sub.actual : 0;
131
- if (p < 5) {
132
- A("sub_line_omitted", { shown: true, raw, method: "usage cut below 5% (decimals dropped)" });
133
- return [];
134
- }
135
- A("sub_usage_pct", {
136
- shown: p,
137
- raw,
138
- method: "saving / API-price cost over the subscription sessions in the window (all sessions if " +
139
- "none is tagged); cost-weighted, decimals dropped",
204
+ A("sub_overage_clause", {
205
+ shown: sub.extraUsage,
206
+ raw: sub.extraUsage,
207
+ method: "the account's hasExtraUsageEnabled (authShape().extraUsage); never a dollar figure",
140
208
  });
141
- return wrap(`On your subscription that is about ${p}% less usage: fewer limit hits, and less ` +
142
- "overage if you use extra usage.");
209
+ return sub.extraUsage ? SUB_LINES.overage : SUB_LINES.limit;
143
210
  }
144
211
  /** -> the screen lines, the variation and the audit of every number on them. */
145
212
  export function buildScreen(inp) {
@@ -148,88 +215,176 @@ export function buildScreen(inp) {
148
215
  audit[k] = e;
149
216
  };
150
217
  const ses = inp.sessions;
151
- const actual = ses.reduce((t, r) => t + r.actual, 0);
152
- const saved = ses.reduce((t, r) => t + r.saved, 0);
153
- const [a, s, e] = pair(actual, saved);
154
- const p = pctDown(saved, actual);
155
- const enough = actual >= MIN_HISTORY_USD;
156
- const variation = enough && s >= STRONG_MIN_USD && p >= STRONG_MIN_PCT ? "A" : enough ? "B" : "C";
157
- A("variation", { shown: variation, raw: { actual, saved }, method: "see screen.ts header" });
218
+ const all = sum(ses);
219
+ const thr = sum(ses.filter((r) => r.through));
220
+ const not = sum(ses.filter((r) => !r.through));
221
+ // A bucket "has usage" when it shows at least $1 at API prices.
222
+ const hasThr = sig3(thr.usd) >= 1;
223
+ const hasNot = sig3(not.usd) >= 1;
224
+ const enough = all.usd >= MIN_HISTORY_USD;
225
+ // Through: the base is the without-Mnemom total (used + saved). Not: what was used.
226
+ const qThr = hasThr ? qualifying(thr.saved, thr.savedTokens, thr.tokens + thr.savedTokens) : null;
227
+ const qNot = hasNot ? qualifying(not.saved, not.savedTokens, not.tokens) : null;
228
+ const variation = !enough
229
+ ? "C"
230
+ : !qThr && !qNot
231
+ ? "B"
232
+ : !hasThr
233
+ ? "A"
234
+ : hasNot
235
+ ? "mixed"
236
+ : "through";
237
+ A("variation", {
238
+ shown: variation,
239
+ raw: { through: thr, not_through: not },
240
+ method: "see screen.ts header; raw = each bucket's honest sums (negative sessions kept)",
241
+ });
158
242
  const L = ["Welcome to Mnemom.", ""];
159
243
  L.push(...(variation === "C"
160
244
  ? wrap("We didn't find enough Claude Code history on this machine to look at.")
161
245
  : wrap(`We looked at your last ${DAYS} days of Claude Code. It all stayed on this machine.`)));
162
- L.push("");
163
- if (variation === "A") {
164
- A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
165
- A("saved_pct", {
166
- shown: p,
167
- raw: (100 * saved) / actual,
168
- method: "decimals dropped (rounded DOWN)",
169
- });
170
- const viaRaw = ses.reduce((t, r) => t + (r.viaMnemom ? r.saved : 0), 0);
171
- const via = sig2(viaRaw);
172
- const rest = sig2(saved - viaRaw);
173
- if (via < 1) {
174
- A("saved_usd", { shown: s, raw: saved, method: SAVED_METHOD });
175
- A("with_mnemom_usd", { shown: e, raw: actual - saved, method: "spent shown - saved shown" });
176
- L.push(...wrap(`At API prices that usage cost about ${usd(a)}. With Mnemom: about ${usd(e)}.`));
246
+ /** Start a block: one blank line after whatever came before. */
247
+ const gap = () => {
248
+ if (L[L.length - 1] !== "")
177
249
  L.push("");
178
- L.push(...wrap(`That's about ${usd(s)} less (${p}%), and you don't change how you work.`));
250
+ };
251
+ const usage = (key, b) => {
252
+ A(`${key}_tokens`, { shown: tokShown(b.tokens), raw: b.tokens, method: TOKENS_METHOD });
253
+ A(`${key}_usd`, { shown: sig3(b.usd), raw: b.usd, method: SPENT_METHOD });
254
+ return { t: tokens(b.tokens), u: usd(sig3(b.usd)) };
255
+ };
256
+ const saving = (key, b, q, method, tokMethod, base) => {
257
+ A(`${key}_saved_tokens`, { shown: q.tok, raw: b.savedTokens, method: tokMethod });
258
+ A(`${key}_saved_pct`, {
259
+ shown: q.pct,
260
+ raw: q.rawPct,
261
+ method: `${base}; decimals dropped (rounded DOWN)`,
262
+ });
263
+ A(`${key}_saved_usd`, { shown: q.usd, raw: b.saved, method });
264
+ };
265
+ /** The per-bucket lines; only called when the through bucket has usage. */
266
+ const bucketBlock = () => {
267
+ gap();
268
+ const { t, u } = usage("through", thr);
269
+ L.push(...wrap(`Through Mnemom: ${t} tokens, about ${u} at API prices.`));
270
+ if (qThr) {
271
+ saving("through", thr, qThr, REALIZED_METHOD, REALIZED_METHOD, "saved tokens / (used + saved)");
272
+ L.push(...wrap(`That saved about ${tokens(thr.savedTokens)} tokens (${qThr.pct}%), about ${usd(qThr.usd)}.`));
179
273
  }
180
274
  else {
181
- const m = `${SAVED_METHOD}; sessions the launch ledger shows ran through Mnemom`;
182
- A("saved_via_mnemom_usd", { shown: via, raw: viaRaw, method: m });
183
- L.push(...wrap(`At API prices that usage comes to about ${usd(a)} without Mnemom.`));
184
- L.push("");
185
- if (rest < 1) {
186
- L.push(...wrap(`Mnemom saved you about ${usd(via)} of it (${p}%), and you didn't change how you work.`));
275
+ A("through_saved_not_shown", {
276
+ shown: null,
277
+ raw: { usd: thr.saved, tokens: thr.savedTokens },
278
+ method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
279
+ });
280
+ }
281
+ if (hasNot) {
282
+ gap();
283
+ const { t, u } = usage("not_through", not);
284
+ L.push(...wrap(`Not through Mnemom: ${t} tokens, about ${u} at API prices.`));
285
+ if (qNot) {
286
+ saving("not_through", not, qNot, SAVED_METHOD, SAVED_TOKENS_METHOD, "saved tokens / used");
287
+ L.push(...wrap(`Mnemom would have cut about ${tokens(not.savedTokens)} tokens (${qNot.pct}%), ` +
288
+ `about ${usd(qNot.usd)}.`));
187
289
  }
188
290
  else {
189
- A("saved_rest_usd", {
190
- shown: rest,
191
- raw: saved - viaRaw,
192
- method: `${SAVED_METHOD}; the rest`,
291
+ A("not_through_saved_not_shown", {
292
+ shown: null,
293
+ raw: { usd: not.saved, tokens: not.savedTokens },
294
+ method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
193
295
  });
194
- L.push(...wrap(`Mnemom saved you about ${usd(via)} of it, and would have saved about ${usd(rest)} ` +
195
- "more on the sessions that didn't run through it."));
196
296
  }
197
297
  }
198
- // A has a saving, so the session with the largest saving exists and saved something.
199
- const big = ses.reduce((m, r) => (r.saved > m.saved ? r : m));
200
- const [ba, , be] = pair(big.actual, big.saved);
201
- A("biggest_session_usd", {
202
- shown: ba,
203
- raw: big.actual,
204
- method: "the session with the largest saving; its API-price cost, 3 sig figs DOWN",
205
- });
206
- A("biggest_session_with_mnemom_usd", {
207
- shown: be,
208
- raw: big.actual - big.saved,
209
- method: "its cost shown - its saving (2 sig figs DOWN)",
210
- });
211
- L.push("");
212
- const where = big.label ? `${big.label} on ${day(big.end)}` : `on ${day(big.end)}`;
213
- L.push(...wrap(big.viaMnemom
214
- ? `Your biggest session, ${where}: ${usd(ba)} without Mnemom, about ${usd(be)} with it.`
215
- : `Your biggest session, ${where}: ${usd(ba)}, with Mnemom about ${usd(be)}.`));
216
- const subL = subLines(inp.sub, A);
217
- if (subL.length)
218
- L.push("", ...subL);
298
+ };
299
+ /** The session with the largest dollar saving, if it rounds to >= $5. */
300
+ const biggest = (pool) => {
301
+ let big = null;
302
+ for (const r of pool)
303
+ if (!big || r.saved > big.saved)
304
+ big = r;
305
+ return big && sig2(big.saved) >= STRONG_MIN_USD ? big : null;
306
+ };
307
+ const where = (r) => r.label ? `${r.label} on ${day(r.end)}` : `on ${day(r.end)}`;
308
+ /**
309
+ * The biggest line, worded by the session's bucket (operator 2026-10-02): through, the
310
+ * realized saving of what it would have cost without Mnemom (used + saved); not
311
+ * through, what it cost and the would-have saving.
312
+ */
313
+ const biggestLine = (big) => {
314
+ gap();
315
+ if (big.through) {
316
+ const without = big.actual + big.saved;
317
+ const ba = sig3(without);
318
+ const bs = Math.min(sig2(big.saved), ba);
319
+ A("biggest_saving_usd", {
320
+ shown: bs,
321
+ raw: big.saved,
322
+ method: `the session with the largest dollar saving (${REALIZED_METHOD})`,
323
+ });
324
+ A("biggest_saving_of_usd", {
325
+ shown: ba,
326
+ raw: without,
327
+ method: "that session's cost without Mnemom at API prices (used + saved), 3 sig figs DOWN",
328
+ });
329
+ L.push(...wrap(`Your biggest saving, ${where(big)}: about ${usd(bs)} of ${usd(ba)}.`));
330
+ }
331
+ else {
332
+ const ba = sig3(big.actual);
333
+ const bs = Math.min(sig2(big.saved), ba);
334
+ A("biggest_session_usd", {
335
+ shown: ba,
336
+ raw: big.actual,
337
+ method: "the session with the largest saving; its API-price cost, 3 sig figs DOWN",
338
+ });
339
+ A("biggest_session_saved_usd", {
340
+ shown: bs,
341
+ raw: big.saved,
342
+ method: `${SAVED_METHOD}; that session`,
343
+ });
344
+ L.push(...wrap(`Your biggest session, ${where(big)}: ${usd(ba)}, about ${usd(bs)} less with Mnemom.`));
345
+ }
346
+ };
347
+ if (variation === "A") {
348
+ gap();
349
+ const { t, u } = usage("spent", not);
350
+ L.push(...wrap(`You used about ${t} tokens, about ${u} at API prices.`));
351
+ const q = qNot;
352
+ saving("spent", not, q, SAVED_METHOD, SAVED_TOKENS_METHOD, "saved tokens / used");
353
+ L.push(...wrap(`With Mnemom: about ${tokens(not.savedTokens)} fewer tokens (${q.pct}%), about ` +
354
+ `${usd(q.usd)} less.`));
355
+ const big = biggest(ses.filter((r) => !r.through));
356
+ if (big)
357
+ biggestLine(big);
358
+ gap();
359
+ L.push(...subLines(inp.sub, A));
360
+ }
361
+ else if (variation === "mixed" || variation === "through") {
362
+ bucketBlock();
363
+ // The largest dollar saving across the buckets whose saving line is shown.
364
+ const big = biggest(ses.filter((r) => (r.through ? qThr : qNot)));
365
+ if (big)
366
+ biggestLine(big);
367
+ gap();
368
+ L.push(...subLines(inp.sub, A));
219
369
  }
220
370
  else {
221
371
  if (variation === "B") {
222
- A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
223
- A("saved_usd_not_shown", {
224
- shown: null,
225
- raw: saved,
226
- method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
227
- });
228
- L.push(...wrap(`Your sessions used about ${usd(a)} at API prices. They stayed small, so Mnemom ` +
229
- "wouldn't have changed that much."));
372
+ if (hasThr) {
373
+ bucketBlock();
374
+ }
375
+ else {
376
+ const { t, u } = usage("spent", all);
377
+ A("saved_not_shown", {
378
+ shown: null,
379
+ raw: { usd: all.saved, tokens: all.savedTokens },
380
+ method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
381
+ });
382
+ gap();
383
+ L.push(...wrap(`Your sessions used about ${t} tokens, about ${u} at API prices. They stayed ` +
384
+ "small, so Mnemom wouldn't have changed that much."));
385
+ }
230
386
  }
231
- if (variation === "B")
232
- L.push("");
387
+ gap();
233
388
  L.push(...wrap(FORWARD_LINE));
234
389
  A("forward_range_pct", {
235
390
  shown: "15 to 25",
@@ -237,11 +392,12 @@ export function buildScreen(inp) {
237
392
  method: FORWARD_SOURCE,
238
393
  });
239
394
  }
240
- L.push("", PRICE_LINE, "", ...LIVE_LINES);
395
+ gap();
396
+ L.push(PRICE_LINE, "", ...LIVE_LINES);
241
397
  A("sessions", {
242
398
  shown: null,
243
- raw: ses.length,
244
- method: "kept sessions with requests in the window",
399
+ raw: { all: all.n, through: thr.n, not_through: not.n },
400
+ method: "sessions with requests in the window, by bucket",
245
401
  });
246
402
  return { lines: L.map(clean), audit, variation };
247
403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.8-next.0",
3
+ "version": "0.17.8",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {