@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -30,26 +30,24 @@
30
30
  // promises for graceful-teardown drain but never blocks on a slow run.
31
31
  import { spawn } from 'node:child_process';
32
32
  import { createHash, randomBytes, randomUUID } from 'node:crypto';
33
- import { join } from 'node:path';
34
33
  import { setTimeout as delay } from 'node:timers/promises';
35
34
  import { rmSync } from 'node:fs';
36
35
  import { synthesizedPageDocument, writeSynthesizedPage } from '../core/human/page-synth.js';
37
36
  import { ticketDir } from '../core/human/root.js';
38
37
  import { preparePage, publishPage } from '../core/human/tickets.js';
39
- import { readConfig } from '../core/config.js';
38
+ import { resolvePageComponents } from '../core/config.js';
40
39
  import { dueClockCrons, dueOverlapCrons, expiredIdleCrons, runningCrons, acquireCronRunLease, releaseCronRunLease, recordCronRun, listCronRuns, advanceCron, consumeCron, cancelCron, killLeasedCronRun, verifyLeasedCronRun, getCron, setCronState, setCronLastOutputHash, } from '../core/canvas/crons.js';
41
40
  import { getNode } from '../core/canvas/canvas.js';
42
41
  import { fullName } from '../core/canvas/labels.js';
43
42
  import { killProcessGroup } from '../core/canvas/pid.js';
44
43
  import { appendInbox } from '../core/feed/inbox.js';
45
- import { atomicWriteJson } from '../core/fs-utils.js';
46
44
  import { emitEvent } from '../core/events/emit.js';
47
45
  import { loadProfileManifest } from '../core/profiles/manifest.js';
48
46
  import { hasNoNaturalCycle } from '../core/runtime/revive-all.js';
49
47
  import { spawnChild } from '../core/runtime/spawn.js';
50
48
  import { cronWakeOrigin } from '../core/runtime/bearings.js';
51
49
  import { nextSlotAfter, cadenceDisplay } from '../core/wake.js';
52
- import { buildOperationalEnvBase } from '../core/runtime/spawn-env.js';
50
+ import { ADMITTED_HOST_PATH_ENV, buildOperationalEnvBase, resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
53
51
  import { parseSink } from './cron-sink.js';
54
52
  import { readProfileEnvVars } from '../core/profiles/env-store.js';
55
53
  /** Output head caps for the run log (spec: "capped, e.g. 4KB"). */
@@ -163,6 +161,16 @@ function resolveLiveProfile(c) {
163
161
  return null;
164
162
  }
165
163
  }
164
+ function resolveLiveProfileName(c) {
165
+ if (c.profile == null)
166
+ return undefined;
167
+ try {
168
+ return loadProfileManifest(c.profile).manifest.name;
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ }
166
174
  // ---------------------------------------------------------------------------
167
175
  // Delivery sinks
168
176
  // ---------------------------------------------------------------------------
@@ -213,9 +221,9 @@ async function deliverSpawnSink(c, kind, stdout) {
213
221
  });
214
222
  return res.node.node_id;
215
223
  }
