@animalabs/agent-framework 0.32.0 → 0.33.0

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 (288) hide show
  1. package/README.md +204 -14
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +185 -78
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +279 -102
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  17. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/scripts.js +24 -0
  19. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  20. package/dist/src/config-checks.d.ts +7 -0
  21. package/dist/src/config-checks.d.ts.map +1 -1
  22. package/dist/src/config-checks.js +14 -0
  23. package/dist/src/config-checks.js.map +1 -1
  24. package/dist/src/context-manager-errors.d.ts +25 -0
  25. package/dist/src/context-manager-errors.d.ts.map +1 -0
  26. package/dist/src/context-manager-errors.js +31 -0
  27. package/dist/src/context-manager-errors.js.map +1 -0
  28. package/dist/src/context-options.d.ts +67 -0
  29. package/dist/src/context-options.d.ts.map +1 -0
  30. package/dist/src/context-options.js +85 -0
  31. package/dist/src/context-options.js.map +1 -0
  32. package/dist/src/deferred-import.d.ts +93 -0
  33. package/dist/src/deferred-import.d.ts.map +1 -0
  34. package/dist/src/deferred-import.js +177 -0
  35. package/dist/src/deferred-import.js.map +1 -0
  36. package/dist/src/dendrite/audience.d.ts +223 -0
  37. package/dist/src/dendrite/audience.d.ts.map +1 -0
  38. package/dist/src/dendrite/audience.js +202 -0
  39. package/dist/src/dendrite/audience.js.map +1 -0
  40. package/dist/src/dendrite/index.d.ts +5 -3
  41. package/dist/src/dendrite/index.d.ts.map +1 -1
  42. package/dist/src/dendrite/index.js +2 -1
  43. package/dist/src/dendrite/index.js.map +1 -1
  44. package/dist/src/dendrite/presets.d.ts +10 -5
  45. package/dist/src/dendrite/presets.d.ts.map +1 -1
  46. package/dist/src/dendrite/presets.js +17 -5
  47. package/dist/src/dendrite/presets.js.map +1 -1
  48. package/dist/src/dendrite/registry.d.ts +62 -7
  49. package/dist/src/dendrite/registry.d.ts.map +1 -1
  50. package/dist/src/dendrite/registry.js +179 -10
  51. package/dist/src/dendrite/registry.js.map +1 -1
  52. package/dist/src/dendrite/types.d.ts +91 -8
  53. package/dist/src/dendrite/types.d.ts.map +1 -1
  54. package/dist/src/foreign-rounds.d.ts +69 -0
  55. package/dist/src/foreign-rounds.d.ts.map +1 -0
  56. package/dist/src/foreign-rounds.js +152 -0
  57. package/dist/src/foreign-rounds.js.map +1 -0
  58. package/dist/src/framework.d.ts +1976 -818
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7566 -3422
  61. package/dist/src/framework.js.map +1 -1
  62. package/dist/src/gate/event-gate.d.ts +85 -15
  63. package/dist/src/gate/event-gate.d.ts.map +1 -1
  64. package/dist/src/gate/event-gate.js +421 -47
  65. package/dist/src/gate/event-gate.js.map +1 -1
  66. package/dist/src/gate/index.d.ts +1 -1
  67. package/dist/src/gate/index.d.ts.map +1 -1
  68. package/dist/src/gate/index.js.map +1 -1
  69. package/dist/src/gate/rule-scope.d.ts +30 -0
  70. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  71. package/dist/src/gate/rule-scope.js +41 -0
  72. package/dist/src/gate/rule-scope.js.map +1 -0
  73. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  74. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  75. package/dist/src/gate/suppress-script-worker.js +103 -0
  76. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  77. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  78. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  79. package/dist/src/gate/suppress-scripts.js +403 -0
  80. package/dist/src/gate/suppress-scripts.js.map +1 -0
  81. package/dist/src/gate/suppression-review.d.ts +143 -0
  82. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  83. package/dist/src/gate/suppression-review.js +463 -0
  84. package/dist/src/gate/suppression-review.js.map +1 -0
  85. package/dist/src/gate/suppression-stats.d.ts +125 -0
  86. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  87. package/dist/src/gate/suppression-stats.js +214 -0
  88. package/dist/src/gate/suppression-stats.js.map +1 -0
  89. package/dist/src/gate/types.d.ts +135 -7
  90. package/dist/src/gate/types.d.ts.map +1 -1
  91. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  92. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  93. package/dist/src/gate/wake-onboarding.js +277 -0
  94. package/dist/src/gate/wake-onboarding.js.map +1 -0
  95. package/dist/src/held-writes.d.ts +31 -23
  96. package/dist/src/held-writes.d.ts.map +1 -1
  97. package/dist/src/held-writes.js +44 -59
  98. package/dist/src/held-writes.js.map +1 -1
  99. package/dist/src/index.d.ts +5 -2
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +3 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  104. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  105. package/dist/src/jsonrpc-envelope.js +66 -0
  106. package/dist/src/jsonrpc-envelope.js.map +1 -0
  107. package/dist/src/mcpl/channel-registry.d.ts +131 -15
  108. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  109. package/dist/src/mcpl/channel-registry.js +290 -46
  110. package/dist/src/mcpl/channel-registry.js.map +1 -1
  111. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  112. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  113. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  114. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  115. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  116. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  117. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  118. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  119. package/dist/src/mcpl/inbound-source.js +2 -1
  120. package/dist/src/mcpl/inbound-source.js.map +1 -1
  121. package/dist/src/mcpl/inference-router.d.ts +6 -6
  122. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  123. package/dist/src/mcpl/inference-router.js.map +1 -1
  124. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  125. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-coalescer.js +105 -15
  127. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  128. package/dist/src/mcpl/push-handler.d.ts +9 -7
  129. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  130. package/dist/src/mcpl/push-handler.js.map +1 -1
  131. package/dist/src/mcpl/server-connection.d.ts +13 -10
  132. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  133. package/dist/src/mcpl/server-connection.js +134 -56
  134. package/dist/src/mcpl/server-connection.js.map +1 -1
  135. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  136. package/dist/src/mcpl/tool-classes.js +3 -0
  137. package/dist/src/mcpl/tool-classes.js.map +1 -1
  138. package/dist/src/mcpl/types.d.ts +8 -2
  139. package/dist/src/mcpl/types.d.ts.map +1 -1
  140. package/dist/src/mcpl/types.js.map +1 -1
  141. package/dist/src/module-registry.d.ts +24 -1
  142. package/dist/src/module-registry.d.ts.map +1 -1
  143. package/dist/src/module-registry.js +68 -18
  144. package/dist/src/module-registry.js.map +1 -1
  145. package/dist/src/modules/discord/index.d.ts +45 -8
  146. package/dist/src/modules/discord/index.d.ts.map +1 -1
  147. package/dist/src/modules/discord/index.js +237 -86
  148. package/dist/src/modules/discord/index.js.map +1 -1
  149. package/dist/src/modules/history/index.d.ts +59 -0
  150. package/dist/src/modules/history/index.d.ts.map +1 -1
  151. package/dist/src/modules/history/index.js +432 -67
  152. package/dist/src/modules/history/index.js.map +1 -1
  153. package/dist/src/modules/history/semantic.d.ts +6 -5
  154. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  155. package/dist/src/modules/history/semantic.js +15 -22
  156. package/dist/src/modules/history/semantic.js.map +1 -1
  157. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  158. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  159. package/dist/src/modules/workspace/index.d.ts +28 -7
  160. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/index.js +76 -22
  162. package/dist/src/modules/workspace/index.js.map +1 -1
  163. package/dist/src/notice-tags.d.ts +10 -1
  164. package/dist/src/notice-tags.d.ts.map +1 -1
  165. package/dist/src/notice-tags.js +10 -1
  166. package/dist/src/notice-tags.js.map +1 -1
  167. package/dist/src/operator-log.d.ts +36 -3
  168. package/dist/src/operator-log.d.ts.map +1 -1
  169. package/dist/src/operator-log.js +2 -2
  170. package/dist/src/operator-log.js.map +1 -1
  171. package/dist/src/presentation.d.ts +746 -0
  172. package/dist/src/presentation.d.ts.map +1 -0
  173. package/dist/src/presentation.js +1435 -0
  174. package/dist/src/presentation.js.map +1 -0
  175. package/dist/src/prose-drafts.d.ts +15 -11
  176. package/dist/src/prose-drafts.d.ts.map +1 -1
  177. package/dist/src/prose-drafts.js +16 -12
  178. package/dist/src/prose-drafts.js.map +1 -1
  179. package/dist/src/read-marks.d.ts +192 -0
  180. package/dist/src/read-marks.d.ts.map +1 -0
  181. package/dist/src/read-marks.js +242 -0
  182. package/dist/src/read-marks.js.map +1 -0
  183. package/dist/src/recovery/offline-branch.d.ts +91 -6
  184. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  185. package/dist/src/recovery/offline-branch.js +273 -29
  186. package/dist/src/recovery/offline-branch.js.map +1 -1
  187. package/dist/src/recovery/recover-cli.js +22 -3
  188. package/dist/src/recovery/recover-cli.js.map +1 -1
  189. package/dist/src/send-tools.d.ts +5 -9
  190. package/dist/src/send-tools.d.ts.map +1 -1
  191. package/dist/src/send-tools.js +40 -7
  192. package/dist/src/send-tools.js.map +1 -1
  193. package/dist/src/stored-injections.d.ts +2 -2
  194. package/dist/src/stored-injections.js +2 -2
  195. package/dist/src/stored-removal.d.ts +17 -4
  196. package/dist/src/stored-removal.d.ts.map +1 -1
  197. package/dist/src/stored-removal.js +29 -5
  198. package/dist/src/stored-removal.js.map +1 -1
  199. package/dist/src/tool-result-guard.d.ts +12 -1
  200. package/dist/src/tool-result-guard.d.ts.map +1 -1
  201. package/dist/src/tool-result-guard.js +42 -8
  202. package/dist/src/tool-result-guard.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +1159 -79
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +3709 -363
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/tune-out/tools.d.ts +114 -17
  208. package/dist/src/tune-out/tools.d.ts.map +1 -1
  209. package/dist/src/tune-out/tools.js +87 -27
  210. package/dist/src/tune-out/tools.js.map +1 -1
  211. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  212. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  213. package/dist/src/tune-out/window-anchor.js +202 -0
  214. package/dist/src/tune-out/window-anchor.js.map +1 -0
  215. package/dist/src/types/agent.d.ts +82 -20
  216. package/dist/src/types/agent.d.ts.map +1 -1
  217. package/dist/src/types/events.d.ts +2 -0
  218. package/dist/src/types/events.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +32 -12
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +180 -69
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +510 -36
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +32 -3
  227. package/src/agent.ts +323 -120
  228. package/src/api/mcp-server-core.ts +54 -17
  229. package/src/api/server.ts +4 -23
  230. package/src/code-execution/isolate/scripts.ts +28 -3
  231. package/src/config-checks.ts +14 -0
  232. package/src/context-manager-errors.ts +37 -0
  233. package/src/context-options.ts +96 -0
  234. package/src/deferred-import.ts +239 -0
  235. package/src/dendrite/audience.ts +315 -0
  236. package/src/dendrite/index.ts +14 -2
  237. package/src/dendrite/presets.ts +18 -6
  238. package/src/dendrite/registry.ts +209 -12
  239. package/src/dendrite/types.ts +92 -8
  240. package/src/foreign-rounds.ts +199 -0
  241. package/src/framework.ts +7804 -3546
  242. package/src/gate/event-gate.ts +428 -65
  243. package/src/gate/index.ts +3 -0
  244. package/src/gate/rule-scope.ts +45 -0
  245. package/src/gate/suppress-script-worker.ts +89 -0
  246. package/src/gate/suppress-scripts.ts +414 -0
  247. package/src/gate/suppression-review.ts +517 -0
  248. package/src/gate/suppression-stats.ts +314 -0
  249. package/src/gate/types.ts +141 -7
  250. package/src/gate/wake-onboarding.ts +309 -0
  251. package/src/held-writes.ts +49 -60
  252. package/src/index.ts +9 -2
  253. package/src/jsonrpc-envelope.ts +81 -0
  254. package/src/mcpl/channel-registry.ts +353 -85
  255. package/src/mcpl/checkpoint-manager.ts +3 -2
  256. package/src/mcpl/feature-set-manager.ts +2 -1
  257. package/src/mcpl/inbound-source.ts +2 -1
  258. package/src/mcpl/inference-router.ts +7 -4
  259. package/src/mcpl/push-coalescer.ts +193 -34
  260. package/src/mcpl/push-handler.ts +10 -5
  261. package/src/mcpl/server-connection.ts +141 -60
  262. package/src/mcpl/tool-classes.ts +3 -0
  263. package/src/mcpl/types.ts +8 -2
  264. package/src/module-registry.ts +73 -17
  265. package/src/modules/discord/index.ts +229 -81
  266. package/src/modules/history/index.ts +439 -70
  267. package/src/modules/history/semantic.ts +15 -21
  268. package/src/modules/workspace/disk-agreement.ts +1 -1
  269. package/src/modules/workspace/index.ts +85 -23
  270. package/src/notice-tags.ts +10 -1
  271. package/src/operator-log.ts +36 -3
  272. package/src/presentation.ts +1676 -0
  273. package/src/prose-drafts.ts +16 -12
  274. package/src/read-marks.ts +328 -0
  275. package/src/recovery/offline-branch.ts +346 -32
  276. package/src/recovery/recover-cli.ts +21 -3
  277. package/src/send-tools.ts +40 -7
  278. package/src/stored-injections.ts +2 -2
  279. package/src/stored-removal.ts +31 -7
  280. package/src/tool-result-guard.ts +41 -7
  281. package/src/tune-out/coordinator.ts +4089 -457
  282. package/src/tune-out/tools.ts +160 -43
  283. package/src/tune-out/window-anchor.ts +277 -0
  284. package/src/types/agent.ts +86 -20
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +32 -12
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
@@ -16,6 +16,7 @@ import { maskNegotiatedCapabilities } from './capability-mask.js';
16
16
  import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
17
17
  import { CAPABILITY_DISABLED, boundedForLog, describeCause } from './errors.js';
18
18
  import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
19
+ import { jsonKind, readEnvelope } from '../jsonrpc-envelope.js';
19
20
  import {
20
21
  LEGACY_MCP_PROTOCOL_VERSION,
21
22
  MODERN_MCP_PROTOCOL_VERSION,
@@ -367,11 +368,32 @@ interface HeldItem {
367
368
  kept?: true;
368
369
  }
369
370
 
370
- /** A request's method for a log line, whatever the server sent there. */
371
- function describeMethod(method: unknown): string {
372
- return typeof method === 'string'
373
- ? `method ${logValue(method)}`
374
- : `a method of type ${method === null ? 'null' : typeof method}`;
371
+ /** Whether a response that can't be matched to a request of ours, its id
372
+ * being neither a string nor a number or missing, is logged: only a
373
+ * JSON-RPC 2.0 error is, since that's how a server says it couldn't read a
374
+ * message of ours (JSON-RPC 2.0 §5). Anything else id-less, such as a
375
+ * server's own JSON log line with `"id": null`, is ignored silently, as it
376
+ * was before #475, so a server's log output can't fill the host's. */
377
+ function isJsonRpcError(response: object): boolean {
378
+ return (response as { jsonrpc?: unknown }).jsonrpc === '2.0' && Object.hasOwn(response, 'error');
379
+ }
380
+
381
+ /** The log line for such an error. The host's own request ids are numbers,
382
+ * so it can't be matched to one. Once connected it may be the answer to a
383
+ * request of ours, which gets nothing from it and ends as any unanswered
384
+ * request does, by its timeout where it has one. (During the handshake an
385
+ * error with id null or no id answers initialize.) Its error member is the
386
+ * server's text, whatever JSON value it is, so it's printed redacted with
387
+ * `redact`, the server's redaction (logValueRedacted), as handleResponse
388
+ * prints a matched error's code and message. */
389
+ function unmatchedResponse(
390
+ serverId: string,
391
+ response: { id?: unknown; error?: unknown },
392
+ redact: (text: string) => string,
393
+ ): string {
394
+ const which = Object.hasOwn(response, 'id') ? `whose id is ${jsonKind(response.id)}` : 'with no id';
395
+ return `[mcpl] ${serverId}: ignored a response ${which}, which can't be matched to a request of ours; ` +
396
+ `its error: ${logValueRedacted(response.error, redact)}`;
375
397
  }
376
398
 
377
399
  /**
@@ -700,13 +722,14 @@ export class McplServerConnection extends EventEmitter {
700
722
  // Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
701
723
  private config: McplServerConfig | null = null;
702
724
  /** The redactor of the server's text for `config`, made at its first error
703
- * answer, by when `config` is set, and kept: a framework-launched child's
704
- * `env` is a getter that reads the awareness ledger, so a redactor built
705
- * per answer would read it per answer (#540's review, R1-2). An answer
706
- * takes one build of it for its words, data and log line (`now()`), so
707
- * it reads the ledger at most once. It builds again only after a build
708
- * that failed, which withholds that answer's text, or once a dial notes
709
- * something new (serverTextRedactor). */
725
+ * answer or response it can't match (handleResponse), by when `config`
726
+ * is set, and kept: a framework-launched child's `env` is a getter that
727
+ * reads the awareness ledger, so a redactor built per answer would read
728
+ * it per answer (#540's review, R1-2). An answer takes one build of it
729
+ * for its words, data and log line (`now()`), so it reads the ledger at
730
+ * most once. It builds again only after a build that failed, which
731
+ * withholds that answer's text, or once a dial notes something new
732
+ * (serverTextRedactor). */
710
733
  private redactor: ServerTextRedactor | null = null;
711
734
  private hostCapabilities: McplHostCapabilities | null = null;
712
735
  private reconnectEnabled = false;
@@ -1358,15 +1381,35 @@ export class McplServerConnection extends EventEmitter {
1358
1381
  // initialize response, whatever its id: a server's own ids can start
1359
1382
  // at 0, this handshake's. MCP lets a server ping before
1360
1383
  // initialization completes, and the ping gets its empty result, as
1361
- // on a connected transport. Anything else it sends now is ignored.
1362
- if ('method' in msg) {
1363
- const request = msg as unknown as JsonRpcRequest;
1364
- if (request.method === 'ping' && request.id != null) {
1365
- transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} }));
1384
+ // on a connected transport, `"id": null` included
1385
+ // (src/jsonrpc-envelope.ts). Anything else it sends now is ignored,
1386
+ // an invalid request included. Of its responses, the one with
1387
+ // initialize's id answers it, and so does a JSON-RPC 2.0 error with
1388
+ // id null or with no id: a peer answers a request it couldn't read
1389
+ // that way (JSON-RPC 2.0 §5; MCP 2025-11-25, Basic), initialize is
1390
+ // the only request this host has out, and a peer never answers a
1391
+ // response. Such an error means the server can't read this host's
1392
+ // messages, so the handshake fails at once, with the error's message.
1393
+ // `"jsonrpc": "2.0"` keeps a server's own JSON log line with an
1394
+ // error key and id null, or no id, from failing it; one with
1395
+ // initialize's id is taken for the answer, whatever its jsonrpc. Of
1396
+ // the other responses, a JSON-RPC 2.0 error whose id is neither a
1397
+ // string nor a number is logged, as on a connected transport, and
1398
+ // any other is ignored.
1399
+ const envelope = readEnvelope(msg as unknown as Record<string, unknown>);
1400
+ if (envelope.kind === 'request' && envelope.method === 'ping') {
1401
+ transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: envelope.id, result: {} }));
1402
+ return;
1403
+ }
1404
+ if (envelope.kind !== 'response') return;
1405
+ const namesNoRequest = msg.id === null || !Object.hasOwn(msg, 'id');
1406
+ const answersInitialize = msg.id === initId || (namesNoRequest && msg.jsonrpc === '2.0' && Boolean(msg.error));
1407
+ if (!answersInitialize) {
1408
+ if (typeof msg.id !== 'string' && typeof msg.id !== 'number' && isJsonRpcError(msg)) {
1409
+ console.error(unmatchedResponse(config.id, msg, (text) => redactServerText(text, config)));
1366
1410
  }
1367
1411
  return;
1368
1412
  }
