@camstack/addon-pipeline 1.2.296 → 1.2.298

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 (33) hide show
  1. package/dist/audio-analyzer/index.js +571 -305
  2. package/dist/audio-analyzer/index.mjs +571 -305
  3. package/dist/{default-detection-model-D1daTtqT.js → default-detection-model-BTJZvSr_.js} +368 -5
  4. package/dist/{default-detection-model-Co578D8C.mjs → default-detection-model-Bim_g_Ui.mjs} +339 -6
  5. package/dist/detection-pipeline/index.js +521 -217
  6. package/dist/detection-pipeline/index.mjs +520 -216
  7. package/dist/{dist-CCd0Q3nr.mjs → dist-DVz_88D_.mjs} +80 -123
  8. package/dist/{dist-8up-f2TX.js → dist-L_HlB0At.js} +91 -140
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-lpQgHes9.js → node-CmzZ_-zB.js} +1 -1
  12. package/dist/{node-DgMSXSWP.mjs → node-Cr8Ch1ZN.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +213 -203
  14. package/dist/pipeline-runner/index.mjs +213 -203
  15. package/dist/{process-memory-DFC_O5zE.mjs → process-memory-DO7La1yB.mjs} +1 -1
  16. package/dist/{process-memory-CX_92V_r.js → process-memory-FfRDGu11.js} +1 -1
  17. package/dist/recorder/index.js +3 -3
  18. package/dist/recorder/index.mjs +3 -3
  19. package/dist/{segment-demux-js-FZbBuk3F.mjs → segment-demux-js-BuioTwCh.mjs} +1 -1
  20. package/dist/{segment-demux-js-DzBx6NN2.js → segment-demux-js-Cd7btWna.js} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_i7oFBl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CsbrrsyY.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DisouA2F.mjs +26 -0
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dti8Ex88.mjs +26 -0
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-BPtppL3W.mjs → hostInit-NCsHzij7.mjs} +2 -2
  28. package/dist/stream-broker/index.js +3 -3
  29. package/dist/stream-broker/index.mjs +3 -3
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DUGQKsKL.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CkbplMHA.mjs +0 -26
@@ -1,6 +1,6 @@
1
- import { D as MOTION_CLOSE_AFTER_MS, Dn as _enum, Ft as pipelineRunnerCapability, Ln as string, Mn as literal, Nn as number, Nt as pickNativeLeaseOverride, O as NativeLeaseAdmissionSchema, On as array, Pn as object, Sn as nodePin, cn as BaseAddon, gn as defineCustomActions, h as DEFAULT_DETAIL_CROP_CONVENTION, hn as customAction, i as BackgroundRunRegistry, jn as lazy, jt as pickDetailCropConvention, k as NativeLeaseSettingsSchema, kn as boolean, mn as createEvent, nt as deriveDetailCropRect, pt as failureContributionCapability, tn as errMsg, v as DEFAULT_NATIVE_LEASE_SETTINGS, vt as loadContributionCapability, w as FailureCounters, y as DEVICE_BACKEND_TO_FORMAT, zn as EventCategory } from "../dist-CCd0Q3nr.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DgMSXSWP.mjs";
3
- import { a as localFrameRegistry, d as getStepDefinition, i as rootObjectGate, o as resolveFrameViewGeometry, p as RETIRED_MODEL_IDS, r as macroGateVerdict } from "../default-detection-model-Co578D8C.mjs";
1
+ import { An as lazy, At as pickDetailCropConvention, D as MOTION_CLOSE_AFTER_MS, Dn as array, En as _enum, In as string, Mn as number, Nn as object, Nt as pipelineRunnerCapability, O as NativeLeaseAdmissionSchema, On as boolean, Rn as EventCategory, en as errMsg, h as DEFAULT_DETAIL_CROP_CONVENTION, hn as defineCustomActions, i as BackgroundRunRegistry, jn as literal, jt as pickNativeLeaseOverride, k as NativeLeaseSettingsSchema, mn as customAction, nt as deriveDetailCropRect, pn as createEvent, pt as failureContributionCapability, sn as BaseAddon, v as DEFAULT_NATIVE_LEASE_SETTINGS, vt as loadContributionCapability, w as FailureCounters, xn as nodePin } from "../dist-DVz_88D_.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Cr8Ch1ZN.mjs";
3
+ import { c as macroGateVerdict, d as resolveFrameViewGeometry, g as getStepDefinition, l as rootObjectGate, o as placeDetailStep, r as CustomModelCatalog, s as stepsForElectedPool, u as localFrameRegistry, v as RETIRED_MODEL_IDS } from "../default-detection-model-Bim_g_Ui.mjs";
4
4
  import { t as isWorkerReply } from "../worker-protocol-D_NzPcnh.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
@@ -631,6 +631,28 @@ function positiveOrUndefined(value) {
631
631
  return Number.isFinite(value) && value > 0 ? value : void 0;
632
632
  }
633
633
  //#endregion
634
+ //#region src/pipeline-runner/retained-reader-hold.ts
635
+ /**
636
+ * Run `read` with `handle`'s parked frame held as a CONSUMER for its whole
637
+ * duration (D656).
638
+ *
639
+ * `getNativeCrop` has two rungs: a native crop, then — on a miss — the retained
640
+ * full-frame fallback. Between them nothing is in flight: the crop's reply
641
+ * settles the worker's last pending request in the same turn, and the fallback's
642
+ * `toBuffer` is only sent on a later microtask. A parked frame does not keep a
643
+ * draining worker alive, so without this hold the drain killed the worker in
644
+ * that gap and the fallback was lost. One rule instead of a timing trick: a
645
+ * read in progress is a consumer.
646
+ */
647
+ async function withRetainedReader(store, handle, read) {
648
+ const release = store?.holdReader(handle) ?? null;
649
+ try {
650
+ return await read();
651
+ } finally {
652
+ release?.();
653
+ }
654
+ }
655
+ //#endregion
634
656
  //#region src/session-decode/native-crop-lease-age.ts
635
657
  /**
636
658
  * THE age bound on a native crop lease (ADR-0419).
@@ -973,6 +995,8 @@ function createRetainedContentFallback(registry, ref) {
973
995
  timestamp: ref.timestamp,
974
996
  ...ref.capturedAt !== void 0 ? { capturedAt: ref.capturedAt } : {}
975
997
  };
998
+ } catch {
999
+ return null;
976
1000
  } finally {
977
1001
  lease.release(reason);
978
1002
  }
@@ -1415,6 +1439,14 @@ var RetainedFrameStore = class RetainedFrameStore {
1415
1439
  if (current) current.hydrating = void 0;
1416
1440
  }
1417
1441
  }
1442
+ /**
1443
+ * Hold `handle`'s parked frame as a reader until the returned release runs
1444
+ * (D656). `null` when the handle has no un-hydrated fallback — bytes already
1445
+ * in RAM need no worker.
1446
+ */
1447
+ holdReader(handle) {
1448
+ return this.entries.get(keyOf(handle))?.fallback?.holdReader?.() ?? null;
1449
+ }
1418
1450
  /** Drop every retained frame (node/addon shutdown). */
