@camstack/addon-post-analysis 1.2.181 → 1.2.183

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 audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-DEAreP8H.mjs";
1
+ import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg$1, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-DvmLRRIq.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";
@@ -27813,6 +27813,7 @@ var StoragePressureTracker = class {
27813
27813
  var OWNER_KINDS = [
27814
27814
  "event",
27815
27815
  "track",
27816
+ "summary",
27816
27817
  "face",
27817
27818
  "identity",
27818
27819
  "plate",
@@ -28396,7 +28397,8 @@ var MediaStore = class {
28396
28397
  relativePath: path
28397
28398
  })).toString("base64"),
28398
28399
  sizeBytes,
28399
- timestamp
28400
+ timestamp,
28401
+ deviceId: Number(data["deviceId"])
28400
28402
  };
28401
28403
  } catch {
28402
28404
  return null;
@@ -30670,7 +30672,7 @@ async function mapWithConcurrency(items, limit, mapper) {
30670
30672
  * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
30671
30673
  * all-zero track scores 0.
30672
30674
  */
30673
- var WEIGHT_CONFIDENCE = .3;
30675
+ var WEIGHT_CONFIDENCE$1 = .3;
30674
30676
  var WEIGHT_CLASS = .2;
30675
30677
  var WEIGHT_DWELL = .2;
30676
30678
  var WEIGHT_PROXIMITY = .12;
@@ -30708,7 +30710,7 @@ var ANIMAL_CLASSES = new Set([
30708
30710
  "cat"
30709
30711
  ]);
30710
30712
  /** Clamp to [0,1]. */
30711
- function clamp01$1(x) {
30713
+ function clamp01$2(x) {
30712
30714
  if (Number.isNaN(x)) return 0;
30713
30715
  if (x < 0) return 0;
30714
30716
  if (x > 1) return 1;
@@ -30742,7 +30744,7 @@ function computeImportance(input) {
30742
30744
  const terms = [
30743
30745
  {
30744
30746
  reason: "confidence",
30745
- value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
30747
+ value: WEIGHT_CONFIDENCE$1 * clamp01$2(input.peakConfidence)
30746
30748
  },
30747
30749
  {
30748
30750
  reason: "class",
@@ -30750,15 +30752,15 @@ function computeImportance(input) {
30750
30752
  },
30751
30753
  {
30752
30754
  reason: "dwell",
30753
- value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
30755
+ value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
30754
30756
  },
30755
30757
  {
30756
30758
  reason: "proximity",
30757
- value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
30759
+ value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
30758
30760
  },
30759
30761
  {
30760
30762
  reason: "travel",
30761
- value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
30763
+ value: WEIGHT_TRAVEL * clamp01$2(input.totalDistance / 2)
30762
30764
  },
30763
30765
  {
30764
30766
  reason: "zone",
@@ -30777,11 +30779,11 @@ function computeImportance(input) {
30777
30779
  }
30778
30780
  const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
30779
30781
  if (input.netDisplacementFrac !== void 0 && input.pathSpanFrac !== void 0 && input.netDisplacementFrac < .02 && input.pathSpanFrac < .04 && !identified) return {
30780
- importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
30782
+ importance: clamp01$2(sum * STATIC_IMPORTANCE_MULTIPLIER),
30781
30783
  reason: best.reason
30782
30784
  };
30783
30785
  return {
30784
- importance: clamp01$1(sum),
30786
+ importance: clamp01$2(sum),
30785
30787
  reason: best.reason
30786
30788
  };
30787
30789
  }
@@ -33479,7 +33481,7 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
33479
33481
  * it, so it can be loose enough to see the near misses.
33480
33482
  */
33481
33483
  var CO_MOVING_MIN_CONTAINMENT = .6;
33482
- function intersectionArea$3(a, b) {
33484
+ function intersectionArea$4(a, b) {
33483
33485
  return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
33484
33486
  }
33485
33487
  /**
@@ -33499,7 +33501,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
33499
33501
  if (o.className === b.className) continue;
33500
33502
  const areaO = o.bbox.w * o.bbox.h;
33501
33503
  if (areaO <= 0) continue;
33502
- const inter = intersectionArea$3(b.bbox, o.bbox);
33504
+ const inter = intersectionArea$4(b.bbox, o.bbox);
33503
33505
  if (inter <= 0) continue;
33504
33506
  const union = areaB + areaO - inter;
33505
33507
  const containment = inter / Math.min(areaB, areaO);
@@ -33931,7 +33933,7 @@ function pairRiders(detections) {
33931
33933
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
33932
33934
  */
33933
33935
  var GROWTH_CENTROID_FACTOR = 2.3;
33934
- function intersectionArea$2(a, b) {
33936
+ function intersectionArea$3(a, b) {
33935
33937
  const x1 = Math.max(a.x, b.x);
33936
33938
  const y1 = Math.max(a.y, b.y);
33937
33939
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -33969,7 +33971,7 @@ function growthAdjacentMatch(track, det, ctx) {
33969
33971
  const larger = Math.max(trackArea, detArea);
33970
33972
  const smaller = Math.min(trackArea, detArea);
33971
33973
  if (larger / smaller < 1.5) return false;
33972
- if (intersectionArea$2(track, det) / smaller >= .5) return false;
33974
+ if (intersectionArea$3(track, det) / smaller >= .5) return false;
33973
33975
  const tc = bboxCentroid(track);
33974
33976
  const dc = bboxCentroid(det);
33975
33977
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -35955,6 +35957,7 @@ var EMPTY_CASCADE_TALLY = {
35955
35957
  faces: 0,
35956
35958
  plates: 0,
35957
35959
  embeddings: 0,
35960
+ summaries: 0,
35958
35961
  bytesReclaimed: 0
35959
35962
  };
35960
35963
  /** Fold a leaf store's receipts into a running tally. */
@@ -35998,6 +36001,13 @@ function addLeafCounts(tally, counts) {
35998
36001
  bytesReclaimed
35999
36002
  };
36000
36003
  break;
36004
+ case "summaries":
36005
+ next = {
36006
+ ...next,
36007
+ summaries: next.summaries + c.rows,
36008
+ bytesReclaimed
36009
+ };
36010
+ break;
36001
36011
  }
36002
36012
  }
36003
36013
  return next;
@@ -36011,6 +36021,7 @@ function mergeCascadeTallies(a, b) {
36011
36021
  faces: a.faces + b.faces,
36012
36022
  plates: a.plates + b.plates,
36013
36023
  embeddings: a.embeddings + b.embeddings,
36024
+ summaries: a.summaries + b.summaries,
36014
36025
  bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
36015
36026
  };
36016
36027
  }
@@ -36073,6 +36084,7 @@ function absentCascadeLegs(stores) {
36073
36084
  if (!stores.faceStore) absent.push("faces");
36074
36085
  if (!stores.plateStore) absent.push("plates");
36075
36086
  if (!stores.objectEmbeddingStore) absent.push("embeddings");
36087
+ if (!stores.summaryStore) absent.push("summaries");
36076
36088
  return absent;
36077
36089
  }
36078
36090
  function buildAnalyticsCascadeRegistry(stores) {
@@ -36080,6 +36092,7 @@ function buildAnalyticsCascadeRegistry(stores) {
36080
36092
  if (stores.faceStore) registry.push(stores.faceStore);
36081
36093
  if (stores.plateStore) registry.push(stores.plateStore);
36082
36094
  if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
36095
+ if (stores.summaryStore) registry.push(stores.summaryStore);
36083
36096
  return registry;
36084
36097
  }
36085
36098
  /**
@@ -46719,6 +46732,15 @@ var CACHE_CONTROL = "public, max-age=31536000, immutable";
46719
46732
  var THUMB_DEFAULT_SIZE = 160;
46720
46733
  var THUMB_MIN_SIZE = 64;
46721
46734
  var THUMB_MAX_SIZE = 320;
46735
+ /** Set by the HUB for a `csv_*` share token admitted on this plane; never by a client. */
46736
+ var SHARE_DEVICES_HEADER = "x-camstack-share-devices";
46737
+ /** The device ids a share scope allows, or `null` when the request carries no scope. */
46738
+ function shareScopeDevices(headers) {
46739
+ const raw = headers[SHARE_DEVICES_HEADER];
46740
+ const value = Array.isArray(raw) ? raw.join(",") : raw;
46741
+ if (value === void 0) return null;
46742
+ return new Set(value.split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((id) => Number.isInteger(id) && id >= 0));
46743
+ }
46722
46744
  /**
46723
46745
  * Parse the `?kind=…` query into a preferred stored media kind. Returns null
46724
46746
  * when unset. The value is a free-form kind token (e.g. `crop`); the resolver
@@ -46791,6 +46813,14 @@ function createEventMediaHandler(deps) {
46791
46813
  res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
46792
46814
  return;
46793
46815
  }
46816
+ const scope = shareScopeDevices(req.headers);
46817
+ if (scope !== null) {
46818
+ const owner = media.deviceId ?? null;
46819
+ if (owner === null || !scope.has(owner)) {
46820
+ res.writeHead(403).end();
46821
+ return;
46822
+ }
46823
+ }
46794
46824
  const etag = "\"" + media.key + "\"";
46795
46825
  const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
46796
46826
  if (req.headers["if-none-match"] === etag) {
@@ -48261,7 +48291,7 @@ var RetrainAnnotationStore = class {
48261
48291
  };
48262
48292
  //#endregion
48263
48293
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
48264
- function clamp01(v) {
48294
+ function clamp01$1(v) {
48265
48295
  return v < 0 ? 0 : v > 1 ? 1 : v;
48266
48296
  }
48267
48297
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -48337,10 +48367,10 @@ function deriveZoneCropRect(bbox, zone) {
48337
48367
  function deriveSubjectCropRect(bbox, padding) {
48338
48368
  if (!isUsableRect(bbox)) return null;
48339
48369
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
48340
- const cropX = clamp01(bbox.x - bbox.w * pad);
48341
- const cropY = clamp01(bbox.y - bbox.h * pad);
48342
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
48343
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
48370
+ const cropX = clamp01$1(bbox.x - bbox.w * pad);
48371
+ const cropY = clamp01$1(bbox.y - bbox.h * pad);
48372
+ const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
48373
+ const cropBottom = clamp01$1(bbox.y + bbox.h * (1 + pad));
48344
48374
  const cropW = cropRight - cropX;
48345
48375
  const cropH = cropBottom - cropY;
48346
48376
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -51714,7 +51744,7 @@ async function persistTrackKeyFrame(deps, input) {
51714
51744
  ...drop,
51715
51745
  meta: {
51716
51746
  ...drop.meta,
51717
- error: errMsg(err)
51747
+ error: errMsg$1(err)
51718
51748
  }
51719
51749
  });
51720
51750
  }
@@ -51723,7 +51753,7 @@ async function persistTrackKeyFrame(deps, input) {
51723
51753
  keyFrameKey
51724
51754
  };
51725
51755
  } catch (err) {
51726
- const error = errMsg(err);
51756
+ const error = errMsg$1(err);
51727
51757
  deps.logger.warn("key-frame capture failed", {
51728
51758
  ...drop,
51729
51759
  meta: {
@@ -52591,7 +52621,7 @@ function isSceneSizedBox(bbox, frameWidth, frameHeight) {
52591
52621
  const frame = frameWidth * frameHeight;
52592
52622
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
52593
52623
  }
52594
- function intersectionArea$1(a, b) {
52624
+ function intersectionArea$2(a, b) {
52595
52625
  const ix1 = Math.max(a.x, b.x);
52596
52626
  const iy1 = Math.max(a.y, b.y);
52597
52627
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -52604,7 +52634,7 @@ function detectionSwallowsEntry(det, parked) {
52604
52634
  const detArea = det.w * det.h;
52605
52635
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
52606
52636
  if (!centroidInside(det, centroid(parked))) return false;
52607
- return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52637
+ return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52608
52638
  }
52609
52639
  function parkedEntryHoldsDetection(entry, detBbox, config) {
52610
52640
  const overlap = iou(entry.bbox, detBbox);
@@ -53436,7 +53466,7 @@ var VEHICLE_CLASSES = [
53436
53466
  "vehicle"
53437
53467
  ];
53438
53468
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
53439
- function intersectionArea(a, b) {
53469
+ function intersectionArea$1(a, b) {
53440
53470
  const left = Math.max(a.x, b.x);
53441
53471
  const right = Math.min(a.x + a.w, b.x + b.w);
53442
53472
  const top = Math.max(a.y, b.y);
@@ -53453,7 +53483,7 @@ function intersectionArea(a, b) {
53453
53483
  function containedFraction(person, vehicle) {
53454
53484
  const area = person.w * person.h;
53455
53485
  if (area <= 0) return 0;
53456
- return intersectionArea(person, vehicle) / area;
53486
+ return intersectionArea$1(person, vehicle) / area;
53457
53487
  }
53458
53488
  /**
53459
53489
  * Is this person box contained in any vehicle box in the same frame?
@@ -56240,6 +56270,359 @@ async function reconcileGallerySamples(deps, options) {
56240
56270
  stoppedByCeiling
56241
56271
  };
56242
56272
  }
56273
+ //#endregion
56274
+ //#region src/pipeline-analytics/summary/summary-store.ts
56275
+ var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
56276
+ var SUMMARY_MEMBERS_COLLECTION = "pipeline-analytics:summary-members";
56277
+ var SUMMARIES_COLUMNS = [
56278
+ {
56279
+ name: "id",
56280
+ type: "TEXT",
56281
+ primaryKey: true,
56282
+ notNull: true
56283
+ },
56284
+ {
56285
+ name: "deviceId",
56286
+ type: "INTEGER",
56287
+ notNull: true
56288
+ },
56289
+ {
56290
+ name: "openedAt",
56291
+ type: "INTEGER",
56292
+ notNull: true
56293
+ },
56294
+ {
56295
+ name: "lastActiveAt",
56296
+ type: "INTEGER",
56297
+ notNull: true
56298
+ },
56299
+ {
56300
+ name: "sealedAt",
56301
+ type: "INTEGER"
56302
+ },
56303
+ (
56304
+ /** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
56305
+ {
56306
+ name: "openDeviceId",
56307
+ type: "INTEGER"
56308
+ }),
56309
+ {
56310
+ name: "memberCount",
56311
+ type: "INTEGER",
56312
+ notNull: true
56313
+ }
56314
+ ];
56315
+ var SUMMARIES_INDEXES = [{
56316
+ name: "summaries_open_device",
56317
+ columns: ["openDeviceId"],
56318
+ unique: true
56319
+ }, {
56320
+ name: "summaries_device_opened",
56321
+ columns: ["deviceId", "openedAt"]
56322
+ }];
56323
+ var SUMMARY_MEMBERS_COLUMNS = [
56324
+ {
56325
+ name: "id",
56326
+ type: "TEXT",
56327
+ primaryKey: true,
56328
+ notNull: true
56329
+ },
56330
+ {
56331
+ name: "summaryId",
56332
+ type: "TEXT",
56333
+ notNull: true
56334
+ },
56335
+ {
56336
+ name: "trackId",
56337
+ type: "TEXT",
56338
+ notNull: true
56339
+ },
56340
+ {
56341
+ name: "deviceId",
56342
+ type: "INTEGER",
56343
+ notNull: true
56344
+ },
56345
+ (
56346
+ /** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
56347
+ {
56348
+ name: "timestamp",
56349
+ type: "INTEGER",
56350
+ notNull: true
56351
+ })
56352
+ ];
56353
+ var SUMMARY_MEMBERS_INDEXES = [{
56354
+ name: "summary_members_summary",
56355
+ columns: ["summaryId"]
56356
+ }, {
56357
+ name: "summary_members_track",
56358
+ columns: ["trackId"]
56359
+ }];
56360
+ function summaryId(deviceId, openedAt) {
56361
+ return `sum-${deviceId}-${openedAt}`;
56362
+ }
56363
+ function memberRowId(summaryIdValue, trackId) {
56364
+ return `${summaryIdValue}:${trackId}`;
56365
+ }
56366
+ function num$1(value, fallback = 0) {
56367
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
56368
+ }
56369
+ function toRow$1(id, data) {
56370
+ return {
56371
+ id,
56372
+ deviceId: num$1(data["deviceId"]),
56373
+ openedAt: num$1(data["openedAt"]),
56374
+ lastActiveAt: num$1(data["lastActiveAt"]),
56375
+ sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
56376
+ memberCount: num$1(data["memberCount"])
56377
+ };
56378
+ }
56379
+ function isUniqueViolation(err) {
56380
+ return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
56381
+ }
56382
+ var SummaryStore = class {
56383
+ store;
56384
+ logger;
56385
+ onEvict;
56386
+ constructor(deps) {
56387
+ this.store = deps.store;
56388
+ this.logger = deps.logger;
56389
+ this.onEvict = deps.onEvict;
56390
+ }
56391
+ static async declare(store) {
56392
+ await Promise.all([store.declareCollection.mutate({
56393
+ collection: SUMMARIES_COLLECTION,
56394
+ columns: [...SUMMARIES_COLUMNS],
56395
+ indexes: [...SUMMARIES_INDEXES]
56396
+ }), store.declareCollection.mutate({
56397
+ collection: SUMMARY_MEMBERS_COLLECTION,
56398
+ columns: [...SUMMARY_MEMBERS_COLUMNS],
56399
+ indexes: [...SUMMARY_MEMBERS_INDEXES]
56400
+ })]);
56401
+ }
56402
+ /** The camera's OPEN summary, from the row — `null` when none. */
56403
+ async findOpen(deviceId) {
56404
+ const first = (await this.store.query.query({
56405
+ collection: SUMMARIES_COLLECTION,
56406
+ filter: {
56407
+ where: { openDeviceId: deviceId },
56408
+ limit: 1
56409
+ }
56410
+ }))[0];
56411
+ return first === void 0 ? null : toRow$1(first.id, first.data);
56412
+ }
56413
+ async listOpen() {
56414
+ return (await this.store.query.query({
56415
+ collection: SUMMARIES_COLLECTION,
56416
+ filter: {
56417
+ where: { sealedAt: null },
56418
+ limit: 1e4
56419
+ }
56420
+ })).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data));
56421
+ }
56422
+ async membersOf(summaryIdValue) {
56423
+ return (await this.store.query.query({
56424
+ collection: SUMMARY_MEMBERS_COLLECTION,
56425
+ filter: {
56426
+ where: { summaryId: summaryIdValue },
56427
+ limit: 1e4
56428
+ }
56429
+ })).map((r) => ({
56430
+ summaryId: summaryIdValue,
56431
+ trackId: String(r.data["trackId"] ?? ""),
56432
+ deviceId: num$1(r.data["deviceId"]),
56433
+ joinedAt: num$1(r.data["timestamp"])
56434
+ }));
56435
+ }
56436
+ /**
56437
+ * Put a track into the camera's open summary, opening one when there is
56438
+ * none. Racing opens are settled by the store: the loser's insert hits the
56439
+ * unique `openDeviceId` and is re-read as an absorb.
56440
+ */
56441
+ async openOrAbsorb(input) {
56442
+ const existing = await this.findOpen(input.deviceId);
56443
+ if (existing !== null) return this.absorb(existing, input);
56444
+ const id = summaryId(input.deviceId, input.at);
56445
+ try {
56446
+ await this.store.set.mutate({
56447
+ collection: SUMMARIES_COLLECTION,
56448
+ key: id,
56449
+ value: {
56450
+ deviceId: input.deviceId,
56451
+ openedAt: input.at,
56452
+ lastActiveAt: input.at,
56453
+ sealedAt: null,
56454
+ openDeviceId: input.deviceId,
56455
+ memberCount: 1
56456
+ }
56457
+ });
56458
+ } catch (err) {
56459
+ if (!isUniqueViolation(err)) throw err;
56460
+ const winner = await this.findOpen(input.deviceId);
56461
+ if (winner === null) throw err;
56462
+ return this.absorb(winner, input);
56463
+ }
56464
+ await this.addMember(id, input);
56465
+ return {
56466
+ id,
56467
+ deviceId: input.deviceId,
56468
+ openedAt: input.at,
56469
+ lastActiveAt: input.at,
56470
+ sealedAt: null,
56471
+ memberCount: 1
56472
+ };
56473
+ }
56474
+ async absorb(summary, input) {
56475
+ const members = await this.membersOf(summary.id);
56476
+ if (members.some((m) => m.trackId === input.trackId)) return summary;
56477
+ await this.addMember(summary.id, input);
56478
+ const memberCount = members.length + 1;
56479
+ const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
56480
+ await this.store.updateWhere.mutate({
56481
+ collection: SUMMARIES_COLLECTION,
56482
+ filter: { where: {
56483
+ openDeviceId: summary.deviceId,
56484
+ openedAt: summary.openedAt
56485
+ } },
56486
+ data: {
56487
+ memberCount,
56488
+ lastActiveAt
56489
+ }
56490
+ });
56491
+ return {
56492
+ ...summary,
56493
+ memberCount,
56494
+ lastActiveAt
56495
+ };
56496
+ }
56497
+ async addMember(summaryIdValue, input) {
56498
+ await this.store.insertMany.mutate({
56499
+ collection: SUMMARY_MEMBERS_COLLECTION,
56500
+ records: [{
56501
+ id: memberRowId(summaryIdValue, input.trackId),
56502
+ data: {
56503
+ summaryId: summaryIdValue,
56504
+ trackId: input.trackId,
56505
+ deviceId: input.deviceId,
56506
+ timestamp: input.at
56507
+ }
56508
+ }]
56509
+ });
56510
+ }
56511
+ /**
56512
+ * Seal in place. `true` when THIS call sealed it; `false` when the row was
56513
+ * already sealed (or gone) — the second sealer learns, it does not write.
56514
+ */
56515
+ async seal(summary, sealedAt) {
56516
+ const { updated } = await this.store.updateWhere.mutate({
56517
+ collection: SUMMARIES_COLLECTION,
56518
+ filter: { where: {
56519
+ openDeviceId: summary.deviceId,
56520
+ openedAt: summary.openedAt
56521
+ } },
56522
+ data: {
56523
+ sealedAt,
56524
+ openDeviceId: null
56525
+ }
56526
+ });
56527
+ if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
56528
+ tags: { deviceId: summary.deviceId },
56529
+ meta: {
56530
+ summaryId: summary.id,
56531
+ updated
56532
+ }
56533
+ });
56534
+ return updated >= 1;
56535
+ }
56536
+ /**
56537
+ * Track cascade leaf: a member row whose track is gone is gone too, and
56538
+ * the summary's count follows. The envelope itself outlives its members
56539
+ * — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
56540
+ */
56541
+ async deleteByTracks(trackIds) {
56542
+ if (trackIds.length === 0) return [{
56543
+ family: "summaries",
56544
+ rows: 0,
56545
+ bytes: 0
56546
+ }];
56547
+ const members = await this.store.query.query({
56548
+ collection: SUMMARY_MEMBERS_COLLECTION,
56549
+ filter: {
56550
+ whereIn: { trackId: [...trackIds] },
56551
+ limit: 1e4
56552
+ }
56553
+ });
56554
+ if (members.length === 0) return [{
56555
+ family: "summaries",
56556
+ rows: 0,
56557
+ bytes: 0
56558
+ }];
56559
+ const { deleted } = await this.store.deleteWhere.mutate({
56560
+ collection: SUMMARY_MEMBERS_COLLECTION,
56561
+ filter: { whereIn: { trackId: [...trackIds] } }
56562
+ });
56563
+ const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
56564
+ for (const summaryIdValue of touched) {
56565
+ const remaining = await this.membersOf(summaryIdValue);
56566
+ const raw = await this.store.get.query({
56567
+ collection: SUMMARIES_COLLECTION,
56568
+ key: summaryIdValue
56569
+ });
56570
+ if (typeof raw !== "object" || raw === null) continue;
56571
+ const row = raw;
56572
+ await this.store.updateWhere.mutate({
56573
+ collection: SUMMARIES_COLLECTION,
56574
+ filter: { where: {
56575
+ deviceId: num$1(row["deviceId"]),
56576
+ openedAt: num$1(row["openedAt"])
56577
+ } },
56578
+ data: { memberCount: remaining.length }
56579
+ });
56580
+ }
56581
+ return [{
56582
+ family: "summaries",
56583
+ rows: deleted,
56584
+ bytes: 0
56585
+ }];
56586
+ }
56587
+ /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
56588
+ async existingIds(ids) {
56589
+ const found = /* @__PURE__ */ new Set();
56590
+ await Promise.all(ids.map(async (id) => {
56591
+ const raw = await this.store.get.query({
56592
+ collection: SUMMARIES_COLLECTION,
56593
+ key: id
56594
+ });
56595
+ if (typeof raw === "object" && raw !== null) found.add(id);
56596
+ }));
56597
+ return found;
56598
+ }
56599
+ /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
56600
+ async evictBefore(deviceId, cutoffMs) {
56601
+ const rows = await this.store.query.query({
56602
+ collection: SUMMARIES_COLLECTION,
56603
+ filter: {
56604
+ where: { deviceId },
56605
+ whereBetween: { openedAt: [0, cutoffMs - 1] },
56606
+ limit: 1e4
56607
+ }
56608
+ });
56609
+ if (rows.length === 0) return 0;
56610
+ const ids = rows.map((r) => r.id);
56611
+ await this.onEvict?.(ids);
56612
+ await this.store.deleteWhere.mutate({
56613
+ collection: SUMMARY_MEMBERS_COLLECTION,
56614
+ filter: { whereIn: { summaryId: ids } }
56615
+ });
56616
+ const { deleted } = await this.store.deleteWhere.mutate({
56617
+ collection: SUMMARIES_COLLECTION,
56618
+ filter: {
56619
+ where: { deviceId },
56620
+ whereBetween: { openedAt: [0, cutoffMs - 1] }
56621
+ }
56622
+ });
56623
+ return deleted;
56624
+ }
56625
+ };
56243
56626
  /**
56244
56627
  * Where the completion marker lives.
56245
56628
  *
@@ -58535,6 +58918,10 @@ var ANALYTICS_COLLECTIONS = [
58535
58918
  collection: PLATES_COLLECTION,
58536
58919
  classification: derived(PLATE_COLUMNS)
58537
58920
  },
58921
+ {
58922
+ collection: SUMMARY_MEMBERS_COLLECTION,
58923
+ classification: derived(SUMMARY_MEMBERS_COLUMNS)
58924
+ },
58538
58925
  {
58539
58926
  collection: MEDIA_COLLECTION,
58540
58927
  classification: {
@@ -58578,6 +58965,13 @@ var ANALYTICS_COLLECTIONS = [
58578
58965
  rootId: "ops-log"
58579
58966
  }
58580
58967
  },
58968
+ {
58969
+ collection: SUMMARIES_COLLECTION,
58970
+ classification: {
58971
+ kind: "retention-root",
58972
+ rootId: "summary"
58973
+ }
58974
+ },
58581
58975
  {
58582
58976
  collection: IDENTITIES_COLLECTION,
58583
58977
  classification: {
@@ -58737,6 +59131,10 @@ function resolveMediaOwner(row) {
58737
59131
  kind: "event",
58738
59132
  eventId: ownerId
58739
59133
  };
59134
+ case "summary": return {
59135
+ kind: "summary",
59136
+ summaryId: ownerId
59137
+ };
58740
59138
  case "face": return ownerId.startsWith("face-") ? {
58741
59139
  kind: "track",
58742
59140
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -58823,11 +59221,14 @@ var DEFAULT_ORPHAN_LIMITS = {
58823
59221
  function questionsFor(refs) {
58824
59222
  const trackIds = /* @__PURE__ */ new Set();
58825
59223
  const eventIds = /* @__PURE__ */ new Set();
59224
+ const summaryIds = /* @__PURE__ */ new Set();
58826
59225
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
58827
59226
  else if (ref.kind === "event") eventIds.add(ref.eventId);
59227
+ else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
58828
59228
  return {
58829
59229
  trackIds: [...trackIds],
58830
- eventIds: [...eventIds]
59230
+ eventIds: [...eventIds],
59231
+ summaryIds: [...summaryIds]
58831
59232
  };
58832
59233
  }
58833
59234
  /**
@@ -58859,12 +59260,14 @@ async function auditScope(scope, deps, limits, cursor) {
58859
59260
  break;
58860
59261
  }
58861
59262
  const refs = rows.map((row) => scope.resolve(row));
58862
- const { trackIds, eventIds } = questionsFor(refs);
59263
+ const { trackIds, eventIds, summaryIds } = questionsFor(refs);
58863
59264
  let liveTracks;
58864
59265
  let liveEvents;
59266
+ let liveSummaries;
58865
59267
  try {
58866
59268
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
58867
59269
  liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
59270
+ liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
58868
59271
  } catch (err) {
58869
59272
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
58870
59273
  collection: scope.collection,
@@ -58888,7 +59291,7 @@ async function auditScope(scope, deps, limits, cursor) {
58888
59291
  tally.unresolvable += 1;
58889
59292
  continue;
58890
59293
  }
58891
- if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : false) continue;
59294
+ if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
58892
59295
  const ts = num(row.data, scope.timestampField);
58893
59296
  if (ts !== null && ts > graceFloor) {
58894
59297
  tally.withinGrace += 1;
@@ -59138,12 +59541,14 @@ async function sweepTracklessRoots(deps) {
59138
59541
  cutoffMs
59139
59542
  });
59140
59543
  const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
59544
+ const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
59141
59545
  const evicted = {
59142
59546
  motion: events.motion,
59143
59547
  audio: events.audio,
59144
- sensor
59548
+ sensor,
59549
+ summaries
59145
59550
  };
59146
- if (evicted.motion.length + evicted.audio.length + evicted.sensor > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
59551
+ if (evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
59147
59552
  } catch (err) {
59148
59553
  deps.onError(deviceId, err);
59149
59554
  }
@@ -60103,7 +60508,7 @@ function createNativeFrameTransport(deps) {
60103
60508
  } catch (err) {
60104
60509
  logger.debug("full-frame jpeg fetch failed", { meta: {
60105
60510
  nodeId: handle.nodeId,
60106
- error: errMsg(err)
60511
+ error: errMsg$1(err)
60107
60512
  } });
60108
60513
  return null;
60109
60514
  }
@@ -60152,7 +60557,7 @@ function createNativeFrameTransport(deps) {
60152
60557
  try {
60153
60558
  return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
60154
60559
  } catch (err) {
60155
- cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
60560
+ cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg$1(err) } });
60156
60561
  return null;
60157
60562
  }
60158
60563
  };
@@ -60201,7 +60606,7 @@ function createNativeFrameTransport(deps) {
60201
60606
  const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH).catch((err) => {
60202
60607
  logger.child("DisplayCrop").debug("fallback full-frame fetch failed — treating as a miss", { meta: {
60203
60608
  nodeId: handle.nodeId,
60204
- error: errMsg(err)
60609
+ error: errMsg$1(err)
60205
60610
  } });
60206
60611
  return null;
60207
60612
  });
@@ -64838,6 +65243,310 @@ function rowFromMetadata(id, metadata) {
64838
65243
  };
64839
65244
  }
64840
65245
  //#endregion
65246
+ //#region src/pipeline-analytics/summary/summary-score.ts
65247
+ /** A member whose own box is covered this much by others is hidden. */
65248
+ var OCCLUDED_COVER_FRACTION = .5;
65249
+ /** A box covering this share of the frame scores full marks on area. */
65250
+ var FULL_AREA_SHARE = .25;
65251
+ var WEIGHT_CONFIDENCE = .3;
65252
+ var WEIGHT_AREA = .25;
65253
+ var WEIGHT_RECOGNITION = .25;
65254
+ var WEIGHT_PLACEMENT = .2;
65255
+ function intersectionArea(a, b) {
65256
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
65257
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
65258
+ return w > 0 && h > 0 ? w * h : 0;
65259
+ }
65260
+ /**
65261
+ * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
65262
+ * capped at the own area: when two others overlap each other on top of this
65263
+ * box the sum over-counts, which only ever calls a member MORE hidden, never
65264
+ * less — the safe side for a shot meant to show everyone.
65265
+ */
65266
+ function coveredFraction(own, others) {
65267
+ const area = own.width * own.height;
65268
+ if (area <= 0) return 1;
65269
+ const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
65270
+ return Math.min(1, covered / area);
65271
+ }
65272
+ function clamp01(value) {
65273
+ return Math.max(0, Math.min(1, value));
65274
+ }
65275
+ function placement(box, frameWidth, frameHeight) {
65276
+ const cx = box.x + box.width / 2;
65277
+ const cy = box.y + box.height / 2;
65278
+ const dx = cx - frameWidth / 2;
65279
+ const dy = cy - frameHeight / 2;
65280
+ const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
65281
+ return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
65282
+ }
65283
+ function memberQuality(d, frameWidth, frameHeight) {
65284
+ const frameArea = frameWidth * frameHeight;
65285
+ const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
65286
+ return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
65287
+ }
65288
+ /** Score one frame for one summary. Pure; never throws on empty input. */
65289
+ function scoreSummaryFrame(input) {
65290
+ const present = input.detections.filter((d) => input.members.has(d.trackId));
65291
+ const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
65292
+ if (visible.length === 0) return {
65293
+ visible: 0,
65294
+ quality: 0
65295
+ };
65296
+ const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
65297
+ return {
65298
+ visible: visible.length,
65299
+ quality
65300
+ };
65301
+ }
65302
+ /**
65303
+ * Lexicographic: more visible members wins outright; only equal counts fall
65304
+ * through to quality. Positive when `a` is the better shot.
65305
+ */
65306
+ function compareSummaryScores(a, b) {
65307
+ if (a.visible !== b.visible) return a.visible - b.visible;
65308
+ return a.quality - b.quality;
65309
+ }
65310
+ /** A candidate replaces the best only when it is STRICTLY better — a tie keeps
65311
+ * the pixels already materialised (they cost a native round trip). */
65312
+ function isBetterSummaryScore(candidate, best) {
65313
+ if (candidate.visible === 0) return false;
65314
+ return best === null || compareSummaryScores(candidate, best) > 0;
65315
+ }
65316
+ //#endregion
65317
+ //#region src/pipeline-analytics/summary/group-shot.ts
65318
+ /**
65319
+ * The running argmax of one open summary's group shot.
65320
+ *
65321
+ * RAM only, on purpose: it decides WHEN pixels are worth a native round trip
65322
+ * (only when the maximum improves — D9/D18), it is not the record of the
65323
+ * shot. The record is the media row the caller writes under the summary's
65324
+ * id; after a restart an open summary simply starts its argmax again and
65325
+ * replaces the row the next time it beats what it can remember, which is
65326
+ * nothing. That is a weaker shot for one session, never a wrong one.
65327
+ */
65328
+ var GroupShotTracker = class {
65329
+ best = /* @__PURE__ */ new Map();
65330
+ /**
65331
+ * Offer a frame. Returns `true` when it beats the summary's best so far —
65332
+ * the caller's cue to materialise the pixels for THIS frame, now, while its
65333
+ * handle is still leased.
65334
+ */
65335
+ offer(summaryId, score, frame) {
65336
+ if (!isBetterSummaryScore(score, (this.best.get(summaryId) ?? null)?.score ?? null)) return false;
65337
+ this.best.set(summaryId, {
65338
+ score,
65339
+ frame
65340
+ });
65341
+ return true;
65342
+ }
65343
+ bestOf(summaryId) {
65344
+ return this.best.get(summaryId) ?? null;
65345
+ }
65346
+ /** A sealed or evicted summary takes no more frames; drop its state. */
65347
+ forget(summaryId) {
65348
+ this.best.delete(summaryId);
65349
+ }
65350
+ get size() {
65351
+ return this.best.size;
65352
+ }
65353
+ };
65354
+ //#endregion
65355
+ //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
65356
+ async function persistSummaryGroupShot(deps, input) {
65357
+ const { deviceId, summaryId, frameHandle } = input;
65358
+ const drop = {
65359
+ tags: { deviceId },
65360
+ meta: {
65361
+ summaryId,
65362
+ nodeId: frameHandle.nodeId
65363
+ }
65364
+ };
65365
+ try {
65366
+ const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
65367
+ const native = result?.frame;
65368
+ if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
65369
+ deps.logger.warn("group shot native miss — the previous shot stands", {
65370
+ ...drop,
65371
+ meta: {
65372
+ ...drop.meta,
65373
+ shmId: frameHandle.shmId
65374
+ }
65375
+ });
65376
+ return { kind: "native-miss" };
65377
+ }
65378
+ if (!keyFrameAcceptsTier(result.tier)) {
65379
+ deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
65380
+ ...drop,
65381
+ meta: {
65382
+ ...drop.meta,
65383
+ shmId: frameHandle.shmId,
65384
+ tier: result.tier
65385
+ }
65386
+ });
65387
+ return { kind: "ram-tier" };
65388
+ }
65389
+ const { keyFrame, keyFrameSmall } = await deps.encodeVariants(native);
65390
+ const keyFrameKey = await deps.media.putReplacing({
65391
+ deviceId,
65392
+ ownerKind: "summary",
65393
+ ownerId: summaryId,
65394
+ kind: "keyFrame",
65395
+ timestamp: input.timestamp,
65396
+ data: keyFrame
65397
+ });
65398
+ try {
65399
+ await deps.media.putReplacing({
65400
+ deviceId,
65401
+ ownerKind: "summary",
65402
+ ownerId: summaryId,
65403
+ kind: "keyFrameSmall",
65404
+ timestamp: input.timestamp,
65405
+ data: keyFrameSmall
65406
+ });
65407
+ } catch (err) {
65408
+ deps.logger.warn("group shot web companion write failed — the native shot is stored", {
65409
+ ...drop,
65410
+ meta: {
65411
+ ...drop.meta,
65412
+ error: errMsg$1(err)
65413
+ }
65414
+ });
65415
+ }
65416
+ return {
65417
+ kind: "landed",
65418
+ keyFrameKey
65419
+ };
65420
+ } catch (err) {
65421
+ const error = errMsg$1(err);
65422
+ deps.logger.warn("group shot capture failed", {
65423
+ ...drop,
65424
+ meta: {
65425
+ ...drop.meta,
65426
+ error
65427
+ }
65428
+ });
65429
+ return {
65430
+ kind: "error",
65431
+ error
65432
+ };
65433
+ }
65434
+ }
65435
+ //#endregion
65436
+ //#region src/pipeline-analytics/summary/summary-sealer.ts
65437
+ function errMsg(err) {
65438
+ return err instanceof Error ? err.message : String(err);
65439
+ }
65440
+ var SummarySealer = class {
65441
+ deps;
65442
+ sweepInFlight = false;
65443
+ /**
65444
+ * What the FRAME path may ask at frame-rate: the open summary of a camera
65445
+ * and the members it has seen THIS process open. Fed only by this class's
65446
+ * own writes, never the authority (D224): a fresh process answers `null`
65447
+ * until a track opens, and a summary open across a restart is scored on the
65448
+ * members that open after it — a weaker shot, never a wrong one.
65449
+ */
65450
+ openMirror = /* @__PURE__ */ new Map();
65451
+ constructor(deps) {
65452
+ this.deps = deps;
65453
+ }
65454
+ openFor(deviceId) {
65455
+ return this.openMirror.get(deviceId) ?? null;
65456
+ }
65457
+ async onTrackOpened(input) {
65458
+ try {
65459
+ const row = await this.deps.summaries.openOrAbsorb({
65460
+ deviceId: input.deviceId,
65461
+ trackId: input.trackId,
65462
+ at: this.deps.now()
65463
+ });
65464
+ const mirror = this.openMirror.get(input.deviceId);
65465
+ if (mirror !== void 0 && mirror.id === row.id) mirror.members.add(input.trackId);
65466
+ else this.openMirror.set(input.deviceId, {
65467
+ id: row.id,
65468
+ members: new Set([input.trackId])
65469
+ });
65470
+ } catch (err) {
65471
+ this.deps.logger.warn("summary: could not open or absorb for a track", {
65472
+ tags: { deviceId: input.deviceId },
65473
+ meta: {
65474
+ trackId: input.trackId,
65475
+ error: errMsg(err)
65476
+ }
65477
+ });
65478
+ }
65479
+ }
65480
+ /** One sweep at a time (THE latch). A tick that finds one in flight returns. */
65481
+ async sweep() {
65482
+ if (this.sweepInFlight) return;
65483
+ this.sweepInFlight = true;
65484
+ try {
65485
+ const open = await this.deps.summaries.listOpen();
65486
+ for (const summary of open) await this.sealIfDone(summary);
65487
+ } catch (err) {
65488
+ this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
65489
+ } finally {
65490
+ this.sweepInFlight = false;
65491
+ }
65492
+ }
65493
+ async sealIfDone(summary) {
65494
+ const { id: summaryId, deviceId } = summary;
65495
+ let openTracks;
65496
+ try {
65497
+ openTracks = await this.deps.openTrackIds(deviceId);
65498
+ } catch (err) {
65499
+ this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
65500
+ tags: { deviceId },
65501
+ meta: {
65502
+ summaryId,
65503
+ error: errMsg(err)
65504
+ }
65505
+ });
65506
+ return;
65507
+ }
65508
+ if (openTracks.length > 0) return;
65509
+ let quiet;
65510
+ try {
65511
+ quiet = await this.deps.isCameraQuiet(deviceId);
65512
+ } catch (err) {
65513
+ this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
65514
+ tags: { deviceId },
65515
+ meta: {
65516
+ summaryId,
65517
+ error: errMsg(err)
65518
+ }
65519
+ });
65520
+ return;
65521
+ }
65522
+ if (!quiet) return;
65523
+ const sealedAt = this.deps.now();
65524
+ try {
65525
+ if (await this.deps.summaries.seal(summary, sealedAt)) {
65526
+ const mirror = this.openMirror.get(deviceId);
65527
+ const members = mirror?.id === summaryId ? mirror.members : /* @__PURE__ */ new Set();
65528
+ if (mirror?.id === summaryId) this.openMirror.delete(deviceId);
65529
+ this.deps.onSealed?.(summaryId, members);
65530
+ this.deps.logger.info("summary sealed", {
65531
+ tags: { deviceId },
65532
+ meta: {
65533
+ summaryId,
65534
+ sealedAt
65535
+ }
65536
+ });
65537
+ }
65538
+ } catch (err) {
65539
+ this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
65540
+ tags: { deviceId },
65541
+ meta: {
65542
+ summaryId,
65543
+ error: errMsg(err)
65544
+ }
65545
+ });
65546
+ }
65547
+ }
65548
+ };
65549
+ //#endregion
64841
65550
  //#region src/pipeline-analytics/store/settings-raw-mirror.ts
64842
65551
  /** How long a mirrored entry is served before a refresh is started behind the
64843
65552
  * next reader. See the module doc for why this matches the per-kind caches'
@@ -66150,6 +66859,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66150
66859
  storageMigrationLeaseId = null;
66151
66860
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
66152
66861
  sensorEventStore = null;
66862
+ /** The per-camera session envelope (D359). Bound with the other stores at boot. */
66863
+ summaryStore = null;
66864
+ /** The ONLY writer of a summary's lifecycle (D359). */
66865
+ summarySealer = null;
66866
+ /** The running argmax of every open summary's group shot (D359 phase 3). RAM only. */
66867
+ groupShots = new GroupShotTracker();
66868
+ /**
66869
+ * Tracks a gallery match has named, for the group-shot score's recognition
66870
+ * term. "Recognised so far" rather than "at that instant": the match arrives
66871
+ * on the detail plane, after the frame; the frames that follow carry it.
66872
+ * Pruned when the track's summary seals, so it is bounded by open summaries.
66873
+ */
66874
+ recognizedTrackIds = /* @__PURE__ */ new Set();
66153
66875
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
66154
66876
  * so the DeviceStateChanged handler stays cheap. */
66155
66877
  linkedCamerasCache = null;
@@ -66429,7 +67151,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66429
67151
  };
