@north-light/crouter 0.3.312 → 0.3.314

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 (497) hide show
  1. package/dist/api/client.d.ts +27 -30
  2. package/dist/api/client.js +80 -67
  3. package/dist/api/dto/broker-ops.d.ts +10 -20
  4. package/dist/api/dto/canvas.d.ts +1 -10
  5. package/dist/api/dto/health.d.ts +113 -0
  6. package/dist/api/dto/human-requests.d.ts +14 -3
  7. package/dist/api/dto/human.d.ts +2 -48
  8. package/dist/api/dto/human.js +3 -2
  9. package/dist/api/dto/inbox.d.ts +14 -0
  10. package/dist/api/dto/mail.d.ts +43 -0
  11. package/dist/api/dto/nodes.d.ts +11 -8
  12. package/dist/api/dto/recovery.d.ts +96 -0
  13. package/dist/api/dto/reviews.d.ts +2 -2
  14. package/dist/api/routes.d.ts +9 -6
  15. package/dist/api/routes.js +11 -11
  16. package/dist/build-root.d.ts +4 -0
  17. package/dist/build-root.js +11 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +35 -47
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +1 -1
  20. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +12 -5
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
  22. package/dist/cli.js +10 -1
  23. package/dist/clients/__tests__/helpers/human-daemon.d.ts +11 -0
  24. package/dist/clients/__tests__/helpers/human-daemon.js +60 -0
  25. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  26. package/dist/clients/attach/chrome/inbox-strip.d.ts +3 -3
  27. package/dist/clients/attach/chrome/inbox-strip.js +17 -9
  28. package/dist/clients/attach/chrome/ticket-panel.d.ts +2 -2
  29. package/dist/clients/attach/chrome/ticket-panel.js +77 -46
  30. package/dist/clients/attach/overlays/graph.js +6 -9
  31. package/dist/clients/attach/render/chat-view.d.ts +5 -4
  32. package/dist/clients/attach/render/chat-view.js +22 -13
  33. package/dist/clients/attach/render/page-block.d.ts +11 -7
  34. package/dist/clients/attach/render/page-block.js +64 -55
  35. package/dist/clients/attach/viewer.js +757 -746
  36. package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +56 -56
  37. package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +45 -32
  38. package/dist/clients/inbox/controller.d.ts +42 -14
  39. package/dist/clients/inbox/controller.js +240 -124
  40. package/dist/clients/inbox/dto-adapt.d.ts +28 -0
  41. package/dist/clients/inbox/dto-adapt.js +93 -0
  42. package/dist/clients/inbox/page-adapter.d.ts +5 -4
  43. package/dist/clients/inbox/page-adapter.js +12 -12
  44. package/dist/clients/inbox/resolve.d.ts +10 -5
  45. package/dist/clients/inbox/resolve.js +13 -12
  46. package/dist/clients/inbox/review/review-client.d.ts +0 -1
  47. package/dist/clients/inbox/review/review-client.js +0 -4
  48. package/dist/clients/inbox/review-adapter.d.ts +2 -1
  49. package/dist/clients/inbox/review-adapter.js +3 -5
  50. package/dist/clients/inbox/surface.d.ts +2 -2
  51. package/dist/clients/inbox/surface.js +19 -8
  52. package/dist/clients/inbox/tui.d.ts +3 -3
  53. package/dist/commands/__tests__/human.test.js +31 -15
  54. package/dist/commands/__tests__/integration/dashboard-snapshot.test.js +36 -11
  55. package/dist/commands/{sys/__tests__ → __tests__/integration}/migrate.test.js +142 -36
  56. package/dist/commands/__tests__/revive-now-gate.test.js +2 -0
  57. package/dist/commands/api-client.d.ts +5 -4
  58. package/dist/commands/api-client.js +13 -7
  59. package/dist/commands/canvas-history/shared.js +1 -1
  60. package/dist/commands/canvas-history.js +1 -1
  61. package/dist/commands/canvas-roster.js +1 -1
  62. package/dist/commands/canvas.js +1 -2
  63. package/dist/commands/human/feedback.js +20 -17
  64. package/dist/commands/human/prompts.js +45 -36
  65. package/dist/commands/human/queue.js +73 -123
  66. package/dist/commands/human/review.js +6 -16
  67. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +2 -0
  68. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +2 -0
  69. package/dist/commands/memory/__tests__/repository-root-lint.test.js +2 -0
  70. package/dist/commands/node-inspect-artifacts.js +1 -1
  71. package/dist/commands/node-lifecycle-revive.d.ts +2 -2
  72. package/dist/commands/node-lifecycle-revive.js +9 -19
  73. package/dist/commands/pkg/plugin-manage.js +0 -1
  74. package/dist/commands/profile/show.js +4 -12
  75. package/dist/commands/surface-tmux-spread.js +1 -5
  76. package/dist/commands/sys/__tests__/config-model-ladders.test.js +23 -30
  77. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +37 -11
  78. package/dist/commands/sys/branch.d.ts +2 -0
  79. package/dist/commands/sys/branch.js +19 -0
  80. package/dist/commands/sys/config.js +106 -33
  81. package/dist/commands/sys/daemon.d.ts +2 -0
  82. package/dist/commands/sys/daemon.js +130 -7
  83. package/dist/commands/sys/doctor.js +5 -15
  84. package/dist/commands/sys/migrate.js +21 -92
  85. package/dist/commands/sys/setup-settings-runner.js +4 -2
  86. package/dist/commands/sys.js +2 -15
  87. package/dist/core/__tests__/boot-fault-attempt.test.js +23 -1
  88. package/dist/core/__tests__/boot.test.js +32 -25
  89. package/dist/core/__tests__/broker-launch-candidates.test.js +49 -0
  90. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +149 -176
  91. package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +59 -10
  92. package/dist/core/__tests__/canvas-inbox-watcher.test.js +76 -20
  93. package/dist/core/__tests__/canvas.test.js +18 -47
  94. package/dist/core/__tests__/child-death-wake.test.js +29 -7
  95. package/dist/core/__tests__/cli-help-contract.test.js +2 -0
  96. package/dist/core/__tests__/close.test.js +29 -10
  97. package/dist/core/__tests__/collectible.test.js +2 -1
  98. package/dist/core/__tests__/config-change-delta.test.js +2 -0
  99. package/dist/core/__tests__/context-intro.test.js +13 -1
  100. package/dist/core/__tests__/cron-armed-context-failure.test.js +17 -12
  101. package/dist/core/__tests__/cron-held-settlement.test.js +21 -3
  102. package/dist/core/__tests__/cron-node-sink-parked-root.test.js +29 -10
  103. package/dist/core/__tests__/cron-self-cancel-delivery.test.js +14 -2
  104. package/dist/core/__tests__/cron-timeout-descendants.test.js +13 -1
  105. package/dist/core/__tests__/daemon-boot.test.js +51 -14
  106. package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
  107. package/dist/core/__tests__/fixtures/fake-engine.js +8 -3
  108. package/dist/core/__tests__/focuses.test.js +10 -5
  109. package/dist/core/__tests__/helpers/harness.js +50 -18
  110. package/dist/core/__tests__/history-inbox.test.js +12 -16
  111. package/dist/core/__tests__/human-action-delivery.test.js +48 -28
  112. package/dist/core/__tests__/human-actions.test.js +7 -3
  113. package/dist/core/__tests__/inline-memory-refs.test.js +7 -2
  114. package/dist/core/__tests__/integration/broker-fork-seam.test.js +37 -10
  115. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +42 -0
  116. package/dist/core/__tests__/integration/broker-snapshot-history.test.js +42 -29
  117. package/dist/core/__tests__/integration/broker-wedge-supervision.test.js +36 -35
  118. package/dist/core/__tests__/integration/command-plugins.test.js +15 -6
  119. package/dist/core/__tests__/integration/deferred-no-wake.test.js +113 -92
  120. package/dist/core/__tests__/integration/fresh-home-commands.test.js +49 -0
  121. package/dist/core/__tests__/integration/host-teardown-process-group.test.js +9 -5
  122. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +16 -3
  123. package/dist/core/__tests__/integration/plugin-revalidate.test.js +4 -0
  124. package/dist/core/__tests__/integration/profile-pause.test.js +4 -4
  125. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +9 -12
  126. package/dist/core/__tests__/integration/revive.test.js +67 -21
  127. package/dist/core/__tests__/integration/spawn-root.test.js +51 -8
  128. package/dist/core/__tests__/integration/subscription-delivery.test.js +33 -21
  129. package/dist/core/__tests__/integration/worktree-land.test.js +5 -2
  130. package/dist/core/__tests__/integration/worktree-reap.test.js +15 -1
  131. package/dist/core/__tests__/kickoff.test.js +36 -39
  132. package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +2 -1
  133. package/dist/core/__tests__/lifecycle.test.js +2 -1
  134. package/dist/core/__tests__/live-lifecycle-prompt.test.js +13 -1
  135. package/dist/core/__tests__/memory-resolver-precedence.test.js +2 -0
  136. package/dist/core/__tests__/migration.test.js +145 -31
  137. package/dist/core/__tests__/model-pin-durability.test.js +4 -1
  138. package/dist/core/__tests__/model-routes-config.test.js +13 -5
  139. package/dist/core/__tests__/node-outcome.test.js +93 -26
  140. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  141. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -1
  142. package/dist/core/__tests__/on-read-nested-store.test.js +2 -1
  143. package/dist/core/__tests__/passive-subscription.test.js +50 -157
  144. package/dist/core/__tests__/plugin-kinds.test.js +6 -4
  145. package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +6 -1
  146. package/dist/core/__tests__/plugin-page-components.test.js +6 -4
  147. package/dist/core/__tests__/preview-registry-sync.test.js +2 -0
  148. package/dist/core/__tests__/profile-project-memory-delivery.test.js +4 -1
  149. package/dist/core/__tests__/prune.test.js +11 -5
  150. package/dist/core/__tests__/push-final-guard.test.js +14 -7
  151. package/dist/core/__tests__/relaunch-root.test.js +30 -2
  152. package/dist/core/__tests__/remote-canvas-target.test.js +7 -1
  153. package/dist/core/__tests__/repository-root-identity.test.js +4 -0
  154. package/dist/core/__tests__/review-model-floor.test.js +6 -3
  155. package/dist/core/__tests__/revive-all.test.js +13 -3
  156. package/dist/core/__tests__/revive-capacity.test.js +26 -4
  157. package/dist/core/__tests__/revive-parked-fresh.test.js +31 -8
  158. package/dist/core/__tests__/seam/broker-attach-stream.test.js +1 -1
  159. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +4 -3
  160. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +11 -14
  161. package/dist/core/__tests__/seam/broker-provider-retry.test.js +85 -71
  162. package/dist/core/__tests__/seam/dormancy-release.test.js +33 -19
  163. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +7 -5
  164. package/dist/core/__tests__/seam/inbox-reference-guidance.test.js +20 -6
  165. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +27 -3
  166. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +2 -1
  167. package/dist/core/__tests__/stop-guard.test.js +2 -1
  168. package/dist/core/__tests__/subscription-pool-credential-guard.test.js +4 -3
  169. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +18 -4
  170. package/dist/core/__tests__/yield-ensures-daemon.test.js +2 -1
  171. package/dist/core/bootstrap.js +2 -0
  172. package/dist/core/broker-client/__tests__/transport-socket.test.js +51 -6
  173. package/dist/core/broker-client/transport-socket.d.ts +1 -0
  174. package/dist/core/broker-client/transport-socket.js +13 -5
  175. package/dist/core/canvas/__tests__/attention.test.js +29 -35
  176. package/dist/core/canvas/__tests__/history-transcript.test.js +6 -6
  177. package/dist/core/canvas/__tests__/render-remote.test.js +25 -9
  178. package/dist/core/canvas/attention.d.ts +2 -5
  179. package/dist/core/canvas/attention.js +15 -64
  180. package/dist/core/canvas/boot.d.ts +6 -7
  181. package/dist/core/canvas/boot.js +5 -9
  182. package/dist/core/canvas/canvas.d.ts +13 -109
  183. package/dist/core/canvas/canvas.js +140 -401
  184. package/dist/core/canvas/crons.d.ts +2 -0
  185. package/dist/core/canvas/crons.js +16 -15
  186. package/dist/core/canvas/daemon-owner.js +10 -0
  187. package/dist/core/canvas/db.d.ts +2 -2
  188. package/dist/core/canvas/db.js +2 -18
  189. package/dist/core/canvas/deliveries.d.ts +104 -0
  190. package/dist/core/canvas/deliveries.js +354 -0
  191. package/dist/core/canvas/executions.d.ts +29 -0
  192. package/dist/core/canvas/executions.js +68 -0
  193. package/dist/core/canvas/history.js +35 -35
  194. package/dist/core/canvas/index.d.ts +3 -1
  195. package/dist/core/canvas/index.js +3 -1
  196. package/dist/core/canvas/migrations.js +20 -12
  197. package/dist/core/canvas/nav-model.js +1 -3
  198. package/dist/core/canvas/nav-render.d.ts +4 -4
  199. package/dist/core/canvas/nav-render.js +11 -21
  200. package/dist/core/canvas/node-sql-migration.d.ts +3 -0
  201. package/dist/core/canvas/node-sql-migration.js +261 -0
  202. package/dist/core/canvas/paths.d.ts +2 -16
  203. package/dist/core/canvas/paths.js +2 -31
  204. package/dist/core/canvas/recovery.d.ts +76 -0
  205. package/dist/core/canvas/recovery.js +588 -0
  206. package/dist/core/canvas/remote-canvas-source.js +4 -0
  207. package/dist/core/canvas/render-source.d.ts +16 -27
  208. package/dist/core/canvas/render-source.js +16 -45
  209. package/dist/core/canvas/sessions.d.ts +33 -0
  210. package/dist/core/canvas/sessions.js +78 -0
  211. package/dist/core/canvas/types.d.ts +33 -33
  212. package/dist/core/canvas/types.js +2 -5
  213. package/dist/core/canvas/waits.d.ts +18 -0
  214. package/dist/core/canvas/waits.js +70 -0
  215. package/dist/core/canvas/worktrees.d.ts +40 -0
  216. package/dist/core/canvas/worktrees.js +93 -0
  217. package/dist/core/command.js +11 -0
  218. package/dist/core/config.d.ts +17 -12
  219. package/dist/core/config.js +106 -155
  220. package/dist/core/feed/__tests__/inbox-migrate.test.js +27 -31
  221. package/dist/core/feed/feed.d.ts +0 -6
  222. package/dist/core/feed/feed.js +68 -56
  223. package/dist/core/feed/inbox.d.ts +7 -81
  224. package/dist/core/feed/inbox.js +46 -378
  225. package/dist/core/feed/messages.d.ts +58 -0
  226. package/dist/core/feed/messages.js +217 -0
  227. package/dist/core/fs-utils.d.ts +4 -0
  228. package/dist/core/fs-utils.js +21 -1
  229. package/dist/core/human/answer.d.ts +1 -1
  230. package/dist/core/human/answer.js +1 -1
  231. package/dist/core/human/claim.d.ts +1 -1
  232. package/dist/core/human/claim.js +2 -4
  233. package/dist/core/human/component-docs.js +1 -1
  234. package/dist/core/human/feedback-companion.js +3 -2
  235. package/dist/core/human/feedback.d.ts +5 -8
  236. package/dist/core/human/feedback.js +57 -85
  237. package/dist/core/human/page-authoring.d.ts +30 -0
  238. package/dist/core/human/page-authoring.js +19 -0
  239. package/dist/core/human/requests.d.ts +136 -0
  240. package/dist/core/human/requests.js +515 -0
  241. package/dist/core/human/root.d.ts +0 -7
  242. package/dist/core/human/root.js +0 -17
  243. package/dist/core/human/scan.d.ts +2 -7
  244. package/dist/core/human/scan.js +48 -132
  245. package/dist/core/human/summary.d.ts +1 -1
  246. package/dist/core/human/summary.js +1 -1
  247. package/dist/core/human/tickets.d.ts +23 -83
  248. package/dist/core/human/tickets.js +92 -243
  249. package/dist/core/human/types.d.ts +5 -1
  250. package/dist/core/installed-plugins.js +8 -3
  251. package/dist/core/keybindings/__tests__/resolve.test.js +19 -17
  252. package/dist/core/locked-file.d.ts +6 -3
  253. package/dist/core/locked-file.js +36 -9
  254. package/dist/core/memory/extensions.js +2 -0
  255. package/dist/core/preview-registry.js +0 -1
  256. package/dist/core/profiles/__tests__/env-store.test.js +2 -0
  257. package/dist/core/profiles/default-binding.d.ts +0 -4
  258. package/dist/core/profiles/default-binding.js +12 -58
  259. package/dist/core/profiles/env-store.js +8 -3
  260. package/dist/core/profiles/manifest.d.ts +2 -37
  261. package/dist/core/profiles/manifest.js +35 -161
  262. package/dist/core/profiles/select.js +3 -0
  263. package/dist/core/review/__tests__/capture-origin.test.js +13 -2
  264. package/dist/core/review/__tests__/comments.test.js +28 -10
  265. package/dist/core/review/__tests__/remap.test.js +28 -10
  266. package/dist/core/review/__tests__/stage-identity.test.js +17 -7
  267. package/dist/core/review/birth.d.ts +1 -1
  268. package/dist/core/review/birth.js +34 -5
  269. package/dist/core/review/comments.js +1 -1
  270. package/dist/core/review/document.js +1 -1
  271. package/dist/core/review/project.d.ts +5 -1
  272. package/dist/core/review/project.js +14 -36
  273. package/dist/core/review/realize.d.ts +3 -6
  274. package/dist/core/review/realize.js +51 -168
  275. package/dist/core/review/remap.js +1 -1
  276. package/dist/core/review/stage.d.ts +3 -2
  277. package/dist/core/review/stage.js +2 -3
  278. package/dist/core/review/store.d.ts +3 -21
  279. package/dist/core/review/store.js +140 -186
  280. package/dist/core/review/types.d.ts +0 -1
  281. package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +9 -5
  282. package/dist/core/runtime/__tests__/spawn-env.test.js +8 -9
  283. package/dist/core/runtime/broker/auth-reload.js +9 -10
  284. package/dist/core/runtime/broker/daemon-ops.d.ts +25 -8
  285. package/dist/core/runtime/broker/daemon-ops.js +42 -9
  286. package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
  287. package/dist/core/runtime/broker/extension-abort.js +11 -0
  288. package/dist/core/runtime/broker/fault-retry.d.ts +15 -6
  289. package/dist/core/runtime/broker/fault-retry.js +142 -95
  290. package/dist/core/runtime/broker/inbox.d.ts +4 -32
  291. package/dist/core/runtime/broker/inbox.js +4 -92
  292. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  293. package/dist/core/runtime/broker/rebind.js +21 -12
  294. package/dist/core/runtime/broker-persona-guidance.d.ts +1 -1
  295. package/dist/core/runtime/broker-persona-guidance.js +2 -13
  296. package/dist/core/runtime/broker.js +28 -22
  297. package/dist/core/runtime/close.d.ts +3 -9
  298. package/dist/core/runtime/close.js +16 -27
  299. package/dist/core/runtime/fault.d.ts +3 -93
  300. package/dist/core/runtime/fault.js +9 -419
  301. package/dist/core/runtime/kickoff.d.ts +3 -6
  302. package/dist/core/runtime/kickoff.js +12 -18
  303. package/dist/core/runtime/launch.js +1 -1
  304. package/dist/core/runtime/lifecycle.js +40 -7
  305. package/dist/core/runtime/placement.js +2 -2
  306. package/dist/core/runtime/recycle.js +8 -2
  307. package/dist/core/runtime/reset.js +22 -6
  308. package/dist/core/runtime/revive.js +226 -218
  309. package/dist/core/runtime/spawn.js +14 -3
  310. package/dist/core/scoped-state/db.d.ts +18 -0
  311. package/dist/core/scoped-state/db.js +135 -0
  312. package/dist/core/scoped-state/migrate.d.ts +8 -0
  313. package/dist/core/scoped-state/migrate.js +181 -0
  314. package/dist/core/scoped-state/paths.d.ts +9 -0
  315. package/dist/core/scoped-state/paths.js +27 -0
  316. package/dist/core/scoped-state/profiles.d.ts +12 -0
  317. package/dist/core/scoped-state/profiles.js +62 -0
  318. package/dist/core/scoped-state/providers.d.ts +16 -0
  319. package/dist/core/scoped-state/providers.js +8 -0
  320. package/dist/core/scoped-state/schema.d.ts +3 -0
  321. package/dist/core/scoped-state/schema.js +26 -0
  322. package/dist/core/scoped-state/settings.d.ts +28 -0
  323. package/dist/core/scoped-state/settings.js +82 -0
  324. package/dist/core/subscription-state.d.ts +29 -48
  325. package/dist/core/subscription-state.js +176 -712
  326. package/dist/core/worktree-close.js +4 -4
  327. package/dist/core/worktree-mutation-async.js +2 -2
  328. package/dist/core/worktree-sweep.js +11 -16
  329. package/dist/core/worktree.js +6 -12
  330. package/dist/daemon/__tests__/autostart-storm.test.js +1 -39
  331. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +2 -2
  332. package/dist/daemon/__tests__/helpers/source-daemon.js +7 -6
  333. package/dist/daemon/__tests__/integration/migration-startup.test.js +59 -26
  334. package/dist/daemon/__tests__/node-deadline-lane.test.js +2 -1
  335. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +39 -15
  336. package/dist/daemon/__tests__/node-outcome-delivery.test.js +39 -10
  337. package/dist/daemon/__tests__/reconciler-signature.test.js +2 -2
  338. package/dist/daemon/__tests__/subtree-filter-freshness.test.js +2 -2
  339. package/dist/daemon/api/__tests__/broker-settle-park.test.js +17 -12
  340. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +27 -5
  341. package/dist/daemon/api/__tests__/model-config.test.js +8 -8
  342. package/dist/daemon/api/__tests__/node-create-description.test.js +22 -5
  343. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -1
  344. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +10 -12
  345. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -1
  346. package/dist/daemon/api/__tests__/profile-metadata-route.test.js +4 -1
  347. package/dist/daemon/api/__tests__/reopen-delivery.test.js +26 -8
  348. package/dist/daemon/api/__tests__/seam/api-server.test.js +24 -32
  349. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +4 -1
  350. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +5 -4
  351. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +27 -41
  352. package/dist/daemon/api/__tests__/submit-result.test.js +3 -2
  353. package/dist/daemon/api/bridge.js +1 -1
  354. package/dist/daemon/api/handlers/bash-jobs.js +6 -7
  355. package/dist/daemon/api/handlers/broker-ops.js +47 -84
  356. package/dist/daemon/api/handlers/broker-recovery.d.ts +4 -0
  357. package/dist/daemon/api/handlers/broker-recovery.js +251 -0
  358. package/dist/daemon/api/handlers/canvas.js +2 -44
  359. package/dist/daemon/api/handlers/daemon.js +41 -0
  360. package/dist/daemon/api/handlers/feedback-comments.js +2 -2
  361. package/dist/daemon/api/handlers/health.js +10 -5
  362. package/dist/daemon/api/handlers/human-requests.js +168 -188
  363. package/dist/daemon/api/handlers/human.js +3 -151
  364. package/dist/daemon/api/handlers/inbox-page.d.ts +4 -0
  365. package/dist/daemon/api/handlers/inbox-page.js +89 -0
  366. package/dist/daemon/api/handlers/inbox.js +21 -120
  367. package/dist/daemon/api/handlers/messages.d.ts +1 -0
  368. package/dist/daemon/api/handlers/messages.js +73 -15
  369. package/dist/daemon/api/handlers/node-outcomes.js +58 -13
  370. package/dist/daemon/api/handlers/nodes.js +7 -13
  371. package/dist/daemon/api/handlers/reviews.js +4 -29
  372. package/dist/daemon/api/map.d.ts +25 -3
  373. package/dist/daemon/api/map.js +84 -14
  374. package/dist/daemon/api/server.js +68 -9
  375. package/dist/daemon/control.d.ts +18 -0
  376. package/dist/daemon/cron/disposition.d.ts +2 -0
  377. package/dist/daemon/cron/disposition.js +8 -5
  378. package/dist/daemon/cron/settle.js +37 -13
  379. package/dist/daemon/cron/sinks.d.ts +18 -1
  380. package/dist/daemon/cron/sinks.js +47 -93
  381. package/dist/daemon/crtrd-cli.js +4 -19
  382. package/dist/daemon/crtrd.d.ts +2 -0
  383. package/dist/daemon/crtrd.js +206 -81
  384. package/dist/daemon/fleet.js +5 -6
  385. package/dist/daemon/human/finish.d.ts +12 -24
  386. package/dist/daemon/human/finish.js +88 -285
  387. package/dist/daemon/human/settle.d.ts +40 -0
  388. package/dist/daemon/human/settle.js +89 -0
  389. package/dist/daemon/human/sweep.d.ts +20 -5
  390. package/dist/daemon/human/sweep.js +56 -60
  391. package/dist/daemon/manage.d.ts +21 -16
  392. package/dist/daemon/manage.js +85 -51
  393. package/dist/daemon/messaging/node-message.d.ts +1 -1
  394. package/dist/daemon/messaging/node-message.js +9 -12
  395. package/dist/daemon/park-activity.js +3 -3
  396. package/dist/daemon/reconcilers/bash-deadline.js +6 -7
  397. package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -1
  398. package/dist/daemon/reconcilers/broker-supervision.js +41 -19
  399. package/dist/daemon/reconcilers/controller-death.js +6 -18
  400. package/dist/daemon/reconcilers/delivery-lane.d.ts +9 -0
  401. package/dist/daemon/reconcilers/delivery-lane.js +130 -0
  402. package/dist/daemon/reconcilers/live-obligation.js +3 -3
  403. package/dist/daemon/reconcilers/node-deadline.js +1 -1
  404. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +1 -1
  405. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +10 -12
  406. package/dist/daemon/reconcilers/node-lifecycle/tick.js +5 -5
  407. package/dist/daemon/reconcilers/node-lifecycle/wants-execution.js +1 -1
  408. package/dist/daemon/reconcilers/pending-review-submit.js +3 -3
  409. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -8
  410. package/dist/daemon/reconcilers/storage-maintenance.js +13 -43
  411. package/dist/daemon/review/companion.js +5 -11
  412. package/dist/daemon/review/deliver.d.ts +1 -5
  413. package/dist/daemon/review/deliver.js +3 -52
  414. package/dist/daemon/review/finish.d.ts +2 -27
  415. package/dist/daemon/review/finish.js +79 -263
  416. package/dist/daemon/review/sweep.d.ts +3 -10
  417. package/dist/daemon/review/sweep.js +13 -131
  418. package/dist/daemon/startup-policy.d.ts +0 -5
  419. package/dist/daemon/startup-policy.js +0 -5
  420. package/dist/index.d.ts +2 -1
  421. package/dist/index.js +1 -1
  422. package/dist/migrations/__tests__/activation-concurrency.test.js +3 -2
  423. package/dist/migrations/__tests__/activation.test.js +9 -6
  424. package/dist/migrations/activation.d.ts +3 -2
  425. package/dist/migrations/activation.js +2 -4
  426. package/dist/migrations/corpus.d.ts +2 -5
  427. package/dist/migrations/corpus.js +9 -20
  428. package/dist/migrations/runner.d.ts +3 -1
  429. package/dist/migrations/runner.js +6 -2
  430. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +21 -89
  431. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +4 -2
  432. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +51 -33
  433. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +16 -2
  434. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +13 -1
  435. package/dist/pi-extensions/canvas-inbox-watcher.js +98 -244
  436. package/dist/pi-extensions/canvas-passive-context.d.ts +4 -8
  437. package/dist/pi-extensions/canvas-passive-context.js +85 -37
  438. package/dist/pi-extensions/canvas-stophook.js +47 -31
  439. package/dist/shared/env.d.ts +1 -0
  440. package/dist/shared/env.js +3 -0
  441. package/package.json +1 -1
  442. package/runtime.lock.json +5 -5
  443. package/dist/clients/attach/__tests__/page-block.test.js +0 -54
  444. package/dist/clients/inbox/review-visibility.d.ts +0 -5
  445. package/dist/clients/inbox/review-visibility.js +0 -19
  446. package/dist/commands/canvas-rebuild-index.d.ts +0 -2
  447. package/dist/commands/canvas-rebuild-index.js +0 -46
  448. package/dist/core/__tests__/human-cancel-guard.test.js +0 -110
  449. package/dist/core/__tests__/human-deliver.test.js +0 -256
  450. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +0 -1
  451. package/dist/core/__tests__/human-node-not-supervised.test.js +0 -89
  452. package/dist/core/__tests__/integration/human-deliver-e2e.test.d.ts +0 -1
  453. package/dist/core/__tests__/integration/human-deliver-e2e.test.js +0 -131
  454. package/dist/core/__tests__/phase4-review-store.test.d.ts +0 -1
  455. package/dist/core/__tests__/phase4-review-store.test.js +0 -144
  456. package/dist/core/__tests__/watchdog-abort-arms-retry.test.d.ts +0 -1
  457. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +0 -110
  458. package/dist/core/canvas/human-deliveries.d.ts +0 -39
  459. package/dist/core/canvas/human-deliveries.js +0 -75
  460. package/dist/core/canvas/node-outcome-deliveries.d.ts +0 -64
  461. package/dist/core/canvas/node-outcome-deliveries.js +0 -159
  462. package/dist/core/feed/passive.d.ts +0 -3
  463. package/dist/core/feed/passive.js +0 -53
  464. package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +0 -1
  465. package/dist/core/human/__tests__/inbox-cancel-worker.js +0 -7
  466. package/dist/core/human/__tests__/inbox-claim-worker.d.ts +0 -1
  467. package/dist/core/human/__tests__/inbox-claim-worker.js +0 -17
  468. package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +0 -1
  469. package/dist/core/human/__tests__/inbox-stale-claim-worker.js +0 -8
  470. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +0 -1
  471. package/dist/core/human/__tests__/integration/inbox-core.test.js +0 -145
  472. package/dist/core/human/__tests__/page-tickets.test.d.ts +0 -1
  473. package/dist/core/human/__tests__/page-tickets.test.js +0 -161
  474. package/dist/core/review/ticket-filter.d.ts +0 -17
  475. package/dist/core/review/ticket-filter.js +0 -21
  476. package/dist/core/runtime/broker/passive.d.ts +0 -17
  477. package/dist/core/runtime/broker/passive.js +0 -44
  478. package/dist/core/runtime/busy.d.ts +0 -46
  479. package/dist/core/runtime/busy.js +0 -150
  480. package/dist/daemon/__tests__/startup-block-marker.test.d.ts +0 -1
  481. package/dist/daemon/__tests__/startup-block-marker.test.js +0 -86
  482. package/dist/daemon/api/__tests__/review-terminal-conflict.test.d.ts +0 -1
  483. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +0 -98
  484. package/dist/daemon/human/deliver-action.d.ts +0 -3
  485. package/dist/daemon/human/deliver-action.js +0 -9
  486. package/dist/daemon/node-outcome/deliver-outcome.d.ts +0 -3
  487. package/dist/daemon/node-outcome/deliver-outcome.js +0 -9
  488. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +0 -10
  489. package/dist/daemon/reconcilers/human-delivery-lane.js +0 -41
  490. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +0 -10
  491. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +0 -41
  492. package/dist/daemon/startup-block-marker.d.ts +0 -25
  493. package/dist/daemon/startup-block-marker.js +0 -99
  494. /package/dist/{clients/attach/__tests__/page-block.test.d.ts → api/dto/mail.js} +0 -0
  495. /package/dist/{commands/sys/__tests__/migrate.test.d.ts → api/dto/recovery.js} +0 -0
  496. /package/dist/{core/__tests__/human-cancel-guard.test.d.ts → commands/__tests__/integration/migrate.test.d.ts} +0 -0
  497. /package/dist/core/__tests__/{human-deliver.test.d.ts → integration/fresh-home-commands.test.d.ts} +0 -0
