@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
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-Biq62zt4.js");
6
+ const require_dist = require("../dist-Cwc0TUQr.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  let node_fs = require("node:fs");
@@ -15,87 +15,6 @@ node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
15
15
  let node_child_process = require("node:child_process");
16
16
  let sharp = require("sharp");
17
17
  sharp = require_model_download_service_C_IHWnXx.__toESM(sharp);
18
- //#region src/detection-pipeline/engine-store-keys.ts
19
- /**
20
- * Per-node scoping for the detection-pipeline engine cascade.
21
- *
22
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
23
- * settings-store cap is hub-resident; every node's detection instance reads and
24
- * writes the same keys). That is correct for node-agnostic settings (pipeline
25
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
26
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
27
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
28
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
29
- * another node that has no NPU.
30
- *
31
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
32
- * Everything else in the store stays shared. There is NO fallback to a legacy
33
- * un-scoped value for ANY node (the hub included) and no in-code migration: a
34
- * bare key (written before scoping, or by an older build) is inert — invisible
35
- * to every node. A node with no scoped key reads `undefined`, which drives its
36
- * OWN platform auto-pick (`autoPickAndPersist` / `reprobeEngine` persist the
37
- * node's own probe under `<key>@<node>`). Serving a bare value to any node
38
- * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
39
- * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
40
- * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
41
- */
42
- var ENGINE_CASCADE_KEYS = [
43
- "engineBackend",
44
- "engineDevice",
45
- "probedBestEngine"
46
- ];
47
- function isEngineCascadeKey(key) {
48
- return ENGINE_CASCADE_KEYS.includes(key);
49
- }
50
- /**
51
- * Normalise a raw kernel node id to the bare node id used for scoping.
52
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
53
- * per-NODE, so strip the addon segment. Falls back to `hub`.
54
- */
55
- function normalizeEngineNodeId(rawNodeId) {
56
- const raw = rawNodeId ?? "hub";
57
- return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
58
- }
59
- /** The node-scoped store key for an engine cascade field. */
60
- function nodeEngineKey(base, nodeId) {
61
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
62
- }
63
- /**
64
- * Read an engine cascade value for a node: the node-scoped key if present,
65
- * otherwise `undefined` — for EVERY node, the hub included. There is no
66
- * fallback to the legacy un-scoped key: a bare value in the store is inert.
67
- * A node with no scoped key gets `undefined` so its own platform-driven
68
- * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
69
- * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
70
- */
71
- function readNodeEngineValue(store, base, nodeId) {
72
- return store[nodeEngineKey(base, nodeId)];
73
- }
74
- /**
75
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
76
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
77
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
78
- * keys for OTHER nodes are dropped from the projection (not relevant to this
79
- * node's form).
80
- */
81
- function projectNodeEngine(store, nodeId) {
82
- const out = {};
83
- const scopedForAnyNode = /* @__PURE__ */ new Set();
84
- for (const key of Object.keys(store)) {
85
- const atIdx = key.indexOf("@");
86
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
87
- scopedForAnyNode.add(key);
88
- continue;
89
- }
90
- out[key] = store[key];
91
- }
92
- for (const base of ENGINE_CASCADE_KEYS) {
93
- const value = readNodeEngineValue(store, base, nodeId);
94
- if (value !== void 0) out[base] = value;
95
- }
96
- return out;
97
- }
98
- //#endregion
99
18
  //#region src/detection-pipeline/runtimes.ts
100
19
  var KNOWN_PLATFORMS = [
101
20
  "darwin",
@@ -267,6 +186,88 @@ function pickBestRuntime(env, bestBackendHint) {
267
186
  };
268
187
  }
269
188
  //#endregion