1369
- if (msg.id !== initId) return;
1370
1413
  cleanup();
1371
1414
  if (msg.error) {
1372
1415
  // describeCause rather than the template's own conversion: the
@@ -1739,8 +1782,9 @@ export class McplServerConnection extends EventEmitter {
1739
1782
  * following `nextCursor` across pages (2024-11-05 pagination). Cursors are
1740
1783
  * opaque strings, `''` included: only an absent or null `nextCursor` ends
1741
1784
  * the inventory (a null one is how some servers say "no more", and main
1742
- * read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
1743
- * that isn't a string), or cursors that don't terminate (a repeat, or more
1785
+ * read it as a complete page). A page that isn't well formed (no `tools` array, an entry
1786
+ * that isn't an object with a string `name`, a cursor that isn't a string), or cursors
1787
+ * that don't terminate (a repeat, or more
1744
1788
  * than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
1745
1789
  * empty inventory published as complete. The read tap (RFC-014) is handed
1746
1790
  * the complete inventory once, with the last page's `_meta`, as the last
@@ -1755,9 +1799,12 @@ export class McplServerConnection extends EventEmitter {
1755
1799
  let first: Record<string, unknown> | undefined;
1756
1800
  const read: McpToolDefinition[] = [];
1757
1801
  const revs = new Set<unknown>();
1802
+ const isTool = (entry: unknown): boolean =>
1803
+ !!entry && typeof entry === 'object' && typeof (entry as { name?: unknown }).name === 'string';
1758
1804
  const onRead = (result: unknown) => {
1759
1805
  if (!result || typeof result !== 'object' || !Array.isArray((result as { tools?: unknown }).tools)) return;
1760
1806
  const r = result as { tools: McpToolDefinition[]; nextCursor?: unknown; _meta?: unknown };
1807
+ if (!r.tools.every(isTool)) return;
1761
1808
  for (const t of r.tools) read.push(t);
1762
1809
  const rev = (r._meta as Record<string, unknown> | undefined)?.['mcpl/rev'];
1763
1810
  if (typeof rev === 'number') revs.add(rev);
@@ -1774,6 +1821,8 @@ export class McplServerConnection extends EventEmitter {
1774
1821
  if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
1775
1822
  throw malformed('no tools array');
1776
1823
  }
1824
+ const unreadable = (result.tools as unknown[]).findIndex((entry) => !isTool(entry));
1825
+ if (unreadable !== -1) throw malformed(`entry ${unreadable + 1} isn't an object with a string name`);
1777
1826
  first ??= result;
1778
1827
  for (const t of result.tools as McpToolDefinition[]) tools.push(t);
1779
1828
  if (result.nextCursor === undefined || result.nextCursor === null) {
@@ -2389,10 +2438,10 @@ export class McplServerConnection extends EventEmitter {
2389
2438
  // passed is the dead process's (afterBoundary).
2390
2439
  const epoch = this.transportEpoch;
2391
2440
  transport.on('line', (line: string) => {
2392
- const msg = messageFromLine(this.id, line, transport) as JsonRpcRequest | JsonRpcResponse | undefined;
2441
+ const msg = messageFromLine(this.id, line, transport);
2393
2442
  if (!msg) return;
2394
2443
  try {
2395
- this.dispatchMessage(msg, epoch);
2444
+ this.dispatchMessage(msg as Record<string, unknown>, epoch);
2396
2445
  } catch (error) {
2397
2446
  const request = msg as { method?: unknown; id?: unknown };
2398
2447
  this.reportInboundFailure(error, {
@@ -2404,25 +2453,47 @@ export class McplServerConnection extends EventEmitter {
2404
2453
  }
2405
2454
 
2406
2455
  /** Contain synchronous dispatch failures; async RPC handlers own their promises. */
2407
- private dispatchMessage(msg: JsonRpcRequest | JsonRpcResponse, epoch: number): void {
2408
- // Is this a response to one of our outbound requests?
2409
- if ('id' in msg && msg.id != null && !('method' in msg)) {
2410
- const late = epoch !== this.transportEpoch;
2411
- // RFC-014: its hints apply as it's read, whether or not anyone still
2412
- // waits for it. A dead transport's revs are its own, so a response
2413
- // read after its boundary is never tapped.
2414
- if (!late) this.tapResponse(msg as JsonRpcResponse);
2415
- this.handleResponse(msg as JsonRpcResponse, late);
2416
- return;
2456
+ private dispatchMessage(msg: Record<string, unknown>, epoch: number): void {
2457
+ // The envelope is read once, here (src/jsonrpc-envelope.ts says how).
2458
+ const envelope = readEnvelope(msg);
2459
+ switch (envelope.kind) {
2460
+ case 'response': {
2461
+ // Never answered, so a server that answers what it can't read, with
2462
+ // id null, can't start an exchange of errors with us. One read after
2463
+ // its transport's boundary is late (handleResponse).
2464
+ const late = epoch !== this.transportEpoch;
2465
+ // RFC-014: its hints apply as it's read, whether or not anyone still
2466
+ // waits for it. A dead transport's revs are its own, so a response
2467
+ // read after its boundary is never tapped.
2468
+ if (!late) this.tapResponse(msg as unknown as JsonRpcResponse);
2469
+ this.handleResponse(msg as unknown as JsonRpcResponse, late);
2470
+ return;
2471
+ }
2472
+ case 'neither':
2473
+ // Not JSON-RPC: ignored like the server's other unrelated output.
2474
+ return;
2475
+ case 'invalid': {
2476
+ // Answered on the transport it came on, as every request is. One
2477
+ // read after that transport's boundary is the dead process's and
2478
+ // isn't answered: sendErrorResponse logs the answer it didn't send.
2479
+ this.sendErrorResponse(envelope.id, envelope.error.code, envelope.error.message, undefined, epoch);
2480
+ if (epoch !== this.transportEpoch) return;
2481
+ const forMethod = envelope.method !== undefined ? ` for method ${logValue(envelope.method)}` : '';
2482
+ console.error(`[mcpl] ${this.id}: answered -32600 to an invalid request${forMethod}: ${envelope.problems}`);
2483
+ return;
2484
+ }
2417
2485
  }
2418
2486
 
2419
- // It is an inbound request or notification from the server
2420
- const request = msg as JsonRpcRequest;
2487
+ // It is an inbound request or notification from the server, its method
2488
+ // a string. A request's id is a string, a number or null, and `"id": null`
2489
+ // is answered with id null; a notification has none.
2490
+ const request = msg as unknown as JsonRpcRequest;
2491
+ const requestId = envelope.kind === 'request' ? envelope.id : undefined;
2421
2492
 
2422
2493
  // Read after its transport's boundary, it is the dead process's: the
2423
2494
  // boundary's rule applies (afterBoundary).
2424
2495
  if (epoch !== this.transportEpoch) {
2425
- this.afterBoundary(request, epoch);
2496
+ this.afterBoundary(request, requestId, epoch);
2426
2497
  return;
2427
2498
  }
2428
2499
 
@@ -2432,7 +2503,7 @@ export class McplServerConnection extends EventEmitter {
2432
2503
  // paused plane mustn't hold that answer. A ping sent as a notification
2433
2504
  // has nothing to answer. The handshake answers one too.
2434
2505
  if (request.method === 'ping') {
2435
- if (request.id != null) this.sendResponse(request.id, {});
2506
+ if (requestId !== undefined) this.sendResponse(requestId, {});
2436
2507
  return;
2437
2508
  }
2438
2509
 
@@ -2443,11 +2514,11 @@ export class McplServerConnection extends EventEmitter {
2443
2514
  if (request.method === 'push/event') this.tap({ kind: 'push', params: request.params });
2444
2515
  else if (request.method === 'tools/hints' && this.readTap) {
2445
2516
  this.tap({ kind: 'hints', params: request.params });
2446
- if (request.id != null) this.sendResponse(request.id, {});
2517
+ if (requestId !== undefined) this.sendResponse(requestId, {});
2447
2518
  return;
2448
2519
  }
2449
2520
 
2450
- const eventName = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2521
+ const eventName = Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2451
2522
  ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
2452
2523
 
2453
2524
  // §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
@@ -2459,8 +2530,8 @@ export class McplServerConnection extends EventEmitter {
2459
2530
  // routed, control-plane classified, and mutating declarations — a
2460
2531
  // live ungated self-attestation path (PR #79 review blocker 4).
2461
2532
  if (request.method === McplMethod.FeatureSetsChanged) {
2462
- if (request.id != null) {
2463
- this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
2533
+ if (requestId !== undefined) {
2534
+ this.sendErrorResponse(requestId, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
2464
2535
  } else {
2465
2536
  console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
2466
2537
  }
@@ -2468,8 +2539,8 @@ export class McplServerConnection extends EventEmitter {
2468
2539
  }
2469
2540
 
2470
2541
  if (request.method === McplMethod.ScopeElevate) {
2471
- if (request.id != null) {
2472
- this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
2542
+ if (requestId !== undefined) {
2543
+ this.sendErrorResponse(requestId, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
2473
2544
  }
2474
2545
  return;
2475
2546
  }
@@ -2481,20 +2552,19 @@ export class McplServerConnection extends EventEmitter {
2481
2552
 
2482
2553
  if (!eventName) {
2483
2554
  // §6.6: a request that will never be answered MUST get an error, so a
2484
- // request (it has an id) whose method isn't a string, or isn't one this
2485
- // host handles, is answered -32601. A notification nobody handles is
2486
- // ignored, as JSON-RPC asks.
2487
- if (request.id != null) {
2488
- this.sendErrorResponse(request.id, -32601, 'Method not found');
2489
- console.error(`[mcpl] ${this.id}: answered -32601 to a request for ${describeMethod(request.method)}, which this host doesn't handle`);
2555
+ // request whose method isn't one this host handles is answered -32601.
2556
+ // A notification nobody handles is ignored, as JSON-RPC asks.
2557
+ if (requestId !== undefined) {
2558
+ this.sendErrorResponse(requestId, -32601, 'Method not found');
2559
+ console.error(`[mcpl] ${this.id}: answered -32601 to a request for method ${logValue(request.method)}, which this host doesn't handle`);
2490
2560
  }
2491
2561
  return;
2492
2562
  }
2493
2563
 
2494
2564
  // Emit the typed event with params and (for requests) a respond callback
2495
- if (request.id != null) {
2565
+ if (requestId !== undefined) {
2496
2566
  // Server expects a response — provide a respond helper
2497
- this.dispatchInboundEvent(eventName, request.params, this.responderFor(request.id, epoch));
2567
+ this.dispatchInboundEvent(eventName, request.params, this.responderFor(requestId, epoch));
2498
2568
  } else {
2499
2569
  // Notification — no response expected
2500
2570
  this.dispatchInboundEvent(eventName, request.params);
@@ -2504,9 +2574,10 @@ export class McplServerConnection extends EventEmitter {
2504
2574
  /**
2505
2575
  * A request's responder, bound to the transport it came on (`epoch`): an
2506
2576
  * answer, or a streamed inference's chunk, sent after that transport has
2507
- * closed reaches nothing, never a server that didn't ask it.
2577
+ * closed reaches nothing, never a server that didn't ask it. A request
2578
+ * sent with id null is answered with id null.
2508
2579
  */
2509
- private responderFor(id: string | number, epoch: number) {
2580
+ private responderFor(id: string | number | null, epoch: number) {
2510
2581
  return {
2511
2582
  id,
2512
2583
  signal: epoch === this.transportEpoch ? this.transportEnded.signal : AbortSignal.abort(),
@@ -2531,17 +2602,17 @@ export class McplServerConnection extends EventEmitter {
2531
2602
  * it while its writer lives, with no grant to come, so it drops and logs
2532
2603
  * all of it; hold's bounds cap what any other keeps.
2533
2604
  */
2534
- private afterBoundary(request: JsonRpcRequest, epoch: number): void {
2535
- const event = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2605
+ private afterBoundary(request: JsonRpcRequest, requestId: string | number | null | undefined, epoch: number): void {
2606
+ const event = Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
2536
2607
  ? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
2537
2608
  if (this.closedForGood || event === undefined || !McplServerConnection.WORLD_EVENTS.has(event)) {
2538
2609
  console.error(
2539
2610
  `[mcpl] ${this.id}: dropped a message read after its transport closed` +
2540
- `${this.closedForGood ? ', with no transport to come' : ''}: ${describeMethod(request.method)}`,
2611
+ `${this.closedForGood ? ', with no transport to come' : ''}: method ${logValue(request.method)}`,
2541
2612
  );
2542
2613
  return;
2543
2614
  }
2544
- this.hold(event, request.id != null ? [request.params, this.responderFor(request.id, epoch)] : [request.params], true);
2615
+ this.hold(event, requestId !== undefined ? [request.params, this.responderFor(requestId, epoch)] : [request.params], true);
2545
2616
  }
2546
2617
 
2547
2618
  /** Live delivery and delayed plane flushes share the same containment boundary. */
@@ -2596,7 +2667,7 @@ export class McplServerConnection extends EventEmitter {
2596
2667
  /** Tap a response to a hinted request (RFC-014): its result's hints, or a
2597
2668
  * list's statement, apply now, awaited or not. */
2598
2669
  private tapResponse(response: JsonRpcResponse): void {
2599
- if (!this.readTap) return;
2670
+ if (!this.readTap || response.id === null) return;
2600
2671
  const sent = this.hintedRequests.get(response.id);
2601
2672
  if (!sent) return;
2602
2673
  this.hintedRequests.delete(response.id);
@@ -2609,6 +2680,16 @@ export class McplServerConnection extends EventEmitter {
2609
2680
  * Handle a JSON-RPC response by resolving/rejecting the corresponding pending request.
2610
2681
  */
2611
2682
  private handleResponse(response: JsonRpcResponse, late = false): void {
2683
+ // A response whose id is neither a string nor a number, or that has
2684
+ // none, can't be matched to a request of ours, on this transport or an
2685
+ // earlier one. It isn't an orphan, which is a late answer to one, so
2686
+ // it's ignored, and logged when it's a JSON-RPC 2.0 error.
2687
+ if (typeof response.id !== 'string' && typeof response.id !== 'number') {
2688
+ if (isJsonRpcError(response)) {
2689
+ console.error(unmatchedResponse(this.id, response, (this.redactor ??= serverTextRedactor(this.config ?? {})).now()));
2690
+ }
2691
+ return;
2692
+ }
2612
2693
  // One read after its transport's boundary answers a request the close
2613
2694
  // ended. It's matched against none of this connection's request records,
2614
2695
  // since the next transport numbers its requests afresh, and goes the way
@@ -2706,7 +2787,7 @@ export class McplServerConnection extends EventEmitter {
2706
2787
  /**
2707
2788
  * Send a successful JSON-RPC response back to the server.
2708
2789
  */
2709
- private sendResponse(id: string | number, result: unknown, epoch = this.transportEpoch): void {
2790
+ private sendResponse(id: string | number | null, result: unknown, epoch = this.transportEpoch): void {
2710
2791
  // A closed connection's epoch is past every request's, so a refusal
2711
2792
  // while it is closed is logged too.
2712
2793
  if (!this.sameTransport(id, epoch) || this.closed) return;
@@ -2717,7 +2798,7 @@ export class McplServerConnection extends EventEmitter {
2717
2798
  /** Whether a response to a request received on `epoch` may go out now.
2718
2799
  * The id is the server's, so it's logged as a field; `answer`, which the
2719
2800
  * caller has formatted for the log, ends the line. */
2720
- private sameTransport(id: string | number, epoch: number, answer?: string): boolean {
2801
+ private sameTransport(id: string | number | null, epoch: number, answer?: string): boolean {
2721
2802
  if (epoch === this.transportEpoch) return true;
2722
2803
  console.error(
2723
2804
  `[mcpl] ${this.id}: not answering request ${logValue(id)}: the transport it came on has closed${answer ? `, with ${answer}` : ''}`,
@@ -2729,7 +2810,7 @@ export class McplServerConnection extends EventEmitter {
2729
2810
  * Send a JSON-RPC error response back to the server.
2730
2811
  */
2731
2812
  private sendErrorResponse(
2732
- id: string | number,
2813
+ id: string | number | null,
2733
2814
  code: number,
2734
2815
  message: string,
2735
2816
  data?: unknown,
@@ -87,8 +87,11 @@ export const BUILTIN_TOOL_CLASSES: Readonly<Record<string, readonly ToolClass[]>
87
87
  wake: ['control'],
88
88
  wake_add_rule: ['control'],
89
89
  wake_remove_rule: ['control'],
90
+ wake_accept_suggestions: ['control'],
91
+ wake_decline: ['control'],
90
92
  event_tags: ['control'],
91
93
  prose_help: ['control'],
94
+ prose_locus: ['control'],
92
95
  // Files and blobs in the agent's own stores.
93
96
  save_recent_image: ['files'],
94
97
  read_image: ['files'],
package/src/mcpl/types.ts CHANGED
@@ -22,7 +22,8 @@
22
22
  export interface JsonRpcRequest {
23
23
  jsonrpc: '2.0';
24
24
  method: string;
25
- id?: string | number;
25
+ /** A request's id; null is allowed, though JSON-RPC 2.0 discourages it. */
26
+ id?: string | number | null;
26
27
  params?: Record<string, unknown>;
27
28
  }
28
29
 
@@ -31,7 +32,8 @@ export interface JsonRpcRequest {
31
32
  */
32
33
  export interface JsonRpcResponse {
33
34
  jsonrpc: '2.0';
34
- id: string | number;
35
+ /** The request's id, or null when the request's own couldn't be read. */
36
+ id: string | number | null;
35
37
  result?: unknown;
36
38
  error?: JsonRpcError;
37
39
  }
@@ -596,6 +598,10 @@ export interface FeatureSetDeclaration {
596
598
  coreTags?: string[];
597
599
  tags?: Record<string, unknown>;
598
600
  keyed?: Record<string, unknown>;
601
+ /** Ordered suggested rules (RFC-001 §5.1). Advisory: applied only on
602
+ * acceptance (§5.2), which wake onboarding asks the agent for. */
603
+ suggestedTreatment?: unknown[];
604
+ /** @deprecated RFC-001 revision 1's name for suggestedTreatment; still read. */
599
605
  defaultTreatment?: unknown[];
600
606
  open?: boolean;
601
607
  };
@@ -1,11 +1,15 @@
1
1
  import type { JsStateRegistration, JsStore } from '@animalabs/chronicle';
2
2
  import type { ContentBlock } from '@animalabs/membrane';
3
3
  import type {
4
+ ContextManager,
4
5
  MessageId,
5
6
  MessageMetadata,
6
7
  MessageQuery,
7
8
  ContextInjection,
9
+ StoredMessage,
10
+ WindowStatus,
8
11
  } from '@animalabs/context-manager';
12
+ import { WriteOutcomeUnknownError } from './context-manager-errors.js';
9
13
  import type {
10
14
  Module,
11
15
  ModuleContext,
@@ -25,7 +29,6 @@ import type {
25
29
  ModuleContextInjection,
26
30
  MessageQueryResult,
27
31
  } from './types/index.js';
28
- import { heldWriteId } from './held-writes.js';
29
32
  import { boundedErrorMessage } from './error-message.js';
30
33
  import type { Agent } from './agent.js';
31
34
  import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
@@ -150,6 +153,13 @@ export class ModuleRegistry {
150
153
  message: string,
151
154
  data?: Record<string, unknown>,
152
155
  ) => void;
156
+ /** The window lookups ModuleContext.windowStatusOf, observes, viewKeeps
157
+ * and agentNameOf give modules (#291), when the framework offers them.
158
+ * Readonly-public, as notifyOpsFn is. */
159
+ readonly windowStatusOfFn?: (id: MessageId, opts?: { forAgent?: string }) => WindowStatus;
160
+ readonly observesFn?: (observer: string, observed: string) => boolean;
161
+ readonly viewKeepsFn?: (row: StoredMessage, opts?: { forAgent?: string }) => boolean;
162
+ readonly agentNameOfFn?: (contextManager: ContextManager) => string | undefined;
153
163
 
154
164
  constructor(
155
165
  store: JsStore,
@@ -170,6 +180,14 @@ export class ModuleRegistry {
170
180
  onTrace: (listener: TraceEventListener) => () => void;
171
181
  callTool: (call: ToolCall) => Promise<ToolResult>;
172
182
  notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
183
+ /** Where a row stands with an agent's window (ModuleContext.windowStatusOf). */
184
+ windowStatusOf?: (id: MessageId, opts?: { forAgent?: string }) => WindowStatus;
185
+ /** Whether one agent observes another's own rows (ModuleContext.observes). */
186
+ observes?: (observer: string, observed: string) => boolean;
187
+ /** Whether an agent's view keeps a stored row (ModuleContext.viewKeeps). */
188
+ viewKeeps?: (row: StoredMessage, opts?: { forAgent?: string }) => boolean;
189
+ /** The live agent whose context a context manager is (ModuleContext.agentNameOf). */
190
+ agentNameOf?: (contextManager: ContextManager) => string | undefined;
173
191
  }
174
192
  ) {
175
193
  this.store = store;
@@ -184,6 +202,10 @@ export class ModuleRegistry {
184
202
  this.onTraceFn = options.onTrace;
185
203
  this.callToolFn = options.callTool;
186
204
  this.notifyOpsFn = options.notifyOps;
205
+ this.windowStatusOfFn = options.windowStatusOf;
206
+ this.observesFn = options.observes;
207
+ this.viewKeepsFn = options.viewKeeps;
208
+ this.agentNameOfFn = options.agentNameOf;
187
209
  this.dropRetiredPushFn = options.dropRetiredPush;
188
210
  }
189
211
 
@@ -748,6 +770,12 @@ class ModuleContextImpl implements ModuleContext {
748
770
  private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
749
771
  private onTraceFn: (listener: TraceEventListener) => () => void;
750
772
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
773
+ /** Offered only when the registry was given the framework's lookups, so
774
+ * a module can tell a framework that has them from one that hasn't. */
775
+ readonly windowStatusOf?: (id: MessageId, opts?: { forAgent?: string }) => WindowStatus;
776
+ readonly observes?: (observer: string, observed: string) => boolean;
777
+ readonly viewKeeps?: (row: StoredMessage, opts?: { forAgent?: string }) => boolean;
778
+ readonly agentNameOf?: (contextManager: ContextManager) => string | undefined;
751
779
 
752
780
  // External ID mapping stored in module state
753
781
  private externalIdMap: Map<string, MessageId> = new Map();
@@ -794,6 +822,14 @@ class ModuleContextImpl implements ModuleContext {
794
822
  this.pushEventFn = pushEvent;
795
823
  this.onTraceFn = onTrace;
796
824
  this.callToolFn = callTool;
825
+ const windowStatusOf = registry.windowStatusOfFn;
826
+ if (windowStatusOf) this.windowStatusOf = (id, opts) => windowStatusOf(id, opts);
827
+ const observes = registry.observesFn;
828
+ if (observes) this.observes = (observer, observed) => observes(observer, observed);
829
+ const viewKeeps = registry.viewKeepsFn;
830
+ if (viewKeeps) this.viewKeeps = (row, opts) => viewKeeps(row, opts);
831
+ const agentNameOf = registry.agentNameOfFn;
832
+ if (agentNameOf) this.agentNameOf = (contextManager) => agentNameOf(contextManager);
797
833
 
798
834
  // Load external ID map from state if exists
799
835
  const state = this.getState<{ externalIdMap?: Record<string, string> }>();
@@ -951,15 +987,35 @@ class ModuleContextImpl implements ModuleContext {
951
987
  throw new Error(`A message for agent ${JSON.stringify(opts.forAgent)} can't carry metadata.external: `
952
988
  + "its external id would be looked up in the primary's window. Leave forAgent out, or the external id.");
953
989
  }
954
- const id = this.addMessageFn(participant, content, metadata, opts);
990
+ let id: MessageId;
991
+ try {
992
+ id = this.addMessageFn(participant, content, metadata, opts);
993
+ } catch (error) {
994
+ // A write whose store threw where context-manager can't tell whether
995
+ // it landed (WriteOutcomeUnknownError, which the write is answered by)
996
+ // may be stored: its external id maps to '', as a write kept in memory
997
+ // does, so findMessageByExternalId looks it up in the store. A mapping
998
+ // that can't be persisted is logged, and the write's own error thrown.
999
+ if (metadata?.external && error instanceof WriteOutcomeUnknownError) {
1000
+ this.externalIdMap.set(`${metadata.external.source}:${metadata.external.id}`, '');
1001
+ try {
1002
+ this.persistExternalIdMap();
1003
+ } catch (persist) {
1004
+ console.error(`[module:${this.moduleName}] the external id of a write that may be stored could not be persisted:`, persist);
1005
+ }
1006
+ }
1007
+ throw error;
1008
+ }
955
1009
 
956
- // Track external ID if provided. A write the framework holds keeps '',
957
- // as it did before held writes had handles: the map is module state an
958
- // earlier release reads back, and a handle is no id to it.
959
- // findMessageByExternalId finds the held write by its external id.
1010
+ // Track external ID if provided: the stored id the write was given
1011
+ // (#291 stores every write at once), or '' for one kept in memory (a
1012
+ // surgery's hold, or the wait for its agent's recovery), as earlier
1013
+ // releases kept '' for a write they held.
1014
+ // findMessageByExternalId finds such a write by its external id once it
1015
+ // is stored.
960
1016
  if (metadata?.external) {
961
1017
  const key = `${metadata.external.source}:${metadata.external.id}`;
962
- this.externalIdMap.set(key, heldWriteId(id) === undefined ? id : '');
1018
+ this.externalIdMap.set(key, id);
963
1019
  // Persist the mapping
964
1020
  this.persistExternalIdMap();
965
1021
  }
@@ -991,15 +1047,16 @@ class ModuleContextImpl implements ModuleContext {
991
1047
  if (entry === undefined) return null;
992
1048
  // A stored id answers while the store holds its row, and once the row is
993
1049
  // gone the entry goes, as removeMessage drops it, except from a context
994
- // whose module has stopped, which keeps its mapping as it was. The '' a held
995
- // write keeps here (addMessage) names no row, so the message is looked up in
996
- // the store by its external id, held rows included, and the first row found
997
- // is taken, as context-manager's findByExternalId takes it: a held row
998
- // answers by its handle and leaves the entry as it is, and a stored row's id
999
- // becomes the entry. With nothing found, '' stays, as earlier releases kept
1000
- // it: in a module's start(), the writes the last process held don't show
1001
- // yet, since create() restores them after the modules start, and a write
1002
- // that shows later finds its entry.
1050
+ // whose module has stopped, which keeps its mapping as it was. A '' here
1051
+ // names no row: a write kept in memory (a surgery's hold, or the wait for
1052
+ // its agent's recovery), or one an earlier release held, whose map it
1053
+ // persisted with ''. So the message is looked up in the store by its
1054
+ // external id, and the first row found is taken, as context-manager's
1055
+ // findByExternalId takes it, and its id becomes the entry (but in a
1056
+ // stopped module's context). With nothing found, '' stays: in a module's
1057
+ // start(), a write an earlier release left queued isn't stored yet, since
1058
+ // create() imports those after the modules start, and a write that shows
1059
+ // later finds its entry.
1003
1060
  if (entry !== '') {
1004
1061
  if (this.getMessageFn(entry)) return entry;
1005
1062
  if (!this.retired) {
@@ -1009,7 +1066,6 @@ class ModuleContextImpl implements ModuleContext {
1009
1066
  return null;
1010
1067
  }
1011
1068
  const row = this.queryMessagesFn({ source, externalIds: [externalId] }).messages[0];
1012
- if (row?.held) return row.id;
1013
1069
  if (!row) return null;
1014
1070
  if (!this.retired) {
1015
1071
  this.externalIdMap.set(key, row.id);