@north-light/crouter 0.3.233 → 0.3.235

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 (411) hide show
  1. package/dist/api/client.js +16 -15
  2. package/dist/api/dto/nodes.d.ts +11 -0
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +1 -1
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +14 -17
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -11
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +8 -8
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +10 -11
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +2 -2
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +3 -3
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -7
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +8 -8
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +6 -6
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +3 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +1 -1
  15. package/dist/clients/attach/chrome/ticket-panel.js +15 -2
  16. package/dist/clients/attach/overlays/graph.js +0 -3
  17. package/dist/clients/attach/overlays/mcp.js +1 -2
  18. package/dist/clients/attach/render/chat-view.js +0 -12
  19. package/dist/clients/attach/render/crtr-output.js +0 -2
  20. package/dist/clients/attach/render/tool-calls.js +0 -6
  21. package/dist/clients/attach/slash/dispatch.js +8 -11
  22. package/dist/clients/attach/viewer.js +535 -537
  23. package/dist/clients/conversation/projection.js +0 -2
  24. package/dist/clients/inbox/controller.js +22 -3
  25. package/dist/clients/inbox/review/companion-pane.js +0 -4
  26. package/dist/clients/inbox/review-adapter.js +0 -7
  27. package/dist/clients/inbox/tui/slots.d.ts +3 -3
  28. package/dist/clients/inbox/tui/types.d.ts +2 -2
  29. package/dist/commands/api-client.js +0 -8
  30. package/dist/commands/attention.js +1 -9
  31. package/dist/commands/canvas-browse.js +2 -1
  32. package/dist/commands/canvas-config.js +0 -10
  33. package/dist/commands/canvas-history.js +2 -1
  34. package/dist/commands/cron.js +10 -23
  35. package/dist/commands/dashboard.js +0 -2
  36. package/dist/commands/human/feedback.js +2 -1
  37. package/dist/commands/human/prompts.js +2 -5
  38. package/dist/commands/human/queue.js +2 -7
  39. package/dist/commands/human/request.js +0 -14
  40. package/dist/commands/human/review.js +2 -1
  41. package/dist/commands/human.js +1 -1
  42. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
  43. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.d.ts +1 -0
  44. package/dist/commands/memory/__tests__/memory-list-and-lint-ignore.test.js +96 -0
  45. package/dist/commands/memory/delete.js +1 -1
  46. package/dist/commands/memory/edit.js +6 -3
  47. package/dist/commands/memory/find.js +2 -2
  48. package/dist/commands/memory/lint.js +2 -1
  49. package/dist/commands/memory/list.d.ts +4 -0
  50. package/dist/commands/memory/list.js +20 -14
  51. package/dist/commands/memory/move.js +1 -1
  52. package/dist/commands/memory/read.d.ts +2 -7
  53. package/dist/commands/memory/read.js +5 -16
  54. package/dist/commands/memory/shared.d.ts +1 -0
  55. package/dist/commands/memory/shared.js +17 -8
  56. package/dist/commands/memory/write.js +8 -4
  57. package/dist/commands/node/bash.js +2 -1
  58. package/dist/commands/node/create.js +2 -1
  59. package/dist/commands/node/inspect.js +11 -10
  60. package/dist/commands/node/lifecycle.js +5 -13
  61. package/dist/commands/node/message.js +11 -6
  62. package/dist/commands/node/subscription.js +7 -2
  63. package/dist/commands/node/wait.js +9 -4
  64. package/dist/commands/node-context.js +2 -1
  65. package/dist/commands/node-lifecycle-revive.js +0 -2
  66. package/dist/commands/node-worktree.js +2 -1
  67. package/dist/commands/pkg/browse/actions.d.ts +1 -1
  68. package/dist/commands/pkg/browse/actions.js +4 -4
  69. package/dist/commands/pkg/browse/catalog.d.ts +1 -1
  70. package/dist/commands/pkg/browse/catalog.js +15 -15
  71. package/dist/commands/pkg/browse/command-view.d.ts +1 -1
  72. package/dist/commands/pkg/browse/command-view.js +3 -3
  73. package/dist/commands/pkg/browse/detail-panel.d.ts +1 -1
  74. package/dist/commands/pkg/browse/detail-panel.js +8 -8
  75. package/dist/commands/pkg/browse/doc-view.d.ts +1 -1
  76. package/dist/commands/pkg/browse/doc-view.js +3 -3
  77. package/dist/commands/pkg/browse/list-panel.d.ts +1 -1
  78. package/dist/commands/pkg/browse/list-panel.js +3 -3
  79. package/dist/commands/pkg/browse/model.d.ts +3 -3
  80. package/dist/commands/pkg/browse/shell.d.ts +2 -2
  81. package/dist/commands/pkg/browse/shell.js +13 -13
  82. package/dist/commands/pkg/plugin-manage.js +3 -2
  83. package/dist/commands/pkg/shared.js +0 -2
  84. package/dist/commands/pkg.js +1 -0
  85. package/dist/commands/profile/delete.js +2 -1
  86. package/dist/commands/profile/rename.js +1 -1
  87. package/dist/commands/push.js +2 -3
  88. package/dist/commands/revive.js +0 -2
  89. package/dist/commands/surface/node/placement.js +2 -1
  90. package/dist/commands/surface-edit.js +2 -1
  91. package/dist/commands/sys/config.js +1 -4
  92. package/dist/commands/sys/daemon.js +0 -10
  93. package/dist/commands/sys/doctor.js +3 -2
  94. package/dist/commands/sys/panels/broker-limits-panel.d.ts +2 -2
  95. package/dist/commands/sys/panels/broker-limits-panel.js +4 -4
  96. package/dist/commands/sys/panels/dependencies-panel.d.ts +2 -2
  97. package/dist/commands/sys/panels/dependencies-panel.js +2 -2
  98. package/dist/commands/sys/panels/keybindings-panel.d.ts +1 -1
  99. package/dist/commands/sys/panels/keybindings-panel.js +5 -5
  100. package/dist/commands/sys/panels/models-panel.d.ts +4 -4
  101. package/dist/commands/sys/panels/models-panel.js +11 -10
  102. package/dist/commands/sys/panels/plugins-panel.d.ts +2 -2
  103. package/dist/commands/sys/panels/plugins-panel.js +2 -2
  104. package/dist/commands/sys/panels/prefs-panel.d.ts +2 -2
  105. package/dist/commands/sys/panels/prefs-panel.js +4 -4
  106. package/dist/commands/sys/panels/profiles-panel.d.ts +1 -1
  107. package/dist/commands/sys/panels/profiles-panel.js +14 -13
  108. package/dist/commands/sys/panels/provider-panel.d.ts +3 -3
  109. package/dist/commands/sys/panels/provider-panel.js +8 -8
  110. package/dist/commands/sys/settings-shell.d.ts +3 -3
  111. package/dist/commands/sys/settings-shell.js +15 -15
  112. package/dist/commands/sys/setup-settings-runner.d.ts +3 -3
  113. package/dist/commands/sys/setup-settings-runner.js +5 -5
  114. package/dist/commands/sys/setup-wizard.d.ts +4 -4
  115. package/dist/commands/sys/setup-wizard.js +8 -8
  116. package/dist/commands/sys/support/prepare.js +4 -4
  117. package/dist/commands/sys/support/submit.js +2 -2
  118. package/dist/commands/sys/sync-deps.js +0 -2
  119. package/dist/commands/sys/sync-project-guidance.js +2 -1
  120. package/dist/commands/sys/sysprompt.js +2 -1
  121. package/dist/commands/sys.js +1 -0
  122. package/dist/core/__tests__/boot.test.js +0 -8
  123. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +23 -1
  124. package/dist/core/__tests__/canvas-db-boundary.test.d.ts +1 -0
  125. package/dist/core/__tests__/canvas-db-boundary.test.js +46 -0
  126. package/dist/core/__tests__/canvas.test.js +1 -1
  127. package/dist/core/__tests__/child-death-wake.test.js +0 -2
  128. package/dist/core/__tests__/cli-help-contract.test.d.ts +1 -0
  129. package/dist/core/__tests__/cli-help-contract.test.js +150 -0
  130. package/dist/core/__tests__/cron-armed-context-failure.test.d.ts +1 -0
  131. package/dist/core/__tests__/cron-armed-context-failure.test.js +145 -0
  132. package/dist/core/__tests__/daemon-wedge.test.js +0 -4
  133. package/dist/core/__tests__/dead-node-policy-table.test.js +0 -4
  134. package/dist/core/__tests__/fixtures/fake-engine.js +4 -14
  135. package/dist/core/__tests__/fixtures/fake-pi-host.js +5 -5
  136. package/dist/core/__tests__/focuses.test.js +0 -6
  137. package/dist/core/__tests__/fs-utils.test.d.ts +1 -0
  138. package/dist/core/__tests__/fs-utils.test.js +50 -0
  139. package/dist/core/__tests__/helpers/harness.js +1 -1
  140. package/dist/core/__tests__/inline-memory-refs.test.js +0 -2
  141. package/dist/core/__tests__/integration/broker-fork-seam.test.js +0 -2
  142. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +3 -16
  143. package/dist/core/__tests__/integration/command-plugins.test.js +21 -24
  144. package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -6
  145. package/dist/core/__tests__/integration/flagship-lifecycle.test.js +0 -26
  146. package/dist/core/__tests__/integration/live-mutation-verbs.test.js +0 -4
  147. package/dist/core/__tests__/integration/live-mutation.test.js +1 -5
  148. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +1 -5
  149. package/dist/core/__tests__/integration/revive.test.js +0 -12
  150. package/dist/core/__tests__/integration/subscription-delivery.test.js +0 -6
  151. package/dist/core/__tests__/integration/tmux-surface.test.js +24 -8
  152. package/dist/core/__tests__/integration/worktree-reap.test.js +0 -6
  153. package/dist/core/__tests__/lifecycle.test.js +0 -18
  154. package/dist/core/__tests__/memory-resolver-precedence.test.js +1 -1
  155. package/dist/core/__tests__/migration.test.js +2 -2
  156. package/dist/core/__tests__/on-read-dedup-resume.test.js +3 -3
  157. package/dist/core/__tests__/pid-identity-match.test.js +1 -1
  158. package/dist/core/__tests__/plugin-kinds.test.js +1 -1
  159. package/dist/core/__tests__/plugin-page-components.test.js +1 -1
  160. package/dist/core/__tests__/preview-registry-sync.test.js +42 -1
  161. package/dist/core/__tests__/respawn-throttle.test.js +0 -4
  162. package/dist/core/__tests__/revive-all.test.js +5 -5
  163. package/dist/core/__tests__/revive-parked-fresh.test.js +62 -11
  164. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +0 -8
  165. package/dist/core/__tests__/seam/broker-attach-stream.test.js +0 -8
  166. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +0 -2
  167. package/dist/core/__tests__/tmux-driver-boundary.test.d.ts +1 -0
  168. package/dist/core/__tests__/tmux-driver-boundary.test.js +50 -0
  169. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +11 -6
  170. package/dist/core/activity-signal.d.ts +7 -0
  171. package/dist/core/activity-signal.js +38 -0
  172. package/dist/core/auto-update.js +2 -1
  173. package/dist/core/bootstrap.js +2 -1
  174. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
  175. package/dist/core/canvas/__tests__/remote-transport.test.js +1 -1
  176. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  177. package/dist/core/canvas/browse/app.js +3 -2
  178. package/dist/core/canvas/browse/model.d.ts +1 -1
  179. package/dist/core/canvas/browse/model.js +0 -4
  180. package/dist/core/canvas/canvas.d.ts +22 -0
  181. package/dist/core/canvas/canvas.js +61 -19
  182. package/dist/core/canvas/crons.js +0 -6
  183. package/dist/core/canvas/db.d.ts +14 -20
  184. package/dist/core/canvas/db.js +30 -1332
  185. package/dist/core/canvas/focuses.js +0 -4
  186. package/dist/core/canvas/history.js +2 -1
  187. package/dist/core/canvas/meta-file.js +18 -27
  188. package/dist/core/canvas/migrations.d.ts +20 -0
  189. package/dist/core/canvas/migrations.js +1339 -0
  190. package/dist/core/canvas/nav-model.js +1 -11
  191. package/dist/core/canvas/nav-render.js +0 -8
  192. package/dist/core/canvas/remote-canvas-source.d.ts +8 -0
  193. package/dist/core/canvas/remote-canvas-source.js +9 -3
  194. package/dist/core/canvas/render-source.js +31 -13
  195. package/dist/core/command-manifests/manifest.js +0 -10
  196. package/dist/core/command-manifests/registry.d.ts +2 -0
  197. package/dist/core/command-manifests/schema.js +0 -4
  198. package/dist/core/command-plugins/compose.js +4 -2
  199. package/dist/core/command-plugins/discovery.d.ts +1 -0
  200. package/dist/core/command-plugins/discovery.js +1 -1
  201. package/dist/core/command-plugins/transport/http-invoke.js +0 -14
  202. package/dist/core/command.js +2 -6
  203. package/dist/core/config.js +2 -1
  204. package/dist/core/fault-classifier.js +8 -0
  205. package/dist/core/feed/inbox.d.ts +10 -0
  206. package/dist/core/feed/inbox.js +18 -12
  207. package/dist/core/fs-utils.d.ts +9 -2
  208. package/dist/core/fs-utils.js +7 -15
  209. package/dist/core/help.js +0 -8
  210. package/dist/core/host-exports/export.d.ts +2 -2
  211. package/dist/core/host-exports/export.js +9 -12
  212. package/dist/core/human/answer.d.ts +2 -2
  213. package/dist/core/human/convention.d.ts +2 -5
  214. package/dist/core/human/convention.js +3 -13
  215. package/dist/core/human/page-schema.d.ts +2 -2
  216. package/dist/core/human/page-schema.js +2 -2
  217. package/dist/core/human/root.js +6 -23
  218. package/dist/core/human/scan.js +24 -7
  219. package/dist/core/human/tickets.d.ts +4 -1
  220. package/dist/core/human/tickets.js +20 -6
  221. package/dist/core/io.js +0 -12
  222. package/dist/core/memory/identity.js +0 -4
  223. package/dist/core/memory/inline-ref-inventory.js +3 -2
  224. package/dist/core/memory/lint.js +3 -6
  225. package/dist/core/memory-resolver.d.ts +5 -0
  226. package/dist/core/memory-resolver.js +14 -12
  227. package/dist/core/pagination.d.ts +2 -0
  228. package/dist/core/pagination.js +2 -3
  229. package/dist/core/preview-registry.d.ts +14 -0
  230. package/dist/core/preview-registry.js +2 -4
  231. package/dist/core/preview-result-path.js +2 -1
  232. package/dist/core/profiles/manifest.d.ts +37 -5
  233. package/dist/core/profiles/manifest.js +44 -24
  234. package/dist/core/profiles/select.js +0 -4
  235. package/dist/core/review/signal.js +4 -23
  236. package/dist/core/review/ticket-filter.d.ts +3 -3
  237. package/dist/core/review/ticket-filter.js +4 -9
  238. package/dist/core/runtime/__tests__/engine-route.test.js +1 -1
  239. package/dist/core/runtime/__tests__/spawn-env.test.js +0 -8
  240. package/dist/core/runtime/bearings-render.d.ts +56 -0
  241. package/dist/core/runtime/bearings-render.js +241 -0
  242. package/dist/core/runtime/bearings.d.ts +2 -17
  243. package/dist/core/runtime/bearings.js +12 -198
  244. package/dist/core/runtime/broker/auth-reload.d.ts +39 -0
  245. package/dist/core/runtime/broker/auth-reload.js +149 -0
  246. package/dist/core/runtime/broker/engine-drive.d.ts +26 -0
  247. package/dist/core/runtime/broker/engine-drive.js +257 -0
  248. package/dist/core/runtime/broker/engine-routing.d.ts +143 -0
  249. package/dist/core/runtime/broker/engine-routing.js +211 -0
  250. package/dist/core/runtime/broker/event-projection.js +3 -2
  251. package/dist/core/runtime/broker/fault-retry.js +11 -4
  252. package/dist/core/runtime/broker/frame-client.d.ts +38 -0
  253. package/dist/core/runtime/broker/frame-client.js +129 -0
  254. package/dist/core/runtime/broker/frame-dispatch.d.ts +12 -189
  255. package/dist/core/runtime/broker/frame-dispatch.js +89 -1476
  256. package/dist/core/runtime/broker/frame-engine-control.d.ts +20 -0
  257. package/dist/core/runtime/broker/frame-engine-control.js +231 -0
  258. package/dist/core/runtime/broker/frame-memory-refs.d.ts +19 -0
  259. package/dist/core/runtime/broker/frame-memory-refs.js +41 -0
  260. package/dist/core/runtime/broker/frame-model-query.d.ts +13 -0
  261. package/dist/core/runtime/broker/frame-model-query.js +69 -0
  262. package/dist/core/runtime/broker/frame-read.d.ts +19 -0
  263. package/dist/core/runtime/broker/frame-read.js +116 -0
  264. package/dist/core/runtime/broker/frame-replies.d.ts +24 -0
  265. package/dist/core/runtime/broker/frame-replies.js +64 -0
  266. package/dist/core/runtime/broker/frame-session.d.ts +34 -0
  267. package/dist/core/runtime/broker/frame-session.js +203 -0
  268. package/dist/core/runtime/broker/rebind.js +50 -0
  269. package/dist/core/runtime/broker/turn-ignition.d.ts +42 -0
  270. package/dist/core/runtime/broker/turn-ignition.js +74 -0
  271. package/dist/core/runtime/broker-extension-render.js +9 -85
  272. package/dist/core/runtime/broker-protocol.js +0 -4
  273. package/dist/core/runtime/broker-sdk.js +2 -1
  274. package/dist/core/runtime/broker.d.ts +1 -1
  275. package/dist/core/runtime/broker.js +5 -23
  276. package/dist/core/runtime/fault.js +3 -2
  277. package/dist/core/runtime/headless-pi.js +3 -2
  278. package/dist/core/runtime/host.js +2 -3
  279. package/dist/core/runtime/kickoff.js +0 -4
  280. package/dist/core/runtime/launch-target.js +2 -1
  281. package/dist/core/runtime/launch.d.ts +2 -2
  282. package/dist/core/runtime/launch.js +20 -10
  283. package/dist/core/runtime/lifecycle.js +17 -26
  284. package/dist/core/runtime/managed-provider-cooling.d.ts +1 -1
  285. package/dist/core/runtime/naming.js +0 -2
  286. package/dist/core/runtime/nodes.js +2 -1
  287. package/dist/core/runtime/persona.js +0 -6
  288. package/dist/core/runtime/placement-tmux.js +2 -5
  289. package/dist/core/runtime/placement.js +4 -11
  290. package/dist/core/runtime/recap.js +4 -3
  291. package/dist/core/runtime/reopen.js +4 -7
  292. package/dist/core/runtime/reset.js +0 -4
  293. package/dist/core/runtime/resume-root.js +0 -2
  294. package/dist/core/runtime/revive.js +17 -13
  295. package/dist/core/runtime/session-list-cache.js +2 -2
  296. package/dist/core/runtime/shell-expansion.js +0 -2
  297. package/dist/core/runtime/spawn.js +0 -2
  298. package/dist/core/runtime/stop-guard.js +28 -16
  299. package/dist/core/runtime/stream-watchdog.d.ts +0 -1
  300. package/dist/core/runtime/stream-watchdog.js +13 -18
  301. package/dist/core/runtime/tmux-bindings.js +0 -2
  302. package/dist/core/runtime/tmux-driver.d.ts +12 -0
  303. package/dist/core/runtime/tmux-driver.js +18 -13
  304. package/dist/core/runtime/warm-pool.js +0 -4
  305. package/dist/core/scope.js +4 -3
  306. package/dist/core/spawn.js +2 -3
  307. package/dist/core/subscription-state.js +10 -17
  308. package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +3 -3
  309. package/dist/core/substrate/frontmatter-validation.d.ts +1 -0
  310. package/dist/core/substrate/frontmatter-validation.js +1 -1
  311. package/dist/core/substrate/render.js +0 -12
  312. package/dist/core/substrate/schema.js +0 -12
  313. package/dist/core/substrate/surface-match.js +0 -2
  314. package/dist/core/termrender/display.js +2 -1
  315. package/dist/core/tui/page-host.d.ts +3 -3
  316. package/dist/core/tui/page-host.js +4 -4
  317. package/dist/core/tui/panel.js +3 -3
  318. package/dist/core/wake.js +0 -12
  319. package/dist/core/worktree.js +0 -2
  320. package/dist/daemon/__tests__/reconciler-signature.test.d.ts +1 -0
  321. package/dist/daemon/__tests__/reconciler-signature.test.js +36 -0
  322. package/dist/daemon/api/__tests__/error-seam.test.js +27 -0
  323. package/dist/daemon/api/__tests__/seam/api-server.test.js +2 -16
  324. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +3 -11
  325. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +0 -6
  326. package/dist/daemon/api/handlers/attach.js +2 -1
  327. package/dist/daemon/api/handlers/broker-ops.js +3 -6
  328. package/dist/daemon/api/handlers/canvas.js +12 -18
  329. package/dist/daemon/api/handlers/feedback-comments.js +0 -8
  330. package/dist/daemon/api/handlers/health.js +3 -2
  331. package/dist/daemon/api/handlers/human-requests.js +0 -10
  332. package/dist/daemon/api/handlers/human.js +1 -5
  333. package/dist/daemon/api/handlers/inbox.js +0 -18
  334. package/dist/daemon/api/handlers/memory.js +1 -1
  335. package/dist/daemon/api/handlers/messages.js +16 -20
  336. package/dist/daemon/api/handlers/nodes.js +23 -22
  337. package/dist/daemon/api/handlers/reports.js +1 -1
  338. package/dist/daemon/api/handlers/reviews.js +1 -6
  339. package/dist/daemon/api/handlers/worktree.js +6 -22
  340. package/dist/daemon/api/map.d.ts +4 -2
  341. package/dist/daemon/api/map.js +22 -8
  342. package/dist/daemon/api/router.js +0 -4
  343. package/dist/daemon/api/server.js +2 -5
  344. package/dist/daemon/boot-migrations.d.ts +14 -0
  345. package/dist/daemon/boot-migrations.js +31 -0
  346. package/dist/daemon/cron/armed-context.d.ts +21 -0
  347. package/dist/daemon/cron/armed-context.js +100 -0
  348. package/dist/daemon/cron/capture.d.ts +20 -0
  349. package/dist/daemon/cron/capture.js +45 -0
  350. package/dist/daemon/cron/disposition.d.ts +19 -0
  351. package/dist/daemon/cron/disposition.js +52 -0
  352. package/dist/daemon/cron/escalation.d.ts +12 -0
  353. package/dist/daemon/cron/escalation.js +124 -0
  354. package/dist/daemon/cron/held.d.ts +4 -0
  355. package/dist/daemon/cron/held.js +20 -0
  356. package/dist/daemon/cron/lease-recovery.d.ts +3 -0
  357. package/dist/daemon/cron/lease-recovery.js +119 -0
  358. package/dist/daemon/cron/live-runs.d.ts +11 -0
  359. package/dist/daemon/cron/live-runs.js +23 -0
  360. package/dist/daemon/cron/passes.d.ts +11 -0
  361. package/dist/daemon/cron/passes.js +78 -0
  362. package/dist/daemon/cron/settle.d.ts +28 -0
  363. package/dist/daemon/cron/settle.js +37 -0
  364. package/dist/daemon/cron/sinks.d.ts +2 -0
  365. package/dist/daemon/cron/sinks.js +100 -0
  366. package/dist/daemon/cron-run.d.ts +5 -41
  367. package/dist/daemon/cron-run.js +193 -882
  368. package/dist/daemon/crtrd-cli.js +2 -1
  369. package/dist/daemon/crtrd.js +22 -22
  370. package/dist/daemon/fleet.js +0 -4
  371. package/dist/daemon/human/deliver-action.js +2 -1
  372. package/dist/daemon/human/sweep.js +15 -4
  373. package/dist/daemon/manage.d.ts +2 -2
  374. package/dist/daemon/manage.js +4 -13
  375. package/dist/daemon/pidfile.js +2 -2
  376. package/dist/daemon/reconcilers/broker-supervision.js +27 -30
  377. package/dist/daemon/reconcilers/dormant-inbox.d.ts +1 -1
  378. package/dist/daemon/reconcilers/dormant-inbox.js +2 -1
  379. package/dist/daemon/reconcilers/live-obligation.js +2 -2
  380. package/dist/daemon/reconcilers/pending-review-submit.d.ts +1 -1
  381. package/dist/daemon/reconcilers/pending-review-submit.js +2 -1
  382. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -1
  383. package/dist/daemon/reconcilers/storage-maintenance.js +2 -1
  384. package/dist/daemon/review/companion.js +2 -2
  385. package/dist/daemon/review/sweep.js +8 -3
  386. package/dist/migrations/001-surfaces-frontmatter.js +0 -6
  387. package/dist/migrations/corpus.js +2 -5
  388. package/dist/migrations/registry.js +15 -2
  389. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +0 -4
  390. package/dist/pi-extensions/broker-local.js +2 -1
  391. package/dist/pi-extensions/canvas-bash-valve.js +7 -20
  392. package/dist/pi-extensions/canvas-context-intro.js +2 -7
  393. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  394. package/dist/pi-extensions/canvas-goal-capture.js +2 -1
  395. package/dist/pi-extensions/canvas-inbox-watcher.js +5 -12
  396. package/dist/pi-extensions/canvas-passive-context.js +2 -5
  397. package/dist/pi-extensions/canvas-preview-result.js +2 -1
  398. package/dist/pi-extensions/canvas-prompt-scrub.js +2 -1
  399. package/dist/pi-extensions/canvas-recap.js +3 -16
  400. package/dist/pi-extensions/canvas-review-boundary.js +2 -5
  401. package/dist/pi-extensions/canvas-stophook.js +2 -26
  402. package/dist/pi-extensions/canvas-structured-output.js +2 -1
  403. package/dist/pi-extensions/canvas-tool-guide.js +2 -1
  404. package/dist/shared/__tests__/env-boundary.test.d.ts +1 -0
  405. package/dist/shared/__tests__/env-boundary.test.js +93 -0
  406. package/dist/shared/env.d.ts +68 -0
  407. package/dist/shared/env.js +171 -0
  408. package/package.json +3 -1
  409. package/runtime.lock.json +2 -2
  410. package/dist/core/canvas/render.d.ts +0 -47
  411. package/dist/core/canvas/render.js +0 -245
@@ -94,6 +94,8 @@ export declare const REVIEW_ERROR_MAP: Readonly<Record<ReviewErrorCode, {
94
94
  * `finalization_forbidden`, or 404 `node_not_found`;
95
95
  * - a `NodeIdConflictError` (a `--node-id`/`node_id` spawn at an id that
96
96
  * already exists) → 409 `node_id_exists`;
97
+ * - a `WorktreeError` → its own code at 400 (404 for `unknown_node`), with
98
+ * `next` + git `detail` preserved as CrtrError details;
97
99
  * - `reviveNode`'s finalization-latch throw → 409 `already_finalized`;
98
100
  * - an "unknown node" throw (reviveNode / transition / getNode-null handler) →
99
101
  * 404 `node_not_found`;
@@ -104,7 +106,7 @@ export declare const REVIEW_ERROR_MAP: Readonly<Record<ReviewErrorCode, {
104
106
  * The recognized plain-`Error` cases match on the throw sites' messages (the
105
107
  * only signal those sites carry); the source sites are named above so a message
106
108
  * change there is caught by the A-11 tests. */
107
- export declare function errorToStatus(err: unknown): {
109
+ export declare function errorToStatus(thrown: unknown): {
108
110
  status: number;
109
111
  code: string;
110
112
  };
@@ -115,7 +117,7 @@ export declare function errorToStatus(err: unknown): {
115
117
  * `internal error`: it surfaces the bounded cause (`boundedCause`) so a caller
116
118
  * is never left with a causeless internal, while the router logs the full
117
119
  * error (with stack) server-side. */
118
- export declare function toErrorBody(err: unknown): {
120
+ export declare function toErrorBody(thrown: unknown): {
119
121
  status: number;
120
122
  body: ErrorBody;
121
123
  };
@@ -16,13 +16,12 @@ import { FinalizationError } from '../../core/feed/feed.js';
16
16
  import { NodeIdConflictError } from '../../core/runtime/nodes.js';
17
17
  import { readContextTokens } from '../../core/canvas/telemetry.js';
18
18
  import { CrtrError } from '../../core/errors.js';
19
+ import { WorktreeError } from '../../core/worktree.js';
19
20
  import { ExitCode } from '../../types.js';
20
21
  import { sinkDisplay } from '../cron-sink.js';
21
22
  import { responsePath } from '../../core/human/convention.js';
22
23
  import { ticketDir } from '../../core/human/root.js';
23
- // ---------------------------------------------------------------------------
24
24
  // Node projections
25
- // ---------------------------------------------------------------------------
26
25
  /** Project the queryable row (what `listNodes` yields) into a summary DTO. */
27
26
  export function toNodeSummaryDTO(row) {
28
27
  return {
@@ -144,9 +143,7 @@ export function toNodeDetailDTO(meta, edges) {
144
143
  dto.worktree = toNodeWorktreeDTO(meta.managed_worktree);
145
144
  return dto;
146
145
  }
147
- // ---------------------------------------------------------------------------
148
146
  // Action-result projections
149
- // ---------------------------------------------------------------------------
150
147
  /** Project a `reviveNode` result. `revived` is true only when a fresh launch
151
148
  * actually happened — the double-revive no-op (already-alive) returns without a
152
149
  * `launch` handle, so its absence is the "nothing relaunched" signal. `status`
@@ -398,9 +395,7 @@ export function toFeedbackResultDTO(result) {
398
395
  }
399
396
  const _exitIntentParity = true;
400
397
  void _exitIntentParity;
401
- // ---------------------------------------------------------------------------
402
398
  // Error → HTTP status mapping (spec §8)
403
- // ---------------------------------------------------------------------------
404
399
  /** The only internal review-error to public HTTP projection. Every internal
405
400
  * review failure is listed explicitly so handlers never need to invent a status
406
401
  * or public code. */
@@ -436,6 +431,8 @@ export const REVIEW_ERROR_MAP = Object.freeze({
436
431
  * `finalization_forbidden`, or 404 `node_not_found`;
437
432
  * - a `NodeIdConflictError` (a `--node-id`/`node_id` spawn at an id that
438
433
  * already exists) → 409 `node_id_exists`;
434
+ * - a `WorktreeError` → its own code at 400 (404 for `unknown_node`), with
435
+ * `next` + git `detail` preserved as CrtrError details;
439
436
  * - `reviveNode`'s finalization-latch throw → 409 `already_finalized`;
440
437
  * - an "unknown node" throw (reviveNode / transition / getNode-null handler) →
441
438
  * 404 `node_not_found`;
@@ -446,7 +443,8 @@ export const REVIEW_ERROR_MAP = Object.freeze({
446
443
  * The recognized plain-`Error` cases match on the throw sites' messages (the
447
444
  * only signal those sites carry); the source sites are named above so a message
448
445
  * change there is caught by the A-11 tests. */
449
- export function errorToStatus(err) {
446
+ export function errorToStatus(thrown) {
447
+ const err = normalizeThrow(thrown);
450
448
  // An already-shaped API-style throw (e.g. a re-thrown ApiError) — trust it.
451
449
  if (typeof err === 'object' &&
452
450
  err !== null &&
@@ -485,6 +483,21 @@ export function errorToStatus(err) {
485
483
  }
486
484
  return { status: 500, code: 'internal' };
487
485
  }
486
+ /** A `WorktreeError` is bad input or a repository-state conflict, never a daemon
487
+ * fault, so it is projected onto the `CrtrError` contract: its specific code,
488
+ * its `next` remediation, and its git `detail` all survive to the caller
489
+ * instead of collapsing into an opaque 500. The details carry the FULL
490
+ * `{error, message, next}` payload so `apiErrorToCliError` reconstructs the
491
+ * worktree contract verbatim rather than degrading to `invalid_request`. */
492
+ function normalizeThrow(err) {
493
+ if (!(err instanceof WorktreeError))
494
+ return err;
495
+ const exitCode = err.code === 'unknown_node' ? ExitCode.NOT_FOUND : ExitCode.GENERAL;
496
+ const details = { error: err.code, message: err.message, next: err.next };
497
+ if (err.detail !== undefined)
498
+ details['received'] = err.detail;
499
+ return new CrtrError(err.code, err.message, exitCode, details);
500
+ }
488
501
  /** Remove the store row from a terminal-conflict detail before it reaches the wire.
489
502
  * Only the approved `ReviewDTO` fields cross this boundary; all other review
490
503
  * errors retain their explicitly specified internal detail object. */
@@ -545,7 +558,8 @@ function boundedCause(err) {
545
558
  * `internal error`: it surfaces the bounded cause (`boundedCause`) so a caller
546
559
  * is never left with a causeless internal, while the router logs the full
547
560
  * error (with stack) server-side. */
548
- export function toErrorBody(err) {
561
+ export function toErrorBody(thrown) {
562
+ const err = normalizeThrow(thrown);
549
563
  const { status, code } = errorToStatus(err);
550
564
  if (status >= 500) {
551
565
  return { status, body: { error: { code, message: boundedCause(err) } } };
@@ -102,9 +102,7 @@ export class Router {
102
102
  }
103
103
  }
104
104
  }
105
- // ---------------------------------------------------------------------------
106
105
  // Matching
107
- // ---------------------------------------------------------------------------
108
106
  /** Split a path/pattern into non-empty segments (leading/trailing '/' ignored). */
109
107
  function splitPath(p) {
110
108
  return p.split('/').filter((s) => s.length > 0);
@@ -127,9 +125,7 @@ function matchSegments(pattern, actual) {
127
125
  }
128
126
  return params;
129
127
  }
130
- // ---------------------------------------------------------------------------
131
128
  // Body + response I/O
132
- // ---------------------------------------------------------------------------
133
129
  class BodyTooLargeError extends Error {
134
130
  }
135
131
  /** Read and JSON-parse the request body. Returns `undefined` for an empty body.
@@ -16,6 +16,7 @@ import { createServer } from 'node:http';
16
16
  import { chmodSync, existsSync, unlinkSync } from 'node:fs';
17
17
  import { apiSocketPath } from '../../core/canvas/paths.js';
18
18
  import { emitEvent } from '../../core/events/emit.js';
19
+ import { envCrtrdTcp } from '../../shared/env.js';
19
20
  import { Router } from './router.js';
20
21
  import { handleAttachUpgrade } from './bridge.js';
21
22
  import { attachRoutes } from './handlers/attach.js';
@@ -204,10 +205,8 @@ export function createApiServer(opts = {}) {
204
205
  emitEvent({ event: 'api.server.failed', level: 'error', error: err });
205
206
  });
206
207
  };
207
- // -------------------------------------------------------------------------
208
208
  // Unix socket — ALWAYS bound. Unlink any stale socket first (mirror
209
209
  // broker.ts's listener setup), listen, then chmod 0600 once the file exists.
210
- // -------------------------------------------------------------------------
211
210
  const socketServer = createServer(onRequest);
212
211
  socketServer.on('upgrade', onUpgrade);
213
212
  bindErrorHandler(socketServer);
@@ -230,11 +229,9 @@ export function createApiServer(opts = {}) {
230
229
  });
231
230
  }
232
231
  });
233
- // -------------------------------------------------------------------------
234
232
  // TCP — opt-in only. A SECOND http.Server sharing the same handlers.
235
- // -------------------------------------------------------------------------
236
233
  let tcpServer;
237
- const tcpSpec = opts.tcp ?? process.env['CRTRD_TCP'];
234
+ const tcpSpec = opts.tcp ?? envCrtrdTcp();
238
235
  if (tcpSpec !== undefined && tcpSpec !== '') {
239
236
  const parsed = parseTcp(tcpSpec);
240
237
  if (parsed === null) {
@@ -0,0 +1,14 @@
1
+ export interface BootMigrationResult {
2
+ changed: boolean;
3
+ summary: Record<string, unknown>;
4
+ }
5
+ export interface BootMigration {
6
+ readonly id: string;
7
+ readonly description: string;
8
+ /** Runs unconditionally at every daemon boot. Must be safe to re-run every
9
+ * time (idempotent via its own convergence, not a version gate) and must
10
+ * isolate its own per-item failures — a thrown error here is NOT caught by
11
+ * the boot sequence and would abort daemon startup. */
12
+ apply(): BootMigrationResult;
13
+ }
14
+ export declare const BOOT_MIGRATIONS: readonly BootMigration[];
@@ -0,0 +1,31 @@
1
+ // The ordered boot-migration list — the THIRD of this repo's three migration
2
+ // homes, and the one for filesystem repairs that must run unconditionally at
3
+ // every daemon boot, before supervision can revive anything. No version gate
4
+ // (there is no single database to hold a `user_version`, and no `crtr sys
5
+ // migrate` invocation to drive it): each entry converges on its own, the same
6
+ // way `migrateLegacyInboxIdentities` already does — a re-run over
7
+ // already-migrated state rewrites nothing.
8
+ //
9
+ // The other two homes:
10
+ // - `../core/canvas/migrations.ts` — canvas.db's versioned schema chain,
11
+ // gated by `PRAGMA user_version`, applied once per database inside
12
+ // `migrate()`.
13
+ // - `../migrations/registry.ts` — the `STATE_MIGRATIONS` registry for
14
+ // on-disk memory documents, profile manifests, and the corpus lane,
15
+ // driven by `crtr sys migrate`.
16
+ //
17
+ // Authoring contract: one entry per boot-time filesystem repair, appended
18
+ // here in order. `crtrd.ts` iterates this list once at boot and emits one
19
+ // `daemon.boot_migration.applied` event per entry that reports a change —
20
+ // add a step here instead of a bespoke call in the boot sequence.
21
+ import { migrateLegacyInboxIdentities } from '../core/feed/inbox-migrate.js';
22
+ export const BOOT_MIGRATIONS = [
23
+ {
24
+ id: 'legacy-inbox-identities',
25
+ description: 'normalize pre-cutover inbox entry/operation ids (UUID-spelled or missing) and cursors (ISO-timestamp boundaries) into canonical 32-lowercase-hex identities the strict inbox reader requires.',
26
+ apply: () => {
27
+ const summary = migrateLegacyInboxIdentities();
28
+ return { changed: summary.nodes > 0 || summary.failures > 0, summary: { ...summary } };
29
+ },
30
+ },
31
+ ];
@@ -0,0 +1,21 @@
1
+ import type { Cron } from '../../core/canvas/crons.js';
2
+ /** The cron's profile, re-validated now and returned in canonical id form.
3
+ *
4
+ * A DELETED profile is the spec's root fallback: null. An UNREADABLE one is
5
+ * not — the profile is still there, so running as root would execute the
6
+ * command under an identity nobody armed, with the profile's own env store and
7
+ * purview silently replaced. That throws, and the run fails loudly once
8
+ * instead of running wrong on every tick. */
9
+ export declare function resolveLiveProfile(c: Cron): string | null;
10
+ export declare function resolveLiveProfileOrRoot(c: Cron): string | null;
11
+ export declare function resolveLiveProfileName(c: Cron): string | undefined;
12
+ /** A stored context cannot be rebuilt; the run fails rather than running under a context nobody armed. */
13
+ export declare class ArmedContextError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ export interface ArmedRunContext {
17
+ profileId: string | null;
18
+ env: NodeJS.ProcessEnv;
19
+ }
20
+ /** Rebuild the armed context. Corrupt snapshots and unreadable profiles fail rather than silently changing its identity or environment. */
21
+ export declare function buildArmedRunContext(c: Cron, runId: string): ArmedRunContext;
@@ -0,0 +1,100 @@
1
+ import { envHomeOverride } from '../../shared/env.js';
2
+ import { loadProfileManifest, lookupProfile } from '../../core/profiles/manifest.js';
3
+ import { readProfileEnvVars } from '../../core/profiles/env-store.js';
4
+ import { ADMITTED_HOST_PATH_ENV, buildOperationalEnvBase } from '../../core/runtime/spawn-env.js';
5
+ /** The cron's profile, re-validated now and returned in canonical id form.
6
+ *
7
+ * A DELETED profile is the spec's root fallback: null. An UNREADABLE one is
8
+ * not — the profile is still there, so running as root would execute the
9
+ * command under an identity nobody armed, with the profile's own env store and
10
+ * purview silently replaced. That throws, and the run fails loudly once
11
+ * instead of running wrong on every tick. */
12
+ export function resolveLiveProfile(c) {
13
+ if (c.profile == null)
14
+ return null;
15
+ let lookup;
16
+ try {
17
+ lookup = lookupProfile(c.profile);
18
+ }
19
+ catch {
20
+ return null; // an ambiguous NAME resolves no better than a deleted profile
21
+ }
22
+ if (lookup.kind === 'resolved')
23
+ return lookup.entry.profileId;
24
+ if (lookup.kind === 'absent')
25
+ return null;
26
+ throw new ArmedContextError(`the armed profile "${c.profile}" is still on this host but its manifest could not be read (${lookup.manifestPath}) — running as root would silently give this cron a different identity, env store, and purview. Repair the manifest, or re-arm the cron under a profile that resolves.`);
27
+ }
28
+ // The escalation node must still report an unreadable manifest, so its own spawn falls back to root.
29
+ export function resolveLiveProfileOrRoot(c) {
30
+ try {
31
+ return resolveLiveProfile(c);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function resolveLiveProfileName(c) {
38
+ if (c.profile == null)
39
+ return undefined;
40
+ try {
41
+ return loadProfileManifest(c.profile).manifest.name;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** A stored context cannot be rebuilt; the run fails rather than running under a context nobody armed. */
48
+ export class ArmedContextError extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = 'ArmedContextError';
52
+ }
53
+ }
54
+ // Invalid snapshots are corruption: dropping `--env` would run under an environment nobody armed, so fail instead.
55
+ function parseArmedEnv(envJson) {
56
+ if (envJson == null)
57
+ return {};
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(envJson);
61
+ }
62
+ catch (err) {
63
+ throw new ArmedContextError(`the armed env snapshot is not valid JSON (${err.message}) — this cron's stored env_json is corrupt, so the run would silently drop every armed variable. Re-arm the cron with a valid --env.`);
64
+ }
65
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
66
+ throw new ArmedContextError("the armed env snapshot is not a JSON object — this cron's stored env_json is corrupt. Re-arm the cron with a valid --env.");
67
+ }
68
+ const extraEnv = {};
69
+ for (const [k, v] of Object.entries(parsed)) {
70
+ if (typeof v !== 'string') {
71
+ throw new ArmedContextError(`the armed env snapshot's value for "${k}" is not a string — this cron's stored env_json is corrupt, so the run would silently drop that variable. Re-arm the cron with a valid --env.`);
72
+ }
73
+ extraEnv[k] = v;
74
+ }
75
+ return extraEnv;
76
+ }
77
+ /** Rebuild the armed context. Corrupt snapshots and unreadable profiles fail rather than silently changing its identity or environment. */
78
+ export function buildArmedRunContext(c, runId) {
79
+ const extraEnv = parseArmedEnv(c.env_json);
80
+ const profileId = resolveLiveProfile(c);
81
+ const operationalEnv = buildOperationalEnvBase({ targetCwd: c.cwd, targetProfileId: profileId });
82
+ const env = {
83
+ ...operationalEnv,
84
+ ...readProfileEnvVars(profileId),
85
+ ...extraEnv,
86
+ ...(profileId !== null ? { CRTR_PROFILE_ID: profileId } : {}),
87
+ CRTR_CRON_ID: c.cron_id,
88
+ CRTR_CRON_NAME: c.name,
89
+ CRTR_RUN_ID: runId,
90
+ };
91
+ // Preserve the daemon identity: otherwise child `crtr` resolves the default canvas.
92
+ const canvasHomeOverride = envHomeOverride();
93
+ if (canvasHomeOverride !== undefined && canvasHomeOverride !== '') {
94
+ env['CRTR_HOME'] = canvasHomeOverride;
95
+ }
96
+ if (c.anchor_node != null)
97
+ env['CRTR_ANCHOR'] = c.anchor_node;
98
+ env[ADMITTED_HOST_PATH_ENV] = operationalEnv[ADMITTED_HOST_PATH_ENV];
99
+ return { profileId, env };
100
+ }
@@ -0,0 +1,20 @@
1
+ /** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
2
+ export declare const CRON_OUTPUT_HEAD_BYTES: number;
3
+ /** Bound between the run-timeout SIGTERM and a forced SIGKILL escalation, so
4
+ * a signal-trapping command cannot stall teardown's detached drain. */
5
+ export declare const CRON_KILL_GRACE_MS = 2000;
6
+ /** Poll interval while waiting for a SIGTERMed stale-lease run to actually die
7
+ * (each probe is a `ps` call, so this is deliberately coarse). */
8
+ export declare const CRON_DEATH_POLL_MS = 250;
9
+ /** sha256 of empty stdout — the change-detection hash a run that produced no
10
+ * output carries (a failed spawn, a recovered stale lease). */
11
+ export declare const EMPTY_STDOUT_SHA256: string;
12
+ /** A byte-capped capture of one output stream's head. Bytes are buffered and
13
+ * decoded exactly once, at the end — decoding per chunk both mis-measured the
14
+ * cap (UTF-16 code units, not bytes) and corrupted any multi-byte sequence
15
+ * that straddled a chunk boundary. Truncation is MARKED, never silent: a
16
+ * clipped body reads as complete output to whoever receives the delivery. */
17
+ export declare function newHeadCapture(): {
18
+ take(chunk: Buffer): void;
19
+ decode(): string;
20
+ };
@@ -0,0 +1,45 @@
1
+ // src/daemon/cron/capture.ts — output capture for a cron run's child process,
2
+ // plus the timing/hash constants the capture and kill mechanics share.
3
+ import { createHash } from 'node:crypto';
4
+ /** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
5
+ export const CRON_OUTPUT_HEAD_BYTES = 4 * 1024;
6
+ /** Bound between the run-timeout SIGTERM and a forced SIGKILL escalation, so
7
+ * a signal-trapping command cannot stall teardown's detached drain. */
8
+ export const CRON_KILL_GRACE_MS = 2000;
9
+ /** Poll interval while waiting for a SIGTERMed stale-lease run to actually die
10
+ * (each probe is a `ps` call, so this is deliberately coarse). */
11
+ export const CRON_DEATH_POLL_MS = 250;
12
+ /** sha256 of empty stdout — the change-detection hash a run that produced no
13
+ * output carries (a failed spawn, a recovered stale lease). */
14
+ export const EMPTY_STDOUT_SHA256 = createHash('sha256').update('').digest('hex');
15
+ /** A byte-capped capture of one output stream's head. Bytes are buffered and
16
+ * decoded exactly once, at the end — decoding per chunk both mis-measured the
17
+ * cap (UTF-16 code units, not bytes) and corrupted any multi-byte sequence
18
+ * that straddled a chunk boundary. Truncation is MARKED, never silent: a
19
+ * clipped body reads as complete output to whoever receives the delivery. */
20
+ export function newHeadCapture() {
21
+ const chunks = [];
22
+ let bytes = 0;
23
+ let truncated = false;
24
+ return {
25
+ take(chunk) {
26
+ const room = CRON_OUTPUT_HEAD_BYTES - bytes;
27
+ if (room <= 0) {
28
+ truncated = true;
29
+ return;
30
+ }
31
+ if (chunk.length > room) {
32
+ chunks.push(chunk.subarray(0, room));
33
+ bytes = CRON_OUTPUT_HEAD_BYTES;
34
+ truncated = true;
35
+ return;
36
+ }
37
+ chunks.push(chunk);
38
+ bytes += chunk.length;
39
+ },
40
+ decode() {
41
+ const text = Buffer.concat(chunks).toString('utf8');
42
+ return truncated ? `${text}\n[… output truncated at ${CRON_OUTPUT_HEAD_BYTES} bytes]` : text;
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,19 @@
1
+ import { type Cron } from '../../core/canvas/crons.js';
2
+ export interface RunSettleContext {
3
+ runId: string;
4
+ startedAtIso: string;
5
+ exitCode: number | null;
6
+ timedOut: boolean;
7
+ runTimeoutS: number;
8
+ processErrorMessage: string | null;
9
+ stdoutHead: string;
10
+ /** Hash the full output: a capped head can hide a change. */
11
+ stdoutSha256: string;
12
+ stderrHead: string;
13
+ }
14
+ export interface Disposition {
15
+ delivered: string;
16
+ /** A paused row is retained so `cron resume` can re-arm it. */
17
+ paused: boolean;
18
+ }
19
+ export declare function disposeSettledRun(c: Cron, ctx: RunSettleContext, outOfBand: boolean, selfCanceled?: boolean): Promise<Disposition>;
@@ -0,0 +1,52 @@
1
+ import { getCron, setCronLastOutputHash } from '../../core/canvas/crons.js';
2
+ import { escalateFailure, failureSummary } from './escalation.js';
3
+ import { deliverToSink } from './sinks.js';
4
+ export async function disposeSettledRun(c, ctx, outOfBand, selfCanceled = false) {
5
+ const prefix = outOfBand ? 'out-of-band: ' : '';
6
+ // A self-cancel still delivers its already-produced output; an external cancellation does not.
7
+ const fresh = getCron(c.cron_id);
8
+ if (fresh === null && !selfCanceled)
9
+ return { delivered: `${prefix}cron canceled mid-run — nothing delivered`, paused: false };
10
+ const effective = fresh ?? c;
11
+ const failed = ctx.timedOut || ctx.processErrorMessage !== null || ctx.exitCode !== 0;
12
+ // `cron run` compares but never rewrites the scheduled baseline.
13
+ let changed = true;
14
+ if (effective.on_output === 'on-change') {
15
+ changed = effective.last_output_hash !== ctx.stdoutSha256;
16
+ if (!outOfBand && fresh !== null)
17
+ setCronLastOutputHash(c.cron_id, ctx.stdoutSha256);
18
+ }
19
+ if (failed) {
20
+ if (effective.on_output === 'silent')
21
+ return { delivered: `${prefix}silent`, paused: false };
22
+ if (outOfBand)
23
+ return { delivered: `out-of-band: ${failureSummary(ctx, null)} — not escalated`, paused: false };
24
+ return { delivered: await escalateFailure(effective, ctx, null), paused: true };
25
+ }
26
+ switch (effective.on_output) {
27
+ case 'silent':
28
+ return { delivered: `${prefix}silent`, paused: false };
29
+ case 'on-failure':
30
+ return { delivered: `${prefix}ok — nothing to deliver (on-failure)`, paused: false };
31
+ case 'on-change':
32
+ if (!changed)
33
+ return { delivered: `${prefix}unchanged — not delivered (on-change)`, paused: false };
34
+ return deliverOrEscalate(effective, ctx, outOfBand, prefix);
35
+ case 'always':
36
+ return deliverOrEscalate(effective, ctx, outOfBand, prefix);
37
+ }
38
+ }
39
+ async function deliverOrEscalate(c, ctx, outOfBand, prefix) {
40
+ if (ctx.stdoutHead.trim() === '') {
41
+ return { delivered: `${prefix}empty stdout — nothing delivered`, paused: false };
42
+ }
43
+ try {
44
+ return { delivered: `${prefix}${await deliverToSink(c, ctx.stdoutHead)}`, paused: false };
45
+ }
46
+ catch (err) {
47
+ const why = `delivery failed: ${err.message}`;
48
+ if (outOfBand)
49
+ return { delivered: `out-of-band: ${why} — not escalated`, paused: false };
50
+ return { delivered: await escalateFailure(c, ctx, why), paused: true };
51
+ }
52
+ }
@@ -0,0 +1,12 @@
1
+ import { type Cron } from '../../core/canvas/crons.js';
2
+ import type { RunSettleContext } from './disposition.js';
3
+ export declare function failureSummary(ctx: RunSettleContext, deliveryFailure: string | null): string;
4
+ /** Pause the cron and spawn the escalation node in the cron's originating
5
+ * profile (root fallback). Returns the run-log `delivered` string. Never
6
+ * throws: an escalation spawn failure is recorded and logged, the pause
7
+ * stands (the row must not keep refiring a broken command). */
8
+ export declare function escalateFailure(c: Cron, ctx: RunSettleContext, deliveryFailure: string | null): Promise<string>;
9
+ /** The delivery-failure line an escalated crashed ONE-SHOT carries: its side
10
+ * effects may already have landed in full, so a human/agent decides whether to
11
+ * redo it rather than the daemon re-firing it. */
12
+ export declare function crashedOneShotSummary(why: string): string;
@@ -0,0 +1,124 @@
1
+ // src/daemon/cron/escalation.ts — the cron failure path: pause the row and
2
+ // spawn a terminal node holding everything it needs to deal with the failure.
3
+ // Pausing IS the dedup — a paused row is never due, so nothing refires while
4
+ // the escalation node works.
5
+ import { getNode } from '../../core/canvas/canvas.js';
6
+ import { fullName } from '../../core/canvas/labels.js';
7
+ import { listCronRuns, setCronState } from '../../core/canvas/crons.js';
8
+ import { emitEvent } from '../../core/events/emit.js';
9
+ import { spawnChild } from '../../core/runtime/spawn.js';
10
+ import { cronWakeOrigin } from '../../core/runtime/bearings.js';
11
+ import { cadenceDisplay } from '../../core/wake.js';
12
+ import { resolveLiveProfileOrRoot } from './armed-context.js';
13
+ export function failureSummary(ctx, deliveryFailure) {
14
+ if (deliveryFailure !== null)
15
+ return deliveryFailure;
16
+ if (ctx.timedOut)
17
+ return `run timed out after ${ctx.runTimeoutS}s (killed; recorded exit -1)`;
18
+ if (ctx.processErrorMessage !== null)
19
+ return `run process error: ${ctx.processErrorMessage}`;
20
+ return `command exited ${ctx.exitCode ?? '(no exit observed)'}`;
21
+ }
22
+ /** The escalation node's kickoff: the cron's identity/config, this failure,
23
+ * the run-log ring, the creating-node reference, and a mandate that names its
24
+ * actual options (resume after verifying / cancel / ask the human). */
25
+ function buildEscalationKickoff(c, ctx, deliveryFailure) {
26
+ const creator = c.created_by != null ? getNode(c.created_by) : null;
27
+ const creatorLine = c.created_by == null
28
+ ? 'created by: (no creating node recorded)'
29
+ : creator !== null
30
+ ? `created by: node ${c.created_by} ("${fullName(creator)}") — still on the canvas; consider telling it what happened`
31
+ : `created by: node ${c.created_by} (now gone)`;
32
+ const schedule = c.recur != null ? `every ${cadenceDisplay(c.recur)}` : `one-shot at ${c.fire_at}`;
33
+ const priorRuns = listCronRuns(c.cron_id)
34
+ .slice(0, 10)
35
+ .map((r) => {
36
+ const exit = r.exit_code === null ? 'no exit' : `exit ${r.exit_code}`;
37
+ const head = (r.stderr_head ?? r.stdout_head ?? '').split('\n')[0]?.slice(0, 120) ?? '';
38
+ return `- ${r.started} — ${exit}, ${r.duration_ms}ms — ${r.delivered ?? ''}${head !== '' ? ` — ${head}` : ''}`;
39
+ });
40
+ return [
41
+ `A scheduled cron FAILED and has paused itself. You were spawned to deal with it.`,
42
+ '',
43
+ `- cron: "${c.name}" (${c.cron_id})`,
44
+ `- schedule: ${schedule}${c.tz != null ? ` (tz ${c.tz})` : ''}`,
45
+ `- context: cwd ${c.cwd}${c.profile != null ? `, profile ${c.profile}` : ''}`,
46
+ `- ${creatorLine}`,
47
+ `- this failure: ${failureSummary(ctx, deliveryFailure)}`,
48
+ '',
49
+ '## The command',
50
+ '',
51
+ '```bash',
52
+ c.command,
53
+ '```',
54
+ '',
55
+ '## This run',
56
+ '',
57
+ `- exit: ${ctx.timedOut ? '-1 (timeout)' : String(ctx.exitCode)}`,
58
+ `- stdout (head): ${ctx.stdoutHead === '' ? '(empty)' : ''}`,
59
+ ...(ctx.stdoutHead !== '' ? ['```', ctx.stdoutHead, '```'] : []),
60
+ `- stderr (head): ${ctx.stderrHead === '' ? '(empty)' : ''}`,
61
+ ...(ctx.stderrHead !== '' ? ['```', ctx.stderrHead, '```'] : []),
62
+ '',
63
+ '## Recent runs (most recent first, this failure not yet recorded)',
64
+ '',
65
+ ...(priorRuns.length > 0 ? priorRuns : ['(no prior runs recorded)']),
66
+ '',
67
+ '## Your mandate',
68
+ '',
69
+ `Diagnose why it failed and decide what to do:`,
70
+ `- Transient or fixable: fix the environment/command context, verify with \`crtr cron run ${c.cron_id}\` (fires now, out of band, shows you the result), then \`crtr cron resume ${c.cron_id}\` to re-arm it.`,
71
+ `- Obsolete: \`crtr cron cancel ${c.cron_id}\`.`,
72
+ `- The user's call (or the fix needs a change you cannot make): \`crtr human send\`.`,
73
+ '',
74
+ 'The cron stays paused until someone resumes it — nothing refires while you work.',
75
+ ].join('\n');
76
+ }
77
+ /** Pause the cron and spawn the escalation node in the cron's originating
78
+ * profile (root fallback). Returns the run-log `delivered` string. Never
79
+ * throws: an escalation spawn failure is recorded and logged, the pause
80
+ * stands (the row must not keep refiring a broken command). */
81
+ export async function escalateFailure(c, ctx, deliveryFailure) {
82
+ setCronState(c.cron_id, 'paused');
83
+ const creatorAlive = c.created_by != null && getNode(c.created_by) !== null;
84
+ try {
85
+ const res = await spawnChild({
86
+ kind: 'general',
87
+ cwd: c.cwd,
88
+ prompt: buildEscalationKickoff(c, ctx, deliveryFailure),
89
+ // The spec's word is TERMINAL: a node that deals with this failure and
90
+ // reaps. With the creator gone there is nobody to parent it, but
91
+ // parentless must not mean resident-and-forefront — see deliverSpawnSink.
92
+ ...(creatorAlive ? { parent: c.created_by } : { root: true, rootLifecycle: 'terminal' }),
93
+ readiness: 'launch',
94
+ profile: resolveLiveProfileOrRoot(c),
95
+ wakeOrigin: cronWakeOrigin(c, 'node_birth'),
96
+ });
97
+ emitEvent({
98
+ level: 'info',
99
+ event: 'cron.failure.escalated',
100
+ fields: { cron_id: c.cron_id, name: c.name, escalation_node: res.node.node_id },
101
+ });
102
+ return `failure: cron paused, escalation node ${res.node.node_id} spawned`;
103
+ }
104
+ catch (err) {
105
+ emitEvent({
106
+ level: 'error',
107
+ event: 'cron.escalation.spawn_failed',
108
+ error: err,
109
+ fields: { cron_id: c.cron_id, name: c.name },
110
+ });
111
+ return `failure: cron paused; escalation spawn failed: ${err.message}`;
112
+ }
113
+ }
114
+ /** The delivery-failure line an escalated crashed ONE-SHOT carries: its side
115
+ * effects may already have landed in full, so a human/agent decides whether to
116
+ * redo it rather than the daemon re-firing it. */
117
+ export function crashedOneShotSummary(why) {
118
+ return [
119
+ `${why}.`,
120
+ 'This is a ONE-SHOT, so it has been PAUSED rather than released: its side effects may have completed in full,',
121
+ 'and re-firing would enact them a second time. Determine whether the work actually landed, then either',
122
+ '`crtr cron run` it to redo it deliberately, or `crtr cron cancel` it.',
123
+ ].join(' ');
124
+ }
@@ -0,0 +1,4 @@
1
+ import { type Cron, type HeldSettle } from '../../core/canvas/crons.js';
2
+ export declare const EXIT_HELD = 75;
3
+ export declare const HELD_DELIVERED = "deferred (exit 75) \u2014 held for poke";
4
+ export declare function decideHeldSettle(c: Cron, startedAtIso: string, finishedAtMs: number): HeldSettle | undefined;
@@ -0,0 +1,20 @@
1
+ import { getCron } from '../../core/canvas/crons.js';
2
+ // Exit 75 parks an owed occurrence; the daemon honors the gate but does not implement it.
3
+ export const EXIT_HELD = 75;
4
+ export const HELD_DELIVERED = 'deferred (exit 75) — held for poke';
5
+ // A held one-shot without expiry is poke-only; leaving it past due would re-fire every tick.
6
+ const HELD_ONE_SHOT_FAR_FUTURE = '9999-12-31T23:59:59.999Z';
7
+ // Re-read `last_poke_at`: a poke can land while the gate check runs.
8
+ export function decideHeldSettle(c, startedAtIso, finishedAtMs) {
9
+ const fresh = getCron(c.cron_id);
10
+ if (fresh === null)
11
+ return undefined;
12
+ if (fresh.state === 'active' && fresh.last_poke_at !== null && startedAtIso < fresh.last_poke_at) {
13
+ // A mid-run poke cannot unpark a lease, so re-due an active row; paused rows must remain parked.
14
+ return { kind: 'redue', nowIso: new Date(finishedAtMs).toISOString() };
15
+ }
16
+ if (c.recur != null) {
17
+ return { kind: 'park' };
18
+ }
19
+ return { kind: 'park', fireAt: fresh.expires_at ?? HELD_ONE_SHOT_FAR_FUTURE };
20
+ }
@@ -0,0 +1,3 @@
1
+ import type { RunDueCronOptions } from './passes.js';
2
+ /** Recover leases from prior daemon instances. Verify launch identity, not pid liveness: a reboot can recycle the pid, and killing its new owner would signal a stranger. */
3
+ export declare function recoverStaleCronLeases(now: number, options?: RunDueCronOptions): void;