@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
@@ -317,18 +317,24 @@ export class DiscordModule {
317
317
  liveDuringSync = new Set();
318
318
  // The Discord messages whose live create is on its way to the store: from
319
319
  // the moment the live path takes one up, through its conversion and the
320
- // event queue, until a query shows the row onProcess answered with, stored
321
- // or held. The framework holds a module's writes while the agent's turn is
322
- // alive or the host is quiesced, and queryMessages shows a held write's
323
- // row, marked held, so the next Discord event finds it either way and ends
324
- // its entry. A history sync stores none of these messages, so no message is
320
+ // event queue, until a query shows the row onProcess answered with. The
321
+ // framework stores a module's write at once, whatever the agent's turn or
322
+ // the host's mode (#291); only a live operator surgery's hold, or a context
323
+ // manager refusing its work, keeps it in memory, which a restart loses,
324
+ // until the release or the branch move that re-initializes the manager
325
+ // stores it. So the next Discord event once it is stored
326
+ // finds its row, marked held while the agent's window doesn't hold it
327
+ // (asModuleRow), and ends its entry. A history sync stores none of these messages, so no message is
325
328
  // stored twice. A delete before the create is handled drops it: nothing is
326
329
  // stored and nobody is woken. Any message's entry ends when its delete is
327
330
  // handled, since a sync running then skips the message (noteLive) and a
328
- // later one doesn't fetch it. An edit while it converts is folded into it.
329
- // A create whose answer the framework fails to write or hold shows no row,
330
- // and keeps its entry, and the sync off its message, until its delete is
331
- // handled or the module stops.
331
+ // later one doesn't fetch it, but for an answered create with no row yet
332
+ // (`writing`), whose entry keeps its delete for the release that stores
333
+ // it (LiveCreate.unapplied). An edit while it converts is folded into it.
334
+ // A create whose answer the framework fails to write shows no row,
335
+ // and keeps its entry, and the sync off its message, until the module
336
+ // stops, but for one a surgery's hold kept, whose release, losing it,
337
+ // ends its entry (`message:lost`, endLostCreate).
332
338
  creating = new Map();
333
339
  // Typing keepalive: agentName -> { interval, deadline }. Started on
334
340
  // inference:started, stopped on the turn's terminal trace event. Discord's
@@ -357,19 +363,31 @@ export class DiscordModule {
357
363
  // inference and its turn ending, so it stays alive for the whole turn
358
364
  // (including tool-call cycles) and stops cleanly whether the agent
359
365
  // responds, errors, or decides to stay silent.
360
- if (this.config.autoTyping) {
361
- this.unsubscribeTrace = ctx.onTrace((event) => {
362
- const agentName = event.agentName;
363
- if (!agentName)
364
- return;
365
- if (event.type === 'inference:started' && !event.silent) {
366
- this.startTypingLoop(agentName);
367
- }
368
- else if (TURN_TERMINAL_TRACES.has(event.type)) {
369
- this.stopTypingLoop(agentName);
370
- }
371
- });
372
- }
366
+ this.unsubscribeTrace = ctx.onTrace((event) => {
367
+ // The edits and deletes kept for a held create are applied, or its
368
+ // record ended, on the trace of the surgery's release that stores or
369
+ // loses it (applyUnapplied, endLostCreate): the exception the README
370
+ // names to traces being observability-only.
371
+ if (event.type === 'message:added' && event.source === 'surgery-release') {
372
+ this.applyUnapplied();
373
+ return;
374
+ }
375
+ if (event.type === 'message:lost' && event.external?.source === 'discord') {
376
+ this.endLostCreate(event.external.id, event.mayBeStored === true);
377
+ return;
378
+ }
379
+ if (!this.config.autoTyping)
380
+ return;
381
+ const agentName = event.agentName;
382
+ if (!agentName)
383
+ return;
384
+ if (event.type === 'inference:started' && !event.silent) {
385
+ this.startTypingLoop(agentName);
386
+ }
387
+ else if (TURN_TERMINAL_TRACES.has(event.type)) {
388
+ this.stopTypingLoop(agentName);
389
+ }
390
+ });
373
391
  // Set up Discord event handlers
374
392
  this.client.onMessage((message) => this.handleDiscordMessage(message));
375
393
  this.client.onMessageEdit((id, content, attachments) => this.handleDiscordMessageEdit(id, content, attachments));
@@ -668,12 +686,13 @@ export class DiscordModule {
668
686
  // A stopped module answers nothing (stop()).
669
687
  if (!this.ctx)
670
688
  return {};
671
- // Each Discord event ends the entries whose row a query shows now, stored
672
- // or held.
689
+ // Each Discord event ends the entries whose row a query shows now,
690
+ // having applied what was kept for them first (applyUnapplied): an
691
+ // entry kept where its lost create's read threw (endLostCreate) still
692
+ // holds the edits or the delete made while its create was held.
673
693
  if (event.type.startsWith('discord:')) {
674
- for (const [id, live] of this.creating)
675
- if (live.phase === 'writing' && this.storedRow(id))
676
- this.creating.delete(id);
694
+ this.applyUnapplied();
695
+ this.endShownCreates();
677
696
  }
678
697
  // Handle new Discord message
679
698
  if (event.type === 'discord:message') {
@@ -725,7 +744,7 @@ export class DiscordModule {
725
744
  // when an event carries no blocks; text first, as TEXT_FIRST marks.
726
745
  const contentBlocks = msg.contentBlocks ?? textFirst(storedOwnText(msg.content));
727
746
  this.noteLive(msg.discordMessageId);
728
- // A message a history sync wrote first, stored or held, keeps that row:
747
+ // A message a history sync wrote first keeps that row:
729
748
  // the create adds none, and still sets the reply context and asks for
730
749
  // inference, since the message arrived live. Either way the entry waits
731
750
  // until a Discord event finds the message's row.
@@ -754,54 +773,11 @@ export class DiscordModule {
754
773
  };
755
774
  }
756
775
  // Handle Discord message edit
757
- if (event.type === 'discord:edit') {
758
- const edit = event;
759
- // The message's row, not only its id: an edit changes the message's own
760
- // text and keeps what the host added beside it, but for the blocks of
761
- // attachments the edit took away, when the client passed the edit's
762
- // attachments and the row is text-first.
763
- // An edit that comes while the live path converts the message's create
764
- // is folded into that create (handleDiscordMessageEdit). Any other edit
765
- // to a message with no row, stored or held, is dropped: a history sync
766
- // running now stores the text it fetched, and the next sync's text rule
767
- // changes it. A row stored in shards can't be edited one shard at a
768
- // time, so it keeps its text, and the host's log says so. Only an edit
769
- // that writes marks the message live, so an update that changes nothing
770
- // leaves a running sync free to rewrite the row.
771
- const stored = this.storedRow(edit.discordMessageId);
772
- if (!stored)
773
- return {};
774
- if (stored.bodyGroupId) {
775
- 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`);
776
- return {};
777
- }
778
- const texted = contentForText(stored, edit.newContent);
779
- const content = edit.attachments && stored.metadata?.[TEXT_FIRST] === true
780
- ? withoutGoneAttachments(texted ?? stored.content, edit.attachments)
781
- : texted;
782
- if (!content || content === stored.content)
783
- return {};
784
- this.noteLive(edit.discordMessageId);
785
- return { editMessages: [{ messageId: stored.id, content }] };
786
- }
776
+ if (event.type === 'discord:edit')
777
+ return this.answerEdit(event);
787
778
  // Handle Discord message delete
788
- if (event.type === 'discord:delete') {
789
- const del = event;
790
- // Every row of the message goes, so one stored twice doesn't outlive
791
- // its delete as a copy. A row stored in shards is removed whole: the
792
- // framework takes out the body a shard belongs to. The message is the
793
- // live path's even when no row is found, so a running sync doesn't
794
- // store what its fetch may still hold, and that ends its entry.
795
- this.noteLive(del.discordMessageId);
796
- this.creating.delete(del.discordMessageId);
797
- const rows = this.storedRows(del.discordMessageId);
798
- if (rows.length === 0) {
799
- return {};
800
- }
801
- return {
802
- removeMessages: oncePerBody(rows),
803
- };
804
- }
779
+ if (event.type === 'discord:delete')
780
+ return this.answerDelete(event);
805
781
  return {};
806
782
  }
807
783
  /**
@@ -934,8 +910,8 @@ export class DiscordModule {
934
910
  }
935
911
  /**
936
912
  * Sync history for a specific channel - compare with Context Manager and update.
937
- * Uses queryMessages() to enumerate the channel's rows, a held write's among
938
- * them, for proper edit/delete detection.
913
+ * Uses queryMessages() to enumerate the channel's stored rows, for proper
914
+ * edit/delete detection.
939
915
  *
940
916
  * The live path can store, edit or remove a row whenever the sync awaits.
941
917
  * So the sync fetches history and runs the live conversion it needs first,
@@ -1078,8 +1054,8 @@ export class DiscordModule {
1078
1054
  * edit replaces a row's own text and keeps what the host added beside it.
1079
1055
  * Each id's rows are in store order. A message stored twice holds two,
1080
1056
  * of which a sync acts on the first, as the live path does (storedRow),
1081
- * and a delete takes both. A write the framework holds is a row too
1082
- * (storedRows). */
1057
+ * and a delete takes both. A row the agent hasn't been shown yet (`held`)
1058
+ * is a stored row as any other (storedRows). */
1083
1059
  storedByDiscordId(ctx, channelId) {
1084
1060
  const rows = new Map();
1085
1061
  for (const msg of ctx.queryMessages({ source: 'discord', metadata: { channelId } }).messages) {
@@ -1095,16 +1071,20 @@ export class DiscordModule {
1095
1071
  return rows;
1096
1072
  }
1097
1073
  /** The stored rows of a Discord message, by its id in the row's metadata,
1098
- * in store order. A write the framework holds comes after them as a held
1099
- * row, which an edit or removal reaches as it does a stored one. The
1074
+ * in store order. Every write is stored (#291), but for one a surgery's
1075
+ * hold or a context manager refusing its work keeps in memory, which a
1076
+ * restart loses: a row the agent's window doesn't hold yet reads `held`
1077
+ * (one a tune-out holds reads `heldFromResidents`, whatever its window),
1078
+ * in its place, and an edit or a removal
1079
+ * reaches it as any other, and every window that holds it. The
1100
1080
  * module context's findMessageByExternalId holds only rows the module
1101
1081
  * stored through its context, not those a process response added, so it
1102
1082
  * misses what the live path stored. */
1103
1083
  storedRows(discordMessageId) {
1104
1084
  return this.ctx?.queryMessages({ source: 'discord', externalIds: [discordMessageId] }).messages ?? [];
1105
1085
  }
1106
- /** The row an edit or a create acts on: a message's first row, a stored
1107
- * one before a held one (storedRows). */
1086
+ /** The row an edit or a create acts on: a message's first row in store
1087
+ * order (storedRows). */
1108
1088
  storedRow(discordMessageId) {
1109
1089
  return this.storedRows(discordMessageId)[0];
1110
1090
  }
@@ -1308,14 +1288,185 @@ export class DiscordModule {
1308
1288
  ...(attachments ? { attachments } : {}),
1309
1289
  });
1310
1290
  }
1291
+ answerEdit(edit) {
1292
+ // The message's row, not only its id: an edit changes the message's own
1293
+ // text and keeps what the host added beside it, but for the blocks of
1294
+ // attachments the edit took away, when the client passed the edit's
1295
+ // attachments and the row is text-first.
1296
+ // An edit that comes while the live path converts the message's create
1297
+ // is folded into that create (handleDiscordMessageEdit). One that comes
1298
+ // once onProcess answered the create and finds no row is kept on its
1299
+ // entry (LiveCreate.unapplied), unless a delete is kept there already,
1300
+ // and so is one that comes while edits kept there wait for a sweep that
1301
+ // finds the row (sweptRow threw, or the row came after the sweep): it
1302
+ // goes behind them, and applyUnapplied applies them in order, so an
1303
+ // older kept edit never lands over it. Any other edit
1304
+ // to a message with no row is dropped: a history sync
1305
+ // running now stores the text it fetched, and the next sync's text rule
1306
+ // changes it. A row stored in shards can't be edited one shard at a
1307
+ // time, so it keeps its text, and the host's log says so. Only an edit
1308
+ // that writes marks the message live, so an update that changes nothing
1309
+ // leaves a running sync free to rewrite the row.
1310
+ const kept = this.creating.get(edit.discordMessageId);
1311
+ const keep = () => {
1312
+ if (kept?.phase === 'writing' && kept.unapplied !== 'delete') {
1313
+ kept.unapplied = [...(kept.unapplied ?? []), { text: edit.newContent, ...(edit.attachments ? { attachments: edit.attachments } : {}) }];
1314
+ }
1315
+ return {};
1316
+ };
1317
+ if (kept?.phase === 'writing' && Array.isArray(kept.unapplied))
1318
+ return keep();
1319
+ const stored = this.storedRow(edit.discordMessageId);
1320
+ if (!stored)
1321
+ return keep();
1322
+ if (stored.bodyGroupId) {
1323
+ 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`);
1324
+ return {};
1325
+ }
1326
+ const texted = contentForText(stored, edit.newContent);
1327
+ const content = edit.attachments && stored.metadata?.[TEXT_FIRST] === true
1328
+ ? withoutGoneAttachments(texted ?? stored.content, edit.attachments)
1329
+ : texted;
1330
+ if (!content || content === stored.content)
1331
+ return {};
1332
+ this.noteLive(edit.discordMessageId);
1333
+ return { editMessages: [{ messageId: stored.id, content }] };
1334
+ }
1335
+ answerDelete(del) {
1336
+ // Every row of the message goes, so one stored twice doesn't outlive
1337
+ // its delete as a copy. A row stored in shards is removed whole: the
1338
+ // framework takes out the body a shard belongs to. The message is the
1339
+ // live path's even when no row is found, so a running sync doesn't
1340
+ // store what its fetch may still hold, and that ends its entry, but for
1341
+ // an answered create with no row, whose entry keeps the delete its
1342
+ // handler recorded (LiveCreate.unapplied) for the release.
1343
+ this.noteLive(del.discordMessageId);
1344
+ const rows = this.storedRows(del.discordMessageId);
1345
+ if (rows.length === 0 && this.creating.get(del.discordMessageId)?.phase === 'writing')
1346
+ return {};
1347
+ this.creating.delete(del.discordMessageId);
1348
+ if (rows.length === 0) {
1349
+ return {};
1350
+ }
1351
+ return {
1352
+ removeMessages: oncePerBody(rows),
1353
+ };
1354
+ }
1355
+ /**
1356
+ * A write the release of a surgery's hold couldn't store (`message:lost`):
1357
+ * an answered create of the message (`writing`) with no row then ends,
1358
+ * with the edits or delete kept for it (LiveCreate.unapplied), since no
1359
+ * row comes for them to reach. Its delete, a history sync and a live
1360
+ * create of the message are no longer passed over. A definite loss
1361
+ * stored none of it, so it ends with no read. One whose outcome is
1362
+ * unknown (`mayBeStored`) is looked for: where its row landed, no
1363
+ * `message:added` came for it, so what was kept for it is applied now,
1364
+ * and the entry waits, as any answered create, for the next Discord event
1365
+ * to find the row. Where that lookup throws, the entry is flagged
1366
+ * (`lostUnread`), and the next event's sweep that reads the store ends it
1367
+ * where it finds no row.
1368
+ */
1369
+ endLostCreate(discordMessageId, mayBeStored) {
1370
+ if (this.creating.get(discordMessageId)?.phase !== 'writing')
1371
+ return;
1372
+ if (mayBeStored) {
1373
+ let landed;
1374
+ try {
1375
+ landed = this.storedRow(discordMessageId) !== undefined;
1376
+ }
1377
+ catch (error) {
1378
+ 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);
1379
+ const live = this.creating.get(discordMessageId);
1380
+ if (live)
1381
+ live.lostUnread = true;
1382
+ return;
1383
+ }
1384
+ if (landed) {
1385
+ this.applyUnapplied();
1386
+ return;
1387
+ }
1388
+ }
1389
+ this.creating.delete(discordMessageId);
1390
+ }
1391
+ /**
1392
+ * The row of an answered create, looked for at an event's sweep, or
1393
+ * undefined where the lookup throws, which is logged: that entry waits
1394
+ * for the next event, and the others are swept all the same.
1395
+ */
1396
+ sweptRow(discordMessageId) {
1397
+ try {
1398
+ return { row: this.storedRow(discordMessageId) };
1399
+ }
1400
+ catch (error) {
1401
+ console.error(`Discord: whether message ${discordMessageId}'s create left a row couldn't be read, so its record waits for the next event:`, error);
1402
+ return undefined;
1403
+ }
1404
+ }
1405
+ /**
1406
+ * Ends each answered create (`writing`) whose row a query shows now
1407
+ * (sweptRow), but one still holding what was kept for it, which
1408
+ * applyUnapplied couldn't apply (its lookup threw): it waits for the
1409
+ * next event, so what was kept isn't dropped with it. A lost create
1410
+ * whose release's lookup threw (`lostUnread`) ends where the lookup now
1411
+ * reads no row: no row comes for it.
1412
+ */
1413
+ endShownCreates() {
1414
+ for (const [id, live] of this.creating) {
1415
+ if (live.phase !== 'writing')
1416
+ continue;
1417
+ const swept = this.sweptRow(id);
1418
+ if (swept === undefined)
1419
+ continue;
1420
+ if (swept.row ? live.unapplied === undefined : live.lostUnread)
1421
+ this.creating.delete(id);
1422
+ }
1423
+ }
1424
+ applyUnapplied() {
1425
+ const ctx = this.ctx;
1426
+ if (!ctx)
1427
+ return;
1428
+ for (const [discordMessageId, live] of this.creating) {
1429
+ const unapplied = live.unapplied;
1430
+ if (unapplied === undefined || !this.sweptRow(discordMessageId)?.row)
1431
+ continue;
1432
+ live.unapplied = undefined;
1433
+ const apply = (what, step) => {
1434
+ try {
1435
+ step();
1436
+ }
1437
+ catch (error) {
1438
+ 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);
1439
+ }
1440
+ };
1441
+ if (unapplied === 'delete') {
1442
+ apply('the delete', () => {
1443
+ for (const id of this.answerDelete({ type: 'discord:delete', discordMessageId }).removeMessages ?? [])
1444
+ ctx.removeMessage(id);
1445
+ });
1446
+ continue;
1447
+ }
1448
+ for (const { text, attachments } of unapplied) {
1449
+ apply('an edit', () => {
1450
+ const answer = this.answerEdit({ type: 'discord:edit', discordMessageId, newContent: text, ...(attachments ? { attachments } : {}) });
1451
+ for (const { messageId, content } of answer.editMessages ?? [])
1452
+ ctx.editMessage(messageId, content);
1453
+ });
1454
+ }
1455
+ }
1456
+ }
1311
1457
  handleDiscordMessageDelete(messageId) {
1312
1458
  if (!this.ctx)
1313
1459
  return;
1314
- // A delete before the message's create is handled drops that create.
1315
- // Its event still goes, for a row a history sync stored first.
1460
+ // A delete before the message's create is handled drops that create,
1461
+ // and one after it was answered is kept on its entry for the release
1462
+ // that stores it (LiveCreate.unapplied). Its event still goes, for a row
1463
+ // a history sync stored first.
1316
1464
  const live = this.creating.get(messageId);
1317
- if (live)
1465
+ if (live) {
1318
1466
  live.deleted = true;
1467
+ if (live.phase === 'writing')
1468
+ live.unapplied = 'delete';
1469
+ }
1319
1470
  // Only queue the event - state writes happen in onProcess()
1320
1471
  this.ctx.pushEvent({
1321
1472
  type: 'discord:delete',