@relayflows/sdk 2.0.35 → 2.0.37

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 (228) hide show
  1. package/dist/adapters/base.d.ts +16 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +15 -0
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/codex.d.ts.map +1 -1
  7. package/dist/adapters/codex.js +4 -0
  8. package/dist/adapters/codex.js.map +1 -1
  9. package/dist/adapters/usage-limit.d.ts +2 -0
  10. package/dist/adapters/usage-limit.d.ts.map +1 -0
  11. package/dist/adapters/usage-limit.js +17 -0
  12. package/dist/adapters/usage-limit.js.map +1 -0
  13. package/dist/authored-activity.d.ts +17 -0
  14. package/dist/authored-activity.d.ts.map +1 -0
  15. package/dist/authored-activity.js +218 -0
  16. package/dist/authored-activity.js.map +1 -0
  17. package/dist/authored-child-run.d.ts +34 -0
  18. package/dist/authored-child-run.d.ts.map +1 -0
  19. package/dist/authored-child-run.js +128 -0
  20. package/dist/authored-child-run.js.map +1 -0
  21. package/dist/authored-flow-error.d.ts +32 -3
  22. package/dist/authored-flow-error.d.ts.map +1 -1
  23. package/dist/authored-flow-error.js +3 -1
  24. package/dist/authored-flow-error.js.map +1 -1
  25. package/dist/authored-flow-executor.d.ts +11 -1
  26. package/dist/authored-flow-executor.d.ts.map +1 -1
  27. package/dist/authored-flow-executor.js +32 -1
  28. package/dist/authored-flow-executor.js.map +1 -1
  29. package/dist/authored-helper-effect.d.ts.map +1 -1
  30. package/dist/authored-helper-effect.js +3 -2
  31. package/dist/authored-helper-effect.js.map +1 -1
  32. package/dist/authored-mcp.d.ts.map +1 -1
  33. package/dist/authored-mcp.js +4 -2
  34. package/dist/authored-mcp.js.map +1 -1
  35. package/dist/authored-node-entry.js +9 -1
  36. package/dist/authored-node-entry.js.map +1 -1
  37. package/dist/authored-node-runner.d.ts.map +1 -1
  38. package/dist/authored-node-runner.js +42 -9
  39. package/dist/authored-node-runner.js.map +1 -1
  40. package/dist/authored-plugin-effect.d.ts.map +1 -1
  41. package/dist/authored-plugin-effect.js +4 -2
  42. package/dist/authored-plugin-effect.js.map +1 -1
  43. package/dist/authored-preflight.d.ts +1 -1
  44. package/dist/authored-preflight.d.ts.map +1 -1
  45. package/dist/authored-preflight.js +2 -2
  46. package/dist/authored-preflight.js.map +1 -1
  47. package/dist/authored-root.d.ts +12 -7
  48. package/dist/authored-root.d.ts.map +1 -1
  49. package/dist/authored-root.js +24 -0
  50. package/dist/authored-root.js.map +1 -1
  51. package/dist/authored-step-output.d.ts +24 -8
  52. package/dist/authored-step-output.d.ts.map +1 -1
  53. package/dist/authored-step-output.js +81 -15
  54. package/dist/authored-step-output.js.map +1 -1
  55. package/dist/authored-subscription-park.d.ts +5 -0
  56. package/dist/authored-subscription-park.d.ts.map +1 -0
  57. package/dist/authored-subscription-park.js +47 -0
  58. package/dist/authored-subscription-park.js.map +1 -0
  59. package/dist/authored-verdict.d.ts +2 -0
  60. package/dist/authored-verdict.d.ts.map +1 -1
  61. package/dist/authored-verdict.js +2 -1
  62. package/dist/authored-verdict.js.map +1 -1
  63. package/dist/authored-worker-step.d.ts +1 -1
  64. package/dist/authored-worker-step.d.ts.map +1 -1
  65. package/dist/authored-worker-step.js +9 -4
  66. package/dist/authored-worker-step.js.map +1 -1
  67. package/dist/cli/check-activities.d.ts +8 -0
  68. package/dist/cli/check-activities.d.ts.map +1 -0
  69. package/dist/cli/check-activities.js +155 -0
  70. package/dist/cli/check-activities.js.map +1 -0
  71. package/dist/cli/check.d.ts +2 -0
  72. package/dist/cli/check.d.ts.map +1 -1
  73. package/dist/cli/check.js +3 -3
  74. package/dist/cli/check.js.map +1 -1
  75. package/dist/cli/cli-probe.d.ts +2 -2
  76. package/dist/cli/cli-probe.d.ts.map +1 -1
  77. package/dist/cli/cli-probe.js +22 -9
  78. package/dist/cli/cli-probe.js.map +1 -1
  79. package/dist/cli/direct-run.d.ts.map +1 -1
  80. package/dist/cli/direct-run.js +17 -4
  81. package/dist/cli/direct-run.js.map +1 -1
  82. package/dist/cli/run.d.ts +18 -5
  83. package/dist/cli/run.d.ts.map +1 -1
  84. package/dist/cli/run.js +37 -12
  85. package/dist/cli/run.js.map +1 -1
  86. package/dist/cli/subscription-report.d.ts +5 -0
  87. package/dist/cli/subscription-report.d.ts.map +1 -0
  88. package/dist/cli/subscription-report.js +48 -0
  89. package/dist/cli/subscription-report.js.map +1 -0
  90. package/dist/cli-adapter.d.ts +4 -2
  91. package/dist/cli-adapter.d.ts.map +1 -1
  92. package/dist/cli-adapter.js +4 -0
  93. package/dist/cli-adapter.js.map +1 -1
  94. package/dist/cli.d.ts +1 -1
  95. package/dist/cli.d.ts.map +1 -1
  96. package/dist/cli.js +13 -5
  97. package/dist/cli.js.map +1 -1
  98. package/dist/communication/local.d.ts +1 -1
  99. package/dist/communication/local.d.ts.map +1 -1
  100. package/dist/communication/local.js +2 -2
  101. package/dist/communication/local.js.map +1 -1
  102. package/dist/communication/worker.d.ts +1 -1
  103. package/dist/communication/worker.d.ts.map +1 -1
  104. package/dist/communication/worker.js +4 -4
  105. package/dist/communication/worker.js.map +1 -1
  106. package/dist/decimal-dollars.d.ts +12 -0
  107. package/dist/decimal-dollars.d.ts.map +1 -0
  108. package/dist/decimal-dollars.js +39 -0
  109. package/dist/decimal-dollars.js.map +1 -0
  110. package/dist/effect-channel.d.ts.map +1 -1
  111. package/dist/effect-channel.js +2 -0
  112. package/dist/effect-channel.js.map +1 -1
  113. package/dist/failure-kinds.d.ts +2 -2
  114. package/dist/failure-kinds.d.ts.map +1 -1
  115. package/dist/failure-kinds.js +2 -0
  116. package/dist/failure-kinds.js.map +1 -1
  117. package/dist/helper-instruction.d.ts +3 -0
  118. package/dist/helper-instruction.d.ts.map +1 -0
  119. package/dist/helper-instruction.js +3 -0
  120. package/dist/helper-instruction.js.map +1 -0
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/journal-client.d.ts +21 -3
  125. package/dist/journal-client.d.ts.map +1 -1
  126. package/dist/journal-client.js +45 -6
  127. package/dist/journal-client.js.map +1 -1
  128. package/dist/journal-reader.d.ts.map +1 -1
  129. package/dist/journal-reader.js +2 -0
  130. package/dist/journal-reader.js.map +1 -1
  131. package/dist/llm-worker.d.ts +2 -1
  132. package/dist/llm-worker.d.ts.map +1 -1
  133. package/dist/llm-worker.js +7 -2
  134. package/dist/llm-worker.js.map +1 -1
  135. package/dist/local-agent-environment.d.ts +13 -0
  136. package/dist/local-agent-environment.d.ts.map +1 -0
  137. package/dist/local-agent-environment.js +104 -0
  138. package/dist/local-agent-environment.js.map +1 -0
  139. package/dist/local-agent.d.ts +4 -1
  140. package/dist/local-agent.d.ts.map +1 -1
  141. package/dist/local-agent.js +15 -5
  142. package/dist/local-agent.js.map +1 -1
  143. package/dist/preflight.d.ts +6 -0
  144. package/dist/preflight.d.ts.map +1 -1
  145. package/dist/preflight.js +36 -10
  146. package/dist/preflight.js.map +1 -1
  147. package/dist/protocol.d.ts +187 -1
  148. package/dist/protocol.d.ts.map +1 -1
  149. package/dist/reported-cost-total.d.ts +41 -0
  150. package/dist/reported-cost-total.d.ts.map +1 -0
  151. package/dist/reported-cost-total.js +63 -0
  152. package/dist/reported-cost-total.js.map +1 -0
  153. package/dist/reported-cost.d.ts +24 -0
  154. package/dist/reported-cost.d.ts.map +1 -0
  155. package/dist/reported-cost.js +49 -0
  156. package/dist/reported-cost.js.map +1 -0
  157. package/dist/run-state.d.ts +9 -11
  158. package/dist/run-state.d.ts.map +1 -1
  159. package/dist/run-state.js +15 -38
  160. package/dist/run-state.js.map +1 -1
  161. package/dist/step-settling.d.ts +11 -0
  162. package/dist/step-settling.d.ts.map +1 -0
  163. package/dist/step-settling.js +22 -0
  164. package/dist/step-settling.js.map +1 -0
  165. package/dist/worker-cli.d.ts +1 -1
  166. package/dist/worker-cli.d.ts.map +1 -1
  167. package/dist/worker-cli.js +3 -3
  168. package/dist/worker-cli.js.map +1 -1
  169. package/dist/worker.d.ts +2 -0
  170. package/dist/worker.d.ts.map +1 -1
  171. package/dist/worker.js +5 -2
  172. package/dist/worker.js.map +1 -1
  173. package/dist/yaml-helper-effect.d.ts.map +1 -1
  174. package/dist/yaml-helper-effect.js +2 -1
  175. package/dist/yaml-helper-effect.js.map +1 -1
  176. package/dist/yaml-helpers.d.ts.map +1 -1
  177. package/dist/yaml-helpers.js +2 -1
  178. package/dist/yaml-helpers.js.map +1 -1
  179. package/package.json +2 -2
  180. package/src/adapters/base.ts +19 -0
  181. package/src/adapters/claude.ts +15 -0
  182. package/src/adapters/codex.ts +6 -0
  183. package/src/adapters/usage-limit.ts +17 -0
  184. package/src/authored-activity.ts +251 -0
  185. package/src/authored-child-run.ts +138 -0
  186. package/src/authored-flow-error.ts +33 -0
  187. package/src/authored-flow-executor.ts +44 -1
  188. package/src/authored-helper-effect.ts +3 -2
  189. package/src/authored-mcp.ts +4 -2
  190. package/src/authored-node-entry.ts +11 -2
  191. package/src/authored-node-runner.ts +43 -9
  192. package/src/authored-plugin-effect.ts +4 -2
  193. package/src/authored-preflight.ts +2 -2
  194. package/src/authored-root.ts +34 -4
  195. package/src/authored-step-output.ts +88 -15
  196. package/src/authored-subscription-park.ts +46 -0
  197. package/src/authored-verdict.ts +2 -1
  198. package/src/authored-worker-step.ts +9 -3
  199. package/src/cli/check-activities.ts +144 -0
  200. package/src/cli/check.ts +6 -3
  201. package/src/cli/cli-probe.ts +23 -9
  202. package/src/cli/direct-run.ts +16 -1
  203. package/src/cli/run.ts +59 -14
  204. package/src/cli/subscription-report.ts +51 -0
  205. package/src/cli-adapter.ts +8 -2
  206. package/src/cli.ts +13 -6
  207. package/src/communication/local.ts +3 -2
  208. package/src/communication/worker.ts +7 -4
  209. package/src/decimal-dollars.ts +38 -0
  210. package/src/effect-channel.ts +2 -0
  211. package/src/failure-kinds.ts +2 -0
  212. package/src/helper-instruction.ts +2 -0
  213. package/src/index.ts +6 -0
  214. package/src/journal-client.ts +60 -7
  215. package/src/journal-reader.ts +2 -0
  216. package/src/llm-worker.ts +6 -1
  217. package/src/local-agent-environment.ts +92 -0
  218. package/src/local-agent.ts +19 -4
  219. package/src/preflight.ts +46 -10
  220. package/src/protocol.ts +151 -0
  221. package/src/reported-cost-total.ts +82 -0
  222. package/src/reported-cost.ts +51 -0
  223. package/src/run-state.ts +25 -37
  224. package/src/step-settling.ts +19 -0
  225. package/src/worker-cli.ts +3 -2
  226. package/src/worker.ts +9 -2
  227. package/src/yaml-helper-effect.ts +2 -1
  228. package/src/yaml-helpers.ts +2 -1