66430
67152
  this.ctx.logger.info("nc-action data-plane served", { meta: { served: served !== null } });
66431
67153
  } catch (err) {
66432
- this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg(err) } });
67154
+ this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg$1(err) } });
66433
67155
  }
66434
67156
  }
66435
67157
  /** Mints a button's callback URL once the action plane is served; null until
@@ -66463,7 +67185,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66463
67185
  await store.sweep();
66464
67186
  await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
66465
67187
  } catch (err) {
66466
- this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg(err) } });
67188
+ this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg$1(err) } });
66467
67189
  }
66468
67190
  };
66469
67191
  run();
@@ -66523,7 +67245,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66523
67245
  retentionDays: Math.round(store.retentionMs / 864e5)
66524
67246
  } });
66525
67247
  } catch (err) {
66526
- this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
67248
+ this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg$1(err) } });
66527
67249
  }
66528
67250
  }
66529
67251
  /** Public base URL for event thumbnails: `/addon/<addonId>/event-media`.
@@ -66767,7 +67489,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66767
67489
  tags: { deviceId },
66768
67490
  meta: {
66769
67491
  deviceId,
66770
- error: errMsg(error)
67492
+ error: errMsg$1(error)
66771
67493
  }
66772
67494
  });
66773
67495
  }
@@ -66796,7 +67518,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66796
67518
  tags: { deviceId },
66797
67519
  meta: {
66798
67520
  deviceId,
66799
- error: errMsg(error)
67521
+ error: errMsg$1(error)
66800
67522
  }
66801
67523
  });
66802
67524
  }
@@ -67257,7 +67979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67257
67979
  */
