@mnemom/mnemom 0.17.31 → 0.17.32-next.0

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.
Files changed (40) hide show
  1. package/dist/base-url-proxy.mjs +30 -0
  2. package/dist/commands/agent-fuel.d.ts +75 -0
  3. package/dist/commands/agent-fuel.js +298 -0
  4. package/dist/commands/agent-implicit-notice.d.ts +31 -0
  5. package/dist/commands/agent-implicit-notice.js +73 -0
  6. package/dist/commands/agent-session.d.ts +2 -1
  7. package/dist/commands/agent-session.js +2 -1
  8. package/dist/commands/agent.d.ts +52 -1
  9. package/dist/commands/agent.js +111 -26
  10. package/dist/commands/status.js +1 -1
  11. package/dist/commands/update.js +3 -1
  12. package/dist/commands/validate.d.ts +1 -1
  13. package/dist/commands/validate.js +1 -1
  14. package/dist/index.js +51 -12
  15. package/dist/lib/agent.d.ts +32 -19
  16. package/dist/lib/agent.js +34 -32
  17. package/dist/lib/api.d.ts +53 -0
  18. package/dist/lib/api.js +31 -10
  19. package/dist/lib/canonical-json.d.ts +12 -0
  20. package/dist/lib/canonical-json.js +22 -0
  21. package/dist/lib/conduct-receipt.d.ts +81 -0
  22. package/dist/lib/conduct-receipt.js +177 -0
  23. package/dist/lib/contract-hash.d.ts +61 -0
  24. package/dist/lib/contract-hash.js +126 -0
  25. package/dist/lib/experimental.d.ts +10 -1
  26. package/dist/lib/experimental.js +35 -1
  27. package/dist/lib/oauth.d.ts +2 -1
  28. package/dist/lib/oauth.js +2 -1
  29. package/dist/lib/statusline-bin.d.ts +2 -2
  30. package/dist/lib/statusline-bin.js +4 -7
  31. package/dist/lib/statusline-feed.d.ts +33 -0
  32. package/dist/lib/statusline-feed.js +68 -0
  33. package/dist/lib/statusline.d.ts +39 -88
  34. package/dist/lib/statusline.js +58 -250
  35. package/dist/lib/update/decide.d.ts +22 -3
  36. package/dist/lib/update/decide.js +35 -12
  37. package/dist/lib/update/doctor.js +5 -1
  38. package/dist/lib/update/run.d.ts +2 -0
  39. package/dist/lib/update/run.js +22 -8
  40. package/package.json +1 -1
@@ -75,7 +75,7 @@ export const EXPERIMENTAL_FEATURES = [
75
75
  {
76
76
  // No flag, launcher.toml key or env of its own: `mnemom experimental enable` is the switch.
77
77
  id: "measured-savings",
78
- summary: "The status bar shows the saving the gateway measured on this session instead of the benchmark estimate",
78
+ summary: "The gateway measures each request's saving into the response usage (the status bar never shows it)",
79
79
  since: "0.17.16",
80
80
  gates: {
81
81
  flags: [],
@@ -84,6 +84,29 @@ export const EXPERIMENTAL_FEATURES = [
84
84
  env: [],
85
85
  },
86
86
  },
87
+ {
88
+ // No flag, launcher.toml key or env of its own: `mnemom experimental enable` is the switch.
89
+ id: "record",
90
+ summary: "Print the conduct receipt at exit: the session's measured model cost and μ, or the record link",
91
+ since: "0.17.32",
92
+ gates: {
93
+ flags: [],
94
+ configKeys: [],
95
+ commands: [],
96
+ env: [],
97
+ },
98
+ },
99
+ {
100
+ id: "fuel",
101
+ summary: "The fuel step: `mnemom agent fuel <file>` seals a batch of job contracts with one confirmation and commits them to the repo",
102
+ since: "0.17.32",
103
+ gates: {
104
+ flags: [],
105
+ configKeys: [],
106
+ commands: ["mnemom agent fuel"],
107
+ env: [],
108
+ },
109
+ },
87
110
  ];
88
111
  export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
89
112
  export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
@@ -126,6 +149,17 @@ export function resolveExperimental(opts) {
126
149
  unknown: stored.filter((t) => !isExperimentalId(t)),
127
150
  };
128
151
  }
