@camstack/addon-pipeline 1.2.70 → 1.2.71

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