@camstack/addon-pipeline 1.2.127 → 1.2.129

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 (35) hide show
  1. package/dist/{addon-utils-BnprivSs.js → addon-utils-CKB-2oM9.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +241 -95
  5. package/dist/detection-pipeline/index.mjs +239 -93
  6. package/dist/{dist-HLAOw6Pe.mjs → dist-CMio-1qq.mjs} +47 -31
  7. package/dist/{dist-BZUXb5S5.js → dist-D9x2_csL.js} +47 -31
  8. package/dist/{event-loop-stall-monitor-BH-cL_hg.mjs → event-loop-stall-monitor-B9dxYxyf.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-BdArVUlx.js → event-loop-stall-monitor-UW8nsiMb.js} +1 -1
  10. package/dist/{lazy-sharp-DTDXrhMG.js → lazy-sharp-DrLwLKpg.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-CDPRLalN.mjs → process-memory-C6es8dMA.mjs} +1 -1
  16. package/dist/{process-memory-Dh1SyzdJ.js → process-memory-CSJ7-Wlp.js} +1 -1
  17. package/dist/recorder/index.js +121 -33
  18. package/dist/recorder/index.mjs +120 -32
  19. package/dist/{segment-demux-js-D-TLcHW5.mjs → segment-demux-js-BRCjXbI0.mjs} +1 -1
  20. package/dist/{segment-demux-js-DRutggd6.js → segment-demux-js-DiLDoV9_.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +1 -1
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BDR5z0Z9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-1O99xPTr.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxppPmXr.mjs +26 -0
  26. package/dist/stream-broker/demux-worker-child.js +1 -1
  27. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  28. package/dist/stream-broker/{hostInit-Dmw5LWmP.mjs → hostInit-8scvCWua.mjs} +2 -2
  29. package/dist/stream-broker/index.js +530 -154
  30. package/dist/stream-broker/index.mjs +530 -154
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/dist/{worker-protocol-h7Smb_0i.mjs → worker-protocol-BHNWB9Ve.mjs} +1 -1
  33. package/dist/{worker-protocol-BxkCoVjw.js → worker-protocol-BmBYCioC.js} +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cs8bmrWP.mjs +0 -26
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BZUXb5S5.js");
1
+ const require_dist = require("./dist-D9x2_csL.js");
2
2
  require("node:crypto");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_dist.__toESM(node_path, 1);
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BZUXb5S5.js");
6
- const require_process_memory = require("../process-memory-Dh1SyzdJ.js");
7
- const require_addon_utils = require("../addon-utils-BnprivSs.js");
5
+ const require_dist = require("../dist-D9x2_csL.js");
6
+ const require_process_memory = require("../process-memory-CSJ7-Wlp.js");
7
+ const require_addon_utils = require("../addon-utils-CKB-2oM9.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_dist.__toESM(node_path);
10
10
  let node_fs = require("node:fs");
@@ -1,6 +1,6 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { A as audioAnalyzerCapability, Dt as hydrateSchema, J as mapAudioLabelToMacro, St as BaseAddon, _ as HF_BASE_URL, gt as errMsg, k as audioAnalysisCapability, lt as resolvePoolMemoryPolicy, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, x as PoolMemoryWatchdog } from "../dist-HLAOw6Pe.mjs";
3
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-CDPRLalN.mjs";
2
+ import { A as audioAnalyzerCapability, Dt as hydrateSchema, J as mapAudioLabelToMacro, St as BaseAddon, _ as HF_BASE_URL, gt as errMsg, k as audioAnalysisCapability, lt as resolvePoolMemoryPolicy, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, x as PoolMemoryWatchdog } from "../dist-CMio-1qq.mjs";
3
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-C6es8dMA.mjs";
4
4
  import { i as downloadFile } from "../addon-utils-CgBCF-mE.mjs";
5
5
  import * as path$1 from "node:path";
6
6
  import * as fs from "node:fs";
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BZUXb5S5.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-BdArVUlx.js");
7
- const require_lazy_sharp = require("../lazy-sharp-DTDXrhMG.js");
8
- const require_process_memory = require("../process-memory-Dh1SyzdJ.js");
9
- const require_addon_utils = require("../addon-utils-BnprivSs.js");
5
+ const require_dist = require("../dist-D9x2_csL.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-UW8nsiMb.js");
7
+ const require_lazy_sharp = require("../lazy-sharp-DrLwLKpg.js");
8
+ const require_process_memory = require("../process-memory-CSJ7-Wlp.js");
9
+ const require_addon_utils = require("../addon-utils-CKB-2oM9.js");
10
10
  let sharp = require("sharp");
11
11
  sharp = require_dist.__toESM(sharp);
12
12
  let node_child_process = require("node:child_process");
@@ -235,6 +235,94 @@ function pickBestRuntime(env, bestBackendHint) {
235
235
  };
236
236
  }
237
237
  //#endregion
238
+ //#region src/detection-pipeline/engine/pool-provisioning.ts
239
+ /** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
240
+ var OVERRIDABLE_KEYS = [
241
+ "concurrency",
242
+ "numWorkers",
243
+ "batchMode",
244
+ "windowMs",
245
+ "maxBatchSize",
246
+ "numStreams",
247
+ "intraOpThreads"
248
+ ];
249
+ /** Backends that are not a runtime of their own and run on onnxruntime. */
250
+ var BACKEND_TO_RUNTIME = {
251
+ onnx: "onnx",
252
+ cpu: "onnx",
253
+ cuda: "onnx",
254
+ openvino: "openvino",
255
+ coreml: "coreml",
256
+ edgetpu: "edgetpu"
257
+ };
258
+ /**
259
+ * The registry runtime a device-pool backend provisions from. Unknown backends
260
+ * floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
261
+ * format, so a pool never comes up unprovisioned.
262
+ */
263
+ function runtimeIdForBackend(backend) {
264
+ return BACKEND_TO_RUNTIME[backend] ?? "onnx";
265
+ }
266
+ /**
267
+ * Resolve the provisioning for a pool running `engine`, applying the precedence
268
+ * documented at the top of this module. `engine.device` is not consulted today
269
+ * (see the module doc) but is part of the signature so a bench-backed
270
+ * per-device row lands here and nowhere else.
271
+ */
272
+ function resolvePoolProvisioning(engine, override = {}) {
273
+ const runtime = runtimeIdForBackend(engine.backend);
274
+ const profile = tuningFor(runtime);
275
+ return {
276
+ runtime,
277
+ overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
278
+ concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
279
+ numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
280
+ batchMode: override.batchMode ?? profile.batchMode,
281
+ windowMs: override.windowMs ?? profile.windowMs,
282
+ maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
283
+ numStreams: override.numStreams ?? profile.numStreams,
284
+ intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
285
+ };
286
+ }
287
+ function positiveNumber(value) {
288
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
289
+ }
290
+ function batchMode(value) {
291
+ return value === "none" || value === "list" || value === "window" ? value : void 0;
292
+ }
293
+ /**
294
+ * Read the operator override out of the detection addon's settings. Only a
295
+ * NON-AUTO value counts: the sliders persist `0` and the select persists `''`
296
+ * for "Auto (backend default)", and those must resolve to the runtime's
297
+ * registry value, not to a literal 0 worker threads.
298
+ *
299
+ * The result is deliberately sparse — an empty object means "nothing
300
+ * overridden", which is what lets a per-device pool provision from its own
301
+ * runtime instead of inheriting the node's.
302
+ */
303
+ function readOperatorOverride(settings) {
304
+ const override = {};
305
+ const concurrency = positiveNumber(settings.concurrency);
306
+ if (concurrency !== void 0) override.concurrency = concurrency;
307
+ const numWorkers = positiveNumber(settings.numWorkers);
308
+ if (numWorkers !== void 0) override.numWorkers = numWorkers;
309
+ const mode = batchMode(settings.batchMode);
310
+ if (mode !== void 0) override.batchMode = mode;
311
+ const windowMs = positiveNumber(settings.windowMs);
312
+ if (windowMs !== void 0) override.windowMs = windowMs;
313
+ const maxBatchSize = positiveNumber(settings.maxBatchSize);
314
+ if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
315
+ const numStreams = positiveNumber(settings.numStreams);
316
+ if (numStreams !== void 0) override.numStreams = numStreams;
317
+ const intraOpThreads = positiveNumber(settings.intraOpThreads);
318
+ if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
319
+ return override;
320
+ }
321
+ /** Two overrides carry the same fields with the same values. */
322
+ function overridesEqual(a, b) {
323
+ return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
324
+ }
325
+ //#endregion
238
326
  //#region src/detection-pipeline/engine-store-keys.ts
239
327
  /**
240
328
  * Node-id normalisation for the detection pipeline.
@@ -445,6 +533,122 @@ function resolveDefaultDetectionModel(backend, device, format, getStepDef = requ
445
533
  return require_event_loop_stall_monitor.getDefaultModelForFormatFromDef(def, format);
446
534
  }
447
535
  //#endregion
536
+ //#region src/detection-pipeline/device-override-mirror.ts
537
+ /**
538
+ * The per-device detection overrides, kept fresh OFF the frame path.
539
+ *
540
+ * ## The read this replaces
541
+ *
542
+ * `runPipeline` resolved a device's overrides by calling
543
+ * `settings.readDeviceStore(deviceId)` on **every inference frame**. Measured
544
+ * on the live hub 2026-08-25: `get detection-pipeline:addon-devices`, 1 143
545
+ * calls a minute at 0.44 rows per call — the disk asked, nineteen times a
546
+ * second, whether an operator has ever changed a threshold.
547
+ *
548
+ * ## Why this is not "a cache in the consumer" (D224)
549
+ *
550
+ * D224 forbids a SECOND authority over a fact that belongs to somebody else:
551
+ * the failure it records is a consumer caching a value whose write path does
552
+ * not run through that consumer, so an operator's own change is the one read
553
+ * the cache gets wrong.
554
+ *
555
+ * This addon OWNS the row. `<addonId>:addon-devices` is detection-pipeline's
556
+ * own store, and the only writers are this addon's `updateDeviceSettings` and
557
+ * its `clearDeviceOverrides` — both of which call {@link invalidate}. A write
558
+ * is therefore its own invalidation, exactly as D224 requires, and the window
559
+ * below governs only the case nobody writes through this process.
560
+ *
561
+ * ## The window, declared
562
+ *
563
+ * {@link DEVICE_OVERRIDE_STALE_MS} is what an operator's change costs on a node
564
+ * that did NOT serve the write — this addon runs per node, and a settings write
565
+ * lands on one of them. Thirty seconds is the trade: it removes 99.9% of the
566
+ * reads (one per device per window instead of one per frame) and it is short
567
+ * enough that "I moved the threshold and nothing happened" is not a support
568
+ * question. Ten minutes — {@link OPERATOR_WRITTEN_STALE_MS}, the constant for a
569
+ * cap whose value only an operator changes — is right for a POLLED reader that
570
+ * costs a camera round trip; this read costs one local SQLite `get`, and the
571
+ * cheap read buys a short window.
572
+ *
573
+ * A stale entry is SERVED and revalidated behind the caller, never awaited: a
574
+ * frame that waits for the settings store is a frame the pipeline dropped, and
575
+ * the reader is by definition about to ask again.
576
+ */
577
+ /**
578
+ * How long a mirrored entry is served before a refresh is started behind the
579
+ * next reader. See the module doc for why it is 30 s and not ten minutes.
580
+ */
581
+ var DEVICE_OVERRIDE_STALE_MS = 3e4;
582
+ /**
583
+ * How long a FAILED refresh is not retried.
584
+ *
585
+ * A refresh that throws leaves `fetchedAt` where it was, so without a cooldown
586
+ * every subsequent frame would start another one — a broken settings store
587
+ * would be hammered at frame rate by the very mechanism built to stop that.
588
+ */
589
+ var DEVICE_OVERRIDE_MISS_COOLDOWN_MS = 5e3;
590
+ var DeviceOverrideMirror = class {
591
+ deps;
592
+ staleMs;
593
+ missCooldownMs;
594
+ entries = /* @__PURE__ */ new Map();
595
+ /** In-flight reads, so a burst of frames on a cold device costs ONE read and
596
+ * not one per frame — the exact failure this class exists to remove. */
597
+ inFlight = /* @__PURE__ */ new Map();
598
+ now;
599
+ constructor(deps, staleMs = DEVICE_OVERRIDE_STALE_MS, missCooldownMs = DEVICE_OVERRIDE_MISS_COOLDOWN_MS) {
600
+ this.deps = deps;
601
+ this.staleMs = staleMs;
602
+ this.missCooldownMs = missCooldownMs;
603
+ this.now = deps.now ?? (() => Date.now());
604
+ }
605
+ /**
606
+ * The device's overrides.
607
+ *
608
+ * COLD awaits — the first frame on a camera has to get the real answer, and a
609
+ * default is exactly what "the operator lowered this threshold" looks like.
610
+ * WARM returns immediately, stale or not, and starts the refresh behind the
611
+ * caller.
612
+ */
613
+ async resolve(deviceId) {
614
+ const entry = this.entries.get(deviceId);
615
+ if (entry === void 0) return this.load(deviceId);
616
+ if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
617
+ return entry.value;
618
+ }
619
+ /** Drop a device's entry. Called by every writer in this process, so an
620
+ * operator's own change is never served from the mirror. */
621
+ invalidate(deviceId) {
622
+ this.entries.delete(deviceId);
623
+ }
624
+ invalidateAll() {
625
+ this.entries.clear();
626
+ }
627
+ async load(deviceId) {
628
+ const pending = this.inFlight.get(deviceId);
629
+ if (pending !== void 0) return pending;
630
+ const run = this.deps.read(deviceId).then((value) => {
631
+ this.entries.set(deviceId, {
632
+ value,
633
+ fetchedAt: this.now()
634
+ });
635
+ return value;
636
+ }).catch((err) => {
637
+ this.deps.onRefreshFailed?.(deviceId, err);
638
+ const existing = this.entries.get(deviceId);
639
+ if (existing !== void 0) {
640
+ existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
641
+ return existing.value;
642
+ }
643
+ return {};
644
+ }).finally(() => {
645
+ this.inFlight.delete(deviceId);
646
+ });
647
+ this.inFlight.set(deviceId, run);
648
+ return run;
649
+ }
650
+ };
651
+ //#endregion
448
652
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
449
653
  /**
450
654
  * SharedInferencePool — TypeScript wrapper for inference_pool.py.
@@ -1987,94 +2191,6 @@ function deviceKeyOf(engine) {
1987
2191
  return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
1988
2192
  }
1989
2193
  //#endregion
1990
- //#region src/detection-pipeline/engine/pool-provisioning.ts
1991
- /** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
1992
- var OVERRIDABLE_KEYS = [
1993
- "concurrency",
1994
- "numWorkers",
1995
- "batchMode",
1996
- "windowMs",
1997
- "maxBatchSize",
1998
- "numStreams",
1999
- "intraOpThreads"
2000
- ];
2001
- /** Backends that are not a runtime of their own and run on onnxruntime. */
2002
- var BACKEND_TO_RUNTIME = {
2003
- onnx: "onnx",
2004
- cpu: "onnx",
2005
- cuda: "onnx",
2006
- openvino: "openvino",
2007
- coreml: "coreml",
2008
- edgetpu: "edgetpu"
2009
- };
2010
- /**
2011
- * The registry runtime a device-pool backend provisions from. Unknown backends
2012
- * floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
2013
- * format, so a pool never comes up unprovisioned.
2014
- */
2015
- function runtimeIdForBackend(backend) {
2016
- return BACKEND_TO_RUNTIME[backend] ?? "onnx";
2017
- }
2018
- /**
2019
- * Resolve the provisioning for a pool running `engine`, applying the precedence
2020
- * documented at the top of this module. `engine.device` is not consulted today
2021
- * (see the module doc) but is part of the signature so a bench-backed
2022
- * per-device row lands here and nowhere else.
2023
- */
2024
- function resolvePoolProvisioning(engine, override = {}) {
2025
- const runtime = runtimeIdForBackend(engine.backend);
2026
- const profile = tuningFor(runtime);
2027
- return {
2028
- runtime,
2029
- overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
2030
- concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
2031
- numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
2032
- batchMode: override.batchMode ?? profile.batchMode,
2033
- windowMs: override.windowMs ?? profile.windowMs,
2034
- maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
2035
- numStreams: override.numStreams ?? profile.numStreams,
2036
- intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
2037
- };
2038
- }
2039
- function positiveNumber(value) {
2040
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
2041
- }
2042
- function batchMode(value) {
2043
- return value === "none" || value === "list" || value === "window" ? value : void 0;
2044
- }
2045
- /**
2046
- * Read the operator override out of the detection addon's settings. Only a
2047
- * NON-AUTO value counts: the sliders persist `0` and the select persists `''`
2048
- * for "Auto (backend default)", and those must resolve to the runtime's
2049
- * registry value, not to a literal 0 worker threads.
2050
- *
2051
- * The result is deliberately sparse — an empty object means "nothing
2052
- * overridden", which is what lets a per-device pool provision from its own
2053
- * runtime instead of inheriting the node's.
2054
- */
2055
- function readOperatorOverride(settings) {
2056
- const override = {};
2057
- const concurrency = positiveNumber(settings.concurrency);
2058
- if (concurrency !== void 0) override.concurrency = concurrency;
2059
- const numWorkers = positiveNumber(settings.numWorkers);
2060
- if (numWorkers !== void 0) override.numWorkers = numWorkers;
2061
- const mode = batchMode(settings.batchMode);
2062
- if (mode !== void 0) override.batchMode = mode;
2063
- const windowMs = positiveNumber(settings.windowMs);
2064
- if (windowMs !== void 0) override.windowMs = windowMs;
2065
- const maxBatchSize = positiveNumber(settings.maxBatchSize);
2066
- if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
2067
- const numStreams = positiveNumber(settings.numStreams);
2068
- if (numStreams !== void 0) override.numStreams = numStreams;
2069
- const intraOpThreads = positiveNumber(settings.intraOpThreads);
2070
- if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
2071
- return override;
2072
- }
2073
- /** Two overrides carry the same fields with the same values. */
2074
- function overridesEqual(a, b) {
2075
- return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
2076
- }
2077
- //#endregion
2078
2194
  //#region src/detection-pipeline/engine/engine-factory.ts
2079
2195
  var BACKEND_TO_POOL_RUNTIME = {
2080
2196
  coreml: "coreml",
@@ -6227,6 +6343,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6227
6343
  readDeviceStore;
6228
6344
  /** Delete all per-device settings for a device (used by `clearDeviceOverrides`). */
6229
6345
  clearDeviceStore;
6346
+ /**
6347
+ * The per-device overrides the FRAME path reads, kept fresh off that path.
6348
+ *
6349
+ * Public so the addon class can drop a device on its own
6350
+ * `updateDeviceSettings` — the write and the mirror have to be the same
6351
+ * process's, or the mirror becomes the second authority D224 forbids.
6352
+ */
6353
+ deviceOverrides;
6230
6354
  constructor(settings, modelsDir, logger, eventBus = null, detectionConfigSchemaSource = null, executorOptions = {}) {
6231
6355
  this.modelsDir = modelsDir;
6232
6356
  this.eventBus = eventBus;
@@ -6238,6 +6362,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6238
6362
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
6239
6363
  const clearDeviceStore = settings.clearDeviceStore;
6240
6364
  this.clearDeviceStore = clearDeviceStore ? (deviceId) => clearDeviceStore.call(settings, deviceId) : async () => void 0;
6365
+ this.deviceOverrides = new DeviceOverrideMirror({
6366
+ read: (deviceId) => this.readDeviceStore(deviceId),
6367
+ onRefreshFailed: (deviceId, err) => this.log.warn("per-device detection overrides refresh failed — serving the last value", {
6368
+ tags: { deviceId },
6369
+ meta: { error: require_dist.errMsg(err) }
6370
+ })
6371
+ });
6241
6372
  this.currentEngine = ONNX_FLOOR;
6242
6373
  this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
6243
6374
  runtime: this.currentEngine.runtime,
@@ -7422,7 +7553,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7422
7553
  setupMs = performance.now() - wallT0 - decodeMs;
7423
7554
  const isRuntime = Boolean(runtimeFrame || runtimeFrameRef);
7424
7555
  const effectiveDeviceId = input.deviceId ?? 0;
7425
- const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
7556
+ const deviceOverrides = effectiveDeviceId > 0 ? await this.deviceOverrides.resolve(effectiveDeviceId) : {};
7426
7557
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
7427
7558
  const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
7428
7559
  let cropZoneBbox;
@@ -8491,7 +8622,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8491
8622
  if (!api) return 0;
8492
8623
  let devices = [];
8493
8624
  try {
8494
- devices = await api.deviceManager.listAll.query({});
8625
+ devices = await api.deviceManager.listAll.query({ projection: "slim" });
8495
8626
  } catch (err) {
8496
8627
  this.log.warn("clearDeviceOverrides: device enumeration failed — skipping per-device clear", { meta: { error: require_dist.errMsg(err) } });
8497
8628
  return 0;
@@ -8499,6 +8630,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8499
8630
  let cleared = 0;
8500
8631
  for (const device of devices) try {
8501
8632
  await this.clearDeviceStore(device.id);
8633
+ this.deviceOverrides.invalidate(device.id);
8502
8634
  cleared++;
8503
8635
  } catch (err) {
8504
8636
  this.log.warn("clearDeviceOverrides: failed to clear per-device overrides for device", {
@@ -9056,6 +9188,20 @@ function isProfileSliderKey(key) {
9056
9188
  }
9057
9189
  var DetectionPipelineAddon = class extends require_dist.BaseAddon {
9058
9190
  provider = null;
9191
+ /**
9192
+ * An operator's own write drops the mirror the frame path reads.
9193
+ *
9194
+ * This override is the half of `device-override-mirror.ts` that keeps it from
9195
+ * being the second authority D224 forbids: `BaseAddon.updateDeviceSettings`
9196
+ * writes THIS addon's `<addonId>:addon-devices` row, so the write and the
9197
+ * mirror are in one process and the write invalidates. Without it the
9198
+ * operator's own change would be the one read the mirror got wrong, which is
9199
+ * the exact failure D224 records.
9200
+ */
9201
+ async updateDeviceSettings(deviceId, patch) {
9202
+ await super.updateDeviceSettings(deviceId, patch);
9203
+ this.provider?.deviceOverrides.invalidate(deviceId);
9204
+ }
9059
9205
  /** Last non-null probed hardware PER NODE — reused when the probe transiently
9060
9206
  * returns null so offered backends / device lists don't collapse. Keyed by
9061
9207
  * node because the hub addon probes other nodes when serving their config.