@mnemom/mnemom 0.17.3-next.5 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Experimental features — the single registry and the pure logic around it.
3
+ *
4
+ * An experimental feature is OFF unless the user turns it on (`mnemom experimental
5
+ * enable <id>`, stored in ~/.mnemom/config.json `experimental.enabled`, or the
6
+ * MNEMOM_EXPERIMENTAL env var). While it is off its flags and commands are hidden
7
+ * from --help, using one fails with a pointer to `mnemom experimental enable`, and
8
+ * its launcher.toml keys are ignored with the same pointer. The contract: an
9
+ * experimental feature may change or be removed in any release, with no
10
+ * deprecation period.
11
+ *
12
+ * Everything here is pure or dependency-injected; index.ts and the command
13
+ * modules are the thin drivers.
14
+ */
15
+ export const EXPERIMENTAL_FEATURES = [
16
+ {
17
+ id: "context-handoff",
18
+ summary: "Register the `handoff(note)` MCP tool and turn on the gateway's context-handoff capability for the session",
19
+ since: "0.17.3",
20
+ gates: {
21
+ flags: ["--context-handoff"],
22
+ configKeys: ["context_handoff"],
23
+ commands: [],
24
+ env: ["MNEMOM_AGENT_CONTEXT_HANDOFF"],
25
+ },
26
+ },
27
+ {
28
+ id: "slack",
29
+ summary: "Attach the Slack channel to a session: messages addressed to it arrive in it, and it can reply",
30
+ since: "0.17.3",
31
+ gates: {
32
+ flags: ["--slack"],
33
+ configKeys: ["slack_channel", "slack_listen", "slack_allow"],
34
+ commands: ["mnemom agent slack"],
35
+ env: [],
36
+ },
37
+ },
38
+ {
39
+ id: "implicit",
40
+ summary: "Implicit goal alignment: the gateway infers the goal from your messages and revises it as you steer",
41
+ since: "0.17.3",
42
+ gates: {
43
+ flags: ["--implicit"],
44
+ configKeys: ["goal_mode = implicit"],
45
+ commands: ["mnemom agent goal show|pin|reset"],
46
+ env: [],
47
+ },
48
+ },
49
+ ];
50
+ export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
51
+ export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
52
+ export const EXPERIMENTAL_CONTRACT = "Experimental features may change or be removed in any release, with no deprecation period.";
53
+ export function isExperimentalId(value) {
54
+ return EXPERIMENTAL_IDS.includes(value);
55
+ }
56
+ /** Registry order, deduplicated, unknown ids dropped. */
57
+ function normalize(ids) {
58
+ const set = new Set(ids);
59
+ return EXPERIMENTAL_IDS.filter((id) => set.has(id));
60
+ }
61
+ /**
62
+ * Which experimental features are on. Precedence: MNEMOM_EXPERIMENTAL (a comma
63
+ * list of ids, `all`, or `none`) replaces the stored set; else config.json
64
+ * `experimental.enabled`; else everything off.
65
+ */
66
+ export function resolveExperimental(opts) {
67
+ const raw = (opts.env[EXPERIMENTAL_ENV] ?? "").trim();
68
+ if (raw !== "") {
69
+ const tokens = raw
70
+ .split(",")
71
+ .map((t) => t.trim().toLowerCase())
72
+ .filter(Boolean);
73
+ if (tokens.includes("all"))
74
+ return { enabled: [...EXPERIMENTAL_IDS], source: "env", unknown: [] };
75
+ const listed = tokens.filter((t) => t !== "none");
76
+ return {
77
+ enabled: normalize(listed),
78
+ source: "env",
79
+ unknown: listed.filter((t) => !isExperimentalId(t)),
80
+ };
81
+ }
82
+ const stored = Array.isArray(opts.stored?.enabled) ? opts.stored.enabled.map(String) : undefined;
83
+ if (stored === undefined)
84
+ return { enabled: [], source: "default", unknown: [] };
85
+ return {
86
+ enabled: normalize(stored),
87
+ source: "config",
88
+ unknown: stored.filter((t) => !isExperimentalId(t)),
89
+ };
90
+ }
91
+ /** The one-line stderr notice for a launch with experimental features on; null when none are. */
92
+ export function experimentalNotice(enabled) {
93
+ if (enabled.length === 0)
94
+ return null;
95
+ return `mnemom agent: experimental: ${enabled.join(", ")} — may change or break without notice`;
96
+ }
97
+ /** The error for using an experimental flag or command while its feature is off. */
98
+ export function gatedMessage(what, id) {
99
+ return `${what} is experimental. Turn it on with \`mnemom experimental enable ${id}\`.`;
100
+ }
101
+ /** Thrown when a gated flag or command is used while its feature is off; the CLI exits 2. */
102
+ export class ExperimentalGateError extends Error {
103
+ exitCode = 2;
104
+ constructor(what, id) {
105
+ super(gatedMessage(what, id));
106
+ this.name = "ExperimentalGateError";
107
+ }
108
+ }
109
+ /** Throw for the first use whose feature is off. */
110
+ export function assertEnabled(uses, enabled) {
111
+ for (const u of uses) {
112
+ if (u.used && !enabled.includes(u.id))
113
+ throw new ExperimentalGateError(u.what, u.id);
114
+ }
115
+ }
116
+ /**
117
+ * The feature a launcher.toml key/value belongs to, when setting it only has an
118
+ * effect with that feature on. `goal_mode = off`, `context_handoff = false` and
119
+ * `slack_listen = false` are harmless either way.
120
+ */
121
+ export function experimentalIdForConfigKey(key, value) {
122
+ switch (key) {
123
+ case "slack_channel":
124
+ case "slack_allow":
125
+ return "slack";
126
+ case "slack_listen":
127
+ return value === "true" ? "slack" : undefined;
128
+ case "context_handoff":
129
+ return value === "true" ? "context-handoff" : undefined;
130
+ case "goal_mode":
131
+ return value === "implicit" ? "implicit" : undefined;
132
+ default:
133
+ return undefined;
134
+ }
135
+ }
136
+ /** Is an env value one of the truthy spellings resolveContextHandoff accepts? */
137
+ function truthy(raw) {
138
+ return ["1", "true", "on", "yes"].includes((raw ?? "").trim().toLowerCase());
139
+ }
140
+ /**
141
+ * The stderr lines for experimental settings a launch ignores because their feature
142
+ * is off: launcher.toml keys (`entries` as `[key, value]`) and MNEMOM_AGENT_CONTEXT_HANDOFF.
143
+ */
144
+ export function ignoredSettingLines(opts) {
145
+ const lines = [];
146
+ for (const [key, value] of opts.entries) {
147
+ const id = experimentalIdForConfigKey(key, value);
148
+ if (id && !opts.enabled.includes(id)) {
149
+ lines.push(`mnemom agent: ignoring launcher.toml ${gatedMessage(key, id)}`);
150
+ }
151
+ }
152
+ if (truthy(opts.env.MNEMOM_AGENT_CONTEXT_HANDOFF) && !opts.enabled.includes("context-handoff")) {
153
+ lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_CONTEXT_HANDOFF", "context-handoff")}`);
154
+ }
155
+ return lines;
156
+ }
157
+ /** The features an existing launcher.toml already uses (the one-time migration). */
158
+ export function featuresInUse(entries) {
159
+ return normalize(entries.flatMap(([k, v]) => {
160
+ const id = experimentalIdForConfigKey(k, v);
161
+ return id ? [id] : [];
162
+ }));
163
+ }
164
+ /** One-time notice for users who had implicit goal alignment by default (no goal_mode set). */
165
+ export const IMPLICIT_NOW_OFF_NOTICE = "mnemom: implicit goal alignment is now experimental and OFF by default (it used to be on). " +
166
+ "Turn it back on with `mnemom experimental enable implicit`, or seal a goal per launch with --goal.\n";
167
+ /**
168
+ * First run after the upgrade that introduced experimental features: config.json has
169
+ * no `experimental` section yet, so turn on whatever the user's launcher.toml already
170
+ * uses, persist (an empty list too — the section's presence marks the migration
171
+ * done), and say once which were kept on. A malformed launcher.toml skips the
172
+ * migration this run (the launch reports that file itself) so it retries next time.
173
+ * Never throws.
174
+ */
175
+ export function migrateExperimental(deps) {
176
+ try {
177
+ if (deps.stored() !== undefined)
178
+ return null;
179
+ const entries = deps.launcherEntries();
180
+ const ids = featuresInUse(entries);
181
+ deps.persist(ids);
182
+ if (ids.length > 0) {
183
+ deps.writeErr(`mnemom: ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} now experimental and kept on because your launcher.toml uses ${ids.length === 1 ? "it" : "them"} — see \`mnemom experimental list\`.\n`);
184
+ }
185
+ // Implicit was the unconfigured DEFAULT, so most users relied on it without a
186
+ // goal_mode entry. Don't let it switch off silently for them: say so once.
187
+ const optedOut = entries.some(([k, v]) => k === "goal_mode" && v === "off");
188
+ if (!ids.includes("implicit") && !optedOut)
189
+ deps.writeErr(IMPLICIT_NOW_OFF_NOTICE);
190
+ return ids;
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ /** The feature a launcher.toml key belongs to at all (for marking `mnemom agent config list`). */
197
+ export function configKeyFeature(key) {
198
+ if (key === "slack_channel" || key === "slack_listen" || key === "slack_allow")
199
+ return "slack";
200
+ if (key === "context_handoff")
201
+ return "context-handoff";
202
+ if (key === "goal_mode")
203
+ return "implicit";
204
+ return undefined;
205
+ }
206
+ /**
207
+ * ` (experimental, off)` for a launcher.toml key whose feature is off, else "". With a
208
+ * `value`, only a setting that would take effect is marked (`goal_mode = off` is not).
209
+ */
210
+ export function configKeyMark(key, value, enabled) {
211
+ const id = value === undefined ? configKeyFeature(key) : experimentalIdForConfigKey(key, value);
212
+ return id && !enabled.includes(id) ? " (experimental, off)" : "";
213
+ }
214
+ /** `null` when `id` is on, else the gated-use message. */
215
+ export function gateCheck(what, id, enabled) {
216
+ return enabled.includes(id) ? null : gatedMessage(what, id);
217
+ }
218
+ /**
219
+ * How a command's catch reports an error: a gated use prints its guidance bare and
220
+ * exits 2; anything else keeps the CLI's `Error: <message>` and exit 1.
221
+ */
222
+ export function reportCliError(error) {
223
+ if (error instanceof ExperimentalGateError)
224
+ return { args: [error.message], code: 2 };
225
+ return { args: ["Error:", error instanceof Error ? error.message : error], code: 1 };
226
+ }
@@ -15,6 +15,7 @@
15
15
  * resolvers in lib/code.ts keep the built-in default as the final fallback);
16
16
  * the caller folds the plain-value settings under the CLI flags.
17
17
  */
18
+ import type { ExperimentalId } from "./experimental.js";
18
19
  import { type ContextProfile, type GoalModeDefault, type LaunchShape } from "./agent.js";
19
20
  /** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
20
21
  export declare const LAUNCHER_CONFIG_PATH: string;
@@ -98,3 +99,11 @@ export declare function writeLauncherConfig(cfg: LauncherConfig, path?: string):
98
99
  * (see resolveLaunchShape): terminal→"0", remote-control→"1", server→"server".
99
100
  */
100
101
  export declare function applyConfigToEnv(cfg: LauncherConfig, env?: NodeJS.ProcessEnv): void;
102
+ /** The set keys of `cfg` as `[key, value]` pairs (CONFIG_KEYS order, values stringified). */
103
+ export declare function launcherEntries(cfg: LauncherConfig): Array<[string, string]>;
104
+ /**
105
+ * `cfg` without the settings of experimental features that are off (a copy; `cfg` is
106
+ * untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
107
+ * off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
108
+ */
109
+ export declare function withoutDisabledExperimental(cfg: LauncherConfig, enabled: readonly ExperimentalId[]): LauncherConfig;
@@ -86,7 +86,7 @@ function ensureGuardrails(cfg) {
86
86
  export const CONFIG_KEYS = [
87
87
  {
88
88
  key: "gateway",
89
- describe: "Gateway host (default https://gateway.mnemom.ai — the us-2/prod cell)",
89
+ describe: "Gateway host (default: the gateway of the cell the CLI uses — prod unless MNEMOM_ENV/MNEMOM_RING/MNEMOM_GATEWAY_URL say otherwise; a different cell is refused)",
90
90
  get: (c) => c.gateway,
91
91
  set: (c, v) => {
92
92
  c.gateway = coerceNonEmpty("gateway", v);
@@ -402,3 +402,28 @@ export function applyConfigToEnv(cfg, env = process.env) {
402
402
  env.MNEMOM_AGENT_CONTEXT_PROFILE = cfg.contextProfile;
403
403
  }
404
404
  }
405
+ /** The set keys of `cfg` as `[key, value]` pairs (CONFIG_KEYS order, values stringified). */
406
+ export function launcherEntries(cfg) {
407
+ return CONFIG_KEYS.flatMap((spec) => {
408
+ const v = spec.get(cfg);
409
+ return v === undefined ? [] : [[spec.key, String(v)]];
410
+ });
411
+ }
412
+ /**
413
+ * `cfg` without the settings of experimental features that are off (a copy; `cfg` is
414
+ * untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
415
+ * off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
416
+ */
417
+ export function withoutDisabledExperimental(cfg, enabled) {
418
+ const out = { ...cfg };
419
+ if (!enabled.includes("slack")) {
420
+ delete out.slackChannel;
421
+ delete out.slackListen;
422
+ delete out.slackAllow;
423
+ }
424
+ if (!enabled.includes("context-handoff"))
425
+ delete out.contextHandoff;
426
+ if (!enabled.includes("implicit") && out.goalMode === "implicit")
427
+ delete out.goalMode;
428
+ return out;
429
+ }
@@ -53,7 +53,9 @@ export function ensureRouter(argv = [process.execPath, process.argv[1] ?? "mnemo
53
53
  }
54
54
  /** Run the router until the process is killed. Reconnects on disconnect. */
55
55
  export async function runRouter(opts) {
56
- const log = opts.log ?? ((l) => console.error(`[slack-router] ${l}`));
56
+ // Lines carry Slack-supplied values (channel, handles, disconnect reason);
57
+ // strip CR/LF so a crafted message cannot forge extra log lines.
58
+ const log = opts.log ?? ((l) => console.error(`[slack-router] ${l.replace(/\n|\r/g, "")}`));
57
59
  const fetchImpl = opts.fetchImpl ?? fetch;
58
60
  const WS = globalThis.WebSocket;
59
61
  if (!WS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.3-next.5",
3
+ "version": "0.17.3",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {