@mnemom/mnemom 0.17.4-next.6 → 0.17.4-next.8
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-doctor.js +9 -2
- package/dist/commands/agent.d.ts +36 -4
- package/dist/commands/agent.js +130 -18
- package/dist/index.js +12 -1
- package/dist/lib/agent.d.ts +55 -11
- package/dist/lib/agent.js +68 -11
- package/dist/lib/console-key.d.ts +40 -0
- package/dist/lib/console-key.js +77 -0
- 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 +20 -2
- package/dist/lib/login-detect.d.ts +10 -4
- package/dist/lib/login-detect.js +23 -5
- package/dist/lib/subscription-login.d.ts +2 -1
- package/dist/lib/subscription-login.js +1 -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;
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
|
-
import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
|
|
16
|
+
import { anthropicKeySource, checkRcCapability, defaultConsoleKeyDeps, resolveCliBin, } from "./agent.js";
|
|
17
|
+
import { readConsoleKey } from "../lib/console-key.js";
|
|
17
18
|
import { resolveAgentAuth } from "../lib/subscription-login.js";
|
|
18
19
|
import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
|
|
19
20
|
import { getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
@@ -52,11 +53,17 @@ export async function agentDoctorCommand(options = {}) {
|
|
|
52
53
|
auth = resolveAgentAuth(undefined, env);
|
|
53
54
|
}
|
|
54
55
|
catch (err) {
|
|
55
|
-
add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription)");
|
|
56
|
+
add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription, console)");
|
|
56
57
|
}
|
|
57
58
|
if (auth === "subscription") {
|
|
58
59
|
add("ok", "auth: subscription — Claude Code uses your claude.ai login (Team/Enterprise plans only); no Anthropic key needed");
|
|
59
60
|
}
|
|
61
|
+
else if (auth === "console") {
|
|
62
|
+
if (readConsoleKey(defaultConsoleKeyDeps(env)))
|
|
63
|
+
add("ok", "auth: console — using the API key your Claude Code Anthropic Console login created");
|
|
64
|
+
else
|
|
65
|
+
add("miss", "auth: console — Claude Code has no Anthropic Console login on this machine", 'run `claude`, then /login → "Anthropic Console account" — or `mnemom agent config set auth api-key`');
|
|
66
|
+
}
|
|
60
67
|
else {
|
|
61
68
|
const keySrc = anthropicKeySource(env);
|
|
62
69
|
if (keySrc === "env")
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
29
|
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
30
30
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
31
|
+
import { type ConsoleKeyDeps } from "../lib/console-key.js";
|
|
31
32
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
32
33
|
export interface AgentOptions {
|
|
33
34
|
goal?: string;
|
|
@@ -79,6 +80,13 @@ export interface AgentOptions {
|
|
|
79
80
|
* ⇒ no header key, no MCP server — the launch is byte-identical to a build without this flag.
|
|
80
81
|
*/
|
|
81
82
|
contextHandoff?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* EXPERIMENTAL, off by default (payload-bound fix): tells the gateway
|
|
85
|
+
* `GATEWAY_USAGE_REAL_SIZE=on` + `GATEWAY_COMPACTION_INTERCEPT=on` for this session only (real
|
|
86
|
+
* context size in usage, so Claude Code compacts at its OWN threshold; the compaction request
|
|
87
|
+
* answered at once from the gateway's fold). Absent/false ⇒ no header key.
|
|
88
|
+
*/
|
|
89
|
+
compactionIntercept?: boolean;
|
|
82
90
|
/**
|
|
83
91
|
* Governed-agent provisioning: undefined = AUTO (provision-with-confirm when
|
|
84
92
|
* interactive, warn-and-continue otherwise), true = force (`--setup`), false =
|
|
@@ -116,11 +124,30 @@ export declare function passthroughResumeId(passthrough: readonly string[]): str
|
|
|
116
124
|
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
117
125
|
* (else the newest matching row), reused without asking;
|
|
118
126
|
* - 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.
|
|
127
|
+
* conversation, n mints a new one. No TTY: Y.
|
|
121
128
|
* The ledger read is best-effort: a failure means a new id.
|
|
122
129
|
*/
|
|
123
|
-
export declare function resolveLaunchConversationId(input:
|
|
130
|
+
export declare function resolveLaunchConversationId(input: LaunchContinuationInput): Promise<string>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether Claude Code still has the transcript of session `id`
|
|
133
|
+
* (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
|
|
134
|
+
* project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
|
|
135
|
+
* resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
|
|
136
|
+
*/
|
|
137
|
+
export declare function ccSessionTranscriptExists(id: string, env: NodeJS.ProcessEnv): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
|
|
140
|
+
* of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
|
|
141
|
+
* when the caller passed no session flag of their own: the gateway gives the conversation's main
|
|
142
|
+
* goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
|
|
143
|
+
* in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
|
|
144
|
+
* conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveLaunchContinuation(input: LaunchContinuationInput): Promise<{
|
|
147
|
+
conversationId: string;
|
|
148
|
+
resumeCcSessionId: string | null;
|
|
149
|
+
}>;
|
|
150
|
+
interface LaunchContinuationInput {
|
|
124
151
|
session: string;
|
|
125
152
|
orgId: string;
|
|
126
153
|
agent: string;
|
|
@@ -131,7 +158,9 @@ export declare function resolveLaunchConversationId(input: {
|
|
|
131
158
|
interactive: boolean;
|
|
132
159
|
records: () => SessionRecord[];
|
|
133
160
|
askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
134
|
-
|
|
161
|
+
/** Whether Claude Code still has that session's transcript (ccSessionTranscriptExists). */
|
|
162
|
+
ccSessionExists: (ccSessionId: string) => boolean;
|
|
163
|
+
}
|
|
135
164
|
/**
|
|
136
165
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
137
166
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -163,6 +192,8 @@ export declare function recordSessionLaunch(ctx: {
|
|
|
163
192
|
}): Promise<void>;
|
|
164
193
|
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
165
194
|
export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
|
|
195
|
+
/** The real machine behind lib/console-key.ts: the macOS keychain via `security`, no shell. */
|
|
196
|
+
export declare function defaultConsoleKeyDeps(env: NodeJS.ProcessEnv): ConsoleKeyDeps;
|
|
166
197
|
/**
|
|
167
198
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
168
199
|
* customer adoption), in order:
|
|
@@ -318,3 +349,4 @@ export declare function printPlan(ctx: {
|
|
|
318
349
|
/** The experimental features on for this launch (the header's `experimental` array). */
|
|
319
350
|
experimental?: readonly string[];
|
|
320
351
|
}): void;
|
|
352
|
+
export {};
|
package/dist/commands/agent.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
29
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
|
-
import { tmpdir, homedir, hostname } from "node:os";
|
|
31
|
+
import { tmpdir, homedir, hostname, userInfo } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
@@ -41,14 +41,17 @@ import { experimentalNotice } from "../lib/experimental.js";
|
|
|
41
41
|
import { fmt } from "../lib/format.js";
|
|
42
42
|
import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
43
43
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
44
|
+
import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
|
|
44
45
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
45
46
|
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,
|
|
47
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
47
48
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
48
49
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
49
50
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
50
51
|
const CODE_STORE = LAUNCHER_STORE_PATH;
|
|
51
52
|
const say = (line = "") => console.error(line);
|
|
53
|
+
/** The one-line launch note when the compaction intercept is ON. */
|
|
54
|
+
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
55
|
/**
|
|
53
56
|
* The one-line launch note for a non-default context profile (`balanced`, the default, is
|
|
54
57
|
* silent — nothing is sent). `off` keeps the wording `--no-context` has always printed.
|
|
@@ -85,8 +88,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
85
88
|
// Nothing configured it yet: detect Claude Code's own login and, when there is a real
|
|
86
89
|
// choice, ask once and save the answer (lib/login-detect.ts). Never on --dry-run.
|
|
87
90
|
if (!options.dryRun && !authConfigured(options.auth, env)) {
|
|
91
|
+
const loginState = readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"));
|
|
88
92
|
auth = await chooseAuth({
|
|
89
|
-
login: detectClaudeLogin(
|
|
93
|
+
login: detectClaudeLogin(loginState),
|
|
94
|
+
consoleKeyAvailable: () => readConsoleKey(defaultConsoleKeyDeps(env)) !== undefined,
|
|
90
95
|
interactive: isInteractive(),
|
|
91
96
|
select: askSelect,
|
|
92
97
|
persist: (choice) => {
|
|
@@ -149,11 +154,20 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
149
154
|
say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
|
|
150
155
|
contextHandoff = false;
|
|
151
156
|
}
|
|
157
|
+
const compactionIntercept = resolveCompactionIntercept({
|
|
158
|
+
compactionIntercept: options.compactionIntercept,
|
|
159
|
+
env,
|
|
160
|
+
});
|
|
152
161
|
const experimental = options.experimental ?? [];
|
|
153
|
-
const sessionConfig = buildSessionConfig({
|
|
162
|
+
const sessionConfig = buildSessionConfig({
|
|
163
|
+
profile: contextProfile,
|
|
164
|
+
contextHandoff,
|
|
165
|
+
compactionIntercept,
|
|
166
|
+
});
|
|
154
167
|
const sessionConfigB64 = buildSessionConfigB64({
|
|
155
168
|
profile: contextProfile,
|
|
156
169
|
contextHandoff,
|
|
170
|
+
compactionIntercept,
|
|
157
171
|
experimental,
|
|
158
172
|
});
|
|
159
173
|
const expNotice = experimentalNotice(experimental);
|
|
@@ -203,6 +217,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
203
217
|
if (auth === "subscription") {
|
|
204
218
|
say("mnemom agent: auth = subscription — Claude Code would use your claude.ai login (Team/Enterprise plans only) with a Mnemom API key; no Anthropic key.");
|
|
205
219
|
}
|
|
220
|
+
if (auth === "console") {
|
|
221
|
+
say("mnemom agent: auth = console — the launch would use the API key your Claude Code Anthropic Console login created.");
|
|
222
|
+
}
|
|
206
223
|
if (expNotice)
|
|
207
224
|
say(expNotice);
|
|
208
225
|
if (previewShape === "remote-control") {
|
|
@@ -212,6 +229,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
212
229
|
if (contextHandoff) {
|
|
213
230
|
say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
|
|
214
231
|
}
|
|
232
|
+
if (compactionIntercept)
|
|
233
|
+
say(COMPACTION_INTERCEPT_NOTE);
|
|
215
234
|
return;
|
|
216
235
|
}
|
|
217
236
|
// Real launch: make sure we actually have a CLI to run. If we couldn't find it,
|
|
@@ -222,9 +241,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
222
241
|
}
|
|
223
242
|
// ── resolve the Anthropic key (zero 1Password; never logged) ──
|
|
224
243
|
// Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
244
|
+
// A Console launch uses the key Claude Code's Console login created, then runs as api-key.
|
|
245
|
+
const anthropicKey = auth === "subscription"
|
|
246
|
+
? undefined
|
|
247
|
+
: auth === "console"
|
|
248
|
+
? requireConsoleKey(defaultConsoleKeyDeps(env))
|
|
249
|
+
: await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource });
|
|
228
250
|
// ── account + µ-balance gate (no free tier) ─────────────────────────────────
|
|
229
251
|
// mnemom agent runs governed and on µ, so before we seal a contract or launch we
|
|
230
252
|
// (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
|
|
@@ -330,7 +352,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
330
352
|
// so the goal the gateway inferred there is still the one it steers by. Resolved here, after
|
|
331
353
|
// sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
|
|
332
354
|
// match the earlier launch.
|
|
333
|
-
const
|
|
355
|
+
const continuation = await resolveLaunchContinuation({
|
|
334
356
|
session: scenario,
|
|
335
357
|
orgId: account.orgId,
|
|
336
358
|
agent,
|
|
@@ -341,7 +363,19 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
341
363
|
interactive: isInteractive(),
|
|
342
364
|
records: () => readSessionRecords({ dir: MNEMOM_DIR }),
|
|
343
365
|
askYesNo: (q, d) => askYesNo(q, d),
|
|
366
|
+
ccSessionExists: (id) => ccSessionTranscriptExists(id, process.env),
|
|
344
367
|
});
|
|
368
|
+
const conversationId = continuation.conversationId;
|
|
369
|
+
// A relaunch that continues an earlier conversation resumes that Claude Code session, so the
|
|
370
|
+
// same CC session id keeps owning the conversation's main goal on the gateway (a fresh
|
|
371
|
+
// --session-id would land on a new, empty m.<sha16> thread). The server shape hosts many
|
|
372
|
+
// sessions and pins none.
|
|
373
|
+
if (continuation.resumeCcSessionId && shape !== "server") {
|
|
374
|
+
options = {
|
|
375
|
+
...options,
|
|
376
|
+
passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
345
379
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
346
380
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
347
381
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -389,6 +423,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
389
423
|
say(expNotice);
|
|
390
424
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
391
425
|
sayContextProfile(contextProfile);
|
|
426
|
+
if (compactionIntercept)
|
|
427
|
+
say(COMPACTION_INTERCEPT_NOTE);
|
|
392
428
|
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
393
429
|
// undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
|
|
394
430
|
// the config is written to a 0600 temp file and referenced by PATH only — never inlined into
|
|
@@ -489,21 +525,62 @@ export function passthroughResumeId(passthrough) {
|
|
|
489
525
|
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
490
526
|
* (else the newest matching row), reused without asking;
|
|
491
527
|
* - 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.
|
|
528
|
+
* conversation, n mints a new one. No TTY: Y.
|
|
494
529
|
* The ledger read is best-effort: a failure means a new id.
|
|
495
530
|
*/
|
|
496
531
|
export async function resolveLaunchConversationId(input) {
|
|
497
|
-
|
|
498
|
-
|
|
532
|
+
return (await resolveLaunchContinuation(input)).conversationId;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Whether the caller's passthrough already chooses the Claude Code session (`--session-id`,
|
|
536
|
+
* `--resume` / `-r`, `--continue` / `-c`, `--fork-session`): exactly when pickCcSessionId would
|
|
537
|
+
* NOT mint one, so the two never disagree about which flags count.
|
|
538
|
+
*/
|
|
539
|
+
function choosesCcSession(passthrough) {
|
|
540
|
+
return !pickCcSessionId([...passthrough], () => "").inject;
|
|
541
|
+
}
|
|
542
|
+
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;
|
|
543
|
+
/**
|
|
544
|
+
* Whether Claude Code still has the transcript of session `id`
|
|
545
|
+
* (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
|
|
546
|
+
* project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
|
|
547
|
+
* resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
|
|
548
|
+
*/
|
|
549
|
+
export function ccSessionTranscriptExists(id, env) {
|
|
550
|
+
if (!CC_SESSION_ID_RE.test(id))
|
|
551
|
+
return false;
|
|
552
|
+
const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
|
|
553
|
+
try {
|
|
554
|
+
return readdirSync(projects).some((d) => existsSync(join(projects, d, `${id}.jsonl`)));
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
|
|
562
|
+
* of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
|
|
563
|
+
* when the caller passed no session flag of their own: the gateway gives the conversation's main
|
|
564
|
+
* goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
|
|
565
|
+
* in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
|
|
566
|
+
* conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
|
|
567
|
+
*/
|
|
568
|
+
export async function resolveLaunchContinuation(input) {
|
|
569
|
+
const fresh = () => ({
|
|
570
|
+
conversationId: resolveConversationId(undefined),
|
|
571
|
+
resumeCcSessionId: null,
|
|
572
|
+
});
|
|
573
|
+
if ((input.explicit ?? "").trim()) {
|
|
574
|
+
return { conversationId: resolveConversationId(input.explicit), resumeCcSessionId: null };
|
|
575
|
+
}
|
|
499
576
|
if (!input.implicit)
|
|
500
|
-
return
|
|
577
|
+
return fresh();
|
|
501
578
|
let records;
|
|
502
579
|
try {
|
|
503
580
|
records = input.records();
|
|
504
581
|
}
|
|
505
582
|
catch {
|
|
506
|
-
return
|
|
583
|
+
return fresh();
|
|
507
584
|
}
|
|
508
585
|
const found = findResumeCandidate(records, {
|
|
509
586
|
session: input.session,
|
|
@@ -513,15 +590,24 @@ export async function resolveLaunchConversationId(input) {
|
|
|
513
590
|
resumeId: passthroughResumeId(input.passthrough),
|
|
514
591
|
});
|
|
515
592
|
if (!found)
|
|
516
|
-
return
|
|
593
|
+
return fresh();
|
|
517
594
|
const conv = found.record.conversation_id;
|
|
518
595
|
if (found.source === "session_name" && input.interactive) {
|
|
519
596
|
const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
|
|
520
597
|
if (!yes)
|
|
521
|
-
return
|
|
598
|
+
return fresh();
|
|
522
599
|
}
|
|
523
600
|
say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
|
|
524
|
-
|
|
601
|
+
const cc = found.record.cc_session_id?.trim() ?? "";
|
|
602
|
+
let resumeCcSessionId = null;
|
|
603
|
+
if (found.source === "session_name" && cc && !choosesCcSession(input.passthrough)) {
|
|
604
|
+
if (input.ccSessionExists(cc))
|
|
605
|
+
resumeCcSessionId = cc;
|
|
606
|
+
else {
|
|
607
|
+
say(`mnemom agent: Claude Code no longer has session ${cc}, so this launch starts a new one (its goal starts empty on the gateway).`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return { conversationId: resolveConversationId(conv), resumeCcSessionId };
|
|
525
611
|
}
|
|
526
612
|
/**
|
|
527
613
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
@@ -598,6 +684,23 @@ export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
|
|
|
598
684
|
say,
|
|
599
685
|
};
|
|
600
686
|
}
|
|
687
|
+
/** The real machine behind lib/console-key.ts: the macOS keychain via `security`, no shell. */
|
|
688
|
+
export function defaultConsoleKeyDeps(env) {
|
|
689
|
+
return {
|
|
690
|
+
platform: process.platform,
|
|
691
|
+
env,
|
|
692
|
+
username: () => userInfo().username,
|
|
693
|
+
readKeychain: (service, account) => {
|
|
694
|
+
const r = spawnSync("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
|
|
695
|
+
encoding: "utf8",
|
|
696
|
+
timeout: 5_000,
|
|
697
|
+
});
|
|
698
|
+
return r.status === 0 ? r.stdout : undefined;
|
|
699
|
+
},
|
|
700
|
+
claudeJson: readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"))
|
|
701
|
+
.claudeJson,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
601
704
|
/**
|
|
602
705
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
603
706
|
* customer adoption), in order:
|
|
@@ -868,7 +971,11 @@ function launchTerminal(ctx) {
|
|
|
868
971
|
applyToolSearchDefault(childEnv);
|
|
869
972
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
870
973
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
871
|
-
|
|
974
|
+
// Exact compaction signal for the gateway (x-claude-code-request-class: compaction), on every
|
|
975
|
+
// terminal launch — interactive and `-p` passthrough alike; a user's own value wins. The
|
|
976
|
+
// user's CLAUDE_AUTOCOMPACT_PCT_OVERRIDE / CLAUDE_CODE_AUTO_COMPACT_WINDOW ride through
|
|
977
|
+
// untouched in the copied env (USER_AUTOCOMPACT_ENV).
|
|
978
|
+
applyGatewayHintHeaders(childEnv);
|
|
872
979
|
if (ctx.options.slack)
|
|
873
980
|
args.push(...slackChannelArgs(sessionName));
|
|
874
981
|
if (ctx.contextHint) {
|
|
@@ -1112,6 +1219,11 @@ function spawnRcCli(ctx) {
|
|
|
1112
1219
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
1113
1220
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
1114
1221
|
childEnv[SESSION_NAME_ENV] = rcName;
|
|
1222
|
+
// Same exact compaction signal as the terminal shape. Not an ANTHROPIC_* var, so the RC gate
|
|
1223
|
+
// ignores it (like HTTPS_PROXY above); the proxy forwards every non-hop request header, so the
|
|
1224
|
+
// x-claude-code-* hints reach the gateway. The user's own autocompact env is not on the strip
|
|
1225
|
+
// list and rides through untouched.
|
|
1226
|
+
applyGatewayHintHeaders(childEnv);
|
|
1115
1227
|
// Under RC Claude Code sees a first-party host (the proxy, not ANTHROPIC_BASE_URL, routes to the
|
|
1116
1228
|
// gateway), so tool search is already on by default; setting it anyway keeps every launch shape
|
|
1117
1229
|
// on the same switch. A user value is kept.
|
package/dist/index.js
CHANGED
|
@@ -143,7 +143,7 @@ const agentCmd = program
|
|
|
143
143
|
.option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
|
|
144
144
|
.option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
|
|
145
145
|
.option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
|
|
146
|
-
.option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login)")
|
|
146
|
+
.option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login) | console (the API key your Claude Code Anthropic Console login created)")
|
|
147
147
|
.option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
|
|
148
148
|
.addOption(new Option("--effort <level>", "Reasoning effort").choices([
|
|
149
149
|
"low",
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent` with an Anthropic Console login (`auth = console`).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `/login` → "Anthropic Console account" uses OAuth only to create a real
|
|
5
|
+
* Anthropic API key, which Claude Code stores and then sends as `x-api-key` ("/login
|
|
6
|
+
* managed key"). This module reads that key from where Claude Code keeps it, so the launch
|
|
7
|
+
* runs exactly like `auth = api-key` — same headers, same birth and claim, same identity —
|
|
8
|
+
* with no key for the user to copy. The key is read per launch and never stored by us.
|
|
9
|
+
*
|
|
10
|
+
* Where Claude Code keeps it (Claude Code 2.1.285, confirmed against the binary and a live
|
|
11
|
+
* probe on 2026-10-02):
|
|
12
|
+
* - macOS: the login keychain, service `Claude Code`, account $USER. When
|
|
13
|
+
* CLAUDE_CONFIG_DIR is set the service gains `-<first 8 hex of sha256(the raw
|
|
14
|
+
* CLAUDE_CONFIG_DIR value, NFC)>`; CLAUDE_SECURESTORAGE_CONFIG_DIR, when set, replaces
|
|
15
|
+
* that value (empty = no suffix).
|
|
16
|
+
* - everywhere (and the macOS fallback): `primaryApiKey` in Claude Code's .claude.json.
|
|
17
|
+
*
|
|
18
|
+
* A `claude auth login --console` *profile* (keyless OAuth, ~/.config/anthropic) creates no
|
|
19
|
+
* such key, so it is not read here.
|
|
20
|
+
*
|
|
21
|
+
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
22
|
+
*/
|
|
23
|
+
export interface ConsoleKeyDeps {
|
|
24
|
+
platform: NodeJS.Platform;
|
|
25
|
+
env: Record<string, string | undefined>;
|
|
26
|
+
/** os.userInfo().username, for when $USER is unset. */
|
|
27
|
+
username(): string;
|
|
28
|
+
/** The keychain password for (service, account), or undefined when absent. */
|
|
29
|
+
readKeychain(service: string, account: string): string | undefined;
|
|
30
|
+
/** Parsed .claude.json (see readClaudeLoginState), or undefined. */
|
|
31
|
+
claudeJson: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** The keychain service name Claude Code stores the managed key under. */
|
|
34
|
+
export declare function claudeKeychainService(env: Record<string, string | undefined>): string;
|
|
35
|
+
/** The keychain account Claude Code uses: $USER, else the OS username, else a fixed name. */
|
|
36
|
+
export declare function claudeKeychainAccount(env: Record<string, string | undefined>, username: () => string): string;
|
|
37
|
+
/** Claude Code's Console-login API key on this machine, or undefined. Never throws. */
|
|
38
|
+
export declare function readConsoleKey(deps: ConsoleKeyDeps): string | undefined;
|
|
39
|
+
/** The key for an `auth = console` launch; throws with what to do when there is none. */
|
|
40
|
+
export declare function requireConsoleKey(deps: ConsoleKeyDeps): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent` with an Anthropic Console login (`auth = console`).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `/login` → "Anthropic Console account" uses OAuth only to create a real
|
|
5
|
+
* Anthropic API key, which Claude Code stores and then sends as `x-api-key` ("/login
|
|
6
|
+
* managed key"). This module reads that key from where Claude Code keeps it, so the launch
|
|
7
|
+
* runs exactly like `auth = api-key` — same headers, same birth and claim, same identity —
|
|
8
|
+
* with no key for the user to copy. The key is read per launch and never stored by us.
|
|
9
|
+
*
|
|
10
|
+
* Where Claude Code keeps it (Claude Code 2.1.285, confirmed against the binary and a live
|
|
11
|
+
* probe on 2026-10-02):
|
|
12
|
+
* - macOS: the login keychain, service `Claude Code`, account $USER. When
|
|
13
|
+
* CLAUDE_CONFIG_DIR is set the service gains `-<first 8 hex of sha256(the raw
|
|
14
|
+
* CLAUDE_CONFIG_DIR value, NFC)>`; CLAUDE_SECURESTORAGE_CONFIG_DIR, when set, replaces
|
|
15
|
+
* that value (empty = no suffix).
|
|
16
|
+
* - everywhere (and the macOS fallback): `primaryApiKey` in Claude Code's .claude.json.
|
|
17
|
+
*
|
|
18
|
+
* A `claude auth login --console` *profile* (keyless OAuth, ~/.config/anthropic) creates no
|
|
19
|
+
* such key, so it is not read here.
|
|
20
|
+
*
|
|
21
|
+
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
const ACCOUNT_RE = /^[a-zA-Z0-9._-]+$/;
|
|
25
|
+
/** The keychain service name Claude Code stores the managed key under. */
|
|
26
|
+
export function claudeKeychainService(env) {
|
|
27
|
+
const secure = env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
|
|
28
|
+
const dir = secure !== undefined ? secure : (env.CLAUDE_CONFIG_DIR ?? "");
|
|
29
|
+
if (!dir)
|
|
30
|
+
return "Claude Code";
|
|
31
|
+
const hash = createHash("sha256").update(dir.normalize("NFC")).digest("hex").slice(0, 8);
|
|
32
|
+
return `Claude Code-${hash}`;
|
|
33
|
+
}
|
|
34
|
+
/** The keychain account Claude Code uses: $USER, else the OS username, else a fixed name. */
|
|
35
|
+
export function claudeKeychainAccount(env, username) {
|
|
36
|
+
let name;
|
|
37
|
+
try {
|
|
38
|
+
name = env.USER || username();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
name = "";
|
|
42
|
+
}
|
|
43
|
+
return ACCOUNT_RE.test(name) ? name : "claude-code-user";
|
|
44
|
+
}
|
|
45
|
+
/** The key is spliced into a newline-joined header block, so CR/LF must never survive. */
|
|
46
|
+
function clean(raw) {
|
|
47
|
+
const v = (raw ?? "").replace(/[\r\n]+/g, "").trim();
|
|
48
|
+
return v || undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Claude Code's Console-login API key on this machine, or undefined. Never throws. */
|
|
51
|
+
export function readConsoleKey(deps) {
|
|
52
|
+
if (deps.platform === "darwin") {
|
|
53
|
+
let fromKeychain;
|
|
54
|
+
try {
|
|
55
|
+
fromKeychain = clean(deps.readKeychain(claudeKeychainService(deps.env), claudeKeychainAccount(deps.env, deps.username)));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
fromKeychain = undefined;
|
|
59
|
+
}
|
|
60
|
+
if (fromKeychain)
|
|
61
|
+
return fromKeychain;
|
|
62
|
+
}
|
|
63
|
+
const json = deps.claudeJson;
|
|
64
|
+
const stored = json && typeof json === "object"
|
|
65
|
+
? json.primaryApiKey
|
|
66
|
+
: undefined;
|
|
67
|
+
return typeof stored === "string" ? clean(stored) : undefined;
|
|
68
|
+
}
|
|
69
|
+
/** The key for an `auth = console` launch; throws with what to do when there is none. */
|
|
70
|
+
export function requireConsoleKey(deps) {
|
|
71
|
+
const key = readConsoleKey(deps);
|
|
72
|
+
if (key)
|
|
73
|
+
return key;
|
|
74
|
+
throw new Error("mnemom agent: auth = console, but Claude Code has no Anthropic Console login on this machine.\n" +
|
|
75
|
+
' Sign in: run `claude`, then /login → "Anthropic Console account".\n' +
|
|
76
|
+
" Or use a key instead: `mnemom agent config set auth api-key`.");
|
|
77
|
+
}
|
|
@@ -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;
|
|
@@ -135,7 +135,7 @@ export const CONFIG_KEYS = [
|
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
key: "auth",
|
|
138
|
-
describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login`,
|
|
138
|
+
describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login, console = your Claude Code Anthropic Console login's key`,
|
|
139
139
|
get: (c) => c.auth,
|
|
140
140
|
set: (c, v) => {
|
|
141
141
|
c.auth = coerceAgentAuth(v);
|
|
@@ -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;
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Four ways Claude Code can be signed in, and what the gateway does with each today:
|
|
7
7
|
* - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
|
|
8
8
|
* - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
|
|
9
|
-
* - Anthropic Console login →
|
|
9
|
+
* - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
|
|
10
|
+
* API key Claude Code created is read from its keychain (lib/console-key.ts). A
|
|
11
|
+
* `claude auth login --console` profile creates no key → not supported yet.
|
|
10
12
|
* - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
|
|
11
13
|
* An Anthropic API key always works (`auth = api-key`, the default).
|
|
12
14
|
*
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
*
|
|
17
19
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
20
|
*/
|
|
19
|
-
import type
|
|
21
|
+
import { type AgentAuth } from "./subscription-login.js";
|
|
20
22
|
export type ThirdPartyProvider = "bedrock" | "vertex" | "foundry";
|
|
21
23
|
export type ClaudeLogin = {
|
|
22
24
|
kind: "subscription";
|
|
@@ -46,10 +48,13 @@ export type UnsupportedLogin = Extract<ClaudeLogin, {
|
|
|
46
48
|
/** Why a detected login can't be used through the gateway. */
|
|
47
49
|
export declare function unsupportedReason(login: UnsupportedLogin): string;
|
|
48
50
|
export declare const MENU_SUBSCRIPTION = "Claude subscription \u2014 Team or Enterprise (your claude.ai login)";
|
|
51
|
+
export declare const MENU_CONSOLE = "Anthropic Console \u2014 the API key your Claude Code /login created (API billing)";
|
|
49
52
|
export declare const MENU_API_KEY = "Anthropic API key";
|
|
50
53
|
export interface ChooseAuthDeps {
|
|
51
54
|
login: ClaudeLogin;
|
|
52
55
|
interactive: boolean;
|
|
56
|
+
/** Whether Claude Code's Console-login key is readable (lib/console-key.ts); asked only for a Console login. */
|
|
57
|
+
consoleKeyAvailable(): boolean;
|
|
53
58
|
select(question: string, options: string[]): Promise<string | null>;
|
|
54
59
|
persist(auth: AgentAuth): void;
|
|
55
60
|
say(line: string): void;
|
|
@@ -58,8 +63,9 @@ export interface ChooseAuthDeps {
|
|
|
58
63
|
* Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
|
|
59
64
|
* - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
|
|
60
65
|
* - A login we can't use → api-key, say why once, save it so the note isn't repeated.
|
|
61
|
-
* - Team/Enterprise
|
|
62
|
-
* auth without asking); interactive → a
|
|
66
|
+
* - Team/Enterprise, or a Console login whose key is readable, and non-interactive →
|
|
67
|
+
* api-key, nothing saved (never switch a script's auth without asking); interactive → a
|
|
68
|
+
* two-option menu (that login or an API key), answer saved.
|
|
63
69
|
*/
|
|
64
70
|
export declare function chooseAuth(deps: ChooseAuthDeps): Promise<AgentAuth>;
|
|
65
71
|
/** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
|
package/dist/lib/login-detect.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Four ways Claude Code can be signed in, and what the gateway does with each today:
|
|
7
7
|
* - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
|
|
8
8
|
* - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
|
|
9
|
-
* - Anthropic Console login →
|
|
9
|
+
* - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
|
|
10
|
+
* API key Claude Code created is read from its keychain (lib/console-key.ts). A
|
|
11
|
+
* `claude auth login --console` profile creates no key → not supported yet.
|
|
10
12
|
* - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
|
|
11
13
|
* An Anthropic API key always works (`auth = api-key`, the default).
|
|
12
14
|
*
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
*
|
|
17
19
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
20
|
*/
|
|
21
|
+
import { AGENT_AUTHS } from "./subscription-login.js";
|
|
19
22
|
const PROVIDER_SWITCHES = [
|
|
20
23
|
["CLAUDE_CODE_USE_BEDROCK", "bedrock"],
|
|
21
24
|
["CLAUDE_CODE_USE_VERTEX", "vertex"],
|
|
@@ -60,14 +63,15 @@ export function unsupportedReason(login) {
|
|
|
60
63
|
case "individual":
|
|
61
64
|
return `your Claude ${login.plan} plan is a personal plan — Mnemom supports subscription login on Team and Enterprise plans only`;
|
|
62
65
|
case "console":
|
|
63
|
-
return "Anthropic Console login is not supported yet";
|
|
66
|
+
return "your Anthropic Console login has no API key Claude Code created with /login (a `claude auth login --console` profile is not supported yet)";
|
|
64
67
|
case "third-party":
|
|
65
68
|
return `${PROVIDER_NAMES[login.provider]} is not supported yet`;
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
export const MENU_SUBSCRIPTION = "Claude subscription — Team or Enterprise (your claude.ai login)";
|
|
72
|
+
export const MENU_CONSOLE = "Anthropic Console — the API key your Claude Code /login created (API billing)";
|
|
69
73
|
export const MENU_API_KEY = "Anthropic API key";
|
|
70
|
-
const SAVED_HINT =
|
|
74
|
+
const SAVED_HINT = `change it any time with \`mnemom agent config set auth ${AGENT_AUTHS.join("|")}\``;
|
|
71
75
|
function save(deps, auth, line) {
|
|
72
76
|
try {
|
|
73
77
|
deps.persist(auth);
|
|
@@ -82,13 +86,27 @@ function save(deps, auth, line) {
|
|
|
82
86
|
* Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
|
|
83
87
|
* - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
|
|
84
88
|
* - A login we can't use → api-key, say why once, save it so the note isn't repeated.
|
|
85
|
-
* - Team/Enterprise
|
|
86
|
-
* auth without asking); interactive → a
|
|
89
|
+
* - Team/Enterprise, or a Console login whose key is readable, and non-interactive →
|
|
90
|
+
* api-key, nothing saved (never switch a script's auth without asking); interactive → a
|
|
91
|
+
* two-option menu (that login or an API key), answer saved.
|
|
87
92
|
*/
|
|
88
93
|
export async function chooseAuth(deps) {
|
|
89
94
|
const { login } = deps;
|
|
90
95
|
if (login.kind === "none")
|
|
91
96
|
return "api-key";
|
|
97
|
+
if (login.kind === "console" && deps.consoleKeyAvailable()) {
|
|
98
|
+
if (!deps.interactive)
|
|
99
|
+
return "api-key";
|
|
100
|
+
const picked = await deps.select("mnemom agent: Claude Code is signed in with an Anthropic Console account. How should it sign in through Mnemom?", [MENU_CONSOLE, MENU_API_KEY]);
|
|
101
|
+
if (picked === MENU_CONSOLE) {
|
|
102
|
+
return save(deps, "console", "mnemom agent: using your Anthropic Console login's API key.");
|
|
103
|
+
}
|
|
104
|
+
if (picked === MENU_API_KEY) {
|
|
105
|
+
return save(deps, "api-key", "mnemom agent: using an Anthropic API key.");
|
|
106
|
+
}
|
|
107
|
+
deps.say("mnemom agent: no choice made — using an Anthropic API key for this launch.");
|
|
108
|
+
return "api-key";
|
|
109
|
+
}
|
|
92
110
|
if (login.kind !== "subscription") {
|
|
93
111
|
return save(deps, "api-key", `mnemom agent: ${unsupportedReason(login)}, so this launch uses an Anthropic API key.`);
|
|
94
112
|
}
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
18
|
*/
|
|
19
19
|
import type { LaunchShape } from "./agent.js";
|
|
20
|
-
|
|
20
|
+
/** `console` = the API key Claude Code's Anthropic Console login created (lib/console-key.ts). */
|
|
21
|
+
export type AgentAuth = "api-key" | "subscription" | "console";
|
|
21
22
|
export declare const AGENT_AUTHS: readonly AgentAuth[];
|
|
22
23
|
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
23
24
|
export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
18
|
*/
|
|
19
|
-
export const AGENT_AUTHS = ["api-key", "subscription"];
|
|
19
|
+
export const AGENT_AUTHS = ["api-key", "subscription", "console"];
|
|
20
20
|
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
21
21
|
export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
22
22
|
/** Env override for the Mnemom key a subscription launch sends. */
|