@mnemom/mnemom 0.17.5 → 0.17.6-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -148,6 +148,47 @@ gateway support: a gateway without it drops both keys (they are echoed back in
148
148
  `x-mnemom-session-config-dropped`) and the session behaves exactly as with the setting off. `--no-compaction-intercept` turns it off for
149
149
  one launch.
150
150
 
151
+ #### Compaction target (`--compact-at`)
152
+
153
+ `--compact-at <n|200k|1m|off>` lets you play with WHEN the gateway pulls this session's own
154
+ compaction point forward, instead of leaving it entirely to Claude Code's normal 1M-window
155
+ schedule. **Unset, `1m`, or `off` all mean no target** — Claude Code runs its normal 1M window
156
+ and the launcher sends nothing new (byte-identical to a launch without this setting at all). A
157
+ token count below 1M (k/m suffixes OK, e.g. `200k` or `350000`, from 50k up to just under 1M)
158
+ arms a gateway-side target for this session: the per-session config header carries
159
+ `GATEWAY_USAGE_REAL_SIZE=on` + `GATEWAY_COMPACTION_INTERCEPT=on` (the two keys a target needs to
160
+ arm — the same pair `--compaction-intercept` turns on) plus
161
+ `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS=<n>`. It never sends `GATEWAY_COMPACTION_REFOLD` — that
162
+ costlier opt-in stays separate. It never touches Claude Code's own autocompact env or window.
163
+
164
+ A target only works **below Claude Code's own compaction point for the window the session runs
165
+ on**: 967k on a 1M window, 167k on a 200k window (Haiku, models with no 1M window, or
166
+ `CLAUDE_CODE_DISABLE_1M_CONTEXT`). At or above that point the gateway ignores it and says
167
+ nothing back, so the launcher prints a warning at launch (and in `--dry-run`) when it can tell
168
+ the target will be ignored, and a note when it can't tell the window (a passthrough `-- --model`,
169
+ a resume, or a CLI other than Claude Code).
170
+
171
+ **Arming a target needs the same opt-in as `--compaction-intercept`** (`mnemom experimental
172
+ enable compaction-intercept`) — a target arms the identical pair of gateway keys, so it needs
173
+ the identical gate; the flag itself stays visible in `--help` (it's the knob you asked for), only
174
+ actually _arming_ a target is gated. Passing `--no-compaction-intercept` together with an armed
175
+ `--compact-at` is refused outright (they contradict: the target cannot arm without the intercept
176
+ you just turned off).
177
+
178
+ Set it per launch, as a standing default, or in the environment (flag > env > `launcher.toml` >
179
+ default):
180
+
181
+ ```bash
182
+ mnemom experimental enable compaction-intercept # one-time opt-in
183
+ mnemom agent my-task --compact-at 200k # this launch only
184
+ mnemom agent config set compact_at 200k # persists in ~/.mnemom/launcher.toml
185
+ export MNEMOM_COMPACT_AT=200k # for every launch in this shell
186
+ ```
187
+
188
+ `--dry-run` prints the resolved target (`compaction: gateway at 200k` or `compaction: Claude
189
+ Code default (1M)`) and, when a target is set, the decoded session-config keys; the session
190
+ ledger records the effective value.
191
+
151
192
  ## How It Works
152
193
 
153
194
  ```
@@ -199,6 +199,9 @@ function deleteKey(cfg, key) {
199
199
  case "context_profile":
200
200
  delete cfg.contextProfile;
201
201
  break;
202
+ case "compact_at":
203
+ delete cfg.compactAt;
204
+ break;
202
205
  case "auth":
203
206
  delete cfg.auth;
204
207
  break;
@@ -70,6 +70,14 @@ export interface AgentOptions {
70
70
  * `balanced`. `--no-context` is `off`; combined with balanced|aggressive the launch refuses.
71
71
  */
72
72
  contextProfile?: ContextProfile;
73
+ /**
74
+ * The gateway compaction target this launch runs under (`--compact-at`), already parsed by
75
+ * commander's argParser: `undefined` = the flag was not given (fall back to
76
+ * `MNEMOM_COMPACT_AT` / default), `null` = an explicit `1m`/`off` (no target, same as
77
+ * default), a number = tokens. Below 1M it arms `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS` +
78
+ * the #2216 intercept/real-size keys for this session only; unset/null sends nothing new.
79
+ */
80
+ compactAt?: number | null;
73
81
  /** context-hint seed default: commander `--no-context-hint` sets this false. */
74
82
  contextHint?: boolean;
75
83
  /** Attach the Slack channel server (`--slack`; lib/slack-launch.ts). */
@@ -189,6 +197,8 @@ export declare function recordSessionLaunch(ctx: {
189
197
  ccSessionId?: string | null;
190
198
  /** The resolved context profile this launch runs under. */
191
199
  contextProfile: ContextProfile;
200
+ /** The resolved compaction target this launch runs under (`resolveCompactAt`). */
201
+ compactAt: number | null;
192
202
  }): Promise<void>;
193
203
  /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
194
204
  export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
@@ -344,6 +354,8 @@ export declare function printPlan(ctx: {
344
354
  cli: ResolvedCli;
345
355
  /** The resolved context profile (`resolveContextProfile`). */
346
356
  contextProfile: ContextProfile;
357
+ /** The resolved compaction target (`resolveCompactAt`); `null` = no target (default). */
358
+ compactAt: number | null;
347
359
  /** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
348
360
  sessionConfig: Record<string, string>;
349
361
  /** The experimental features on for this launch (the header's `experimental` array). */
@@ -31,6 +31,7 @@ import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname, userInfo } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
+ import { statuslineLaunch } from "../lib/statusline.js";
34
35
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
35
36
  import { createApiKey, getAgentByName } from "../lib/api.js";
36
37
  import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
@@ -44,7 +45,7 @@ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, }
44
45
  import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
45
46
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
46
47
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
47
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
48
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
48
49
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
49
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
50
51
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -64,6 +65,37 @@ function sayContextProfile(profile) {
64
65
  say("mnemom agent: context profile aggressive — the gateway folds earlier and keeps a tighter working set.");
65
66
  }
66
67
  }
68
+ /**
69
+ * The one-line launch-banner note for the resolved compaction target (`resolveCompactAt`):
70
+ * printed on EVERY launch (dry-run and real), the same way `door`/`conversation` are — there is
71
+ * always a value (default or an explicit target), unlike the context-profile note above which
72
+ * is silent for the common case.
73
+ */
74
+ function sayCompactAt(compactAt, window) {
75
+ say(`mnemom agent: compaction: ${describeCompactAt(compactAt)}`);
76
+ // A target at/above the window's own compaction point is ignored by the gateway, which never
77
+ // says so back — tell the user here instead of reporting it as if it took effect.
78
+ const warning = compactAtWindowWarning(compactAt, window);
79
+ if (warning)
80
+ say(warning);
81
+ }
82
+ /**
83
+ * The launch's window (resolveWindowModel) — Claude Code only: it is the adapter that reads the
84
+ * window off the model id. `null` for any other CLI (or none resolved).
85
+ */
86
+ function windowModelFor(options, cli) {
87
+ return cli?.name === "claude"
88
+ ? resolveWindowModel({
89
+ explicit: options.model,
90
+ passthrough: options.passthrough ?? [],
91
+ env: process.env,
92
+ settings: () => readCcSettings({
93
+ cwd: process.cwd(),
94
+ configDir: (process.env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"),
95
+ }),
96
+ })
97
+ : null;
98
+ }
67
99
  /** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
68
100
  export async function agentCommand(scenarioArg, options = {}) {
69
101
  // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
@@ -158,16 +190,27 @@ export async function agentCommand(scenarioArg, options = {}) {
158
190
  compactionIntercept: options.compactionIntercept,
159
191
  env,
160
192
  });
193
+ // Compaction target: `--compact-at` (undefined ⇒ MNEMOM_COMPACT_AT, which
194
+ // applyConfigToEnv already folded the `compact_at` setting under), else null (no target —
195
+ // Claude Code's own 1M window, nothing new sent). See lib/agent.ts for the full contract.
196
+ const compactAt = resolveCompactAt({ compactAt: options.compactAt, env });
197
+ // An explicit --no-compaction-intercept (compactionIntercept === false, never the mere
198
+ // absence of --compaction-intercept) conflicts with an armed --compact-at: the target
199
+ // cannot arm without the intercept it just turned off. Refuse rather than silently pick
200
+ // a side.
201
+ assertCompactAtCompatible({ compactionIntercept: options.compactionIntercept, compactAt });
161
202
  const experimental = options.experimental ?? [];
162
203
  const sessionConfig = buildSessionConfig({
163
204
  profile: contextProfile,
164
205
  contextHandoff,
165
206
  compactionIntercept,
207
+ compactAt,
166
208
  });
167
209
  const sessionConfigB64 = buildSessionConfigB64({
168
210
  profile: contextProfile,
169
211
  contextHandoff,
170
212
  compactionIntercept,
213
+ compactAt,
171
214
  experimental,
172
215
  });
173
216
  const expNotice = experimentalNotice(experimental);
@@ -211,6 +254,7 @@ export async function agentCommand(scenarioArg, options = {}) {
211
254
  yolo,
212
255
  cli: planCli,
213
256
  contextProfile,
257
+ compactAt,
214
258
  sessionConfig,
215
259
  experimental,
216
260
  });
@@ -226,6 +270,7 @@ export async function agentCommand(scenarioArg, options = {}) {
226
270
  say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
227
271
  }
228
272
  sayContextProfile(contextProfile);
273
+ sayCompactAt(compactAt, launchWindow(windowModelFor(options, cli)));
229
274
  if (contextHandoff) {
230
275
  say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
231
276
  }
@@ -388,17 +433,7 @@ export async function agentCommand(scenarioArg, options = {}) {
388
433
  // window suffix and supports 1M (resolveWindowModel); an explicit choice — the
389
434
  // CLAUDE_CODE_DISABLE_1M_CONTEXT opt-out, a passthrough `-- --model`, a suffixed id — is kept.
390
435
  // Claude Code only: it is the adapter that reads the window off the model id.
391
- const win = cli.name === "claude"
392
- ? resolveWindowModel({
393
- explicit: options.model,
394
- passthrough: options.passthrough ?? [],
395
- env: process.env,
396
- settings: () => readCcSettings({
397
- cwd: process.cwd(),
398
- configDir: (process.env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"),
399
- }),
400
- })
401
- : null;
436
+ const win = windowModelFor(options, cli);
402
437
  const launchOptions = win?.reason === "one-m" ? { ...options, model: win.model } : options;
403
438
  if (win?.reason === "one-m") {
404
439
  say(`mnemom agent: model ${win.from} → ${win.model} (1M context window; set ${DISABLE_1M_ENV}=1, or pass the model as \`-- --model <id>\`, to keep the standard window).`);
@@ -417,12 +452,14 @@ export async function agentCommand(scenarioArg, options = {}) {
417
452
  goalMode,
418
453
  ccSessionId: cc.id,
419
454
  contextProfile,
455
+ compactAt,
420
456
  });
