@relayflows/sdk 2.0.30 → 2.0.32

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 (347) hide show
  1. package/dist/agent-artifacts.d.ts.map +1 -1
  2. package/dist/agent-artifacts.js +2 -9
  3. package/dist/agent-artifacts.js.map +1 -1
  4. package/dist/agent-cwd.d.ts +69 -0
  5. package/dist/agent-cwd.d.ts.map +1 -0
  6. package/dist/agent-cwd.js +140 -0
  7. package/dist/agent-cwd.js.map +1 -0
  8. package/dist/artifact-scan-policy.d.ts +35 -0
  9. package/dist/artifact-scan-policy.d.ts.map +1 -0
  10. package/dist/artifact-scan-policy.js +49 -0
  11. package/dist/artifact-scan-policy.js.map +1 -0
  12. package/dist/authored-completion-record.d.ts +35 -0
  13. package/dist/authored-completion-record.d.ts.map +1 -0
  14. package/dist/authored-completion-record.js +92 -0
  15. package/dist/authored-completion-record.js.map +1 -0
  16. package/dist/authored-completion.d.ts +99 -0
  17. package/dist/authored-completion.d.ts.map +1 -0
  18. package/dist/authored-completion.js +184 -0
  19. package/dist/authored-completion.js.map +1 -0
  20. package/dist/authored-flow-error.d.ts +13 -1
  21. package/dist/authored-flow-error.d.ts.map +1 -1
  22. package/dist/authored-flow-error.js +12 -0
  23. package/dist/authored-flow-error.js.map +1 -1
  24. package/dist/authored-flow-executor.d.ts +7 -30
  25. package/dist/authored-flow-executor.d.ts.map +1 -1
  26. package/dist/authored-flow-executor.js +54 -47
  27. package/dist/authored-flow-executor.js.map +1 -1
  28. package/dist/authored-human.d.ts.map +1 -1
  29. package/dist/authored-human.js +1 -3
  30. package/dist/authored-human.js.map +1 -1
  31. package/dist/authored-node-entry.js +4 -1
  32. package/dist/authored-node-entry.js.map +1 -1
  33. package/dist/authored-node-runner.d.ts +9 -1
  34. package/dist/authored-node-runner.d.ts.map +1 -1
  35. package/dist/authored-node-runner.js +92 -17
  36. package/dist/authored-node-runner.js.map +1 -1
  37. package/dist/authored-preflight.d.ts +4 -0
  38. package/dist/authored-preflight.d.ts.map +1 -0
  39. package/dist/authored-preflight.js +50 -0
  40. package/dist/authored-preflight.js.map +1 -0
  41. package/dist/authored-root.d.ts +5 -0
  42. package/dist/authored-root.d.ts.map +1 -1
  43. package/dist/authored-root.js +20 -2
  44. package/dist/authored-root.js.map +1 -1
  45. package/dist/authored-step-output.d.ts +35 -10
  46. package/dist/authored-step-output.d.ts.map +1 -1
  47. package/dist/authored-step-output.js +108 -40
  48. package/dist/authored-step-output.js.map +1 -1
  49. package/dist/authored-verdict.d.ts +28 -0
  50. package/dist/authored-verdict.d.ts.map +1 -0
  51. package/dist/authored-verdict.js +79 -0
  52. package/dist/authored-verdict.js.map +1 -0
  53. package/dist/authored-worker-step.d.ts +5 -2
  54. package/dist/authored-worker-step.d.ts.map +1 -1
  55. package/dist/authored-worker-step.js +51 -13
  56. package/dist/authored-worker-step.js.map +1 -1
  57. package/dist/cli/check-worker-surface.d.ts +27 -0
  58. package/dist/cli/check-worker-surface.d.ts.map +1 -0
  59. package/dist/cli/check-worker-surface.js +63 -0
  60. package/dist/cli/check-worker-surface.js.map +1 -0
  61. package/dist/cli/check.d.ts +28 -3
  62. package/dist/cli/check.d.ts.map +1 -1
  63. package/dist/cli/check.js +27 -140
  64. package/dist/cli/check.js.map +1 -1
  65. package/dist/cli/cli-probe.d.ts +20 -0
  66. package/dist/cli/cli-probe.d.ts.map +1 -0
  67. package/dist/cli/cli-probe.js +166 -0
  68. package/dist/cli/cli-probe.js.map +1 -0
  69. package/dist/cli/cloud-format.d.ts +9 -0
  70. package/dist/cli/cloud-format.d.ts.map +1 -0
  71. package/dist/cli/cloud-format.js +77 -0
  72. package/dist/cli/cloud-format.js.map +1 -0
  73. package/dist/cli/cloud-live.d.ts +39 -0
  74. package/dist/cli/cloud-live.d.ts.map +1 -0
  75. package/dist/cli/cloud-live.js +325 -0
  76. package/dist/cli/cloud-live.js.map +1 -0
  77. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  78. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  79. package/dist/cli/cloud-mirror-session.js +264 -0
  80. package/dist/cli/cloud-mirror-session.js.map +1 -0
  81. package/dist/cli/cloud-read.d.ts +3 -7
  82. package/dist/cli/cloud-read.d.ts.map +1 -1
  83. package/dist/cli/cloud-read.js +14 -266
  84. package/dist/cli/cloud-read.js.map +1 -1
  85. package/dist/cli/cloud-refusal.d.ts +39 -0
  86. package/dist/cli/cloud-refusal.d.ts.map +1 -0
  87. package/dist/cli/cloud-refusal.js +95 -0
  88. package/dist/cli/cloud-refusal.js.map +1 -0
  89. package/dist/cli/cloud-status-view.d.ts +7 -0
  90. package/dist/cli/cloud-status-view.d.ts.map +1 -0
  91. package/dist/cli/cloud-status-view.js +214 -0
  92. package/dist/cli/cloud-status-view.js.map +1 -0
  93. package/dist/cli/direct-run.d.ts.map +1 -1
  94. package/dist/cli/direct-run.js +16 -2
  95. package/dist/cli/direct-run.js.map +1 -1
  96. package/dist/cli/hn-monitor.d.ts +0 -13
  97. package/dist/cli/hn-monitor.d.ts.map +1 -1
  98. package/dist/cli/hn-monitor.js +2 -1
  99. package/dist/cli/hn-monitor.js.map +1 -1
  100. package/dist/cli/local-agent-remedy.d.ts +80 -0
  101. package/dist/cli/local-agent-remedy.d.ts.map +1 -0
  102. package/dist/cli/local-agent-remedy.js +116 -0
  103. package/dist/cli/local-agent-remedy.js.map +1 -0
  104. package/dist/cli/observer-session.d.ts +41 -0
  105. package/dist/cli/observer-session.d.ts.map +1 -0
  106. package/dist/cli/observer-session.js +78 -0
  107. package/dist/cli/observer-session.js.map +1 -0
  108. package/dist/cli/run.d.ts +30 -2
  109. package/dist/cli/run.d.ts.map +1 -1
  110. package/dist/cli/run.js +186 -24
  111. package/dist/cli/run.js.map +1 -1
  112. package/dist/cli/status.d.ts +9 -0
  113. package/dist/cli/status.d.ts.map +1 -1
  114. package/dist/cli/status.js +31 -2
  115. package/dist/cli/status.js.map +1 -1
  116. package/dist/cli/step-evidence.d.ts +90 -0
  117. package/dist/cli/step-evidence.d.ts.map +1 -0
  118. package/dist/cli/step-evidence.js +259 -0
  119. package/dist/cli/step-evidence.js.map +1 -0
  120. package/dist/cli/step-failure.d.ts +14 -4
  121. package/dist/cli/step-failure.d.ts.map +1 -1
  122. package/dist/cli/step-failure.js +41 -76
  123. package/dist/cli/step-failure.js.map +1 -1
  124. package/dist/cli-commands.d.ts +12 -0
  125. package/dist/cli-commands.d.ts.map +1 -1
  126. package/dist/cli-commands.js +3 -0
  127. package/dist/cli-commands.js.map +1 -1
  128. package/dist/cli.d.ts +17 -0
  129. package/dist/cli.d.ts.map +1 -1
  130. package/dist/cli.js +134 -57
  131. package/dist/cli.js.map +1 -1
  132. package/dist/cloud-deploy.d.ts +1 -1
  133. package/dist/cloud-deploy.d.ts.map +1 -1
  134. package/dist/cloud-deploy.js +24 -8
  135. package/dist/cloud-deploy.js.map +1 -1
  136. package/dist/cloud-mirror-ledger.d.ts +40 -0
  137. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  138. package/dist/cloud-mirror-ledger.js +142 -0
  139. package/dist/cloud-mirror-ledger.js.map +1 -0
  140. package/dist/cloud-mirror-step.d.ts +168 -0
  141. package/dist/cloud-mirror-step.d.ts.map +1 -0
  142. package/dist/cloud-mirror-step.js +504 -0
  143. package/dist/cloud-mirror-step.js.map +1 -0
  144. package/dist/cloud-mirror-transport.d.ts +94 -0
  145. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  146. package/dist/cloud-mirror-transport.js +163 -0
  147. package/dist/cloud-mirror-transport.js.map +1 -0
  148. package/dist/cloud-mirror.d.ts +132 -0
  149. package/dist/cloud-mirror.d.ts.map +1 -0
  150. package/dist/cloud-mirror.js +556 -0
  151. package/dist/cloud-mirror.js.map +1 -0
  152. package/dist/cloud-read.d.ts +15 -0
  153. package/dist/cloud-read.d.ts.map +1 -1
  154. package/dist/cloud-read.js +20 -1
  155. package/dist/cloud-read.js.map +1 -1
  156. package/dist/cloud-run-record.d.ts +27 -0
  157. package/dist/cloud-run-record.d.ts.map +1 -0
  158. package/dist/cloud-run-record.js +56 -0
  159. package/dist/cloud-run-record.js.map +1 -0
  160. package/dist/cloud-run.d.ts +2 -9
  161. package/dist/cloud-run.d.ts.map +1 -1
  162. package/dist/cloud-run.js +3 -20
  163. package/dist/cloud-run.js.map +1 -1
  164. package/dist/communication/local.d.ts.map +1 -1
  165. package/dist/communication/local.js +2 -1
  166. package/dist/communication/local.js.map +1 -1
  167. package/dist/communication/worker.d.ts +1 -1
  168. package/dist/communication/worker.d.ts.map +1 -1
  169. package/dist/communication/worker.js +13 -5
  170. package/dist/communication/worker.js.map +1 -1
  171. package/dist/compile.d.ts.map +1 -1
  172. package/dist/compile.js +28 -6
  173. package/dist/compile.js.map +1 -1
  174. package/dist/direct-input.d.ts.map +1 -1
  175. package/dist/direct-input.js +21 -1
  176. package/dist/direct-input.js.map +1 -1
  177. package/dist/failure-kinds.d.ts +96 -6
  178. package/dist/failure-kinds.d.ts.map +1 -1
  179. package/dist/failure-kinds.js +29 -2
  180. package/dist/failure-kinds.js.map +1 -1
  181. package/dist/flow-requirements.d.ts.map +1 -1
  182. package/dist/flow-requirements.js +24 -9
  183. package/dist/flow-requirements.js.map +1 -1
  184. package/dist/gate-contract.d.ts +7 -0
  185. package/dist/gate-contract.d.ts.map +1 -1
  186. package/dist/gate-contract.js +6 -1
  187. package/dist/gate-contract.js.map +1 -1
  188. package/dist/helper-preflight.d.ts.map +1 -1
  189. package/dist/helper-preflight.js +10 -3
  190. package/dist/helper-preflight.js.map +1 -1
  191. package/dist/helper-reference.d.ts +25 -0
  192. package/dist/helper-reference.d.ts.map +1 -0
  193. package/dist/helper-reference.js +129 -0
  194. package/dist/helper-reference.js.map +1 -0
  195. package/dist/hosted-extension-runtime.js +1 -1
  196. package/dist/index.d.ts +1 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js.map +1 -1
  199. package/dist/journal-client.d.ts +3 -1
  200. package/dist/journal-client.d.ts.map +1 -1
  201. package/dist/journal-client.js +17 -2
  202. package/dist/journal-client.js.map +1 -1
  203. package/dist/journal-projection.d.ts +16 -0
  204. package/dist/journal-projection.d.ts.map +1 -0
  205. package/dist/journal-projection.js +108 -0
  206. package/dist/journal-projection.js.map +1 -0
  207. package/dist/llm-worker.js +1 -1
  208. package/dist/llm-worker.js.map +1 -1
  209. package/dist/local-agent.d.ts +1 -1
  210. package/dist/local-agent.d.ts.map +1 -1
  211. package/dist/local-agent.js +4 -3
  212. package/dist/local-agent.js.map +1 -1
  213. package/dist/named-gate-lowering.js +35 -5
  214. package/dist/named-gate-lowering.js.map +1 -1
  215. package/dist/named-gate-preflight.d.ts +27 -0
  216. package/dist/named-gate-preflight.d.ts.map +1 -0
  217. package/dist/named-gate-preflight.js +47 -0
  218. package/dist/named-gate-preflight.js.map +1 -0
  219. package/dist/named-gates.d.ts +1 -1
  220. package/dist/named-gates.d.ts.map +1 -1
  221. package/dist/named-gates.js +3 -0
  222. package/dist/named-gates.js.map +1 -1
  223. package/dist/observer-link.d.ts +7 -0
  224. package/dist/observer-link.d.ts.map +1 -1
  225. package/dist/observer-link.js +3 -1
  226. package/dist/observer-link.js.map +1 -1
  227. package/dist/package-version.d.ts +10 -0
  228. package/dist/package-version.d.ts.map +1 -0
  229. package/dist/package-version.js +22 -0
  230. package/dist/package-version.js.map +1 -0
  231. package/dist/preflight.d.ts +14 -0
  232. package/dist/preflight.d.ts.map +1 -1
  233. package/dist/preflight.js +25 -3
  234. package/dist/preflight.js.map +1 -1
  235. package/dist/protocol.d.ts +2 -0
  236. package/dist/protocol.d.ts.map +1 -1
  237. package/dist/redact.d.ts +42 -0
  238. package/dist/redact.d.ts.map +1 -1
  239. package/dist/redact.js +128 -0
  240. package/dist/redact.js.map +1 -1
  241. package/dist/relay-cli.d.ts.map +1 -1
  242. package/dist/relay-cli.js +1 -21
  243. package/dist/relay-cli.js.map +1 -1
  244. package/dist/run-projection.d.ts +83 -0
  245. package/dist/run-projection.d.ts.map +1 -0
  246. package/dist/run-projection.js +199 -0
  247. package/dist/run-projection.js.map +1 -0
  248. package/dist/shell-word.d.ts +19 -0
  249. package/dist/shell-word.d.ts.map +1 -0
  250. package/dist/shell-word.js +23 -0
  251. package/dist/shell-word.js.map +1 -0
  252. package/dist/spec.d.ts +52 -3
  253. package/dist/spec.d.ts.map +1 -1
  254. package/dist/spec.js.map +1 -1
  255. package/dist/step-fields.d.ts +1 -1
  256. package/dist/step-fields.js +1 -1
  257. package/dist/step-fields.js.map +1 -1
  258. package/dist/steps-green.d.ts +27 -0
  259. package/dist/steps-green.d.ts.map +1 -0
  260. package/dist/steps-green.js +164 -0
  261. package/dist/steps-green.js.map +1 -0
  262. package/dist/validate.d.ts.map +1 -1
  263. package/dist/validate.js +39 -8
  264. package/dist/validate.js.map +1 -1
  265. package/dist/worker-lease.d.ts +8 -1
  266. package/dist/worker-lease.d.ts.map +1 -1
  267. package/dist/worker-lease.js +34 -4
  268. package/dist/worker-lease.js.map +1 -1
  269. package/dist/worker.d.ts +8 -0
  270. package/dist/worker.d.ts.map +1 -1
  271. package/dist/worker.js +17 -9
  272. package/dist/worker.js.map +1 -1
  273. package/package.json +3 -7
  274. package/src/agent-artifacts.ts +2 -10
  275. package/src/agent-cwd.ts +139 -0
  276. package/src/artifact-scan-policy.ts +51 -0
  277. package/src/authored-completion-record.ts +119 -0
  278. package/src/authored-completion.ts +203 -0
  279. package/src/authored-flow-error.ts +13 -1
  280. package/src/authored-flow-executor.ts +91 -58
  281. package/src/authored-human.ts +1 -4
  282. package/src/authored-node-entry.ts +4 -1
  283. package/src/authored-node-runner.ts +96 -22
  284. package/src/authored-preflight.ts +46 -0
  285. package/src/authored-root.ts +20 -2
  286. package/src/authored-step-output.ts +139 -42
  287. package/src/authored-verdict.ts +87 -0
  288. package/src/authored-worker-step.ts +63 -14
  289. package/src/cli/check-worker-surface.ts +66 -0
  290. package/src/cli/check.ts +59 -166
  291. package/src/cli/cli-probe.ts +194 -0
  292. package/src/cli/cloud-format.ts +82 -0
  293. package/src/cli/cloud-live.ts +366 -0
  294. package/src/cli/cloud-mirror-session.ts +329 -0
  295. package/src/cli/cloud-read.ts +13 -282
  296. package/src/cli/cloud-refusal.ts +113 -0
  297. package/src/cli/cloud-status-view.ts +213 -0
  298. package/src/cli/direct-run.ts +19 -2
  299. package/src/cli/hn-monitor.ts +2 -1
  300. package/src/cli/local-agent-remedy.ts +160 -0
  301. package/src/cli/observer-session.ts +107 -0
  302. package/src/cli/run.ts +233 -28
  303. package/src/cli/status.ts +59 -3
  304. package/src/cli/step-evidence.ts +310 -0
  305. package/src/cli/step-failure.ts +57 -81
  306. package/src/cli-commands.ts +3 -0
  307. package/src/cli.ts +155 -60
  308. package/src/cloud-deploy.ts +26 -8
  309. package/src/cloud-mirror-ledger.ts +161 -0
  310. package/src/cloud-mirror-step.ts +618 -0
  311. package/src/cloud-mirror-transport.ts +261 -0
  312. package/src/cloud-mirror.ts +633 -0
  313. package/src/cloud-read.ts +25 -1
  314. package/src/cloud-run-record.ts +64 -0
  315. package/src/cloud-run.ts +5 -24
  316. package/src/communication/local.ts +2 -1
  317. package/src/communication/worker.ts +13 -5
  318. package/src/compile.ts +28 -6
  319. package/src/direct-input.ts +22 -1
  320. package/src/failure-kinds.ts +104 -2
  321. package/src/flow-requirements.ts +24 -9
  322. package/src/gate-contract.ts +13 -1
  323. package/src/helper-preflight.ts +10 -3
  324. package/src/helper-reference.ts +130 -0
  325. package/src/hosted-extension-runtime.ts +1 -1
  326. package/src/index.ts +2 -0
  327. package/src/journal-client.ts +19 -2
  328. package/src/journal-projection.ts +106 -0
  329. package/src/llm-worker.ts +1 -1
  330. package/src/local-agent.ts +4 -2
  331. package/src/named-gate-lowering.ts +35 -5
  332. package/src/named-gate-preflight.ts +47 -0
  333. package/src/named-gates.ts +3 -0
  334. package/src/observer-link.ts +10 -1
  335. package/src/package-version.ts +23 -0
  336. package/src/preflight.ts +38 -7
  337. package/src/protocol.ts +2 -0
  338. package/src/redact.ts +111 -0
  339. package/src/relay-cli.ts +1 -24
  340. package/src/run-projection.ts +247 -0
  341. package/src/shell-word.ts +24 -0
  342. package/src/spec.ts +54 -3
  343. package/src/step-fields.ts +1 -1
  344. package/src/steps-green.ts +168 -0
  345. package/src/validate.ts +35 -8
  346. package/src/worker-lease.ts +35 -5
  347. package/src/worker.ts +27 -11
@@ -0,0 +1,213 @@
1
+ // The `flows status --cloud` page: one run record and its step rows, rendered
2
+ // in the grammar `cli/status.ts` already uses for a local run.
3
+ //
4
+ // Moved out of `cli/cloud-read.ts` so the one-shot read and the watched read
5
+ // (cli/cloud-live.ts) draw the identical page through the identical scrubber.
6
+ // A watched frame that redacted differently to the page it replaces would be
7
+ // a leak nobody could see from either file alone.
8
+ //
9
+ // Live rows are the part this view has that the local one does not have to
10
+ // think about: Cloud's step snapshot is the only evidence, and where it is
11
+ // silent the row stays silent. An elapsed time is derived only for a step that
12
+ // is demonstrably running, an attempt number is printed only where the
13
+ // snapshot establishes one, and neither a maximum-attempt denominator nor a
14
+ // wait id is invented — Cloud's step rows carry neither.
15
+
16
+ import { isCloudRunActive } from '../cloud-run-record.js';
17
+ import { redact } from '../redact.js';
18
+ import type { CloudRunDetail, CloudStep } from '../cloud-read.js';
19
+ import { ago, dollars, errorLines, safe, thousands } from './cloud-format.js';
20
+ import { formatDuration } from './status.js';
21
+
22
+ const GLYPH: Record<string, string> = {
23
+ completed: '✓', succeeded: '✓', failed: '✗', cancelled: '✗',
24
+ running: '↻', backoff: '↻', waiting: '⏸', needs_human: '⏸',
25
+ pending: '○', queued: '○',
26
+ };
27
+
28
+ /**
29
+ * Step statuses that mean the step is in flight, in the local view's
30
+ * vocabulary (`cli/status.ts`). These are the rows with no `durationMs` and no
31
+ * `endTime`: their timing has to be derived, and their attempt number is the
32
+ * one being attempted rather than a count of the ones that finished.
33
+ */
34
+ const ACTIVE_STEP_STATUSES: readonly string[] = ['running', 'backoff', 'waiting', 'needs_human'];
35
+
36
+ function glyph(step: CloudStep): string {
37
+ if (step.status === 'completed' && step.completion_reason !== null && step.completion_reason !== 'success') return '✗';
38
+ return GLYPH[step.status] ?? '○';
39
+ }
40
+
41
+ /** The `steps N: 3 done · 1 running` line, counted off whatever Cloud called each status. */
42
+ function stepCounts(steps: readonly CloudStep[]): string {
43
+ const counts = new Map<string, number>();
44
+ for (const step of steps) counts.set(step.status, (counts.get(step.status) ?? 0) + 1);
45
+ const parts = [...counts].map(([status, count]) => `${count} ${status}`);
46
+ return `steps ${steps.length}${parts.length === 0 ? '' : `: ${parts.join(' · ')}`}`;
47
+ }
48
+
49
+ /**
50
+ * How long an in-flight step has been in flight.
51
+ *
52
+ * Only from a start timestamp the row actually carries, and only while the row
53
+ * says the step has not ended: a finished step whose `endTime` is missing keeps
54
+ * whatever duration Cloud reported rather than being advanced to now, which
55
+ * would make a step that ended an hour ago look like it is still burning. A
56
+ * start in the future is clock skew between Cloud and here, not negative time,
57
+ * so it clamps to zero.
58
+ */
59
+ function liveElapsed(step: CloudStep, now: number): number | null {
60
+ if (!ACTIVE_STEP_STATUSES.includes(step.status) || step.ended_at !== null || step.started_at === null) return null;
61
+ const started = Date.parse(step.started_at);
62
+ return Number.isFinite(started) ? Math.max(0, now - started) : null;
63
+ }
64
+
65
+ /**
66
+ * The attempt cell.
67
+ *
68
+ * A finished step counts the attempt records Cloud kept. An in-flight step
69
+ * wants the number of the attempt *now running*, and `attempts` cannot supply
70
+ * it: those entries are completion records, so during a second attempt the
71
+ * array still holds one entry and would print `attempt 1`. `retryCount` plus
72
+ * one is that number — but only where the row also shows the step was
73
+ * dispatched, because a row with no start timestamp has not attempted
74
+ * anything and `attempt 1` would be a claim about work that has not begun.
75
+ * Where neither establishes it, the cell is omitted: the local view prints no
76
+ * attempt at zero either. No `/max` denominator — Cloud's step rows carry no
77
+ * maximum, and printing one would invent a budget.
78
+ */
79
+ function attemptCell(step: CloudStep): string | null {
80
+ if (!ACTIVE_STEP_STATUSES.includes(step.status)) {
81
+ const attempts = step.attempts.length;
82
+ return attempts > 0 ? `${attempts} attempt${attempts === 1 ? '' : 's'}` : null;
83
+ }
84
+ if (step.retry_count === null || step.started_at === null) return null;
85
+ return `attempt ${Math.max(0, Math.trunc(step.retry_count)) + 1}`;
86
+ }
87
+
88
+ function renderStep(step: CloudStep, runId: string, idWidth: number, now: number): string[] {
89
+ const cells = [
90
+ safe(step.step_name).slice(0, 24).padEnd(idWidth),
91
+ step.step_type.padEnd(13),
92
+ step.status.padEnd(11),
93
+ ];
94
+ const attempt = attemptCell(step);
95
+ if (attempt !== null) cells.push(attempt);
96
+ // The derived time first, not second: a row that is still in flight can
97
+ // carry a `wallclockMs` from the attempt that already ended, and preferring
98
+ // it would freeze the cell at that attempt's duration while the step runs
99
+ // on. `liveElapsed` is null for every row that is not demonstrably in
100
+ // flight, so a finished row still prints the duration Cloud reported.
101
+ const elapsed = liveElapsed(step, now) ?? step.duration_ms;
102
+ if (elapsed !== null) cells.push(formatDuration(elapsed));
103
+ if (step.completion_reason !== null) cells.push(safe(step.completion_reason));
104
+ if (step.gate !== null) cells.push(`gate: ${safe(step.gate.gate)} ${safe(step.gate.verdict)}`);
105
+ const lines = [` ${glyph(step)} ${cells.join(' ')}`.trimEnd()];
106
+
107
+ if (step.gate !== null && step.gate.verdict !== 'pass' && step.gate.detail.length > 0) {
108
+ lines.push(` gate: ${safe(step.gate.gate)} ${step.gate.verdict.toUpperCase()} — ${JSON.stringify(safe(step.gate.detail))}`);
109
+ }
110
+ const transcript = step.transcript;
111
+ if (transcript !== null) {
112
+ const facts = [
113
+ transcript.model === null ? null : safe(transcript.model),
114
+ transcript.num_turns === null ? null : `${transcript.num_turns} turn${transcript.num_turns === 1 ? '' : 's'}`,
115
+ transcript.total_calls === null ? null : `${transcript.total_calls} tool call${transcript.total_calls === 1 ? '' : 's'}`,
116
+ transcript.total_cost_usd === null ? null : dollars(transcript.total_cost_usd),
117
+ ].filter((fact): fact is string => fact !== null);
118
+ lines.push(` transcript (attempt ${transcript.attempt ?? step.attempts.length})${facts.length === 0 ? '' : `: ${facts.join(' · ')}`}`);
119
+ if (transcript.frames_total !== null || transcript.bytes_total !== null) {
120
+ const frames = transcript.frames_total === null ? ''
121
+ : `${thousands(transcript.frames_kept ?? transcript.frames_total)} of ${thousands(transcript.frames_total)} frames`;
122
+ const bytes = transcript.bytes_total === null ? '' : `${thousands(transcript.bytes_total)} bytes`;
123
+ const cut = transcript.truncated ? ', truncated' : '';
124
+ lines.push(` ${[frames, bytes].filter((part) => part.length > 0).join(', ')}${cut}`);
125
+ }
126
+ if (transcript.tokens_in !== null || transcript.tokens_out !== null) {
127
+ const cache = [
128
+ transcript.cache_read === null ? null : `${thousands(transcript.cache_read)} cache read`,
129
+ transcript.cache_creation === null ? null : `${thousands(transcript.cache_creation)} cache write`,
130
+ ].filter((part): part is string => part !== null);
131
+ lines.push(` ${thousands(transcript.tokens_in ?? 0)} in / ${thousands(transcript.tokens_out ?? 0)} out`
132
+ + `${cache.length === 0 ? '' : ` · ${cache.join(' · ')}`}`);
133
+ }
134
+ if (transcript.tools.length > 0) {
135
+ lines.push(` tools: ${transcript.tools.map((tool) =>
136
+ `${safe(tool.name)} ×${tool.calls ?? 0}${tool.errors ? ` (${tool.errors} error${tool.errors === 1 ? '' : 's'})` : ''}`).join(', ')}`);
137
+ }
138
+ for (const call of transcript.last_calls) {
139
+ const excerpt = call.input_excerpt === null ? '' : ` ${safe(call.input_excerpt).slice(0, 120)}`;
140
+ lines.push(` call ${call.seq ?? '?'} ${safe(call.name)}${excerpt} → ${thousands(call.result_bytes ?? 0)} bytes`);
141
+ }
142
+ lines.push(` artifacts: ${transcript.artifacts.length === 0
143
+ ? 'none' : transcript.artifacts.slice(0, 10).map(safe).join(', ')}`);
144
+ }
145
+ if (step.sandbox_id.length > 0) lines.push(` logs: flows logs ${runId} --step ${safe(step.sandbox_id)}`);
146
+ return lines;
147
+ }
148
+
149
+ export function renderCloudStatus(run: CloudRunDetail, steps: readonly CloudStep[], now: number): string[] {
150
+ let tokensIn = 0;
151
+ let tokensOut = 0;
152
+ let cost = 0;
153
+ for (const step of steps) {
154
+ tokensIn += step.tokens_in ?? 0;
155
+ tokensOut += step.tokens_out ?? 0;
156
+ cost += step.cost_usd ?? 0;
157
+ }
158
+ // Summed from the step rows, because the run record carries no total. Cloud
159
+ // stores `costUsd` as a float, so this is a float sum — not the exact decimal
160
+ // addition the local journal fold does (run-state.ts `addDollars`). Shown to
161
+ // six places and labelled `spend`, the same word the local view uses.
162
+ const spend = `${thousands(tokensIn)} in / ${thousands(tokensOut)} out / ${dollars(cost)}`;
163
+ const finished = run.completion_reason === null ? '' : ` finished ${safe(run.completion_reason)}`;
164
+ const lines = [
165
+ `RUN ${run.run_id} ${safe(run.name)} ${run.status} started ${ago(run.created_at, now)}${finished} spend ${spend}`,
166
+ stepCounts(steps),
167
+ ];
168
+ // `steps 0` alone reads as "this run has no steps", which is a claim about
169
+ // the run. For a run that is still going it is a fact about the snapshot:
170
+ // Cloud served no rows, this time.
171
+ if (steps.length === 0 && isCloudRunActive(run.status)) lines.push('No step snapshot available yet.');
172
+ if (run.authority !== null) {
173
+ const facts = [
174
+ run.authority.surface_version === null ? null : `surface ${run.authority.surface_version}`,
175
+ run.authority.artifact_sha256 === null ? null : `artifact ${run.authority.artifact_sha256.slice(0, 12)}`,
176
+ run.authority.source_commit === null ? null : `commit ${run.authority.source_commit.slice(0, 12)}`,
177
+ ].filter((fact): fact is string => fact !== null);
178
+ if (facts.length > 0) lines.push(`authority ${facts.join(' · ')}`);
179
+ }
180
+ if (run.pull_request_url !== null) lines.push(`pr ${run.pull_request_url}`);
181
+ if (run.error !== null) lines.push('error', ...errorLines(run.error, ' '));
182
+ lines.push('');
183
+ const idWidth = Math.max(4, ...steps.map((step) => Math.min(24, step.step_name.length)));
184
+ for (const step of steps) lines.push(...renderStep(step, run.run_id, idWidth, now));
185
+ return lines;
186
+ }
187
+
188
+ /** Every free-text field of the run record, through the status page's redactor. */
189
+ export function scrubRun(run: CloudRunDetail, env: NodeJS.ProcessEnv): CloudRunDetail {
190
+ return {
191
+ ...run,
192
+ name: redact(run.name, env),
193
+ error: run.error === null ? null : redact(run.error, env),
194
+ };
195
+ }
196
+
197
+ /** The same, for the step rows — including every string inside the digest. */
198
+ export function scrubSteps(steps: readonly CloudStep[], env: NodeJS.ProcessEnv): CloudStep[] {
199
+ return steps.map((step) => ({
200
+ ...step,
201
+ output_summary: step.output_summary === null ? null : redact(step.output_summary, env),
202
+ gate: step.gate === null ? null : { ...step.gate, detail: redact(step.gate.detail, env) },
203
+ transcript: step.transcript === null ? null : {
204
+ ...step.transcript,
205
+ model: step.transcript.model === null ? null : redact(step.transcript.model, env),
206
+ last_calls: step.transcript.last_calls.map((call) => ({
207
+ ...call,
208
+ input_excerpt: call.input_excerpt === null ? null : redact(call.input_excerpt, env),
209
+ })),
210
+ artifacts: step.transcript.artifacts.map((path) => redact(path, env)),
211
+ },
212
+ }));
213
+ }
@@ -1,3 +1,4 @@
1
+ import { onWorkerFailure } from '../worker-lease.js';
1
2
  import { McpStepError } from '../authored-mcp.js';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { authoredLocalAgentStream } from '../authored-admission.js';
