@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
@@ -1,127 +1,39 @@
1
- // The canvas data-access layer. The one place that reads/writes the node+edge
2
- // model. Later phases (spawn, push, lifecycle, daemon) call only this.
3
- //
4
- // Source-of-truth split: a node's meta.json is canonical for its own fields;
5
- // the db row is a queryable index re-derivable from it. The subscribes_to edges
6
- // are db-authoritative (mutable, many-writers — what WAL is for).
1
+ // The canvas data-access layer. Nodes, their sessions, executions, and managed
2
+ // worktrees are SQL-authoritative. Node directories hold only artifacts.
7
3
  import { envNodeId } from '../../shared/env.js';
8
- import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
4
+ import { existsSync, realpathSync, rmSync, statSync } from 'node:fs';
9
5
  import { join, resolve } from 'node:path';
10
6
  import { openDb, withCanvasWrite } from './db.js';
11
- import { withMetaLock, readMetaObject, writeMetaObjectLocked, writeJsonFileDurably } from './meta-file.js';
12
- import { isPidAlive, recordedPidLiveness, capturePidIdentities, identitiesMatch, isLegacyTicksIdentity, isBootScopedIdentity, } from './pid.js';
13
- import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, } from './paths.js';
14
- // meta.json (durable identity — the source of truth for what PERSISTS)
15
- //
16
- // One authoritative store per fact: meta.json holds NodeIdentity only; live
17
- // runtime fields are authoritative in the WAL'd `nodes` row. getNode() hydrates
18
- // the two back into the historical NodeMeta view.
19
- /** The identity keys meta.json persists. Listed explicitly so no runtime field
20
- * can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
21
- const IDENTITY_KEYS = [
22
- 'node_id', 'name', 'description', 'title', 'icon', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
23
- 'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'creator', 'fork_from', 'fork_source_file', 'review_binding', 'passive_default', 'pi_session_id',
24
- 'pi_session_file', 'cycle_pending', 'model_override',
25
- 'launch', 'managed_worktree',
26
- ];
27
- /** Project any node object down to its durable-identity subset. */
28
- function toIdentity(m) {
29
- const out = {};
30
- for (const k of IDENTITY_KEYS) {
31
- if (m[k] !== undefined)
32
- out[k] = m[k];
33
- }
34
- for (const key of ['pi_session_file', 'fork_source_file']) {
35
- if (typeof out[key] === 'string')
36
- out[key] = canonicalSessionFile(out[key]);
37
- }
38
- return out;
39
- }
40
- function readMeta(nodeId) {
41
- const p = nodeMetaPath(nodeId);
42
- if (!existsSync(p))
43
- return null;
44
- // Legacy metas may still carry runtime fields on disk; toIdentity-on-read is
45
- // unnecessary (callers go through getNode, which overlays the row), but the
46
- // raw parse is typed as identity — extra props are ignored.
47
- //
48
- // Tolerate an EMPTY or unparseable meta.json by treating it as MISSING (null)
49
- // rather than throwing. A bare write-temp-then-rename without fsync can leave a
50
- // 0-byte meta.json on an NFS-backed home after the holding process/sandbox dies
51
- // (see writeMeta + the guest cold-boot bug it guards against); a single corrupt
52
- // node must not crash rebuildIndex/revive for the whole canvas. Recovery
53
- // (re-seed from a durable snapshot) is the caller's job.
54
- const raw = readFileSync(p, 'utf8');
55
- if (raw.trim() === '')
56
- return null;
57
- try {
58
- return JSON.parse(raw);
59
- }
60
- catch {
7
+ import { activateArmedExternalActionDelivery, armExternalActionDelivery } from './deliveries.js';
8
+ import { findNodeBySessionFileSql } from './sessions.js';
9
+ import { readFault, readLiveTurn } from './recovery.js';
10
+ import { hasUnreconciledWorktree, openWorktree, readWorktree } from './worktrees.js';
11
+ import { isPidAlive, recordedPidLiveness } from './pid.js';
12
+ import { ensureHome, ensureNodeDirs, jobDir, nodeDir } from './paths.js';
13
+ function jsonValue(value, field) {
14
+ if (value === undefined || value === null)
61
15
  return null;
62
- }
63
- }
64
- /** Serialize ONLY the identity subset → meta.json never holds runtime fields.
65
- *
66
- * Durable write: fsync the temp file before the rename, then fsync the parent
67
- * directory so the rename itself is on disk. A bare writeFileSync+rename is not
68
- * crash-durable — a host crash or abrupt sandbox teardown shortly after the
69
- * rename can surface a 0-byte or missing meta.json (observed on a guest home as
70
- * a cold-boot 0-byte meta.json → rebuild-index/revive JSON.parse('')). meta.json
71
- * is written rarely (birth, polymorph, session-id capture, naming) so the
72
- * per-write fsync cost is negligible. */
73
- function writeMeta(meta) {
74
- writeJsonFileDurably(nodeMetaPath(meta.node_id), toIdentity(meta));
75
- }
76
- // row index — identity columns are a derived projection of meta; runtime
77
- // columns are authoritative. The two have DIFFERENT writers: upsertRow only
78
- // ever touches identity (so a re-index never clobbers live runtime), while
79
- // createNode seeds runtime once and the atomic setters own it thereafter.
80
- /** Upsert the IDENTITY columns of a node's row. ON CONFLICT updates identity
81
- * ONLY — runtime columns (status/intent/pi_pid/window/tmux_session/pane) are left
82
- * exactly as they are, so re-indexing or an identity edit never disturbs live
83
- * state. A fresh insert takes the schema defaults for runtime. */
84
- function upsertRow(meta) {
85
- openDb()
86
- .prepare(`INSERT INTO nodes (node_id, name, description, cycles, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file)
87
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
- ON CONFLICT(node_id) DO UPDATE SET
89
- name=excluded.name, description=excluded.description, cycles=excluded.cycles,
90
- kind=excluded.kind, mode=excluded.mode,
91
- lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
92
- profile_id=excluded.profile_id, parent=excluded.parent,
93
- pi_session_file=excluded.pi_session_file`)
94
- .run(meta.node_id, meta.name, meta.description ?? null, meta.cycles ?? null, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null);
16
+ const encoded = JSON.stringify(value);
17
+ if (encoded === undefined)
18
+ throw new TypeError(`${field} must be JSON-serializable`);
19
+ return encoded;
95
20
  }
96
21
  function insertArmedOutcomeDelivery(db, nodeId, delivery) {
97
- const payloadJson = delivery.payload === undefined ? null : JSON.stringify(delivery.payload);
98
- if (payloadJson === undefined)
99
- throw new TypeError('outcome delivery payload must be JSON-serializable');
100
- const now = Date.now();
101
- db.prepare(`INSERT INTO node_outcome_deliveries
102
- (node_id, state, action_name, argv_json, cwd, config_origin, payload_json, next_attempt_at, created_at, updated_at)
103
- VALUES (?, 'armed', ?, ?, ?, ?, ?, 0, ?, ?)`).run(nodeId, delivery.actionName, JSON.stringify(delivery.argv), delivery.cwd, delivery.configOrigin, payloadJson, now, now);
104
- }
105
- function seedRow(meta, db = openDb()) {
106
- db
107
- .prepare(`INSERT INTO nodes
108
- (node_id, name, description, cycles, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file,
109
- status, intent, waiting_for, final_report, finalized_at, deadline_at, pi_pid, "window", tmux_session, pane)
110
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111
- ON CONFLICT(node_id) DO UPDATE SET
112
- name=excluded.name, description=excluded.description, cycles=excluded.cycles,
113
- kind=excluded.kind, mode=excluded.mode,
114
- lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
115
- profile_id=excluded.profile_id, parent=excluded.parent,
116
- pi_session_file=excluded.pi_session_file,
117
- status=excluded.status, intent=excluded.intent, waiting_for=excluded.waiting_for,
118
- final_report=excluded.final_report, finalized_at=excluded.finalized_at,
119
- pi_pid=excluded.pi_pid, "window"=excluded."window", tmux_session=excluded.tmux_session,
120
- pane=excluded.pane`)
121
- .run(meta.node_id, meta.name, meta.description ?? null, meta.cycles ?? null, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.deadline_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
22
+ armExternalActionDelivery(db, {
23
+ destinationId: delivery.actionName,
24
+ adapter: {
25
+ adapter: 'subprocess', argv: delivery.argv, cwd: delivery.cwd,
26
+ config_origin: delivery.configOrigin, payload: delivery.payload ?? null,
27
+ },
28
+ idempotencyKey: `node-outcome:${nodeId}`,
29
+ });
30
+ }
31
+ function seedRow(meta, db) {
32
+ db.prepare(`INSERT INTO nodes (node_id, name, description, title, icon, cycles, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, spawned_by, creator, fork_from, fork_source_file, created, passive_default, cycle_pending, model_override, persona_ack_json, review_binding_json, launch_config_json, status, intent, waiting_for, final_report, finalized_at, deadline_at, "window", tmux_session, pane) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
33
+ .run(meta.node_id, meta.name, meta.description ?? null, meta.title ?? null, meta.icon ?? null, meta.cycles ?? null, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.spawned_by ?? null, meta.creator ?? null, meta.fork_from ?? null, meta.fork_source_file ?? null, meta.created, meta.passive_default === true ? 1 : 0, meta.cycle_pending === true ? 1 : 0, meta.model_override ?? null, jsonValue(meta.persona_ack, 'persona_ack'), jsonValue(meta.review_binding, 'review_binding'), jsonValue(meta.launch, 'launch'), meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.deadline_at ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
122
34
  }
123
35
  function rowFrom(r) {
124
- return {
36
+ const node = {
125
37
  node_id: r['node_id'],
126
38
  name: r['name'],
127
39
  description: r['description'] ?? undefined,
@@ -156,75 +68,54 @@ function rowFrom(r) {
156
68
  window: r['window'] ?? null,
157
69
  tmux_session: r['tmux_session'] ?? null,
158
70
  pane: r['pane'] ?? null,
71
+ fault: null,
72
+ streaming: false,
159
73
  };
160
- }
161
- /** The authoritative runtime fields for `nodeId`, read from its row. Null when
162
- * no row exists yet (the hydration then falls back to whatever the meta held). */
163
- function runtimeFromRow(nodeId) {
164
- const r = openDb()
165
- .prepare('SELECT status, intent, waiting_for, frozen_at, respawn_failures, respawn_not_before, launched_at, last_settled_at, terminal_reason, final_report, finalized_at, outcome_kind, outcome_reason, outcome_at, outcome_revision, outcome_detail_json, deadline_at, pi_pid, pi_pid_identity, "window", tmux_session, pane FROM nodes WHERE node_id = ?')
166
- .get(nodeId);
167
- if (r === undefined)
168
- return null;
169
- return {
170
- status: r['status'] ?? 'active',
171
- intent: r['intent'] ?? null,
172
- waiting_for: r['waiting_for'] ?? null,
173
- frozen_at: r['frozen_at'] ?? null,
174
- respawn_failures: r['respawn_failures'] ?? 0,
175
- respawn_not_before: r['respawn_not_before'] ?? null,
176
- launched_at: r['launched_at'] ?? null,
177
- last_settled_at: r['last_settled_at'] ?? null,
178
- terminal_reason: r['terminal_reason'] ?? null,
179
- final_report: r['final_report'] ?? null,
180
- finalized_at: r['finalized_at'] ?? null,
181
- outcome_kind: r['outcome_kind'] ?? null,
182
- outcome_reason: r['outcome_reason'] ?? null,
183
- outcome_at: r['outcome_at'] ?? null,
184
- outcome_revision: r['outcome_revision'] ?? 0,
185
- outcome_detail_json: r['outcome_detail_json'] ?? null,
186
- deadline_at: r['deadline_at'] ?? null,
187
- pi_pid: r['pi_pid'] ?? null,
188
- pi_pid_identity: r['pi_pid_identity'] ?? null,
189
- window: r['window'] ?? null,
190
- tmux_session: r['tmux_session'] ?? null,
191
- pane: r['pane'] ?? null,
192
- };
74
+ const fault = readFault(node.node_id);
75
+ node.fault = fault?.link === 'pi→provider' && fault.retry.by === 'daemon' && fault.retry.disposition === 'auto' && !isPidAlive(node.pi_pid)
76
+ ? null
77
+ : fault;
78
+ node.streaming = readLiveTurn(node.node_id)?.busy === true && isPidAlive(node.pi_pid);
79
+ return node;
193
80
  }
194
81
  // Nodes
195
- /** Create a node: scaffold its dirs, persist identity to meta.json, and seed the
196
- * row (identity + runtime from the incoming meta). Returns the hydrated view. */
82
+ /** Create a SQL-authoritative node, then ensure its artifact directories exist. */
197
83
  export function createNode(meta, opts = {}) {
198
84
  ensureHome();
199
- const scaffolded = !existsSync(nodeDir(meta.node_id));
200
- ensureNodeDirs(meta.node_id);
201
- try {
202
- writeMeta(meta);
203
- }
204
- catch (error) {
205
- // No row exists yet, so a dir left here is invisible to every surface and to
206
- // ghost-row reaping. Only unwind a dir this call created.
207
- if (scaffolded)
208
- rmSync(nodeDir(meta.node_id), { recursive: true, force: true });
209
- throw error;
210
- }
211
85
  withCanvasWrite((db) => {
212
86
  seedRow(meta, db);
87
+ if (meta.managed_worktree !== undefined && meta.managed_worktree !== null)
88
+ openWorktree(meta.node_id, meta.managed_worktree, db);
213
89
  if (opts.outcomeDelivery !== undefined)
214
90
  insertArmedOutcomeDelivery(db, meta.node_id, opts.outcomeDelivery);
215
91
  });
92
+ ensureNodeDirs(meta.node_id);
216
93
  return getNode(meta.node_id);
217
94
  }
218
- /** The canonical node record: durable identity (meta.json) ∪ authoritative
219
- * runtime (the row). Null if unknown. */
95
+ /** The canonical node record, hydrated from SQL. Null if unknown. */
220
96
  export function getNode(nodeId) {
221
- const ident = readMeta(nodeId);
222
- if (ident === null)
97
+ const row = openDb().prepare('SELECT * FROM node_rows WHERE node_id = ?').get(nodeId);
98
+ if (row === undefined)
223
99
  return null;
224
- const rt = runtimeFromRow(nodeId);
225
- // The row is authoritative for runtime; overlay it over identity. When no row
226
- // exists yet (rare — pre-rebuild), keep whatever the meta carried.
227
- return { ...ident, ...(rt ?? {}) };
100
+ const worktree = readWorktree(nodeId);
101
+ return {
102
+ ...rowFrom(row),
103
+ title: row['title'] ?? undefined,
104
+ icon: row['icon'] ?? undefined,
105
+ spawned_by: row['spawned_by'] ?? null,
106
+ creator: row['creator'] ?? null,
107
+ fork_from: row['fork_from'] ?? null,
108
+ fork_source_file: row['fork_source_file'] ?? null,
109
+ passive_default: row['passive_default'] === 1,
110
+ cycle_pending: row['cycle_pending'] === 1,
111
+ model_override: row['model_override'] ?? null,
112
+ persona_ack: row['persona_ack_json'] == null ? undefined : JSON.parse(row['persona_ack_json']),
113
+ review_binding: row['review_binding_json'] == null ? undefined : JSON.parse(row['review_binding_json']),
114
+ launch: row['launch_config_json'] == null ? undefined : JSON.parse(row['launch_config_json']),
115
+ pi_session_id: row['pi_session_id'] ?? null,
116
+ pi_session_file: row['pi_session_file'] ?? null,
117
+ ...(worktree === null ? {} : { managed_worktree: worktree }),
118
+ };
228
119
  }
229
120
  export function canonicalSessionFile(sessionFile) {
230
121
  const absolute = resolve(sessionFile);
@@ -232,20 +123,12 @@ export function canonicalSessionFile(sessionFile) {
232
123
  }
233
124
  /** Resolve the crouter node that owns one exact Pi session file. */
234
125
  export function findNodeBySessionFile(sessionFile) {
235
- const canonical = canonicalSessionFile(sessionFile);
236
- // v36 makes this unique. LIMIT 1 also keeps the lookup total for a canvas
237
- // opened during repair of an older duplicate projection: the deterministic
238
- // oldest row remains the owner instead of turning a lookup into a crash.
239
- const match = openDb()
240
- .prepare('SELECT node_id FROM nodes WHERE pi_session_file = ? ORDER BY created, node_id LIMIT 1')
241
- .get(canonical);
242
- return match === undefined ? null : getNode(match.node_id);
126
+ const nodeId = findNodeBySessionFileSql(sessionFile);
127
+ return nodeId === null ? null : getNode(nodeId);
243
128
  }
244
129
  /** The indexed row (from the db) — cheap for queries that don't need full meta. */
245
130
  export function getRow(nodeId) {
246
- const r = openDb()
247
- .prepare('SELECT * FROM nodes WHERE node_id = ?')
248
- .get(nodeId);
131
+ const r = openDb().prepare('SELECT * FROM node_rows WHERE node_id = ?').get(nodeId);
249
132
  return r ? rowFrom(r) : null;
250
133
  }
251
134
  /** The node row whose durable LOCATION pane is `pane`, or null. Lets placement
@@ -253,14 +136,12 @@ export function getRow(nodeId) {
253
136
  * to adopt a caller's pane as a focus). pane is not UNIQUE in the schema, but a
254
137
  * live pane backs at most one node, so this returns the single match. */
255
138
  export function getRowByPane(pane) {
256
- const r = openDb()
257
- .prepare('SELECT * FROM nodes WHERE pane = ?')
258
- .get(pane);
139
+ const r = openDb().prepare('SELECT * FROM node_rows WHERE pane = ?').get(pane);
259
140
  return r ? rowFrom(r) : null;
260
141
  }
261
142
  /** Nodes whose persisted spawn deadline has elapsed and that remain live work. */
262
143
  export function dueNodeDeadlines(nowIso) {
263
- return openDb().prepare(`SELECT * FROM nodes
144
+ return openDb().prepare(`SELECT * FROM node_rows
264
145
  WHERE deadline_at IS NOT NULL AND outcome_at IS NULL
265
146
  AND deadline_at <= ? AND status IN ('active','idle') AND kind != 'human'
266
147
  ORDER BY deadline_at, created, node_id`).all(nowIso).map(rowFrom);
@@ -287,24 +168,56 @@ export function shiftProfileNodeDeadlines(profileId, pauseDurationMs) {
287
168
  return shifted;
288
169
  });
289
170
  }
290
- /** Merge an IDENTITY patch into a node's meta.json and re-index its identity
291
- * columns. Identity has a single writer per node, so this read-modify-write is
292
- * safe (the contended runtime fields were moved out — see the atomic setters
293
- * below). Returns the hydrated view (runtime included). */
171
+ function sqlInput(value, field) {
172
+ if (value === null || value === undefined)
173
+ return null;
174
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint' || value instanceof Uint8Array)
175
+ return value;
176
+ throw new TypeError(`${field} must be a SQLite scalar`);
177
+ }
178
+ /** Update node identity/configuration columns. Sessions and worktrees have named stores. */
294
179
  export function updateNode(nodeId, patch) {
295
- const path = nodeMetaPath(nodeId);
296
- // The whole read/merge/write runs under the SAME per-node lock a migration
297
- // takes (meta-file.ts), so a concurrent v15 normalization and an identity edit
298
- // cannot interleave into a lost write or a restored legacy hook prefix.
299
- const next = withMetaLock(path, () => {
300
- const cur = readMetaObject(path);
301
- if (cur === null)
180
+ const fields = [];
181
+ const add = (key, column = key) => {
182
+ if (Object.prototype.hasOwnProperty.call(patch, key))
183
+ fields.push([column, sqlInput(patch[key], key)]);
184
+ };
185
+ add('name');
186
+ add('description');
187
+ add('title');
188
+ add('icon');
189
+ add('cycles');
190
+ add('kind');
191
+ add('mode');
192
+ add('lifecycle');
193
+ add('cwd');
194
+ add('host_kind');
195
+ add('profile_id');
196
+ add('parent');
197
+ add('spawned_by');
198
+ add('creator');
199
+ add('fork_from');
200
+ add('fork_source_file');
201
+ add('model_override');
202
+ if (Object.prototype.hasOwnProperty.call(patch, 'passive_default'))
203
+ fields.push(['passive_default', patch.passive_default === true ? 1 : 0]);
204
+ if (Object.prototype.hasOwnProperty.call(patch, 'cycle_pending'))
205
+ fields.push(['cycle_pending', patch.cycle_pending === true ? 1 : 0]);
206
+ if (Object.prototype.hasOwnProperty.call(patch, 'persona_ack'))
207
+ fields.push(['persona_ack_json', jsonValue(patch.persona_ack, 'persona_ack')]);
208
+ if (Object.prototype.hasOwnProperty.call(patch, 'review_binding'))
209
+ fields.push(['review_binding_json', jsonValue(patch.review_binding, 'review_binding')]);
210
+ if (Object.prototype.hasOwnProperty.call(patch, 'launch'))
211
+ fields.push(['launch_config_json', jsonValue(patch.launch, 'launch')]);
212
+ if (fields.length === 0) {
213
+ const existing = getNode(nodeId);
214
+ if (existing === null)
302
215
  throw new Error(`unknown node: ${nodeId}`);
303
- const identity = toIdentity({ ...cur, ...patch, node_id: nodeId });
304
- writeMetaObjectLocked(path, identity);
305
- return identity;
306
- });
307
- upsertRow(next);
216
+ return existing;
217
+ }
218
+ const result = openDb().prepare(`UPDATE nodes SET ${fields.map(([column]) => `${column} = ?`).join(', ')} WHERE node_id = ?`).run(...fields.map(([, value]) => value), nodeId);
219
+ if (result.changes !== 1)
220
+ throw new Error(`unknown node: ${nodeId}`);
308
221
  return getNode(nodeId);
309
222
  }
310
223
  // Atomic runtime setters — each one a single-statement UPDATE on the WAL'd row,
@@ -390,20 +303,13 @@ export function transitionStatusIntent(nodeId, fields, opts = {}) {
390
303
  if (Number(result.changes) === 0)
391
304
  return 0;
392
305
  const latched = db.prepare('SELECT outcome_at FROM nodes WHERE node_id = ?').get(nodeId);
393
- if (outcome.supersede === true || latched.outcome_at === outcome.at) {
394
- const now = Date.now();
395
- if (outcome.supersede === true) {
396
- db.prepare(`UPDATE node_outcome_deliveries
397
- SET state = 'pending', attempt = 0, document_json = ?, next_attempt_at = ?,
398
- claimed_at = NULL, claim_owner = NULL, accepted_at = NULL, permanent_failed_at = NULL,
399
- last_failure_json = NULL, updated_at = ?
400
- WHERE node_id = ?`).run(outcome.documentJson ?? null, now, now, nodeId);
401
- }
402
- else {
403
- db.prepare(`UPDATE node_outcome_deliveries
404
- SET state = 'pending', next_attempt_at = ?, document_json = ?, updated_at = ?
405
- WHERE node_id = ? AND state = 'armed' AND ? IS NOT NULL`).run(now, outcome.documentJson ?? null, now, nodeId, outcome.documentJson ?? null);
406
- }
306
+ if ((outcome.supersede === true || latched.outcome_at === outcome.at) && outcome.documentJson !== null && outcome.documentJson !== undefined) {
307
+ activateArmedExternalActionDelivery(db, {
308
+ idempotencyKey: `node-outcome:${nodeId}`,
309
+ label: nodeId,
310
+ bodyJson: outcome.documentJson,
311
+ supersede: outcome.supersede === true,
312
+ });
407
313
  }
408
314
  return Number(result.changes);
409
315
  });
@@ -434,32 +340,6 @@ export function setPresence(nodeId, presence) {
434
340
  .prepare('UPDATE nodes SET tmux_session = ?, "window" = ?, pane = ? WHERE node_id = ?')
435
341
  .run(presence.tmux_session ?? null, presence.window ?? null, presence.pane ?? null, nodeId);
436
342
  }
437
- /** Record the live pi pid (daemon liveness signal) PLUS its launch-time
438
- * identity fingerprint, in one atomic write:
439
- * `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
440
- * taken RIGHT NOW, while `pid` is the process we just spawned/bound and
441
- * therefore definitely still exists — this is the LAUNCH-time baseline
442
- * `headlessBrokerHost.teardown()` later compares against before signaling
443
- * anything. A failed probe (rare — `ps` itself misbehaving) or a probe that
444
- * finds no row for `pid` (raced away in the instant between spawn and this
445
- * call) both write `NULL`: no baseline recorded, so the teardown guard fails
446
- * open exactly like a node booted before this column existed — never a false
447
- * identity that could itself cause a false mismatch later. */
448
- export function recordPid(nodeId, pid) {
449
- const identity = capturePidIdentities([pid])?.get(pid) ?? null;
450
- openDb()
451
- .prepare('UPDATE nodes SET pi_pid = ?, pi_pid_identity = ? WHERE node_id = ?')
452
- .run(pid, identity, nodeId);
453
- }
454
- /** Clear the pi pid (window-backed relaunch, before the fresh pi re-records it)
455
- * AND its recorded launch-time identity — the two are written together by
456
- * `recordPid` and must be cleared together, or a subsequent relaunch that
457
- * reuses the pid namespace could compare its own fresh pid against a STALE
458
- * identity left over from the pid this row held before. */
459
- export function clearRecordedPid(nodeId) {
460
- openDb().prepare('UPDATE nodes SET pi_pid = NULL, pi_pid_identity = NULL WHERE node_id = ?').run(nodeId);
461
- }
462
- export const clearPid = clearRecordedPid;
463
343
  export function recordLaunch(nodeId, at) {
464
344
  openDb()
465
345
  .prepare('UPDATE nodes SET launched_at = ?, frozen_at = NULL, respawn_not_before = NULL WHERE node_id = ?')
@@ -479,70 +359,6 @@ export function recordRespawnAttempt(nodeId, attempt) {
479
359
  .prepare('UPDATE nodes SET respawn_failures = ?, respawn_not_before = ? WHERE node_id = ?')
480
360
  .run(attempt.failures, attempt.notBefore, nodeId);
481
361
  }
482
- /** ONE-TIME completion of the boot-scoped-identity format migration, run once
483
- * at daemon startup (the natural boot-reconciliation chokepoint, right after
484
- * `reconcileBootLiveness`). It closes the gap the `identitiesMatch` legacy
485
- * lane only TOLERATES: a broker that survives an in-place crouter upgrade
486
- * keeps its old `lstart#ticks` row indefinitely (`recordPid` only fires on
487
- * launch/revive/session_start), so the cross-boot ticks-collision exposure the
488
- * boot-scoped base was added to close never provably shuts for those rows.
489
- *
490
- * For every row whose RECORDED identity is legacy-format while this platform
491
- * now composes new-format (a readable kernel boot_id):
492
- * - if the recorded pid's CURRENT live identity matches the recorded one via
493
- * the migration lane (same live process, `identitiesMatch` true) AND that
494
- * current identity is itself boot-scoped — re-record it with the new
495
- * composer, completing the migration in place;
496
- * - otherwise (pid gone, probe failed, reused — no match, or no readable
497
- * boot_id for it) — leave the row untouched for the normal liveness
498
- * handling that already runs each tick.
499
- *
500
- * After this runs once, the legacy compare lane only ever engages for
501
- * genuinely no-boot_id platforms. NOT a per-caller reconciliation layer — a
502
- * single startup sweep. `captureIdentity` is injectable for unit tests.
503
- *
504
- * GATED on proven same-boot provenance by its caller (daemon startup runs it
505
- * only when `reconcileBootLiveness` returned `sameBootProven` — see
506
- * `bootProvenSame`). This is what makes a bare ticks match safe to bless as a
507
- * current-boot identity: without that proof an equal-ticks collision with an
508
- * unrelated process from a DIFFERENT boot could be rewritten as
509
- * `<currentBootId>#<ticks>`, permanently baptizing a stranger.
510
- *
511
- * Not transactional by design: it never HOLDS the write lock across the (slow,
512
- * per-row) `ps` probe. Each re-record is a compare-and-swap single-row UPDATE
513
- * constrained by ALL three snapshotted values (`node_id`, `pi_pid`,
514
- * `pi_pid_identity`), counted only when a row actually changed. So if the old
515
- * broker exits and a concurrent revive atomically records a fresh
516
- * `(pi_pid, pi_pid_identity)` between this sweep's probe and its update, the
517
- * CAS matches zero rows and the migration is skipped rather than clobbering
518
- * the newer identity with the stale one. */
519
- export function migrateLegacyPidIdentities(captureIdentity = (pid) => capturePidIdentities([pid])?.get(pid)) {
520
- const db = openDb();
521
- const rows = db
522
- .prepare('SELECT node_id, pi_pid, pi_pid_identity FROM nodes WHERE pi_pid IS NOT NULL AND pi_pid_identity IS NOT NULL')
523
- .all();
524
- // Compare-and-swap: rewrite only if the row STILL carries the exact pid +
525
- // legacy identity we probed. A concurrent revive between probe and update
526
- // records a new (pi_pid, pi_pid_identity) pair, so this WHERE fails to match
527
- // and we skip rather than clobber the fresh baseline.
528
- const upd = db.prepare('UPDATE nodes SET pi_pid_identity = ? WHERE node_id = ? AND pi_pid = ? AND pi_pid_identity = ?');
529
- const migrated = [];
530
- for (const row of rows) {
531
- if (!isLegacyTicksIdentity(row.pi_pid_identity))
532
- continue;
533
- const current = captureIdentity(row.pi_pid);
534
- if (current === undefined)
535
- continue; // pid gone / probe failed → normal liveness handling
536
- if (!isBootScopedIdentity(current))
537
- continue; // no readable boot_id for it → nothing to upgrade
538
- if (!identitiesMatch(current, row.pi_pid_identity))
539
- continue; // reused → leave to liveness
540
- const res = upd.run(current, row.node_id, row.pi_pid, row.pi_pid_identity);
541
- if (res.changes > 0)
542
- migrated.push(row.node_id); // skipped if a concurrent revive re-recorded
543
- }
544
- return { migrated };
545
- }
546
362
  /** The one clause that hides unclaimed warm-pool spares from every listing.
547
363
  * A spare is a real node with a booted broker, but nobody has been handed it
548
364
  * yet — surfacing it would put a phantom empty conversation in the user's
@@ -564,6 +380,16 @@ export function listNodesByProfile(profileId) {
564
380
  });
565
381
  }
566
382
  /** All rows, optionally filtered by status and terminal execution traces. Unclaimed warm spares are excluded. */
383
+ /** Every node's durable memory-resolution coordinates, including unclaimed warm-pool spares. */
384
+ export function listNodeMemoryTargets() {
385
+ return openDb()
386
+ .prepare('SELECT node_id, cwd, profile_id FROM nodes ORDER BY created')
387
+ .all()
388
+ .map((row) => {
389
+ const node = row;
390
+ return { nodeId: node.node_id, cwd: node.cwd, profileId: node.profile_id };
391
+ });
392
+ }
567
393
  export function listNodes(filter) {
568
394
  const clauses = [NOT_A_SPARE];
569
395
  const params = [];
@@ -577,7 +403,7 @@ export function listNodes(filter) {
577
403
  if (filter?.withRecordedPidOrFrozen === true)
578
404
  clauses.push('(pi_pid IS NOT NULL OR frozen_at IS NOT NULL)');
579
405
  const rows = openDb()
580
- .prepare(`SELECT * FROM nodes WHERE ${clauses.join(' AND ')} ORDER BY created`)
406
+ .prepare(`SELECT * FROM node_rows WHERE ${clauses.join(' AND ')} ORDER BY created`)
581
407
  .all(...params);
582
408
  return rows.map(rowFrom);
583
409
  }
@@ -612,7 +438,7 @@ export function unregisterWarmSpare(nodeId) {
612
438
  function warmSpareRowsFor(db, recipeKey) {
613
439
  const sql = `SELECT w.node_id AS node_id, w.recipe_key AS recipe_key, w.created AS created,
614
440
  n.status AS status, n.pi_pid AS pi_pid, n.pi_pid_identity AS pi_pid_identity
615
- FROM warm_pool w JOIN nodes n ON n.node_id = w.node_id` +
441
+ FROM warm_pool w JOIN node_rows n ON n.node_id = w.node_id` +
616
442
  (recipeKey === undefined ? '' : ' WHERE w.recipe_key = ?') +
617
443
  ' ORDER BY w.created';
618
444
  const rows = (recipeKey === undefined ? db.prepare(sql).all() : db.prepare(sql).all(recipeKey));
@@ -662,8 +488,7 @@ function spareIsClaimable(row) {
662
488
  * The claim also restamps the row's `created` to `created` — birth time is a
663
489
  * spare's MINT time, and consumers sort conversations on it, so an hour-old
664
490
  * spare must not sort as an hour-old conversation. This is the one sanctioned
665
- * write of `created` after birth (`upsertRow` deliberately never touches it);
666
- * the caller mirrors the same stamp into meta.json. */
491
+ * write of `created` after birth. */
667
492
  export function claimWarmSpare(recipeKey, created, opts = {}) {
668
493
  return withCanvasWrite((db) => {
669
494
  const row = warmSpareRowsFor(db, recipeKey).find(spareIsClaimable);
@@ -694,7 +519,7 @@ export function childrenOf(id) {
694
519
  .all(id).map((r) => r.node_id);
695
520
  }
696
521
  /** Every node's lean roster row, oldest first — a single scan indexed by
697
- * `idx_nodes_created`. No per-row `getNode`/meta.json read, no N+1. */
522
+ * `idx_nodes_created`, with no per-row hydration or N+1 query. */
698
523
  export function rosterNodes() {
699
524
  return openDb()
700
525
  .prepare(`SELECT node_id, name, kind, mode, lifecycle, status, cwd, host_kind, parent, created
@@ -735,7 +560,7 @@ export function rosterEdges() {
735
560
  * last session HERE"), so it never teleports across projects. */
736
561
  export function listResumableRoots(cwd) {
737
562
  const rows = openDb()
738
- .prepare(`SELECT * FROM nodes
563
+ .prepare(`SELECT * FROM node_rows
739
564
  WHERE parent IS NULL AND kind != 'human'
740
565
  AND status IN ('active','idle') AND cwd = ?
741
566
  AND ${NOT_A_SPARE}
@@ -914,46 +739,6 @@ export function withFreshTerminalGuard(nodeId, isTerminal, body) {
914
739
  return { kind: 'ran', result: body() };
915
740
  });
916
741
  }
917
- // Index rebuild
918
- /** Rebuild node rows from on-disk metas (the db node table is a derived index).
919
- * Only the IDENTITY columns are rebuilt — they are a projection of meta. The
920
- * runtime columns (status/intent/pi_pid/window/tmux_session/pane) are NOT in meta
921
- * and NOT re-derivable from it: they describe live process/presence state, so
922
- * an existing row keeps them and a freshly re-created row takes the schema's
923
- * quiescent defaults (status='active', the rest null). The daemon reconciles
924
- * liveness from tmux reality, not from a stale file.
925
- * Edges are left intact — subscribes_to is db-authoritative; spawned_by is
926
- * re-derived from each meta's `spawned_by` (fallback: `parent` for legacy metas). */
927
- export function rebuildIndex() {
928
- if (!existsSync(nodesRoot()))
929
- return;
930
- // Collect every on-disk meta first, then index in TWO passes. Under the
931
- // edges→nodes FK (migration v4), a `spawned_by` edge insert whose endpoint
932
- // row isn't present yet violates the constraint — so ALL node rows must exist
933
- // before ANY edge is added.
934
- const metas = [];
935
- for (const id of readdirSync(nodesRoot())) {
936
- if (!existsSync(join(nodeDir(id), 'meta.json')))
937
- continue;
938
- const meta = readMeta(id);
939
- if (!meta)
940
- continue;
941
- metas.push(meta);
942
- }
943
- // Pass 1 — upsert every node row (the edge endpoints).
944
- for (const meta of metas)
945
- upsertRow(meta);
946
- // Pass 2 — add the audit-only `spawned_by` provenance edges. Skip any whose
947
- // provenance node has no on-disk meta (a deleted/pruned ancestor): the FK
948
- // would reject it, and an orphan provenance edge is exactly what v4 makes
949
- // unrepresentable.
950
- const known = new Set(metas.map((m) => m.node_id));
951
- for (const meta of metas) {
952
- const prov = meta.spawned_by ?? meta.parent;
953
- if (prov && known.has(prov))
954
- recordSpawn(meta.node_id, prov);
955
- }
956
- }
957
742
  /** True when this node's identity is still required to own a managed checkout.
958
743
  *
959
744
  * Ownership outlives the OPEN state. A closed record whose cleanup is still
@@ -969,10 +754,7 @@ export function rebuildIndex() {
969
754
  * the checkout and retains its branch for a person on purpose, so there is no
970
755
  * residual for the sweep to own. */
971
756
  function hasUnreconciledManagedWorktree(nodeId) {
972
- const wt = getNode(nodeId)?.managed_worktree;
973
- if (wt == null || wt.state === 'abandoned')
974
- return false;
975
- return wt.state === 'open' || wt.cleanup !== 'complete';
757
+ return hasUnreconciledWorktree(nodeId);
976
758
  }
977
759
  /** Whether a row may be removed by an automatic collection sweep. */
978
760
  export function isCollectible(row, opts = {}) {
@@ -1050,7 +832,7 @@ export function pruneNodes(opts) {
1050
832
  const selfId = envNodeId() ?? '';
1051
833
  const db = openDb();
1052
834
  const terminal = db
1053
- .prepare(`SELECT * FROM nodes
835
+ .prepare(`SELECT * FROM node_rows
1054
836
  WHERE status IN ('dead', 'done', 'canceled') AND created < ?
1055
837
  ORDER BY created`)
1056
838
  .all(cutoff)
@@ -1060,7 +842,7 @@ export function pruneNodes(opts) {
1060
842
  // Stale non-terminal sweep (opt-in): active | idle past the TTL whose process
1061
843
  // is provably gone (pi_pid NULL or not alive). Never the caller itself.
1062
844
  const stale = !includeStale ? [] : db
1063
- .prepare(`SELECT node_id, status, created, pi_pid FROM nodes
845
+ .prepare(`SELECT node_id, status, created, pi_pid FROM node_rows
1064
846
  WHERE status IN ('active', 'idle') AND created < ?
1065
847
  ORDER BY created`)
1066
848
  .all(cutoff)
@@ -1082,49 +864,6 @@ export function pruneNodes(opts) {
1082
864
  deletePrunedNodes(candidates);
1083
865
  return { pruned: candidates, dryRun };
1084
866
  }
1085
- /** Drop rows whose on-disk `nodes/<id>/` dir is gone — a node that no longer
1086
- * exists in the only place it is durably stored.
1087
- *
1088
- * `createNode` scaffolds the dir and writes meta.json BEFORE seeding the row, and
1089
- * both delete paths ({@link deleteNode}, `deletePrunedNodes`) commit the row
1090
- * removal BEFORE removing the dir, so a row with no dir is never a transient of
1091
- * either — it is a node whose durable state was lost. Nothing about it is
1092
- * recoverable: `getNode` reads meta.json and returns null, so every surface that
1093
- * resolves a node (revive, focus, close, rename, message) rejects it as unknown
1094
- * while the roster keeps listing it. The row IS the residue, so deleting it is
1095
- * the whole repair; there is no dir left to remove.
1096
- *
1097
- * Because `active`/`idle` ghosts are the ones that jam surfaces, status is not a
1098
- * filter here — count-based pruning protects those rows precisely so daemon
1099
- * crash-recovery can resume them, and that reason does not survive the dir being
1100
- * gone.
1101
- *
1102
- * Refuses to act on "can't tell": an unreadable nodes root, or a readable one
1103
- * holding no node dirs at all while rows exist, reads as a detached or wiped
1104
- * store rather than per-node loss, and reaps nothing. Returns the reaped ids. */
1105
- export function reapGhostRows() {
1106
- const rows = listNodes();
1107
- if (rows.length === 0)
1108
- return [];
1109
- let onDisk;
1110
- try {
1111
- onDisk = new Set(readdirSync(nodesRoot()));
1112
- }
1113
- catch {
1114
- return []; // nodes root absent or unreadable — can't tell, so touch nothing
1115
- }
1116
- if (onDisk.size === 0)
1117
- return [];
1118
- const ghosts = rows.map((r) => r.node_id).filter((id) => !onDisk.has(id));
1119
- if (ghosts.length === 0)
1120
- return [];
1121
- withCanvasWrite((db) => {
1122
- const del = db.prepare('DELETE FROM nodes WHERE node_id = ?');
1123
- for (const id of ghosts)
1124
- del.run(id);
1125
- });
1126
- return ghosts;
1127
- }
1128
867
  /** Hard-delete a set of pruned nodes in one transaction (the edges→nodes and
1129
868
  * anchored-crons FKs cascade their coupled state), followed by best-effort
1130
869
  * on-disk `nodes/<id>/` removal. Shared by TTL and count pruning so their