@parall/cli 1.51.0 → 1.52.1
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/commands/clip.d.ts.map +1 -1
- package/dist/commands/clip.js +36 -8
- package/dist/commands/messages.d.ts +30 -0
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +35 -8
- package/dist/lib/edge-exec.d.ts +50 -34
- package/dist/lib/edge-exec.d.ts.map +1 -1
- package/dist/lib/edge-exec.js +79 -39
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAiSpD"}
|
package/dist/commands/clip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveCredentials } from '../lib/client.js';
|
|
2
2
|
import { invokeClipAwaitingActivation } from '../lib/clip-invoke.js';
|
|
3
|
-
import {
|
|
3
|
+
import { execEdgeClipWithBoundedWait } from '../lib/edge-exec.js';
|
|
4
4
|
import { printJson, printError, parsePositiveInt } from '../lib/output.js';
|
|
5
5
|
/**
|
|
6
6
|
* Derive a CLI-friendly alias from a registry source string.
|
|
@@ -134,6 +134,32 @@ export function registerClipCommands(program) {
|
|
|
134
134
|
printError(err);
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
+
clip
|
|
138
|
+
.command('tools')
|
|
139
|
+
.description('List the MCP tool schemas (name, description, inputSchema) of an MCP clip by alias — the discovery step before `clip exec <clip> <tool>`')
|
|
140
|
+
.argument('<alias>', 'Clip alias')
|
|
141
|
+
.action(async (alias) => {
|
|
142
|
+
try {
|
|
143
|
+
const { client, orgId } = resolveCredentials();
|
|
144
|
+
// Resolve alias to clip ID — same lookup `clip connections`/`info` use.
|
|
145
|
+
const clips = await client.listClips(orgId);
|
|
146
|
+
const found = clips.find((c) => c.alias === alias);
|
|
147
|
+
if (!found) {
|
|
148
|
+
printError(new Error(`Clip with alias "${alias}" not found`));
|
|
149
|
+
}
|
|
150
|
+
// MCP tools are NOT frozen in the clip manifest, so `clip info` cannot
|
|
151
|
+
// show them; the live server snapshot behind getClipMCPConfig is the
|
|
152
|
+
// only source. name/description/inputSchema ride verbatim in each tool
|
|
153
|
+
// object — print it as-is (untrusted external data, not instructions).
|
|
154
|
+
// A non-MCP clip has no config here: the server answers 404 NOT_FOUND
|
|
155
|
+
// ("this clip has no MCP config"), which printError surfaces faithfully.
|
|
156
|
+
const config = await client.getClipMCPConfig(orgId, found.id);
|
|
157
|
+
printJson(config.tools ?? []);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
printError(err);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
137
163
|
clip
|
|
138
164
|
.command('invoke')
|
|
139
165
|
.description('Invoke a clip command')
|
|
@@ -225,13 +251,15 @@ export function registerClipCommands(program) {
|
|
|
225
251
|
profile: opts?.profile,
|
|
226
252
|
timeout: timeoutMs,
|
|
227
253
|
};
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
254
|
+
// The three provably-safe refusals — EDGE_ACTIVATING (cold start),
|
|
255
|
+
// EDGE_BUSY (single exec slot), EDGE_CONCURRENCY_LIMIT (org capacity)
|
|
256
|
+
// — are absorbed here with a bounded wait (per-code Retry-After
|
|
257
|
+
// pacing, one absolute deadline, one correlation id) instead of
|
|
258
|
+
// making every agent hand-roll a loop. Everything else —
|
|
259
|
+
// OUTCOME_UNKNOWN above all — propagates untouched: a dispatched
|
|
260
|
+
// command may have executed, and retrying it is not this tool's
|
|
261
|
+
// call. See lib/edge-exec.ts.
|
|
262
|
+
const result = await execEdgeClipWithBoundedWait(client, orgId, req);
|
|
235
263
|
if (result.success) {
|
|
236
264
|
printJson(result.data ?? null);
|
|
237
265
|
}
|
|
@@ -1,3 +1,33 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Message ↔ session/step attribution for a send. Lane context WINS over the
|
|
4
|
+
* process-level runtime context: the lane file is written per dispatch (a
|
|
5
|
+
* fork turn carries its child ase_/step), while PRLL_CONTEXT_FILE on
|
|
6
|
+
* single-process runtimes (codex app-server) is pinned to the MAIN session
|
|
7
|
+
* for the process's whole life — letting it win stamps fork replies and
|
|
8
|
+
* their dispatch rows with main's session. Runtime context only fills what
|
|
9
|
+
* the lane context does not provide.
|
|
10
|
+
*
|
|
11
|
+
* session and step are resolved as a PAIR, never independently: the server
|
|
12
|
+
* validates agent_step_id first and derives agent_session_id FROM that step,
|
|
13
|
+
* ignoring the supplied session whenever the step is valid (its check is
|
|
14
|
+
* chat-level, so a main-session step passes for a same-chat fork); when the
|
|
15
|
+
* step is stale it falls back to the supplied session instead. Either half
|
|
16
|
+
* can therefore decide the attribution, so a pair mixed from two sources has
|
|
17
|
+
* no single correct reading. The lane file writes session_id and step_id
|
|
18
|
+
* independently and may carry either alone — so once the lane supplies ANY
|
|
19
|
+
* attribution it owns both fields, and the runtime fills a gap only when it
|
|
20
|
+
* demonstrably describes the same session the lane names.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveSendAttribution(ctx: {
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
stepId?: string;
|
|
25
|
+
}, laneCtx: {
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
stepId?: string;
|
|
28
|
+
} | null): {
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
stepId?: string;
|
|
31
|
+
};
|
|
2
32
|
export declare function registerMessageCommands(program: Command): void;
|
|
3
33
|
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5C,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GACtD;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASzC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAoPvD"}
|
|
@@ -4,6 +4,36 @@ import { LaneContextError, markLaneReplyCommitted, resolveCredentials, resolveLa
|
|
|
4
4
|
import { printError, printJson, printRefHint, stripPrllScheme } from '../lib/output.js';
|
|
5
5
|
import { NO_BODY_ERROR, resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC, } from '../lib/text-input.js';
|
|
6
6
|
import { uploadFile } from '../lib/upload.js';
|
|
7
|
+
/**
|
|
8
|
+
* Message ↔ session/step attribution for a send. Lane context WINS over the
|
|
9
|
+
* process-level runtime context: the lane file is written per dispatch (a
|
|
10
|
+
* fork turn carries its child ase_/step), while PRLL_CONTEXT_FILE on
|
|
11
|
+
* single-process runtimes (codex app-server) is pinned to the MAIN session
|
|
12
|
+
* for the process's whole life — letting it win stamps fork replies and
|
|
13
|
+
* their dispatch rows with main's session. Runtime context only fills what
|
|
14
|
+
* the lane context does not provide.
|
|
15
|
+
*
|
|
16
|
+
* session and step are resolved as a PAIR, never independently: the server
|
|
17
|
+
* validates agent_step_id first and derives agent_session_id FROM that step,
|
|
18
|
+
* ignoring the supplied session whenever the step is valid (its check is
|
|
19
|
+
* chat-level, so a main-session step passes for a same-chat fork); when the
|
|
20
|
+
* step is stale it falls back to the supplied session instead. Either half
|
|
21
|
+
* can therefore decide the attribution, so a pair mixed from two sources has
|
|
22
|
+
* no single correct reading. The lane file writes session_id and step_id
|
|
23
|
+
* independently and may carry either alone — so once the lane supplies ANY
|
|
24
|
+
* attribution it owns both fields, and the runtime fills a gap only when it
|
|
25
|
+
* demonstrably describes the same session the lane names.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveSendAttribution(ctx, laneCtx) {
|
|
28
|
+
if (!laneCtx?.sessionId && !laneCtx?.stepId) {
|
|
29
|
+
return { sessionId: ctx.sessionId, stepId: ctx.stepId };
|
|
30
|
+
}
|
|
31
|
+
const sameSession = laneCtx.sessionId !== undefined && laneCtx.sessionId === ctx.sessionId;
|
|
32
|
+
return {
|
|
33
|
+
sessionId: laneCtx.sessionId,
|
|
34
|
+
stepId: laneCtx.stepId ?? (sameSession ? ctx.stepId : undefined),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
7
37
|
export function registerMessageCommands(program) {
|
|
8
38
|
const messages = program.command('messages').description('Manage messages');
|
|
9
39
|
messages
|
|
@@ -129,10 +159,6 @@ export function registerMessageCommands(program) {
|
|
|
129
159
|
req.thread_root_id = threadRootId;
|
|
130
160
|
if (opts.reply === false)
|
|
131
161
|
req.hints = { no_reply: true };
|
|
132
|
-
if (ctx.stepId)
|
|
133
|
-
req.agent_step_id = ctx.stepId;
|
|
134
|
-
if (ctx.sessionId)
|
|
135
|
-
req.agent_session_id = ctx.sessionId;
|
|
136
162
|
// Dispatch lane binding (PRLL_CONTEXT_DIR contract): a send whose
|
|
137
163
|
// exact (chat, thread) target has an active lane context rides the
|
|
138
164
|
// server's dispatch ledger — the lane token authorizes the write,
|
|
@@ -143,6 +169,11 @@ export function registerMessageCommands(program) {
|
|
|
143
169
|
// per-invocation idempotency key. All of this stays out of the
|
|
144
170
|
// model's view.
|
|
145
171
|
const laneCtx = resolveLaneDispatchContext(chatId, threadRootId);
|
|
172
|
+
const attribution = resolveSendAttribution(ctx, laneCtx);
|
|
173
|
+
if (attribution.stepId)
|
|
174
|
+
req.agent_step_id = attribution.stepId;
|
|
175
|
+
if (attribution.sessionId)
|
|
176
|
+
req.agent_session_id = attribution.sessionId;
|
|
146
177
|
let usedReplyKey = false;
|
|
147
178
|
if (laneCtx) {
|
|
148
179
|
req.dispatch_lane = laneCtx.lane;
|
|
@@ -153,10 +184,6 @@ export function registerMessageCommands(program) {
|
|
|
153
184
|
else {
|
|
154
185
|
req.idempotency_key = `cli:${randomUUID()}`;
|
|
155
186
|
}
|
|
156
|
-
if (!req.agent_step_id && laneCtx.stepId)
|
|
157
|
-
req.agent_step_id = laneCtx.stepId;
|
|
158
|
-
if (!req.agent_session_id && laneCtx.sessionId)
|
|
159
|
-
req.agent_session_id = laneCtx.sessionId;
|
|
160
187
|
}
|
|
161
188
|
else {
|
|
162
189
|
// By-source dispatch binding (parel — PRLL_DISPATCH_SOURCE_* env,
|
package/dist/lib/edge-exec.d.ts
CHANGED
|
@@ -1,55 +1,71 @@
|
|
|
1
1
|
import type { EdgeClipExecResult, ExecEdgeClipRequest, ParallClient } from '@parall/sdk';
|
|
2
2
|
/**
|
|
3
|
-
* Cloud (hosted) Edge
|
|
3
|
+
* Cloud (hosted) Edge bounded wait — CLI-side retry for `clip exec` across the
|
|
4
|
+
* three refusals that are provably safe to retry
|
|
5
|
+
* (edge-concurrency-reclaim-redesign §2.3 / §3.1):
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* - 503 EDGE_ACTIVATING — the pod is cold-starting. Answered before
|
|
8
|
+
* any publish; "nothing happened" is locally provable.
|
|
9
|
+
* - 429 EDGE_CONCURRENCY_LIMIT — the org is at capacity. Same pre-publish
|
|
10
|
+
* guarantee, from the EnsureLease branch.
|
|
11
|
+
* - 409 EDGE_BUSY — the pod is running another exec. STRICTLY
|
|
12
|
+
* WEAKER guarantee: the request DID reach the pod, which refused it before
|
|
13
|
+
* starting the worker. Its safety rests on that reply being received — a
|
|
14
|
+
* reply lost on the way back surfaces as OUTCOME_UNKNOWN, never as
|
|
15
|
+
* EDGE_BUSY, so "thought busy, actually ran" cannot happen.
|
|
12
16
|
*
|
|
13
|
-
*
|
|
17
|
+
* Each code follows its own pace: the server's Retry-After wins when present;
|
|
18
|
+
* without it (older servers) each code falls back to its own default schedule.
|
|
19
|
+
* All three share ONE absolute deadline — the CLI's task boundary — never a
|
|
20
|
+
* per-code sum. On expiry the LAST typed ApiError is rethrown as-is, so the
|
|
21
|
+
* error's code names which class of wait ran out.
|
|
22
|
+
*
|
|
23
|
+
* Everything else propagates on first occurrence — above all OUTCOME_UNKNOWN
|
|
14
24
|
* (504): the command was dispatched and MAY HAVE EXECUTED; an automatic retry
|
|
15
25
|
* could post, order or delete twice. That decision belongs to the caller, with
|
|
16
26
|
* the request id from the error message, never to a loop in here.
|
|
17
27
|
*
|
|
18
|
-
* Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-
|
|
28
|
+
* Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-error
|
|
19
29
|
* semantics for every other consumer; only the CLI exec behavior waits.
|
|
20
|
-
* Mirrors lib/clip-invoke.ts (the v2 hosted-browser wait) — same budget, same
|
|
21
|
-
* schedule, same exact-code matching discipline.
|
|
22
30
|
*/
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
* roughly this budget plus one command timeout. */
|
|
26
|
-
export declare const
|
|
27
|
-
/**
|
|
31
|
+
/** One absolute wait deadline shared by ALL waitable codes (never per-code).
|
|
32
|
+
* Each attempt keeps its own full command timeout, so worst-case wall clock
|
|
33
|
+
* is roughly this budget plus one command timeout. */
|
|
34
|
+
export declare const EDGE_EXEC_WAIT_BUDGET_MS = 120000;
|
|
35
|
+
/** EDGE_ACTIVATING fallback schedule when the server sends no Retry-After
|
|
36
|
+
* (matches the pre-Retry-After deployed behavior); the last entry repeats. */
|
|
28
37
|
export declare const EDGE_ACTIVATION_BACKOFF_MS: number[];
|
|
38
|
+
/** EDGE_BUSY fallback: the in-flight exec finishes within its command timeout,
|
|
39
|
+
* so poll on the order of seconds. */
|
|
40
|
+
export declare const EDGE_BUSY_FALLBACK_BACKOFF_MS = 5000;
|
|
41
|
+
/** EDGE_CONCURRENCY_LIMIT fallback: capacity frees only through another
|
|
42
|
+
* lease's full release + teardown — slower and high-variance. */
|
|
43
|
+
export declare const EDGE_CONCURRENCY_FALLBACK_BACKOFF_MS = 15000;
|
|
29
44
|
/** Injectable time hooks so tests never really sleep. */
|
|
30
45
|
export type EdgeActivationWaitHooks = {
|
|
31
46
|
sleep?: (ms: number) => Promise<void>;
|
|
32
47
|
now?: () => number;
|
|
33
48
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
*/
|
|
49
|
+
type WaitableCode = 'EDGE_ACTIVATING' | 'EDGE_CONCURRENCY_LIMIT' | 'EDGE_BUSY';
|
|
50
|
+
/** The waitable refusal this error carries, or null. Only a real SDK ApiError
|
|
51
|
+
* with the exact status+code pairing counts — duck-typed shapes, bare
|
|
52
|
+
* statuses and everything else (routing errors, repair, command failures,
|
|
53
|
+
* timeouts, OUTCOME_UNKNOWN, other 5xx) mean waiting is pointless or unsafe. */
|
|
54
|
+
export declare function waitableEdgeRefusal(err: unknown): WaitableCode | null;
|
|
55
|
+
/** Back-compat predicate: exactly the cold-start signal. */
|
|
41
56
|
export declare function isEdgeActivationPending(err: unknown): boolean;
|
|
42
57
|
/**
|
|
43
|
-
* Execute an Edge clip,
|
|
44
|
-
*
|
|
58
|
+
* Execute an Edge clip, waiting (bounded) through activation, busy and
|
|
59
|
+
* capacity refusals.
|
|
45
60
|
*
|
|
46
|
-
* Retries the IDENTICAL request
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* Any other failure is rethrown immediately.
|
|
61
|
+
* Retries the IDENTICAL request, pacing each code by its server Retry-After
|
|
62
|
+
* (fallback: the code's own schedule), all under one absolute deadline. The
|
|
63
|
+
* whole loop carries one `correlation_id` (the caller's, or one minted here)
|
|
64
|
+
* so the server's exec audit reads the retries as a single logical call. Once
|
|
65
|
+
* the deadline passes, the LAST typed ApiError is rethrown as-is —
|
|
66
|
+
* status/code intact — so the printed error stays machine-readable and names
|
|
67
|
+
* the class of wait that ran out. Any other failure is rethrown immediately.
|
|
53
68
|
*/
|
|
54
|
-
export declare function
|
|
69
|
+
export declare function execEdgeClipWithBoundedWait(client: Pick<ParallClient, 'execEdgeClip'>, orgId: string, req: ExecEdgeClipRequest, hooks?: EdgeActivationWaitHooks): Promise<EdgeClipExecResult>;
|
|
70
|
+
export {};
|
|
55
71
|
//# sourceMappingURL=edge-exec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edge-exec.d.ts","sourceRoot":"","sources":["../../src/lib/edge-exec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGzF
|
|
1
|
+
{"version":3,"file":"edge-exec.d.ts","sourceRoot":"","sources":["../../src/lib/edge-exec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;uDAEuD;AACvD,eAAO,MAAM,wBAAwB,SAAU,CAAC;AAEhD;+EAC+E;AAC/E,eAAO,MAAM,0BAA0B,UAAsB,CAAC;AAE9D;uCACuC;AACvC,eAAO,MAAM,6BAA6B,OAAQ,CAAC;AAEnD;kEACkE;AAClE,eAAO,MAAM,oCAAoC,QAAS,CAAC;AAE3D,yDAAyD;AACzD,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAIF,KAAK,YAAY,GAAG,iBAAiB,GAAG,wBAAwB,GAAG,WAAW,CAAC;AAgB/E;;;iFAGiF;AACjF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAIrE;AAED,4DAA4D;AAC5D,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,EAC1C,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,mBAAmB,EACxB,KAAK,CAAC,EAAE,uBAAuB,GAC9B,OAAO,CAAC,kBAAkB,CAAC,CAiC7B"}
|
package/dist/lib/edge-exec.js
CHANGED
|
@@ -1,74 +1,114 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { ApiError } from '@parall/sdk';
|
|
3
3
|
/**
|
|
4
|
-
* Cloud (hosted) Edge
|
|
4
|
+
* Cloud (hosted) Edge bounded wait — CLI-side retry for `clip exec` across the
|
|
5
|
+
* three refusals that are provably safe to retry
|
|
6
|
+
* (edge-concurrency-reclaim-redesign §2.3 / §3.1):
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - 503 EDGE_ACTIVATING — the pod is cold-starting. Answered before
|
|
9
|
+
* any publish; "nothing happened" is locally provable.
|
|
10
|
+
* - 429 EDGE_CONCURRENCY_LIMIT — the org is at capacity. Same pre-publish
|
|
11
|
+
* guarantee, from the EnsureLease branch.
|
|
12
|
+
* - 409 EDGE_BUSY — the pod is running another exec. STRICTLY
|
|
13
|
+
* WEAKER guarantee: the request DID reach the pod, which refused it before
|
|
14
|
+
* starting the worker. Its safety rests on that reply being received — a
|
|
15
|
+
* reply lost on the way back surfaces as OUTCOME_UNKNOWN, never as
|
|
16
|
+
* EDGE_BUSY, so "thought busy, actually ran" cannot happen.
|
|
13
17
|
*
|
|
14
|
-
*
|
|
18
|
+
* Each code follows its own pace: the server's Retry-After wins when present;
|
|
19
|
+
* without it (older servers) each code falls back to its own default schedule.
|
|
20
|
+
* All three share ONE absolute deadline — the CLI's task boundary — never a
|
|
21
|
+
* per-code sum. On expiry the LAST typed ApiError is rethrown as-is, so the
|
|
22
|
+
* error's code names which class of wait ran out.
|
|
23
|
+
*
|
|
24
|
+
* Everything else propagates on first occurrence — above all OUTCOME_UNKNOWN
|
|
15
25
|
* (504): the command was dispatched and MAY HAVE EXECUTED; an automatic retry
|
|
16
26
|
* could post, order or delete twice. That decision belongs to the caller, with
|
|
17
27
|
* the request id from the error message, never to a loop in here.
|
|
18
28
|
*
|
|
19
|
-
* Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-
|
|
29
|
+
* Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-error
|
|
20
30
|
* semantics for every other consumer; only the CLI exec behavior waits.
|
|
21
|
-
* Mirrors lib/clip-invoke.ts (the v2 hosted-browser wait) — same budget, same
|
|
22
|
-
* schedule, same exact-code matching discipline.
|
|
23
31
|
*/
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
* roughly this budget plus one command timeout. */
|
|
27
|
-
export const
|
|
28
|
-
/**
|
|
32
|
+
/** One absolute wait deadline shared by ALL waitable codes (never per-code).
|
|
33
|
+
* Each attempt keeps its own full command timeout, so worst-case wall clock
|
|
34
|
+
* is roughly this budget plus one command timeout. */
|
|
35
|
+
export const EDGE_EXEC_WAIT_BUDGET_MS = 120_000;
|
|
36
|
+
/** EDGE_ACTIVATING fallback schedule when the server sends no Retry-After
|
|
37
|
+
* (matches the pre-Retry-After deployed behavior); the last entry repeats. */
|
|
29
38
|
export const EDGE_ACTIVATION_BACKOFF_MS = [500, 1_000, 2_000];
|
|
39
|
+
/** EDGE_BUSY fallback: the in-flight exec finishes within its command timeout,
|
|
40
|
+
* so poll on the order of seconds. */
|
|
41
|
+
export const EDGE_BUSY_FALLBACK_BACKOFF_MS = 5_000;
|
|
42
|
+
/** EDGE_CONCURRENCY_LIMIT fallback: capacity frees only through another
|
|
43
|
+
* lease's full release + teardown — slower and high-variance. */
|
|
44
|
+
export const EDGE_CONCURRENCY_FALLBACK_BACKOFF_MS = 15_000;
|
|
30
45
|
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/** Exact (status, code) pairs — a matching code on the wrong status is a
|
|
47
|
+
* malformed answer and must fail fast, not wait. attempt counts per code so
|
|
48
|
+
* the activation ramp is not consumed by interleaved busy/capacity waits. */
|
|
49
|
+
const WAITABLE = {
|
|
50
|
+
EDGE_ACTIVATING: {
|
|
51
|
+
status: 503,
|
|
52
|
+
fallbackMs: (attempt) => EDGE_ACTIVATION_BACKOFF_MS[Math.min(attempt, EDGE_ACTIVATION_BACKOFF_MS.length - 1)],
|
|
53
|
+
},
|
|
54
|
+
EDGE_CONCURRENCY_LIMIT: { status: 429, fallbackMs: () => EDGE_CONCURRENCY_FALLBACK_BACKOFF_MS },
|
|
55
|
+
EDGE_BUSY: { status: 409, fallbackMs: () => EDGE_BUSY_FALLBACK_BACKOFF_MS },
|
|
56
|
+
};
|
|
57
|
+
/** The waitable refusal this error carries, or null. Only a real SDK ApiError
|
|
58
|
+
* with the exact status+code pairing counts — duck-typed shapes, bare
|
|
59
|
+
* statuses and everything else (routing errors, repair, command failures,
|
|
60
|
+
* timeouts, OUTCOME_UNKNOWN, other 5xx) mean waiting is pointless or unsafe. */
|
|
61
|
+
export function waitableEdgeRefusal(err) {
|
|
62
|
+
if (!(err instanceof ApiError) || !err.code)
|
|
63
|
+
return null;
|
|
64
|
+
const spec = WAITABLE[err.code];
|
|
65
|
+
return spec && err.status === spec.status ? err.code : null;
|
|
66
|
+
}
|
|
67
|
+
/** Back-compat predicate: exactly the cold-start signal. */
|
|
38
68
|
export function isEdgeActivationPending(err) {
|
|
39
|
-
return err
|
|
69
|
+
return waitableEdgeRefusal(err) === 'EDGE_ACTIVATING';
|
|
40
70
|
}
|
|
41
71
|
/**
|
|
42
|
-
* Execute an Edge clip,
|
|
43
|
-
*
|
|
72
|
+
* Execute an Edge clip, waiting (bounded) through activation, busy and
|
|
73
|
+
* capacity refusals.
|
|
44
74
|
*
|
|
45
|
-
* Retries the IDENTICAL request
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Any other failure is rethrown immediately.
|
|
75
|
+
* Retries the IDENTICAL request, pacing each code by its server Retry-After
|
|
76
|
+
* (fallback: the code's own schedule), all under one absolute deadline. The
|
|
77
|
+
* whole loop carries one `correlation_id` (the caller's, or one minted here)
|
|
78
|
+
* so the server's exec audit reads the retries as a single logical call. Once
|
|
79
|
+
* the deadline passes, the LAST typed ApiError is rethrown as-is —
|
|
80
|
+
* status/code intact — so the printed error stays machine-readable and names
|
|
81
|
+
* the class of wait that ran out. Any other failure is rethrown immediately.
|
|
52
82
|
*/
|
|
53
|
-
export async function
|
|
83
|
+
export async function execEdgeClipWithBoundedWait(client, orgId, req, hooks) {
|
|
54
84
|
const sleep = hooks?.sleep ?? realSleep;
|
|
55
85
|
const now = hooks?.now ?? Date.now;
|
|
56
86
|
const pinned = {
|
|
57
87
|
...req,
|
|
58
88
|
correlation_id: req.correlation_id ?? randomUUID(),
|
|
59
89
|
};
|
|
60
|
-
const deadline = now() +
|
|
61
|
-
|
|
90
|
+
const deadline = now() + EDGE_EXEC_WAIT_BUDGET_MS;
|
|
91
|
+
const attempts = {
|
|
92
|
+
EDGE_ACTIVATING: 0,
|
|
93
|
+
EDGE_CONCURRENCY_LIMIT: 0,
|
|
94
|
+
EDGE_BUSY: 0,
|
|
95
|
+
};
|
|
96
|
+
for (;;) {
|
|
62
97
|
try {
|
|
63
98
|
return await client.execEdgeClip(orgId, pinned);
|
|
64
99
|
}
|
|
65
100
|
catch (err) {
|
|
66
|
-
|
|
101
|
+
const code = waitableEdgeRefusal(err);
|
|
102
|
+
if (!code)
|
|
67
103
|
throw err;
|
|
68
104
|
const remaining = deadline - now();
|
|
69
105
|
if (remaining <= 0)
|
|
70
106
|
throw err;
|
|
71
|
-
const
|
|
107
|
+
const retryAfterSeconds = err.retryAfterSeconds;
|
|
108
|
+
const backoff = typeof retryAfterSeconds === 'number' && retryAfterSeconds > 0
|
|
109
|
+
? retryAfterSeconds * 1000
|
|
110
|
+
: WAITABLE[code].fallbackMs(attempts[code]);
|
|
111
|
+
attempts[code] += 1;
|
|
72
112
|
// Clamp to the remaining budget so the final sleep can't overrun it.
|
|
73
113
|
await sleep(Math.min(backoff, remaining));
|
|
74
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "CLI client for Parall — universal agent & human access to Parall API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"diff": "^8.0.3",
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@parall/
|
|
40
|
-
"@parall/
|
|
39
|
+
"@parall/sdk": "1.52.1",
|
|
40
|
+
"@parall/agent-core": "1.52.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/js-yaml": "^4.0.9",
|
|
44
44
|
"@types/node": "^22.0.0",
|
|
45
45
|
"typescript": "^5.7.0",
|
|
46
|
-
"@parall/agent-core": "1.
|
|
46
|
+
"@parall/agent-core": "1.52.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc",
|