@animalabs/agent-framework 0.21.1 → 0.22.1

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 (348) hide show
  1. package/README.md +54 -4
  2. package/dist/src/agent.d.ts +17 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +105 -43
  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 +41 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/code-execution/py-runner.d.ts +22 -1
  10. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  11. package/dist/src/code-execution/py-runner.js +64 -9
  12. package/dist/src/code-execution/py-runner.js.map +1 -1
  13. package/dist/src/code-execution/runtime-py.d.ts +4 -0
  14. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  15. package/dist/src/code-execution/runtime-py.js +215 -38
  16. package/dist/src/code-execution/runtime-py.js.map +1 -1
  17. package/dist/src/framework.d.ts +569 -59
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3091 -542
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +21 -0
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +40 -7
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/index.d.ts +7 -2
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  30. package/dist/src/mcpl/channel-registry.js +26 -3
  31. package/dist/src/mcpl/channel-registry.js.map +1 -1
  32. package/dist/src/mcpl/conversation-router.d.ts +28 -1
  33. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  34. package/dist/src/mcpl/conversation-router.js +17 -0
  35. package/dist/src/mcpl/conversation-router.js.map +1 -1
  36. package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
  37. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  38. package/dist/src/mcpl/feature-set-manager.js +31 -7
  39. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  40. package/dist/src/mcpl/index.d.ts +4 -0
  41. package/dist/src/mcpl/index.d.ts.map +1 -1
  42. package/dist/src/mcpl/index.js +1 -0
  43. package/dist/src/mcpl/index.js.map +1 -1
  44. package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
  45. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  46. package/dist/src/mcpl/prose-stream-router.js +85 -22
  47. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  48. package/dist/src/mcpl/push-coalescer.d.ts +32 -4
  49. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  50. package/dist/src/mcpl/push-coalescer.js +46 -13
  51. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  52. package/dist/src/mcpl/push-handler.d.ts +5 -3
  53. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  54. package/dist/src/mcpl/push-handler.js +41 -11
  55. package/dist/src/mcpl/push-handler.js.map +1 -1
  56. package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
  57. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  58. package/dist/src/mcpl/reference-fetcher.js +20 -9
  59. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  60. package/dist/src/mcpl/references.d.ts +29 -6
  61. package/dist/src/mcpl/references.d.ts.map +1 -1
  62. package/dist/src/mcpl/references.js +57 -16
  63. package/dist/src/mcpl/references.js.map +1 -1
  64. package/dist/src/mcpl/server-connection.d.ts +32 -1
  65. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  66. package/dist/src/mcpl/server-connection.js +76 -11
  67. package/dist/src/mcpl/server-connection.js.map +1 -1
  68. package/dist/src/mcpl/server-registry.d.ts +5 -2
  69. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  70. package/dist/src/mcpl/server-registry.js +25 -7
  71. package/dist/src/mcpl/server-registry.js.map +1 -1
  72. package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
  73. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  74. package/dist/src/mcpl/tool-lifecycle.js +47 -1
  75. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  76. package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
  77. package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
  78. package/dist/src/mcpl/tool-pattern-check.js +79 -0
  79. package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
  80. package/dist/src/mcpl/types.d.ts +15 -5
  81. package/dist/src/mcpl/types.d.ts.map +1 -1
  82. package/dist/src/mcpl/types.js.map +1 -1
  83. package/dist/src/mcpl/visible-content.d.ts +39 -0
  84. package/dist/src/mcpl/visible-content.d.ts.map +1 -0
  85. package/dist/src/mcpl/visible-content.js +54 -0
  86. package/dist/src/mcpl/visible-content.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +90 -6
  88. package/dist/src/module-registry.d.ts.map +1 -1
  89. package/dist/src/module-registry.js +260 -18
  90. package/dist/src/module-registry.js.map +1 -1
  91. package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
  92. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
  93. package/dist/src/modules/workspace/branch-intent.js +83 -0
  94. package/dist/src/modules/workspace/branch-intent.js.map +1 -0
  95. package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
  96. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
  97. package/dist/src/modules/workspace/disk-agreement.js +259 -0
  98. package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
  99. package/dist/src/modules/workspace/effects.d.ts +84 -0
  100. package/dist/src/modules/workspace/effects.d.ts.map +1 -0
  101. package/dist/src/modules/workspace/effects.js +368 -0
  102. package/dist/src/modules/workspace/effects.js.map +1 -0
  103. package/dist/src/modules/workspace/index.d.ts +155 -17
  104. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  105. package/dist/src/modules/workspace/index.js +1101 -424
  106. package/dist/src/modules/workspace/index.js.map +1 -1
  107. package/dist/src/modules/workspace/observe.d.ts +288 -0
  108. package/dist/src/modules/workspace/observe.d.ts.map +1 -0
  109. package/dist/src/modules/workspace/observe.js +825 -0
  110. package/dist/src/modules/workspace/observe.js.map +1 -0
  111. package/dist/src/modules/workspace/reconcile.d.ts +238 -0
  112. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/reconcile.js +909 -0
  114. package/dist/src/modules/workspace/reconcile.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +6 -59
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  117. package/dist/src/modules/workspace/sync.js +6 -257
  118. package/dist/src/modules/workspace/sync.js.map +1 -1
  119. package/dist/src/modules/workspace/types.d.ts +77 -10
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  121. package/dist/src/modules/workspace/types.js.map +1 -1
  122. package/dist/src/operator-log.d.ts +41 -0
  123. package/dist/src/operator-log.d.ts.map +1 -1
  124. package/dist/src/operator-log.js.map +1 -1
  125. package/dist/src/provider-waits.d.ts +216 -0
  126. package/dist/src/provider-waits.d.ts.map +1 -0
  127. package/dist/src/provider-waits.js +482 -0
  128. package/dist/src/provider-waits.js.map +1 -0
  129. package/dist/src/record-journal.d.ts +127 -0
  130. package/dist/src/record-journal.d.ts.map +1 -0
  131. package/dist/src/record-journal.js +202 -0
  132. package/dist/src/record-journal.js.map +1 -0
  133. package/dist/src/refusal-reactions.d.ts +14 -12
  134. package/dist/src/refusal-reactions.d.ts.map +1 -1
  135. package/dist/src/refusal-reactions.js +14 -12
  136. package/dist/src/refusal-reactions.js.map +1 -1
  137. package/dist/src/shutdown.d.ts +13 -0
  138. package/dist/src/shutdown.d.ts.map +1 -0
  139. package/dist/src/shutdown.js +46 -0
  140. package/dist/src/shutdown.js.map +1 -0
  141. package/dist/src/silent-heartbeat.d.ts +74 -0
  142. package/dist/src/silent-heartbeat.d.ts.map +1 -0
  143. package/dist/src/silent-heartbeat.js +77 -0
  144. package/dist/src/silent-heartbeat.js.map +1 -0
  145. package/dist/src/tool-presentation.d.ts +50 -0
  146. package/dist/src/tool-presentation.d.ts.map +1 -0
  147. package/dist/src/tool-presentation.js +235 -0
  148. package/dist/src/tool-presentation.js.map +1 -0
  149. package/dist/src/tool-result-guard.d.ts +142 -6
  150. package/dist/src/tool-result-guard.d.ts.map +1 -1
  151. package/dist/src/tool-result-guard.js +357 -36
  152. package/dist/src/tool-result-guard.js.map +1 -1
  153. package/dist/src/tool-result-history.d.ts +13 -0
  154. package/dist/src/tool-result-history.d.ts.map +1 -1
  155. package/dist/src/tool-result-history.js +21 -1
  156. package/dist/src/tool-result-history.js.map +1 -1
  157. package/dist/src/types/agent.d.ts +18 -2
  158. package/dist/src/types/agent.d.ts.map +1 -1
  159. package/dist/src/types/events.d.ts +16 -0
  160. package/dist/src/types/events.d.ts.map +1 -1
  161. package/dist/src/types/framework.d.ts +12 -0
  162. package/dist/src/types/framework.d.ts.map +1 -1
  163. package/dist/src/types/module.d.ts +26 -4
  164. package/dist/src/types/module.d.ts.map +1 -1
  165. package/dist/src/types/trace.d.ts +87 -3
  166. package/dist/src/types/trace.d.ts.map +1 -1
  167. package/dist/test/batched-wake-locus.test.d.ts +2 -0
  168. package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
  169. package/dist/test/batched-wake-locus.test.js +166 -0
  170. package/dist/test/batched-wake-locus.test.js.map +1 -0
  171. package/dist/test/code-execution.test.js +386 -2
  172. package/dist/test/code-execution.test.js.map +1 -1
  173. package/dist/test/component-defaults.test.d.ts +2 -0
  174. package/dist/test/component-defaults.test.d.ts.map +1 -0
  175. package/dist/test/component-defaults.test.js +54 -0
  176. package/dist/test/component-defaults.test.js.map +1 -0
  177. package/dist/test/conversation-routing.test.js +57 -1
  178. package/dist/test/conversation-routing.test.js.map +1 -1
  179. package/dist/test/discord-awareness-outbox.test.js +3 -0
  180. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  181. package/dist/test/ephemeral-settle.test.js +3 -0
  182. package/dist/test/ephemeral-settle.test.js.map +1 -1
  183. package/dist/test/event-gate.test.js +118 -0
  184. package/dist/test/event-gate.test.js.map +1 -1
  185. package/dist/test/explicit-prose-routing.test.js +975 -0
  186. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  187. package/dist/test/framework.test.js +160 -1
  188. package/dist/test/framework.test.js.map +1 -1
  189. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  190. package/dist/test/helpers/coalescing-fixture.js +3 -2
  191. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  192. package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
  193. package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
  194. package/dist/test/helpers/pytc-stop-rows.js +143 -0
  195. package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
  196. package/dist/test/inference-exhaustion-rewind.test.js +142 -14
  197. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  198. package/dist/test/inference-failure-bounds.test.d.ts +2 -0
  199. package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
  200. package/dist/test/inference-failure-bounds.test.js +1201 -0
  201. package/dist/test/inference-failure-bounds.test.js.map +1 -0
  202. package/dist/test/late-events-after-stop.test.d.ts +2 -0
  203. package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
  204. package/dist/test/late-events-after-stop.test.js +122 -0
  205. package/dist/test/late-events-after-stop.test.js.map +1 -0
  206. package/dist/test/mcpl-awareness-barrier.test.js +141 -0
  207. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  208. package/dist/test/mcpl-coalescing.test.js +97 -22
  209. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  210. package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
  211. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
  212. package/dist/test/mcpl-config-diagnostics.test.js +452 -0
  213. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
  214. package/dist/test/mcpl-empty-content.test.d.ts +2 -0
  215. package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
  216. package/dist/test/mcpl-empty-content.test.js +368 -0
  217. package/dist/test/mcpl-empty-content.test.js.map +1 -0
  218. package/dist/test/mcpl-manifest-changed.test.js +2 -1
  219. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  220. package/dist/test/mcpl-reference-fetcher.test.js +232 -4
  221. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  222. package/dist/test/mcpl-reference-fields.test.js +69 -2
  223. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  224. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
  225. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  226. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
  227. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
  228. package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
  229. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
  230. package/dist/test/mcpl-ws-transport.test.js +103 -0
  231. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  232. package/dist/test/ops-alert.test.js +2 -1
  233. package/dist/test/ops-alert.test.js.map +1 -1
  234. package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
  235. package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
  236. package/dist/test/placed-reaction-baseline.test.js +312 -0
  237. package/dist/test/placed-reaction-baseline.test.js.map +1 -0
  238. package/dist/test/prose-stream-router.test.js +41 -0
  239. package/dist/test/prose-stream-router.test.js.map +1 -1
  240. package/dist/test/provider-acceleration-cooldown.test.js +5 -1
  241. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  242. package/dist/test/provider-waits.test.d.ts +2 -0
  243. package/dist/test/provider-waits.test.d.ts.map +1 -0
  244. package/dist/test/provider-waits.test.js +2196 -0
  245. package/dist/test/provider-waits.test.js.map +1 -0
  246. package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
  247. package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
  248. package/dist/test/reaction-baseline-platform.test.js +124 -0
  249. package/dist/test/reaction-baseline-platform.test.js.map +1 -0
  250. package/dist/test/record-journal.test.d.ts +6 -0
  251. package/dist/test/record-journal.test.d.ts.map +1 -0
  252. package/dist/test/record-journal.test.js +269 -0
  253. package/dist/test/record-journal.test.js.map +1 -0
  254. package/dist/test/refusal-reaction-baseline.test.js +10 -3
  255. package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
  256. package/dist/test/save-recent-image.test.js +240 -2
  257. package/dist/test/save-recent-image.test.js.map +1 -1
  258. package/dist/test/shutdown-errors.test.d.ts +2 -0
  259. package/dist/test/shutdown-errors.test.d.ts.map +1 -0
  260. package/dist/test/shutdown-errors.test.js +662 -0
  261. package/dist/test/shutdown-errors.test.js.map +1 -0
  262. package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
  263. package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
  264. package/dist/test/silent-heartbeat-rows.test.js +451 -0
  265. package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
  266. package/dist/test/silent-heartbeat.test.js +49 -38
  267. package/dist/test/silent-heartbeat.test.js.map +1 -1
  268. package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
  269. package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
  270. package/dist/test/stop-with-work-in-flight.test.js +672 -0
  271. package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
  272. package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
  273. package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
  274. package/dist/test/surgery-marks-receipt.test.js +540 -0
  275. package/dist/test/surgery-marks-receipt.test.js.map +1 -0
  276. package/dist/test/tool-batch-hook.test.d.ts +8 -0
  277. package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
  278. package/dist/test/tool-batch-hook.test.js +275 -0
  279. package/dist/test/tool-batch-hook.test.js.map +1 -0
  280. package/dist/test/tool-presentation.test.d.ts +2 -0
  281. package/dist/test/tool-presentation.test.d.ts.map +1 -0
  282. package/dist/test/tool-presentation.test.js +268 -0
  283. package/dist/test/tool-presentation.test.js.map +1 -0
  284. package/dist/test/tool-result-guard.test.js +2545 -19
  285. package/dist/test/tool-result-guard.test.js.map +1 -1
  286. package/dist/test/tool-result-history.test.js +2 -1
  287. package/dist/test/tool-result-history.test.js.map +1 -1
  288. package/dist/test/turn-trigger-provenance.test.js +35 -0
  289. package/dist/test/turn-trigger-provenance.test.js.map +1 -1
  290. package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
  291. package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
  292. package/dist/test/unconvertible-tool-result.test.js +358 -0
  293. package/dist/test/unconvertible-tool-result.test.js.map +1 -0
  294. package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
  295. package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
  296. package/dist/test/workspace-materialize-freshness.test.js +252 -0
  297. package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
  298. package/dist/test/workspace-three-way.test.d.ts +12 -0
  299. package/dist/test/workspace-three-way.test.d.ts.map +1 -0
  300. package/dist/test/workspace-three-way.test.js +3194 -0
  301. package/dist/test/workspace-three-way.test.js.map +1 -0
  302. package/dist/tsconfig.tsbuildinfo +1 -1
  303. package/package.json +2 -2
  304. package/src/agent.ts +124 -45
  305. package/src/api/server.ts +43 -4
  306. package/src/code-execution/py-runner.ts +66 -9
  307. package/src/code-execution/runtime-py.ts +215 -38
  308. package/src/framework.ts +3191 -544
  309. package/src/gate/event-gate.ts +59 -7
  310. package/src/index.ts +18 -2
  311. package/src/mcpl/channel-registry.ts +24 -3
  312. package/src/mcpl/conversation-router.ts +34 -1
  313. package/src/mcpl/feature-set-manager.ts +36 -11
  314. package/src/mcpl/index.ts +4 -0
  315. package/src/mcpl/prose-stream-router.ts +79 -23
  316. package/src/mcpl/push-coalescer.ts +62 -13
  317. package/src/mcpl/push-handler.ts +41 -12
  318. package/src/mcpl/reference-fetcher.ts +18 -8
  319. package/src/mcpl/references.ts +57 -13
  320. package/src/mcpl/server-connection.ts +95 -13
  321. package/src/mcpl/server-registry.ts +24 -7
  322. package/src/mcpl/tool-lifecycle.ts +77 -2
  323. package/src/mcpl/tool-pattern-check.ts +106 -0
  324. package/src/mcpl/types.ts +15 -5
  325. package/src/mcpl/visible-content.ts +57 -0
  326. package/src/module-registry.ts +269 -25
  327. package/src/modules/workspace/branch-intent.ts +123 -0
  328. package/src/modules/workspace/disk-agreement.ts +325 -0
  329. package/src/modules/workspace/effects.ts +362 -0
  330. package/src/modules/workspace/index.ts +1098 -446
  331. package/src/modules/workspace/observe.ts +865 -0
  332. package/src/modules/workspace/reconcile.ts +1157 -0
  333. package/src/modules/workspace/sync.ts +6 -336
  334. package/src/modules/workspace/types.ts +75 -10
  335. package/src/operator-log.ts +30 -0
  336. package/src/provider-waits.ts +523 -0
  337. package/src/record-journal.ts +246 -0
  338. package/src/refusal-reactions.ts +14 -12
  339. package/src/shutdown.ts +49 -0
  340. package/src/silent-heartbeat.ts +141 -0
  341. package/src/tool-presentation.ts +188 -0
  342. package/src/tool-result-guard.ts +384 -34
  343. package/src/tool-result-history.ts +25 -1
  344. package/src/types/agent.ts +18 -2
  345. package/src/types/events.ts +16 -0
  346. package/src/types/framework.ts +12 -0
  347. package/src/types/module.ts +27 -4
  348. package/src/types/trace.ts +94 -3
