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

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.
@@ -74,11 +74,20 @@ export interface EnsureAgentOptions {
74
74
  gatewayHost: string;
75
75
  /** Override interactivity (defaults to isInteractive()). */
76
76
  interactive?: boolean;
77
+ /** Where the served-agent cache lives (defaults to ~/.mnemom). */
78
+ cacheDir?: string;
77
79
  }
80
+ /** A bare launch never waits on the served-agent probe longer than this. */
81
+ export declare const SERVED_PROBE_TIMEOUT_MS = 10000;
78
82
  export interface EnsureAgentResult {
79
83
  slug: string;
80
84
  /** true only when this call actually birthed/claimed/published. */
81
85
  provisioned: boolean;
86
+ /**
87
+ * The mnm- id the gateway serves for (key, slug), when this call learned it (a verified name
88
+ * match, a mismatch, or a fresh birth). Absent when it never asked the gateway.
89
+ */
90
+ agentId?: string;
82
91
  }
83
92
  /**
84
93
  * Ensure `slug` is a usable governed agent in the customer's org. Returns
@@ -26,11 +26,12 @@
26
26
  import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
27
27
  import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
28
28
  import { getActiveOrg } from "../lib/cli-config.js";
29
- import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
29
+ import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
30
30
  import { fmt } from "../lib/format.js";
31
31
  import { askYesNo, isInteractive } from "../lib/prompt.js";
32
32
  import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
33
33
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
34
+ import { readServedAgent, servedFingerprint, writeServedAgent } from "../lib/served-agent-cache.js";
34
35
  import { deriveHashProof } from "./agents.js";
35
36
  import { birthThroughGateway } from "./wrap.js";
36
37
  import { resolveAnthropicKey } from "./agent.js";
@@ -104,6 +105,8 @@ export class AgentIdentityMismatchError extends Error {
104
105
  this.name = "AgentIdentityMismatchError";
105
106
  }
106
107
  }
108
+ /** A bare launch never waits on the served-agent probe longer than this. */
109
+ export const SERVED_PROBE_TIMEOUT_MS = 10_000;
107
110
  /**
108
111
  * Ensure `slug` is a usable governed agent in the customer's org. Returns
109
112
  * {provisioned:false} when it is already usable, when provisioning is not
@@ -115,33 +118,61 @@ export class AgentIdentityMismatchError extends Error {
115
118
  */
116
119
  export async function ensureGovernedAgent(opts) {
117
120
  const slug = opts.slug;
121
+ const cacheDir = opts.cacheDir ?? MNEMOM_DIR;
118
122
  const interactive = opts.interactive ?? isInteractive();
119
123
  // 1. Already usable? A named agent in the caller's fleet is claimed by them.
120
124
  const loggedIn = await isLoggedIn();
121
125
  if (loggedIn) {
122
126
  const existing = await getAgentByName(slug).catch(() => null);
123
127
  if (existing) {
128
+ // MNE-7650: the name is client-controlled and the API exposes no
129
+ // agent_hash, so a name match proves nothing about which agent the
130
+ // gateway will serve for (key, slug) — it looks agents up by
131
+ // hash(credential, name) only. Ask the gateway on EVERY launch, not just
132
+ // under --setup: the same 16-token "hello" the birth path uses answers
133
+ // with the served mnm- id on x-mnemom-agent. A bare launch that trusted
134
+ // the name recorded and reported an org agent while the gateway governed
135
+ // a different, unclaimed one (a second Anthropic key on a shared name).
136
+ //
137
+ // The answer is stable per (key, name, gateway), so a bare launch reuses a cached one and
138
+ // only probes (time-boxed) the first time; --setup always re-asks.
139
+ const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
140
+ const fp = servedFingerprint(opts.anthropicKey, slug, gatewayUrl);
141
+ const cached = opts.setup === true ? null : readServedAgent(cacheDir, fp);
142
+ let served;
143
+ try {
144
+ served =
145
+ cached ??
146
+ (await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey, {
147
+ timeoutMs: opts.setup === true ? undefined : SERVED_PROBE_TIMEOUT_MS,
148
+ }));
149
+ }
150
+ catch (err) {
151
+ // --setup must know which agent it governs; a bare launch carries on
152
+ // with the id unresolved rather than guessing it from the name.
153
+ if (opts.setup === true)
154
+ throw err;
155
+ say(fmt.dim(`mnemom agent: couldn't confirm which agent the gateway serves for '${slug}' (${err instanceof Error ? err.message : String(err)}) — launching anyway.`));
156
+ return { slug, provisioned: false };
157
+ }
158
+ if (!cached)
159
+ writeServedAgent(cacheDir, fp, served);
160
+ if (served !== existing.id) {
161
+ if (opts.setup === true)
162
+ throw new AgentIdentityMismatchError(slug, existing.id, served);
163
+ warnServedMismatch(slug, existing.id, served);
164
+ return { slug, provisioned: false, agentId: served };
165
+ }
124
166
  // On an EXPLICIT `mnemom agent setup`, repair/complete the cards on the
125
167
  // already-claimed agent (e.g. a protection card that failed on a prior run
126
168
  // before we minted its one-time grant). A bare launch leaves it alone.
127
169
  if (opts.setup === true) {
128
- // MNE-7650: the name is client-controlled and the API exposes no
129
- // agent_hash, so a name match proves nothing about which agent the
130
- // gateway will serve for (key, slug) — it looks agents up by
131
- // hash(credential, name) only. Ask the gateway: the same 16-token
132
- // "hello" the birth path uses answers with the served mnm- id on
133
- // x-mnemom-agent. Govern that agent or none.
134
- const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
135
- const served = await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey);
136
- if (served !== existing.id) {
137
- throw new AgentIdentityMismatchError(slug, existing.id, served);
138
- }
139
170
  say(fmt.dim(`mnemom agent: '${slug}' is already claimed (${existing.id}, the agent the gateway serves) — refreshing its cards.`));
140
171
  await publishDefaultCards(existing.id);
141
172
  persistAgentSlug(slug);
142
- return { slug, provisioned: true };
173
+ return { slug, provisioned: true, agentId: existing.id };
143
174
  }
144
- return { slug, provisioned: false };
175
+ return { slug, provisioned: false, agentId: existing.id };
145
176
  }
