@mnemom/mnemom 0.17.6-next.2 → 0.17.6

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.
@@ -20,7 +20,9 @@ import { mergeAgentConfig } from "../lib/agent-config.js";
20
20
  import { askSelect, askInput, isInteractive } from "../lib/prompt.js";
21
21
  import { getGatewayUrl } from "../lib/config.js";
22
22
  import { fmt } from "../lib/format.js";
23
+ import { NOOP_SESSION_CONFIG_B64, SESSION_CONFIG_HEADER } from "../lib/agent.js";
23
24
  const AGENT_HEADER = "x-mnemom-agent";
25
+ const MNEMOM_KEY_HEADER = "x-mnemom-api-key";
24
26
  const PROVIDERS = {
25
27
  anthropic: {
26
28
  label: "Anthropic",
@@ -208,6 +210,10 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
208
210
  [prov.keyHeader]: authValue,
209
211
  [AGENT_HEADER]: agentName,
210
212
  };
213
+ if (opts.mnemomKey) {
214
+ headers[MNEMOM_KEY_HEADER] = opts.mnemomKey;
215
+ headers[SESSION_CONFIG_HEADER] = NOOP_SESSION_CONFIG_B64;
216
+ }
211
217
  if (provider === "anthropic") {
212
218
  headers["anthropic-version"] = "2023-06-01";
213
219
  }
@@ -235,13 +241,24 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
235
241
  messages: [{ role: "user", content: "hello" }],
236
242
  });
237
243
  }
