@mnemom/mnemom 0.17.7-next.0 → 0.17.7-next.2

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
@@ -87,20 +87,32 @@ or `launch` in `launcher.toml`) also skips the Remote Control capability probe
87
87
  #### Context profile: how the gateway folds a session's context
88
88
 
89
89
  `--context-profile <off|balanced|aggressive|adaptive>` picks how the Mnemom gateway folds a
90
- session's context. Each profile serves a different set of use cases:
91
-
92
- - **`balanced`** (the default) keeps more of the earlier work verbatim and folds later —
93
- for long sessions that revisit earlier files and decisions.
90
+ session's context. **Unset is the default:** the launch sends no fold setting at all and the
91
+ gateway cell's own default decides (the launch note reads `context: cell default`). A profile
92
+ you choose is sent in full, so it means the same thing on every cell whatever that cell's
93
+ default is. Each profile serves a different set of use cases:
94
+
95
+ - **`balanced`** keeps more of the earlier work verbatim and folds later — for long sessions
96
+ that revisit earlier files and decisions. It sends `CONTEXT_RUNTIME_SIZE_POLICY=off`, so the
97
+ cell's own fold budgets apply at every size, even on a cell whose default is `adaptive`.
94
98
  - **`aggressive`** folds earlier and keeps a tighter working set — for cost-sensitive
95
- sessions and short, focused tasks.
96
- - **`adaptive`** (opt-in) lets the gateway size the fold to the conversation's original
97
- context: no fold below 200k tokens (folding a small conversation costs more than it
98
- saves), the `aggressive` budgets from 200k to 1M, and `balanced` above 1M. It sends
99
+ sessions and short, focused tasks. It sends its three budget keys plus
100
+ `CONTEXT_RUNTIME_SIZE_POLICY=off`.
101
+ - **`adaptive`** lets the gateway size the fold to the conversation's original context: no
102
+ fold below 200k tokens (folding a small conversation costs more than it saves), the
103
+ `aggressive` budgets from 200k to 1M, and `balanced` above 1M. It sends
99
104
  `CONTEXT_RUNTIME_SIZE_POLICY=banded` in the per-session config; the band thresholds are the
100
105
  gateway's defaults.
101
- - **`off`** leaves compaction to the coding agent.
106
+ - **`off`** leaves compaction to the coding agent. It sends `CONTEXT_RUNTIME_ENABLED=false`
107
+ plus `GATEWAY_USAGE_REAL_SIZE=off` and `GATEWAY_COMPACTION_INTERCEPT=off`, so a session with
108
+ no fold never has its compaction intercepted or its reported context size rewritten, even on
109
+ a cell whose default turns the intercept on. `--compaction-intercept` or `--compact-at <n>` on
110
+ the same launch is refused; a standing `compaction_intercept` / `compact_at` setting is left
111
+ out for that session (the launch note says so).
102
112
 
103
- Set it per launch, as a standing default, or in the environment (flag > env > `launcher.toml`):
113
+ Choose it for one launch, as a standing default, or in the environment (flag > env >
114
+ `launcher.toml`). Only `mnemom agent config set` (or editing `launcher.toml`) saves a default;
115
+ a flag applies to that launch alone and is never written anywhere later launches read:
104
116
 
105
117
  ```bash
106
118
  mnemom agent my-task --context-profile aggressive # this launch only
@@ -109,11 +121,16 @@ export MNEMOM_AGENT_CONTEXT_PROFILE=aggressive # for every launch in this s
109
121
  mnemom agent my-task --context-profile adaptive # size-banded folding, this launch only
110
122
  ```
111
123
 
124
+ A launch that resumes a Claude Code session (`-- --resume <id>`, or the implicit-goal relaunch)
125
+ keeps the profile that session ran under, ahead of the standing default; a `--context-profile`
126
+ flag on the resuming launch still wins.
127
+
112
128
  `--no-context` (or `context = false` in `launcher.toml`) is the same as `off`. Pass one or the
113
129
  other: `--no-context` together with `--context-profile balanced`, `aggressive` or `adaptive` is refused. A
114
130
  `--context-profile` flag overrides a standing `context = false` in `launcher.toml`. `--dry-run`
115
- prints the resolved profile and, when the launch sends the per-session config header, the
116
- decoded key/value list; `mnemom agent show` and the session ledger record the profile.
131
+ prints the resolved profile (`cell default` when none is chosen) and, when the launch sends the
132
+ per-session config header, the decoded key/value list; `mnemom agent show` and the session
133
+ ledger record a chosen profile.
117
134
 
118
135
  #### Context handoff (experimental, off by default)
119
136
 