146
177
  }
147
178
  // 2. Are we allowed to provision here?
@@ -186,7 +217,17 @@ export async function ensureGovernedAgent(opts) {
186
217
  await publishDefaultCards(agentId);
187
218
  // 6. Remember the slug so future launches skip straight to usable.
188
219
  persistAgentSlug(slug);
189
- return { slug, provisioned: true };
220
+ return { slug, provisioned: true, agentId };
221
+ }
222
+ /**
223
+ * A bare launch whose name matches an org agent the gateway does NOT serve for this key. Not
224
+ * fatal (no --setup asked to govern it), but the user must know their governance lands on the
225
+ * served agent, and how to get a governed one of their own.
226
+ */
227
+ function warnServedMismatch(slug, namedId, servedId) {
228
+ say(fmt.warn(`mnemom agent: '${slug}' in your org is ${namedId}, but the gateway serves ${servedId} for this Anthropic key + name — this session is governed as ${servedId}, which is not claimed to your org.`));
229
+ say(fmt.dim(" The gateway identity is hash(Anthropic key, name), so another key on the same name is a different agent. " +
230
+ `Use a name of your own: \`mnemom agent <scenario> --agent <unique-name> --setup\` (or claim the served one: \`mnemom agent claim ${servedId} --key <this key>\`).`));
190
231
  }
