@north-light/crouter 0.3.311 → 0.3.313

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 (520) 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 +746 -735
  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 +17 -5
  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/host-teardown-process-group.test.js +9 -5
  121. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +312 -1
  122. package/dist/core/__tests__/integration/plugin-revalidate.test.js +4 -0
  123. package/dist/core/__tests__/integration/profile-pause.test.js +4 -4
  124. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +9 -12
  125. package/dist/core/__tests__/integration/revive.test.js +67 -21
  126. package/dist/core/__tests__/integration/spawn-root.test.js +62 -19
  127. package/dist/core/__tests__/integration/subscription-delivery.test.js +33 -21
  128. package/dist/core/__tests__/integration/worktree-land.test.js +5 -2
  129. package/dist/core/__tests__/integration/worktree-reap.test.js +15 -1
  130. package/dist/core/__tests__/kickoff.test.js +36 -39
  131. package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +2 -1
  132. package/dist/core/__tests__/lifecycle.test.js +2 -1
  133. package/dist/core/__tests__/live-lifecycle-prompt.test.js +13 -1
  134. package/dist/core/__tests__/memory-resolver-precedence.test.js +2 -0
  135. package/dist/core/__tests__/migration.test.js +145 -31
  136. package/dist/core/__tests__/model-pin-durability.test.js +4 -1
  137. package/dist/core/__tests__/model-routes-config.test.js +13 -5
  138. package/dist/core/__tests__/node-outcome.test.js +93 -26
  139. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +4 -3
  140. package/dist/core/__tests__/on-read-dedup-resume.test.js +8 -7
  141. package/dist/core/__tests__/on-read-nested-store.test.js +8 -7
  142. package/dist/core/__tests__/passive-subscription.test.js +50 -157
  143. package/dist/core/__tests__/plugin-kinds.test.js +6 -4
  144. package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +6 -1
  145. package/dist/core/__tests__/plugin-page-components.test.js +6 -4
  146. package/dist/core/__tests__/preview-registry-sync.test.js +2 -0
  147. package/dist/core/__tests__/profile-project-memory-delivery.test.js +35 -32
  148. package/dist/core/__tests__/prune.test.js +11 -5
  149. package/dist/core/__tests__/push-final-guard.test.js +14 -7
  150. package/dist/core/__tests__/relaunch-root.test.js +30 -2
  151. package/dist/core/__tests__/remote-canvas-target.test.js +7 -1
  152. package/dist/core/__tests__/repository-root-identity.test.js +4 -0
  153. package/dist/core/__tests__/review-model-floor.test.js +6 -3
  154. package/dist/core/__tests__/revive-all.test.js +13 -3
  155. package/dist/core/__tests__/revive-capacity.test.js +26 -4
  156. package/dist/core/__tests__/revive-parked-fresh.test.js +31 -8
  157. package/dist/core/__tests__/seam/broker-attach-stream.test.js +1 -1
  158. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +4 -3
  159. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +11 -14
  160. package/dist/core/__tests__/seam/broker-provider-retry.test.js +85 -71
  161. package/dist/core/__tests__/seam/dormancy-release.test.js +33 -19
  162. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +7 -5
  163. package/dist/core/__tests__/seam/inbox-reference-guidance.test.js +20 -6
  164. package/dist/core/__tests__/seam/node-outcome-delivery.test.js +27 -3
  165. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +5 -4
  166. package/dist/core/__tests__/stop-guard.test.js +2 -1
  167. package/dist/core/__tests__/subscription-pool-credential-guard.test.js +4 -3
  168. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +19 -5
  169. package/dist/core/__tests__/yield-ensures-daemon.test.js +2 -1
  170. package/dist/core/bootstrap.js +2 -0
  171. package/dist/core/broker-client/__tests__/transport-socket.test.js +51 -6
  172. package/dist/core/broker-client/transport-socket.d.ts +1 -0
  173. package/dist/core/broker-client/transport-socket.js +13 -5
  174. package/dist/core/canvas/__tests__/attention.test.js +29 -35
  175. package/dist/core/canvas/__tests__/history-transcript.test.js +6 -6
  176. package/dist/core/canvas/__tests__/render-remote.test.js +25 -9
  177. package/dist/core/canvas/attention.d.ts +2 -5
  178. package/dist/core/canvas/attention.js +15 -64
  179. package/dist/core/canvas/boot.d.ts +6 -7
  180. package/dist/core/canvas/boot.js +5 -9
  181. package/dist/core/canvas/canvas.d.ts +13 -109
  182. package/dist/core/canvas/canvas.js +140 -401
  183. package/dist/core/canvas/crons.d.ts +2 -0
  184. package/dist/core/canvas/crons.js +16 -15
  185. package/dist/core/canvas/daemon-owner.js +10 -0
  186. package/dist/core/canvas/db.d.ts +2 -2
  187. package/dist/core/canvas/db.js +2 -18
  188. package/dist/core/canvas/deliveries.d.ts +104 -0
  189. package/dist/core/canvas/deliveries.js +354 -0
  190. package/dist/core/canvas/executions.d.ts +29 -0
  191. package/dist/core/canvas/executions.js +68 -0
  192. package/dist/core/canvas/history.js +35 -35
  193. package/dist/core/canvas/index.d.ts +3 -1
  194. package/dist/core/canvas/index.js +3 -1
  195. package/dist/core/canvas/migrations.js +20 -12
  196. package/dist/core/canvas/nav-model.js +1 -3
  197. package/dist/core/canvas/nav-render.d.ts +4 -4
  198. package/dist/core/canvas/nav-render.js +11 -21
  199. package/dist/core/canvas/node-sql-migration.d.ts +3 -0
  200. package/dist/core/canvas/node-sql-migration.js +261 -0
  201. package/dist/core/canvas/paths.d.ts +2 -16
  202. package/dist/core/canvas/paths.js +2 -31
  203. package/dist/core/canvas/recovery.d.ts +76 -0
  204. package/dist/core/canvas/recovery.js +588 -0
  205. package/dist/core/canvas/remote-canvas-source.js +4 -0
  206. package/dist/core/canvas/render-source.d.ts +16 -27
  207. package/dist/core/canvas/render-source.js +16 -45
  208. package/dist/core/canvas/sessions.d.ts +33 -0
  209. package/dist/core/canvas/sessions.js +78 -0
  210. package/dist/core/canvas/types.d.ts +33 -33
  211. package/dist/core/canvas/types.js +2 -5
  212. package/dist/core/canvas/waits.d.ts +18 -0
  213. package/dist/core/canvas/waits.js +70 -0
  214. package/dist/core/canvas/worktrees.d.ts +40 -0
  215. package/dist/core/canvas/worktrees.js +93 -0
  216. package/dist/core/command-hooks/artifact.d.ts +6 -0
  217. package/dist/core/command-hooks/artifact.js +22 -6
  218. package/dist/core/command-hooks/discovery.d.ts +2 -0
  219. package/dist/core/command-hooks/discovery.js +2 -0
  220. package/dist/core/command-hooks/lifecycle-catalog.d.ts +20 -2
  221. package/dist/core/command-hooks/lifecycle-catalog.js +7 -2
  222. package/dist/core/command-hooks/lifecycle-create.d.ts +4 -0
  223. package/dist/core/command-hooks/lifecycle-create.js +51 -0
  224. package/dist/core/command-hooks/schema.d.ts +2 -2
  225. package/dist/core/command-hooks/schema.js +5 -4
  226. package/dist/core/command-hooks/transport/exec-invoke.js +3 -1
  227. package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +18 -8
  228. package/dist/core/command-hooks/transport/exec-lifecycle.js +89 -30
  229. package/dist/core/config.d.ts +17 -12
  230. package/dist/core/config.js +106 -155
  231. package/dist/core/errors.d.ts +1 -0
  232. package/dist/core/errors.js +4 -0
  233. package/dist/core/feed/__tests__/inbox-migrate.test.js +27 -31
  234. package/dist/core/feed/feed.d.ts +0 -6
  235. package/dist/core/feed/feed.js +68 -56
  236. package/dist/core/feed/inbox.d.ts +7 -81
  237. package/dist/core/feed/inbox.js +46 -378
  238. package/dist/core/feed/messages.d.ts +58 -0
  239. package/dist/core/feed/messages.js +217 -0
  240. package/dist/core/fs-utils.d.ts +4 -0
  241. package/dist/core/fs-utils.js +21 -1
  242. package/dist/core/human/answer.d.ts +1 -1
  243. package/dist/core/human/answer.js +1 -1
  244. package/dist/core/human/claim.d.ts +1 -1
  245. package/dist/core/human/claim.js +2 -4
  246. package/dist/core/human/component-docs.js +1 -1
  247. package/dist/core/human/feedback-companion.js +4 -3
  248. package/dist/core/human/feedback.d.ts +5 -8
  249. package/dist/core/human/feedback.js +57 -85
  250. package/dist/core/human/page-authoring.d.ts +30 -0
  251. package/dist/core/human/page-authoring.js +19 -0
  252. package/dist/core/human/requests.d.ts +136 -0
  253. package/dist/core/human/requests.js +515 -0
  254. package/dist/core/human/root.d.ts +0 -7
  255. package/dist/core/human/root.js +0 -17
  256. package/dist/core/human/scan.d.ts +2 -7
  257. package/dist/core/human/scan.js +48 -132
  258. package/dist/core/human/summary.d.ts +1 -1
  259. package/dist/core/human/summary.js +1 -1
  260. package/dist/core/human/tickets.d.ts +23 -83
  261. package/dist/core/human/tickets.js +92 -243
  262. package/dist/core/human/types.d.ts +5 -1
  263. package/dist/core/installed-plugins.js +8 -3
  264. package/dist/core/keybindings/__tests__/resolve.test.js +19 -17
  265. package/dist/core/locked-file.d.ts +6 -3
  266. package/dist/core/locked-file.js +36 -9
  267. package/dist/core/memory/extensions.js +2 -0
  268. package/dist/core/preview-registry.js +0 -1
  269. package/dist/core/profiles/__tests__/env-store.test.js +2 -0
  270. package/dist/core/profiles/default-binding.d.ts +0 -4
  271. package/dist/core/profiles/default-binding.js +12 -58
  272. package/dist/core/profiles/env-store.js +8 -3
  273. package/dist/core/profiles/manifest.d.ts +2 -37
  274. package/dist/core/profiles/manifest.js +35 -161
  275. package/dist/core/profiles/select.js +3 -0
  276. package/dist/core/review/__tests__/capture-origin.test.js +13 -2
  277. package/dist/core/review/__tests__/comments.test.js +28 -10
  278. package/dist/core/review/__tests__/remap.test.js +28 -10
  279. package/dist/core/review/__tests__/stage-identity.test.js +17 -7
  280. package/dist/core/review/birth.d.ts +1 -1
  281. package/dist/core/review/birth.js +34 -5
  282. package/dist/core/review/comments.js +1 -1
  283. package/dist/core/review/companion.d.ts +1 -1
  284. package/dist/core/review/companion.js +2 -2
  285. package/dist/core/review/document.js +1 -1
  286. package/dist/core/review/project.d.ts +5 -1
  287. package/dist/core/review/project.js +14 -36
  288. package/dist/core/review/realize.d.ts +3 -6
  289. package/dist/core/review/realize.js +51 -168
  290. package/dist/core/review/remap.js +1 -1
  291. package/dist/core/review/stage.d.ts +3 -2
  292. package/dist/core/review/stage.js +2 -3
  293. package/dist/core/review/store.d.ts +3 -21
  294. package/dist/core/review/store.js +140 -186
  295. package/dist/core/review/types.d.ts +0 -1
  296. package/dist/core/runtime/__tests__/install-id-cache-scoping.test.js +9 -5
  297. package/dist/core/runtime/__tests__/spawn-env.test.js +8 -9
  298. package/dist/core/runtime/broker/auth-reload.js +9 -10
  299. package/dist/core/runtime/broker/daemon-ops.d.ts +25 -8
  300. package/dist/core/runtime/broker/daemon-ops.js +42 -9
  301. package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
  302. package/dist/core/runtime/broker/extension-abort.js +11 -0
  303. package/dist/core/runtime/broker/fault-retry.d.ts +15 -6
  304. package/dist/core/runtime/broker/fault-retry.js +142 -95
  305. package/dist/core/runtime/broker/inbox.d.ts +4 -32
  306. package/dist/core/runtime/broker/inbox.js +4 -92
  307. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  308. package/dist/core/runtime/broker/rebind.js +21 -12
  309. package/dist/core/runtime/broker-persona-guidance.d.ts +1 -1
  310. package/dist/core/runtime/broker-persona-guidance.js +2 -13
  311. package/dist/core/runtime/broker.js +28 -22
  312. package/dist/core/runtime/close.d.ts +3 -9
  313. package/dist/core/runtime/close.js +16 -27
  314. package/dist/core/runtime/fault.d.ts +3 -93
  315. package/dist/core/runtime/fault.js +9 -419
  316. package/dist/core/runtime/kickoff.d.ts +3 -6
  317. package/dist/core/runtime/kickoff.js +12 -18
  318. package/dist/core/runtime/launch.js +1 -1
  319. package/dist/core/runtime/lifecycle.js +40 -7
  320. package/dist/core/runtime/nodes.d.ts +5 -1
  321. package/dist/core/runtime/nodes.js +26 -11
  322. package/dist/core/runtime/placement.js +2 -2
  323. package/dist/core/runtime/recycle.js +24 -14
  324. package/dist/core/runtime/reset.js +26 -7
  325. package/dist/core/runtime/revive.js +226 -217
  326. package/dist/core/runtime/spawn.d.ts +4 -1
  327. package/dist/core/runtime/spawn.js +43 -5
  328. package/dist/core/runtime/warm-pool.d.ts +2 -2
  329. package/dist/core/runtime/warm-pool.js +23 -3
  330. package/dist/core/scoped-state/db.d.ts +7 -0
  331. package/dist/core/scoped-state/db.js +92 -0
  332. package/dist/core/scoped-state/migrate.d.ts +11 -0
  333. package/dist/core/scoped-state/migrate.js +198 -0
  334. package/dist/core/scoped-state/paths.d.ts +9 -0
  335. package/dist/core/scoped-state/paths.js +27 -0
  336. package/dist/core/scoped-state/profiles.d.ts +12 -0
  337. package/dist/core/scoped-state/profiles.js +62 -0
  338. package/dist/core/scoped-state/providers.d.ts +16 -0
  339. package/dist/core/scoped-state/providers.js +8 -0
  340. package/dist/core/scoped-state/schema.d.ts +3 -0
  341. package/dist/core/scoped-state/schema.js +26 -0
  342. package/dist/core/scoped-state/settings.d.ts +28 -0
  343. package/dist/core/scoped-state/settings.js +82 -0
  344. package/dist/core/subscription-state.d.ts +29 -48
  345. package/dist/core/subscription-state.js +176 -712
  346. package/dist/core/worktree-close.js +4 -4
  347. package/dist/core/worktree-mutation-async.js +2 -2
  348. package/dist/core/worktree-sweep.js +11 -16
  349. package/dist/core/worktree.js +6 -12
  350. package/dist/daemon/__tests__/autostart-storm.test.js +1 -39
  351. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +2 -2
  352. package/dist/daemon/__tests__/helpers/source-daemon.js +7 -6
  353. package/dist/daemon/__tests__/integration/migration-startup.test.js +59 -26
  354. package/dist/daemon/__tests__/node-deadline-lane.test.js +2 -1
  355. package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +41 -17
  356. package/dist/daemon/__tests__/node-outcome-delivery.test.js +39 -10
  357. package/dist/daemon/__tests__/reconciler-signature.test.js +2 -2
  358. package/dist/daemon/__tests__/subtree-filter-freshness.test.js +2 -2
  359. package/dist/daemon/api/__tests__/broker-settle-park.test.js +17 -12
  360. package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +27 -5
  361. package/dist/daemon/api/__tests__/model-config.test.js +8 -8
  362. package/dist/daemon/api/__tests__/node-create-description.test.js +22 -5
  363. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -1
  364. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +10 -12
  365. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +6 -3
  366. package/dist/daemon/api/__tests__/profile-metadata-route.test.js +4 -1
  367. package/dist/daemon/api/__tests__/reopen-delivery.test.js +26 -8
  368. package/dist/daemon/api/__tests__/seam/api-server.test.js +24 -32
  369. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +4 -1
  370. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +5 -4
  371. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +29 -43
  372. package/dist/daemon/api/__tests__/submit-result.test.js +3 -2
  373. package/dist/daemon/api/bridge.js +1 -1
  374. package/dist/daemon/api/handlers/bash-jobs.js +6 -7
  375. package/dist/daemon/api/handlers/broker-ops.js +47 -84
  376. package/dist/daemon/api/handlers/broker-recovery.d.ts +4 -0
  377. package/dist/daemon/api/handlers/broker-recovery.js +251 -0
  378. package/dist/daemon/api/handlers/canvas.js +2 -44
  379. package/dist/daemon/api/handlers/daemon.js +41 -0
  380. package/dist/daemon/api/handlers/feedback-comments.js +2 -2
  381. package/dist/daemon/api/handlers/health.js +10 -5
  382. package/dist/daemon/api/handlers/human-requests.js +168 -188
  383. package/dist/daemon/api/handlers/human.js +3 -148
  384. package/dist/daemon/api/handlers/inbox-page.d.ts +4 -0
  385. package/dist/daemon/api/handlers/inbox-page.js +89 -0
  386. package/dist/daemon/api/handlers/inbox.js +21 -120
  387. package/dist/daemon/api/handlers/messages.d.ts +1 -0
  388. package/dist/daemon/api/handlers/messages.js +73 -15
  389. package/dist/daemon/api/handlers/node-outcomes.js +58 -13
  390. package/dist/daemon/api/handlers/nodes.d.ts +1 -0
  391. package/dist/daemon/api/handlers/nodes.js +41 -42
  392. package/dist/daemon/api/handlers/reviews.js +4 -29
  393. package/dist/daemon/api/map.d.ts +25 -3
  394. package/dist/daemon/api/map.js +84 -14
  395. package/dist/daemon/api/server.js +68 -9
  396. package/dist/daemon/control.d.ts +18 -0
  397. package/dist/daemon/cron/disposition.d.ts +2 -0
  398. package/dist/daemon/cron/disposition.js +8 -5
  399. package/dist/daemon/cron/settle.js +37 -13
  400. package/dist/daemon/cron/sinks.d.ts +18 -1
  401. package/dist/daemon/cron/sinks.js +47 -93
  402. package/dist/daemon/crtrd-cli.js +4 -19
  403. package/dist/daemon/crtrd.d.ts +2 -0
  404. package/dist/daemon/crtrd.js +206 -81
  405. package/dist/daemon/fleet.js +5 -6
  406. package/dist/daemon/human/finish.d.ts +12 -24
  407. package/dist/daemon/human/finish.js +88 -285
  408. package/dist/daemon/human/settle.d.ts +40 -0
  409. package/dist/daemon/human/settle.js +89 -0
  410. package/dist/daemon/human/sweep.d.ts +20 -5
  411. package/dist/daemon/human/sweep.js +56 -60
  412. package/dist/daemon/manage.d.ts +21 -16
  413. package/dist/daemon/manage.js +85 -51
  414. package/dist/daemon/messaging/node-message.d.ts +1 -1
  415. package/dist/daemon/messaging/node-message.js +9 -12
  416. package/dist/daemon/park-activity.js +3 -3
  417. package/dist/daemon/reconcilers/bash-deadline.js +6 -7
  418. package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -1
  419. package/dist/daemon/reconcilers/broker-supervision.js +41 -19
  420. package/dist/daemon/reconcilers/controller-death.js +6 -18
  421. package/dist/daemon/reconcilers/delivery-lane.d.ts +9 -0
  422. package/dist/daemon/reconcilers/delivery-lane.js +130 -0
  423. package/dist/daemon/reconcilers/live-obligation.js +3 -3
  424. package/dist/daemon/reconcilers/node-deadline.js +1 -1
  425. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +1 -1
  426. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +10 -12
  427. package/dist/daemon/reconcilers/node-lifecycle/tick.js +5 -5
  428. package/dist/daemon/reconcilers/node-lifecycle/wants-execution.js +1 -1
  429. package/dist/daemon/reconcilers/pending-review-submit.js +3 -3
  430. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -8
  431. package/dist/daemon/reconcilers/storage-maintenance.js +13 -43
  432. package/dist/daemon/review/companion.js +5 -11
  433. package/dist/daemon/review/deliver.d.ts +1 -5
  434. package/dist/daemon/review/deliver.js +3 -52
  435. package/dist/daemon/review/finish.d.ts +2 -27
  436. package/dist/daemon/review/finish.js +79 -263
  437. package/dist/daemon/review/sweep.d.ts +3 -10
  438. package/dist/daemon/review/sweep.js +13 -131
  439. package/dist/daemon/startup-policy.d.ts +0 -5
  440. package/dist/daemon/startup-policy.js +0 -5
  441. package/dist/hook-authoring.d.ts +28 -2
  442. package/dist/hook-authoring.js +53 -1
  443. package/dist/index.d.ts +3 -2
  444. package/dist/index.js +1 -1
  445. package/dist/migrations/__tests__/activation-concurrency.test.js +3 -2
  446. package/dist/migrations/__tests__/activation.test.js +9 -6
  447. package/dist/migrations/activation.d.ts +3 -2
  448. package/dist/migrations/activation.js +2 -4
  449. package/dist/migrations/corpus.d.ts +2 -5
  450. package/dist/migrations/corpus.js +9 -20
  451. package/dist/migrations/runner.d.ts +3 -1
  452. package/dist/migrations/runner.js +6 -2
  453. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +21 -89
  454. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +4 -2
  455. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +51 -33
  456. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +16 -2
  457. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +13 -1
  458. package/dist/pi-extensions/canvas-inbox-watcher.js +98 -244
  459. package/dist/pi-extensions/canvas-passive-context.d.ts +4 -8
  460. package/dist/pi-extensions/canvas-passive-context.js +85 -37
  461. package/dist/pi-extensions/canvas-stophook.js +47 -31
  462. package/dist/shared/env.d.ts +1 -0
  463. package/dist/shared/env.js +3 -0
  464. package/package.json +1 -1
  465. package/runtime.lock.json +5 -5
  466. package/dist/clients/attach/__tests__/page-block.test.js +0 -54
  467. package/dist/clients/inbox/review-visibility.d.ts +0 -5
  468. package/dist/clients/inbox/review-visibility.js +0 -19
  469. package/dist/commands/canvas-rebuild-index.d.ts +0 -2
  470. package/dist/commands/canvas-rebuild-index.js +0 -46
  471. package/dist/core/__tests__/human-cancel-guard.test.js +0 -110
  472. package/dist/core/__tests__/human-deliver.test.d.ts +0 -1
  473. package/dist/core/__tests__/human-deliver.test.js +0 -256
  474. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +0 -1
  475. package/dist/core/__tests__/human-node-not-supervised.test.js +0 -89
  476. package/dist/core/__tests__/integration/human-deliver-e2e.test.d.ts +0 -1
  477. package/dist/core/__tests__/integration/human-deliver-e2e.test.js +0 -131
  478. package/dist/core/__tests__/phase4-review-store.test.d.ts +0 -1
  479. package/dist/core/__tests__/phase4-review-store.test.js +0 -144
  480. package/dist/core/__tests__/watchdog-abort-arms-retry.test.d.ts +0 -1
  481. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +0 -110
  482. package/dist/core/canvas/human-deliveries.d.ts +0 -39
  483. package/dist/core/canvas/human-deliveries.js +0 -75
  484. package/dist/core/canvas/node-outcome-deliveries.d.ts +0 -64
  485. package/dist/core/canvas/node-outcome-deliveries.js +0 -159
  486. package/dist/core/feed/passive.d.ts +0 -3
  487. package/dist/core/feed/passive.js +0 -53
  488. package/dist/core/human/__tests__/inbox-cancel-worker.d.ts +0 -1
  489. package/dist/core/human/__tests__/inbox-cancel-worker.js +0 -7
  490. package/dist/core/human/__tests__/inbox-claim-worker.d.ts +0 -1
  491. package/dist/core/human/__tests__/inbox-claim-worker.js +0 -17
  492. package/dist/core/human/__tests__/inbox-stale-claim-worker.d.ts +0 -1
  493. package/dist/core/human/__tests__/inbox-stale-claim-worker.js +0 -8
  494. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +0 -1
  495. package/dist/core/human/__tests__/integration/inbox-core.test.js +0 -145
  496. package/dist/core/human/__tests__/page-tickets.test.d.ts +0 -1
  497. package/dist/core/human/__tests__/page-tickets.test.js +0 -161
  498. package/dist/core/review/ticket-filter.d.ts +0 -17
  499. package/dist/core/review/ticket-filter.js +0 -21
  500. package/dist/core/runtime/broker/passive.d.ts +0 -17
  501. package/dist/core/runtime/broker/passive.js +0 -44
  502. package/dist/core/runtime/busy.d.ts +0 -46
  503. package/dist/core/runtime/busy.js +0 -150
  504. package/dist/daemon/__tests__/startup-block-marker.test.d.ts +0 -1
  505. package/dist/daemon/__tests__/startup-block-marker.test.js +0 -86
  506. package/dist/daemon/api/__tests__/review-terminal-conflict.test.d.ts +0 -1
  507. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +0 -98
  508. package/dist/daemon/human/deliver-action.d.ts +0 -3
  509. package/dist/daemon/human/deliver-action.js +0 -9
  510. package/dist/daemon/node-outcome/deliver-outcome.d.ts +0 -3
  511. package/dist/daemon/node-outcome/deliver-outcome.js +0 -9
  512. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +0 -10
  513. package/dist/daemon/reconcilers/human-delivery-lane.js +0 -41
  514. package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +0 -10
  515. package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +0 -41
  516. package/dist/daemon/startup-block-marker.d.ts +0 -25
  517. package/dist/daemon/startup-block-marker.js +0 -99
  518. /package/dist/{clients/attach/__tests__/page-block.test.d.ts → api/dto/mail.js} +0 -0
  519. /package/dist/{commands/sys/__tests__/migrate.test.d.ts → api/dto/recovery.js} +0 -0
  520. /package/dist/{core/__tests__/human-cancel-guard.test.d.ts → commands/__tests__/integration/migrate.test.d.ts} +0 -0
@@ -10,9 +10,11 @@ import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { createNode, getNode, subscribe } from '../canvas/canvas.js';
12
12
  import { reportsDir } from '../canvas/paths.js';
13
- import { closeDb } from '../canvas/db.js';
13
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
14
+ import { acknowledgeNodeMail, claimNodeMail } from '../canvas/deliveries.js';
15
+ import { beginExecution } from '../canvas/executions.js';
14
16
  import { buildReviveKickoff, clearYieldMessage, drainBearings, readYieldMessage, writeYieldMessage, yieldMessagePath, } from '../runtime/kickoff.js';
15
- import { appendInbox, readCursor } from '../feed/inbox.js';
17
+ import { enqueueNodeMail } from '../feed/messages.js';
16
18
  import { appendSituationalContext } from '../runtime/situational-context.js';
17
19
  import { buildContextBearings as buildContextIntro } from '../runtime/bearings.js';
18
20
  let home;
@@ -36,6 +38,7 @@ before(() => {
36
38
  beforeEach(() => {
37
39
  closeDb();
38
40
  rmSync(home, { recursive: true, force: true });
41
+ migrate(openDb());
39
42
  });
40
43
  after(() => {
41
44
  closeDb();
@@ -45,18 +48,23 @@ after(() => {
45
48
  test('drainBearings preserves the yield note until explicit boot confirmation', () => {
46
49
  const id = 'n1';
47
50
  const meta = createNode(node(id));
51
+ const executionId = beginExecution(id, { daemonEpoch: 'test' });
48
52
  writeYieldMessage(id, 'on wake, finish step 3');
49
- appendInbox(id, { from: 'w1', tier: 'normal', kind: 'update', label: 'first', ref: '/tmp/a.md' });
50
- const last = appendInbox(id, { from: 'w1', tier: 'normal', kind: 'update', label: 'second', ref: '/tmp/b.md' });
51
- const b1 = drainBearings(meta);
53
+ enqueueNodeMail(id, { senderNodeId: 'w1', kind: 'update', label: 'first', bodyRef: '/tmp/a.md', urgency: 'normal', wake: 1 });
54
+ enqueueNodeMail(id, { senderNodeId: 'w1', kind: 'update', label: 'second', bodyRef: '/tmp/b.md', urgency: 'normal', wake: 1 });
55
+ const b1 = drainBearings(meta, executionId);
52
56
  assert.equal(b1.yieldMsg?.trim(), 'on wake, finish step 3');
53
57
  assert.ok(b1.unreadDigest !== null && b1.unreadDigest.length > 0);
54
58
  assert.equal(existsSync(yieldMessagePath(id)), true, 'yield note remains pending before confirmation');
55
59
  assert.equal(readYieldMessage(id), 'on wake, finish step 3', 'reader sees the same persisted note');
56
- assert.equal(readCursor(id), last.entry_id, 'cursor advanced to the last unread entry');
57
- const b2 = drainBearings(meta);
60
+ const claim = claimNodeMail(id, executionId, 'wake');
61
+ const receipt = acknowledgeNodeMail(id, executionId, claim.deliveries.map((delivery) => ({
62
+ deliveryId: delivery.deliveryId, attempts: delivery.attempts, disposition: 'acknowledged',
63
+ })));
64
+ assert.deepEqual(receipt.results.map((result) => result.outcome), ['acknowledged', 'acknowledged']);
65
+ const b2 = drainBearings(meta, executionId);
58
66
  assert.equal(b2.yieldMsg, 'on wake, finish step 3', 'a failed launch can read the same note again');
59
- assert.equal(b2.unreadDigest, null, 'the independently committed feed cursor remains drained');
67
+ assert.equal(b2.unreadDigest, null, 'settled deliveries do not return to the next launch');
60
68
  clearYieldMessage(id);
61
69
  clearYieldMessage(id);
62
70
  assert.equal(readYieldMessage(id), null, 'confirmation clear is idempotent');
@@ -65,30 +73,19 @@ test('drainBearings preserves the yield note until explicit boot confirmation',
65
73
  test('fresh kickoff keeps each inbox card directly before its own message', () => {
66
74
  const id = 'ordered-cards';
67
75
  const meta = createNode(node(id));
68
- appendInbox(id, {
69
- from: 'sender',
70
- tier: 'normal',
71
- kind: 'message',
72
- label: 'FIRST-BODY',
73
- data: { body: 'FIRST-BODY', cards: ['<runtime kind="test-card">FIRST-CARD</runtime>'] },
76
+ const executionId = beginExecution(id, { daemonEpoch: 'test' });
77
+ enqueueNodeMail(id, {
78
+ senderNodeId: 'sender', kind: 'message', label: 'FIRST-BODY',
79
+ bodyJson: { body: 'FIRST-BODY', cards: ['<runtime kind="test-card">FIRST-CARD</runtime>'] }, urgency: 'normal', wake: 1,
74
80
  });
75
- appendInbox(id, {
76
- from: 'sender',
77
- tier: 'normal',
78
- kind: 'message',
79
- label: 'SECOND-BODY',
80
- data: { body: 'SECOND-BODY', cards: ['<runtime kind="test-card">SECOND-CARD</runtime>'] },
81
+ enqueueNodeMail(id, {
82
+ senderNodeId: 'sender', kind: 'message', label: 'SECOND-BODY',
83
+ bodyJson: { body: 'SECOND-BODY', cards: ['<runtime kind="test-card">SECOND-CARD</runtime>'] }, urgency: 'normal', wake: 1,
81
84
  });
82
85
  for (const body of ['GROUPED-BODY-1', 'GROUPED-BODY-2', 'GROUPED-BODY-3']) {
83
- appendInbox(id, {
84
- from: 'card-free-sender',
85
- tier: 'normal',
86
- kind: 'message',
87
- label: body,
88
- data: { body },
89
- });
86
+ enqueueNodeMail(id, { senderNodeId: 'card-free-sender', kind: 'message', label: body, bodyJson: { body }, urgency: 'normal', wake: 1 });
90
87
  }
91
- const kickoff = buildReviveKickoff(meta, drainBearings(meta));
88
+ const kickoff = buildReviveKickoff(meta, drainBearings(meta, executionId));
92
89
  const firstCard = kickoff.indexOf('FIRST-CARD');
93
90
  const firstBody = kickoff.indexOf('FIRST-BODY');
94
91
  const secondCard = kickoff.indexOf('SECOND-CARD');
@@ -109,7 +106,7 @@ test('drainBearings leaves persona drift pending until the replacement context e
109
106
  mode: 'orchestrator',
110
107
  persona_ack: oldPersona,
111
108
  }));
112
- const bearings = drainBearings(meta);
109
+ const bearings = drainBearings(meta, beginExecution(meta.node_id, { daemonEpoch: 'test' }));
113
110
  assert.match(bearings.driftGuidance ?? '', /ORCHESTRATOR/);
114
111
  assert.deepEqual(getNode(meta.node_id)?.persona_ack, oldPersona, 'pre-session_start launch work cannot acknowledge the drift');
115
112
  });
@@ -117,7 +114,7 @@ test('the feed block frames awaiting workers as alive + auto-waking, so a fresh
117
114
  const parent = createNode(node('p1'));
118
115
  const child = createNode(node('c1')); // status 'active' by default
119
116
  subscribe(parent.node_id, child.node_id); // parent awaits child
120
- const msg = buildReviveKickoff(parent, drainBearings(parent));
117
+ const msg = buildReviveKickoff(parent, drainBearings(parent, beginExecution(parent.node_id, { daemonEpoch: 'test' })));
121
118
  // The roster names the live child...
122
119
  assert.ok(msg.includes(child.node_id), 'awaiting roster lists the live child');
123
120
  // ...and asserts aliveness + the automatic wake at the source, so the node
@@ -143,7 +140,7 @@ test('a fresh revive is pointed at its subscriptions\' on-disk report history (c
143
140
  const newer = join(dir, '20260608T130000-final.md');
144
141
  writeFileSync(older, '---\n---\nearly progress', 'utf8');
145
142
  writeFileSync(newer, '---\n---\nthe result', 'utf8');
146
- const msg = buildReviveKickoff(parent, drainBearings(parent));
143
+ const msg = buildReviveKickoff(parent, drainBearings(parent, beginExecution(parent.node_id, { daemonEpoch: 'test' })));
147
144
  // The feed block stays condensed: it reports that history exists and surfaces
148
145
  // the newest report ref for immediate dereference, while older refs stay one
149
146
  // command away instead of being preloaded into the revive prompt.
@@ -170,7 +167,7 @@ test('a fresh revive kickoff never carries situational context — it rides the
170
167
  const id = 'n3';
171
168
  const meta = createNode(node(id));
172
169
  appendSituationalContext(id, 'user is mid-checkout on the store applet');
173
- const kickoff = buildReviveKickoff(meta, drainBearings(meta));
170
+ const kickoff = buildReviveKickoff(meta, drainBearings(meta, beginExecution(meta.node_id, { daemonEpoch: 'test' })));
174
171
  assert.doesNotMatch(kickoff, /kind="situational"/, 'visible kickoff never carries the ambient card');
175
172
  assert.doesNotMatch(kickoff, /mid-checkout on the store applet/, 'visible kickoff never carries the ambient text');
176
173
  const hidden = buildContextIntro(id);
@@ -180,20 +177,20 @@ test('a fresh revive kickoff never carries situational context — it rides the
180
177
  test('buildReviveKickoff is pure — building twice eats nothing', () => {
181
178
  const id = 'n2';
182
179
  const meta = createNode(node(id));
180
+ const executionId = beginExecution(id, { daemonEpoch: 'test' });
183
181
  writeYieldMessage(id, 'remember the invariant');
184
- const last = appendInbox(id, { from: 'w', tier: 'normal', kind: 'update', label: 'r', ref: '/tmp/r.md' });
185
- // Drain once (the consuming step), then build twice off the same bearings.
186
- const bearings = drainBearings(meta);
187
- const cursorAfterDrain = readCursor(id);
188
- assert.equal(cursorAfterDrain, last.entry_id);
182
+ enqueueNodeMail(id, { senderNodeId: 'w', kind: 'update', label: 'r', bodyRef: '/tmp/r.md', urgency: 'normal', wake: 1 });
183
+ // Claim once, then build twice from the same immutable bearings.
184
+ const bearings = drainBearings(meta, executionId);
189
185
  const s1 = buildReviveKickoff(meta, bearings);
190
186
  const s2 = buildReviveKickoff(meta, bearings);
191
- // Identical output, and the build mutated nothing: cursor and note unchanged.
187
+ // Identical output, and the build mutates neither delivery receipts nor note.
192
188
  assert.equal(s1, s2);
193
189
  assert.match(s1, /<yield-message>\nremember the invariant\n<\/yield-message>/, 'yield note is delivered inline');
194
190
  const contextInventory = s1.match(/<context-dir path="[^"]+">[\s\S]*?<\/context-dir>/)?.[0];
195
191
  assert.ok(contextInventory, 'context inventory is present');
196
192
  assert.doesNotMatch(contextInventory, /yield-message\.md/, 'ephemeral yield note is not addressable from inventory');
197
- assert.equal(readCursor(id), cursorAfterDrain, 'build does not touch the cursor');
193
+ const claim = claimNodeMail(id, executionId, 'wake');
194
+ assert.equal(claim.deliveries.length, 1, 'build does not settle its claimed delivery');
198
195
  assert.equal(readYieldMessage(id), 'remember the invariant', 'build does not mutate the persisted note');
199
196
  });
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { createNode, getNode } from '../canvas/canvas.js';
7
- import { closeDb } from '../canvas/db.js';
7
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
8
8
  import { transition } from '../runtime/lifecycle.js';
9
9
  let home;
10
10
  function seed(nodeId) {
@@ -30,6 +30,7 @@ before(() => {
30
30
  beforeEach(() => {
31
31
  closeDb();
32
32
  rmSync(home, { recursive: true, force: true });
33
+ migrate(openDb());
33
34
  });
34
35
  after(() => {
35
36
  closeDb();
@@ -11,7 +11,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { join } from 'node:path';
13
13
  import { createNode, getNode, setStatus, setIntent } from '../canvas/canvas.js';
14
- import { closeDb } from '../canvas/db.js';
14
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
15
15
  import { isParked, transition } from '../runtime/lifecycle.js';
16
16
  let home;
17
17
  function node(id) {
@@ -39,6 +39,7 @@ before(() => {
39
39
  beforeEach(() => {
40
40
  closeDb();
41
41
  rmSync(home, { recursive: true, force: true });
42
+ migrate(openDb());
42
43
  });
43
44
  after(() => {
44
45
  closeDb();
@@ -6,10 +6,13 @@ import { mkdtempSync, rmSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import { createNode, updateNode } from '../canvas/canvas.js';
9
- import { closeDb } from '../canvas/db.js';
9
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
10
+ import { ensureScopeInitialized } from '../config.js';
11
+ import { closeScopedStores } from '../scoped-state/db.js';
10
12
  import { renderPreferencesSection } from '../substrate/render-node.js';
11
13
  import { clearSessionCache } from '../substrate/session-cache.js';
12
14
  let home;
15
+ let priorUserHome;
13
16
  function node(id) {
14
17
  return {
15
18
  node_id: id,
@@ -24,18 +27,27 @@ function node(id) {
24
27
  }
25
28
  before(() => {
26
29
  home = mkdtempSync(join(tmpdir(), 'crtr-live-lifecycle-prompt-'));
30
+ priorUserHome = process.env['HOME'];
31
+ process.env['HOME'] = home;
27
32
  process.env['CRTR_HOME'] = home;
28
33
  });
29
34
  beforeEach(() => {
30
35
  closeDb();
31
36
  clearSessionCache();
32
37
  rmSync(home, { recursive: true, force: true });
38
+ ensureScopeInitialized('user', join(home, '.crouter'));
39
+ migrate(openDb());
33
40
  });
34
41
  after(() => {
35
42
  closeDb();
36
43
  clearSessionCache();
44
+ closeScopedStores();
37
45
  rmSync(home, { recursive: true, force: true });
38
46
  delete process.env['CRTR_HOME'];
47
+ if (priorUserHome === undefined)
48
+ delete process.env['HOME'];
49
+ else
50
+ process.env['HOME'] = priorUserHome;
39
51
  });
40
52
  test('a cached resident prompt becomes terminal after a live lifecycle change', () => {
41
53
  createNode(node('live'));
@@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
23
23
  import { join } from 'node:path';
24
24
  import { gitSync } from '../git.js';
25
25
  import { createProfile } from '../profiles/manifest.js';
26
+ import { initializeFreshScopedStore } from '../scoped-state/migrate.js';
26
27
  import { resetScopeCache } from '../scope.js';
27
28
  import { loadMemoryStoreView, openProjectMemoryStore, resolveMemoryCandidates, resolveMemoryDoc, resolveMemoryDocInStore, resolveMemoryDocs, } from '../memory-resolver.js';
28
29
  let home;
@@ -54,6 +55,7 @@ before(() => {
54
55
  prevHome = process.env.HOME;
55
56
  prevProfile = process.env.CRTR_PROFILE_ID;
56
57
  process.env.HOME = home;
58
+ initializeFreshScopedStore({ kind: 'user', root: join(home, '.crouter') });
57
59
  // A wider user-scope doc whose name a project doc used to shadow.
58
60
  mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
59
61
  writeFileSync(join(home, '.crouter', 'memory', 'testing-stance.md'), '---\nkind: preference\n---\nUSER PREFERENCE BODY\n', 'utf8');
@@ -10,9 +10,10 @@ import { canvasDbPath, ensureHome } from '../canvas/paths.js';
10
10
  import { customExtensionPaths } from '../canvas/extensions.js';
11
11
  import { assertMandatoryCanvasExtensions, CANVAS_EXTENSIONS, mergeCanvasExtensions } from '../runtime/canvas-extensions.js';
12
12
  import { updateNode } from '../canvas/canvas.js';
13
+ import { parsePage, parsePageDelivery } from '../human/page.js';
13
14
  import { spawn, spawnSync } from 'node:child_process';
14
15
  const HERE = dirname(fileURLToPath(import.meta.url));
15
- const RUNTIME_COLUMNS = ['intent', 'pi_pid', 'window', 'tmux_session'];
16
+ const RUNTIME_COLUMNS = ['intent', 'window', 'tmux_session'];
16
17
  const RACE_FIXTURE = join(HERE, 'fixtures', 'metadata-migration-race.ts');
17
18
  const PRE_TOOL_GUIDE_CANVAS_EXTENSION_NAMES = [
18
19
  'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
@@ -53,6 +54,11 @@ function userVersion(db) {
53
54
  return db.prepare('PRAGMA user_version').get()
54
55
  .user_version;
55
56
  }
57
+ function openMigratedDb() {
58
+ const db = openDb();
59
+ migrate(db);
60
+ return db;
61
+ }
56
62
  function nodeColumns(db) {
57
63
  return db.prepare('PRAGMA table_info(nodes)').all().map((r) => r.name);
58
64
  }
@@ -77,8 +83,11 @@ after(() => {
77
83
  rmSync(home, { recursive: true, force: true });
78
84
  delete process.env['CRTR_HOME'];
79
85
  });
80
- test('opening a fresh home migrates to the latest user_version', () => {
86
+ test('opening a fresh home leaves schema initialization explicit', () => {
81
87
  const db = openDb();
88
+ assert.equal(userVersion(db), 0);
89
+ assert.equal(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'nodes'").get(), undefined);
90
+ migrate(db);
82
91
  assert.equal(userVersion(db), MIGRATIONS.length);
83
92
  });
84
93
  test('a persisted review-boundary canvas prefix is not treated as custom extensions', () => {
@@ -120,7 +129,7 @@ test('a v14 canvas normalizes historical canvas paths while retaining custom lau
120
129
  const saved = JSON.parse(readFileSync(metaPath, 'utf8'));
121
130
  assert.deepEqual(saved.launch.extensions, ['/plugins/canvas-inbox-watcher.js']);
122
131
  });
123
- test('a v14 migration is protected against concurrent writeback by normal updateNode writes', async () => {
132
+ test('an explicit v14 migration preserves legacy metadata while node updates use SQL state', async () => {
124
133
  const metaPath = join(home, 'nodes', 'saved', 'meta.json');
125
134
  mkdirSync(join(home, 'nodes', 'saved'), { recursive: true });
126
135
  writeFileSync(metaPath, JSON.stringify({
@@ -143,16 +152,17 @@ test('a v14 migration is protected against concurrent writeback by normal update
143
152
  rmSync(marker, { force: true });
144
153
  const { done } = await spawnMigrationRace(metaPath, marker);
145
154
  await waitForPath(marker);
155
+ migrate(raw);
156
+ await done;
157
+ raw.close();
146
158
  const update = updateNode('saved', { name: 'saved-raced' });
147
159
  assert.equal(update.name, 'saved-raced');
148
- await done;
149
160
  const settled = JSON.parse(readFileSync(metaPath, 'utf8'));
150
161
  assert.equal(customExtensionPaths(settled.launch.extensions).length, settled.launch.extensions.length);
151
- assert.deepEqual(settled.launch.extensions, ['/plugins/canvas-inbox-watcher.js'], 'concurrent migration + metadata writes cannot restore retired hook prefixes');
152
- assert.equal(settled.name, 'saved-raced');
153
- raw.close();
162
+ assert.deepEqual(settled.launch.extensions, ['/plugins/canvas-inbox-watcher.js'], 'migration writeback cannot restore retired hook prefixes');
163
+ assert.equal(settled.name, 'saved', 'the legacy conversion input remains unchanged after a SQL node update');
154
164
  });
155
- test('a fresh db has the four additive runtime columns', () => {
165
+ test('a v0 db retains its runtime fields while process state moves to executions', () => {
156
166
  // Build a v0 db by hand: the baseline tables only, user_version 0, no runtime
157
167
  // columns — exactly the shape of the live fleet before this phase.
158
168
  ensureHome();
@@ -189,14 +199,14 @@ test('a fresh db has the four additive runtime columns', () => {
189
199
  assert.equal(userVersion(raw), 0);
190
200
  assert.ok(!nodeColumns(raw).includes('intent'));
191
201
  raw.close();
192
- // openDb() runs the migration runner forward.
193
- const db = openDb();
202
+ const db = openMigratedDb();
194
203
  assert.equal(userVersion(db), MIGRATIONS.length);
195
- // Runtime columns now exist.
204
+ // Node runtime columns now exist; process state is projected from executions.
196
205
  const cols = nodeColumns(db);
197
206
  for (const c of RUNTIME_COLUMNS) {
198
207
  assert.ok(cols.includes(c), `expected nodes.${c} after migration`);
199
208
  }
209
+ assert.ok(!cols.includes('pi_pid'), 'nodes no longer stores process state');
200
210
  // Pre-existing data is intact — no loss, no mutation of existing columns.
201
211
  const row = db
202
212
  .prepare('SELECT node_id, status, cwd FROM nodes WHERE node_id = ?')
@@ -208,10 +218,9 @@ test('a fresh db has the four additive runtime columns', () => {
208
218
  assert.equal(edgeCount, 1);
209
219
  // The new columns are seeded NULL for the migrated row — nothing observed.
210
220
  const rt = db
211
- .prepare('SELECT intent, pi_pid, "window", tmux_session FROM nodes WHERE node_id = ?')
221
+ .prepare('SELECT intent, "window", tmux_session FROM nodes WHERE node_id = ?')
212
222
  .get('n1');
213
223
  assert.equal(rt['intent'], null);
214
- assert.equal(rt['pi_pid'], null);
215
224
  assert.equal(rt['window'], null);
216
225
  assert.equal(rt['tmux_session'], null);
217
226
  });
@@ -240,7 +249,7 @@ test('session-file ownership migration reconciles authoritative metas before cle
240
249
  insert.run('later-claimant', 'later-claimant', 'developer', 'base', 'terminal', 'active', '/tmp', '2026-01-02T00:00:00.000Z', aliasedSessionFile);
241
250
  insert.run('recovered-claimant', 'recovered-claimant', 'developer', 'base', 'terminal', 'active', '/tmp', '2026-01-03T00:00:00.000Z', sessionFile);
242
251
  assert.doesNotThrow(() => migrate(raw), 'a real canvas with pre-existing duplicates must migrate');
243
- const rows = raw.prepare('SELECT node_id, pi_session_file FROM nodes ORDER BY created, node_id').all();
252
+ const rows = raw.prepare('SELECT node_id, pi_session_file FROM node_rows ORDER BY created, node_id').all();
244
253
  assert.deepEqual(rows.map((row) => [row.node_id, row.pi_session_file]), [
245
254
  ['first-owner', sessionFile],
246
255
  ['later-claimant', null],
@@ -252,7 +261,7 @@ test('session-file ownership migration reconciles authoritative metas before cle
252
261
  const recovered = JSON.parse(readFileSync(join(home, 'nodes', 'recovered-claimant', 'meta.json'), 'utf8'));
253
262
  assert.equal(recovered.pi_session_id, 'recovered-claimant-session', 'a stale row cannot clear the authoritative session id');
254
263
  assert.equal(recovered.pi_session_file, recoveredSessionFile, 'a stale row cannot clear the authoritative session file');
255
- assert.throws(() => raw.prepare('UPDATE nodes SET pi_session_file = ? WHERE node_id = ?').run(sessionFile, 'later-claimant'), /UNIQUE/, 'the schema enforces the repaired invariant');
264
+ assert.throws(() => raw.prepare("INSERT INTO sessions (binding_id, session_id, node_id, transcript_path, source_session_id, fork_entry_id, created_at, bound_at, unbound_at) VALUES ('later-duplicate', 'later-duplicate-session', 'later-claimant', ?, NULL, NULL, '2026-01-02T00:00:00.000Z', '2026-01-02T00:00:00.000Z', NULL)").run(sessionFile), /UNIQUE/, 'the SQL session table enforces the repaired invariant');
256
265
  raw.close();
257
266
  });
258
267
  test('forward session-file repair reprojects a surviving v36 meta coordinate before restoring its UNIQUE index', () => {
@@ -280,7 +289,7 @@ test('forward session-file repair reprojects a surviving v36 meta coordinate bef
280
289
  raw.exec('DROP INDEX IF EXISTS idx_nodes_pi_session_file; CREATE UNIQUE INDEX idx_nodes_pi_session_file ON nodes(pi_session_file); PRAGMA user_version = 36;');
281
290
  assert.doesNotThrow(() => migrate(raw));
282
291
  assert.equal(userVersion(raw), MIGRATIONS.length);
283
- const rows = raw.prepare('SELECT node_id, pi_session_file FROM nodes ORDER BY created, node_id').all();
292
+ const rows = raw.prepare('SELECT node_id, pi_session_file FROM node_rows ORDER BY created, node_id').all();
284
293
  assert.deepEqual(rows.map((row) => [row.node_id, row.pi_session_file]), [
285
294
  ['surviving-owner', recoveredSessionFile],
286
295
  ['stale-row', null],
@@ -288,7 +297,7 @@ test('forward session-file repair reprojects a surviving v36 meta coordinate bef
288
297
  const survivor = JSON.parse(readFileSync(join(home, 'nodes', 'surviving-owner', 'meta.json'), 'utf8'));
289
298
  assert.equal(survivor.pi_session_id, 'surviving-session');
290
299
  assert.equal(survivor.pi_session_file, recoveredSessionFile);
291
- assert.throws(() => raw.prepare('UPDATE nodes SET pi_session_file = ? WHERE node_id = ?').run(recoveredSessionFile, 'stale-row'), /UNIQUE/);
300
+ assert.throws(() => raw.prepare("INSERT INTO sessions (binding_id, session_id, node_id, transcript_path, source_session_id, fork_entry_id, created_at, bound_at, unbound_at) VALUES ('stale-duplicate', NULL, 'stale-row', ?, NULL, NULL, '2026-01-02T00:00:00.000Z', '2026-01-02T00:00:00.000Z', NULL)").run(recoveredSessionFile), /UNIQUE/);
292
301
  raw.close();
293
302
  });
294
303
  test('node lifecycle substrate migration adds six columns and backfills terminal reasons', () => {
@@ -314,7 +323,7 @@ test('node lifecycle substrate migration adds six columns and backfills terminal
314
323
  insert.run('crashed', 'crashed', 'general', 'dead', null, null, '/tmp', created);
315
324
  insert.run('active', 'active', 'general', 'active', null, null, '/tmp', created);
316
325
  raw.close();
317
- const db = openDb();
326
+ const db = openMigratedDb();
318
327
  assert.equal(userVersion(db), MIGRATIONS.length);
319
328
  for (const column of lifecycleColumns)
320
329
  assert.ok(nodeColumns(db).includes(column));
@@ -331,8 +340,8 @@ test('node lifecycle substrate migration adds six columns and backfills terminal
331
340
  // v4: edges FK ON DELETE CASCADE (the one irreversible rebuild)
332
341
  /** Build a hand-rolled db at exactly v3 (baseline tables + the four runtime
333
342
  * columns, edges with NO foreign key, `user_version = 3`) — the shape the live
334
- * fleet reaches just before v4. Caller seeds it, we close it; the next openDb()
335
- * runs only v4 over it. */
343
+ * fleet reaches just before v4. Caller seeds it, then explicitly runs v4 and
344
+ * later steps. */
336
345
  function buildV3Db() {
337
346
  ensureHome();
338
347
  const raw = new DatabaseSync(canvasDbPath());
@@ -382,8 +391,8 @@ test('v4 migrates a db with a pre-existing orphan edge without throwing', () =>
382
391
  assert.equal(userVersion(raw), 3);
383
392
  assert.equal(edgeCount(raw), 2);
384
393
  raw.close();
385
- // openDb() runs v4. It must NOT throw on the orphan, and must drop it.
386
- const db = openDb();
394
+ // Explicit migration must not throw on the orphan, and must drop it.
395
+ const db = openMigratedDb();
387
396
  assert.equal(userVersion(db), MIGRATIONS.length); // migrated forward to the head
388
397
  // The orphan edge is filtered; the non-orphan edge + its count are preserved.
389
398
  assert.equal(edgeCount(db), 1);
@@ -435,7 +444,7 @@ test('bug #99: a stale v4 db with later columns already present migrates cleanly
435
444
  assert.equal(userVersion(raw), 4);
436
445
  assert.equal(nodeColumns(raw).includes('pane'), true, 'precondition: pane already exists even though user_version is stale');
437
446
  raw.close();
438
- const db = openDb();
447
+ const db = openMigratedDb();
439
448
  assert.equal(userVersion(db), MIGRATIONS.length);
440
449
  const cols = nodeColumns(db);
441
450
  assert.ok(cols.includes('pane'), 'pane survives the migration');
@@ -536,7 +545,7 @@ test('v22 converts existing schedules into self-contained crons and removes the
536
545
  payload: { kind: 'general', mode: 'base', cwd: '/tmp/work', prompt: 'go', parent: 'owner' },
537
546
  });
538
547
  raw.close();
539
- const db = openDb();
548
+ const db = openMigratedDb();
540
549
  assert.equal(userVersion(db), MIGRATIONS.length);
541
550
  const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name);
542
551
  assert.ok(!tables.includes('triggers'));
@@ -571,8 +580,7 @@ test('v22 converts existing schedules into self-contained crons and removes the
571
580
  assert.match(birth.command, /crtr node new --kind 'general'/);
572
581
  assert.match(birth.command, /go/);
573
582
  });
574
- // The v22 migration runs on EVERY openDb(), so it must never fail on data shape:
575
- // a throw would make every crtr process unable to open the canvas forever. These
583
+ // The explicit v22 migration must handle every declared-legal data shape. These
576
584
  // cases are all shapes the retired schema declared legal.
577
585
  function insertNodeAt(db, id, cwd, created, parent) {
578
586
  db.prepare('INSERT INTO nodes (node_id, name, kind, status, cwd, parent, created) VALUES (?,?,?,?,?,?,?)').run(id, id, 'general', 'active', cwd, parent, created);
@@ -671,7 +679,7 @@ test('v22 converts the legal edge rows and records the ones it cannot express',
671
679
  payload: { body: 'nope' },
672
680
  });
673
681
  raw.close();
674
- const db = openDb();
682
+ const db = openMigratedDb();
675
683
  assert.equal(userVersion(db), MIGRATIONS.length, 'the schema advances despite the unconvertible row');
676
684
  const cron = (id) => db.prepare('SELECT * FROM crons WHERE cron_id = ?').get(id);
677
685
  const pruned = cron('pruned-owner');
@@ -706,7 +714,7 @@ test('v22 re-run over already-converted crons converges instead of colliding on
706
714
  VALUES ('scheduled-message', 'stale', 'owner', 'echo stale', '2026-01-01T00:00:00.000Z', '/tmp/work', '{}', '2020-01-01T00:00:00Z', '2020-01-01T00:00:00Z')
707
715
  `).run();
708
716
  raw.close();
709
- const db = openDb();
717
+ const db = openMigratedDb();
710
718
  assert.equal(userVersion(db), MIGRATIONS.length);
711
719
  const row = db.prepare('SELECT command FROM crons WHERE cron_id = ?').get('scheduled-message');
712
720
  assert.match(row.command, /crtr node message send --to 'target'/, 'the re-run replaces the half-written row');
@@ -754,8 +762,8 @@ test('v22 recovers a db wedged by an interrupted pre-atomicity run', () => {
754
762
  `).run();
755
763
  raw.exec('DROP TABLE triggers');
756
764
  raw.close();
757
- const db = openDb();
758
- assert.equal(userVersion(db), MIGRATIONS.length, 'the schema advances rather than failing every open');
765
+ const db = openMigratedDb();
766
+ assert.equal(userVersion(db), MIGRATIONS.length, 'the schema advances rather than failing every explicit migration');
759
767
  assert.notEqual(db.prepare('SELECT 1 FROM crons WHERE cron_id = ?').get('half-written'), undefined);
760
768
  });
761
769
  // The generated cron scripts are the migration's real product, so the M4
@@ -784,6 +792,112 @@ function runMigratedScript(script, opts) {
784
792
  rmSync(dir, { recursive: true, force: true });
785
793
  return { code: result.status ?? -1, log: written };
786
794
  }
795
+ function buildV39Db() {
796
+ ensureHome();
797
+ const raw = new DatabaseSync(canvasDbPath());
798
+ migrateToVersion(raw, 39);
799
+ return raw;
800
+ }
801
+ function resetV39Home() {
802
+ closeDb();
803
+ rmSync(home, { recursive: true, force: true });
804
+ return buildV39Db();
805
+ }
806
+ function writeLegacyPage(id, source, dialect = 'jsx') {
807
+ const dir = join(home, 'nodes', id);
808
+ mkdirSync(dir, { recursive: true });
809
+ const document = dialect === 'jsx'
810
+ ? 'export default function LegacyPage() { return <Page title="The gate is gone" subtitle="This page was sent with page_surface off — the refusal that used to block it no longer exists. Nothing to answer; it is here to prove delivery." placement="panel" />; }\n'
811
+ : '---\ntitle: The gate is gone\n---\nThis is the stored Markdown page.\n';
812
+ writeFileSync(join(dir, dialect === 'jsx' ? 'page.tsx' : 'page.md'), document);
813
+ writeFileSync(join(dir, 'page.json'), JSON.stringify({
814
+ schema: 'crtr.page/v1', dialect, title: 'The gate is gone',
815
+ subtitle: 'This page was sent with page_surface off — the refusal that used to block it no longer exists. Nothing to answer; it is here to prove delivery.',
816
+ placement: 'panel', document: dialect === 'jsx' ? 'page.tsx' : 'page.md', steps: 1, slots: [], source,
817
+ }));
818
+ return dir;
819
+ }
820
+ test('v40 imports pre-envelope v1 pages and still rejects invalid v2 delivery envelopes', () => {
821
+ const raw = buildV39Db();
822
+ const blocked = writeLegacyPage('page-bba481b2', {
823
+ sessionName: 'general', blockedSince: '2026-08-11T17:32:21.979Z', profileName: 'crouter', nodeId: '3zl47w7d-msowsany-ee8e2bba',
824
+ });
825
+ const emitted = writeLegacyPage('nfy-3b21bc8a', {
826
+ sessionName: 'notify', nodeId: '3zl47w7d-msmeye7h-cb3ff1da',
827
+ }, 'md');
828
+ const legacyBytes = readFileSync(join(blocked, 'page.json'));
829
+ migrate(raw);
830
+ for (const [dir, id, delivery] of [[blocked, 'page-bba481b2', { placement: 'panel', inbox: true, reply: true }], [emitted, 'nfy-3b21bc8a', { placement: 'panel', inbox: true, reply: false }]]) {
831
+ const stored = raw.prepare('SELECT presentation_dir, specification_json FROM human_requests WHERE request_id = ?').get(id);
832
+ const manifest = parsePage(stored.presentation_dir);
833
+ assert.deepEqual(manifest.delivery, delivery);
834
+ assert.deepEqual(parsePageDelivery(stored.presentation_dir), delivery);
835
+ assert.deepEqual(JSON.parse(stored.specification_json), manifest);
836
+ }
837
+ assert.deepEqual(readFileSync(join(blocked, 'page.json')), legacyBytes, 'the source manifest remains untouched');
838
+ raw.close();
839
+ const invalid = resetV39Home();
840
+ const valid = writeLegacyPage('valid-v1-page', { blockedSince: '2026-08-11T17:32:21.979Z' });
841
+ const validBytes = readFileSync(join(valid, 'page.json'));
842
+ const dir = writeLegacyPage('broken-v2-page', { blockedSince: '2026-08-11T17:32:21.979Z' });
843
+ writeFileSync(join(dir, 'page.json'), JSON.stringify({ schema: 'crtr.page/v2', title: 'Broken', delivery: { placement: 'panel', inbox: true, reply: false, extra: true } }));
844
+ assert.throws(() => migrate(invalid), (error) => error instanceof Error && error.message.includes(join(dir, 'page.json')));
845
+ assert.equal(userVersion(invalid), 39, 'the failed migration rolls back');
846
+ assert.deepEqual(readFileSync(join(valid, 'page.json')), validBytes, 'a failed migration does not alter an earlier v1 input');
847
+ invalid.close();
848
+ const malformedV2 = resetV39Home();
849
+ const malformedV2Dir = writeLegacyPage('malformed-v2-page', { blockedSince: '2026-08-11T17:32:21.979Z' });
850
+ writeFileSync(join(malformedV2Dir, 'page.json'), JSON.stringify({ schema: 'crtr.page/v2', title: 'Broken', delivery: { placement: 'panel', inbox: true, reply: false } }));
851
+ assert.throws(() => migrate(malformedV2), (error) => error instanceof Error && error.message.includes(join(malformedV2Dir, 'page.json')));
852
+ malformedV2.close();
853
+ const malformedV1 = resetV39Home();
854
+ const malformedV1Dir = writeLegacyPage('malformed-v1-page', { blockedSince: '2026-08-11T17:32:21.979Z' });
855
+ writeFileSync(join(malformedV1Dir, 'page.json'), JSON.stringify({
856
+ schema: 'crtr.page/v1', dialect: 'jsx', title: 'Broken', placement: 'panel', document: 'page.tsx', steps: 1,
857
+ slots: [{ id: 'out-of-range', kind: 'options', step: 1, config: {} }], source: { blockedSince: '2026-08-11T17:32:21.979Z' },
858
+ }));
859
+ assert.throws(() => migrate(malformedV1), (error) => error instanceof Error && error.message.includes(join(malformedV1Dir, 'page.json')));
860
+ malformedV1.close();
861
+ const malformedMarkdown = resetV39Home();
862
+ const malformedMarkdownDir = writeLegacyPage('malformed-markdown-page', {}, 'md');
863
+ const malformedMarkdownManifest = JSON.parse(readFileSync(join(malformedMarkdownDir, 'page.json'), 'utf8'));
864
+ malformedMarkdownManifest.steps = 2;
865
+ writeFileSync(join(malformedMarkdownDir, 'page.json'), JSON.stringify(malformedMarkdownManifest));
866
+ assert.throws(() => migrate(malformedMarkdown), (error) => error instanceof Error && error.message.includes(join(malformedMarkdownDir, 'page.json')));
867
+ malformedMarkdown.close();
868
+ });
869
+ test('v40 skips recovery witnesses without identity while retaining failures for indexed nodes', () => {
870
+ const raw = buildV39Db();
871
+ const faultDir = join(home, 'nodes', 'orphan-fault', 'job');
872
+ const retryDir = join(home, 'nodes', 'orphan-retry', 'job');
873
+ mkdirSync(faultDir, { recursive: true });
874
+ mkdirSync(retryDir, { recursive: true });
875
+ writeFileSync(join(faultDir, 'fault'), JSON.stringify({
876
+ link: 'viewer↔broker', op: 'connect', kind: 'connection', message: 'ENOENT', since: '2026-09-10T00:00:00.000Z', operation_id: '11111111111111111111111111111111', retry: { disposition: 'fatal' },
877
+ }));
878
+ writeFileSync(join(retryDir, 'provider-retry'), JSON.stringify({
879
+ state: 'pending', sessionFile: '/tmp/orphan-retry.jsonl', anchorEntryId: null,
880
+ fault: { link: 'pi→provider', op: 'request', kind: 'connection', message: 'ECONNRESET', since: '2026-09-10T00:00:00.000Z', operation_id: '22222222222222222222222222222222', retry: { disposition: 'auto', by: 'daemon' } },
881
+ }));
882
+ const resident = join(home, 'nodes', 'metadata-node');
883
+ mkdirSync(join(resident, 'job'), { recursive: true });
884
+ writeFileSync(join(resident, 'meta.json'), JSON.stringify({
885
+ node_id: 'metadata-node', name: 'metadata-node', created: '2026-09-10T00:00:00.000Z', cwd: '/tmp', kind: 'developer', mode: 'base', lifecycle: 'terminal',
886
+ }));
887
+ writeFileSync(join(resident, 'job', 'busy'), '');
888
+ migrate(raw);
889
+ assert.notEqual(raw.prepare('SELECT 1 FROM nodes WHERE node_id = ?').get('metadata-node'), undefined);
890
+ assert.equal(raw.prepare('SELECT COUNT(*) AS count FROM executions WHERE node_id = ?').get('metadata-node').count, 1);
891
+ assert.equal(raw.prepare("SELECT 1 FROM nodes WHERE node_id IN ('orphan-fault', 'orphan-retry')").get(), undefined);
892
+ raw.close();
893
+ const invalid = resetV39Home();
894
+ insertNode(invalid, 'indexed-node');
895
+ const invalidPath = join(home, 'nodes', 'indexed-node', 'job', 'fault');
896
+ mkdirSync(dirname(invalidPath), { recursive: true });
897
+ writeFileSync(invalidPath, 'not json');
898
+ assert.throws(() => migrate(invalid), (error) => error instanceof Error && error.message.includes(invalidPath));
899
+ invalid.close();
900
+ });
787
901
  test('a migrated predicate script delivers its exact body and self-cancels only when the action succeeds', () => {
788
902
  const raw = buildV21Db();
789
903
  insertLegacySchedule(raw, {
@@ -811,7 +925,7 @@ test('a migrated predicate script delivers its exact body and self-cancels only
811
925
  timeoutAt: '2026-01-04T00:00:00.000Z',
812
926
  });
813
927
  raw.close();
814
- const db = openDb();
928
+ const db = openMigratedDb();
815
929
  const script = (id) => db.prepare('SELECT command FROM crons WHERE cron_id = ?').get(id).command;
816
930
  const stored = script('conditional-message');
817
931
  const satisfied = runMigratedScript(stored, { gateReady: true });
@@ -20,9 +20,10 @@ import { join } from 'node:path';
20
20
  import { buildLaunchSpec, isProviderPinnedModelToken, normalizeModel } from '../runtime/launch.js';
21
21
  import { defaultModelLaddersConfig } from '../../types.js';
22
22
  import { persistDormantModel } from '../runtime/model-swap.js';
23
+ import { initializeFreshScopedStore } from '../scoped-state/migrate.js';
23
24
  import { promote, reshapeNode } from '../runtime/promote.js';
24
25
  import { createNode, getNode } from '../canvas/canvas.js';
25
- import { closeDb } from '../canvas/db.js';
26
+ import { closeDb, migrate, openDb } from '../canvas/db.js';
26
27
  const ladders = defaultModelLaddersConfig();
27
28
  // The concrete cell the `ultra` tier normalizes to — the byte-equal collision at
28
29
  // the heart of #328. A raw `ultra` token is UNpinned; this concrete cell is
@@ -36,9 +37,11 @@ before(() => {
36
37
  home = mkdtempSync(join(tmpdir(), 'crtr-model-pin-durability-'));
37
38
  process.env['HOME'] = home; // buildLaunchSpec reads merged launch config here
38
39
  process.env['CRTR_HOME'] = home; // the canvas db lives here
40
+ initializeFreshScopedStore({ kind: 'user', root: join(home, '.crouter') });
39
41
  });
40
42
  beforeEach(() => {
41
43
  closeDb();
44
+ migrate(openDb());
42
45
  });
43
46
  after(() => {
44
47
  closeDb();