@camstack/addon-pipeline 1.1.70 → 1.1.71

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 (34) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +181 -469
  4. package/dist/detection-pipeline/index.mjs +182 -469
  5. package/dist/{dist-CurVt3xC.mjs → dist-DyALtmD7.mjs} +229 -49
  6. package/dist/{dist-BWn5k0A5.js → dist-DzTjxf39.js} +240 -48
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +24 -2
  10. package/dist/pipeline-runner/index.mjs +24 -2
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/session-decode/decode-worker-child.js +177 -12
  14. package/dist/session-decode/decode-worker-child.mjs +177 -12
  15. package/dist/{step-definitions-B7e8rPe3.js → step-definitions-Djy8aZLt.js} +6 -2
  16. package/dist/{step-definitions-BKm17nce.mjs → step-definitions-rmBALHX4.mjs} +6 -2
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-yAAGMET5.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CMP316sx.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BflyNuZv.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-5Zc9wj-A.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-DUzRsTmt.mjs → hostInit-DLJNx0Ly.mjs} +2 -2
  22. package/dist/stream-broker/index.js +1 -1
  23. package/dist/stream-broker/index.mjs +1 -1
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-D6bWMuew.js → MaskShapeCanvas-DI4BY7W2-sOOxiGvP.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-C-1enV21.js → MotionZonesSettings-NcxxQN8r-DdTQuT23.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-By9pdGP7.js → PrivacyMaskSettings-APgPLF7p-BrR0L3ql.js} +1 -1
  28. package/embed-dist/assets/index-D4MGuECB.css +2 -0
  29. package/embed-dist/assets/{index-BPGh79kn.js → index-Dk21l1_3.js} +14 -14
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D68Lj69v.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CDBALHSV.mjs +0 -26
  34. package/embed-dist/assets/index-D2pF9Z3W.css +0 -2
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-BWn5k0A5.js");
7
- const require_step_definitions = require("../step-definitions-B7e8rPe3.js");
6
+ const require_dist = require("../dist-DzTjxf39.js");
7
+ const require_step_definitions = require("../step-definitions-Djy8aZLt.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
9
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
10
10
  let node_child_process = require("node:child_process");
@@ -164,9 +164,6 @@ function pythonRequirementsFor(id) {
164
164
  function tuningFor(id) {
165
165
  return RUNTIME_DETAIL[id].tuning;
166
166
  }
167
- function runtimeLabel(id) {
168
- return RUNTIME_DETAIL[id].label;
169
- }
170
167
  /**
171
168
  * Proactive-install hint kept for back-compat (re-exported from index.ts).
172
169
  * Intel on Linux warrants installing openvino; coremltools covers macOS;
@@ -202,117 +199,27 @@ function pickBestRuntime(env, bestBackendHint) {
202
199
  device: defaultDeviceFor(chosen)
203
200
  };
204
201
  }
205
- /**
206
- * The ordered device candidates a runtime will try for an `'auto'`/`'all'`
207
- * device pick — mirrors what the python inference pool actually resolves
208
- * (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
209
- * ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
210
- * accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
211
- * tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
212
- * caller then keeps the plain `backend/device` label.
213
- */
214
- function engineDeviceSequence(runtimeId, env) {
215
- const hw = env.hardware;
216
- switch (runtimeId) {
217
- case "openvino": {
218
- const seq = [];
219
- if (hw?.npu?.type === "intel-npu") seq.push("NPU");
220
- if (hw?.gpu?.type === "intel") seq.push("GPU");
221
- seq.push("CPU");
222
- return seq;
223
- }
224
- case "coreml": {
225
- const seq = [];
226
- if (hw?.npu?.type === "apple-ane") seq.push("ANE");
227
- if (hw?.gpu != null) seq.push("GPU");
228
- seq.push("CPU");
229
- return seq;
230
- }
231
- default: return [];
232
- }
233
- }
234
202
  //#endregion
235
203
  //#region src/detection-pipeline/engine-store-keys.ts
236
204
  /**
237
- * Per-node scoping for the detection-pipeline engine cascade.
238
- *
239
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
240
- * settings-store cap is hub-resident; every node's detection instance reads and
241
- * writes the same keys). That is correct for node-agnostic settings (pipeline
242
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
243
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
244
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
245
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
246
- * another node that has no NPU.
205
+ * Node-id normalisation for the detection pipeline.
247
206
  *
248
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
249
- * Everything else in the store stays shared. There is NO fallback to a legacy
250
- * un-scoped value for ANY node (the hub included) and no in-code migration: a
251
- * bare key (written before scoping, or by an older build) is inert — invisible
252
- * to every node. A node with no scoped key reads `undefined`, which drives its
253
- * OWN live resolve (`resolveAutoEngine` picks the node's runtime from the local
254
- * kernel each boot; `reprobeEngine` persists an explicit probe under
255
- * `<key>@<node>`). Serving a bare value to any node
256
- * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
257
- * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
258
- * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
207
+ * The legacy per-node engine CASCADE keys (`engineBackend` / `engineDevice` /
208
+ * `probedBestEngine`) were removed together with the operator-facing engine
209
+ * election: each node now runs a device-array of inference pools balanced
210
+ * per-session, and the node-default pool is derived live from the node's own
211
+ * hardware (`resolveAutoEngine`). All that survives from the old cascade module
212
+ * is the node-id normaliser, still used to target the platform-probe per node.
259
213
  */
260
- var ENGINE_CASCADE_KEYS = [
261
- "engineBackend",
262
- "engineDevice",
263
- "probedBestEngine"
264
- ];
265
- function isEngineCascadeKey(key) {
266
- return ENGINE_CASCADE_KEYS.includes(key);
267
- }
268
214
  /**
269
- * Normalise a raw kernel node id to the bare node id used for scoping.
270
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
271
- * per-NODE, so strip the addon segment. Falls back to `hub`.
215
+ * Normalise a raw kernel node id to the bare node id. `localNodeId` can carry a
216
+ * `<node>/<addon>` suffix; probe targeting is per-NODE, so strip the addon
217
+ * segment. Falls back to `hub`.
272
218
  */
273
219
  function normalizeEngineNodeId(rawNodeId) {
274
220
  const raw = rawNodeId ?? "hub";
275
221
  return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
276
222
  }
277
- /** The node-scoped store key for an engine cascade field. */
278
- function nodeEngineKey(base, nodeId) {
279
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
280
- }
281
- /**
282
- * Read an engine cascade value for a node: the node-scoped key if present,
283
- * otherwise `undefined` — for EVERY node, the hub included. There is no
284
- * fallback to the legacy un-scoped key: a bare value in the store is inert.
285
- * A node with no scoped key gets `undefined` so its own platform-driven
286
- * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
287
- * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
288
- */
289
- function readNodeEngineValue(store, base, nodeId) {
290
- return store[nodeEngineKey(base, nodeId)];
291
- }
292
- /**
293
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
294
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
295
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
296
- * keys for OTHER nodes are dropped from the projection (not relevant to this
297
- * node's form).
298
- */
299
- function projectNodeEngine(store, nodeId) {
300
- const out = {};
301
- const scopedForAnyNode = /* @__PURE__ */ new Set();
302
- for (const key of Object.keys(store)) {
303
- const atIdx = key.indexOf("@");
304
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
305
- scopedForAnyNode.add(key);
306
- continue;
307
- }
308
- out[key] = store[key];
309
- }
310
- for (const base of ENGINE_CASCADE_KEYS) {
311
- const value = readNodeEngineValue(store, base, nodeId);
312
- if (value !== void 0) out[base] = value;
313
- }
314
- return out;
315
- }
316
223
  //#endregion
317
224
  //#region src/detection-pipeline/default-detection-model.ts
318
225
  /** The object-detection step id — the only slot this resolver applies to. */
@@ -1550,28 +1457,17 @@ var EngineFactory = class {
1550
1457
  //#endregion
1551
1458
  //#region src/detection-pipeline/engine/resolve-device-engine.ts
1552
1459
  /**
1553
- * Backend → model format for a device pool. Mirrors the Phase-1 device
1554
- * enumeration (`enumerateInferenceDevices`) and the pool-runtime mapping.
1555
- */
1556
- var BACKEND_FORMAT = {
1557
- openvino: "openvino",
1558
- edgetpu: "tflite",
1559
- onnx: "onnx",
1560
- cpu: "onnx",
1561
- cuda: "onnx",
1562
- coreml: "coreml"
1563
- };
1564
- /**
1565
1460
  * Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
1566
1461
  * {@link PipelineEngineChoice} used to build that device's inference pool.
1567
1462
  * Throws on an unknown backend token so a bad `deviceKey` fails loud rather
1568
- * than silently building a wrong pool.
1463
+ * than silently building a wrong pool. Format comes from the shared
1464
+ * {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
1569
1465
  */
1570
1466
  function resolveDeviceEngine(deviceKey) {
1571
1467
  const colon = deviceKey.indexOf(":");
1572
1468
  const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
1573
1469
  const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
1574
- const format = BACKEND_FORMAT[backend];
1470
+ const format = require_dist.DEVICE_BACKEND_TO_FORMAT[backend];
1575
1471
  if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
1576
1472
  return {
1577
1473
  runtime: "python",
@@ -1581,6 +1477,69 @@ function resolveDeviceEngine(deviceKey) {
1581
1477
  };
1582
1478
  }
1583
1479
  //#endregion
1480
+ //#region src/detection-pipeline/engine/idle-pool-reaper.ts
1481
+ var IdlePoolReaper = class {
1482
+ lastUsed = /* @__PURE__ */ new Map();
1483
+ timers = /* @__PURE__ */ new Map();
1484
+ ttlMs;
1485
+ disposeFn;
1486
+ now;
1487
+ setTimer;
1488
+ clearTimer;
1489
+ onError;
1490
+ constructor(opts) {
1491
+ this.ttlMs = opts.ttlMs;
1492
+ this.disposeFn = opts.dispose;
1493
+ this.now = opts.now ?? (() => Date.now());
1494
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1495
+ this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
1496
+ this.onError = opts.onError;
1497
+ }
1498
+ /** Record a use of `key` and (re)arm its idle-eviction timer. */
1499
+ touch(key) {
1500
+ this.lastUsed.set(key, this.now());
1501
+ this.arm(key);
1502
+ }
1503
+ /** Stop tracking `key` (the pool was disposed/removed elsewhere). */
1504
+ cancel(key) {
1505
+ const timer = this.timers.get(key);
1506
+ if (timer) this.clearTimer(timer);
1507
+ this.timers.delete(key);
1508
+ this.lastUsed.delete(key);
1509
+ }
1510
+ /** Clear every timer (shutdown). Does not dispose pools — the caller does. */
1511
+ disposeAll() {
1512
+ for (const timer of this.timers.values()) this.clearTimer(timer);
1513
+ this.timers.clear();
1514
+ this.lastUsed.clear();
1515
+ }
1516
+ arm(key) {
1517
+ const existing = this.timers.get(key);
1518
+ if (existing) this.clearTimer(existing);
1519
+ this.timers.set(key, this.setTimer(() => {
1520
+ this.reap(key);
1521
+ }, this.ttlMs));
1522
+ }
1523
+ async reap(key) {
1524
+ const last = this.lastUsed.get(key);
1525
+ if (last === void 0) return;
1526
+ const idleMs = this.now() - last;
1527
+ if (idleMs < this.ttlMs) {
1528
+ this.timers.set(key, this.setTimer(() => {
1529
+ this.reap(key);
1530
+ }, this.ttlMs - idleMs));
1531
+ return;
1532
+ }
1533
+ this.timers.delete(key);
1534
+ this.lastUsed.delete(key);
1535
+ try {
1536
+ await this.disposeFn(key);
1537
+ } catch (err) {
1538
+ this.onError?.(key, err);
1539
+ }
1540
+ }
1541
+ };
1542
+ //#endregion
1584
1543
  //#region src/detection-pipeline/engine-provisioner.ts
1585
1544
  /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
1586
1545
  var BACKOFF_SCHEDULE_MS = [
@@ -3937,26 +3896,6 @@ var PROBE_DIRECT_RPC_TIMEOUT_MS = 6e4;
3937
3896
  * result is identical (onnx / cpu) but is now derived through the shared rules
3938
3897
  * instead of duplicated inline.
3939
3898
  */
3940
- function onnxFloorPick() {
3941
- return ONNX_FLOOR;
3942
- }
3943
- /**
3944
- * The platform-deterministic engine pick computed SYNCHRONOUSLY from this node's
3945
- * own `process.platform`/`arch` alone (no probe): darwin → coreml, else → onnx
3946
- * (gpu-dependent openvino/cuda need the probe and converge via the auto-pick).
3947
- * Used as the ENGINE fallback when a persisted selection is unsupported on this
3948
- * node — so a stale GLOBAL engine config (e.g. the cluster's OpenVINO choice)
3949
- * can never force an impossible engine onto a node whose platform rejects it.
3950
- */
3951
- function platformDefaultPick() {
3952
- const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
3953
- return {
3954
- runtime: "python",
3955
- backend: pick.runtimeId,
3956
- format: modelFormatFor(pick.runtimeId),
3957
- device: pick.device
3958
- };
3959
- }
3960
3899
  /**
3961
3900
  * Is `backend` even POSSIBLE on this node's OS/arch (ignoring gpu detail)?
3962
3901
  * coreml ⇒ darwin only; openvino ⇒ x64 non-darwin only; onnx ⇒ anywhere. Used to
@@ -4072,6 +4011,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4072
4011
  */
4073
4012
  overrideCache = null;
4074
4013
  overrideCacheTimer = null;
4014
+ /**
4015
+ * Idle-TTL for a per-device inference pool (multi-device C3). A device pool
4016
+ * with no dispatch for this long is disposed and recreated on next use — the
4017
+ * memory guardrail that keeps `factoriesByDevice` from accumulating warm pools
4018
+ * forever. Longer than the override cache (60s) since a device pool is heavier
4019
+ * to respin; still well above any real inter-session gap for an active camera.
4020
+ */
4021
+ static DEVICE_POOL_IDLE_TTL_MS = 12e4;
4022
+ deviceReaper = new IdlePoolReaper({
4023
+ ttlMs: DetectionPipelineProvider.DEVICE_POOL_IDLE_TTL_MS,
4024
+ dispose: (deviceKey) => this.disposeDevicePool(deviceKey),
4025
+ onError: (deviceKey, err) => this.log.warn("Idle device pool dispose failed", { meta: {
4026
+ deviceKey,
4027
+ error: err instanceof Error ? err.message : String(err)
4028
+ } })
4029
+ });
4075
4030
  static OVERRIDE_CACHE_TTL_MS = 6e4;
4076
4031
  /** Read the addon settings store (all keys). */
4077
4032
  readStore;
@@ -4099,20 +4054,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4099
4054
  format: this.currentEngine.format
4100
4055
  } });
4101
4056
  }
4102
- /** Load persisted engine choice. Call after construction. */
4057
+ /**
4058
+ * Call after construction. The node-default engine is always DERIVED live
4059
+ * from this node's own hardware (there is no persisted operator override
4060
+ * anymore — the elected single-engine cascade was removed). Defer the derive
4061
+ * to `setApi()` where the probe cap / kernel resolver is reachable.
4062
+ */
4103
4063
  async init() {
4104
- const stored = await this.loadEngine();
4105
- if (stored) {
4106
- this.currentEngine = stored;
4107
- this.log.info("Engine loaded (persisted)", { meta: {
4108
- runtime: stored.runtime,
4109
- backend: stored.backend,
4110
- format: stored.format
4111
- } });
4112
- } else {
4113
- this.needsAutoPick = true;
4114
- this.log.info("No persisted engine — auto-pick deferred to setApi()");
4115
- }
4064
+ this.needsAutoPick = true;
4065
+ this.log.info("Node-default engine derive deferred to setApi()");
4116
4066
  }
4117
4067
  /**
4118
4068
  * Lazy-install the pip requirements file matching `engine.backend` into
@@ -4186,6 +4136,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4186
4136
  await this.resolveAutoEngine();
4187
4137
  this.needsAutoPick = false;
4188
4138
  this.startProvisioningForCurrentEngine();
4139
+ await this.recomputeConfigIssues().catch((err) => {
4140
+ this.log.warn("config-issue recompute failed after node-default derive", { meta: { error: require_dist.errMsg(err) } });
4141
+ });
4189
4142
  }
4190
4143
  /**
4191
4144
  * Authoritative platform-probe reconcile: query the probe provider
@@ -4213,34 +4166,38 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4213
4166
  }
4214
4167
  }
4215
4168
  /**
4216
- * Resolve the `'auto'` engine LIVE from this node's own kernel inference
4169
+ * Derive the node-default engine LIVE from this node's own kernel inference
4217
4170
  * resolver (`ctx.kernel.inferenceEngine`) — the runtime mirror of
4218
4171
  * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
4219
4172
  * resolves its OWN accelerators in-process, so a forked agent child never
4220
4173
  * adopts the hub's runtime from the `platform-probe` singleton.
4221
4174
  *
4222
- * NEVER persists. `'auto'` is a live-resolved sentinel: the pick is recomputed
4223
- * every boot from the local hardware, so it is inherently self-healing — the
4224
- * old `autoPickAndPersist` sticky-onnx bug (a premature onnx pick persisted
4225
- * during a boot race, then locked forever) cannot recur because the store is
4226
- * never written here. Only an explicit operator override is persisted, and
4227
- * that write happens through the settings UI, not this path.
4175
+ * The default is the TOP-SCORED CONCRETE device from the SAME
4176
+ * `enumerateInferenceDevices` enumeration the orchestrator balances over
4177
+ * (e.g. `openvino:npu`, not `openvino:auto`) NOT `pickBestRuntime`, whose
4178
+ * `defaultDeviceFor('openvino')` is the abstract `'auto'` sentinel. This is
4179
+ * load-bearing for pool COLLAPSE (D2/NEW-1): a `deviceKey`-less/lone session
4180
+ * routes through the default pool, and a balanced session that picks the same
4181
+ * top device (`openvino:npu`) must reuse THAT pool via `resolveDeviceFactory`'s
4182
+ * `enginesEqual(engine, currentEngine)` check — which only matches when the
4183
+ * default carries the concrete device, not `'auto'`. An abstract `'auto'`
4184
+ * default would spawn a THIRD pool contending with the per-device npu/gpu pools
4185
+ * on the same physical accelerators (double model load / VRAM). Falls back to
4186
+ * the onnx/cpu floor when the enumeration is empty. NEVER persists — recomputed
4187
+ * every boot from the local hardware, inherently self-healing.
4228
4188
  */
