@camstack/addon-post-analysis 1.2.3 → 1.2.4

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.
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.3",
6
+ version: "1.2.4",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.3",
21
+ version: "1.2.4",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.3",
36
+ version: "1.2.4",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.3",
39
+ version: "1.2.4",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.3",
48
+ version: "1.2.4",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.3",
84
+ version: "1.2.4",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -3360,6 +3360,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
3360
3360
  for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
3361
3361
  }
3362
3362
  /**
3363
+ * Full whole-track cascade for ONE track: events → their media + track media →
3364
+ * track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
3365
+ * `deleteTracks` provider path flows through the same engine while preserving
3366
+ * the exact legacy call order (event ids stay local to the delete). Adapts the
3367
+ * narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
3368
+ * embeddings) is opted in by the CALLER's registry, not by this legacy shim.
3369
+ */
3370
+ async function cascadeDeleteTrack(stores, trackId) {
3371
+ await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
3372
+ for (const id of ids) {
3373
+ const eventIds = await stores.eventStore.deleteByTrack(id);
3374
+ if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
3375
+ }
3376
+ } }, { deleteByTracks: async (ids) => {
3377
+ await stores.mediaStore.deleteForTracks([...ids]);
3378
+ } }], stores.trackStore, [trackId]);
3379
+ }
3380
+ /**
3363
3381
  * Widened batch whole-track deletion — the ONE engine behind the three §5 entry
3364
3382
  * points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
3365
3383
  * with a single-id list so the track root is deleted last), fires cleanup on
@@ -6478,6 +6496,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
6478
6496
  };
6479
6497
  }
6480
6498
  //#endregion
6499
+ //#region src/shared/frame/crop-extractor.ts
6500
+ /**
6501
+ * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
6502
+ * Coordinates are clamped to frame bounds to avoid out-of-range errors.
6503
+ */
6504
+ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
6505
+ const rawLeft = Math.round(bbox.x * frameWidth);
6506
+ const rawTop = Math.round(bbox.y * frameHeight);
6507
+ const rawWidth = Math.round(bbox.w * frameWidth);
6508
+ const rawHeight = Math.round(bbox.h * frameHeight);
6509
+ const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
6510
+ const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
6511
+ const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
6512
+ const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
6513
+ return {
6514
+ crop: await (0, sharp.default)(frameData, { raw: {
6515
+ width: frameWidth,
6516
+ height: frameHeight,
6517
+ channels: 3
6518
+ } }).extract({
6519
+ left,
6520
+ top,
6521
+ width,
6522
+ height
6523
+ }).jpeg({ quality: 90 }).toBuffer(),
6524
+ width,
6525
+ height
6526
+ };
6527
+ }
6528
+ //#endregion
6481
6529
  //#region src/shared/frame/square-subject-crop.ts
6482
6530
  /** Expansion factor applied to the bbox long side to frame the subject with a
6483
6531
  * little breathing room (operator choice: ×1.2). */
@@ -6716,7 +6764,8 @@ var EventMediaDispatcher = class {
6716
6764
  const snapshots = input.snapshots ?? [];
6717
6765
  const empty = {
6718
6766
  storedSnapshots: [],
6719
- thumbnailTrackIds: []
6767
+ thumbnailTrackIds: [],
6768
+ rasterFallbacks: []
6720
6769
  };
6721
6770
  if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
6722
6771
  let decoded;
@@ -6768,6 +6817,7 @@ var EventMediaDispatcher = class {
6768
6817
  });
6769
6818
  return empty;
6770
6819
  }
6820
+ const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
6771
6821
  for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6772
6822
  for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6773
6823
  const storedSnapshots = [];
@@ -6779,10 +6829,55 @@ var EventMediaDispatcher = class {
6779
6829
  }
6780
6830
  return {
6781
6831
  storedSnapshots,
6782
- thumbnailTrackIds
6832
+ thumbnailTrackIds,
6833
+ rasterFallbacks
6783
6834
  };
6784
6835
  }
