@mnemom/mnemom 0.17.34 → 0.17.36

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 (177) hide show
  1. package/dist/commands/agent-config.js +3 -0
  2. package/dist/commands/agent-goal.d.ts +8 -3
  3. package/dist/commands/agent-goal.js +22 -8
  4. package/dist/commands/agent-run.d.ts +14 -2
  5. package/dist/commands/agent-run.js +28 -7
  6. package/dist/commands/agent-share-render.js +1 -1
  7. package/dist/commands/agent-slack.js +32 -0
  8. package/dist/commands/durable-inbox.d.ts +51 -0
  9. package/dist/commands/durable-inbox.js +134 -0
  10. package/dist/commands/durable-questions.d.ts +64 -0
  11. package/dist/commands/durable-questions.js +161 -0
  12. package/dist/commands/scan.d.ts +13 -3
  13. package/dist/commands/scan.js +48 -10
  14. package/dist/durable/seat-proxy.d.ts +12 -1
  15. package/dist/durable/seat-proxy.js +55 -9
  16. package/dist/durable/seat-socket.d.ts +20 -2
  17. package/dist/durable/seat-socket.js +58 -0
  18. package/dist/durable/turn-chain.d.ts +8 -0
  19. package/dist/durable/turn-chain.js +25 -4
  20. package/dist/host/client.d.ts +41 -0
  21. package/dist/host/client.js +7 -0
  22. package/dist/host/control.d.ts +7 -0
  23. package/dist/host/control.js +15 -0
  24. package/dist/host/daemon.d.ts +20 -0
  25. package/dist/host/daemon.js +74 -1
  26. package/dist/host/inbox-delivery.d.ts +174 -0
  27. package/dist/host/inbox-delivery.js +453 -0
  28. package/dist/host/run.d.ts +18 -0
  29. package/dist/host/run.js +61 -0
  30. package/dist/host/s1-names.d.ts +2 -0
  31. package/dist/host/s1-names.js +2 -0
  32. package/dist/host/tmux.d.ts +7 -0
  33. package/dist/host/tmux.js +19 -0
  34. package/dist/index.js +62 -3
  35. package/dist/lib/api.d.ts +5 -0
  36. package/dist/lib/api.js +16 -0
  37. package/dist/lib/config.d.ts +7 -0
  38. package/dist/lib/config.js +21 -0
  39. package/dist/lib/durable/api.d.ts +11 -0
  40. package/dist/lib/durable/api.js +28 -3
  41. package/dist/lib/durable/gate.d.ts +1 -1
  42. package/dist/lib/durable/gate.js +8 -1
  43. package/dist/lib/durable/live.d.ts +5 -0
  44. package/dist/lib/durable/live.js +19 -0
  45. package/dist/lib/experimental.js +3 -0
  46. package/dist/lib/goal-api.d.ts +11 -4
  47. package/dist/lib/goal-api.js +4 -3
  48. package/dist/lib/launcher-config.d.ts +5 -0
  49. package/dist/lib/launcher-config.js +17 -2
  50. package/dist/lib/prompt.d.ts +4 -2
  51. package/dist/lib/prompt.js +15 -5
  52. package/dist/lib/runner/job.d.ts +2 -2
  53. package/dist/lib/runner/job.js +16 -3
  54. package/dist/lib/runner/pr-body.d.ts +17 -0
  55. package/dist/lib/runner/pr-body.js +42 -0
  56. package/dist/lib/runner/proxy.d.ts +18 -3
  57. package/dist/lib/runner/proxy.js +24 -5
  58. package/dist/lib/scan/pr-source.js +2 -0
  59. package/dist/lib/scan/record.d.ts +60 -9
  60. package/dist/lib/scan/record.js +157 -39
  61. package/dist/lib/scan/repo-picker.d.ts +32 -0
  62. package/dist/lib/scan/repo-picker.js +80 -0
  63. package/dist/lib/scan/report.d.ts +6 -8
  64. package/dist/lib/scan/report.js +21 -7
  65. package/dist/lib/scan/share.d.ts +5 -1
  66. package/dist/lib/scan/share.js +15 -5
  67. package/dist/lib/slack-durable.d.ts +123 -0
  68. package/dist/lib/slack-durable.js +237 -0
  69. package/dist/lib/slack-router.d.ts +7 -0
  70. package/dist/lib/slack-router.js +50 -4
  71. package/dist/vendor/durable-contract/index.js +992 -34
  72. package/dist/vendor/shared/durable-contract/admission.d.ts +81 -2
  73. package/dist/vendor/shared/durable-contract/admission.js +319 -2
  74. package/dist/vendor/shared/durable-contract/cli-surface.js +21 -0
  75. package/dist/vendor/shared/durable-contract/codes.d.ts +60 -0
  76. package/dist/vendor/shared/durable-contract/codes.js +22 -0
  77. package/dist/vendor/shared/durable-contract/events.d.ts +4 -2
  78. package/dist/vendor/shared/durable-contract/events.js +55 -1
  79. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +14 -0
  80. package/dist/vendor/shared/durable-contract/inbox.d.ts +299 -0
  81. package/dist/vendor/shared/durable-contract/inbox.js +462 -0
  82. package/dist/vendor/shared/durable-contract/index.d.ts +2 -0
  83. package/dist/vendor/shared/durable-contract/index.js +2 -0
  84. package/dist/vendor/shared/durable-contract/question.d.ts +218 -0
  85. package/dist/vendor/shared/durable-contract/question.js +373 -0
  86. package/dist/vendor/shared/durable-contract/routes.d.ts +44 -2
  87. package/dist/vendor/shared/durable-contract/routes.js +43 -1
  88. package/dist/vendor/shared/durable-contract/turn.d.ts +15 -1
  89. package/dist/vendor/shared/durable-contract/turn.js +33 -1
  90. package/dist/vendor/shared/durable-contract/vectors.d.ts +45 -0
  91. package/dist/vendor/shared/durable-contract/vectors.js +49 -0
  92. package/node_modules/@mnemom/aip-verifier/LICENSE +190 -0
  93. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +478 -0
  94. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +478 -0
  95. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +971 -0
  96. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +1 -0
  97. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +330 -0
  98. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +330 -0
  99. package/node_modules/@mnemom/aip-verifier/dist/index.js +933 -0
  100. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +1 -0
  101. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +879 -0
  102. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +1 -0
  103. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +96 -0
  104. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +96 -0
  105. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +875 -0
  106. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +1 -0
  107. package/node_modules/@mnemom/aip-verifier/package.json +86 -0
  108. package/node_modules/@noble/ed25519/LICENSE +21 -0
  109. package/node_modules/@noble/ed25519/README.md +371 -0
  110. package/node_modules/@noble/ed25519/index.d.ts +391 -0
  111. package/node_modules/@noble/ed25519/index.js +933 -0
  112. package/node_modules/@noble/ed25519/index.ts +1134 -0
  113. package/node_modules/@noble/ed25519/package.json +54 -0
  114. package/node_modules/@noble/hashes/LICENSE +21 -0
  115. package/node_modules/@noble/hashes/README.md +478 -0
  116. package/node_modules/@noble/hashes/_blake.d.ts +20 -0
  117. package/node_modules/@noble/hashes/_blake.js +59 -0
  118. package/node_modules/@noble/hashes/_md.d.ts +95 -0
  119. package/node_modules/@noble/hashes/_md.js +209 -0
  120. package/node_modules/@noble/hashes/_u64.d.ts +36 -0
  121. package/node_modules/@noble/hashes/_u64.js +77 -0
  122. package/node_modules/@noble/hashes/argon2.d.ts +148 -0
  123. package/node_modules/@noble/hashes/argon2.js +577 -0
  124. package/node_modules/@noble/hashes/blake1.d.ts +162 -0
  125. package/node_modules/@noble/hashes/blake1.js +630 -0
  126. package/node_modules/@noble/hashes/blake2.d.ts +203 -0
  127. package/node_modules/@noble/hashes/blake2.js +573 -0
  128. package/node_modules/@noble/hashes/blake3.d.ts +68 -0
  129. package/node_modules/@noble/hashes/blake3.js +310 -0
  130. package/node_modules/@noble/hashes/eskdf.d.ts +91 -0
  131. package/node_modules/@noble/hashes/eskdf.js +225 -0
  132. package/node_modules/@noble/hashes/hkdf.d.ts +70 -0
  133. package/node_modules/@noble/hashes/hkdf.js +152 -0
  134. package/node_modules/@noble/hashes/hmac.d.ts +50 -0
  135. package/node_modules/@noble/hashes/hmac.js +93 -0
  136. package/node_modules/@noble/hashes/index.d.ts +1 -0
  137. package/node_modules/@noble/hashes/index.js +34 -0
  138. package/node_modules/@noble/hashes/legacy.d.ts +107 -0
  139. package/node_modules/@noble/hashes/legacy.js +348 -0
  140. package/node_modules/@noble/hashes/package.json +91 -0
  141. package/node_modules/@noble/hashes/pbkdf2.d.ts +66 -0
  142. package/node_modules/@noble/hashes/pbkdf2.js +176 -0
  143. package/node_modules/@noble/hashes/scrypt.d.ts +98 -0
  144. package/node_modules/@noble/hashes/scrypt.js +304 -0
  145. package/node_modules/@noble/hashes/sha2.d.ts +176 -0
  146. package/node_modules/@noble/hashes/sha2.js +446 -0
  147. package/node_modules/@noble/hashes/sha3-addons.d.ts +533 -0
  148. package/node_modules/@noble/hashes/sha3-addons.js +973 -0
  149. package/node_modules/@noble/hashes/sha3.d.ts +208 -0
  150. package/node_modules/@noble/hashes/sha3.js +475 -0
  151. package/node_modules/@noble/hashes/src/_blake.ts +67 -0
  152. package/node_modules/@noble/hashes/src/_md.ts +227 -0
  153. package/node_modules/@noble/hashes/src/_u64.ts +111 -0
  154. package/node_modules/@noble/hashes/src/argon2.ts +693 -0
  155. package/node_modules/@noble/hashes/src/blake1.ts +677 -0
  156. package/node_modules/@noble/hashes/src/blake2.ts +648 -0
  157. package/node_modules/@noble/hashes/src/blake3.ts +344 -0
  158. package/node_modules/@noble/hashes/src/eskdf.ts +265 -0
  159. package/node_modules/@noble/hashes/src/hkdf.ts +161 -0
  160. package/node_modules/@noble/hashes/src/hmac.ts +130 -0
  161. package/node_modules/@noble/hashes/src/index.ts +33 -0
  162. package/node_modules/@noble/hashes/src/legacy.ts +361 -0
  163. package/node_modules/@noble/hashes/src/pbkdf2.ts +219 -0
  164. package/node_modules/@noble/hashes/src/scrypt.ts +375 -0
  165. package/node_modules/@noble/hashes/src/sha2.ts +486 -0
  166. package/node_modules/@noble/hashes/src/sha3-addons.ts +1157 -0
  167. package/node_modules/@noble/hashes/src/sha3.ts +525 -0
  168. package/node_modules/@noble/hashes/src/utils.ts +978 -0
  169. package/node_modules/@noble/hashes/src/webcrypto.ts +283 -0
  170. package/node_modules/@noble/hashes/utils.d.ts +560 -0
  171. package/node_modules/@noble/hashes/utils.js +687 -0
  172. package/node_modules/@noble/hashes/webcrypto.d.ts +121 -0
  173. package/node_modules/@noble/hashes/webcrypto.js +228 -0
  174. package/package.json +5 -2
  175. package/plugins/integrity-manifest.json +6 -2
  176. package/plugins/mnemom/hooks/hooks.json +12 -0
  177. package/plugins/mnemom/hooks/seat-ask.mjs +221 -0
