@relayflows/sdk 2.0.37 → 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.
- package/dist/agent-reaper.d.ts +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +3 -2
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +141 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.js +1 -0
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +25 -7
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -0
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +72 -36
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +4 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +91 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +24 -6
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.js.map +1 -0
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +116 -40
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-node-runner.ts +3 -2
- package/src/authored-root.ts +150 -7
- package/src/authored-worker-step.ts +1 -0
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +25 -4
- package/src/cli/cli-probe.ts +73 -37
- package/src/cli/direct-run.ts +4 -1
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +103 -7
- package/src/cli/step-failure.ts +18 -0
- package/src/cloud-run.ts +9 -2
- package/src/communication/worker.ts +24 -9
- package/src/compile.ts +28 -13
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/llm-worker.ts +6 -4
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +9 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/worker-cli.ts +123 -37
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +6 -4
- package/src/wrapper-session.ts +89 -56
package/src/wrapper-session.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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,
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
194
|
-
//
|
|
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.
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
|
339
|
-
//
|
|
340
|
-
// group
|
|
341
|
-
|
|
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
|
-
//
|
|
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
|
/**
|