@camstack/addon-pipeline 1.1.55 → 1.1.56

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 (37) 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 +23 -16
  4. package/dist/detection-pipeline/index.mjs +23 -16
  5. package/dist/{dist-DI57FC8K.js → dist-ClE6_i0p.js} +942 -38
  6. package/dist/{dist-RWGGPwVx.mjs → dist-CrjLGrkj.mjs} +942 -38
  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 +119 -11
  10. package/dist/pipeline-runner/index.mjs +119 -11
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{remote-source-plane-DU0aRSPv.mjs → remote-source-plane-B28iGi7h.mjs} +1 -1
  14. package/dist/{remote-source-plane-CHgvzzA6.js → remote-source-plane-BAqEHSgm.js} +1 -1
  15. package/dist/session-decode/decode-worker-child.js +5 -3
  16. package/dist/session-decode/decode-worker-child.mjs +5 -3
  17. package/dist/{step-definitions-CNBFKjZe.js → step-definitions-C9kUXgC7.js} +2 -2
  18. package/dist/{step-definitions-CP9kVSml.mjs → step-definitions-dCghpdRo.mjs} +2 -2
  19. package/dist/stream-broker/_stub.js +79 -79
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-mDCPzmT3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D4xH4Tm-.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D2z_vRMH.mjs +26 -0
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dn_pxzP-.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-TWCUWhCm.mjs} +2 -2
  23. package/dist/stream-broker/{hostInit-GM_CI22k.mjs → hostInit-Di9WUNlQ.mjs} +2 -2
  24. package/dist/stream-broker/index.js +60 -3
  25. package/dist/stream-broker/index.mjs +60 -3
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BChW0ntM.js → MaskShapeCanvas-DI4BY7W2-Ca93F1js.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DDzqEbSe.js → MotionZonesSettings-NcxxQN8r-CDqgb-WA.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-B879GXaf.js → PrivacyMaskSettings-APgPLF7p-BlxPhY_w.js} +1 -1
  30. package/embed-dist/assets/index-B23riUw8.js +81 -0
  31. package/embed-dist/assets/index-Bmpl9O1K.css +2 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +1 -1
  34. package/python/inference_pool.py +11 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3gTdHEh.mjs +0 -26
  36. package/embed-dist/assets/index-CC06JBcl.css +0 -2
  37. package/embed-dist/assets/index-CNjQ5rAE.js +0 -81
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DI57FC8K.js");
5
+ const require_dist = require("../dist-ClE6_i0p.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { B as DeviceType, E as motionDetectionCapability, L as BaseAddon, U as hydrateSchema, x as evaluateZoneRules } from "../dist-RWGGPwVx.mjs";
1
+ import { B as DeviceType, E as motionDetectionCapability, L as BaseAddon, U as hydrateSchema, x as evaluateZoneRules } from "../dist-CrjLGrkj.mjs";
2
2
  import { FrameRingReaderCache } from "@camstack/shm-ring";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -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-DI57FC8K.js");
6
- const require_remote_source_plane = require("../remote-source-plane-CHgvzzA6.js");
5
+ const require_dist = require("../dist-ClE6_i0p.js");
6
+ const require_remote_source_plane = require("../remote-source-plane-BAqEHSgm.js");
7
7
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
8
8
  const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
9
- const require_step_definitions = require("../step-definitions-CNBFKjZe.js");
9
+ const require_step_definitions = require("../step-definitions-C9kUXgC7.js");
10
10
  let _camstack_shm_ring = require("@camstack/shm-ring");
11
11
  let node_child_process = require("node:child_process");
12
12
  let node_url = require("node:url");
@@ -1395,8 +1395,13 @@ function wrapChild(child) {
1395
1395
  child.send(msg, () => void 0);
1396
1396
  },
1397
1397
  on: (ev, cb) => {
1398
- if (ev === "exit") child.on("exit", () => cb(void 0));
1399
- else child.on("message", (m) => cb(m));
1398
+ if (ev === "exit") {
1399
+ const toInfo = (code, signal) => ({
1400
+ code,
1401
+ signal
1402
+ });
1403
+ child.on("exit", (code, signal) => cb(toInfo(code ?? null, signal ?? null)));
1404
+ } else child.on("message", (m) => cb(m));
1400
1405
  },
1401
1406
  kill: (signal) => {
1402
1407
  if (signal === void 0) {
@@ -1604,6 +1609,11 @@ var SIGKILL_FALLBACK_MS = 1e3;
1604
1609
  var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
1605
1610
  /** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
1606
1611
  var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
1612
+ /** Narrow an 'exit' payload to {@link WorkerExitInfo} (fakes may emit undefined). */
1613
+ function toWorkerExitInfo(v) {
1614
+ const isInfo = (x) => typeof x === "object" && x !== null && "code" in x && "signal" in x;
1615
+ return isInfo(v) ? v : void 0;
1616
+ }
1607
1617
  function toError(err) {
1608
1618
  return err instanceof Error ? err : new Error(String(err));
1609
1619
  }
@@ -1624,6 +1634,8 @@ var SessionDecodeSession = class {
1624
1634
  pendingNativeCrops = /* @__PURE__ */ new Map();
1625
1635
  nextNativeCropId = 1;
1626
1636
  exited = false;
1637
+ /** Set by teardown() so handleExit can tell an intentional stop from a death. */
1638
+ stopRequested = false;
1627
1639
  sigkillTimer = null;
1628
1640
  pullTimeoutTimer = null;
1629
1641
  firstFrameReceived = false;
@@ -1633,7 +1645,7 @@ var SessionDecodeSession = class {
1633
1645
  this.firstFrameTimeoutMs = firstFrameTimeoutMs;
1634
1646
  this.frameTimeoutMs = frameTimeoutMs;
1635
1647
  this.child.on("message", (m) => this.handleMessage(m));
1636
- this.child.on("exit", () => this.handleExit());
1648
+ this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1637
1649
  }
1638
1650
  start(source, opts) {
1639
1651
  this.trySend({
@@ -1725,6 +1737,7 @@ var SessionDecodeSession = class {
1725
1737
  }
1726
1738
  /** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
1727
1739
  teardown() {
1740
+ this.stopRequested = true;
1728
1741
  this.clearPullTimeout();
1729
1742
  this.trySend({ kind: "stop" });
1730
1743
  this.tryKill(void 0);
@@ -1770,7 +1783,11 @@ var SessionDecodeSession = class {
1770
1783
  return;
1771
1784
  }
1772
1785
  }
1773
- handleExit() {
1786
+ handleExit(info) {
1787
+ if (!this.stopRequested && !this.exited) this.logger.warn("session-decode worker exited unexpectedly", { meta: {
1788
+ code: info?.code ?? null,
1789
+ signal: info?.signal ?? null
1790
+ } });
1774
1791
  this.exited = true;
1775
1792
  if (this.sigkillTimer) {
1776
1793
  clearTimeout(this.sigkillTimer);
@@ -2097,6 +2114,7 @@ function startSessionDecodePump(deps) {
2097
2114
  });
2098
2115
  } finally {
2099
2116
  releaseSpawnSlotOnce();
2117
+ if (!stopped) deps.onEnded?.();
2100
2118
  }
2101
2119
  };
2102
2120
  pump();
@@ -2210,10 +2228,13 @@ function denormalizeRect(rect, frameWidth, frameHeight) {
2210
2228
  * Walk `steps` (the attach config's tree) collecting every ENABLED step
2211
2229
  * whose catalog definition's `inputClasses` includes `parentClassName` —
2212
2230
  * these become the subtree's roots. A matched step's own nested `children`
2213
- * ride along UNCHANGED (transitively included: `face-detection` pulls
2231
+ * ride along UNCHANGED here (transitively included: `face-detection` pulls
2214
2232
  * `face-embedding`, `plate-detection` pulls `plate-ocr`) — recursion stops
2215
2233
  * at a match rather than also searching inside it for further matches.
2216
- * `filter`, when given, keeps only matches whose `addonId` is in the list.
2234
+ * `filter`, when given, keeps only the TOP-LEVEL matches whose `addonId` is
2235
+ * in the list — it does NOT prune a match's descendants. Strict per-`steps`
2236
+ * pruning of the descendant chain happens later in {@link runDetailSubtree}
2237
+ * (see {@link pruneChildStepsToRequested}); this function only selects roots.
2217
2238
  */
2218
2239
  function findMatchingDetailSteps(steps, parentClassName, lookupStepDefinition, filter) {
2219
2240
  const matches = [];
@@ -2231,6 +2252,26 @@ function findMatchingDetailSteps(steps, parentClassName, lookupStepDefinition, f
2231
2252
  walk(steps);
2232
2253
  return matches;
2233
2254
  }
2255
+ /**
2256
+ * Prune a matched step's descendant tree to the caller's requested `steps`
2257
+ * set: keep a child ONLY when its `addonId` is in `requested`, recursing into
2258
+ * every kept child. Pure — returns a new step, never mutates the input.
2259
+ *
2260
+ * Applied ONLY on the DETAIL plane (see {@link runDetailSubtree}); the
2261
+ * per-frame executor path is untouched. This is what makes the dispatcher's
2262
+ * `steps` array authoritative: `['face-detection']` runs the detector geometry
2263
+ * ALONE (the `face-embedding` child is dropped — no ArcFace inference), while
2264
+ * `['face-detection','face-embedding']` keeps the child and reproduces the
2265
+ * full detect→embed chain exactly.
2266
+ */
2267
+ function pruneChildStepsToRequested(step, requested) {
2268
+ if (!step.children || step.children.length === 0) return step;
2269
+ const children = step.children.filter((child) => requested.includes(child.addonId)).map((child) => pruneChildStepsToRequested(child, requested));
2270
+ return {
2271
+ ...step,
2272
+ children
2273
+ };
2274
+ }
2234
2275
  /** Map a detection's CROP-pixel bbox back to FRAME-space pixels. */
2235
2276
  function mapBboxToFrameSpace(bbox, crop) {
2236
2277
  const scaleX = crop.frameSpace.w / crop.width;
@@ -2290,10 +2331,21 @@ async function resolveCrop(deps, input) {
2290
2331
  * step's chain against it, and map every result bbox back to frame space.
2291
2332
  * Returns `null` when the camera isn't attached, no configured child
2292
2333
  * matches `input.parent.className`, or no crop source resolves.
2334
+ *
2335
+ * STRICT `steps` semantics: when the caller names an explicit `input.steps`
2336
+ * set, each matched root's descendant chain is pruned to exactly those ids
2337
+ * (see {@link pruneChildStepsToRequested}) — `resolveChildSteps`/`filter`
2338
+ * only selects TOP-LEVEL roots, so without this a matched `face-detection`
2339
+ * would always drag its `face-embedding` child along regardless of what was
2340
+ * asked for. When `input.steps` is omitted, the full configured chain runs
2341
+ * (today's behavior). This is the knob the dispatcher uses to run a periodic
2342
+ * geometry-only face refresh (`['face-detection']`) without re-running ArcFace.
2293
2343
  */
2294
2344
  async function runDetailSubtree(deps, input) {
2295
- const steps = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
2296
- if (!steps || steps.length === 0) return null;
2345
+ const matched = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
2346
+ if (!matched || matched.length === 0) return null;
2347
+ const requested = input.steps;
2348
+ const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
2297
2349
  const crop = await resolveCrop(deps, input);
2298
2350
  if (!crop) return null;
2299
2351
  const details = [];
@@ -2345,6 +2397,14 @@ var OCCUPANCY_BURST_SAFETY_TIMEOUT_MS = 8e3;
2345
2397
  * to have been teed within this window before it will route to the tap path.
2346
2398
  */
2347
2399
  var MOTION_FRESHNESS_WINDOW_MS = 5e3;
2400
+ /** Delay before re-dialing a detection decode whose worker died unexpectedly. */
2401
+ var DETECTION_REDIAL_DELAY_MS = 2e3;
2402
+ /**
2403
+ * Two unexpected decode deaths within this window → stop re-dialing and detach
2404
+ * (the orchestrator re-dispatches on the next motion signal). Bounds the loop
2405
+ * when the camera/source itself is broken.
2406
+ */
2407
+ var DETECTION_REDIAL_GIVEUP_WINDOW_MS = 6e4;
2348
2408
  /**
2349
2409
  * Pure decision helper: returns `true` when a dynamic frame-diff analyzer
2350
2410
  * subscription should be opened for an onboard-motion camera.
@@ -3295,6 +3355,52 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3295
3355
  return null;
3296
3356
  }
3297
3357
  }
3358
+ /** Last unexpected detection-decode end per device — bounds the re-dial. */
3359
+ lastDecodeUnexpectedEndAt = /* @__PURE__ */ new Map();
3360
+ /**
3361
+ * The detection decode pump ended WITHOUT a teardown (worker death /
3362
+ * generator error) while the camera may still be attached. Previously this
3363
+ * zombied the session — phase stayed `active`, frames stopped, and nothing
3364
+ * re-dialed (the pre-fix Channel-closed runner crash used to mask it by
3365
+ * respawning the whole runner). Recovery, bounded:
3366
+ * - first death: clean up the dead subscription and re-dial once (2s);
3367
+ * - second death within 60s: give up re-dialing and DETACH — the
3368
+ * orchestrator re-dispatches cleanly on the next motion/occupancy signal
3369
+ * (the same recovery path validated manually on device 592).
3370
+ */
3371
+ handleDetectionDecodeEnded(deviceId) {
3372
+ const attachment = this.attached.get(deviceId);
3373
+ if (!attachment) return;
3374
+ const log = this.ctx.logger.withTags({ deviceId });
3375
+ attachment.detectionUnsubscribe?.();
3376
+ attachment.detectionUnsubscribe = null;
3377
+ if (attachment.detectionKeepWarmTimer !== null) {
3378
+ clearTimeout(attachment.detectionKeepWarmTimer);
3379
+ attachment.detectionKeepWarmTimer = null;
3380
+ log.warn("detection decode worker died while kept warm — next open re-forks cold");
3381
+ return;
3382
+ }
3383
+ const now = Date.now();
3384
+ const last = this.lastDecodeUnexpectedEndAt.get(deviceId) ?? 0;
3385
+ this.lastDecodeUnexpectedEndAt.set(deviceId, now);
3386
+ if (now - last < DETECTION_REDIAL_GIVEUP_WINDOW_MS) {
3387
+ log.warn("detection decode died twice within the re-dial window — detaching for orchestrator re-dispatch");
3388
+ this.detachInternal(deviceId);
3389
+ return;
3390
+ }
3391
+ log.warn("detection decode ended unexpectedly while attached — re-dialing once");
3392
+ setTimeout(() => {
3393
+ const still = this.attached.get(deviceId);
3394
+ if (!still || still.detectionUnsubscribe !== null) return;
3395
+ this.subscribeDetectionFrames(still.config).then((unsub) => {
3396
+ if (!this.attached.has(deviceId)) {
3397
+ unsub?.();
3398
+ return;
3399
+ }
3400
+ still.detectionUnsubscribe = unsub;
3401
+ });
3402
+ }, DETECTION_REDIAL_DELAY_MS).unref?.();
3403
+ }
3298
3404
  detachInternal(deviceId) {
3299
3405
  const attachment = this.attached.get(deviceId);
3300
3406
  if (!attachment) return;
@@ -3306,6 +3412,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3306
3412
  attachment.motionUnsubscribe?.();
3307
3413
  attachment.detectionUnsubscribe?.();
3308
3414
  this.lastMotionFrameAt.delete(deviceId);
3415
+ this.lastDecodeUnexpectedEndAt.delete(deviceId);
3309
3416
  const remotePlane = this.remotePlanes.get(deviceId);
3310
3417
  if (remotePlane) {
3311
3418
  this.remotePlanes.delete(deviceId);
@@ -3615,6 +3722,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3615
3722
  format: "rgb"
3616
3723
  },
3617
3724
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
3725
+ onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
3618
3726
  onDecodedFrame: (frame, nativeCrop) => {
3619
3727
  const handle = retentionSink.write(frame) ?? void 0;
3620
3728
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
@@ -1,8 +1,8 @@
1
- import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-RWGGPwVx.mjs";
2
- import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-DU0aRSPv.mjs";
1
+ import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-CrjLGrkj.mjs";
2
+ import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-B28iGi7h.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-CyVJTZEO.mjs";
5
- import { a as getStepDefinition } from "../step-definitions-CP9kVSml.mjs";
5
+ import { a as getStepDefinition } from "../step-definitions-dCghpdRo.mjs";
6
6
  import { DecoderFrameRingSink, FrameRingReaderCache, unlinkSegment } from "@camstack/shm-ring";
7
7
  import { fork } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
@@ -1391,8 +1391,13 @@ function wrapChild(child) {
1391
1391
  child.send(msg, () => void 0);
1392
1392
  },
1393
1393
  on: (ev, cb) => {
1394
- if (ev === "exit") child.on("exit", () => cb(void 0));
1395
- else child.on("message", (m) => cb(m));
1394
+ if (ev === "exit") {
1395
+ const toInfo = (code, signal) => ({
1396
+ code,
1397
+ signal
1398
+ });
1399
+ child.on("exit", (code, signal) => cb(toInfo(code ?? null, signal ?? null)));
1400
+ } else child.on("message", (m) => cb(m));
1396
1401
  },
1397
1402
  kill: (signal) => {
1398
1403
  if (signal === void 0) {
@@ -1600,6 +1605,11 @@ var SIGKILL_FALLBACK_MS = 1e3;
1600
1605
  var DEFAULT_FIRST_FRAME_TIMEOUT_MS = 2e4;
1601
1606
  /** Default timeout for every `pull` round-trip AFTER the first frame arrives. */
1602
1607
  var DEFAULT_FRAME_TIMEOUT_MS = 1e4;
1608
+ /** Narrow an 'exit' payload to {@link WorkerExitInfo} (fakes may emit undefined). */
1609
+ function toWorkerExitInfo(v) {
1610
+ const isInfo = (x) => typeof x === "object" && x !== null && "code" in x && "signal" in x;
1611
+ return isInfo(v) ? v : void 0;
1612
+ }
1603
1613
  function toError(err) {
1604
1614
  return err instanceof Error ? err : new Error(String(err));
1605
1615
  }
@@ -1620,6 +1630,8 @@ var SessionDecodeSession = class {
1620
1630
  pendingNativeCrops = /* @__PURE__ */ new Map();
1621
1631
  nextNativeCropId = 1;
1622
1632
  exited = false;
1633
+ /** Set by teardown() so handleExit can tell an intentional stop from a death. */
1634
+ stopRequested = false;
1623
1635
  sigkillTimer = null;
1624
1636
  pullTimeoutTimer = null;
1625
1637
  firstFrameReceived = false;
@@ -1629,7 +1641,7 @@ var SessionDecodeSession = class {
1629
1641
  this.firstFrameTimeoutMs = firstFrameTimeoutMs;
1630
1642
  this.frameTimeoutMs = frameTimeoutMs;
1631
1643
  this.child.on("message", (m) => this.handleMessage(m));
1632
- this.child.on("exit", () => this.handleExit());
1644
+ this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1633
1645
  }
1634
1646
  start(source, opts) {
1635
1647
  this.trySend({
@@ -1721,6 +1733,7 @@ var SessionDecodeSession = class {
1721
1733
  }
1722
1734
  /** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
1723
1735
  teardown() {
1736
+ this.stopRequested = true;
1724
1737
  this.clearPullTimeout();
1725
1738
  this.trySend({ kind: "stop" });
1726
1739
  this.tryKill(void 0);
@@ -1766,7 +1779,11 @@ var SessionDecodeSession = class {
1766
1779
  return;
1767
1780
  }
1768
1781
  }
1769
- handleExit() {
1782
+ handleExit(info) {
1783
+ if (!this.stopRequested && !this.exited) this.logger.warn("session-decode worker exited unexpectedly", { meta: {
1784
+ code: info?.code ?? null,
1785
+ signal: info?.signal ?? null
1786
+ } });
1770
1787
  this.exited = true;
1771
1788
  if (this.sigkillTimer) {
1772
1789
  clearTimeout(this.sigkillTimer);
@@ -2093,6 +2110,7 @@ function startSessionDecodePump(deps) {
2093
2110
  });
2094
2111
  } finally {
2095
2112
  releaseSpawnSlotOnce();
2113
+ if (!stopped) deps.onEnded?.();
2096
2114
  }
2097
2115
  };
2098
2116
  pump();
@@ -2206,10 +2224,13 @@ function denormalizeRect(rect, frameWidth, frameHeight) {
2206
2224
  * Walk `steps` (the attach config's tree) collecting every ENABLED step
2207
2225
  * whose catalog definition's `inputClasses` includes `parentClassName` —
2208
2226
  * these become the subtree's roots. A matched step's own nested `children`
2209
- * ride along UNCHANGED (transitively included: `face-detection` pulls
2227
+ * ride along UNCHANGED here (transitively included: `face-detection` pulls
2210
2228
  * `face-embedding`, `plate-detection` pulls `plate-ocr`) — recursion stops
2211
2229
  * at a match rather than also searching inside it for further matches.
2212
- * `filter`, when given, keeps only matches whose `addonId` is in the list.
2230
+ * `filter`, when given, keeps only the TOP-LEVEL matches whose `addonId` is
2231
+ * in the list — it does NOT prune a match's descendants. Strict per-`steps`
2232
+ * pruning of the descendant chain happens later in {@link runDetailSubtree}
2233
+ * (see {@link pruneChildStepsToRequested}); this function only selects roots.
2213
2234
  */
2214
2235
  function findMatchingDetailSteps(steps, parentClassName, lookupStepDefinition, filter) {
2215
2236
  const matches = [];
@@ -2227,6 +2248,26 @@ function findMatchingDetailSteps(steps, parentClassName, lookupStepDefinition, f
2227
2248
  walk(steps);
2228
2249
  return matches;
2229
2250
  }
2251
+ /**
2252
+ * Prune a matched step's descendant tree to the caller's requested `steps`
2253
+ * set: keep a child ONLY when its `addonId` is in `requested`, recursing into
2254
+ * every kept child. Pure — returns a new step, never mutates the input.
2255
+ *
2256
+ * Applied ONLY on the DETAIL plane (see {@link runDetailSubtree}); the
2257
+ * per-frame executor path is untouched. This is what makes the dispatcher's
2258
+ * `steps` array authoritative: `['face-detection']` runs the detector geometry
2259
+ * ALONE (the `face-embedding` child is dropped — no ArcFace inference), while
2260
+ * `['face-detection','face-embedding']` keeps the child and reproduces the
2261
+ * full detect→embed chain exactly.
2262
+ */
2263
+ function pruneChildStepsToRequested(step, requested) {
2264
+ if (!step.children || step.children.length === 0) return step;
2265
+ const children = step.children.filter((child) => requested.includes(child.addonId)).map((child) => pruneChildStepsToRequested(child, requested));
2266
+ return {
2267
+ ...step,
2268
+ children
2269
+ };
2270
+ }
2230
2271
  /** Map a detection's CROP-pixel bbox back to FRAME-space pixels. */
2231
2272
  function mapBboxToFrameSpace(bbox, crop) {
2232
2273
  const scaleX = crop.frameSpace.w / crop.width;
@@ -2286,10 +2327,21 @@ async function resolveCrop(deps, input) {
2286
2327
  * step's chain against it, and map every result bbox back to frame space.
2287
2328
  * Returns `null` when the camera isn't attached, no configured child
2288
2329
  * matches `input.parent.className`, or no crop source resolves.
2330
+ *
2331
+ * STRICT `steps` semantics: when the caller names an explicit `input.steps`
2332
+ * set, each matched root's descendant chain is pruned to exactly those ids
2333
+ * (see {@link pruneChildStepsToRequested}) — `resolveChildSteps`/`filter`
2334
+ * only selects TOP-LEVEL roots, so without this a matched `face-detection`
2335
+ * would always drag its `face-embedding` child along regardless of what was
2336
+ * asked for. When `input.steps` is omitted, the full configured chain runs
2337
+ * (today's behavior). This is the knob the dispatcher uses to run a periodic
2338
+ * geometry-only face refresh (`['face-detection']`) without re-running ArcFace.
2289
2339
  */
2290
2340
  async function runDetailSubtree(deps, input) {
2291
- const steps = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
2292
- if (!steps || steps.length === 0) return null;
2341
+ const matched = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
2342
+ if (!matched || matched.length === 0) return null;
2343
+ const requested = input.steps;
2344
+ const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
2293
2345
  const crop = await resolveCrop(deps, input);
2294
2346
  if (!crop) return null;
2295
2347
  const details = [];
@@ -2341,6 +2393,14 @@ var OCCUPANCY_BURST_SAFETY_TIMEOUT_MS = 8e3;
2341
2393
  * to have been teed within this window before it will route to the tap path.
2342
2394
  */
2343
2395
  var MOTION_FRESHNESS_WINDOW_MS = 5e3;
2396
+ /** Delay before re-dialing a detection decode whose worker died unexpectedly. */
2397
+ var DETECTION_REDIAL_DELAY_MS = 2e3;
2398
+ /**
2399
+ * Two unexpected decode deaths within this window → stop re-dialing and detach
2400
+ * (the orchestrator re-dispatches on the next motion signal). Bounds the loop
2401
+ * when the camera/source itself is broken.
2402
+ */
2403
+ var DETECTION_REDIAL_GIVEUP_WINDOW_MS = 6e4;
2344
2404
  /**
2345
2405
  * Pure decision helper: returns `true` when a dynamic frame-diff analyzer
2346
2406
  * subscription should be opened for an onboard-motion camera.
@@ -3291,6 +3351,52 @@ var PipelineRunnerAddon = class extends BaseAddon {
3291
3351
  return null;
3292
3352
  }
3293
3353
  }
3354
+ /** Last unexpected detection-decode end per device — bounds the re-dial. */
3355
+ lastDecodeUnexpectedEndAt = /* @__PURE__ */ new Map();
3356
+ /**
3357
+ * The detection decode pump ended WITHOUT a teardown (worker death /
3358
+ * generator error) while the camera may still be attached. Previously this
3359
+ * zombied the session — phase stayed `active`, frames stopped, and nothing
3360
+ * re-dialed (the pre-fix Channel-closed runner crash used to mask it by
3361
+ * respawning the whole runner). Recovery, bounded:
3362
+ * - first death: clean up the dead subscription and re-dial once (2s);
3363
+ * - second death within 60s: give up re-dialing and DETACH — the
3364
+ * orchestrator re-dispatches cleanly on the next motion/occupancy signal
3365
+ * (the same recovery path validated manually on device 592).
3366
+ */
3367
+ handleDetectionDecodeEnded(deviceId) {
3368
+ const attachment = this.attached.get(deviceId);
3369
+ if (!attachment) return;
3370
+ const log = this.ctx.logger.withTags({ deviceId });
3371
+ attachment.detectionUnsubscribe?.();
3372
+ attachment.detectionUnsubscribe = null;
3373
+ if (attachment.detectionKeepWarmTimer !== null) {
3374
+ clearTimeout(attachment.detectionKeepWarmTimer);
3375
+ attachment.detectionKeepWarmTimer = null;
3376
+ log.warn("detection decode worker died while kept warm — next open re-forks cold");
3377
+ return;
3378
+ }
3379
+ const now = Date.now();
3380
+ const last = this.lastDecodeUnexpectedEndAt.get(deviceId) ?? 0;
3381
+ this.lastDecodeUnexpectedEndAt.set(deviceId, now);
3382
+ if (now - last < DETECTION_REDIAL_GIVEUP_WINDOW_MS) {
3383
+ log.warn("detection decode died twice within the re-dial window — detaching for orchestrator re-dispatch");
3384
+ this.detachInternal(deviceId);
3385
+ return;
3386
+ }
3387
+ log.warn("detection decode ended unexpectedly while attached — re-dialing once");
3388
+ setTimeout(() => {
3389
+ const still = this.attached.get(deviceId);
3390
+ if (!still || still.detectionUnsubscribe !== null) return;
3391
+ this.subscribeDetectionFrames(still.config).then((unsub) => {
3392
+ if (!this.attached.has(deviceId)) {
3393
+ unsub?.();
3394
+ return;
3395
+ }
3396
+ still.detectionUnsubscribe = unsub;
3397
+ });
3398
+ }, DETECTION_REDIAL_DELAY_MS).unref?.();
3399
+ }
3294
3400
  detachInternal(deviceId) {
3295
3401
  const attachment = this.attached.get(deviceId);
3296
3402
  if (!attachment) return;
@@ -3302,6 +3408,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
3302
3408
  attachment.motionUnsubscribe?.();
3303
3409
  attachment.detectionUnsubscribe?.();
3304
3410
  this.lastMotionFrameAt.delete(deviceId);
3411
+ this.lastDecodeUnexpectedEndAt.delete(deviceId);
3305
3412
  const remotePlane = this.remotePlanes.get(deviceId);
3306
3413
  if (remotePlane) {
3307
3414
  this.remotePlanes.delete(deviceId);
@@ -3611,6 +3718,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
3611
3718
  format: "rgb"
3612
3719
  },
3613
3720
  acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
3721
+ onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
3614
3722
  onDecodedFrame: (frame, nativeCrop) => {
3615
3723
  const handle = retentionSink.write(frame) ?? void 0;
3616
3724
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("../chunk-D6vf50IK.js");
2
- const require_dist = require("../dist-DI57FC8K.js");
2
+ const require_dist = require("../dist-ClE6_i0p.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
4
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
5
5
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-RWGGPwVx.mjs";
1
+ import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-CrjLGrkj.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { spawn } from "node:child_process";
@@ -1,4 +1,4 @@
1
- import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-RWGGPwVx.mjs";
1
+ import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-CrjLGrkj.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DI57FC8K.js");
1
+ const require_dist = require("./dist-ClE6_i0p.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -956,8 +956,9 @@ var DecodeWorkerChild = class {
956
956
  const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
957
957
  const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
958
958
  const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height);
959
- const dstStride = target.width * 3;
960
- const dstBuffer = Buffer.allocUnsafe(dstStride * target.height);
959
+ const tightStride = target.width * 3;
960
+ const dstStride = tightStride + 63 & -64;
961
+ const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
961
962
  const scaledHeight = scaler.scaleSync([
962
963
  ySlice,
963
964
  uSlice,
@@ -969,7 +970,8 @@ var DecodeWorkerChild = class {
969
970
  ], 0, region.height, [dstBuffer], [dstStride]);
970
971
  if (scaledHeight < 0) throw new Error(`native crop sws_scale failed: ${scaledHeight}`);
971
972
  if (scaledHeight !== target.height) throw new Error(`native crop scaler produced ${scaledHeight} of ${target.height} rows`);
972
- return dstBuffer;
973
+ if (dstStride === tightStride) return dstBuffer.subarray(0, tightStride * target.height);
974
+ return packSinglePlane(dstBuffer, dstStride, target.width, target.height, 3);
973
975
  }
974
976
  /**
975
977
  * Hardware native crop — run the retained GPU surface through a DEDICATED
@@ -952,8 +952,9 @@ var DecodeWorkerChild = class {
952
952
  const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
953
953
  const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
954
954
  const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height);
955
- const dstStride = target.width * 3;
956
- const dstBuffer = Buffer.allocUnsafe(dstStride * target.height);
955
+ const tightStride = target.width * 3;
956
+ const dstStride = tightStride + 63 & -64;
957
+ const dstBuffer = Buffer.allocUnsafe(dstStride * target.height + 64);
957
958
  const scaledHeight = scaler.scaleSync([
958
959
  ySlice,
959
960
  uSlice,
@@ -965,7 +966,8 @@ var DecodeWorkerChild = class {
965
966
  ], 0, region.height, [dstBuffer], [dstStride]);
966
967
  if (scaledHeight < 0) throw new Error(`native crop sws_scale failed: ${scaledHeight}`);
967
968
  if (scaledHeight !== target.height) throw new Error(`native crop scaler produced ${scaledHeight} of ${target.height} rows`);
968
- return dstBuffer;
969
+ if (dstStride === tightStride) return dstBuffer.subarray(0, tightStride * target.height);
970
+ return packSinglePlane(dstBuffer, dstStride, target.width, target.height, 3);
969
971
  }
970
972
  /**
971
973
  * Hardware native crop — run the retained GPU surface through a DEDICATED
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DI57FC8K.js");
1
+ const require_dist = require("./dist-ClE6_i0p.js");
2
2
  //#region src/detection-pipeline/registry/model-catalogs.ts
3
3
  var HF_REPO = "camstack/camstack-models";
4
4
  var HF_SCRYPTED = "scrypted/plugin-models";
@@ -1244,7 +1244,7 @@ var STEP_FACE_DETECTION = new PipelineStepBase({
1244
1244
  defaultConfidence: .5,
1245
1245
  defaultMinParentScore: .7,
1246
1246
  cadence: {
1247
- trigger: "improve",
1247
+ trigger: "periodic",
1248
1248
  minIntervalMs: 1e3
1249
1249
  }
1250
1250
  });
@@ -1,4 +1,4 @@
1
- import { S as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-RWGGPwVx.mjs";
1
+ import { S as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-CrjLGrkj.mjs";
2
2
  //#region src/detection-pipeline/registry/model-catalogs.ts
3
3
  var HF_REPO = "camstack/camstack-models";
4
4
  var HF_SCRYPTED = "scrypted/plugin-models";
@@ -1244,7 +1244,7 @@ var STEP_FACE_DETECTION = new PipelineStepBase({
1244
1244
  defaultConfidence: .5,
1245
1245
  defaultMinParentScore: .7,
1246
1246
  cadence: {
1247
- trigger: "improve",
1247
+ trigger: "periodic",
1248
1248
  minIntervalMs: 1e3
1249
1249
  }
1250
1250
  });