6785
6836
  /**
6837
+ * Cut ONE clean detection-raster subject crop per WANTED track that has a
6838
+ * target (firstFrame or snapshot) in this frame — the "first available
6839
+ * detection-raster frame" of the zero-media fallback. Cropped from the
6840
+ * already-resolved `frameData` at its REAL resolution via {@link extractCrop}
6841
+ * (extract-only — NEVER upscaled). Deduped per trackId (first target wins).
6842
+ * A per-track encode failure is skipped (logged) — a missing fallback simply
6843
+ * leaves the track with no last-resort preview, never an error.
6844
+ */
6845
+ async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
6846
+ if (!wanted || wanted.size === 0) return [];
6847
+ const seen = /* @__PURE__ */ new Set();
6848
+ const out = [];
6849
+ const candidates = [...trackFrames.map((t) => ({
6850
+ trackId: t.trackId,
6851
+ timestamp: t.timestamp,
6852
+ bbox: t.bbox
6853
+ })), ...snapshots.map((s) => ({
6854
+ trackId: s.trackId,
6855
+ timestamp: s.timestamp,
6856
+ bbox: s.bbox
6857
+ }))];
6858
+ for (const c of candidates) {
6859
+ if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
6860
+ seen.add(c.trackId);
6861
+ try {
6862
+ const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
6863
+ W: fw,
6864
+ H: fh
6865
+ }, cropPadding));
6866
+ out.push({
6867
+ trackId: c.trackId,
6868
+ timestamp: c.timestamp,
6869
+ jpeg: crop
6870
+ });
6871
+ } catch (err) {
6872
+ this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
6873
+ trackId: c.trackId,
6874
+ error: err instanceof Error ? err.message : String(err)
6875
+ } });
6876
+ }
6877
+ }
6878
+ return out;
6879
+ }
6880
+ /**
6786
6881
  * Periodic per-track media (§5). The boxed FULL frame is encoded once and
6787
6882
  * shared by the appended `snapshot` (timeline filmstrip) and the rolling
6788
6883
  * `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
@@ -7049,36 +7144,6 @@ var EventMediaDispatcher = class {
7049
7144
  }
7050
7145
  };
7051
7146
  //#endregion
7052
- //#region src/shared/frame/crop-extractor.ts
7053
- /**
7054
- * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
7055
- * Coordinates are clamped to frame bounds to avoid out-of-range errors.
7056
- */
7057
- async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
7058
- const rawLeft = Math.round(bbox.x * frameWidth);
7059
- const rawTop = Math.round(bbox.y * frameHeight);
7060
- const rawWidth = Math.round(bbox.w * frameWidth);
7061
- const rawHeight = Math.round(bbox.h * frameHeight);
7062
- const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
7063
- const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
7064
- const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
7065
- const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
7066
- return {
7067
- crop: await (0, sharp.default)(frameData, { raw: {
7068
- width: frameWidth,
7069
- height: frameHeight,
7070
- channels: 3
7071
- } }).extract({
7072
- left,
7073
- top,
7074
- width,
7075
- height
7076
- }).jpeg({ quality: 90 }).toBuffer(),
7077
- width,
7078
- height
7079
- };
7080
- }
7081
- //#endregion
7082
7147
  //#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
7083
7148
  function selectCropBbox(detection) {
7084
7149
  return detection.refinedBbox ?? detection.bbox;
@@ -8867,6 +8932,16 @@ function planPeriodicMedia(input) {
8867
8932
  bestThumbnail: input.isNewBest || !thumbnailLanded
8868
8933
  };
8869
8934
  }
8935
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8936
+ /**
8937
+ * Classify a closing track's persistence outcome. Pure — see the module header
8938
+ * for the full contract.
8939
+ */
8940
+ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8941
+ if (input.hasMedia) return "persist";
8942
+ if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
8943
+ return input.hasRasterFallback ? "raster-fallback" : "persist";
8944
+ }
8870
8945
  //#endregion
8871
8946
  //#region src/pipeline-analytics/best-thumbnail-guard.ts
8872
8947
  /**
@@ -12126,6 +12201,23 @@ function pickTrackFallbackMedia(files) {
12126
12201
  return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
12127
12202
  }
12128
12203
  /**
12204
+ * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
12205
+ * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
12206
+ * frame at all — the regression class created by retiring `crop`/`fullFrame`
12207
+ * production (2026-07-21), leaving degenerate tracks with only boxed
12208
+ * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
12209
+ * returning undefined. A boxed frame (with the target's box drawn) is a real
12210
+ * preview; an empty tile is not. Clean is always tried FIRST so the common case
12211
+ * still serves an unboxed tile. Undefined only when the owner has NO media.
12212
+ *
12213
+ * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
12214
+ * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
12215
+ * composition pure + unit-testable here.
12216
+ */
12217
+ function resolveCropForcedMedia(files, pickClean) {
12218
+ return pickClean(files) ?? pickTrackFallbackMedia(files);
12219
+ }
12220
+ /**
12129
12221
  * Resolve the default-path media for an event id: the event's own media when it
12130
12222
  * has any, else the owning track's fallback media. Returns `null` when neither
12131
12223
  * yields a blob (caller 404s → the surface shows an icon).
@@ -12481,6 +12573,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12481
12573
  * area) + emit time, so a material improvement is measured against the
12482
12574
  * last emit and debounced. Seeded at `start`, dropped at `end`. */
12483
12575
  trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
12576
+ /** Per-active-track close-policy state (spec §"Zero-media track policy"):
12577
+ * whether the track ever reached the tracker's hit-confirmation
12578
+ * (`trackAge >= minHits`) and the FIRST retained detection-raster subject
12579
+ * crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
12580
+ * suppress / raster-fallback / persist for a track that ends with no media.
12581
+ * `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
12582
+ * on every teardown path. */
12583
+ trackClosureState = /* @__PURE__ */ new Map();
12484
12584
  /** The shared crop extractor (native-res first, detection-frame fallback),
12485
12585
  * captured in the constructor so `processFrame` can crop object thumbnails in
12486
12586
  * the same live-frame window as the face/plate/event-media captures. The
@@ -12972,6 +13072,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12972
13072
  this.bindingCache?.onBindingsChanged(data);
12973
13073
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
12974
13074
  this.trackStore?.clearDevice(data.deviceId);
13075
+ this.clearClosureStateForDevice(data.deviceId);
12975
13076
  this.stationaryRegistry?.forgetDevice(data.deviceId);
12976
13077
  this.overlayState.clearDevice(data.deviceId);
12977
13078
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -12990,6 +13091,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12990
13091
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (ev) => {
12991
13092
  const { deviceId } = ev.data;
12992
13093
  this.trackStore?.clearDevice(deviceId);
13094
+ this.clearClosureStateForDevice(deviceId);
12993
13095
  this.stationaryRegistry?.forgetDevice(deviceId);
12994
13096
  this.overlayState.clearDevice(deviceId);
12995
13097
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -13345,6 +13447,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13345
13447
  this.thumbnailInFlight.clear();
13346
13448
  this.keyFrameInFlight.clear();
13347
13449
  this.trackLifecycleUpdateMem.clear();
13450
+ this.trackClosureState.clear();
13348
13451
  this.objectEmbeddingBestSelector.clear();
13349
13452
  this.levelStateByDevice.clear();
13350
13453
  this.settingsCacheByDevice.clear();
@@ -13463,6 +13566,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13463
13566
  });
13464
13567
  positionsCountById.set(t.trackId, upserted.positions.length);
13465
13568
  }
13569
+ if (result.rawTrackedDetections.length > 0) {
13570
+ const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
13571
+ for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
13572
+ }
13466
13573
  const log = this.ctx.logger.withTags({ deviceId });
13467
13574
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13468
13575
  const firstFrameTargets = [];
@@ -13515,6 +13622,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13515
13622
  ...t.label ? { label: t.label } : {}
13516
13623
  });
13517
13624
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13625
+ this.lastFrameAtByTrack.set(id, result.timestamp);
13518
13626
  }
13519
13627
  this.ctx.eventBus.emit({
13520
13628
  id: `pa-${(0, node_crypto.randomUUID)()}`,
@@ -13686,14 +13794,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13686
13794
  } });
13687
13795
  const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
13688
13796
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
13797
+ const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
13798
+ for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13799
+ for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13689
13800
  this.eventMediaDispatcher.captureForFrame({
13690
13801
  deviceId,
13691
13802
  frameHandle,
13692
13803
  events: eventTargets,
13693
13804
  trackFrames: firstFrameTargets,
13694
13805
  snapshots: snapshotTargets,
13695
- cropPadding: mediaSettings.cropPadding
13806
+ cropPadding: mediaSettings.cropPadding,
13807
+ rasterFallbackWantedTrackIds
13696
13808
  }).then((res) => {
13809
+ for (const rf of res.rasterFallbacks) {
13810
+ const st = this.ensureTrackClosureState(deviceId, rf.trackId);
13811
+ if (!st.rasterFallback) st.rasterFallback = {
13812
+ jpeg: rf.jpeg,
13813
+ timestamp: rf.timestamp
13814
+ };
13815
+ }
13697
13816
  for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
13698
13817
  timestamp: s.timestamp,
13699
13818
  position: {
@@ -14704,6 +14823,44 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14704
14823
  const expired = await this.trackStore.expireStale(Date.now());
14705
14824
  for (const t of expired) {
14706
14825
  const duration = t.lastSeen - t.firstSeen;
14826
+ const closure = this.trackClosureState.get(t.trackId);
14827
+ const outcome = decideZeroMediaPolicy({
14828
+ durationMs: duration,
14829
+ hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
14830
+ confirmed: closure?.confirmed ?? false,
14831
+ hasRasterFallback: closure?.rasterFallback !== void 0
14832
+ });
14833
+ if (outcome === "suppress") {
14834
+ this.ctx.logger.info("track suppressed (zero-media false birth)", {
14835
+ tags: { deviceId: t.deviceId },
14836
+ meta: {
14837
+ trackId: t.trackId,
14838
+ className: t.className,
14839
+ durationMs: duration,
14840
+ positions: t.positions.length
14841
+ }
14842
+ });
14843
+ await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
14844
+ continue;
14845
+ }
14846
+ if (outcome === "raster-fallback" && closure?.rasterFallback) try {
14847
+ await this.mediaStore?.put({
14848
+ deviceId: t.deviceId,
14849
+ ownerKind: "track",
14850
+ ownerId: t.trackId,
14851
+ kind: "thumbnail",
14852
+ timestamp: closure.rasterFallback.timestamp,
14853
+ data: closure.rasterFallback.jpeg
14854
+ });
14855
+ } catch (err) {
14856
+ this.ctx.logger.debug("zero-media raster fallback put failed", {
14857
+ tags: { deviceId: t.deviceId },
14858
+ meta: {
14859
+ trackId: t.trackId,
14860
+ error: String(err)
14861
+ }
14862
+ });
14863
+ }
14707
14864
  this.ctx.logger.info("track ended", {
14708
14865
  tags: { deviceId: t.deviceId },
14709
14866
  meta: {
@@ -14814,12 +14971,53 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14814
14971
  });
14815
14972
  this.emitTrackLifecycle(endPayload, t.lastSeen);
14816
14973
  this.trackLifecycleUpdateMem.delete(t.trackId);
14974
+ this.trackClosureState.delete(t.trackId);
14817
14975
  }
14818
14976
  } catch (err) {
14819
14977
  if (this.shuttingDown) return;
14820
14978
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
14821
14979
  }
14822
14980
  }
14981
+ /**
14982
+ * Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
14983
+ * down its live per-track state (mirroring the natural end MINUS importance
14984
+ * scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
14985
+ * just-persisted row + any events/media so nothing dangles. Best-effort.
14986
+ */
14987
+ async suppressZeroMediaTrack(deviceId, trackId) {
14988
+ const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
14989
+ const dropKeyFrame = () => {
14990
+ this.keyFrameKeyByTrackId.delete(trackId);
14991
+ };
14992
+ if (faceEnd) faceEnd.finally(dropKeyFrame);
14993
+ else dropKeyFrame();
14994
+ this.plateRecognizer?.onTrackEnd(deviceId, trackId);
14995
+ this.bestFrameTracker.delete(trackId);
14996
+ this.objectEmbeddingBestSelector.delete(trackId);
14997
+ this.lastFrameAtByTrack.delete(trackId);
14998
+ this.thumbnailLandedTracks.delete(trackId);
14999
+ this.thumbnailInFlight.delete(trackId);
15000
+ this.keyFrameInFlight.delete(trackId);
15001
+ this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15002
+ this.overlayState.onTrackEnded(deviceId, trackId);
15003
+ this.trackLifecycleUpdateMem.delete(trackId);
15004
+ this.trackClosureState.delete(trackId);
15005
+ if (this.eventStore && this.mediaStore && this.trackStore) try {
15006
+ await cascadeDeleteTrack({
15007
+ eventStore: this.eventStore,
15008
+ mediaStore: this.mediaStore,
15009
+ trackStore: this.trackStore
15010
+ }, trackId);
15011
+ } catch (err) {
15012
+ this.ctx.logger.debug("zero-media suppression cascade failed", {
15013
+ tags: { deviceId },
15014
+ meta: {
15015
+ trackId,
15016
+ error: String(err)
15017
+ }
15018
+ });
15019
+ }
15020
+ }
14823
15021
  async sweepRetention() {
14824
15022
  if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
14825
15023
  const now = Date.now();
@@ -15029,6 +15227,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15029
15227
  this.thumbnailInFlight.delete(track.trackId);
15030
15228
  this.keyFrameInFlight.delete(track.trackId);
15031
15229
  this.trackLifecycleUpdateMem.delete(track.trackId);
15230
+ this.trackClosureState.delete(track.trackId);
15032
15231
  this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
15033
15232
  this.overlayState.onTrackEnded(deviceId, track.trackId);
15034
15233
  this.lastActiveTrackIds.get(key)?.delete(track.trackId);
@@ -15327,6 +15526,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15327
15526
  }
15328
15527
  async clearTracks(input) {
15329
15528
  this.trackStore?.clearDevice(input.deviceId);
15529
+ this.clearClosureStateForDevice(input.deviceId);
15330
15530
  this.stationaryRegistry?.clearDevice(input.deviceId);
15331
15531
  this.overlayState.clearDevice(input.deviceId);
15332
15532
  this.overlaySynthesisWarnAt.delete(input.deviceId);
@@ -15539,6 +15739,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15539
15739
  clearLiveTrackState(deviceId, trackId) {
15540
15740
  this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15541
15741
  this.overlayState.onTrackEnded(deviceId, trackId);
15742
+ this.trackClosureState.delete(trackId);
15743
+ }
15744
+ /** Lazily create + return a track's close-policy state (zero-media policy). */
15745
+ ensureTrackClosureState(deviceId, trackId) {
15746
+ let st = this.trackClosureState.get(trackId);
15747
+ if (!st) {
15748
+ st = {
15749
+ deviceId,
15750
+ confirmed: false
15751
+ };
15752
+ this.trackClosureState.set(trackId, st);
15753
+ }
15754
+ return st;
15755
+ }
15756
+ /** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
15757
+ markTrackConfirmed(deviceId, trackId) {
15758
+ this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
15759
+ }
15760
+ /** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
15761
+ clearClosureStateForDevice(deviceId) {
15762
+ for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
15542
15763
  }
15543
15764
  async deleteTracks(input) {
15544
15765
  const cascade = this.buildTrackCascadeRegistry();
@@ -15833,11 +16054,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15833
16054
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
15834
16055
  if (preferKind !== void 0 && preferKind.length > 0) {
15835
16056
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15836
- const clean = pickCleanMedia([...eventFiles, ...trackFiles], preferKind);
15837
- if (!clean) return null;
16057
+ const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
16058
+ if (!chosen) return null;
15838
16059
  return {
15839
- bytes: Buffer.from(clean.base64, "base64"),
15840
- key: clean.key
16060
+ bytes: Buffer.from(chosen.base64, "base64"),
16061
+ key: chosen.key
15841
16062
  };
15842
16063
  }
15843
16064
  const chosen = await resolveDefaultEventMedia({
@@ -3355,6 +3355,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
3355
3355
  for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
3356
3356
  }
3357
3357
  /**
3358
+ * Full whole-track cascade for ONE track: events → their media + track media →
3359
+ * track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
3360
+ * `deleteTracks` provider path flows through the same engine while preserving
3361
+ * the exact legacy call order (event ids stay local to the delete). Adapts the
3362
+ * narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
3363
+ * embeddings) is opted in by the CALLER's registry, not by this legacy shim.
3364
+ */
3365
+ async function cascadeDeleteTrack(stores, trackId) {
3366
+ await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
3367
+ for (const id of ids) {
3368
+ const eventIds = await stores.eventStore.deleteByTrack(id);
3369
+ if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
3370
+ }
3371
+ } }, { deleteByTracks: async (ids) => {
3372
+ await stores.mediaStore.deleteForTracks([...ids]);
3373
+ } }], stores.trackStore, [trackId]);
3374
+ }
3375
+ /**
3358
3376
  * Widened batch whole-track deletion — the ONE engine behind the three §5 entry
3359
3377
  * points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
3360
3378
  * with a single-id list so the track root is deleted last), fires cleanup on
@@ -6473,6 +6491,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
6473
6491
  };
6474
6492
  }
6475
6493
  //#endregion
6494
+ //#region src/shared/frame/crop-extractor.ts
6495
+ /**
6496
+ * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
6497
+ * Coordinates are clamped to frame bounds to avoid out-of-range errors.
6498
+ */
6499
+ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
6500
+ const rawLeft = Math.round(bbox.x * frameWidth);
6501
+ const rawTop = Math.round(bbox.y * frameHeight);
6502
+ const rawWidth = Math.round(bbox.w * frameWidth);
6503
+ const rawHeight = Math.round(bbox.h * frameHeight);
6504
+ const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
6505
+ const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
6506
+ const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
6507
+ const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
6508
+ return {
6509
+ crop: await sharp(frameData, { raw: {
6510
+ width: frameWidth,
6511
+ height: frameHeight,
6512
+ channels: 3
6513
+ } }).extract({
6514
+ left,
6515
+ top,
6516
+ width,
6517
+ height
6518
+ }).jpeg({ quality: 90 }).toBuffer(),
6519
+ width,
6520
+ height
6521
+ };
6522
+ }
6523
+ //#endregion
6476
6524
  //#region src/shared/frame/square-subject-crop.ts
