@animalabs/agent-framework 0.23.3 → 0.24.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 (355) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +65 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +98 -12
  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 +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/clock-rows.d.ts +115 -0
  10. package/dist/src/clock-rows.d.ts.map +1 -0
  11. package/dist/src/clock-rows.js +222 -0
  12. package/dist/src/clock-rows.js.map +1 -0
  13. package/dist/src/code-execution/engine.d.ts +177 -0
  14. package/dist/src/code-execution/engine.d.ts.map +1 -0
  15. package/dist/src/code-execution/engine.js +494 -0
  16. package/dist/src/code-execution/engine.js.map +1 -0
  17. package/dist/src/code-execution/index.d.ts +5 -10
  18. package/dist/src/code-execution/index.d.ts.map +1 -1
  19. package/dist/src/code-execution/index.js +5 -9
  20. package/dist/src/code-execution/index.js.map +1 -1
  21. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  22. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/calls.js +145 -0
  24. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  26. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  28. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  30. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child-main.js +132 -0
  32. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  33. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  34. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/child.js +315 -0
  36. package/dist/src/code-execution/isolate/child.js.map +1 -0
  37. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  38. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/frames.js +210 -0
  40. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  41. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  42. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/output.js +143 -0
  44. package/dist/src/code-execution/isolate/output.js.map +1 -0
  45. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  46. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/prelude.js +401 -0
  48. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  49. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  50. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/protocol.js +150 -0
  52. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  53. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  54. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/render.js +214 -0
  56. package/dist/src/code-execution/isolate/render.js.map +1 -0
  57. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  58. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/runner.js +854 -0
  60. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  61. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  62. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  63. package/dist/src/code-execution/isolate/scripts.js +357 -0
  64. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  65. package/dist/src/code-execution/quickjs.wasm +0 -0
  66. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  67. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  68. package/dist/src/code-execution/tool-definition.js +176 -142
  69. package/dist/src/code-execution/tool-definition.js.map +1 -1
  70. package/dist/src/deadline-timer.d.ts +3 -0
  71. package/dist/src/deadline-timer.d.ts.map +1 -1
  72. package/dist/src/deadline-timer.js +1 -1
  73. package/dist/src/deadline-timer.js.map +1 -1
  74. package/dist/src/framework.d.ts +1120 -457
  75. package/dist/src/framework.d.ts.map +1 -1
  76. package/dist/src/framework.js +4635 -2505
  77. package/dist/src/framework.js.map +1 -1
  78. package/dist/src/gate/event-gate.d.ts +28 -4
  79. package/dist/src/gate/event-gate.d.ts.map +1 -1
  80. package/dist/src/gate/event-gate.js +89 -13
  81. package/dist/src/gate/event-gate.js.map +1 -1
  82. package/dist/src/held-writes.d.ts +16 -3
  83. package/dist/src/held-writes.d.ts.map +1 -1
  84. package/dist/src/held-writes.js +16 -3
  85. package/dist/src/held-writes.js.map +1 -1
  86. package/dist/src/index.d.ts +8 -6
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/index.js +10 -8
  89. package/dist/src/index.js.map +1 -1
  90. package/dist/src/mcpl/channel-registry.d.ts +483 -115
  91. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  92. package/dist/src/mcpl/channel-registry.js +1735 -425
  93. package/dist/src/mcpl/channel-registry.js.map +1 -1
  94. package/dist/src/mcpl/conversation-router.d.ts +19 -39
  95. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  96. package/dist/src/mcpl/conversation-router.js +27 -59
  97. package/dist/src/mcpl/conversation-router.js.map +1 -1
  98. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  99. package/dist/src/mcpl/convert-block.js +9 -5
  100. package/dist/src/mcpl/convert-block.js.map +1 -1
  101. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  102. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  103. package/dist/src/mcpl/inbound-source.js +130 -0
  104. package/dist/src/mcpl/inbound-source.js.map +1 -1
  105. package/dist/src/mcpl/index.d.ts +2 -1
  106. package/dist/src/mcpl/index.d.ts.map +1 -1
  107. package/dist/src/mcpl/index.js +3 -2
  108. package/dist/src/mcpl/index.js.map +1 -1
  109. package/dist/src/mcpl/log-value.d.ts +6 -0
  110. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  111. package/dist/src/mcpl/log-value.js +8 -0
  112. package/dist/src/mcpl/log-value.js.map +1 -1
  113. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  114. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  115. package/dist/src/mcpl/modern-connection.js +168 -53
  116. package/dist/src/mcpl/modern-connection.js.map +1 -1
  117. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  118. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  119. package/dist/src/mcpl/prose-grammar.js +4 -5
  120. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  121. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  122. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  123. package/dist/src/mcpl/protocol-family.js +174 -17
  124. package/dist/src/mcpl/protocol-family.js.map +1 -1
  125. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  126. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-coalescer.js +227 -50
  128. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  129. package/dist/src/mcpl/push-handler.d.ts +9 -20
  130. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  131. package/dist/src/mcpl/push-handler.js +7 -49
  132. package/dist/src/mcpl/push-handler.js.map +1 -1
  133. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  134. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  135. package/dist/src/mcpl/reference-fetcher.js +4 -4
  136. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  137. package/dist/src/mcpl/server-connection.d.ts +50 -21
  138. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  139. package/dist/src/mcpl/server-connection.js +158 -74
  140. package/dist/src/mcpl/server-connection.js.map +1 -1
  141. package/dist/src/mcpl/tags.d.ts +24 -0
  142. package/dist/src/mcpl/tags.d.ts.map +1 -1
  143. package/dist/src/mcpl/tags.js +33 -0
  144. package/dist/src/mcpl/tags.js.map +1 -1
  145. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  146. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  147. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  148. package/dist/src/mcpl/transport.d.ts +55 -7
  149. package/dist/src/mcpl/transport.d.ts.map +1 -1
  150. package/dist/src/mcpl/transport.js +163 -34
  151. package/dist/src/mcpl/transport.js.map +1 -1
  152. package/dist/src/mcpl/types.d.ts +22 -6
  153. package/dist/src/mcpl/types.d.ts.map +1 -1
  154. package/dist/src/mcpl/types.js.map +1 -1
  155. package/dist/src/module-registry.d.ts +4 -2
  156. package/dist/src/module-registry.d.ts.map +1 -1
  157. package/dist/src/module-registry.js +100 -13
  158. package/dist/src/module-registry.js.map +1 -1
  159. package/dist/src/modules/discord/index.d.ts.map +1 -1
  160. package/dist/src/modules/discord/index.js +8 -13
  161. package/dist/src/modules/discord/index.js.map +1 -1
  162. package/dist/src/modules/history/index.d.ts.map +1 -1
  163. package/dist/src/modules/history/index.js +27 -4
  164. package/dist/src/modules/history/index.js.map +1 -1
  165. package/dist/src/modules/history/semantic.d.ts +15 -3
  166. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  167. package/dist/src/modules/history/semantic.js +27 -5
  168. package/dist/src/modules/history/semantic.js.map +1 -1
  169. package/dist/src/modules/index.d.ts +1 -1
  170. package/dist/src/modules/index.d.ts.map +1 -1
  171. package/dist/src/modules/index.js.map +1 -1
  172. package/dist/src/modules/unknown-tool.d.ts +4 -1
  173. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  174. package/dist/src/modules/unknown-tool.js +4 -1
  175. package/dist/src/modules/unknown-tool.js.map +1 -1
  176. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  177. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  178. package/dist/src/modules/workspace/branch-intent.js +3 -1
  179. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  180. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  181. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-plan.js +794 -0
  183. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  184. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  185. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  186. package/dist/src/modules/workspace/edit-text.js +584 -0
  187. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  188. package/dist/src/modules/workspace/effects.d.ts +5 -4
  189. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/effects.js +78 -25
  191. package/dist/src/modules/workspace/effects.js.map +1 -1
  192. package/dist/src/modules/workspace/index.d.ts +493 -62
  193. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/index.js +2216 -652
  195. package/dist/src/modules/workspace/index.js.map +1 -1
  196. package/dist/src/modules/workspace/observe.d.ts +213 -38
  197. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/observe.js +416 -36
  199. package/dist/src/modules/workspace/observe.js.map +1 -1
  200. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/reconcile.js +15 -8
  202. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  203. package/dist/src/modules/workspace/text.d.ts +32 -0
  204. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  205. package/dist/src/modules/workspace/text.js +67 -0
  206. package/dist/src/modules/workspace/text.js.map +1 -0
  207. package/dist/src/modules/workspace/types.d.ts +210 -6
  208. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  209. package/dist/src/modules/workspace/types.js.map +1 -1
  210. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  211. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  212. package/dist/src/modules/workspace/watcher.js.map +1 -1
  213. package/dist/src/operator-log.d.ts +52 -24
  214. package/dist/src/operator-log.d.ts.map +1 -1
  215. package/dist/src/operator-log.js.map +1 -1
  216. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  217. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  218. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  219. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  220. package/dist/src/recovery/offline-branch.d.ts +26 -1
  221. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  222. package/dist/src/recovery/offline-branch.js +164 -39
  223. package/dist/src/recovery/offline-branch.js.map +1 -1
  224. package/dist/src/recovery/recover-cli.js +23 -4
  225. package/dist/src/recovery/recover-cli.js.map +1 -1
  226. package/dist/src/send-tools.d.ts +11 -0
  227. package/dist/src/send-tools.d.ts.map +1 -0
  228. package/dist/src/send-tools.js +11 -0
  229. package/dist/src/send-tools.js.map +1 -0
  230. package/dist/src/timezone.d.ts +4 -2
  231. package/dist/src/timezone.d.ts.map +1 -1
  232. package/dist/src/timezone.js +4 -2
  233. package/dist/src/timezone.js.map +1 -1
  234. package/dist/src/tool-image-ledger.d.ts +46 -7
  235. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  236. package/dist/src/tool-image-ledger.js +60 -7
  237. package/dist/src/tool-image-ledger.js.map +1 -1
  238. package/dist/src/tool-result-guard.d.ts +14 -11
  239. package/dist/src/tool-result-guard.d.ts.map +1 -1
  240. package/dist/src/tool-result-guard.js +17 -14
  241. package/dist/src/tool-result-guard.js.map +1 -1
  242. package/dist/src/tool-result-history.d.ts +3 -2
  243. package/dist/src/tool-result-history.d.ts.map +1 -1
  244. package/dist/src/tool-result-history.js +3 -2
  245. package/dist/src/tool-result-history.js.map +1 -1
  246. package/dist/src/tune-out/coordinator.d.ts +21 -0
  247. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  248. package/dist/src/tune-out/coordinator.js +142 -40
  249. package/dist/src/tune-out/coordinator.js.map +1 -1
  250. package/dist/src/types/agent.d.ts +31 -6
  251. package/dist/src/types/agent.d.ts.map +1 -1
  252. package/dist/src/types/events.d.ts +66 -9
  253. package/dist/src/types/events.d.ts.map +1 -1
  254. package/dist/src/types/framework.d.ts +69 -62
  255. package/dist/src/types/framework.d.ts.map +1 -1
  256. package/dist/src/types/module.d.ts +40 -8
  257. package/dist/src/types/module.d.ts.map +1 -1
  258. package/dist/src/types/trace.d.ts +111 -10
  259. package/dist/src/types/trace.d.ts.map +1 -1
  260. package/dist/src/usage/types.d.ts +16 -0
  261. package/dist/src/usage/types.d.ts.map +1 -1
  262. package/dist/src/usage/usage-tracker.d.ts +14 -1
  263. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  264. package/dist/src/usage/usage-tracker.js +81 -41
  265. package/dist/src/usage/usage-tracker.js.map +1 -1
  266. package/package.json +6 -8
  267. package/src/agent.ts +99 -12
  268. package/src/api/server.ts +14 -12
  269. package/src/clock-rows.ts +302 -0
  270. package/src/code-execution/engine.ts +595 -0
  271. package/src/code-execution/index.ts +5 -17
  272. package/src/code-execution/isolate/calls.ts +199 -0
  273. package/src/code-execution/isolate/child-entry.ts +5 -0
  274. package/src/code-execution/isolate/child-main.ts +132 -0
  275. package/src/code-execution/isolate/child.ts +327 -0
  276. package/src/code-execution/isolate/frames.ts +209 -0
  277. package/src/code-execution/isolate/output.ts +161 -0
  278. package/src/code-execution/isolate/prelude.ts +409 -0
  279. package/src/code-execution/isolate/protocol.ts +240 -0
  280. package/src/code-execution/isolate/render.ts +230 -0
  281. package/src/code-execution/isolate/runner.ts +911 -0
  282. package/src/code-execution/isolate/scripts.ts +479 -0
  283. package/src/code-execution/quickjs.wasm +0 -0
  284. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  285. package/src/code-execution/tool-definition.ts +236 -167
  286. package/src/deadline-timer.ts +1 -1
  287. package/src/framework.ts +4978 -2671
  288. package/src/gate/event-gate.ts +79 -17
  289. package/src/held-writes.ts +17 -3
  290. package/src/index.ts +32 -22
  291. package/src/mcpl/channel-registry.ts +1892 -471
  292. package/src/mcpl/conversation-router.ts +26 -72
  293. package/src/mcpl/convert-block.ts +9 -5
  294. package/src/mcpl/inbound-source.ts +131 -0
  295. package/src/mcpl/index.ts +4 -2
  296. package/src/mcpl/log-value.ts +9 -0
  297. package/src/mcpl/modern-connection.ts +162 -53
  298. package/src/mcpl/prose-grammar.ts +4 -5
  299. package/src/mcpl/protocol-family.ts +200 -22
  300. package/src/mcpl/push-coalescer.ts +306 -62
  301. package/src/mcpl/push-handler.ts +12 -54
  302. package/src/mcpl/reference-fetcher.ts +4 -4
  303. package/src/mcpl/server-connection.ts +180 -79
  304. package/src/mcpl/tags.ts +36 -0
  305. package/src/mcpl/tool-result-normalize.ts +2 -1
  306. package/src/mcpl/transport.ts +162 -33
  307. package/src/mcpl/types.ts +23 -6
  308. package/src/module-registry.ts +92 -13
  309. package/src/modules/discord/index.ts +8 -13
  310. package/src/modules/history/index.ts +25 -3
  311. package/src/modules/history/semantic.ts +29 -6
  312. package/src/modules/index.ts +1 -0
  313. package/src/modules/unknown-tool.ts +4 -1
  314. package/src/modules/workspace/branch-intent.ts +3 -1
  315. package/src/modules/workspace/edit-plan.ts +954 -0
  316. package/src/modules/workspace/edit-text.ts +618 -0
  317. package/src/modules/workspace/effects.ts +70 -19
  318. package/src/modules/workspace/index.ts +2293 -650
  319. package/src/modules/workspace/observe.ts +465 -56
  320. package/src/modules/workspace/reconcile.ts +14 -7
  321. package/src/modules/workspace/text.ts +68 -0
  322. package/src/modules/workspace/types.ts +211 -6
  323. package/src/modules/workspace/watcher.ts +3 -2
  324. package/src/operator-log.ts +47 -19
  325. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  326. package/src/recovery/offline-branch.ts +189 -55
  327. package/src/recovery/recover-cli.ts +25 -4
  328. package/src/send-tools.ts +11 -0
  329. package/src/timezone.ts +4 -2
  330. package/src/tool-image-ledger.ts +62 -8
  331. package/src/tool-result-guard.ts +18 -15
  332. package/src/tool-result-history.ts +3 -2
  333. package/src/tune-out/coordinator.ts +166 -45
  334. package/src/types/agent.ts +32 -6
  335. package/src/types/events.ts +66 -9
  336. package/src/types/framework.ts +69 -62
  337. package/src/types/module.ts +40 -8
  338. package/src/types/trace.ts +121 -15
  339. package/src/usage/types.ts +16 -0
  340. package/src/usage/usage-tracker.ts +86 -43
  341. package/dist/src/code-execution/py-runner.d.ts +0 -191
  342. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  343. package/dist/src/code-execution/py-runner.js +0 -655
  344. package/dist/src/code-execution/py-runner.js.map +0 -1
  345. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  346. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  347. package/dist/src/code-execution/runtime-py.js +0 -677
  348. package/dist/src/code-execution/runtime-py.js.map +0 -1
  349. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  350. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  351. package/dist/src/mcpl/prose-stream-router.js +0 -291
  352. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  353. package/src/code-execution/py-runner.ts +0 -791
  354. package/src/code-execution/runtime-py.ts +0 -677
  355. package/src/mcpl/prose-stream-router.ts +0 -268
