@mnemom/mnemom 0.17.3-next.0 → 0.17.3-next.1

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.
@@ -33,7 +33,8 @@ import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { MNEMOM_DIR } from "../lib/config.js";
35
35
  import { getAgentByName } from "../lib/api.js";
36
- import { LEDGER_FILE, appendSessionRecord, toLedgerContract } from "../lib/agent-ledger.js";
36
+ import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
37
+ import { loadCliConfig } from "../lib/cli-config.js";
37
38
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
38
39
  import { fmt } from "../lib/format.js";
39
40
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
@@ -377,7 +378,12 @@ export async function recordSessionLaunch(ctx) {
377
378
  contract: toLedgerContract(ctx.envelope?.contract ?? null),
378
379
  goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
379
380
  cc_session_id: ctx.ccSessionId ?? null,
380
- }, { dir: MNEMOM_DIR, now: () => new Date(), pid: process.pid });
381
+ }, {
382
+ dir: MNEMOM_DIR,
383
+ now: () => new Date(),
384
+ pid: process.pid,
385
+ retentionDays: resolveRetentionDays(loadCliConfig().sessions?.retentionDays, process.env),
386
+ });
381
387
  if (!result.ok) {
382
388
  say(fmt.warn(`mnemom agent: could not record this session in ~/.mnemom/${LEDGER_FILE} (${result.error}) — launching anyway.`));
383
389
  }
@@ -1,30 +1,30 @@
1
1
  /**
2
2
  * `mnemom config list|get|set|unset` — CLI-wide preferences in
3
- * ~/.mnemom/config.json. Today: the auto-update keys. (Launcher settings for
3
+ * ~/.mnemom/config.json: auto-update and local session retention. (Launcher settings for
4
4
  * `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
5
5
  *
6
6
  * Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
7
7
  */
8
- import { type InstallHint, type UpdatePrefs } from "../lib/update/decide.js";
9
- import { setUpdatePref } from "../lib/cli-config.js";
8
+ import { type InstallHint } from "../lib/update/decide.js";
9
+ import { setPref, type CliConfig } from "../lib/cli-config.js";
10
10
  export interface CliConfigKey {
11
11
  key: string;
12
12
  describe: string;
13
13
  env: string;
14
14
  /** Stored value (undefined when unset). */
15
- stored(prefs: UpdatePrefs): string | undefined;
15
+ stored(config: CliConfig): string | undefined;
16
16
  /** Value in effect after env + default (`hint` picks the channel default). */
17
- effective(prefs: UpdatePrefs, env: Record<string, string | undefined>, hint: InstallHint): string;
17
+ effective(config: CliConfig, env: Record<string, string | undefined>, hint: InstallHint): string;
18
18
  /** Validate + persist; throws with a user-facing message on a bad value. */
19
- set(raw: string, persist: typeof setUpdatePref): void;
20
- unset(persist: typeof setUpdatePref): void;
19
+ set(raw: string, persist: typeof setPref): void;
20
+ unset(persist: typeof setPref): void;
21
21
  }
22
22
  export declare const CONFIG_KEYS: readonly CliConfigKey[];
23
23
  export declare function findKey(key: string): CliConfigKey;
24
24
  export interface ConfigDeps {
25
- prefs(): UpdatePrefs;
25
+ config(): CliConfig;
26
26
  env: Record<string, string | undefined>;
27
- persist: typeof setUpdatePref;
27
+ persist: typeof setPref;
28
28
  log(line: string): void;
29
29
  /** Running version + last-followed channel (picks the default channel). */
30
30
  hint: InstallHint;
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * `mnemom config list|get|set|unset` — CLI-wide preferences in
3
- * ~/.mnemom/config.json. Today: the auto-update keys. (Launcher settings for
3
+ * ~/.mnemom/config.json: auto-update and local session retention. (Launcher settings for
4
4
  * `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
5
5
  *
6
6
  * Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
7
7
  */
8
8
  import { isUpdateChannel, isUpdateMode, resolveChannel, resolveMode, UPDATE_CHANNELS, UPDATE_MODES, } from "../lib/update/decide.js";
9
- import { loadCliConfig, setUpdatePref } from "../lib/cli-config.js";
9
+ import { DEFAULT_RETENTION_DAYS, parseRetentionDays, resolveRetentionDays, RETENTION_ENV, } from "../lib/agent-ledger.js";
10
+ import { loadCliConfig, setPref } from "../lib/cli-config.js";
10
11
  import { loadUpdateState } from "../lib/update/state.js";
11
12
  import { CLI_VERSION } from "../version.js";
12
13
  export const CONFIG_KEYS = [
@@ -14,29 +15,43 @@ export const CONFIG_KEYS = [
14
15
  key: "update.channel",
15
16
  describe: `Release channel for auto-update: ${UPDATE_CHANNELS.join(" | ")} (default: next on a next build, else latest; next requires an invite)`,
16
17
  env: "MNEMOM_UPDATE_CHANNEL",
17
- stored: (p) => p.channel,
18
- effective: (p, env, hint) => resolveChannel(p, env, undefined, hint),
18
+ stored: (c) => c.update?.channel,
19
+ effective: (c, env, hint) => resolveChannel(c.update ?? {}, env, undefined, hint),
19
20
  set: (raw, persist) => {
20
21
  const v = raw.trim();
21
22
  if (!isUpdateChannel(v))
22
23
  throw new Error(`update.channel must be one of ${UPDATE_CHANNELS.join(", ")} (got '${raw}').`);
23
- persist("channel", v);
24
+ persist("update", "channel", v);
24
25
  },
25
- unset: (persist) => persist("channel", undefined),
26
+ unset: (persist) => persist("update", "channel", undefined),
26
27
  },
27
28
  {
28
29
  key: "update.mode",
29
30
  describe: `Auto-update behaviour: ${UPDATE_MODES.join(" | ")}`,
30
31
  env: "MNEMOM_UPDATE_MODE",
31
- stored: (p) => p.mode,
32
- effective: (p, env) => resolveMode(p, env),
32
+ stored: (c) => c.update?.mode,
33
+ effective: (c, env) => resolveMode(c.update ?? {}, env),
33
34
  set: (raw, persist) => {
34
35
  const v = raw.trim();
35
36
  if (!isUpdateMode(v))
36
37
  throw new Error(`update.mode must be one of ${UPDATE_MODES.join(", ")} (got '${raw}').`);
37
- persist("mode", v);
38
+ persist("update", "mode", v);
38
39
  },
39
- unset: (persist) => persist("mode", undefined),
40
+ unset: (persist) => persist("update", "mode", undefined),
41
+ },
42
+ {
43
+ key: "sessions.retention_days",
44
+ describe: `Days \`mnemom agent sessions\` keeps a launch in ~/.mnemom/sessions.jsonl (default ${DEFAULT_RETENTION_DAYS}; older ones are removed at the next launch)`,
45
+ env: RETENTION_ENV,
46
+ stored: (c) => c.sessions?.retentionDays?.toString(),
47
+ effective: (c, env) => String(resolveRetentionDays(c.sessions?.retentionDays, env)),
48
+ set: (raw, persist) => {
49
+ const days = parseRetentionDays(raw);
50
+ if (days === null)
51
+ throw new Error(`sessions.retention_days must be a whole number of days, at least 1 (got '${raw}').`);
52
+ persist("sessions", "retentionDays", days);
53
+ },
54
+ unset: (persist) => persist("sessions", "retentionDays", undefined),
40
55
  },
41
56
  ];
42
57
  export function findKey(key) {
@@ -47,22 +62,22 @@ export function findKey(key) {
47
62
  }
48
63
  function defaultDeps() {
49
64
  return {
50
- prefs: () => loadCliConfig().update ?? {},
65
+ config: loadCliConfig,
51
66
  env: process.env,
52
- persist: setUpdatePref,
67
+ persist: setPref,
53
68
  log: (line) => console.log(line),
54
69
  hint: { current: CLI_VERSION, followed: loadUpdateState().followChannel },
55
70
  };
56
71
  }
57
72
  /** One line per key: `key = effective (source) — description`. */
58
73
  export function configListCommand(opts = {}, deps = defaultDeps()) {
59
- const prefs = deps.prefs();
74
+ const config = deps.config();
60
75
  const rows = CONFIG_KEYS.map((k) => {
61
- const stored = k.stored(prefs);
76
+ const stored = k.stored(config);
62
77
  const source = deps.env[k.env] ? `env ${k.env}` : stored !== undefined ? "config" : "default";
63
78
  return {
64
79
  key: k.key,
65
- value: k.effective(prefs, deps.env, deps.hint),
80
+ value: k.effective(config, deps.env, deps.hint),
66
81
  source,
67
82
  describe: k.describe,
68
83
  };
@@ -75,7 +90,7 @@ export function configListCommand(opts = {}, deps = defaultDeps()) {
75
90
  deps.log(`${r.key} = ${r.value} (${r.source}) — ${r.describe}`);
76
91
  }
77
92
  export function configGetCommand(key, deps = defaultDeps()) {
78
- deps.log(findKey(key).effective(deps.prefs(), deps.env, deps.hint));
93
+ deps.log(findKey(key).effective(deps.config(), deps.env, deps.hint));
79
94
  }
80
95
  export function configSetCommand(key, value, deps = defaultDeps()) {
81
96
  const spec = findKey(key);
package/dist/index.js CHANGED
@@ -1898,7 +1898,7 @@ program
1898
1898
  .action((options) => runAction(() => updateCommand(options)));
1899
1899
  const configCmd = program
1900
1900
  .command("config")
1901
- .description("CLI-wide preferences in ~/.mnemom/config.json (auto-update channel and mode)");
1901
+ .description("CLI-wide preferences in ~/.mnemom/config.json (auto-update, session retention)");
1902
1902
  configCmd
1903
1903
  .command("list")
1904
1904
  .description("Show every setting, its effective value, and where it came from")
@@ -9,12 +9,24 @@
9
9
  *
10
10
  * Writes are best-effort: `appendSessionRecord` never throws — a failure comes
11
11
  * back as `{ ok: false, error }` and the caller decides how loudly to warn.
12
+ *
13
+ * Retention mirrors Claude Code's `cleanupPeriodDays`: launches older than
14
+ * `sessions.retention_days` (default 30) are dropped when the next one is
15
+ * recorded. No count cap — a long retention keeps everything in the window.
12
16
  */
13
17
  import type { LaunchShape, SealedContract } from "./agent.js";
14
18
  /** The ledger's file name under MNEMOM_DIR. */
15
19
  export declare const LEDGER_FILE = "sessions.jsonl";
16
- /** The ledger keeps only the newest this-many launches. */
17
- export declare const LEDGER_CAP = 200;
20
+ /** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
21
+ export declare const DEFAULT_RETENTION_DAYS = 30;
22
+ export declare const RETENTION_ENV = "MNEMOM_SESSIONS_RETENTION_DAYS";
23
+ /** A retention setting must be a whole number of days, at least 1. */
24
+ export declare function parseRetentionDays(raw: string): number | null;
25
+ /**
26
+ * Precedence: MNEMOM_SESSIONS_RETENTION_DAYS > config.json > 30. An invalid value from either
27
+ * source is ignored (config.json can be hand-edited past `mnemom config set`'s validation).
28
+ */
29
+ export declare function resolveRetentionDays(stored: unknown, env: Record<string, string | undefined>): number;
18
30
  /** The sealed-contract fields the ledger keeps (no goal_id — the statement is the identity). */
19
31
  export interface SessionRecordContract {
20
32
  statement: string;
@@ -76,6 +88,8 @@ export interface LedgerDeps {
76
88
  now: () => Date;
77
89
  /** The writing process's pid (names the rewrite temp file). */
78
90
  pid: number;
91
+ /** Drop launches older than this many days (default DEFAULT_RETENTION_DAYS). */
92
+ retentionDays?: number;
79
93
  }
80
94
  export type AppendResult = {
81
95
  ok: true;
@@ -93,11 +107,11 @@ export declare function toLedgerContract(sealed: SealedContract | null): Session
93
107
  /** The ledger's path under `dir`. */
94
108
  export declare function ledgerPath(dir: string): string;
95
109
  /**
96
- * Append one launch to the ledger, atomically and capped:
110
+ * Append one launch to the ledger, dropping expired ones:
97
111
  * - `dir` is created 0o700 when missing; the file is created 0o600;
98
- * - the new line is appended when the ledger stays within LEDGER_CAP lines,
99
- * otherwise the newest LEDGER_CAP lines (this one included) are rewritten
100
- * via a temp file + rename, so a reader never sees a torn file.
112
+ * - when no launch is older than the retention window the new line is
113
+ * appended; otherwise the surviving lines plus this one are rewritten via a
114
+ * temp file + rename, so a reader never sees a torn file.
101
115
  * Never throws — a failure is returned as `{ ok: false, error }`.
102
116
  */
103
117
  export declare function appendSessionRecord(input: SessionRecordInput, deps: LedgerDeps): AppendResult;
@@ -9,13 +9,36 @@
9
9
  *
10
10
  * Writes are best-effort: `appendSessionRecord` never throws — a failure comes
11
11
  * back as `{ ok: false, error }` and the caller decides how loudly to warn.
12
+ *
13
+ * Retention mirrors Claude Code's `cleanupPeriodDays`: launches older than
14
+ * `sessions.retention_days` (default 30) are dropped when the next one is
15
+ * recorded. No count cap — a long retention keeps everything in the window.
12
16
  */
13
17
  import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
18
  import { join } from "node:path";
15
19
  /** The ledger's file name under MNEMOM_DIR. */
16
20
  export const LEDGER_FILE = "sessions.jsonl";
17
- /** The ledger keeps only the newest this-many launches. */
18
- export const LEDGER_CAP = 200;
21
+ /** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
22
+ export const DEFAULT_RETENTION_DAYS = 30;
23
+ export const RETENTION_ENV = "MNEMOM_SESSIONS_RETENTION_DAYS";
24
+ const DAY_MS = 24 * 60 * 60 * 1000;
25
+ /** A retention setting must be a whole number of days, at least 1. */
26
+ export function parseRetentionDays(raw) {
27
+ const v = raw.trim();
28
+ if (!/^\d+$/.test(v))
29
+ return null;
30
+ const n = Number(v);
31
+ return Number.isSafeInteger(n) && n >= 1 ? n : null;
32
+ }
33
+ /**
34
+ * Precedence: MNEMOM_SESSIONS_RETENTION_DAYS > config.json > 30. An invalid value from either
35
+ * source is ignored (config.json can be hand-edited past `mnemom config set`'s validation).
36
+ */
37
+ export function resolveRetentionDays(stored, env) {
38
+ const fromEnv = env[RETENTION_ENV] === undefined ? null : parseRetentionDays(env[RETENTION_ENV]);
39
+ const fromConfig = typeof stored === "number" && Number.isSafeInteger(stored) && stored >= 1 ? stored : null;
40
+ return fromEnv ?? fromConfig ?? DEFAULT_RETENTION_DAYS;
41
+ }
19
42
  /**
20
43
  * Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
21
44
  * keeps only defined fields). `null` in, `null` out.
@@ -58,25 +81,43 @@ function readLedgerText(path) {
58
81
  }
59
82
  }
60
83
  /**
61
- * Append one launch to the ledger, atomically and capped:
84
+ * True when `line` is a launch that started before `cutoffMs`. A line whose
85
+ * age can't be read is kept — never delete what we can't date.
86
+ */
87
+ function isExpired(line, cutoffMs) {
88
+ try {
89
+ const startedAt = JSON.parse(line).started_at;
90
+ if (typeof startedAt !== "string")
91
+ return false;
92
+ const t = Date.parse(startedAt);
93
+ return !Number.isNaN(t) && t < cutoffMs;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Append one launch to the ledger, dropping expired ones:
62
101
  * - `dir` is created 0o700 when missing; the file is created 0o600;
63
- * - the new line is appended when the ledger stays within LEDGER_CAP lines,
64
- * otherwise the newest LEDGER_CAP lines (this one included) are rewritten
65
- * via a temp file + rename, so a reader never sees a torn file.
102
+ * - when no launch is older than the retention window the new line is
103
+ * appended; otherwise the surviving lines plus this one are rewritten via a
104
+ * temp file + rename, so a reader never sees a torn file.
66
105
  * Never throws — a failure is returned as `{ ok: false, error }`.
67
106
  */
68
107
  export function appendSessionRecord(input, deps) {
69
108
  const path = ledgerPath(deps.dir);
70
109
  try {
71
- const record = { v: 1, started_at: deps.now().toISOString(), ...input };
110
+ const now = deps.now();
111
+ const record = { v: 1, started_at: now.toISOString(), ...input };
72
112
  mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
73
113
  const line = `${JSON.stringify(record)}\n`;
74
114
  const text = readLedgerText(path);
75
115
  const existing = text === null ? [] : nonEmptyLines(text);
76
- if (existing.length + 1 > LEDGER_CAP) {
77
- const kept = [...existing, JSON.stringify(record)].slice(-LEDGER_CAP);
78
- const tmp = join(deps.dir, `.${LEDGER_FILE}.tmp-${deps.pid}-${deps.now().getTime()}`);
79
- writeFileSync(tmp, `${kept.join("\n")}\n`, { mode: 0o600 });
116
+ const cutoff = now.getTime() - (deps.retentionDays ?? DEFAULT_RETENTION_DAYS) * DAY_MS;
117
+ const kept = existing.filter((l) => !isExpired(l, cutoff));
118
+ if (kept.length < existing.length) {
119
+ const tmp = join(deps.dir, `.${LEDGER_FILE}.tmp-${deps.pid}-${now.getTime()}`);
120
+ writeFileSync(tmp, `${[...kept, JSON.stringify(record)].join("\n")}\n`, { mode: 0o600 });
80
121
  renameSync(tmp, path);
81
122
  }
82
123
  else {
@@ -23,10 +23,20 @@ export interface ActiveOrg {
23
23
  slug: string;
24
24
  name: string;
25
25
  }
26
+ /** Local session-ledger preferences (`mnemom config set sessions.retention_days`). */
27
+ export interface SessionPrefs {
28
+ retentionDays?: number;
29
+ }
26
30
  export interface CliConfig {
27
31
  activeOrg?: ActiveOrg;
28
32
  /** Auto-update preferences (`mnemom config set update.channel|update.mode`). */
29
33
  update?: UpdatePrefs;
34
+ sessions?: SessionPrefs;
35
+ }
36
+ /** The config.json sections `mnemom config` edits. */
37
+ export interface PrefSections {
38
+ update: UpdatePrefs;
39
+ sessions: SessionPrefs;
30
40
  }
31
41
  /** Load the config; a missing or corrupt file degrades to `{}`. */
32
42
  export declare function loadCliConfig(): CliConfig;
@@ -35,7 +45,10 @@ export declare function setActiveOrg(org: ActiveOrg | null): void;
35
45
  /** The active org, or undefined when none is set. */
36
46
  export declare function getActiveOrg(): ActiveOrg | undefined;
37
47
  /**
38
- * Set (or with `undefined`, clear) one auto-update preference. Validation is
39
- * the caller's job (commands/config.ts) — this only persists.
48
+ * Set (or with `undefined`, clear) one preference in a config.json section;
49
+ * an emptied section is removed. Validation is the caller's job
50
+ * (commands/config.ts) — this only persists.
40
51
  */
52
+ export declare function setPref<S extends keyof PrefSections, K extends keyof PrefSections[S] & string>(section: S, key: K, value: PrefSections[S][K] | undefined): void;
53
+ /** One auto-update preference — `setPref("update", …)`. */
41
54
  export declare function setUpdatePref<K extends keyof UpdatePrefs>(key: K, value: UpdatePrefs[K] | undefined): void;
@@ -69,15 +69,18 @@ export function getActiveOrg() {
69
69
  return loadCliConfig().activeOrg;
70
70
  }
71
71
  /**
72
- * Set (or with `undefined`, clear) one auto-update preference. Validation is
73
- * the caller's job (commands/config.ts) — this only persists.
72
+ * Set (or with `undefined`, clear) one preference in a config.json section;
73
+ * an emptied section is removed. Validation is the caller's job
74
+ * (commands/config.ts) — this only persists.
74
75
  */
75
- export function setUpdatePref(key, value) {
76
+ export function setPref(section, key, value) {
76
77
  const config = loadCliConfig();
77
- const update = Object.fromEntries(Object.entries({ ...config.update, [key]: value }).filter(([, v]) => v !== undefined));
78
- if (Object.keys(update).length === 0)
79
- delete config.update;
80
- else
81
- config.update = update;
78
+ const next = Object.fromEntries(Object.entries({ ...config[section], [key]: value }).filter(([, v]) => v !== undefined));
79
+ // An emptied section becomes undefined, which JSON.stringify omits.
80
+ config[section] = Object.keys(next).length > 0 ? next : undefined;
82
81
  saveCliConfig(config);
83
82
  }
83
+ /** One auto-update preference — `setPref("update", …)`. */
84
+ export function setUpdatePref(key, value) {
85
+ setPref("update", key, value);
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.3-next.0",
3
+ "version": "0.17.3-next.1",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {