@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.
- package/dist/agent-artifacts.d.ts +18 -9
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +27 -11
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-transcript.d.ts +186 -0
- package/dist/agent-transcript.d.ts.map +1 -0
- package/dist/agent-transcript.js +585 -0
- package/dist/agent-transcript.js.map +1 -0
- package/dist/authored-flow-error.d.ts +33 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +20 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +4 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-node-entry.js +5 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +30 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +3 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.js +13 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/authored-step-index.d.ts +82 -0
- package/dist/authored-step-index.d.ts.map +1 -0
- package/dist/authored-step-index.js +120 -0
- package/dist/authored-step-index.js.map +1 -0
- package/dist/authored-step-output.d.ts +23 -2
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +47 -5
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts +10 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +81 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +67 -12
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-read.d.ts +31 -0
- package/dist/cli/cloud-read.d.ts.map +1 -0
- package/dist/cli/cloud-read.js +482 -0
- package/dist/cli/cloud-read.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +53 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +43 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +321 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/step-failure.d.ts +26 -0
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +99 -10
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +41 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +33 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-http.d.ts +12 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +14 -6
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-read.d.ts +154 -0
- package/dist/cloud-read.d.ts.map +1 -0
- package/dist/cloud-read.js +373 -0
- package/dist/cloud-read.js.map +1 -0
- package/dist/cloud-transcript.d.ts +94 -0
- package/dist/cloud-transcript.d.ts.map +1 -0
- package/dist/cloud-transcript.js +342 -0
- package/dist/cloud-transcript.js.map +1 -0
- package/dist/communication/environment.d.ts +3 -0
- package/dist/communication/environment.d.ts.map +1 -0
- package/dist/communication/environment.js +34 -0
- package/dist/communication/environment.js.map +1 -0
- package/dist/communication/history.d.ts +6 -0
- package/dist/communication/history.d.ts.map +1 -0
- package/dist/communication/history.js +38 -0
- package/dist/communication/history.js.map +1 -0
- package/dist/communication/local.d.ts +6 -0
- package/dist/communication/local.d.ts.map +1 -0
- package/dist/communication/local.js +56 -0
- package/dist/communication/local.js.map +1 -0
- package/dist/communication/preflight.d.ts +6 -0
- package/dist/communication/preflight.d.ts.map +1 -0
- package/dist/communication/preflight.js +37 -0
- package/dist/communication/preflight.js.map +1 -0
- package/dist/communication/projection.d.ts +8 -0
- package/dist/communication/projection.d.ts.map +1 -0
- package/dist/communication/projection.js +23 -0
- package/dist/communication/projection.js.map +1 -0
- package/dist/communication/relay.d.ts +78 -0
- package/dist/communication/relay.d.ts.map +1 -0
- package/dist/communication/relay.js +89 -0
- package/dist/communication/relay.js.map +1 -0
- package/dist/communication/session.d.ts +24 -0
- package/dist/communication/session.d.ts.map +1 -0
- package/dist/communication/session.js +100 -0
- package/dist/communication/session.js.map +1 -0
- package/dist/communication/spec.d.ts +20 -0
- package/dist/communication/spec.d.ts.map +1 -0
- package/dist/communication/spec.js +113 -0
- package/dist/communication/spec.js.map +1 -0
- package/dist/communication/tool.d.ts +3 -0
- package/dist/communication/tool.d.ts.map +1 -0
- package/dist/communication/tool.js +23 -0
- package/dist/communication/tool.js.map +1 -0
- package/dist/communication/tools.d.ts +11 -0
- package/dist/communication/tools.d.ts.map +1 -0
- package/dist/communication/tools.js +65 -0
- package/dist/communication/tools.js.map +1 -0
- package/dist/communication/worker.d.ts +6 -0
- package/dist/communication/worker.d.ts.map +1 -0
- package/dist/communication/worker.js +120 -0
- package/dist/communication/worker.js.map +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +8 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +30 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +9 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +4 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +11 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-reader.d.ts +1 -1
- package/dist/journal-reader.d.ts.map +1 -1
- package/dist/journal-reader.js +3 -1
- package/dist/journal-reader.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +12 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/permissions-preflight.d.ts +22 -0
- package/dist/permissions-preflight.d.ts.map +1 -0
- package/dist/permissions-preflight.js +49 -0
- package/dist/permissions-preflight.js.map +1 -0
- package/dist/preflight.d.ts +13 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +29 -6
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +39 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/pty-sidechannel.d.ts +7 -0
- package/dist/pty-sidechannel.d.ts.map +1 -1
- package/dist/pty-sidechannel.js.map +1 -1
- package/dist/redact.d.ts +16 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +104 -0
- package/dist/redact.js.map +1 -0
- package/dist/run-state.d.ts +124 -0
- package/dist/run-state.d.ts.map +1 -0
- package/dist/run-state.js +368 -0
- package/dist/run-state.js.map +1 -0
- package/dist/spec.d.ts +24 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-env.d.ts +25 -0
- package/dist/step-env.d.ts.map +1 -0
- package/dist/step-env.js +34 -0
- package/dist/step-env.js.map +1 -0
- package/dist/transcript-tail.d.ts +54 -0
- package/dist/transcript-tail.d.ts.map +1 -0
- package/dist/transcript-tail.js +176 -0
- package/dist/transcript-tail.js.map +1 -0
- package/dist/worker-cli.d.ts +10 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +138 -17
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-usage.d.ts +7 -2
- package/dist/worker-usage.d.ts.map +1 -1
- package/dist/worker-usage.js +21 -3
- package/dist/worker-usage.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +41 -7
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-runtime.d.ts +7 -1
- package/dist/wrapper-runtime.d.ts.map +1 -1
- package/dist/wrapper-runtime.js +7 -1
- package/dist/wrapper-runtime.js.map +1 -1
- package/dist/wrapper-session.d.ts +8 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +156 -23
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-artifacts.ts +27 -11
- package/src/agent-transcript.ts +651 -0
- package/src/authored-flow-error.ts +17 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-node-entry.ts +5 -1
- package/src/authored-node-runner.ts +33 -1
- package/src/authored-root.ts +2 -1
- package/src/authored-source-authority.ts +13 -0
- package/src/authored-step-index.ts +157 -0
- package/src/authored-step-output.ts +66 -14
- package/src/authored-worker-step.ts +88 -15
- package/src/cli/check.ts +69 -11
- package/src/cli/cloud-read.ts +518 -0
- package/src/cli/run.ts +43 -24
- package/src/cli/status.ts +349 -0
- package/src/cli/step-failure.ts +100 -10
- package/src/cli-commands.ts +33 -0
- package/src/cli.ts +27 -0
- package/src/cloud-http.ts +32 -6
- package/src/cloud-read.ts +515 -0
- package/src/cloud-transcript.ts +422 -0
- package/src/communication/environment.ts +33 -0
- package/src/communication/history.ts +38 -0
- package/src/communication/local.ts +41 -0
- package/src/communication/preflight.ts +34 -0
- package/src/communication/projection.ts +23 -0
- package/src/communication/relay.ts +102 -0
- package/src/communication/session.ts +84 -0
- package/src/communication/spec.ts +106 -0
- package/src/communication/tool.ts +22 -0
- package/src/communication/tools.ts +50 -0
- package/src/communication/worker.ts +95 -0
- package/src/compile.ts +4 -1
- package/src/failure-kinds.ts +31 -1
- package/src/index.ts +10 -1
- package/src/journal-client.ts +12 -2
- package/src/journal-reader.ts +4 -2
- package/src/llm-worker.ts +12 -1
- package/src/permissions-preflight.ts +53 -0
- package/src/preflight.ts +42 -7
- package/src/protocol.ts +15 -0
- package/src/pty-sidechannel.ts +7 -0
- package/src/redact.ts +106 -0
- package/src/run-state.ts +468 -0
- package/src/spec.ts +18 -1
- package/src/step-env.ts +47 -0
- package/src/transcript-tail.ts +193 -0
- package/src/worker-cli.ts +139 -18
- package/src/worker-usage.ts +26 -3
- package/src/worker.ts +42 -6
- package/src/wrapper-runtime.ts +7 -1
- package/src/wrapper-session.ts +159 -24
package/src/wrapper-session.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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:
|
|
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
|
}
|