@mnemom/mnemom 0.17.4-next.3 → 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.
@@ -0,0 +1,210 @@
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
+ * 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
+ *
17
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
18
+ * - Cost only. Nothing about conduct ("habits", "caught", "checked").
19
+ * - Round DOWN, never overstate; frame the number around the biggest session.
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.
23
+ * - No live counter is promised: `mnemom agent` has no status bar today.
24
+ */
25
+ import { ESTIMATOR_METHOD } from "./backtest.js";
26
+ export const WIDTH = 78;
27
+ export const MAX_LINES = 16;
28
+ /** Less history than this (API prices, in the window) is too little to backtest. */
29
+ export const MIN_HISTORY_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;
33
+ export const DAYS = 30;
34
+ /**
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).
39
+ */
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.";
45
+ /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
46
+ export const LIVE_LINES = [
47
+ "From now on it's live: your sessions run through Mnemom, which keeps long",
48
+ "conversations lean and puts each tool call your agent makes on the record.",
49
+ ];
50
+ // ── rounding (never inflate) ─────────────────────────────────────────────────
51
+ /** 2 significant figures, rounded DOWN; 0 below $1. */
52
+ export function sig2(x) {
53
+ if (!(x >= 1))
54
+ return 0;
55
+ const step = 10 ** Math.max(Math.floor(Math.log10(x)) - 1, 0);
56
+ return Math.floor(x / step + 1e-9) * step;
57
+ }
58
+ /** 3 significant figures, rounded DOWN; 0 below $1. */
59
+ export function sig3(x) {
60
+ if (!(x >= 1))
61
+ return 0;
62
+ const step = 10 ** Math.max(Math.floor(Math.log10(x)) - 2, 0);
63
+ return Math.floor(x / step + 1e-9) * step;
64
+ }
65
+ /**
66
+ * (total shown, saved shown, with-Mnemom shown) with total - saved === with EXACTLY on
67
+ * screen. The implied saving is the shown saving, which never exceeds the raw saving.
68
+ */
69
+ export function pair(total, saved) {
70
+ const sv = sig2(Math.max(saved, 0));
71
+ const tot = sig3(total);
72
+ return [tot, Math.min(sv, tot), Math.max(tot - sv, 0)];
73
+ }
74
+ /** part / whole as a percentage rounded DOWN to a multiple of 5. */
75
+ export function pct5(part, whole) {
76
+ if (whole <= 0 || part <= 0)
77
+ return 0;
78
+ return Math.floor((100 * part) / whole / 5 + 1e-9) * 5;
79
+ }
80
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
81
+ export function day(t) {
82
+ const d = new Date(t * 1000);
83
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
84
+ }
85
+ export function usd(n) {
86
+ return `$${Math.floor(n).toLocaleString("en-US")}`;
87
+ }
88
+ // ── output hygiene ───────────────────────────────────────────────────────────
89
+ 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;
90
+ const PATHISH = /(\/Users\/\S+|\/home\/\S+|\/private\/\S+|~\/\S+)/g;
91
+ export function clean(s) {
92
+ return s.replace(SECRETS, "[REDACTED]").replace(PATHISH, "[path]");
93
+ }
94
+ /** A short, safe project label from Claude Code's encoded project dir, or null. */
95
+ export function labelFor(pretty) {
96
+ let base = pretty.replace(/-{1,2}\.?worktrees-/g, "/").replace(/^-+|-+$/g, "");
97
+ base = base.replace(/-?\d{8}T\d{6}Z/g, "").replace(/-?[0-9a-f]{12,}/g, "");
98
+ if (base.includes("/"))
99
+ base = base.slice(base.lastIndexOf("/") + 1);
100
+ if (!base || clean(base) !== base || !/^[A-Za-z0-9._ -]+$/.test(base))
101
+ return null;
102
+ return base.length <= 36 ? base : `${base.slice(0, 35)}~`;
103
+ }
104
+ export function wrap(text, width = WIDTH) {
105
+ const out = [];
106
+ let cur = "";
107
+ for (const w of text.split(/\s+/).filter(Boolean)) {
108
+ if (cur && cur.length + 1 + w.length > width) {
109
+ out.push(cur);
110
+ cur = w;
111
+ }
112
+ else {
113
+ cur = cur ? `${cur} ${w}` : w;
114
+ }
115
+ }
116
+ if (cur)
117
+ out.push(cur);
118
+ return out;
119
+ }
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)";
123
+ const SAVED_METHOD = `backtest.estimate(): ${ESTIMATOR_METHOD}; 2 sig figs DOWN`;
124
+ function subLines(sub, A) {
125
+ if (!sub)
126
+ return [];
127
+ const p = pct5(sub.saved, sub.actual);
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 [];
132
+ }
133
+ A("sub_usage_pct", {
134
+ shown: p,
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",
138
+ });
139
+ return wrap(`On your subscription that is about ${p}% less usage: fewer limit hits, and less ` +
140
+ "overage if you use extra usage.");
141
+ }
142
+ /** -> the screen lines, the variation and the audit of every number on them. */
143
+ export function buildScreen(inp) {
144
+ const audit = {};
145
+ const A = (k, e) => {
146
+ audit[k] = e;
147
+ };
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));
184
+ }
185
+ else {
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
+ });
202
+ }
203
+ L.push("", PRICE_LINE, "", ...LIVE_LINES);
204
+ A("sessions", {
205
+ shown: null,
206
+ raw: ses.length,
207
+ method: "kept sessions with requests in the window",
208
+ });
209
+ return { lines: L.map(clean), audit, variation };
210
+ }
@@ -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
+ }
package/dist/rc-proxy.mjs CHANGED
@@ -77,6 +77,8 @@
77
77
  // MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