6477
6525
  /** Expansion factor applied to the bbox long side to frame the subject with a
6478
6526
  * little breathing room (operator choice: ×1.2). */
@@ -6711,7 +6759,8 @@ var EventMediaDispatcher = class {
6711
6759
  const snapshots = input.snapshots ?? [];
6712
6760
  const empty = {
6713
6761
  storedSnapshots: [],
6714
- thumbnailTrackIds: []
6762
+ thumbnailTrackIds: [],
6763
+ rasterFallbacks: []
6715
6764
  };
6716
6765
  if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
6717
6766
  let decoded;
@@ -6763,6 +6812,7 @@ var EventMediaDispatcher = class {
6763
6812
  });
6764
6813
  return empty;
6765
6814
  }
6815
+ const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
6766
6816
  for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6767
6817
  for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6768
6818
  const storedSnapshots = [];
@@ -6774,10 +6824,55 @@ var EventMediaDispatcher = class {
6774
6824
  }
6775
6825
  return {
6776
6826
  storedSnapshots,
6777
- thumbnailTrackIds
6827
+ thumbnailTrackIds,
6828
+ rasterFallbacks
6778
6829
  };
6779
6830
  }
6780
6831
  /**
6832
+ * Cut ONE clean detection-raster subject crop per WANTED track that has a
6833
+ * target (firstFrame or snapshot) in this frame — the "first available
6834
+ * detection-raster frame" of the zero-media fallback. Cropped from the
6835
+ * already-resolved `frameData` at its REAL resolution via {@link extractCrop}
6836
+ * (extract-only — NEVER upscaled). Deduped per trackId (first target wins).
6837
+ * A per-track encode failure is skipped (logged) — a missing fallback simply
6838
+ * leaves the track with no last-resort preview, never an error.
6839
+ */
6840
+ async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
6841
+ if (!wanted || wanted.size === 0) return [];
6842
+ const seen = /* @__PURE__ */ new Set();
6843
+ const out = [];
6844
+ const candidates = [...trackFrames.map((t) => ({
6845
+ trackId: t.trackId,
6846
+ timestamp: t.timestamp,
6847
+ bbox: t.bbox
6848
+ })), ...snapshots.map((s) => ({
6849
+ trackId: s.trackId,
6850
+ timestamp: s.timestamp,
6851
+ bbox: s.bbox
6852
+ }))];
6853
+ for (const c of candidates) {
6854
+ if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
6855
+ seen.add(c.trackId);
6856
+ try {
6857
+ const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
6858
+ W: fw,
6859
+ H: fh
6860
+ }, cropPadding));
6861
+ out.push({
6862
+ trackId: c.trackId,
6863
+ timestamp: c.timestamp,
6864
+ jpeg: crop
6865
+ });
6866
+ } catch (err) {
6867
+ this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
6868
+ trackId: c.trackId,
6869
+ error: err instanceof Error ? err.message : String(err)
6870
+ } });
6871
+ }
6872
+ }
6873
+ return out;
6874
+ }
6875
+ /**
6781
6876
  * Periodic per-track media (§5). The boxed FULL frame is encoded once and
6782
6877
  * shared by the appended `snapshot` (timeline filmstrip) and the rolling
6783
6878
  * `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
@@ -7044,36 +7139,6 @@ var EventMediaDispatcher = class {
7044
7139
  }
7045
7140
  };
7046
7141
  //#endregion
7047
- //#region src/shared/frame/crop-extractor.ts
7048
- /**
7049
- * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
7050
- * Coordinates are clamped to frame bounds to avoid out-of-range errors.
7051
- */
7052
- async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
7053
- const rawLeft = Math.round(bbox.x * frameWidth);
7054
- const rawTop = Math.round(bbox.y * frameHeight);
7055
- const rawWidth = Math.round(bbox.w * frameWidth);
7056
- const rawHeight = Math.round(bbox.h * frameHeight);
7057
- const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
7058
- const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
7059
- const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
7060
- const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
7061
- return {
7062
- crop: await sharp(frameData, { raw: {
7063
- width: frameWidth,
7064
- height: frameHeight,
7065
- channels: 3
7066
- } }).extract({
7067
- left,
7068
- top,
7069
- width,
7070
- height
7071
- }).jpeg({ quality: 90 }).toBuffer(),
7072
- width,
7073
- height
7074
- };
7075
- }
7076
- //#endregion
7077
7142
  //#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
