@camstack/addon-pipeline 1.2.43 → 1.2.44

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 +157 -81
  4. package/dist/detection-pipeline/index.mjs +157 -81
  5. package/dist/{dist-UHbKZiSa.js → dist-BuFE5rOK.js} +724 -39
  6. package/dist/{dist-B-VVBzrL.mjs → dist-DOu93i_g.mjs} +659 -40
  7. package/dist/{event-loop-stall-monitor-DfKdC7G4.mjs → event-loop-stall-monitor-CShgQE6l.mjs} +40 -2
  8. package/dist/{event-loop-stall-monitor-C7_L6vHO.js → event-loop-stall-monitor-L08yG6bB.js} +40 -2
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +342 -69
  12. package/dist/pipeline-runner/index.mjs +342 -69
  13. package/dist/recorder/index.js +189 -6
  14. package/dist/recorder/index.mjs +189 -6
  15. package/dist/session-decode/decode-worker-child.js +197 -24
  16. package/dist/session-decode/decode-worker-child.mjs +197 -24
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D7iQbuKY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DiXoZnou.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BRc-m6W-.mjs +26 -0
  20. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-54IklEep.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_hakWAb.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-CKYS9yZO.mjs → hostInit-khRXMnjl.mjs} +2 -2
  22. package/dist/stream-broker/index.js +3952 -1103
  23. package/dist/stream-broker/index.mjs +3953 -1104
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/worker-protocol-BOXlUhWO.mjs +244 -0
  26. package/dist/worker-protocol-VURr0nUh.js +279 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DxZ75vB8.js → MaskShapeCanvas-DI4BY7W2-DHZuYYjH.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BBY2Ztz5.js → MotionZonesSettings-NcxxQN8r-jFDOzPCD.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BC-3QmHa.js → PrivacyMaskSettings-APgPLF7p-BPBXgIX8.js} +1 -1
  30. package/embed-dist/assets/{index-on1wyOnd.js → index-4CtQAybX.js} +12 -12
  31. package/embed-dist/assets/index-DC63og2c.css +2 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCC9fqHH.mjs +0 -26
  35. package/dist/worker-protocol-D7RzZIla.mjs +0 -77
  36. package/dist/worker-protocol-DextwlTX.js +0 -94
  37. package/embed-dist/assets/index-CGdwTcwE.css +0 -2
@@ -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-UHbKZiSa.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-C7_L6vHO.js");
8
- const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
6
+ const require_dist = require("../dist-BuFE5rOK.js");
7
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-L08yG6bB.js");
8
+ const require_worker_protocol = require("../worker-protocol-VURr0nUh.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
10
  const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
11
11
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
@@ -83,9 +83,28 @@ function wrapChild(child) {
83
83
  * Fork one decode-worker child. `source` is part of the coordinator's fork
84
84
  * signature (it forwards the same source in the subsequent `start` message);
85
85
  * the child entry itself is source-independent, so it is not consumed here.
86
+ *
87
+ * `leaseOverride` carries the operator's cluster-wide native-lease settings
88
+ * (`pipeline-orchestrator` globals, read by `native-lease-settings-source.ts`)
89
+ * into the child as environment variables — the child constructs its lease
90
+ * store and demand gate at module load, before the first `start` message, so
91
+ * the values have to be present at spawn. Only the knobs the operator actually
92
+ * SET are emitted, which is what keeps the pre-settings env override
93
+ * (`CAMSTACK_SESSION_NATIVE_LEASE_*`) working for the untouched ones; see
94
+ * `native-lease-config.ts` for the full precedence.
95
+ *
96
+ * The inherited environment is SPREAD, never replaced: this child resolves
97
+ * node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
98
+ * agents, so a replaced env would silently break decoding off the hub.
86
99
  */
87
- function forkDecodeWorker(_source) {
88
- return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], { serialization: "advanced" }));
100
+ function forkDecodeWorker(_source, leaseOverride = {}) {
101
+ return wrapChild((0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
102
+ serialization: "advanced",
103
+ env: {
104
+ ...process.env,
105
+ ...require_worker_protocol.nativeLeaseSettingEnv(leaseOverride)
106
+ }
107
+ }));
89
108
  }