67258
67980
  armAlarmPanelDeclaration(api) {
67259
67981
  this.declareAlarmPanel(api).catch((err) => {
67260
- this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg(err) } });
67982
+ this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg$1(err) } });
67261
67983
  });
67262
67984
  }
67263
67985
  async declareAlarmPanel(api) {
@@ -67388,6 +68110,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67388
68110
  MediaStore.declare(api.settingsStore),
67389
68111
  EventStore.declare(api.settingsStore),
67390
68112
  SensorEventStore.declare(api.settingsStore),
68113
+ SummaryStore.declare(api.settingsStore),
67391
68114
  IdentityStore.declare(api.settingsStore),
67392
68115
  FaceStore.declare(api.settingsStore),
67393
68116
  PlateStore.declare(api.settingsStore),
@@ -67409,7 +68132,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67409
68132
  logger: this.ctx.logger.child("viewer-settings")
67410
68133
  });
67411
68134
  } catch (err) {
67412
- this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg(err) } });
68135
+ this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg$1(err) } });
67413
68136
  }
67414
68137
  runLabelTierMigration({
67415
68138
  store: api.settingsStore,
@@ -67481,7 +68204,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67481
68204
  try {
67482
68205
  locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
67483
68206
  } catch (err) {
67484
- logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
68207
+ logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg$1(err) } });
67485
68208
  this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