4229
4189
  async resolveAutoEngine() {
4230
4190
  let hardware = null;
4231
4191
  try {
4232
4192
  hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
4233
4193
  } catch {}
4234
- const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
4235
- this.currentEngine = {
4236
- runtime: "python",
4237
- backend: pick.runtimeId,
4238
- format: modelFormatFor(pick.runtimeId),
4239
- device: pick.device
4240
- };
4241
- this.log.info("Resolved auto engine (local kernel)", { meta: {
4242
- backend: pick.runtimeId,
4243
- device: pick.device,
4194
+ const devices = require_dist.enumerateInferenceDevices(envToHardwareInfo(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null)));
4195
+ const top = devices.find((d) => d.available) ?? devices[0];
4196
+ this.currentEngine = top ? resolveDeviceEngine(top.key) : ONNX_FLOOR;
4197
+ this.log.info("Derived node-default engine (local kernel)", { meta: {
4198
+ deviceKey: top?.key ?? "onnx:cpu(floor)",
4199
+ backend: this.currentEngine.backend,
4200
+ device: this.currentEngine.device ?? null,
4244
4201
  hadHardware: hardware !== null
4245
4202
  } });
4246
4203
  }
@@ -4319,33 +4276,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4319
4276
  this.provisioner.select(runtimeId, device);
4320
4277
  }
4321
4278
  /**
4322
- * Re-trigger provisioning after the operator changes the engine
4323
- * cascade (`engineBackend`/`engineDevice`). Reloads the persisted
4324
- * selection into `this.currentEngine`, then `select()`s it — the
4325
- * machine cancels any superseded provisioning and drives the new
4326
- * runtime through installing → verifying → ready.
4327
- */
4328
- async onEngineSelectionChanged() {
4329
- const prev = this.currentEngine;
4330
- const stored = await this.loadEngine();
4331
- if (stored) this.currentEngine = stored;
4332
- else await this.resolveAutoEngine();
4333
- const engineChanged = prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "");
4334
- if (engineChanged && this.engineFactory) {
4335
- this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4336
- from: `${prev.backend}/${prev.device ?? "default"}`,
4337
- to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
4338
- } });
4339
- try {
4340
- await this.engineFactory.dispose();
4341
- } catch {}
4342
- this.engineFactory = null;
4343
- this.executor = null;
4344
- }
4345
- this.startProvisioningForCurrentEngine();
4346
- if (engineChanged) await this.recomputeConfigIssues();
4347
- }
4348
- /**
4349
4279
  * Gate A (config-correctness gate at engine change) — eagerly re-resolve
4350
4280
  * the node-default step tree for `this.currentEngine.format` and compute
4351
4281
  * human-readable config issues from the result:
@@ -5789,6 +5719,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5789
5719
  this.engineFactory = null;
5790
5720
  this.executor = null;
5791
5721
  }
5722
+ this.deviceReaper.disposeAll();
5792
5723
  const deviceFactories = Array.from(this.factoriesByDevice.values());
5793
5724
  this.factoriesByDevice.clear();
5794
5725
  await Promise.all(deviceFactories.map((f) => f.dispose().catch(() => void 0)));
@@ -5932,7 +5863,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5932
5863
  return this.engineFactory;
5933
5864
  }
5934
5865
  const existing = this.factoriesByDevice.get(deviceKey);
5935
- if (existing) return existing;
5866
+ if (existing) {
5867
+ this.deviceReaper.touch(deviceKey);
5868
+ return existing;
5869
+ }
5936
5870
  const inflight = this.deviceFactoryInflight.get(deviceKey);
5937
5871
  if (inflight) return inflight;
5938
5872
  const creation = (async () => {
@@ -5953,6 +5887,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5953
5887
  throw err;
5954
5888
  }
5955
5889
  this.factoriesByDevice.set(deviceKey, factory);
5890
+ this.deviceReaper.touch(deviceKey);
5956
5891
  return factory;
5957
5892
  })();
5958
5893
  this.deviceFactoryInflight.set(deviceKey, creation);
@@ -5963,66 +5898,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5963
5898
  }
5964
5899
  }
5965
5900
  /**
5966
- * Phase 2b resolve the engine from the addon's new schema-backed
5967
- * fields (`engineRuntime`, `engineBackend`, `engineDevice`). The legacy
5968
- * `pipelineEngine` JSON-blob fallback was removed with the executor seed
5969
- * (any store predating the per-node cascade simply re-runs auto-pick).
5970
- *
5971
- * The `'auto'` sentinel is treated as NO override (returns null) so the
5972
- * caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
5973
- * self-healing default, never a persisted concrete backend. An explicit
5974
- * concrete backend (onnx/openvino/coreml) is honored as an operator override.
5975
- * Returns null when neither source has a concrete selection; the caller keeps
5976
- * the onnx floor set at construction until `resolveAutoEngine()` runs.
5901
+ * Dispose a single idle device pool (invoked by the {@link deviceReaper} when
5902
+ * `deviceKey` has had no dispatch for the idle-TTL). Removed from the map
5903
+ * FIRST so a concurrent `resolveDeviceFactory` recreates it fresh via the
5904
+ * single-flight guard rather than handing back the disposing instance.
5977
5905
  */
