@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
@@ -12,7 +12,11 @@
12
12
  * so a message stamped slightly out of order is still picked up; the
13
13
  * service dedups by id + text hash, so re-sending is free. Summaries are
14
14
  * watermarked by `createdMs` (a fresh L3 spans months of old timestamps —
15
- * its creation time is the only monotonic signal).
15
+ * its creation time is the only monotonic signal). Both watermarks are
16
+ * kept in the module's state, which follows the store's branches, so each
17
+ * branch sends its own messages and summaries. A branch made at an earlier
18
+ * point, as an undo makes one, mints summary ids its parent already used,
19
+ * so a summary is keyed `sum:<id>@<created>`.
16
20
  *
17
21
  * What gets embedded per message: text blocks verbatim, plus the string
18
22
  * arguments of the agent's private-prose tools (think / journal / skip_reply
@@ -32,6 +36,7 @@ import type { ContextManager, StoredMessage } from '@animalabs/context-manager';
32
36
  import type { ContentBlock } from '@animalabs/membrane';
33
37
  import { safeSlice } from '../../safe-slice.js';
34
38
  import { SEND_TOOLS, bareToolName } from '../../send-tools.js';
39
+ import { storedInjectionMarkOf } from '../../stored-injections.js';
35
40
 
36
41
  export interface SemanticIndexConfig {
37
42
  /** Base URL of the embed-service, e.g. `http://100.90.161.34:8804`. */
@@ -46,9 +51,30 @@ export interface SemanticIndexConfig {
46
51
  syncIntervalMs?: number;
47
52
  /** Items per upsert request. Default 128, max 256 (service limit). */
48
53
  syncBatch?: number;
49
- /** Max items one background tick will push. Default 1024. */
54
+ /**
55
+ * The most items one background tick's pass writes to the index: what the
56
+ * service reports each upsert inserted or updated, with each queued edit
57
+ * counted as one. Deletes aren't counted: a pass sends its queued removals
58
+ * whatever the budget, and deletes the items its summaries had from before
59
+ * they were keyed by creation time as it sends them.
60
+ *
61
+ * A pass also re-sends items the service already holds unchanged. They
62
+ * spend no budget however many there are, and each costs the service a
63
+ * check and a metadata write, with no embedding. They're the overlap
64
+ * behind the message cursor, the summaries since the summary cursor while
65
+ * a delete of their old items keeps failing, and, once on a branch with
66
+ * no cursor yet, as after upgrading to keys with the creation time, the
67
+ * summaries it shares with other branches. A tick that finds a pass
68
+ * already running, such as a search's catch-up, starts none of its own.
69
+ * Default 1024.
70
+ */
50
71
  maxSyncPerTick?: number;
51
- /** Max items a pre-search catch-up will push before searching anyway. Default 256. */
72
+ /**
73
+ * The most items a pre-search catch-up writes to the index before
74
+ * searching anyway, counted as for `maxSyncPerTick`. A search that finds a
75
+ * pass already running, such as a tick's, waits for that pass instead,
76
+ * under that pass's own budget. Default 256.
77
+ */
52
78
  maxSyncBeforeSearch?: number;
53
79
  /** Re-scan window behind the message watermark, ms. Default 10 min. */
54
80
  overlapMs?: number;
@@ -240,7 +266,12 @@ export class SemanticIndexClient {
240
266
  }
241
267
 
242
268
  async upsert(items: IndexItem[]): Promise<{ inserted: number; updated: number; unchanged: number; count: number }> {
243
- return this.call('POST', `/v1/index/${this.ns}/upsert`, { items }, { what: 'a JSON object', is: isObject });
269
+ return this.call('POST', `/v1/index/${this.ns}/upsert`, { items }, {
270
+ // A pass's budget spends the counts, so an answer without them isn't
271
+ // taken as one.
272
+ what: 'counts of the items inserted and updated', is: (json) => isObject(json)
273
+ && Number.isFinite((json as { inserted?: unknown }).inserted) && Number.isFinite((json as { updated?: unknown }).updated),
274
+ });
244
275
  }
245
276
 
246
277
  async delete(ids: string[]): Promise<{ deleted: number }> {
@@ -307,6 +338,10 @@ function authorOf(msg: StoredMessage): string | null {
307
338
  }
308
339
 
309
340
  export function messageToItem(msg: StoredMessage, cfg: { includePrivateTools?: boolean; maxChars?: number }): IndexItem | null {
341
+ // A stored context injection (stored-injections.ts) is a source's context,
342
+ // such as retrieval's lessons, not something that happened: indexed, it
343
+ // would come back to a search as history.
344
+ if (storedInjectionMarkOf(msg.metadata)) return null;
310
345
  const text = messageIndexText(msg, cfg.includePrivateTools ?? true);
311
346
  if (!text) return null;
312
347
  const tsMs = msg.timestamp.getTime();
@@ -339,10 +374,37 @@ export function summaryIndexText(s: { content: string }, cfg: { maxChars?: numbe
339
374
  return safeSlice(s.content.trim(), 0, cfg.maxChars ?? 32_000);
340
375
  }
341
376
 
377
+ /**
378
+ * A summary's index key, `sum:<id>@<created>`. A branch made at an earlier
379
+ * point mints ids its parent already used, so the id alone names two
380
+ * summaries; its creation time, set when it was minted and kept by a branch
381
+ * that inherits it, tells them apart.
382
+ */
383
+ export function summaryKey(id: string, createdMs: number): string {
384
+ return `sum:${id}@${createdMs}`;
385
+ }
386
+
387
+ /**
388
+ * A `sum:` key read back: its summary id, and its creation time, or null for
389
+ * a key in the form written before summaries were keyed by it (`sum:<id>`).
390
+ * Null for a key that isn't a summary's. A finite number written as a string
391
+ * reads back as the same number, so the time compares equal to the
392
+ * summary's own.
393
+ */
394
+ export function parseSummaryKey(key: string): { id: string; createdMs: number | null } | null {
395
+ if (!key.startsWith('sum:')) return null;
396
+ const rest = key.slice(4);
397
+ const at = rest.lastIndexOf('@');
398
+ if (at > 0 && /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(rest.slice(at + 1))) {
399
+ return { id: rest.slice(0, at), createdMs: Number(rest.slice(at + 1)) };
400
+ }
401
+ return rest ? { id: rest, createdMs: null } : null;
402
+ }
403
+
342
404
  export function summaryToItem(s: SummaryLike, cfg: { maxChars?: number }): IndexItem | null {
343
405
  if (!s.content.trim()) return null;
344
406
  return {
345
- id: `sum:${s.id}`,
407
+ id: summaryKey(s.id, s.createdMs),
346
408
  text: summaryIndexText(s, cfg),
347
409
  ts: s.startMs / 1000,
348
410
  kind: 'summary',
@@ -355,12 +417,36 @@ export function summaryToItem(s: SummaryLike, cfg: { maxChars?: number }): Index
355
417
  /** Edits/removals not yet applied to the index (persisted by the host across restarts). */
356
418
  export interface PendingChanges { edited: string[]; removed: string[] }
357
419
 
420
+ /**
421
+ * A branch's sync cursors for one index: the creation time through which its
422
+ * summaries are sent under their keys, and the time through which its
423
+ * messages are sent. Null where nothing is sent yet.
424
+ */
425
+ export interface SyncCursors { namespace: string; summaries: number | null; messages: number | null }
426
+
427
+ /**
428
+ * Where the sync cursors are kept. HistoryModule keeps them in module state,
429
+ * which the store's branches carry, so a branch made at an earlier point
430
+ * starts from the cursors as they were there, and another branch's passes
431
+ * never move them.
432
+ */
433
+ export interface SyncCursorStore {
434
+ read(): SyncCursors | null;
435
+ write(cursors: SyncCursors): void;
436
+ }
437
+
358
438
  export interface SyncReport {
359
439
  pushed: number;
360
440
  inserted: number;
361
441
  updated: number;
362
442
  unchanged: number;
363
- /** True when the tick hit its item cap before reaching the end of the store. */
443
+ /**
444
+ * True when items may be left to send: the pass hit its item cap before
445
+ * the end of the store or of the queued edits, a delete of summaries' old
446
+ * items failed, it failed or was backing off, or the store left the branch
447
+ * it read during it. An edit or removal that a failed send queued again
448
+ * doesn't set it; the next pass sends it.
449
+ */
364
450
  more: boolean;
365
451
  messagesScanned: number;
366
452
  summariesScanned: number;
@@ -368,6 +454,8 @@ export interface SyncReport {
368
454
 
369
455
  export class SemanticIndexer {
370
456
  private inFlight: Promise<SyncReport> | null = null;
457
+ /** The branch each pass read, by its report, for a search that joined it. */
458
+ private readonly passBranch = new WeakMap<SyncReport, string>();
371
459
  private consecutiveFailures = 0;
372
460
  private backoffUntil = 0;
373
461
  lastError: string | null = null;
@@ -411,6 +499,56 @@ export class SemanticIndexer {
411
499
  private deleteUnsupported = false;
412
500
  /** Called whenever the pending edit/remove sets change. */
413
501
  onPendingChange: ((p: PendingChanges) => void) | null = null;
502
+ /**
503
+ * Where the sync cursors are kept, set by the host. Without one, as
504
+ * before the host wires module state or after it unwires it, this indexer
505
+ * goes on from the cursors it last wrote for each branch, which it keeps
506
+ * in memory. A branch it wrote none for starts with none.
507
+ */
508
+ cursorStore: SyncCursorStore | null = null;
509
+ /** The cursors this indexer last wrote, by branch id ('' for a store without branches). */
510
+ private readonly ownCursors = new Map<string, SyncCursors>();
511
+
512
+ /** The store's current branch, by id ('' for a store without branches). */
513
+ branchId(): string {
514
+ return (this.cm as { currentBranch?: () => { id: string } | undefined }).currentBranch?.()?.id ?? '';
515
+ }
516
+
517
+ /**
518
+ * The current branch's cursors for this indexer's index. Cursors kept for
519
+ * another namespace, as before the host's index setting changed, are none
520
+ * here.
521
+ */
522
+ private syncCursors(): { summaries: number | null; messages: number | null } {
523
+ const c = this.cursorStore ? this.cursorStore.read() : this.ownCursors.get(this.branchId()) ?? null;
524
+ return c && c.namespace === this.cfg.namespace ? c : { summaries: null, messages: null };
525
+ }
526
+
527
+ /** Writes the current branch's cursors, which must be `branch`'s. */
528
+ private setSyncCursors(c: { summaries: number | null; messages: number | null }, branch: string): void {
529
+ const cursors: SyncCursors = { namespace: this.cfg.namespace, summaries: c.summaries, messages: c.messages };
530
+ this.ownCursors.set(branch, cursors);
531
+ this.cursorStore?.write(cursors);
532
+ }
533
+
534
+ /**
535
+ * Deletes ids from the index. A 404 in a pass whose /stats answered with
536
+ * stats shows the index is made, so it means a service build without the
537
+ * route: that sets deleteUnsupported and answers the 404's message. Null
538
+ * when the ids are deleted; any other failure throws.
539
+ */
540
+ private async deleteIds(ids: string[], stats: IndexStats | null): Promise<string | null> {
541
+ try {
542
+ await this.client.delete(ids);
543
+ return null;
544
+ } catch (e) {
545
+ if (e instanceof EmbedServiceError && e.status === 404 && stats !== null) {
546
+ this.deleteUnsupported = true;
547
+ return e.message;
548
+ }
549
+ throw e;
550
+ }
551
+ }
414
552
 
415
553
  pendingSnapshot(): PendingChanges { return { edited: [...this.editedIds], removed: [...this.removedIds] }; }
416
554
 
@@ -444,9 +582,15 @@ export class SemanticIndexer {
444
582
 
445
583
  get backingOff(): boolean { return Date.now() < this.backoffUntil; }
446
584
 
585
+ /** The branch the pass that gave this report read, or null if it read none. */
586
+ passedOn(report: SyncReport): string | null { return this.passBranch.get(report) ?? null; }
587
+
447
588
  /**
448
- * Push what the index is missing, up to `maxItems`. Coalesces: a call while
449
- * a sync is already running returns that run's promise instead of racing it.
589
+ * Push what the index is missing, writing at most `maxItems` items by the
590
+ * service's own count (edits one each), and stopping where that runs out,
591
+ * inside a millisecond or a message page too.
592
+ * Coalesces: a call while a sync is already running returns that run's
593
+ * promise instead of racing it.
450
594
  */
451
595
  catchUp(maxItems: number): Promise<SyncReport> {
452
596
  if (this.inFlight) return this.inFlight;
@@ -465,11 +609,10 @@ export class SemanticIndexer {
465
609
  let notStats: EmbedServiceError | null = null;
466
610
  try {
467
611
  // A 2xx that isn't stats, such as a web page from a wrong route, gives no
468
- // watermark: the pass walks from the start, as for a namespace that
469
- // doesn't exist yet, and lastError names the answer. Every pass then
470
- // starts over, and a history longer than a pass's budget is never
471
- // walked to its end, so a pass whose budget runs out fails on that
472
- // answer.
612
+ // service watermark, and lastError names the answer. A branch with no
613
+ // message cursor of its own then walks its messages from the beginning,
614
+ // as for a namespace that doesn't exist yet, so a pass whose budget
615
+ // runs out fails on that answer.
473
616
  let stats: IndexStats | null = null;
474
617
  try {
475
618
  stats = await this.client.stats();
@@ -480,16 +623,66 @@ export class SemanticIndexer {
480
623
  // /stats answered 404, which stats() reads as a namespace that doesn't
481
624
  // exist: there's no index to remove from.
482
625
  const noIndex = stats === null && notStats === null;
483
- const msgWm = stats?.by_kind.message?.max_cursor ?? null;
484
- const sumWm = stats?.by_kind.summary?.max_cursor ?? null;
485
- let budget = maxItems;
626
+ // This branch's cursors, read at each pass so a switch between passes is
627
+ // seen. They're written only when every read this pass made of the
628
+ // store was on the branch read here, so one branch's pass can't move
629
+ // another's cursor past what that branch hasn't sent. A branch with no
630
+ // message cursor starts from the service's watermark, as before cursors
631
+ // were kept per branch, so an upgrade doesn't walk every history again.
632
+ const branch = this.branchId();
633
+ this.passBranch.set(report, branch);
634
+ // Set when a read of the store finds it on another branch.
635
+ let strayed = false;
636
+ // Set when a delete of summaries' old items fails.
637
+ let oldItemsOwed = false;
638
+ const reading = (): void => { if (this.branchId() !== branch) strayed = true; };
639
+ let kept = this.syncCursors();
640
+ // A namespace that doesn't exist holds nothing of this branch's, as after
641
+ // an index is deleted, so the cursors start over, at once, so a pass
642
+ // that fails partway through sending everything again still finds them
643
+ // over.
644
+ if (noIndex && (kept.summaries !== null || kept.messages !== null)) {
645
+ kept = { summaries: null, messages: null };
646
+ this.setSyncCursors(kept, branch);
647
+ }
648
+ // A branch's cursor is its own while the index holds what it sent, and
649
+ // the index's when it doesn't. Every branch's items are in the one
650
+ // index, so an index whose newest item of a kind is older than the
651
+ // cursor may have lost what this branch sent, as a restore from an
652
+ // older backup leaves it, and the pass walks from the index's newest,
653
+ // as the service's watermark did before cursors were kept. Nothing is
654
+ // lost when the newest the branch walked had nothing to index, such as
655
+ // a summary that's blank once trimmed, or was removed since, and then
656
+ // the pass re-sends what lies between, which the service finds
657
+ // unchanged. A kind the stats don't list has no items left.
658
+ const held = (cursor: number | null, kind: string): number | null => {
659
+ if (cursor === null || stats === null) return cursor;
660
+ const newest = stats.by_kind[kind]?.max_cursor ?? null;
661
+ return newest === null ? null : Math.min(cursor, newest);
662
+ };
663
+ const sumCursor = held(kept.summaries, 'summary');
664
+ const msgWm = held(kept.messages, 'message') ?? stats?.by_kind.message?.max_cursor ?? null;
665
+ let sumNext = sumCursor;
666
+ let msgThrough: number | null = null;
667
+ // What the pass may still write. Edits spend one each; summaries and
668
+ // messages spend what the service says each upsert wrote (inserted or
669
+ // updated), so a re-send it finds unchanged, such as the overlap's
670
+ // behind the message cursor, spends no budget and can't freeze a pass.
671
+ // A budget of 1.5 writes one item. One that isn't a number is no limit,
672
+ // as it was before the budget counted writes, and chunks stay a batch.
673
+ const floored = Math.floor(maxItems);
674
+ let budget = Number.isNaN(floored) ? Infinity : floored;
486
675
  let pending: IndexItem[] = [];
487
676
  const flush = async (): Promise<void> => {
488
677
  if (pending.length === 0) return;
489
678
  const r = await this.client.upsert(pending);
490
679
  report.pushed += pending.length; report.inserted += r.inserted; report.updated += r.updated; report.unchanged += r.unchanged;
680
+ budget -= r.inserted + r.updated;
491
681
  pending = [];
492
682
  };
683
+ /** Pending items are sent once they could write all the budget left,
684
+ * so a pass never writes more than its budget. */
685
+ const full = (): boolean => pending.length >= Math.min(batchSize, budget);
493
686
 
494
687
  // Edits and removals first: they never come back round in the time walk.
495
688
  // This phase fails on its own: a failure re-queues its ids, is logged
@@ -530,33 +723,29 @@ export class SemanticIndexer {
530
723
  // search-time checks (on-branch, still indexable) keep them out of
531
724
  // results.
532
725
  if (toDelete.length > 0 && !noIndex && !this.deleteUnsupported) {
726
+ // What the delete held, by kind, for a warning.
727
+ const what = [
728
+ removed.length > 0 ? `${removed.length} removal(s)` : null,
729
+ blanked.length > 0 ? `${blanked.length} edit(s) down to nothing indexable` : null,
730
+ ].filter((s): s is string => s !== null).join(' and ');
533
731
  try {
534
- await this.client.delete(toDelete);
535
- } catch (e) {
536
- const msg = e instanceof Error ? e.message : String(e);
537
- // What the delete held, by kind, for the warning.
538
- const what = [
539
- removed.length > 0 ? `${removed.length} removal(s)` : null,
540
- blanked.length > 0 ? `${blanked.length} edit(s) down to nothing indexable` : null,
541
- ].filter((s): s is string => s !== null).join(' and ');
542
- if (e instanceof EmbedServiceError && e.status === 404 && stats !== null) {
543
- // This pass's /stats answered with stats, so the index is made
544
- // and the 404 is the route's: a service build without /delete.
545
- this.deleteUnsupported = true;
732
+ const missing = await this.deleteIds(toDelete, stats);
733
+ if (missing !== null) {
546
734
  this.warn(
547
735
  `/delete answered 404, so the service lacks that route; dropping ${what}. Later removals and edits `
548
736
  + `down to nothing indexable are dropped too, until the host restarts or binds the history module `
549
- + `again: ${msg}`,
737
+ + `again: ${missing}`,
550
738
  );
551
- } else {
552
- // Any other failure, and a 404 in a pass whose /stats answered
553
- // with something that isn't stats, which doesn't show the index
554
- // is made: re-queued, for a later pass.
555
- for (const id of removed) this.removedIds.add(id);
556
- for (const id of blanked) if (!this.removedIds.has(id)) this.editedIds.add(id);
557
- phaseErrors.push(msg);
558
- this.warn(`delete failed, ${what} re-queued: ${msg}`);
559
739
  }
740
+ } catch (e) {
741
+ // Any other failure, and a 404 in a pass whose /stats answered
742
+ // with something that isn't stats, which doesn't show the index
743
+ // is made: re-queued, for a later pass.
744
+ const msg = e instanceof Error ? e.message : String(e);
745
+ for (const id of removed) this.removedIds.add(id);
746
+ for (const id of blanked) if (!this.removedIds.has(id)) this.editedIds.add(id);
747
+ phaseErrors.push(msg);
748
+ this.warn(`delete failed, ${what} re-queued: ${msg}`);
560
749
  }
561
750
  }
562
751
  try {
@@ -577,21 +766,67 @@ export class SemanticIndexer {
577
766
  // Summaries before messages: they are few, and a message backlog that
578
767
  // exceeds every tick's budget must not keep new summaries out forever.
579
768
  {
769
+ const cursor = sumCursor;
770
+ reading();
580
771
  const all = this.cm.getSummariesInRange({ fromMs: 0, toMs: Number.MAX_SAFE_INTEGER }) as SummaryLike[];
581
772
  report.summariesScanned = all.length;
582
- // `>=`: summaries sharing the watermark's millisecond may be only
583
- // partly indexed (a bounded pass can stop inside a tie). Re-sending
584
- // them is a dedup no-op for the service and costs no budget.
585
- const fresh = all.filter((s) => sumWm === null || s.createdMs >= sumWm).sort((a, b) => a.createdMs - b.createdMs);
773
+ // `>=`: a summary minted in the cursor's millisecond after the pass
774
+ // that wrote it read the store is sent by the next one. Re-sending
775
+ // the others there is a dedup no-op for the service and costs no
776
+ // budget.
777
+ const fresh = all.filter((s) => cursor === null || s.createdMs >= cursor).sort((a, b) => a.createdMs - b.createdMs);
778
+ // Through which creation time this pass sent every summary it read.
779
+ // A pass whose budget runs out inside a millisecond leaves the cursor
780
+ // at the one before, so the next pass sends the rest of that
781
+ // millisecond, re-sending what this one wrote of it at no cost, and
782
+ // counts all of it as new for the old keys below.
783
+ let through: number | null = null;
784
+ // The millisecond of the summaries being sent.
785
+ let at: number | null = null;
786
+ let finished = true;
787
+ // Each newly sent summary's key in the form written before keys
788
+ // carried the creation time, for deleting once its new item is in.
789
+ const oldKeys: string[] = [];
586
790
  for (const s of fresh) {
587
- if (budget <= 0) { report.more = true; break; }
791
+ if (s.createdMs !== at) { through = at; at = s.createdMs; }
792
+ if (budget <= 0) { report.more = true; finished = false; break; }
588
793
  const item = summaryToItem(s, this.cfg);
589
794
  if (!item) continue;
590
795
  pending.push(item);
591
- if (sumWm === null || s.createdMs > sumWm) budget--;
592
- if (pending.length >= batchSize) await flush();
796
+ if (cursor === null || s.createdMs > cursor) oldKeys.push(`sum:${s.id}`);
797
+ if (full()) await flush();
593
798
  }
594
799
  await flush();
800
+ if (finished) through = at;
801
+ // The index's old items go once their summaries' new ones are in, a
802
+ // batch of ids to a request. On a service without /delete they stay,
803
+ // and search leaves them out. A failed delete leaves the cursor, so
804
+ // the next pass sends those summaries again, which the service finds
805
+ // unchanged, and deletes again.
806
+ let deleted = true;
807
+ if (oldKeys.length > 0 && !noIndex && !this.deleteUnsupported) {
808
+ try {
809
+ let missing: string | null = null;
810
+ for (let i = 0; i < oldKeys.length && missing === null; i += batchSize) {
811
+ missing = await this.deleteIds(oldKeys.slice(i, i + batchSize), stats);
812
+ }
813
+ if (missing !== null) {
814
+ this.warn(
815
+ `/delete answered 404, so the service lacks that route; any items these ${oldKeys.length} summaries had `
816
+ + `from before keys carried the creation time stay, and search leaves them out. Later removals and edits `
817
+ + `down to nothing indexable are dropped too, until the host restarts or binds the history module `
818
+ + `again: ${missing}`,
819
+ );
820
+ }
821
+ } catch (e) {
822
+ const msg = e instanceof Error ? e.message : String(e);
823
+ deleted = false;
824
+ oldItemsOwed = true;
825
+ phaseErrors.push(msg);
826
+ this.warn(`deleting ${oldKeys.length} summaries' items from before they were keyed by creation time failed; the next pass tries again: ${msg}`);
827
+ }
828
+ }
829
+ if (deleted && through !== null) sumNext = through;
595
830
  }
596
831
 
597
832
  // Messages: walk forward from (watermark - overlap) via the time index.
@@ -606,32 +841,51 @@ export class SemanticIndexer {
606
841
  // of more than a page within one millisecond is walked, not jumped.
607
842
  let offset = 0;
608
843
  const pageSize = 256;
609
- for (;;) {
844
+ // The walk starts with the overlap's re-sends, which spend no budget, but
845
+ // like every chunk go out no bigger than the budget left, so a pass
846
+ // whose summaries left little sends them a few at a time.
847
+ for (let stopped = false; !stopped;) {
610
848
  if (budget <= 0) { report.more = true; break; }
849
+ reading();
611
850
  const page = this.cm.queryMessagesByTime({ fromMs, offset, limit: pageSize });
612
851
  const msgs = page.messages;
613
852
  report.messagesScanned += msgs.length;
614
853
  for (const m of msgs) {
854
+ // Only what the service writes spends the budget (flush). The
855
+ // overlap re-walk behind the watermark is a dedup no-op for the
856
+ // service, and it must be one for the budget too: when the overlap
857
+ // window held more messages than a tick's budget, every tick spent
858
+ // it all re-sending the same indexed items and never reached the
859
+ // first new one — the index froze at the watermark while reporting
860
+ // merely `more: true`. A pass stops inside a page once the budget
861
+ // is spent; the next one's overlap sends the rest of the
862
+ // millisecond it stopped in.
863
+ if (budget <= 0) { report.more = true; stopped = true; break; }
615
864
  const item = messageToItem(m, this.cfg);
616
865
  if (!item) continue;
617
866
  pending.push(item);
618
- // Only NEW items spend the budget. The overlap re-walk behind the
619
- // watermark is a dedup no-op for the service, and it must be one
620
- // for the budget too: when the overlap window held more messages
621
- // than a tick's budget, every tick spent it all re-sending the same
622
- // indexed items and never reached the first new one — the index
623
- // froze at the watermark while reporting merely `more: true`.
624
- if (msgWm === null || item.cursor === undefined || item.cursor > msgWm) budget--;
625
- if (pending.length >= batchSize) await flush();
867
+ if (item.cursor !== undefined && (msgThrough === null || item.cursor > msgThrough)) msgThrough = item.cursor;
868
+ if (full()) await flush();
626
869
  }
627
- if (msgs.length < pageSize) break;
870
+ if (stopped || msgs.length < pageSize) break;
628
871
  const lastMs = msgs[msgs.length - 1]!.timestamp.getTime();
629
872
  const atLast = msgs.filter((m) => m.timestamp.getTime() === lastMs).length;
630
873
  offset = lastMs === fromMs ? offset + atLast : atLast;
631
874
  fromMs = lastMs;
632
875
  }
633
876
  await flush();
877
+ // Every send of this pass is in, so its cursors stand, unless the store
878
+ // left the branch the pass read during it: what it read may then be
879
+ // another branch's. A pass that moves neither cursor writes no state.
880
+ const msgNext = msgThrough ?? msgWm;
881
+ const left = strayed || this.branchId() !== branch;
882
+ if ((sumNext !== kept.summaries || msgNext !== kept.messages) && !left) {
883
+ this.setSyncCursors({ summaries: sumNext, messages: msgNext }, branch);
884
+ }
634
885
  if (notStats && report.more) throw notStats;
886
+ // The current branch may then have items this pass didn't send, and
887
+ // old items a failed delete left are still to go.
888
+ if (left || oldItemsOwed) report.more = true;
635
889
  this.consecutiveFailures = 0; this.lastError = named(notStats?.message ?? null, ...phaseErrors); this.lastSyncAt = Date.now();
636
890
  return report;
637
891
  } catch (e) {
@@ -0,0 +1,133 @@
1
+ import { constants } from 'node:os';
2
+ import { boundedErrorMessage } from '../../error-message.js';
3
+
4
+ export class Refusal extends Error {
5
+ constructor(message: string) {
6
+ super(message);
7
+ this.name = 'Refusal';
8
+ }
9
+ }
10
+
11
+ export function didNothing(tool: string, body: string, to?: string): string {
12
+ return `The ${tool} did nothing${to !== undefined ? ` to ${JSON.stringify(to)}` : ''}: ${body}`;
13
+ }
14
+
15
+ export function onlyTheHost(can: string): string {
16
+ return `Calling again won't change that; whoever runs this host can ${can}.`;
17
+ }
18
+
19
+ // Node's own message for a failed system call names the host's absolute
20
+ // path, so an agent gets the code's plain words instead.
21
+ export function errnoWords(code: string | undefined): string {
22
+ switch (code) {
23
+ case 'ENOENT': return 'nothing is there';
24
+ case 'ENOTDIR': return 'a part of the path isn\'t a directory';
25
+ case 'EACCES': return 'permission denied';
26
+ case 'EPERM': return 'not permitted';
27
+ case 'EISDIR': return 'it\'s a directory';
28
+ case 'EEXIST': return 'something is already there';
29
+ case 'ENOTEMPTY': return 'the directory isn\'t empty';
30
+ case 'EXDEV': return 'the move would cross from one filesystem to another';
31
+ case 'ELOOP': return 'too many symlinks, or a loop of them';
32
+ case 'ENAMETOOLONG': return 'a name too long for the filesystem';
33
+ case 'ENOSPC': return 'no space left on the disk';
34
+ case 'EDQUOT': return 'the disk quota is used up';
35
+ case 'EFBIG': return 'the file would be too large for the filesystem';
36
+ case 'EROFS': return 'the filesystem is read-only';
37
+ case 'EIO': return 'an input/output error on the disk';
38
+ case 'EBUSY': case 'ETXTBSY': return 'the file is busy';
39
+ case 'EMFILE': case 'ENFILE': return 'too many files are open on this host';
40
+ case 'ENOMEM': return 'this host is out of memory';
41
+ case 'EAGAIN': case 'EWOULDBLOCK': return 'the system was busy';
42
+ case 'EINTR': return 'the system call was interrupted';
43
+ case 'ETIMEDOUT': return 'the system call timed out';
44
+ case 'ESTALE': return 'a stale network file handle';
45
+ case 'EINVAL': return 'the system refused an argument as invalid';
46
+ case undefined: return 'an error the system gave no code for';
47
+ default: return `a system error (${code})`;
48
+ }
49
+ }
50
+
51
+ export function hostFix(code: string | undefined): string | undefined {
52
+ switch (code) {
53
+ case 'ENOSPC': return 'free space on its disk';
54
+ case 'EFBIG': return 'raise the limit on a file\'s size, or keep the files on a filesystem that holds larger ones';
55
+ case 'EDQUOT': return 'raise or free the disk quota';
56
+ case 'EROFS': return 'make the filesystem writable';
57
+ case 'EIO': return 'check the disk';
58
+ case 'EACCES': case 'EPERM': return 'change the permissions';
59
+ case 'EMFILE': case 'ENFILE': return 'raise the limit on open files';
60
+ case 'ELOOP': return 'fix the symlinks on that path';
61
+ case 'ENOMEM': return 'free memory';
62
+ default: return undefined;
63
+ }
64
+ }
65
+
66
+ const PASSING = new Set(['EIO', 'EMFILE', 'ENFILE', 'ENOMEM']);
67
+ const AS_DISK_IS = new Set(['EISDIR', 'ENOTDIR', 'EEXIST', 'ENOTEMPTY', 'EXDEV']);
68
+
69
+ export function systemWords(code: string | undefined, error: unknown): string {
70
+ return code !== undefined ? errnoWords(code) : boundedErrorMessage(error);
71
+ }
72
+
73
+ export function errnoWayOn(code: string | undefined, otherwise = 'Calling again may work.', hostsName = false): string {
74
+ if (code === 'ENAMETOOLONG' && hostsName) return onlyTheHost("give the mount's directory a shorter path");
75
+ const fix = hostFix(code);
76
+ if (fix !== undefined) {
77
+ return code !== undefined && PASSING.has(code) ? `Calling again may work; if it fails the same way, whoever runs this host can ${fix}.` : onlyTheHost(fix);
78
+ }
79
+ if (code === 'ENAMETOOLONG') return "Calling again won't change that; a shorter name would.";
80
+ if (code !== undefined && AS_DISK_IS.has(code)) return "Calling again won't change that while disk holds what it does there.";
81
+ return otherwise;
82
+ }
83
+
84
+ const OS_ERRNO = new Map<number, string>();
85
+ for (const [name, number] of Object.entries(constants.errno)) if (!OS_ERRNO.has(number)) OS_ERRNO.set(number, name);
86
+
87
+ // Chronicle's store errors carry the code GenericFailure, and an I/O
88
+ // failure's message is "IO error: " and Rust's std::io::Error, which ends
89
+ // with the OS error's number: an errno on POSIX, a Win32 code on Windows.
90
+ export function errnoCode(error: unknown): string | undefined {
91
+ const code = typeof error === 'object' && error !== null ? (error as { code?: unknown }).code : undefined;
92
+ if (typeof code === 'string' && /^E[A-Z0-9]+$/.test(code)) return code;
93
+ const os = error instanceof Error && process.platform !== 'win32' ? /^IO error: .*\(os error (\d+)\)$/s.exec(error.message) : null;
94
+ return os !== null ? OS_ERRNO.get(Number(os[1])) : undefined;
95
+ }
96
+
97
+ const CHANGING = new Set(['write', 'edit', 'edit_many', 'delete', 'materialize', 'sync']);
98
+
99
+ export function changesFiles(tool: string): boolean {
100
+ return CHANGING.has(tool);
101
+ }
102
+
103
+ export const ROOT_RECHECK = "Calling again may work; if it fails the same way, whoever runs this host can check the mount's directory and its path.";
104
+
105
+ export const ROOT_UNCHECKED = "Calling again won't change that while the mount's directory is gone from its path, and checks it afresh otherwise; whoever runs this host can check the directory and its path.";
106
+
107
+ export const AWAITS_REOPEN = "The workspace can't check its files against disk until its store is reopened, so calling again won't change that; whoever runs this host can reopen it with a restart.";
108
+
109
+ export interface Unexpected {
110
+ stored?: 'unknown' | 'not' | 'held';
111
+ said?: string;
112
+ reopen?: boolean;
113
+ look?: string;
114
+ }
115
+
116
+ export function unexpectedAnswer(tool: string, subject: string | undefined, error: unknown, how: Unexpected = {}): string {
117
+ const stored = how.stored ?? 'unknown';
118
+ const code = errnoCode(error);
119
+ const words = how.said ?? systemWords(code, error);
120
+ const before = stored !== 'not' ? '' : tool === 'edit_many' ? ', before any of its edits was stored' : `, before the ${tool === 'delete' ? 'deletion' : tool} was stored`;
121
+ const head = `The ${tool}${subject !== undefined ? ` of ${subject}` : ''} stopped on an error the workspace didn't expect${before}: ${words.replace(/\.$/, '')}.`;
122
+ if (stored === 'held') {
123
+ return `${head} ${subject !== undefined ? subject : 'Its files'} already held what the call gave, so ${subject !== undefined ? 'it holds' : 'they hold'} that in the workspace either way.`;
124
+ }
125
+ const changing = stored === 'unknown' && CHANGING.has(tool);
126
+ if (how.reopen === true) return [head, AWAITS_REOPEN, ...(changing ? ["Whether it changed anything isn't known until then."] : [])].join(' ');
127
+ const look = !changing ? null
128
+ : how.look ?? (subject === undefined ? 'read its files to see what they hold now' : `read ${subject} to see what the workspace holds there now`);
129
+ const on = code !== undefined
130
+ ? errnoWayOn(code, "Calling again may work; if it fails the same way, whoever runs this host can find the whole error in this host's log.")
131
+ : "Calling again may work; if it fails the same way, whoever runs this host can find the whole error in this host's log.";
132
+ return [head, ...(look !== null ? [`Whether it changed anything isn't known: ${look}.`] : []), on].join(' ');
133
+ }