package/dist/index.js CHANGED
@@ -1147,6 +1147,64 @@ function takeParentOption(cmd, key) {
1147
1147
  gate(agentSessionsCmd.command("unlock <name>", { hidden }))
1148
1148
  .description("EXPERIMENTAL (durable): unlock a locked session back to tracked (device step)")
1149
1149
  .action((name) => exitWith("`mnemom agent sessions unlock`", () => unlockCommand(name, liveVerbDeps())));
1150
+ // S1 amendment 60 (signed off by Wassim 2026-11-02): the inbox door and its sender allowlist.
1151
+ gate(agentSessionsCmd.command("send <name> [text]", { hidden }))
1152
+ .description("EXPERIMENTAL (durable): send a message to a locked session's inbox; the host daemon gives it to the session as its next prompt (text, or - / nothing for stdin)")
1153
+ .option("--idempotency-key <key>", "Retry-safe key (default: a new one per send)")
1154
+ .option("--sender-key-env <VAR>", "Send as an allowlisted agent: the environment variable holding that agent's key")
1155
+ .option("--conversation <cid>", "The session's conversation id (with --sender-key-env)")
1156
+ .option("--agent-id <id>", "The session's agent id (with --sender-key-env)")
1157
+ .option("--json", "Output the answer as JSON")
1158
+ .action(async (name, text, o, cmd) => {
1159
+ const json = cmd.optsWithGlobals().json === true;
1160
+ const { sendCommand } = await import("./commands/durable-inbox.js");
1161
+ const { liveInboxDeps } = await import("./lib/durable/live.js");
1162
+ return exitWith("`mnemom agent sessions send`", () => sendCommand(name, text, { ...o, json }, liveInboxDeps()));
1163
+ });
1164
+ const sendersCmd = gate(agentSessionsCmd.command("senders", { hidden })).description("EXPERIMENTAL (durable): the agents allowed to send to a session's agent's inbox (list | add | remove)");
1165
+ for (const verb of ["list", "add", "remove"]) {
1166
+ const c = sendersCmd
1167
+ .command(verb === "list" ? "list <name>" : `${verb} <name> <agent>`)
1168
+ .description(verb === "list"
1169
+ ? "List the agents that may send to this session's agent"
1170
+ : verb === "add"
1171
+ ? "Allow one of your agents (name or id) to send to this session's agent"
1172
+ : "Stop an agent sending to this session's agent");
1173
+ if (verb === "add")
1174
+ c.option("--label <label>", "A note shown in the list");
1175
+ c.option("--json", "Output the answer as JSON");
1176
+ c.action(async (name, ...rest) => {
1177
+ const agent = verb === "list" ? undefined : rest[0];
1178
+ const cmd = rest[rest.length - 1];
1179
+ const all = cmd.optsWithGlobals();
1180
+ const { sendersCommand } = await import("./commands/durable-inbox.js");
1181
+ const { liveInboxDeps } = await import("./lib/durable/live.js");
1182
+ return exitWith(`\`mnemom agent sessions senders ${verb}\``, () => sendersCommand(verb, name, agent, { label: all.label, json: all.json }, liveInboxDeps()));
1183
+ });
1184
+ }
1185
+ // S1 amendment 61 (signed off by Wassim 2026-11-02): a locked session's questions to its owner.
1186
+ gate(agentSessionsCmd.command("questions <name>", { hidden }))
1187
+ .description("EXPERIMENTAL (durable): a locked session's questions to you (open ones; --all for every one)")
1188
+ .option("--all", "Include answered, dismissed and expired questions")
1189
+ .option("--json", "Output the answer as JSON")
1190
+ .action(async (name, _o, cmd) => {
1191
+ // `agent sessions` takes --all and --json too, so commander may hand either to it.
1192
+ const g = cmd.optsWithGlobals();
1193
+ const json = g.json === true;
1194
+ const { questionsCommand } = await import("./commands/durable-questions.js");
1195
+ const { liveQuestionDeps } = await import("./lib/durable/live.js");
1196
+ return exitWith("`mnemom agent sessions questions`", () => questionsCommand(name, { all: g.all, json }, liveQuestionDeps()));
1197
+ });
1198
+ gate(agentSessionsCmd.command("answer <name> <qid> [choices...]", { hidden }))
1199
+ .description("EXPERIMENTAL (durable): answer a session's question (<qid> or latest); one choice per question: an option number, a label, or free text")
1200
+ .option("--dismiss", "Dismiss the question without answering it")
1201
+ .option("--json", "Output the answer as JSON")
1202
+ .action(async (name, qid, choices, o, cmd) => {
1203
+ const json = cmd.optsWithGlobals().json === true;
1204
+ const { answerCommand } = await import("./commands/durable-questions.js");
1205
+ const { liveQuestionDeps } = await import("./lib/durable/live.js");
1206
+ return exitWith("`mnemom agent sessions answer`", () => answerCommand(name, qid, choices ?? [], { dismiss: o.dismiss, json }, liveQuestionDeps()));
1207
+ });
1150
1208
  if (hidden)
1151
1209
  agentSessionsCmd.usage(sessionsUsage);
1152
1210
  }
