@camstack/addon-pipeline-orchestrator 1.1.57 → 1.1.58

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.
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.1.55",
21
+ version: "1.1.56",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_orchestrator_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.1.55",
39
+ version: "1.1.56",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
package/dist/index.js CHANGED
@@ -11929,8 +11929,18 @@ var pipelineExecutorCapability = {
11929
11929
  * - `runtime` — main camera-serving engine (no idle TTL).
11930
11930
  * - `warm-override` — benchmark/test override held in the warm
11931
11931
  * cache; auto-disposed after the idle TTL.
11932
+ * - `device-pool` — a concurrent per-device pool (Phase 2
11933
+ * multi-device, keyed by `deviceKey`) resolved
11934
+ * via `resolveDeviceFactory`. Runs alongside the
11935
+ * `runtime` engine on a DIFFERENT accelerator
11936
+ * (NPU / iGPU / Coral) — this is how the
11937
+ * Engines tab shows all pools running at once.
11932
11938
  */
11933
- kind: _enum(["runtime", "warm-override"]),
11939
+ kind: _enum([
11940
+ "runtime",
11941
+ "warm-override",
11942
+ "device-pool"
11943
+ ]),
11934
11944
  /** Native pid of the underlying Python pool (null when no pool). */
11935
11945
  poolPid: number().nullable(),
11936
11946
  /** ms since this factory was last used (null when not warm-tracked). */
@@ -17764,7 +17774,17 @@ var AgentPipelineSettingsSchema = object({
17764
17774
  * it already uses to reach the hub). Set this only when the auto-detected
17765
17775
  * address is wrong (multi-homed host, NAT, custom interface).
17766
17776
  */
17767
- reachableHost: string().optional()
17777
+ reachableHost: string().optional(),
17778
+ /**
17779
+ * Multi-device inference opt-in (Phase 4). Per-node map deviceKey → {enabled,
17780
+ * weight}. Absent / all-disabled ⇒ the node's single default accelerator
17781
+ * (safe default); two+ enabled ⇒ the dispatcher balances detection sessions
17782
+ * across them so they run CONCURRENTLY.
17783
+ */
17784
+ inferenceDevices: record(string(), object({
17785
+ enabled: boolean(),
17786
+ weight: number().positive().optional()
17787
+ })).optional()
17768
17788
  });