191
232
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
192
233
  /**
@@ -141,6 +141,11 @@ export declare function recordSessionLaunch(ctx: {
141
141
  session: string;
142
142
  conversationId: string;
143
143
  agent: string;
144
+ /**
145
+ * The id the gateway serves, as setup learned it (null = asked, unknown). Undefined = setup
146
+ * never asked, so fall back to the org's name lookup.
147
+ */
148
+ agentId?: string | null;
144
149
  account: {
145
150
  orgId: string;
146
151
  orgLabel: string;
@@ -297,19 +297,24 @@ export async function agentCommand(scenarioArg, options = {}) {
297
297
  // provisions-with-confirm when interactive; warns and continues otherwise.
298
298
  // Skipped entirely by `--no-setup`. A provisioning failure never blocks the
299
299
  // launch — it degrades to the unclaimed (fail-open) identity with a warning.
300
+ // The agent id the gateway serves, when setup asked it (undefined = never asked, so the ledger
301
+ // falls back to the org's name lookup).
302
+ let servedAgentId;
300
303
  if (options.setup !== false && !anthropicKey) {
301
304
  // Subscription login: the gateway creates the agent in the Mnemom key's org on the first
302
305
  // turn, so there is nothing to birth or claim here.
303
306
  say(`mnemom agent: subscription login — '${agent}' is created in your org on the first turn (identity: your Mnemom API key).`);
304
307
  }
305
308
  else if (options.setup !== false && anthropicKey) {
309
+ servedAgentId = null; // asked below; stays null if setup can't tell
306
310
  try {
307
- await ensureGovernedAgent({
311
+ const ensured = await ensureGovernedAgent({
308
312
  slug: agent,
309
313
  anthropicKey,
310
314
  setup: options.setup,
311
315
  gatewayHost,
312
316
  });
317
+ servedAgentId = ensured.agentId ?? null;
313
318
  }
314
319
  catch (err) {
315
320
  // An identity mismatch is the one setup failure that must NOT degrade: the
@@ -368,6 +373,7 @@ export async function agentCommand(scenarioArg, options = {}) {
368
373
  session: scenario,
369
374
  conversationId,
370
375
  agent,
376
+ agentId: servedAgentId,
371
377
  account,
372
378
  gatewayHost,
373
379
  shape,
@@ -523,12 +529,19 @@ export async function resolveLaunchConversationId(input) {
523
529
  * ledger write failure warns to stderr and the launch proceeds regardless.
524
530
  */
525
531
  export async function recordSessionLaunch(ctx) {
532
+ // Never re-derive a known id from the name: the name can match an org agent the gateway does
533
+ // not serve for this key (MNE-7650), which is exactly what the ledger must not record.
526
534
  let agentId;
527
- try {
528
- agentId = (await getAgentByName(ctx.agent))?.id ?? null;
535
+ if (ctx.agentId !== undefined) {
536
+ agentId = ctx.agentId;
529
537
  }
530
- catch {
531
- agentId = null; // unresolvable right now — the ledger still records the launch
538
+ else {
539
+ try {
540
+ agentId = (await getAgentByName(ctx.agent))?.id ?? null;
541
+ }
542
+ catch {
543
+ agentId = null; // unresolvable right now — the ledger still records the launch
544
+ }
532
545
  }
533
546
  const result = appendSessionRecord({
534
547
  session: ctx.session,
@@ -24,5 +24,7 @@ export interface WrapOptions {
24
24
  providerKey?: string;
25
25
  }
26
26
  export declare function wrapCommand(options?: WrapOptions): Promise<void>;
27
- export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string): Promise<string>;
27
+ export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string, opts?: {
28
+ timeoutMs?: number;
29
+ }): Promise<string>;
28
30
  export {};
@@ -199,7 +199,7 @@ async function resolveProviderKey(options, prov, nonInteractive, say) {
199
199
  return key;
200
200
  }
201
201
  // ── Birth call ────────────────────────────────────────────────────────────────
202
- export async function birthThroughGateway(gatewayUrl, provider, agentName, providerKey) {
202
+ export async function birthThroughGateway(gatewayUrl, provider, agentName, providerKey, opts = {}) {
203
203
  const prov = PROVIDERS[provider];
204
204
  const authValue = provider === "openai" ? `Bearer ${providerKey}` : providerKey;
205
205
  const headers = {
@@ -235,7 +235,12 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
235
235
  messages: [{ role: "user", content: "hello" }],
236
236
  });
237
237
  }
238
- const response = await fetch(url, { method: "POST", headers, body });
238
+ const response = await fetch(url, {
239
+ method: "POST",
240
+ headers,
241
+ body,
242
+ signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
243
+ });
239
244
  const agentId = response.headers.get(AGENT_HEADER);
240
245
  if (!response.ok && !agentId) {
241
246
  const text = await response.text().catch(() => "");
@@ -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
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which mnm- agent the gateway serves for (Anthropic key, agent name, gateway).
3
+ *
4
+ * The gateway identity is hash(key, name), so the answer for one (key, name, gateway) is stable:
5
+ * once a launch has asked, later launches reuse it instead of paying for another probe
6
+ * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
+ * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ */
9
+ /**
10
+ * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
11
+ * disk, so recovering the key from it must cost what guessing a password would. Name and
12
+ * gateway are the salt — the lookup has to be deterministic, and they make each entry unique.
13
+ */
14
+ export declare function servedFingerprint(anthropicKey: string, slug: string, gateway: string): string;
15
+ /** The cached served id, or null (missing, unreadable, or not an mnm- id). */
16
+ export declare function readServedAgent(dir: string, fingerprint: string): string | null;
17
+ /** Best effort: a failed write only means the next launch asks again. */
18
+ export declare function writeServedAgent(dir: string, fingerprint: string, agentId: string): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which mnm- agent the gateway serves for (Anthropic key, agent name, gateway).
3
+ *
4
+ * The gateway identity is hash(key, name), so the answer for one (key, name, gateway) is stable:
5
+ * once a launch has asked, later launches reuse it instead of paying for another probe
6
+ * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
+ * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ */
9
+ import { scryptSync } from "node:crypto";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ const FILE = "served-agents.json";
13
+ /**
14
+ * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
15
+ * disk, so recovering the key from it must cost what guessing a password would. Name and
16
+ * gateway are the salt — the lookup has to be deterministic, and they make each entry unique.
17
+ */
18
+ export function servedFingerprint(anthropicKey, slug, gateway) {
19
+ return scryptSync(anthropicKey, `mnemom-served-agent-v1\n${slug}\n${gateway}`, 32).toString("hex");
20
+ }
21
+ function readAll(dir) {
22
+ try {
23
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, FILE), "utf8"));
24
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
25
+ ? parsed
26
+ : {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ /** The cached served id, or null (missing, unreadable, or not an mnm- id). */
33
+ export function readServedAgent(dir, fingerprint) {
34
+ const id = readAll(dir)[fingerprint];
35
+ return typeof id === "string" && /^mnm-/.test(id) ? id : null;
36
+ }
37
+ /** Best effort: a failed write only means the next launch asks again. */
38
+ export function writeServedAgent(dir, fingerprint, agentId) {
39
+ try {
40
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ const tmp = path.join(dir, `${FILE}.${process.pid}.tmp`);
42
+ fs.writeFileSync(tmp, JSON.stringify({ ...readAll(dir), [fingerprint]: agentId }, null, 2), {
43
+ mode: 0o600,
44
+ });
45
+ fs.renameSync(tmp, path.join(dir, FILE));
46
+ }
47
+ catch {
48
+ /* best effort */
49
+ }
50
+ }
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.6",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {