@animalabs/agent-framework 0.26.2 → 0.28.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 (371) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent.d.ts +291 -23
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +596 -61
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +5 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +34 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +36 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/cut-off-round.d.ts +24 -0
  18. package/dist/src/cut-off-round.d.ts.map +1 -0
  19. package/dist/src/cut-off-round.js +87 -0
  20. package/dist/src/cut-off-round.js.map +1 -0
  21. package/dist/src/framework.d.ts +1038 -193
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +5967 -1514
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/gate/event-gate.d.ts +405 -46
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/event-gate.js +2646 -399
  28. package/dist/src/gate/event-gate.js.map +1 -1
  29. package/dist/src/gate/index.d.ts +3 -2
  30. package/dist/src/gate/index.d.ts.map +1 -1
  31. package/dist/src/gate/index.js +2 -1
  32. package/dist/src/gate/index.js.map +1 -1
  33. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  34. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  35. package/dist/src/gate/recipe-reconcile.js +325 -0
  36. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  37. package/dist/src/gate/reply-judge.d.ts +116 -0
  38. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  39. package/dist/src/gate/reply-judge.js +467 -0
  40. package/dist/src/gate/reply-judge.js.map +1 -0
  41. package/dist/src/gate/types.d.ts +229 -8
  42. package/dist/src/gate/types.d.ts.map +1 -1
  43. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  44. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  45. package/dist/src/heartbeat/heartbeat.js +752 -0
  46. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  47. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  48. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.js +107 -0
  50. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  51. package/dist/src/index.d.ts +10 -4
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +7 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  56. package/dist/src/mcpl/capability-mask.js +3 -2
  57. package/dist/src/mcpl/capability-mask.js.map +1 -1
  58. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  59. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/channel-registry.js +1154 -435
  61. package/dist/src/mcpl/channel-registry.js.map +1 -1
  62. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  63. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/conversation-router.js +29 -15
  65. package/dist/src/mcpl/conversation-router.js.map +1 -1
  66. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  67. package/dist/src/mcpl/convert-block.js +14 -2
  68. package/dist/src/mcpl/convert-block.js.map +1 -1
  69. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  70. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  71. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  72. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  73. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  74. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  75. package/dist/src/mcpl/inbound-source.js +40 -69
  76. package/dist/src/mcpl/inbound-source.js.map +1 -1
  77. package/dist/src/mcpl/index.d.ts +5 -1
  78. package/dist/src/mcpl/index.d.ts.map +1 -1
  79. package/dist/src/mcpl/index.js +4 -0
  80. package/dist/src/mcpl/index.js.map +1 -1
  81. package/dist/src/mcpl/inference-router.d.ts +14 -2
  82. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  83. package/dist/src/mcpl/inference-router.js +73 -28
  84. package/dist/src/mcpl/inference-router.js.map +1 -1
  85. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  86. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  87. package/dist/src/mcpl/prose-grammar.js +6 -0
  88. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  89. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  90. package/dist/src/mcpl/protocol-family.js +3 -0
  91. package/dist/src/mcpl/protocol-family.js.map +1 -1
  92. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  93. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  94. package/dist/src/mcpl/push-coalescer.js +33 -11
  95. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  96. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  97. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  98. package/dist/src/mcpl/reference-fetcher.js +177 -145
  99. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  100. package/dist/src/mcpl/references.d.ts +380 -46
  101. package/dist/src/mcpl/references.d.ts.map +1 -1
  102. package/dist/src/mcpl/references.js +736 -77
  103. package/dist/src/mcpl/references.js.map +1 -1
  104. package/dist/src/mcpl/server-connection.d.ts +93 -4
  105. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  106. package/dist/src/mcpl/server-connection.js +176 -11
  107. package/dist/src/mcpl/server-connection.js.map +1 -1
  108. package/dist/src/mcpl/tags.d.ts +9 -2
  109. package/dist/src/mcpl/tags.d.ts.map +1 -1
  110. package/dist/src/mcpl/tags.js +12 -2
  111. package/dist/src/mcpl/tags.js.map +1 -1
  112. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  113. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  114. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  115. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  116. package/dist/src/mcpl/tool-presence.d.ts +311 -0
  117. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  118. package/dist/src/mcpl/tool-presence.js +697 -0
  119. package/dist/src/mcpl/tool-presence.js.map +1 -0
  120. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  121. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  123. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts +46 -0
  125. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  126. package/dist/src/mcpl/tool-signature.js +321 -0
  127. package/dist/src/mcpl/tool-signature.js.map +1 -0
  128. package/dist/src/mcpl/toolset-host.d.ts +251 -0
  129. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  130. package/dist/src/mcpl/toolset-host.js +720 -0
  131. package/dist/src/mcpl/toolset-host.js.map +1 -0
  132. package/dist/src/mcpl/toolsets-module.d.ts +83 -0
  133. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  134. package/dist/src/mcpl/toolsets-module.js +523 -0
  135. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  136. package/dist/src/mcpl/transport.d.ts +2 -2
  137. package/dist/src/mcpl/transport.d.ts.map +1 -1
  138. package/dist/src/mcpl/transport.js +3 -2
  139. package/dist/src/mcpl/transport.js.map +1 -1
  140. package/dist/src/mcpl/types.d.ts +24 -2
  141. package/dist/src/mcpl/types.d.ts.map +1 -1
  142. package/dist/src/mcpl/types.js.map +1 -1
  143. package/dist/src/mcpl/visible-content.d.ts +7 -9
  144. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  145. package/dist/src/mcpl/visible-content.js +21 -9
  146. package/dist/src/mcpl/visible-content.js.map +1 -1
  147. package/dist/src/module-registry.d.ts +27 -9
  148. package/dist/src/module-registry.d.ts.map +1 -1
  149. package/dist/src/module-registry.js +77 -17
  150. package/dist/src/module-registry.js.map +1 -1
  151. package/dist/src/modules/discord/index.js +1 -1
  152. package/dist/src/modules/discord/index.js.map +1 -1
  153. package/dist/src/modules/history/index.d.ts +2 -1
  154. package/dist/src/modules/history/index.d.ts.map +1 -1
  155. package/dist/src/modules/history/index.js +73 -28
  156. package/dist/src/modules/history/index.js.map +1 -1
  157. package/dist/src/modules/history/semantic.d.ts +108 -6
  158. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  159. package/dist/src/modules/history/semantic.js +273 -57
  160. package/dist/src/modules/history/semantic.js.map +1 -1
  161. package/dist/src/modules/workspace/answers.d.ts +22 -0
  162. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  163. package/dist/src/modules/workspace/answers.js +123 -0
  164. package/dist/src/modules/workspace/answers.js.map +1 -0
  165. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  166. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/branch-intent.js +6 -2
  168. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  169. package/dist/src/modules/workspace/direct-write.d.ts +184 -0
  170. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  171. package/dist/src/modules/workspace/direct-write.js +417 -0
  172. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  173. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  174. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  176. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  177. package/dist/src/modules/workspace/edit-text.d.ts +27 -12
  178. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/edit-text.js +61 -29
  180. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  181. package/dist/src/modules/workspace/effects.d.ts +10 -0
  182. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/effects.js +21 -14
  184. package/dist/src/modules/workspace/effects.js.map +1 -1
  185. package/dist/src/modules/workspace/index.d.ts +308 -78
  186. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  187. package/dist/src/modules/workspace/index.js +2103 -720
  188. package/dist/src/modules/workspace/index.js.map +1 -1
  189. package/dist/src/modules/workspace/observe.d.ts +75 -63
  190. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  191. package/dist/src/modules/workspace/observe.js +366 -180
  192. package/dist/src/modules/workspace/observe.js.map +1 -1
  193. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  194. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  195. package/dist/src/modules/workspace/read-page.js +219 -0
  196. package/dist/src/modules/workspace/read-page.js.map +1 -0
  197. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/reconcile.js +26 -16
  199. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  200. package/dist/src/modules/workspace/result-text.d.ts +68 -0
  201. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  202. package/dist/src/modules/workspace/result-text.js +94 -0
  203. package/dist/src/modules/workspace/result-text.js.map +1 -0
  204. package/dist/src/modules/workspace/sync.d.ts +9 -0
  205. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/sync.js +14 -0
  207. package/dist/src/modules/workspace/sync.js.map +1 -1
  208. package/dist/src/modules/workspace/text.d.ts +29 -12
  209. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/text.js +35 -33
  211. package/dist/src/modules/workspace/text.js.map +1 -1
  212. package/dist/src/modules/workspace/types.d.ts +137 -60
  213. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/types.js.map +1 -1
  215. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  216. package/dist/src/modules/workspace/watcher.js +8 -3
  217. package/dist/src/modules/workspace/watcher.js.map +1 -1
  218. package/dist/src/prose-drafts.d.ts +4 -3
  219. package/dist/src/prose-drafts.d.ts.map +1 -1
  220. package/dist/src/prose-drafts.js +3 -2
  221. package/dist/src/prose-drafts.js.map +1 -1
  222. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  223. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  224. package/dist/src/reasoning-loss-notice.js +50 -0
  225. package/dist/src/reasoning-loss-notice.js.map +1 -0
  226. package/dist/src/reasoning-loss.d.ts +24 -0
  227. package/dist/src/reasoning-loss.d.ts.map +1 -0
  228. package/dist/src/reasoning-loss.js +102 -0
  229. package/dist/src/reasoning-loss.js.map +1 -0
  230. package/dist/src/send-tools.d.ts +3 -2
  231. package/dist/src/send-tools.d.ts.map +1 -1
  232. package/dist/src/send-tools.js +3 -2
  233. package/dist/src/send-tools.js.map +1 -1
  234. package/dist/src/sent-rows.d.ts +59 -0
  235. package/dist/src/sent-rows.d.ts.map +1 -0
  236. package/dist/src/sent-rows.js +145 -0
  237. package/dist/src/sent-rows.js.map +1 -0
  238. package/dist/src/silent-heartbeat.d.ts +17 -10
  239. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  240. package/dist/src/silent-heartbeat.js.map +1 -1
  241. package/dist/src/stable-hash.d.ts +3 -0
  242. package/dist/src/stable-hash.d.ts.map +1 -0
  243. package/dist/src/stable-hash.js +15 -0
  244. package/dist/src/stable-hash.js.map +1 -0
  245. package/dist/src/stored-injections.d.ts +99 -0
  246. package/dist/src/stored-injections.d.ts.map +1 -0
  247. package/dist/src/stored-injections.js +106 -0
  248. package/dist/src/stored-injections.js.map +1 -0
  249. package/dist/src/text-as-stored.d.ts +4 -3
  250. package/dist/src/text-as-stored.d.ts.map +1 -1
  251. package/dist/src/text-as-stored.js +4 -3
  252. package/dist/src/text-as-stored.js.map +1 -1
  253. package/dist/src/thinking-drops.d.ts +74 -0
  254. package/dist/src/thinking-drops.d.ts.map +1 -0
  255. package/dist/src/thinking-drops.js +96 -0
  256. package/dist/src/thinking-drops.js.map +1 -0
  257. package/dist/src/tool-args.d.ts +12 -0
  258. package/dist/src/tool-args.d.ts.map +1 -1
  259. package/dist/src/tool-args.js +35 -1
  260. package/dist/src/tool-args.js.map +1 -1
  261. package/dist/src/tool-image-ledger.d.ts +9 -8
  262. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  263. package/dist/src/tool-image-ledger.js +9 -8
  264. package/dist/src/tool-image-ledger.js.map +1 -1
  265. package/dist/src/tool-presentation.d.ts.map +1 -1
  266. package/dist/src/tool-presentation.js +3 -1
  267. package/dist/src/tool-presentation.js.map +1 -1
  268. package/dist/src/tool-result-guard.d.ts +12 -6
  269. package/dist/src/tool-result-guard.d.ts.map +1 -1
  270. package/dist/src/tool-result-guard.js +44 -17
  271. package/dist/src/tool-result-guard.js.map +1 -1
  272. package/dist/src/tool-result-history.d.ts +176 -24
  273. package/dist/src/tool-result-history.d.ts.map +1 -1
  274. package/dist/src/tool-result-history.js +411 -91
  275. package/dist/src/tool-result-history.js.map +1 -1
  276. package/dist/src/tune-out/coordinator.d.ts +17 -9
  277. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  278. package/dist/src/tune-out/coordinator.js +84 -37
  279. package/dist/src/tune-out/coordinator.js.map +1 -1
  280. package/dist/src/tune-out/tools.d.ts +4 -1
  281. package/dist/src/tune-out/tools.d.ts.map +1 -1
  282. package/dist/src/types/agent.d.ts +217 -7
  283. package/dist/src/types/agent.d.ts.map +1 -1
  284. package/dist/src/types/events.d.ts +33 -9
  285. package/dist/src/types/events.d.ts.map +1 -1
  286. package/dist/src/types/framework.d.ts +39 -3
  287. package/dist/src/types/framework.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +95 -9
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +91 -3
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/package.json +5 -4
  293. package/src/agent.ts +661 -67
  294. package/src/api/server.ts +5 -4
  295. package/src/auto-continue.ts +43 -0
  296. package/src/code-execution/isolate/scripts.ts +11 -3
  297. package/src/cut-off-round.ts +98 -0
  298. package/src/framework.ts +6168 -1540
  299. package/src/gate/event-gate.ts +2895 -405
  300. package/src/gate/index.ts +10 -1
  301. package/src/gate/recipe-reconcile.ts +355 -0
  302. package/src/gate/reply-judge.ts +512 -0
  303. package/src/gate/types.ts +234 -9
  304. package/src/heartbeat/heartbeat.ts +804 -0
  305. package/src/heartbeat/reminder-time.ts +107 -0
  306. package/src/index.ts +11 -4
  307. package/src/mcpl/capability-mask.ts +3 -2
  308. package/src/mcpl/channel-registry.ts +1222 -468
  309. package/src/mcpl/conversation-router.ts +34 -16
  310. package/src/mcpl/convert-block.ts +14 -1
  311. package/src/mcpl/hook-orchestrator.ts +33 -6
  312. package/src/mcpl/inbound-source.ts +42 -69
  313. package/src/mcpl/index.ts +6 -1
  314. package/src/mcpl/inference-router.ts +79 -34
  315. package/src/mcpl/prose-grammar.ts +7 -0
  316. package/src/mcpl/protocol-family.ts +2 -0
  317. package/src/mcpl/push-coalescer.ts +51 -13
  318. package/src/mcpl/reference-fetcher.ts +201 -162
  319. package/src/mcpl/references.ts +851 -77
  320. package/src/mcpl/server-connection.ts +189 -11
  321. package/src/mcpl/tags.ts +13 -2
  322. package/src/mcpl/tool-lifecycle.ts +21 -0
  323. package/src/mcpl/tool-presence.ts +716 -0
  324. package/src/mcpl/tool-result-normalize.ts +18 -6
  325. package/src/mcpl/tool-signature.ts +316 -0
  326. package/src/mcpl/toolset-host.ts +727 -0
  327. package/src/mcpl/toolsets-module.ts +545 -0
  328. package/src/mcpl/transport.ts +3 -2
  329. package/src/mcpl/types.ts +25 -2
  330. package/src/mcpl/visible-content.ts +22 -9
  331. package/src/module-registry.ts +85 -26
  332. package/src/modules/discord/index.ts +1 -1
  333. package/src/modules/history/index.ts +70 -30
  334. package/src/modules/history/semantic.ts +308 -54
  335. package/src/modules/workspace/answers.ts +133 -0
  336. package/src/modules/workspace/branch-intent.ts +10 -5
  337. package/src/modules/workspace/direct-write.ts +460 -0
  338. package/src/modules/workspace/disk-agreement.ts +52 -21
  339. package/src/modules/workspace/edit-text.ts +84 -35
  340. package/src/modules/workspace/effects.ts +22 -13
  341. package/src/modules/workspace/index.ts +2142 -723
  342. package/src/modules/workspace/observe.ts +381 -177
  343. package/src/modules/workspace/read-page.ts +228 -0
  344. package/src/modules/workspace/reconcile.ts +27 -15
  345. package/src/modules/workspace/result-text.ts +121 -0
  346. package/src/modules/workspace/sync.ts +16 -0
  347. package/src/modules/workspace/text.ts +58 -32
  348. package/src/modules/workspace/types.ts +137 -60
  349. package/src/modules/workspace/watcher.ts +8 -3
  350. package/src/prose-drafts.ts +4 -3
  351. package/src/reasoning-loss-notice.ts +62 -0
  352. package/src/reasoning-loss.ts +120 -0
  353. package/src/send-tools.ts +3 -2
  354. package/src/sent-rows.ts +162 -0
  355. package/src/silent-heartbeat.ts +17 -10
  356. package/src/stable-hash.ts +14 -0
  357. package/src/stored-injections.ts +131 -0
  358. package/src/text-as-stored.ts +4 -3
  359. package/src/thinking-drops.ts +135 -0
  360. package/src/tool-args.ts +34 -1
  361. package/src/tool-image-ledger.ts +9 -8
  362. package/src/tool-presentation.ts +3 -1
  363. package/src/tool-result-guard.ts +45 -17
  364. package/src/tool-result-history.ts +471 -90
  365. package/src/tune-out/coordinator.ts +80 -37
  366. package/src/tune-out/tools.ts +4 -1
  367. package/src/types/agent.ts +220 -7
  368. package/src/types/events.ts +33 -9
  369. package/src/types/framework.ts +36 -3
  370. package/src/types/module.ts +91 -9
  371. package/src/types/trace.ts +94 -3
@@ -14,31 +14,44 @@
14
14
  * a stored message.
15
15
  */
16
16
 
17
+ import { classifyBlock, NON_STRING_TEXT_PLACEHOLDER } from './references.js';
18
+
17
19
  /**
18
20
  * True when `content` holds nothing the model would see: no non-text block,
19
21
  * and no text block whose text has a non-whitespace character. Mirrors the
20
22
  * request sanitizer, which drops text that is not a string or trims to ''.
21
- * Accepts MCPL wire blocks and converted membrane blocks alike.
23
+ * Accepts MCPL wire blocks and converted membrane blocks alike. A wire
24
+ * block is judged as the block the converter makes of it where that's text:
25
+ * MCP's embedded resource shows as the block its contents make
26
+ * (classifyBlock's `embedded`), so one whose contents are blank text holds
27
+ * nothing to see.
22
28
  */
23
29
  export function isVisiblyEmptyContent(content: readonly unknown[] | null | undefined): boolean {
24
30
  if (!Array.isArray(content)) return true;
25
31
  for (const block of content) {
26
32
  if (!block || typeof block !== 'object') continue;
27
33
  const b = block as { type?: unknown; text?: unknown };
34
+ const shown = b.type === 'resource' ? embeddedText(b) : undefined;
35
+ if (shown !== undefined) {
36
+ if (shown.trim() !== '') return false;
37
+ continue;
38
+ }
28
39
  if (b.type !== 'text') return false;
29
40
  if (typeof b.text === 'string' && b.text.trim() !== '') return false;
30
41
  }
31
42
  return true;
32
43
  }
33
44
 
34
- /**
35
- * What the converter (convertBlock, on every lane) shows for a text
36
- * block whose text isn't a string, which only a nonconformant sender writes,
37
- * so the agent learns that a block was there. It is the host's word, not the
38
- * sender's, so it never makes content visible (isVisiblyEmptyConversion).
39
- * The other blocks the converters can't show have theirs in references.ts.
40
- */
41
- export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
45
+ /** The text an embedded resource's contents make, or undefined for any other
46
+ * block, an embedded resource shown as anything but text among them. */
47
+ function embeddedText(block: unknown): string | undefined {
48
+ const c = classifyBlock(block);
49
+ return c.kind === 'embedded' && c.contents.kind === 'block' && c.contents.block.type === 'text' ? c.contents.block.text : undefined;
50
+ }
51
+
52
+ // What the converter shows for a text block whose text isn't a string,
53
+ // beside the converters' other markers in references.ts.
54
+ export { NON_STRING_TEXT_PLACEHOLDER };
42
55
 
43
56
  /** A wire text block whose text isn't a string, which the converter shows as
44
57
  * NON_STRING_TEXT_PLACEHOLDER. */
@@ -21,13 +21,14 @@ import type {
21
21
  ProcessEvent,
22
22
  TraceEventListener,
23
23
  ModuleMessage,
24
+ ModuleContextInjection,
24
25
  MessageQueryResult,
25
26
  } from './types/index.js';
