@camstack/addon-pipeline 1.2.618 → 1.2.620

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 (38) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-BT8HKaes.mjs → custom-model-catalog-BW0-xhwJ.mjs} +2 -2
  4. package/dist/{custom-model-catalog-Yu1sxc4A.js → custom-model-catalog-YxQheit5.js} +2 -2
  5. package/dist/detection-pipeline/index.js +153 -6
  6. package/dist/detection-pipeline/index.mjs +153 -6
  7. package/dist/{dist-CGDR6bSg.js → dist-CoLjAw2z.js} +213 -86
  8. package/dist/{dist-OImyOnr3.mjs → dist-mOyFhoa8.mjs} +213 -86
  9. package/dist/{model-catalogs-DV562sqJ.js → model-catalogs-BstZN0ZA.js} +1 -1
  10. package/dist/{model-catalogs-Cj101H8x.mjs → model-catalogs-PmNZKpMR.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-CSFdbFlw.js → node-CzmiyMnw.js} +1 -1
  14. package/dist/{node-DciF_ZRl.mjs → node-Qmp6FkS2.mjs} +1 -1
  15. package/dist/pipeline-runner/index.js +52 -20
  16. package/dist/pipeline-runner/index.mjs +52 -20
  17. package/dist/{process-memory-CJEkTpRM.js → process-memory-C5Cw8Cxz.js} +1 -1
  18. package/dist/{process-memory-DjIov9TF.mjs → process-memory-DXbuHvOX.mjs} +1 -1
  19. package/dist/recorder/index.js +2 -72
  20. package/dist/recorder/index.mjs +2 -72
  21. package/dist/{segment-demux-js-BsqG99U7.js → segment-demux-js-DGFHiiIf.js} +1 -1
  22. package/dist/{segment-demux-js-B5PU-E-G.mjs → segment-demux-js-Din-INnw.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-BZysHyCo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets--TcGAfJ-.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-BaoBRCJs.mjs +26 -0
  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-Dx-WrMx0.mjs +26 -0
  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-NqO2jvV2.mjs → hostInit-DOiI--7i.mjs} +3 -3
  30. package/dist/stream-broker/index.js +3 -3
  31. package/dist/stream-broker/index.mjs +3 -3
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/inference_pool.py +2 -0
  35. package/python/pool_platform.py +35 -0
  36. package/python/test_pool_platform.py +48 -0
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C1yyuAzs.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-TnEvW5tZ.mjs +0 -26
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CGDR6bSg.js");
7
- const require_model_catalogs = require("../model-catalogs-DV562sqJ.js");
8
- const require_process_memory = require("../process-memory-CJEkTpRM.js");
6
+ const require_dist = require("../dist-CoLjAw2z.js");
7
+ const require_model_catalogs = require("../model-catalogs-BstZN0ZA.js");
8
+ const require_process_memory = require("../process-memory-C5Cw8Cxz.js");
9
9
  let node_fs = require("node:fs");
10
10
  node_fs = require_chunk.__toESM(node_fs);
11
11
  let node_path = require("node:path");
@@ -1,7 +1,7 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as audioAnalyzerCapability, F as PoolMemoryWatchdog, Kn as hydrateSchema, Mn as errMsg, Pn as BaseAddon, Q as audioAnalysisCapability, Qn as nodePin, Rt as mapAudioLabelToMacro, Wn as expandAudioChunkToF32le, h as DEFAULT_AUDIO_ANALYZER_CONFIG, hr as EventCategory, ln as resolvePoolMemoryPolicy, r as AUDIO_BACKEND_CHOICES, zn as audioChunkBytesPerSample } from "../dist-OImyOnr3.mjs";
3
- import { n as AUDIO_CLASSIFIER_MODELS } from "../model-catalogs-Cj101H8x.mjs";
4
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DjIov9TF.mjs";
2
+ import { $ as audioAnalyzerCapability, F as PoolMemoryWatchdog, Kn as hydrateSchema, Mn as errMsg, Pn as BaseAddon, Q as audioAnalysisCapability, Qn as nodePin, Rt as mapAudioLabelToMacro, Wn as expandAudioChunkToF32le, h as DEFAULT_AUDIO_ANALYZER_CONFIG, hr as EventCategory, ln as resolvePoolMemoryPolicy, r as AUDIO_BACKEND_CHOICES, zn as audioChunkBytesPerSample } from "../dist-mOyFhoa8.mjs";
3
+ import { n as AUDIO_CLASSIFIER_MODELS } from "../model-catalogs-PmNZKpMR.mjs";
4
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DXbuHvOX.mjs";
5
5
  import * as fs from "node:fs";
