@relayflows/sdk 2.0.41 → 2.0.43

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 (370) hide show
  1. package/dist/adapters/base.d.ts +12 -1
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/base.js +11 -1
  4. package/dist/adapters/base.js.map +1 -1
  5. package/dist/adapters/claude.d.ts.map +1 -1
  6. package/dist/adapters/claude.js +8 -4
  7. package/dist/adapters/claude.js.map +1 -1
  8. package/dist/adapters/codex.d.ts.map +1 -1
  9. package/dist/adapters/codex.js +15 -6
  10. package/dist/adapters/codex.js.map +1 -1
  11. package/dist/agent-resume.d.ts +8 -0
  12. package/dist/agent-resume.d.ts.map +1 -0
  13. package/dist/agent-resume.js +37 -0
  14. package/dist/agent-resume.js.map +1 -0
  15. package/dist/agent-transcript.d.ts.map +1 -1
  16. package/dist/agent-transcript.js +5 -3
  17. package/dist/agent-transcript.js.map +1 -1
  18. package/dist/authored-flow-error.d.ts +10 -2
  19. package/dist/authored-flow-error.d.ts.map +1 -1
  20. package/dist/authored-flow-error.js.map +1 -1
  21. package/dist/authored-flow-executor.d.ts.map +1 -1
  22. package/dist/authored-flow-executor.js +16 -2
  23. package/dist/authored-flow-executor.js.map +1 -1
  24. package/dist/authored-flow-loader.d.ts +6 -0
  25. package/dist/authored-flow-loader.d.ts.map +1 -1
  26. package/dist/authored-flow-loader.js +18 -6
  27. package/dist/authored-flow-loader.js.map +1 -1
  28. package/dist/authored-helper-effect.js +3 -2
  29. package/dist/authored-helper-effect.js.map +1 -1
  30. package/dist/authored-human.d.ts +2 -0
  31. package/dist/authored-human.d.ts.map +1 -1
  32. package/dist/authored-human.js +4 -0
  33. package/dist/authored-human.js.map +1 -1
  34. package/dist/authored-memory.d.ts +47 -2
  35. package/dist/authored-memory.d.ts.map +1 -1
  36. package/dist/authored-memory.js +394 -15
  37. package/dist/authored-memory.js.map +1 -1
  38. package/dist/authored-node-entry.js +6 -1
  39. package/dist/authored-node-entry.js.map +1 -1
  40. package/dist/authored-node-runner.d.ts +1 -1
  41. package/dist/authored-node-runner.d.ts.map +1 -1
  42. package/dist/authored-node-runner.js +6 -3
  43. package/dist/authored-node-runner.js.map +1 -1
  44. package/dist/authored-root-readback.d.ts +21 -0
  45. package/dist/authored-root-readback.d.ts.map +1 -0
  46. package/dist/authored-root-readback.js +90 -0
  47. package/dist/authored-root-readback.js.map +1 -0
  48. package/dist/authored-root.d.ts.map +1 -1
  49. package/dist/authored-root.js +53 -74
  50. package/dist/authored-root.js.map +1 -1
  51. package/dist/authored-step-output.d.ts +2 -0
  52. package/dist/authored-step-output.d.ts.map +1 -1
  53. package/dist/authored-step-output.js +1 -0
  54. package/dist/authored-step-output.js.map +1 -1
  55. package/dist/authored-worker-step.d.ts.map +1 -1
  56. package/dist/authored-worker-step.js +26 -4
  57. package/dist/authored-worker-step.js.map +1 -1
  58. package/dist/cli/check-helper-body.d.ts +2 -2
  59. package/dist/cli/check-helper-body.d.ts.map +1 -1
  60. package/dist/cli/check-helper-body.js +7 -2
  61. package/dist/cli/check-helper-body.js.map +1 -1
  62. package/dist/cli/check-helper-surface.d.ts +8 -0
  63. package/dist/cli/check-helper-surface.d.ts.map +1 -0
  64. package/dist/cli/check-helper-surface.js +37 -0
  65. package/dist/cli/check-helper-surface.js.map +1 -0
  66. package/dist/cli/check-triggers.d.ts +4 -3
  67. package/dist/cli/check-triggers.d.ts.map +1 -1
  68. package/dist/cli/check-triggers.js +7 -3
  69. package/dist/cli/check-triggers.js.map +1 -1
  70. package/dist/cli/check.d.ts +7 -1
  71. package/dist/cli/check.d.ts.map +1 -1
  72. package/dist/cli/check.js +35 -8
  73. package/dist/cli/check.js.map +1 -1
  74. package/dist/cli/cloud-answer.d.ts +9 -0
  75. package/dist/cli/cloud-answer.d.ts.map +1 -0
  76. package/dist/cli/cloud-answer.js +39 -0
  77. package/dist/cli/cloud-answer.js.map +1 -0
  78. package/dist/cli/cloud-connect-cli.d.ts +2 -1
  79. package/dist/cli/cloud-connect-cli.d.ts.map +1 -1
  80. package/dist/cli/cloud-connect-cli.js +10 -2
  81. package/dist/cli/cloud-connect-cli.js.map +1 -1
  82. package/dist/cli/cloud-human.d.ts +7 -0
  83. package/dist/cli/cloud-human.d.ts.map +1 -0
  84. package/dist/cli/cloud-human.js +22 -0
  85. package/dist/cli/cloud-human.js.map +1 -0
  86. package/dist/cli/cloud-live.d.ts +2 -2
  87. package/dist/cli/cloud-live.d.ts.map +1 -1
  88. package/dist/cli/cloud-live.js +9 -2
  89. package/dist/cli/cloud-live.js.map +1 -1
  90. package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
  91. package/dist/cli/cloud-mirror-session.js +3 -2
  92. package/dist/cli/cloud-mirror-session.js.map +1 -1
  93. package/dist/cli/cloud-run.d.ts +1 -1
  94. package/dist/cli/cloud-run.d.ts.map +1 -1
  95. package/dist/cli/cloud-run.js +12 -0
  96. package/dist/cli/cloud-run.js.map +1 -1
  97. package/dist/cli/detached-record.d.ts +22 -0
  98. package/dist/cli/detached-record.d.ts.map +1 -0
  99. package/dist/cli/detached-record.js +57 -0
  100. package/dist/cli/detached-record.js.map +1 -0
  101. package/dist/cli/detached-run.d.ts +28 -0
  102. package/dist/cli/detached-run.d.ts.map +1 -0
  103. package/dist/cli/detached-run.js +122 -0
  104. package/dist/cli/detached-run.js.map +1 -0
  105. package/dist/cli/diagnostic-fold.d.ts +19 -0
  106. package/dist/cli/diagnostic-fold.d.ts.map +1 -0
  107. package/dist/cli/diagnostic-fold.js +19 -0
  108. package/dist/cli/diagnostic-fold.js.map +1 -0
  109. package/dist/cli/direct-run.d.ts.map +1 -1
  110. package/dist/cli/direct-run.js +16 -2
  111. package/dist/cli/direct-run.js.map +1 -1
  112. package/dist/cli/eval.d.ts +22 -0
  113. package/dist/cli/eval.d.ts.map +1 -0
  114. package/dist/cli/eval.js +131 -0
  115. package/dist/cli/eval.js.map +1 -0
  116. package/dist/cli/journal-timeout.d.ts +12 -0
  117. package/dist/cli/journal-timeout.d.ts.map +1 -0
  118. package/dist/cli/journal-timeout.js +42 -0
  119. package/dist/cli/journal-timeout.js.map +1 -0
  120. package/dist/cli/local-agent-remedy.d.ts.map +1 -1
  121. package/dist/cli/local-agent-remedy.js +5 -4
  122. package/dist/cli/local-agent-remedy.js.map +1 -1
  123. package/dist/cli/observer-session.d.ts +1 -0
  124. package/dist/cli/observer-session.d.ts.map +1 -1
  125. package/dist/cli/observer-session.js +8 -1
  126. package/dist/cli/observer-session.js.map +1 -1
  127. package/dist/cli/reuse.d.ts +8 -0
  128. package/dist/cli/reuse.d.ts.map +1 -1
  129. package/dist/cli/reuse.js +24 -0
  130. package/dist/cli/reuse.js.map +1 -1
  131. package/dist/cli/run.d.ts +8 -2
  132. package/dist/cli/run.d.ts.map +1 -1
  133. package/dist/cli/run.js +72 -56
  134. package/dist/cli/run.js.map +1 -1
  135. package/dist/cli/running-step.d.ts +9 -0
  136. package/dist/cli/running-step.d.ts.map +1 -0
  137. package/dist/cli/running-step.js +156 -0
  138. package/dist/cli/running-step.js.map +1 -0
  139. package/dist/cli-adapter.d.ts +3 -1
  140. package/dist/cli-adapter.d.ts.map +1 -1
  141. package/dist/cli-adapter.js +6 -2
  142. package/dist/cli-adapter.js.map +1 -1
  143. package/dist/cli-commands.d.ts +77 -11
  144. package/dist/cli-commands.d.ts.map +1 -1
  145. package/dist/cli-commands.js +41 -7
  146. package/dist/cli-commands.js.map +1 -1
  147. package/dist/cli-watch.d.ts +6 -2
  148. package/dist/cli-watch.d.ts.map +1 -1
  149. package/dist/cli-watch.js +7 -6
  150. package/dist/cli-watch.js.map +1 -1
  151. package/dist/cli.d.ts +22 -1
  152. package/dist/cli.d.ts.map +1 -1
  153. package/dist/cli.js +154 -50
  154. package/dist/cli.js.map +1 -1
  155. package/dist/cloud-answer.d.ts +35 -0
  156. package/dist/cloud-answer.d.ts.map +1 -0
  157. package/dist/cloud-answer.js +194 -0
  158. package/dist/cloud-answer.js.map +1 -0
  159. package/dist/cloud-auth-store.d.ts +46 -0
  160. package/dist/cloud-auth-store.d.ts.map +1 -0
  161. package/dist/cloud-auth-store.js +171 -0
  162. package/dist/cloud-auth-store.js.map +1 -0
  163. package/dist/cloud-http.d.ts +7 -16
  164. package/dist/cloud-http.d.ts.map +1 -1
  165. package/dist/cloud-http.js +59 -40
  166. package/dist/cloud-http.js.map +1 -1
  167. package/dist/cloud-human-wait.d.ts +10 -0
  168. package/dist/cloud-human-wait.d.ts.map +1 -0
  169. package/dist/cloud-human-wait.js +17 -0
  170. package/dist/cloud-human-wait.js.map +1 -0
  171. package/dist/cloud-mirror-transport.d.ts.map +1 -1
  172. package/dist/cloud-mirror-transport.js +1 -0
  173. package/dist/cloud-mirror-transport.js.map +1 -1
  174. package/dist/cloud-run-record.d.ts +7 -2
  175. package/dist/cloud-run-record.d.ts.map +1 -1
  176. package/dist/cloud-run-record.js +13 -5
  177. package/dist/cloud-run-record.js.map +1 -1
  178. package/dist/cloud-run.d.ts +9 -0
  179. package/dist/cloud-run.d.ts.map +1 -1
  180. package/dist/cloud-run.js +36 -2
  181. package/dist/cloud-run.js.map +1 -1
  182. package/dist/communication/local.d.ts.map +1 -1
  183. package/dist/communication/local.js +6 -1
  184. package/dist/communication/local.js.map +1 -1
  185. package/dist/communication/worker.d.ts.map +1 -1
  186. package/dist/communication/worker.js +12 -2
  187. package/dist/communication/worker.js.map +1 -1
  188. package/dist/compile.d.ts.map +1 -1
  189. package/dist/compile.js +5 -3
  190. package/dist/compile.js.map +1 -1
  191. package/dist/daemon-spec-validation.d.ts +31 -0
  192. package/dist/daemon-spec-validation.d.ts.map +1 -0
  193. package/dist/daemon-spec-validation.js +115 -0
  194. package/dist/daemon-spec-validation.js.map +1 -0
  195. package/dist/failure-kinds.d.ts +6 -4
  196. package/dist/failure-kinds.d.ts.map +1 -1
  197. package/dist/failure-kinds.js +17 -1
  198. package/dist/failure-kinds.js.map +1 -1
  199. package/dist/flow-eval-cloud.d.ts +25 -0
  200. package/dist/flow-eval-cloud.d.ts.map +1 -0
  201. package/dist/flow-eval-cloud.js +108 -0
  202. package/dist/flow-eval-cloud.js.map +1 -0
  203. package/dist/flow-eval-error.d.ts +7 -0
  204. package/dist/flow-eval-error.d.ts.map +1 -0
  205. package/dist/flow-eval-error.js +10 -0
  206. package/dist/flow-eval-error.js.map +1 -0
  207. package/dist/flow-eval-gate.d.ts +15 -0
  208. package/dist/flow-eval-gate.d.ts.map +1 -0
  209. package/dist/flow-eval-gate.js +102 -0
  210. package/dist/flow-eval-gate.js.map +1 -0
  211. package/dist/flow-eval-local.d.ts +54 -0
  212. package/dist/flow-eval-local.d.ts.map +1 -0
  213. package/dist/flow-eval-local.js +220 -0
  214. package/dist/flow-eval-local.js.map +1 -0
  215. package/dist/flow-eval-outcome.d.ts +22 -0
  216. package/dist/flow-eval-outcome.d.ts.map +1 -0
  217. package/dist/flow-eval-outcome.js +37 -0
  218. package/dist/flow-eval-outcome.js.map +1 -0
  219. package/dist/flow-eval-report.d.ts +106 -0
  220. package/dist/flow-eval-report.d.ts.map +1 -0
  221. package/dist/flow-eval-report.js +4 -0
  222. package/dist/flow-eval-report.js.map +1 -0
  223. package/dist/flow-eval-suite.d.ts +46 -0
  224. package/dist/flow-eval-suite.d.ts.map +1 -0
  225. package/dist/flow-eval-suite.js +144 -0
  226. package/dist/flow-eval-suite.js.map +1 -0
  227. package/dist/flow-eval-version.d.ts +59 -0
  228. package/dist/flow-eval-version.d.ts.map +1 -0
  229. package/dist/flow-eval-version.js +303 -0
  230. package/dist/flow-eval-version.js.map +1 -0
  231. package/dist/flow-eval.d.ts +41 -0
  232. package/dist/flow-eval.d.ts.map +1 -0
  233. package/dist/flow-eval.js +201 -0
  234. package/dist/flow-eval.js.map +1 -0
  235. package/dist/helper-operation-use.d.ts +18 -0
  236. package/dist/helper-operation-use.d.ts.map +1 -0
  237. package/dist/helper-operation-use.js +336 -0
  238. package/dist/helper-operation-use.js.map +1 -0
  239. package/dist/helper-preflight.d.ts.map +1 -1
  240. package/dist/helper-preflight.js +16 -2
  241. package/dist/helper-preflight.js.map +1 -1
  242. package/dist/helper-reference.d.ts +33 -0
  243. package/dist/helper-reference.d.ts.map +1 -1
  244. package/dist/helper-reference.js +5 -5
  245. package/dist/helper-reference.js.map +1 -1
  246. package/dist/index.d.ts +6 -1
  247. package/dist/index.d.ts.map +1 -1
  248. package/dist/index.js +5 -1
  249. package/dist/index.js.map +1 -1
  250. package/dist/journal-budgeted-reads.d.ts +41 -0
  251. package/dist/journal-budgeted-reads.d.ts.map +1 -0
  252. package/dist/journal-budgeted-reads.js +142 -0
  253. package/dist/journal-budgeted-reads.js.map +1 -0
  254. package/dist/journal-client.d.ts +34 -29
  255. package/dist/journal-client.d.ts.map +1 -1
  256. package/dist/journal-client.js +45 -151
  257. package/dist/journal-client.js.map +1 -1
  258. package/dist/journal-connection.d.ts +42 -0
  259. package/dist/journal-connection.d.ts.map +1 -0
  260. package/dist/journal-connection.js +209 -0
  261. package/dist/journal-connection.js.map +1 -0
  262. package/dist/journal-read-policy.d.ts +38 -0
  263. package/dist/journal-read-policy.d.ts.map +1 -0
  264. package/dist/journal-read-policy.js +126 -0
  265. package/dist/journal-read-policy.js.map +1 -0
  266. package/dist/named-gate-lowering.d.ts.map +1 -1
  267. package/dist/named-gate-lowering.js +10 -7
  268. package/dist/named-gate-lowering.js.map +1 -1
  269. package/dist/re2js-embedded.d.ts +3 -0
  270. package/dist/re2js-embedded.d.ts.map +1 -0
  271. package/dist/re2js-embedded.js +4 -0
  272. package/dist/re2js-embedded.js.map +1 -0
  273. package/dist/spec.d.ts +10 -0
  274. package/dist/spec.d.ts.map +1 -1
  275. package/dist/spec.js.map +1 -1
  276. package/dist/step-fields.d.ts +1 -1
  277. package/dist/step-fields.js +1 -1
  278. package/dist/step-fields.js.map +1 -1
  279. package/dist/validate.d.ts.map +1 -1
  280. package/dist/validate.js +15 -0
  281. package/dist/validate.js.map +1 -1
  282. package/dist/worker-cli.d.ts +3 -1
  283. package/dist/worker-cli.d.ts.map +1 -1
  284. package/dist/worker-cli.js +15 -5
  285. package/dist/worker-cli.js.map +1 -1
  286. package/dist/worker-lease.d.ts.map +1 -1
  287. package/dist/worker-lease.js +11 -3
  288. package/dist/worker-lease.js.map +1 -1
  289. package/dist/worker.js +1 -1
  290. package/dist/worker.js.map +1 -1
  291. package/package.json +5 -3
  292. package/src/adapters/base.ts +16 -1
  293. package/src/adapters/claude.ts +8 -4
  294. package/src/adapters/codex.ts +15 -6
  295. package/src/agent-resume.ts +36 -0
  296. package/src/agent-transcript.ts +5 -3
  297. package/src/authored-flow-error.ts +9 -0
  298. package/src/authored-flow-executor.ts +18 -1
  299. package/src/authored-flow-loader.ts +27 -10
  300. package/src/authored-helper-effect.ts +3 -2
  301. package/src/authored-human.ts +5 -0
  302. package/src/authored-memory.ts +445 -15
  303. package/src/authored-node-entry.ts +6 -1
  304. package/src/authored-node-runner.ts +6 -3
  305. package/src/authored-root-readback.ts +100 -0
  306. package/src/authored-root.ts +58 -85
  307. package/src/authored-step-output.ts +3 -0
  308. package/src/authored-worker-step.ts +29 -4
  309. package/src/cli/check-helper-body.ts +9 -3
  310. package/src/cli/check-helper-surface.ts +43 -0
  311. package/src/cli/check-triggers.ts +9 -4
  312. package/src/cli/check.ts +37 -7
  313. package/src/cli/cloud-answer.ts +40 -0
  314. package/src/cli/cloud-connect-cli.ts +9 -2
  315. package/src/cli/cloud-human.ts +24 -0
  316. package/src/cli/cloud-live.ts +10 -5
  317. package/src/cli/cloud-mirror-session.ts +3 -2
  318. package/src/cli/cloud-run.ts +10 -1
  319. package/src/cli/detached-record.ts +71 -0
  320. package/src/cli/detached-run.ts +131 -0
  321. package/src/cli/diagnostic-fold.ts +28 -0
  322. package/src/cli/direct-run.ts +14 -1
  323. package/src/cli/eval.ts +141 -0
  324. package/src/cli/journal-timeout.ts +49 -0
  325. package/src/cli/local-agent-remedy.ts +6 -4
  326. package/src/cli/observer-session.ts +7 -1
  327. package/src/cli/reuse.ts +25 -0
  328. package/src/cli/run.ts +75 -67
  329. package/src/cli/running-step.ts +140 -0
  330. package/src/cli-adapter.ts +7 -1
  331. package/src/cli-commands.ts +43 -7
  332. package/src/cli-watch.ts +8 -5
  333. package/src/cli.ts +158 -51
  334. package/src/cloud-answer.ts +202 -0
  335. package/src/cloud-auth-store.ts +185 -0
  336. package/src/cloud-http.ts +61 -50
  337. package/src/cloud-human-wait.ts +23 -0
  338. package/src/cloud-mirror-transport.ts +1 -0
  339. package/src/cloud-run-record.ts +14 -5
  340. package/src/cloud-run.ts +46 -2
  341. package/src/communication/local.ts +6 -1
  342. package/src/communication/worker.ts +11 -3
  343. package/src/compile.ts +5 -3
  344. package/src/daemon-spec-validation.ts +126 -0
  345. package/src/failure-kinds.ts +17 -1
  346. package/src/flow-eval-cloud.ts +114 -0
  347. package/src/flow-eval-error.ts +10 -0
  348. package/src/flow-eval-gate.ts +114 -0
  349. package/src/flow-eval-local.ts +236 -0
  350. package/src/flow-eval-outcome.ts +43 -0
  351. package/src/flow-eval-report.ts +102 -0
  352. package/src/flow-eval-suite.ts +169 -0
  353. package/src/flow-eval-version.ts +316 -0
  354. package/src/flow-eval.ts +234 -0
  355. package/src/helper-operation-use.ts +285 -0
  356. package/src/helper-preflight.ts +17 -2
  357. package/src/helper-reference.ts +6 -6
  358. package/src/index.ts +15 -1
  359. package/src/journal-budgeted-reads.ts +134 -0
  360. package/src/journal-client.ts +59 -166
  361. package/src/journal-connection.ts +222 -0
  362. package/src/journal-read-policy.ts +123 -0
  363. package/src/named-gate-lowering.ts +10 -7
  364. package/src/re2js-embedded.ts +3 -0
  365. package/src/spec.ts +10 -0
  366. package/src/step-fields.ts +1 -1
  367. package/src/validate.ts +14 -0
  368. package/src/worker-cli.ts +14 -3
  369. package/src/worker-lease.ts +11 -3
  370. package/src/worker.ts +1 -1
package/src/cli/reuse.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { JournalClient } from '../journal-client.js';
2
+ import type { RunExecution } from './run.js';
2
3
 
3
4
  /** Count durable facts, including failed executions, across paginated reads. */
4
5
  export async function reuseSummary(client: JournalClient, runId: string, fromRunId: string): Promise<{
@@ -21,3 +22,27 @@ export async function reuseSummary(client: JournalClient, runId: string, fromRun
21
22
  }
22
23
  return { fromRunId, reusedSteps: reused.size, executedSteps: executed.size };
23
24
  }
25
+
26
+ /**
27
+ * Add the reuse summary to an already classified run. The summary is an extra
28
+ * journal read after the outcome is known: if it cannot be answered, report
29
+ * that as a warning and keep the classified outcome rather than recasting a
30
+ * finished run as resumable or failed.
31
+ */
32
+ export async function attachReuseSummary(execution: RunExecution, client: JournalClient,
33
+ runId: string, fromRunId: string): Promise<RunExecution> {
34
+ // Classification already ended on an unanswered read: the run is resumable and
35
+ // another read would only spend a fresh budget against the same delayed daemon.
36
+ if (execution.report.diagnostics.some(diagnostic => diagnostic.kind === 'daemon_unresponsive'
37
+ || diagnostic.kind === 'daemon_unreachable' || diagnostic.kind === 'inspection_interrupted')) return execution;
38
+ try {
39
+ execution.report.reuse = await reuseSummary(client, runId, fromRunId);
40
+ } catch (error) {
41
+ // Optional detail after the outcome is decided: no failure here — an
42
+ // interruption, a cancellation, a bad frame — may replace that outcome.
43
+ const why = error instanceof Error ? error.message : String(error);
44
+ execution.report.diagnostics = [...execution.report.diagnostics, { severity: 'warning', kind: 'reuse_summary_unavailable',
45
+ message: `The run's outcome is final, but the summary of steps reused from ${fromRunId} could not be read: ${why}` }];
46
+ }
47
+ return execution;
48
+ }
package/src/cli/run.ts CHANGED
@@ -1,10 +1,14 @@
1
+ import { waitForRunningStep, type RunningStep } from './running-step.js';
2
+ export { LEASE_SWEEP_GRACE_MS } from './running-step.js';
3
+ import { completedResultUnreadableReport, daemonUnresponsiveReport, isReadInterruption } from './journal-timeout.js';
4
+ import { FLOW_READ_BUDGET_MS } from '../journal-read-policy.js';
1
5
  import { onWorkerFailure } from '../worker-lease.js';
2
6
  import { communicationInstruction } from '../communication/spec.js';
3
7
  import { checkCommunicationEnvironment, CommunicationEnvironmentError } from '../communication/preflight.js';
4
8
  import { parseHumanRecipient } from '../human-to.js';
5
9
  import { parseDigestReference } from '../bundle-transport.js';
6
10
  import { prepareDigestRun } from './run-digest.js';
7
- import { reuseSummary } from './reuse.js';
11
+ import { attachReuseSummary } from './reuse.js';
8
12
  import { resumeHelperEffect } from '../authored-helper-effect.js';
9
13
  import { AuthoredFlowExecutionError, AuthoredHumanParked, type AuthoredHumanWait } from '../authored-flow-error.js';
10
14
  import { answerCommand, resumeCommand } from '../authored-human.js';
@@ -149,7 +153,7 @@ export interface RunLifecycleOptions {
149
153
  onJournalEntry?: (entry: JournalEvent) => void;
150
154
  /** An authored root was admitted: its id is known before its body runs. */
151
155
  onRunStarted?: (run: { runId: string; flow: string; resumed?: boolean }) => void;
152
- /** A declarative root returned its admission receipt, even if no watched entry arrived. */
156
+ /** The daemon admitted a root (declarative or authored, even an already completed one). */
153
157
  onRunReceipt?: (run: { runId: string; flow: string }) => void;
154
158
  localAgent?: boolean;
155
159
  /** `--agent-capacity`: the local workers' concurrency; the default is `DEFAULT_LOCAL_AGENT_CAPACITY`. */
@@ -203,7 +207,8 @@ async function executeCheckedFlow(
203
207
  catch (error) { return { exitCode: 2, report: { ...base, diagnostics: [...base.diagnostics,
204
208
  { severity: 'refusal', kind: 'probe_failed', message: error instanceof Error ? error.message : 'Communication environment could not be checked.' }] } }; }
205
209
  }
206
- const client = new JournalClient(socketPath);
210
+ const client = new JournalClient(socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS,
211
+ ...(options.signal === undefined ? {} : { readSignal: options.signal }) });
207
212
  const connected = await connect(client, 'run', dataDir, base, options);
