@relayflows/sdk 2.0.38 → 2.0.40
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-transcript.d.ts +9 -0
- package/dist/agent-transcript.d.ts.map +1 -1
- package/dist/agent-transcript.js +12 -0
- package/dist/agent-transcript.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +22 -4
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +5 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +52 -3
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +9 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +24 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-versions.d.ts +22 -0
- package/dist/cli/cloud-versions.d.ts.map +1 -0
- package/dist/cli/cloud-versions.js +96 -0
- package/dist/cli/cloud-versions.js.map +1 -0
- package/dist/cli/step-evidence.d.ts +5 -0
- package/dist/cli/step-evidence.d.ts.map +1 -1
- package/dist/cli/step-evidence.js +45 -7
- package/dist/cli/step-evidence.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +13 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +27 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +18 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-transport-evidence.d.ts +30 -0
- package/dist/cli-transport-evidence.d.ts.map +1 -0
- package/dist/cli-transport-evidence.js +31 -0
- package/dist/cli-transport-evidence.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +17 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +8 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-versions-wire.d.ts +25 -0
- package/dist/cloud-versions-wire.d.ts.map +1 -0
- package/dist/cloud-versions-wire.js +39 -0
- package/dist/cloud-versions-wire.js.map +1 -0
- package/dist/cloud-versions.d.ts +47 -0
- package/dist/cloud-versions.d.ts.map +1 -0
- package/dist/cloud-versions.js +131 -0
- package/dist/cloud-versions.js.map +1 -0
- package/dist/compile.d.ts +3 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +37 -13
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +13 -0
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/pty-sidechannel.d.ts +17 -0
- package/dist/pty-sidechannel.d.ts.map +1 -1
- package/dist/pty-sidechannel.js +41 -2
- package/dist/pty-sidechannel.js.map +1 -1
- package/dist/spec.d.ts +6 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +2 -2
- package/dist/step-fields.d.ts.map +1 -1
- package/dist/step-fields.js +2 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/validate.d.ts +2 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +18 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +10 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +116 -44
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +4 -1
- package/dist/worker-spend.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +15 -4
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +2 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +29 -10
- package/dist/wrapper-session.js.map +1 -1
- package/dist/yaml-helpers.js +1 -1
- package/dist/yaml-helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-transcript.ts +12 -0
- package/src/authored-node-runner.ts +23 -5
- package/src/authored-root.ts +5 -6
- package/src/authored-worker-step.ts +58 -3
- package/src/cli/cloud-deploy.ts +21 -7
- package/src/cli/cloud-versions.ts +103 -0
- package/src/cli/step-evidence.ts +51 -7
- package/src/cli/step-failure.ts +13 -0
- package/src/cli-commands.ts +18 -0
- package/src/cli-transport-evidence.ts +70 -0
- package/src/cli.ts +15 -0
- package/src/cloud-deploy.ts +24 -3
- package/src/cloud-versions-wire.ts +58 -0
- package/src/cloud-versions.ts +179 -0
- package/src/compile.ts +39 -13
- package/src/failure-kinds.ts +13 -0
- package/src/pty-sidechannel.ts +38 -2
- package/src/spec.ts +6 -1
- package/src/step-fields.ts +2 -1
- package/src/validate.ts +18 -0
- package/src/worker-cli.ts +140 -42
- package/src/worker-spend.ts +4 -1
- package/src/worker.ts +21 -4
- package/src/wrapper-session.ts +32 -10
- package/src/yaml-helpers.ts +1 -1
|
@@ -2,7 +2,8 @@ import { dirname, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
|
2
2
|
import type { AuthoredBudget } from './authored-budget.js';
|
|
3
3
|
import { parseBudget } from './budget.js';
|
|
4
4
|
import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface';
|
|
5
|
-
import { compileSpec, toKernelSpec } from './compile.js';
|
|
5
|
+
import { compileSpec, toKernelSpec, parseAgentStepTimeout } from './compile.js';
|
|
6
|
+
import { isTransportRetries } from './validate.js';
|
|
6
7
|
import { authoredPreflight } from './authored-preflight.js';
|
|
7
8
|
import { classifyOutcome, type RunLifecycleOptions, type RunReport } from './cli/run.js';
|
|
8
9
|
import type { PreflightDiagnostic } from './preflight.js';
|
|
@@ -18,6 +19,7 @@ import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
|
18
19
|
import type { StepFailedDetails } from './failure-kinds.js';
|
|
19
20
|
import { authoredWorkerSlots, type AuthoredWorkerSlots } from './worker-slots.js';
|
|
20
21
|
|
|
22
|
+
const AGENT_TIMEOUT = Symbol('declared agent timeout');
|
|
21
23
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
22
24
|
|
|
23
25
|
/** Agent and LLM calls share the exact declarative preflight and lease wait. */
|
|
@@ -117,6 +119,7 @@ export function authoredWorkerRunner(
|
|
|
117
119
|
const diagnostic = execution.report.diagnostics.at(-1);
|
|
118
120
|
const details = stepDetails(diagnostic);
|
|
119
121
|
const reason = execution.report.completionReason;
|
|
122
|
+
let indexFailure = '';
|
|
120
123
|
let message = diagnostic?.message
|
|
121
124
|
?? `flow "${definition.name}" step "${id}" did not complete successfully `
|
|
122
125
|
+ `(status: ${execution.report.status ?? 'unknown'})`;
|
|
@@ -127,13 +130,21 @@ export function authoredWorkerRunner(
|
|
|
127
130
|
journalSteps.push(Object.freeze({
|
|
128
131
|
id, runId: outcome.run_id, completionReason: details.completionReason, ...stepEdges?.(id),
|
|
129
132
|
}));
|
|
130
|
-
|
|
133
|
+
indexFailure = await alsoRecord(journal, rootRunId, {
|
|
131
134
|
step: id, runId: outcome.run_id, state: 'completed',
|
|
132
135
|
completionReason: details.completionReason,
|
|
133
136
|
...(details.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
134
137
|
...stepEdges?.(id),
|
|
135
138
|
});
|
|
136
139
|
}
|
|
140
|
+
message += indexFailure;
|
|
141
|
+
// Resolve inside consume so budget accounting does not latch a failure.
|
|
142
|
+
// A failed index append must still fail closed, even for a tolerated timeout.
|
|
143
|
+
if (indexFailure === '' && reason === 'step_failed' && execution.report.status === 'failed'
|
|
144
|
+
&& spec.steps.length === 1 && step.type === 'agent' && step.timeoutMs !== undefined
|
|
145
|
+
&& details?.stepId === id && details.stepType === 'agent' && details.completionReason === 'timeout') {
|
|
146
|
+
return { [AGENT_TIMEOUT]: details.stderrTail ?? message };
|
|
147
|
+
}
|
|
137
148
|
throw new AuthoredFlowExecutionError(
|
|
138
149
|
'step_failed', message,
|
|
139
150
|
isSurfaceCompletionReason(reason) ? reason : undefined,
|
|
@@ -217,15 +228,47 @@ export function authoredWorkerRunner(
|
|
|
217
228
|
`f.agent options.transport must be 'direct' or 'relay' (got ${JSON.stringify(options.transport)}).`,
|
|
218
229
|
);
|
|
219
230
|
}
|
|
231
|
+
if (options.maxIterations !== undefined
|
|
232
|
+
&& (!Number.isSafeInteger(options.maxIterations) || options.maxIterations < 1)) {
|
|
233
|
+
throw new AuthoredFlowExecutionError(
|
|
234
|
+
'agent_cli_unresolved',
|
|
235
|
+
`f.agent options.maxIterations must be a positive integer (got ${JSON.stringify(options.maxIterations)}).`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (options.transportRetries !== undefined && !isTransportRetries(options.transportRetries)) {
|
|
239
|
+
throw new AuthoredFlowExecutionError(
|
|
240
|
+
'agent_cli_unresolved',
|
|
241
|
+
`f.agent options.transportRetries must be a non-negative integer (got ${JSON.stringify(options.transportRetries)}).`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (options.recoveryMode !== undefined
|
|
245
|
+
&& options.recoveryMode !== 'reset'
|
|
246
|
+
&& options.recoveryMode !== 'inspect'
|
|
247
|
+
&& options.recoveryMode !== 'manual') {
|
|
248
|
+
throw new AuthoredFlowExecutionError(
|
|
249
|
+
'agent_cli_unresolved',
|
|
250
|
+
`f.agent options.recoveryMode must be 'reset', 'inspect', or 'manual' (got ${JSON.stringify(options.recoveryMode)}).`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
220
253
|
const cwdTransport = agentCwdTransportError(declaredCwd, options.transport);
|
|
221
254
|
if (cwdTransport !== undefined) {
|
|
222
255
|
throw new AuthoredFlowExecutionError('agent_cli_unresolved', `f.agent options.${cwdTransport}.`);
|
|
223
256
|
}
|
|
257
|
+
let timeoutMs: number | undefined;
|
|
258
|
+
if (options.timeout !== undefined) {
|
|
259
|
+
try { timeoutMs = parseAgentStepTimeout(options.timeout); }
|
|
260
|
+
catch (error) { throw new AuthoredFlowExecutionError('agent_cli_unresolved', String(error)); }
|
|
261
|
+
if (options.transport === 'relay' || (options.maxIterations ?? 1) > 1) {
|
|
262
|
+
throw new AuthoredFlowExecutionError('agent_cli_unresolved',
|
|
263
|
+
'f.agent timeout requires direct transport and maxIterations 1.');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
224
266
|
const permissions = options.permissions;
|
|
225
267
|
const permissionsSnapshot = permissions === undefined ? undefined
|
|
226
268
|
: snapshotJsonValue(permissions, 'f.agent options.permissions') as unknown as PermissionsSpec;
|
|
227
269
|
const output = await run({
|
|
228
270
|
id, type: 'agent', instruction: options.task,
|
|
271
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
229
272
|
...(permissionsSnapshot === undefined ? {} : { permissions: permissionsSnapshot }),
|
|
230
273
|
...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }),
|
|
231
274
|
...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }),
|
|
@@ -233,11 +276,17 @@ export function authoredWorkerRunner(
|
|
|
233
276
|
...(options.model === undefined ? {} : { model: options.model }),
|
|
234
277
|
...(declaredCwd === undefined ? {} : { cwd: declaredCwd }),
|
|
235
278
|
...(options.transport === undefined ? {} : { transport: options.transport }),
|
|
279
|
+
...(options.maxIterations === undefined ? {} : { maxIterations: options.maxIterations }),
|
|
280
|
+
...(options.transportRetries === undefined ? {} : { transportRetries: options.transportRetries }),
|
|
281
|
+
...(options.recoveryMode === undefined ? {} : { recoveryMode: options.recoveryMode }),
|
|
236
282
|
...(verification === undefined ? {} : { verification }),
|
|
237
283
|
});
|
|
238
284
|
if (typeof output !== 'object' || output === null || Array.isArray(output)) {
|
|
239
285
|
throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`);
|
|
240
286
|
}
|
|
287
|
+
if (AGENT_TIMEOUT in output && timeoutMs !== undefined) {
|
|
288
|
+
return { completionReason: 'timeout', summary: String(output[AGENT_TIMEOUT]), artifacts: [] };
|
|
289
|
+
}
|
|
241
290
|
const stdout = 'stdout_tail' in output ? output.stdout_tail : undefined;
|
|
242
291
|
// The worker that ran the CLI measured the artifacts and journaled them
|
|
243
292
|
// in the step's output; read that fact back rather than re-scanning a
|
|
@@ -245,7 +294,7 @@ export function authoredWorkerRunner(
|
|
|
245
294
|
const journaled = 'artifacts' in output ? output.artifacts : undefined;
|
|
246
295
|
const artifacts = Array.isArray(journaled) && journaled.every(entry => typeof entry === 'string')
|
|
247
296
|
? [...journaled] : [];
|
|
248
|
-
return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts };
|
|
297
|
+
return { completionReason: 'success', summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts };
|
|
249
298
|
},
|
|
250
299
|
async llm(id: string, prompt: string, options?: LlmOptions, verification?: NamedGate): Promise<unknown> {
|
|
251
300
|
if (options !== undefined && (typeof options !== 'object' || options === null || options.output === undefined)) {
|
|
@@ -343,8 +392,14 @@ function stepDetails(
|
|
|
343
392
|
...(found.completionReason === undefined ? {} : { completionReason: found.completionReason }),
|
|
344
393
|
...(found.attempt === undefined ? {} : { attempt: found.attempt }),
|
|
345
394
|
...(found.maxIterations === undefined ? {} : { maxIterations: found.maxIterations }),
|
|
395
|
+
...(found.transportRetries === undefined ? {} : { transportRetries: found.transportRetries }),
|
|
346
396
|
...(found.unchargedAttempts === undefined ? {} : { unchargedAttempts: found.unchargedAttempts }),
|
|
347
397
|
...(found.exitCode === undefined ? {} : { exitCode: found.exitCode }),
|
|
398
|
+
...(found.transportPhase === undefined ? {} : { transportPhase: found.transportPhase }),
|
|
399
|
+
...(found.transportCause === undefined ? {} : { transportCause: found.transportCause }),
|
|
400
|
+
...(found.signal === undefined ? {} : { signal: found.signal }),
|
|
401
|
+
...(found.errorCode === undefined ? {} : { errorCode: found.errorCode }),
|
|
402
|
+
...(found.retryableTransport === undefined ? {} : { retryableTransport: found.retryableTransport }),
|
|
348
403
|
...(found.stdoutTail === undefined ? {} : { stdoutTail: found.stdoutTail }),
|
|
349
404
|
...(found.stderrTail === undefined ? {} : { stderrTail: found.stderrTail }),
|
|
350
405
|
...(found.detail === undefined ? {} : { detail: found.detail }),
|
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -4,13 +4,17 @@ import {
|
|
|
4
4
|
type FlowTriggerSource,
|
|
5
5
|
} from '../cloud-deploy.js';
|
|
6
6
|
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { describeVersionChange } from '../cloud-versions-wire.js';
|
|
8
|
+
import { runCloudDeployVersionCli } from './cloud-versions.js';
|
|
7
9
|
import { cliConnectPrompt, flowRequirementsForPath, harnessRemedy } from './cloud-connect-cli.js';
|
|
8
10
|
import type { CliIo } from '../cli.js';
|
|
9
11
|
|
|
10
12
|
export interface CloudDeployArgs {
|
|
11
13
|
command: 'cloud-deploy';
|
|
12
14
|
value: string;
|
|
13
|
-
|
|
15
|
+
/** Update form: the flow (name or listener id) this source becomes the next version of. */
|
|
16
|
+
flow: string | undefined;
|
|
17
|
+
repo: string | undefined;
|
|
14
18
|
on: string[];
|
|
15
19
|
approver: string | undefined;
|
|
16
20
|
name: string | undefined;
|
|
@@ -25,6 +29,11 @@ export interface CloudDeployArgs {
|
|
|
25
29
|
/**
|
|
26
30
|
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
27
31
|
* --approver <handle> [--name <n>] [--agents <list>] [--draft] [--no-connect] [--json]`
|
|
32
|
+
* `flows deploy <flow.ts> --flow <name-or-listener-id> [--plugin <ref>] [--no-connect] [--json]`
|
|
33
|
+
*
|
|
34
|
+
* The `--flow` form makes the source the next version of an existing flow and
|
|
35
|
+
* changes nothing else; listener settings given beside it are refused when run,
|
|
36
|
+
* by name, rather than silently ignored.
|
|
28
37
|
*
|
|
29
38
|
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
30
39
|
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
@@ -32,6 +41,7 @@ export interface CloudDeployArgs {
|
|
|
32
41
|
*/
|
|
33
42
|
export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs | undefined {
|
|
34
43
|
let value: string | undefined;
|
|
44
|
+
let flow: string | undefined;
|
|
35
45
|
let repo: string | undefined;
|
|
36
46
|
let approver: string | undefined;
|
|
37
47
|
let name: string | undefined;
|
|
@@ -72,11 +82,12 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
72
82
|
i += 1;
|
|
73
83
|
continue;
|
|
74
84
|
}
|
|
75
|
-
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
|
|
85
|
+
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on' || arg === '--flow') {
|
|
76
86
|
const next = args[i + 1];
|
|
77
87
|
if (next === undefined || next.startsWith('-')) return undefined;
|
|
78
88
|
i += 1;
|
|
79
89
|
if (arg === '--on') { on.push(next); continue; }
|
|
90
|
+
if (arg === '--flow') { if (flow !== undefined) return undefined; flow = next; continue; }
|
|
80
91
|
if (arg === '--repo') { if (repo !== undefined) return undefined; repo = next; continue; }
|
|
81
92
|
if (arg === '--approver') { if (approver !== undefined) return undefined; approver = next; continue; }
|
|
82
93
|
if (name !== undefined) return undefined;
|
|
@@ -86,8 +97,9 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
86
97
|
if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
87
98
|
value = arg;
|
|
88
99
|
}
|
|
89
|
-
if (value === undefined
|
|
90
|
-
|
|
100
|
+
if (value === undefined) return undefined;
|
|
101
|
+
if (flow === undefined && (repo === undefined || on.length === 0)) return undefined;
|
|
102
|
+
return { command: 'cloud-deploy', value, flow, repo, on, approver, name, agents, draft, noConnect, json, plugins };
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
function describeSource(source: FlowTriggerSource): string {
|
|
@@ -96,6 +108,7 @@ function describeSource(source: FlowTriggerSource): string {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
111
|
+
if (args.flow !== undefined) return runCloudDeployVersionCli({ ...args, flow: args.flow }, io);
|
|
99
112
|
const agents = args.agents === undefined ? undefined : parseAgentHarnessesOr(args.agents);
|
|
100
113
|
// What the source declares, so a harness refusal names the right remedy
|
|
101
114
|
// even when `--agents` was not given; the loader's own failure is reported
|
|
@@ -111,7 +124,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
111
124
|
if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
|
|
112
125
|
const deployment = await deployToCloud({
|
|
113
126
|
path: args.value,
|
|
114
|
-
repository: parseRepository(args.repo),
|
|
127
|
+
repository: parseRepository(args.repo!),
|
|
115
128
|
sources: args.on.map(parseTriggerSource),
|
|
116
129
|
approver: args.approver,
|
|
117
130
|
draft: args.draft,
|
|
@@ -124,7 +137,8 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
124
137
|
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
125
138
|
return 0;
|
|
126
139
|
}
|
|
127
|
-
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
140
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
141
|
+
+ (deployment.version === undefined ? '' : ` · ${describeVersionChange(deployment.version)}`));
|
|
128
142
|
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
129
143
|
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
130
144
|
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
@@ -181,7 +195,7 @@ export async function runCloudUndeployCli({ agentId, json }: { agentId: string;
|
|
|
181
195
|
}
|
|
182
196
|
}
|
|
183
197
|
|
|
184
|
-
function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
198
|
+
export function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
185
199
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
186
200
|
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
187
201
|
// Cloud names the missing coding-agent credential on activation; say how it is connected.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import {
|
|
3
|
+
activateCloudFlowVersion, deployVersionToCloud, getCloudListener, resolveCloudFlow,
|
|
4
|
+
} from '../cloud-versions.js';
|
|
5
|
+
import { describeVersionChange } from '../cloud-versions-wire.js';
|
|
6
|
+
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { cliConnectPrompt } from './cloud-connect-cli.js';
|
|
8
|
+
import { reportCloudFailure, type CloudDeployArgs } from './cloud-deploy.js';
|
|
9
|
+
import type { CliIo } from '../cli.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `flows deploy <flow.ts> --flow <name-or-id>`: the source becomes the next
|
|
13
|
+
* version of that flow. Listener settings are not part of a version, so a
|
|
14
|
+
* flag that would change one is refused by name instead of being dropped.
|
|
15
|
+
*/
|
|
16
|
+
export async function runCloudDeployVersionCli(
|
|
17
|
+
args: CloudDeployArgs & { flow: string }, io: CliIo,
|
|
18
|
+
): Promise<0 | 1 | 2> {
|
|
19
|
+
try {
|
|
20
|
+
const settings = [
|
|
21
|
+
args.repo === undefined ? undefined : '--repo',
|
|
22
|
+
args.on.length === 0 ? undefined : '--on',
|
|
23
|
+
args.approver === undefined ? undefined : '--approver',
|
|
24
|
+
args.name === undefined ? undefined : '--name',
|
|
25
|
+
args.agents === undefined ? undefined : '--agents',
|
|
26
|
+
args.draft ? '--draft' : undefined,
|
|
27
|
+
].filter((flag): flag is string => flag !== undefined);
|
|
28
|
+
if (settings.length > 0) {
|
|
29
|
+
throw new CloudFlowError('invalid_input',
|
|
30
|
+
`--flow deploys a new version of the flow's source only; ${settings.join(', ')} stay on the listener. `
|
|
31
|
+
+ 'Drop them, or change them in the Cloud dashboard.');
|
|
32
|
+
}
|
|
33
|
+
const connect = cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json });
|
|
34
|
+
const deployment = await deployVersionToCloud({
|
|
35
|
+
path: args.value,
|
|
36
|
+
flow: args.flow,
|
|
37
|
+
...(connect === undefined ? {} : { connect }),
|
|
38
|
+
...(args.plugins.length === 0 ? {} : { plugins: args.plugins }),
|
|
39
|
+
});
|
|
40
|
+
if (args.json) {
|
|
41
|
+
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
|
|
45
|
+
+ ` · ${describeVersionChange(deployment.version)}`);
|
|
46
|
+
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
47
|
+
const requires = describeFlowRequirements(deployment.requirements);
|
|
48
|
+
if (requires) io.stdout(` requires: ${requires}`);
|
|
49
|
+
for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
|
|
50
|
+
io.stdout('New runs launch this version; runs already started finish on theirs. History: flows versions '
|
|
51
|
+
+ JSON.stringify(deployment.name));
|
|
52
|
+
return 0;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return reportCloudFailure(error, args.json, io);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `flows versions <name-or-id>`: newest first, the active one marked. */
|
|
59
|
+
export async function runCloudVersionsCli(
|
|
60
|
+
{ flow, json }: { flow: string; json: boolean }, io: CliIo,
|
|
61
|
+
): Promise<0 | 1 | 2> {
|
|
62
|
+
try {
|
|
63
|
+
const listener = await getCloudListener(await resolveCloudFlow(flow));
|
|
64
|
+
if (json) {
|
|
65
|
+
io.stdout(JSON.stringify({
|
|
66
|
+
ok: true, agentId: listener.agentId, name: listener.name,
|
|
67
|
+
activeVersion: listener.activeVersion?.version ?? null, versions: listener.versions,
|
|
68
|
+
}));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (listener.versions.length === 0) {
|
|
72
|
+
io.stdout(`${listener.agentId} ${JSON.stringify(listener.name)} has no recorded versions.`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
io.stdout(`${listener.agentId} ${JSON.stringify(listener.name)} ${listener.status}`);
|
|
76
|
+
for (const version of listener.versions) {
|
|
77
|
+
const active = version.version === listener.activeVersion?.version ? ' (active)' : '';
|
|
78
|
+
io.stdout(` version ${version.version}${active} ${version.createdAt} ${version.origin} sha256 ${version.sourceSha256.slice(0, 12)}`);
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return reportCloudFailure(error, json, io);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `flows rollback <name-or-id> <version>`: move the pointer; later versions stay recorded. */
|
|
87
|
+
export async function runCloudRollbackCli(
|
|
88
|
+
{ flow, version, json }: { flow: string; version: string; json: boolean }, io: CliIo,
|
|
89
|
+
): Promise<0 | 1 | 2> {
|
|
90
|
+
try {
|
|
91
|
+
// Cloud numbers versions up to nine digits; anything longer cannot exist.
|
|
92
|
+
if (!/^[1-9][0-9]{0,8}$/u.test(version)) {
|
|
93
|
+
throw new CloudFlowError('invalid_input', `A version is a positive whole number, got "${version}".`);
|
|
94
|
+
}
|
|
95
|
+
const result = await activateCloudFlowVersion(flow, Number(version));
|
|
96
|
+
io.stdout(json
|
|
97
|
+
? JSON.stringify({ ok: true, ...result })
|
|
98
|
+
: `ACTIVATED ${result.agentId} ${result.status} · ${describeVersionChange(result.version)}`);
|
|
99
|
+
return 0;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return reportCloudFailure(error, json, io);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/cli/step-evidence.ts
CHANGED
|
@@ -17,6 +17,13 @@ export const ATTEMPT_TAIL_BYTES = 256;
|
|
|
17
17
|
*/
|
|
18
18
|
const PRODUCER_TRUNCATED = /…\s*\((?:render bounded|[\d,]+ bytes truncated)\)$/u;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* What the worker puts in front of a stderr it cut to its tail (`boundedTail`
|
|
22
|
+
* in worker.ts and cli-transport-evidence.ts). The same rule applies: equal
|
|
23
|
+
* surviving tails cannot establish equal failures.
|
|
24
|
+
*/
|
|
25
|
+
const STDERR_TRUNCATED = /^\[(?:worker|transport) stderr: [\d,]+ bytes truncated\]…/u;
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* The evidence fields a step failure reports, as the journal held them.
|
|
22
29
|
*
|
|
@@ -29,6 +36,11 @@ const PRODUCER_TRUNCATED = /…\s*\((?:render bounded|[\d,]+ bytes truncated)\)$
|
|
|
29
36
|
*/
|
|
30
37
|
export interface SelectedEvidence {
|
|
31
38
|
exitCode?: number;
|
|
39
|
+
transportPhase?: string;
|
|
40
|
+
transportCause?: string;
|
|
41
|
+
signal?: string;
|
|
42
|
+
errorCode?: string;
|
|
43
|
+
retryableTransport?: boolean;
|
|
32
44
|
/** Present only when nonempty. */
|
|
33
45
|
stdoutTail?: string;
|
|
34
46
|
/** Present whenever the journal carried one, including the empty string. */
|
|
@@ -57,16 +69,18 @@ export interface SelectedEvidence {
|
|
|
57
69
|
export function selectEvidence(payload: Record<string, unknown>): SelectedEvidence {
|
|
58
70
|
const detail = record(payload['verification'])?.['detail'];
|
|
59
71
|
const rendered = typeof detail === 'string' ? parsed(detail) : undefined;
|
|
72
|
+
const trajectory = record(payload['trajectory_tail']);
|
|
73
|
+
const transport = record(trajectory?.['transport']);
|
|
60
74
|
const candidates = [
|
|
61
75
|
record(payload['output']),
|
|
62
|
-
|
|
76
|
+
trajectory,
|
|
63
77
|
rendered,
|
|
64
78
|
].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
|
|
65
79
|
const structured = candidates.find(processShaped) ?? candidates[0];
|
|
66
|
-
const exitCode = structured?.['exit_code'];
|
|
80
|
+
const exitCode = structured?.['exit_code'] ?? transport?.['exit_code'];
|
|
67
81
|
const stdout = structured?.['stdout_tail'];
|
|
68
|
-
const stderr = structured?.['stderr_tail'];
|
|
69
|
-
const transcript = record(
|
|
82
|
+
const stderr = structured?.['stderr_tail'] ?? transport?.['stderr_tail'];
|
|
83
|
+
const transcript = record(trajectory?.['transcript']);
|
|
70
84
|
const failure = record(transcript?.['failure']);
|
|
71
85
|
const excerpt = failure?.['excerpt'];
|
|
72
86
|
const transcriptPath = record(transcript?.['file'])?.['path'];
|
|
@@ -77,6 +91,11 @@ export function selectEvidence(payload: Record<string, unknown>): SelectedEviden
|
|
|
77
91
|
? excerpt : undefined;
|
|
78
92
|
return {
|
|
79
93
|
...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
|
|
94
|
+
...(typeof transport?.['phase'] === 'string' ? { transportPhase: transport['phase'] } : {}),
|
|
95
|
+
...(typeof transport?.['cause'] === 'string' ? { transportCause: transport['cause'] } : {}),
|
|
96
|
+
...(typeof transport?.['signal'] === 'string' ? { signal: transport['signal'] } : {}),
|
|
97
|
+
...(typeof transport?.['error_code'] === 'string' ? { errorCode: transport['error_code'] } : {}),
|
|
98
|
+
...(typeof transport?.['retryable'] === 'boolean' ? { retryableTransport: transport['retryable'] } : {}),
|
|
80
99
|
...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: stdout } : {}),
|
|
81
100
|
...(typeof stderr === 'string' ? { stderrTail: stderr } : {}),
|
|
82
101
|
...(excerptDetail !== undefined ? { detail: excerptDetail }
|
|
@@ -117,6 +136,11 @@ export function terminalEvidence(payload: Record<string, unknown>): Partial<Step
|
|
|
117
136
|
const selected = selectEvidence(payload);
|
|
118
137
|
return {
|
|
119
138
|
...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
|
|
139
|
+
...(selected.transportPhase === undefined ? {} : { transportPhase: formatStepExcerpt(selected.transportPhase) }),
|
|
140
|
+
...(selected.transportCause === undefined ? {} : { transportCause: formatStepExcerpt(selected.transportCause) }),
|
|
141
|
+
...(selected.signal === undefined ? {} : { signal: formatStepExcerpt(selected.signal) }),
|
|
142
|
+
...(selected.errorCode === undefined ? {} : { errorCode: formatStepExcerpt(selected.errorCode) }),
|
|
143
|
+
...(selected.retryableTransport === undefined ? {} : { retryableTransport: selected.retryableTransport }),
|
|
120
144
|
...(selected.stdoutTail === undefined ? {} : { stdoutTail: formatStepExcerpt(selected.stdoutTail) }),
|
|
121
145
|
...(selected.stderrTail === undefined ? {} : { stderrTail: formatStepExcerpt(selected.stderrTail) }),
|
|
122
146
|
...(selected.detail === undefined ? {} : { detail: formatStepExcerpt(selected.detail) }),
|
|
@@ -151,6 +175,13 @@ export function attemptFailure(
|
|
|
151
175
|
completionReason,
|
|
152
176
|
...(typeof disposition === 'string' && disposition.length > 0 ? { disposition } : {}),
|
|
153
177
|
...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
|
|
178
|
+
// Closed-vocabulary scalars, so a crash that left no stderr still says how
|
|
179
|
+
// it died (`signal=SIGKILL`) rather than reading as no evidence at all.
|
|
180
|
+
...(selected.signal === undefined ? {} : { signal: bound(selected.signal) }),
|
|
181
|
+
...(selected.transportCause === undefined ? {} : { transportCause: bound(selected.transportCause) }),
|
|
182
|
+
...(selected.transportPhase === undefined ? {} : { transportPhase: bound(selected.transportPhase) }),
|
|
183
|
+
...(selected.errorCode === undefined ? {} : { errorCode: bound(selected.errorCode) }),
|
|
184
|
+
...(selected.retryableTransport === undefined ? {} : { retryableTransport: selected.retryableTransport }),
|
|
154
185
|
...(selected.stdoutTail === undefined ? {} : { stdoutTail: bound(selected.stdoutTail) }),
|
|
155
186
|
...(selected.stderrTail === undefined ? {} : { stderrTail: bound(selected.stderrTail) }),
|
|
156
187
|
...(selected.detail === undefined ? {} : { detail: bound(selected.detail) }),
|
|
@@ -177,15 +208,18 @@ export function failureCause(
|
|
|
177
208
|
const verification = record(payload['verification']);
|
|
178
209
|
const output = record(payload['output']);
|
|
179
210
|
const trajectory = record(payload['trajectory_tail']);
|
|
211
|
+
const transport = record(trajectory?.['transport']);
|
|
180
212
|
const failure = record(record(trajectory?.['transcript'])?.['failure']);
|
|
181
213
|
const verificationDetail = text(verification?.['detail']);
|
|
182
214
|
const accounts = [
|
|
183
215
|
verificationDetail,
|
|
184
216
|
text(output?.['stdout_tail']), text(output?.['stderr_tail']),
|
|
185
217
|
text(trajectory?.['stdout_tail']), text(trajectory?.['stderr_tail']),
|
|
218
|
+
text(transport?.['phase']), text(transport?.['cause']), text(transport?.['signal']),
|
|
219
|
+
text(transport?.['error_code']), text(transport?.['stderr_tail']),
|
|
186
220
|
text(failure?.['excerpt']),
|
|
187
221
|
];
|
|
188
|
-
const exitCodes = [output?.['exit_code'], trajectory?.['exit_code']]
|
|
222
|
+
const exitCodes = [output?.['exit_code'], trajectory?.['exit_code'], transport?.['exit_code']]
|
|
189
223
|
.filter(value => typeof value === 'number');
|
|
190
224
|
return {
|
|
191
225
|
key: JSON.stringify([
|
|
@@ -197,13 +231,15 @@ export function failureCause(
|
|
|
197
231
|
completionReason === 'verification_failed' || completionReason === 'retries_exhausted'
|
|
198
232
|
? 'kernel_rejected' : completionReason,
|
|
199
233
|
text(verification?.['gate']), text(verification?.['verdict']), verificationDetail,
|
|
200
|
-
...exitCodes, ...accounts, text(failure?.['kind']),
|
|
234
|
+
...exitCodes, ...accounts, transport?.['retryable'], text(failure?.['kind']),
|
|
201
235
|
]),
|
|
202
236
|
// An attempt that journaled nothing about itself cannot agree with
|
|
203
237
|
// another one; it can only fail to disagree.
|
|
204
238
|
recorded: exitCodes.length > 0 || accounts.some(account => account !== undefined),
|
|
205
239
|
producerTruncated: (verificationDetail !== undefined
|
|
206
240
|
&& PRODUCER_TRUNCATED.test(verificationDetail))
|
|
241
|
+
|| [output?.['stderr_tail'], trajectory?.['stderr_tail'], transport?.['stderr_tail']]
|
|
242
|
+
.some(stderr => typeof stderr === 'string' && STDERR_TRUNCATED.test(stderr))
|
|
207
243
|
// The transcript digest's own flag (`boundTranscriptDigest`,
|
|
208
244
|
// agent-transcript.ts): the worker already cut this excerpt, so equal
|
|
209
245
|
// survivors cannot establish identical failures either.
|
|
@@ -255,8 +291,16 @@ export function renderAttemptHistory(details: StepFailedDetails): string {
|
|
|
255
291
|
}
|
|
256
292
|
|
|
257
293
|
function renderAttempt(attempt: StepAttemptFailure): string {
|
|
294
|
+
const transport = [
|
|
295
|
+
attempt.signal === undefined ? '' : ` signal=${attempt.signal}`,
|
|
296
|
+
attempt.transportCause === undefined ? '' : ` transport=${attempt.transportCause}`,
|
|
297
|
+
attempt.transportPhase === undefined ? '' : ` phase=${attempt.transportPhase}`,
|
|
298
|
+
attempt.errorCode === undefined ? '' : ` error_code=${attempt.errorCode}`,
|
|
299
|
+
attempt.retryableTransport === undefined ? '' : ` retryable=${attempt.retryableTransport}`,
|
|
300
|
+
].join('');
|
|
258
301
|
const head = ` attempt ${attempt.attempt ?? '?'}: ${attempt.completionReason ?? 'unknown'}`
|
|
259
302
|
+ (attempt.exitCode === undefined ? '' : ` exit=${attempt.exitCode}`)
|
|
303
|
+
+ transport
|
|
260
304
|
+ (attempt.truncated ? ' (excerpt truncated)' : '');
|
|
261
305
|
// An empty `stderrTail` is a journaled fact but not an account of anything,
|
|
262
306
|
// so it never displaces useful stdout the way `detail ?? stderr ?? stdout`
|
|
@@ -264,7 +308,7 @@ function renderAttempt(attempt: StepAttemptFailure): string {
|
|
|
264
308
|
const accounts: Array<[string, string]> = [
|
|
265
309
|
['detail', attempt.detail], ['stderr', attempt.stderrTail], ['stdout', attempt.stdoutTail],
|
|
266
310
|
].filter((pair): pair is [string, string] => typeof pair[1] === 'string' && pair[1].length > 0);
|
|
267
|
-
if (accounts.length === 0) return `${head} — no failure evidence recorded
|
|
311
|
+
if (accounts.length === 0) return transport === '' ? `${head} — no failure evidence recorded` : head;
|
|
268
312
|
const [only] = accounts;
|
|
269
313
|
if (accounts.length === 1 && !only![1].includes('\n')) return `${head} — ${only![0]}: ${only![1]}`;
|
|
270
314
|
return [head, ...accounts.map(([label, value]) => indent(`${label}: ${value}`))].join('\n');
|
package/src/cli/step-failure.ts
CHANGED
|
@@ -60,6 +60,7 @@ export async function stepFailureDetails(
|
|
|
60
60
|
// enforcing (relayflowd-core/src/entry.rs `AttemptStartedPayload`). They are
|
|
61
61
|
// collected on the same walk and reported only when the journal held them.
|
|
62
62
|
const budgets = new Map<string, number>();
|
|
63
|
+
const transportBudgets = new Map<string, number>();
|
|
63
64
|
// Keyed by step, so interleaved steps never pool their attempts and a page
|
|
64
65
|
// boundary never splits one step's history.
|
|
65
66
|
const histories = new Map<string, AttemptHistory>();
|
|
@@ -82,6 +83,10 @@ export async function stepFailureDetails(
|
|
|
82
83
|
if (typeof maxIterations === 'number' && Number.isSafeInteger(maxIterations) && maxIterations > 0) {
|
|
83
84
|
budgets.set(stepId, maxIterations);
|
|
84
85
|
}
|
|
86
|
+
const transportRetries = record(entry['payload'])?.['max_transport_retries'];
|
|
87
|
+
if (typeof transportRetries === 'number' && Number.isSafeInteger(transportRetries) && transportRetries >= 0) {
|
|
88
|
+
transportBudgets.set(stepId, transportRetries);
|
|
89
|
+
}
|
|
85
90
|
continue;
|
|
86
91
|
}
|
|
87
92
|
if (entry['entry_type'] !== 'step.completed') continue;
|
|
@@ -117,12 +122,14 @@ export async function stepFailureDetails(
|
|
|
117
122
|
const stepType = snapshot.steps[stepId]?.type;
|
|
118
123
|
const attempt = entry['attempt'];
|
|
119
124
|
const maxIterations = budgets.get(stepId);
|
|
125
|
+
const transportRetries = transportBudgets.get(stepId);
|
|
120
126
|
failures.set(stepId, {
|
|
121
127
|
stepId,
|
|
122
128
|
completionReason,
|
|
123
129
|
...(stepType === undefined ? {} : { stepType }),
|
|
124
130
|
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
125
131
|
...(maxIterations === undefined ? {} : { maxIterations }),
|
|
132
|
+
...(transportRetries === undefined ? {} : { transportRetries }),
|
|
126
133
|
...((uncharged.get(stepId) ?? 0) > 0 ? { unchargedAttempts: uncharged.get(stepId) } : {}),
|
|
127
134
|
...terminalEvidence(payload),
|
|
128
135
|
// A single failed attempt is already fully described by the scalars
|
|
@@ -166,7 +173,13 @@ export function renderStepEvidence(details: StepFailedDetails): string {
|
|
|
166
173
|
+ (details.stepType === undefined ? '' : ` (${details.stepType})`)
|
|
167
174
|
+ ` completionReason: ${details.completionReason}`
|
|
168
175
|
+ renderAttempt(details)
|
|
176
|
+
+ (details.transportRetries === undefined ? '' : ` transportRetries=${details.transportRetries}`)
|
|
169
177
|
+ (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
|
|
178
|
+
+ (details.signal === undefined ? '' : ` signal=${details.signal}`)
|
|
179
|
+
+ (details.transportCause === undefined ? '' : ` transport=${details.transportCause}`)
|
|
180
|
+
+ (details.transportPhase === undefined ? '' : ` phase=${details.transportPhase}`)
|
|
181
|
+
+ (details.errorCode === undefined ? '' : ` error_code=${details.errorCode}`)
|
|
182
|
+
+ (details.retryableTransport === undefined ? '' : ` retryable=${details.retryableTransport}`)
|
|
170
183
|
+ '.'
|
|
171
184
|
+ renderAttemptHistory(details)
|
|
172
185
|
+ (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
|
package/src/cli-commands.ts
CHANGED
|
@@ -163,11 +163,29 @@ export const CLI_VERBS = [
|
|
|
163
163
|
{ flags: '--name <name>', description: 'Name for the hosted listener' },
|
|
164
164
|
{ flags: '--draft', description: 'Create the listener without activating it' },
|
|
165
165
|
{ flags: '--plugin <ref>', description: 'Send-only GitHub flow-extension ref; repeatable. Does not write flows.json' },
|
|
166
|
+
{ flags: '--flow <name|listener-id>', description: 'Deploy the source as the next version of this flow; its settings stay unchanged' },
|
|
166
167
|
NO_CONNECT_OPTION,
|
|
167
168
|
JSON_OPTION,
|
|
168
169
|
],
|
|
169
170
|
variants: ['deploy', 'cloud-deploy'],
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: 'versions',
|
|
174
|
+
description: 'List a hosted flow’s source versions, the active one marked',
|
|
175
|
+
args: [{ name: 'flow', description: 'Flow name or listener id', required: true }],
|
|
176
|
+
options: [JSON_OPTION],
|
|
177
|
+
variants: ['versions'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'rollback',
|
|
181
|
+
description: 'Make an earlier (or later) recorded version of a hosted flow the active one',
|
|
182
|
+
args: [
|
|
183
|
+
{ name: 'flow', description: 'Flow name or listener id', required: true },
|
|
184
|
+
{ name: 'version', description: 'Version number to activate', required: true },
|
|
185
|
+
],
|
|
186
|
+
options: [JSON_OPTION],
|
|
187
|
+
variants: ['rollback'],
|
|
188
|
+
},
|
|
171
189
|
{
|
|
172
190
|
name: 'deployments',
|
|
173
191
|
description: 'List this workspace’s hosted trigger listeners',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { boundedTail, redactText } from './agent-transcript.js';
|
|
2
|
+
|
|
3
|
+
export type CliTransportPhase = 'spawn' | 'close' | 'abort' | 'timeout' | 'result_exit_grace';
|
|
4
|
+
export type CliTransportCause =
|
|
5
|
+
| 'exited'
|
|
6
|
+
| 'nonzero_exit'
|
|
7
|
+
| 'signal'
|
|
8
|
+
| 'close_without_status'
|
|
9
|
+
| 'spawn_error'
|
|
10
|
+
| 'process_error'
|
|
11
|
+
| 'lease_lost'
|
|
12
|
+
| 'timeout'
|
|
13
|
+
| 'result_exit_timeout'
|
|
14
|
+
| 'codex_stdin_lifecycle';
|
|
15
|
+
|
|
16
|
+
export interface CliTransportEvidence {
|
|
17
|
+
phase: CliTransportPhase;
|
|
18
|
+
cause: CliTransportCause;
|
|
19
|
+
exit_code: number | null;
|
|
20
|
+
signal: NodeJS.Signals | null;
|
|
21
|
+
/** OS/libuv error code, never an unbounded error message. */
|
|
22
|
+
error_code?: string;
|
|
23
|
+
/** True only for a closed set of infrastructure failures. */
|
|
24
|
+
retryable: boolean;
|
|
25
|
+
/** Redacted UTF-8 tail with its truncation made explicit. */
|
|
26
|
+
stderr_tail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Infrastructure classification is deliberately narrower than "nonzero". */
|
|
30
|
+
export function agentCompletionReason(
|
|
31
|
+
result: { exit_code: number | null; transport?: CliTransportEvidence },
|
|
32
|
+
): 'success' | 'crashed' | 'timeout' | 'worker_error' {
|
|
33
|
+
if (result.exit_code === 0) return 'success';
|
|
34
|
+
if (result.transport?.retryable === true) return 'crashed';
|
|
35
|
+
if (result.transport?.cause === 'timeout') return 'timeout';
|
|
36
|
+
return 'worker_error';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TRANSPORT_STDERR_MAX_BYTES = 2 * 1024;
|
|
40
|
+
const RETRYABLE_SPAWN_ERROR_CODES = new Set(['EAGAIN', 'EMFILE', 'ENFILE', 'ENOMEM', 'ETXTBSY']);
|
|
41
|
+
|
|
42
|
+
export function isRetryableSpawnError(code: string | undefined): boolean {
|
|
43
|
+
return code !== undefined && RETRYABLE_SPAWN_ERROR_CODES.has(code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function transportEvidence(
|
|
47
|
+
value: {
|
|
48
|
+
phase: CliTransportPhase;
|
|
49
|
+
cause: CliTransportCause;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
signal: NodeJS.Signals | null;
|
|
52
|
+
stderr: string;
|
|
53
|
+
errorCode?: string;
|
|
54
|
+
retryable: boolean;
|
|
55
|
+
},
|
|
56
|
+
env: NodeJS.ProcessEnv,
|
|
57
|
+
): CliTransportEvidence {
|
|
58
|
+
const bounded = boundedTail(redactText(value.stderr, env), TRANSPORT_STDERR_MAX_BYTES, 'transport stderr: ');
|
|
59
|
+
const errorCode = value.errorCode !== undefined && /^[A-Z0-9_-]{1,64}$/.test(value.errorCode)
|
|
60
|
+
? value.errorCode : undefined;
|
|
61
|
+
return {
|
|
62
|
+
phase: value.phase,
|
|
63
|
+
cause: value.cause,
|
|
64
|
+
exit_code: value.exitCode,
|
|
65
|
+
signal: value.signal,
|
|
66
|
+
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
67
|
+
retryable: value.retryable,
|
|
68
|
+
stderr_tail: bounded.text,
|
|
69
|
+
};
|
|
70
|
+
}
|