@pinet/broker-core 0.2.4 → 0.2.7
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/agent-messaging.d.ts +27 -4
- package/dist/hibernation-commands.d.ts +123 -0
- package/dist/hibernation-commands.js +287 -0
- package/dist/hibernation-orchestrator.d.ts +327 -0
- package/dist/hibernation-orchestrator.js +1096 -0
- package/dist/hibernation-projection.d.ts +20 -0
- package/dist/hibernation-projection.js +60 -0
- package/dist/hibernation-status.d.ts +141 -0
- package/dist/hibernation-status.js +390 -0
- package/dist/hibernation-telemetry.d.ts +54 -0
- package/dist/hibernation-telemetry.js +119 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/leader.d.ts +134 -5
- package/dist/leader.js +359 -26
- package/dist/lifecycle.d.ts +5 -0
- package/dist/lifecycle.js +59 -0
- package/dist/mail-classification.d.ts +6 -1
- package/dist/message-send.d.ts +4 -3
- package/dist/router.d.ts +14 -1
- package/dist/router.js +15 -0
- package/dist/schema.d.ts +181 -2
- package/dist/schema.js +1243 -17
- package/dist/types.d.ts +288 -1
- package/dist/types.js +6 -0
- package/package.json +5 -5
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { BrokerDB } from "./schema.js";
|
|
2
|
+
import type { AgentInfo, RuntimeGenerationAcceptance, TmuxAgentRuntimeSpec, WakeTriggerKind } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Result of asking a follower to cooperatively checkpoint before exit. When
|
|
5
|
+
* `hibernateSafe` is false the orchestrator aborts hibernation and keeps the
|
|
6
|
+
* runtime alive rather than exiting an unsafe process.
|
|
7
|
+
*/
|
|
8
|
+
export interface HibernationCheckpointOutcome {
|
|
9
|
+
hibernateSafe: boolean;
|
|
10
|
+
reason: string | null;
|
|
11
|
+
sessionResumeRef: string | null;
|
|
12
|
+
pendingInboxCount: number;
|
|
13
|
+
rssBytes: number | null;
|
|
14
|
+
}
|
|
15
|
+
/** Everything a launch adapter needs to bring back exactly the fenced runtime. */
|
|
16
|
+
export interface RuntimeLaunchContext {
|
|
17
|
+
agentId: string;
|
|
18
|
+
stableId: string;
|
|
19
|
+
wakeLeaseId: string;
|
|
20
|
+
fenceToken: number;
|
|
21
|
+
reservedGeneration: number;
|
|
22
|
+
/** Per-attempt nonce the launched runtime must echo back on registration. */
|
|
23
|
+
reservationNonce: string;
|
|
24
|
+
correlationId: string;
|
|
25
|
+
spec: TmuxAgentRuntimeSpec;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Opaque, attempt-bound identity for a runtime a single wake attempt launched.
|
|
29
|
+
* Returned by {@link HibernationTmuxController.respawnRuntime} and required by
|
|
30
|
+
* the attempt-scoped stop/liveness checks so that retry cleanup proves the
|
|
31
|
+
* EXACT process THIS attempt spawned is gone — never the pre-hibernation runtime
|
|
32
|
+
* (whose recorded PID generation is already dead) nor a different attempt.
|
|
33
|
+
*/
|
|
34
|
+
export interface RuntimeAttemptHandle {
|
|
35
|
+
/** The reservation nonce of the launching attempt (binds the handle to it). */
|
|
36
|
+
readonly reservationNonce: string;
|
|
37
|
+
/** The tmux pane the attempt launched into (a real adapter locates the PID). */
|
|
38
|
+
readonly tmuxTarget: string;
|
|
39
|
+
/** OS pid of the launched runtime, if the adapter captured it. */
|
|
40
|
+
readonly pid: number | null;
|
|
41
|
+
}
|
|
42
|
+
/** Controls the dormant Pi runtime process (never tmux itself). */
|
|
43
|
+
export interface HibernationProcessController {
|
|
44
|
+
/** Ask the follower to flush a checkpoint and confirm hibernate safety. */
|
|
45
|
+
requestCheckpoint(spec: TmuxAgentRuntimeSpec): Promise<HibernationCheckpointOutcome>;
|
|
46
|
+
/** Gracefully stop the live Pi runtime (TERM then bounded KILL) before hibernation. */
|
|
47
|
+
stopRuntime(spec: TmuxAgentRuntimeSpec): Promise<{
|
|
48
|
+
stopped: boolean;
|
|
49
|
+
rssBytes: number | null;
|
|
50
|
+
}>;
|
|
51
|
+
/** True while the recorded Pi PID/process generation is still alive. */
|
|
52
|
+
isRuntimeAlive(spec: TmuxAgentRuntimeSpec): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Stop the runtime a SPECIFIC wake attempt launched, addressed by its
|
|
55
|
+
* attempt-bound handle (not the durable spec). Used by retry cleanup to prove
|
|
56
|
+
* the exact failed-attempt process is gone before relaunching.
|
|
57
|
+
*/
|
|
58
|
+
stopLaunchedAttempt(handle: RuntimeAttemptHandle): Promise<{
|
|
59
|
+
stopped: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
/** True while the runtime a SPECIFIC wake attempt launched is still alive. */
|
|
62
|
+
isLaunchedAttemptAlive(handle: RuntimeAttemptHandle): Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
/** Reads/writes the attachable tmux shell that outlives the Pi runtime. */
|
|
65
|
+
export interface HibernationTmuxController {
|
|
66
|
+
/** True if the recorded tmux session/pane still exists (operator-attachable). */
|
|
67
|
+
isSessionAttachable(spec: TmuxAgentRuntimeSpec): Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Launch exactly one replacement runtime into the recorded pane. The launched
|
|
70
|
+
* runtime is expected to register presenting the reservation fence. Resolves
|
|
71
|
+
* once the launch command has been issued (registration is confirmed
|
|
72
|
+
* separately via {@link BrokerDB.acceptRuntimeGeneration}). On a successful
|
|
73
|
+
* launch it returns an attempt-bound {@link RuntimeAttemptHandle} so retry
|
|
74
|
+
* cleanup can prove THIS attempt's runtime is stopped before relaunching; a
|
|
75
|
+
* launch that yields no handle is treated as unprovable (fail closed).
|
|
76
|
+
*/
|
|
77
|
+
respawnRuntime(ctx: RuntimeLaunchContext): Promise<{
|
|
78
|
+
launched: boolean;
|
|
79
|
+
handle: RuntimeAttemptHandle | null;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
export interface HibernationOrchestratorConfig {
|
|
83
|
+
/** Max time to wait for the cooperative checkpoint handshake. */
|
|
84
|
+
handshakeTimeoutMs: number;
|
|
85
|
+
/** Lease TTL for a wake operation. */
|
|
86
|
+
wakeLeaseMs: number;
|
|
87
|
+
/** Lease TTL for a hibernate operation. */
|
|
88
|
+
hibernateLeaseMs: number;
|
|
89
|
+
/** Time to wait for a launched runtime to register + accept its generation. */
|
|
90
|
+
registrationTimeoutMs: number;
|
|
91
|
+
/** Max wake attempts before quarantining as reap-candidate. */
|
|
92
|
+
maxWakeAttempts: number;
|
|
93
|
+
/** Global concurrent in-flight wakes. */
|
|
94
|
+
maxConcurrentWakes: number;
|
|
95
|
+
/** Per-repo concurrent in-flight wakes. */
|
|
96
|
+
maxConcurrentWakesPerRepo: number;
|
|
97
|
+
}
|
|
98
|
+
export declare const DEFAULT_ORCHESTRATOR_CONFIG: HibernationOrchestratorConfig;
|
|
99
|
+
export interface HibernationOrchestratorDeps {
|
|
100
|
+
db: BrokerDB;
|
|
101
|
+
process: HibernationProcessController;
|
|
102
|
+
tmux: HibernationTmuxController;
|
|
103
|
+
brokerInstanceId: string;
|
|
104
|
+
config?: Partial<HibernationOrchestratorConfig>;
|
|
105
|
+
/** Injectable clock (ms). Defaults to Date.now. */
|
|
106
|
+
now?: () => number;
|
|
107
|
+
/** Injectable id generator. Defaults to crypto.randomUUID. */
|
|
108
|
+
newId?: () => string;
|
|
109
|
+
/**
|
|
110
|
+
* Confirm the launched runtime registered and its generation was accepted.
|
|
111
|
+
* Resolves true once accepted, false on timeout. Injected so tests can drive
|
|
112
|
+
* registration deterministically; the runtime wires this to the socket server.
|
|
113
|
+
*/
|
|
114
|
+
awaitRuntimeRegistration?: (ctx: RuntimeLaunchContext) => Promise<boolean>;
|
|
115
|
+
}
|
|
116
|
+
export interface HibernateResult {
|
|
117
|
+
ok: boolean;
|
|
118
|
+
agentId: string;
|
|
119
|
+
correlationId: string;
|
|
120
|
+
state: string;
|
|
121
|
+
reason: string;
|
|
122
|
+
rssBytesBefore?: number | null;
|
|
123
|
+
rssBytesAfter?: number | null;
|
|
124
|
+
durationMs?: number;
|
|
125
|
+
}
|
|
126
|
+
export interface WakeResult {
|
|
127
|
+
ok: boolean;
|
|
128
|
+
agentId: string;
|
|
129
|
+
correlationId: string;
|
|
130
|
+
state: string;
|
|
131
|
+
reason: string;
|
|
132
|
+
runtimeGeneration?: number;
|
|
133
|
+
attempts?: number;
|
|
134
|
+
durationMs?: number;
|
|
135
|
+
}
|
|
136
|
+
/** Outcome of reconciling one agent/queue-row stranded by a broker crash. */
|
|
137
|
+
export interface StrandedWakeRecovery {
|
|
138
|
+
/** Agent id, or (for `requeued`) the orphaned dispatch row's agent id. */
|
|
139
|
+
agentId: string;
|
|
140
|
+
/**
|
|
141
|
+
* - `completed` — a stranded `waking` whose generation was already accepted; only the
|
|
142
|
+
* final live transition was lost, so it was finished to `live`.
|
|
143
|
+
* - `quarantined` — a stranded `waking` or `hibernating` with an uncertain runtime; moved
|
|
144
|
+
* to `reap-candidate` for manual review rather than risk a double launch.
|
|
145
|
+
* - `requeued` — a `dispatching` wake-queue row orphaned mid-dispatch by a crash;
|
|
146
|
+
* returned to `queued` so a fresh dispatch pass can pick it up.
|
|
147
|
+
*/
|
|
148
|
+
action: "completed" | "quarantined" | "requeued";
|
|
149
|
+
}
|
|
150
|
+
export declare function wakeTriggerPriority(kind: WakeTriggerKind): number;
|
|
151
|
+
/**
|
|
152
|
+
* Broker-managed hibernation lifecycle orchestrator.
|
|
153
|
+
*
|
|
154
|
+
* Composes the durable primitives in {@link BrokerDB} (fenced leases, CAS
|
|
155
|
+
* lifecycle transitions, runtime specs, checkpoint receipts, generation
|
|
156
|
+
* reservations, wake queue, telemetry events) with injected process/tmux
|
|
157
|
+
* adapters. Every path fails closed: on any anomaly it releases its lease and
|
|
158
|
+
* quarantines the agent as `reap-candidate` with an actionable reason rather
|
|
159
|
+
* than guessing, rerouting affinity work, or killing a PID on PID alone.
|
|
160
|
+
*/
|
|
161
|
+
export declare class HibernationOrchestrator {
|
|
162
|
+
private readonly db;
|
|
163
|
+
private readonly process;
|
|
164
|
+
private readonly tmux;
|
|
165
|
+
private readonly brokerInstanceId;
|
|
166
|
+
private readonly config;
|
|
167
|
+
private readonly now;
|
|
168
|
+
private readonly newId;
|
|
169
|
+
private readonly awaitRuntimeRegistration;
|
|
170
|
+
constructor(deps: HibernationOrchestratorDeps);
|
|
171
|
+
/**
|
|
172
|
+
* Advance an eligible, free, broker-managed agent through `grace` to `idle`
|
|
173
|
+
* so it becomes hibernation-ready. Idempotent and fenced by CAS. Used by both
|
|
174
|
+
* the manual `pinet hibernate` path and the auto scheduler. Never forces a
|
|
175
|
+
* working/unsafe/ineligible agent forward.
|
|
176
|
+
*/
|
|
177
|
+
prepareHibernation(agentId: string, opts?: {
|
|
178
|
+
reason?: string;
|
|
179
|
+
actor?: string;
|
|
180
|
+
correlationId?: string;
|
|
181
|
+
}): {
|
|
182
|
+
ready: boolean;
|
|
183
|
+
state: string;
|
|
184
|
+
reason: string;
|
|
185
|
+
};
|
|
186
|
+
hibernate(agentId: string, opts?: {
|
|
187
|
+
reason?: string;
|
|
188
|
+
actor?: string;
|
|
189
|
+
trigger?: string;
|
|
190
|
+
correlationId?: string;
|
|
191
|
+
}): Promise<HibernateResult>;
|
|
192
|
+
/**
|
|
193
|
+
* Cold-wake a hibernated agent as a single accepted runtime generation, then
|
|
194
|
+
* transition to `live` so the durable inbox drains in order. Concurrent
|
|
195
|
+
* triggers contend on the fenced wake lease; only one wins.
|
|
196
|
+
*/
|
|
197
|
+
wake(agentId: string, opts?: {
|
|
198
|
+
reason?: string;
|
|
199
|
+
actor?: string;
|
|
200
|
+
trigger?: WakeTriggerKind;
|
|
201
|
+
correlationId?: string;
|
|
202
|
+
}): Promise<WakeResult>;
|
|
203
|
+
/**
|
|
204
|
+
* Best-effort proof that a launched wake ATTEMPT's runtime is gone, addressed
|
|
205
|
+
* by its attempt-bound handle (never the durable spec). Fail-closed: a missing
|
|
206
|
+
* handle, an unconfirmed stop, a still-alive probe, OR any adapter throw all
|
|
207
|
+
* count as "not proven gone", so the caller quarantines (`wake_ambiguous_launch`)
|
|
208
|
+
* rather than relaunch on / strand a possibly-live runtime.
|
|
209
|
+
*/
|
|
210
|
+
private proveAttemptStopped;
|
|
211
|
+
/**
|
|
212
|
+
* Race-free, TRI-STATE settle of a wake attempt against the acceptance
|
|
213
|
+
* boundary. Delegates to the transactional {@link BrokerDB.finalizeWakeAttempt}:
|
|
214
|
+
*
|
|
215
|
+
* - `"accepted"` — the socket already accepted our generation. The runtime is
|
|
216
|
+
* live+bound; it must be promoted, NEVER stopped.
|
|
217
|
+
* - `"fenced-unaccepted"` — the settle transaction COMMITTED a consumption of
|
|
218
|
+
* THIS attempt's exact-nonce reservation, so the launched runtime can never
|
|
219
|
+
* be accepted afterwards. Only now is a subsequent prove-stop safe.
|
|
220
|
+
* - `"unknown"` — the settle transaction threw and did NOT commit, so the
|
|
221
|
+
* reservation was NOT provably consumed. An acceptance may still race an
|
|
222
|
+
* async prove-stop, so the caller must NOT stop or retry the attempt; it
|
|
223
|
+
* leaves the identity `waking` for `recoverStrandedWakes` to reconcile once
|
|
224
|
+
* the atomic settle can commit. A best-effort read here would be unsafe:
|
|
225
|
+
* reading "not accepted" does not prevent a concurrent acceptance, so we
|
|
226
|
+
* must never downgrade `unknown` to `fenced-unaccepted`.
|
|
227
|
+
*/
|
|
228
|
+
private settleWakeAttempt;
|
|
229
|
+
/**
|
|
230
|
+
* Finalize an ACCEPTED wake to `live`. Acceptance is irreversible: the socket
|
|
231
|
+
* bound this runtime to our exact lease/fence/reservation and atomically
|
|
232
|
+
* advanced+consumed the generation, so the runtime is live+connected. The
|
|
233
|
+
* `waking -> live` promotion is pure bookkeeping — driven with an unfenced
|
|
234
|
+
* administrative CAS (a lease that expired *after* acceptance must not throw a
|
|
235
|
+
* fenced transition and quarantine an already-live runtime) and GUARDED so any
|
|
236
|
+
* post-acceptance DB fault (transition, inbox count, wake completion) leaves the
|
|
237
|
+
* identity in `waking` (`woken_recovery_pending`) for `recoverStrandedWakes` to
|
|
238
|
+
* finish to `live`, rather than quarantining a live worker.
|
|
239
|
+
*/
|
|
240
|
+
private promoteAcceptedWake;
|
|
241
|
+
/**
|
|
242
|
+
* Attempt to accept a launched runtime's generation on registration. Called
|
|
243
|
+
* by the socket server (or the injected registration waiter). Idempotent and
|
|
244
|
+
* fenced: only the reservation's exact lease/fence/generation is accepted.
|
|
245
|
+
*/
|
|
246
|
+
acceptRuntimeRegistration(input: {
|
|
247
|
+
agentId: string;
|
|
248
|
+
wakeLeaseId: string;
|
|
249
|
+
fenceToken: number;
|
|
250
|
+
reservedGeneration: number;
|
|
251
|
+
reservationNonce: string;
|
|
252
|
+
}): RuntimeGenerationAcceptance;
|
|
253
|
+
/**
|
|
254
|
+
* Reconcile lifecycle + wake-queue state left inconsistent by a broker crash.
|
|
255
|
+
* Intended to run once on broker startup (and is safe to re-run). DB-only and
|
|
256
|
+
* idempotent. Three classes of strand are repaired:
|
|
257
|
+
*
|
|
258
|
+
* 1. Agents in `waking` (crash between generation acceptance and the final
|
|
259
|
+
* `waking -> live` transition):
|
|
260
|
+
* - If a runtime already accepted its generation (reservation consumed and
|
|
261
|
+
* runtime_generation advanced past the checkpoint's generation) only the
|
|
262
|
+
* final live transition was lost → complete to `live` so the inbox drains.
|
|
263
|
+
* - Otherwise the wake outcome is uncertain (a runtime may or may not have
|
|
264
|
+
* launched) → fail closed to `reap-candidate` for manual review.
|
|
265
|
+
* 2. Agents in `hibernating` (crash mid-hibernate, before reaching the durable
|
|
266
|
+
* `hibernated` state): the runtime may or may not have been torn down, so
|
|
267
|
+
* completing to `hibernated` risks a double launch on the next wake → fail
|
|
268
|
+
* closed to `reap-candidate` for manual review.
|
|
269
|
+
* 3. Wake-queue rows left in `dispatching` (crash mid-dispatch): the owning
|
|
270
|
+
* dispatch loop is gone, so return them to `queued` (they also block the
|
|
271
|
+
* unique active-agent index until reclaimed) so a fresh pass re-dispatches.
|
|
272
|
+
*
|
|
273
|
+
* Only a lease held by THIS live broker instance causes a skip (we are still
|
|
274
|
+
* actively driving that operation). A lease owned by a *different* instance is
|
|
275
|
+
* orphaned from a prior, now-dead broker — a crash normally leaves precisely
|
|
276
|
+
* such an unexpired-but-orphaned lease — so it is reconciled immediately rather
|
|
277
|
+
* than waiting out its TTL (during which the row would otherwise be stranded).
|
|
278
|
+
*/
|
|
279
|
+
recoverStrandedWakes(opts?: {
|
|
280
|
+
now?: number;
|
|
281
|
+
}): StrandedWakeRecovery[];
|
|
282
|
+
/**
|
|
283
|
+
* Enqueue a wake trigger for a hibernated agent. Idempotent per agent and
|
|
284
|
+
* priority-ordered (targeted work first). Never fans out to broadcast.
|
|
285
|
+
*/
|
|
286
|
+
enqueueWakeTrigger(input: {
|
|
287
|
+
agentId: string;
|
|
288
|
+
triggerKind: WakeTriggerKind;
|
|
289
|
+
reason: string;
|
|
290
|
+
triggerMessageId?: number | null;
|
|
291
|
+
correlationId?: string;
|
|
292
|
+
}): void;
|
|
293
|
+
/**
|
|
294
|
+
* Dispatch queued wakes respecting global and per-repo concurrency limits,
|
|
295
|
+
* in priority then oldest-first order. Returns the results of wakes started
|
|
296
|
+
* this pass. Safe to call repeatedly (e.g. on a timer or after each trigger).
|
|
297
|
+
*/
|
|
298
|
+
dispatchWakeQueue(): Promise<WakeResult[]>;
|
|
299
|
+
private selectNextDispatchableWake;
|
|
300
|
+
private defaultAwaitRegistration;
|
|
301
|
+
private refuseHibernate;
|
|
302
|
+
private refuseWake;
|
|
303
|
+
private recordRefusal;
|
|
304
|
+
/**
|
|
305
|
+
* Drive a fenced lifecycle transition bound to the *live* held lease. Passing
|
|
306
|
+
* the full lease identity (fence + id + operation + current time) lets the DB
|
|
307
|
+
* reject an expired, superseded, or wrong-operation lease rather than trusting
|
|
308
|
+
* the fence token alone. `now` is read fresh per call so a lease that expires
|
|
309
|
+
* mid-operation cannot authorize a later transition.
|
|
310
|
+
*/
|
|
311
|
+
private transitionFenced;
|
|
312
|
+
/**
|
|
313
|
+
* Fail-closed *safety* transition to a quarantine/abort state. Unlike a
|
|
314
|
+
* forward-progress transition this is deliberately UNFENCED: it must be able
|
|
315
|
+
* to fire even when our own lease has expired mid-operation (e.g. a wake whose
|
|
316
|
+
* cumulative adapter waits outran the lease TTL), otherwise the agent would be
|
|
317
|
+
* stranded in `waking`/`hibernating`. Safety is preserved by the version CAS
|
|
318
|
+
* inside `transitionAgentLifecycle`: if another broker legitimately advanced
|
|
319
|
+
* the agent (bumping the version) our recovery CAS fails and we do not clobber
|
|
320
|
+
* it; if nobody else touched it, we move it to the safe state.
|
|
321
|
+
*/
|
|
322
|
+
private transitionAdministrative;
|
|
323
|
+
private quarantine;
|
|
324
|
+
private quarantineWake;
|
|
325
|
+
}
|
|
326
|
+
/** Convenience: derive whether an agent is a durable hibernation identity. */
|
|
327
|
+
export declare function isDurableHibernationState(agent: AgentInfo): boolean;
|