@mnemom/mnemom 0.17.25 → 0.17.26

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.
Files changed (43) hide show
  1. package/dist/commands/agent-config.js +9 -0
  2. package/dist/commands/agent-doctor.d.ts +3 -2
  3. package/dist/commands/agent-doctor.js +22 -7
  4. package/dist/commands/agent-goal.js +7 -2
  5. package/dist/commands/agent-sessions.js +2 -1
  6. package/dist/commands/agent-setup.d.ts +104 -4
  7. package/dist/commands/agent-setup.js +414 -18
  8. package/dist/commands/agent-slack.d.ts +4 -0
  9. package/dist/commands/agent-slack.js +67 -21
  10. package/dist/commands/agent.d.ts +29 -1
  11. package/dist/commands/agent.js +89 -12
  12. package/dist/commands/wrap.d.ts +2 -0
  13. package/dist/commands/wrap.js +13 -1
  14. package/dist/index.js +19 -5
  15. package/dist/lib/agent-identity-route.d.ts +21 -3
  16. package/dist/lib/agent-identity-route.js +42 -15
  17. package/dist/lib/agent-ledger.d.ts +5 -0
  18. package/dist/lib/agent.d.ts +62 -1
  19. package/dist/lib/agent.js +55 -0
  20. package/dist/lib/api.d.ts +33 -1
  21. package/dist/lib/api.js +10 -0
  22. package/dist/lib/experimental.js +14 -2
  23. package/dist/lib/goal-api.d.ts +3 -3
  24. package/dist/lib/goal-api.js +5 -2
  25. package/dist/lib/launcher-config.d.ts +18 -0
  26. package/dist/lib/launcher-config.js +47 -1
  27. package/dist/lib/oauth.d.ts +7 -0
  28. package/dist/lib/oauth.js +92 -17
  29. package/dist/lib/served-agent-cache.d.ts +8 -1
  30. package/dist/lib/served-agent-cache.js +11 -2
  31. package/dist/lib/slack-channel.d.ts +1 -0
  32. package/dist/lib/slack-channel.js +30 -12
  33. package/dist/lib/slack-route.d.ts +72 -9
  34. package/dist/lib/slack-route.js +113 -24
  35. package/dist/lib/slack-router.d.ts +37 -2
  36. package/dist/lib/slack-router.js +98 -13
  37. package/dist/lib/slack-state.d.ts +105 -4
  38. package/dist/lib/slack-state.js +230 -11
  39. package/dist/lib/subscription-login.d.ts +12 -3
  40. package/dist/lib/subscription-login.js +19 -7
  41. package/dist/mcp/handoff-server.mjs +31 -3
  42. package/dist/rc-proxy.mjs +14 -0
  43. package/package.json +1 -1
@@ -24,34 +24,61 @@ export class IdentityRouteMissingError extends Error {
24
24
  this.name = "IdentityRouteMissingError";
25
25
  }
26
26
  }
