@camstack/addon-pipeline-orchestrator 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/@mf-types/compiled-types/widgets/PipelineQuickStats.d.ts +3 -0
  2. package/dist/@mf-types/compiled-types/widgets/PipelineQuickStats.d.ts.map +1 -0
  3. package/dist/@mf-types/compiled-types/widgets/ZoneEditor.d.ts +17 -0
  4. package/dist/@mf-types/compiled-types/widgets/ZoneEditor.d.ts.map +1 -0
  5. package/dist/@mf-types/compiled-types/widgets/ZonesTab.d.ts +7 -0
  6. package/dist/@mf-types/compiled-types/widgets/ZonesTab.d.ts.map +1 -0
  7. package/dist/@mf-types/compiled-types/widgets/index.d.ts +13 -0
  8. package/dist/@mf-types/compiled-types/widgets/index.d.ts.map +1 -0
  9. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts +60 -0
  10. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts.map +1 -0
  11. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts +23 -0
  12. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts.map +1 -0
  13. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts +15 -0
  14. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts.map +1 -0
  15. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneOverlay.d.ts +19 -0
  16. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneOverlay.d.ts.map +1 -0
  17. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneRulesEditor.d.ts +9 -0
  18. package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneRulesEditor.d.ts.map +1 -0
  19. package/dist/@mf-types/widgets.d.ts +2 -0
  20. package/dist/@mf-types.d.ts +3 -0
  21. package/dist/@mf-types.zip +0 -0
  22. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-CCBTZBOa.mjs +12 -0
  23. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CvusB3VY.mjs +17 -0
  24. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BrYYwKk_.mjs +34 -0
  25. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-DoWbefqS.mjs +104 -0
  26. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-D4eEXltm.mjs +85 -0
  27. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-CVrnrGED.mjs +62 -0
  28. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs-B848Fc_m.mjs +88 -0
  29. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-B-q1guKT.mjs +29 -0
  30. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-Cg6QsnjR.mjs +36 -0
  31. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-Dp8hqYOB.mjs +45 -0
  32. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CeGb2_QF.mjs +6 -0
  33. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-BBmNf5hf.mjs +34 -0
  34. package/dist/_stub.js +16268 -0
  35. package/dist/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-C5Dbnljh.mjs +157 -0
  36. package/dist/client-BkQItW6e.mjs +9836 -0
  37. package/dist/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +211 -0
  38. package/dist/hostInit-xzBPWUz0.mjs +168 -0
  39. package/dist/index-BI-_eQhe.mjs +185 -0
  40. package/dist/index-BJzn4K_R.mjs +2603 -0
  41. package/dist/index-BZ6YICSw.mjs +17936 -0
  42. package/dist/index-Bj470a3A.mjs +725 -0
  43. package/dist/index-C3iAUQqS.mjs +533 -0
  44. package/dist/index-CWkKuNLr.mjs +232 -0
  45. package/dist/index-Cj-UePAd.mjs +435 -0
  46. package/dist/index-D0dNM7_R.mjs +2892 -0
  47. package/dist/index-DNWfP1gi.mjs +2464 -0
  48. package/dist/index-DnFVXz0U.mjs +14162 -0
  49. package/dist/index-xncRG7-x.mjs +2713 -0
  50. package/dist/index.d.mts +907 -0
  51. package/dist/index.d.ts +907 -0
  52. package/dist/index.js +18670 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/index.mjs +18668 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/jsx-runtime-CJ4xYF4l.mjs +55 -0
  57. package/dist/remoteEntry.js +85 -0
  58. package/dist/virtualExposes-8FzWTdq3.mjs +42 -0
  59. package/package.json +95 -0
