@mnemom/mnemom 0.17.6-next.0 → 0.17.6-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent.d.ts +12 -0
- package/dist/commands/agent.js +50 -12
- package/dist/index.js +55 -2
- package/dist/lib/agent-ledger.d.ts +8 -0
- package/dist/lib/agent.d.ts +94 -6
- package/dist/lib/agent.js +184 -7
- package/dist/lib/experimental.d.ts +11 -2
- package/dist/lib/experimental.js +21 -2
- package/dist/lib/launcher-config.d.ts +11 -2
- package/dist/lib/launcher-config.js +32 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,47 @@ gateway support: a gateway without it drops both keys (they are echoed back in
|
|
|
148
148
|
`x-mnemom-session-config-dropped`) and the session behaves exactly as with the setting off. `--no-compaction-intercept` turns it off for
|
|
149
149
|
one launch.
|
|
150
150
|
|
|
151
|
+
#### Compaction target (`--compact-at`)
|
|
152
|
+
|
|
153
|
+
`--compact-at <n|200k|1m|off>` lets you play with WHEN the gateway pulls this session's own
|
|
154
|
+
compaction point forward, instead of leaving it entirely to Claude Code's normal 1M-window
|
|
155
|
+
schedule. **Unset, `1m`, or `off` all mean no target** — Claude Code runs its normal 1M window
|
|
156
|
+
and the launcher sends nothing new (byte-identical to a launch without this setting at all). A
|
|
157
|
+
token count below 1M (k/m suffixes OK, e.g. `200k` or `350000`, from 50k up to just under 1M)
|
|
158
|
+
arms a gateway-side target for this session: the per-session config header carries
|
|
159
|
+
`GATEWAY_USAGE_REAL_SIZE=on` + `GATEWAY_COMPACTION_INTERCEPT=on` (the two keys a target needs to
|
|
160
|
+
arm — the same pair `--compaction-intercept` turns on) plus
|
|
161
|
+
`GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS=<n>`. It never sends `GATEWAY_COMPACTION_REFOLD` — that
|
|
162
|
+
costlier opt-in stays separate. It never touches Claude Code's own autocompact env or window.
|
|
163
|
+
|
|
164
|
+
A target only works **below Claude Code's own compaction point for the window the session runs
|
|
165
|
+
on**: 967k on a 1M window, 167k on a 200k window (Haiku, models with no 1M window, or
|
|
166
|
+
`CLAUDE_CODE_DISABLE_1M_CONTEXT`). At or above that point the gateway ignores it and says
|
|
167
|
+
nothing back, so the launcher prints a warning at launch (and in `--dry-run`) when it can tell
|
|
168
|
+
the target will be ignored, and a note when it can't tell the window (a passthrough `-- --model`,
|
|
169
|
+
a resume, or a CLI other than Claude Code).
|
|
170
|
+
|
|
171
|
+
**Arming a target needs the same opt-in as `--compaction-intercept`** (`mnemom experimental
|
|
172
|
+
enable compaction-intercept`) — a target arms the identical pair of gateway keys, so it needs
|
|
173
|
+
the identical gate; the flag itself stays visible in `--help` (it's the knob you asked for), only
|
|
174
|
+
actually _arming_ a target is gated. Passing `--no-compaction-intercept` together with an armed
|
|
175
|
+
`--compact-at` is refused outright (they contradict: the target cannot arm without the intercept
|
|
176
|
+
you just turned off).
|
|
177
|
+
|
|
178
|
+
Set it per launch, as a standing default, or in the environment (flag > env > `launcher.toml` >
|
|
179
|
+
default):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
mnemom experimental enable compaction-intercept # one-time opt-in
|
|
183
|
+
mnemom agent my-task --compact-at 200k # this launch only
|
|
184
|
+
mnemom agent config set compact_at 200k # persists in ~/.mnemom/launcher.toml
|
|
185
|
+
export MNEMOM_COMPACT_AT=200k # for every launch in this shell
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--dry-run` prints the resolved target (`compaction: gateway at 200k` or `compaction: Claude
|
|
189
|
+
Code default (1M)`) and, when a target is set, the decoded session-config keys; the session
|
|
190
|
+
ledger records the effective value.
|
|
191
|
+
|
|
151
192
|
## How It Works
|
|
152
193
|
|
|
153
194
|
```
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface AgentOptions {
|
|
|
70
70
|
* `balanced`. `--no-context` is `off`; combined with balanced|aggressive the launch refuses.
|
|
71
71
|
*/
|
|
72
72
|
contextProfile?: ContextProfile;
|
|
73
|
+
/**
|
|
74
|
+
* The gateway compaction target this launch runs under (`--compact-at`), already parsed by
|
|
75
|
+
* commander's argParser: `undefined` = the flag was not given (fall back to
|
|
76
|
+
* `MNEMOM_COMPACT_AT` / default), `null` = an explicit `1m`/`off` (no target, same as
|
|
77
|
+
* default), a number = tokens. Below 1M it arms `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS` +
|
|
78
|
+
* the #2216 intercept/real-size keys for this session only; unset/null sends nothing new.
|
|
79
|
+
*/
|
|
80
|
+
compactAt?: number | null;
|
|
73
81
|
/** context-hint seed default: commander `--no-context-hint` sets this false. */
|
|
74
82
|
contextHint?: boolean;
|
|
75
83
|
/** Attach the Slack channel server (`--slack`; lib/slack-launch.ts). */
|
|
@@ -189,6 +197,8 @@ export declare function recordSessionLaunch(ctx: {
|
|
|
189
197
|
ccSessionId?: string | null;
|
|
190
198
|
/** The resolved context profile this launch runs under. */
|
|
191
199
|
contextProfile: ContextProfile;
|
|
200
|
+
/** The resolved compaction target this launch runs under (`resolveCompactAt`). */
|
|
201
|
+
compactAt: number | null;
|
|
192
202
|
}): Promise<void>;
|
|
193
203
|
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
194
204
|
export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
|
|
@@ -344,6 +354,8 @@ export declare function printPlan(ctx: {
|
|
|
344
354
|
cli: ResolvedCli;
|
|
345
355
|
/** The resolved context profile (`resolveContextProfile`). */
|
|
346
356
|
contextProfile: ContextProfile;
|
|
357
|
+
/** The resolved compaction target (`resolveCompactAt`); `null` = no target (default). */
|
|
358
|
+
compactAt: number | null;
|
|
347
359
|
/** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
|
|
348
360
|
sessionConfig: Record<string, string>;
|
|
349
361
|
/** The experimental features on for this launch (the header's `experimental` array). */
|
package/dist/commands/agent.js
CHANGED
|
@@ -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, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
48
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
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. */
|
|
@@ -65,6 +65,37 @@ function sayContextProfile(profile) {
|
|
|
65
65
|
say("mnemom agent: context profile aggressive — the gateway folds earlier and keeps a tighter working set.");
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The one-line launch-banner note for the resolved compaction target (`resolveCompactAt`):
|
|
70
|
+
* printed on EVERY launch (dry-run and real), the same way `door`/`conversation` are — there is
|
|
71
|
+
* always a value (default or an explicit target), unlike the context-profile note above which
|
|
72
|
+
* is silent for the common case.
|
|
73
|
+
*/
|
|
74
|
+
function sayCompactAt(compactAt, window) {
|
|
75
|
+
say(`mnemom agent: compaction: ${describeCompactAt(compactAt)}`);
|
|
76
|
+
// A target at/above the window's own compaction point is ignored by the gateway, which never
|
|
77
|
+
// says so back — tell the user here instead of reporting it as if it took effect.
|
|
78
|
+
const warning = compactAtWindowWarning(compactAt, window);
|
|
79
|
+
if (warning)
|
|
80
|
+
say(warning);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The launch's window (resolveWindowModel) — Claude Code only: it is the adapter that reads the
|
|
84
|
+
* window off the model id. `null` for any other CLI (or none resolved).
|
|
85
|
+
*/
|
|
86
|
+
function windowModelFor(options, cli) {
|
|
87
|
+
return cli?.name === "claude"
|
|
88
|
+
? resolveWindowModel({
|
|
89
|
+
explicit: options.model,
|
|
90
|
+
passthrough: options.passthrough ?? [],
|
|
91
|
+
env: process.env,
|
|
92
|
+
settings: () => readCcSettings({
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
configDir: (process.env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"),
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
68
99
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
69
100
|
export async function agentCommand(scenarioArg, options = {}) {
|
|
70
101
|
// ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
|
|
@@ -159,16 +190,27 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
159
190
|
compactionIntercept: options.compactionIntercept,
|
|
160
191
|
env,
|
|
161
192
|
});
|
|
193
|
+
// Compaction target: `--compact-at` (undefined ⇒ MNEMOM_COMPACT_AT, which
|
|
194
|
+
// applyConfigToEnv already folded the `compact_at` setting under), else null (no target —
|
|
195
|
+
// Claude Code's own 1M window, nothing new sent). See lib/agent.ts for the full contract.
|
|
196
|
+
const compactAt = resolveCompactAt({ compactAt: options.compactAt, env });
|
|
197
|
+
// An explicit --no-compaction-intercept (compactionIntercept === false, never the mere
|
|
198
|
+
// absence of --compaction-intercept) conflicts with an armed --compact-at: the target
|
|
199
|
+
// cannot arm without the intercept it just turned off. Refuse rather than silently pick
|
|
200
|
+
// a side.
|
|
201
|
+
assertCompactAtCompatible({ compactionIntercept: options.compactionIntercept, compactAt });
|
|
162
202
|
const experimental = options.experimental ?? [];
|
|
163
203
|
const sessionConfig = buildSessionConfig({
|
|
164
204
|
profile: contextProfile,
|
|
165
205
|
contextHandoff,
|
|
166
206
|
compactionIntercept,
|
|
207
|
+
compactAt,
|
|
167
208
|
});
|
|
168
209
|
const sessionConfigB64 = buildSessionConfigB64({
|
|
169
210
|
profile: contextProfile,
|
|
170
211
|
contextHandoff,
|
|
171
212
|
compactionIntercept,
|
|
213
|
+
compactAt,
|
|
172
214
|
experimental,
|
|
173
215
|
});
|
|
174
216
|
const expNotice = experimentalNotice(experimental);
|
|
@@ -212,6 +254,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
212
254
|
yolo,
|
|
213
255
|
cli: planCli,
|
|
214
256
|
contextProfile,
|
|
257
|
+
compactAt,
|
|
215
258
|
sessionConfig,
|
|
216
259
|
experimental,
|
|
217
260
|
});
|
|
@@ -227,6 +270,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
227
270
|
say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
|
|
228
271
|
}
|
|
229
272
|
sayContextProfile(contextProfile);
|
|
273
|
+
sayCompactAt(compactAt, launchWindow(windowModelFor(options, cli)));
|
|
230
274
|
if (contextHandoff) {
|
|
231
275
|
say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
|
|
232
276
|
}
|
|
@@ -389,17 +433,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
389
433
|
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
390
434
|
// CLAUDE_CODE_DISABLE_1M_CONTEXT opt-out, a passthrough `-- --model`, a suffixed id — is kept.
|
|
391
435
|
// Claude Code only: it is the adapter that reads the window off the model id.
|
|
392
|
-
const win = cli
|
|
393
|
-
? resolveWindowModel({
|
|
394
|
-
explicit: options.model,
|
|
395
|
-
passthrough: options.passthrough ?? [],
|
|
396
|
-
env: process.env,
|
|
397
|
-
settings: () => readCcSettings({
|
|
398
|
-
cwd: process.cwd(),
|
|
399
|
-
configDir: (process.env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"),
|
|
400
|
-
}),
|
|
401
|
-
})
|
|
402
|
-
: null;
|
|
436
|
+
const win = windowModelFor(options, cli);
|
|
403
437
|
const launchOptions = win?.reason === "one-m" ? { ...options, model: win.model } : options;
|
|
404
438
|
if (win?.reason === "one-m") {
|
|
405
439
|
say(`mnemom agent: model ${win.from} → ${win.model} (1M context window; set ${DISABLE_1M_ENV}=1, or pass the model as \`-- --model <id>\`, to keep the standard window).`);
|
|
@@ -418,12 +452,14 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
418
452
|
goalMode,
|
|
419
453
|
ccSessionId: cc.id,
|
|
420
454
|
contextProfile,
|
|
455
|
+
compactAt,
|
|
421
456
|
});
|
|
422
457
|
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
423
458
|
if (expNotice)
|
|
424
459
|
say(expNotice);
|
|
425
460
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
426
461
|
sayContextProfile(contextProfile);
|
|
462
|
+
sayCompactAt(compactAt, launchWindow(win));
|
|
427
463
|
if (compactionIntercept)
|
|
428
464
|
say(COMPACTION_INTERCEPT_NOTE);
|
|
429
465
|
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
@@ -651,6 +687,7 @@ export async function recordSessionLaunch(ctx) {
|
|
|
651
687
|
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
652
688
|
goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
|
|
653
689
|
context_profile: ctx.contextProfile,
|
|
690
|
+
compact_at: ctx.compactAt,
|
|
654
691
|
cc_session_id: ctx.ccSessionId ?? null,
|
|
655
692
|
...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
|
|
656
693
|
}, {
|
|
@@ -1527,6 +1564,7 @@ export function printPlan(ctx) {
|
|
|
1527
1564
|
L.push(` conversation: ${ctx.conversationId}`);
|
|
1528
1565
|
L.push(` yolo: ${ctx.yolo ? "on (--dangerously-skip-permissions)" : "off"}`);
|
|
1529
1566
|
L.push(` context: ${ctx.contextProfile}`);
|
|
1567
|
+
L.push(` compaction: ${describeCompactAt(ctx.compactAt)}`);
|
|
1530
1568
|
const overlay = Object.entries(ctx.sessionConfig);
|
|
1531
1569
|
if (overlay.length > 0) {
|
|
1532
1570
|
L.push(` session config (${SESSION_CONFIG_HEADER}):`);
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledE
|
|
|
33
33
|
import { loadExperimentalState } from "./lib/cli-config.js";
|
|
34
34
|
import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
|
|
35
35
|
import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
|
|
36
|
-
import { parseContextProfile, resolveImplicit } from "./lib/agent.js";
|
|
36
|
+
import { parseCompactAt, parseContextProfile, resolveImplicit, } from "./lib/agent.js";
|
|
37
37
|
import { wrapCommand } from "./commands/wrap.js";
|
|
38
38
|
import { onboardCommand } from "./commands/onboard.js";
|
|
39
39
|
import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
|
|
@@ -124,10 +124,24 @@ const collectRepeatable = (value, previous) => {
|
|
|
124
124
|
previous.push(value);
|
|
125
125
|
return previous;
|
|
126
126
|
};
|
|
127
|
+
/**
|
|
128
|
+
* `mnemom agent help [verb]`: a visible commander sub-verb (`list`, `sessions`, …) prints
|
|
129
|
+
* its own help; anything else — no verb, or an in-action verb like `setup`/`doctor`/
|
|
130
|
+
* `config` that the agent help itself documents — prints the agent help.
|
|
131
|
+
*/
|
|
132
|
+
function printAgentHelp(verb) {
|
|
133
|
+
const sub = verb
|
|
134
|
+
? agentCmd
|
|
135
|
+
.createHelp()
|
|
136
|
+
.visibleCommands(agentCmd)
|
|
137
|
+
.find((c) => c.name() === verb || c.aliases().includes(verb))
|
|
138
|
+
: undefined;
|
|
139
|
+
(sub ?? agentCmd).outputHelp();
|
|
140
|
+
}
|
|
127
141
|
const agentCmd = program
|
|
128
142
|
.command("agent [session] [passthrough...]")
|
|
129
143
|
.description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name and an optional goal. " +
|
|
130
|
-
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state.")
|
|
144
|
+
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
|
|
131
145
|
.option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
|
|
132
146
|
.option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
|
|
133
147
|
.option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
|
|
@@ -170,6 +184,22 @@ const agentCmd = program
|
|
|
170
184
|
catch (e) {
|
|
171
185
|
throw new InvalidArgumentError(e.message);
|
|
172
186
|
}
|
|
187
|
+
}))
|
|
188
|
+
.addOption(new Option("--compact-at <n|200k|1m|off>", 'Play with WHEN the gateway pulls this session\'s own compaction point forward, instead of leaving it to Claude Code\'s normal 1M-window schedule. A token count below 1M (k/m suffix OK, e.g. "200k") arms the gateway\'s compaction target for this session; unset, "1m" or "off" all mean no target — Claude Code runs its normal 1M window and nothing new is sent. Arming a target needs `mnemom experimental enable compaction-intercept` (the same opt-in --compaction-intercept uses — a target arms the identical gateway keys) and is refused if you also pass --no-compaction-intercept. Never touches Claude Code\'s own autocompact env/window. Also: config key compact_at, env MNEMOM_COMPACT_AT.').argParser((v) => {
|
|
189
|
+
// Validate with the same parser the other two surfaces (MNEMOM_COMPACT_AT, launcher.toml
|
|
190
|
+
// compact_at) use, but return the validated RAW string rather than parseCompactAt's
|
|
191
|
+
// `number | null` — commander's own option-value handling coerces a parseArg return of
|
|
192
|
+
// `null` to `''` for a mandatory-argument option (Command.js's `handleOptionValue`: any
|
|
193
|
+
// `val == null` after parseArg falls through to the "not normal" `val = ''` branch), which
|
|
194
|
+
// would turn "1m"/"off" into a lone empty string instead of "no target". The real
|
|
195
|
+
// `number | null` conversion happens once, right after commander hands back `opts.compactAt`.
|
|
196
|
+
try {
|
|
197
|
+
parseCompactAt(v, "--compact-at");
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
throw new InvalidArgumentError(e.message);
|
|
201
|
+
}
|
|
202
|
+
return v;
|
|
173
203
|
}))
|
|
174
204
|
.addOption(new Option("--context-handoff", "EXPERIMENTAL (`mnemom experimental enable context-handoff`): register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session").hideHelp(hiddenUnless("context-handoff")))
|
|
175
205
|
.option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
|
|
@@ -179,6 +209,13 @@ const agentCmd = program
|
|
|
179
209
|
.option("--first-run", "Show the first-run look again: what Mnemom would have saved on your last 30 days of Claude Code (runs on this machine, read-only, nothing uploaded) — then exit")
|
|
180
210
|
.action(async (scenario, passthrough, opts) => {
|
|
181
211
|
try {
|
|
212
|
+
// `help` is reserved: `mnemom agent help [verb]` prints help and never launches
|
|
213
|
+
// (nor redeems an --invite). Without this, `help` was taken as a session name and
|
|
214
|
+
// the verb passed through to Claude Code as the opening prompt.
|
|
215
|
+
if (scenario === "help") {
|
|
216
|
+
printAgentHelp(passthrough[0]);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
182
219
|
// Invitation redemption — `mnemom agent invite <token>` (sub-verb) or
|
|
183
220
|
// `mnemom agent --invite <token> [session]` — composes with the launch in
|
|
184
221
|
// runAgentInvocation (commands/code-invite.ts, unit-tested); everything below
|
|
@@ -195,6 +232,10 @@ const agentCmd = program
|
|
|
195
232
|
}
|
|
196
233
|
async function launchAgent(scenario) {
|
|
197
234
|
const experimental = loadExperimentalState().enabled;
|
|
235
|
+
// The one place the flag's raw string becomes `number | null` (see the argParser's
|
|
236
|
+
// comment for why commander can't carry `null` through parseArg directly). Already
|
|
237
|
+
// validated by the argParser, so this never throws.
|
|
238
|
+
const compactAtFlag = opts.compactAt !== undefined ? parseCompactAt(opts.compactAt, "--compact-at") : undefined;
|
|
198
239
|
// Sub-verb: `mnemom agent config …` manages persistent settings and never
|
|
199
240
|
// launches. (A scenario literally named "config" is reserved for this.)
|
|
200
241
|
if (scenario === "config") {
|
|
@@ -223,6 +264,14 @@ const agentCmd = program
|
|
|
223
264
|
id: "compaction-intercept",
|
|
224
265
|
used: opts.compactionIntercept === true,
|
|
225
266
|
},
|
|
267
|
+
// --compact-at arms the SAME two gateway keys --compaction-intercept does
|
|
268
|
+
// (buildSessionConfig ORs them) — it needs the identical opt-in, not a free pass
|
|
269
|
+
// around it. `null`/unset ("1m"/"off"/no flag) is not a target, so it never gates.
|
|
270
|
+
{
|
|
271
|
+
what: "--compact-at",
|
|
272
|
+
id: "compaction-intercept",
|
|
273
|
+
used: typeof compactAtFlag === "number",
|
|
274
|
+
},
|
|
226
275
|
], experimental);
|
|
227
276
|
if (opts.gateway)
|
|
228
277
|
process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
|
|
@@ -306,6 +355,10 @@ const agentCmd = program
|
|
|
306
355
|
slack: opts.slack ?? cfg.slackListen,
|
|
307
356
|
context: opts.context,
|
|
308
357
|
contextProfile: opts.contextProfile,
|
|
358
|
+
// Same gate: off ⇒ an explicit null (no target), so MNEMOM_COMPACT_AT cannot arm it —
|
|
359
|
+
// an armed --compact-at was already refused above, and ignoredSettingLines told the
|
|
360
|
+
// user the env value is ignored, so this is what makes that true.
|
|
361
|
+
compactAt: experimental.includes("compaction-intercept") ? compactAtFlag : null,
|
|
309
362
|
// Off ⇒ an explicit false, so MNEMOM_AGENT_CONTEXT_HANDOFF cannot turn it on.
|
|
310
363
|
contextHandoff: experimental.includes("context-handoff") ? opts.contextHandoff : false,
|
|
311
364
|
// Same gate: off ⇒ an explicit false, so MNEMOM_AGENT_COMPACTION_INTERCEPT cannot turn it on.
|
|
@@ -81,6 +81,14 @@ export interface SessionRecord {
|
|
|
81
81
|
* before profiles existed omit it (still `v: 1`).
|
|
82
82
|
*/
|
|
83
83
|
context_profile?: ContextProfile;
|
|
84
|
+
/**
|
|
85
|
+
* The resolved gateway compaction target (`--compact-at` / `compact_at` /
|
|
86
|
+
* `MNEMOM_COMPACT_AT`): `null` means no target (Claude Code's own 1M window; the default,
|
|
87
|
+
* and what `1m`/`off` also resolve to), a number is the token count sent as
|
|
88
|
+
* `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS`. Optional: records written before this setting
|
|
89
|
+
* existed omit it (still `v: 1`).
|
|
90
|
+
*/
|
|
91
|
+
compact_at?: number | null;
|
|
84
92
|
/**
|
|
85
93
|
* The experimental features on for the launch (`mnemom experimental`). Optional and only
|
|
86
94
|
* written when at least one was on; older records omit it (still `v: 1`).
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -322,10 +322,89 @@ export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
|
322
322
|
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
|
|
323
323
|
* - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
|
|
324
324
|
* gateway's own fold digest (no model call), so the compaction never blocks the user.
|
|
325
|
-
* Sent together, value `"on"`,
|
|
325
|
+
* Sent together, value `"on"`, when the `compaction_intercept` setting is on
|
|
326
|
+
* (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
|
|
327
|
+
* target cannot be honored without both.
|
|
326
328
|
*/
|
|
327
329
|
export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
|
|
328
330
|
export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
|
|
331
|
+
export declare const GATEWAY_COMPACTION_UPLOAD_TARGET_KEY = "GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS";
|
|
332
|
+
/** The lowest token count `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` will accept. */
|
|
333
|
+
export declare const COMPACT_AT_MIN_TOKENS = 50000;
|
|
334
|
+
/**
|
|
335
|
+
* The parser's ceiling (exclusive): the largest window Claude Code runs. It is NOT where a target
|
|
336
|
+
* stops working — the gateway ignores a target at or above the compaction point of the window
|
|
337
|
+
* the session actually runs on (COMPACT_AT_TRIGGER_1M / COMPACT_AT_TRIGGER_200K, below), which
|
|
338
|
+
* `compactAtWindowWarning` tells the user about at launch.
|
|
339
|
+
*/
|
|
340
|
+
export declare const COMPACT_AT_MAX_TOKENS = 1000000;
|
|
341
|
+
/**
|
|
342
|
+
* Claude Code's own compaction point on a 1M and on a 200k window — the gateway's
|
|
343
|
+
* `ALIGN_T_1M` / `ALIGN_T_200K` (gateway/src/compaction-align.ts). The gateway honours an upload
|
|
344
|
+
* target only BELOW the point of the window the request runs on (`target_over_threshold`
|
|
345
|
+
* otherwise), and says nothing back to the client when it ignores one.
|
|
346
|
+
*/
|
|
347
|
+
export declare const COMPACT_AT_TRIGGER_1M = 967000;
|
|
348
|
+
export declare const COMPACT_AT_TRIGGER_200K = 167000;
|
|
349
|
+
/** The context window a launch runs on, as far as the launcher can tell before spawning. */
|
|
350
|
+
export type LaunchWindow = "1m" | "200k" | "unknown";
|
|
351
|
+
/**
|
|
352
|
+
* `resolveWindowModel`'s answer as a window. `one-m` and a model already suffixed `[1m]` are
|
|
353
|
+
* 1M; the `CLAUDE_CODE_DISABLE_1M_CONTEXT` opt-out and a model with no 1M window are 200k (the
|
|
354
|
+
* request then carries no 1M beta, so the gateway uses its 200k point too). A passthrough
|
|
355
|
+
* `-- --model`, a resume, Claude Code's own default, any other suffix, and a non-Claude-Code CLI
|
|
356
|
+
* (`null`) are unknown.
|
|
357
|
+
*/
|
|
358
|
+
export declare function launchWindow(win: WindowModel | null): LaunchWindow;
|
|
359
|
+
/**
|
|
360
|
+
* The launch warning for a target the gateway will (or may) ignore because it is at or above
|
|
361
|
+
* the compaction point of the window this launch runs on; `null` when the target can take
|
|
362
|
+
* effect (or there is no target). On an unknown window a target in `[167k, 967k)` works only
|
|
363
|
+
* if the session lands on a 1M window, so the line says so instead of staying silent.
|
|
364
|
+
*/
|
|
365
|
+
export declare function compactAtWindowWarning(compactAt: number | null, window: LaunchWindow): string | null;
|
|
366
|
+
/**
|
|
367
|
+
* Parse a `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` value. `source` names the
|
|
368
|
+
* surface in the error so a customer knows which setting to fix (same convention as
|
|
369
|
+
* `parseContextProfile`).
|
|
370
|
+
*
|
|
371
|
+
* - "" (unset), "1m", "off" (trimmed, case-insensitive) → `null`: no gateway target —
|
|
372
|
+
* Claude Code runs its normal 1M window and nothing new is sent.
|
|
373
|
+
* - a bare integer, or one with a `k`/`m` suffix (`200k` → 200000, `0.5m` → 500000) →
|
|
374
|
+
* that many tokens, when it is in `[COMPACT_AT_MIN_TOKENS, COMPACT_AT_MAX_TOKENS)`.
|
|
375
|
+
* - anything else, or a number outside that range (use `1m`/`off` for the ceiling and
|
|
376
|
+
* above, never a bare `1000000`) → throws with a human-readable message.
|
|
377
|
+
*/
|
|
378
|
+
export declare function parseCompactAt(raw: string, source: string): number | null;
|
|
379
|
+
/**
|
|
380
|
+
* Which compaction target this launch runs under. Precedence, highest wins:
|
|
381
|
+
* 1. `opts.compactAt` — an already-parsed `--compact-at` flag value (`undefined` when the
|
|
382
|
+
* flag was not given at all; `null` is a valid, explicit "no target" from `1m`/`off`).
|
|
383
|
+
* 2. `MNEMOM_COMPACT_AT` (which `applyConfigToEnv` sets from the `compact_at` launcher.toml
|
|
384
|
+
* key, UNDER any real env var — so an operator's own env always wins over the file).
|
|
385
|
+
* 3. `null` — the default: no target, nothing new sent.
|
|
386
|
+
*/
|
|
387
|
+
export declare function resolveCompactAt(opts: {
|
|
388
|
+
compactAt?: number | null;
|
|
389
|
+
env?: NodeJS.ProcessEnv;
|
|
390
|
+
}): number | null;
|
|
391
|
+
/**
|
|
392
|
+
* `--no-compaction-intercept` is a deliberate, documented "keep the intercept off for this
|
|
393
|
+
* launch no matter what toml/env says" signal. A compaction target needs that SAME intercept
|
|
394
|
+
* to arm (`buildSessionConfig` ORs them), so pairing an explicit `--no-compaction-intercept`
|
|
395
|
+
* with an armed `--compact-at` is a contradiction — refuse rather than silently pick a side
|
|
396
|
+
* (either silently re-arming the intercept the user just turned off, or silently sending an
|
|
397
|
+
* inert upload-target the gateway can never act on). `compactionIntercept` here is the raw,
|
|
398
|
+
* pre-`resolveCompactionIntercept` flag value: `false` only when `--no-compaction-intercept`
|
|
399
|
+
* was explicitly passed (or the experimental gate forced it, which `compactAt` cannot reach
|
|
400
|
+
* past either — see index.ts's `assertEnabled`), never the mere absence of `--compaction-intercept`.
|
|
401
|
+
*/
|
|
402
|
+
export declare function assertCompactAtCompatible(opts: {
|
|
403
|
+
compactionIntercept?: boolean;
|
|
404
|
+
compactAt: number | null;
|
|
405
|
+
}): void;
|
|
406
|
+
/** The launch-banner line for the resolved compaction target (always printed). */
|
|
407
|
+
export declare function describeCompactAt(compactAt: number | null): string;
|
|
329
408
|
/**
|
|
330
409
|
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
|
|
331
410
|
* compaction`, `x-claude-code-compaction: auto | manual | reactive`,
|
|
@@ -484,14 +563,18 @@ export declare function resolveContextProfile(opts: {
|
|
|
484
563
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
485
564
|
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
|
|
486
565
|
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
487
|
-
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the
|
|
488
|
-
*
|
|
489
|
-
*
|
|
566
|
+
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
567
|
+
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
568
|
+
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
569
|
+
* when a target is set. Empty for `balanced` with everything off — the common case, where no
|
|
570
|
+
* header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
490
571
|
*/
|
|
491
572
|
export declare function buildSessionConfig(opts: {
|
|
492
573
|
profile: ContextProfile;
|
|
493
574
|
contextHandoff: boolean;
|
|
494
575
|
compactionIntercept?: boolean;
|
|
576
|
+
/** `resolveCompactAt` output: `null` (or absent) ⇒ no target, send nothing new. */
|
|
577
|
+
compactAt?: number | null;
|
|
495
578
|
}): Record<string, string>;
|
|
496
579
|
/**
|
|
497
580
|
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
@@ -502,6 +585,7 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
502
585
|
profile: ContextProfile;
|
|
503
586
|
contextHandoff: boolean;
|
|
504
587
|
compactionIntercept?: boolean;
|
|
588
|
+
compactAt?: number | null;
|
|
505
589
|
experimental?: readonly string[];
|
|
506
590
|
}): {
|
|
507
591
|
v: 1;
|
|
@@ -510,12 +594,15 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
510
594
|
} | undefined;
|
|
511
595
|
/**
|
|
512
596
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
513
|
-
* nothing to override (the gateway's own defaults stand — `balanced` with
|
|
514
|
-
* byte-identical to a launch before profiles existed).
|
|
597
|
+
* nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
|
|
598
|
+
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
515
599
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
516
600
|
* `aggressive` → the three fold-tuning keys, `balanced` → nothing;
|
|
517
601
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
518
602
|
* `"on"` (the `compaction_intercept` setting);
|
|
603
|
+
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
604
|
+
* `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`; unset, `1m` or `off` → nothing beyond
|
|
605
|
+
* what `compactionIntercept` already sent (Claude Code's own 1M window, untouched);
|
|
519
606
|
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
520
607
|
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
521
608
|
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
@@ -526,6 +613,7 @@ export declare function buildSessionConfigB64(opts: {
|
|
|
526
613
|
profile: ContextProfile;
|
|
527
614
|
contextHandoff: boolean;
|
|
528
615
|
compactionIntercept?: boolean;
|
|
616
|
+
compactAt?: number | null;
|
|
529
617
|
experimental?: readonly string[];
|
|
530
618
|
}): string | undefined;
|
|
531
619
|
/**
|
package/dist/lib/agent.js
CHANGED
|
@@ -489,10 +489,175 @@ export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
|
489
489
|
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
|
|
490
490
|
* - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
|
|
491
491
|
* gateway's own fold digest (no model call), so the compaction never blocks the user.
|
|
492
|
-
* Sent together, value `"on"`,
|
|
492
|
+
* Sent together, value `"on"`, when the `compaction_intercept` setting is on
|
|
493
|
+
* (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below) — a
|
|
494
|
+
* target cannot be honored without both.
|
|
493
495
|
*/
|
|
494
496
|
export const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
|
|
495
497
|
export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Compaction target (`--compact-at`, launcher.toml `compact_at`, MNEMOM_COMPACT_AT)
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
//
|
|
502
|
+
// Lets an operator play with WHEN the gateway pulls this session's own client-visible
|
|
503
|
+
// compaction point forward (GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS, #2251), instead of
|
|
504
|
+
// leaving Claude Code to compact on its own 1M-window schedule. Unset / `1m` / `off` all
|
|
505
|
+
// mean the SAME thing — no target — so a launch with nothing else configured sends no new
|
|
506
|
+
// header keys at all and behaves exactly as it did before this setting existed: Claude
|
|
507
|
+
// Code's own 1M window, no gateway-triggered compaction. A value strictly below 1,000,000
|
|
508
|
+
// arms the compaction intercept (USAGE_REAL_SIZE_KEY + COMPACTION_INTERCEPT_KEY, above — the
|
|
509
|
+
// same two keys `--compaction-intercept` arms, #2216, both required for the upload pull to be
|
|
510
|
+
// honored instead of silently inert) plus the target itself. Refold (GATEWAY_COMPACTION_REFOLD)
|
|
511
|
+
// is a SEPARATE, costlier opt-in (+9.5pts measured) — this setting never touches it. This never
|
|
512
|
+
// sets a Claude Code autocompact env/window: Claude Code's own compaction math is untouched;
|
|
513
|
+
// only what the GATEWAY reports back changes.
|
|
514
|
+
export const GATEWAY_COMPACTION_UPLOAD_TARGET_KEY = "GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS";
|
|
515
|
+
/** The lowest token count `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` will accept. */
|
|
516
|
+
export const COMPACT_AT_MIN_TOKENS = 50_000;
|
|
517
|
+
/**
|
|
518
|
+
* The parser's ceiling (exclusive): the largest window Claude Code runs. It is NOT where a target
|
|
519
|
+
* stops working — the gateway ignores a target at or above the compaction point of the window
|
|
520
|
+
* the session actually runs on (COMPACT_AT_TRIGGER_1M / COMPACT_AT_TRIGGER_200K, below), which
|
|
521
|
+
* `compactAtWindowWarning` tells the user about at launch.
|
|
522
|
+
*/
|
|
523
|
+
export const COMPACT_AT_MAX_TOKENS = 1_000_000;
|
|
524
|
+
/**
|
|
525
|
+
* Claude Code's own compaction point on a 1M and on a 200k window — the gateway's
|
|
526
|
+
* `ALIGN_T_1M` / `ALIGN_T_200K` (gateway/src/compaction-align.ts). The gateway honours an upload
|
|
527
|
+
* target only BELOW the point of the window the request runs on (`target_over_threshold`
|
|
528
|
+
* otherwise), and says nothing back to the client when it ignores one.
|
|
529
|
+
*/
|
|
530
|
+
export const COMPACT_AT_TRIGGER_1M = 967_000;
|
|
531
|
+
export const COMPACT_AT_TRIGGER_200K = 167_000;
|
|
532
|
+
/**
|
|
533
|
+
* `resolveWindowModel`'s answer as a window. `one-m` and a model already suffixed `[1m]` are
|
|
534
|
+
* 1M; the `CLAUDE_CODE_DISABLE_1M_CONTEXT` opt-out and a model with no 1M window are 200k (the
|
|
535
|
+
* request then carries no 1M beta, so the gateway uses its 200k point too). A passthrough
|
|
536
|
+
* `-- --model`, a resume, Claude Code's own default, any other suffix, and a non-Claude-Code CLI
|
|
537
|
+
* (`null`) are unknown.
|
|
538
|
+
*/
|
|
539
|
+
export function launchWindow(win) {
|
|
540
|
+
if (win === null)
|
|
541
|
+
return "unknown";
|
|
542
|
+
switch (win.reason) {
|
|
543
|
+
case "one-m":
|
|
544
|
+
return "1m";
|
|
545
|
+
case "suffixed":
|
|
546
|
+
return /\[1m\]\s*$/i.test(win.model ?? "") ? "1m" : "unknown";
|
|
547
|
+
case "opt-out":
|
|
548
|
+
case "unsupported":
|
|
549
|
+
return "200k";
|
|
550
|
+
default:
|
|
551
|
+
return "unknown";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The launch warning for a target the gateway will (or may) ignore because it is at or above
|
|
556
|
+
* the compaction point of the window this launch runs on; `null` when the target can take
|
|
557
|
+
* effect (or there is no target). On an unknown window a target in `[167k, 967k)` works only
|
|
558
|
+
* if the session lands on a 1M window, so the line says so instead of staying silent.
|
|
559
|
+
*/
|
|
560
|
+
export function compactAtWindowWarning(compactAt, window) {
|
|
561
|
+
if (compactAt === null)
|
|
562
|
+
return null;
|
|
563
|
+
const at = formatTokenCount(compactAt);
|
|
564
|
+
const t1m = formatTokenCount(COMPACT_AT_TRIGGER_1M);
|
|
565
|
+
const t200k = formatTokenCount(COMPACT_AT_TRIGGER_200K);
|
|
566
|
+
if (compactAt >= COMPACT_AT_TRIGGER_1M ||
|
|
567
|
+
(window === "200k" && compactAt >= COMPACT_AT_TRIGGER_200K)) {
|
|
568
|
+
const [w, t] = window === "200k" ? ["200k", t200k] : window === "1m" ? ["1M", t1m] : ["any", t1m];
|
|
569
|
+
return (`mnemom agent: warning: compaction target ${at} is at or above Claude Code's own compaction ` +
|
|
570
|
+
`point on ${w === "any" ? "any" : `this launch's ${w}`} window (${t}), so the gateway will ignore it. ` +
|
|
571
|
+
`Pick a value below ${t}.`);
|
|
572
|
+
}
|
|
573
|
+
if (window === "unknown" && compactAt >= COMPACT_AT_TRIGGER_200K) {
|
|
574
|
+
return (`mnemom agent: note: compaction target ${at} takes effect only on a 1M window (below ${t1m}); ` +
|
|
575
|
+
`if this session runs on a 200k window, the gateway's point is ${t200k} and it ignores the target.`);
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Parse a `--compact-at` / `compact_at` / `MNEMOM_COMPACT_AT` value. `source` names the
|
|
581
|
+
* surface in the error so a customer knows which setting to fix (same convention as
|
|
582
|
+
* `parseContextProfile`).
|
|
583
|
+
*
|
|
584
|
+
* - "" (unset), "1m", "off" (trimmed, case-insensitive) → `null`: no gateway target —
|
|
585
|
+
* Claude Code runs its normal 1M window and nothing new is sent.
|
|
586
|
+
* - a bare integer, or one with a `k`/`m` suffix (`200k` → 200000, `0.5m` → 500000) →
|
|
587
|
+
* that many tokens, when it is in `[COMPACT_AT_MIN_TOKENS, COMPACT_AT_MAX_TOKENS)`.
|
|
588
|
+
* - anything else, or a number outside that range (use `1m`/`off` for the ceiling and
|
|
589
|
+
* above, never a bare `1000000`) → throws with a human-readable message.
|
|
590
|
+
*/
|
|
591
|
+
export function parseCompactAt(raw, source) {
|
|
592
|
+
const v = raw.trim().toLowerCase();
|
|
593
|
+
if (v === "" || v === "1m" || v === "off")
|
|
594
|
+
return null;
|
|
595
|
+
const m = /^(\d+(?:\.\d+)?)(k|m)?$/.exec(v);
|
|
596
|
+
if (!m) {
|
|
597
|
+
throw new Error(`${source} must be a token count (optionally with a k/m suffix), '1m', or 'off' (got '${raw}').`);
|
|
598
|
+
}
|
|
599
|
+
const mult = m[2] === "k" ? 1_000 : m[2] === "m" ? 1_000_000 : 1;
|
|
600
|
+
const tokens = Math.round(Number(m[1]) * mult);
|
|
601
|
+
if (!Number.isFinite(tokens) ||
|
|
602
|
+
tokens < COMPACT_AT_MIN_TOKENS ||
|
|
603
|
+
tokens >= COMPACT_AT_MAX_TOKENS) {
|
|
604
|
+
throw new Error(`${source} must be between ${COMPACT_AT_MIN_TOKENS} and ${COMPACT_AT_MAX_TOKENS - 1} tokens` +
|
|
605
|
+
` (or '1m' / 'off' for no gateway compaction target); got '${raw}'.`);
|
|
606
|
+
}
|
|
607
|
+
return tokens;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Which compaction target this launch runs under. Precedence, highest wins:
|
|
611
|
+
* 1. `opts.compactAt` — an already-parsed `--compact-at` flag value (`undefined` when the
|
|
612
|
+
* flag was not given at all; `null` is a valid, explicit "no target" from `1m`/`off`).
|
|
613
|
+
* 2. `MNEMOM_COMPACT_AT` (which `applyConfigToEnv` sets from the `compact_at` launcher.toml
|
|
614
|
+
* key, UNDER any real env var — so an operator's own env always wins over the file).
|
|
615
|
+
* 3. `null` — the default: no target, nothing new sent.
|
|
616
|
+
*/
|
|
617
|
+
export function resolveCompactAt(opts) {
|
|
618
|
+
if (opts.compactAt !== undefined)
|
|
619
|
+
return opts.compactAt;
|
|
620
|
+
const raw = (opts.env?.MNEMOM_COMPACT_AT ?? "").trim();
|
|
621
|
+
if (raw === "")
|
|
622
|
+
return null;
|
|
623
|
+
return parseCompactAt(raw, "MNEMOM_COMPACT_AT");
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* `--no-compaction-intercept` is a deliberate, documented "keep the intercept off for this
|
|
627
|
+
* launch no matter what toml/env says" signal. A compaction target needs that SAME intercept
|
|
628
|
+
* to arm (`buildSessionConfig` ORs them), so pairing an explicit `--no-compaction-intercept`
|
|
629
|
+
* with an armed `--compact-at` is a contradiction — refuse rather than silently pick a side
|
|
630
|
+
* (either silently re-arming the intercept the user just turned off, or silently sending an
|
|
631
|
+
* inert upload-target the gateway can never act on). `compactionIntercept` here is the raw,
|
|
632
|
+
* pre-`resolveCompactionIntercept` flag value: `false` only when `--no-compaction-intercept`
|
|
633
|
+
* was explicitly passed (or the experimental gate forced it, which `compactAt` cannot reach
|
|
634
|
+
* past either — see index.ts's `assertEnabled`), never the mere absence of `--compaction-intercept`.
|
|
635
|
+
*/
|
|
636
|
+
export function assertCompactAtCompatible(opts) {
|
|
637
|
+
if (opts.compactionIntercept === false && opts.compactAt !== null) {
|
|
638
|
+
throw new Error("mnemom agent: --compact-at needs the compaction intercept to arm, but it is off for " +
|
|
639
|
+
"this launch (--no-compaction-intercept, or the compaction-intercept experimental " +
|
|
640
|
+
"feature is disabled). Drop --no-compaction-intercept (and run `mnemom experimental " +
|
|
641
|
+
"enable compaction-intercept`), or pass --compact-at off.");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* `200000` → `"200k"`, anything not k-aligned → the bare digits. Every real caller only ever
|
|
646
|
+
* passes a `parseCompactAt`-validated value, which is bounded to
|
|
647
|
+
* `[COMPACT_AT_MIN_TOKENS, COMPACT_AT_MAX_TOKENS)` — i.e. `[50_000, 999_999]` — so there is no
|
|
648
|
+
* multiple of 1,000,000 to round to an `"Nm"` form; that case is intentionally not handled here.
|
|
649
|
+
*/
|
|
650
|
+
function formatTokenCount(tokens) {
|
|
651
|
+
if (tokens % 1_000 === 0)
|
|
652
|
+
return `${tokens / 1_000}k`;
|
|
653
|
+
return String(tokens);
|
|
654
|
+
}
|
|
655
|
+
/** The launch-banner line for the resolved compaction target (always printed). */
|
|
656
|
+
export function describeCompactAt(compactAt) {
|
|
657
|
+
return compactAt === null
|
|
658
|
+
? "Claude Code default (1M)"
|
|
659
|
+
: `gateway at ${formatTokenCount(compactAt)}`;
|
|
660
|
+
}
|
|
496
661
|
/**
|
|
497
662
|
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
|
|
498
663
|
* compaction`, `x-claude-code-compaction: auto | manual | reactive`,
|
|
@@ -768,9 +933,11 @@ export function resolveContextProfile(opts) {
|
|
|
768
933
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
769
934
|
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
|
|
770
935
|
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
771
|
-
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the
|
|
772
|
-
*
|
|
773
|
-
*
|
|
936
|
+
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
937
|
+
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
|
|
938
|
+
* cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
|
|
939
|
+
* when a target is set. Empty for `balanced` with everything off — the common case, where no
|
|
940
|
+
* header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
774
941
|
*/
|
|
775
942
|
export function buildSessionConfig(opts) {
|
|
776
943
|
const config = {};
|
|
@@ -778,10 +945,17 @@ export function buildSessionConfig(opts) {
|
|
|
778
945
|
config[k] = v;
|
|
779
946
|
if (opts.contextHandoff)
|
|
780
947
|
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
781
|
-
|
|
948
|
+
const hasCompactionTarget = opts.compactAt !== undefined && opts.compactAt !== null;
|
|
949
|
+
// A target needs the compaction intercept + real-size rewrite to arm (#2216) — without them
|
|
950
|
+
// the upload-target key is silently inert (compaction-align.ts: "armed only inside the
|
|
951
|
+
// intercept"). Deliberately NOT GATEWAY_COMPACTION_REFOLD — a separate, costlier opt-in.
|
|
952
|
+
if (opts.compactionIntercept || hasCompactionTarget) {
|
|
782
953
|
config[USAGE_REAL_SIZE_KEY] = "on";
|
|
783
954
|
config[COMPACTION_INTERCEPT_KEY] = "on";
|
|
784
955
|
}
|
|
956
|
+
if (hasCompactionTarget) {
|
|
957
|
+
config[GATEWAY_COMPACTION_UPLOAD_TARGET_KEY] = String(opts.compactAt);
|
|
958
|
+
}
|
|
785
959
|
return config;
|
|
786
960
|
}
|
|
787
961
|
/**
|
|
@@ -800,12 +974,15 @@ export function buildSessionConfigPayload(opts) {
|
|
|
800
974
|
}
|
|
801
975
|
/**
|
|
802
976
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
803
|
-
* nothing to override (the gateway's own defaults stand — `balanced` with
|
|
804
|
-
* byte-identical to a launch before profiles existed).
|
|
977
|
+
* nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
|
|
978
|
+
* byte-identical to a launch before profiles existed). Independent overlays:
|
|
805
979
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
806
980
|
* `aggressive` → the three fold-tuning keys, `balanced` → nothing;
|
|
807
981
|
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
808
982
|
* `"on"` (the `compaction_intercept` setting);
|
|
983
|
+
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
984
|
+
* `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`; unset, `1m` or `off` → nothing beyond
|
|
985
|
+
* what `compactionIntercept` already sent (Claude Code's own 1M window, untouched);
|
|
809
986
|
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
810
987
|
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
811
988
|
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
* modules are the thin drivers.
|
|
14
14
|
*/
|
|
15
15
|
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "compaction-intercept";
|
|
16
|
+
/**
|
|
17
|
+
* Is a `compact_at` / `MNEMOM_COMPACT_AT` value (trimmed, case-insensitive) an ARMED gateway
|
|
18
|
+
* target, as opposed to "no target" (`""`, `"1m"`, `"off"`)? Deliberately duplicates
|
|
19
|
+
* `parseCompactAt`'s no-target spellings rather than importing it, to keep this registry
|
|
20
|
+
* dependency-free (see the module doc); an unparseable value (bad k/m suffix, out of range)
|
|
21
|
+
* is treated as armed so the gate errs toward requiring the opt-in rather than toward silently
|
|
22
|
+
* admitting a value the real parser would reject anyway.
|
|
23
|
+
*/
|
|
24
|
+
export declare function compactAtArmed(raw: string | undefined): boolean;
|
|
16
25
|
export interface ExperimentalFeature {
|
|
17
26
|
id: ExperimentalId;
|
|
18
27
|
summary: string;
|
|
@@ -73,8 +82,8 @@ export declare function assertEnabled(uses: ReadonlyArray<{
|
|
|
73
82
|
export declare function experimentalIdForConfigKey(key: string, value: string): ExperimentalId | undefined;
|
|
74
83
|
/**
|
|
75
84
|
* The stderr lines for experimental settings a launch ignores because their feature
|
|
76
|
-
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF
|
|
77
|
-
* MNEMOM_AGENT_COMPACTION_INTERCEPT.
|
|
85
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF,
|
|
86
|
+
* MNEMOM_AGENT_COMPACTION_INTERCEPT and MNEMOM_COMPACT_AT (an armed target only).
|
|
78
87
|
*/
|
|
79
88
|
export declare function ignoredSettingLines(opts: {
|
|
80
89
|
entries: ReadonlyArray<[string, string]>;
|
package/dist/lib/experimental.js
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
* Everything here is pure or dependency-injected; index.ts and the command
|
|
13
13
|
* modules are the thin drivers.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Is a `compact_at` / `MNEMOM_COMPACT_AT` value (trimmed, case-insensitive) an ARMED gateway
|
|
17
|
+
* target, as opposed to "no target" (`""`, `"1m"`, `"off"`)? Deliberately duplicates
|
|
18
|
+
* `parseCompactAt`'s no-target spellings rather than importing it, to keep this registry
|
|
19
|
+
* dependency-free (see the module doc); an unparseable value (bad k/m suffix, out of range)
|
|
20
|
+
* is treated as armed so the gate errs toward requiring the opt-in rather than toward silently
|
|
21
|
+
* admitting a value the real parser would reject anyway.
|
|
22
|
+
*/
|
|
23
|
+
export function compactAtArmed(raw) {
|
|
24
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
25
|
+
return v !== "" && v !== "1m" && v !== "off";
|
|
26
|
+
}
|
|
15
27
|
export const EXPERIMENTAL_FEATURES = [
|
|
16
28
|
{
|
|
17
29
|
id: "context-handoff",
|
|
@@ -140,6 +152,9 @@ export function experimentalIdForConfigKey(key, value) {
|
|
|
140
152
|
return value === "true" ? "context-handoff" : undefined;
|
|
141
153
|
case "compaction_intercept":
|
|
142
154
|
return value === "true" ? "compaction-intercept" : undefined;
|
|
155
|
+
case "compact_at":
|
|
156
|
+
// Arms the SAME gateway keys compaction_intercept does — same gate.
|
|
157
|
+
return compactAtArmed(value) ? "compaction-intercept" : undefined;
|
|
143
158
|
case "goal_mode":
|
|
144
159
|
return value === "implicit" ? "implicit" : undefined;
|
|
145
160
|
default:
|
|
@@ -152,8 +167,8 @@ function truthy(raw) {
|
|
|
152
167
|
}
|
|
153
168
|
/**
|
|
154
169
|
* The stderr lines for experimental settings a launch ignores because their feature
|
|
155
|
-
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF
|
|
156
|
-
* MNEMOM_AGENT_COMPACTION_INTERCEPT.
|
|
170
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF,
|
|
171
|
+
* MNEMOM_AGENT_COMPACTION_INTERCEPT and MNEMOM_COMPACT_AT (an armed target only).
|
|
157
172
|
*/
|
|
158
173
|
export function ignoredSettingLines(opts) {
|
|
159
174
|
const lines = [];
|
|
@@ -170,6 +185,10 @@ export function ignoredSettingLines(opts) {
|
|
|
170
185
|
!opts.enabled.includes("compaction-intercept")) {
|
|
171
186
|
lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_COMPACTION_INTERCEPT", "compaction-intercept")}`);
|
|
172
187
|
}
|
|
188
|
+
if (compactAtArmed(opts.env.MNEMOM_COMPACT_AT) &&
|
|
189
|
+
!opts.enabled.includes("compaction-intercept")) {
|
|
190
|
+
lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_COMPACT_AT", "compaction-intercept")}`);
|
|
191
|
+
}
|
|
173
192
|
return lines;
|
|
174
193
|
}
|
|
175
194
|
/** The features an existing launcher.toml already uses (the one-time migration). */
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* the caller folds the plain-value settings under the CLI flags.
|
|
17
17
|
*/
|
|
18
18
|
import { type AgentAuth } from "./subscription-login.js";
|
|
19
|
-
import type
|
|
19
|
+
import { type ExperimentalId } from "./experimental.js";
|
|
20
20
|
import { type ContextProfile, type GoalModeDefault, type LaunchShape } from "./agent.js";
|
|
21
21
|
/** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
|
|
22
22
|
export declare const LAUNCHER_CONFIG_PATH: string;
|
|
@@ -61,6 +61,12 @@ export interface LauncherConfig {
|
|
|
61
61
|
* folds earlier with a tighter working set. `--context-profile` overrides it per launch.
|
|
62
62
|
*/
|
|
63
63
|
contextProfile?: ContextProfile;
|
|
64
|
+
/**
|
|
65
|
+
* The gateway compaction target (`--compact-at`; unset means no target — Claude Code's own
|
|
66
|
+
* 1M window, nothing new sent). Stored as the validated raw string (e.g. "200k", "1m",
|
|
67
|
+
* "off", "350000"), parsed with `parseCompactAt` at the point of use.
|
|
68
|
+
*/
|
|
69
|
+
compactAt?: string;
|
|
64
70
|
guardrails?: {
|
|
65
71
|
maxTurns?: number;
|
|
66
72
|
budgetUsd?: number;
|
|
@@ -113,6 +119,9 @@ export declare function launcherEntries(cfg: LauncherConfig): Array<[string, str
|
|
|
113
119
|
/**
|
|
114
120
|
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
115
121
|
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
116
|
-
* off, `compaction_intercept`
|
|
122
|
+
* off, `compaction_intercept` (and an ARMED `compact_at` — it arms the same two gateway keys,
|
|
123
|
+
* so it needs the same opt-in; `compact_at = "off"`/`"1m"` carries no gated behaviour and is
|
|
124
|
+
* kept) while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is
|
|
125
|
+
* off (`goal_mode = off` is kept).
|
|
117
126
|
*/
|
|
118
127
|
export declare function withoutDisabledExperimental(cfg: LauncherConfig, enabled: readonly ExperimentalId[]): LauncherConfig;
|
|
@@ -20,7 +20,8 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
import { parse as parseToml, stringify as stringifyToml, TomlError } from "smol-toml";
|
|
22
22
|
import { MNEMOM_DIR } from "./config.js";
|
|
23
|
-
import {
|
|
23
|
+
import { compactAtArmed } from "./experimental.js";
|
|
24
|
+
import { CONTEXT_PROFILES, isPositiveInt, isPositiveNumber, parseCompactAt, parseContextProfile, } from "./agent.js";
|
|
24
25
|
/** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
|
|
25
26
|
export const LAUNCHER_CONFIG_PATH = join(MNEMOM_DIR, "launcher.toml");
|
|
26
27
|
const LAUNCH_SHAPES = ["terminal", "remote-control", "server"];
|
|
@@ -67,6 +68,13 @@ function coerceNonEmpty(name, raw) {
|
|
|
67
68
|
throw new Error(`${name} must be a non-empty string.`);
|
|
68
69
|
return v;
|
|
69
70
|
}
|
|
71
|
+
function coerceCompactAt(raw) {
|
|
72
|
+
const v = raw.trim().toLowerCase();
|
|
73
|
+
// Validate against the same parser the flag/env use, but keep the human-chosen spelling
|
|
74
|
+
// ("200k" vs "200000") — resolveCompactAt re-parses it at launch time anyway.
|
|
75
|
+
parseCompactAt(v, "compact_at");
|
|
76
|
+
return v;
|
|
77
|
+
}
|
|
70
78
|
function coerceMemberIds(raw) {
|
|
71
79
|
const ids = raw
|
|
72
80
|
.split(/[\s,]+/)
|
|
@@ -213,6 +221,14 @@ export const CONFIG_KEYS = [
|
|
|
213
221
|
c.contextProfile = parseContextProfile(v, "context_profile");
|
|
214
222
|
},
|
|
215
223
|
},
|
|
224
|
+
{
|
|
225
|
+
key: "compact_at",
|
|
226
|
+
describe: "Gateway compaction target: a token count (k/m suffix OK), '1m' or 'off' for none (default: none — Claude Code's own 1M window)",
|
|
227
|
+
get: (c) => c.compactAt,
|
|
228
|
+
set: (c, v) => {
|
|
229
|
+
c.compactAt = coerceCompactAt(v);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
216
232
|
{
|
|
217
233
|
key: "guardrails.max_turns",
|
|
218
234
|
describe: "Contract guardrail: turn ceiling (positive integer)",
|
|
@@ -297,6 +313,8 @@ export function parseLauncherConfig(text) {
|
|
|
297
313
|
cfg.compactionIntercept = coerceBool("compaction_intercept", str(raw.compaction_intercept) ?? "");
|
|
298
314
|
if (raw.context_profile !== undefined)
|
|
299
315
|
cfg.contextProfile = parseContextProfile(str(raw.context_profile) ?? "", "context_profile");
|
|
316
|
+
if (raw.compact_at !== undefined)
|
|
317
|
+
cfg.compactAt = coerceCompactAt(str(raw.compact_at) ?? "");
|
|
300
318
|
const g = raw.guardrails;
|
|
301
319
|
if (g !== undefined) {
|
|
302
320
|
if (typeof g !== "object" || g === null || Array.isArray(g)) {
|
|
@@ -371,6 +389,8 @@ export function serializeLauncherConfig(cfg) {
|
|
|
371
389
|
doc.compaction_intercept = cfg.compactionIntercept;
|
|
372
390
|
if (cfg.contextProfile !== undefined)
|
|
373
391
|
doc.context_profile = cfg.contextProfile;
|
|
392
|
+
if (cfg.compactAt !== undefined)
|
|
393
|
+
doc.compact_at = cfg.compactAt;
|
|
374
394
|
const g = cfg.guardrails;
|
|
375
395
|
if (g && (g.maxTurns !== undefined || g.budgetUsd !== undefined || g.stallTurns !== undefined)) {
|
|
376
396
|
const gr = {};
|
|
@@ -433,6 +453,9 @@ export function applyConfigToEnv(cfg, env = process.env) {
|
|
|
433
453
|
if (cfg.contextProfile !== undefined && !(env.MNEMOM_AGENT_CONTEXT_PROFILE ?? "").trim()) {
|
|
434
454
|
env.MNEMOM_AGENT_CONTEXT_PROFILE = cfg.contextProfile;
|
|
435
455
|
}
|
|
456
|
+
if (cfg.compactAt !== undefined && !(env.MNEMOM_COMPACT_AT ?? "").trim()) {
|
|
457
|
+
env.MNEMOM_COMPACT_AT = cfg.compactAt;
|
|
458
|
+
}
|
|
436
459
|
}
|
|
437
460
|
/** The set keys of `cfg` as `[key, value]` pairs (CONFIG_KEYS order, values stringified). */
|
|
438
461
|
export function launcherEntries(cfg) {
|
|
@@ -444,7 +467,10 @@ export function launcherEntries(cfg) {
|
|
|
444
467
|
/**
|
|
445
468
|
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
446
469
|
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
447
|
-
* off, `compaction_intercept`
|
|
470
|
+
* off, `compaction_intercept` (and an ARMED `compact_at` — it arms the same two gateway keys,
|
|
471
|
+
* so it needs the same opt-in; `compact_at = "off"`/`"1m"` carries no gated behaviour and is
|
|
472
|
+
* kept) while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is
|
|
473
|
+
* off (`goal_mode = off` is kept).
|
|
448
474
|
*/
|
|
449
475
|
export function withoutDisabledExperimental(cfg, enabled) {
|
|
450
476
|
const out = { ...cfg };
|
|
@@ -455,8 +481,11 @@ export function withoutDisabledExperimental(cfg, enabled) {
|
|
|
455
481
|
}
|
|
456
482
|
if (!enabled.includes("context-handoff"))
|
|
457
483
|
delete out.contextHandoff;
|
|
458
|
-
if (!enabled.includes("compaction-intercept"))
|
|
484
|
+
if (!enabled.includes("compaction-intercept")) {
|
|
459
485
|
delete out.compactionIntercept;
|
|
486
|
+
if (compactAtArmed(out.compactAt))
|
|
487
|
+
delete out.compactAt;
|
|
488
|
+
}
|
|
460
489
|
if (!enabled.includes("implicit") && out.goalMode === "implicit")
|
|
461
490
|
delete out.goalMode;
|
|
462
491
|
return out;
|