@mnemom/mnemom 0.17.4-next.6 → 0.17.4-next.7
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 +19 -0
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent.d.ts +33 -4
- package/dist/commands/agent.js +99 -13
- package/dist/index.js +11 -0
- package/dist/lib/agent.d.ts +55 -11
- package/dist/lib/agent.js +68 -11
- package/dist/lib/experimental.d.ts +3 -2
- package/dist/lib/experimental.js +22 -2
- package/dist/lib/launcher-config.d.ts +7 -1
- package/dist/lib/launcher-config.js +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,25 @@ mnemom agent config set context_handoff off # back to the default
|
|
|
129
129
|
This is experimental: it only ever ADDS the one tool (no gateway behavior changes for a
|
|
130
130
|
session that leaves the setting off, and no change to Claude Code's own `/compact`).
|
|
131
131
|
|
|
132
|
+
#### Compaction signal and intercept
|
|
133
|
+
|
|
134
|
+
Every launch shape (terminal, `-- -p …` headless, `--remote-control`, `--server`) starts Claude
|
|
135
|
+
Code with `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`, so its requests carry the
|
|
136
|
+
`x-claude-code-request-class` / `x-claude-code-compaction` hint headers, which mark exactly
|
|
137
|
+
which request is a compaction (a gateway that does not read them ignores them). A value you set yourself (even `0`) wins. The launcher
|
|
138
|
+
never sets or changes your own auto-compaction settings (`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`,
|
|
139
|
+
`CLAUDE_CODE_AUTO_COMPACT_WINDOW`, or `autoCompactWindow` in settings.json).
|
|
140
|
+
|
|
141
|
+
**Experimental, off by default.** `--compaction-intercept` (after `mnemom experimental enable
|
|
142
|
+
compaction-intercept`; standing default `mnemom agent config set compaction_intercept on`, or
|
|
143
|
+
`MNEMOM_AGENT_COMPACTION_INTERCEPT=1`) sends `GATEWAY_USAGE_REAL_SIZE=on` and
|
|
144
|
+
`GATEWAY_COMPACTION_INTERCEPT=on` in the per-session config header, asking the gateway to report
|
|
145
|
+
the real context size (so Claude Code compacts at its own threshold) and to answer that
|
|
146
|
+
compaction at once from its fold instead of a blocking model call. This needs the companion
|
|
147
|
+
gateway support: a gateway without it drops both keys (they are echoed back in
|
|
148
|
+
`x-mnemom-session-config-dropped`) and the session behaves exactly as with the setting off. `--no-compaction-intercept` turns it off for
|
|
149
|
+
one launch.
|
|
150
|
+
|
|
132
151
|
## How It Works
|
|
133
152
|
|
|
134
153
|
```
|
|
@@ -193,6 +193,9 @@ function deleteKey(cfg, key) {
|
|
|
193
193
|
case "context_handoff":
|
|
194
194
|
delete cfg.contextHandoff;
|
|
195
195
|
break;
|
|
196
|
+
case "compaction_intercept":
|
|
197
|
+
delete cfg.compactionIntercept;
|
|
198
|
+
break;
|
|
196
199
|
case "context_profile":
|
|
197
200
|
delete cfg.contextProfile;
|
|
198
201
|
break;
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -79,6 +79,13 @@ export interface AgentOptions {
|
|
|
79
79
|
* ⇒ no header key, no MCP server — the launch is byte-identical to a build without this flag.
|
|
80
80
|
*/
|
|
81
81
|
contextHandoff?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* EXPERIMENTAL, off by default (payload-bound fix): tells the gateway
|
|
84
|
+
* `GATEWAY_USAGE_REAL_SIZE=on` + `GATEWAY_COMPACTION_INTERCEPT=on` for this session only (real
|
|
85
|
+
* context size in usage, so Claude Code compacts at its OWN threshold; the compaction request
|
|
86
|
+
* answered at once from the gateway's fold). Absent/false ⇒ no header key.
|
|
87
|
+
*/
|
|
88
|
+
compactionIntercept?: boolean;
|
|
82
89
|
/**
|
|
83
90
|
* Governed-agent provisioning: undefined = AUTO (provision-with-confirm when
|
|
84
91
|
* interactive, warn-and-continue otherwise), true = force (`--setup`), false =
|
|
@@ -116,11 +123,30 @@ export declare function passthroughResumeId(passthrough: readonly string[]): str
|
|
|
116
123
|
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
117
124
|
* (else the newest matching row), reused without asking;
|
|
118
125
|
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
119
|
-
* conversation, n mints a new one.
|
|
120
|
-
* resumes inside it with /resume). No TTY: Y.
|
|
126
|
+
* conversation, n mints a new one. No TTY: Y.
|
|
121
127
|
* The ledger read is best-effort: a failure means a new id.
|
|
122
128
|
*/
|
|
123
|
-
export declare function resolveLaunchConversationId(input:
|
|
129
|
+
export declare function resolveLaunchConversationId(input: LaunchContinuationInput): Promise<string>;
|
|
130
|
+
/**
|
|
131
|
+
* Whether Claude Code still has the transcript of session `id`
|
|
132
|
+
* (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
|
|
133
|
+
* project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
|
|
134
|
+
* resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
|
|
135
|
+
*/
|
|
136
|
+
export declare function ccSessionTranscriptExists(id: string, env: NodeJS.ProcessEnv): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
|
|
139
|
+
* of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
|
|
140
|
+
* when the caller passed no session flag of their own: the gateway gives the conversation's main
|
|
141
|
+
* goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
|
|
142
|
+
* in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
|
|
143
|
+
* conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
|
|
144
|
+
*/
|
|
145
|
+
export declare function resolveLaunchContinuation(input: LaunchContinuationInput): Promise<{
|
|
146
|
+
conversationId: string;
|
|
147
|
+
resumeCcSessionId: string | null;
|
|
148
|
+
}>;
|
|
149
|
+
interface LaunchContinuationInput {
|
|
124
150
|
session: string;
|
|
125
151
|
orgId: string;
|
|
126
152
|
agent: string;
|
|
@@ -131,7 +157,9 @@ export declare function resolveLaunchConversationId(input: {
|
|
|
131
157
|
interactive: boolean;
|
|
132
158
|
records: () => SessionRecord[];
|
|
133
159
|
askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
134
|
-
|
|
160
|
+
/** Whether Claude Code still has that session's transcript (ccSessionTranscriptExists). */
|
|
161
|
+
ccSessionExists: (ccSessionId: string) => boolean;
|
|
162
|
+
}
|
|
135
163
|
/**
|
|
136
164
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
137
165
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -318,3 +346,4 @@ export declare function printPlan(ctx: {
|
|
|
318
346
|
/** The experimental features on for this launch (the header's `experimental` array). */
|
|
319
347
|
experimental?: readonly string[];
|
|
320
348
|
}): void;
|
|
349
|
+
export {};
|
package/dist/commands/agent.js
CHANGED
|
@@ -43,12 +43,14 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
|
43
43
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
44
44
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
45
45
|
import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
46
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, 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,
|
|
46
|
+
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";
|
|
47
47
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
48
48
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
49
49
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
50
50
|
const CODE_STORE = LAUNCHER_STORE_PATH;
|
|
51
51
|
const say = (line = "") => console.error(line);
|
|
52
|
+
/** The one-line launch note when the compaction intercept is ON. */
|
|
53
|
+
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.";
|
|
52
54
|
/**
|
|
53
55
|
* The one-line launch note for a non-default context profile (`balanced`, the default, is
|
|
54
56
|
* silent — nothing is sent). `off` keeps the wording `--no-context` has always printed.
|
|
@@ -149,11 +151,20 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
149
151
|
say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
|
|
150
152
|
contextHandoff = false;
|
|
151
153
|
}
|
|
154
|
+
const compactionIntercept = resolveCompactionIntercept({
|
|
155
|
+
compactionIntercept: options.compactionIntercept,
|
|
156
|
+
env,
|
|
157
|
+
});
|
|
152
158
|
const experimental = options.experimental ?? [];
|
|
153
|
-
const sessionConfig = buildSessionConfig({
|
|
159
|
+
const sessionConfig = buildSessionConfig({
|
|
160
|
+
profile: contextProfile,
|
|
161
|
+
contextHandoff,
|
|
162
|
+
compactionIntercept,
|
|
163
|
+
});
|
|
154
164
|
const sessionConfigB64 = buildSessionConfigB64({
|
|
155
165
|
profile: contextProfile,
|
|
156
166
|
contextHandoff,
|
|
167
|
+
compactionIntercept,
|
|
157
168
|
experimental,
|
|
158
169
|
});
|
|
159
170
|
const expNotice = experimentalNotice(experimental);
|
|
@@ -212,6 +223,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
212
223
|
if (contextHandoff) {
|
|
213
224
|
say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
|
|
214
225
|
}
|
|
226
|
+
if (compactionIntercept)
|
|
227
|
+
say(COMPACTION_INTERCEPT_NOTE);
|
|
215
228
|
return;
|
|
216
229
|
}
|
|
217
230
|
// Real launch: make sure we actually have a CLI to run. If we couldn't find it,
|
|
@@ -330,7 +343,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
330
343
|
// so the goal the gateway inferred there is still the one it steers by. Resolved here, after
|
|
331
344
|
// sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
|
|
332
345
|
// match the earlier launch.
|
|
333
|
-
const
|
|
346
|
+
const continuation = await resolveLaunchContinuation({
|
|
334
347
|
session: scenario,
|
|
335
348
|
orgId: account.orgId,
|
|
336
349
|
agent,
|
|
@@ -341,7 +354,19 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
341
354
|
interactive: isInteractive(),
|
|
342
355
|
records: () => readSessionRecords({ dir: MNEMOM_DIR }),
|
|
343
356
|
askYesNo: (q, d) => askYesNo(q, d),
|
|
357
|
+
ccSessionExists: (id) => ccSessionTranscriptExists(id, process.env),
|
|
344
358
|
});
|
|
359
|
+
const conversationId = continuation.conversationId;
|
|
360
|
+
// A relaunch that continues an earlier conversation resumes that Claude Code session, so the
|
|
361
|
+
// same CC session id keeps owning the conversation's main goal on the gateway (a fresh
|
|
362
|
+
// --session-id would land on a new, empty m.<sha16> thread). The server shape hosts many
|
|
363
|
+
// sessions and pins none.
|
|
364
|
+
if (continuation.resumeCcSessionId && shape !== "server") {
|
|
365
|
+
options = {
|
|
366
|
+
...options,
|
|
367
|
+
passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
345
370
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
346
371
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
347
372
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -389,6 +414,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
389
414
|
say(expNotice);
|
|
390
415
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
391
416
|
sayContextProfile(contextProfile);
|
|
417
|
+
if (compactionIntercept)
|
|
418
|
+
say(COMPACTION_INTERCEPT_NOTE);
|
|
392
419
|
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
393
420
|
// undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
|
|
394
421
|
// the config is written to a 0600 temp file and referenced by PATH only — never inlined into
|
|
@@ -489,21 +516,62 @@ export function passthroughResumeId(passthrough) {
|
|
|
489
516
|
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
490
517
|
* (else the newest matching row), reused without asking;
|
|
491
518
|
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
492
|
-
* conversation, n mints a new one.
|
|
493
|
-
* resumes inside it with /resume). No TTY: Y.
|
|
519
|
+
* conversation, n mints a new one. No TTY: Y.
|
|
494
520
|
* The ledger read is best-effort: a failure means a new id.
|
|
495
521
|
*/
|
|
496
522
|
export async function resolveLaunchConversationId(input) {
|
|
497
|
-
|
|
498
|
-
|
|
523
|
+
return (await resolveLaunchContinuation(input)).conversationId;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Whether the caller's passthrough already chooses the Claude Code session (`--session-id`,
|
|
527
|
+
* `--resume` / `-r`, `--continue` / `-c`, `--fork-session`): exactly when pickCcSessionId would
|
|
528
|
+
* NOT mint one, so the two never disagree about which flags count.
|
|
529
|
+
*/
|
|
530
|
+
function choosesCcSession(passthrough) {
|
|
531
|
+
return !pickCcSessionId([...passthrough], () => "").inject;
|
|
532
|
+
}
|
|
533
|
+
const CC_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
534
|
+
/**
|
|
535
|
+
* Whether Claude Code still has the transcript of session `id`
|
|
536
|
+
* (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
|
|
537
|
+
* project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
|
|
538
|
+
* resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
|
|
539
|
+
*/
|
|
540
|
+
export function ccSessionTranscriptExists(id, env) {
|
|
541
|
+
if (!CC_SESSION_ID_RE.test(id))
|
|
542
|
+
return false;
|
|
543
|
+
const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
|
|
544
|
+
try {
|
|
545
|
+
return readdirSync(projects).some((d) => existsSync(join(projects, d, `${id}.jsonl`)));
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
|
|
553
|
+
* of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
|
|
554
|
+
* when the caller passed no session flag of their own: the gateway gives the conversation's main
|
|
555
|
+
* goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
|
|
556
|
+
* in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
|
|
557
|
+
* conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
|
|
558
|
+
*/
|
|
559
|
+
export async function resolveLaunchContinuation(input) {
|
|
560
|
+
const fresh = () => ({
|
|
561
|
+
conversationId: resolveConversationId(undefined),
|
|
562
|
+
resumeCcSessionId: null,
|
|
563
|
+
});
|
|
564
|
+
if ((input.explicit ?? "").trim()) {
|
|
565
|
+
return { conversationId: resolveConversationId(input.explicit), resumeCcSessionId: null };
|
|
566
|
+
}
|
|
499
567
|
if (!input.implicit)
|
|
500
|
-
return
|
|
568
|
+
return fresh();
|
|
501
569
|
let records;
|
|
502
570
|
try {
|
|
503
571
|
records = input.records();
|
|
504
572
|
}
|
|
505
573
|
catch {
|
|
506
|
-
return
|
|
574
|
+
return fresh();
|
|
507
575
|
}
|
|
508
576
|
const found = findResumeCandidate(records, {
|
|
509
577
|
session: input.session,
|
|
@@ -513,15 +581,24 @@ export async function resolveLaunchConversationId(input) {
|
|
|
513
581
|
resumeId: passthroughResumeId(input.passthrough),
|
|
514
582
|
});
|
|
515
583
|
if (!found)
|
|
516
|
-
return
|
|
584
|
+
return fresh();
|
|
517
585
|
const conv = found.record.conversation_id;
|
|
518
586
|
if (found.source === "session_name" && input.interactive) {
|
|
519
587
|
const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
|
|
520
588
|
if (!yes)
|
|
521
|
-
return
|
|
589
|
+
return fresh();
|
|
522
590
|
}
|
|
523
591
|
say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
|
|
524
|
-
|
|
592
|
+
const cc = found.record.cc_session_id?.trim() ?? "";
|
|
593
|
+
let resumeCcSessionId = null;
|
|
594
|
+
if (found.source === "session_name" && cc && !choosesCcSession(input.passthrough)) {
|
|
595
|
+
if (input.ccSessionExists(cc))
|
|
596
|
+
resumeCcSessionId = cc;
|
|
597
|
+
else {
|
|
598
|
+
say(`mnemom agent: Claude Code no longer has session ${cc}, so this launch starts a new one (its goal starts empty on the gateway).`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { conversationId: resolveConversationId(conv), resumeCcSessionId };
|
|
525
602
|
}
|
|
526
603
|
/**
|
|
527
604
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
@@ -868,7 +945,11 @@ function launchTerminal(ctx) {
|
|
|
868
945
|
applyToolSearchDefault(childEnv);
|
|
869
946
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
870
947
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
871
|
-
|
|
948
|
+
// Exact compaction signal for the gateway (x-claude-code-request-class: compaction), on every
|
|
949
|
+
// terminal launch — interactive and `-p` passthrough alike; a user's own value wins. The
|
|
950
|
+
// user's CLAUDE_AUTOCOMPACT_PCT_OVERRIDE / CLAUDE_CODE_AUTO_COMPACT_WINDOW ride through
|
|
951
|
+
// untouched in the copied env (USER_AUTOCOMPACT_ENV).
|
|
952
|
+
applyGatewayHintHeaders(childEnv);
|
|
872
953
|
if (ctx.options.slack)
|
|
873
954
|
args.push(...slackChannelArgs(sessionName));
|
|
874
955
|
if (ctx.contextHint) {
|
|
@@ -1112,6 +1193,11 @@ function spawnRcCli(ctx) {
|
|
|
1112
1193
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
1113
1194
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
1114
1195
|
childEnv[SESSION_NAME_ENV] = rcName;
|
|
1196
|
+
// Same exact compaction signal as the terminal shape. Not an ANTHROPIC_* var, so the RC gate
|
|
1197
|
+
// ignores it (like HTTPS_PROXY above); the proxy forwards every non-hop request header, so the
|
|
1198
|
+
// x-claude-code-* hints reach the gateway. The user's own autocompact env is not on the strip
|
|
1199
|
+
// list and rides through untouched.
|
|
1200
|
+
applyGatewayHintHeaders(childEnv);
|
|
1115
1201
|
// Under RC Claude Code sees a first-party host (the proxy, not ANTHROPIC_BASE_URL, routes to the
|
|
1116
1202
|
// gateway), so tool search is already on by default; setting it anyway keeps every launch shape
|
|
1117
1203
|
// on the same switch. A user value is kept.
|
package/dist/index.js
CHANGED
|
@@ -173,6 +173,8 @@ const agentCmd = program
|
|
|
173
173
|
}))
|
|
174
174
|
.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
175
|
.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")
|
|
176
|
+
.addOption(new Option("--compaction-intercept", "EXPERIMENTAL (`mnemom experimental enable compaction-intercept`): tell the gateway to report the real context size (Claude Code compacts at its own threshold) and answer the compaction at once from its fold, for this session").hideHelp(hiddenUnless("compaction-intercept")))
|
|
177
|
+
.option("--no-compaction-intercept", "Keep the compaction intercept off for this launch even when launcher.toml (compaction_intercept = true) or MNEMOM_AGENT_COMPACTION_INTERCEPT turns it on")
|
|
176
178
|
.option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
|
|
177
179
|
.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")
|
|
178
180
|
.action(async (scenario, passthrough, opts) => {
|
|
@@ -216,6 +218,11 @@ const agentCmd = program
|
|
|
216
218
|
used: opts.contextHandoff === true,
|
|
217
219
|
},
|
|
218
220
|
{ what: "--implicit", id: "implicit", used: opts.implicit === true },
|
|
221
|
+
{
|
|
222
|
+
what: "--compaction-intercept",
|
|
223
|
+
id: "compaction-intercept",
|
|
224
|
+
used: opts.compactionIntercept === true,
|
|
225
|
+
},
|
|
219
226
|
], experimental);
|
|
220
227
|
if (opts.gateway)
|
|
221
228
|
process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
|
|
@@ -301,6 +308,10 @@ const agentCmd = program
|
|
|
301
308
|
contextProfile: opts.contextProfile,
|
|
302
309
|
// Off ⇒ an explicit false, so MNEMOM_AGENT_CONTEXT_HANDOFF cannot turn it on.
|
|
303
310
|
contextHandoff: experimental.includes("context-handoff") ? opts.contextHandoff : false,
|
|
311
|
+
// Same gate: off ⇒ an explicit false, so MNEMOM_AGENT_COMPACTION_INTERCEPT cannot turn it on.
|
|
312
|
+
compactionIntercept: experimental.includes("compaction-intercept")
|
|
313
|
+
? opts.compactionIntercept
|
|
314
|
+
: false,
|
|
304
315
|
experimental,
|
|
305
316
|
setup: opts.setup,
|
|
306
317
|
keySource: cfg.keySource,
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -228,15 +228,6 @@ export declare function resolveWindowModel(input: {
|
|
|
228
228
|
env: Record<string, string | undefined>;
|
|
229
229
|
settings: () => CcSettingsView[];
|
|
230
230
|
}): WindowModel;
|
|
231
|
-
/**
|
|
232
|
-
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
|
|
233
|
-
* `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
|
|
234
|
-
* its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
|
|
235
|
-
* them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
|
|
236
|
-
* only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
|
|
237
|
-
* teammates off the main implicit goal.
|
|
238
|
-
*/
|
|
239
|
-
export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
240
231
|
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
241
232
|
export declare function sessionDisplayName(agent: string, scenario: string): string;
|
|
242
233
|
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
|
@@ -322,6 +313,42 @@ export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
|
|
|
322
313
|
* header without the tool.
|
|
323
314
|
*/
|
|
324
315
|
export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
316
|
+
/**
|
|
317
|
+
* The two gateway env keys the `compaction_intercept` setting turns on for a single session
|
|
318
|
+
* (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
|
|
319
|
+
* - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
|
|
320
|
+
* REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
|
|
321
|
+
* auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
|
|
322
|
+
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
|
|
323
|
+
* - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
|
|
324
|
+
* gateway's own fold digest (no model call), so the compaction never blocks the user.
|
|
325
|
+
* Sent together, value `"on"`, ONLY when the setting is on (see `resolveCompactionIntercept`).
|
|
326
|
+
*/
|
|
327
|
+
export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
|
|
328
|
+
export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
|
|
329
|
+
/**
|
|
330
|
+
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
|
|
331
|
+
* compaction`, `x-claude-code-compaction: auto | manual | reactive`,
|
|
332
|
+
* `x-claude-code-context-compacted` on the next turn, `x-claude-code-prompt-id`). Off by default
|
|
333
|
+
* in the client; the launcher turns it on for EVERY launch shape (terminal, `-p` passthrough,
|
|
334
|
+
* Remote Control interactive and server) so the gateway gets an exact compaction signal instead
|
|
335
|
+
* of relying on the body rule alone. Headers only — no behaviour change in the client. The
|
|
336
|
+
* gateway also reads them to keep summaries, titles, compaction and teammates off the main
|
|
337
|
+
* implicit goal.
|
|
338
|
+
*/
|
|
339
|
+
export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
340
|
+
/**
|
|
341
|
+
* Claude Code's own auto-compaction knobs (env). The launcher NEVER sets, changes or strips
|
|
342
|
+
* them: whatever the user configured is what the client honours (the `autoCompactWindow`
|
|
343
|
+
* settings.json key is likewise never touched — the launcher writes no settings file).
|
|
344
|
+
*/
|
|
345
|
+
export declare const USER_AUTOCOMPACT_ENV: readonly ["CLAUDE_AUTOCOMPACT_PCT_OVERRIDE", "CLAUDE_CODE_AUTO_COMPACT_WINDOW"];
|
|
346
|
+
/**
|
|
347
|
+
* Turn Claude Code's gateway hint headers on in a child env, unless the user already set
|
|
348
|
+
* `CLAUDE_CODE_GATEWAY_HINT_HEADERS` themselves (any non-blank value, `0` included, wins).
|
|
349
|
+
* Mutates and returns `childEnv`. Touches no other key.
|
|
350
|
+
*/
|
|
351
|
+
export declare function applyGatewayHintHeaders(childEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
325
352
|
/**
|
|
326
353
|
* The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
|
|
327
354
|
* is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
|
|
@@ -456,13 +483,15 @@ export declare function resolveContextProfile(opts: {
|
|
|
456
483
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
457
484
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
458
485
|
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
|
|
459
|
-
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON
|
|
460
|
-
*
|
|
486
|
+
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
487
|
+
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the compaction
|
|
488
|
+
* intercept is ON. Empty for `balanced` with both off — the common case, where no header is sent at all.
|
|
461
489
|
* Exported so `--dry-run` can list exactly what the header carries.
|
|
462
490
|
*/
|
|
463
491
|
export declare function buildSessionConfig(opts: {
|
|
464
492
|
profile: ContextProfile;
|
|
465
493
|
contextHandoff: boolean;
|
|
494
|
+
compactionIntercept?: boolean;
|
|
466
495
|
}): Record<string, string>;
|
|
467
496
|
/**
|
|
468
497
|
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
@@ -472,6 +501,7 @@ export declare function buildSessionConfig(opts: {
|
|
|
472
501
|
export declare function buildSessionConfigPayload(opts: {
|
|
473
502
|
profile: ContextProfile;
|
|
474
503
|
contextHandoff: boolean;
|
|
504
|
+
compactionIntercept?: boolean;
|
|
475
505
|
experimental?: readonly string[];
|
|
476
506
|
}): {
|
|
477
507
|
v: 1;
|
|
@@ -484,6 +514,8 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
484
514
|
* byte-identical to a launch before profiles existed). Two independent overlays:
|
|
485
515
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
486
516
|
* `aggressive` → the three fold-tuning keys, `balanced` → nothing;
|
|
517
|
+
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
518
|
+
* `"on"` (the `compaction_intercept` setting);
|
|
487
519
|
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
488
520
|
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
489
521
|
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
@@ -493,6 +525,7 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
493
525
|
export declare function buildSessionConfigB64(opts: {
|
|
494
526
|
profile: ContextProfile;
|
|
495
527
|
contextHandoff: boolean;
|
|
528
|
+
compactionIntercept?: boolean;
|
|
496
529
|
experimental?: readonly string[];
|
|
497
530
|
}): string | undefined;
|
|
498
531
|
/**
|
|
@@ -507,6 +540,17 @@ export declare function resolveContextHandoff(opts: {
|
|
|
507
540
|
contextHandoff?: boolean;
|
|
508
541
|
env?: NodeJS.ProcessEnv;
|
|
509
542
|
}): boolean;
|
|
543
|
+
/**
|
|
544
|
+
* Is the compaction intercept (`GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT` in the
|
|
545
|
+
* session-config header) ON for this launch? OFF by default, same shape as
|
|
546
|
+
* `resolveContextHandoff`: an explicit `--compaction-intercept` / `--no-compaction-intercept`
|
|
547
|
+
* wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which `applyConfigToEnv` sets from the
|
|
548
|
+
* `compaction_intercept` config key), else OFF.
|
|
549
|
+
*/
|
|
550
|
+
export declare function resolveCompactionIntercept(opts: {
|
|
551
|
+
compactionIntercept?: boolean;
|
|
552
|
+
env?: NodeJS.ProcessEnv;
|
|
553
|
+
}): boolean;
|
|
510
554
|
/** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
|
|
511
555
|
export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
512
556
|
/**
|
package/dist/lib/agent.js
CHANGED
|
@@ -301,15 +301,6 @@ export function resolveWindowModel(input) {
|
|
|
301
301
|
return { model: explicit, reason: "unsupported" };
|
|
302
302
|
return { model: `${base}${ONE_M_SUFFIX}`, reason: "one-m", from: base };
|
|
303
303
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
|
|
306
|
-
* `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
|
|
307
|
-
* its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
|
|
308
|
-
* them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
|
|
309
|
-
* only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
|
|
310
|
-
* teammates off the main implicit goal.
|
|
311
|
-
*/
|
|
312
|
-
export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
313
304
|
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
314
305
|
export function sessionDisplayName(agent, scenario) {
|
|
315
306
|
return sanitizeAgentName(`${agent}-${scenario}`) || agent;
|
|
@@ -489,6 +480,50 @@ const CONTEXT_PROFILE_CONFIG = {
|
|
|
489
480
|
* header without the tool.
|
|
490
481
|
*/
|
|
491
482
|
export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
483
|
+
/**
|
|
484
|
+
* The two gateway env keys the `compaction_intercept` setting turns on for a single session
|
|
485
|
+
* (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
|
|
486
|
+
* - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
|
|
487
|
+
* REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
|
|
488
|
+
* auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
|
|
489
|
+
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
|
|
490
|
+
* - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
|
|
491
|
+
* gateway's own fold digest (no model call), so the compaction never blocks the user.
|
|
492
|
+
* Sent together, value `"on"`, ONLY when the setting is on (see `resolveCompactionIntercept`).
|
|
493
|
+
*/
|
|
494
|
+
export const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
|
|
495
|
+
export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
|
|
496
|
+
/**
|
|
497
|
+
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
|
|
498
|
+
* compaction`, `x-claude-code-compaction: auto | manual | reactive`,
|
|
499
|
+
* `x-claude-code-context-compacted` on the next turn, `x-claude-code-prompt-id`). Off by default
|
|
500
|
+
* in the client; the launcher turns it on for EVERY launch shape (terminal, `-p` passthrough,
|
|
501
|
+
* Remote Control interactive and server) so the gateway gets an exact compaction signal instead
|
|
502
|
+
* of relying on the body rule alone. Headers only — no behaviour change in the client. The
|
|
503
|
+
* gateway also reads them to keep summaries, titles, compaction and teammates off the main
|
|
504
|
+
* implicit goal.
|
|
505
|
+
*/
|
|
506
|
+
export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
507
|
+
/**
|
|
508
|
+
* Claude Code's own auto-compaction knobs (env). The launcher NEVER sets, changes or strips
|
|
509
|
+
* them: whatever the user configured is what the client honours (the `autoCompactWindow`
|
|
510
|
+
* settings.json key is likewise never touched — the launcher writes no settings file).
|
|
511
|
+
*/
|
|
512
|
+
export const USER_AUTOCOMPACT_ENV = [
|
|
513
|
+
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
|
|
514
|
+
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
515
|
+
];
|
|
516
|
+
/**
|
|
517
|
+
* Turn Claude Code's gateway hint headers on in a child env, unless the user already set
|
|
518
|
+
* `CLAUDE_CODE_GATEWAY_HINT_HEADERS` themselves (any non-blank value, `0` included, wins).
|
|
519
|
+
* Mutates and returns `childEnv`. Touches no other key.
|
|
520
|
+
*/
|
|
521
|
+
export function applyGatewayHintHeaders(childEnv) {
|
|
522
|
+
if ((childEnv[GATEWAY_HINT_HEADERS_ENV] ?? "").trim() === "") {
|
|
523
|
+
childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
|
|
524
|
+
}
|
|
525
|
+
return childEnv;
|
|
526
|
+
}
|
|
492
527
|
const SEALED_ONLY_FLAGS = "--goal/--requirement/--allow/--forbid/--goal-id";
|
|
493
528
|
/**
|
|
494
529
|
* `--implicit` and a sealed contract are mutually exclusive: on the gateway a
|
|
@@ -732,8 +767,9 @@ export function resolveContextProfile(opts) {
|
|
|
732
767
|
* The decoded per-session env overlay for this launch — the `config` object of the
|
|
733
768
|
* `x-mnemom-session-config` payload, in wire order: the profile's keys first
|
|
734
769
|
* (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
|
|
735
|
-
* for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON
|
|
736
|
-
*
|
|
770
|
+
* 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 compaction
|
|
772
|
+
* intercept is ON. Empty for `balanced` with both off — the common case, where no header is sent at all.
|
|
737
773
|
* Exported so `--dry-run` can list exactly what the header carries.
|
|
738
774
|
*/
|
|
739
775
|
export function buildSessionConfig(opts) {
|
|
@@ -742,6 +778,10 @@ export function buildSessionConfig(opts) {
|
|
|
742
778
|
config[k] = v;
|
|
743
779
|
if (opts.contextHandoff)
|
|
744
780
|
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
781
|
+
if (opts.compactionIntercept) {
|
|
782
|
+
config[USAGE_REAL_SIZE_KEY] = "on";
|
|
783
|
+
config[COMPACTION_INTERCEPT_KEY] = "on";
|
|
784
|
+
}
|
|
745
785
|
return config;
|
|
746
786
|
}
|
|
747
787
|
/**
|
|
@@ -764,6 +804,8 @@ export function buildSessionConfigPayload(opts) {
|
|
|
764
804
|
* byte-identical to a launch before profiles existed). Two independent overlays:
|
|
765
805
|
* - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
|
|
766
806
|
* `aggressive` → the three fold-tuning keys, `balanced` → nothing;
|
|
807
|
+
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
|
|
808
|
+
* `"on"` (the `compaction_intercept` setting);
|
|
767
809
|
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
768
810
|
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
769
811
|
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
@@ -792,6 +834,21 @@ export function resolveContextHandoff(opts) {
|
|
|
792
834
|
return false;
|
|
793
835
|
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
794
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Is the compaction intercept (`GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT` in the
|
|
839
|
+
* session-config header) ON for this launch? OFF by default, same shape as
|
|
840
|
+
* `resolveContextHandoff`: an explicit `--compaction-intercept` / `--no-compaction-intercept`
|
|
841
|
+
* wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which `applyConfigToEnv` sets from the
|
|
842
|
+
* `compaction_intercept` config key), else OFF.
|
|
843
|
+
*/
|
|
844
|
+
export function resolveCompactionIntercept(opts) {
|
|
845
|
+
if (opts.compactionIntercept !== undefined)
|
|
846
|
+
return opts.compactionIntercept;
|
|
847
|
+
const raw = (opts.env?.MNEMOM_AGENT_COMPACTION_INTERCEPT ?? "").trim().toLowerCase();
|
|
848
|
+
if (raw === "")
|
|
849
|
+
return false;
|
|
850
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
851
|
+
}
|
|
795
852
|
/** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
|
|
796
853
|
export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
797
854
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Everything here is pure or dependency-injected; index.ts and the command
|
|
13
13
|
* modules are the thin drivers.
|
|
14
14
|
*/
|
|
15
|
-
export type ExperimentalId = "context-handoff" | "slack" | "implicit";
|
|
15
|
+
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "compaction-intercept";
|
|
16
16
|
export interface ExperimentalFeature {
|
|
17
17
|
id: ExperimentalId;
|
|
18
18
|
summary: string;
|
|
@@ -73,7 +73,8 @@ export declare function assertEnabled(uses: ReadonlyArray<{
|
|
|
73
73
|
export declare function experimentalIdForConfigKey(key: string, value: string): ExperimentalId | undefined;
|
|
74
74
|
/**
|
|
75
75
|
* The stderr lines for experimental settings a launch ignores because their feature
|
|
76
|
-
* is off: launcher.toml keys (`entries` as `[key, value]`) and
|
|
76
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF and
|
|
77
|
+
* MNEMOM_AGENT_COMPACTION_INTERCEPT.
|
|
77
78
|
*/
|
|
78
79
|
export declare function ignoredSettingLines(opts: {
|
|
79
80
|
entries: ReadonlyArray<[string, string]>;
|
package/dist/lib/experimental.js
CHANGED
|
@@ -46,6 +46,17 @@ export const EXPERIMENTAL_FEATURES = [
|
|
|
46
46
|
env: [],
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
id: "compaction-intercept",
|
|
51
|
+
summary: "Have the gateway report the real context size (so Claude Code compacts at its own threshold) and answer the compaction at once from its fold",
|
|
52
|
+
since: "0.17.4",
|
|
53
|
+
gates: {
|
|
54
|
+
flags: ["--compaction-intercept"],
|
|
55
|
+
configKeys: ["compaction_intercept"],
|
|
56
|
+
commands: [],
|
|
57
|
+
env: ["MNEMOM_AGENT_COMPACTION_INTERCEPT"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
49
60
|
];
|
|
50
61
|
export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
|
|
51
62
|
export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
|
|
@@ -127,19 +138,22 @@ export function experimentalIdForConfigKey(key, value) {
|
|
|
127
138
|
return value === "true" ? "slack" : undefined;
|
|
128
139
|
case "context_handoff":
|
|
129
140
|
return value === "true" ? "context-handoff" : undefined;
|
|
141
|
+
case "compaction_intercept":
|
|
142
|
+
return value === "true" ? "compaction-intercept" : undefined;
|
|
130
143
|
case "goal_mode":
|
|
131
144
|
return value === "implicit" ? "implicit" : undefined;
|
|
132
145
|
default:
|
|
133
146
|
return undefined;
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
|
-
/** Is an env value one of the truthy spellings resolveContextHandoff
|
|
149
|
+
/** Is an env value one of the truthy spellings resolveContextHandoff / resolveCompactionIntercept accept? */
|
|
137
150
|
function truthy(raw) {
|
|
138
151
|
return ["1", "true", "on", "yes"].includes((raw ?? "").trim().toLowerCase());
|
|
139
152
|
}
|
|
140
153
|
/**
|
|
141
154
|
* The stderr lines for experimental settings a launch ignores because their feature
|
|
142
|
-
* is off: launcher.toml keys (`entries` as `[key, value]`) and
|
|
155
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF and
|
|
156
|
+
* MNEMOM_AGENT_COMPACTION_INTERCEPT.
|
|
143
157
|
*/
|
|
144
158
|
export function ignoredSettingLines(opts) {
|
|
145
159
|
const lines = [];
|
|
@@ -152,6 +166,10 @@ export function ignoredSettingLines(opts) {
|
|
|
152
166
|
if (truthy(opts.env.MNEMOM_AGENT_CONTEXT_HANDOFF) && !opts.enabled.includes("context-handoff")) {
|
|
153
167
|
lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_CONTEXT_HANDOFF", "context-handoff")}`);
|
|
154
168
|
}
|
|
169
|
+
if (truthy(opts.env.MNEMOM_AGENT_COMPACTION_INTERCEPT) &&
|
|
170
|
+
!opts.enabled.includes("compaction-intercept")) {
|
|
171
|
+
lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_COMPACTION_INTERCEPT", "compaction-intercept")}`);
|
|
172
|
+
}
|
|
155
173
|
return lines;
|
|
156
174
|
}
|
|
157
175
|
/** The features an existing launcher.toml already uses (the one-time migration). */
|
|
@@ -199,6 +217,8 @@ export function configKeyFeature(key) {
|
|
|
199
217
|
return "slack";
|
|
200
218
|
if (key === "context_handoff")
|
|
201
219
|
return "context-handoff";
|
|
220
|
+
if (key === "compaction_intercept")
|
|
221
|
+
return "compaction-intercept";
|
|
202
222
|
if (key === "goal_mode")
|
|
203
223
|
return "implicit";
|
|
204
224
|
return undefined;
|
|
@@ -49,6 +49,12 @@ export interface LauncherConfig {
|
|
|
49
49
|
* every launch. Off means byte-identical to a launch with no MNE-7564 change at all.
|
|
50
50
|
*/
|
|
51
51
|
contextHandoff?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Compaction-intercept default (EXPERIMENTAL, off when unset): `true` sends
|
|
54
|
+
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` in the per-session
|
|
55
|
+
* config header for every launch. Off means no header key at all.
|
|
56
|
+
*/
|
|
57
|
+
compactionIntercept?: boolean;
|
|
52
58
|
/**
|
|
53
59
|
* How the gateway folds each session's context (`balanced` when unset): `off` leaves
|
|
54
60
|
* compaction to the coding agent, `balanced` keeps more earlier work verbatim, `aggressive`
|
|
@@ -107,6 +113,6 @@ export declare function launcherEntries(cfg: LauncherConfig): Array<[string, str
|
|
|
107
113
|
/**
|
|
108
114
|
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
109
115
|
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
110
|
-
* off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
116
|
+
* off, `compaction_intercept` while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
111
117
|
*/
|
|
112
118
|
export declare function withoutDisabledExperimental(cfg: LauncherConfig, enabled: readonly ExperimentalId[]): LauncherConfig;
|
|
@@ -197,6 +197,14 @@ export const CONFIG_KEYS = [
|
|
|
197
197
|
c.contextHandoff = coerceBool("context_handoff", v);
|
|
198
198
|
},
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
key: "compaction_intercept",
|
|
202
|
+
describe: "EXPERIMENTAL: tell the gateway GATEWAY_USAGE_REAL_SIZE=on + GATEWAY_COMPACTION_INTERCEPT=on (off by default)",
|
|
203
|
+
get: (c) => c.compactionIntercept,
|
|
204
|
+
set: (c, v) => {
|
|
205
|
+
c.compactionIntercept = coerceBool("compaction_intercept", v);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
200
208
|
{
|
|
201
209
|
key: "context_profile",
|
|
202
210
|
describe: `How the gateway folds each session's context (${CONTEXT_PROFILES.join("|")}; balanced by default)`,
|
|
@@ -285,6 +293,8 @@ export function parseLauncherConfig(text) {
|
|
|
285
293
|
cfg.slackChannel = coerceNonEmpty("slack_channel", str(raw.slack_channel) ?? "");
|
|
286
294
|
if (raw.context_handoff !== undefined)
|
|
287
295
|
cfg.contextHandoff = coerceBool("context_handoff", str(raw.context_handoff) ?? "");
|
|
296
|
+
if (raw.compaction_intercept !== undefined)
|
|
297
|
+
cfg.compactionIntercept = coerceBool("compaction_intercept", str(raw.compaction_intercept) ?? "");
|
|
288
298
|
if (raw.context_profile !== undefined)
|
|
289
299
|
cfg.contextProfile = parseContextProfile(str(raw.context_profile) ?? "", "context_profile");
|
|
290
300
|
const g = raw.guardrails;
|
|
@@ -357,6 +367,8 @@ export function serializeLauncherConfig(cfg) {
|
|
|
357
367
|
doc.slack_allow = cfg.slackAllow;
|
|
358
368
|
if (cfg.contextHandoff !== undefined)
|
|
359
369
|
doc.context_handoff = cfg.contextHandoff;
|
|
370
|
+
if (cfg.compactionIntercept !== undefined)
|
|
371
|
+
doc.compaction_intercept = cfg.compactionIntercept;
|
|
360
372
|
if (cfg.contextProfile !== undefined)
|
|
361
373
|
doc.context_profile = cfg.contextProfile;
|
|
362
374
|
const g = cfg.guardrails;
|
|
@@ -414,6 +426,10 @@ export function applyConfigToEnv(cfg, env = process.env) {
|
|
|
414
426
|
if (cfg.contextHandoff !== undefined && !(env.MNEMOM_AGENT_CONTEXT_HANDOFF ?? "").trim()) {
|
|
415
427
|
env.MNEMOM_AGENT_CONTEXT_HANDOFF = cfg.contextHandoff ? "1" : "0";
|
|
416
428
|
}
|
|
429
|
+
if (cfg.compactionIntercept !== undefined &&
|
|
430
|
+
!(env.MNEMOM_AGENT_COMPACTION_INTERCEPT ?? "").trim()) {
|
|
431
|
+
env.MNEMOM_AGENT_COMPACTION_INTERCEPT = cfg.compactionIntercept ? "1" : "0";
|
|
432
|
+
}
|
|
417
433
|
if (cfg.contextProfile !== undefined && !(env.MNEMOM_AGENT_CONTEXT_PROFILE ?? "").trim()) {
|
|
418
434
|
env.MNEMOM_AGENT_CONTEXT_PROFILE = cfg.contextProfile;
|
|
419
435
|
}
|
|
@@ -428,7 +444,7 @@ export function launcherEntries(cfg) {
|
|
|
428
444
|
/**
|
|
429
445
|
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
430
446
|
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
431
|
-
* off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
447
|
+
* off, `compaction_intercept` while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
432
448
|
*/
|
|
433
449
|
export function withoutDisabledExperimental(cfg, enabled) {
|
|
434
450
|
const out = { ...cfg };
|
|
@@ -439,6 +455,8 @@ export function withoutDisabledExperimental(cfg, enabled) {
|
|
|
439
455
|
}
|
|
440
456
|
if (!enabled.includes("context-handoff"))
|
|
441
457
|
delete out.contextHandoff;
|
|
458
|
+
if (!enabled.includes("compaction-intercept"))
|
|
459
|
+
delete out.compactionIntercept;
|
|
442
460
|
if (!enabled.includes("implicit") && out.goalMode === "implicit")
|
|
443
461
|
delete out.goalMode;
|
|
444
462
|
return out;
|