@camstack/addon-pipeline 1.2.48 → 1.2.50

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 (34) hide show
  1. package/dist/{model-download-service-D8B-4ktF-7-YxE9Wx.js → addon-utils-DveoBR6G.js} +219 -3
  2. package/dist/{model-download-service-D8B-4ktF-BB7oZL3y.mjs → addon-utils-QuxtOdK1.mjs} +219 -3
  3. package/dist/audio-analyzer/index.js +4 -4
  4. package/dist/audio-analyzer/index.mjs +2 -2
  5. package/dist/detection-pipeline/index.js +9 -9
  6. package/dist/detection-pipeline/index.mjs +3 -3
  7. package/dist/{dist-CksKE1Sn.js → dist-Ci2-N-XC.js} +76 -2
  8. package/dist/{dist-DoTw0NM3.mjs → dist-hRt5QB5i.mjs} +76 -2
  9. package/dist/{event-loop-stall-monitor-DzdwBr4X.js → event-loop-stall-monitor-B_EOn2Q1.js} +1 -1
  10. package/dist/{event-loop-stall-monitor-CtktCf3u.mjs → event-loop-stall-monitor-DUSB3ymZ.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +127 -114
  14. package/dist/pipeline-runner/index.mjs +127 -114
  15. package/dist/recorder/index.js +5 -5
  16. package/dist/recorder/index.mjs +2 -2
  17. package/dist/session-decode/decode-worker-child.js +57 -13
  18. package/dist/session-decode/decode-worker-child.mjs +57 -13
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BNISghj9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DwYxOvHx.mjs} +1 -1
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FL2mrnl7.mjs +26 -0
  22. package/dist/stream-broker/{hostInit-DqD1qq8w.mjs → hostInit-yLEcTDS8.mjs} +1 -1
  23. package/dist/stream-broker/index.js +3 -3
  24. package/dist/stream-broker/index.mjs +2 -2
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/dist/{worker-protocol-CuEcTi66.js → worker-protocol-CxpdncMB.js} +1 -1
  27. package/dist/{worker-protocol-4_4L1QOe.mjs → worker-protocol-D_6eAFbv.mjs} +1 -1
  28. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-EcWZ5m8e.js → MaskShapeCanvas-DI4BY7W2-kAtChdz2.js} +1 -1
  29. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CfiyCD3z.js → MotionZonesSettings-NcxxQN8r-DEwkvjzt.js} +1 -1
  30. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ESPy4XXk.js → PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js} +1 -1
  31. package/embed-dist/assets/{index-C3FKV1x-.js → index-Dv7fSuDA.js} +12 -12
  32. package/embed-dist/index.html +1 -1
  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-Cj2xMupA.mjs +0 -26
@@ -18385,6 +18385,23 @@ var CameraRecordingStatusSchema = object({
18385
18385
  active: boolean(),
18386
18386
  storageBytes: number()
18387
18387
  });
18388
+ /** One stage of the fan-out that could NOT be read, and how long it cost. */
18389
+ var CameraStatusDegradationSchema = object({
18390
+ stage: _enum([
18391
+ "source",
18392
+ "broker",
18393
+ "detection",
18394
+ "recording",
18395
+ "switches"
18396
+ ]),
18397
+ reason: _enum([
18398
+ "timeout",
18399
+ "error",
18400
+ "partial"
18401
+ ]),
18402
+ /** Wall-clock ms spent on the stage before it was abandoned. */
18403
+ elapsedMs: number()
18404
+ });
18388
18405
  /**
18389
18406
  * Aggregated per-camera pipeline status — server-composed, single call.
18390
18407
  *
@@ -18415,9 +18432,28 @@ var CameraStatusSchema = object({
18415
18432
  * differently — a quiet camera that looks identical to a dead one is the
18416
18433
  * silence-reads-as-never-happened trap this repo keeps paying for.
18417
18434
  *
18418
- * Empty when nothing is off. Never contains a switch no provider offers.
18435
+ * Empty when nothing is off, and never contains a switch no provider offers
18436
+ * — but an empty list is only a POSITIVE claim when `degraded` does not name
18437
+ * `'switches'`. When it does, the switch set could not be read and nothing
18438
+ * here may be rendered as "the operator turned nothing off": that is the
18439
+ * D62 failure (a camera we could not read painted as broken) in the very
18440
+ * field that exists to prevent it.
18419
18441
  */
18420
18442
  switchedOff: array(CameraSwitchIdSchema).readonly(),