17769
17789
  var CameraPipelineForAgentSchema = object({
17770
17790
  steps: array(PipelineStepInputSchema).readonly(),
@@ -31437,7 +31457,11 @@ var StoredAgentPipelineSettingsSchema = object({
31437
31457
  decode: boolean().optional(),
31438
31458
  audio: boolean().optional(),
31439
31459
  ingest: boolean().optional(),
31440
- reachableHost: string().optional()
31460
+ reachableHost: string().optional(),
31461
+ inferenceDevices: record(string(), object({
31462
+ enabled: boolean(),
31463
+ weight: number().positive().optional()
31464
+ })).optional()
31441
31465
  });
31442
31466
  var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
31443
31467
  var StoredCameraStepOverridePatchSchema = object({
@@ -32001,6 +32025,20 @@ var PipelineSettingsStore = class PipelineSettingsStore {
32001
32025
  return weights;
32002
32026
  }
32003
32027
  /**
32028
+ * The ENABLED inference devices for a node (Phase 4 multi-device opt-in),
32029
+ * as a `deviceKey → weight` map. Only entries with `enabled:true` are
32030
+ * returned; `weight` defaults to 1. Empty when the node opted into nothing
32031
+ * (or fewer than one device is enabled) — the caller then leaves `deviceKey`
32032
+ * unset and the runner uses its single default accelerator.
32033
+ */
32034
+ async buildNodeInferenceDevices(nodeId) {
32035
+ const cfg = (await this.agentSettingsState.get())[nodeId]?.inferenceDevices;
32036
+ const out = {};
32037
+ if (!cfg) return out;
32038
+ for (const [deviceKey, entry] of Object.entries(cfg)) if (entry.enabled) out[deviceKey] = entry.weight && entry.weight > 0 ? entry.weight : 1;
32039
+ return out;
32040
+ }
32041
+ /**
32004
32042
  * Structural runtime check for `CameraPipelineConfig`. Persisted JSON
32005
32043
  * may be stale or partial across upgrades, so we gate reads on shape
32006
32044
  * rather than trusting the raw object. False = silently ignore that
@@ -32516,14 +32554,15 @@ var ReconcileController = class ReconcileController {
32516
32554
  //#region src/detection-session-registry.ts
32517
32555
  var DetectionSessionRegistry = class {
32518
32556
  sessions = /* @__PURE__ */ new Map();
32519
- register(deviceId, nodeId, startedAt) {
32557
+ register(deviceId, nodeId, startedAt, deviceKey) {
32520
32558
  const existing = this.sessions.get(deviceId);
32521
32559
  if (existing?.teardownTimer) clearTimeout(existing.teardownTimer);
32522
32560
  const session = {
32523
32561
  deviceId,
32524
32562
  nodeId,
32525
32563
  startedAt,
32526
- teardownTimer: null
32564
+ teardownTimer: null,
32565
+ ...deviceKey ? { deviceKey } : {}
32527
32566
  };
32528
32567
  this.sessions.set(deviceId, session);
32529
32568
  return session;
@@ -32565,6 +32604,41 @@ var DetectionSessionRegistry = class {
32565
32604
  }
32566
32605
  };
32567
32606
  //#endregion
32607
+ //#region src/device-balancer.ts
32608
+ /**
32609
+ * Per-node inference-device balancer (Phase 4 multi-device).
32610
+ *
32611
+ * Given the ENABLED device pools for a node (deviceKey → relative weight) and
32612
+ * the count of ACTIVE detection sessions currently on each pool, pick the pool
32613
+ * a NEW session should land on so concurrent sessions spread across the node's
32614
+ * accelerators (NPU / iGPU / Coral / additional Corals) proportionally to
32615
+ * weight — the classic weighted least-loaded choice used by the node balancer,
32616
+ * applied one level down at the device granularity.
32617
+ *
32618
+ * Returns `null` when fewer than TWO devices are enabled: with zero the caller
32619
+ * leaves `deviceKey` unset (the runner's single default accelerator); with
32620
+ * exactly one there is nothing to balance and pinning every session to that one
32621
+ * key is redundant (the default already serves it). Balancing only earns its
32622
+ * keep when there's a real choice.
32623
+ */
32624
+ function pickInferenceDevice(enabled, activeCountByDevice) {
32625
+ const keys = Object.keys(enabled);
32626
+ if (keys.length < 2) return null;
32627
+ let best = null;
32628
+ let bestLoad = Number.POSITIVE_INFINITY;
32629
+ let bestWeight = -1;
32630
+ for (const key of keys) {
32631
+ const weight = enabled[key] > 0 ? enabled[key] : 1;
32632
+ const load = (activeCountByDevice[key] ?? 0) / weight;
32633
+ if (load < bestLoad || load === bestLoad && weight > bestWeight) {
32634
+ best = key;
32635
+ bestLoad = load;
32636
+ bestWeight = weight;
32637
+ }
32638
+ }
32639
+ return best;
32640
+ }
32641
+ //#endregion
32568
32642
  //#region src/session-dispatch-controller.ts
32569
32643
  var SessionDispatchController = class {
32570
32644
  deps;
@@ -32782,10 +32856,15 @@ var SessionDispatchController = class {
32782
32856
  return;
32783
32857
  }
32784
32858
  const targetNodeId = decision.agentNodeId;
32859
+ const enabledDevices = await this.deps.settingsStore.buildNodeInferenceDevices(targetNodeId);
32860
+ const activeByDevice = {};
32861
+ for (const s of this.sessionRegistry.list()) if (s.nodeId === targetNodeId && s.deviceKey) activeByDevice[s.deviceKey] = (activeByDevice[s.deviceKey] ?? 0) + 1;
32862
+ const deviceKey = pickInferenceDevice(enabledDevices, activeByDevice) ?? void 0;
32785
32863
  const pipelineConfig = await this.deps.settingsStore.resolvePipelineForDevice(deviceId, targetNodeId);
32786
32864
  const zones = await this.deps.listZones(deviceId);
32787
32865
  const sessionConfig = {
32788
32866
  deviceId,
32867
+ ...deviceKey ? { deviceKey } : {},
32789
32868
  motionCooldownMs: config.motionCooldownMs,
32790
32869
  motionFps: config.motionFps,
32791
32870
  detectionFps: config.detectionFps,
@@ -32807,10 +32886,13 @@ var SessionDispatchController = class {
32807
32886
  try {
32808
32887
  await this.deps.attachOn(targetNodeId, sessionConfig);
32809
32888
  this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
32810
- this.sessionRegistry.register(deviceId, targetNodeId, Date.now());
32889
+ this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), deviceKey);
32811
32890
  log.info("detection session: dispatched on motion", {
32812
32891
  tags: { nodeId: targetNodeId },
32813
- meta: { reason: decision.reason }
32892
+ meta: {
32893
+ reason: decision.reason,
32894
+ ...deviceKey ? { deviceKey } : {}
32895
+ }
32814
32896
  });
32815
32897
  } finally {
32816
32898
  dispatchGuard.dispose();
package/dist/index.mjs CHANGED
@@ -11901,8 +11901,18 @@ var pipelineExecutorCapability = {
11901
11901
  * - `runtime` — main camera-serving engine (no idle TTL).
11902
11902
  * - `warm-override` — benchmark/test override held in the warm
11903
11903
  * cache; auto-disposed after the idle TTL.
11904
+ * - `device-pool` — a concurrent per-device pool (Phase 2
11905
+ * multi-device, keyed by `deviceKey`) resolved
11906
+ * via `resolveDeviceFactory`. Runs alongside the
11907
+ * `runtime` engine on a DIFFERENT accelerator
11908
+ * (NPU / iGPU / Coral) — this is how the
11909
+ * Engines tab shows all pools running at once.
11904
11910
  */
11905
- kind: _enum(["runtime", "warm-override"]),
11911
+ kind: _enum([
11912
+ "runtime",
11913
+ "warm-override",
11914
+ "device-pool"
11915
+ ]),
11906
11916
  /** Native pid of the underlying Python pool (null when no pool). */
11907
11917
  poolPid: number().nullable(),
11908
11918
  /** ms since this factory was last used (null when not warm-tracked). */
@@ -17736,7 +17746,17 @@ var AgentPipelineSettingsSchema = object({
17736
17746
  * it already uses to reach the hub). Set this only when the auto-detected
17737
17747
  * address is wrong (multi-homed host, NAT, custom interface).
17738
17748
  */
17739
- reachableHost: string().optional()
17749
+ reachableHost: string().optional(),
17750
+ /**
17751
+ * Multi-device inference opt-in (Phase 4). Per-node map deviceKey → {enabled,
17752
+ * weight}. Absent / all-disabled ⇒ the node's single default accelerator
17753
+ * (safe default); two+ enabled ⇒ the dispatcher balances detection sessions
17754
+ * across them so they run CONCURRENTLY.
17755
+ */
17756
+ inferenceDevices: record(string(), object({
17757
+ enabled: boolean(),
17758
+ weight: number().positive().optional()
17759
+ })).optional()
17740
17760
  });
17741
17761
  var CameraPipelineForAgentSchema = object({
17742
17762
  steps: array(PipelineStepInputSchema).readonly(),
@@ -31409,7 +31429,11 @@ var StoredAgentPipelineSettingsSchema = object({
31409
31429
  decode: boolean().optional(),
31410
31430
  audio: boolean().optional(),
31411
31431
  ingest: boolean().optional(),
31412
- reachableHost: string().optional()
31432
+ reachableHost: string().optional(),
31433
+ inferenceDevices: record(string(), object({
31434
+ enabled: boolean(),
31435
+ weight: number().positive().optional()
31436
+ })).optional()
31413
31437
  });
31414
31438
  var AgentSettingsMapSchema = record(string(), StoredAgentPipelineSettingsSchema);
31415
31439
  var StoredCameraStepOverridePatchSchema = object({
@@ -31973,6 +31997,20 @@ var PipelineSettingsStore = class PipelineSettingsStore {
31973
31997
  return weights;
31974
31998
  }
31975
31999
  /**
32000
+ * The ENABLED inference devices for a node (Phase 4 multi-device opt-in),
32001
+ * as a `deviceKey → weight` map. Only entries with `enabled:true` are
32002
+ * returned; `weight` defaults to 1. Empty when the node opted into nothing
32003
+ * (or fewer than one device is enabled) — the caller then leaves `deviceKey`
32004
+ * unset and the runner uses its single default accelerator.
32005
+ */
32006
+ async buildNodeInferenceDevices(nodeId) {
32007
+ const cfg = (await this.agentSettingsState.get())[nodeId]?.inferenceDevices;
32008
+ const out = {};
32009
+ if (!cfg) return out;
32010
+ for (const [deviceKey, entry] of Object.entries(cfg)) if (entry.enabled) out[deviceKey] = entry.weight && entry.weight > 0 ? entry.weight : 1;
32011
+ return out;
32012
+ }
32013
+ /**
31976
32014
  * Structural runtime check for `CameraPipelineConfig`. Persisted JSON
31977
32015
  * may be stale or partial across upgrades, so we gate reads on shape
31978
32016
  * rather than trusting the raw object. False = silently ignore that
@@ -32488,14 +32526,15 @@ var ReconcileController = class ReconcileController {
32488
32526
  //#region src/detection-session-registry.ts
32489
32527
  var DetectionSessionRegistry = class {
32490
32528
  sessions = /* @__PURE__ */ new Map();
32491
- register(deviceId, nodeId, startedAt) {
32529
+ register(deviceId, nodeId, startedAt, deviceKey) {
32492
32530
  const existing = this.sessions.get(deviceId);
32493
32531
  if (existing?.teardownTimer) clearTimeout(existing.teardownTimer);
32494
32532
  const session = {
32495
32533
  deviceId,
32496
32534
  nodeId,
32497
32535
  startedAt,
32498
- teardownTimer: null
32536
+ teardownTimer: null,
32537
+ ...deviceKey ? { deviceKey } : {}
32499
32538
  };
32500
32539
  this.sessions.set(deviceId, session);
32501
32540
  return session;
@@ -32537,6 +32576,41 @@ var DetectionSessionRegistry = class {
32537
32576
  }
32538
32577
  };
32539
32578
  //#endregion
32579
+ //#region src/device-balancer.ts
32580
+ /**
32581
+ * Per-node inference-device balancer (Phase 4 multi-device).
32582
+ *
32583
+ * Given the ENABLED device pools for a node (deviceKey → relative weight) and
32584
+ * the count of ACTIVE detection sessions currently on each pool, pick the pool
32585
+ * a NEW session should land on so concurrent sessions spread across the node's
32586
+ * accelerators (NPU / iGPU / Coral / additional Corals) proportionally to
32587
+ * weight — the classic weighted least-loaded choice used by the node balancer,
32588
+ * applied one level down at the device granularity.
32589
+ *
32590
+ * Returns `null` when fewer than TWO devices are enabled: with zero the caller
32591
+ * leaves `deviceKey` unset (the runner's single default accelerator); with
32592
+ * exactly one there is nothing to balance and pinning every session to that one
32593
+ * key is redundant (the default already serves it). Balancing only earns its
32594
+ * keep when there's a real choice.
32595
+ */
32596
+ function pickInferenceDevice(enabled, activeCountByDevice) {
32597
+ const keys = Object.keys(enabled);
32598
+ if (keys.length < 2) return null;
32599
+ let best = null;
32600
+ let bestLoad = Number.POSITIVE_INFINITY;
32601
+ let bestWeight = -1;
32602
+ for (const key of keys) {
32603
+ const weight = enabled[key] > 0 ? enabled[key] : 1;
32604
+ const load = (activeCountByDevice[key] ?? 0) / weight;
32605
+ if (load < bestLoad || load === bestLoad && weight > bestWeight) {
32606
+ best = key;
32607
+ bestLoad = load;
32608
+ bestWeight = weight;
32609
+ }
32610
+ }
32611
+ return best;
32612
+ }
32613
+ //#endregion
32540
32614
  //#region src/session-dispatch-controller.ts
32541
32615
  var SessionDispatchController = class {
32542
32616
  deps;
@@ -32754,10 +32828,15 @@ var SessionDispatchController = class {
32754
32828
  return;
32755
32829
  }
32756
32830
  const targetNodeId = decision.agentNodeId;
32831
+ const enabledDevices = await this.deps.settingsStore.buildNodeInferenceDevices(targetNodeId);
32832
+ const activeByDevice = {};
32833
+ for (const s of this.sessionRegistry.list()) if (s.nodeId === targetNodeId && s.deviceKey) activeByDevice[s.deviceKey] = (activeByDevice[s.deviceKey] ?? 0) + 1;
32834
+ const deviceKey = pickInferenceDevice(enabledDevices, activeByDevice) ?? void 0;
32757
32835
  const pipelineConfig = await this.deps.settingsStore.resolvePipelineForDevice(deviceId, targetNodeId);
32758
32836
  const zones = await this.deps.listZones(deviceId);
32759
32837
  const sessionConfig = {
32760
32838
  deviceId,
32839
+ ...deviceKey ? { deviceKey } : {},
32761
32840
  motionCooldownMs: config.motionCooldownMs,
32762
32841
  motionFps: config.motionFps,
32763
32842
  detectionFps: config.detectionFps,
@@ -32779,10 +32858,13 @@ var SessionDispatchController = class {
32779
32858
  try {
32780
32859
  await this.deps.attachOn(targetNodeId, sessionConfig);
32781
32860
  this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
32782
- this.sessionRegistry.register(deviceId, targetNodeId, Date.now());
32861
+ this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), deviceKey);
32783
32862
  log.info("detection session: dispatched on motion", {
32784
32863
  tags: { nodeId: targetNodeId },
32785
- meta: { reason: decision.reason }
32864
+ meta: {
32865
+ reason: decision.reason,
32866
+ ...deviceKey ? { deviceKey } : {}
32867
+ }
32786
32868
  });
32787
32869
  } finally {
32788
32870
  dispatchGuard.dispose();
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Q4YOYkCV.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-BiQO4cUV.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.1.57",
3
+ "version": "1.1.58",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",