67486
68209
  return DEFAULT_MEDIA_LOCATION;
67487
68210
  }
@@ -67649,6 +68372,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67649
68372
  store: api.settingsStore,
67650
68373
  logger: logger.child("SensorEventStore")
67651
68374
  });
68375
+ this.summaryStore = new SummaryStore({
68376
+ store: api.settingsStore,
68377
+ logger: this.ctx.logger.child("summary"),
68378
+ onEvict: async (ids) => {
68379
+ await this.mediaStore?.deleteForOwner("summary", ids);
68380
+ }
68381
+ });
68382
+ this.summarySealer = new SummarySealer({
68383
+ summaries: this.summaryStore,
68384
+ openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
68385
+ isCameraQuiet: async (deviceId) => (this.stationaryRegistry?.count(deviceId) ?? 0) === 0,
68386
+ now: () => Date.now(),
68387
+ logger: this.ctx.logger.child("summary-sealer"),
68388
+ onSealed: (summaryId, members) => {
68389
+ this.groupShots.forget(summaryId);
68390
+ for (const trackId of members) this.recognizedTrackIds.delete(trackId);
68391
+ }
68392
+ });
67652
68393
  this.packageDropDetector = new PackageDropDetector({
67653
68394
  events: eventStore,
67654
68395
  emit: {
@@ -67686,7 +68427,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67686
68427
  onPersisted: (ev, phase) => this.notificationCenter?.onPackageEventPersisted(ev, phase),
67687
68428
  onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
67688
68429
  scope,
67689
- error: errMsg(err)
68430
+ error: errMsg$1(err)
67690
68431
  } })