7078
7143
  function selectCropBbox(detection) {
7079
7144
  return detection.refinedBbox ?? detection.bbox;
@@ -8862,6 +8927,16 @@ function planPeriodicMedia(input) {
8862
8927
  bestThumbnail: input.isNewBest || !thumbnailLanded
8863
8928
  };
8864
8929
  }
8930
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8931
+ /**
8932
+ * Classify a closing track's persistence outcome. Pure — see the module header
8933
+ * for the full contract.
8934
+ */
8935
+ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8936
+ if (input.hasMedia) return "persist";
8937
+ if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
8938
+ return input.hasRasterFallback ? "raster-fallback" : "persist";
8939
+ }
8865
8940
  //#endregion
8866
8941
  //#region src/pipeline-analytics/best-thumbnail-guard.ts
8867
8942
  /**
@@ -12121,6 +12196,23 @@ function pickTrackFallbackMedia(files) {
12121
12196
  return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
12122
12197
  }
12123
12198
  /**
12199
+ * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
12200
+ * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
12201
+ * frame at all — the regression class created by retiring `crop`/`fullFrame`
12202
+ * production (2026-07-21), leaving degenerate tracks with only boxed
12203
+ * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
12204
+ * returning undefined. A boxed frame (with the target's box drawn) is a real
12205
+ * preview; an empty tile is not. Clean is always tried FIRST so the common case
12206
+ * still serves an unboxed tile. Undefined only when the owner has NO media.
12207
+ *
12208
+ * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
12209
+ * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
12210
+ * composition pure + unit-testable here.
12211
+ */
12212
+ function resolveCropForcedMedia(files, pickClean) {
12213
+ return pickClean(files) ?? pickTrackFallbackMedia(files);
12214
+ }
12215
+ /**
12124
12216
  * Resolve the default-path media for an event id: the event's own media when it
12125
12217
  * has any, else the owning track's fallback media. Returns `null` when neither
12126
12218
  * yields a blob (caller 404s → the surface shows an icon).
@@ -12476,6 +12568,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12476
12568
  * area) + emit time, so a material improvement is measured against the
12477
12569
  * last emit and debounced. Seeded at `start`, dropped at `end`. */
12478
12570
  trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
