@mnemom/mnemom 0.17.4 → 0.17.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.
@@ -3,6 +3,11 @@
3
3
  * backtest the user's last 30 days of Claude Code sessions on this machine and show what
4
4
  * Mnemom would have saved. `mnemom agent --first-run` shows it again at any time.
5
5
  *
6
+ * Every session in the window is priced (operator 2026-10-02: "count all sessions"),
7
+ * including ones that already ran through Mnemom: their transcripts hold the full,
8
+ * unfolded context the client sent. Only copies of one session are dropped. The governed
9
+ * classification is still run, for the audit counts only.
10
+ *
6
11
  * Privacy: READ-ONLY on ~/.claude and every transcript; no network call anywhere in this
7
12
  * module tree; only token counts, model ids, timestamps and Claude Code's own limit
8
13
  * records are read. Writes only the offer state file and a local audit (both 0600,
@@ -13,6 +18,7 @@
13
18
  * Enter / y shows the screen and records acceptance; n skips until the next launch;
14
19
  * never stops asking. A non-interactive launch is never asked and writes nothing.
15
20
  */
21
+ import { type ScopeResult, type Session } from "./scope.js";
16
22
  import { type Audit, type Variation } from "./screen.js";
17
23
  export declare const STATE_FILE = "first-run.json";
18
24
  export declare const AUDIT_FILE = "first-run-audit.json";
@@ -51,6 +57,20 @@ export interface FirstRunResult {
51
57
  }
52
58
  /** Scan, classify, price and build the screen. Read-only on every transcript. */
53
59
  export declare function computeFirstRun(opts?: ScanOptions): FirstRunResult;
60
+ /**
61
+ * The sessions the screen prices: every session in the window, one per session id (an
62
+ * ungoverned copy first), none without a parent transcript.
63
+ */
64
+ export declare function pricedSessions(scope: ScopeResult): Array<{
65
+ s: Session;
66
+ viaMnemom: boolean;
67
+ }>;
68
+ /**
69
+ * Proof a session ran through Mnemom: `mnemom agent` recorded it in the launch ledger.
70
+ * Every other governed signal (an Agent-SDK host, a text marker, a time window) only
71
+ * means it may have, so those sessions are not said to have been saved on.
72
+ */
73
+ export declare const LEDGER_SIGNALS: Set<string>;
54
74
  /** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
55
75
  export declare function assertScreenBudget(lines: readonly string[]): void;
56
76
  /** A transcript's mtime (ms), 0 when it vanished since discovery (it then sorts first). */
@@ -3,6 +3,11 @@
3
3
  * backtest the user's last 30 days of Claude Code sessions on this machine and show what
4
4
  * Mnemom would have saved. `mnemom agent --first-run` shows it again at any time.
5
5
  *
6
+ * Every session in the window is priced (operator 2026-10-02: "count all sessions"),
7
+ * including ones that already ran through Mnemom: their transcripts hold the full,
8
+ * unfolded context the client sent. Only copies of one session are dropped. The governed
9
+ * classification is still run, for the audit counts only.
10
+ *
6
11
  * Privacy: READ-ONLY on ~/.claude and every transcript; no network call anywhere in this
7
12
  * module tree; only token counts, model ids, timestamps and Claude Code's own limit
8
13
  * records are read. Writes only the offer state file and a local audit (both 0600,
@@ -95,8 +100,8 @@ export function computeFirstRun(opts = {}) {
95
100
  const counts = {
96
101
  sessions_total: scope.sessions.size,
97
102
  sessions_recent: scope.inWindow,
98
- excluded_governed_or_doubt: scope.excluded.size,
99
- kept: scope.kept.size,
103
+ governed_or_doubt: scope.excluded.size,
104
+ ungoverned: scope.kept.size,
100
105
  };
101
106
  for (const sigs of scope.excluded.values()) {
102
107
  for (const s of sigs)
@@ -104,10 +109,11 @@ export function computeFirstRun(opts = {}) {
104
109
  }
105
110
  // Oldest first, so a message copied into a later (resumed / forked) transcript is billed
106
111
  // once, to its origin.
107
- const ordered = [...scope.kept.values()]
108
- .map((s) => ({ s, m: mtimeOf(s.top) }))
109
- .sort((a, b) => a.m - b.m)
110
- .map((x) => x.s);
112
+ const priced = pricedSessions(scope);
113
+ counts.priced = priced.length;
114
+ const ordered = priced
115
+ .map((x) => ({ ...x, m: mtimeOf(x.s.top) }))
116
+ .sort((a, b) => a.m - b.m);
111
117
  const seen = new Set();
112
118
  const shapes = new Map();
113
119
  const sessions = [];
@@ -117,7 +123,7 @@ export function computeFirstRun(opts = {}) {
117
123
  // their subscription (lib/subscription-login.ts), so only they get the usage sentence.
118
124
  let orgLogin = false;
119
125
  const seatInputs = new Map();
120
- ordered.forEach((s, i) => {
126
+ ordered.forEach(({ s, viaMnemom }, i) => {
121
127
  if (i % 25 === 0)
122
128
  opts.progress?.(`pricing sessions ${i + 1}/${ordered.length}`);
123
129
  const paths = [s.top, ...[...s.subs].sort()];
@@ -154,6 +160,7 @@ export function computeFirstRun(opts = {}) {
154
160
  end,
155
161
  actual: e.actual,
156
162
  saved: e.saved,
163
+ viaMnemom,
157
164
  };
158
165
  sessions.push(sc);
159
166
  if (cls === "subscription_org")
@@ -178,6 +185,27 @@ export function computeFirstRun(opts = {}) {
178
185
  };
179
186
  return { lines, audit, variation, counts, seconds: (Date.now() - t0) / 1000 };
180
187
  }
188
+ /**
189
+ * The sessions the screen prices: every session in the window, one per session id (an
190
+ * ungoverned copy first), none without a parent transcript.
191
+ */
192
+ export function pricedSessions(scope) {
193
+ const out = new Map();
194
+ for (const key of [...scope.kept.keys(), ...scope.excluded.keys()]) {
195
+ const s = scope.sessions.get(key);
196
+ if (!s.top || out.has(s.sid))
197
+ continue;
198
+ const sigs = scope.excluded.get(key) ?? [];
199
+ out.set(s.sid, { s, viaMnemom: sigs.some((x) => LEDGER_SIGNALS.has(x)) });
200
+ }
201
+ return [...out.values()];
202
+ }
203
+ /**
204
+ * Proof a session ran through Mnemom: `mnemom agent` recorded it in the launch ledger.
205
+ * Every other governed signal (an Agent-SDK host, a text marker, a time window) only
206
+ * means it may have, so those sessions are not said to have been saved on.
207
+ */
208
+ export const LEDGER_SIGNALS = new Set(["registry_cc_id", "registry_conv_id"]);
181
209
  /** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
182
210
  export function assertScreenBudget(lines) {
183
211
  if (lines.length > MAX_LINES || lines.some((l) => l.length > WIDTH)) {
@@ -2,10 +2,13 @@
2
2
  * Which local Claude Code sessions the first-run look may count, and how each was billed.
3
3
  *
4
4
  * Operator rulings (2026-09-30 / 2026-10-01), binding:
5
- * - The ONLY exclusion is "governed" (already ran through Mnemom); when in doubt,
6
- * exclude. A headless `claude -p` session (entrypoint sdk-cli) is excluded: the env of
7
- * the script that launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
8
- * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
5
+ * - "Governed" (already ran through Mnemom, or in doubt) is classified here and
6
+ * recorded in the audit; since 2026-10-02 it no longer removes a session from the
7
+ * priced set (index.ts pricedSessions: every session is counted). A headless
8
+ * `claude -p` session (entrypoint sdk-cli) is marked: the env of the script that
9
+ * launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
10
+ * - Sessions are discovered in ~/.claude/projects, every ~/.claude-*\/projects, and
11
+ * $CLAUDE_CONFIG_DIR/projects wherever it points (a Console login often lives there).
9
12
  * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
10
13
  * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
11
14
  * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
@@ -37,8 +40,11 @@ export interface ScopeResult {
37
40
  kept: Map<string, Session>;
38
41
  excluded: Map<string, string[]>;
39
42
  }
40
- /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
41
- export declare function defaultProjectRoots(home?: string): string[];
43
+ /**
44
+ * ~/.claude/projects, ~/.claude-*\/projects and $CLAUDE_CONFIG_DIR/projects that exist
45
+ * (realpath, de-duplicated). CLAUDE_CONFIG_DIR can point anywhere, not only ~/.claude-*.
46
+ */
47
+ export declare function defaultProjectRoots(home?: string, env?: Record<string, string | undefined>): string[];
42
48
  /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
43
49
  export declare function discover(roots: readonly string[]): Map<string, Session>;
44
50
  export interface ClassifyOptions {
@@ -2,10 +2,13 @@
2
2
  * Which local Claude Code sessions the first-run look may count, and how each was billed.
3
3
  *
4
4
  * Operator rulings (2026-09-30 / 2026-10-01), binding:
5
- * - The ONLY exclusion is "governed" (already ran through Mnemom); when in doubt,
6
- * exclude. A headless `claude -p` session (entrypoint sdk-cli) is excluded: the env of
7
- * the script that launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
8
- * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
5
+ * - "Governed" (already ran through Mnemom, or in doubt) is classified here and
6
+ * recorded in the audit; since 2026-10-02 it no longer removes a session from the
7
+ * priced set (index.ts pricedSessions: every session is counted). A headless
8
+ * `claude -p` session (entrypoint sdk-cli) is marked: the env of the script that
9
+ * launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
10
+ * - Sessions are discovered in ~/.claude/projects, every ~/.claude-*\/projects, and
11
+ * $CLAUDE_CONFIG_DIR/projects wherever it points (a Console login often lives there).
9
12
  * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
10
13
  * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
11
14
  * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
@@ -44,8 +47,11 @@ function list(dir) {
44
47
  return [];
45
48
  }
46
49
  }
47
- /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
48
- export function defaultProjectRoots(home = homedir()) {
50
+ /**
51
+ * ~/.claude/projects, ~/.claude-*\/projects and $CLAUDE_CONFIG_DIR/projects that exist
52
+ * (realpath, de-duplicated). CLAUDE_CONFIG_DIR can point anywhere, not only ~/.claude-*.
53
+ */
54
+ export function defaultProjectRoots(home = homedir(), env = process.env) {
49
55
  const out = [];
50
56
  const add = (p) => {
51
57
  const rp = realOrNull(p);
@@ -57,6 +63,9 @@ export function defaultProjectRoots(home = homedir()) {
57
63
  if (d.startsWith(".claude-"))
58
64
  add(join(home, d, "projects"));
59
65
  }
66
+ const configDir = (env.CLAUDE_CONFIG_DIR ?? "").trim();
67
+ if (configDir)
68
+ add(join(configDir, "projects"));
60
69
  return out;
61
70
  }
62
71
  /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
@@ -8,7 +8,9 @@
8
8
  * two lines make the same claim:
9
9
  * A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
10
10
  * the saving, the biggest session; plus ONE subscription sentence for a Team /
11
- * Enterprise subscriber whose usage cut rounds to >= 5%.
11
+ * Enterprise subscriber whose usage cut rounds to >= 5%. Where sessions ran
12
+ * through Mnemom (launch ledger), that part is said to be SAVED: what it
13
+ * saved you, and what it would have saved on the rest (operator 2026-10-02).
12
14
  * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
13
15
  * "wouldn't have changed that much" line, and the forward line.
14
16
  * C none too little history: the forward line only.
@@ -64,6 +66,8 @@ export interface SessionCost {
64
66
  end: number;
65
67
  actual: number;
66
68
  saved: number;
69
+ /** The launch ledger proves it ran through Mnemom: its saving is said to be SAVED. */
70
+ viaMnemom?: boolean;
67
71
  }
68
72
  /** A Team / Enterprise subscriber's usage, at API prices (local evidence only). */
69
73
  export interface SubUsage {
@@ -8,7 +8,9 @@
8
8
  * two lines make the same claim:
9
9
  * A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
10
10
  * the saving, the biggest session; plus ONE subscription sentence for a Team /
11
- * Enterprise subscriber whose usage cut rounds to >= 5%.
11
+ * Enterprise subscriber whose usage cut rounds to >= 5%. Where sessions ran
12
+ * through Mnemom (launch ledger), that part is said to be SAVED: what it
13
+ * saved you, and what it would have saved on the rest (operator 2026-10-02).
12
14
  * B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
13
15
  * "wouldn't have changed that much" line, and the forward line.
14
16
  * C none too little history: the forward line only.
@@ -160,11 +162,33 @@ export function buildScreen(inp) {
160
162
  L.push("");
161
163
  if (variation === "A") {
162
164
  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
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.`));
166
+ const viaRaw = ses.reduce((t, r) => t + (r.viaMnemom ? r.saved : 0), 0);
167
+ const via = sig2(viaRaw);
168
+ const rest = sig2(saved - viaRaw);
169
+ if (via < 1) {
170
+ A("saved_usd", { shown: s, raw: saved, method: SAVED_METHOD });
171
+ A("with_mnemom_usd", { shown: e, raw: actual - saved, method: "spent shown - saved shown" });
172
+ L.push(...wrap(`At API prices that usage cost about ${usd(a)}. With Mnemom: about ${usd(e)}.`));
173
+ L.push(...wrap(`That's about ${usd(s)} less (${p}%), and you don't change how you work.`));
174
+ }
175
+ else {
176
+ const m = `${SAVED_METHOD}; sessions the launch ledger shows ran through Mnemom`;
177
+ A("saved_via_mnemom_usd", { shown: via, raw: viaRaw, method: m });
178
+ L.push(...wrap(`At API prices that usage comes to about ${usd(a)} without Mnemom.`));
179
+ if (rest < 1) {
180
+ L.push(...wrap(`Mnemom saved you about ${usd(via)} of it (${p}%), and you didn't change how you work.`));
181
+ }
182
+ else {
183
+ A("saved_rest_usd", {
184
+ shown: rest,
185
+ raw: saved - viaRaw,
186
+ method: `${SAVED_METHOD}; the rest`,
187
+ });
188
+ L.push(...wrap(`Mnemom saved you about ${usd(via)} of it, and would have saved about ${usd(rest)} ` +
189
+ "more on the sessions that didn't run through it."));
190
+ }
191
+ }
168
192
  // A has a saving, so the session with the largest saving exists and saved something.
169
193
  const big = ses.reduce((m, r) => (r.saved > m.saved ? r : m));
170
194
  const [ba, , be] = pair(big.actual, big.saved);
@@ -179,7 +203,9 @@ export function buildScreen(inp) {
179
203
  method: "its cost shown - its saving (2 sig figs DOWN)",
180
204
  });
181
205
  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)}.`));
206
+ L.push(...wrap(big.viaMnemom
207
+ ? `Your biggest session, ${where}: ${usd(ba)} without Mnemom, about ${usd(be)} with it.`
208
+ : `Your biggest session, ${where}: ${usd(ba)}, with Mnemom about ${usd(be)}.`));
183
209
  L.push(...subLines(inp.sub, A));
184
210
  }
185
211
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4",
3
+ "version": "0.17.5",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {