@relayflows/sdk 2.0.36 → 2.0.38

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 (230) hide show
  1. package/dist/agent-reaper.d.ts +1 -1
  2. package/dist/agent-reaper.d.ts.map +1 -1
  3. package/dist/agent-reaper.js +6 -4
  4. package/dist/agent-reaper.js.map +1 -1
  5. package/dist/authored-budget.d.ts +26 -1
  6. package/dist/authored-budget.d.ts.map +1 -1
  7. package/dist/authored-budget.js +72 -2
  8. package/dist/authored-budget.js.map +1 -1
  9. package/dist/authored-flow-error.d.ts +7 -1
  10. package/dist/authored-flow-error.d.ts.map +1 -1
  11. package/dist/authored-flow-error.js.map +1 -1
  12. package/dist/authored-flow-executor.d.ts +2 -0
  13. package/dist/authored-flow-executor.d.ts.map +1 -1
  14. package/dist/authored-flow-executor.js +2 -1
  15. package/dist/authored-flow-executor.js.map +1 -1
  16. package/dist/authored-helper-effect.d.ts.map +1 -1
  17. package/dist/authored-helper-effect.js +3 -2
  18. package/dist/authored-helper-effect.js.map +1 -1
  19. package/dist/authored-mcp.d.ts.map +1 -1
  20. package/dist/authored-mcp.js +2 -0
  21. package/dist/authored-mcp.js.map +1 -1
  22. package/dist/authored-node-entry.js +6 -0
  23. package/dist/authored-node-entry.js.map +1 -1
  24. package/dist/authored-node-runner.d.ts.map +1 -1
  25. package/dist/authored-node-runner.js +5 -3
  26. package/dist/authored-node-runner.js.map +1 -1
  27. package/dist/authored-plugin-effect.d.ts.map +1 -1
  28. package/dist/authored-plugin-effect.js +2 -0
  29. package/dist/authored-plugin-effect.js.map +1 -1
  30. package/dist/authored-preflight.d.ts +1 -1
  31. package/dist/authored-preflight.d.ts.map +1 -1
  32. package/dist/authored-preflight.js +2 -2
  33. package/dist/authored-preflight.js.map +1 -1
  34. package/dist/authored-root.d.ts +2 -0
  35. package/dist/authored-root.d.ts.map +1 -1
  36. package/dist/authored-root.js +146 -6
  37. package/dist/authored-root.js.map +1 -1
  38. package/dist/authored-verdict.d.ts +2 -0
  39. package/dist/authored-verdict.d.ts.map +1 -1
  40. package/dist/authored-verdict.js +2 -1
  41. package/dist/authored-verdict.js.map +1 -1
  42. package/dist/authored-worker-step.d.ts +1 -1
  43. package/dist/authored-worker-step.d.ts.map +1 -1
  44. package/dist/authored-worker-step.js +3 -2
  45. package/dist/authored-worker-step.js.map +1 -1
  46. package/dist/child-stop.d.ts +3 -1
  47. package/dist/child-stop.d.ts.map +1 -1
  48. package/dist/child-stop.js +42 -2
  49. package/dist/child-stop.js.map +1 -1
  50. package/dist/cli/add-extension.js +1 -1
  51. package/dist/cli/add-extension.js.map +1 -1
  52. package/dist/cli/check.d.ts +2 -0
  53. package/dist/cli/check.d.ts.map +1 -1
  54. package/dist/cli/check.js +28 -10
  55. package/dist/cli/check.js.map +1 -1
  56. package/dist/cli/cli-probe.d.ts +3 -2
  57. package/dist/cli/cli-probe.d.ts.map +1 -1
  58. package/dist/cli/cli-probe.js +79 -42
  59. package/dist/cli/cli-probe.js.map +1 -1
  60. package/dist/cli/direct-run.d.ts.map +1 -1
  61. package/dist/cli/direct-run.js +11 -3
  62. package/dist/cli/direct-run.js.map +1 -1
  63. package/dist/cli/pinned-cli-alias.d.ts +13 -0
  64. package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
  65. package/dist/cli/pinned-cli-alias.js +58 -0
  66. package/dist/cli/pinned-cli-alias.js.map +1 -0
  67. package/dist/cli/run.d.ts +8 -0
  68. package/dist/cli/run.d.ts.map +1 -1
  69. package/dist/cli/run.js +106 -17
  70. package/dist/cli/run.js.map +1 -1
  71. package/dist/cli/step-failure.d.ts.map +1 -1
  72. package/dist/cli/step-failure.js +18 -0
  73. package/dist/cli/step-failure.js.map +1 -1
  74. package/dist/cloud-run.d.ts.map +1 -1
  75. package/dist/cloud-run.js +9 -2
  76. package/dist/cloud-run.js.map +1 -1
  77. package/dist/communication/local.d.ts +1 -1
  78. package/dist/communication/local.d.ts.map +1 -1
  79. package/dist/communication/local.js +2 -2
  80. package/dist/communication/local.js.map +1 -1
  81. package/dist/communication/worker.d.ts +1 -1
  82. package/dist/communication/worker.d.ts.map +1 -1
  83. package/dist/communication/worker.js +27 -9
  84. package/dist/communication/worker.js.map +1 -1
  85. package/dist/compile.d.ts.map +1 -1
  86. package/dist/compile.js +16 -7
  87. package/dist/compile.js.map +1 -1
  88. package/dist/decimal-dollars.d.ts +12 -0
  89. package/dist/decimal-dollars.d.ts.map +1 -0
  90. package/dist/decimal-dollars.js +39 -0
  91. package/dist/decimal-dollars.js.map +1 -0
  92. package/dist/effect-channel.d.ts.map +1 -1
  93. package/dist/effect-channel.js +2 -0
  94. package/dist/effect-channel.js.map +1 -1
  95. package/dist/failure-kinds.d.ts +9 -1
  96. package/dist/failure-kinds.d.ts.map +1 -1
  97. package/dist/failure-kinds.js +6 -0
  98. package/dist/failure-kinds.js.map +1 -1
  99. package/dist/flow-extension-loader.d.ts +1 -1
  100. package/dist/flow-extension-loader.d.ts.map +1 -1
  101. package/dist/flow-extension-loader.js +143 -14
  102. package/dist/flow-extension-loader.js.map +1 -1
  103. package/dist/flow-extension-manifest.d.ts +1 -0
  104. package/dist/flow-extension-manifest.d.ts.map +1 -1
  105. package/dist/flow-extension-manifest.js +12 -3
  106. package/dist/flow-extension-manifest.js.map +1 -1
  107. package/dist/helper-instruction.d.ts +3 -0
  108. package/dist/helper-instruction.d.ts.map +1 -0
  109. package/dist/helper-instruction.js +3 -0
  110. package/dist/helper-instruction.js.map +1 -0
  111. package/dist/hosted-extension-manifest.d.ts.map +1 -1
  112. package/dist/hosted-extension-manifest.js +25 -4
  113. package/dist/hosted-extension-manifest.js.map +1 -1
  114. package/dist/hosted-extension-runtime.js +1 -1
  115. package/dist/hosted-extension-sandbox.js +2 -2
  116. package/dist/journal-client.d.ts +5 -2
  117. package/dist/journal-client.d.ts.map +1 -1
  118. package/dist/journal-client.js +12 -4
  119. package/dist/journal-client.js.map +1 -1
  120. package/dist/llm-worker.d.ts +2 -1
  121. package/dist/llm-worker.d.ts.map +1 -1
  122. package/dist/llm-worker.js +10 -3
  123. package/dist/llm-worker.js.map +1 -1
  124. package/dist/local-agent-environment.d.ts +13 -0
  125. package/dist/local-agent-environment.d.ts.map +1 -0
  126. package/dist/local-agent-environment.js +104 -0
  127. package/dist/local-agent-environment.js.map +1 -0
  128. package/dist/local-agent.d.ts +1 -1
  129. package/dist/local-agent.d.ts.map +1 -1
  130. package/dist/local-agent.js +2 -2
  131. package/dist/local-agent.js.map +1 -1
  132. package/dist/mcp-stdio.d.ts +2 -1
  133. package/dist/mcp-stdio.d.ts.map +1 -1
  134. package/dist/mcp-stdio.js +38 -21
  135. package/dist/mcp-stdio.js.map +1 -1
  136. package/dist/preflight.d.ts +2 -0
  137. package/dist/preflight.d.ts.map +1 -1
  138. package/dist/preflight.js.map +1 -1
  139. package/dist/protocol.d.ts +30 -0
  140. package/dist/protocol.d.ts.map +1 -1
  141. package/dist/reported-cost-total.d.ts +41 -0
  142. package/dist/reported-cost-total.d.ts.map +1 -0
  143. package/dist/reported-cost-total.js +63 -0
  144. package/dist/reported-cost-total.js.map +1 -0
  145. package/dist/reported-cost.d.ts +24 -0
  146. package/dist/reported-cost.d.ts.map +1 -0
  147. package/dist/reported-cost.js +49 -0
  148. package/dist/reported-cost.js.map +1 -0
  149. package/dist/resolved-cli-identity.d.ts +14 -0
  150. package/dist/resolved-cli-identity.d.ts.map +1 -0
  151. package/dist/resolved-cli-identity.js +19 -0
  152. package/dist/resolved-cli-identity.js.map +1 -0
  153. package/dist/run-state.d.ts +9 -11
  154. package/dist/run-state.d.ts.map +1 -1
  155. package/dist/run-state.js +15 -38
  156. package/dist/run-state.js.map +1 -1
  157. package/dist/worker-cli.d.ts +3 -1
  158. package/dist/worker-cli.d.ts.map +1 -1
  159. package/dist/worker-cli.js +118 -42
  160. package/dist/worker-cli.js.map +1 -1
  161. package/dist/worker-lease.d.ts +3 -1
  162. package/dist/worker-lease.d.ts.map +1 -1
  163. package/dist/worker-lease.js +47 -9
  164. package/dist/worker-lease.js.map +1 -1
  165. package/dist/worker.d.ts +2 -0
  166. package/dist/worker.d.ts.map +1 -1
  167. package/dist/worker.js +8 -3
  168. package/dist/worker.js.map +1 -1
  169. package/dist/wrapper-session.d.ts +3 -1
  170. package/dist/wrapper-session.d.ts.map +1 -1
  171. package/dist/wrapper-session.js +93 -53
  172. package/dist/wrapper-session.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/agent-reaper.ts +7 -4
  181. package/src/authored-budget.ts +73 -1
  182. package/src/authored-flow-error.ts +6 -0
  183. package/src/authored-flow-executor.ts +4 -1
  184. package/src/authored-helper-effect.ts +3 -2
  185. package/src/authored-mcp.ts +2 -0
  186. package/src/authored-node-entry.ts +8 -1
  187. package/src/authored-node-runner.ts +5 -3
  188. package/src/authored-plugin-effect.ts +2 -0
  189. package/src/authored-preflight.ts +2 -2
  190. package/src/authored-root.ts +157 -7
  191. package/src/authored-verdict.ts +2 -1
  192. package/src/authored-worker-step.ts +3 -1
  193. package/src/child-stop.ts +40 -1
  194. package/src/cli/add-extension.ts +1 -1
  195. package/src/cli/check.ts +31 -7
  196. package/src/cli/cli-probe.ts +80 -42
  197. package/src/cli/direct-run.ts +11 -2
  198. package/src/cli/pinned-cli-alias.ts +74 -0
  199. package/src/cli/run.ts +118 -14
  200. package/src/cli/step-failure.ts +18 -0
  201. package/src/cloud-run.ts +9 -2
  202. package/src/communication/local.ts +3 -2
  203. package/src/communication/worker.ts +29 -11
  204. package/src/compile.ts +28 -13
  205. package/src/decimal-dollars.ts +38 -0
  206. package/src/effect-channel.ts +2 -0
  207. package/src/failure-kinds.ts +14 -0
  208. package/src/flow-extension-loader.ts +154 -14
  209. package/src/flow-extension-manifest.ts +6 -3
  210. package/src/helper-instruction.ts +2 -0
  211. package/src/hosted-extension-manifest.ts +28 -5
  212. package/src/hosted-extension-runtime.ts +1 -1
  213. package/src/hosted-extension-sandbox.ts +2 -2
  214. package/src/journal-client.ts +14 -5
  215. package/src/llm-worker.ts +11 -4
  216. package/src/local-agent-environment.ts +92 -0
  217. package/src/local-agent.ts +2 -1
  218. package/src/mcp-stdio.ts +34 -20
  219. package/src/preflight.ts +2 -0
  220. package/src/protocol.ts +31 -0
  221. package/src/reported-cost-total.ts +82 -0
  222. package/src/reported-cost.ts +51 -0
  223. package/src/resolved-cli-identity.ts +42 -0
  224. package/src/run-state.ts +25 -37
  225. package/src/worker-cli.ts +126 -39
  226. package/src/worker-lease.ts +50 -9
  227. package/src/worker.ts +14 -5
  228. package/src/wrapper-session.ts +89 -56
  229. package/src/yaml-helper-effect.ts +2 -1
  230. package/src/yaml-helpers.ts +2 -1