152
+ /**
153
+ * Features a prerelease (`-next.N`) build makes available without `mnemom experimental
154
+ * enable`: door one's T16 turns implicit goals on by default on `next` (decisions 12 and
155
+ * 22), so its flag, its launcher.toml value and `mnemom agent goal` are not refused there.
156
+ * Stable is unchanged.
157
+ */
158
+ export const PRERELEASE_AVAILABLE = ["implicit"];
159
+ /** The features whose flags, keys and commands are usable: the enabled ones, plus PRERELEASE_AVAILABLE on a prerelease. */
160
+ export function availableFeatures(enabled, prerelease) {
161
+ return normalize(prerelease ? [...enabled, ...PRERELEASE_AVAILABLE] : enabled);
162
+ }
129
163
  /** The one-line stderr notice for a launch with experimental features on; null when none are. */
130
164
  export function experimentalNotice(enabled) {
131
165
  if (enabled.length === 0)
@@ -42,7 +42,8 @@ export interface AuthServerMetadata {
42
42
  }
43
43
  /**
44
44
  * Fetch (and process-cache) the AS metadata document. We resolve it relative to
45
- * the active API base URL so staging/local point at their own AS.
45
+ * the active API base URL so local (and any non-default `Environment`) points
46
+ * at its own AS.
46
47
  */
47
48
  export declare function discover(): Promise<AuthServerMetadata>;
48
49
  /** Reset the process-level discovery cache (used by tests). */
package/dist/lib/oauth.js CHANGED
@@ -117,7 +117,8 @@ async function oauthFetch(what, url, init = {}) {
117
117
  let cachedMetadata = null;
118
118
  /**
119
119
  * Fetch (and process-cache) the AS metadata document. We resolve it relative to
120
- * the active API base URL so staging/local point at their own AS.
120
+ * the active API base URL so local (and any non-default `Environment`) points
121
+ * at its own AS.
121
122
  */
122
123
  export async function discover() {
123
124
  if (cachedMetadata)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Entry for the `mnemom agent` status-bar counter (lib/statusline.ts). Claude Code runs it
3
- * on every status update, so it imports only the counter — not the whole CLI.
2
+ * Entry for the `mnemom agent` status-bar segment (lib/statusline.ts). Claude Code runs it
3
+ * on every status update, so it imports only the segment — not the whole CLI.
4
4
  */
5
5
  export {};
@@ -1,17 +1,14 @@
1
1
  /**
2
- * Entry for the `mnemom agent` status-bar counter (lib/statusline.ts). Claude Code runs it
3
- * on every status update, so it imports only the counter — not the whole CLI.
2
+ * Entry for the `mnemom agent` status-bar segment (lib/statusline.ts). Claude Code runs it
3
+ * on every status update, so it imports only the segment — not the whole CLI.
4
4
  */
5
- import { homedir } from "node:os";
6
- import { join } from "node:path";
7
- import { INNER_ENV, measuredMode, render } from "./statusline.js";
5
+ import { FEED_ENV, INNER_ENV, render } from "./statusline.js";
8
6
  const chunks = [];
9
7
  process.stdin.on("data", (c) => chunks.push(c));
10
8
  process.stdin.on("end", () => {
11
9
  const line = render(Buffer.concat(chunks).toString("utf8"), {
12
- stateDir: join(homedir(), ".mnemom", "statusline"),
13
10
  inner: process.env[INNER_ENV] || undefined,
14
- measured: measuredMode(process.env),
11
+ feed: process.env[FEED_ENV] || undefined,
15
12
  });
16
13
  process.stdout.write(`${line}\n`);
17
14
  });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The launcher side of the fed status line (W1.4, decision 14), prerelease only.
3
+ *
4
+ * The local base-URL proxy records the gateway's `x-mnemom-lineage-key` for this session in
5
+ * its record-state file (the one the conduct receipt reads). While Claude Code runs, the launcher reads Record's accumulator totals for that
6
+ * lineage every few seconds and writes them to the feed file the status-line bin reads
7
+ * (lib/statusline.ts). The bin never touches the network.
8
+ *
9
+ * Fail open everywhere: no lineage yet, a dark route (404), a non-billing role (403) or any
10
+ * error leaves the feed as it was, so the status line stays "mnemom" until fed.
11
+ */
12
+ import { type RecordTotals } from "./api.js";
13
+ /** How often the launcher re-reads the totals. */
14
+ export declare const FEED_INTERVAL_MS = 10000;
15
+ export interface FeedOptions {
16
+ /** The proxy's record-state JSON (MNEMOM_AGENT_BU_RECORD_STATE), carrying `lineage_key`. */
17
+ recordStateFile: string;
18
+ /** Read by the status-line bin (MNEMOM_STATUSLINE_FEED). */
19
+ feedFile: string;
20
+ orgId: string;
21
+ intervalMs?: number;
22
+ /** Test seam; the real read is getRecordTotals (throws on any non-200). */
23
+ fetchTotals?: (orgId: string, lineageKey: string) => Promise<RecordTotals | null>;
24
+ }
25
+ /**
26
+ * The lineage key the proxy recorded, or null when there is none (yet) or it is not the
27
+ * gateway's `cid:`/`cr:` shape (the proxy's own check is looser; this read is exact).
28
+ */
29
+ export declare function readLineage(path: string): string | null;
30
+ /** One poll: read the lineage, fetch its totals, write the feed. Never throws. */
31
+ export declare function feedOnce(o: FeedOptions): Promise<boolean>;
32
+ /** Poll until stopped. The timer is unref'd, so it never holds the launcher open. */
33
+ export declare function startFeed(o: FeedOptions): () => void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The launcher side of the fed status line (W1.4, decision 14), prerelease only.
3
+ *
4
+ * The local base-URL proxy records the gateway's `x-mnemom-lineage-key` for this session in
5
+ * its record-state file (the one the conduct receipt reads). While Claude Code runs, the launcher reads Record's accumulator totals for that
6
+ * lineage every few seconds and writes them to the feed file the status-line bin reads
7
+ * (lib/statusline.ts). The bin never touches the network.
8
+ *
9
+ * Fail open everywhere: no lineage yet, a dark route (404), a non-billing role (403) or any
10
+ * error leaves the feed as it was, so the status line stays "mnemom" until fed.
11
+ */
12
+ import { renameSync, writeFileSync } from "node:fs";
13
+ import { getRecordTotals } from "./api.js";
14
+ import { readRecordState } from "./conduct-receipt.js";
15
+ import { LINEAGE_KEY_RE, MMU_RE } from "./statusline.js";
16
+ /** How often the launcher re-reads the totals. */
17
+ export const FEED_INTERVAL_MS = 10_000;
18
+ /**
19
+ * The lineage key the proxy recorded, or null when there is none (yet) or it is not the
20
+ * gateway's `cid:`/`cr:` shape (the proxy's own check is looser; this read is exact).
21
+ */
22
+ export function readLineage(path) {
23
+ const key = readRecordState(path).lineageKey;
24
+ return key && LINEAGE_KEY_RE.test(key) ? key : null;
25
+ }
26
+ /** One poll: read the lineage, fetch its totals, write the feed. Never throws. */
27
+ export async function feedOnce(o) {
28
+ const key = readLineage(o.recordStateFile);
29
+ if (!key)
30
+ return false;
31
+ try {
32
+ const totals = await (o.fetchTotals ?? getRecordTotals)(o.orgId, key);
33
+ if (!totals || totals.lineage_key !== key)
34
+ return false;
35
+ // Persist only amounts the bin would render: a malformed reply never replaces a good feed.
36
+ const mu = totals.mu_charged_mmu;
37
+ if (typeof totals.model_cost_mmu !== "string" || !MMU_RE.test(totals.model_cost_mmu)) {
38
+ return false;
39
+ }
40
+ if (mu !== null && (typeof mu !== "string" || !MMU_RE.test(mu)))
41
+ return false;
42
+ const tmp = `${o.feedFile}.tmp`;
43
+ writeFileSync(tmp, JSON.stringify({
44
+ model_cost_mmu: totals.model_cost_mmu,
45
+ mu_charged_mmu: totals.mu_charged_mmu,
46
+ }));
47
+ renameSync(tmp, o.feedFile);
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /** Poll until stopped. The timer is unref'd, so it never holds the launcher open. */
55
+ export function startFeed(o) {
56
+ let running = false;
57
+ const tick = () => {
58
+ if (running)
59
+ return;
60
+ running = true;
61
+ void feedOnce(o).finally(() => {
62
+ running = false;
63
+ });
64
+ };
65
+ const timer = setInterval(tick, o.intervalMs ?? FEED_INTERVAL_MS);
66
+ timer.unref();
67
+ return () => clearInterval(timer);
68
+ }
@@ -1,108 +1,61 @@
1
1
  /**
2
- * The `mnemom agent` status-bar savings counter: a Claude Code statusLine command that
3
- * prints "mnemom saved ~$X this session".
2
+ * The `mnemom agent` status-bar segment: a Claude Code statusLine command that prints
3
+ * "mnemom", after the user's own status line when they have one.
4
4
  *
5
- * The number is the first-run look's estimator (first-run/backtest.ts), applied live to this
6
- * session's own transcript: each request priced at API list prices, times the empirical
7
- * curve's lower-bound saving rate for its original context. So the counter and the
8
- * first-run screen always agree, and it rounds DOWN to the cent.
5
+ * The segment never says "saved" and never shows an estimate (decision 14). The old
6
+ * "mnemom saved ~$X this session" counter priced the transcript with the first-run look's
7
+ * benchmark estimate; it is gone on every channel, stable included.
9
8
  *
10
- * Claude Code runs the command on every status update with its status JSON on stdin
11
- * (session_id, transcript_path, …). It must be fast, so the transcript is read
12
- * incrementally: per session, a state file (~/.mnemom/statusline/<session>.json, 0600)
13
- * keeps each file's byte offset and each message id's contribution, and a tick reads at
14
- * most MAX_BYTES_PER_TICK of new bytes (a big resumed session catches up over ticks).
15
- *
16
- * Measured mode (the `measured-savings` experimental feature; the launch sets
17
- * MNEMOM_STATUSLINE_MEASURED=1): the gateway measures each request's saving — the stock
18
- * request's input cost minus what it billed — and writes it into the response usage as
19
- * `mnemom_savings` inside a `usage.iterations` entry, the one place Claude Code keeps an
20
- * unknown usage key in its transcript. The counter sums those per message id
21
- * ("mnemom saved $X this session", no "~": it is measured) and makes no claim before the
22
- * first one arrives. Its state lives in its own file (<session>.measured.json), so the two
23
- * modes never share a count.
9
+ * On a prerelease (`next`) launch the segment is fed: the launcher writes this session's
10
+ * measured model cost and μ from Record's accumulator to the file named in
11
+ * MNEMOM_STATUSLINE_FEED (lib/statusline-feed.ts), and the segment reads
12
+ * "mnemom · $0.42 · 1.70 μ". Until that file holds valid totals it is only "mnemom".
13
+ * Stable launches set no feed, so stable is only "mnemom".
24
14
  *
25
15
  * A user's own statusLine is never replaced: the launch passes it in
26
16
  * MNEMOM_STATUSLINE_INNER, it runs with the same stdin, and our segment is appended.
27
- * Read-only on every transcript; no network. It lives outside lib/first-run because it runs
28
- * the user's own statusLine command, and the first-run code is held to spawning nothing.
17
+ * No transcript reads and no network: the only file read is the feed.
29
18
  */
30
- export declare const STATE_DIR = "statusline";
31
- export declare const MAX_BYTES_PER_TICK: number;
32
19
  /** The launcher passes the user's own statusLine command here. */
33
20
  export declare const INNER_ENV = "MNEMOM_STATUSLINE_INNER";
34
- /** Set to 1 to launch without the counter. */
21
+ /** Set to 1 to launch without the segment. */
35
22
  export declare const NO_STATUSLINE_ENV = "MNEMOM_AGENT_NO_STATUSLINE";
36
- /** The launcher sets this to 1 when the gateway measures the session's saving. */
37
- export declare const MEASURED_ENV = "MNEMOM_STATUSLINE_MEASURED";
38
23
  export declare const INNER_TIMEOUT_MS = 2000;
39
- export interface SessionState {
40
- v: 1;
41
- /** Byte offset read so far, per transcript file. */
42
- files: Record<string, number>;
43
- /** Per message id: [list-price cost, saving]. A re-written id replaces its old share. */
44
- ids: Record<string, [number, number]>;
45
- actual: number;
46
- saved: number;
47
- }
48
- export declare function freshState(): SessionState;
49
- /** Measured mode's state (<session>.measured.json). */
50
- export interface MeasuredState {
51
- v: 1;
52
- files: Record<string, number>;
53
- /** Per message id: the gateway's measured saving (USD). A re-written id replaces its old share. */
54
- ids: Record<string, number>;
55
- saved: number;
56
- /** Has any record carried a measurement yet? Until then the segment makes no claim. */
57
- seen: boolean;
58
- }
59
- export declare function freshMeasuredState(): MeasuredState;
60
- /** The main transcript, then its subagent transcripts (sorted). */
61
- export declare function sessionFiles(transcriptPath: string): string[];
62
- /**
63
- * The complete lines of `path` from `offset`, at most `maxBytes` of them, and the offset
64
- * after the last complete line. null: the file is unreadable or shorter than `offset`.
65
- */
66
- export declare function readNewLines(path: string, offset: number, maxBytes: number): {
67
- lines: Buffer[];
68
- next: number;
69
- } | null;
70
- /** Read what is new in the session's files into `st`. A file that shrank resets the count. */
71
- export declare function update(st: SessionState, files: string[], maxBytes?: number): SessionState;
72
- /** A `usage.iterations` entry's measured saving (`mnemom_savings` v1), or null. */
73
- export declare function measuredSaving(entry: unknown): number | null;
24
+ /** What the segment says until it is fed. */
25
+ export declare const SEGMENT = "mnemom";
26
+ /** The launcher names the feed file here (prerelease launches only). */
27
+ export declare const FEED_ENV = "MNEMOM_STATUSLINE_FEED";
74
28
  /**
75
- * One transcript line → the gateway's measured saving for its request, or null (not an
76
- * assistant record, or no measurement in its `message.usage.iterations`). `id` is the message
77
- * id (or record uuid); null when the record carries neither. Claude Code writes one record per
78
- * content block, all with the same id and usage, so the id is what dedupes them.
29
+ * The gateway's per-conversation lineage key (deriveLineageKey): `cid:` or `cr:` + 16 hex.
30
+ * base-url-proxy.mjs (a standalone script) keeps its own copy; a test pins the two equal.
79
31
  */
80
- export declare function parseMeasuredLine(line: Buffer): {
81
- id: string | null;
82
- saved: number;
83
- } | null;
84
- /** Measured mode's `update`: sums each request's measured saving, replace-by-id. */
85
- export declare function updateMeasured(st: MeasuredState, files: string[], maxBytes?: number): MeasuredState;
86
- /** The segment, rounded DOWN to the cent and never below $0.00. */
87
- export declare function segment(saved: number): string;
88
- /** Measured mode's segment: no claim until the gateway has measured something; no "~". */
89
- export declare function measuredSegment(st: MeasuredState): string;
90
- export declare function loadState(path: string): SessionState;
91
- export declare function loadMeasuredState(path: string): MeasuredState;
92
- export declare function saveState(path: string, st: SessionState | MeasuredState): void;
32
+ export declare const LINEAGE_KEY_RE: RegExp;
33
+ /** A millimu amount as the record route returns it: a decimal string, no sign, no leading 0. */
34
+ export declare const MMU_RE: RegExp;
35
+ export interface FedTotals {
36
+ /** Integer millimu (1 μ = 1¢ = 1000 mmu), decimal string. */
37
+ model_cost_mmu: string;
38
+ /** null: no billing row yet, shown "μ not charged" (OD-9). */
39
+ mu_charged_mmu: string | null;
40
+ }
41
+ /** The feed file's totals, or null when it is absent or anything in it is off. */
42
+ export declare function readFeed(path: string): FedTotals | null;
43
+ /** Model cost in dollars: 1000 mmu = 1 μ = 1¢. Under half a cent reads "<$0.01", never "$0.00". */
44
+ export declare function formatCost(mmu: string): string;
45
+ /** μ charged, two places; null (no billing row) reads "μ not charged" (OD-9). */
46
+ export declare function formatMu(mmu: string | null): string;
47
+ /** The segment for these totals: "mnemom · $0.42 · 1.70 μ". */
48
+ export declare function fedSegment(t: FedTotals): string;
93
49
  /** Run the user's own statusLine command with the same stdin; its first line, or "". */
94
50
  export declare function runInner(command: string, input: string): string;
95
51
  export interface RenderDeps {
96
- stateDir: string;
97
52
  inner?: string;
98
- /** Measured mode (`measuredMode`): the gateway's measured saving, not the estimate. */
99
- measured?: boolean;
100
53
  runInner?: (command: string, input: string) => string;
54
+ /** The feed file (FEED_ENV); absent on stable, so the segment is only "mnemom". */
55
+ feed?: string;
101
56
  }
102
- /** Is the counter in measured mode? The launch's MEASURED_ENV, exactly "1". */
103
- export declare function measuredMode(env: NodeJS.ProcessEnv): boolean;
104
57
  /** Claude Code's status JSON → the one status line. Never throws. */
105
- export declare function render(stdin: string, deps: RenderDeps): string;
58
+ export declare function render(stdin: string, deps?: RenderDeps): string;
106
59
  export interface UserStatusLine {
107
60
  command: string;
108
61
  padding?: number;
@@ -113,9 +66,8 @@ export interface UserStatusLine {
113
66
  */
114
67
  export declare function userStatusLine(cwd: string, configDir: string): UserStatusLine | null;
115
68
  /**
116
- * Launch args + env that add the counter, or null when it should stay off: the opt-out
69
+ * Launch args + env that add the segment, or null when it should stay off: the opt-out
117
70
  * env, or the user passing their own --settings (two would fight over precedence).
118
- * `measured` puts the counter in measured mode (MEASURED_ENV).
119
71
  */
120
72
  export declare function statuslineLaunch(o: {
121
73
  passthrough: readonly string[];
@@ -124,7 +76,6 @@ export declare function statuslineLaunch(o: {
124
76
  home?: string;
125
77
  node: string;
126
78
  bin: string;
127
- measured?: boolean;
128
79
  }): {
129
80
  args: string[];
130
81
  env: Record<string, string>;