12571
+ /** Per-active-track close-policy state (spec §"Zero-media track policy"):
12572
+ * whether the track ever reached the tracker's hit-confirmation
12573
+ * (`trackAge >= minHits`) and the FIRST retained detection-raster subject
12574
+ * crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
12575
+ * suppress / raster-fallback / persist for a track that ends with no media.
12576
+ * `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
12577
+ * on every teardown path. */
12578
+ trackClosureState = /* @__PURE__ */ new Map();
12479
12579
  /** The shared crop extractor (native-res first, detection-frame fallback),
12480
12580
  * captured in the constructor so `processFrame` can crop object thumbnails in
12481
12581
  * the same live-frame window as the face/plate/event-media captures. The
@@ -12967,6 +13067,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12967
13067
  this.bindingCache?.onBindingsChanged(data);
12968
13068
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
12969
13069
  this.trackStore?.clearDevice(data.deviceId);
13070
+ this.clearClosureStateForDevice(data.deviceId);
12970
13071
  this.stationaryRegistry?.forgetDevice(data.deviceId);
12971
13072
  this.overlayState.clearDevice(data.deviceId);
12972
13073
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -12985,6 +13086,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12985
13086
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
12986
13087
  const { deviceId } = ev.data;
12987
13088
  this.trackStore?.clearDevice(deviceId);
13089
+ this.clearClosureStateForDevice(deviceId);
12988
13090
  this.stationaryRegistry?.forgetDevice(deviceId);
12989
13091
  this.overlayState.clearDevice(deviceId);
12990
13092
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -13340,6 +13442,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13340
13442
  this.thumbnailInFlight.clear();
13341
13443
  this.keyFrameInFlight.clear();
13342
13444
  this.trackLifecycleUpdateMem.clear();
13445
+ this.trackClosureState.clear();
13343
13446
  this.objectEmbeddingBestSelector.clear();
13344
13447
  this.levelStateByDevice.clear();
13345
13448
  this.settingsCacheByDevice.clear();
@@ -13458,6 +13561,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13458
13561
  });
13459
13562
  positionsCountById.set(t.trackId, upserted.positions.length);
13460
13563
  }
13564
+ if (result.rawTrackedDetections.length > 0) {
13565
+ const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
13566
+ for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
13567
+ }
13461
13568
  const log = this.ctx.logger.withTags({ deviceId });
13462
13569
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13463
13570
  const firstFrameTargets = [];
@@ -13510,6 +13617,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13510
13617
  ...t.label ? { label: t.label } : {}
13511
13618
  });
13512
13619
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13620
+ this.lastFrameAtByTrack.set(id, result.timestamp);
13513
13621
  }
13514
13622
  this.ctx.eventBus.emit({
13515
13623
  id: `pa-${randomUUID()}`,
@@ -13681,14 +13789,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13681
13789
  } });
13682
13790
  const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
13683
13791
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
13792
+ const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
13793
+ for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13794
+ for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13684
13795
  this.eventMediaDispatcher.captureForFrame({
13685
13796
  deviceId,
13686
13797
  frameHandle,
13687
13798
  events: eventTargets,
13688
13799
  trackFrames: firstFrameTargets,
13689
13800
  snapshots: snapshotTargets,
13690
- cropPadding: mediaSettings.cropPadding
13801
+ cropPadding: mediaSettings.cropPadding,
13802
+ rasterFallbackWantedTrackIds
13691
13803
  }).then((res) => {
13804
+ for (const rf of res.rasterFallbacks) {
13805
+ const st = this.ensureTrackClosureState(deviceId, rf.trackId);
13806
+ if (!st.rasterFallback) st.rasterFallback = {
13807
+ jpeg: rf.jpeg,
13808
+ timestamp: rf.timestamp
13809
+ };
13810
+ }
13692
13811
  for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
13693
13812
  timestamp: s.timestamp,
13694
13813
  position: {
@@ -14699,6 +14818,44 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14699
14818
  const expired = await this.trackStore.expireStale(Date.now());
14700
14819
  for (const t of expired) {
14701
14820
  const duration = t.lastSeen - t.firstSeen;
14821
+ const closure = this.trackClosureState.get(t.trackId);
14822
+ const outcome = decideZeroMediaPolicy({
14823
+ durationMs: duration,
14824
+ hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
14825
+ confirmed: closure?.confirmed ?? false,
14826
+ hasRasterFallback: closure?.rasterFallback !== void 0
14827
+ });
14828
+ if (outcome === "suppress") {
14829
+ this.ctx.logger.info("track suppressed (zero-media false birth)", {
14830
+ tags: { deviceId: t.deviceId },
14831
+ meta: {
14832
+ trackId: t.trackId,
14833
+ className: t.className,
14834
+ durationMs: duration,
14835
+ positions: t.positions.length
14836
+ }
14837
+ });
14838
+ await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
14839
+ continue;
14840
+ }
14841
+ if (outcome === "raster-fallback" && closure?.rasterFallback) try {
14842
+ await this.mediaStore?.put({
14843
+ deviceId: t.deviceId,
14844
+ ownerKind: "track",
14845
+ ownerId: t.trackId,
14846
+ kind: "thumbnail",
14847
+ timestamp: closure.rasterFallback.timestamp,
14848
+ data: closure.rasterFallback.jpeg
14849
+ });
14850
+ } catch (err) {
14851
+ this.ctx.logger.debug("zero-media raster fallback put failed", {
14852
+ tags: { deviceId: t.deviceId },
14853
+ meta: {
14854
+ trackId: t.trackId,
14855
+ error: String(err)
14856
+ }
14857
+ });
14858
+ }
14702
14859
  this.ctx.logger.info("track ended", {
14703
14860
  tags: { deviceId: t.deviceId },
14704
14861
  meta: {
@@ -14809,12 +14966,53 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14809
14966
  });
14810
14967
  this.emitTrackLifecycle(endPayload, t.lastSeen);
14811
14968
  this.trackLifecycleUpdateMem.delete(t.trackId);
14969
+ this.trackClosureState.delete(t.trackId);
14812
14970
  }
14813
14971
  } catch (err) {
14814
14972
  if (this.shuttingDown) return;
14815
14973
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
14816
14974
  }
14817
14975
  }
14976
+ /**
14977
+ * Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
14978
+ * down its live per-track state (mirroring the natural end MINUS importance
14979
+ * scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
14980
+ * just-persisted row + any events/media so nothing dangles. Best-effort.
14981
+ */
14982
+ async suppressZeroMediaTrack(deviceId, trackId) {
14983
+ const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
14984
+ const dropKeyFrame = () => {
14985
+ this.keyFrameKeyByTrackId.delete(trackId);
14986
+ };
14987
+ if (faceEnd) faceEnd.finally(dropKeyFrame);
14988
+ else dropKeyFrame();
14989
+ this.plateRecognizer?.onTrackEnd(deviceId, trackId);
14990
+ this.bestFrameTracker.delete(trackId);
14991
+ this.objectEmbeddingBestSelector.delete(trackId);
14992
+ this.lastFrameAtByTrack.delete(trackId);
14993
+ this.thumbnailLandedTracks.delete(trackId);
14994
+ this.thumbnailInFlight.delete(trackId);
14995
+ this.keyFrameInFlight.delete(trackId);
14996
+ this.detailDispatcher?.onTrackEnded(deviceId, trackId);
14997
+ this.overlayState.onTrackEnded(deviceId, trackId);
14998
+ this.trackLifecycleUpdateMem.delete(trackId);
14999
+ this.trackClosureState.delete(trackId);
15000
+ if (this.eventStore && this.mediaStore && this.trackStore) try {
15001
+ await cascadeDeleteTrack({
15002
+ eventStore: this.eventStore,
15003
+ mediaStore: this.mediaStore,
15004
+ trackStore: this.trackStore
15005
+ }, trackId);
15006
+ } catch (err) {
15007
+ this.ctx.logger.debug("zero-media suppression cascade failed", {
15008
+ tags: { deviceId },
15009
+ meta: {
15010
+ trackId,
15011
+ error: String(err)
15012
+ }
15013
+ });
15014
+ }
15015
+ }
14818
15016
  async sweepRetention() {
14819
15017
  if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
14820
15018
  const now = Date.now();
@@ -15024,6 +15222,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15024
15222
  this.thumbnailInFlight.delete(track.trackId);
15025
15223
  this.keyFrameInFlight.delete(track.trackId);
15026
15224
  this.trackLifecycleUpdateMem.delete(track.trackId);
15225
+ this.trackClosureState.delete(track.trackId);
15027
15226
  this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
15028
15227
  this.overlayState.onTrackEnded(deviceId, track.trackId);
15029
15228
  this.lastActiveTrackIds.get(key)?.delete(track.trackId);
@@ -15322,6 +15521,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15322
15521
  }
