@camstack/addon-pipeline 1.0.7 → 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 (51) hide show
  1. package/dist/audio-analyzer/index.js +2 -3
  2. package/dist/audio-analyzer/index.mjs +2 -3
  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 +373 -184
  8. package/dist/detection-pipeline/index.mjs +373 -184
  9. package/dist/{dist-CjrjeaDd.mjs → dist-BA6DR_jV.mjs} +128 -5
  10. package/dist/{dist-G45MVm6i.js → dist-BLcTVvol.js} +151 -4
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +1 -1
  14. package/dist/pipeline-runner/index.mjs +1 -1
  15. package/dist/recorder/index.js +3 -3
  16. package/dist/recorder/index.mjs +3 -3
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Tbqpu0v3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs} +3 -3
  19. 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
  20. package/dist/stream-broker/{hostInit-tIev5Gd9.mjs → hostInit-zRy9SzlX.mjs} +3 -3
  21. package/dist/stream-broker/index.js +19 -19
  22. package/dist/stream-broker/index.mjs +19 -19
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C0kKwNX_.js → MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js} +1 -1
  25. package/embed-dist/assets/MotionZonesSettings-NcxxQN8r-CQzEnQoq.js +1 -0
  26. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C2SRtNe6.js → PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js} +1 -1
  27. package/embed-dist/assets/{index-B2LRyXWh.js → index-CSuLwWK-.js} +3 -3
  28. package/embed-dist/index.html +1 -1
  29. package/package.json +1 -1
  30. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  31. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  34. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  35. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  36. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  37. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  38. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  39. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  40. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  41. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  42. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  43. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  44. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  47. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  48. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  49. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  50. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DCsgcqTa.mjs +0 -26
  51. package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CYtJc892.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as EventCategory, C as evaluateZoneRules, F as errMsg, I as BaseAddon, S as detectionPipelineCapability, U as createEvent, W as hydrateSchema, X as sleep, a as COCO_TO_MACRO, i as COCO_80_LABELS, k as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as hfModelUrl } from "../dist-CjrjeaDd.mjs";
2
+ import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-BA6DR_jV.mjs";
3
3
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C7AjBsX9-B0ekM6dF.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
@@ -2158,6 +2158,78 @@ var EngineFactory = class {
2158
2158
  }
2159
2159
  };
2160
2160
  //#endregion
2161
+ //#region src/detection-pipeline/engine-store-keys.ts
2162
+ /**
2163
+ * Per-node scoping for the detection-pipeline engine cascade.
2164
+ *
2165
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
2166
+ * settings-store cap is hub-resident; every node's detection instance reads and
2167
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
2168
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
2169
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
2170
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
2171
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
2172
+ * another node that has no NPU.
2173
+ *
2174
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
2175
+ * Everything else in the store stays shared. A legacy un-scoped value (written
2176
+ * before this change, or by an older build) is read as a migration fallback and
2177
+ * re-persisted under the node-scoped key on the next write.
2178
+ */
2179
+ var ENGINE_CASCADE_KEYS = [
2180
+ "engineBackend",
2181
+ "engineDevice",
2182
+ "probedBestEngine"
2183
+ ];
2184
+ function isEngineCascadeKey(key) {
2185
+ return ENGINE_CASCADE_KEYS.includes(key);
2186
+ }
2187
+ /**
2188
+ * Normalise a raw kernel node id to the bare node id used for scoping.
2189
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
2190
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
2191
+ */
2192
+ function normalizeEngineNodeId(rawNodeId) {
2193
+ const raw = rawNodeId ?? "hub";
2194
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
2195
+ }
2196
+ /** The node-scoped store key for an engine cascade field. */
2197
+ function nodeEngineKey(base, nodeId) {
2198
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
2199
+ }
2200
+ /**
2201
+ * Read an engine cascade value for a node: the node-scoped key if present,
2202
+ * otherwise the legacy un-scoped value (migration), otherwise undefined.
2203
+ */
2204
+ function readNodeEngineValue(store, base, nodeId) {
2205
+ const scoped = store[nodeEngineKey(base, nodeId)];
2206
+ return scoped !== void 0 ? scoped : store[base];
2207
+ }
2208
+ /**
2209
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
2210
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
2211
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
2212
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
2213
+ * node's form).
2214
+ */
2215
+ function projectNodeEngine(store, nodeId) {
2216
+ const out = {};
2217
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
2218
+ for (const key of Object.keys(store)) {
2219
+ const atIdx = key.indexOf("@");
2220
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
2221
+ scopedForAnyNode.add(key);
2222
+ continue;
2223
+ }
2224
+ out[key] = store[key];
2225
+ }
2226
+ for (const base of ENGINE_CASCADE_KEYS) {
2227
+ const value = readNodeEngineValue(store, base, nodeId);
2228
+ if (value !== void 0) out[base] = value;
2229
+ }
2230
+ return out;
2231
+ }
2232
+ //#endregion
2161
2233
  //#region src/detection-pipeline/postprocess/dispatch.ts
