@mnemom/mnemom 0.17.28 → 0.17.29

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.
@@ -22,7 +22,8 @@
22
22
  */
23
23
  import { basename } from "node:path";
24
24
  import { sanitizeAgentName } from "../lib/agent.js";
25
- export const NO_SESSION_NAME_MESSAGE = "mnemom agent: a session name is required here (e.g. `mnemom agent onboarding`). " +
25
+ export const NO_SESSION_NAME_MESSAGE = "mnemom agent: a session name is required here: pass it before `--` (e.g. `mnemom agent onboarding`) " +
26
+ "or as `--name <x>` (also `-n <x>` / `--name=<x>`) after `--` (e.g. `mnemom agent -- --name onboarding -p …`). " +
26
27
  "In a terminal, plain `mnemom agent` asks for one.";
27
28
  /** Current folder name, sanitised like an agent slug; "session" when that is empty. */
28
29
  export function defaultSessionName(cwd = process.cwd()) {
@@ -391,7 +391,7 @@ export declare function resolvePreflightedAccount(): Promise<{
391
391
  * absent or older, and leaves one at this version or newer as it is. Bump it whenever
392
392
  * the content below changes. (`card_version` is the schema date, not this.)
393
393
  */
394
- export declare const CODE_CARD_CONTENT_VERSION = "mnemom-agent-default/2.1.0";
394
+ export declare const CODE_CARD_CONTENT_VERSION = "mnemom-agent-default/2.1.2";
395
395
  /**
396
396
  * Whether a refresh should replace this card's content with the current default: true when
397
397
  * it carries no content_version, or an older (or unparseable) `mnemom-agent-default/` one.
@@ -412,6 +412,7 @@ export interface CardPrincipal {
412
412
  * takes a role or org, not personal data. The agent is never its own principal.
413
413
  */
414
414
  export declare function orgPrincipal(orgId?: string | null): CardPrincipal;
415
+ export declare const IN_PLACE_FLOOR: number[];
415
416
  export declare function buildCodeAlignmentCard(agentId: string, modes?: Pick<CardModes, "autonomy" | "integrity" | "allowUnmappedTools">, principal?: CardPrincipal): Record<string, unknown>;
416
417
  export declare function buildCodeProtectionCard(agentId: string, mode?: string): Record<string, unknown>;
417
418
  export {};
@@ -903,13 +903,15 @@ export function keptPrincipal(own, opts) {
903
903
  *
904
904
  * `existing` (a refresh that read the agent's own cards) decides content per card: a card
905
905
  * that is missing, carries no content_version, or an older default gets the current default
906
- * content; a card already at this default or a newer one keeps its own content and only has
907
- * its modes set. Without `existing` (a new agent, or a refresh a --governance flag decided)
906
+ * content, except a 2.1.x default, which is brought forward in place (upgradeInPlace) so its
907
+ * edits survive; a card already at this default or a newer one keeps its own content and only
908
+ * has its modes set. Without `existing` (a new agent, or a refresh a --governance flag decided)
908
909
  * both cards get the current default content.
909
910
  */
910
911
  async function publishDefaultCards(agentId, modes, opts = {}) {
911
- const ownAlign = opts.existing?.align;
912
- const ownProt = opts.existing?.prot;
912
+ // A 2.1.x card is brought forward in place first (upgradeInPlace), so it is then kept.
913
+ const ownAlign = carryForward(opts.existing?.align, "alignment");
914
+ const ownProt = carryForward(opts.existing?.prot, "protection");
913
915
  const keepAlign = !!ownAlign && !needsContentUpgrade(ownAlign);
914
916
  const keepProt = !!ownProt && !needsContentUpgrade(ownProt);
915
917
  // A kept card's principal follows the agent's current org (keptPrincipal).
@@ -1215,12 +1217,20 @@ export async function resolvePreflightedAccount() {
1215
1217
  * absent or older, and leaves one at this version or newer as it is. Bump it whenever
1216
1218
  * the content below changes. (`card_version` is the schema date, not this.)
1217
1219
  */
1218
- export const CODE_CARD_CONTENT_VERSION = "mnemom-agent-default/2.1.0";
1220
+ export const CODE_CARD_CONTENT_VERSION = "mnemom-agent-default/2.1.2";
1219
1221
  const CONTENT_LINEAGE = "mnemom-agent-default/";
1220
1222
  function parseContentSemver(v) {
1221
1223
  const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
1222
1224
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
1223
1225
  }
1226
+ /** Negative, zero or positive as semver `a` is older than, equal to or newer than `b`. */
1227
+ function compareSemver(a, b) {
1228
+ for (let i = 0; i < 3; i++) {
1229
+ if (a[i] !== b[i])
1230
+ return a[i] - b[i];
1231
+ }
1232
+ return 0;
1233
+ }
1224
1234
  /**
1225
1235
  * Whether a refresh should replace this card's content with the current default: true when
1226
1236
  * it carries no content_version, or an older (or unparseable) `mnemom-agent-default/` one.
@@ -1291,6 +1301,114 @@ const CODE_BOUNDED_ACTIONS = [
1291
1301
  "WebFetch",
1292
1302
  "WebSearch",
1293
1303
  ];
1304
+ // Cross-session messaging between Claude Code sessions (2.1.1). Mapped to a capability, the
1305
+ // lane-lead card's shape: the card names `converse` with its tools, and each tool is bounded
1306
+ // as "<tool>: converse" (AAP checks the part before the colon, so the observer agrees).
1307
+ const CODE_CONVERSE_TOOLS = ["SendMessage", "ListAgents"];
1308
+ // A session's own schedule (2.1.2), e.g. a seat's hourly self-check. Same shape, its own
1309
+ // capability: none of these talks to another session, and `scheduling` is the policy engine's
1310
+ // catalog name for exactly these four tools.
1311
+ const CODE_SCHEDULING_TOOLS = ["CronCreate", "CronDelete", "CronList", "ScheduleWakeup"];
1312
+ /**
1313
+ * The capabilities the default maps, each with the version that added it. Every version
1314
+ * since IN_PLACE_FLOOR only ADDED one of these (the capability and its "<tool>: <capability>"
1315
+ * bounded actions), which is what lets upgradeInPlace bring a card forward without replacing
1316
+ * it. A version that changes or removes default content (or changes the protection card)
1317
+ * must raise IN_PLACE_FLOOR to itself, so older cards are replaced whole again.
1318
+ */
1319
+ const CODE_CAPABILITIES = [
1320
+ {
1321
+ name: "converse",
1322
+ since: [2, 1, 1],
1323
+ description: "Message and list the other Claude Code sessions",
1324
+ tools: CODE_CONVERSE_TOOLS,
1325
+ },
1326
+ {
1327
+ name: "scheduling",
1328
+ since: [2, 1, 2],
1329
+ description: "Schedule, list and cancel this session's own checks",
1330
+ tools: CODE_SCHEDULING_TOOLS,
1331
+ },
1332
+ ];
1333
+ export const IN_PLACE_FLOOR = [2, 1, 0];
1334
+ /**
1335
+ * Whether an AAP action list names `tool`: a case-insensitive exact match, or the part before
1336
+ * ':' of a "name: description" entry. The policy engine's own rule (shared/policy-engine
1337
+ * src/action-list.ts actionListIncludes), mirrored because the CLI's engine dependency is the
1338
+ * published 0.3.0, which predates it.
1339
+ */
1340
+ function actionListNames(list, tool) {
1341
+ const target = tool.toLowerCase();
1342
+ return list.some((entry) => {
1343
+ if (typeof entry !== "string")
1344
+ return false;
1345
+ if (entry.toLowerCase() === target)
1346
+ return true;
1347
+ const colon = entry.indexOf(":");
1348
+ return colon > 0 && entry.substring(0, colon).trim().toLowerCase() === target;
1349
+ });
1350
+ }
1351
+ /**
1352
+ * A stamped default from IN_PLACE_FLOOR up to (not including) this version, brought forward
1353
+ * in place: its own content kept (`mnemom card edit` round-trips the stamp, so it may carry
1354
+ * the customer's edits), plus each capability added since its version, stamped current.
1355
+ * A capability the card already declares is the customer's and is left as it is, with no
1356
+ * bounded actions added for it; a tool the card already names in bounded_actions or
1357
+ * forbidden_actions is not bound again. Null when the card is not such a default (an older
1358
+ * one), or has no autonomy.bounded_actions list to bind into (the api requires one): either
1359
+ * is replaced whole, as before. A forbidden_actions that is not a list of strings may hold a
1360
+ * forbid this cannot read, so it fails closed: no capability is added and nothing is bound;
1361
+ * the card keeps its own content and is restamped. So is a NARROWED card, one whose
1362
+ * bounded_actions no longer names every tool its stamped version's default bounded.
1363
+ *
1364
+ * Called only by carryForward, after needsContentUpgrade: the stamp is then absent,
1365
+ * unparseable, or an older default of this lineage, so only the floor needs checking.
1366
+ */
1367
+ function upgradeInPlace(card, kind) {
1368
+ const v = card.content_version;
1369
+ const have = typeof v === "string" ? parseContentSemver(v.slice(CONTENT_LINEAGE.length)) : null;
1370
+ if (!have || compareSemver(have, IN_PLACE_FLOOR) < 0)
1371
+ return null;
1372
+ const upgraded = { ...card, content_version: CODE_CARD_CONTENT_VERSION };
1373
+ if (kind === "protection")
1374
+ return upgraded;
1375
+ const autonomy = card.autonomy;
1376
+ if (!isRecord(autonomy) || !Array.isArray(autonomy.bounded_actions))
1377
+ return null;
1378
+ const forbidden = autonomy.forbidden_actions ?? [];
1379
+ if (!Array.isArray(forbidden) || forbidden.some((entry) => typeof entry !== "string")) {
1380
+ return upgraded;
1381
+ }
1382
+ const bounded = [...autonomy.bounded_actions];
1383
+ // A card cut below its version's default bounded set was narrowed on purpose, so it gains
1384
+ // no grant (0.17.28 would not have touched it either): restamped with its own content.
1385
+ const stampedDefault = [
1386
+ ...CODE_BOUNDED_ACTIONS,
1387
+ ...CODE_CAPABILITIES.filter((cap) => compareSemver(cap.since, have) <= 0).flatMap((cap) => cap.tools),
1388
+ ];
1389
+ if (!stampedDefault.every((tool) => actionListNames(bounded, tool)))
1390
+ return upgraded;
1391
+ const capabilities = isRecord(card.capabilities) ? { ...card.capabilities } : {};
1392
+ for (const cap of CODE_CAPABILITIES) {
1393
+ if (compareSemver(cap.since, have) <= 0 || cap.name in capabilities)
1394
+ continue;
1395
+ capabilities[cap.name] = { description: cap.description, tools: [...cap.tools] };
1396
+ for (const tool of cap.tools) {
1397
+ if (!actionListNames(bounded, tool) && !actionListNames(forbidden, tool)) {
1398
+ bounded.push(`${tool}: ${cap.name}`);
1399
+ }
1400
+ }
1401
+ }
1402
+ upgraded.capabilities = capabilities;
1403
+ upgraded.autonomy = { ...autonomy, bounded_actions: bounded };
1404
+ return upgraded;
1405
+ }
1406
+ /** An existing card as a refresh decides on it: a 2.1.x default brought forward in place. */
1407
+ function carryForward(card, kind) {
1408
+ if (!card || !needsContentUpgrade(card))
1409
+ return card;
1410
+ return upgradeInPlace(card, kind) ?? card;
1411
+ }
1294
1412
  export function buildCodeAlignmentCard(agentId, modes = {
1295
1413
  autonomy: "off",
1296
1414
  integrity: "off",
@@ -1339,8 +1457,15 @@ export function buildCodeAlignmentCard(agentId, modes = {
1339
1457
  },
1340
1458
  },
1341
1459
  },
1460
+ capabilities: Object.fromEntries(CODE_CAPABILITIES.map((cap) => [
1461
+ cap.name,
1462
+ { description: cap.description, tools: [...cap.tools] },
1463
+ ])),
1342
1464
  autonomy: {
1343
- bounded_actions: [...CODE_BOUNDED_ACTIONS],
1465
+ bounded_actions: [
1466
+ ...CODE_BOUNDED_ACTIONS,
1467
+ ...CODE_CAPABILITIES.flatMap((cap) => cap.tools.map((tool) => `${tool}: ${cap.name}`)),
1468
+ ],
1344
1469
  forbidden_actions: [
1345
1470
  "force_push",
1346
1471
  "push_protected_branch",
@@ -45,7 +45,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
45
45
  import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
46
46
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
47
47
  import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, readComposedProtection, resolveDefaultSlug, resolvePinnedAgent, resolvePreflightedAccount, } from "./agent-setup.js";
48
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_KEY_ENV_NAME, malformedAgentIdMessage, parseAgentRef, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, passthroughModel, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
48
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_KEY_ENV_NAME, malformedAgentIdMessage, parseAgentRef, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, passthroughModel, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, liftPassthroughName, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
49
49
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
51
  import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
@@ -163,6 +163,9 @@ function windowModelFor(options, cli) {
163
163
  }
164
164
  /** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
165
165
  export async function agentCommand(scenarioArg, options = {}) {
166
+ // An unusable name after `--` refuses here, as a rejected launch: the RC path lifts it again
167
+ // in the proxy-ready timer, where a throw would be an uncaught crash, not a refusal.
168
+ liftPassthroughName(options.passthrough ?? []);
166
169
  // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
167
170
  // goal alignment is resolved upstream: --goal seals a contract, implicit alignment
168
171
  // is on by default only while the `implicit` experimental feature is on.
@@ -1515,18 +1518,10 @@ function launchRemoteControl(ctx) {
1515
1518
  }
1516
1519
  function spawnRcCli(ctx) {
1517
1520
  // Session name for the claude.ai session list: "<agent>-<scenario>", unless
1518
- // the caller passed their own `--name <x>` after `--` (lifted out here).
1519
- const rawPassthrough = ctx.options.passthrough ?? [];
1520
- let rcName = sessionDisplayName(ctx.agent, ctx.scenario);
1521
- const passthrough = [];
1522
- for (let i = 0; i < rawPassthrough.length; i++) {
1523
- if (rawPassthrough[i] === "--name" && i + 1 < rawPassthrough.length) {
1524
- rcName = rawPassthrough[i + 1];
1525
- i++; // consume the value
1526
- continue;
1527
- }
1528
- passthrough.push(rawPassthrough[i]);
1529
- }
1521
+ // the caller passed their own name after `--` (`--name x`, `--name=x`, `-n x`; lifted out here).
1522
+ const lifted = liftPassthroughName(ctx.options.passthrough ?? []);
1523
+ const rcName = lifted.name ?? sessionDisplayName(ctx.agent, ctx.scenario);
1524
+ const passthrough = lifted.rest;
1530
1525
  const scopedOptions = { ...ctx.options, passthrough };
1531
1526
  // Every auth-shaped ANTHROPIC_* var is UNSET so the RC gate sees a first-party
1532
1527
  // OAuth setup; NO_PROXY/HTTP_PROXY unset so api.anthropic.com is not excluded.
@@ -1605,7 +1600,9 @@ function spawnRcCli(ctx) {
1605
1600
  else {
1606
1601
  // Interactive RC in THIS terminal: the CLI's own `--remote-control <name>`
1607
1602
  // startup flag; --model/--effort/--trim-mcp/yolo apply as in a normal launch.
1608
- // buildCliArgs already includes the (name-stripped) passthrough.
1603
+ // buildCliArgs already includes the (name-stripped) passthrough. `--name` carries the
1604
+ // same name to the session itself, so its cross-session name (what ListAgents and
1605
+ // SendMessage address) is the seat name, not one Claude Code picks.
1609
1606
  const flagArgs = buildCliArgs(scopedOptions, {
1610
1607
  yolo: ctx.yolo,
1611
1608
  allowCliFlags: true,
@@ -1613,7 +1610,7 @@ function spawnRcCli(ctx) {
1613
1610
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1614
1611
  pluginDir: ctx.plugin?.dir,
1615
1612
  });
1616
- args = ["--remote-control", rcName, ...flagArgs];
1613
+ args = ["--remote-control", rcName, "--name", rcName, ...flagArgs];
1617
1614
  if (ctx.options.slack)
1618
1615
  args.push(...slackChannelArgs(rcName));
1619
1616
  say(`mnemom agent: remote-control: interactive session in THIS terminal with Remote Control on (session name '${rcName}'); it also appears under Remote Control at claude.ai/code and in the Claude app. Exit the CLI to stop everything.`);
@@ -20,6 +20,7 @@ import { mergeAgentConfig } from "../lib/agent-config.js";
20
20
  import { askSelect, askInput, isInteractive } from "../lib/prompt.js";
21
21
  import { getGatewayUrl } from "../lib/config.js";
22
22
  import { fmt } from "../lib/format.js";
23
+ import { OPENAI_WRAP_DEFAULT_MODEL, openaiBirthProbeBody } from "../lib/birth-probe.js";
23
24
  const AGENT_HEADER = "x-mnemom-agent";
24
25
  const PROVIDERS = {
25
26
  anthropic: {
@@ -33,9 +34,7 @@ const PROVIDERS = {
33
34
  label: "OpenAI",
34
35
  keyEnv: "OPENAI_API_KEY",
35
36
  keyHeader: "Authorization",
36
- // gpt-6-luna: the successor MNE-8754 ruled for gpt-4o-mini, which OpenAI shuts down
37
- // 2027-01-14 (MNE-8755).
38
- defaultModel: "gpt-6-luna",
37
+ defaultModel: OPENAI_WRAP_DEFAULT_MODEL,
39
38
  basePath: "/openai",
40
39
  },
41
40
  gemini: {
@@ -220,14 +219,7 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
220
219
  switch (provider) {
221
220
  case "openai":
222
221
  url = `${gatewayUrl}${prov.basePath}/v1/chat/completions`;
223
- // max_completion_tokens, not max_tokens: gpt-6-luna is a reasoning-profile model and
224
- // OpenAI rejects max_tokens for those on Chat Completions (models-registry.ts
225
- // REASONING_SUPPORTED_PARAMS). Older chat models accept max_completion_tokens too.
226
- body = JSON.stringify({
227
- model: prov.defaultModel,
228
- max_completion_tokens: 16,
229
- messages: [{ role: "user", content: "hello" }],
230
- });
222
+ body = JSON.stringify(openaiBirthProbeBody(prov.defaultModel));
231
223
  break;
232
224
  case "gemini":
233
225
  url = `${gatewayUrl}${prov.basePath}/v1beta/models/${prov.defaultModel}:generateContent`;
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledE
37
37
  import { loadExperimentalState } from "./lib/cli-config.js";
38
38
  import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
39
39
  import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
40
- import { parseCompactAt, parseContextProfile, resolveImplicit, selectLaunchAgent, } from "./lib/agent.js";
40
+ import { parseCompactAt, parseContextProfile, resolveImplicit, selectLaunchAgent, splitSessionArg, } from "./lib/agent.js";
41
41
  import { wrapCommand } from "./commands/wrap.js";
42
42
  import { onboardCommand } from "./commands/onboard.js";
43
43
  import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
@@ -228,8 +228,13 @@ const agentCmd = program
228
228
  .option("--no-compaction-intercept", "Turn the compaction intercept off for this launch (it is on by default) — Claude Code's compaction goes to the model as usual. To turn it off for every launch: config key compaction_intercept = false, or env MNEMOM_AGENT_COMPACTION_INTERCEPT=0")
229
229
  .option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
230
230
  .option("--first-run", "Show the first-run look again: what Mnemom would have saved on your last 30 days of Claude Code (runs on this machine, read-only, nothing uploaded) — then exit")
231
- .action(async (scenario, passthrough, opts) => {
231
+ .action(async (sessionArg, passthroughArg, opts) => {
232
+ let scenario;
233
+ let passthrough;
232
234
  try {
235
+ // A Claude Code flag after `--` is never the session name, and an unusable or reserved
236
+ // name given there refuses before any sub-verb runs (splitSessionArg).
237
+ ({ scenario, passthrough } = splitSessionArg(sessionArg, passthroughArg));
233
238
  // `help` is reserved: `mnemom agent help [verb]` prints help and never launches
234
239
  // (nor redeems an --invite). Without this, `help` was taken as a session name and
235
240
  // the verb passed through to Claude Code as the opening prompt.
@@ -322,8 +322,37 @@ export declare function resumeCommand(ctx: {
322
322
  }): string;
323
323
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
324
324
  export declare function sessionDisplayName(agent: string, scenario: string): string;
325
- /** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
325
+ /**
326
+ * A caller's own session name in the Claude Code args after `--`, in each spelling Claude Code
327
+ * takes (`--name <x>`, `--name=<x>`, `-n <x>`; the last one wins), and the args without them.
328
+ * Throws when a name is given but unusable: no value, a flag as its value (`--name -p x` would
329
+ * take `-p` and drop headless mode), or a name with no letters or digits (blank, `!!!`), which
330
+ * would share its Slack state files with every other such name (normSession is empty).
331
+ */
332
+ export declare function liftPassthroughName(passthrough: readonly string[]): {
333
+ name: string | undefined;
334
+ rest: string[];
335
+ };
336
+ /** A caller's own session name in the passthrough args, if any (liftPassthroughName). */
326
337
  export declare function passthroughName(passthrough: readonly string[]): string | undefined;
338
+ /**
339
+ * The words `mnemom agent [session]` routes to a sub-verb (or a commander subcommand), so a
340
+ * session can never take one through `--name`: `-- --name setup -p x` would run provisioning.
341
+ */
342
+ export declare const AGENT_SUB_VERBS: ReadonlySet<string>;
343
+ /**
344
+ * The session name and Claude Code args of `mnemom agent [session] [passthrough...]`, with a
345
+ * flag commander took as the session put back. Commander fills `[session]` with the first
346
+ * operand even after `--`, so `mnemom agent -- --name x -p` arrived as session "--name" and
347
+ * passthrough `x -p` (probe P8). A leading-dash session can only have come from after `--`
348
+ * (before it, commander parses a flag as an option), so it returns to the front of the
349
+ * passthrough; with no session left, a passthrough `--name <x>` names it. Throws on an
350
+ * unusable name (liftPassthroughName) or a sub-verb word (AGENT_SUB_VERBS).
351
+ */
352
+ export declare function splitSessionArg(session: string | undefined, passthrough: readonly string[]): {
353
+ scenario: string | undefined;
354
+ passthrough: string[];
355
+ };
327
356
  /** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
328
357
  export declare function resolveConversationId(raw: string | undefined): string;
329
358
  /** A strictly-positive integer (rejects 0, negatives, decimals, NaN, non-numeric strings). */
package/dist/lib/agent.js CHANGED
@@ -23,6 +23,7 @@ import { createHash, randomUUID } from "node:crypto";
23
23
  import { readFileSync } from "node:fs";
24
24
  import { join } from "node:path";
25
25
  import { getGatewayUrl } from "./config.js";
26
+ import { normSession } from "./slack-route.js";
26
27
  /** The us-2/prod gateway host. The default door — never any other cell silently. */
27
28
  export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
28
29
  /**
@@ -384,14 +385,80 @@ export function resumeCommand(ctx) {
384
385
  export function sessionDisplayName(agent, scenario) {
385
386
  return sanitizeAgentName(`${agent}-${scenario}`) || agent;
386
387
  }
387
- /** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
388
- export function passthroughName(passthrough) {
388
+ /**
389
+ * A caller's own session name in the Claude Code args after `--`, in each spelling Claude Code
390
+ * takes (`--name <x>`, `--name=<x>`, `-n <x>`; the last one wins), and the args without them.
391
+ * Throws when a name is given but unusable: no value, a flag as its value (`--name -p x` would
392
+ * take `-p` and drop headless mode), or a name with no letters or digits (blank, `!!!`), which
393
+ * would share its Slack state files with every other such name (normSession is empty).
394
+ */
395
+ export function liftPassthroughName(passthrough) {
389
396
  let name;
390
- for (let i = 0; i < passthrough.length - 1; i++) {
391
- if (passthrough[i] === "--name")
392
- name = passthrough[i + 1];
397
+ const rest = [];
398
+ for (let i = 0; i < passthrough.length; i++) {
399
+ const arg = passthrough[i];
400
+ if (arg.startsWith("--name=")) {
401
+ name = arg.slice("--name=".length);
402
+ }
403
+ else if (arg === "--name" || arg === "-n") {
404
+ const value = passthrough[i + 1];
405
+ if (value === undefined || value.startsWith("-")) {
406
+ throw new Error(`mnemom agent: ${arg} needs a session name after it (e.g. \`${arg} onboarding\`), not ${value === undefined ? "nothing" : `the flag \`${value}\``}.`);
407
+ }
408
+ name = value;
409
+ i++;
410
+ }
411
+ else {
412
+ rest.push(arg);
413
+ continue;
414
+ }
415
+ if (!normSession(name)) {
416
+ throw new Error(`mnemom agent: '${name}' is not a usable session name: give it a letter or a digit.`);
417
+ }
418
+ }
419
+ return { name, rest };
420
+ }
421
+ /** A caller's own session name in the passthrough args, if any (liftPassthroughName). */
422
+ export function passthroughName(passthrough) {
423
+ return liftPassthroughName(passthrough).name;
424
+ }
425
+ /**
426
+ * The words `mnemom agent [session]` routes to a sub-verb (or a commander subcommand), so a
427
+ * session can never take one through `--name`: `-- --name setup -p x` would run provisioning.
428
+ */
429
+ export const AGENT_SUB_VERBS = new Set([
430
+ "help",
431
+ "invite",
432
+ "config",
433
+ "slack",
434
+ "doctor",
435
+ "setup",
436
+ "list",
437
+ "claim",
438
+ "move",
439
+ "export",
440
+ "sessions",
441
+ "show",
442
+ "goal",
443
+ ]);
444
+ /**
445
+ * The session name and Claude Code args of `mnemom agent [session] [passthrough...]`, with a
446
+ * flag commander took as the session put back. Commander fills `[session]` with the first
447
+ * operand even after `--`, so `mnemom agent -- --name x -p` arrived as session "--name" and
448
+ * passthrough `x -p` (probe P8). A leading-dash session can only have come from after `--`
449
+ * (before it, commander parses a flag as an option), so it returns to the front of the
450
+ * passthrough; with no session left, a passthrough `--name <x>` names it. Throws on an
451
+ * unusable name (liftPassthroughName) or a sub-verb word (AGENT_SUB_VERBS).
452
+ */
453
+ export function splitSessionArg(session, passthrough) {
454
+ if (!session?.startsWith("-"))
455
+ return { scenario: session, passthrough: [...passthrough] };
456
+ const args = [session, ...passthrough];
457
+ const scenario = passthroughName(args);
458
+ if (scenario !== undefined && AGENT_SUB_VERBS.has(scenario.trim().toLowerCase())) {
459
+ throw new Error(`mnemom agent: '${scenario}' is a \`mnemom agent\` sub-verb, not a session name; pass another --name.`);
393
460
  }
394
- return name;
461
+ return { scenario, passthrough: args };
395
462
  }
396
463
  /** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
397
464
  export function resolveConversationId(raw) {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The OpenAI half of `mnemom wrap`'s birth probe, kept free of imports so
3
+ * `gateway/test/providers-live/openai-live.test.ts` can send these exact bytes
4
+ * to OpenAI without installing the CLI's dependencies (MNE-8755).
5
+ */
6
+ export declare const OPENAI_WRAP_DEFAULT_MODEL = "gpt-6-luna";
7
+ /**
8
+ * Chat Completions body for the one-time birth call. max_completion_tokens, not
9
+ * max_tokens: gpt-6-luna is a reasoning-profile model and OpenAI rejects max_tokens
10
+ * for those on Chat Completions (models-registry.ts REASONING_SUPPORTED_PARAMS).
11
+ * Older chat models accept max_completion_tokens too.
12
+ */
13
+ export declare function openaiBirthProbeBody(model: string): {
14
+ model: string;
15
+ max_completion_tokens: number;
16
+ messages: {
17
+ role: "user";
18
+ content: string;
19
+ }[];
20
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The OpenAI half of `mnemom wrap`'s birth probe, kept free of imports so
3
+ * `gateway/test/providers-live/openai-live.test.ts` can send these exact bytes
4
+ * to OpenAI without installing the CLI's dependencies (MNE-8755).
5
+ */
6
+ // gpt-6-luna replaces gpt-4o-mini, which OpenAI shuts down 2027-01-14. MNE-8754 ruled the
7
+ // successor; MNE-8755 made the switch.
8
+ export const OPENAI_WRAP_DEFAULT_MODEL = "gpt-6-luna";
9
+ /**
10
+ * Chat Completions body for the one-time birth call. max_completion_tokens, not
11
+ * max_tokens: gpt-6-luna is a reasoning-profile model and OpenAI rejects max_tokens
12
+ * for those on Chat Completions (models-registry.ts REASONING_SUPPORTED_PARAMS).
13
+ * Older chat models accept max_completion_tokens too.
14
+ */
15
+ export function openaiBirthProbeBody(model) {
16
+ return {
17
+ model,
18
+ max_completion_tokens: 16,
19
+ messages: [{ role: "user", content: "hello" }],
20
+ };
21
+ }
@@ -10,11 +10,17 @@ export declare const MNEMOM_DIR: string;
10
10
  export type Environment = "production" | "local";
11
11
  /**
12
12
  * Thrown for `MNEMOM_ENV=staging`: the value used to be accepted, and the ring
13
- * it named is gone (MNE-7968). Failing loud is deliberate — the two silent
14
- * alternatives are both wrong: resolving to the dead staging hostnames makes
15
- * every call fail with a DNS/connection error that does not say why, and
16
- * falling through to production would send a caller who asked for a non-prod
17
- * ring at real customer data.
13
+ * it named is gone (MNE-7968, retired 2026-10-08 by platform#2446). Failing
14
+ * loud is deliberate. The two silent alternatives are both wrong: resolving to
15
+ * the dead staging hostnames makes every call fail with a DNS/connection error
16
+ * that does not say why, and falling through to production would send a caller
17
+ * who asked for a non-prod ring at real customer data.
18
+ *
19
+ * The message is customer-facing (published CLI), so it names only the public
20
+ * values. No internal card ids, no internal rings (MNE-8291: the Azure test
21
+ * cell is not a customer surface), and no link: the staging retirement has no
22
+ * public changelog entry by ruling (2026-10-16), so the message carries the
23
+ * facts itself.
18
24
  */
19
25
  export declare class RetiredEnvironmentError extends Error {
20
26
  constructor();
@@ -23,18 +23,22 @@ const WEBSITE_URLS = {
23
23
  };
24
24
  /**
25
25
  * Thrown for `MNEMOM_ENV=staging`: the value used to be accepted, and the ring
26
- * it named is gone (MNE-7968). Failing loud is deliberate — the two silent
27
- * alternatives are both wrong: resolving to the dead staging hostnames makes
28
- * every call fail with a DNS/connection error that does not say why, and
29
- * falling through to production would send a caller who asked for a non-prod
30
- * ring at real customer data.
26
+ * it named is gone (MNE-7968, retired 2026-10-08 by platform#2446). Failing
27
+ * loud is deliberate. The two silent alternatives are both wrong: resolving to
28
+ * the dead staging hostnames makes every call fail with a DNS/connection error
29
+ * that does not say why, and falling through to production would send a caller
30
+ * who asked for a non-prod ring at real customer data.
31
+ *
32
+ * The message is customer-facing (published CLI), so it names only the public
33
+ * values. No internal card ids, no internal rings (MNE-8291: the Azure test
34
+ * cell is not a customer surface), and no link: the staging retirement has no
35
+ * public changelog entry by ruling (2026-10-16), so the message carries the
36
+ * facts itself.
31
37
  */
32
38
  export class RetiredEnvironmentError extends Error {
33
39
  constructor() {
34
- super("MNEMOM_ENV=staging is no longer supported: the staging ring was retired (MNE-7968) " +
35
- "and nothing serves api-staging / gateway-staging.mnemom.ai. " +
36
- "Use MNEMOM_RING=us-1 for the preview cell, MNEMOM_ENV=local for a local stack, " +
37
- "or set MNEMOM_API_URL / MNEMOM_GATEWAY_URL / MNEMOM_WEBSITE_URL to target another cell.");
40
+ super("MNEMOM_ENV=staging is no longer supported: the staging environment was retired on 2026-10-08. " +
41
+ "Supported MNEMOM_ENV values are production (the default) and local.");
38
42
  this.name = "RetiredEnvironmentError";
39
43
  }
40
44
  }
@@ -67,7 +67,12 @@ export interface PostRecord {
67
67
  */
68
68
  export type ThreadIndex = Map<string, Map<string, Map<string, string | undefined>>>;
69
69
  export declare function buildThreadIndex(posts: Iterable<PostRecord>): ThreadIndex;
70
- /** Normalise a session name for comparison (same shape the launcher emits). */
70
+ /**
71
+ * Normalise a session name for comparison (same shape the launcher emits): the
72
+ * lowercase alphanumeric runs joined by single dashes. Matching the runs is one
73
+ * linear pass, with no trim regex (`/-+$/` backtracks quadratically on a run of
74
+ * dashes).
75
+ */
71
76
  export declare function normSession(raw: string): string;
72
77
  /**
73
78
  * The live session `name` addresses: an exact (normalised) match, else the ONE live
@@ -54,12 +54,14 @@ export function buildThreadIndex(posts) {
54
54
  }
55
55
  return idx;
56
56
  }
57
- /** Normalise a session name for comparison (same shape the launcher emits). */
57
+ /**
58
+ * Normalise a session name for comparison (same shape the launcher emits): the
59
+ * lowercase alphanumeric runs joined by single dashes. Matching the runs is one
60
+ * linear pass, with no trim regex (`/-+$/` backtracks quadratically on a run of
61
+ * dashes).
62
+ */
58
63
  export function normSession(raw) {
59
- return raw
60
- .toLowerCase()
61
- .replace(/[^a-z0-9]+/g, "-")
62
- .replace(/^-+|-+$/g, "");
64
+ return (raw.toLowerCase().match(/[a-z0-9]+/g) ?? []).join("-");
63
65
  }
64
66
  const HANDLE_RE = /(?:^|[\s(])@([a-z0-9][a-z0-9-]*[a-z0-9])(?=$|[\s,.:;!?)])/gi;
65
67
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.28",
3
+ "version": "0.17.29",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {