@camstack/addon-pipeline 1.1.28 → 1.1.30

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 (39) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/audio-codec-ffmpeg/index.js +1 -5
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -5
  5. package/dist/decoder-ffmpeg/index.js +102 -352
  6. package/dist/decoder-ffmpeg/index.mjs +82 -332
  7. package/dist/decoder-nodeav/index.js +6 -4
  8. package/dist/decoder-nodeav/index.mjs +6 -4
  9. package/dist/detection-pipeline/index.js +254 -186
  10. package/dist/detection-pipeline/index.mjs +254 -186
  11. package/dist/{dist-Biq62zt4.js → dist-Cwc0TUQr.js} +7 -42
  12. package/dist/{dist-C6_wgXqF.mjs → dist-DjuGmyG9.mjs} +7 -42
  13. package/dist/ffmpeg-args-C5GPp8Cw.mjs +323 -0
  14. package/dist/ffmpeg-args-D6h1edXK.js +418 -0
  15. package/dist/frame-dropper-AjheBGMG.mjs +22 -0
  16. package/dist/frame-dropper-DKLM6pMz.js +27 -0
  17. package/dist/{frame-handle-plane-BIoY6nRV.mjs → frame-handle-plane-Bkxz-TTD.mjs} +1 -1
  18. package/dist/{frame-handle-plane-D6BzyEgy.js → frame-handle-plane-DQNCTrpC.js} +1 -1
  19. package/dist/{frame-ring-sink-9J0wCdLF.js → frame-ring-sink-8LLV-cvH.js} +1 -1
  20. package/dist/{frame-ring-sink-Cs9vby6v.mjs → frame-ring-sink-ClEWjiRU.mjs} +1 -1
  21. package/dist/motion-wasm/index.js +1 -1
  22. package/dist/motion-wasm/index.mjs +1 -1
  23. package/dist/pipeline-runner/index.js +2 -2
  24. package/dist/pipeline-runner/index.mjs +2 -2
  25. package/dist/recorder/index.js +1 -1
  26. package/dist/recorder/index.mjs +1 -1
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  28. package/dist/stream-broker/{hostInit-C0SuwQhL.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  29. package/dist/stream-broker/index.js +168 -121
  30. package/dist/stream-broker/index.mjs +159 -112
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BSCcPdPf.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  35. package/embed-dist/assets/{index-CeZV1B-2.js → index-CpUy8OIE.js} +9 -9
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  39. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, j as nodePin, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-C6_wgXqF.mjs";
2
+ import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-DjuGmyG9.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
@@ -8,87 +8,6 @@ import * as path$1 from "node:path";
8
8
  import * as os from "node:os";
9
9
  import { spawn } from "node:child_process";
10
10
  import sharp from "sharp";
11
- //#region src/detection-pipeline/engine-store-keys.ts
12
- /**
13
- * Per-node scoping for the detection-pipeline engine cascade.
14
- *
15
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
16
- * settings-store cap is hub-resident; every node's detection instance reads and
17
- * writes the same keys). That is correct for node-agnostic settings (pipeline
18
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
19
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
20
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
21
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
22
- * another node that has no NPU.
23
- *
24
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
25
- * Everything else in the store stays shared. There is NO fallback to a legacy
26
- * un-scoped value for ANY node (the hub included) and no in-code migration: a
27
- * bare key (written before scoping, or by an older build) is inert — invisible
28
- * to every node. A node with no scoped key reads `undefined`, which drives its
29
- * OWN platform auto-pick (`autoPickAndPersist` / `reprobeEngine` persist the
30
- * node's own probe under `<key>@<node>`). Serving a bare value to any node
31
- * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
32
- * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
33
- * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
34
- */
35
- var ENGINE_CASCADE_KEYS = [
36
- "engineBackend",
37
- "engineDevice",
38
- "probedBestEngine"
39
- ];
40
- function isEngineCascadeKey(key) {
41
- return ENGINE_CASCADE_KEYS.includes(key);
42
- }
43
- /**
44
- * Normalise a raw kernel node id to the bare node id used for scoping.
45
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
46
- * per-NODE, so strip the addon segment. Falls back to `hub`.
47
- */
48
- function normalizeEngineNodeId(rawNodeId) {
49
- const raw = rawNodeId ?? "hub";
50
- return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
51
- }
52
- /** The node-scoped store key for an engine cascade field. */
53
- function nodeEngineKey(base, nodeId) {
54
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
55
- }
56
- /**
57
- * Read an engine cascade value for a node: the node-scoped key if present,
58
- * otherwise `undefined` — for EVERY node, the hub included. There is no
59
- * fallback to the legacy un-scoped key: a bare value in the store is inert.
60
- * A node with no scoped key gets `undefined` so its own platform-driven
61
- * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
62
- * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
63
- */
64
- function readNodeEngineValue(store, base, nodeId) {
65
- return store[nodeEngineKey(base, nodeId)];
66
- }
67
- /**
68
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
69
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
70
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
71
- * keys for OTHER nodes are dropped from the projection (not relevant to this
72
- * node's form).
73
- */
74
- function projectNodeEngine(store, nodeId) {
75
- const out = {};
76
- const scopedForAnyNode = /* @__PURE__ */ new Set();
77
- for (const key of Object.keys(store)) {
78
- const atIdx = key.indexOf("@");
79
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
80
- scopedForAnyNode.add(key);
81
- continue;
82
- }
83
- out[key] = store[key];
84
- }
85
- for (const base of ENGINE_CASCADE_KEYS) {
86
- const value = readNodeEngineValue(store, base, nodeId);
87
- if (value !== void 0) out[base] = value;
88
- }
89
- return out;
90
- }
91
- //#endregion
92
11
  //#region src/detection-pipeline/runtimes.ts
