@camstack/addon-pipeline-orchestrator 1.2.289 → 1.2.291

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/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -8048,7 +8048,7 @@ import { errMsg } from '@camstack/types'
8048
8048
  * Extract a human-readable message from an unknown error value.
8049
8049
  * Replaces the ubiquitous `errMsg(err)` pattern.
8050
8050
  */
8051
- function errMsg$1(err) {
8051
+ function errMsg$2(err) {
8052
8052
  if (err instanceof Error) return err.message;
8053
8053
  if (typeof err === "string") return err;
8054
8054
  return String(err);
@@ -11551,7 +11551,53 @@ var ModelFormatEntrySchema = object({
11551
11551
  /** Multi-file payload (directory members or sibling files). */
11552
11552
  files: array(string()).readonly().optional(),
11553
11553
  /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
11554
- runtimes: array(_enum(["python"])).readonly().optional()
11554
+ runtimes: array(_enum(["python"])).readonly().optional(),
11555
+ /**
11556
+ * Content revision of the artefact, for one hosted OFF the CamStack mirror.
11557
+ * A format on the mirror leaves it unset: its revision (the sha256 of its
11558
+ * weight file) comes from the downloader's generated table, kept in step
11559
+ * with the mirror by `scripts/check-hosted-model-revisions.ts` (D744). A
11560
+ * node records the revision it downloaded next to the file
11561
+ * (`<file>.revision`); a copy with no record or a different one is stale
11562
+ * unless its weight file hashes to the revision, and a stale copy is
11563
+ * downloaded again, the old one serving until the new one is complete. No
11564
+ * revision at all ⇒ a file on disk is current, whatever it holds (D743).
11565
+ */
11566
+ revision: string().min(1).optional()
11567
+ });
11568
+ /**
11569
+ * What a node holds for one (model, format) (D746). `current` is a complete
11570
+ * copy at its catalog revision — exactly what `downloaded: true` means.
11571
+ * `stale` is bytes on disk that are not (yet) proven current under D743/D744:
11572
+ * no or a different revision record, or a missing sibling. `missing` is no
11573
+ * copy. `downloaded` alone folds `stale` into "not downloaded", which hid
11574
+ * reclaimable bytes and a copy about to be replaced.
11575
+ */
11576
+ var ModelOnDiskStateSchema = _enum([
11577
+ "current",
11578
+ "stale",
11579
+ "missing"
11580
+ ]);
11581
+ /**
11582
+ * The numeric precision of one format's BUILD (D746) — reported only when the
11583
+ * build states it (file-name token, an INT8 variant, an EdgeTPU compile, a
11584
+ * CamStack Core ML export). Absent means "not stated", never "fp32".
11585
+ */
11586
+ var ModelFormatPrecisionSchema = _enum([
11587
+ "fp32",
11588
+ "fp16",
11589
+ "int8"
11590
+ ]);
11591
+ /**
11592
+ * One format of one model as a node's `pipelineExecutor.getSchema` reports it.
11593
+ * `onDisk` / `precision` are optional for version skew: a runner that predates
11594
+ * them omits them, and a reader falls back to `downloaded`.
11595
+ */
11596
+ var PipelineModelFormatStatusSchema = object({
11597
+ downloaded: boolean(),
11598
+ sizeMB: number(),
11599
+ onDisk: ModelOnDiskStateSchema.optional(),
11600
+ precision: ModelFormatPrecisionSchema.optional()
11555
11601
  });
11556
11602
  /**
11557
11603
  * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
@@ -24297,7 +24343,7 @@ var FrameRefSchema = object({
24297
24343
  timestamp: number(),
24298
24344
  capturedAt: number().optional()
24299
24345
  });
24300
- var ModelFormatSchema$1 = _enum([
24346
+ var ModelFormatSchema$2 = _enum([
24301
24347
  "onnx",
24302
24348
  "coreml",
24303
24349
  "openvino",
@@ -24315,7 +24361,7 @@ var PipelineSlotSchema = _enum([
24315
24361
  var PipelineEngineChoiceSchema = object({
24316
24362
  runtime: literal("python"),
24317
24363
  backend: string(),
24318
- format: ModelFormatSchema$1,
24364
+ format: ModelFormatSchema$2,
24319
24365
  device: string().optional()
24320
24366
  });
24321
24367
  /**
@@ -24372,10 +24418,7 @@ var PipelineTemplateSchema$1 = object({
24372
24418
  var PipelineModelOptionSchema = object({
24373
24419
  id: string(),
24374
24420
  name: string(),
24375
- formats: record(string(), object({
24376
- downloaded: boolean(),
24377
- sizeMB: number()
24378
- })),
24421
+ formats: record(string(), PipelineModelFormatStatusSchema),
24379
24422
  group: ModelVariantGroupSchema.optional(),
24380
24423
  legacy: boolean().optional(),
24381
24424
  provider: ModelProviderIdSchema.optional(),
@@ -24590,6 +24633,32 @@ var pipelineExecutorCapability = {
24590
24633
  auth: "admin"
24591
24634
  }),
24592
24635
  /**
24636
+ * Best settings (D745): the MODEL-BOUND thresholds (`bindsToModel` keys of
24637
+ * the object-detection root, and their binding keys) stored in THIS node's
24638
+ * per-camera detector settings — and, unless `dryRun`, remove them so the
24639
+ * running model's own defaults apply. Every other key of a camera's store
24640
+ * is kept. `nodeId` is the ROUTING key. `deviceIds` absent ⇒ every camera
24641
+ * with a store. A camera whose store could not be read is `refused`, never
24642
+ * reported as clean.
24643
+ */
24644
+ clearModelBoundDeviceOverrides: method(object({
24645
+ nodeId: string(),
24646
+ deviceIds: array(number()).readonly().optional(),
24647
+ dryRun: boolean()
24648
+ }), object({
24649
+ entries: array(object({
24650
+ deviceId: number(),
24651
+ keys: array(string()).readonly()
24652
+ })).readonly(),
24653
+ refused: array(object({
24654
+ deviceId: number(),
24655
+ reason: string()
24656
+ })).readonly()
24657
+ }), {
24658
+ kind: "mutation",
24659
+ auth: "admin"
24660
+ }),
24661
+ /**
24593
24662
  * Which of THIS node's inference devices the executor currently refuses,
24594
24663
  * and why. `nodeId` is the ROUTING key (stripped by the generated router).
24595
24664
  *
@@ -24684,12 +24753,12 @@ var pipelineExecutorCapability = {
24684
24753
  downloadModel: method(object({
24685
24754
  addonId: string(),
24686
24755
  modelId: string(),
24687
- format: ModelFormatSchema$1
24756
+ format: ModelFormatSchema$2
24688
24757
  }), DownloadModelResultSchema, { kind: "mutation" }),
24689
24758
  deleteModel: method(object({
24690
24759
  addonId: string(),
24691
24760
  modelId: string(),
24692
- format: ModelFormatSchema$1
24761
+ format: ModelFormatSchema$2
24693
24762
  }), object({ success: literal(true) }), { kind: "mutation" }),
24694
24763
  /**
24695
24764
  * Stateless single-frame execution. Callers (runner, benchmark) pass
@@ -24935,6 +25004,315 @@ var pipelineExecutorCapability = {
24935
25004
  getDetectionConfigSchema: method(_void(), ConfigUISchemaNullableBridge)
24936
25005
  }
24937
25006
  };
25007
+ /**
25008
+ * Wire shapes of the Load balancing surface (D748): the session placement
25009
+ * policy, the atomic share write, the observed placement statistics, the
25010
+ * hardware share suggestion and the stale device pins.
25011
+ */
25012
+ /**
25013
+ * How a new detection session picks its node and device.
25014
+ * - `least-loaded` — the historical rule: lowest `attached / weight`, a
25015
+ * constant node order at a tie (D37). An idle node always ties at 0, so a
25016
+ * weight only bites once a node already holds a session.
25017
+ * - `share` — the share of PARALLEL sessions is held to the weights, and a
25018
+ * full or drowning node yields to a free one (D748).
25019
+ */
25020
+ var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
25021
+ var DEFAULT_SESSION_PLACEMENT_POLICY = "least-loaded";
25022
+ /** The global-settings key the policy is stored under (orchestrator `load` section). */
25023
+ var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
25024
+ /** Parse a stored value; anything unknown is the default (nothing changes on deploy). */
25025
+ function parseSessionPlacementPolicy(value) {
25026
+ const parsed = SessionPlacementPolicySchema.safeParse(value);
25027
+ return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
25028
+ }
25029
+ var LoadBalancingSharesInputSchema = object({
25030
+ /** nodeId → share (> 0). A node left out keeps its weight. */
25031
+ nodes: record(string(), number().positive()).default({}),
25032
+ /** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
25033
+ devices: record(string(), record(string(), number().positive())).default({})
25034
+ });
25035
+ var PlacementStatsInputSchema = object({
25036
+ /** Window the observed shares are averaged over (default 1 h, max 24 h). */
25037
+ windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
25038
+ var PlacementDeviceStatsSchema = object({
25039
+ deviceKey: string(),
25040
+ /** Sessions running on this device now. */
25041
+ sessionsNow: number(),
25042
+ /** Sessions started on this device inside the window. */
25043
+ sessionsStarted: number(),
25044
+ /** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
25045
+ observedShare: number().nullable()
25046
+ });
25047
+ var PlacementNodeStatsSchema = object({
25048
+ nodeId: string(),
25049
+ sessionsNow: number(),
25050
+ sessionsStarted: number(),
25051
+ /** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
25052
+ observedShare: number().nullable(),
25053
+ /** Most sessions this node ran at once inside the window. */
25054
+ peakSessions: number(),
25055
+ /** Sessions a share election handed elsewhere because this node was full or drowning. */
25056
+ yieldsAway: number(),
25057
+ devices: array(PlacementDeviceStatsSchema).readonly()
25058
+ });
25059
+ /** A camera's device pin that the session path cannot honour. */
25060
+ var StaleDevicePinSchema = object({
25061
+ deviceId: number(),
25062
+ deviceKey: string(),
25063
+ /** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
25064
+ reason: _enum(["disabled", "absent"])
25065
+ });
25066
+ var PlacementStatsSchema = object({
25067
+ policy: SessionPlacementPolicySchema,
25068
+ windowMs: number(),
25069
+ /** How much of the window was actually observed (the hub keeps it in memory). */
25070
+ observedForMs: number(),
25071
+ sessionsNow: number(),
25072
+ /** Most parallel sessions across the cluster inside the window. */
25073
+ clusterPeakSessions: number(),
25074
+ nodes: array(PlacementNodeStatsSchema).readonly(),
25075
+ stalePins: array(StaleDevicePinSchema).readonly()
25076
+ });
25077
+ var SuggestSharesInputSchema = object({ headroom: number().min(.3).max(.5).default(.4) });
25078
+ var SuggestedDeviceShareSchema = object({
25079
+ deviceKey: string(),
25080
+ /** Share of the node (0–100, rounded). */
25081
+ share: number(),
25082
+ modelId: string().nullable(),
25083
+ /** Inferences per second at that model, from the measured table. */
25084
+ fps: number().nullable(),
25085
+ measured: boolean(),
25086
+ /** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
25087
+ suggestedMaxSessions: number().nullable()
25088
+ });
25089
+ var SuggestedNodeShareSchema = object({
25090
+ nodeId: string(),
25091
+ /** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
25092
+ share: number().nullable(),
25093
+ /** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
25094
+ capacitySessions: number(),
25095
+ devices: array(SuggestedDeviceShareSchema).readonly(),
25096
+ skippedReason: string().nullable()
25097
+ });
25098
+ var SuggestSharesResultSchema = object({
25099
+ headroom: number(),
25100
+ /** Camera detection rate the capacity is counted in (the fastest camera's fps). */
25101
+ cameraFps: number(),
25102
+ /** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
25103
+ iterations: number(),
25104
+ nodes: array(SuggestedNodeShareSchema).readonly()
25105
+ });
25106
+ /**
25107
+ * The wire shapes of the Best settings plan and apply (D745), used by the
25108
+ * `pipeline-orchestrator` cap. The recommender's own types
25109
+ * (`recommend.ts`, `demand.ts`) are the TS face of the same data; these
25110
+ * schemas are what crosses the cap boundary.
25111
+ */
25112
+ var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
25113
+ var HardwareClassIdSchema = _enum([
25114
+ "intel-core-ultra",
25115
+ "intel-n-series",
25116
+ "apple-m-base",
25117
+ "apple-m-pro",
25118
+ "unknown"
25119
+ ]);
25120
+ var AcceleratorKindSchema = _enum([
25121
+ "intel-npu",
25122
+ "intel-igpu",
25123
+ "x86-cpu",
25124
+ "arm-cpu",
25125
+ "apple-ane",
25126
+ "coral"
25127
+ ]);
25128
+ var RecommendationReasonSchema = discriminatedUnion("kind", [
25129
+ object({
25130
+ kind: literal("fits"),
25131
+ utilisation: number()
25132
+ }),
25133
+ object({
25134
+ kind: literal("tie-faster"),
25135
+ over: string(),
25136
+ recallDelta: number()
25137
+ }),
25138
+ object({
25139
+ kind: literal("stepped-down"),
25140
+ from: string(),
25141
+ to: string(),
25142
+ limitedBy: string()
25143
+ }),
25144
+ object({
25145
+ kind: literal("no-fit-lightest"),
25146
+ utilisation: number()
25147
+ }),
25148
+ object({
25149
+ kind: literal("derived-latency"),
25150
+ modelId: string()
25151
+ }),
25152
+ object({
25153
+ kind: literal("estimated-load"),
25154
+ observedForMs: number().nullable()
25155
+ }),
25156
+ object({ kind: literal("cpu-device") }),
25157
+ object({ kind: literal("coral-untouched") }),
25158
+ object({ kind: literal("device-disabled") }),
25159
+ object({ kind: literal("device-unavailable") }),
25160
+ object({ kind: literal("node-offline") }),
25161
+ object({ kind: literal("no-hardware-facts") }),
25162
+ object({
25163
+ kind: literal("no-measured-data"),
25164
+ hardwareClass: HardwareClassIdSchema
25165
+ }),
25166
+ object({
25167
+ kind: literal("no-family-build"),
25168
+ format: ModelFormatSchema$1
25169
+ })
25170
+ ]);
25171
+ var DeviceDemandSchema = object({
25172
+ nodeId: string(),
25173
+ deviceKey: string(),
25174
+ demandFps: number(),
25175
+ maxCameraFps: number(),
25176
+ alwaysOnCameras: number(),
25177
+ onMotionSessions: number(),
25178
+ onMotionSource: _enum([
25179
+ "observed-24h",
25180
+ "estimate",
25181
+ "none"
25182
+ ]),
25183
+ observedForMs: number().nullable(),
25184
+ /** How on-motion sessions were charged: per-device observation, or split by share (D748). */
25185
+ placement: SessionPlacementPolicySchema
25186
+ });
25187
+ var DeviceModelChoiceSchema = object({
25188
+ modelId: string(),
25189
+ p50Ms: number(),
25190
+ fps: number(),
25191
+ measured: boolean(),
25192
+ utilisation: number(),
25193
+ personRecall: number()
25194
+ });
25195
+ var DeviceRecommendationSchema = object({
25196
+ deviceKey: string(),
25197
+ accelerator: AcceleratorKindSchema.nullable(),
25198
+ enabled: boolean(),
25199
+ available: boolean(),
25200
+ demand: DeviceDemandSchema.nullable(),
25201
+ chosen: DeviceModelChoiceSchema.nullable(),
25202
+ reasons: array(RecommendationReasonSchema).readonly()
25203
+ });
25204
+ var NodeRecommendationSchema = object({
25205
+ nodeId: string(),
25206
+ reachable: boolean(),
25207
+ hardwareClass: HardwareClassIdSchema.nullable(),
25208
+ cpuModel: string().nullable(),
25209
+ skipped: boolean(),
25210
+ acceleratorModelId: string().nullable(),
25211
+ devices: array(DeviceRecommendationSchema).readonly(),
25212
+ reasons: array(RecommendationReasonSchema).readonly()
25213
+ });
25214
+ var ModelPinChangeSchema = object({
25215
+ stepId: string(),
25216
+ before: string().nullable(),
25217
+ after: string().nullable()
25218
+ });
25219
+ var NodeDeviceChangeSchema = object({
25220
+ nodeId: string(),
25221
+ deviceKey: string(),
25222
+ madeExplicit: boolean(),
25223
+ modelPins: array(ModelPinChangeSchema).readonly(),
25224
+ thresholds: array(object({
25225
+ stepId: string(),
25226
+ keys: array(string()).readonly()
25227
+ })).readonly()
25228
+ });
25229
+ var CameraOverrideClearSchema = object({
25230
+ deviceId: number(),
25231
+ source: _enum(["step-override", "wholesale"]),
25232
+ nodeId: string(),
25233
+ deviceKey: string().nullable(),
25234
+ stepId: string(),
25235
+ modelIdBefore: string().nullable(),
25236
+ keys: array(string()).readonly()
25237
+ });
25238
+ var DetectorStoreClearSchema = object({
25239
+ deviceId: number(),
25240
+ keys: array(string()).readonly()
25241
+ });
25242
+ var BestSettingsWarningSchema = discriminatedUnion("kind", [
25243
+ object({
25244
+ kind: literal("detector-store-cluster-wide"),
25245
+ skippedNodes: array(string()).readonly()
25246
+ }),
25247
+ object({
25248
+ kind: literal("detector-store-unreadable"),
25249
+ error: string()
25250
+ }),
25251
+ object({
25252
+ kind: literal("coral-enabled"),
25253
+ nodeId: string(),
25254
+ deviceKey: string()
25255
+ })
25256
+ ]);
25257
+ var BestSettingsOptionsSchema = object({
25258
+ /** Share of each accelerator's throughput kept free (0.3–0.5). */
25259
+ headroom: number().min(.3).max(.5).default(.4),
25260
+ /**
25261
+ * What-if shares (D748): plan as if these node/device shares were stored
25262
+ * and the policy were `share`. Absent = the stored shares under the stored
25263
+ * policy. A what-if plan is for display; apply always re-plans from what
25264
+ * is stored.
25265
+ */
25266
+ shares: LoadBalancingSharesInputSchema.optional()
25267
+ });
25268
+ var BestSettingsPlanSchema = object({
25269
+ /** Hash of everything the apply will write — the apply refuses a different one. */
25270
+ planHash: string(),
25271
+ family: literal("yolov9"),
25272
+ headroom: number(),
25273
+ /** Planned under what-if shares (D748): for display — apply refuses it as stale. */
25274
+ whatIf: boolean(),
25275
+ generatedAt: number(),
25276
+ nodes: array(NodeRecommendationSchema).readonly(),
25277
+ nodeChanges: array(NodeDeviceChangeSchema).readonly(),
25278
+ cameraClears: array(CameraOverrideClearSchema).readonly(),
25279
+ detectorStoreClears: array(DetectorStoreClearSchema).readonly(),
25280
+ warnings: array(BestSettingsWarningSchema).readonly()
25281
+ });
25282
+ var BestSettingsApplyInputSchema = object({
25283
+ planHash: string(),
25284
+ headroom: number().min(.3).max(.5).default(.4)
25285
+ });
25286
+ var BestSettingsPhaseResultSchema = object({
25287
+ phase: _enum([
25288
+ "download",
25289
+ "camera-overrides",
25290
+ "detector-thresholds",
25291
+ "node-models",
25292
+ "redispatch"
25293
+ ]),
25294
+ ok: boolean(),
25295
+ detail: string()
25296
+ });
25297
+ var BestSettingsApplyResultSchema = discriminatedUnion("kind", [
25298
+ object({
25299
+ kind: literal("applied"),
25300
+ planHash: string(),
25301
+ phases: array(BestSettingsPhaseResultSchema).readonly()
25302
+ }),
25303
+ object({
25304
+ kind: literal("aborted"),
25305
+ phases: array(BestSettingsPhaseResultSchema).readonly()
25306
+ }),
25307
+ object({
25308
+ kind: literal("stale"),
25309
+ plan: BestSettingsPlanSchema
25310
+ }),
25311
+ object({
25312
+ kind: literal("refused"),
25313
+ reason: _enum(["apply-in-progress"])
25314
+ })
25315
+ ]);
24938
25316
  object({
24939
25317
  activeCameras: number(),
24940
25318
  throttledCameras: number(),
@@ -26343,7 +26721,6 @@ var CameraStepOverridePatchSchema = object({
26343
26721
  settings: record(string(), unknown()).readonly().optional()
26344
26722
  });
26345
26723
  var CameraPipelineSettingsSchema = object({
26346
- pinnedAgentNodeId: string().optional(),
26347
26724
  stepToggles: record(string(), boolean()).optional(),
26348
26725
  stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
26349
26726
  pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
@@ -26991,18 +27368,6 @@ var pipelineOrchestratorCapability = {
26991
27368
  auth: "admin"
26992
27369
  }),
26993
27370
  /**
26994
- * Set a node's detection WEIGHT (relative share for the quota balancer).
26995
- * `null` clears it (back to the implicit weight 1). Unpinned cameras
26996
- * distribute proportionally to weight; `maxCameras` still clamps on top.
26997
- */
26998
- setAgentDetectWeight: method(object({
26999
- agentNodeId: string(),
27000
- detectWeight: number().positive().nullable()
27001
- }), object({ success: literal(true) }), {
27002
- kind: "mutation",
27003
- auth: "admin"
27004
- }),
27005
- /**
27006
27371
  * Set one node's placement CAPABILITIES — the per-node record that
27007
27372
  * replaced the four flat orchestrator lists (`enabledNodes`,
27008
27373
  * `enabledDecoderNodes`, `enabledAudioNodes`, `remoteSourcingNodes`).
@@ -27121,6 +27486,54 @@ var pipelineOrchestratorCapability = {
27121
27486
  kind: "mutation",
27122
27487
  auth: "admin"
27123
27488
  }),
27489
+ /**
27490
+ * Best settings (D745) — DRY RUN. Recommends ONE detector family (YOLOv9)
27491
+ * for the whole cluster from each node's static hardware class, the
27492
+ * measured latency table and the detection demand, and lists every write
27493
+ * the apply would make: the node model pins set, the enrichment pins and
27494
+ * model-bound thresholds removed (node level and per camera), and the
27495
+ * per-camera detector thresholds cleared. Writes nothing. Step
27496
+ * enablement, device enablement, pins, recording, notifications, zones and
27497
+ * rules are never part of it. `planHash` names exactly this diff.
27498
+ */
27499
+ planBestSettings: method(BestSettingsOptionsSchema, BestSettingsPlanSchema, { auth: "admin" }),
27500
+ /**
27501
+ * Best settings (D745) — APPLY the plan the operator confirmed. Recomputes
27502
+ * the plan and refuses (`stale`, with the fresh plan) when its hash is not
27503
+ * `planHash`, so what is written is exactly the confirmed diff. Offline
27504
+ * nodes are skipped; there is no undo.
27505
+ */
27506
+ applyBestSettings: method(BestSettingsApplyInputSchema, BestSettingsApplyResultSchema, {
27507
+ kind: "mutation",
27508
+ auth: "admin"
27509
+ }),
27510
+ /**
27511
+ * Load balancing (D748) — write node shares (`detectWeight`) and per-node
27512
+ * device shares (inference-device `weight`) in ONE atomic read-modify-write
27513
+ * of the agent settings — the only writer of node shares (the per-node
27514
+ * `setAgentDetectWeight` it replaced raced its siblings, one RMW each).
27515
+ * A node or device left out keeps its weight. Never changes the
27516
+ * `sessionPlacement` policy.
27517
+ */
27518
+ setLoadBalancingShares: method(LoadBalancingSharesInputSchema, object({ success: literal(true) }), {
27519
+ kind: "mutation",
27520
+ auth: "admin"
27521
+ }),
27522
+ /**
27523
+ * Load balancing (D748) — what placement actually did: per node and
27524
+ * device, sessions now, sessions started and the time-weighted share of
27525
+ * PARALLEL sessions over `windowMs`; the cluster's parallel peak; yields
27526
+ * a share election made; and every device pin the session path cannot
27527
+ * honour. In memory on the hub — a restart starts a new window.
27528
+ */
27529
+ getPlacementStats: method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
27530
+ /**
27531
+ * Load balancing (D748) — shares proportional to each node's measured
27532
+ * capacity at the models Best settings would pick, iterated (≤ 3 rounds)
27533
+ * until the shares stop moving; per device, an ADVISORY session cap.
27534
+ * Writes nothing.
27535
+ */
27536
+ suggestShares: method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
27124
27537
  /** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
27125
27538
  getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
27126
27539
  /** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
@@ -42922,6 +43335,12 @@ Object.freeze({
42922
43335
  addonId: null,
42923
43336
  access: "delete"
42924
43337
  },
43338
+ "pipelineExecutor.clearModelBoundDeviceOverrides": {
43339
+ capName: "pipeline-executor",
43340
+ capScope: "system",
43341
+ addonId: null,
43342
+ access: "delete"
43343
+ },
42925
43344
  "pipelineExecutor.deleteModel": {
42926
43345
  capName: "pipeline-executor",
42927
43346
  capScope: "system",
@@ -43138,6 +43557,12 @@ Object.freeze({
43138
43557
  addonId: null,
43139
43558
  access: "view"
43140
43559
  },
43560
+ "pipelineOrchestrator.applyBestSettings": {
43561
+ capName: "pipeline-orchestrator",
43562
+ capScope: "system",
43563
+ addonId: null,
43564
+ access: "create"
43565
+ },
43141
43566
  "pipelineOrchestrator.applyDeviceSettingsPatch": {
43142
43567
  capName: "pipeline-orchestrator",
43143
43568
  capScope: "system",
@@ -43288,6 +43713,12 @@ Object.freeze({
43288
43713
  addonId: null,
43289
43714
  access: "view"
43290
43715
  },
43716
+ "pipelineOrchestrator.getPlacementStats": {
43717
+ capName: "pipeline-orchestrator",
43718
+ capScope: "system",
43719
+ addonId: null,
43720
+ access: "view"
43721
+ },
43291
43722
  "pipelineOrchestrator.getReconcileFromDiskStatus": {
43292
43723
  capName: "pipeline-orchestrator",
43293
43724
  capScope: "system",
@@ -43312,6 +43743,12 @@ Object.freeze({
43312
43743
  addonId: null,
43313
43744
  access: "create"
43314
43745
  },
43746
+ "pipelineOrchestrator.planBestSettings": {
43747
+ capName: "pipeline-orchestrator",
43748
+ capScope: "system",
43749
+ addonId: null,
43750
+ access: "view"
43751
+ },
43315
43752
  "pipelineOrchestrator.rebalance": {
43316
43753
  capName: "pipeline-orchestrator",
43317
43754
  capScope: "system",
@@ -43372,12 +43809,6 @@ Object.freeze({
43372
43809
  addonId: null,
43373
43810
  access: "create"
43374
43811
  },
43375
- "pipelineOrchestrator.setAgentDetectWeight": {
43376
- capName: "pipeline-orchestrator",
43377
- capScope: "system",
43378
- addonId: null,
43379
- access: "create"
43380
- },
43381
43812
  "pipelineOrchestrator.setAgentInferenceDevices": {
43382
43813
  capName: "pipeline-orchestrator",
43383
43814
  capScope: "system",
@@ -43432,12 +43863,24 @@ Object.freeze({
43432
43863
  addonId: null,
43433
43864
  access: "create"
43434
43865
  },
43866
+ "pipelineOrchestrator.setLoadBalancingShares": {
43867
+ capName: "pipeline-orchestrator",
43868
+ capScope: "system",
43869
+ addonId: null,
43870
+ access: "create"
43871
+ },
43435
43872
  "pipelineOrchestrator.setPipelineDevicePin": {
43436
43873
  capName: "pipeline-orchestrator",
43437
43874
  capScope: "system",
43438
43875
  addonId: null,
43439
43876
  access: "create"
43440
43877
  },
43878
+ "pipelineOrchestrator.suggestShares": {
43879
+ capName: "pipeline-orchestrator",
43880
+ capScope: "system",
43881
+ addonId: null,
43882
+ access: "view"
43883
+ },
43441
43884
  "pipelineOrchestrator.unassignAudio": {
43442
43885
  capName: "pipeline-orchestrator",
43443
43886
  capScope: "system",
@@ -48762,6 +49205,11 @@ Object.freeze({
48762
49205
  form: "single",
48763
49206
  optional: false
48764
49207
  }],
49208
+ "pipelineExecutor.clearModelBoundDeviceOverrides": [{
49209
+ name: "deviceIds",
49210
+ form: "array",
49211
+ optional: true
49212
+ }],
48765
49213
  "pipelineExecutor.runPipeline": [{
48766
49214
  name: "deviceId",
48767
49215
  form: "single",
@@ -50656,6 +51104,891 @@ var REMOVED_MODEL_IDS = {
50656
51104
  function isRemovedModelId(stepId, modelId) {
50657
51105
  return REMOVED_MODEL_IDS[stepId]?.includes(modelId) ?? false;
50658
51106
  }
51107
+ /** Suffix of the sibling key that names the model a stored value was made for. */
51108
+ var MODEL_BINDING_SUFFIX = "ModelId";
51109
+ /** `minConfidencePerson` → `minConfidencePersonModelId`. */
51110
+ function modelBindingKey(key) {
51111
+ return `${key}${MODEL_BINDING_SUFFIX}`;
51112
+ }
51113
+ /**
51114
+ * The keys of every `bindsToModel` field of a step's config schema, groups
51115
+ * flattened (D738). One definition for the runner (which applies the binding)
51116
+ * and the orchestrator's Best settings (which clears those overrides, D745).
51117
+ */
51118
+ function collectModelBoundKeys(fields) {
51119
+ return fields.flatMap((field) => {
51120
+ if (field.type === "group") return collectModelBoundKeys(field.fields);
51121
+ return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
51122
+ });
51123
+ }
51124
+ var APPLE_CHIP = /^Apple M(\d+)(?:\s+(Pro|Max|Ultra))?\b/i;
51125
+ var INTEL_CORE_ULTRA = /Core\(TM\)\s+Ultra|Core Ultra/i;
51126
+ var INTEL_N_SERIES = /Intel\(R\)\s+N\d{2,3}\b|\bIntel\s+N\d{2,3}\b/i;
51127
+ function parseAppleChip(cpuModel) {
51128
+ const match = APPLE_CHIP.exec(cpuModel.trim());
51129
+ if (match === null) return null;
51130
+ const generation = Number(match[1]);
51131
+ const variantRaw = (match[2] ?? "base").toLowerCase();
51132
+ return {
51133
+ generation,
51134
+ variant: variantRaw === "pro" || variantRaw === "max" || variantRaw === "ultra" ? variantRaw : "base"
51135
+ };
51136
+ }
51137
+ /**
51138
+ * Classify a node. Apple base chips (M1–M4) map to the measured M2 column, the
51139
+ * Pro/Max/Ultra chips to the measured M3 Pro column — the M2 was measured at
51140
+ * 1.0–1.2× the M3 Pro on the Neural Engine, so the generation inside a variant
51141
+ * matters less than the column's own error.
51142
+ */
51143
+ function classifyInferenceHardware(hw) {
51144
+ const cpuModel = hw.cpuModel;
51145
+ const apple = parseAppleChip(cpuModel);
51146
+ if (apple !== null) return {
51147
+ hardwareClass: apple.variant === "base" ? "apple-m-base" : "apple-m-pro",
51148
+ cpuModel,
51149
+ apple
51150
+ };
51151
+ if (INTEL_CORE_ULTRA.test(cpuModel)) return {
51152
+ hardwareClass: "intel-core-ultra",
51153
+ cpuModel
51154
+ };
51155
+ if (INTEL_N_SERIES.test(cpuModel)) return {
51156
+ hardwareClass: "intel-n-series",
51157
+ cpuModel
51158
+ };
51159
+ return {
51160
+ hardwareClass: "unknown",
51161
+ cpuModel
51162
+ };
51163
+ }
51164
+ /**
51165
+ * The accelerator kind of one of a node's device keys (`openvino:npu`,
51166
+ * `openvino:gpu`, `coreml:all`, `edgetpu:usb`, `cpu`, `onnx:cpu`, …). `null`
51167
+ * for a key this table has no column for.
51168
+ */
51169
+ function acceleratorKindOf(deviceKey, hw) {
51170
+ const [backend = "", device = ""] = deviceKey.split(":");
51171
+ if (backend === "edgetpu") return "coral";
51172
+ if (backend === "coreml") return device === "cpu" ? "arm-cpu" : "apple-ane";
51173
+ if (backend === "openvino" && device === "npu") return "intel-npu";
51174
+ if (backend === "openvino" && device === "gpu") return "intel-igpu";
51175
+ if (backend === "cpu" || device === "cpu") return hw.arch === "arm64" ? "arm-cpu" : "x86-cpu";
51176
+ return null;
51177
+ }
51178
+ var DETECTOR_PERF = {
51179
+ "intel-core-ultra": {
51180
+ "intel-npu": {
51181
+ "yolov9t-320": {
51182
+ p50Ms: 2.9,
51183
+ fps: 383,
51184
+ measured: true
51185
+ },
51186
+ "yolov9t-320-int8": {
51187
+ p50Ms: 3.8,
51188
+ fps: 333,
51189
+ measured: true
51190
+ },
51191
+ "yolov9t-640": {
51192
+ p50Ms: 7.4,
51193
+ fps: 139,
51194
+ measured: true
51195
+ },
51196
+ "yolov9t-640-int8": {
51197
+ p50Ms: 7.5,
51198
+ fps: 144,
51199
+ measured: true
51200
+ },
51201
+ "yolov9s-320": {
51202
+ p50Ms: 4.4,
51203
+ fps: 241,
51204
+ measured: true
51205
+ },
51206
+ "yolov9s-320-int8": {
51207
+ p50Ms: 4.1,
51208
+ fps: 264,
51209
+ measured: true
51210
+ },
51211
+ "yolov9s-640": {
51212
+ p50Ms: 12.7,
51213
+ fps: 82,
51214
+ measured: true
51215
+ },
51216
+ "yolov9s-640-int8": {
51217
+ p50Ms: 10.7,
51218
+ fps: 99,
51219
+ measured: true
51220
+ },
51221
+ "yolov9m-320": {
51222
+ p50Ms: 8.4,
51223
+ fps: 123,
51224
+ measured: true
51225
+ },
51226
+ "yolov9m-320-int8": {
51227
+ p50Ms: 6.5,
51228
+ fps: 165,
51229
+ measured: true
51230
+ },
51231
+ "yolov9m-640": {
51232
+ p50Ms: 29.5,
51233
+ fps: 32,
51234
+ measured: true
51235
+ },
51236
+ "yolov9m-640-int8": {
51237
+ p50Ms: 19.5,
51238
+ fps: 52,
51239
+ measured: true
51240
+ }
51241
+ },
51242
+ "intel-igpu": {
51243
+ "yolov9t-320": {
51244
+ p50Ms: 2.6,
51245
+ fps: 405,
51246
+ measured: true
51247
+ },
51248
+ "yolov9t-320-int8": {
51249
+ p50Ms: 2.8,
51250
+ fps: 324,
51251
+ measured: true
51252
+ },
51253
+ "yolov9t-640": {
51254
+ p50Ms: 6.5,
51255
+ fps: 157,
51256
+ measured: true
51257
+ },
51258
+ "yolov9t-640-int8": {
51259
+ p50Ms: 5.6,
51260
+ fps: 185,
51261
+ measured: true
51262
+ },
51263
+ "yolov9s-320": {
51264
+ p50Ms: 4.6,
51265
+ fps: 200,
51266
+ measured: true
51267
+ },
51268
+ "yolov9s-320-int8": {
51269
+ p50Ms: 4.1,
51270
+ fps: 239,
51271
+ measured: true
51272
+ },
51273
+ "yolov9s-640": {
51274
+ p50Ms: 15.2,
51275
+ fps: 69,
51276
+ measured: true
51277
+ },
51278
+ "yolov9s-640-int8": {
51279
+ p50Ms: 10.5,
51280
+ fps: 99,
51281
+ measured: true
51282
+ },
51283
+ "yolov9m-320": {
51284
+ p50Ms: 11.4,
51285
+ fps: 88,
51286
+ measured: true
51287
+ },
51288
+ "yolov9m-320-int8": {
51289
+ p50Ms: 7,
51290
+ fps: 127,
51291
+ measured: true
51292
+ },
51293
+ "yolov9m-640": {
51294
+ p50Ms: 40,
51295
+ fps: 25,
51296
+ measured: true
51297
+ },
51298
+ "yolov9m-640-int8": {
51299
+ p50Ms: 23.5,
51300
+ fps: 44,
51301
+ measured: true
51302
+ }
51303
+ },
51304
+ "x86-cpu": {
51305
+ "yolov9t-320": {
51306
+ p50Ms: 6.9,
51307
+ fps: 145,
51308
+ measured: true
51309
+ },
51310
+ "yolov9t-320-int8": {
51311
+ p50Ms: 2.7,
51312
+ fps: 405,
51313
+ measured: true
51314
+ },
51315
+ "yolov9t-640": {
51316
+ p50Ms: 23,
51317
+ fps: 44,
51318
+ measured: true
51319
+ },
51320
+ "yolov9t-640-int8": {
51321
+ p50Ms: 8.8,
51322
+ fps: 124,
51323
+ measured: true
51324
+ },
51325
+ "yolov9s-320": {
51326
+ p50Ms: 15.8,
51327
+ fps: 63,
51328
+ measured: true
51329
+ },
51330
+ "yolov9s-320-int8": {
51331
+ p50Ms: 6.5,
51332
+ fps: 180,
51333
+ measured: true
51334
+ },
51335
+ "yolov9s-640": {
51336
+ p50Ms: 58.2,
51337
+ fps: 17,
51338
+ measured: true
51339
+ },
51340
+ "yolov9s-640-int8": {
51341
+ p50Ms: 20.1,
51342
+ fps: 52,
51343
+ measured: true
51344
+ },
51345
+ "yolov9m-320": {
51346
+ p50Ms: 40,
51347
+ fps: 25,
51348
+ measured: true
51349
+ },
51350
+ "yolov9m-320-int8": {
51351
+ p50Ms: 14.2,
51352
+ fps: 74,
51353
+ measured: true
51354
+ },
51355
+ "yolov9m-640": {
51356
+ p50Ms: 155.8,
51357
+ fps: 6,
51358
+ measured: true
51359
+ },
51360
+ "yolov9m-640-int8": {
51361
+ p50Ms: 52.4,
51362
+ fps: 19,
51363
+ measured: true
51364
+ }
51365
+ }
51366
+ },
51367
+ "intel-n-series": {
51368
+ "intel-igpu": {
51369
+ "yolov9t-320": {
51370
+ p50Ms: 15,
51371
+ fps: 48,
51372
+ measured: true
51373
+ },
51374
+ "yolov9t-320-int8": {
51375
+ p50Ms: 14.1,
51376
+ fps: 67,
51377
+ measured: true
51378
+ },
51379
+ "yolov9t-640": {
51380
+ p50Ms: 38.5,
51381
+ fps: 26,
51382
+ measured: true
51383
+ },
51384
+ "yolov9t-640-int8": {
51385
+ p50Ms: 33.1,
51386
+ fps: 33,
51387
+ measured: true
51388
+ },
51389
+ "yolov9s-320": {
51390
+ p50Ms: 41.4,
51391
+ fps: 24,
51392
+ measured: true
51393
+ },
51394
+ "yolov9s-320-int8": {
51395
+ p50Ms: 23.3,
51396
+ fps: 34,
51397
+ measured: true
51398
+ },
51399
+ "yolov9s-640": {
51400
+ p50Ms: 127.4,
51401
+ fps: 8,
51402
+ measured: true
51403
+ },
51404
+ "yolov9s-640-int8": {
51405
+ p50Ms: 92.9,
51406
+ fps: 11,
51407
+ measured: true
51408
+ },
51409
+ "yolov9m-320": {
51410
+ p50Ms: 63.5,
51411
+ fps: 16,
51412
+ measured: true
51413
+ },
51414
+ "yolov9m-320-int8": {
51415
+ p50Ms: 39.4,
51416
+ fps: 26,
51417
+ measured: true
51418
+ },
51419
+ "yolov9m-640": {
51420
+ p50Ms: 215.5,
51421
+ fps: 5,
51422
+ measured: true
51423
+ },
51424
+ "yolov9m-640-int8": {
51425
+ p50Ms: 139.1,
51426
+ fps: 7,
51427
+ measured: true
51428
+ }
51429
+ },
51430
+ "x86-cpu": {
51431
+ "yolov9t-320": {
51432
+ p50Ms: 86.2,
51433
+ fps: 12,
51434
+ measured: true
51435
+ },
51436
+ "yolov9t-320-int8": {
51437
+ p50Ms: 52.7,
51438
+ fps: 20,
51439
+ measured: true
51440
+ },
51441
+ "yolov9t-640": {
51442
+ p50Ms: 322.1,
51443
+ fps: 3,
51444
+ measured: true
51445
+ },
51446
+ "yolov9t-640-int8": {
51447
+ p50Ms: 205.6,
51448
+ fps: 5,
51449
+ measured: true
51450
+ },
51451
+ "yolov9s-320": {
51452
+ p50Ms: 164.4,
51453
+ fps: 6,
51454
+ measured: true
51455
+ },
51456
+ "yolov9s-320-int8": {
51457
+ p50Ms: 86.9,
51458
+ fps: 12,
51459
+ measured: true
51460
+ },
51461
+ "yolov9s-640": {
51462
+ p50Ms: 341.3,
51463
+ fps: 3,
51464
+ measured: true
51465
+ },
51466
+ "yolov9s-640-int8": {
51467
+ p50Ms: 185.3,
51468
+ fps: 5,
51469
+ measured: true
51470
+ },
51471
+ "yolov9m-320": {
51472
+ p50Ms: 166.8,
51473
+ fps: 6,
51474
+ measured: true
51475
+ },
51476
+ "yolov9m-320-int8": {
51477
+ p50Ms: 226.3,
51478
+ fps: 4,
51479
+ measured: true
51480
+ },
51481
+ "yolov9m-640": {
51482
+ p50Ms: 663.5,
51483
+ fps: 2,
51484
+ measured: true
51485
+ },
51486
+ "yolov9m-640-int8": {
51487
+ p50Ms: 324.4,
51488
+ fps: 3,
51489
+ measured: true
51490
+ }
51491
+ }
51492
+ },
51493
+ "apple-m-pro": { "apple-ane": {
51494
+ "yolov9t-320": {
51495
+ p50Ms: 1.1,
51496
+ fps: 952,
51497
+ measured: true
51498
+ },
51499
+ "yolov9t-640": {
51500
+ p50Ms: 3,
51501
+ fps: 333,
51502
+ measured: true
51503
+ },
51504
+ "yolov9s-320": {
51505
+ p50Ms: 1.5,
51506
+ fps: 667,
51507
+ measured: true
51508
+ },
51509
+ "yolov9s-640": {
51510
+ p50Ms: 5.1,
51511
+ fps: 196,
51512
+ measured: true
51513
+ },
51514
+ "yolov9m-320": {
51515
+ p50Ms: 3.2,
51516
+ fps: 312,
51517
+ measured: true
51518
+ },
51519
+ "yolov9m-640": {
51520
+ p50Ms: 10.2,
51521
+ fps: 98,
51522
+ measured: true
51523
+ }
51524
+ } },
51525
+ "apple-m-base": { "apple-ane": {
51526
+ "yolov9t-320": {
51527
+ p50Ms: 1.32,
51528
+ fps: 758,
51529
+ measured: false
51530
+ },
51531
+ "yolov9t-640": {
51532
+ p50Ms: 3.45,
51533
+ fps: 290,
51534
+ measured: true
51535
+ },
51536
+ "yolov9s-320": {
51537
+ p50Ms: 1.8,
51538
+ fps: 556,
51539
+ measured: false
51540
+ },
51541
+ "yolov9s-640": {
51542
+ p50Ms: 5.46,
51543
+ fps: 183,
51544
+ measured: true
51545
+ },
51546
+ "yolov9m-320": {
51547
+ p50Ms: 3.79,
51548
+ fps: 264,
51549
+ measured: true
51550
+ },
51551
+ "yolov9m-640": {
51552
+ p50Ms: 10.34,
51553
+ fps: 97,
51554
+ measured: true
51555
+ }
51556
+ } }
51557
+ };
51558
+ var DETECTOR_ACCURACY = {
51559
+ "yolov9m-320": {
51560
+ personPrecision: .979,
51561
+ personRecall: .649
51562
+ },
51563
+ "yolov9m-320-int8": {
51564
+ personPrecision: .974,
51565
+ personRecall: .684
51566
+ },
51567
+ "yolov9m-640": {
51568
+ personPrecision: .976,
51569
+ personRecall: .854
51570
+ },
51571
+ "yolov9m-640-int8": {
51572
+ personPrecision: .976,
51573
+ personRecall: .847
51574
+ },
51575
+ "yolov9s-320": {
51576
+ personPrecision: .958,
51577
+ personRecall: .479
51578
+ },
51579
+ "yolov9s-320-int8": {
51580
+ personPrecision: .969,
51581
+ personRecall: .438
51582
+ },
51583
+ "yolov9s-640": {
51584
+ personPrecision: .986,
51585
+ personRecall: .753
51586
+ },
51587
+ "yolov9s-640-int8": {
51588
+ personPrecision: .963,
51589
+ personRecall: .823
51590
+ },
51591
+ "yolov9t-320": {
51592
+ personPrecision: .946,
51593
+ personRecall: .368
51594
+ },
51595
+ "yolov9t-320-int8": {
51596
+ personPrecision: .943,
51597
+ personRecall: .347
51598
+ },
51599
+ "yolov9t-640": {
51600
+ personPrecision: .979,
51601
+ personRecall: .646
51602
+ },
51603
+ "yolov9t-640-int8": {
51604
+ personPrecision: .979,
51605
+ personRecall: .646
51606
+ }
51607
+ };
51608
+ var DEFAULT_DEMAND_OPTIONS = {
51609
+ onMotionConcurrency: .25,
51610
+ onMotionMinConcurrent: 2
51611
+ };
51612
+ function deviceId(nodeId, deviceKey) {
51613
+ return `${nodeId}\u0000${deviceKey}`;
51614
+ }
51615
+ function charges(load) {
51616
+ return load.mode !== "disabled" && load.runsPipelineRoot && load.fps > 0;
51617
+ }
51618
+ /** The highest `count` rates, summed. */
51619
+ function topRatesSum(rates, count) {
51620
+ return rates.toSorted((a, b) => b - a).slice(0, Math.max(0, count)).reduce((sum, r) => sum + r, 0);
51621
+ }
51622
+ /** Concurrent on-motion sessions assumed cluster-wide without an observation. */
51623
+ function estimatedConcurrentSessions(cameraCount, options) {
51624
+ if (cameraCount <= 0) return 0;
51625
+ const share = Math.ceil(options.onMotionConcurrency * cameraCount);
51626
+ return Math.min(cameraCount, Math.max(share, Math.min(cameraCount, options.onMotionMinConcurrent)));
51627
+ }
51628
+ /**
51629
+ * Demand on every eligible device of the cluster.
51630
+ */
51631
+ function estimateDeviceDemand(input) {
51632
+ const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
51633
+ const placement = input.shares ? "share" : "least-loaded";
51634
+ const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
51635
+ const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
51636
+ const weightByNode = /* @__PURE__ */ new Map();
51637
+ const eligibleCountByNode = /* @__PURE__ */ new Map();
51638
+ for (const d of input.eligible) {
51639
+ weightByNode.set(d.nodeId, (weightByNode.get(d.nodeId) ?? 0) + Math.max(d.weight, 0));
51640
+ eligibleCountByNode.set(d.nodeId, (eligibleCountByNode.get(d.nodeId) ?? 0) + 1);
51641
+ }
51642
+ const pinnedFps = /* @__PURE__ */ new Map();
51643
+ const alwaysOnFps = /* @__PURE__ */ new Map();
51644
+ const alwaysOnCount = /* @__PURE__ */ new Map();
51645
+ const maxFps = /* @__PURE__ */ new Map();
51646
+ const onMotionRates = [];
51647
+ const bump = (map, key, by) => {
51648
+ map.set(key, (map.get(key) ?? 0) + by);
51649
+ };
51650
+ const noteMax = (key, fps) => {
51651
+ maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
51652
+ };
51653
+ const pinNodeOf = (pin) => {
51654
+ if (pin.nodeId !== null) return pin.nodeId;
51655
+ const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
51656
+ return holders.length === 1 ? holders[0].nodeId : null;
51657
+ };
51658
+ let alwaysOnTotal = 0;
51659
+ for (const cam of input.cameras) {
51660
+ if (!charges(cam)) continue;
51661
+ const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
51662
+ if (cam.devicePin !== null && pinNode !== null) {
51663
+ const key = deviceId(pinNode, cam.devicePin.deviceKey);
51664
+ if (!eligibleById.has(key)) continue;
51665
+ bump(pinnedFps, key, cam.fps);
51666
+ noteMax(key, cam.fps);
51667
+ continue;
51668
+ }
51669
+ const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
51670
+ if (standing !== null) {
51671
+ if (cam.mode === "always-on") alwaysOnTotal += 1;
51672
+ const nodeWeight = weightByNode.get(standing) ?? 0;
51673
+ for (const d of input.eligible) {
51674
+ if (d.nodeId !== standing || nodeWeight <= 0) continue;
51675
+ const key = deviceId(d.nodeId, d.deviceKey);
51676
+ const share = Math.max(d.weight, 0) / nodeWeight;
51677
+ bump(alwaysOnFps, key, cam.fps * share);
51678
+ bump(alwaysOnCount, key, share);
51679
+ noteMax(key, cam.fps);
51680
+ }
51681
+ continue;
51682
+ }
51683
+ onMotionRates.push(cam.fps);
51684
+ }
51685
+ const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
51686
+ const nodeCount = weightByNode.size;
51687
+ const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
51688
+ const clusterPeak = input.shares?.clusterPeak ?? null;
51689
+ const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
51690
+ const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
51691
+ const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
51692
+ return input.eligible.map((d) => {
51693
+ const key = deviceId(d.nodeId, d.deviceKey);
51694
+ const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
51695
+ const alwaysOn = alwaysOnCount.get(key) ?? 0;
51696
+ let onMotionSessions = 0;
51697
+ let onMotionSource = "none";
51698
+ if (onMotionRates.length > 0 && input.shares !== void 0) {
51699
+ const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
51700
+ const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
51701
+ const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
51702
+ onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
51703
+ onMotionSource = clusterObserved ? "observed-24h" : "estimate";
51704
+ } else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
51705
+ onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
51706
+ onMotionSource = "observed-24h";
51707
+ } else {
51708
+ const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
51709
+ const share = nodeCount > 0 && nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight / nodeCount : 0;
51710
+ onMotionSessions = Math.ceil(estimatedTotal * share);
51711
+ onMotionSource = "estimate";
51712
+ }
51713
+ const demandFps = (pinnedFps.get(key) ?? 0) + (alwaysOnFps.get(key) ?? 0) + topRatesSum(onMotionRates, onMotionSessions);
51714
+ return {
51715
+ nodeId: d.nodeId,
51716
+ deviceKey: d.deviceKey,
51717
+ demandFps: Math.round(demandFps * 100) / 100,
51718
+ maxCameraFps: Math.max(maxFps.get(key) ?? 0, fastestOnMotion),
51719
+ alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
51720
+ onMotionSessions,
51721
+ onMotionSource,
51722
+ observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
51723
+ placement
51724
+ };
51725
+ }).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
51726
+ }
51727
+ /** The family the cluster runs. */
51728
+ var BEST_SETTINGS_FAMILY = "yolov9";
51729
+ function round(value, digits = 3) {
51730
+ const f = 10 ** digits;
51731
+ return Math.round(value * f) / f;
51732
+ }
51733
+ function utilisationOf(cell, demand) {
51734
+ return cell.fps > 0 ? round((demand?.demandFps ?? 0) / cell.fps) : Number.POSITIVE_INFINITY;
51735
+ }
51736
+ function fitsOn(cell, demand, headroom) {
51737
+ const budget = cell.fps * (1 - headroom);
51738
+ return budget >= (demand?.demandFps ?? 0) && budget >= (demand?.maxCameraFps ?? 0);
51739
+ }
51740
+ function candidatesFor(contexts, formatsByModel) {
51741
+ return Object.keys(DETECTOR_ACCURACY).filter((id) => id.startsWith(BEST_SETTINGS_FAMILY)).filter((id) => contexts.every((c) => c.column[id] !== void 0 && (formatsByModel[id] ?? []).includes(c.device.format))).map((id) => ({
51742
+ modelId: id,
51743
+ personRecall: DETECTOR_ACCURACY[id]?.personRecall ?? 0
51744
+ })).toSorted((a, b) => b.personRecall - a.personRecall || a.modelId.localeCompare(b.modelId));
51745
+ }
51746
+ function totalLatency(contexts, modelId) {
51747
+ return contexts.reduce((sum, c) => sum + (c.column[modelId]?.p50Ms ?? Number.POSITIVE_INFINITY), 0);
51748
+ }
51749
+ function worstUtilisation(contexts, modelId) {
51750
+ return contexts.reduce((worst, c) => {
51751
+ const cell = c.column[modelId];
51752
+ return Math.max(worst, cell ? utilisationOf(cell, c.demand) : Number.POSITIVE_INFINITY);
51753
+ }, 0);
51754
+ }
51755
+ /** One model for a group of devices that must agree (step 2–4 of the header). */
51756
+ function pickForGroup(contexts, formatsByModel, headroom) {
51757
+ const candidates = candidatesFor(contexts, formatsByModel);
51758
+ if (candidates.length === 0) return null;
51759
+ const fitting = candidates.filter((c) => contexts.every((ctx) => {
51760
+ const cell = ctx.column[c.modelId];
51761
+ return cell !== void 0 && fitsOn(cell, ctx.demand, headroom);
51762
+ }));
51763
+ if (fitting.length === 0) {
51764
+ const lightest = candidates.toSorted((a, b) => worstUtilisation(contexts, a.modelId) - worstUtilisation(contexts, b.modelId))[0];
51765
+ if (lightest === void 0) return null;
51766
+ return {
51767
+ modelId: lightest.modelId,
51768
+ reasons: [{
51769
+ kind: "no-fit-lightest",
51770
+ utilisation: worstUtilisation(contexts, lightest.modelId)
51771
+ }]
51772
+ };
51773
+ }
51774
+ const best = fitting[0];
51775
+ if (best === void 0) return null;
51776
+ const chosen = fitting.filter((c) => best.personRecall - c.personRecall <= .02).toSorted((a, b) => totalLatency(contexts, a.modelId) - totalLatency(contexts, b.modelId) || b.personRecall - a.personRecall)[0] ?? best;
51777
+ const reasons = [];
51778
+ const mostAccurate = candidates[0];
51779
+ if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
51780
+ const limitedBy = contexts.find((ctx) => {
51781
+ const cell = ctx.column[mostAccurate.modelId];
51782
+ return cell === void 0 || !fitsOn(cell, ctx.demand, headroom);
51783
+ })?.device.key ?? "";
51784
+ reasons.push({
51785
+ kind: "stepped-down",
51786
+ from: mostAccurate.modelId,
51787
+ to: best.modelId,
51788
+ limitedBy
51789
+ });
51790
+ }
51791
+ if (chosen.modelId !== best.modelId) reasons.push({
51792
+ kind: "tie-faster",
51793
+ over: best.modelId,
51794
+ recallDelta: round(best.personRecall - chosen.personRecall)
51795
+ });
51796
+ return {
51797
+ modelId: chosen.modelId,
51798
+ reasons
51799
+ };
51800
+ }
51801
+ function choiceOn(ctx, modelId) {
51802
+ const cell = ctx.column[modelId];
51803
+ if (cell === void 0) return null;
51804
+ return {
51805
+ modelId,
51806
+ p50Ms: cell.p50Ms,
51807
+ fps: cell.fps,
51808
+ measured: cell.measured,
51809
+ utilisation: utilisationOf(cell, ctx.demand),
51810
+ personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
51811
+ };
51812
+ }
51813
+ function deviceReasonsFor(ctx, choice) {
51814
+ const out = [{
51815
+ kind: "fits",
51816
+ utilisation: choice.utilisation
51817
+ }];
51818
+ if (!choice.measured) out.push({
51819
+ kind: "derived-latency",
51820
+ modelId: choice.modelId
51821
+ });
51822
+ if (ctx.demand?.onMotionSource === "estimate") out.push({
51823
+ kind: "estimated-load",
51824
+ observedForMs: ctx.demand.observedForMs
51825
+ });
51826
+ return out;
51827
+ }
51828
+ function skippedNode(node, reason, hardwareClass) {
51829
+ return {
51830
+ nodeId: node.nodeId,
51831
+ reachable: node.reachable,
51832
+ hardwareClass,
51833
+ cpuModel: node.hardware?.cpuModel ?? null,
51834
+ skipped: true,
51835
+ acceleratorModelId: null,
51836
+ devices: node.devices.map((d) => ({
51837
+ deviceKey: d.key,
51838
+ accelerator: node.hardware ? acceleratorKindOf(d.key, node.hardware) : null,
51839
+ enabled: d.enabled,
51840
+ available: d.available,
51841
+ demand: null,
51842
+ chosen: null,
51843
+ reasons: []
51844
+ })),
51845
+ reasons: [reason]
51846
+ };
51847
+ }
51848
+ /** Recommend one node. See the module header. */
51849
+ function recommendNode(input) {
51850
+ const { node } = input;
51851
+ const headroom = input.headroom ?? .4;
51852
+ const perf = input.perf ?? DETECTOR_PERF;
51853
+ if (!node.reachable) return skippedNode(node, { kind: "node-offline" }, null);
51854
+ if (node.hardware === null) return skippedNode(node, { kind: "no-hardware-facts" }, null);
51855
+ const hardware = node.hardware;
51856
+ const { hardwareClass, cpuModel } = classifyInferenceHardware(hardware);
51857
+ const classTable = perf[hardwareClass];
51858
+ if (hardwareClass === "unknown" || classTable === void 0) return skippedNode(node, {
51859
+ kind: "no-measured-data",
51860
+ hardwareClass
51861
+ }, hardwareClass);
51862
+ const demandByKey = new Map(input.demand.filter((d) => d.nodeId === node.nodeId).map((d) => [d.deviceKey, d]));
51863
+ const devices = node.devices.toSorted((a, b) => a.key.localeCompare(b.key));
51864
+ const contexts = /* @__PURE__ */ new Map();
51865
+ const early = /* @__PURE__ */ new Map();
51866
+ for (const device of devices) {
51867
+ const accelerator = acceleratorKindOf(device.key, hardware);
51868
+ if (accelerator === "coral") {
51869
+ early.set(device.key, device.enabled ? [{ kind: "coral-untouched" }] : []);
51870
+ continue;
51871
+ }
51872
+ if (!device.enabled) {
51873
+ early.set(device.key, [{ kind: "device-disabled" }]);
51874
+ continue;
51875
+ }
51876
+ if (!device.available) {
51877
+ early.set(device.key, [{ kind: "device-unavailable" }]);
51878
+ continue;
51879
+ }
51880
+ const column = accelerator === null ? void 0 : classTable[accelerator];
51881
+ if (accelerator === null || column === void 0) {
51882
+ early.set(device.key, [{
51883
+ kind: "no-family-build",
51884
+ format: device.format
51885
+ }]);
51886
+ continue;
51887
+ }
51888
+ contexts.set(device.key, {
51889
+ device,
51890
+ accelerator,
51891
+ column,
51892
+ demand: demandByKey.get(device.key) ?? null
51893
+ });
51894
+ }
51895
+ const isCpu = (a) => a === "x86-cpu" || a === "arm-cpu";
51896
+ const accelerators = [...contexts.values()].filter((c) => !isCpu(c.accelerator));
51897
+ const cpus = [...contexts.values()].filter((c) => isCpu(c.accelerator));
51898
+ const chosenByKey = /* @__PURE__ */ new Map();
51899
+ const reasonsByKey = /* @__PURE__ */ new Map();
51900
+ const nodeReasons = [];
51901
+ let acceleratorModelId = null;
51902
+ const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, headroom) : null;
51903
+ if (groupPick !== null) {
51904
+ acceleratorModelId = groupPick.modelId;
51905
+ nodeReasons.push(...groupPick.reasons);
51906
+ for (const ctx of accelerators) {
51907
+ const choice = choiceOn(ctx, groupPick.modelId);
51908
+ if (choice === null) continue;
51909
+ chosenByKey.set(ctx.device.key, choice);
51910
+ reasonsByKey.set(ctx.device.key, deviceReasonsFor(ctx, choice));
51911
+ }
51912
+ } else for (const ctx of accelerators) reasonsByKey.set(ctx.device.key, [{
51913
+ kind: "no-family-build",
51914
+ format: ctx.device.format
51915
+ }]);
51916
+ for (const ctx of cpus) {
51917
+ const pick = pickForGroup([ctx], node.formatsByModel, headroom);
51918
+ const choice = pick === null ? null : choiceOn(ctx, pick.modelId);
51919
+ if (pick === null || choice === null) {
51920
+ reasonsByKey.set(ctx.device.key, [{
51921
+ kind: "no-family-build",
51922
+ format: ctx.device.format
51923
+ }]);
51924
+ continue;
51925
+ }
51926
+ chosenByKey.set(ctx.device.key, choice);
51927
+ reasonsByKey.set(ctx.device.key, [
51928
+ { kind: "cpu-device" },
51929
+ ...pick.reasons,
51930
+ ...deviceReasonsFor(ctx, choice)
51931
+ ]);
51932
+ }
51933
+ return {
51934
+ nodeId: node.nodeId,
51935
+ reachable: true,
51936
+ hardwareClass,
51937
+ cpuModel,
51938
+ skipped: chosenByKey.size === 0,
51939
+ acceleratorModelId,
51940
+ devices: devices.map((device) => ({
51941
+ deviceKey: device.key,
51942
+ accelerator: acceleratorKindOf(device.key, hardware),
51943
+ enabled: device.enabled,
51944
+ available: device.available,
51945
+ demand: demandByKey.get(device.key) ?? null,
51946
+ chosen: chosenByKey.get(device.key) ?? null,
51947
+ reasons: early.get(device.key) ?? reasonsByKey.get(device.key) ?? []
51948
+ })),
51949
+ reasons: nodeReasons
51950
+ };
51951
+ }
51952
+ /** Deficits equal up to float noise (0.7 × 2 − 1 is 0.3999…, not 0.4). */
51953
+ var DEFICIT_EPSILON = 1e-9;
51954
+ function byDeficit(a, b) {
51955
+ const diff = b.deficit - a.deficit;
51956
+ return (Math.abs(diff) < DEFICIT_EPSILON ? 0 : diff) || b.candidate.share - a.candidate.share || a.candidate.key.localeCompare(b.candidate.key);
51957
+ }
51958
+ function spareOf(c) {
51959
+ return c.spare ?? Number.POSITIVE_INFINITY;
51960
+ }
51961
+ function electByShare(candidates) {
51962
+ if (candidates.length === 0) return { kind: "none" };
51963
+ const shareSum = candidates.reduce((s, c) => s + Math.max(c.share, 0), 0);
51964
+ const running = candidates.reduce((s, c) => s + Math.max(c.running, 0), 0);
51965
+ const scored = candidates.map((candidate) => ({
51966
+ candidate,
51967
+ deficit: (shareSum > 0 ? Math.max(candidate.share, 0) / shareSum : 1 / candidates.length) * (running + 1) - candidate.running
51968
+ })).toSorted(byDeficit);
51969
+ const turn = scored[0].candidate;
51970
+ if (!turn.capped && !turn.saturated) return {
51971
+ kind: "elected",
51972
+ key: turn.key,
51973
+ reason: "share"
51974
+ };
51975
+ const free = scored.filter((s) => !s.candidate.capped && !s.candidate.saturated).toSorted((a, b) => spareOf(b.candidate) - spareOf(a.candidate) || byDeficit(a, b));
51976
+ const yieldReason = turn.capped ? "at-cap" : "saturated";
51977
+ if (free.length > 0) return {
51978
+ kind: "elected",
51979
+ key: free[0].candidate.key,
51980
+ reason: "yield",
51981
+ turnKey: turn.key,
51982
+ yieldReason
51983
+ };
51984
+ const uncapped = scored.find((s) => !s.candidate.capped);
51985
+ if (uncapped === void 0) return { kind: "none" };
51986
+ return {
51987
+ kind: "elected",
51988
+ key: uncapped.candidate.key,
51989
+ reason: "share"
51990
+ };
51991
+ }
50659
51992
  /**
50660
51993
  * THE detail-crop convention — the single derivation of the rectangle a
50661
51994
  * detail/enrichment step (clip-embedding, face-detection, plate-detection…)
@@ -51430,7 +52763,7 @@ var AudioSubscriptionController = class {
51430
52763
  } catch (err) {
51431
52764
  this.deps.logger.error("audio re-attach failed", {
51432
52765
  tags: { deviceId },
51433
- meta: { error: errMsg$1(err) }
52766
+ meta: { error: errMsg$2(err) }
51434
52767
  });
51435
52768
  }
51436
52769
  }
@@ -51541,7 +52874,7 @@ var AudioSubscriptionController = class {
51541
52874
  this.closeMotionAudioWindow(deviceId, "motion-cooldown-elapsed").catch((err) => {
51542
52875
  this.deps.logger.warn("lazy audio: cooldown teardown failed", {
51543
52876
  tags: { deviceId },
51544
- meta: { error: errMsg$1(err) }
52877
+ meta: { error: errMsg$2(err) }
51545
52878
  });
51546
52879
  });
51547
52880
  }, cooldownMs);
@@ -51560,7 +52893,7 @@ var AudioSubscriptionController = class {
51560
52893
  this.closeMotionAudioWindow(deviceId, "quiet-window-elapsed").catch((err) => {
51561
52894
  this.deps.logger.warn("lazy audio: quiet-window teardown failed", {
51562
52895
  tags: { deviceId },
51563
- meta: { error: errMsg$1(err) }
52896
+ meta: { error: errMsg$2(err) }
51564
52897
  });
51565
52898
  });
51566
52899
  }, windowMs);
@@ -51762,7 +53095,7 @@ var AudioSubscriptionController = class {
51762
53095
  meta: {
51763
53096
  audioNodeId,
51764
53097
  brokerId: audioBrokerId,
51765
- error: errMsg$1(err)
53098
+ error: errMsg$2(err)
51766
53099
  }
51767
53100
  });
51768
53101
  });
@@ -51837,7 +53170,7 @@ async function readOrRefuse(deps, deviceId) {
51837
53170
  try {
51838
53171
  device = await deps.readDevice(deviceId);
51839
53172
  } catch (err) {
51840
- return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$1(err)}`);
53173
+ return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$2(err)}`);
51841
53174
  }
51842
53175
  if (device === null) return refuse(deps, deviceId, `no device ${deviceId}`);
51843
53176
  return device;
@@ -53167,7 +54500,7 @@ var LoadShedController = class LoadShedController {
53167
54500
  if (await this.tryClusterRelocate(deviceId).catch((err) => {
53168
54501
  this.deps.logger.warn("Load management: cluster relocate failed", {
53169
54502
  tags: { deviceId },
53170
- meta: { error: errMsg$1(err) }
54503
+ meta: { error: errMsg$2(err) }
53171
54504
  });
53172
54505
  return false;
53173
54506
  })) {
@@ -53181,7 +54514,7 @@ var LoadShedController = class LoadShedController {
53181
54514
  this.deps.api()?.pipelineRunner.detachCamera.mutate({ deviceId }).catch((err) => {
53182
54515
  this.deps.logger.warn("Load management: detachCamera failed", {
53183
54516
  tags: { deviceId },
53184
- meta: { error: errMsg$1(err) }
54517
+ meta: { error: errMsg$2(err) }
53185
54518
  });
53186
54519
  });
53187
54520
  this.ensureLoadShedResumeTimer();
@@ -53213,7 +54546,7 @@ var LoadShedController = class LoadShedController {
53213
54546
  tags: { deviceId },
53214
54547
  meta: {
53215
54548
  from: currentNode,
53216
- error: errMsg$1(err)
54549
+ error: errMsg$2(err)
53217
54550
  }
53218
54551
  });
53219
54552
  });
@@ -53256,7 +54589,7 @@ var LoadShedController = class LoadShedController {
53256
54589
  this.deps.redispatchSingleCamera(deviceId).catch((err) => {
53257
54590
  this.deps.logger.warn("Load management: auto-resume dispatch failed", {
53258
54591
  tags: { deviceId },
53259
- meta: { error: errMsg$1(err) }
54592
+ meta: { error: errMsg$2(err) }
53260
54593
  });
53261
54594
  });
53262
54595
  }
@@ -53421,7 +54754,7 @@ var AgentLoadService = class AgentLoadService {
53421
54754
  ms,
53422
54755
  outcome: "failed",
53423
54756
  load: null,
53424
- error: errMsg$1(outcome.error)
54757
+ error: errMsg$2(outcome.error)
53425
54758
  };
53426
54759
  return {
53427
54760
  nodeId,
@@ -53888,7 +55221,7 @@ function buildCameraSensorsPorts(api, logger) {
53888
55221
  } catch (err) {
53889
55222
  return {
53890
55223
  ok: false,
53891
- error: errMsg$1(err)
55224
+ error: errMsg$2(err)
53892
55225
  };
53893
55226
  }
53894
55227
  };
@@ -54152,7 +55485,7 @@ var CameraSensorsController = class {
54152
55485
  async #readFleet() {
54153
55486
  const read = await this.#deps.listCameraIds().catch((err) => ({
54154
55487
  kind: "unreadable",
54155
- error: errMsg$1(err)
55488
+ error: errMsg$2(err)
54156
55489
  }));
54157
55490
  if (read.kind === "unreadable") {
54158
55491
  this.#deps.logger.warn("camera-sensors: camera list unreadable — mappings stay unloaded", { meta: { error: read.error } });
@@ -54179,7 +55512,7 @@ var CameraSensorsController = class {
54179
55512
  } catch (err) {
54180
55513
  this.#deps.logger.error("camera-sensors: refresh failed — nothing applied", {
54181
55514
  tags: { deviceId: cameraId },
54182
- meta: { error: errMsg$1(err) }
55515
+ meta: { error: errMsg$2(err) }
54183
55516
  });
54184
55517
  }
54185
55518
  }
@@ -54272,7 +55605,7 @@ async function readCameraSensorsSetting(read) {
54272
55605
  } catch (err) {
54273
55606
  return {
54274
55607
  state: "unreadable",
54275
- reason: `settings read failed — ${errMsg$1(err)}`
55608
+ reason: `settings read failed — ${errMsg$2(err)}`
54276
55609
  };
54277
55610
  }
54278
55611
  }
@@ -54582,7 +55915,7 @@ var CameraMotionSlice = class {
54582
55915
  st.chain = st.chain.then(job).catch((err) => {
54583
55916
  this.#deps.logger.error("camera motion slice: write job failed", {
54584
55917
  tags: { deviceId },
54585
- meta: { error: errMsg$1(err) }
55918
+ meta: { error: errMsg$2(err) }
54586
55919
  });
54587
55920
  });
54588
55921
  }
@@ -54646,7 +55979,7 @@ var CameraMotionSlice = class {
54646
55979
  meta: {
54647
55980
  detected: slice.detected,
54648
55981
  reason,
54649
- error: errMsg$1(err)
55982
+ error: errMsg$2(err)
54650
55983
  }
54651
55984
  });
54652
55985
  return;
@@ -54676,7 +56009,7 @@ var CameraMotionSlice = class {
54676
56009
  async #republishAll() {
54677
56010
  const listed = await this.#deps.listCameraIds().catch((err) => ({
54678
56011
  kind: "unreadable",
54679
- error: errMsg$1(err)
56012
+ error: errMsg$2(err)
54680
56013
  }));
54681
56014
  if (!this.#open || this.#stopped) return;
54682
56015
  if (listed.kind === "unreadable") this.#deps.logger.warn("camera motion slice: camera list unreadable — only cameras with motion since boot are re-published", { meta: { error: listed.error } });
@@ -54798,7 +56131,7 @@ function hubMotionSliceStore(api) {
54798
56131
  } catch (err) {
54799
56132
  return {
54800
56133
  kind: "unreadable",
54801
- error: errMsg$1(err)
56134
+ error: errMsg$2(err)
54802
56135
  };
54803
56136
  }
54804
56137
  }
@@ -56670,7 +58003,7 @@ function warnSampled(deps, deviceId, source, message, meta) {
56670
58003
  function warnUnreachable(deps, deviceId, source, err) {
56671
58004
  warnSampled(deps, deviceId, source, UNREACHABLE_MESSAGE, {
56672
58005
  source,
56673
- error: errMsg$1(err)
58006
+ error: errMsg$2(err)
56674
58007
  });
56675
58008
  }
56676
58009
  /**
@@ -57944,7 +59277,7 @@ var ONBOARD_DETECTION_SOURCE = "onboard";
57944
59277
  * configs, and it reads them the same way the runner does.
57945
59278
  */
57946
59279
  var DETECTION_SOURCES_WHEN_ABSENT = ["pipeline"];
57947
- function errMsg(err) {
59280
+ function errMsg$1(err) {
57948
59281
  return err instanceof Error ? err.message : String(err);
57949
59282
  }
57950
59283
  /**
@@ -57963,7 +59296,7 @@ async function reconcileOnboardForwarding(input, deps) {
57963
59296
  log.warn("onboard forwarding not reconciled — could not tell whether the camera has the cap", { meta: {
57964
59297
  wantsOnboard,
57965
59298
  detectionSources: [...detectionSources],
57966
- error: errMsg(err)
59299
+ error: errMsg$1(err)
57967
59300
  } });
57968
59301
  return {
57969
59302
  kind: "read-failed",
@@ -57981,7 +59314,7 @@ async function reconcileOnboardForwarding(input, deps) {
57981
59314
  log.warn("onboard forwarding not reconciled — the vendor flag could not be read", { meta: {
57982
59315
  wantsOnboard,
57983
59316
  detectionSources: [...detectionSources],
57984
- error: errMsg(err)
59317
+ error: errMsg$1(err)
57985
59318
  } });
57986
59319
  return {
57987
59320
  kind: "read-failed",
@@ -57999,7 +59332,7 @@ async function reconcileOnboardForwarding(input, deps) {
57999
59332
  was: current,
58000
59333
  wanted: wantsOnboard,
58001
59334
  detectionSources: [...detectionSources],
58002
- error: errMsg(err)
59335
+ error: errMsg$1(err)
58003
59336
  } });
58004
59337
  return {
58005
59338
  kind: "write-failed",
@@ -58195,7 +59528,7 @@ var DetectionWiringController = class {
58195
59528
  try {
58196
59529
  raw = await this.deps.ctx().settings.readDeviceStore(deviceId);
58197
59530
  } catch (err) {
58198
- const msg = errMsg$1(err);
59531
+ const msg = errMsg$2(err);
58199
59532
  this.deps.logger.error("resolveDeviceDetectionSettings failed", {
58200
59533
  tags: { deviceId },
58201
59534
  meta: { error: msg }
@@ -58226,7 +59559,7 @@ var DetectionWiringController = class {
58226
59559
  motionDetectionEnabled
58227
59560
  });
58228
59561
  } catch (err) {
58229
- const msg = errMsg$1(err);
59562
+ const msg = errMsg$2(err);
58230
59563
  this.deps.logger.error("resolveDeviceDetectionSettings narrowing failed", {
58231
59564
  tags: { deviceId },
58232
59565
  meta: { error: msg }
@@ -58261,7 +59594,7 @@ var DetectionWiringController = class {
58261
59594
  } catch (err) {
58262
59595
  this.deps.logger.debug("fetchAssignedProfiles: slot read failed (transient) — treating as unknown", {
58263
59596
  tags: { deviceId },
58264
- meta: { error: errMsg$1(err) }
59597
+ meta: { error: errMsg$2(err) }
58265
59598
  });
58266
59599
  return null;
58267
59600
  }
@@ -58337,7 +59670,7 @@ var DetectionWiringController = class {
58337
59670
  if (config.motionSources.includes("analyzer")) try {
58338
59671
  await this.deps.session.ensureMotionWatch(deviceId, config);
58339
59672
  } catch (err) {
58340
- log.error("ensureMotionWatch failed", { meta: { error: errMsg$1(err) } });
59673
+ log.error("ensureMotionWatch failed", { meta: { error: errMsg$2(err) } });
58341
59674
  }
58342
59675
  log.info("detection armed (session-time) — watching for motion", { meta: { motion: config.motionSources.join("+") } });
58343
59676
  } else {
@@ -58358,14 +59691,14 @@ var DetectionWiringController = class {
58358
59691
  try {
58359
59692
  await this.deps.placement.dispatchCamera(runnerConfig);
58360
59693
  } catch (err) {
58361
- const msg = errMsg$1(err);
59694
+ const msg = errMsg$2(err);
58362
59695
  log.error("dispatchCamera failed", { meta: { error: msg } });
58363
59696
  }
58364
59697
  }
58365
59698
  try {
58366
59699
  await this.deps.audio.resubscribe(deviceId, config);
58367
59700
  } catch (err) {
58368
- const msg = errMsg$1(err);
59701
+ const msg = errMsg$2(err);
58369
59702
  log.error("subscribeAudioStream failed", { meta: { error: msg } });
58370
59703
  }
58371
59704
  if (config.detectionMode !== "on-motion") log.info("Detection dispatched", { meta: {
@@ -58381,7 +59714,7 @@ var DetectionWiringController = class {
58381
59714
  try {
58382
59715
  await this.deps.placement.releaseCamera({ deviceId });
58383
59716
  } catch (err) {
58384
- const msg = errMsg$1(err);
59717
+ const msg = errMsg$2(err);
58385
59718
  this.deps.logger.error("releaseCamera failed", {
58386
59719
  tags: { deviceId },
58387
59720
  meta: { error: msg }
@@ -58407,7 +59740,7 @@ var DetectionWiringController = class {
58407
59740
  } catch (err) {
58408
59741
  this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
58409
59742
  tags: { deviceId },
58410
- meta: { error: errMsg$1(err) }
59743
+ meta: { error: errMsg$2(err) }
58411
59744
  });
58412
59745
  }
58413
59746
  this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
@@ -58431,7 +59764,7 @@ var DetectionWiringController = class {
58431
59764
  } catch (err) {
58432
59765
  this.deps.logger.debug("device existence check failed — proceeding as before", {
58433
59766
  tags: { deviceId },
58434
- meta: { error: errMsg$1(err) }
59767
+ meta: { error: errMsg$2(err) }
58435
59768
  });
58436
59769
  return false;
58437
59770
  }
@@ -58548,7 +59881,7 @@ var DetectionWiringController = class {
58548
59881
  await this.deps.attachOn(assignment.agentNodeId, nextConfig);
58549
59882
  log.info("pipeline.camera-updated: hot-reloaded", { tags: { nodeId: assignment.agentNodeId } });
58550
59883
  } catch (err) {
58551
- const msg = errMsg$1(err);
59884
+ const msg = errMsg$2(err);
58552
59885
  log.error("pipeline.camera-updated: attachOn failed", {
58553
59886
  tags: { nodeId: assignment.agentNodeId },
58554
59887
  meta: { error: msg }
@@ -58624,7 +59957,7 @@ var DetectionWiringController = class {
58624
59957
  this.deps.audio.resubscribe(deviceId, audioCfg).catch((err) => {
58625
59958
  this.deps.logger.warn("watchdog audio re-subscribe failed", {
58626
59959
  tags: { deviceId },
58627
- meta: { error: errMsg$1(err) }
59960
+ meta: { error: errMsg$2(err) }
58628
59961
  });
58629
59962
  });
58630
59963
  }
@@ -58718,6 +60051,35 @@ function pickInferenceDevice(input) {
58718
60051
  const loadByKey = new Map((input.loads ?? []).map((l) => [l.deviceKey, l]));
58719
60052
  const healthyKeys = eligibleKeys.filter((k) => !isDeviceSaturated(loadByKey.get(k)));
58720
60053
  const consideredKeys = pin !== null || healthyKeys.length === 0 || healthyKeys.length === eligibleKeys.length ? eligibleKeys : healthyKeys;
60054
+ if (input.policy === "share" && pin === null) {
60055
+ const election = electByShare(eligibleKeys.map((key) => {
60056
+ const load = loadByKey.get(key);
60057
+ const attached = load?.attachedCameras ?? 0;
60058
+ const cap = input.caps?.[key];
60059
+ const hasCap = cap !== null && cap !== void 0 && cap > 0;
60060
+ return {
60061
+ key,
60062
+ share: input.eligible[key] ?? 1,
60063
+ running: attached,
60064
+ capped: hasCap && attached >= cap,
60065
+ saturated: isDeviceSaturated(load),
60066
+ ...hasCap ? { spare: Math.max(0, cap - attached) } : {}
60067
+ };
60068
+ }));
60069
+ if (election.kind === "none") return {
60070
+ kind: "default-pool",
60071
+ reason: "all-devices-at-cap",
60072
+ eligibleKeys
60073
+ };
60074
+ return {
60075
+ kind: "selected",
60076
+ deviceKey: election.key,
60077
+ reason: "share",
60078
+ tieBreak: null,
60079
+ eligibleKeys,
60080
+ tiedKeys: [election.key]
60081
+ };
60082
+ }
58721
60083
  const balancerInput = {
58722
60084
  nodes: consideredKeys.map((k) => deviceAsLoad(k, loadByKey.get(k))),
58723
60085
  preferredAgent: pin,
@@ -58823,7 +60185,7 @@ var DeviceConfigContributions = class {
58823
60185
  issue: configIssue
58824
60186
  } });
58825
60187
  this.deps.topology.refreshNodeCapabilities().then(() => this.deps.redispatchAllActiveCameras("ingest-owner-changed")).catch((err) => {
58826
- this.deps.ctx().logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg$1(err) } });
60188
+ this.deps.ctx().logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg$2(err) } });
58827
60189
  });
58828
60190
  }
58829
60191
  this.deps.clearOnSettingsChange();
@@ -59277,10 +60639,25 @@ function rootRefusal(input, droppedRoots) {
59277
60639
  droppedRoots
59278
60640
  };
59279
60641
  }
60642
+ /**
60643
+ * Whose provisioning an attach applies. The elected device when there is one.
60644
+ * With NO elected device the runner's default pool serves the camera; when the
60645
+ * node has exactly one eligible device the balancer returns
60646
+ * `single-eligible-device` and that default pool IS that device (a Mac's
60647
+ * `coreml:all`, an N100 with only its iGPU on), so its model pin and the
60648
+ * camera's override stored under its key apply (D745 — before, the empty key
60649
+ * was read, which no store writes, and the pin was silently never run).
60650
+ * Otherwise `''`: the node's folded settings with no hardware fields.
60651
+ */
60652
+ function provisioningKeyFor(input) {
60653
+ if (input.deviceKey !== void 0) return input.deviceKey;
60654
+ const eligibleKeys = Object.keys(input.eligible);
60655
+ return eligibleKeys.length === 1 ? eligibleKeys[0] ?? "" : "";
60656
+ }
59280
60657
  /** Steps 3 of the module header: provision the resolved structure. */
59281
60658
  async function provisionAttachPipeline(input, roster, resolved, store) {
59282
60659
  const { deviceId, nodeId, deviceKey } = input;
59283
- const electedKey = deviceKey ?? "";
60660
+ const electedKey = provisioningKeyFor(input);
59284
60661
  const rosterKeys = (roster ?? []).map((d) => d.deviceKey);
59285
60662
  const sources = await store.readDeviceProvisioning(deviceId, nodeId, [...new Set([electedKey, ...rosterKeys])]);
59286
60663
  const elected = sources.get(electedKey);
@@ -59888,7 +61265,7 @@ var PlacementService = class {
59888
61265
  async releaseCamera(input) {
59889
61266
  const current = this.deps.ledger.getAssignment(input.deviceId);
59890
61267
  if (current) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
59891
- const msg = errMsg$1(err);
61268
+ const msg = errMsg$2(err);
59892
61269
  this.deps.ctx().logger.debug("releaseCamera detach failed", {
59893
61270
  tags: { deviceId: input.deviceId },
59894
61271
  meta: { error: msg }
@@ -59916,7 +61293,7 @@ var PlacementService = class {
59916
61293
  pipelineNodeId: input.agentNodeId,
59917
61294
  [PREFERRED_AGENT_SETTING]: null
59918
61295
  }).catch((err) => {
59919
- const msg = errMsg$1(err);
61296
+ const msg = errMsg$2(err);
59920
61297
  ctx.logger.warn("assignPipeline: failed to persist pin", {
59921
61298
  tags: { deviceId: input.deviceId },
59922
61299
  meta: { error: msg }
@@ -59948,7 +61325,7 @@ var PlacementService = class {
59948
61325
  }
59949
61326
  const current = this.deps.ledger.getAssignment(input.deviceId);
59950
61327
  if (current && current.agentNodeId !== input.agentNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
59951
- const msg = errMsg$1(err);
61328
+ const msg = errMsg$2(err);
59952
61329
  ctx.logger.debug("assignPipeline detach-old failed", {
59953
61330
  tags: { deviceId: input.deviceId },
59954
61331
  meta: { error: msg }
@@ -59973,7 +61350,7 @@ var PlacementService = class {
59973
61350
  await ctx.settings?.writeDeviceStore(input.deviceId, { pipelineDeviceKey: clearing ? "auto" : input.deviceKey }).catch((err) => {
59974
61351
  ctx.logger.warn("setPipelineDevicePin: failed to persist device pin", {
59975
61352
  tags: { deviceId: input.deviceId },
59976
- meta: { error: errMsg$1(err) }
61353
+ meta: { error: errMsg$2(err) }
59977
61354
  });
59978
61355
  });
59979
61356
  const sessionNode = this.deps.activeSessionNode(input.deviceId);
@@ -59996,7 +61373,7 @@ var PlacementService = class {
59996
61373
  [PREFERRED_AGENT_SETTING]: null,
59997
61374
  pipelineNodeId: "auto"
59998
61375
  }).catch((err) => {
59999
- const msg = errMsg$1(err);
61376
+ const msg = errMsg$2(err);
60000
61377
  ctx.logger.warn("unassignPipeline: failed to clear pin", {
60001
61378
  tags: { deviceId: input.deviceId },
60002
61379
  meta: { error: msg }
@@ -60017,7 +61394,7 @@ var PlacementService = class {
60017
61394
  else {
60018
61395
  const targetNodeId = decision.agentNodeId;
60019
61396
  if (current && current.agentNodeId !== targetNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
60020
- const msg = errMsg$1(err);
61397
+ const msg = errMsg$2(err);
60021
61398
  ctx.logger.debug("unassignPipeline detach-old failed", {
60022
61399
  tags: { deviceId: input.deviceId },
60023
61400
  meta: { error: msg }
@@ -60037,7 +61414,7 @@ var PlacementService = class {
60037
61414
  }
60038
61415
  }
60039
61416
  if (current) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
60040
- const msg = errMsg$1(err);
61417
+ const msg = errMsg$2(err);
60041
61418
  ctx.logger.debug("unassignPipeline detach failed", {
60042
61419
  tags: { deviceId: input.deviceId },
60043
61420
  meta: { error: msg }
@@ -60056,7 +61433,7 @@ var PlacementService = class {
60056
61433
  async runAutoRebalance() {
60057
61434
  if (!this.deps.ctxIfReady()) return;
60058
61435
  await this.deps.reconcilePlacementFromRunners().catch((err) => {
60059
- this.deps.ctx().logger.debug("auto-rebalance: attachment reconcile failed", { meta: { error: errMsg$1(err) } });
61436
+ this.deps.ctx().logger.debug("auto-rebalance: attachment reconcile failed", { meta: { error: errMsg$2(err) } });
60060
61437
  });
60061
61438
  if (this.autoRebalanceInFlight) return;
60062
61439
  if (this.deps.topology.enabledNodes.length < 2) return;
@@ -60065,7 +61442,7 @@ var PlacementService = class {
60065
61442
  const { migrated } = await this.rebalance({ minImprovement: AUTO_REBALANCE_MIN_IMPROVEMENT });
60066
61443
  if (migrated > 0) this.deps.ctx().logger.info("auto-rebalance: corrected placement drift", { meta: { migrated } });
60067
61444
  } catch (err) {
60068
- this.deps.ctx().logger.debug("auto-rebalance pass failed", { meta: { error: errMsg$1(err) } });
61445
+ this.deps.ctx().logger.debug("auto-rebalance pass failed", { meta: { error: errMsg$2(err) } });
60069
61446
  } finally {
60070
61447
  this.autoRebalanceInFlight = false;
60071
61448
  }
@@ -60137,7 +61514,7 @@ var PlacementService = class {
60137
61514
  const targetConfig = withAttachPipeline(config, placement.pipeline);
60138
61515
  if (current) {
60139
61516
  await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
60140
- const msg = errMsg$1(err);
61517
+ const msg = errMsg$2(err);
60141
61518
  ctx.logger.debug("rebalance detach-old failed", {
60142
61519
  tags: { deviceId },
60143
61520
  meta: { error: msg }
@@ -60542,7 +61919,7 @@ var NodeLifecycleHandler = class {
60542
61919
  nodeId: decision.agentNodeId
60543
61920
  } });
60544
61921
  } catch (err) {
60545
- const msg = errMsg$1(err);
61922
+ const msg = errMsg$2(err);
60546
61923
  this.deps.logger.error("Failover: attach failed", {
60547
61924
  tags: {
60548
61925
  deviceId,
@@ -60572,7 +61949,7 @@ var NodeLifecycleHandler = class {
60572
61949
  this.deps.schedulePendingRetry();
60573
61950
  if (failoverPolicy.onReconnect === "rebalance") {
60574
61951
  await this.deps.rebalance().catch((err) => {
60575
- const msg = errMsg$1(err);
61952
+ const msg = errMsg$2(err);
60576
61953
  this.deps.logger.error("rebalance after reconnect failed", { meta: { error: msg } });
60577
61954
  });
60578
61955
  return;
@@ -60598,7 +61975,7 @@ var NodeLifecycleHandler = class {
60598
61975
  this.deps.ledger.recordAssignment(deviceId, nodeId, "failover", false);
60599
61976
  restored++;
60600
61977
  } catch (err) {
60601
- const msg = errMsg$1(err);
61978
+ const msg = errMsg$2(err);
60602
61979
  this.deps.logger.error("restore attach failed", {
60603
61980
  tags: {
60604
61981
  deviceId,
@@ -60692,7 +62069,7 @@ var NodeLifecycleHandler = class {
60692
62069
  deviceId,
60693
62070
  nodeId
60694
62071
  },
60695
- meta: { error: errMsg$1(err) }
62072
+ meta: { error: errMsg$2(err) }
60696
62073
  });
60697
62074
  }
60698
62075
  this.deps.logger.info("readiness: detection-pipeline reseeded agent defaults + redispatched cameras", {
@@ -60706,7 +62083,7 @@ var NodeLifecycleHandler = class {
60706
62083
  this.reattachStaleEmpties(nodeId).catch((err) => {
60707
62084
  this.deps.logger.debug("readiness deferred sweep failed", {
60708
62085
  tags: { nodeId },
60709
- meta: { error: errMsg$1(err) }
62086
+ meta: { error: errMsg$2(err) }
60710
62087
  });
60711
62088
  });
60712
62089
  }, 3e3);
@@ -60717,7 +62094,7 @@ var NodeLifecycleHandler = class {
60717
62094
  tags: { nodeId },
60718
62095
  meta: {
60719
62096
  epoch: t.epoch,
60720
- error: errMsg$1(err)
62097
+ error: errMsg$2(err)
60721
62098
  }
60722
62099
  });
60723
62100
  }
@@ -60796,7 +62173,7 @@ var NodeLifecycleHandler = class {
60796
62173
  deviceId,
60797
62174
  nodeId
60798
62175
  },
60799
- meta: { error: errMsg$1(err) }
62176
+ meta: { error: errMsg$2(err) }
60800
62177
  });
60801
62178
  }
60802
62179
  }
@@ -61118,7 +62495,7 @@ var NodeStressController = class NodeStressController {
61118
62495
  if (this.timer) return;
61119
62496
  this.timer = setInterval(() => {
61120
62497
  this.sweep().catch((err) => {
61121
- this.deps.logger.warn("node-stress sweep failed", { meta: { error: errMsg$1(err) } });
62498
+ this.deps.logger.warn("node-stress sweep failed", { meta: { error: errMsg$2(err) } });
61122
62499
  });
61123
62500
  }, NodeStressController.SWEEP_INTERVAL_MS);
61124
62501
  }
@@ -61291,7 +62668,7 @@ var NodeStressController = class NodeStressController {
61291
62668
  meta: {
61292
62669
  from: plan.fromNodeId,
61293
62670
  to: plan.toNodeId,
61294
- error: errMsg$1(err)
62671
+ error: errMsg$2(err)
61295
62672
  }
61296
62673
  });
61297
62674
  } finally {
@@ -61988,7 +63365,7 @@ var NodeTopologyService = class {
61988
63365
  try {
61989
63366
  blob = await this.deps.readAgentSettingsMap();
61990
63367
  } catch (err) {
61991
- this.deps.logger.warn("refreshNodeCapabilities: agent settings read failed", { meta: { error: errMsg$1(err) } });
63368
+ this.deps.logger.warn("refreshNodeCapabilities: agent settings read failed", { meta: { error: errMsg$2(err) } });
61992
63369
  }
61993
63370
  const nodeIds = new Set(["hub"]);
61994
63371
  for (const nodeId of Object.keys(blob)) if (typeof nodeId === "string" && nodeId.length > 0 && !nodeId.includes("/")) nodeIds.add(nodeId);
@@ -62162,7 +63539,7 @@ function wireOrchestratorSubscriptions(deps) {
62162
63539
  const unsubInferenceResult = deps.eventBus.subscribe({ category: EventCategory.PipelineInferenceResult }, (event) => {
62163
63540
  if (!isEvent(event, EventCategory.PipelineInferenceResult)) return;
62164
63541
  deps.handleInferenceResult(event.data).catch((err) => {
62165
- const msg = errMsg$1(err);
63542
+ const msg = errMsg$2(err);
62166
63543
  deps.logger.error("PipelineInferenceResult post-processing failed", {
62167
63544
  tags: { deviceId: event.data.deviceId },
62168
63545
  meta: { error: msg }
@@ -62181,7 +63558,7 @@ function wireOrchestratorSubscriptions(deps) {
62181
63558
  deps.logger.info("received onProfileSlotsChanged", { tags: { deviceId } });
62182
63559
  deps.handleDeviceRegistered(deviceId).catch((err) => {
62183
63560
  if (isTornDown) return;
62184
- const msg = errMsg$1(err);
63561
+ const msg = errMsg$2(err);
62185
63562
  deps.logger.error("handleDeviceRegistered failed", {
62186
63563
  tags: { deviceId },
62187
63564
  meta: { error: msg }
@@ -62193,7 +63570,7 @@ function wireOrchestratorSubscriptions(deps) {
62193
63570
  const unsubDeviceUnregistered = deps.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (event) => {
62194
63571
  const { deviceId } = event.data;
62195
63572
  deps.handleDeviceUnregistered(deviceId).catch((err) => {
62196
- const msg = errMsg$1(err);
63573
+ const msg = errMsg$2(err);
62197
63574
  deps.logger.error("handleDeviceUnregistered failed", {
62198
63575
  tags: { deviceId },
62199
63576
  meta: { error: msg }
@@ -62206,7 +63583,7 @@ function wireOrchestratorSubscriptions(deps) {
62206
63583
  deps.cameraSensors.requestCamera(deviceId, "settings-updated");
62207
63584
  deps.handleSettingsChanged(deviceId).catch((err) => {
62208
63585
  if (isTornDown) return;
62209
- const msg = errMsg$1(err);
63586
+ const msg = errMsg$2(err);
62210
63587
  deps.logger.error("handleSettingsChanged failed", {
62211
63588
  tags: { deviceId },
62212
63589
  meta: { error: msg }
@@ -62216,7 +63593,7 @@ function wireOrchestratorSubscriptions(deps) {
62216
63593
  const unsubCameraUpdated = deps.eventBus.subscribe({ category: EventCategory.PipelineCameraUpdated }, (event) => {
62217
63594
  if (!isEvent(event, EventCategory.PipelineCameraUpdated)) return;
62218
63595
  deps.handlePipelineCameraUpdated(event.data.deviceId, event.data.config).catch((err) => {
62219
- const msg = errMsg$1(err);
63596
+ const msg = errMsg$2(err);
62220
63597
  deps.logger.error("handlePipelineCameraUpdated failed", {
62221
63598
  tags: { deviceId: event.data.deviceId },
62222
63599
  meta: { error: msg }
@@ -62229,7 +63606,7 @@ function wireOrchestratorSubscriptions(deps) {
62229
63606
  if (!(reason === "wrapper-activated" || reason === "wrapper-deactivated") || !(capName === "detection-pipeline" || capName === "audio-analysis" || capName === "motion-detection")) return;
62230
63607
  deps.handleSettingsChanged(deviceId).catch((err) => {
62231
63608
  if (isTornDown) return;
62232
- const msg = errMsg$1(err);
63609
+ const msg = errMsg$2(err);
62233
63610
  deps.logger.error("binding-changed re-dispatch failed", {
62234
63611
  tags: { deviceId },
62235
63612
  meta: {
@@ -62257,7 +63634,7 @@ function wireOrchestratorSubscriptions(deps) {
62257
63634
  tags: { deviceId },
62258
63635
  meta: {
62259
63636
  detected,
62260
- error: errMsg$1(err)
63637
+ error: errMsg$2(err)
62261
63638
  }
62262
63639
  });
62263
63640
  });
@@ -62304,7 +63681,7 @@ function wireOrchestratorSubscriptions(deps) {
62304
63681
  tags: { deviceId },
62305
63682
  meta: {
62306
63683
  detected,
62307
- error: errMsg$1(err)
63684
+ error: errMsg$2(err)
62308
63685
  }
62309
63686
  });
62310
63687
  });
@@ -62570,7 +63947,7 @@ async function reattachAfterPackageZoneChange(deps, deviceId) {
62570
63947
  deviceId,
62571
63948
  nodeId
62572
63949
  },
62573
- meta: { error: errMsg$1(err) }
63950
+ meta: { error: errMsg$2(err) }
62574
63951
  });
62575
63952
  }
62576
63953
  }
@@ -62851,7 +64228,7 @@ async function resetRemovedModelPins(deps, trigger) {
62851
64228
  } catch (err) {
62852
64229
  deps.logger.warn("removed-model pin reset skipped — custom model registry unreadable", { meta: {
62853
64230
  trigger,
62854
- error: errMsg$1(err)
64231
+ error: errMsg$2(err)
62855
64232
  } });
62856
64233
  return [];
62857
64234
  }
@@ -62870,7 +64247,7 @@ async function resetRemovedModelPins(deps, trigger) {
62870
64247
  } catch (err) {
62871
64248
  deps.logger.warn("removed-model pin reset failed", { meta: {
62872
64249
  trigger,
62873
- error: errMsg$1(err)
64250
+ error: errMsg$2(err)
62874
64251
  } });
62875
64252
  return [];
62876
64253
  }
@@ -63026,7 +64403,6 @@ var StoredCameraPipelineForAgentSchema = object({
63026
64403
  }).nullable()
63027
64404
  });
63028
64405
  var StoredCameraPipelineSettingsSchema = object({
63029
- pinnedAgentNodeId: string().optional(),
63030
64406
  stepToggles: record(string(), boolean()).optional(),
63031
64407
  stepOverridesByDevice: record(string(), record(string(), record(string(), StoredCameraStepOverridePatchSchema))).optional(),
63032
64408
  stepOverridesByAgent: record(string(), record(string(), LegacyCameraStepOverridePatchSchema)).optional(),
@@ -63229,6 +64605,21 @@ var PipelineSettingsStore = class PipelineSettingsStore {
63229
64605
  });
63230
64606
  }
63231
64607
  /**
64608
+ * Rewrite the whole agent-settings map in ONE read-modify-write under
64609
+ * {@link blobWriteLock} — atomic across every node of the blob (Best
64610
+ * settings, D745, writes several nodes' devices at once). `transform` must
64611
+ * be pure: it may run on a fresh read.
64612
+ */
64613
+ async rewriteAgentSettingsMap(transform) {
64614
+ await this.blobWriteLock.run(PipelineSettingsStore.AGENT_SETTINGS_LOCK_KEY, async () => {
64615
+ await this.agentSettingsState.set(transform(await this.readAgentSettingsMap()));
64616
+ });
64617
+ }
64618
+ /** Rewrite the whole camera-settings map in one durable `update()` (D745). */
64619
+ async rewriteCameraSettingsMap(transform) {
64620
+ await this.cameraSettingsState.update(transform);
64621
+ }
64622
+ /**
63232
64623
  * Drop one agent's persisted settings entry. Returns whether an entry
63233
64624
  * existed (`false` also when the store is already disposed — the locked
63234
64625
  * section becomes a no-op then). Serialized via {@link blobWriteLock} for
@@ -63485,6 +64876,10 @@ var PipelineSettingsStore = class PipelineSettingsStore {
63485
64876
  * epochs and operator resets — a bound, not a budget.
63486
64877
  */
63487
64878
  static CATALOG_FENCE_REREADS = 3;
64879
+ /** The node's catalog, read through the cache (awaits the node when cold). */
64880
+ async readCatalogForNode(nodeId) {
64881
+ return this.getCatalogForAgent(nodeId);
64882
+ }
63488
64883
  async getCatalogForAgent(nodeId) {
63489
64884
  const api = this.deps.api();
63490
64885
  if (!api) return null;
@@ -63502,7 +64897,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
63502
64897
  this.catalogReadFailing.add(nodeId);
63503
64898
  this.deps.logger.warn("node catalog read failed — placements on it wait or stay pending", {
63504
64899
  tags: { nodeId },
63505
- meta: { error: errMsg$1(err) }
64900
+ meta: { error: errMsg$2(err) }
63506
64901
  });
63507
64902
  }
63508
64903
  return null;
@@ -63589,7 +64984,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
63589
64984
  tags: { deviceId },
63590
64985
  meta: {
63591
64986
  capName: "detection-pipeline",
63592
- error: errMsg$1(err)
64987
+ error: errMsg$2(err)
63593
64988
  }
63594
64989
  });
63595
64990
  return true;
@@ -64422,7 +65817,7 @@ var ReconcileController = class ReconcileController {
64422
65817
  try {
64423
65818
  slots = await api.streamBroker.listAllProfileSlots.query();
64424
65819
  } catch (err) {
64425
- this.deps.logger.debug("dispatch reconcile skipped — stream-broker not ready", { meta: { error: errMsg$1(err) } });
65820
+ this.deps.logger.debug("dispatch reconcile skipped — stream-broker not ready", { meta: { error: errMsg$2(err) } });
64426
65821
  return;
64427
65822
  }
64428
65823
  const desired = desiredDeviceIdsFromSlots(slots);
@@ -64449,7 +65844,7 @@ var ReconcileController = class ReconcileController {
64449
65844
  } catch (err) {
64450
65845
  this.deps.logger.warn("dispatch reconcile: handleDeviceRegistered failed", {
64451
65846
  tags: { deviceId },
64452
- meta: { error: errMsg$1(err) }
65847
+ meta: { error: errMsg$2(err) }
64453
65848
  });
64454
65849
  }
64455
65850
  await this.reconcilePlacementFromRunners();
@@ -64477,7 +65872,7 @@ var ReconcileController = class ReconcileController {
64477
65872
  const devices = await api.deviceManager.listAll.query({ projection: "slim" });
64478
65873
  return new Set(devices.map((device) => device.id));
64479
65874
  } catch (err) {
64480
- this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg$1(err) } });
65875
+ this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg$2(err) } });
64481
65876
  return null;
64482
65877
  }
64483
65878
  }
@@ -64503,7 +65898,7 @@ var ReconcileController = class ReconcileController {
64503
65898
  } catch (err) {
64504
65899
  this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
64505
65900
  tags: { deviceId },
64506
- meta: { error: errMsg$1(err) }
65901
+ meta: { error: errMsg$2(err) }
64507
65902
  });
64508
65903
  }
64509
65904
  }
@@ -64560,7 +65955,7 @@ var ReconcileController = class ReconcileController {
64560
65955
  this.deps.logger.debug("placement reconcile skipped node — getLocalCameras failed", {
64561
65956
  tags: { nodeId },
64562
65957
  meta: {
64563
- error: outcome.kind === "rejected" ? errMsg$1(outcome.error) : "budget exceeded",
65958
+ error: outcome.kind === "rejected" ? errMsg$2(outcome.error) : "budget exceeded",
64564
65959
  ms
64565
65960
  }
64566
65961
  });
@@ -64590,7 +65985,7 @@ var ReconcileController = class ReconcileController {
64590
65985
  nodeId,
64591
65986
  deviceId
64592
65987
  },
64593
- meta: { error: errMsg$1(err) }
65988
+ meta: { error: errMsg$2(err) }
64594
65989
  });
64595
65990
  })));
64596
65991
  }
@@ -64632,7 +66027,7 @@ var ReconcileController = class ReconcileController {
64632
66027
  this.deps.handleDeviceRegistered(deviceId).catch((err) => {
64633
66028
  this.deps.logger.debug("slot-read retry: handleDeviceRegistered failed", {
64634
66029
  tags: { deviceId },
64635
- meta: { error: errMsg$1(err) }
66030
+ meta: { error: errMsg$2(err) }
64636
66031
  });
64637
66032
  });
64638
66033
  }, delay);
@@ -64710,7 +66105,7 @@ var ReconcileController = class ReconcileController {
64710
66105
  } catch (err) {
64711
66106
  this.deps.logger.warn("pending retry: dispatchCamera failed", {
64712
66107
  tags: { deviceId },
64713
- meta: { error: errMsg$1(err) }
66108
+ meta: { error: errMsg$2(err) }
64714
66109
  });
64715
66110
  stillPending++;
64716
66111
  }
@@ -64788,7 +66183,7 @@ var ReconcileController = class ReconcileController {
64788
66183
  await this.deps.detachOn(assignment.agentNodeId, deviceId).catch((err) => {
64789
66184
  this.deps.logger.debug("remote-health exhausted-detach failed", {
64790
66185
  tags: { deviceId },
64791
- meta: { error: errMsg$1(err) }
66186
+ meta: { error: errMsg$2(err) }
64792
66187
  });
64793
66188
  });
64794
66189
  return;
@@ -64811,14 +66206,14 @@ var ReconcileController = class ReconcileController {
64811
66206
  await this.deps.detachOn(assignment.agentNodeId, deviceId).catch((err) => {
64812
66207
  this.deps.logger.warn("remote-health detach failed", {
64813
66208
  tags: { deviceId },
64814
- meta: { error: errMsg$1(err) }
66209
+ meta: { error: errMsg$2(err) }
64815
66210
  });
64816
66211
  });
64817
66212
  this.deps.ledger.dropAssignment(deviceId, "silent");
64818
66213
  await this.deps.redispatchCamera(cfg).catch((err) => {
64819
66214
  this.deps.logger.warn("remote-health re-dispatch failed", {
64820
66215
  tags: { deviceId },
64821
- meta: { error: errMsg$1(err) }
66216
+ meta: { error: errMsg$2(err) }
64822
66217
  });
64823
66218
  });
64824
66219
  }
@@ -64993,7 +66388,7 @@ var OccupancyRecheckController = class {
64993
66388
  tags: { deviceId },
64994
66389
  meta: {
64995
66390
  deviceId,
64996
- error: errMsg$1(err)
66391
+ error: errMsg$2(err)
64997
66392
  }
64998
66393
  });
64999
66394
  });
@@ -65127,7 +66522,7 @@ var OccupancyRecheckController = class {
65127
66522
  tags: { deviceId },
65128
66523
  meta: {
65129
66524
  deviceId,
65130
- error: errMsg$1(err)
66525
+ error: errMsg$2(err)
65131
66526
  }
65132
66527
  });
65133
66528
  });
@@ -65182,7 +66577,7 @@ var OccupancyRecheckController = class {
65182
66577
  meta: {
65183
66578
  deviceId,
65184
66579
  nodeId,
65185
- error: errMsg$1(err),
66580
+ error: errMsg$2(err),
65186
66581
  kind
65187
66582
  }
65188
66583
  });
@@ -65191,7 +66586,7 @@ var OccupancyRecheckController = class {
65191
66586
  }
65192
66587
  const answer = await this.deps.requestBurstOn(nodeId, deviceId, frames, reasonTag).catch((err) => ({
65193
66588
  started: false,
65194
- error: errMsg$1(err)
66589
+ error: errMsg$2(err)
65195
66590
  }));
65196
66591
  if (!answer.started) {
65197
66592
  const refusedBecause = "refusedBecause" in answer ? answer.refusedBecause : void 0;
@@ -65239,7 +66634,7 @@ var OccupancyRecheckController = class {
65239
66634
  meta: {
65240
66635
  deviceId,
65241
66636
  nodeId,
65242
- error: errMsg$1(err)
66637
+ error: errMsg$2(err)
65243
66638
  }
65244
66639
  });
65245
66640
  });
@@ -65296,7 +66691,7 @@ var OccupancyRecheckController = class {
65296
66691
  meta: {
65297
66692
  deviceId,
65298
66693
  nodeId: burst.nodeId,
65299
- error: errMsg$1(err)
66694
+ error: errMsg$2(err)
65300
66695
  }
65301
66696
  });
65302
66697
  });
@@ -65511,7 +66906,15 @@ var TtlMemo = class {
65511
66906
  //#endregion
65512
66907
  //#region src/detection-session-registry.ts
65513
66908
  var DetectionSessionRegistry = class {
66909
+ onChange;
65514
66910
  sessions = /* @__PURE__ */ new Map();
66911
+ /**
66912
+ * @param onChange called after every register / deregister / clear with the
66913
+ * running set — feeds the placement statistics (D748). Never on a no-op.
66914
+ */
66915
+ constructor(onChange = () => {}) {
66916
+ this.onChange = onChange;
66917
+ }
65515
66918
  register(deviceId, nodeId, startedAt, triggeredBy, deviceKey) {
65516
66919
  const existing = this.sessions.get(deviceId);
65517
66920
  if (existing?.teardownTimer) clearTimeout(existing.teardownTimer);
@@ -65524,6 +66927,7 @@ var DetectionSessionRegistry = class {
65524
66927
  ...deviceKey ? { deviceKey } : {}
65525
66928
  };
65526
66929
  this.sessions.set(deviceId, session);
66930
+ this.onChange(this.list());
65527
66931
  return session;
65528
66932
  }
65529
66933
  get(deviceId) {
@@ -65537,6 +66941,7 @@ var DetectionSessionRegistry = class {
65537
66941
  if (!session) return void 0;
65538
66942
  if (session.teardownTimer) clearTimeout(session.teardownTimer);
65539
66943
  this.sessions.delete(deviceId);
66944
+ this.onChange(this.list());
65540
66945
  return session;
65541
66946
  }
65542
66947
  setTeardownTimer(deviceId, timer) {
@@ -65559,9 +66964,39 @@ var DetectionSessionRegistry = class {
65559
66964
  }
65560
66965
  clear() {
65561
66966
  for (const session of this.sessions.values()) if (session.teardownTimer) clearTimeout(session.teardownTimer);
66967
+ const hadSessions = this.sessions.size > 0;
65562
66968
  this.sessions.clear();
66969
+ if (hadSessions) this.onChange([]);
65563
66970
  }
65564
66971
  };
66972
+ //#endregion
66973
+ //#region src/session-share-placement.ts
66974
+ function nodeSaturated(load) {
66975
+ return load.devices.length > 0 && load.devices.every((d) => isDeviceSaturated(d));
66976
+ }
66977
+ function electNodeByShare(input) {
66978
+ return electByShare(input.loads.filter((l) => l.nodeId.length > 0 && input.eligible.includes(l.nodeId)).map((load) => {
66979
+ const cap = input.caps[load.nodeId];
66980
+ const hasCap = cap !== null && cap !== void 0 && cap > 0;
66981
+ const weight = input.weights[load.nodeId];
66982
+ return {
66983
+ key: load.nodeId,
66984
+ share: weight !== void 0 && weight > 0 ? weight : 1,
66985
+ running: input.runningByNode.get(load.nodeId) ?? 0,
66986
+ capped: hasCap && load.attachedCameras >= cap,
66987
+ saturated: nodeSaturated(load),
66988
+ ...hasCap ? { spare: Math.max(0, cap - load.attachedCameras) } : {}
66989
+ };
66990
+ }));
66991
+ }
66992
+ /** Parallel sessions per node: sessions and standing cameras, each camera once. */
66993
+ function countRunningByNode(placements) {
66994
+ const seen = /* @__PURE__ */ new Map();
66995
+ for (const p of placements) if (!seen.has(p.deviceId)) seen.set(p.deviceId, p.nodeId);
66996
+ const out = /* @__PURE__ */ new Map();
66997
+ for (const nodeId of seen.values()) out.set(nodeId, (out.get(nodeId) ?? 0) + 1);
66998
+ return out;
66999
+ }
65565
67000
  /**
65566
67001
  * Minimum time a detection session stays open, whatever closes it.
65567
67002
  *
@@ -65674,7 +67109,15 @@ var SessionDispatchController = class {
65674
67109
  * (not `private`) — see the class doc for the two production/test reasons
65675
67110
  * `index.ts` reaches through it directly.
65676
67111
  */
65677
- sessionRegistry = new DetectionSessionRegistry();
67112
+ sessionRegistry = new DetectionSessionRegistry((sessions) => this.deps.placementStats?.noteSessions(sessions.map((s) => ({
67113
+ nodeId: s.nodeId,
67114
+ deviceKey: s.deviceKey ?? ""
67115
+ }))));
67116
+ /**
67117
+ * `deviceId|deviceKey` of device pins the session path has already said it
67118
+ * ignored — one line per stale pin, not one per session (D748).
67119
+ */
67120
+ reportedStalePins = /* @__PURE__ */ new Set();
65678
67121
  /**
65679
67122
  * Per-device async mutex for session dispatch/teardown critical sections —
65680
67123
  * backed by the generic `KeyedAsyncLock` (mirrors the audio controller's
@@ -65953,7 +67396,7 @@ var SessionDispatchController = class {
65953
67396
  deviceId,
65954
67397
  nodeId: session.nodeId
65955
67398
  },
65956
- meta: { error: errMsg$1(err) }
67399
+ meta: { error: errMsg$2(err) }
65957
67400
  });
65958
67401
  });
65959
67402
  this.deps.dropAssignment(deviceId, "silent");
@@ -66135,6 +67578,76 @@ var SessionDispatchController = class {
66135
67578
  });
66136
67579
  }
66137
67580
  /**
67581
+ * The session's node. `least-loaded` (and any node pin, under either
67582
+ * policy) is `balance()` exactly as before. `share` holds the node shares
67583
+ * of PARALLEL sessions (D748): sessions now running and standing cameras
67584
+ * count toward their node, and a node at its cap or drowning yields —
67585
+ * logged with the reason, and counted against the node in the stats.
67586
+ */
67587
+ electSessionNode(input) {
67588
+ const balanced = () => balance({
67589
+ nodes: input.loads,
67590
+ preferredAgent: input.preferredAgent,
67591
+ nodeCaps: input.nodeCaps,
67592
+ weights: input.weights,
67593
+ eligibleNodes: input.eligible
67594
+ });
67595
+ if (input.policy !== "share" || input.preferredAgent) return balanced();
67596
+ const running = countRunningByNode([...this.sessionRegistry.list().map((s) => ({
67597
+ deviceId: s.deviceId,
67598
+ nodeId: s.nodeId
67599
+ })), ...this.deps.ledger.listAssignments().map((a) => ({
67600
+ deviceId: a.deviceId,
67601
+ nodeId: a.agentNodeId
67602
+ }))]);
67603
+ const election = electNodeByShare({
67604
+ loads: input.loads,
67605
+ eligible: input.eligible,
67606
+ weights: input.weights,
67607
+ caps: input.nodeCaps,
67608
+ runningByNode: running
67609
+ });
67610
+ if (election.kind === "none") return balanced();
67611
+ if (election.reason === "yield") {
67612
+ this.deps.placementStats?.noteYield(election.turnKey);
67613
+ this.deps.logger.info("detection session: share turn yielded — the node had no room", {
67614
+ tags: {
67615
+ deviceId: input.deviceId,
67616
+ nodeId: election.key
67617
+ },
67618
+ meta: {
67619
+ turnNodeId: election.turnKey,
67620
+ reason: election.yieldReason
67621
+ }
67622
+ });
67623
+ }
67624
+ const load = input.loads.find((l) => l.nodeId === election.key);
67625
+ return {
67626
+ kind: "assigned",
67627
+ agentNodeId: election.key,
67628
+ reason: "capacity",
67629
+ score: load ? load.attachedCameras + load.queueDepthTotal : 0
67630
+ };
67631
+ }
67632
+ /**
67633
+ * Say ONCE that a camera's device pin names a device this node does not
67634
+ * offer (disabled, absent): the session path ignores it, and the Load
67635
+ * balancing tab offers to clear it. Never cleared here (D748).
67636
+ */
67637
+ noteStalePin(deviceId, pinnedDeviceKey, enabledDevices) {
67638
+ if (pinnedDeviceKey === null || pinnedDeviceKey in enabledDevices) return;
67639
+ const key = `${deviceId}|${pinnedDeviceKey}`;
67640
+ if (this.reportedStalePins.has(key)) return;
67641
+ this.reportedStalePins.add(key);
67642
+ this.deps.logger.warn("device pin ignored — the pinned device is not enabled on the elected node", {
67643
+ tags: { deviceId },
67644
+ meta: {
67645
+ pin: pinnedDeviceKey,
67646
+ enabled: Object.keys(enabledDevices).join(",")
67647
+ }
67648
+ });
67649
+ }
67650
+ /**
66138
67651
  * Dispatch a session-time detection attach for an on-motion camera whose
66139
67652
  * `decideSessionAction` returned `'dispatch'`. Builds a session
66140
67653
  * `RunnerCameraConfig` the same way `startDetection` does, but forces
@@ -66170,7 +67683,7 @@ var SessionDispatchController = class {
66170
67683
  };
66171
67684
  if (reconcileBudgetExceeded) log.warn("detection session: placement census overran its budget — placing without it", { meta: { budgetMs: 500 } });
66172
67685
  else if (censusOutcome.kind === "rejected") log.warn("detection session: placement census failed — placing on the ledger as it is", { meta: {
66173
- error: errMsg$1(censusOutcome.error),
67686
+ error: errMsg$2(censusOutcome.error),
66174
67687
  reconcileMs
66175
67688
  } });
66176
67689
  const balanceStartedAt = Date.now();
@@ -66185,12 +67698,17 @@ var SessionDispatchController = class {
66185
67698
  const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
66186
67699
  const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
66187
67700
  if (usableEligible.length > 0 && usableEligible.length < allEligible.length) log.info("nodes dropped from session placement — no usable inference device", { meta: { dropped: allEligible.filter((n) => !usableEligible.includes(n)) } });
66188
- const decision = balance({
66189
- nodes: loads,
67701
+ const nodeCaps = await this.nodeCaps.get(CLUSTER_KEY);
67702
+ const weights = await this.nodeWeights.get(CLUSTER_KEY);
67703
+ const policy = this.deps.sessionPlacement?.() ?? "least-loaded";
67704
+ const decision = this.electSessionNode({
67705
+ deviceId,
67706
+ policy,
67707
+ loads,
66190
67708
  preferredAgent,
66191
- nodeCaps: await this.nodeCaps.get(CLUSTER_KEY),
66192
- weights: await this.nodeWeights.get(CLUSTER_KEY),
66193
- eligibleNodes: eligible
67709
+ nodeCaps,
67710
+ weights,
67711
+ eligible
66194
67712
  });
66195
67713
  if (!decision || decision.kind !== "assigned") {
66196
67714
  log.warn("detection session: no eligible node — will retry on next motion", { meta: { reason: decision?.kind === "pending" ? decision.reason : "no-runners-online" } });
@@ -66203,12 +67721,14 @@ var SessionDispatchController = class {
66203
67721
  const targetLoad = loads.find((n) => n.nodeId === targetNodeId);
66204
67722
  const deviceCaps = await this.inferenceDeviceCaps.get(targetNodeId);
66205
67723
  const pinnedDeviceKey = await this.deps.readPipelineDevicePin(deviceId);
67724
+ this.noteStalePin(deviceId, pinnedDeviceKey, enabledDevices);
66206
67725
  const selection = pickInferenceDevice({
66207
67726
  eligible: enabledDevices,
66208
67727
  loads: targetLoad?.devices ?? [],
66209
67728
  caps: deviceCaps,
66210
- pinnedDeviceKey,
66211
- rotation: this.deps.inferenceRotation
67729
+ pinnedDeviceKey: pinnedDeviceKey !== null && pinnedDeviceKey in enabledDevices ? pinnedDeviceKey : null,
67730
+ rotation: this.deps.inferenceRotation,
67731
+ policy
66212
67732
  });
66213
67733
  const deviceKey = selectedDeviceKey(selection);
66214
67734
  log.info("inference device selection", {
@@ -66296,6 +67816,7 @@ var SessionDispatchController = class {
66296
67816
  attachedAt = Date.now();
66297
67817
  this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
66298
67818
  this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), triggeredBy, deviceKey);
67819
+ this.deps.placementStats?.noteStart(targetNodeId, deviceKey ?? "");
66299
67820
  if (this.sessionRefusals.delete(deviceId)) log.info("detection session refusal cleared — a session opened", {
66300
67821
  tags: {
66301
67822
  deviceId,
@@ -66364,7 +67885,7 @@ var SessionDispatchController = class {
66364
67885
  await this.resolveSessionLifecycleLocked(deviceId, reason);
66365
67886
  }).catch((err) => {
66366
67887
  log.warn("detection session: lifecycle re-check failed", { meta: {
66367
- error: errMsg$1(err),
67888
+ error: errMsg$2(err),
66368
67889
  reason
66369
67890
  } });
66370
67891
  });
@@ -66469,7 +67990,7 @@ var SessionDispatchController = class {
66469
67990
  log.warn("detection session: teardown detach failed", {
66470
67991
  tags: { nodeId: session.nodeId },
66471
67992
  meta: {
66472
- error: errMsg$1(err),
67993
+ error: errMsg$2(err),
66473
67994
  reason
66474
67995
  }
66475
67996
  });
@@ -66576,7 +68097,7 @@ var SessionDispatchController = class {
66576
68097
  deviceId,
66577
68098
  nodeId: session.nodeId
66578
68099
  },
66579
- meta: { error: errMsg$1(err) }
68100
+ meta: { error: errMsg$2(err) }
66580
68101
  });
66581
68102
  });
66582
68103
  this.sessionRegistry.deregister(deviceId);
@@ -66597,7 +68118,7 @@ var SessionDispatchController = class {
66597
68118
  deviceId,
66598
68119
  nodeId: watchNodeId
66599
68120
  },
66600
- meta: { error: errMsg$1(err) }
68121
+ meta: { error: errMsg$2(err) }
66601
68122
  });
66602
68123
  });
66603
68124
  this.motionWatchByDevice.delete(deviceId);
@@ -66780,7 +68301,7 @@ async function sweepMirror(deps, hydrator, cameraIds) {
66780
68301
  tags: { deviceId },
66781
68302
  meta: {
66782
68303
  mirror: hydrator.mirror,
66783
- error: errMsg$1(err)
68304
+ error: errMsg$2(err)
66784
68305
  }
66785
68306
  });
66786
68307
  return "threw";
@@ -66820,7 +68341,7 @@ async function readCameraIds(deps) {
66820
68341
  lastError = err;
66821
68342
  }
66822
68343
  if (Date.now() >= deadline) {
66823
- deps.logger.warn("zones mirror boot hydration SKIPPED — camera list never became readable; mirror-only consumers keep whatever the last write left", { meta: { error: lastError === null ? "ctx.api unavailable" : errMsg$1(lastError) } });
68344
+ deps.logger.warn("zones mirror boot hydration SKIPPED — camera list never became readable; mirror-only consumers keep whatever the last write left", { meta: { error: lastError === null ? "ctx.api unavailable" : errMsg$2(lastError) } });
66824
68345
  return null;
66825
68346
  }
66826
68347
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
@@ -67441,6 +68962,7 @@ async function buildOrchestratorControllers(deps) {
67441
68962
  readGlobalSettings: () => globalSettings,
67442
68963
  onCensus: (entries) => {
67443
68964
  motionSliceGate?.noteCensus(entries);
68965
+ deps.onRunnerCensus(entries.flatMap((e) => e.load === null ? [] : [e.load]));
67444
68966
  providerSliceGate?.refreshIfStale("runner-census");
67445
68967
  }
67446
68968
  });
@@ -67467,7 +68989,7 @@ async function buildOrchestratorControllers(deps) {
67467
68989
  } catch (err) {
67468
68990
  return {
67469
68991
  kind: "unknown",
67470
- reason: errMsg$1(err)
68992
+ reason: errMsg$2(err)
67471
68993
  };
67472
68994
  }
67473
68995
  };
@@ -67552,18 +69074,19 @@ async function buildOrchestratorControllers(deps) {
67552
69074
  readGlobalSettings: () => globalSettings
67553
69075
  });
67554
69076
  await NodeStressMoveLedger.declare(deps.ctx().api.settingsStore).then(() => nodeStress.hydrate()).catch((err) => {
67555
- deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$1(err) } });
69077
+ deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$2(err) } });
67556
69078
  });
67557
69079
  await NodeStressLts.declare(deps.ctx().api.settingsStore).catch((err) => {
67558
- deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$1(err) } });
69080
+ deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$2(err) } });
67559
69081
  });
67560
69082
  const nodeStressLtsTimer = setInterval(() => {
67561
69083
  nodeStressLts.flushDue().catch((err) => {
67562
- deps.ctx().logger.warn("node-stress statistics flush failed", { meta: { error: errMsg$1(err) } });
69084
+ deps.ctx().logger.warn("node-stress statistics flush failed", { meta: { error: errMsg$2(err) } });
67563
69085
  });
67564
69086
  }, 6e4);
67565
69087
  nodeStress.start();
67566
69088
  const inferenceRotation = new RoundRobinInferenceDeviceRotation();
69089
+ const readSessionPlacement = () => parseSessionPlacementPolicy(globalSettings?.[SESSION_PLACEMENT_SETTING_KEY]);
67567
69090
  /**
67568
69091
  * Can the watchdog's audio stage ever receive a signal for this camera?
67569
69092
  * Only `absent` — POSITIVE evidence of no audio track — disarms it.
@@ -67647,6 +69170,8 @@ async function buildOrchestratorControllers(deps) {
67647
69170
  getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
67648
69171
  readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
67649
69172
  inferenceRotation,
69173
+ sessionPlacement: () => readSessionPlacement(),
69174
+ placementStats: deps.placementStats,
67650
69175
  logger: deps.ctx().logger
67651
69176
  });
67652
69177
  const deviceConfig = new DeviceConfigContributions({
@@ -67709,7 +69234,7 @@ async function buildOrchestratorControllers(deps) {
67709
69234
  } catch (err) {
67710
69235
  return {
67711
69236
  kind: "unreadable",
67712
- error: errMsg$1(err)
69237
+ error: errMsg$2(err)
67713
69238
  };
67714
69239
  }
67715
69240
  };
@@ -67743,7 +69268,7 @@ async function buildOrchestratorControllers(deps) {
67743
69268
  } catch (err) {
67744
69269
  return {
67745
69270
  kind: "unreadable",
67746
- error: errMsg$1(err)
69271
+ error: errMsg$2(err)
67747
69272
  };
67748
69273
  }
67749
69274
  },
@@ -67774,7 +69299,7 @@ async function buildOrchestratorControllers(deps) {
67774
69299
  } catch (err) {
67775
69300
  return {
67776
69301
  kind: "unreadable",
67777
- error: errMsg$1(err)
69302
+ error: errMsg$2(err)
67778
69303
  };
67779
69304
  }
67780
69305
  },
@@ -67787,7 +69312,7 @@ async function buildOrchestratorControllers(deps) {
67787
69312
  } catch (err) {
67788
69313
  return {
67789
69314
  kind: "unreadable",
67790
- error: errMsg$1(err)
69315
+ error: errMsg$2(err)
67791
69316
  };
67792
69317
  }
67793
69318
  },
@@ -67858,7 +69383,7 @@ async function buildOrchestratorControllers(deps) {
67858
69383
  deps.applyRuntimeSettings(globalSettings);
67859
69384
  await topology.refreshNodeCapabilities();
67860
69385
  } catch (err) {
67861
- const msg = errMsg$1(err);
69386
+ const msg = errMsg$2(err);
67862
69387
  deps.ctx().logger.warn("orchestrator settings load failed — using defaults", { meta: { error: msg } });
67863
69388
  }
67864
69389
  const rawNodeId = deps.ctx().kernel.localNodeId ?? "hub";
@@ -67899,7 +69424,7 @@ async function buildOrchestratorControllers(deps) {
67899
69424
  audio.markNodeOffline(nodeId);
67900
69425
  deps.forgetInferenceDeviceProbe(nodeId);
67901
69426
  nodeLifecycle.handleNodeDisconnect(nodeId).catch((err) => {
67902
- const msg = errMsg$1(err);
69427
+ const msg = errMsg$2(err);
67903
69428
  deps.ctx().logger.error("failover after disconnect failed", {
67904
69429
  tags: { nodeId },
67905
69430
  meta: { error: msg }
@@ -67934,7 +69459,7 @@ async function buildOrchestratorControllers(deps) {
67934
69459
  deps.subscribeToAudioAnalyzerReadiness(nodeId);
67935
69460
  }
67936
69461
  nodeLifecycle.handleNodeConnect(nodeId).catch((err) => {
67937
- const msg = errMsg$1(err);
69462
+ const msg = errMsg$2(err);
67938
69463
  deps.ctx().logger.error("reconnect handling failed", {
67939
69464
  tags: { nodeId },
67940
69465
  meta: { error: msg }
@@ -67953,7 +69478,7 @@ async function buildOrchestratorControllers(deps) {
67953
69478
  meta: { capName }
67954
69479
  });
67955
69480
  nodeLifecycle.handleNodeDisconnect(nodeId).catch((err) => {
67956
- const msg = errMsg$1(err);
69481
+ const msg = errMsg$2(err);
67957
69482
  deps.ctx().logger.error("failover after cap-down failed", {
67958
69483
  tags: { nodeId },
67959
69484
  meta: {
@@ -68016,7 +69541,7 @@ async function buildOrchestratorControllers(deps) {
68016
69541
  doneFlag: settingsStore.bindingsMigrationDoneState,
68017
69542
  logger: deps.ctx().logger
68018
69543
  }).catch((err) => {
68019
- deps.ctxIfReady()?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg$1(err) } });
69544
+ deps.ctxIfReady()?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg$2(err) } });
68020
69545
  });
68021
69546
  migrateStepGating({
68022
69547
  readCameraSettings: () => settingsStore.readCameraSettingsMap(),
@@ -68029,7 +69554,7 @@ async function buildOrchestratorControllers(deps) {
68029
69554
  for (let i = 0; deps.ctxIfReady()?.api == null && i < 30; i++) await new Promise((r) => setTimeout(r, 200));
68030
69555
  if (deps.ctxIfReady() !== null) await deps.resetRemovedModelPins("boot");
68031
69556
  }).catch((err) => {
68032
- deps.ctxIfReady()?.logger.warn("step-gating / per-node-device migration failed — retry next boot", { meta: { error: errMsg$1(err) } });
69557
+ deps.ctxIfReady()?.logger.warn("step-gating / per-node-device migration failed — retry next boot", { meta: { error: errMsg$2(err) } });
68033
69558
  });
68034
69559
  const zoneRulesProvider = new ZoneRulesProvider({
68035
69560
  logger: deps.ctx().logger.child("zone-rules"),
@@ -68071,7 +69596,7 @@ async function buildOrchestratorControllers(deps) {
68071
69596
  deviceId,
68072
69597
  nodeId: assignment.agentNodeId
68073
69598
  },
68074
- meta: { error: errMsg$1(err) }
69599
+ meta: { error: errMsg$2(err) }
68075
69600
  });
68076
69601
  });
68077
69602
  }
@@ -68087,7 +69612,7 @@ async function buildOrchestratorControllers(deps) {
68087
69612
  }],
68088
69613
  logger: deps.ctx().logger.child("zones")
68089
69614
  }).catch((err) => {
68090
- deps.ctxIfReady()?.logger.warn("zones mirror boot hydration failed", { meta: { error: errMsg$1(err) } });
69615
+ deps.ctxIfReady()?.logger.warn("zones mirror boot hydration failed", { meta: { error: errMsg$2(err) } });
68091
69616
  });
68092
69617
  const unsubOrchestratorSubscriptions = wireOrchestratorSubscriptions({
68093
69618
  eventBus: deps.ctx().eventBus,
@@ -68148,10 +69673,10 @@ async function buildOrchestratorControllers(deps) {
68148
69673
  deviceSettingsSchema: () => deps.deviceSettingsSchema()
68149
69674
  });
68150
69675
  await detectionWiring.hydrateFeaturesMirror().catch((err) => {
68151
- deps.ctx().logger.warn("device-features mirror not restored — this boot starts cold", { meta: { error: errMsg$1(err) } });
69676
+ deps.ctx().logger.warn("device-features mirror not restored — this boot starts cold", { meta: { error: errMsg$2(err) } });
68152
69677
  });
68153
69678
  cameraSensors.start().catch((err) => {
68154
- deps.ctx().logger.warn("camera sensors: boot refresh failed — waiting for the next lifecycle event", { meta: { error: errMsg$1(err) } });
69679
+ deps.ctx().logger.warn("camera sensors: boot refresh failed — waiting for the next lifecycle event", { meta: { error: errMsg$2(err) } });
68155
69680
  });
68156
69681
  return {
68157
69682
  ledger,
@@ -68180,7 +69705,8 @@ async function buildOrchestratorControllers(deps) {
68180
69705
  unsubAgentOffline,
68181
69706
  unsubOrchestratorSubscriptions,
68182
69707
  pendingRetryTimer,
68183
- autoRebalanceTimer
69708
+ autoRebalanceTimer,
69709
+ sessionPlacement: () => readSessionPlacement()
68184
69710
  };
68185
69711
  }
68186
69712
  //#endregion
@@ -68225,6 +69751,20 @@ function buildGlobalSettingsSections(options) {
68225
69751
  title: "Load Balancer",
68226
69752
  tab: "pipeline",
68227
69753
  fields: [
69754
+ {
69755
+ key: SESSION_PLACEMENT_SETTING_KEY,
69756
+ type: "select",
69757
+ label: "Session placement",
69758
+ description: "How a new detection session picks its node and device. Least-loaded = the node with the fewest sessions per unit of weight; an idle node always wins the tie, so weights only matter once nodes are busy. Honour shares = the share of PARALLEL sessions is held to the node and device weights, and a node at its cap yields to a free one. Moving a weight slider never changes this policy.",
69759
+ options: [{
69760
+ value: "least-loaded",
69761
+ label: "Least-loaded"
69762
+ }, {
69763
+ value: "share",
69764
+ label: "Honour shares"
69765
+ }],
69766
+ default: DEFAULT_SESSION_PLACEMENT_POLICY
69767
+ },
68228
69768
  {
68229
69769
  key: "fpsMinThreshold",
68230
69770
  type: "slider",
@@ -68783,6 +70323,959 @@ var pipelineOrchestratorWidgets = [{
68783
70323
  defaultRows: 4
68784
70324
  }];
68785
70325
  //#endregion
70326
+ //#region src/best-settings/plan-diff.ts
70327
+ /**
70328
+ * The Best settings writes, as pure transforms of the orchestrator's two
70329
+ * settings blobs (D745). The plan runs them on the current blobs to SHOW the
70330
+ * diff; the apply runs the same functions inside each blob's read-modify-write.
70331
+ *
70332
+ * What changes — and nothing else:
70333
+ * - node level (`agentSettings[node].inferenceDevices[deviceKey].steps`): the
70334
+ * object-detection `modelId` of every recommended device is set; the
70335
+ * `modelId` pin of every other in-scope step is removed (each runs its
70336
+ * step default again); every model-bound threshold (and its binding key) is
70337
+ * removed from every device's step `settings`, so the running model's own
70338
+ * defaults apply (D738–D741). `enabled`, `weight`, `maxSessions`,
70339
+ * `jumpDeviceKey` and every other setting are kept.
70340
+ * - camera level, scoped to the touched nodes: the same `modelId` pins and
70341
+ * model-bound thresholds are removed from `stepOverridesByDevice[node]` and
70342
+ * from the wholesale `pipelineByAgent[node]` snapshot (recursively). A
70343
+ * wholesale step keeps `enabled`, `children`, `jumpDeviceKey` and its other
70344
+ * settings: the attach provisions the node pin over the snapshot
70345
+ * (`applyDeviceProvisioning`: override ?? base), so once the snapshot pin
70346
+ * is gone the node's recommended model runs.
70347
+ * - never: `stepToggles`, devices' enablement, pins of
70348
+ * embedding steps (cluster-scoped, changing one forces a re-index).
70349
+ */
70350
+ /** The root detector step. */
70351
+ var DETECTOR_STEP_ID = "object-detection";
70352
+ /** Steps whose `modelId` pin Best settings never touches. */
70353
+ function isModelPinKept(stepId) {
70354
+ return stepId.endsWith("-embedding");
70355
+ }
70356
+ function boundKeysFor(input, stepId) {
70357
+ return input.boundKeysByStep[stepId] ?? [];
70358
+ }
70359
+ /** The model-bound keys present in `settings` (values or bindings), and the rest. */
70360
+ function stripBound(settings, boundKeys) {
70361
+ if (settings === void 0) return {
70362
+ kept: void 0,
70363
+ removed: []
70364
+ };
70365
+ const kept = { ...settings };
70366
+ const removed = [];
70367
+ for (const key of boundKeys) for (const k of [key, modelBindingKey(key)]) if (k in kept) {
70368
+ delete kept[k];
70369
+ removed.push(k);
70370
+ }
70371
+ return {
70372
+ kept,
70373
+ removed: removed.toSorted()
70374
+ };
70375
+ }
70376
+ function pinInScope(stepId) {
70377
+ return !isModelPinKept(stepId);
70378
+ }
70379
+ /** Rebuild one stored step without the in-scope pin and bound keys. */
70380
+ function scrubStep(stepId, step, boundKeys, targetModel) {
70381
+ const before = step.modelId ?? null;
70382
+ let modelId = step.modelId;
70383
+ if (stepId === "object-detection") {
70384
+ if (targetModel !== void 0) modelId = targetModel;
70385
+ } else if (pinInScope(stepId)) modelId = void 0;
70386
+ const { kept, removed } = stripBound(step.settings, boundKeys);
70387
+ const after = modelId ?? null;
70388
+ const next = {
70389
+ ...modelId !== void 0 ? { modelId } : {},
70390
+ ...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {},
70391
+ ...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
70392
+ };
70393
+ return {
70394
+ next: Object.keys(next).length > 0 ? next : null,
70395
+ pin: before !== after ? {
70396
+ stepId,
70397
+ before,
70398
+ after
70399
+ } : null,
70400
+ cleared: removed
70401
+ };
70402
+ }
70403
+ /** The node-level transform. Returns the next blob and every change it made. */
70404
+ function applyNodeBestSettings(blob, input) {
70405
+ const next = { ...blob };
70406
+ const changes = [];
70407
+ for (const nodeId of [...input.touchedNodes].toSorted()) {
70408
+ const entry = blob[nodeId];
70409
+ const targets = new Map((input.targets.get(nodeId) ?? []).map((t) => [t.deviceKey, t]));
70410
+ const stored = entry?.inferenceDevices ?? {};
70411
+ const deviceKeys = [...new Set([...Object.keys(stored), ...targets.keys()])].toSorted();
70412
+ const nextDevices = {};
70413
+ for (const deviceKey of deviceKeys) {
70414
+ const device = stored[deviceKey];
70415
+ const target = targets.get(deviceKey);
70416
+ const steps = { ...device?.steps };
70417
+ if (target !== void 0 && steps["object-detection"] === void 0) steps[DETECTOR_STEP_ID] = {};
70418
+ const nextSteps = {};
70419
+ const pins = [];
70420
+ const thresholds = [];
70421
+ for (const stepId of Object.keys(steps).toSorted()) {
70422
+ const step = steps[stepId];
70423
+ if (step === void 0) continue;
70424
+ const scrubbed = scrubStep(stepId, step, boundKeysFor(input, stepId), target?.modelId);
70425
+ if (scrubbed.next !== null) nextSteps[stepId] = scrubbed.next;
70426
+ if (scrubbed.pin !== null) pins.push(scrubbed.pin);
70427
+ if (scrubbed.cleared.length > 0) thresholds.push({
70428
+ stepId,
70429
+ keys: scrubbed.cleared
70430
+ });
70431
+ }
70432
+ const madeExplicit = device === void 0;
70433
+ nextDevices[deviceKey] = {
70434
+ ...device !== void 0 ? (({ steps: _steps, ...rest }) => rest)(device) : { enabled: target?.effectiveEnabled ?? false },
70435
+ ...Object.keys(nextSteps).length > 0 ? { steps: nextSteps } : {}
70436
+ };
70437
+ if (pins.length > 0 || thresholds.length > 0) changes.push({
70438
+ nodeId,
70439
+ deviceKey,
70440
+ madeExplicit,
70441
+ modelPins: pins,
70442
+ thresholds
70443
+ });
70444
+ }
70445
+ if (entry === void 0 && deviceKeys.length === 0) continue;
70446
+ next[nodeId] = {
70447
+ ...entry ?? { maxCameras: null },
70448
+ ...deviceKeys.length > 0 ? { inferenceDevices: nextDevices } : {}
70449
+ };
70450
+ }
70451
+ return {
70452
+ next,
70453
+ changes
70454
+ };
70455
+ }
70456
+ /** Scrub a wholesale snapshot tree; collects one clear per changed step. */
70457
+ function scrubTree(steps, input, onClear) {
70458
+ return steps.map((step) => {
70459
+ const children = step.children ? scrubTree(step.children, input, onClear) : void 0;
70460
+ const inScope = step.addonId === "object-detection" || pinInScope(step.addonId);
70461
+ const { kept, removed } = stripBound(step.settings, boundKeysFor(input, step.addonId));
70462
+ const dropPin = inScope && step.modelId !== void 0;
70463
+ if (dropPin || removed.length > 0) onClear(step.addonId, dropPin ? step.modelId ?? null : null, removed);
70464
+ const { modelId: _modelId, settings: _settings, children: _children, ...rest } = step;
70465
+ return {
70466
+ ...rest,
70467
+ ...!dropPin && step.modelId !== void 0 ? { modelId: step.modelId } : {},
70468
+ ...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {},
70469
+ ...children !== void 0 ? { children } : {}
70470
+ };
70471
+ });
70472
+ }
70473
+ /** The camera-level transform. Returns the next blob and every override it removed. */
70474
+ function applyCameraBestSettings(blob, input) {
70475
+ const next = {};
70476
+ const clears = [];
70477
+ for (const key of Object.keys(blob).toSorted((a, b) => Number(a) - Number(b))) {
70478
+ const camera = blob[key];
70479
+ if (camera === void 0) continue;
70480
+ const deviceId = Number(key);
70481
+ let nextCamera = camera;
70482
+ const byNode = camera.stepOverridesByDevice;
70483
+ if (byNode !== void 0) {
70484
+ const nextByNode = {};
70485
+ for (const [nodeId, byDevice] of Object.entries(byNode)) {
70486
+ if (!input.touchedNodes.has(nodeId)) {
70487
+ nextByNode[nodeId] = byDevice;
70488
+ continue;
70489
+ }
70490
+ const nextByDevice = {};
70491
+ for (const [deviceKey, byStep] of Object.entries(byDevice)) {
70492
+ const nextByStep = {};
70493
+ for (const [stepId, patch] of Object.entries(byStep)) {
70494
+ const inScope = stepId === "object-detection" || pinInScope(stepId);
70495
+ const { kept, removed } = stripBound(patch.settings, boundKeysFor(input, stepId));
70496
+ const dropPin = inScope && patch.modelId !== void 0;
70497
+ if (dropPin || removed.length > 0) clears.push({
70498
+ deviceId,
70499
+ source: "step-override",
70500
+ nodeId,
70501
+ deviceKey,
70502
+ stepId,
70503
+ modelIdBefore: dropPin ? patch.modelId ?? null : null,
70504
+ keys: removed
70505
+ });
70506
+ const rebuilt = {
70507
+ ...!dropPin && patch.modelId !== void 0 ? { modelId: patch.modelId } : {},
70508
+ ...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {}
70509
+ };
70510
+ if (Object.keys(rebuilt).length > 0) nextByStep[stepId] = rebuilt;
70511
+ }
70512
+ if (Object.keys(nextByStep).length > 0) nextByDevice[deviceKey] = nextByStep;
70513
+ }
70514
+ if (Object.keys(nextByDevice).length > 0) nextByNode[nodeId] = nextByDevice;
70515
+ }
70516
+ const { stepOverridesByDevice: _drop, ...rest } = nextCamera;
70517
+ nextCamera = {
70518
+ ...rest,
70519
+ ...Object.keys(nextByNode).length > 0 ? { stepOverridesByDevice: nextByNode } : {}
70520
+ };
70521
+ }
70522
+ const wholesale = camera.pipelineByAgent;
70523
+ if (wholesale !== void 0) {
70524
+ const nextWholesale = {};
70525
+ for (const [nodeId, snapshot] of Object.entries(wholesale)) {
70526
+ if (!input.touchedNodes.has(nodeId)) {
70527
+ nextWholesale[nodeId] = snapshot;
70528
+ continue;
70529
+ }
70530
+ nextWholesale[nodeId] = {
70531
+ ...snapshot,
70532
+ steps: scrubTree(snapshot.steps, input, (stepId, modelIdBefore, keys) => {
70533
+ clears.push({
70534
+ deviceId,
70535
+ source: "wholesale",
70536
+ nodeId,
70537
+ deviceKey: null,
70538
+ stepId,
70539
+ modelIdBefore,
70540
+ keys
70541
+ });
70542
+ })
70543
+ };
70544
+ }
70545
+ nextCamera = {
70546
+ ...nextCamera,
70547
+ pipelineByAgent: nextWholesale
70548
+ };
70549
+ }
70550
+ next[key] = nextCamera;
70551
+ }
70552
+ return {
70553
+ next,
70554
+ clears
70555
+ };
70556
+ }
70557
+ //#endregion
70558
+ //#region src/best-settings/best-settings-service.ts
70559
+ /**
70560
+ * Best settings (D745): gather the inputs, compute the plan, and apply exactly
70561
+ * the plan the operator confirmed.
70562
+ *
70563
+ * The plan is the recommender (`@camstack/types` best-settings) over every
70564
+ * runner node, plus the diff of the writes (`plan-diff.ts`) and the
70565
+ * per-camera detector thresholds the hub's executor would clear. Its
70566
+ * `planHash` names everything the apply writes; the apply recomputes the plan
70567
+ * and refuses a different hash, so a configuration change between the two is
70568
+ * shown to the operator again instead of being written over.
70569
+ *
70570
+ * Apply order, each phase reported:
70571
+ * 1. download every newly picked detector on its node FIRST — a node never
70572
+ * switches to a model it does not have (an N100 compile takes 5–25 s, a
70573
+ * Core ML package 40 MB); any failure stops the apply before a write;
70574
+ * 2. camera overrides (one camera-blob read-modify-write);
70575
+ * 3. per-camera detector thresholds (the hub executor's store);
70576
+ * 4. node model pins (one agent-blob read-modify-write);
70577
+ * 5. re-dispatch the cameras of every touched node.
70578
+ * A failure after phase 2 leaves "overrides cleared, previous node model",
70579
+ * which is a valid configuration (the defaults run); re-running converges.
70580
+ */
70581
+ function errMsg(err) {
70582
+ return err instanceof Error ? err.message : String(err);
70583
+ }
70584
+ /** Canonical JSON (sorted keys) — the plan hash must not depend on key order. */
70585
+ function canonical(value) {
70586
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
70587
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, v]) => v !== void 0).toSorted(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
70588
+ return JSON.stringify(value);
70589
+ }
70590
+ function formatsByModelOf(catalog) {
70591
+ const out = {};
70592
+ for (const slot of catalog?.slots ?? []) for (const addon of slot.addons) {
70593
+ if (addon.id !== "object-detection") continue;
70594
+ for (const model of addon.models) out[model.id] = MODEL_FORMATS.filter((f) => f in model.formats);
70595
+ }
70596
+ return out;
70597
+ }
70598
+ function boundKeysOf(catalogs) {
70599
+ const byStep = /* @__PURE__ */ new Map();
70600
+ for (const catalog of catalogs) for (const slot of catalog?.slots ?? []) for (const addon of slot.addons) {
70601
+ const keys = collectModelBoundKeys(addon.configSchema ?? []);
70602
+ if (keys.length === 0) continue;
70603
+ const set = byStep.get(addon.id) ?? /* @__PURE__ */ new Set();
70604
+ for (const k of keys) set.add(k);
70605
+ byStep.set(addon.id, set);
70606
+ }
70607
+ return Object.fromEntries([...byStep].map(([step, set]) => [step, [...set].toSorted()]));
70608
+ }
70609
+ /** Bounded fixed point of the hardware share suggestion (D748). */
70610
+ var SUGGEST_MAX_ROUNDS = 3;
70611
+ /** Integer shares summing to 100 (largest remainder). */
70612
+ function toPercent(weights) {
70613
+ const total = [...weights.values()].reduce((a, b) => a + b, 0);
70614
+ const out = /* @__PURE__ */ new Map();
70615
+ if (total <= 0) return out;
70616
+ const raw = [...weights].map(([k, w]) => ({
70617
+ k,
70618
+ exact: w / total * 100
70619
+ }));
70620
+ let assigned = 0;
70621
+ for (const r of raw) {
70622
+ out.set(r.k, Math.floor(r.exact));
70623
+ assigned += Math.floor(r.exact);
70624
+ }
70625
+ const byRemainder = raw.toSorted((a, b) => b.exact - Math.floor(b.exact) - (a.exact - Math.floor(a.exact)) || a.k.localeCompare(b.k));
70626
+ for (let i = 0; i < 100 - assigned; i++) {
70627
+ const r = byRemainder[i % byRemainder.length];
70628
+ out.set(r.k, (out.get(r.k) ?? 0) + 1);
70629
+ }
70630
+ return out;
70631
+ }
70632
+ var BestSettingsService = class {
70633
+ deps;
70634
+ applying = false;
70635
+ constructor(deps) {
70636
+ this.deps = deps;
70637
+ }
70638
+ async plan(input) {
70639
+ return (await this.computePlan(input.headroom, input.shares)).plan;
70640
+ }
70641
+ /**
70642
+ * Shares proportional to each node's capacity at the models Best settings
70643
+ * would pick under those very shares (D748). Model and load depend on each
70644
+ * other, so it iterates from equal shares until the rounded shares stop
70645
+ * moving, at most {@link SUGGEST_MAX_ROUNDS} rounds. Per device it reports an
70646
+ * ADVISORY session cap — never written, never enforced.
70647
+ */
70648
+ async suggestShares(input) {
70649
+ let shares = {
70650
+ nodes: {},
70651
+ devices: {}
70652
+ };
70653
+ let result = [];
70654
+ let cameraFps = 10;
70655
+ let rounds = 0;
70656
+ for (let round = 1; round <= SUGGEST_MAX_ROUNDS; round++) {
70657
+ rounds = round;
70658
+ const rec = await this.recommend(input.headroom, shares);
70659
+ cameraFps = rec.cameraFps;
70660
+ result = this.sharesFrom(rec, input.headroom);
70661
+ const next = {
70662
+ nodes: Object.fromEntries(result.flatMap((n) => n.share !== null && n.share > 0 ? [[n.nodeId, n.share]] : [])),
70663
+ devices: Object.fromEntries(result.map((n) => [n.nodeId, Object.fromEntries(n.devices.flatMap((d) => d.share > 0 ? [[d.deviceKey, d.share]] : []))]))
70664
+ };
70665
+ const settled = JSON.stringify(next) === JSON.stringify(shares);
70666
+ shares = next;
70667
+ if (settled) break;
70668
+ }
70669
+ return {
70670
+ headroom: input.headroom,
70671
+ cameraFps,
70672
+ iterations: rounds,
70673
+ nodes: [...result]
70674
+ };
70675
+ }
70676
+ sharesFrom(rec, headroom) {
70677
+ const capacityByNode = /* @__PURE__ */ new Map();
70678
+ const perNode = rec.recommendations.map((r) => {
70679
+ const devices = r.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
70680
+ const fps = d.chosen?.fps ?? 0;
70681
+ return {
70682
+ d,
70683
+ fps,
70684
+ sessions: rec.cameraFps > 0 ? Math.floor(fps * (1 - headroom) / rec.cameraFps) : 0
70685
+ };
70686
+ });
70687
+ const capacity = devices.reduce((sum, x) => sum + x.sessions, 0);
70688
+ if (!r.skipped && capacity > 0) capacityByNode.set(r.nodeId, capacity);
70689
+ return {
70690
+ r,
70691
+ devices,
70692
+ capacity
70693
+ };
70694
+ });
70695
+ const nodePct = toPercent(capacityByNode);
70696
+ return perNode.map(({ r, devices, capacity }) => {
70697
+ const devicePct = toPercent(new Map(devices.map((x) => [x.d.deviceKey, Math.max(x.fps, 0)])));
70698
+ return {
70699
+ nodeId: r.nodeId,
70700
+ share: nodePct.get(r.nodeId) ?? null,
70701
+ capacitySessions: capacity,
70702
+ skippedReason: r.skipped ? r.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity",
70703
+ devices: devices.map((x) => ({
70704
+ deviceKey: x.d.deviceKey,
70705
+ share: devicePct.get(x.d.deviceKey) ?? 0,
70706
+ modelId: x.d.chosen?.modelId ?? null,
70707
+ fps: x.d.chosen?.fps ?? null,
70708
+ measured: x.d.chosen?.measured ?? false,
70709
+ suggestedMaxSessions: x.sessions
70710
+ }))
70711
+ };
70712
+ });
70713
+ }
70714
+ /**
70715
+ * Gather every node and recommend. `shares` (a what-if or the suggestion's
70716
+ * round) forces share placement with those shares overlaid on the stored
70717
+ * ones; absent, the STORED policy decides how on-motion load is charged.
70718
+ */
70719
+ async recommend(headroom, shares) {
70720
+ const nodeIds = [...new Set(["hub", ...this.deps.runnerNodeIds()])].toSorted();
70721
+ const gathered = await Promise.all(nodeIds.map(async (nodeId) => {
70722
+ const [devices, hardware, catalog] = await Promise.all([
70723
+ this.deps.readNodeDevices(nodeId),
70724
+ this.deps.readHardware(nodeId).catch(() => null),
70725
+ this.deps.readCatalog(nodeId).catch(() => null)
70726
+ ]);
70727
+ return {
70728
+ nodeId,
70729
+ devices,
70730
+ hardware,
70731
+ catalog
70732
+ };
70733
+ }));
70734
+ const deviceShare = (nodeId, key, stored) => shares?.devices[nodeId]?.[key] ?? stored;
70735
+ const nodes = gathered.map((g) => ({
70736
+ nodeId: g.nodeId,
70737
+ reachable: g.devices.reachable && g.catalog !== null,
70738
+ hardware: g.hardware,
70739
+ devices: g.devices.devices.map((d) => ({
70740
+ key: d.key,
70741
+ format: d.format,
70742
+ enabled: d.enabled,
70743
+ available: d.available
70744
+ })),
70745
+ formatsByModel: formatsByModelOf(g.catalog)
70746
+ }));
70747
+ const eligible = gathered.flatMap((g) => g.devices.reachable ? g.devices.devices.filter((d) => d.enabled && d.available).map((d) => ({
70748
+ nodeId: g.nodeId,
70749
+ deviceKey: d.key,
70750
+ weight: deviceShare(g.nodeId, d.key, d.weight)
70751
+ })) : []);
70752
+ const cameras = await this.deps.cameraLoads();
70753
+ const policy = shares ? "share" : this.deps.sessionPlacement();
70754
+ const demand = estimateDeviceDemand({
70755
+ cameras,
70756
+ eligible,
70757
+ peaks: this.deps.peaks(),
70758
+ ...policy === "share" ? { shares: {
70759
+ nodes: {
70760
+ ...await this.deps.readNodeShares(),
70761
+ ...shares?.nodes
70762
+ },
70763
+ clusterPeak: this.deps.clusterPeak()
70764
+ } } : {}
70765
+ });
70766
+ const recommendations = nodes.map((node) => recommendNode({
70767
+ node,
70768
+ demand,
70769
+ headroom
70770
+ }));
70771
+ const cameraFps = cameras.reduce((m, c) => c.mode !== "disabled" && c.runsPipelineRoot ? Math.max(m, c.fps) : m, 0);
70772
+ return {
70773
+ gathered,
70774
+ nodes,
70775
+ recommendations,
70776
+ cameraFps: cameraFps > 0 ? cameraFps : 10
70777
+ };
70778
+ }
70779
+ async computePlan(headroom, shares) {
70780
+ const { gathered, nodes, recommendations } = await this.recommend(headroom, shares);
70781
+ const touchedNodes = new Set(recommendations.filter((r) => !r.skipped).map((r) => r.nodeId));
70782
+ const targets = /* @__PURE__ */ new Map();
70783
+ const formats = /* @__PURE__ */ new Map();
70784
+ for (const rec of recommendations) {
70785
+ if (rec.skipped) continue;
70786
+ const node = nodes.find((n) => n.nodeId === rec.nodeId);
70787
+ const list = [];
70788
+ const byKey = /* @__PURE__ */ new Map();
70789
+ for (const d of rec.devices) {
70790
+ if (d.chosen === null) continue;
70791
+ list.push({
70792
+ deviceKey: d.deviceKey,
70793
+ modelId: d.chosen.modelId,
70794
+ effectiveEnabled: d.enabled
70795
+ });
70796
+ const format = node?.devices.find((x) => x.key === d.deviceKey)?.format;
70797
+ if (format !== void 0) byKey.set(d.deviceKey, format);
70798
+ }
70799
+ targets.set(rec.nodeId, list);
70800
+ formats.set(rec.nodeId, byKey);
70801
+ }
70802
+ const diffInput = {
70803
+ touchedNodes,
70804
+ targets,
70805
+ boundKeysByStep: boundKeysOf(gathered.map((g) => g.catalog))
70806
+ };
70807
+ const nodeChanges = applyNodeBestSettings(await this.deps.readAgentSettings(), diffInput).changes;
70808
+ const cameraClears = applyCameraBestSettings(await this.deps.readCameraSettings(), diffInput).clears;
70809
+ const warnings = [];
70810
+ let detectorStoreClears = [];
70811
+ if (touchedNodes.size > 0) try {
70812
+ const answer = await this.deps.detectorStore({ dryRun: true });
70813
+ detectorStoreClears = answer.entries.map((e) => ({
70814
+ deviceId: e.deviceId,
70815
+ keys: [...e.keys]
70816
+ }));
70817
+ if (answer.refused.length > 0) warnings.push({
70818
+ kind: "detector-store-unreadable",
70819
+ error: `${answer.refused.length} camera store(s) unreadable`
70820
+ });
70821
+ } catch (err) {
70822
+ warnings.push({
70823
+ kind: "detector-store-unreadable",
70824
+ error: errMsg(err)
70825
+ });
70826
+ }
70827
+ const skippedNodes = recommendations.filter((r) => r.skipped).map((r) => r.nodeId);
70828
+ if (detectorStoreClears.length > 0 && skippedNodes.length > 0) warnings.push({
70829
+ kind: "detector-store-cluster-wide",
70830
+ skippedNodes
70831
+ });
70832
+ for (const rec of recommendations) for (const d of rec.devices) if (d.accelerator === "coral" && d.enabled) warnings.push({
70833
+ kind: "coral-enabled",
70834
+ nodeId: rec.nodeId,
70835
+ deviceKey: d.deviceKey
70836
+ });
70837
+ return {
70838
+ plan: {
70839
+ planHash: createHash("sha256").update(canonical({
70840
+ version: 1,
70841
+ headroom,
70842
+ whatIf: shares ?? null,
70843
+ picks: recommendations.map((r) => ({
70844
+ nodeId: r.nodeId,
70845
+ skipped: r.skipped,
70846
+ devices: r.devices.map((d) => [d.deviceKey, d.chosen?.modelId ?? null])
70847
+ })),
70848
+ nodeChanges,
70849
+ cameraClears,
70850
+ detectorStoreClears
70851
+ })).digest("hex"),
70852
+ family: "yolov9",
70853
+ headroom,
70854
+ whatIf: shares !== void 0,
70855
+ generatedAt: this.deps.now(),
70856
+ nodes: recommendations,
70857
+ nodeChanges: [...nodeChanges],
70858
+ cameraClears: [...cameraClears],
70859
+ detectorStoreClears,
70860
+ warnings
70861
+ },
70862
+ diffInput,
70863
+ formats
70864
+ };
70865
+ }
70866
+ async apply(input) {
70867
+ if (this.applying) return {
70868
+ kind: "refused",
70869
+ reason: "apply-in-progress"
70870
+ };
70871
+ this.applying = true;
70872
+ try {
70873
+ return await this.applyLocked(input);
70874
+ } finally {
70875
+ this.applying = false;
70876
+ }
70877
+ }
70878
+ async applyLocked(input) {
70879
+ const { plan, diffInput, formats } = await this.computePlan(input.headroom);
70880
+ if (plan.planHash !== input.planHash) {
70881
+ this.deps.logger.warn("best settings: stale plan refused", { meta: {
70882
+ confirmed: input.planHash,
70883
+ current: plan.planHash
70884
+ } });
70885
+ return {
70886
+ kind: "stale",
70887
+ plan
70888
+ };
70889
+ }
70890
+ const phases = [];
70891
+ const downloads = plan.nodeChanges.flatMap((c) => c.modelPins.filter((p) => p.stepId === "object-detection" && p.after !== null && p.after !== p.before).map((p) => ({
70892
+ nodeId: c.nodeId,
70893
+ modelId: p.after ?? "",
70894
+ format: formats.get(c.nodeId)?.get(c.deviceKey)
70895
+ })));
70896
+ const unique = new Map(downloads.map((d) => [`${d.nodeId}|${d.modelId}|${d.format}`, d]));
70897
+ try {
70898
+ for (const d of unique.values()) {
70899
+ if (d.format === void 0) continue;
70900
+ await this.deps.downloadModel(d.nodeId, d.modelId, d.format);
70901
+ }
70902
+ phases.push({
70903
+ phase: "download",
70904
+ ok: true,
70905
+ detail: `${unique.size} model(s) present on their nodes`
70906
+ });
70907
+ } catch (err) {
70908
+ phases.push({
70909
+ phase: "download",
70910
+ ok: false,
70911
+ detail: errMsg(err)
70912
+ });
70913
+ this.deps.logger.warn("best settings: a model download failed — nothing was written", { meta: { error: errMsg(err) } });
70914
+ return {
70915
+ kind: "aborted",
70916
+ phases
70917
+ };
70918
+ }
70919
+ try {
70920
+ await this.deps.rewriteCameraSettings((prev) => applyCameraBestSettings(prev, diffInput).next);
70921
+ for (const c of plan.cameraClears) this.deps.logger.info("best settings: camera override cleared", {
70922
+ tags: {
70923
+ deviceId: c.deviceId,
70924
+ nodeId: c.nodeId
70925
+ },
70926
+ meta: {
70927
+ deviceId: c.deviceId,
70928
+ source: c.source,
70929
+ deviceKey: c.deviceKey,
70930
+ stepId: c.stepId,
70931
+ modelIdBefore: c.modelIdBefore,
70932
+ keys: c.keys
70933
+ }
70934
+ });
70935
+ phases.push({
70936
+ phase: "camera-overrides",
70937
+ ok: true,
70938
+ detail: `${plan.cameraClears.length} cleared`
70939
+ });
70940
+ } catch (err) {
70941
+ phases.push({
70942
+ phase: "camera-overrides",
70943
+ ok: false,
70944
+ detail: errMsg(err)
70945
+ });
70946
+ }
70947
+ if (plan.detectorStoreClears.length > 0) try {
70948
+ const answer = await this.deps.detectorStore({
70949
+ dryRun: false,
70950
+ deviceIds: plan.detectorStoreClears.map((c) => c.deviceId)
70951
+ });
70952
+ phases.push({
70953
+ phase: "detector-thresholds",
70954
+ ok: answer.refused.length === 0,
70955
+ detail: `${answer.entries.length} cleared, ${answer.refused.length} refused`
70956
+ });
70957
+ } catch (err) {
70958
+ phases.push({
70959
+ phase: "detector-thresholds",
70960
+ ok: false,
70961
+ detail: errMsg(err)
70962
+ });
70963
+ }
70964
+ try {
70965
+ await this.deps.rewriteAgentSettings((prev) => applyNodeBestSettings(prev, diffInput).next);
70966
+ for (const c of plan.nodeChanges) for (const pin of c.modelPins) this.deps.logger.info("best settings: node model set", {
70967
+ tags: { nodeId: c.nodeId },
70968
+ meta: {
70969
+ nodeId: c.nodeId,
70970
+ deviceKey: c.deviceKey,
70971
+ stepId: pin.stepId,
70972
+ from: pin.before,
70973
+ to: pin.after
70974
+ }
70975
+ });
70976
+ phases.push({
70977
+ phase: "node-models",
70978
+ ok: true,
70979
+ detail: `${plan.nodeChanges.length} device(s) updated`
70980
+ });
70981
+ } catch (err) {
70982
+ phases.push({
70983
+ phase: "node-models",
70984
+ ok: false,
70985
+ detail: errMsg(err)
70986
+ });
70987
+ }
70988
+ this.deps.invalidateCatalog();
70989
+ const failed = [];
70990
+ for (const nodeId of [...diffInput.touchedNodes].toSorted()) try {
70991
+ await this.deps.redispatch(nodeId);
70992
+ } catch (err) {
70993
+ failed.push(`${nodeId}: ${errMsg(err)}`);
70994
+ }
70995
+ phases.push({
70996
+ phase: "redispatch",
70997
+ ok: failed.length === 0,
70998
+ detail: failed.length === 0 ? `${diffInput.touchedNodes.size} node(s)` : failed.join("; ")
70999
+ });
71000
+ for (const rec of plan.nodes.filter((r) => r.skipped)) this.deps.logger.info("best settings: node skipped", {
71001
+ tags: { nodeId: rec.nodeId },
71002
+ meta: {
71003
+ nodeId: rec.nodeId,
71004
+ reasons: rec.reasons
71005
+ }
71006
+ });
71007
+ this.deps.logger.info("best settings: applied", { meta: {
71008
+ planHash: plan.planHash,
71009
+ nodeChanges: plan.nodeChanges.length,
71010
+ cameraClears: plan.cameraClears.length,
71011
+ detectorStoreClears: plan.detectorStoreClears.length,
71012
+ phases
71013
+ } });
71014
+ return {
71015
+ kind: "applied",
71016
+ planHash: plan.planHash,
71017
+ phases
71018
+ };
71019
+ }
71020
+ };
71021
+ //#endregion
71022
+ //#region src/best-settings/device-peak-tracker.ts
71023
+ var BUCKET_MS = 3600 * 1e3;
71024
+ var DevicePeakTracker = class DevicePeakTracker {
71025
+ now;
71026
+ series = /* @__PURE__ */ new Map();
71027
+ cluster = null;
71028
+ constructor(now = () => Date.now()) {
71029
+ this.now = now;
71030
+ }
71031
+ static key(nodeId, deviceKey) {
71032
+ return `${nodeId}\u0000${deviceKey}`;
71033
+ }
71034
+ /** Fold one runner census in. A runner that did not answer is simply absent. */
71035
+ noteCensus(loads) {
71036
+ const now = this.now();
71037
+ const bucket = now - now % BUCKET_MS;
71038
+ let clusterTotal = 0;
71039
+ for (const load of loads) {
71040
+ const rows = load.devices.length > 0 ? load.devices : [{
71041
+ deviceKey: "",
71042
+ attachedCameras: load.attachedCameras
71043
+ }];
71044
+ for (const device of rows) {
71045
+ clusterTotal += device.attachedCameras;
71046
+ const key = DevicePeakTracker.key(load.nodeId, device.deviceKey);
71047
+ let s = this.series.get(key);
71048
+ if (s === void 0) {
71049
+ s = {
71050
+ firstSeenAt: now,
71051
+ buckets: /* @__PURE__ */ new Map()
71052
+ };
71053
+ this.series.set(key, s);
71054
+ }
71055
+ s.buckets.set(bucket, Math.max(s.buckets.get(bucket) ?? 0, device.attachedCameras));
71056
+ for (const start of s.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) s.buckets.delete(start);
71057
+ }
71058
+ }
71059
+ if (loads.length === 0) return;
71060
+ const c = this.cluster ?? {
71061
+ firstSeenAt: now,
71062
+ buckets: /* @__PURE__ */ new Map()
71063
+ };
71064
+ this.cluster = c;
71065
+ c.buckets.set(bucket, Math.max(c.buckets.get(bucket) ?? 0, clusterTotal));
71066
+ for (const start of c.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) c.buckets.delete(start);
71067
+ }
71068
+ /** The cluster-wide parallel peak over the window, or null before any census. */
71069
+ clusterPeak() {
71070
+ const c = this.cluster;
71071
+ if (c === null) return null;
71072
+ const now = this.now();
71073
+ let peak = 0;
71074
+ for (const [start, value] of c.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
71075
+ return {
71076
+ peakSessions: peak,
71077
+ observedForMs: now - c.firstSeenAt
71078
+ };
71079
+ }
71080
+ /** Every observed device's peak over the window and how long it has been watched. */
71081
+ peaks() {
71082
+ const now = this.now();
71083
+ const out = [];
71084
+ for (const [key, s] of this.series) {
71085
+ const [nodeId = "", deviceKey = ""] = key.split("\0");
71086
+ let peak = 0;
71087
+ for (const [start, value] of s.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
71088
+ out.push({
71089
+ nodeId,
71090
+ deviceKey,
71091
+ peakSessions: peak,
71092
+ observedForMs: now - s.firstSeenAt
71093
+ });
71094
+ }
71095
+ return out.toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
71096
+ }
71097
+ };
71098
+ //#endregion
71099
+ //#region src/session-placement-stats.ts
71100
+ var RETAIN_MS = 1440 * 60 * 1e3;
71101
+ function keyOf(nodeId, deviceKey) {
71102
+ return `${nodeId}\u0000${deviceKey}`;
71103
+ }
71104
+ function split(key) {
71105
+ const [nodeId = "", deviceKey = ""] = key.split("\0");
71106
+ return {
71107
+ nodeId,
71108
+ deviceKey
71109
+ };
71110
+ }
71111
+ var SessionPlacementStats = class {
71112
+ now;
71113
+ segments = [];
71114
+ starts = [];
71115
+ yields = [];
71116
+ startedAt;
71117
+ constructor(now = () => Date.now()) {
71118
+ this.now = now;
71119
+ this.startedAt = now();
71120
+ }
71121
+ /** The running-session set changed: open a new segment with these counts. */
71122
+ noteSessions(sessions) {
71123
+ const counts = /* @__PURE__ */ new Map();
71124
+ for (const s of sessions) {
71125
+ const k = keyOf(s.nodeId, s.deviceKey);
71126
+ counts.set(k, (counts.get(k) ?? 0) + 1);
71127
+ }
71128
+ this.segments.push({
71129
+ at: this.now(),
71130
+ counts
71131
+ });
71132
+ this.prune();
71133
+ }
71134
+ noteStart(nodeId, deviceKey) {
71135
+ this.starts.push({
71136
+ at: this.now(),
71137
+ nodeId,
71138
+ deviceKey
71139
+ });
71140
+ this.prune();
71141
+ }
71142
+ /** A share election handed `nodeId`'s turn to another node. */
71143
+ noteYield(nodeId) {
71144
+ this.yields.push({
71145
+ at: this.now(),
71146
+ nodeId
71147
+ });
71148
+ this.prune();
71149
+ }
71150
+ /** For tests: how many segments are retained. */
71151
+ segmentCount() {
71152
+ return this.segments.length;
71153
+ }
71154
+ prune() {
71155
+ const floor = this.now() - RETAIN_MS;
71156
+ while (this.segments.length > 1 && this.segments[1].at <= floor) this.segments.shift();
71157
+ while (this.starts.length > 0 && this.starts[0].at < floor) this.starts.shift();
71158
+ while (this.yields.length > 0 && this.yields[0].at < floor) this.yields.shift();
71159
+ }
71160
+ snapshot(windowMs) {
71161
+ const now = this.now();
71162
+ const from = now - windowMs;
71163
+ const integral = /* @__PURE__ */ new Map();
71164
+ let clusterPeak = 0;
71165
+ for (let i = 0; i < this.segments.length; i++) {
71166
+ const seg = this.segments[i];
71167
+ const end = this.segments[i + 1]?.at ?? now;
71168
+ const start = Math.max(seg.at, from);
71169
+ if (end <= start) continue;
71170
+ let total = 0;
71171
+ for (const [k, n] of seg.counts) {
71172
+ integral.set(k, (integral.get(k) ?? 0) + n * (end - start));
71173
+ total += n;
71174
+ }
71175
+ clusterPeak = Math.max(clusterPeak, total);
71176
+ }
71177
+ const current = this.segments.at(-1)?.counts ?? /* @__PURE__ */ new Map();
71178
+ const nodeIds = /* @__PURE__ */ new Set();
71179
+ for (const k of [...integral.keys(), ...current.keys()]) nodeIds.add(split(k).nodeId);
71180
+ for (const s of this.starts) if (s.at >= from) nodeIds.add(s.nodeId);
71181
+ const clusterIntegral = [...integral.values()].reduce((a, b) => a + b, 0);
71182
+ const nodes = [...nodeIds].toSorted().map((nodeId) => {
71183
+ const deviceKeys = /* @__PURE__ */ new Set();
71184
+ for (const k of [...integral.keys(), ...current.keys()]) {
71185
+ const s = split(k);
71186
+ if (s.nodeId === nodeId) deviceKeys.add(s.deviceKey);
71187
+ }
71188
+ for (const s of this.starts) if (s.at >= from && s.nodeId === nodeId) deviceKeys.add(s.deviceKey);
71189
+ const nodeIntegral = [...deviceKeys].reduce((sum, d) => sum + (integral.get(keyOf(nodeId, d)) ?? 0), 0);
71190
+ const devices = [...deviceKeys].toSorted().map((deviceKey) => ({
71191
+ deviceKey,
71192
+ sessionsNow: current.get(keyOf(nodeId, deviceKey)) ?? 0,
71193
+ sessionsStarted: this.starts.filter((s) => s.at >= from && s.nodeId === nodeId && s.deviceKey === deviceKey).length,
71194
+ observedShare: nodeIntegral > 0 ? (integral.get(keyOf(nodeId, deviceKey)) ?? 0) / nodeIntegral : null
71195
+ }));
71196
+ let peak = 0;
71197
+ for (let i = 0; i < this.segments.length; i++) {
71198
+ const seg = this.segments[i];
71199
+ if ((this.segments[i + 1]?.at ?? now) <= Math.max(seg.at, from)) continue;
71200
+ let n = 0;
71201
+ for (const [k, c] of seg.counts) if (split(k).nodeId === nodeId) n += c;
71202
+ peak = Math.max(peak, n);
71203
+ }
71204
+ return {
71205
+ nodeId,
71206
+ sessionsNow: devices.reduce((s, d) => s + d.sessionsNow, 0),
71207
+ sessionsStarted: devices.reduce((s, d) => s + d.sessionsStarted, 0),
71208
+ observedShare: clusterIntegral > 0 && nodeIntegral > 0 ? nodeIntegral / clusterIntegral : null,
71209
+ peakSessions: peak,
71210
+ yieldsAway: this.yields.filter((y) => y.at >= from && y.nodeId === nodeId).length,
71211
+ devices
71212
+ };
71213
+ });
71214
+ return {
71215
+ observedForMs: Math.min(windowMs, now - this.startedAt),
71216
+ sessionsNow: [...current.values()].reduce((a, b) => a + b, 0),
71217
+ clusterPeakSessions: clusterPeak,
71218
+ nodes
71219
+ };
71220
+ }
71221
+ };
71222
+ //#endregion
71223
+ //#region src/load-balancing-shares.ts
71224
+ function nextDevices(stored, shares, effective) {
71225
+ let changed = false;
71226
+ const out = { ...stored };
71227
+ for (const [key, weight] of Object.entries(shares)) {
71228
+ const existing = stored?.[key];
71229
+ if (existing !== void 0) {
71230
+ out[key] = {
71231
+ ...existing,
71232
+ weight
71233
+ };
71234
+ changed = true;
71235
+ continue;
71236
+ }
71237
+ const enabled = effective[key];
71238
+ if (enabled === void 0) continue;
71239
+ out[key] = {
71240
+ enabled,
71241
+ weight
71242
+ };
71243
+ changed = true;
71244
+ }
71245
+ return changed ? out : stored;
71246
+ }
71247
+ function applyLoadBalancingShares(prev, write) {
71248
+ const next = { ...prev };
71249
+ const nodeIds = new Set([...Object.keys(write.nodes), ...Object.keys(write.devices)]);
71250
+ for (const nodeId of nodeIds) {
71251
+ const existing = prev[nodeId];
71252
+ const nodeShare = write.nodes[nodeId];
71253
+ const devices = nextDevices(existing?.inferenceDevices, write.devices[nodeId] ?? {}, write.effectiveEnabled[nodeId] ?? {});
71254
+ if (nodeShare === void 0 && devices === existing?.inferenceDevices) continue;
71255
+ next[nodeId] = {
71256
+ ...existing ?? { maxCameras: null },
71257
+ ...nodeShare !== void 0 ? { detectWeight: nodeShare } : {},
71258
+ ...devices !== void 0 ? { inferenceDevices: devices } : {}
71259
+ };
71260
+ }
71261
+ return next;
71262
+ }
71263
+ //#endregion
71264
+ //#region src/stale-device-pins.ts
71265
+ function findStaleDevicePins(pins, nodes) {
71266
+ const usable = /* @__PURE__ */ new Map();
71267
+ for (const node of nodes) for (const d of node.devices) usable.set(d.key, (usable.get(d.key) ?? false) || d.enabled && d.available);
71268
+ return pins.flatMap((pin) => {
71269
+ const state = usable.get(pin.deviceKey);
71270
+ if (state === true) return [];
71271
+ return [{
71272
+ deviceId: pin.deviceId,
71273
+ deviceKey: pin.deviceKey,
71274
+ reason: state === false ? "disabled" : "absent"
71275
+ }];
71276
+ }).toSorted((a, b) => a.deviceId - b.deviceId);
71277
+ }
71278
+ //#endregion
68786
71279
  //#region src/index.ts
68787
71280
  /**
68788
71281
  * addon-pipeline-orchestrator — hub-side camera-to-agent load balancer.
@@ -68850,6 +71343,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
68850
71343
  * not a claim this flag makes.
68851
71344
  */
68852
71345
  clusterModelChoiceReady = false;
71346
+ /** The per-device 24 h peak of concurrent sessions, fed by every runner census (D745). */
71347
+ devicePeaks = new DevicePeakTracker();
71348
+ /** What session placement did (D748): the Load balancing tab's "actual" column. */
71349
+ placementStats = new SessionPlacementStats();
71350
+ /** The stored session placement policy (D748); `least-loaded` before init. */
71351
+ readSessionPlacement = () => "least-loaded";
71352
+ bestSettingsService = null;
68853
71353
  /**
68854
71354
  * Single owner of the shared placement state (`assignments`/`cameraConfigs`/
68855
71355
  * `pendingReasons`/`dispatchingDevices`) — see `placement-ledger.ts` (S6
@@ -69126,6 +71626,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
69126
71626
  rebalance: (opts) => this.rebalance(opts),
69127
71627
  setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
69128
71628
  maintenancePaused: () => this.storageMigrationLeaseId !== null,
71629
+ onRunnerCensus: (loads) => this.devicePeaks.noteCensus(loads),
71630
+ placementStats: this.placementStats,
69129
71631
  setTopology: (topology) => {
69130
71632
  this.topology = topology;
69131
71633
  },
@@ -69152,6 +71654,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
69152
71654
  this.nodeStress = controllers.nodeStress;
69153
71655
  this.placement = controllers.placement;
69154
71656
  this.session = controllers.session;
71657
+ this.readSessionPlacement = controllers.sessionPlacement;
69155
71658
  this.deviceConfig = controllers.deviceConfig;
69156
71659
  this.cameraStatusService = controllers.cameraStatusService;
69157
71660
  this.cameraSwitchService = controllers.cameraSwitchService;
@@ -69267,7 +71770,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
69267
71770
  for (const deviceId of this.detectionWiring.activeDetectionDeviceIds()) try {
69268
71771
  await this.releaseCamera({ deviceId });
69269
71772
  } catch (err) {
69270
- const msg = errMsg$1(err);
71773
+ const msg = errMsg$2(err);
69271
71774
  this.ctx.logger.debug("releaseCamera failed during shutdown", {
69272
71775
  tags: { deviceId },
69273
71776
  meta: { error: msg }
@@ -69644,26 +72147,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
69644
72147
  this.reconcile.schedulePendingRetry();
69645
72148
  return { success: true };
69646
72149
  }
69647
- async setAgentDetectWeight(input) {
69648
- const existing = (await this.settingsStore.readAgentSettingsMap())[input.agentNodeId];
69649
- const weight = input.detectWeight ?? 1;
69650
- const next = existing ? {
69651
- ...existing,
69652
- detectWeight: weight
69653
- } : {
69654
- maxCameras: null,
69655
- detectWeight: weight
69656
- };
69657
- await this.settingsStore.writeAgentSettings(input.agentNodeId, next);
69658
- this.ctx.logger.info("agentSettings.detectWeight updated", {
69659
- tags: { nodeId: input.agentNodeId },
69660
- meta: { detectWeight: weight ?? null }
69661
- });
69662
- this.rebalance().catch((err) => {
69663
- this.ctx.logger.warn("setAgentDetectWeight: rebalance failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
69664
- });
69665
- return { success: true };
69666
- }
69667
72150
  /**
69668
72151
  * Set one node's placement capabilities — the per-node record that replaced
69669
72152
  * the four flat orchestrator lists. Each flag is a tri-state patch: omit to
@@ -70062,7 +72545,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70062
72545
  tags: { nodeId },
70063
72546
  meta: {
70064
72547
  nodeId,
70065
- error: errMsg$1(err)
72548
+ error: errMsg$2(err)
70066
72549
  }
70067
72550
  });
70068
72551
  return null;
@@ -70168,7 +72651,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70168
72651
  tags: { nodeId },
70169
72652
  meta: {
70170
72653
  nodeId,
70171
- error: errMsg$1(err)
72654
+ error: errMsg$2(err)
70172
72655
  }
70173
72656
  });
70174
72657
  return {
@@ -70215,7 +72698,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70215
72698
  } catch (err) {
70216
72699
  this.ctx.logger.warn("resetNodePipelineDefaults: executor clearDeviceOverrides failed (node offline?)", {
70217
72700
  tags: { nodeId: node },
70218
- meta: { error: errMsg$1(err) }
72701
+ meta: { error: errMsg$2(err) }
70219
72702
  });
70220
72703
  }
70221
72704
  this.settingsStore.invalidateCatalog();
@@ -70226,7 +72709,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70226
72709
  } catch (err) {
70227
72710
  this.ctx.logger.warn("resetNodePipelineDefaults: effective-model probe failed (node offline?)", {
70228
72711
  tags: { nodeId: node },
70229
- meta: { error: errMsg$1(err) }
72712
+ meta: { error: errMsg$2(err) }
70230
72713
  });
70231
72714
  }
70232
72715
  this.ctx.logger.info("node pipeline reset to hardware-aware defaults", {
@@ -70242,6 +72725,137 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70242
72725
  clearedCameraOverrides
70243
72726
  };
70244
72727
  }
72728
+ bestSettings() {
72729
+ if (this.bestSettingsService !== null) return this.bestSettingsService;
72730
+ const store = this.settingsStore;
72731
+ this.bestSettingsService = new BestSettingsService({
72732
+ logger: this.ctx.logger,
72733
+ runnerNodeIds: () => this.topology.collectRunnerNodeIds(),
72734
+ readNodeDevices: (nodeId) => this.getNodeInferenceDevices({ nodeId }),
72735
+ readHardware: async (nodeId) => {
72736
+ const api = this.ctx.api;
72737
+ if (!api) return null;
72738
+ return nodeId === this.localNodeId ? api.platformProbe.getHardware.query() : api.platformProbe.getHardware.query({ nodeId });
72739
+ },
72740
+ readCatalog: (nodeId) => store.readCatalogForNode(nodeId),
72741
+ readAgentSettings: () => store.readAgentSettingsMap(),
72742
+ readCameraSettings: () => store.readCameraSettingsMap(),
72743
+ rewriteAgentSettings: (transform) => store.rewriteAgentSettingsMap(transform),
72744
+ rewriteCameraSettings: (transform) => store.rewriteCameraSettingsMap(transform),
72745
+ cameraLoads: () => this.bestSettingsCameraLoads(),
72746
+ peaks: () => this.devicePeaks.peaks(),
72747
+ clusterPeak: () => this.devicePeaks.clusterPeak(),
72748
+ sessionPlacement: () => this.readSessionPlacement(),
72749
+ readNodeShares: () => store.buildNodeWeights(),
72750
+ detectorStore: async (input) => {
72751
+ const api = this.ctx.api;
72752
+ if (!api) throw new Error("ctx.api unavailable");
72753
+ return api.pipelineExecutor.clearModelBoundDeviceOverrides.mutate({
72754
+ nodeId: this.localNodeId,
72755
+ dryRun: input.dryRun,
72756
+ ...input.deviceIds !== void 0 ? { deviceIds: input.deviceIds } : {}
72757
+ });
72758
+ },
72759
+ downloadModel: async (nodeId, modelId, format) => {
72760
+ const api = this.ctx.api;
72761
+ if (!api) throw new Error("ctx.api unavailable");
72762
+ await api.pipelineExecutor.downloadModel.mutate({
72763
+ addonId: "object-detection",
72764
+ modelId,
72765
+ format
72766
+ }, nodePin(nodeId));
72767
+ },
72768
+ invalidateCatalog: () => store.invalidateCatalog(),
72769
+ redispatch: (nodeId) => this.placement.redispatchCamerasOnAgent(nodeId),
72770
+ now: () => Date.now()
72771
+ });
72772
+ return this.bestSettingsService;
72773
+ }
72774
+ /** Every camera with detection started, as the recommender charges it (D745). */
72775
+ async bestSettingsCameraLoads() {
72776
+ const standing = new Map(this.getPipelineAssignments().map((a) => [a.deviceId, a.agentNodeId]));
72777
+ const loads = [];
72778
+ for (const [deviceId, config] of this.detectionWiring.activeDetections) {
72779
+ const nodePin = await this.readPipelinePin(deviceId);
72780
+ const pinnedKey = await this.readPipelineDevicePin(deviceId);
72781
+ const standingNode = config.detectionMode === "always-on" ? standing.get(deviceId) ?? null : null;
72782
+ loads.push({
72783
+ deviceId,
72784
+ mode: config.detectionMode,
72785
+ fps: config.detectionFps,
72786
+ runsPipelineRoot: (config.detectionSources ?? ["pipeline"]).includes("pipeline"),
72787
+ standingNodeId: standingNode,
72788
+ nodePinId: nodePin,
72789
+ devicePin: pinnedKey !== null ? {
72790
+ nodeId: nodePin ?? standingNode,
72791
+ deviceKey: pinnedKey
72792
+ } : null
72793
+ });
72794
+ }
72795
+ return loads;
72796
+ }
72797
+ async planBestSettings(input) {
72798
+ return this.bestSettings().plan(input);
72799
+ }
72800
+ /**
72801
+ * Node + device shares in ONE read-modify-write of the agent settings. A
72802
+ * device with no stored entry is made explicit with its effective
72803
+ * enablement, read from the probe-aware device list. Never touches the
72804
+ * `sessionPlacement` policy.
72805
+ */
72806
+ async setLoadBalancingShares(input) {
72807
+ const effectiveEnabled = {};
72808
+ for (const nodeId of Object.keys(input.devices)) {
72809
+ const read = await this.getNodeInferenceDevices({ nodeId });
72810
+ effectiveEnabled[nodeId] = Object.fromEntries(read.devices.map((d) => [d.key, d.enabled]));
72811
+ }
72812
+ await this.settingsStore.rewriteAgentSettingsMap((prev) => applyLoadBalancingShares(prev, {
72813
+ nodes: input.nodes,
72814
+ devices: input.devices,
72815
+ effectiveEnabled
72816
+ }));
72817
+ this.ctx.logger.info("load balancing shares updated", { meta: {
72818
+ nodes: JSON.stringify(input.nodes),
72819
+ devices: JSON.stringify(input.devices),
72820
+ policy: this.readSessionPlacement()
72821
+ } });
72822
+ if (Object.keys(input.nodes).length > 0) this.rebalance().catch((err) => {
72823
+ this.ctx.logger.warn("setLoadBalancingShares: rebalance failed", { meta: { error: errMsg$2(err) } });
72824
+ });
72825
+ return { success: true };
72826
+ }
72827
+ async getPlacementStats(input) {
72828
+ const snap = this.placementStats.snapshot(input.windowMs);
72829
+ return {
72830
+ policy: this.readSessionPlacement(),
72831
+ windowMs: input.windowMs,
72832
+ observedForMs: snap.observedForMs,
72833
+ sessionsNow: snap.sessionsNow,
72834
+ clusterPeakSessions: snap.clusterPeakSessions,
72835
+ nodes: snap.nodes,
72836
+ stalePins: await this.listStaleDevicePins()
72837
+ };
72838
+ }
72839
+ /** Device pins the session path cannot honour — see `stale-device-pins.ts`. */
72840
+ async listStaleDevicePins() {
72841
+ const nodeIds = [...new Set(["hub", ...this.topology.collectRunnerNodeIds()])];
72842
+ const offered = await Promise.all(nodeIds.map(async (nodeId) => ({ devices: (await this.getNodeInferenceDevices({ nodeId }).catch(() => null))?.devices ?? [] })));
72843
+ const pins = [];
72844
+ for (const deviceId of this.detectionWiring.activeDetections.keys()) {
72845
+ const deviceKey = await this.readPipelineDevicePin(deviceId);
72846
+ if (deviceKey !== null) pins.push({
72847
+ deviceId,
72848
+ deviceKey
72849
+ });
72850
+ }
72851
+ return findStaleDevicePins(pins, offered);
72852
+ }
72853
+ async suggestShares(input) {
72854
+ return this.bestSettings().suggestShares(input);
72855
+ }
72856
+ async applyBestSettings(input) {
72857
+ return this.bestSettings().apply(input);
72858
+ }
70245
72859
  async getCameraSettings(input) {
70246
72860
  return (await this.settingsStore.readCameraSettingsMap())[String(input.deviceId)] ?? null;
70247
72861
  }
@@ -70498,7 +73112,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
70498
73112
  tags: { deviceId },
70499
73113
  meta: {
70500
73114
  capName,
70501
- error: errMsg$1(err)
73115
+ error: errMsg$2(err)
70502
73116
  }
70503
73117
  });
70504
73118
  return true;