@@ -2326,10 +2384,10 @@ program
2326
2384
  program
2327
2385
  .command("scan")
2328
2386
  .description("Read-only local scan of GitHub PR metadata; the report is emailed to you every time (counts only, no repo names) and Mnemom keeps nothing at share level none")
2329
- .option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated)")
2387
+ .option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated; omit on a terminal to pick from your repos)")
2330
2388
  .option("--start <YYYY-MM-DD>", "Period start (default: today)")
2331
2389
  .option("--end <YYYY-MM-DD>", "Period end (default: today)")
2332
- .option("--share <level>", "summary | full | none (default: ask)")
2390
+ .option("--share <level>", "summary | full | none (default: full when asked; none when not interactive)")
2333
2391
  .option("--cto-email <email>", "CTO who receives the report (required unless you are the CTO; asked when omitted)")
2334
2392
  .option("--monthly-spend <usd>", "Your own approximate monthly AI spend (self-reported; never sent or saved; needs MNEMOM_ORG_SCAN_MODE=true)")
2335
2393
  .option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com (needs MNEMOM_ORG_SCAN_MODE=true)")
@@ -2337,7 +2395,8 @@ program
2337
2395
  .option("--org <org_id>", "Your org id; the report is emailed to you for this org (default: your only organization, else the scan stops and asks). Also selects the Record totals preview (needs MNEMOM_ORG_SCAN_MODE=true)")