@@ -10,18 +10,21 @@ import { mkdirSync, rmSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import registerCanvasInboxWatcher from '../../pi-extensions/canvas-inbox-watcher.js';
13
- import { appendInbox, readCursor } from '../feed/inbox.js';
13
+ import { acknowledgedNodeMailCount, resetStaleExecutionMail } from '../canvas/deliveries.js';
14
+ import { beginExecution } from '../canvas/executions.js';
15
+ import { enqueueNodeMail } from '../feed/messages.js';
14
16
  import { drainBearings } from '../runtime/kickoff.js';
15
17
  import { appendSituationalContext } from '../runtime/situational-context.js';
16
18
  import { createEngineDriver } from '../runtime/broker/engine-drive.js';
17
19
  import { createTurnAdmissionGate } from '../runtime/broker/turn-admission.js';
18
20
  import { BROKER_IDLE_TURN_START, heldDeferredInboxPromptJoin, } from '../runtime/broker/held-deferred-inbox.js';
19
- import { createNode, getNode, recordPid, setIntent } from '../canvas/canvas.js';
21
+ import { createNode, getNode, setIntent } from '../canvas/canvas.js';
20
22
  import { createProfile, pauseProfile, resumeProfile } from '../profiles/manifest.js';
21
- import { closeDb } from '../canvas/db.js';
23
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
24
+ import { closeScopedStores } from '../scoped-state/db.js';
25
+ import { initializeFreshScopedStore } from '../scoped-state/migrate.js';
26
+ import { resetScopeCache } from '../scope.js';
22
27
  import { createApiServer } from '../../daemon/api/server.js';
23
- import { CrtrClient } from '../../api/client.js';
24
- import { ApiError } from '../../api/errors.js';
25
28
  // Drive the watcher's injectable cadence seam (CRTR_WATCHER_TICK_MS /
26
29
  // CRTR_WATCHER_DEBOUNCE_MS) at a fast tempo so the test sleeps milliseconds, not
27
30
  // seconds. SETTLE_MS still allows a resolve+seed tick, a read tick, and the
@@ -30,17 +33,28 @@ const TICK_MS = 20;
30
33
  const DEBOUNCE_MS = 25;
31
34
  const SETTLE_MS = TICK_MS * 2 + DEBOUNCE_MS + 200;
32
35
  let origHome;
36
+ let origUserHome;
33
37
  let origNode;
38
+ let origExecution;
34
39
  const homes = [];
35
40
  const disposers = [];
36
41
  let server;
37
- /** Point CRTR_HOME at a fresh temp canvas root and bind CRTR_NODE_ID. */
42
+ /** Bind each case to isolated canvas and user SQL state. */
38
43
  function freshNode(nodeId) {
39
- const home = join(tmpdir(), `crtr-w-${Math.random().toString(36).slice(2, 8)}`);
44
+ closeDb();
45
+ closeScopedStores();
46
+ resetScopeCache();
47
+ const root = join(tmpdir(), `crtr-w-${Math.random().toString(36).slice(2, 8)}`);
48
+ const home = join(root, 'home');
49
+ const canvas = join(root, 'canvas');
40
50
  mkdirSync(home, { recursive: true });
41
- homes.push(home);
42
- process.env['CRTR_HOME'] = home;
51
+ mkdirSync(canvas, { recursive: true });
52
+ homes.push(root);
53
+ process.env['HOME'] = home;
54
+ process.env['CRTR_HOME'] = canvas;
43
55
  process.env['CRTR_NODE_ID'] = nodeId;
56
+ initializeFreshScopedStore({ kind: 'user', root: join(home, '.crouter') });
57
+ migrate(openDb());
44
58
  server = createApiServer();
45
59
  }
46
60
  function makeFakePi() {
@@ -70,7 +84,8 @@ function makeFakePi() {
70
84
  // the one boundary where `hasPendingMessages()` still gates a commit.
71
85
  async function settle(pi, opts = {}) {
72
86
  const pending = opts.pending === true;
73
- await pi.fire('agent_settled', { type: 'agent_settled' }, { isIdle: () => true, hasPendingMessages: () => pending });
87
+ const idle = opts.idle ?? true;
88
+ await pi.fire('agent_settled', { type: 'agent_settled' }, { isIdle: () => idle, hasPendingMessages: () => pending });
74
89
  }
75
90
  // Fire session_shutdown — pi's graceful-teardown safety net for a settle that
76
91
  // never fired before exit. The real handler takes NO event ctx of its own; it
@@ -110,27 +125,48 @@ function ensureWatcherNode() {
110
125
  cwd: '/tmp',
111
126
  kind: 'general',
112
127
  mode: 'base',
113
- // Resident: these cases are about the watcher's own cursor/handoff/hold
114
- // contract at each tier as sent. A terminal node's mail is raised to the
115
- // steering tier on append (feed/inbox.ts), which would route every case
116
- // here as a steer.
117
128
  lifecycle: 'resident',
118
129
  status: 'active',
119
130
  });
120
- recordPid(nodeId, process.pid);
131
+ }
132
+ function publishMail(nodeId, input) {
133
+ const priorNode = process.env['CRTR_NODE_ID'];
134
+ process.env['CRTR_NODE_ID'] = nodeId;
135
+ ensureWatcherNode();
136
+ if (priorNode === undefined)
137
+ delete process.env['CRTR_NODE_ID'];
138
+ else
139
+ process.env['CRTR_NODE_ID'] = priorNode;
140
+ return enqueueNodeMail(nodeId, {
141
+ kind: input.kind,
142
+ label: input.label,
143
+ senderNodeId: input.from,
144
+ bodyRef: input.ref,
145
+ bodyJson: input.data,
146
+ disposition: input.disposition,
147
+ urgency: input.tier,
148
+ wake: 1,
149
+ });
150
+ }
151
+ function startWatcherExecution(nodeId) {
152
+ process.env['CRTR_EXECUTION_ID'] = beginExecution(nodeId, { daemonEpoch: 'test' });
153
+ resetStaleExecutionMail(Date.now());
121
154
  }
122
155
  function registerWatcher(pi) {
123
156
  ensureWatcherNode();
124
- recordPid(process.env['CRTR_NODE_ID'], process.pid);
157
+ const nodeId = process.env['CRTR_NODE_ID'];
158
+ startWatcherExecution(nodeId);
125
159
  const dispose = registerCanvasInboxWatcher(pi);
126
- void pi.fire('session_start', { type: 'session_start' }, undefined);
160
+ void server.ready.then(() => pi.fire('session_start', { type: 'session_start' }, undefined));
127
161
  return dispose;
128
162
  }
129
163
  let origTick;
130
164
  let origDebounce;
131
165
  before(() => {
132
166
  origHome = process.env['CRTR_HOME'];
167
+ origUserHome = process.env['HOME'];
133
168
  origNode = process.env['CRTR_NODE_ID'];
169
+ origExecution = process.env['CRTR_EXECUTION_ID'];
134
170
  origTick = process.env['CRTR_WATCHER_TICK_MS'];
135
171
  origDebounce = process.env['CRTR_WATCHER_DEBOUNCE_MS'];
136
172
  process.env['CRTR_WATCHER_TICK_MS'] = String(TICK_MS);
@@ -142,16 +178,26 @@ afterEach(async () => {
142
178
  await server?.close();
143
179
  server = undefined;
144
180
  closeDb();
181
+ closeScopedStores();
182
+ resetScopeCache();
145
183
  });
146
184
  after(() => {
147
185
  if (origHome === undefined)
148
186
  delete process.env['CRTR_HOME'];
149
187
  else
150
188
  process.env['CRTR_HOME'] = origHome;
189
+ if (origUserHome === undefined)
190
+ delete process.env['HOME'];
191
+ else
192
+ process.env['HOME'] = origUserHome;
151
193
  if (origNode === undefined)
152
194
  delete process.env['CRTR_NODE_ID'];
153
195
  else
154
196
  process.env['CRTR_NODE_ID'] = origNode;
197
+ if (origExecution === undefined)
198
+ delete process.env['CRTR_EXECUTION_ID'];
199
+ else
200
+ process.env['CRTR_EXECUTION_ID'] = origExecution;
155
201
  if (origTick === undefined)
156
202
  delete process.env['CRTR_WATCHER_TICK_MS'];
157
203
  else
@@ -160,6 +206,7 @@ after(() => {
160
206
  delete process.env['CRTR_WATCHER_DEBOUNCE_MS'];
161
207
  else
162
208
  process.env['CRTR_WATCHER_DEBOUNCE_MS'] = origDebounce;
209
+ resetScopeCache();
163
210
  for (const h of homes) {
164
211
  try {
165
212
  rmSync(h, { recursive: true, force: true });
@@ -174,7 +221,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
174
221
  const meta = {
175
222
  node_id: 'node-refresh', name: 'r', created: new Date().toISOString(),
176
223
  cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
177
- status: 'active', intent: 'refresh',
224
+ status: 'active',
178
225
  };
179
226
  createNode(meta);
180
227
  const pi = makeFakePi();
@@ -182,69 +229,16 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
182
229
  // Streaming (mid-turn) when a child finishes — normally this would steer.
183
230
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
184
231
  await wait(TICK_MS + 100);
185
- appendInbox('node-refresh', { from: 'child-x', tier: 'urgent', kind: 'final', label: 'done' });
232
+ publishMail('node-refresh', { from: 'child-x', tier: 'urgent', kind: 'final', label: 'done' });
186
233
  await waitForFirstInjection(pi);
187
- assert.equal(pi.injected.length, 1, 'the live broker receives the handoff before its durable cursor boundary');
188
- // The cursor operation decides the refresh hold at its durable commit
189
- // boundary; clearing intent does not manufacture a second handoff.
234
+ assert.equal(pi.injected.length, 1, 'the live broker receives the handoff before its delivery receipt');
235
+ // The delivery receipt decides the refresh hold; clearing intent does not
236
+ // manufacture a second handoff.
237
+ setIntent('node-refresh', 'refresh');
190
238
  setIntent('node-refresh', null);
191
239
  await wait(SETTLE_MS);
192
240
  assert.equal(pi.injected.length, 1, 'the same handoff is not delivered twice');
193
241
  });
194
- test('replacement session binding holds a late old-broker cursor commit', async () => {
195
- freshNode('node-refresh-replacement');
196
- closeDb();
197
- const meta = {
198
- node_id: 'node-refresh-replacement', name: 'r', created: new Date().toISOString(),
199
- cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
200
- status: 'active', intent: 'refresh',
201
- };
202
- createNode(meta);
203
- const entry = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff before replacement' });
204
- recordPid(meta.node_id, 101);
205
- const client = CrtrClient.forLocalSocket({ autostart: false });
206
- // The replacement binds first, consuming refresh and recording its own
207
- // process generation. The old broker's delayed request must still hold.
208
- await client.bindBrokerSession(meta.node_id, {
209
- piSessionId: 'replacement-session', sessionFile: '/tmp/replacement.jsonl',
210
- pid: 202, reason: 'startup', reviewBoundaryIds: [],
211
- });
212
- const oldResult = await client.advanceBrokerInboxCursor(meta.node_id, {
213
- throughEntryId: entry.entry_id, brokerPid: 101,
214
- });
215
- assert.deepEqual(oldResult, { action: 'hold_refresh' });
216
- assert.equal(readCursor(meta.node_id), undefined, 'the stale broker cannot consume the replacement replay');
217
- const replacementResult = await client.advanceBrokerInboxCursor(meta.node_id, {
218
- throughEntryId: entry.entry_id, brokerPid: 202,
219
- });
220
- assert.deepEqual(replacementResult, { action: 'advanced' });
221
- assert.equal(readCursor(meta.node_id), entry.entry_id, 'only the replacement broker advances the durable cursor');
222
- });
223
- test('cursor rejects a nonexistent or backward physical entry', async () => {
224
- freshNode('node-cursor-physical-boundary');
225
- closeDb();
226
- const meta = {
227
- node_id: 'node-cursor-physical-boundary', name: 'r', created: new Date().toISOString(),
228
- cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident', status: 'active',
229
- };
230
- createNode(meta);
231
- recordPid(meta.node_id, 303);
232
- const first = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'first' });
233
- const second = appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'second' });
234
- const client = CrtrClient.forLocalSocket({ autostart: false });
235
- const rejected = (request) => client
236
- .advanceBrokerInboxCursor(meta.node_id, request)
237
- .then(() => assert.fail('cursor request should fail'))
238
- .catch((error) => {
239
- assert.ok(error instanceof ApiError);
240
- assert.equal(error.status, 400);
241
- });
242
- await rejected({ throughEntryId: 'f'.repeat(32), brokerPid: 303 });
243
- assert.equal(readCursor(meta.node_id), undefined, 'a well-shaped unknown id cannot poison the cursor');
244
- await client.advanceBrokerInboxCursor(meta.node_id, { throughEntryId: second.entry_id, brokerPid: 303 });
245
- await rejected({ throughEntryId: first.entry_id, brokerPid: 303 });
246
- assert.equal(readCursor(meta.node_id), second.entry_id, 'a late commit cannot move the durable cursor backward');
247
- });
248
242
  test('refresh-yield replays a handoff that reached the discarded conversation', async () => {
249
243
  freshNode('node-refresh-handoff');
250
244
  closeDb();
@@ -258,15 +252,17 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
258
252
  disposers.push(registerWatcher(pi));
259
253
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
260
254
  await wait(TICK_MS + 100);
261
- appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at yield' });
255
+ publishMail(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at yield' });
262
256
  await waitForFirstInjection(pi);
263
257
  assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before yield');
264
258
  // Yield discards that conversation before it can act on the queued handoff.
265
259
  // Its settle must leave the entry durable for the fresh revive's kickoff.
266
260
  setIntent(meta.node_id, 'refresh');
267
261
  await settle(pi);
268
- assert.equal(readCursor(meta.node_id), undefined, 'the discarded conversation cannot commit its inbox handoff');
269
- const bearings = drainBearings(getNode(meta.node_id));
262
+ assert.equal(acknowledgedNodeMailCount(meta.node_id), 0, 'the discarded conversation cannot commit its inbox handoff');
263
+ const replacementExecution = beginExecution(meta.node_id, { daemonEpoch: 'test' });
264
+ resetStaleExecutionMail(Date.now());
265
+ const bearings = drainBearings(getNode(meta.node_id), replacementExecution);
270
266
  assert.match(bearings.unreadDigest ?? '', /handoff at yield/, 'the fresh revive receives the uncommitted handoff');
271
267
  });
272
268
  test('idle: a finished node triggers a fresh turn (no deliverAs)', async () => {
@@ -275,7 +271,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
275
271
  disposers.push(registerWatcher(pi));
276
272
  // No agent_start fired → watcher treats the node as idle.
277
273
  await wait(TICK_MS + 100);
278
- appendInbox('node-idle', { from: 'child-3', tier: 'normal', kind: 'final', label: 'done while idle' });
274
+ publishMail('node-idle', { from: 'child-3', tier: 'normal', kind: 'final', label: 'done while idle' });
279
275
  await waitForFirstInjection(pi);
280
276
  assert.equal(pi.injected.length, 1);
281
277
  assert.equal(pi.injected[0].deliverAs, undefined, 'idle → sendUserMessage triggers a turn, no deliverAs');
@@ -285,7 +281,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
285
281
  const pi = makeFakePi();
286
282
  disposers.push(registerWatcher(pi));
287
283
  await wait(TICK_MS + 100);
288
- const entry = appendInbox('node-deferred-engine-prompt', {
284
+ const entry = publishMail('node-deferred-engine-prompt', {
289
285
  from: 'child-4',
290
286
  tier: 'deferred',
291
287
  kind: 'update',
@@ -317,11 +313,11 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
317
313
  assert.equal(order.length, 2, 'one held card and the one human prompt share the admitted operation');
318
314
  assert.match(order[0], /held update/, 'the held inbox card is first');
319
315
  assert.equal(order[1], 'where are we?', 'the human body follows the held card');
320
- assert.equal(readCursor('node-deferred-engine-prompt'), undefined, 'the joined handoff waits for settlement');
316
+ assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 0, 'the joined handoff waits for settlement');
321
317
  await pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
322
318
  await settle(pi);
323
- assert.equal(readCursor('node-deferred-engine-prompt'), entry.entry_id, 'agent_settled commits the broker-joined handoff');
324
- const staleFollowUpEntry = appendInbox('node-deferred-engine-prompt', {
319
+ assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 1, 'agent_settled commits the broker-joined handoff');
320
+ const staleFollowUpEntry = publishMail('node-deferred-engine-prompt', {
325
321
  from: 'child-4',
326
322
  tier: 'deferred',
327
323
  kind: 'update',
@@ -335,14 +331,14 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
335
331
  assert.equal(order.at(-1), 'continue from there');
336
332
  await pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
337
333
  await settle(pi);
338
- assert.equal(readCursor('node-deferred-engine-prompt'), staleFollowUpEntry.entry_id, 'the m-C prompt settles its joined handoff too');
334
+ assert.equal(acknowledgedNodeMailCount('node-deferred-engine-prompt'), 2, 'the follow-up prompt settles its joined handoff too');
339
335
  });
340
336
  test('a held deferred batch is prepended to an admitted human prompt and replays if that prompt reloads before settlement', async () => {
341
337
  freshNode('node-deferred-human-prompt');
342
338
  const pi = makeFakePi();
343
339
  disposers.push(registerWatcher(pi));
344
340
  await wait(TICK_MS + 100);
345
- const entry = appendInbox('node-deferred-human-prompt', {
341
+ const entry = publishMail('node-deferred-human-prompt', {
346
342
  from: 'child-4',
347
343
  tier: 'deferred',
348
344
  kind: 'update',
@@ -356,9 +352,9 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
356
352
  });
357
353
  assert.equal(firstPromptPrefix.length, 1, 'the held card is sent before the human prompt starts');
358
354
  assert.match(firstPromptPrefix[0], /held update/);
359
- assert.equal(readCursor('node-deferred-human-prompt'), undefined, 'the pre-prompt handoff is not committed');
355
+ assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 0, 'the pre-prompt handoff is not committed');
360
356
  await shutdown(pi);
361
- assert.equal(readCursor('node-deferred-human-prompt'), undefined, 'a reload before agent_settled leaves the held entry replayable');
357
+ assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 0, 'a reload before agent_settled leaves the held entry replayable');
362
358
  const replacement = makeFakePi();
363
359
  disposers.push(registerWatcher(replacement));
364
360
  await wait(SETTLE_MS);
@@ -370,20 +366,14 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
370
366
  assert.match(replayedPrefix[0], /held update/);
371
367
  await replacement.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
372
368
  await settle(replacement);
373
- assert.equal(readCursor('node-deferred-human-prompt'), entry.entry_id, 'only agent_settled commits the prompt-joined handoff');
369
+ assert.equal(acknowledgedNodeMailCount('node-deferred-human-prompt'), 1, 'only agent_settled commits the prompt-joined handoff');
374
370
  });
375
371
  // Pause makes the profile inert while its broker is still alive: the entry
376
372
  // must be RETAINED, not consumed. A critical entry is the sharpest case —
377
373
  // its route aborts the live turn, exactly the punch-through pause forbids.
378
374
  test('a paused profile holds queued entries in a live broker until resume', async () => {
379
375
  freshNode('node-paused-profile');
380
- closeDb();
381
- const origUserHome = process.env.HOME;
382
376
  const origProfile = process.env['CRTR_PROFILE_ID'];
383
- const userHome = join(tmpdir(), `crtr-w-home-${Math.random().toString(36).slice(2, 8)}`);
384
- mkdirSync(userHome, { recursive: true });
385
- homes.push(userHome);
386
- process.env.HOME = userHome;
387
377
  try {
388
378
  const profile = createProfile('paused watcher', [{ path: '/tmp', memory: 'content' }]);
389
379
  pauseProfile(profile.profileId);
@@ -391,20 +381,16 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
391
381
  const pi = makeFakePi();
392
382
  disposers.push(registerWatcher(pi));
393
383
  await wait(TICK_MS + 100);
394
- appendInbox('node-paused-profile', { from: 'child-1', tier: 'critical', kind: 'update', label: 'sent while paused' });
384
+ publishMail('node-paused-profile', { from: 'child-1', tier: 'critical', kind: 'update', label: 'sent while paused' });
395
385
  await wait(SETTLE_MS);
396
386
  assert.equal(pi.injected.length, 0, 'a paused profile delivers nothing to its live broker');
397
- assert.equal(readCursor('node-paused-profile'), undefined, 'the held entry stays unconsumed');
387
+ assert.equal(acknowledgedNodeMailCount('node-paused-profile'), 0, 'the held entry stays unconsumed');
398
388
  resumeProfile(profile.profileId);
399
389
  await waitForFirstInjection(pi);
400
390
  assert.equal(pi.injected.length, 1, 'resume delivers the entry that waited');
401
391
  assert.match(pi.injected[0].content, /sent while paused/);
402
392
  }
403
393
  finally {
404
- if (origUserHome === undefined)
405
- delete process.env.HOME;
406
- else
407
- process.env.HOME = origUserHome;
408
394
  if (origProfile === undefined)
409
395
  delete process.env['CRTR_PROFILE_ID'];
410
396
  else
@@ -435,16 +421,17 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
435
421
  };
436
422
  // An unread entry already sits in the inbox before the watcher is even
437
423
  // registered — the same shape as a revived broker with pre-existing work.
438
- const entry = appendInbox('node-action-gate', { from: 'child-1', tier: 'normal', kind: 'update', label: 'pre-bind entry' });
424
+ const entry = publishMail('node-action-gate', { from: 'child-1', tier: 'normal', kind: 'update', label: 'pre-bind entry' });
439
425
  // Registration alone (the extension factory, before Pi has bound actions)
440
426
  // must be side-effect-free: no timer armed, no tick run, no action attempted
441
427
  // — even across several tick+debounce windows.
442
428
  ensureWatcherNode();
429
+ startWatcherExecution('node-action-gate');
443
430
  disposers.push(registerCanvasInboxWatcher(pi));
444
431
  await wait(TICK_MS * 5 + DEBOUNCE_MS + 50);
445
432
  assert.equal(sendAttempts, 0, 'no action call is attempted before session_start arms the watcher');
446
433
  assert.equal(pi.injected.length, 0, 'nothing handed off before session_start');
447
- assert.equal(readCursor('node-action-gate'), undefined, 'the durable cursor is untouched before session_start');
434
+ assert.equal(acknowledgedNodeMailCount('node-action-gate'), 0, 'no delivery is acknowledged before session_start');
448
435
  // session_start is pi's real bind signal: actions work from here on. Arming
449
436
  // now must pick up the already-buffered pre-bind entry.
450
437
  actionsReady = true;
@@ -452,9 +439,9 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
452
439
  await waitForFirstInjection(pi);
453
440
  assert.equal(sendAttempts, 1, 'exactly one action attempt once session_start arms the watcher');
454
441
  assert.equal(pi.injected.length, 1, 'exactly one handoff once actions are bound');
455
- assert.equal(readCursor('node-action-gate'), undefined, 'still uncommitted until agent_settled');
442
+ assert.equal(acknowledgedNodeMailCount('node-action-gate'), 0, 'still uncommitted until agent_settled');
456
443
  await settle(pi);
457
- assert.equal(readCursor('node-action-gate'), entry.entry_id, 'cursor committed at agent_settled');
444
+ assert.equal(acknowledgedNodeMailCount('node-action-gate'), 1, 'agent_settled acknowledges the delivery');
458
445
  // A repeated session_start (e.g. a resume) must not re-arm a second
459
446
  // interval or re-run the initial tick — no duplicate delivery.
460
447
  pi.fire('session_start', { type: 'session_start' }, undefined);
@@ -465,14 +452,15 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
465
452
  test('disposal before any session_start is safe (no timer to leak, no action ever attempted)', async () => {
466
453
  freshNode('node-dispose-before-start');
467
454
  const pi = makeFakePi();
468
- appendInbox('node-dispose-before-start', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never armed' });
455
+ publishMail('node-dispose-before-start', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never armed' });
456
+ startWatcherExecution('node-dispose-before-start');
469
457
  const dispose = registerCanvasInboxWatcher(pi);
470
458
  // Dispose immediately, before session_start ever fires — models a reload
471
459
  // that discards this generation before Pi finishes binding.
472
460
  dispose();
473
461
  await wait(TICK_MS * 3 + DEBOUNCE_MS + 50);
474
462
  assert.equal(pi.injected.length, 0, 'a watcher disposed before session_start never delivers');
475
- assert.equal(readCursor('node-dispose-before-start'), undefined, 'disposal before start commits nothing');
463
+ assert.equal(acknowledgedNodeMailCount('node-dispose-before-start'), 0, 'disposal before start commits nothing');
476
464
  // A session_start firing after disposal (a stale handler on a discarded
477
465
  // generation) must also be inert.
478
466
  pi.fire('session_start', { type: 'session_start' }, undefined);
@@ -480,38 +468,26 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
480
468
  assert.equal(pi.injected.length, 0, 'session_start after disposal does not resurrect the discarded generation');
481
469
  });
482
470
  });
483
- // Durable-cursor model (at-least-once, one conservative boundary): the in-memory
484
- // `cursor` advances at buffer time (so a burst is never re-read into the same
485
- // watcher's buffer), but the on-disk cursor commits at `agent_settled` — never
486
- // on the fire-and-forget send return, never at `agent_end` (which fires before
487
- // pi decides on retry/continuation). A send returning normally is a HANDOFF,
488
- // and the commit clamps below anything still buffered. agent_settled is
489
- // authoritative and commits unconditionally, with no `hasPendingMessages()`
490
- // veto: pi's contract guarantees no queued continuation remains once it fires.
491
- // `session_shutdown` is the graceful-teardown safety net for a settle that
492
- // never fired before exit; THERE `ctx.hasPendingMessages()` still gates the
493
- // commit, because an unconsumed queued send dies with the process. A watcher
494
- // that dies (crash, SIGTERM, daemon kick) before a committing settle leaves the
495
- // on-disk cursor behind, so a fresh watcher re-reads and re-delivers rather
496
- // than silently losing the entry. See canvas-inbox-watcher.ts's header/inline
497
- // comments on `handedOffWatermark`, `commitTarget`, `commitAtSettle`, and
498
- // `commitAtShutdown` for the full rationale.
499
- describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
500
- test('mid-turn handoff, crash before settle: on-disk cursor NOT advanced, fresh watcher re-delivers', async () => {
501
- freshNode('node-cursor-crash');
471
+ // SQL delivery receipts preserve at-least-once delivery. A claimed delivery is
472
+ // acknowledged only at agent_settled, never at the fire-and-forget send return
473
+ // or agent_end. A watcher that exits before acknowledgment leaves its claim for
474
+ // the replacement execution to inherit and deliver again. session_shutdown
475
+ // retains a claim when Pi still has pending messages, because that queued send
476
+ // would otherwise die with the process.
477
+ describe('canvas inbox watcher — SQL delivery receipts (at-least-once)', () => {
478
+ test('mid-turn handoff, crash before settle: delivery remains unacknowledged and a fresh watcher re-delivers', async () => {
479
+ freshNode('node-receipt-crash');
502
480
  const pi1 = makeFakePi();
503
481
  disposers.push(registerWatcher(pi1));
504
482
  // Streaming (mid-turn) so this delivers via followUp, not the idle path.
505
483
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
506
484
  await wait(TICK_MS + 100);
507
- appendInbox('node-cursor-crash', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still working' });
485
+ publishMail('node-receipt-crash', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still working' });
508
486
  await waitForFirstInjection(pi1);
509
487
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
510
488
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
511
- // The durability boundary: no settle has fired, so the on-disk cursor must
512
- // still be untouched — this is the assertion that proves the fix (a
513
- // commit-at-buffer-time / commit-on-send variant would have committed here).
514
- assert.equal(readCursor('node-cursor-crash'), undefined, 'on-disk cursor not committed before a settle');
489
+ // The delivery remains handed off until settle; a send return alone must not acknowledge it.
490
+ assert.equal(acknowledgedNodeMailCount('node-receipt-crash'), 0, 'delivery is not acknowledged before a settle');
515
491
  // Simulate the watcher dying without a graceful session_shutdown — never
516
492
  // settle OR fire session_shutdown on pi1. The broker's SIGTERM handler DOES
517
493
  // run a JS handler (disposeAndExit: session.dispose() + process.exit), but
@@ -525,44 +501,43 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
525
501
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the same uncommitted entry');
526
502
  assert.match(pi2.injected[0].content, /still working/);
527
503
  });
528
- test('mid-turn handoff + agent_settled: cursor committed, fresh watcher does NOT redeliver', async () => {
529
- freshNode('node-cursor-committed');
504
+ test('mid-turn handoff + agent_settled: delivery acknowledged, fresh watcher does NOT redeliver', async () => {
505
+ freshNode('node-receipt-committed');
530
506
  const pi1 = makeFakePi();
531
507
  disposers.push(registerWatcher(pi1));
532
508
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
533
509
  await wait(TICK_MS + 100);
534
- const entry = appendInbox('node-cursor-committed', { from: 'child-2', tier: 'normal', kind: 'update', label: 'still working' });
510
+ const entry = publishMail('node-receipt-committed', { from: 'child-2', tier: 'normal', kind: 'update', label: 'still working' });
535
511
  await waitForFirstInjection(pi1);
536
512
  assert.equal(pi1.injected.length, 1);
537
- assert.equal(readCursor('node-cursor-committed'), undefined, 'not yet committed before the settle');
513
+ assert.equal(acknowledgedNodeMailCount('node-receipt-committed'), 0, 'not yet committed before the settle');
538
514
  // The turn settles with pi's queue drained (the followUp was consumed) —
539
- // THAT commits the cursor.
515
+ // THAT acknowledges the delivery.
540
516
  await settle(pi1);
541
- assert.equal(readCursor('node-cursor-committed'), entry.entry_id, 'cursor committed to disk at agent_settled');
517
+ assert.equal(acknowledgedNodeMailCount('node-receipt-committed'), 1, 'delivery acknowledged at agent_settled');
542
518
  const pi2 = makeFakePi();
543
519
  disposers.push(registerWatcher(pi2));
544
520
  await wait(SETTLE_MS);
545
521
  assert.equal(pi2.injected.length, 0, 'a fresh watcher does not redeliver an already-committed entry');
546
522
  });
547
523
  test('idle-path delivery: NOT committed on the void send, committed at agent_settled', async () => {
548
- freshNode('node-cursor-idle');
524
+ freshNode('node-receipt-idle');
549
525
  const pi = makeFakePi();
550
526
  disposers.push(registerWatcher(pi));
551
527
  // No agent_start fired → watcher treats the node as idle.
552
528
  await wait(TICK_MS + 100);
553
- const entry = appendInbox('node-cursor-idle', { from: 'child-3', tier: 'normal', kind: 'update', label: 'idle update' });
529
+ const entry = publishMail('node-receipt-idle', { from: 'child-3', tier: 'normal', kind: 'update', label: 'idle update' });
554
530
  await waitForFirstInjection(pi);
555
531
  assert.equal(pi.injected.length, 1);
556
532
  assert.equal(pi.injected[0].deliverAs, undefined, 'idle delivery has no deliverAs');
557
533
  // The idle send triggers a turn, but its void return is a handoff, not proof
558
- // of acceptance — the cursor stays put until the turn settles. (One rule: no
559
- // immediate idle-path commit.)
560
- assert.equal(readCursor('node-cursor-idle'), undefined, 'idle send does not commit on the fire-and-forget return');
534
+ // of acceptance — the delivery remains handed off until the turn settles.
535
+ assert.equal(acknowledgedNodeMailCount('node-receipt-idle'), 0, 'idle send does not commit on the fire-and-forget return');
561
536
  await settle(pi);
562
- assert.equal(readCursor('node-cursor-idle'), entry.entry_id, 'idle-path delivery commits at the turn it started settling');
537
+ assert.equal(acknowledgedNodeMailCount('node-receipt-idle'), 1, 'idle-path delivery commits at the turn it started settling');
563
538
  });
564
- test('synchronous send throw then agent_settled: durable cursor NOT advanced past the re-buffered entry, fresh watcher re-delivers', async () => {
565
- freshNode('node-cursor-failsend');
539
+ test('synchronous send throw then agent_settled: re-buffered delivery remains unacknowledged and a fresh watcher re-delivers', async () => {
540
+ freshNode('node-receipt-failsend');
566
541
  const pi1 = makeFakePi();
567
542
  // Persistent synchronous send failure: the entry is read + buffered, flush
568
543
  // throws, it is re-buffered and never handed off.
@@ -570,12 +545,12 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
570
545
  disposers.push(registerWatcher(pi1));
571
546
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
572
547
  await wait(TICK_MS + 100);
573
- appendInbox('node-cursor-failsend', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never delivered' });
548
+ publishMail('node-receipt-failsend', { from: 'child-1', tier: 'normal', kind: 'update', label: 'never delivered' });
574
549
  await wait(SETTLE_MS);
575
550
  assert.equal(pi1.injected.length, 0, 'the failing send delivered nothing');
576
551
  // agent_settled must NOT commit past the still-re-buffered (never-handed-off) entry.
577
552
  await settle(pi1);
578
- assert.equal(readCursor('node-cursor-failsend'), undefined, 'durable cursor not advanced past a re-buffered, never-delivered entry');
553
+ assert.equal(acknowledgedNodeMailCount('node-receipt-failsend'), 0, 're-buffered, never-delivered mail is not acknowledged');
579
554
  const pi2 = makeFakePi();
580
555
  disposers.push(registerWatcher(pi2));
581
556
  await waitForFirstInjection(pi2);
@@ -583,7 +558,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
583
558
  assert.match(pi2.injected[0].content, /never delivered/);
584
559
  });
585
560
  test('critical abort then abort-generated settle: pending critical entry NOT committed, fresh watcher re-delivers', async () => {
586
- freshNode('node-cursor-critical');
561
+ freshNode('node-receipt-critical');
587
562
  const pi1 = makeFakePi();
588
563
  let aborted = 0;
589
564
  disposers.push(registerWatcher(pi1));
@@ -591,15 +566,15 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
591
566
  // preempts via ctx.abort()).
592
567
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false, abort: () => { aborted += 1; } });
593
568
  await wait(TICK_MS + 100);
594
- appendInbox('node-cursor-critical', { from: 'child-1', tier: 'critical', kind: 'update', label: 'critical work' });
569
+ publishMail('node-receipt-critical', { from: 'child-1', tier: 'critical', kind: 'update', label: 'critical work' });
595
570
  await wait(SETTLE_MS);
596
571
  assert.ok(aborted >= 1, 'the critical entry aborted the live turn');
597
572
  assert.equal(pi1.injected.length, 0, 'the critical path re-buffers, it does not deliver mid-stream');
598
573
  // The abort-generated agent_end/settle must not commit the pending critical
599
574
  // entry (it is re-buffered, never handed off).
600
- pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () => true });
601
- await settle(pi1);
602
- assert.equal(readCursor('node-cursor-critical'), undefined, 'abort-generated settle does not commit the pending critical entry before its retry');
575
+ pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () => false });
576
+ await settle(pi1, { idle: false });
577
+ assert.equal(acknowledgedNodeMailCount('node-receipt-critical'), 0, 'abort-generated settle does not commit the pending critical entry before its retry');
603
578
  const pi2 = makeFakePi();
604
579
  disposers.push(registerWatcher(pi2));
605
580
  await waitForFirstInjection(pi2);
@@ -607,7 +582,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
607
582
  assert.match(pi2.injected[0].content, /critical work/);
608
583
  });
609
584
  test('batch A settles while batch B still debounces: commit stops at A, fresh watcher re-delivers B', async () => {
610
- freshNode('node-cursor-batchB');
585
+ freshNode('node-receipt-batchB');
611
586
  // Wide debounce for THIS watcher so B stays buffered-but-unflushed when we
612
587
  // settle A; the watcher captures the cadence at registration, so restore the
613
588
  // file default immediately after for the fresh (pi2) watcher.
@@ -617,17 +592,16 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
617
592
  process.env['CRTR_WATCHER_DEBOUNCE_MS'] = String(DEBOUNCE_MS);
618
593
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
619
594
  await wait(TICK_MS + 60);
620
- const a = appendInbox('node-cursor-batchB', { from: 'child-a', tier: 'normal', kind: 'update', label: 'batch A' });
595
+ const a = publishMail('node-receipt-batchB', { from: 'child-a', tier: 'normal', kind: 'update', label: 'batch A' });
621
596
  await waitForFirstInjection(pi1);
622
597
  assert.equal(pi1.injected.length, 1, 'batch A delivered mid-turn');
623
598
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
624
- const b = appendInbox('node-cursor-batchB', { from: 'child-b', tier: 'normal', kind: 'update', label: 'batch B' });
599
+ const b = publishMail('node-receipt-batchB', { from: 'child-b', tier: 'normal', kind: 'update', label: 'batch B' });
625
600
  await wait(70); // B read into the buffer but < 200ms debounce → not yet flushed
626
601
  // A's turn settles while B is still debouncing in the buffer. The commit must
627
602
  // clamp below the still-buffered B and land on A only.
628
603
  await settle(pi1);
629
- assert.equal(readCursor('node-cursor-batchB'), a.entry_id, 'commit lands on handed-off batch A');
630
- assert.notEqual(readCursor('node-cursor-batchB'), b.entry_id, 'commit does not cross the still-buffered batch B');
604
+ assert.equal(acknowledgedNodeMailCount('node-receipt-batchB'), 1, 'commit lands on handed-off batch A');
631
605
  const pi2 = makeFakePi();
632
606
  disposers.push(registerWatcher(pi2));
633
607
  await waitForFirstInjection(pi2);
@@ -640,26 +614,25 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
640
614
  // — a context reporting pending sends (hasPendingMessages() = true) no longer
641
615
  // vetoes it. This is the exact real-incident invariant behind #353/#362: a
642
616
  // settle-time veto that declines the sole commit, followed by cooperative
643
- // idle-release exit, permanently stalls the cursor with no later settle to
644
- // clear it.
617
+ // idle-release exit, permanently leaves the mail handed off with no later settle.
645
618
  test('agent_settled commits unconditionally even when hasPendingMessages() reports pending', async () => {
646
- freshNode('node-cursor-failhold');
619
+ freshNode('node-receipt-failhold');
647
620
  const pi1 = makeFakePi();
648
621
  disposers.push(registerWatcher(pi1));
649
622
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
650
623
  await wait(TICK_MS + 100);
651
- const entry = appendInbox('node-cursor-failhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'queued across a failure' });
624
+ const entry = publishMail('node-receipt-failhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'queued across a failure' });
652
625
  await waitForFirstInjection(pi1);
653
626
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
654
627
  assert.equal(pi1.injected[0].deliverAs, 'followUp');
655
628
  // A failure/aborted agent_end is never a commit point regardless of queue
656
629
  // state, so it must NOT commit.
657
630
  pi1.fire('agent_end', { type: 'agent_end', messages: [] }, { isIdle: () => false });
658
- assert.equal(readCursor('node-cursor-failhold'), undefined, 'a failure agent_end does not commit');
631
+ assert.equal(acknowledgedNodeMailCount('node-receipt-failhold'), 0, 'a failure agent_end does not commit');
659
632
  // The settle commits even though the context reports pending sends —
660
633
  // agent_settled no longer declines on hasPendingMessages().
661
634
  await settle(pi1, { pending: true });
662
- assert.equal(readCursor('node-cursor-failhold'), entry.entry_id, 'agent_settled commits unconditionally, even when hasPendingMessages() reports pending');
635
+ assert.equal(acknowledgedNodeMailCount('node-receipt-failhold'), 1, 'agent_settled commits unconditionally, even when hasPendingMessages() reports pending');
663
636
  const pi2 = makeFakePi();
664
637
  disposers.push(registerWatcher(pi2));
665
638
  await wait(SETTLE_MS);
@@ -671,10 +644,10 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
671
644
  // the process — committing past it would durably lose a message pi never
672
645
  // delivered.
673
646
  test('session_shutdown keeps a refresh handoff replayable when no messages are pending', async () => {
674
- freshNode('node-cursor-refresh-shutdown');
647
+ freshNode('node-receipt-refresh-shutdown');
675
648
  closeDb();
676
649
  const meta = {
677
- node_id: 'node-cursor-refresh-shutdown', name: 'r', created: new Date().toISOString(),
650
+ node_id: 'node-receipt-refresh-shutdown', name: 'r', created: new Date().toISOString(),
678
651
  cwd: '/tmp', kind: 'general', mode: 'base', lifecycle: 'resident',
679
652
  status: 'active',
680
653
  };
@@ -683,26 +656,26 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
683
656
  disposers.push(registerWatcher(pi));
684
657
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
685
658
  await wait(TICK_MS + 100);
686
- appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at shutdown' });
659
+ publishMail(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at shutdown' });
687
660
  await waitForFirstInjection(pi);
688
661
  assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before refresh');
689
662
  setIntent(meta.node_id, 'refresh');
690
663
  await shutdown(pi, { pending: false });
691
- assert.equal(readCursor(meta.node_id), undefined, 'a refresh keeps the shutdown handoff uncommitted for replay');
664
+ assert.equal(acknowledgedNodeMailCount(meta.node_id), 0, 'a refresh keeps the shutdown handoff uncommitted for replay');
692
665
  });
693
666
  test('session_shutdown still holds while hasPendingMessages() reports pending (no agent_settled fired)', async () => {
694
- freshNode('node-cursor-shutdownhold');
667
+ freshNode('node-receipt-shutdownhold');
695
668
  const pi1 = makeFakePi();
696
669
  const dispose = registerWatcher(pi1);
697
670
  pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
698
671
  await wait(TICK_MS + 100);
699
- const entry = appendInbox('node-cursor-shutdownhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still queued at shutdown' });
672
+ const entry = publishMail('node-receipt-shutdownhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still queued at shutdown' });
700
673
  await waitForFirstInjection(pi1);
701
674
  assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
702
675
  // session_shutdown fires with no prior agent_settled, and the context
703
676
  // reports an unconsumed queued send — the hold declines the commit.
704
677
  await shutdown(pi1, { pending: true });
705
- assert.equal(readCursor('node-cursor-shutdownhold'), undefined, 'session_shutdown declines to commit while pi reports pending sends');
678
+ assert.equal(acknowledgedNodeMailCount('node-receipt-shutdownhold'), 0, 'session_shutdown declines to commit while pi reports pending sends');
706
679
  dispose();
707
680
  // A fresh watcher re-reads and re-delivers, then a drained settle commits.
708
681
  const pi2 = makeFakePi();
@@ -711,27 +684,27 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
711
684
  assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
712
685
  assert.match(pi2.injected[0].content, /still queued at shutdown/);
713
686
  await settle(pi2);
714
- assert.equal(readCursor('node-cursor-shutdownhold'), entry.entry_id, 'the redelivered entry commits once the fresh watcher settles cleanly');
687
+ assert.equal(acknowledgedNodeMailCount('node-receipt-shutdownhold'), 1, 'the redelivered entry commits once the fresh watcher settles cleanly');
715
688
  });
716
689
  // Situational custom sends do not enter pi's user steer/followUp queue, so
717
690
  // hasPendingMessages() never reflects them: a situational handoff commits at
718
691
  // the next drained settle (a situational send that async-rejected is the
719
692
  // accepted silent-loss window, not held).
720
693
  test('situational custom send: committed at the next drained agent_settled', async () => {
721
- freshNode('node-cursor-situational');
722
- appendSituationalContext('node-cursor-situational', 'ambient checkout state');
694
+ freshNode('node-receipt-situational');
695
+ appendSituationalContext('node-receipt-situational', 'ambient checkout state');
723
696
  const pi1 = makeFakePi();
724
697
  disposers.push(registerWatcher(pi1));
725
698
  // Idle so the situational-only entry delivers as a hidden custom message.
726
699
  await wait(TICK_MS + 100);
727
- const entry = appendInbox('node-cursor-situational', {
700
+ const entry = publishMail('node-receipt-situational', {
728
701
  from: null, tier: 'normal', kind: 'message', label: '(ambient context updated)',
729
702
  data: { situational: true, situationalOnly: true },
730
703
  });
731
704
  await wait(SETTLE_MS);
732
705
  assert.equal(pi1.sentMessages.length, 1, 'the hidden custom message was sent');
733
- assert.equal(readCursor('node-cursor-situational'), undefined, 'not committed before the settle');
706
+ assert.equal(acknowledgedNodeMailCount('node-receipt-situational'), 0, 'not committed before the settle');
734
707
  await settle(pi1);
735
- assert.equal(readCursor('node-cursor-situational'), entry.entry_id, 'the situational handoff commits at the drained settle');
708
+ assert.equal(acknowledgedNodeMailCount('node-receipt-situational'), 1, 'the situational handoff commits at the drained settle');
736
709
  });
737
710
  });