189
+ //#region src/detection-pipeline/engine-store-keys.ts
190
+ /**
191
+ * Per-node scoping for the detection-pipeline engine cascade.
192
+ *
193
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
194
+ * settings-store cap is hub-resident; every node's detection instance reads and
195
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
196
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
197
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
198
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
199
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
200
+ * another node that has no NPU.
201
+ *
202
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
203
+ * Everything else in the store stays shared. There is NO fallback to a legacy
204
+ * un-scoped value for ANY node (the hub included) and no in-code migration: a
205
+ * bare key (written before scoping, or by an older build) is inert — invisible
206
+ * to every node. A node with no scoped key reads `undefined`, which drives its
207
+ * OWN live resolve (`resolveAutoEngine` picks the node's runtime from the local
208
+ * kernel each boot; `reprobeEngine` persists an explicit probe under
209
+ * `<key>@<node>`). Serving a bare value to any node
210
+ * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
211
+ * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
212
+ * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
213
+ */
214
+ var ENGINE_CASCADE_KEYS = [
215
+ "engineBackend",
216
+ "engineDevice",
217
+ "probedBestEngine"
218
+ ];
219
+ function isEngineCascadeKey(key) {
220
+ return ENGINE_CASCADE_KEYS.includes(key);
221
+ }
222
+ /**
223
+ * Normalise a raw kernel node id to the bare node id used for scoping.
224
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
225
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
226
+ */
227
+ function normalizeEngineNodeId(rawNodeId) {
228
+ const raw = rawNodeId ?? "hub";
229
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
230
+ }
231
+ /** The node-scoped store key for an engine cascade field. */
232
+ function nodeEngineKey(base, nodeId) {
233
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
234
+ }
235
+ /**
236
+ * Read an engine cascade value for a node: the node-scoped key if present,
237
+ * otherwise `undefined` — for EVERY node, the hub included. There is no
238
+ * fallback to the legacy un-scoped key: a bare value in the store is inert.
239
+ * A node with no scoped key gets `undefined` so its own platform-driven
240
+ * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
241
+ * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
242
+ */
243
+ function readNodeEngineValue(store, base, nodeId) {
244
+ return store[nodeEngineKey(base, nodeId)];
245
+ }
246
+ /**
247
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
248
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
249
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
250
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
251
+ * node's form).
252
+ */
253
+ function projectNodeEngine(store, nodeId) {
254
+ const out = {};
255
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
256
+ for (const key of Object.keys(store)) {
257
+ const atIdx = key.indexOf("@");
258
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
259
+ scopedForAnyNode.add(key);
260
+ continue;
261
+ }
262
+ out[key] = store[key];
263
+ }
264
+ for (const base of ENGINE_CASCADE_KEYS) {
265
+ const value = readNodeEngineValue(store, base, nodeId);
266
+ if (value !== void 0) out[base] = value;
267
+ }
268
+ return out;
269
+ }
270
+ //#endregion
270
271
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
271
272
  /**
272
273
  * SharedInferencePool — TypeScript wrapper for inference_pool.py.
@@ -335,6 +336,51 @@ var PoolHandle = class {
335
336
  }
336
337
  async dispose() {}
337
338
  };
339
+ /**
340
+ * How long to wait for a worker to exit on SIGTERM before escalating to
341
+ * SIGKILL. A worker idle between frames exits well under this; only a worker
342
+ * stuck in a native inference call (openvino/onnx C++) needs the escalation.
343
+ */
344
+ var POOL_WORKER_TERM_GRACE_MS = 2e3;
345
+ /**
346
+ * Terminate a spawned child GRACEFULLY THEN FORCEFULLY: end stdin, send
347
+ * SIGTERM, and if the process hasn't exited within `graceMs`, send SIGKILL.
348
+ * Resolves once the process has exited (or was already dead).
349
+ *
350
+ * The old teardown sent SIGTERM and dropped the reference immediately — a
351
+ * worker busy in a native inference loop ignores SIGTERM and ORPHANS at full
352
+ * CPU (observed after an engine re-spin). SIGKILL cannot be ignored, so the
353
+ * escalation guarantees the process dies. Exported for tests.
354
+ */
355
+ async function terminateChild(proc, graceMs) {
356
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
357
+ try {
358
+ proc.stdin?.end();
359
+ } catch {}
360
+ await new Promise((resolve) => {
361
+ let settled = false;
362
+ let timer = null;
363
+ const done = () => {
364
+ if (settled) return;
365
+ settled = true;
366
+ if (timer) clearTimeout(timer);
367
+ resolve();
368
+ };
369
+ proc.once("exit", done);
370
+ try {
371
+ proc.kill("SIGTERM");
372
+ } catch {
373
+ done();
374
+ return;
375
+ }
376
+ timer = setTimeout(() => {
377
+ try {
378
+ proc.kill("SIGKILL");
379
+ } catch {}
380
+ done();
381
+ }, graceMs);
382
+ });
383
+ }
338
384
  var PoolWorker = class {
339
385
  process = null;
340
386
  receiveBuffer = Buffer.alloc(0);
@@ -490,12 +536,11 @@ var PoolWorker = class {
490
536
  return await this.dispatch(MSG_COMMAND, payload);
491
537
  }
492
538
  async dispose() {
493
- if (this.process) {
494
- this.process.stdin?.end();
495
- this.process.kill("SIGTERM");
496
- this.process = null;
497
- this.ready = false;
498
- }
539
+ const proc = this.process;
540
+ if (!proc) return;
541
+ this.process = null;
542
+ this.ready = false;
543
+ await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
499
544
  }
500
545
  dispatch(msgType, payload) {
501
546
  const reqId = this.allocRequestId();
@@ -4579,56 +4624,53 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4579
4624
  error: require_dist.errMsg(err)
4580
4625
  } });
