@relayflows/sdk 2.0.21 → 2.0.23

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 (247) hide show
  1. package/dist/agent-artifacts.d.ts +18 -9
  2. package/dist/agent-artifacts.d.ts.map +1 -1
  3. package/dist/agent-artifacts.js +27 -11
  4. package/dist/agent-artifacts.js.map +1 -1
  5. package/dist/agent-transcript.d.ts +186 -0
  6. package/dist/agent-transcript.d.ts.map +1 -0
  7. package/dist/agent-transcript.js +585 -0
  8. package/dist/agent-transcript.js.map +1 -0
  9. package/dist/authored-flow-error.d.ts +33 -1
  10. package/dist/authored-flow-error.d.ts.map +1 -1
  11. package/dist/authored-flow-error.js +20 -1
  12. package/dist/authored-flow-error.js.map +1 -1
  13. package/dist/authored-flow-executor.d.ts.map +1 -1
  14. package/dist/authored-flow-executor.js +4 -1
  15. package/dist/authored-flow-executor.js.map +1 -1
  16. package/dist/authored-node-entry.js +5 -1
  17. package/dist/authored-node-entry.js.map +1 -1
  18. package/dist/authored-node-runner.d.ts +11 -0
  19. package/dist/authored-node-runner.d.ts.map +1 -1
  20. package/dist/authored-node-runner.js +30 -1
  21. package/dist/authored-node-runner.js.map +1 -1
  22. package/dist/authored-root.js +3 -1
  23. package/dist/authored-root.js.map +1 -1
  24. package/dist/authored-source-authority.js +13 -0
  25. package/dist/authored-source-authority.js.map +1 -1
  26. package/dist/authored-step-index.d.ts +82 -0
  27. package/dist/authored-step-index.d.ts.map +1 -0
  28. package/dist/authored-step-index.js +120 -0
  29. package/dist/authored-step-index.js.map +1 -0
  30. package/dist/authored-step-output.d.ts +23 -2
  31. package/dist/authored-step-output.d.ts.map +1 -1
  32. package/dist/authored-step-output.js +47 -5
  33. package/dist/authored-step-output.js.map +1 -1
  34. package/dist/authored-worker-step.d.ts +10 -2
  35. package/dist/authored-worker-step.d.ts.map +1 -1
  36. package/dist/authored-worker-step.js +81 -7
  37. package/dist/authored-worker-step.js.map +1 -1
  38. package/dist/cli/check.d.ts.map +1 -1
  39. package/dist/cli/check.js +67 -12
  40. package/dist/cli/check.js.map +1 -1
  41. package/dist/cli/cloud-read.d.ts +31 -0
  42. package/dist/cli/cloud-read.d.ts.map +1 -0
  43. package/dist/cli/cloud-read.js +482 -0
  44. package/dist/cli/cloud-read.js.map +1 -0
  45. package/dist/cli/run.d.ts +2 -0
  46. package/dist/cli/run.d.ts.map +1 -1
  47. package/dist/cli/run.js +53 -24
  48. package/dist/cli/run.js.map +1 -1
  49. package/dist/cli/status.d.ts +43 -0
  50. package/dist/cli/status.d.ts.map +1 -0
  51. package/dist/cli/status.js +321 -0
  52. package/dist/cli/status.js.map +1 -0
  53. package/dist/cli/step-failure.d.ts +26 -0
  54. package/dist/cli/step-failure.d.ts.map +1 -1
  55. package/dist/cli/step-failure.js +99 -10
  56. package/dist/cli/step-failure.js.map +1 -1
  57. package/dist/cli-commands.d.ts +41 -0
  58. package/dist/cli-commands.d.ts.map +1 -1
  59. package/dist/cli-commands.js +33 -0
  60. package/dist/cli-commands.js.map +1 -1
  61. package/dist/cli.d.ts +4 -2
  62. package/dist/cli.d.ts.map +1 -1
  63. package/dist/cli.js +26 -0
  64. package/dist/cli.js.map +1 -1
  65. package/dist/cloud-http.d.ts +12 -0
  66. package/dist/cloud-http.d.ts.map +1 -1
  67. package/dist/cloud-http.js +14 -6
  68. package/dist/cloud-http.js.map +1 -1
  69. package/dist/cloud-read.d.ts +154 -0
  70. package/dist/cloud-read.d.ts.map +1 -0
  71. package/dist/cloud-read.js +373 -0
  72. package/dist/cloud-read.js.map +1 -0
  73. package/dist/cloud-transcript.d.ts +94 -0
  74. package/dist/cloud-transcript.d.ts.map +1 -0
  75. package/dist/cloud-transcript.js +342 -0
  76. package/dist/cloud-transcript.js.map +1 -0
  77. package/dist/communication/environment.d.ts +3 -0
  78. package/dist/communication/environment.d.ts.map +1 -0
  79. package/dist/communication/environment.js +34 -0
  80. package/dist/communication/environment.js.map +1 -0
  81. package/dist/communication/history.d.ts +6 -0
  82. package/dist/communication/history.d.ts.map +1 -0
  83. package/dist/communication/history.js +38 -0
  84. package/dist/communication/history.js.map +1 -0
  85. package/dist/communication/local.d.ts +6 -0
  86. package/dist/communication/local.d.ts.map +1 -0
  87. package/dist/communication/local.js +56 -0
  88. package/dist/communication/local.js.map +1 -0
  89. package/dist/communication/preflight.d.ts +6 -0
  90. package/dist/communication/preflight.d.ts.map +1 -0
  91. package/dist/communication/preflight.js +37 -0
  92. package/dist/communication/preflight.js.map +1 -0
  93. package/dist/communication/projection.d.ts +8 -0
  94. package/dist/communication/projection.d.ts.map +1 -0
  95. package/dist/communication/projection.js +23 -0
  96. package/dist/communication/projection.js.map +1 -0
  97. package/dist/communication/relay.d.ts +78 -0
  98. package/dist/communication/relay.d.ts.map +1 -0
  99. package/dist/communication/relay.js +89 -0
  100. package/dist/communication/relay.js.map +1 -0
  101. package/dist/communication/session.d.ts +24 -0
  102. package/dist/communication/session.d.ts.map +1 -0
  103. package/dist/communication/session.js +100 -0
  104. package/dist/communication/session.js.map +1 -0
  105. package/dist/communication/spec.d.ts +20 -0
  106. package/dist/communication/spec.d.ts.map +1 -0
  107. package/dist/communication/spec.js +113 -0
  108. package/dist/communication/spec.js.map +1 -0
  109. package/dist/communication/tool.d.ts +3 -0
  110. package/dist/communication/tool.d.ts.map +1 -0
  111. package/dist/communication/tool.js +23 -0
  112. package/dist/communication/tool.js.map +1 -0
  113. package/dist/communication/tools.d.ts +11 -0
  114. package/dist/communication/tools.d.ts.map +1 -0
  115. package/dist/communication/tools.js +65 -0
  116. package/dist/communication/tools.js.map +1 -0
  117. package/dist/communication/worker.d.ts +6 -0
  118. package/dist/communication/worker.d.ts.map +1 -0
  119. package/dist/communication/worker.js +120 -0
  120. package/dist/communication/worker.js.map +1 -0
  121. package/dist/compile.d.ts.map +1 -1
  122. package/dist/compile.js +8 -1
  123. package/dist/compile.js.map +1 -1
  124. package/dist/failure-kinds.d.ts +30 -2
  125. package/dist/failure-kinds.d.ts.map +1 -1
  126. package/dist/failure-kinds.js +9 -0
  127. package/dist/failure-kinds.js.map +1 -1
  128. package/dist/index.d.ts +3 -1
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +2 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/journal-client.d.ts +4 -1
  133. package/dist/journal-client.d.ts.map +1 -1
  134. package/dist/journal-client.js +11 -2
  135. package/dist/journal-client.js.map +1 -1
  136. package/dist/journal-reader.d.ts +1 -1
  137. package/dist/journal-reader.d.ts.map +1 -1
  138. package/dist/journal-reader.js +3 -1
  139. package/dist/journal-reader.js.map +1 -1
  140. package/dist/llm-worker.d.ts.map +1 -1
  141. package/dist/llm-worker.js +12 -1
  142. package/dist/llm-worker.js.map +1 -1
  143. package/dist/permissions-preflight.d.ts +22 -0
  144. package/dist/permissions-preflight.d.ts.map +1 -0
  145. package/dist/permissions-preflight.js +49 -0
  146. package/dist/permissions-preflight.js.map +1 -0
  147. package/dist/preflight.d.ts +13 -2
  148. package/dist/preflight.d.ts.map +1 -1
  149. package/dist/preflight.js +29 -6
  150. package/dist/preflight.js.map +1 -1
  151. package/dist/protocol.d.ts +39 -1
  152. package/dist/protocol.d.ts.map +1 -1
  153. package/dist/pty-sidechannel.d.ts +7 -0
  154. package/dist/pty-sidechannel.d.ts.map +1 -1
  155. package/dist/pty-sidechannel.js.map +1 -1
  156. package/dist/redact.d.ts +16 -0
  157. package/dist/redact.d.ts.map +1 -0
  158. package/dist/redact.js +104 -0
  159. package/dist/redact.js.map +1 -0
  160. package/dist/run-state.d.ts +124 -0
  161. package/dist/run-state.d.ts.map +1 -0
  162. package/dist/run-state.js +368 -0
  163. package/dist/run-state.js.map +1 -0
  164. package/dist/spec.d.ts +24 -1
  165. package/dist/spec.d.ts.map +1 -1
  166. package/dist/spec.js.map +1 -1
  167. package/dist/step-env.d.ts +25 -0
  168. package/dist/step-env.d.ts.map +1 -0
  169. package/dist/step-env.js +34 -0
  170. package/dist/step-env.js.map +1 -0
  171. package/dist/transcript-tail.d.ts +54 -0
  172. package/dist/transcript-tail.d.ts.map +1 -0
  173. package/dist/transcript-tail.js +176 -0
  174. package/dist/transcript-tail.js.map +1 -0
  175. package/dist/worker-cli.d.ts +10 -0
  176. package/dist/worker-cli.d.ts.map +1 -1
  177. package/dist/worker-cli.js +138 -17
  178. package/dist/worker-cli.js.map +1 -1
  179. package/dist/worker-usage.d.ts +7 -2
  180. package/dist/worker-usage.d.ts.map +1 -1
  181. package/dist/worker-usage.js +21 -3
  182. package/dist/worker-usage.js.map +1 -1
  183. package/dist/worker.d.ts +1 -0
  184. package/dist/worker.d.ts.map +1 -1
  185. package/dist/worker.js +41 -7
  186. package/dist/worker.js.map +1 -1
  187. package/dist/wrapper-runtime.d.ts +7 -1
  188. package/dist/wrapper-runtime.d.ts.map +1 -1
  189. package/dist/wrapper-runtime.js +7 -1
  190. package/dist/wrapper-runtime.js.map +1 -1
  191. package/dist/wrapper-session.d.ts +8 -0
  192. package/dist/wrapper-session.d.ts.map +1 -1
  193. package/dist/wrapper-session.js +156 -23
  194. package/dist/wrapper-session.js.map +1 -1
  195. package/package.json +14 -2
  196. package/src/agent-artifacts.ts +27 -11
  197. package/src/agent-transcript.ts +651 -0
  198. package/src/authored-flow-error.ts +17 -0
  199. package/src/authored-flow-executor.ts +4 -1
  200. package/src/authored-node-entry.ts +5 -1
  201. package/src/authored-node-runner.ts +33 -1
  202. package/src/authored-root.ts +2 -1
  203. package/src/authored-source-authority.ts +13 -0
  204. package/src/authored-step-index.ts +157 -0
  205. package/src/authored-step-output.ts +66 -14
  206. package/src/authored-worker-step.ts +88 -15
  207. package/src/cli/check.ts +69 -11
  208. package/src/cli/cloud-read.ts +518 -0
  209. package/src/cli/run.ts +43 -24
  210. package/src/cli/status.ts +349 -0
  211. package/src/cli/step-failure.ts +100 -10
  212. package/src/cli-commands.ts +33 -0
  213. package/src/cli.ts +27 -0
  214. package/src/cloud-http.ts +32 -6
  215. package/src/cloud-read.ts +515 -0
  216. package/src/cloud-transcript.ts +422 -0
  217. package/src/communication/environment.ts +33 -0
  218. package/src/communication/history.ts +38 -0
  219. package/src/communication/local.ts +41 -0
  220. package/src/communication/preflight.ts +34 -0
  221. package/src/communication/projection.ts +23 -0
  222. package/src/communication/relay.ts +102 -0
  223. package/src/communication/session.ts +84 -0
  224. package/src/communication/spec.ts +106 -0
  225. package/src/communication/tool.ts +22 -0
  226. package/src/communication/tools.ts +50 -0
  227. package/src/communication/worker.ts +95 -0
  228. package/src/compile.ts +4 -1
  229. package/src/failure-kinds.ts +31 -1
  230. package/src/index.ts +10 -1
  231. package/src/journal-client.ts +12 -2
  232. package/src/journal-reader.ts +4 -2
  233. package/src/llm-worker.ts +12 -1
  234. package/src/permissions-preflight.ts +53 -0
  235. package/src/preflight.ts +42 -7
  236. package/src/protocol.ts +15 -0
  237. package/src/pty-sidechannel.ts +7 -0
  238. package/src/redact.ts +106 -0
  239. package/src/run-state.ts +468 -0
  240. package/src/spec.ts +18 -1
  241. package/src/step-env.ts +47 -0
  242. package/src/transcript-tail.ts +193 -0
  243. package/src/worker-cli.ts +139 -18
  244. package/src/worker-usage.ts +26 -3
  245. package/src/worker.ts +42 -6
  246. package/src/wrapper-runtime.ts +7 -1
  247. package/src/wrapper-session.ts +159 -24
@@ -13,6 +13,14 @@ import {
13
13
 
14
14
  export interface WrapperSessionLimits {
15
15
  handshakeTimeoutMs: number;
16
+ /**
17
+ * How long the wrapper may work after acknowledging the request, or `0` for
18
+ * no deadline at all — the same convention `CliInvocation.timeoutMs` already
19
+ * uses for a native CLI spawn, where `worker-cli.ts` arms its timer only
20
+ * `if (invocation.timeoutMs > 0)`. `0` is the default, so a wrapper-backed
21
+ * step is bounded by the lease abort exactly as a `claude` or `codex` step
22
+ * is, and not by a constant neither of those pays.
23
+ */
16
24
  executionTimeoutMs: number;
17
25
  maxOutputBytes: number;
18
26
  }
@@ -25,7 +33,7 @@ export interface WrapperSessionResult {
25
33
 
26
34
  const DEFAULT_LIMITS: WrapperSessionLimits = {
27
35
  handshakeTimeoutMs: 10_000,
28
- executionTimeoutMs: 300_000,
36
+ executionTimeoutMs: 0,
29
37
  maxOutputBytes: 1_048_576,
30
38
  };
31
39
  const HANDSHAKE_OUTPUT_LIMIT = 8_192;
@@ -36,6 +44,19 @@ const HANDSHAKE_OUTPUT_LIMIT = 8_192;
36
44
  * not depend on `'close'`: past this point the reader settles regardless.
37
45
  */
38
46
  const SETTLE_AFTER_KILL_MS = 250;
47
+ /**
48
+ * How long a session with NO execution deadline waits, after the wrapper
49
+ * process itself is gone, for its stdio to finish draining.
50
+ *
51
+ * The execution deadline used to be what settled a wrapper that exited while a
52
+ * descendant still held an inherited pipe, because `'close'` waits on every
53
+ * one of them. That job is real; keying it on elapsed time was not. It is
54
+ * keyed on the wrapper's own exit instead, which is the fact that actually
55
+ * matters. Reaching the end of this grace is not proof that a pipe was leaked
56
+ * — buffered output may simply still be in flight — so the grace is a policy
57
+ * and the stop that follows it is bounded, not a diagnosis.
58
+ */
59
+ const DRAIN_AFTER_EXIT_MS = 250;
39
60
 
40
61
  /**
41
62
  * Identify and execute a custom wrapper in one pinned process. No private
@@ -108,14 +129,29 @@ function executePinnedWrapper(
108
129
  let phase: 'identity' | 'ack' | 'execute' = 'identity';
109
130
  let protocolError: string | undefined;
110
131
  let settled = false;
132
+ let outputFinalized = false;
133
+ let childExited = false;
134
+ let exitCode: number | null = null;
135
+ /** The handshake deadline, then the execution deadline if there is one. */
111
136
  let lifecycleTimer: NodeJS.Timeout | undefined;
137
+ /** `terminate`'s own settle deadline. */
112
138
  let settleTimer: NodeJS.Timeout | undefined;
139
+ /** The post-exit drain grace, armed only when execution is unlimited. */
140
+ 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.
113
147
 
114
148
  const finish = (result: WrapperSessionResult): void => {
115
149
  if (settled) return;
116
150
  settled = true;
117
151
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
118
152
  if (settleTimer !== undefined) clearTimeout(settleTimer);
153
+ if (drainTimer !== undefined) clearTimeout(drainTimer);
154
+ if (drainSettleTimer !== undefined) clearTimeout(drainSettleTimer);
119
155
  signal?.removeEventListener('abort', onAbort);
120
156
  resolve(result);
121
157
  };
@@ -152,6 +188,12 @@ function executePinnedWrapper(
152
188
  if (protocolError !== undefined) return;
153
189
  protocolError = message;
154
190
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
191
+ lifecycleTimer = undefined;
192
+ // 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.
195
+ if (drainTimer !== undefined) clearTimeout(drainTimer);
196
+ drainTimer = undefined;
155
197
  // Same reach as an abort, only gentler first: this stop must find the
156
198
  // whole group, or a descendant outlives the session still holding the
157
199
  // stdio it inherited.
@@ -171,6 +213,13 @@ function executePinnedWrapper(
171
213
  };
172
214
  const startExecutionTimer = (): void => {
173
215
  if (lifecycleTimer !== undefined) clearTimeout(lifecycleTimer);
216
+ lifecycleTimer = undefined;
217
+ // `0` is no deadline, exactly as `worker-cli.ts` reads
218
+ // `invocation.timeoutMs` for a native CLI. The handshake deadline it
219
+ // just replaced was a liveness check and is over; nothing else is armed,
220
+ // and what bounds the work from here is the lease abort — the same thing
221
+ // that bounds a `claude` or `codex` step.
222
+ if (limits.executionTimeoutMs <= 0) return;
174
223
  lifecycleTimer = setTimeout(() => terminate(
175
224
  `CLI ${JSON.stringify(cli)} execution timed out after ${limits.executionTimeoutMs}ms.`,
176
225
  ), limits.executionTimeoutMs);
@@ -210,6 +259,12 @@ function executePinnedWrapper(
210
259
  }
211
260
  phase = 'execute';
212
261
  startExecutionTimer();
262
+ // The wrapper may already be gone: `'exit'` is delivered as soon as the
263
+ // process dies, while the bytes it wrote are still being read out of
264
+ // the pipe. The exit is remembered rather than acted on, and the drain
265
+ // grace starts here, once the acknowledgement it waits behind has
266
+ // actually been consumed.
267
+ startDrainGrace();
213
268
  }
214
269
  };
215
270
  const acceptExecutionData = (chunk: string): void => {
@@ -228,6 +283,83 @@ function executePinnedWrapper(
228
283
  }
229
284
  };
230
285
  const handshakeComplete = (): boolean => phase === 'execute';
286
+ /**
287
+ * Fold the last un-terminated line into the captured output, exactly once.
288
+ *
289
+ * Two settles can now reach this — `'close'` and the drain grace — and the
290
+ * pending buffer may only be spent by one of them. A second finalization
291
+ * that appended it again would duplicate the wrapper's own result, and a
292
+ * buffer left in place would go on being counted against `maxOutputBytes`
293
+ * by any later chunk.
294
+ */
295
+ const finalizeOutput = (): void => {
296
+ if (outputFinalized) return;
297
+ outputFinalized = true;
298
+ if (protocolError === undefined && phase === 'execute' && executionPending.length > 0) {
299
+ if (normalizeLine(executionPending) === WRAPPER_EXECUTE_TOKEN) {
300
+ protocolError = `CLI ${JSON.stringify(cli)} emitted a duplicate execute protocol frame.`;
301
+ } else if (!exceedsOutputLimit(0)) {
302
+ appendStdout(executionPending);
303
+ }
304
+ }
305
+ executionPending = '';
306
+ };
307
+ /**
308
+ * The one result builder. A recorded protocol or output failure outranks
309
+ * the exit status: a wrapper that violated the contract is refused even
310
+ * when the process it ran in went on to exit 0.
311
+ */
312
+ const executionResult = (code: number | null): WrapperSessionResult => {
313
+ finalizeOutput();
314
+ if (protocolError !== undefined || phase !== 'execute') {
315
+ return failure(
316
+ protocolError
317
+ ?? `CLI ${JSON.stringify(cli)} exited before completing the ${WRAPPER_IDENTIFY_TOKEN} same-process handshake.`,
318
+ );
319
+ }
320
+ return {
321
+ exit_code: code,
322
+ stdout_tail: stdout.join(''),
323
+ stderr_tail: Buffer.concat(stderr).toString('utf8'),
324
+ };
325
+ };
326
+ const drainExpired = (): void => {
327
+ drainTimer = undefined;
328
+ if (settled) return;
329
+ 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
+ // The wrapper is gone and `'close'` has still not arrived, so something
336
+ // it left behind is holding stdio it inherited. Stop what is reachable —
337
+ // 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
+ );
346
+ // 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.
349
+ finishOnChildExit(result);
350
+ };
351
+ /**
352
+ * Arm the post-exit drain, for an unlimited session that has both seen the
353
+ * wrapper exit and consumed its acknowledgement. A session with an explicit
354
+ * execution deadline keeps its existing behaviour: that deadline is what
355
+ * bounds a withheld `'close'` there, and nothing about it changes.
356
+ */
357
+ const startDrainGrace = (): void => {
358
+ if (limits.executionTimeoutMs > 0) return;
359
+ if (!childExited || phase !== 'execute') return;
360
+ if (settled || drainTimer !== undefined || protocolError !== undefined) return;
361
+ drainTimer = setTimeout(drainExpired, DRAIN_AFTER_EXIT_MS);
362
+ };
231
363
 
232
364
  lifecycleTimer = setTimeout(() => terminate(
233
365
  `CLI ${JSON.stringify(cli)} did not identify as ${WRAPPER_IDENTIFY_TOKEN} within ${limits.handshakeTimeoutMs}ms.`,
@@ -235,7 +367,11 @@ function executePinnedWrapper(
235
367
 
236
368
  child.stdout.setEncoding('utf8');
237
369
  child.stdout.on('data', (chunk: string) => {
238
- if (protocolError !== undefined) return;
370
+ // Once the output has been finalized the result is built and the
371
+ // captured bytes are spent. A late chunk — the drain grace has expired
372
+ // and a descendant still owns the pipe — may neither be appended to a
373
+ // result already reported nor re-judged against a limit already applied.
374
+ if (protocolError !== undefined || outputFinalized) return;
239
375
  if (phase === 'execute') {
240
376
  acceptExecutionData(chunk);
241
377
  return;
@@ -273,7 +409,7 @@ function executePinnedWrapper(
273
409
  }
274
410
  });
275
411
  child.stderr.on('data', (chunk: Buffer) => {
276
- if (protocolError !== undefined || exceedsOutputLimit(chunk.byteLength)) return;
412
+ if (protocolError !== undefined || outputFinalized || exceedsOutputLimit(chunk.byteLength)) return;
277
413
  capturedBytes += chunk.byteLength;
278
414
  stderr.push(chunk);
279
415
  });
@@ -281,42 +417,41 @@ function executePinnedWrapper(
281
417
  // A child that closes stdin before acknowledgement is classified on close.
282
418
  });
283
419
  child.once('error', (error) => finishOnChildExit(failure(error.message)));
284
- child.once('close', (code) => {
285
- if (protocolError === undefined && phase === 'execute' && executionPending.length > 0) {
286
- if (normalizeLine(executionPending) === WRAPPER_EXECUTE_TOKEN) {
287
- protocolError = `CLI ${JSON.stringify(cli)} emitted a duplicate execute protocol frame.`;
288
- } else if (!exceedsOutputLimit(0)) {
289
- appendStdout(executionPending);
290
- }
291
- }
292
- if (protocolError !== undefined || phase !== 'execute') {
293
- finishOnChildExit(failure(
294
- protocolError
295
- ?? `CLI ${JSON.stringify(cli)} exited before completing the ${WRAPPER_IDENTIFY_TOKEN} same-process handshake.`,
296
- ));
297
- return;
298
- }
299
- finishOnChildExit({
300
- exit_code: code,
301
- stdout_tail: stdout.join(''),
302
- stderr_tail: Buffer.concat(stderr).toString('utf8'),
303
- });
420
+ // `'exit'` is evidence about the DIRECT CHILD only: the wrapper process is
421
+ // gone, whether or not anything it spawned still holds the stdio it
422
+ // inherited. That is exactly the fact the drain grace waits on, and it is
423
+ // not evidence of a dead group — nothing here settles or stops.
424
+ child.once('exit', (code) => {
425
+ childExited = true;
426
+ exitCode = code;
427
+ startDrainGrace();
304
428
  });
429
+ child.once('close', (code) => finishOnChildExit(executionResult(code)));
305
430
  });
306
431
  }
307
432
 
308
433
  function sessionLimits(overrides: Partial<WrapperSessionLimits>): WrapperSessionLimits {
309
434
  return {
310
435
  handshakeTimeoutMs: positiveLimit(overrides.handshakeTimeoutMs, DEFAULT_LIMITS.handshakeTimeoutMs),
311
- executionTimeoutMs: positiveLimit(overrides.executionTimeoutMs, DEFAULT_LIMITS.executionTimeoutMs),
436
+ executionTimeoutMs: durationLimit(overrides.executionTimeoutMs, DEFAULT_LIMITS.executionTimeoutMs),
312
437
  maxOutputBytes: positiveLimit(overrides.maxOutputBytes, DEFAULT_LIMITS.maxOutputBytes),
313
438
  };
314
439
  }
315
440
 
441
+ /** A bound a wrapper may not switch off: `0` and below fall back. */
316
442
  function positiveLimit(value: number | undefined, fallback: number): number {
317
443
  return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
318
444
  }
319
445
 
446
+ /**
447
+ * A duration that may be switched off, so `0` is a value and not a missing
448
+ * one. Reusing {@link positiveLimit} here would coerce an explicit "no
449
+ * deadline" back to the fallback, which is how the default became unreachable.
450
+ */
451
+ function durationLimit(value: number | undefined, fallback: number): number {
452
+ return value !== undefined && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
453
+ }
454
+
320
455
  function normalizeLine(line: string): string {
321
456
  return line.endsWith('\r') ? line.slice(0, -1) : line;
322
457
  }