93
12
  var KNOWN_PLATFORMS = [
94
13
  "darwin",
@@ -260,6 +179,88 @@ function pickBestRuntime(env, bestBackendHint) {
260
179
  };
261
180
  }
262
181
  //#endregion
182
+ //#region src/detection-pipeline/engine-store-keys.ts
183
+ /**
184
+ * Per-node scoping for the detection-pipeline engine cascade.
185
+ *
186
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
187
+ * settings-store cap is hub-resident; every node's detection instance reads and
188
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
189
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
190
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
191
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
192
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
193
+ * another node that has no NPU.
194
+ *
195
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
196
+ * Everything else in the store stays shared. There is NO fallback to a legacy
197
+ * un-scoped value for ANY node (the hub included) and no in-code migration: a
198
+ * bare key (written before scoping, or by an older build) is inert — invisible
199
+ * to every node. A node with no scoped key reads `undefined`, which drives its
200
+ * OWN live resolve (`resolveAutoEngine` picks the node's runtime from the local
201
+ * kernel each boot; `reprobeEngine` persists an explicit probe under
202
+ * `<key>@<node>`). Serving a bare value to any node
203
+ * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
204
+ * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
205
+ * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
206
+ */
207
+ var ENGINE_CASCADE_KEYS = [
208
+ "engineBackend",
209
+ "engineDevice",
210
+ "probedBestEngine"
211
+ ];
212
+ function isEngineCascadeKey(key) {
213
+ return ENGINE_CASCADE_KEYS.includes(key);
214
+ }
215
+ /**
216
+ * Normalise a raw kernel node id to the bare node id used for scoping.
217
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
218
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
219
+ */
220
+ function normalizeEngineNodeId(rawNodeId) {
221
+ const raw = rawNodeId ?? "hub";
222
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
223
+ }
224
+ /** The node-scoped store key for an engine cascade field. */
225
+ function nodeEngineKey(base, nodeId) {
226
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
227
+ }
228
+ /**
229
+ * Read an engine cascade value for a node: the node-scoped key if present,
230
+ * otherwise `undefined` — for EVERY node, the hub included. There is no
231
+ * fallback to the legacy un-scoped key: a bare value in the store is inert.
232
+ * A node with no scoped key gets `undefined` so its own platform-driven
233
+ * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
234
+ * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
235
+ */
236
+ function readNodeEngineValue(store, base, nodeId) {
237
+ return store[nodeEngineKey(base, nodeId)];
238
+ }
239
+ /**
240
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
241
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
242
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
243
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
244
+ * node's form).
245
+ */
246
+ function projectNodeEngine(store, nodeId) {
247
+ const out = {};
248
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
249
+ for (const key of Object.keys(store)) {
250
+ const atIdx = key.indexOf("@");
251
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
252
+ scopedForAnyNode.add(key);
253
+ continue;
254
+ }
255
+ out[key] = store[key];
256
+ }
257
+ for (const base of ENGINE_CASCADE_KEYS) {
258
+ const value = readNodeEngineValue(store, base, nodeId);
259
+ if (value !== void 0) out[base] = value;
260
+ }
261
+ return out;
262
+ }
263
+ //#endregion
263
264
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
264
265
  /**
265
266
  * SharedInferencePool — TypeScript wrapper for inference_pool.py.
@@ -328,6 +329,51 @@ var PoolHandle = class {
328
329
  }
329
330
  async dispose() {}
330
331
  };
332
+ /**
333
+ * How long to wait for a worker to exit on SIGTERM before escalating to
334
+ * SIGKILL. A worker idle between frames exits well under this; only a worker
335
+ * stuck in a native inference call (openvino/onnx C++) needs the escalation.
336
+ */
337
+ var POOL_WORKER_TERM_GRACE_MS = 2e3;
338
+ /**
339
+ * Terminate a spawned child GRACEFULLY THEN FORCEFULLY: end stdin, send
340
+ * SIGTERM, and if the process hasn't exited within `graceMs`, send SIGKILL.
341
+ * Resolves once the process has exited (or was already dead).
342
+ *
343
+ * The old teardown sent SIGTERM and dropped the reference immediately — a
344
+ * worker busy in a native inference loop ignores SIGTERM and ORPHANS at full
345
+ * CPU (observed after an engine re-spin). SIGKILL cannot be ignored, so the
346
+ * escalation guarantees the process dies. Exported for tests.
347
+ */
348
+ async function terminateChild(proc, graceMs) {
349
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
350
+ try {
351
+ proc.stdin?.end();
352
+ } catch {}
353
+ await new Promise((resolve) => {
354
+ let settled = false;
355
+ let timer = null;
356
+ const done = () => {
357
+ if (settled) return;
358
+ settled = true;
359
+ if (timer) clearTimeout(timer);
360
+ resolve();
361
+ };
362
+ proc.once("exit", done);
363
+ try {
364
+ proc.kill("SIGTERM");
365
+ } catch {
366
+ done();
367
+ return;
368
+ }
369
+ timer = setTimeout(() => {
370
+ try {
371
+ proc.kill("SIGKILL");
372
+ } catch {}
373
+ done();
374
+ }, graceMs);
375
+ });
376
+ }
331
377
  var PoolWorker = class {
332
378
  process = null;
333
379
  receiveBuffer = Buffer.alloc(0);
@@ -483,12 +529,11 @@ var PoolWorker = class {
483
529
  return await this.dispatch(MSG_COMMAND, payload);
484
530
  }
485
531
  async dispose() {
486
- if (this.process) {
487
- this.process.stdin?.end();
488
- this.process.kill("SIGTERM");
489
- this.process = null;
490
- this.ready = false;
491
- }
532
+ const proc = this.process;
533
+ if (!proc) return;
534
+ this.process = null;
535
+ this.ready = false;
536
+ await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
492
537
  }
493
538
  dispatch(msgType, payload) {
494
539
  const reqId = this.allocRequestId();
@@ -4572,56 +4617,53 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4572
4617
  error: errMsg(err)
4573
4618
  } });
