@camstack/addon-pipeline 1.0.6 → 1.0.8

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 (56) hide show
  1. package/dist/audio-analyzer/index.js +7 -9
  2. package/dist/audio-analyzer/index.mjs +3 -4
  3. package/dist/audio-codec-nodeav/index.js +2 -2
  4. package/dist/audio-codec-nodeav/index.mjs +2 -2
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +386 -198
  8. package/dist/detection-pipeline/index.mjs +374 -185
  9. package/dist/{dist-DsDFrG0I.mjs → dist-BA6DR_jV.mjs} +1798 -1638
  10. package/dist/{dist-BiUtYscO.js → dist-BLcTVvol.js} +1821 -1637
  11. package/dist/model-download-service-C7AjBsX9-B0ekM6dF.mjs +301 -0
  12. package/dist/model-download-service-C7AjBsX9-rXY-VFDk.js +358 -0
  13. package/dist/motion-wasm/index.js +1 -1
  14. package/dist/motion-wasm/index.mjs +1 -1
  15. package/dist/pipeline-runner/index.js +1 -1
  16. package/dist/pipeline-runner/index.mjs +1 -1
  17. package/dist/recorder/index.js +6 -7
  18. package/dist/recorder/index.mjs +4 -4
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbTGCEnd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs} +3 -3
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +26 -0
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dsz9DmNr.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CHcXI1Wf.mjs} +1 -1
  23. package/dist/stream-broker/{hostInit-BJ3QDdFs.mjs → hostInit-zRy9SzlX.mjs} +3 -3
  24. package/dist/stream-broker/index.js +27 -28
  25. package/dist/stream-broker/index.mjs +21 -21
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B4oJIlgF.js → MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js} +1 -1
  28. package/embed-dist/assets/MotionZonesSettings-NcxxQN8r-CQzEnQoq.js +1 -0
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CyTsHaor.js → PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js} +1 -1
  30. package/embed-dist/assets/index-CSuLwWK-.js +80 -0
  31. package/embed-dist/index.html +1 -1
  32. package/package.json +1 -1
  33. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  34. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  35. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  37. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  38. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  39. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  40. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  41. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  42. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  43. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  44. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  45. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  46. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  47. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  48. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  49. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  50. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  51. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  52. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  53. package/dist/chunk-D6vf50IK.js +0 -28
  54. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-3STWM0yI.mjs +0 -26
  55. package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CUopGB1R.js +0 -1
  56. package/embed-dist/assets/index-hwJEVIPM.js +0 -80
@@ -2,18 +2,17 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-BiUtYscO.js");
5
+ const require_model_download_service_C7AjBsX9 = require("../model-download-service-C7AjBsX9-rXY-VFDk.js");
6
+ const require_dist = require("../dist-BLcTVvol.js");
7
7
  let node_fs = require("node:fs");
8
- node_fs = require_chunk.__toESM(node_fs);
8
+ node_fs = require_model_download_service_C7AjBsX9.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
- node_path = require_chunk.__toESM(node_path);
10
+ node_path = require_model_download_service_C7AjBsX9.__toESM(node_path);
11
11
  let node_os = require("node:os");
12
- node_os = require_chunk.__toESM(node_os);
13
- let _camstack_system = require("@camstack/system");
12
+ node_os = require_model_download_service_C7AjBsX9.__toESM(node_os);
14
13
  let node_child_process = require("node:child_process");
15
14
  let sharp = require("sharp");
16
- sharp = require_chunk.__toESM(sharp);
15
+ sharp = require_model_download_service_C7AjBsX9.__toESM(sharp);
17
16
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
18
17
  var MSG_COMMAND = 0;
19
18
  var MSG_INFER_JPEG = 1;
@@ -2166,6 +2165,78 @@ var EngineFactory = class {
2166
2165
  }
2167
2166
  };
2168
2167
  //#endregion