238
- const response = await fetch(url, {
244
+ const post = (h) => fetch(url, {
239
245
  method: "POST",
240
- headers,
246
+ headers: h,
241
247
  body,
242
248
  signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
243
249
  });
244
- const agentId = response.headers.get(AGENT_HEADER);
250
+ let response = await post(headers);
251
+ let agentId = response.headers.get(AGENT_HEADER);
252
+ // An agent an earlier plain call already birthed sits unclaimed in the sandbox org, and the
253
+ // gateway refuses agent traffic for it with 402 `agent_unclaimed` (and no agent id) — yet that
254
+ // is exactly the agent `mnemom agent setup` must claim. Ask once more as plain traffic (no
255
+ // session config): that serves it exactly as before and returns the id for the claim step.
256
+ if (!agentId && SESSION_CONFIG_HEADER in headers && (await isAgentUnclaimedRefusal(response))) {
257
+ const plain = { ...headers };
258
+ Reflect.deleteProperty(plain, SESSION_CONFIG_HEADER);
259
+ response = await post(plain);
260
+ agentId = response.headers.get(AGENT_HEADER);
261
+ }
245
262
  if (!response.ok && !agentId) {
246
263
  const text = await response.text().catch(() => "");
247
264
  throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
@@ -251,6 +268,22 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
251
268
  }
252
269
  return agentId;
253
270
  }
271
+ /**
272
+ * Is `response` the gateway's agent-traffic refusal for an agent still unclaimed in the sandbox
273
+ * org — 402 with `error.details.reason === "agent_unclaimed"`? Reads a clone, so the caller can
274
+ * still read the body. Anything unparseable is `false`.
275
+ */
276
+ async function isAgentUnclaimedRefusal(response) {
277
+ if (response.status !== 402)
278
+ return false;
279
+ try {
280
+ const body = (await response.clone().json());
281
+ return body?.error?.details?.reason === "agent_unclaimed";
282
+ }
283
+ catch {
284
+ return false;
285
+ }
286
+ }
254
287
  // ── Starter cards ─────────────────────────────────────────────────────────────
255
288
  function buildStarterAlignmentCard(agentId) {
256
289
  return {
package/dist/index.js CHANGED
@@ -175,7 +175,7 @@ const agentCmd = program
175
175
  .addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
176
176
  .option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
177
177
  .option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
178
- .addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
178
+ .addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "adaptive" sizes the fold to the conversation: no fold below 200k tokens, aggressive from 200k to 1M, balanced above. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
179
179
  // One validator for all three surfaces (flag, MNEMOM_AGENT_CONTEXT_PROFILE,
180
180
  // launcher.toml context_profile): trimmed, case-insensitive, same error wording.
181
181
  try {
@@ -293,6 +293,12 @@ export declare const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
293
293
  export declare const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKENS";
294
294
  export declare const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
295
295
  export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
296
+ /**
297
+ * The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
298
+ * size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
299
+ * stay at the gateway's own defaults.
300
+ */
301
+ export declare const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
296
302
  /**
297
303
  * The named presets for how the gateway folds a session's context (`--context-profile`,
298
304
  * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
@@ -301,8 +307,11 @@ export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT
301
307
  * - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
302
308
  * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
303
309
  * earlier and keeps a tighter working set.
310
+ * - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
311
+ * conversation's original size: no fold below 200k tokens, the aggressive
312
+ * budgets from 200k to 1M, balanced above. Opt-in; never the default.
304
313
  */
305
- export declare const CONTEXT_PROFILES: readonly ["off", "balanced", "aggressive"];
314
+ export declare const CONTEXT_PROFILES: readonly ["off", "balanced", "aggressive", "adaptive"];
306
315
  export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
307
316
  /**
308
317
  * The gateway env key that gates the whole context-handoff capability (level lines +
@@ -462,8 +471,10 @@ export declare function launchPreferenceExplicit(opts: {
462
471
  * Has an orchestrator already run the session + µ-balance preflight for this
463
472
  * launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
464
473
  * checked sign-in and balance themselves before spawning many launches; it skips
465
- * the per-launch session/personal-org/balance round trips. The Mnemom Agent
466
- * entitlement (which also gates Remote Control, fail-closed) is still fetched.
474
+ * the per-launch session prompt and balance round trips. The launch org still
475
+ * resolves as on a normal launch (active org, else personal org — the launch's
476
+ * Mnemom key is minted for it), and the Mnemom Agent entitlement (which also
477
+ * gates Remote Control, fail-closed) is still fetched for it.
467
478
  * Only the exact value `1` counts — anything else, including `true` or ` 1`, is
468
479
  * a normal launch.
469
480
  */
@@ -512,7 +523,11 @@ export declare function resolveRemoteControlShape(args: {
512
523
  export declare function buildCustomHeaders(opts: {
513
524
  /** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
514
525
  anthropicKey?: string;
515
- /** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
526
+ /**
527
+ * The Mnemom key every `mnemom agent` launch sends (lib/subscription-login.ts
528
+ * resolveLaunchMnemomKey) — the gateway refuses the session without it, and on a subscription
529
+ * login it is also the governed-identity anchor.
530
+ */
516
531
  mnemomKey?: string;
517
532
  agent: string;
518
533
  conversationId: string;
@@ -543,7 +558,7 @@ export declare function parseContextProfile(raw: string, source: string): Contex
543
558
  /**
544
559
  * Which context profile this launch runs under. Precedence, highest wins:
545
560
  * 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
546
- * balanced|aggressive` it is a CONFLICT and the launch refuses (never a silent override);
561
+ * balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
547
562
  * `--context-profile off` agrees with it.
548
563
  * 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
549
564
  * profile wins);
@@ -561,8 +576,8 @@ export declare function resolveContextProfile(opts: {
561
576
  /**
562
577
  * The decoded per-session env overlay for this launch — the `config` object of the
563
578
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
564
- * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
565
- * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
579
+ * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
580
+ * `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
566
581
  * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
567
582
  * `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
568
583
  * cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
@@ -597,7 +612,8 @@ export declare function buildSessionConfigPayload(opts: {
597
612
  * nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
598
613
  * byte-identical to a launch before profiles existed). Independent overlays:
599
614
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
600
- * `aggressive` → the three fold-tuning keys, `balanced` → nothing;
615
+ * `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
616
+ * "banded"`, `balanced` → nothing;
601
617
  * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
602
618
  * `"on"` (the `compaction_intercept` setting);
603
619
  * - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
@@ -616,6 +632,14 @@ export declare function buildSessionConfigB64(opts: {
616
632
  compactAt?: number | null;
617
633
  experimental?: readonly string[];
618
634
  }): string | undefined;
635
+ /**
636
+ * A no-op `x-mnemom-session-config` value: `{ v: 1, config: {} }` overrides nothing (the gateway
637
+ * applies an empty set, exactly its own defaults). buildSessionConfigB64 returns `undefined` for
638
+ * the defaults — no header at all; this is for a call that must be Mnemom agent traffic (the
639
+ * gateway's agent-traffic gate keys on the header's presence) without changing any setting: the
640
+ * launcher's birth probe (commands/wrap.ts birthThroughGateway).
641
+ */
642
+ export declare const NOOP_SESSION_CONFIG_B64: string;
619
643
  /**
620
644
  * Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
621
645
  * this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
@@ -651,6 +675,12 @@ export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
651
675
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
652
676
  */
653
677
  export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
678
+ /**
679
+ * Beside `HANDOFF_RC_KEY_ENV_NAME`: the non-`ANTHROPIC_*` var `spawnRcCli` sets to the launch's
680
+ * Mnemom key when context handoff is on, so the handoff call carries the `x-mnemom-api-key` the
681
+ * gateway requires on every `mnemom agent` request.
682
+ */
683
+ export declare const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
654
684
  /**
655
685
  * How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
656
686
  * WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
@@ -682,6 +712,12 @@ export interface HandoffMcpParams {
682
712
  /** The gateway ORIGIN (e.g. https://gateway.mnemom.ai) — see `resolveHandoffGatewayHost`. */
683
713
  gatewayHost: string;
684
714
  anthropicKeySource: HandoffAnthropicKeySource;
715
+ /**
716
+ * Where the server reads the Mnemom key (`x-mnemom-api-key`) from — same contract as
717
+ * `anthropicKeySource`: `ANTHROPIC_CUSTOM_HEADERS` (its `x-mnemom-api-key:` line) on a terminal
718
+ * launch, `HANDOFF_RC_MNEMOM_KEY_ENV_NAME` under Remote Control. Absent ⇒ no Mnemom key header.
719
+ */
720
+ mnemomKeySource?: HandoffAnthropicKeySource;
685
721
  agent: string;
686
722
  conversationId: string;
687
723
  /** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
package/dist/lib/agent.js CHANGED
@@ -451,6 +451,12 @@ export const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
451
451
  export const CONTEXT_TAIL_BUDGET_KEY = "CONTEXT_RUNTIME_TAIL_BUDGET_TOKENS";
452
452
  export const CONTEXT_READ_CACHE_KEY = "CONTEXT_RUNTIME_READ_CACHE_TOKENS";
453
453
  export const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM";
454
+ /**
455
+ * The gateway key the `adaptive` context profile sets (allow-listed per session): turns on the
456
+ * size-banded fold policy. Only the literal `banded` enables it; the band thresholds and budgets
457
+ * stay at the gateway's own defaults.
458
+ */
459
+ export const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
454
460
  /**
455
461
  * The named presets for how the gateway folds a session's context (`--context-profile`,
456
462
  * launcher.toml `context_profile`, `MNEMOM_AGENT_CONTEXT_PROFILE`):
@@ -459,8 +465,11 @@ export const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT_RECLAIM
459
465
  * - `balanced` → NO fold keys: the gateway's own defaults stand. The default.
460
466
  * - `aggressive` → a smaller tail budget + read cache and head-breakpoint reclaim on: folds
461
467
  * earlier and keeps a tighter working set.
468
+ * - `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY: "banded"` — the gateway picks the fold by the
469
+ * conversation's original size: no fold below 200k tokens, the aggressive
470
+ * budgets from 200k to 1M, balanced above. Opt-in; never the default.
462
471
  */
463
- export const CONTEXT_PROFILES = ["off", "balanced", "aggressive"];
472
+ export const CONTEXT_PROFILES = ["off", "balanced", "aggressive", "adaptive"];
464
473
  /** The per-session env overlay each profile expands to (key order is the wire order). */
465
474
  const CONTEXT_PROFILE_CONFIG = {
466
475
  off: [[CONTEXT_RUNTIME_KEY, "false"]],
@@ -470,6 +479,7 @@ const CONTEXT_PROFILE_CONFIG = {
470
479
  [CONTEXT_READ_CACHE_KEY, "8000"],
471
480
  [CONTEXT_HEAD_RECLAIM_KEY, "true"],
472
481
  ],
482
+ adaptive: [[CONTEXT_SIZE_POLICY_KEY, "banded"]],
473
483
  };
474
484
  /**
475
485
  * The gateway env key that gates the whole context-handoff capability (level lines +
@@ -798,8 +808,10 @@ export function launchPreferenceExplicit(opts, env = process.env) {
798
808
  * Has an orchestrator already run the session + µ-balance preflight for this
799
809
  * launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
800
810
  * checked sign-in and balance themselves before spawning many launches; it skips
801
- * the per-launch session/personal-org/balance round trips. The Mnemom Agent
802
- * entitlement (which also gates Remote Control, fail-closed) is still fetched.
811
+ * the per-launch session prompt and balance round trips. The launch org still
812
+ * resolves as on a normal launch (active org, else personal org — the launch's
813
+ * Mnemom key is minted for it), and the Mnemom Agent entitlement (which also
814
+ * gates Remote Control, fail-closed) is still fetched for it.
803
815
  * Only the exact value `1` counts — anything else, including `true` or ` 1`, is
804
816
  * a normal launch.
805
817
  */
@@ -902,7 +914,7 @@ export function parseContextProfile(raw, source) {
902
914
  /**
903
915
  * Which context profile this launch runs under. Precedence, highest wins:
904
916
  * 1. `--no-context` — always `off`. Combined with an explicit `--context-profile
905
- * balanced|aggressive` it is a CONFLICT and the launch refuses (never a silent override);
917
+ * balanced|aggressive|adaptive` it is a CONFLICT and the launch refuses (never a silent override);
906
918
  * `--context-profile off` agrees with it.
907
919
  * 2. an explicit `--context-profile <profile>` flag (`--context` alongside it is fine — the
908
920
  * profile wins);
@@ -931,8 +943,8 @@ export function resolveContextProfile(opts) {
931
943
  /**
932
944
  * The decoded per-session env overlay for this launch — the `config` object of the
933
945
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
934
- * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
935
- * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
946
+ * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`;
947
+ * `CONTEXT_RUNTIME_SIZE_POLICY` for `adaptive`; nothing for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
936
948
  * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
937
949
  * `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the target
938
950
  * cannot be honored without the intercept), then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`
@@ -977,7 +989,8 @@ export function buildSessionConfigPayload(opts) {
977
989
  * nothing to override (the gateway's own defaults stand — `balanced` with everything else off,
978
990
  * byte-identical to a launch before profiles existed). Independent overlays:
979
991
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
980
- * `aggressive` → the three fold-tuning keys, `balanced` → nothing;
992
+ * `aggressive` → the three fold-tuning keys, `adaptive` → `CONTEXT_RUNTIME_SIZE_POLICY:
993
+ * "banded"`, `balanced` → nothing;
981
994
  * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
982
995
  * `"on"` (the `compaction_intercept` setting);
983
996
  * - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
@@ -995,6 +1008,14 @@ export function buildSessionConfigB64(opts) {
995
1008
  return undefined;
996
1009
  return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
997
1010
  }
1011
+ /**
1012
+ * A no-op `x-mnemom-session-config` value: `{ v: 1, config: {} }` overrides nothing (the gateway
1013
+ * applies an empty set, exactly its own defaults). buildSessionConfigB64 returns `undefined` for
1014
+ * the defaults — no header at all; this is for a call that must be Mnemom agent traffic (the
1015
+ * gateway's agent-traffic gate keys on the header's presence) without changing any setting: the
1016
+ * launcher's birth probe (commands/wrap.ts birthThroughGateway).
1017
+ */
1018
+ export const NOOP_SESSION_CONFIG_B64 = Buffer.from(JSON.stringify({ v: 1, config: {} }), "utf8").toString("base64url");
998
1019
  /**
999
1020
  * Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
1000
1021
  * this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
@@ -1038,6 +1059,12 @@ export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
1038
1059
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
1039
1060
  */
1040
1061
  export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
1062
+ /**
1063
+ * Beside `HANDOFF_RC_KEY_ENV_NAME`: the non-`ANTHROPIC_*` var `spawnRcCli` sets to the launch's
1064
+ * Mnemom key when context handoff is on, so the handoff call carries the `x-mnemom-api-key` the
1065
+ * gateway requires on every `mnemom agent` request.
1066
+ */
1067
+ export const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
1041
1068
  /** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
1042
1069
  export function buildHandoffMcpConfig(p) {
1043
1070
  const env = {
@@ -1046,6 +1073,8 @@ export function buildHandoffMcpConfig(p) {
1046
1073
  MNEMOM_HANDOFF_AGENT: p.agent,
1047
1074
  MNEMOM_HANDOFF_CONVERSATION_ID: p.conversationId,
1048
1075
  };
1076
+ if (p.mnemomKeySource)
1077
+ env.MNEMOM_HANDOFF_MNEMOM_KEY_ENV_NAME = p.mnemomKeySource.envName;
1049
1078
  if (p.sessionConfigB64)
1050
1079
  env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
1051
1080
  return {
package/dist/lib/api.d.ts CHANGED
@@ -1069,6 +1069,27 @@ export declare function listApiKeys(): Promise<ApiKeyListItem[]>;
1069
1069
  * for non-staff, admin:org for users not in any org-admin role).
1070
1070
  */
1071
1071
  export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
1072
+ /**
1073
+ * POST /v1/orgs/{org_id}/api-keys — mint an ORG-owned key (its `org_id` is that org, so the
1074
+ * gateway births a new agent there). Owners and admins may mint any scopes; a member may mint a
1075
+ * key whose scopes are exactly ["gateway"]. A refusal (viewer/auditor, a non-member, or an API
1076
+ * that predates member org keys) is thrown as MnemomApiError with its status (403/404).
1077
+ */
1078
+ export declare function createOrgApiKey(orgId: string, name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
1079
+ /**
1080
+ * GET /v1/api-keys/whoami authenticated by `key` ALONE (`X-Mnemom-Api-Key`, no session): does
1081
+ * the API accept this key? true = 200. false = 401/403 — revoked, rotated or unknown, OR a valid
1082
+ * `gateway`-only key: the API refuses every key without an `api:*` scope (ADR-049), so false
1083
+ * alone never proves a key dead. Throws on anything else (network, timeout, 429, 5xx): the
1084
+ * caller reads that as "could not check".
1085
+ */
1086
+ export declare function apiAcceptsMnemomKey(key: string, timeoutMs: number): Promise<boolean>;
1087
+ /**
1088
+ * GET /v1/api-keys with a timeout, for checking a key: the signed-in caller's ACTIVE keys (a
1089
+ * revoked or rotated key is absent). null when there is no usable session (no credential, or
1090
+ * 401/403 after the refresh retry). Throws on anything else (network, timeout, 5xx).
1091
+ */
1092
+ export declare function listOwnApiKeysForCheck(timeoutMs: number): Promise<ApiKeyListItem[] | null>;
1072
1093
  /**
1073
1094
  * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
1074
1095
  * Returns the full new secret only on this call. The new key inherits
@@ -1078,9 +1099,13 @@ export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promi
1078
1099
  export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
1079
1100
  /**
1080
1101
  * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
1081
- * audit; `is_active` flips to false and `revoked_at` is timestamped.
1102
+ * audit; `is_active` flips to false and `revoked_at` is timestamped. Only
1103
+ * a key the signed-in user created (an org key they minted included) is
1104
+ * found; anything else is a 404. `timeoutMs` bounds the call.
1082
1105
  */
1083
- export declare function revokeApiKey(keyId: string): Promise<void>;
1106
+ export declare function revokeApiKey(keyId: string, opts?: {
1107
+ timeoutMs?: number;
1108
+ }): Promise<void>;
1084
1109
  export interface LicenseValidationResult {
1085
1110
  valid: boolean;
1086
1111
  license_id?: string;
package/dist/lib/api.js CHANGED
@@ -1385,6 +1385,65 @@ export async function createApiKey(name, scopes) {
1385
1385
  }
1386
1386
  return (await response.json());
1387
1387
  }
1388
+ /**
1389
+ * POST /v1/orgs/{org_id}/api-keys — mint an ORG-owned key (its `org_id` is that org, so the
1390
+ * gateway births a new agent there). Owners and admins may mint any scopes; a member may mint a
1391
+ * key whose scopes are exactly ["gateway"]. A refusal (viewer/auditor, a non-member, or an API
1392
+ * that predates member org keys) is thrown as MnemomApiError with its status (403/404).
1393
+ */
1394
+ export async function createOrgApiKey(orgId, name, scopes) {
1395
+ const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(sanitizeForHttp(orgId))}/api-keys`);
1396
+ const response = await fetchWithAuthRetry(url, async () => ({
1397
+ method: "POST",
1398
+ headers: { ...(await authHeaders()), "Content-Type": "application/json" },
1399
+ body: JSON.stringify({ name, scopes }),
1400
+ }));
1401
+ if (!response.ok) {
1402
+ if (response.status === 401)
1403
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1404
+ throw await readApiError(response, "Failed to create org api key");
1405
+ }
1406
+ return (await response.json());
1407
+ }
1408
+ /**
1409
+ * GET /v1/api-keys/whoami authenticated by `key` ALONE (`X-Mnemom-Api-Key`, no session): does
1410
+ * the API accept this key? true = 200. false = 401/403 — revoked, rotated or unknown, OR a valid
1411
+ * `gateway`-only key: the API refuses every key without an `api:*` scope (ADR-049), so false
1412
+ * alone never proves a key dead. Throws on anything else (network, timeout, 429, 5xx): the
1413
+ * caller reads that as "could not check".
1414
+ */
1415
+ export async function apiAcceptsMnemomKey(key, timeoutMs) {
1416
+ const url = validateUrl(`${API_BASE}/v1/api-keys/whoami`);
1417
+ const response = await mnemomFetch(url, {
1418
+ headers: { "X-Mnemom-Api-Key": sanitizeForHttp(key), Accept: "application/json" },
1419
+ signal: AbortSignal.timeout(timeoutMs),
1420
+ });
1421
+ if (response.ok)
1422
+ return true;
1423
+ if (response.status === 401 || response.status === 403)
1424
+ return false;
1425
+ throw new MnemomApiError(response.status, `api-key whoami: HTTP ${response.status}`);
1426
+ }
1427
+ /**
1428
+ * GET /v1/api-keys with a timeout, for checking a key: the signed-in caller's ACTIVE keys (a
1429
+ * revoked or rotated key is absent). null when there is no usable session (no credential, or
1430
+ * 401/403 after the refresh retry). Throws on anything else (network, timeout, 5xx).
1431
+ */
1432
+ export async function listOwnApiKeysForCheck(timeoutMs) {
1433
+ if ((await resolveAuth()).type === "none")
1434
+ return null;
1435
+ const url = validateUrl(`${API_BASE}/v1/api-keys`);
1436
+ const response = await fetchWithAuthRetry(url, async () => ({
1437
+ headers: { ...(await authHeaders()), Accept: "application/json" },
1438
+ signal: AbortSignal.timeout(timeoutMs),
1439
+ }));
1440
+ if (response.status === 401 || response.status === 403)
1441
+ return null;
1442
+ if (!response.ok) {
1443
+ throw new MnemomApiError(response.status, `list api keys: HTTP ${response.status}`);
1444
+ }
1445
+ return (await response.json()).keys ?? [];
1446
+ }
1388
1447
  /**
1389
1448
  * POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
1390
1449
  * Returns the full new secret only on this call. The new key inherits
@@ -1410,13 +1469,16 @@ export async function rotateApiKey(keyId) {
1410
1469
  }
1411
1470
  /**
1412
1471
  * DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
1413
- * audit; `is_active` flips to false and `revoked_at` is timestamped.
1472
+ * audit; `is_active` flips to false and `revoked_at` is timestamped. Only
1473
+ * a key the signed-in user created (an org key they minted included) is
1474
+ * found; anything else is a 404. `timeoutMs` bounds the call.
1414
1475
  */
1415
- export async function revokeApiKey(keyId) {
1476
+ export async function revokeApiKey(keyId, opts = {}) {
1416
1477
  const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}`);
1417
1478
  const response = await fetchWithAuthRetry(url, async () => ({
1418
1479
  method: "DELETE",
1419
1480
  headers: await authHeaders(),
1481
+ signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
1420
1482
  }));
1421
1483
  if (!response.ok) {
1422
1484
  if (response.status === 401) {
@@ -58,7 +58,8 @@ export interface LauncherConfig {
58
58
  /**
59
59
  * How the gateway folds each session's context (`balanced` when unset): `off` leaves
60
60
  * compaction to the coding agent, `balanced` keeps more earlier work verbatim, `aggressive`
61
- * folds earlier with a tighter working set. `--context-profile` overrides it per launch.
61
+ * folds earlier with a tighter working set, `adaptive` sizes the fold to the conversation
62
+ * (none below 200k tokens, aggressive up to 1M, balanced above). `--context-profile` overrides it per launch.
62
63
  */
63
64
  contextProfile?: ContextProfile;
64
65
  /**