@@ -179,7 +196,8 @@ enable compaction-intercept`) — a target arms the identical pair of gateway ke
179
196
  the identical gate; the flag itself stays visible in `--help` (it's the knob you asked for), only
180
197
  actually _arming_ a target is gated. Passing `--no-compaction-intercept` together with an armed
181
198
  `--compact-at` is refused outright (they contradict: the target cannot arm without the intercept
182
- you just turned off).
199
+ you just turned off). So is an armed `--compact-at` (or `--compaction-intercept`) with context
200
+ profile `off`: with the fold off there is no fold to answer the compaction from.
183
201
 
184
202
  Set it per launch, as a standing default, or in the environment (flag > env > `launcher.toml` >
185
203
  default):
@@ -66,8 +66,9 @@ export interface AgentOptions {
66
66
  context?: boolean;
67
67
  /**
68
68
  * How the gateway folds this session's context (`--context-profile`): `off`, `balanced`,
69
- * `aggressive` or `adaptive`. Undefined ⇒ MNEMOM_AGENT_CONTEXT_PROFILE (the `context_profile` setting), else
70
- * `balanced`. `--no-context` is `off`; combined with balanced|aggressive|adaptive the launch refuses.
69
+ * `aggressive` or `adaptive`. Undefined ⇒ the resumed session's own profile, else
70
+ * MNEMOM_AGENT_CONTEXT_PROFILE (the `context_profile` setting), else none — the cell default,
71
+ * nothing sent. `--no-context` is `off`; combined with balanced|aggressive|adaptive the launch refuses.
71
72
  */
72
73
  contextProfile?: ContextProfile;
73
74
  /**
@@ -195,8 +196,8 @@ export declare function recordSessionLaunch(ctx: {
195
196
  goalMode: string | null;
196
197
  /** The Claude Code session id (pickCcSessionId); null/absent when Claude Code picks it. */
197
198
  ccSessionId?: string | null;
198
- /** The resolved context profile this launch runs under. */
199
- contextProfile: ContextProfile;
199
+ /** The resolved context profile this launch runs under; `null` = none chosen (cell default). */
200
+ contextProfile: ContextProfile | null;
200
201
  /** The resolved compaction target this launch runs under (`resolveCompactAt`). */
201
202
  compactAt: number | null;
202
203
  }): Promise<void>;
@@ -374,8 +375,8 @@ export declare function printPlan(ctx: {
374
375
  goalMode: string | null;
375
376
  yolo: boolean;
376
377
  cli: ResolvedCli;
377
- /** The resolved context profile (`resolveContextProfile`). */
378
- contextProfile: ContextProfile;
378
+ /** The resolved context profile (`resolveContextProfile`); `null` = none chosen (cell default). */
379
+ contextProfile: ContextProfile | null;
379
380
  /** The resolved compaction target (`resolveCompactAt`); `null` = no target (default). */
380
381
  compactAt: number | null;
381
382
  /** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
@@ -35,7 +35,7 @@ import { statuslineLaunch } from "../lib/statusline.js";
35
35
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
36
36
  import { apiAcceptsMnemomKey, createOrgApiKey, getAgentByName, listOwnApiKeysForCheck, revokeApiKey, } from "../lib/api.js";
37
37
  import { checkMnemomKey, MNEMOM_KEY_ENV, readLocalMnemomKey, readStoredMnemomKey, resolveAgentAuth, resolveLaunchMnemomKey, storePatchForMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
38
- import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
38
+ import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
39
39
  import { getActiveOrg, loadCliConfig } from "../lib/cli-config.js";
40
40
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
41
41
  import { experimentalNotice } from "../lib/experimental.js";
@@ -45,7 +45,7 @@ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, }
45
45
  import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
46
46
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
47
47
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
48
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_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, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_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";
49
49
  import { LAUNCHER_STORE_PATH, mergeStore, readStore } from "../lib/launcher-store.js";
50
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
51
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -53,14 +53,34 @@ const CODE_STORE = LAUNCHER_STORE_PATH;
53
53
  const say = (line = "") => console.error(line);
54
54
  /** The one-line launch note when the compaction intercept is ON. */
55
55
  const COMPACTION_INTERCEPT_NOTE = "mnemom agent: compaction intercept ON (experimental) — requested from the gateway (GATEWAY_USAGE_REAL_SIZE, GATEWAY_COMPACTION_INTERCEPT); a gateway without the companion support drops both keys and the session runs as before.";
56
+ const OFF_DROPS_COMPACTION_NOTE = "mnemom agent: compaction intercept off for this session — context profile off leaves compaction to the coding agent, so the standing compaction_intercept / compact_at setting is not sent.";
56
57
  /**
57
- * The one-line launch note for a non-default context profile (`balanced`, the default, is
58
- * silent — nothing is sent). `off` keeps the wording `--no-context` has always printed.
58
+ * The compaction notes for a resolved launch. Context profile `off` sends the intercept off and
59
+ * no target (`buildSessionConfig`), so a standing intercept or target is reported as dropped
60
+ * rather than as ON.
61
+ */
62
+ function sayCompaction(profile, compactionIntercept, compactAt, window) {
63
+ const off = profile === "off";
64
+ sayCompactAt(off ? null : compactAt, window);
65
+ if (off && (compactionIntercept || compactAt !== null))
66
+ say(OFF_DROPS_COMPACTION_NOTE);
67
+ else if (compactionIntercept)
68
+ say(COMPACTION_INTERCEPT_NOTE);
69
+ }
70
+ /**
71
+ * The one-line launch note for the context profile: `null` (none chosen — nothing is sent) names
72
+ * the cell default. `off` keeps the wording `--no-context` has always printed.
59
73
  */
60
74
  function sayContextProfile(profile) {
61
- if (profile === "off") {
75
+ if (profile === null) {
76
+ say("mnemom agent: context: cell default");
77
+ }
78
+ else if (profile === "off") {
62
79
  say("mnemom agent: context folding OFF for this session (--no-context or context = false).");
63
80
  }
81
+ else if (profile === "balanced") {
82
+ say("mnemom agent: context profile balanced — the gateway keeps more of the earlier work verbatim and folds later.");
83
+ }
64
84
  else if (profile === "aggressive") {
65
85
  say("mnemom agent: context profile aggressive — the gateway folds earlier and keeps a tighter working set.");
66
86
  }
@@ -174,11 +194,12 @@ export async function agentCommand(scenarioArg, options = {}) {
174
194
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
175
195
  const yolo = options.yolo !== false; // default ON
176
196
  const contextHint = options.contextHint !== false; // default ON
177
- // Context profile: `balanced` (the gateway's own defaults) unless --context-profile /
178
- // MNEMOM_AGENT_CONTEXT_PROFILE (the `context_profile` setting) says otherwise; --no-context
179
- // (or `context = false` in launcher.toml → MNEMOM_AGENT_CONTEXT) is `off`. Anything but
180
- // balanced rides in the per-session config header; `undefined` sessionConfigB64 ⇒ nothing sent.
181
- const contextProfile = resolveContextProfile({
197
+ // Context profile: none (null — the cell's own default, no fold key sent) unless
198
+ // --context-profile / MNEMOM_AGENT_CONTEXT_PROFILE (the `context_profile` setting) chooses
199
+ // one; --no-context (or `context = false` in launcher.toml → MNEMOM_AGENT_CONTEXT) is `off`. A
200
+ // chosen profile rides in the per-session config header; `undefined` sessionConfigB64 ⇒
201
+ // nothing sent. A resume re-resolves it below, once the resumed session is known.
202
+ let contextProfile = resolveContextProfile({
182
203
  contextProfile: options.contextProfile,
183
204
  context: options.context,
184
205
  env,
@@ -202,6 +223,13 @@ export async function agentCommand(scenarioArg, options = {}) {
202
223
  // cannot arm without the intercept it just turned off. Refuse rather than silently pick
203
224
  // a side.
204
225
  assertCompactAtCompatible({ compactionIntercept: options.compactionIntercept, compactAt });
226
+ // Context profile off sends the intercept off: an explicit --compaction-intercept or
227
+ // --compact-at <N> in the same launch contradicts it. A resume re-checks below.
228
+ assertOffProfileCompatible({
229
+ profile: contextProfile,
230
+ compactionIntercept: options.compactionIntercept,
231
+ compactAt: options.compactAt,
232
+ });
205
233
  const experimental = options.experimental ?? [];
206
234
  const sessionConfig = buildSessionConfig({
207
235
  profile: contextProfile,
@@ -209,7 +237,7 @@ export async function agentCommand(scenarioArg, options = {}) {
209
237
  compactionIntercept,
210
238
  compactAt,
211
239
  });
212
- const sessionConfigB64 = buildSessionConfigB64({
240
+ let sessionConfigB64 = buildSessionConfigB64({
213
241
  profile: contextProfile,
214
242
  contextHandoff,
215
243
  compactionIntercept,
@@ -273,12 +301,10 @@ export async function agentCommand(scenarioArg, options = {}) {
273
301
  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.");
274
302
  }
275
303
  sayContextProfile(contextProfile);
276
- sayCompactAt(compactAt, launchWindow(windowModelFor(options, cli)));
304
+ sayCompaction(contextProfile, compactionIntercept, compactAt, launchWindow(windowModelFor(options, cli)));
277
305
  if (contextHandoff) {
278
306
  say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
279
307
  }
280
- if (compactionIntercept)
281
- say(COMPACTION_INTERCEPT_NOTE);
282
308
  return;
283
309
  }
284
310
  // Real launch: make sure we actually have a CLI to run. If we couldn't find it,
@@ -431,6 +457,38 @@ export async function agentCommand(scenarioArg, options = {}) {
431
457
  passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
432
458
  };
433
459
  }
460
+ // A launch that resumes a Claude Code session (the continuation above, or the caller's own
461
+ // `-- --resume <id>`) keeps the profile that session ran under, unless this launch chose one
462
+ // with a flag. Read from the ledger only — nothing here writes a default anywhere.
463
+ const resumeId = shape === "server" ? null : passthroughResumeId(options.passthrough ?? []);
464
+ if (resumeId && options.contextProfile === undefined && options.context !== false) {
465
+ let resumed;
466
+ try {
467
+ resumed = resumedContextProfile(readSessionRecords({ dir: MNEMOM_DIR }), {
468
+ ccSessionId: resumeId,
469
+ orgId: account.orgId,
470
+ gatewayHost,
471
+ });
472
+ }
473
+ catch {
474
+ resumed = null; // best-effort: an unreadable ledger keeps the launch's own resolution
475
+ }
476
+ if (resumed !== null && resumed !== contextProfile) {
477
+ contextProfile = resolveContextProfile({ context: options.context, resumed, env });
478
+ assertOffProfileCompatible({
479
+ profile: contextProfile,
480
+ compactionIntercept: options.compactionIntercept,
481
+ compactAt: options.compactAt,
482
+ });
483
+ sessionConfigB64 = buildSessionConfigB64({
484
+ profile: contextProfile,
485
+ contextHandoff,
486
+ compactionIntercept,
487
+ compactAt,
488
+ experimental,
489
+ });
490
+ }
491
+ }
434
492
  // ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
435
493
  // launch so `mnemom agent sessions` / `show` can find this conversation later.
436
494
  // Written after provisioning so a freshly claimed agent's id resolves. Holds
@@ -462,16 +520,14 @@ export async function agentCommand(scenarioArg, options = {}) {
462
520
  goalMode,
463
521
  ccSessionId: cc.id,
464
522
  contextProfile,
465
- compactAt,
523
+ compactAt: contextProfile === "off" ? null : compactAt,
466
524
  });
467
525
  logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
468
526
  if (expNotice)
469
527
  say(expNotice);
470
528
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
471
529
  sayContextProfile(contextProfile);
472
- sayCompactAt(compactAt, launchWindow(win));
473
- if (compactionIntercept)
474
- say(COMPACTION_INTERCEPT_NOTE);
530
+ sayCompaction(contextProfile, compactionIntercept, compactAt, launchWindow(win));
475
531
  // ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
476
532
  // undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
477
533
  // the config is written to a 0600 temp file and referenced by PATH only — never inlined into
@@ -703,7 +759,9 @@ export async function recordSessionLaunch(ctx) {
703
759
  cli: ctx.cli.name,
704
760
  contract: toLedgerContract(ctx.envelope?.contract ?? null),
705
761
  goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
706
- context_profile: ctx.contextProfile,
762
+ ...(ctx.contextProfile !== null
763
+ ? { context_profile: ctx.contextProfile, context_profile_v: 2 }
764
+ : {}),
707
765
  compact_at: ctx.compactAt,
708
766
  cc_session_id: ctx.ccSessionId ?? null,
709
767
  ...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
@@ -1618,7 +1676,7 @@ export function printPlan(ctx) {
1618
1676
  L.push(` agent: ${ctx.agent}`);
1619
1677
  L.push(` conversation: ${ctx.conversationId}`);
1620
1678
  L.push(` yolo: ${ctx.yolo ? "on (--dangerously-skip-permissions)" : "off"}`);
1621
- L.push(` context: ${ctx.contextProfile}`);
1679
+ L.push(` context: ${ctx.contextProfile ?? "cell default"}`);
1622
1680
  L.push(` compaction: ${describeCompactAt(ctx.compactAt)}`);
1623
1681
  const overlay = Object.entries(ctx.sessionConfig);
1624
1682
  if (overlay.length > 0) {
package/dist/index.js CHANGED
@@ -175,7 +175,7 @@ const agentCmd = program
175
175
  .addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
176
176
  .option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
177
177
  .option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
178
- .addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "adaptive" sizes the fold to the conversation: no fold below 200k tokens, aggressive from 200k to 1M, balanced above. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
178
+ .addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. Unset (the default): nothing is sent and the gateway cell\'s own default decides. "balanced" keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "adaptive" sizes the fold to the conversation: no fold below 200k tokens, aggressive from 200k to 1M, balanced above. "off" leaves compaction to the coding agent and never intercepts it (fold, compaction intercept and real-size usage all off; refused with --compaction-intercept or --compact-at <n>). Applies to this launch only (a resumed session keeps its own); to change the default for every launch: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
179
179
  // One validator for all three surfaces (flag, MNEMOM_AGENT_CONTEXT_PROFILE,
180
180
  // launcher.toml context_profile): trimmed, case-insensitive, same error wording.
181
181
  try {
@@ -14,7 +14,7 @@
14
14
  * `sessions.retention_days` (default 30) are dropped when the next one is
15
15
  * recorded. No count cap — a long retention keeps everything in the window.
16
16
  */
17
- import type { ContextProfile, LaunchShape, SealedContract } from "./agent.js";
17
+ import { type ContextProfile, type LaunchShape, type SealedContract } from "./agent.js";
18
18
  /** The ledger's file name under MNEMOM_DIR. */
19
19
  export declare const LEDGER_FILE = "sessions.jsonl";
20
20
  /** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
@@ -77,10 +77,17 @@ export interface SessionRecord {
77
77
  cc_session_id?: string | null;
78
78
  /**
79
79
  * The context profile the launch ran under (`--context-profile` / `context_profile` /
80
- * `MNEMOM_AGENT_CONTEXT_PROFILE`, or `balanced` by default). Optional: records written
81
- * before profiles existed omit it (still `v: 1`).
80
+ * `MNEMOM_AGENT_CONTEXT_PROFILE`, or the resumed session's own). Absent when none was chosen
81
+ * (the cell default) and on records written before profiles existed (still `v: 1`).
82
82
  */
83
83
  context_profile?: ContextProfile;
84
+ /**
85
+ * `2` when `context_profile` was written with complete profiles (`balanced` sends size policy
86
+ * off, `aggressive` adds it). Absent on older records, where `balanced` was also written for a
87
+ * launch that chose nothing and sent no fold key — so a resume reads an unmarked `balanced` as
88
+ * the cell default, which is what that session actually ran under.
89
+ */
90
+ context_profile_v?: 2;
84
91
  /**
85
92
  * The resolved gateway compaction target (`--compact-at` / `compact_at` /
86
93
  * `MNEMOM_COMPACT_AT`): `null` means no target (Claude Code's own 1M window; the default,
@@ -168,3 +175,15 @@ export declare function findResumeCandidate(records: readonly SessionRecord[], w
168
175
  gatewayHost: string;
169
176
  resumeId?: string | null;
170
177
  }): ResumeCandidate | null;
178
+ /**
179
+ * The context profile the Claude Code session `ccSessionId` ran under — the newest row with the
180
+ * same org and gateway host that pinned that session — so a launch that resumes it
181
+ * (`--resume <id>`) keeps that session's own profile. `null` when no row matches, the row chose
182
+ * no profile (the cell default), it is an older row's `balanced` (see `context_profile_v`), or its
183
+ * value is not a known profile (the file is hand-editable).
184
+ */
185
+ export declare function resumedContextProfile(records: readonly SessionRecord[], want: {
186
+ ccSessionId: string | null;
187
+ orgId: string;
188
+ gatewayHost: string;
189
+ }): ContextProfile | null;
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
+ import { CONTEXT_PROFILES, } from "./agent.js";
19
20
  /** The ledger's file name under MNEMOM_DIR. */
20
21
  export const LEDGER_FILE = "sessions.jsonl";
21
22
  /** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
@@ -197,3 +198,25 @@ export function findResumeCandidate(records, want) {
197
198
  }
198
199
  return latest ? { record: latest, source: "session_name", byCcSession: false } : null;
199
200
  }
201
+ /**
202
+ * The context profile the Claude Code session `ccSessionId` ran under — the newest row with the
203
+ * same org and gateway host that pinned that session — so a launch that resumes it
204
+ * (`--resume <id>`) keeps that session's own profile. `null` when no row matches, the row chose
205
+ * no profile (the cell default), it is an older row's `balanced` (see `context_profile_v`), or its
206
+ * value is not a known profile (the file is hand-editable).
207
+ */
208
+ export function resumedContextProfile(records, want) {
209
+ const id = want.ccSessionId?.trim();
210
+ if (!id || !want.orgId)
211
+ return null;
212
+ for (let i = records.length - 1; i >= 0; i--) {
213
+ const r = records[i];
214
+ if (r.cc_session_id !== id || r.org_id !== want.orgId || r.gateway_host !== want.gatewayHost)
215
+ continue;
216
+ const p = r.context_profile;
217
+ if (p === "balanced" && r.context_profile_v !== 2)
218
+ return null; // an older row: sent nothing
219
+ return p !== undefined && CONTEXT_PROFILES.includes(p) ? p : null;
220
+ }
221
+ return null;
222
+ }
@@ -294,22 +294,44 @@ export declare const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKE
294
294
  export declare const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
295
295
  export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
296
296
  /**
297
- * The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
298
- * size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
299
- * stay at the gateway's own defaults.
297
+ * The gateway key that turns the size-banded fold policy on or off (allow-listed per session).
298
+ * Only the literal `banded` enables it; any other value — the profiles send `off` — disables it,
299
+ * and a session's value replaces the cell's own. The band thresholds and budgets stay at the
300
+ * gateway's own defaults.
300
301
  */
301
302
  export declare const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
303
+ /**
304
+ * The two gateway env keys the `compaction_intercept` setting turns on for a single session
305
+ * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
306
+ * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
307
+ * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
308
+ * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
309
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
310
+ * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
311
+ * gateway's own fold digest (no model call), so the compaction never blocks the user.
312
+ * Sent together, value `"on"`, when the `compaction_intercept` setting is on
313
+ * (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
314
+ * target cannot be honored without both. The `off` context profile sends both as `"off"`.
315
+ */
316
+ export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
317
+ export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
302
318
  /**
303
319
  * The named presets for how the gateway folds a session's context (`--context-profile`,
304
- * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
305
- * - `off` → `CONTEXT_RUNTIME_ENABLED: "false"` — compaction is left to the coding agent
306
- * (the same bytes `--no-context` has always sent).
307
- * - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
308
- * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
309
- * earlier and keeps a tighter working set.
320
+ * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`). Choosing NONE sends no fold
321
+ * key at all and the cell's own default decides (see `resolveContextProfile`). A chosen profile
322
+ * is complete on its own, so a change to a cell's default can never change what it means:
323
+ * - `off` → `CONTEXT_RUNTIME_ENABLED: "false"` plus `GATEWAY_USAGE_REAL_SIZE: "off"` and
324
+ * `GATEWAY_COMPACTION_INTERCEPT: "off"` — compaction is left to the coding agent
325
+ * and never intercepted, even on a cell whose default turns the intercept on.
326
+ * The gateway already skips the intercept with the fold off, but the real-size
327
+ * usage rewrite is not gated on the fold, so `off` turns both off by name.
328
+ * - `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"` — the cell's own fold budgets, never
329
+ * the size bands, even on a cell whose default is adaptive.
330
+ * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on, size
331
+ * policy off: folds earlier and keeps a tighter working set at every size.
310
332
  * - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
311
333
  * conversation's original size: no fold below 200k tokens, the aggressive
312
- * budgets from 200k to 1M, balanced above. Opt-in; never the default.
334
+ * budgets from 200k to 1M, balanced above.
313
335
  */
314
336
  export declare const CONTEXT_PROFILES: readonly ["off", "balanced", "aggressive", "adaptive"];
315
337
  export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
@@ -322,21 +344,6 @@ export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
322
344
  * header without the tool.
323
345
  */
324
346
  export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
325
- /**
326
- * The two gateway env keys the `compaction_intercept` setting turns on for a single session
327
- * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
328
- * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
329
- * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
330
- * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
331
- * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
332
- * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
333
- * gateway's own fold digest (no model call), so the compaction never blocks the user.
334
- * Sent together, value `"on"`, when the `compaction_intercept` setting is on
335
- * (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
336
- * target cannot be honored without both.
337
- */
338
- export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
339
- export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
340
347
  export declare const GATEWAY_COMPACTION_UPLOAD_TARGET_KEY = "GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS";
341
348
  /** The lowest token count `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` will accept. */
342
349
  export declare const COMPACT_AT_MIN_TOKENS = 50000;
@@ -412,6 +419,19 @@ export declare function assertCompactAtCompatible(opts: {
412
419
  compactionIntercept?: boolean;
413
420
  compactAt: number | null;
414
421
  }): void;
422
+ /**
423
+ * The `off` context profile turns the fold off, and the compaction intercept answers a compaction
424
+ * from that fold, so `off` sends the intercept and real-size keys as `"off"`. An explicit
425
+ * `--compaction-intercept` or `--compact-at <N>` in the same launch asks for the opposite —
426
+ * refuse rather than silently drop the flag. A standing intercept or target (environment or
427
+ * launcher.toml) is not refused: the session's `off` wins and `buildSessionConfig` leaves it out.
428
+ * `compactionIntercept` / `compactAt` here are the raw flag values (`undefined` when not passed).
429
+ */
430
+ export declare function assertOffProfileCompatible(opts: {
431
+ profile: ContextProfile | null;
432
+ compactionIntercept?: boolean;
433
+ compactAt?: number | null;
434
+ }): void;
415
435
  /** The launch-banner line for the resolved compaction target (always printed). */
416
436
  export declare function describeCompactAt(compactAt: number | null): string;
417
437
  /**
@@ -556,36 +576,45 @@ export declare function resolveContextFold(opts: {
556
576
  */
557
577
  export declare function parseContextProfile(raw: string, source: string): ContextProfile;
558
578
  /**
559
- * Which context profile this launch runs under. Precedence, highest wins:
579
+ * Which context profile this launch runs under, or `null` when none was chosen anywhere — then
580
+ * no fold key is sent and the cell's own default decides. Precedence, highest wins:
560
581
  * 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
561
582
  * balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
562
583
  * `--context-profile off` agrees with it.
563
584
  * 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
564
585
  * profile wins);
565
- * 3. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
586
+ * 3. `resumed`: the profile the session this launch resumes ran under (from the session
587
+ * ledger), so a resume keeps that session's own choice over the saved default;
588
+ * 4. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
566
589
  * config key) — an invalid value is an error, never a silent default;
567
- * 4. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
590
+ * 5. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
568
591
  * (→ `MNEMOM_AGENT_CONTEXT`, see `resolveContextFold`) means `off`;
569
- * 5. `balanced` (the default).
592
+ * 6. `null` — unset, the cell default.
570
593
  */
