@relayflows/sdk 2.0.12 → 2.0.14
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/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +16 -7
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +5 -0
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/budget-preflight.d.ts +17 -6
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +9 -6
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +8 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +5 -0
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +8 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +23 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +78 -11
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts +40 -0
- package/dist/cli/step-failure.d.ts.map +1 -0
- package/dist/cli/step-failure.js +150 -0
- package/dist/cli/step-failure.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/daemon-connection.d.ts +7 -0
- package/dist/daemon-connection.d.ts.map +1 -1
- package/dist/daemon-connection.js +7 -0
- package/dist/daemon-connection.js.map +1 -1
- package/dist/failure-kinds.d.ts +25 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +5 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +2 -6
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js.map +1 -1
- package/dist/model-pricing.d.ts +7 -5
- package/dist/model-pricing.d.ts.map +1 -1
- package/dist/model-pricing.js +7 -5
- package/dist/model-pricing.js.map +1 -1
- package/dist/preflight.d.ts +5 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +22 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +22 -7
- package/dist/spec.d.ts.map +1 -1
- package/dist/worker-spend.d.ts +13 -9
- package/dist/worker-spend.d.ts.map +1 -1
- package/dist/worker-spend.js +22 -8
- package/dist/worker-spend.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-budget.ts +35 -9
- package/src/authored-flow-executor.ts +5 -0
- package/src/budget-preflight.ts +17 -6
- package/src/cli/build.ts +7 -0
- package/src/cli/deploy.ts +4 -0
- package/src/cli/direct-run.ts +8 -0
- package/src/cli/run.ts +94 -11
- package/src/cli/step-failure.ts +160 -0
- package/src/cli.ts +4 -4
- package/src/daemon-connection.ts +8 -0
- package/src/failure-kinds.ts +25 -3
- package/src/journal-client.ts +2 -1
- package/src/model-pricing.ts +7 -5
- package/src/preflight.ts +5 -0
- package/src/protocol.ts +15 -2
- package/src/spec.ts +23 -1
- package/src/worker-spend.ts +25 -9
- package/dist/cli/deterministic-failure.d.ts +0 -5
- package/dist/cli/deterministic-failure.d.ts.map +0 -1
- package/dist/cli/deterministic-failure.js +0 -66
- package/dist/cli/deterministic-failure.js.map +0 -1
- package/src/cli/deterministic-failure.ts +0 -69
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { parseDigestReference } from './bundle-transport.js';
|
|
|
30
30
|
import { parseBuildArgs, runBuild, type BuildArgs } from './cli/build.js';
|
|
31
31
|
import { runHnMonitor } from './cli/hn-monitor.js';
|
|
32
32
|
import { runTickRunner } from './cli/tick-runner.js';
|
|
33
|
+
import { DEFAULT_DATA_DIR } from './daemon-connection.js';
|
|
33
34
|
import {
|
|
34
35
|
mintObserverUrl,
|
|
35
36
|
resolveObserverLinkEnv,
|
|
@@ -60,7 +61,6 @@ type ParsedArgs =
|
|
|
60
61
|
intervalMs: number; epochMs: number | undefined; maxCatchUp: number | undefined;
|
|
61
62
|
pollIntervalMs: number | undefined };
|
|
62
63
|
|
|
63
|
-
const DEFAULT_DATA_DIR = '.relayflowd';
|
|
64
64
|
const USAGE = [
|
|
65
65
|
'Usage:',
|
|
66
66
|
'flows add <helper-name|@flows/helper-name>',
|
|
@@ -299,7 +299,7 @@ async function observerUrlFrom(
|
|
|
299
299
|
if (mint === undefined) return undefined;
|
|
300
300
|
const outcome = await mint;
|
|
301
301
|
if (outcome.warning !== undefined) {
|
|
302
|
-
io.stderr(`[observer] token mint failed: ${outcome.warning};
|
|
302
|
+
io.stderr(`[observer] token mint failed: ${outcome.warning}; continuing without an observer link (the run is unaffected)`);
|
|
303
303
|
}
|
|
304
304
|
return outcome.observerUrl;
|
|
305
305
|
}
|
|
@@ -394,11 +394,11 @@ export async function finalizeObserverLine(
|
|
|
394
394
|
const outcome = await Promise.race([mint, timeout]);
|
|
395
395
|
if (timer !== undefined) clearTimeout(timer);
|
|
396
396
|
if (outcome === TIMED_OUT) {
|
|
397
|
-
io.stderr('[observer] mint did not complete in time;
|
|
397
|
+
io.stderr('[observer] mint did not complete in time; continuing without an observer link (the run is unaffected)');
|
|
398
398
|
return;
|
|
399
399
|
}
|
|
400
400
|
if (outcome.warning !== undefined) {
|
|
401
|
-
io.stderr(`[observer] token mint failed: ${outcome.warning};
|
|
401
|
+
io.stderr(`[observer] token mint failed: ${outcome.warning}; continuing without an observer link (the run is unaffected)`);
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
if (outcome.observerUrl !== undefined) io.stdout(`Observer: ${outcome.observerUrl}`);
|
package/src/daemon-connection.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const CONNECTION_FILE = 'connection.json';
|
|
|
41
41
|
export const SOCKET_FILE = 'relayflowd.sock';
|
|
42
42
|
export const DAEMON_LOG_FILE = 'relayflowd.log';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The data dir every verb defaults to. It lives here, next to the other paths
|
|
46
|
+
* derived from it, so the CLI's `--data-dir` default and the failure
|
|
47
|
+
* diagnostic that decides whether to echo `--data-dir` back at an operator
|
|
48
|
+
* cannot drift apart.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_DATA_DIR = '.relayflowd';
|
|
51
|
+
|
|
44
52
|
/** `<data-dir>/connection.json`, exactly the shape in §1. */
|
|
45
53
|
export interface DaemonConnection {
|
|
46
54
|
socket_path: string;
|
package/src/failure-kinds.ts
CHANGED
|
@@ -62,8 +62,11 @@ export const CHECK_FAILURE_KINDS = [
|
|
|
62
62
|
* accepting every output must not be reported as if it constrained one.
|
|
63
63
|
*
|
|
64
64
|
* `budget_unmetered` names an LLM/agent step under a dollar budget whose model
|
|
65
|
-
* has no frozen price (including Codex, which selects its own model).
|
|
66
|
-
*
|
|
65
|
+
* has no frozen price (including Codex, which selects its own model). A
|
|
66
|
+
* missing price never refuses: the step runs, journals its tokens with
|
|
67
|
+
* `dollars_unmetered: true`, and cannot cross `maxDollars`; token limits still
|
|
68
|
+
* apply. It replaced the `budget_missing_price` refusal (#421), which older
|
|
69
|
+
* `flows check` reports may still show.
|
|
67
70
|
*/
|
|
68
71
|
export const PREFLIGHT_WARNING_KINDS = [
|
|
69
72
|
'unprovable_effects',
|
|
@@ -122,13 +125,32 @@ export type PreflightWarningKind = (typeof PREFLIGHT_WARNING_KINDS)[number];
|
|
|
122
125
|
export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
|
|
123
126
|
export type RunWarningKind = (typeof RUN_WARNING_KINDS)[number];
|
|
124
127
|
|
|
125
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Optional evidence on the existing step_failed diagnostic, not a new kind.
|
|
130
|
+
*
|
|
131
|
+
* Every field is optional because a failure must be reportable on whatever it
|
|
132
|
+
* left behind. A deterministic step leaves `exitCode` plus output tails; an
|
|
133
|
+
* agent or llm step leaves `completionReason` and whatever the daemon captured
|
|
134
|
+
* into `detail` (see cli/step-failure.ts). Absent means "not journaled", never
|
|
135
|
+
* "zero" — an exit code is only ever reported when one was actually recorded.
|
|
136
|
+
*/
|
|
126
137
|
export interface StepFailedDetails {
|
|
127
138
|
stepId?: string;
|
|
139
|
+
/** `deterministic` | `llm` | `agent`, when the run snapshot named one. */
|
|
140
|
+
stepType?: string;
|
|
141
|
+
/** The kernel's per-step reason, e.g. `worker_error`, `retries_exhausted`. */
|
|
142
|
+
completionReason?: string;
|
|
128
143
|
exitCode?: number;
|
|
129
144
|
/** Terminal-safe UTF-8 excerpt, at most 1,024 bytes. */
|
|
145
|
+
stdoutTail?: string;
|
|
146
|
+
/** Terminal-safe UTF-8 excerpt, at most 1,024 bytes. */
|
|
130
147
|
stderrTail?: string;
|
|
148
|
+
/** The daemon's own account, when it was not a render of the fields above. */
|
|
149
|
+
detail?: string;
|
|
150
|
+
/** A runnable `flows replay` invocation for this run. */
|
|
131
151
|
hint?: string;
|
|
152
|
+
/** The on-disk journal for this run, when the data dir is known. */
|
|
153
|
+
journalPath?: string;
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
const CHECK_FAILURE_KIND_SET: ReadonlySet<string> = new Set(CHECK_FAILURE_KINDS);
|
package/src/journal-client.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type Request,
|
|
23
23
|
type Response,
|
|
24
24
|
type ServerEvent,
|
|
25
|
+
type StepUsage,
|
|
25
26
|
} from './protocol.js';
|
|
26
27
|
import type { KernelRunSpec, StepType } from './spec.js';
|
|
27
28
|
|
|
@@ -366,7 +367,7 @@ export class JournalClient extends EventEmitter {
|
|
|
366
367
|
completionReason: CompletionReason,
|
|
367
368
|
extra: {
|
|
368
369
|
output?: unknown;
|
|
369
|
-
usage?:
|
|
370
|
+
usage?: StepUsage;
|
|
370
371
|
started_pins?: Pins;
|
|
371
372
|
end_pins?: Pins;
|
|
372
373
|
effects?: EffectRef[];
|
package/src/model-pricing.ts
CHANGED
|
@@ -14,11 +14,13 @@ export function hasPricing(model: string | undefined): boolean {
|
|
|
14
14
|
/**
|
|
15
15
|
* Cost accounting for a step's declared model.
|
|
16
16
|
*
|
|
17
|
-
* Returns `undefined` for unpriced models
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* dollars
|
|
21
|
-
*
|
|
17
|
+
* Returns `undefined` for unpriced models, meaning "no known dollar cost" —
|
|
18
|
+
* never a zero price. Workers do not omit usage on that signal: `workerSpend`
|
|
19
|
+
* falls back to unmetered usage (reported tokens plus `dollars_unmetered: true`,
|
|
20
|
+
* no `dollars`), so token ceilings still count the step and the journal records
|
|
21
|
+
* its dollars as unknown. An unpriced step is unmetered, not refused:
|
|
22
|
+
* `budgetDiagnostics` warns about it at preflight and it cannot cross
|
|
23
|
+
* `maxDollars`. Codex model ids need no entry here; Codex selects its own model.
|
|
22
24
|
*/
|
|
23
25
|
export function pricedUsage(model: string | undefined, input = 0, output = 0):
|
|
24
26
|
| { tokens_in: number; tokens_out: number; dollars: string }
|
package/src/preflight.ts
CHANGED
|
@@ -117,6 +117,11 @@ export type PreflightDiagnostic = PreflightRefusal | PreflightWarning;
|
|
|
117
117
|
export interface PreflightResult {
|
|
118
118
|
plugins?: readonly LoadedPlugin[];
|
|
119
119
|
mcpTools?: Readonly<Record<string, readonly string[]>>;
|
|
120
|
+
/**
|
|
121
|
+
* True when no diagnostic is a refusal. `ok: true` may still carry warnings
|
|
122
|
+
* (e.g. `budget_unmetered`); callers that report to a person must surface
|
|
123
|
+
* `diagnostics`, not just branch on `ok`.
|
|
124
|
+
*/
|
|
120
125
|
ok: boolean;
|
|
121
126
|
gates: StepGateInspection[];
|
|
122
127
|
resolutions: CliResolution[];
|
package/src/protocol.ts
CHANGED
|
@@ -72,10 +72,23 @@ export interface HelloResult {
|
|
|
72
72
|
export interface StepSpend {
|
|
73
73
|
tokens_input: number;
|
|
74
74
|
tokens_output: number;
|
|
75
|
+
/** Metered dollars; a lower bound when `dollars_unmetered` is set. */
|
|
75
76
|
dollars: number;
|
|
77
|
+
/** Present (true) only when the step spent tokens of unknown dollar cost. */
|
|
78
|
+
dollars_unmetered?: true;
|
|
76
79
|
wallclock_ms: number;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Worker-reported `step.complete` usage. A priced step reports exact
|
|
84
|
+
* `dollars`; a step whose model has no frozen price reports its tokens with
|
|
85
|
+
* `dollars_unmetered: true` and no dollar amount, so unknown cost is never
|
|
86
|
+
* journaled as a measured $0. See `workerSpend` for the full contract.
|
|
87
|
+
*/
|
|
88
|
+
export type StepUsage =
|
|
89
|
+
| { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: never }
|
|
90
|
+
| { tokens_in: number; tokens_out: number; dollars_unmetered: true; dollars?: never };
|
|
91
|
+
|
|
79
92
|
export interface RunStartParams {
|
|
80
93
|
/** Caller-owned retry identity. Reuse with a different spec is refused. */
|
|
81
94
|
admission_key?: string;
|
|
@@ -130,7 +143,7 @@ export interface RunGetResult {
|
|
|
130
143
|
run_id: string;
|
|
131
144
|
status: RunStatus;
|
|
132
145
|
steps: Record<string, StepSnapshot>;
|
|
133
|
-
budget: { tokens_in: number; tokens_out: number; dollars: string };
|
|
146
|
+
budget: { tokens_in: number; tokens_out: number; dollars: string; dollars_unmetered?: true };
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
export interface RunWatchParams {
|
|
@@ -307,7 +320,7 @@ export interface StepCompleteParams {
|
|
|
307
320
|
idempotency_key: string;
|
|
308
321
|
completionReason: CompletionReason;
|
|
309
322
|
output?: unknown;
|
|
310
|
-
usage?:
|
|
323
|
+
usage?: StepUsage;
|
|
311
324
|
started_pins?: Pins;
|
|
312
325
|
end_pins?: Pins;
|
|
313
326
|
effects?: EffectRef[];
|
package/src/spec.ts
CHANGED
|
@@ -452,9 +452,31 @@ export interface KernelAgentStep extends KernelStepCommon {
|
|
|
452
452
|
|
|
453
453
|
export type KernelStepSpec = KernelDeterministicStep | KernelLlmStep | KernelAgentStep;
|
|
454
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Accounting carried into a run that continues an earlier one. The authored
|
|
457
|
+
* executor lowers each step to its own kernel run and sends the previous run's
|
|
458
|
+
* totals here. It mirrors the kernel's `PriorSpend` field for field, so the
|
|
459
|
+
* handoff cannot quietly narrow what a carried budget can express.
|
|
460
|
+
*/
|
|
461
|
+
export interface KernelPriorSpend {
|
|
462
|
+
tokens_in: number;
|
|
463
|
+
tokens_out: number;
|
|
464
|
+
/** Metered dollars only; a lower bound when `dollars_unmetered` is set. */
|
|
465
|
+
dollars: string;
|
|
466
|
+
wallclock_ms: number;
|
|
467
|
+
day?: number;
|
|
468
|
+
/**
|
|
469
|
+
* At least one carried charge spent tokens of unknown dollar cost, so
|
|
470
|
+
* `dollars` is a lower bound rather than a measured total. Omitted when
|
|
471
|
+
* false, so a fully metered flow's payload keeps its exact bytes and a
|
|
472
|
+
* kernel that predates the key never receives it.
|
|
473
|
+
*/
|
|
474
|
+
dollars_unmetered?: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
455
477
|
export interface KernelBudgetSpec {
|
|
456
478
|
pricing?: 'frozen';
|
|
457
|
-
prior_spend?:
|
|
479
|
+
prior_spend?: KernelPriorSpend;
|
|
458
480
|
max_tokens?: number;
|
|
459
481
|
max_wallclock_ms?: number;
|
|
460
482
|
window?: 'day';
|
package/src/worker-spend.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { pricedUsage } from './model-pricing.js';
|
|
2
|
+
import type { StepUsage } from './protocol.js';
|
|
2
3
|
import type { WorkerCliResult } from './worker-cli.js';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Usage for a step whose dollar cost is unknown (unpriced or undeclared model).
|
|
7
|
+
*
|
|
8
|
+
* It carries the reported tokens and `dollars_unmetered: true`, and never a
|
|
9
|
+
* `dollars` amount: unknown cost is not journaled as a measured $0. The kernel
|
|
10
|
+
* records the flag on the step's `budget`/`spend` and on the run total, counts
|
|
11
|
+
* the tokens toward token ceilings, and compares only metered dollars against
|
|
12
|
+
* `maxDollars`. A CLI that reported no tokens still marks the step unmetered,
|
|
13
|
+
* with zero tokens — the same token accounting any unreported step gets.
|
|
14
|
+
*/
|
|
15
|
+
function unmeteredUsage(input: number | undefined, output: number | undefined): StepUsage {
|
|
16
|
+
return { tokens_in: input ?? 0, tokens_out: output ?? 0, dollars_unmetered: true };
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
/**
|
|
@@ -12,12 +21,19 @@ function unmeteredUsage(input: number | undefined, output: number | undefined) {
|
|
|
12
21
|
* (non-integer or negative) are the one remaining failure mode — those are
|
|
13
22
|
* journaled as `worker_error` with the usage projected from clamped counts.
|
|
14
23
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
24
|
+
* Contract across preflight → worker → kernel (keep all three aligned):
|
|
25
|
+
* - preflight (`budgetDiagnostics`) warns `budget_unmetered` for a
|
|
26
|
+
* dollar-budgeted step with no frozen price and lets it run;
|
|
27
|
+
* - this function returns priced usage (`dollars`) for a priced model, and
|
|
28
|
+
* unmetered usage (`dollars_unmetered: true`, no `dollars`) otherwise —
|
|
29
|
+
* never `undefined` for a successful decode, so token ceilings always see the
|
|
30
|
+
* step and the journal always says whether its dollars are known;
|
|
31
|
+
* - the kernel (`machine/budget.rs`) enforces tokens on every charge and
|
|
32
|
+
* dollars on metered charges only, and rejects unmetered usage that also
|
|
33
|
+
* claims non-zero dollars.
|
|
34
|
+
* `tests/budget-unmetered-live.test.ts` pins the chain end to end.
|
|
19
35
|
*/
|
|
20
|
-
export function workerSpend(result: WorkerCliResult, model?: string) {
|
|
36
|
+
export function workerSpend(result: WorkerCliResult, model?: string): { result: WorkerCliResult; usage: StepUsage | undefined } {
|
|
21
37
|
try {
|
|
22
38
|
const usage = pricedUsage(model, result.tokens_input, result.tokens_output)
|
|
23
39
|
?? unmeteredUsage(result.tokens_input, result.tokens_output);
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
2
|
-
import type { JournalClient } from '../journal-client.js';
|
|
3
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
4
|
-
export declare function deterministicFailureDetails(client: JournalClient, runId: string): Promise<StepFailedDetails | undefined>;
|
|
5
|
-
//# sourceMappingURL=deterministic-failure.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deterministic-failure.d.ts","sourceRoot":"","sources":["../../src/cli/deterministic-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,qFAAqF;AACrF,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAqCxC"}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const STDERR_TAIL_BYTES = 1_024;
|
|
2
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
3
|
-
export async function deterministicFailureDetails(client, runId) {
|
|
4
|
-
const snapshot = await client.runGet(runId);
|
|
5
|
-
if (!Object.values(snapshot.steps).some(step => step.type === 'deterministic'))
|
|
6
|
-
return undefined;
|
|
7
|
-
let fromSeq = 1;
|
|
8
|
-
const failures = new Map();
|
|
9
|
-
while (true) {
|
|
10
|
-
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
11
|
-
if (entries.length === 0)
|
|
12
|
-
break;
|
|
13
|
-
for (const raw of entries) {
|
|
14
|
-
const entry = record(raw);
|
|
15
|
-
const seq = entry?.['seq'];
|
|
16
|
-
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
|
|
17
|
-
throw new Error('invalid journal sequence in command failure inspection');
|
|
18
|
-
}
|
|
19
|
-
fromSeq = seq + 1;
|
|
20
|
-
const stepId = entry?.['step_id'];
|
|
21
|
-
if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string'
|
|
22
|
-
|| snapshot.steps[stepId]?.type !== 'deterministic')
|
|
23
|
-
continue;
|
|
24
|
-
// A later completion supersedes an earlier failed attempt.
|
|
25
|
-
failures.delete(stepId);
|
|
26
|
-
const payload = record(entry['payload']);
|
|
27
|
-
// Post-#292 the kernel preserves the captured `{exit_code, stdout_tail,
|
|
28
|
-
// stderr_tail}` in `output` on failed completions; prefer it. Fall back
|
|
29
|
-
// to `trajectory_tail` for journal records emitted before that fix.
|
|
30
|
-
const output = record(payload?.['output']) ?? record(payload?.['trajectory_tail']);
|
|
31
|
-
const exitCode = output?.['exit_code'];
|
|
32
|
-
if (payload?.['disposition'] !== 'step_done' || payload['completionReason'] === 'success'
|
|
33
|
-
|| typeof exitCode !== 'number' || !Number.isSafeInteger(exitCode) || exitCode === 0)
|
|
34
|
-
continue;
|
|
35
|
-
failures.set(stepId, {
|
|
36
|
-
stepId,
|
|
37
|
-
exitCode,
|
|
38
|
-
stderrTail: stderrTail(output?.['stderr_tail']),
|
|
39
|
-
hint: `flows replay ${shellQuote(runId)} --at ${shellQuote(stepId)}`,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return [...failures.values()].at(-1);
|
|
44
|
-
}
|
|
45
|
-
function record(value) {
|
|
46
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
47
|
-
? value : undefined;
|
|
48
|
-
}
|
|
49
|
-
function stderrTail(value) {
|
|
50
|
-
if (typeof value !== 'string')
|
|
51
|
-
return '';
|
|
52
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
53
|
-
let start = Math.max(0, bytes.length - STDERR_TAIL_BYTES);
|
|
54
|
-
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
55
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80)
|
|
56
|
-
start += 1;
|
|
57
|
-
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
58
|
-
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
59
|
-
return bytes.subarray(start).toString('utf8')
|
|
60
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
61
|
-
}
|
|
62
|
-
function shellQuote(value) {
|
|
63
|
-
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
64
|
-
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
65
|
-
}
|
|
66
|
-
//# sourceMappingURL=deterministic-failure.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deterministic-failure.js","sourceRoot":"","sources":["../../src/cli/deterministic-failure.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { StepFailedDetails } from '../failure-kinds.js';
|
|
2
|
-
import type { JournalClient } from '../journal-client.js';
|
|
3
|
-
|
|
4
|
-
const STDERR_TAIL_BYTES = 1_024;
|
|
5
|
-
|
|
6
|
-
/** Read completion evidence through the existing protocol, including later pages. */
|
|
7
|
-
export async function deterministicFailureDetails(
|
|
8
|
-
client: JournalClient,
|
|
9
|
-
runId: string,
|
|
10
|
-
): Promise<StepFailedDetails | undefined> {
|
|
11
|
-
const snapshot = await client.runGet(runId);
|
|
12
|
-
if (!Object.values(snapshot.steps).some(step => step.type === 'deterministic')) return undefined;
|
|
13
|
-
let fromSeq = 1;
|
|
14
|
-
const failures = new Map<string, StepFailedDetails>();
|
|
15
|
-
while (true) {
|
|
16
|
-
const { entries } = await client.journalRead(runId, fromSeq, 100);
|
|
17
|
-
if (entries.length === 0) break;
|
|
18
|
-
for (const raw of entries) {
|
|
19
|
-
const entry = record(raw);
|
|
20
|
-
const seq = entry?.['seq'];
|
|
21
|
-
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < fromSeq) {
|
|
22
|
-
throw new Error('invalid journal sequence in command failure inspection');
|
|
23
|
-
}
|
|
24
|
-
fromSeq = seq + 1;
|
|
25
|
-
const stepId = entry?.['step_id'];
|
|
26
|
-
if (entry?.['entry_type'] !== 'step.completed' || typeof stepId !== 'string'
|
|
27
|
-
|| snapshot.steps[stepId]?.type !== 'deterministic') continue;
|
|
28
|
-
// A later completion supersedes an earlier failed attempt.
|
|
29
|
-
failures.delete(stepId);
|
|
30
|
-
const payload = record(entry['payload']);
|
|
31
|
-
// Post-#292 the kernel preserves the captured `{exit_code, stdout_tail,
|
|
32
|
-
// stderr_tail}` in `output` on failed completions; prefer it. Fall back
|
|
33
|
-
// to `trajectory_tail` for journal records emitted before that fix.
|
|
34
|
-
const output = record(payload?.['output']) ?? record(payload?.['trajectory_tail']);
|
|
35
|
-
const exitCode = output?.['exit_code'];
|
|
36
|
-
if (payload?.['disposition'] !== 'step_done' || payload['completionReason'] === 'success'
|
|
37
|
-
|| typeof exitCode !== 'number' || !Number.isSafeInteger(exitCode) || exitCode === 0) continue;
|
|
38
|
-
failures.set(stepId, {
|
|
39
|
-
stepId,
|
|
40
|
-
exitCode,
|
|
41
|
-
stderrTail: stderrTail(output?.['stderr_tail']),
|
|
42
|
-
hint: `flows replay ${shellQuote(runId)} --at ${shellQuote(stepId)}`,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return [...failures.values()].at(-1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function record(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
-
? value as Record<string, unknown> : undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function stderrTail(value: unknown): string {
|
|
55
|
-
if (typeof value !== 'string') return '';
|
|
56
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
57
|
-
let start = Math.max(0, bytes.length - STDERR_TAIL_BYTES);
|
|
58
|
-
// Drop a partial leading code point, avoiding replacement-byte expansion.
|
|
59
|
-
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
60
|
-
// Preserve tabs/newlines; replace binary controls (including ESC and CR),
|
|
61
|
-
// C1 controls and Unicode formatting controls without growing the excerpt.
|
|
62
|
-
return bytes.subarray(start).toString('utf8')
|
|
63
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, character => character === '\n' || character === '\t' ? character : '?');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function shellQuote(value: string): string {
|
|
67
|
-
return /^[A-Za-z0-9_-]+$/.test(value) && !value.startsWith('-')
|
|
68
|
-
? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
69
|
-
}
|