@@ -1,5 +1,6 @@
1
- import { spawn } from 'node:child_process';
2
- import { FORCE_KILL_DELAY_MS, childStop, ownsProcessGroup } from './child-stop.js';
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { childStop, ownsProcessGroup } from './child-stop.js';
3
+ import { reapOnExit } from './agent-reaper.js';
3
4
  import {
4
5
  WRAPPER_EXECUTE_TOKEN,
5
6
  WRAPPER_IDENTIFY_ARG,
@@ -10,6 +11,8 @@ import {
10
11
  sameWrapperIdentity,
11
12
  type WrapperIdentity,
12
13
  } from './wrapper-runtime.js';
14
+ import { basename } from 'node:path';
15
+ import { pinCliAlias } from './cli/pinned-cli-alias.js';
13
16
 
14
17
  export interface WrapperSessionLimits {
15
18
  handshakeTimeoutMs: number;
@@ -43,7 +46,6 @@ const HANDSHAKE_OUTPUT_LIMIT = 8_192;
43
46
  * descendant of the wrapper can withhold forever. Resolution therefore may
44
47
  * not depend on `'close'`: past this point the reader settles regardless.
45
48
  */
46
- const SETTLE_AFTER_KILL_MS = 250;
47
49
  /**
48
50
  * How long a session with NO execution deadline waits, after the wrapper
49
51
  * process itself is gone, for its stdio to finish draining.
@@ -72,6 +74,8 @@ export function runWrapperSession(
72
74
  signal?: AbortSignal,
73
75
  /** Working directory for the wrapper process; the artifact scanner uses the same root. */
74
76
  cwd?: string,
77
+ /** Authored invocation basename retained while executing pinned canonical bytes. */
78
+ argv0?: string,
75
79
  ): Promise<WrapperSessionResult> {
76
80
  if (signal?.aborted) return Promise.reject(signal.reason);
77
81
  if (signal !== undefined && process.platform === 'win32') {
@@ -100,10 +104,10 @@ export function runWrapperSession(
100
104
  ));
101
105
  }
102
106
 
103
- return executePinnedWrapper(cli, identity, request, env, limits, signal, cwd);
107
+ return executePinnedWrapper(cli, identity, request, env, limits, signal, cwd, argv0);
104
108
  }
