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

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,7 @@
13
13
  * Enter / y shows the screen and records acceptance; n skips until the next launch;
14
14
  * never stops asking. A non-interactive launch is never asked and writes nothing.
15
15
  */
16
- import { type Audit } from "./screen.js";
16
+ import { type Audit, type Variation } from "./screen.js";
17
17
  export declare const STATE_FILE = "first-run.json";
18
18
  export declare const AUDIT_FILE = "first-run-audit.json";
19
19
  export declare const OFFER_TEXT: string[];
@@ -45,6 +45,7 @@ export interface ScanOptions {
45
45
  export interface FirstRunResult {
46
46
  lines: string[];
47
47
  audit: Audit;
48
+ variation?: Variation;
48
49
  counts: Record<string, number>;
49
50
  seconds: number;
50
51
  }
@@ -18,8 +18,8 @@ import { homedir } from "node:os";
18
18
  import { dirname, join } from "node:path";
19
19
  import { estimate, loadTranscript, origContext, ruleRate } from "./backtest.js";
20
20
  import { analyzeSeat } from "./seat.js";
21
- import { authShape, billingClass, buildScope, configDirOf, prettyProject, } from "./scope.js";
22
- import { BIGGEST_MIN_SAVED_USD, BIGGEST_MIN_SECONDS, DAYS, MAX_LINES, WIDTH, buildScreen, labelFor, } from "./screen.js";
21
+ import { ORG_SEAT_TYPES, authShape, billingClass, buildScope, configDirOf, prettyProject, } from "./scope.js";
22
+ import { DAYS, MAX_LINES, WIDTH, buildScreen, labelFor, } from "./screen.js";
23
23
  export const STATE_FILE = "first-run.json";
24
24
  export const AUDIT_FILE = "first-run-audit.json";
25
25
  export const OFFER_TEXT = [
@@ -76,11 +76,6 @@ export function recordAnswer(st, ans, now) {
76
76
  out.accepted_at = iso;
77
77
  return out;
78
78
  }
79
- function planName(orgTypes) {
80
- const names = [...orgTypes].map((t) => (t === "claude_enterprise" ? "Enterprise" : "Team"));
81
- const uniq = [...new Set(names)].sort();
82
- return uniq.length === 1 ? uniq[0] : "Team or Enterprise";
83
- }
84
79
  /** Scan, classify, price and build the screen. Read-only on every transcript. */
85
80
  export function computeFirstRun(opts = {}) {
86
81
  const t0 = Date.now();
@@ -115,13 +110,13 @@ export function computeFirstRun(opts = {}) {
115
110
  .map((x) => x.s);
116
111
  const seen = new Set();
117
112
  const shapes = new Map();
118
- const api = { sessions: 0, actual: 0, saved: 0 };
119
- const sub = { sessions: 0, actual: 0, saved: 0 };
113
+ const sessions = [];
114
+ const subSessions = [];
115
+ // A Team / Enterprise subscriber: a subscription-billed session, or a Team / Enterprise
116
+ // login in any kept session's config dir. Only these plans can run through Mnemom on
117
+ // their subscription (lib/subscription-login.ts), so only they get the usage sentence.
118
+ let orgLogin = false;
120
119
  const seatInputs = new Map();
121
- const orgTypes = new Set();
122
- const candidates = [];
123
- let unsupported = 0;
124
- let unknown = 0;
125
120
  ordered.forEach((s, i) => {
126
121
  if (i % 25 === 0)
127
122
  opts.progress?.(`pricing sessions ${i + 1}/${ordered.length}`);
@@ -137,58 +132,51 @@ export function computeFirstRun(opts = {}) {
137
132
  shape = authShape(cfg, home);
138
133
  shapes.set(cfg, shape);
139
134
  }
135
+ const subLogin = shape.oauth &&
136
+ (shape.billingType ?? "").includes("subscription") &&
137
+ ORG_SEAT_TYPES.has(shape.orgType ?? "");
140
138
  const { cls } = billingClass(s, rows, shape);
141
139
  counts[`billing_${cls}`] = (counts[`billing_${cls}`] ?? 0) + 1;
142
- const rowsIn = rows.filter((r) => r.t >= cut);
143
140
  if (cls === "subscription_org") {
144
- // subscription_org implies a known Team / Enterprise organizationType.
145
- orgTypes.add(shape.orgType);
141
+ // Recorded in the audit only: the screen never shows a hit count or overage dollars.
146
142
  const acct = seatInputs.get(cfg) ?? { extra: shape.extraUsage, ss: [] };
147
143
  acct.ss.push({ rows, rates: rows.map((r) => ruleRate(origContext(r)).screen), paths });
148
144
  seatInputs.set(cfg, acct);
149
145
  }
146
+ const rowsIn = rows.filter((r) => r.t >= cut);
150
147
  if (!rowsIn.length)
151
148
  return;
152
- if (cls === "subscription_consumer" || cls === "subscription_unknown_type") {
153
- unsupported++;
154
- return;
155
- }
156
- if (cls === "unknown") {
157
- unknown++;
158
- return;
159
- }
149
+ // Every class is priced at API list prices; the screen says so.
160
150
  const e = estimate(rowsIn);
161
- const bucket = cls === "api" ? api : sub;
162
- bucket.sessions++;
163
- bucket.actual += e.actual;
164
- bucket.saved += e.saved;
165
- // Top + subagent rows are not in one time order: take the true first and last request.
166
- const start = rowsIn.reduce((m, r) => Math.min(m, r.t), Infinity);
167
151
  const end = rowsIn.reduce((m, r) => Math.max(m, r.t), -Infinity);
168
- if (end - start >= BIGGEST_MIN_SECONDS && e.saved >= BIGGEST_MIN_SAVED_USD) {
169
- candidates.push({
170
- label: labelFor(prettyProject(s.pdir)),
171
- start,
172
- end,
173
- actual: e.actual,
174
- saved: e.saved,
175
- billing: cls === "api" ? "api" : "subscription",
176
- });
177
- }
152
+ const sc = {
153
+ label: labelFor(prettyProject(s.pdir)),
154
+ end,
155
+ actual: e.actual,
156
+ saved: e.saved,
157
+ };
158
+ sessions.push(sc);
159
+ if (cls === "subscription_org")
160
+ subSessions.push(sc);
161
+ orgLogin ||= subLogin;
178
162
  });
179
163
  const seat = [...seatInputs.values()].map((a) => analyzeSeat(a.ss, a.extra, now));
180
- const biggest = candidates.sort((a, b) => b.actual - a.actual)[0] ?? null;
181
- const input = {
182
- api: api.sessions ? api : null,
183
- sub: sub.sessions ? { ...sub, plan: planName(orgTypes), seat } : null,
184
- unsupported,
185
- unknown,
186
- biggest,
187
- };
188
- const { lines, audit } = buildScreen(input);
164
+ const use = subSessions.length ? subSessions : sessions;
165
+ const sub = subSessions.length || orgLogin
166
+ ? {
167
+ sessions: use.length,
168
+ actual: use.reduce((t, r) => t + r.actual, 0),
169
+ saved: use.reduce((t, r) => t + r.saved, 0),
170
+ }
171
+ : null;
172
+ const { lines, audit, variation } = buildScreen({ sessions, sub });
189
173
  assertScreenBudget(lines);
190
- audit.seat = { shown: null, raw: seat, method: "seat.analyzeSeat per Team/Enterprise account" };
191
- return { lines, audit, counts, seconds: (Date.now() - t0) / 1000 };
174
+ audit.seat = {
175
+ shown: null,
176
+ raw: seat,
177
+ method: "seat.analyzeSeat per Team/Enterprise account; recorded, never shown",
178
+ };
179
+ return { lines, audit, variation, counts, seconds: (Date.now() - t0) / 1000 };
192
180
  }
193
181
  /** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
194
182
  export function assertScreenBudget(lines) {
@@ -4,33 +4,43 @@
4
4
  * unit-tested. Every number on it is recorded in the audit with its raw value and
5
5
  * method.
6
6
  *
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%.
12
+ * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
13
+ * "wouldn't have changed that much" line, and the forward line.
14
+ * C none too little history: the forward line only.
15
+ * All three end with the price line and the live lines.
16
+ *
7
17
  * Operator rulings (2026-09-30 / 2026-10-01), binding:
8
18
  * - Cost only. Nothing about conduct ("habits", "caught", "checked").
9
19
  * - Round DOWN, never overstate; frame the number around the biggest session.
10
- * - API-key sessions: dollars. Team / Enterprise subscription sessions: usage-limit
11
- * headroom (more work per window, fewer limit stops, less overage) plus the saving
12
- * at API list price, labelled as such — never a lower seat bill.
13
- * - A saving that rounds below $1 gets the honest "won't change … much" line.
20
+ * - Every kept session is priced at API list prices whatever it was billed on, and the
21
+ * screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
22
+ * overage dollar figure or a limit-hit count.
14
23
  * - No live counter is promised: `mnemom agent` has no status bar today.
15
24
  */
16
- import type { SeatAnalysis } from "./seat.js";
17
25
  export declare const WIDTH = 78;
18
26
  export declare const MAX_LINES = 16;
19
- /** Less history than this (list price, in the window) is too little to backtest. */
27
+ /** Less history than this (API prices, in the window) is too little to backtest. */
20
28
  export declare const MIN_HISTORY_USD = 5;
21
- /** The biggest session is shown only if it ran this long and saved at least this much. */
22
- export declare const BIGGEST_MIN_SECONDS = 1800;
23
- export declare const BIGGEST_MIN_SAVED_USD = 5;
29
+ /** Variation A iff the shown saving is at least this many dollars and this many percent. */
30
+ export declare const STRONG_MIN_USD = 5;
31
+ export declare const STRONG_MIN_PCT = 5;
24
32
  export declare const DAYS = 30;
25
33
  /**
26
- * Source: mnemom-adw scratchpad/wow-first-run/COST-REPORT.md, measured chain45 total
27
- * bill (S44 C2 fold-only -15.8%, S43 C2 -19.5%, S43 C1 -15.7%), stated as the rounded-down
28
- * range. The d240p1 ~45% figure is NOT used (its re-bill undercounts output).
34
+ * The forward line: the best finished chain45 run with its error margin (operator
35
+ * 2026-10-01, until something better): S43 C2 total bill -19.5% [-23.9, -14.9], Opus 5.5,
36
+ * n=23, 95% CI rounded to 15-25. The d240p1 ~45% figure is NOT used (its re-bill
37
+ * undercounts output).
29
38
  */
30
- export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 20% in our tests.";
39
+ export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 25% in our tests.";
40
+ /** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
41
+ export declare const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
31
42
  /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
32
43
  export declare const LIVE_LINES: string[];
33
- export declare const SUPPORTED_LINE = "Mnemom works with an API key or a Claude Team or Enterprise login.";
34
44
  /** 2 significant figures, rounded DOWN; 0 below $1. */
35
45
  export declare function sig2(x: number): number;
36
46
  /** 3 significant figures, rounded DOWN; 0 below $1. */
@@ -42,41 +52,31 @@ export declare function sig3(x: number): number;
42
52
  export declare function pair(total: number, saved: number): [number, number, number];
43
53
  /** part / whole as a percentage rounded DOWN to a multiple of 5. */
44
54
  export declare function pct5(part: number, whole: number): number;
45
- /** Wall-clock length rounded DOWN: "11 h", "1.5 h", "40 min". */
46
- export declare function hoursDown(sec: number): string;
47
55
  export declare function day(t: number): string;
48
56
  export declare function usd(n: number): string;
49
57
  export declare function clean(s: string): string;
50
58
  /** A short, safe project label from Claude Code's encoded project dir, or null. */
51
59
  export declare function labelFor(pretty: string): string | null;
52
60
  export declare function wrap(text: string, width?: number): string[];
53
- export interface Totals {
54
- sessions: number;
55
- /** List-price cost of the requests in the window. */
61
+ /** One kept session's requests in the window, priced at API list prices. */
62
+ export interface SessionCost {
63
+ label: string | null;
64
+ end: number;
56
65
  actual: number;
57
- /** The conservative saving on those requests (backtest.estimate). */
58
66
  saved: number;
59
67
  }
60
- export interface Biggest {
61
- label: string | null;
62
- start: number;
63
- end: number;
68
+ /** A Team / Enterprise subscriber's usage, at API prices (local evidence only). */
69
+ export interface SubUsage {
70
+ /** Subscription-billed sessions in the window (all sessions when none is tagged). */
71
+ sessions: number;
64
72
  actual: number;
65
73
  saved: number;
66
- billing: "api" | "subscription";
67
74
  }
68
75
  export interface ScreenInput {
69
- /** API-key sessions in the window (null: none). */
70
- api: Totals | null;
71
- /** Team / Enterprise subscription sessions in the window (null: none). */
72
- sub: (Totals & {
73
- plan: string;
74
- seat: SeatAnalysis[];
75
- }) | null;
76
- /** Sessions on a plan the gateway does not support, or with no billing evidence. */
77
- unsupported: number;
78
- unknown: number;
79
- biggest: Biggest | null;
76
+ /** Every kept session with requests in the window. */
77
+ sessions: SessionCost[];
78
+ /** null: not a Team / Enterprise subscriber. */
79
+ sub: SubUsage | null;
80
80
  }
81
81
  export interface AuditEntry {
82
82
  shown: unknown;
@@ -84,8 +84,10 @@ export interface AuditEntry {
84
84
  method: string;
85
85
  }
86
86
  export type Audit = Record<string, AuditEntry>;
87
- /** -> the screen lines and the audit of every number on them. */
87
+ export type Variation = "A" | "B" | "C";
88
+ /** -> the screen lines, the variation and the audit of every number on them. */
88
89
  export declare function buildScreen(inp: ScreenInput): {
89
90
  lines: string[];
90
91
  audit: Audit;
92
+ variation: Variation;
91
93
  };
@@ -4,38 +4,49 @@
4
4
  * unit-tested. Every number on it is recorded in the audit with its raw value and
5
5
  * method.
6
6
  *
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%.
12
+ * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
13
+ * "wouldn't have changed that much" line, and the forward line.
14
+ * C none too little history: the forward line only.
15
+ * All three end with the price line and the live lines.
16
+ *
7
17
  * Operator rulings (2026-09-30 / 2026-10-01), binding:
8
18
  * - Cost only. Nothing about conduct ("habits", "caught", "checked").
9
19
  * - Round DOWN, never overstate; frame the number around the biggest session.
10
- * - API-key sessions: dollars. Team / Enterprise subscription sessions: usage-limit
11
- * headroom (more work per window, fewer limit stops, less overage) plus the saving
12
- * at API list price, labelled as such — never a lower seat bill.
13
- * - A saving that rounds below $1 gets the honest "won't change … much" line.
20
+ * - Every kept session is priced at API list prices whatever it was billed on, and the
21
+ * screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
22
+ * overage dollar figure or a limit-hit count.
14
23
  * - No live counter is promised: `mnemom agent` has no status bar today.
15
24
  */
16
25
  import { ESTIMATOR_METHOD } from "./backtest.js";
17
26
  export const WIDTH = 78;
18
27
  export const MAX_LINES = 16;
19
- /** Less history than this (list price, in the window) is too little to backtest. */
28
+ /** Less history than this (API prices, in the window) is too little to backtest. */
20
29
  export const MIN_HISTORY_USD = 5;
21
- /** The biggest session is shown only if it ran this long and saved at least this much. */
22
- export const BIGGEST_MIN_SECONDS = 1800;
23
- export const BIGGEST_MIN_SAVED_USD = 5;
30
+ /** Variation A iff the shown saving is at least this many dollars and this many percent. */
31
+ export const STRONG_MIN_USD = 5;
32
+ export const STRONG_MIN_PCT = 5;
24
33
  export const DAYS = 30;
25
34
  /**
26
- * Source: mnemom-adw scratchpad/wow-first-run/COST-REPORT.md, measured chain45 total
27
- * bill (S44 C2 fold-only -15.8%, S43 C2 -19.5%, S43 C1 -15.7%), stated as the rounded-down
28
- * range. The d240p1 ~45% figure is NOT used (its re-bill undercounts output).
35
+ * The forward line: the best finished chain45 run with its error margin (operator
36
+ * 2026-10-01, until something better): S43 C2 total bill -19.5% [-23.9, -14.9], Opus 5.5,
37
+ * n=23, 95% CI rounded to 15-25. The d240p1 ~45% figure is NOT used (its re-bill
38
+ * undercounts output).
29
39
  */
30
- export const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 20% in our tests.";
31
- const FORWARD_SOURCE = "COST-REPORT.md measured chain45 total bill: S44 C2 -15.8%, S43 C2 -19.5%, S43 C1 -15.7%; " +
32
- "stated as 15 to 20%";
40
+ export const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 25% in our tests.";
41
+ const FORWARD_SOURCE = "operator 2026-10-01: S43 C2 total bill -19.5% [-23.9, -14.9] (Opus 5.5, n=23), 95% CI " +
42
+ "rounded to 15 to 25; measured vs stock at the 1M default";
43
+ /** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
44
+ export const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
33
45
  /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
34
46
  export const LIVE_LINES = [
35
47
  "From now on it's live: your sessions run through Mnemom, which keeps long",
36
48
  "conversations lean and puts each tool call your agent makes on the record.",
37
49
  ];
38
- export const SUPPORTED_LINE = "Mnemom works with an API key or a Claude Team or Enterprise login.";
39
50
  // ── rounding (never inflate) ─────────────────────────────────────────────────
40
51
  /** 2 significant figures, rounded DOWN; 0 below $1. */
41
52
  export function sig2(x) {
@@ -66,15 +77,6 @@ export function pct5(part, whole) {
66
77
  return 0;
67
78
  return Math.floor((100 * part) / whole / 5 + 1e-9) * 5;
68
79
  }
69
- /** Wall-clock length rounded DOWN: "11 h", "1.5 h", "40 min". */
70
- export function hoursDown(sec) {
71
- const h = sec / 3600;
72
- if (h >= 2)
73
- return `${Math.floor(h)} h`;
74
- if (h >= 1)
75
- return `${(Math.floor(h * 10) / 10).toFixed(1)} h`;
76
- return `${Math.max(Math.floor(sec / 60), 1)} min`;
77
- }
78
80
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
79
81
  export function day(t) {
80
82
  const d = new Date(t * 1000);
@@ -115,210 +117,94 @@ export function wrap(text, width = WIDTH) {
115
117
  out.push(cur);
116
118
  return out;
117
119
  }
118
- const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id, requests in the " +
119
- "last 30 days; 3 sig figs DOWN; a lower bound (side calls not in transcripts are missing)";
120
+ const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id, every kept " +
121
+ "session's requests in the last 30 days, priced at API list prices whatever they were billed " +
122
+ "on; 3 sig figs DOWN; a lower bound (side calls not in transcripts are missing)";
120
123
  const SAVED_METHOD = `backtest.estimate(): ${ESTIMATOR_METHOD}; 2 sig figs DOWN`;
121
- function apiBlock(api, both, A) {
122
- const [a, s, e] = pair(api.actual, api.saved);
123
- const p = pct5(api.saved, api.actual);
124
- const who = both ? "On your API key you spent" : "You spent";
125
- A("api_spent_usd", { shown: a, raw: api.actual, method: SPENT_METHOD });
126
- if (s < 1) {
127
- A("api_small_saving", { shown: true, raw: api.saved, method: "saving rounds below $1" });
128
- return wrap(`${who} about ${usd(a)}. Mnemom won't change your bill much.`);
129
- }
130
- A("api_saved_usd", { shown: s, raw: api.saved, method: SAVED_METHOD });
131
- A("api_with_mnemom_usd", {
132
- shown: e,
133
- raw: api.actual - api.saved,
134
- method: "spent shown - saved shown",
135
- });
136
- A("api_saved_pct", {
137
- shown: p,
138
- raw: (100 * api.saved) / api.actual,
139
- method: "rounded DOWN to 5s",
140
- });
141
- const pctPart = p >= 5 ? `, ${p}%,` : "";
142
- return [
143
- ...wrap(`${who} about ${usd(a)}. With Mnemom: about ${usd(e)}.`),
144
- ...wrap(`That's about ${usd(s)} back${pctPart} and you don't change how you work.`),
145
- ];
146
- }
147
- /** The seat-evidence line: overage paid (real dollars) or limit stops. At most 2 lines. */
148
- function seatEvidence(seat, A) {
149
- const ov = seat.flatMap((x) => (x.overage ? [x.overage] : []));
150
- const paid = ov.reduce((t, o) => t + o.paidUsd, 0);
151
- if (paid >= 1) {
152
- const wm = ov.reduce((t, o) => t + o.withMnemomUsd, 0);
153
- const [p, , w] = pair(paid, paid - wm);
154
- A("seat_overage_usd", { shown: p, raw: paid, method: ov[0].method });
155
- A("seat_overage_with_mnemom_usd", { shown: w, raw: wm, method: "paid shown - avoided shown" });
156
- return wrap(`You paid about ${usd(p)} in extra usage. With Mnemom, about ${usd(w)} of extra usage.`);
157
- }
158
- const hs = seat.flatMap((x) => (x.hits ? [x.hits] : []));
159
- const n = hs.reduce((t, h) => t + h.n, 0);
160
- if (n < 1)
124
+ function subLines(sub, A) {
125
+ if (!sub)
161
126
  return [];
162
- const wm = hs.reduce((t, h) => t + h.withMnemom, 0);
163
- const fewer = hs.reduce((t, h) => t + h.notInferableWithSaving, 0);
164
- const hrs = Math.floor(hs.reduce((t, h) => t + h.waitedS, 0) / 3600);
165
- A("seat_limit_hits", {
166
- shown: n,
167
- raw: n,
168
- method: "Claude Code's own limit records, last 30 days",
169
- });
170
- A("seat_hits_with_mnemom", { shown: wm, raw: wm, method: hs[0].method });
171
- A("seat_waited_hours", {
172
- shown: hrs,
173
- raw: hs.reduce((t, h) => t + h.waitedS, 0) / 3600,
174
- method: "hours DOWN",
175
- });
176
- const wait = hrs >= 1 ? ` and waited about ${hrs} hour${hrs === 1 ? "" : "s"}` : "";
177
- let tail = "";
178
- if (wm < n)
179
- tail = ` With Mnemom, about ${wm}.`;
180
- else if (fewer)
181
- tail = " With Mnemom, fewer.";
182
- return wrap(`You hit your plan's usage limit ${n} time${n === 1 ? "" : "s"}${wait}.${tail}`);
183
- }
184
- function subBlock(sub, A) {
185
- const [u, s, w] = pair(sub.actual, sub.saved);
186
127
  const p = pct5(sub.saved, sub.actual);
187
- const n = sub.sessions;
188
- A("sub_usage_list_usd", {
189
- shown: u,
190
- raw: sub.actual,
191
- method: `${SPENT_METHOD}; list-price equivalent, not a bill`,
192
- });
193
- const head = wrap(`On your Claude ${sub.plan} plan: ${n} session${n === 1 ? "" : "s"}, about ${usd(u)} of usage at API list price.`);
194
- if (s < 1) {
195
- A("sub_small_saving", { shown: true, raw: sub.saved, method: "saving rounds below $1" });
196
- return { lines: [...head, "Mnemom won't change your usage much."], evidence: [] };
128
+ const raw = sub.actual > 0 ? (100 * sub.saved) / sub.actual : 0;
129
+ if (p < 5) {
130
+ A("sub_line_omitted", { shown: true, raw, method: "usage cut rounds DOWN below 5%" });
131
+ return [];
197
132
  }
198
- A("sub_saved_list_usd", { shown: s, raw: sub.saved, method: SAVED_METHOD });
199
- A("sub_with_mnemom_list_usd", {
200
- shown: w,
201
- raw: sub.actual - sub.saved,
202
- method: "usage shown - saved shown",
203
- });
204
- // Using a fraction f less per request fits 1/(1-f) - 1 >= f more work in a window; we
205
- // state f itself, rounded down to 5s, which never exceeds the true headroom.
206
- A("sub_headroom_pct", {
133
+ A("sub_usage_pct", {
207
134
  shown: p,
208
- raw: (100 * sub.saved) / sub.actual,
209
- method: "states the usage saving %, rounded DOWN to 5s: a lower bound on the extra work per window, " +
210
- "which is saved / (usage - saved)",
135
+ raw,
136
+ method: "saving / API-price cost over the subscription sessions in the window (all sessions if " +
137
+ "none is tagged); cost-weighted, rounded DOWN to 5s",
211
138
  });
212
- const room = p >= 5
213
- ? `room for about ${p}% more work in each usage window before a limit.`
214
- : "a little more room in each usage window before a limit.";
215
- return {
216
- lines: [
217
- ...head,
218
- ...wrap(`With Mnemom: about ${usd(w)} at list price (not your bill), so ${room}`),
219
- ],
220
- evidence: seatEvidence(sub.seat, A),
221
- };
139
+ return wrap(`On your subscription that is about ${p}% less usage: fewer limit hits, and less ` +
140
+ "overage if you use extra usage.");
222
141
  }
223
- function biggestBlock(b, A) {
224
- const [a, , e] = pair(b.actual, b.saved);
225
- const hrs = hoursDown(b.end - b.start);
226
- const lp = b.billing === "subscription" ? " at list price" : "";
227
- A("biggest_session_usd", {
228
- shown: a,
229
- raw: b.actual,
230
- method: "largest bill among sessions >= 30 min with >= $5 saved; 3 sig figs DOWN",
231
- });
232
- A("biggest_session_with_mnemom_usd", {
233
- shown: e,
234
- raw: b.actual - b.saved,
235
- method: "its bill shown - its saving (2 sig figs DOWN)",
236
- });
237
- A("biggest_session_hours", {
238
- shown: hrs,
239
- raw: (b.end - b.start) / 3600,
240
- method: "first to last request, wall clock; DOWN",
241
- });
242
- const head = b.label
243
- ? `Your biggest session: ${b.label}, ${day(b.end)}, ${hrs}, ${usd(a)}${lp}.`
244
- : `Your biggest session, on ${day(b.end)}: ${hrs}, ${usd(a)}${lp}.`;
245
- const tail = b.billing === "subscription"
246
- ? `With Mnemom it would have used about ${usd(e)} of that.`
247
- : `With Mnemom it would have cost about ${usd(e)}.`;
248
- return [...wrap(head), tail];
249
- }
250
- /** -> the screen lines and the audit of every number on them. */
142
+ /** -> the screen lines, the variation and the audit of every number on them. */
251
143
  export function buildScreen(inp) {
252
144
  const audit = {};
253
145
  const A = (k, e) => {
254
146
  audit[k] = e;
255
147
  };
256
- const apiOk = !!inp.api && inp.api.actual >= MIN_HISTORY_USD;
257
- const subOk = !!inp.sub && inp.sub.actual >= MIN_HISTORY_USD;
258
- const sections = [];
259
- // drop: 0 = never dropped; higher = dropped first when the screen is over budget.
260
- const push = (lines, drop = 0) => {
261
- sections.push({ lines: ["", ...lines], drop });
262
- };
263
- sections.push({ lines: ["Welcome to Mnemom."], drop: 0 });
264
- let anySaving = false;
265
- if (apiOk || subOk) {
266
- push(wrap(`We looked at your last ${DAYS} days of Claude Code. It all stayed on this machine.`));
267
- // Frame the number around the biggest session: it leads.
268
- if (inp.biggest)
269
- push(biggestBlock(inp.biggest, A), 2);
270
- else
271
- A("biggest_session_omitted", {
272
- shown: true,
273
- raw: null,
274
- method: "no session >= 30 min with >= $5 saved",
275
- });
276
- if (apiOk && inp.api) {
277
- const lines = apiBlock(inp.api, subOk, A);
278
- anySaving ||= "api_saved_usd" in audit;
279
- push(lines);
280
- }
281
- if (subOk && inp.sub) {
282
- const { lines, evidence } = subBlock(inp.sub, A);
283
- anySaving ||= "sub_saved_list_usd" in audit;
284
- push(lines);
285
- if (evidence.length)
286
- sections[sections.length - 1].lines.push(...evidence);
287
- }
288
- if (!anySaving) {
289
- push([FORWARD_LINE], 3);
290
- A("forward_range_pct", {
291
- shown: "15 to 20",
292
- raw: [15.7, 15.8, 19.5],
293
- method: FORWARD_SOURCE,
294
- });
295
- }
148
+ const ses = inp.sessions;
149
+ const actual = ses.reduce((t, r) => t + r.actual, 0);
150
+ const saved = ses.reduce((t, r) => t + r.saved, 0);
151
+ const [a, s, e] = pair(actual, saved);
152
+ const p = pct5(saved, actual);
153
+ const enough = actual >= MIN_HISTORY_USD;
154
+ const variation = enough && s >= STRONG_MIN_USD && p >= STRONG_MIN_PCT ? "A" : enough ? "B" : "C";
155
+ A("variation", { shown: variation, raw: { actual, saved }, method: "see screen.ts header" });
156
+ const L = ["Welcome to Mnemom.", ""];
157
+ L.push(...(variation === "C"
158
+ ? wrap("We didn't find enough Claude Code history on this machine to look at.")
159
+ : wrap(`We looked at your last ${DAYS} days of Claude Code. It all stayed on this machine.`)));
160
+ L.push("");
161
+ if (variation === "A") {
162
+ A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
163
+ A("saved_usd", { shown: s, raw: saved, method: SAVED_METHOD });
164
+ A("with_mnemom_usd", { shown: e, raw: actual - saved, method: "spent shown - saved shown" });
165
+ A("saved_pct", { shown: p, raw: (100 * saved) / actual, method: "rounded DOWN to 5s" });
166
+ L.push(...wrap(`At API prices that usage cost about ${usd(a)}. With Mnemom: about ${usd(e)}.`));
167
+ L.push(...wrap(`That's about ${usd(s)} less (${p}%), and you don't change how you work.`));
168
+ // A has a saving, so the session with the largest saving exists and saved something.
169
+ const big = ses.reduce((m, r) => (r.saved > m.saved ? r : m));
170
+ const [ba, , be] = pair(big.actual, big.saved);
171
+ A("biggest_session_usd", {
172
+ shown: ba,
173
+ raw: big.actual,
174
+ method: "the session with the largest saving; its API-price cost, 3 sig figs DOWN",
175
+ });
176
+ A("biggest_session_with_mnemom_usd", {
177
+ shown: be,
178
+ raw: big.actual - big.saved,
179
+ method: "its cost shown - its saving (2 sig figs DOWN)",
180
+ });
181
+ const where = big.label ? `${big.label} on ${day(big.end)}` : `on ${day(big.end)}`;
182
+ L.push(...wrap(`Your biggest session, ${where}: ${usd(ba)}, with Mnemom about ${usd(be)}.`));
183
+ L.push(...subLines(inp.sub, A));
296
184
  }
297
185
  else {
298
- push(["We looked at your recent Claude Code sessions. It all stayed on this machine."]);
299
- if (inp.unsupported > 0)
300
- push([SUPPORTED_LINE]);
186
+ if (variation === "B") {
187
+ A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
188
+ A("saved_usd_not_shown", {
189
+ shown: null,
190
+ raw: saved,
191
+ method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
192
+ });
193
+ L.push(...wrap(`Your sessions used about ${usd(a)} at API prices. They stayed small, so Mnemom ` +
194
+ "wouldn't have changed that much."));
195
+ }
196
+ L.push(FORWARD_LINE);
197
+ A("forward_range_pct", {
198
+ shown: "15 to 25",
199
+ raw: [-23.9, -19.5, -14.9],
200
+ method: FORWARD_SOURCE,
201
+ });
301
202
  }
302
- push(LIVE_LINES);
203
+ L.push("", PRICE_LINE, "", ...LIVE_LINES);
303
204
  A("sessions", {
304
205
  shown: null,
305
- raw: {
306
- api: inp.api?.sessions ?? 0,
307
- subscription: inp.sub?.sessions ?? 0,
308
- unsupported: inp.unsupported,
309
- unknown: inp.unknown,
310
- },
311
- method: "kept (ungoverned) sessions with requests in the window, by billing class",
206
+ raw: ses.length,
207
+ method: "kept sessions with requests in the window",
312
208
  });
313
- const count = () => sections.reduce((t, s) => t + s.lines.length, 0);
314
- for (const level of [3, 2]) {
315
- if (count() <= MAX_LINES)
316
- break;
317
- for (let i = sections.length - 1; i >= 0; i--) {
318
- if (sections[i].drop === level)
319
- sections.splice(i, 1);
320
- }
321
- }
322
- const lines = sections.flatMap((s) => s.lines).map(clean);
323
- return { lines, audit };
209
+ return { lines: L.map(clean), audit, variation };
324
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4-next.4",
3
+ "version": "0.17.4-next.5",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {