@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.
@@ -1,4 +1,4 @@
1
- import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-CT2SouNJ.mjs";
1
+ import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-DWZqNymj.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -6,6 +6,7 @@ import * as path$1 from "node:path";
6
6
  import path from "node:path";
7
7
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import { execFile } from "node:child_process";
9
+ import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
9
10
  import sharp from "sharp";
10
11
  import os from "node:os";
11
12
  import { Buffer as Buffer$1 } from "node:buffer";
@@ -14913,7 +14914,7 @@ function buildOccupancyStatus(input) {
14913
14914
  * `this.byId.clear()` happened to sit AFTER the `await` that threw. Nothing
14914
14915
  * said so, and hoisting one line would have silently turned every transient
14915
14916
  * store error into a hub that stops notifying — the reversal behind the D130
14916
- * flood and the `3f9345fc3` occupancy cold-seed loss. The ledger's contract
14917
+ * flood and the occupancy cold-seed loss. The ledger's contract
14917
14918
  * rule 2 (a failed load NEVER clears the mirror, deliberately no knob) makes
14918
14919
  * that reversal unexpressible here. `rule-store-load-failure-direction.spec.ts`
14919
14920
  * holds the direction and records what was removed to make it red.
@@ -18130,7 +18131,7 @@ function clockOf$2(atMs) {
18130
18131
  *
18131
18132
  * ## The `.partial()` trap this file exists to avoid
18132
18133
  *
18133
- * Commit `e0921ac6e`, found in production 2026-08-12: `NcRulePatchSchema` was
18134
+ * Found in production 2026-08-12: `NcRulePatchSchema` was
18134
18135
  * `NcRuleInputSchema.partial()`, and Zod 4 `.partial()` KEEPS the inner
18135
18136
  * `.default()`. Parsing `{ name: 'x' }` therefore materialised every defaulted
18136
18137
  * key, and `{ ...existing, ...patch }` wrote them over the rule — a camera- and
@@ -34880,6 +34881,8 @@ var SortTracker = class SortTracker {
34880
34881
  refusalSampler = new SpawnRefusalSampler();
34881
34882
  /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34882
34883
  spawnRefusalObserver = null;
34884
+ /** Unsampled copy for the frame path — see `setSpawnRefusalSink`. */
34885
+ refusalSink = null;
34883
34886
  /** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
34884
34887
  spawnBesideSampler = new SpawnBesideCoastingSampler();
34885
34888
  /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
@@ -34992,11 +34995,7 @@ var SortTracker = class SortTracker {
34992
34995
  }
34993
34996
  /** Emit one dropped-detection record, subject to the refusal sampler. */
34994
34997
  reportRefusal(reason, det, extra = {}) {
34995
- const observer = this.spawnRefusalObserver;
34996
- if (!observer) return;
34997
- const refusalCount = this.refusalSampler.next(reason);
34998
- if (refusalCount === void 0) return;
34999
- observer({
34998
+ const base = {
35000
34999
  reason,
35001
35000
  detClass: det.class,
35002
35001
  detScore: det.score,
@@ -35008,13 +35007,31 @@ var SortTracker = class SortTracker {
35008
35007
  againstTrackId: extra.against.id,
35009
35008
  againstTrackClass: extra.against.class
35010
35009
  } : {},
35011
- refusalCount,
35012
- sampledEvery: 10,
35013
35010
  ...extra.droppedTrackId !== void 0 ? { droppedTrackId: extra.droppedTrackId } : {},
35014
35011
  ...extra.trackLifeMs !== void 0 ? { trackLifeMs: extra.trackLifeMs } : {}
35012
+ };
35013
+ this.refusalSink?.(base);
35014
+ const observer = this.spawnRefusalObserver;
35015
+ if (!observer) return;
35016
+ const refusalCount = this.refusalSampler.next(reason);
35017
+ if (refusalCount === void 0) return;
35018
+ observer({
35019
+ ...base,
35020
+ refusalCount,
35021
+ sampledEvery: 10
35015
35022
  });
35016
35023
  }
35017
35024
  /**
35025
+ * Wire the UNSAMPLED refusal sink — the frame path's copy.
35026
+ *
35027
+ * Separate from {@link setSpawnRefusalObserver} because the two want
35028
+ * different things from the same record: the observer wants a readable log
35029
+ * and accepts a sample; this decides whether an EVENT exists.
35030
+ */
35031
+ setSpawnRefusalSink(sink) {
35032
+ this.refusalSink = sink;
35033
+ }
35034
+ /**
35018
35035
  * Report a TRACK that ends having emitted nothing — the last silent branch
35019
35036
  * on the detection→position path (D391).
35020
35037
  *
@@ -36108,6 +36125,20 @@ var FrameProcessor = class {
36108
36125
  /** Sink + sampler for the drops this stage owns (the detection-stage zone
36109
36126
  * filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
36110
36127
  spawnRefusalObserver = null;
36128
+ /** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
36129
+ spawnRefusalForward = null;
36130
+ /**
36131
+ * The spawn refusals of the frame being processed.
36132
+ *
36133
+ * Filled by the same sink that feeds the diagnostic log, and drained at the
36134
+ * end of `process`. It exists because a refused detection inside a watched
36135
+ * zone still has to become an EVENT: an immediate zone rule asks "is there an
36136
+ * animal in the flowerbed now?", which does not need "is it the same animal
36137
+ * as before?" answered first. See `zone-detection-emits-without-a-track`.
36138
+ */
36139
+ refusalsThisFrame = [];
36140
+ /** Running count of events this path has produced — the volume measure. */
36141
+ zoneRefusalEventCount = 0;
36111
36142
  zoneRefusalSampler = new SpawnRefusalSampler();
36112
36143
  /** Optional stationary-object gate (parked-object suppression). Null until
36113
36144
  * the addon wires it via {@link setStationaryGate}. */
@@ -36122,6 +36153,9 @@ var FrameProcessor = class {
36122
36153
  this.deviceId = deviceId;
36123
36154
  this.source = source;
36124
36155
  this.tracker = new SortTracker(trackerConfig);
36156
+ this.tracker.setSpawnRefusalSink((record) => {
36157
+ this.refusalsThisFrame.push(record);
36158
+ });
36125
36159
  this.stateAnalyzer = new StateAnalyzer(stateConfig);
36126
36160
  this.eventEmitter = new DetectionEventEmitter(emitterConfig);
36127
36161
  this.zones = [];
@@ -36228,6 +36262,55 @@ var FrameProcessor = class {
36228
36262
  * ate it. Sampled like every other refusal: an excluded box is excluded on
36229
36263
  * every frame of a passage.
36230
36264
  */
36265
+ /**
36266
+ * Turn this frame's refused spawns into events, but only those inside a zone.
36267
+ *
36268
+ * Drains {@link refusalsThisFrame}. Each surviving record becomes an
36269
+ * `object.detected` event with NO track — carrying the zones it fell in, the
36270
+ * percentage of the box inside each, and the refusal reason so a consumer can
36271
+ * tell it apart from a track whose media went missing.
36272
+ *
36273
+ * `zone-filtered` refusals are excluded on purpose: that reason means the
36274
+ * operator's own detection rules already said the box does not count on this
36275
+ * camera, and re-admitting it here would overrule a decision they made.
36276
+ */
36277
+ eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36278
+ const refusals = this.refusalsThisFrame;
36279
+ this.refusalsThisFrame = [];
36280
+ if (refusals.length === 0 || this.zones.length === 0) return [];
36281
+ const out = [];
36282
+ for (const r of refusals) {
36283
+ if (r.reason === "zone-filtered") continue;
36284
+ const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36285
+ if (memberships.length === 0) continue;
36286
+ this.zoneRefusalEventCount += 1;
36287
+ out.push({
36288
+ id: randomUUID(),
36289
+ kind: "object",
36290
+ deviceId: this.deviceId,
36291
+ timestamp,
36292
+ frameId: frame.frameId,
36293
+ className: r.detClass,
36294
+ confidence: r.detScore,
36295
+ bbox: r.bbox,
36296
+ frameWidth,
36297
+ frameHeight,
36298
+ zones: memberships.map((m) => m.zoneId),
36299
+ zoneOverlaps: memberships.map((m) => ({
36300
+ zoneId: m.zoneId,
36301
+ ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36302
+ overlapPct: Math.round(m.overlap * 1e3) / 10
36303
+ })),
36304
+ spawnRefused: r.reason
36305
+ });
36306
+ }
36307
+ return out;
36308
+ }
36309
+ /** How many zone-refusal events this processor has emitted — the measure that
36310
+ * says whether this path is a trickle or a storm. Read by the addon. */
36311
+ zoneRefusalEventsEmitted() {
36312
+ return this.zoneRefusalEventCount;
36313
+ }
36231
36314
  reportZoneFiltered(excluded) {
36232
36315
  const observer = this.spawnRefusalObserver;
36233
36316
  if (!observer || excluded.length === 0) return;
@@ -36629,9 +36712,11 @@ var FrameProcessor = class {
36629
36712
  frameHeight
36630
36713
  };
36631
36714
  };
36715
+ const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36632
36716
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36633
36717
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36634
36718
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36719
+ objectEvents.push(...zoneRefusalEvents);
36635
36720
  return {
36636
36721
  deviceId: this.deviceId,
36637
36722
  timestamp,
@@ -47245,6 +47330,7 @@ var TrackDetailDispatcher = class {
47245
47330
  if (!dev) return;
47246
47331
  dev.scheduler.onTrackEnded(trackId);
47247
47332
  dev.tracks.delete(trackId);
47333
+ dev.parcelEnriched.delete(trackId);
47248
47334
  dev.candidateBest.delete(trackId);
47249
47335
  if (dev.tracks.size === 0) this.clearTimer(dev);
47250
47336
  }
@@ -47254,6 +47340,7 @@ var TrackDetailDispatcher = class {
47254
47340
  this.clearTimer(dev);
47255
47341
  dev.queue.length = 0;
47256
47342
  dev.tracks.clear();
47343
+ dev.parcelEnriched.clear();
47257
47344
  dev.candidateBest.clear();
47258
47345
  }
47259
47346
  this.devices.clear();
@@ -47264,6 +47351,7 @@ var TrackDetailDispatcher = class {
47264
47351
  dev = {
47265
47352
  scheduler: new DetailScheduler(),
47266
47353
  tracks: /* @__PURE__ */ new Map(),
47354
+ parcelEnriched: /* @__PURE__ */ new Map(),
47267
47355
  candidateBest: new BestDetectionTracker(CANDIDATE_IMPROVE_POLICY),
47268
47356
  queue: [],
47269
47357
  coastedFramesIgnored: 0,
@@ -47380,19 +47468,21 @@ var TrackDetailDispatcher = class {
47380
47468
  *
47381
47469
  * 1. `frameHandle` — cheapest when it hits: no payload moves and the runner's
47382
47470
  * leaf children re-cut their ROI at native resolution. Tried first, always.
47383
- * 2. `frameJpeg` — the freshest frame's FULL raster, fetched from the node that
47384
- * owns the handle and handed to the runner UNCUT, so the rectangle still
47385
- * comes from the single derivation (D52). This rung exists because rung 1 is
47386
- * structurally unreachable for most dispatches: the decode worker's native
47387
- * lease is 1.2 s and a tick + inference routinely lands outside it, while
47388
- * the retained store holds the frame for 3 s.
47471
+ * 2. `frameJpeg` — the track's PARKED key frame (D422): the full native frame
47472
+ * the owning node copied at the best instant, retrieved by track and handed
47473
+ * to the runner UNCUT, so the rectangle still comes from the single
47474
+ * derivation (D52). This rung exists because rung 1 is structurally
47475
+ * unreachable for most dispatches a tick + inference routinely lands
47476
+ * outside the lease and it cannot miss on age. It runs ONCE per parcel
47477
+ * per step; a dispatch that finds the same parcel already enriched skips
47478
+ * it, so a coasting track does not re-embed the same image every second.
47389
47479
  * 3. `cropJpeg` — the pre-cut tile. Last, because the caller decided its
47390
47480
  * rectangle and because in practice the same native miss that emptied rung 1
47391
47481
  * empties this one too.
47392
47482
  *
47393
- * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
47394
- * demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
47395
- * rung 1 missed — never a frame-rate stream, and never raw pixels.
47483
+ * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG retrieved
47484
+ * on demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only
47485
+ * after rung 1 missed — never a frame-rate stream, and never raw pixels.
47396
47486
  *
47397
47487
  * ONE outcome stops the ladder cold: the runner's typed `not-attached`
47398
47488
  * refusal (`detail-refusal.ts`). It names the camera's SESSION state, not
@@ -47430,34 +47520,39 @@ var TrackDetailDispatcher = class {
47430
47520
  }
47431
47521
  });
47432
47522
  }
47433
- if (this.deps.captureFullFrameBase64 !== void 0 && frame.frameHandle !== void 0) try {
47434
- const full = await this.deps.captureFullFrameBase64(frame);
47435
- 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", {
47523
+ if (this.deps.captureParkedFrame !== void 0) try {
47524
+ const parked = await this.deps.captureParkedFrame(deviceId, req.trackId);
47525
+ 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", {
47436
47526
  tags: { deviceId },
47437
47527
  meta: {
47438
47528
  trackId: req.trackId,
47439
47529
  stepId: req.stepId,
47440
- frameWidth: full.width,
47441
- frameHeight: full.height,
47442
- bboxFrameWidth: frame.frameWidth,
47443
- bboxFrameHeight: frame.frameHeight
47530
+ parkedTimestamp: parked.timestamp
47444
47531
  }
47445
47532
  });
47446
47533
  else {
47447
47534
  const derived = await this.deps.runDetailSubtree({
47448
47535
  deviceId,
47449
- frameJpeg: full.base64,
47450
- parent,
47536
+ frameJpeg: parked.base64,
47537
+ parent: {
47538
+ bbox: { ...parked.bbox },
47539
+ className: frame.className
47540
+ },
47451
47541
  steps
47452
- }, frame.nodeId);
47453
- if (derived !== null) return derived.details;
47542
+ }, parked.nodeId);
47543
+ if (derived !== null) {
47544
+ const perStep = dev.parcelEnriched.get(req.trackId) ?? /* @__PURE__ */ new Map();
47545
+ perStep.set(req.stepId, parked.timestamp);
47546
+ dev.parcelEnriched.set(req.trackId, perStep);
47547
+ return derived.details;
47548
+ }
47454
47549
  }
47455
47550
  } catch (err) {
47456
47551
  if (isDetailNotAttachedRefusal(err)) {
47457
47552
  this.warnNotAttachedThrottled(deviceId, dev, req);
47458
47553
  return null;
47459
47554
  }
47460
- this.deps.logger.debug("detail full-frame fallback call failed", {
47555
+ this.deps.logger.debug("detail parked-frame rung failed", {
47461
47556
  tags: { deviceId },
47462
47557
  meta: {
47463
47558
  trackId: req.trackId,
@@ -47524,7 +47619,7 @@ var TrackDetailDispatcher = class {
47524
47619
  trackId: req.trackId,
47525
47620
  stepId: req.stepId,
47526
47621
  reason: req.reason,
47527
- triedFullFrame: this.deps.captureFullFrameBase64 !== void 0,
47622
+ triedParkedFrame: this.deps.captureParkedFrame !== void 0,
47528
47623
  triedCropJpeg: this.deps.captureCropBase64 !== void 0
47529
47624
  }
47530
47625
  });
@@ -49486,6 +49581,87 @@ var FaceRecognizer = class {
49486
49581
  }
49487
49582
  }
49488
49583
  };
49584
+ /** Read the window. Pure — this is the judgement the module exists to make. */
49585
+ function lagVerdict(report) {
49586
+ const bus = report.busLagMaxMs;
49587
+ if (bus === null) return "unknown";
49588
+ return bus >= 5e3 ? "starved" : "delivered-on-time";
49589
+ }
49590
+ /** Does this window need an operator's eyes? Unknown never counts as alarming
49591
+ * — that is the fabricated verdict D8 forbids. */
49592
+ function isAlarmingLagReport(report) {
49593
+ const bus = report.busLagMaxMs;
49594
+ const capture = report.captureLagMaxMs;
49595
+ return bus !== null && bus >= 5e3 || capture !== null && capture >= 5e3;
49596
+ }
49597
+ /**
49598
+ * Folds per-frame delivery lag into per-device windows.
49599
+ *
49600
+ * Not a logger: it returns the line to write (or null) and the caller decides
49601
+ * level and tags. That keeps the throttle — the part that can wreck the frame
49602
+ * path — testable without a logger fake.
49603
+ */
49604
+ var FrameLagObserver = class {
49605
+ windows = /* @__PURE__ */ new Map();
49606
+ /**
49607
+ * Fold one inference result into `deviceId`'s window.
49608
+ *
49609
+ * @param now Receipt time, read by the caller once at handler entry.
49610
+ * @param emittedAt Runner-side emit stamp; undefined ⇒ unknown (D8).
49611
+ * @param capturedAt Frame shm-commit stamp; undefined ⇒ unknown.
49612
+ * @returns The closed window when this frame ends one, else null.
49613
+ */
49614
+ observe(deviceId, now, emittedAt, capturedAt) {
49615
+ const existing = this.windows.get(deviceId);
49616
+ const w = existing ?? {
49617
+ startedAt: now,
49618
+ frames: 0,
49619
+ busMax: null,
49620
+ busMin: null,
49621
+ busUnknown: 0,
49622
+ captureMax: null,
49623
+ captureUnknown: 0
49624
+ };
49625
+ if (existing === void 0) this.windows.set(deviceId, w);
49626
+ w.frames += 1;
49627
+ if (typeof emittedAt === "number") {
49628
+ const busLag = now - emittedAt;
49629
+ if (w.busMax === null || busLag > w.busMax) w.busMax = busLag;
49630
+ if (w.busMin === null || busLag < w.busMin) w.busMin = busLag;
49631
+ } else w.busUnknown += 1;
49632
+ if (typeof capturedAt === "number") {
49633
+ const captureLag = now - capturedAt;
49634
+ if (w.captureMax === null || captureLag > w.captureMax) w.captureMax = captureLag;
49635
+ } else w.captureUnknown += 1;
49636
+ if (existing !== void 0 && now - w.startedAt < 3e4) return null;
49637
+ const report = {
49638
+ deviceId,
49639
+ frames: w.frames,
49640
+ busLagMaxMs: w.busMax,
49641
+ busLagMinMs: w.busMin,
49642
+ busLagUnknownFrames: w.busUnknown,
49643
+ captureLagMaxMs: w.captureMax,
49644
+ captureLagUnknownFrames: w.captureUnknown,
49645
+ windowMs: now - w.startedAt
49646
+ };
49647
+ w.startedAt = now;
49648
+ w.frames = 0;
49649
+ w.busMax = null;
49650
+ w.busMin = null;
49651
+ w.busUnknown = 0;
49652
+ w.captureMax = null;
49653
+ w.captureUnknown = 0;
49654
+ return report;
49655
+ }
49656
+ /** Drop a device's window (unbound camera, shutdown). */
49657
+ forget(deviceId) {
49658
+ this.windows.delete(deviceId);
49659
+ }
49660
+ /** Drop every window. */
49661
+ clear() {
49662
+ this.windows.clear();
49663
+ }
49664
+ };
49489
49665
  //#endregion
49490
49666
  //#region src/pipeline-analytics/location-aware-media-storage.ts
49491
49667
  /**
@@ -53808,6 +53984,17 @@ async function encodeKeyFrameVariants(native) {
53808
53984
  })
53809
53985
  };
53810
53986
  }
53987
+ /**
53988
+ * The web companion from the PARKED key frame's own JPEG (D422): the parcel is
53989
+ * stored verbatim as `keyFrame`, and `keyFrameSmall` is this — the same bytes
53990
+ * downscaled to {@link KEYFRAME_SMALL_MAX_WIDTH}. One decode, one encode,
53991
+ * never a second fetch; `withoutEnlargement` never upscales a sub-960 frame.
53992
+ * The by-handle twin ({@link encodeKeyFrameVariants}) remains for the
53993
+ * summary group shot, which still fetches a native surface by handle.
53994
+ */
53995
+ async function deriveKeyFrameSmall(keyFrameJpeg) {
53996
+ return sharp(keyFrameJpeg).resize(960, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
53997
+ }
53811
53998
  //#endregion
53812
53999
  //#region src/pipeline-analytics/pipeline/key-frame-metrics.ts
53813
54000
  var DEFAULT_FLUSH_INTERVAL_MS = 6e4;
@@ -53833,11 +54020,8 @@ var KeyFrameCaptureLog = class {
53833
54020
  w.landed += 1;
53834
54021
  if (sample.retry) w.retryLanded += 1;
53835
54022
  break;
53836
- case "native-miss":
53837
- w.nativeMiss += 1;
53838
- break;
53839
- case "ram-tier":
53840
- w.ramTier += 1;
54023
+ case "parcel-missing":
54024
+ w.parcelMissing += 1;
53841
54025
  break;
53842
54026
  case "error":
53843
54027
  w.error += 1;
@@ -53866,8 +54050,7 @@ var KeyFrameCaptureLog = class {
53866
54050
  landed: 0,
53867
54051
  retryAttempts: 0,
53868
54052
  retryLanded: 0,
53869
- nativeMiss: 0,
53870
- ramTier: 0,
54053
+ parcelMissing: 0,
53871
54054
  error: 0,
53872
54055
  skippedNodeOffline: 0
53873
54056
  };
@@ -53882,8 +54065,7 @@ var KeyFrameCaptureLog = class {
53882
54065
  landed: w.landed,
53883
54066
  retryAttempts: w.retryAttempts,
53884
54067
  retryLanded: w.retryLanded,
53885
- nativeMiss: w.nativeMiss,
53886
- ramTier: w.ramTier,
54068
+ parcelMissing: w.parcelMissing,
53887
54069
  error: w.error,
53888
54070
  skippedNodeOffline: w.skippedNodeOffline
53889
54071
  };
@@ -53892,63 +54074,44 @@ var KeyFrameCaptureLog = class {
53892
54074
  //#endregion
53893
54075
  //#region src/pipeline-analytics/pipeline/key-frame-persist.ts
53894
54076
  /**
53895
- * Fetch, encode and store one track's key frame + its 960 companion.
54077
+ * Read, derive and store one track's key frame + its web companion.
53896
54078
  *
53897
- * NATIVE-OR-NOTHING: a miss writes nothing and the caller's bounded retry
53898
- * re-fires later. The `keyFrame` write lands FIRST and its key is what the
53899
- * outcome carries — a `keyFrameSmall` failure afterwards must not re-arm a
54079
+ * PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
54080
+ * parks a later frame. The `keyFrame` write lands FIRST and its key is what
54081
+ * the outcome carries — a `keyFrameSmall` failure afterwards must not re-arm a
53900
54082
  * retry that would re-shoot a key frame the rebuild can already read.
53901
54083
  */
53902
54084
  async function persistTrackKeyFrame(deps, input) {
53903
- const { deviceId, trackId, frameHandle, retry } = input;
54085
+ const { deviceId, trackId, retry } = input;
53904
54086
  const drop = {
53905
54087
  tags: { deviceId },
53906
54088
  meta: {
53907
54089
  trackId,
53908
- nodeId: frameHandle.nodeId,
53909
54090
  retry
53910
54091
  }
53911
54092
  };
53912
54093
  try {
53913
- const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
53914
- const native = result?.frame;
53915
- if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
53916
- deps.logger.error("key-frame native miss — no upscale, will retry while budget remains", {
53917
- ...drop,
53918
- meta: {
53919
- ...drop.meta,
53920
- shmId: frameHandle.shmId
53921
- }
53922
- });
53923
- return { kind: "native-miss" };
54094
+ const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
54095
+ if (parcel === null || parcel.jpeg.byteLength === 0) {
54096
+ deps.logger.error("key-frame parcel missing nothing to write, retry parks a later frame", { ...drop });
54097
+ return { kind: "parcel-missing" };
53924
54098
  }
53925
- if (!keyFrameAcceptsTier(result.tier)) {
53926
- deps.logger.warn("keyFrame native miss — RAM tier rejected, will retry while budget remains", {
53927
- ...drop,
53928
- meta: {
53929
- ...drop.meta,
53930
- shmId: frameHandle.shmId,
53931
- tier: result.tier
53932
- }
53933
- });
53934
- return { kind: "ram-tier" };
53935
- }
53936
- const { keyFrame, keyFrameSmall } = await deps.encodeVariants(native);
53937
54099
  const keyFrameKey = await deps.media.putReplacing({
53938
54100
  deviceId,
53939
54101
  ownerKind: "track",
53940
54102
  ownerId: trackId,
53941
54103
  kind: "keyFrame",
53942
- timestamp: input.timestamp,
53943
- data: keyFrame
54104
+ timestamp: parcel.timestamp,
54105
+ data: parcel.jpeg
53944
54106
  });
53945
54107
  try {
54108
+ const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
53946
54109
  await deps.media.putReplacing({
53947
54110
  deviceId,
53948
54111
  ownerKind: "track",
53949
54112
  ownerId: trackId,
53950
54113
  kind: "keyFrameSmall",
53951
- timestamp: input.timestamp,
54114
+ timestamp: parcel.timestamp,
53952
54115
  data: keyFrameSmall
53953
54116
  });
53954
54117
  } catch (err) {
@@ -53956,17 +54119,29 @@ async function persistTrackKeyFrame(deps, input) {
53956
54119
  ...drop,
53957
54120
  meta: {
53958
54121
  ...drop.meta,
54122
+ nodeId: parcel.nodeId,
53959
54123
  error: errMsg$1(err)
53960
54124
  }
53961
54125
  });
53962
54126
  }
54127
+ deps.logger.debug("key frame written from the parked parcel", {
54128
+ ...drop,
54129
+ meta: {
54130
+ ...drop.meta,
54131
+ nodeId: parcel.nodeId,
54132
+ width: parcel.width,
54133
+ height: parcel.height,
54134
+ bytes: parcel.jpeg.byteLength,
54135
+ parkedTimestamp: parcel.timestamp
54136
+ }
54137
+ });
53963
54138
  return {
53964
54139
  kind: "landed",
53965
54140
  keyFrameKey
53966
54141
  };
53967
54142
  } catch (err) {
53968
54143
  const error = errMsg$1(err);
53969
- deps.logger.warn("key-frame capture failed", {
54144
+ deps.logger.warn("key-frame write failed", {
53970
54145
  ...drop,
53971
54146
  meta: {
53972
54147
  ...drop.meta,
@@ -54153,6 +54328,61 @@ function createKeyFrameCrop(deps) {
54153
54328
  };
54154
54329
  }
54155
54330
  //#endregion
54331
+ //#region src/pipeline-analytics/pipeline/track-park.ts
54332
+ /** The whole frame — what a `keyFrame` park cuts. */
54333
+ var FULL_FRAME = {
54334
+ x: 0,
54335
+ y: 0,
54336
+ w: 1,
54337
+ h: 1
54338
+ };
54339
+ /** The two parcels a best-frame decision orders, in a fixed order. */
54340
+ function buildParkOrders(decision) {
54341
+ const subject = {
54342
+ bbox: { ...decision.bbox },
54343
+ frameWidth: decision.frameWidth,
54344
+ frameHeight: decision.frameHeight
54345
+ };
54346
+ const layout = deliveredWideCentralSquareLayout(decision.bbox, {
54347
+ W: decision.frameWidth,
54348
+ H: decision.frameHeight
54349
+ });
54350
+ const base = {
54351
+ deviceId: decision.deviceId,
54352
+ trackId: decision.trackId,
54353
+ handle: decision.frameHandle,
54354
+ timestamp: decision.timestamp,
54355
+ subject
54356
+ };
54357
+ return [{
54358
+ ...base,
54359
+ kind: "keyFrame",
54360
+ bbox: FULL_FRAME,
54361
+ maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
54362
+ }, {
54363
+ ...base,
54364
+ kind: "thumbnail",
54365
+ bbox: layout.fetch,
54366
+ layout
54367
+ }];
54368
+ }
54369
+ /**
54370
+ * The subject's box in the PARCEL's pixel space. The box was measured on the
54371
+ * ≤640 detection raster; the parcel is native-resolution. Both show the same
54372
+ * scene at the same aspect, so a uniform per-axis scale places the box on the
54373
+ * parcel — the runner then derives the crop rectangle from it (D52).
54374
+ */
54375
+ function scaleSubjectIntoParcel(subject, parcelWidth, parcelHeight) {
54376
+ const sx = parcelWidth / subject.frameWidth;
54377
+ const sy = parcelHeight / subject.frameHeight;
54378
+ return {
54379
+ x: subject.bbox.x * sx,
54380
+ y: subject.bbox.y * sy,
54381
+ w: subject.bbox.w * sx,
54382
+ h: subject.bbox.h * sy
54383
+ };
54384
+ }
54385
+ //#endregion
54156
54386
  //#region src/pipeline-analytics/pipeline/native-detection.ts
54157
54387
  /**
54158
54388
  * Nominal frame size used to denormalize native `[0,1]` boxes when a
@@ -58683,7 +58913,28 @@ async function reconcileGallerySamples(deps, options) {
58683
58913
  }
58684
58914
  //#endregion
58685
58915
  //#region src/pipeline-analytics/summary/summary-store.ts
58916
+ /**
58917
+ * @durable class=ledger owner=pipeline-analytics
58918
+ * write="one row per SUMMARY — a window of co-present tracks on one camera
58919
+ * group. Opened by `openOrAbsorb` when the first member arrives, updated
58920
+ * in place while the window absorbs more, and finalised by `seal` when the
58921
+ * window closes. Per WINDOW, never per frame or per track: the sealed
58922
+ * summary is what a notification rule reads."
58923
+ * retention="age-keyed on `sealedAt`, swept with the rest of the analytics
58924
+ * retention. An unsealed row belongs to a window still open and is never
58925
+ * swept by age — it is closed or cascaded away with its members."
58926
+ */
58686
58927
  var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
58928
+ /**
58929
+ * @durable class=ledger owner=pipeline-analytics
58930
+ * write="one row per (summary, track) membership, written as a window
58931
+ * absorbs a track. The join that lets a sealed summary name its subjects
58932
+ * without re-reading the track store."
58933
+ * retention="cascaded — a member dies with its summary (`delete-track-cascade`
58934
+ * counts them as leaves) or with the track it names. It carries no clock of
58935
+ * its own: an age sweep here could outlive or predecease the summary the
58936
+ * row only means anything inside of."
58937
+ */
58687
58938
  var SUMMARY_MEMBERS_COLLECTION = "pipeline-analytics:summary-members";
58688
58939
  var SUMMARIES_COLUMNS = [
58689
58940
  {
@@ -63511,35 +63762,6 @@ function createNativeFrameTransport(deps) {
63511
63762
  const getRemoteFrame = async (handle) => requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame);
63512
63763
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame);
63513
63764
  const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
63514
- const getFullFrameJpegBase64 = async (handle, maxWidth) => {
63515
- if (!pipelineRunnerApi?.getNativeCrop) return null;
63516
- try {
63517
- const full = await pipelineRunnerApi.getNativeCrop.query({
63518
- handle,
63519
- bbox: {
63520
- x: 0,
63521
- y: 0,
63522
- w: 1,
63523
- h: 1
63524
- },
63525
- maxWidth,
63526
- encodeJpeg: true
63527
- }, nodePin(handle.nodeId));
63528
- if (!full?.jpeg || full.width <= 0 || full.height <= 0) return null;
63529
- return {
63530
- base64: full.jpeg,
63531
- width: full.width,
63532
- height: full.height,
63533
- tier: full.tier === "ram-fullframe" ? "ram-fullframe" : "native"
63534
- };
63535
- } catch (err) {
63536
- logger.debug("full-frame jpeg fetch failed", { meta: {
63537
- nodeId: handle.nodeId,
63538
- error: errMsg$1(err)
63539
- } });
63540
- return null;
63541
- }
63542
- };
63543
63765
  const cropMetricLogger = logger.child("NativeCrop");
63544
63766
  let nativeHits = 0;
63545
63767
  let nativeFallbacks = 0;
@@ -63619,7 +63841,6 @@ function createNativeFrameTransport(deps) {
63619
63841
  getRemoteFrame,
63620
63842
  getNativeFullFrameRgb,
63621
63843
  getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
63622
- getFullFrameJpegBase64,
63623
63844
  getNativeCropJpeg,
63624
63845
  captureCrop: createCaptureCrop({
63625
63846
  tryNativeCrop,
@@ -63730,6 +63951,233 @@ function parseTopologySnapshotNodes(data) {
63730
63951
  return parsed;
63731
63952
  }
63732
63953
  //#endregion
63954
+ //#region src/pipeline-analytics/runtime/parked-frame-client.ts
63955
+ /**
63956
+ * Bound on the mirror. A track's record leaves at release; this is the
63957
+ * backstop for a lost close. At the busiest measured hub (~12 sessions,
63958
+ * a few tracks each) 4 096 is an hour of tracks, past which the OLDEST record
63959
+ * is forgotten — its parcel still ages out on the node on its own.
63960
+ */
63961
+ var MAX_MIRRORED_TRACKS = 4096;
63962
+ /** One "runner has no park method" line per camera per minute. */
63963
+ var SKEW_WARN_THROTTLE_MS = 6e4;
63964
+ function inFlightKey(trackId, kind) {
63965
+ return `${trackId} ${kind}`;
63966
+ }
63967
+ /** Strip the hub-side context off an order: only the wire fields travel. */
63968
+ function toWireInput(order) {
63969
+ return {
63970
+ deviceId: order.deviceId,
63971
+ trackId: order.trackId,
63972
+ kind: order.kind,
63973
+ handle: order.handle,
63974
+ bbox: order.bbox,
63975
+ ...order.maxWidth !== void 0 ? { maxWidth: order.maxWidth } : {},
63976
+ timestamp: order.timestamp
63977
+ };
63978
+ }
63979
+ function createParkedFrameClient(deps) {
63980
+ const { api, logger } = deps;
63981
+ const now = deps.now ?? Date.now;
63982
+ /** trackId → kind → what the hub kept beside the parcel. */
63983
+ const mirror = /* @__PURE__ */ new Map();
63984
+ const inFlight = /* @__PURE__ */ new Map();
63985
+ const skewWarnedAt = /* @__PURE__ */ new Map();
63986
+ const remember = (order, answer) => {
63987
+ let kinds = mirror.get(order.trackId);
63988
+ if (kinds === void 0) {
63989
+ kinds = /* @__PURE__ */ new Map();
63990
+ mirror.set(order.trackId, kinds);
63991
+ while (mirror.size > MAX_MIRRORED_TRACKS) {
63992
+ const oldest = mirror.keys().next().value;
63993
+ if (oldest === void 0) break;
63994
+ mirror.delete(oldest);
63995
+ }
63996
+ }
63997
+ kinds.set(order.kind, {
63998
+ nodeId: answer.nodeId,
63999
+ deviceId: order.deviceId,
64000
+ kind: order.kind,
64001
+ timestamp: order.timestamp,
64002
+ width: answer.width,
64003
+ height: answer.height,
64004
+ subject: order.subject,
64005
+ ...order.layout !== void 0 ? { layout: order.layout } : {}
64006
+ });
64007
+ };
64008
+ const warnSkew = (order) => {
64009
+ const at = skewWarnedAt.get(order.deviceId) ?? 0;
64010
+ if (now() - at < SKEW_WARN_THROTTLE_MS) return;
64011
+ skewWarnedAt.set(order.deviceId, now());
64012
+ logger.warn("track frame park impossible — the runner api has no park method (version skew)", {
64013
+ tags: { deviceId: order.deviceId },
64014
+ meta: {
64015
+ trackId: order.trackId,
64016
+ kind: order.kind,
64017
+ nodeId: order.handle.nodeId
64018
+ }
64019
+ });
64020
+ };
64021
+ const sendPark = async (order) => {
64022
+ if (api === void 0) {
64023
+ warnSkew(order);
64024
+ return false;
64025
+ }
64026
+ if (deps.isNodeOffline?.(order.handle.nodeId) === true) {
64027
+ logger.debug("track frame park skipped — the owning node is offline in the registry", {
64028
+ tags: { deviceId: order.deviceId },
64029
+ meta: {
64030
+ trackId: order.trackId,
64031
+ kind: order.kind,
64032
+ nodeId: order.handle.nodeId
64033
+ }
64034
+ });
64035
+ return false;
64036
+ }
64037
+ const meta = {
64038
+ trackId: order.trackId,
64039
+ kind: order.kind,
64040
+ nodeId: order.handle.nodeId,
64041
+ handle: `${order.handle.shmId}#${order.handle.slot}#${order.handle.seq}`
64042
+ };
64043
+ try {
64044
+ const answer = await api.parkTrackFrame.mutate(toWireInput(order), nodePin(order.handle.nodeId));
64045
+ if (answer.parked) {
64046
+ remember(order, answer);
64047
+ logger.debug("track frame parked", {
64048
+ tags: { deviceId: order.deviceId },
64049
+ meta: {
64050
+ ...meta,
64051
+ bytes: answer.bytes,
64052
+ replaced: answer.replaced
64053
+ }
64054
+ });
64055
+ return true;
64056
+ }
64057
+ logger.warn("track frame park refused by the owning node", {
64058
+ tags: { deviceId: order.deviceId },
64059
+ meta: {
64060
+ ...meta,
64061
+ reason: answer.reason,
64062
+ handleAgeMs: answer.handleAgeMs,
64063
+ ...answer.detail !== void 0 ? { detail: answer.detail } : {}
64064
+ }
64065
+ });
64066
+ return false;
64067
+ } catch (err) {
64068
+ logger.warn("track frame park failed — the order did not reach the node", {
64069
+ tags: { deviceId: order.deviceId },
64070
+ meta: {
64071
+ ...meta,
64072
+ error: errMsg$1(err)
64073
+ }
64074
+ });
64075
+ return false;
64076
+ }
64077
+ };
64078
+ const park = (order) => {
64079
+ const key = inFlightKey(order.trackId, order.kind);
64080
+ const pending = inFlight.get(key);
64081
+ if (pending !== void 0) return pending;
64082
+ const run = sendPark(order).finally(() => {
64083
+ if (inFlight.get(key) === run) inFlight.delete(key);
64084
+ });
64085
+ inFlight.set(key, run);
64086
+ return run;
64087
+ };
64088
+ const awaitPark = async (trackId, kind) => {
64089
+ const pending = inFlight.get(inFlightKey(trackId, kind));
64090
+ if (pending !== void 0) await pending;
64091
+ };
64092
+ const info = (trackId, kind) => mirror.get(trackId)?.get(kind);
64093
+ const fetch = async (trackId, kind) => {
64094
+ const known = info(trackId, kind);
64095
+ if (known === void 0 || api === void 0) return null;
64096
+ if (deps.isNodeOffline?.(known.nodeId) === true) {
64097
+ logger.warn("parked frame retrieval skipped — the holding node is offline in the registry", {
64098
+ tags: { deviceId: known.deviceId },
64099
+ meta: {
64100
+ trackId,
64101
+ kind,
64102
+ nodeId: known.nodeId
64103
+ }
64104
+ });
64105
+ return null;
64106
+ }
64107
+ try {
64108
+ const parcel = await api.getParkedTrackFrame.query({
64109
+ deviceId: known.deviceId,
64110
+ trackId,
64111
+ kind
64112
+ }, nodePin(known.nodeId));
64113
+ if (parcel === null) {
64114
+ logger.warn("parked frame gone at retrieval — the node no longer holds it", {
64115
+ tags: { deviceId: known.deviceId },
64116
+ meta: {
64117
+ trackId,
64118
+ kind,
64119
+ nodeId: known.nodeId,
64120
+ parkedTimestamp: known.timestamp
64121
+ }
64122
+ });
64123
+ return null;
64124
+ }
64125
+ return {
64126
+ jpeg: Buffer.from(parcel.jpeg, "base64"),
64127
+ width: parcel.width,
64128
+ height: parcel.height,
64129
+ timestamp: parcel.timestamp,
64130
+ info: known
64131
+ };
64132
+ } catch (err) {
64133
+ logger.warn("parked frame retrieval failed", {
64134
+ tags: { deviceId: known.deviceId },
64135
+ meta: {
64136
+ trackId,
64137
+ kind,
64138
+ nodeId: known.nodeId,
64139
+ error: errMsg$1(err)
64140
+ }
64141
+ });
64142
+ return null;
64143
+ }
64144
+ };
64145
+ const release = (trackId) => {
64146
+ const kinds = mirror.get(trackId);
64147
+ if (kinds === void 0) return;
64148
+ mirror.delete(trackId);
64149
+ if (api === void 0) return;
64150
+ const targets = /* @__PURE__ */ new Map();
64151
+ for (const record of kinds.values()) targets.set(record.nodeId, record.deviceId);
64152
+ for (const [nodeId, deviceId] of targets) api.releaseParkedTrackFrames.mutate({
64153
+ deviceId,
64154
+ trackId
64155
+ }, nodePin(nodeId)).catch((err) => {
64156
+ logger.debug("parked frame release did not reach the node", {
64157
+ tags: { deviceId },
64158
+ meta: {
64159
+ trackId,
64160
+ nodeId,
64161
+ error: errMsg$1(err)
64162
+ }
64163
+ });
64164
+ });
64165
+ };
64166
+ const dispose = () => {
64167
+ mirror.clear();
64168
+ inFlight.clear();
64169
+ skewWarnedAt.clear();
64170
+ };
64171
+ return {
64172
+ park,
64173
+ awaitPark,
64174
+ info,
64175
+ fetch,
64176
+ release,
64177
+ dispose
64178
+ };
64179
+ }
64180
+ //#endregion
63733
64181
  //#region src/pipeline-analytics/runtime/widget-manifest.ts
63734
64182
  /** The remote-module coordinates shared by every analytics widget. */
63735
64183
  var REMOTE_NAME = "addon_pipeline_analytics_widgets";
@@ -64831,7 +65279,8 @@ function isUniformRgbFrame(data, width, height) {
64831
65279
  * frame downscaled to {@link SNAPSHOT_MAX_WIDTH} (960) with ONLY the track's own
64832
65280
  * box burned in, falling back to the ≤640 detection frame (honest lower res,
64833
65281
  * never an upscale) on a native miss. The best `thumbnail` stays a clean,
64834
- * uncapped native subject crop (native-or-nothing).
65282
+ * uncapped native subject crop — read from the track's PARKED slab, by track
65283
+ * (D422), never by handle.
64835
65284
  *
64836
65285
  * The detection frame is resolved ONCE per call (blank-frame guard); native full
64837
65286
  * frames + subject crops are fetched per target by handle. Frames recycle in
@@ -65099,10 +65548,10 @@ var EventMediaDispatcher = class {
65099
65548
  if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
65100
65549
  let thumbnailWritten = false;
65101
65550
  if (sn.bestThumbnail) {
65102
- const variants = await this.cropSubjectVariants(deviceId, sn.trackId, frameHandle, fw, fh, sn.bbox);
65551
+ const variants = await this.cropSubjectVariants(deviceId, sn.trackId);
65103
65552
  if (variants) {
65104
- thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail, "native");
65105
- await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall, "native");
65553
+ thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", variants.timestamp, variants.thumbnail, "native");
65554
+ await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", variants.timestamp, variants.thumbnailSmall, "native");
65106
65555
  }
65107
65556
  }
65108
65557
  return {
@@ -65180,81 +65629,49 @@ var EventMediaDispatcher = class {
65180
65629
  }
65181
65630
  /**
65182
65631
  * The best-shot subject crop as its TWO persisted variants (best-crop
65183
- * fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22). ONE native
65184
- * fetch: the in-frame slab of the 16:9 central-square WINDOW
65185
- * ({@link wideCentralSquareLayout} a `side × 16/9` window whose middle square
65186
- * of side `max(w,h)×1.2` fully contains the subject, clamped/anchored so the
65187
- * subject stays in that central square at every frame edge) is requested from
65188
- * the runner's retained native surface with NO `maxWidth` (uncapped TRUE
65189
- * native). It is composed into the full 16:9 window (lateral letterbox only for
65190
- * the part outside the frame) the `thumbnail`. The `thumbnailSmall` is
65191
- * DERIVED by downscaling that SAME window to the 480 long-side cap (480×270)
65192
- * never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
65193
- * never upscales a smaller window).
65194
- *
65195
- * Both variants share the SAME framing + central-square containment guarantee:
65196
- * wide consumers use the 16:9 image as-is (subject centered, no cut); square
65197
- * consumers center-crop the middle square (subject always inside it).
65198
- *
65199
- * NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
65200
- * `null` after a loud `logger.warn`; the caller SKIPS the write and the
65201
- * per-frame retry lands a real native crop later. Never a local resize
65202
- * upscale of a ≤640 tile (a blurred lie).
65203
- */
65204
- async cropSubjectVariants(deviceId, trackId, frameHandle, fw, fh, bbox) {
65205
- if (!this.deps.getNativeCropJpeg) {
65206
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", {
65207
- tags: { deviceId },
65208
- meta: {
65209
- shmId: frameHandle.shmId,
65210
- reason: "no-native-cap"
65211
- }
65212
- });
65213
- this.deps.onSubjectCropMiss?.(deviceId, trackId, "no-native-cap");
65214
- return null;
65215
- }
65632
+ * fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22), from the
65633
+ * track's PARKED slab (D422). ONE read, by `(deviceId, trackId)`: the
65634
+ * in-frame slab of the 16:9 central-square WINDOW
65635
+ * ({@link wideCentralSquareLayout}) the runner cut at uncapped native
65636
+ * resolution the instant the track's best was decided. It is composed into
65637
+ * the full 16:9 window (lateral letterbox only for the part outside the
65638
+ * frame) the `thumbnail`. The `thumbnailSmall` is DERIVED by downscaling
65639
+ * that SAME window to the 480 long-side cap never a second read, never
65640
+ * upscaled ({@link deriveThumbnailSmall} never upscales a smaller window).
65641
+ *
65642
+ * Both variants share the SAME framing + central-square containment
65643
+ * guarantee: wide consumers use the 16:9 image as-is; square consumers
65644
+ * center-crop the middle square (subject always inside it).
65645
+ *
65646
+ * PARCEL-OR-NOTHING: no parcel (or a throw) returns `null` after a loud
65647
+ * `logger.warn`; the caller SKIPS the write and the per-frame retry parks a
65648
+ * later frame. Never a by-handle crop, never a local upscale of a ≤640 tile.
65649
+ */
65650
+ async cropSubjectVariants(deviceId, trackId) {
65216
65651
  try {
65217
- const layout = deliveredWideCentralSquareLayout(bbox, {
65218
- W: fw,
65219
- H: fh
65220
- });
65221
- if (centralSquareContainmentFraction(bbox, {
65222
- W: fw,
65223
- H: fh
65224
- }) < .5) this.deps.logger.info("best-shot delivered via contained square layout (edge subject)", {
65225
- tags: { deviceId },
65226
- meta: {
65227
- bbox,
65228
- frameW: fw,
65229
- frameH: fh
65230
- }
65231
- });
65232
- const askedAt = Date.now();
65233
- const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch, void 0, deviceId);
65234
- if (!slab) {
65235
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", {
65652
+ const parked = await this.deps.getParkedSubjectSlab(deviceId, trackId);
65653
+ if (!parked) {
65654
+ this.deps.logger.warn("best-shot parcel missing — no thumbnail this frame, will retry", {
65236
65655
  tags: { deviceId },
65237
65656
  meta: {
65238
- shmId: frameHandle.shmId,
65239
- handle: `${frameHandle.shmId}#${frameHandle.slot}#${frameHandle.seq}`,
65240
- handleNodeId: frameHandle.nodeId,
65241
- roundTripMs: Date.now() - askedAt,
65242
- reason: "native-miss"
65657
+ trackId,
65658
+ reason: "parcel-missing"
65243
65659
  }
65244
65660
  });
65245
- this.deps.onSubjectCropMiss?.(deviceId, trackId, "native-miss");
65661
+ this.deps.onSubjectCropMiss?.(deviceId, trackId, "parcel-missing");
65246
65662
  return null;
65247
65663
  }
65248
- const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
65664
+ const thumbnail = await composeWideCentralSquareThumbnail(parked.slab, parked.layout);
65249
65665
  return {
65250
65666
  thumbnail,
65251
- thumbnailSmall: await deriveThumbnailSmall(thumbnail)
65667
+ thumbnailSmall: await deriveThumbnailSmall(thumbnail),
65668
+ timestamp: parked.timestamp
65252
65669
  };
65253
65670
  } catch (err) {
65254
- this.deps.logger.warn("native subject crop misswill retry, no upscale", {
65671
+ this.deps.logger.warn("best-shot parcel read failedno thumbnail this frame, will retry", {
65255
65672
  tags: { deviceId },
65256
65673
  meta: {
65257
- shmId: frameHandle.shmId,
65674
+ trackId,
65258
65675
  reason: "threw",
65259
65676
  error: err instanceof Error ? err.message : String(err)
65260
65677
  }
@@ -70545,7 +70962,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70545
70962
  * the FULL cleanup litany on every teardown path (and cancels the track's
70546
70963
  * queued captures via the onClose hook) and `closeDevice` finally clears
70547
70964
  * per-track state on device removal (the two documented leaks). */
70548
- residents = new TrackResidentState({ onClose: (trackId) => this.captureScheduler.cancelForTrack(trackId) });
70965
+ residents = new TrackResidentState({ onClose: (trackId) => {
70966
+ this.captureScheduler.cancelForTrack(trackId);
70967
+ this.parkedFrames?.release(trackId);
70968
+ } });
70969
+ /**
70970
+ * The hub's side of a PARK (D422): orders the owning node to keep a track's
70971
+ * best frame the instant `BestDetectionTracker` accepts it, and answers the
70972
+ * keyFrame writer, the best-shot thumbnail and the detail plane's full-frame
70973
+ * rung by `(deviceId, trackId, kind)` — never by handle. Null until init.
70974
+ */
70975
+ parkedFrames = null;
70549
70976
  /** Defence in depth for the 2026-08-21 `b50e09f7` incident: a row the track
70550
70977
  * store has already CLOSED never earns another media target, whatever the
70551
70978
  * tracker keeps handing back. Reads the store's in-RAM close record only —
@@ -70655,6 +71082,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70655
71082
  * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
70656
71083
  getNativeKeyFrameRgb = null;
70657
71084
  shuttingDown = false;
71085
+ /**
71086
+ * Per-device delivery-lag windows, folded at `handleInferenceResult` entry.
71087
+ * The instrument that tells a late DELIVERY (starved) apart from a slow
71088
+ * `processFrame` (waiting) — see `frame-lag-observer.ts`. Costs one map read
71089
+ * and ~10 numeric ops per frame, and writes one line per device per 30 s.
71090
+ */
71091
+ frameLag = new FrameLagObserver();
71092
+ /**
71093
+ * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
71094
+ *
71095
+ * The other half of the frame-lag line: that one says whether the events
71096
+ * arrived late, this one says whether THIS loop was blocked while they did.
71097
+ * The 5-minute `[mem]` heartbeat cannot see a 10 s freeze — it fired at
71098
+ * dt = 300020 / 300066 / 299991 ms straight through the 2026-09-09 silences.
71099
+ */
71100
+ stopStallMonitor = null;
70658
71101
  /** True only on the cluster's designated post-processing node. When false the
70659
71102
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
70660
71103
  isPostProcessingNode = true;
@@ -70873,6 +71316,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70873
71316
  super({});
70874
71317
  }
70875
71318
  async onInitialize() {
71319
+ this.stopStallMonitor = startEventLoopStallMonitor(this.ctx.logger);
70876
71320
  const rawApi = this.ctx.api;
70877
71321
  if (!rawApi) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
70878
71322
  const api = censusApi(rawApi, this.readCensus);
@@ -70923,17 +71367,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70923
71367
  noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
70924
71368
  });
70925
71369
  this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
71370
+ this.parkedFrames = createParkedFrameClient({
71371
+ api: api.pipelineRunner,
71372
+ logger: logger.child("ParkedFrames"),
71373
+ isNodeOffline: (nodeId) => this.nodeLiveness.isKnownOffline(nodeId, Date.now())
71374
+ });
70926
71375
  this.captureCrop = transport.captureCrop;
70927
71376
  this.eventMediaDispatcher = new EventMediaDispatcher({
70928
71377
  getRemoteFrame: transport.getRemoteFrame,
70929
71378
  getNativeCropJpeg: transport.getNativeCropJpeg,
70930
71379
  getNativeFullFrameRgb: transport.getNativeFullFrameRgb,
71380
+ getParkedSubjectSlab: (deviceId, trackId) => this.fetchParkedSubjectSlab(deviceId, trackId),
70931
71381
  mediaStore: stores.mediaStore,
70932
71382
  logger: logger.child("EventMediaDispatcher"),
70933
71383
  onSubjectCropMiss: (deviceId, trackId, reason) => this.residents.notePreviewCaptureMiss(deviceId, trackId, `thumbnail:${reason}`)
70934
71384
  });
70935
71385
  this.buildRecognizers(api, logger, stores, transport);
70936
- this.buildDetailDispatcher(api, logger, transport);
71386
+ this.buildDetailDispatcher(api, logger);
70937
71387
  const capProviders = this.buildAnalyticsProviders(api, logger);
70938
71388
  await step("buildNotificationCenter", () => this.buildNotificationCenter(api, logger, stores));
70939
71389
  await step("serveEventMediaDataPlane", () => this.serveEventMediaDataPlane());
@@ -71763,7 +72213,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71763
72213
  * demand via `pipelineRunner.runDetailSubtree`. The dispatcher owns the
71764
72214
  * per-device schedulers, concurrency cap, and 1 s tick; result routing +
71765
72215
  * the pinned call + crop fallback are injected here. */
71766
- buildDetailDispatcher(api, logger, transport) {
72216
+ buildDetailDispatcher(api, logger) {
71767
72217
  const runnerApi = api.pipelineRunner;
71768
72218
  this.detailDispatcher = new TrackDetailDispatcher({
71769
72219
  logger: logger.child("DetailDispatcher"),
@@ -71773,13 +72223,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71773
72223
  return runnerApi.runDetailSubtree.mutate(input);
71774
72224
  },
71775
72225
  routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
71776
- captureFullFrameBase64: async (frame) => {
71777
- if (frame.frameHandle === void 0) return null;
71778
- const full = await transport.getFullFrameJpegBase64(frame.frameHandle, frame.frameWidth);
71779
- return full === null ? null : {
71780
- base64: full.base64,
71781
- width: full.width,
71782
- height: full.height
72226
+ captureParkedFrame: async (_deviceId, trackId) => {
72227
+ const parcel = await this.fetchParkedParcel(trackId, "keyFrame");
72228
+ if (parcel === null) return null;
72229
+ return {
72230
+ base64: parcel.jpeg.toString("base64"),
72231
+ width: parcel.width,
72232
+ height: parcel.height,
72233
+ bbox: scaleSubjectIntoParcel(parcel.info.subject, parcel.width, parcel.height),
72234
+ nodeId: parcel.info.nodeId,
72235
+ timestamp: parcel.timestamp
71783
72236
  };
71784
72237
  },
71785
72238
  captureCropBase64: async (frame) => {
@@ -73377,6 +73830,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73377
73830
  }
73378
73831
  async onShutdown() {
73379
73832
  this.shuttingDown = true;
73833
+ if (this.stopStallMonitor) {
73834
+ this.stopStallMonitor();
73835
+ this.stopStallMonitor = null;
73836
+ }
73380
73837
  this.sceneEngine?.stop();
73381
73838
  this.sceneEngine = null;
73382
73839
  if (this.sceneReloadTimer !== null) {
@@ -73417,10 +73874,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73417
73874
  this.audioMarkers?.flush(Date.now());
73418
73875
  this.detailDispatcher?.dispose();
73419
73876
  this.detailDispatcher = null;
73877
+ this.parkedFrames?.dispose();
73420
73878
  this.overlayState.clear();
73421
73879
  this.overlaySynthesisWarnAt.clear();
73422
73880
  this.processors.clear();
73423
73881
  this.lastActiveTrackIds.clear();
73882
+ this.frameLag.clear();
73424
73883
  this.lastFrameDimsByDevice.clear();
73425
73884
  this.dropoutSkipsByKey.clear();
73426
73885
  this.residents.clearAll();
@@ -73460,6 +73919,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73460
73919
  async handleInferenceResult(data) {
73461
73920
  if (this.shuttingDown) return;
73462
73921
  const { deviceId, frame } = data;
73922
+ const lag = this.frameLag.observe(deviceId, Date.now(), data.emittedAt, data.capturedAt);
73923
+ if (lag !== null) {
73924
+ const verdict = lagVerdict(lag);
73925
+ (isAlarmingLagReport(lag) ? this.ctx.logger.warn.bind(this.ctx.logger) : this.ctx.logger.info.bind(this.ctx.logger))("pipeline-analytics frame lag", {
73926
+ tags: { deviceId },
73927
+ meta: {
73928
+ verdict,
73929
+ frames: lag.frames,
73930
+ windowMs: lag.windowMs,
73931
+ busLagMaxMs: lag.busLagMaxMs,
73932
+ busLagMinMs: lag.busLagMinMs,
73933
+ busLagUnknownFrames: lag.busLagUnknownFrames,
73934
+ captureLagMaxMs: lag.captureLagMaxMs,
73935
+ captureLagUnknownFrames: lag.captureLagUnknownFrames
73936
+ }
73937
+ });
73938
+ }
73463
73939
  await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
73464
73940
  }
73465
73941
  /**
@@ -74057,7 +74533,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74057
74533
  let plateCrops = 0;
74058
74534
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
74059
74535
  else plateCrops += 1;
74060
- const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
74536
+ const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
74061
74537
  this.offerGroupShot(deviceId, result, frameHandle);
74062
74538
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
74063
74539
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
@@ -74176,7 +74652,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74176
74652
  }).catch(() => {});
74177
74653
  }
74178
74654
  const keyFrameCaptures = selectKeyFrameCaptures(snapshotTargets);
74179
- if (keyFrameCaptures.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameCaptures, frameHandle);
74655
+ if (keyFrameCaptures.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameCaptures);
74180
74656
  }
74181
74657
  if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
74182
74658
  deviceId,
@@ -75106,10 +75582,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75106
75582
  });
75107
75583
  }).catch(() => {});
75108
75584
  }
75109
- async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
75110
- const getNative = this.getNativeKeyFrameRgb;
75585
+ /**
75586
+ * Order the parcels a best-frame decision wants (D422). Skipped entirely at
75587
+ * a node the registry already called offline (D58) — logged on the
75588
+ * transition by `noteCapturesGated`, counted in the key-frame window.
75589
+ * `onlyIfMissing` is the retry rule: a later frame may fill a kind that has
75590
+ * no parcel, never replace one the ranking preferred.
75591
+ */
75592
+ issueParkOrders(decision, kinds, onlyIfMissing) {
75593
+ const client = this.parkedFrames;
75594
+ if (client === null) return;
75595
+ if (this.nodeLiveness.isKnownOffline(decision.frameHandle.nodeId, Date.now())) {
75596
+ this.noteCapturesGated(decision.deviceId, decision.frameHandle.nodeId);
75597
+ return;
75598
+ }
75599
+ for (const order of buildParkOrders(decision)) {
75600
+ if (!kinds.includes(order.kind)) continue;
75601
+ if (onlyIfMissing && client.info(order.trackId, order.kind) !== void 0) continue;
75602
+ client.park(order);
75603
+ }
75604
+ }
75605
+ /** A parcel by track, after any park in flight for it has landed. */
75606
+ async fetchParkedParcel(trackId, kind) {
75607
+ const client = this.parkedFrames;
75608
+ if (client === null) return null;
75609
+ await client.awaitPark(trackId, kind);
75610
+ return client.fetch(trackId, kind);
75611
+ }
75612
+ /** The best-shot slab for the dispatcher: the parcel plus the layout the hub kept. */
75613
+ async fetchParkedSubjectSlab(_deviceId, trackId) {
75614
+ const parcel = await this.fetchParkedParcel(trackId, "thumbnail");
75615
+ if (parcel === null || parcel.info.layout === void 0) return null;
75616
+ return {
75617
+ slab: parcel.jpeg,
75618
+ layout: parcel.info.layout,
75619
+ timestamp: parcel.timestamp
75620
+ };
75621
+ }
75622
+ async persistKeyFrames(deviceId, timestamp, captures) {
75111
75623
  const mediaStore = this.mediaStore;
75112
- if (!getNative || !mediaStore) return;
75624
+ if (this.parkedFrames === null || !mediaStore) return;
75113
75625
  await Promise.all(captures.map((capture) => this.captureScheduler.request({
75114
75626
  deviceId,
75115
75627
  trackId: capture.trackId,
@@ -75119,16 +75631,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75119
75631
  exec: async () => {
75120
75632
  this.residents.noteKeyFrameAttempt(deviceId, capture.trackId, timestamp, capture.retry);
75121
75633
  const outcome = await persistTrackKeyFrame({
75122
- fetchNativeFullFrame: getNative,
75123
- encodeVariants: encodeKeyFrameVariants,
75634
+ fetchParkedKeyFrame: async (parcelDeviceId, trackId) => {
75635
+ const parcel = await this.fetchParkedParcel(trackId, "keyFrame");
75636
+ if (parcel === null || parcel.info.deviceId !== parcelDeviceId) return null;
75637
+ return {
75638
+ jpeg: parcel.jpeg,
75639
+ width: parcel.width,
75640
+ height: parcel.height,
75641
+ timestamp: parcel.timestamp,
75642
+ nodeId: parcel.info.nodeId
75643
+ };
75644
+ },
75645
+ deriveSmall: deriveKeyFrameSmall,
75124
75646
  media: mediaStore,
75125
75647
  logger: this.ctx.logger
75126
75648
  }, {
75127
75649
  deviceId,
75128
75650
  trackId: capture.trackId,
75129
- timestamp,
75130
- frameHandle,
75131
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
75132
75651
  retry: capture.retry
75133
75652
  });
75134
75653
  if (outcome.kind === "landed") {
@@ -75208,10 +75727,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75208
75727
  isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
75209
75728
  captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
75210
75729
  captureKeyFrame: async (req) => {
75730
+ this.issueParkOrders({
75731
+ deviceId: req.deviceId,
75732
+ trackId: req.trackId,
75733
+ timestamp: req.timestamp,
75734
+ frameHandle: req.frameHandle,
75735
+ bbox: { ...req.bodyBbox },
75736
+ frameWidth: req.frameWidth,
75737
+ frameHeight: req.frameHeight
75738
+ }, ["keyFrame"], true);
75211
75739
  await this.persistKeyFrames(req.deviceId, req.timestamp, [{
75212
75740
  trackId: req.trackId,
75213
75741
  retry: false
75214
- }], req.frameHandle);
75742
+ }]);
75215
75743
  }
75216
75744
  }, input);
75217
75745
  }
@@ -75265,10 +75793,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75265
75793
  * (coalescing, per-device budget and land-only bookkeeping identical). */
75266
75794
  captureSemanticBestShot(request) {
75267
75795
  const { deviceId, trackId, timestamp, bbox, frameHandle } = request;
75796
+ this.issueParkOrders({
75797
+ deviceId,
75798
+ trackId,
75799
+ timestamp,
75800
+ frameHandle,
75801
+ bbox: { ...bbox },
75802
+ frameWidth: request.frameWidth,
75803
+ frameHeight: request.frameHeight
75804
+ }, ["keyFrame", "thumbnail"], false);
75268
75805
  this.persistKeyFrames(deviceId, timestamp, [{
75269
75806
  trackId,
75270
75807
  retry: false
75271
- }], frameHandle);
75808
+ }]);
75272
75809
  const dispatcher = this.eventMediaDispatcher;
75273
75810
  if (!dispatcher) return;
75274
75811
  if (this.captureScheduler.isKeyActive(captureCoalesceKey(deviceId, "thumbnail", trackId))) {
@@ -75419,7 +75956,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75419
75956
  }
75420
75957
  return retries;
75421
75958
  }
75422
- buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
75959
+ buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight, frameHandle) {
75423
75960
  const targets = [];
75424
75961
  for (const t of tracked) {
75425
75962
  if (t.matchedThisFrame === false) continue;
@@ -75505,6 +76042,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75505
76042
  state: this.residents.keyFrameCapture(t.trackId),
75506
76043
  now: timestamp
75507
76044
  }).kind;
76045
+ if (frameHandle !== void 0 && plausibleBox) {
76046
+ const decision = {
76047
+ deviceId,
76048
+ trackId: t.trackId,
76049
+ timestamp,
76050
+ frameHandle,
76051
+ bbox: {
76052
+ x: t.bbox.x,
76053
+ y: t.bbox.y,
76054
+ w: t.bbox.w,
76055
+ h: t.bbox.h
76056
+ },
76057
+ frameWidth,
76058
+ frameHeight
76059
+ };
76060
+ if (isNewBest) this.issueParkOrders(decision, ["keyFrame", "thumbnail"], false);
76061
+ else {
76062
+ const retryKinds = [];
76063
+ if (keyFrame === "retry") retryKinds.push("keyFrame");
76064
+ if (bestThumbnail) retryKinds.push("thumbnail");
76065
+ if (retryKinds.length > 0) this.issueParkOrders(decision, retryKinds, true);
76066
+ }
76067
+ }
75508
76068
  const suppressedBy = (() => {
75509
76069
  if (skipParkedThumb) return "parked-thumbnail";
75510
76070
  if (!isNewBest || bestThumbnail) return void 0;