1419
1451
  clear() {
1420
1452
  for (const entry of this.entries.values()) entry.fallback?.release?.();
@@ -1581,6 +1613,25 @@ var SESSION_DECODE_DRAIN_BOUND_MS = 1500;
1581
1613
  */
1582
1614
  var NATIVE_CROP_REFUSED_DRAINING = "session draining — crop requested after teardown";
1583
1615
  /**
1616
+ * The refusal for a `toBuffer` asked for once the worker has been told to exit
1617
+ * (D656). A read during a DRAIN is served — that is what the drain is for — but
1618
+ * after the kill no reply can come, and a raw rejection read as a worker fault.
1619
+ */
1620
+ var TO_BUFFER_REFUSED_KILLED = "session killed — read requested after the worker was told to exit";
1621
+ /** The refusal for a `toBuffer` the worker is no longer there to answer. */
1622
+ var TO_BUFFER_REFUSED_EXITED = "worker already exited";
1623
+ /** A read the session declined to send, by name — never a worker fault. */
1624
+ var SessionDecodeReadRefusedError = class extends Error {
1625
+ reason;
1626
+ frameId;
1627
+ constructor(reason, frameId) {
1628
+ super(`session-decode-coordinator: ${reason} (frameId=${frameId})`);
1629
+ this.reason = reason;
1630
+ this.frameId = frameId;
1631
+ this.name = "SessionDecodeReadRefusedError";
1632
+ }
1633
+ };
1634
+ /**
1584
1635
  * How often the cold-start wait REPORTS that it is still waiting. Task 0
1585
1636
  * live-measured cold-start (restream dial + hwaccel context + keyframe wait) at
1586
1637
  * 5-8s, so this must tolerate a slow-but-healthy start.
@@ -1664,11 +1715,18 @@ var SessionDecodeSession = class {
1664
1715
  /** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
1665
1716
  killSent = false;
1666
1717
  /**
1667
- * Images yielded to the consumer and not yet closed. Each is a frame the
1668
- * consumer still holds a reference to — queued, in flight, or being read —
1669
- * and each dies with the worker. This is what a drain waits on.
1718
+ * Images yielded and not yet closed that a CONSUMER still holds — queued,
1719
+ * in flight, or being read. Each dies with the worker and somebody was going
1720
+ * to read it first. This is what a drain waits on.
1721
+ */
1722
+ consumerImages = 0;
1723
+ /**
1724
+ * Images yielded and not yet closed that only a RETENTION holder keeps —
1725
+ * parked for a read that may come later (the on-demand JPEG fallback, D656).
1726
+ * They die with the worker too, but nobody reads them inside any bound worth
1727
+ * holding a worker for, so a drain does NOT wait on them.
1670
1728
  */
1671
- openImages = 0;
1729
+ retainedImages = 0;
1672
1730
  /** Armed while a drain waits; its expiry kills with frames still held. */
1673
1731
  drainTimer = null;
1674
1732
  sigkillTimer = null;
@@ -1722,7 +1780,8 @@ var SessionDecodeSession = class {
1722
1780
  }
1723
1781
  /** Send `toBuffer` for `frameId` and resolve with the matching `buffer` reply's bytes. */
1724
1782
  requestBuffer(frameId, opts) {
1725
- if (this.exited) return Promise.reject(/* @__PURE__ */ new Error(`session-decode-coordinator: worker already exited (frameId=${frameId})`));
1783
+ if (this.exited) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_EXITED, frameId));
1784
+ if (this.killSent) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_KILLED, frameId));
1726
1785
  return new Promise((resolve, reject) => {
1727
1786
  const waiter = {
1728
1787
  resolve,
@@ -1811,13 +1870,28 @@ var SessionDecodeSession = class {
1811
1870
  frameId
1812
1871
  });
1813
1872
  }
1814
- /** A yielded image the consumer now holds — see {@link openImages}. */
1873
+ /** A yielded image the consumer now holds — see {@link consumerImages}. */
1815
1874
  noteImageOpened() {
1816
- this.openImages += 1;
1875
+ this.consumerImages += 1;
1817
1876
  }
1818
- /** The consumer let go of a yielded image; a waiting drain may now be done. */
1819
- noteImageClosed() {
1820
- this.openImages = Math.max(0, this.openImages - 1);
1877
+ /**
1878
+ * An open image moved between a consumer and a retention-only holder
1879
+ * (D656). Losing its last consumer may complete a waiting drain.
1880
+ */
1881
+ noteImageHolder(pendingConsumer) {
1882
+ if (pendingConsumer) {
1883
+ this.retainedImages = Math.max(0, this.retainedImages - 1);
1884
+ this.consumerImages += 1;
1885
+ return;
1886
+ }
1887
+ this.consumerImages = Math.max(0, this.consumerImages - 1);
1888
+ this.retainedImages += 1;
1889
+ this.settleDrainIfDone();
1890
+ }
1891
+ /** A yielded image was closed; a waiting drain may now be done. */
1892
+ noteImageClosed(pendingConsumer) {
1893
+ if (pendingConsumer) this.consumerImages = Math.max(0, this.consumerImages - 1);
1894
+ else this.retainedImages = Math.max(0, this.retainedImages - 1);
1821
1895
  this.settleDrainIfDone();
1822
1896
  }
1823
1897
  /**
@@ -1868,15 +1942,29 @@ var SessionDecodeSession = class {
1868
1942
  this.tryKill("SIGKILL");
1869
1943
  }
1870
1944
  /**
1871
- * Frames the consumer still holds, reads still waiting on the worker, and
1945
+ * Frames a CONSUMER still holds, reads still waiting on the worker, and
1872
1946
  * native crops already REQUESTED. A crop asked for before the teardown
1873
1947
  * request is waited for; one asked for after it is refused by name — post-analysis's
1874
1948
  * ordinary crop arrives 2.2–9.9 s after the frame, past any bound worth
1875
1949
  * holding a worker for, and is served from subject tiles that die with the
1876
1950
  * worker either way.
1951
+ *
1952
+ * A frame only PARKED for such a late read ({@link retainedImages}) is not
1953
+ * counted, for the same reason (D656): counting it made nearly every
1954
+ * teardown wait the full bound for frames nobody was going to read. Its read,
1955
+ * once STARTED, is a pending read like any other and is waited for.
1877
1956
  */
1878
1957
  unsettledCount() {
1879
- return this.openImages + this.pendingReadCount() + this.pendingNativeCrops.size;
1958
+ return this.consumerImages + this.pendingReadCount() + this.pendingNativeCrops.size;
1959
+ }
1960
+ /** What the session still holds, per category — every drain line carries it. */
1961
+ heldCounts() {
1962
+ return {
1963
+ consumerFrames: this.consumerImages,
1964
+ retainedFrames: this.retainedImages,
1965
+ pendingReads: this.pendingReadCount(),
1966
+ pendingCrops: this.pendingNativeCrops.size
1967
+ };
1880
1968
  }
1881
1969
  pendingReadCount() {
1882
1970
  let n = 0;
@@ -1900,9 +1988,7 @@ var SessionDecodeSession = class {
1900
1988
  this.logger.warn("session-decode-coordinator: drain bound expired — killing the worker with frames still held", {
1901
1989
  tags: { deviceId: this.deviceId },
1902
1990
  meta: {
1903
- unsettledFrames: this.openImages,
1904
- pendingReads: this.pendingReadCount(),
1905
- pendingCrops: this.pendingNativeCrops.size,
1991
+ ...this.heldCounts(),
1906
1992
  boundMs: SESSION_DECODE_DRAIN_BOUND_MS
1907
1993
  }
1908
1994
  });
@@ -1924,6 +2010,13 @@ var SessionDecodeSession = class {
1924
2010
  killNow(mode) {
1925
2011
  if (this.killSent) return;
1926
2012
  this.killSent = true;
2013
+ if (this.retainedImages > 0) this.logger.debug("session-decode-coordinator: retained frames die with the worker", {
2014
+ tags: { deviceId: this.deviceId },
2015
+ meta: {
2016
+ mode,
2017
+ ...this.heldCounts()
2018
+ }
2019
+ });
1927
2020
  this.trySend({ kind: "stop" });
1928
2021
  if (mode === "immediate") {
1929
2022
  this.tryKill("SIGKILL");
@@ -2025,10 +2118,7 @@ var SessionDecodeSession = class {
2025
2118
  meta: {
2026
2119
  code: info?.code ?? null,
2027
2120
  signal: info?.signal ?? null,
2028
- ...this.stopRequested ? {
2029
- unsettledFrames: this.openImages,
2030
- pendingReads: this.pendingReadCount()
2031
- } : {}
2121
+ ...this.stopRequested ? this.heldCounts() : {}
2032
2122
  }
2033
2123
  });
2034
2124
  this.exited = true;
@@ -2226,6 +2316,7 @@ var SessionDecodeSession = class {
2226
2316
  /** Build the {@link FrameImage} adapter for one `frame` reply. */
2227
2317
  function buildFrameImage(session, reply) {
2228
2318
  let closed = false;
2319
+ let pendingConsumer = true;
2229
2320
  session.noteImageOpened();
2230
2321
  return {
2231
2322
  width: reply.width,
@@ -2240,10 +2331,15 @@ function buildFrameImage(session, reply) {
2240
2331
  cutTiles: (bboxes) => session.cutTiles(reply.frameId, bboxes),
2241
2332
  releaseNativeLease: () => session.releaseLease(reply.frameId),
2242
2333
  markInferred: () => session.markInferred(reply.frameId),
2334
+ setPendingConsumer: (pending) => {
2335
+ if (closed || pending === pendingConsumer) return;
2336
+ pendingConsumer = pending;
2337
+ session.noteImageHolder(pending);
2338
+ },
2243
2339
  close: () => {
2244
2340
  if (closed) return;
2245
2341
  closed = true;
2246
- session.noteImageClosed();
2342
+ session.noteImageClosed(pendingConsumer);
2247
2343
  }
2248
2344
  };
2249
2345
  }
@@ -3919,173 +4015,6 @@ async function runDetailSubtree(deps, input) {
3919
4015
  return { details };
3920
4016
  }
3921
4017
  //#endregion
3922
- //#region src/pipeline-runner/device-jump.ts
3923
- /**
3924
- * Step-tree device jump (same-node, phase 1) — the PURE resolution core.
3925
- *
3926
- * Given the camera's elected ("effective") inference device and the node's
3927
- * enabled-device roster, decide which device ONE enrichment step should run on:
3928
- *
3929
- * 1. A manual `jumpDeviceKey` override wins when it names a candidate that can
3930
- * run the step (bypasses the auto choice; operator intent).
3931
- * 2. Otherwise, if the effective device's format can run the step's model →
3932
- * NO jump (the step inherits the effective device — the common case).
3933
- * 3. Otherwise AUTO-jump: among the node's OTHER enabled devices whose format
3934
- * can run the step (and that are under their `maxSessions` cap), pick the
3935
- * weighted-least-loaded one — mirroring the balancer's per-device core.
3936
- * 4. If nothing on the node can run it → `no-candidate` (the caller emits the
3937
- * loud once-per-(camera,step) warn that replaces today's silent degrade).
3938
- *
3939
- * Framework-free + side-effect-free so the whole decision table is unit-testable
3940
- * without a live runner/catalog. The `canRun(format)` predicate is injected by
3941
- * the runner (which owns the model catalog); this module never imports it.
3942
- */
3943
- /** The `ModelFormat` of a `<backend>:<device>` key, or `undefined` for an unknown backend. */
3944
- function formatOf(deviceKey) {
3945
- const colon = deviceKey.indexOf(":");
3946
- return DEVICE_BACKEND_TO_FORMAT[colon >= 0 ? deviceKey.slice(0, colon) : deviceKey];
3947
- }
3948
- /** True when the step's model has a build for `deviceKey`'s format. */
3949
- function canRunOn(deviceKey, canRun) {
3950
- const format = formatOf(deviceKey);
3951
- return format !== void 0 && canRun(format);
3952
- }
3953
- /** Effective weighted load of a device (session count ÷ weight); lower = preferred. */
3954
- function weightedLoad(d) {
3955
- return d.attachedCameras / (d.weight > 0 ? d.weight : 1);
3956
- }
3957
- /**
3958
- * Weighted-least-loaded pick with a deterministic `deviceKey` tie-break —
3959
- * mirrors the node/device balancer's decision core (least `load/weight`, ties
3960
- * broken stably). `devices` is assumed non-empty.
3961
- */
3962
- function pickWeightedLeastLoaded(devices) {
3963
- return devices.reduce((best, d) => {
3964
- const dScore = weightedLoad(d);
3965
- const bScore = weightedLoad(best);
3966
- if (dScore < bScore) return d;
3967
- if (dScore > bScore) return best;
3968
- return d.deviceKey < best.deviceKey ? d : best;
3969
- });
3970
- }
3971
- /**
3972
- * Build the `canRun` predicate for ONE enrichment dispatch. The step and its
3973
- * enabled descendants ride the SAME `runPipeline` call — one pool, one format
3974
- * (detail-subtree contract: "its inherited children ride the same call → same
3975
- * device") — so a format can run the dispatch only when EVERY catalog-known
3976
- * step in the subtree ships a build for it. Gating only the subtree TOP let a
3977
- * `face-detection` with a tflite build stay on the Coral while its
3978
- * `face-embedding` child (no tflite build anywhere) doomed the call.
3979
- * Unknown/custom addonIds (`getStepDef` → null) have no catalog to gate on
3980
- * and never block. `getStepDef` is injected — this module stays catalog-free.
3981
- */
3982
- function buildSubtreeCanRun(step, getStepDef) {
3983
- const defs = [];
3984
- const walk = (s) => {
3985
- if (s.enabled === false) return;
3986
- const def = getStepDef(s.addonId);
3987
- if (def !== null) defs.push(def);
3988
- for (const child of s.children ?? []) walk(child);
3989
- };
3990
- walk(step);
3991
- return (format) => defs.every((def) => def.models.some((m) => m.formats[format]));
3992
- }
3993
- /**
3994
- * Resolve the device one enrichment step runs on. See the module header for the
3995
- * decision table. Pure — never throws, no I/O.
3996
- */
3997
- function resolveStepDevice(input) {
3998
- const { effectiveDeviceKey, manualJumpDeviceKey, canRun } = input;
3999
- if (manualJumpDeviceKey !== void 0 && manualJumpDeviceKey !== effectiveDeviceKey) {
4000
- const target = input.devices.find((d) => d.deviceKey === manualJumpDeviceKey);
4001
- if (target && canRunOn(target.deviceKey, canRun)) return {
4002
- kind: "jumped",
4003
- deviceKey: target.deviceKey,
4004
- reason: "manual"
4005
- };
4006
- }
4007
- if (canRunOn(effectiveDeviceKey, canRun)) return {
4008
- kind: "no-jump",
4009
- deviceKey: effectiveDeviceKey
4010
- };
4011
- const runnable = input.devices.filter((d) => d.deviceKey !== effectiveDeviceKey && (d.maxSessions === null || d.attachedCameras < d.maxSessions) && canRunOn(d.deviceKey, canRun));
4012
- if (runnable.length === 0) return {
4013
- kind: "no-candidate",
4014
- deviceKey: effectiveDeviceKey
4015
- };
4016
- return {
4017
- kind: "jumped",
4018
- deviceKey: pickWeightedLeastLoaded(runnable).deviceKey,
4019
- reason: "auto"
4020
- };
4021
- }
4022
- /**
4023
- * Re-provision a jumped subtree for its TARGET device (D650): every step's
4024
- * `modelId` / `settings` are REPLACED by the target's entry — the orchestrator
4025
- * computed it as what the step would carry had the camera been elected onto
4026
- * that device (its per-(node,device) base with the camera's override for it).
4027
- * A step absent from the map runs with neither, i.e. the target format's
4028
- * default model. The elected device's provisioning never follows a step across:
4029
- * it was chosen for a format this subtree does not run on.
4030
- */
4031
- function provisionSubtreeForDevice(step, provisioning) {
4032
- const own = provisioning[step.addonId];
4033
- const { modelId: _electedModelId, settings: _electedSettings, ...rest } = step;
4034
- return {
4035
- ...rest,
4036
- ...own?.modelId !== void 0 ? { modelId: own.modelId } : {},
4037
- ...own?.settings !== void 0 ? { settings: own.settings } : {},
4038
- ...step.children !== void 0 ? { children: step.children.map((c) => provisionSubtreeForDevice(c, provisioning)) } : {}
4039
- };
4040
- }
4041
- /**
4042
- * Place ONE detail subtree (D650): which same-node device runs it
4043
- * ({@link resolveStepDevice}) and, when it jumps, the provisioning it runs
4044
- * with there ({@link provisionSubtreeForDevice}). Pure — the caller logs.
4045
- *
4046
- * A roster entry without `steps` came from an orchestrator that predates D650:
4047
- * the step keeps what it was sent with, as it did before.
4048
- */
4049
- function placeDetailStep(input) {
4050
- const { step, effectiveDeviceKey, roster } = input;
4051
- if (!effectiveDeviceKey) return {
4052
- deviceKey: void 0,
4053
- step,
4054
- decision: null,
4055
- provisioned: false
4056
- };
4057
- if (!roster || roster.length < 2) return {
4058
- deviceKey: effectiveDeviceKey,
4059
- step,
4060
- decision: null,
4061
- provisioned: false
4062
- };
4063
- const decision = resolveStepDevice({
4064
- effectiveDeviceKey,
4065
- devices: roster.map((d) => ({
4066
- deviceKey: d.deviceKey,
4067
- weight: d.weight,
4068
- maxSessions: d.maxSessions,
4069
- attachedCameras: input.attachedByKey.get(d.deviceKey) ?? 0
4070
- })),
4071
- ...step.jumpDeviceKey !== void 0 ? { manualJumpDeviceKey: step.jumpDeviceKey } : {},
4072
- canRun: buildSubtreeCanRun(step, input.getStepDef)
4073
- });
4074
- if (decision.kind !== "jumped") return {
4075
- deviceKey: decision.deviceKey,
4076
- step,
4077
- decision,
4078
- provisioned: false
4079
- };
4080
- const provisioning = roster.find((d) => d.deviceKey === decision.deviceKey)?.steps;
4081
- return {
4082
- deviceKey: decision.deviceKey,
4083
- step: provisioning === void 0 ? step : provisionSubtreeForDevice(step, provisioning),
4084
- decision,
4085
- provisioned: provisioning !== void 0
4086
- };
4087
- }
4088
- //#endregion
4089
4018
  //#region src/pipeline-runner/device-load.ts
4090
4019
  /** The backend prefix of a `<backend>:<device>` key (bare `cpu` → `cpu`). */
4091
4020
  function backendOf(deviceKey) {
@@ -5369,7 +5298,7 @@ var ON_DEMAND_JPEG_VIEW = {
5369
5298
  format: "jpeg"
5370
5299
  };
5371
5300
  function createOnDemandJpegFallback(registry, ref) {
5372
- const lease = registry.acquire(ref);
5301
+ const lease = registry.acquire(ref, "retention");
5373
5302
  let cached;
5374
5303
  let released = false;
5375
5304
  const releaseLease = (reason) => {
@@ -5409,6 +5338,11 @@ function createOnDemandJpegFallback(registry, ref) {
5409
5338
  },
5410
5339
  release() {
5411
5340
  releaseLease("eviction");
5341
+ },
5342
+ holdReader() {
5343
+ if (released || cached !== void 0) return null;
5344
+ const reader = registry.acquire(ref);
5345
+ return reader ? () => reader.release("success") : null;
5412
5346
  }
5413
5347
  };
5414
5348
  }
@@ -7254,11 +7188,14 @@ async function runStatelessStep(deps, input) {
7254
7188
  return refuse(deps, input, "execution-failed", errorMessage(err));
7255
7189
  }
7256
7190
  if (result === null) return refuse(deps, input, "execution-failed", "the executor returned no frame result for the crop");
7191
+ const ranModelId = result.rootModelId;
7192
+ if (ranModelId === void 0 || ranModelId === "") return refuse(deps, input, "execution-failed", `the executor result names no model — refusing rather than assuming "${input.modelId}" ran`);
7193
+ if (ranModelId !== input.modelId) return refuse(deps, input, "model-not-servable", `the executor ran "${ranModelId}" for a call pinned to "${input.modelId}" — its output would not be comparable with the pass`);
7257
7194
  const details = result.detections.map((detection) => toDetailResult(detection, input.stepId, crop));
7258
7195
  return {
7259
7196
  kind: "ran",
7260
7197
  nodeId: deps.nodeId,
7261
- modelId: input.modelId,
7198
+ modelId: ranModelId,
7262
7199
  details
7263
7200
  };
7264
7201
  }
@@ -7667,6 +7604,15 @@ function tryGetStepDefinition(stepId) {
7667
7604
  }
7668
7605
  }
7669
7606
  /**
7607
+ * A detail-plane step: its catalog definition consumes a parent class
7608
+ * (`inputClasses` non-null). Unknown/custom addonIds are root-plane — the
7609
+ * same reading as the executor's frame-plane skip and `deriveDetailSteps`.
7610
+ */
7611
+ function isDetailPlaneStepId(stepId) {
7612
+ const def = tryGetStepDefinition(stepId);
7613
+ return def !== null && def.inputClasses !== null;
7614
+ }
7615
+ /**
7670
7616
  * Precomputes the `detailSteps` announce (two-plane design) for a camera's
7671
7617
  * configured step tree: every ENABLED step whose catalog definition is a
7672
7618
  * detail/child step (`inputClasses !== null` — a root step like
@@ -8499,14 +8445,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8499
8445
  height: cached.height,
8500
8446
  format: cached.format
8501
8447
  }, pin)).frameId;
8502
- await api.pipelineExecutor.runPipeline.mutate({ ...buildBenchRunPipelineInput({
8448
+ const benchModelId = (await api.pipelineExecutor.runPipeline.mutate({ ...buildBenchRunPipelineInput({
8503
8449
  steps: stepsToRun,
8504
8450
  frame: sharedFrame
8505
- }) }, pin);
8451
+ }) }, pin)).rootModelId;
8452
+ if (benchModelId === void 0 || benchModelId === "") throw new Error(`synthetic bench: the warmup of "${rootStep.addonId}" named no model — refusing to time an unnamed variant`);
8506
8453
  const warmupCount = input.warmup ?? 1;
8507
8454
  for (let w = 0; w < warmupCount; w++) await api.pipelineExecutor.inferCached.mutate({
8508
8455
  stepId: rootStep.addonId,
8509
- frameId: poolFrameId
8456
+ frameId: poolFrameId,
8457
+ modelId: benchModelId
8510
8458
  }, pin);
8511
8459
  const wallTimings = [];
8512
8460
  const inferTimings = [];
@@ -8524,7 +8472,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8524
8472
  const t0 = performance.now();
8525
8473
  const result = await api.pipelineExecutor.inferCached.mutate({
8526
8474
  stepId: rootStep.addonId,
8527
- frameId: poolFrameId
8475
+ frameId: poolFrameId,
8476
+ modelId: benchModelId
8528
8477
  }, pin);
8529
8478
  const wallMs = performance.now() - t0;
8530
8479
  const r = result;
@@ -9199,6 +9148,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9199
9148
  * resolve legitimately want the lower-RES honest frame).
9200
9149
  */
9201
9150
  async getNativeCrop(input) {
9151
+ return withRetainedReader(this.retainedFrames, input.handle, () => this.resolveNativeCropBothRungs(input));
9152
+ }
9153
+ /** {@link getNativeCrop}'s two rungs, run under its reader hold. */
9154
+ async resolveNativeCropBothRungs(input) {
9202
9155
  const deviceId = this.nativeCropRegistry.deviceIdFor(input.handle) ?? 0;
9203
9156
  const isFullFrame = input.bbox.x <= .001 && input.bbox.y <= .001 && input.bbox.w >= .999 && input.bbox.h >= .999;
9204
9157
  this.frameLazy.noteOnDemandRequest({
@@ -9714,6 +9667,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9714
9667
  */
9715
9668
  async runDetailSubtree(input) {
9716
9669
  const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
9670
+ await this.customModels.refresh(this.ctxIfReady?.api);
9717
9671
  const outcome = await runDetailSubtree({
9718
9672
  resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
9719
9673
  getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
@@ -9842,6 +9796,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9842
9796
  * global settings, NOT from the attach config. See `crop-convention-source.ts`
9843
9797
  * for why eleven attach builders make the attach config the wrong carrier.
9844
9798
  */
9799
+ /**
9800
+ * The custom-model registry, merged into the catalog the device jump judges
9801
+ * (D657): a custom model with a tflite build, pinned on a Coral, runs there.
9802
+ * Refreshed on the async boundaries (`runInference`, `runDetailSubtree`),
9803
+ * read synchronously by the placement.
9804
+ */
9805
+ customModels = new CustomModelCatalog({ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) });
9845
9806
  cropConvention = new CropConventionSource({ logger: {
9846
9807
  info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
9847
9808
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
@@ -9945,18 +9906,63 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9945
9906
  */
9946
9907
  placeDetailStepForCamera(deviceId, step) {
9947
9908
  const config = this.attached.get(deviceId)?.config;
9948
- const placement = placeDetailStep({
9949
- step,
9950
- effectiveDeviceKey: config?.deviceKey,
9951
- roster: config?.inferenceDevices,
9952
- attachedByKey: this.perDeviceAttachedCounts(),
9953
- getStepDef: tryGetStepDefinition
9954
- });
9909
+ const placement = this.placementOf(config, step, this.perDeviceAttachedCounts());
9955
9910
  const decision = placement.decision;
9956
9911
  if (decision?.kind === "no-candidate") this.warnStepHasNoRunnableDevice(deviceId, step.addonId, decision.deviceKey);
9957
9912
  else if (decision?.kind === "jumped" && config?.deviceKey) this.logStepJumped(deviceId, config.deviceKey, decision, placement);
9958
9913
  return placement;
9959
9914
  }
9915
+ /**
9916
+ * Where one detail subtree of this camera runs — the ONE decision both the
9917
+ * detail plane (which dispatches it) and the frame plane (which decides what
9918
+ * the elected pool pre-warms, D657) read. Pure: nothing is logged here.
9919
+ */
9920
+ placementOf(config, step, attachedByKey) {
9921
+ return placeDetailStep({
9922
+ step,
9923
+ effectiveDeviceKey: config?.deviceKey,
9924
+ roster: config?.inferenceDevices,
9925
+ attachedByKey,
9926
+ getStepDef: this.customModels.stepDefinition
9927
+ });
9928
+ }
9929
+ /**
9930
+ * The tree this camera's frame-plane dispatch carries (D657): a detail
9931
+ * subtree the placement sends to another device (or to none) is left out,
9932
+ * so its model is never loaded on the elected pool — it loads on its
9933
+ * target, with the target's provisioning, when the detail plane dispatches
9934
+ * it there. Said once per (camera, elected device, set left out).
9935
+ */
9936
+ stepsForElectedPoolOf(deviceId, steps) {
9937
+ const config = this.attached.get(deviceId)?.config;
9938
+ if (config?.deviceKey === void 0 || (config.inferenceDevices?.length ?? 0) < 2) {
9939
+ this.noteElectedPoolDelegation(deviceId, void 0, []);
9940
+ return steps;
9941
+ }
9942
+ const attachedByKey = this.perDeviceAttachedCounts();
9943
+ const result = stepsForElectedPool(steps, isDetailPlaneStepId, (step) => this.placementOf(config, step, attachedByKey));
9944
+ this.noteElectedPoolDelegation(deviceId, config.deviceKey, result.delegated);
9945
+ return result.delegated.length === 0 ? steps : result.steps;
9946
+ }
9947
+ /**
9948
+ * Say once per STATE CHANGE which detail steps a camera's elected pool no
9949
+ * longer loads: a camera moving A → B → A says so again on A.
9950
+ */
9951
+ noteElectedPoolDelegation(deviceId, electedDeviceKey, delegated) {
9952
+ const state = `${electedDeviceKey ?? ""}|${delegated.join(",")}`;
9953
+ if (this.electedPoolDelegationState.get(deviceId) === state) return;
9954
+ this.electedPoolDelegationState.set(deviceId, state);
9955
+ if (delegated.length === 0) return;
9956
+ this.ctx?.logger.info("detail steps not loaded on the elected pool — they run elsewhere", {
9957
+ tags: { deviceId },
9958
+ meta: {
9959
+ electedDeviceKey: electedDeviceKey ?? null,
9960
+ steps: [...delegated]
9961
+ }
9962
+ });
9963
+ }
9964
+ /** Per camera, the last (elected device, steps left out) state that was logged. */
9965
+ electedPoolDelegationState = /* @__PURE__ */ new Map();
9960
9966
  /** `${deviceId}:${addonId}:${target}` triples whose jump has been logged. */
9961
9967
  loggedStepJumps = /* @__PURE__ */ new Set();
9962
9968
  logStepJumped(deviceId, electedDeviceKey, decision, placement) {
@@ -10193,6 +10199,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10193
10199
  const warnPrefix = `${deviceId}:`;
10194
10200
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
10195
10201
  this.attached.delete(deviceId);
10202
+ this.electedPoolDelegationState.delete(deviceId);
10196
10203
  this.lastMotionAt.delete(deviceId);
10197
10204
  this.lastEmittedCameraMetrics.delete(deviceId);
10198
10205
  this.retention?.forgetDevice(deviceId);
@@ -10903,6 +10910,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10903
10910
  }
10904
10911
  const camConfig = this.attached.get(deviceId)?.config;
10905
10912
  const steps = camConfig?.steps;
10913
+ await this.customModels.refresh(api);
10906
10914
  if (!steps) {
10907
10915
  log.warn("runInference: no steps in attach config — skipping frame (legacy attach?)");
10908
10916
  return null;
@@ -10937,6 +10945,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10937
10945
  });
10938
10946
  }
10939
10947
  }
10948
+ dispatchSteps = this.stepsForElectedPoolOf(deviceId, dispatchSteps);
10940
10949
  let image;
10941
10950
  const executorCoLocated = this.executorCoLocated();
10942
10951
  if (!executorCoLocated) {
@@ -11066,6 +11075,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
11066
11075
  const cropResult = await api.pipelineExecutor.runPipeline.mutate({
11067
11076
  steps: [step],
11068
11077
  image: view.jpeg,
11078
+ deviceId,
11069
11079
  plane: "frame",
11070
11080
  ...deviceKey ? { deviceKey } : {}
11071
11081
  });
@@ -1,4 +1,4 @@
1
- import { Ot as parseProcStatus } from "./dist-CCd0Q3nr.mjs";
1
+ import { Ot as parseProcStatus } from "./dist-DVz_88D_.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-8up-f2TX.js");
2
+ const require_dist = require("./dist-L_HlB0At.js");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -3,8 +3,8 @@ 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-8up-f2TX.js");
7
- const require_node = require("../node-lpQgHes9.js");
6
+ const require_dist = require("../dist-L_HlB0At.js");
7
+ const require_node = require("../node-CmzZ_-zB.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
@@ -14003,7 +14003,7 @@ function bandTriggerSources(band) {
14003
14003
  * (`packages/types/src/types/detection.ts:448`) and
14004
14004
  * `RecordingTriggers.audioThresholdDbfs` are the SAME unit and the SAME
14005
14005
  * scale — dBFS, 0 = full scale, more negative = quieter — produced by
14006
- * `audio-analyzer-provider.ts:77` (`20 * log10(rms)`). No conversion exists
14006
+ * `AudioAnalyzerProvider.analyseChunk` in `audio-analyzer/addons/analyzer/index.ts` (`20 * log10(rms)`). No conversion exists
14007
14007
  * between them because none is needed: the one other place in this repo that
14008
14008
  * compares the two values does the same bare `>=`
14009
14009
  * (`notification-center/audio-rule-matcher.ts:221`). Inventing a conversion