4581
4626
  }
4582
- await this.autoPickAndPersist();
4627
+ await this.resolveAutoEngine();
4583
4628
  this.needsAutoPick = false;
4584
4629
  this.startProvisioningForCurrentEngine();
4585
4630
  }
4586
4631
  /**
4587
- * Auto-pick the best supported runtime at first boot (no stored engine).
4588
- * Uses the platform-probe cap's hardware + bestScore hint when available;
4589
- * falls back to platform/arch when the probe cap is not yet reachable.
4632
+ * Resolve the `'auto'` engine LIVE from this node's own kernel inference
4633
+ * resolver (`ctx.kernel.inferenceEngine`) the runtime mirror of
4634
+ * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
4635
+ * resolves its OWN accelerators in-process, so a forked agent child never
4636
+ * adopts the hub's runtime from the `platform-probe` singleton.
4590
4637
  *
4591
- * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4592
- * boots load it via `loadEngine()` and skip this path but ONLY when the
4593
- * probe actually answered (real `hardware` or a `bestScore` hint). If the
4594
- * probe query failed (cold-start race, cap momentarily unreachable), the
4595
- * pick floors to onnx purely for lack of information; persisting that would
4596
- * LOCK onnx and skip auto-pick on every future boot even after the
4597
- * accelerator surfaces. In that case we set the in-memory floor for liveness
4598
- * but leave the store untouched so the next boot re-attempts the pick.
4638
+ * NEVER persists. `'auto'` is a live-resolved sentinel: the pick is recomputed
4639
+ * every boot from the local hardware, so it is inherently self-healing the
4640
+ * old `autoPickAndPersist` sticky-onnx bug (a premature onnx pick persisted
4641
+ * during a boot race, then locked forever) cannot recur because the store is
4642
+ * never written here. Only an explicit operator override is persisted, and
4643
+ * that write happens through the settings UI, not this path.
4599
4644
  */