571
594
  export declare function resolveContextProfile(opts: {
572
595
  contextProfile?: ContextProfile;
573
596
  context?: boolean;
597
+ resumed?: ContextProfile | null;
574
598
  env?: NodeJS.ProcessEnv;
575
- }): ContextProfile;
599
+ }): ContextProfile | null;
576
600
  /**
577
601
  * The decoded per-session env overlay for this launch — the `config` object of the
578
602
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
579
- * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
580
- * `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
603
+ * (`CONTEXT_RUNTIME_ENABLED: "false"` + real-size and intercept `"off"` for `off`; the three fold-tuning keys + size policy off for
604
+ * `aggressive`; size policy off for `balanced`; size policy banded for `adaptive`; nothing when
605
+ * no profile was chosen), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
581
606
  * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
582
607
  * `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
583
608
  * cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
584
- * when a target is set. Empty for `balanced` with everything off — the common case, where no
585
- * header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
609
+ * when a target is set. The `off` profile wins over the intercept and the target: a session with
610
+ * no fold never has its compaction intercepted (`assertOffProfileCompatible` refuses the explicit
611
+ * flag pair; a standing intercept or target from the environment or launcher.toml is dropped).
612
+ * Empty with no profile chosen and everything off — the common case, where
613
+ * no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
586
614
  */