67691
68432
  });
67692
68433
  this.linkedCamerasCache = new LinkedCamerasCache({
@@ -67700,7 +68441,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67700
68441
  linkedDevicesBatch: { getLinkedDevicesBatch: (input) => api.deviceManager.getLinkedDevicesBatch.query(input) },
67701
68442
  onError: (scope, err) => logger.warn("linked-cameras cache refresh failed", { meta: {
67702
68443
  scope,
67703
- error: errMsg(err)
68444
+ error: errMsg$1(err)
67704
68445
  } })
67705
68446
  });
67706
68447
  const syntheticTracks = new SyntheticTrackMaterializer({
@@ -67709,7 +68450,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67709
68450
  tracks: trackStore,
67710
68451
  onError: (scope, err) => logger.warn("synthetic track error", { meta: {
67711
68452
  scope,
67712
- error: errMsg(err)
68453
+ error: errMsg$1(err)
67713
68454
  } })
67714
68455
  });
67715
68456
  this.syntheticTracks = syntheticTracks;
@@ -67721,7 +68462,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67721
68462
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
67722
68463
  meta: {
67723
68464
  scope,
67724
- error: errMsg(err)
68465
+ error: errMsg$1(err)
67725
68466
  }
67726
68467
  })
67727
68468
  });
@@ -67731,7 +68472,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67731
68472
  logger: logger.child("AudioMarkers"),
67732
68473
  onError: (scope, err) => logger.warn("audio marker error", { meta: {
67733
68474
  scope,
67734
- error: errMsg(err)
68475
+ error: errMsg$1(err)
67735
68476
  } })
67736
68477
  });
67737
68478
  const galleryVectorStore = api.vectorStore;
@@ -68052,7 +68793,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68052
68793
  } catch (err) {
68053
68794
  this.ctx.logger.debug("zone ownership read failed", {
68054
68795
  tags: { deviceId },
68055
- meta: { error: errMsg(err) }
68796
+ meta: { error: errMsg$1(err) }
68056
68797
  });
68057
68798
  }
68058
68799
  return out;
@@ -68112,7 +68853,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68112
68853
  } catch (err) {
68113
68854
  return {
68114
68855
  ok: false,
68115
- error: errMsg(err)
68856
+ error: errMsg$1(err)
68116
68857
  };
68117
68858
  }
68118
68859
  },
@@ -68132,7 +68873,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68132
68873
  } catch (err) {
68133
68874
  return {
68134
68875
  ok: false,
68135
- error: errMsg(err)
68876
+ error: errMsg$1(err)
68136
68877
  };
68137
68878
  }
68138
68879
  },
@@ -68488,7 +69229,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68488
69229
  } catch (err) {
68489
69230
  this.ctx.logger.warn("readEventThumbnail failed", { meta: {
68490
69231
  eventId: id,
68491
- error: errMsg(err)
69232
+ error: errMsg$1(err)
68492
69233
  } });
68493
69234
  throw err;
68494
69235
  }
@@ -68496,6 +69237,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68496
69237
  this.eventMediaDataPlane = await this.ctx.dataPlane?.serve({
68497
69238
  prefix: "event-media",
68498
69239
  access: planeAccess("event-media"),
69240
+ share: ["events-view"],
68499
69241
  handler
68500
69242
  }) ?? null;
68501
69243
  this.eventMediaBaseUrl = this.eventMediaDataPlane !== null ? `/addon/${this.ctx.id}/event-media` : null;
@@ -68508,7 +69250,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68508
69250
  access: planeAccess("event-media")
68509
69251
  } });
68510
69252
  } catch (err) {
68511
- this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg(err) } });
69253
+ this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg$1(err) } });
68512
69254
  }
68513
69255
  }
68514
69256
  /**
@@ -68534,7 +69276,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68534
69276
  handler
68535
69277
  }) ?? null;
68536
69278
  } catch (err) {
68537
- this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg(err) } });
69279
+ this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
68538
69280
  }
68539
69281
  }
68540
69282
  /**
@@ -68835,7 +69577,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68835
69577
  handler
68836
69578
  }) ?? null;
68837
69579
  } catch (err) {
68838
- this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg(err) } });
69580
+ this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
68839
69581
  }
68840
69582
  }
68841
69583
  /**
@@ -68864,7 +69606,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68864
69606
  handler
68865
69607
  }) ?? null;
68866
69608
  } catch (err) {
68867
- this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg(err) } });
69609
+ this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
68868
69610
  }
68869
69611
  }
68870
69612
  /**
@@ -68929,7 +69671,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68929
69671
  tags: { deviceId },
68930
69672
  meta: {
68931
69673
  deviceId,
68932
- error: errMsg(err)
69674
+ error: errMsg$1(err)
68933
69675
  }
68934
69676
  });
68935
69677
  return null;
@@ -69041,7 +69783,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69041
69783
  }, RETENTION_SWEEP_INTERVAL_MS);
69042
69784
  this.ltsFlushTimer = setInterval(() => {
69043
69785
  this.lts?.flushDue().catch((err) => {
69044
- this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg(err) } });
69786
+ this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg$1(err) } });
69045
69787
  });
69046
69788
  }, LTS_FLUSH_INTERVAL_MS);
69047
69789
  this.startOrphanAuditTimer();
@@ -69062,7 +69804,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69062
69804
  } catch (err) {
69063
69805
  this.ctx.logger.error("artifacts storage location is NOT available — enrolling a face, a plate or a scene reference will FAIL until it is", { meta: {
69064
69806
  location: ARTIFACTS_MEDIA_LOCATION,
69065
- error: errMsg(err),
69807
+ error: errMsg$1(err),
69066
69808
  hint: "the declaration is seeded when a storage-provider registers; restart this node so the manifest is re-read, or create an artifacts location. Refusing the write is deliberate — the footage volume is where the last gallery was lost (D197), and the addon data dir carries no media (D341)."
69067
69809
  } });
69068
69810
  return;
@@ -69079,7 +69821,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69079
69821
  hint: "the pixels are gone for good; re-enrol these people from a photo"
69080
69822
  } });
69081
69823
  } catch (err) {
69082
- this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg(err) } });
69824
+ this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg$1(err) } });
69083
69825
  }
69084
69826
  try {
69085
69827
  const identityStore = this.identityStore;
@@ -69111,7 +69853,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69111
69853
  maxReclaimPerRun: 16
69112
69854
  })).reclaimed > 0) await this.faceRecognizer?.refreshGallery();
69113
69855
  } catch (err) {
69114
- this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg(err) } });
69856
+ this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg$1(err) } });
69115
69857
  }
69116
69858
  }
69117
69859
  /** Schedule the orphan audit on the operator's interval (hours). */
@@ -69253,7 +69995,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69253
69995
  engine.start();
69254
69996
  this.sceneEngine = engine;
69255
69997
  this.sceneReloadTimer = setInterval(() => {
69256
- store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg(err) } }));
69998
+ store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg$1(err) } }));
69257
69999
  }, SCENE_RELOAD_INTERVAL_MS);
69258
70000
  this.sceneReloadTimer.unref?.();
69259
70001
  }
@@ -69297,7 +70039,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69297
70039
  lng: status.location.longitude
69298
70040
  };
69299
70041
  } catch (err) {
69300
- this.ctx.logger.debug("site location read failed", { meta: { error: errMsg(err) } });
70042
+ this.ctx.logger.debug("site location read failed", { meta: { error: errMsg$1(err) } });
69301
70043
  }
69302
70044
  this.siteCoordsCache = {
69303
70045
  value,
@@ -69316,7 +70058,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69316
70058
  } catch (err) {
69317
70059
  this.ctx.logger.debug("scene slice mirror failed", {
69318
70060
  tags: { deviceId },
69319
- meta: { error: errMsg(err) }
70061
+ meta: { error: errMsg$1(err) }
69320
70062
  });
69321
70063
  }
69322
70064
  }
@@ -69861,6 +70603,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69861
70603
  className: t.className
69862
70604
  }
69863
70605
  });
70606
+ this.summarySealer?.onTrackOpened({
70607
+ deviceId,
70608
+ trackId: id
70609
+ });
69864
70610
  const startPayload = buildTrackLifecyclePayload({
69865
70611
  deviceId,
69866
70612
  trackId: id,
@@ -70078,6 +70824,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70078
70824
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
70079
70825
  else plateCrops += 1;
70080
70826
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
70827
+ this.offerGroupShot(deviceId, result, frameHandle);
70081
70828
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
70082
70829
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
70083
70830
  const rasterFallbackCandidates = [];
@@ -70389,7 +71136,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70389
71136
  } catch (err) {
70390
71137
  this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
70391
71138
  nodeId,
70392
- error: errMsg(err)
71139
+ error: errMsg$1(err)
70393
71140
  } });
70394
71141
  }
70395
71142
  this.confirmationDetectorStepByNode.set(nodeId, {
@@ -70424,7 +71171,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70424
71171
  } catch (err) {
70425
71172
  this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
70426
71173
  tags: { deviceId },
70427
- meta: { error: errMsg(err) }
71174
+ meta: { error: errMsg$1(err) }
70428
71175
  });
70429
71176
  return null;
70430
71177
  }
@@ -70728,7 +71475,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70728
71475
  meta: {
70729
71476
  trackId,
70730
71477
  stepId: d.stepId,
70731
- error: errMsg(err)
71478
+ error: errMsg$1(err)
70732
71479
  }
70733
71480
  });
70734
71481
  }
@@ -70834,7 +71581,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70834
71581
  try {
70835
71582
  value = (await this.ctx.api.embeddingEncoder.getInfo.query())?.modelId ?? null;
70836
71583
  } catch (err) {
70837
- this.ctx.logger.debug("resolveClipModelId: getInfo failed", { meta: { error: errMsg(err) } });
71584
+ this.ctx.logger.debug("resolveClipModelId: getInfo failed", { meta: { error: errMsg$1(err) } });
70838
71585
  }
70839
71586
  this.clipModelIdCache = {
70840
71587
  value,
@@ -70892,7 +71639,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70892
71639
  tags: { deviceId },
70893
71640
  meta: {
70894
71641
  trackId: t.trackId,
70895
- error: errMsg(err)
71642
+ error: errMsg$1(err)
70896
71643
  }
70897
71644
  });
70898
71645
  }
@@ -70960,6 +71707,63 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70960
71707
  * is the scheduling (coalescing, priority, budget) and the bookkeeping the
70961
71708
  * retry gate reads.
70962
71709
  */
71710
+ /**
71711
+ * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
71712
+ * Metadata only, at frame-rate: the frame is scored against the members the
71713
+ * sealer has mirrored, and the pixels are fetched — by handle, at the owning
71714
+ * node, as a native key frame — only when this frame beats the best so far.
71715
+ */
71716
+ offerGroupShot(deviceId, result, frameHandle) {
71717
+ const open = this.summarySealer?.openFor(deviceId);
71718
+ if (!open || frameHandle === void 0) return;
71719
+ const score = scoreSummaryFrame({
71720
+ members: open.members,
71721
+ frameWidth: result.frameWidth,
71722
+ frameHeight: result.frameHeight,
71723
+ detections: result.tracked.map((t) => ({
71724
+ trackId: t.trackId,
71725
+ bbox: {
71726
+ x: t.bbox.x,
71727
+ y: t.bbox.y,
71728
+ width: t.bbox.w,
71729
+ height: t.bbox.h
71730
+ },
71731
+ confidence: t.confidence,
71732
+ recognized: this.recognizedTrackIds.has(t.trackId)
71733
+ }))
71734
+ });
71735
+ if (!this.groupShots.offer(open.id, score, { timestamp: result.timestamp })) return;
71736
+ const getNative = this.getNativeKeyFrameRgb;
71737
+ const mediaStore = this.mediaStore;
71738
+ if (!getNative || !mediaStore) return;
71739
+ const summaryId = open.id;
71740
+ this.captureScheduler.request({
71741
+ deviceId,
71742
+ kind: "keyFrame",
71743
+ coalesceKey: `summary:${summaryId}:keyFrame`,
71744
+ exec: () => persistSummaryGroupShot({
71745
+ fetchNativeFullFrame: getNative,
71746
+ encodeVariants: encodeKeyFrameVariants,
71747
+ media: mediaStore,
71748
+ logger: this.ctx.logger
71749
+ }, {
71750
+ deviceId,
71751
+ summaryId,
71752
+ timestamp: result.timestamp,
71753
+ frameHandle,
71754
+ maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
71755
+ })
71756
+ }).then((outcome) => {
71757
+ if (outcome?.kind === "landed") this.ctx.logger.debug("summary group shot landed", {
71758
+ tags: { deviceId },
71759
+ meta: {
71760
+ summaryId,
71761
+ visible: score.visible,
71762
+ quality: Number(score.quality.toFixed(3))
71763
+ }
71764
+ });
71765
+ }).catch(() => {});
71766
+ }
70963
71767
  async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
70964
71768
  const getNative = this.getNativeKeyFrameRgb;
70965
71769
  const mediaStore = this.mediaStore;
@@ -71045,7 +71849,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71045
71849
  meta: {
71046
71850
  trackId,
71047
71851
  keyFrameKey,
71048
- error: errMsg(err)
71852
+ error: errMsg$1(err)
71049
71853
  }
71050
71854
  });
71051
71855
  }
@@ -71104,6 +71908,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71104
71908
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
71105
71909
  * fine-classification attribution. */