105
109
 
106
- function executePinnedWrapper(
110
+ async function executePinnedWrapper(
107
111
  cli: string,
108
112
  identity: WrapperIdentity,
109
113
  request: string,
@@ -111,16 +115,35 @@ function executePinnedWrapper(
111
115
  limits: WrapperSessionLimits,
112
116
  signal?: AbortSignal,
113
117
  cwd?: string,
118
+ argv0?: string,
114
119
  ): Promise<WrapperSessionResult> {
115
- return new Promise((resolve) => {
116
- const ownsGroup = ownsProcessGroup(signal);
117
- const child = spawn(identity.executable, [WRAPPER_IDENTIFY_ARG], {
120
+ let pinned: Awaited<ReturnType<typeof pinCliAlias>>;
121
+ try {
122
+ pinned = await pinCliAlias(identity.executable, argv0 ?? basename(cli));
123
+ } catch (error) {
124
+ return failure(
125
+ `CLI ${JSON.stringify(cli)} wrapper invocation alias could not be pinned: ${String(error)}`,
126
+ );
127
+ }
128
+ if (signal?.aborted) {
129
+ pinned.release();
130
+ return failure('Agent execution aborted: lease ownership lost.');
131
+ }
132
+ const ownsGroup = ownsProcessGroup(signal);
133
+ let child: ChildProcessWithoutNullStreams;
134
+ try {
135
+ child = spawn(pinned.executable, [WRAPPER_IDENTIFY_ARG], {
118
136
  stdio: ['pipe', 'pipe', 'pipe'],
119
137
  env,
120
138
  detached: ownsGroup,
139
+ ...(argv0 === undefined ? {} : { argv0 }),
121
140
  ...(cwd === undefined ? {} : { cwd }),
122
141
  });
123
- const stop = childStop(child, ownsGroup);
142
+ } catch (error) {
143
+ pinned.release();
144
+ throw error;
145
+ }
146
+ return new Promise((resolve) => {
124
147
  const stdout: string[] = [];
125
148
  const stderr: Buffer[] = [];
126
149
  let handshakePending = '';
@@ -134,27 +157,49 @@ function executePinnedWrapper(
134
157
  let exitCode: number | null = null;
135
158
  /** The handshake deadline, then the execution deadline if there is one. */
136
159
  let lifecycleTimer: NodeJS.Timeout | undefined;
137
- /** `terminate`'s own settle deadline. */
138
- let settleTimer: NodeJS.Timeout | undefined;
139
160
  /** The post-exit drain grace, armed only when execution is unlimited. */
140
161
  let drainTimer: NodeJS.Timeout | undefined;
141
- /** The settle deadline the drain's own stop owes, after its escalation. */
142
- let drainSettleTimer: NodeJS.Timeout | undefined;
143
- // Each deadline owns its own handle. They can be armed at the same time —
144
- // a drain grace can be running when an over-limit final line terminates
145
- // the session — and a shared variable would drop the only reference to
146
- // one of them and leave it pending past the settle.
147
162
 
148
163
  const finish = (result: WrapperSessionResult): void => {
149
164
  if (settled) return;
150
165
  settled = true;
151
166
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
152
- if (settleTimer !== undefined) clearTimeout(settleTimer);
153
167
  if (drainTimer !== undefined) clearTimeout(drainTimer);
154
- if (drainSettleTimer !== undefined) clearTimeout(drainSettleTimer);
155
168
  signal?.removeEventListener('abort', onAbort);
156
169
  resolve(result);
157
170
  };
171
+ let pendingStopResult: { result: WrapperSessionResult; priority: 'normal' | 'abort' } | undefined;
172
+ let releaseReaper = (): void => {};
173
+ const stop = childStop(child, ownsGroup, undefined, (stopError) => {
174
+ if (stopError !== undefined) {
175
+ finish(failure(stopError.message));
176
+ return;
177
+ }
178
+ try {
179
+ pinned.release();
180
+ releaseReaper();
181
+ } catch (error) {
182
+ finish(failure(`CLI ${JSON.stringify(cli)} wrapper invocation alias cleanup failed: ${String(error)}`));
183
+ return;
184
+ }
185
+ if (pendingStopResult !== undefined) finish(pendingStopResult.result);
186
+ });
187
+ releaseReaper = ownsGroup ? reapOnExit(stop, () => {
188
+ try { pinned.release(); } catch { /* host exit cannot report another result */ }
189
+ }) : () => {};
190
+ const finishAfterStop = (
191
+ result: WrapperSessionResult,
192
+ action: 'kill' | 'terminate',
193
+ priority: 'normal' | 'abort' = 'normal',
194
+ ): void => {
195
+ // Lease loss is authoritative. It may replace a protocol/drain result
196
+ // whose stop is still being proved, but no later timer may replace it.
197
+ if (pendingStopResult !== undefined) {
198
+ if (pendingStopResult.priority === 'abort' || priority === 'normal') return;
199
+ }
200
+ pendingStopResult = { result, priority };
201
+ stop[action]();
202
+ };
158
203
  /**
159
204
  * INVARIANT: a session may not settle until either the process group is
160
205
  * confirmed dead or the escalation has actually run.
@@ -166,21 +211,23 @@ function executePinnedWrapper(
166
211
  * one. Every child-level settle therefore goes through
167
212
  * `maySettleOnChildExit`, which is the one place that asks the GROUP.
168
213
  *
169
- * When it says no, `terminate`'s own deadline settles instead, with a
170
- * byte-identical `failure(protocolError)` result. That deadline is armed
171
- * whenever an escalation is — both come from the single `terminate` below —
172
- * so refusing here can defer a settle but can never strand one.
214
+ * When it says no, the shared stop callback settles instead, with the
215
+ * pending result or a fail-closed group-confirmation error. Every stop is
216
+ * bounded, so refusing here can defer a settle but can never strand one.
173
217
  */
174
218
  const finishOnChildExit = (result: WrapperSessionResult): void => {
175
219
  // Asked before `finish`, and asked even once we have already settled:
176
220
  // this is also the only place a pointless escalation is refunded, and a
177
- // session that settled on `terminate`'s deadline still owes that refund.
221
+ // session whose result is pending on `terminate` still owes that refund.
178
222
  if (!stop.maySettleOnChildExit()) return;
179
223
  finish(result);
180
224
  };
181
225
  const onAbort = (): void => {
182
- stop.kill();
183
- finish(failure('Agent execution aborted: lease ownership lost.'));
226
+ if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
227
+ lifecycleTimer = undefined;
228
+ if (drainTimer !== undefined) clearTimeout(drainTimer);
229
+ drainTimer = undefined;
230
+ finishAfterStop(failure('Agent execution aborted: lease ownership lost.'), 'kill', 'abort');
184
231
  };
185
232
  signal?.addEventListener('abort', onAbort, { once: true });
186
233
  if (signal?.aborted) { onAbort(); return; }
@@ -190,26 +237,17 @@ function executePinnedWrapper(
190
237
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
191
238
  lifecycleTimer = undefined;
192
239
  // A refusal wins over a drain that was going to report the wrapper's own
193
- // exit as a success: from here this settle belongs to the deadline armed
194
- // below, and a drain that still fired would stop the tree a second time.
240
+ // exit as a success: from here this settle belongs to the shared stop
241
+ // callback, and a drain that still fired would stop the tree a second time.
195
242
  if (drainTimer !== undefined) clearTimeout(drainTimer);
196
243
  drainTimer = undefined;
197
244
  // Same reach as an abort, only gentler first: this stop must find the
198
245
  // whole group, or a descendant outlives the session still holding the
199
246
  // stdio it inherited.
200
- stop.terminate();
201
- // The reader owns the bound. `'close'` is emitted only after every
202
- // inherited stdio pipe closes, so a wrapper that leaves a descendant
203
- // holding one withholds it forever and strands the step with no
204
- // `completionReason` at all. Settle on our own deadline instead — the
205
- // same shape `spawnInvocation` uses in worker-cli.ts, where the timer
206
- // resolves rather than delegating to a child-controlled event. The
207
- // result is byte-identical to the one the `'close'` path would build
208
- // for this `protocolError`, so this changes only WHEN we settle.
209
- settleTimer = setTimeout(
210
- () => finish(failure(message)),
211
- FORCE_KILL_DELAY_MS + SETTLE_AFTER_KILL_MS,
212
- );
247
+ // The shared stop owns both the liveness bound and settlement. Its
248
+ // callback preserves this refusal once group death is proved, or
249
+ // replaces it with a fail-closed confirmation error when it is not.
250
+ finishAfterStop(failure(message), 'terminate');
213
251
  };
214
252
  const startExecutionTimer = (): void => {
215
253
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
@@ -326,26 +364,21 @@ function executePinnedWrapper(
326
364
  const drainExpired = (): void => {
327
365
  drainTimer = undefined;
328
366
  if (settled) return;
367
+ // A protocol stop or lease abort already owns settlement. In particular,
368
+ // a drain timer armed before abort may not replace the abort while group
369
+ // death is still being confirmed.
370
+ if (pendingStopResult !== undefined) return;
329
371
  const result = executionResult(exitCode);
330
- // Finalizing can itself refuse the session — an over-limit final line —
331
- // and `terminate` has then already stopped the tree and armed its own
332
- // settle deadline. Stopping or arming a second time here would leave two
333
- // deadlines racing for one settle.
334
- if (settleTimer !== undefined) return;
335
372
  // The wrapper is gone and `'close'` has still not arrived, so something
336
373
  // it left behind is holding stdio it inherited. Stop what is reachable —
337
374
  // a settle that releases no pipes lets the step complete while
338
- // `flows run` never exits — and then own the settle regardless: the
339
- // force kill has no callback, and a descendant that escaped into its own
340
- // group before the wrapper died is not traceable to this spawn at all.
341
- stop.terminate();
342
- drainSettleTimer = setTimeout(
343
- () => finish(executionResult(exitCode)),
344
- FORCE_KILL_DELAY_MS + SETTLE_AFTER_KILL_MS,
345
- );
375
+ // `flows run` never exits. A descendant that escaped into its own group
376
+ // before the wrapper died is not traceable to this spawn, but every
377
+ // group the stop can address must be proved gone before this result wins.
378
+ finishAfterStop(result, 'terminate');
346
379
  // Settle now if the stop turned out to have nothing to reach; otherwise
347
- // the deadline above owns it, and a `'close'` that arrives once the
348
- // group is empty may still settle earlier.
380
+ // its confirmation callback owns the result, and a `'close'` that arrives
381
+ // once the group is empty may still settle earlier.
349
382
  finishOnChildExit(result);
350
383
  };
351
384
  /**
@@ -8,6 +8,7 @@ import type { StepDispatchEvent } from './protocol.js';
8
8
  import { atomicJson, readSlackReceipt, receiptPath, slackWriteback } from './slack-writeback.js';
9
9
  import { invokeHelper, type HelperCall } from './yaml-helpers.js';
10
10
  import { withWorkerLease } from './worker-lease.js';
11
+ import { NO_MODEL_COST } from './reported-cost.js';
11
12
 
12
13
  export function helperMount(provider: string, env = process.env): string | undefined {
13
14
  const root = [env.RELAYFILE_MOUNT_PATH, env.WORKSPACE_ROOT, env.WORKFORCE_SANDBOX_ROOT,
@@ -73,6 +74,6 @@ export async function completeHelperDispatch(
73
74
  return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt };
74
75
  });
75
76
  await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
76
- dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins,
77
+ dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, reported_cost: NO_MODEL_COST,
77
78
  effects: [{ surface_path: surfacePath, idempotency_key: dispatch.idempotency_key }] });
78
79
  }
@@ -3,6 +3,7 @@ import { githubClient, linearClient, slackClient, type RelayTransport } from '@r
3
3
  import type { AgentStepSpec, KernelAgentStep, YamlHelperParams } from './spec.js';
4
4
  import { canonicalize } from './canonical.js';
5
5
  import { unknownKeyErrors } from './unknown-keys.js';
6
+ import { HELPER_INSTRUCTION_PREFIX } from './helper-instruction.js';
6
7
 
7
8
  const string = { type: 'string', minLength: 1 };
8
9
  const text = { type: 'string' };
@@ -61,7 +62,7 @@ export type HelperCall = { [P in keyof YamlHelperParams]: { [V in keyof YamlHelp
61
62
  { type: 'effect'; provider: P; verb: V; params: YamlHelperParams[P][V] }
62
63
  }[keyof YamlHelperParams[P]] }[keyof YamlHelperParams];
63
64
 
64
- const prefix = 'relayflows:helper:v1\n';
65
+ const prefix = HELPER_INSTRUCTION_PREFIX;
65
66
  const commonFields = ['id', 'dependsOn', 'maxIterations', 'verification', 'output'] as const;
66
67
  const isObject = (value: unknown): value is Record<string, unknown> =>
67
68
  value !== null && typeof value === 'object' && !Array.isArray(value);