26
27
  import { heldWriteId } from './held-writes.js';
27
28
  import type { Agent } from './agent.js';
28
29
  import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
29
30
  import { logValue } from './mcpl/log-value.js';
30
- import { toolArgsProblems } from './tool-args.js';
31
+ import { asCalled, markedInputRefusal, toolArgsProblems } from './tool-args.js';
31
32
 
32
33
  /** How the registry queues a module's event: its push names the module. A
33
34
  * `ProcessQueue` serves too, ignoring the name. */
@@ -110,11 +111,11 @@ export class ModuleRegistry {
110
111
  private store: JsStore;
111
112
  private queue: ModuleQueue;
112
113
  private getAgents: () => Agent[];
113
- private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
114
+ private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
114
115
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
115
116
  private removeMessageFn: (id: MessageId) => void;
116
117
  private getMessageFn: (id: MessageId) => ModuleMessage | null;
117
- private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
118
+ private queryMessagesFn: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
118
119
 
119
120
  private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
120
121
  private onTraceFn: (listener: TraceEventListener) => () => void;
@@ -134,11 +135,11 @@ export class ModuleRegistry {
134
135
  queue: ModuleQueue,
135
136
  options: {
136
137
  getAgents: () => Agent[];
137
- addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
138
+ addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
138
139
  editMessage: (id: MessageId, content: ContentBlock[]) => void;
139
140
  removeMessage: (id: MessageId) => void;
140
141
  getMessage: (id: MessageId) => ModuleMessage | null;
141
- queryMessages: (filter: MessageQuery) => MessageQueryResult;
142
+ queryMessages: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
142
143
  /** Called with the pushing module's name. */
143
144
  pushEvent: (event: ProcessEvent, moduleName?: string) => void;
144
145
  onTrace: (listener: TraceEventListener) => () => void;
@@ -230,7 +231,8 @@ export class ModuleRegistry {
230
231
 
231
232
  /**
232
233
  * Set state for a module by name.
233
- * Used by the framework to apply stateUpdate from EventResponse atomically.
234
+ * Used by the framework to apply stateUpdate from EventResponse after its
235
+ * message changes.
234
236
  */
235
237
  setModuleState(moduleName: string, state: unknown): void {
236
238
  const ctx = this.moduleContexts.get(moduleName);
@@ -315,11 +317,17 @@ export class ModuleRegistry {
315
317
  * A call whose input isn't an object, or lacks a key the tool's schema
316
318
  * requires, is answered before the module runs, with an error result that
317
319
  * opens "Nothing was done:" and names what to send (toolArgsProblems).
320
+ * So is a call membrane marked `unparseableInput`, unless its text is JSON
321
+ * `null`, which reaches the module as a call with no arguments
322
+ * (markedInputRefusal).
318
323
  * A caller that has already resolved the tool's definition, as the `utils`
319
324
  * tool has a utility's, passes it as `definition`, and it's checked against
320
325
  * that one.
321
326
  */
322
- async handleToolCall(call: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
327
+ async handleToolCall(given: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
328
+ const marked = markedInputRefusal(given);
329
+ if (marked !== undefined) return { success: false, error: marked, isError: true };
330
+ const call = asCalled(given);
323
331
  // Parse module--tool name
324
332
  const sepIndex = call.name.indexOf('--');
325
333
  if (sepIndex === -1) {
@@ -439,8 +447,18 @@ export class ModuleRegistry {
439
447
 
440
448
  /**
441
449
  * Gather context injections from all modules before inference.
442
- * Calls each module's gatherContext() in parallel with a per-module timeout.
443
- * Fail-open: timed-out or erroring modules are skipped (with a stderr line).
450
+ * Calls each module's gatherContext() in parallel with a per-module timeout,
451
+ * and returns their injections in the modules' registration order, whatever
452
+ * order they finish in. Fail-open: timed-out or erroring modules are skipped
453
+ * (with a stderr line), whether the call throws, rejects, or answers with
454
+ * something that isn't an array of objects, or with a `system` entry whose
455
+ * content isn't an array of objects, every block of which the agent's
456
+ * buildSystemPrompt reads, and one that answers after its timeout adds
457
+ * nothing. An array returned without a promise is taken as the module's
458
+ * answer. An entry's other fields are the module's to get right. Each
459
+ * module's budget starts once its call returns, after its own synchronous
460
+ * work and every earlier module's; a later module's synchronous work still
461
+ * counts against it.
444
462
  *
445
463
  * The per-module budget is `module.contextTimeoutMs` when declared, else the
446
464
  * `timeoutMs` default (15s). Modules whose gatherContext does real work —
@@ -450,12 +468,16 @@ export class ModuleRegistry {
450
468
  * to MAX_CONTEXT_TIMEOUT_MS so no single module can hold every turn hostage.
451
469
  * Adapted from Anarchid/agent-framework@mcpl-module-proto.
452
470
  */
453
- async gatherContext(agentName: string, timeoutMs = 15_000): Promise<ContextInjection[]> {
454
- const injections: ContextInjection[] = [];
471
+ async gatherContext(agentName: string, timeoutMs = 15_000): Promise<ModuleContextInjection[]> {
472
+ // Each module's result in registration order, whatever order they finish
473
+ // in: a stored injection's content hash follows that order, and a module
474
+ // that answers after its timeout adds nothing (it's skipped, fail-open).
475
+ const results: ModuleContextInjection[][] = [];
455
476
  const promises: Promise<void>[] = [];
456
477
 
457
478
  for (const module of this.modules.values()) {
458
- if (!module.gatherContext) continue;
479
+ const gather = module.gatherContext;
480
+ if (!gather) continue;
459
481
 
460
482
  // Clamp the per-module budget. A module declaring `contextTimeoutMs`
461
483
  // legitimately needs more than the shared default (e.g. a retrieval
@@ -467,14 +489,43 @@ export class ModuleRegistry {
467
489
  MAX_CONTEXT_TIMEOUT_MS,
468
490
  );
469
491
  let timer: ReturnType<typeof setTimeout> | undefined;
492
+ let timedOut = false;
493
+ const slot = results.length;
494
+ results.push([]);
495
+ // Called here, before its timer is set, so its budget starts after its
496
+ // synchronous work; a call that throws is skipped as a rejection is.
497
+ let call: Promise<ModuleContextInjection[]>;
498
+ try {
499
+ call = Promise.resolve(gather.call(module, agentName));
500
+ } catch (err) {
501
+ call = Promise.reject(err);
502
+ }
470
503
  const promise = Promise.race([
471
- module.gatherContext(agentName).then(r => {
472
- injections.push(...r);
504
+ call.then(r => {
505
+ if (timedOut) return;
506
+ // Checked before it's kept, so an answer that isn't an array of
507
+ // objects skips the module, as does a system entry whose content
508
+ // isn't an array of objects: buildSystemPrompt reads every block
509
+ // of it, and one it can't read would fail the turn. Each array is
510
+ // read through Array.from, which reads a hole as `undefined`, as
511
+ // context-manager's compile does when it spreads the content.
512
+ if (!Array.isArray(r) || Array.from(r).some((entry) => entry === null || typeof entry !== 'object')) {
513
+ throw new TypeError(`gatherContext answered with ${Array.isArray(r) ? 'an array holding an entry that is not an object' : r === null ? 'null' : typeof r}, not an array of objects`);
514
+ }
515
+ const unreadable = r.find((entry) => entry.position === 'system'
516
+ && !(Array.isArray(entry.content) && Array.from(entry.content).every((block) => block !== null && typeof block === 'object')));
517
+ if (unreadable) {
518
+ throw new TypeError(`gatherContext answered with a system injection in namespace ${logValue(unreadable.namespace)} whose content isn't an array of objects`);
519
+ }
473
520
  this.noteBlocksNotShown(module.name, r);
521
+ results[slot] = [...r];
474
522
  }),
475
523
  new Promise<void>((_, rej) => {
476
524
  timer = setTimeout(
477
- () => rej(new Error(`gatherContext timed out after ${budgetMs}ms`)),
525
+ () => {
526
+ timedOut = true;
527
+ rej(new Error(`gatherContext timed out after ${budgetMs}ms`));
528
+ },
478
529
  budgetMs,
479
530
  );
480
531
  }),
@@ -486,7 +537,7 @@ export class ModuleRegistry {
486
537
  }
487
538
 
488
539
  await Promise.all(promises);
489
- return injections;
540
+ return results.flat();
490
541
  }
491
542
 
492
543
  /**
@@ -607,11 +658,11 @@ class ModuleContextImpl implements ModuleContext {
607
658
  private registry: ModuleRegistry;
608
659
  readonly isRestart: boolean;
609
660
  private getAgentsFn: () => Agent[];
610
- private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
661
+ private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
611
662
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
612
663
  private removeMessageFn: (id: MessageId) => void;
613
664
  private getMessageFn: (id: MessageId) => ModuleMessage | null;
614
- private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
665
+ private queryMessagesFn: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
615
666
  private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
616
667
  private onTraceFn: (listener: TraceEventListener) => () => void;
617
668
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
@@ -627,11 +678,11 @@ class ModuleContextImpl implements ModuleContext {
627
678
  registry: ModuleRegistry,
628
679
  isRestart: boolean,
629
680
  getAgents: () => Agent[],
630
- addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId,
681
+ addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId,
631
682
  editMessage: (id: MessageId, content: ContentBlock[]) => void,
632
683
  removeMessage: (id: MessageId) => void,
633
684
  getMessage: (id: MessageId) => ModuleMessage | null,
634
- queryMessages: (filter: MessageQuery) => MessageQueryResult,
685
+ queryMessages: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult,
635
686
  pushEvent: (event: ProcessEvent, moduleName?: string) => void,
636
687
  onTrace: (listener: TraceEventListener) => () => void,
637
688
  callTool: (call: ToolCall) => Promise<ToolResult>
@@ -753,9 +804,17 @@ class ModuleContextImpl implements ModuleContext {
753
804
  addMessage(
754
805
  participant: string,
755
806
  content: ContentBlock[],
756
- metadata?: MessageMetadata & { external?: ExternalIdRef }
807
+ metadata?: MessageMetadata & { external?: ExternalIdRef },
808
+ opts?: { forAgent?: string },
757
809
  ): MessageId {
758
- const id = this.addMessageFn(participant, content, metadata);
810
+ // The external id map is read in the primary's store
811
+ // (findMessageByExternalId), so a mapping into another window would
812
+ // answer wrong.
813
+ if (metadata?.external && opts?.forAgent !== undefined) {
814
+ throw new Error(`A message for agent ${JSON.stringify(opts.forAgent)} can't carry metadata.external: `
815
+ + "its external id would be looked up in the primary's window. Leave forAgent out, or the external id.");
816
+ }
817
+ const id = this.addMessageFn(participant, content, metadata, opts);
759
818
 
760
819
  // Track external ID if provided. A write the framework holds keeps '',
761
820
  // as it did before held writes had handles: the map is module state an
@@ -819,8 +878,8 @@ class ModuleContextImpl implements ModuleContext {
819
878
  return this.getMessageFn(id);
820
879
  }
821
880
 
822
- queryMessages(filter: MessageQuery): MessageQueryResult {
823
- return this.queryMessagesFn(filter);
881
+ queryMessages(filter: MessageQuery, opts?: { forAgent?: string }): MessageQueryResult {
882
+ return this.queryMessagesFn(filter, opts);
824
883
  }
825
884
 
826
885
  getAgents(): AgentInfo[] {
@@ -976,7 +1035,7 @@ function withoutRefusal(call: ToolCall, result: ToolResult): ToolResult {
976
1035
  }
977
1036
 
978
1037
  /** A result's flags, which consumers read for their truth: booleans here. */
979
- const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
1038
+ const RESULT_FLAGS = ['success', 'isError', 'endTurn', 'dataIsContent'] as const;
980
1039
 
981
1040
  /**
982
1041
  * A module's tool result as the framework can store, send and trace it. A
@@ -1047,7 +1106,7 @@ export function checkedToolResult(call: ToolCall, result: unknown): { result: To
1047
1106
  endTurn = Boolean(fields.endTurn);
1048
1107
  read('error');
1049
1108
  ownText = errorText(fields.error);
1050
- for (const key of ['success', 'isError', 'data']) read(key);
1109
+ for (const key of ['success', 'isError', 'data', 'dataIsContent']) read(key);
1051
1110
  for (const key of Object.keys(candidate)) {
1052
1111
  if (key !== 'toJSON' && !(key in fields)) fields[key] = candidate[key];
1053
1112
  }
@@ -828,7 +828,7 @@ export class DiscordModule implements Module {
828
828
  const stored = this.storedRow(msg.discordMessageId);
829
829
  if (live) live.phase = 'writing';
830
830
 
831
- // Return declarative response - framework applies all writes atomically
831
+ // Return declarative response - the framework applies its writes
832
832
  return {
833
833
  ...(stored ? {} : {
834
834
  addMessages: [
@@ -49,7 +49,7 @@ import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../t
49
49
  import type { EventResponse, ProcessState } from '../../types/module.js';
50
50
  import type { SearchWorkerMessage, SearchWorkerMatch } from './search-regex-worker.js';
51
51
  import type { ChannelRegistry } from '../../mcpl/channel-registry.js';
52
- import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges } from './semantic.js';
52
+ import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges, type SyncCursors } from './semantic.js';
53
53
  import { unknownToolMessage } from '../unknown-tool.js';
54
54
  import { safeSlice } from '../../safe-slice.js';
55
55
 
@@ -535,7 +535,7 @@ export class HistoryModule implements Module {
535
535
  channelId?: string;
536
536
  n: number;
537
537
  side: 'newest' | 'oldest';
538
- }): { messages: StoredMessage[]; more: boolean } {
538
+ }): { messages: StoredMessage[]; more: boolean; total?: number } {
539
539
  const cm = this.cm as ContextManager;
540
540
  const { fromMs, toMs, channelId, n, side } = opts;
541
541
  const newest = side === 'newest';
@@ -553,8 +553,8 @@ export class HistoryModule implements Module {
553
553
  const fetch = (f: number | undefined, t: number | undefined, limit: number, offset = 0) =>
554
554
  limit <= 0 ? [] : cm.queryMessagesByTimeAndChannel({ fromMs: f, toMs: t, channelId, limit, offset }).messages;
555
555
  const total = count(fromMs, toMs);
556
- if (n === 0) return { messages: [], more: total > 0 };
557
- if (total <= n) return { messages: dirSort(fetch(fromMs, toMs, total)), more: false };
556
+ if (n === 0) return { messages: [], more: total > 0, total };
557
+ if (total <= n) return { messages: dirSort(fetch(fromMs, toMs, total)), more: false, total };
558
558
 
559
559
  // The window is parameterised by its FAR bound `e`: [e, toMs] for the
560
560
  // newest side, [fromMs, e] for the oldest; its count is monotone in e.
@@ -580,7 +580,7 @@ export class HistoryModule implements Module {
580
580
  }
581
581
  const fetchCap = Math.max(4 * n, n + 1000);
582
582
  if (inCount <= fetchCap) {
583
- return { messages: dirSort(fetch(...win(inside), inCount)).slice(0, n), more: true };
583
+ return { messages: dirSort(fetch(...win(inside), inCount)).slice(0, n), more: true, total };
584
584
  }
585
585
  // Overshoot: bisect the far bound. The empty side starts one past the
586
586
  // range's near end (newest: past toMs / any date; oldest: before lo).
@@ -595,7 +595,7 @@ export class HistoryModule implements Module {
595
595
  inside = mid;
596
596
  inCount = c;
597
597
  if (c <= fetchCap) {
598
- return { messages: dirSort(fetch(...win(mid), c)).slice(0, n), more: true };
598
+ return { messages: dirSort(fetch(...win(mid), c)).slice(0, n), more: true, total };
599
599
  }
600
600
  }
601
601
  }
@@ -607,7 +607,7 @@ export class HistoryModule implements Module {
607
607
  const need = n - near.length;
608
608
  const ties = count(inside, inside);
609
609
  const edgeMs = fetch(inside, inside, need, newest ? Math.max(0, ties - need) : 0);
610
- return { messages: dirSort([...near, ...edgeMs]).slice(0, n), more: true };
610
+ return { messages: dirSort([...near, ...edgeMs]).slice(0, n), more: true, total };
611
611
  }
612
612
 
613
613
  async start(ctx: ModuleContext): Promise<void> {
@@ -624,6 +624,16 @@ export class HistoryModule implements Module {
624
624
  const prev = this.ctx?.getState<Record<string, unknown>>() ?? {};
625
625
  this.ctx?.setState({ ...prev, semanticPending: p });
626
626
  };
627
+ // Module state follows the store's branches, so each branch keeps its
628
+ // own sync cursors, and a branch made at an earlier point inherits them
629
+ // as they were there.
630
+ this.indexer.cursorStore = {
631
+ read: () => this.ctx?.getState<{ semanticCursors?: SyncCursors }>()?.semanticCursors ?? null,
632
+ write: (cursors) => {
633
+ const prev = this.ctx?.getState<Record<string, unknown>>() ?? {};
634
+ this.ctx?.setState({ ...prev, semanticCursors: cursors });
635
+ },
636
+ };
627
637
  this.indexer.attach();
628
638
  }
629
639
 
@@ -631,13 +641,16 @@ export class HistoryModule implements Module {
631
641
  this.ctx = null;
632
642
  if (this.syncTimer) { clearInterval(this.syncTimer); this.syncTimer = null; }
633
643
  if (this.firstSyncTimer) { clearTimeout(this.firstSyncTimer); this.firstSyncTimer = null; }
634
- if (this.indexer) this.indexer.onPendingChange = null;
644
+ // Unwired from the state it can no longer reach, the indexer goes on
645
+ // from the cursors it last wrote for each branch, kept in memory.
646
+ if (this.indexer) { this.indexer.onPendingChange = null; this.indexer.cursorStore = null; }
635
647
  this.indexer?.dispose();
636
648
  }
637
649
 
638
650
  /**
639
651
  * Background sync into the semantic index: every `syncIntervalMs` (default
640
- * 60 s) push up to `maxSyncPerTick` new items. Idempotent to call — starts
652
+ * 60 s) write at most `maxSyncPerTick` items to the index, counted as its
653
+ * doc says. Idempotent to call — starts
641
654
  * once we have both a context-manager (bind) and a config; `unref`'d so it
642
655
  * never keeps a shutting-down process alive. A first tick runs after 5 s so
643
656
  * a fresh store starts backfilling immediately rather than a minute later.
@@ -1137,7 +1150,8 @@ export class HistoryModule implements Module {
1137
1150
  hint:
1138
1151
  (pageFull
1139
1152
  ? 'More matching messages exist. '
1140
- : `Stopped after scanning ${scanned} messages of the window without reaching its end. `) +
1153
+ : `Stopped after scanning ${scanned} messages of the window without reaching its end` +
1154
+ (page.length === 0 ? ', so returning none is not a negative result for the window. ' : '. ')) +
1141
1155
  `Continue by repeating this call with the same from/to/channelId/author plus ` +
1142
1156
  `the fields of \`resume\` (windowOffset:${resume.windowOffset}, offset:${resume.offset}, afterId).`,
1143
1157
  };
@@ -1360,6 +1374,7 @@ export class HistoryModule implements Module {
1360
1374
  !!skipRange && m.timestamp.getTime() === edgeMs && m.sequence >= skipRange[0] && m.sequence <= skipRange[1];
1361
1375
  let windowMessages: StoredMessage[];
1362
1376
  let truncated: boolean;
1377
+ let unscanned: number | undefined;
1363
1378
  {
1364
1379
  // Over-fetch by the number of messages at the edge millisecond (the
1365
1380
  // only ones that can be skipped), then drop the skipped ones.
@@ -1368,6 +1383,7 @@ export class HistoryModule implements Module {
1368
1383
  const pool = skipRange ? w.messages.filter((m) => !isSkipped(m)) : w.messages;
1369
1384
  truncated = w.more || pool.length > maxScan;
1370
1385
  windowMessages = pool.slice(0, maxScan);
1386
+ if (w.total !== undefined) unscanned = w.total - (w.messages.length - pool.length) - windowMessages.length;
1371
1387
  }
1372
1388
  const candidatePoolSize = windowMessages.length;
1373
1389
  const candidates = authorFilter ? windowMessages.filter(authorFilter) : windowMessages;
@@ -1381,7 +1397,7 @@ export class HistoryModule implements Module {
1381
1397
  // the pool was never looked at) or the pool itself was truncated. The
1382
1398
  // timestamp is the continuation point — from: (oldest) / to: (newest).
1383
1399
  // Boundary is inclusive, so a message at exactly that instant may repeat.
1384
- const resumeInfo = (scanned: number): Record<string, unknown> => {
1400
+ const resumeInfo = (scanned: number, matched: number): Record<string, unknown> => {
1385
1401
  const stoppedEarly = scanned < candidates.length;
1386
1402
  if (!stoppedEarly && !truncated) return {};
1387
1403
  // Stopped at limit: the last candidate actually looked at. Scanned the
@@ -1419,9 +1435,16 @@ export class HistoryModule implements Module {
1419
1435
  hint:
1420
1436
  (stoppedEarly
1421
1437
  ? 'Stopped at limit; more candidates remain. '
1422
- : `Only the ${order} ${candidatePoolSize} messages of this range were scanned (maxScan). `) +
1438
+ : matched === 0
1439
+ ? `No match among the ${order} ${candidatePoolSize} ${candidatePoolSize === 1 ? 'message' : 'messages'} ` +
1440
+ 'scanned (maxScan); ' +
1441
+ (unscanned === undefined
1442
+ ? 'the rest of this range was not searched'
1443
+ : `${unscanned} more in this range ${unscanned === 1 ? 'was' : 'were'} not searched`) +
1444
+ ', so this is not a negative result for the range. '
1445
+ : `Only the ${order} ${candidatePoolSize} messages of this range were scanned (maxScan). `) +
1423
1446
  `Continue by repeating the call with the fields of \`resume\` (${bound}:"${at}" and skipSequences), ` +
1424
- 'or raise limit/maxScan, or narrow with channelId/author/dates.',
1447
+ `or raise limit/maxScan, or narrow with ${[channelId === undefined ? 'channelId' : null, stoppedEarly && input.author === undefined ? 'author' : null, 'dates'].filter((w) => w !== null).join('/')}.`,
1425
1448
  };
1426
1449
  };
1427
1450
 
@@ -1436,7 +1459,7 @@ export class HistoryModule implements Module {
1436
1459
  // on a deadline. See search-regex-worker.ts's header for the full
1437
1460
  // rationale.
1438
1461
  const { matches, scanned } = await this.searchWithRegexWorker(candidates, query, flags, limit);
1439
- return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned), matches } };
1462
+ return { success: true, data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches } };
1440
1463
  }
1441
1464
 
1442
1465
  // Plain substring search (String.prototype.indexOf) is inherently
@@ -1468,7 +1491,7 @@ export class HistoryModule implements Module {
1468
1491
 
1469
1492
  return {
1470
1493
  success: true,
1471
- data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned), matches },
1494
+ data: { scanned, ...poolInfo, truncated, ...resumeInfo(scanned, matches.length), matches },
1472
1495
  };
1473
1496
  }
1474
1497
 
@@ -1608,7 +1631,10 @@ export class HistoryModule implements Module {
1608
1631
  );
1609
1632
  }
1610
1633
 
1611
- // Bounded catch-up so the newest messages are searchable; never block on a backlog.
1634
+ // A catch-up bounded in what it writes, so the newest messages are
1635
+ // searchable without waiting on a backlog. It can re-send many items the
1636
+ // index holds unchanged, as on a branch's first pass (maxSyncPerTick's
1637
+ // doc), which costs a request per batch and no embedding.
1612
1638
  let sync: SyncReport | null = null;
1613
1639
  if (this.indexer) {
1614
1640
  try { sync = await this.indexer.catchUp(this.semanticCfg.maxSyncBeforeSearch ?? 256); } catch { /* reported via indexer.lastError */ }
@@ -1672,9 +1698,10 @@ export class HistoryModule implements Module {
1672
1698
  // to the model — getMessage/getSummary rebuild on branch switch, so an
1673
1699
  // undone message, or a summary this branch doesn't hold, answers null.
1674
1700
  // A branch made from an earlier point, as an undo makes one, can mint a
1675
- // summary id another branch already used, so a summary hit's score,
1676
- // timestamp and date filtering can still be that other summary's.
1677
- // k is small, so this is at most `limit` local lookups.
1701
+ // summary id another branch already used, so a summary's key carries its
1702
+ // creation time too, and a hit whose time isn't this branch's summary's
1703
+ // is another branch's. k is small, so this is at most `limit` local
1704
+ // lookups.
1678
1705
  // A message hit whose current text has nothing left to index (edited
1679
1706
  // blank, delete not yet applied) is dropped too, so its stale snippet is
1680
1707
  // never served. Dropped hits are replaced by over-fetching: when the
@@ -1685,14 +1712,12 @@ export class HistoryModule implements Module {
1685
1712
  // A message hit's snippet is rebuilt from the CURRENT local text, never
1686
1713
  // the remote copy: an edit not yet synced (or one the index never got)
1687
1714
  // must not leak a passage the edit removed. Only the ranking can be stale.
1688
- // A summary hit's snippet is rebuilt from the local summary too: a
1689
- // service that keeps only vectors and ids (no text) still yields one, and
1690
- // a summary whose id another branch also minted shows this branch's text
1691
- // in its snippet and chars.
1715
+ // A summary hit's snippet is rebuilt from the local summary too, so a
1716
+ // service that keeps only vectors and ids (no text) still yields one.
1692
1717
  const currentText = new Map<string, string>();
1693
- const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' => {
1718
+ const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' | 'superseded' => {
1694
1719
  const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
1695
- const sumId = /^sum:(.+)$/.exec(h.id)?.[1];
1720
+ const sum = parseSummaryKey(h.id);
1696
1721
  if (msgId !== undefined) {
1697
1722
  const m = cm.getMessage(msgId);
1698
1723
  if (!m) return 'offBranch';
@@ -1701,9 +1726,18 @@ export class HistoryModule implements Module {
1701
1726
  currentText.set(h.id, text);
1702
1727
  return 'ok';
1703
1728
  }
1704
- if (sumId !== undefined) {
1705
- const s = cm.getSummary(sumId);
1729
+ if (sum !== null) {
1730
+ // An item from before keys carried the creation time is never a hit.
1731
+ // One whose id names no summary on this branch is another branch's,
1732
+ // and is counted as such. One whose id this branch has can hold this
1733
+ // branch's summary or another's under the same id, and isn't counted.
1734
+ // Until this branch's passes send the summary under its own key, as
1735
+ // the search's catch-up does within its budget after any queued
1736
+ // edits, a search can miss it.
1737
+ const s = cm.getSummary(sum.id);
1706
1738
  if (!s) return 'offBranch';
1739
+ if (sum.createdMs === null) return 'superseded';
1740
+ if (s.created !== sum.createdMs) return 'offBranch';
1707
1741
  const text = summaryIndexText(s, this.semanticCfg!);
1708
1742
  // An empty text here is mostly defensive: context-manager drops a
1709
1743
  // summary that's empty after trimming when it loads, and refuses to
@@ -1771,10 +1805,13 @@ export class HistoryModule implements Module {
1771
1805
  const meta = typeof h.meta === 'object' && h.meta !== null && !Array.isArray(h.meta)
1772
1806
  ? h.meta as { participant?: unknown; author?: unknown }
1773
1807
  : null;
1808
+ // A summary is shown as `sum:<id>`, as it was before its key carried its creation time.
1809
+ const sum = parseSummaryKey(h.id);
1810
+ const id = sum ? `sum:${sum.id}` : h.id;
1774
1811
  const missing = [...(h.ts !== null && timestamp === null ? ['ts'] : []), ...(meta === null ? ['meta'] : [])];
1775
- if (missing.length > 0) unread.push(`${shown(h.id)} (${missing.join(' and ')})`);
1812
+ if (missing.length > 0) unread.push(`${shown(id)} (${missing.join(' and ')})`);
1776
1813
  return {
1777
- id: h.id,
1814
+ id,
1778
1815
  /** Raw message id for a `msg:` hit (null for summaries) — pass it to extract({ aroundId }). */
1779
1816
  messageId: h.id.startsWith('msg:') ? h.id.slice(4) : null,
1780
1817
  kind: h.kind,
@@ -1797,7 +1834,10 @@ export class HistoryModule implements Module {
1797
1834
  hits,
1798
1835
  index: {
1799
1836
  indexed: res.count_indexed,
1800
- behind: sync ? sync.more : this.indexer === null ? null : true,
1837
+ // The pass may have read another branch: one the search joined
1838
+ // that started before a switch, or one the search left while it
1839
+ // waited.
1840
+ behind: this.indexer === null ? null : sync && this.indexer.passedOn(sync) === this.indexer.branchId() ? sync.more : true,
1801
1841
  syncedThisCall: sync ? sync.pushed : 0,
1802
1842
  lastError: this.indexer?.lastError ?? null,
1803
1843
  /** Hits the index returned for messages/summaries not on the current branch (undone, checked out past). */