@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,231 @@
1
+ // Supervisor-side drainTimeout accumulator and escalation path.
2
+ //
3
+ // When the supervisor's control loop sends `drain` to the workflow
4
+ // process, the child's `DrainController` flips its signal and the
5
+ // runtime body picks up the change on the next tick at each of the
6
+ // four observation points. Steps whose declared `drainBehavior` is
7
+ // `"cancel"` abort their local controller; steps whose behavior is
8
+ // `"wait"` continue running.
9
+ //
10
+ // The supervisor's accumulator tracks wall-clock time spent against
11
+ // `"cancel"`-behavior work after drain has been issued. Time spent
12
+ // while every in-flight step is `"wait"`-behavior does NOT tick the
13
+ // accumulator -- waiting for a human-in-the-loop pause should not
14
+ // burn the drain budget. On `drainTimeout` expiry, the accumulator
15
+ // invokes the supervisor's injected `signAsPrincipal("supervisor",
16
+ // ...)` callback to obtain a signed `CancelRequested{origin:
17
+ // "supervisor-drain"}` and commits it via the injected substrate
18
+ // handle. The runtime body's existing cancellation cascade handles
19
+ // teardown from there.
20
+ //
21
+ // The accumulator is started by the supervisor on receipt of the
22
+ // host's drain command; it is stopped when (a) drainTimeout expires
23
+ // and the CancelRequested commit lands, or (b) the run reaches a
24
+ // terminal phase before expiry. Pausing is driven by the supervisor's
25
+ // view of which step kinds are in flight -- it consults the same
26
+ // `behaviorFor` projection the workflow-host child uses.
27
+ import { getLogger } from "@intx/log";
28
+ import { commitCancelRequested } from "./cancel-signing.js";
29
+ const logger = getLogger(["workflow-host", "supervisor", "drain-timeout"]);
30
+ /**
31
+ * Default `drainTimeout` per deployment. Operators override this via
32
+ * a per-deployment policy; this value is the spec's locked default.
33
+ */
34
+ export const DEFAULT_DRAIN_TIMEOUT_MS = 60_000;
35
+ /**
36
+ * Construct the supervisor's drainTimeout accumulator. The returned
37
+ * handle is single-use: once `escalated` flips true the accumulator
38
+ * has committed `CancelRequested{origin: "supervisor-drain"}` and
39
+ * cannot tick again.
40
+ */
41
+ export function createDrainTimeoutAccumulator(opts) {
42
+ const drainTimeoutMs = opts.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
43
+ const reason = opts.reason ?? "drainTimeout expired";
44
+ let state = { phase: "idle" };
45
+ let escalated = false;
46
+ /**
47
+ * Active terminal-event iterator the accumulator is consuming for
48
+ * its tracked runId. Held so the dispose path can finalise it via
49
+ * `return()`. `null` when no source binding was supplied or the
50
+ * iterator has already been finalised.
51
+ */
52
+ let terminalIterator = null;
53
+ /** Promise the per-runId terminal-watcher coroutine resolves into. */
54
+ let terminalWatcherDone = null;
55
+ /** Latest pending escalation commit; awaited by `disposed()`. */
56
+ let escalationPending = null;
57
+ let disposedPromise = null;
58
+ function armTerminalWatcher() {
59
+ if (opts.terminalEventSource === undefined)
60
+ return;
61
+ if (terminalIterator !== null)
62
+ return;
63
+ const iterable = opts.terminalEventSource(opts.runId);
64
+ const iterator = iterable[Symbol.asyncIterator]();
65
+ terminalIterator = iterator;
66
+ terminalWatcherDone = (async () => {
67
+ try {
68
+ // The first terminal event the source yields is the signal to
69
+ // settle. The source pre-filters on runId, so any element it
70
+ // produces applies to the tracked run.
71
+ const next = await iterator.next();
72
+ if (next.done === true)
73
+ return;
74
+ settleOnTerminal();
75
+ }
76
+ catch (cause) {
77
+ // The iterator's failure does not need an explicit
78
+ // escalation: the outer `setTimer`-based deadline keeps
79
+ // ticking against the same `state.phase === "running"` slot
80
+ // and fires `escalate()` if no terminal event arrives. The
81
+ // warn here surfaces the iterator failure to operator logs
82
+ // so a persistent broken event source is visible; the
83
+ // accumulator's contract is preserved by the timer's
84
+ // fall-through.
85
+ const message = cause instanceof Error ? cause.message : String(cause);
86
+ logger.warn `terminal-event watcher for run ${opts.runId} threw: ${message}`;
87
+ }
88
+ })();
89
+ }
90
+ function settleOnTerminal() {
91
+ if (escalated)
92
+ return;
93
+ if (state.phase === "stopped" || state.phase === "escalated")
94
+ return;
95
+ if (state.phase === "running") {
96
+ opts.clearTimer(state.timer);
97
+ }
98
+ state = { phase: "stopped" };
99
+ }
100
+ function remainingMs() {
101
+ if (state.phase === "running") {
102
+ const elapsed = opts.now() - state.startedAt + state.baseline;
103
+ return Math.max(0, drainTimeoutMs - elapsed);
104
+ }
105
+ if (state.phase === "paused") {
106
+ return Math.max(0, drainTimeoutMs - state.baseline);
107
+ }
108
+ return drainTimeoutMs;
109
+ }
110
+ function arm(baseline) {
111
+ const remaining = drainTimeoutMs - baseline;
112
+ if (remaining <= 0) {
113
+ void escalate();
114
+ return;
115
+ }
116
+ const timer = opts.setTimer(() => {
117
+ // The timer is the authority for escalation -- the accumulator
118
+ // does not poll. When the timer fires we transition to
119
+ // `escalated` and commit through the substrate.
120
+ void escalate();
121
+ }, remaining);
122
+ state = {
123
+ phase: "running",
124
+ startedAt: opts.now(),
125
+ baseline,
126
+ timer,
127
+ };
128
+ }
129
+ function escalate() {
130
+ if (escalated)
131
+ return;
132
+ escalated = true;
133
+ if (state.phase === "running") {
134
+ opts.clearTimer(state.timer);
135
+ }
136
+ state = { phase: "escalated" };
137
+ // The terminal watcher's role ends when the deadline fires: the
138
+ // commit below races the on-disk CancelRequested against any
139
+ // natural terminal arrival, and either way the accumulator settles
140
+ // here. Finalising the iterator lets the producer free its
141
+ // resources.
142
+ finaliseTerminalWatcher();
143
+ const origin = "supervisor-drain";
144
+ escalationPending = commitCancelRequested({
145
+ substrate: opts.substrate,
146
+ repoId: opts.repoId,
147
+ ref: opts.ref,
148
+ deploymentId: opts.deploymentId,
149
+ runId: opts.runId,
150
+ origin,
151
+ reason,
152
+ at: new Date().toISOString(),
153
+ signAsPrincipal: opts.signAsPrincipal,
154
+ })
155
+ .then(() => undefined)
156
+ .catch((cause) => {
157
+ const message = cause instanceof Error ? cause.message : String(cause);
158
+ logger.error `drainTimeout escalation commit failed for run ${opts.runId}: ${message}`;
159
+ throw cause instanceof Error ? cause : new Error(message);
160
+ });
161
+ }
162
+ function finaliseTerminalWatcher() {
163
+ const iterator = terminalIterator;
164
+ if (iterator === null)
165
+ return;
166
+ terminalIterator = null;
167
+ if (typeof iterator.return !== "function")
168
+ return;
169
+ // Fire the iterator's `return()` so the producer side observes the
170
+ // cancel. The producer (`subscribeKind`-backed) wires its own
171
+ // `AbortSignal`; finalising the iterator surfaces as an abort
172
+ // through the substrate's `subscribe` primitive, which is the
173
+ // contract `subscribeKind` documents.
174
+ void iterator.return(undefined).catch((cause) => {
175
+ const message = cause instanceof Error ? cause.message : String(cause);
176
+ logger.warn `terminal-event watcher return() for run ${opts.runId} threw: ${message}`;
177
+ });
178
+ }
179
+ return {
180
+ start() {
181
+ if (state.phase !== "idle")
182
+ return;
183
+ armTerminalWatcher();
184
+ arm(0);
185
+ },
186
+ pause() {
187
+ if (state.phase !== "running")
188
+ return;
189
+ const elapsed = opts.now() - state.startedAt + state.baseline;
190
+ opts.clearTimer(state.timer);
191
+ state = { phase: "paused", baseline: elapsed };
192
+ },
193
+ resume() {
194
+ if (state.phase !== "paused")
195
+ return;
196
+ arm(state.baseline);
197
+ },
198
+ stop() {
199
+ if (state.phase === "running") {
200
+ opts.clearTimer(state.timer);
201
+ }
202
+ finaliseTerminalWatcher();
203
+ if (state.phase === "escalated")
204
+ return;
205
+ state = { phase: "stopped" };
206
+ },
207
+ accumulatedMs() {
208
+ return drainTimeoutMs - remainingMs();
209
+ },
210
+ get escalated() {
211
+ return escalated;
212
+ },
213
+ disposed() {
214
+ if (disposedPromise !== null)
215
+ return disposedPromise;
216
+ disposedPromise = (async () => {
217
+ if (escalationPending !== null) {
218
+ await escalationPending.catch(() => {
219
+ /* error already logged in `escalate`. */
220
+ });
221
+ }
222
+ if (terminalWatcherDone !== null) {
223
+ await terminalWatcherDone.catch(() => {
224
+ /* error already logged in the watcher coroutine. */
225
+ });
226
+ }
227
+ })();
228
+ return disposedPromise;
229
+ },
230
+ };
231
+ }
@@ -0,0 +1,7 @@
1
+ export { createWorkflowSupervisor, DEFAULT_TERMINAL_WRITE_WATCHDOG_MS, type CancelCommitInfo, type CancelRequestOpts, type DeliverSignalOpts, type DeliverSourcesOpts, type DrainOpts, type RecycleOpts, type SpawnOpts, type SpawnResult, type WorkflowSupervisor, } from "./supervisor.js";
2
+ export { assembleCredentialsSnapshot, defaultStepRepoId, hashGrants, STEP_GRANTS_PATH, STEP_GRANTS_REF, type AssembleCredentialsSnapshotOpts, type CredentialsSnapshot, type CredentialsSnapshotStep, type DeriveStepAddress, type DeriveStepRepoId, } from "./credentials.js";
3
+ export { commitCancelRequested, SUPERVISOR_PRINCIPAL_KIND, type CommitCancelRequestedOpts, type CommitCancelRequestedResult, } from "./cancel-signing.js";
4
+ export { createDrainTimeoutAccumulator, DEFAULT_DRAIN_TIMEOUT_MS, type DrainTimeoutAccumulator, type DrainTimeoutAccumulatorFactory, type DrainTimeoutOpts, } from "./drain-timeout.js";
5
+ export { DEFAULT_KILL_TIMEOUT_MS } from "./child-termination.js";
6
+ export { createRecyclePolicy, triggerRecycle, DEFAULT_POLICY_INTERVAL_MS, MAX_BUFFERED_MAIL, type ChildWiring, type RecycleAttempt, type RecycleContext, type RecycleOrigin, type RecyclePolicy, type RecyclePolicyBounds, type RecyclePolicyOpts, type TriggerRecycleOpts, } from "./recycle.js";
7
+ export type { DeriveMailAuditRef, DispatchStructuralCounters, DispatchSubstrateLeg, DispatchTimingMark, InboxPrimitives, MailAuditRef, MailBusBindings, PrincipalSigner, SignedPayload, SubprocessHandle, SubprocessSpawner, TerminalEventSource, TerminalRunEvent, WorkflowSupervisorBindings, WorkflowSupervisorPrincipalKind, } from "./types.js";
@@ -0,0 +1,6 @@
1
+ export { createWorkflowSupervisor, DEFAULT_TERMINAL_WRITE_WATCHDOG_MS, } from "./supervisor.js";
2
+ export { assembleCredentialsSnapshot, defaultStepRepoId, hashGrants, STEP_GRANTS_PATH, STEP_GRANTS_REF, } from "./credentials.js";
3
+ export { commitCancelRequested, SUPERVISOR_PRINCIPAL_KIND, } from "./cancel-signing.js";
4
+ export { createDrainTimeoutAccumulator, DEFAULT_DRAIN_TIMEOUT_MS, } from "./drain-timeout.js";
5
+ export { DEFAULT_KILL_TIMEOUT_MS } from "./child-termination.js";
6
+ export { createRecyclePolicy, triggerRecycle, DEFAULT_POLICY_INTERVAL_MS, MAX_BUFFERED_MAIL, } from "./recycle.js";
@@ -0,0 +1,212 @@
1
+ import { type ControlChannelSender, type ControlPayload, type EventPayload } from "../ipc/index.js";
2
+ import { type CredentialsSnapshot } from "./credentials.js";
3
+ import type { SubprocessHandle, WorkflowSupervisorBindings } from "./types.js";
4
+ /**
5
+ * Bound on the supervisor's mail buffer across the kill/respawn gap.
6
+ * A real workflow's inbound rate is well below this; saturation
7
+ * indicates either an upstream stuck on the deployment or a recycle
8
+ * stuck partway through. Either case is one the operator must see.
9
+ */
10
+ export declare const MAX_BUFFERED_MAIL = 256;
11
+ /**
12
+ * Default supervisor-policy check interval. The policy thread wakes
13
+ * roughly every minute, evaluates the configured bounds against the
14
+ * live child, and triggers a recycle if any threshold has been
15
+ * crossed. Operator-overridable via the supervisor's policy bindings.
16
+ */
17
+ export declare const DEFAULT_POLICY_INTERVAL_MS = 60000;
18
+ /**
19
+ * Origin tag the recycle path stamps onto its log messages so an
20
+ * operator scanning logs can distinguish operator-initiated,
21
+ * policy-initiated, and self-initiated recycles at a glance.
22
+ */
23
+ export type RecycleOrigin = "operator" | "policy" | "self";
24
+ export interface RecycleAttempt {
25
+ /** Origin the recycle was initiated from. */
26
+ readonly origin: RecycleOrigin;
27
+ /** Human-readable reason carried with the recycle through to the audit log. */
28
+ readonly reason: string;
29
+ /** ChannelId the recycled child was minted with. */
30
+ readonly newChannelId: string;
31
+ /** ChannelId the previous child was running under. */
32
+ readonly previousChannelId: string;
33
+ }
34
+ /**
35
+ * Per-handle subprocess wiring the recycle path owns. The supervisor
36
+ * passes the live child's wiring on entry; the recycle path replaces
37
+ * it with the freshly-spawned child's wiring before returning.
38
+ */
39
+ export interface ChildWiring {
40
+ handle: SubprocessHandle;
41
+ controlSender: ControlChannelSender;
42
+ channelId: string;
43
+ eventPump: Promise<void>;
44
+ }
45
+ /**
46
+ * Bindings the supervisor passes into `triggerRecycle`. The shape
47
+ * mirrors the subset of supervisor state the recycle sequence
48
+ * touches; it intentionally does NOT include the supervisor's mail-
49
+ * subscription disposer (the supervisor holds the registration across
50
+ * the recycle) nor the mail-bus binding itself (the recycle path does
51
+ * not re-register).
52
+ */
53
+ export interface RecycleContext {
54
+ /** The supervisor's full bindings, reused on respawn for credentials and spawn. */
55
+ readonly bindings: WorkflowSupervisorBindings;
56
+ /** Step ids in this deployment's `stepOrder` for credentials re-assembly. */
57
+ readonly stepOrder: readonly string[];
58
+ /** Definition hash carried on respawn env (unchanged across recycle). */
59
+ readonly definitionHash: string;
60
+ /**
61
+ * Warm-keep flag carried on the respawn env (design §3b). Unchanged
62
+ * across recycle: the respawned child rebuilds its empty warm-agent
63
+ * cache lazily on the next message, so the deterministic warm-keep
64
+ * decision must survive the respawn rather than be re-derived.
65
+ */
66
+ readonly warmKeep: boolean;
67
+ /** Forward target for InferenceEvents the new child publishes. */
68
+ readonly onInferenceEvent: (event: EventPayload) => void;
69
+ /** Live child wiring on entry; replaced before return. */
70
+ readonly current: ChildWiring;
71
+ /** Supervisor-side drain primitive; sends the existing drain mail. */
72
+ readonly drain: (deadlineMs: number) => Promise<void>;
73
+ /**
74
+ * Replay any `processing/` claim-check entries for the deployment's
75
+ * mail address back to `inbox/` so the FIFO ordering survives the
76
+ * recycle. Invoked AFTER the drain step settles and BEFORE the kill
77
+ * step lands, which eliminates the race window where a processing
78
+ * entry would exist with no owner. The supervisor closes this
79
+ * callback over its `inboxPrimitives.replayProcessingToInbox` plus
80
+ * the deployment's substrate principal and repo identity.
81
+ */
82
+ readonly replayProcessingToInbox: () => Promise<void>;
83
+ /**
84
+ * Abort the prior cohort's terminal source and wake the dispatch
85
+ * loop so it exits before the kill step lands. Invoked AFTER drain
86
+ * and replay settle and BEFORE the kill -- earlier would starve the
87
+ * drain step's accumulators of live terminal events; later would
88
+ * race the kill against the dispatch loop's next iteration.
89
+ */
90
+ readonly abortPriorCohort: () => void;
91
+ /**
92
+ * Onward sink the supervisor uses to install the new child wiring
93
+ * once the freshly-spawned child has emitted `ready` and the
94
+ * credentialsSnapshot has been re-assembled.
95
+ */
96
+ readonly installNewChild: (next: {
97
+ wiring: ChildWiring;
98
+ credentialsSnapshot: CredentialsSnapshot;
99
+ /**
100
+ * Live upstream control iterator the new child's receiver
101
+ * yields. The supervisor's upstream-control pump consumes the
102
+ * iterator after `installNewChild` returns so child-initiated
103
+ * `recycle.request` frames on the new wiring continue to funnel
104
+ * through `triggerRecycle`.
105
+ */
106
+ controlIncoming: AsyncGenerator<ControlPayload, void, void>;
107
+ }) => void;
108
+ /**
109
+ * Crash hook the new child's IPC channels wire to. Identical shape
110
+ * to the supervisor's spawn-time onCrash so a frame violation on the
111
+ * recycled wiring tears the deployment down through the same path.
112
+ */
113
+ readonly onCrash: (reason: string) => void;
114
+ /**
115
+ * Optional kill-timeout override (ms). Defaults to
116
+ * `DEFAULT_KILL_TIMEOUT_MS`.
117
+ */
118
+ readonly killTimeoutMs?: number;
119
+ /**
120
+ * Deadline (ms) for the respawned child's `ready` handshake, matching
121
+ * the bound the spawn path applies. The supervisor resolves the
122
+ * effective value at its edge (`bindings.readyTimeoutMs ??
123
+ * DEFAULT_READY_TIMEOUT_MS`) and passes it through; the `??` fallback
124
+ * here only fires for a direct test caller.
125
+ */
126
+ readonly readyTimeoutMs?: number;
127
+ /**
128
+ * Optional drain deadline (ms) used in step 1. The supervisor's own
129
+ * drainTimeout accumulator escalates separately; this deadline is
130
+ * the wait the recycle path itself observes before proceeding to
131
+ * step 2. Defaults to the drain accumulator's default.
132
+ */
133
+ readonly drainDeadlineMs?: number;
134
+ /**
135
+ * Optional setTimer/clearTimer pair used by the SIGKILL escalation
136
+ * wait. Production wires `setTimeout`/`clearTimeout`; tests inject
137
+ * a deterministic timer so the SIGKILL window is observable.
138
+ */
139
+ readonly setTimer?: (cb: () => void, ms: number) => unknown;
140
+ readonly clearTimer?: (handle: unknown) => void;
141
+ }
142
+ export interface TriggerRecycleOpts {
143
+ origin: RecycleOrigin;
144
+ reason: string;
145
+ }
146
+ /**
147
+ * Run the six-step recycle sequence. The function returns once the
148
+ * new child has emitted `ready` and the supervisor has drained its
149
+ * buffered mail into it; the supervisor installs the new wiring via
150
+ * `ctx.installNewChild` before that point.
151
+ */
152
+ export declare function triggerRecycle(ctx: RecycleContext, opts: TriggerRecycleOpts): Promise<RecycleAttempt>;
153
+ export interface RecyclePolicyBounds {
154
+ /**
155
+ * Maximum uptime (ms) for the workflow-process child before a
156
+ * recycle is triggered. `undefined` disables the bound.
157
+ */
158
+ maxUptimeMs?: number;
159
+ /**
160
+ * Maximum resident-set size (bytes) for the workflow-process child
161
+ * before a recycle is triggered. `undefined` disables the bound.
162
+ * The supervisor's `readRssBytes` callback is consulted on every
163
+ * policy tick; an absent callback disables the bound regardless of
164
+ * the threshold.
165
+ */
166
+ maxRssBytes?: number;
167
+ /**
168
+ * Maximum age (ms) since grants were last refreshed before a
169
+ * recycle is triggered. The supervisor's `readGrantsAgeMs` callback
170
+ * is consulted on every policy tick; an absent callback disables
171
+ * the bound regardless of the threshold.
172
+ */
173
+ maxGrantsAgeMs?: number;
174
+ }
175
+ export interface RecyclePolicyOpts {
176
+ /** Bounds the policy evaluates each tick. */
177
+ bounds: RecyclePolicyBounds;
178
+ /** Tick interval in ms. Defaults to `DEFAULT_POLICY_INTERVAL_MS`. */
179
+ intervalMs?: number;
180
+ /** Wall-clock reader; production wires `() => Date.now()`. */
181
+ now: () => number;
182
+ /** Spawn-time wall-clock the policy compares against `now()`. */
183
+ spawnedAt: number;
184
+ /** Per-tick RSS reader; absent disables the `maxRssBytes` bound. */
185
+ readRssBytes?: () => number | undefined;
186
+ /** Per-tick grants-age reader; absent disables the staleness bound. */
187
+ readGrantsAgeMs?: () => number | undefined;
188
+ /** Timer setter; production wires `setInterval`-style via `setTimer`. */
189
+ setTimer: (cb: () => void, ms: number) => unknown;
190
+ /** Timer disposer; production wires the matching `clearTimer`. */
191
+ clearTimer: (handle: unknown) => void;
192
+ /**
193
+ * Recycle entry point the policy invokes on a threshold trip. The
194
+ * supervisor's `recycle()` method wraps `triggerRecycle` and is the
195
+ * production callback.
196
+ */
197
+ trigger: (reason: string) => Promise<void>;
198
+ }
199
+ export interface RecyclePolicy {
200
+ /** Stop the timer; idempotent. */
201
+ stop(): void;
202
+ /** Evaluate the bounds once and trigger if any are tripped. */
203
+ tick(): Promise<void>;
204
+ }
205
+ /**
206
+ * Start the supervisor-policy periodic recycle check. Returns a
207
+ * handle the supervisor calls `stop()` on at shutdown. The policy is
208
+ * single-trigger per tick: even if multiple bounds are tripped on the
209
+ * same tick, exactly one `trigger` invocation lands with a reason
210
+ * naming the first tripped bound.
211
+ */
212
+ export declare function createRecyclePolicy(opts: RecyclePolicyOpts): RecyclePolicy;