@camstack/addon-pipeline 1.2.130 → 1.2.132

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-DH78p79i.js → addon-utils-DzCn6z7v.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 +332 -20
  5. package/dist/detection-pipeline/index.mjs +330 -18
  6. package/dist/{dist-Wt81pN5Y.mjs → dist-BZYUUBUx.mjs} +1900 -1714
  7. package/dist/{dist-DOw-izoI.js → dist-DBP47QOz.js} +1900 -1714
  8. package/dist/{event-loop-stall-monitor-Bld7jNCb.js → event-loop-stall-monitor-CiH_MHfO.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-CtQCOZqm.mjs → event-loop-stall-monitor-IACE22fK.mjs} +1 -1
  10. package/dist/{lazy-sharp-BbNI2ZD-.js → lazy-sharp-B_kETjyQ.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-1_D3EZT9.mjs → process-memory-C7rsraCR.mjs} +1 -1
  16. package/dist/{process-memory-D9H5qjDl.js → process-memory-CtXQAA3d.js} +1 -1
  17. package/dist/recorder/index.js +326 -17
  18. package/dist/recorder/index.mjs +325 -16
  19. package/dist/{segment-demux-js-BW-Q-j2A.mjs → segment-demux-js-BL1UDdiI.mjs} +1 -1
  20. package/dist/{segment-demux-js-C2jBBqGW.js → segment-demux-js-wTzFi5UW.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 +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DlKJZ-_M.mjs} +3 -3
  25. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DXKTNEae.mjs} +1 -1
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUw_aTfK.mjs} +1 -1
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-DEVCEYgu.mjs → hostInit-kDKgfUjp.mjs} +3 -3
  30. package/dist/stream-broker/index.js +2 -2
  31. package/dist/stream-broker/index.mjs +2 -2
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-nAZl_Rsv.js → worker-protocol-C3zaz0Jv.js} +1 -1
  34. package/dist/{worker-protocol-rpnr7Ldd.mjs → worker-protocol-Cut8984B.mjs} +1 -1
  35. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DOw-izoI.js");
1
+ const require_dist = require("./dist-DBP47QOz.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-DOw-izoI.js");
6
- const require_process_memory = require("../process-memory-D9H5qjDl.js");
7
- const require_addon_utils = require("../addon-utils-DH78p79i.js");
5
+ const require_dist = require("../dist-DBP47QOz.js");
6
+ const require_process_memory = require("../process-memory-CtXQAA3d.js");
7
+ const require_addon_utils = require("../addon-utils-DzCn6z7v.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-Wt81pN5Y.mjs";
3
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-1_D3EZT9.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-BZYUUBUx.mjs";
3
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-C7rsraCR.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-DOw-izoI.js");
6
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Bld7jNCb.js");
7
- const require_lazy_sharp = require("../lazy-sharp-BbNI2ZD-.js");
8
- const require_process_memory = require("../process-memory-D9H5qjDl.js");
9
- const require_addon_utils = require("../addon-utils-DH78p79i.js");
5
+ const require_dist = require("../dist-DBP47QOz.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-CiH_MHfO.js");
7
+ const require_lazy_sharp = require("../lazy-sharp-B_kETjyQ.js");
8
+ const require_process_memory = require("../process-memory-CtXQAA3d.js");
9
+ const require_addon_utils = require("../addon-utils-DzCn6z7v.js");
10
10
  let sharp = require("sharp");
11
11
  sharp = require_dist.__toESM(sharp);
12
12
  let node_child_process = require("node:child_process");
@@ -908,15 +908,20 @@ var PoolWorker = class {
908
908
  } });
909
909
  this.rejectAll(err);
910
910
  });
