@intx/workflow-host 0.2.2

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 (81) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +287 -0
  3. package/dist/adapters/blob-substrate.d.ts +49 -0
  4. package/dist/adapters/blob-substrate.js +140 -0
  5. package/dist/adapters/repo-store.d.ts +39 -0
  6. package/dist/adapters/repo-store.js +344 -0
  7. package/dist/adapters/spawn-child.d.ts +74 -0
  8. package/dist/adapters/spawn-child.js +152 -0
  9. package/dist/adapters/step-invoker.d.ts +114 -0
  10. package/dist/adapters/step-invoker.js +360 -0
  11. package/dist/child/env-bootstrap.d.ts +56 -0
  12. package/dist/child/env-bootstrap.js +120 -0
  13. package/dist/child/from-process-env.d.ts +127 -0
  14. package/dist/child/from-process-env.js +183 -0
  15. package/dist/child/index.d.ts +9 -0
  16. package/dist/child/index.js +9 -0
  17. package/dist/child/outbound-mail-bridge.d.ts +36 -0
  18. package/dist/child/outbound-mail-bridge.js +143 -0
  19. package/dist/child/proxy-repo-store.d.ts +27 -0
  20. package/dist/child/proxy-repo-store.js +200 -0
  21. package/dist/child/run-child.d.ts +320 -0
  22. package/dist/child/run-child.js +900 -0
  23. package/dist/child/self-discovery.d.ts +29 -0
  24. package/dist/child/self-discovery.js +57 -0
  25. package/dist/child/substrate-write-bridge.d.ts +72 -0
  26. package/dist/child/substrate-write-bridge.js +188 -0
  27. package/dist/child/supervisor-backed-transport.d.ts +10 -0
  28. package/dist/child/supervisor-backed-transport.js +113 -0
  29. package/dist/child/warm-agent-cache.d.ts +78 -0
  30. package/dist/child/warm-agent-cache.js +112 -0
  31. package/dist/drain-controller.d.ts +37 -0
  32. package/dist/drain-controller.js +46 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.js +10 -0
  35. package/dist/ipc/control-channel.d.ts +336 -0
  36. package/dist/ipc/control-channel.js +532 -0
  37. package/dist/ipc/crypto.d.ts +46 -0
  38. package/dist/ipc/crypto.js +126 -0
  39. package/dist/ipc/envelope.d.ts +53 -0
  40. package/dist/ipc/envelope.js +88 -0
  41. package/dist/ipc/event-channel.d.ts +677 -0
  42. package/dist/ipc/event-channel.js +278 -0
  43. package/dist/ipc/index.d.ts +4 -0
  44. package/dist/ipc/index.js +143 -0
  45. package/dist/mail-bus/hub-transport-adapter.d.ts +30 -0
  46. package/dist/mail-bus/hub-transport-adapter.js +76 -0
  47. package/dist/mail-bus/index.d.ts +1 -0
  48. package/dist/mail-bus/index.js +1 -0
  49. package/dist/seams/index.d.ts +3 -0
  50. package/dist/seams/index.js +3 -0
  51. package/dist/seams/scheduler-adapter.d.ts +3 -0
  52. package/dist/seams/scheduler-adapter.js +24 -0
  53. package/dist/seams/scheduler.d.ts +94 -0
  54. package/dist/seams/scheduler.js +397 -0
  55. package/dist/seams/signal-channel.d.ts +74 -0
  56. package/dist/seams/signal-channel.js +304 -0
  57. package/dist/supervisor/cancel-signing.d.ts +68 -0
  58. package/dist/supervisor/cancel-signing.js +144 -0
  59. package/dist/supervisor/child-termination.d.ts +51 -0
  60. package/dist/supervisor/child-termination.js +76 -0
  61. package/dist/supervisor/credentials.d.ts +101 -0
  62. package/dist/supervisor/credentials.js +153 -0
  63. package/dist/supervisor/dispatch-attribution.d.ts +37 -0
  64. package/dist/supervisor/dispatch-attribution.js +114 -0
  65. package/dist/supervisor/drain-timeout.d.ts +127 -0
  66. package/dist/supervisor/drain-timeout.js +231 -0
  67. package/dist/supervisor/index.d.ts +7 -0
  68. package/dist/supervisor/index.js +6 -0
  69. package/dist/supervisor/recycle.d.ts +212 -0
  70. package/dist/supervisor/recycle.js +440 -0
  71. package/dist/supervisor/run-event-compaction.d.ts +34 -0
  72. package/dist/supervisor/run-event-compaction.js +115 -0
  73. package/dist/supervisor/spawn-env.d.ts +39 -0
  74. package/dist/supervisor/spawn-env.js +36 -0
  75. package/dist/supervisor/supervisor.d.ts +202 -0
  76. package/dist/supervisor/supervisor.js +2244 -0
  77. package/dist/supervisor/terminal-broadcaster.d.ts +45 -0
  78. package/dist/supervisor/terminal-broadcaster.js +184 -0
  79. package/dist/supervisor/types.d.ts +542 -0
  80. package/dist/supervisor/types.js +10 -0
  81. package/package.json +35 -0
