@camstack/addon-pipeline 1.2.200 → 1.2.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-c1LY6thP.mjs → default-detection-model-fUMLQHB2.mjs} +1 -1
  4. package/dist/{default-detection-model-BsmxtyZx.js → default-detection-model-nKgB-_mg.js} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-D2YTTkRf.js → dist-Bd875pfj.js} +540 -232
  8. package/dist/{dist-mEsrAAc3.mjs → dist-D2jcEuZD.mjs} +493 -233
  9. package/dist/{lazy-sharp-B5bW34uC.js → lazy-sharp-B4KrLp0u.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-CUxep5tc.mjs → node-CmfXSgr3.mjs} +420 -381
  13. package/dist/{node-x3YokJkD.js → node-dwhLC9Fh.js} +423 -378
  14. package/dist/pipeline-runner/index.js +41 -26
  15. package/dist/pipeline-runner/index.mjs +40 -25
  16. package/dist/{process-memory-CFc4D9A2.js → process-memory-BDRJK5Vq.js} +1 -1
  17. package/dist/{process-memory-BCqpTruN.mjs → process-memory-CH5qO7AX.mjs} +1 -1
  18. package/dist/recorder/index.js +4455 -4097
  19. package/dist/recorder/index.mjs +6085 -5727
  20. package/dist/{segment-demux-js-xHtB450F.mjs → segment-demux-js-BHTuEBhC.mjs} +1 -1
  21. package/dist/{segment-demux-js-DUZVURQd.js → segment-demux-js-cmfLROII.js} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +234 -24
  23. package/dist/session-decode/decode-worker-child.mjs +233 -23
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZcCQR5t.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CrzkEz67.mjs +26 -0
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-cYr6Swud.mjs} +1 -1
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-D_cZQhBA.mjs → hostInit-CZt5Lq_M.mjs} +3 -3
  31. package/dist/stream-broker/index.js +3 -3
  32. package/dist/stream-broker/index.mjs +3 -3
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-CRDaBlJl.js → worker-protocol--PcFsRrE.js} +1 -1
  35. package/dist/{worker-protocol-oUamwZSn.mjs → worker-protocol-D4GyRNnY.mjs} +1 -1
  36. package/package.json +5 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.mjs +0 -26
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { Ot as errMsg } from "./dist-mEsrAAc3.mjs";
2
+ import { It as errMsg } from "./dist-D2jcEuZD.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
5
5
  var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-D2YTTkRf.js");
