@camstack/addon-pipeline 1.2.201 → 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.
@@ -1093,7 +1093,7 @@ var SessionDecodeSession = class {
1093
1093
  * rejects — native crops are strictly best-effort with a free fallback.
1094
1094
  */
1095
1095
  requestNativeCrop(frameId, bbox, maxWidth) {
1096
- if (this.exited) return Promise.resolve(null);
1096
+ if (this.exited) return Promise.resolve({ kind: "miss" });
1097
1097
  const requestId = this.nextNativeCropId++;
1098
1098
  return new Promise((resolve) => {
1099
1099
  this.pendingNativeCrops.set(requestId, { resolve });
@@ -1107,7 +1107,7 @@ var SessionDecodeSession = class {
1107
1107
  });
1108
1108
  } catch {
1109
1109
  this.pendingNativeCrops.delete(requestId);
1110
- resolve(null);
1110
+ resolve({ kind: "miss" });
1111
1111
  }
1112
1112
  });
1113
1113
  }
@@ -1187,10 +1187,13 @@ var SessionDecodeSession = class {
1187
1187
  return;
1188
1188
  case "nativeCropResult":
1189
1189
  this.settleNativeCrop(raw.requestId, {
1190
- bytes: raw.bytes,
1191
- width: raw.width,
1192
- height: raw.height,
1193
- ...raw.source ? { source: raw.source } : {}
1190
+ kind: "crop",
1191
+ crop: {
1192
+ bytes: raw.bytes,
1193
+ width: raw.width,
1194
+ height: raw.height,
1195
+ ...raw.source ? { source: raw.source } : {}
1196
+ }
1194
1197
  });
1195
1198
  return;
1196
1199
  case "nativeCropMiss":
@@ -1198,7 +1201,10 @@ var SessionDecodeSession = class {
1198
1201
  requestId: raw.requestId,
1199
1202
  reason: raw.reason
1200
1203
  } });
1201
- this.settleNativeCrop(raw.requestId, null);
1204
+ this.settleNativeCrop(raw.requestId, {
1205
+ kind: "miss",
1206
+ ...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
1207
+ });
1202
1208
  return;
1203
1209
  case "error":
1204
1210
  this.settleError(raw.message, raw.frameId);
@@ -1235,15 +1241,15 @@ var SessionDecodeSession = class {
1235
1241
  }
1236
1242
  for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
1237
1243
  this.pendingToBuffers.clear();
1238
- for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(null);
1244
+ for (const waiter of this.pendingNativeCrops.values()) waiter.resolve({ kind: "miss" });
1239
1245
  this.pendingNativeCrops.clear();
1240
1246
  }