216
- /** Deliver stdout through crouter as a durable notify page (fire-and-forget
217
- * inbox state — jobs whose output is a person's call). Mirrors `crtr human
218
- * notify` minus the CLI-env source stamp: the page names the cron instead. */
224
+ /** Deliver stdout through crouter as a standalone human page. Mirrors
225
+ * `crtr human send --inbox` for a display-only page, minus the CLI-env source
226
+ * stamp: the page names the cron instead. */
219
227
  function deliverHumanSink(c, stdout) {
220
228
  const synthesized = writeSynthesizedPage(synthesizedPageDocument(`⏰ cron ${c.name}`, `Scheduled cron "${c.name}" delivered output for your attention.`, stdout, false));
221
229
  // Cron stdout rides into the page verbatim, so it can carry invalid page
@@ -223,19 +231,21 @@ function deliverHumanSink(c, stdout) {
223
231
  // half-built `nfy-*` directory behind.
224
232
  let prepared;
225
233
  try {
234
+ const profileName = resolveLiveProfileName(c);
226
235
  prepared = preparePage({
227
236
  sourceFile: synthesized.path,
228
- dialect: 'md',
229
- source: { sessionName: `cron ${c.name} (${c.cron_id})` },
230
- productKinds: readConfig('user').page_components,
237
+ source: {
238
+ sessionName: `cron ${c.name} (${c.cron_id})`,
239
+ ...(profileName === undefined ? {} : { profileName }),
240
+ },
241
+ productKinds: resolvePageComponents(),
242
+ delivery: { placement: 'inline', inbox: true, reply: false },
231
243
  });
232
244
  }
233
245
  finally {
234
246
  rmSync(synthesized.dir, { recursive: true, force: true });
235
247
  }
236
- const id = `nfy-${randomBytes(4).toString('hex')}`;
237
- const rc = { mode: 'notify' };
238
- atomicWriteJson(join(ticketDir(id), 'run.json'), rc);
248
+ const id = `page-${randomBytes(4).toString('hex')}`;
239
249
  publishPage(ticketDir(id), prepared);
240
250
  }
241
251
  /** Route stdout to the cron's armed sink. Returns the delivered string on
@@ -253,7 +263,7 @@ async function deliverToSink(c, stdout) {
253
263
  return `delivered: spawned ${nodeId}`;
254
264
  }
255
265
  deliverHumanSink(c, stdout);
256
- return 'delivered: human notify';
266
+ return 'delivered: human send --inbox';
257
267
  }
258
268
  function failureSummary(ctx, deliveryFailure) {
259
269
  if (deliveryFailure !== null)
@@ -314,7 +324,7 @@ function buildEscalationKickoff(c, ctx, deliveryFailure) {
314
324
  `Diagnose why it failed and decide what to do:`,
315
325
  `- 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.`,
316
326
  `- Obsolete: \`crtr cron cancel ${c.cron_id}\`.`,
317
- `- The user's call (or the fix needs a change you cannot make): \`crtr human ask\`.`,
327
+ `- The user's call (or the fix needs a change you cannot make): \`crtr human send\`.`,
318
328
  '',
319
329
  'The cron stays paused until someone resumes it — nothing refires while you work.',
320
330
  ].join('\n');
@@ -625,8 +635,9 @@ export function executeCron(c, opts) {
625
635
  // Layering: profile store < the arm-time `--env` snapshot (an explicit
626
636
  // choice) < crtr's own constructed CRTR_* names, which always win.
627
637
  const profileId = resolveLiveProfile(c);
638
+ const operationalEnv = buildOperationalEnvBase({ targetCwd: c.cwd, targetProfileId: profileId });
628
639
  const env = {
629
- ...buildOperationalEnvBase({ targetCwd: c.cwd, targetProfileId: profileId }),
640
+ ...operationalEnv,
630
641
  ...readProfileEnvVars(profileId),
631
642
  ...extraEnv,
632
643
  ...(profileId !== null ? { CRTR_PROFILE_ID: profileId } : {}),
@@ -646,11 +657,12 @@ export function executeCron(c, opts) {
646
657
  }
647
658
  if (c.anchor_node != null)
648
659
  env['CRTR_ANCHOR'] = c.anchor_node;
660
+ env[ADMITTED_HOST_PATH_ENV] = operationalEnv[ADMITTED_HOST_PATH_ENV];
649
661
  const startedAtMs = Date.now();
650
662
  const startedAtIso = new Date(startedAtMs).toISOString();
651
663
  let child;
652
664
  try {
653
- child = spawn('bash', ['-c', c.command], {
665
+ child = spawn(resolveAdmittedHostCommand('bash', env, c.cwd), ['-c', c.command], {
654
666
  cwd: c.cwd,
655
667
  env,
656
668
  detached: true, // own process group — killProcessGroup(-pid) reaps the whole tree
@@ -8,7 +8,7 @@
8
8
  // The fleet registry's entry IS the node's liveness — no pid probe, no grace
9
9
  // clock. A broker's real `exit` event enqueues an exit-policy job on the
10
10
  // daemon's serial work lane (never inline on the Node event callback), where
11
- // classifyDeadNode — one pure 9-row policy table over durable row state —
11
+ // classifyDeadNode — one pure 8-row policy table over durable row state —
12
12
  // decides forget / dormant / respawn / boot-failure. The startup recovery
13
13
  // sweep classifies boot-time dead rows through the SAME table, so exit
14
14
  // recovery and startup recovery can never diverge. Every broker's argv is
@@ -10,12 +10,16 @@ import type { HostHandle } from '../core/runtime/host.js';
10
10
  * seeing a silent `dead`. */
11
11
  export declare function surfaceBootFailure(meta: NodeMeta): Promise<void>;
12
12
  /** Whether a dead relaunch should resume the saved session strictly, or retry
13
- * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
14
- * before session_start must retry AS a cycle — a strict reopen only restores
15
- * the transcript at an idle prompt, it cannot restart the dead engine turn.
16
- * A dirty interrupted BUSY turn also requests a cycle; a broker-certified
17
- * clean abort is handled explicitly by classifyDeadNode before this fallback. */
18
- export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null, interruptedTurn?: boolean): boolean;
13
+ * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
14
+ * session_start must retry AS a cycle: the fresh branch it was opening is not
15
+ * confirmed, so resuming would silently continue the old leaf the refresh was
16
+ * meant to leave behind.
17
+ *
18
+ * An interrupted turn is NOT a reason to cycle. A strict reopen does restore
19
+ * the transcript at an idle prompt without restarting the dead turn — but the
20
+ * fix for that is the continuation prompt recovery delivers (see `#respawn`),
21
+ * not throwing the conversation away. */
22
+ export declare function retryResumeMode(meta: Pick<NodeMeta, 'cycle_pending'> | null): boolean;
19
23
  export type DeadNodeAction = 'forget' | 'respawn-fresh' | 'dormant' | 'respawn-cycle' | 'respawn-resume' | 'boot-failure';
20
24
  /** The narrow durable state the policy table reads. Structural (a Pick, not
21
25
  * NodeMeta itself) so tests fabricate table rows directly. */
@@ -23,10 +27,18 @@ export type DeadNodeRow = Pick<NodeMeta, 'status' | 'intent' | 'pi_session_id' |
23
27
  /** One pure function producing the action for a node known-dead. Used at
24
28
  * exactly two call sites — an exit-policy job and the startup recovery sweep
25
29
  * (design D-12) — so exit recovery and startup recovery can never diverge.
26
- * Evaluated in table order; `busy` is the on-disk busy marker. A clean-abort
27
- * certificate is written by the broker only after Pi persisted its aborted
28
- * entry; without it, a set busy marker means a dirty mid-turn crash. */
29
- export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean, cleanAbort?: boolean): DeadNodeAction;
30
+ * Evaluated in table order; `busy` is the on-disk busy marker.
31
+ *
32
+ * `busy` no longer selects the ACTION. Whether the engine died mid-turn or
33
+ * parked at an idle prompt, the saved transcript is equally resumable — pi
34
+ * persists whole entries (`appendFileSync` per line) so a killed generation
35
+ * simply leaves the file ending at its last complete entry, and the loader
36
+ * skips a malformed line rather than rejecting the session. A mid-turn death
37
+ * needs the turn re-driven, which is a continuation prompt on top of the
38
+ * resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
39
+ * only to route a node that died before session_start ever recorded a session
40
+ * to a fresh relaunch instead of terminalizing it as a boot failure. */
41
+ export declare function classifyDeadNode(row: DeadNodeRow | null, busy: boolean): DeadNodeAction;
30
42
  /** An exit with uptime at or above this resets the consecutive-failure
31
43
  * counter before classification; a shorter-lived exit increments it. */
32
44
  export declare const HEALTHY_UPTIME_MS = 60000;
@@ -51,21 +51,33 @@ async function surfaceRecoveryFailure(meta, error) {
51
51
  await pushUrgent(meta.node_id, body, { from: meta.node_id });
52
52
  }
53
53
  /** Whether a dead relaunch should resume the saved session strictly, or retry
54
- * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies
55
- * before session_start must retry AS a cycle — a strict reopen only restores
56
- * the transcript at an idle prompt, it cannot restart the dead engine turn.
57
- * A dirty interrupted BUSY turn also requests a cycle; a broker-certified
58
- * clean abort is handled explicitly by classifyDeadNode before this fallback. */
59
- export function retryResumeMode(meta, interruptedTurn = false) {
60
- return meta?.cycle_pending !== true && !interruptedTurn;
54
+ * as a fresh cycle instead. A cycling attempt (cycle_pending) that dies before
55
+ * session_start must retry AS a cycle: the fresh branch it was opening is not
56
+ * confirmed, so resuming would silently continue the old leaf the refresh was
57
+ * meant to leave behind.
58
+ *
59
+ * An interrupted turn is NOT a reason to cycle. A strict reopen does restore
60
+ * the transcript at an idle prompt without restarting the dead turn — but the
61
+ * fix for that is the continuation prompt recovery delivers (see `#respawn`),
62
+ * not throwing the conversation away. */
63
+ export function retryResumeMode(meta) {
64
+ return meta?.cycle_pending !== true;
61
65
  }
62
66
  /** One pure function producing the action for a node known-dead. Used at
63
67
  * exactly two call sites — an exit-policy job and the startup recovery sweep
64
68
  * (design D-12) — so exit recovery and startup recovery can never diverge.
65
- * Evaluated in table order; `busy` is the on-disk busy marker. A clean-abort
66
- * certificate is written by the broker only after Pi persisted its aborted
67
- * entry; without it, a set busy marker means a dirty mid-turn crash. */
68
- export function classifyDeadNode(row, busy, cleanAbort = false) {
69
+ * Evaluated in table order; `busy` is the on-disk busy marker.
70
+ *
71
+ * `busy` no longer selects the ACTION. Whether the engine died mid-turn or
72
+ * parked at an idle prompt, the saved transcript is equally resumable — pi
73
+ * persists whole entries (`appendFileSync` per line) so a killed generation
74
+ * simply leaves the file ending at its last complete entry, and the loader
75
+ * skips a malformed line rather than rejecting the session. A mid-turn death
76
+ * needs the turn re-driven, which is a continuation prompt on top of the
77
+ * resume (see `#respawn`), not a fresh branch. `busy` therefore survives here
78
+ * only to route a node that died before session_start ever recorded a session
79
+ * to a fresh relaunch instead of terminalizing it as a boot failure. */
80
+ export function classifyDeadNode(row, busy) {
69
81
  if (row === null)
70
82
  return 'forget'; // row 1: row deleted
71
83
  if (row.status === 'done' || row.status === 'canceled' || row.status === 'dead')
@@ -74,15 +86,13 @@ export function classifyDeadNode(row, busy, cleanAbort = false) {
74
86
  return 'respawn-fresh'; // row 3
75
87
  if (row.intent === 'idle-release')
76
88
  return 'dormant'; // row 4
77
- if (cleanAbort && row.pi_session_id != null)
78
- return 'respawn-resume'; // row 5: broker-certified interrupted turn
79
- if (busy)
80
- return 'respawn-cycle'; // row 6: dirty interrupted turn
81
89
  if (row.pi_session_id != null)
82
- return retryResumeMode(row, false) ? 'respawn-resume' : 'respawn-cycle'; // row 7
83
- if (row.fork_from != null && row.pi_session_id == null)
84
- return 'respawn-fresh'; // row 8: bounded ordinary fork-birth retry
85
- return 'boot-failure'; // row 9
90
+ return retryResumeMode(row) ? 'respawn-resume' : 'respawn-cycle'; // row 5: any saved session
91
+ if (busy)
92
+ return 'respawn-fresh'; // row 6: died mid-turn before a session was ever recorded
93
+ if (row.fork_from != null)
94
+ return 'respawn-fresh'; // row 7: bounded ordinary fork-birth retry
95
+ return 'boot-failure'; // row 8
86
96
  }
87
97
  // ---------------------------------------------------------------------------
88
98
  // Respawn throttle (replaces the stranded-relaunch counters, event-driven)
@@ -248,8 +258,12 @@ export class DaemonFleet {
248
258
  }
249
259
  async #applyPolicy(nodeId, origin) {
250
260
  const meta = getNode(nodeId);
251
- const cleanAbort = hasCleanAbort(nodeId);
252
- const action = classifyDeadNode(meta, isBusy(nodeId), cleanAbort);
261
+ // Two durable witnesses mean the dead broker had a turn in flight. A dirty
262
+ // death leaves the busy marker behind; a bounded graceful abort clears busy
263
+ // as the run settles, then writes the clean-abort certificate. Both need the
264
+ // same recovery: resume the saved transcript and re-drive the stopped turn.
265
+ const interruptedTurn = isBusy(nodeId) || hasCleanAbort(nodeId);
266
+ const action = classifyDeadNode(meta, interruptedTurn);
253
267
  switch (action) {
254
268
  case 'forget':
255
269
  // Fanouts for terminal exits already happened at the transition site.
@@ -271,12 +285,12 @@ export class DaemonFleet {
271
285
  case 'respawn-fresh':
272
286
  case 'respawn-cycle':
273
287
  case 'respawn-resume':
274
- await this.#respawn(nodeId, meta, action, origin, cleanAbort);
288
+ await this.#respawn(nodeId, meta, action, origin, interruptedTurn);
275
289
  break;
276
290
  }
277
291
  return action;
278
292
  }
279
- async #respawn(nodeId, meta, action, origin, cleanAbort) {
293
+ async #respawn(nodeId, meta, action, origin, interruptedTurn) {
280
294
  const state = this.#throttle.get(nodeId) ?? { failures: 0 };
281
295
  if (state.failures >= MAX_RESPAWN_ATTEMPTS) {
282
296
  await this.#terminalize(nodeId, meta, {
@@ -327,7 +341,12 @@ export class DaemonFleet {
327
341
  try {
328
342
  reviveNode(nodeId, {
329
343
  resume: action === 'respawn-resume',
330
- wakeReason: cleanAbort ? 'runtime-restart-abort' : undefined,
344
+ // A resumed session whose turn was interrupted comes back parked at an
345
+ // idle prompt with the dead turn unfinished. The continuation re-drives
346
+ // it in place, so the conversation continues instead of being rebuilt
347
+ // from disk on a fresh branch. Only a true resume can carry it; a cycle
348
+ // or fresh relaunch gets its own kickoff.
349
+ wakeReason: action === 'respawn-resume' && interruptedTurn ? 'runtime-restart-abort' : undefined,
331
350
  // Replacing a dead broker instance is recovery, not a wake: nothing
332
351
  // external arrived for this node, so an armed deadline survives the
333
352
  // relaunch instead of being consumed by it.
@@ -1,13 +1,19 @@
1
1
  import type { TicketResult } from '../../core/human/types.js';
2
+ /** Every ticket-ending action retires the ticket's feedback companion — done
3
+ * when active or idle, never left resident. Idempotent and replay-safe; a
4
+ * corrupt feedback file must not block result publication. Exported for the
5
+ * daemon-start sweep, which must settle terminal tickets whether or not a
6
+ * reply bridge exists. */
7
+ export declare function settleFeedbackCompanion(ticketId: string): void;
2
8
  /**
3
9
  * The one completion path for a page ticket: claim takeover, exclusive result
4
- * publication, then the asking node's wake. Every addressing surface — the
5
- * node-addressed resolve route and the ticket-id-addressed inbox respond route —
6
- * finishes a page here, so an answer always reaches the node that asked.
10
+ * publication, then optional reply delivery. Every addressing surface routes
11
+ * through this function, so the reply-route descriptor is the sole authority
12
+ * on whether completion reaches a bridge.
7
13
  */
8
- export declare function resolvePageTicket(nodeId: string, responses: unknown, productKinds?: string[]): Promise<TicketResult>;
14
+ export declare function resolvePageTicket(ticketId: string, responses: unknown): Promise<TicketResult>;
9
15
  /** Cancel a ticket with the store's first-writer-wins publication. */
10
- export declare function cancelHumanTicket(nodeId: string, opts: {
16
+ export declare function cancelHumanTicket(ticketId: string, opts: {
11
17
  reason?: string;
12
18
  actor?: string;
13
19
  }): Promise<TicketResult>;
@@ -16,4 +22,4 @@ export declare function cancelHumanTicket(nodeId: string, opts: {
16
22
  * receipt: every action below is replay-safe, so lost delivery can be retried
17
23
  * by a later resolve/cancel attempt or the daemon-start sweep.
18
24
  */
19
- export declare function deliverTerminalResult(nodeId: string): Promise<boolean>;
25
+ export declare function deliverTerminalResult(ticketId: string): Promise<boolean>;
@@ -1,36 +1,94 @@
1
- import { join } from 'node:path';
2
1
  import { ApiError } from '../../api/index.js';
3
- import { getNode, subscribersOf } from '../../core/canvas/canvas.js';
2
+ import { getRow, subscribersOf } from '../../core/canvas/canvas.js';
4
3
  import { ticketDir } from '../../core/human/root.js';
4
+ import { parsePage } from '../../core/human/page.js';
5
5
  import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
6
- import { responsePath } from '../../core/human/convention.js';
7
- import { readJsonOrNull } from '../../core/fs-utils.js';
6
+ import { describePageAnswer } from '../../core/human/answer.js';
7
+ import { renderAnswerText } from '../../core/human/answer-text.js';
8
+ import { pagePath, readTicketReplyRoute, responsePath } from '../../core/human/convention.js';
9
+ import { readPageFeedback } from '../../core/human/feedback.js';
10
+ import { pageDocumentSha256 } from '../../core/human/feedback-companion.js';
8
11
  import { appendInbox } from '../../core/feed/inbox.js';
9
12
  import { FinalDeliveryError, pushFinal } from '../../core/feed/feed.js';
10
13
  import { transition } from '../../core/runtime/lifecycle.js';
11
- /**
12
- * The answer a page's asking node wakes with. Slot responses are small and
13
- * typed, so they are inlined as compact JSON rather than left behind the path:
14
- * the node reads its answers out of its own report. An acknowledgement carries
15
- * no responses at all and reads as one line saying the human saw the page.
16
- * Interim rendering — the designed answer view is phase 5.
17
- */
18
- function renderPageAnswer(result, resultPath) {
19
- const entries = Object.entries(result.responses);
20
- const lines = [];
21
- if (entries.length === 0) {
22
- lines.push('The human read the page and dismissed it. No slot responses came back, so the acknowledgement is the whole answer.');
23
- }
24
- else {
25
- lines.push(`The human answered ${entries.length} slot${entries.length === 1 ? '' : 's'}:`, '');
26
- for (const [slotId, response] of entries)
27
- lines.push(`${slotId}: ${JSON.stringify(response)}`);
28
- const summary = result.summary.trim();
29
- if (summary !== '')
30
- lines.push('', summary);
31
- }
32
- lines.push('', `Response metadata (slot ids, typed responses, timestamp): ${resultPath}`);
33
- return lines.join('\n');
14
+ import { emitEvent } from '../../core/events/emit.js';
15
+ import { retireCompanion } from '../companion-retire.js';
16
+ /** Every ticket-ending action retires the ticket's feedback companion — done
17
+ * when active or idle, never left resident. Idempotent and replay-safe; a
18
+ * corrupt feedback file must not block result publication. Exported for the
19
+ * daemon-start sweep, which must settle terminal tickets whether or not a
20
+ * reply bridge exists. */
21
+ export function settleFeedbackCompanion(ticketId) {
22
+ try {
23
+ const companionNodeId = readPageFeedback(ticketDir(ticketId)).companionNodeId;
24
+ if (companionNodeId !== undefined)
25
+ retireCompanion(companionNodeId, 'approved');
26
+ }
27
+ catch (error) {
28
+ emitEvent({ level: 'warn', event: 'human.feedback_companion.retire_failed', error });
29
+ }
30
+ }
31
+ /** The settle-time feedback addendum to a page completion — answer and
32
+ * cancellation alike: a change notice when the final page differs from the
33
+ * version first commented on, and each still-open item's quote and note.
34
+ * Silent when the page is unchanged and no feedback remains open; never a
35
+ * manufactured prose diff. */
36
+ function feedbackAddendum(dir) {
37
+ let sections;
38
+ try {
39
+ const feedback = readPageFeedback(dir);
40
+ if (feedback.companionNodeId === undefined)
41
+ return '';
42
+ sections = [];
43
+ if (feedback.firstCommentedSha256 !== undefined && pageDocumentSha256(dir) !== feedback.firstCommentedSha256) {
44
+ sections.push(`The page changed during feedback: the person commented on it and its companion conversation (node ${feedback.companionNodeId}) revised it in place. ` +
45
+ `Reread the final page at ${pagePath(dir, 'jsx')} before interpreting this outcome.`);
46
+ }
47
+ const open = feedback.comments.filter((comment) => comment.status === 'open');
48
+ if (open.length > 0) {
49
+ const items = open.map((comment) => [
50
+ ...comment.quote.split('\n').map((line) => `> ${line}`),
51
+ comment.note,
52
+ ].join('\n'));
53
+ sections.push([
54
+ `Feedback left unresolved at settlement — the companion conversation (node ${feedback.companionNodeId}) had not dealt with these items:`,
55
+ ...items,
56
+ ].join('\n\n'));
57
+ }
58
+ }
59
+ catch {
60
+ // The answer itself is the deliverable; a broken feedback record adds nothing.
61
+ return '';
62
+ }
63
+ return sections.length === 0 ? '' : `\n\n${sections.join('\n\n')}`;
64
+ }
65
+ /** A corrupt manifest must not strand the asking node: the raw response remains its escape hatch. */
66
+ function renderCorruptPageAnswer(result, resultPath) {
67
+ return [
68
+ 'The page manifest could not be read, so the raw response is provided.',
69
+ '',
70
+ JSON.stringify(result.responses),
71
+ '',
72
+ `Response record: ${resultPath}`,
73
+ ].join('\n');
74
+ }
75
+ function pageTitle(dir) {
76
+ try {
77
+ return parsePage(dir).title;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ function renderPageAnswer(dir, result, resultPath) {
84
+ let manifest;
85
+ try {
86
+ manifest = parsePage(dir);
87
+ }
88
+ catch {
89
+ return renderCorruptPageAnswer(result, resultPath);
90
+ }
91
+ return renderAnswerText(describePageAnswer(manifest, result.responses, { completedAt: result.completedAt }), resultPath) + feedbackAddendum(dir);
34
92
  }
35
93
  function deliveryFailed(error) {
36
94
  // A canonical final is committed inside its own write boundary and can never
@@ -42,9 +100,9 @@ function deliveryFailed(error) {
42
100
  const detail = error instanceof Error ? error.message : String(error);
43
101
  return new ApiError(500, 'delivery_failed', `The answer is recorded and delivery will be retried: ${detail}`);
44
102
  }
45
- async function deliverOrConflict(nodeId) {
103
+ async function deliverOrConflict(ticketId) {
46
104
  try {
47
- await deliverTerminalResult(nodeId);
105
+ await deliverTerminalResult(ticketId);
48
106
  }
49
107
  catch (error) {
50
108
  throw deliveryFailed(error);
@@ -53,16 +111,16 @@ async function deliverOrConflict(nodeId) {
53
111
  }
54
112
  /**
55
113
  * The one completion path for a page ticket: claim takeover, exclusive result
56
- * publication, then the asking node's wake. Every addressing surface — the
57
- * node-addressed resolve route and the ticket-id-addressed inbox respond route —
58
- * finishes a page here, so an answer always reaches the node that asked.
114
+ * publication, then optional reply delivery. Every addressing surface routes
115
+ * through this function, so the reply-route descriptor is the sole authority
116
+ * on whether completion reaches a bridge.
59
117
  */
60
- export async function resolvePageTicket(nodeId, responses, productKinds = []) {
61
- const completed = takeoverAndCompletePage(ticketDir(nodeId), responses, { host: 'crtrd', pid: process.pid }, { productKinds });
118
+ export async function resolvePageTicket(ticketId, responses) {
119
+ const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid });
62
120
  if (!completed.won)
63
- return deliverOrConflict(nodeId);
121
+ return deliverOrConflict(ticketId);
64
122
  try {
65
- await deliverTerminalResult(nodeId);
123
+ await deliverTerminalResult(ticketId);
66
124
  }
67
125
  catch (error) {
68
126
  throw deliveryFailed(error);
@@ -70,12 +128,12 @@ export async function resolvePageTicket(nodeId, responses, productKinds = []) {
70
128
  return completed.result;
71
129
  }
72
130
  /** Cancel a ticket with the store's first-writer-wins publication. */
73
- export async function cancelHumanTicket(nodeId, opts) {
74
- const canceled = cancelTicket(ticketDir(nodeId), opts);
131
+ export async function cancelHumanTicket(ticketId, opts) {
132
+ const canceled = cancelTicket(ticketDir(ticketId), opts);
75
133
  if (canceled.status === 'already_resolved')
76
- return deliverOrConflict(nodeId);
134
+ return deliverOrConflict(ticketId);
77
135
  try {
78
- await deliverTerminalResult(nodeId);
136
+ await deliverTerminalResult(ticketId);
79
137
  }
80
138
  catch (error) {
81
139
  throw deliveryFailed(error);
@@ -87,37 +145,59 @@ export async function cancelHumanTicket(nodeId, opts) {
87
145
  * receipt: every action below is replay-safe, so lost delivery can be retried
88
146
  * by a later resolve/cancel attempt or the daemon-start sweep.
89
147
  */
90
- export async function deliverTerminalResult(nodeId) {
91
- const dir = ticketDir(nodeId);
92
- const run = readJsonOrNull(join(dir, 'run.json'));
93
- if (run?.mode === 'notify')
94
- return false;
148
+ export async function deliverTerminalResult(ticketId) {
149
+ const dir = ticketDir(ticketId);
95
150
  const result = readTicketResult(dir);
96
151
  if (result === null)
97
152
  return false;
153
+ // Companion retirement rides the committed result, BEFORE the reply-route
154
+ // gate: every ticket-ending path (resolve, cancel, conflict replay) funnels
155
+ // through here, and an inbox-only page with no reply bridge still settles
156
+ // its companion. The daemon-start sweep settles the tickets that die before
157
+ // reaching this call.
158
+ settleFeedbackCompanion(ticketId);
159
+ const replyRoute = readTicketReplyRoute(dir);
160
+ if (replyRoute === null)
161
+ return false;
162
+ const bridgeNodeId = replyRoute.bridge_node_id;
98
163
  projectReviewOutput(dir);
99
- const node = getNode(nodeId);
164
+ // The ROW, not node metadata: a bridge whose row has been pruned may retain
165
+ // its ticket files, but no lifecycle target remains. No row means nothing is
166
+ // left to deliver to.
167
+ const node = getRow(bridgeNodeId);
100
168
  if (node === null || node.status === 'dead' || node.status === 'canceled' || (node.status === 'done' && node.final_report !== null)) {
101
169
  return true;
102
170
  }
103
171
  if (result.kind === 'canceled') {
104
- const note = result.reason !== undefined && result.reason !== '' ? ` — ${result.reason}` : '';
172
+ const title = pageTitle(dir);
173
+ const reason = result.reason !== undefined && result.reason !== '' ? result.reason : 'no reason was given';
174
+ // A human dismissal is a rejection, not a routing event: say so plainly so
175
+ // the agent acts on the "no" instead of re-raising the same ask.
176
+ const dismissed = result.actor === 'human';
177
+ const subject = dismissed
178
+ ? (title === undefined ? `human interaction ${ticketId} dismissed without answering` : `human interaction ${ticketId} — “${title}” dismissed without answering`)
179
+ : (title === undefined ? `human interaction ${ticketId} canceled — no answer is coming` : `human interaction ${ticketId} — “${title}” canceled, no answer is coming`);
180
+ // Dismissal and withdrawal are settlement paths too: the sender still
181
+ // needs any unresolved feedback and the exceptional page-change notice.
182
+ const body = (dismissed
183
+ ? `The user dismissed ${title === undefined ? `the human interaction ${ticketId}` : `“${title}”`} without answering — a deliberate “no, I'm not answering this.” Treat it as a rejection: no response will arrive, no partial answer exists, and the same ask must not be raised again.`
184
+ : (title === undefined ? `The human interaction ${ticketId} was canceled — ${reason}. No response will arrive, and any partial answers the user had entered were discarded unsent.` : `The page “${title}” was canceled — ${reason}. No response will arrive, and any partial answers the user had entered were discarded unsent.`)) + feedbackAddendum(dir);
105
185
  // One failing subscriber must not silence the rest, and the bridge stays
106
186
  // live until every notice lands: a partial fanout leaves the ticket for the
107
187
  // next resolve/cancel attempt or the daemon-start sweep to replay. A
108
188
  // subscriber notified twice reads the same idempotent "no answer is coming";
109
189
  // a subscriber never notified waits forever.
110
190
  const failedTo = [];
111
- for (const subscriber of subscribersOf(nodeId)) {
191
+ for (const subscriber of subscribersOf(bridgeNodeId)) {
112
192
  if (result.actor !== undefined && subscriber.node_id === result.actor)
113
193
  continue;
114
194
  try {
115
195
  appendInbox(subscriber.node_id, {
116
- from: nodeId,
117
- tier: 'deferred',
196
+ from: bridgeNodeId,
197
+ tier: 'normal',
118
198
  kind: 'message',
119
- label: `human interaction ${nodeId} canceled — no answer is coming${note}`,
120
- data: { body: `The human interaction ${nodeId} was canceled${note}. No response will arrive.` },
199
+ label: subject,
200
+ data: { body },
121
201
  });
122
202
  }
123
203
  catch {
@@ -126,11 +206,11 @@ export async function deliverTerminalResult(nodeId) {
126
206
  }
127
207
  if (failedTo.length > 0)
128
208
  throw new Error(`cancellation notice failed for ${failedTo.join(', ')}`);
129
- transition(nodeId, 'finish');
209
+ transition(bridgeNodeId, 'finish');
130
210
  return true;
131
211
  }
132
212
  if (result.kind === 'page') {
133
- await pushFinal(nodeId, renderPageAnswer(result, responsePath(dir)));
213
+ await pushFinal(bridgeNodeId, renderPageAnswer(dir, result, responsePath(dir)));
134
214
  }
135
215
  return true;
136
216
  }
@@ -1,11 +1,11 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { getNode } from '../../core/canvas/canvas.js';
3
+ import { getRow } from '../../core/canvas/canvas.js';
4
4
  import { getReviewByBridge } from '../../core/review/store.js';
5
5
  import { emitEvent } from '../../core/events/emit.js';
6
6
  import { ticketsRoot } from '../../core/human/root.js';
7
7
  import { readTicketResult } from '../../core/human/tickets.js';
8
- import { deliverTerminalResult } from './finish.js';
8
+ import { deliverTerminalResult, settleFeedbackCompanion } from './finish.js';
9
9
  function isTerminalBridge(status) {
10
10
  return status === 'done' || status === 'dead' || status === 'canceled';
11
11
  }
@@ -27,7 +27,13 @@ export async function reconcileUndeliveredTickets() {
27
27
  // finisher retries only when its winning operation is explicitly repeated.
28
28
  if (getReviewByBridge(entry) !== null)
29
29
  continue;
30
- const bridge = getNode(entry);
30
+ // A terminal page ticket retires its feedback companion whether or not a
31
+ // reply bridge exists: inbox-only pages have no bridge row, yet the
32
+ // daemon can die between result publication and companion retirement.
33
+ settleFeedbackCompanion(entry);
34
+ // The ROW is what carries status; `getNode` would answer from a pruned
35
+ // bridge's surviving meta.json with no status at all.
36
+ const bridge = getRow(entry);
31
37
  if (bridge === null || isTerminalBridge(bridge.status))
32
38
  continue;
33
39
  if (await deliverTerminalResult(entry))
@@ -0,0 +1,7 @@
1
+ import type { DeleteProfileResultDTO } from '../api/index.js';
2
+ /** Validate and enact one forced profile deletion. A request from a node that
3
+ * belongs to the target profile is acknowledged before its broker is stopped. */
4
+ export declare function requestProfileDeletion(name: string, body: unknown): Promise<{
5
+ status: 200 | 202;
6
+ body: DeleteProfileResultDTO;
7
+ }>;