@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.
Files changed (79) hide show
  1. package/dist/authored-budget.d.ts.map +1 -1
  2. package/dist/authored-budget.js +16 -7
  3. package/dist/authored-budget.js.map +1 -1
  4. package/dist/authored-flow-executor.d.ts.map +1 -1
  5. package/dist/authored-flow-executor.js +5 -0
  6. package/dist/authored-flow-executor.js.map +1 -1
  7. package/dist/budget-preflight.d.ts +17 -6
  8. package/dist/budget-preflight.d.ts.map +1 -1
  9. package/dist/budget-preflight.js +9 -6
  10. package/dist/budget-preflight.js.map +1 -1
  11. package/dist/cli/build.d.ts.map +1 -1
  12. package/dist/cli/build.js +8 -0
  13. package/dist/cli/build.js.map +1 -1
  14. package/dist/cli/deploy.d.ts.map +1 -1
  15. package/dist/cli/deploy.js +5 -0
  16. package/dist/cli/deploy.js.map +1 -1
  17. package/dist/cli/direct-run.d.ts.map +1 -1
  18. package/dist/cli/direct-run.js +8 -1
  19. package/dist/cli/direct-run.js.map +1 -1
  20. package/dist/cli/run.d.ts +23 -0
  21. package/dist/cli/run.d.ts.map +1 -1
  22. package/dist/cli/run.js +78 -11
  23. package/dist/cli/run.js.map +1 -1
  24. package/dist/cli/step-failure.d.ts +40 -0
  25. package/dist/cli/step-failure.d.ts.map +1 -0
  26. package/dist/cli/step-failure.js +150 -0
  27. package/dist/cli/step-failure.js.map +1 -0
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +4 -4
  30. package/dist/cli.js.map +1 -1
  31. package/dist/daemon-connection.d.ts +7 -0
  32. package/dist/daemon-connection.d.ts.map +1 -1
  33. package/dist/daemon-connection.js +7 -0
  34. package/dist/daemon-connection.js.map +1 -1
  35. package/dist/failure-kinds.d.ts +25 -3
  36. package/dist/failure-kinds.d.ts.map +1 -1
  37. package/dist/failure-kinds.js +5 -2
  38. package/dist/failure-kinds.js.map +1 -1
  39. package/dist/journal-client.d.ts +2 -6
  40. package/dist/journal-client.d.ts.map +1 -1
  41. package/dist/journal-client.js.map +1 -1
  42. package/dist/model-pricing.d.ts +7 -5
  43. package/dist/model-pricing.d.ts.map +1 -1
  44. package/dist/model-pricing.js +7 -5
  45. package/dist/model-pricing.js.map +1 -1
  46. package/dist/preflight.d.ts +5 -0
  47. package/dist/preflight.d.ts.map +1 -1
  48. package/dist/preflight.js.map +1 -1
  49. package/dist/protocol.d.ts +22 -5
  50. package/dist/protocol.d.ts.map +1 -1
  51. package/dist/spec.d.ts +22 -7
  52. package/dist/spec.d.ts.map +1 -1
  53. package/dist/worker-spend.d.ts +13 -9
  54. package/dist/worker-spend.d.ts.map +1 -1
  55. package/dist/worker-spend.js +22 -8
  56. package/dist/worker-spend.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/authored-budget.ts +35 -9
  59. package/src/authored-flow-executor.ts +5 -0
  60. package/src/budget-preflight.ts +17 -6
  61. package/src/cli/build.ts +7 -0
  62. package/src/cli/deploy.ts +4 -0
  63. package/src/cli/direct-run.ts +8 -0
  64. package/src/cli/run.ts +94 -11
  65. package/src/cli/step-failure.ts +160 -0
  66. package/src/cli.ts +4 -4
  67. package/src/daemon-connection.ts +8 -0
  68. package/src/failure-kinds.ts +25 -3
  69. package/src/journal-client.ts +2 -1
  70. package/src/model-pricing.ts +7 -5
  71. package/src/preflight.ts +5 -0
  72. package/src/protocol.ts +15 -2
  73. package/src/spec.ts +23 -1
  74. package/src/worker-spend.ts +25 -9
  75. package/dist/cli/deterministic-failure.d.ts +0 -5
  76. package/dist/cli/deterministic-failure.d.ts.map +0 -1
  77. package/dist/cli/deterministic-failure.js +0 -66
  78. package/dist/cli/deterministic-failure.js.map +0 -1
  79. 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}; skipping observer link`);
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; skipping observer link');
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}; skipping observer link`);
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}`);
@@ -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;
@@ -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). Pricing
66
- * is light enforcement: the step runs and simply contributes no dollars.
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
- /** Optional evidence on the existing step_failed diagnostic, not a new kind. */
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);
@@ -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?: { tokens_in: number; tokens_out: number; dollars: string };
370
+ usage?: StepUsage;
370
371
  started_pins?: Pins;
371
372
  end_pins?: Pins;
372
373
  effects?: EffectRef[];
@@ -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 callers should omit `usage`
18
- * from the journal payload rather than sending nulls that break the kernel
19
- * wire schema. An unpriced step is unmetered, not refused: it contributes no
20
- * dollars to a budget, and `budgetDiagnostics` warns about it at preflight.
21
- * Codex model ids need no entry here; Codex selects its own model.
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?: { tokens_in: number; tokens_out: number; dollars: string };
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?: { tokens_in: number; tokens_out: number; dollars: string; wallclock_ms: number; day?: number };
479
+ prior_spend?: KernelPriorSpend;
458
480
  max_tokens?: number;
459
481
  max_wallclock_ms?: number;
460
482
  window?: 'day';
@@ -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
- /** Zero-dollar usage for an unpriced step, only when the CLI reported tokens. */
5
- function unmeteredUsage(input: number | undefined, output: number | undefined) {
6
- if (input === undefined || output === undefined) return undefined;
7
- return { tokens_in: input, tokens_out: output, dollars: '0.000000' };
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
- * Unpriced models are NOT a failure here: the step journals zero dollars, so it
16
- * never trips a dollar budget, but its reported tokens still count toward any
17
- * token budget. Preflight (see `budgetDiagnostics`) warns that such a step is
18
- * unmetered for dollars.
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
- }