1
+ const require_dist = require("./dist-Bd875pfj.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  //#region ../../node_modules/mp4box/dist/mp4box.all.js
4
4
  var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_worker_protocol = require("../worker-protocol-CRDaBlJl.js");
3
- const require_lazy_sharp = require("../lazy-sharp-B5bW34uC.js");
2
+ const require_worker_protocol = require("../worker-protocol--PcFsRrE.js");
3
+ const require_lazy_sharp = require("../lazy-sharp-B4KrLp0u.js");
4
4
  //#region src/session-decode/color-conversion.ts
5
5
  /**
6
6
  * Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
@@ -656,20 +656,6 @@ var NativeLeaseStore = class {
656
656
  }
657
657
  };
658
658
  //#endregion
659
- //#region src/session-decode/to-buffer-miss.ts
660
- /** Which rung lost the frame, from what the worker knows at the miss. */
661
- function toBufferMissRung(facts) {
662
- if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
663
- if (!facts.marked) return "never-leased";
664
- if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
665
- return "released";
666
- }
667
- /** The error line the runner sees: the rung first, the numbers after it. */
668
- function describeToBufferMiss(facts) {
669
- const rung = toBufferMissRung(facts);
670
- return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
671
- }
672
- //#endregion
673
659
  //#region src/session-decode/native-tile-geometry.ts
674
660
  /**
675
661
  * Pure geometry for the **native subject tile** — the compressed, subject-sized
@@ -921,6 +907,127 @@ var NativeTileStore = class {
921
907
  this.bytes -= entry.bytes;
922
908
  }
923
909
  };
910
+ /**
911
+ * The set of tile cuts currently between "I have this frame's pixels" and
912
+ * "the stores can answer for it", plus the latency of the ones that finished.
913
+ */
914
+ var PendingCuts = class {
915
+ waitMs;
916
+ now;
917
+ latencySampleCap;
918
+ cuts = /* @__PURE__ */ new Map();
919
+ /** Insertion-ordered ring of completed cut latencies (ms). */
920
+ latencies = [];
921
+ constructor(options) {
922
+ this.waitMs = Math.max(0, options.waitMs);
923
+ this.now = options.now ?? Date.now;
924
+ this.latencySampleCap = Math.max(1, options.latencySamples ?? 256);
925
+ }
926
+ /** Cuts currently in flight (tests / metrics). */
927
+ get inFlight() {
928
+ return this.cuts.size;
929
+ }
930
+ /**
931
+ * A cut for `frameId` has started. Call BEFORE the first `await` in the cut,
932
+ * so no request can observe the gap this module exists to cover.
933
+ *
934
+ * A duplicate id should never occur (frame ids are monotonic); settling the
935
+ * prior entry rather than replacing it keeps any waiter on it from hanging.
936
+ */
937
+ begin(frameId) {
938
+ this.settle(frameId);
939
+ let finish = () => {};
940
+ const done = new Promise((resolve) => {
941
+ finish = resolve;
942
+ });
943
+ this.cuts.set(frameId, {
944
+ startedAt: this.now(),
945
+ done,
946
+ finish
947
+ });
948
+ }
949
+ /**
950
+ * The cut for `frameId` is over — the stores now hold whatever it produced.
951
+ * Call from the cut's `finally`, so a throwing encode releases waiters too.
952
+ * Idempotent.
953
+ */
954
+ settle(frameId) {
955
+ const cut = this.cuts.get(frameId);
956
+ if (!cut) return;
957
+ this.cuts.delete(frameId);
958
+ this.noteLatency(this.now() - cut.startedAt);
959
+ cut.finish();
960
+ }
961
+ /**
962
+ * Wait for `frameId`'s cut to land, bounded.
963
+ *
964
+ * `none` means there was nothing in flight, and the caller must NOT read that
965
+ * as "wait exhausted" — it is the ordinary answer for a frame whose cut has
966
+ * already finished or never ran.
967
+ */
968
+ async waitFor(frameId) {
969
+ const cut = this.cuts.get(frameId);
970
+ if (!cut) return "none";
971
+ if (this.waitMs === 0) return "timeout";
972
+ let timer;
973
+ const expired = new Promise((resolve) => {
974
+ timer = setTimeout(() => resolve("timeout"), this.waitMs);
975
+ timer.unref?.();
976
+ });
977
+ try {
978
+ return await Promise.race([cut.done.then(() => "settled"), expired]);
979
+ } finally {
980
+ if (timer !== void 0) clearTimeout(timer);
981
+ }
982
+ }
983
+ /**
984
+ * Release every waiter and forget every in-flight cut — re-dial and teardown.
985
+ * A waiter left behind here would outlive the worker that could settle it.
986
+ */
987
+ clear() {
988
+ for (const cut of this.cuts.values()) cut.finish();
989
+ this.cuts.clear();
990
+ }
991
+ /**
992
+ * The `percentile`-th cut latency in ms over the retained samples, or `null`
993
+ * when nothing has completed yet. `null` rather than 0: a worker that has cut
994
+ * nothing and a worker that cuts instantly must not read the same.
995
+ */
996
+ latencyMs(percentile) {
997
+ if (this.latencies.length === 0) return null;
998
+ const sorted = [...this.latencies].sort((a, b) => a - b);
999
+ const clamped = Math.min(100, Math.max(0, percentile));
1000
+ const index = Math.min(sorted.length - 1, Math.ceil(clamped / 100 * sorted.length) - 1);
1001
+ return sorted[Math.max(0, index)] ?? null;
1002
+ }
1003
+ noteLatency(ms) {
1004
+ this.latencies.push(Math.max(0, ms));
1005
+ while (this.latencies.length > this.latencySampleCap) this.latencies.shift();
1006
+ }
1007
+ };
1008
+ /**
1009
+ * Serve a frame's retained tiles, waiting for an in-flight cut ONLY when
1010
+ * waiting could change the answer.
1011
+ *
1012
+ * The order is the whole point. Anything already retained is served at once —
1013
+ * a cut in flight for a LATER subject of the same frame must not delay a
1014
+ * request the stores can already answer. Only an empty store consults
1015
+ * {@link PendingCuts}, and only then is a success attributable to the wait.
1016
+ */
1017
+ async function attemptTileServe(pending, frameId, source) {
1018
+ if (source.hasRetained()) return {
1019
+ served: await source.serve(),
1020
+ wait: "none",
1021
+ resolvedByWait: false
1022
+ };
1023
+ const wait = await pending.waitFor(frameId);
1024
+ const served = await source.serve();
1025
+ return {
1026
+ served,
1027
+ wait,
1028
+ resolvedByWait: served !== null && wait === "settled"
1029
+ };
1030
+ }
924
1031
  //#endregion
925
1032
  //#region src/session-decode/planar-crop-source.ts
926
1033
  /** Layout a detection `toBuffer` can read, or `null` when it must name the
@@ -950,6 +1057,19 @@ function nativeCropSourceTag(format) {
950
1057
  case "rgb24": return "rgb:";
951
1058
  }
952
1059
  }
1060
+ /**
1061
+ * Bytes a crop of `rows` rows actually reads, measured from the crop's ORIGIN.
1062
+ *
1063
+ * The LAST row needs only its own `rowBytes`, never a whole stride — the stride
1064
+ * is the distance to the NEXT row, and for the last row there is no next row.
1065
+ * Demanding `stride * rows` instead overshoots the plane's end by exactly the
1066
+ * horizontal offset, which is invisible everywhere except where the crop's
1067
+ * bottom IS the frame's bottom. See {@link resolvePlanarCropSource}.
1068
+ */
1069
+ function planarSpan(rows, stride, rowBytes) {
1070
+ if (rows <= 0) return 0;
1071
+ return (rows - 1) * stride + rowBytes;
1072
+ }
953
1073
  /** A plane view starting at `offset`, or `null` when the plane is too short. */
954
1074
  function viewAt(plane, offset, requiredSpan) {
955
1075
  if (!plane) return null;
@@ -968,6 +1088,28 @@ function viewAt(plane, offset, requiredSpan) {
968
1088
  * An odd origin is floored onto the chroma grid rather than rejected: the
969
1089
  * geometry helper already aligns to even, and flooring reads the chroma sample
970
1090
  * that CONTAINS the origin instead of inventing a half sample.
1091
+ *
1092
+ * ## The bottom edge (2026-09-07)
1093
+ *
1094
+ * The span demanded from each plane used to be `stride * region.height` from
1095
+ * the crop ORIGIN. A stride is the distance to the NEXT row, and the last row
1096
+ * of a crop has no next row — it needs only `left + width` bytes. So the demand
1097
+ * overshot the plane's end by exactly `left`, which is invisible everywhere
1098
+ * except when the crop's bottom IS the frame's last row. `resolveNativeTileRegion`
1099
+ * clamps a padded box to precisely that, and `clampEven` floors both `top` and
1100
+ * `height`, so a bottom-clamped tile lands flush every time.
1101
+ *
1102
+ * The result was that every subject standing near the camera — the biggest, the
1103
+ * closest, the best shot — lost its native tile, and the hold path threw for
1104
+ * the same rectangles. Measured over 6 h on the live cluster: **100 of 100**
1105
+ * logged `missing plane data` geometries had `top + height === frameHeight`
1106
+ * with `left > 0`, 101 of 102 on dev:590 (salone, 3840x2160).
1107
+ *
1108
+ * The check is now the region's real extent, so a crop that genuinely runs past
1109
+ * a plane — one row too tall, or a last row reaching past the stride — is still
1110
+ * refused. The views handed back are unchanged: `subarray(offset)` always ran
1111
+ * to the plane's true end, so nothing here ever sized a buffer, and tightening
1112
+ * the predicate cannot shorten one.
971
1113
  */
972
1114
  function resolvePlanarCropSource(kind, planes, strides, region) {
973
1115
  if (!planes || planes.length < PLANE_COUNT[kind]) return null;
@@ -978,10 +1120,11 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
978
1120
  const left = Math.max(0, region.left);
979
1121
  const chromaRow = Math.floor(top / 2);
980
1122
  const chromaHeight = Math.ceil(region.height / 2);
981
- const y = viewAt(planes[0], top * yStride + left, yStride * region.height);
1123
+ const chromaWidth = Math.ceil(Math.max(0, region.width) / 2);
1124
+ const y = viewAt(planes[0], top * yStride + left, planarSpan(region.height, yStride, region.width));
982
1125
  if (kind === "nv12") {
983
1126
  const uvOffset = chromaRow * cStride + Math.floor(left / 2) * 2;
984
- const uv = viewAt(planes[1], uvOffset, cStride * chromaHeight);
1127
+ const uv = viewAt(planes[1], uvOffset, planarSpan(chromaHeight, cStride, chromaWidth * 2));
985
1128
  if (!y || !uv) return null;
986
1129
  return {
987
1130
  planes: [y, uv],
@@ -990,8 +1133,8 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
990
1133
  }
991
1134
  const vStride = strides?.[2] ?? cStride;
992
1135
  const chromaOffset = chromaRow * cStride + Math.floor(left / 2);
993
- const u = viewAt(planes[1], chromaOffset, cStride * chromaHeight);
994
- const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), vStride * chromaHeight);
1136
+ const u = viewAt(planes[1], chromaOffset, planarSpan(chromaHeight, cStride, chromaWidth));
1137
+ const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), planarSpan(chromaHeight, vStride, chromaWidth));
995
1138
  if (!y || !u || !v) return null;
996
1139
  return {
997
1140
  planes: [
@@ -1331,6 +1474,20 @@ async function cutSubjectTiles(frame, bboxes, ports) {
1331
1474
  return tiles;
1332
1475
  }
1333
1476
  //#endregion
1477
+ //#region src/session-decode/to-buffer-miss.ts
1478
+ /** Which rung lost the frame, from what the worker knows at the miss. */
1479
+ function toBufferMissRung(facts) {
1480
+ if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
1481
+ if (!facts.marked) return "never-leased";
1482
+ if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
1483
+ return "released";
1484
+ }
1485
+ /** The error line the runner sees: the rung first, the numbers after it. */
1486
+ function describeToBufferMiss(facts) {
1487
+ const rung = toBufferMissRung(facts);
1488
+ return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
1489
+ }
1490
+ //#endregion
1334
1491
  //#region src/session-decode/decode-worker-child.ts
1335
1492
  /** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
1336
1493
  function toLeasedNavFrame(frame) {
@@ -1458,6 +1615,28 @@ var NATIVE_TILE_MAX_PER_FRAME = (() => {
1458
1615
  /** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
1459
1616
  var NATIVE_TILE_JPEG_QUALITY = 90;
1460
1617
  /**
1618
+ * How long a native-crop request waits for a tile cut that is still ENCODING.
1619
+ *
1620
+ * The cut reads its pixels synchronously at `FrameResult` time but only writes
1621
+ * the stores after N JPEG encodes; a request landing in between reads two empty
1622
+ * stores and is told `no retained tile for this frame` — the same sentence a
1623
+ * frame that was never cut gets. Measured 2026-09-07: misses onset at ~100 ms
1624
+ * (only 0.1% below it) and peak at 500-1000 ms, on cameras that cut a tile for
1625
+ * 99.7% of their inferred frames, so the window — not any lifetime — is what
1626
+ * they are landing in.
1627
+ *
1628
+ * 500 ms deliberately, not more: the confirmation gate bounds each crop attempt
1629
+ * at 300 ms and the capture scheduler drops an enrichment request older than
1630
+ * 3 s, so a miss held too long stops being a fast retryable `null` and becomes
1631
+ * an upstream timeout. Widen it via `CAMSTACK_SESSION_TILE_CUT_WAIT_MS` only
1632
+ * once `cutLatencyMs` on the metrics line says what the encode window really
1633
+ * is; `0` disables the wait and restores the pre-2026-09-07 miss profile.
1634
+ */
1635
+ var TILE_CUT_WAIT_MS = (() => {
1636
+ const raw = Number(process.env["CAMSTACK_SESSION_TILE_CUT_WAIT_MS"]);
1637
+ return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 500;
1638
+ })();
1639
+ /**
1461
1640
  * Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
1462
1641
  * per-frame native downloads run only within this window of the last
1463
1642
  * native-crop request (or dial start). `0` disables the GATE (legacy always-on
@@ -1598,6 +1777,14 @@ function errMessage(err) {
1598
1777
  return String(err);
1599
1778
  }
1600
1779
  /**
1780
+ * Render a cut-latency percentile for the metrics line. `-` when nothing has
1781
+ * been cut yet: a worker that cut nothing and one that cuts in 0 ms must not
1782
+ * read the same, or the number that sizes {@link TILE_CUT_WAIT_MS} is a guess.
1783
+ */
1784
+ function formatLatency(ms) {
1785
+ return ms === null ? "-" : String(Math.round(ms));
1786
+ }
1787
+ /**
1601
1788
  * RTSP low-latency demuxer options — mirrors
1602
1789
  * `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
1603
1790
  * than cross-imported: this file is a standalone forked entry point, not an
@@ -1802,6 +1989,19 @@ var DecodeWorkerChild = class {
1802
1989
  tileHits = 0;
1803
1990
  tileMisses = 0;
1804
1991
  tileCutFailures = 0;
1992
+ /**
1993
+ * Serves that produced pixels ONLY because the request waited for a cut that
1994
+ * was still encoding — i.e. misses before {@link TILE_CUT_WAIT_MS} existed.
1995
+ *
1996
+ * This is the counter the write-gap hypothesis stands or falls on. If it
1997
+ * stays near zero while `tileMisses` does not move, the misses were never a
1998
+ * timing race and the question becomes WHICH frames get cut at all.
1999
+ */
2000
+ tileMissesResolvedByWait = 0;
2001
+ /** Requests that gave up on an in-flight cut at the bound — dropped work. */
2002
+ tileWaitTimeouts = 0;
2003
+ /** Cuts between "I have the pixels" and "the stores can answer". */
2004
+ pendingCuts = new PendingCuts({ waitMs: TILE_CUT_WAIT_MS });
1805
2005
  /** Scene tiles cut / served, on the throughput line. */
1806
2006
  scenesCut = 0;
1807
2007
  sceneHits = 0;
@@ -2058,7 +2258,7 @@ var DecodeWorkerChild = class {
2058
2258
  const skipped = this.framesSkipped + this.frames.droppedCount;
2059
2259
  const rssMb = Math.round(process.memoryUsage().rss / 1048576);
2060
2260
  const ageS = Math.round((now - this.startedAt) / 1e3);
2061
- 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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
2261
+ 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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMissesResolvedByWait:${this.tileMissesResolvedByWait}, tileWaitTimeouts:${this.tileWaitTimeouts}, cutLatencyMsP50:${formatLatency(this.pendingCuts.latencyMs(50))}, cutLatencyMsP95:${formatLatency(this.pendingCuts.latencyMs(95))}, cutsInFlight:${this.pendingCuts.inFlight}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
2062
2262
  this.lastMetricsAt = now;
2063
2263
  this.lastDeliveredSnapshot = this.framesDelivered;
2064
2264
  if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
@@ -2210,10 +2410,16 @@ var DecodeWorkerChild = class {
2210
2410
  */
2211
2411
  async serveTileFallback(requestId, frameId, bbox, maxWidth) {
2212
2412
  try {
2213
- const served = await this.serveFromTile(frameId, bbox, maxWidth);
2413
+ const attempt = await attemptTileServe(this.pendingCuts, frameId, {
2414
+ hasRetained: () => this.tileStore.get(frameId).length > 0 || this.sceneStore.get(frameId).length > 0,
2415
+ serve: () => this.serveFromTile(frameId, bbox, maxWidth)
2416
+ });
2417
+ const served = attempt.served;
2418
+ if (attempt.wait === "timeout") this.tileWaitTimeouts += 1;
2214
2419
  if (served) {
2215
2420
  this.tileHits += 1;
2216
2421
  this.nativeCropHits++;
2422
+ if (attempt.resolvedByWait) this.tileMissesResolvedByWait += 1;
2217
2423
  this.send({
2218
2424
  kind: "nativeCropResult",
2219
2425
  requestId,
@@ -2229,7 +2435,7 @@ var DecodeWorkerChild = class {
2229
2435
  this.send({
2230
2436
  kind: "nativeCropMiss",
2231
2437
  requestId,
2232
- reason: this.tileMissReason(frameId)
2438
+ reason: this.tileMissReason(frameId, attempt.wait)
2233
2439
  });
2234
2440
  } catch (err) {
2235
2441
  this.tileMisses += 1;
@@ -2249,7 +2455,8 @@ var DecodeWorkerChild = class {
2249
2455
  * "the rectangle is outside every container" are three different sentences,
2250
2456
  * and one "not retained" hid all of them.
2251
2457
  */
2252
- tileMissReason(frameId) {
2458
+ tileMissReason(frameId, wait) {
2459
+ if (wait === "timeout") return `frame ${frameId} released and its tile cut did not finish within ${TILE_CUT_WAIT_MS} ms — the request outran the encode, not the retention`;
2253
2460
  if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
2254
2461
  if (!this.sceneStore.enabled) return `frame ${frameId} released and no retained subject tile contains the requested rectangle; scene tiles are DISABLED (sceneBudgetMb 0), so a full-frame request cannot be served at all`;
2255
2462
  if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
@@ -2281,6 +2488,7 @@ var DecodeWorkerChild = class {
2281
2488
  * explanation, which is the failure this whole redesign exists to remove.
2282
2489
  */
2283
2490
  async handleCutTiles(frameId, bboxes) {
2491
+ this.pendingCuts.begin(frameId);
2284
2492
  try {
2285
2493
  if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
2286
2494
  const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
@@ -2362,6 +2570,7 @@ var DecodeWorkerChild = class {
2362
2570
  this.sceneStore.put(frameId, [scene]);
2363
2571
  }
2364
2572
  } finally {
2573
+ this.pendingCuts.settle(frameId);
2365
2574
  this.noteLeaseRelease(frameId, this.leaseStore.release(frameId), "cut");
2366
2575
  }
2367
2576
  }
@@ -3144,6 +3353,7 @@ var DecodeWorkerChild = class {
3144
3353
  this.leaseStore.clear();
3145
3354
  this.tileStore.clear();
3146
3355
  this.sceneStore.clear();
3356
+ this.pendingCuts.clear();
3147
3357
  this.tileFrameWidth = 0;
3148
3358
  this.tileFrameHeight = 0;
3149
3359
  this.nativeLeaseDownloadFilter?.close();