@@ -24,6 +24,13 @@ import type {
24
24
  TraceEventListener,
25
25
  } from './types/index.js';
26
26
  import type { Agent } from './agent.js';
27
+ import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
28
+
29
+ /** How the registry queues a module's event: its push names the module. A
30
+ * `ProcessQueue` serves too, ignoring the name. */
31
+ export interface ModuleQueue {
32
+ push(event: ProcessEvent, moduleName?: string): void;
33
+ }
27
34
 
28
35
  const MODULE_STATE_PREFIX = 'modules/';
29
36
 
@@ -46,6 +53,15 @@ export function isStateExistsError(err: unknown): boolean {
46
53
  */
47
54
  const MAX_CONTEXT_TIMEOUT_MS = 30_000;
48
55
 
56
+ /**
57
+ * Bound on each module's onToolBatchComplete. The hook runs inside the
58
+ * framework's sequential event loop, so a wedged module would stall every
59
+ * agent's events, not just its own round; past this the round goes on without
60
+ * it. A module with its own semantic deadline (the workspace scan's) sets it
61
+ * below this one, so it can record the miss itself.
62
+ */
63
+ export const TOOL_BATCH_HOOK_TIMEOUT_MS = 30_000;
64
+
49
65
  /**
50
66
  * Registered speech handler.
51
67
  */
@@ -60,11 +76,15 @@ interface SpeechHandler {
60
76
  * Registry for managing modules.
61
77
  */
62
78
  export class ModuleRegistry {
79
+ /** Each module's tool-batch hook still running, past its bound or not. */
80
+ private batchHooksRunning = new Map<string, Promise<unknown>>();
81
+
63
82
  private modules: Map<string, Module> = new Map();
83
+ private stopping = new Map<Module, Promise<void>>();
64
84
  private moduleContexts: Map<string, ModuleContextImpl> = new Map();
65
85
  private speechHandlers: SpeechHandler[] = [];
66
86
  private store: JsStore;
67
- private queue: ProcessQueue;
87
+ private queue: ModuleQueue;
68
88
  private getAgents: () => Agent[];
69
89
  private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
70
90
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
@@ -72,7 +92,7 @@ export class ModuleRegistry {
72
92
  private getMessageFn: (id: MessageId) => StoredMessage | null;
73
93
  private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
74
94
 
75
- private pushEventFn: (event: ProcessEvent) => void;
95
+ private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
76
96
  private onTraceFn: (listener: TraceEventListener) => () => void;
77
97
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
78
98
  /** Framework ops-alert channel, surfaced to modules via
@@ -87,7 +107,7 @@ export class ModuleRegistry {
87
107
 
88
108
  constructor(
89
109
  store: JsStore,
90
- queue: ProcessQueue,
110
+ queue: ModuleQueue,
91
111
  options: {
92
112
  getAgents: () => Agent[];
93
113
  addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
@@ -95,7 +115,8 @@ export class ModuleRegistry {
95
115
  removeMessage: (id: MessageId) => void;
96
116
  getMessage: (id: MessageId) => StoredMessage | null;
97
117
  queryMessages: (filter: MessageQuery) => MessageQueryResult;
98
- pushEvent: (event: ProcessEvent) => void;
118
+ /** Called with the pushing module's name. */
119
+ pushEvent: (event: ProcessEvent, moduleName?: string) => void;
99
120
  onTrace: (listener: TraceEventListener) => () => void;
100
121
  callTool: (call: ToolCall) => Promise<ToolResult>;
101
122
  notifyOps?: (kind: string, agentName: string, message: string, data?: Record<string, unknown>) => void;
@@ -173,9 +194,7 @@ export class ModuleRegistry {
173
194
  throw new Error(`Module not found: ${name}`);
174
195
  }
175
196
 
176
- await module.stop();
177
- this.modules.delete(name);
178
- this.moduleContexts.delete(name);
197
+ await this.stopModule(module);
179
198
  }
180
199
 
181
200
  /**
@@ -261,7 +280,20 @@ export class ModuleRegistry {
261
280
  }
262
281
 
263
282
  /**
264
- * Handle a tool call by routing to the appropriate module.
283
+ * A module tool's call for a path that stores or sends its result: the
284
+ * model's dispatch, a script's inner call, the `utils` meta-tool for a
285
+ * model. The result is checked and read once, in its JSON form
286
+ * ({@link storableToolResult}).
287
+ */
288
+ async storableToolCall(call: ToolCall): Promise<ToolResult> {
289
+ return storableToolResult(call, await this.handleToolCall(call));
290
+ }
291
+
292
+ /**
293
+ * Handle a tool call by routing to the appropriate module: the module's own
294
+ * result, unchecked, as it returned it, for a caller that uses the result
295
+ * in process (`ModuleContext.callTool`, `executeToolCall`, or a host
296
+ * calling the registry). A module's throw is answered as an error result.
265
297
  */
266
298
  async handleToolCall(call: ToolCall): Promise<ToolResult> {
267
299
  // Parse module--tool name
@@ -297,23 +329,37 @@ export class ModuleRegistry {
297
329
  try {
298
330
  return await module.handleToolCall(moduleCall);
299
331
  } catch (error) {
300
- return {
301
- success: false,
302
- error: error instanceof Error ? error.message : String(error),
303
- isError: true,
304
- };
332
+ return thrownResult(error);
305
333
  }
306
334
  }
307
335
 
308
336
  /**
309
- * Stop all modules.
337
+ * Stop all modules. A caller may bound its wait without cancelling cleanup.
338
+ * The default waits for each module's own shutdown contract.
310
339
  */
311
- async stopAll(): Promise<void> {
312
- const stopPromises = Array.from(this.modules.values()).map((m) => m.stop());
313
- await Promise.all(stopPromises);
314
- this.modules.clear();
315
- this.moduleContexts.clear();
316
- this.speechHandlers = [];
340
+ async stopAll(timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS): Promise<void> {
341
+ validateShutdownTimeout(timeoutMs);
342
+ await waitForShutdown(Array.from(this.modules.values(), module => ({
343
+ label: `module:${module.name}`, promise: this.stopModule(module),
344
+ })), timeoutMs);
345
+ }
346
+
347
+ private stopModule(module: Module): Promise<void> {
348
+ const existing = this.stopping.get(module);
349
+ if (existing) return existing;
350
+ const attempt = Promise.resolve().then(() => module.stop()).then(() => {
351
+ if (this.modules.get(module.name) === module) {
352
+ this.modules.delete(module.name);
353
+ this.moduleContexts.delete(module.name);
354
+ this.speechHandlers = this.speechHandlers.filter(handler => handler.moduleName !== module.name);
355
+ }
356
+ this.stopping.delete(module);
357
+ }, error => {
358
+ this.stopping.delete(module);
359
+ throw error;
360
+ });
361
+ this.stopping.set(module, attempt);
362
+ return attempt;
317
363
  }
318
364
 
319
365
  /**
@@ -397,6 +443,57 @@ export class ModuleRegistry {
397
443
  return injections;
398
444
  }
399
445
 
446
+ /**
447
+ * Tell modules an agent's tool batch completed (see
448
+ * Module.onToolBatchComplete). Hooks run in parallel, each awaited up to
449
+ * `timeoutMs`; a hook that throws or times out is logged and skipped, so a
450
+ * wedged module can't hold the round. Returns the modules that didn't
451
+ * finish, for the caller's trace.
452
+ */
453
+ async notifyToolBatchComplete(
454
+ agentName: string,
455
+ timeoutMs = TOOL_BATCH_HOOK_TIMEOUT_MS,
456
+ ): Promise<Array<{ module: string; error: string }>> {
457
+ const failures: Array<{ module: string; error: string }> = [];
458
+ const promises: Promise<void>[] = [];
459
+ for (const module of this.modules.values()) {
460
+ if (!module.onToolBatchComplete) continue;
461
+ // A hook still running from an earlier batch (it outlasted its bound)
462
+ // isn't called again or waited for: a wedged module costs one bound,
463
+ // not one per batch.
464
+ if (this.batchHooksRunning.has(module.name)) {
465
+ failures.push({ module: module.name, error: 'still running from an earlier batch' });
466
+ continue;
467
+ }
468
+ // Called from a promise, so a hook that throws synchronously, before
469
+ // returning its promise, is caught below like any other failure
470
+ // instead of escaping the round (the result would never be provided).
471
+ const call = Promise.resolve().then(() => module.onToolBatchComplete!(agentName));
472
+ this.batchHooksRunning.set(module.name, call);
473
+ const settled = (): void => {
474
+ if (this.batchHooksRunning.get(module.name) === call) this.batchHooksRunning.delete(module.name);
475
+ };
476
+ call.then(settled, settled);
477
+ let timer: ReturnType<typeof setTimeout> | undefined;
478
+ promises.push(
479
+ Promise.race([
480
+ call,
481
+ new Promise<void>((_, rej) => {
482
+ timer = setTimeout(() => rej(new Error(`onToolBatchComplete timed out after ${timeoutMs}ms`)), timeoutMs);
483
+ }),
484
+ ])
485
+ .catch((err: unknown) => {
486
+ const error = err instanceof Error ? err.message : String(err);
487
+ failures.push({ module: module.name, error });
488
+ console.error(`[${module.name}] onToolBatchComplete:`, err);
489
+ })
490
+ .finally(() => clearTimeout(timer)),
491
+ );
492
+ }
493
+ await Promise.all(promises);
494
+ return failures;
495
+ }
496
+
400
497
  /**
401
498
  * Dispatch speech to registered handlers.
402
499
  */
@@ -447,7 +544,7 @@ class ModuleContextImpl implements ModuleContext {
447
544
  private removeMessageFn: (id: MessageId) => void;
448
545
  private getMessageFn: (id: MessageId) => StoredMessage | null;
449
546
  private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
450
- private pushEventFn: (event: ProcessEvent) => void;
547
+ private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
451
548
  private onTraceFn: (listener: TraceEventListener) => () => void;
452
549
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
453
550
 
@@ -458,7 +555,7 @@ class ModuleContextImpl implements ModuleContext {
458
555
  moduleName: string,
459
556
  store: JsStore,
460
557
  stateId: string,
461
- queue: ProcessQueue,
558
+ queue: ModuleQueue,
462
559
  registry: ModuleRegistry,
463
560
  isRestart: boolean,
464
561
  getAgents: () => Agent[],
@@ -467,14 +564,15 @@ class ModuleContextImpl implements ModuleContext {
467
564
  removeMessage: (id: MessageId) => void,
468
565
  getMessage: (id: MessageId) => StoredMessage | null,
469
566
  queryMessages: (filter: MessageQuery) => MessageQueryResult,
470
- pushEvent: (event: ProcessEvent) => void,
567
+ pushEvent: (event: ProcessEvent, moduleName?: string) => void,
471
568
  onTrace: (listener: TraceEventListener) => () => void,
472
569
  callTool: (call: ToolCall) => Promise<ToolResult>
473
570
  ) {
474
571
  this.moduleName = moduleName;
475
572
  this.store = store;
476
573
  this.stateId = stateId;
477
- this.queue = queue;
574
+ // The deprecated handle: a push names this module, as pushEvent() does.
575
+ this.queue = { push: (event) => queue.push(event, moduleName) };
478
576
  this.registry = registry;
479
577
  this.isRestart = isRestart;
480
578
  this.getAgentsFn = getAgents;
@@ -657,7 +755,7 @@ class ModuleContextImpl implements ModuleContext {
657
755
  }
658
756
 
659
757
  pushEvent(event: ProcessEvent): void {
660
- this.pushEventFn(event);
758
+ this.pushEventFn(event, this.moduleName);
661
759
  }
662
760
 
663
761
  private persistExternalIdMap(): void {
@@ -711,3 +809,149 @@ class ProcessStateImpl implements ProcessState {
711
809
  this.ctx.pushEvent(event);
712
810
  }
713
811
  }
812
+
813
+ /** A result's flags, which consumers read for their truth: booleans here. */
814
+ const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
815
+
816
+ /**
817
+ * A module's tool result as the framework can store, send and trace it. A
818
+ * module runs in the host's process, so it can return what JSON can't hold,
819
+ * such as a cyclic object, a BigInt or a `toJSON` that throws, no result
820
+ * object at all, or one whose getters or Proxy traps throw, or answer
821
+ * differently each time they're read. Everything after this (the stored
822
+ * history, the request sent to the model, traces, the tool-result guard's
823
+ * audit record, a script's result) would throw on it, and the agent's turn
824
+ * would never settle.
825
+ *
826
+ * So the result is read once, here, and this never throws. Each field a
827
+ * consumer reads is read by name, so a result class's getters count, and any
828
+ * other own field is kept, except a `toJSON`: consumers read a result's
829
+ * fields, never a `toJSON` of the result. What's returned is the JSON form of
830
+ * those fields, parsed, so every consumer holds plain data and no module code
831
+ * runs again. It's taken where the framework stores or sends a result
832
+ * ({@link ModuleRegistry.storableToolCall}). A caller that uses the result
833
+ * in process (`ModuleContext.callTool`, `executeToolCall`) gets the module's
834
+ * own result, as the module returned it ({@link ModuleRegistry.handleToolCall}).
835
+ * The flags are booleans, read for their truth as consumers read them.
836
+ * `error` is its text form, which is all a consumer reads of it, so an error
837
+ * value JSON can't hold costs nothing. An error that says nothing (null,
838
+ * false or empty) is left out, so the result reads as having no error text.
839
+ *
840
+ * A result that can't be read or converted becomes an error result naming the
841
+ * tool, keeping the module's own error text and its `endTurn` when they could
842
+ * be read. So does data that is a function or a symbol, which JSON leaves out
843
+ * whole.
844
+ */
845
+ export function storableToolResult(call: ToolCall, result: unknown): ToolResult {
846
+ return checkedToolResult(call, result).result;
847
+ }
848
+
849
+ /**
850
+ * {@link storableToolResult}, saying whether the module's result was usable:
851
+ * read and converted (`usable`), or replaced by the error result naming the
852
+ * tool. A caller that stores the result and hands the module's own result
853
+ * back, as `puppetToolCall` does, hands back the error result instead when
854
+ * there was nothing usable to store.
855
+ */
856
+ export function checkedToolResult(call: ToolCall, result: unknown): { result: ToolResult; usable: boolean } {
857
+ // What's been read so far, kept by an error result if a later read throws.
858
+ let ownText: string | undefined;
859
+ let endTurn = false;
860
+ const unusable = (problem: string): { result: ToolResult; usable: boolean } => {
861
+ const own = ownText ? `, with this error: ${ownText}` : '.';
862
+ const error = `Tool '${call.name}' ${problem}. The call itself completed${own}`;
863
+ console.error(`[tools] ${call.callerAgentName ? `agent ${call.callerAgentName}: ` : ''}${error}`);
864
+ return { result: { success: false, isError: true, error, ...(endTurn ? { endTurn: true } : {}) }, usable: false };
865
+ };
866
+ const reasonOf = (error: unknown) => thrownText(error).split('\n')[0] || 'no reason given';
867
+ try {
868
+ if (!result || typeof result !== 'object' || Array.isArray(result) || result instanceof Error) {
869
+ const what = result === undefined || result === null ? String(result)
870
+ : Array.isArray(result) ? 'an array' : result instanceof Error ? 'an Error' : `a ${typeof result}`;
871
+ if (result instanceof Error) ownText = errorText(result);
872
+ return unusable(`returned ${what} instead of a result object, so there's nothing to store or send`);
873
+ }
874
+ const candidate = result as Record<string, unknown>;
875
+ // No prototype, so an own field named like an inherited one is a field.
876
+ const fields: Record<string, unknown> = Object.create(null);
877
+ const read = (key: string) => { if (key in candidate) fields[key] = candidate[key]; };
878
+ // endTurn first and the error's text next, so an error result keeps
879
+ // whichever of them could be read when a later read throws.
880
+ read('endTurn');
881
+ endTurn = Boolean(fields.endTurn);
882
+ read('error');
883
+ ownText = errorText(fields.error);
884
+ for (const key of ['success', 'isError', 'data']) read(key);
885
+ for (const key of Object.keys(candidate)) {
886
+ if (key !== 'toJSON' && !(key in fields)) fields[key] = candidate[key];
887
+ }
888
+ // The error is kept as its text, which consumers read; its value isn't.
889
+ delete fields.error;
890
+ // The flags are kept as their truth, which is all consumers read of them,
891
+ // before anything is serialized, so a flag's own value (a BigInt, a
892
+ // function, an object with a toJSON) neither breaks nor runs anything.
893
+ for (const flag of RESULT_FLAGS) {
894
+ if (fields[flag] !== undefined) fields[flag] = Boolean(fields[flag]);
895
+ }
896
+ // An error result reads as its error, so only a non-error result's data matters.
897
+ if (!fields.isError && (typeof fields.data === 'function' || typeof fields.data === 'symbol')) {
898
+ return unusable(`returned a ${typeof fields.data} as its data, which JSON leaves out, so there's nothing to store or send`);
899
+ }
900
+ let json: string;
901
+ try {
902
+ json = JSON.stringify(fields);
903
+ } catch (error) {
904
+ return unusable(`returned a result that can't be converted to JSON, so it can't be stored or sent (${reasonOf(error)})`);
905
+ }
906
+ const storable = JSON.parse(json) as Record<string, unknown>;
907
+ if (ownText !== undefined) storable.error = ownText;
908
+ return { result: storable as unknown as ToolResult, usable: true };
909
+ } catch (error) {
910
+ return unusable(`returned a result that can't be read, so it can't be stored or sent (${reasonOf(error)})`);
911
+ }
912
+ }
913
+
914
+ /** A result's `error` as text, always a string or none: a string as it is,
915
+ * an `Error`'s message read the same way (a module's value, so not always a
916
+ * string), an object's own `message`, any other value's JSON, and a
917
+ * primitive JSON can't hold (a BigInt, a symbol) as its String(). None for a
918
+ * value that says nothing: absent, null, false or empty, which consumers
919
+ * then report as "Unknown error". */
920
+ function errorText(error: unknown): string | undefined {
921
+ if (error === undefined || error === null || error === false || error === '') return undefined;
922
+ if (typeof error === 'string') return error;
923
+ if (error instanceof Error) return errorText(error.message);
924
+ const message = (error as { message?: unknown }).message;
925
+ if (typeof message === 'string' && message) return message;
926
+ try {
927
+ const json = JSON.stringify(error) as string | undefined;
928
+ if (json !== undefined) return json;
929
+ } catch {
930
+ // A cyclic or BigInt-holding value: nothing faithful to show as JSON.
931
+ }
932
+ if (typeof error !== 'object' && typeof error !== 'function') return thrownText(error);
933
+ return 'an error value with no text form';
934
+ }
935
+
936
+ /** A module's throw, answered as an error result: its text, or none when it
937
+ * has none, so consumers say "Unknown error". */
938
+ export function thrownResult(error: unknown): ToolResult {
939
+ const text = thrownText(error);
940
+ return { success: false, isError: true, ...(text ? { error: text } : {}) };
941
+ }
942
+
943
+ /** A thrown value's whole text, always a string, for a result's error, and
944
+ * for a value that has none too. An `Error`'s message is a module's value,
945
+ * so it may not be a string: one that isn't is given its String(), and an
946
+ * absent one is empty. String() throws on an object without a prototype,
947
+ * and such a value is named as having no text form. */
948
+ function thrownText(value: unknown): string {
949
+ try {
950
+ if (!(value instanceof Error)) return String(value);
951
+ const message: unknown = value.message;
952
+ if (typeof message === 'string') return message;
953
+ return message === undefined || message === null ? '' : String(message);
954
+ } catch {
955
+ return 'a value with no text form';
956
+ }
957
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Branch-local intent about workspace paths: what the workspace on THIS branch
3
+ * deliberately did or still owes, kept in a per-mount Chronicle tree state so
4
+ * it rewinds with the workspace tree it qualifies (a branch switch that brings
5
+ * back a deleted file brings back its missing tombstone too).
6
+ *
7
+ * - tombstone: the workspace deleted the path while disk kept a copy (no
8
+ * autoMaterialize, or a refused unlink). The hash is the deleted entry's,
9
+ * so even a legacy entry with no disk evidence keeps a durable intent.
10
+ * - origin 'store': the entry was created through workspace tools and disk
11
+ * has never agreed with it.
12
+ * - conflict: disk and workspace diverged in a way no rule may resolve
13
+ * silently; kept until a path sync, a forced materialize, or convergence.
14
+ *
15
+ * Each record is a small JSON blob addressed from the tree, so reading one is
16
+ * treeGet + getBlob and writing one is storeBlob + treeSet.
17
+ */
18
+
19
+ import type { JsStore } from '@animalabs/chronicle';
20
+
21
+ export type ConflictKind =
22
+ /** Disk, workspace and the last agreement all differ. */
23
+ | 'both-changed'
24
+ /** Disk deleted a file whose workspace copy differs from the last agreement. */
25
+ | 'deleted-on-disk'
26
+ /** Disk changed a file the workspace deleted. */
27
+ | 'changed-after-workspace-delete'
28
+ /** Disk holds a file at the path of a workspace-created entry, with other content. */
29
+ | 'store-origin-collision'
30
+ /** A legacy entry whose disk copy differs, with no evidence of which side changed. */
31
+ | 'unknown-provenance'
32
+ /** A module write or adoption was interrupted, and disk holds neither outcome. */
33
+ | 'interrupted';
34
+
35
+ /** The disk side of a conflict, as observed when it was recorded. */
36
+ export interface DiskCounterpart {
37
+ hash: string;
38
+ size: number;
39
+ mtimeMs: number;
40
+ /** Blob hash of the stored copy (text within limits); absent when only referenced. */
41
+ stored?: string;
42
+ }
43
+
44
+ export interface ConflictRecord {
45
+ kind: ConflictKind;
46
+ /** When the conflict was recorded (ms). */
47
+ at: number;
48
+ /** null when disk had no file. */
49
+ disk: DiskCounterpart | null;
50
+ }
51
+
52
+ export interface BranchIntent {
53
+ tombstone?: { hash: string };
54
+ origin?: 'store';
55
+ conflict?: ConflictRecord;
56
+ /**
57
+ * Why a recorded intent can't be read (its blob missing from the store, or
58
+ * not an intent): a tombstone or conflict it held is unknown, never absent.
59
+ * Never written; a put replaces the damaged record.
60
+ */
61
+ unreadable?: string;
62
+ }
63
+
64
+ function isEmpty(intent: BranchIntent): boolean {
65
+ return intent.tombstone === undefined && intent.origin === undefined && intent.conflict === undefined &&
66
+ intent.unreadable === undefined;
67
+ }
68
+
69
+ /** A recorded intent's blob, read; a damaged one says why it can't be. */
70
+ function readIntent(store: JsStore, blobHash: string): BranchIntent {
71
+ const blob = store.getBlob(blobHash);
72
+ if (!blob) return { unreadable: "its record's blob is missing from the store" };
73
+ try {
74
+ const parsed = JSON.parse(blob.toString('utf-8')) as unknown;
75
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as BranchIntent;
76
+ } catch {
77
+ // reported below
78
+ }
79
+ return { unreadable: 'its record is not a branch intent' };
80
+ }
81
+
82
+ export class BranchIntents {
83
+ constructor(private readonly store: JsStore, readonly treeStateId: string) {}
84
+
85
+ get(path: string): BranchIntent | null {
86
+ const entry = this.store.treeGet(this.treeStateId, path);
87
+ if (!entry) return null;
88
+ return readIntent(this.store, entry.blobHash);
89
+ }
90
+
91
+ /** Replace a path's intent; an empty intent removes the record. */
92
+ put(path: string, intent: BranchIntent | null): void {
93
+ if (intent?.unreadable !== undefined) {
94
+ const { unreadable: _damaged, ...rest } = intent;
95
+ intent = rest;
96
+ }
97
+ if (!intent || isEmpty(intent)) {
98
+ if (this.store.treeGet(this.treeStateId, path)) this.store.treeRemove(this.treeStateId, path);
99
+ return;
100
+ }
101
+ const bytes = Buffer.from(JSON.stringify(intent), 'utf-8');
102
+ const blobHash = this.store.storeBlob(bytes, 'application/json');
103
+ this.store.treeSet(this.treeStateId, path, { blobHash, size: bytes.byteLength, mode: 0 });
104
+ }
105
+
106
+ /** Update one path's intent in place. */
107
+ update(path: string, change: (current: BranchIntent) => BranchIntent): void {
108
+ this.put(path, change({ ...(this.get(path) ?? {}) }));
109
+ }
110
+
111
+ /** Every recorded intent under a prefix ('' for the whole mount). */
112
+ list(prefix = ''): Array<[string, BranchIntent]> {
113
+ const out: Array<[string, BranchIntent]> = [];
114
+ for (const entry of this.store.treeList(this.treeStateId, prefix ? prefix + '/' : undefined)) {
115
+ out.push([entry.path, readIntent(this.store, entry.blobHash)]);
116
+ }
117
+ if (prefix) {
118
+ const exact = this.get(prefix);
119
+ if (exact) out.push([prefix, exact]);
120
+ }
121
+ return out;
122
+ }
123
+ }