78
78
  // MNEMOM_AGENT_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
79
79
  // MNEMOM_AGENT_RC_PORT listen port (default 0 = ephemeral)
80
+ // MNEMOM_AGENT_TURN_HEADER 0/off: no x-mnemom-turn header (default on in implicit mode;
81
+ // see turn-origin.mjs)
80
82
  //
81
83
  // stdout: exactly one line, `READY <port> <ca.pem path>`, once listening. Node ≥ 18, no deps.
82
84
  import http from "node:http";
@@ -87,6 +89,7 @@ import os from "node:os";
87
89
  import path from "node:path";
88
90
  import crypto from "node:crypto";
89
91
  import { execFileSync } from "node:child_process";
92
+ import { TURN_HEADER, TurnOrigins, turnHeaderEnabled, turnHeaderFor } from "./turn-origin.mjs";
90
93
 
91
94
  const MITM_HOST = "api.anthropic.com";
92
95
  const env = process.env;
@@ -118,6 +121,9 @@ if (THREAD_MODE !== "reject" && THREAD_MODE !== "strip")
118
121
  const LOGFILE = env.MNEMOM_AGENT_RC_PROXY_LOG || "";
119
122
  const PARENT = Number(env.MNEMOM_AGENT_RC_PARENT_PID || 0);
120
123
  const PORT = Number(env.MNEMOM_AGENT_RC_PORT || 0);
124
+ // x-mnemom-turn (turn-origin.mjs): each main request's turn origins, read from Claude Code's own
125
+ // transcript. Implicit goal mode only — a sealed contract does not infer a goal from the user.
126
+ const TURNS = GOAL_MODE && turnHeaderEnabled(env) ? new TurnOrigins() : null;
121
127
 
122
128
  const log = (line) => {
123
129
  // One record per line: gateway header values and error bodies are spliced into log lines, so a
@@ -411,6 +417,7 @@ function handle(req, res, viaPlain) {
411
417
  agent: gatewayAgent,
412
418
  };
413
419
  noteCcSession(headers["x-claude-code-session-id"]);
420
+ delete headers["x-mnemom-turn"]; // only this proxy states turn origins
414
421
  if (!SUBSCRIPTION) {
415
422
  delete headers.authorization; // the OAuth bearer stays local
416
423
  headers["x-api-key"] = secrets.anthropic;
@@ -523,7 +530,15 @@ function handle(req, res, viaPlain) {
523
530
  rejectThread(res, tag, thread);
524
531
  return;
525
532
  }
526
- forward(body);
533
+ if (!TURNS) {
534
+ forward(body);
535
+ return;
536
+ }
537
+ turnHeaderFor(TURNS, headers).then((t) => {
538
+ if (t.value) headers[TURN_HEADER] = t.value;
539
+ log(`${tag} turn=${t.note}`);
540
+ forward(body);
541
+ });
527
542
  });
528
543
  } else {
529
544
  forward();