@@ -0,0 +1,907 @@
1
+ import * as _camstack_types from '@camstack/types';
2
+ import { RunnerLocalLoad, PipelineAssignment, IPipelineOrchestratorProvider, RunnerCameraConfig, BaseAddon, AddonInitResult, CameraMetrics, AgentLoadSummary, PipelineOrchestratorGlobalMetrics, DecoderAssignment, AgentPipelineSettings, AgentAddonConfig, CameraPipelineSettings, CameraStepOverridePatch, CameraPipelineConfig, CameraPipelineTemplate, ConfigUISchemaWithValues, CameraDetectionConfig } from '@camstack/types';
3
+ import { z } from 'zod';
4
+
5
+ /**
6
+ * Input to the load balancer: one entry per online runner node.
7
+ */
8
+ interface BalancerInput {
9
+ /** Per-node load snapshots fetched from each runner's `getLocalLoad()` cap. */
10
+ readonly nodes: readonly RunnerLocalLoad[];
11
+ /** Optional L1 affinity — if set and the node is online, it wins unconditionally. */
12
+ readonly preferredAgent?: string | null;
13
+ }
14
+ /**
15
+ * Output of the load balancer: the picked agent nodeId plus the reason the
16
+ * balancer chose it. `null` when no online runner is available.
17
+ */
18
+ interface BalancerDecision {
19
+ readonly agentNodeId: string;
20
+ readonly reason: 'manual' | 'capacity';
21
+ readonly score: number;
22
+ }
23
+ /**
24
+ * Compute the L2 capacity score for a runner node. Lower is better.
25
+ * The score is a weighted sum of the runner's active workload so the balancer
26
+ * prefers agents that are serving fewer cameras OR draining queues quickly.
27
+ *
28
+ * Rationale:
29
+ * - `attachedCameras * avgInferenceFps` approximates the total inference rate
30
+ * the agent is currently sustaining (not just how many cameras are assigned).
31
+ * - `queueDepthTotal` penalises agents that are falling behind the frame feed.
32
+ */
33
+ declare function computeCapacityScore(load: RunnerLocalLoad): number;
34
+ /**
35
+ * Run the two-level camera balancer.
36
+ *
37
+ * L1 (manual affinity): if `preferredAgent` names an online node, return it.
38
+ * L2 (capacity): compute capacity scores and pick the lowest.
39
+ *
40
+ * Returns `null` when no runners are online. The orchestrator decides how to
41
+ * react — typically by logging and deferring the assignment until a runner
42
+ * comes online.
43
+ */
44
+ declare function balance(input: BalancerInput): BalancerDecision | null;
45
+ interface DecoderBalancerInput {
46
+ readonly decoderNodes: readonly RunnerLocalLoad[];
47
+ readonly pipelineNodeId: string;
48
+ readonly preferredDecoderNode?: string | null;
49
+ }
50
+ interface DecoderBalancerDecision {
51
+ readonly decoderNodeId: string;
52
+ readonly reason: 'manual' | 'capacity' | 'co-located';
53
+ readonly score: number;
54
+ }
55
+ /**
56
+ * Choose decoder node. Priority: manual pin → co-located with pipeline → capacity.
57
+ */
58
+ declare function balanceDecoder(input: DecoderBalancerInput): DecoderBalancerDecision | null;
59
+
60
+ /**
61
+ * Custom-action catalog exposed through `api.addons.custom` (Task 9.1 PoC).
62
+ *
63
+ * The orchestrator's cap surface is the contract for all runtime traffic
64
+ * (assignCamera / unassignCamera / rebalance / getGlobalMetrics etc). This
65
+ * catalog is reserved for read-only diagnostics that are intentionally
66
+ * outside the cap — they expose internal state (balancer caches, enabledNodes
67
+ * set, active detection count) that is useful for admin tooling but does not
68
+ * belong on the capability contract.
69
+ */
70
+ declare const OrchestratorDiagnosticsSchema: z.ZodObject<{
71
+ localNodeId: z.ZodString;
72
+ knownRunnerNodes: z.ZodArray<z.ZodString>;
73
+ cachedAgentLoadNodeIds: z.ZodArray<z.ZodString>;
74
+ enabledNodes: z.ZodArray<z.ZodString>;
75
+ assignedDeviceCount: z.ZodNumber;
76
+ cameraConfigCount: z.ZodNumber;
77
+ activeDetectionCount: z.ZodNumber;
78
+ }, z.core.$strip>;
79
+ type OrchestratorDiagnostics = z.infer<typeof OrchestratorDiagnosticsSchema>;
80
+ declare const pipelineOrchestratorActions: {
81
+ dumpState: _camstack_types.CustomActionSpec<z.ZodVoid, z.ZodObject<{
82
+ localNodeId: z.ZodString;
83
+ knownRunnerNodes: z.ZodArray<z.ZodString>;
84
+ cachedAgentLoadNodeIds: z.ZodArray<z.ZodString>;
85
+ enabledNodes: z.ZodArray<z.ZodString>;
86
+ assignedDeviceCount: z.ZodNumber;
87
+ cameraConfigCount: z.ZodNumber;
88
+ activeDetectionCount: z.ZodNumber;
89
+ }, z.core.$strip>, "query", "protected", {
90
+ readonly kind: "system";
91
+ }>;
92
+ };
93
+ type PipelineOrchestratorActions = typeof pipelineOrchestratorActions;
94
+ /**
95
+ * Result of a successful dispatch. Returned by `dispatchCamera` so the
96
+ * caller (DetectionWiringService) can log / emit follow-up events with
97
+ * the same rationale the balancer used.
98
+ */
99
+ interface DispatchResult {
100
+ readonly success: true;
101
+ readonly agentNodeId: string;
102
+ readonly reason: PipelineAssignment["reason"];
103
+ }
104
+ /**
105
+ * Local-only provider surface resolved via the kernel capability registry.
106
+ * Extends the cap interface with the in-process dispatch entrypoints that
107
+ * can't cleanly round-trip through the tRPC serializer because they carry
108
+ * a full `PipelineConfig` payload attached to the `RunnerCameraConfig`.
109
+ */
110
+ interface IPipelineOrchestratorLocalProvider extends IPipelineOrchestratorProvider {
111
+ /**
112
+ * In-process dispatch entrypoint. Caches the config, runs the load
113
+ * balancer, dispatches the attach call to the chosen runner.
114
+ */
115
+ dispatchCamera(runnerConfig: RunnerCameraConfig): Promise<DispatchResult>;
116
+ /** In-process release entrypoint — detach + clear cached state. */
117
+ releaseCamera(input: {
118
+ deviceId: number;
119
+ }): Promise<{
120
+ success: true;
121
+ }>;
122
+ }
123
+ declare class PipelineOrchestratorAddon extends BaseAddon implements IPipelineOrchestratorLocalProvider {
124
+ /** This node's Moleculer nodeId (from this.ctx.kernel.localNodeId). */
125
+ private localNodeId;
126
+ /** Set of nodeIds known to be running pipeline-runner; maintained via AgentOnline/AgentOffline events. */
127
+ private readonly knownRunnerNodes;
128
+ /** EventBus unsubscribes for agent online/offline events. */
129
+ private unsubAgentOnline;
130
+ private unsubAgentOffline;
131
+ /**
132
+ * Readiness tracker — drives `detection-pipeline` seed on every
133
+ * provider life cycle. Prefers the kernel-shared singleton via
134
+ * `ctx.kernel.readinessRegistry`; falls back to a local instance for
135
+ * test contexts without a kernel.
136
+ */
137
+ private readinessRegistry;
138
+ /** True when this addon owns the registry (local fallback) and should `close()` it on shutdown. */
139
+ private ownsReadinessRegistry;
140
+ /** Per-node readiness subscription unsubs — torn down on AgentOffline / shutdown. */
141
+ private readonly readinessSubsByNode;
142
+ /** Per-node idempotency guard — re-seed only when the producer's epoch advances. */
143
+ private readonly lastSeenReadinessEpochByNode;
144
+ /** Most recent RunnerCameraConfig per device — used by rebalance + re-dispatch. */
145
+ private readonly cameraConfigs;
146
+ /**
147
+ * Per-camera zones CRUD provider. Constructed lazily in `onInitialize`
148
+ * because it captures `this.ctx` for settings + api access; cleared
149
+ * to null in `onShutdown` so a soft-restart is observable.
150
+ */
151
+ private zonesProvider;
152
+ /**
153
+ * Per-camera zone-rules CRUD provider — bulk-replace per stage,
154
+ * mirrors to dev.state slices `motion-zone-rules` /
155
+ * `detection-zone-rules` so consumer addons subscribe per stage.
156
+ */
157
+ private zoneRulesProvider;
158
+ /** In-memory assignment map — mirrored into events + per-device settings for the pin. */
159
+ private readonly assignments;
160
+ /** Per-device audio node assignment — nodeId of the audio-analyzer handling this device's chunks. */
161
+ private readonly audioNodeByDevice;
162
+ /** Assignments with metadata — replaces plain audioNodeByDevice values as the source of truth. */
163
+ private readonly audioAssignments;
164
+ /** Nodes known to have a ready audio-analyzer. */
165
+ private readonly readyAudioNodes;
166
+ /** Last seen readiness epoch per node for audio-analyzer. */
167
+ private readonly lastSeenAudioEpochByNode;
168
+ /** Last observed agent-load snapshot per runner, populated during each dispatch. */
169
+ private cachedAgentLoad;
170
+ /**
171
+ * Cached `PipelineSchema` per engine key (`runtime/backend/format`).
172
+ * Populated on demand by {@link getCatalogForEngine} via
173
+ * `pipelineExecutor.getSchema({nodeId})` and invalidated on every
174
+ * `$node.connected` / `$node.disconnected` — node changes can mean
175
+ * model downloads arrived, addons enabled, etc. TTL is a soft
176
+ * guard only (invalidation is the main signal).
177
+ */
178
+ private catalogCache;
179
+ private static readonly CATALOG_CACHE_TTL_MS;
180
+ /** Runtime failover policy, driven by `getConfigSchema` fields + `onConfigChange`. */
181
+ private failoverPolicy;
182
+ /**
183
+ * Hub-wide allow-list of node ids eligible to run the detection
184
+ * pipeline. Driven by the `enabledNodes` multiselect in the addon
185
+ * schema. Hydrated from the schema default (`['hub']`) on first
186
+ * boot; every other node must be explicitly added by the operator.
187
+ * The list is a strict whitelist — disabled nodes stay connected
188
+ * for other capabilities (metrics, logs, cluster-wide events) but
189
+ * never receive camera assignments.
190
+ */
191
+ private enabledNodes;
192
+ /**
193
+ * Hub-wide allow-list of node ids eligible to run decoder sessions.
194
+ * Driven by the `enabledDecoderNodes` multiselect in the addon schema.
195
+ * Defaults to `['hub']` — the hub always has a decoder available.
196
+ */
197
+ private enabledDecoderNodes;
198
+ /**
199
+ * Hub-wide allow-list of node ids eligible to run audio-analyzer sessions.
200
+ * Driven by the `enabledAudioNodes` multiselect in the addon schema.
201
+ * Defaults to `['hub']`.
202
+ */
203
+ private enabledAudioNodes;
204
+ /**
205
+ * Full global settings snapshot kept in memory so synchronous cap methods
206
+ * (e.g. `getGlobalOrchestrationSettings`) can resolve without awaiting the
207
+ * backing store. Refreshed in `initialize` and `onConfigChange`.
208
+ */
209
+ private globalSettings;
210
+ /** Per-device active detection config — set on startDetection, cleared on stopDetection. */
211
+ private readonly activeDetections;
212
+ /** Per-device audio broker unsubscribe callbacks. */
213
+ private readonly audioSubscriptions;
214
+ /** Per-device pending lazy-audio teardown timer (closes audio sub
215
+ * after motion cooldown when audioMode === 'on-motion'). */
216
+ private readonly lazyAudioTeardownTimers;
217
+ /** Event bus subscription lifetimes. */
218
+ private unsubDeviceRegistered;
219
+ /** Pending CameraStreams.profileSlotsChanged debounce timers, cancelled on shutdown. */
220
+ private profileSlotTimers;
221
+ private unsubDeviceUnregistered;
222
+ private unsubSettingsChanged;
223
+ private unsubInferenceResult;
224
+ private unsubProviderDetection;
225
+ private unsubCameraUpdated;
226
+ private unsubBindingsChanged;
227
+ private unsubCameraMetrics;
228
+ private unsubLazyAudioMotion;
229
+ /** Per-camera last-seen actualFps for load management enforcement. */
230
+ private readonly cameraFpsMap;
231
+ /**
232
+ * Load-shed state per camera:
233
+ * - `lowSinceTs`: timestamp of the first metrics snapshot that
234
+ * dipped below `fpsMinThreshold`, or `null` when fps is healthy.
235
+ * Camera pauses when `now - lowSinceTs >= fpsLowWindowMs`.
236
+ * Resets to `null` the moment fps recovers above the threshold,
237
+ * so transient dips (GC, model swap) don't accumulate.
238
+ * - `pausedAt`: timestamp when camera was paused (null = not paused)
239
+ * - `resumeAttempts`: number of auto-resume attempts (drives backoff)
240
+ */
241
+ private readonly loadShedState;
242
+ /** Timer for the auto-resume sweep. */
243
+ private loadShedResumeTimer;
244
+ private initTimestamp;
245
+ constructor();
246
+ protected onInitialize(): Promise<AddonInitResult<PipelineOrchestratorActions>>;
247
+ /**
248
+ * One-shot migration from the legacy per-device flags
249
+ * (`audioEnabled:false` / `pipelineEnabled:false` in the orchestrator's
250
+ * device-store, `disableInference:true` in the cameraSettings map) to
251
+ * the new binding system. For each match we call
252
+ * `device-manager.setWrapperActive(false)` so the equivalent state
253
+ * sticks after the flags are gone. Guarded by an addon-store flag so
254
+ * it runs at most once per cluster install.
255
+ *
256
+ * Runs in the background after onInitialize — the `ctx.api` usually
257
+ * lights up after a short delay once the device-manager + capability
258
+ * registry finish wiring, so we poll briefly and give up with a warn
259
+ * when the window closes. Failure re-arms on next boot since we only
260
+ * persist the flag after a successful pass.
261
+ */
262
+ private migrateLegacyFlagsToBindings;
263
+ /**
264
+ * Build the diagnostic snapshot exposed via the `dumpState` custom action.
265
+ * Pure read — never mutates state. Extracted from `handleCustomAction` so
266
+ * tests can assert the snapshot shape directly without going through the
267
+ * dispatcher.
268
+ */
269
+ private dumpDiagnostics;
270
+ protected onShutdown(): Promise<void>;
271
+ dispatchCamera(runnerConfig: RunnerCameraConfig): Promise<DispatchResult>;
272
+ releaseCamera(input: {
273
+ deviceId: number;
274
+ }): Promise<{
275
+ success: true;
276
+ }>;
277
+ assignPipeline(input: {
278
+ deviceId: number;
279
+ agentNodeId: string;
280
+ }): Promise<{
281
+ success: true;
282
+ }>;
283
+ unassignPipeline(input: {
284
+ deviceId: number;
285
+ }): Promise<{
286
+ success: true;
287
+ }>;
288
+ rebalance(): Promise<{
289
+ migrated: number;
290
+ }>;
291
+ getPipelineAssignments(): readonly PipelineAssignment[];
292
+ getPipelineAssignment(input: {
293
+ deviceId: number;
294
+ }): PipelineAssignment | null;
295
+ /**
296
+ * Per-camera live metrics. Centralised entry point for the UI: the
297
+ * orchestrator owns the device→agent assignment map, so it knows
298
+ * which runner holds the camera and can route the cap call to the
299
+ * exact node. Calling `pipelineRunner.getCameraMetrics` directly
300
+ * from the UI breaks in multi-agent setups because the hub-side cap
301
+ * router resolves the runner singleton against the registry's first-
302
+ * registered provider, which races between agents and may not be the
303
+ * one the orchestrator actually dispatched the camera to.
304
+ */
305
+ getCameraMetrics(input: {
306
+ deviceId: number;
307
+ }): Promise<CameraMetrics | null>;
308
+ /**
309
+ * Live snapshot of every runner's load. Triggers a fresh
310
+ * `collectAgentLoad()` on each call so the UI's "Live load" panel
311
+ * always reflects reality — the in-memory cache is only populated
312
+ * during dispatch / rebalance, so without a refresh here the panel
313
+ * would stay empty until the next dispatch event (problematic after
314
+ * a server restart that rehydrates assignments from disk without
315
+ * re-dispatching).
316
+ */
317
+ getAgentLoad(): Promise<readonly AgentLoadSummary[]>;
318
+ getGlobalMetrics(): Promise<PipelineOrchestratorGlobalMetrics>;
319
+ /**
320
+ * Call `attachCamera` on the target runner via this.ctx.api. The generated cap
321
+ * router extracts `nodeId` from the input and routes to local or remote
322
+ * runner transparently — no broker reference needed.
323
+ */
324
+ private attachOn;
325
+ private detachOn;
326
+ private localRunnerNodeId;
327
+ /**
328
+ * Enumerate every runner-capable node currently known (populated via
329
+ * AgentOnline/AgentOffline events). Used to populate the `enabledNodes`
330
+ * multiselect options at schema-build time.
331
+ *
332
+ * Forked child nodeIds (`hub/classifier`, `agent-1/motion-wasm`) are
333
+ * filtered out — they name isolated-process services, never dispatchable
334
+ * pipeline runners. The on-write guards in AgentOnline + applyRuntimeSettings
335
+ * already exclude them, but we filter here too so any legacy seed that
336
+ * slipped in before those guards existed doesn't leak into the UI picker.
337
+ */
338
+ private collectRunnerNodeIds;
339
+ /**
340
+ * Enumerate every node currently known as decoder-capable. Reuses
341
+ * `knownRunnerNodes` since any runner node can potentially host a
342
+ * decoder session. `hub` is always included. Forked child nodeIds are
343
+ * filtered out for the same reason as `collectRunnerNodeIds`.
344
+ */
345
+ private collectDecoderNodeIds;
346
+ /**
347
+ * Enumerate every node currently known as audio-analyzer-capable.
348
+ * Uses `knownRunnerNodes` as the universe and filters out child
349
+ * nodeIds (containing '/') the same way the other collectors do.
350
+ */
351
+ private collectAudioNodeIds;
352
+ /**
353
+ * Resolve which node should run the decoder for a given device.
354
+ * Reads the per-device `decoderNodeId` pin first (manual override),
355
+ * then falls back to the `balanceDecoder` algorithm which prefers
356
+ * co-location with the pipeline node, then capacity.
357
+ */
358
+ private resolveDecoderNode;
359
+ /**
360
+ * Return `true` when `nodeId` is in the `enabledNodes` whitelist.
361
+ * A strict list — an empty whitelist means "no node allowed", not
362
+ * "all nodes allowed". The schema default (`['hub']`) guarantees
363
+ * fresh installs always have at least the hub enabled.
364
+ */
365
+ private isNodeEnabled;
366
+ /**
367
+ * Query every online runner in the cluster for its current load, plus the
368
+ * local runner if co-located. Refreshes the `cachedAgentLoad` snapshot so
369
+ * `getAgentLoad()` / `getGlobalMetrics()` can return fresh data.
370
+ *
371
+ * `{onlyEnabled: true}` applies the `enabledNodes` whitelist filter —
372
+ * used when the caller is the dispatcher / balancer and must only
373
+ * see runners eligible for new camera assignments. The default
374
+ * (unfiltered) collects every known runner, which is what the UI
375
+ * wants: operators need visibility into every connected node, even
376
+ * ones currently disabled from the dispatch pool.
377
+ */
378
+ private collectAgentLoad;
379
+ private refreshAgentLoadCache;
380
+ private readPreferredAgent;
381
+ private readAudioNodePin;
382
+ private collectAudioNodeLoad;
383
+ private recordAudioAssignment;
384
+ private dispatchAudio;
385
+ private recordAssignment;
386
+ private emitUnassigned;
387
+ /**
388
+ * Failover handler. When a runner node goes offline, the behaviour is
389
+ * driven by the persisted failover policy (`failoverPolicy`):
390
+ * - `onDisconnect: 'migrate'` → move non-pinned cameras to another runner
391
+ * - `onDisconnect: 'leave-unassigned'` → drop all assignments for the node
392
+ * - `pinnedOnDisconnect: 'leave-pinned'` → pinned cameras stay unassigned
393
+ * - `pinnedOnDisconnect: 'unpin-and-migrate'` → clear the pin + migrate
394
+ */
395
+ private handleNodeDisconnect;
396
+ /**
397
+ * Reconnect handler. When a runner node comes back online, the behaviour
398
+ * is driven by the `onReconnect` policy:
399
+ * - `restore` → re-attach cameras previously assigned to that node
400
+ * (only the ones still known in `cameraConfigs`)
401
+ * - `rebalance` → run a full `rebalance()` pass so non-pinned cameras
402
+ * can migrate back onto the newly-available runner
403
+ */
404
+ private handleNodeConnect;
405
+ /**
406
+ * Subscribe to `detection-pipeline` readiness transitions for `nodeId`.
407
+ * Idempotent: re-subscribing the same node is a no-op. The handler
408
+ * skips same-epoch replays so repeated `ready` emits don't force
409
+ * redundant reseeds.
410
+ */
411
+ private subscribeToDetectionPipelineReadiness;
412
+ private unsubscribeDetectionPipelineReadiness;
413
+ /**
414
+ * Subscribe to `audio-analyzer` readiness transitions for `nodeId`.
415
+ * Idempotent — re-subscribing the same node is a no-op (uses its own
416
+ * key space separate from detection readiness subscriptions).
417
+ */
418
+ private subscribeToAudioAnalyzerReadiness;
419
+ private handleAudioAnalyzerReadiness;
420
+ /**
421
+ * Act on a `detection-pipeline` transition: on `ready` with a new
422
+ * epoch (= producer restart or first-time ready), invalidate the
423
+ * catalog cache, re-seed that node's `agentAddonDefaults`, and
424
+ * redispatch every camera currently assigned to it. Everything else
425
+ * (down, starting, same-epoch keepalive) is skipped.
426
+ */
427
+ private handleDetectionPipelineReadiness;
428
+ /**
429
+ * Walk every cached RunnerCameraConfig and re-attach any whose stored
430
+ * `steps` are empty AND whose latest resolve produces a non-empty
431
+ * pipeline. Catches the "camera dispatched mid-readiness" race where
432
+ * the initial attach landed BEFORE the catalog was cached but the
433
+ * subsequent readiness sweep already finished iterating.
434
+ */
435
+ private reattachStaleEmpties;
436
+ getCapabilityBindings(input: {
437
+ nodeId: string;
438
+ }): Promise<Readonly<Record<string, string>>>;
439
+ setCapabilityBinding(input: {
440
+ nodeId: string;
441
+ capName: string;
442
+ addonId: string;
443
+ }): Promise<{
444
+ success: true;
445
+ }>;
446
+ assignDecoder(input: {
447
+ readonly deviceId: number;
448
+ readonly nodeId: string;
449
+ }): Promise<void>;
450
+ unassignDecoder(input: {
451
+ readonly deviceId: number;
452
+ }): Promise<void>;
453
+ /**
454
+ * Per-camera decoder node accessor (Phase 8). Exposes the existing
455
+ * private `resolveDecoderNode` logic as a cap method so
456
+ * `stream-broker.createBroker` can filter its decoder provider
457
+ * selection deterministically. Caller hint: pass `pipelineNodeId` to
458
+ * bias the balancer toward co-location with the runner; omitted → the
459
+ * last-known assignment, else 'hub'.
460
+ */
461
+ getDecoderAssignment(input: {
462
+ readonly deviceId: number;
463
+ readonly pipelineNodeId?: string;
464
+ }): Promise<DecoderAssignment>;
465
+ getDecoderAssignments(): Promise<readonly DecoderAssignment[]>;
466
+ assignAudio(input: {
467
+ readonly deviceId: number;
468
+ readonly nodeId: string;
469
+ }): Promise<{
470
+ success: true;
471
+ }>;
472
+ unassignAudio(input: {
473
+ readonly deviceId: number;
474
+ }): Promise<{
475
+ success: true;
476
+ }>;
477
+ getAudioAssignment(input: {
478
+ readonly deviceId: number;
479
+ }): Promise<{
480
+ nodeId: string;
481
+ pinned: boolean;
482
+ assignedAt: number;
483
+ } | null>;
484
+ getAudioNodeLoad(): Promise<Array<{
485
+ nodeId: string;
486
+ deviceCount: number;
487
+ }>>;
488
+ getAudioAssignments(): Promise<Array<{
489
+ deviceId: number;
490
+ nodeId: string;
491
+ pinned: boolean;
492
+ assignedAt: number;
493
+ }>>;
494
+ getAgentSettings(input: {
495
+ readonly agentNodeId: string;
496
+ }): Promise<AgentPipelineSettings | null>;
497
+ listAgentSettings(): Promise<readonly {
498
+ readonly nodeId: string;
499
+ readonly settings: AgentPipelineSettings;
500
+ }[]>;
501
+ setAgentAddonDefaults(input: {
502
+ readonly agentNodeId: string;
503
+ readonly defaults: Record<string, AgentAddonConfig>;
504
+ }): Promise<{
505
+ success: true;
506
+ }>;
507
+ removeAgentSettings(input: {
508
+ readonly agentNodeId: string;
509
+ }): Promise<{
510
+ success: boolean;
511
+ removed: boolean;
512
+ }>;
513
+ getCameraSettings(input: {
514
+ readonly deviceId: number;
515
+ }): Promise<CameraPipelineSettings | null>;
516
+ setCameraStepToggle(input: {
517
+ readonly deviceId: number;
518
+ readonly addonId: string;
519
+ readonly enabled: boolean | null;
520
+ }): Promise<{
521
+ success: true;
522
+ }>;
523
+ getCameraStepOverrides(input: {
524
+ readonly deviceId: number;
525
+ }): Promise<Record<string, Record<string, CameraStepOverridePatch>> | null>;
526
+ setCameraStepOverride(input: {
527
+ readonly deviceId: number;
528
+ readonly agentNodeId: string;
529
+ readonly addonId: string;
530
+ readonly patch: CameraStepOverridePatch | null;
531
+ }): Promise<{
532
+ success: true;
533
+ }>;
534
+ setCameraPipelineForAgent(input: {
535
+ readonly deviceId: number;
536
+ readonly agentNodeId: string;
537
+ readonly pipeline: {
538
+ steps: CameraPipelineConfig["steps"];
539
+ audio: {
540
+ modelId: string;
541
+ enabled: boolean;
542
+ } | null;
543
+ } | null;
544
+ }): Promise<{
545
+ success: true;
546
+ }>;
547
+ resolvePipeline(input: {
548
+ readonly deviceId: number;
549
+ readonly agentNodeId?: string;
550
+ }): Promise<CameraPipelineConfig>;
551
+ /** Emit `pipeline.camera-updated` carrying the freshly-resolved config. */
552
+ private emitResolvedCameraUpdated;
553
+ /** Re-dispatch every camera currently assigned to `nodeId` — used when agent defaults change. */
554
+ private redispatchCamerasOnAgent;
555
+ listTemplates(): Promise<readonly CameraPipelineTemplate[]>;
556
+ saveTemplate(input: {
557
+ readonly name: string;
558
+ readonly description?: string;
559
+ readonly config: CameraPipelineConfig;
560
+ }): Promise<CameraPipelineTemplate>;
561
+ updateTemplate(input: {
562
+ readonly id: string;
563
+ readonly name?: string;
564
+ readonly description?: string;
565
+ readonly config?: CameraPipelineConfig;
566
+ }): Promise<CameraPipelineTemplate>;
567
+ deleteTemplate(input: {
568
+ readonly id: string;
569
+ }): Promise<{
570
+ success: true;
571
+ }>;
572
+ /** Read the templates map from the addon store. */
573
+ private readTemplatesMap;
574
+ /** Emit the `pipeline.camera-updated` event so the internal subscriber hot-reloads the runner. */
575
+ private emitCameraUpdated;
576
+ /** Read the `agentSettings` map (keyed on `nodeId`) from the addon store. */
577
+ private readAgentSettingsMap;
578
+ /** Overwrite one agent's settings. Does NOT touch other agents' entries. */
579
+ private writeAgentSettings;
580
+ /** Read the `cameraSettings` map (keyed on `deviceId` as string) from the addon store. */
581
+ private readCameraSettingsMap;
582
+ /** Overwrite one camera's settings. */
583
+ private writeCameraSettings;
584
+ /**
585
+ * Fetch `pipelineExecutor.getSchema({nodeId})` through the standard cap
586
+ * router, cached per `(nodeId, engineKey)` with a short TTL. Returns
587
+ * `null` when the executor can't be reached (no runner yet attached)
588
+ * — callers fall back to an empty pipeline in that case.
589
+ */
590
+ private getCatalogForAgent;
591
+ /**
592
+ * Ensure `agentSettings[nodeId]` exists and is up-to-date with the
593
+ * current catalog. Triggered on orchestrator init (for the hub) and on
594
+ * every `$node.connected` for remote runners.
595
+ *
596
+ * Behaviour:
597
+ * - Fetch catalog for the node.
598
+ * - Call `seedAgentAddonDefaults(current.addonDefaults, engine, catalog)` — preserves
599
+ * operator customisations, adds missing addons, drops orphans.
600
+ * - Persist if anything changed.
601
+ *
602
+ * Idempotent: a second call with the same catalog is a no-op.
603
+ */
604
+ private seedAgentSettingsFromCatalog;
605
+ /**
606
+ * Fetch the engine triple (`runtime/backend/device`) from the
607
+ * `detection-pipeline` addon's global settings on the target node.
608
+ * This is the authoritative engine source since phase 2f. Returns
609
+ * `null` when the addon hasn't responded yet or doesn't expose the
610
+ * fields — callers fall back to `catalog.selectedEngine`.
611
+ */
612
+ private readDetectionPipelineEngine;
613
+ /** Sleep helper used inside readiness retry loops. */
614
+ private static sleep;
615
+ /**
616
+ * Resolve the pipeline for a device. NEVER returns an empty/fallback
617
+ * config: if the catalog or agent settings aren't available yet, this
618
+ * blocks (with backoff) until the detection-pipeline cap is registered
619
+ * and a non-null catalog comes back. Cameras must never be dispatched
620
+ * with `engine=node/cpu, steps=[]` because the runner then attaches
621
+ * the camera with no steps and silently never recovers.
622
+ *
623
+ * Two legitimate exceptions to "never empty":
624
+ * 1. The device's `detection-pipeline` binding is INACTIVE — the
625
+ * operator explicitly turned inference off for this camera.
626
+ * 2. `cam.pipelineByAgent[agentNodeId]` carries a saved wholesale
627
+ * override with empty steps — also operator intent.
628
+ *
629
+ * Everything else (boot ordering, catalog cold-start, transient cap
630
+ * registration delay) loops with backoff until the catalog responds.
631
+ */
632
+ private resolvePipelineForDevice;
633
+ /**
634
+ * Block until `pipeline-executor` is ready on `nodeId` AND
635
+ * `getCatalogForAgent` returns a non-null catalog AND `agentSettings`
636
+ * for that node has populated `addonDefaults`.
637
+ *
638
+ * `acquireCapability` defaults to infinite wait now — a single call
639
+ * carries the readiness gate. The thin retry below only handles
640
+ * the secondary case where the cap is "ready" but a derived call
641
+ * (getSchema) still returns null due to Moleculer service-registry
642
+ * propagation lag.
643
+ */
644
+ private waitForAgentAndCatalog;
645
+ /**
646
+ * Block until a real engine choice is available. `acquireCapability`
647
+ * defaults to infinite wait; the post-acquire loop only handles the
648
+ * propagation gap where the cap is `ready` but the derived calls
649
+ * still return null for a tick.
650
+ */
651
+ private waitForEngine;
652
+ /**
653
+ * Structural runtime check for `CameraPipelineConfig`. Persisted JSON
654
+ * may be stale or partial across upgrades, so we gate reads on shape
655
+ * rather than trusting the raw object. False = silently ignore that
656
+ * entry and fall back to defaults — the orchestrator's job is to
657
+ * never crash on corrupt store state.
658
+ */
659
+ private isCameraPipelineConfig;
660
+ private isPipelineTemplate;
661
+ /**
662
+ * Generate a short template id. Collision risk at N≤1000 templates is
663
+ * effectively zero (12 hex chars = 48 bits of entropy). Not a UUID
664
+ * because the persisted shape is a plain map keyed by id and shorter
665
+ * keys keep the JSON compact.
666
+ */
667
+ private generateTemplateId;
668
+ /** Build the addon-level schema (cluster-wide tunables: balancer + failover). */
669
+ protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
670
+ /** Build the device-level schema (per-camera detection wiring). */
671
+ protected deviceSettingsSchema(): _camstack_types.ConfigUISchema;
672
+ updateGlobalSettings(patch: Record<string, unknown>): Promise<void>;
673
+ private writeDeviceOrchestratorSettings;
674
+ getDeviceSettingsContribution(input: {
675
+ deviceId: number;
676
+ }): Promise<ConfigUISchemaWithValues | null>;
677
+ getDeviceLiveContribution(input: {
678
+ deviceId: number;
679
+ }): Promise<ConfigUISchemaWithValues | null>;
680
+ /**
681
+ * Best-effort camera-type check used to gate the settings/live
682
+ * contributions on non-camera devices (Lights, Switches, Sensors,
683
+ * Buttons). Returns `true` on lookup failure so a transient
684
+ * device-manager hiccup never silently hides legitimate camera
685
+ * sections.
686
+ */
687
+ private isCameraDevice;
688
+ applyDeviceSettingsPatch(input: {
689
+ deviceId: number;
690
+ patch: Record<string, unknown>;
691
+ }): Promise<{
692
+ success: true;
693
+ }>;
694
+ /**
695
+ * Peel the pipeline-config keys out of an inbound patch. The remaining
696
+ * patch is forwarded to the existing device-level store writer. JSON
697
+ * textarea values are parsed here — `FormBuilder` serialises JSON
698
+ * editors as strings on submit, so the cap boundary has to reverse it
699
+ * before the content lands in the typed pipelines map.
700
+ */
701
+ private splitPipelinePatch;
702
+ /**
703
+ * Apply a `cameraPipeline` patch emitted by the `pipeline-editor`
704
+ * ConfigField. Routes the full `CameraPipelineConfig` into the
705
+ * 3-level `cameraSettings[deviceId].pipelineByAgent[agent]` store.
706
+ * The target agent is the camera's current assignment (or the local
707
+ * hub when unassigned). Emits `pipeline.camera-updated` so the
708
+ * internal subscriber hot-reloads the runner.
709
+ */
710
+ private applyPipelinePatch;
711
+ /** Tolerant JSON parser used to round-trip textarea-isJson fields. */
712
+ private tryParseJson;
713
+ /**
714
+ * Apply the subset of global settings that affect runtime behaviour
715
+ * (balancer thresholds, broker timeout, failover policy) into the
716
+ * in-memory state so subsequent `balance()` / `handleNodeDisconnect`
717
+ * calls use the new values immediately.
718
+ */
719
+ private applyRuntimeSettings;
720
+ private get api();
721
+ /**
722
+ * Resolve this addon's own per-device detection settings — combines the
723
+ * raw device store with the device schema defaults via `hydrateSchema()`,
724
+ * then narrows to a typed DTO. Replaces the old call to
725
+ * `this.ctx.settings!.getDevice` which used the deleted multi-level resolver
726
+ * (the new model has no overlap between addon-level and device-level
727
+ * keys, so there's no merge across levels — only schema defaults are
728
+ * applied to fill in keys missing from the device store).
729
+ */
730
+ /**
731
+ * True when `capName` is bound (`kind:'wrapped'`) for `deviceId` via
732
+ * `device-manager.getBindings`. Single source of truth for the
733
+ * "binding-gated cap" checks sprinkled around this addon (detection
734
+ * pipeline, audio analysis, …). Failures log-and-return `true` — the
735
+ * safe default is "cap on" unless the operator has explicitly toggled
736
+ * the wrapper off via `device-manager.setWrapperActive`.
737
+ */
738
+ private isCapActiveForDevice;
739
+ private isDetectionPipelineActive;
740
+ private isAudioAnalysisActive;
741
+ private isMotionDetectionActive;
742
+ /**
743
+ * Resolve the device's feature array. Used by the resolver to pick
744
+ * the right `DeviceProfile` when computing per-camera scheduling
745
+ * defaults. Reads via the `device-manager.getDevice` cap because the
746
+ * orchestrator may run on a different node from the live IDevice;
747
+ * the live registry isn't available cross-process.
748
+ */
749
+ private lookupDeviceFeatures;
750
+ /**
751
+ * True when the device has a native `motion` or `native-object-detection`
752
+ * cap registered by its driver — i.e. the camera does motion detection
753
+ * itself (Reolink Baichuan onboard PIR, ONVIF event-driven, etc.).
754
+ * Drives the dynamic `motionSources` default in
755
+ * `resolveDeviceDetectionSettings`: when the operator hasn't pinned a
756
+ * value, `['onboard']` is preferred over `['analyzer']` so we don't
757
+ * burn CPU running a redundant motion pipeline.
758
+ *
759
+ * Detection is via `device-manager.getBindings`: any binding entry of
760
+ * kind `'native'` whose capName is `motion` or
761
+ * `native-object-detection` qualifies. Failures default to `false` so
762
+ * we err toward the analyzer (never lose detection coverage when the
763
+ * binding lookup hiccups).
764
+ */
765
+ private deviceHasOnboardMotionCap;
766
+ private resolveDeviceDetectionSettings;
767
+ /**
768
+ * True when the device has registered a native `motion` or
769
+ * `native-object-detection` cap provider — i.e. the camera's driver
770
+ * addon does motion detection on the device itself. Drives the
771
+ * dynamic motionSource default in `resolveDeviceDetectionSettings`.
772
+ * Each lookup is isolated: a throw for one cap (legitimate "no
773
+ * provider registered" from the registry) must not mask the presence
774
+ * of the other cap.
775
+ */
776
+ /**
777
+ * Resolve the requested profile against the set of currently-assigned
778
+ * profile slots. Returns the profile id itself if the slot is live;
779
+ * otherwise walks the quality chain so a device missing the requested
780
+ * profile still gets a detection subscription on the closest assigned
781
+ * slot. Returns `null` only when nothing is assigned.
782
+ */
783
+ private resolveAssignedProfile;
784
+ /**
785
+ * Fetch the current profile-slot map for a device from the system
786
+ * stream-broker cap (`listAllProfileSlots`). Returns a map of
787
+ * `CamProfile → sourceCamStreamId` so callers can both check which
788
+ * profiles are live AND resolve a profile to the cam-stream-keyed
789
+ * brokerId (`${deviceId}/${camStreamId}`) used by the broker.
790
+ */
791
+ private fetchAssignedProfiles;
792
+ /**
793
+ * Build a `CameraDetectionConfig` from the resolved per-device
794
+ * orchestrator settings + the device's currently-assigned profile
795
+ * slots. Returns `null` when the device has no assigned slot or when
796
+ * settings cannot be resolved.
797
+ *
798
+ * `motionStreamId` / `detectionStreamId` hold a `camStreamId` value
799
+ * (e.g. `'native:main'`, `'rtsp:sub'`). The pipeline-runner
800
+ * concatenates `${deviceId}/${motionStreamId}` to build the broker
801
+ * id, matching the cam-stream-keyed brokers published by the
802
+ * stream-broker. The profile chosen by `motionStreamProfile` /
803
+ * `detectionStreamProfile` is resolved to the cam-stream backing it
804
+ * via the slot map fetched from `listAllProfileSlots`.
805
+ */
806
+ buildDetectionConfig(deviceId: number): Promise<CameraDetectionConfig | null>;
807
+ /**
808
+ * Start detection for a camera: dispatch to runner via the orchestrator
809
+ * local path and subscribe to the audio broker if audio is enabled.
810
+ */
811
+ startDetection(deviceId: number, config: CameraDetectionConfig): Promise<void>;
812
+ /** Stop detection for a camera: release via releaseCamera + audio cleanup. */
813
+ stopDetection(deviceId: number): Promise<void>;
814
+ /** Stop detection and purge all persisted config for a removed device. */
815
+ private handleDeviceUnregistered;
816
+ /**
817
+ * Handle a profile-slot change: build a fresh detection config from
818
+ * per-device settings and start detection if motion is enabled.
819
+ * Fires on `camera-streams.onProfileSlotsChanged` — any publish,
820
+ * retract, or assignment mutation in the stream-broker lands here.
821
+ */
822
+ private handleDeviceRegistered;
823
+ /**
824
+ * Field-by-field comparison of two `CameraDetectionConfig` records.
825
+ * `motionSources` is an unordered set; everything else is a primitive.
826
+ */
827
+ private static detectionConfigEquals;
828
+ /**
829
+ * Handle an orchestration settings update: rebuild the config and restart
830
+ * detection with the new values.
831
+ */
832
+ private handleSettingsChanged;
833
+ /**
834
+ * Hot-reload the runner currently hosting `deviceId` with a fresh
835
+ * pipeline config. Called from the `PipelineCameraUpdated` subscriber —
836
+ * any mutation to `pipelines[deviceId]` flows through this path so the
837
+ * operator's edit reaches the next frame without a rebalance or a full
838
+ * detection restart.
839
+ *
840
+ * No-ops (with a debug log) when the camera has no cached
841
+ * RunnerCameraConfig — means the camera either isn't registered yet or
842
+ * isn't assigned to a runner. The next DeviceStreamsRegistered cycle
843
+ * will read the fresh config from the store and dispatch normally.
844
+ */
845
+ private handlePipelineCameraUpdated;
846
+ /**
847
+ * Re-emit `PipelineInferenceResult` as `DetectionResult` when the
848
+ * frame carries detections. Kept deliberately thin: refinement
849
+ * (tracking, zone/state analysis, per-kind event persistence) is now
850
+ * owned end-to-end by `addon-pipeline-analytics`, which subscribes to
851
+ * `PipelineInferenceResult` directly. Consumers that want
852
+ * enriched-event-driven notifications subscribe to
853
+ * `pipeline-analytics.detection-event` + drive `notificationOutput`
854
+ * themselves.
855
+ */
856
+ /**
857
+ * Load management enforcement — called on every camera metrics snapshot.
858
+ *
859
+ * Time-based window: a camera must stay below `fpsMinThreshold` for
860
+ * at least `fpsLowWindowMs` (default 60s) before it is paused. The
861
+ * window resets the moment fps recovers above threshold, so a
862
+ * transient dip (GC, model swap, brief stall) doesn't accumulate
863
+ * across recoveries.
864
+ */
865
+ private static readonly DEFAULT_FPS_LOW_WINDOW_MS;
866
+ private enforceLoadManagement;
867
+ /** Base cooldown before first auto-resume attempt. */
868
+ private static readonly LOAD_SHED_BASE_COOLDOWN_MS;
869
+ /** Maximum backoff cap. */
870
+ private static readonly LOAD_SHED_MAX_COOLDOWN_MS;
871
+ /** How often the sweep runs. */
872
+ private static readonly LOAD_SHED_SWEEP_INTERVAL_MS;
873
+ private ensureLoadShedResumeTimer;
874
+ private loadShedResumeSweep;
875
+ /**
876
+ * Re-dispatch a single camera through the normal assignment pipeline.
877
+ * Used by auto-resume and settings-change recovery.
878
+ */
879
+ private redispatchSingleCamera;
880
+ private handleInferenceResult;
881
+ /**
882
+ * Lazy audio gate handler. Fires on every `MotionOnMotionChanged`
883
+ * event for a device whose `audioMode === 'on-motion'`.
884
+ *
885
+ * - `detected:true` → cancel any pending teardown, open audio sub
886
+ * if not already open. Idempotent: re-fires of the same `true`
887
+ * while subscribed are no-ops aside from cancelling the timer.
888
+ * - `detected:false` → schedule teardown after the device's
889
+ * motion cooldown window. Mirrors the runner's own `'on-motion'`
890
+ * detection-mode behaviour where the camera stays in `active`
891
+ * for the cooldown window after the last motion ping.
892
+ *
893
+ * Idempotency is critical because `MotionOnMotionChanged` can fire
894
+ * frequently (every 1-2s while motion is active on Reolink); each
895
+ * rearm just keeps the audio stream open without restarting it.
896
+ */
897
+ private handleLazyAudioMotion;
898
+ /**
899
+ * Subscribe to decoded audio chunks for a camera and feed them into the
900
+ * audio-analyzer. Reads the analyzer's settings via its own
901
+ * `resolveDeviceSettings(deviceId)` method so the orchestrator does not
902
+ * touch the audio-analyzer schema field names directly.
903
+ */
904
+ private subscribeAudioStream;
905
+ }
906
+
907
+ export { type BalancerDecision, type BalancerInput, type DecoderBalancerDecision, type DecoderBalancerInput, type DispatchResult, type IPipelineOrchestratorLocalProvider, type OrchestratorDiagnostics, type PipelineOrchestratorActions, balance, balanceDecoder, computeCapacityScore, PipelineOrchestratorAddon as default, pipelineOrchestratorActions };