@camstack/addon-pipeline 1.2.70 → 1.2.72

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 (29) 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 +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-BPaa4_z6.mjs → dist-CNYUiN27.mjs} +158 -43
  6. package/dist/{dist-cR-pvD_z.js → dist-DwBdGpkx.js} +158 -43
  7. package/dist/{event-loop-stall-monitor-STRdnQWm.js → event-loop-stall-monitor-1o85rEbu.js} +1 -1
  8. package/dist/{event-loop-stall-monitor-B79REtL2.mjs → event-loop-stall-monitor-EvJwrf2f.mjs} +1 -1
  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 +95 -28
  12. package/dist/pipeline-runner/index.mjs +95 -28
  13. package/dist/recorder/index.js +365 -73
  14. package/dist/recorder/index.mjs +365 -73
  15. package/dist/session-decode/decode-worker-child.js +483 -52
  16. package/dist/session-decode/decode-worker-child.mjs +483 -52
  17. package/dist/stream-broker/_stub.js +660 -556
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B-HGBNad.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BEBWZAKQ.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DkUf57A0.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-CExO0piw.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-B12DHOv7.mjs → hostInit-aSIXKJAL.mjs} +3 -3
  22. package/dist/stream-broker/index.js +1 -1
  23. package/dist/stream-broker/index.mjs +1 -1
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/{worker-protocol-CBO8nwQj.mjs → worker-protocol-C_sGeitR.mjs} +26 -19
  26. package/dist/{worker-protocol-C7V1qIIA.js → worker-protocol-D0H-aG2_.js} +26 -19
  27. package/package.json +1 -1
  28. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1CqbPrT.mjs +0 -26
  29. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-NjvjJWz-.mjs +0 -26
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../chunk-emK7D4bc.js");
3
- const require_worker_protocol = require("../worker-protocol-C7V1qIIA.js");
3
+ const require_worker_protocol = require("../worker-protocol-D0H-aG2_.js");
4
+ const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
4
5
  let node_url = require("node:url");
5
6
  let node_fs = require("node:fs");
6
7
  node_fs = require_chunk.__toESM(node_fs);
@@ -539,20 +540,31 @@ var NativeFrameRing = class {
539
540
  //#endregion
540
541
  //#region src/session-decode/native-lease-store.ts
541
542
  /**
542
- * A hard-capped-by-bytes, TTL-and-release map from worker frameId → retained
543
- * RAM frame. Insertion-ordered (`Map`), so budget eviction is FIFO by age.
543
+ * A count- and byte-bounded, release-driven map from worker frameId → held RAM
544
+ * frame. Insertion-ordered (`Map`), so every eviction is FIFO by age.
544
545
  */
545
546
  var NativeLeaseStore = class {
546
547
  entries = /* @__PURE__ */ new Map();
547
548
  budgetBytes;
548
- ttlMs;
549
+ maxFrames;
549
550
  now;
550
551
  bytes = 0;
552
+ /**
553
+ * Frames dropped because the hold bound or the byte ceiling was reached
554
+ * before their release arrived. Non-zero means the runner is not keeping up —
555
+ * a different sentence from "the crop was late", and the reason this is a
556
+ * counter and not a silent eviction.
557
+ */
558
+ overflowed = 0;
551
559
  constructor(options) {
552
560
  this.budgetBytes = options.budgetBytes;
553
- this.ttlMs = options.ttlMs;
561
+ this.maxFrames = Math.max(1, options.maxFrames);
554
562
  this.now = options.now ?? Date.now;
555
563
  }
564
+ /** Frames dropped by a bound rather than released (tests / metrics). */
565
+ get overflowCount() {
566
+ return this.overflowed;
567
+ }
556
568
  /** `false` when the store is disabled (`budgetBytes <= 0`). */
557
569
  get enabled() {
558
570
  return this.budgetBytes > 0;
@@ -578,43 +590,42 @@ var NativeLeaseStore = class {
578
590
  return false;
579
591
  }
580
592
  this.drop(frameId);
581
- this.sweepExpired();
582
593
  this.entries.set(frameId, {
583
594
  frame,
584
595
  insertedAt: this.now()
585
596
  });
586
597
  this.bytes += frame.byteLength;
587
- while (this.bytes > this.budgetBytes && this.entries.size > 0) {
598
+ while ((this.entries.size > this.maxFrames || this.bytes > this.budgetBytes) && this.entries.size > 0) {
588
599
  const oldest = this.entries.keys().next().value;
589
600
  if (oldest === void 0) break;
601
+ if (oldest === frameId) break;
602
+ this.overflowed += 1;
590
603
  this.drop(oldest);
591
604
  }
592
605
  return true;
593
606
  }
594
607
  /**
595
- * The retained frame for `frameId`, or `null` if never leased, already
596
- * released/evicted, OR aged past its TTL (an expired lease is freed + dropped
597
- * here so a caller that reads it just after expiry does not resurrect it).
608
+ * The held frame for `frameId`, or `null` if never held or already
609
+ * released/evicted. No age check: a frame is held until an EVENT ends the
610
+ * hold, and a store that also expired on a clock would reintroduce the
611
+ * unreachable window this design removed.
598
612
  */
599
613
  get(frameId) {
614
+ return this.entries.get(frameId)?.frame ?? null;
615
+ }
616
+ /** How long `frameId` has been held, or `null` when it is not held. */
617
+ ageMs(frameId) {
600
618
  const entry = this.entries.get(frameId);
601
- if (!entry) return null;
602
- if (this.now() - entry.insertedAt > this.ttlMs) {
603
- this.drop(frameId);
604
- return null;
605
- }
606
- return entry.frame;
619
+ return entry ? this.now() - entry.insertedAt : null;
607
620
  }
608
- /** Free + drop the lease for `frameId` (the prompt release path). Idempotent. */
621
+ /**
622
+ * Free + drop the hold for `frameId` — the runner has this frame's result and
623
+ * has taken what it wanted. Idempotent, and the ORDINARY exit.
624
+ */
609
625
  release(frameId) {
610
626
  this.drop(frameId);
611
627
  }
612
- /** Free + drop every lease older than the TTL. */
613
- sweepExpired() {
614
- const cutoff = this.now() - this.ttlMs;
615
- for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
616
- }
617
- /** Free + drop every retained lease. Idempotent (re-dial + teardown call it). */
628
+ /** Free + drop every held frame. Idempotent (re-dial + teardown call it). */
618
629
  clear() {
619
630
  for (const entry of this.entries.values()) entry.frame.free();
620
631
  this.entries.clear();
@@ -630,6 +641,244 @@ var NativeLeaseStore = class {
630
641
  }
631
642
  };
632
643
  //#endregion
644
+ //#region src/session-decode/native-tile-geometry.ts
645
+ /**
646
+ * Pure geometry for the **native subject tile** — the compressed, subject-sized
647
+ * container that replaces retaining a whole native frame.
648
+ *
649
+ * > **NOT WIRED YET.** This module is Phase 1 of
650
+ * > `docs/design/2026-08-13-native-lease-two-tier-redesign.md` and nothing
651
+ * > imports it. It landed with the design because it is the piece whose failure
652
+ * > is SILENT. If you find it still unimported after the next structural train,
653
+ * > delete it — a helper nobody calls reads as verification of a design that
654
+ * > was never built.
655
+ *
656
+ * ## What a tile is, and what it is emphatically not
657
+ *
658
+ * A tile is a **container**: the detection's box, padded generously, clamped
659
+ * inside the frame and even-aligned, cut from the native surface at the one
660
+ * instant the frame is still alive and kept as compressed bytes long after it
661
+ * is gone.
662
+ *
663
+ * It is NOT a model input and it is NOT a crop convention. The rectangle a
664
+ * model is fed is still derived in exactly one place — `deriveDetailCropRect`,
665
+ * reached only through `pipeline-runner/detail-subtree.ts`'s `cutFromFullFrame`
666
+ * ([D52](../../../../docs/decisions/adr-0052.md)) — and is then cut FROM the
667
+ * tile raster instead of from a frame that no longer exists. Two paddings are
668
+ * therefore never composed: the tile's padding decides only how much scene is
669
+ * kept available, and the convention's padding decides the rectangle, exactly
670
+ * as it does today against a full frame.
671
+ *
672
+ * ## The one rule this file exists to enforce
673
+ *
674
+ * **A tile serves a request only when it strictly CONTAINS the requested
675
+ * rectangle.** Not clamps it, not stretches it, not re-pads it — contains it.
676
+ * A rectangle clamped to fit inside a tile would enrich a *different* part of
677
+ * the scene, land in the same vector index as correctly-cut neighbours, and
678
+ * error nowhere: the exact shape of the defect D52 was written for. A
679
+ * non-contained rectangle is a MISS, and the caller falls to the next rung
680
+ * (re-decode from the recorded GOP).
681
+ *
682
+ * Geometry only: no pixels, no node-av, no process state.
683
+ */
684
+ /**
685
+ * How much scene is kept around the detection box, as a fraction of the box's
686
+ * own width/height, applied on every side.
687
+ *
688
+ * Sized to sit comfortably above the detail-crop convention's own padding
689
+ * (0.15 today, `DEFAULT_DETAIL_CROP_CONVENTION`) so the derived model rectangle
690
+ * lands INSIDE the tile even after a track's box has drifted for a second or
691
+ * two — which is precisely the interval a tile has to survive. Raising it costs
692
+ * bytes quadratically; lowering it converts hits into misses. It is a default,
693
+ * not a convention: nothing about a vector's meaning depends on it, because
694
+ * nothing is ever encoded from the tile's own rectangle.
695
+ */
696
+ var NATIVE_TILE_PADDING_RATIO = .35;
697
+ /**
698
+ * Resolve the tile container for one detection box.
699
+ *
700
+ * The box arrives NORMALISED against the analysis frame; the tile is expressed
701
+ * in NATIVE frame pixels, because that is the raster the worker cuts from. Both
702
+ * are the same scene at different scales, so the normalised box maps directly.
703
+ *
704
+ * Degenerate and out-of-frame inputs are handled by clamping to the frame — a
705
+ * detection touching the edge yields a smaller tile, never a read past the
706
+ * plane. A zero-area box widens to the 2px chroma minimum.
707
+ */
708
+ function resolveNativeTileRegion(frameWidth, frameHeight, bbox, paddingRatio = NATIVE_TILE_PADDING_RATIO) {
709
+ const pad = Number.isFinite(paddingRatio) && paddingRatio > 0 ? paddingRatio : 0;
710
+ const boxLeft = bbox.x * frameWidth;
711
+ const boxTop = bbox.y * frameHeight;
712
+ const boxWidth = bbox.w * frameWidth;
713
+ const boxHeight = bbox.h * frameHeight;
714
+ const padX = boxWidth * pad;
715
+ const padY = boxHeight * pad;
716
+ const rawLeft = Math.max(0, boxLeft - padX);
717
+ const rawTop = Math.max(0, boxTop - padY);
718
+ const rawRight = Math.min(frameWidth, boxLeft + boxWidth + padX);
719
+ const rawBottom = Math.min(frameHeight, boxTop + boxHeight + padY);
720
+ const left = clampEven(Math.floor(rawLeft), 0, Math.max(0, frameWidth - 2));
721
+ const top = clampEven(Math.floor(rawTop), 0, Math.max(0, frameHeight - 2));
722
+ return {
723
+ region: {
724
+ left,
725
+ top,
726
+ width: clampEven(Math.ceil(rawRight) - left, 2, frameWidth - left),
727
+ height: clampEven(Math.ceil(rawBottom) - top, 2, frameHeight - top)
728
+ },
729
+ paddingRatio: pad
730
+ };
731
+ }
732
+ /**
733
+ * Whether `rect` lies wholly inside `tile` — the serve gate.
734
+ *
735
+ * Deliberately strict and deliberately boolean: there is no "close enough"
736
+ * verdict, because the caller's only two honest options are to cut the exact
737
+ * rectangle or to report a miss. Touching the tile's edge counts as inside; a
738
+ * single pixel past it does not.
739
+ */
740
+ function tileContains(tile, rect) {
741
+ if (rect.width <= 0 || rect.height <= 0) return false;
742
+ return rect.left >= tile.left && rect.top >= tile.top && rect.left + rect.width <= tile.left + tile.width && rect.top + rect.height <= tile.top + tile.height;
743
+ }
744
+ /**
745
+ * Translate a FRAME-space rectangle into TILE-space, or `null` when the tile
746
+ * does not contain it.
747
+ *
748
+ * `null` is the miss the caller reports; it must never be turned into a clamp.
749
+ * The returned rectangle is a pure translation — no scaling, no re-alignment —
750
+ * so a caller that cut `rect` from the full frame and a caller that cuts the
751
+ * translated rectangle from the tile obtain the same pixels.
752
+ */
753
+ function rectWithinTile(tile, rect) {
754
+ if (!tileContains(tile, rect)) return null;
755
+ return {
756
+ left: rect.left - tile.left,
757
+ top: rect.top - tile.top,
758
+ width: rect.width,
759
+ height: rect.height
760
+ };
761
+ }
762
+ /**
763
+ * Pick the tile that can serve `rect`, or `null` when none contains it.
764
+ *
765
+ * Smallest containing tile wins: a tighter container means less scene around
766
+ * the subject and therefore a smaller decode, and among tiles that all satisfy
767
+ * the containment rule the choice cannot affect the pixels — every one of them
768
+ * would yield the same rectangle of the same frame.
769
+ *
770
+ * `null` is a MISS the caller reports and falls through on. It must never
771
+ * become "use the closest tile and clamp": see this module's header.
772
+ */
773
+ function selectTileForRect(tiles, rect) {
774
+ let best = null;
775
+ let bestArea = Number.POSITIVE_INFINITY;
776
+ for (const tile of tiles) {
777
+ const local = rectWithinTile(tile.region, rect);
778
+ if (local === null) continue;
779
+ const area = tile.region.width * tile.region.height;
780
+ if (area >= bestArea) continue;
781
+ bestArea = area;
782
+ best = {
783
+ tile,
784
+ local
785
+ };
786
+ }
787
+ return best;
788
+ }
789
+ //#endregion
790
+ //#region src/session-decode/native-tile-store.ts
791
+ /**
792
+ * A byte-bounded, TTL'd map from worker frameId → the subject tiles cut from
793
+ * that frame. Insertion-ordered (`Map`), so budget eviction is FIFO by age.
794
+ */
795
+ var NativeTileStore = class {
796
+ entries = /* @__PURE__ */ new Map();
797
+ budgetBytes;
798
+ ttlMs;
799
+ now;
800
+ bytes = 0;
801
+ constructor(options) {
802
+ this.budgetBytes = options.budgetBytes;
803
+ this.ttlMs = options.ttlMs;
804
+ this.now = options.now ?? Date.now;
805
+ }
806
+ /** `false` when tiles are disabled (`budgetBytes <= 0`). */
807
+ get enabled() {
808
+ return this.budgetBytes > 0;
809
+ }
810
+ /** Number of frames with retained tiles (tests / metrics). */
811
+ get frameCount() {
812
+ return this.entries.size;
813
+ }
814
+ /** Total retained tiles across every frame (tests / metrics). */
815
+ get tileCount() {
816
+ let total = 0;
817
+ for (const entry of this.entries.values()) total += entry.tiles.length;
818
+ return total;
819
+ }
820
+ /** Total resident bytes (tests / metrics). */
821
+ get totalBytes() {
822
+ return this.bytes;
823
+ }
824
+ /**
825
+ * Retain `tiles` for `frameId`. Returns `false` when the store is disabled —
826
+ * the caller then simply has nothing to fall back on, exactly as before this
827
+ * store existed. Sweeps expired entries first, then evicts oldest-first while
828
+ * over budget; the entry just admitted is never the one evicted.
829
+ */
830
+ put(frameId, tiles) {
831
+ if (!this.enabled || tiles.length === 0) return false;
832
+ this.drop(frameId);
833
+ this.sweepExpired();
834
+ let size = 0;
835
+ for (const tile of tiles) size += tile.bytes.byteLength;
836
+ this.entries.set(frameId, {
837
+ tiles,
838
+ bytes: size,
839
+ insertedAt: this.now()
840
+ });
841
+ this.bytes += size;
842
+ while (this.bytes > this.budgetBytes && this.entries.size > 1) {
843
+ const oldest = this.entries.keys().next().value;
844
+ if (oldest === void 0 || oldest === frameId) break;
845
+ this.drop(oldest);
846
+ }
847
+ return true;
848
+ }
849
+ /**
850
+ * The tiles retained for `frameId`, or an empty array when there are none or
851
+ * they have aged out. An expired entry is dropped here so a caller reading
852
+ * just after expiry cannot resurrect it.
853
+ */
854
+ get(frameId) {
855
+ const entry = this.entries.get(frameId);
856
+ if (!entry) return [];
857
+ if (this.now() - entry.insertedAt > this.ttlMs) {
858
+ this.drop(frameId);
859
+ return [];
860
+ }
861
+ return entry.tiles;
862
+ }
863
+ /** Drop every entry older than the TTL. */
864
+ sweepExpired() {
865
+ const cutoff = this.now() - this.ttlMs;
866
+ for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
867
+ }
868
+ /** Drop everything. Idempotent (re-dial + teardown call it). */
869
+ clear() {
870
+ this.entries.clear();
871
+ this.bytes = 0;
872
+ }
873
+ /** Remove one frame's tiles, keeping the byte tally exact. */
874
+ drop(frameId) {
875
+ const entry = this.entries.get(frameId);
876
+ if (!entry) return;
877
+ this.entries.delete(frameId);
878
+ this.bytes -= entry.bytes;
879
+ }
880
+ };
881
+ //#endregion
633
882
  //#region src/session-decode/pinned-rgb-crop.ts
634
883
  /**
635
884
  * Pure decision/plumbing helpers for the `pinnedRgbCrop` native-crop color fix
@@ -849,30 +1098,48 @@ var NATIVE_RING_CAP = (() => {
849
1098
  */
850
1099
  var NATIVE_LEASE_KNOBS = require_worker_protocol.resolveNativeLeaseKnobs(process.env);
851
1100
  /**
852
- * Hard RAM budget (bytes) for the native-frame LEASE store — the primary
853
- * native-crop survival window. Unlike {@link NATIVE_RING_CAP} (GPU surfaces,
854
- * leak-prone tiny), a lease is a downloaded RAM copy, so the window is sized
855
- * by memory, not a 2-frame count, and the late cross-process crop reliably
856
- * hits. The primary eviction is the {@link NATIVE_LEASE_TTL_MS} TTL — a 64MB
857
- * budget held only ~5 native 4K frames (~0.2s), which the late crop still
858
- * outran; the budget is a HIGH safety ceiling (default 1024MB) so the TTL is
859
- * the effective cap and the ~40-200ms crop reliably lands within it. `0`
860
- * DISABLES the lease and falls back to the {@link NATIVE_RING_CAP} GPU ring.
1101
+ * Hard RAM budget (bytes) for the native-frame HOLD store — the OOM ceiling,
1102
+ * and since 2026-08-13 nothing else. {@link NATIVE_LEASE_HOLD_FRAMES} is what
1103
+ * decides how much is held; this is the number above which something is wrong.
1104
+ * `0` DISABLES the hold and falls back to the {@link NATIVE_RING_CAP} GPU ring.
861
1105
  */
862
1106
  var NATIVE_LEASE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.budgetMb * 1024 * 1024;
863
1107
  /**
864
- * TTL (ms) after which a native-frame lease is treated as a miss and reclaimed.
865
- * The backstop that bounds in-flight RAM even if the explicit `releaseNativeLease`
866
- * is dropped. Must comfortably exceed the FULL late-crop horizon: detection
867
- * inference + the cross-process inference-result hop to hub post-analysis +
868
- * tracking + the tRPC crop round-trip back — on a busy camera with many
869
- * concurrent tracks that is well past the old 500 ms (which the busiest cameras'
870
- * subject crops outran, missing to the ≤640 fallback). Default 1200 ms; still
871
- * bounded by the per-session {@link NATIVE_LEASE_BUDGET_BYTES} ceiling and the
872
- * {@link NATIVE_LEASE_ACTIVITY_WINDOW_MS} demand gate, so idle cameras retain
873
- * nothing.
1108
+ * How many delivered frames are HELD at once, each waiting for its own
1109
+ * detection result. The ordinary exit is `cutTiles` / `releaseNativeLease`; this
1110
+ * bound only stops a runner that stopped answering from pinning RAM, and every
1111
+ * frame it drops is counted (`holdOverflow`).
874
1112
  */
875
- var NATIVE_LEASE_TTL_MS = NATIVE_LEASE_KNOBS.values.ttlMs;
1113
+ var NATIVE_LEASE_HOLD_FRAMES = NATIVE_LEASE_KNOBS.values.holdFrames;
1114
+ /**
1115
+ * Hard RAM budget (bytes) for the SUBJECT TILE store — what survives a frame.
1116
+ * `0` turns tiles off and restores the pre-2026-08-13 miss profile.
1117
+ */
1118
+ var NATIVE_TILE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.tileBudgetMb * 1024 * 1024;
1119
+ /**
1120
+ * How long a subject tile is served. A TTL is honest HERE and dishonest on the
1121
+ * hold: nothing is waiting on a tile, it is speculative retention against a
1122
+ * request that may never come. Sized an order of magnitude above the measured
1123
+ * ask age (2.2-9.9 s on this cluster) because it can be — a tile is ~60-120 KB.
1124
+ * Env-tunable for a bisect; not an operator knob, because the RAM it costs is
1125
+ * already the operator's knob.
1126
+ */
1127
+ var NATIVE_TILE_TTL_MS = (() => {
1128
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_TILE_TTL_MS"]);
1129
+ return Number.isFinite(raw) && raw > 0 ? raw : 3e4;
1130
+ })();
1131
+ /**
1132
+ * Most tiles cut from ONE frame, highest-confidence first (the runner orders
1133
+ * them). The bound exists because the cut runs on the decode worker between two
1134
+ * pulls: a frame that detected twenty subjects must not turn one late crop into
1135
+ * twenty encodes. Anything past it is counted, not silently dropped.
1136
+ */
1137
+ var NATIVE_TILE_MAX_PER_FRAME = (() => {
1138
+ const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_TILE_MAX_PER_FRAME"]);
1139
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
1140
+ })();
1141
+ /** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
1142
+ var NATIVE_TILE_JPEG_QUALITY = 90;
876
1143
  /**
877
1144
  * Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
878
1145
  * per-frame native downloads run only within this window of the last
@@ -1179,8 +1446,31 @@ var DecodeWorkerChild = class {
1179
1446
  */
1180
1447
  leaseStore = new NativeLeaseStore({
1181
1448
  budgetBytes: NATIVE_LEASE_BUDGET_BYTES,
1182
- ttlMs: NATIVE_LEASE_TTL_MS
1449
+ maxFrames: NATIVE_LEASE_HOLD_FRAMES
1450
+ });
1451
+ /**
1452
+ * What SURVIVES a held frame: one compressed native tile per subject, cut at
1453
+ * the instant the frame's detection result arrived. Every late native crop
1454
+ * that the hold can no longer serve is served from here — which measurement
1455
+ * says is the ordinary case, not the exception. See `native-tile-store.ts`.
1456
+ */
1457
+ tileStore = new NativeTileStore({
1458
+ budgetBytes: NATIVE_TILE_BUDGET_BYTES,
1459
+ ttlMs: NATIVE_TILE_TTL_MS
1183
1460
  });
1461
+ /** Tiles cut / served / refused-for-containment, on the throughput line. */
1462
+ tilesCut = 0;
1463
+ tileHits = 0;
1464
+ tileMisses = 0;
1465
+ tileCutFailures = 0;
1466
+ /**
1467
+ * Native frame dimensions the retained tiles were cut from. A session's
1468
+ * resolution does not change (a re-dial clears the store), so ONE pair is
1469
+ * enough — and it has to be remembered because a tile outlives the frame that
1470
+ * knew it.
1471
+ */
1472
+ tileFrameWidth = 0;
1473
+ tileFrameHeight = 0;
1184
1474
  /**
1185
1475
  * Demand gate for lease capture — armed at dial start + on every native-crop
1186
1476
  * request; while cold, {@link captureNativeFrame} declines retention so the
@@ -1306,6 +1596,9 @@ var DecodeWorkerChild = class {
1306
1596
  case "nativeCrop":
1307
1597
  this.handleNativeCrop(message.requestId, message.frameId, message.bbox, message.maxWidth);
1308
1598
  return;
1599
+ case "cutTiles":
1600
+ this.handleCutTiles(message.frameId, message.bboxes);
1601
+ return;
1309
1602
  case "releaseNativeLease":
1310
1603
  this.leaseStore.release(message.frameId);
1311
1604
  return;
@@ -1371,7 +1664,7 @@ var DecodeWorkerChild = class {
1371
1664
  const skipped = this.framesSkipped + this.frames.droppedCount;
1372
1665
  const rssMb = Math.round(process.memoryUsage().rss / 1048576);
1373
1666
  const ageS = Math.round((now - this.startedAt) / 1e3);
1374
- this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1667
+ this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
1375
1668
  this.lastMetricsAt = now;
1376
1669
  this.lastDeliveredSnapshot = this.framesDelivered;
1377
1670
  if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
@@ -1440,12 +1733,7 @@ var DecodeWorkerChild = class {
1440
1733
  if (this.admission.noteCropRequest(frameId)) this.emitStderr(`decode-worker-child: native crop named an UNINFERRED frame (frameId ${frameId}) — the 'inferred' lease-admission premise does not hold on this path; total=${this.admission.unmarkedCrops}\n`);
1441
1734
  const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
1442
1735
  if (!frame) {
1443
- this.nativeCropMisses++;
1444
- this.send({
1445
- kind: "nativeCropMiss",
1446
- requestId,
1447
- reason: `frame ${frameId} not retained (lease/ring evicted before crop)`
1448
- });
1736
+ this.serveTileFallback(requestId, frameId, bbox, maxWidth);
1449
1737
  return;
1450
1738
  }
1451
1739
  try {
@@ -1470,11 +1758,151 @@ var DecodeWorkerChild = class {
1470
1758
  });
1471
1759
  }
1472
1760
  }
1473
- /** The GPU-ring or reserved-slot frame for `frameId` (lease-disabled / newest paths). */
1761
+ /**
1762
+ * Answer a native-crop request from the tile tier, or report the miss.
1763
+ *
1764
+ * Separate from {@link handleNativeCrop} because it is ASYNC (a JPEG decode)
1765
+ * while the hold path is synchronous, and because the reason strings differ:
1766
+ * `no tile contains the requested rectangle` and `no tiles retained` have
1767
+ * different fixes, and a single "not retained" hid both.
1768
+ */
1769
+ async serveTileFallback(requestId, frameId, bbox, maxWidth) {
1770
+ try {
1771
+ const served = await this.serveFromTile(frameId, bbox, maxWidth);
1772
+ if (served) {
1773
+ this.tileHits += 1;
1774
+ this.nativeCropHits++;
1775
+ this.send({
1776
+ kind: "nativeCropResult",
1777
+ requestId,
1778
+ bytes: served.bytes,
1779
+ width: served.width,
1780
+ height: served.height
1781
+ });
1782
+ return;
1783
+ }
1784
+ this.tileMisses += 1;
1785
+ this.nativeCropMisses++;
1786
+ this.send({
1787
+ kind: "nativeCropMiss",
1788
+ requestId,
1789
+ reason: this.tileStore.enabled ? `frame ${frameId} released and no retained subject tile contains the requested rectangle` : `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`
1790
+ });
1791
+ } catch (err) {
1792
+ this.tileMisses += 1;
1793
+ this.nativeCropMisses++;
1794
+ const reason = errMessage(err);
1795
+ this.emitStderr(`decode-worker-child: tile serve failed (frame ${frameId}) — ${reason}\n`);
1796
+ this.send({
1797
+ kind: "nativeCropMiss",
1798
+ requestId,
1799
+ reason: `tile serve failed — ${reason}`
1800
+ });
1801
+ }
1802
+ }
1803
+ /** The GPU-ring or reserved-slot frame for `frameId` (hold-disabled / newest paths). */
1474
1804
  resolveRetainedFrame(frameId) {
1475
1805
  return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
1476
1806
  }
1477
1807
  /**
1808
+ * The runner has this frame's detections. Cut one native TILE per subject and
1809
+ * end the hold.
1810
+ *
1811
+ * This is the hinge of the two-tier design: it runs at the last instant the
1812
+ * frame's native pixels exist, and what it leaves behind is a handful of
1813
+ * subject-sized JPEGs instead of a ~24.9 MB raster. A frame on which nothing
1814
+ * was detected leaves nothing at all, which is the asymmetry the old
1815
+ * per-frame lease could not express.
1816
+ *
1817
+ * Three properties are load-bearing:
1818
+ * - the hold ENDS regardless of what happens here (`finally`), including on
1819
+ * an encode failure — a tile is a bonus, a pinned frame is a leak;
1820
+ * - the tile is a CONTAINER, padded wider than any model-input rectangle the
1821
+ * cluster convention can produce. The rectangle a model is fed is still
1822
+ * derived once, by the runner, and cut out of the tile in
1823
+ * {@link serveFromTile} ([D52](../../../../docs/decisions/adr-0052.md));
1824
+ * - every drop is counted. A silently-skipped tile is a crop miss with no
1825
+ * explanation, which is the failure this whole redesign exists to remove.
1826
+ */
1827
+ async handleCutTiles(frameId, bboxes) {
1828
+ try {
1829
+ if (!this.tileStore.enabled || bboxes.length === 0) return;
1830
+ const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
1831
+ if (!frame) {
1832
+ this.tileCutFailures += 1;
1833
+ return;
1834
+ }
1835
+ this.tileFrameWidth = frame.width;
1836
+ this.tileFrameHeight = frame.height;
1837
+ const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
1838
+ if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
1839
+ const tiles = [];
1840
+ for (const bbox of wanted) {
1841
+ const { region } = resolveNativeTileRegion(frame.width, frame.height, bbox, NATIVE_TILE_PADDING_RATIO);
1842
+ const resolved = {
1843
+ region,
1844
+ target: {
1845
+ width: region.width,
1846
+ height: region.height
1847
+ }
1848
+ };
1849
+ try {
1850
+ const raw = frame.isHwFrame() ? this.nativeHwCropToBuffer(frame, resolved) : this.nativeScaleCropToBuffer(frame, resolved);
1851
+ const jpeg = await (await require_lazy_sharp.getSharp())(raw, { raw: {
1852
+ width: region.width,
1853
+ height: region.height,
1854
+ channels: 3
1855
+ } }).jpeg({ quality: NATIVE_TILE_JPEG_QUALITY }).toBuffer();
1856
+ tiles.push({
1857
+ region,
1858
+ bytes: new Uint8Array(jpeg)
1859
+ });
1860
+ } catch (err) {
1861
+ this.tileCutFailures += 1;
1862
+ this.emitStderr(`decode-worker-child: subject tile cut FAILED on frame ${frameId} — ${errMessage(err)}\n`);
1863
+ }
1864
+ }
1865
+ if (tiles.length > 0) {
1866
+ this.tilesCut += tiles.length;
1867
+ this.tileStore.put(frameId, tiles);
1868
+ }
1869
+ } finally {
1870
+ this.leaseStore.release(frameId);
1871
+ }
1872
+ }
1873
+ /**
1874
+ * Serve a native crop from a retained subject TILE — the rung that covers the
1875
+ * ordinary case, where the request arrives seconds after the frame is gone.
1876
+ *
1877
+ * The rectangle is the SAME one {@link resolveNativeCrop} would have cut from
1878
+ * the full frame; only the raster it is taken from differs. A rectangle no
1879
+ * tile CONTAINS is a miss (`null`) — never a clamp into the nearest tile,
1880
+ * which would enrich a different part of the scene and error nowhere.
1881
+ */
1882
+ async serveFromTile(frameId, bbox, maxWidth) {
1883
+ const tiles = this.tileStore.get(frameId);
1884
+ if (tiles.length === 0) return null;
1885
+ const width = this.tileFrameWidth;
1886
+ const height = this.tileFrameHeight;
1887
+ if (width <= 0 || height <= 0) return null;
1888
+ const { region, target } = resolveNativeCropGeometry(width, height, bbox, maxWidth);
1889
+ const served = selectTileForRect(tiles, region);
1890
+ if (!served) return null;
1891
+ let pipeline = (await require_lazy_sharp.getSharp())(served.tile.bytes).extract({
1892
+ left: served.local.left,
1893
+ top: served.local.top,
1894
+ width: served.local.width,
1895
+ height: served.local.height
1896
+ });
1897
+ if (target.width !== served.local.width || target.height !== served.local.height) pipeline = pipeline.resize(target.width, target.height, { fit: "fill" });
1898
+ const raw = await pipeline.removeAlpha().raw().toBuffer();
1899
+ return {
1900
+ bytes: new Uint8Array(raw),
1901
+ width: target.width,
1902
+ height: target.height
1903
+ };
1904
+ }
1905
+ /**
1478
1906
  * Resolve a decoded frame's `(colorSpace, colorRange)` tags to the EXPLICIT
1479
1907
  * `(matrix, range)` every RGB conversion site pins — the RC-5 fix. `colorSpace`
1480
1908
  * / `colorRange` are node-av branded `number` subtypes, so they pass to the
@@ -2128,6 +2556,9 @@ var DecodeWorkerChild = class {
2128
2556
  closeInput() {
2129
2557
  this.nativeRing.clear();
2130
2558
  this.leaseStore.clear();
2559
+ this.tileStore.clear();
2560
+ this.tileFrameWidth = 0;
2561
+ this.tileFrameHeight = 0;
2131
2562
  this.nativeLeaseDownloadFilter?.close();
2132
2563
  this.nativeLeaseDownloadFilter = null;
2133
2564
  this.nativeLeaseDownloadFilterKey = "";