@relayflows/sdk 2.0.22 → 2.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-artifacts.d.ts +18 -9
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +27 -11
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/authored-flow-error.d.ts +33 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +20 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +4 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-node-entry.js +5 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +30 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +3 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.js +13 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/authored-step-index.d.ts +82 -0
- package/dist/authored-step-index.d.ts.map +1 -0
- package/dist/authored-step-index.js +120 -0
- package/dist/authored-step-index.js.map +1 -0
- package/dist/authored-step-output.d.ts +23 -2
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +47 -5
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts +10 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +81 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +67 -12
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-read.d.ts +31 -0
- package/dist/cli/cloud-read.d.ts.map +1 -0
- package/dist/cli/cloud-read.js +482 -0
- package/dist/cli/cloud-read.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +53 -25
- 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 +14 -0
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-failure.d.ts +26 -0
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +68 -3
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +29 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +23 -1
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -2
- package/dist/cli.js.map +1 -1
- package/dist/cloud-http.d.ts +12 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +14 -6
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-read.d.ts +154 -0
- package/dist/cloud-read.d.ts.map +1 -0
- package/dist/cloud-read.js +373 -0
- package/dist/cloud-read.js.map +1 -0
- package/dist/cloud-transcript.d.ts +94 -0
- package/dist/cloud-transcript.d.ts.map +1 -0
- package/dist/cloud-transcript.js +342 -0
- package/dist/cloud-transcript.js.map +1 -0
- package/dist/communication/environment.d.ts +3 -0
- package/dist/communication/environment.d.ts.map +1 -0
- package/dist/communication/environment.js +34 -0
- package/dist/communication/environment.js.map +1 -0
- package/dist/communication/history.d.ts +6 -0
- package/dist/communication/history.d.ts.map +1 -0
- package/dist/communication/history.js +38 -0
- package/dist/communication/history.js.map +1 -0
- package/dist/communication/local.d.ts +6 -0
- package/dist/communication/local.d.ts.map +1 -0
- package/dist/communication/local.js +56 -0
- package/dist/communication/local.js.map +1 -0
- package/dist/communication/preflight.d.ts +6 -0
- package/dist/communication/preflight.d.ts.map +1 -0
- package/dist/communication/preflight.js +37 -0
- package/dist/communication/preflight.js.map +1 -0
- package/dist/communication/projection.d.ts +8 -0
- package/dist/communication/projection.d.ts.map +1 -0
- package/dist/communication/projection.js +23 -0
- package/dist/communication/projection.js.map +1 -0
- package/dist/communication/relay.d.ts +78 -0
- package/dist/communication/relay.d.ts.map +1 -0
- package/dist/communication/relay.js +89 -0
- package/dist/communication/relay.js.map +1 -0
- package/dist/communication/session.d.ts +24 -0
- package/dist/communication/session.d.ts.map +1 -0
- package/dist/communication/session.js +100 -0
- package/dist/communication/session.js.map +1 -0
- package/dist/communication/spec.d.ts +20 -0
- package/dist/communication/spec.d.ts.map +1 -0
- package/dist/communication/spec.js +113 -0
- package/dist/communication/spec.js.map +1 -0
- package/dist/communication/tool.d.ts +3 -0
- package/dist/communication/tool.d.ts.map +1 -0
- package/dist/communication/tool.js +23 -0
- package/dist/communication/tool.js.map +1 -0
- package/dist/communication/tools.d.ts +11 -0
- package/dist/communication/tools.d.ts.map +1 -0
- package/dist/communication/tools.js +65 -0
- package/dist/communication/tools.js.map +1 -0
- package/dist/communication/worker.d.ts +6 -0
- package/dist/communication/worker.d.ts.map +1 -0
- package/dist/communication/worker.js +120 -0
- package/dist/communication/worker.js.map +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +8 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +16 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +1 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +4 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +11 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/preflight.d.ts +13 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +23 -6
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +39 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +8 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +29 -50
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -1
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +8 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +156 -23
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-artifacts.ts +27 -11
- package/src/authored-flow-error.ts +17 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-node-entry.ts +5 -1
- package/src/authored-node-runner.ts +33 -1
- package/src/authored-root.ts +2 -1
- package/src/authored-source-authority.ts +13 -0
- package/src/authored-step-index.ts +157 -0
- package/src/authored-step-output.ts +66 -14
- package/src/authored-worker-step.ts +88 -15
- package/src/cli/check.ts +69 -11
- package/src/cli/cloud-read.ts +518 -0
- package/src/cli/run.ts +43 -25
- package/src/cli/status.ts +20 -0
- package/src/cli/step-failure.ts +68 -3
- package/src/cli-commands.ts +23 -1
- package/src/cli.ts +20 -1
- package/src/cloud-http.ts +32 -6
- package/src/cloud-read.ts +515 -0
- package/src/cloud-transcript.ts +422 -0
- package/src/communication/environment.ts +33 -0
- package/src/communication/history.ts +38 -0
- package/src/communication/local.ts +41 -0
- package/src/communication/preflight.ts +34 -0
- package/src/communication/projection.ts +23 -0
- package/src/communication/relay.ts +102 -0
- package/src/communication/session.ts +84 -0
- package/src/communication/spec.ts +106 -0
- package/src/communication/tool.ts +22 -0
- package/src/communication/tools.ts +50 -0
- package/src/communication/worker.ts +95 -0
- package/src/compile.ts +4 -1
- package/src/failure-kinds.ts +16 -0
- package/src/index.ts +10 -1
- package/src/journal-client.ts +12 -2
- package/src/preflight.ts +36 -7
- package/src/protocol.ts +15 -0
- package/src/spec.ts +2 -0
- package/src/worker-cli.ts +28 -47
- package/src/worker.ts +10 -0
- package/src/wrapper-session.ts +159 -24
|
@@ -46,7 +46,20 @@ function installPinnedSourceLoader(sources: Map<string, string>): void {
|
|
|
46
46
|
pinnedSourceLoaderInstalled = true;
|
|
47
47
|
// Node >=22.14 supports register and stripTypeScriptTypes. A loader thread
|
|
48
48
|
// receives the captured bytes, never reopening mutable authored paths.
|
|
49
|
+
// The `stripTypeScriptTypes` call below makes Node emit
|
|
50
|
+
// `ExperimentalWarning: stripTypeScriptTypes …` on the child's inherited
|
|
51
|
+
// stderr, where a bootstrap that reads the CLI's stderr takes it for the
|
|
52
|
+
// error text. It is emitted on this loader thread, so a listener on the
|
|
53
|
+
// main thread cannot intercept it, and `--disable-warning` on the child
|
|
54
|
+
// would silence every ExperimentalWarning the authored body raises too.
|
|
55
|
+
// Dropping it by its own text, on this thread only, is the narrow form:
|
|
56
|
+
// Node's own printer still handles every other warning.
|
|
49
57
|
const loader = `
|
|
58
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
59
|
+
process.emitWarning = (warning, ...rest) => {
|
|
60
|
+
if (rest[0] === 'ExperimentalWarning' && String(warning).startsWith('stripTypeScriptTypes')) return;
|
|
61
|
+
emitWarning(warning, ...rest);
|
|
62
|
+
};
|
|
50
63
|
import {stripTypeScriptTypes} from 'node:module';
|
|
51
64
|
let sources;
|
|
52
65
|
export function initialize(data){sources=new Map(data);}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { JournalClient } from './journal-client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The root run's durable index of the child runs an authored body opened.
|
|
5
|
+
*
|
|
6
|
+
* An authored flow is not one kernel run: every `f.run`, `f.agent`, `f.llm`
|
|
7
|
+
* and lowered gate is its own single-step child run with its own
|
|
8
|
+
* `<dataDir>/runs/<runId>.sqlite3`. The only thing that ties a child back to
|
|
9
|
+
* the root id a caller (or Cloud) knows is an index — and until now that index
|
|
10
|
+
* existed only in the root's SUCCESS completion output
|
|
11
|
+
* (`authored-root.ts` `driveRoot`). A failed body took
|
|
12
|
+
* `terminalizeRootFailure`, which journals `{ error }` and nothing else, so a
|
|
13
|
+
* failed run named no child at all. A failed run is exactly when its children
|
|
14
|
+
* are worth reading.
|
|
15
|
+
*
|
|
16
|
+
* The completion output could not be made to carry it either: the root is
|
|
17
|
+
* lowered as `type: 'agent'`, and the kernel nulls a failed agent's `output`
|
|
18
|
+
* (`preserve_failure_output`, relayflowd-core/src/machine.rs), preserving only
|
|
19
|
+
* a 2,000-character render in `verification.detail`. So the index is written
|
|
20
|
+
* where a failure cannot erase it — appended to a stream on the ROOT run as
|
|
21
|
+
* each child is opened, using the same `journal.streamAppend` durability the
|
|
22
|
+
* executor already relies on for predicate verdicts.
|
|
23
|
+
*
|
|
24
|
+
* Each record is bounded on its own rather than the index being truncated as
|
|
25
|
+
* one blob: a hundredth child must not push the first one out of the record.
|
|
26
|
+
*/
|
|
27
|
+
export const AUTHORED_STEP_STREAM = 'authored-steps';
|
|
28
|
+
|
|
29
|
+
/** Versioned so a reader can tell this record from a future one. */
|
|
30
|
+
const RECORD_KIND = 'relayflows.authored-step.v1';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-field cap. Step ids and run ids are short by construction; the cap
|
|
34
|
+
* exists so a hostile or buggy id cannot make one record unbounded.
|
|
35
|
+
*/
|
|
36
|
+
const FIELD_CHARS = 256;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `admitted` and `completed` are kept distinct on purpose. `admitted` says
|
|
40
|
+
* only "this child run exists and here is where to read it" — it never
|
|
41
|
+
* carries a `completionReason`, because manufacturing one for an attempt
|
|
42
|
+
* that has not completed would turn an unfinished child into a false fact.
|
|
43
|
+
* `completed` is written only from a terminal `step.completed` the journal
|
|
44
|
+
* actually holds.
|
|
45
|
+
*/
|
|
46
|
+
export interface AuthoredStepRecord {
|
|
47
|
+
readonly index: typeof RECORD_KIND;
|
|
48
|
+
readonly step: string;
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
readonly state: 'admitted' | 'completed';
|
|
51
|
+
/** Present only on `completed`. */
|
|
52
|
+
readonly completionReason?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The kernel step the completion came from, when it is not the authored
|
|
55
|
+
* operation's own id. A child spec has two steps once a named gate is
|
|
56
|
+
* lowered, so the terminal failure can be `<step>.verify` rather than
|
|
57
|
+
* `<step>` — saying which is the difference between an index and a guess.
|
|
58
|
+
*/
|
|
59
|
+
readonly kernelStep?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Append one record, or do nothing when there is no root to append to.
|
|
64
|
+
*
|
|
65
|
+
* The undefined-root case is the non-durable executor seam (`executeAuthoredFlow`
|
|
66
|
+
* called without `rootRunId`), which has no root run and therefore nothing to
|
|
67
|
+
* index. It is not a silent fallback: there is no durable place for the record
|
|
68
|
+
* to go, and the caller already knows it opted out of one.
|
|
69
|
+
*
|
|
70
|
+
* Append failures propagate (AGENTS.md rule 4). Callers on a path that is
|
|
71
|
+
* already failing must surface the persistence error ALONGSIDE the original
|
|
72
|
+
* failure rather than letting it replace it — see `alsoRecord`.
|
|
73
|
+
*/
|
|
74
|
+
export async function recordAuthoredChild(
|
|
75
|
+
journal: JournalClient,
|
|
76
|
+
rootRunId: string | undefined,
|
|
77
|
+
record: Omit<AuthoredStepRecord, 'index'>,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
if (rootRunId === undefined) return;
|
|
80
|
+
await journal.streamAppend(rootRunId, AUTHORED_STEP_STREAM, {
|
|
81
|
+
index: RECORD_KIND,
|
|
82
|
+
step: bound(record.step),
|
|
83
|
+
runId: bound(record.runId),
|
|
84
|
+
state: record.state,
|
|
85
|
+
...(record.completionReason === undefined ? {} : { completionReason: bound(record.completionReason) }),
|
|
86
|
+
...(record.kernelStep === undefined || record.kernelStep === record.step
|
|
87
|
+
? {} : { kernelStep: bound(record.kernelStep) }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Record a child on a path that is already throwing, keeping both facts.
|
|
93
|
+
*
|
|
94
|
+
* A failed append must not be relabelled as successful collection, and it must
|
|
95
|
+
* not replace the failure the caller is about to report. It returns the note
|
|
96
|
+
* to append to that failure's message instead.
|
|
97
|
+
*/
|
|
98
|
+
export async function alsoRecord(
|
|
99
|
+
journal: JournalClient,
|
|
100
|
+
rootRunId: string | undefined,
|
|
101
|
+
record: Omit<AuthoredStepRecord, 'index'>,
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
await recordAuthoredChild(journal, rootRunId, record);
|
|
105
|
+
return '';
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return `\nCould not persist the step index: ${error instanceof Error ? error.message : String(error)}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fold the root's stream back into one record per authored step.
|
|
113
|
+
*
|
|
114
|
+
* A `completed` record supersedes the `admitted` one for the same step; first
|
|
115
|
+
* admission order is preserved so the index reads in the order the body ran.
|
|
116
|
+
* Records this version does not recognise are skipped rather than trusted.
|
|
117
|
+
*/
|
|
118
|
+
export async function readAuthoredStepIndex(
|
|
119
|
+
journal: JournalClient,
|
|
120
|
+
rootRunId: string,
|
|
121
|
+
): Promise<AuthoredStepRecord[]> {
|
|
122
|
+
const index = new Map<string, AuthoredStepRecord>();
|
|
123
|
+
let offset = 0;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const page = await journal.streamRead(rootRunId, AUTHORED_STEP_STREAM, offset, 1000);
|
|
126
|
+
for (const message of page.messages) {
|
|
127
|
+
// `stream.read` returns either the envelope or the bare message,
|
|
128
|
+
// matching how the executor reads predicate verdicts.
|
|
129
|
+
const raw = (message as { message?: unknown }).message ?? message;
|
|
130
|
+
if (!isAuthoredStepRecord(raw)) continue;
|
|
131
|
+
const previous = index.get(raw.step);
|
|
132
|
+
// An `admitted` record arriving after a `completed` one (a resumed body
|
|
133
|
+
// re-admitting the same child under its stable admission key) must not
|
|
134
|
+
// un-complete it.
|
|
135
|
+
if (previous?.state === 'completed' && raw.state === 'admitted') continue;
|
|
136
|
+
index.set(raw.step, raw);
|
|
137
|
+
}
|
|
138
|
+
if (page.messages.length === 0 || page.next_offset <= offset) break;
|
|
139
|
+
offset = page.next_offset;
|
|
140
|
+
}
|
|
141
|
+
return [...index.values()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isAuthoredStepRecord(value: unknown): value is AuthoredStepRecord {
|
|
145
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
146
|
+
const record = value as Partial<AuthoredStepRecord>;
|
|
147
|
+
return record.index === RECORD_KIND
|
|
148
|
+
&& typeof record.step === 'string' && record.step.length > 0
|
|
149
|
+
&& typeof record.runId === 'string' && record.runId.length > 0
|
|
150
|
+
&& (record.state === 'admitted' || record.state === 'completed')
|
|
151
|
+
&& (record.completionReason === undefined || typeof record.completionReason === 'string')
|
|
152
|
+
&& (record.kernelStep === undefined || typeof record.kernelStep === 'string');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bound(value: string): string {
|
|
156
|
+
return value.length <= FIELD_CHARS ? value : `${value.slice(0, FIELD_CHARS)}…`;
|
|
157
|
+
}
|
|
@@ -3,6 +3,21 @@ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
|
3
3
|
import type { JournalClient } from './journal-client.js';
|
|
4
4
|
import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome } from './protocol.js';
|
|
5
5
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
6
|
+
import type { StepFailedDetails } from './failure-kinds.js';
|
|
7
|
+
import { inspectionHint, renderInspection, renderStepEvidence, stepFailureDetails } from './cli/step-failure.js';
|
|
8
|
+
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What an authored operation needs in order to leave readable evidence:
|
|
12
|
+
* the root run its child index belongs to, and the data dir whose journals
|
|
13
|
+
* a reader would open. Both optional — the non-durable executor seam has no
|
|
14
|
+
* root, and a caller that did not name a data dir still gets the `flows
|
|
15
|
+
* replay <run-id>` hint, just without the on-disk path.
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthoredStepContext {
|
|
18
|
+
readonly rootRunId?: string;
|
|
19
|
+
readonly dataDir?: string;
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
/**
|
|
8
23
|
* Shared by every `f.*` verb that lowers to one kernel step run in isolation:
|
|
@@ -18,12 +33,23 @@ import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
|
18
33
|
* terminal state. Given that, a single read from the start of this run's
|
|
19
34
|
* (small, single-step) journal is enough — no polling here, and no run
|
|
20
35
|
* outcome ever needs re-checking.
|
|
36
|
+
*
|
|
37
|
+
* A non-success completion used to throw `journal step "run-1" completed with
|
|
38
|
+
* retries_exhausted` and stop there, discarding the evidence it was holding:
|
|
39
|
+
* the kernel PRESERVES a failed deterministic step's `output`
|
|
40
|
+
* (`preserve_failure_output`, relayflowd-core/src/machine.rs), so
|
|
41
|
+
* `{exit_code, stdout_tail, stderr_tail}` — the answer to "which command
|
|
42
|
+
* failed and what did it print" — was in hand and dropped. Four different
|
|
43
|
+
* causes produced that one identical line. It now reads the same extractor
|
|
44
|
+
* `f.agent`/`f.llm` already reach through `classifyOutcome`, so both step
|
|
45
|
+
* families report one grammar instead of two divergent ones.
|
|
21
46
|
*/
|
|
22
47
|
export async function readCompletedStepOutput(
|
|
23
48
|
journal: JournalClient,
|
|
24
49
|
runId: string,
|
|
25
50
|
stepId: string,
|
|
26
51
|
journalSteps: AuthoredFlowJournalStep[],
|
|
52
|
+
context: AuthoredStepContext = {},
|
|
27
53
|
): Promise<unknown> {
|
|
28
54
|
const entries = (await journal.journalRead(runId, 1)).entries;
|
|
29
55
|
const completed = entries.find((entry) => isStepCompleted(entry, stepId));
|
|
@@ -34,13 +60,29 @@ export async function readCompletedStepOutput(
|
|
|
34
60
|
const reason = completed.payload.completionReason;
|
|
35
61
|
journalSteps.push(Object.freeze({ id: stepId, runId, completionReason: reason }));
|
|
36
62
|
if (reason !== 'success') {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
let message = `journal step "${stepId}" completed with ${reason}`;
|
|
64
|
+
let details: StepFailedDetails | undefined;
|
|
65
|
+
try {
|
|
66
|
+
details = await stepFailureDetails(journal, runId);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Inspection must not erase the already known step failure; the two
|
|
69
|
+
// failures stay separately visible, as in classifyOutcome.
|
|
70
|
+
message += ` Could not inspect the failed step: ${errorMessage(error)}`;
|
|
71
|
+
}
|
|
72
|
+
if (details !== undefined) message += renderStepEvidence(details);
|
|
73
|
+
// Appended whatever inspection found — including nothing. A failure shape
|
|
74
|
+
// this reader does not recognise must still end with somewhere to go.
|
|
75
|
+
const where = inspectionHint(runId, details?.stepId ?? stepId, context.dataDir);
|
|
76
|
+
message += renderInspection(where);
|
|
77
|
+
message += await alsoRecord(journal, context.rootRunId, {
|
|
78
|
+
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
79
|
+
...(details?.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
80
|
+
});
|
|
81
|
+
throw new AuthoredFlowExecutionError('step_failed', message, reason, runId, { ...details, ...where });
|
|
43
82
|
}
|
|
83
|
+
await recordAuthoredChild(journal, context.rootRunId, {
|
|
84
|
+
step: stepId, runId, state: 'completed', completionReason: reason,
|
|
85
|
+
});
|
|
44
86
|
return completed.payload.output;
|
|
45
87
|
}
|
|
46
88
|
|
|
@@ -49,15 +91,21 @@ export async function readSuccessfulOutput(
|
|
|
49
91
|
outcome: RunOutcome,
|
|
50
92
|
stepId: string,
|
|
51
93
|
journalSteps: AuthoredFlowJournalStep[],
|
|
94
|
+
context: AuthoredStepContext = {},
|
|
52
95
|
): Promise<string> {
|
|
53
|
-
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
const output = await readCompletedStepOutput(journal, outcome.run_id, stepId, journalSteps, context)
|
|
97
|
+
.catch(error => {
|
|
98
|
+
if (error instanceof AuthoredFlowExecutionError
|
|
99
|
+
&& (error.completionReason === 'timeout' || error.completionReason === 'lease_expired')) {
|
|
100
|
+
// A timeout is still a failed command, and the evidence extracted for
|
|
101
|
+
// it is the same evidence. Re-labelling the error must not delete it.
|
|
102
|
+
throw new AuthoredFlowExecutionError('lease_exceeded',
|
|
103
|
+
`f.run step "${stepId}" exceeded its command timeout.`
|
|
104
|
+
+ ` ${error.message.slice(`${error.code}: `.length)}`,
|
|
105
|
+
error.completionReason, error.runId, error.details);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
61
109
|
if (!isRecord(output) || typeof output['stdout_tail'] !== 'string') {
|
|
62
110
|
throw protocolViolation(outcome.run_id, `step "${stepId}" has no string stdout_tail`);
|
|
63
111
|
}
|
|
@@ -102,6 +150,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
102
150
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
103
151
|
}
|
|
104
152
|
|
|
153
|
+
function errorMessage(error: unknown): string {
|
|
154
|
+
return error instanceof Error ? error.message : String(error);
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
function protocolViolation(runId: string, detail: string): AuthoredFlowExecutionError {
|
|
106
158
|
return new AuthoredFlowExecutionError(
|
|
107
159
|
'journal_protocol_violation',
|
|
@@ -3,15 +3,17 @@ import { parseBudget } from './budget.js';
|
|
|
3
3
|
import type { AgentOptions, AgentResult, LlmOptions, NamedGate } from '@relayflows/surface';
|
|
4
4
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
5
5
|
import { checkAuthoredFlow } from './cli/check.js';
|
|
6
|
-
import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js';
|
|
6
|
+
import { classifyOutcome, type RunLifecycleOptions, type RunReport } from './cli/run.js';
|
|
7
7
|
import type { PreflightDiagnostic } from './preflight.js';
|
|
8
8
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
9
9
|
import type { JournalClient } from './journal-client.js';
|
|
10
10
|
import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
|
|
11
|
-
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
|
|
11
|
+
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput, type AuthoredStepContext } from './authored-step-output.js';
|
|
12
12
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
13
13
|
import { snapshotJsonValue } from './json-value.js';
|
|
14
14
|
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
15
|
+
import { alsoRecord, recordAuthoredChild } from './authored-step-index.js';
|
|
16
|
+
import type { StepFailedDetails } from './failure-kinds.js';
|
|
15
17
|
|
|
16
18
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
17
19
|
|
|
@@ -22,6 +24,10 @@ export function authoredWorkerRunner(
|
|
|
22
24
|
localAgentStream?: string, budget?: AuthoredBudget, headerBudget?: unknown,
|
|
23
25
|
rootRunId?: string,
|
|
24
26
|
) {
|
|
27
|
+
const context: AuthoredStepContext = {
|
|
28
|
+
...(rootRunId === undefined ? {} : { rootRunId }),
|
|
29
|
+
...(waitOptions.dataDir === undefined ? {} : { dataDir: waitOptions.dataDir }),
|
|
30
|
+
};
|
|
25
31
|
async function run(step: StepSpec): Promise<unknown> {
|
|
26
32
|
const id = step.id;
|
|
27
33
|
const authoring: FlowSpec = { version: SPEC_SCHEMA_VERSION, name: `${definition.name}/${id}`, steps: [step], ...(headerBudget === undefined ? {} : { budget: parseBudget(headerBudget) }) };
|
|
@@ -46,6 +52,10 @@ export function authoredWorkerRunner(
|
|
|
46
52
|
}
|
|
47
53
|
const spec = toKernelSpec(resolved);
|
|
48
54
|
const consume = async (outcome: import('./protocol.js').RunOutcome) => {
|
|
55
|
+
// Written BEFORE the wait, not after it. An agent runs for as long as its
|
|
56
|
+
// lease allows; if this process dies mid-step, this record is the only
|
|
57
|
+
// thing that still names the child run holding the evidence.
|
|
58
|
+
await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
|
|
49
59
|
// Reuse the declarative CLI's own wait/classification (cli/run.ts) rather
|
|
50
60
|
// than a hand-rolled poll: `step.completed` and the run's own terminal
|
|
51
61
|
// state are appended as two SEPARATE actions (kernel/relayflowd-core/src/machine.rs
|
|
@@ -69,17 +79,38 @@ export function authoredWorkerRunner(
|
|
|
69
79
|
);
|
|
70
80
|
}
|
|
71
81
|
if (execution.exitCode !== 0) {
|
|
82
|
+
// `execution.report.completionReason` is the RUN's reason (normally
|
|
83
|
+
// `step_failed`), not the step's. The step's terminal facts are the
|
|
84
|
+
// ones `classifyOutcome` already extracted with `stepFailureDetails`
|
|
85
|
+
// and assigned onto this diagnostic — read them from there rather than
|
|
86
|
+
// re-walking the journal or mislabelling the run reason as a step one.
|
|
87
|
+
const diagnostic = execution.report.diagnostics.at(-1);
|
|
88
|
+
const details = stepDetails(diagnostic);
|
|
72
89
|
const reason = execution.report.completionReason;
|
|
90
|
+
let message = diagnostic?.message
|
|
91
|
+
?? `flow "${definition.name}" step "${id}" did not complete successfully `
|
|
92
|
+
+ `(status: ${execution.report.status ?? 'unknown'})`;
|
|
93
|
+
// Only a reason the journal actually recorded for a step is indexed;
|
|
94
|
+
// an unfinished or unreadable child is left as `admitted`, never given
|
|
95
|
+
// a manufactured completion.
|
|
96
|
+
if (isSurfaceCompletionReason(details?.completionReason)) {
|
|
97
|
+
journalSteps.push(Object.freeze({
|
|
98
|
+
id, runId: outcome.run_id, completionReason: details.completionReason,
|
|
99
|
+
}));
|
|
100
|
+
message += await alsoRecord(journal, rootRunId, {
|
|
101
|
+
step: id, runId: outcome.run_id, state: 'completed',
|
|
102
|
+
completionReason: details.completionReason,
|
|
103
|
+
...(details.stepId === undefined ? {} : { kernelStep: details.stepId }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
73
106
|
throw new AuthoredFlowExecutionError(
|
|
74
|
-
'step_failed',
|
|
75
|
-
execution.report.diagnostics.at(-1)?.message
|
|
76
|
-
?? `flow "${definition.name}" step "${id}" did not complete successfully `
|
|
77
|
-
+ `(status: ${execution.report.status ?? 'unknown'})`,
|
|
107
|
+
'step_failed', message,
|
|
78
108
|
isSurfaceCompletionReason(reason) ? reason : undefined,
|
|
79
109
|
outcome.run_id,
|
|
110
|
+
details,
|
|
80
111
|
);
|
|
81
112
|
}
|
|
82
|
-
return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps);
|
|
113
|
+
return readCompletedStepOutput(journal, outcome.run_id, id, journalSteps, context);
|
|
83
114
|
};
|
|
84
115
|
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
85
116
|
return budget === undefined
|
|
@@ -173,11 +204,19 @@ export function authoredWorkerRunner(
|
|
|
173
204
|
};
|
|
174
205
|
}
|
|
175
206
|
|
|
176
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Deterministic commands execute inline under their per-invocation lease.
|
|
209
|
+
*
|
|
210
|
+
* `context` carries the root run (so each child is indexed on it) and the
|
|
211
|
+
* data dir (so a failure can name the journal on disk). It is the same object
|
|
212
|
+
* the worker runner builds; a failed `f.run` and a failed `f.agent` now report
|
|
213
|
+
* through one grammar.
|
|
214
|
+
*/
|
|
177
215
|
export function authoredDeterministicRunner(
|
|
178
216
|
name: string, journal: JournalClient, journalSteps: AuthoredFlowJournalStep[], budget: AuthoredBudget,
|
|
179
|
-
|
|
217
|
+
context: AuthoredStepContext = {},
|
|
180
218
|
) {
|
|
219
|
+
const rootRunId = context.rootRunId;
|
|
181
220
|
return async (id: string, command: string, terminal = false, leaseMs?: number, verification?: NamedGate): Promise<string> => {
|
|
182
221
|
const spec = toKernelSpec(compileSpec({
|
|
183
222
|
version: SPEC_SCHEMA_VERSION,
|
|
@@ -189,11 +228,45 @@ export function authoredDeterministicRunner(
|
|
|
189
228
|
}],
|
|
190
229
|
}));
|
|
191
230
|
const admissionKey = authoredChildAdmissionKey(rootRunId, id);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
231
|
+
const consume = async (outcome: import('./protocol.js').RunOutcome): Promise<string> => {
|
|
232
|
+
await recordAuthoredChild(journal, rootRunId, { step: id, runId: outcome.run_id, state: 'admitted' });
|
|
233
|
+
return readSuccessfulOutput(journal, outcome, id, journalSteps, context);
|
|
234
|
+
};
|
|
235
|
+
if (terminal) return consume(await journal.runStart(spec, undefined, admissionKey));
|
|
236
|
+
return budget.execute(journal, spec, consume, admissionKey);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Lift the step-shaped fields off a run diagnostic.
|
|
242
|
+
*
|
|
243
|
+
* `RunDiagnostic extends StepFailedDetails`, so `classifyOutcome`'s
|
|
244
|
+
* `Object.assign(diagnostic, details)` leaves the extraction right there. The
|
|
245
|
+
* fields are picked explicitly rather than spread, so `severity`, `kind` and
|
|
246
|
+
* `message` — which describe the RUN, not the step — cannot leak into
|
|
247
|
+
* something typed as the step's evidence.
|
|
248
|
+
*/
|
|
249
|
+
function stepDetails(
|
|
250
|
+
diagnostic: RunReport['diagnostics'][number] | undefined,
|
|
251
|
+
): StepFailedDetails | undefined {
|
|
252
|
+
if (diagnostic === undefined) return undefined;
|
|
253
|
+
// `RunDiagnostic` is the only member of this union that extends
|
|
254
|
+
// `StepFailedDetails`; the check-report shapes carry none of these fields,
|
|
255
|
+
// so reading them as optionals yields `undefined` and contributes nothing.
|
|
256
|
+
const found = diagnostic as Partial<StepFailedDetails>;
|
|
257
|
+
const details: StepFailedDetails = {
|
|
258
|
+
...(found.stepId === undefined ? {} : { stepId: found.stepId }),
|
|
259
|
+
...(found.stepType === undefined ? {} : { stepType: found.stepType }),
|
|
260
|
+
...(found.completionReason === undefined ? {} : { completionReason: found.completionReason }),
|
|
261
|
+
...(found.attempt === undefined ? {} : { attempt: found.attempt }),
|
|
262
|
+
...(found.maxIterations === undefined ? {} : { maxIterations: found.maxIterations }),
|
|
263
|
+
...(found.exitCode === undefined ? {} : { exitCode: found.exitCode }),
|
|
264
|
+
...(found.stdoutTail === undefined ? {} : { stdoutTail: found.stdoutTail }),
|
|
265
|
+
...(found.stderrTail === undefined ? {} : { stderrTail: found.stderrTail }),
|
|
266
|
+
...(found.detail === undefined ? {} : { detail: found.detail }),
|
|
267
|
+
...(found.transcriptPath === undefined ? {} : { transcriptPath: found.transcriptPath }),
|
|
268
|
+
...(found.hint === undefined ? {} : { hint: found.hint }),
|
|
269
|
+
...(found.journalPath === undefined ? {} : { journalPath: found.journalPath }),
|
|
198
270
|
};
|
|
271
|
+
return Object.keys(details).length === 0 ? undefined : details;
|
|
199
272
|
}
|
package/src/cli/check.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { communicationInstruction } from '../communication/spec.js';
|
|
2
|
+
import { checkCommunicationEnvironment } from '../communication/preflight.js';
|
|
3
|
+
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
1
4
|
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
5
|
import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
|
|
3
6
|
import { spawnSync } from 'node:child_process';
|
|
@@ -159,6 +162,16 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
159
162
|
config.directory,
|
|
160
163
|
)
|
|
161
164
|
: undefined;
|
|
165
|
+
if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
166
|
+
try { checkCommunicationEnvironment(flow); }
|
|
167
|
+
catch (error) {
|
|
168
|
+
result.ok = false;
|
|
169
|
+
result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
|
|
170
|
+
message: error instanceof Error ? error.message : 'Communication environment could not be checked.' });
|
|
171
|
+
}
|
|
172
|
+
result.diagnostics.push({ severity: 'warning', kind: 'budget_unmetered',
|
|
173
|
+
message: 'Managed communication sessions do not report token or dollar usage. Budget ceilings cannot bound their spend; communication.timeoutMs bounds their duration.' });
|
|
174
|
+
}
|
|
162
175
|
return {
|
|
163
176
|
report: {
|
|
164
177
|
ok: result.ok,
|
|
@@ -169,7 +182,7 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
169
182
|
diagnostics: result.diagnostics,
|
|
170
183
|
requirements: safeRequirements(authoring, config.cli),
|
|
171
184
|
},
|
|
172
|
-
...(flow !== undefined ? { flow } : {}),
|
|
185
|
+
...(result.ok && flow !== undefined ? { flow } : {}),
|
|
173
186
|
};
|
|
174
187
|
} catch (error) {
|
|
175
188
|
const failure = error instanceof CheckFailure
|
|
@@ -375,7 +388,7 @@ function findConfig(start: string): string | undefined {
|
|
|
375
388
|
function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes {
|
|
376
389
|
return {
|
|
377
390
|
helper: helperReady,
|
|
378
|
-
cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model),
|
|
391
|
+
cli: (cli, source, model, execution) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model, execution),
|
|
379
392
|
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
380
393
|
// A deterministic step runs in the daemon's working directory — the
|
|
381
394
|
// directory `flows run` was invoked from, or Cloud's code mount — not in
|
|
@@ -416,12 +429,22 @@ function probeCli(
|
|
|
416
429
|
cli: string,
|
|
417
430
|
directory: string,
|
|
418
431
|
model?: string,
|
|
432
|
+
execution?: 'managed',
|
|
419
433
|
): CliProbeResult {
|
|
420
434
|
const executable = resolveExecutable(cli, directory);
|
|
421
435
|
if (executable === undefined) return { exists: false, authenticated: false };
|
|
422
436
|
const kind = cliAdapterKind(executable);
|
|
437
|
+
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
438
|
+
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
439
|
+
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
440
|
+
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
441
|
+
return { exists: true, supported: true, authenticated: 'unverified' };
|
|
442
|
+
}
|
|
443
|
+
const environment = execution === 'managed'
|
|
444
|
+
? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
|
|
445
|
+
const probe = (invocation: CliInvocation) => runProbe(executable, directory, invocation, environment);
|
|
423
446
|
const identification = adapterIdentification(kind);
|
|
424
|
-
const identified =
|
|
447
|
+
const identified = probe(identification.invocation);
|
|
425
448
|
if (
|
|
426
449
|
identified.status !== 0
|
|
427
450
|
|| (identification.expectedStdout !== undefined
|
|
@@ -435,7 +458,7 @@ function probeCli(
|
|
|
435
458
|
return {
|
|
436
459
|
exists: true,
|
|
437
460
|
supported: true,
|
|
438
|
-
authenticated:
|
|
461
|
+
authenticated: probe(auth).status === 0,
|
|
439
462
|
authCommand,
|
|
440
463
|
};
|
|
441
464
|
}
|
|
@@ -445,7 +468,7 @@ function probeCli(
|
|
|
445
468
|
// A successful real provider round trip (or identified wrapper probe)
|
|
446
469
|
// proves both auth and exact-model access. On failure, run the adapter's
|
|
447
470
|
// actual auth command solely to classify auth vs model access truthfully.
|
|
448
|
-
if (
|
|
471
|
+
if (probe(scoped).status === 0) {
|
|
449
472
|
return {
|
|
450
473
|
exists: true,
|
|
451
474
|
supported: true,
|
|
@@ -455,35 +478,70 @@ function probeCli(
|
|
|
455
478
|
modelCommand,
|
|
456
479
|
};
|
|
457
480
|
}
|
|
458
|
-
const
|
|
481
|
+
const authProbe = probe(auth);
|
|
482
|
+
const authenticated = authProbe.status === 0;
|
|
459
483
|
return {
|
|
460
484
|
exists: true,
|
|
461
485
|
supported: true,
|
|
462
|
-
authenticated
|
|
486
|
+
authenticated,
|
|
463
487
|
modelAvailable: false,
|
|
464
488
|
authCommand,
|
|
465
489
|
modelCommand,
|
|
490
|
+
// Only on failure: on success there is nothing to explain, and the output
|
|
491
|
+
// is the most identity-bearing thing this function touches.
|
|
492
|
+
...(authenticated
|
|
493
|
+
? {}
|
|
494
|
+
: {
|
|
495
|
+
authExitCode: authProbe.status,
|
|
496
|
+
authFailureDetail: redactProbeOutput(
|
|
497
|
+
`${authProbe.stderr}${authProbe.stdout}`,
|
|
498
|
+
).trim().slice(0, 500),
|
|
499
|
+
}),
|
|
466
500
|
};
|
|
467
501
|
}
|
|
468
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Redact anything that looks like a credential or an account identifier.
|
|
505
|
+
*
|
|
506
|
+
* `auth status` output is diagnostic, but it is also the one place an account
|
|
507
|
+
* email, org id or token fragment can appear. The point of surfacing it is to
|
|
508
|
+
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
509
|
+
* refusal message that gets pasted into issues does not.
|
|
510
|
+
*/
|
|
511
|
+
function redactProbeOutput(text: string): string {
|
|
512
|
+
return text
|
|
513
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
514
|
+
.replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
|
|
515
|
+
.replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
|
|
516
|
+
}
|
|
517
|
+
|
|
469
518
|
function runProbe(
|
|
470
519
|
executable: string,
|
|
471
520
|
directory: string,
|
|
472
521
|
invocation: CliInvocation,
|
|
473
|
-
|
|
474
|
-
|
|
522
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
523
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
524
|
+
const env = { ...environment };
|
|
475
525
|
delete env[MODEL_ENV];
|
|
476
526
|
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
477
527
|
const result = spawnSync(executable, invocation.args, {
|
|
478
528
|
cwd: directory,
|
|
479
529
|
encoding: 'utf8',
|
|
480
|
-
|
|
530
|
+
// stderr was 'ignore'. A failing `auth status` writes its reason there, so
|
|
531
|
+
// discarding it made every authentication refusal structurally
|
|
532
|
+
// undiagnosable: the refusal could say a probe exited non-zero and never
|
|
533
|
+
// what it said. Captured, then redacted at the point of use.
|
|
534
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
481
535
|
timeout: invocation.timeoutMs,
|
|
482
536
|
env,
|
|
483
537
|
});
|
|
484
538
|
const failure = classifySpawnFailure(result.error, result.signal, invocation.timeoutMs);
|
|
485
539
|
if (failure !== undefined) throw failure;
|
|
486
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
status: result.status,
|
|
542
|
+
stdout: result.stdout,
|
|
543
|
+
stderr: result.stderr ?? '',
|
|
544
|
+
};
|
|
487
545
|
}
|
|
488
546
|
|
|
489
547
|
function resolveExecutable(command: string, directory: string): string | undefined {
|