71106
71910
  promoteSemanticRecognition(input) {
71911
+ this.recognizedTrackIds.add(input.trackId);
71107
71912
  const ctx = this.ctxIfReady;
71108
71913
  if (ctx === null) return;
71109
71914
  promoteSemanticBestFrame({
@@ -71531,7 +72336,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71531
72336
  tags: { deviceId: input.deviceId },
71532
72337
  meta: {
71533
72338
  className: ref.className,
71534
- error: errMsg(err)
72339
+ error: errMsg$1(err)
71535
72340
  }
71536
72341
  });
71537
72342
  }
@@ -71563,7 +72368,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71563
72368
  tags: { deviceId: track.deviceId },
71564
72369
  meta: {
71565
72370
  episodeId: ref.episodeId,
71566
- error: errMsg(err)
72371
+ error: errMsg$1(err)
71567
72372
  }
71568
72373
  });
71569
72374
  }
@@ -71885,6 +72690,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71885
72690
  async sweepExpiredTracks() {
71886
72691
  if (this.shuttingDown || !this.trackStore) return;
71887
72692
  await this.trackCloser.sweep();
72693
+ await this.summarySealer?.sweep();
71888
72694
  }
71889
72695
  /** Unified retention (Phase 1): earliest retained footage per device — the
71890
72696
  * follow-recordings cutoff source. Cached 10 min: availability answers in
@@ -71935,7 +72741,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71935
72741
  listTrackedDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
71936
72742
  }, (half, err) => this.ctx.logger.warn("retention scope half unavailable — sweeping on the other", { meta: {
71937
72743
  half,
71938
- error: errMsg(err)
72744
+ error: errMsg$1(err)
71939
72745
  } }));
71940
72746
  }
71941
72747
  /**
@@ -71981,6 +72787,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71981
72787
  await this.runTrackRetentionSweep(deviceIds, now);
71982
72788
  const eventStore = this.eventStore;
71983
72789
  const sensorEventStore = this.sensorEventStore;
72790
+ const summaryStore = this.summaryStore;
71984
72791
  await sweepTracklessRoots({
71985
72792
  deviceIds,
71986
72793
  nowMs: now,
@@ -71990,15 +72797,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71990
72797
  cutoffMs
71991
72798
  }),
71992
72799
  evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
72800
+ evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
71993
72801
  onEvicted: async (deviceId, evicted, cutoffs) => {
71994
- const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
72802
+ const total = evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries;
71995
72803
  await this.eventsOpsLog?.append({
71996
72804
  op: "prune",
71997
72805
  reason: "retention",
71998
72806
  deviceId,
71999
72807
  itemsAffected: total,
72000
72808
  bytesReclaimed: 0,
72001
- detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor`
72809
+ detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor, ${evicted.summaries} summaries`
72002
72810
  });
72003
72811
  this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
72004
72812
  tags: { deviceId },
@@ -72006,6 +72814,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72006
72814
  motion: evicted.motion.length,
72007
72815
  audio: evicted.audio.length,
72008
72816
  sensor: evicted.sensor,
72817
+ summaries: evicted.summaries,
72009
72818
  mode: cutoffs.effectiveMode,
72010
72819
  cutoffMs: cutoffs.cutoffMs
72011
72820
  }
@@ -72825,7 +73634,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72825
73634
  const status = await api.pipelineOrchestrator.getCameraStatus.query({ deviceId: input.deviceId }).catch((err) => {
72826
73635
  this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
72827
73636
  tags: { deviceId: input.deviceId },
72828
- meta: { error: errMsg(err) }
73637
+ meta: { error: errMsg$1(err) }
72829
73638
  });
72830
73639
  return null;
72831
73640
  });
@@ -72844,7 +73653,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72844
73653
  tags: { deviceId: input.deviceId },
72845
73654
  meta: {
72846
73655
  scope,
72847
- error: errMsg(err)
73656
+ error: errMsg$1(err)
72848
73657
  }
72849
73658
  })
72850
73659
  }, input.deviceId);
@@ -72877,7 +73686,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72877
73686
  const statuses = await api.pipelineOrchestrator.getCameraStatuses.query({ deviceIds }).catch((err) => {
72878
73687
  this.ctx.logger.warn("listEventKindsBatch: getCameraStatuses failed", { meta: {
72879
73688
  deviceCount: deviceIds.length,
72880
- error: errMsg(err)
73689
+ error: errMsg$1(err)
72881
73690
  } });
72882
73691
  return [];
72883
73692
  });
@@ -72889,7 +73698,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72889
73698
  }, deviceIds).catch((err) => {
72890
73699
  this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
72891
73700
  deviceCount: deviceIds.length,
72892
- error: errMsg(err)
73701
+ error: errMsg$1(err)
72893
73702
  } });
72894
73703
  return null;
72895
73704
  });
@@ -72909,13 +73718,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72909
73718
  tags: { deviceId },
72910
73719
  meta: {
72911
73720
  scope,
72912
- error: errMsg(err)
73721
+ error: errMsg$1(err)
72913
73722
  }
72914
73723
  })
72915
73724
  }, deviceId);
72916
73725
  }, (deviceId, err) => this.ctx.logger.warn("listEventKindsBatch: compose threw — empty vocabulary", {
72917
73726
  tags: { deviceId },
72918
- meta: { error: errMsg(err) }
73727
+ meta: { error: errMsg$1(err) }
72919
73728
  }));
72920
73729
  }
72921
73730
  /** Per-camera sensor-event history (state changes of linked devices). */
@@ -72961,7 +73770,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72961
73770
  tags: { deviceId: data.deviceId },
72962
73771
  meta: {
72963
73772
  capName: data.capName,
72964
- error: errMsg(err)
73773
+ error: errMsg$1(err)
72965
73774
  }
72966
73775
  });
72967
73776
  }
@@ -73004,7 +73813,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73004
73813
  tags: { deviceId: data.deviceId },
73005
73814
  meta: {
73006
73815
  capName: data.capName,
73007
- error: errMsg(err)
73816
+ error: errMsg$1(err)
73008
73817
  }
73009
73818
  });
73010
73819
  }
@@ -73102,7 +73911,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73102
73911
  } catch (err) {
73103
73912
  this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
73104
73913
  tags: { deviceId },
73105
- meta: { error: errMsg(err) }
73914
+ meta: { error: errMsg$1(err) }
73106
73915
  });
73107
73916
  return;
73108
73917
  }
@@ -73338,6 +74147,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73338
74147
  },
73339
74148
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
73340
74149
  liveEventIds: (ids) => eventStore.existingEventIds(ids),
74150
+ liveSummaryIds: (ids) => {
74151
+ const summaries = this.summaryStore;
74152
+ if (!summaries) throw new Error("summary store not built — probe unavailable");
74153
+ return summaries.existingIds(ids);
74154
+ },
73341
74155
  reclaim: async (collection, key) => {
73342
74156
  if (collection === "pipeline-analytics:media") {
73343
74157
  await mediaStore.deleteByKey(key);
@@ -73771,7 +74585,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73771
74585
  nodeId
73772
74586
  };
73773
74587
  } catch (err) {
73774
- const reason = errMsg(err);
74588
+ const reason = errMsg$1(err);
73775
74589
  failures.push(`${nodeId}: ${reason}`);
73776
74590
  this.ctx.logger.warn("photo enrollment: a node could not analyse the photograph", { meta: {
73777
74591
  nodeId,
@@ -74485,7 +75299,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74485
75299
  mediaStore,
74486
75300
  faceStore: this.faceStore,
74487
75301
  plateStore: this.plateStore,
74488
- objectEmbeddingStore: this.objectEmbeddingStore
75302
+ objectEmbeddingStore: this.objectEmbeddingStore,
75303
+ summaryStore: this.summaryStore
74489
75304
  };
74490
75305
  const absent = absentCascadeLegs(stores);
74491
75306
  if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
@@ -74971,7 +75786,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74971
75786
  if (!file) return null;
74972
75787
  return {
74973
75788
  bytes: Buffer.from(file.base64, "base64"),
74974
- key: file.key
75789
+ key: file.key,
75790
+ deviceId: file.deviceId
74975
75791
  };
74976
75792
  }
74977
75793
  /**
@@ -74985,13 +75801,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74985
75801
  try {
74986
75802
  return {
74987
75803
  bytes: await makeSquareThumb(media.bytes, variant.size),
74988
- key: `${media.key}|t${variant.size}`
75804
+ key: `${media.key}|t${variant.size}`,
75805
+ deviceId: media.deviceId
74989
75806
  };
74990
75807
  } catch (err) {
74991
75808
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
74992
75809
  key: media.key,
74993
75810
  size: variant.size,
74994
- error: errMsg(err)
75811
+ error: errMsg$1(err)
74995
75812
  } });
74996
75813
  return media;
74997
75814
  }
@@ -75015,7 +75832,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75015
75832
  const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
75016
75833
  return file === null ? null : {
75017
75834
  bytes: Buffer.from(file.base64, "base64"),
75018
- key: file.key
75835
+ key: file.key,
75836
+ deviceId: file.deviceId
75019
75837
  };
75020
75838
  };
75021
75839
  const eventFiles = await listInfo("event", id);