18443
+ /**
18444
+ * Stages of the bounded fan-out that were CUT SHORT — a timeout or a
18445
+ * rejection — and whose block is therefore `null` because we could not
18446
+ * READ it, not because there is nothing there.
18447
+ *
18448
+ * Without this, three different facts arrive as the same `null`: "the stage
18449
+ * timed out", "the stage failed", and "this camera legitimately has no
18450
+ * decoder / no recording". Every surface that draws a conclusion from a null
18451
+ * block (or from an empty `switchedOff`) must consult this first; a stage
18452
+ * named here supports no conclusion at all, only "unknown".
18453
+ *
18454
+ * Empty on a clean read — the overwhelmingly common case.
18455
+ */
18456
+ degraded: array(CameraStatusDegradationSchema).readonly(),
18421
18457
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
18422
18458
  fetchedAt: number()
18423
18459
  });
@@ -21137,7 +21173,29 @@ var FaceInfoSchema = object({
21137
21173
  recognizedIdentityId: string().optional(),
21138
21174
  identityName: string().optional(),
21139
21175
  assigned: boolean(),
21176
+ /**
21177
+ * The crop, inline, base64.
21178
+ *
21179
+ * **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
21180
+ * field alone is ~2.87 MiB, re-sent in full on every operator assign and
21181
+ * every 30 s poll, base64-inflated over the msgpack socket and held in the
21182
+ * query heap. It stays for callers that have not migrated; `includeCrops:
21183
+ * false` turns it off once they have.
21184
+ */
21140
21185
  base64: string().optional(),
21186
+ /**
21187
+ * Same crop, as a data-plane URL for `<img src>` — the move the admin
21188
+ * snapshot surfaces made on 2026-08-08.
21189
+ *
21190
+ * Served by the `event-media` plane, which resolves a raw MediaStore key and
21191
+ * is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
21192
+ * cookie, so no header plumbing is needed. The bytes then ride the browser's
21193
+ * HTTP cache with an ETag and `immutable`, instead of the WebSocket.
21194
+ *
21195
+ * Absent when the face has no stored crop, or when the addon has no data
21196
+ * plane — callers fall back to {@link base64}.
21197
+ */
21198
+ cropUrl: string().optional(),
21141
21199
  /** Design B: the face bbox (pixel space) on the key frame — lets a detail
21142
21200
  * view draw the box over the native `keyFrameMediaKey` frame. Absent on
21143
21201
  * legacy rows written before design B. */
@@ -21202,7 +21260,23 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
21202
21260
  auth: "admin"
21203
21261
  }), method(object({
21204
21262
  limit: number().int().positive().optional(),
21205
- filter: FaceFilterEnum.optional()
21263
+ filter: FaceFilterEnum.optional(),
21264
+ /**
21265
+ * Inline the base64 crop on every row. Default `true` — the existing
21266
+ * behaviour, kept so no caller breaks.
21267
+ *
21268
+ * Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
21269
+ * drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
21270
+ * the browser cache the images.
21271
+ *
21272
+ * **This is an INPUT field, so it does not reach the addon until the
21273
+ * next train.** The hub router validates cap inputs against its own
21274
+ * compiled Zod, which strips a key it does not know — verified today
21275
+ * on the OUTPUT side, where an additive field DOES arrive immediately
21276
+ * (`Track.hasFace`). Until the train ships, sending `false` is
21277
+ * harmless and simply keeps the crops inline.
21278
+ */
21279
+ includeCrops: boolean().optional()
21206
21280
  }).optional(), array(FaceInfoSchema).readonly()), method(object({
21207
21281
  deviceId: number().int(),
21208
21282
  trackId: string()
@@ -18385,6 +18385,23 @@ var CameraRecordingStatusSchema = object({
18385
18385
  active: boolean(),
18386
18386
  storageBytes: number()
18387
18387
  });
18388
+ /** One stage of the fan-out that could NOT be read, and how long it cost. */
18389
+ var CameraStatusDegradationSchema = object({
18390
+ stage: _enum([
18391
+ "source",
18392
+ "broker",
18393
+ "detection",
18394
+ "recording",
18395
+ "switches"
18396
+ ]),
18397
+ reason: _enum([
18398
+ "timeout",
18399
+ "error",
18400
+ "partial"
18401
+ ]),
18402
+ /** Wall-clock ms spent on the stage before it was abandoned. */
18403
+ elapsedMs: number()
18404
+ });
18388
18405
  /**
18389
18406
  * Aggregated per-camera pipeline status — server-composed, single call.
18390
18407
  *
@@ -18415,9 +18432,28 @@ var CameraStatusSchema = object({
18415
18432
  * differently — a quiet camera that looks identical to a dead one is the
18416
18433
  * silence-reads-as-never-happened trap this repo keeps paying for.
18417
18434
  *
18418
- * Empty when nothing is off. Never contains a switch no provider offers.
18435
+ * Empty when nothing is off, and never contains a switch no provider offers
18436
+ * — but an empty list is only a POSITIVE claim when `degraded` does not name
18437
+ * `'switches'`. When it does, the switch set could not be read and nothing
18438
+ * here may be rendered as "the operator turned nothing off": that is the
18439
+ * D62 failure (a camera we could not read painted as broken) in the very
18440
+ * field that exists to prevent it.
18419
18441
  */
18420
18442
  switchedOff: array(CameraSwitchIdSchema).readonly(),
18443
+ /**
18444
+ * Stages of the bounded fan-out that were CUT SHORT — a timeout or a
18445
+ * rejection — and whose block is therefore `null` because we could not
18446
+ * READ it, not because there is nothing there.
18447
+ *
18448
+ * Without this, three different facts arrive as the same `null`: "the stage
18449
+ * timed out", "the stage failed", and "this camera legitimately has no
18450
+ * decoder / no recording". Every surface that draws a conclusion from a null
18451
+ * block (or from an empty `switchedOff`) must consult this first; a stage
18452
+ * named here supports no conclusion at all, only "unknown".
18453
+ *
18454
+ * Empty on a clean read — the overwhelmingly common case.
18455
+ */
18456
+ degraded: array(CameraStatusDegradationSchema).readonly(),
18421
18457
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
18422
18458
  fetchedAt: number()
18423
18459
  });
@@ -21137,7 +21173,29 @@ var FaceInfoSchema = object({
21137
21173
  recognizedIdentityId: string().optional(),
21138
21174
  identityName: string().optional(),
21139
21175
  assigned: boolean(),
21176
+ /**
21177
+ * The crop, inline, base64.
21178
+ *
21179
+ * **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
21180
+ * field alone is ~2.87 MiB, re-sent in full on every operator assign and
21181
+ * every 30 s poll, base64-inflated over the msgpack socket and held in the
21182
+ * query heap. It stays for callers that have not migrated; `includeCrops:
21183
+ * false` turns it off once they have.
21184
+ */
21140
21185
  base64: string().optional(),
21186
+ /**
21187
+ * Same crop, as a data-plane URL for `<img src>` — the move the admin
21188
+ * snapshot surfaces made on 2026-08-08.
21189
+ *
21190
+ * Served by the `event-media` plane, which resolves a raw MediaStore key and
21191
+ * is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
21192
+ * cookie, so no header plumbing is needed. The bytes then ride the browser's
21193
+ * HTTP cache with an ETag and `immutable`, instead of the WebSocket.
21194
+ *
21195
+ * Absent when the face has no stored crop, or when the addon has no data
21196
+ * plane — callers fall back to {@link base64}.
21197
+ */
21198
+ cropUrl: string().optional(),
21141
21199
  /** Design B: the face bbox (pixel space) on the key frame — lets a detail
21142
21200
  * view draw the box over the native `keyFrameMediaKey` frame. Absent on
21143
21201
  * legacy rows written before design B. */
@@ -21202,7 +21260,23 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
21202
21260
  auth: "admin"
21203
21261
  }), method(object({
21204
21262
  limit: number().int().positive().optional(),
21205
- filter: FaceFilterEnum.optional()
21263
+ filter: FaceFilterEnum.optional(),
21264
+ /**
21265
+ * Inline the base64 crop on every row. Default `true` — the existing
21266
+ * behaviour, kept so no caller breaks.
21267
+ *
21268
+ * Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
21269
+ * drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
21270
+ * the browser cache the images.
21271
+ *
21272
+ * **This is an INPUT field, so it does not reach the addon until the
21273
+ * next train.** The hub router validates cap inputs against its own
21274
+ * compiled Zod, which strips a key it does not know — verified today
21275
+ * on the OUTPUT side, where an additive field DOES arrive immediately
21276
+ * (`Track.hasFace`). Until the train ships, sending `false` is
21277
+ * harmless and simply keeps the crops inline.
21278
+ */
21279
+ includeCrops: boolean().optional()
21206
21280
  }).optional(), array(FaceInfoSchema).readonly()), method(object({
21207
21281
  deviceId: number().int(),
21208
21282
  trackId: string()
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-CksKE1Sn.js");
1
+ const require_dist = require("./dist-Ci2-N-XC.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- import { L as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-DoTw0NM3.mjs";
1
+ import { L as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-hRt5QB5i.mjs";
2
2
  import { PerformanceObserver } from "node:perf_hooks";
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CksKE1Sn.js");
5
+ const require_dist = require("../dist-Ci2-N-XC.js");
6
6
  const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { B as motionDetectionCapability, I as evaluateZoneRules, at as BaseAddon, ct as DeviceType, ut as hydrateSchema } from "../dist-DoTw0NM3.mjs";
1
+ import { B as motionDetectionCapability, I as evaluateZoneRules, at as BaseAddon, ct as DeviceType, ut as hydrateSchema } from "../dist-hRt5QB5i.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } 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-CksKE1Sn.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DzdwBr4X.js");
8
- const require_worker_protocol = require("../worker-protocol-CuEcTi66.js");
6
+ const require_dist = require("../dist-Ci2-N-XC.js");
7
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-B_EOn2Q1.js");
8
+ const require_worker_protocol = require("../worker-protocol-CxpdncMB.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");
@@ -1824,116 +1824,6 @@ var CropConventionSource = class {
1824
1824
  }
1825
1825
  };
1826
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
1937
1827
  //#region src/pipeline-runner/detail-subtree.ts
1938
1828
  /** Did the native-lease fetch report a miss (rather than return pixels)? */
1939
1829
  function isNativeCropMiss(fetch) {
@@ -2508,6 +2398,116 @@ function resolveNativeCropSource(isFullFrame, nativeHit) {
2508
2398
  return isFullFrame ? "ram-fullframe" : "miss";
2509
2399
  }
2510
2400
  //#endregion
2401
+ //#region src/pipeline-runner/native-lease-settings-source.ts
2402
+ /**
2403
+ * Where the runner gets the cluster-wide native-frame LEASE knobs.
2404
+ *
2405
+ * ## Why a settings read and not the attach config
2406
+ *
2407
+ * Identical reasoning to `crop-convention-source.ts`, and worth restating
2408
+ * because the temptation is the same: `RunnerCameraConfig` is the obvious
2409
+ * orchestrator→runner channel and it is the wrong one. Eleven call sites build
2410
+ * one; a cluster-wide constant threaded through eleven builders eventually
2411
+ * reaches ten of them, and the cameras attached by the forgotten builder just
2412
+ * quietly run a different lease window. Instead the runner reads the OWNER
2413
+ * addon's global settings directly, hub-routed by `AddonCallGateway`, so an
2414
+ * agent runner transparently reads the hub-central store with no placement
2415
+ * branching.
2416
+ *
2417
+ * ## Deliberately NOT node-scoped
2418
+ *
2419
+ * The read passes no `nodeId`. With one, `addon-settings` would project that
2420
+ * node's scoped value and two nodes could hold different lease windows for the
2421
+ * same camera — so whether a stored crop is native pixels or the ≤640 fallback
2422
+ * would depend on where the balancer happened to place it, which is invisible
2423
+ * from the stored media.
2424
+ *
2425
+ * ## What it hands out
2426
+ *
2427
+ * A {@link NativeLeaseSettingsOverride} — only the knobs the operator SET. The
2428
+ * decode worker resolves the rest (env var, then the shipped default); see
2429
+ * `session-decode/native-lease-config.ts` for the precedence. Handing out a
2430
+ * fully-resolved triple here would erase the "operator set nothing" state and
2431
+ * with it the env-var escape hatch.
2432
+ */
2433
+ /**
2434
+ * The addon whose GLOBAL settings own the knobs. `pipeline-orchestrator` is
2435
+ * hub-resident and neutral — the same owner as the detail-crop convention and
2436
+ * the per-node decoder-backend selector.
2437
+ */
2438
+ var NATIVE_LEASE_OWNER_ADDON_ID = "pipeline-orchestrator";
2439
+ /**
2440
+ * Whether two overrides name the same knobs with the same values.
2441
+ *
2442
+ * **Every knob has to appear here.** This is the equality that decides whether
2443
+ * a re-read is ADOPTED at all: a knob missing from the comparison is read from
2444
+ * the hub, judged identical, and discarded, so the operator's change never
2445
+ * reaches a decode worker and nothing anywhere says so. `admission` shipped
2446
+ * missing from this list and did exactly that — the setting was stored, the
2447
+ * hub served it, the runner fetched it, and every worker kept spawning with the
2448
+ * old value.
2449
+ */
2450
+ function sameOverride(a, b) {
2451
+ return a.ttlMs === b.ttlMs && a.budgetMb === b.budgetMb && a.activityMs === b.activityMs && a.admission === b.admission;
2452
+ }
2453
+ /**
2454
+ * TTL-cached read of the cluster native-lease knobs.
2455
+ *
2456
+ * {@link current} is synchronous because the fork that consumes it is;
2457
+ * {@link refresh} is what talks to the hub, and the runner awaits it on the
2458
+ * path that precedes a decode-worker spawn.
2459
+ */
2460
+ var NativeLeaseSettingsSource = class {
2461
+ override = {};
2462
+ lastReadAtMs = Number.NEGATIVE_INFINITY;
2463
+ inFlight = null;
2464
+ logger;
2465
+ now;
2466
+ ttlMs;
2467
+ constructor(options) {
2468
+ this.logger = options.logger;
2469
+ this.now = options.now ?? (() => Date.now());
2470
+ this.ttlMs = options.ttlMs ?? 3e4;
2471
+ }
2472
+ /** The operator's overrides as last read — `{}` until a read lands. */
2473
+ current() {
2474
+ return this.override;
2475
+ }
2476
+ /**
2477
+ * Re-read if the cached value has expired. Concurrent callers share one
2478
+ * in-flight read; a failure keeps the LAST KNOWN value rather than reverting
2479
+ * to `{}`, because a revert would silently hand the next-spawned workers a
2480
+ * different RAM profile than the ones already running.
2481
+ */
2482
+ async refresh(api) {
2483
+ if (api === void 0) return this.override;
2484
+ if (this.now() - this.lastReadAtMs < this.ttlMs) return this.override;
2485
+ this.inFlight ??= this.read(api).finally(() => {
2486
+ this.inFlight = null;
2487
+ });
2488
+ await this.inFlight;
2489
+ return this.override;
2490
+ }
2491
+ async read(api) {
2492
+ try {
2493
+ const next = require_dist.pickNativeLeaseOverride(await api.addonSettings.getGlobalSettings.query({ addonId: NATIVE_LEASE_OWNER_ADDON_ID }));
2494
+ this.lastReadAtMs = this.now();
2495
+ if (sameOverride(next, this.override)) return;
2496
+ this.logger.info("native lease settings changed — applies to NEW decode sessions", { meta: {
2497
+ from: { ...this.override },
2498
+ to: { ...next }
2499
+ } });
2500
+ this.override = next;
2501
+ } catch (err) {
2502
+ this.logger.warn("native lease settings read failed — keeping the last known value", { meta: {
2503
+ owner: NATIVE_LEASE_OWNER_ADDON_ID,
2504
+ inUse: { ...this.override },
2505
+ error: err instanceof Error ? err.message : String(err)
2506
+ } });
2507
+ }
2508
+ }
2509
+ };
2510
+ //#endregion
2511
2511
  //#region src/pipeline-runner/frame-governor.ts
2512
2512
  var FrameGovernor = class {
2513
2513
  config;
@@ -4986,7 +4986,20 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4986
4986
  if (resolved.tier === "native" && resolved.pixels !== null) return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
4987
4987
  if (resolved.tier !== "ram-fullframe") return null;
4988
4988
  const crop = this.retainedFrames?.crop(input.handle, input.bbox, input.maxWidth);
4989
- if (!crop) return null;
4989
+ if (!crop) {
4990
+ this.ctx?.logger.warn("native full-frame miss — the retained store no longer holds this frame", {
4991
+ tags: { deviceId: this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
4992
+ meta: {
4993
+ handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
4994
+ handleNodeId: input.handle.nodeId,
4995
+ nativeMissReason: resolved.missReason,
4996
+ nativeHandleAgeMs: resolved.handleAgeMs,
4997
+ maxWidth: input.maxWidth ?? null,
4998
+ hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
4999
+ }
5000
+ });
5001
+ return null;
5002
+ }
4990
5003
  return this.encodeNativeCropResult({
4991
5004
  bytes: crop.bytes,
4992
5005
  width: crop.width,