@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.
- package/dist/lib/first-run/backtest.d.ts +23 -3
- package/dist/lib/first-run/backtest.js +35 -4
- package/dist/lib/first-run/index.d.ts +35 -14
- package/dist/lib/first-run/index.js +82 -37
- package/dist/lib/first-run/replay.d.ts +86 -0
- package/dist/lib/first-run/replay.js +162 -0
- package/dist/lib/first-run/screen.d.ts +67 -31
- package/dist/lib/first-run/screen.js +260 -104
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
12
|
-
* module tree; only token counts, model ids, timestamps
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
12
|
-
* module tree; only token counts, model ids, timestamps
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
132
|
+
let subscriber = false;
|
|
133
|
+
let extraUsage = false;
|
|
125
134
|
const seatInputs = new Map();
|
|
126
|
-
ordered.forEach(({ s,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
106
|
+
savedTokens: number;
|
|
107
|
+
/** It ran through Mnemom (index.ts throughEvidence). */
|
|
108
|
+
through: boolean;
|
|
71
109
|
}
|
|
72
|
-
/** A Team / Enterprise subscriber
|
|
73
|
-
export interface
|
|
74
|
-
/**
|
|
75
|
-
|
|
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
|
|
116
|
+
/** Every session with requests in the window. */
|
|
81
117
|
sessions: SessionCost[];
|
|
82
118
|
/** null: not a Team / Enterprise subscriber. */
|
|
83
|
-
sub:
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
/**
|
|
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,
|
|
123
|
-
"
|
|
124
|
-
"
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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("
|
|
190
|
-
shown:
|
|
191
|
-
raw: saved
|
|
192
|
-
method:
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
+
gap();
|
|
396
|
+
L.push(PRICE_LINE, "", ...LIVE_LINES);
|
|
241
397
|
A("sessions", {
|
|
242
398
|
shown: null,
|
|
243
|
-
raw:
|
|
244
|
-
method: "
|
|
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
|
}
|