90
109
  //#endregion
91
110
  //#region src/session-decode/guarded-session-decode.ts
@@ -305,7 +324,7 @@ var RETAINED_FRAME_BUDGET_BYTES = (() => {
305
324
  * missing thumbnails/firstFrames concentrated on exactly those cameras).
306
325
  * 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
307
326
  * so the worst-case protected footprint stays well inside the byte budget for
308
- * realistic concurrency (4 active cams ≈ 44 MB of 96); the hard ceiling always
327
+ * realistic concurrency (4 active cams ≈ 44 MB of 160); the hard ceiling always
309
328
  * overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
310
329
  * `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
311
330
  */
@@ -657,6 +676,19 @@ var SessionDecodeSession = class {
657
676
  frameId
658
677
  });
659
678
  }
679
+ /**
680
+ * Tell the worker `frameId` reached inference, so its native lease is worth
681
+ * keeping. Fire and forget, no reply. Ordering matters and is free here: the
682
+ * caller runs in the same turn that consumed the frame, and the pull that
683
+ * decides this frame's admission is issued after that turn returns.
684
+ */
685
+ markInferred(frameId) {
686
+ if (this.exited) return;
687
+ this.trySend({
688
+ kind: "markInferred",
689
+ frameId
690
+ });
691
+ }
660
692
  /** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
661
693
  teardown() {
662
694
  this.stopRequested = true;
@@ -877,6 +909,7 @@ function buildFrameImage(session, reply) {
877
909
  },
878
910
  nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
879
911
  releaseNativeLease: () => session.releaseLease(reply.frameId),
912
+ markInferred: () => session.markInferred(reply.frameId),
880
913
  close: () => {
881
914
  closed = true;
882
915
  }
@@ -1006,7 +1039,10 @@ function startSessionDecodePump(deps) {
1006
1039
  const decoded = await toDecodedFrame(frame, format);
1007
1040
  const image = frame.image;
1008
1041
  const cropFn = image.nativeCrop;
1009
- const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), { release: () => image.releaseNativeLease?.() }) : void 0;
1042
+ const nativeCrop = cropFn ? Object.assign((bbox, maxWidth) => cropFn(bbox, maxWidth), {
1043
+ release: () => image.releaseNativeLease?.(),
1044
+ markInferred: () => image.markInferred?.()
1045
+ }) : void 0;
1010
1046
  deps.onDecodedFrame(decoded, nativeCrop);
1011
1047
  } catch (err) {
1012
1048
  deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
@@ -1788,7 +1824,121 @@ var CropConventionSource = class {
1788
1824
  }
1789
1825
  };
1790
1826
  //#endregion
1827
+ //#region src/pipeline-runner/native-lease-settings-source.ts
1828
+ /**
1829
+ * Where the runner gets the cluster-wide native-frame LEASE knobs.
1830
+ *
1831
+ * ## Why a settings read and not the attach config
1832
+ *
1833
+ * Identical reasoning to `crop-convention-source.ts`, and worth restating
1834
+ * because the temptation is the same: `RunnerCameraConfig` is the obvious
1835
+ * orchestrator→runner channel and it is the wrong one. Eleven call sites build
1836
+ * one; a cluster-wide constant threaded through eleven builders eventually
1837
+ * reaches ten of them, and the cameras attached by the forgotten builder just
1838
+ * quietly run a different lease window. Instead the runner reads the OWNER
1839
+ * addon's global settings directly, hub-routed by `AddonCallGateway`, so an
1840
+ * agent runner transparently reads the hub-central store with no placement
1841
+ * branching.
1842
+ *
1843
+ * ## Deliberately NOT node-scoped
1844
+ *
1845
+ * The read passes no `nodeId`. With one, `addon-settings` would project that
1846
+ * node's scoped value and two nodes could hold different lease windows for the
1847
+ * same camera — so whether a stored crop is native pixels or the ≤640 fallback
1848
+ * would depend on where the balancer happened to place it, which is invisible
1849
+ * from the stored media.
1850
+ *
1851
+ * ## What it hands out
1852
+ *
1853
+ * A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
1854
+ * decode worker resolves the rest (env var, then the shipped default); see
1855
+ * `session-decode/native-lease-config.ts` for the precedence. Handing out a
1856
+ * fully-resolved triple here would erase the "operator set nothing" state and
1857
+ * with it the env-var escape hatch.
1858
+ */
1859
+ /**
1860
+ * The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
1861
+ * hub-resident and neutral — the same owner as the detail-crop convention and
1862
+ * the per-node decoder-backend selector.
1863
+ */
1864
+ var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
1865
+ /**
1866
+ * Whether two overrides name the same knobs with the same values.
1867
+ *
1868
+ * **Every knob has to appear here.** This is the equality that decides whether
1869
+ * a re-read is ADOPTED at all: a knob missing from the comparison is read from
1870
+ * the hub, judged identical, and discarded, so the operator's change never
1871
+ * reaches a decode worker and nothing anywhere says so. `admission` shipped
1872
+ * missing from this list and did exactly that — the setting was stored, the
1873
+ * hub served it, the runner fetched it, and every worker kept spawning with the
1874
+ * old value.
1875
+ */
1876
+ function sameOverride(a, b) {
1877
+ return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
1878
+ }
1879
+ /**
1880
+ * TTL-cached read of the cluster native-lease knobs.
1881
+ *
1882
+ * {@link current} is synchronous because the fork that consumes it is;
1883
+ * {@link refresh} is what talks to the hub, and the runner awaits it on the
1884
+ * path that precedes a decode-worker spawn.
1885
+ */
1886
+ var NativeLeaseSettingsSource = class {
1887
+ override = {};
1888
+ lastReadAtMs = Number.NEGATIVE_INFINITY;
1889
+ inFlight = null;
1890
+ logger;
1891
+ now;
1892
+ ttlMs;
1893
+ constructor(options) {
1894
+ this.logger = options.logger;
1895
+ this.now = options.now ?? (() => Date.now());
1896
+ this.ttlMs = options.ttlMs ?? 3e4;
1897
+ }
1898
+ /** The operator's overrides as last read — `{}` until a read lands. */
1899
+ current() {
1900
+ return this.override;
1901
+ }
1902
+ /**
1903
+ * Re-read if the cached value has expired. Concurrent callers share one
1904
+ * in-flight read; a failure keeps the LAST KNOWN value rather than reverting
1905
+ * to `{}`, because a revert would silently hand the next-spawned workers a
1906
+ * different RAM profile than the ones already running.
1907
+ */
1908
+ async refresh(api) {
1909
+ if (api === void 0) return this.override;
1910
+ if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
1911
+ this.inFlight ??= this.read(api).finally(() => {
1912
+ this.inFlight = null;
1913
+ });
1914
+ await this.inFlight;
1915
+ return this.override;
1916
+ }
1917
+ async read(api) {
1918
+ try {
1919
+ const next = require_dist.pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
1920
+ this.lastReadAtMs = this.now();
1921
+ if (sameOverride(next, this.override)) return;
1922
+ this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
1923
+ from: { ...this.override },
1924
+ to: { ...next }
1925
+ } });
1926
+ this.override = next;
1927
+ } catch (err) {
1928
+ this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
1929
+ owner: NATIVE_LEASE_OWNER_ADDON_ID,
1930
+ inUse: { ...this.override },
1931
+ error: err instanceof Error ? err.message : String(err)
1932
+ } });
1933
+ }
1934
+ }
1935
+ };
1936
+ //#endregion
1791
1937
  //#region src/pipeline-runner/detail-subtree.ts
1938
+ /** Did the native-lease fetch report a miss (rather than return pixels)? */
1939
+ function isNativeCropMiss(fetch) {
1940
+ return "miss" in fetch;
1941
+ }
1792
1942
  /** Normalize a frame-space pixel rect to `[0,1]` by the given frame dims. */
1793
1943
  function normalizeRect(rect, frameWidth, frameHeight) {
1794
1944
  return {
@@ -1940,40 +2090,82 @@ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
1940
2090
  };
1941
2091
  }
1942
2092
  async function resolveCrop(deps, input, convention) {
2093
+ let nativeMiss = null;
1943
2094
  if (input.frameHandle) {
1944
2095
  const normalized = normalizeRect(require_dist.deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
1945
2096
  const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
1946
- if (native) return {
1947
- ...native,
1948
- source: "native"
1949
- };
2097
+ if (native !== null) {
2098
+ if (!isNativeCropMiss(native)) return {
2099
+ crop: {
2100
+ ...native,
2101
+ source: "native"
2102
+ },
2103
+ nativeMiss
2104
+ };
2105
+ nativeMiss = native;
2106
+ }
1950
2107
  }
1951
- if (input.frameJpeg) return cutFromFullFrame(input.frameJpeg, {
1952
- kind: "frame-pixels",
1953
- rect: input.parent.bbox
1954
- }, convention);
2108
+ if (input.frameJpeg) return {
2109
+ crop: await cutFromFullFrame(input.frameJpeg, {
2110
+ kind: "frame-pixels",
2111
+ rect: input.parent.bbox
2112
+ }, convention),
2113
+ nativeMiss
2114
+ };
1955
2115
  if (input.cropJpeg) {
1956
2116
  const jpeg = Buffer.from(input.cropJpeg, "base64");
1957
2117
  const meta = await (await require_lazy_sharp.getSharp())(jpeg).metadata();
1958
2118
  const width = meta.width ?? 0;
1959
2119
  const height = meta.height ?? 0;
1960
- if (width <= 0 || height <= 0) return null;
2120
+ if (width <= 0 || height <= 0) return {
2121
+ crop: null,
2122
+ nativeMiss
2123
+ };
1961
2124
  return {
1962
- jpeg,
1963
- width,
1964
- height,
1965
- frameSpace: input.parent.bbox,
1966
- source: "fallback"
2125
+ crop: {
2126
+ jpeg,
2127
+ width,
2128
+ height,
2129
+ frameSpace: input.parent.bbox,
2130
+ source: "fallback"
2131
+ },
2132
+ nativeMiss
1967
2133
  };
1968
2134
  }
1969
- return null;
2135
+ return {
2136
+ crop: null,
2137
+ nativeMiss
2138
+ };
1970
2139
  }
1971
2140
  /**
1972
- * Run the DETAIL subtree for a single tracked detection: resolve a crop
1973
- * (native lease `cropJpeg` fallback), run each matched top-level child
1974
- * step's chain against it, and map every result bbox back to frame space.
1975
- * Returns `null` when the camera isn't attached, no configured child
1976
- * matches `input.parent.className`, or no crop source resolves.
2141
+ * Run the DETAIL subtree for a single tracked detection: resolve a crop, run
2142
+ * each matched top-level child step's chain against it, and map every result
2143
+ * bbox back to frame space. Returns `null` when the camera isn't attached, no
2144
+ * configured child matches `input.parent.className`, or no crop source
2145
+ * resolves.
2146
+ *
2147
+ * ## The three pixel sources, in the order they are tried
2148
+ *
2149
+ * The distinction between them is not the pixels — it is WHICH SIDE derives the
2150
+ * rectangle, and conflating that is how one vector index came to hold two
2151
+ * feature spaces ([D52](../../../../docs/decisions/adr-0052.md)):
2152
+ *
2153
+ * 1. `frameHandle` — the node-local native lease. Cheapest when it hits (no
2154
+ * payload crosses anything) and the only rung that yields a `nativeCropRef`,
2155
+ * so leaf children re-cut their ROI at native resolution. **The runner cuts.**
2156
+ * 2. `frameJpeg` — a FULL frame the caller still holds. **The runner cuts**, with
2157
+ * the same `deriveDetailCropRect` and the same convention, so the rectangle is
2158
+ * identical to what rung 1 would have asked for. Reached when the lease has
2159
+ * already expired — which, for the live detail plane, is the common case: the
2160
+ * dispatcher's tick plus a 500–900 ms inference routinely outlives a 1.2 s
2161
+ * lease, and before this rung was wired the step was simply abandoned.
2162
+ * 3. `cropJpeg` — an ALREADY-CUT tile, used verbatim, no padding and no squaring.
2163
+ * **The caller decided the rectangle**, so it is outside the single-derivation
2164
+ * guarantee and is only for the lease-miss retry.
2165
+ *
2166
+ * A rung-1 miss is remembered even when a lower rung succeeds, and travels into
2167
+ * the drop line below: "the native lease never hits any more" is precisely the
2168
+ * condition that goes invisible once something else covers for it.
1977
2169
  *
1978
2170
  * STRICT `steps` semantics: when the caller names an explicit `input.steps`
1979
2171
  * set, each matched root's descendant chain is pruned to exactly those ids
@@ -2009,7 +2201,7 @@ async function runDetailSubtree(deps, input) {
2009
2201
  }
2010
2202
  const requested = input.steps;
2011
2203
  const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
2012
- const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
2204
+ const { crop, nativeMiss } = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
2013
2205
  if (!crop) {
2014
2206
  deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
2015
2207
  tags: { deviceId: input.deviceId },
@@ -2017,7 +2209,9 @@ async function runDetailSubtree(deps, input) {
2017
2209
  parentClass: input.parent.className,
2018
2210
  hadFrameHandle: input.frameHandle !== void 0,
2019
2211
  hadFrameJpeg: input.frameJpeg !== void 0,
2020
- hadCropJpeg: input.cropJpeg !== void 0
2212
+ hadCropJpeg: input.cropJpeg !== void 0,
2213
+ nativeMissReason: nativeMiss?.miss ?? null,
2214
+ nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
2021
2215
  }
2022
2216
  });
2023
2217
  return null;
@@ -2972,16 +3166,24 @@ var PipelineRunner = class {
2972
3166
  handle
2973
3167
  });
2974
3168
  }
3169
+ /**
3170
+ * Returns whether the frame was ADMITTED to the detection queue — i.e.
3171
+ * whether it will reach inference. The caller uses that to mark the frame's
3172
+ * native lease worth keeping (`NativeCropRequester.markInferred`): a native
3173
+ * crop can only ever name a frameId that rode an inference result, so a
3174
+ * frame rejected here is one nobody can ask for. A `false` here is the whole
3175
+ * saving; do not turn it back into `void` for tidiness.
3176
+ */
2975
3177
  enqueueDetectionFrame(deviceId, frame, handle) {
2976
3178
  const state = this.cameras.get(deviceId);
2977
- if (!state) return;
3179
+ if (!state) return false;
2978
3180
  this.arrivalCounts.set(deviceId, (this.arrivalCounts.get(deviceId) ?? 0) + 1);
2979
- if (state.phase !== "active") return;
3181
+ if (state.phase !== "active") return false;
2980
3182
  const interval = this.governor.minIntervalMs(deviceId);
2981
3183
  if (interval > 0) {
2982
3184
  const last = this.lastAdmittedAt.get(deviceId) ?? 0;
2983
3185
  const now = Date.now();
2984
- if (now - last < interval) return;
3186
+ if (now - last < interval) return false;
2985
3187
  this.lastAdmittedAt.set(deviceId, now);
2986
3188
  }
2987
3189
  frame._enqueuedAt = Date.now();
@@ -2989,6 +3191,7 @@ var PipelineRunner = class {
2989
3191
  frame,
2990
3192
  handle
2991
3193
  });
3194
+ return true;
2992
3195
  }
2993
3196
  /**
2994
3197
  * `handle` is optional (mirrors {@link DetectionQueueEntry.handle}) — the
@@ -3000,13 +3203,14 @@ var PipelineRunner = class {
3000
3203
  */
3001
3204
  enqueueOccupancyFrame(deviceId, frame, handle) {
3002
3205
  const state = this.cameras.get(deviceId);
3003
- if (!state) return;
3004
- if (state.phase !== "watching") return;
3206
+ if (!state) return false;
3207
+ if (state.phase !== "watching") return false;
3005
3208
  frame._enqueuedAt = Date.now();
3006
3209
  state.detectionQueue.enqueue({
3007
3210
  frame,
3008
3211
  handle
3009
3212
  });
3213
+ return true;
3010
3214
  }
3011
3215
  /**
3012
3216
  * Report a motion event for a camera. Drives the unified phase
@@ -4769,13 +4973,38 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4769
4973
  * resolve legitimately want the lower-RES honest frame).
4770
4974
  */
4771
4975
  async getNativeCrop(input) {
4976
+ const resolved = await this.resolveNativeCropPixels(input);
4977
+ if (resolved.tier === "native" && resolved.pixels !== null) return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
4978
+ if (resolved.tier !== "ram-fullframe") return null;
4979
+ const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
4980
+ if (!crop) return null;
4981
+ return this.encodeNativeCropResult({
4982
+ bytes: crop.bytes,
4983
+ width: crop.width,
4984
+ height: crop.height
4985
+ }, resolved.tier, input.encodeJpeg);
4986
+ }
4987
+ /**
4988
+ * The native-lease half of {@link getNativeCrop}, extracted so the DETAIL path
4989
+ * can keep the miss REASON the cap's `null` return has no room for. Both
4990
+ * callers share this one decision — a second copy of the tier arithmetic is
4991
+ * how a quality tier starts differing by caller.
4992
+ *
4993
+ * Logging level follows the CONSEQUENCE, not the event: a native miss that the
4994
+ * RAM tier is allowed to serve (a full-frame resolve) cost nobody anything and
4995
+ * stays at debug; a miss the caller is handed as `null` is lost work and is a
4996
+ * warn, carrying the numbers that decide the fix.
4997
+ */
4998
+ async resolveNativeCropPixels(input) {
4772
4999
  const b = input.bbox;
4773
5000
  const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
4774
5001
  const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
4775
5002
  let nativeResult = null;
4776
5003
  let missReason = null;
5004
+ let handleAgeMs = null;
4777
5005
  if (!isUnboundedFullFrame) {
4778
5006
  const found = this.nativeCropRegistry.get(input.handle);
5007
+ handleAgeMs = found?.ageMs ?? null;
4779
5008
  if (found) {
4780
5009
  const result = await found.request(input.bbox, input.maxWidth);
4781
5010
  if (result) nativeResult = {
@@ -4785,28 +5014,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4785
5014
  };
4786
5015
  else missReason = "worker-lease-gone";
4787
5016
  } else missReason = "no-registry-entry";
4788
- if (missReason !== null) this.ctx.logger.debug("native crop miss", {
4789
- tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
4790
- meta: {
4791
- reason: missReason,
4792
- handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
4793
- handleNodeId: input.handle.nodeId,
4794
- handleAgeMs: found?.ageMs ?? null,
4795
- registrySize: this.nativeCropRegistry.size,
4796
- registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
4797
- }
4798
- });
5017
+ if (missReason !== null) {
5018
+ const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
5019
+ const fields = {
5020
+ tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
5021
+ meta: {
5022
+ reason: missReason,
5023
+ servedTier: tierOnMiss,
5024
+ handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
5025
+ handleNodeId: input.handle.nodeId,
5026
+ handleAgeMs,
5027
+ registrySize: this.nativeCropRegistry.size,
5028
+ registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
5029
+ }
5030
+ };
5031
+ if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
5032
+ else this.ctx.logger.debug("native crop miss", fields);
5033
+ }
4799
5034
  }
4800
- const tier = resolveNativeCropSource(isFullFrame, nativeResult !== null);
4801
- if (tier === "native" && nativeResult) return this.encodeNativeCropResult(nativeResult, tier, input.encodeJpeg);
4802
- if (tier !== "ram-fullframe") return null;
4803
- const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
4804
- if (!crop) return null;
4805
- return this.encodeNativeCropResult({
4806
- bytes: crop.bytes,
4807
- width: crop.width,
4808
- height: crop.height
4809
- }, tier, input.encodeJpeg);
5035
+ return {
5036
+ pixels: nativeResult,
5037
+ tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
5038
+ missReason,
5039
+ handleAgeMs
5040
+ };
4810
5041
  }
4811
5042
  /**
4812
5043
  * Shape a resolved RGB crop into a `getNativeCrop` result. Raw path (default):
@@ -4929,6 +5160,34 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4929
5160
  info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
4930
5161
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
4931
5162
  } });
5163
+ /**
5164
+ * The cluster-wide native-frame LEASE knobs (TTL / RAM ceiling / demand
5165
+ * window) — read from the orchestrator's global settings for the same reason
5166
+ * as the crop convention above, and injected into each decode worker at fork
5167
+ * time. See `native-lease-settings-source.ts`.
5168
+ */
5169
+ nativeLease = new NativeLeaseSettingsSource({ logger: {
5170
+ info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
5171
+ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
5172
+ } });
5173
+ /**
5174
+ * Fork a decode worker carrying the operator's lease knobs. Used in place of
5175
+ * a bare `forkDecodeWorker` reference at every session-decode spawn site: the
5176
+ * knobs are read at spawn, so a changed setting reaches NEW sessions and
5177
+ * leaves running ones alone (a live session would have to be re-dialled).
5178
+ */
5179
+ forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current());
5180
+ /**
5181
+ * Refresh the lease knobs before a decode worker is spawned. Awaited on the
5182
+ * `acquire` leg of `startGuardedSessionDecode` (which always completes before
5183
+ * `startPump` forks), so the spawn sees a value at most one TTL stale rather
5184
+ * than whatever a background refresh happened to have landed.
5185
+ * `NativeLeaseSettingsSource.refresh` swallows its own failures, so this can
5186
+ * never break a dial.
5187
+ */
5188
+ async refreshNativeLeaseSettings() {
5189
+ await this.nativeLease.refresh(this.ctxIfReady?.api);
5190
+ }
4932
5191
  /** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
4933
5192
  detailSubtreeLogger() {
4934
5193
  return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
@@ -5011,18 +5270,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5011
5270
  * lease (same path the `getNativeCrop` cap method above uses), encodes the
5012
5271
  * raw RGB crop to JPEG, and denormalizes `paddedBbox` back to frame-space
5013
5272
  * pixels for the `frameSpace` the caller maps result bboxes against.
5273
+ *
5274
+ * Returns the MISS REASON rather than a bare `null`. This is the only caller
5275
+ * that can carry it anywhere useful — the cap's return shape has no room for
5276
+ * it, and the detail plane's drop line is where an operator reads why an
5277
+ * enrichment dispatch produced nothing.
5014
5278
  */
5015
5279
  async getNativeCropForDetail(_deviceId, handle, paddedBbox) {
5016
- const native = await this.getNativeCrop({
5280
+ const resolved = await this.resolveNativeCropPixels({
5017
5281
  handle,
5018
- bbox: paddedBbox,
5019
- encodeJpeg: true
5282
+ bbox: paddedBbox
5020
5283
  });
5021
- if (!native?.jpeg) return null;
5284
+ if (resolved.pixels === null || resolved.tier !== "native") return {
5285
+ miss: resolved.missReason ?? "worker-lease-gone",
5286
+ handleAgeMs: resolved.handleAgeMs
5287
+ };
5288
+ const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
5289
+ if (!encoded?.jpeg) return {
5290
+ miss: "jpeg-encode-failed",
5291
+ handleAgeMs: resolved.handleAgeMs
5292
+ };
5022
5293
  return {
5023
- jpeg: Buffer.from(native.jpeg, "base64"),
5024
- width: native.width,
5025
- height: native.height,
5294
+ jpeg: Buffer.from(encoded.jpeg, "base64"),
5295
+ width: encoded.width,
5296
+ height: encoded.height,
5026
5297
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
5027
5298
  };
5028
5299
  }
@@ -5247,6 +5518,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5247
5518
  * Kept as a method so the two call sites need no change.
5248
5519
  */
5249
5520
  async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
5521
+ await this.refreshNativeLeaseSettings();
5250
5522
  return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
5251
5523
  deviceId,
5252
5524
  streamId,
@@ -5288,8 +5560,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5288
5560
  */
5289
5561
  enqueueSharedDetectionFrame(deviceId, frame, handle) {
5290
5562
  const runner = this.runner;
5291
- if (!runner) return;
5292
- runner.enqueueDetectionFrame(deviceId, frame, handle);
5563
+ if (!runner) return false;
5564
+ const admitted = runner.enqueueDetectionFrame(deviceId, frame, handle);
5293
5565
  const sink = this.sharedMotionSinks.get(deviceId);
5294
5566
  if (sink) {
5295
5567
  const now = this.sharedSinkNow();
@@ -5298,6 +5570,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5298
5570
  runner.enqueueMotionFrame(deviceId, rgbFrameToGrayFrame(frame), void 0);
5299
5571
  }
5300
5572
  }
5573
+ return admitted;
5301
5574
  }
5302
5575
  /**
5303
5576
  * Epic C P1 detection path for a flagged camera: decode `rgb` in a
@@ -5353,7 +5626,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5353
5626
  codec: source.codec
5354
5627
  } });
5355
5628
  return startSessionDecodePump({
5356
- fork: forkDecodeWorker,
5629
+ fork: this.forkDecodeWorkerWithLease,
5357
5630
  logger: log,
5358
5631
  source,
5359
5632
  opts: {
@@ -5365,7 +5638,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5365
5638
  onDecodedFrame: (frame, nativeCrop) => {
5366
5639
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
5367
5640
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
5368
- this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
5641
+ if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle)) nativeCrop?.markInferred();
5369
5642
  }
5370
5643
  });
5371
5644
  }
@@ -5398,7 +5671,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5398
5671
  codec: source.codec
5399
5672
  } });
5400
5673
  return startSessionDecodePump({
5401
- fork: forkDecodeWorker,
5674
+ fork: this.forkDecodeWorkerWithLease,
5402
5675
  logger: log,
5403
5676
  source,
5404
5677
  opts: {
@@ -5870,7 +6143,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5870
6143
  logger: log,
5871
6144
  acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
5872
6145
  startPump: (source) => startSessionDecodePump({
5873
- fork: forkDecodeWorker,
6146
+ fork: this.forkDecodeWorkerWithLease,
5874
6147
  logger: log,
5875
6148
  source,
5876
6149
  opts: {
@@ -5883,7 +6156,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5883
6156
  if (settled) return;
5884
6157
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
5885
6158
  if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
5886
- runner.enqueueOccupancyFrame(deviceId, frame, handle);
6159
+ if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
5887
6160
  count++;
5888
6161
  if (count >= frames) {
5889
6162
  clearTimeout(safetyTimer);