6
6
  import * as path$1 from "node:path";
7
7
  import { ensureModel } from "@camstack/system/addon-utils";
@@ -1,5 +1,5 @@
1
- import { Mt as inferModelProvider, b as DEVICE_BACKEND_TO_FORMAT, et as audioKindId, f as COCO_80_LABELS, hn as stepModelRunsOnFormat, i as AUDIO_MACRO_LABELS, k as LABEL_SUPPLY_VOCABULARIES, p as COCO_TO_MACRO } from "./dist-OImyOnr3.mjs";
2
- import { a as FACE_DETECTION_MODELS, c as OBJECT_DETECTION_MODELS, d as PLATE_OCR_MODELS, f as SEGMENTATION_REFINER_MODELS, i as CLIP_EMBEDDING_MODELS, l as PACKAGE_DETECTION_MODELS, n as AUDIO_CLASSIFIER_MODELS, o as FACE_EMBEDDING_MODELS, p as VEHICLE_CLASSIFIER_MODELS, r as BIRD_CLASSIFIER_MODELS, s as INSTANCE_SEGMENTATION_MODELS, t as ANIMAL_CLASSIFIER_MODELS, u as PLATE_DETECTION_MODELS } from "./model-catalogs-Cj101H8x.mjs";
1
+ import { Mt as inferModelProvider, b as DEVICE_BACKEND_TO_FORMAT, et as audioKindId, f as COCO_80_LABELS, hn as stepModelRunsOnFormat, i as AUDIO_MACRO_LABELS, k as LABEL_SUPPLY_VOCABULARIES, p as COCO_TO_MACRO } from "./dist-mOyFhoa8.mjs";
2
+ import { a as FACE_DETECTION_MODELS, c as OBJECT_DETECTION_MODELS, d as PLATE_OCR_MODELS, f as SEGMENTATION_REFINER_MODELS, i as CLIP_EMBEDDING_MODELS, l as PACKAGE_DETECTION_MODELS, n as AUDIO_CLASSIFIER_MODELS, o as FACE_EMBEDDING_MODELS, p as VEHICLE_CLASSIFIER_MODELS, r as BIRD_CLASSIFIER_MODELS, s as INSTANCE_SEGMENTATION_MODELS, t as ANIMAL_CLASSIFIER_MODELS, u as PLATE_DETECTION_MODELS } from "./model-catalogs-PmNZKpMR.mjs";
3
3
  import { t as isWorkerReply } from "./worker-protocol-D_NzPcnh.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  //#region src/detection-pipeline/registry/effective-confidence.ts
@@ -1,5 +1,5 @@
1
- const require_dist = require("./dist-CGDR6bSg.js");
2
- const require_model_catalogs = require("./model-catalogs-DV562sqJ.js");
1
+ const require_dist = require("./dist-CoLjAw2z.js");
2
+ const require_model_catalogs = require("./model-catalogs-BstZN0ZA.js");
3
3
  const require_worker_protocol = require("./worker-protocol-C-G8qmye.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  //#region src/detection-pipeline/registry/effective-confidence.ts
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CGDR6bSg.js");
7
- const require_node = require("../node-CSFdbFlw.js");
8
- const require_custom_model_catalog = require("../custom-model-catalog-Yu1sxc4A.js");
6
+ const require_dist = require("../dist-CoLjAw2z.js");
7
+ const require_node = require("../node-CzmiyMnw.js");
8
+ const require_custom_model_catalog = require("../custom-model-catalog-YxQheit5.js");
9
9
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
10
- const require_process_memory = require("../process-memory-CJEkTpRM.js");
10
+ const require_process_memory = require("../process-memory-C5Cw8Cxz.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
13
  let node_fs = require("node:fs");
@@ -1221,6 +1221,39 @@ async function terminateChild(proc, graceMs) {
1221
1221
  }, graceMs);
1222
1222
  });
1223
1223
  }
1224
+ /**
1225
+ * How long a POISONED worker is given to write its stack after SIGUSR1,
1226
+ * before SIGTERM. `faulthandler` writes from a C signal handler, so a dump
1227
+ * takes milliseconds even while a native call holds the GIL; the bound is
1228
+ * there for the worker that cannot answer at all. Never more than ~2 s: the
1229
+ * recycle it delays is the one that hands the device back to the budget.
1230
+ */
1231
+ var POOL_STACK_DUMP_WAIT_MS = 1500;
1232
+ /**
1233
+ * Ask a hung worker for every thread's Python stack (SIGUSR1, registered by
1234
+ * the pool with `faulthandler` → its stderr, which the host logs under the
1235
+ * worker label), then wait up to `waitMs` — or until it exits — so the dump
1236
+ * is written before the caller terminates it. Exported for tests.
1237
+ */
1238
+ async function requestStackDump(proc, waitMs, platform = process.platform) {
1239
+ if (platform === "win32") return "unsupported";
1240
+ if (proc.exitCode !== null || proc.signalCode !== null) return "already-exited";
1241
+ try {
1242
+ proc.kill("SIGUSR1");
1243
+ } catch {
1244
+ return "signal-failed";
1245
+ }
1246
+ await new Promise((resolve) => {
1247
+ const done = () => {
1248
+ clearTimeout(timer);
1249
+ proc.removeListener("exit", done);
1250
+ resolve();
1251
+ };
1252
+ const timer = setTimeout(done, waitMs);
1253
+ proc.once("exit", done);
1254
+ });
1255
+ return "signalled";
1256
+ }
1224
1257
  var PoolWorker = class {
1225
1258
  process = null;
1226
1259
  receiveBuffer = Buffer.alloc(0);
@@ -2012,7 +2045,30 @@ var PoolWorker = class {
2012
2045
  if (this.recycling || this.exited || this.process === null) return;
2013
2046
  this.recycling = true;
2014
2047
  if (this.recycleBackstop) clearTimeout(this.recycleBackstop);
2015
- terminateChild(this.process, POOL_WORKER_TERM_GRACE_MS);
2048
+ const proc = this.process;
2049
+ this.dumpStackOfPoisoned(proc).then(() => terminateChild(proc, POOL_WORKER_TERM_GRACE_MS));
2050
+ }
2051
+ /**
2052
+ * Before a POISONED worker is killed, ask it where it hung (SIGUSR1 →
2053
+ * `faulthandler`, bounded by {@link POOL_STACK_DUMP_WAIT_MS}) and say where
2054
+ * the answer went: the worker's stderr, which this host logs line by line as
2055
+ * `[<worker label>] …` warnings just before the exit line. A hang that is
2056
+ * killed without this leaves "it hung" as the whole record.
2057
+ */
2058
+ async dumpStackOfPoisoned(proc) {
2059
+ if (this.poisonVerdict === null) return;
2060
+ const outcome = await requestStackDump(proc, POOL_STACK_DUMP_WAIT_MS);
2061
+ this.log.warn("poisoned pool worker asked for its stack before SIGTERM", { meta: {
2062
+ worker: this.opts.workerLabel,
2063
+ pid: proc.pid ?? null,
2064
+ runtime: this.opts.poolRuntime,
2065
+ device: this.opts.device ?? "default",
2066
+ reason: this.poisonVerdict.reason,
2067
+ outcome,
2068
+ signal: "SIGUSR1",
2069
+ waitedMs: outcome === "signalled" ? POOL_STACK_DUMP_WAIT_MS : 0,
2070
+ dumpWrittenTo: outcome === "signalled" ? `worker stderr — this log, lines prefixed [${this.opts.workerLabel}]` : "none"
2071
+ } });
2016
2072
  }
2017
2073
  rejectAll(err) {
2018
2074
  const entries = [...this.pending.values()];
@@ -4359,6 +4415,53 @@ var InferenceDeviceLivenessSupervisor = class {
4359
4415
  }
4360
4416
  };
4361
4417
  //#endregion
4418
+ //#region src/detection-pipeline/engine/frame-device-reelection.ts
4419
+ function routeLiveFrameDevice(input) {
4420
+ const requested = input.requestedDeviceKey;
4421
+ if (requested === void 0) return {
4422
+ kind: "as-elected",
4423
+ deviceKey: void 0
4424
+ };
4425
+ const from = input.unusable(requested);
4426
+ if (from === null) return {
4427
+ kind: "as-elected",
4428
+ deviceKey: requested
4429
+ };
4430
+ if (requested === input.defaultDeviceKey || input.unusable(input.defaultDeviceKey) !== null) return {
4431
+ kind: "no-alternative",
4432
+ deviceKey: requested,
4433
+ from
4434
+ };
4435
+ return {
4436
+ kind: "re-elected",
4437
+ deviceKey: void 0,
4438
+ from,
4439
+ to: input.defaultDeviceKey
4440
+ };
4441
+ }
4442
+ /**
4443
+ * Per camera, the routing last said — so a 6 fps camera says "re-elected"
4444
+ * once, not six times a second, and says it again when it is back.
4445
+ */
4446
+ var FrameReelectionNotes = class {
4447
+ said = /* @__PURE__ */ new Map();
4448
+ note(deviceId, routing) {
4449
+ const previous = this.said.get(deviceId);
4450
+ if (routing.kind === "as-elected") {
4451
+ if (previous === void 0) return null;
4452
+ this.said.delete(deviceId);
4453
+ return routing.deviceKey !== void 0 ? {
4454
+ kind: "restored",
4455
+ deviceKey: routing.deviceKey
4456
+ } : null;
4457
+ }
4458
+ const state = `${routing.kind}|${routing.from.deviceKey}|${routing.from.state}`;
4459
+ if (previous === state) return null;
4460
+ this.said.set(deviceId, state);
4461
+ return routing;
4462
+ }
4463
+ };
4464
+ //#endregion
4362
4465
  //#region src/detection-pipeline/engine/pool-memory-guard.ts
4363
4466
  function createDetectionPoolMemoryGuard(source, logger) {
4364
4467
  const log = logger.child("pool-memory");
@@ -9729,9 +9832,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9729
9832
  emit(`Image: ${imageWidth}×${imageHeight}`);
9730
9833
  }
9731
9834
  decodeMs = performance.now() - decodeT0;
9835
+ const liveDeviceKey = isRuntime && input.replay !== true && input.deviceId !== void 0 && input.sessionId === void 0 ? this.routeLiveFrameDevice(input.deviceKey, input.deviceId) : input.deviceKey;
9732
9836
  const dispatchResolution = this.resolveStepsForDispatch({
9733
9837
  steps: input.steps,
9734
- deviceKey: input.deviceKey,
9838
+ deviceKey: liveDeviceKey,
9735
9839
  deviceId: input.deviceId,
9736
9840
  plane: input.plane
9737
9841
  });
@@ -10871,6 +10975,49 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
10871
10975
  * (`encodeModelByte`) that pool could never have served inference again even
10872
10976
  * if its worker had come back.
10873
10977
  */
10978
+ /** Per camera, the last live-frame re-election said (see `frame-device-reelection.ts`). */
10979
+ frameReelections = new FrameReelectionNotes();
10980
+ /**
10981
+ * The deviceKey a live frame dispatches on: the elected one while it is
10982
+ * usable, else the node default pool. Each change is said once per camera,
10983
+ * naming the device — a camera whose frames silently moved pool, or were
10984
+ * silently refused, is the failure this exists to end.
10985
+ */
10986
+ routeLiveFrameDevice(requestedDeviceKey, deviceId) {
10987
+ const routing = routeLiveFrameDevice({
10988
+ requestedDeviceKey,
10989
+ defaultDeviceKey: require_custom_model_catalog.deviceKeyOf(this.currentEngine),
10990
+ unusable: (key) => this.deviceLiveness.unusableReason(key)
10991
+ });
10992
+ const note = this.frameReelections.note(deviceId, routing);
10993
+ if (note !== null) {
10994
+ const tags = { tags: { deviceId } };
10995
+ if (note.kind === "re-elected") this.log.warn("elected inference device unusable — frames re-elected onto the default pool", {
10996
+ ...tags,
10997
+ meta: {
10998
+ deviceKey: note.from.deviceKey,
10999
+ state: note.from.state,
11000
+ deaths: note.from.deaths,
11001
+ lastError: note.from.lastError,
11002
+ reElectedTo: note.to
11003
+ }
11004
+ });
11005
+ else if (note.kind === "no-alternative") this.log.warn("elected inference device unusable and no other pool here — frames refused", {
11006
+ ...tags,
11007
+ meta: {
11008
+ deviceKey: note.from.deviceKey,
11009
+ state: note.from.state,
11010
+ deaths: note.from.deaths,
11011
+ lastError: note.from.lastError
11012
+ }
11013
+ });
11014
+ else this.log.info("elected inference device usable again — frames back on it", {
11015
+ ...tags,
11016
+ meta: { deviceKey: note.deviceKey }
11017
+ });
11018
+ }
11019
+ return routing.deviceKey;
11020
+ }
10874
11021
  refuseIfDeviceUnusable(deviceKey) {
10875
11022
  const unhealthy = this.deviceLiveness.unusableReason(deviceKey);
10876
11023
  if (unhealthy) throw new InferenceDeviceUnusableError(unhealthy);
@@ -1,9 +1,9 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { Bn as createEvent, Ct as evaluateZoneRules, F as PoolMemoryWatchdog, Gn as hfModelUrl, It as loadContributionCapability, Jt as pickClusterStepSettings, Kn as hydrateSchema, Kt as overlayClusterStepSettings, Mn as errMsg$1, O as LABEL_SUPPLY_MACRO, Pn as BaseAddon, Pt as linkedLabelStepId, Qn as nodePin, St as enumerateInferenceDevices, Wt as modelBindingKey, X as applyModelBoundSettings, Zt as pipelineExecutorCapability, _t as detectionPipelineCapability, bn as supportedRuntimes$1, ct as containingParentIndex, d as CLUSTER_MODEL_SCOPED_STEPS, fn as rootSuppliesOf, ft as defaultDeviceFor$1, g as DEFAULT_CLUSTER_STEP_SETTINGS, hn as stepModelRunsOnFormat, hr as EventCategory, in as resolveClusterStepModelId, ir as array, ln as resolvePoolMemoryPolicy, mr as union, n as APPLE_SA_TO_MACRO, pn as runtimeDevices$1, pr as string, q as YAMNET_TO_MACRO, st as collectModelBoundKeys, tr as sleep, ur as object, yt as edgetpuDelegateDevice } from "../dist-OImyOnr3.mjs";
3
- import { c as readProcessCost } from "../node-DciF_ZRl.mjs";
4
- import { C as resolveModelDefaultConfidence, S as landmarkPrecisionVerdict, _ as ALL_STEPS, b as getStepDefinition, f as localFrameRegistry, g as ALL_PIPELINE_STEPS, l as resolveDefaultDetectionModel, n as mergeCustomModels, o as deviceKeyOf, r as buildSubtreeCanRun, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, v as getDefaultModelForFormat, x as resolveModelForFormat, y as getStep } from "../custom-model-catalog-BT8HKaes.mjs";
2
+ import { Bn as createEvent, Ct as evaluateZoneRules, F as PoolMemoryWatchdog, Gn as hfModelUrl, It as loadContributionCapability, Jt as pickClusterStepSettings, Kn as hydrateSchema, Kt as overlayClusterStepSettings, Mn as errMsg$1, O as LABEL_SUPPLY_MACRO, Pn as BaseAddon, Pt as linkedLabelStepId, Qn as nodePin, St as enumerateInferenceDevices, Wt as modelBindingKey, X as applyModelBoundSettings, Zt as pipelineExecutorCapability, _t as detectionPipelineCapability, bn as supportedRuntimes$1, ct as containingParentIndex, d as CLUSTER_MODEL_SCOPED_STEPS, fn as rootSuppliesOf, ft as defaultDeviceFor$1, g as DEFAULT_CLUSTER_STEP_SETTINGS, hn as stepModelRunsOnFormat, hr as EventCategory, in as resolveClusterStepModelId, ir as array, ln as resolvePoolMemoryPolicy, mr as union, n as APPLE_SA_TO_MACRO, pn as runtimeDevices$1, pr as string, q as YAMNET_TO_MACRO, st as collectModelBoundKeys, tr as sleep, ur as object, yt as edgetpuDelegateDevice } from "../dist-mOyFhoa8.mjs";
3
+ import { c as readProcessCost } from "../node-Qmp6FkS2.mjs";
4
+ import { C as resolveModelDefaultConfidence, S as landmarkPrecisionVerdict, _ as ALL_STEPS, b as getStepDefinition, f as localFrameRegistry, g as ALL_PIPELINE_STEPS, l as resolveDefaultDetectionModel, n as mergeCustomModels, o as deviceKeyOf, r as buildSubtreeCanRun, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, v as getDefaultModelForFormat, x as resolveModelForFormat, y as getStep } from "../custom-model-catalog-BW0-xhwJ.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
6
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DjIov9TF.mjs";
6
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DXbuHvOX.mjs";
7
7
  import * as os$1 from "node:os";
8
8
  import * as fs from "node:fs";
9
9
  import * as path$1 from "node:path";
@@ -1214,6 +1214,39 @@ async function terminateChild(proc, graceMs) {
1214
1214
  }, graceMs);
1215
1215
  });