911
- this.process.on("exit", (code) => {
912
- if (code !== 0 && code !== null) {
913
- this.log.error("Worker process exited", { meta: {
914
- worker: this.opts.workerLabel,
915
- code
916
- } });
917
- this.rejectAll(/* @__PURE__ */ new Error(`Worker process exited with code ${code}`));
918
- }
911
+ const spawnedProcess = this.process;
912
+ spawnedProcess.on("exit", (code, signal) => {
919
913
  this.ready = false;
914
+ if (this.process !== spawnedProcess) return;
915
+ this.log.error("Worker process exited", { meta: {
916
+ worker: this.opts.workerLabel,
917
+ pid: spawnedProcess.pid ?? null,
918
+ runtime: this.opts.poolRuntime,
919
+ device: this.opts.device ?? "default",
920
+ code,
921
+ signal,
922
+ inFlight: this.pending.size
923
+ } });
924
+ this.rejectAll(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: worker process exited (code=${code ?? "null"}, signal=${signal ?? "none"})`));
920
925
  });
921
926
  this.process.stdout.on("data", (chunk) => {
922
927
  const t0 = Date.now();
@@ -1044,6 +1049,7 @@ var PoolWorker = class {
1044
1049
  this.process = null;
1045
1050
  this.ready = false;
1046
1051
  await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
1052
+ this.rejectAll(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: pool disposed while the request was in flight`));
1047
1053
  }
1048
1054
  /**
1049
1055
  * Shed an inference request when the worker already has {@link
@@ -2356,6 +2362,23 @@ var EngineFactory = class {
2356
2362
  active: l.active
2357
2363
  }));
2358
2364
  }
2365
+ /**
2366
+ * Is this factory's pool alive and able to answer?
2367
+ *
2368
+ * `SharedInferencePool.isReady()` and `PoolWorker.isReady()` have always
2369
+ * existed; this factory surfaced `getPoolPids()` and `getPoolBacklog()` and
2370
+ * **not** readiness, so the one signal every caller needed sat one layer
2371
+ * below all of them and was never lifted. That is why a pool whose Python
2372
+ * worker was SIGABRT'd 1.3 s after `Pool ready` was handed back to 370 000
2373
+ * consecutive dispatches over 31 hours: `resolveDeviceFactory` had nothing
2374
+ * to ask.
2375
+ *
2376
+ * A factory that never initialised answers `false` — a pool that is not
2377
+ * there cannot serve a frame, and the caller's job is the same either way.
2378
+ */
2379
+ isReady() {
2380
+ return this.pool?.isReady() ?? false;
2381
+ }
2359
2382
  /** Native pid of the underlying Python pool, if any. */
2360
2383
  getPoolPid() {
2361
2384
  return this.pool?.getPid() ?? null;
@@ -2662,24 +2685,160 @@ var IdlePoolReaper = class {
2662
2685
  }
2663
2686
  };
2664
2687
  //#endregion
2688
+ //#region src/detection-pipeline/engine/inference-device-liveness.ts
2689
+ /**
2690
+ * A dispatch refused because its inference device is not usable.
2691
+ *
2692
+ * Distinct from `PoolWorker[w0]: not initialized`, which said the same thing
2693
+ * for three different states and so distinguished none of them. This one names
2694
+ * the device, the state and the underlying death, so a single rejected frame
2695
+ * is decidable without correlating logs.
2696
+ */
2697
+ var InferenceDeviceUnusableError = class extends Error {
2698
+ deviceKey;
2699
+ state;
2700
+ constructor(unhealthy) {
2701
+ super(`inference device "${unhealthy.deviceKey}" is ${unhealthy.state} after ${unhealthy.deaths} pool death(s): ${unhealthy.lastError}`);
2702
+ this.name = "InferenceDeviceUnusableError";
2703
+ this.deviceKey = unhealthy.deviceKey;
2704
+ this.state = unhealthy.state;
2705
+ }
2706
+ };
2707
+ /**
2708
+ * Defaults. Three deaths inside ten minutes trips the breaker, so a
2709
+ * deterministic crash costs **three** Python spawns instead of one per frame,
2710
+ * and a flaky-once-an-hour pool still self-heals forever.
2711
+ */
2712
+ var DEFAULT_INFERENCE_DEVICE_LIVENESS = {
2713
+ windowMs: 600 * 1e3,
2714
+ maxDeaths: 3,
2715
+ maxBackoffMs: 3e4
2716
+ };
2717
+ var InferenceDeviceLivenessSupervisor = class {
2718
+ opts;
2719
+ state = /* @__PURE__ */ new Map();
2720
+ constructor(opts = DEFAULT_INFERENCE_DEVICE_LIVENESS) {
2721
+ this.opts = opts;
2722
+ }
2723
+ /**
2724
+ * Record one pool death for `deviceKey` and decide what happens next.
2725
+ * `now` is injectable for deterministic tests. Never throws.
2726
+ */
2727
+ recordDeath(deviceKey, error, now = Date.now()) {
2728
+ const cutoff = now - this.opts.windowMs;
2729
+ const recent = [...(this.state.get(deviceKey)?.deaths ?? []).filter((t) => t >= cutoff), now];
2730
+ if (recent.length >= this.opts.maxDeaths) {
2731
+ this.state.set(deviceKey, {
2732
+ deaths: recent,
2733
+ failed: true,
2734
+ retryAtMs: Number.POSITIVE_INFINITY,
2735
+ lastError: error,
2736
+ since: now
2737
+ });
2738
+ return {
2739
+ action: "failed",
2740
+ deathsInWindow: recent.length
2741
+ };
2742
+ }
2743
+ const backoffMs = Math.min(this.opts.maxBackoffMs, 500 * 2 ** Math.min(6, Math.max(0, recent.length - 1)));
2744
+ this.state.set(deviceKey, {
2745
+ deaths: recent,
2746
+ failed: false,
2747
+ retryAtMs: now + backoffMs,
2748
+ lastError: error,
2749
+ since: now
2750
+ });
2751
+ return {
2752
+ action: "rebuild",
2753
+ backoffMs,
2754
+ deathsInWindow: recent.length
2755
+ };
2756
+ }
2757
+ /** Has the breaker tripped for this device? */
2758
+ isFailed(deviceKey) {
2759
+ return this.state.get(deviceKey)?.failed ?? false;
2760
+ }
2761
+ /**
2762
+ * Why this device cannot be used right now, or `null` when it may be
2763
+ * (re)built. A device inside its backoff answers `'backoff'`; a device whose
2764
+ * budget is spent answers `'failed'` forever.
2765
+ */
2766
+ unusableReason(deviceKey, now = Date.now()) {
2767
+ const s = this.state.get(deviceKey);
2768
+ if (!s) return null;
2769
+ if (s.failed) return {
2770
+ deviceKey,
2771
+ state: "failed",
2772
+ since: s.since,
2773
+ deaths: s.deaths.length,
2774
+ lastError: s.lastError
2775
+ };
2776
+ if (now < s.retryAtMs) return {
2777
+ deviceKey,
2778
+ state: "backoff",
2779
+ since: s.since,
2780
+ deaths: s.deaths.length,
2781
+ lastError: s.lastError
2782
+ };
2783
+ return null;
2784
+ }
2785
+ /**
2786
+ * Every device the breaker currently refuses, sorted by key.
2787
+ *
2788
+ * This is what the health channel publishes. `'backoff'` entries are
2789
+ * included deliberately: a device mid-backoff cannot serve a frame either,
2790
+ * and the reader's own arm/apply reluctance (D49) is what keeps a single
2791
+ * transient observation from excluding it.
2792
+ */
2793
+ unhealthy(now = Date.now()) {
2794
+ const out = [];
2795
+ for (const deviceKey of [...this.state.keys()].toSorted()) {
2796
+ const reason = this.unusableReason(deviceKey, now);
2797
+ if (reason) out.push(reason);
2798
+ }
2799
+ return out;
2800
+ }
2801
+ /**
2802
+ * Operator re-arm: forget everything about `deviceKey` so the next dispatch
2803
+ * builds a fresh pool with a full budget. Returns whether there was anything
2804
+ * to forget — a terminal state nobody can clear is a silent fault, and this
2805
+ * is the clearing.
2806
+ */
2807
+ rearm(deviceKey) {
2808
+ return this.state.delete(deviceKey);
2809
+ }
2810
+ /** Forget every device (shutdown / full engine re-spin). */
2811
+ reset() {
2812
+ this.state.clear();
2813
+ }
2814
+ };
2815
+ //#endregion
2665
2816
  //#region src/detection-pipeline/engine/pool-memory-guard.ts
2666
2817
  function createDetectionPoolMemoryGuard(source, logger) {
2667
2818
  const log = logger.child("pool-memory");
2819
+ /** Pools already reported dead — the `pool is DEAD` line fires on the EDGE.
2820
+ * A pool nobody dispatches to is never condemned, so without this gate one
2821
+ * idle corpse writes an ERROR every sweep forever, which is how a real
2822
+ * signal becomes noise. Cleared when the pool comes back or goes away. */
2823
+ const reportedDead = /* @__PURE__ */ new Set();
2668
2824
  return new require_dist.PoolMemoryWatchdog({
2669
2825
  policy: require_dist.resolvePoolMemoryPolicy(process.env),
2670
2826
  log,
2671
2827
  restart: (key) => source.restartPool(key),
2672
2828
  sample: async () => {
2673
2829
  const out = [];
2830
+ const seen = /* @__PURE__ */ new Set();
2674
2831
  for (const pool of source.listPools()) {
2675
- const telemetry = await samplePool(pool, log);
2832
+ seen.add(pool.key);
2833
+ const telemetry = await samplePool(pool, log, reportedDead);
2676
2834
  if (telemetry) out.push(telemetry);
2677
2835
  }
2836
+ for (const key of [...reportedDead]) if (!seen.has(key)) reportedDead.delete(key);
2678
2837
  return out;
2679
2838
  }
2680
2839
  });
2681
2840
  }
2682
- async function samplePool(pool, log) {
2841
+ async function samplePool(pool, log, reportedDead) {
2683
2842
  const pids = pool.factory.getPoolPids();
2684
2843
  if (pids.length === 0) return null;
2685
2844
  let maxRssBytes = 0;
@@ -2703,7 +2862,18 @@ async function samplePool(pool, log) {
2703
2862
  threads: mem.threads
2704
2863
  });
2705
2864
  }
2706
- if (workers.length === 0) return null;
2865
+ if (workers.length === 0) {
2866
+ if (!pool.factory.isReady()) {
2867
+ if (!reportedDead.has(pool.key)) {
2868
+ reportedDead.add(pool.key);
2869
+ log.error("inference pool is DEAD — every worker pid is gone and the pool is not ready", { meta: {
2870
+ poolKey: pool.key,
2871
+ pids
2872
+ } });
2873
+ }
2874
+ } else reportedDead.delete(pool.key);
2875
+ return null;
2876
+ }
2707
2877
  const backlog = pool.factory.getPoolBacklog();
2708
2878
  const memStats = await pool.factory.getPoolMemStats().catch((err) => err instanceof Error ? err.message : String(err));
2709
2879
  return {
@@ -8408,7 +8578,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8408
8578
  * If global steps exist, initializes with them. Otherwise, creates an empty pool.
8409
8579
  */
8410
8580
  async ensureEngineFactory() {
8411
- if (this.engineFactory) return;
8581
+ const defaultDeviceKey = deviceKeyOf(this.currentEngine);
8582
+ this.refuseIfDeviceUnusable(defaultDeviceKey);
8583
+ if (this.engineFactory) {
8584
+ if (this.engineFactory.isReady()) return;
8585
+ const dead = this.engineFactory;
8586
+ this.engineFactory = null;
8587
+ this.noteDeviceDeath(defaultDeviceKey, "pool worker is not ready");
8588
+ await dead.dispose().catch(() => void 0);
8589
+ this.refuseIfDeviceUnusable(defaultDeviceKey);
8590
+ }
8412
8591
  const inflight = this.engineFactoryInflight;
8413
8592
  if (inflight) {
8414
8593
  await inflight;
@@ -8430,11 +8609,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8430
8609
  } catch (err) {
8431
8610
  await factory.dispose().catch(() => void 0);
8432
8611
  this.log.error("Node-default pool failed to initialize", { meta: {
8433
- deviceKey: deviceKeyOf(this.currentEngine),
8612
+ deviceKey: defaultDeviceKey,
8434
8613
  backend: this.currentEngine.backend,
8435
8614
  device: this.currentEngine.device ?? null,
8436
8615
  error: require_dist.errMsg(err)
8437
8616
  } });
8617
+ this.noteDeviceDeath(defaultDeviceKey, require_dist.errMsg(err));
8438
8618
  throw err;
8439
8619
  }
8440
8620
  this.engineFactory = factory;
@@ -8483,16 +8663,147 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8483
8663
  * fail (the loser can't claim the already-held device).
8484
8664
  */
8485
8665
  deviceFactoryInflight = /* @__PURE__ */ new Map();
8666
+ /**
8667
+ * Per-`deviceKey` restart budget for the Python pools (D6 discipline).
8668
+ *
8669
+ * In-memory and per-runner ON PURPOSE. The terminal state means "this
8670
+ * accelerator aborted N times in a row a few minutes ago"; a runner respawn
8671
+ * re-derives it in three dispatches if it is still true, and persisting it
8672
+ * would outlive the driver update that fixes it. The operator's explicit
8673
+ * re-arm is {@link rearmInferenceDevice}, and the state is never invisible:
8674
+ * it is published on {@link getInferenceDeviceHealth}.
8675
+ */
8676
+ deviceLiveness = new InferenceDeviceLivenessSupervisor();
8677
+ /**
8678
+ * Throw if `deviceKey` is currently refused — before a single Python process
8679
+ * is spawned, a model index is allocated, or a frame is copied.
8680
+ *
8681
+ * The cost of NOT asking, measured: each frame of the 2026-08-25 storm ran
8682
+ * the full `models to load` → `allocateIndex` → `loadModel` → throw cycle at
8683
+ * 13 Hz for 31 hours, leaking one model index per frame — and past 254
8684
+ * (`encodeModelByte`) that pool could never have served inference again even
8685
+ * if its worker had come back.
8686
+ */
8687
+ refuseIfDeviceUnusable(deviceKey) {
8688
+ const unhealthy = this.deviceLiveness.unusableReason(deviceKey);
8689
+ if (unhealthy) throw new InferenceDeviceUnusableError(unhealthy);
8690
+ }
8691
+ /**
8692
+ * Record one pool death against the budget and SAY what was decided.
8693
+ *
8694
+ * One line per state change, not one per dispatch: the refusals that follow
8695
+ * are already carried by the rejected frame's own error. The `failed`
8696
+ * transition is an ERROR because it is the terminal state — an operator who
8697
+ * never sees this line has a silently half-dead node, which is the failure
8698
+ * this whole path exists to end.
8699
+ */
8700
+ noteDeviceDeath(deviceKey, reason) {
8701
+ const decision = this.deviceLiveness.recordDeath(deviceKey, reason);
8702
+ if (decision.action === "failed") {
8703
+ this.log.error("inference device FAILED — restart budget exhausted, no pool will be spawned", { meta: {
8704
+ deviceKey,
8705
+ deathsInWindow: decision.deathsInWindow,
8706
+ reason,
8707
+ rearm: "pipelineExecutor.rearmInferenceDevice"
8708
+ } });
8709
+ return;
8710
+ }
8711
+ this.log.warn("inference pool died — rebuilding under the restart budget", { meta: {
8712
+ deviceKey,
8713
+ deathsInWindow: decision.deathsInWindow,
8714
+ backoffMs: decision.backoffMs,
8715
+ reason
8716
+ } });
8717
+ }
8718
+ /**
8719
+ * Retire a dead per-device pool: drop it from the map FIRST (so a concurrent
8720
+ * dispatch rebuilds fresh through the single-flight guard rather than
8721
+ * grabbing the corpse), cancel its idle timer, charge the budget, then
8722
+ * dispose — which rejects whatever was still in flight on it with a reason
8723
+ * rather than letting those requests sit until their own deadlines.
8724
+ */
8725
+ async condemnDeviceFactory(deviceKey, factory, reason) {
8726
+ if (this.factoriesByDevice.get(deviceKey) === factory) {
8727
+ this.factoriesByDevice.delete(deviceKey);
8728
+ this.deviceReaper.cancel(deviceKey);
8729
+ this.noteDeviceDeath(deviceKey, reason);
8730
+ }
8731
+ await factory.dispose().catch(() => void 0);
8732
+ }
8733
+ /**
8734
+ * Every inference device this node currently refuses, and why.
8735
+ *
8736
+ * The CHANNEL the 2026-08-26 analysis found missing. Pool health lived
8737
+ * entirely inside this addon and reached no routing decision at any level:
8738
+ * the capability gate is keyed on model FORMAT and so is permanently blind
8739
+ * between `openvino:gpu` and `openvino:npu`, and the orchestrator's live
8740
+ * probe answers about HARDWARE, which was present the whole time. So the
8741
+ * balancer kept handing this node cameras by rotation — 20 sessions onto a
8742
+ * dead pool in 20 minutes — and every one of them lost its frames.
8743
+ *
8744
+ * Two sources, deliberately merged into one answer:
8745
+ * - the breaker's own `failed` / `backoff` states, and
8746
+ * - any factory currently cached but NOT ready. A pool nobody has dispatched
8747
+ * to since it died has not yet been condemned by the dispatch path, and a
8748
+ * device with no traffic is exactly the one that must stop being chosen
8749
+ * BEFORE the traffic arrives. Charging the budget is the dispatch path's
8750
+ * job; reporting is free and costs no spawn.
8751
+ *
8752
+ * Read-only and synchronous over in-memory state: it never probes, never
8753
+ * spawns, and never throws — the reader's whole safety argument rests on a
8754
+ * failed read changing nothing, and a read that can fail for its own reasons
8755
+ * would poison that.
8756
+ */
8757
+ async getInferenceDeviceHealth() {
8758
+ const byKey = /* @__PURE__ */ new Map();
8759
+ for (const entry of this.deviceLiveness.unhealthy()) byKey.set(entry.deviceKey, entry);
8760
+ const live = [...this.factoriesByDevice, ...this.engineFactory ? [[deviceKeyOf(this.currentEngine), this.engineFactory]] : []];
8761
+ for (const [deviceKey, factory] of live) {
8762
+ if (factory.isReady() || byKey.has(deviceKey)) continue;
8763
+ byKey.set(deviceKey, {
8764
+ deviceKey,
8765
+ state: "backoff",
8766
+ since: Date.now(),
8767
+ deaths: 0,
8768
+ lastError: "pool worker is not ready"
8769
+ });
8770
+ }
8771
+ return { unhealthy: [...byKey.values()].toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey)) };
8772
+ }
8773
+ /**
8774
+ * Operator re-arm of a terminally failed inference device: forget the budget
8775
+ * so the next dispatch builds a fresh pool.
8776
+ *
8777
+ * A terminal state with no way out is a silent fault dressed as a safety
8778
+ * feature. This is the way out, and it is the ONLY automatic-looking one on
8779
+ * offer: there is deliberately no timed probation, because the crash this
8780
+ * bounds is deterministic and a probation would simply respawn Python
8781
+ * forever at a slower rate. Re-arming a device that was never failed is a
8782
+ * no-op, reported as such.
8783
+ */
8784
+ async rearmInferenceDevice(input) {
8785
+ const rearmed = this.deviceLiveness.rearm(input.deviceKey);
8786
+ this.log.info("inference device re-armed by operator", { meta: {
8787
+ deviceKey: input.deviceKey,
8788
+ rearmed
8789
+ } });
8790
+ return { rearmed };
8791
+ }
8486
8792
  async resolveDeviceFactory(deviceKey) {
8487
8793
  const engine = resolveDeviceEngine(deviceKey);
8488
8794
  if (enginesEqual(engine, this.currentEngine)) {
8489
8795
  await this.ensureEngineFactory();
8490
8796
  return this.engineFactory;
8491
8797
  }
8798
+ this.refuseIfDeviceUnusable(deviceKey);
8492
8799
  const existing = this.factoriesByDevice.get(deviceKey);
8493
8800
  if (existing) {
8494
- this.deviceReaper.touch(deviceKey);
8495
- return existing;
8801
+ if (existing.isReady()) {
8802
+ this.deviceReaper.touch(deviceKey);
8803
+ return existing;
8804
+ }
8805
+ await this.condemnDeviceFactory(deviceKey, existing, "pool worker is not ready");
8806
+ this.refuseIfDeviceUnusable(deviceKey);
8496
8807
  }
8497
8808
  const inflight = this.deviceFactoryInflight.get(deviceKey);
8498
8809
  if (inflight) return inflight;
@@ -8517,6 +8828,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8517
8828
  error: err instanceof Error ? err.message : String(err)
8518
8829
  } });
8519
8830
  await factory.dispose().catch(() => void 0);
8831
+ this.noteDeviceDeath(deviceKey, require_dist.errMsg(err));
8520
8832
  throw err;
8521
8833
  }
8522
8834
  this.factoriesByDevice.set(deviceKey, factory);