@camstack/addon-post-analysis 1.2.213 → 1.2.216

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DA1sJKXM.js");
5
+ const require_dist = require("../dist-B_6UZCut.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -10,6 +10,7 @@ let node_path$1 = require_dist.__toESM(node_path, 1);
10
10
  node_path = require_dist.__toESM(node_path);
11
11
  let node_crypto = require("node:crypto");
12
12
  let node_child_process = require("node:child_process");
13
+ let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
13
14
  let sharp = require("sharp");
14
15
  sharp = require_dist.__toESM(sharp);
15
16
  let node_os = require("node:os");
@@ -3223,8 +3224,8 @@ async function raceTimeout(call, timeoutMs) {
3223
3224
  * detail the encoder can see.
3224
3225
  */
3225
3226
  async function downscaleJpeg(bytes, maxPx) {
3226
- const { default: sharp$15 } = await import("sharp");
3227
- const out = await sharp$15(Buffer.from(bytes)).resize({
3227
+ const { default: sharp$16 } = await import("sharp");
3228
+ const out = await sharp$16(Buffer.from(bytes)).resize({
3228
3229
  width: maxPx,
3229
3230
  height: maxPx,
3230
3231
  fit: "inside",
@@ -11740,8 +11741,8 @@ var NcDispatcher = class {
11740
11741
  try {
11741
11742
  const points = (await this.deps.getZonePolygons?.(deviceId, zoneIds) ?? []).flat();
11742
11743
  if (points.length === 0) return null;
11743
- const { default: sharp$14 } = await import("sharp");
11744
- const img = sharp$14(Buffer.from(jpeg));
11744
+ const { default: sharp$15 } = await import("sharp");
11745
+ const img = sharp$15(Buffer.from(jpeg));
11745
11746
  const meta = await img.metadata();
11746
11747
  const W = meta.width ?? 0;
11747
11748
  const H = meta.height ?? 0;
@@ -14920,7 +14921,7 @@ function buildOccupancyStatus(input) {
14920
14921
  * `this.byId.clear()` happened to sit AFTER the `await` that threw. Nothing
14921
14922
  * said so, and hoisting one line would have silently turned every transient
14922
14923
  * store error into a hub that stops notifying — the reversal behind the D130
14923
- * flood and the `3f9345fc3` occupancy cold-seed loss. The ledger's contract
14924
+ * flood and the occupancy cold-seed loss. The ledger's contract
14924
14925
  * rule 2 (a failed load NEVER clears the mirror, deliberately no knob) makes
14925
14926
  * that reversal unexpressible here. `rule-store-load-failure-direction.spec.ts`
14926
14927
  * holds the direction and records what was removed to make it red.
@@ -18137,7 +18138,7 @@ function clockOf$2(atMs) {
18137
18138
  *
18138
18139
  * ## The `.partial()` trap this file exists to avoid
18139
18140
  *
18140
- * Commit `e0921ac6e`, found in production 2026-08-12: `NcRulePatchSchema` was
18141
+ * Found in production 2026-08-12: `NcRulePatchSchema` was
18141
18142
  * `NcRuleInputSchema.partial()`, and Zod 4 `.partial()` KEEPS the inner
18142
18143
  * `.default()`. Parsing `{ name: 'x' }` therefore materialised every defaulted
18143
18144
  * key, and `{ ...existing, ...patch }` wrote them over the rule — a camera- and
@@ -19032,8 +19033,8 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
19032
19033
  ];
19033
19034
  if (bbox === void 0) return files;
19034
19035
  try {
19035
- const { default: sharp$13 } = await import("sharp");
19036
- const meta = await sharp$13(Buffer.from(jpeg)).metadata();
19036
+ const { default: sharp$14 } = await import("sharp");
19037
+ const meta = await sharp$14(Buffer.from(jpeg)).metadata();
19037
19038
  const W = meta.width ?? 0;
19038
19039
  const H = meta.height ?? 0;
19039
19040
  if (W === 0 || H === 0) return files;
@@ -19041,7 +19042,7 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
19041
19042
  const top = Math.max(0, Math.min(H - 1, Math.floor(bbox.y * H)));
19042
19043
  const width = Math.max(1, Math.min(W - left, Math.ceil(bbox.w * W)));
19043
19044
  const height = Math.max(1, Math.min(H - top, Math.ceil(bbox.h * H)));
19044
- const cropBuf = await sharp$13(Buffer.from(jpeg)).extract({
19045
+ const cropBuf = await sharp$14(Buffer.from(jpeg)).extract({
19045
19046
  left,
19046
19047
  top,
19047
19048
  width,
@@ -19063,7 +19064,7 @@ async function deriveSyntheticMedia(jpeg, bbox, timestamp) {
19063
19064
  });
19064
19065
  const stroke = Math.max(2, Math.round(Math.min(W, H) / 240));
19065
19066
  const overlay = Buffer.from(`<svg width="${W}" height="${H}"><rect x="${left}" y="${top}" width="${width}" height="${height}" fill="none" stroke="#22d3ee" stroke-width="${stroke}"/></svg>`);
19066
- const boxedBuf = await sharp$13(Buffer.from(jpeg)).composite([{
19067
+ const boxedBuf = await sharp$14(Buffer.from(jpeg)).composite([{
19067
19068
  input: overlay,
19068
19069
  top: 0,
19069
19070
  left: 0
@@ -34905,6 +34906,8 @@ var SortTracker = class SortTracker {
34905
34906
  refusalSampler = new SpawnRefusalSampler();
34906
34907
  /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34907
34908
  spawnRefusalObserver = null;
34909
+ /** Unsampled copy for the frame path — see `setSpawnRefusalSink`. */
34910
+ refusalSink = null;
34908
34911
  /** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
34909
34912
  spawnBesideSampler = new SpawnBesideCoastingSampler();
34910
34913
  /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
@@ -35017,11 +35020,7 @@ var SortTracker = class SortTracker {
35017
35020
  }
35018
35021
  /** Emit one dropped-detection record, subject to the refusal sampler. */
35019
35022
  reportRefusal(reason, det, extra = {}) {
35020
- const observer = this.spawnRefusalObserver;
35021
- if (!observer) return;
35022
- const refusalCount = this.refusalSampler.next(reason);
35023
- if (refusalCount === void 0) return;
35024
- observer({
35023
+ const base = {
35025
35024
  reason,
35026
35025
  detClass: det.class,
35027
35026
  detScore: det.score,
@@ -35033,13 +35032,31 @@ var SortTracker = class SortTracker {
35033
35032
  againstTrackId: extra.against.id,
35034
35033
  againstTrackClass: extra.against.class
35035
35034
  } : {},
35036
- refusalCount,
35037
- sampledEvery: 10,
35038
35035
  ...extra.droppedTrackId !== void 0 ? { droppedTrackId: extra.droppedTrackId } : {},
35039
35036
  ...extra.trackLifeMs !== void 0 ? { trackLifeMs: extra.trackLifeMs } : {}
35037
+ };
35038
+ this.refusalSink?.(base);
35039
+ const observer = this.spawnRefusalObserver;
35040
+ if (!observer) return;
35041
+ const refusalCount = this.refusalSampler.next(reason);
35042
+ if (refusalCount === void 0) return;
35043
+ observer({
35044
+ ...base,
35045
+ refusalCount,
35046
+ sampledEvery: 10
35040
35047
  });
35041
35048
  }
35042
35049
  /**
35050
+ * Wire the UNSAMPLED refusal sink — the frame path's copy.
35051
+ *
35052
+ * Separate from {@link setSpawnRefusalObserver} because the two want
35053
+ * different things from the same record: the observer wants a readable log
35054
+ * and accepts a sample; this decides whether an EVENT exists.
35055
+ */
35056
+ setSpawnRefusalSink(sink) {
35057
+ this.refusalSink = sink;
35058
+ }
35059
+ /**
35043
35060
  * Report a TRACK that ends having emitted nothing — the last silent branch
35044
35061
  * on the detection→position path (D391).
35045
35062
  *
@@ -36133,6 +36150,20 @@ var FrameProcessor = class {
36133
36150
  /** Sink + sampler for the drops this stage owns (the detection-stage zone
36134
36151
  * filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
36135
36152
  spawnRefusalObserver = null;
36153
+ /** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
36154
+ spawnRefusalForward = null;
36155
+ /**
36156
+ * The spawn refusals of the frame being processed.
36157
+ *
36158
+ * Filled by the same sink that feeds the diagnostic log, and drained at the
36159
+ * end of `process`. It exists because a refused detection inside a watched
36160
+ * zone still has to become an EVENT: an immediate zone rule asks "is there an
36161
+ * animal in the flowerbed now?", which does not need "is it the same animal
36162
+ * as before?" answered first. See `zone-detection-emits-without-a-track`.
36163
+ */
36164
+ refusalsThisFrame = [];
36165
+ /** Running count of events this path has produced — the volume measure. */
36166
+ zoneRefusalEventCount = 0;
36136
36167
  zoneRefusalSampler = new SpawnRefusalSampler();
36137
36168
  /** Optional stationary-object gate (parked-object suppression). Null until
36138
36169
  * the addon wires it via {@link setStationaryGate}. */
@@ -36147,6 +36178,9 @@ var FrameProcessor = class {
36147
36178
  this.deviceId = deviceId;
36148
36179
  this.source = source;
36149
36180
  this.tracker = new SortTracker(trackerConfig);
36181
+ this.tracker.setSpawnRefusalSink((record) => {
36182
+ this.refusalsThisFrame.push(record);
36183
+ });
36150
36184
  this.stateAnalyzer = new StateAnalyzer(stateConfig);
36151
36185
  this.eventEmitter = new DetectionEventEmitter(emitterConfig);
36152
36186
  this.zones = [];
@@ -36253,6 +36287,55 @@ var FrameProcessor = class {
36253
36287
  * ate it. Sampled like every other refusal: an excluded box is excluded on
36254
36288
  * every frame of a passage.
36255
36289
  */
36290
+ /**
36291
+ * Turn this frame's refused spawns into events, but only those inside a zone.
36292
+ *
36293
+ * Drains {@link refusalsThisFrame}. Each surviving record becomes an
36294
+ * `object.detected` event with NO track — carrying the zones it fell in, the
36295
+ * percentage of the box inside each, and the refusal reason so a consumer can
36296
+ * tell it apart from a track whose media went missing.
36297
+ *
36298
+ * `zone-filtered` refusals are excluded on purpose: that reason means the
36299
+ * operator's own detection rules already said the box does not count on this
36300
+ * camera, and re-admitting it here would overrule a decision they made.
36301
+ */
36302
+ eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36303
+ const refusals = this.refusalsThisFrame;
36304
+ this.refusalsThisFrame = [];
36305
+ if (refusals.length === 0 || this.zones.length === 0) return [];
36306
+ const out = [];
36307
+ for (const r of refusals) {
36308
+ if (r.reason === "zone-filtered") continue;
36309
+ const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36310
+ if (memberships.length === 0) continue;
36311
+ this.zoneRefusalEventCount += 1;
36312
+ out.push({
36313
+ id: (0, node_crypto.randomUUID)(),
36314
+ kind: "object",
36315
+ deviceId: this.deviceId,
36316
+ timestamp,
36317
+ frameId: frame.frameId,
36318
+ className: r.detClass,
36319
+ confidence: r.detScore,
36320
+ bbox: r.bbox,
36321
+ frameWidth,
36322
+ frameHeight,
36323
+ zones: memberships.map((m) => m.zoneId),
36324
+ zoneOverlaps: memberships.map((m) => ({
36325
+ zoneId: m.zoneId,
36326
+ ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36327
+ overlapPct: Math.round(m.overlap * 1e3) / 10
36328
+ })),
36329
+ spawnRefused: r.reason
36330
+ });
36331
+ }
36332
+ return out;
36333
+ }
36334
+ /** How many zone-refusal events this processor has emitted — the measure that
36335
+ * says whether this path is a trickle or a storm. Read by the addon. */
36336
+ zoneRefusalEventsEmitted() {
36337
+ return this.zoneRefusalEventCount;
36338
+ }
36256
36339
  reportZoneFiltered(excluded) {
36257
36340
  const observer = this.spawnRefusalObserver;
36258
36341
  if (!observer || excluded.length === 0) return;
@@ -36654,9 +36737,11 @@ var FrameProcessor = class {
36654
36737
  frameHeight
36655
36738
  };
36656
36739
  };
36740
+ const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36657
36741
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36658
36742
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36659
36743
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36744
+ objectEvents.push(...zoneRefusalEvents);
36660
36745
  return {
36661
36746
  deviceId: this.deviceId,
36662
36747
  timestamp,
@@ -47319,6 +47404,7 @@ var TrackDetailDispatcher = class {
47319
47404
  if (!dev) return;
47320
47405
  dev.scheduler.onTrackEnded(trackId);
47321
47406
  dev.tracks.delete(trackId);
47407
+ dev.parcelEnriched.delete(trackId);
47322
47408
  dev.candidateBest.delete(trackId);
47323
47409
  if (dev.tracks.size === 0) this.clearTimer(dev);
47324
47410
  }
@@ -47328,6 +47414,7 @@ var TrackDetailDispatcher = class {
47328
47414
  this.clearTimer(dev);
47329
47415
  dev.queue.length = 0;
47330
47416
  dev.tracks.clear();
47417
+ dev.parcelEnriched.clear();
47331
47418
  dev.candidateBest.clear();
47332
47419
  }
47333
47420
  this.devices.clear();
@@ -47338,6 +47425,7 @@ var TrackDetailDispatcher = class {
47338
47425
  dev = {
47339
47426
  scheduler: new DetailScheduler(),
47340
47427
  tracks: /* @__PURE__ */ new Map(),
47428
+ parcelEnriched: /* @__PURE__ */ new Map(),
47341
47429
  candidateBest: new BestDetectionTracker(CANDIDATE_IMPROVE_POLICY),
47342
47430
  queue: [],
47343
47431
  coastedFramesIgnored: 0,
@@ -47454,19 +47542,21 @@ var TrackDetailDispatcher = class {
47454
47542
  *
47455
47543
  * 1. `frameHandle` — cheapest when it hits: no payload moves and the runner's
47456
47544
  * leaf children re-cut their ROI at native resolution. Tried first, always.
47457
- * 2. `frameJpeg` — the freshest frame's FULL raster, fetched from the node that
47458
- * owns the handle and handed to the runner UNCUT, so the rectangle still
47459
- * comes from the single derivation (D52). This rung exists because rung 1 is
47460
- * structurally unreachable for most dispatches: the decode worker's native
47461
- * lease is 1.2 s and a tick + inference routinely lands outside it, while
47462
- * the retained store holds the frame for 3 s.
47545
+ * 2. `frameJpeg` — the track's PARKED key frame (D422): the full native frame
47546
+ * the owning node copied at the best instant, retrieved by track and handed
47547
+ * to the runner UNCUT, so the rectangle still comes from the single
47548
+ * derivation (D52). This rung exists because rung 1 is structurally
47549
+ * unreachable for most dispatches a tick + inference routinely lands
47550
+ * outside the lease and it cannot miss on age. It runs ONCE per parcel
47551
+ * per step; a dispatch that finds the same parcel already enriched skips
47552
+ * it, so a coasting track does not re-embed the same image every second.
47463
47553
  * 3. `cropJpeg` — the pre-cut tile. Last, because the caller decided its
47464
47554
  * rectangle and because in practice the same native miss that emptied rung 1
47465
47555
  * empties this one too.
47466
47556
  *
47467
- * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
47468
- * demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
47469
- * rung 1 missed — never a frame-rate stream, and never raw pixels.
47557
+ * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG retrieved
47558
+ * on demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only
47559
+ * after rung 1 missed — never a frame-rate stream, and never raw pixels.
47470
47560
  *
47471
47561
  * ONE outcome stops the ladder cold: the runner's typed `not-attached`
47472
47562
  * refusal (`detail-refusal.ts`). It names the camera's SESSION state, not
@@ -47504,34 +47594,39 @@ var TrackDetailDispatcher = class {
47504
47594
  }
47505
47595
  });
47506
47596
  }
47507
- if (this.deps.captureFullFrameBase64 !== void 0 && frame.frameHandle !== void 0) try {
47508
- const full = await this.deps.captureFullFrameBase64(frame);
47509
- if (full !== null) if (full.width !== frame.frameWidth || full.height !== frame.frameHeight) this.deps.logger.warn("detail full-frame fallback refused frame is not the bbox scale", {
47597
+ if (this.deps.captureParkedFrame !== void 0) try {
47598
+ const parked = await this.deps.captureParkedFrame(deviceId, req.trackId);
47599
+ if (parked !== null) if (dev.parcelEnriched.get(req.trackId)?.get(req.stepId) === parked.timestamp) this.deps.logger.debug("detail parcel already enriched for this step skipping", {
47510
47600
  tags: { deviceId },
47511
47601
  meta: {
47512
47602
  trackId: req.trackId,
47513
47603
  stepId: req.stepId,
47514
- frameWidth: full.width,
47515
- frameHeight: full.height,
47516
- bboxFrameWidth: frame.frameWidth,
47517
- bboxFrameHeight: frame.frameHeight
47604
+ parkedTimestamp: parked.timestamp
47518
47605
  }
47519
47606
  });
47520
47607
  else {
47521
47608
  const derived = await this.deps.runDetailSubtree({
47522
47609
  deviceId,
47523
- frameJpeg: full.base64,
47524
- parent,
47610
+ frameJpeg: parked.base64,
47611
+ parent: {
47612
+ bbox: { ...parked.bbox },
47613
+ className: frame.className
47614
+ },
47525
47615
  steps
47526
- }, frame.nodeId);
47527
- if (derived !== null) return derived.details;
47616
+ }, parked.nodeId);
47617
+ if (derived !== null) {
47618
+ const perStep = dev.parcelEnriched.get(req.trackId) ?? /* @__PURE__ */ new Map();
47619
+ perStep.set(req.stepId, parked.timestamp);
47620
+ dev.parcelEnriched.set(req.trackId, perStep);
47621
+ return derived.details;
47622
+ }
47528
47623
  }
47529
47624
  } catch (err) {
47530
47625
  if (isDetailNotAttachedRefusal(err)) {
47531
47626
  this.warnNotAttachedThrottled(deviceId, dev, req);
47532
47627
  return null;
47533
47628
  }
47534
- this.deps.logger.debug("detail full-frame fallback call failed", {
47629
+ this.deps.logger.debug("detail parked-frame rung failed", {
47535
47630
  tags: { deviceId },
47536
47631
  meta: {
47537
47632
  trackId: req.trackId,
@@ -47598,7 +47693,7 @@ var TrackDetailDispatcher = class {
47598
47693
  trackId: req.trackId,
47599
47694
  stepId: req.stepId,
47600
47695
  reason: req.reason,
47601
- triedFullFrame: this.deps.captureFullFrameBase64 !== void 0,
47696
+ triedParkedFrame: this.deps.captureParkedFrame !== void 0,
47602
47697
  triedCropJpeg: this.deps.captureCropBase64 !== void 0
47603
47698
  }
47604
47699
  });
@@ -49560,6 +49655,87 @@ var FaceRecognizer = class {
49560
49655
  }
49561
49656
  }
49562
49657
  };
49658
+ /** Read the window. Pure — this is the judgement the module exists to make. */
49659
+ function lagVerdict(report) {
49660
+ const bus = report.busLagMaxMs;
49661
+ if (bus === null) return "unknown";
49662
+ return bus >= 5e3 ? "starved" : "delivered-on-time";
49663
+ }
49664
+ /** Does this window need an operator's eyes? Unknown never counts as alarming
49665
+ * — that is the fabricated verdict D8 forbids. */
49666
+ function isAlarmingLagReport(report) {
49667
+ const bus = report.busLagMaxMs;
49668
+ const capture = report.captureLagMaxMs;
49669
+ return bus !== null && bus >= 5e3 || capture !== null && capture >= 5e3;
49670
+ }
49671
+ /**
49672
+ * Folds per-frame delivery lag into per-device windows.
49673
+ *
49674
+ * Not a logger: it returns the line to write (or null) and the caller decides
49675
+ * level and tags. That keeps the throttle — the part that can wreck the frame
49676
+ * path — testable without a logger fake.
49677
+ */
49678
+ var FrameLagObserver = class {
49679
+ windows = /* @__PURE__ */ new Map();
49680
+ /**
49681
+ * Fold one inference result into `deviceId`'s window.
49682
+ *
49683
+ * @param now Receipt time, read by the caller once at handler entry.
49684
+ * @param emittedAt Runner-side emit stamp; undefined ⇒ unknown (D8).
49685
+ * @param capturedAt Frame shm-commit stamp; undefined ⇒ unknown.
49686
+ * @returns The closed window when this frame ends one, else null.
49687
+ */
49688
+ observe(deviceId, now, emittedAt, capturedAt) {
49689
+ const existing = this.windows.get(deviceId);
49690
+ const w = existing ?? {
49691
+ startedAt: now,
49692
+ frames: 0,
49693
+ busMax: null,
49694
+ busMin: null,
49695
+ busUnknown: 0,
49696
+ captureMax: null,
49697
+ captureUnknown: 0
49698
+ };
49699
+ if (existing === void 0) this.windows.set(deviceId, w);
49700
+ w.frames += 1;
49701
+ if (typeof emittedAt === "number") {
49702
+ const busLag = now - emittedAt;
49703
+ if (w.busMax === null || busLag > w.busMax) w.busMax = busLag;
49704
+ if (w.busMin === null || busLag < w.busMin) w.busMin = busLag;
49705
+ } else w.busUnknown += 1;
49706
+ if (typeof capturedAt === "number") {
49707
+ const captureLag = now - capturedAt;
49708
+ if (w.captureMax === null || captureLag > w.captureMax) w.captureMax = captureLag;
49709
+ } else w.captureUnknown += 1;
49710
+ if (existing !== void 0 && now - w.startedAt < 3e4) return null;
49711
+ const report = {
49712
+ deviceId,
49713
+ frames: w.frames,
49714
+ busLagMaxMs: w.busMax,
49715
+ busLagMinMs: w.busMin,
49716
+ busLagUnknownFrames: w.busUnknown,
49717
+ captureLagMaxMs: w.captureMax,
49718
+ captureLagUnknownFrames: w.captureUnknown,
49719
+ windowMs: now - w.startedAt
49720
+ };
49721
+ w.startedAt = now;
49722
+ w.frames = 0;
49723
+ w.busMax = null;
49724
+ w.busMin = null;
49725
+ w.busUnknown = 0;
49726
+ w.captureMax = null;
49727
+ w.captureUnknown = 0;
49728
+ return report;
49729
+ }
49730
+ /** Drop a device's window (unbound camera, shutdown). */
49731
+ forget(deviceId) {
49732
+ this.windows.delete(deviceId);
49733
+ }
49734
+ /** Drop every window. */
49735
+ clear() {
49736
+ this.windows.clear();
49737
+ }
49738
+ };
49563
49739
  //#endregion
49564
49740
  //#region src/pipeline-analytics/location-aware-media-storage.ts
49565
49741
  /**
@@ -53882,6 +54058,17 @@ async function encodeKeyFrameVariants(native) {
53882
54058
  })
53883
54059
  };
53884
54060
  }
54061
+ /**
54062
+ * The web companion from the PARKED key frame's own JPEG (D422): the parcel is
54063
+ * stored verbatim as `keyFrame`, and `keyFrameSmall` is this — the same bytes
54064
+ * downscaled to {@link KEYFRAME_SMALL_MAX_WIDTH}. One decode, one encode,
54065
+ * never a second fetch; `withoutEnlargement` never upscales a sub-960 frame.
54066
+ * The by-handle twin ({@link encodeKeyFrameVariants}) remains for the
54067
+ * summary group shot, which still fetches a native surface by handle.
54068
+ */
54069
+ async function deriveKeyFrameSmall(keyFrameJpeg) {
54070
+ return (0, sharp.default)(keyFrameJpeg).resize(960, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
54071
+ }
53885
54072
  //#endregion
53886
54073
  //#region src/pipeline-analytics/pipeline/key-frame-metrics.ts
53887
54074
  var DEFAULT_FLUSH_INTERVAL_MS = 6e4;
@@ -53907,11 +54094,8 @@ var KeyFrameCaptureLog = class {
53907
54094
  w.landed += 1;
53908
54095
  if (sample.retry) w.retryLanded += 1;
53909
54096
  break;
53910
- case "native-miss":
53911
- w.nativeMiss += 1;
53912
- break;
53913
- case "ram-tier":
53914
- w.ramTier += 1;
54097
+ case "parcel-missing":
54098
+ w.parcelMissing += 1;
53915
54099
  break;
53916
54100
  case "error":
53917
54101
  w.error += 1;
@@ -53940,8 +54124,7 @@ var KeyFrameCaptureLog = class {
53940
54124
  landed: 0,
53941
54125
  retryAttempts: 0,
53942
54126
  retryLanded: 0,
53943
- nativeMiss: 0,
53944
- ramTier: 0,
54127
+ parcelMissing: 0,
53945
54128
  error: 0,
53946
54129
  skippedNodeOffline: 0
53947
54130
  };
@@ -53956,8 +54139,7 @@ var KeyFrameCaptureLog = class {
53956
54139
  landed: w.landed,
53957
54140
  retryAttempts: w.retryAttempts,
53958
54141
  retryLanded: w.retryLanded,
53959
- nativeMiss: w.nativeMiss,
53960
- ramTier: w.ramTier,
54142
+ parcelMissing: w.parcelMissing,
53961
54143
  error: w.error,
53962
54144
  skippedNodeOffline: w.skippedNodeOffline
53963
54145
  };
@@ -53966,63 +54148,44 @@ var KeyFrameCaptureLog = class {
53966
54148
  //#endregion
53967
54149
  //#region src/pipeline-analytics/pipeline/key-frame-persist.ts
53968
54150
  /**
53969
- * Fetch, encode and store one track's key frame + its 960 companion.
54151
+ * Read, derive and store one track's key frame + its web companion.
53970
54152
  *
53971
- * NATIVE-OR-NOTHING: a miss writes nothing and the caller's bounded retry
53972
- * re-fires later. The `keyFrame` write lands FIRST and its key is what the
53973
- * outcome carries — a `keyFrameSmall` failure afterwards must not re-arm a
54153
+ * PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
54154
+ * parks a later frame. The `keyFrame` write lands FIRST and its key is what
54155
+ * the outcome carries — a `keyFrameSmall` failure afterwards must not re-arm a
53974
54156
  * retry that would re-shoot a key frame the rebuild can already read.
53975
54157
  */
53976
54158
  async function persistTrackKeyFrame(deps, input) {
53977
- const { deviceId, trackId, frameHandle, retry } = input;
54159
+ const { deviceId, trackId, retry } = input;
53978
54160
  const drop = {
53979
54161
  tags: { deviceId },
53980
54162
  meta: {
53981
54163
  trackId,
53982
- nodeId: frameHandle.nodeId,
53983
54164
  retry
53984
54165
  }
53985
54166
  };
53986
54167
  try {
53987
- const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
53988
- const native = result?.frame;
53989
- if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
53990
- deps.logger.error("key-frame native miss — no upscale, will retry while budget remains", {
53991
- ...drop,
53992
- meta: {
53993
- ...drop.meta,
53994
- shmId: frameHandle.shmId
53995
- }
53996
- });
53997
- return { kind: "native-miss" };
54168
+ const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
54169
+ if (parcel === null || parcel.jpeg.byteLength === 0) {
54170
+ deps.logger.error("key-frame parcel missing nothing to write, retry parks a later frame", { ...drop });
54171
+ return { kind: "parcel-missing" };
53998
54172
  }
53999
- if (!keyFrameAcceptsTier(result.tier)) {
54000
- deps.logger.warn("keyFrame native miss — RAM tier rejected, will retry while budget remains", {
54001
- ...drop,
54002
- meta: {
54003
- ...drop.meta,
54004
- shmId: frameHandle.shmId,
54005
- tier: result.tier
54006
- }
54007
- });
54008
- return { kind: "ram-tier" };
54009
- }
54010
- const { keyFrame, keyFrameSmall } = await deps.encodeVariants(native);
54011
54173
  const keyFrameKey = await deps.media.putReplacing({
54012
54174
  deviceId,
54013
54175
  ownerKind: "track",
54014
54176
  ownerId: trackId,
54015
54177
  kind: "keyFrame",
54016
- timestamp: input.timestamp,
54017
- data: keyFrame
54178
+ timestamp: parcel.timestamp,
54179
+ data: parcel.jpeg
54018
54180
  });
54019
54181
  try {
54182
+ const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
54020
54183
  await deps.media.putReplacing({
54021
54184
  deviceId,
54022
54185
  ownerKind: "track",
54023
54186
  ownerId: trackId,
54024
54187
  kind: "keyFrameSmall",
54025
- timestamp: input.timestamp,
54188
+ timestamp: parcel.timestamp,
54026
54189
  data: keyFrameSmall
54027
54190
  });
54028
54191
  } catch (err) {
@@ -54030,17 +54193,29 @@ async function persistTrackKeyFrame(deps, input) {
54030
54193
  ...drop,
54031
54194
  meta: {
54032
54195
  ...drop.meta,
54196
+ nodeId: parcel.nodeId,
54033
54197
  error: require_dist.errMsg(err)
54034
54198
  }
54035
54199
  });
54036
54200
  }
54201
+ deps.logger.debug("key frame written from the parked parcel", {
54202
+ ...drop,
54203
+ meta: {
54204
+ ...drop.meta,
54205
+ nodeId: parcel.nodeId,
54206
+ width: parcel.width,
54207
+ height: parcel.height,
54208
+ bytes: parcel.jpeg.byteLength,
54209
+ parkedTimestamp: parcel.timestamp
54210
+ }
54211
+ });
54037
54212
  return {
54038
54213
  kind: "landed",
54039
54214
  keyFrameKey
54040
54215
  };
54041
54216
  } catch (err) {
54042
54217
  const error = require_dist.errMsg(err);
54043
- deps.logger.warn("key-frame capture failed", {
54218
+ deps.logger.warn("key-frame write failed", {
54044
54219
  ...drop,
54045
54220
  meta: {
54046
54221
  ...drop.meta,
@@ -54227,6 +54402,61 @@ function createKeyFrameCrop(deps) {
54227
54402
  };
54228
54403
  }
54229
54404
  //#endregion
54405
+ //#region src/pipeline-analytics/pipeline/track-park.ts
54406
+ /** The whole frame — what a `keyFrame` park cuts. */
54407
+ var FULL_FRAME = {
54408
+ x: 0,
54409
+ y: 0,
54410
+ w: 1,
54411
+ h: 1
54412
+ };
54413
+ /** The two parcels a best-frame decision orders, in a fixed order. */
54414
+ function buildParkOrders(decision) {
54415
+ const subject = {
54416
+ bbox: { ...decision.bbox },
54417
+ frameWidth: decision.frameWidth,
54418
+ frameHeight: decision.frameHeight
54419
+ };
54420
+ const layout = deliveredWideCentralSquareLayout(decision.bbox, {
54421
+ W: decision.frameWidth,
54422
+ H: decision.frameHeight
54423
+ });
54424
+ const base = {
54425
+ deviceId: decision.deviceId,
54426
+ trackId: decision.trackId,
54427
+ handle: decision.frameHandle,
54428
+ timestamp: decision.timestamp,
54429
+ subject
54430
+ };
54431
+ return [{
54432
+ ...base,
54433
+ kind: "keyFrame",
54434
+ bbox: FULL_FRAME,
54435
+ maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
54436
+ }, {
54437
+ ...base,
54438
+ kind: "thumbnail",
54439
+ bbox: layout.fetch,
54440
+ layout
54441
+ }];
54442
+ }
54443
+ /**
54444
+ * The subject's box in the PARCEL's pixel space. The box was measured on the
54445
+ * ≤640 detection raster; the parcel is native-resolution. Both show the same
54446
+ * scene at the same aspect, so a uniform per-axis scale places the box on the
54447
+ * parcel — the runner then derives the crop rectangle from it (D52).
54448
+ */
54449
+ function scaleSubjectIntoParcel(subject, parcelWidth, parcelHeight) {
54450
+ const sx = parcelWidth / subject.frameWidth;
54451
+ const sy = parcelHeight / subject.frameHeight;
54452
+ return {
54453
+ x: subject.bbox.x * sx,
54454
+ y: subject.bbox.y * sy,
54455
+ w: subject.bbox.w * sx,
54456
+ h: subject.bbox.h * sy
54457
+ };
54458
+ }
54459
+ //#endregion
54230
54460
  //#region src/pipeline-analytics/pipeline/native-detection.ts
54231
54461
  /**
54232
54462
  * Nominal frame size used to denormalize native `[0,1]` boxes when a
@@ -58757,7 +58987,28 @@ async function reconcileGallerySamples(deps, options) {
58757
58987
  }
58758
58988
  //#endregion
58759
58989
  //#region src/pipeline-analytics/summary/summary-store.ts
58990
+ /**
58991
+ * @durable class=ledger owner=pipeline-analytics
58992
+ * write="one row per SUMMARY — a window of co-present tracks on one camera
58993
+ * group. Opened by `openOrAbsorb` when the first member arrives, updated
58994
+ * in place while the window absorbs more, and finalised by `seal` when the
58995
+ * window closes. Per WINDOW, never per frame or per track: the sealed
58996
+ * summary is what a notification rule reads."
58997
+ * retention="age-keyed on `sealedAt`, swept with the rest of the analytics
58998
+ * retention. An unsealed row belongs to a window still open and is never
58999
+ * swept by age — it is closed or cascaded away with its members."
59000
+ */
58760
59001
  var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
59002
+ /**
59003
+ * @durable class=ledger owner=pipeline-analytics
59004
+ * write="one row per (summary, track) membership, written as a window
59005
+ * absorbs a track. The join that lets a sealed summary name its subjects
59006
+ * without re-reading the track store."
59007
+ * retention="cascaded — a member dies with its summary (`delete-track-cascade`
59008
+ * counts them as leaves) or with the track it names. It carries no clock of
59009
+ * its own: an age sweep here could outlive or predecease the summary the
59010
+ * row only means anything inside of."
59011
+ */
58761
59012
  var SUMMARY_MEMBERS_COLLECTION = "pipeline-analytics:summary-members";
58762
59013
  var SUMMARIES_COLUMNS = [
58763
59014
  {
@@ -63585,35 +63836,6 @@ function createNativeFrameTransport(deps) {
63585
63836
  const getRemoteFrame = async (handle) => requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame);
63586
63837
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame);
63587
63838
  const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
63588
- const getFullFrameJpegBase64 = async (handle, maxWidth) => {
63589
- if (!pipelineRunnerApi?.getNativeCrop) return null;
63590
- try {
63591
- const full = await pipelineRunnerApi.getNativeCrop.query({
63592
- handle,
63593
- bbox: {
63594
- x: 0,
63595
- y: 0,
63596
- w: 1,
63597
- h: 1
63598
- },
63599
- maxWidth,
63600
- encodeJpeg: true
63601
- }, require_dist.nodePin(handle.nodeId));
63602
- if (!full?.jpeg || full.width <= 0 || full.height <= 0) return null;
63603
- return {
63604
- base64: full.jpeg,
63605
- width: full.width,
63606
- height: full.height,
63607
- tier: full.tier === "ram-fullframe" ? "ram-fullframe" : "native"
63608
- };
63609
- } catch (err) {
63610
- logger.debug("full-frame jpeg fetch failed", { meta: {
63611
- nodeId: handle.nodeId,
63612
- error: require_dist.errMsg(err)
63613
- } });
63614
- return null;
63615
- }
63616
- };
63617
63839
  const cropMetricLogger = logger.child("NativeCrop");
63618
63840
  let nativeHits = 0;
63619
63841
  let nativeFallbacks = 0;
@@ -63693,7 +63915,6 @@ function createNativeFrameTransport(deps) {
63693
63915
  getRemoteFrame,
63694
63916
  getNativeFullFrameRgb,
63695
63917
  getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
63696
- getFullFrameJpegBase64,
63697
63918
  getNativeCropJpeg,
63698
63919
  captureCrop: createCaptureCrop({
63699
63920
  tryNativeCrop,
@@ -63804,6 +64025,233 @@ function parseTopologySnapshotNodes(data) {
63804
64025
  return parsed;
63805
64026
  }
63806
64027
  //#endregion
64028
+ //#region src/pipeline-analytics/runtime/parked-frame-client.ts
64029
+ /**
64030
+ * Bound on the mirror. A track's record leaves at release; this is the
64031
+ * backstop for a lost close. At the busiest measured hub (~12 sessions,
64032
+ * a few tracks each) 4 096 is an hour of tracks, past which the OLDEST record
64033
+ * is forgotten — its parcel still ages out on the node on its own.
64034
+ */
64035
+ var MAX_MIRRORED_TRACKS = 4096;
64036
+ /** One "runner has no park method" line per camera per minute. */
64037
+ var SKEW_WARN_THROTTLE_MS = 6e4;
64038
+ function inFlightKey(trackId, kind) {
64039
+ return `${trackId} ${kind}`;
64040
+ }
64041
+ /** Strip the hub-side context off an order: only the wire fields travel. */
64042
+ function toWireInput(order) {
64043
+ return {
64044
+ deviceId: order.deviceId,
64045
+ trackId: order.trackId,
64046
+ kind: order.kind,
64047
+ handle: order.handle,
64048
+ bbox: order.bbox,
64049
+ ...order.maxWidth !== void 0 ? { maxWidth: order.maxWidth } : {},
64050
+ timestamp: order.timestamp
64051
+ };
64052
+ }
64053
+ function createParkedFrameClient(deps) {
64054
+ const { api, logger } = deps;
64055
+ const now = deps.now ?? Date.now;
64056
+ /** trackId → kind → what the hub kept beside the parcel. */
64057
+ const mirror = /* @__PURE__ */ new Map();
64058
+ const inFlight = /* @__PURE__ */ new Map();
64059
+ const skewWarnedAt = /* @__PURE__ */ new Map();
64060
+ const remember = (order, answer) => {
64061
+ let kinds = mirror.get(order.trackId);
64062
+ if (kinds === void 0) {
64063
+ kinds = /* @__PURE__ */ new Map();
64064
+ mirror.set(order.trackId, kinds);
64065
+ while (mirror.size > MAX_MIRRORED_TRACKS) {
64066
+ const oldest = mirror.keys().next().value;
64067
+ if (oldest === void 0) break;
64068
+ mirror.delete(oldest);
64069
+ }
64070
+ }
64071
+ kinds.set(order.kind, {
64072
+ nodeId: answer.nodeId,
64073
+ deviceId: order.deviceId,
64074
+ kind: order.kind,
64075
+ timestamp: order.timestamp,
64076
+ width: answer.width,
64077
+ height: answer.height,
64078
+ subject: order.subject,
64079
+ ...order.layout !== void 0 ? { layout: order.layout } : {}
64080
+ });
64081
+ };
64082
+ const warnSkew = (order) => {
64083
+ const at = skewWarnedAt.get(order.deviceId) ?? 0;
64084
+ if (now() - at < SKEW_WARN_THROTTLE_MS) return;
64085
+ skewWarnedAt.set(order.deviceId, now());
64086
+ logger.warn("track frame park impossible — the runner api has no park method (version skew)", {
64087
+ tags: { deviceId: order.deviceId },
64088
+ meta: {
64089
+ trackId: order.trackId,
64090
+ kind: order.kind,
64091
+ nodeId: order.handle.nodeId
64092
+ }
64093
+ });
64094
+ };
64095
+ const sendPark = async (order) => {
64096
+ if (api === void 0) {
64097
+ warnSkew(order);
64098
+ return false;
64099
+ }
64100
+ if (deps.isNodeOffline?.(order.handle.nodeId) === true) {
64101
+ logger.debug("track frame park skipped — the owning node is offline in the registry", {
64102
+ tags: { deviceId: order.deviceId },
64103
+ meta: {
64104
+ trackId: order.trackId,
64105
+ kind: order.kind,
64106
+ nodeId: order.handle.nodeId
64107
+ }
64108
+ });
64109
+ return false;
64110
+ }
64111
+ const meta = {
64112
+ trackId: order.trackId,
64113
+ kind: order.kind,
64114
+ nodeId: order.handle.nodeId,
64115
+ handle: `${order.handle.shmId}#${order.handle.slot}#${order.handle.seq}`
64116
+ };
64117
+ try {
64118
+ const answer = await api.parkTrackFrame.mutate(toWireInput(order), require_dist.nodePin(order.handle.nodeId));
64119
+ if (answer.parked) {
64120
+ remember(order, answer);
64121
+ logger.debug("track frame parked", {
64122
+ tags: { deviceId: order.deviceId },
64123
+ meta: {
64124
+ ...meta,
64125
+ bytes: answer.bytes,
64126
+ replaced: answer.replaced
64127
+ }
64128
+ });
64129
+ return true;
64130
+ }
64131
+ logger.warn("track frame park refused by the owning node", {
64132
+ tags: { deviceId: order.deviceId },
64133
+ meta: {
64134
+ ...meta,
64135
+ reason: answer.reason,
64136
+ handleAgeMs: answer.handleAgeMs,
64137
+ ...answer.detail !== void 0 ? { detail: answer.detail } : {}
64138
+ }
64139
+ });
64140
+ return false;
64141
+ } catch (err) {
64142
+ logger.warn("track frame park failed — the order did not reach the node", {
64143
+ tags: { deviceId: order.deviceId },
64144
+ meta: {
64145
+ ...meta,
64146
+ error: require_dist.errMsg(err)
64147
+ }
64148
+ });
64149
+ return false;
64150
+ }
64151
+ };
64152
+ const park = (order) => {
64153
+ const key = inFlightKey(order.trackId, order.kind);
64154
+ const pending = inFlight.get(key);
64155
+ if (pending !== void 0) return pending;
64156
+ const run = sendPark(order).finally(() => {
64157
+ if (inFlight.get(key) === run) inFlight.delete(key);
64158
+ });
64159
+ inFlight.set(key, run);
64160
+ return run;
64161
+ };
64162
+ const awaitPark = async (trackId, kind) => {
64163
+ const pending = inFlight.get(inFlightKey(trackId, kind));
64164
+ if (pending !== void 0) await pending;
64165
+ };
64166
+ const info = (trackId, kind) => mirror.get(trackId)?.get(kind);
64167
+ const fetch = async (trackId, kind) => {
64168
+ const known = info(trackId, kind);
64169
+ if (known === void 0 || api === void 0) return null;
64170
+ if (deps.isNodeOffline?.(known.nodeId) === true) {
64171
+ logger.warn("parked frame retrieval skipped — the holding node is offline in the registry", {
64172
+ tags: { deviceId: known.deviceId },
64173
+ meta: {
64174
+ trackId,
64175
+ kind,
64176
+ nodeId: known.nodeId
64177
+ }
64178
+ });
64179
+ return null;
64180
+ }
64181
+ try {
64182
+ const parcel = await api.getParkedTrackFrame.query({
64183
+ deviceId: known.deviceId,
64184
+ trackId,
64185
+ kind
64186
+ }, require_dist.nodePin(known.nodeId));
64187
+ if (parcel === null) {
64188
+ logger.warn("parked frame gone at retrieval — the node no longer holds it", {
64189
+ tags: { deviceId: known.deviceId },
64190
+ meta: {
64191
+ trackId,
64192
+ kind,
64193
+ nodeId: known.nodeId,
64194
+ parkedTimestamp: known.timestamp
64195
+ }
64196
+ });
64197
+ return null;
64198
+ }
64199
+ return {
64200
+ jpeg: Buffer.from(parcel.jpeg, "base64"),
64201
+ width: parcel.width,
64202
+ height: parcel.height,
64203
+ timestamp: parcel.timestamp,
64204
+ info: known
64205
+ };
64206
+ } catch (err) {
64207
+ logger.warn("parked frame retrieval failed", {
64208
+ tags: { deviceId: known.deviceId },
64209
+ meta: {
64210
+ trackId,
64211
+ kind,
64212
+ nodeId: known.nodeId,
64213
+ error: require_dist.errMsg(err)
64214
+ }
64215
+ });
64216
+ return null;
64217
+ }
64218
+ };
64219
+ const release = (trackId) => {
64220
+ const kinds = mirror.get(trackId);
64221
+ if (kinds === void 0) return;
64222
+ mirror.delete(trackId);
64223
+ if (api === void 0) return;
64224
+ const targets = /* @__PURE__ */ new Map();
64225
+ for (const record of kinds.values()) targets.set(record.nodeId, record.deviceId);
64226
+ for (const [nodeId, deviceId] of targets) api.releaseParkedTrackFrames.mutate({
64227
+ deviceId,
64228
+ trackId
64229
+ }, require_dist.nodePin(nodeId)).catch((err) => {
64230
+ logger.debug("parked frame release did not reach the node", {
64231
+ tags: { deviceId },
64232
+ meta: {
64233
+ trackId,
64234
+ nodeId,
64235
+ error: require_dist.errMsg(err)
64236
+ }
64237
+ });
64238
+ });
64239
+ };
64240
+ const dispose = () => {
64241
+ mirror.clear();
64242
+ inFlight.clear();
64243
+ skewWarnedAt.clear();
64244
+ };
64245
+ return {
64246
+ park,
64247
+ awaitPark,
64248
+ info,
64249
+ fetch,
64250
+ release,
64251
+ dispose
64252
+ };
64253
+ }
64254
+ //#endregion
63807
64255
  //#region src/pipeline-analytics/runtime/widget-manifest.ts
63808
64256
  /** The remote-module coordinates shared by every analytics widget. */
63809
64257
  var REMOTE_NAME = "addon_pipeline_analytics_widgets";
@@ -64905,7 +65353,8 @@ function isUniformRgbFrame(data, width, height) {
64905
65353
  * frame downscaled to {@link SNAPSHOT_MAX_WIDTH} (960) with ONLY the track's own
64906
65354
  * box burned in, falling back to the ≤640 detection frame (honest lower res,
64907
65355
  * never an upscale) on a native miss. The best `thumbnail` stays a clean,
64908
- * uncapped native subject crop (native-or-nothing).
65356
+ * uncapped native subject crop — read from the track's PARKED slab, by track
65357
+ * (D422), never by handle.
64909
65358
  *
64910
65359
  * The detection frame is resolved ONCE per call (blank-frame guard); native full
64911
65360
  * frames + subject crops are fetched per target by handle. Frames recycle in
@@ -65173,10 +65622,10 @@ var EventMediaDispatcher = class {
65173
65622
  if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
65174
65623
  let thumbnailWritten = false;
65175
65624
  if (sn.bestThumbnail) {
65176
- const variants = await this.cropSubjectVariants(deviceId, sn.trackId, frameHandle, fw, fh, sn.bbox);
65625
+ const variants = await this.cropSubjectVariants(deviceId, sn.trackId);
65177
65626
  if (variants) {
65178
- thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail, "native");
65179
- await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall, "native");
65627
+ thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", variants.timestamp, variants.thumbnail, "native");
65628
+ await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", variants.timestamp, variants.thumbnailSmall, "native");
65180
65629
  }
65181
65630
  }
65182
65631
  return {
@@ -65254,81 +65703,49 @@ var EventMediaDispatcher = class {
65254
65703
  }
65255
65704
  /**
65256
65705
  * The best-shot subject crop as its TWO persisted variants (best-crop
65257
- * fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22). ONE native
65258
- * fetch: the in-frame slab of the 16:9 central-square WINDOW
65259
- * ({@link wideCentralSquareLayout} a `side × 16/9` window whose middle square
65260
- * of side `max(w,h)×1.2` fully contains the subject, clamped/anchored so the
65261
- * subject stays in that central square at every frame edge) is requested from
65262
- * the runner's retained native surface with NO `maxWidth` (uncapped TRUE
65263
- * native). It is composed into the full 16:9 window (lateral letterbox only for
65264
- * the part outside the frame) the `thumbnail`. The `thumbnailSmall` is
65265
- * DERIVED by downscaling that SAME window to the 480 long-side cap (480×270)
65266
- * never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
65267
- * never upscales a smaller window).
65268
- *
65269
- * Both variants share the SAME framing + central-square containment guarantee:
65270
- * wide consumers use the 16:9 image as-is (subject centered, no cut); square
65271
- * consumers center-crop the middle square (subject always inside it).
65272
- *
65273
- * NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
65274
- * `null` after a loud `logger.warn`; the caller SKIPS the write and the
65275
- * per-frame retry lands a real native crop later. Never a local resize
65276
- * upscale of a ≤640 tile (a blurred lie).
65277
- */
65278
- async cropSubjectVariants(deviceId, trackId, frameHandle, fw, fh, bbox) {
65279
- if (!this.deps.getNativeCropJpeg) {
65280
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", {
65281
- tags: { deviceId },
65282
- meta: {
65283
- shmId: frameHandle.shmId,
65284
- reason: "no-native-cap"
65285
- }
65286
- });
65287
- this.deps.onSubjectCropMiss?.(deviceId, trackId, "no-native-cap");
65288
- return null;
65289
- }
65706
+ * fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22), from the
65707
+ * track's PARKED slab (D422). ONE read, by `(deviceId, trackId)`: the
65708
+ * in-frame slab of the 16:9 central-square WINDOW
65709
+ * ({@link wideCentralSquareLayout}) the runner cut at uncapped native
65710
+ * resolution the instant the track's best was decided. It is composed into
65711
+ * the full 16:9 window (lateral letterbox only for the part outside the
65712
+ * frame) the `thumbnail`. The `thumbnailSmall` is DERIVED by downscaling
65713
+ * that SAME window to the 480 long-side cap never a second read, never
65714
+ * upscaled ({@link deriveThumbnailSmall} never upscales a smaller window).
65715
+ *
65716
+ * Both variants share the SAME framing + central-square containment
65717
+ * guarantee: wide consumers use the 16:9 image as-is; square consumers
65718
+ * center-crop the middle square (subject always inside it).
65719
+ *
65720
+ * PARCEL-OR-NOTHING: no parcel (or a throw) returns `null` after a loud
65721
+ * `logger.warn`; the caller SKIPS the write and the per-frame retry parks a
65722
+ * later frame. Never a by-handle crop, never a local upscale of a ≤640 tile.
65723
+ */
65724
+ async cropSubjectVariants(deviceId, trackId) {
65290
65725
  try {
65291
- const layout = deliveredWideCentralSquareLayout(bbox, {
65292
- W: fw,
65293
- H: fh
65294
- });
65295
- if (centralSquareContainmentFraction(bbox, {
65296
- W: fw,
65297
- H: fh
65298
- }) < .5) this.deps.logger.info("best-shot delivered via contained square layout (edge subject)", {
65299
- tags: { deviceId },
65300
- meta: {
65301
- bbox,
65302
- frameW: fw,
65303
- frameH: fh
65304
- }
65305
- });
65306
- const askedAt = Date.now();
65307
- const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch, void 0, deviceId);
65308
- if (!slab) {
65309
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", {
65726
+ const parked = await this.deps.getParkedSubjectSlab(deviceId, trackId);
65727
+ if (!parked) {
65728
+ this.deps.logger.warn("best-shot parcel missing — no thumbnail this frame, will retry", {
65310
65729
  tags: { deviceId },
65311
65730
  meta: {
65312
- shmId: frameHandle.shmId,
65313
- handle: `${frameHandle.shmId}#${frameHandle.slot}#${frameHandle.seq}`,
65314
- handleNodeId: frameHandle.nodeId,
65315
- roundTripMs: Date.now() - askedAt,
65316
- reason: "native-miss"
65731
+ trackId,
65732
+ reason: "parcel-missing"
65317
65733
  }
65318
65734
  });
65319
- this.deps.onSubjectCropMiss?.(deviceId, trackId, "native-miss");
65735
+ this.deps.onSubjectCropMiss?.(deviceId, trackId, "parcel-missing");
65320
65736
  return null;
65321
65737
  }
65322
- const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
65738
+ const thumbnail = await composeWideCentralSquareThumbnail(parked.slab, parked.layout);
65323
65739
  return {
65324
65740
  thumbnail,
65325
- thumbnailSmall: await deriveThumbnailSmall(thumbnail)
65741
+ thumbnailSmall: await deriveThumbnailSmall(thumbnail),
65742
+ timestamp: parked.timestamp
65326
65743
  };
65327
65744
  } catch (err) {
65328
- this.deps.logger.warn("native subject crop misswill retry, no upscale", {
65745
+ this.deps.logger.warn("best-shot parcel read failedno thumbnail this frame, will retry", {
65329
65746
  tags: { deviceId },
65330
65747
  meta: {
65331
- shmId: frameHandle.shmId,
65748
+ trackId,
65332
65749
  reason: "threw",
65333
65750
  error: err instanceof Error ? err.message : String(err)
65334
65751
  }
@@ -70619,7 +71036,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70619
71036
  * the FULL cleanup litany on every teardown path (and cancels the track's
70620
71037
  * queued captures via the onClose hook) and `closeDevice` finally clears
70621
71038
  * per-track state on device removal (the two documented leaks). */
70622
- residents = new TrackResidentState({ onClose: (trackId) => this.captureScheduler.cancelForTrack(trackId) });
71039
+ residents = new TrackResidentState({ onClose: (trackId) => {
71040
+ this.captureScheduler.cancelForTrack(trackId);
71041
+ this.parkedFrames?.release(trackId);
71042
+ } });
71043
+ /**
71044
+ * The hub's side of a PARK (D422): orders the owning node to keep a track's
71045
+ * best frame the instant `BestDetectionTracker` accepts it, and answers the
71046
+ * keyFrame writer, the best-shot thumbnail and the detail plane's full-frame
71047
+ * rung by `(deviceId, trackId, kind)` — never by handle. Null until init.
71048
+ */
71049
+ parkedFrames = null;
70623
71050
  /** Defence in depth for the 2026-08-21 `b50e09f7` incident: a row the track
70624
71051
  * store has already CLOSED never earns another media target, whatever the
70625
71052
  * tracker keeps handing back. Reads the store's in-RAM close record only —
@@ -70729,6 +71156,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70729
71156
  * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
70730
71157
  getNativeKeyFrameRgb = null;
70731
71158
  shuttingDown = false;
71159
+ /**
71160
+ * Per-device delivery-lag windows, folded at `handleInferenceResult` entry.
71161
+ * The instrument that tells a late DELIVERY (starved) apart from a slow
71162
+ * `processFrame` (waiting) — see `frame-lag-observer.ts`. Costs one map read
71163
+ * and ~10 numeric ops per frame, and writes one line per device per 30 s.
71164
+ */
71165
+ frameLag = new FrameLagObserver();
71166
+ /**
71167
+ * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
71168
+ *
71169
+ * The other half of the frame-lag line: that one says whether the events
71170
+ * arrived late, this one says whether THIS loop was blocked while they did.
71171
+ * The 5-minute `[mem]` heartbeat cannot see a 10 s freeze — it fired at
71172
+ * dt = 300020 / 300066 / 299991 ms straight through the 2026-09-09 silences.
71173
+ */
71174
+ stopStallMonitor = null;
70732
71175
  /** True only on the cluster's designated post-processing node. When false the
70733
71176
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
70734
71177
  isPostProcessingNode = true;
@@ -70947,6 +71390,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70947
71390
  super({});
70948
71391
  }
70949
71392
  async onInitialize() {
71393
+ this.stopStallMonitor = (0, _camstack_system_addon_utils.startEventLoopStallMonitor)(this.ctx.logger);
70950
71394
  const rawApi = this.ctx.api;
70951
71395
  if (!rawApi) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
70952
71396
  const api = censusApi(rawApi, this.readCensus);
@@ -70997,17 +71441,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70997
71441
  noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
70998
71442
  });
70999
71443
  this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
71444
+ this.parkedFrames = createParkedFrameClient({
71445
+ api: api.pipelineRunner,
71446
+ logger: logger.child("ParkedFrames"),
71447
+ isNodeOffline: (nodeId) => this.nodeLiveness.isKnownOffline(nodeId, Date.now())
71448
+ });
71000
71449
  this.captureCrop = transport.captureCrop;
71001
71450
  this.eventMediaDispatcher = new EventMediaDispatcher({
71002
71451
  getRemoteFrame: transport.getRemoteFrame,
71003
71452
  getNativeCropJpeg: transport.getNativeCropJpeg,
71004
71453
  getNativeFullFrameRgb: transport.getNativeFullFrameRgb,
71454
+ getParkedSubjectSlab: (deviceId, trackId) => this.fetchParkedSubjectSlab(deviceId, trackId),
71005
71455
  mediaStore: stores.mediaStore,
71006
71456
  logger: logger.child("EventMediaDispatcher"),
71007
71457
  onSubjectCropMiss: (deviceId, trackId, reason) => this.residents.notePreviewCaptureMiss(deviceId, trackId, `thumbnail:${reason}`)
71008
71458
  });
71009
71459
  this.buildRecognizers(api, logger, stores, transport);
71010
- this.buildDetailDispatcher(api, logger, transport);
71460
+ this.buildDetailDispatcher(api, logger);
71011
71461
  const capProviders = this.buildAnalyticsProviders(api, logger);
71012
71462
  await step("buildNotificationCenter", () => this.buildNotificationCenter(api, logger, stores));
71013
71463
  await step("serveEventMediaDataPlane", () => this.serveEventMediaDataPlane());
@@ -71837,7 +72287,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71837
72287
  * demand via `pipelineRunner.runDetailSubtree`. The dispatcher owns the
71838
72288
  * per-device schedulers, concurrency cap, and 1 s tick; result routing +
71839
72289
  * the pinned call + crop fallback are injected here. */
71840
- buildDetailDispatcher(api, logger, transport) {
72290
+ buildDetailDispatcher(api, logger) {
71841
72291
  const runnerApi = api.pipelineRunner;
71842
72292
  this.detailDispatcher = new TrackDetailDispatcher({
71843
72293
  logger: logger.child("DetailDispatcher"),
@@ -71847,13 +72297,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71847
72297
  return runnerApi.runDetailSubtree.mutate(input);
71848
72298
  },
71849
72299
  routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
71850
- captureFullFrameBase64: async (frame) => {
71851
- if (frame.frameHandle === void 0) return null;
71852
- const full = await transport.getFullFrameJpegBase64(frame.frameHandle, frame.frameWidth);
71853
- return full === null ? null : {
71854
- base64: full.base64,
71855
- width: full.width,
71856
- height: full.height
72300
+ captureParkedFrame: async (_deviceId, trackId) => {
72301
+ const parcel = await this.fetchParkedParcel(trackId, "keyFrame");
72302
+ if (parcel === null) return null;
72303
+ return {
72304
+ base64: parcel.jpeg.toString("base64"),
72305
+ width: parcel.width,
72306
+ height: parcel.height,
72307
+ bbox: scaleSubjectIntoParcel(parcel.info.subject, parcel.width, parcel.height),
72308
+ nodeId: parcel.info.nodeId,
72309
+ timestamp: parcel.timestamp
71857
72310
  };
71858
72311
  },
71859
72312
  captureCropBase64: async (frame) => {
@@ -73451,6 +73904,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73451
73904
  }
73452
73905
  async onShutdown() {
73453
73906
  this.shuttingDown = true;
73907
+ if (this.stopStallMonitor) {
73908
+ this.stopStallMonitor();
73909
+ this.stopStallMonitor = null;
73910
+ }
73454
73911
  this.sceneEngine?.stop();
73455
73912
  this.sceneEngine = null;
73456
73913
  if (this.sceneReloadTimer !== null) {
@@ -73491,10 +73948,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73491
73948
  this.audioMarkers?.flush(Date.now());
73492
73949
  this.detailDispatcher?.dispose();
73493
73950
  this.detailDispatcher = null;
73951
+ this.parkedFrames?.dispose();
73494
73952
  this.overlayState.clear();
73495
73953
  this.overlaySynthesisWarnAt.clear();
73496
73954
  this.processors.clear();
73497
73955
  this.lastActiveTrackIds.clear();
73956
+ this.frameLag.clear();
73498
73957
  this.lastFrameDimsByDevice.clear();
73499
73958
  this.dropoutSkipsByKey.clear();
73500
73959
  this.residents.clearAll();
@@ -73534,6 +73993,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73534
73993
  async handleInferenceResult(data) {
73535
73994
  if (this.shuttingDown) return;
73536
73995
  const { deviceId, frame } = data;
73996
+ const lag = this.frameLag.observe(deviceId, Date.now(), data.emittedAt, data.capturedAt);
73997
+ if (lag !== null) {
73998
+ const verdict = lagVerdict(lag);
73999
+ (isAlarmingLagReport(lag) ? this.ctx.logger.warn.bind(this.ctx.logger) : this.ctx.logger.info.bind(this.ctx.logger))("pipeline-analytics frame lag", {
74000
+ tags: { deviceId },
74001
+ meta: {
74002
+ verdict,
74003
+ frames: lag.frames,
74004
+ windowMs: lag.windowMs,
74005
+ busLagMaxMs: lag.busLagMaxMs,
74006
+ busLagMinMs: lag.busLagMinMs,
74007
+ busLagUnknownFrames: lag.busLagUnknownFrames,
74008
+ captureLagMaxMs: lag.captureLagMaxMs,
74009
+ captureLagUnknownFrames: lag.captureLagUnknownFrames
74010
+ }
74011
+ });
74012
+ }
73537
74013
  await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
73538
74014
  }
73539
74015
  /**
@@ -74131,7 +74607,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74131
74607
  let plateCrops = 0;
74132
74608
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
74133
74609
  else plateCrops += 1;
74134
- const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
74610
+ const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
74135
74611
  this.offerGroupShot(deviceId, result, frameHandle);
74136
74612
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
74137
74613
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
@@ -74250,7 +74726,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74250
74726
  }).catch(() => {});
74251
74727
  }
74252
74728
  const keyFrameCaptures = selectKeyFrameCaptures(snapshotTargets);
74253
- if (keyFrameCaptures.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameCaptures, frameHandle);
74729
+ if (keyFrameCaptures.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameCaptures);
74254
74730
  }
74255
74731
  if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
74256
74732
  deviceId,
@@ -75180,10 +75656,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75180
75656
  });
75181
75657
  }).catch(() => {});
75182
75658
  }
75183
- async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
75184
- const getNative = this.getNativeKeyFrameRgb;
75659
+ /**
75660
+ * Order the parcels a best-frame decision wants (D422). Skipped entirely at
75661
+ * a node the registry already called offline (D58) — logged on the
75662
+ * transition by `noteCapturesGated`, counted in the key-frame window.
75663
+ * `onlyIfMissing` is the retry rule: a later frame may fill a kind that has
75664
+ * no parcel, never replace one the ranking preferred.
75665
+ */
75666
+ issueParkOrders(decision, kinds, onlyIfMissing) {
75667
+ const client = this.parkedFrames;
75668
+ if (client === null) return;
75669
+ if (this.nodeLiveness.isKnownOffline(decision.frameHandle.nodeId, Date.now())) {
75670
+ this.noteCapturesGated(decision.deviceId, decision.frameHandle.nodeId);
75671
+ return;
75672
+ }
75673
+ for (const order of buildParkOrders(decision)) {
75674
+ if (!kinds.includes(order.kind)) continue;
75675
+ if (onlyIfMissing && client.info(order.trackId, order.kind) !== void 0) continue;
75676
+ client.park(order);
75677
+ }
75678
+ }
75679
+ /** A parcel by track, after any park in flight for it has landed. */
75680
+ async fetchParkedParcel(trackId, kind) {
75681
+ const client = this.parkedFrames;
75682
+ if (client === null) return null;
75683
+ await client.awaitPark(trackId, kind);
75684
+ return client.fetch(trackId, kind);
75685
+ }
75686
+ /** The best-shot slab for the dispatcher: the parcel plus the layout the hub kept. */
75687
+ async fetchParkedSubjectSlab(_deviceId, trackId) {
75688
+ const parcel = await this.fetchParkedParcel(trackId, "thumbnail");
75689
+ if (parcel === null || parcel.info.layout === void 0) return null;
75690
+ return {
75691
+ slab: parcel.jpeg,
75692
+ layout: parcel.info.layout,
75693
+ timestamp: parcel.timestamp
75694
+ };
75695
+ }
75696
+ async persistKeyFrames(deviceId, timestamp, captures) {
75185
75697
  const mediaStore = this.mediaStore;
75186
- if (!getNative || !mediaStore) return;
75698
+ if (this.parkedFrames === null || !mediaStore) return;
75187
75699
  await Promise.all(captures.map((capture) => this.captureScheduler.request({
75188
75700
  deviceId,
75189
75701
  trackId: capture.trackId,
@@ -75193,16 +75705,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75193
75705
  exec: async () => {
75194
75706
  this.residents.noteKeyFrameAttempt(deviceId, capture.trackId, timestamp, capture.retry);
75195
75707
  const outcome = await persistTrackKeyFrame({
75196
- fetchNativeFullFrame: getNative,
75197
- encodeVariants: encodeKeyFrameVariants,
75708
+ fetchParkedKeyFrame: async (parcelDeviceId, trackId) => {
75709
+ const parcel = await this.fetchParkedParcel(trackId, "keyFrame");
75710
+ if (parcel === null || parcel.info.deviceId !== parcelDeviceId) return null;
75711
+ return {
75712
+ jpeg: parcel.jpeg,
75713
+ width: parcel.width,
75714
+ height: parcel.height,
75715
+ timestamp: parcel.timestamp,
75716
+ nodeId: parcel.info.nodeId
75717
+ };
75718
+ },
75719
+ deriveSmall: deriveKeyFrameSmall,
75198
75720
  media: mediaStore,
75199
75721
  logger: this.ctx.logger
75200
75722
  }, {
75201
75723
  deviceId,
75202
75724
  trackId: capture.trackId,
75203
- timestamp,
75204
- frameHandle,
75205
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
75206
75725
  retry: capture.retry
75207
75726
  });
75208
75727
  if (outcome.kind === "landed") {
@@ -75282,10 +75801,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75282
75801
  isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
75283
75802
  captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
75284
75803
  captureKeyFrame: async (req) => {
75804
+ this.issueParkOrders({
75805
+ deviceId: req.deviceId,
75806
+ trackId: req.trackId,
75807
+ timestamp: req.timestamp,
75808
+ frameHandle: req.frameHandle,
75809
+ bbox: { ...req.bodyBbox },
75810
+ frameWidth: req.frameWidth,
75811
+ frameHeight: req.frameHeight
75812
+ }, ["keyFrame"], true);
75285
75813
  await this.persistKeyFrames(req.deviceId, req.timestamp, [{
75286
75814
  trackId: req.trackId,
75287
75815
  retry: false
75288
- }], req.frameHandle);
75816
+ }]);
75289
75817
  }
75290
75818
  }, input);
75291
75819
  }
@@ -75339,10 +75867,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75339
75867
  * (coalescing, per-device budget and land-only bookkeeping identical). */
75340
75868
  captureSemanticBestShot(request) {
75341
75869
  const { deviceId, trackId, timestamp, bbox, frameHandle } = request;
75870
+ this.issueParkOrders({
75871
+ deviceId,
75872
+ trackId,
75873
+ timestamp,
75874
+ frameHandle,
75875
+ bbox: { ...bbox },
75876
+ frameWidth: request.frameWidth,
75877
+ frameHeight: request.frameHeight
75878
+ }, ["keyFrame", "thumbnail"], false);
75342
75879
  this.persistKeyFrames(deviceId, timestamp, [{
75343
75880
  trackId,
75344
75881
  retry: false
75345
- }], frameHandle);
75882
+ }]);
75346
75883
  const dispatcher = this.eventMediaDispatcher;
75347
75884
  if (!dispatcher) return;
75348
75885
  if (this.captureScheduler.isKeyActive(captureCoalesceKey(deviceId, "thumbnail", trackId))) {
@@ -75493,7 +76030,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75493
76030
  }
75494
76031
  return retries;
75495
76032
  }
75496
- buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
76033
+ buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight, frameHandle) {
75497
76034
  const targets = [];
75498
76035
  for (const t of tracked) {
75499
76036
  if (t.matchedThisFrame === false) continue;
@@ -75579,6 +76116,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75579
76116
  state: this.residents.keyFrameCapture(t.trackId),
75580
76117
  now: timestamp
75581
76118
  }).kind;
76119
+ if (frameHandle !== void 0 && plausibleBox) {
76120
+ const decision = {
76121
+ deviceId,
76122
+ trackId: t.trackId,
76123
+ timestamp,
76124
+ frameHandle,
76125
+ bbox: {
76126
+ x: t.bbox.x,
76127
+ y: t.bbox.y,
76128
+ w: t.bbox.w,
76129
+ h: t.bbox.h
76130
+ },
76131
+ frameWidth,
76132
+ frameHeight
76133
+ };
76134
+ if (isNewBest) this.issueParkOrders(decision, ["keyFrame", "thumbnail"], false);
76135
+ else {
76136
+ const retryKinds = [];
76137
+ if (keyFrame === "retry") retryKinds.push("keyFrame");
76138
+ if (bestThumbnail) retryKinds.push("thumbnail");
76139
+ if (retryKinds.length > 0) this.issueParkOrders(decision, retryKinds, true);
76140
+ }
76141
+ }
75582
76142
  const suppressedBy = (() => {
75583
76143
  if (skipParkedThumb) return "parked-thumbnail";
75584
76144
  if (!isNewBest || bestThumbnail) return void 0;