@@ -0,0 +1,101 @@
1
+ import type { Principal, RepoId, RepoStore } from "@intx/hub-sessions/substrate";
2
+ /**
3
+ * Path inside each step's `agent-state` repo that carries the step's
4
+ * grants. The agent-state kind handler accepts state writes under the
5
+ * `state/` subtree; the grants snapshot rides at
6
+ * `state/grants.json` as a single canonical JSON document.
7
+ */
8
+ export declare const STEP_GRANTS_PATH = "state/grants.json";
9
+ /**
10
+ * Ref the supervisor reads each step's grants from. The agent-state
11
+ * kind handler defines the deploy ref under `refs/heads/deploy`; the
12
+ * state-bearing refs the sidecar writes to use the default
13
+ * `refs/heads/main` convention.
14
+ */
15
+ export declare const STEP_GRANTS_REF = "refs/heads/main";
16
+ export type CredentialsSnapshotStep = {
17
+ /** Workflow step id from `WorkflowDefinition.stepOrder`. */
18
+ stepId: string;
19
+ /** Mail address the step's agent presents to the bus. */
20
+ address: string;
21
+ /** Opaque `grants` array as committed in the step's repo. */
22
+ grants: readonly unknown[];
23
+ /** sha256 hex over the canonical JSON of `grants`. */
24
+ contentHash: string;
25
+ };
26
+ export type CredentialsSnapshot = {
27
+ /** Step-id keyed entries in `stepOrder` traversal order. */
28
+ steps: readonly CredentialsSnapshotStep[];
29
+ };
30
+ /**
31
+ * Caller-supplied derivation of the per-step mail address from the
32
+ * deployment id and step id. The supervisor cannot encode the
33
+ * deployment-domain inside library code; the wiring module supplies
34
+ * the strategy the host owns.
35
+ */
36
+ export type DeriveStepAddress = (args: {
37
+ deploymentId: string;
38
+ stepId: string;
39
+ }) => string;
40
+ /**
41
+ * Caller-supplied override of the per-step `agent-state` repo identity
42
+ * the supervisor reads grants from. Defaults to the
43
+ * `<deploymentId>-<stepId>` convention (`defaultStepRepoId`); the
44
+ * single-step launched-agent deploy supplies a derivation that returns
45
+ * the legacy agent-state repo so the child reads grants from the same
46
+ * repo the legacy agent identity already keys.
47
+ */
48
+ export type DeriveStepRepoId = (args: {
49
+ deploymentId: string;
50
+ stepId: string;
51
+ }) => RepoId;
52
+ export type AssembleCredentialsSnapshotOpts = {
53
+ /** Substrate handle the supervisor reads from. */
54
+ repoStore: RepoStore;
55
+ /** Principal presented for each step's read. */
56
+ principal: Principal;
57
+ /**
58
+ * Step ids in the deployment's `stepOrder`. The trivial workflow
59
+ * passes a single entry; multi-step deployments pass every step in
60
+ * the order the workflow asset declared.
61
+ */
62
+ stepOrder: readonly string[];
63
+ /** Deployment id used in agent-state repo identity and address derivation. */
64
+ deploymentId: string;
65
+ /** Per-step mail-address derivation callback. */
66
+ deriveStepAddress: DeriveStepAddress;
67
+ /**
68
+ * Optional override for the `agent-state` repo's id. Callers that
69
+ * follow the documented convention (`<deploymentId>-<stepId>`) can
70
+ * omit this; tests and bespoke layouts can supply their own.
71
+ */
72
+ deriveStepRepoId?: DeriveStepRepoId;
73
+ };
74
+ /**
75
+ * Default mapping from `(deploymentId, stepId)` to the agent-state
76
+ * repo id: `<deploymentId>-<stepId>`, isolating each step's grants in
77
+ * its own repo. Applied to a one-step `stepOrder` it yields a single
78
+ * such repo. The single-step launched-agent deploy overrides this
79
+ * default (see `DeriveStepRepoId`) to reuse the legacy agent-state
80
+ * repo.
81
+ */
82
+ export declare function defaultStepRepoId(args: {
83
+ deploymentId: string;
84
+ stepId: string;
85
+ }): RepoId;
86
+ /**
87
+ * Compute the per-step content hash used to pin a credentialsSnapshot
88
+ * push to a specific grants payload. Stable across processes because
89
+ * the JSON.stringify pass produces the same byte string for a given
90
+ * grants array.
91
+ */
92
+ export declare function hashGrants(grants: readonly unknown[]): Promise<string>;
93
+ /**
94
+ * Assemble the per-deployment `credentialsSnapshot` from each step's
95
+ * `agent-state` repo. The supervisor invokes this at spawn time and
96
+ * again on every `grants-updated` mail. The result rides the
97
+ * control IPC as a single payload; the child pins each step's grants
98
+ * to the supplied hash so it can ignore an out-of-order push that
99
+ * arrives after a fresher one.
100
+ */
101
+ export declare function assembleCredentialsSnapshot(opts: AssembleCredentialsSnapshotOpts): Promise<CredentialsSnapshot>;
@@ -0,0 +1,153 @@
1
+ // Per-step `credentialsSnapshot` assembly.
2
+ //
3
+ // At spawn time, the supervisor reads each workflow step's grants out
4
+ // of its `agent-state` repo and forwards the resulting per-step
5
+ // snapshot to the workflow-process over the control IPC. The snapshot
6
+ // never lands on disk in the child's view and is never placed in
7
+ // spawn-time env -- the env carries only the IPC trust anchors. The
8
+ // child receives credentials over the authenticated control channel
9
+ // once both halves are connected.
10
+ //
11
+ // Each step's snapshot is pinned to a content hash so the child can
12
+ // detect a stale snapshot (e.g. one that arrived after a `grants-
13
+ // updated` push the child already processed). The hash is the
14
+ // sha256 of the canonicalized JSON serialization of the snapshot's
15
+ // per-step `grants` array -- not the git tree SHA of the underlying
16
+ // `agent-state` repo's `grants` ref. The substrate's tree SHA is
17
+ // implementation-coupled to the path of the file inside the repo;
18
+ // hashing the canonical JSON gives a stable identifier the child can
19
+ // compare across pushes without taking a dependency on the
20
+ // substrate's git layout.
21
+ //
22
+ // Per-step address derivation (Q6.4 discovery decision):
23
+ // - Multi-step deployments use `<deploymentId>-<stepId>@<domain>`.
24
+ // - Trivial (single-step) deployments use the deployment's own
25
+ // mail address as the sole step's address.
26
+ // The derivation is supplied by the caller as a `deriveStepAddress`
27
+ // callback so the supervisor doesn't have to encode the deployment-
28
+ // domain into the workflow-host package.
29
+ import { type } from "arktype";
30
+ import { hexEncode } from "@intx/types";
31
+ /**
32
+ * Path inside each step's `agent-state` repo that carries the step's
33
+ * grants. The agent-state kind handler accepts state writes under the
34
+ * `state/` subtree; the grants snapshot rides at
35
+ * `state/grants.json` as a single canonical JSON document.
36
+ */
37
+ export const STEP_GRANTS_PATH = "state/grants.json";
38
+ /**
39
+ * Ref the supervisor reads each step's grants from. The agent-state
40
+ * kind handler defines the deploy ref under `refs/heads/deploy`; the
41
+ * state-bearing refs the sidecar writes to use the default
42
+ * `refs/heads/main` convention.
43
+ */
44
+ export const STEP_GRANTS_REF = "refs/heads/main";
45
+ /**
46
+ * Shape of the per-step grants file. The supervisor surfaces this
47
+ * untyped to the child -- the child's authorize layer narrows the
48
+ * inner `grants` entries against its own grant-rule validator.
49
+ */
50
+ const StepGrantsFile = type({
51
+ grants: "unknown[]",
52
+ }).onUndeclaredKey("ignore");
53
+ /**
54
+ * Default mapping from `(deploymentId, stepId)` to the agent-state
55
+ * repo id: `<deploymentId>-<stepId>`, isolating each step's grants in
56
+ * its own repo. Applied to a one-step `stepOrder` it yields a single
57
+ * such repo. The single-step launched-agent deploy overrides this
58
+ * default (see `DeriveStepRepoId`) to reuse the legacy agent-state
59
+ * repo.
60
+ */
61
+ export function defaultStepRepoId(args) {
62
+ return {
63
+ kind: "agent-state",
64
+ id: `${args.deploymentId}-${args.stepId}`,
65
+ };
66
+ }
67
+ /**
68
+ * Read one step's grants file from disk via the substrate's working-
69
+ * tree directory. The substrate documents `getRepoDir` as a pure path
70
+ * computation -- the sibling production adapters (`repo-store.ts`,
71
+ * `spawn-child.ts`) use the same working-tree-read pattern when they
72
+ * need synchronous access to a ref's tip without round-tripping
73
+ * through the git object database.
74
+ *
75
+ * A missing grants file is treated as "no grants" (empty array) so
76
+ * a step whose repo carries no grants file does not crash; a
77
+ * malformed file does crash, because the file's presence implies
78
+ * the deploy orchestrator intended a snapshot and a structural
79
+ * failure is a programming bug at the boundary.
80
+ */
81
+ async function readStepGrants(opts, repoId) {
82
+ const fs = await import("node:fs/promises");
83
+ const path = await import("node:path");
84
+ const dir = opts.repoStore.getRepoDir(repoId);
85
+ const filePath = path.join(dir, STEP_GRANTS_PATH);
86
+ let raw;
87
+ try {
88
+ raw = await fs.readFile(filePath, "utf8");
89
+ }
90
+ catch (cause) {
91
+ if (isErrnoNotFound(cause))
92
+ return [];
93
+ throw cause;
94
+ }
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(raw);
98
+ }
99
+ catch (cause) {
100
+ throw new Error(`supervisor credentialsSnapshot: ${repoId.kind}/${repoId.id}:${STEP_GRANTS_PATH} is not valid JSON`, { cause });
101
+ }
102
+ const validated = StepGrantsFile(parsed);
103
+ if (validated instanceof type.errors) {
104
+ throw new Error(`supervisor credentialsSnapshot: ${repoId.kind}/${repoId.id}:${STEP_GRANTS_PATH} failed validation: ${validated.summary}`);
105
+ }
106
+ return validated.grants;
107
+ }
108
+ /**
109
+ * Compute the per-step content hash used to pin a credentialsSnapshot
110
+ * push to a specific grants payload. Stable across processes because
111
+ * the JSON.stringify pass produces the same byte string for a given
112
+ * grants array.
113
+ */
114
+ export async function hashGrants(grants) {
115
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(grants)));
116
+ return hexEncode(new Uint8Array(digest));
117
+ }
118
+ /**
119
+ * Assemble the per-deployment `credentialsSnapshot` from each step's
120
+ * `agent-state` repo. The supervisor invokes this at spawn time and
121
+ * again on every `grants-updated` mail. The result rides the
122
+ * control IPC as a single payload; the child pins each step's grants
123
+ * to the supplied hash so it can ignore an out-of-order push that
124
+ * arrives after a fresher one.
125
+ */
126
+ export async function assembleCredentialsSnapshot(opts) {
127
+ const deriveRepoId = opts.deriveStepRepoId ?? defaultStepRepoId;
128
+ const steps = [];
129
+ for (const stepId of opts.stepOrder) {
130
+ const repoId = deriveRepoId({
131
+ deploymentId: opts.deploymentId,
132
+ stepId,
133
+ });
134
+ const grants = await readStepGrants(opts, repoId);
135
+ const address = opts.deriveStepAddress({
136
+ deploymentId: opts.deploymentId,
137
+ stepId,
138
+ });
139
+ steps.push({
140
+ stepId,
141
+ address,
142
+ grants,
143
+ contentHash: await hashGrants(grants),
144
+ });
145
+ }
146
+ return { steps };
147
+ }
148
+ function isErrnoNotFound(cause) {
149
+ if (cause === null || typeof cause !== "object")
150
+ return false;
151
+ const code = cause.code;
152
+ return code === "ENOENT";
153
+ }
@@ -0,0 +1,37 @@
1
+ import type { DispatchStructuralCounters } from "./types.js";
2
+ /**
3
+ * Sample the four structural counters against the workflow-run repo's
4
+ * on-disk working tree. Cheap filesystem reads; invoked only when the
5
+ * D2 observer is wired, at each leg's `"end"` mark, so the per-leg slope
6
+ * can be correlated with the grower that explains it.
7
+ */
8
+ export declare function sampleStructuralCounters(repoDir: string): DispatchStructuralCounters;
9
+ /**
10
+ * Measurement-only forced-repack toggle for the §10c A/B. When the boot
11
+ * edge supplies a non-zero `everyMessages`, the supervisor invokes
12
+ * `maybeRepack` once per dispatched message (after `markConsumed`, still
13
+ * under the single-writer discipline -- the dispatch loop processes one
14
+ * message at a time and no concurrent commit is in flight at that point),
15
+ * and every `everyMessages`-th message forces a `git gc`/repack of the
16
+ * workflow-run repo. If forcing a repack flattens the per-leg slope, the
17
+ * cost is loose-object/pack growth (cheap pack/gc fix); if it does not,
18
+ * the cost is the per-commit root-tree rewrite scaling with `runs/` +
19
+ * `consumed/` fan-out (run-model change). Absent toggle => never repacks.
20
+ */
21
+ export type RepackToggle = {
22
+ everyMessages: number;
23
+ };
24
+ /**
25
+ * Force a synchronous `git gc` of the repo at `repoDir`. Synchronous
26
+ * (`spawnSync`) so the supervisor's single-writer invariant is trivially
27
+ * preserved: the dispatch loop is the sole caller and blocks here, so no
28
+ * `writeTreePreservingPrefix` commit can interleave with the repack.
29
+ * Returns the wall-clock duration in ms and whether the gc succeeded;
30
+ * a non-zero exit is surfaced (not swallowed) so a broken A/B run does
31
+ * not silently report "repack had no effect".
32
+ */
33
+ export declare function forceRepack(repoDir: string): {
34
+ ok: boolean;
35
+ durationMs: number;
36
+ detail: string;
37
+ };
@@ -0,0 +1,114 @@
1
+ // D2 per-leg substrate-attribution instrumentation (measurement-only).
2
+ //
3
+ // This module is the off-by-default observability + A/B surface the D2
4
+ // latency re-attribution drives. NONE of it runs in production: the
5
+ // supervisor only reaches these helpers when a `onDispatchTiming`
6
+ // observer is wired (the same env-gated seam the 4.7 latency gate added)
7
+ // and only consults the repack toggle when the boot edge supplies a
8
+ // non-zero repack interval. With the observer unwired and the toggle
9
+ // absent, the supervisor samples no clock, reads no directory, and forks
10
+ // no `git gc` -- the dispatch path is byte-for-byte unchanged.
11
+ //
12
+ // The counters are deliberately filesystem-level reads against the
13
+ // workflow-run repo's on-disk working tree (resolved via
14
+ // `RepoStore.getRepoDir`, a pure path computation), NOT reaches into
15
+ // isogit internals. That keeps the attribution honest about what it can
16
+ // observe cheaply and avoids coupling the measurement to the storage
17
+ // layer's private object model. `runs/` and `addresses/<addr>/consumed/`
18
+ // fan-out is exactly the never-pruned tree growth the design (§9)
19
+ // implicates; loose-object count + `.git` byte size are the pack-growth
20
+ // proxies the §10c repack A/B discriminates against.
21
+ import { spawnSync } from "node:child_process";
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import { countLooseObjects, gitBytes } from "@intx/storage-isogit";
25
+ const RUNS_DIR = "runs";
26
+ const ADDRESSES_DIR = "addresses";
27
+ const CONSUMED_DIR = "consumed";
28
+ /**
29
+ * Count the immediate child entries of `dir`. Returns 0 when the
30
+ * directory does not exist yet (the repo's first commit has not created
31
+ * the subtree) -- absence is a real "fan-out is zero", not an error to
32
+ * surface, because the sampler runs on every leg including ones that fire
33
+ * before the subtree exists.
34
+ */
35
+ function countEntries(dir) {
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(dir);
39
+ }
40
+ catch (cause) {
41
+ if (cause instanceof Error &&
42
+ cause.code === "ENOENT") {
43
+ return 0;
44
+ }
45
+ throw cause;
46
+ }
47
+ return entries.length;
48
+ }
49
+ /**
50
+ * Total entries under every `addresses/<segment>/consumed/`. The address
51
+ * segment is `urlEncoded(address)` on disk; rather than re-deriving the
52
+ * exact encoding (and risking drift from the kind handler), enumerate the
53
+ * address segments present and sum each one's `consumed/` fan-out. For a
54
+ * single warm deployment there is exactly one segment, but the sum is
55
+ * correct for any number.
56
+ */
57
+ function countConsumed(repoDir) {
58
+ const addressesDir = path.join(repoDir, ADDRESSES_DIR);
59
+ let segments;
60
+ try {
61
+ segments = fs.readdirSync(addressesDir);
62
+ }
63
+ catch (cause) {
64
+ if (cause instanceof Error &&
65
+ cause.code === "ENOENT") {
66
+ return 0;
67
+ }
68
+ throw cause;
69
+ }
70
+ let total = 0;
71
+ for (const segment of segments) {
72
+ total += countEntries(path.join(addressesDir, segment, CONSUMED_DIR));
73
+ }
74
+ return total;
75
+ }
76
+ /**
77
+ * Sample the four structural counters against the workflow-run repo's
78
+ * on-disk working tree. Cheap filesystem reads; invoked only when the
79
+ * D2 observer is wired, at each leg's `"end"` mark, so the per-leg slope
80
+ * can be correlated with the grower that explains it.
81
+ */
82
+ export function sampleStructuralCounters(repoDir) {
83
+ return {
84
+ runsFanOut: countEntries(path.join(repoDir, RUNS_DIR)),
85
+ consumedFanOut: countConsumed(repoDir),
86
+ looseObjects: countLooseObjects(repoDir),
87
+ gitBytes: gitBytes(repoDir),
88
+ };
89
+ }
90
+ /**
91
+ * Force a synchronous `git gc` of the repo at `repoDir`. Synchronous
92
+ * (`spawnSync`) so the supervisor's single-writer invariant is trivially
93
+ * preserved: the dispatch loop is the sole caller and blocks here, so no
94
+ * `writeTreePreservingPrefix` commit can interleave with the repack.
95
+ * Returns the wall-clock duration in ms and whether the gc succeeded;
96
+ * a non-zero exit is surfaced (not swallowed) so a broken A/B run does
97
+ * not silently report "repack had no effect".
98
+ */
99
+ export function forceRepack(repoDir) {
100
+ const t0 = performance.now();
101
+ const result = spawnSync("git", ["-C", repoDir, "gc", "--quiet", "--prune=now"], { encoding: "utf8" });
102
+ const durationMs = performance.now() - t0;
103
+ if (result.error !== undefined) {
104
+ return { ok: false, durationMs, detail: result.error.message };
105
+ }
106
+ if (result.status !== 0) {
107
+ return {
108
+ ok: false,
109
+ durationMs,
110
+ detail: `git gc exited ${String(result.status)}: ${result.stderr}`,
111
+ };
112
+ }
113
+ return { ok: true, durationMs, detail: "" };
114
+ }
@@ -0,0 +1,127 @@
1
+ import type { PrincipalSigner, TerminalEventSource } from "./types.js";
2
+ import type { RepoId, RepoStore as SubstrateRepoStore } from "@intx/hub-sessions/substrate";
3
+ /**
4
+ * Default `drainTimeout` per deployment. Operators override this via
5
+ * a per-deployment policy; this value is the spec's locked default.
6
+ */
7
+ export declare const DEFAULT_DRAIN_TIMEOUT_MS = 60000;
8
+ export type DrainTimeoutOpts = {
9
+ /**
10
+ * Substrate handle through which the escalation commit lands.
11
+ * Reused from the supervisor's bindings -- the accumulator does
12
+ * not own a substrate of its own.
13
+ */
14
+ substrate: SubstrateRepoStore;
15
+ /** Workflow-run repo for this deployment. */
16
+ repoId: RepoId;
17
+ /** Workflow-run repo ref the supervisor commits events to. */
18
+ ref: string;
19
+ /** Deployment id baked into the supervisor's signing principal. */
20
+ deploymentId: string;
21
+ /** Run id the drain is being escalated against. */
22
+ runId: string;
23
+ /**
24
+ * Host-supplied per-principal signing callback the supervisor uses
25
+ * to mint the CancelRequested signature. The drain origin is
26
+ * `supervisor-drain`.
27
+ */
28
+ signAsPrincipal: PrincipalSigner;
29
+ /** ISO-8601 reason for the CancelRequested escalation. */
30
+ reason?: string;
31
+ /**
32
+ * Operator-overridable per-deployment timeout. Defaults to
33
+ * `DEFAULT_DRAIN_TIMEOUT_MS`.
34
+ */
35
+ drainTimeoutMs?: number;
36
+ /**
37
+ * Deterministic clock for the accumulator. Tests inject a
38
+ * controllable clock; production wires `() => Date.now()`.
39
+ */
40
+ now: () => number;
41
+ /**
42
+ * Scheduling primitive the accumulator uses for its tick callback.
43
+ * Tests inject a deterministic timer; production wires
44
+ * `(cb, ms) => setTimeout(cb, ms)`.
45
+ */
46
+ setTimer: (cb: () => void, ms: number) => unknown;
47
+ /**
48
+ * Disposer paired with `setTimer`. Production wires
49
+ * `(handle) => clearTimeout(handle as ReturnType<typeof setTimeout>)`.
50
+ */
51
+ clearTimer: (handle: unknown) => void;
52
+ /**
53
+ * Optional per-runId terminal-event source the accumulator consumes
54
+ * to settle early when the tracked run reaches a terminal phase
55
+ * before the drainTimeout fires. When absent, the accumulator falls
56
+ * back to timer-only settlement (the pre-binding behaviour). The
57
+ * supervisor wires this against its `terminalEventSource` binding;
58
+ * the iterator is finalised via `return()` whenever the accumulator
59
+ * settles (terminal, timeout, or `stop()`).
60
+ */
61
+ terminalEventSource?: TerminalEventSource;
62
+ };
63
+ /**
64
+ * Handle the supervisor holds onto so it can pause/resume ticking
65
+ * during step-kind transitions, and so it can stop the accumulator
66
+ * cleanly when the run reaches a terminal phase before the
67
+ * drainTimeout expires.
68
+ */
69
+ export interface DrainTimeoutAccumulator {
70
+ /**
71
+ * Begin counting cancel-time. Idempotent; a second call after the
72
+ * accumulator has already started is a no-op so the supervisor's
73
+ * control loop can re-issue drain without breaking ordering.
74
+ */
75
+ start(): void;
76
+ /**
77
+ * Pause the accumulator. The supervisor calls this when its view
78
+ * of the workflow's in-flight steps transitions from cancel-mode
79
+ * to wait-mode (e.g., the last cancel-mode step completed and only
80
+ * `awaitSignal` steps remain). Time spent paused does not count
81
+ * toward the timeout.
82
+ */
83
+ pause(): void;
84
+ /**
85
+ * Resume the accumulator. Inverse of `pause`. Time elapsed since
86
+ * the last `start` or `resume` carries forward -- pause/resume is
87
+ * about wall-clock attribution, not zeroing the count.
88
+ */
89
+ resume(): void;
90
+ /**
91
+ * Stop the accumulator without escalating. Called when the run
92
+ * reaches a terminal phase before drainTimeout expires.
93
+ */
94
+ stop(): void;
95
+ /** Total accumulated cancel-time at the current moment, in ms. */
96
+ accumulatedMs(): number;
97
+ /** Whether the accumulator has already escalated. */
98
+ readonly escalated: boolean;
99
+ /**
100
+ * Resolves when every asynchronous resource the accumulator owns has
101
+ * been disposed: the escalation commit (if armed) has finished, and
102
+ * the per-runId terminal-event watcher (if armed) has had its
103
+ * iterator finalised. The supervisor awaits this in `shutdown` so a
104
+ * still-running watcher cannot fire a settle against a torn-down
105
+ * supervisor.
106
+ *
107
+ * Idempotent: calling `disposed()` multiple times returns the same
108
+ * promise. Resolves once `stop()` or the timer-driven escalation has
109
+ * run.
110
+ */
111
+ disposed(): Promise<void>;
112
+ }
113
+ /**
114
+ * Factory shape the supervisor binds against. Mirrors
115
+ * `createDrainTimeoutAccumulator` exactly; surfaced so tests can
116
+ * inject a mock factory through `WorkflowSupervisorBindings.
117
+ * drainTimeoutAccumulatorFactory` and observe the supervisor's
118
+ * drain-arming sequence without rigging a real timer host.
119
+ */
120
+ export type DrainTimeoutAccumulatorFactory = (opts: DrainTimeoutOpts) => DrainTimeoutAccumulator;
121
+ /**
122
+ * Construct the supervisor's drainTimeout accumulator. The returned
123
+ * handle is single-use: once `escalated` flips true the accumulator
124
+ * has committed `CancelRequested{origin: "supervisor-drain"}` and
125
+ * cannot tick again.
126
+ */
127
+ export declare function createDrainTimeoutAccumulator(opts: DrainTimeoutOpts): DrainTimeoutAccumulator;