421
457
  logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
422
458
  if (expNotice)
423
459
  say(expNotice);
424
460
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
425
461
  sayContextProfile(contextProfile);
462
+ sayCompactAt(compactAt, launchWindow(win));
426
463
  if (compactionIntercept)
427
464
  say(COMPACTION_INTERCEPT_NOTE);
428
465
  // ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
@@ -650,6 +687,7 @@ export async function recordSessionLaunch(ctx) {
650
687
  contract: toLedgerContract(ctx.envelope?.contract ?? null),
651
688
  goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
652
689
  context_profile: ctx.contextProfile,
690
+ compact_at: ctx.compactAt,
653
691
  cc_session_id: ctx.ccSessionId ?? null,
654
692
  ...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
655
693
  }, {
@@ -978,6 +1016,21 @@ function launchTerminal(ctx) {
978
1016
  applyGatewayHintHeaders(childEnv);
979
1017
  if (ctx.options.slack)
980
1018
  args.push(...slackChannelArgs(sessionName));
1019
+ // The status-bar savings counter (lib/statusline.ts). Claude Code only; the
1020
+ // user's own statusLine keeps running in front of it.
1021
+ if (ctx.cli.name === "claude") {
1022
+ const sl = statuslineLaunch({
1023
+ passthrough: ctx.options.passthrough ?? [],
1024
+ env: childEnv,
1025
+ cwd: process.cwd(),
1026
+ node: process.execPath,
1027
+ bin: join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "statusline-bin.js"),
1028
+ });
1029
+ if (sl) {
1030
+ args.push(...sl.args);
1031
+ Object.assign(childEnv, sl.env);
1032
+ }
1033
+ }
981
1034
  if (ctx.contextHint) {
982
1035
  seedContextHintFlag(childEnv);
983
1036
  childEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
@@ -1511,6 +1564,7 @@ export function printPlan(ctx) {
1511
1564
  L.push(` conversation: ${ctx.conversationId}`);
1512
1565
  L.push(` yolo: ${ctx.yolo ? "on (--dangerously-skip-permissions)" : "off"}`);
1513
1566
  L.push(` context: ${ctx.contextProfile}`);
1567
+ L.push(` compaction: ${describeCompactAt(ctx.compactAt)}`);
1514
1568
  const overlay = Object.entries(ctx.sessionConfig);
1515
1569
  if (overlay.length > 0) {
1516
1570
  L.push(` session config (${SESSION_CONFIG_HEADER}):`);
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledE
33
33
  import { loadExperimentalState } from "./lib/cli-config.js";
34
34
  import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
35
35
  import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
36
- import { parseContextProfile, resolveImplicit } from "./lib/agent.js";
36
+ import { parseCompactAt, parseContextProfile, resolveImplicit, } from "./lib/agent.js";
37
37
  import { wrapCommand } from "./commands/wrap.js";
38
38
  import { onboardCommand } from "./commands/onboard.js";
39
39
  import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
@@ -124,10 +124,24 @@ const collectRepeatable = (value, previous) => {
124
124
  previous.push(value);
125
125
  return previous;
126
126
  };
127
+ /**
128
+ * `mnemom agent help [verb]`: a visible commander sub-verb (`list`, `sessions`, …) prints
129
+ * its own help; anything else — no verb, or an in-action verb like `setup`/`doctor`/
130
+ * `config` that the agent help itself documents — prints the agent help.
131
+ */
132
+ function printAgentHelp(verb) {
133
+ const sub = verb
134
+ ? agentCmd
135
+ .createHelp()
136
+ .visibleCommands(agentCmd)
137
+ .find((c) => c.name() === verb || c.aliases().includes(verb))
138
+ : undefined;
139
+ (sub ?? agentCmd).outputHelp();
140
+ }
127
141
  const agentCmd = program
128
142
  .command("agent [session] [passthrough...]")
129
143
  .description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name and an optional goal. " +
130
- "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state.")
144
+ "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
131
145
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
132
146
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
133
147
  .option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
@@ -170,6 +184,22 @@ const agentCmd = program
170
184
  catch (e) {
171
185
  throw new InvalidArgumentError(e.message);
172
186
  }
187
+ }))
188
+ .addOption(new Option("--compact-at <n|200k|1m|off>", 'Play with WHEN the gateway pulls this session\'s own compaction point forward, instead of leaving it to Claude Code\'s normal 1M-window schedule. A token count below 1M (k/m suffix OK, e.g. "200k") arms the gateway\'s compaction target for this session; unset, "1m" or "off" all mean no target — Claude Code runs its normal 1M window and nothing new is sent. Arming a target needs `mnemom experimental enable compaction-intercept` (the same opt-in --compaction-intercept uses — a target arms the identical gateway keys) and is refused if you also pass --no-compaction-intercept. Never touches Claude Code\'s own autocompact env/window. Also: config key compact_at, env MNEMOM_COMPACT_AT.').argParser((v) => {
189
+ // Validate with the same parser the other two surfaces (MNEMOM_COMPACT_AT, launcher.toml
190
+ // compact_at) use, but return the validated RAW string rather than parseCompactAt's
191
+ // `number | null` — commander's own option-value handling coerces a parseArg return of
192
+ // `null` to `''` for a mandatory-argument option (Command.js's `handleOptionValue`: any
193
+ // `val == null` after parseArg falls through to the "not normal" `val = ''` branch), which
194
+ // would turn "1m"/"off" into a lone empty string instead of "no target". The real
195
+ // `number | null` conversion happens once, right after commander hands back `opts.compactAt`.
196
+ try {
197
+ parseCompactAt(v, "--compact-at");
198
+ }
199
+ catch (e) {
200
+ throw new InvalidArgumentError(e.message);
201
+ }
202
+ return v;
173
203
  }))