2168
+ //#region src/detection-pipeline/engine-store-keys.ts
2169
+ /**
2170
+ * Per-node scoping for the detection-pipeline engine cascade.
2171
+ *
2172
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
2173
+ * settings-store cap is hub-resident; every node's detection instance reads and
2174
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
2175
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
2176
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
2177
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
2178
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
2179
+ * another node that has no NPU.
2180
+ *
2181
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
2182
+ * Everything else in the store stays shared. A legacy un-scoped value (written
2183
+ * before this change, or by an older build) is read as a migration fallback and
2184
+ * re-persisted under the node-scoped key on the next write.
2185
+ */
2186
+ var ENGINE_CASCADE_KEYS = [
2187
+ "engineBackend",
2188
+ "engineDevice",
2189
+ "probedBestEngine"
2190
+ ];
2191
+ function isEngineCascadeKey(key) {
2192
+ return ENGINE_CASCADE_KEYS.includes(key);
2193
+ }
2194
+ /**
2195
+ * Normalise a raw kernel node id to the bare node id used for scoping.
2196
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
2197
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
2198
+ */
2199
+ function normalizeEngineNodeId(rawNodeId) {
2200
+ const raw = rawNodeId ?? "hub";
2201
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
2202
+ }
2203
+ /** The node-scoped store key for an engine cascade field. */
2204
+ function nodeEngineKey(base, nodeId) {
2205
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
2206
+ }
2207
+ /**
2208
+ * Read an engine cascade value for a node: the node-scoped key if present,
2209
+ * otherwise the legacy un-scoped value (migration), otherwise undefined.
2210
+ */
2211
+ function readNodeEngineValue(store, base, nodeId) {
2212
+ const scoped = store[nodeEngineKey(base, nodeId)];
2213
+ return scoped !== void 0 ? scoped : store[base];
2214
+ }
2215
+ /**
2216
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
2217
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
2218
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
2219
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
2220
+ * node's form).
2221
+ */
2222
+ function projectNodeEngine(store, nodeId) {
2223
+ const out = {};
2224
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
2225
+ for (const key of Object.keys(store)) {
2226
+ const atIdx = key.indexOf("@");
2227
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
2228
+ scopedForAnyNode.add(key);
2229
+ continue;
2230
+ }
2231
+ out[key] = store[key];
2232
+ }
2233
+ for (const base of ENGINE_CASCADE_KEYS) {
2234
+ const value = readNodeEngineValue(store, base, nodeId);
2235
+ if (value !== void 0) out[base] = value;
2236
+ }
2237
+ return out;
2238
+ }
2239
+ //#endregion
2169
2240
  //#region src/detection-pipeline/postprocess/dispatch.ts