1241
1247
  /** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
1242
- settleNativeCrop(requestId, result) {
1248
+ settleNativeCrop(requestId, attempt) {
1243
1249
  const waiter = this.pendingNativeCrops.get(requestId);
1244
1250
  if (!waiter) return;
1245
1251
  this.pendingNativeCrops.delete(requestId);
1246
- waiter.resolve(result);
1252
+ waiter.resolve(attempt);
1247
1253
  }
1248
1254
  settlePendingPull(outcome) {
1249
1255
  const waiter = this.pendingPull;
@@ -2907,7 +2913,8 @@ async function runDetailSubtree(deps, input) {
2907
2913
  hadFrameJpeg: input.frameJpeg !== void 0,
2908
2914
  hadCropJpeg: input.cropJpeg !== void 0,
2909
2915
  nativeMissReason: nativeMiss?.miss ?? null,
2910
- nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
2916
+ nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null,
2917
+ nativeWorkerReason: nativeMiss?.workerReason ?? null
2911
2918
  }
2912
2919
  });
2913
2920
  return null;
@@ -7243,6 +7250,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7243
7250
  handleNodeId: input.handle.nodeId,
7244
7251
  nativeMissReason: resolved.missReason,
7245
7252
  nativeHandleAgeMs: resolved.handleAgeMs,
7253
+ ...resolved.workerReason !== void 0 ? { nativeWorkerReason: resolved.workerReason } : {},
7246
7254
  maxWidth: input.maxWidth ?? null,
7247
7255
  hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
7248
7256
  }
@@ -7526,18 +7534,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7526
7534
  let nativeResult = null;
7527
7535
  let missReason = null;
7528
7536
  let handleAgeMs = null;
7537
+ let workerReason;
7529
7538
  if (!isUnboundedFullFrame) {
7530
7539
  const found = this.nativeCropRegistry.get(input.handle);
7531
7540
  handleAgeMs = found?.ageMs ?? null;
7532
7541
  if (found) {
7533
- const result = await found.request(input.bbox, input.maxWidth);
7534
- if (result) nativeResult = {
7535
- bytes: result.bytes,
7536
- width: result.width,
7537
- height: result.height,
7538
- ...result.source ? { source: result.source } : {}
7542
+ const attempt = await found.request(input.bbox, input.maxWidth);
7543
+ if (attempt.kind === "crop") nativeResult = {
7544
+ bytes: attempt.crop.bytes,
7545
+ width: attempt.crop.width,
7546
+ height: attempt.crop.height,
7547
+ ...attempt.crop.source ? { source: attempt.crop.source } : {}
7539
7548
  };
7540
- else missReason = "worker-lease-gone";
7549
+ else {
7550
+ missReason = "worker-lease-gone";
7551
+ workerReason = attempt.workerReason;
7552
+ }
7541
7553
  } else missReason = "no-registry-entry";
7542
7554
  if (missReason !== null) {
7543
7555
  const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
@@ -7550,7 +7562,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7550
7562
  handleNodeId: input.handle.nodeId,
7551
7563
  handleAgeMs,
7552
7564
  registrySize: this.nativeCropRegistry.size,
7553
- registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
7565
+ registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP,
7566
+ ...workerReason !== void 0 ? { workerReason } : {}
7554
7567
  }
7555
7568
  };
7556
7569
  if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
@@ -7561,7 +7574,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7561
7574
  pixels: nativeResult,
7562
7575
  tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
7563
7576
  missReason,
7564
- handleAgeMs
7577
+ handleAgeMs,
7578
+ workerReason
7565
7579
  };
7566
7580
  }
7567
7581
  /**
@@ -7871,7 +7885,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7871
7885
  });
7872
7886
  if (resolved.pixels === null || resolved.tier !== "native") return {
7873
7887
  miss: resolved.missReason ?? "worker-lease-gone",
7874
- handleAgeMs: resolved.handleAgeMs
7888
+ handleAgeMs: resolved.handleAgeMs,
7889
+ ...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
7875
7890
  };
7876
7891
  const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
7877
7892
  if (!encoded?.jpeg) return {
@@ -1087,7 +1087,7 @@ var SessionDecodeSession = class {
1087
1087
  * rejects — native crops are strictly best-effort with a free fallback.
1088
1088
  */
1089
1089
  requestNativeCrop(frameId, bbox, maxWidth) {
1090
- if (this.exited) return Promise.resolve(null);
1090
+ if (this.exited) return Promise.resolve({ kind: "miss" });
1091
1091
  const requestId = this.nextNativeCropId++;
1092
1092
  return new Promise((resolve) => {
1093
1093
  this.pendingNativeCrops.set(requestId, { resolve });
@@ -1101,7 +1101,7 @@ var SessionDecodeSession = class {
1101
1101
  });
1102
1102
  } catch {
1103
1103
  this.pendingNativeCrops.delete(requestId);
1104
- resolve(null);
1104
+ resolve({ kind: "miss" });
1105
1105
  }
1106
1106
  });
1107
1107
  }
@@ -1181,10 +1181,13 @@ var SessionDecodeSession = class {
1181
1181
  return;
1182
1182
  case "nativeCropResult":
1183
1183
  this.settleNativeCrop(raw.requestId, {
1184
- bytes: raw.bytes,
1185
- width: raw.width,
1186
- height: raw.height,
1187
- ...raw.source ? { source: raw.source } : {}
1184
+ kind: "crop",
1185
+ crop: {
1186
+ bytes: raw.bytes,
1187
+ width: raw.width,
1188
+ height: raw.height,
1189
+ ...raw.source ? { source: raw.source } : {}
1190
+ }
1188
1191
  });
1189
1192
  return;
1190
1193
  case "nativeCropMiss":
@@ -1192,7 +1195,10 @@ var SessionDecodeSession = class {
1192
1195
  requestId: raw.requestId,
1193
1196
  reason: raw.reason
1194
1197
  } });