5978
- async loadEngine() {
5979
- const store = await this.readStore();
5980
- const storedRuntime = store["engineRuntime"];
5981
- const node = this.localProbeNodeId();
5982
- const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5983
- if (storedBackend === "auto") return null;
5984
- if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
5985
- const backend = storedBackend;
5986
- const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5987
- const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5988
- const floor = onnxFloorPick();
5989
- const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5990
- if (!backendPossibleOnPlatform(migratedBackend)) {
5991
- const platformDefault = platformDefaultPick();
5992
- this.log.warn("Stored engine backend impossible on this platform — using platform default", { meta: {
5993
- stored: migratedBackend,
5994
- fallback: platformDefault.backend
5995
- } });
5996
- this.writeStore({
5997
- [nodeEngineKey("engineBackend", node)]: platformDefault.backend,
5998
- ...platformDefault.device !== void 0 ? { [nodeEngineKey("engineDevice", node)]: platformDefault.device } : {}
5999
- }).catch((err) => {
6000
- this.log.warn("Failed to persist platform-default engine after impossible stored pick", { meta: { error: err instanceof Error ? err.message : String(err) } });
6001
- });
6002
- return platformDefault;
6003
- }
6004
- const device = storedDevice || floor.device;
6005
- return {
6006
- runtime: "python",
6007
- backend: migratedBackend,
6008
- format: backendToFormat(migratedBackend),
6009
- ...device ? { device } : {}
6010
- };
6011
- }
6012
- return null;
5906
+ async disposeDevicePool(deviceKey) {
5907
+ const factory = this.factoriesByDevice.get(deviceKey);
5908
+ if (!factory) return;
5909
+ this.factoriesByDevice.delete(deviceKey);
5910
+ await factory.dispose();
5911
+ this.log.info("Idle device pool evicted", { meta: {
5912
+ deviceKey,
5913
+ idleTtlMs: DetectionPipelineProvider.DEVICE_POOL_IDLE_TTL_MS
5914
+ } });
6013
5915
  }
