@norskvideo/norsk-auto-manager 0.1.0

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.
@@ -0,0 +1,1070 @@
1
+ import * as ManagerPB from "@norskvideo/norsk-api/lib/manager_pb";
2
+ import { Clock } from "./clock";
3
+ import { Bundle as TypedBundle, BundleId, NodeInventory as TypedNodeInventory, NodeService, PriorityBand, ServiceId } from "./types";
4
+ import { ValidationError, ValidationResult } from "./validation";
5
+ import { PlacementFailureReason, PlacementPool, PlacementResult, PlacementTrace, ResilienceFlag } from "./placement";
6
+ import { NorskManager, JobId, JobInfo, JobWithHistory, NodeId, NodeMetadata, Role, RunningNodeMetadata } from "@norskvideo/norsk-manager-sdk";
7
+ import type * as ManagerSdk from "@norskvideo/norsk-manager-sdk";
8
+ export type NodeState = "starting" | "running" | "stopping" | "stopped" | "terminating" | "terminated";
9
+ export type NodeSummary = {
10
+ nodeId: NodeId;
11
+ nodeMetadata?: NodeMetadata;
12
+ runningNodeMetadata?: RunningNodeMetadata;
13
+ nodeState: NodeState;
14
+ lastActivity: Date;
15
+ provisional?: {
16
+ jobId: JobId;
17
+ role: Role;
18
+ };
19
+ };
20
+ export interface AutoSettings {
21
+ url?: string;
22
+ /**
23
+ * Abort the connection / run. Aborting before `run()` connects rejects it
24
+ * (so a connect-with-timeout retry loop can give up cleanly without
25
+ * leaving an abandoned SDK connection alive); aborting a running
26
+ * AutoManager tears down its Manager connection, ending the run loop.
27
+ */
28
+ signal?: AbortSignal;
29
+ /** Seconds-from-now window for jobs to be considered pending in the eventStream. */
30
+ pendingWindow: number;
31
+ /** Seconds stopped nodes persist before being terminated. */
32
+ removeStoppedNodesAfter: number;
33
+ /** Configured pools; a bundle names one, placement walks its tiers. */
34
+ placementPools: PlacementPool[];
35
+ /**
36
+ * Window (ms) for counting jobRejected events per-node. Default 5_000.
37
+ * Once `rejectionEscalationCount` rejections fall inside this window the
38
+ * node is marked "unhealthy" and excluded from placement until the next
39
+ * `nodeInventoryUpdated` event clears the mark.
40
+ */
41
+ rejectionBackoffMs?: number;
42
+ /** Default 3. */
43
+ rejectionEscalationCount?: number;
44
+ /**
45
+ * After a node fails (`nodeStopping`), how long to keep it excluded from
46
+ * placement. Default 30_000 ms. Cleared by a subsequent
47
+ * `nodeInventoryUpdated` event reporting the node as reachable again.
48
+ */
49
+ failureBackoffMs?: number;
50
+ /**
51
+ * Phase C2: per-replica restart-rate ceiling on the recovery path.
52
+ * When a replica's restart count in the trailing hour exceeds this,
53
+ * AutoManager stops re-placing it (and every other replica of the
54
+ * same bundle), marks the bundle broken, and fires `onBundleBroken`.
55
+ * Cleared when the bundle is updated or deleted, or when a fresh
56
+ * `jobUpdated` arrives for the broken job. Default 6 (per design).
57
+ */
58
+ maxRestartsPerHour?: number;
59
+ /**
60
+ * Phase C3: node startup grace. If a node stays in `nodeStarting`
61
+ * for longer than this, AutoManager terminates it — Manager emits a
62
+ * `nodeStopping` and the existing band-aware recovery path replaces
63
+ * the job. Default 120_000 ms.
64
+ *
65
+ * (In v1, "job in starting" and "node in starting" collapse onto
66
+ * the same signal because `running → ready` is auto-marked. The
67
+ * design's distinction matters when explicit readiness lands.)
68
+ */
69
+ jobStartupGraceMs?: number;
70
+ /**
71
+ * Phase C4 — per-band recovery policy. When set, takes precedence
72
+ * over the flat fields above. Bronze always drops (`onFailure:
73
+ * "drop"`) — no per-band restart options. Gold and silver get
74
+ * their own `maxRestartsPerHour`; gold additionally has
75
+ * `restartPlacement: "preferHotSpare"` baked in by the recovery
76
+ * path. Omitted bands fall back to the flat top-level fields, then
77
+ * to defaults.
78
+ */
79
+ bands?: BandConfig;
80
+ /**
81
+ * Phase C4 — placement / recovery tunables grouped into one typed
82
+ * object. Same precedence as `bands`: object field beats flat field
83
+ * beats default. The flat fields above are preserved for one
84
+ * release; new code should prefer this shape.
85
+ */
86
+ placement?: PlacementConfig;
87
+ /**
88
+ * Phase C6 — hot-spare reconciliation configuration. One entry per
89
+ * (pool, optional band). The reconciler runs on node lifecycle
90
+ * events and on a periodic tick: counts live spares per entry,
91
+ * provisions short, terminates excess. Empty/omitted = no spares.
92
+ */
93
+ hotSpares?: HotSpareConfig[];
94
+ /**
95
+ * Phase F — NodeService configurations. Each entry describes a
96
+ * long-running service that consumes capabilities (typically
97
+ * exclusive hardware) and advertises new ones for sibling jobs
98
+ * to consume. AutoManager's reconciler places eager services on
99
+ * every eligible node and tears them down when a node loses
100
+ * eligibility.
101
+ *
102
+ * Worker-side capability advertisement is required for sibling
103
+ * jobs to actually see the provided capabilities — see
104
+ * `§Worker-side work` in the design doc.
105
+ */
106
+ nodeServices?: NodeService[];
107
+ /**
108
+ * Phase F — how often the NodeService reconciler's periodic tick
109
+ * fires. Event-driven reconciles handle most cases; the tick is a
110
+ * safety net. Default 60_000 ms.
111
+ */
112
+ nodeServiceReconcileIntervalMs?: number;
113
+ /**
114
+ * Phase C6 — how often the spare reconciler's periodic tick fires.
115
+ * Event-driven reconciles handle most cases; the tick is a safety
116
+ * net for missed signals. Default 60_000 ms.
117
+ */
118
+ hotSpareReconcileIntervalMs?: number;
119
+ /**
120
+ * Phase E — per-phase deadline configuration for in-flight
121
+ * migrations. If a migration sits in a phase longer than the
122
+ * configured window, AutoManager auto-aborts with reason
123
+ * `phase timeout: <phase>`. Operators can still abort manually via
124
+ * `abortMigration` before the deadline fires.
125
+ */
126
+ migration?: MigrationConfig;
127
+ onError?: (err: unknown) => void;
128
+ onNodeStarting?: (node: NodeId) => void;
129
+ onNodeStarted?: (node: NodeSummary) => void;
130
+ /** Fired when the daemon emits a NodeStopping event — the node is
131
+ * going away but may not have fully terminated yet. For cluster
132
+ * workers Ctrl-C'd at the keyboard this is the signal that fires
133
+ * (NodeStopped/NodeTerminated come later, after the daemon's
134
+ * keepalive timeout or cleanup loop). Consumers wanting prompt
135
+ * "worker dropped" UI updates listen here. */
136
+ onNodeStopping?: (nodeId: NodeId) => void;
137
+ onNodeStopped?: (nodeId: NodeId) => void;
138
+ onNodeTerminated?: (nodeId: NodeId) => void;
139
+ onBundleUpdated?: (bundle: TypedBundle) => void;
140
+ onBundleDeleted?: (bundleId: BundleId) => void;
141
+ /**
142
+ * Fired when the daemon appends a new audit-log entry (any source).
143
+ * Pure pass-through of the SDK's `AuditLogEntry` — AutoManager doesn't
144
+ * own audit semantics, it just forwards the live append so consumers
145
+ * can tail the log without re-polling GetAuditLog.
146
+ */
147
+ onAuditAppended?: (entry: ManagerSdk.AuditLogEntry) => void;
148
+ /**
149
+ * Fires when a node accumulates enough rejections within
150
+ * `rejectionBackoffMs` to be marked unhealthy. Operators may want to
151
+ * surface this in monitoring.
152
+ */
153
+ onNodeUnhealthy?: (nodeId: NodeId) => void;
154
+ /**
155
+ * Fired after `jobMap` is updated in response to a `jobUpdated` or
156
+ * `jobPending` event. Carries the full `JobWithHistory` so consumers
157
+ * can render state transitions without a separate SDK round-trip.
158
+ */
159
+ onJobUpdated?: (jobWithHistory: JobWithHistory) => void;
160
+ /**
161
+ * Fired for an informational job message from the worker — launch
162
+ * progress (pulling-images / starting-containers / health), container
163
+ * events, service lifecycle. Lets consumers surface live launch feedback
164
+ * tied to a job without changing its placement state.
165
+ */
166
+ onJobInfo?: (info: JobInfo) => void;
167
+ /**
168
+ * Fired after a job is removed from `jobMap` in response to a
169
+ * `jobDeleted` or `jobOutOfWindow` event.
170
+ */
171
+ onJobDeleted?: (jobId: JobId) => void;
172
+ /**
173
+ * Fired on every `jobRejected` event after AutoManager has updated
174
+ * its inventory cache. Note the node is not necessarily marked
175
+ * unhealthy by this single occurrence — see `onNodeUnhealthy` for
176
+ * escalation.
177
+ */
178
+ onJobRejected?: (info: JobRejectedInfo) => void;
179
+ /**
180
+ * Fired on `jobConfigUpdated` after AutoManager has updated its
181
+ * mirror of the job's `managerConfiguration`.
182
+ */
183
+ onJobConfigUpdated?: (jobId: JobId, config: string) => void;
184
+ /**
185
+ * Fires every time the placement engine runs, regardless of outcome.
186
+ * Carries the full trace so consumers (e.g. an operator UI) can show
187
+ * "why did my job land here?" without re-running placement
188
+ * themselves.
189
+ */
190
+ onPlacementDecision?: (info: PlacementDecisionInfo) => void;
191
+ /**
192
+ * Fires when the placement engine returns `kind: "failure"`. Today
193
+ * the failure also surfaces via `onError` as a stringified message;
194
+ * this typed callback is the recommended path. The two are not
195
+ * mutually exclusive — `onError` is preserved for back-compat.
196
+ */
197
+ onPlacementFailed?: (info: PlacementFailedInfo) => void;
198
+ /**
199
+ * Fired whenever the underlying gRPC channel changes connectivity
200
+ * state. Maps the five gRPC connectivity states to the strings
201
+ * `idle | connecting | ready | transientFailure | shutdown`.
202
+ * Lets a consumer surface "connected / reconnecting / disconnected"
203
+ * without holding a separate NorskManager handle.
204
+ */
205
+ onConnectionStateChange?: (state: ConnectionState) => void;
206
+ /**
207
+ * Phase-C: fired when a bundle's restart rate exceeds the band's
208
+ * `maxRestartsPerHour` and AutoManager marks it broken. Declared
209
+ * now so consumers can wire the callback; not fired in pre-Phase-C
210
+ * code.
211
+ */
212
+ onBundleBroken?: (bundleId: BundleId, reason: string) => void;
213
+ /**
214
+ * Phase-C: fired when a bronze-band job fails and is dropped
215
+ * (rather than restarted). Declared now; not fired in pre-Phase-C
216
+ * code.
217
+ */
218
+ onJobDropped?: (jobId: JobId) => void;
219
+ /**
220
+ * Phase-C: fired when a hot-spare pool's live spare count is below
221
+ * its configured target. Declared now; not fired in pre-Phase-C
222
+ * code.
223
+ */
224
+ onSpareUnderflow?: (pool: string, current: number, target: number) => void;
225
+ /**
226
+ * Fired when a bundle's backup is best-effort placed inside a failure
227
+ * domain its resilience policy asked to avoid (no compliant node had
228
+ * capacity). The backup is running, but DR posture is reduced — surface
229
+ * it so an operator can act / re-place when capacity frees.
230
+ */
231
+ onResilienceDegraded?: (info: ResilienceDegradedInfo) => void;
232
+ /**
233
+ * Phase E — fired on every migration phase transition. Use for
234
+ * telemetry / ops UIs. The payload is the post-transition state;
235
+ * `phaseHistory[0]` is launch, `phaseHistory.at(-1)` is the current
236
+ * phase at the time of the callback.
237
+ */
238
+ onMigrationPhase?: (info: MigrationState) => void;
239
+ /** Phase E — fired once when a migration reaches `done`. */
240
+ onMigrationCompleted?: (migrationId: MigrationId) => void;
241
+ /** Phase E — fired once when a migration enters `aborted`. */
242
+ onMigrationAborted?: (migrationId: MigrationId, reason: string) => void;
243
+ /** Optional clock injection. Production unset (RealClock); tests pass MockClock. */
244
+ clock?: Clock;
245
+ }
246
+ /** Fixed role for the migration-target instance. No concurrent migrations per Job. @public */
247
+ export declare const MIGRATION_TARGET_ROLE = "migrationTarget";
248
+ /**
249
+ * Fixed role assigned to runtime jobs that exist to back a
250
+ * NodeService. Worker reads `WorkerJob.role === NODE_SERVICE_ROLE`
251
+ * to recognise the instance as a service rather than a regular
252
+ * workload, and adds the service's `provides[]` capabilities to its
253
+ * inventory once the job reaches `ready`.
254
+ *
255
+ * @public
256
+ */
257
+ export declare const NODE_SERVICE_ROLE = "nodeService";
258
+ /** @public */
259
+ export type MigrationId = string;
260
+ /** @public */
261
+ export type MigrationPhase = "launching" | "awaitingTargetReady" | "awaitingSourceStop" | "applying" | "done" | "aborted";
262
+ /**
263
+ * Phase E migration state, kept entirely in AutoManager. Per the
264
+ * design, "no concurrent migrations per Job" — `sourceJobId` is the
265
+ * primary index; `migrationId` is a stable opaque handle for ops
266
+ * tooling.
267
+ *
268
+ * @public
269
+ */
270
+ export interface MigrationState {
271
+ migrationId: MigrationId;
272
+ sourceJobId: JobId;
273
+ sourceRole: Role;
274
+ targetNodeId: NodeId;
275
+ startedAt: Date;
276
+ phase: MigrationPhase;
277
+ phaseHistory: {
278
+ phase: MigrationPhase;
279
+ at: Date;
280
+ }[];
281
+ /** Set when phase becomes "aborted"; undefined while in flight or on done. */
282
+ abortReason?: string;
283
+ /** Timestamp the source reported as its last-output, captured during cutover. */
284
+ cutoverTimestampNs?: number;
285
+ }
286
+ /**
287
+ * Coarse connection state surfaced by `onConnectionStateChange`. Maps
288
+ * to gRPC's `ChannelConnectivityState` values via NorskManager's
289
+ * existing watcher.
290
+ *
291
+ * @public
292
+ */
293
+ export type ConnectionState = "idle" | "connecting" | "ready" | "transientFailure" | "shutdown";
294
+ /**
295
+ * Phase C4 — per-band recovery policy. Bronze always drops on
296
+ * failure; gold and silver restart with their own restart-rate
297
+ * ceilings. Gold's `restartPlacement: "preferHotSpare"` is the
298
+ * default behaviour of the recovery path (placement engine biases
299
+ * scoring toward spare-tagged nodes when the recovering job is gold).
300
+ *
301
+ * Omitted bands fall through to the flat `AutoSettings` fields, then
302
+ * to compiled-in defaults.
303
+ *
304
+ * @public
305
+ */
306
+ export interface BandConfig {
307
+ gold?: {
308
+ onFailure: "restart";
309
+ restartPlacement: "preferHotSpare" | "any";
310
+ /** Default: 6 */
311
+ maxRestartsPerHour?: number;
312
+ };
313
+ silver?: {
314
+ onFailure: "restart";
315
+ restartPlacement: "any";
316
+ /** Default: 6 */
317
+ maxRestartsPerHour?: number;
318
+ };
319
+ bronze?: {
320
+ onFailure: "drop";
321
+ };
322
+ }
323
+ /**
324
+ * Phase C4 — placement / recovery tunables. Same fields as the flat
325
+ * top-level settings; this typed grouping is the recommended path.
326
+ * Omitted fields fall through to the flat settings, then to
327
+ * compiled-in defaults.
328
+ *
329
+ * @public
330
+ */
331
+ export interface PlacementConfig {
332
+ /** Default: 5_000 ms. */
333
+ rejectionBackoffMs?: number;
334
+ /** Default: 3. */
335
+ rejectionEscalationCount?: number;
336
+ /** Default: 30_000 ms. */
337
+ failureBackoffMs?: number;
338
+ /** Default: 120_000 ms. */
339
+ jobStartupGraceMs?: number;
340
+ }
341
+ /**
342
+ * Phase C6 — keep `targetCount` warm-running nodes ready per pool
343
+ * (and optional band). The reconciler provisions short, terminates
344
+ * excess. Cluster pools cannot auto-scale — `targetCount` there
345
+ * means "refuse to place onto a spare-tagged node unless this is a
346
+ * recovery placement"; reconciler emits `onSpareUnderflow` and leaves
347
+ * the rest to operator IT.
348
+ *
349
+ * @public
350
+ */
351
+ export interface HotSpareConfig {
352
+ pool: string;
353
+ targetCount: number;
354
+ /** Optional: dedicate spares to a specific band. */
355
+ forBand?: PriorityBand;
356
+ spec: NodeSpec;
357
+ }
358
+ /**
359
+ * Phase C6 — what kind of node the spare reconciler should
360
+ * provision. For elastic (aws/oci) pools, `instanceType` is
361
+ * required. For cluster pools, the spec is only used to constrain
362
+ * which existing cluster nodes are eligible to be tagged as spare
363
+ * (future).
364
+ *
365
+ * @public
366
+ */
367
+ export interface NodeSpec {
368
+ /** AwsInstanceType | OciShape; required for elastic pools. */
369
+ instanceType?: string;
370
+ /** Cloud-specific region or AZ; pass through to provider. */
371
+ region?: string;
372
+ az?: string;
373
+ }
374
+ /**
375
+ * Phase E — per-phase deadlines for in-flight migrations. Set any
376
+ * field to override its default; omitted fields use the compiled-in
377
+ * default below.
378
+ *
379
+ * @public
380
+ */
381
+ export interface MigrationConfig {
382
+ /** Default 120_000 ms — combined "launch + ready" window for the target. */
383
+ launchingDeadlineMs?: number;
384
+ /** Default 120_000 ms — workflow has this long after target running to signal ready. */
385
+ awaitingTargetReadyDeadlineMs?: number;
386
+ /** Default 30_000 ms — source workflow has this long to stop output after the cutover signal. */
387
+ awaitingSourceStopDeadlineMs?: number;
388
+ /** Default 10_000 ms — target workflow has this long to apply the handover. */
389
+ applyingDeadlineMs?: number;
390
+ }
391
+ /**
392
+ * Phase C4 — result of `validateBundleAdmission`. `ok` means the
393
+ * bundle passes static validation AND every (replica, jobName)
394
+ * placeDryRun would succeed at the current moment. The two failure
395
+ * modes are distinct: `validationErrors` is "the bundle spec itself
396
+ * is broken" (operator must fix); `noCapacity` is "current cluster
397
+ * state can't host this" (operator must add capacity or wait).
398
+ *
399
+ * @public
400
+ */
401
+ export type AdmissionResult = {
402
+ kind: "ok";
403
+ } | {
404
+ kind: "validationErrors";
405
+ errors: ValidationError[];
406
+ } | {
407
+ kind: "noCapacity";
408
+ unplaceable: AdmissionUnplaceable[];
409
+ };
410
+ /** @public */
411
+ export interface AdmissionUnplaceable {
412
+ replicaIndex: number;
413
+ jobName: string;
414
+ reason: PlacementFailureReason;
415
+ }
416
+ /**
417
+ * Phase D4 — operational metrics snapshot. Cumulative counters since
418
+ * `startedAt`; per-pool gauges are point-in-time at call. Consumers
419
+ * (telemetry pipelines, ops UIs) poll `AutoManager.metrics()` on
420
+ * their own cadence.
421
+ *
422
+ * @public
423
+ */
424
+ export interface AutoManagerMetrics {
425
+ /** AutoManager.run() entry time; undefined before runInternal starts. */
426
+ startedAt?: Date;
427
+ /** Milliseconds since startedAt, 0 if not yet started. */
428
+ uptimeMs: number;
429
+ placementsAttempted: number;
430
+ /**
431
+ * Placement that successfully dispatched its wire call (startJob /
432
+ * createAwsNode / createOciNode). Counted post-RPC, so a successful
433
+ * decision whose dispatch then fails does not bump this counter —
434
+ * see `placementsDispatchFailed`.
435
+ */
436
+ placementsSucceeded: number;
437
+ /** Placement engine returned `failure` (no suitable target). */
438
+ placementsFailed: number;
439
+ /**
440
+ * Decision succeeded but the downstream RPC (startJob /
441
+ * createAwsNode / createOciNode) rejected. The job is left
442
+ * unplaced; AutoManager surfaces the error via onError.
443
+ */
444
+ placementsDispatchFailed: number;
445
+ jobsRejected: number;
446
+ bundlesBroken: number;
447
+ /** Bronze-band drops. */
448
+ jobsDropped: number;
449
+ pools: PoolMetrics[];
450
+ /** Bundles currently marked broken by the restart-rate ceiling. */
451
+ brokenBundleIds: BundleId[];
452
+ }
453
+ /** @public */
454
+ export interface PoolMetrics {
455
+ poolName: string;
456
+ nodeCount: number;
457
+ /** Spare-tagged, running, no jobs assigned. */
458
+ liveSpares: number;
459
+ /** Sum of `targetCount` across hotSpares entries for this pool. */
460
+ spareTarget: number;
461
+ /**
462
+ * Mean reservedCapacity / totalCapacity across nodes with inventory.
463
+ * 0 if there are no nodes with inventory.
464
+ */
465
+ usedCapacityFraction: number;
466
+ }
467
+ /**
468
+ * Payload for `onPlacementDecision`. The `bundleId`/`replicaIndex`/
469
+ * `jobName` triple identifies which bundle member the decision was made
470
+ * for; `result` is the placement engine's verdict; `trace` is the
471
+ * per-pool filter/score breakdown described in §6 of the design.
472
+ *
473
+ * @public
474
+ */
475
+ export interface PlacementDecisionInfo {
476
+ jobId: JobId;
477
+ bundleId: BundleId;
478
+ replicaIndex: number;
479
+ jobName: string;
480
+ result: PlacementResult;
481
+ trace: PlacementTrace;
482
+ at: Date;
483
+ }
484
+ /**
485
+ * Payload for `onPlacementFailed`. `triedPools` is the ordered list of
486
+ * tier names AutoManager attempted within the bundle's `pool` before
487
+ * giving up.
488
+ *
489
+ * @public
490
+ */
491
+ export interface PlacementFailedInfo {
492
+ jobId: JobId;
493
+ bundleId: BundleId;
494
+ replicaIndex: number;
495
+ jobName: string;
496
+ reason: PlacementFailureReason;
497
+ triedPools: string[];
498
+ at: Date;
499
+ }
500
+ /**
501
+ * Payload for `onResilienceDegraded`. Fired when a bundle's backup
502
+ * (replicaIndex 1) is placed inside a failure domain its resilience policy
503
+ * asked to avoid, because no compliant node had capacity. `violated` lists
504
+ * the anti-affinity flags that couldn't be honoured. The placement still
505
+ * happened — a degraded backup beats no backup — but DR posture is reduced.
506
+ *
507
+ * @public
508
+ */
509
+ export interface ResilienceDegradedInfo {
510
+ jobId: JobId;
511
+ bundleId: BundleId;
512
+ replicaIndex: number;
513
+ jobName: string;
514
+ violated: ResilienceFlag[];
515
+ /** Set when the primary is on interruptible (spot) capacity with no
516
+ * durable backup — see ResilienceDegradation.interruptiblePrimary. */
517
+ interruptiblePrimary?: boolean;
518
+ at: Date;
519
+ }
520
+ /**
521
+ * Payload for `onJobRejected`. `inventory` is the typed snapshot the
522
+ * worker sent alongside the rejection so consumers can render "why" in
523
+ * the same UI tick as the rejection itself.
524
+ *
525
+ * @public
526
+ */
527
+ export interface JobRejectedInfo {
528
+ jobId: JobId;
529
+ nodeId: NodeId;
530
+ reason: ManagerPB.JobRejected["reason"];
531
+ inventory?: TypedNodeInventory;
532
+ at: Date;
533
+ }
534
+ export declare class AutoManager {
535
+ norsk: NorskManager;
536
+ settings: AutoSettings;
537
+ clock: Clock;
538
+ jobMap: Map<JobId, JobWithHistory>;
539
+ nodeMap: Map<NodeId, NodeSummary>;
540
+ bundleMap: Map<BundleId, TypedBundle>;
541
+ inventoryMap: Map<NodeId, TypedNodeInventory>;
542
+ private rejectionsPerNode;
543
+ private unhealthyNodes;
544
+ private recentFailures;
545
+ private restartHistory;
546
+ private jobToNode;
547
+ private placementsByJob;
548
+ private pendingPlacementTimers;
549
+ private pendingStopTimers;
550
+ private pendingPlacementRetries;
551
+ private lastPlacementAttemptAt;
552
+ private brokenBundles;
553
+ private startupGraceTimers;
554
+ private pendingSpareProvisions;
555
+ private hotSpareReconcileInterval?;
556
+ private cordonedNodes;
557
+ private metricsCounters;
558
+ private migrationStates;
559
+ private migrationByJobId;
560
+ private migrationPhaseTimers;
561
+ private nodeServicePlacements;
562
+ private nodeServiceReconcileInterval?;
563
+ /**
564
+ * The moment `runInternal()` first entered (i.e. the eventStream was
565
+ * established). Undefined until then. Powers "AutoManager up for N
566
+ * minutes" affordances.
567
+ * @public
568
+ */
569
+ startedAt?: Date;
570
+ private cleanupInterval?;
571
+ private closed;
572
+ /** @public */
573
+ static run(settings: AutoSettings): Promise<[AutoManager, Promise<void>]>;
574
+ /** @public */
575
+ terminateNode(nodeId: NodeId): Promise<void>;
576
+ /**
577
+ * Phase C8 — cordon a node. AutoManager projects this onto every
578
+ * placement decision: cordoned nodes are filtered out, so no new
579
+ * placements land on them. Existing jobs continue running.
580
+ * Idempotent.
581
+ * @public
582
+ */
583
+ cordonNode(nodeId: NodeId): void;
584
+ /**
585
+ * Phase C8 — un-cordon a node, restoring it to the placement pool.
586
+ * Idempotent.
587
+ * @public
588
+ */
589
+ uncordonNode(nodeId: NodeId): void;
590
+ /**
591
+ * Phase C8 — whether a node is currently cordoned by AutoManager.
592
+ * @public
593
+ */
594
+ isCordoned(nodeId: NodeId): boolean;
595
+ /**
596
+ * Phase C8 — drain a node. Cordons it (so no new placements land
597
+ * here) and terminates the node — Manager emits `nodeStopping`
598
+ * for every job hosted there, and the existing band-aware recovery
599
+ * path re-places them onto other nodes (excluding this cordoned
600
+ * one). Bronze jobs are dropped; gold prefers hot spares.
601
+ *
602
+ * v1 takes the immediate-terminate path; the design's "graceful
603
+ * shuffle without recovery gap" needs Phase E job migration to
604
+ * land first.
605
+ * @public
606
+ */
607
+ drainNode(nodeId: NodeId): Promise<void>;
608
+ /**
609
+ * Shut down this AutoManager. Cancels periodic intervals and closes
610
+ * the underlying NorskManager — which causes the event-loop iterator
611
+ * to terminate, resolving the `Promise<void>` returned from `run()`.
612
+ * Idempotent.
613
+ * @public
614
+ */
615
+ close(): void;
616
+ /**
617
+ * Alias for `close()`. Provided for symmetry with the
618
+ * `using`-style disposal conventions emerging in TypeScript.
619
+ * @public
620
+ */
621
+ dispose(): void;
622
+ /**
623
+ * AutoManager v1 — submit a new bundle. The bundle is validated by
624
+ * Manager (per §4.3) and expanded into N replica jobs server-side;
625
+ * AutoManager mirrors the resulting state via the bundleUpdated +
626
+ * jobUpdated events. The returned bundleId matches the one Manager
627
+ * assigns (typically the same as bundle.bundleId, but Manager has
628
+ * the final word).
629
+ *
630
+ * @public
631
+ */
632
+ createBundle(bundle: TypedBundle): Promise<BundleId>;
633
+ /**
634
+ * AutoManager v1 — update an existing bundle.
635
+ * @public
636
+ */
637
+ updateBundle(bundle: TypedBundle): Promise<void>;
638
+ /**
639
+ * AutoManager v1 — delete a bundle and all its replica jobs.
640
+ * @public
641
+ */
642
+ deleteBundle(bundleId: BundleId): Promise<void>;
643
+ /**
644
+ * AutoManager v1 — activate a dormant (start_mode = "manual") bundle
645
+ * job. Manager removes the dormant tag and re-emits jobUpdated;
646
+ * AutoManager's placement path picks the job up via the standard
647
+ * flow. See §10.2 of the design.
648
+ * @public
649
+ */
650
+ startBundleJob(bundleId: BundleId, replicaIndex: number, jobName: string): Promise<void>;
651
+ /**
652
+ * AutoManager v1 — deactivate a bundle job (tag-flip).
653
+ *
654
+ * Runtime termination of an in-flight workload and empty-node
655
+ * release with linger are pre-go-live work tracked as Phase E.5
656
+ * (§10.2.1). Until they land, calling stopBundleJob on an active
657
+ * job marks it dormant for future placement but does not stop the
658
+ * currently-running worker.
659
+ * @public
660
+ */
661
+ stopBundleJob(bundleId: BundleId, replicaIndex: number, jobName: string): Promise<void>;
662
+ /**
663
+ * AutoManager v1 — typed view of the jobs Manager has told us about.
664
+ * `JobWithHistory` carries the state-transition log for each job so
665
+ * consumers can render lifecycle timelines without a separate
666
+ * round-trip.
667
+ * @public
668
+ */
669
+ jobs(): JobWithHistory[];
670
+ nodes(): NodeSummary[];
671
+ /**
672
+ * AutoManager v1 — typed view of the bundle definitions Manager has
673
+ * told us about. Mirror only; AutoManager owns no durable state.
674
+ * @public
675
+ */
676
+ bundles(): TypedBundle[];
677
+ /**
678
+ * Phase D4 — snapshot of AutoManager's operational metrics.
679
+ * Cumulative counters since `startedAt`; per-pool gauges computed
680
+ * at call time. Cheap to call repeatedly — consumers poll on their
681
+ * own cadence and compute deltas if they want rates.
682
+ * @public
683
+ */
684
+ metrics(): AutoManagerMetrics;
685
+ private computePoolMetrics;
686
+ /**
687
+ * Look up a single bundle by id. Convenience over `bundles().find(...)`.
688
+ * @public
689
+ */
690
+ bundleById(bundleId: BundleId): TypedBundle | undefined;
691
+ /**
692
+ * Every job (with history) that belongs to a given bundle. Derived
693
+ * from `tags.bundleId` on the job entries in jobMap. Returns an empty
694
+ * array if the bundle is unknown or has no jobs.
695
+ * @public
696
+ */
697
+ jobsForBundle(bundleId: BundleId): JobWithHistory[];
698
+ /**
699
+ * The node the given job is currently placed on, or undefined if
700
+ * the job hasn't been placed (or has been deleted). Useful for
701
+ * operator UIs that render "job X is on node Y" and for proxy
702
+ * routers that need to know which worker hosts each job.
703
+ *
704
+ * O(1) lookup against the `jobToNode` index — set at placement
705
+ * time, deleted on jobDeleted, rebuilt from job history on R7
706
+ * reconnect. Works regardless of whether the node hosts one job
707
+ * (cloud, today) or many (cluster, or any future cloud N:1)
708
+ * because the assignment is recorded against the JOB rather than
709
+ * scanned out of NODE tags.
710
+ * @public
711
+ */
712
+ nodeForJob(jobId: JobId): NodeSummary | undefined;
713
+ /**
714
+ * Current placement of every job AutoManager has bound to a node,
715
+ * newest-first by decision timestamp. Mirrors `nodeForJob` but
716
+ * returns the full `PlacementDecisionInfo` (so consumers get the
717
+ * pool, trace, replicaIndex etc., not just the node) — and exposes
718
+ * it as queryable live state rather than only via the
719
+ * `onPlacementDecision` event stream.
720
+ *
721
+ * Hydrated jobs (placed in a previous AutoManager generation, picked
722
+ * up on R7 reconnect) get an entry with an empty `trace.attempts`
723
+ * and a decision inferred from job history. Consumers that *need*
724
+ * a real trace should subscribe to `onPlacementDecision` for live
725
+ * decisions; this getter is for "where is everything right now?"
726
+ * operator views.
727
+ *
728
+ * @public
729
+ */
730
+ placements(): PlacementDecisionInfo[];
731
+ /**
732
+ * O(1) lookup for the most recent placement decision binding `jobId`
733
+ * to a node. Returns undefined for jobs that have never been placed
734
+ * (or whose placement attempts have only failed). @public
735
+ */
736
+ placementForJob(jobId: JobId): PlacementDecisionInfo | undefined;
737
+ /**
738
+ * Run the placement engine against AutoManager's current inventory
739
+ * snapshot and configured pools, without actually placing or
740
+ * provisioning anything. Powers preview affordances in operator
741
+ * UIs ("if I submitted this bundle now, where would each replica
742
+ * land?").
743
+ *
744
+ * `jobName` is optional; if omitted the *first* job in the bundle
745
+ * is used. For multi-job bundles, supply the name to get a decision
746
+ * specific to that job.
747
+ *
748
+ * Returns undefined if the bundle's named job is missing or the
749
+ * replica index is out of range.
750
+ * @public
751
+ */
752
+ placeDryRun(bundle: TypedBundle, replicaIndex: number, jobName?: string): {
753
+ result: PlacementResult;
754
+ trace: PlacementTrace;
755
+ } | undefined;
756
+ /**
757
+ * Static validation of a bundle against AutoManager's currently
758
+ * configured pools. Equivalent to calling the module-level
759
+ * `validateBundle` with `PoolDescriptor[]` derived from
760
+ * `AutoSettings.placementPools` — elastic pools project their
761
+ * `candidateInstanceTypes` into node profiles; cluster pools project
762
+ * their currently-known live inventory.
763
+ * @public
764
+ */
765
+ validateBundle(bundle: TypedBundle): ValidationResult;
766
+ /**
767
+ * Phase C4 — packaged admission check. Combines `validateBundle`
768
+ * (static spec sanity) with `placeDryRun` for every (replica,
769
+ * jobName) pair. Returns `ok` only when both pass for every
770
+ * member; otherwise reports the structured failure. Use as a
771
+ * pre-submit guard in operator-facing tooling.
772
+ *
773
+ * Per the design's §3.4 band table, all bands reject at admission
774
+ * on "no capacity right now" — there's no band differentiation
775
+ * here. (Gold/silver get *queued* internally on capacity-after-
776
+ * submit failures via the rejection re-place path, but that's a
777
+ * runtime concern, not admission.)
778
+ *
779
+ * @public
780
+ */
781
+ validateBundleAdmission(bundle: TypedBundle): AdmissionResult;
782
+ private poolDescriptorsForValidation;
783
+ /**
784
+ * AutoManager v1 — typed inventory snapshot for a single node, or
785
+ * undefined if the worker hasn't (yet) advertised one.
786
+ * @public
787
+ */
788
+ inventory(nodeId: NodeId): TypedNodeInventory | undefined;
789
+ /**
790
+ * AutoManager v1 — typed inventory snapshots for every node we know
791
+ * about. Iteration order is insertion order.
792
+ * @public
793
+ */
794
+ inventories(): {
795
+ nodeId: NodeId;
796
+ inventory: TypedNodeInventory;
797
+ }[];
798
+ private handleNodeInventoryUpdated;
799
+ /**
800
+ * A running job has had its config updated at runtime (operator action,
801
+ * job-driven, or other side channel). Refresh our in-memory copy so a
802
+ * subsequent `redeploy` (placeAndProvision triggered by node failure
803
+ * etc.) hands the new config rather than the launch one.
804
+ *
805
+ * Manager is responsible for persisting the new value; AutoManager
806
+ * just keeps its mirror up to date.
807
+ */
808
+ private handleJobConfigUpdated;
809
+ private handleJobRejected;
810
+ private recordRejection;
811
+ private currentExcludeSet;
812
+ /**
813
+ * Phase C2 — restart-rate check. Records the current attempt; if the
814
+ * trailing-hour count crosses the band's `maxRestartsPerHour`, marks
815
+ * the bundle broken, fires `onBundleBroken`, and returns true
816
+ * (caller must short-circuit). Returns false when within budget.
817
+ */
818
+ private shouldBreakOnRestart;
819
+ /**
820
+ * Whether the named bundle has tripped the restart ceiling and is
821
+ * being held by AutoManager. Cleared by `bundleUpdated` (operator
822
+ * pushed a fresh spec) or `bundleDeleted`.
823
+ * @public
824
+ */
825
+ isBundleBroken(bundleId: BundleId): boolean;
826
+ /**
827
+ * Initiate a migration of a running job to a target node. AutoManager
828
+ * provisions a second instance of the same Job on `targetNodeId` with
829
+ * role="migrationTarget" and drives the cutover handshake. Returns
830
+ * a stable `migrationId` for tracking.
831
+ *
832
+ * Throws if a migration is already in flight for `sourceJobId` (no
833
+ * concurrent migrations per Job).
834
+ *
835
+ * `sourceRole` defaults to whichever role the first node hosting
836
+ * `sourceJobId` has — adequate for single-instance Jobs. Specify
837
+ * explicitly for multi-replica bundles.
838
+ *
839
+ * @public
840
+ */
841
+ migrateJob(sourceJobId: JobId, targetNodeId: NodeId, sourceRole?: Role): Promise<MigrationId>;
842
+ /**
843
+ * Abort an in-flight migration. AutoManager sends MigrationAbort to
844
+ * any live source/target instances and marks the migration aborted.
845
+ * No-op (with warning log) for an unknown migration id.
846
+ *
847
+ * @public
848
+ */
849
+ abortMigration(migrationId: MigrationId, reason: string): Promise<void>;
850
+ /** All known migrations (active + done + aborted). @public */
851
+ migrations(): MigrationState[];
852
+ /** Look up a single migration by id. @public */
853
+ migrationById(migrationId: MigrationId): MigrationState | undefined;
854
+ /** Migration active for the given source jobId, if any. @public */
855
+ migrationForJob(jobId: JobId): MigrationState | undefined;
856
+ /**
857
+ * Find the role of the (first) node currently running this Job.
858
+ * Returns undefined if no node hosts the job.
859
+ */
860
+ private inferSourceRole;
861
+ private advanceMigration;
862
+ /** Phase E deadlines — re-arm the per-phase timer for the migration. */
863
+ private armMigrationPhaseDeadline;
864
+ private cfgMigrationPhaseDeadline;
865
+ private markMigrationDone;
866
+ private markMigrationAborted;
867
+ private failMigration;
868
+ private dispatchMigrationAborts;
869
+ private handleMigrationTargetReady;
870
+ private handleMigrationSourceStoppedOutput;
871
+ private handleMigrationAborted;
872
+ private findNodeForJobKey;
873
+ private cfgRejectionBackoffMs;
874
+ private cfgRejectionEscalationCount;
875
+ private cfgFailureBackoffMs;
876
+ private cfgJobStartupGraceMs;
877
+ private cfgMaxRestartsPerHour;
878
+ private placeAndProvision;
879
+ private inventoriesAsNodeViews;
880
+ /** Map a placed jobId to the RunningJob view the placement engine uses
881
+ * for affinity / spread, via the job's bundle-context tags. Empty when
882
+ * the job lacks bundle tags. */
883
+ private runningJobFor;
884
+ /** Sum the resource requirements of the given placed jobs into a node
885
+ * reservation. Capacity / cores / capabilities come from each job's
886
+ * bundle spec — the canonical typed requirements the placement engine
887
+ * scores against, looked up via the job's bundleId / jobName tags. Jobs
888
+ * whose bundle or spec we don't have yet contribute nothing. */
889
+ private derivedReservation;
890
+ /** The canonical typed requirements for a placed job, from its bundle
891
+ * spec (the same source `placeAndProvision` scores against). */
892
+ private requirementsForPlacedJob;
893
+ /**
894
+ * Phase C5 — is the named node currently tagged as a hot spare?
895
+ * Spares are warm-running nodes provisioned (by the C6 reconciler)
896
+ * with `NodeMetadata.tags["spare"] = "true"`. The optional
897
+ * `tags["spareBand"]` ("gold" | "silver") dedicates a spare to a
898
+ * band; if absent, the spare is band-agnostic.
899
+ * @public
900
+ */
901
+ isHotSpareNode(nodeId: NodeId): boolean;
902
+ /**
903
+ * Phase C5 — band a spare is dedicated to, if any. `undefined` for a
904
+ * non-spare or for a band-agnostic spare.
905
+ * @public
906
+ */
907
+ spareBandForNode(nodeId: NodeId): PriorityBand | undefined;
908
+ /**
909
+ * Drive every configured `hotSpares` entry toward its `targetCount`.
910
+ * Counts live + pending spares matching (pool, band); provisions
911
+ * short, terminates excess (oldest live first). Cluster pools are
912
+ * non-elastic — under-target just fires `onSpareUnderflow` and
913
+ * leaves provisioning to the operator. Idempotent and cheap; safe
914
+ * to call on every lifecycle event plus the periodic tick.
915
+ */
916
+ private reconcileHotSpares;
917
+ private spareNodesMatching;
918
+ private pendingSparesMatching;
919
+ private provisionHotSpare;
920
+ /**
921
+ * @public
922
+ * Current placements for each configured NodeService. Returns a
923
+ * snapshot map of `serviceId → (nodeId → runtimeJobId)`.
924
+ */
925
+ nodeServices(): NodeService[];
926
+ /**
927
+ * @public
928
+ * Nodes on which the named NodeService is currently placed,
929
+ * keyed by the runtime job's jobId.
930
+ */
931
+ nodeServicePlacementsFor(serviceId: ServiceId): Map<NodeId, JobId>;
932
+ /**
933
+ * Drive every configured NodeService toward its lifecycle target.
934
+ * v1 only handles `eager` — places on every eligible node.
935
+ * `lazy` placements + capability-loss-driven consumer recovery (F3)
936
+ * are deferred.
937
+ */
938
+ private reconcileNodeServices;
939
+ private reconcileEagerNodeService;
940
+ /**
941
+ * Identify nodes in the service's named pools whose current inventory
942
+ * satisfies its `consumes` requirements. Used both for placement and
943
+ * for tear-down decisions.
944
+ */
945
+ private eligibleNodesForService;
946
+ private nodeSatisfiesConsumes;
947
+ private placeNodeService;
948
+ private teardownNodeService;
949
+ private tagsForJob;
950
+ private poolNameForNode;
951
+ private tierNameForNode;
952
+ private azForNode;
953
+ private cloudForNode;
954
+ private handleBundleUpdated;
955
+ private handleBundleDeleted;
956
+ private clearRestartHistoryForBundle;
957
+ private nodesByJob;
958
+ private handleJobChanged;
959
+ /** Read the `__stopDateTime` tag off the job, and (re)schedule the
960
+ * bundle's stop timer accordingly. Tag is set per-job but applies
961
+ * to the whole bundle — first-job-wins for read; all jobs in a
962
+ * bundle share the same value at submit. */
963
+ private reconcileStopTimer;
964
+ private scheduleStopTimer;
965
+ /**
966
+ * Update (or clear) a bundle's auto-stop time. Calls `updateBundle`
967
+ * so the new value persists via the daemon's tag store and any other
968
+ * clients see it through their event streams. Pass `undefined` to
969
+ * cancel a previously-set stop time.
970
+ *
971
+ * Throws if the bundle isn't known to this AutoManager (no live
972
+ * bundleMap entry); operators wanting to set stop on a not-yet-
973
+ * streamed bundle should retry after JobPending fires.
974
+ * @public
975
+ */
976
+ setBundleStopTime(bundleId: BundleId, stopDateTime?: Date): Promise<void>;
977
+ /**
978
+ * Snapshot of all currently-scheduled bundle stop timers. UIs render
979
+ * this alongside `pendingPlacements()` for an "upcoming events"
980
+ * view.
981
+ * @public
982
+ */
983
+ bundleStopTimes(): Array<{
984
+ bundleId: BundleId;
985
+ stopAt: Date;
986
+ }>;
987
+ /** A job that should be running but currently has no node bound to
988
+ * it: it carries placement `requirements`, isn't manual-start, is in
989
+ * a placeable state (`pre` / `active`), and `nodesByJob` is empty.
990
+ * This is the single source of truth for "this job wants a placement
991
+ * and doesn't have one" — shared by initial hydration, the per-job
992
+ * `handleJobChanged` path, and the capacity-driven reconcile sweep,
993
+ * so the decision lives in exactly one place. */
994
+ private wantsPlacementNow;
995
+ /**
996
+ * Re-run placement for every job that wants a node but hasn't got
997
+ * one. Called whenever a *capacity-increasing* event arrives — a node
998
+ * comes online (`nodeStarted`), a worker reports fresh inventory
999
+ * (`nodeInventoryUpdated`, which also covers an un-cordon or a node
1000
+ * that has just freed reserved capacity), or a job finishes and
1001
+ * releases its node (`jobDeleted`). Those are the only transitions
1002
+ * that can turn a previously-unplaceable job placeable, so they are
1003
+ * exactly when re-running the engine is worthwhile — there is no
1004
+ * polling retry, the trigger is always a concrete event.
1005
+ *
1006
+ * Placement stays implemented once: each candidate routes through
1007
+ * `placeOrDefer` (the same entry point hydration and `handleJobChanged`
1008
+ * use), inheriting its scheduling, the in-flight gate, and the
1009
+ * failure path. Jobs already waiting on a schedule timer or a recovery
1010
+ * backoff are skipped — their own timers own them — so a capacity
1011
+ * event never disturbs a future-scheduled or backing-off job.
1012
+ *
1013
+ * Cheap and idempotent (mirrors `reconcileHotSpares` /
1014
+ * `reconcileNodeServices`); safe to call on every relevant event.
1015
+ */
1016
+ private reconcilePendingPlacements;
1017
+ /** Decide whether to place immediately or defer until the job's
1018
+ * scheduled `startDateTime` (minus pool lead time) arrives.
1019
+ * Re-entry from job-update events: cancels and re-evaluates so a
1020
+ * schedule change moves the timer. Hydrated jobs with `startTime -
1021
+ * lead` in the past place immediately (operator intent was to run;
1022
+ * schedule was a "when", not a "drop if missed"). */
1023
+ private placeOrDefer;
1024
+ /** Maximum `placementLeadMs` across the bundle pool's tiers (worst case
1025
+ * — guarantees the job is ready by `startDateTime` regardless of which
1026
+ * tier the placement engine ends up picking). Falls back to the per-kind
1027
+ * default when a tier doesn't specify one. Returns 0 if the bundle or
1028
+ * pool isn't found — we'd rather race the schedule than indefinitely
1029
+ * delay placement. */
1030
+ private placementLeadMsForJob;
1031
+ /**
1032
+ * Currently-scheduled placements (jobs AutoManager has deferred
1033
+ * because their `startDateTime - leadTime` is still in the future).
1034
+ * Returns one entry per pending timer; the UI can render an
1035
+ * "upcoming jobs" list and show when each will go live.
1036
+ * @public
1037
+ */
1038
+ pendingPlacements(): Array<{
1039
+ jobId: JobId;
1040
+ scheduledFor: Date;
1041
+ }>;
1042
+ private handleJobDeleted;
1043
+ private handleNodeStarting;
1044
+ /** Phase C3. Schedule the grace timer; replace any prior timer. */
1045
+ private armStartupGrace;
1046
+ private cancelStartupGrace;
1047
+ private handleNodeStarted;
1048
+ private advanceMigrationsOnNodeStarted;
1049
+ private handleNodeStopping;
1050
+ private handleNodeStopped;
1051
+ private dropNodeServicePlacementsOnNode;
1052
+ private updateNodeMap;
1053
+ }
1054
+ export declare function getJobIdForNode(nodeMetadata: NodeMetadata | undefined): JobId | undefined;
1055
+ /**
1056
+ * Walk a job's history backward and return the nodeId where it is
1057
+ * currently placed (or was most recently placed). Prefers `running`
1058
+ * entries; falls back to `provisioned` for jobs still booting their
1059
+ * node. Returns undefined for jobs that have never been placed or
1060
+ * whose history is empty (defensive — runtime data always has at
1061
+ * least a `created` entry, which carries no nodeMetadata).
1062
+ *
1063
+ * This is the canonical "where does this job live" lookup and
1064
+ * underpins `AutoManager.nodeForJob`. It works regardless of whether
1065
+ * a node hosts one job (cloud, today) or many (cluster, or future
1066
+ * cloud N:1) because the nodeId is recorded against the JOB rather
1067
+ * than scanned out of NODE tags.
1068
+ */
1069
+ export declare function currentNodeIdForJob(jh: JobWithHistory | undefined): NodeId | undefined;
1070
+ //# sourceMappingURL=automanager.d.ts.map