@relayflows/sdk 2.0.42 → 2.0.43
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/adapters/base.d.ts +12 -1
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +11 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +8 -4
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +15 -6
- package/dist/adapters/codex.js.map +1 -1
- package/dist/agent-resume.d.ts +8 -0
- package/dist/agent-resume.d.ts.map +1 -0
- package/dist/agent-resume.js +37 -0
- package/dist/agent-resume.js.map +1 -0
- package/dist/agent-transcript.d.ts.map +1 -1
- package/dist/agent-transcript.js +5 -3
- package/dist/agent-transcript.js.map +1 -1
- package/dist/authored-flow-error.d.ts +10 -2
- package/dist/authored-flow-error.d.ts.map +1 -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 +16 -2
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts +6 -0
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +18 -6
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-helper-effect.js +3 -2
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-human.d.ts +2 -0
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +4 -0
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-memory.d.ts +47 -2
- package/dist/authored-memory.d.ts.map +1 -1
- package/dist/authored-memory.js +394 -15
- package/dist/authored-memory.js.map +1 -1
- package/dist/authored-node-entry.js +6 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +6 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root-readback.d.ts +21 -0
- package/dist/authored-root-readback.d.ts.map +1 -0
- package/dist/authored-root-readback.js +90 -0
- package/dist/authored-root-readback.js.map +1 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +53 -74
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +2 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +1 -0
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +26 -4
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-helper-body.d.ts +2 -2
- package/dist/cli/check-helper-body.d.ts.map +1 -1
- package/dist/cli/check-helper-body.js +7 -2
- package/dist/cli/check-helper-body.js.map +1 -1
- package/dist/cli/check-helper-surface.d.ts +8 -0
- package/dist/cli/check-helper-surface.d.ts.map +1 -0
- package/dist/cli/check-helper-surface.js +37 -0
- package/dist/cli/check-helper-surface.js.map +1 -0
- package/dist/cli/check-triggers.d.ts +4 -3
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +7 -3
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +7 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +35 -8
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-answer.d.ts +9 -0
- package/dist/cli/cloud-answer.d.ts.map +1 -0
- package/dist/cli/cloud-answer.js +39 -0
- package/dist/cli/cloud-answer.js.map +1 -0
- package/dist/cli/cloud-connect-cli.d.ts +2 -1
- package/dist/cli/cloud-connect-cli.d.ts.map +1 -1
- package/dist/cli/cloud-connect-cli.js +10 -2
- package/dist/cli/cloud-connect-cli.js.map +1 -1
- package/dist/cli/cloud-human.d.ts +7 -0
- package/dist/cli/cloud-human.d.ts.map +1 -0
- package/dist/cli/cloud-human.js +22 -0
- package/dist/cli/cloud-human.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +2 -2
- package/dist/cli/cloud-live.d.ts.map +1 -1
- package/dist/cli/cloud-live.js +9 -2
- package/dist/cli/cloud-live.js.map +1 -1
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
- package/dist/cli/cloud-mirror-session.js +3 -2
- package/dist/cli/cloud-mirror-session.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +1 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +12 -0
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/detached-record.d.ts +22 -0
- package/dist/cli/detached-record.d.ts.map +1 -0
- package/dist/cli/detached-record.js +57 -0
- package/dist/cli/detached-record.js.map +1 -0
- package/dist/cli/detached-run.d.ts +28 -0
- package/dist/cli/detached-run.d.ts.map +1 -0
- package/dist/cli/detached-run.js +122 -0
- package/dist/cli/detached-run.js.map +1 -0
- package/dist/cli/diagnostic-fold.d.ts +19 -0
- package/dist/cli/diagnostic-fold.d.ts.map +1 -0
- package/dist/cli/diagnostic-fold.js +19 -0
- package/dist/cli/diagnostic-fold.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/eval.js +131 -0
- package/dist/cli/eval.js.map +1 -0
- package/dist/cli/journal-timeout.d.ts +12 -0
- package/dist/cli/journal-timeout.d.ts.map +1 -0
- package/dist/cli/journal-timeout.js +42 -0
- package/dist/cli/journal-timeout.js.map +1 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -1
- package/dist/cli/local-agent-remedy.js +5 -4
- package/dist/cli/local-agent-remedy.js.map +1 -1
- package/dist/cli/observer-session.d.ts +1 -0
- package/dist/cli/observer-session.d.ts.map +1 -1
- package/dist/cli/observer-session.js +8 -1
- package/dist/cli/observer-session.js.map +1 -1
- package/dist/cli/reuse.d.ts +8 -0
- package/dist/cli/reuse.d.ts.map +1 -1
- package/dist/cli/reuse.js +24 -0
- package/dist/cli/reuse.js.map +1 -1
- package/dist/cli/run.d.ts +8 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +72 -56
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/running-step.d.ts +9 -0
- package/dist/cli/running-step.d.ts.map +1 -0
- package/dist/cli/running-step.js +156 -0
- package/dist/cli/running-step.js.map +1 -0
- package/dist/cli-adapter.d.ts +3 -1
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +6 -2
- package/dist/cli-adapter.js.map +1 -1
- package/dist/cli-commands.d.ts +77 -11
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +41 -7
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-watch.d.ts +6 -2
- package/dist/cli-watch.d.ts.map +1 -1
- package/dist/cli-watch.js +7 -6
- package/dist/cli-watch.js.map +1 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +154 -50
- package/dist/cli.js.map +1 -1
- package/dist/cloud-answer.d.ts +35 -0
- package/dist/cloud-answer.d.ts.map +1 -0
- package/dist/cloud-answer.js +194 -0
- package/dist/cloud-answer.js.map +1 -0
- package/dist/cloud-auth-store.d.ts +46 -0
- package/dist/cloud-auth-store.d.ts.map +1 -0
- package/dist/cloud-auth-store.js +171 -0
- package/dist/cloud-auth-store.js.map +1 -0
- package/dist/cloud-http.d.ts +7 -16
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +59 -40
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-human-wait.d.ts +10 -0
- package/dist/cloud-human-wait.d.ts.map +1 -0
- package/dist/cloud-human-wait.js +17 -0
- package/dist/cloud-human-wait.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -1
- package/dist/cloud-mirror-transport.js +1 -0
- package/dist/cloud-mirror-transport.js.map +1 -1
- package/dist/cloud-run-record.d.ts +7 -2
- package/dist/cloud-run-record.d.ts.map +1 -1
- package/dist/cloud-run-record.js +13 -5
- package/dist/cloud-run-record.js.map +1 -1
- package/dist/cloud-run.d.ts +9 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +36 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +6 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +12 -2
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -3
- package/dist/compile.js.map +1 -1
- package/dist/daemon-spec-validation.d.ts +31 -0
- package/dist/daemon-spec-validation.d.ts.map +1 -0
- package/dist/daemon-spec-validation.js +115 -0
- package/dist/daemon-spec-validation.js.map +1 -0
- package/dist/failure-kinds.d.ts +6 -4
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +17 -1
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-eval-cloud.d.ts +25 -0
- package/dist/flow-eval-cloud.d.ts.map +1 -0
- package/dist/flow-eval-cloud.js +108 -0
- package/dist/flow-eval-cloud.js.map +1 -0
- package/dist/flow-eval-error.d.ts +7 -0
- package/dist/flow-eval-error.d.ts.map +1 -0
- package/dist/flow-eval-error.js +10 -0
- package/dist/flow-eval-error.js.map +1 -0
- package/dist/flow-eval-gate.d.ts +15 -0
- package/dist/flow-eval-gate.d.ts.map +1 -0
- package/dist/flow-eval-gate.js +102 -0
- package/dist/flow-eval-gate.js.map +1 -0
- package/dist/flow-eval-local.d.ts +54 -0
- package/dist/flow-eval-local.d.ts.map +1 -0
- package/dist/flow-eval-local.js +220 -0
- package/dist/flow-eval-local.js.map +1 -0
- package/dist/flow-eval-outcome.d.ts +22 -0
- package/dist/flow-eval-outcome.d.ts.map +1 -0
- package/dist/flow-eval-outcome.js +37 -0
- package/dist/flow-eval-outcome.js.map +1 -0
- package/dist/flow-eval-report.d.ts +106 -0
- package/dist/flow-eval-report.d.ts.map +1 -0
- package/dist/flow-eval-report.js +4 -0
- package/dist/flow-eval-report.js.map +1 -0
- package/dist/flow-eval-suite.d.ts +46 -0
- package/dist/flow-eval-suite.d.ts.map +1 -0
- package/dist/flow-eval-suite.js +144 -0
- package/dist/flow-eval-suite.js.map +1 -0
- package/dist/flow-eval-version.d.ts +59 -0
- package/dist/flow-eval-version.d.ts.map +1 -0
- package/dist/flow-eval-version.js +303 -0
- package/dist/flow-eval-version.js.map +1 -0
- package/dist/flow-eval.d.ts +41 -0
- package/dist/flow-eval.d.ts.map +1 -0
- package/dist/flow-eval.js +201 -0
- package/dist/flow-eval.js.map +1 -0
- package/dist/helper-operation-use.d.ts +18 -0
- package/dist/helper-operation-use.d.ts.map +1 -0
- package/dist/helper-operation-use.js +336 -0
- package/dist/helper-operation-use.js.map +1 -0
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +16 -2
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +33 -0
- package/dist/helper-reference.d.ts.map +1 -1
- package/dist/helper-reference.js +5 -5
- package/dist/helper-reference.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-budgeted-reads.d.ts +41 -0
- package/dist/journal-budgeted-reads.d.ts.map +1 -0
- package/dist/journal-budgeted-reads.js +142 -0
- package/dist/journal-budgeted-reads.js.map +1 -0
- package/dist/journal-client.d.ts +34 -29
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +45 -151
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-connection.d.ts +42 -0
- package/dist/journal-connection.d.ts.map +1 -0
- package/dist/journal-connection.js +209 -0
- package/dist/journal-connection.js.map +1 -0
- package/dist/journal-read-policy.d.ts +38 -0
- package/dist/journal-read-policy.d.ts.map +1 -0
- package/dist/journal-read-policy.js +126 -0
- package/dist/journal-read-policy.js.map +1 -0
- package/dist/named-gate-lowering.d.ts.map +1 -1
- package/dist/named-gate-lowering.js +2 -6
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/re2js-embedded.d.ts +3 -0
- package/dist/re2js-embedded.d.ts.map +1 -0
- package/dist/re2js-embedded.js +4 -0
- package/dist/re2js-embedded.js.map +1 -0
- package/dist/spec.d.ts +10 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +15 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +15 -5
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +11 -3
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +5 -3
- package/src/adapters/base.ts +16 -1
- package/src/adapters/claude.ts +8 -4
- package/src/adapters/codex.ts +15 -6
- package/src/agent-resume.ts +36 -0
- package/src/agent-transcript.ts +5 -3
- package/src/authored-flow-error.ts +9 -0
- package/src/authored-flow-executor.ts +18 -1
- package/src/authored-flow-loader.ts +27 -10
- package/src/authored-helper-effect.ts +3 -2
- package/src/authored-human.ts +5 -0
- package/src/authored-memory.ts +445 -15
- package/src/authored-node-entry.ts +6 -1
- package/src/authored-node-runner.ts +6 -3
- package/src/authored-root-readback.ts +100 -0
- package/src/authored-root.ts +58 -85
- package/src/authored-step-output.ts +3 -0
- package/src/authored-worker-step.ts +29 -4
- package/src/cli/check-helper-body.ts +9 -3
- package/src/cli/check-helper-surface.ts +43 -0
- package/src/cli/check-triggers.ts +9 -4
- package/src/cli/check.ts +37 -7
- package/src/cli/cloud-answer.ts +40 -0
- package/src/cli/cloud-connect-cli.ts +9 -2
- package/src/cli/cloud-human.ts +24 -0
- package/src/cli/cloud-live.ts +10 -5
- package/src/cli/cloud-mirror-session.ts +3 -2
- package/src/cli/cloud-run.ts +10 -1
- package/src/cli/detached-record.ts +71 -0
- package/src/cli/detached-run.ts +131 -0
- package/src/cli/diagnostic-fold.ts +28 -0
- package/src/cli/direct-run.ts +14 -1
- package/src/cli/eval.ts +141 -0
- package/src/cli/journal-timeout.ts +49 -0
- package/src/cli/local-agent-remedy.ts +6 -4
- package/src/cli/observer-session.ts +7 -1
- package/src/cli/reuse.ts +25 -0
- package/src/cli/run.ts +75 -67
- package/src/cli/running-step.ts +140 -0
- package/src/cli-adapter.ts +7 -1
- package/src/cli-commands.ts +43 -7
- package/src/cli-watch.ts +8 -5
- package/src/cli.ts +158 -51
- package/src/cloud-answer.ts +202 -0
- package/src/cloud-auth-store.ts +185 -0
- package/src/cloud-http.ts +61 -50
- package/src/cloud-human-wait.ts +23 -0
- package/src/cloud-mirror-transport.ts +1 -0
- package/src/cloud-run-record.ts +14 -5
- package/src/cloud-run.ts +46 -2
- package/src/communication/local.ts +6 -1
- package/src/communication/worker.ts +11 -3
- package/src/compile.ts +5 -3
- package/src/daemon-spec-validation.ts +126 -0
- package/src/failure-kinds.ts +17 -1
- package/src/flow-eval-cloud.ts +114 -0
- package/src/flow-eval-error.ts +10 -0
- package/src/flow-eval-gate.ts +114 -0
- package/src/flow-eval-local.ts +236 -0
- package/src/flow-eval-outcome.ts +43 -0
- package/src/flow-eval-report.ts +102 -0
- package/src/flow-eval-suite.ts +169 -0
- package/src/flow-eval-version.ts +316 -0
- package/src/flow-eval.ts +234 -0
- package/src/helper-operation-use.ts +285 -0
- package/src/helper-preflight.ts +17 -2
- package/src/helper-reference.ts +6 -6
- package/src/index.ts +15 -1
- package/src/journal-budgeted-reads.ts +134 -0
- package/src/journal-client.ts +59 -166
- package/src/journal-connection.ts +222 -0
- package/src/journal-read-policy.ts +123 -0
- package/src/named-gate-lowering.ts +2 -6
- package/src/re2js-embedded.ts +3 -0
- package/src/spec.ts +10 -0
- package/src/step-fields.ts +1 -1
- package/src/validate.ts +14 -0
- package/src/worker-cli.ts +14 -3
- package/src/worker-lease.ts +11 -3
- package/src/worker.ts +1 -1
package/src/journal-client.ts
CHANGED
|
@@ -7,26 +7,36 @@
|
|
|
7
7
|
// AGENTS.md rule 4). The kernel binary (`kernel/relayflowd serve`) speaks
|
|
8
8
|
// this transport, including out-of-band leases, watches, events, and durable
|
|
9
9
|
// stream plumbing. The client's framing and failure behavior is covered by a
|
|
10
|
-
// loopback double in tests.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// loopback double in tests. Framing and the socket lifecycle live in
|
|
11
|
+
// journal-connection.ts; budgeted reads in journal-budgeted-reads.ts.
|
|
12
|
+
|
|
13
|
+
import { READ_ONLY_VERBS } from './journal-read-policy.js';
|
|
14
|
+
import { JournalConnection, JournalProtocolError } from './journal-connection.js';
|
|
15
|
+
export { JournalFrameError, JournalProtocolError } from './journal-connection.js';
|
|
16
|
+
import { BudgetedReads } from './journal-budgeted-reads.js';
|
|
17
|
+
export { JournalReadInterruptedError, JournalRequestTimeoutError } from './journal-read-policy.js';
|
|
13
18
|
export { walkJournal, JournalReadError, type JournalEvent, type JournalReadFailure } from './journal-reader.js';
|
|
14
|
-
import { randomUUID } from 'node:crypto';
|
|
15
|
-
import { createConnection, type Socket } from 'node:net';
|
|
16
19
|
import type { VerbContract, EventEmitParams, EventSubmitParams, ReportedCost } from './protocol.js';
|
|
17
20
|
import {
|
|
18
21
|
PROTOCOL_VERSION,
|
|
19
22
|
type CompletionReason,
|
|
20
23
|
type EffectRef,
|
|
21
24
|
type Pins,
|
|
22
|
-
type Request,
|
|
23
|
-
type Response,
|
|
24
|
-
type ServerEvent,
|
|
25
25
|
type StepUsage,
|
|
26
26
|
} from './protocol.js';
|
|
27
27
|
import type { KernelRunSpec, StepType } from './spec.js';
|
|
28
28
|
|
|
29
29
|
export interface JournalClientOptions {
|
|
30
|
+
/** Opt-in total budget for read-only requests; interactive clients stay single-shot. */
|
|
31
|
+
readBudgetMs?: number;
|
|
32
|
+
/** Cancels every budgeted read, so a run's lifecycle abort never waits out the budget. */
|
|
33
|
+
readSignal?: AbortSignal;
|
|
34
|
+
/**
|
|
35
|
+
* Retry this session's own `hello` within the read budget. Only for sessions
|
|
36
|
+
* opened after a run is admitted (an authored child, its verifier); a
|
|
37
|
+
* top-level command's handshake stays a single-shot compatibility check.
|
|
38
|
+
*/
|
|
39
|
+
budgetHandshake?: boolean;
|
|
30
40
|
/** Override the timeout for bounded protocol requests (ms). Default 30000. */
|
|
31
41
|
requestTimeoutMs?: number;
|
|
32
42
|
/**
|
|
@@ -42,42 +52,21 @@ export interface JournalClientOptions {
|
|
|
42
52
|
connectTimeoutMs?: number;
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/** A structured rejection returned by relayflowd over the journal protocol. */
|
|
52
|
-
export class JournalProtocolError extends Error {
|
|
53
|
-
readonly code: string;
|
|
54
|
-
verb?: string;
|
|
55
|
+
export class JournalClient extends JournalConnection {
|
|
56
|
+
private readonly budgeted: BudgetedReads | undefined;
|
|
57
|
+
private readonly budgetHandshake: boolean;
|
|
58
|
+
/** Additive request capabilities the daemon advertised at `hello`; none until then. */
|
|
59
|
+
private features: ReadonlySet<string> = new Set();
|
|
55
60
|
|
|
56
|
-
constructor(
|
|
57
|
-
super(
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
61
|
+
constructor(socketPath: string, options: JournalClientOptions = {}) {
|
|
62
|
+
super(socketPath, options.requestTimeoutMs ?? 30_000, options.connectTimeoutMs ?? 2_000);
|
|
63
|
+
this.budgetHandshake = options.budgetHandshake === true;
|
|
64
|
+
this.budgeted = options.readBudgetMs === undefined ? undefined
|
|
65
|
+
: new BudgetedReads(this, options.readBudgetMs, options.readSignal);
|
|
60
66
|
}
|
|
61
|
-
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
private features: ReadonlySet<string> = new Set();
|
|
66
|
-
private socket: Socket | null = null;
|
|
67
|
-
/** Why the connection ended, so a later "not connected" names its cause rather than hiding it. */
|
|
68
|
-
private disconnectCause: Error | undefined;
|
|
69
|
-
private buffer = '';
|
|
70
|
-
private readonly pending = new Map<string, Pending>();
|
|
71
|
-
private readonly requestTimeoutMs: number;
|
|
72
|
-
private readonly connectTimeoutMs: number;
|
|
73
|
-
|
|
74
|
-
constructor(
|
|
75
|
-
readonly socketPath: string,
|
|
76
|
-
options: JournalClientOptions = {},
|
|
77
|
-
) {
|
|
78
|
-
super();
|
|
79
|
-
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
80
|
-
this.connectTimeoutMs = options.connectTimeoutMs ?? 2_000;
|
|
68
|
+
protected override connectionClosed(cause: unknown, unexpected: boolean): void {
|
|
69
|
+
this.budgeted?.close(cause, unexpected);
|
|
81
70
|
}
|
|
82
71
|
|
|
83
72
|
/** Independent session for an SDK helper worker; preserves the caller's registration. */
|
|
@@ -87,139 +76,41 @@ export class JournalClient extends EventEmitter {
|
|
|
87
76
|
});
|
|
88
77
|
}
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.failAll(new Error(`journal client: connect timed out after ${this.connectTimeoutMs}ms`));
|
|
99
|
-
reject(new Error(`journal client: connect timed out after ${this.connectTimeoutMs}ms`));
|
|
100
|
-
}, this.connectTimeoutMs);
|
|
101
|
-
const onError = (err: Error): void => {
|
|
102
|
-
clearTimeout(timer);
|
|
103
|
-
socket.removeAllListeners();
|
|
104
|
-
this.failAll(err);
|
|
105
|
-
reject(new Error(`journal client: connect failed: ${err.message}`));
|
|
106
|
-
};
|
|
107
|
-
socket.once('error', onError);
|
|
108
|
-
socket.once('connect', () => {
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
socket.removeListener('error', onError);
|
|
111
|
-
socket.on('error', (err) => { this.disconnectCause ??= err; this.failAll(err); });
|
|
112
|
-
socket.on('data', (chunk) => this.onData(chunk));
|
|
113
|
-
socket.on('close', () => {
|
|
114
|
-
const closed = new Error('journal client: connection closed');
|
|
115
|
-
this.disconnectCause ??= closed;
|
|
116
|
-
this.failAll(closed);
|
|
117
|
-
});
|
|
118
|
-
this.disconnectCause = undefined;
|
|
119
|
-
this.socket = socket;
|
|
120
|
-
resolve();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
79
|
+
private request<V extends keyof VerbContract>(verb: V, params: VerbContract[V]['params'],
|
|
80
|
+
timeoutMs: number | null = this.requestTimeoutMs, signal?: AbortSignal): Promise<VerbContract[V]['result']> {
|
|
81
|
+
// Every allowlisted read takes the policy, even after the primary dropped: its
|
|
82
|
+
// closed state then answers with the typed interruption recorded at the drop.
|
|
83
|
+
if (this.budgeted !== undefined && timeoutMs !== null && READ_ONLY_VERBS.has(verb)) {
|
|
84
|
+
return this.budgeted.read(verb, params, timeoutMs, signal);
|
|
85
|
+
}
|
|
86
|
+
return this.requestOnce(verb, params, timeoutMs, signal);
|
|
123
87
|
}
|
|
124
88
|
|
|
125
89
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* later "not connected" rejection.
|
|
90
|
+
* Cancel budgeted reads also when `signal` aborts — a root attempt's lease —
|
|
91
|
+
* until the returned release is called. A no-op without a read budget.
|
|
129
92
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
? new Error('journal client: closed by caller')
|
|
133
|
-
: new Error(`journal client: closed after ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
134
|
-
this.disconnectCause ??= closed;
|
|
135
|
-
this.failAll(closed);
|
|
136
|
-
this.socket?.destroy();
|
|
137
|
-
this.socket = null;
|
|
138
|
-
this.buffer = '';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private onData(chunk: Buffer): void {
|
|
142
|
-
this.buffer += chunk.toString('utf8');
|
|
143
|
-
let nl: number;
|
|
144
|
-
while ((nl = this.buffer.indexOf('\n')) !== -1) {
|
|
145
|
-
const line = this.buffer.slice(0, nl);
|
|
146
|
-
this.buffer = this.buffer.slice(nl + 1);
|
|
147
|
-
if (line.length > 0) this.onLine(line);
|
|
148
|
-
}
|
|
93
|
+
scopeReads(signal: AbortSignal): () => void {
|
|
94
|
+
return this.budgeted?.scope(signal) ?? (() => {});
|
|
149
95
|
}
|
|
150
96
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (typeof (msg as Response).id === 'string' && 'ok' in (msg as Response)) {
|
|
162
|
-
const res = msg as Response;
|
|
163
|
-
const pending = this.pending.get(res.id);
|
|
164
|
-
if (!pending) return; // reply for an already-timed-out request
|
|
165
|
-
this.pending.delete(res.id);
|
|
166
|
-
if (pending.timer !== undefined) clearTimeout(pending.timer);
|
|
167
|
-
if (res.ok) pending.resolve(res.result);
|
|
168
|
-
else pending.reject(new JournalProtocolError(res.error.code, res.error.message));
|
|
169
|
-
} else {
|
|
170
|
-
const ev = msg as ServerEvent;
|
|
171
|
-
this.emit(ev.event, ev.data);
|
|
172
|
-
this.emit('event', ev);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private failAll(err: Error): void {
|
|
177
|
-
for (const [, p] of this.pending) {
|
|
178
|
-
if (p.timer !== undefined) clearTimeout(p.timer);
|
|
179
|
-
p.reject(err);
|
|
180
|
-
}
|
|
181
|
-
this.pending.clear();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private request<V extends keyof VerbContract>(
|
|
185
|
-
verb: V,
|
|
186
|
-
params: VerbContract[V]['params'],
|
|
187
|
-
timeoutMs: number | null = this.requestTimeoutMs,
|
|
188
|
-
): Promise<VerbContract[V]['result']> {
|
|
189
|
-
return new Promise((resolve, reject) => {
|
|
190
|
-
if (!this.socket || this.socket.destroyed) {
|
|
191
|
-
const cause = this.disconnectCause;
|
|
192
|
-
reject(new Error(
|
|
193
|
-
`journal client: not connected (${verb})${cause === undefined ? '' : `: ${cause.message}`}`,
|
|
194
|
-
cause === undefined ? undefined : { cause },
|
|
195
|
-
));
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const id = randomUUID();
|
|
199
|
-
const frame: Request = { id, verb: verb as string, params };
|
|
200
|
-
const timer = timeoutMs === null ? undefined : setTimeout(() => {
|
|
201
|
-
this.pending.delete(id);
|
|
202
|
-
reject(new Error(`journal client: ${verb} timed out after ${timeoutMs}ms`));
|
|
203
|
-
}, timeoutMs);
|
|
204
|
-
this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });
|
|
205
|
-
this.socket.write(JSON.stringify(frame) + '\n', (err) => {
|
|
206
|
-
if (err) {
|
|
207
|
-
const p = this.pending.get(id);
|
|
208
|
-
if (p) {
|
|
209
|
-
if (p.timer !== undefined) clearTimeout(p.timer);
|
|
210
|
-
this.pending.delete(id);
|
|
211
|
-
p.reject(new Error(`journal client: ${verb} write failed: ${err.message}`));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
97
|
+
/**
|
|
98
|
+
* End every budgeted read now and refuse later ones; the session itself stays
|
|
99
|
+
* open. The reads end as read interruptions, so a worker hands its attempt
|
|
100
|
+
* back to the kernel rather than completing it as a failure.
|
|
101
|
+
*/
|
|
102
|
+
cancelReads(): void {
|
|
103
|
+
this.budgeted?.close(new Error('journal client: reads canceled'), true);
|
|
216
104
|
}
|
|
217
105
|
|
|
218
106
|
// --- Typed verb methods (gate 1 minimal set, kernel DESIGN.md §5) --------
|
|
219
107
|
|
|
220
108
|
/** Handshake; version mismatch is a hard error. Records the daemon's additive features. */
|
|
221
109
|
async hello(client: string): Promise<VerbContract['hello']['result']> {
|
|
222
|
-
const
|
|
110
|
+
const params = { protocol: PROTOCOL_VERSION, client };
|
|
111
|
+
// A flow client's handshake meets the same CPU load as its reads.
|
|
112
|
+
const result = this.budgeted === undefined || !this.budgetHandshake ? await this.request('hello', params)
|
|
113
|
+
: await this.budgeted.handshake(params, this.requestTimeoutMs);
|
|
223
114
|
this.features = new Set(Array.isArray(result.features) ? result.features.filter(f => typeof f === 'string') : []);
|
|
224
115
|
return result;
|
|
225
116
|
}
|
|
@@ -255,13 +146,15 @@ export class JournalClient extends EventEmitter {
|
|
|
255
146
|
}
|
|
256
147
|
|
|
257
148
|
/** Snapshot for legibility. */
|
|
258
|
-
runGet(runId: string): Promise<VerbContract['run.get']['result']> {
|
|
259
|
-
return this.request('run.get', { run_id: runId });
|
|
149
|
+
runGet(runId: string, options: { signal?: AbortSignal } = {}): Promise<VerbContract['run.get']['result']> {
|
|
150
|
+
return this.request('run.get', { run_id: runId }, this.requestTimeoutMs, options.signal);
|
|
260
151
|
}
|
|
261
152
|
|
|
262
153
|
/**
|
|
263
|
-
* Open a push stream of every appended entry.
|
|
264
|
-
* entries
|
|
154
|
+
* Open a push stream of every appended entry. The daemon writes the run's
|
|
155
|
+
* existing entries before this resolves (server.rs `watch_with_replay`), so
|
|
156
|
+
* the replay is fully delivered first; later entries arrive as they are
|
|
157
|
+
* appended. Entries arrive as `'entry'` events: `client.on('entry', (entry) => …)`.
|
|
265
158
|
*/
|
|
266
159
|
runWatch(runId: string): Promise<VerbContract['run.watch']['result']> {
|
|
267
160
|
return this.request('run.watch', { run_id: runId });
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Journal-protocol v0 client (kernel DESIGN.md §5).
|
|
2
|
+
//
|
|
3
|
+
// A real client implementation of the wire protocol — newline-delimited JSON
|
|
4
|
+
// over a unix socket. It correlates requests by `id`, demultiplexes
|
|
5
|
+
// server-pushed events, and is fail-closed: a connection drop or write error
|
|
6
|
+
// rejects every pending request (a journal write that fails fails the step;
|
|
7
|
+
// AGENTS.md rule 4). The kernel binary (`kernel/relayflowd serve`) speaks
|
|
8
|
+
// this transport, including out-of-band leases, watches, events, and durable
|
|
9
|
+
// stream plumbing. The client's framing and failure behavior is covered by a
|
|
10
|
+
// loopback double in tests.
|
|
11
|
+
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { createConnection, type Socket } from 'node:net';
|
|
15
|
+
import { JournalRequestTimeoutError } from './journal-read-policy.js';
|
|
16
|
+
import type { Request, Response, ServerEvent, VerbContract } from './protocol.js';
|
|
17
|
+
|
|
18
|
+
interface Pending {
|
|
19
|
+
resolve: (value: unknown) => void;
|
|
20
|
+
reject: (err: Error) => void;
|
|
21
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A structured rejection returned by relayflowd over the journal protocol. */
|
|
25
|
+
export class JournalProtocolError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
verb?: string;
|
|
28
|
+
|
|
29
|
+
constructor(code: string, message: string) {
|
|
30
|
+
super(`${code}: ${message}`);
|
|
31
|
+
this.name = 'JournalProtocolError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The daemon sent a frame this client cannot parse: a protocol violation, never retried. */
|
|
37
|
+
export class JournalFrameError extends Error {
|
|
38
|
+
constructor(message = 'journal client: malformed frame from server') {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'JournalFrameError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Framing, socket lifecycle and request correlation; the typed verbs live on JournalClient. */
|
|
45
|
+
export class JournalConnection extends EventEmitter {
|
|
46
|
+
protected socket: Socket | null = null;
|
|
47
|
+
private connectingSocket: Socket | undefined;
|
|
48
|
+
/** Why the connection ended, so a later "not connected" names its cause rather than hiding it. */
|
|
49
|
+
protected disconnectCause: Error | undefined;
|
|
50
|
+
private buffer = '';
|
|
51
|
+
private readonly pending = new Map<string, Pending>();
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
readonly socketPath: string,
|
|
55
|
+
protected readonly requestTimeoutMs: number,
|
|
56
|
+
protected readonly connectTimeoutMs: number,
|
|
57
|
+
) {
|
|
58
|
+
super();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Hook for sessions layered on this one: `unexpected` is a drop the caller did not ask for. */
|
|
62
|
+
protected connectionClosed(_cause: unknown, _unexpected: boolean): void {}
|
|
63
|
+
|
|
64
|
+
/** Open the unix socket connection. Rejects on connect failure (fail-closed). */
|
|
65
|
+
connect(): Promise<void> {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
if (this.socket) return resolve();
|
|
68
|
+
const socket = createConnection({ path: this.socketPath });
|
|
69
|
+
this.connectingSocket = socket;
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
socket.removeAllListeners();
|
|
72
|
+
socket.destroy();
|
|
73
|
+
this.failAll(new Error(`journal client: connect timed out after ${this.connectTimeoutMs}ms`));
|
|
74
|
+
reject(new Error(`journal client: connect timed out after ${this.connectTimeoutMs}ms`));
|
|
75
|
+
}, this.connectTimeoutMs);
|
|
76
|
+
const onError = (err: Error): void => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
socket.removeAllListeners();
|
|
79
|
+
this.failAll(err);
|
|
80
|
+
reject(new Error(`journal client: connect failed: ${err.message}`));
|
|
81
|
+
};
|
|
82
|
+
socket.once('error', onError);
|
|
83
|
+
socket.once('connect', () => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
socket.removeListener('error', onError);
|
|
86
|
+
socket.on('error', (err) => { this.disconnectCause ??= err; this.failAll(err); });
|
|
87
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
88
|
+
socket.on('close', () => {
|
|
89
|
+
const closed = new Error('journal client: connection closed');
|
|
90
|
+
this.connectionClosed(closed, this.socket === socket);
|
|
91
|
+
this.disconnectCause ??= closed;
|
|
92
|
+
this.failAll(closed);
|
|
93
|
+
// Only a drop the caller did not ask for; close() clears this.socket first.
|
|
94
|
+
// Sessions with nothing pending (a registered watch) must hear it too.
|
|
95
|
+
if (this.socket === socket) this.emit('disconnected', closed);
|
|
96
|
+
});
|
|
97
|
+
this.disconnectCause = undefined;
|
|
98
|
+
this.connectingSocket = undefined;
|
|
99
|
+
this.socket = socket;
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Close the connection and reject any pending requests. A `cause` (for
|
|
107
|
+
* example the worker failure that forced the close) is carried into every
|
|
108
|
+
* later "not connected" rejection.
|
|
109
|
+
*/
|
|
110
|
+
close(cause?: unknown): void {
|
|
111
|
+
const closed = cause === undefined
|
|
112
|
+
? new Error('journal client: closed by caller')
|
|
113
|
+
: new Error(`journal client: closed after ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
114
|
+
this.connectionClosed(cause, false);
|
|
115
|
+
this.connectingSocket?.destroy(closed);
|
|
116
|
+
this.disconnectCause ??= closed;
|
|
117
|
+
this.failAll(closed);
|
|
118
|
+
this.socket?.destroy();
|
|
119
|
+
this.socket = null;
|
|
120
|
+
this.buffer = '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private onData(chunk: Buffer): void {
|
|
124
|
+
this.buffer += chunk.toString('utf8');
|
|
125
|
+
let nl: number;
|
|
126
|
+
while ((nl = this.buffer.indexOf('\n')) !== -1) {
|
|
127
|
+
const line = this.buffer.slice(0, nl);
|
|
128
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
129
|
+
if (line.length > 0) this.onLine(line);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private onLine(line: string): void {
|
|
134
|
+
let msg: Response | ServerEvent;
|
|
135
|
+
try {
|
|
136
|
+
msg = JSON.parse(line) as Response | ServerEvent;
|
|
137
|
+
} catch {
|
|
138
|
+
// A malformed frame is a protocol violation; fail closed.
|
|
139
|
+
const error = new JournalFrameError();
|
|
140
|
+
this.failAll(error);
|
|
141
|
+
// Sessions with nothing pending (a registered watch) must hear it too.
|
|
142
|
+
this.emit('protocol_error', error);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof (msg as Response).id === 'string' && 'ok' in (msg as Response)) {
|
|
147
|
+
const res = msg as Response;
|
|
148
|
+
const pending = this.pending.get(res.id);
|
|
149
|
+
if (!pending) return; // reply for an already-timed-out request
|
|
150
|
+
this.pending.delete(res.id);
|
|
151
|
+
if (pending.timer !== undefined) clearTimeout(pending.timer);
|
|
152
|
+
if (res.ok) pending.resolve(res.result);
|
|
153
|
+
else pending.reject(new JournalProtocolError(res.error.code, res.error.message));
|
|
154
|
+
} else {
|
|
155
|
+
const ev = msg as ServerEvent;
|
|
156
|
+
this.emit(ev.event, ev.data);
|
|
157
|
+
this.emit('event', ev);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private failAll(err: Error): void {
|
|
162
|
+
for (const [, p] of this.pending) {
|
|
163
|
+
if (p.timer !== undefined) clearTimeout(p.timer);
|
|
164
|
+
p.reject(err);
|
|
165
|
+
}
|
|
166
|
+
this.pending.clear();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @internal One unretried request on this session; flow reads go through `request`. */
|
|
170
|
+
requestOnce<V extends keyof VerbContract>(
|
|
171
|
+
verb: V,
|
|
172
|
+
params: VerbContract[V]['params'],
|
|
173
|
+
timeoutMs: number | null = this.requestTimeoutMs,
|
|
174
|
+
signal?: AbortSignal,
|
|
175
|
+
): Promise<VerbContract[V]['result']> {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
reject(signal.reason);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!this.socket || this.socket.destroyed) {
|
|
182
|
+
const cause = this.disconnectCause;
|
|
183
|
+
reject(new Error(
|
|
184
|
+
`journal client: not connected (${verb})${cause === undefined ? '' : `: ${cause.message}`}`,
|
|
185
|
+
cause === undefined ? undefined : { cause },
|
|
186
|
+
));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const id = randomUUID();
|
|
190
|
+
const started = performance.now();
|
|
191
|
+
const frame: Request = { id, verb: verb as string, params };
|
|
192
|
+
// Cancellation forgets the request; a late reply is ignored like a timed-out one.
|
|
193
|
+
const onAbort = () => {
|
|
194
|
+
const p = this.pending.get(id);
|
|
195
|
+
if (p === undefined) return;
|
|
196
|
+
if (p.timer !== undefined) clearTimeout(p.timer);
|
|
197
|
+
this.pending.delete(id);
|
|
198
|
+
p.reject(signal!.reason);
|
|
199
|
+
};
|
|
200
|
+
const settle = <A>(fn: (arg: A) => void) => (arg: A) => {
|
|
201
|
+
signal?.removeEventListener('abort', onAbort);
|
|
202
|
+
fn(arg);
|
|
203
|
+
};
|
|
204
|
+
const timer = timeoutMs === null ? undefined : setTimeout(() => {
|
|
205
|
+
this.pending.delete(id);
|
|
206
|
+
settle(reject)(new JournalRequestTimeoutError(verb, timeoutMs, 1, performance.now() - started));
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
this.pending.set(id, { resolve: settle(resolve as (v: unknown) => void), reject: settle(reject), timer });
|
|
209
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
210
|
+
this.socket.write(JSON.stringify(frame) + '\n', (err) => {
|
|
211
|
+
if (err) {
|
|
212
|
+
const p = this.pending.get(id);
|
|
213
|
+
if (p) {
|
|
214
|
+
if (p.timer !== undefined) clearTimeout(p.timer);
|
|
215
|
+
this.pending.delete(id);
|
|
216
|
+
p.reject(new Error(`journal client: ${verb} write failed: ${err.message}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { setMaxListeners } from 'node:events';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
|
|
4
|
+
export const READ_ONLY_VERBS = new Set(['run.get', 'journal.read', 'stream.read', 'subscription.inspect']);
|
|
5
|
+
export const FLOW_READ_BUDGET_MS = 300_000;
|
|
6
|
+
|
|
7
|
+
export class JournalRequestTimeoutError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
readonly verb: string,
|
|
10
|
+
readonly timeoutMs: number,
|
|
11
|
+
readonly attempts = 1,
|
|
12
|
+
readonly elapsedMs = timeoutMs,
|
|
13
|
+
readonly readBudgetMs?: number,
|
|
14
|
+
) {
|
|
15
|
+
super(readBudgetMs === undefined
|
|
16
|
+
? `journal client: ${verb} timed out after ${timeoutMs}ms`
|
|
17
|
+
: `journal client: ${verb} timed out after ${attempts} attempts in ${Math.round(elapsedMs)}ms (read budget ${readBudgetMs}ms); relayflowd may be delayed by CPU load`);
|
|
18
|
+
this.name = 'JournalRequestTimeoutError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The dedicated read session lost its transport (closed, reset, not connected)
|
|
24
|
+
* while the primary session may still be healthy. Like a timeout, this says
|
|
25
|
+
* nothing about the run itself: retry within the budget, then leave the run
|
|
26
|
+
* resumable. Protocol refusals and write failures are never this error.
|
|
27
|
+
*/
|
|
28
|
+
export class JournalReadInterruptedError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
readonly verb: string,
|
|
31
|
+
readonly attempts = 1,
|
|
32
|
+
readonly elapsedMs = 0,
|
|
33
|
+
readonly readBudgetMs?: number,
|
|
34
|
+
options?: ErrorOptions & { retryable?: boolean },
|
|
35
|
+
) {
|
|
36
|
+
const cause = options?.cause instanceof Error ? `: ${options.cause.message}` : '';
|
|
37
|
+
super(readBudgetMs === undefined
|
|
38
|
+
? `journal client: ${verb} read session was interrupted${cause}`
|
|
39
|
+
: `journal client: ${verb} read session was interrupted after ${attempts} attempts in ${Math.round(elapsedMs)}ms (read budget ${readBudgetMs}ms)${cause}`,
|
|
40
|
+
options);
|
|
41
|
+
this.name = 'JournalReadInterruptedError';
|
|
42
|
+
this.retryable = options?.retryable !== false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** False when the session it ran on is gone, so retrying it cannot help. */
|
|
46
|
+
readonly retryable: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A read that could not be answered — by timeout or lost read transport — not a run failure. */
|
|
50
|
+
export function isReadInterruptionError(error: unknown): error is JournalRequestTimeoutError | JournalReadInterruptedError {
|
|
51
|
+
return (error instanceof JournalRequestTimeoutError
|
|
52
|
+
&& (READ_ONLY_VERBS.has(error.verb) || error.verb === 'run.watch' || error.verb === 'hello'))
|
|
53
|
+
|| error instanceof JournalReadInterruptedError;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Serial admission bounds read amplification, including retries, per body client. */
|
|
57
|
+
export class JournalReadPolicy {
|
|
58
|
+
private tail: Promise<unknown> = Promise.resolve();
|
|
59
|
+
private readonly closed = new AbortController();
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
// Concurrent children share this cancellation signal; each waiter removes its listener.
|
|
63
|
+
setMaxListeners(0, this.closed.signal);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
close(reason: unknown = new Error('journal client: closed by caller')): void { this.closed.abort(reason); }
|
|
67
|
+
|
|
68
|
+
async read<T>(verb: string, timeoutMs: number, budgetMs: number,
|
|
69
|
+
request: (timeoutMs: number, signal: AbortSignal) => Promise<T>, caller?: AbortSignal): Promise<T> {
|
|
70
|
+
const started = performance.now();
|
|
71
|
+
const deadline = started + budgetMs;
|
|
72
|
+
let attempts = 0;
|
|
73
|
+
let last: unknown;
|
|
74
|
+
const exhausted = () => last instanceof JournalReadInterruptedError
|
|
75
|
+
? new JournalReadInterruptedError(verb, attempts, performance.now() - started, budgetMs, { cause: last.cause })
|
|
76
|
+
: new JournalRequestTimeoutError(verb, timeoutMs, attempts, performance.now() - started, budgetMs);
|
|
77
|
+
// A caller's cancellation settles this read and drains its queued attempt.
|
|
78
|
+
const signal = caller === undefined ? this.closed.signal : AbortSignal.any([this.closed.signal, caller]);
|
|
79
|
+
// Budget exhaustion also aborts the queued work, so a stalled setup or
|
|
80
|
+
// request does not hold the serialized queue after its caller gave up.
|
|
81
|
+
const budget = new AbortController();
|
|
82
|
+
const attemptSignal = AbortSignal.any([signal, budget.signal]);
|
|
83
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
84
|
+
let onClose: () => void = () => {};
|
|
85
|
+
let deliver: (value: T) => void = () => {};
|
|
86
|
+
let fail: (error: unknown) => void = () => {};
|
|
87
|
+
const response = new Promise<T>((resolve, reject) => {
|
|
88
|
+
deliver = resolve;
|
|
89
|
+
fail = reject;
|
|
90
|
+
timer = setTimeout(() => { const error = exhausted(); budget.abort(error); reject(error); }, budgetMs);
|
|
91
|
+
onClose = () => reject(signal.reason);
|
|
92
|
+
signal.addEventListener('abort', onClose, { once: true });
|
|
93
|
+
if (signal.aborted) onClose();
|
|
94
|
+
});
|
|
95
|
+
const job = this.tail.then(async () => {
|
|
96
|
+
while (true) {
|
|
97
|
+
attemptSignal.throwIfAborted();
|
|
98
|
+
const remaining = deadline - performance.now();
|
|
99
|
+
if (remaining <= 0) throw exhausted();
|
|
100
|
+
attempts += 1;
|
|
101
|
+
try {
|
|
102
|
+
return await request(Math.min(remaining, timeoutMs * 2 ** Math.max(0, attempts - 2)), attemptSignal);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
attemptSignal.throwIfAborted();
|
|
105
|
+
if (!(error instanceof JournalRequestTimeoutError
|
|
106
|
+
|| (error instanceof JournalReadInterruptedError && error.retryable))) throw error;
|
|
107
|
+
last = error;
|
|
108
|
+
const left = deadline - performance.now();
|
|
109
|
+
if (left <= 0) throw exhausted();
|
|
110
|
+
// Jitter plus escalating bounds keep a slow daemon from collecting a retry storm.
|
|
111
|
+
await sleep(Math.min(left, Math.min(1000, timeoutMs / 4) * (0.5 + Math.random())), undefined, { signal: attemptSignal });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
this.tail = job.catch(() => {});
|
|
116
|
+
void job.then(deliver, fail);
|
|
117
|
+
try { return await response; }
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
signal.removeEventListener('abort', onClose);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { deflateRawSync } from 'node:zlib';
|
|
1
|
+
import { RE2JS_DEFLATED_BASE64 } from './re2js-embedded.js';
|
|
4
2
|
import { bindingDependencies } from './input-binding.js';
|
|
5
3
|
import { isNamedGate, regexFlags } from './named-gates.js';
|
|
6
4
|
import type { NamedDataGate, StepSpec, VerificationSpec } from './spec.js';
|
|
7
5
|
|
|
8
6
|
const quote = (text: string): string => `'${text.replaceAll("'", "'\\''")}'`;
|
|
9
|
-
let re2Source: string | undefined;
|
|
10
7
|
|
|
11
8
|
/** Embed the pinned engine in the command: execution does not resolve npm or SDK paths. */
|
|
12
9
|
function embeddedRE2(): string {
|
|
13
|
-
|
|
14
|
-
return `const re2={};new Function('exports',require('node:zlib').inflateRawSync(Buffer.from(${JSON.stringify(re2Source)},'base64')).toString())(re2);`;
|
|
10
|
+
return `const re2={};new Function('exports',require('node:zlib').inflateRawSync(Buffer.from(${JSON.stringify(RE2JS_DEFLATED_BASE64)},'base64')).toString())(re2);`;
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
/**
|