587
615
  export declare function buildSessionConfig(opts: {
588
- profile: ContextProfile;
616
+ /** `resolveContextProfile` output: `null` ⇒ no profile chosen, no fold key sent. */
617
+ profile: ContextProfile | null;
589
618
  contextHandoff: boolean;
590
619
  compactionIntercept?: boolean;
591
620
  /** `resolveCompactAt` output: `null` (or absent) ⇒ no target, send nothing new. */
@@ -597,7 +626,7 @@ export declare function buildSessionConfig(opts: {
597
626
  * gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
598
627
  */
599
628
  export declare function buildSessionConfigPayload(opts: {
600
- profile: ContextProfile;
629
+ profile: ContextProfile | null;
601
630
  contextHandoff: boolean;
602
631
  compactionIntercept?: boolean;
603
632
  compactAt?: number | null;
@@ -609,11 +638,12 @@ export declare function buildSessionConfigPayload(opts: {
609
638
  } | undefined;
610
639
  /**
611
640
  * The `x-mnemom-session-config` value for this launch, or `undefined` when there is
612
- * nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
613
- * byte-identical to a launch before profiles existed). Independent overlays:
641
+ * nothing to override (the cell's own defaults stand — no profile chosen with everything else
642
+ * off, byte-identical to a launch before profiles existed). Independent overlays:
614
643
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
615
- * `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
616
- * "banded"`, `balanced` → nothing;
644
+ * `aggressive` → the three fold-tuning keys + `CONTEXT_RUNTIME_SIZE_POLICY: "off"`,
645
+ * `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"`, `adaptive` →
646
+ * `CONTEXT_RUNTIME_SIZE_POLICY: "banded"`, none chosen → nothing;
617
647
  * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
618
648
  * `"on"` (the `compaction_intercept` setting);
619
649
  * - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
@@ -626,7 +656,7 @@ export declare function buildSessionConfigPayload(opts: {
626
656
  * base64url so it survives the gateway's inbound strip.
627
657
  */
628
658
  export declare function buildSessionConfigB64(opts: {
629
- profile: ContextProfile;
659
+ profile: ContextProfile | null;
630
660
  contextHandoff: boolean;
631
661
  compactionIntercept?: boolean;
632
662
  compactAt?: number | null;
package/dist/lib/agent.js CHANGED
@@ -452,32 +452,59 @@ export const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKENS";
452
452
  export const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
453
453
  export const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
454
454
  /**
455
- * The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
456
- * size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
457
- * stay at the gateway's own defaults.
455
+ * The gateway key that turns the size-banded fold policy on or off (allow-listed per session).
456
+ * Only the literal `banded` enables it; any other value — the profiles send `off` — disables it,
457
+ * and a session's value replaces the cell's own. The band thresholds and budgets stay at the
458
+ * gateway's own defaults.
458
459
  */
459
460
  export const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
461
+ /**
462
+ * The two gateway env keys the `compaction_intercept` setting turns on for a single session
463
+ * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
464
+ * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
465
+ * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
466
+ * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
467
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
468
+ * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
469
+ * gateway's own fold digest (no model call), so the compaction never blocks the user.
470
+ * Sent together, value `"on"`, when the `compaction_intercept` setting is on
471
+ * (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
472
+ * target cannot be honored without both. The `off` context profile sends both as `"off"`.
473
+ */
474
+ export const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
475
+ export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
460
476
  /**
461
477
  * The named presets for how the gateway folds a session's context (`--context-profile`,
462
- * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
463
- * - `off` → `CONTEXT_RUNTIME_ENABLED: "false"` — compaction is left to the coding agent
464
- * (the same bytes `--no-context` has always sent).
465
- * - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
466
- * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
467
- * earlier and keeps a tighter working set.
478
+ * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`). Choosing NONE sends no fold
479
+ * key at all and the cell's own default decides (see `resolveContextProfile`). A chosen profile
480
+ * is complete on its own, so a change to a cell's default can never change what it means:
481
+ * - `off` → `CONTEXT_RUNTIME_ENABLED: "false"` plus `GATEWAY_USAGE_REAL_SIZE: "off"` and
482
+ * `GATEWAY_COMPACTION_INTERCEPT: "off"` — compaction is left to the coding agent
483
+ * and never intercepted, even on a cell whose default turns the intercept on.
484
+ * The gateway already skips the intercept with the fold off, but the real-size
485
+ * usage rewrite is not gated on the fold, so `off` turns both off by name.
486
+ * - `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"` — the cell's own fold budgets, never
487
+ * the size bands, even on a cell whose default is adaptive.
488
+ * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on, size
489
+ * policy off: folds earlier and keeps a tighter working set at every size.
468
490
  * - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
469
491
  * conversation's original size: no fold below 200k tokens, the aggressive
470
- * budgets from 200k to 1M, balanced above. Opt-in; never the default.
492
+ * budgets from 200k to 1M, balanced above.
471
493
  */
472
494
  export const CONTEXT_PROFILES = ["off", "balanced", "aggressive", "adaptive"];
473
495
  /** The per-session env overlay each profile expands to (key order is the wire order). */
474
496
  const CONTEXT_PROFILE_CONFIG = {
475
- off: [[CONTEXT_RUNTIME_KEY, "false"]],
476
- balanced: [],
497
+ off: [
498
+ [CONTEXT_RUNTIME_KEY, "false"],
499
+ [USAGE_REAL_SIZE_KEY, "off"],
500
+ [COMPACTION_INTERCEPT_KEY, "off"],
501
+ ],
502
+ balanced: [[CONTEXT_SIZE_POLICY_KEY, "off"]],
477
503
  aggressive: [
478
504
  [CONTEXT_TAIL_BUDGET_KEY, "60000"],
479
505
  [CONTEXT_READ_CACHE_KEY, "8000"],
480
506
  [CONTEXT_HEAD_RECLAIM_KEY, "true"],
507
+ [CONTEXT_SIZE_POLICY_KEY, "off"],
481
508
  ],
482
509
  adaptive: [[CONTEXT_SIZE_POLICY_KEY, "banded"]],
483
510
  };
@@ -490,21 +517,6 @@ const CONTEXT_PROFILE_CONFIG = {
490
517
  * header without the tool.
491
518
  */
492
519
  export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
493
- /**
494
- * The two gateway env keys the `compaction_intercept` setting turns on for a single session
495
- * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
496
- * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
497
- * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
498
- * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
499
- * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
500
- * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
501
- * gateway's own fold digest (no model call), so the compaction never blocks the user.
502
- * Sent together, value `"on"`, when the `compaction_intercept` setting is on
503
- * (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
504
- * target cannot be honored without both.
505
- */
506
- export const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
507
- export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
508
520
  // ---------------------------------------------------------------------------
509
521
  // Compaction target (`--compact-at`, launcher.toml `compact_at`, MNEMOM_COMPACT_AT)
510
522
  // ---------------------------------------------------------------------------
@@ -651,6 +663,28 @@ export function assertCompactAtCompatible(opts) {
651
663
  "enable compaction-intercept`), or pass --compact-at off.");
652
664
  }
653
665
  }
666
+ /**
667
+ * The `off` context profile turns the fold off, and the compaction intercept answers a compaction
668
+ * from that fold, so `off` sends the intercept and real-size keys as `"off"`. An explicit
669
+ * `--compaction-intercept` or `--compact-at <N>` in the same launch asks for the opposite —
670
+ * refuse rather than silently drop the flag. A standing intercept or target (environment or
671
+ * launcher.toml) is not refused: the session's `off` wins and `buildSessionConfig` leaves it out.
672
+ * `compactionIntercept` / `compactAt` here are the raw flag values (`undefined` when not passed).
673
+ */
674
+ export function assertOffProfileCompatible(opts) {
675
+ if (opts.profile !== "off")
676
+ return;
677
+ const flag = opts.compactionIntercept === true
678
+ ? "--compaction-intercept"
679
+ : typeof opts.compactAt === "number"
680
+ ? "--compact-at"
681
+ : null;
682
+ if (flag === null)
683
+ return;
684
+ throw new Error(`mnemom agent: context profile off and ${flag} disagree — with the fold off the gateway has ` +
685
+ `no fold to answer a compaction from, so off sends the compaction intercept off. Drop ` +
686
+ `${flag}, or choose another --context-profile.`);
687
+ }
654
688
  /**
655
689
  * `200000` → `"200k"`, anything not k-aligned → the bare digits. Every real caller only ever
656
690
  * passes a `parseCompactAt`-validated value, which is bounded to
@@ -912,17 +946,20 @@ export function parseContextProfile(raw, source) {
912
946
  throw new Error(`${source} must be one of ${CONTEXT_PROFILES.join(", ")} (got '${raw}').`);
913
947
  }
914
948
  /**
915
- * Which context profile this launch runs under. Precedence, highest wins:
949
+ * Which context profile this launch runs under, or `null` when none was chosen anywhere — then
950
+ * no fold key is sent and the cell's own default decides. Precedence, highest wins:
916
951
  * 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
917
952
  * balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
918
953
  * `--context-profile off` agrees with it.
919
954
  * 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
920
955
  * profile wins);
921
- * 3. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
956
+ * 3. `resumed`: the profile the session this launch resumes ran under (from the session
957
+ * ledger), so a resume keeps that session's own choice over the saved default;
958
+ * 4. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
922
959
  * config key) — an invalid value is an error, never a silent default;
923
- * 4. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
960
+ * 5. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
924
961
  * (→ `MNEMOM_AGENT_CONTEXT`, see `resolveContextFold`) means `off`;
925
- * 5. `balanced` (the default).
962
+ * 6. `null` — unset, the cell default.
926
963
  */
927
964
  export function resolveContextProfile(opts) {
928
965
  if (opts.context === false) {
@@ -933,34 +970,44 @@ export function resolveContextProfile(opts) {
933
970
  }
934
971
  if (opts.contextProfile !== undefined)
935
972
  return opts.contextProfile;
973
+ if (opts.resumed)
974
+ return opts.resumed;
936
975
  const raw = (opts.env?.MNEMOM_AGENT_CONTEXT_PROFILE ?? "").trim();
937
976
  if (raw !== "")
938
977
  return parseContextProfile(raw, "MNEMOM_AGENT_CONTEXT_PROFILE");
939
978
  if (!resolveContextFold({ context: opts.context, env: opts.env }))
940
979
  return "off";
941
- return "balanced";
980
+ return null;
942
981
  }
943
982
  /**
944
983
  * The decoded per-session env overlay for this launch — the `config` object of the
945
984
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
946
- * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
947
- * `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
985
+ * (`CONTEXT_RUNTIME_ENABLED: "false"` + real-size and intercept `"off"` for `off`; the three fold-tuning keys + size policy off for
986
+ * `aggressive`; size policy off for `balanced`; size policy banded for `adaptive`; nothing when
987
+ * no profile was chosen), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
948
988
  * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
949
989
  * `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
950
990
  * cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
951
- * when a target is set. Empty for `balanced` with everything off — the common case, where no
952
- * header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
991
+ * when a target is set. The `off` profile wins over the intercept and the target: a session with
992
+ * no fold never has its compaction intercepted (`assertOffProfileCompatible` refuses the explicit
993
+ * flag pair; a standing intercept or target from the environment or launcher.toml is dropped).
994
+ * Empty with no profile chosen and everything off — the common case, where
995
+ * no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
953
996
  */
954
997
  export function buildSessionConfig(opts) {
955
998
  const config = {};
956
- for (const [k, v] of CONTEXT_PROFILE_CONFIG[opts.profile])
957
- config[k] = v;
999
+ if (opts.profile !== null) {
1000
+ for (const [k, v] of CONTEXT_PROFILE_CONFIG[opts.profile])
1001
+ config[k] = v;
1002
+ }
958
1003
  if (opts.contextHandoff)
959
1004
  config[CONTEXT_HANDOFF_KEY] = "true";
960
1005
  const hasCompactionTarget = opts.compactAt !== undefined && opts.compactAt !== null;
961
1006
  // A target needs the compaction intercept + real-size rewrite to arm (#2216) — without them
962
1007
  // the upload-target key is silently inert (compaction-align.ts: "armed only inside the
963
1008
  // intercept"). Deliberately NOT GATEWAY_COMPACTION_REFOLD — a separate, costlier opt-in.
1009
+ if (opts.profile === "off")
1010
+ return config;
964
1011
  if (opts.compactionIntercept || hasCompactionTarget) {
965
1012
  config[USAGE_REAL_SIZE_KEY] = "on";
966
1013
  config[COMPACTION_INTERCEPT_KEY] = "on";
@@ -986,11 +1033,12 @@ export function buildSessionConfigPayload(opts) {
986
1033
  }
987
1034
  /**
988
1035
  * The `x-mnemom-session-config` value for this launch, or `undefined` when there is
989
- * nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
990
- * byte-identical to a launch before profiles existed). Independent overlays:
1036
+ * nothing to override (the cell's own defaults stand — no profile chosen with everything else
1037
+ * off, byte-identical to a launch before profiles existed). Independent overlays:
991
1038
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
992
- * `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
993
- * "banded"`, `balanced` → nothing;
1039
+ * `aggressive` → the three fold-tuning keys + `CONTEXT_RUNTIME_SIZE_POLICY: "off"`,
1040
+ * `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"`, `adaptive` →
1041
+ * `CONTEXT_RUNTIME_SIZE_POLICY: "banded"`, none chosen → nothing;
994
1042
  * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
995
1043
  * `"on"` (the `compaction_intercept` setting);
996
1044
  * - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
@@ -56,10 +56,11 @@ export interface LauncherConfig {
56
56
  */
57
57
  compactionIntercept?: boolean;
58
58
  /**
59
- * How the gateway folds each session's context (`balanced` when unset): `off` leaves
60
- * compaction to the coding agent, `balanced` keeps more earlier work verbatim, `aggressive`
61
- * folds earlier with a tighter working set, `adaptive` sizes the fold to the conversation
62
- * (none below 200k tokens, aggressive up to 1M, balanced above). `--context-profile` overrides it per launch.
59
+ * How the gateway folds each session's context (unset: nothing is sent, the gateway cell's
60
+ * own default decides): `off` leaves compaction to the coding agent, `balanced` keeps more
61
+ * earlier work verbatim, `aggressive` folds earlier with a tighter working set, `adaptive`
62
+ * sizes the fold to the conversation (none below 200k tokens, aggressive up to 1M, balanced
63
+ * above). `--context-profile` overrides it for one launch without saving anything.
63
64
  */
64
65
  contextProfile?: ContextProfile;
65
66
  /**
@@ -215,7 +215,7 @@ export const CONFIG_KEYS = [
215
215
  },
216
216
  {
217
217
  key: "context_profile",
218
- describe: `How the gateway folds each session's context (${CONTEXT_PROFILES.join("|")}; balanced by default)`,
218
+ describe: `How the gateway folds each session's context (${CONTEXT_PROFILES.join("|")}; unset = the gateway cell's default)`,
219
219
  get: (c) => c.contextProfile,
220
220
  set: (c, v) => {
221
221
  c.contextProfile = parseContextProfile(v, "context_profile");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.7-next.0",
3
+ "version": "0.17.7-next.2",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {