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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * The first-run screen: one plain terminal screen (at most 16 lines of at most 78
3
+ * columns, ASCII) built from the backtest numbers. Pure — no I/O — so every layout is
4
+ * unit-tested. Every number on it is recorded in the audit with its raw value and
5
+ * method.
6
+ *
7
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
8
+ * - Cost only. Nothing about conduct ("habits", "caught", "checked").
9
+ * - 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.
14
+ * - No live counter is promised: `mnemom agent` has no status bar today.
15
+ */
16
+ import { ESTIMATOR_METHOD } from "./backtest.js";
17
+ export const WIDTH = 78;
18
+ export const MAX_LINES = 16;
19
+ /** Less history than this (list price, in the window) is too little to backtest. */
20
+ 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;
24
+ export const DAYS = 30;
25
+ /**
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).
29
+ */
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%";
33
+ /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
34
+ export const LIVE_LINES = [
35
+ "From now on it's live: your sessions run through Mnemom, which keeps long",
36
+ "conversations lean and puts each tool call your agent makes on the record.",
37
+ ];
38
+ export const SUPPORTED_LINE = "Mnemom works with an API key or a Claude Team or Enterprise login.";
39
+ // ── rounding (never inflate) ─────────────────────────────────────────────────
40
+ /** 2 significant figures, rounded DOWN; 0 below $1. */
41
+ export function sig2(x) {
42
+ if (!(x >= 1))
43
+ return 0;
44
+ const step = 10 ** Math.max(Math.floor(Math.log10(x)) - 1, 0);
45
+ return Math.floor(x / step + 1e-9) * step;
46
+ }
47
+ /** 3 significant figures, rounded DOWN; 0 below $1. */
48
+ export function sig3(x) {
49
+ if (!(x >= 1))
50
+ return 0;
51
+ const step = 10 ** Math.max(Math.floor(Math.log10(x)) - 2, 0);
52
+ return Math.floor(x / step + 1e-9) * step;
53
+ }
54
+ /**
55
+ * (total shown, saved shown, with-Mnemom shown) with total - saved === with EXACTLY on
56
+ * screen. The implied saving is the shown saving, which never exceeds the raw saving.
57
+ */
58
+ export function pair(total, saved) {
59
+ const sv = sig2(Math.max(saved, 0));
60
+ const tot = sig3(total);
61
+ return [tot, Math.min(sv, tot), Math.max(tot - sv, 0)];
62
+ }
63
+ /** part / whole as a percentage rounded DOWN to a multiple of 5. */
64
+ export function pct5(part, whole) {
65
+ if (whole <= 0 || part <= 0)
66
+ return 0;
67
+ return Math.floor((100 * part) / whole / 5 + 1e-9) * 5;
68
+ }
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
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
79
+ export function day(t) {
80
+ const d = new Date(t * 1000);
81
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
82
+ }
83
+ export function usd(n) {
84
+ return `$${Math.floor(n).toLocaleString("en-US")}`;
85
+ }
86
+ // ── output hygiene ───────────────────────────────────────────────────────────
87
+ 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;
88
+ const PATHISH = /(\/Users\/\S+|\/home\/\S+|\/private\/\S+|~\/\S+)/g;
89
+ export function clean(s) {
90
+ return s.replace(SECRETS, "[REDACTED]").replace(PATHISH, "[path]");
91
+ }
92
+ /** A short, safe project label from Claude Code's encoded project dir, or null. */
93
+ export function labelFor(pretty) {
94
+ let base = pretty.replace(/-{1,2}\.?worktrees-/g, "/").replace(/^-+|-+$/g, "");
95
+ base = base.replace(/-?\d{8}T\d{6}Z/g, "").replace(/-?[0-9a-f]{12,}/g, "");
96
+ if (base.includes("/"))
97
+ base = base.slice(base.lastIndexOf("/") + 1);
98
+ if (!base || clean(base) !== base || !/^[A-Za-z0-9._ -]+$/.test(base))
99
+ return null;
100
+ return base.length <= 36 ? base : `${base.slice(0, 35)}~`;
101
+ }
102
+ export function wrap(text, width = WIDTH) {
103
+ const out = [];
104
+ let cur = "";
105
+ for (const w of text.split(/\s+/).filter(Boolean)) {
106
+ if (cur && cur.length + 1 + w.length > width) {
107
+ out.push(cur);
108
+ cur = w;
109
+ }
110
+ else {
111
+ cur = cur ? `${cur} ${w}` : w;
112
+ }
113
+ }
114
+ if (cur)
115
+ out.push(cur);
116
+ return out;
117
+ }
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 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)
161
+ 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
+ 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: [] };
197
+ }
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", {
207
+ 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)",
211
+ });
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
+ };
222
+ }
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. */
251
+ export function buildScreen(inp) {
252
+ const audit = {};
253
+ const A = (k, e) => {
254
+ audit[k] = e;
255
+ };
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
+ }
296
+ }
297
+ 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]);
301
+ }
302
+ push(LIVE_LINES);
303
+ A("sessions", {
304
+ 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",
312
+ });
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 };
324
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Seat economics for Team / Enterprise subscription sessions. A seat is flat-rate up to
3
+ * its usage limits; past them the user either STOPS until the window resets, or (extra
4
+ * usage on) keeps going and pays overage at API list rates. So for a seat user Mnemom's
5
+ * lower usage means more work per window, fewer or shorter stops and less overage —
6
+ * never a lower seat bill.
7
+ *
8
+ * Sources (Claude Code 2.1.285 binary):
9
+ * windows rateLimitType five_hour | seven_day (+ per-model weekly caps, folded into the
10
+ * weekly window); display names "session limit" / "weekly limit".
11
+ * stop a plan-limit 429 is written as an API-error assistant record whose text is
12
+ * "You've hit your <limit name> · resets <time>" (also "Usage limit reached …").
13
+ * overage "… now using usage credits instead of your plan limits" / "You're now using
14
+ * extra usage" mark the switch to paid overage. Measured 2026-09-30: UI-only
15
+ * today — no transcript records one — so an allowance is pinned only when a
16
+ * record does carry it.
17
+ *
18
+ * Only records Claude Code itself writes (system, API-error, or `<synthetic>` assistant
19
+ * records) count — a user or model message that merely discusses limits never does.
20
+ * Read-only; no network.
21
+ */
22
+ import { type Row } from "./backtest.js";
23
+ export declare const WINDOW_S: {
24
+ readonly five_hour: number;
25
+ readonly seven_day: number;
26
+ };
27
+ export type WindowType = keyof typeof WINDOW_S;
28
+ export declare const DAYS = 30;
29
+ export interface LimitEvent {
30
+ t: number;
31
+ kind: "hit" | "extra_on" | "extra_cap";
32
+ wtype: WindowType | null;
33
+ blockedS: number;
34
+ }
35
+ /** Limit / extra-usage events from Claude Code's own records, sorted by time. */
36
+ export declare function limitEvents(paths: readonly string[]): LimitEvent[];
37
+ export interface SeatSessionInput {
38
+ rows: readonly Row[];
39
+ /** Screen rate per row (backtest.ruleRate(...).screen). */
40
+ rates: readonly number[];
41
+ paths: readonly string[];
42
+ }
43
+ export interface SeatAnalysis {
44
+ extraUsage: boolean | null;
45
+ requests30d: number;
46
+ usageListUsd30d: number;
47
+ withMnemomListUsd30d: number;
48
+ events30d: {
49
+ hit: number;
50
+ extra_on: number;
51
+ extra_cap: number;
52
+ };
53
+ allowance: Partial<Record<WindowType, {
54
+ usd: number;
55
+ source: string;
56
+ }>>;
57
+ overage?: {
58
+ paidUsd: number;
59
+ withMnemomUsd: number;
60
+ overPeriods: number;
61
+ stayedInside: number;
62
+ method: string;
63
+ };
64
+ hits?: {
65
+ n: number;
66
+ waitedS: number;
67
+ avoided: number;
68
+ notInferable: number;
69
+ notInferableWithSaving: number;
70
+ withMnemom: number;
71
+ method: string;
72
+ };
73
+ }
74
+ /** One account's seat sessions → the seat block's numbers and their method. */
75
+ export declare function analyzeSeat(sessions: readonly SeatSessionInput[], extraUsage: boolean | null, now?: number, days?: number): SeatAnalysis;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Seat economics for Team / Enterprise subscription sessions. A seat is flat-rate up to
3
+ * its usage limits; past them the user either STOPS until the window resets, or (extra
4
+ * usage on) keeps going and pays overage at API list rates. So for a seat user Mnemom's
5
+ * lower usage means more work per window, fewer or shorter stops and less overage —
6
+ * never a lower seat bill.
7
+ *
8
+ * Sources (Claude Code 2.1.285 binary):
9
+ * windows rateLimitType five_hour | seven_day (+ per-model weekly caps, folded into the
10
+ * weekly window); display names "session limit" / "weekly limit".
11
+ * stop a plan-limit 429 is written as an API-error assistant record whose text is
12
+ * "You've hit your <limit name> · resets <time>" (also "Usage limit reached …").
13
+ * overage "… now using usage credits instead of your plan limits" / "You're now using
14
+ * extra usage" mark the switch to paid overage. Measured 2026-09-30: UI-only
15
+ * today — no transcript records one — so an allowance is pinned only when a
16
+ * record does carry it.
17
+ *
18
+ * Only records Claude Code itself writes (system, API-error, or `<synthetic>` assistant
19
+ * records) count — a user or model message that merely discusses limits never does.
20
+ * Read-only; no network.
21
+ */
22
+ import { rowCost } from "./backtest.js";
23
+ import { forEachLine, isoSeconds, parseJson } from "./lines.js";
24
+ export const WINDOW_S = { five_hour: 5 * 3600, seven_day: 7 * 86400 };
25
+ const TYPE_BY_NAME = [
26
+ ["session limit", "five_hour"],
27
+ ["weekly limit", "seven_day"],
28
+ ["opus limit", "seven_day"],
29
+ ["sonnet limit", "seven_day"],
30
+ ];
31
+ const HIT_MARKERS = ["hit your limit", "usage limit reached", "limit reached again"];
32
+ const ON_MARKERS = [
33
+ "now using extra usage",
34
+ "extra usage is now covering",
35
+ "now using usage credits instead of your plan",
36
+ "is drawing from usage credits",
37
+ "now uses usage credits",
38
+ ];
39
+ const CAP_MARKERS = ["out of extra usage"];
40
+ export const DAYS = 30;
41
+ function textOf(m) {
42
+ if (typeof m === "string")
43
+ return m;
44
+ const c = m?.content;
45
+ if (typeof c === "string")
46
+ return c;
47
+ if (Array.isArray(c)) {
48
+ return c
49
+ .map((x) => (x && typeof x === "object" && typeof x.text === "string" ? x.text : ""))
50
+ .join(" ");
51
+ }
52
+ return "";
53
+ }
54
+ /** Limit / extra-usage events from Claude Code's own records, sorted by time. */
55
+ export function limitEvents(paths) {
56
+ const ev = [];
57
+ for (const fp of paths) {
58
+ const recs = [];
59
+ forEachLine(fp, (line) => {
60
+ const d = parseJson(line);
61
+ if (!d)
62
+ return;
63
+ const t = isoSeconds(d.timestamp);
64
+ if (t === null)
65
+ return;
66
+ const m = d.message;
67
+ const own = d.type === "system" ||
68
+ d.isApiErrorMessage === true ||
69
+ (d.type === "assistant" && !!m && typeof m === "object" && m.model === "<synthetic>");
70
+ let txt = "";
71
+ if (own)
72
+ txt = textOf(m) || (typeof d.content === "string" ? d.content : "");
73
+ recs.push({ t, own, txt });
74
+ });
75
+ recs.sort((a, b) => a.t - b.t);
76
+ recs.forEach((r, i) => {
77
+ if (!r.own || !r.txt)
78
+ return;
79
+ const low = r.txt.toLowerCase();
80
+ const isHit = HIT_MARKERS.some((k) => low.includes(k)) ||
81
+ (low.includes("limit") && low.includes("resets"));
82
+ const kind = isHit
83
+ ? "hit"
84
+ : ON_MARKERS.some((k) => low.includes(k))
85
+ ? "extra_on"
86
+ : CAP_MARKERS.some((k) => low.includes(k))
87
+ ? "extra_cap"
88
+ : null;
89
+ if (!kind)
90
+ return;
91
+ const wtype = TYPE_BY_NAME.find(([n]) => low.includes(n))?.[1] ?? null;
92
+ let blockedS = 0;
93
+ if (kind === "hit") {
94
+ // The stop lasts until the session's next record not written by Claude Code itself,
95
+ // capped at the window; a session that ended at the stop waited 0 (never guessed).
96
+ const next = recs.slice(i + 1).find((x) => !x.own);
97
+ blockedS = next ? Math.min(next.t - r.t, WINDOW_S[wtype ?? "five_hour"]) : 0;
98
+ }
99
+ ev.push({ t: r.t, kind, wtype, blockedS });
100
+ });
101
+ }
102
+ return ev.sort((a, b) => a.t - b.t);
103
+ }
104
+ /** Usage windows start at the first request after the previous window ended. */
105
+ function windowIndex(ts, length) {
106
+ const out = [];
107
+ let start = null;
108
+ let k = -1;
109
+ for (const t of ts) {
110
+ if (start === null || t >= start + length) {
111
+ start = t;
112
+ k++;
113
+ }
114
+ out.push(k);
115
+ }
116
+ return out;
117
+ }
118
+ /** One account's seat sessions → the seat block's numbers and their method. */
119
+ export function analyzeSeat(sessions, extraUsage, now = Date.now() / 1000, days = DAYS) {
120
+ const cut = now - days * 86400;
121
+ const reqs = [];
122
+ for (const s of sessions) {
123
+ s.rows.forEach((r, i) => {
124
+ const c = rowCost(r);
125
+ reqs.push([r.t, c, c * (1 - (s.rates[i] ?? 0))]);
126
+ });
127
+ }
128
+ reqs.sort((a, b) => a[0] - b[0]);
129
+ const ev = limitEvents(sessions.flatMap((s) => s.paths));
130
+ const ev30 = ev.filter((e) => e.t >= cut);
131
+ const in30 = reqs.filter((x) => x[0] >= cut);
132
+ const out = {
133
+ extraUsage,
134
+ requests30d: in30.length,
135
+ usageListUsd30d: in30.reduce((a, x) => a + x[1], 0),
136
+ withMnemomListUsd30d: in30.reduce((a, x) => a + x[2], 0),
137
+ events30d: {
138
+ hit: ev30.filter((e) => e.kind === "hit").length,
139
+ extra_on: ev30.filter((e) => e.kind === "extra_on").length,
140
+ extra_cap: ev30.filter((e) => e.kind === "extra_cap").length,
141
+ },
142
+ allowance: {},
143
+ };
144
+ const ts = reqs.map((x) => x[0]);
145
+ const win = {
146
+ five_hour: windowIndex(ts, WINDOW_S.five_hour),
147
+ seven_day: windowIndex(ts, WINDOW_S.seven_day),
148
+ };
149
+ const firstAtOrAfter = (t) => {
150
+ const j = ts.findIndex((x) => x >= t);
151
+ return j === -1 ? ts.length : j;
152
+ };
153
+ // Allowance: usage in the window before an extra-usage transition record. An untyped
154
+ // marker is pinned to the shorter window; the LARGEST pin wins (less overage avoided).
155
+ for (const e of ev) {
156
+ if (e.kind !== "extra_on" || !reqs.length)
157
+ continue;
158
+ const w = e.wtype ?? "five_hour";
159
+ const i = firstAtOrAfter(e.t);
160
+ if (i === 0)
161
+ continue;
162
+ const k = win[w][i - 1];
163
+ let before = 0;
164
+ for (let j = 0; j < i; j++)
165
+ if (win[w][j] === k)
166
+ before += reqs[j][1];
167
+ const prev = out.allowance[w]?.usd ?? 0;
168
+ out.allowance[w] = { usd: Math.max(prev, before), source: "extra-usage transition record" };
169
+ }
170
+ // Overage (extra usage on): a request is overage once ANY window with a known allowance
171
+ // is used up. Same windows in both worlds (request times do not move).
172
+ const allow = Object.entries(out.allowance);
173
+ if (extraUsage && allow.length) {
174
+ const walk = (col) => {
175
+ const used = new Map();
176
+ let over = 0;
177
+ const periods = new Set();
178
+ reqs.forEach((x, j) => {
179
+ let isOver = false;
180
+ for (const [w, cap] of allow) {
181
+ const id = `${w}:${win[w][j]}`;
182
+ if ((used.get(id) ?? 0) >= cap.usd) {
183
+ isOver = true;
184
+ if (x[0] >= cut)
185
+ periods.add(id);
186
+ }
187
+ used.set(id, (used.get(id) ?? 0) + x[col]);
188
+ }
189
+ if (isOver && x[0] >= cut)
190
+ over += x[col];
191
+ });
192
+ return { over, periods };
193
+ };
194
+ const a = walk(1);
195
+ const b = walk(2);
196
+ out.overage = {
197
+ paidUsd: a.over,
198
+ withMnemomUsd: b.over,
199
+ overPeriods: a.periods.size,
200
+ stayedInside: [...a.periods].filter((p) => !b.periods.has(p)).length,
201
+ method: "overage = list-price usage past the allowance in its window; with Mnemom the same walk on " +
202
+ "usage x (1 - rate); a period stays inside when it has no overage left",
203
+ };
204
+ }
205
+ // Stops: a hit is AVOIDED only if the saving in its window before the stop covers the
206
+ // FULL list cost of the work done in the 5 h after the stop ended.
207
+ const hits = ev30.filter((e) => e.kind === "hit");
208
+ if (hits.length) {
209
+ let avoided = 0;
210
+ let unknown = 0;
211
+ let unknownSaving = 0;
212
+ for (const e of hits) {
213
+ const w = e.wtype ?? "five_hour";
214
+ const i = firstAtOrAfter(e.t);
215
+ if (i === 0) {
216
+ unknown++;
217
+ continue;
218
+ }
219
+ const k = win[w][i - 1];
220
+ let saved = 0;
221
+ for (let j = 0; j < i; j++)
222
+ if (win[w][j] === k)
223
+ saved += reqs[j][1] - reqs[j][2];
224
+ const resume = e.t + e.blockedS;
225
+ const cont = e.blockedS
226
+ ? reqs.filter((x) => resume <= x[0] && x[0] < resume + WINDOW_S.five_hour)
227
+ : [];
228
+ if (!cont.length) {
229
+ unknown++;
230
+ if (saved > 0)
231
+ unknownSaving++;
232
+ continue;
233
+ }
234
+ if (saved >= cont.reduce((a, x) => a + x[1], 0))
235
+ avoided++;
236
+ }
237
+ out.hits = {
238
+ n: hits.length,
239
+ waitedS: hits.reduce((a, e) => a + e.blockedS, 0),
240
+ avoided,
241
+ notInferable: unknown,
242
+ notInferableWithSaving: unknownSaving,
243
+ withMnemom: hits.length - avoided,
244
+ method: "a hit is avoided only if the list-price saving in its window before the stop >= the full " +
245
+ "list-price cost of the work done in the 5 h after the stop ended; a hit whose overshoot " +
246
+ "cannot be inferred counts as NOT avoided",
247
+ };
248
+ }
249
+ return out;
250
+ }