@mnemom/mnemom 0.17.7 → 0.17.8-next.0
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 +31 -13
- package/dist/commands/agent.d.ts +7 -6
- package/dist/commands/agent.js +78 -20
- package/dist/index.js +1 -1
- package/dist/lib/agent-ledger.d.ts +22 -3
- package/dist/lib/agent-ledger.js +23 -0
- package/dist/lib/agent.d.ts +71 -41
- package/dist/lib/agent.js +90 -42
- package/dist/lib/launcher-config.d.ts +5 -4
- package/dist/lib/launcher-config.js +1 -1
- package/package.json +1 -1
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.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
decoded key/value list; `mnemom agent show` and the session
|
|
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):
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -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 ⇒
|
|
70
|
-
*
|
|
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>;
|
|
@@ -352,8 +353,8 @@ export declare function printPlan(ctx: {
|
|
|
352
353
|
goalMode: string | null;
|
|
353
354
|
yolo: boolean;
|
|
354
355
|
cli: ResolvedCli;
|
|
355
|
-
/** The resolved context profile (`resolveContextProfile`). */
|
|
356
|
-
contextProfile: ContextProfile;
|
|
356
|
+
/** The resolved context profile (`resolveContextProfile`); `null` = none chosen (cell default). */
|
|
357
|
+
contextProfile: ContextProfile | null;
|
|
357
358
|
/** The resolved compaction target (`resolveCompactAt`); `null` = no target (default). */
|
|
358
359
|
compactAt: number | null;
|
|
359
360
|
/** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
|
package/dist/commands/agent.js
CHANGED
|
@@ -35,7 +35,7 @@ import { statuslineLaunch } from "../lib/statusline.js";
|
|
|
35
35
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
36
36
|
import { createApiKey, getAgentByName } from "../lib/api.js";
|
|
37
37
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, 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 { 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, 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, 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 } 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
|
|
58
|
-
*
|
|
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 ===
|
|
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:
|
|
178
|
-
// MNEMOM_AGENT_CONTEXT_PROFILE (the `context_profile` setting)
|
|
179
|
-
// (or `context = false` in launcher.toml → MNEMOM_AGENT_CONTEXT) is `off`.
|
|
180
|
-
//
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -424,6 +450,38 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
424
450
|
passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
|
|
425
451
|
};
|
|
426
452
|
}
|
|
453
|
+
// A launch that resumes a Claude Code session (the continuation above, or the caller's own
|
|
454
|
+
// `-- --resume <id>`) keeps the profile that session ran under, unless this launch chose one
|
|
455
|
+
// with a flag. Read from the ledger only — nothing here writes a default anywhere.
|
|
456
|
+
const resumeId = shape === "server" ? null : passthroughResumeId(options.passthrough ?? []);
|
|
457
|
+
if (resumeId && options.contextProfile === undefined && options.context !== false) {
|
|
458
|
+
let resumed;
|
|
459
|
+
try {
|
|
460
|
+
resumed = resumedContextProfile(readSessionRecords({ dir: MNEMOM_DIR }), {
|
|
461
|
+
ccSessionId: resumeId,
|
|
462
|
+
orgId: account.orgId,
|
|
463
|
+
gatewayHost,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
resumed = null; // best-effort: an unreadable ledger keeps the launch's own resolution
|
|
468
|
+
}
|
|
469
|
+
if (resumed !== null && resumed !== contextProfile) {
|
|
470
|
+
contextProfile = resolveContextProfile({ context: options.context, resumed, env });
|
|
471
|
+
assertOffProfileCompatible({
|
|
472
|
+
profile: contextProfile,
|
|
473
|
+
compactionIntercept: options.compactionIntercept,
|
|
474
|
+
compactAt: options.compactAt,
|
|
475
|
+
});
|
|
476
|
+
sessionConfigB64 = buildSessionConfigB64({
|
|
477
|
+
profile: contextProfile,
|
|
478
|
+
contextHandoff,
|
|
479
|
+
compactionIntercept,
|
|
480
|
+
compactAt,
|
|
481
|
+
experimental,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
427
485
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
428
486
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
429
487
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -455,16 +513,14 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
455
513
|
goalMode,
|
|
456
514
|
ccSessionId: cc.id,
|
|
457
515
|
contextProfile,
|
|
458
|
-
compactAt,
|
|
516
|
+
compactAt: contextProfile === "off" ? null : compactAt,
|
|
459
517
|
});
|
|
460
518
|
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
461
519
|
if (expNotice)
|
|
462
520
|
say(expNotice);
|
|
463
521
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
464
522
|
sayContextProfile(contextProfile);
|
|
465
|
-
|
|
466
|
-
if (compactionIntercept)
|
|
467
|
-
say(COMPACTION_INTERCEPT_NOTE);
|
|
523
|
+
sayCompaction(contextProfile, compactionIntercept, compactAt, launchWindow(win));
|
|
468
524
|
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
469
525
|
// undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
|
|
470
526
|
// the config is written to a 0600 temp file and referenced by PATH only — never inlined into
|
|
@@ -689,7 +745,9 @@ export async function recordSessionLaunch(ctx) {
|
|
|
689
745
|
cli: ctx.cli.name,
|
|
690
746
|
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
691
747
|
goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
|
|
692
|
-
|
|
748
|
+
...(ctx.contextProfile !== null
|
|
749
|
+
? { context_profile: ctx.contextProfile, context_profile_v: 2 }
|
|
750
|
+
: {}),
|
|
693
751
|
compact_at: ctx.compactAt,
|
|
694
752
|
cc_session_id: ctx.ccSessionId ?? null,
|
|
695
753
|
...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
|
|
@@ -1566,7 +1624,7 @@ export function printPlan(ctx) {
|
|
|
1566
1624
|
L.push(` agent: ${ctx.agent}`);
|
|
1567
1625
|
L.push(` conversation: ${ctx.conversationId}`);
|
|
1568
1626
|
L.push(` yolo: ${ctx.yolo ? "on (--dangerously-skip-permissions)" : "off"}`);
|
|
1569
|
-
L.push(` context: ${ctx.contextProfile}`);
|
|
1627
|
+
L.push(` context: ${ctx.contextProfile ?? "cell default"}`);
|
|
1570
1628
|
L.push(` compaction: ${describeCompactAt(ctx.compactAt)}`);
|
|
1571
1629
|
const overlay = Object.entries(ctx.sessionConfig);
|
|
1572
1630
|
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.
|
|
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
|
|
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
|
|
81
|
-
* before profiles existed
|
|
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;
|
package/dist/lib/agent-ledger.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -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
|
|
298
|
-
*
|
|
299
|
-
*
|
|
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
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* - `
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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.
|
|
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
|
/**
|
|
@@ -550,36 +570,45 @@ export declare function resolveContextFold(opts: {
|
|
|
550
570
|
*/
|
|
551
571
|
export declare function parseContextProfile(raw: string, source: string): ContextProfile;
|
|
552
572
|
/**
|
|
553
|
-
* Which context profile this launch runs under
|
|
573
|
+
* Which context profile this launch runs under, or `null` when none was chosen anywhere — then
|
|
574
|
+
* no fold key is sent and the cell's own default decides. Precedence, highest wins:
|
|
554
575
|
* 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
|
|
555
576
|
* balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
556
577
|
* `--context-profile off` agrees with it.
|
|
557
578
|
* 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
|
|
558
579
|
* profile wins);
|
|
559
|
-
* 3. `
|
|
580
|
+
* 3. `resumed`: the profile the session this launch resumes ran under (from the session
|
|
581
|
+
* ledger), so a resume keeps that session's own choice over the saved default;
|
|
582
|
+
* 4. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
|
|
560
583
|
* config key) — an invalid value is an error, never a silent default;
|
|
561
|
-
*
|
|
584
|
+
* 5. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
|
|
562
585
|
* (→ `MNEMOM_AGENT_CONTEXT`, see `resolveContextFold`) means `off`;
|
|
563
|
-
*
|
|
586
|
+
* 6. `null` — unset, the cell default.
|
|
564
587
|
*/
|
|
565
588
|
export declare function resolveContextProfile(opts: {
|
|
566
589
|
contextProfile?: ContextProfile;
|
|
567
590
|
context?: boolean;
|
|
591
|
+
resumed?: ContextProfile | null;
|
|
568
592
|
env?: NodeJS.ProcessEnv;
|
|
569
|
-
}): ContextProfile;
|
|
593
|
+
}): ContextProfile | null;
|
|
570
594
|
/**
|
|
571
595
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
572
596
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
573
|
-
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for
|
|
574
|
-
* `
|
|
597
|
+
* (`CONTEXT_RUNTIME_ENABLED: "false"` + real-size and intercept `"off"` for `off`; the three fold-tuning keys + size policy off for
|
|
598
|
+
* `aggressive`; size policy off for `balanced`; size policy banded for `adaptive`; nothing when
|
|
599
|
+
* no profile was chosen), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
575
600
|
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
576
601
|
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
577
602
|
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
578
|
-
* when a target is set.
|
|
579
|
-
*
|
|
603
|
+
* when a target is set. The `off` profile wins over the intercept and the target: a session with
|
|
604
|
+
* no fold never has its compaction intercepted (`assertOffProfileCompatible` refuses the explicit
|
|
605
|
+
* flag pair; a standing intercept or target from the environment or launcher.toml is dropped).
|
|
606
|
+
* Empty with no profile chosen and everything off — the common case, where
|
|
607
|
+
* no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
580
608
|
*/
|
|
581
609
|
export declare function buildSessionConfig(opts: {
|
|
582
|
-
|
|
610
|
+
/** `resolveContextProfile` output: `null` ⇒ no profile chosen, no fold key sent. */
|
|
611
|
+
profile: ContextProfile | null;
|
|
583
612
|
contextHandoff: boolean;
|
|
584
613
|
compactionIntercept?: boolean;
|
|
585
614
|
/** `resolveCompactAt` output: `null` (or absent) ⇒ no target, send nothing new. */
|
|
@@ -591,7 +620,7 @@ export declare function buildSessionConfig(opts: {
|
|
|
591
620
|
* gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
|
|
592
621
|
*/
|
|
593
622
|
export declare function buildSessionConfigPayload(opts: {
|
|
594
|
-
profile: ContextProfile;
|
|
623
|
+
profile: ContextProfile | null;
|
|
595
624
|
contextHandoff: boolean;
|
|
596
625
|
compactionIntercept?: boolean;
|
|
597
626
|
compactAt?: number | null;
|
|
@@ -603,11 +632,12 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
603
632
|
} | undefined;
|
|
604
633
|
/**
|
|
605
634
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
606
|
-
* nothing to override (the
|
|
607
|
-
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
635
|
+
* nothing to override (the cell's own defaults stand — no profile chosen with everything else
|
|
636
|
+
* off, byte-identical to a launch before profiles existed). Independent overlays:
|
|
608
637
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
609
|
-
* `aggressive` → the three fold-tuning keys
|
|
610
|
-
* "
|
|
638
|
+
* `aggressive` → the three fold-tuning keys + `CONTEXT_RUNTIME_SIZE_POLICY: "off"`,
|
|
639
|
+
* `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"`, `adaptive` →
|
|
640
|
+
* `CONTEXT_RUNTIME_SIZE_POLICY: "banded"`, none chosen → nothing;
|
|
611
641
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
612
642
|
* `"on"` (the `compaction_intercept` setting);
|
|
613
643
|
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
@@ -620,7 +650,7 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
620
650
|
* base64url so it survives the gateway's inbound strip.
|
|
621
651
|
*/
|
|
622
652
|
export declare function buildSessionConfigB64(opts: {
|
|
623
|
-
profile: ContextProfile;
|
|
653
|
+
profile: ContextProfile | null;
|
|
624
654
|
contextHandoff: boolean;
|
|
625
655
|
compactionIntercept?: boolean;
|
|
626
656
|
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
|
|
456
|
-
*
|
|
457
|
-
*
|
|
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
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* - `
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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.
|
|
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: [
|
|
476
|
-
|
|
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
|
|
@@ -910,17 +944,20 @@ export function parseContextProfile(raw, source) {
|
|
|
910
944
|
throw new Error(`${source} must be one of ${CONTEXT_PROFILES.join(", ")} (got '${raw}').`);
|
|
911
945
|
}
|
|
912
946
|
/**
|
|
913
|
-
* Which context profile this launch runs under
|
|
947
|
+
* Which context profile this launch runs under, or `null` when none was chosen anywhere — then
|
|
948
|
+
* no fold key is sent and the cell's own default decides. Precedence, highest wins:
|
|
914
949
|
* 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
|
|
915
950
|
* balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
|
|
916
951
|
* `--context-profile off` agrees with it.
|
|
917
952
|
* 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
|
|
918
953
|
* profile wins);
|
|
919
|
-
* 3. `
|
|
954
|
+
* 3. `resumed`: the profile the session this launch resumes ran under (from the session
|
|
955
|
+
* ledger), so a resume keeps that session's own choice over the saved default;
|
|
956
|
+
* 4. `MNEMOM_AGENT_CONTEXT_PROFILE` (which `applyConfigToEnv` sets from the `context_profile`
|
|
920
957
|
* config key) — an invalid value is an error, never a silent default;
|
|
921
|
-
*
|
|
958
|
+
* 5. the folding boolean when NO profile is set anywhere: `context = false` in launcher.toml
|
|
922
959
|
* (→ `MNEMOM_AGENT_CONTEXT`, see `resolveContextFold`) means `off`;
|
|
923
|
-
*
|
|
960
|
+
* 6. `null` — unset, the cell default.
|
|
924
961
|
*/
|
|
925
962
|
export function resolveContextProfile(opts) {
|
|
926
963
|
if (opts.context === false) {
|
|
@@ -931,34 +968,44 @@ export function resolveContextProfile(opts) {
|
|
|
931
968
|
}
|
|
932
969
|
if (opts.contextProfile !== undefined)
|
|
933
970
|
return opts.contextProfile;
|
|
971
|
+
if (opts.resumed)
|
|
972
|
+
return opts.resumed;
|
|
934
973
|
const raw = (opts.env?.MNEMOM_AGENT_CONTEXT_PROFILE ?? "").trim();
|
|
935
974
|
if (raw !== "")
|
|
936
975
|
return parseContextProfile(raw, "MNEMOM_AGENT_CONTEXT_PROFILE");
|
|
937
976
|
if (!resolveContextFold({ context: opts.context, env: opts.env }))
|
|
938
977
|
return "off";
|
|
939
|
-
return
|
|
978
|
+
return null;
|
|
940
979
|
}
|
|
941
980
|
/**
|
|
942
981
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
943
982
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
944
|
-
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for
|
|
945
|
-
* `
|
|
983
|
+
* (`CONTEXT_RUNTIME_ENABLED: "false"` + real-size and intercept `"off"` for `off`; the three fold-tuning keys + size policy off for
|
|
984
|
+
* `aggressive`; size policy off for `balanced`; size policy banded for `adaptive`; nothing when
|
|
985
|
+
* no profile was chosen), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
946
986
|
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
947
987
|
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
948
988
|
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
949
|
-
* when a target is set.
|
|
950
|
-
*
|
|
989
|
+
* when a target is set. The `off` profile wins over the intercept and the target: a session with
|
|
990
|
+
* no fold never has its compaction intercepted (`assertOffProfileCompatible` refuses the explicit
|
|
991
|
+
* flag pair; a standing intercept or target from the environment or launcher.toml is dropped).
|
|
992
|
+
* Empty with no profile chosen and everything off — the common case, where
|
|
993
|
+
* no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
951
994
|
*/
|
|
952
995
|
export function buildSessionConfig(opts) {
|
|
953
996
|
const config = {};
|
|
954
|
-
|
|
955
|
-
|
|
997
|
+
if (opts.profile !== null) {
|
|
998
|
+
for (const [k, v] of CONTEXT_PROFILE_CONFIG[opts.profile])
|
|
999
|
+
config[k] = v;
|
|
1000
|
+
}
|
|
956
1001
|
if (opts.contextHandoff)
|
|
957
1002
|
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
958
1003
|
const hasCompactionTarget = opts.compactAt !== undefined && opts.compactAt !== null;
|
|
959
1004
|
// A target needs the compaction intercept + real-size rewrite to arm (#2216) — without them
|
|
960
1005
|
// the upload-target key is silently inert (compaction-align.ts: "armed only inside the
|
|
961
1006
|
// intercept"). Deliberately NOT GATEWAY_COMPACTION_REFOLD — a separate, costlier opt-in.
|
|
1007
|
+
if (opts.profile === "off")
|
|
1008
|
+
return config;
|
|
962
1009
|
if (opts.compactionIntercept || hasCompactionTarget) {
|
|
963
1010
|
config[USAGE_REAL_SIZE_KEY] = "on";
|
|
964
1011
|
config[COMPACTION_INTERCEPT_KEY] = "on";
|
|
@@ -984,11 +1031,12 @@ export function buildSessionConfigPayload(opts) {
|
|
|
984
1031
|
}
|
|
985
1032
|
/**
|
|
986
1033
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
987
|
-
* nothing to override (the
|
|
988
|
-
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
1034
|
+
* nothing to override (the cell's own defaults stand — no profile chosen with everything else
|
|
1035
|
+
* off, byte-identical to a launch before profiles existed). Independent overlays:
|
|
989
1036
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
990
|
-
* `aggressive` → the three fold-tuning keys
|
|
991
|
-
* "
|
|
1037
|
+
* `aggressive` → the three fold-tuning keys + `CONTEXT_RUNTIME_SIZE_POLICY: "off"`,
|
|
1038
|
+
* `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"`, `adaptive` →
|
|
1039
|
+
* `CONTEXT_RUNTIME_SIZE_POLICY: "banded"`, none chosen → nothing;
|
|
992
1040
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
993
1041
|
* `"on"` (the `compaction_intercept` setting);
|
|
994
1042
|
* - `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 (
|
|
60
|
-
* compaction to the coding agent, `balanced` keeps more
|
|
61
|
-
* folds earlier with a tighter working set, `adaptive`
|
|
62
|
-
* (none below 200k tokens, aggressive up to 1M, balanced
|
|
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("|")};
|
|
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");
|