4600
- async autoPickAndPersist() {
4645
+ async resolveAutoEngine() {
4601
4646
  let hardware = null;
4602
4647
  try {
4603
- const api = this.addonCtx?.api;
4604
- if (api) hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
4648
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
4605
4649
  } catch {}
4606
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
4607
- const engine = {
4650
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
4651
+ this.currentEngine = {
4608
4652
  runtime: "python",
4609
4653
  backend: pick.runtimeId,
4610
4654
  format: modelFormatFor(pick.runtimeId),
4611
4655
  device: pick.device
4612
4656
  };
4613
- this.currentEngine = engine;
4614
- if (!(pick.runtimeId !== "onnx" || hardware !== null)) {
4615
- this.log.info("Auto-pick: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
4616
- backend: pick.runtimeId,
4617
- device: pick.device
4618
- } });
4619
- return;
4620
- }
4621
- const apNode = this.localProbeNodeId();
4622
- await this.writeStore({
4623
- [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4624
- [nodeEngineKey("engineDevice", apNode)]: pick.device
4625
- });
4626
- this.log.info("Auto-picked engine (platform-deterministic)", { meta: {
4657
+ this.log.info("Resolved auto engine (local kernel)", { meta: {
4627
4658
  backend: pick.runtimeId,
4628
4659
  device: pick.device,
4629
- hadProbeHardware: hardware !== null
4660
+ hadHardware: hardware !== null
4630
4661
  } });
4631
4662
  }
4663
+ /**
4664
+ * The RUNTIME that `'auto'` actually resolved to on this node (the live
4665
+ * `currentEngine.backend`), narrowed to a {@link RuntimeId}. The addon feeds
4666
+ * this into `resolveBackendTuning` when the stored config is `'auto'`, so the
4667
+ * pool tuning + respawn detection track the RESOLVED backend rather than the
4668
+ * `'auto'` sentinel (which would otherwise floor to onnx tuning).
4669
+ */
4670
+ resolvedEngineBackend() {
4671
+ const backend = this.currentEngine.backend;
4672
+ return backend === "openvino" || backend === "coreml" ? backend : "onnx";
4673
+ }
4632
4674
  /** Map a backend string to a known RuntimeId, flooring to onnx. */
4633
4675
  toRuntimeId(backend) {
4634
4676
  return [
@@ -4702,6 +4744,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4702
4744
  const prev = this.currentEngine;
4703
4745
  const stored = await this.loadEngine();
4704
4746
  if (stored) this.currentEngine = stored;
4747
+ else await this.resolveAutoEngine();
4705
4748
  if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
4706
4749
  this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4707
4750
  from: `${prev.backend}/${prev.device ?? "default"}`,
@@ -4803,17 +4846,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4803
4846
  }, { ...state }));
4804
4847
  }
4805
4848
  /**
4806
- * Fetch the probed HARDWARE for engine + device gating. Returns null when the
4807
- * probe cap is not yet reachable (cold-start / probe addon not installed), so
4808
- * callers fall back to the registry's safe minimum. The engine OFFER derives
4809
- * from hardware ONLY — install state (probe `scores`) never gates it.
4849
+ * Fetch this node's inference HARDWARE (gpu/npu) for engine + device gating,
4850
+ * from the local kernel resolver. Returns null when the resolver is not wired
4851
+ * yet (cold-start), so callers fall back to the registry's safe minimum. The
4852
+ * engine OFFER derives from hardware ONLY — install state never gates it.
4810
4853
  */
4811
4854
  /**
4812
- * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4813
- * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4814
- * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4815
- * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4816
- * have) and pin a device the node can't run, breaking provisioning.
4855
+ * The local Moleculer node id (child-suffix stripped). Used to scope this
4856
+ * node's engine-cascade store keys (`<key>@<node>`). Hardware itself is now
4857
+ * resolved in-process via `ctx.kernel.inferenceEngine` (never the
4858
+ * `platform-probe` singleton, whose no-nodeId query resolved to the HUB and
4859
+ * pinned a device a remote agent couldn't run) so this id is a store-scoping
4860
+ * concern, not a probe-routing one.
4817
4861
  */
4818
4862
  localProbeNodeId() {
4819
4863
  const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
@@ -4821,9 +4865,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4821
4865
  }
4822
4866
  async fetchProbeGatingData() {
4823
4867
  try {
4824
- const api = this.addonCtx?.api;
4825
- if (!api) return { hardware: null };
4826
- return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4868
+ return { hardware: await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null };
4827
4869
  } catch {
4828
4870
  return { hardware: null };
4829
4871
  }
@@ -6209,15 +6251,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6209
6251
  * Phase 2b — resolve the engine from the addon's new schema-backed
6210
6252
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
6211
6253
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
6212
- * Returns null when neither source has anything; the caller keeps
6213
- * the onnx floor set at construction until autoPickAndPersist() runs.
6254
+ *
6255
+ * The `'auto'` sentinel is treated as NO override (returns null) so the
6256
+ * caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
6257
+ * self-healing default, never a persisted concrete backend. An explicit
6258
+ * concrete backend (onnx/openvino/coreml) is honored as an operator override.
6259
+ * Returns null when neither source has a concrete selection; the caller keeps
6260
+ * the onnx floor set at construction until `resolveAutoEngine()` runs.
6214
6261
  */
6215
6262
  async loadEngine() {
6216
6263
  const store = await this.readStore();
6217
6264
  const storedRuntime = store["engineRuntime"];
6218
6265
  const node = this.localProbeNodeId();
6219
6266
  const storedBackend = readNodeEngineValue(store, "engineBackend", node);
6220
- if (typeof storedBackend === "string" && storedBackend.length > 0) {
6267
+ if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
6221
6268
  const backend = storedBackend;
6222
6269
  const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
6223
6270
  const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
@@ -6371,12 +6418,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6371
6418
  return { success: true };
6372
6419
  }
6373
6420
  async reprobeEngine() {
6374
- const api = this.addonCtx?.api;
6375
6421
  let hardware = null;
6376
- if (api) try {
6377
- hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
6422
+ try {
6423
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6378
6424
  } catch {}
6379
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
6425
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6380
6426
  const best = {
6381
6427
  runtime: "python",
6382
6428
  backend: pick.runtimeId,
@@ -6386,17 +6432,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6386
6432
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6387
6433
  const rpNode = this.localProbeNodeId();
6388
6434
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6389
- await this.writeStore({
6390
- [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
6391
- [nodeEngineKey("engineBackend", rpNode)]: best.backend,
6392
- [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
6393
- });
6394
- this.log.info("Re-probed engine (platform-deterministic) — wrote back", { meta: {
6435
+ await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
6436
+ this.log.info("Re-probed engine (local kernel) — updated probedBestEngine hint", { meta: {
6395
6437
  backend: best.backend,
6396
6438
  device: best.device ?? null,
6397
6439
  probedBestEngine: probedLabel
6398
6440
  } });
6399
- } else this.log.info("Re-probe: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
6441
+ } else this.log.info("Re-probe: onnx floor pending gpu probe — hint NOT persisted", { meta: {
6400
6442
  backend: best.backend,
6401
6443
  device: best.device ?? null
6402
6444
  } });
@@ -6813,7 +6855,7 @@ var DEFAULT_CONFIG = {
6813
6855
  intraOpThreads: 0,
6814
6856
  numWorkers: 0,
6815
6857
  engineRuntime: "python",
6816
- engineBackend: "onnx",
6858
+ engineBackend: "auto",
6817
6859
  engineDevice: "cpu",
6818
6860
  probedBestEngine: "",
6819
6861
  activeEngine: ""
@@ -6866,7 +6908,7 @@ var POOL_BOUND_KEYS = [
6866
6908
  "intraOpThreads",
6867
6909
  "numWorkers"
6868
6910
  ];
6869
- var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6911
+ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.BaseAddon {
6870
6912
  provider = null;
6871
6913
  /** Last non-null probed hardware PER NODE — reused when the probe transiently
6872
6914
  * returns null so offered backends / device lists don't collapse. Keyed by
@@ -6941,7 +6983,10 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6941
6983
  type: "select",
6942
6984
  key: "engineBackend",
6943
6985
  label: "Execution provider",
6944
- options: [...STATIC_BACKEND_OPTIONS],
6986
+ options: [{
6987
+ value: "auto",
6988
+ label: "Auto (best for this node)"
6989
+ }, ...STATIC_BACKEND_OPTIONS],
6945
6990
  default: DEFAULT_CONFIG.engineBackend,
6946
6991
  immediate: true,
6947
6992
  perNode: true
@@ -7082,12 +7127,17 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7082
7127
  const env = await this.probeHardwareEnv(targetNode);
7083
7128
  const hardware = env.hardware;
7084
7129
  const offered = supportedRuntimes(env);
7085
- const runtimeBackends = offered.map((id) => ({
7130
+ const runtimeBackends = [{
7131
+ value: "auto",
7132
+ label: "Auto (best for this node)"
7133
+ }, ...offered.map((id) => ({
7086
7134
  value: id,
7087
7135
  label: runtimeLabel(id)
7088
- }));
7136
+ }))];
7089
7137
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
7138
+ const isAuto = storedBackend === "auto";
7090
7139
  const backend = (() => {
7140
+ if (isAuto) return pickBestRuntime(env, null).runtimeId;
7091
7141
  const rid = toRuntimeId(storedBackend);
7092
7142
  if (rid === "onnx") return "onnx";
7093
7143
  if (offered.includes(rid)) return rid;
@@ -7098,9 +7148,10 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7098
7148
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
7099
7149
  const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
7100
7150
  const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
7151
+ const displayBackend = isAuto ? "auto" : backend;
7101
7152
  const raw = {
7102
7153
  ...merged,
7103
- engineBackend: backend,
7154
+ engineBackend: displayBackend,
7104
7155
  engineDevice: device,
7105
7156
  activeEngine
7106
7157
  };
@@ -7218,7 +7269,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7218
7269
  const api = this.ctxIfReady?.api;
7219
7270
  if (!api) return null;
7220
7271
  const node = nodeId ?? this.localNodeId();
7221
- const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(node)))?.hardware;
7272
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query({ nodeId: node }))?.hardware;
7222
7273
  if (!hw) return null;
7223
7274
  return {
7224
7275
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -7256,7 +7307,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7256
7307
  * a reason to disagree.
7257
7308
  */
7258
7309
  resolveBackendTuning() {
7259
- const t = tuningFor(toRuntimeId(this.config.engineBackend));
7310
+ const t = tuningFor(this.config.engineBackend === "auto" ? this.provider?.resolvedEngineBackend() ?? "onnx" : toRuntimeId(this.config.engineBackend));
7260
7311
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
7261
7312
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
7262
7313
  return {
@@ -7269,6 +7320,34 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7269
7320
  numWorkers: num(t["numWorkers"], 1)
7270
7321
  };
7271
7322
  }
7323
+ /**
7324
+ * Construct a fresh provider with the given effective tuning. Single source of
7325
+ * truth for the provider wiring — shared by `onInitialize`, the post-resolve
7326
+ * tuning reconciliation, and the `onConfigChanged` pool respawn.
7327
+ */
7328
+ buildProvider(modelsDir, tuning) {
7329
+ if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
7330
+ return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7331
+ concurrency: tuning.concurrency,
7332
+ tuning: {
7333
+ batchMode: tuning.batchMode,
7334
+ windowMs: tuning.windowMs,
7335
+ maxBatchSize: tuning.maxBatchSize,
7336
+ numStreams: tuning.numStreams,
7337
+ intraOpThreads: tuning.intraOpThreads
7338
+ },
7339
+ numWorkers: tuning.numWorkers,
7340
+ pythonPath: this.pythonPath,
7341
+ pythonAddonDir: this.pythonAddonDir
7342
+ });
7343
+ }
7344
+ /**
7345
+ * Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
7346
+ * post-resolve / config-change respawn is actually needed).
7347
+ */
7348
+ static poolTuningEqual(a, b) {
7349
+ 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;
7350
+ }
7272
7351
  async onInitialize() {
7273
7352
  const modelsDir = await this.resolveModelsDir();
7274
7353
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
@@ -7277,22 +7356,20 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7277
7356
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
7278
7357
  else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
7279
7358
  await this.proactivelyInstallOpenvino();
7280
- const effectiveTuning = this.resolveBackendTuning();
7281
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7282
- concurrency: effectiveTuning.concurrency,
7283
- tuning: {
7284
- batchMode: effectiveTuning.batchMode,
7285
- windowMs: effectiveTuning.windowMs,
7286
- maxBatchSize: effectiveTuning.maxBatchSize,
7287
- numStreams: effectiveTuning.numStreams,
7288
- intraOpThreads: effectiveTuning.intraOpThreads
7289
- },
7290
- numWorkers: effectiveTuning.numWorkers,
7291
- pythonPath: this.pythonPath,
7292
- pythonAddonDir: this.pythonAddonDir
7293
- });
7359
+ const initialTuning = this.resolveBackendTuning();
7360
+ this.provider = this.buildProvider(modelsDir, initialTuning);
7294
7361
  await this.provider.init();
7295
7362
  await this.provider.setApi(this.ctx);
7363
+ const resolvedTuning = this.resolveBackendTuning();
7364
+ if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
7365
+ this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
7366
+ from: initialTuning,
7367
+ to: resolvedTuning
7368
+ } });
7369
+ this.provider = this.buildProvider(modelsDir, resolvedTuning);
7370
+ await this.provider.init();
7371
+ await this.provider.setApi(this.ctx);
7372
+ }
7296
7373
  if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
7297
7374
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7298
7375
  });
@@ -7349,13 +7426,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7349
7426
  * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
7350
7427
  *
7351
7428
  * Failure is non-fatal: a warning is logged and the addon continues with
7352
- * the onnx-cpu baseline. The hardware query itself is also best-effort —
7353
- * if the platform-probe cap isn't wired yet the error is caught here.
7429
+ * the onnx-cpu baseline. The hardware resolve itself is also best-effort —
7430
+ * if the kernel resolver isn't wired yet the error is caught here.
7354
7431
  */
7355
7432
  async proactivelyInstallOpenvino() {
7356
7433
  if (!this.pythonAddonDir || !this.pythonPath) return;
7357
7434
  try {
7358
- const hardware = await this.ctx.api.platformProbe.getHardware.query();
7435
+ const hw = await this.ctx.kernel?.inferenceEngine?.resolveHardware();
7436
+ const hardware = {
7437
+ platform: process.platform,
7438
+ gpu: hw?.gpu ?? null,
7439
+ npu: hw?.npu ?? null
7440
+ };
7359
7441
  if (!shouldInstallOpenvino(hardware)) return;
7360
7442
  this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
7361
7443
  gpu: hardware.gpu?.type ?? null,
@@ -7434,21 +7516,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
7434
7516
  this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
7435
7517
  }
7436
7518
  const modelsDir = await this.resolveModelsDir();
7437
- if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available during respawn");
7438
- const effectiveTuning = this.resolveBackendTuning();
7439
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7440
- concurrency: effectiveTuning.concurrency,
7441
- tuning: {
7442
- batchMode: effectiveTuning.batchMode,
7443
- windowMs: effectiveTuning.windowMs,
7444
- maxBatchSize: effectiveTuning.maxBatchSize,
7445
- numStreams: effectiveTuning.numStreams,
7446
- intraOpThreads: effectiveTuning.intraOpThreads
7447
- },
7448
- numWorkers: effectiveTuning.numWorkers,
7449
- pythonPath: this.pythonPath,
7450
- pythonAddonDir: this.pythonAddonDir
7451
- });
7519
+ this.provider = this.buildProvider(modelsDir, this.resolveBackendTuning());
7452
7520
  await this.provider.init();
7453
7521
  await this.provider.setApi(this.ctx);
7454
7522
  await this.provider.warmPool();