@@ -15,6 +16,7 @@ import { DirectInputError, parseDirectInput } from '../direct-input.js';
15
16
  import { JournalClient } from '../journal-client.js';
16
17
  import { inputFailureReport } from './check.js';
17
18
  import { checkAuthoredTriggers } from './check-triggers.js';
19
+ import { authoredInput, authoredWorkerRemedy, localAgentRemedy } from './local-agent-remedy.js';
18
20
  import {
19
21
  authoredCompletion,
20
22
  authoredHumanParked,
@@ -82,7 +84,7 @@ export async function runDirectFlow(
82
84
  await llmClient.connect();
83
85
  await llmClient.hello('flows-local-llm');
84
86
  localLlm = new LlmWorker(llmClient, `${localAgent.stream}-llm`, workerCapacity);
85
- localLlm.on('error', error => { llmFailure = error; client.close(); });
87
+ localLlm.on('error', onWorkerFailure('local-llm', error => { llmFailure = error; client.close(); }));
86
88
  await localLlm.attach();
87
89
  }
88
90
  const result = await executeDurableAuthoredFlow(
@@ -94,6 +96,7 @@ export async function runDirectFlow(
94
96
  ...(localAgent === undefined ? {} : { workerCapacity }),
95
97
  lifecycle: {
96
98
  onProgress: options.onProgress,
99
+ ...(options.onRunStarted !== undefined ? { onRunStarted: options.onRunStarted } : {}),
97
100
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
98
101
  ...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
99
102
  },
@@ -163,12 +166,26 @@ export async function runDirectFlow(
163
166
  ...base,
164
167
  ok: false,
165
168
  runId: error.runId,
169
+ rootRunId: error.rootRunId,
166
170
  socketPath,
167
171
  status: 'parked',
172
+ parkCause: error.parkCause,
168
173
  diagnostics: [...base.diagnostics, {
169
174
  severity: 'parked',
170
175
  kind: 'run_parked',
171
- message: error.message,
176
+ // The remedy is rendered here rather than by the child's own
177
+ // `classifyOutcome`, which is deliberately silent for authored
178
+ // paths: this is the only frame that knows both the flow path and
179
+ // the `--input` argument a new run has to repeat, and it knows
180
+ // whether a worker was already attached.
181
+ //
182
+ // `inputArgument` is the word this process was invoked with, so a
183
+ // run started from a file names that file rather than the megabyte
184
+ // inside it — the thing the journal can no longer tell a resume.
185
+ message: error.message + localAgentRemedy(authoredWorkerRemedy(
186
+ error.parkCause, options.localAgent === true,
187
+ { path, input: authoredInput(inputArgument), dataDir },
188
+ )),
172
189
  }],
173
190
  },
174
191
  };
@@ -1,3 +1,4 @@
1
+ import { onWorkerFailure } from '../worker-lease.js';
1
2
  /**
2
3
  * `flows hn-monitor start` — CLI-inlined proactive workload for gate 2.
3
4
  *
@@ -169,7 +170,7 @@ async function defaultAttachWorker(
169
170
  // synchronously, bypasses the drain-aware close(), and crashes the
170
171
  // process. Subscribe BEFORE attach so an error during attach is not
171
172
  // lost.
172
- worker.on('error', onWorkerError);
173
+ worker.on('error', onWorkerFailure('hn-monitor', onWorkerError));
173
174
  await worker.attach();
174
175
  return worker;
175
176
  }
@@ -0,0 +1,160 @@
1
+ import { resumeCommand } from '../authored-human.js';
2
+ import type { ParkCause } from '../failure-kinds.js';
3
+ import { shellQuote, shellWord } from '../shell-word.js';
4
+
5
+ /**
6
+ * What to tell someone whose run stopped for want of an agent worker.
7
+ *
8
+ * A closed union rather than a bag of optional fields: the remedy for a spec
9
+ * run is a *new* run, for a spec resume it is *this* run, and for an authored
10
+ * `.flow.ts` it is a new run that must carry the same `--input` — three
11
+ * different sentences, and no combination of them is meaningful. `attached`
12
+ * is the case the message used to get wrong by omission: a worker WAS offered
13
+ * and none of them was eligible, so naming the flag again is not the fix.
14
+ */
15
+ export type LocalAgentRemedy =
16
+ /** Nothing to say: not a worker park, or the caller renders it elsewhere. */
17
+ | { kind: 'none' }
18
+ | { kind: 'attached' }
19
+ | { kind: 'spec-run'; path: string; dataDir?: string }
20
+ | { kind: 'spec-resume'; runId: string; dataDir?: string }
21
+ | { kind: 'authored-run'; path: string; input: AuthoredInput; dataDir?: string };
22
+
23
+ /**
24
+ * The `--input` argument a new run of an authored flow would have to repeat.
25
+ *
26
+ * Three outcomes, not an optional string, because each is a different sentence
27
+ * and only one of them ends in a command. None of them is `{}`: fabricating an
28
+ * input would hand over a command that runs a different flow invocation than
29
+ * the one that parked.
30
+ */
31
+ export type AuthoredInput =
32
+ /** The argument this run was started with, rendered back verbatim. */
33
+ | { kind: 'inline'; argument: string }
34
+ /** The journal recorded no input argument to repeat. */
35
+ | { kind: 'absent' }
36
+ /** Recorded, but too many bytes to hand a shell as one word. */
37
+ | { kind: 'oversized'; bytes: number };
38
+
39
+ /**
40
+ * The largest `--input` value worth printing inline, in bytes.
41
+ *
42
+ * Linux caps a single `execve` argument at `MAX_ARG_STRLEN` — 32 pages,
43
+ * 131072 bytes including the terminating NUL — independently of the much
44
+ * larger `ARG_MAX` total. `MAX_DIRECT_INPUT_BYTES` admits a full mebibyte, so
45
+ * a run started from a perfectly ordinary input file can have recorded more
46
+ * input than any shell can pass back. Printing it anyway yields a command that
47
+ * dies with `Argument list too long` before the CLI is even reached, which is
48
+ * no better than the park it was meant to answer.
49
+ */
50
+ const MAX_INLINE_INPUT_BYTES = 131_071;
51
+
52
+ /** How a recorded input argument should be rendered, given its size. */
53
+ export function authoredInput(argument: string | undefined): AuthoredInput {
54
+ if (argument === undefined) return { kind: 'absent' };
55
+ const bytes = Buffer.byteLength(argument, 'utf8');
56
+ return bytes > MAX_INLINE_INPUT_BYTES ? { kind: 'oversized', bytes } : { kind: 'inline', argument };
57
+ }
58
+
59
+ /**
60
+ * The declared-surface caveat, stated as the condition it is.
61
+ *
62
+ * A step that declares a workspace or a stream needs a worker holding those
63
+ * pins, and the local agent worker holds only its own stream. That is worth
64
+ * saying next to the command — but it is a caveat on the remedy, not a
65
+ * diagnosis of this park, and it is phrased so it cannot be read as one.
66
+ */
67
+ const PINS = 'Declared workspace or stream surfaces require a worker that holds their pins.';
68
+
69
+ /**
70
+ * The remedy clause appended to a worker-park diagnostic, with a leading
71
+ * space, or `''` when there is nothing to add.
72
+ */
73
+ export function localAgentRemedy(remedy: LocalAgentRemedy): string {
74
+ switch (remedy.kind) {
75
+ case 'none':
76
+ return '';
77
+ case 'attached':
78
+ // Never "pass --local-agent": it was passed. The honest report is that
79
+ // a worker was offered and the daemon matched none of them to this step.
80
+ return ' A local agent worker is already attached to this run, so passing --local-agent again'
81
+ + ' changes nothing: no attached worker was eligible for this step.'
82
+ + ` ${PINS} The local agent worker holds only its own stream.`;
83
+ case 'spec-run':
84
+ return ` To start a new run with a local agent worker: ${
85
+ newRunCommand(remedy.path, undefined, remedy.dataDir)}. ${PINS}`;
86
+ case 'spec-resume':
87
+ // This run is resumable, so the remedy is this run — not a new one.
88
+ return ` To continue this run with a local agent worker: ${
89
+ resumeCommand(remedy.runId, remedy.dataDir, true)}. ${PINS}`;
90
+ case 'authored-run':
91
+ return authoredRunRemedy(remedy);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Either a runnable new run, or the requirement stated in prose.
97
+ *
98
+ * Prose, not a command, whenever the `--input` cannot be printed as something
99
+ * a shell would deliver intact: a `.flow.ts` invocation without its `--input`
100
+ * is refused (`input_missing`), and a placeholder or a truncation in its place
101
+ * is worse than a sentence, because it looks runnable.
102
+ */
103
+ function authoredRunRemedy(remedy: Extract<LocalAgentRemedy, { kind: 'authored-run' }>): string {
104
+ if (remedy.input.kind === 'inline') {
105
+ return ` To start a new run with a local agent worker: ${
106
+ newRunCommand(remedy.path, remedy.input.argument, remedy.dataDir)}. ${PINS}`;
107
+ }
108
+ const requirement = ' A local agent worker is admitted at run start, so this run needs a new one.'
109
+ + ` Starting one needs --local-agent, the flow path ${shellQuote(remedy.path)}, and the same`
110
+ + ' --input this flow takes; ';
111
+ return requirement + (remedy.input.kind === 'absent'
112
+ ? 'the journal recorded no input argument to repeat here.'
113
+ // Naming the size says which input, and says why this clause is prose:
114
+ // the recorded value is larger than one `execve` argument may be, so a
115
+ // printed `--input '<json>'` would fail before the CLI ran at all.
116
+ : `the journal recorded ${remedy.input.bytes} bytes of input, more than a shell`
117
+ + ' can carry in one argument, so pass the input file this run was started from.')
118
+ + ` ${PINS}`;
119
+ }
120
+
121
+ /**
122
+ * Which remedy an authored `.flow.ts` park gets, decided once for both authored
123
+ * boundaries (`cli/direct-run.ts` on a fresh run, `resumeFlow` on a resume).
124
+ *
125
+ * The cause is trusted, not guessed: anything other than `worker_unavailable` —
126
+ * including an absent cause — yields `none`, so a `needs_human` recovery wait is
127
+ * never answered with "attach a worker", and an unclassified park says nothing
128
+ * at all rather than something plausible.
129
+ *
130
+ * `path` is optional because the caller may only have the journal to go on. A
131
+ * park with no known flow path yields `none` for the same reason an unknown
132
+ * input yields prose: the point of this clause is a command someone can run.
133
+ */
134
+ export function authoredWorkerRemedy(
135
+ parkCause: ParkCause | undefined,
136
+ attached: boolean,
137
+ run: { path?: string; input: AuthoredInput; dataDir?: string },
138
+ ): LocalAgentRemedy {
139
+ if (parkCause !== 'worker_unavailable') return { kind: 'none' };
140
+ if (attached) return { kind: 'attached' };
141
+ if (run.path === undefined) return { kind: 'none' };
142
+ return { kind: 'authored-run', path: run.path, input: run.input, dataDir: run.dataDir };
143
+ }
144
+
145
+ /**
146
+ * `flows run --local-agent <path> [--input <input>] [--data-dir <dir>]`.
147
+ *
148
+ * The flag/path prefix is byte-for-byte what the spec-run message has always
149
+ * emitted, so the one thing people already grep for still matches. `--input`
150
+ * and `--data-dir` follow the path because the parser is order-insensitive
151
+ * (cli.ts `parseRunArgs`), and keeping the prefix intact matters more than
152
+ * mirroring the usage line's flag order.
153
+ */
154
+ function newRunCommand(path: string, input: string | undefined, dataDir: string | undefined): string {
155
+ return `flows run --local-agent ${shellQuote(path)}`
156
+ + (input === undefined ? '' : ` --input ${shellQuote(input)}`)
157
+ // `shellWord` here, matching `resumeCommand`/`answerCommand`: a data dir is
158
+ // this machine's path, and the two renderings must not disagree.
159
+ + (dataDir === undefined ? '' : ` --data-dir ${shellWord(dataDir)}`);
160
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The observer half of `flows run` / `flows resume`: once the run id is known,
3
+ * project the run into its `wf-<runId>` channel and mint a read-only link
4
+ * scoped to that channel, so the link opens on this run's step graph instead
5
+ * of an arbitrary workspace channel. The link is printed on stderr the moment
6
+ * it exists, so a human can follow the run live, and again after `RUN`.
7
+ *
8
+ * Best-effort throughout: no workspace key means no session, and a projection
9
+ * or mint failure is a labeled stderr line. The run never depends on it.
10
+ */
11
+
12
+ import type { CliIo } from '../cli.js';
13
+ import { createJournalProjector } from '../journal-projection.js';
14
+ import type { JournalEvent } from '../journal-reader.js';
15
+ import { mintObserverUrl, resolveObserverLinkEnv, type MintObserverOptions } from '../observer-link.js';
16
+ import type { ProgressEvent } from '../progress.js';
17
+ import {
18
+ createRunProjection, runChannelName, type DeclaredStep, type ProjectionFetch, type RunProjection,
19
+ } from '../run-projection.js';
20
+ import type { RunReport } from './run.js';
21
+
22
+ type MintOutcome = { observerUrl?: string; warning?: string };
23
+
24
+ /** Bounded so an unreachable Relaycast cannot hold the CLI open. */
25
+ const DRAIN_GRACE_MS = 5_000;
26
+
27
+ export interface ObserverSession {
28
+ /** YAML runs: every entry `run.start {watch}` / `run.watch` pushes. */
29
+ onJournalEntry(entry: JournalEvent): void;
30
+ /** Authored runs: the root's id and name, known once it is admitted. */
31
+ onRunStarted(run: { runId: string; flow: string; resumed?: boolean }): void;
32
+ /** Authored runs: step transitions from the body's executor. */
33
+ onProgress(event: ProgressEvent): void;
34
+ /** Close the projection from the final report and settle the link. */
35
+ finish(report: RunReport): Promise<MintOutcome> | undefined;
36
+ }
37
+
38
+ export interface ObserverSessionDeps {
39
+ fetch?: ProjectionFetch;
40
+ mint?: (options: MintObserverOptions) => Promise<MintOutcome>;
41
+ }
42
+
43
+ export function createObserverSession(
44
+ command: 'run' | 'resume',
45
+ io: CliIo,
46
+ env: NodeJS.ProcessEnv = process.env,
47
+ deps: ObserverSessionDeps = {},
48
+ ): ObserverSession | undefined {
49
+ const link = resolveObserverLinkEnv(env);
50
+ if (link.suppressed || link.workspaceKey === undefined) return undefined;
51
+ const workspaceKey = link.workspaceKey;
52
+ const mint = deps.mint ?? mintObserverUrl;
53
+ const liveSinceMs = command === 'resume' ? Date.now() : 0;
54
+ let projection: RunProjection | undefined;
55
+ let minted: Promise<MintOutcome> | undefined;
56
+ let rootRunId: string | undefined;
57
+
58
+ const mintFor = (runId: string): Promise<MintOutcome> => minted ??= mint({
59
+ workspaceKey,
60
+ channel: runChannelName(runId),
61
+ ...(link.baseUrl === undefined ? {} : { baseUrl: link.baseUrl }),
62
+ ...(link.dashboardUrl === undefined ? {} : { dashboardUrl: link.dashboardUrl }),
63
+ }).catch((error: unknown) => ({ warning: error instanceof Error ? error.message : 'unknown mint error' }));
64
+
65
+ const open = (run: { runId: string; flow: string; steps?: DeclaredStep[]; resumed?: boolean }): RunProjection => {
66
+ if (projection !== undefined) return projection;
67
+ rootRunId = run.runId;
68
+ projection = createRunProjection({
69
+ workspaceKey,
70
+ ...(link.baseUrl === undefined ? {} : { baseUrl: link.baseUrl }),
71
+ ...(deps.fetch === undefined ? {} : { fetch: deps.fetch }),
72
+ diagnostic: message => io.stderr(`[observer] ${message}`),
73
+ }, run);
74
+ void mintFor(run.runId).then(outcome => {
75
+ if (outcome.observerUrl !== undefined) io.stderr(`Observer: ${outcome.observerUrl}`);
76
+ });
77
+ return projection;
78
+ };
79
+ const project = createJournalProjector(
80
+ run => open({ ...run, ...(liveSinceMs > 0 ? { resumed: true } : {}) }), liveSinceMs,
81
+ );
82
+
83
+ return {
84
+ onJournalEntry(entry) {
85
+ try { project(entry); } catch (error) {
86
+ io.stderr(`[observer] could not project journal entry ${entry.seq}: ${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ },
89
+ onRunStarted: run => { open(run); },
90
+ onProgress(event) { projection?.step(event); },
91
+ finish(report) {
92
+ const runId = rootRunId ?? report.runId;
93
+ if (runId === undefined) return undefined;
94
+ if (projection === undefined) {
95
+ io.stderr('[observer] this run was not projected (the daemon did not stream its journal); '
96
+ + 'the observer link opens an empty channel');
97
+ }
98
+ projection?.finish({
99
+ status: report.status === 'parked' ? 'parked' : report.completionReason === 'canceled' ? 'canceled'
100
+ : report.ok ? 'completed' : 'failed',
101
+ ...(report.completionReason === undefined ? {} : { completionReason: report.completionReason }),
102
+ });
103
+ const drained = projection?.close(DRAIN_GRACE_MS) ?? Promise.resolve();
104
+ return drained.then(() => mintFor(runId));
105
+ },
106
+ };
107
+ }