1216
1216
  }
1217
+ /**
1218
+ * How long a POISONED worker is given to write its stack after SIGUSR1,
1219
+ * before SIGTERM. `faulthandler` writes from a C signal handler, so a dump
1220
+ * takes milliseconds even while a native call holds the GIL; the bound is
1221
+ * there for the worker that cannot answer at all. Never more than ~2 s: the
1222
+ * recycle it delays is the one that hands the device back to the budget.
1223
+ */
1224
+ var POOL_STACK_DUMP_WAIT_MS = 1500;
1225
+ /**
1226
+ * Ask a hung worker for every thread's Python stack (SIGUSR1, registered by
1227
+ * the pool with `faulthandler` → its stderr, which the host logs under the
1228
+ * worker label), then wait up to `waitMs` — or until it exits — so the dump
1229
+ * is written before the caller terminates it. Exported for tests.
1230
+ */
1231
+ async function requestStackDump(proc, waitMs, platform = process.platform) {
1232
+ if (platform === "win32") return "unsupported";
1233
+ if (proc.exitCode !== null || proc.signalCode !== null) return "already-exited";
1234
+ try {
1235
+ proc.kill("SIGUSR1");
1236
+ } catch {
1237
+ return "signal-failed";
1238
+ }
1239
+ await new Promise((resolve) => {
1240
+ const done = () => {
1241
+ clearTimeout(timer);
1242
+ proc.removeListener("exit", done);
1243
+ resolve();
1244
+ };
1245
+ const timer = setTimeout(done, waitMs);
1246
+ proc.once("exit", done);
1247
+ });
1248
+ return "signalled";
1249
+ }
1217
1250
  var PoolWorker = class {
1218
1251
  process = null;
1219
1252
  receiveBuffer = Buffer.alloc(0);
@@ -2005,7 +2038,30 @@ var PoolWorker = class {
2005
2038
  if (this.recycling || this.exited || this.process === null) return;
2006
2039
  this.recycling = true;
2007
2040
  if (this.recycleBackstop) clearTimeout(this.recycleBackstop);
2008
- terminateChild(this.process, POOL_WORKER_TERM_GRACE_MS);
2041
+ const proc = this.process;
2042
+ this.dumpStackOfPoisoned(proc).then(() => terminateChild(proc, POOL_WORKER_TERM_GRACE_MS));
2043
+ }
2044
+ /**
2045
+ * Before a POISONED worker is killed, ask it where it hung (SIGUSR1 →
2046
+ * `faulthandler`, bounded by {@link POOL_STACK_DUMP_WAIT_MS}) and say where
2047
+ * the answer went: the worker's stderr, which this host logs line by line as
2048
+ * `[<worker label>] …` warnings just before the exit line. A hang that is
2049
+ * killed without this leaves "it hung" as the whole record.
2050
+ */
2051
+ async dumpStackOfPoisoned(proc) {
2052
+ if (this.poisonVerdict === null) return;
2053
+ const outcome = await requestStackDump(proc, POOL_STACK_DUMP_WAIT_MS);
2054
+ this.log.warn("poisoned pool worker asked for its stack before SIGTERM", { meta: {
2055
+ worker: this.opts.workerLabel,
2056
+ pid: proc.pid ?? null,
2057
+ runtime: this.opts.poolRuntime,
2058
+ device: this.opts.device ?? "default",
2059
+ reason: this.poisonVerdict.reason,
2060
+ outcome,
2061
+ signal: "SIGUSR1",
2062
+ waitedMs: outcome === "signalled" ? POOL_STACK_DUMP_WAIT_MS : 0,
2063
+ dumpWrittenTo: outcome === "signalled" ? `worker stderr — this log, lines prefixed [${this.opts.workerLabel}]` : "none"
2064
+ } });
2009
2065
  }
2010
2066
  rejectAll(err) {
2011
2067
  const entries = [...this.pending.values()];
@@ -4352,6 +4408,53 @@ var InferenceDeviceLivenessSupervisor = class {
4352
4408
  }
4353
4409
  };
4354
4410
  //#endregion
4411
+ //#region src/detection-pipeline/engine/frame-device-reelection.ts
4412
+ function routeLiveFrameDevice(input) {
4413
+ const requested = input.requestedDeviceKey;
4414
+ if (requested === void 0) return {
4415
+ kind: "as-elected",
4416
+ deviceKey: void 0
4417
+ };
4418
+ const from = input.unusable(requested);
4419
+ if (from === null) return {
4420
+ kind: "as-elected",
4421
+ deviceKey: requested
4422
+ };
4423
+ if (requested === input.defaultDeviceKey || input.unusable(input.defaultDeviceKey) !== null) return {
4424
+ kind: "no-alternative",
4425
+ deviceKey: requested,
4426
+ from
4427
+ };
4428
+ return {
4429
+ kind: "re-elected",
4430
+ deviceKey: void 0,
4431
+ from,
4432
+ to: input.defaultDeviceKey
4433
+ };
4434
+ }
4435
+ /**
4436
+ * Per camera, the routing last said — so a 6 fps camera says "re-elected"
4437
+ * once, not six times a second, and says it again when it is back.
4438
+ */
4439
+ var FrameReelectionNotes = class {
4440
+ said = /* @__PURE__ */ new Map();
4441
+ note(deviceId, routing) {
4442
+ const previous = this.said.get(deviceId);
4443
+ if (routing.kind === "as-elected") {
4444
+ if (previous === void 0) return null;
4445
+ this.said.delete(deviceId);
4446
+ return routing.deviceKey !== void 0 ? {
4447
+ kind: "restored",
4448
+ deviceKey: routing.deviceKey
4449
+ } : null;
4450
+ }
4451
+ const state = `${routing.kind}|${routing.from.deviceKey}|${routing.from.state}`;
4452
+ if (previous === state) return null;
4453
+ this.said.set(deviceId, state);
4454
+ return routing;
4455
+ }
4456
+ };
4457
+ //#endregion
4355
4458
  //#region src/detection-pipeline/engine/pool-memory-guard.ts
4356
4459
  function createDetectionPoolMemoryGuard(source, logger) {
4357
4460
  const log = logger.child("pool-memory");
@@ -9722,9 +9825,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9722
9825
  emit(`Image: ${imageWidth}×${imageHeight}`);
9723
9826
  }
9724
9827
  decodeMs = performance.now() - decodeT0;
9828
+ const liveDeviceKey = isRuntime && input.replay !== true && input.deviceId !== void 0 && input.sessionId === void 0 ? this.routeLiveFrameDevice(input.deviceKey, input.deviceId) : input.deviceKey;
9725
9829
  const dispatchResolution = this.resolveStepsForDispatch({
9726
9830
  steps: input.steps,
9727
- deviceKey: input.deviceKey,
9831
+ deviceKey: liveDeviceKey,
9728
9832
  deviceId: input.deviceId,
9729
9833
  plane: input.plane
9730
9834
  });
@@ -10864,6 +10968,49 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
10864
10968
  * (`encodeModelByte`) that pool could never have served inference again even
10865
10969
  * if its worker had come back.
10866
10970
  */
10971
+ /** Per camera, the last live-frame re-election said (see `frame-device-reelection.ts`). */
10972
+ frameReelections = new FrameReelectionNotes();
10973
+ /**
10974
+ * The deviceKey a live frame dispatches on: the elected one while it is
10975
+ * usable, else the node default pool. Each change is said once per camera,
10976
+ * naming the device — a camera whose frames silently moved pool, or were
10977
+ * silently refused, is the failure this exists to end.
10978
+ */
10979
+ routeLiveFrameDevice(requestedDeviceKey, deviceId) {
10980
+ const routing = routeLiveFrameDevice({
10981
+ requestedDeviceKey,
10982
+ defaultDeviceKey: deviceKeyOf(this.currentEngine),
10983
+ unusable: (key) => this.deviceLiveness.unusableReason(key)
10984
+ });
10985
+ const note = this.frameReelections.note(deviceId, routing);
10986
+ if (note !== null) {
10987
+ const tags = { tags: { deviceId } };
10988
+ if (note.kind === "re-elected") this.log.warn("elected inference device unusable — frames re-elected onto the default pool", {
10989
+ ...tags,
10990
+ meta: {
10991
+ deviceKey: note.from.deviceKey,
10992
+ state: note.from.state,
10993
+ deaths: note.from.deaths,
10994
+ lastError: note.from.lastError,
10995
+ reElectedTo: note.to
10996
+ }
10997
+ });
10998
+ else if (note.kind === "no-alternative") this.log.warn("elected inference device unusable and no other pool here — frames refused", {
10999
+ ...tags,
11000
+ meta: {
11001
+ deviceKey: note.from.deviceKey,
11002
+ state: note.from.state,
11003
+ deaths: note.from.deaths,
11004
+ lastError: note.from.lastError
11005
+ }
11006
+ });
11007
+ else this.log.info("elected inference device usable again — frames back on it", {
11008
+ ...tags,
11009
+ meta: { deviceKey: note.deviceKey }
11010
+ });
11011
+ }
11012
+ return routing.deviceKey;
11013
+ }
10867
11014
  refuseIfDeviceUnusable(deviceKey) {
10868
11015
  const unhealthy = this.deviceLiveness.unusableReason(deviceKey);
10869
11016
  if (unhealthy) throw new InferenceDeviceUnusableError(unhealthy);