1195
- this.settleNativeCrop(raw.requestId, null);
1198
+ this.settleNativeCrop(raw.requestId, {
1199
+ kind: "miss",
1200
+ ...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
1201
+ });
1196
1202
  return;
1197
1203
  case "error":
1198
1204
  this.settleError(raw.message, raw.frameId);
@@ -1229,15 +1235,15 @@ var SessionDecodeSession = class {
1229
1235
  }
1230
1236
  for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
1231
1237
  this.pendingToBuffers.clear();
1232
- for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(null);
1238
+ for (const waiter of this.pendingNativeCrops.values()) waiter.resolve({ kind: "miss" });
1233
1239
  this.pendingNativeCrops.clear();
1234
1240
  }
1235
1241
  /** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
1236
- settleNativeCrop(requestId, result) {
1242
+ settleNativeCrop(requestId, attempt) {
1237
1243
  const waiter = this.pendingNativeCrops.get(requestId);
1238
1244
  if (!waiter) return;
1239
1245
  this.pendingNativeCrops.delete(requestId);
1240
- waiter.resolve(result);
1246
+ waiter.resolve(attempt);
1241
1247
  }
1242
1248
  settlePendingPull(outcome) {
1243
1249
  const waiter = this.pendingPull;
@@ -2900,7 +2906,8 @@ async function runDetailSubtree(deps, input) {
2900
2906
  hadFrameJpeg: input.frameJpeg !== void 0,
2901
2907
  hadCropJpeg: input.cropJpeg !== void 0,
2902
2908
  nativeMissReason: nativeMiss?.miss ?? null,
2903
- nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
2909
+ nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null,
2910
+ nativeWorkerReason: nativeMiss?.workerReason ?? null
2904
2911
  }
2905
2912
  });
2906
2913
  return null;
@@ -7236,6 +7243,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7236
7243
  handleNodeId: input.handle.nodeId,
7237
7244
  nativeMissReason: resolved.missReason,
7238
7245
  nativeHandleAgeMs: resolved.handleAgeMs,
7246
+ ...resolved.workerReason !== void 0 ? { nativeWorkerReason: resolved.workerReason } : {},
7239
7247
  maxWidth: input.maxWidth ?? null,
7240
7248
  hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
7241
7249
  }
@@ -7519,18 +7527,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7519
7527
  let nativeResult = null;
7520
7528
  let missReason = null;
7521
7529
  let handleAgeMs = null;
7530
+ let workerReason;
7522
7531
  if (!isUnboundedFullFrame) {
7523
7532
  const found = this.nativeCropRegistry.get(input.handle);
7524
7533
  handleAgeMs = found?.ageMs ?? null;
7525
7534
  if (found) {
7526
- const result = await found.request(input.bbox, input.maxWidth);
7527
- if (result) nativeResult = {
7528
- bytes: result.bytes,
7529
- width: result.width,
7530
- height: result.height,
7531
- ...result.source ? { source: result.source } : {}
7535
+ const attempt = await found.request(input.bbox, input.maxWidth);
7536
+ if (attempt.kind === "crop") nativeResult = {
7537
+ bytes: attempt.crop.bytes,
7538
+ width: attempt.crop.width,
7539
+ height: attempt.crop.height,
7540
+ ...attempt.crop.source ? { source: attempt.crop.source } : {}
7532
7541
  };
7533
- else missReason = "worker-lease-gone";
7542
+ else {
7543
+ missReason = "worker-lease-gone";
7544
+ workerReason = attempt.workerReason;
7545
+ }
7534
7546
  } else missReason = "no-registry-entry";
7535
7547
  if (missReason !== null) {
7536
7548
  const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
@@ -7543,7 +7555,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7543
7555
  handleNodeId: input.handle.nodeId,
7544
7556
  handleAgeMs,
7545
7557
  registrySize: this.nativeCropRegistry.size,
7546
- registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
7558
+ registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP,
7559
+ ...workerReason !== void 0 ? { workerReason } : {}
7547
7560
  }
7548
7561
  };
7549
7562
  if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
@@ -7554,7 +7567,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7554
7567
  pixels: nativeResult,
7555
7568
  tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
7556
7569
  missReason,
7557
- handleAgeMs
7570
+ handleAgeMs,
7571
+ workerReason
7558
7572
  };
7559
7573
  }
7560
7574
  /**
@@ -7864,7 +7878,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7864
7878
  });
7865
7879
  if (resolved.pixels === null || resolved.tier !== "native") return {
7866
7880
  miss: resolved.missReason ?? "worker-lease-gone",
7867
- handleAgeMs: resolved.handleAgeMs
7881
+ handleAgeMs: resolved.handleAgeMs,
7882
+ ...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
7868
7883
  };
7869
7884
  const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
7870
7885
  if (!encoded?.jpeg) return {
@@ -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();
@@ -655,20 +655,6 @@ var NativeLeaseStore = class {
655
655
  }
656
656
  };
657
657
  //#endregion
658
- //#region src/session-decode/to-buffer-miss.ts
659
- /** Which rung lost the frame, from what the worker knows at the miss. */
660
- function toBufferMissRung(facts) {
661
- if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
662
- if (!facts.marked) return "never-leased";
663
- if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
664
- return "released";
665
- }
666
- /** The error line the runner sees: the rung first, the numbers after it. */
667
- function describeToBufferMiss(facts) {
668
- const rung = toBufferMissRung(facts);
669
- 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}` : ""}}`;
670
- }
671
- //#endregion
672
658
  //#region src/session-decode/native-tile-geometry.ts
673
659
  /**
674
660
  * Pure geometry for the **native subject tile** — the compressed, subject-sized
@@ -920,6 +906,127 @@ var NativeTileStore = class {
920
906
  this.bytes -= entry.bytes;
921
907
  }
922
908
  };
909
+ /**
910
+ * The set of tile cuts currently between "I have this frame's pixels" and
911
+ * "the stores can answer for it", plus the latency of the ones that finished.
912
+ */
913
+ var PendingCuts = class {
914
+ waitMs;
915
+ now;
916
+ latencySampleCap;
917
+ cuts = /* @__PURE__ */ new Map();
918
+ /** Insertion-ordered ring of completed cut latencies (ms). */
919
+ latencies = [];
920
+ constructor(options) {
921
+ this.waitMs = Math.max(0, options.waitMs);
922
+ this.now = options.now ?? Date.now;
923
+ this.latencySampleCap = Math.max(1, options.latencySamples ?? 256);
924
+ }
925
+ /** Cuts currently in flight (tests / metrics). */
926
+ get inFlight() {
927
+ return this.cuts.size;
928
+ }
929
+ /**
930
+ * A cut for `frameId` has started. Call BEFORE the first `await` in the cut,
931
+ * so no request can observe the gap this module exists to cover.
932
+ *
933
+ * A duplicate id should never occur (frame ids are monotonic); settling the
934
+ * prior entry rather than replacing it keeps any waiter on it from hanging.
935
+ */
936
+ begin(frameId) {
937
+ this.settle(frameId);
938
+ let finish = () => {};
939
+ const done = new Promise((resolve) => {
940
+ finish = resolve;
941
+ });
942
+ this.cuts.set(frameId, {
943
+ startedAt: this.now(),
944
+ done,
945
+ finish
946
+ });
947
+ }
948
+ /**
949
+ * The cut for `frameId` is over — the stores now hold whatever it produced.
950
+ * Call from the cut's `finally`, so a throwing encode releases waiters too.
951
+ * Idempotent.
952
+ */
953
+ settle(frameId) {
954
+ const cut = this.cuts.get(frameId);
955
+ if (!cut) return;
956
+ this.cuts.delete(frameId);
957
+ this.noteLatency(this.now() - cut.startedAt);
958
+ cut.finish();
959
+ }
960
+ /**
961
+ * Wait for `frameId`'s cut to land, bounded.
962
+ *
963
+ * `none` means there was nothing in flight, and the caller must NOT read that
964
+ * as "wait exhausted" — it is the ordinary answer for a frame whose cut has
965
+ * already finished or never ran.
966
+ */
967
+ async waitFor(frameId) {
968
+ const cut = this.cuts.get(frameId);
969
+ if (!cut) return "none";
970
+ if (this.waitMs === 0) return "timeout";
971
+ let timer;
972
+ const expired = new Promise((resolve) => {
973
+ timer = setTimeout(() => resolve("timeout"), this.waitMs);
974
+ timer.unref?.();
975
+ });
976
+ try {
977
+ return await Promise.race([cut.done.then(() => "settled"), expired]);
978
+ } finally {
979
+ if (timer !== void 0) clearTimeout(timer);
980
+ }
981
+ }
982
+ /**
983
+ * Release every waiter and forget every in-flight cut — re-dial and teardown.
984
+ * A waiter left behind here would outlive the worker that could settle it.
985
+ */
986
+ clear() {
987
+ for (const cut of this.cuts.values()) cut.finish();
988
+ this.cuts.clear();
989
+ }
990
+ /**
991
+ * The `percentile`-th cut latency in ms over the retained samples, or `null`
992
+ * when nothing has completed yet. `null` rather than 0: a worker that has cut
993
+ * nothing and a worker that cuts instantly must not read the same.
994
+ */
995
+ latencyMs(percentile) {
996
+ if (this.latencies.length === 0) return null;
997
+ const sorted = [...this.latencies].sort((a, b) => a - b);
998
+ const clamped = Math.min(100, Math.max(0, percentile));
999
+ const index = Math.min(sorted.length - 1, Math.ceil(clamped / 100 * sorted.length) - 1);
1000
+ return sorted[Math.max(0, index)] ?? null;
1001
+ }
1002
+ noteLatency(ms) {
1003
+ this.latencies.push(Math.max(0, ms));
1004
+ while (this.latencies.length > this.latencySampleCap) this.latencies.shift();
1005
+ }
1006
+ };
1007
+ /**
1008
+ * Serve a frame's retained tiles, waiting for an in-flight cut ONLY when
1009
+ * waiting could change the answer.
1010
+ *
1011
+ * The order is the whole point. Anything already retained is served at once —
1012
+ * a cut in flight for a LATER subject of the same frame must not delay a
1013
+ * request the stores can already answer. Only an empty store consults
1014
+ * {@link PendingCuts}, and only then is a success attributable to the wait.
1015
+ */
1016
+ async function attemptTileServe(pending, frameId, source) {
1017
+ if (source.hasRetained()) return {
1018
+ served: await source.serve(),
1019
+ wait: "none",
1020
+ resolvedByWait: false
1021
+ };
1022
+ const wait = await pending.waitFor(frameId);
1023
+ const served = await source.serve();
1024
+ return {
1025
+ served,
1026
+ wait,
1027
+ resolvedByWait: served !== null && wait === "settled"
1028
+ };
1029
+ }
923
1030
  //#endregion