174
204
  .addOption(new Option("--context-handoff", "EXPERIMENTAL (`mnemom experimental enable context-handoff`): register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session").hideHelp(hiddenUnless("context-handoff")))
175
205
  .option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
@@ -179,6 +209,13 @@ const agentCmd = program
179
209
  .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")
180
210
  .action(async (scenario, passthrough, opts) => {
181
211
  try {
212
+ // `help` is reserved: `mnemom agent help [verb]` prints help and never launches
213
+ // (nor redeems an --invite). Without this, `help` was taken as a session name and
214
+ // the verb passed through to Claude Code as the opening prompt.
215
+ if (scenario === "help") {
216
+ printAgentHelp(passthrough[0]);
217
+ return;
218
+ }
182
219
  // Invitation redemption — `mnemom agent invite <token>` (sub-verb) or
183
220
  // `mnemom agent --invite <token> [session]` — composes with the launch in
184
221
  // runAgentInvocation (commands/code-invite.ts, unit-tested); everything below
@@ -195,6 +232,10 @@ const agentCmd = program
195
232
  }
196
233
  async function launchAgent(scenario) {
197
234
  const experimental = loadExperimentalState().enabled;
235
+ // The one place the flag's raw string becomes `number | null` (see the argParser's
236
+ // comment for why commander can't carry `null` through parseArg directly). Already
237
+ // validated by the argParser, so this never throws.
238
+ const compactAtFlag = opts.compactAt !== undefined ? parseCompactAt(opts.compactAt, "--compact-at") : undefined;
198
239
  // Sub-verb: `mnemom agent config …` manages persistent settings and never
199
240
  // launches. (A scenario literally named "config" is reserved for this.)
200
241
  if (scenario === "config") {
@@ -223,6 +264,14 @@ const agentCmd = program
223
264
  id: "compaction-intercept",
224
265
  used: opts.compactionIntercept === true,
225
266
  },
267
+ // --compact-at arms the SAME two gateway keys --compaction-intercept does
268
+ // (buildSessionConfig ORs them) — it needs the identical opt-in, not a free pass
269
+ // around it. `null`/unset ("1m"/"off"/no flag) is not a target, so it never gates.
270
+ {
271
+ what: "--compact-at",
272
+ id: "compaction-intercept",
273
+ used: typeof compactAtFlag === "number",
274
+ },
226
275
  ], experimental);
227
276
  if (opts.gateway)
228
277
  process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
@@ -306,6 +355,10 @@ const agentCmd = program
306
355
  slack: opts.slack ?? cfg.slackListen,
307
356
  context: opts.context,
308
357
  contextProfile: opts.contextProfile,
358
+ // Same gate: off ⇒ an explicit null (no target), so MNEMOM_COMPACT_AT cannot arm it —
359
+ // an armed --compact-at was already refused above, and ignoredSettingLines told the
360
+ // user the env value is ignored, so this is what makes that true.
361
+ compactAt: experimental.includes("compaction-intercept") ? compactAtFlag : null,
309
362
  // Off ⇒ an explicit false, so MNEMOM_AGENT_CONTEXT_HANDOFF cannot turn it on.
310
363
  contextHandoff: experimental.includes("context-handoff") ? opts.contextHandoff : false,
311
364
  // Same gate: off ⇒ an explicit false, so MNEMOM_AGENT_COMPACTION_INTERCEPT cannot turn it on.
@@ -81,6 +81,14 @@ export interface SessionRecord {
81
81
  * before profiles existed omit it (still `v: 1`).
82
82
  */
83
83
  context_profile?: ContextProfile;
84
+ /**
85
+ * The resolved gateway compaction target (`--compact-at` / `compact_at` /
86
+ * `MNEMOM_COMPACT_AT`): `null` means no target (Claude Code's own 1M window; the default,
87
+ * and what `1m`/`off` also resolve to), a number is the token count sent as
88
+ * `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS`. Optional: records written before this setting
89
+ * existed omit it (still `v: 1`).
90
+ */
91
+ compact_at?: number | null;
84
92
  /**
85
93
  * The experimental features on for the launch (`mnemom experimental`). Optional and only
86
94
  * written when at least one was on; older records omit it (still `v: 1`).
@@ -322,10 +322,89 @@ export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
322
322
  * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
323
323
  * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
324
324
  * gateway's own fold digest (no model call), so the compaction never blocks the user.
325
- * Sent together, value `"on"`, ONLY when the setting is on (see `resolveCompactionIntercept`).
325
+ * Sent together, value `"on"`, when the `compaction_intercept` setting is on
326
+ * (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
327
+ * target cannot be honored without both.
326
328
  */
327
329
  export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
328
330
  export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
331
+ export declare const GATEWAY_COMPACTION_UPLOAD_TARGET_KEY = "GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS";
332
+ /** The lowest token count `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` will accept. */
333
+ export declare const COMPACT_AT_MIN_TOKENS = 50000;
334
+ /**
335
+ * The parser's ceiling (exclusive): the largest window Claude Code runs. It is NOT where a target
336
+ * stops working — the gateway ignores a target at or above the compaction point of the window
337
+ * the session actually runs on (COMPACT_AT_TRIGGER_1M / COMPACT_AT_TRIGGER_200K, below), which
338
+ * `compactAtWindowWarning` tells the user about at launch.
339
+ */
340
+ export declare const COMPACT_AT_MAX_TOKENS = 1000000;
341
+ /**
342
+ * Claude Code's own compaction point on a 1M and on a 200k window — the gateway's
343
+ * `ALIGN_T_1M` / `ALIGN_T_200K` (gateway/src/compaction-align.ts). The gateway honours an upload
344
+ * target only BELOW the point of the window the request runs on (`target_over_threshold`
345
+ * otherwise), and says nothing back to the client when it ignores one.
346
+ */
347
+ export declare const COMPACT_AT_TRIGGER_1M = 967000;
348
+ export declare const COMPACT_AT_TRIGGER_200K = 167000;
349
+ /** The context window a launch runs on, as far as the launcher can tell before spawning. */
350
+ export type LaunchWindow = "1m" | "200k" | "unknown";
351
+ /**
352
+ * `resolveWindowModel`'s answer as a window. `one-m` and a model already suffixed `[1m]` are
353
+ * 1M; the `CLAUDE_CODE_DISABLE_1M_CONTEXT` opt-out and a model with no 1M window are 200k (the
354
+ * request then carries no 1M beta, so the gateway uses its 200k point too). A passthrough
355
+ * `-- --model`, a resume, Claude Code's own default, any other suffix, and a non-Claude-Code CLI
356
+ * (`null`) are unknown.
357
+ */
358
+ export declare function launchWindow(win: WindowModel | null): LaunchWindow;
359
+ /**
360
+ * The launch warning for a target the gateway will (or may) ignore because it is at or above
361
+ * the compaction point of the window this launch runs on; `null` when the target can take
362
+ * effect (or there is no target). On an unknown window a target in `[167k, 967k)` works only
363
+ * if the session lands on a 1M window, so the line says so instead of staying silent.
364
+ */
365
+ export declare function compactAtWindowWarning(compactAt: number | null, window: LaunchWindow): string | null;
366
+ /**
367
+ * Parse a `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` value. `source` names the
368
+ * surface in the error so a customer knows which setting to fix (same convention as
369
+ * `parseContextProfile`).
370
+ *
371
+ * - "" (unset), "1m", "off" (trimmed, case-insensitive) → `null`: no gateway target —
372
+ * Claude Code runs its normal 1M window and nothing new is sent.
373
+ * - a bare integer, or one with a `k`/`m` suffix (`200k` → 200000, `0.5m` → 500000) →
374
+ * that many tokens, when it is in `[COMPACT_AT_MIN_TOKENS, COMPACT_AT_MAX_TOKENS)`.
375
+ * - anything else, or a number outside that range (use `1m`/`off` for the ceiling and
376
+ * above, never a bare `1000000`) → throws with a human-readable message.
377
+ */
378
+ export declare function parseCompactAt(raw: string, source: string): number | null;
379
+ /**
380
+ * Which compaction target this launch runs under. Precedence, highest wins:
381
+ * 1. `opts.compactAt` — an already-parsed `--compact-at` flag value (`undefined` when the
382
+ * flag was not given at all; `null` is a valid, explicit "no target" from `1m`/`off`).
383
+ * 2. `MNEMOM_COMPACT_AT` (which `applyConfigToEnv` sets from the `compact_at` launcher.toml
384
+ * key, UNDER any real env var — so an operator's own env always wins over the file).
385
+ * 3. `null` — the default: no target, nothing new sent.
386
+ */
387
+ export declare function resolveCompactAt(opts: {
388
+ compactAt?: number | null;
389
+ env?: NodeJS.ProcessEnv;
390
+ }): number | null;
391
+ /**
392
+ * `--no-compaction-intercept` is a deliberate, documented "keep the intercept off for this
393
+ * launch no matter what toml/env says" signal. A compaction target needs that SAME intercept
394
+ * to arm (`buildSessionConfig` ORs them), so pairing an explicit `--no-compaction-intercept`
395
+ * with an armed `--compact-at` is a contradiction — refuse rather than silently pick a side
396
+ * (either silently re-arming the intercept the user just turned off, or silently sending an
397
+ * inert upload-target the gateway can never act on). `compactionIntercept` here is the raw,
398
+ * pre-`resolveCompactionIntercept` flag value: `false` only when `--no-compaction-intercept`
399
+ * was explicitly passed (or the experimental gate forced it, which `compactAt` cannot reach
400
+ * past either — see index.ts's `assertEnabled`), never the mere absence of `--compaction-intercept`.
401
+ */
402
+ export declare function assertCompactAtCompatible(opts: {
403
+ compactionIntercept?: boolean;
404
+ compactAt: number | null;
405
+ }): void;
406
+ /** The launch-banner line for the resolved compaction target (always printed). */
407
+ export declare function describeCompactAt(compactAt: number | null): string;
329
408
  /**
330
409
  * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
331
410
  * compaction`, `x-claude-code-compaction: auto | manual | reactive`,
@@ -484,14 +563,18 @@ export declare function resolveContextProfile(opts: {
484
563
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
485
564
  * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
486
565
  * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
487
- * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the compaction
488
- * intercept is ON. Empty for `balanced` with both off — the common case, where no header is sent at all.
489
- * Exported so `--dry-run` can list exactly what the header carries.
566
+ * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
567
+ * `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
568
+ * cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
569
+ * when a target is set. Empty for `balanced` with everything off — the common case, where no
570
+ * header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
490
571
  */
491
572
  export declare function buildSessionConfig(opts: {
492
573
  profile: ContextProfile;
493
574
  contextHandoff: boolean;
494
575
  compactionIntercept?: boolean;
576
+ /** `resolveCompactAt` output: `null` (or absent) ⇒ no target, send nothing new. */
577
+ compactAt?: number | null;
495
578
  }): Record<string, string>;
496
579
  /**
497
580
  * The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
@@ -502,6 +585,7 @@ export declare function buildSessionConfigPayload(opts: {
502
585
  profile: ContextProfile;
503
586
  contextHandoff: boolean;
504
587
  compactionIntercept?: boolean;
588
+ compactAt?: number | null;
505
589
  experimental?: readonly string[];
506
590
  }): {
507
591
  v: 1;
@@ -510,12 +594,15 @@ export declare function buildSessionConfigPayload(opts: {
510
594
  } | undefined;
511
595
  /**
512
596
  * The `x-mnemom-session-config` value for this launch, or `undefined` when there is
513
- * nothing to override (the gateway's own defaults stand — `balanced` with handoff off,
514
- * byte-identical to a launch before profiles existed). Two independent overlays:
597
+ * nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
598
+ * byte-identical to a launch before profiles existed). Independent overlays:
515
599
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
516
600
  * `aggressive` → the three fold-tuning keys, `balanced` → nothing;
517
601
  * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
518
602
  * `"on"` (the `compaction_intercept` setting);
603
+ * - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
604
+ * `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`; unset, `1m` or `off` → nothing beyond
605
+ * what `compactionIntercept` already sent (Claude Code's own 1M window, untouched);
519
606
  * - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
520
607
  * setting) — the SAME header the `handoff` MCP tool's own call to
521
608
  * `/mnemom/context/handoff` must also send, since the gateway resolves the flag
@@ -526,6 +613,7 @@ export declare function buildSessionConfigB64(opts: {
526
613
  profile: ContextProfile;
527
614
  contextHandoff: boolean;
528
615
  compactionIntercept?: boolean;
616
+ compactAt?: number | null;
529
617
  experimental?: readonly string[];
530
618
  }): string | undefined;
531
619
  /**