15323
15522
  async clearTracks(input) {
15324
15523
  this.trackStore?.clearDevice(input.deviceId);
15524
+ this.clearClosureStateForDevice(input.deviceId);
15325
15525
  this.stationaryRegistry?.clearDevice(input.deviceId);
15326
15526
  this.overlayState.clearDevice(input.deviceId);
15327
15527
  this.overlaySynthesisWarnAt.delete(input.deviceId);
@@ -15534,6 +15734,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15534
15734
  clearLiveTrackState(deviceId, trackId) {
15535
15735
  this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15536
15736
  this.overlayState.onTrackEnded(deviceId, trackId);
15737
+ this.trackClosureState.delete(trackId);
15738
+ }
15739
+ /** Lazily create + return a track's close-policy state (zero-media policy). */
15740
+ ensureTrackClosureState(deviceId, trackId) {
15741
+ let st = this.trackClosureState.get(trackId);
15742
+ if (!st) {
15743
+ st = {
15744
+ deviceId,
15745
+ confirmed: false
15746
+ };
15747
+ this.trackClosureState.set(trackId, st);
15748
+ }
15749
+ return st;
15750
+ }
15751
+ /** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
15752
+ markTrackConfirmed(deviceId, trackId) {
15753
+ this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
15754
+ }
15755
+ /** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
15756
+ clearClosureStateForDevice(deviceId) {
15757
+ for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
15537
15758
  }
15538
15759
  async deleteTracks(input) {
15539
15760
  const cascade = this.buildTrackCascadeRegistry();
@@ -15828,11 +16049,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15828
16049
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
15829
16050
  if (preferKind !== void 0 && preferKind.length > 0) {
15830
16051
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15831
- const clean = pickCleanMedia([...eventFiles, ...trackFiles], preferKind);
15832
- if (!clean) return null;
16052
+ const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
16053
+ if (!chosen) return null;
15833
16054
  return {
15834
- bytes: Buffer.from(clean.base64, "base64"),
15835
- key: clean.key
16055
+ bytes: Buffer.from(chosen.base64, "base64"),
16056
+ key: chosen.key
15836
16057
  };
15837
16058
  }
15838
16059
  const chosen = await resolveDefaultEventMedia({
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4dWm6ZL.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChHurWQz.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "CamStack Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",