27
+ /** `{error:{code,message}}` (the gateway's envelope) → its code; anything else → undefined. */
28
+ function errorCode(text) {
29
+ try {
30
+ const body = JSON.parse(text);
31
+ const code = body?.error?.code;
32
+ return typeof code === "string" ? code : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
27
38
  /**
28
- * The mnm- id the gateway serves for (Mnemom key, name). Its response also carries claimed /
29
- * org_id / resolved_by / created; the launcher reads the claim state from the api instead, so
30
- * only the id is used here.
39
+ * The mnm- id the gateway serves for (Mnemom key, name[, pin]). Its response also carries
40
+ * claimed / org_id / created; the launcher reads the claim state from the api instead. A non-2xx
41
+ * answer throws a MnemomApiError carrying the gateway's error code (agent_pin_forbidden,
42
+ * agent_pin_mismatch, agent_name_ambiguous, mnemom_api_key_required, …) so the caller can say
43
+ * exactly what to fix.
31
44
  */
32
- export async function resolveServedIdentity(gatewayUrl, agentName, mnemomKey, opts = {}) {
45
+ export async function resolveServedIdentityDetail(gatewayUrl, agentName, mnemomKey, opts = {}) {
46
+ const headers = {
47
+ Accept: "application/json",
48
+ "x-mnemom-api-key": mnemomKey,
49
+ "x-mnemom-agent": agentName,
50
+ };
51
+ // AGENT_ID_HEADER (lib/agent.ts), spelled out so this module imports nothing from the launcher.
52
+ if (opts.pin)
53
+ headers["x-mnemom-agent-id"] = opts.pin;
33
54
  const response = await fetch(`${gatewayUrl}${IDENTITY_ROUTE_PATH}`, {
34
55
  method: "POST",
35
- headers: {
36
- Accept: "application/json",
37
- "x-mnemom-api-key": mnemomKey,
38
- "x-mnemom-agent": agentName,
39
- },
56
+ headers,
40
57
  signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
41
58
  });
42
59
  if (response.status === 404)
43
60
  throw new IdentityRouteMissingError(gatewayUrl);
44
- if (response.status === 401) {
45
- throw new MnemomApiError(401, "the gateway rejected your Mnemom API key");
46
- }
47
61
  if (!response.ok) {
48
62
  const text = await response.text().catch(() => "");
49
- throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
63
+ const code = errorCode(text);
64
+ if (response.status === 401 && code !== "mnemom_api_key_required") {
65
+ throw new MnemomApiError(401, "the gateway rejected your Mnemom API key", { code });
66
+ }
67
+ throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`, { code });
50
68
  }
51
69
  const body = (await response.json().catch(() => null));
52
70
  const agentId = body?.agent_id;
53
- if (typeof agentId !== "string" || !/^mnm-/.test(agentId)) {
71
+ // Only `mnm-` + [A-Za-z0-9-]: the id is echoed into request headers (x-mnemom-agent-id), the
72
+ // sessions ledger and a printed resume hint, so nothing else from the gateway is trusted there.
73
+ if (typeof agentId !== "string" || !/^mnm-[A-Za-z0-9-]+$/.test(agentId)) {
54
74
  throw new Error(`Gateway returned no mnm- agent id from ${IDENTITY_ROUTE_PATH}.`);
55
75
  }
56
- return agentId;
76
+ return {
77
+ agentId,
78
+ resolvedBy: typeof body?.resolved_by === "string" ? body.resolved_by : null,
79
+ };
80
+ }
81
+ /** {@link resolveServedIdentityDetail}, the id only (an unpinned launch). */
82
+ export async function resolveServedIdentity(gatewayUrl, agentName, mnemomKey, opts = {}) {
83
+ return (await resolveServedIdentityDetail(gatewayUrl, agentName, mnemomKey, opts)).agentId;
57
84
  }
@@ -49,6 +49,11 @@ export interface SessionRecord {
49
49
  agent_slug: string;
50
50
  /** The agent record id when it resolved at launch, else null. */
51
51
  agent_id: string | null;
52
+ /**
53
+ * true when the launch PINNED `agent_id` (`--agent mnm-<uuid>` / launcher.toml `agent_id`), so
54
+ * a resume pins it again. Absent on an unpinned launch (and on every row written before pins).
55
+ */
56
+ agent_pinned?: true;
52
57
  org_id: string;
53
58
  org_label: string;
54
59
  gateway_host: string;
@@ -134,6 +134,52 @@ export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
134
134
  * session's model traffic went elsewhere (MNE-7564 review).
135
135
  */
136
136
  export declare function resolveHandoffGatewayHost(door: string): string;
137
+ /**
138
+ * The explicit agent pin header (`x-mnemom-agent-id: mnm-<uuid>`). The gateway honours it only
139
+ * with a verified Mnemom key, serves exactly that agent (the caller's own, claimed, live one) and
140
+ * never falls back to a name or births an agent; otherwise it refuses (400 agent_pin_mismatch,
141
+ * 403 agent_pin_forbidden, 401 mnemom_api_key_required).
142
+ */
143
+ export declare const AGENT_ID_HEADER = "x-mnemom-agent-id";
144
+ /** A full Mnemom agent id: `mnm-` + a lowercase UUID. */
145
+ export declare const AGENT_ID_RE: RegExp;
146
+ /** What an `--agent` / launcher.toml `agent` / `agent_id` value names. */
147
+ export type AgentRef = {
148
+ kind: "id";
149
+ id: string;
150
+ } | {
151
+ kind: "name";
152
+ name: string;
153
+ }
154
+ /** Starts with `mnm-` but is not a full id (a truncated/garbled paste). */
155
+ | {
156
+ kind: "malformed-id";
157
+ raw: string;
158
+ };
159
+ /**
160
+ * Classify an agent reference. A full `mnm-<uuid>` (any case — ids are lowercase on the wire, so
161
+ * it is lowercased) is an ID: the launch pins that exact agent. Anything else starting with
162
+ * `mnm-` is a MALFORMED id and refused by the caller: treating it as a name would make the
163
+ * gateway birth a brand-new agent named after the id (the 2026-10-08 restart-resume strays). Any
164
+ * other value is a NAME.
165
+ */
166
+ export declare function parseAgentRef(raw: string): AgentRef;
167
+ /**
168
+ * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
169
+ * outright — an id there pins, a name there launches by name even when launcher.toml pins;
170
+ * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
171
+ * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
172
+ */
173
+ export declare function selectLaunchAgent(sources: {
174
+ flag?: string;
175
+ configAgent?: string;
176
+ configAgentId?: string;
177
+ }): {
178
+ agent?: string;
179
+ agentId?: string;
180
+ };
181
+ /** The refusal for a `mnm-…` value that is not a full agent id. */
182
+ export declare function malformedAgentIdMessage(raw: string, source: string): string;
137
183
  /**
138
184
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
139
185
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -597,9 +643,14 @@ export declare function resolveRemoteControlShape(args: {
597
643
  export declare function buildCustomHeaders(opts: {
598
644
  /** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
599
645
  anthropicKey?: string;
600
- /** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
646
+ /**
647
+ * The Mnemom key: the governed-identity anchor on a subscription-login launch, and the
648
+ * principal an api-key launch proves when one is stored (lib/subscription-login.ts).
649
+ */
601
650
  mnemomKey?: string;
602
651
  agent: string;
652
+ /** The pinned agent id (`--agent mnm-<uuid>` / launcher.toml `agent_id`), sent as AGENT_ID_HEADER. */
653
+ agentId?: string;
603
654
  conversationId: string;
604
655
  contractB64?: string;
605
656
  /** `buildGoalModeHeader` output for an `--implicit` launch. */
@@ -769,6 +820,14 @@ export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
769
820
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
770
821
  */
771
822
  export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
823
+ /**
824
+ * The Remote Control counterpart for the Mnemom key: under RC the key rides only the proxy's
825
+ * stdin, so when the handoff tool is registered on a launch that has one, `spawnRcCli` sets this
826
+ * dedicated, non-ANTHROPIC var (same reasoning as HANDOFF_RC_KEY_ENV_NAME). The terminal shape
827
+ * needs none — the server reads the `x-mnemom-api-key:` line of its inherited
828
+ * `ANTHROPIC_CUSTOM_HEADERS`.
829
+ */
830
+ export declare const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
772
831
  /**
773
832
  * How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
774
833
  * WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
@@ -801,6 +860,8 @@ export interface HandoffMcpParams {
801
860
  gatewayHost: string;
802
861
  anthropicKeySource: HandoffAnthropicKeySource;
803
862
  agent: string;
863
+ /** The pinned agent id, when the launch pins one — the handoff call carries the same pin. */
864
+ agentId?: string;
804
865
  conversationId: string;
805
866
  /** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
806
867
  sessionConfigB64?: string;
package/dist/lib/agent.js CHANGED
@@ -108,6 +108,50 @@ export function isProdGateway(env = process.env) {
108
108
  export function resolveHandoffGatewayHost(door) {
109
109
  return new URL(door).origin;
110
110
  }
111
+ /**
112
+ * The explicit agent pin header (`x-mnemom-agent-id: mnm-<uuid>`). The gateway honours it only
113
+ * with a verified Mnemom key, serves exactly that agent (the caller's own, claimed, live one) and
114
+ * never falls back to a name or births an agent; otherwise it refuses (400 agent_pin_mismatch,
115
+ * 403 agent_pin_forbidden, 401 mnemom_api_key_required).
116
+ */
117
+ export const AGENT_ID_HEADER = "x-mnemom-agent-id";
118
+ /** A full Mnemom agent id: `mnm-` + a lowercase UUID. */
119
+ export const AGENT_ID_RE = /^mnm-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
120
+ /**
121
+ * Classify an agent reference. A full `mnm-<uuid>` (any case — ids are lowercase on the wire, so
122
+ * it is lowercased) is an ID: the launch pins that exact agent. Anything else starting with
123
+ * `mnm-` is a MALFORMED id and refused by the caller: treating it as a name would make the
124
+ * gateway birth a brand-new agent named after the id (the 2026-10-08 restart-resume strays). Any
125
+ * other value is a NAME.
126
+ */
127
+ export function parseAgentRef(raw) {
128
+ const v = raw.trim();
129
+ const lower = v.toLowerCase();
130
+ if (AGENT_ID_RE.test(lower))
131
+ return { kind: "id", id: lower };
132
+ if (lower.startsWith("mnm-"))
133
+ return { kind: "malformed-id", raw: v };
134
+ return { kind: "name", name: v };
135
+ }
136
+ /**
137
+ * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
138
+ * outright — an id there pins, a name there launches by name even when launcher.toml pins;
139
+ * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
140
+ * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
141
+ */
142
+ export function selectLaunchAgent(sources) {
143
+ if (sources.flag?.trim())
144
+ return { agent: sources.flag };
145
+ if (sources.configAgentId?.trim())
146
+ return { agentId: sources.configAgentId };
147
+ return sources.configAgent?.trim() ? { agent: sources.configAgent } : {};
148
+ }
149
+ /** The refusal for a `mnm-…` value that is not a full agent id. */
150
+ export function malformedAgentIdMessage(raw, source) {
151
+ return (`mnemom agent: ${source} '${raw}' looks like an agent id but isn't a full one (mnm-<uuid>). ` +
152
+ "Pass the complete id (see `mnemom agents`), or a plain agent name. " +
153
+ "It is not used as a name: that would create a new agent named after the id.");
154
+ }
111
155
  /**
112
156
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
113
157
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -966,6 +1010,7 @@ export function buildCustomHeaders(opts) {
966
1010
  ...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
967
1011
  ...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
968
1012
  `x-mnemom-agent: ${opts.agent}`,
1013
+ ...(opts.agentId ? [`${AGENT_ID_HEADER}: ${opts.agentId}`] : []),
969
1014
  `x-mnemom-conversation-id: ${opts.conversationId}`,
970
1015
  ];
971
1016
  if (opts.contractB64)
@@ -1177,6 +1222,14 @@ export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
1177
1222
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
1178
1223
  */
1179
1224
  export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
1225
+ /**
1226
+ * The Remote Control counterpart for the Mnemom key: under RC the key rides only the proxy's
1227
+ * stdin, so when the handoff tool is registered on a launch that has one, `spawnRcCli` sets this
1228
+ * dedicated, non-ANTHROPIC var (same reasoning as HANDOFF_RC_KEY_ENV_NAME). The terminal shape
1229
+ * needs none — the server reads the `x-mnemom-api-key:` line of its inherited
1230
+ * `ANTHROPIC_CUSTOM_HEADERS`.
1231
+ */
1232
+ export const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
1180
1233
  /** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
1181
1234
  export function buildHandoffMcpConfig(p) {
1182
1235
  const env = {
@@ -1187,6 +1240,8 @@ export function buildHandoffMcpConfig(p) {
1187
1240
  };
1188
1241
  if (p.sessionConfigB64)
1189
1242
  env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
1243
+ if (p.agentId)
1244
+ env.MNEMOM_HANDOFF_AGENT_ID = p.agentId;
1190
1245
  return {
1191
1246
  mcpServers: {
1192
1247
  [HANDOFF_MCP_SERVER_NAME]: { command: p.nodeBin, args: [p.serverScript], env },
package/dist/lib/api.d.ts CHANGED
@@ -31,6 +31,13 @@ export interface Agent {
31
31
  claimed_at?: string | null;
32
32
  name?: string | null;
33
33
  claimed?: boolean;
34
+ /** Owner (org-member) view: the org the agent lives in. */
35
+ org_id?: string | null;
36
+ /**
37
+ * `org_member` on the full record, `authenticated` / `anonymous` on the public projection
38
+ * (handleGetAgent). Absent on an older api.
39
+ */
40
+ caller?: string;
34
41
  }
35
42
  /**
36
43
  * GET /v1/integrity/:id — components/schemas/IntegrityScore:
@@ -247,6 +254,13 @@ export interface AgentListItem {
247
254
  * routing here avoids the false "Not authenticated" error (MNE-194 F10).
248
255
  */
249
256
  export declare function listAgents(): Promise<AgentListItem[]>;
257
+ /**
258
+ * Every agent the caller OWNS, across all their orgs — `GET /v1/agents?scope=all`: rows whose
259
+ * `created_by` or `claimed_by` is the caller (MNE-191), newest first, up to 500. No org and no
260
+ * claim state on the rows (read those with {@link getAgent}); a tombstoned agent can still be
261
+ * listed and 404s there.
262
+ */
263
+ export declare function listMyAgents(): Promise<AgentListItem[]>;
250
264
  export interface OrgListItem {
251
265
  org_id: string;
252
266
  name: string;
@@ -645,7 +659,9 @@ export declare function putProtectionCard(agentId: string, body: string, content
645
659
  /**
646
660
  * The per-layer view of an agent's card (ADR-053 `?include=sources`): the raw
647
661
  * card the agent layer itself carries, next to the composed card every layer
648
- * produced. We type the two rows we read; the platform/org/teams rows ride along.
662
+ * produced. The platform, org and team rows are optional here because an older
663
+ * api, or a test double, can omit them. Shape per mnemom-api
664
+ * `buildAgentSourcesEnvelope` (src/composition/agent-handlers.ts).
649
665
  */
650
666
  export interface CardSources {
651
667
  agent: {
@@ -656,6 +672,22 @@ export interface CardSources {
656
672
  card_json: Record<string, unknown> | null;
657
673
  available: boolean;
658
674
  };
675
+ platform?: {
676
+ card_json: Record<string, unknown> | null;
677
+ available: boolean;
678
+ };
679
+ org?: {
680
+ card_json: Record<string, unknown> | null;
681
+ available: boolean;
682
+ org_id?: string | null;
683
+ org_name?: string | null;
684
+ };
685
+ teams?: Array<{
686
+ team_id: string;
687
+ team_name?: string | null;
688
+ card_json: Record<string, unknown> | null;
689
+ available: boolean;
690
+ }>;
659
691
  [key: string]: unknown;
660
692
  }
661
693
  /**
package/dist/lib/api.js CHANGED
@@ -211,6 +211,16 @@ export async function listAgents() {
211
211
  const data = await gFetch("/v1/agents?limit=100", {}, { fallback: "Failed to list agents" });
212
212
  return data.agents ?? [];
213
213
  }
214
+ /**
215
+ * Every agent the caller OWNS, across all their orgs — `GET /v1/agents?scope=all`: rows whose
216
+ * `created_by` or `claimed_by` is the caller (MNE-191), newest first, up to 500. No org and no
217
+ * claim state on the rows (read those with {@link getAgent}); a tombstoned agent can still be
218
+ * listed and 404s there.
219
+ */
220
+ export async function listMyAgents() {
221
+ const data = await gFetch("/v1/agents?scope=all&limit=500", {}, { fallback: "Failed to list your agents" });
222
+ return data.agents ?? [];
223
+ }
214
224
  /**
215
225
  * GET /v1/orgs — list every org the user is a member of, including the
216
226
  * personal-org-of-one (per ADR-044 Option A, GitHub model). Personal sorts
@@ -50,7 +50,13 @@ export const EXPERIMENTAL_FEATURES = [
50
50
  since: "0.17.3",
51
51
  gates: {
52
52
  flags: ["--slack"],
53
- configKeys: ["slack_channel", "slack_listen", "slack_allow"],
53
+ configKeys: [
54
+ "slack_channel",
55
+ "slack_listen",
56
+ "slack_allow",
57
+ "slack_aliases",
58
+ "slack_sticky_threads",
59
+ ],
54
60
  commands: ["mnemom agent slack"],
55
61
  env: [],
56
62
  },
@@ -156,6 +162,8 @@ export function experimentalIdForConfigKey(key, value) {
156
162
  case "slack_allow":
157
163
  return "slack";
158
164
  case "slack_listen":
165
+ case "slack_aliases":
166
+ case "slack_sticky_threads":
159
167
  return value === "true" ? "slack" : undefined;
160
168
  case "context_handoff":
161
169
  return value === "true" ? "context-handoff" : undefined;
@@ -229,7 +237,11 @@ export function migrateExperimental(deps) {
229
237
  }
230
238
  /** The feature a launcher.toml key belongs to at all (for marking `mnemom agent config list`). */
231
239
  export function configKeyFeature(key) {
232
- if (key === "slack_channel" || key === "slack_listen" || key === "slack_allow")
240
+ if (key === "slack_channel" ||
241
+ key === "slack_listen" ||
242
+ key === "slack_allow" ||
243
+ key === "slack_aliases" ||
244
+ key === "slack_sticky_threads")
233
245
  return "slack";
234
246
  if (key === "context_handoff")
235
247
  return "context-handoff";
@@ -7,8 +7,8 @@
7
7
  * POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
8
8
  *
9
9
  * Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
10
- * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
11
- * launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
10
+ * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, `x-mnemom-api-key` when the
11
+ * launch had a Mnemom key, and `x-mnemom-agent-id: <record.agent_id>` when the launch pinned it. The gateway hashes them exactly as a proxied request and only LOOKS
12
12
  * UP the agent. Key material is never printed: errors carry the status, the gateway's error code
13
13
  * and its message, nothing else.
14
14
  *
@@ -24,7 +24,7 @@ export type GoalOp = "show" | "pin" | "reset";
24
24
  export declare const GOAL_THREAD_RE: RegExp;
25
25
  export declare function isValidGoalThread(thread: string): boolean;
26
26
  /** The ledger fields the control call needs. */
27
- export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id">;
27
+ export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id"> & Partial<Pick<SessionRecord, "agent_id" | "agent_pinned">>;
28
28
  export interface GoalControlCreds {
29
29
  /** The Anthropic key the launch sent as `x-api-key`. */
30
30
  anthropicKey: string;
@@ -7,8 +7,8 @@
7
7
  * POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
8
8
  *
9
9
  * Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
10
- * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
11
- * launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
10
+ * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, `x-mnemom-api-key` when the
11
+ * launch had a Mnemom key, and `x-mnemom-agent-id: <record.agent_id>` when the launch pinned it. The gateway hashes them exactly as a proxied request and only LOOKS
12
12
  * UP the agent. Key material is never printed: errors carry the status, the gateway's error code
13
13
  * and its message, nothing else.
14
14
  *
@@ -58,6 +58,9 @@ export function goalControlHeaders(record, creds) {
58
58
  };
59
59
  if (creds.mnemomKey)
60
60
  headers["x-mnemom-api-key"] = creds.mnemomKey;
61
+ // A pinned launch's control calls carry the same pin, so they reach the agent it ran as.
62
+ if (record.agent_pinned && record.agent_id)
63
+ headers["x-mnemom-agent-id"] = record.agent_id;
61
64
  return headers;
62
65
  }
63
66
  /** `{error:{code,message}}` (the gateway's envelope) → its parts; anything else → nulls. */
@@ -27,6 +27,11 @@ export interface LauncherConfig {
27
27
  gateway?: string;
28
28
  model?: string;
29
29
  agent?: string;
30
+ /**
31
+ * The agent this machine's launches PIN (`x-mnemom-agent-id`): a full `mnm-<uuid>`. `--agent`
32
+ * on the command line wins over it; it wins over `agent`.
33
+ */
34
+ agentId?: string;
30
35
  effort?: string;
31
36
  cli?: string;
32
37
  launch?: LaunchShape;
@@ -43,6 +48,17 @@ export interface LauncherConfig {
43
48
  slackListen?: boolean;
44
49
  /** Slack member ids whose messages the router may deliver into a session. */
45
50
  slackAllow?: string[];
51
+ /**
52
+ * Router: honour `mnemom agent slack alias` handles (off when unset). Off means the
53
+ * router routes exactly as it did before aliases existed; registered aliases are kept
54
+ * but ignored.
55
+ */
56
+ slackAliases?: boolean;
57
+ /**
58
+ * Router: a thread a session was addressed in keeps routing replies to it (off when
59
+ * unset). Off means only threads a session posted in route replies, as before.
60
+ */
61
+ slackStickyThreads?: boolean;
46
62
  /**
47
63
  * Context-handoff default (EXPERIMENTAL, off when unset): `true` registers the `handoff`
48
64
  * MCP tool and sends `CONTEXT_HANDOFF_ENABLED: "true"` in the per-session config header for
@@ -86,6 +102,8 @@ export interface ConfigKeySpec {
86
102
  get(cfg: LauncherConfig): string | number | boolean | undefined;
87
103
  set(cfg: LauncherConfig, raw: string): void;
88
104
  }
105
+ /** launcher.toml `agent_id`: a full `mnm-<uuid>` (any case; stored lowercase). */
106
+ export declare function coerceAgentId(v: string): string;
89
107
  /** The full set of settable keys — drives get/set/unset/list and validation. */
90
108
  export declare const CONFIG_KEYS: ConfigKeySpec[];
91
109
  export declare function configKeySpec(key: string): ConfigKeySpec | undefined;
@@ -20,7 +20,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
20
20
  import { dirname, join } from "node:path";
21
21
  import { parse as parseToml, stringify as stringifyToml, TomlError } from "smol-toml";
22
22
  import { MNEMOM_DIR } from "./config.js";
23
- import { CONTEXT_PROFILES, isPositiveInt, isPositiveNumber, parseCompactAt, parseContextProfile, } from "./agent.js";
23
+ import { CONTEXT_PROFILES, isPositiveInt, isPositiveNumber, parseCompactAt, parseAgentRef, parseContextProfile, } from "./agent.js";
24
24
  /** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
25
25
  export const LAUNCHER_CONFIG_PATH = join(MNEMOM_DIR, "launcher.toml");
26
26
  const LAUNCH_SHAPES = ["terminal", "remote-control", "server"];
@@ -61,6 +61,14 @@ function coerceGoalMode(raw) {
61
61
  return v;
62
62
  throw new Error(`goal_mode must be one of ${GOAL_MODES.join(", ")} (got '${raw}'). A sealed contract needs a statement, so it is always a per-launch --goal.`);
63
63
  }
64
+ /** launcher.toml `agent_id`: a full `mnm-<uuid>` (any case; stored lowercase). */
65
+ export function coerceAgentId(v) {
66
+ const ref = parseAgentRef(v);
67
+ if (ref.kind !== "id") {
68
+ throw new Error(`agent_id must be a full agent id, mnm-<uuid> (got '${v.trim()}').`);
69
+ }
70
+ return ref.id;
71
+ }
64
72
  function coerceNonEmpty(name, raw) {
65
73
  const v = raw.trim();
66
74
  if (!v)
@@ -116,6 +124,14 @@ export const CONFIG_KEYS = [
116
124
  c.agent = coerceNonEmpty("agent", v);
117
125
  },
118
126
  },
127
+ {
128
+ key: "agent_id",
129
+ describe: "Pin launches to this agent id (mnm-<uuid>; x-mnemom-agent-id) — `--agent` wins over it, it wins over `agent`",
130
+ get: (c) => c.agentId,
131
+ set: (c, v) => {
132
+ c.agentId = coerceAgentId(v);
133
+ },
134
+ },
119
135
  {
120
136
  key: "effort",
121
137
  describe: `Reasoning effort (${EFFORTS.join("|")})`,
@@ -196,6 +212,22 @@ export const CONFIG_KEYS = [
196
212
  c.slackAllow = coerceMemberIds(v);
197
213
  },
198
214
  },
215
+ {
216
+ key: "slack_aliases",
217
+ describe: "Slack router: route `mnemom agent slack alias` handles, queueing for a stopped session (off by default)",
218
+ get: (c) => c.slackAliases,
219
+ set: (c, v) => {
220
+ c.slackAliases = coerceBool("slack_aliases", v);
221
+ },
222
+ },
223
+ {
224
+ key: "slack_sticky_threads",
225
+ describe: "Slack router: a thread a session was addressed in keeps routing replies to it (off by default)",
226
+ get: (c) => c.slackStickyThreads,
227
+ set: (c, v) => {
228
+ c.slackStickyThreads = coerceBool("slack_sticky_threads", v);
229
+ },
230
+ },
199
231
  {
200
232
  key: "context_handoff",
201
233
  describe: "EXPERIMENTAL: register the `handoff` MCP tool + tell the gateway CONTEXT_HANDOFF_ENABLED=true (off by default)",
@@ -284,6 +316,8 @@ export function parseLauncherConfig(text) {
284
316
  cfg.model = coerceNonEmpty("model", str(raw.model) ?? "");
285
317
  if (raw.agent !== undefined)
286
318
  cfg.agent = coerceNonEmpty("agent", str(raw.agent) ?? "");
319
+ if (raw.agent_id !== undefined)
320
+ cfg.agentId = coerceAgentId(str(raw.agent_id) ?? "");
287
321
  if (raw.effort !== undefined)
288
322
  cfg.effort = coerceEffort(str(raw.effort) ?? "");
289
323
  if (raw.cli !== undefined)
@@ -306,6 +340,10 @@ export function parseLauncherConfig(text) {
306
340
  }
307
341
  if (raw.slack_channel !== undefined)
308
342
  cfg.slackChannel = coerceNonEmpty("slack_channel", str(raw.slack_channel) ?? "");
343
+ if (raw.slack_aliases !== undefined)
344
+ cfg.slackAliases = coerceBool("slack_aliases", str(raw.slack_aliases) ?? "");
345
+ if (raw.slack_sticky_threads !== undefined)
346
+ cfg.slackStickyThreads = coerceBool("slack_sticky_threads", str(raw.slack_sticky_threads) ?? "");
309
347
  if (raw.context_handoff !== undefined)
310
348
  cfg.contextHandoff = coerceBool("context_handoff", str(raw.context_handoff) ?? "");
311
349
  if (raw.compaction_intercept !== undefined)
@@ -362,6 +400,8 @@ export function serializeLauncherConfig(cfg) {
362
400
  doc.model = cfg.model;
363
401
  if (cfg.agent !== undefined)
364
402
  doc.agent = cfg.agent;
403
+ if (cfg.agentId !== undefined)
404
+ doc.agent_id = cfg.agentId;
365
405
  if (cfg.effort !== undefined)
366
406
  doc.effort = cfg.effort;
367
407
  if (cfg.cli !== undefined)
@@ -382,6 +422,10 @@ export function serializeLauncherConfig(cfg) {
382
422
  doc.slack_listen = cfg.slackListen;
383
423
  if (cfg.slackAllow !== undefined)
384
424
  doc.slack_allow = cfg.slackAllow;
425
+ if (cfg.slackAliases !== undefined)
426
+ doc.slack_aliases = cfg.slackAliases;
427
+ if (cfg.slackStickyThreads !== undefined)
428
+ doc.slack_sticky_threads = cfg.slackStickyThreads;
385
429
  if (cfg.contextHandoff !== undefined)
386
430
  doc.context_handoff = cfg.contextHandoff;
387
431
  if (cfg.compactionIntercept !== undefined)
@@ -476,6 +520,8 @@ export function withoutDisabledExperimental(cfg, enabled) {
476
520
  delete out.slackChannel;
477
521
  delete out.slackListen;
478
522
  delete out.slackAllow;
523
+ delete out.slackAliases;
524
+ delete out.slackStickyThreads;
479
525
  }
480
526
  if (!enabled.includes("context-handoff"))
481
527
  delete out.contextHandoff;
@@ -21,6 +21,13 @@
21
21
  * cached, so there is no client secret to embed (the AS advertises
22
22
  * token_endpoint_auth_method "none" — public clients only).
23
23
  */
24
+ /**
25
+ * The per-request deadline for OAuth calls (ms): the env override when it is a
26
+ * positive integer (capped at 5 min), else the default.
27
+ */
28
+ export declare function oauthHttpTimeoutMs(env?: NodeJS.ProcessEnv): number;
29
+ /** The name carried by an error thrown when an AS round-trip hit its deadline. */
30
+ export declare const OAUTH_TIMEOUT_ERROR = "OAuthTimeoutError";
24
31
  export interface AuthServerMetadata {
25
32
  issuer: string;
26
33
  authorization_endpoint: string;