6014
5916
  /**
6015
- * Re-run the platform probe for the inference engine and persist the
6016
- * result into the addon's own global-settings keys. Because those
6017
- * fields are declared `requiresRestart: true`, the
6018
- * `updateGlobalSettings` pathway auto-schedules an addon restart —
6019
- * here we do the write directly via the settings store and trigger
6020
- * restart manually through the addons cap.
6021
- *
6022
- * Returns the chosen engine so the UI can confirm the pick without a
6023
- * second round-trip.
6024
- */
6025
- /**
6026
5917
  * Phase 2c — flat per-addonId map holding the video-pipeline step
6027
5918
  * config (`modelId` / `settings`). No `enabled` field: whether an
6028
5919
  * addon step runs is a capability-binding concern, not a
@@ -6207,35 +6098,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6207
6098
  } });
6208
6099
  return { success: true };
6209
6100
  }
6210
- async reprobeEngine() {
6211
- let hardware = null;
6212
- try {
6213
- hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6214
- } catch {}
6215
- const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
6216
- const pick = pickBestRuntime(env, null);
6217
- const best = {
6218
- runtime: "python",
6219
- backend: pick.runtimeId,
6220
- format: modelFormatFor(pick.runtimeId),
6221
- device: pick.device
6222
- };
6223
- const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
6224
- const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
6225
- const rpNode = this.localProbeNodeId();
6226
- if (pick.runtimeId !== "onnx" || hardware !== null) {
6227
- await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
6228
- this.log.info("Re-probed engine (local kernel) — updated probedBestEngine hint", { meta: {
6229
- backend: best.backend,
6230
- device: best.device ?? null,
6231
- probedBestEngine: probedLabel
6232
- } });
6233
- } else this.log.info("Re-probe: onnx floor pending gpu probe — hint NOT persisted", { meta: {
6234
- backend: best.backend,
6235
- device: best.device ?? null
6236
- } });
6237
- return best;
6238
- }
6239
6101
  async getReferenceAudioFiles() {
6240
6102
  const dir = resolveReferenceAudioDir();
6241
6103
  if (!dir) return [];
@@ -6590,47 +6452,6 @@ function resolveAddonPythonDir() {
6590
6452
  for (const c of candidates) if (node_fs.existsSync(node_path.join(c, "inference_pool.py"))) return c;
6591
6453
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
6592
6454
  }
6593
- var BACKEND_TO_FORMAT = {
6594
- cpu: "onnx",
6595
- coreml: "coreml",
6596
- openvino: "openvino",
6597
- onnx: "onnx",
6598
- edgetpu: "tflite"
6599
- };
6600
- /**
6601
- * Every runtime the registry knows about, in offer order. Used only to build
6602
- * the STATIC base schema (returned by `globalSettingsSchema()` before a live
6603
- * ctx exists, e.g. addon-registry boot introspection). The live, hardware-
6604
- * gated subset is injected by `getGlobalSettings()` via `supportedRuntimes(env)`.
6605
- */
6606
- var ALL_RUNTIME_IDS = [
6607
- "onnx",
6608
- "openvino",
6609
- "coreml",
6610
- "edgetpu"
6611
- ];
6612
- /**
6613
- * Narrow an arbitrary stored backend string to a known `RuntimeId`. Legacy or
6614
- * unknown values (e.g. the removed `'cpu'` backend) fall back to the `onnx`
6615
- * floor so the registry lookup never throws.
6616
- */
6617
- function toRuntimeId(backend) {
6618
- return ALL_RUNTIME_IDS.find((id) => id === backend) ?? "onnx";
6619
- }
6620
- /** Static universe of backend options (every runtime). Labels from the registry. */
6621
- var STATIC_BACKEND_OPTIONS = ALL_RUNTIME_IDS.map((id) => ({
6622
- value: id,
6623
- label: runtimeLabel(id)
6624
- }));
6625
- /**
6626
- * Static device options for the schema's default backend. The live device
6627
- * list per the selected backend + probed hardware is injected by
6628
- * `getGlobalSettings()` via `runtimeDevices(backend, hardware)`.
6629
- */
6630
- var STATIC_DEFAULT_DEVICE_OPTIONS = runtimeDevices(ALL_RUNTIME_IDS[0], null).map((d) => ({
6631
- value: d.value,
6632
- label: d.label
6633
- }));
6634
6455
  var DEFAULT_CONFIG = {
6635
6456
  concurrency: 0,
6636
6457
  batchMode: "",
@@ -6639,17 +6460,8 @@ var DEFAULT_CONFIG = {
6639
6460
  numStreams: 0,
6640
6461
  intraOpThreads: 0,
6641
6462
  numWorkers: 0,
6642
- engineRuntime: "python",
6643
- engineBackend: "auto",
6644
- engineDevice: "cpu",
6645
- probedBestEngine: "",
6646
- activeEngine: "",
6647
6463
  activeModelSubstitutions: ""
6648
6464
  };
6649
- /** Derive the model-format from a backend value. Called by the provider. */
6650
- function backendToFormat(backend) {
6651
- return BACKEND_TO_FORMAT[backend] ?? "onnx";
6652
- }
6653
6465
  /**
6654
6466
  * Cadence for the per-node engine-inventory snapshot. Engines change
6655
6467
  * slowly (manual spin/kill, idle eviction, attach/detach). 5 s strikes
@@ -6742,63 +6554,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
6742
6554
  tab: "engine",
6743
6555
  order: 0,
6744
6556
  description: "Vision-detection execution provider (object detection, face detection, plate OCR). Inference always runs via the embedded Python runtime. Provider and hardware options are gated by what the platform probe reports as available on this host. Changes restart the addon.",
6745
- fields: [
6746
- this.field({
6747
- type: "text",
6748
- key: "probedBestEngine",
6749
- label: "Probed best",
6750
- description: "Auto-detected best engine for this host (format: provider/device, e.g. \"openvino/npu\"). Click the refresh icon to re-run the probe — the detected provider and hardware device are written back into the two fields below, overwriting any manual override.",
6751
- readonlyField: true,
6752
- default: "",
6753
- perNode: true,
6754
- actions: [{
6755
- action: "reprobe-engine",
6756
- icon: "refresh-cw",
6757
- tooltip: "Re-probe engine"
6758
- }]
6759
- }),
6760
- this.field({
6761
- type: "text",
6762
- key: "activeEngine",
6763
- label: "Active engine",
6764
- 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.",
6765
- readonlyField: true,
6766
- default: ""
6767
- }),
6768
- this.field({
6769
- type: "text",
6770
- key: "activeModelSubstitutions",
6771
- label: "Model substitutions",
6772
- description: "Node-local model substitutions currently in effect: a step's chosen model has no build for this node's engine format, so a compatible default is running instead (format: \"step: chosen→running\"). Hidden when every step matches.",
6773
- readonlyField: true,
6774
- default: "",
6775
- showWhen: {
6776
- field: "activeModelSubstitutions",
6777
- notEquals: ""
6778
- }
6779
- }),
6780
- this.field({
6781
- type: "select",
6782
- key: "engineBackend",
6783
- label: "Execution provider",
6784
- options: [{
6785
- value: "auto",
6786
- label: "Auto (best for this node)"
6787
- }, ...STATIC_BACKEND_OPTIONS],
6788
- default: DEFAULT_CONFIG.engineBackend,
6789
- immediate: true,
6790
- perNode: true
6791
- }),
6792
- this.field({
6793
- type: "select",
6794
- key: "engineDevice",
6795
- label: "Hardware device",
6796
- options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6797
- default: DEFAULT_CONFIG.engineDevice,
6798
- immediate: true,
6799
- perNode: true
6800
- })
6801
- ]
6557
+ fields: [this.field({
6558
+ type: "text",
6559
+ key: "activeModelSubstitutions",
6560
+ label: "Model substitutions",
6561
+ description: "Node-local model substitutions currently in effect: a step's chosen model has no build for this node's engine format, so a compatible default is running instead (format: \"step: chosen→running\"). Hidden when every step matches.",
6562
+ readonlyField: true,
6563
+ default: "",
6564
+ showWhen: {
6565
+ field: "activeModelSubstitutions",
6566
+ notEquals: ""
6567
+ }
6568
+ })]
6802
6569
  }, {
6803
6570
  id: "inference-executor",
6804
6571
  title: "Inference executor",
@@ -6827,15 +6594,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
6827
6594
  label: "Window (cross-camera accumulator)"
6828
6595
  }
6829
6596
  ],
6830
- default: "",
6831
- showWhen: {
6832
- field: "engineBackend",
6833
- in: [
6834
- "coreml",
6835
- "onnx",
6836
- "cpu"
6837
- ]
6838
- }
6597
+ default: ""
6839
6598
  }),
6840
6599
  this.field({
6841
6600
  type: "slider",
@@ -6901,59 +6660,31 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
6901
6660
  }] });
6902
6661
  }
6903
6662
  /**
6904
- * Override to inject the hardware-driven backend + device options.
6663
+ * Override to inject the read-only model-substitution status + the
6664
+ * per-backend tuning-slider defaults.
6905
6665
  *
6906
- * The OFFERED backend list comes from `supportedRuntimes(env)` platform,
6907
- * arch and probed hardware ONLY, never from which Python packages are
6908
- * installed. A host's hardware is what it can physically run; install state
6909
- * is provisioned on demand (Phase 2). Device options come from
6910
- * `runtimeDevices(backend, hardware)` (probe-driven; default-only when the
6911
- * probe is unreachable). Stored backend / device snap back to the registry
6912
- * floor / default when they fall outside the offered set.
6666
+ * Engine SELECTION is no longer operator-facing (the elected single-engine
6667
+ * cascade was removed): each node runs a device-array of inference pools
6668
+ * balanced per-session, and the node-default pool is derived live from the
6669
+ * node's own hardware. So this override no longer offers a backend/device
6670
+ * dropdown or a probed/active-engine readout — it only:
6671
+ * 1. resolves the node's derived-default backend (via the probe) to suggest
6672
+ * the matrix-tuned slider defaults that won the bench for that backend, and
6673
+ * 2. surfaces the live `activeModelSubstitutions` readout.
6913
6674
  */
6914
6675
  async getGlobalSettings(overlay, _cap, nodeId) {
6915
6676
  const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6916
6677
  const rawStored = await this.resolveUiSettingsStore();
6917
6678
  if (rawStored === null) return null;
6918
- const stored = projectNodeEngine(rawStored, targetNode);
6919
- const storedBackendRaw = stored["engineBackend"];
6920
- if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6921
6679
  const merged = overlay ? {
6922
- ...stored,
6680
+ ...rawStored,
6923
6681
  ...overlay
6924
- } : stored;
6925
- const env = await this.probeHardwareEnv(targetNode);
6926
- const hardware = env.hardware;
6927
- const offered = supportedRuntimes(env);
6928
- const runtimeBackends = [{
6929
- value: "auto",
6930
- label: "Auto (best for this node)"
6931
- }, ...offered.map((id) => ({
6932
- value: id,
6933
- label: runtimeLabel(id)
6934
- }))];
6935
- const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6936
- const isAuto = storedBackend === "auto";
6937
- const backend = (() => {
6938
- if (isAuto) return pickBestRuntime(env, null).runtimeId;
6939
- const rid = toRuntimeId(storedBackend);
6940
- if (rid === "onnx") return "onnx";
6941
- if (offered.includes(rid)) return rid;
6942
- return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6943
- })();
6944
- const deviceOptions = runtimeDevices(backend, hardware);
6945
- const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6946
- const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6947
- const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6948
- const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6682
+ } : rawStored;
6683
+ const backend = pickBestRuntime(await this.probeHardwareEnv(targetNode), null).runtimeId;
6949
6684
  const substitutions = this.provider?.getActiveModelSubstitutions() ?? [];
6950
6685
  const activeModelSubstitutions = substitutions.length === 0 ? "" : substitutions.map((s) => `${s.addonId}: ${s.chosen}→${s.running}`).join(", ");
6951
- const displayBackend = isAuto ? "auto" : backend;
6952
6686
  const raw = {
6953
6687
  ...merged,
6954
- engineBackend: displayBackend,
6955
- engineDevice: device,
6956
- activeEngine,
6957
6688
  activeModelSubstitutions
6958
6689
  };
6959
6690
  const schema = this.globalSettingsSchema();
@@ -6965,18 +6696,6 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
6965
6696
  ...section,
6966
6697
  fields: section.fields.map((field) => {
6967
6698
  if (field.type === "select") {
6968
- if (field.key === "engineBackend") return {
6969
- ...field,
6970
- options: [...runtimeBackends]
6971
- };
6972
- if (field.key === "engineDevice") return {
6973
- ...field,
6974
- options: deviceOptions.map((d) => ({
6975
- value: d.value,
6976
- label: d.label
6977
- })),
6978
- default: defaultDeviceFor(backend)
6979
- };
6980
6699
  if (field.key === "batchMode" && tuning["batchMode"] !== void 0) return {
6981
6700
  ...field,
6982
6701
  default: tuning["batchMode"]
@@ -7109,7 +6828,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
7109
6828
  * a reason to disagree.
7110
6829
  */
7111
6830
  resolveBackendTuning() {
7112
- const t = tuningFor(this.config.engineBackend === "auto" ? this.provider?.resolvedEngineBackend() ?? "onnx" : toRuntimeId(this.config.engineBackend));
6831
+ const t = tuningFor(this.provider?.resolvedEngineBackend() ?? "onnx");
7113
6832
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
7114
6833
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
7115
6834
  return {
@@ -7172,9 +6891,6 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
7172
6891
  await this.provider.init();
7173
6892
  await this.provider.setApi(this.ctx);
7174
6893
  }
7175
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
7176
- this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7177
- });
7178
6894
  await this.provider.warmPool();
7179
6895
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
7180
6896
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -7288,21 +7004,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
7288
7004
  }
7289
7005
  /**
7290
7006
  * BaseAddon calls `onConfigChanged` after every settings write. This is the
7291
- * SOLE apply path for engine + tuning changes — none of those fields declare
7007
+ * SOLE apply path for tuning changes — none of those fields declare
7292
7008
  * `requiresRestart` anymore (an addon restart re-probes hardware + reloads
7293
- * every model on every set, which is exactly what we want to avoid). Instead:
7294
- * - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) in-place
7295
- * pool respawn when the snapshot flips (`poolConfigChanged` below).
7296
- * - Engine cascade (`engineBackend`/`engineDevice`) the provider's
7297
- * `onEngineSelectionChanged` disposes the device-bound factory so the next
7298
- * runPipeline rebuilds the pool on the new selection, in place.
7299
- * Both apply optimistically — the inference gate stays closed for the short
7300
- * re-spin, frames are dropped (never crashed), no addon bounce.
7009
+ * every model on every set, which is exactly what we want to avoid): pool-bound
7010
+ * tuning (`concurrency`/`batchMode`/`windowMs`/…) drives an in-place pool
7011
+ * respawn when the snapshot flips (`poolConfigChanged` below). The engine
7012
+ * SELECTION is no longer operator-facing — the node-default backend is derived
7013
+ * once at boot (`setApi` `resolveAutoEngine`) from static hardware, so there
7014
+ * is no per-write engine re-select. The respawn applies optimistically — the
7015
+ * inference gate stays closed for the short re-spin, frames are dropped (never
7016
+ * crashed), no addon bounce.
7301
7017
  */
7302
7018
  async onConfigChanged() {
7303
- if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
7304
- this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
7305
- });
7306
7019
  if (!this.poolConfigChanged()) return;
7307
7020
  if (!this.provider) {
7308
7021
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -7329,7 +7042,6 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
7329
7042
  //#endregion
7330
7043
  exports.ALL_STEPS = require_step_definitions.ALL_STEPS;
7331
7044
  exports.DetectionPipelineProvider = DetectionPipelineProvider;
7332
- exports.backendToFormat = backendToFormat;
7333
7045
  exports.default = DetectionPipelineAddon;
7334
7046
  exports.getDefaultModelForFormat = require_step_definitions.getDefaultModelForFormat;
7335
7047
  exports.getStepDefinition = require_step_definitions.getStepDefinition;