2162
2234
  var VALID_KINDS = new Set([
2163
2235
  "detections",
@@ -2197,7 +2269,7 @@ function postprocessYolo(output, stepDef) {
2197
2269
  const letterbox = output.letterbox;
2198
2270
  const dets = [];
2199
2271
  for (let i = 0; i < numBoxes; i++) {
2200
- const cx = tensor[0 * numBoxes + i];
2272
+ const cx = tensor[i];
2201
2273
  const cy = tensor[1 * numBoxes + i];
2202
2274
  const w = tensor[2 * numBoxes + i];
2203
2275
  const h = tensor[3 * numBoxes + i];
@@ -2353,7 +2425,7 @@ function postprocessArcface(output, _stepDef) {
2353
2425
  let sumSq = 0;
2354
2426
  for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
2355
2427
  const norm = Math.sqrt(sumSq);
2356
- const normalized = new Array(tensor.length);
2428
+ const normalized = Array.from({ length: tensor.length });
2357
2429
  for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
2358
2430
  return {
2359
2431
  kind: "embedding",
@@ -3528,34 +3600,69 @@ function walkFieldsForDefaults(fields, out) {
3528
3600
  }
3529
3601
  //#endregion
3530
3602
  //#region src/detection-pipeline/runtimes.ts
3531
- var AUTO = {
3532
- value: "auto",
3533
- label: "Auto"
3534
- };
3535
- var CPU = {
3536
- value: "cpu",
3537
- label: "CPU"
3538
- };
3539
- var RUNTIMES = [
3540
- {
3541
- id: "onnx",
3603
+ var KNOWN_PLATFORMS = [
3604
+ "darwin",
3605
+ "linux",
3606
+ "win32"
3607
+ ];
3608
+ var KNOWN_ARCHES = ["arm64", "x64"];
3609
+ var KNOWN_GPU_TYPES = [
3610
+ "nvidia",
3611
+ "amd",
3612
+ "intel",
3613
+ "apple"
3614
+ ];
3615
+ var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
3616
+ function toKnownPlatform(p) {
3617
+ return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
3618
+ }
3619
+ function toKnownArch(a) {
3620
+ return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
3621
+ }
3622
+ function gpuInfoFrom(hw) {
3623
+ if (!hw.gpu) return null;
3624
+ const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
3625
+ if (!type) return null;
3626
+ return {
3627
+ type,
3628
+ name: ""
3629
+ };
3630
+ }
3631
+ function npuInfoFrom(hw) {
3632
+ if (!hw.npu) return null;
3633
+ const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
3634
+ if (!type) return null;
3635
+ return { type };
3636
+ }
3637
+ function envToHardwareInfo(env) {
3638
+ if (!env.hardware) return null;
3639
+ return {
3640
+ platform: toKnownPlatform(env.platform),
3641
+ arch: toKnownArch(env.arch),
3642
+ cpuModel: "",
3643
+ cpuCores: 0,
3644
+ totalRAM_MB: 0,
3645
+ availableRAM_MB: 0,
3646
+ gpu: gpuInfoFrom(env.hardware),
3647
+ npu: npuInfoFrom(env.hardware)
3648
+ };
3649
+ }
3650
+ function probedToHardwareInfo(hw) {
3651
+ if (!hw) return null;
3652
+ return {
3653
+ platform: toKnownPlatform(process.platform),
3654
+ arch: toKnownArch(process.arch),
3655
+ cpuModel: "",
3656
+ cpuCores: 0,
3657
+ totalRAM_MB: 0,
3658
+ availableRAM_MB: 0,
3659
+ gpu: gpuInfoFrom(hw),
3660
+ npu: npuInfoFrom(hw)
3661
+ };
3662
+ }
3663
+ var RUNTIME_DETAIL = {
3664
+ onnx: {
3542
3665
  label: "ONNX Runtime",
3543
- supports: () => true,
3544
- devices: (hw) => {
3545
- if (!hw) return [CPU];
3546
- const out = [CPU];
3547
- if (hw.gpu?.type === "nvidia") out.push({
3548
- value: "cuda",
3549
- label: "CUDA"
3550
- });
3551
- if (hw.npu?.type === "apple-ane") out.push({
3552
- value: "coreml",
3553
- label: "CoreML EP"
3554
- });
3555
- return out;
3556
- },
3557
- defaultDevice: "cpu",
3558
- modelFormat: "onnx",
3559
3666
  pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3560
3667
  tuning: {
3561
3668
  concurrency: 4,
@@ -3564,25 +3671,8 @@ var RUNTIMES = [
3564
3671
  intraOpThreads: 0
3565
3672
  }
3566
3673
  },
3567
- {
3568
- id: "openvino",
3674
+ openvino: {
3569
3675
  label: "OpenVINO",
3570
- supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3571
- devices: (hw) => {
3572
- if (!hw) return [AUTO];
3573
- const out = [AUTO, CPU];
3574
- if (hw.gpu?.type === "intel") out.push({
3575
- value: "gpu",
3576
- label: "GPU"
3577
- });
3578
- if (hw.npu?.type === "intel-npu") out.push({
3579
- value: "npu",
3580
- label: "NPU"
3581
- });
3582
- return out;
3583
- },
3584
- defaultDevice: "auto",
3585
- modelFormat: "openvino",
3586
3676
  pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3587
3677
  tuning: {
3588
3678
  concurrency: 1,
@@ -3590,29 +3680,8 @@ var RUNTIMES = [
3590
3680
  numStreams: 0
3591
3681
  }
3592
3682
  },
3593
- {
3594
- id: "coreml",
3683
+ coreml: {
3595
3684
  label: "CoreML",
3596
- supports: (env) => env.platform === "darwin",
3597
- devices: (hw) => {
3598
- const all = {
3599
- value: "all",
3600
- label: "All (ANE + GPU + CPU)"
3601
- };
3602
- if (!hw) return [all];
3603
- const out = [all];
3604
- if (hw.npu?.type === "apple-ane") out.push({
3605
- value: "ane",
3606
- label: "Apple Neural Engine"
3607
- });
3608
- out.push({
3609
- value: "gpu",
3610
- label: "GPU"
3611
- }, CPU);
3612
- return out;
3613
- },
3614
- defaultDevice: "all",
3615
- modelFormat: "coreml",
3616
3685
  pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3617
3686
  tuning: {
3618
3687
  concurrency: 1,
@@ -3622,32 +3691,50 @@ var RUNTIMES = [
3622
3691
  numWorkers: 1
3623
3692
  }
3624
3693
  }
3625
- ];
3626
- function def(id) {
3627
- const d = RUNTIMES.find((r) => r.id === id);
3628
- if (!d) throw new Error(`Unknown runtime: ${id}`);
3629
- return d;
3630
- }
3694
+ };
3695
+ /**
3696
+ * Returns the list of supported runtime IDs for the given hardware env.
3697
+ * Delegates to `@camstack/types` `supportedRuntimes`.
3698
+ */
3631
3699
  function supportedRuntimes(env) {
3632
- return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3700
+ return supportedRuntimes$1(envToHardwareInfo(env));
3633
3701
  }
3702
+ /**
3703
+ * Returns the device options for a given runtime and probed hardware.
3704
+ * Delegates to `@camstack/types` `runtimeDevices`.
3705
+ */
3634
3706
  function runtimeDevices(id, hardware) {
3635
- return def(id).devices(hardware);
3707
+ return runtimeDevices$1(id, probedToHardwareInfo(hardware));
3636
3708
  }
3709
+ /**
3710
+ * Returns the default device string for a given runtime.
3711
+ * Delegates to `@camstack/types` `defaultDeviceFor`.
3712
+ */
3637
3713
  function defaultDeviceFor(id) {
3638
- return def(id).defaultDevice;
3639
- }
3640
- function pythonRequirementsFor(id) {
3641
- return def(id).pythonRequirements;
3714
+ return defaultDeviceFor$1(id);
3642
3715
  }
3716
+ /** Model format for each inference runtime supported by the detection pipeline. */
3717
+ var RUNTIME_FORMAT = {
3718
+ onnx: "onnx",
3719
+ openvino: "openvino",
3720
+ coreml: "coreml"
3721
+ };
3722
+ /**
3723
+ * Returns the model format required for a given runtime.
3724
+ * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
3725
+ * matching the engine-provisioner's own ModelFormat type.
3726
+ */
3643
3727
  function modelFormatFor(id) {
3644
- return def(id).modelFormat;
3728
+ return RUNTIME_FORMAT[id];
3729
+ }
3730
+ function pythonRequirementsFor(id) {
3731
+ return RUNTIME_DETAIL[id].pythonRequirements;
3645
3732
  }
3646
3733
  function tuningFor(id) {
3647
- return def(id).tuning;
3734
+ return RUNTIME_DETAIL[id].tuning;
3648
3735
  }
3649
3736
  function runtimeLabel(id) {
3650
- return def(id).label;
3737
+ return RUNTIME_DETAIL[id].label;
3651
3738
  }
3652
3739
  /**
3653
3740
  * Proactive-install hint kept for back-compat (re-exported from index.ts).
@@ -3988,6 +4075,27 @@ function toProbedHardware(hw) {
3988
4075
  gpu: hw.gpu ? { type: hw.gpu.type } : null
3989
4076
  };
3990
4077
  }
4078
+ var ONNX_FLOOR = {
4079
+ runtime: "python",
4080
+ backend: "onnx",
4081
+ format: "onnx",
4082
+ device: "cpu"
4083
+ };
4084
+ /**
4085
+ * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
4086
+ * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
4087
+ * result is identical (onnx / cpu) but is now derived through the shared rules
4088
+ * instead of duplicated inline.
4089
+ */
4090
+ function onnxFloorPick() {
4091
+ const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
4092
+ return {
4093
+ runtime: "python",
4094
+ backend: pick.runtimeId,
4095
+ format: modelFormatFor(pick.runtimeId),
4096
+ device: pick.device
4097
+ };
4098
+ }
3991
4099
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3992
4100
  modelsDir;
3993
4101
  eventBus;
@@ -4077,7 +4185,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4077
4185
  this.readStore = () => settings.readAddonStore();
4078
4186
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4079
4187
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4080
- this.currentEngine = DetectionPipelineProvider.detectBestEngine();
4188
+ this.currentEngine = ONNX_FLOOR;
4081
4189
  this.log.info("Engine selected (default)", { meta: {
4082
4190
  runtime: this.currentEngine.runtime,
4083
4191
  backend: this.currentEngine.backend,
@@ -4143,62 +4251,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4143
4251
  isReady() {
4144
4252
  return this.ready;
4145
4253
  }
4146
- /** Detect the best inference engine for this platform. */
4147
- static detectBestEngine() {
4148
- const platform = process.platform;
4149
- const arch = process.arch;
4150
- if (platform === "darwin" && arch === "arm64") return {
4151
- runtime: "python",
4152
- backend: "coreml",
4153
- format: "coreml",
4154
- device: "all"
4155
- };
4156
- if (platform === "darwin") return {
4157
- runtime: "python",
4158
- backend: "coreml",
4159
- format: "coreml",
4160
- device: "gpu"
4161
- };
4162
- try {
4163
- const { execFileSync } = __require("node:child_process");
4164
- execFileSync("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"], {
4165
- timeout: 3e3,
4166
- stdio: "pipe"
4167
- });
4168
- return {
4169
- runtime: "python",
4170
- backend: "onnx",
4171
- format: "onnx",
4172
- device: "cuda"
4173
- };
4174
- } catch {}
4175
- try {
4176
- const fsmod = __require("node:fs");
4177
- const isIntel = __require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
4178
- const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
4179
- const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
4180
- if (isIntel && (hasIgpu || hasNpu)) return {
4181
- runtime: "python",
4182
- backend: "openvino",
4183
- format: "openvino",
4184
- device: "auto"
4185
- };
4186
- } catch {}
4187
- return {
4188
- runtime: "python",
4189
- backend: "onnx",
4190
- format: "onnx",
4191
- device: "cpu"
4192
- };
4193
- }
4194
4254
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
4195
4255
  async setApi(addonCtx) {
4196
4256
  this.addonCtx = addonCtx;
4197
- if (this.needsAutoPick) {
4257
+ if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
4198
4258
  await this.autoPickAndPersist();
4199
4259
  this.needsAutoPick = false;
4260
+ this.startProvisioningForCurrentEngine();
4261
+ } else {
4262
+ this.startProvisioningForCurrentEngine();
4263
+ const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4264
+ if (state !== "ready") return;
4265
+ unsubscribe();
4266
+ if (!this.needsAutoPick) return;
4267
+ this.autoPickAndPersist().then(() => {
4268
+ this.needsAutoPick = false;
4269
+ this.startProvisioningForCurrentEngine();
4270
+ });
4271
+ });
4272
+ this.addonCtx.addDisposer(unsubscribe);
4200
4273
  }
4201
- this.startProvisioningForCurrentEngine();
4274
+ else this.startProvisioningForCurrentEngine();
4202
4275
  }
4203
4276
  /**
4204
4277
  * Auto-pick the best supported runtime at first boot (no stored engine).
@@ -4213,7 +4286,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4213
4286
  try {
4214
4287
  const api = this.addonCtx?.api;
4215
4288
  if (api) {
4216
- const caps = await api.platformProbe.getCapabilities.query();
4289
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
4217
4290
  hardware = caps?.hardware ?? null;
4218
4291
  const bs = caps?.bestScore;
4219
4292
  if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
@@ -4227,9 +4300,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4227
4300
  device: pick.device
4228
4301
  };
4229
4302
  this.currentEngine = engine;
4303
+ const apNode = this.localProbeNodeId();
4230
4304
  await this.writeStore({
4231
- engineBackend: pick.runtimeId,
4232
- engineDevice: pick.device
4305
+ [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4306
+ [nodeEngineKey("engineDevice", apNode)]: pick.device
4233
4307
  });
4234
4308
  this.log.info("Auto-picked engine at first boot", { meta: {
4235
4309
  backend: pick.runtimeId,
@@ -4402,11 +4476,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4402
4476
  * callers fall back to the registry's safe minimum. The engine OFFER derives
4403
4477
  * from hardware ONLY — install state (probe `scores`) never gates it.
4404
4478
  */
4479
+ /**
4480
+ * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4481
+ * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4482
+ * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4483
+ * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4484
+ * have) and pin a device the node can't run, breaking provisioning.
4485
+ */
4486
+ localProbeNodeId() {
4487
+ const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
4488
+ return raw.includes("/") ? raw.split("/")[0] : raw;
4489
+ }
4405
4490
  async fetchProbeGatingData() {
4406
4491
  try {
4407
4492
  const api = this.addonCtx?.api;
4408
4493
  if (!api) return { hardware: null };
4409
- return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
4494
+ return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4410
4495
  } catch {
4411
4496
  return { hardware: null };
4412
4497
  }
@@ -5471,7 +5556,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5471
5556
  this.engineFactory = null;
5472
5557
  this.executor = null;
5473
5558
  }
5474
- for (const id of [...this.deviceProxies.keys()]) this.releaseDeviceProxy(id);
5559
+ for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5475
5560
  }
5476
5561
  /**
5477
5562
  * Resolve and cache a {@link DeviceProxy} for the given camera. Pins
@@ -5655,25 +5740,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5655
5740
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
5656
5741
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
5657
5742
  * Returns null when neither source has anything; the caller keeps
5658
- * whatever `detectBestEngine()` picked at construction.
5743
+ * the onnx floor set at construction until autoPickAndPersist() runs.
5659
5744
  */
5660
5745
  async loadEngine() {
5661
5746
  const store = await this.readStore();
5662
5747
  const storedRuntime = store["engineRuntime"];
5663
- const storedBackend = store["engineBackend"];
5748
+ const node = this.localProbeNodeId();
5749
+ const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5664
5750
  if (typeof storedBackend === "string" && storedBackend.length > 0) {
5665
5751
  const backend = storedBackend;
5666
- const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5667
- const detected = DetectionPipelineProvider.detectBestEngine();
5752
+ const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5753
+ const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5754
+ const floor = onnxFloorPick();
5668
5755
  const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5669
5756
  if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5670
- this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5757
+ this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
5671
5758
  stored: migratedBackend,
5672
- fallback: `${detected.backend}`
5759
+ fallback: `${floor.backend}`
5673
5760
  } });
5674
- return detected;
5761
+ return floor;
5675
5762
  }
5676
- const device = storedDevice || detected.device;
5763
+ const device = storedDevice || floor.device;
5677
5764
  return {
5678
5765
  runtime: "python",
5679
5766
  backend: migratedBackend,
@@ -5845,18 +5932,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5845
5932
  const api = this.addonCtx?.api;
5846
5933
  let best;
5847
5934
  if (api) try {
5848
- const caps = await api.platformProbe.getCapabilities.query();
5935
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
5849
5936
  const bs = caps?.bestScore;
5850
5937
  if (bs && bs.runtime === "python") {
5851
5938
  const probeBackend = bs.backend;
5852
5939
  const probeDevice = (() => {
5853
5940
  const hw = caps.hardware;
5854
- if (probeBackend === "coreml") return hw?.npu?.type === "apple-ane" ? "ane" : "all";
5855
- if (probeBackend === "openvino") {
5856
- if (hw?.npu?.type === "intel-npu") return "npu";
5857
- if (hw?.gpu?.type === "intel") return "gpu";
5858
- return "auto";
5859
- }
5941
+ if (probeBackend === "openvino") return defaultDeviceFor("openvino");
5942
+ if (probeBackend === "coreml") return defaultDeviceFor("coreml");
5860
5943
  if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5861
5944
  return "cpu";
5862
5945
  })();
@@ -5866,16 +5949,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5866
5949
  format: backendToFormat(probeBackend),
5867
5950
  device: probeDevice
5868
5951
  };
5869
- } else best = DetectionPipelineProvider.detectBestEngine();
5952
+ } else best = onnxFloorPick();
5870
5953
  } catch {
5871
- best = DetectionPipelineProvider.detectBestEngine();
5954
+ best = onnxFloorPick();
5872
5955
  }
5873
- else best = DetectionPipelineProvider.detectBestEngine();
5956
+ else best = onnxFloorPick();
5874
5957
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5958
+ const rpNode = this.localProbeNodeId();
5875
5959
  await this.writeStore({
5876
- probedBestEngine: probedLabel,
5877
- engineBackend: best.backend,
5878
- engineDevice: best.device ?? "cpu"
5960
+ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
5961
+ [nodeEngineKey("engineBackend", rpNode)]: best.backend,
5962
+ [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
5879
5963
  });
5880
5964
  this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5881
5965
  backend: best.backend,
@@ -6298,7 +6382,8 @@ var DEFAULT_CONFIG = {
6298
6382
  engineRuntime: "python",
6299
6383
  engineBackend: "onnx",
6300
6384
  engineDevice: "cpu",
6301
- probedBestEngine: ""
6385
+ probedBestEngine: "",
6386
+ activeEngine: ""
6302
6387
  };
6303
6388
  /** Derive the model-format from a backend value. Called by the provider. */
6304
6389
  function backendToFormat(backend) {
@@ -6341,6 +6426,15 @@ var POOL_BOUND_KEYS = [
6341
6426
  ];
6342
6427
  var DetectionPipelineAddon = class extends BaseAddon {
6343
6428
  provider = null;
6429
+ /** Last non-null probed hardware PER NODE — reused when the probe transiently
6430
+ * returns null so offered backends / device lists don't collapse. Keyed by
6431
+ * node because the hub addon probes other nodes when serving their config. */
6432
+ lastGoodHardwareByNode = /* @__PURE__ */ new Map();
6433
+ /** This node's effective engine selection, cached from the node-scoped store
6434
+ * so the synchronous `resolveBackendTuning` and the reprobe gate don't read
6435
+ * the cluster-shared bare keys. Refreshed at init + on every config change. */
6436
+ nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
6437
+ nodeProbedBestEngine = "";
6344
6438
  engineMetricsTimer = null;
6345
6439
  /** Snapshot-equality cache for engine-metrics emit. Most ticks
6346
6440
  * the engine inventory is unchanged (no model load/unload), so
@@ -6395,6 +6489,14 @@ var DetectionPipelineAddon = class extends BaseAddon {
6395
6489
  tooltip: "Re-probe engine"
6396
6490
  }]
6397
6491
  }),
6492
+ this.field({
6493
+ type: "text",
6494
+ key: "activeEngine",
6495
+ label: "Active engine",
6496
+ 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.",
6497
+ readonlyField: true,
6498
+ default: ""
6499
+ }),
6398
6500
  this.field({
6399
6501
  type: "select",
6400
6502
  key: "engineBackend",
@@ -6531,14 +6633,18 @@ var DetectionPipelineAddon = class extends BaseAddon {
6531
6633
  * probe is unreachable). Stored backend / device snap back to the registry
6532
6634
  * floor / default when they fall outside the offered set.
6533
6635
  */
6534
- async getGlobalSettings(overlay) {
6535
- const ctx = this.ctxIfReady;
6536
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
6636
+ async getGlobalSettings(overlay, _cap, nodeId) {
6637
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6638
+ const rawStored = await this.resolveUiSettingsStore();
6639
+ if (rawStored === null) return null;
6640
+ const stored = projectNodeEngine(rawStored, targetNode);
6641
+ const storedBackendRaw = stored["engineBackend"];
6642
+ if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6537
6643
  const merged = overlay ? {
6538
6644
  ...stored,
6539
6645
  ...overlay
6540
6646
  } : stored;
6541
- const env = await this.probeHardwareEnv();
6647
+ const env = await this.probeHardwareEnv(targetNode);
6542
6648
  const hardware = env.hardware;
6543
6649
  const offered = supportedRuntimes(env);
6544
6650
  const runtimeBackends = offered.map((id) => ({
@@ -6546,14 +6652,22 @@ var DetectionPipelineAddon = class extends BaseAddon {
6546
6652
  label: runtimeLabel(id)
6547
6653
  }));
6548
6654
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6549
- const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6655
+ const backend = (() => {
6656
+ const rid = toRuntimeId(storedBackend);
6657
+ if (rid === "onnx") return "onnx";
6658
+ if (offered.includes(rid)) return rid;
6659
+ return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6660
+ })();
6550
6661
  const deviceOptions = runtimeDevices(backend, hardware);
6551
6662
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6552
6663
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6664
+ const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6665
+ const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6553
6666
  const raw = {
6554
6667
  ...merged,
6555
6668
  engineBackend: backend,
6556
- engineDevice: device
6669
+ engineDevice: device,
6670
+ activeEngine
6557
6671
  };
6558
6672
  const schema = this.globalSettingsSchema();
6559
6673
  if (!schema) return { sections: [] };
@@ -6584,15 +6698,15 @@ var DetectionPipelineAddon = class extends BaseAddon {
6584
6698
  if (field.type === "slider" && "key" in field) {
6585
6699
  const tuned = tuning[field.key];
6586
6700
  const sliderField = field;
6587
- let patched = typeof tuned === "number" ? {
6701
+ let patchedField = typeof tuned === "number" ? {
6588
6702
  ...sliderField,
6589
6703
  default: tuned
6590
6704
  } : sliderField;
6591
- if (sliderField.key === "concurrency" && backend === "coreml") patched = {
6592
- ...patched,
6705
+ if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
6706
+ ...patchedField,
6593
6707
  max: 4
6594
6708
  };
6595
- return patched;
6709
+ return patchedField;
6596
6710
  }
6597
6711
  return field;
6598
6712
  })
@@ -6609,23 +6723,41 @@ var DetectionPipelineAddon = class extends BaseAddon {
6609
6723
  * Protected seam — overridable by tests (canned hardware) and reused by the
6610
6724
  * Phase 2 auto-pick path. NEVER reads install state.
6611
6725
  */
6612
- async probeHardwareEnv() {
6613
- const hardware = await this.resolveProbeHardware();
6726
+ /**
6727
+ * Resolve the persisted UI settings store, or `null` when it can't be read
6728
+ * right now (ctx/settings not wired — mid-restart). A readable-but-empty
6729
+ * store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
6730
+ * uses the null signal to avoid serving fabricated empty-store defaults
6731
+ * during the restart window. Seam kept protected so tests can drive the
6732
+ * unreadable vs empty vs populated cases without faking a full AddonContext.
6733
+ */
6734
+ async resolveUiSettingsStore() {
6735
+ const settings = this.ctxIfReady?.settings;
6736
+ if (!settings) return null;
6737
+ return await settings.readAddonStore() ?? {};
6738
+ }
6739
+ async probeHardwareEnv(nodeId) {
6740
+ const node = nodeId ?? this.localNodeId();
6741
+ const hardware = await this.resolveProbeHardware(node);
6742
+ if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
6743
+ const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
6614
6744
  return {
6615
6745
  platform: process.platform,
6616
6746
  arch: process.arch,
6617
- hardware
6747
+ hardware: effective
6618
6748
  };
6619
6749
  }
6620
6750
  /**
6621
- * Fetch the probed hardware from the platform-probe cap. Returns null when
6622
- * the cap is not reachable (caller falls back to the registry's safe minimum).
6751
+ * Fetch the probed hardware from the platform-probe cap for `nodeId` (default
6752
+ * = self). Returns null when the cap is not reachable (caller falls back to
6753
+ * the registry's safe minimum).
6623
6754
  */
6624
- async resolveProbeHardware() {
6755
+ async resolveProbeHardware(nodeId) {
6625
6756
  try {
6626
6757
  const api = this.ctxIfReady?.api;
6627
6758
  if (!api) return null;
6628
- const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6759
+ const node = nodeId ?? this.localNodeId();
6760
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, nodePin(node)))?.hardware;
6629
6761
  if (!hw) return null;
6630
6762
  return {
6631
6763
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -6636,6 +6768,61 @@ var DetectionPipelineAddon = class extends BaseAddon {
6636
6768
  }
6637
6769
  }
6638
6770
  /**
6771
+ * Bare node id used to scope the engine cascade in the shared store. MUST
6772
+ * match the provider's `localProbeNodeId()` (kernel.localNodeId, default
6773
+ * 'hub') so the UI write path (this) and the provider read/write path
6774
+ * (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
6775
+ * `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
6776
+ * when kernel.localNodeId was absent, so UI saves landed on a key the
6777
+ * provider never read — silently losing the per-node selection.
6778
+ */
6779
+ localNodeId() {
6780
+ return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
6781
+ }
6782
+ /**
6783
+ * Refresh this node's cached engine selection from the node-scoped store.
6784
+ * `resolveBackendTuning` is synchronous and the reprobe gate runs before the
6785
+ * provider exists, so both read these cached fields instead of the
6786
+ * cluster-shared bare keys (which belong to no single node). Best-effort: a
6787
+ * transiently-unreadable store leaves the last cached values in place.
6788
+ */
6789
+ async refreshNodeEngineFromStore() {
6790
+ const store = await this.resolveUiSettingsStore();
6791
+ if (store === null) return;
6792
+ const node = this.localNodeId();
6793
+ const backend = readNodeEngineValue(store, "engineBackend", node);
6794
+ if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
6795
+ const probed = readNodeEngineValue(store, "probedBestEngine", node);
6796
+ this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
6797
+ }
6798
+ /**
6799
+ * Persist a settings patch, mirroring the engine cascade fields to the TARGET
6800
+ * node's scoped keys so each node keeps an INDEPENDENT engine selection in the
6801
+ * cluster-central store. The hub addon serves writes for every node, so it
6802
+ * scopes by the requested `nodeId` (default self).
6803
+ *
6804
+ * When the target IS this node, `super.updateGlobalSettings` drives the normal
6805
+ * apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
6806
+ * and the bare engine keys it writes are shadowed by the node-scoped keys on
6807
+ * read. When the target is a SIBLING node, we persist the scoped engine keys +
6808
+ * the non-engine bare keys but DON'T run this node's restart/reprovision — the
6809
+ * owning node applies its own engine selection on its next (re)start.
6810
+ */
6811
+ async updateGlobalSettings(patch, nodeId) {
6812
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6813
+ const patchRecord = patch;
6814
+ const scopedEngine = {};
6815
+ for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
6816
+ if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
6817
+ if (targetNode === this.localNodeId()) {
6818
+ await super.updateGlobalSettings(patch, nodeId);
6819
+ return;
6820
+ }
6821
+ const sharedPatch = {};
6822
+ for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
6823
+ if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
6824
+ }
6825
+ /**
6639
6826
  * Resolve the effective pool tuning for the configured backend.
6640
6827
  *
6641
6828
  * Reads the registry's `tuningFor(backend)` and ignores any persisted
@@ -6649,7 +6836,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6649
6836
  * a reason to disagree.
6650
6837
  */
6651
6838
  resolveBackendTuning() {
6652
- const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6839
+ const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
6653
6840
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6654
6841
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6655
6842
  return {
@@ -6668,6 +6855,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6668
6855
  relativePath: ""
6669
6856
  }).catch(() => "camstack-data/models");
6670
6857
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6858
+ await this.refreshNodeEngineFromStore();
6671
6859
  this.pythonAddonDir = resolveAddonPythonDir();
6672
6860
  const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6673
6861
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
@@ -6689,7 +6877,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6689
6877
  });
6690
6878
  await this.provider.init();
6691
6879
  await this.provider.setApi(this.ctx);
6692
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6880
+ if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6693
6881
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6694
6882
  });
6695
6883
  await this.provider.warmPool();
@@ -6741,8 +6929,8 @@ var DetectionPipelineAddon = class extends BaseAddon {
6741
6929
  /**
6742
6930
  * Proactively install the OpenVINO Python package when Intel hardware
6743
6931
  * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6744
- * provider is constructed, so the module is available when the platform
6745
- * probe runs its next `import openvino.runtime` check.
6932
+ * provider is constructed, so the module is RUNNABLE when the engine is
6933
+ * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
6746
6934
  *
6747
6935
  * Failure is non-fatal: a warning is logged and the addon continues with
6748
6936
  * the onnx-cpu baseline. The hardware query itself is also best-effort —
@@ -6811,6 +6999,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6811
6999
  * lifecycle (engineFactory rebuild on next runPipeline).
6812
7000
  */
6813
7001
  async onConfigChanged() {
7002
+ await this.refreshNodeEngineFromStore();
6814
7003
  if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6815
7004
  this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6816
7005
  });