@@ -1,11 +1,13 @@
1
1
  import { COMPLETION_REASONS, RUN_COMPLETION_REASONS, FLOW_COMPLETION_REASONS, type FlowCompletionReason } from '@relayflows/surface';
2
2
  import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
3
  import type { JournalClient } from './journal-client.js';
4
- import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome } from './protocol.js';
4
+ import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome, RunStatus } from './protocol.js';
5
5
  import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
6
6
  import type { StepFailedDetails } from './failure-kinds.js';
7
7
  import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './cli/step-failure.js';
8
8
  import { alsoRecord, recordAuthoredChild, type AuthoredStepEdges } from './authored-step-index.js';
9
+ import { readChildJournal, waitForTerminalChildRun } from './authored-child-run.js';
10
+ import { settlingCompletion } from './step-settling.js';
9
11
 
10
12
  /**
11
13
  * What an authored operation needs in order to leave readable evidence:
@@ -19,6 +21,8 @@ export interface AuthoredStepContext {
19
21
  readonly dataDir?: string;
20
22
  /** The step's label and causal predecessors, carried on every record about it. */
21
23
  readonly stepEdges?: (step: string) => AuthoredStepEdges | undefined;
24
+ /** Cancels the wait for an adopted child run that is not yet terminal. */
25
+ readonly signal?: AbortSignal;
22
26
  }
23
27
 
24
28
  /**
@@ -27,12 +31,26 @@ export interface AuthoredStepContext {
27
31
  * clean success before handing the raw `output` back for verb-specific
28
32
  * extraction (a plain string for `f.run`, an `AgentResult` for `f.agent`).
29
33
  *
30
- * Callers are responsible for having already waited for the run to reach a
31
- * terminal state — `f.run`'s caller relies on `runStart`'s own immediate
32
- * response (the kernel drives a deterministic step to completion inline, no
33
- * race); `f.agent`'s caller relies on `classifyOutcome` (cli/run.ts) having
34
- * already polled to one. Given that, a single read from the start of this
35
- * run's small journal is enough, and no polling happens here.
34
+ * The run must be terminal before it is judged. Usually it already is —
35
+ * `f.run`'s `runStart` drives a deterministic step to completion inline, and
36
+ * `f.agent`'s caller has polled one with `classifyOutcome` (cli/run.ts). But a
37
+ * child ADOPTED through its admission key on resume is returned as it stands,
38
+ * possibly mid-attempt or mid-backoff on the request that first started it,
39
+ * so unless the step has already settled as a failure, a run that neither
40
+ * the caller's own kernel answer (`knownStatus`) nor a journaled
41
+ * `run.completed` shows terminal is waited out first (`waitForTerminalChildRun`, bounded by
42
+ * the child's own lease and backoff). A success is never read off a child
43
+ * whose run is not terminal. A child that cannot settle — parked on a worker
44
+ * nobody attached — reports its latest attempt that ran to a failed result,
45
+ * or is refused.
46
+ *
47
+ * The step's verdict is its TERMINAL completion. A retried attempt journals a
48
+ * `step.completed` too — `disposition: retry` (crash recovery's `crashed`, a
49
+ * `worker_error`, a failed gate with iterations left) — and it precedes the
50
+ * attempt that settled the step. Taking the first `step.completed` read that
51
+ * retried attempt's failure as the step's, and failed a child that completed
52
+ * `success`. Only the last completion whose disposition is not `retry` or
53
+ * `park` is read, recorded and indexed.
36
54
  *
37
55
  * What is NOT delegated to the caller is the run's verdict. A child spec has
38
56
  * more than one step whenever the author declared a `.gate()`, and that gate
@@ -56,15 +74,63 @@ export async function readCompletedStepOutput(
56
74
  stepId: string,
57
75
  journalSteps: AuthoredFlowJournalStep[],
58
76
  context: AuthoredStepContext = {},
77
+ // `suspended` (an Event Await boundary, `RunReport.status`) is not terminal:
78
+ // it is judged exactly like a run still in flight.
79
+ knownStatus?: RunStatus | 'suspended',
59
80
  ): Promise<unknown> {
60
- const entries = (await journal.journalRead(runId, 1)).entries;
61
- const completed = entries.find((entry) => isStepCompleted(entry, stepId));
62
- if (!isStepCompleted(completed, stepId)) {
63
- throw protocolViolation(runId, `journal has no step.completed for "${stepId}"`);
81
+ let entries = await readChildJournal(journal, runId);
82
+ let completed = terminalCompletion(entries, stepId);
83
+ // A success is never accepted before the child RUN is terminal: a declared
84
+ // gate lowers to a dependent step (`lowerNamedGates`) that may not even have
85
+ // started when the producer's success is journaled, and the run's verdict,
86
+ // not the producer's, is the operation's. Only a settled FAILURE is final
87
+ // on its own — the step has no attempt left.
88
+ // `knownStatus` is the kernel's own answer the caller already holds (the
89
+ // `run.start` receipt, or the outcome `classifyOutcome` polled to): a
90
+ // terminal one is as authoritative as a journaled `run.completed`.
91
+ const settledFailure = completed !== undefined && completed.payload.completionReason !== 'success';
92
+ const terminal = knownStatus === 'completed' || knownStatus === 'failed'
93
+ || entries.some(entry => runCompletionReason(entry) !== undefined);
94
+ if (!terminal && !settledFailure) {
95
+ const status = await waitForTerminalChildRun(journal, runId, context.signal);
96
+ entries = await readChildJournal(journal, runId);
97
+ completed = terminalCompletion(entries, stepId);
98
+ if (status !== 'completed' && status !== 'failed') {
99
+ if (completed?.payload.completionReason === 'success') {
100
+ throw protocolViolation(runId,
101
+ `child run for step "${stepId}" is not terminal (status: ${status}); its verdict is still pending`);
102
+ }
103
+ if (completed === undefined) {
104
+ // Nothing will settle the step from here: the child is parked on a
105
+ // worker the caller did not keep attached (a helper drives exactly
106
+ // one attempt, and the retry dispatched after it dies with its
107
+ // connection). The latest attempt that ran to a result — not one the
108
+ // kernel recorded as dead (`completed_by: kernel`,
109
+ // machine/recovery.rs) — is then why the step stands where it does,
110
+ // and is reported as that: never as a success, never silently.
111
+ const attempts = entries.filter((entry): entry is StepCompletedEntry => isStepCompleted(entry, stepId));
112
+ const latest = attempts.filter(entry => entry.payload.completed_by !== 'kernel').at(-1) ?? attempts.at(-1);
113
+ if (latest === undefined || latest.payload.completionReason === 'success') {
114
+ throw protocolViolation(runId, `child run for step "${stepId}" is not terminal (status: ${status})`);
115
+ }
116
+ completed = latest;
117
+ }
118
+ }
119
+ }
120
+ const runReason = entries.map(runCompletionReason).find((value) => value !== undefined);
121
+ const edges = context.stepEdges?.(stepId);
122
+ if (completed === undefined) {
123
+ // Canceled or out of budget while the step was between attempts: the run
124
+ // settled with no terminal completion for the step. That is the run's
125
+ // failure to report, not a malformed journal.
126
+ if (runReason !== undefined && runReason !== 'success') {
127
+ throw await stepFailure(journal, runId, stepId, runReason, context, edges,
128
+ `journal run for step "${stepId}" completed with ${runReason}`);
129
+ }
130
+ throw protocolViolation(runId, `journal has no terminal step.completed for "${stepId}"`);
64
131
  }
65
132
 
66
133
  const reason = completed.payload.completionReason;
67
- const edges = context.stepEdges?.(stepId);
68
134
  journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason, ...edges }));
69
135
  if (reason !== 'success') {
70
136
  throw await stepFailure(journal, runId, stepId, reason, context, edges,
@@ -76,7 +142,6 @@ export async function readCompletedStepOutput(
76
142
  // verdict. Reading only the producer's entry let a failed gate resolve as
77
143
  // though the command had passed — the `|| true` invisibility this module's
78
144
  // recording policy exists to remove, reappearing one layer up.
79
- const runReason = entries.map(runCompletionReason).find((value) => value !== undefined);
80
145
  if (runReason !== undefined && runReason !== 'success') {
81
146
  throw await stepFailure(journal, runId, stepId, runReason, context, edges,
82
147
  `journal run for step "${stepId}" completed with ${runReason}`);
@@ -130,7 +195,7 @@ export async function readSuccessfulOutput(
130
195
  journalSteps: AuthoredFlowJournalStep[],
131
196
  context: AuthoredStepContext = {},
132
197
  ): Promise<string> {
133
- const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
198
+ const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context, outcome.status)
134
199
  .catch(relabelCommandTimeout(stepId));
135
200
  if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
136
201
  throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
@@ -166,7 +231,7 @@ export async function readRecordedOutcome(
166
231
  journalSteps: AuthoredFlowJournalStep[],
167
232
  context: AuthoredStepContext = {},
168
233
  ): Promise<RecordedRunOutcome> {
169
- const envelope = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
234
+ const envelope = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context, outcome.status)
170
235
  .catch(relabelCommandTimeout(stepId));
171
236
  const exitCode = isRecord(envelope) ? envelope['exit_code'] : undefined;
172
237
  if (!isRecord(envelope) || typeof envelope['stdout_tail'] !== 'string'
@@ -204,10 +269,18 @@ interface StepCompletedEntry {
204
269
  step_id: string;
205
270
  payload: {
206
271
  completionReason: ProtocolCompletionReason;
272
+ disposition?: unknown;
273
+ completed_by?: unknown;
207
274
  output: unknown;
208
275
  };
209
276
  }
210
277
 
278
+ /** The step's LAST settling completion, when it is well formed. */
279
+ function terminalCompletion(entries: readonly unknown[], stepId: string): StepCompletedEntry | undefined {
280
+ const settling = settlingCompletion(entries, stepId);
281
+ return isStepCompleted(settling, stepId) ? settling : undefined;
282
+ }
283
+
211
284
  function isStepCompleted(value: unknown, stepId: string): value is StepCompletedEntry {
212
285
  if (!isRecord(value) || value['entry_type'] !== 'step.completed' || value['step_id'] !== stepId) {
213
286
  return false;
@@ -0,0 +1,46 @@
1
+ import type { AuthoredFlowSuspension } from './authored-flow-error.js';
2
+ import type { JournalClient } from './journal-client.js';
3
+
4
+ /** Recover the handoff report without starting a worker on a still-open wait. */
5
+ export async function readSubscriptionPark(
6
+ journal: JournalClient, runId: string,
7
+ ): Promise<AuthoredFlowSuspension | undefined> {
8
+ const open = new Map<string, { subscription_id: string; phase: 'activation' | 'event_wait' }>();
9
+ const prepared = new Map<string, Record<string, unknown>>();
10
+ let from = 1;
11
+ for (;;) {
12
+ const { entries } = await journal.journalRead(runId, from, 500);
13
+ for (const raw of entries) {
14
+ const entry = raw as { seq: number; entry_type: string; step_id?: string; payload: Record<string, unknown> };
15
+ from = entry.seq + 1;
16
+ const p = entry.payload;
17
+ if (entry.entry_type === 'subscription.prepared') prepared.set(String(p['subscription_id']), p);
18
+ if (entry.entry_type === 'wait.event' && entry.step_id === 'authored-root'
19
+ && typeof p['event_key'] === 'string' && p['event_key'].startsWith('subscription.park:')) {
20
+ const park = JSON.parse(p['event_key'].slice('subscription.park:'.length)) as Record<string, unknown>;
21
+ if (typeof park['subscription_id'] !== 'string'
22
+ || (park['phase'] !== 'activation' && park['phase'] !== 'event_wait')) {
23
+ throw new Error('malformed durable subscription park');
24
+ }
25
+ open.set(String(p['wait_id']), { subscription_id: park['subscription_id'], phase: park['phase'] });
26
+ }
27
+ if (entry.entry_type === 'wait.completed') open.delete(String(p['wait_id']));
28
+ }
29
+ if (entries.length < 500) break;
30
+ }
31
+ const park = [...open.values()].at(-1);
32
+ if (park === undefined) return undefined;
33
+ const p = prepared.get(park.subscription_id);
34
+ if (p === undefined || typeof p['stream'] !== 'string' || typeof p['deadline_at_ms'] !== 'number') {
35
+ throw new Error('subscription park has no prepared boundary');
36
+ }
37
+ const common = { subscriptionId: park.subscription_id, stream: p['stream'], deadlineAtMs: p['deadline_at_ms'] };
38
+ if (park.phase === 'event_wait') return { kind: 'event_wait', ...common };
39
+ if (!Array.isArray(p['event_types']) || !p['event_types'].every(v => typeof v === 'string')
40
+ || typeof p['settle_ms'] !== 'number' || typeof p['idle_ms'] !== 'number' || typeof p['include_self'] !== 'boolean') {
41
+ throw new Error('subscription park has malformed prepared facts');
42
+ }
43
+ return { kind: 'activation', ...common, eventTypes: p['event_types'], settleMs: p['settle_ms'],
44
+ idleMs: p['idle_ms'], includeSelf: p['include_self'],
45
+ ...(p['pattern'] === undefined || p['pattern'] === null ? {} : { pattern: p['pattern'] as Record<string, unknown> }) };
46
+ }
@@ -67,7 +67,8 @@ export function authoredVerdictOf(events: readonly JournalEvent[]): AuthoredVerd
67
67
  return Object.freeze({ reason, ...(detail === undefined ? {} : { detail }) });
68
68
  }
69
69
 
70
- function declaresAuthoredRoot(spawned: JournalEvent): boolean {
70
+ /** Whether a journal's spawned spec is a real authored root, not a step merely named `authored-root`. */
71
+ export function declaresAuthoredRoot(spawned: JournalEvent): boolean {
71
72
  const spec = record(record(spawned.payload)?.['spec']);
72
73
  const steps = spec?.['steps'];
73
74
  if (!Array.isArray(steps)) return false;
@@ -27,6 +27,7 @@ export function authoredWorkerRunner(
27
27
  localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
28
28
  rootRunId?: string, workerCapacity?: number, stepEdges?: AuthoredStepContext['stepEdges'],
29
29
  sharedSlots?: AuthoredWorkerSlots,
30
+ agentEnvironment?: NodeJS.ProcessEnv,
30
31
  ) {
31
32
  // Sized to the attached local workers, so concurrent calls wait here for a
32
33
  // slot instead of being admitted and parked for want of a free worker.
@@ -36,11 +37,12 @@ export function authoredWorkerRunner(
36
37
  agent: slots.agent.scope(),
37
38
  llm: slots.llm.scope(),
38
39
  };
39
- const check = authoredPreflight(flowPath);
40
+ const check = authoredPreflight(flowPath, agentEnvironment);
40
41
  const context: AuthoredStepContext = {
41
42
  ...(rootRunId === undefined ? {} : { rootRunId }),
42
43
  ...(waitOptions.dataDir === undefined ? {} : { dataDir: waitOptions.dataDir }),
43
44
  ...(stepEdges === undefined ? {} : { stepEdges }),
45
+ ...(waitOptions.signal === undefined ? {} : { signal: waitOptions.signal }),
44
46
  };
45
47
  async function run(step: StepSpec): Promise<unknown> {
46
48
  const id = step.id;
@@ -56,8 +58,12 @@ export function authoredWorkerRunner(
56
58
  (diagnostic): diagnostic is PreflightDiagnostic & { severity: 'refusal' } =>
57
59
  diagnostic.severity === 'refusal',
58
60
  );
61
+ // Environment refusals keep their own kind: folding a usage limit or an
62
+ // outdated CLI into `*_cli_unresolved` reported them as `invalid_spec`,
63
+ // telling the author to fix a flow that was never wrong.
59
64
  throw new AuthoredFlowExecutionError(
60
- refusal?.kind === 'budget_syntax_invalid' ? refusal.kind
65
+ refusal?.kind === 'budget_syntax_invalid' || refusal?.kind === 'cli_outdated'
66
+ || refusal?.kind === 'provider_usage_limited' ? refusal.kind
61
67
  : step.type === 'llm' ? 'llm_cli_unresolved' : 'agent_cli_unresolved',
62
68
  refusal?.message
63
69
  ?? `flow "${definition.name}" step "${id}": no CLI could be resolved for f.${step.type} `
@@ -135,7 +141,7 @@ export function authoredWorkerRunner(
135
141
  details,
136
142
  );
137
143
  }
138
- return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, context);
144
+ return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, context, execution.report.status);
139
145
  };
140
146
  const admissionKey = authoredChildAdmissionKey(rootRunId, id);
141
147
  const admit = async () => budget === undefined
@@ -0,0 +1,144 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import ts from 'typescript';
3
+ import { inputFailureReport, type CheckExecution } from './check.js';
4
+
5
+ /**
6
+ * Fail closed on body-level `f.on` calls that visibly omit an end bound.
7
+ * Dynamic options cannot be proved at check time and are refused here too;
8
+ * runtime validation covers JS callers and values assembled at runtime.
9
+ */
10
+ export async function checkAuthoredActivities(path: string): Promise<CheckExecution> {
11
+ try {
12
+ const source = await readFile(path, 'utf8');
13
+ const file = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true);
14
+ let diagnostic: string | undefined;
15
+ const visit = (node: ts.Node, context: ContextNames): void => {
16
+ if (diagnostic !== undefined) return;
17
+ if (ts.isCallExpression(node) && isContextOn(node.expression, context)) {
18
+ const options = node.arguments[1];
19
+ if (options === undefined || !ts.isObjectLiteralExpression(options)) {
20
+ diagnostic = 'body-level f.on() requires literal idle and deadline bounds (unbounded_subscription)';
21
+ return;
22
+ }
23
+ const keys = new Set(options.properties.flatMap((property) => {
24
+ if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) return [];
25
+ return ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? [property.name.text] : [];
26
+ }));
27
+ if (!keys.has('idle') || !keys.has('deadline')) {
28
+ diagnostic = 'body-level f.on() requires both idle and deadline bounds (unbounded_subscription)';
29
+ return;
30
+ }
31
+ }
32
+ // A nested function that rebinds a context name is not the flow context.
33
+ const inner = ts.isFunctionLike(node) ? shadow(context, node.parameters) : context;
34
+ ts.forEachChild(node, (child) => visit(child, inner));
35
+ };
36
+ for (const body of authoredBodies(file)) {
37
+ if (body.body !== undefined) visit(body.body, contextNames(body));
38
+ }
39
+ if (diagnostic !== undefined) {
40
+ return { report: inputFailureReport({ kind: 'invalid_spec', message: diagnostic }, path) };
41
+ }
42
+ return { report: { ok: true, path, gates: [], resolutions: [], diagnostics: [] } };
43
+ } catch (error) {
44
+ return { report: inputFailureReport({ kind: 'invalid_spec',
45
+ message: error instanceof Error ? error.message : 'Cannot inspect body-level activities' }, path) };
46
+ }
47
+ }
48
+
49
+ interface ContextNames {
50
+ /** Identifiers bound to a body's flow context, e.g. `f` or `ctx`. */
51
+ readonly objects: Set<string>;
52
+ /** Local names bound to a destructured `on`, e.g. `({ on }) => on(...)`. */
53
+ readonly on: Set<string>;
54
+ }
55
+
56
+ type BodyFunction = ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration;
57
+
58
+ /**
59
+ * Every authored body in this file: the last function argument of a
60
+ * `flow(...)` call, each trigger handler registered with `.on(trigger, body)`
61
+ * on a flow handle, and the default export, inline or named by an identifier
62
+ * declared in the same file. Only these bodies are checked.
63
+ */
64
+ function authoredBodies(file: ts.SourceFile): Set<BodyFunction> {
65
+ const functions = new Map<string, BodyFunction>();
66
+ const handles = new Set<string>();
67
+ const isFlowHandle = (node: ts.Expression): boolean => {
68
+ if (ts.isIdentifier(node)) return handles.has(node.text);
69
+ if (!ts.isCallExpression(node)) return false;
70
+ if (ts.isIdentifier(node.expression)) return node.expression.text === 'flow';
71
+ return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'on'
72
+ && isFlowHandle(node.expression.expression);
73
+ };
74
+ const bodies = new Set<BodyFunction>();
75
+ const references: string[] = [];
76
+ const addBody = (node: ts.Node | undefined): void => {
77
+ if (node === undefined) return;
78
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node)) bodies.add(node);
79
+ else if (ts.isIdentifier(node)) references.push(node.text);
80
+ };
81
+ const visit = (node: ts.Node): void => {
82
+ if (ts.isFunctionDeclaration(node) && node.name !== undefined) functions.set(node.name.text, node);
83
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer !== undefined
84
+ && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
85
+ functions.set(node.name.text, node.initializer);
86
+ }
87
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer !== undefined
88
+ && isFlowHandle(node.initializer)) {
89
+ handles.add(node.name.text);
90
+ }
91
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'flow') {
92
+ addBody(node.arguments[node.arguments.length - 1]);
93
+ }
94
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
95
+ && node.expression.name.text === 'on' && isFlowHandle(node.expression.expression)) {
96
+ addBody(node.arguments[1]);
97
+ }
98
+ if (ts.isExportAssignment(node) && !node.isExportEquals) addBody(node.expression);
99
+ if (ts.isFunctionDeclaration(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)) addBody(node);
100
+ ts.forEachChild(node, visit);
101
+ };
102
+ visit(file);
103
+ for (const name of references) {
104
+ const body = functions.get(name);
105
+ if (body !== undefined) bodies.add(body);
106
+ }
107
+ return bodies;
108
+ }
109
+
110
+ function contextNames(body: BodyFunction): ContextNames {
111
+ const names: ContextNames = { objects: new Set(), on: new Set() };
112
+ const parameter = body.parameters[0]?.name;
113
+ if (parameter === undefined) return names;
114
+ if (ts.isIdentifier(parameter)) names.objects.add(parameter.text);
115
+ else if (ts.isObjectBindingPattern(parameter)) {
116
+ for (const element of parameter.elements) {
117
+ const key = element.propertyName ?? element.name;
118
+ if (ts.isIdentifier(key) && key.text === 'on' && ts.isIdentifier(element.name)) names.on.add(element.name.text);
119
+ }
120
+ }
121
+ return names;
122
+ }
123
+
124
+ function shadow(context: ContextNames, parameters: ts.NodeArray<ts.ParameterDeclaration>): ContextNames {
125
+ const bound = new Set<string>();
126
+ const collect = (name: ts.BindingName): void => {
127
+ if (ts.isIdentifier(name)) bound.add(name.text);
128
+ else for (const element of name.elements) if (!ts.isOmittedExpression(element)) collect(element.name);
129
+ };
130
+ for (const parameter of parameters) collect(parameter.name);
131
+ if (bound.size === 0) return context;
132
+ return {
133
+ objects: new Set([...context.objects].filter(name => !bound.has(name))),
134
+ on: new Set([...context.on].filter(name => !bound.has(name))),
135
+ };
136
+ }
137
+
138
+ function isContextOn(expression: ts.LeftHandSideExpression, context: ContextNames): boolean {
139
+ if (ts.isIdentifier(expression)) return context.on.has(expression.text);
140
+ return ts.isPropertyAccessExpression(expression)
141
+ && ts.isIdentifier(expression.expression)
142
+ && context.objects.has(expression.expression.text)
143
+ && expression.name.text === 'on';
144
+ }
package/src/cli/check.ts CHANGED
@@ -123,6 +123,8 @@ type CliProbeOutcomeMap = Map<string, CliProbeOutcome>;
123
123
  * anything that depends on how the flow is about to be invoked opts in here.
124
124
  */
125
125
  export interface CheckInvocation {
126
+ /** Environment used only by provider executable/auth/model probes. */
127
+ environment?: NodeJS.ProcessEnv;
126
128
  /**
127
129
  * Report `agent_worker_unresolved` when the spec has `agent` steps.
128
130
  *
@@ -202,7 +204,7 @@ export function checkAuthoredFlow(
202
204
  : projectConfigOrOptions as ProjectConfig | undefined;
203
205
  const effectiveProbeCache = cliProbeCache ?? options?.probeCache;
204
206
  const config = projectConfig ?? readProjectConfig(dirname(absolutePath));
205
- const probes = systemProbes(dirname(absolutePath), config);
207
+ const probes = systemProbes(dirname(absolutePath), config, invocation.environment);
206
208
  const result = preflight(authoring, {
207
209
  ...(effectiveProbeCache === undefined ? {} : { cliProbeCache: effectiveProbeCache }),
208
210
  projectCli: config.cli,
@@ -450,10 +452,11 @@ function findConfig(start: string): string | undefined {
450
452
  }
451
453
  }
452
454
 
453
- function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes {
455
+ function systemProbes(flowDirectory: string, config: ProjectConfig, environment?: NodeJS.ProcessEnv): PreflightProbes {
454
456
  return {
455
457
  helper: helperReady,
456
- cli: (cli, source, model, execution) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model, execution),
458
+ cli: (cli, source, model, execution) => probeCli(cli,
459
+ source === 'project' ? config.directory : flowDirectory, model, execution, environment),
457
460
  executor: (trigger) => config.executors.includes(trigger.executor),
458
461
  // A deterministic step runs in the daemon's working directory — the
459
462
  // directory `flows run` was invoked from, or Cloud's code mount — not in
@@ -2,8 +2,8 @@ import { accessSync, constants } from 'node:fs';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { execFile, spawnSync } from 'node:child_process';
4
4
  import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
5
- import { adapterIdentification, authenticationProbe, cliAdapterKind, displayInvocation,
6
- modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
5
+ import { adapterIdentification, authenticationProbe, classifyModelProbeFailure, cliAdapterKind,
6
+ displayInvocation, modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
7
7
  import { MODEL_ENV } from '../worker-cli.js';
8
8
  import { CliProbeError, type CliProbeResult } from '../preflight.js';
9
9
 
@@ -27,8 +27,8 @@ export async function probeCliAsync(...args: Parameters<typeof probeSequence>):
27
27
  return next.value;
28
28
  }
29
29
 
30
- export function resolveExecutable(command: string, directory: string): string | undefined {
31
- return driveSync(executableSequence(command, directory));
30
+ export function resolveExecutable(command: string, directory: string, environment: NodeJS.ProcessEnv = process.env): string | undefined {
31
+ return driveSync(executableSequence(command, directory, environment));
32
32
  }
33
33
 
34
34
  function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
@@ -79,8 +79,9 @@ function* probeSequence(
79
79
  directory: string,
80
80
  model?: string,
81
81
  execution?: 'managed',
82
+ sourceEnvironment: NodeJS.ProcessEnv = process.env,
82
83
  ): Generator<ProbeRequest, CliProbeResult, ProbeOutput> {
83
- const executable = yield* executableSequence(cli, directory);
84
+ const executable = yield* executableSequence(cli, directory, sourceEnvironment);
84
85
  if (executable === undefined) return { exists: false, authenticated: false };
85
86
  const kind = cliAdapterKind(executable);
86
87
  // Relay owns interactive CLI launch/injection. Its generic PTY path is not
@@ -90,7 +91,8 @@ function* probeSequence(
90
91
  return { exists: true, supported: true, authenticated: 'unverified' };
91
92
  }
92
93
  const environment = execution === 'managed'
93
- ? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
94
+ ? { ...brokerEnvironment(sourceEnvironment), ...agentEnvironment(executable, sourceEnvironment) }
95
+ : sourceEnvironment;
94
96
  const probe = (invocation: CliInvocation): ProbeRequest => ({ executable, directory, invocation, environment });
95
97
  const identification = adapterIdentification(kind);
96
98
  const identified = yield probe(identification.invocation);
@@ -117,7 +119,8 @@ function* probeSequence(
117
119
  // A successful real provider round trip (or identified wrapper probe)
118
120
  // proves both auth and exact-model access. On failure, run the adapter's
119
121
  // actual auth command solely to classify auth vs model access truthfully.
120
- if ((yield probe(scoped)).status === 0) {
122
+ const scopedProbe = yield probe(scoped);
123
+ if (scopedProbe.status === 0) {
121
124
  return {
122
125
  exists: true,
123
126
  supported: true,
@@ -129,6 +132,14 @@ function* probeSequence(
129
132
  }
130
133
  const authProbe = yield probe(auth);
131
134
  const authenticated = authProbe.status === 0;
135
+ // The scoped probe's own output is the only evidence of WHY it failed. A
136
+ // usage-limited credential or an outdated CLI exits non-zero exactly like an
137
+ // unknown model; discarding the output made all three "verify the model
138
+ // name". Classified on the raw text, surfaced redacted — stdout first,
139
+ // because Claude Code prints the provider's API error there and its own
140
+ // catalog warnings on stderr.
141
+ const failure = classifyModelProbeFailure(kind, `${scopedProbe.stdout}\n${scopedProbe.stderr}`);
142
+ const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim()).slice(0, 500);
132
143
  return {
133
144
  exists: true,
134
145
  supported: true,
@@ -136,6 +147,9 @@ function* probeSequence(
136
147
  modelAvailable: false,
137
148
  authCommand,
138
149
  modelCommand,
150
+ modelExitCode: scopedProbe.status,
151
+ ...(failure === undefined ? {} : { modelFailure: failure }),
152
+ ...(modelDetail.length > 0 ? { modelFailureDetail: modelDetail } : {}),
139
153
  // Only on failure: on success there is nothing to explain, and the output
140
154
  // is the most identity-bearing thing this function touches.
141
155
  ...(authenticated
@@ -164,7 +178,7 @@ function redactProbeOutput(text: string): string {
164
178
  .replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
165
179
  }
166
180
 
167
- function* executableSequence(command: string, directory: string): Generator<ProbeRequest, string | undefined, ProbeOutput> {
181
+ function* executableSequence(command: string, directory: string, environment: NodeJS.ProcessEnv): Generator<ProbeRequest, string | undefined, ProbeOutput> {
168
182
  if (command.includes('/') || isAbsolute(command)) {
169
183
  const path = isAbsolute(command) ? command : resolve(directory, command);
170
184
  try {
@@ -175,7 +189,7 @@ function* executableSequence(command: string, directory: string): Generator<Prob
175
189
  }
176
190
  }
177
191
  const result = yield { executable: 'which', directory: process.cwd(),
178
- invocation: { args: [command], timeoutMs: 5_000 }, environment: process.env };
192
+ invocation: { args: [command], timeoutMs: 5_000 }, environment };
179
193
  return result.status === 0 ? result.stdout.trim() : undefined;
180
194
  }
181
195
 
@@ -6,6 +6,7 @@ import { McpPreflightError } from './check-typescript.js';
6
6
  import { attachLocalAgent } from '../local-agent.js';
7
7
  import { LlmWorker } from '../llm-worker.js';
8
8
  import { DEFAULT_LOCAL_AGENT_CAPACITY } from '../worker-slots.js';
9
+ import { localAgentEnvironment } from '../local-agent-environment.js';
9
10
  import {
10
11
  AuthoredFlowExecutionError,
11
12
  } from '../authored-flow-executor.js';
@@ -25,6 +26,7 @@ import {
25
26
  emptyReport,
26
27
  fromCheckReport,
27
28
  protocolFailure,
29
+ suspendedExecution,
28
30
  socketFor,
29
31
  type RunExecution,
30
32
  type RunLifecycleOptions,
@@ -37,6 +39,9 @@ export async function runDirectFlow(
37
39
  dataDir: string,
38
40
  options: RunLifecycleOptions = {},
39
41
  ): Promise<RunExecution> {
42
+ // Consume and close the agent-only descriptor before authored source is
43
+ // imported by trigger preflight. Authored code never receives this object.
44
+ const agentEnvironment = localAgentEnvironment();
40
45
  let input: unknown;
41
46
  try {
42
47
  input = parseDirectInput(inputArgument);
@@ -77,13 +82,14 @@ export async function runDirectFlow(
77
82
  if (options.localAgent) {
78
83
  localAgent = await attachLocalAgent(
79
84
  client, dataDir, options.onPtyReady, authoredLocalAgentStream(admissionIdentity), workerCapacity,
85
+ undefined, [], agentEnvironment,
80
86
  );
81
87
  // A session owns one worker registration. Keep the workspace-free LLM
82
88
  // worker on its own connection so it cannot replace the agent worker.
83
89
  llmClient = new JournalClient(socketPath);
84
90
  await llmClient.connect();
85
91
  await llmClient.hello('flows-local-llm');
86
- localLlm = new LlmWorker(llmClient, `${localAgent.stream}-llm`, workerCapacity);
92
+ localLlm = new LlmWorker(llmClient, `${localAgent.stream}-llm`, workerCapacity, agentEnvironment);
87
93
  localLlm.on('error', onWorkerFailure('local-llm', error => { llmFailure = error; client.close(); }));
88
94
  await localLlm.attach();
89
95
  }
@@ -92,8 +98,10 @@ export async function runDirectFlow(
92
98
  {
93
99
  dataDir,
94
100
  admissionKey: admissionIdentity,
101
+ onAdmitted: runId => { base.rootRunId = runId; },
95
102
  localAgentStream: localAgent?.stream,
96
103
  ...(localAgent === undefined ? {} : { workerCapacity }),
104
+ ...(agentEnvironment === undefined ? {} : { agentEnvironment }),
97
105
  lifecycle: {
98
106
  onProgress: options.onProgress,
99
107
  ...(options.onRunStarted !== undefined ? { onRunStarted: options.onRunStarted } : {}),
@@ -102,6 +110,9 @@ export async function runDirectFlow(
102
110
  },
103
111
  },
104
112
  );
113
+ if (result.state === 'suspended') {
114
+ return suspendedExecution('run', base, socketPath, result.rootRunId, result);
115
+ }
105
116
  const terminal = result.journalSteps.at(-1);
106
117
  if (terminal === undefined) {
107
118
  return protocolFailure('run', base, socketPath, new Error(
@@ -139,6 +150,8 @@ export async function runDirectFlow(
139
150
  && (error.code === 'helper_slack.credential_missing'
140
151
  || error.code === 'helper_slack.mount_required'
141
152
  || error.code === 'budget_syntax_invalid'
153
+ || error.code === 'cli_outdated'
154
+ || error.code === 'provider_usage_limited'
142
155
  || error.code === 'unsupported_promise_lifecycle'
143
156
  || error.code === 'unsupported_header'
144
157
  || error.code === 'agent_cli_unresolved'
@@ -152,6 +165,8 @@ export async function runDirectFlow(
152
165
  error.code === 'helper_slack.credential_missing'
153
166
  || error.code === 'helper_slack.mount_required'
154
167
  || error.code === 'budget_syntax_invalid'
168
+ || error.code === 'cli_outdated'
169
+ || error.code === 'provider_usage_limited'
155
170
  ) ? error.code : 'invalid_spec',
156
171
  message: error.message,
157
172
  }, path)),