@mnemom/mnemom 0.17.28 → 0.17.30

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",
@@ -88,6 +88,12 @@ export interface AgentOptions {
88
88
  * the #2216 intercept/real-size keys for this session only; unset/null sends nothing new.
89
89
  */
90
90
  compactAt?: number | null;
91
+ /**
92
+ * `--allow-autocompact-off` (or MNEMOM_AGENT_ALLOW_AUTOCOMPACT_OFF=1): keep Claude Code's
93
+ * auto-compaction off when the user turned it off. Default: the launch turns it back on for
94
+ * this session and says so (lib/autocompact-guard.ts).
95
+ */
96
+ allowAutocompactOff?: boolean;
91
97
  /** context-hint seed default: commander `--no-context-hint` sets this false. */
92
98
  contextHint?: boolean;
93
99
  /** Attach the Slack channel server (`--slack`; lib/slack-launch.ts). */
@@ -32,6 +32,7 @@ import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { MEASURED_ENV, statuslineLaunch } from "../lib/statusline.js";
35
+ import { ALLOW_AUTOCOMPACT_OFF_ENV, dropAutocompactOffEnv, hasOwnSettingsFlag, mergedSettingsArgs, planAutocompactGuard, } from "../lib/autocompact-guard.js";
35
36
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
36
37
  import { createApiKey } from "../lib/api.js";
37
38
  import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
@@ -45,7 +46,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
45
46
  import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
46
47
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
47
48
  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";
49
+ 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
50
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
51
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
52
  import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
@@ -163,6 +164,9 @@ function windowModelFor(options, cli) {
163
164
  }
164
165
  /** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
165
166
  export async function agentCommand(scenarioArg, options = {}) {
167
+ // An unusable name after `--` refuses here, as a rejected launch: the RC path lifts it again
168
+ // in the proxy-ready timer, where a throw would be an uncaught crash, not a refusal.
169
+ liftPassthroughName(options.passthrough ?? []);
166
170
  // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
167
171
  // goal alignment is resolved upstream: --goal seals a contract, implicit alignment
168
172
  // is on by default only while the `implicit` experimental feature is on.
@@ -1263,18 +1267,20 @@ function launchTerminal(ctx) {
1263
1267
  // decision alone: a value inherited from an outer `mnemom agent` session never rides along.
1264
1268
  Reflect.deleteProperty(childEnv, MEASURED_ENV);
1265
1269
  if (ctx.cli.name === "claude") {
1270
+ const passthrough = ctx.options.passthrough ?? [];
1266
1271
  const sl = statuslineLaunch({
1267
- passthrough: ctx.options.passthrough ?? [],
1272
+ passthrough,
1268
1273
  env: childEnv,
1269
1274
  cwd: process.cwd(),
1270
1275
  node: process.execPath,
1271
1276
  bin: join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "statusline-bin.js"),
1272
1277
  measured: ctx.usageSavings,
1273
1278
  });
1274
- if (sl) {
1275
- args.push(...sl.args);
1279
+ if (sl)
1276
1280
  Object.assign(childEnv, sl.env);
1277
- }
1281
+ // Autocompact guard (lib/autocompact-guard.ts): one --settings carries both layers.
1282
+ const guard = applyAutocompactGuard(childEnv, ctx.options, !hasOwnSettingsFlag(passthrough));
1283
+ args.push(...mergedSettingsArgs(guard, sl?.settings ?? null));
1278
1284
  }
1279
1285
  if (ctx.contextHint) {
1280
1286
  seedContextHintFlag(childEnv);
@@ -1515,18 +1521,10 @@ function launchRemoteControl(ctx) {
1515
1521
  }
1516
1522
  function spawnRcCli(ctx) {
1517
1523
  // 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
- }
1524
+ // the caller passed their own name after `--` (`--name x`, `--name=x`, `-n x`; lifted out here).
1525
+ const lifted = liftPassthroughName(ctx.options.passthrough ?? []);
1526
+ const rcName = lifted.name ?? sessionDisplayName(ctx.agent, ctx.scenario);
1527
+ const passthrough = lifted.rest;
1530
1528
  const scopedOptions = { ...ctx.options, passthrough };
1531
1529
  // Every auth-shaped ANTHROPIC_* var is UNSET so the RC gate sees a first-party
1532
1530
  // OAuth setup; NO_PROXY/HTTP_PROXY unset so api.anthropic.com is not excluded.
@@ -1552,6 +1550,10 @@ function spawnRcCli(ctx) {
1552
1550
  childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
1553
1551
  childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
1554
1552
  childEnv[SESSION_NAME_ENV] = rcName;
1553
+ // Autocompact guard (lib/autocompact-guard.ts). The interactive shape gets the --settings layer
1554
+ // below; the headless server only drops the env switches (its sessions are spawned by the server).
1555
+ const rcGuard = ctx.cli.name === "claude" &&
1556
+ applyAutocompactGuard(childEnv, ctx.options, ctx.shape !== "server" && !hasOwnSettingsFlag(passthrough));
1555
1557
  // Same exact compaction signal as the terminal shape. Not an ANTHROPIC_* var, so the RC gate
1556
1558
  // ignores it (like HTTPS_PROXY above); the proxy forwards every non-hop request header, so the
1557
1559
  // x-claude-code-* hints reach the gateway. The user's own autocompact env is not on the strip
@@ -1605,7 +1607,9 @@ function spawnRcCli(ctx) {
1605
1607
  else {
1606
1608
  // Interactive RC in THIS terminal: the CLI's own `--remote-control <name>`
1607
1609
  // startup flag; --model/--effort/--trim-mcp/yolo apply as in a normal launch.
1608
- // buildCliArgs already includes the (name-stripped) passthrough.
1610
+ // buildCliArgs already includes the (name-stripped) passthrough. `--name` carries the
1611
+ // same name to the session itself, so its cross-session name (what ListAgents and
1612
+ // SendMessage address) is the seat name, not one Claude Code picks.
1609
1613
  const flagArgs = buildCliArgs(scopedOptions, {
1610
1614
  yolo: ctx.yolo,
1611
1615
  allowCliFlags: true,
@@ -1613,7 +1617,9 @@ function spawnRcCli(ctx) {
1613
1617
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1614
1618
  pluginDir: ctx.plugin?.dir,
1615
1619
  });
1616
- args = ["--remote-control", rcName, ...flagArgs];
1620
+ args = ["--remote-control", rcName, "--name", rcName, ...flagArgs];
1621
+ if (rcGuard)
1622
+ args.push(...mergedSettingsArgs(true, null));
1617
1623
  if (ctx.options.slack)
1618
1624
  args.push(...slackChannelArgs(rcName));
1619
1625
  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.`);
@@ -1807,6 +1813,25 @@ export function pickCcSessionId(passthrough, newId = randomUUID) {
1807
1813
  // ───────────────────────────────────────────────────────────────────────────
1808
1814
  // Context-hint seed (opt-out) — best-effort, never throws
1809
1815
  // ───────────────────────────────────────────────────────────────────────────
1816
+ /**
1817
+ * Warn when Claude Code's auto-compaction is off for this launch and, unless the user opted out,
1818
+ * drop its env switches from `childEnv`. Returns true when the caller should add the
1819
+ * `--settings` layer that turns it back on (only when `canPassSettings`).
1820
+ */
1821
+ function applyAutocompactGuard(childEnv, options, canPassSettings) {
1822
+ const plan = planAutocompactGuard({
1823
+ env: childEnv,
1824
+ cwd: process.cwd(),
1825
+ allowOff: options.allowAutocompactOff === true || process.env[ALLOW_AUTOCOMPACT_OFF_ENV] === "1",
1826
+ canPassSettings,
1827
+ });
1828
+ if (plan.warning)
1829
+ say(plan.warning);
1830
+ if (!plan.override)
1831
+ return false;
1832
+ dropAutocompactOffEnv(childEnv);
1833
+ return canPassSettings;
1834
+ }
1810
1835
  /**
1811
1836
  * Set cachedGrowthBookFeatures.tengu_hazel_osprey = true in the local Claude
1812
1837
  * Code config (${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json) so the client offers the
@@ -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";
@@ -180,6 +180,7 @@ const agentCmd = program
180
180
  "xhigh",
181
181
  "max",
182
182
  ]))
183
+ .option("--allow-autocompact-off", 'Keep Claude Code\'s auto-compact off when you turned it off (DISABLE_AUTO_COMPACT, DISABLE_COMPACT or autoCompactEnabled: false). By default the launch turns it back on for this session and says so: with it off, a long session ends in "Prompt is too long" at the context limit. Also: env MNEMOM_AGENT_ALLOW_AUTOCOMPACT_OFF=1')
183
184
  .option("--trim-mcp", "Launch the CLI with no MCP servers (--strict-mcp-config + empty --mcp-config)")
184
185
  .option("--invite <token>", "Redeem a Mnemom Agent invitation (pinv_…): unlocks Mnemom Agent for you + credits your starter µ; launches too when a session name is given")
185
186
  .option("--setup", "Re-check the served agent and refresh its default cards before launch")
@@ -228,8 +229,13 @@ const agentCmd = program
228
229
  .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
230
  .option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
230
231
  .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) => {
232
+ .action(async (sessionArg, passthroughArg, opts) => {
233
+ let scenario;
234
+ let passthrough;
232
235
  try {
236
+ // A Claude Code flag after `--` is never the session name, and an unusable or reserved
237
+ // name given there refuses before any sub-verb runs (splitSessionArg).
238
+ ({ scenario, passthrough } = splitSessionArg(sessionArg, passthroughArg));
233
239
  // `help` is reserved: `mnemom agent help [verb]` prints help and never launches
234
240
  // (nor redeems an --invite). Without this, `help` was taken as a session name and
235
241
  // the verb passed through to Claude Code as the opening prompt.
@@ -374,6 +380,7 @@ const agentCmd = program
374
380
  model: opts.model ?? cfg.model,
375
381
  effort: opts.effort ?? cfg.effort,
376
382
  trimMcp: opts.trimMcp,
383
+ allowAutocompactOff: opts.allowAutocompactOff,
377
384
  yolo: opts.yolo,
378
385
  contextHint: opts.contextHint,
379
386
  plugin: opts.plugin,
@@ -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,67 @@
1
+ /** Opt-out env: "1" keeps the user's autocompact-off setting as it is (warning only). */
2
+ export declare const ALLOW_AUTOCOMPACT_OFF_ENV = "MNEMOM_AGENT_ALLOW_AUTOCOMPACT_OFF";
3
+ /** Claude Code's env switches that turn auto-compaction off (DISABLE_COMPACT also /compact). */
4
+ export declare const AUTOCOMPACT_OFF_ENV: readonly ["DISABLE_AUTO_COMPACT", "DISABLE_COMPACT"];
5
+ /** The `--settings` layer that turns it back on. */
6
+ export declare const AUTOCOMPACT_ON_SETTINGS: {
7
+ readonly autoCompactEnabled: true;
8
+ readonly env: {
9
+ readonly DISABLE_AUTO_COMPACT: "0";
10
+ readonly DISABLE_COMPACT: "0";
11
+ };
12
+ };
13
+ /** One switch that turns autocompact off, named the way the user would find it. */
14
+ export interface AutocompactOffSwitch {
15
+ /** e.g. `DISABLE_AUTO_COMPACT=1` or `autoCompactEnabled: false`. */
16
+ setting: string;
17
+ /** e.g. `your environment` or a file path. */
18
+ where: string;
19
+ }
20
+ /**
21
+ * Claude Code's truthiness for these env values. A settings file's `env` block can hold a JSON
22
+ * boolean: `true` turns it off and `false` clears an off process env (MEASURED, 2.1.301).
23
+ */
24
+ export declare function envOn(v: unknown): boolean;
25
+ /**
26
+ * Every switch that leaves Claude Code's autocompact off for a launch from `cwd`, in the order
27
+ * Claude Code layers them (later wins): the global config, then the user, project and local
28
+ * settings files for `autoCompactEnabled`; the process env, then each settings file's `env` block
29
+ * for the two env switches. Only the switches still in force are returned (a later layer turning
30
+ * it back on clears an earlier one). Managed (policy) settings are not read — the launcher could
31
+ * not override them anyway. Never throws.
32
+ */
33
+ export declare function detectAutocompactOff(o: {
34
+ env: NodeJS.ProcessEnv;
35
+ cwd: string;
36
+ home?: string;
37
+ }): AutocompactOffSwitch[];
38
+ /** True when the user passed their own `--settings` (a second one would replace ours whole). */
39
+ export declare function hasOwnSettingsFlag(passthrough: readonly string[]): boolean;
40
+ export interface AutocompactGuardPlan {
41
+ /** What turned it off; empty ⇒ nothing to do, no warning. */
42
+ switches: AutocompactOffSwitch[];
43
+ /** True ⇒ the launcher turns it back on for this child (env drop + settings layer). */
44
+ override: boolean;
45
+ /** The one warning line to print, or null when nothing is off. */
46
+ warning: string | null;
47
+ }
48
+ /**
49
+ * Decide what the launch does about autocompact. `canPassSettings` is false where the launcher
50
+ * cannot add a `--settings` layer (the user's own `--settings`, the headless RC server); there the
51
+ * env switches are still dropped, but a settings-file switch can only be warned about.
52
+ */
53
+ export declare function planAutocompactGuard(o: {
54
+ env: NodeJS.ProcessEnv;
55
+ cwd: string;
56
+ home?: string;
57
+ allowOff: boolean;
58
+ canPassSettings: boolean;
59
+ }): AutocompactGuardPlan;
60
+ /** Drop the env switches from a child env. Mutates and returns it. */
61
+ export declare function dropAutocompactOffEnv(childEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
62
+ /**
63
+ * The child's `--settings` args: our layer merged with the status-bar layer (`extra`), since
64
+ * Claude Code keeps only the LAST `--settings` it is given (MEASURED, 2.1.301) — two would make
65
+ * one of them vanish. Empty when there is nothing to pass.
66
+ */
67
+ export declare function mergedSettingsArgs(guard: boolean, extra: Record<string, unknown> | null): string[];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Autocompact guard — Claude Code's own auto-compaction switched off ends a long session in a
3
+ * lockout: once the context reaches the client's hard block (967k on a 1M window), every turn
4
+ * fails locally with "Prompt is too long", nothing reaches the gateway, and only a manual
5
+ * /compact gets the session going again. Reproduced 2026-10-17 through the launcher on the test
6
+ * cell (Claude Code 2.1.301): with autocompact on, every crossing compacts (the gateway intercepts
7
+ * it); with DISABLE_AUTO_COMPACT=1 the session locks at 966,753 and the debug file shows no
8
+ * `autocompact:` line at all.
9
+ *
10
+ * At launch the launcher finds every switch that turns it off and, unless the user opts out
11
+ * (`--allow-autocompact-off` / MNEMOM_AGENT_ALLOW_AUTOCOMPACT_OFF=1), turns it back on for the
12
+ * child only: the env switches are dropped from the child env and a `--settings` layer sets
13
+ * `autoCompactEnabled: true` with both env switches `"0"`. Flag settings outrank every settings
14
+ * file and the global config, and their `env` block outranks the process env (MEASURED against
15
+ * 2.1.301 for each source below). Nothing on disk is changed.
16
+ *
17
+ * Claude Code's gate (2.1.301): `zp(){ if (DISABLE_COMPACT || DISABLE_AUTO_COMPACT) return false;
18
+ * return settings("autoCompactEnabled", true) }`; the env values count as on when they are
19
+ * 1/true/yes/on (trimmed, any case).
20
+ */
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ /** Opt-out env: "1" keeps the user's autocompact-off setting as it is (warning only). */
25
+ export const ALLOW_AUTOCOMPACT_OFF_ENV = "MNEMOM_AGENT_ALLOW_AUTOCOMPACT_OFF";
26
+ /** Claude Code's env switches that turn auto-compaction off (DISABLE_COMPACT also /compact). */
27
+ export const AUTOCOMPACT_OFF_ENV = ["DISABLE_AUTO_COMPACT", "DISABLE_COMPACT"];
28
+ /** The `--settings` layer that turns it back on. */
29
+ export const AUTOCOMPACT_ON_SETTINGS = {
30
+ autoCompactEnabled: true,
31
+ env: { DISABLE_AUTO_COMPACT: "0", DISABLE_COMPACT: "0" },
32
+ };
33
+ /**
34
+ * Claude Code's truthiness for these env values. A settings file's `env` block can hold a JSON
35
+ * boolean: `true` turns it off and `false` clears an off process env (MEASURED, 2.1.301).
36
+ */
37
+ export function envOn(v) {
38
+ if (v === undefined || v === null)
39
+ return false;
40
+ return ["1", "true", "yes", "on"].includes(String(v).trim().toLowerCase());
41
+ }
42
+ function readJsonObject(file) {
43
+ try {
44
+ if (!existsSync(file))
45
+ return null;
46
+ const d = JSON.parse(readFileSync(file, "utf8"));
47
+ return d && typeof d === "object" && !Array.isArray(d) ? d : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Every switch that leaves Claude Code's autocompact off for a launch from `cwd`, in the order
55
+ * Claude Code layers them (later wins): the global config, then the user, project and local
56
+ * settings files for `autoCompactEnabled`; the process env, then each settings file's `env` block
57
+ * for the two env switches. Only the switches still in force are returned (a later layer turning
58
+ * it back on clears an earlier one). Managed (policy) settings are not read — the launcher could
59
+ * not override them anyway. Never throws.
60
+ */
61
+ export function detectAutocompactOff(o) {
62
+ const home = o.home ?? homedir();
63
+ const configDir = o.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
64
+ const globalFile = join(o.env.CLAUDE_CONFIG_DIR || home, ".claude.json");
65
+ const settingsFiles = [
66
+ join(configDir, "settings.json"),
67
+ join(o.cwd, ".claude", "settings.json"),
68
+ join(o.cwd, ".claude", "settings.local.json"),
69
+ ];
70
+ // autoCompactEnabled: the global config, then each settings file (a settings value wins).
71
+ let enabled = null;
72
+ const global = readJsonObject(globalFile);
73
+ if (typeof global?.autoCompactEnabled === "boolean") {
74
+ enabled = { value: global.autoCompactEnabled, where: globalFile };
75
+ }
76
+ const settings = settingsFiles.map((f) => ({ file: f, data: readJsonObject(f) }));
77
+ for (const s of settings) {
78
+ if (typeof s.data?.autoCompactEnabled === "boolean") {
79
+ enabled = { value: s.data.autoCompactEnabled, where: s.file };
80
+ }
81
+ }
82
+ const out = [];
83
+ if (enabled && !enabled.value) {
84
+ out.push({ setting: "autoCompactEnabled: false", where: enabled.where });
85
+ }
86
+ // The env switches: the process env, then each settings file's env block (last one wins).
87
+ for (const key of AUTOCOMPACT_OFF_ENV) {
88
+ let last = o.env[key] !== undefined ? { value: o.env[key], where: "your environment" } : null;
89
+ for (const s of settings) {
90
+ const env = s.data?.env;
91
+ if (env && typeof env === "object" && !Array.isArray(env) && key in env) {
92
+ last = { value: env[key], where: `${s.file} (env)` };
93
+ }
94
+ }
95
+ if (last && envOn(last.value)) {
96
+ out.push({ setting: `${key}=${String(last.value).trim()}`, where: last.where });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ /** True when the user passed their own `--settings` (a second one would replace ours whole). */
102
+ export function hasOwnSettingsFlag(passthrough) {
103
+ return passthrough.some((a) => a === "--settings" || a.startsWith("--settings="));
104
+ }
105
+ /**
106
+ * Decide what the launch does about autocompact. `canPassSettings` is false where the launcher
107
+ * cannot add a `--settings` layer (the user's own `--settings`, the headless RC server); there the
108
+ * env switches are still dropped, but a settings-file switch can only be warned about.
109
+ */
110
+ export function planAutocompactGuard(o) {
111
+ const switches = detectAutocompactOff(o);
112
+ if (switches.length === 0)
113
+ return { switches, override: false, warning: null };
114
+ const named = switches.map((s) => `${s.setting} in ${s.where}`).join("; ");
115
+ const lead = `mnemom agent: Claude Code auto-compact is off (${named}). On a long session that ends in ` +
116
+ `"Prompt is too long" at the context limit, with every turn refused until you run /compact.`;
117
+ if (o.allowOff) {
118
+ return {
119
+ switches,
120
+ override: false,
121
+ warning: `${lead} Leaving it off (--allow-autocompact-off).`,
122
+ };
123
+ }
124
+ const fromFile = switches.some((s) => !s.where.startsWith("your environment"));
125
+ if (fromFile && !o.canPassSettings) {
126
+ return {
127
+ switches,
128
+ override: true,
129
+ warning: `${lead} mnemom could not turn it back on for this launch (your own --settings is in use, ` +
130
+ `or this is a headless server): set "autoCompactEnabled": true there, or turn Auto-compact on in /config.`,
131
+ };
132
+ }
133
+ return {
134
+ switches,
135
+ override: true,
136
+ warning: `${lead} Turning it back on for this session only (pass --allow-autocompact-off to keep it off).`,
137
+ };
138
+ }
139
+ /** Drop the env switches from a child env. Mutates and returns it. */
140
+ export function dropAutocompactOffEnv(childEnv) {
141
+ for (const key of AUTOCOMPACT_OFF_ENV)
142
+ Reflect.deleteProperty(childEnv, key);
143
+ return childEnv;
144
+ }
145
+ /**
146
+ * The child's `--settings` args: our layer merged with the status-bar layer (`extra`), since
147
+ * Claude Code keeps only the LAST `--settings` it is given (MEASURED, 2.1.301) — two would make
148
+ * one of them vanish. Empty when there is nothing to pass.
149
+ */
150
+ export function mergedSettingsArgs(guard, extra) {
151
+ if (!guard && !extra)
152
+ return [];
153
+ return [
154
+ "--settings",
155
+ JSON.stringify({ ...(extra ?? {}), ...(guard ? AUTOCOMPACT_ON_SETTINGS : {}) }),
156
+ ];
157
+ }
@@ -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
  /**
@@ -128,4 +128,5 @@ export declare function statuslineLaunch(o: {
128
128
  }): {
129
129
  args: string[];
130
130
  env: Record<string, string>;
131
+ settings: Record<string, unknown>;
131
132
  } | null;
@@ -339,5 +339,7 @@ export function statuslineLaunch(o) {
339
339
  const env = user ? { [INNER_ENV]: user.command } : {};
340
340
  if (o.measured)
341
341
  env[MEASURED_ENV] = "1";
342
- return { args: ["--settings", JSON.stringify({ statusLine })], env };
342
+ // `settings` is the same layer as `args`, for a caller that merges it with its own (Claude Code
343
+ // keeps only the last --settings it is given).
344
+ return { args: ["--settings", JSON.stringify({ statusLine })], env, settings: { statusLine } };
343
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.28",
3
+ "version": "0.17.30",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {