@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,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_crypto = require("node:crypto");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs, 1);
@@ -1,4 +1,4 @@
1
- import { in as buildFfmpegArgs, on as isSoftwareDecode, rn as Fmp4BoxSplitter, tn as errMsg } from "./dist-CCd0Q3nr.mjs";
1
+ import { an as isSoftwareDecode, en as errMsg, nn as Fmp4BoxSplitter, rn as buildFfmpegArgs } from "./dist-DVz_88D_.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
@@ -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-8up-f2TX.js");
7
- const require_node = require("../node-lpQgHes9.js");
8
- const require_default_detection_model = require("../default-detection-model-D1daTtqT.js");
6
+ const require_dist = require("../dist-L_HlB0At.js");
7
+ const require_node = require("../node-CmzZ_-zB.js");
8
+ const require_default_detection_model = require("../default-detection-model-BTJZvSr_.js");
9
9
  const require_worker_protocol = require("../worker-protocol-C-G8qmye.js");
10
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
11
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
@@ -638,6 +638,28 @@ function positiveOrUndefined(value) {
638
638
  return Number.isFinite(value) && value > 0 ? value : void 0;
639
639
  }
640
640
  //#endregion
641
+ //#region src/pipeline-runner/retained-reader-hold.ts
642
+ /**
643
+ * Run `read` with `handle`'s parked frame held as a CONSUMER for its whole
644
+ * duration (D656).
645
+ *
646
+ * `getNativeCrop` has two rungs: a native crop, then — on a miss — the retained
647
+ * full-frame fallback. Between them nothing is in flight: the crop's reply
648
+ * settles the worker's last pending request in the same turn, and the fallback's
649
+ * `toBuffer` is only sent on a later microtask. A parked frame does not keep a
650
+ * draining worker alive, so without this hold the drain killed the worker in
651
+ * that gap and the fallback was lost. One rule instead of a timing trick: a
652
+ * read in progress is a consumer.
653
+ */
654
+ async function withRetainedReader(store, handle, read) {
655
+ const release = store?.holdReader(handle) ?? null;
656
+ try {
657
+ return await read();
658
+ } finally {
659
+ release?.();
660
+ }
661
+ }
662
+ //#endregion
641
663
  //#region src/session-decode/native-crop-lease-age.ts
642
664
  /**
643
665
  * THE age bound on a native crop lease (ADR-0419).
@@ -980,6 +1002,8 @@ function createRetainedContentFallback(registry, ref) {
980
1002
  timestamp: ref.timestamp,
981
1003
  ...ref.capturedAt !== void 0 ? { capturedAt: ref.capturedAt } : {}
982
1004
  };
1005
+ } catch {
1006
+ return null;
983
1007
  } finally {
984
1008
  lease.release(reason);
985
1009
  }
@@ -1422,6 +1446,14 @@ var RetainedFrameStore = class RetainedFrameStore {
1422
1446
  if (current) current.hydrating = void 0;
1423
1447
  }
1424
1448
  }
1449
+ /**
1450
+ * Hold `handle`'s parked frame as a reader until the returned release runs
1451
+ * (D656). `null` when the handle has no un-hydrated fallback — bytes already
1452
+ * in RAM need no worker.
1453
+ */
1454
+ holdReader(handle) {
1455
+ return this.entries.get(keyOf(handle))?.fallback?.holdReader?.() ?? null;
1456
+ }
1425
1457
  /** Drop every retained frame (node/addon shutdown). */
1426
1458
  clear() {
1427
1459
  for (const entry of this.entries.values()) entry.fallback?.release?.();
@@ -1588,6 +1620,25 @@ var SESSION_DECODE_DRAIN_BOUND_MS = 1500;
1588
1620
  */
1589
1621
  var NATIVE_CROP_REFUSED_DRAINING = "session draining — crop requested after teardown";
1590
1622
  /**
1623
+ * The refusal for a `toBuffer` asked for once the worker has been told to exit
1624
+ * (D656). A read during a DRAIN is served — that is what the drain is for — but
1625
+ * after the kill no reply can come, and a raw rejection read as a worker fault.
1626
+ */
1627
+ var TO_BUFFER_REFUSED_KILLED = "session killed — read requested after the worker was told to exit";
1628
+ /** The refusal for a `toBuffer` the worker is no longer there to answer. */
1629
+ var TO_BUFFER_REFUSED_EXITED = "worker already exited";
1630
+ /** A read the session declined to send, by name — never a worker fault. */
1631
+ var SessionDecodeReadRefusedError = class extends Error {
1632
+ reason;
1633
+ frameId;
1634
+ constructor(reason, frameId) {
1635
+ super(`session-decode-coordinator: ${reason} (frameId=${frameId})`);
1636
+ this.reason = reason;
1637
+ this.frameId = frameId;
1638
+ this.name = "SessionDecodeReadRefusedError";
1639
+ }
1640
+ };
1641
+ /**
1591
1642
  * How often the cold-start wait REPORTS that it is still waiting. Task 0
1592
1643
  * live-measured cold-start (restream dial + hwaccel context + keyframe wait) at
1593
1644
  * 5-8s, so this must tolerate a slow-but-healthy start.
@@ -1671,11 +1722,18 @@ var SessionDecodeSession = class {
1671
1722
  /** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
1672
1723
  killSent = false;
1673
1724
  /**
1674
- * Images yielded to the consumer and not yet closed. Each is a frame the
1675
- * consumer still holds a reference to — queued, in flight, or being read —
1676
- * and each dies with the worker. This is what a drain waits on.
1725
+ * Images yielded and not yet closed that a CONSUMER still holds — queued,
1726
+ * in flight, or being read. Each dies with the worker and somebody was going
1727
+ * to read it first. This is what a drain waits on.
1728
+ */
1729
+ consumerImages = 0;
1730
+ /**
1731
+ * Images yielded and not yet closed that only a RETENTION holder keeps —
1732
+ * parked for a read that may come later (the on-demand JPEG fallback, D656).
1733
+ * They die with the worker too, but nobody reads them inside any bound worth
1734
+ * holding a worker for, so a drain does NOT wait on them.
1677
1735
  */
1678
- openImages = 0;
1736
+ retainedImages = 0;
1679
1737
  /** Armed while a drain waits; its expiry kills with frames still held. */
1680
1738
  drainTimer = null;
1681
1739
  sigkillTimer = null;
@@ -1729,7 +1787,8 @@ var SessionDecodeSession = class {
1729
1787
  }
1730
1788
  /** Send `toBuffer` for `frameId` and resolve with the matching `buffer` reply's bytes. */
1731
1789
  requestBuffer(frameId, opts) {
1732
- if (this.exited) return Promise.reject(/* @__PURE__ */ new Error(`session-decode-coordinator: worker already exited (frameId=${frameId})`));
1790
+ if (this.exited) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_EXITED, frameId));
1791
+ if (this.killSent) return Promise.reject(new SessionDecodeReadRefusedError(TO_BUFFER_REFUSED_KILLED, frameId));
1733
1792
  return new Promise((resolve, reject) => {
1734
1793
  const waiter = {
1735
1794
  resolve,
@@ -1818,13 +1877,28 @@ var SessionDecodeSession = class {
1818
1877
  frameId
1819
1878
  });
1820
1879
  }
1821
- /** A yielded image the consumer now holds — see {@link openImages}. */
1880
+ /** A yielded image the consumer now holds — see {@link consumerImages}. */
1822
1881
  noteImageOpened() {
1823
- this.openImages += 1;
1882
+ this.consumerImages += 1;
1824
1883
  }
1825
- /** The consumer let go of a yielded image; a waiting drain may now be done. */
1826
- noteImageClosed() {
1827
- this.openImages = Math.max(0, this.openImages - 1);
1884
+ /**
1885
+ * An open image moved between a consumer and a retention-only holder
1886
+ * (D656). Losing its last consumer may complete a waiting drain.
1887
+ */
1888
+ noteImageHolder(pendingConsumer) {
1889
+ if (pendingConsumer) {
1890
+ this.retainedImages = Math.max(0, this.retainedImages - 1);
1891
+ this.consumerImages += 1;
1892
+ return;
1893
+ }
1894
+ this.consumerImages = Math.max(0, this.consumerImages - 1);
1895
+ this.retainedImages += 1;
1896
+ this.settleDrainIfDone();
1897
+ }
1898
+ /** A yielded image was closed; a waiting drain may now be done. */
1899
+ noteImageClosed(pendingConsumer) {
1900
+ if (pendingConsumer) this.consumerImages = Math.max(0, this.consumerImages - 1);
1901
+ else this.retainedImages = Math.max(0, this.retainedImages - 1);
1828
1902
  this.settleDrainIfDone();
1829
1903
  }
1830
1904
  /**
@@ -1875,15 +1949,29 @@ var SessionDecodeSession = class {
1875
1949
  this.tryKill("SIGKILL");
1876
1950
  }
1877
1951
  /**
1878
- * Frames the consumer still holds, reads still waiting on the worker, and
1952
+ * Frames a CONSUMER still holds, reads still waiting on the worker, and
1879
1953
  * native crops already REQUESTED. A crop asked for before the teardown
1880
1954
  * request is waited for; one asked for after it is refused by name — post-analysis's
1881
1955
  * ordinary crop arrives 2.2–9.9 s after the frame, past any bound worth
1882
1956
  * holding a worker for, and is served from subject tiles that die with the
1883
1957
  * worker either way.
1958
+ *
1959
+ * A frame only PARKED for such a late read ({@link retainedImages}) is not
1960
+ * counted, for the same reason (D656): counting it made nearly every
1961
+ * teardown wait the full bound for frames nobody was going to read. Its read,
1962
+ * once STARTED, is a pending read like any other and is waited for.
1884
1963
  */
1885
1964
  unsettledCount() {
1886
- return this.openImages + this.pendingReadCount() + this.pendingNativeCrops.size;
1965
+ return this.consumerImages + this.pendingReadCount() + this.pendingNativeCrops.size;
1966
+ }
1967
+ /** What the session still holds, per category — every drain line carries it. */
1968
+ heldCounts() {
1969
+ return {
1970
+ consumerFrames: this.consumerImages,
1971
+ retainedFrames: this.retainedImages,
1972
+ pendingReads: this.pendingReadCount(),
1973
+ pendingCrops: this.pendingNativeCrops.size
1974
+ };
1887
1975
  }
1888
1976
  pendingReadCount() {
1889
1977
  let n = 0;
@@ -1907,9 +1995,7 @@ var SessionDecodeSession = class {
1907
1995
  this.logger.warn("session-decode-coordinator: drain bound expired — killing the worker with frames still held", {
1908
1996
  tags: { deviceId: this.deviceId },
1909
1997
  meta: {
1910
- unsettledFrames: this.openImages,
1911
- pendingReads: this.pendingReadCount(),
1912
- pendingCrops: this.pendingNativeCrops.size,
1998
+ ...this.heldCounts(),
1913
1999
  boundMs: SESSION_DECODE_DRAIN_BOUND_MS
1914
2000
  }
1915
2001
  });
@@ -1931,6 +2017,13 @@ var SessionDecodeSession = class {
1931
2017
  killNow(mode) {
1932
2018
  if (this.killSent) return;
1933
2019
  this.killSent = true;
2020
+ if (this.retainedImages > 0) this.logger.debug("session-decode-coordinator: retained frames die with the worker", {
2021
+ tags: { deviceId: this.deviceId },
2022
+ meta: {
2023
+ mode,
2024
+ ...this.heldCounts()
2025
+ }
2026
+ });
1934
2027
  this.trySend({ kind: "stop" });
1935
2028
  if (mode === "immediate") {
1936
2029
  this.tryKill("SIGKILL");
@@ -2032,10 +2125,7 @@ var SessionDecodeSession = class {
2032
2125
  meta: {
2033
2126
  code: info?.code ?? null,
2034
2127
  signal: info?.signal ?? null,
2035
- ...this.stopRequested ? {
2036
- unsettledFrames: this.openImages,
2037
- pendingReads: this.pendingReadCount()
2038
- } : {}
2128
+ ...this.stopRequested ? this.heldCounts() : {}
2039
2129
  }
2040
2130
  });
2041
2131
  this.exited = true;
@@ -2233,6 +2323,7 @@ var SessionDecodeSession = class {
2233
2323
  /** Build the {@link FrameImage} adapter for one `frame` reply. */
2234
2324
  function buildFrameImage(session, reply) {
2235
2325
  let closed = false;
2326
+ let pendingConsumer = true;
2236
2327
  session.noteImageOpened();
2237
2328
  return {
2238
2329
  width: reply.width,
@@ -2247,10 +2338,15 @@ function buildFrameImage(session, reply) {
2247
2338
  cutTiles: (bboxes) => session.cutTiles(reply.frameId, bboxes),
2248
2339
  releaseNativeLease: () => session.releaseLease(reply.frameId),
2249
2340
  markInferred: () => session.markInferred(reply.frameId),
2341
+ setPendingConsumer: (pending) => {
2342
+ if (closed || pending === pendingConsumer) return;
2343
+ pendingConsumer = pending;
2344
+ session.noteImageHolder(pending);
2345
+ },
2250
2346
  close: () => {
2251
2347
  if (closed) return;
2252
2348
  closed = true;
2253
- session.noteImageClosed();
2349
+ session.noteImageClosed(pendingConsumer);
2254
2350
  }
2255
2351
  };
2256
2352
  }
@@ -3927,173 +4023,6 @@ async function runDetailSubtree(deps, input) {
3927
4023
  return { details };
3928
4024
  }
3929
4025
  //#endregion
3930
- //#region src/pipeline-runner/device-jump.ts
3931
- /**
3932
- * Step-tree device jump (same-node, phase 1) — the PURE resolution core.
3933
- *
3934
- * Given the camera's elected ("effective") inference device and the node's
3935
- * enabled-device roster, decide which device ONE enrichment step should run on:
3936
- *
3937
- * 1. A manual `jumpDeviceKey` override wins when it names a candidate that can
3938
- * run the step (bypasses the auto choice; operator intent).
3939
- * 2. Otherwise, if the effective device's format can run the step's model →
3940
- * NO jump (the step inherits the effective device — the common case).
3941
- * 3. Otherwise AUTO-jump: among the node's OTHER enabled devices whose format
3942
- * can run the step (and that are under their `maxSessions` cap), pick the
3943
- * weighted-least-loaded one — mirroring the balancer's per-device core.
3944
- * 4. If nothing on the node can run it → `no-candidate` (the caller emits the
3945
- * loud once-per-(camera,step) warn that replaces today's silent degrade).
3946
- *
3947
- * Framework-free + side-effect-free so the whole decision table is unit-testable
3948
- * without a live runner/catalog. The `canRun(format)` predicate is injected by
3949
- * the runner (which owns the model catalog); this module never imports it.
3950
- */
3951
- /** The `ModelFormat` of a `<backend>:<device>` key, or `undefined` for an unknown backend. */
3952
- function formatOf(deviceKey) {
3953
- const colon = deviceKey.indexOf(":");
3954
- return require_dist.DEVICE_BACKEND_TO_FORMAT[colon >= 0 ? deviceKey.slice(0, colon) : deviceKey];
3955
- }
3956
- /** True when the step's model has a build for `deviceKey`'s format. */
3957
- function canRunOn(deviceKey, canRun) {
3958
- const format = formatOf(deviceKey);
3959
- return format !== void 0 && canRun(format);
3960
- }
3961
- /** Effective weighted load of a device (session count ÷ weight); lower = preferred. */
3962
- function weightedLoad(d) {
3963
- return d.attachedCameras / (d.weight > 0 ? d.weight : 1);
3964
- }
3965
- /**
3966
- * Weighted-least-loaded pick with a deterministic `deviceKey` tie-break —
3967
- * mirrors the node/device balancer's decision core (least `load/weight`, ties
3968
- * broken stably). `devices` is assumed non-empty.
3969
- */
3970
- function pickWeightedLeastLoaded(devices) {
3971
- return devices.reduce((best, d) => {
3972
- const dScore = weightedLoad(d);
3973
- const bScore = weightedLoad(best);
3974
- if (dScore < bScore) return d;
3975
- if (dScore > bScore) return best;
3976
- return d.deviceKey < best.deviceKey ? d : best;
3977
- });
3978
- }
3979
- /**
3980
- * Build the `canRun` predicate for ONE enrichment dispatch. The step and its
3981
- * enabled descendants ride the SAME `runPipeline` call — one pool, one format
3982
- * (detail-subtree contract: "its inherited children ride the same call → same
3983
- * device") — so a format can run the dispatch only when EVERY catalog-known
3984
- * step in the subtree ships a build for it. Gating only the subtree TOP let a
3985
- * `face-detection` with a tflite build stay on the Coral while its
3986
- * `face-embedding` child (no tflite build anywhere) doomed the call.
3987
- * Unknown/custom addonIds (`getStepDef` → null) have no catalog to gate on
3988
- * and never block. `getStepDef` is injected — this module stays catalog-free.
3989
- */
3990
- function buildSubtreeCanRun(step, getStepDef) {
3991
- const defs = [];
3992
- const walk = (s) => {
3993
- if (s.enabled === false) return;
3994
- const def = getStepDef(s.addonId);
3995
- if (def !== null) defs.push(def);
3996
- for (const child of s.children ?? []) walk(child);
3997
- };
3998
- walk(step);
3999
- return (format) => defs.every((def) => def.models.some((m) => m.formats[format]));
4000
- }
4001
- /**
4002
- * Resolve the device one enrichment step runs on. See the module header for the
4003
- * decision table. Pure — never throws, no I/O.
4004
- */
4005
- function resolveStepDevice(input) {
4006
- const { effectiveDeviceKey, manualJumpDeviceKey, canRun } = input;
4007
- if (manualJumpDeviceKey !== void 0 && manualJumpDeviceKey !== effectiveDeviceKey) {
4008
- const target = input.devices.find((d) => d.deviceKey === manualJumpDeviceKey);
4009
- if (target && canRunOn(target.deviceKey, canRun)) return {
4010
- kind: "jumped",
4011
- deviceKey: target.deviceKey,
4012
- reason: "manual"
4013
- };
4014
- }
4015
- if (canRunOn(effectiveDeviceKey, canRun)) return {
4016
- kind: "no-jump",
4017
- deviceKey: effectiveDeviceKey
4018
- };
4019
- const runnable = input.devices.filter((d) => d.deviceKey !== effectiveDeviceKey && (d.maxSessions === null || d.attachedCameras < d.maxSessions) && canRunOn(d.deviceKey, canRun));
4020
- if (runnable.length === 0) return {
4021
- kind: "no-candidate",
4022
- deviceKey: effectiveDeviceKey
4023
- };
4024
- return {
4025
- kind: "jumped",
4026
- deviceKey: pickWeightedLeastLoaded(runnable).deviceKey,
4027
- reason: "auto"
4028
- };
4029
- }
4030
- /**
4031
- * Re-provision a jumped subtree for its TARGET device (D650): every step's
4032
- * `modelId` / `settings` are REPLACED by the target's entry — the orchestrator
4033
- * computed it as what the step would carry had the camera been elected onto
4034
- * that device (its per-(node,device) base with the camera's override for it).
4035
- * A step absent from the map runs with neither, i.e. the target format's
4036
- * default model. The elected device's provisioning never follows a step across:
4037
- * it was chosen for a format this subtree does not run on.
4038
- */
4039
- function provisionSubtreeForDevice(step, provisioning) {
4040
- const own = provisioning[step.addonId];
4041
- const { modelId: _electedModelId, settings: _electedSettings, ...rest } = step;
4042
- return {
4043
- ...rest,
4044
- ...own?.modelId !== void 0 ? { modelId: own.modelId } : {},
4045
- ...own?.settings !== void 0 ? { settings: own.settings } : {},
4046
- ...step.children !== void 0 ? { children: step.children.map((c) => provisionSubtreeForDevice(c, provisioning)) } : {}
4047
- };
4048
- }
4049
- /**
4050
- * Place ONE detail subtree (D650): which same-node device runs it
4051
- * ({@link resolveStepDevice}) and, when it jumps, the provisioning it runs
4052
- * with there ({@link provisionSubtreeForDevice}). Pure — the caller logs.
4053
- *
4054
- * A roster entry without `steps` came from an orchestrator that predates D650:
4055
- * the step keeps what it was sent with, as it did before.
4056
- */
4057
- function placeDetailStep(input) {
4058
- const { step, effectiveDeviceKey, roster } = input;
4059
- if (!effectiveDeviceKey) return {
4060
- deviceKey: void 0,
4061
- step,
4062
- decision: null,
4063
- provisioned: false
4064
- };
4065
- if (!roster || roster.length < 2) return {
4066
- deviceKey: effectiveDeviceKey,
4067
- step,
4068
- decision: null,
4069
- provisioned: false
4070
- };
4071
- const decision = resolveStepDevice({
4072
- effectiveDeviceKey,
4073
- devices: roster.map((d) => ({
4074
- deviceKey: d.deviceKey,
4075
- weight: d.weight,
4076
- maxSessions: d.maxSessions,
4077
- attachedCameras: input.attachedByKey.get(d.deviceKey) ?? 0
4078
- })),
4079
- ...step.jumpDeviceKey !== void 0 ? { manualJumpDeviceKey: step.jumpDeviceKey } : {},
4080
- canRun: buildSubtreeCanRun(step, input.getStepDef)
4081
- });
4082
- if (decision.kind !== "jumped") return {
4083
- deviceKey: decision.deviceKey,
4084
- step,
4085
- decision,
4086
- provisioned: false
4087
- };
4088
- const provisioning = roster.find((d) => d.deviceKey === decision.deviceKey)?.steps;
4089
- return {
4090
- deviceKey: decision.deviceKey,
4091
- step: provisioning === void 0 ? step : provisionSubtreeForDevice(step, provisioning),
4092
- decision,
4093
- provisioned: provisioning !== void 0
4094
- };
4095
- }
4096
- //#endregion
4097
4026
  //#region src/pipeline-runner/device-load.ts
4098
4027
  /** The backend prefix of a `<backend>:<device>` key (bare `cpu` → `cpu`). */
4099
4028
  function backendOf(deviceKey) {
@@ -5377,7 +5306,7 @@ var ON_DEMAND_JPEG_VIEW = {
5377
5306
  format: "jpeg"
5378
5307
  };
5379
5308
  function createOnDemandJpegFallback(registry, ref) {
5380
- const lease = registry.acquire(ref);
5309
+ const lease = registry.acquire(ref, "retention");
5381
5310
  let cached;
5382
5311
  let released = false;
5383
5312
  const releaseLease = (reason) => {
@@ -5417,6 +5346,11 @@ function createOnDemandJpegFallback(registry, ref) {
5417
5346
  },
5418
5347
  release() {
5419
5348
  releaseLease("eviction");
5349
+ },
5350
+ holdReader() {
5351
+ if (released || cached !== void 0) return null;
5352
+ const reader = registry.acquire(ref);
5353
+ return reader ? () => reader.release("success") : null;
5420
5354
  }
5421
5355
  };
5422
5356
  }
@@ -7262,11 +7196,14 @@ async function runStatelessStep(deps, input) {
7262
7196
  return refuse(deps, input, "execution-failed", errorMessage(err));
7263
7197
  }
7264
7198
  if (result === null) return refuse(deps, input, "execution-failed", "the executor returned no frame result for the crop");
7199
+ const ranModelId = result.rootModelId;
7200
+ if (ranModelId === void 0 || ranModelId === "") return refuse(deps, input, "execution-failed", `the executor result names no model — refusing rather than assuming "${input.modelId}" ran`);
7201
+ 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`);
7265
7202
  const details = result.detections.map((detection) => toDetailResult(detection, input.stepId, crop));
7266
7203
  return {
7267
7204
  kind: "ran",
7268
7205
  nodeId: deps.nodeId,
7269
- modelId: input.modelId,
7206
+ modelId: ranModelId,
7270
7207
  details
7271
7208
  };
7272
7209
  }
@@ -7675,6 +7612,15 @@ function tryGetStepDefinition(stepId) {
7675
7612
  }
7676
7613
  }
7677
7614
  /**
7615
+ * A detail-plane step: its catalog definition consumes a parent class
7616
+ * (`inputClasses` non-null). Unknown/custom addonIds are root-plane — the
7617
+ * same reading as the executor's frame-plane skip and `deriveDetailSteps`.
7618
+ */
7619
+ function isDetailPlaneStepId(stepId) {
7620
+ const def = tryGetStepDefinition(stepId);
7621
+ return def !== null && def.inputClasses !== null;
7622
+ }
7623
+ /**
7678
7624
  * Precomputes the `detailSteps` announce (two-plane design) for a camera's
7679
7625
  * configured step tree: every ENABLED step whose catalog definition is a
7680
7626
  * detail/child step (`inputClasses !== null` — a root step like
@@ -8507,14 +8453,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8507
8453
  height: cached.height,
8508
8454
  format: cached.format
8509
8455
  }, pin)).frameId;
8510
- await api.pipelineExecutor.runPipeline.mutate({ ...buildBenchRunPipelineInput({
8456
+ const benchModelId = (await api.pipelineExecutor.runPipeline.mutate({ ...buildBenchRunPipelineInput({
8511
8457
  steps: stepsToRun,
8512
8458
  frame: sharedFrame
8513
- }) }, pin);
8459
+ }) }, pin)).rootModelId;
8460
+ if (benchModelId === void 0 || benchModelId === "") throw new Error(`synthetic bench: the warmup of "${rootStep.addonId}" named no model — refusing to time an unnamed variant`);
8514
8461
  const warmupCount = input.warmup ?? 1;
8515
8462
  for (let w = 0; w < warmupCount; w++) await api.pipelineExecutor.inferCached.mutate({
8516
8463
  stepId: rootStep.addonId,
8517
- frameId: poolFrameId
8464
+ frameId: poolFrameId,
8465
+ modelId: benchModelId
8518
8466
  }, pin);
8519
8467
  const wallTimings = [];
8520
8468
  const inferTimings = [];
@@ -8532,7 +8480,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8532
8480
  const t0 = performance.now();
8533
8481
  const result = await api.pipelineExecutor.inferCached.mutate({
8534
8482
  stepId: rootStep.addonId,
8535
- frameId: poolFrameId
8483
+ frameId: poolFrameId,
8484
+ modelId: benchModelId
8536
8485
  }, pin);
8537
8486
  const wallMs = performance.now() - t0;
8538
8487
  const r = result;
@@ -9207,6 +9156,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9207
9156
  * resolve legitimately want the lower-RES honest frame).
9208
9157
  */
9209
9158
  async getNativeCrop(input) {
9159
+ return withRetainedReader(this.retainedFrames, input.handle, () => this.resolveNativeCropBothRungs(input));
9160
+ }
9161
+ /** {@link getNativeCrop}'s two rungs, run under its reader hold. */
9162
+ async resolveNativeCropBothRungs(input) {
9210
9163
  const deviceId = this.nativeCropRegistry.deviceIdFor(input.handle) ?? 0;
9211
9164
  const isFullFrame = input.bbox.x <= .001 && input.bbox.y <= .001 && input.bbox.w >= .999 && input.bbox.h >= .999;
9212
9165
  this.frameLazy.noteOnDemandRequest({
@@ -9722,6 +9675,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9722
9675
  */
9723
9676
  async runDetailSubtree(input) {
9724
9677
  const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
9678
+ await this.customModels.refresh(this.ctxIfReady?.api);
9725
9679
  const outcome = await runDetailSubtree({
9726
9680
  resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
9727
9681
  getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
@@ -9850,6 +9804,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9850
9804
  * global settings, NOT from the attach config. See `crop-convention-source.ts`
9851
9805
  * for why eleven attach builders make the attach config the wrong carrier.
9852
9806
  */
9807
+ /**
9808
+ * The custom-model registry, merged into the catalog the device jump judges
9809
+ * (D657): a custom model with a tflite build, pinned on a Coral, runs there.
9810
+ * Refreshed on the async boundaries (`runInference`, `runDetailSubtree`),
9811
+ * read synchronously by the placement.
9812
+ */
9813
+ customModels = new require_default_detection_model.CustomModelCatalog({ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) });
9853
9814
  cropConvention = new CropConventionSource({ logger: {
9854
9815
  info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
9855
9816
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
@@ -9953,18 +9914,63 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9953
9914
  */
9954
9915
  placeDetailStepForCamera(deviceId, step) {
9955
9916
  const config = this.attached.get(deviceId)?.config;
9956
- const placement = placeDetailStep({
9957
- step,
9958
- effectiveDeviceKey: config?.deviceKey,
9959
- roster: config?.inferenceDevices,
9960
- attachedByKey: this.perDeviceAttachedCounts(),
9961
- getStepDef: tryGetStepDefinition
9962
- });
9917
+ const placement = this.placementOf(config, step, this.perDeviceAttachedCounts());
9963
9918
  const decision = placement.decision;
9964
9919
  if (decision?.kind === "no-candidate") this.warnStepHasNoRunnableDevice(deviceId, step.addonId, decision.deviceKey);
9965
9920
  else if (decision?.kind === "jumped" && config?.deviceKey) this.logStepJumped(deviceId, config.deviceKey, decision, placement);
9966
9921
  return placement;
9967
9922
  }
9923
+ /**
9924
+ * Where one detail subtree of this camera runs — the ONE decision both the
9925
+ * detail plane (which dispatches it) and the frame plane (which decides what
9926
+ * the elected pool pre-warms, D657) read. Pure: nothing is logged here.
9927
+ */
9928
+ placementOf(config, step, attachedByKey) {
9929
+ return require_default_detection_model.placeDetailStep({
9930
+ step,
9931
+ effectiveDeviceKey: config?.deviceKey,
9932
+ roster: config?.inferenceDevices,
9933
+ attachedByKey,
9934
+ getStepDef: this.customModels.stepDefinition
9935
+ });
9936
+ }
9937
+ /**
9938
+ * The tree this camera's frame-plane dispatch carries (D657): a detail
9939
+ * subtree the placement sends to another device (or to none) is left out,
9940
+ * so its model is never loaded on the elected pool — it loads on its
9941
+ * target, with the target's provisioning, when the detail plane dispatches
9942
+ * it there. Said once per (camera, elected device, set left out).
9943
+ */
9944
+ stepsForElectedPoolOf(deviceId, steps) {
9945
+ const config = this.attached.get(deviceId)?.config;
9946
+ if (config?.deviceKey === void 0 || (config.inferenceDevices?.length ?? 0) < 2) {
9947
+ this.noteElectedPoolDelegation(deviceId, void 0, []);
9948
+ return steps;
9949
+ }
9950
+ const attachedByKey = this.perDeviceAttachedCounts();
9951
+ const result = require_default_detection_model.stepsForElectedPool(steps, isDetailPlaneStepId, (step) => this.placementOf(config, step, attachedByKey));
9952
+ this.noteElectedPoolDelegation(deviceId, config.deviceKey, result.delegated);
9953
+ return result.delegated.length === 0 ? steps : result.steps;
9954
+ }
9955
+ /**
9956
+ * Say once per STATE CHANGE which detail steps a camera's elected pool no
9957
+ * longer loads: a camera moving A → B → A says so again on A.
9958
+ */
9959
+ noteElectedPoolDelegation(deviceId, electedDeviceKey, delegated) {
9960
+ const state = `${electedDeviceKey ?? ""}|${delegated.join(",")}`;
9961
+ if (this.electedPoolDelegationState.get(deviceId) === state) return;
9962
+ this.electedPoolDelegationState.set(deviceId, state);
9963
+ if (delegated.length === 0) return;
9964
+ this.ctx?.logger.info("detail steps not loaded on the elected pool — they run elsewhere", {
9965
+ tags: { deviceId },
9966
+ meta: {
9967
+ electedDeviceKey: electedDeviceKey ?? null,
9968
+ steps: [...delegated]
9969
+ }
9970
+ });
9971
+ }
9972
+ /** Per camera, the last (elected device, steps left out) state that was logged. */
9973
+ electedPoolDelegationState = /* @__PURE__ */ new Map();
9968
9974
  /** `${deviceId}:${addonId}:${target}` triples whose jump has been logged. */
9969
9975
  loggedStepJumps = /* @__PURE__ */ new Set();
9970
9976
  logStepJumped(deviceId, electedDeviceKey, decision, placement) {
@@ -10201,6 +10207,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10201
10207
  const warnPrefix = `${deviceId}:`;
10202
10208
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
10203
10209
  this.attached.delete(deviceId);
10210
+ this.electedPoolDelegationState.delete(deviceId);
10204
10211
  this.lastMotionAt.delete(deviceId);
10205
10212
  this.lastEmittedCameraMetrics.delete(deviceId);
10206
10213
  this.retention?.forgetDevice(deviceId);
@@ -10911,6 +10918,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10911
10918
  }
10912
10919
  const camConfig = this.attached.get(deviceId)?.config;
10913
10920
  const steps = camConfig?.steps;
10921
+ await this.customModels.refresh(api);
10914
10922
  if (!steps) {
10915
10923
  log.warn("runInference: no steps in attach config — skipping frame (legacy attach?)");
10916
10924
  return null;
@@ -10945,6 +10953,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10945
10953
  });
10946
10954
  }
10947
10955
  }
10956
+ dispatchSteps = this.stepsForElectedPoolOf(deviceId, dispatchSteps);
10948
10957
  let image;
10949
10958
  const executorCoLocated = this.executorCoLocated();
10950
10959
  if (!executorCoLocated) {
@@ -11074,6 +11083,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11074
11083
  const cropResult = await api.pipelineExecutor.runPipeline.mutate({
11075
11084
  steps: [step],
11076
11085
  image: view.jpeg,
11086
+ deviceId,
11077
11087
  plane: "frame",
11078
11088
  ...deviceKey ? { deviceKey } : {}
11079
11089
  });