@relayflows/sdk 2.0.30 → 2.0.32
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.map +1 -1
- package/dist/agent-artifacts.js +2 -9
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-cwd.d.ts +69 -0
- package/dist/agent-cwd.d.ts.map +1 -0
- package/dist/agent-cwd.js +140 -0
- package/dist/agent-cwd.js.map +1 -0
- package/dist/artifact-scan-policy.d.ts +35 -0
- package/dist/artifact-scan-policy.d.ts.map +1 -0
- package/dist/artifact-scan-policy.js +49 -0
- package/dist/artifact-scan-policy.js.map +1 -0
- package/dist/authored-completion-record.d.ts +35 -0
- package/dist/authored-completion-record.d.ts.map +1 -0
- package/dist/authored-completion-record.js +92 -0
- package/dist/authored-completion-record.js.map +1 -0
- package/dist/authored-completion.d.ts +99 -0
- package/dist/authored-completion.d.ts.map +1 -0
- package/dist/authored-completion.js +184 -0
- package/dist/authored-completion.js.map +1 -0
- package/dist/authored-flow-error.d.ts +13 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +12 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +7 -30
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +54 -47
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +1 -3
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +9 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +92 -17
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-preflight.d.ts +4 -0
- package/dist/authored-preflight.d.ts.map +1 -0
- package/dist/authored-preflight.js +50 -0
- package/dist/authored-preflight.js.map +1 -0
- package/dist/authored-root.d.ts +5 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +35 -10
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +108 -40
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-verdict.d.ts +28 -0
- package/dist/authored-verdict.d.ts.map +1 -0
- package/dist/authored-verdict.js +79 -0
- package/dist/authored-verdict.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +51 -13
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-worker-surface.d.ts +27 -0
- package/dist/cli/check-worker-surface.d.ts.map +1 -0
- package/dist/cli/check-worker-surface.js +63 -0
- package/dist/cli/check-worker-surface.js.map +1 -0
- package/dist/cli/check.d.ts +28 -3
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +27 -140
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +20 -0
- package/dist/cli/cli-probe.d.ts.map +1 -0
- package/dist/cli/cli-probe.js +166 -0
- package/dist/cli/cli-probe.js.map +1 -0
- package/dist/cli/cloud-format.d.ts +9 -0
- package/dist/cli/cloud-format.d.ts.map +1 -0
- package/dist/cli/cloud-format.js +77 -0
- package/dist/cli/cloud-format.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +39 -0
- package/dist/cli/cloud-live.d.ts.map +1 -0
- package/dist/cli/cloud-live.js +325 -0
- package/dist/cli/cloud-live.js.map +1 -0
- package/dist/cli/cloud-mirror-session.d.ts +97 -0
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
- package/dist/cli/cloud-mirror-session.js +264 -0
- package/dist/cli/cloud-mirror-session.js.map +1 -0
- package/dist/cli/cloud-read.d.ts +3 -7
- package/dist/cli/cloud-read.d.ts.map +1 -1
- package/dist/cli/cloud-read.js +14 -266
- package/dist/cli/cloud-read.js.map +1 -1
- package/dist/cli/cloud-refusal.d.ts +39 -0
- package/dist/cli/cloud-refusal.d.ts.map +1 -0
- package/dist/cli/cloud-refusal.js +95 -0
- package/dist/cli/cloud-refusal.js.map +1 -0
- package/dist/cli/cloud-status-view.d.ts +7 -0
- package/dist/cli/cloud-status-view.d.ts.map +1 -0
- package/dist/cli/cloud-status-view.js +214 -0
- package/dist/cli/cloud-status-view.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/hn-monitor.d.ts +0 -13
- package/dist/cli/hn-monitor.d.ts.map +1 -1
- package/dist/cli/hn-monitor.js +2 -1
- package/dist/cli/hn-monitor.js.map +1 -1
- package/dist/cli/local-agent-remedy.d.ts +80 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -0
- package/dist/cli/local-agent-remedy.js +116 -0
- package/dist/cli/local-agent-remedy.js.map +1 -0
- package/dist/cli/observer-session.d.ts +41 -0
- package/dist/cli/observer-session.d.ts.map +1 -0
- package/dist/cli/observer-session.js +78 -0
- package/dist/cli/observer-session.js.map +1 -0
- package/dist/cli/run.d.ts +30 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +186 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-evidence.d.ts +90 -0
- package/dist/cli/step-evidence.d.ts.map +1 -0
- package/dist/cli/step-evidence.js +259 -0
- package/dist/cli/step-evidence.js.map +1 -0
- package/dist/cli/step-failure.d.ts +14 -4
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +41 -76
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +12 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +3 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +134 -57
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +1 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +24 -8
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-mirror-ledger.d.ts +40 -0
- package/dist/cloud-mirror-ledger.d.ts.map +1 -0
- package/dist/cloud-mirror-ledger.js +142 -0
- package/dist/cloud-mirror-ledger.js.map +1 -0
- package/dist/cloud-mirror-step.d.ts +168 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -0
- package/dist/cloud-mirror-step.js +504 -0
- package/dist/cloud-mirror-step.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +94 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -0
- package/dist/cloud-mirror-transport.js +163 -0
- package/dist/cloud-mirror-transport.js.map +1 -0
- package/dist/cloud-mirror.d.ts +132 -0
- package/dist/cloud-mirror.d.ts.map +1 -0
- package/dist/cloud-mirror.js +556 -0
- package/dist/cloud-mirror.js.map +1 -0
- package/dist/cloud-read.d.ts +15 -0
- package/dist/cloud-read.d.ts.map +1 -1
- package/dist/cloud-read.js +20 -1
- package/dist/cloud-read.js.map +1 -1
- package/dist/cloud-run-record.d.ts +27 -0
- package/dist/cloud-run-record.d.ts.map +1 -0
- package/dist/cloud-run-record.js +56 -0
- package/dist/cloud-run-record.js.map +1 -0
- package/dist/cloud-run.d.ts +2 -9
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +3 -20
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +13 -5
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +28 -6
- package/dist/compile.js.map +1 -1
- package/dist/direct-input.d.ts.map +1 -1
- package/dist/direct-input.js +21 -1
- package/dist/direct-input.js.map +1 -1
- package/dist/failure-kinds.d.ts +96 -6
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +29 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +24 -9
- package/dist/flow-requirements.js.map +1 -1
- package/dist/gate-contract.d.ts +7 -0
- package/dist/gate-contract.d.ts.map +1 -1
- package/dist/gate-contract.js +6 -1
- package/dist/gate-contract.js.map +1 -1
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +10 -3
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +25 -0
- package/dist/helper-reference.d.ts.map +1 -0
- package/dist/helper-reference.js +129 -0
- package/dist/helper-reference.js.map +1 -0
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +17 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-projection.d.ts +16 -0
- package/dist/journal-projection.d.ts.map +1 -0
- package/dist/journal-projection.js +108 -0
- package/dist/journal-projection.js.map +1 -0
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +4 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/named-gate-lowering.js +35 -5
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gate-preflight.d.ts +27 -0
- package/dist/named-gate-preflight.d.ts.map +1 -0
- package/dist/named-gate-preflight.js +47 -0
- package/dist/named-gate-preflight.js.map +1 -0
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +3 -0
- package/dist/named-gates.js.map +1 -1
- package/dist/observer-link.d.ts +7 -0
- package/dist/observer-link.d.ts.map +1 -1
- package/dist/observer-link.js +3 -1
- package/dist/observer-link.js.map +1 -1
- package/dist/package-version.d.ts +10 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +22 -0
- package/dist/package-version.js.map +1 -0
- package/dist/preflight.d.ts +14 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +25 -3
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/redact.d.ts +42 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +128 -0
- package/dist/redact.js.map +1 -1
- package/dist/relay-cli.d.ts.map +1 -1
- package/dist/relay-cli.js +1 -21
- package/dist/relay-cli.js.map +1 -1
- package/dist/run-projection.d.ts +83 -0
- package/dist/run-projection.d.ts.map +1 -0
- package/dist/run-projection.js +199 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/shell-word.d.ts +19 -0
- package/dist/shell-word.d.ts.map +1 -0
- package/dist/shell-word.js +23 -0
- package/dist/shell-word.js.map +1 -0
- package/dist/spec.d.ts +52 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/steps-green.d.ts +27 -0
- package/dist/steps-green.d.ts.map +1 -0
- package/dist/steps-green.js +164 -0
- package/dist/steps-green.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +39 -8
- package/dist/validate.js.map +1 -1
- package/dist/worker-lease.d.ts +8 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +34 -4
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/package.json +3 -7
- package/src/agent-artifacts.ts +2 -10
- package/src/agent-cwd.ts +139 -0
- package/src/artifact-scan-policy.ts +51 -0
- package/src/authored-completion-record.ts +119 -0
- package/src/authored-completion.ts +203 -0
- package/src/authored-flow-error.ts +13 -1
- package/src/authored-flow-executor.ts +91 -58
- package/src/authored-human.ts +1 -4
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +96 -22
- package/src/authored-preflight.ts +46 -0
- package/src/authored-root.ts +20 -2
- package/src/authored-step-output.ts +139 -42
- package/src/authored-verdict.ts +87 -0
- package/src/authored-worker-step.ts +63 -14
- package/src/cli/check-worker-surface.ts +66 -0
- package/src/cli/check.ts +59 -166
- package/src/cli/cli-probe.ts +194 -0
- package/src/cli/cloud-format.ts +82 -0
- package/src/cli/cloud-live.ts +366 -0
- package/src/cli/cloud-mirror-session.ts +329 -0
- package/src/cli/cloud-read.ts +13 -282
- package/src/cli/cloud-refusal.ts +113 -0
- package/src/cli/cloud-status-view.ts +213 -0
- package/src/cli/direct-run.ts +19 -2
- package/src/cli/hn-monitor.ts +2 -1
- package/src/cli/local-agent-remedy.ts +160 -0
- package/src/cli/observer-session.ts +107 -0
- package/src/cli/run.ts +233 -28
- package/src/cli/status.ts +59 -3
- package/src/cli/step-evidence.ts +310 -0
- package/src/cli/step-failure.ts +57 -81
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +155 -60
- package/src/cloud-deploy.ts +26 -8
- package/src/cloud-mirror-ledger.ts +161 -0
- package/src/cloud-mirror-step.ts +618 -0
- package/src/cloud-mirror-transport.ts +261 -0
- package/src/cloud-mirror.ts +633 -0
- package/src/cloud-read.ts +25 -1
- package/src/cloud-run-record.ts +64 -0
- package/src/cloud-run.ts +5 -24
- package/src/communication/local.ts +2 -1
- package/src/communication/worker.ts +13 -5
- package/src/compile.ts +28 -6
- package/src/direct-input.ts +22 -1
- package/src/failure-kinds.ts +104 -2
- package/src/flow-requirements.ts +24 -9
- package/src/gate-contract.ts +13 -1
- package/src/helper-preflight.ts +10 -3
- package/src/helper-reference.ts +130 -0
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/index.ts +2 -0
- package/src/journal-client.ts +19 -2
- package/src/journal-projection.ts +106 -0
- package/src/llm-worker.ts +1 -1
- package/src/local-agent.ts +4 -2
- package/src/named-gate-lowering.ts +35 -5
- package/src/named-gate-preflight.ts +47 -0
- package/src/named-gates.ts +3 -0
- package/src/observer-link.ts +10 -1
- package/src/package-version.ts +23 -0
- package/src/preflight.ts +38 -7
- package/src/protocol.ts +2 -0
- package/src/redact.ts +111 -0
- package/src/relay-cli.ts +1 -24
- package/src/run-projection.ts +247 -0
- package/src/shell-word.ts +24 -0
- package/src/spec.ts +54 -3
- package/src/step-fields.ts +1 -1
- package/src/steps-green.ts +168 -0
- package/src/validate.ts +35 -8
- package/src/worker-lease.ts +35 -5
- package/src/worker.ts +27 -11
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { redact } from '../redact.js';
|
|
2
|
+
import { formatStepExcerpt } from './step-excerpt.js';
|
|
3
|
+
import type {
|
|
4
|
+
AttemptEvidenceComparison,
|
|
5
|
+
StepAttemptFailure,
|
|
6
|
+
StepFailedDetails,
|
|
7
|
+
} from '../failure-kinds.js';
|
|
8
|
+
|
|
9
|
+
/** Per-attempt history is context beside that account, not a second copy of it. */
|
|
10
|
+
export const ATTEMPT_TAIL_BYTES = 256;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the daemon appends when it cut a render before journaling it
|
|
14
|
+
* (`worker_failure_detail`, relayflowd/src/engine/remote.rs). Evidence that
|
|
15
|
+
* arrived already truncated cannot establish that two attempts failed
|
|
16
|
+
* identically, however equal the surviving bytes are.
|
|
17
|
+
*/
|
|
18
|
+
const PRODUCER_TRUNCATED = /…\s*\((?:render bounded|[\d,]+ bytes truncated)\)$/u;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The evidence fields a step failure reports, as the journal held them.
|
|
22
|
+
*
|
|
23
|
+
* Extraction and display bounds are separated deliberately. The terminal
|
|
24
|
+
* attempt renders at 1,024 bytes and each historical attempt at 256, but the
|
|
25
|
+
* COMPARISON that decides whether a retry failed differently runs on the
|
|
26
|
+
* unbounded record (`failureCause`): two different errors can share a 256-byte
|
|
27
|
+
* suffix, and calling those the same failure is the bug this reader exists to
|
|
28
|
+
* avoid.
|
|
29
|
+
*/
|
|
30
|
+
export interface SelectedEvidence {
|
|
31
|
+
exitCode?: number;
|
|
32
|
+
/** Present only when nonempty. */
|
|
33
|
+
stdoutTail?: string;
|
|
34
|
+
/** Present whenever the journal carried one, including the empty string. */
|
|
35
|
+
stderrTail?: string;
|
|
36
|
+
detail?: string;
|
|
37
|
+
transcriptPath?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pull the process-shaped fields out of whichever field carried them.
|
|
42
|
+
*
|
|
43
|
+
* `verification.detail` is last because it is the daemon's own render rather
|
|
44
|
+
* than the worker's structured report — but for an agent step it is the only
|
|
45
|
+
* thing that survives, so it is parsed when it parses and kept verbatim when
|
|
46
|
+
* it does not. A truncated render (the daemon caps at 2,000 chars and appends
|
|
47
|
+
* a truncation note) will not parse; that falls through to the raw string,
|
|
48
|
+
* which is still the account of what went wrong.
|
|
49
|
+
*
|
|
50
|
+
* An agent or llm completion now also carries `trajectory_tail.transcript`
|
|
51
|
+
* (agent-transcript.ts) on every attempt. That object is not process-shaped,
|
|
52
|
+
* so it must not shadow the render that is: the first candidate that carries
|
|
53
|
+
* an exit code or a tail wins. The digest contributes what only it has — the
|
|
54
|
+
* failure excerpt the worker picked out of the provider's frames, and the
|
|
55
|
+
* path of the transcript file.
|
|
56
|
+
*/
|
|
57
|
+
export function selectEvidence(payload: Record<string, unknown>): SelectedEvidence {
|
|
58
|
+
const detail = record(payload['verification'])?.['detail'];
|
|
59
|
+
const rendered = typeof detail === 'string' ? parsed(detail) : undefined;
|
|
60
|
+
const candidates = [
|
|
61
|
+
record(payload['output']),
|
|
62
|
+
record(payload['trajectory_tail']),
|
|
63
|
+
rendered,
|
|
64
|
+
].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
|
|
65
|
+
const structured = candidates.find(processShaped) ?? candidates[0];
|
|
66
|
+
const exitCode = structured?.['exit_code'];
|
|
67
|
+
const stdout = structured?.['stdout_tail'];
|
|
68
|
+
const stderr = structured?.['stderr_tail'];
|
|
69
|
+
const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
|
|
70
|
+
const failure = record(transcript?.['failure']);
|
|
71
|
+
const excerpt = failure?.['excerpt'];
|
|
72
|
+
const transcriptPath = record(transcript?.['file'])?.['path'];
|
|
73
|
+
// The excerpt is the worker's own pick of the failure; a `stderr` excerpt
|
|
74
|
+
// is the same bytes as `stderrTail`, so it is not printed twice.
|
|
75
|
+
const excerptDetail = typeof excerpt === 'string' && excerpt.length > 0
|
|
76
|
+
&& !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
|
|
77
|
+
? excerpt : undefined;
|
|
78
|
+
return {
|
|
79
|
+
...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
|
|
80
|
+
...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: stdout } : {}),
|
|
81
|
+
...(typeof stderr === 'string' ? { stderrTail: stderr } : {}),
|
|
82
|
+
...(excerptDetail !== undefined ? { detail: excerptDetail }
|
|
83
|
+
: typeof detail === 'string' && detail.length > 0 && !duplicates(detail, rendered, exitCode)
|
|
84
|
+
? { detail } : {}),
|
|
85
|
+
...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether printing `verification.detail` would print bytes already printed.
|
|
91
|
+
*
|
|
92
|
+
* Only two shapes do. The daemon's render of a worker failure IS the
|
|
93
|
+
* `{exit_code, stdout_tail, stderr_tail}` report (`worker_failure_detail`,
|
|
94
|
+
* relayflowd/src/engine/remote.rs), which a deterministic step journals
|
|
95
|
+
* alongside its preserved `output`; and the `exit_code` gate's verdict on its
|
|
96
|
+
* own is the exit code that is already reported beside it (`exit code was 1`,
|
|
97
|
+
* relayflowd-core/src/verify.rs).
|
|
98
|
+
*
|
|
99
|
+
* Every other verdict is the ONLY account of its failure. `output did not
|
|
100
|
+
* contain "READY"` accompanies exit 0 and empty tails, so suppressing the
|
|
101
|
+
* detail whenever some field happened to be process-shaped left the attempt
|
|
102
|
+
* saying `exit=0 — no failure evidence recorded` while the journal held the
|
|
103
|
+
* gate error — the first attempt hidden again, inside the diagnostic that
|
|
104
|
+
* exists to surface it.
|
|
105
|
+
*/
|
|
106
|
+
function duplicates(
|
|
107
|
+
detail: string,
|
|
108
|
+
rendered: Record<string, unknown> | undefined,
|
|
109
|
+
exitCode: unknown,
|
|
110
|
+
): boolean {
|
|
111
|
+
if (rendered !== undefined && processShaped(rendered)) return true;
|
|
112
|
+
return typeof exitCode === 'number' && detail === `exit code was ${exitCode}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The `StepFailedDetails` scalars for one completion, as a failure excerpt. */
|
|
116
|
+
export function terminalEvidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
|
|
117
|
+
const selected = selectEvidence(payload);
|
|
118
|
+
return {
|
|
119
|
+
...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
|
|
120
|
+
...(selected.stdoutTail === undefined ? {} : { stdoutTail: formatStepExcerpt(selected.stdoutTail) }),
|
|
121
|
+
...(selected.stderrTail === undefined ? {} : { stderrTail: formatStepExcerpt(selected.stderrTail) }),
|
|
122
|
+
...(selected.detail === undefined ? {} : { detail: formatStepExcerpt(selected.detail) }),
|
|
123
|
+
...(selected.transcriptPath === undefined ? {} : { transcriptPath: selected.transcriptPath }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One historical attempt's record, redacted and then bounded.
|
|
129
|
+
*
|
|
130
|
+
* Redaction runs first so a credential cannot survive by sitting outside the
|
|
131
|
+
* excerpt window on one attempt and inside it on another. Control-character
|
|
132
|
+
* replacement alone is not redaction: an agent that echoed `rk_live_...` into
|
|
133
|
+
* its stderr would otherwise reach a terminal through this new field.
|
|
134
|
+
*/
|
|
135
|
+
export function attemptFailure(
|
|
136
|
+
attempt: unknown,
|
|
137
|
+
completionReason: string,
|
|
138
|
+
disposition: unknown,
|
|
139
|
+
payload: Record<string, unknown>,
|
|
140
|
+
): StepAttemptFailure {
|
|
141
|
+
const selected = selectEvidence(payload);
|
|
142
|
+
let truncated = false;
|
|
143
|
+
const bound = (value: string): string => {
|
|
144
|
+
const redacted = redact(value);
|
|
145
|
+
if (Buffer.byteLength(redacted, 'utf8') > ATTEMPT_TAIL_BYTES) truncated = true;
|
|
146
|
+
return tail(redacted, ATTEMPT_TAIL_BYTES);
|
|
147
|
+
};
|
|
148
|
+
const record_: StepAttemptFailure = {
|
|
149
|
+
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0
|
|
150
|
+
? { attempt } : {}),
|
|
151
|
+
completionReason,
|
|
152
|
+
...(typeof disposition === 'string' && disposition.length > 0 ? { disposition } : {}),
|
|
153
|
+
...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
|
|
154
|
+
...(selected.stdoutTail === undefined ? {} : { stdoutTail: bound(selected.stdoutTail) }),
|
|
155
|
+
...(selected.stderrTail === undefined ? {} : { stderrTail: bound(selected.stderrTail) }),
|
|
156
|
+
...(selected.detail === undefined ? {} : { detail: bound(selected.detail) }),
|
|
157
|
+
};
|
|
158
|
+
return truncated ? { ...record_, truncated } : record_;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Everything about a completion that bears on WHY it failed, unbounded.
|
|
163
|
+
*
|
|
164
|
+
* Deliberately not the display selection: `selectEvidence` suppresses the
|
|
165
|
+
* daemon's render when process-shaped output exists, and prefers a transcript
|
|
166
|
+
* excerpt over both, so two attempts can show identical excerpts over
|
|
167
|
+
* different gate verdicts. Everything the journal recorded about the failure
|
|
168
|
+
* is read here instead, before any bound is applied.
|
|
169
|
+
*
|
|
170
|
+
* Timestamps, transcript paths, dispositions and attempt numbers are NOT
|
|
171
|
+
* causes and are excluded: they differ on every attempt by construction.
|
|
172
|
+
*/
|
|
173
|
+
export function failureCause(
|
|
174
|
+
completionReason: string,
|
|
175
|
+
payload: Record<string, unknown>,
|
|
176
|
+
): AttemptCause {
|
|
177
|
+
const verification = record(payload['verification']);
|
|
178
|
+
const output = record(payload['output']);
|
|
179
|
+
const trajectory = record(payload['trajectory_tail']);
|
|
180
|
+
const failure = record(record(trajectory?.['transcript'])?.['failure']);
|
|
181
|
+
const verificationDetail = text(verification?.['detail']);
|
|
182
|
+
const accounts = [
|
|
183
|
+
verificationDetail,
|
|
184
|
+
text(output?.['stdout_tail']), text(output?.['stderr_tail']),
|
|
185
|
+
text(trajectory?.['stdout_tail']), text(trajectory?.['stderr_tail']),
|
|
186
|
+
text(failure?.['excerpt']),
|
|
187
|
+
];
|
|
188
|
+
const exitCodes = [output?.['exit_code'], trajectory?.['exit_code']]
|
|
189
|
+
.filter(value => typeof value === 'number');
|
|
190
|
+
return {
|
|
191
|
+
key: JSON.stringify([
|
|
192
|
+
// `verification_failed` on a retry and `retries_exhausted` at the budget
|
|
193
|
+
// limit are the SAME fallback over the same failure: `completion_actions`
|
|
194
|
+
// picks the label from the branch it took, not from the cause. Comparing
|
|
195
|
+
// them literally would report "the retry failed differently" on every
|
|
196
|
+
// ordinary repeated failure. No other pair of labels is collapsed.
|
|
197
|
+
completionReason === 'verification_failed' || completionReason === 'retries_exhausted'
|
|
198
|
+
? 'kernel_rejected' : completionReason,
|
|
199
|
+
text(verification?.['gate']), text(verification?.['verdict']), verificationDetail,
|
|
200
|
+
...exitCodes, ...accounts, text(failure?.['kind']),
|
|
201
|
+
]),
|
|
202
|
+
// An attempt that journaled nothing about itself cannot agree with
|
|
203
|
+
// another one; it can only fail to disagree.
|
|
204
|
+
recorded: exitCodes.length > 0 || accounts.some(account => account !== undefined),
|
|
205
|
+
producerTruncated: (verificationDetail !== undefined
|
|
206
|
+
&& PRODUCER_TRUNCATED.test(verificationDetail))
|
|
207
|
+
// The transcript digest's own flag (`boundTranscriptDigest`,
|
|
208
|
+
// agent-transcript.ts): the worker already cut this excerpt, so equal
|
|
209
|
+
// survivors cannot establish identical failures either.
|
|
210
|
+
|| failure?.['truncated'] === true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface AttemptCause {
|
|
215
|
+
readonly key: string;
|
|
216
|
+
readonly recorded: boolean;
|
|
217
|
+
readonly producerTruncated: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Only attempts that recorded something can establish a difference.
|
|
222
|
+
*
|
|
223
|
+
* An attempt that journaled no account of itself differs from every other one
|
|
224
|
+
* on paper while saying nothing about why it failed, and reporting that as
|
|
225
|
+
* `differs` would put "an earlier attempt may have had side effects" on a
|
|
226
|
+
* crash that is evidence of nothing. Truncation is not the same case: two
|
|
227
|
+
* renders the daemon already cut can only look MORE alike than they were, so
|
|
228
|
+
* surviving bytes that disagree still prove the causes disagreed.
|
|
229
|
+
*/
|
|
230
|
+
export function compareAttempts(causes: readonly AttemptCause[]): AttemptEvidenceComparison {
|
|
231
|
+
const recorded = causes.filter(cause => cause.recorded);
|
|
232
|
+
if (new Set(recorded.map(cause => cause.key)).size > 1) return 'differs';
|
|
233
|
+
return recorded.length === causes.length && causes.every(cause => !cause.producerTruncated)
|
|
234
|
+
? 'unchanged' : 'unknown';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const COMPARISON_CLAUSE: Record<AttemptEvidenceComparison, string> = {
|
|
238
|
+
differs: 'recorded failure evidence differs. An earlier attempt may have had side effects.',
|
|
239
|
+
unchanged: 'recorded failure evidence is unchanged across them.',
|
|
240
|
+
unknown: 'recorded failure evidence is incomplete, so whether the causes differ is unknown.',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Every failed attempt, oldest first, under one line saying whether they
|
|
245
|
+
* agree. Rendered in full rather than elided: `flows logs` prints the runner
|
|
246
|
+
* log without eliding it, so an attempt dropped here is an attempt that
|
|
247
|
+
* reaches no reader at all.
|
|
248
|
+
*/
|
|
249
|
+
export function renderAttemptHistory(details: StepFailedDetails): string {
|
|
250
|
+
const attempts = details.attempts;
|
|
251
|
+
if (attempts === undefined || attempts.length < 2) return '';
|
|
252
|
+
const comparison = COMPARISON_CLAUSE[details.attemptEvidence ?? 'unknown'];
|
|
253
|
+
return [`\nAttempts: ${attempts.length} failed; ${comparison}`, ...attempts.map(renderAttempt)]
|
|
254
|
+
.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderAttempt(attempt: StepAttemptFailure): string {
|
|
258
|
+
const head = ` attempt ${attempt.attempt ?? '?'}: ${attempt.completionReason ?? 'unknown'}`
|
|
259
|
+
+ (attempt.exitCode === undefined ? '' : ` exit=${attempt.exitCode}`)
|
|
260
|
+
+ (attempt.truncated ? ' (excerpt truncated)' : '');
|
|
261
|
+
// An empty `stderrTail` is a journaled fact but not an account of anything,
|
|
262
|
+
// so it never displaces useful stdout the way `detail ?? stderr ?? stdout`
|
|
263
|
+
// would. Both are kept when both exist.
|
|
264
|
+
const accounts: Array<[string, string]> = [
|
|
265
|
+
['detail', attempt.detail], ['stderr', attempt.stderrTail], ['stdout', attempt.stdoutTail],
|
|
266
|
+
].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`;
|
|
268
|
+
const [only] = accounts;
|
|
269
|
+
if (accounts.length === 1 && !only![1].includes('\n')) return `${head} — ${only![0]}: ${only![1]}`;
|
|
270
|
+
return [head, ...accounts.map(([label, value]) => indent(`${label}: ${value}`))].join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Continuation lines are indented past the label so the list stays readable. */
|
|
274
|
+
function indent(block: string): string {
|
|
275
|
+
const [first, ...rest] = block.split('\n');
|
|
276
|
+
return [` ${first}`, ...rest.map(line => ` ${line}`)].join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function text(value: unknown): string | undefined {
|
|
280
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function processShaped(value: Record<string, unknown>): boolean {
|
|
284
|
+
return typeof value['exit_code'] === 'number'
|
|
285
|
+
|| typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parsed(value: string): Record<string, unknown> | undefined {
|
|
289
|
+
try {
|
|
290
|
+
return record(JSON.parse(value));
|
|
291
|
+
} catch {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function record(value: unknown): Record<string, unknown> | undefined {
|
|
297
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
298
|
+
? value as Record<string, unknown> : undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function tail(value: string, limit: number): string {
|
|
302
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
303
|
+
let start = Math.max(0, bytes.length - limit);
|
|
304
|
+
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
305
|
+
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
306
|
+
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
307
|
+
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
308
|
+
return bytes.subarray(start).toString('utf8')
|
|
309
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
310
|
+
}
|
package/src/cli/step-failure.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { DEFAULT_DATA_DIR } from '../daemon-connection.js';
|
|
3
|
-
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
3
|
+
import type { StepAttemptFailure, StepFailedDetails } from '../failure-kinds.js';
|
|
4
4
|
import type { JournalClient } from '../journal-client.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
attemptFailure,
|
|
7
|
+
compareAttempts,
|
|
8
|
+
failureCause,
|
|
9
|
+
record,
|
|
10
|
+
renderAttemptHistory,
|
|
11
|
+
terminalEvidence,
|
|
12
|
+
type AttemptCause,
|
|
13
|
+
} from './step-evidence.js';
|
|
14
|
+
|
|
15
|
+
/** Every failed attempt of one step, in journal order, with its causes. */
|
|
16
|
+
interface AttemptHistory {
|
|
17
|
+
records: StepAttemptFailure[];
|
|
18
|
+
causes: AttemptCause[];
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Read what a failed step left in the journal — for any step type.
|
|
@@ -21,10 +35,16 @@ import { formatStepExcerpt } from './step-excerpt.js';
|
|
|
21
35
|
* agent or llm step the worker's `{exit_code, stdout_tail, stderr_tail}` is
|
|
22
36
|
* nulled out of `output` and survives only as the bounded render the daemon
|
|
23
37
|
* captured into `verification.detail` (`worker_failure_detail`,
|
|
24
|
-
* relayflowd/src/engine/remote.rs). Both are read
|
|
25
|
-
* daemon's render is re-parsed when it carries that same shape
|
|
26
|
-
*
|
|
27
|
-
*
|
|
38
|
+
* relayflowd/src/engine/remote.rs). Both are read by `selectEvidence`, in that
|
|
39
|
+
* order, and the daemon's render is re-parsed when it carries that same shape.
|
|
40
|
+
*
|
|
41
|
+
* EVERY failed attempt is collected, not only the terminal one. The kernel
|
|
42
|
+
* appends a `step.completed` per attempt, so a retried step's first failure is
|
|
43
|
+
* in the journal — but `retries_exhausted` reported only the last attempt, and
|
|
44
|
+
* a retry that fails differently (because the first attempt already had a side
|
|
45
|
+
* effect) is exactly when the last attempt is the least informative record
|
|
46
|
+
* there is. The scalar fields still describe the terminal attempt; `attempts`
|
|
47
|
+
* is additive.
|
|
28
48
|
*/
|
|
29
49
|
export async function stepFailureDetails(
|
|
30
50
|
client: JournalClient,
|
|
@@ -40,6 +60,9 @@ export async function stepFailureDetails(
|
|
|
40
60
|
// enforcing (relayflowd-core/src/entry.rs `AttemptStartedPayload`). They are
|
|
41
61
|
// collected on the same walk and reported only when the journal held them.
|
|
42
62
|
const budgets = new Map<string, number>();
|
|
63
|
+
// Keyed by step, so interleaved steps never pool their attempts and a page
|
|
64
|
+
// boundary never splits one step's history.
|
|
65
|
+
const histories = new Map<string, AttemptHistory>();
|
|
43
66
|
while (true) {
|
|
44
67
|
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
45
68
|
if (entries.length === 0) break;
|
|
@@ -63,15 +86,27 @@ export async function stepFailureDetails(
|
|
|
63
86
|
// A later completion supersedes an earlier failed attempt.
|
|
64
87
|
failures.delete(stepId);
|
|
65
88
|
const payload = record(entry['payload']);
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
const completionReason = payload?.['completionReason'];
|
|
90
|
+
// A success — or a completion this reader cannot read — ends the failure
|
|
91
|
+
// history too: what a step that eventually succeeded printed on the way
|
|
92
|
+
// is not the diagnosis of a later, different failure. The journal still
|
|
93
|
+
// holds every entry; only this diagnostic's candidate is cleared.
|
|
94
|
+
if (payload === undefined || typeof completionReason !== 'string' || completionReason === 'success') {
|
|
95
|
+
histories.delete(stepId);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const history = histories.get(stepId) ?? { records: [], causes: [] };
|
|
99
|
+
// Recorded for `retry` and `park` alike: the kernel's disposition says
|
|
100
|
+
// what it did next, not whether the attempt failed.
|
|
101
|
+
history.records.push(attemptFailure(entry['attempt'], completionReason, payload['disposition'], payload));
|
|
102
|
+
history.causes.push(failureCause(completionReason, payload));
|
|
103
|
+
histories.set(stepId, history);
|
|
68
104
|
// A terminal completion that is not a success is the failure, whatever
|
|
69
105
|
// its step type. The old predicate also demanded a non-zero `exit_code`,
|
|
70
106
|
// which no agent completion carries and which a deterministic step that
|
|
71
107
|
// exits 0 and then fails its gate does not carry either — both were
|
|
72
108
|
// silently skipped.
|
|
73
|
-
if (payload['disposition'] !== 'step_done'
|
|
74
|
-
|| typeof completionReason !== 'string' || completionReason === 'success') continue;
|
|
109
|
+
if (payload['disposition'] !== 'step_done') continue;
|
|
75
110
|
const stepType = snapshot.steps[stepId]?.type;
|
|
76
111
|
const attempt = entry['attempt'];
|
|
77
112
|
const maxIterations = budgets.get(stepId);
|
|
@@ -81,7 +116,13 @@ export async function stepFailureDetails(
|
|
|
81
116
|
...(stepType === undefined ? {} : { stepType }),
|
|
82
117
|
...(typeof attempt === 'number' && Number.isSafeInteger(attempt) && attempt > 0 ? { attempt } : {}),
|
|
83
118
|
...(maxIterations === undefined ? {} : { maxIterations }),
|
|
84
|
-
...
|
|
119
|
+
...terminalEvidence(payload),
|
|
120
|
+
// A single failed attempt is already fully described by the scalars
|
|
121
|
+
// above; repeating it as a one-element history would add a clause to
|
|
122
|
+
// every ordinary failure report without adding a fact.
|
|
123
|
+
...(history.records.length > 1
|
|
124
|
+
? { attempts: history.records, attemptEvidence: compareAttempts(history.causes) }
|
|
125
|
+
: {}),
|
|
85
126
|
});
|
|
86
127
|
}
|
|
87
128
|
}
|
|
@@ -104,6 +145,10 @@ export async function stepFailureDetails(
|
|
|
104
145
|
* stops it being misread. Unknown values are omitted rather than defaulted —
|
|
105
146
|
* an absent budget must not be reported as one attempt.
|
|
106
147
|
*
|
|
148
|
+
* The attempt history follows that line and precedes the terminal attempt's
|
|
149
|
+
* own clauses, so the first error is visible without scrolling past the last
|
|
150
|
+
* one's output tails.
|
|
151
|
+
*
|
|
107
152
|
* Lives next to `StepFailedDetails`'s extractor rather than in `cli/run.ts` so
|
|
108
153
|
* the authored `f.run` path can render the same grammar without importing the
|
|
109
154
|
* run lifecycle (which would be a cycle).
|
|
@@ -115,6 +160,7 @@ export function renderStepEvidence(details: StepFailedDetails): string {
|
|
|
115
160
|
+ renderAttempt(details)
|
|
116
161
|
+ (details.exitCode === undefined ? '' : ` exit=${details.exitCode}`)
|
|
117
162
|
+ '.'
|
|
163
|
+
+ renderAttemptHistory(details)
|
|
118
164
|
+ (details.detail === undefined ? '' : `\nDetail: ${details.detail}`)
|
|
119
165
|
+ (details.stdoutTail ? `\nStdout (captured excerpt):\n${details.stdoutTail}` : '')
|
|
120
166
|
+ (details.stderrTail ? `\nStderr (captured excerpt):\n${details.stderrTail}` : '')
|
|
@@ -163,76 +209,6 @@ export function inspectionHint(
|
|
|
163
209
|
};
|
|
164
210
|
}
|
|
165
211
|
|
|
166
|
-
/**
|
|
167
|
-
* Pull the process-shaped fields out of whichever field carried them.
|
|
168
|
-
*
|
|
169
|
-
* `verification.detail` is last because it is the daemon's own render rather
|
|
170
|
-
* than the worker's structured report — but for an agent step it is the only
|
|
171
|
-
* thing that survives, so it is parsed when it parses and kept verbatim when
|
|
172
|
-
* it does not. A truncated render (the daemon caps at 2,000 chars and appends
|
|
173
|
-
* a truncation note) will not parse; that falls through to the raw string,
|
|
174
|
-
* which is still the account of what went wrong.
|
|
175
|
-
*
|
|
176
|
-
* An agent or llm completion now also carries `trajectory_tail.transcript`
|
|
177
|
-
* (agent-transcript.ts) on every attempt. That object is not process-shaped,
|
|
178
|
-
* so it must not shadow the render that is: the first candidate that carries
|
|
179
|
-
* an exit code or a tail wins. The digest contributes what only it has — the
|
|
180
|
-
* failure excerpt the worker picked out of the provider's frames, and the
|
|
181
|
-
* path of the transcript file.
|
|
182
|
-
*/
|
|
183
|
-
function evidence(payload: Record<string, unknown>): Partial<StepFailedDetails> {
|
|
184
|
-
const detail = record(payload['verification'])?.['detail'];
|
|
185
|
-
const candidates = [
|
|
186
|
-
record(payload['output']),
|
|
187
|
-
record(payload['trajectory_tail']),
|
|
188
|
-
typeof detail === 'string' ? parsed(detail) : undefined,
|
|
189
|
-
].filter((candidate): candidate is Record<string, unknown> => candidate !== undefined);
|
|
190
|
-
const structured = candidates.find(processShaped) ?? candidates[0];
|
|
191
|
-
const exitCode = structured?.['exit_code'];
|
|
192
|
-
const stdout = structured?.['stdout_tail'];
|
|
193
|
-
const stderr = structured?.['stderr_tail'];
|
|
194
|
-
const structuredShape = structured !== undefined && processShaped(structured);
|
|
195
|
-
const transcript = record(record(payload['trajectory_tail'])?.['transcript']);
|
|
196
|
-
const failure = record(transcript?.['failure']);
|
|
197
|
-
const failureExcerpt = failure?.['excerpt'];
|
|
198
|
-
const transcriptPath = record(transcript?.['file'])?.['path'];
|
|
199
|
-
// The excerpt is the worker's own pick of the failure; a `stderr` excerpt
|
|
200
|
-
// is the same bytes as `stderrTail`, so it is not printed twice.
|
|
201
|
-
const excerptDetail = typeof failureExcerpt === 'string' && failureExcerpt.length > 0
|
|
202
|
-
&& !(failure?.['kind'] === 'stderr' && typeof stderr === 'string')
|
|
203
|
-
? formatStepExcerpt(failureExcerpt) : undefined;
|
|
204
|
-
return {
|
|
205
|
-
...(typeof exitCode === 'number' && Number.isSafeInteger(exitCode) ? { exitCode } : {}),
|
|
206
|
-
...(typeof stdout === 'string' && stdout.length > 0 ? { stdoutTail: formatStepExcerpt(stdout) } : {}),
|
|
207
|
-
...(typeof stderr === 'string' ? { stderrTail: formatStepExcerpt(stderr) } : {}),
|
|
208
|
-
// Keep the daemon's account only when it was NOT just a render of the
|
|
209
|
-
// fields above — otherwise the same bytes print twice.
|
|
210
|
-
...(excerptDetail !== undefined ? { detail: excerptDetail }
|
|
211
|
-
: typeof detail === 'string' && detail.length > 0 && !structuredShape
|
|
212
|
-
? { detail: formatStepExcerpt(detail) } : {}),
|
|
213
|
-
...(typeof transcriptPath === 'string' && transcriptPath.length > 0 ? { transcriptPath } : {}),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function processShaped(value: Record<string, unknown>): boolean {
|
|
218
|
-
return typeof value['exit_code'] === 'number'
|
|
219
|
-
|| typeof value['stdout_tail'] === 'string' || typeof value['stderr_tail'] === 'string';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function parsed(value: string): Record<string, unknown> | undefined {
|
|
223
|
-
try {
|
|
224
|
-
return record(JSON.parse(value));
|
|
225
|
-
} catch {
|
|
226
|
-
return undefined;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function record(value: unknown): Record<string, unknown> | undefined {
|
|
231
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
232
|
-
? value as Record<string, unknown> : undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
212
|
function shellQuote(value: string): string {
|
|
237
213
|
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
238
214
|
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
package/src/cli-commands.ts
CHANGED
|
@@ -96,6 +96,7 @@ const LOCAL_EXECUTION_OPTIONS = [
|
|
|
96
96
|
},
|
|
97
97
|
{ flags: '--no-spawn', description: 'Require a running relayflowd rather than starting one' },
|
|
98
98
|
{ flags: '--no-observer-link', description: 'Do not mint an observer link for this run' },
|
|
99
|
+
{ flags: '--cloud-mirror', description: 'Also put this run on the Cloud dashboard (also FLOWS_CLOUD_MIRROR=1)' },
|
|
99
100
|
{
|
|
100
101
|
flags: '--allow-human-influenced',
|
|
101
102
|
description: 'Proceed even though the run carries human-influenced state',
|
|
@@ -196,6 +197,7 @@ export const CLI_VERBS = [
|
|
|
196
197
|
options: [
|
|
197
198
|
{ flags: '--step <name>', description: 'Show that agent step’s transcript instead of the runner log' },
|
|
198
199
|
{ flags: '--raw', description: 'Print the transcript JSONL unrendered (still redacted)' },
|
|
200
|
+
{ flags: '--follow', description: 'Append new runner output until the run ends; exits with the run’s outcome' },
|
|
199
201
|
JSON_OPTION,
|
|
200
202
|
],
|
|
201
203
|
variants: ['logs'],
|
|
@@ -331,6 +333,7 @@ export const CLI_VERBS = [
|
|
|
331
333
|
DATA_DIR_OPTION,
|
|
332
334
|
{ flags: '--tail <n>', description: 'Lines of each agent attempt’s transcript tail to show' },
|
|
333
335
|
{ flags: '--cloud', description: 'Read the run from Cloud instead of a local journal; needs the run id' },
|
|
336
|
+
{ flags: '--watch', description: 'With --cloud: redraw until the run ends, then exit with its outcome' },
|
|
334
337
|
],
|
|
335
338
|
variants: ['status'],
|
|
336
339
|
},
|