package/src/agent.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type StoredRowLike,
20
20
  } from './silent-heartbeat.js';
21
21
  import { randomUUID } from 'node:crypto';
22
+ import { resolveClockConfig, type ResolvedClockConfig } from './clock-rows.js';
22
23
  import {
23
24
  nonErrorResultText,
24
25
  truncateForHistory,
@@ -167,6 +168,10 @@ export class Agent {
167
168
  readonly proseRouting: 'locus' | 'explicit' | 'hybrid' | 'disabled';
168
169
  /** Exact whole-response known-tool wrapper containment (default off). */
169
170
  readonly toolWrapperProseGuard: boolean;
171
+ /** Clock rows' interval and precision (see AgentConfig.clock); undefined
172
+ * when the agent writes none of its own (a resident still reads those
173
+ * another resident writes into the shared slot). */
174
+ readonly clock: ResolvedClockConfig | undefined;
170
175
  /** Prompt-cache TTL forwarded to the provider (see AgentConfig.cacheTtl). */
171
176
  readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
172
177
  /** Emit prompt-cache markers on requests (see AgentConfig.promptCaching). */
@@ -296,6 +301,7 @@ export class Agent {
296
301
  this.toolResultGuard = new ToolResultGuard(config.name, contextManager, config.toolResultGuard === true);
297
302
  this.proseRouting = config.proseRouting ?? 'locus';
298
303
  this.toolWrapperProseGuard = config.toolWrapperProseGuard === true;
304
+ this.clock = resolveClockConfig(config.name, config.clock);
299
305
  this.cacheTtl = config.cacheTtl ?? '1h';
300
306
  this.promptCaching = config.promptCaching !== false;
301
307
  this.prefillUserMessage = config.prefillUserMessage;
@@ -924,28 +930,89 @@ export class Agent {
924
930
  }
925
931
 
926
932
  /**
927
- * RFC-006 consumed watermark: the newest stored message at the last compile
928
- * of a model request, or at the last tool boundary that injected messages
929
- * into a live one, per branch. Everything above it has never been in a
930
- * request and may still be replaced or withdrawn in place by its sender.
933
+ * RFC-006 consumed watermark, per branch: the newest stored message as of
934
+ * the latest of the agent's creation, a committing compile that returns,
935
+ * and a tool boundary that injected messages into a live stream declaring
936
+ * it carries them (`supportsInjectedMessages` true: membrane's native
937
+ * path, from membrane 0.6.0). A compile counts from when it takes its
938
+ * value, before it awaits, so one that began before a boundary's mark
939
+ * doesn't roll the mark back (applyCompileWatermark), and a dry run, such
940
+ * as previewActivation's, doesn't move it. Each move takes the store's
941
+ * head, a scalar, so it also covers stored messages the agent wasn't
942
+ * given, such as another slot's.
943
+ *
944
+ * A message above it may still be replaced or withdrawn in place by its
945
+ * sender, though it may already have been in one of the agent's requests.
946
+ * A live stream carries each round's own content and results, which it
947
+ * stores at its tool boundaries, into its next request. It can also carry
948
+ * the messages injected at a boundary whose stream didn't say whether it
949
+ * carries injections, as no membrane 0.5.x stream does: its native path
950
+ * carries them and its XML path drops them, and either way they stay
951
+ * above it.
931
952
  */
932
953
  private consumedWatermark: { branch: string; sequence: number } | null = null;
933
- /** Counts marks: a compile writes its watermark back only if no mark was
934
- * made while it ran (a tool boundary's, or another compile's). */
954
+ /** Counts marks (markContextConsumed's and applyCompileWatermark's), so a
955
+ * compile that finishes can tell whether one was made while it ran. */
935
956
  private watermarkMarks = 0;
957
+ /**
958
+ * The newest stored message a compile or a tool boundary may have put in
959
+ * front of the agent, per branch. It moves where the consumed watermark
960
+ * does, and sooner: when a committing compile takes its value, before it
961
+ * awaits, and at every tool boundary that injects, whether or not the
962
+ * stream says it carries injected messages. A message above it may still
963
+ * have been shown, though: for one, a live stream carries each round's own
964
+ * content and results, stored at a boundary that injected nothing, into its
965
+ * next request. A compile that throws assembled nothing, and gives its move
966
+ * back where it can (unshowFailedCompile). A scalar counts a prefix, so a
967
+ * message stored below it without having been shown counts as shown, which
968
+ * errs toward a marker.
969
+ */
970
+ private shownWatermark: { branch: string; sequence: number } | null = null;
936
971
 
937
972
  getConsumedWatermark(): { branch: string; sequence: number } | null {
938
973
  return this.consumedWatermark;
939
974
  }
940
975
 
976
+ getShownWatermark(): { branch: string; sequence: number } | null {
977
+ return this.shownWatermark;
978
+ }
979
+
941
980
  /** Mark everything currently stored as consumed (the agent's creation, at
942
- * boot or later, and a tool boundary that injected messages into the live
943
- * stream). */
981
+ * boot or later, and a tool boundary that injected messages into a live
982
+ * stream declaring it carries them; see consumedWatermark). */
944
983
  markContextConsumed(): void {
945
984
  // Tolerates partial context-manager doubles (tests build Agents over
946
- // stubs): without a watermark nothing is ever treated as unread.
985
+ // stubs). Without a watermark, a reader that looks for one takes its own
986
+ // side: the coalescer counts the message as read, and the framework's
987
+ // deferredWriteReadOrGone and unreadNoticesForPrimary count it as unread.
947
988
  this.consumedWatermark = this.pendingWatermark();
948
989
  this.watermarkMarks++;
990
+ this.shownWatermark = this.consumedWatermark;
991
+ }
992
+
993
+ /** Mark everything currently stored as possibly shown (a tool boundary
994
+ * that injected messages, whatever its stream says it carries). Without
995
+ * a watermark the coalescer counts a message as shown, as it counts one
996
+ * read without a consumed watermark. */
997
+ markContextShown(): void {
998
+ this.shownWatermark = this.pendingWatermark();
999
+ }
1000
+
1001
+ /** A committing compile that threw assembled nothing, so its own move of
1002
+ * the shown watermark goes back, unless a boundary or another compile has
1003
+ * moved the watermark since. It goes back to the watermark as the compile
1004
+ * found it, which can be another compile's claim that has failed as well.
1005
+ * Of two overlapping committing compiles, if the first to start throws
1006
+ * first, it gives nothing back (the second's claim is current), and if
1007
+ * the second then throws, it restores the first's failed claim. Messages
1008
+ * up to that claim then count as shown, although no request held those
1009
+ * above the watermark the first compile found, which errs toward a marker
1010
+ * until the next compile or boundary moves the watermark. */
1011
+ private unshowFailedCompile(
1012
+ watermark: { branch: string; sequence: number } | null,
1013
+ before: { branch: string; sequence: number } | null,
1014
+ ): void {
1015
+ if (watermark && this.shownWatermark === watermark) this.shownWatermark = before;
949
1016
  }
950
1017
 
951
1018
  /**
@@ -953,7 +1020,9 @@ export class Agent {
953
1020
  * the watermark only advances: if a mark was made while the compile ran,
954
1021
  * the compile's (older) one is applied only where it is later on that
955
1022
  * mark's branch, so a boundary's mark is never rolled back by a compile
956
- * that started before it (a preview during a live turn).
1023
+ * that started before it (a committing compile, such as compileContext's,
1024
+ * run during a live turn; a dry run, such as previewActivation's, never
1025
+ * reaches here).
957
1026
  */
958
1027
  private applyCompileWatermark(watermark: { branch: string; sequence: number }, marksBefore: number): void {
959
1028
  const current = this.consumedWatermark;
@@ -993,8 +1062,16 @@ export class Agent {
993
1062
  // fails assembled nothing, so the watermark moves only on success.
994
1063
  const watermark = this.pendingWatermark();
995
1064
  const marksBefore = this.watermarkMarks;
1065
+ const shownBefore = this.shownWatermark;
1066
+ if (watermark) this.shownWatermark = watermark;
996
1067
  const compile = () => this.contextManager.compile(this.compileBudget(budget));
997
- const result = budget ? await compile() : await this.compileAtLiveBudget(compile);
1068
+ let result: CompileResult;
1069
+ try {
1070
+ result = budget ? await compile() : await this.compileAtLiveBudget(compile);
1071
+ } catch (error) {
1072
+ this.unshowFailedCompile(watermark, shownBefore);
1073
+ throw error;
1074
+ }
998
1075
  if (watermark) this.applyCompileWatermark(watermark, marksBefore);
999
1076
  return result;
1000
1077
  }
@@ -1035,8 +1112,18 @@ export class Agent {
1035
1112
  // any compile before it selects.
1036
1113
  const watermark = opts?.dryRun ? null : this.pendingWatermark();
1037
1114
  const marksBefore = this.watermarkMarks;
1115
+ // Shown from the compile's start: what it assembles is in front of the
1116
+ // agent before the consumed watermark moves, once it returns.
1117
+ const shownBefore = this.shownWatermark;
1118
+ if (watermark) this.shownWatermark = watermark;
1038
1119
  const compile = () => this.contextManager.compile(this.compileBudget(budget), injections, opts as never);
1039
- const result = budget || opts?.dryRun ? await compile() : await this.compileAtLiveBudget(compile);
1120
+ let result: CompileResult;
1121
+ try {
1122
+ result = budget || opts?.dryRun ? await compile() : await this.compileAtLiveBudget(compile);
1123
+ } catch (error) {
1124
+ this.unshowFailedCompile(watermark, shownBefore);
1125
+ throw error;
1126
+ }
1040
1127
  if (watermark) this.applyCompileWatermark(watermark, marksBefore);
1041
1128
  return result;
1042
1129
  }
package/src/api/server.ts CHANGED
@@ -489,6 +489,12 @@ export class ApiServer {
489
489
  if (!params.participant || !params.content) {
490
490
  throw new Error('participant and content are required');
491
491
  }
492
+ // ApiModule stores the message as one text block, which context-manager
493
+ // refuses unless its text is a string, so content that isn't text is
494
+ // refused here, where the caller hears it, rather than at dispatch.
495
+ if (typeof params.content !== 'string') {
496
+ throw new Error('content must be text');
497
+ }
492
498
 
493
499
  // Push api:message event for the ApiModule to handle
494
500
  this.framework.pushEvent({
@@ -587,14 +593,14 @@ export class ApiServer {
587
593
 
588
594
  const from = this.currentBranch;
589
595
 
590
- // The framework switches the store, starts the delivery of its Discord
591
- // awareness marks behind the data-plane gate, and re-materializes the
592
- // config mount. It refuses during a live turn or once stop() has begun,
593
- // and stop() waits for one already running. It moves the store once any
594
- // maintenance already running has finished, so the move can come after
595
- // this call returns. The event names this switch's own move, from the
596
- // branch read here to the one it was asked for: another switch can move
597
- // the store again before this one continues.
596
+ // The framework switches the store and re-materializes the config mount;
597
+ // a branch switch adds or removes no Discord awareness marks. It refuses
598
+ // during a live turn or once stop() has begun, and stop() waits for one
599
+ // already running. It moves the store once any maintenance already
600
+ // running has finished, so the move can come after this call returns.
601
+ // The event names this switch's own move, from the branch read here to
602
+ // the one it was asked for: another switch can move the store again
603
+ // before this one continues.
598
604
  let switched = false;
599
605
  try {
600
606
  await this.framework.switchBranch(params.name);
@@ -647,8 +653,6 @@ export class ApiServer {
647
653
  const result = this.framework.undoLastTurn(params.agentName);
648
654
 
649
655
  if (result.undone) {
650
- // Marks follow behind the data-plane gate; the undo doesn't wait for Discord.
651
- this.framework.startDiscordAwarenessDelivery('undo', params.agentName);
652
656
  this.broadcast('branch:switched', {
653
657
  from: result.fromBranch,
654
658
  to: result.toBranch,
@@ -667,8 +671,6 @@ export class ApiServer {
667
671
  const result = this.framework.redo(params.agentName);
668
672
 
669
673
  if (result.redone) {
670
- // Marks follow behind the data-plane gate; the redo doesn't wait for Discord.
671
- this.framework.startDiscordAwarenessDelivery('redo', params.agentName);
672
674
  this.broadcast('branch:switched', {
673
675
  from: result.fromBranch,
674
676
  to: result.toBranch,
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Clock rows: a time marker an agent's window takes when an interval
3
+ * boundary has passed since the last one it holds.
4
+ *
5
+ * The model is a tick event at every boundary of the agent's local wall
6
+ * clock (every 10 minutes, say, from local midnight). The ticks aren't
7
+ * written as they fire. One is written when the window next takes something
8
+ * (an incoming event, a tool round's results, the start of a turn), and only
9
+ * the latest of the ticks that fired meanwhile: nothing was written between
10
+ * them, and a clock wakes no one, so the window reads exactly as it would
11
+ * had every tick been written and the earlier ones coalesced away.
12
+ *
13
+ * A clock never lies about order. One that lands where its tick would have
14
+ * been (the row before it is no newer than the boundary) reads the
15
+ * boundary's time, which is in the past by the time it is written. One that
16
+ * can't (the boundary passed while the agent was mid-reply or a tool ran:
17
+ * a tool's results must follow its call directly, so nothing can be written
18
+ * between them) reads the time it is written instead.
19
+ *
20
+ * Everything the next clock needs is read back from the window: the last
21
+ * clock row's time, wherever it is, and the newest row's. A window that
22
+ * holds no clock row takes one the next time it would take a clock (an
23
+ * incoming event, a turn's start, a tool boundary), so a window that has
24
+ * just had a clock turned on, or a fork seeded with copies of its
25
+ * template's rows (which carry none of their metadata), reads the time
26
+ * then. A restart, a rollback or a second resident sharing the window
27
+ * leaves nothing stale to reconcile.
28
+ */
29
+
30
+ import type { ContentBlock } from '@animalabs/membrane';
31
+ import { resolveTimeZone } from './timezone.js';
32
+
33
+ /** `AgentConfig.clock`: when the agent's window takes clock rows. */
34
+ export interface ClockConfig {
35
+ /** Minutes between boundaries. Give this or `intervalSeconds`. */
36
+ intervalMinutes?: number;
37
+ /** Seconds between boundaries, for clocks finer than a minute. */
38
+ intervalSeconds?: number;
39
+ /**
40
+ * How a clock reads: `'minutes'` (`10:50`) or `'seconds'` (`10:50:30`).
41
+ * Defaults to minutes for an interval of whole minutes, else seconds.
42
+ */
43
+ precision?: 'minutes' | 'seconds';
44
+ }
45
+
46
+ export interface ResolvedClockConfig {
47
+ intervalSeconds: number;
48
+ precision: 'minutes' | 'seconds';
49
+ }
50
+
51
+ /** `metadata.kind` of a clock row. */
52
+ export const CLOCK_ROW_KIND = 'clock';
53
+
54
+ /** `metadata.clock` of a clock row. */
55
+ export interface ClockStamp {
56
+ /** The instant the row reads, epoch ms. */
57
+ at: number;
58
+ /** Whether that instant is the boundary itself (else, when it was written). */
59
+ onBoundary: boolean;
60
+ /** The IANA zone it reads in. */
61
+ zone: string;
62
+ }
63
+
64
+ const DAY_SECONDS = 86_400;
65
+
66
+ /**
67
+ * Validate `AgentConfig.clock`. Undefined when the agent has no clock; throws
68
+ * on a setting that can't be read as one, naming the agent.
69
+ */
70
+ export function resolveClockConfig(agentName: string, config: ClockConfig | undefined): ResolvedClockConfig | undefined {
71
+ if (config === undefined) return undefined;
72
+ const at = `Agent "${agentName}" clock`;
73
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
74
+ throw new Error(`${at} must be an object such as { intervalMinutes: 10 }, or left out for no clock rows of its own`);
75
+ }
76
+ const { intervalMinutes, intervalSeconds, precision } = config;
77
+ if ((intervalMinutes === undefined) === (intervalSeconds === undefined)) {
78
+ throw new Error(`${at} takes exactly one of intervalMinutes and intervalSeconds`);
79
+ }
80
+ const given = intervalMinutes !== undefined ? intervalMinutes : intervalSeconds;
81
+ if (typeof given !== 'number' || !Number.isInteger(given) || given <= 0) {
82
+ throw new Error(
83
+ `${at}.${intervalMinutes !== undefined ? 'intervalMinutes' : 'intervalSeconds'} must be a positive whole number`,
84
+ );
85
+ }
86
+ const seconds = intervalMinutes !== undefined ? given * 60 : given;
87
+ if (seconds > DAY_SECONDS) {
88
+ throw new Error(`${at} interval is at most a day (1440 minutes); boundaries count from local midnight`);
89
+ }
90
+ if (precision !== undefined && precision !== 'minutes' && precision !== 'seconds') {
91
+ throw new Error(`${at}.precision must be 'minutes' or 'seconds'`);
92
+ }
93
+ const resolved = precision ?? (seconds % 60 === 0 ? 'minutes' : 'seconds');
94
+ if (resolved === 'minutes' && seconds % 60 !== 0) {
95
+ throw new Error(`${at}: an interval of ${seconds} seconds isn't whole minutes, so its clocks need precision 'seconds'`);
96
+ }
97
+ return { intervalSeconds: seconds, precision: resolved };
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Wall-clock arithmetic in a zone
102
+ // ---------------------------------------------------------------------------
103
+
104
+ interface WallFields {
105
+ year: number;
106
+ month: number;
107
+ day: number;
108
+ hour: number;
109
+ minute: number;
110
+ second: number;
111
+ /** Offset from UTC at this instant, in minutes. */
112
+ offsetMinutes: number;
113
+ }
114
+
115
+ const formatters = new Map<string, Intl.DateTimeFormat>();
116
+
117
+ function formatterFor(zone: string): Intl.DateTimeFormat {
118
+ let formatter = formatters.get(zone);
119
+ if (!formatter) {
120
+ formatter = new Intl.DateTimeFormat('en-CA', {
121
+ timeZone: zone,
122
+ year: 'numeric',
123
+ month: '2-digit',
124
+ day: '2-digit',
125
+ hour: '2-digit',
126
+ minute: '2-digit',
127
+ second: '2-digit',
128
+ hourCycle: 'h23',
129
+ });
130
+ formatters.set(zone, formatter);
131
+ }
132
+ return formatter;
133
+ }
134
+
135
+ function wallFields(at: number, zone: string): WallFields {
136
+ const parts = formatterFor(zone).formatToParts(new Date(at));
137
+ const get = (type: Intl.DateTimeFormatPartTypes): number =>
138
+ Number(parts.find((part) => part.type === type)?.value ?? NaN);
139
+ const fields = {
140
+ year: get('year'),
141
+ month: get('month'),
142
+ day: get('day'),
143
+ hour: get('hour'),
144
+ minute: get('minute'),
145
+ second: get('second'),
146
+ };
147
+ const shownAsUtc = Date.UTC(fields.year, fields.month - 1, fields.day, fields.hour, fields.minute, fields.second);
148
+ const offsetMinutes = Math.round((shownAsUtc - Math.floor(at / 1000) * 1000) / 60_000);
149
+ return { ...fields, offsetMinutes };
150
+ }
151
+
152
+ /**
153
+ * The latest boundary at or before `now`: an instant at which the zone's
154
+ * wall clock read a whole multiple of the interval since local midnight. A
155
+ * boundary whose wall time a DST change skips doesn't occur; one it repeats
156
+ * occurs twice.
157
+ *
158
+ * While the zone's offset stays the same, its wall clock runs with real
159
+ * time, so the latest boundary is `now` less how far the wall clock has run
160
+ * past its last multiple of the interval. Where that instant has another
161
+ * offset, the offset changed in between and no boundary falls between the
162
+ * change and `now`: the walk goes on from just before the change. The change
163
+ * is found by searching between the two instants, whatever their offsets
164
+ * are. The same offset at both is taken to mean no change between them: in
165
+ * the tz data, no zone changes its offset twice within a day, the longest
166
+ * interval, from 2024 through 2040. Throws if the walk passes 24 changes,
167
+ * which no real zone reaches, rather than return an instant that isn't a
168
+ * boundary.
169
+ */
170
+ export function latestBoundary(now: number, intervalSeconds: number, zone: string): number {
171
+ let at = now;
172
+ // A pass that doesn't return steps back past an offset change, and a day
173
+ // of a real zone holds at most two of them; running out of passes is a
174
+ // defect, and it throws rather than return an instant that isn't a boundary.
175
+ for (let pass = 0; pass < 24; pass++) {
176
+ const f = wallFields(at, zone);
177
+ const secondOfDay = f.hour * 3600 + f.minute * 60 + f.second;
178
+ const subSecond = at - Math.floor(at / 1000) * 1000;
179
+ const candidate = at - (secondOfDay % intervalSeconds) * 1000 - subSecond;
180
+ if (wallFields(candidate, zone).offsetMinutes === f.offsetMinutes) return candidate;
181
+ at = offsetTakenAt(candidate, at, f.offsetMinutes, zone) - 1;
182
+ }
183
+ throw new Error(`no clock boundary found within 24 offset changes of ${new Date(now).toISOString()} in ${zone}`);
184
+ }
185
+
186
+ /**
187
+ * The instant in (`from`, `to`] at which the zone took `offset`, given that
188
+ * `to` reads at that offset and `from` doesn't, to the millisecond.
189
+ */
190
+ function offsetTakenAt(from: number, to: number, offset: number, zone: string): number {
191
+ let before = from;
192
+ let after = to;
193
+ while (after - before > 1) {
194
+ const mid = Math.floor((before + after) / 2);
195
+ if (wallFields(mid, zone).offsetMinutes === offset) after = mid;
196
+ else before = mid;
197
+ }
198
+ return after;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // What a clock reads
203
+ // ---------------------------------------------------------------------------
204
+
205
+ const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
206
+
207
+ const pad = (n: number): string => String(n).padStart(2, '0');
208
+
209
+ function localDate(at: number, zone: string): string {
210
+ const f = wallFields(at, zone);
211
+ return `${f.year}-${pad(f.month)}-${pad(f.day)}`;
212
+ }
213
+
214
+ function isUtcZone(zone: string): boolean {
215
+ return /^(Etc\/)?(UTC|UCT|Zulu|Universal|GMT|Greenwich)$/.test(zone) || zone === 'Etc/GMT0' || zone === 'GMT0';
216
+ }
217
+
218
+ /**
219
+ * A clock row's text: `Clock: 10:50 Europe/Berlin (UTC+02:00)`, with the
220
+ * weekday and date ahead of the time when `withDate`
221
+ * (`Clock: Thursday 2026-10-22, 00:00 Europe/Berlin (UTC+02:00)`).
222
+ */
223
+ export function formatClockText(
224
+ at: number,
225
+ zone: string,
226
+ precision: 'minutes' | 'seconds',
227
+ withDate: boolean,
228
+ ): string {
229
+ const f = wallFields(at, zone);
230
+ const time = precision === 'seconds'
231
+ ? `${pad(f.hour)}:${pad(f.minute)}:${pad(f.second)}`
232
+ : `${pad(f.hour)}:${pad(f.minute)}`;
233
+ const weekday = WEEKDAYS[new Date(Date.UTC(f.year, f.month - 1, f.day)).getUTCDay()];
234
+ const date = withDate ? `${weekday} ${f.year}-${pad(f.month)}-${pad(f.day)}, ` : '';
235
+ const sign = f.offsetMinutes < 0 ? '-' : '+';
236
+ const offset = Math.abs(f.offsetMinutes);
237
+ const where = isUtcZone(zone) && f.offsetMinutes === 0
238
+ ? 'UTC'
239
+ : `${zone} (UTC${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)})`;
240
+ return `Clock: ${date}${time} ${where}`;
241
+ }
242
+
243
+ /** A clock row to store. */
244
+ export interface PlannedClock {
245
+ content: ContentBlock[];
246
+ stamp: ClockStamp;
247
+ }
248
+
249
+ /**
250
+ * The clock row the window takes next, or null when no boundary has passed
251
+ * since the last clock it holds. The framework asks before an incoming
252
+ * event, at a turn's start and at a tool boundary (storeDueClock).
253
+ *
254
+ * - `newestRowAt`: the newest row's time, which the clock goes after.
255
+ * - `lastClockAt`: the time the window's last clock row reads, when it holds
256
+ * one. Without one, no boundary counts as told: the window takes a clock
257
+ * now, and it carries the date.
258
+ */
259
+ export function planClock(args: {
260
+ now: number;
261
+ newestRowAt: number | undefined;
262
+ lastClockAt: number | undefined;
263
+ config: ResolvedClockConfig;
264
+ zone: string;
265
+ }): PlannedClock | null {
266
+ const { now, newestRowAt, lastClockAt, config } = args;
267
+ const zone = resolveTimeZone(args.zone);
268
+ const boundary = latestBoundary(now, config.intervalSeconds, zone);
269
+ const told = lastClockAt !== undefined ? latestBoundary(lastClockAt, config.intervalSeconds, zone) : -Infinity;
270
+ if (boundary <= told) return null;
271
+ // The boundary's own time only where its tick would have been: no row
272
+ // after the boundary comes before the clock. Otherwise the time it is
273
+ // written, and never earlier than the row it follows.
274
+ const onBoundary = newestRowAt === undefined || newestRowAt <= boundary;
275
+ const at = onBoundary ? boundary : Math.max(now, newestRowAt);
276
+ const withDate = lastClockAt === undefined || localDate(lastClockAt, zone) !== localDate(at, zone);
277
+ return {
278
+ content: [{ type: 'text', text: formatClockText(at, zone, config.precision, withDate) }],
279
+ stamp: { at, onBoundary, zone },
280
+ };
281
+ }
282
+
283
+ /** The clock stamp a stored row's metadata carries, if it is a clock row. */
284
+ export function clockStampOf(metadata: unknown): ClockStamp | undefined {
285
+ const m = metadata as { kind?: unknown; clock?: { at?: unknown } } | undefined;
286
+ if (m?.kind !== CLOCK_ROW_KIND || typeof m.clock?.at !== 'number') return undefined;
287
+ return m.clock as ClockStamp;
288
+ }
289
+
290
+ /** Whether a row can have a clock written before it: not the agent's own
291
+ * (an assistant turn), no tool blocks (a tool's results follow its call
292
+ * directly), and not itself a clock. */
293
+ export function takesClockBefore(
294
+ participant: string,
295
+ content: readonly ContentBlock[],
296
+ metadata: unknown,
297
+ agentName: string,
298
+ ): boolean {
299
+ return participant !== agentName
300
+ && !content.some((b) => b.type === 'tool_use' || b.type === 'tool_result')
301
+ && clockStampOf(metadata) === undefined;
302
+ }