@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.
- package/LICENSE +176 -0
- package/README.md +287 -0
- package/dist/adapters/blob-substrate.d.ts +49 -0
- package/dist/adapters/blob-substrate.js +140 -0
- package/dist/adapters/repo-store.d.ts +39 -0
- package/dist/adapters/repo-store.js +344 -0
- package/dist/adapters/spawn-child.d.ts +74 -0
- package/dist/adapters/spawn-child.js +152 -0
- package/dist/adapters/step-invoker.d.ts +114 -0
- package/dist/adapters/step-invoker.js +360 -0
- package/dist/child/env-bootstrap.d.ts +56 -0
- package/dist/child/env-bootstrap.js +120 -0
- package/dist/child/from-process-env.d.ts +127 -0
- package/dist/child/from-process-env.js +183 -0
- package/dist/child/index.d.ts +9 -0
- package/dist/child/index.js +9 -0
- package/dist/child/outbound-mail-bridge.d.ts +36 -0
- package/dist/child/outbound-mail-bridge.js +143 -0
- package/dist/child/proxy-repo-store.d.ts +27 -0
- package/dist/child/proxy-repo-store.js +200 -0
- package/dist/child/run-child.d.ts +320 -0
- package/dist/child/run-child.js +900 -0
- package/dist/child/self-discovery.d.ts +29 -0
- package/dist/child/self-discovery.js +57 -0
- package/dist/child/substrate-write-bridge.d.ts +72 -0
- package/dist/child/substrate-write-bridge.js +188 -0
- package/dist/child/supervisor-backed-transport.d.ts +10 -0
- package/dist/child/supervisor-backed-transport.js +113 -0
- package/dist/child/warm-agent-cache.d.ts +78 -0
- package/dist/child/warm-agent-cache.js +112 -0
- package/dist/drain-controller.d.ts +37 -0
- package/dist/drain-controller.js +46 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/ipc/control-channel.d.ts +336 -0
- package/dist/ipc/control-channel.js +532 -0
- package/dist/ipc/crypto.d.ts +46 -0
- package/dist/ipc/crypto.js +126 -0
- package/dist/ipc/envelope.d.ts +53 -0
- package/dist/ipc/envelope.js +88 -0
- package/dist/ipc/event-channel.d.ts +677 -0
- package/dist/ipc/event-channel.js +278 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +143 -0
- package/dist/mail-bus/hub-transport-adapter.d.ts +30 -0
- package/dist/mail-bus/hub-transport-adapter.js +76 -0
- package/dist/mail-bus/index.d.ts +1 -0
- package/dist/mail-bus/index.js +1 -0
- package/dist/seams/index.d.ts +3 -0
- package/dist/seams/index.js +3 -0
- package/dist/seams/scheduler-adapter.d.ts +3 -0
- package/dist/seams/scheduler-adapter.js +24 -0
- package/dist/seams/scheduler.d.ts +94 -0
- package/dist/seams/scheduler.js +397 -0
- package/dist/seams/signal-channel.d.ts +74 -0
- package/dist/seams/signal-channel.js +304 -0
- package/dist/supervisor/cancel-signing.d.ts +68 -0
- package/dist/supervisor/cancel-signing.js +144 -0
- package/dist/supervisor/child-termination.d.ts +51 -0
- package/dist/supervisor/child-termination.js +76 -0
- package/dist/supervisor/credentials.d.ts +101 -0
- package/dist/supervisor/credentials.js +153 -0
- package/dist/supervisor/dispatch-attribution.d.ts +37 -0
- package/dist/supervisor/dispatch-attribution.js +114 -0
- package/dist/supervisor/drain-timeout.d.ts +127 -0
- package/dist/supervisor/drain-timeout.js +231 -0
- package/dist/supervisor/index.d.ts +7 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/recycle.d.ts +212 -0
- package/dist/supervisor/recycle.js +440 -0
- package/dist/supervisor/run-event-compaction.d.ts +34 -0
- package/dist/supervisor/run-event-compaction.js +115 -0
- package/dist/supervisor/spawn-env.d.ts +39 -0
- package/dist/supervisor/spawn-env.js +36 -0
- package/dist/supervisor/supervisor.d.ts +202 -0
- package/dist/supervisor/supervisor.js +2244 -0
- package/dist/supervisor/terminal-broadcaster.d.ts +45 -0
- package/dist/supervisor/terminal-broadcaster.js +184 -0
- package/dist/supervisor/types.d.ts +542 -0
- package/dist/supervisor/types.js +10 -0
- 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;
|