924
1031
  //#region src/session-decode/planar-crop-source.ts
925
1032
  /** Layout a detection `toBuffer` can read, or `null` when it must name the
@@ -949,6 +1056,19 @@ function nativeCropSourceTag(format) {
949
1056
  case "rgb24": return "rgb:";
950
1057
  }
951
1058
  }
1059
+ /**
1060
+ * Bytes a crop of `rows` rows actually reads, measured from the crop's ORIGIN.
1061
+ *
1062
+ * The LAST row needs only its own `rowBytes`, never a whole stride — the stride
1063
+ * is the distance to the NEXT row, and for the last row there is no next row.
1064
+ * Demanding `stride * rows` instead overshoots the plane's end by exactly the
1065
+ * horizontal offset, which is invisible everywhere except where the crop's
1066
+ * bottom IS the frame's bottom. See {@link resolvePlanarCropSource}.
1067
+ */
1068
+ function planarSpan(rows, stride, rowBytes) {
1069
+ if (rows <= 0) return 0;
1070
+ return (rows - 1) * stride + rowBytes;
1071
+ }
952
1072
  /** A plane view starting at `offset`, or `null` when the plane is too short. */
953
1073
  function viewAt(plane, offset, requiredSpan) {
954
1074
  if (!plane) return null;
@@ -967,6 +1087,28 @@ function viewAt(plane, offset, requiredSpan) {
967
1087
  * An odd origin is floored onto the chroma grid rather than rejected: the
968
1088
  * geometry helper already aligns to even, and flooring reads the chroma sample
969
1089
  * that CONTAINS the origin instead of inventing a half sample.
1090
+ *
1091
+ * ## The bottom edge (2026-09-07)
1092
+ *
1093
+ * The span demanded from each plane used to be `stride * region.height` from
1094
+ * the crop ORIGIN. A stride is the distance to the NEXT row, and the last row
1095
+ * of a crop has no next row — it needs only `left + width` bytes. So the demand
1096
+ * overshot the plane's end by exactly `left`, which is invisible everywhere
1097
+ * except when the crop's bottom IS the frame's last row. `resolveNativeTileRegion`
1098
+ * clamps a padded box to precisely that, and `clampEven` floors both `top` and
1099
+ * `height`, so a bottom-clamped tile lands flush every time.
1100
+ *
1101
+ * The result was that every subject standing near the camera — the biggest, the
1102
+ * closest, the best shot — lost its native tile, and the hold path threw for
1103
+ * the same rectangles. Measured over 6 h on the live cluster: **100 of 100**
1104
+ * logged `missing plane data` geometries had `top + height === frameHeight`
1105
+ * with `left > 0`, 101 of 102 on dev:590 (salone, 3840x2160).
1106
+ *
1107
+ * The check is now the region's real extent, so a crop that genuinely runs past
1108
+ * a plane — one row too tall, or a last row reaching past the stride — is still
1109
+ * refused. The views handed back are unchanged: `subarray(offset)` always ran
1110
+ * to the plane's true end, so nothing here ever sized a buffer, and tightening
1111
+ * the predicate cannot shorten one.
970
1112
  */
971
1113
  function resolvePlanarCropSource(kind, planes, strides, region) {
972
1114
  if (!planes || planes.length < PLANE_COUNT[kind]) return null;
@@ -977,10 +1119,11 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
977
1119
  const left = Math.max(0, region.left);
978
1120
  const chromaRow = Math.floor(top / 2);
979
1121
  const chromaHeight = Math.ceil(region.height / 2);
980
- const y = viewAt(planes[0], top * yStride + left, yStride * region.height);
1122
+ const chromaWidth = Math.ceil(Math.max(0, region.width) / 2);
1123
+ const y = viewAt(planes[0], top * yStride + left, planarSpan(region.height, yStride, region.width));
981
1124
  if (kind === "nv12") {
982
1125
  const uvOffset = chromaRow * cStride + Math.floor(left / 2) * 2;
983
- const uv = viewAt(planes[1], uvOffset, cStride * chromaHeight);
1126
+ const uv = viewAt(planes[1], uvOffset, planarSpan(chromaHeight, cStride, chromaWidth * 2));
984
1127
  if (!y || !uv) return null;
985
1128
  return {
986
1129
  planes: [y, uv],
@@ -989,8 +1132,8 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
989
1132
  }
990
1133
  const vStride = strides?.[2] ?? cStride;
991
1134
  const chromaOffset = chromaRow * cStride + Math.floor(left / 2);
992
- const u = viewAt(planes[1], chromaOffset, cStride * chromaHeight);
993
- const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), vStride * chromaHeight);
1135
+ const u = viewAt(planes[1], chromaOffset, planarSpan(chromaHeight, cStride, chromaWidth));
1136
+ const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), planarSpan(chromaHeight, vStride, chromaWidth));
994
1137
  if (!y || !u || !v) return null;
995
1138
  return {
996
1139
  planes: [
@@ -1330,6 +1473,20 @@ async function cutSubjectTiles(frame, bboxes, ports) {
1330
1473
  return tiles;
1331
1474
  }
1332
1475
  //#endregion
1476
+ //#region src/session-decode/to-buffer-miss.ts
1477
+ /** Which rung lost the frame, from what the worker knows at the miss. */
1478
+ function toBufferMissRung(facts) {
1479
+ if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
1480
+ if (!facts.marked) return "never-leased";
1481
+ if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
1482
+ return "released";
1483
+ }
1484
+ /** The error line the runner sees: the rung first, the numbers after it. */
1485
+ function describeToBufferMiss(facts) {
1486
+ const rung = toBufferMissRung(facts);
1487
+ 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}` : ""}}`;
1488
+ }
1489
+ //#endregion
1333
1490
  //#region src/session-decode/decode-worker-child.ts
1334
1491
  /** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
1335
1492
  function toLeasedNavFrame(frame) {
@@ -1457,6 +1614,28 @@ var NATIVE_TILE_MAX_PER_FRAME = (() => {
1457
1614
  /** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
1458
1615
  var NATIVE_TILE_JPEG_QUALITY = 90;
1459
1616
  /**
1617
+ * How long a native-crop request waits for a tile cut that is still ENCODING.
1618
+ *
1619
+ * The cut reads its pixels synchronously at `FrameResult` time but only writes
1620
+ * the stores after N JPEG encodes; a request landing in between reads two empty
1621
+ * stores and is told `no retained tile for this frame` — the same sentence a
1622
+ * frame that was never cut gets. Measured 2026-09-07: misses onset at ~100 ms
1623
+ * (only 0.1% below it) and peak at 500-1000 ms, on cameras that cut a tile for
1624
+ * 99.7% of their inferred frames, so the window — not any lifetime — is what
1625
+ * they are landing in.
1626
+ *
1627
+ * 500 ms deliberately, not more: the confirmation gate bounds each crop attempt
1628
+ * at 300 ms and the capture scheduler drops an enrichment request older than
1629
+ * 3 s, so a miss held too long stops being a fast retryable `null` and becomes
1630
+ * an upstream timeout. Widen it via `CAMSTACK_SESSION_TILE_CUT_WAIT_MS` only
1631
+ * once `cutLatencyMs` on the metrics line says what the encode window really
1632
+ * is; `0` disables the wait and restores the pre-2026-09-07 miss profile.
1633
+ */
1634
+ var TILE_CUT_WAIT_MS = (() => {
1635
+ const raw = Number(process.env["CAMSTACK_SESSION_TILE_CUT_WAIT_MS"]);
1636
+ return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 500;
1637
+ })();
1638
+ /**
1460
1639
  * Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
1461
1640
  * per-frame native downloads run only within this window of the last
1462
1641
  * native-crop request (or dial start). `0` disables the GATE (legacy always-on
@@ -1597,6 +1776,14 @@ function errMessage(err) {
1597
1776
  return String(err);
1598
1777
  }
1599
1778
  /**
1779
+ * Render a cut-latency percentile for the metrics line. `-` when nothing has
1780
+ * been cut yet: a worker that cut nothing and one that cuts in 0 ms must not
1781
+ * read the same, or the number that sizes {@link TILE_CUT_WAIT_MS} is a guess.
1782
+ */
1783
+ function formatLatency(ms) {
1784
+ return ms === null ? "-" : String(Math.round(ms));
1785
+ }
1786
+ /**
1600
1787
  * RTSP low-latency demuxer options — mirrors
1601
1788
  * `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
1602
1789
  * than cross-imported: this file is a standalone forked entry point, not an
@@ -1801,6 +1988,19 @@ var DecodeWorkerChild = class {
1801
1988
  tileHits = 0;
1802
1989
  tileMisses = 0;
1803
1990
  tileCutFailures = 0;
1991
+ /**
1992
+ * Serves that produced pixels ONLY because the request waited for a cut that
1993
+ * was still encoding — i.e. misses before {@link TILE_CUT_WAIT_MS} existed.
1994
+ *
1995
+ * This is the counter the write-gap hypothesis stands or falls on. If it
1996
+ * stays near zero while `tileMisses` does not move, the misses were never a
1997
+ * timing race and the question becomes WHICH frames get cut at all.
1998
+ */
1999
+ tileMissesResolvedByWait = 0;
2000
+ /** Requests that gave up on an in-flight cut at the bound — dropped work. */
2001
+ tileWaitTimeouts = 0;
2002
+ /** Cuts between "I have the pixels" and "the stores can answer". */
2003
+ pendingCuts = new PendingCuts({ waitMs: TILE_CUT_WAIT_MS });
1804
2004
  /** Scene tiles cut / served, on the throughput line. */
1805
2005
  scenesCut = 0;
1806
2006
  sceneHits = 0;
@@ -2057,7 +2257,7 @@ var DecodeWorkerChild = class {
2057
2257
  const skipped = this.framesSkipped + this.frames.droppedCount;
2058
2258
  const rssMb = Math.round(process.memoryUsage().rss / 1048576);
2059
2259
  const ageS = Math.round((now - this.startedAt) / 1e3);
2060
- 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`);
2260
+ 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`);
2061
2261
  this.lastMetricsAt = now;
2062
2262
  this.lastDeliveredSnapshot = this.framesDelivered;
2063
2263
  if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
@@ -2209,10 +2409,16 @@ var DecodeWorkerChild = class {
2209
2409
  */
2210
2410
  async serveTileFallback(requestId, frameId, bbox, maxWidth) {
2211
2411
  try {
2212
- const served = await this.serveFromTile(frameId, bbox, maxWidth);
2412
+ const attempt = await attemptTileServe(this.pendingCuts, frameId, {
2413
+ hasRetained: () => this.tileStore.get(frameId).length > 0 || this.sceneStore.get(frameId).length > 0,
2414
+ serve: () => this.serveFromTile(frameId, bbox, maxWidth)
2415
+ });
2416
+ const served = attempt.served;
2417
+ if (attempt.wait === "timeout") this.tileWaitTimeouts += 1;
2213
2418
  if (served) {
2214
2419
  this.tileHits += 1;
2215
2420
  this.nativeCropHits++;
2421
+ if (attempt.resolvedByWait) this.tileMissesResolvedByWait += 1;
2216
2422
  this.send({
2217
2423
  kind: "nativeCropResult",
2218
2424
  requestId,
@@ -2228,7 +2434,7 @@ var DecodeWorkerChild = class {
2228
2434
  this.send({
2229
2435
  kind: "nativeCropMiss",
2230
2436
  requestId,
2231
- reason: this.tileMissReason(frameId)
2437
+ reason: this.tileMissReason(frameId, attempt.wait)
2232
2438
  });
2233
2439
  } catch (err) {
2234
2440
  this.tileMisses += 1;
@@ -2248,7 +2454,8 @@ var DecodeWorkerChild = class {
2248
2454
  * "the rectangle is outside every container" are three different sentences,
2249
2455
  * and one "not retained" hid all of them.
2250
2456
  */
2251
- tileMissReason(frameId) {
2457
+ tileMissReason(frameId, wait) {
2458
+ 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`;
2252
2459
  if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
2253
2460
  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`;
2254
2461
  if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
@@ -2280,6 +2487,7 @@ var DecodeWorkerChild = class {
2280
2487
  * explanation, which is the failure this whole redesign exists to remove.
2281
2488
  */
2282
2489
  async handleCutTiles(frameId, bboxes) {
2490
+ this.pendingCuts.begin(frameId);
2283
2491
  try {
2284
2492
  if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
2285
2493
  const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
@@ -2361,6 +2569,7 @@ var DecodeWorkerChild = class {
2361
2569
  this.sceneStore.put(frameId, [scene]);
2362
2570
  }
2363
2571
  } finally {
2572
+ this.pendingCuts.settle(frameId);
2364
2573
  this.noteLeaseRelease(frameId, this.leaseStore.release(frameId), "cut");
2365
2574
  }
2366
2575
  }
@@ -3143,6 +3352,7 @@ var DecodeWorkerChild = class {
3143
3352
  this.leaseStore.clear();
3144
3353
  this.tileStore.clear();
3145
3354
  this.sceneStore.clear();
3355
+ this.pendingCuts.clear();
3146
3356
  this.tileFrameWidth = 0;
3147
3357
  this.tileFrameHeight = 0;
3148
3358
  this.nativeLeaseDownloadFilter?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.201",
3
+ "version": "1.2.202",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",