208
213
  if (connected !== undefined) return connected;
209
214
 
@@ -225,6 +230,7 @@ async function executeCheckedFlow(
225
230
  }
226
231
  if (options.onJournalEntry !== undefined) client.on('entry', options.onJournalEntry);
227
232
  const outcome = await startWatched(client, spec, options);
233
+ base.runId = outcome.run_id;
228
234
  // `run.start { watch: true }` is an event stream, not the source of the
229
235
  // root identity. A short deterministic run can finish before its first
230
236
  // watched entry is delivered (and an older daemon may refuse `watch`), so
@@ -234,11 +240,10 @@ async function executeCheckedFlow(
234
240
  // truthful "not projected" diagnostic.
235
241
  options.onRunReceipt?.({ runId: outcome.run_id, flow: spec.name ?? 'flow' });
236
242
  const execution = await classifyOutcome(client, 'run', outcome, base, socketPath, { ...options, dataDir });
237
- if (options.reuseFromRunId !== undefined) {
238
- execution.report.reuse = await reuseSummary(client, outcome.run_id, options.reuseFromRunId);
239
- }
240
- return execution;
243
+ return options.reuseFromRunId === undefined ? execution
244
+ : await attachReuseSummary(execution, client, outcome.run_id, options.reuseFromRunId);
241
245
  } catch (error) {
246
+ if (isReadInterruption(error)) return daemonUnresponsiveReport('run', base, socketPath, error, base.runId, options, dataDir);
242
247
  if (error instanceof CommunicationEnvironmentError) return { exitCode: 2, report: { ...base,
243
248
  diagnostics: [...base.diagnostics, { severity: 'refusal', kind: 'probe_failed', message: error.message }] } };
244
249
  if (error instanceof JournalProtocolError && (
@@ -289,7 +294,8 @@ export async function resumeFlow(
289
294
  const agentEnvironment = localAgentEnvironment();
290
295
  const socketPath = socketFor(dataDir);
291
296
  const base = emptyReport('resume');
292
- const client = new JournalClient(socketPath);
297
+ const client = new JournalClient(socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS,
298
+ ...(options.signal === undefined ? {} : { readSignal: options.signal }) });
293
299
  const connected = await connect(client, 'resume', dataDir, base, options);
294
300
  if (connected !== undefined) return connected;
295
301
 
@@ -379,6 +385,7 @@ export async function resumeFlow(
379
385
  await client.runWatch(runId).catch(() => client.off('entry', onEntry));
380
386
  }
381
387
  let outcome = await client.runResume(runId, options.allowHumanInfluenced);
388
+ options.onRunReceipt?.({ runId, flow: 'flow' });
382
389
  if (await resumeHelperEffect(client, runId, dataDir)) {
383
390
  outcome = await client.runResume(runId, options.allowHumanInfluenced);
384
391
  }
@@ -408,6 +415,14 @@ export async function resumeFlow(
408
415
  if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
409
416
  return authoredStepFailure('resume', base, socketPath, error, runId);
410
417
  }
418
+ if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
419
+ return completedResultUnreadableReport('resume', base, socketPath, error);
420
+ }
421
+ if (error instanceof AuthoredFlowExecutionError) {
422
+ const memoryFailure = memoryWriteFailure('resume', base, socketPath, error, runId);
423
+ if (memoryFailure !== undefined) return memoryFailure;
424
+ }
425
+ if (isReadInterruption(error)) return daemonUnresponsiveReport('resume', base, socketPath, error, runId, options, dataDir);
411
426
  if (error instanceof AuthoredFlowExecutionError && error.code === 'root_lease_lost') {
412
427
  return rootLeaseLostReport('resume', base, socketPath, error, runId);
413
428
  }
@@ -822,6 +837,19 @@ export async function connect(
822
837
  /// needs a stubbed client rather than a real run -- the integration test that
823
838
  /// found it reproduced the bug roughly 1 time in 12.
824
839
  export async function classifyOutcome(
840
+ client: JournalClient, command: RunCommand, outcome: RunOutcome,
841
+ base: CheckReport | RunReport, socketPath: string, options: RunLifecycleOptions,
842
+ ): Promise<RunExecution> {
843
+ try { return await classifyOutcomeInner(client, command, outcome, base, socketPath, options); }
844
+ catch (error) {
845
+ // Authored child calls have no CLI socket path: let their durable root
846
+ // handle the interruption, rather than turn it into a child step failure.
847
+ if (socketPath !== '' && isReadInterruption(error)) return daemonUnresponsiveReport(command, base, socketPath, error, outcome.run_id, options);
848
+ throw error;
849
+ }
850
+ }
851
+
852
+ async function classifyOutcomeInner(
825
853
  client: JournalClient,
826
854
  command: RunCommand,
827
855
  outcome: RunOutcome,
@@ -929,6 +957,7 @@ export async function classifyOutcome(
929
957
  kind: current.completion_reason,
930
958
  message: `Run "${current.run_id}" failed with completionReason: ${current.completion_reason}.`,
931
959
  };
960
+ let interrupted: RunDiagnostic | undefined;
932
961
  if (current.completion_reason === 'step_failed') {
933
962
  let details: StepFailedDetails | undefined;
934
963
  try {
@@ -936,6 +965,10 @@ export async function classifyOutcome(
936
965
  } catch (error) {
937
966
  // Inspection must not erase the already known run failure.
938
967
  diagnostic.message += ` Could not inspect the failed step: ${errorMessage(error)}`;
968
+ // Mark an unanswered read so optional follow-up reads do not spend a
969
+ // fresh budget on the same delayed daemon.
970
+ if (isReadInterruption(error)) interrupted = { severity: 'warning', kind: 'inspection_interrupted',
971
+ message: `The failed step could not be inspected: ${errorMessage(error)}` };
939
972
  }
940
973
  if (details !== undefined) {
941
974
  Object.assign(diagnostic, details);
@@ -952,7 +985,7 @@ export async function classifyOutcome(
952
985
  exitCode: 1,
953
986
  report: {
954
987
  ...report,
955
- diagnostics: [...report.diagnostics, diagnostic],
988
+ diagnostics: [...report.diagnostics, diagnostic, ...(interrupted === undefined ? [] : [interrupted])],
956
989
  },
957
990
  };
958
991
  }
@@ -1021,10 +1054,6 @@ interface OutOfBandInspection {
1021
1054
  backoffStep?: ParkedStep;
1022
1055
  }
1023
1056
 
1024
- interface RunningStep extends ParkedStep {
1025
- leaseDeadlineMs: number;
1026
- }
1027
-
1028
1057
  // Bound on re-polling a run that reports `running` with no identifiable step.
1029
1058
  // 40 x 50ms = 2s, far longer than the sub-second window observed in #179, and
1030
1059
  // short enough that a genuinely stuck run still reports rather than hangs.
@@ -1071,62 +1100,41 @@ async function inspectOutOfBandStep(
1071
1100
  };
1072
1101
  }
1073
1102
 
1074
- // Match kernel/relayflowd/src/server/client.rs: allow the lease sweep to dispatch a retry.
1075
- export const LEASE_SWEEP_GRACE_MS = 5_000;
1076
- // How often to re-ask the daemon about a step whose lease looks expired here.
1077
1103
  const EXPIRED_LEASE_POLL_MS = 250;
1078
- // A deadline unchanged for a whole lease (relayflowd LEASE_RENEWAL_MS, 30s)
1079
- // plus the sweep grace was renewed by nobody: the attempt is dead and the
1080
- // daemon never swept it. Fail rather than hang.
1081
- const STALE_LEASE_MS = 30_000 + LEASE_SWEEP_GRACE_MS;
1082
1104
 
1083
- async function waitForRunningStep(
1084
- client: JournalClient,
1085
- runId: string,
1086
- runningStep: RunningStep,
1087
- options: RunLifecycleOptions,
1088
- ): Promise<void> {
1089
- let leaseDeadlineMs = runningStep.leaseDeadlineMs;
1090
- if (!Number.isFinite(leaseDeadlineMs)) {
1091
- throw new Error(`running step "${runningStep.id}" omitted lease_deadline_ms`);
1092
- }
1093
- options.onWait?.({
1094
- runId,
1095
- stepId: runningStep.id,
1096
- stepType: runningStep.type,
1097
- leaseDeadlineMs,
1098
- });
1099
- // When the lease deadline last changed, on this process's monotonic clock.
1100
- // A live worker renews every ~10s, moving the deadline; a dead lease the
1101
- // daemon has not swept keeps it. That -- not a comparison of the daemon's
1102
- // deadline with our wall clock -- is what separates the two under skew.
1103
- let deadlineSeenAt = performance.now();
1104
- while (true) {
1105
- throwIfCanceled(options.signal, runningStep.id);
1106
- const remainingMs = leaseDeadlineMs + LEASE_SWEEP_GRACE_MS - Date.now();
1107
- if (performance.now() - deadlineSeenAt > STALE_LEASE_MS) {
1108
- throw new Error(
1109
- `worker lease for step "${runningStep.id}" expired at ${leaseDeadlineMs} without completion`,
1110
- );
1111
- }
1112
- // Past the deadline by THIS clock alone is not an expiry: the daemon may
1113
- // still hold the lease (skew) or be sweeping it into a retry. Keep
1114
- // following the step, at a slower poll, until the daemon says otherwise.
1115
- await delay(remainingMs <= 0 ? EXPIRED_LEASE_POLL_MS : Math.min(50, remainingMs), options.signal);
1116
- const snapshot = await client.runGet(runId);
1117
- const step = snapshot.steps[runningStep.id];
1118
- if (step?.state !== 'running') return;
1119
- if (step.lease_deadline_ms !== undefined && step.lease_deadline_ms !== leaseDeadlineMs) {
1120
- leaseDeadlineMs = step.lease_deadline_ms;
1121
- deadlineSeenAt = performance.now();
1122
- options.onWait?.({
1123
- runId,
1124
- stepId: runningStep.id,
1125
- stepType: runningStep.type,
1126
- leaseDeadlineMs,
1127
- });
1128
- }
1129
- }
1105
+ /**
1106
+ * `f.memory` failures keep their own kind instead of collapsing into
1107
+ * `protocol_error`. By then the root has been terminalized as failed, so the
1108
+ * report names it the way a step failure does.
1109
+ */
1110
+ export function memoryWriteFailure(
1111
+ command: RunCommand,
1112
+ base: CheckReport | RunReport,
1113
+ socketPath: string,
1114
+ error: AuthoredFlowExecutionError,
1115
+ fallbackRunId?: string,
1116
+ ): RunExecution | undefined {
1117
+ if (error.code !== 'memory_finding_invalid' && error.code !== 'memory_unwritable'
1118
+ && error.code !== 'memory_unjournaled' && error.code !== 'memory_unreachable') return undefined;
1119
+ const rootRunId = error.rootRunId ?? fallbackRunId;
1120
+ const runId = error.runId ?? rootRunId;
1121
+ return {
1122
+ exitCode: 1,
1123
+ report: {
1124
+ ...fromBase(command, base),
1125
+ ok: false,
1126
+ ...(runId === undefined ? {} : { runId }),
1127
+ ...(rootRunId === undefined ? {} : { rootRunId }),
1128
+ socketPath,
1129
+ status: 'failed',
1130
+ completionReason: 'step_failed',
1131
+ diagnostics: [...base.diagnostics, {
1132
+ severity: 'failure', kind: error.code,
1133
+ // The `<code>: ` prefix `AuthoredFlowExecutionError` adds is redundant once the diagnostic is labelled.
1134
+ message: error.message.startsWith(`${error.code}: `) ? error.message.slice(error.code.length + 2) : error.message,
1135
+ }],
1136
+ },
1137
+ };
1130
1138
  }
1131
1139
 
1132
1140
  export function protocolFailure(
@@ -0,0 +1,140 @@
1
+ import { JournalFrameError, JournalProtocolError, type JournalClient } from '../journal-client.js';
2
+ import { AuthoredFlowExecutionError } from '../authored-flow-error.js';
3
+ import type { JournalEvent } from '../journal-reader.js';
4
+ import type { RunLifecycleOptions, ParkedStep } from './run.js';
5
+
6
+ export const LEASE_SWEEP_GRACE_MS = 5_000;
7
+ const LEASE_POLL_MS = 2_000;
8
+ const STALE_LEASE_MS = 30_000 + LEASE_SWEEP_GRACE_MS;
9
+ export interface RunningStep extends ParkedStep { leaseDeadlineMs: number }
10
+
11
+ /** Watch completion; snapshots only refresh the non-journaled live lease deadline. */
12
+ export async function waitForRunningStep(client: JournalClient, runId: string,
13
+ runningStep: RunningStep, options: RunLifecycleOptions): Promise<void> {
14
+ let leaseDeadlineMs = runningStep.leaseDeadlineMs;
15
+ if (!Number.isFinite(leaseDeadlineMs)) {
16
+ throw new Error(`running step "${runningStep.id}" omitted lease_deadline_ms`);
17
+ }
18
+ const notify = () => options.onWait?.({ runId, stepId: runningStep.id,
19
+ stepType: runningStep.type, leaseDeadlineMs });
20
+ notify();
21
+ let deadlineSeenAt = performance.now();
22
+ // The protocol has no unwatch. Closing this scoped, non-worker session removes
23
+ // the server watcher, even on cancellation or a read/registration failure.
24
+ const watch = client.createPeer();
25
+ let settled = false;
26
+ let ready = false;
27
+ let finish: () => void = () => {};
28
+ let cancel: () => void = () => {};
29
+ let fail: (error: unknown) => void = () => {};
30
+ const canceled = () => new Error(`worker wait for step "${runningStep.id}" was canceled`);
31
+ const completed = new Promise<void>((resolve, reject) => {
32
+ finish = resolve;
33
+ cancel = () => reject(canceled());
34
+ fail = reject;
35
+ });
36
+ // The subscription handshake itself may be pending when cancellation arrives.
37
+ void completed.catch(() => {});
38
+ // A completion push aborts the lease snapshot in flight: the answer is known.
39
+ let snapshotRead: AbortController | undefined;
40
+ const onEntry = (entry: JournalEvent) => {
41
+ if (entry.run_id !== runId) return;
42
+ if (entry.entry_type === 'run.completed') settled = true;
43
+ else if (entry.step_id === runningStep.id) {
44
+ if (entry.entry_type === 'step.attempt.started') settled = false;
45
+ if (entry.entry_type === 'step.completed' || entry.entry_type === 'wait.human') settled = true;
46
+ }
47
+ // Fold the entire replay before deciding: an older attempt may have failed.
48
+ if (ready && settled) {
49
+ finish();
50
+ snapshotRead?.abort(new Error(`step "${runningStep.id}" completed during the lease snapshot`));
51
+ }
52
+ };
53
+ watch.on('entry', onEntry);
54
+ // A malformed frame after registration has no pending request to reject: fail closed here.
55
+ let protocolFailure: unknown;
56
+ const onProtocolError = (error: unknown) => { protocolFailure ??= error; fail(error); snapshotRead?.abort(error); };
57
+ watch.on('protocol_error', onProtocolError);
58
+ // A dropped watch loses its completion pushes: a resumable read interruption.
59
+ const onDisconnected = (error: Error) => {
60
+ if (settled) return;
61
+ const lost = new AuthoredFlowExecutionError('daemon_unresponsive',
62
+ `the completion watch for step "${runningStep.id}" disconnected: ${error.message}`);
63
+ protocolFailure ??= lost;
64
+ fail(lost);
65
+ snapshotRead?.abort(lost);
66
+ };
67
+ watch.on('disconnected', onDisconnected);
68
+ options.signal?.addEventListener('abort', cancel, { once: true });
69
+ if (options.signal?.aborted) cancel();
70
+ try {
71
+ // Every transport failure up to and including the watch registration is a
72
+ // read interruption: the step may still be running and the run resumable.
73
+ let unconfirmed: unknown;
74
+ await Promise.race([watch.connect().then(() => watch.hello('flows-step-watch'))
75
+ .then(() => watch.runWatch(runId)).catch(error => {
76
+ // The replay precedes the response, but a drop may have truncated it
77
+ // (an older completion, the retry not yet seen): confirm below.
78
+ if (settled) { unconfirmed = error; return; }
79
+ // Refusals and malformed frames are protocol facts, not lost transport: fail closed.
80
+ if (error instanceof JournalProtocolError || error instanceof JournalFrameError) throw error;
81
+ throw new AuthoredFlowExecutionError('daemon_unresponsive',
82
+ `could not establish the completion watch: ${error instanceof Error ? error.message : String(error)}`);
83
+ }), completed]);
84
+ if (unconfirmed !== undefined) {
85
+ const snapshot = await client.runGet(runId, options.signal === undefined ? {} : { signal: options.signal });
86
+ if (snapshot.steps[runningStep.id]?.state !== 'running') return;
87
+ throw new AuthoredFlowExecutionError('daemon_unresponsive',
88
+ `could not establish the completion watch: ${unconfirmed instanceof Error ? unconfirmed.message : String(unconfirmed)}`);
89
+ }
90
+ ready = true;
91
+ if (settled) return;
92
+ while (true) {
93
+ let timer: ReturnType<typeof setTimeout> | undefined;
94
+ let releaseTimer: () => void = () => {};
95
+ try {
96
+ const elapsed = await Promise.race([
97
+ new Promise<boolean>(resolve => { releaseTimer = () => resolve(false); timer = setTimeout(() => resolve(true), LEASE_POLL_MS); }),
98
+ completed.then(() => false),
99
+ ]);
100
+ if (!elapsed) return;
101
+ } finally { clearTimeout(timer); releaseTimer(); }
102
+ // Drain the read before leaving the authored promise scope. A raced but
103
+ // unresolved read would look like unawaited derived work to that scope,
104
+ // so cancellation aborts the read itself and it settles at once.
105
+ const read = snapshotRead = new AbortController();
106
+ const signal = options.signal === undefined ? read.signal
107
+ : AbortSignal.any([options.signal, read.signal]);
108
+ const snapshot = await client.runGet(runId, { signal })
109
+ .catch(error => {
110
+ if (options.signal?.aborted) throw canceled();
111
+ if (protocolFailure !== undefined) throw protocolFailure;
112
+ // A completion push aborted this read: completion was observed, and
113
+ // finish() is one-shot, even if a retry attempt started right after.
114
+ if (settled || read.signal.aborted) return undefined;
115
+ throw error;
116
+ })
117
+ .finally(() => { snapshotRead = undefined; });
118
+ if (options.signal?.aborted) throw canceled();
119
+ if (protocolFailure !== undefined) throw protocolFailure;
120
+ if (settled || snapshot === undefined || snapshot.steps[runningStep.id]?.state !== 'running') return;
121
+ const deadline = snapshot.steps[runningStep.id]?.lease_deadline_ms;
122
+ if (deadline !== undefined && deadline !== leaseDeadlineMs) {
123
+ leaseDeadlineMs = deadline;
124
+ deadlineSeenAt = performance.now();
125
+ notify();
126
+ } else if (performance.now() - deadlineSeenAt > STALE_LEASE_MS) {
127
+ // A stalled sweep/renewal is not evidence that the body itself failed.
128
+ throw new AuthoredFlowExecutionError('daemon_unresponsive',
129
+ `worker lease for step "${runningStep.id}" expired at ${leaseDeadlineMs} without completion; relayflowd's lease sweep may be delayed by CPU load`);
130
+ }
131
+ }
132
+ } finally {
133
+ finish();
134
+ options.signal?.removeEventListener('abort', cancel);
135
+ watch.off('entry', onEntry);
136
+ watch.off('protocol_error', onProtocolError);
137
+ watch.off('disconnected', onDisconnected);
138
+ watch.close();
139
+ }
140
+ }
@@ -84,8 +84,14 @@ export function agentExecution(
84
84
  kind: CliAdapterKind,
85
85
  instruction: string,
86
86
  model?: string,
87
+ resume?: string,
87
88
  ): CliInvocation {
88
- return registeredAdapters()[kind].buildAgentInvocation(instruction, model);
89
+ return registeredAdapters()[kind].buildAgentInvocation(instruction, model, resume);
90
+ }
91
+
92
+ /** Whether this CLI can continue a recorded session (`f.agent` `resume`). */
93
+ export function agentResumable(kind: CliAdapterKind): boolean {
94
+ return registeredAdapters()[kind].resumable === true;
89
95
  }
90
96
 
91
97
  /** Reuse the workspace-free model probe's provider flags for a real LLM call. */
@@ -88,7 +88,8 @@ const NO_CONNECT_OPTION: CliOptionSpec = {
88
88
  const LOCAL_EXECUTION_OPTIONS = [
89
89
  JSON_OPTION,
90
90
  DATA_DIR_OPTION,
91
- { flags: '--local-agent', description: 'Run agent steps in this process instead of a worker' },
91
+ { flags: '--local-agent', description: 'Run agent steps in this process (bound to this terminal unless --detach)' },
92
+ { flags: '--detach', description: 'Start in a separate process, print the run handle and exit; follow with flows status' },
92
93
  {
93
94
  flags: '--agent-capacity <n>',
94
95
  description: `With --local-agent, how many agent steps (and, separately, LLM steps) run at once (1-${MAX_LOCAL_AGENT_CAPACITY})`,
@@ -117,20 +118,22 @@ export const CLI_VERBS = [
117
118
  },
118
119
  {
119
120
  name: 'answer',
120
- description: 'Answer a run’s parked f.human question; `flows resume` then continues the body',
121
+ description: 'Answer a parked f.human; --cloud also resumes, local answers need flows resume',
121
122
  args: [
122
123
  { name: 'run-id', description: 'Run parked on the question', required: true },
123
- { name: 'wait-id', description: 'Which question to answer, named human-<n> in the order the body asked', required: true },
124
- { name: 'answer', description: 'The decision, as yes or no (also true or false)', required: true },
124
+ { name: 'wait-id-or-answer', description: 'Local: human-<n> wait id; Cloud: yes or no', required: true },
125
+ { name: 'answer', description: 'Local decision, as yes or no (also true or false)', required: false },
125
126
  ],
126
127
  options: [
127
128
  JSON_OPTION,
129
+ { flags: '--cloud', description: 'Answer and resume a Cloud run using its open wait' },
130
+ { flags: '--source <path>', description: 'Original Cloud flow source when the stored copy is unavailable or truncated' },
128
131
  DATA_DIR_OPTION,
129
132
  { flags: '--no-spawn', description: 'Require a running relayflowd rather than starting one' },
130
133
  { flags: '--note <text>', description: 'Reason recorded on the journal alongside the answer' },
131
134
  { flags: '--by <identity>', description: 'Who answered, when relaying a person’s decision; defaults to the OS user' },
132
135
  ],
133
- variants: ['answer'],
136
+ variants: ['answer', 'cloud-answer'],
134
137
  },
135
138
  {
136
139
  name: 'build',
@@ -145,9 +148,10 @@ export const CLI_VERBS = [
145
148
  },
146
149
  {
147
150
  name: 'check',
148
- description: 'Compile and preflight a flow without running it, or opening a daemon socket',
151
+ description: 'Compile, preflight, and validate with the installed runtime when available; never start a run',
149
152
  args: [{ name: 'source', description: 'flow.ts, flow.yaml, or spec.json', required: true }],
150
- options: [JSON_OPTION, { flags: '--watch', description: 'Re-check on every change to the flow and its imports' }],
153
+ options: [{ flags: '--against-daemon', description: 'Require acceptance by the installed relayflowd validator (FLOWS_CHECK_AGAINST_DAEMON=1)' }, { flags: '--no-daemon-check', description: 'Local compile and preflight only (FLOWS_NO_DAEMON_CHECK=1)' }, JSON_OPTION, { flags: '--watch', description: 'Re-check on every change to the flow and its imports' },
154
+ { flags: '--explain-warnings', description: 'List every unprovable-effects warning per step instead of one summary line' }],
151
155
  variants: ['check'],
152
156
  },
153
157
  {
@@ -192,6 +196,23 @@ export const CLI_VERBS = [
192
196
  options: [JSON_OPTION],
193
197
  variants: ['deployments'],
194
198
  },
199
+ {
200
+ name: 'eval',
201
+ description: 'Re-execute a flow against a frozen suite of input cases and gate on the score report',
202
+ args: [{ name: 'flow', description: 'flow.ts, flow.yaml, or spec.json: the exact version to evaluate', required: true }],
203
+ options: [
204
+ { flags: '--cases <suite.json>', description: 'Required. Frozen suite: { name, cases: [{ id, input?, expect? }], thresholds? }' },
205
+ JSON_OPTION,
206
+ { flags: '--cloud', description: 'Execute each case as a hosted Cloud run instead of a local run' },
207
+ DATA_DIR_OPTION,
208
+ { flags: '--local-agent', description: 'Attach a local agent worker to each local run' },
209
+ { flags: '--concurrency <n>', description: 'Cases executed at once', defaultValue: '1' },
210
+ { flags: '--report <file>', description: 'Also write the JSON report to this file' },
211
+ { flags: '--baseline <report.json>', description: 'Fail the gate on any case that passed in this prior report' },
212
+ { flags: '--expect-version <sha256:hex>', description: 'Refuse unless the flow version (its local source manifest) hashes to exactly this' },
213
+ ],
214
+ variants: ['eval'],
215
+ },
195
216
  {
196
217
  name: 'hn-monitor',
197
218
  description: 'Hacker News monitor: poll for matching stories and launch a flow per hit',
@@ -427,3 +448,18 @@ export type _EveryDeclaredVariantExists = AssertNever<Exclude<DeclaredVariant, P
427
448
  * from drifting apart.
428
449
  */
429
450
  export const CLI_VERB_NAMES: ReadonlySet<string> = new Set(CLI_VERBS.map((verb) => verb.name));
451
+
452
+ /**
453
+ * Flags `runCli` handles before verb parsing, each only as the sole argument.
454
+ * The CLI reference renders this list, so it is the only place they are spelled.
455
+ */
456
+ export const CLI_GLOBAL_OPTIONS = [
457
+ { flags: '-h, --help', description: 'Print usage and exit.' },
458
+ { flags: '-V, --version', description: 'Print the SDK version and exit.' },
459
+ ] as const satisfies readonly CliOptionSpec[];
460
+
461
+ /** Whether `args` is exactly one spelling of the global option whose long form is `long`. */
462
+ export function isGlobalOption(args: readonly string[], long: '--help' | '--version'): boolean {
463
+ const option = CLI_GLOBAL_OPTIONS.find((candidate) => candidate.flags.split(', ').includes(long));
464
+ return args.length === 1 && option !== undefined && option.flags.split(', ').includes(args[0]!);
465
+ }
package/src/cli-watch.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DaemonValidationMode } from './daemon-spec-validation.js';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { accessSync, constants, existsSync, readFileSync, watch, type FSWatcher } from 'node:fs';
3
4
  import { dirname, join, resolve, sep } from 'node:path';
@@ -11,17 +12,18 @@ type ExitCode = 0 | 1 | 2 | 3;
11
12
  /** A runner around the ordinary CLI, with a fresh module cache for every check. */
12
13
  export async function watchCheck(
13
14
  path: string,
14
- json: boolean,
15
+ options: { json: boolean; explainWarnings: boolean },
15
16
  io: CliIo,
16
17
  /** Cancellation, owned by the caller; this function installs no signal handler. */
17
18
  signal: AbortSignal,
19
+ daemonValidation: DaemonValidationMode = 'auto',
18
20
  ): Promise<ExitCode> {
19
21
  try {
20
22
  return await watchChecks({
21
23
  path,
22
24
  signal,
23
- check: () => checkOnce(path, json, io),
24
- clear: () => { if (!json) io.stdout('\x1b[2J\x1b[H'); },
25
+ check: () => checkOnce(path, { ...options, daemonValidation }, io),
26
+ clear: () => { if (!options.json) io.stdout('\x1b[2J\x1b[H'); },
25
27
  });
26
28
  } catch (error) {
27
29
  io.stderr(`REFUSED [input_unreadable] Could not watch "${path}": ${error instanceof Error ? error.message : String(error)}`);
@@ -140,13 +142,14 @@ function discoverWatchPaths(path: string, imports: Map<string, string[]>): Set<s
140
142
  return paths;
141
143
  }
142
144
 
143
- function checkOnce(path: string, json: boolean, io: CliIo): Promise<ExitCode> {
145
+ function checkOnce(path: string, options: { json: boolean; explainWarnings: boolean; daemonValidation: DaemonValidationMode }, io: CliIo): Promise<ExitCode> {
146
+ const mode = options.daemonValidation;
144
147
  // Importing an authored TS file in this process again would return its stale
145
148
  // cached definition (including transitive imports). A fresh CLI process uses
146
149
  // precisely the same check pipeline and reporting as a one-shot invocation.
147
150
  const entry = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './cli.ts' : './cli.js', import.meta.url));
148
151
  return new Promise((resolveCode, reject) => {
149
- const child = spawn(process.execPath, [...process.execArgv, entry, 'check', ...(json ? ['--json'] : []), path], {
152
+ const child = spawn(process.execPath, [...process.execArgv, entry, 'check', ...(mode === 'required' ? ['--against-daemon'] : mode === 'off' ? ['--no-daemon-check'] : []), ...(options.json ? ['--json'] : []), ...(options.explainWarnings ? ['--explain-warnings'] : []), path], {
150
153
  stdio: ['ignore', 'pipe', 'pipe'],
151
154
  // Ctrl-C belongs to the watcher; let an active check finish so its report
152
155
  // and exit status remain a pair, then release all resources.