2170
2241
  var VALID_KINDS = new Set([
2171
2242
  "detections",
@@ -2205,7 +2276,7 @@ function postprocessYolo(output, stepDef) {
2205
2276
  const letterbox = output.letterbox;
2206
2277
  const dets = [];
2207
2278
  for (let i = 0; i < numBoxes; i++) {
2208
- const cx = tensor[0 * numBoxes + i];
2279
+ const cx = tensor[i];
2209
2280
  const cy = tensor[1 * numBoxes + i];
2210
2281
  const w = tensor[2 * numBoxes + i];
2211
2282
  const h = tensor[3 * numBoxes + i];
@@ -2361,7 +2432,7 @@ function postprocessArcface(output, _stepDef) {
2361
2432
  let sumSq = 0;
2362
2433
  for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
2363
2434
  const norm = Math.sqrt(sumSq);
2364
- const normalized = new Array(tensor.length);
2435
+ const normalized = Array.from({ length: tensor.length });
2365
2436
  for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
2366
2437
  return {
2367
2438
  kind: "embedding",
@@ -3536,34 +3607,69 @@ function walkFieldsForDefaults(fields, out) {
3536
3607
  }
3537
3608
  //#endregion
3538
3609
  //#region src/detection-pipeline/runtimes.ts
3539
- var AUTO = {
3540
- value: "auto",
3541
- label: "Auto"
3542
- };
3543
- var CPU = {
3544
- value: "cpu",
3545
- label: "CPU"
3546
- };
3547
- var RUNTIMES = [
3548
- {
3549
- id: "onnx",
3610
+ var KNOWN_PLATFORMS = [
3611
+ "darwin",
3612
+ "linux",
3613
+ "win32"
3614
+ ];
3615
+ var KNOWN_ARCHES = ["arm64", "x64"];
3616
+ var KNOWN_GPU_TYPES = [
3617
+ "nvidia",
3618
+ "amd",
3619
+ "intel",
3620
+ "apple"
3621
+ ];
3622
+ var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
3623
+ function toKnownPlatform(p) {
3624
+ return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
3625
+ }
3626
+ function toKnownArch(a) {
3627
+ return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
3628
+ }
3629
+ function gpuInfoFrom(hw) {
3630
+ if (!hw.gpu) return null;
3631
+ const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
3632
+ if (!type) return null;
3633
+ return {
3634
+ type,
3635
+ name: ""
3636
+ };
3637
+ }
3638
+ function npuInfoFrom(hw) {
3639
+ if (!hw.npu) return null;
3640
+ const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
3641
+ if (!type) return null;
3642
+ return { type };
3643
+ }
3644
+ function envToHardwareInfo(env) {
3645
+ if (!env.hardware) return null;
3646
+ return {
3647
+ platform: toKnownPlatform(env.platform),
3648
+ arch: toKnownArch(env.arch),
3649
+ cpuModel: "",
3650
+ cpuCores: 0,
3651
+ totalRAM_MB: 0,
3652
+ availableRAM_MB: 0,
3653
+ gpu: gpuInfoFrom(env.hardware),
3654
+ npu: npuInfoFrom(env.hardware)
3655
+ };
3656
+ }
3657
+ function probedToHardwareInfo(hw) {
3658
+ if (!hw) return null;
3659
+ return {
3660
+ platform: toKnownPlatform(process.platform),
3661
+ arch: toKnownArch(process.arch),
3662
+ cpuModel: "",
3663
+ cpuCores: 0,
3664
+ totalRAM_MB: 0,
3665
+ availableRAM_MB: 0,
3666
+ gpu: gpuInfoFrom(hw),
3667
+ npu: npuInfoFrom(hw)
3668
+ };
3669
+ }
3670
+ var RUNTIME_DETAIL = {
3671
+ onnx: {
3550
3672
  label: "ONNX Runtime",
3551
- supports: () => true,
3552
- devices: (hw) => {
3553
- if (!hw) return [CPU];
3554
- const out = [CPU];
3555
- if (hw.gpu?.type === "nvidia") out.push({
3556
- value: "cuda",
3557
- label: "CUDA"
3558
- });
3559
- if (hw.npu?.type === "apple-ane") out.push({
3560
- value: "coreml",
3561
- label: "CoreML EP"
3562
- });
3563
- return out;
3564
- },
3565
- defaultDevice: "cpu",
3566
- modelFormat: "onnx",
3567
3673
  pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3568
3674
  tuning: {
3569
3675
  concurrency: 4,
@@ -3572,25 +3678,8 @@ var RUNTIMES = [
3572
3678
  intraOpThreads: 0
3573
3679
  }
3574
3680
  },
3575
- {
3576
- id: "openvino",
3681
+ openvino: {
3577
3682
  label: "OpenVINO",
3578
- supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3579
- devices: (hw) => {
3580
- if (!hw) return [AUTO];
3581
- const out = [AUTO, CPU];
3582
- if (hw.gpu?.type === "intel") out.push({
3583
- value: "gpu",
3584
- label: "GPU"
3585
- });
3586
- if (hw.npu?.type === "intel-npu") out.push({
3587
- value: "npu",
3588
- label: "NPU"
3589
- });
3590
- return out;
3591
- },
3592
- defaultDevice: "auto",
3593
- modelFormat: "openvino",
3594
3683
  pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3595
3684
  tuning: {
3596
3685
  concurrency: 1,
@@ -3598,29 +3687,8 @@ var RUNTIMES = [
3598
3687
  numStreams: 0
3599
3688
  }
3600
3689
  },
3601
- {
3602
- id: "coreml",
3690
+ coreml: {
3603
3691
  label: "CoreML",
3604
- supports: (env) => env.platform === "darwin",
3605
- devices: (hw) => {
3606
- const all = {
3607
- value: "all",
3608
- label: "All (ANE + GPU + CPU)"
3609
- };
3610
- if (!hw) return [all];
3611
- const out = [all];
3612
- if (hw.npu?.type === "apple-ane") out.push({
3613
- value: "ane",
3614
- label: "Apple Neural Engine"
3615
- });
3616
- out.push({
3617
- value: "gpu",
3618
- label: "GPU"
3619
- }, CPU);
3620
- return out;
3621
- },
3622
- defaultDevice: "all",
3623
- modelFormat: "coreml",
3624
3692
  pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3625
3693
  tuning: {
3626
3694
  concurrency: 1,
@@ -3630,32 +3698,50 @@ var RUNTIMES = [
3630
3698
  numWorkers: 1
3631
3699
  }
3632
3700
  }
3633
- ];
3634
- function def(id) {
3635
- const d = RUNTIMES.find((r) => r.id === id);
3636
- if (!d) throw new Error(`Unknown runtime: ${id}`);
3637
- return d;
3638
- }
3701
+ };
3702
+ /**
3703
+ * Returns the list of supported runtime IDs for the given hardware env.
3704
+ * Delegates to `@camstack/types` `supportedRuntimes`.
3705
+ */
3639
3706
  function supportedRuntimes(env) {
3640
- return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3707
+ return require_dist.supportedRuntimes(envToHardwareInfo(env));
3641
3708
  }
3709
+ /**
3710
+ * Returns the device options for a given runtime and probed hardware.
3711
+ * Delegates to `@camstack/types` `runtimeDevices`.
3712
+ */
3642
3713
  function runtimeDevices(id, hardware) {
3643
- return def(id).devices(hardware);
3714
+ return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
3644
3715
  }
3716
+ /**
3717
+ * Returns the default device string for a given runtime.
3718
+ * Delegates to `@camstack/types` `defaultDeviceFor`.
3719
+ */
3645
3720
  function defaultDeviceFor(id) {
3646
- return def(id).defaultDevice;
3647
- }
3648
- function pythonRequirementsFor(id) {
3649
- return def(id).pythonRequirements;
3721
+ return require_dist.defaultDeviceFor(id);
3650
3722
  }
3723
+ /** Model format for each inference runtime supported by the detection pipeline. */
3724
+ var RUNTIME_FORMAT = {
3725
+ onnx: "onnx",
3726
+ openvino: "openvino",
3727
+ coreml: "coreml"
3728
+ };
3729
+ /**
3730
+ * Returns the model format required for a given runtime.
3731
+ * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
3732
+ * matching the engine-provisioner's own ModelFormat type.
3733
+ */
3651
3734
  function modelFormatFor(id) {
3652
- return def(id).modelFormat;
3735
+ return RUNTIME_FORMAT[id];
3736
+ }
3737
+ function pythonRequirementsFor(id) {
3738
+ return RUNTIME_DETAIL[id].pythonRequirements;
3653
3739
  }
3654
3740
  function tuningFor(id) {
3655
- return def(id).tuning;
3741
+ return RUNTIME_DETAIL[id].tuning;
3656
3742
  }
3657
3743
  function runtimeLabel(id) {
3658
- return def(id).label;
3744
+ return RUNTIME_DETAIL[id].label;
3659
3745
  }
3660
3746
  /**
3661
3747
  * Proactive-install hint kept for back-compat (re-exported from index.ts).
@@ -3996,6 +4082,27 @@ function toProbedHardware(hw) {
3996
4082
  gpu: hw.gpu ? { type: hw.gpu.type } : null
3997
4083
  };
3998
4084
  }
4085
+ var ONNX_FLOOR = {
4086
+ runtime: "python",
4087
+ backend: "onnx",
4088
+ format: "onnx",
4089
+ device: "cpu"
4090
+ };
4091
+ /**
4092
+ * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
4093
+ * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
4094
+ * result is identical (onnx / cpu) but is now derived through the shared rules
4095
+ * instead of duplicated inline.
4096
+ */
4097
+ function onnxFloorPick() {
4098
+ const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
4099
+ return {
4100
+ runtime: "python",
4101
+ backend: pick.runtimeId,
4102
+ format: modelFormatFor(pick.runtimeId),
4103
+ device: pick.device
4104
+ };
4105
+ }
3999
4106
  var DetectionPipelineProvider = class DetectionPipelineProvider {
4000
4107
  modelsDir;
4001
4108
  eventBus;
@@ -4085,7 +4192,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4085
4192
  this.readStore = () => settings.readAddonStore();
4086
4193
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4087
4194
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4088
- this.currentEngine = DetectionPipelineProvider.detectBestEngine();
4195
+ this.currentEngine = ONNX_FLOOR;
4089
4196
  this.log.info("Engine selected (default)", { meta: {
4090
4197
  runtime: this.currentEngine.runtime,
4091
4198
  backend: this.currentEngine.backend,
@@ -4151,62 +4258,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4151
4258
  isReady() {
4152
4259
  return this.ready;
4153
4260
  }
4154
- /** Detect the best inference engine for this platform. */
4155
- static detectBestEngine() {
4156
- const platform = process.platform;
4157
- const arch = process.arch;
4158
- if (platform === "darwin" && arch === "arm64") return {
4159
- runtime: "python",
4160
- backend: "coreml",
4161
- format: "coreml",
4162
- device: "all"
4163
- };
4164
- if (platform === "darwin") return {
4165
- runtime: "python",
4166
- backend: "coreml",
4167
- format: "coreml",
4168
- device: "gpu"
4169
- };
4170
- try {
4171
- const { execFileSync } = require("node:child_process");
4172
- execFileSync("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"], {
4173
- timeout: 3e3,
4174
- stdio: "pipe"
4175
- });
4176
- return {
4177
- runtime: "python",
4178
- backend: "onnx",
4179
- format: "onnx",
4180
- device: "cuda"
4181
- };
4182
- } catch {}
4183
- try {
4184
- const fsmod = require("node:fs");
4185
- const isIntel = require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
4186
- const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
4187
- const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
4188
- if (isIntel && (hasIgpu || hasNpu)) return {
4189
- runtime: "python",
4190
- backend: "openvino",
4191
- format: "openvino",
4192
- device: "auto"
4193
- };
4194
- } catch {}
4195
- return {
4196
- runtime: "python",
4197
- backend: "onnx",
4198
- format: "onnx",
4199
- device: "cpu"
4200
- };
4201
- }
4202
4261
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
4203
4262
  async setApi(addonCtx) {
4204
4263
  this.addonCtx = addonCtx;
4205
- if (this.needsAutoPick) {
4264
+ if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
4206
4265
  await this.autoPickAndPersist();
4207
4266
  this.needsAutoPick = false;
4267
+ this.startProvisioningForCurrentEngine();
4268
+ } else {
4269
+ this.startProvisioningForCurrentEngine();
4270
+ const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4271
+ if (state !== "ready") return;
4272
+ unsubscribe();
4273
+ if (!this.needsAutoPick) return;
4274
+ this.autoPickAndPersist().then(() => {
4275
+ this.needsAutoPick = false;
4276
+ this.startProvisioningForCurrentEngine();
4277
+ });
4278
+ });
4279
+ this.addonCtx.addDisposer(unsubscribe);
4208
4280
  }
4209
- this.startProvisioningForCurrentEngine();
4281
+ else this.startProvisioningForCurrentEngine();
4210
4282
  }
4211
4283
  /**
4212
4284
  * Auto-pick the best supported runtime at first boot (no stored engine).
@@ -4221,7 +4293,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4221
4293
  try {
4222
4294
  const api = this.addonCtx?.api;
4223
4295
  if (api) {
4224
- const caps = await api.platformProbe.getCapabilities.query();
4296
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
4225
4297
  hardware = caps?.hardware ?? null;
4226
4298
  const bs = caps?.bestScore;
4227
4299
  if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
@@ -4235,9 +4307,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4235
4307
  device: pick.device
4236
4308
  };
4237
4309
  this.currentEngine = engine;
4310
+ const apNode = this.localProbeNodeId();
4238
4311
  await this.writeStore({
4239
- engineBackend: pick.runtimeId,
4240
- engineDevice: pick.device
4312
+ [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4313
+ [nodeEngineKey("engineDevice", apNode)]: pick.device
4241
4314
  });
4242
4315
  this.log.info("Auto-picked engine at first boot", { meta: {
4243
4316
  backend: pick.runtimeId,
@@ -4354,7 +4427,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4354
4427
  if (!step.enabled) continue;
4355
4428
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4356
4429
  if (!modelEntry) continue;
4357
- if ((0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) continue;
4430
+ if (require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4358
4431
  await this.downloadWithRetry(modelEntry, format, 3);
4359
4432
  }
4360
4433
  }
@@ -4410,11 +4483,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4410
4483
  * callers fall back to the registry's safe minimum. The engine OFFER derives
4411
4484
  * from hardware ONLY — install state (probe `scores`) never gates it.
4412
4485
  */
4486
+ /**
4487
+ * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4488
+ * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4489
+ * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4490
+ * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4491
+ * have) and pin a device the node can't run, breaking provisioning.
4492
+ */
4493
+ localProbeNodeId() {
4494
+ const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
4495
+ return raw.includes("/") ? raw.split("/")[0] : raw;
4496
+ }
4413
4497
  async fetchProbeGatingData() {
4414
4498
  try {
4415
4499
  const api = this.addonCtx?.api;
4416
4500
  if (!api) return { hardware: null };
4417
- return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
4501
+ return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4418
4502
  } catch {
4419
4503
  return { hardware: null };
4420
4504
  }
@@ -4551,7 +4635,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4551
4635
  const formats = {};
4552
4636
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4553
4637
  if (!entry) continue;
4554
- const downloaded = (0, _camstack_system.isModelDownloaded)(this.modelsDir, m, formatKey);
4638
+ const downloaded = require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, m, formatKey);
4555
4639
  formats[formatKey] = {
4556
4640
  url: entry.url,
4557
4641
  sizeMB: entry.sizeMB,
@@ -4674,7 +4758,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4674
4758
  const { modelId, format, addonId } = input;
4675
4759
  const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4676
4760
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4677
- if (!(0, _camstack_system.deleteModelFromDisk)(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4761
+ if (!require_model_download_service_C7AjBsX9.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4678
4762
  this.log.info("Model deleted from disk", { meta: {
4679
4763
  modelId,
4680
4764
  format
@@ -5129,7 +5213,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5129
5213
  const format = this.currentEngine?.format ?? "onnx";
5130
5214
  for (const step of needed) {
5131
5215
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5132
- if (modelEntry && !(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
5216
+ if (modelEntry && !require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) {
5133
5217
  this.log.info("Downloading model for step", { meta: {
5134
5218
  modelId: step.modelId,
5135
5219
  format,
@@ -5154,7 +5238,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5154
5238
  /** Download a model with retry + exponential backoff */
5155
5239
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
5156
5240
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
5157
- await (0, _camstack_system.ensureModel)(this.modelsDir, entry, format, onProgress);
5241
+ await require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, entry, format, onProgress);
5158
5242
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
5159
5243
  return;
5160
5244
  } catch (err) {
@@ -5479,7 +5563,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5479
5563
  this.engineFactory = null;
5480
5564
  this.executor = null;
5481
5565
  }
5482
- for (const id of [...this.deviceProxies.keys()]) this.releaseDeviceProxy(id);
5566
+ for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5483
5567
  }
5484
5568
  /**
5485
5569
  * Resolve and cache a {@link DeviceProxy} for the given camera. Pins
@@ -5631,11 +5715,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5631
5715
  } });
5632
5716
  continue;
5633
5717
  }
5634
- if (!(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5718
+ if (!require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5635
5719
  modelId: step.modelId,
5636
5720
  format
5637
5721
  } });
5638
- downloads.push((0, _camstack_system.ensureModel)(this.modelsDir, modelEntry, format).then(() => {}));
5722
+ downloads.push(require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
5639
5723
  }
5640
5724
  await Promise.all(downloads);
5641
5725
  await this.ensureBackendDeps(this.currentEngine);
@@ -5663,25 +5747,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5663
5747
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
5664
5748
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
5665
5749
  * Returns null when neither source has anything; the caller keeps
5666
- * whatever `detectBestEngine()` picked at construction.
5750
+ * the onnx floor set at construction until autoPickAndPersist() runs.
5667
5751
  */
5668
5752
  async loadEngine() {
5669
5753
  const store = await this.readStore();
5670
5754
  const storedRuntime = store["engineRuntime"];
5671
- const storedBackend = store["engineBackend"];
5755
+ const node = this.localProbeNodeId();
5756
+ const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5672
5757
  if (typeof storedBackend === "string" && storedBackend.length > 0) {
5673
5758
  const backend = storedBackend;
5674
- const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5675
- const detected = DetectionPipelineProvider.detectBestEngine();
5759
+ const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5760
+ const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5761
+ const floor = onnxFloorPick();
5676
5762
  const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5677
5763
  if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5678
- this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5764
+ this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
5679
5765
  stored: migratedBackend,
5680
- fallback: `${detected.backend}`
5766
+ fallback: `${floor.backend}`
5681
5767
  } });
5682
- return detected;
5768
+ return floor;
5683
5769
  }
5684
- const device = storedDevice || detected.device;
5770
+ const device = storedDevice || floor.device;
5685
5771
  return {
5686
5772
  runtime: "python",
5687
5773
  backend: migratedBackend,
@@ -5853,18 +5939,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5853
5939
  const api = this.addonCtx?.api;
5854
5940
  let best;
5855
5941
  if (api) try {
5856
- const caps = await api.platformProbe.getCapabilities.query();
5942
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
5857
5943
  const bs = caps?.bestScore;
5858
5944
  if (bs && bs.runtime === "python") {
5859
5945
  const probeBackend = bs.backend;
5860
5946
  const probeDevice = (() => {
5861
5947
  const hw = caps.hardware;
5862
- if (probeBackend === "coreml") return hw?.npu?.type === "apple-ane" ? "ane" : "all";
5863
- if (probeBackend === "openvino") {
5864
- if (hw?.npu?.type === "intel-npu") return "npu";
5865
- if (hw?.gpu?.type === "intel") return "gpu";
5866
- return "auto";
5867
- }
5948
+ if (probeBackend === "openvino") return defaultDeviceFor("openvino");
5949
+ if (probeBackend === "coreml") return defaultDeviceFor("coreml");
5868
5950
  if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5869
5951
  return "cpu";
5870
5952
  })();
@@ -5874,16 +5956,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5874
5956
  format: backendToFormat(probeBackend),
5875
5957
  device: probeDevice
5876
5958
  };
5877
- } else best = DetectionPipelineProvider.detectBestEngine();
5959
+ } else best = onnxFloorPick();
5878
5960
  } catch {
5879
- best = DetectionPipelineProvider.detectBestEngine();
5961
+ best = onnxFloorPick();
5880
5962
  }
5881
- else best = DetectionPipelineProvider.detectBestEngine();
5963
+ else best = onnxFloorPick();
5882
5964
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5965
+ const rpNode = this.localProbeNodeId();
5883
5966
  await this.writeStore({
5884
- probedBestEngine: probedLabel,
5885
- engineBackend: best.backend,
5886
- engineDevice: best.device ?? "cpu"
5967
+ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
5968
+ [nodeEngineKey("engineBackend", rpNode)]: best.backend,
5969
+ [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
5887
5970
  });
5888
5971
  this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5889
5972
  backend: best.backend,
@@ -6047,7 +6130,7 @@ function buildSchemaSlots(format, modelsDir) {
6047
6130
  id: m.id,
6048
6131
  name: m.name,
6049
6132
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
6050
- downloaded: (0, _camstack_system.isModelDownloaded)(modelsDir, m, f),
6133
+ downloaded: require_model_download_service_C7AjBsX9.isModelDownloaded(modelsDir, m, f),
6051
6134
  sizeMB: entry.sizeMB
6052
6135
  }]))
6053
6136
  })),
@@ -6306,7 +6389,8 @@ var DEFAULT_CONFIG = {
6306
6389
  engineRuntime: "python",
6307
6390
  engineBackend: "onnx",
6308
6391
  engineDevice: "cpu",
6309
- probedBestEngine: ""
6392
+ probedBestEngine: "",
6393
+ activeEngine: ""
6310
6394
  };
6311
6395
  /** Derive the model-format from a backend value. Called by the provider. */
6312
6396
  function backendToFormat(backend) {
@@ -6349,6 +6433,15 @@ var POOL_BOUND_KEYS = [
6349
6433
  ];
6350
6434
  var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6351
6435
  provider = null;
6436
+ /** Last non-null probed hardware PER NODE — reused when the probe transiently
6437
+ * returns null so offered backends / device lists don't collapse. Keyed by
6438
+ * node because the hub addon probes other nodes when serving their config. */
6439
+ lastGoodHardwareByNode = /* @__PURE__ */ new Map();
6440
+ /** This node's effective engine selection, cached from the node-scoped store
6441
+ * so the synchronous `resolveBackendTuning` and the reprobe gate don't read
6442
+ * the cluster-shared bare keys. Refreshed at init + on every config change. */
6443
+ nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
6444
+ nodeProbedBestEngine = "";
6352
6445
  engineMetricsTimer = null;
6353
6446
  /** Snapshot-equality cache for engine-metrics emit. Most ticks
6354
6447
  * the engine inventory is unchanged (no model load/unload), so
@@ -6403,6 +6496,14 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6403
6496
  tooltip: "Re-probe engine"
6404
6497
  }]
6405
6498
  }),
6499
+ this.field({
6500
+ type: "text",
6501
+ key: "activeEngine",
6502
+ label: "Active engine",
6503
+ description: "The runtime/device the inference pool ACTUALLY loaded on this node, with its provisioning state (format: provider/device (state)). If this differs from the selected provider below — e.g. \"onnx/cpu (ready)\" while OpenVINO is selected — the chosen accelerator could not load on this host and the engine fell back to the ONNX-CPU baseline.",
6504
+ readonlyField: true,
6505
+ default: ""
6506
+ }),
6406
6507
  this.field({
6407
6508
  type: "select",
6408
6509
  key: "engineBackend",
@@ -6539,14 +6640,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6539
6640
  * probe is unreachable). Stored backend / device snap back to the registry
6540
6641
  * floor / default when they fall outside the offered set.
6541
6642
  */
6542
- async getGlobalSettings(overlay) {
6543
- const ctx = this.ctxIfReady;
6544
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
6643
+ async getGlobalSettings(overlay, _cap, nodeId) {
6644
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6645
+ const rawStored = await this.resolveUiSettingsStore();
6646
+ if (rawStored === null) return null;
6647
+ const stored = projectNodeEngine(rawStored, targetNode);
6648
+ const storedBackendRaw = stored["engineBackend"];
6649
+ if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6545
6650
  const merged = overlay ? {
6546
6651
  ...stored,
6547
6652
  ...overlay
6548
6653
  } : stored;
6549
- const env = await this.probeHardwareEnv();
6654
+ const env = await this.probeHardwareEnv(targetNode);
6550
6655
  const hardware = env.hardware;
6551
6656
  const offered = supportedRuntimes(env);
6552
6657
  const runtimeBackends = offered.map((id) => ({
@@ -6554,14 +6659,22 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6554
6659
  label: runtimeLabel(id)
6555
6660
  }));
6556
6661
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6557
- const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6662
+ const backend = (() => {
6663
+ const rid = toRuntimeId(storedBackend);
6664
+ if (rid === "onnx") return "onnx";
6665
+ if (offered.includes(rid)) return rid;
6666
+ return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6667
+ })();
6558
6668
  const deviceOptions = runtimeDevices(backend, hardware);
6559
6669
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6560
6670
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6671
+ const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6672
+ const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6561
6673
  const raw = {
6562
6674
  ...merged,
6563
6675
  engineBackend: backend,
6564
- engineDevice: device
6676
+ engineDevice: device,
6677
+ activeEngine
6565
6678
  };
6566
6679
  const schema = this.globalSettingsSchema();
6567
6680
  if (!schema) return { sections: [] };
@@ -6592,15 +6705,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6592
6705
  if (field.type === "slider" && "key" in field) {
6593
6706
  const tuned = tuning[field.key];
6594
6707
  const sliderField = field;
6595
- let patched = typeof tuned === "number" ? {
6708
+ let patchedField = typeof tuned === "number" ? {
6596
6709
  ...sliderField,
6597
6710
  default: tuned
6598
6711
  } : sliderField;
6599
- if (sliderField.key === "concurrency" && backend === "coreml") patched = {
6600
- ...patched,
6712
+ if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
6713
+ ...patchedField,
6601
6714
  max: 4
6602
6715
  };
6603
- return patched;
6716
+ return patchedField;
6604
6717
  }
6605
6718
  return field;
6606
6719
  })
@@ -6617,23 +6730,41 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6617
6730
  * Protected seam — overridable by tests (canned hardware) and reused by the
6618
6731
  * Phase 2 auto-pick path. NEVER reads install state.
6619
6732
  */
6620
- async probeHardwareEnv() {
6621
- const hardware = await this.resolveProbeHardware();
6733
+ /**
6734
+ * Resolve the persisted UI settings store, or `null` when it can't be read
6735
+ * right now (ctx/settings not wired — mid-restart). A readable-but-empty
6736
+ * store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
6737
+ * uses the null signal to avoid serving fabricated empty-store defaults
6738
+ * during the restart window. Seam kept protected so tests can drive the
6739
+ * unreadable vs empty vs populated cases without faking a full AddonContext.
6740
+ */
6741
+ async resolveUiSettingsStore() {
6742
+ const settings = this.ctxIfReady?.settings;
6743
+ if (!settings) return null;
6744
+ return await settings.readAddonStore() ?? {};
6745
+ }
6746
+ async probeHardwareEnv(nodeId) {
6747
+ const node = nodeId ?? this.localNodeId();
6748
+ const hardware = await this.resolveProbeHardware(node);
6749
+ if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
6750
+ const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
6622
6751
  return {
6623
6752
  platform: process.platform,
6624
6753
  arch: process.arch,
6625
- hardware
6754
+ hardware: effective
6626
6755
  };
6627
6756
  }
6628
6757
  /**
6629
- * Fetch the probed hardware from the platform-probe cap. Returns null when
6630
- * the cap is not reachable (caller falls back to the registry's safe minimum).
6758
+ * Fetch the probed hardware from the platform-probe cap for `nodeId` (default
6759
+ * = self). Returns null when the cap is not reachable (caller falls back to
6760
+ * the registry's safe minimum).
6631
6761
  */
6632
- async resolveProbeHardware() {
6762
+ async resolveProbeHardware(nodeId) {
6633
6763
  try {
6634
6764
  const api = this.ctxIfReady?.api;
6635
6765
  if (!api) return null;
6636
- const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6766
+ const node = nodeId ?? this.localNodeId();
6767
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(node)))?.hardware;
6637
6768
  if (!hw) return null;
6638
6769
  return {
6639
6770
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -6644,6 +6775,61 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6644
6775
  }
6645
6776
  }
6646
6777
  /**
6778
+ * Bare node id used to scope the engine cascade in the shared store. MUST
6779
+ * match the provider's `localProbeNodeId()` (kernel.localNodeId, default
6780
+ * 'hub') so the UI write path (this) and the provider read/write path
6781
+ * (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
6782
+ * `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
6783
+ * when kernel.localNodeId was absent, so UI saves landed on a key the
6784
+ * provider never read — silently losing the per-node selection.
6785
+ */
6786
+ localNodeId() {
6787
+ return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
6788
+ }
6789
+ /**
6790
+ * Refresh this node's cached engine selection from the node-scoped store.
6791
+ * `resolveBackendTuning` is synchronous and the reprobe gate runs before the
6792
+ * provider exists, so both read these cached fields instead of the
6793
+ * cluster-shared bare keys (which belong to no single node). Best-effort: a
6794
+ * transiently-unreadable store leaves the last cached values in place.
6795
+ */
6796
+ async refreshNodeEngineFromStore() {
6797
+ const store = await this.resolveUiSettingsStore();
6798
+ if (store === null) return;
6799
+ const node = this.localNodeId();
6800
+ const backend = readNodeEngineValue(store, "engineBackend", node);
6801
+ if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
6802
+ const probed = readNodeEngineValue(store, "probedBestEngine", node);
6803
+ this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
6804
+ }
6805
+ /**
6806
+ * Persist a settings patch, mirroring the engine cascade fields to the TARGET
6807
+ * node's scoped keys so each node keeps an INDEPENDENT engine selection in the
6808
+ * cluster-central store. The hub addon serves writes for every node, so it
6809
+ * scopes by the requested `nodeId` (default self).
6810
+ *
6811
+ * When the target IS this node, `super.updateGlobalSettings` drives the normal
6812
+ * apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
6813
+ * and the bare engine keys it writes are shadowed by the node-scoped keys on
6814
+ * read. When the target is a SIBLING node, we persist the scoped engine keys +
6815
+ * the non-engine bare keys but DON'T run this node's restart/reprovision — the
6816
+ * owning node applies its own engine selection on its next (re)start.
6817
+ */
6818
+ async updateGlobalSettings(patch, nodeId) {
6819
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6820
+ const patchRecord = patch;
6821
+ const scopedEngine = {};
6822
+ for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
6823
+ if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
6824
+ if (targetNode === this.localNodeId()) {
6825
+ await super.updateGlobalSettings(patch, nodeId);
6826
+ return;
6827
+ }
6828
+ const sharedPatch = {};
6829
+ for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
6830
+ if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
6831
+ }
6832
+ /**
6647
6833
  * Resolve the effective pool tuning for the configured backend.
6648
6834
  *
6649
6835
  * Reads the registry's `tuningFor(backend)` and ignores any persisted
@@ -6657,7 +6843,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6657
6843
  * a reason to disagree.
6658
6844
  */
6659
6845
  resolveBackendTuning() {
6660
- const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6846
+ const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
6661
6847
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6662
6848
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6663
6849
  return {
@@ -6676,6 +6862,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6676
6862
  relativePath: ""
6677
6863
  }).catch(() => "camstack-data/models");
6678
6864
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6865
+ await this.refreshNodeEngineFromStore();
6679
6866
  this.pythonAddonDir = resolveAddonPythonDir();
6680
6867
  const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6681
6868
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
@@ -6697,7 +6884,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6697
6884
  });
6698
6885
  await this.provider.init();
6699
6886
  await this.provider.setApi(this.ctx);
6700
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6887
+ if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6701
6888
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6702
6889
  });
6703
6890
  await this.provider.warmPool();
@@ -6749,8 +6936,8 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6749
6936
  /**
6750
6937
  * Proactively install the OpenVINO Python package when Intel hardware
6751
6938
  * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6752
- * provider is constructed, so the module is available when the platform
6753
- * probe runs its next `import openvino.runtime` check.
6939
+ * provider is constructed, so the module is RUNNABLE when the engine is
6940
+ * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
6754
6941
  *
6755
6942
  * Failure is non-fatal: a warning is logged and the addon continues with
6756
6943
  * the onnx-cpu baseline. The hardware query itself is also best-effort —
@@ -6819,6 +7006,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6819
7006
  * lifecycle (engineFactory rebuild on next runPipeline).
6820
7007
  */
6821
7008
  async onConfigChanged() {
7009
+ await this.refreshNodeEngineFromStore();
6822
7010
  if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6823
7011
  this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6824
7012
  });