@relayflows/sdk 2.0.0

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 (231) hide show
  1. package/dist/authored-flow-error.d.ts +9 -0
  2. package/dist/authored-flow-error.d.ts.map +1 -0
  3. package/dist/authored-flow-error.js +13 -0
  4. package/dist/authored-flow-error.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +28 -0
  6. package/dist/authored-flow-executor.d.ts.map +1 -0
  7. package/dist/authored-flow-executor.js +203 -0
  8. package/dist/authored-flow-executor.js.map +1 -0
  9. package/dist/authored-flow-lifecycle.d.ts +61 -0
  10. package/dist/authored-flow-lifecycle.d.ts.map +1 -0
  11. package/dist/authored-flow-lifecycle.js +284 -0
  12. package/dist/authored-flow-lifecycle.js.map +1 -0
  13. package/dist/authored-flow-loader.d.ts +7 -0
  14. package/dist/authored-flow-loader.d.ts.map +1 -0
  15. package/dist/authored-flow-loader.js +39 -0
  16. package/dist/authored-flow-loader.js.map +1 -0
  17. package/dist/authored-flow-operation.d.ts +43 -0
  18. package/dist/authored-flow-operation.d.ts.map +1 -0
  19. package/dist/authored-flow-operation.js +176 -0
  20. package/dist/authored-flow-operation.js.map +1 -0
  21. package/dist/authored-flow.d.ts +12 -0
  22. package/dist/authored-flow.d.ts.map +1 -0
  23. package/dist/authored-flow.js +13 -0
  24. package/dist/authored-flow.js.map +1 -0
  25. package/dist/authored-promise-graph.d.ts +67 -0
  26. package/dist/authored-promise-graph.d.ts.map +1 -0
  27. package/dist/authored-promise-graph.js +206 -0
  28. package/dist/authored-promise-graph.js.map +1 -0
  29. package/dist/backlog-picker.d.ts +55 -0
  30. package/dist/backlog-picker.d.ts.map +1 -0
  31. package/dist/backlog-picker.js +117 -0
  32. package/dist/backlog-picker.js.map +1 -0
  33. package/dist/canonical.d.ts +18 -0
  34. package/dist/canonical.d.ts.map +1 -0
  35. package/dist/canonical.js +55 -0
  36. package/dist/canonical.js.map +1 -0
  37. package/dist/cli/check.d.ts +30 -0
  38. package/dist/cli/check.d.ts.map +1 -0
  39. package/dist/cli/check.js +324 -0
  40. package/dist/cli/check.js.map +1 -0
  41. package/dist/cli/direct-run.d.ts +3 -0
  42. package/dist/cli/direct-run.d.ts.map +1 -0
  43. package/dist/cli/direct-run.js +69 -0
  44. package/dist/cli/direct-run.js.map +1 -0
  45. package/dist/cli/hn-monitor.d.ts +107 -0
  46. package/dist/cli/hn-monitor.d.ts.map +1 -0
  47. package/dist/cli/hn-monitor.js +220 -0
  48. package/dist/cli/hn-monitor.js.map +1 -0
  49. package/dist/cli/interruptible-sleep.d.ts +13 -0
  50. package/dist/cli/interruptible-sleep.d.ts.map +1 -0
  51. package/dist/cli/interruptible-sleep.js +31 -0
  52. package/dist/cli/interruptible-sleep.js.map +1 -0
  53. package/dist/cli/run.d.ts +54 -0
  54. package/dist/cli/run.d.ts.map +1 -0
  55. package/dist/cli/run.js +316 -0
  56. package/dist/cli/run.js.map +1 -0
  57. package/dist/cli/tick-runner.d.ts +151 -0
  58. package/dist/cli/tick-runner.d.ts.map +1 -0
  59. package/dist/cli/tick-runner.js +258 -0
  60. package/dist/cli/tick-runner.js.map +1 -0
  61. package/dist/cli-adapter.d.ts +29 -0
  62. package/dist/cli-adapter.d.ts.map +1 -0
  63. package/dist/cli-adapter.js +92 -0
  64. package/dist/cli-adapter.js.map +1 -0
  65. package/dist/cli-executable.d.ts +3 -0
  66. package/dist/cli-executable.d.ts.map +1 -0
  67. package/dist/cli-executable.js +10 -0
  68. package/dist/cli-executable.js.map +1 -0
  69. package/dist/cli.d.ts +9 -0
  70. package/dist/cli.d.ts.map +1 -0
  71. package/dist/cli.js +348 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/compile.d.ts +42 -0
  74. package/dist/compile.d.ts.map +1 -0
  75. package/dist/compile.js +515 -0
  76. package/dist/compile.js.map +1 -0
  77. package/dist/demo-hn-monitor.d.ts +2 -0
  78. package/dist/demo-hn-monitor.d.ts.map +1 -0
  79. package/dist/demo-hn-monitor.js +118 -0
  80. package/dist/demo-hn-monitor.js.map +1 -0
  81. package/dist/dir-watcher-poller.d.ts +73 -0
  82. package/dist/dir-watcher-poller.d.ts.map +1 -0
  83. package/dist/dir-watcher-poller.js +80 -0
  84. package/dist/dir-watcher-poller.js.map +1 -0
  85. package/dist/direct-input.d.ts +10 -0
  86. package/dist/direct-input.d.ts.map +1 -0
  87. package/dist/direct-input.js +61 -0
  88. package/dist/direct-input.js.map +1 -0
  89. package/dist/failure-kinds.d.ts +25 -0
  90. package/dist/failure-kinds.d.ts.map +1 -0
  91. package/dist/failure-kinds.js +64 -0
  92. package/dist/failure-kinds.js.map +1 -0
  93. package/dist/gate-contract.d.ts +24 -0
  94. package/dist/gate-contract.d.ts.map +1 -0
  95. package/dist/gate-contract.js +50 -0
  96. package/dist/gate-contract.js.map +1 -0
  97. package/dist/hn-poller.d.ts +50 -0
  98. package/dist/hn-poller.d.ts.map +1 -0
  99. package/dist/hn-poller.js +79 -0
  100. package/dist/hn-poller.js.map +1 -0
  101. package/dist/index.d.ts +21 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +30 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/journal-client.d.ts +122 -0
  106. package/dist/journal-client.d.ts.map +1 -0
  107. package/dist/journal-client.js +277 -0
  108. package/dist/journal-client.js.map +1 -0
  109. package/dist/json-schema-bound.d.ts +7 -0
  110. package/dist/json-schema-bound.d.ts.map +1 -0
  111. package/dist/json-schema-bound.js +353 -0
  112. package/dist/json-schema-bound.js.map +1 -0
  113. package/dist/json-schema.d.ts +5 -0
  114. package/dist/json-schema.d.ts.map +1 -0
  115. package/dist/json-schema.js +72 -0
  116. package/dist/json-schema.js.map +1 -0
  117. package/dist/json-value.d.ts +6 -0
  118. package/dist/json-value.d.ts.map +1 -0
  119. package/dist/json-value.js +94 -0
  120. package/dist/json-value.js.map +1 -0
  121. package/dist/model-name.d.ts +7 -0
  122. package/dist/model-name.d.ts.map +1 -0
  123. package/dist/model-name.js +21 -0
  124. package/dist/model-name.js.map +1 -0
  125. package/dist/output-schema.d.ts +8 -0
  126. package/dist/output-schema.d.ts.map +1 -0
  127. package/dist/output-schema.js +28 -0
  128. package/dist/output-schema.js.map +1 -0
  129. package/dist/preflight.d.ts +80 -0
  130. package/dist/preflight.d.ts.map +1 -0
  131. package/dist/preflight.js +361 -0
  132. package/dist/preflight.js.map +1 -0
  133. package/dist/protocol.d.ts +359 -0
  134. package/dist/protocol.d.ts.map +1 -0
  135. package/dist/protocol.js +15 -0
  136. package/dist/protocol.js.map +1 -0
  137. package/dist/spec.d.ts +294 -0
  138. package/dist/spec.d.ts.map +1 -0
  139. package/dist/spec.js +13 -0
  140. package/dist/spec.js.map +1 -0
  141. package/dist/step-dependencies.d.ts +2 -0
  142. package/dist/step-dependencies.d.ts.map +1 -0
  143. package/dist/step-dependencies.js +88 -0
  144. package/dist/step-dependencies.js.map +1 -0
  145. package/dist/step-fields.d.ts +17 -0
  146. package/dist/step-fields.d.ts.map +1 -0
  147. package/dist/step-fields.js +32 -0
  148. package/dist/step-fields.js.map +1 -0
  149. package/dist/tick-source.d.ts +205 -0
  150. package/dist/tick-source.d.ts.map +1 -0
  151. package/dist/tick-source.js +234 -0
  152. package/dist/tick-source.js.map +1 -0
  153. package/dist/unknown-keys.d.ts +7 -0
  154. package/dist/unknown-keys.d.ts.map +1 -0
  155. package/dist/unknown-keys.js +47 -0
  156. package/dist/unknown-keys.js.map +1 -0
  157. package/dist/validate.d.ts +7 -0
  158. package/dist/validate.d.ts.map +1 -0
  159. package/dist/validate.js +458 -0
  160. package/dist/validate.js.map +1 -0
  161. package/dist/work-package-consumer.d.ts +33 -0
  162. package/dist/work-package-consumer.d.ts.map +1 -0
  163. package/dist/work-package-consumer.js +44 -0
  164. package/dist/work-package-consumer.js.map +1 -0
  165. package/dist/work-package-validator.d.ts +15 -0
  166. package/dist/work-package-validator.d.ts.map +1 -0
  167. package/dist/work-package-validator.js +72 -0
  168. package/dist/work-package-validator.js.map +1 -0
  169. package/dist/worker-cli.d.ts +16 -0
  170. package/dist/worker-cli.d.ts.map +1 -0
  171. package/dist/worker-cli.js +77 -0
  172. package/dist/worker-cli.js.map +1 -0
  173. package/dist/worker.d.ts +57 -0
  174. package/dist/worker.d.ts.map +1 -0
  175. package/dist/worker.js +129 -0
  176. package/dist/worker.js.map +1 -0
  177. package/dist/wrapper-runtime.d.ts +10 -0
  178. package/dist/wrapper-runtime.d.ts.map +1 -0
  179. package/dist/wrapper-runtime.js +83 -0
  180. package/dist/wrapper-runtime.js.map +1 -0
  181. package/dist/wrapper-session.d.ts +16 -0
  182. package/dist/wrapper-session.d.ts.map +1 -0
  183. package/dist/wrapper-session.js +244 -0
  184. package/dist/wrapper-session.js.map +1 -0
  185. package/package.json +50 -0
  186. package/src/authored-flow-error.ts +31 -0
  187. package/src/authored-flow-executor.ts +365 -0
  188. package/src/authored-flow-lifecycle.ts +344 -0
  189. package/src/authored-flow-loader.ts +44 -0
  190. package/src/authored-flow-operation.ts +252 -0
  191. package/src/authored-flow.ts +21 -0
  192. package/src/authored-promise-graph.ts +196 -0
  193. package/src/backlog-picker.ts +153 -0
  194. package/src/canonical.ts +57 -0
  195. package/src/cli/check.ts +391 -0
  196. package/src/cli/direct-run.ts +86 -0
  197. package/src/cli/hn-monitor.ts +286 -0
  198. package/src/cli/interruptible-sleep.ts +27 -0
  199. package/src/cli/run.ts +439 -0
  200. package/src/cli/tick-runner.ts +378 -0
  201. package/src/cli-adapter.ts +115 -0
  202. package/src/cli-executable.ts +12 -0
  203. package/src/cli.ts +364 -0
  204. package/src/compile.ts +573 -0
  205. package/src/demo-hn-monitor.ts +132 -0
  206. package/src/dir-watcher-poller.ts +122 -0
  207. package/src/direct-input.ts +69 -0
  208. package/src/failure-kinds.ts +77 -0
  209. package/src/gate-contract.ts +72 -0
  210. package/src/hn-poller.ts +104 -0
  211. package/src/index.ts +195 -0
  212. package/src/journal-client.ts +379 -0
  213. package/src/json-schema-bound.ts +357 -0
  214. package/src/json-schema.ts +65 -0
  215. package/src/json-value.ts +110 -0
  216. package/src/model-name.ts +20 -0
  217. package/src/output-schema.ts +33 -0
  218. package/src/preflight.ts +482 -0
  219. package/src/protocol.ts +364 -0
  220. package/src/spec.ts +343 -0
  221. package/src/step-dependencies.ts +100 -0
  222. package/src/step-fields.ts +36 -0
  223. package/src/tick-source.ts +334 -0
  224. package/src/unknown-keys.ts +52 -0
  225. package/src/validate.ts +503 -0
  226. package/src/work-package-consumer.ts +73 -0
  227. package/src/work-package-validator.ts +90 -0
  228. package/src/worker-cli.ts +110 -0
  229. package/src/worker.ts +150 -0
  230. package/src/wrapper-runtime.ts +93 -0
  231. package/src/wrapper-session.ts +287 -0
package/src/cli/run.ts ADDED
@@ -0,0 +1,439 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { toKernelSpec } from '../compile.js';
3
+ import type { RunFailureKind } from '../failure-kinds.js';
4
+ import { JournalClient, JournalProtocolError } from '../journal-client.js';
5
+ import type { PreflightDiagnostic } from '../preflight.js';
6
+ import type {
7
+ RunCompletionReason,
8
+ RunOutcome,
9
+ RunStatus,
10
+ } from '../protocol.js';
11
+ import type { StepType } from '../spec.js';
12
+ import {
13
+ checkFlow,
14
+ type CheckInputDiagnostic,
15
+ type CheckReport,
16
+ } from './check.js';
17
+
18
+ export type RunExitCode = 0 | 1 | 2 | 3;
19
+ export type RunCommand = 'run' | 'resume';
20
+
21
+ export interface ParkedStep {
22
+ id: string;
23
+ type: Extract<StepType, 'llm' | 'agent'>;
24
+ }
25
+
26
+ export interface RunDiagnostic {
27
+ severity: 'refusal' | 'failure' | 'parked';
28
+ kind: RunFailureKind | RunCompletionReason;
29
+ message: string;
30
+ }
31
+
32
+ export interface RunReport {
33
+ ok: boolean;
34
+ command: RunCommand;
35
+ path?: string;
36
+ runId?: string;
37
+ socketPath?: string;
38
+ status?: RunStatus;
39
+ completionReason?: RunCompletionReason;
40
+ completedSteps?: number;
41
+ parkedStep?: ParkedStep;
42
+ projectConfigPath?: string;
43
+ resolutions: CheckReport['resolutions'];
44
+ diagnostics: Array<PreflightDiagnostic | CheckInputDiagnostic | RunDiagnostic>;
45
+ }
46
+
47
+ export interface RunExecution {
48
+ exitCode: RunExitCode;
49
+ report: RunReport;
50
+ }
51
+
52
+ export interface RunProgress {
53
+ runId: string;
54
+ stepId: string;
55
+ stepType: Extract<StepType, 'llm' | 'agent'>;
56
+ leaseDeadlineMs: number;
57
+ }
58
+
59
+ export interface RunLifecycleOptions {
60
+ signal?: AbortSignal;
61
+ onWait?: (progress: RunProgress) => void;
62
+ }
63
+
64
+ export async function runFlow(
65
+ path: string,
66
+ dataDir: string,
67
+ options: RunLifecycleOptions = {},
68
+ ): Promise<RunExecution> {
69
+ const checked = checkFlow(path);
70
+ if (!checked.report.ok || checked.flow === undefined) {
71
+ return { exitCode: 2, report: fromCheckReport('run', checked.report) };
72
+ }
73
+
74
+ return executeCheckedFlow(checked, dataDir, options);
75
+ }
76
+
77
+ async function executeCheckedFlow(
78
+ checked: ReturnType<typeof checkFlow>,
79
+ dataDir: string,
80
+ options: RunLifecycleOptions,
81
+ ): Promise<RunExecution> {
82
+ const socketPath = socketFor(dataDir);
83
+ const client = new JournalClient(socketPath);
84
+ const connected = await connect(client, 'run', dataDir, checked.report);
85
+ if (connected !== undefined) return connected;
86
+
87
+ try {
88
+ const spec = toKernelSpec(checked.flow!);
89
+ const outcome = await client.runStart(spec);
90
+ return await classifyOutcome(client, 'run', outcome, checked.report, socketPath, options);
91
+ } catch (error) {
92
+ return protocolFailure('run', checked.report, socketPath, error);
93
+ } finally {
94
+ client.close();
95
+ }
96
+ }
97
+
98
+ export async function resumeFlow(
99
+ runId: string,
100
+ dataDir: string,
101
+ options: RunLifecycleOptions = {},
102
+ ): Promise<RunExecution> {
103
+ const socketPath = socketFor(dataDir);
104
+ const base = emptyReport('resume');
105
+ const client = new JournalClient(socketPath);
106
+ const connected = await connect(client, 'resume', dataDir, base);
107
+ if (connected !== undefined) return connected;
108
+
109
+ try {
110
+ const outcome = await client.runResume(runId);
111
+ return await classifyOutcome(client, 'resume', outcome, base, socketPath, options);
112
+ } catch (error) {
113
+ if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
114
+ return protocolFailure('resume', base, socketPath, error, runId);
115
+ }
116
+ return {
117
+ exitCode: 2,
118
+ report: {
119
+ ...base,
120
+ runId,
121
+ socketPath,
122
+ diagnostics: [{
123
+ severity: 'refusal',
124
+ kind: 'run_unavailable',
125
+ message: `Run "${runId}" could not be resumed by relayflowd at "${socketPath}": ${errorMessage(error)}`,
126
+ }],
127
+ },
128
+ };
129
+ } finally {
130
+ client.close();
131
+ }
132
+ }
133
+
134
+ export async function connect(
135
+ client: JournalClient,
136
+ command: RunCommand,
137
+ dataDir: string,
138
+ base: CheckReport | RunReport,
139
+ ): Promise<RunExecution | undefined> {
140
+ const socketPath = socketFor(dataDir);
141
+ try {
142
+ await client.connect();
143
+ } catch {
144
+ client.close();
145
+ return {
146
+ exitCode: 2,
147
+ report: {
148
+ ...fromBase(command, base),
149
+ socketPath,
150
+ diagnostics: [
151
+ ...base.diagnostics,
152
+ {
153
+ severity: 'refusal',
154
+ kind: 'daemon_unreachable',
155
+ message: `No compatible relayflowd is listening at "${socketPath}". Start it with: relayflowd --data-dir ${JSON.stringify(dataDir)} serve`,
156
+ },
157
+ ],
158
+ },
159
+ };
160
+ }
161
+ try {
162
+ await client.hello(`flows-${command}`);
163
+ return undefined;
164
+ } catch (error) {
165
+ client.close();
166
+ return protocolFailure(command, base, socketPath, error);
167
+ }
168
+ }
169
+
170
+ /// Exported for tests. The `running`-with-no-identifiable-step branch (#179)
171
+ /// only occurs in a sub-second window against a live daemon, so pinning it
172
+ /// needs a stubbed client rather than a real run -- the integration test that
173
+ /// found it reproduced the bug roughly 1 time in 12.
174
+ export async function classifyOutcome(
175
+ client: JournalClient,
176
+ command: RunCommand,
177
+ outcome: RunOutcome,
178
+ base: CheckReport | RunReport,
179
+ socketPath: string,
180
+ options: RunLifecycleOptions,
181
+ ): Promise<RunExecution> {
182
+ let current = outcome;
183
+ let parkedStep: ParkedStep | undefined;
184
+ let needsHuman = false;
185
+ let unclassifiedPolls = 0;
186
+ while (current.status === 'parked') {
187
+ const inspection = await inspectOutOfBandStep(client, current.run_id);
188
+ if (inspection?.parkedStep !== undefined) {
189
+ parkedStep = inspection.parkedStep;
190
+ needsHuman = inspection.needsHuman;
191
+ break;
192
+ }
193
+ if (inspection?.runningStep !== undefined) {
194
+ await waitForRunningStep(client, current.run_id, inspection.runningStep, options);
195
+ current = await client.runResume(current.run_id);
196
+ continue;
197
+ }
198
+ if (inspection?.status === 'completed' || inspection?.status === 'failed') {
199
+ current = await client.runResume(current.run_id);
200
+ continue;
201
+ }
202
+ // The run is still RUNNING but no step is identifiable at this instant.
203
+ //
204
+ // That is a healthy state, not a protocol error. It happens when a worker
205
+ // has just completed the step this run parked on and the daemon has not yet
206
+ // finished driving what follows: nothing is `needs_human`, `runnable` or
207
+ // `running` for a moment, while the snapshot's own status is `running`.
208
+ // Breaking here left `status === 'parked'` with no `parkedStep`, so the
209
+ // tail reported `parked without a classifiable completion` -- a spurious
210
+ // failure on a run that was about to succeed (#179). Reproduced 1 in 4-15
211
+ // locally; the probe that caught it printed
212
+ // `inspection={"status":"running","needsHuman":false}`.
213
+ //
214
+ // So poll it, bounded. Resuming immediately would spin, since the daemon
215
+ // needs a moment to advance.
216
+ if (inspection?.status === 'running' && unclassifiedPolls < MAX_UNCLASSIFIED_POLLS) {
217
+ unclassifiedPolls += 1;
218
+ // `delay(ms, signal)`, not a bare setTimeout: every other wait in this
219
+ // file is cancel-aware, and an uncancellable one here would keep polling
220
+ // the daemon for up to 2s after a Ctrl-C or a lifecycle abort.
221
+ throwIfCanceled(options.signal, current.run_id);
222
+ await delay(UNCLASSIFIED_POLL_MS, options.signal);
223
+ // Redundant resumes are safe: `run.resume` is idempotent on a run that
224
+ // is already progressing -- it returns the current state rather than
225
+ // re-dispatching. This loop leans on that up to MAX_UNCLASSIFIED_POLLS
226
+ // times while the daemon is mid-transition.
227
+ current = await client.runResume(current.run_id);
228
+ continue;
229
+ }
230
+ // Fail closed rather than loop forever: if it never resolves, the original
231
+ // error below still fires and says so.
232
+ break;
233
+ }
234
+
235
+ const report: RunReport = {
236
+ ...fromBase(command, base),
237
+ ok: current.status === 'completed' && current.completion_reason === 'success',
238
+ runId: current.run_id,
239
+ socketPath,
240
+ status: current.status,
241
+ ...(current.completion_reason !== null ? { completionReason: current.completion_reason } : {}),
242
+ completedSteps: current.completed_steps,
243
+ };
244
+
245
+ if (report.ok) return { exitCode: 0, report };
246
+ if (current.status === 'failed' && current.completion_reason !== null) {
247
+ return {
248
+ exitCode: 1,
249
+ report: {
250
+ ...report,
251
+ diagnostics: [...report.diagnostics, {
252
+ severity: 'failure',
253
+ kind: current.completion_reason,
254
+ message: `Run "${current.run_id}" failed with completionReason: ${current.completion_reason}.`,
255
+ }],
256
+ },
257
+ };
258
+ }
259
+ if (current.status === 'parked' && parkedStep !== undefined) {
260
+ return {
261
+ exitCode: 3,
262
+ report: {
263
+ ...report,
264
+ parkedStep,
265
+ diagnostics: [...report.diagnostics, {
266
+ severity: 'parked',
267
+ kind: 'run_parked',
268
+ message: needsHuman
269
+ ? `Run "${current.run_id}" parked at step "${parkedStep.id}" (${parkedStep.type}): waiting for human recovery after the worker attempt failed.`
270
+ : `Run "${current.run_id}" parked at step "${parkedStep.id}" (${parkedStep.type}): no worker is attached for step type "${parkedStep.type}".`,
271
+ }],
272
+ },
273
+ };
274
+ }
275
+ return protocolFailure(command, base, socketPath, new Error(
276
+ `relayflowd returned status ${current.status} without a classifiable completion`,
277
+ ), current.run_id);
278
+ }
279
+
280
+ interface OutOfBandInspection {
281
+ status: RunStatus;
282
+ parkedStep?: ParkedStep;
283
+ needsHuman: boolean;
284
+ runningStep?: RunningStep;
285
+ }
286
+
287
+ interface RunningStep extends ParkedStep {
288
+ leaseDeadlineMs: number;
289
+ }
290
+
291
+ // Bound on re-polling a run that reports `running` with no identifiable step.
292
+ // 40 x 50ms = 2s, far longer than the sub-second window observed in #179, and
293
+ // short enough that a genuinely stuck run still reports rather than hangs.
294
+ const MAX_UNCLASSIFIED_POLLS = 40;
295
+ const UNCLASSIFIED_POLL_MS = 50;
296
+
297
+ async function inspectOutOfBandStep(
298
+ client: JournalClient,
299
+ runId: string,
300
+ ): Promise<OutOfBandInspection | undefined> {
301
+ const snapshot = await client.runGet(runId);
302
+ const entries = Object.entries(snapshot.steps);
303
+ const humanEntry = entries.find(([, step]) =>
304
+ step.type !== 'deterministic' && step.state === 'needs_human',
305
+ );
306
+ const runnableEntry = entries.find(([, step]) =>
307
+ step.type !== 'deterministic' && step.state === 'runnable',
308
+ );
309
+ const runningEntry = entries.find(([, step]) =>
310
+ step.type !== 'deterministic' && step.state === 'running',
311
+ );
312
+ const parkedEntry = humanEntry ?? runnableEntry;
313
+ const parkedStep = parkedEntry === undefined ? undefined : {
314
+ id: parkedEntry[0],
315
+ type: parkedEntry[1].type as Extract<StepType, 'llm' | 'agent'>,
316
+ };
317
+ const runningStep = runningEntry === undefined ? undefined : {
318
+ id: runningEntry[0],
319
+ type: runningEntry[1].type as Extract<StepType, 'llm' | 'agent'>,
320
+ leaseDeadlineMs: runningEntry[1].lease_deadline_ms ?? Number.NaN,
321
+ };
322
+ return {
323
+ status: snapshot.status,
324
+ needsHuman: humanEntry !== undefined,
325
+ ...(parkedStep !== undefined ? { parkedStep } : {}),
326
+ ...(runningStep !== undefined ? { runningStep } : {}),
327
+ };
328
+ }
329
+
330
+ async function waitForRunningStep(
331
+ client: JournalClient,
332
+ runId: string,
333
+ runningStep: RunningStep,
334
+ options: RunLifecycleOptions,
335
+ ): Promise<void> {
336
+ let leaseDeadlineMs = runningStep.leaseDeadlineMs;
337
+ if (!Number.isFinite(leaseDeadlineMs)) {
338
+ throw new Error(`running step "${runningStep.id}" omitted lease_deadline_ms`);
339
+ }
340
+ options.onWait?.({
341
+ runId,
342
+ stepId: runningStep.id,
343
+ stepType: runningStep.type,
344
+ leaseDeadlineMs,
345
+ });
346
+ while (true) {
347
+ throwIfCanceled(options.signal, runningStep.id);
348
+ const remainingMs = leaseDeadlineMs - Date.now();
349
+ if (remainingMs <= 0) {
350
+ throw new Error(
351
+ `worker lease for step "${runningStep.id}" expired at ${leaseDeadlineMs} without completion`,
352
+ );
353
+ }
354
+ await delay(Math.min(50, remainingMs), options.signal);
355
+ const snapshot = await client.runGet(runId);
356
+ const step = snapshot.steps[runningStep.id];
357
+ if (step?.state !== 'running') return;
358
+ if (step.lease_deadline_ms !== undefined && step.lease_deadline_ms !== leaseDeadlineMs) {
359
+ leaseDeadlineMs = step.lease_deadline_ms;
360
+ options.onWait?.({
361
+ runId,
362
+ stepId: runningStep.id,
363
+ stepType: runningStep.type,
364
+ leaseDeadlineMs,
365
+ });
366
+ }
367
+ }
368
+ }
369
+
370
+ export function protocolFailure(
371
+ command: RunCommand,
372
+ base: CheckReport | RunReport,
373
+ socketPath: string,
374
+ error: unknown,
375
+ runId?: string,
376
+ ): RunExecution {
377
+ return {
378
+ exitCode: 1,
379
+ report: {
380
+ ...fromBase(command, base),
381
+ ...(runId !== undefined ? { runId } : {}),
382
+ socketPath,
383
+ diagnostics: [...base.diagnostics, {
384
+ severity: 'failure',
385
+ kind: 'protocol_error',
386
+ message: `relayflowd could not complete the ${command} request: ${errorMessage(error)}`,
387
+ }],
388
+ },
389
+ };
390
+ }
391
+
392
+ export function fromCheckReport(command: RunCommand, report: CheckReport): RunReport {
393
+ return {
394
+ ok: false,
395
+ command,
396
+ ...(report.path !== undefined ? { path: report.path } : {}),
397
+ ...(report.projectConfigPath !== undefined ? { projectConfigPath: report.projectConfigPath } : {}),
398
+ resolutions: report.resolutions,
399
+ diagnostics: report.diagnostics,
400
+ };
401
+ }
402
+
403
+ export function emptyReport(command: RunCommand): RunReport {
404
+ return { ok: false, command, resolutions: [], diagnostics: [] };
405
+ }
406
+
407
+ function fromBase(command: RunCommand, base: CheckReport | RunReport): RunReport {
408
+ return 'command' in base ? base : fromCheckReport(command, base);
409
+ }
410
+
411
+ export function socketFor(dataDir: string): string {
412
+ return join(resolve(dataDir), 'relayflowd.sock');
413
+ }
414
+
415
+ function errorMessage(error: unknown): string {
416
+ return error instanceof Error ? error.message : 'unknown protocol error';
417
+ }
418
+
419
+ function throwIfCanceled(signal: AbortSignal | undefined, stepId: string): void {
420
+ if (signal?.aborted === true) throw new Error(`waiting for running step "${stepId}" was canceled`);
421
+ }
422
+
423
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
424
+ return new Promise((resolveDelay, rejectDelay) => {
425
+ const finish = (): void => {
426
+ signal?.removeEventListener('abort', cancel);
427
+ resolveDelay();
428
+ };
429
+ const timer = setTimeout(finish, ms);
430
+ if (signal === undefined) return;
431
+ const cancel = (): void => {
432
+ clearTimeout(timer);
433
+ signal.removeEventListener('abort', cancel);
434
+ rejectDelay(new Error('worker wait canceled'));
435
+ };
436
+ if (signal.aborted) cancel();
437
+ else signal.addEventListener('abort', cancel, { once: true });
438
+ });
439
+ }