4574
4619
  }
4575
- await this.autoPickAndPersist();
4620
+ await this.resolveAutoEngine();
4576
4621
  this.needsAutoPick = false;
4577
4622
  this.startProvisioningForCurrentEngine();
4578
4623
  }
4579
4624
  /**
4580
- * Auto-pick the best supported runtime at first boot (no stored engine).
4581
- * Uses the platform-probe cap's hardware + bestScore hint when available;
4582
- * falls back to platform/arch when the probe cap is not yet reachable.
4625
+ * Resolve the `'auto'` engine LIVE from this node's own kernel inference
4626
+ * resolver (`ctx.kernel.inferenceEngine`) the runtime mirror of
4627
+ * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
4628
+ * resolves its OWN accelerators in-process, so a forked agent child never
4629
+ * adopts the hub's runtime from the `platform-probe` singleton.
4583
4630
  *
4584
- * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4585
- * boots load it via `loadEngine()` and skip this path but ONLY when the
4586
- * probe actually answered (real `hardware` or a `bestScore` hint). If the
4587
- * probe query failed (cold-start race, cap momentarily unreachable), the
4588
- * pick floors to onnx purely for lack of information; persisting that would
4589
- * LOCK onnx and skip auto-pick on every future boot even after the
4590
- * accelerator surfaces. In that case we set the in-memory floor for liveness
4591
- * but leave the store untouched so the next boot re-attempts the pick.
4631
+ * NEVER persists. `'auto'` is a live-resolved sentinel: the pick is recomputed
4632
+ * every boot from the local hardware, so it is inherently self-healing the
4633
+ * old `autoPickAndPersist` sticky-onnx bug (a premature onnx pick persisted
4634
+ * during a boot race, then locked forever) cannot recur because the store is
4635
+ * never written here. Only an explicit operator override is persisted, and
4636
+ * that write happens through the settings UI, not this path.
4592
4637
  */
4593
- async autoPickAndPersist() {
4638
+ async resolveAutoEngine() {
4594
4639
  let hardware = null;
4595
4640
  try {
4596
- const api = this.addonCtx?.api;
4597
- if (api) hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
4641
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
4598
4642
  } catch {}
4599
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
4600
- const engine = {
4643
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
4644
+ this.currentEngine = {
4601
4645
  runtime: "python",
4602
4646
  backend: pick.runtimeId,
4603
4647
  format: modelFormatFor(pick.runtimeId),
4604
4648
  device: pick.device
4605
4649
  };
4606
- this.currentEngine = engine;
4607
- if (!(pick.runtimeId !== "onnx" || hardware !== null)) {
4608
- this.log.info("Auto-pick: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
4609
- backend: pick.runtimeId,
4610
- device: pick.device
4611
- } });
4612
- return;
4613
- }
4614
- const apNode = this.localProbeNodeId();
4615
- await this.writeStore({
4616
- [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4617
- [nodeEngineKey("engineDevice", apNode)]: pick.device
4618
- });
4619
- this.log.info("Auto-picked engine (platform-deterministic)", { meta: {
4650
+ this.log.info("Resolved auto engine (local kernel)", { meta: {
4620
4651
  backend: pick.runtimeId,
4621
4652
  device: pick.device,
4622
- hadProbeHardware: hardware !== null
4653
+ hadHardware: hardware !== null
4623
4654
  } });
4624
4655
  }
4656
+ /**
4657
+ * The RUNTIME that `'auto'` actually resolved to on this node (the live
4658
+ * `currentEngine.backend`), narrowed to a {@link RuntimeId}. The addon feeds
4659
+ * this into `resolveBackendTuning` when the stored config is `'auto'`, so the
4660
+ * pool tuning + respawn detection track the RESOLVED backend rather than the
4661
+ * `'auto'` sentinel (which would otherwise floor to onnx tuning).
4662
+ */
4663
+ resolvedEngineBackend() {
4664
+ const backend = this.currentEngine.backend;
4665
+ return backend === "openvino" || backend === "coreml" ? backend : "onnx";
4666
+ }
4625
4667
  /** Map a backend string to a known RuntimeId, flooring to onnx. */
4626
4668
  toRuntimeId(backend) {
4627
4669
  return [
@@ -4695,6 +4737,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4695
4737
  const prev = this.currentEngine;
4696
4738
  const stored = await this.loadEngine();
4697
4739
  if (stored) this.currentEngine = stored;
4740
+ else await this.resolveAutoEngine();
4698
4741
  if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
4699
4742
  this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4700
4743
  from: `${prev.backend}/${prev.device ?? "default"}`,
@@ -4796,17 +4839,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4796
4839
  }, { ...state }));
4797
4840
  }
4798
4841
  /**
4799
- * Fetch the probed HARDWARE for engine + device gating. Returns null when the
4800
- * probe cap is not yet reachable (cold-start / probe addon not installed), so
4801
- * callers fall back to the registry's safe minimum. The engine OFFER derives
4802
- * from hardware ONLY — install state (probe `scores`) never gates it.
4842
+ * Fetch this node's inference HARDWARE (gpu/npu) for engine + device gating,
4843
+ * from the local kernel resolver. Returns null when the resolver is not wired
4844
+ * yet (cold-start), so callers fall back to the registry's safe minimum. The
4845
+ * engine OFFER derives from hardware ONLY — install state never gates it.
4803
4846
  */
4804
4847
  /**
4805
- * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4806
- * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4807
- * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4808
- * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4809
- * have) and pin a device the node can't run, breaking provisioning.
4848
+ * The local Moleculer node id (child-suffix stripped). Used to scope this
4849
+ * node's engine-cascade store keys (`<key>@<node>`). Hardware itself is now
4850
+ * resolved in-process via `ctx.kernel.inferenceEngine` (never the
4851
+ * `platform-probe` singleton, whose no-nodeId query resolved to the HUB and
4852
+ * pinned a device a remote agent couldn't run) so this id is a store-scoping
4853
+ * concern, not a probe-routing one.
4810
4854
  */
4811
4855
  localProbeNodeId() {
4812
4856
  const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
@@ -4814,9 +4858,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4814
4858
  }
4815
4859
  async fetchProbeGatingData() {
4816
4860
  try {
4817
- const api = this.addonCtx?.api;
4818
- if (!api) return { hardware: null };
4819
- return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4861
+ return { hardware: await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null };
4820
4862
  } catch {
4821
4863
  return { hardware: null };
4822
4864
  }
@@ -6202,15 +6244,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6202
6244
  * Phase 2b — resolve the engine from the addon's new schema-backed
6203
6245
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
6204
6246
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
6205
- * Returns null when neither source has anything; the caller keeps
6206
- * the onnx floor set at construction until autoPickAndPersist() runs.
6247
+ *
6248
+ * The `'auto'` sentinel is treated as NO override (returns null) so the
6249
+ * caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
6250
+ * self-healing default, never a persisted concrete backend. An explicit
6251
+ * concrete backend (onnx/openvino/coreml) is honored as an operator override.
6252
+ * Returns null when neither source has a concrete selection; the caller keeps
6253
+ * the onnx floor set at construction until `resolveAutoEngine()` runs.
6207
6254
  */
6208
6255
  async loadEngine() {
6209
6256
  const store = await this.readStore();
6210
6257
  const storedRuntime = store["engineRuntime"];
6211
6258
  const node = this.localProbeNodeId();
6212
6259
  const storedBackend = readNodeEngineValue(store, "engineBackend", node);
6213
- if (typeof storedBackend === "string" && storedBackend.length > 0) {
6260
+ if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
6214
6261
  const backend = storedBackend;
6215
6262
  const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
6216
6263
  const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
@@ -6364,12 +6411,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6364
6411
  return { success: true };
6365
6412
  }
6366
6413
  async reprobeEngine() {
6367
- const api = this.addonCtx?.api;
6368
6414
  let hardware = null;
6369
- if (api) try {
6370
- hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
6415
+ try {
6416
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6371
6417
  } catch {}
6372
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
6418
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6373
6419
  const best = {
6374
6420
  runtime: "python",
6375
6421
  backend: pick.runtimeId,
@@ -6379,17 +6425,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6379
6425
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6380
6426
  const rpNode = this.localProbeNodeId();
6381
6427
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6382
- await this.writeStore({
6383
- [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
6384
- [nodeEngineKey("engineBackend", rpNode)]: best.backend,
6385
- [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
6386
- });
6387
- this.log.info("Re-probed engine (platform-deterministic) — wrote back", { meta: {
6428
+ await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
6429
+ this.log.info("Re-probed engine (local kernel) — updated probedBestEngine hint", { meta: {
6388
6430
  backend: best.backend,
6389
6431
  device: best.device ?? null,
6390
6432
  probedBestEngine: probedLabel
6391
6433
  } });
6392
- } else this.log.info("Re-probe: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
6434
+ } else this.log.info("Re-probe: onnx floor pending gpu probe — hint NOT persisted", { meta: {
6393
6435
  backend: best.backend,
6394
6436
  device: best.device ?? null
6395
6437
  } });
@@ -6806,7 +6848,7 @@ var DEFAULT_CONFIG = {
6806
6848
  intraOpThreads: 0,
6807
6849
  numWorkers: 0,
6808
6850
  engineRuntime: "python",
6809
- engineBackend: "onnx",
6851
+ engineBackend: "auto",
6810
6852
  engineDevice: "cpu",
6811
6853
  probedBestEngine: "",
6812
6854
  activeEngine: ""
@@ -6859,7 +6901,7 @@ var POOL_BOUND_KEYS = [
6859
6901
  "intraOpThreads",
6860
6902
  "numWorkers"
6861
6903
  ];
6862
- var DetectionPipelineAddon = class extends BaseAddon {
6904
+ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6863
6905
  provider = null;
6864
6906
  /** Last non-null probed hardware PER NODE — reused when the probe transiently
6865
6907
  * returns null so offered backends / device lists don't collapse. Keyed by
@@ -6934,7 +6976,10 @@ var DetectionPipelineAddon = class extends BaseAddon {
6934
6976
  type: "select",
6935
6977
  key: "engineBackend",
6936
6978
  label: "Execution provider",
6937
- options: [...STATIC_BACKEND_OPTIONS],
6979
+ options: [{
6980
+ value: "auto",
6981
+ label: "Auto (best for this node)"
6982
+ }, ...STATIC_BACKEND_OPTIONS],
6938
6983
  default: DEFAULT_CONFIG.engineBackend,
6939
6984
  immediate: true,
6940
6985
  perNode: true
@@ -7075,12 +7120,17 @@ var DetectionPipelineAddon = class extends BaseAddon {
7075
7120
  const env = await this.probeHardwareEnv(targetNode);
7076
7121
  const hardware = env.hardware;
7077
7122
  const offered = supportedRuntimes(env);
7078
- const runtimeBackends = offered.map((id) => ({
7123
+ const runtimeBackends = [{
7124
+ value: "auto",
7125
+ label: "Auto (best for this node)"
7126
+ }, ...offered.map((id) => ({
7079
7127
  value: id,
7080
7128
  label: runtimeLabel(id)
7081
- }));
7129
+ }))];
7082
7130
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
7131
+ const isAuto = storedBackend === "auto";
7083
7132
  const backend = (() => {
7133
+ if (isAuto) return pickBestRuntime(env, null).runtimeId;
7084
7134
  const rid = toRuntimeId(storedBackend);
7085
7135
  if (rid === "onnx") return "onnx";
7086
7136
  if (offered.includes(rid)) return rid;
@@ -7091,9 +7141,10 @@ var DetectionPipelineAddon = class extends BaseAddon {
7091
7141
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
7092
7142
  const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
7093
7143
  const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
7144
+ const displayBackend = isAuto ? "auto" : backend;
7094
7145
  const raw = {
7095
7146
  ...merged,
7096
- engineBackend: backend,
7147
+ engineBackend: displayBackend,
7097
7148
  engineDevice: device,
7098
7149
  activeEngine
7099
7150
  };
@@ -7211,7 +7262,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7211
7262
  const api = this.ctxIfReady?.api;
7212
7263
  if (!api) return null;
7213
7264
  const node = nodeId ?? this.localNodeId();
7214
- const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, nodePin(node)))?.hardware;
7265
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query({ nodeId: node }))?.hardware;
7215
7266
  if (!hw) return null;
7216
7267
  return {
7217
7268
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -7249,7 +7300,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7249
7300
  * a reason to disagree.
7250
7301
  */
7251
7302
  resolveBackendTuning() {
7252
- const t = tuningFor(toRuntimeId(this.config.engineBackend));
7303
+ const t = tuningFor(this.config.engineBackend === "auto" ? this.provider?.resolvedEngineBackend() ?? "onnx" : toRuntimeId(this.config.engineBackend));
7253
7304
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
7254
7305
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
7255
7306
  return {
@@ -7262,6 +7313,34 @@ var DetectionPipelineAddon = class extends BaseAddon {
7262
7313
  numWorkers: num(t["numWorkers"], 1)
7263
7314
  };
7264
7315
  }
7316
+ /**
7317
+ * Construct a fresh provider with the given effective tuning. Single source of
7318
+ * truth for the provider wiring — shared by `onInitialize`, the post-resolve
7319
+ * tuning reconciliation, and the `onConfigChanged` pool respawn.
7320
+ */
7321
+ buildProvider(modelsDir, tuning) {
7322
+ if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
7323
+ return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7324
+ concurrency: tuning.concurrency,
7325
+ tuning: {
7326
+ batchMode: tuning.batchMode,
7327
+ windowMs: tuning.windowMs,
7328
+ maxBatchSize: tuning.maxBatchSize,
7329
+ numStreams: tuning.numStreams,
7330
+ intraOpThreads: tuning.intraOpThreads
7331
+ },
7332
+ numWorkers: tuning.numWorkers,
7333
+ pythonPath: this.pythonPath,
7334
+ pythonAddonDir: this.pythonAddonDir
7335
+ });
7336
+ }
7337
+ /**
7338
+ * Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
7339
+ * post-resolve / config-change respawn is actually needed).
7340
+ */
7341
+ static poolTuningEqual(a, b) {
7342
+ return a.concurrency === b.concurrency && a.batchMode === b.batchMode && a.windowMs === b.windowMs && a.maxBatchSize === b.maxBatchSize && a.numStreams === b.numStreams && a.intraOpThreads === b.intraOpThreads && a.numWorkers === b.numWorkers;
7343
+ }
7265
7344
  async onInitialize() {
7266
7345
  const modelsDir = await this.resolveModelsDir();
7267
7346
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
@@ -7270,22 +7349,20 @@ var DetectionPipelineAddon = class extends BaseAddon {
7270
7349
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
7271
7350
  else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
7272
7351
  await this.proactivelyInstallOpenvino();
7273
- const effectiveTuning = this.resolveBackendTuning();
7274
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7275
- concurrency: effectiveTuning.concurrency,
7276
- tuning: {
7277
- batchMode: effectiveTuning.batchMode,
7278
- windowMs: effectiveTuning.windowMs,
7279
- maxBatchSize: effectiveTuning.maxBatchSize,
7280
- numStreams: effectiveTuning.numStreams,
7281
- intraOpThreads: effectiveTuning.intraOpThreads
7282
- },
7283
- numWorkers: effectiveTuning.numWorkers,
7284
- pythonPath: this.pythonPath,
7285
- pythonAddonDir: this.pythonAddonDir
7286
- });
7352
+ const initialTuning = this.resolveBackendTuning();
7353
+ this.provider = this.buildProvider(modelsDir, initialTuning);
7287
7354
  await this.provider.init();
7288
7355
  await this.provider.setApi(this.ctx);
7356
+ const resolvedTuning = this.resolveBackendTuning();
7357
+ if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
7358
+ this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
7359
+ from: initialTuning,
7360
+ to: resolvedTuning
7361
+ } });
7362
+ this.provider = this.buildProvider(modelsDir, resolvedTuning);
7363
+ await this.provider.init();
7364
+ await this.provider.setApi(this.ctx);
7365
+ }
7289
7366
  if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
7290
7367
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7291
7368
  });
@@ -7342,13 +7419,18 @@ var DetectionPipelineAddon = class extends BaseAddon {
7342
7419
  * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
7343
7420
  *
7344
7421
  * Failure is non-fatal: a warning is logged and the addon continues with
7345
- * the onnx-cpu baseline. The hardware query itself is also best-effort —
7346
- * if the platform-probe cap isn't wired yet the error is caught here.
7422
+ * the onnx-cpu baseline. The hardware resolve itself is also best-effort —
7423
+ * if the kernel resolver isn't wired yet the error is caught here.
7347
7424
  */
7348
7425
  async proactivelyInstallOpenvino() {
7349
7426
  if (!this.pythonAddonDir || !this.pythonPath) return;
7350
7427
  try {
7351
- const hardware = await this.ctx.api.platformProbe.getHardware.query();
7428
+ const hw = await this.ctx.kernel?.inferenceEngine?.resolveHardware();
7429
+ const hardware = {
7430
+ platform: process.platform,
7431
+ gpu: hw?.gpu ?? null,
7432
+ npu: hw?.npu ?? null
7433
+ };
7352
7434
  if (!shouldInstallOpenvino(hardware)) return;
7353
7435
  this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
7354
7436
  gpu: hardware.gpu?.type ?? null,
@@ -7427,21 +7509,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7427
7509
  this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
7428
7510
  }
7429
7511
  const modelsDir = await this.resolveModelsDir();
7430
- if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available during respawn");
7431
- const effectiveTuning = this.resolveBackendTuning();
7432
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7433
- concurrency: effectiveTuning.concurrency,
7434
- tuning: {
7435
- batchMode: effectiveTuning.batchMode,
7436
- windowMs: effectiveTuning.windowMs,
7437
- maxBatchSize: effectiveTuning.maxBatchSize,
7438
- numStreams: effectiveTuning.numStreams,
7439
- intraOpThreads: effectiveTuning.intraOpThreads
7440
- },
7441
- numWorkers: effectiveTuning.numWorkers,
7442
- pythonPath: this.pythonPath,
7443
- pythonAddonDir: this.pythonAddonDir
7444
- });
7512
+ this.provider = this.buildProvider(modelsDir, this.resolveBackendTuning());
7445
7513
  await this.provider.init();
7446
7514
  await this.provider.setApi(this.ctx);
7447
7515
  await this.provider.warmPool();