@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
@@ -350,7 +350,8 @@ interface LiveCreate {
350
350
  /** converting: the live path fetches its attachments; queued: its event
351
351
  * waits in the queue; writing: onProcess answered it, and each Discord
352
352
  * event after it looks for the message's row, and ends the entry once it
353
- * shows. */
353
+ * shows, or a release that couldn't store its write ends it with no row
354
+ * (`message:lost`, endLostCreate). */
354
355
  phase: 'converting' | 'queued' | 'writing';
355
356
  /** A delete for the message came before its create was handled, so no row
356
357
  * comes, and the entry waits for that delete to be handled. */
@@ -358,6 +359,11 @@ interface LiveCreate {
358
359
  /** The latest edit's text and attachments, from while it converted. */
359
360
  text?: string;
360
361
  attachments?: DiscordAttachment[];
362
+ unapplied?: 'delete' | Array<{ text: string; attachments?: DiscordAttachment[] }>;
363
+ /** A release lost its write without saying whether it landed, and the
364
+ * lookup made then threw (endLostCreate): the next sweep that reads the
365
+ * store and finds no row ends the entry, as that lookup would have. */
366
+ lostUnread?: true;
361
367
  }
362
368
 
363
369
  /**
@@ -402,18 +408,24 @@ export class DiscordModule implements Module {
402
408
 
403
409
  // The Discord messages whose live create is on its way to the store: from
404
410
  // the moment the live path takes one up, through its conversion and the
405
- // event queue, until a query shows the row onProcess answered with, stored
406
- // or held. The framework holds a module's writes while the agent's turn is
407
- // alive or the host is quiesced, and queryMessages shows a held write's
408
- // row, marked held, so the next Discord event finds it either way and ends
409
- // its entry. A history sync stores none of these messages, so no message is
411
+ // event queue, until a query shows the row onProcess answered with. The
412
+ // framework stores a module's write at once, whatever the agent's turn or
413
+ // the host's mode (#291); only a live operator surgery's hold, or a context
414
+ // manager refusing its work, keeps it in memory, which a restart loses,
415
+ // until the release or the branch move that re-initializes the manager
416
+ // stores it. So the next Discord event once it is stored
417
+ // finds its row, marked held while the agent's window doesn't hold it
418
+ // (asModuleRow), and ends its entry. A history sync stores none of these messages, so no message is
410
419
  // stored twice. A delete before the create is handled drops it: nothing is
411
420
  // stored and nobody is woken. Any message's entry ends when its delete is
412
421
  // handled, since a sync running then skips the message (noteLive) and a
413
- // later one doesn't fetch it. An edit while it converts is folded into it.
414
- // A create whose answer the framework fails to write or hold shows no row,
415
- // and keeps its entry, and the sync off its message, until its delete is
416
- // handled or the module stops.
422
+ // later one doesn't fetch it, but for an answered create with no row yet
423
+ // (`writing`), whose entry keeps its delete for the release that stores
424
+ // it (LiveCreate.unapplied). An edit while it converts is folded into it.
425
+ // A create whose answer the framework fails to write shows no row,
426
+ // and keeps its entry, and the sync off its message, until the module
427
+ // stops, but for one a surgery's hold kept, whose release, losing it,
428
+ // ends its entry (`message:lost`, endLostCreate).
417
429
  private readonly creating = new Map<string, LiveCreate>();
418
430
 
419
431
  // Typing keepalive: agentName -> { interval, deadline }. Started on
@@ -449,17 +461,28 @@ export class DiscordModule implements Module {
449
461
  // inference and its turn ending, so it stays alive for the whole turn
450
462
  // (including tool-call cycles) and stops cleanly whether the agent
451
463
  // responds, errors, or decides to stay silent.
452
- if (this.config.autoTyping) {
453
- this.unsubscribeTrace = ctx.onTrace((event: TraceEvent) => {
454
- const agentName = (event as { agentName?: string }).agentName;
455
- if (!agentName) return;
456
- if (event.type === 'inference:started' && !event.silent) {
457
- this.startTypingLoop(agentName);
458
- } else if (TURN_TERMINAL_TRACES.has(event.type)) {
459
- this.stopTypingLoop(agentName);
460
- }
461
- });
462
- }
464
+ this.unsubscribeTrace = ctx.onTrace((event: TraceEvent) => {
465
+ // The edits and deletes kept for a held create are applied, or its
466
+ // record ended, on the trace of the surgery's release that stores or
467
+ // loses it (applyUnapplied, endLostCreate): the exception the README
468
+ // names to traces being observability-only.
469
+ if (event.type === 'message:added' && event.source === 'surgery-release') {
470
+ this.applyUnapplied();
471
+ return;
472
+ }
473
+ if (event.type === 'message:lost' && event.external?.source === 'discord') {
474
+ this.endLostCreate(event.external.id, event.mayBeStored === true);
475
+ return;
476
+ }
477
+ if (!this.config.autoTyping) return;
478
+ const agentName = (event as { agentName?: string }).agentName;
479
+ if (!agentName) return;
480
+ if (event.type === 'inference:started' && !event.silent) {
481
+ this.startTypingLoop(agentName);
482
+ } else if (TURN_TERMINAL_TRACES.has(event.type)) {
483
+ this.stopTypingLoop(agentName);
484
+ }
485
+ });
463
486
 
464
487
  // Set up Discord event handlers
465
488
  this.client.onMessage((message) => this.handleDiscordMessage(message));
@@ -772,10 +795,13 @@ export class DiscordModule implements Module {
772
795
  // A stopped module answers nothing (stop()).
773
796
  if (!this.ctx) return {};
774
797
 
775
- // Each Discord event ends the entries whose row a query shows now, stored
776
- // or held.
798
+ // Each Discord event ends the entries whose row a query shows now,
799
+ // having applied what was kept for them first (applyUnapplied): an
800
+ // entry kept where its lost create's read threw (endLostCreate) still
801
+ // holds the edits or the delete made while its create was held.
777
802
  if (event.type.startsWith('discord:')) {
778
- for (const [id, live] of this.creating) if (live.phase === 'writing' && this.storedRow(id)) this.creating.delete(id);
803
+ this.applyUnapplied();
804
+ this.endShownCreates();
779
805
  }
780
806
 
781
807
  // Handle new Discord message
@@ -834,7 +860,7 @@ export class DiscordModule implements Module {
834
860
  const contentBlocks = msg.contentBlocks ?? textFirst(storedOwnText(msg.content));
835
861
  this.noteLive(msg.discordMessageId);
836
862
 
837
- // A message a history sync wrote first, stored or held, keeps that row:
863
+ // A message a history sync wrote first keeps that row:
838
864
  // the create adds none, and still sets the reply context and asks for
839
865
  // inference, since the message arrived live. Either way the entry waits
840
866
  // until a Discord event finds the message's row.
@@ -864,53 +890,10 @@ export class DiscordModule implements Module {
864
890
  }
865
891
 
866
892
  // Handle Discord message edit
867
- if (event.type === 'discord:edit') {
868
- const edit = event as DiscordEditEvent;
869
- // The message's row, not only its id: an edit changes the message's own
870
- // text and keeps what the host added beside it, but for the blocks of
871
- // attachments the edit took away, when the client passed the edit's
872
- // attachments and the row is text-first.
873
- // An edit that comes while the live path converts the message's create
874
- // is folded into that create (handleDiscordMessageEdit). Any other edit
875
- // to a message with no row, stored or held, is dropped: a history sync
876
- // running now stores the text it fetched, and the next sync's text rule
877
- // changes it. A row stored in shards can't be edited one shard at a
878
- // time, so it keeps its text, and the host's log says so. Only an edit
879
- // that writes marks the message live, so an update that changes nothing
880
- // leaves a running sync free to rewrite the row.
881
- const stored = this.storedRow(edit.discordMessageId);
882
- if (!stored) return {};
883
- if (stored.bodyGroupId) {
884
- console.warn(`Discord: An update to message ${edit.discordMessageId} wasn't applied: it's stored in shards, which can't be edited, so if its text changed, the agent still has the earlier text`);
885
- return {};
886
- }
887
- const texted = contentForText(stored, edit.newContent);
888
- const content = edit.attachments && stored.metadata?.[TEXT_FIRST] === true
889
- ? withoutGoneAttachments(texted ?? stored.content, edit.attachments)
890
- : texted;
891
- if (!content || content === stored.content) return {};
892
- this.noteLive(edit.discordMessageId);
893
- return { editMessages: [{ messageId: stored.id, content }] };
894
- }
893
+ if (event.type === 'discord:edit') return this.answerEdit(event as DiscordEditEvent);
895
894
 
896
895
  // Handle Discord message delete
897
- if (event.type === 'discord:delete') {
898
- const del = event as DiscordDeleteEvent;
899
- // Every row of the message goes, so one stored twice doesn't outlive
900
- // its delete as a copy. A row stored in shards is removed whole: the
901
- // framework takes out the body a shard belongs to. The message is the
902
- // live path's even when no row is found, so a running sync doesn't
903
- // store what its fetch may still hold, and that ends its entry.
904
- this.noteLive(del.discordMessageId);
905
- this.creating.delete(del.discordMessageId);
906
- const rows = this.storedRows(del.discordMessageId);
907
- if (rows.length === 0) {
908
- return {};
909
- }
910
- return {
911
- removeMessages: oncePerBody(rows),
912
- };
913
- }
896
+ if (event.type === 'discord:delete') return this.answerDelete(event as DiscordDeleteEvent);
914
897
 
915
898
  return {};
916
899
  }
@@ -1077,8 +1060,8 @@ export class DiscordModule implements Module {
1077
1060
 
1078
1061
  /**
1079
1062
  * Sync history for a specific channel - compare with Context Manager and update.
1080
- * Uses queryMessages() to enumerate the channel's rows, a held write's among
1081
- * them, for proper edit/delete detection.
1063
+ * Uses queryMessages() to enumerate the channel's stored rows, for proper
1064
+ * edit/delete detection.
1082
1065
  *
1083
1066
  * The live path can store, edit or remove a row whenever the sync awaits.
1084
1067
  * So the sync fetches history and runs the live conversion it needs first,
@@ -1232,8 +1215,8 @@ export class DiscordModule implements Module {
1232
1215
  * edit replaces a row's own text and keeps what the host added beside it.
1233
1216
  * Each id's rows are in store order. A message stored twice holds two,
1234
1217
  * of which a sync acts on the first, as the live path does (storedRow),
1235
- * and a delete takes both. A write the framework holds is a row too
1236
- * (storedRows). */
1218
+ * and a delete takes both. A row the agent hasn't been shown yet (`held`)
1219
+ * is a stored row as any other (storedRows). */
1237
1220
  private storedByDiscordId(ctx: ModuleContext, channelId: string): Map<string, StoredMessage[]> {
1238
1221
  const rows = new Map<string, StoredMessage[]>();
1239
1222
  for (const msg of ctx.queryMessages({ source: 'discord', metadata: { channelId } }).messages) {
@@ -1247,8 +1230,12 @@ export class DiscordModule implements Module {
1247
1230
  }
1248
1231
 
1249
1232
  /** The stored rows of a Discord message, by its id in the row's metadata,
1250
- * in store order. A write the framework holds comes after them as a held
1251
- * row, which an edit or removal reaches as it does a stored one. The
1233
+ * in store order. Every write is stored (#291), but for one a surgery's
1234
+ * hold or a context manager refusing its work keeps in memory, which a
1235
+ * restart loses: a row the agent's window doesn't hold yet reads `held`
1236
+ * (one a tune-out holds reads `heldFromResidents`, whatever its window),
1237
+ * in its place, and an edit or a removal
1238
+ * reaches it as any other, and every window that holds it. The
1252
1239
  * module context's findMessageByExternalId holds only rows the module
1253
1240
  * stored through its context, not those a process response added, so it
1254
1241
  * misses what the live path stored. */
@@ -1256,8 +1243,8 @@ export class DiscordModule implements Module {
1256
1243
  return this.ctx?.queryMessages({ source: 'discord', externalIds: [discordMessageId] }).messages ?? [];
1257
1244
  }
1258
1245
 
1259
- /** The row an edit or a create acts on: a message's first row, a stored
1260
- * one before a held one (storedRows). */
1246
+ /** The row an edit or a create acts on: a message's first row in store
1247
+ * order (storedRows). */
1261
1248
  private storedRow(discordMessageId: string): StoredMessage | undefined {
1262
1249
  return this.storedRows(discordMessageId)[0];
1263
1250
  }
@@ -1482,13 +1469,174 @@ export class DiscordModule implements Module {
1482
1469
  });
1483
1470
  }
1484
1471
 
1472
+ private answerEdit(edit: DiscordEditEvent): EventResponse {
1473
+ // The message's row, not only its id: an edit changes the message's own
1474
+ // text and keeps what the host added beside it, but for the blocks of
1475
+ // attachments the edit took away, when the client passed the edit's
1476
+ // attachments and the row is text-first.
1477
+ // An edit that comes while the live path converts the message's create
1478
+ // is folded into that create (handleDiscordMessageEdit). One that comes
1479
+ // once onProcess answered the create and finds no row is kept on its
1480
+ // entry (LiveCreate.unapplied), unless a delete is kept there already,
1481
+ // and so is one that comes while edits kept there wait for a sweep that
1482
+ // finds the row (sweptRow threw, or the row came after the sweep): it
1483
+ // goes behind them, and applyUnapplied applies them in order, so an
1484
+ // older kept edit never lands over it. Any other edit
1485
+ // to a message with no row is dropped: a history sync
1486
+ // running now stores the text it fetched, and the next sync's text rule
1487
+ // changes it. A row stored in shards can't be edited one shard at a
1488
+ // time, so it keeps its text, and the host's log says so. Only an edit
1489
+ // that writes marks the message live, so an update that changes nothing
1490
+ // leaves a running sync free to rewrite the row.
1491
+ const kept = this.creating.get(edit.discordMessageId);
1492
+ const keep = () => {
1493
+ if (kept?.phase === 'writing' && kept.unapplied !== 'delete') {
1494
+ kept.unapplied = [...(kept.unapplied ?? []), { text: edit.newContent, ...(edit.attachments ? { attachments: edit.attachments } : {}) }];
1495
+ }
1496
+ return {};
1497
+ };
1498
+ if (kept?.phase === 'writing' && Array.isArray(kept.unapplied)) return keep();
1499
+ const stored = this.storedRow(edit.discordMessageId);
1500
+ if (!stored) return keep();
1501
+ if (stored.bodyGroupId) {
1502
+ console.warn(`Discord: An update to message ${edit.discordMessageId} wasn't applied: it's stored in shards, which can't be edited, so if its text changed, the agent still has the earlier text`);
1503
+ return {};
1504
+ }
1505
+ const texted = contentForText(stored, edit.newContent);
1506
+ const content = edit.attachments && stored.metadata?.[TEXT_FIRST] === true
1507
+ ? withoutGoneAttachments(texted ?? stored.content, edit.attachments)
1508
+ : texted;
1509
+ if (!content || content === stored.content) return {};
1510
+ this.noteLive(edit.discordMessageId);
1511
+ return { editMessages: [{ messageId: stored.id, content }] };
1512
+ }
1513
+
1514
+ private answerDelete(del: DiscordDeleteEvent): EventResponse {
1515
+ // Every row of the message goes, so one stored twice doesn't outlive
1516
+ // its delete as a copy. A row stored in shards is removed whole: the
1517
+ // framework takes out the body a shard belongs to. The message is the
1518
+ // live path's even when no row is found, so a running sync doesn't
1519
+ // store what its fetch may still hold, and that ends its entry, but for
1520
+ // an answered create with no row, whose entry keeps the delete its
1521
+ // handler recorded (LiveCreate.unapplied) for the release.
1522
+ this.noteLive(del.discordMessageId);
1523
+ const rows = this.storedRows(del.discordMessageId);
1524
+ if (rows.length === 0 && this.creating.get(del.discordMessageId)?.phase === 'writing') return {};
1525
+ this.creating.delete(del.discordMessageId);
1526
+ if (rows.length === 0) {
1527
+ return {};
1528
+ }
1529
+ return {
1530
+ removeMessages: oncePerBody(rows),
1531
+ };
1532
+ }
1533
+
1534
+ /**
1535
+ * A write the release of a surgery's hold couldn't store (`message:lost`):
1536
+ * an answered create of the message (`writing`) with no row then ends,
1537
+ * with the edits or delete kept for it (LiveCreate.unapplied), since no
1538
+ * row comes for them to reach. Its delete, a history sync and a live
1539
+ * create of the message are no longer passed over. A definite loss
1540
+ * stored none of it, so it ends with no read. One whose outcome is
1541
+ * unknown (`mayBeStored`) is looked for: where its row landed, no
1542
+ * `message:added` came for it, so what was kept for it is applied now,
1543
+ * and the entry waits, as any answered create, for the next Discord event
1544
+ * to find the row. Where that lookup throws, the entry is flagged
1545
+ * (`lostUnread`), and the next event's sweep that reads the store ends it
1546
+ * where it finds no row.
1547
+ */
1548
+ private endLostCreate(discordMessageId: string, mayBeStored: boolean): void {
1549
+ if (this.creating.get(discordMessageId)?.phase !== 'writing') return;
1550
+ if (mayBeStored) {
1551
+ let landed: boolean;
1552
+ try {
1553
+ landed = this.storedRow(discordMessageId) !== undefined;
1554
+ } catch (error) {
1555
+ console.error(`Discord: whether message ${discordMessageId}'s lost create left a row couldn't be read, so its record waits for the next event to find its row:`, error);
1556
+ const live = this.creating.get(discordMessageId);
1557
+ if (live) live.lostUnread = true;
1558
+ return;
1559
+ }
1560
+ if (landed) {
1561
+ this.applyUnapplied();
1562
+ return;
1563
+ }
1564
+ }
1565
+ this.creating.delete(discordMessageId);
1566
+ }
1567
+
1568
+ /**
1569
+ * The row of an answered create, looked for at an event's sweep, or
1570
+ * undefined where the lookup throws, which is logged: that entry waits
1571
+ * for the next event, and the others are swept all the same.
1572
+ */
1573
+ private sweptRow(discordMessageId: string): { row: StoredMessage | undefined } | undefined {
1574
+ try {
1575
+ return { row: this.storedRow(discordMessageId) };
1576
+ } catch (error) {
1577
+ console.error(`Discord: whether message ${discordMessageId}'s create left a row couldn't be read, so its record waits for the next event:`, error);
1578
+ return undefined;
1579
+ }
1580
+ }
1581
+
1582
+ /**
1583
+ * Ends each answered create (`writing`) whose row a query shows now
1584
+ * (sweptRow), but one still holding what was kept for it, which
1585
+ * applyUnapplied couldn't apply (its lookup threw): it waits for the
1586
+ * next event, so what was kept isn't dropped with it. A lost create
1587
+ * whose release's lookup threw (`lostUnread`) ends where the lookup now
1588
+ * reads no row: no row comes for it.
1589
+ */
1590
+ private endShownCreates(): void {
1591
+ for (const [id, live] of this.creating) {
1592
+ if (live.phase !== 'writing') continue;
1593
+ const swept = this.sweptRow(id);
1594
+ if (swept === undefined) continue;
1595
+ if (swept.row ? live.unapplied === undefined : live.lostUnread) this.creating.delete(id);
1596
+ }
1597
+ }
1598
+
1599
+ private applyUnapplied(): void {
1600
+ const ctx = this.ctx;
1601
+ if (!ctx) return;
1602
+ for (const [discordMessageId, live] of this.creating) {
1603
+ const unapplied = live.unapplied;
1604
+ if (unapplied === undefined || !this.sweptRow(discordMessageId)?.row) continue;
1605
+ live.unapplied = undefined;
1606
+ const apply = (what: string, step: () => void) => {
1607
+ try {
1608
+ step();
1609
+ } catch (error) {
1610
+ console.error(`Discord: ${what} of message ${discordMessageId}, made while its create was kept in memory, threw as the release applied it, and may not have been applied:`, error);
1611
+ }
1612
+ };
1613
+ if (unapplied === 'delete') {
1614
+ apply('the delete', () => {
1615
+ for (const id of this.answerDelete({ type: 'discord:delete', discordMessageId }).removeMessages ?? []) ctx.removeMessage(id);
1616
+ });
1617
+ continue;
1618
+ }
1619
+ for (const { text, attachments } of unapplied) {
1620
+ apply('an edit', () => {
1621
+ const answer = this.answerEdit({ type: 'discord:edit', discordMessageId, newContent: text, ...(attachments ? { attachments } : {}) });
1622
+ for (const { messageId, content } of answer.editMessages ?? []) ctx.editMessage(messageId, content);
1623
+ });
1624
+ }
1625
+ }
1626
+ }
1627
+
1485
1628
  private handleDiscordMessageDelete(messageId: string): void {
1486
1629
  if (!this.ctx) return;
1487
1630
 
1488
- // A delete before the message's create is handled drops that create.
1489
- // Its event still goes, for a row a history sync stored first.
1631
+ // A delete before the message's create is handled drops that create,
1632
+ // and one after it was answered is kept on its entry for the release
1633
+ // that stores it (LiveCreate.unapplied). Its event still goes, for a row
1634
+ // a history sync stored first.
1490
1635
  const live = this.creating.get(messageId);
1491
- if (live) live.deleted = true;
1636
+ if (live) {
1637
+ live.deleted = true;
1638
+ if (live.phase === 'writing') live.unapplied = 'delete';
1639
+ }
1492
1640
 
1493
1641
  // Only queue the event - state writes happen in onProcess()
1494
1642
  this.ctx.pushEvent({