2338
2396
  .action(async (options) => {
2339
2397
  try {
2340
- const repos = parseRepoFlags(options.repo);
2398
+ // Omitted --repo is allowed here: scanCommand picks from your repos on a terminal, else stops with a hint.
2399
+ const repos = options.repo === undefined ? [] : parseRepoFlags(options.repo);
2341
2400
  if (!repos)
2342
2401
  throw new Error("--repo owner/name is required (repeatable)");
2343
2402
  const today = new Date().toISOString().slice(0, 10);
package/dist/lib/api.d.ts CHANGED
@@ -215,11 +215,14 @@ export declare class MnemomApiError extends Error {
215
215
  readonly conflictAgentId?: string;
216
216
  /** Top-level enforce-hook sibling (present only on a spec-rewritten body). */
217
217
  readonly specDeviation?: SpecDeviation;
218
+ /** The server's `Retry-After` in ms (delta-seconds or HTTP-date form), when it sent a valid one. */
219
+ readonly retryAfterMs?: number;
218
220
  constructor(status: number, message: string, opts?: {
219
221
  code?: string;
220
222
  details?: unknown;
221
223
  conflictAgentId?: string;
222
224
  specDeviation?: SpecDeviation;
225
+ retryAfterMs?: number;
223
226
  });
224
227
  }
225
228
  /**
@@ -229,6 +232,8 @@ export declare class MnemomApiError extends Error {
229
232
  * reflects the true intended status behind any synthetic-500 rewrite.
230
233
  */
231
234
  export declare function readApiError(response: Response, fallback: string): Promise<MnemomApiError>;
235
+ /** `Retry-After` header to ms: delta-seconds or an HTTP-date. Anything else is undefined. */
236
+ export declare function parseRetryAfterMs(value: string | null | undefined, nowMs: number): number | undefined;
232
237
  export declare function postApi<T>(endpoint: string, body: unknown, opts?: {
233
238
  idempotencyKey?: string;
234
239
  }): Promise<T>;
package/dist/lib/api.js CHANGED
@@ -85,9 +85,12 @@ export class MnemomApiError extends Error {
85
85
  conflictAgentId;
86
86
  /** Top-level enforce-hook sibling (present only on a spec-rewritten body). */
87
87
  specDeviation;
88
+ /** The server's `Retry-After` in ms (delta-seconds or HTTP-date form), when it sent a valid one. */
89
+ retryAfterMs;
88
90
  constructor(status, message, opts = {}) {
89
91
  super(message);
90
92
  this.name = "MnemomApiError";
93
+ this.retryAfterMs = opts.retryAfterMs;
91
94
  this.status = status;
92
95
  this.code = opts.code;
93
96
  this.details = opts.details;
@@ -110,8 +113,21 @@ export async function readApiError(response, fallback) {
110
113
  details: parsed.details,
111
114
  conflictAgentId: parsed.conflict_agent_id,
112
115
  specDeviation: parsed.spec_deviation,
116
+ retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"), Date.now()),
113
117
  });
114
118
  }
119
+ /** `Retry-After` header to ms: delta-seconds or an HTTP-date. Anything else is undefined. */
120
+ export function parseRetryAfterMs(value, nowMs) {
121
+ if (!value)
122
+ return undefined;
123
+ const v = value.trim();
124
+ if (/^\d{1,9}$/.test(v))
125
+ return Number(v) * 1000;
126
+ if (!/[A-Za-z]/.test(v))
127
+ return undefined; // an HTTP-date names its day and month
128
+ const at = Date.parse(v);
129
+ return Number.isNaN(at) ? undefined : Math.max(0, at - nowMs);
130
+ }
115
131
  export async function postApi(endpoint, body, opts = {}) {
116
132
  const url = validateUrl(`${API_BASE}${endpoint}`);
117
133
  const headers = {
@@ -42,6 +42,13 @@ export declare function setRing(ring: CellRing | undefined): void;
42
42
  export declare function getRing(): CellRing | undefined;
43
43
  /** Override the active API base for this process (clears on undefined/empty). */
44
44
  export declare function setApiUrlOverride(url: string | undefined): void;
45
+ /**
46
+ * True when the API is not production but the gateway is the production one. A lease key minted
47
+ * by a non-production cell's API means nothing to the production gateway, which answers every
48
+ * model call 401 and still receives the key (door2-path run 41783208169), so `agent run` refuses
49
+ * this pairing. A base that does not parse decides nothing. Pure.
50
+ */
51
+ export declare function gatewayRingMismatch(apiUrl: string, gatewayUrl: string): boolean;
45
52
  export declare function getApiUrl(): string;
46
53
  export declare function getGatewayUrl(): string;
47
54
  export declare function getWebsiteUrl(): string;
@@ -113,6 +113,27 @@ function ringUrls() {
113
113
  const ring = getRing();
114
114
  return ring ? RING_URLS[ring] : undefined;
115
115
  }
116
+ function originOf(url) {
117
+ try {
118
+ return new URL(url).origin.toLowerCase();
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * True when the API is not production but the gateway is the production one. A lease key minted
126
+ * by a non-production cell's API means nothing to the production gateway, which answers every
127
+ * model call 401 and still receives the key (door2-path run 41783208169), so `agent run` refuses
128
+ * this pairing. A base that does not parse decides nothing. Pure.
129
+ */
130
+ export function gatewayRingMismatch(apiUrl, gatewayUrl) {
131
+ const api = originOf(apiUrl);
132
+ const gateway = originOf(gatewayUrl);
133
+ if (api === null || gateway === null)
134
+ return false;
135
+ return api !== originOf(API_URLS.production) && gateway === originOf(GATEWAY_URLS.production);
136
+ }
116
137
  export function getApiUrl() {
117
138
  return (apiUrlOverride ?? envBase("MNEMOM_API_URL") ?? ringUrls()?.api ?? API_URLS[getEnvironment()]);
118
139
  }
@@ -108,6 +108,17 @@ export declare class DurableApi {
108
108
  createSession(cid: string, agentId: string, body: Record<string, unknown>): Promise<ApiAnswer>;
109
109
  /** An owner session verb (S1 ROUTES.own_*). */
110
110
  ownerVerb(route: RouteName, cid: string, agentId: string, body?: unknown): Promise<ApiAnswer>;
111
+ /**
112
+ * An S1 amendment 60 inbox route on a session (own_inbox_send, own_inbox_list, own_inbox_senders,
113
+ * own_inbox_sender_add, own_inbox_sender_remove), with the route's own method. `credential`
114
+ * sends an allowlisted sender agent's key instead of the caller's (own_inbox_send only).
115
+ */
116
+ inbox(route: "own_inbox_send" | "own_inbox_list" | "own_inbox_senders" | "own_inbox_sender_add" | "own_inbox_sender_remove", cid: string, agentId: string, body?: unknown, credential?: DurableCredential): Promise<ApiAnswer>;
117
+ /**
118
+ * S1 amendment 61: GET ROUTES.own_questions / POST ROUTES.own_answer for one session (owner
119
+ * credential only; an allowlisted sender may not answer).
120
+ */
121
+ questions(route: "own_questions" | "own_answer", cid: string, agentId: string, body?: unknown): Promise<ApiAnswer>;
111
122
  /** POST ROUTES.own_lock with the closed S1 lock body (spec, executor_id, own_agent). */
112
123
  lock(cid: string, agentId: string, body: unknown): Promise<ApiAnswer>;
113
124
  /**
@@ -17,7 +17,7 @@
17
17
  * GET /v1/agent/sessions/{cid}/lock?agent_id=&dry_run=true -> the doctor's lock preflight
18
18
  */
19
19
  import { randomUUID } from "node:crypto";
20
- import { routePath } from "../../vendor/shared/durable-contract/routes.js";
20
+ import { ROUTES, routePath } from "../../vendor/shared/durable-contract/routes.js";
21
21
  /** CLI registry timeout, connect plus total (C01-R18: at most 10 s). */
22
22
  export const REGISTRY_TIMEOUT_MS = 10_000;
23
23
  /** The registry could not be read: transport error, timeout or a non-durable 5xx. */
@@ -75,7 +75,7 @@ export class DurableApi {
75
75
  this.deps = deps;
76
76
  }
77
77
  async request(method, path, opts = {}) {
78
- const cred = await this.deps.credential();
78
+ const cred = opts.credential ?? (await this.deps.credential());
79
79
  const headers = { Accept: "application/json" };
80
80
  if (cred.type === "jwt")
81
81
  headers.Authorization = `Bearer ${cred.token.trim()}`;
@@ -90,7 +90,8 @@ export class DurableApi {
90
90
  headers["Content-Type"] = "application/json";
91
91
  body = JSON.stringify(opts.body);
92
92
  }
93
- if (method === "POST")
93
+ // Every write carries one: POST, and DELETE (own_inbox_sender_remove) since amendment 60.
94
+ if (method !== "GET")
94
95
  headers["Idempotency-Key"] = randomUUID();
95
96
  const url = new URL(path, this.deps.apiBase().replace(/\/+$/, "") + "/").href;
96
97
  const ctrl = new AbortController();
@@ -159,6 +160,30 @@ export class DurableApi {
159
160
  body,
160
161
  });
161
162
  }
163
+ /**
164
+ * An S1 amendment 60 inbox route on a session (own_inbox_send, own_inbox_list, own_inbox_senders,
165
+ * own_inbox_sender_add, own_inbox_sender_remove), with the route's own method. `credential`
166
+ * sends an allowlisted sender agent's key instead of the caller's (own_inbox_send only).
167
+ */
168
+ async inbox(route, cid, agentId, body, credential) {
169
+ if (credential !== undefined && route !== "own_inbox_send")
170
+ throw new Error(`a sender credential goes only with own_inbox_send (got ${route})`);
171
+ const method = ROUTES[route].method;
172
+ return this.request(method, `${routePath(route, cid)}${this.q({ agent_id: agentId })}`, {
173
+ ...(body === undefined ? {} : { body }),
174
+ ...(credential ? { credential } : {}),
175
+ });
176
+ }
177
+ /**
178
+ * S1 amendment 61: GET ROUTES.own_questions / POST ROUTES.own_answer for one session (owner
179
+ * credential only; an allowlisted sender may not answer).
180
+ */
181
+ async questions(route, cid, agentId, body) {
182
+ const method = ROUTES[route].method;
183
+ return this.request(method, `${routePath(route, cid)}${this.q({ agent_id: agentId })}`, {
184
+ ...(body === undefined ? {} : { body }),
185
+ });
186
+ }
162
187
  /** POST ROUTES.own_lock with the closed S1 lock body (spec, executor_id, own_agent). */
163
188
  async lock(cid, agentId, body) {
164
189
  return this.ownerVerb("own_lock", cid, agentId, body);
@@ -16,7 +16,7 @@ export declare const DURABLE_ID: "durable";
16
16
  /** `mnemom agent <word>` words that exist only with durable on. */
17
17
  export declare const DURABLE_VERB_WORDS: readonly ["lock", "pause", "resume", "done", "kill", "amend", "host", "containment"];
18
18
  /** `mnemom agent sessions <sub>` sub-verbs that exist only with durable on. */
19
- export declare const DURABLE_SESSIONS_SUBVERBS: readonly ["unlock", "adopt"];
19
+ export declare const DURABLE_SESSIONS_SUBVERBS: readonly ["unlock", "adopt", "send", "senders", "questions", "answer"];
20
20
  export declare function durableOn(enabled: readonly ExperimentalId[]): boolean;
21
21
  /**
22
22
  * The options that take a value, by flag spelling, as commander parses them: a `required` one always
@@ -25,7 +25,14 @@ export const DURABLE_VERB_WORDS = [
25
25
  "containment",
26
26
  ];
27
27
  /** `mnemom agent sessions <sub>` sub-verbs that exist only with durable on. */
28
- export const DURABLE_SESSIONS_SUBVERBS = ["unlock", "adopt"];
28
+ export const DURABLE_SESSIONS_SUBVERBS = [
29
+ "unlock",
30
+ "adopt",
31
+ "send",
32
+ "senders",
33
+ "questions",
34
+ "answer",
35
+ ];
29
36
  export function durableOn(enabled) {
30
37
  return enabled.includes(DURABLE_ID);
31
38
  }
@@ -7,6 +7,8 @@ import { type LauncherConfig } from "../launcher-config.js";
7
7
  import { type ProcDeps } from "../../durable/proc.js";
8
8
  import type { LauncherKey } from "../../vendor/shared/durable-contract/fixlines.js";
9
9
  import type { DurableVerbDeps } from "../../commands/durable-verbs.js";
10
+ import type { InboxVerbDeps } from "../../commands/durable-inbox.js";
11
+ import type { QuestionVerbDeps } from "../../commands/durable-questions.js";
10
12
  import type { SessionsReadDeps } from "../../commands/durable-sessions.js";
11
13
  import { type DoctorLine } from "../../commands/durable-doctor.js";
12
14
  import { type OriginalLaunch, type LockDeps, type LockOutcome } from "../../commands/durable-lock.js";
@@ -24,6 +26,9 @@ export declare function liveLauncherKey(load?: () => {
24
26
  /** PIDs of local claude processes whose argv names `ccSessionId` (pure over a `ps` listing). */
25
27
  export declare function claudePidsFor(psOutput: string, ccSessionId: string, selfPid: number): number[];
26
28
  export declare function liveVerbDeps(): DurableVerbDeps;
29
+ /** The inbox verbs' deps: the verb deps, the environment (sender key) and stdin. */
30
+ export declare function liveInboxDeps(): InboxVerbDeps;
31
+ export declare function liveQuestionDeps(): QuestionVerbDeps;
27
32
  export declare function liveSessionsReadDeps(): SessionsReadDeps;
28
33
  export declare function liveLockDeps(): LockDeps;
29
34
  /** The live original's exact argv, its parent launcher's passthrough and its shape (C14-R13). */
@@ -120,6 +120,25 @@ export function liveVerbDeps() {
120
120
  stopTrackedClaude,
121
121
  };
122
122
  }
123
+ /** The inbox verbs' deps: the verb deps, the environment (sender key) and stdin. */
124
+ export function liveInboxDeps() {
125
+ const base = liveVerbDeps();
126
+ return {
127
+ ...base,
128
+ api: base.api,
129
+ env: process.env,
130
+ readStdin: async () => {
131
+ const chunks = [];
132
+ for await (const c of process.stdin)
133
+ chunks.push(c);
134
+ return Buffer.concat(chunks).toString("utf8");
135
+ },
136
+ };
137
+ }
138
+ export function liveQuestionDeps() {
139
+ const base = liveVerbDeps();
140
+ return { ...base, api: base.api };
141
+ }
123
142
  export function liveSessionsReadDeps() {
124
143
  return {
125
144
  api: liveDurableApi(),
@@ -54,6 +54,7 @@ export const EXPERIMENTAL_FEATURES = [
54
54
  "slack_channel",
55
55
  "slack_listen",
56
56
  "slack_allow",
57
+ "slack_owner",
57
58
  "slack_aliases",
58
59
  "slack_sticky_threads",
59
60
  ],
@@ -217,6 +218,7 @@ export function experimentalIdForConfigKey(key, value) {
217
218
  switch (key) {
218
219
  case "slack_channel":
219
220
  case "slack_allow":
221
+ case "slack_owner":
220
222
  return "slack";
221
223
  case "slack_listen":
222
224
  case "slack_aliases":
@@ -299,6 +301,7 @@ export function configKeyFeature(key) {
299
301
  if (key === "slack_channel" ||
300
302
  key === "slack_listen" ||
301
303
  key === "slack_allow" ||
304
+ key === "slack_owner" ||
302
305
  key === "slack_aliases" ||
303
306
  key === "slack_sticky_threads")
304
307
  return "slack";
@@ -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>`, `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
10
+ * `resolveAnthropicKey`, or a subscription launch's Mnemom key), `x-mnemom-agent: <record.agent_slug>`,
11
+ * `x-mnemom-api-key` when the 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
  *
@@ -25,12 +25,19 @@ 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
27
  export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id"> & Partial<Pick<SessionRecord, "agent_id" | "agent_pinned">>;
28
- export interface GoalControlCreds {
28
+ /**
29
+ * An api-key launch: its Anthropic key (sent as `x-api-key`) and the Mnemom key it sent beside it,
30
+ * if any. A subscription launch had no Anthropic key: its Mnemom key is the credential.
31
+ */
32
+ export type GoalControlCreds = {
29
33
  /** The Anthropic key the launch sent as `x-api-key`. */
30
34
  anthropicKey: string;
31
35
  /** The Mnemom key, only when the launch sent `x-mnemom-api-key`. */
32
36
  mnemomKey?: string | null;
33
- }
37
+ } | {
38
+ anthropicKey?: undefined;
39
+ mnemomKey: string;
40
+ };
34
41
  export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
35
42
  export interface GoalControlDeps {
36
43
  fetch: FetchLike;
@@ -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>`, `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
10
+ * `resolveAnthropicKey`, or a subscription launch's Mnemom key), `x-mnemom-agent: <record.agent_slug>`,
11
+ * `x-mnemom-api-key` when the 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
  *
@@ -52,7 +52,8 @@ export function goalControlUrl(record, op, thread = "main") {
52
52
  /** The launch's identity headers (never logged). */
53
53
  export function goalControlHeaders(record, creds) {
54
54
  const headers = {
55
- "x-api-key": creds.anthropicKey,
55
+ // A subscription session's Mnemom key: the gateway resolves the claimed agent from it.
56
+ "x-api-key": creds.anthropicKey !== undefined ? creds.anthropicKey : creds.mnemomKey,
56
57
  "x-mnemom-agent": record.agent_slug,
57
58
  accept: "application/json",
58
59
  };
@@ -49,6 +49,11 @@ export interface LauncherConfig {
49
49
  slackListen?: boolean;
50
50
  /** Slack member ids whose messages the router may deliver into a session. */
51
51
  slackAllow?: string[];
52
+ /**
53
+ * The owner's own Slack member ids. With durable on, a message from one of them to a locked
54
+ * durable session goes to its durable inbox as from the owner (S1 amendment 60).
55
+ */
56
+ slackOwner?: string[];
52
57
  /**
53
58
  * Router: honour `mnemom agent slack alias` handles (off when unset). Off means the
54
59
  * router routes exactly as it did before aliases existed; registered aliases are kept
@@ -100,14 +100,14 @@ function coerceCompactAt(raw) {
100
100
  parseCompactAt(v, "compact_at");
101
101
  return v;
102
102
  }
103
- function coerceMemberIds(raw) {
103
+ function coerceMemberIds(raw, key = "slack_allow") {
104
104
  const ids = raw
105
105
  .split(/[\s,]+/)
106
106
  .map((x) => x.trim())
107
107
  .filter(Boolean);
108
108
  const bad = ids.filter((x) => !/^[UW][A-Z0-9]{6,}$/.test(x));
109
109
  if (!ids.length || bad.length) {
110
- throw new Error(`slack_allow must be Slack member ids like U012ABCDEF (Profile → ⋯ → Copy member ID); got '${raw}'.`);
110
+ throw new Error(`${key} must be Slack member ids like U012ABCDEF (Profile → ⋯ → Copy member ID); got '${raw}'.`);
111
111
  }
112
112
  return ids;
113
113
  }
@@ -238,6 +238,14 @@ export const CONFIG_KEYS = [
238
238
  c.slackAllow = coerceMemberIds(v);
239
239
  },
240
240
  },
241
+ {
242
+ key: "slack_owner",
243
+ describe: "Your own Slack member ids (comma-separated): with durable on, your messages reach a locked durable session's inbox as from you",
244
+ get: (c) => c.slackOwner?.join(","),
245
+ set: (c, v) => {
246
+ c.slackOwner = coerceMemberIds(v, "slack_owner");
247
+ },
248
+ },
241
249
  {
242
250
  key: "slack_aliases",
243
251
  describe: "Slack router: route `mnemom agent slack alias` handles, queueing for a stopped session (off by default)",
@@ -366,6 +374,10 @@ export function parseLauncherConfig(text) {
366
374
  const v = raw.slack_allow;
367
375
  cfg.slackAllow = coerceMemberIds(Array.isArray(v) ? v.map(String).join(",") : String(v));
368
376
  }
377
+ if (raw.slack_owner !== undefined) {
378
+ const v = raw.slack_owner;
379
+ cfg.slackOwner = coerceMemberIds(Array.isArray(v) ? v.map(String).join(",") : String(v), "slack_owner");
380
+ }
369
381
  if (raw.slack_channel !== undefined)
370
382
  cfg.slackChannel = coerceNonEmpty("slack_channel", str(raw.slack_channel) ?? "");
371
383
  if (raw.slack_aliases !== undefined)
@@ -452,6 +464,8 @@ export function serializeLauncherConfig(cfg) {
452
464
  doc.slack_listen = cfg.slackListen;
453
465
  if (cfg.slackAllow !== undefined)
454
466
  doc.slack_allow = cfg.slackAllow;
467
+ if (cfg.slackOwner !== undefined)
468
+ doc.slack_owner = cfg.slackOwner;
455
469
  if (cfg.slackAliases !== undefined)
456
470
  doc.slack_aliases = cfg.slackAliases;
457
471
  if (cfg.slackStickyThreads !== undefined)
@@ -550,6 +564,7 @@ export function withoutDisabledExperimental(cfg, enabled) {
550
564
  delete out.slackChannel;
551
565
  delete out.slackListen;
552
566
  delete out.slackAllow;
567
+ delete out.slackOwner;
553
568
  delete out.slackAliases;
554
569
  delete out.slackStickyThreads;
555
570
  }
@@ -19,9 +19,11 @@ export declare function askInput(question: string, mask?: boolean): Promise<stri
19
19
  export declare function askMultiSelect(question: string, options: string[]): Promise<string[]>;
20
20
  /**
21
21
  * Single-select prompt. Displays numbered options, user enters a number.
22
- * Returns selected label or null if invalid.
22
+ * Returns selected label or null if invalid. With `defaultIndex` (0-based), pressing Enter on an
23
+ * interactive terminal picks that option; piped (non-TTY) input never uses it, so an empty or missing
24
+ * line still returns null and the caller keeps its fail-closed fallback.
23
25
  */
24
- export declare function askSelect(question: string, options: string[]): Promise<string | null>;
26
+ export declare function askSelect(question: string, options: string[], defaultIndex?: number): Promise<string | null>;
25
27
  /**
26
28
  * Check if running in non-interactive mode (no TTY)
27
29
  */
@@ -80,8 +80,12 @@ export async function askInput(question, mask = false) {
80
80
  process.stdout.write(`${question} `);
81
81
  const lines = await readPipedStdinLines();
82
82
  const next = lines.shift();
83
- process.stdout.write("\n");
84
- return (next ?? "").trim();
83
+ const answer = (next ?? "").trim();
84
+ // A terminal shows what the user typed; a pipe does not, which left a bare "Question: " line in
85
+ // logs and scan output. Echo an unmasked answer so the line reads "Question: value". A masked
86
+ // answer (API key / password) is never echoed.
87
+ process.stdout.write(mask ? "\n" : `${answer}\n`);
88
+ return answer;
85
89
  }
86
90
  const rl = readline.createInterface({
87
91
  input: process.stdin,
@@ -169,9 +173,11 @@ function resolveSelection(answer, options) {
169
173
  }
170
174
  /**
171
175
  * Single-select prompt. Displays numbered options, user enters a number.
172
- * Returns selected label or null if invalid.
176
+ * Returns selected label or null if invalid. With `defaultIndex` (0-based), pressing Enter on an
177
+ * interactive terminal picks that option; piped (non-TTY) input never uses it, so an empty or missing
178
+ * line still returns null and the caller keeps its fail-closed fallback.
173
179
  */
174
- export async function askSelect(question, options) {
180
+ export async function askSelect(question, options, defaultIndex) {
175
181
  console.log(`\n${question}`);
176
182
  for (let i = 0; i < options.length; i++) {
177
183
  console.log(` ${i + 1}) ${options[i]}`);
@@ -194,8 +200,12 @@ export async function askSelect(question, options) {
194
200
  output: process.stdout,
195
201
  });
196
202
  return new Promise((resolve) => {
197
- rl.question("Select: ", (answer) => {
203
+ const hasDefault = defaultIndex !== undefined && defaultIndex >= 0 && defaultIndex < options.length;
204
+ const prompt = hasDefault ? `Select [${defaultIndex + 1}]: ` : "Select: ";
205
+ rl.question(prompt, (answer) => {
198
206
  rl.close();
207
+ if (hasDefault && answer.trim() === "")
208
+ return resolve(options[defaultIndex]);
199
209
  resolve(resolveSelection(answer, options));
200
210
  });
201
211
  });
@@ -14,7 +14,7 @@
14
14
  import { type HttpResult, type RunnerApi } from "./executor-client.js";
15
15
  import { type OutcomeFields, type RefusedReason } from "./outcome.js";
16
16
  import { type PhaseInvocation } from "./phases.js";
17
- import type { ModelProxy, ProxyState } from "./proxy.js";
17
+ import { type ModelProxy, type ProxyState } from "./proxy.js";
18
18
  import { type RunnerFailMode } from "./settings.js";
19
19
  /** A claimed job's working copy. */
20
20
  export interface Workspace {
@@ -42,7 +42,7 @@ export interface JobDeps {
42
42
  /** Run one phase's process; resolves its exit code. Rejects only when it could not start. */
43
43
  runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
44
44
  /** Push the job branch and open its PR; resolves the PR URL. */
45
- openPr(repo: string, ws: Workspace, title: string): Promise<string>;
45
+ openPr(repo: string, ws: Workspace, title: string, body: string): Promise<string>;
46
46
  /** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
47
47
  commitsAhead(ws: Workspace): Promise<number>;
48
48
  enableAutoMerge(prUrl: string): Promise<void>;
@@ -19,6 +19,8 @@ import { sealedContractHeader } from "./contract-header.js";
19
19
  import { errorCode, errorDetails } from "./executor-client.js";
20
20
  import { doneOutcome, failedOutcome, phaseReason, refusedOutcome, } from "./outcome.js";
21
21
  import { planPhase } from "./phases.js";
22
+ import { canonNodeKey, prBody } from "./pr-body.js";
23
+ import { requestSummary } from "./proxy.js";
22
24
  import { resolveMergeStrategy } from "./settings.js";
23
25
  const TOLERATED = RUNNER_OPEN_MODE_TOLERATED;
24
26
  const OPEN_MODE_RETRIES = 3;
@@ -154,7 +156,14 @@ function parseJob(body) {
154
156
  const agentId = str(b.agent_id);
155
157
  if (agentId === null || !AGENT_ID_RE.test(agentId))
156
158
  return null;
157
- return { repo, agent_id: agentId, contract_sha256: sha, version, contract: b.contract };
159
+ return {
160
+ repo,
161
+ agent_id: agentId,
162
+ contract_sha256: sha,
163
+ version,
164
+ contract: b.contract,
165
+ canon_node_key: canonNodeKey(b.canon_node_key),
166
+ };
158
167
  }
159
168
  /**
160
169
  * Run one candidate session to an outcome. Never throws for a Mnemom error: every one is an
@@ -354,7 +363,7 @@ export async function runJob(sessionId, opts, deps) {
354
363
  await reportFail(deps, sessionId, claim.epoch, "runner_failed", "pr_no_changes");
355
364
  return { kind: "outcome", outcome: failedOutcome(facts, "pr_no_changes") };
356
365
  }
357
- facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`);
366
+ facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`, prBody(job));
358
367
  }
359
368
  catch (e) {
360
369
  deps.log(`opening the PR failed: ${e.message}`);
@@ -377,7 +386,11 @@ export async function runJob(sessionId, opts, deps) {
377
386
  if (heartbeat)
378
387
  clearInterval(heartbeat);
379
388
  opts.stop.removeEventListener("abort", onStop);
380
- await proxy?.close();
389
+ if (proxy) {
390
+ await proxy.close();
391
+ // One line per job that reached its phases: what every model request carried (W3.11).
392
+ deps.log(`model requests: ${JSON.stringify(requestSummary(proxy.forwarded))}`);
393
+ }
381
394
  // A workspace left behind never changes the outcome; it is logged for the operator.
382
395
  if (ws)
383
396
  await deps.cleanup(ws).catch((e) => deps.log(`cleanup failed: ${e.message}`));
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The body of the PR `mnemom agent run` opens: the job's goal line from the sealed contract and the
3
+ * Canon node its seal points to, both from the job read the runner already holds (no extra call).
4
+ * Never the contract body or any key: the goal is the contract's `statement`, on one line and
5
+ * capped; the node is cited only when the job read names a well-formed one, and a job without one
6
+ * still opens its PR, with no citation.
7
+ */
8
+ /** The goal line's cap, in characters: a PR body line, not the contract. */
9
+ export declare const PR_GOAL_MAX = 280;
10
+ /** The node key when well formed, else null. */
11
+ export declare function canonNodeKey(v: unknown): string | null;
12
+ /** The sealed contract's statement on one line, capped; null when the bytes carry none. */
13
+ export declare function goalLine(contract: string): string | null;
14
+ export declare function prBody(job: {
15
+ contract: string;
16
+ canon_node_key: string | null;
17
+ }): string;