@camstack/addon-post-analysis 1.2.297 → 1.2.298

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 NcRuleSchema, $n as string, $t as isSystemEventSummaryRule, A as LockStateSchema, An as evaluateSensorEdge, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as isDeviceScopedCap, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventSummaryScheduleOf, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as zoneAnalyticsCapability, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as vectorDimFromBase64, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as CamProfileSchema, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as array, Gt as faceGalleryCapability, I as MotionTriggeredBySchema, In as createEvent, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as literal, Jt as isEventOwnerType, K as NC_TEMPLATE_VARS, Kn as boolean, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as customAction, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as DeviceType, Mt as buildEventKindDescriptor, Nn as errMsg$1, Nt as consumableLowPercentOf, O as FailureCounters, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as BaseAddon, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as record, Qt as isSystemDelivery, R as NC_CONDITION_CATALOG, Rn as defineCustomActions, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventFilterApplies, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as templateVarsFor, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as sleep$1, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as nodePin, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as _enum, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as object, Xt as isMediaOwnerType, Y as NcConditionDescriptorSchema, Yn as number, Yt as isKnownTemplateVar, Z as NcRuleInputSchema, Zn as partialRecord, Zt as isScheduleActive, _n as servedTemplateVars, a as CLIP_EMBEDDING_STEP_ID, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as systemEventDeliveryProblems, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as readDeviceStateFrom, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as resolveClipSearchMinScore, dt as OpsLogEntrySchema, en as isVectorIndexDimConflict, er as unknown, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveLocationMode, g as DEFAULT_EVENT_COLOR, gn as sensorRaisedOf, gt as RetrainStatusSchema, h as CoverStateSchema, hn as sceneMonitorCapability, ht as RebuildObjectEmbeddingsRefusalSchema, in as occupancyScope, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as isSourceCap, jt as audioModeOf, k as LabelAttributionSchema, kn as canonicalHash, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as readMotionTriggeredBy, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as ruleBelongsToAlarm$1, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as mayWriteToLocation, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as pipelineAnalyticsCapability, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as discriminatedUnion, qt as isDetectionMacroClass, rn as notificationRulesCapability, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, sn as plateGalleryCapability, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as kebabToCamel, tr as EventCategory, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readTimelapseGeneratedAt, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as storageOccupancyCapability, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as templateCarriesSummary, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as systemEventFamilyProblems, xt as SceneMonitorSchema, yn as subKindsOf, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as hydrateSchema } from "../clip-model-registry-BP_o2Oi4.mjs";
1
+ import { $ as NcRuleSchema, $n as string, $t as isSystemEventSummaryRule, A as LockStateSchema, An as evaluateSensorEdge, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as isDeviceScopedCap, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventSummaryScheduleOf, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as zoneAnalyticsCapability, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as vectorDimFromBase64, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as CamProfileSchema, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as array, Gt as faceGalleryCapability, I as MotionTriggeredBySchema, In as createEvent, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as literal, Jt as isEventOwnerType, K as NC_TEMPLATE_VARS, Kn as boolean, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as customAction, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as DeviceType, Mt as buildEventKindDescriptor, Nn as errMsg, Nt as consumableLowPercentOf, O as FailureCounters, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as BaseAddon, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as record, Qt as isSystemDelivery, R as NC_CONDITION_CATALOG, Rn as defineCustomActions, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventFilterApplies, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as templateVarsFor, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as sleep$1, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as nodePin, W as NC_TANK_SYSTEM_EVENT_KINDS, Wn as _enum, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as object, Xt as isMediaOwnerType, Y as NcConditionDescriptorSchema, Yn as number, Yt as isKnownTemplateVar, Z as NcRuleInputSchema, Zn as partialRecord, Zt as isScheduleActive, _n as servedTemplateVars, a as CLIP_EMBEDDING_STEP_ID, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as systemEventDeliveryProblems, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as readDeviceStateFrom, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as resolveClipSearchMinScore, dt as OpsLogEntrySchema, en as isVectorIndexDimConflict, er as unknown, et as NcRuleTargetSchema, f as BaseDevice, fn as resolveLocationMode, g as DEFAULT_EVENT_COLOR, gn as sensorRaisedOf, gt as RetrainStatusSchema, h as CoverStateSchema, hn as sceneMonitorCapability, ht as RebuildObjectEmbeddingsRefusalSchema, in as occupancyScope, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as isSourceCap, jt as audioModeOf, k as LabelAttributionSchema, kn as canonicalHash, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as readMotionTriggeredBy, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as ruleBelongsToAlarm$1, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as mayWriteToLocation, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as pipelineAnalyticsCapability, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as discriminatedUnion, qt as isDetectionMacroClass, rn as notificationRulesCapability, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, sn as plateGalleryCapability, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as kebabToCamel, tr as EventCategory, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readTimelapseGeneratedAt, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as storageOccupancyCapability, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as templateCarriesSummary, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as systemEventFamilyProblems, xt as SceneMonitorSchema, yn as subKindsOf, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as hydrateSchema } from "../clip-model-registry-D9WaswEs.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";
@@ -2708,8 +2708,8 @@ var NC_ARTIFACTS_INDEXES = [
2708
2708
  }
2709
2709
  ];
2710
2710
  /** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
2711
- var TIME_MAX$1 = 864e13;
2712
- var TIME_MIN$1 = -864e13;
2711
+ var TIME_MAX = 864e13;
2712
+ var TIME_MIN = -864e13;
2713
2713
  /** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
2714
2714
  * entries by default), so this only has to be comfortably above it. */
2715
2715
  var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
@@ -2949,7 +2949,7 @@ var NcArtifactIndex = class {
2949
2949
  try {
2950
2950
  const expired = await this.store.deleteWhere.mutate({
2951
2951
  collection: NC_ARTIFACTS_COLLECTION,
2952
- filter: { whereBetween: { expiresAt: [TIME_MIN$1, this.now() - 1] } }
2952
+ filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
2953
2953
  });
2954
2954
  removed += expired.deleted;
2955
2955
  if (keptIds === null) {
@@ -2991,8 +2991,8 @@ var NcArtifactIndex = class {
2991
2991
  whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
2992
2992
  if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
2993
2993
  const whereBetween = {
2994
- capturedAt: [query.fromMs ?? TIME_MIN$1, query.toMs ?? TIME_MAX$1],
2995
- expiresAt: [this.now(), TIME_MAX$1]
2994
+ capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
2995
+ expiresAt: [this.now(), TIME_MAX]
2996
2996
  };
2997
2997
  return {
2998
2998
  ...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
@@ -15434,21 +15434,6 @@ var NcSummaryRuleInputSchema = object({
15434
15434
  deviceIds: DeviceIdsField,
15435
15435
  window: NcSummaryWindowSchema,
15436
15436
  filters: NcSummaryFiltersSchema.optional(),
15437
- /**
15438
- * What ONE line of this digest is: a subject, or a group of subjects that
15439
- * arrived together.
15440
- *
15441
- * `tracks` is what the digest has always done and stays the default — the
15442
- * operator's instruction when groups were added was that the first behaviour
15443
- * remains. `groups` collapses the members of each session summary to one
15444
- * entry, so a family walking in is one line and not four
15445
- * (`summary-group-collapse.ts`).
15446
- *
15447
- * `.default('tracks')` is safe here where `NcSummaryFiltersSchema` fields are
15448
- * not: this is a field of the RULE, not of the partial `filters` object that
15449
- * a patch materialises wholesale.
15450
- */
15451
- subject: _enum(["tracks", "groups"]).default("tracks"),
15452
15437
  maxTiles: MaxTilesField.default(12),
15453
15438
  maxAiImages: MaxAiImagesField.default(6),
15454
15439
  rank: NcSummaryRankSchema.default("importance"),
@@ -15483,7 +15468,6 @@ var NcSummaryRulePatchSchema = object({
15483
15468
  maxTiles: MaxTilesField.optional(),
15484
15469
  maxAiImages: MaxAiImagesField.optional(),
15485
15470
  rank: NcSummaryRankSchema.optional(),
15486
- subject: _enum(["tracks", "groups"]).optional(),
15487
15471
  barrierSeconds: BarrierSecondsField.optional(),
15488
15472
  targets: TargetsField.optional(),
15489
15473
  template: NcSummaryTemplateSchema.nullable().optional(),
@@ -26277,52 +26261,6 @@ function buildSummaryOutboxInputs(input) {
26277
26261
  });
26278
26262
  }
26279
26263
  //#endregion
26280
- //#region src/notification-center/summary/summary-group-collapse.ts
26281
- function collapseToGroups(candidates, groups) {
26282
- const claim = /* @__PURE__ */ new Map();
26283
- for (const group of groups) for (const trackId of group.memberTrackIds) if (!claim.has(trackId)) claim.set(trackId, group);
26284
- const survivorsByGroup = /* @__PURE__ */ new Map();
26285
- for (const candidate of candidates) {
26286
- const group = claim.get(candidate.trackId);
26287
- if (group === void 0) continue;
26288
- const bucket = survivorsByGroup.get(group.summaryId);
26289
- if (bucket === void 0) survivorsByGroup.set(group.summaryId, [candidate]);
26290
- else bucket.push(candidate);
26291
- }
26292
- const representative = /* @__PURE__ */ new Map();
26293
- for (const [summaryId, survivors] of survivorsByGroup) {
26294
- const group = groups.find((g) => g.summaryId === summaryId);
26295
- if (group === void 0) continue;
26296
- const chosen = survivors.find((c) => c.trackId === group.primaryTrackId) ?? survivors[0];
26297
- if (chosen === void 0) continue;
26298
- representative.set(summaryId, {
26299
- ...chosen,
26300
- groupSize: survivors.length,
26301
- ...group.groupShotMediaId !== null ? { groupShotMediaId: group.groupShotMediaId } : {}
26302
- });
26303
- }
26304
- const kept = [];
26305
- let collapsed = 0;
26306
- for (const candidate of candidates) {
26307
- const group = claim.get(candidate.trackId);
26308
- if (group === void 0) {
26309
- kept.push(candidate);
26310
- continue;
26311
- }
26312
- const chosen = representative.get(group.summaryId);
26313
- if (chosen === void 0) {
26314
- kept.push(candidate);
26315
- continue;
26316
- }
26317
- if (chosen.trackId === candidate.trackId) kept.push(chosen);
26318
- else collapsed += 1;
26319
- }
26320
- return {
26321
- kept,
26322
- collapsed
26323
- };
26324
- }
26325
- //#endregion
26326
26264
  //#region src/notification-center/summary/summary-selection.ts
26327
26265
  /**
26328
26266
  * Which tracks become tiles — filter, rank, and the per-camera quota.
@@ -26916,8 +26854,7 @@ var NcSummaryProducer = class {
26916
26854
  try {
26917
26855
  const candidates = await this.collectCandidates(rule, window);
26918
26856
  const filtered = filterSummaryCandidates(candidates, rule.filters, this.deps.zoneOwner);
26919
- const grouped = await this.collapseIfGrouping(rule, window, filtered.kept);
26920
- const selection = selectSummaryTiles(grouped.kept, {
26857
+ const selection = selectSummaryTiles(filtered.kept, {
26921
26858
  maxTiles: rule.maxTiles,
26922
26859
  rank: rule.rank
26923
26860
  });
@@ -26946,7 +26883,7 @@ var NcSummaryProducer = class {
26946
26883
  const tileLoad = await this.loadTiles({
26947
26884
  rule,
26948
26885
  preferred: selection.tiles,
26949
- refill: remainingSummaryCandidates(grouped.kept, selection.tiles, rule.rank),
26886
+ refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
26950
26887
  maxTiles: rule.maxTiles
26951
26888
  });
26952
26889
  const loaded = tileLoad.tiles;
@@ -26987,7 +26924,7 @@ var NcSummaryProducer = class {
26987
26924
  priority: rule.priority,
26988
26925
  tileCount: loaded.length,
26989
26926
  cameraCount: new Set(loaded.map((t) => t.candidate.deviceId)).size,
26990
- matchedCount: grouped.kept.length,
26927
+ matchedCount: filtered.kept.length,
26991
26928
  detections,
26992
26929
  ...mosaic.ref !== null ? { mosaic: mosaic.ref } : {},
26993
26930
  generatedAt: this.deps.now(),
@@ -26999,7 +26936,7 @@ var NcSummaryProducer = class {
26999
26936
  this.deps.logger.info("summary produced", { meta: {
27000
26937
  ...meta,
27001
26938
  scanned: candidates.length,
27002
- matched: grouped.kept.length,
26939
+ matched: filtered.kept.length,
27003
26940
  droppedStationary: filtered.droppedStationary,
27004
26941
  droppedUnconfirmed: filtered.droppedUnconfirmed,
27005
26942
  unknownConfirmation: filtered.unknownConfirmation,
@@ -27022,7 +26959,7 @@ var NcSummaryProducer = class {
27022
26959
  window,
27023
26960
  tiles: loaded.length,
27024
26961
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
27025
- matched: grouped.kept.length,
26962
+ matched: filtered.kept.length,
27026
26963
  droppedStationary: filtered.droppedStationary,
27027
26964
  enqueued,
27028
26965
  ...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
@@ -27043,54 +26980,6 @@ var NcSummaryProducer = class {
27043
26980
  };
27044
26981
  }
27045
26982
  }
27046
- /**
27047
- * Fold each session summary's surviving members into one entry — or do
27048
- * nothing at all, which is what `tracks` (the default) means.
27049
- *
27050
- * A failure to read the groups does NOT fail the digest: it falls back to
27051
- * subjects and says so. A digest of four lines instead of one is a worse
27052
- * reading, not a lost one, and losing the night's record over a store read
27053
- * is the wrong trade.
27054
- */
27055
- async collapseIfGrouping(rule, window, kept) {
27056
- if (rule.subject !== "groups") return {
27057
- kept,
27058
- collapsed: 0
27059
- };
27060
- const listGroups = this.deps.listSummaryGroups;
27061
- if (listGroups === void 0) {
27062
- this.deps.logger.warn("summary rule asks for groups but this node cannot read them", { meta: { ruleId: rule.id } });
27063
- return {
27064
- kept,
27065
- collapsed: 0
27066
- };
27067
- }
27068
- const groups = [];
27069
- for (const deviceId of rule.deviceIds) try {
27070
- groups.push(...await listGroups({
27071
- deviceId,
27072
- sinceMs: window.startMs,
27073
- untilMs: window.endMs
27074
- }));
27075
- } catch (err) {
27076
- this.deps.logger.warn("summary could not read a camera’s groups — read as subjects", {
27077
- tags: { deviceId },
27078
- meta: {
27079
- ruleId: rule.id,
27080
- error: String(err)
27081
- }
27082
- });
27083
- }
27084
- const outcome = collapseToGroups(kept, groups);
27085
- this.deps.logger.info("summary read as groups", { meta: {
27086
- ruleId: rule.id,
27087
- groups: groups.length,
27088
- subjects: kept.length,
27089
- lines: outcome.kept.length,
27090
- collapsed: outcome.collapsed
27091
- } });
27092
- return outcome;
27093
- }
27094
26983
  /** Every camera's tracks in the window, capped per camera and counted. */
27095
26984
  async collectCandidates(rule, window) {
27096
26985
  const all = [];
@@ -27141,25 +27030,6 @@ var NcSummaryProducer = class {
27141
27030
  seenIds.add(candidate.trackId);
27142
27031
  const tags = { deviceId: candidate.deviceId };
27143
27032
  try {
27144
- const groupShot = candidate.groupShotMediaId;
27145
- if (groupShot !== void 0) {
27146
- const jpeg = await this.deps.readMedia(groupShot);
27147
- if (jpeg !== null && jpeg.byteLength > 0) {
27148
- tiles.push({
27149
- candidate,
27150
- jpeg
27151
- });
27152
- continue;
27153
- }
27154
- this.deps.logger.info("summary tile: group shot unreadable — using a member", {
27155
- tags,
27156
- meta: {
27157
- ruleId: input.rule.id,
27158
- trackId: candidate.trackId,
27159
- mediaId: groupShot
27160
- }
27161
- });
27162
- }
27163
27033
  const rows = await this.deps.listMediaInfo(candidate.trackId);
27164
27034
  const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
27165
27035
  if (picked === null) {
@@ -35435,7 +35305,6 @@ var StoragePressureTracker = class {
35435
35305
  */
35436
35306
  var OWNER_KINDS = [
35437
35307
  "track",
35438
- "summary",
35439
35308
  "face",
35440
35309
  "identity",
35441
35310
  "plate",
@@ -35497,10 +35366,19 @@ function isWriterBoundedOwnerKind(value) {
35497
35366
  * `{ kind: 'retired-owner' }` and the orphan audit reclaims it under the same
35498
35367
  * grace window and ceilings as any other orphan.
35499
35368
  *
35369
+ * `'summary'` is the same case one entity later: the session summary (the
35370
+ * proximity group, D489/D490) was deleted on 2026-09-29 (D680) with
35371
+ * `pipeline-analytics:summaries` / `:summary-members`. Its group shots were
35372
+ * `summary`-owned media rows; they are reclaimed the same way.
35373
+ *
35500
35374
  * Deliberately NOT a member of {@link OWNER_KINDS}: the type is what stops a
35501
35375
  * new writer, and this list is only what the READ side must still recognise.
35502
35376
  */
35503
- var RETIRED_OWNER_KINDS = ["group", "event"];
35377
+ var RETIRED_OWNER_KINDS = [
35378
+ "group",
35379
+ "summary",
35380
+ "event"
35381
+ ];
35504
35382
  /** Is this an owner kind whose owning code is gone? See {@link RETIRED_OWNER_KINDS}. */
35505
35383
  function isRetiredMediaOwnerKind(value) {
35506
35384
  return RETIRED_OWNER_KINDS.includes(value);
@@ -42869,6 +42747,33 @@ var FaceSourceStats = class {
42869
42747
  };
42870
42748
  //#endregion
42871
42749
  //#region src/pipeline-analytics/pipeline/key-frame-capture.ts
42750
+ /**
42751
+ * Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
42752
+ * (Design B — one native full-frame per track at its best-detection moment).
42753
+ *
42754
+ * ## Why this exists (the missing native keyFrame)
42755
+ *
42756
+ * `keyFrame` was historically captured ONLY inside the CLIP object-embedding
42757
+ * best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
42758
+ * Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
42759
+ * a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
42760
+ * wide), so that gate was never satisfied and the native `keyFrame` was NEVER
42761
+ * produced — every stored frame stayed at the ≤640×360 detection resolution.
42762
+ *
42763
+ * The fix decouples the `keyFrame` from the clip path: it is selected on the
42764
+ * GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
42765
+ * `thumbnail`) by {@link selectKeyFrameCaptures}.
42766
+ *
42767
+ * ## How the pixels arrive, since D422
42768
+ *
42769
+ * Not by handle. At the best instant the owning node PARKS the whole frame at
42770
+ * {@link KEYFRAME_NATIVE_MAX_WIDTH} (`track-park.ts` orders it; the runner
42771
+ * refuses a ≤640 `ram-fullframe` answer at park time), and `persistKeyFrames`
42772
+ * stores that parcel verbatim as `keyFrame` (`key-frame-persist.ts`). The web
42773
+ * companion is {@link deriveKeyFrameSmall} of the same bytes — never a second
42774
+ * fetch. A retrieval keyed by a track never falls back to a handle
42775
+ * (`scripts/check-parked-frame-no-handle-fallback.ts`).
42776
+ */
42872
42777
  /** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
42873
42778
  * Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
42874
42779
  * resize, 3840 ≤ 4096); an 8K cam is bounded to 4096 (still far past the retired
@@ -42894,47 +42799,10 @@ function selectKeyFrameCaptures(targets) {
42894
42799
  return selected;
42895
42800
  }
42896
42801
  /**
42897
- * Gate for the clean best-shot native `keyFrame` (design 2026-07-21): the keyFrame
42898
- * is the track's CLEAN native best frame and MUST NEVER be persisted from the
42899
- * ≤640 RAM fallback (`tier: 'ram-fullframe'`) — that would pin the stored keyFrame
42900
- * at detection resolution. On a `ram-fullframe` tier the caller drops the write
42901
- * and the per-frame re-fire retries a real native surface later.
42902
- *
42903
- * An ABSENT tier (`undefined`, from a pre-tier runner on a mixed-version cluster)
42904
- * is ACCEPTED exactly as before the tier field existed — back-compat, never break
42905
- * mixed-version clusters. Only an explicit `ram-fullframe` is rejected.
42906
- *
42907
- * NOTE: this gates the native `keyFrame` ONLY. The 960 boxed TIMELINE tiles keep
42908
- * accepting the ≤640 fallback (an honest low-res timeline is acceptable there).
42909
- */
42910
- function keyFrameAcceptsTier(tier) {
42911
- return tier !== "ram-fullframe";
42912
- }
42913
- /**
42914
- * Encode BOTH key-frame variants from a single decoded native full frame. No box
42915
- * is drawn (the `keyFrame` is the clean best full frame); `drawBoxedFrame([])`
42916
- * is used purely as the raw-rgb → JPEG (+ optional downscale) encoder so the two
42917
- * variants share one code path. The caller has ALREADY fetched the native frame
42918
- * at {@link KEYFRAME_NATIVE_MAX_WIDTH}, so `keyFrame` re-encodes it verbatim
42919
- * (no resize) and `keyFrameSmall` downscales the same buffer to ≤960 —
42920
- * `withoutEnlargement` never upscales a sub-960 native frame.
42921
- */
42922
- async function encodeKeyFrameVariants(native) {
42923
- return {
42924
- keyFrame: await drawBoxedFrame(native.data, native.width, native.height, [], { quality: 88 }),
42925
- keyFrameSmall: await drawBoxedFrame(native.data, native.width, native.height, [], {
42926
- quality: 88,
42927
- maxWidth: 960
42928
- })
42929
- };
42930
- }
42931
- /**
42932
42802
  * The web companion from the PARKED key frame's own JPEG (D422): the parcel is
42933
42803
  * stored verbatim as `keyFrame`, and `keyFrameSmall` is this — the same bytes
42934
42804
  * downscaled to {@link KEYFRAME_SMALL_MAX_WIDTH}. One decode, one encode,
42935
42805
  * never a second fetch; `withoutEnlargement` never upscales a sub-960 frame.
42936
- * The by-handle twin ({@link encodeKeyFrameVariants}) remains for the
42937
- * summary group shot, which still fetches a native surface by handle.
42938
42806
  */
42939
42807
  async function deriveKeyFrameSmall(keyFrameJpeg) {
42940
42808
  return sharp(keyFrameJpeg).resize(960, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
@@ -43157,7 +43025,7 @@ async function mapWithConcurrency(items, limit, mapper) {
43157
43025
  * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
43158
43026
  * all-zero track scores 0.
43159
43027
  */
43160
- var WEIGHT_CONFIDENCE$1 = .3;
43028
+ var WEIGHT_CONFIDENCE = .3;
43161
43029
  var WEIGHT_CLASS = .2;
43162
43030
  var WEIGHT_DWELL = .2;
43163
43031
  var WEIGHT_PROXIMITY = .12;
@@ -43195,7 +43063,7 @@ var ANIMAL_CLASSES = new Set([
43195
43063
  "cat"
43196
43064
  ]);
43197
43065
  /** Clamp to [0,1]. */
43198
- function clamp01$2(x) {
43066
+ function clamp01$1(x) {
43199
43067
  if (Number.isNaN(x)) return 0;
43200
43068
  if (x < 0) return 0;
43201
43069
  if (x > 1) return 1;
@@ -43229,7 +43097,7 @@ function computeImportance(input) {
43229
43097
  const terms = [
43230
43098
  {
43231
43099
  reason: "confidence",
43232
- value: WEIGHT_CONFIDENCE$1 * clamp01$2(input.peakConfidence)
43100
+ value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
43233
43101
  },
43234
43102
  {
43235
43103
  reason: "class",
@@ -43237,15 +43105,15 @@ function computeImportance(input) {
43237
43105
  },
43238
43106
  {
43239
43107
  reason: "dwell",
43240
- value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
43108
+ value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
43241
43109
  },
43242
43110
  {
43243
43111
  reason: "proximity",
43244
- value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
43112
+ value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
43245
43113
  },
43246
43114
  {
43247
43115
  reason: "travel",
43248
- value: WEIGHT_TRAVEL * clamp01$2(input.totalDistance / 2)
43116
+ value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
43249
43117
  },
43250
43118
  {
43251
43119
  reason: "zone",
@@ -43264,11 +43132,11 @@ function computeImportance(input) {
43264
43132
  }
43265
43133
  const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
43266
43134
  if (input.netDisplacementFrac !== void 0 && input.pathSpanFrac !== void 0 && input.netDisplacementFrac < .02 && input.pathSpanFrac < .04 && !identified) return {
43267
- importance: clamp01$2(sum * STATIC_IMPORTANCE_MULTIPLIER),
43135
+ importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
43268
43136
  reason: best.reason
43269
43137
  };
43270
43138
  return {
43271
- importance: clamp01$2(sum),
43139
+ importance: clamp01$1(sum),
43272
43140
  reason: best.reason
43273
43141
  };
43274
43142
  }
@@ -46258,7 +46126,7 @@ var ClosedRowMediaGate = class {
46258
46126
  * it, so it can be loose enough to see the near misses.
46259
46127
  */
46260
46128
  var CO_MOVING_MIN_CONTAINMENT = .6;
46261
- function intersectionArea$4(a, b) {
46129
+ function intersectionArea$3(a, b) {
46262
46130
  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));
46263
46131
  }
46264
46132
  /**
@@ -46278,7 +46146,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
46278
46146
  if (o.className === b.className) continue;
46279
46147
  const areaO = o.bbox.w * o.bbox.h;
46280
46148
  if (areaO <= 0) continue;
46281
- const inter = intersectionArea$4(b.bbox, o.bbox);
46149
+ const inter = intersectionArea$3(b.bbox, o.bbox);
46282
46150
  if (inter <= 0) continue;
46283
46151
  const union = areaB + areaO - inter;
46284
46152
  const containment = inter / Math.min(areaB, areaO);
@@ -47278,7 +47146,7 @@ var LooseBindSampler = class {
47278
47146
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
47279
47147
  */
47280
47148
  var GROWTH_CENTROID_FACTOR = 2.3;
47281
- function intersectionArea$3(a, b) {
47149
+ function intersectionArea$2(a, b) {
47282
47150
  const x1 = Math.max(a.x, b.x);
47283
47151
  const y1 = Math.max(a.y, b.y);
47284
47152
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -47317,7 +47185,7 @@ function growthAdjacentMatch(track, det, ctx) {
47317
47185
  const larger = Math.max(trackArea, detArea);
47318
47186
  const smaller = Math.min(trackArea, detArea);
47319
47187
  if (larger / smaller < 1.5) return false;
47320
- if (intersectionArea$3(track, det) / smaller >= .5) return false;
47188
+ if (intersectionArea$2(track, det) / smaller >= .5) return false;
47321
47189
  const tc = bboxCentroid(track);
47322
47190
  const dc = bboxCentroid(det);
47323
47191
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -50441,7 +50309,6 @@ var EMPTY_CASCADE_TALLY = {
50441
50309
  faces: 0,
50442
50310
  plates: 0,
50443
50311
  embeddings: 0,
50444
- summaries: 0,
50445
50312
  bytesReclaimed: 0
50446
50313
  };
50447
50314
  /** Fold a leaf store's receipts into a running tally. */
@@ -50485,13 +50352,6 @@ function addLeafCounts(tally, counts) {
50485
50352
  bytesReclaimed
50486
50353
  };
50487
50354
  break;
50488
- case "summaries":
50489
- next = {
50490
- ...next,
50491
- summaries: next.summaries + c.rows,
50492
- bytesReclaimed
50493
- };
50494
- break;
50495
50355
  }
50496
50356
  }
50497
50357
  return next;
@@ -50505,7 +50365,6 @@ function mergeCascadeTallies(a, b) {
50505
50365
  faces: a.faces + b.faces,
50506
50366
  plates: a.plates + b.plates,
50507
50367
  embeddings: a.embeddings + b.embeddings,
50508
- summaries: a.summaries + b.summaries,
50509
50368
  bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
50510
50369
  };
50511
50370
  }
@@ -50568,7 +50427,6 @@ function absentCascadeLegs(stores) {
50568
50427
  if (!stores.faceStore) absent.push("faces");
50569
50428
  if (!stores.plateStore) absent.push("plates");
50570
50429
  if (!stores.objectEmbeddingStore) absent.push("embeddings");
50571
- if (!stores.summaryStore) absent.push("summaries");
50572
50430
  return absent;
50573
50431
  }
50574
50432
  function buildAnalyticsCascadeRegistry(stores) {
@@ -50576,7 +50434,6 @@ function buildAnalyticsCascadeRegistry(stores) {
50576
50434
  if (stores.faceStore) registry.push(stores.faceStore);
50577
50435
  if (stores.plateStore) registry.push(stores.plateStore);
50578
50436
  if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
50579
- if (stores.summaryStore) registry.push(stores.summaryStore);
50580
50437
  return registry;
50581
50438
  }
50582
50439
  /**
@@ -55097,10 +54954,6 @@ function trackColumnsFor(projection, needsPositions = false) {
55097
54954
  * whatever order the engine felt like and a cursor that cannot resume.
55098
54955
  */
55099
54956
  var RECENT_IDS_COLUMNS = ["lastSeen"];
55100
- /** How many track ids one filter pass may collect per tier. Bounded for the
55101
- * same reason every bulk read here is: an unbounded scan of this table is
55102
- * what D447 measured holding the settings thread for seconds. */
55103
- var TRACK_MATCH_SCAN_LIMIT = 2e4;
55104
54957
  /** Spreadable `columns` for a store query — omitted entirely for `full`. */
55105
54958
  function projectionArg(projection, needsPositions = false) {
55106
54959
  const columns = trackColumnsFor(projection, needsPositions);
@@ -56217,42 +56070,6 @@ var TrackStore = class {
56217
56070
  * current subject out from under it. The orphan audit's grace window would
56218
56071
  * hide this for an hour and then stop hiding it.
56219
56072
  */
56220
- /**
56221
- * Track ids whose CLASS, sub-class or instance matches any of `terms`.
56222
- *
56223
- * The summaries' filter asks its question here rather than on a copy: a
56224
- * label is a fact about the TRACK, it is decided while the track lives and
56225
- * can still be re-decided afterwards (class voting, a late face match, an
56226
- * operator enrolling a face and renaming that person). A copy stamped onto
56227
- * the summary would answer with whatever was true at one instant and never
56228
- * learn — which is the whole reason this lookup exists instead.
56229
- *
56230
- * Three queries because the filter compiler has no OR across fields, and one
56231
- * term can be any of the three tiers: `animal` is a class, `dog` is a
56232
- * sub-class, `Roberta` is an instance. Bounded, like every other bulk read
56233
- * here.
56234
- */
56235
- async trackIdsMatching(terms) {
56236
- const wanted = terms.filter((t) => t.length > 0);
56237
- if (wanted.length === 0) return [];
56238
- const ids = /* @__PURE__ */ new Set();
56239
- for (const field of [
56240
- "className",
56241
- "label",
56242
- "subLabel"
56243
- ]) {
56244
- const rows = await this.store.query.query({
56245
- collection: TRACKS_COLLECTION,
56246
- filter: {
56247
- whereIn: { [field]: [...wanted] },
56248
- limit: TRACK_MATCH_SCAN_LIMIT
56249
- },
56250
- columns: ["id"]
56251
- });
56252
- for (const row of rows) ids.add(row.id);
56253
- }
56254
- return [...ids];
56255
- }
56256
56073
  async existingTrackIds(trackIds) {
56257
56074
  const PAGE = 500;
56258
56075
  const found = /* @__PURE__ */ new Set();
@@ -65140,7 +64957,7 @@ var RetrainAnnotationStore = class {
65140
64957
  };
65141
64958
  //#endregion
65142
64959
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
65143
- function clamp01$1(v) {
64960
+ function clamp01(v) {
65144
64961
  return v < 0 ? 0 : v > 1 ? 1 : v;
65145
64962
  }
65146
64963
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -65216,10 +65033,10 @@ function deriveZoneCropRect(bbox, zone) {
65216
65033
  function deriveSubjectCropRect(bbox, padding) {
65217
65034
  if (!isUsableRect(bbox)) return null;
65218
65035
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
65219
- const cropX = clamp01$1(bbox.x - bbox.w * pad);
65220
- const cropY = clamp01$1(bbox.y - bbox.h * pad);
65221
- const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
65222
- const cropBottom = clamp01$1(bbox.y + bbox.h * (1 + pad));
65036
+ const cropX = clamp01(bbox.x - bbox.w * pad);
65037
+ const cropY = clamp01(bbox.y - bbox.h * pad);
65038
+ const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
65039
+ const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
65223
65040
  const cropW = cropRight - cropX;
65224
65041
  const cropH = cropBottom - cropY;
65225
65042
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -68762,7 +68579,7 @@ async function persistTrackKeyFrame(deps, input) {
68762
68579
  meta: {
68763
68580
  ...drop.meta,
68764
68581
  nodeId: parcel.nodeId,
68765
- error: errMsg$1(err)
68582
+ error: errMsg(err)
68766
68583
  }
68767
68584
  });
68768
68585
  }
@@ -68782,7 +68599,7 @@ async function persistTrackKeyFrame(deps, input) {
68782
68599
  keyFrameMediaId
68783
68600
  };
68784
68601
  } catch (err) {
68785
- const error = errMsg$1(err);
68602
+ const error = errMsg(err);
68786
68603
  deps.logger.warn("key-frame write failed", {
68787
68604
  ...drop,
68788
68605
  meta: {
@@ -69796,7 +69613,7 @@ function isOversizeBox(bbox, frameWidth, frameHeight) {
69796
69613
  const frame = frameWidth * frameHeight;
69797
69614
  return frame > 0 && bbox.w * bbox.h / frame >= .3;
69798
69615
  }
69799
- function intersectionArea$2(a, b) {
69616
+ function intersectionArea$1(a, b) {
69800
69617
  const ix1 = Math.max(a.x, b.x);
69801
69618
  const iy1 = Math.max(a.y, b.y);
69802
69619
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -69809,7 +69626,7 @@ function detectionSwallowsEntry(det, parked) {
69809
69626
  const detArea = det.w * det.h;
69810
69627
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
69811
69628
  if (!centroidInside(det, centroid(parked))) return false;
69812
- return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69629
+ return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69813
69630
  }
69814
69631
  /**
69815
69632
  * True when `det` is a FRAGMENT of the parked object — much smaller than the
@@ -69832,7 +69649,7 @@ function entrySwallowsDetection(parked, det) {
69832
69649
  const detArea = det.w * det.h;
69833
69650
  if (entryArea <= 0 || detArea <= 0) return false;
69834
69651
  if (detArea > entryArea / 1.35) return false;
69835
- return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69652
+ return intersectionArea$1(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69836
69653
  }
69837
69654
  function parkedEntryHoldsDetection(entry, detBbox, config) {
69838
69655
  const overlap = iou(entry.bbox, detBbox);
@@ -70773,7 +70590,7 @@ var VEHICLE_CLASSES = [
70773
70590
  "vehicle"
70774
70591
  ];
70775
70592
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
70776
- function intersectionArea$1(a, b) {
70593
+ function intersectionArea(a, b) {
70777
70594
  const left = Math.max(a.x, b.x);
70778
70595
  const right = Math.min(a.x + a.w, b.x + b.w);
70779
70596
  const top = Math.max(a.y, b.y);
@@ -70790,7 +70607,7 @@ function intersectionArea$1(a, b) {
70790
70607
  function containedFraction(person, vehicle) {
70791
70608
  const area = person.w * person.h;
70792
70609
  if (area <= 0) return 0;
70793
- return intersectionArea$1(person, vehicle) / area;
70610
+ return intersectionArea(person, vehicle) / area;
70794
70611
  }
70795
70612
  /**
70796
70613
  * Is this person box contained in any vehicle box in the same frame?
@@ -74386,895 +74203,6 @@ async function reconcileGallerySamples(deps, options) {
74386
74203
  stoppedByCeiling
74387
74204
  };
74388
74205
  }
74389
- //#endregion
74390
- //#region src/pipeline-analytics/summary/summary-membership.ts
74391
- /**
74392
- * Who belongs to a summary, and which member is its primary.
74393
- *
74394
- * ## What this corrects
74395
- *
74396
- * `openOrAbsorb` absorbed ANY track born while a summary was open on the
74397
- * camera, and `primary` went to whoever opened it. Both were measured wrong on
74398
- * the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
74399
- * summaries describing "who was here together", and a dog that walked in ahead
74400
- * of its owner outranked the person behind it.
74401
- *
74402
- * The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
74403
- * sono sempre separati come eventi sintetici di sensori e pacchi."
74404
- *
74405
- * ## Why exclusion and priority live together
74406
- *
74407
- * They are the same question asked twice — "what is a subject in a group" —
74408
- * and answering it in two files is how the two drift. A class that stops being
74409
- * a member must not keep a rank, and a class that gains a rank must be a
74410
- * member.
74411
- *
74412
- * Pure, so the surprising cases are pinned rather than discovered: the dog that
74413
- * arrives first, the tie between two people, and the member whose class is not
74414
- * known yet.
74415
- */
74416
- /** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
74417
- * except this — which is the one place it must be parsed. */
74418
- var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
74419
- /**
74420
- * Classes the operator keeps OUT of a group, by name.
74421
- *
74422
- * A delivered package is an event about a PLACE, not a member of something that
74423
- * arrived together — the operator named it alongside the sensors.
74424
- */
74425
- var NON_MEMBER_CLASSES = new Set(["package"]);
74426
- /**
74427
- * Is this track a member of a group at all?
74428
- *
74429
- * An unknown class is NOT a reason to refuse. The grouping is the point and the
74430
- * label is decoration, and nothing revisits this decision — a member dropped
74431
- * here is dropped for the life of the summary.
74432
- */
74433
- function isSummaryEligible(trackId, className) {
74434
- if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
74435
- return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
74436
- }
74437
- /**
74438
- * The operator's order: person › animal › vehicle › everything else.
74439
- *
74440
- * Higher is more important. Everything unnamed — including a class that has not
74441
- * resolved yet — sits below all three rather than above them: promoting an
74442
- * unknown would let a momentary mis-read take the group's identity, and the
74443
- * group shot with it.
74444
- */
74445
- var CLASS_RANK = new Map([
74446
- ["person", 3],
74447
- ["animal", 2],
74448
- ["vehicle", 1]
74449
- ]);
74450
- function classPriority(className) {
74451
- if (className === null || className === void 0) return 0;
74452
- return CLASS_RANK.get(className) ?? 0;
74453
- }
74454
- /**
74455
- * Which member is the primary: highest class rank, ties broken by EARLIEST
74456
- * arrival.
74457
- *
74458
- * The tie-break matters more than it looks. Two people are equal, so without it
74459
- * the primary would flip on every join between equals — and the primary is what
74460
- * a consumer reads to name the group, so it would rename itself for no reason
74461
- * an operator could see.
74462
- *
74463
- * `null` for no members, never a fabricated id.
74464
- */
74465
- function primaryTrackId(members) {
74466
- let best = null;
74467
- for (const m of members) {
74468
- if (best === null) {
74469
- best = m;
74470
- continue;
74471
- }
74472
- const rank = classPriority(m.className);
74473
- const bestRank = classPriority(best.className);
74474
- if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
74475
- }
74476
- return best?.trackId ?? null;
74477
- }
74478
- //#endregion
74479
- //#region src/pipeline-analytics/summary/summary-store.ts
74480
- /**
74481
- * @durable class=ledger owner=pipeline-analytics
74482
- * write="one row per SUMMARY — a window of co-present tracks on one camera
74483
- * group. Opened by `openOrAbsorb` when the first member arrives, updated
74484
- * in place while the window absorbs more, and finalised by `seal` when the
74485
- * window closes. Per WINDOW, never per frame or per track: the sealed
74486
- * summary is what a notification rule reads."
74487
- * retention="age-keyed on `sealedAt`, swept with the rest of the analytics
74488
- * retention. An unsealed row belongs to a window still open and is never
74489
- * swept by age — it is closed or cascaded away with its members."
74490
- */
74491
- var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
74492
- /**
74493
- * @durable class=ledger owner=pipeline-analytics
74494
- * write="one row per (summary, track) membership, written as a window
74495
- * absorbs a track. The join that lets a sealed summary name its subjects
74496
- * without re-reading the track store."
74497
- * retention="cascaded — a member dies with its summary (`delete-track-cascade`
74498
- * counts them as leaves) or with the track it names. It carries no clock of
74499
- * its own: an age sweep here could outlive or predecease the summary the
74500
- * row only means anything inside of."
74501
- */
74502
- var SUMMARY_MEMBERS_COLLECTION$1 = "pipeline-analytics:summary-members";
74503
- var SUMMARIES_COLUMNS = [
74504
- {
74505
- name: "id",
74506
- type: "TEXT",
74507
- primaryKey: true,
74508
- notNull: true
74509
- },
74510
- {
74511
- name: "deviceId",
74512
- type: "INTEGER",
74513
- notNull: true
74514
- },
74515
- {
74516
- name: "openedAt",
74517
- type: "INTEGER",
74518
- notNull: true
74519
- },
74520
- {
74521
- name: "lastActiveAt",
74522
- type: "INTEGER",
74523
- notNull: true
74524
- },
74525
- {
74526
- name: "sealedAt",
74527
- type: "INTEGER"
74528
- },
74529
- (
74530
- /** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
74531
- {
74532
- name: "openDeviceId",
74533
- type: "INTEGER"
74534
- }),
74535
- {
74536
- name: "memberCount",
74537
- type: "INTEGER",
74538
- notNull: true
74539
- }
74540
- ];
74541
- var SUMMARIES_INDEXES = [(
74542
- /**
74543
- * NOT unique since 2026-09-13. A summary is a GROUP, not a camera session, so
74544
- * one camera can hold several open at once — a family at the door while
74545
- * somebody else crosses the far end of the drive, or a group that split and
74546
- * whose halves are now two groups.
74547
- *
74548
- * The uniqueness was D359's third guarantee, and the other two survive it in
74549
- * a stricter form: an open is still idempotent, now keyed on the BIRTH
74550
- * (`deviceId`, `openedAt`, founding track) rather than on the camera and the
74551
- * millisecond — two subjects detected in one frame are two groups, not one —
74552
- * and sealing is still a conditional transition, addressed by id because
74553
- * `(openDeviceId, openedAt)` no longer names one row. Those are what stopped
74554
- * the 2026-08-24 incident of eleven sealed rows for one group; "one open per
74555
- * camera" was the part that also forbade the model the operator asked for.
74556
- *
74557
- * The index itself is create-only in this system — `declareCollection` emits
74558
- * `CREATE ... IF NOT EXISTS` and nothing anywhere drops an index — so on a
74559
- * database that already has the UNIQUE version this declaration changes
74560
- * nothing until that index is dropped by hand. Until then `openOrAbsorb`
74561
- * degrades to exactly the old behaviour (the unique violation is caught and
74562
- * the newcomer joins the winner), which is why the drop is a switch and not a
74563
- * prerequisite.
74564
- */
74565
- {
74566
- name: "summaries_open_device",
74567
- columns: ["openDeviceId"]
74568
- }), {
74569
- name: "summaries_device_opened",
74570
- columns: ["deviceId", "openedAt"]
74571
- }];
74572
- var SUMMARY_MEMBERS_COLUMNS = [
74573
- {
74574
- name: "id",
74575
- type: "TEXT",
74576
- primaryKey: true,
74577
- notNull: true
74578
- },
74579
- {
74580
- name: "summaryId",
74581
- type: "TEXT",
74582
- notNull: true
74583
- },
74584
- {
74585
- name: "trackId",
74586
- type: "TEXT",
74587
- notNull: true
74588
- },
74589
- {
74590
- name: "deviceId",
74591
- type: "INTEGER",
74592
- notNull: true
74593
- },
74594
- (
74595
- /** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
74596
- {
74597
- name: "timestamp",
74598
- type: "INTEGER",
74599
- notNull: true
74600
- }),
74601
- (
74602
- /**
74603
- * What this member IS to the summary: see {@link SummaryMemberRole}.
74604
- *
74605
- * NULLABLE, and not because a role is optional. `declareCollection` is
74606
- * additive — it ALTERs the column in — and SQLite refuses to add a `NOT NULL`
74607
- * column with no default to a table that already has rows. A default would be
74608
- * worse than null: it would stamp `primary` onto every member written before
74609
- * roles existed, including the siblings, and nothing downstream could tell
74610
- * the fabricated ones from the real ones.
74611
- */
74612
- {
74613
- name: "role",
74614
- type: "TEXT"
74615
- }),
74616
- (
74617
- /**
74618
- * The member's subject class, so "which classes were present" is answerable
74619
- * without joining `tracks`. Nullable for the same additive reason, AND
74620
- * because a birth genuinely may not know a class yet — the membership is the
74621
- * point, the label is decoration.
74622
- */
74623
- {
74624
- name: "className",
74625
- type: "TEXT"
74626
- })
74627
- ];
74628
- var SUMMARY_MEMBERS_INDEXES = [
74629
- {
74630
- name: "summary_members_summary",
74631
- columns: ["summaryId"]
74632
- },
74633
- {
74634
- name: "summary_members_track",
74635
- columns: ["trackId"]
74636
- },
74637
- (
74638
- /** The filter's own index: "which groups hold an `animal`" walks members by
74639
- * class, never summaries by hope. */
74640
- {
74641
- name: "summary_members_class",
74642
- columns: ["className"]
74643
- })
74644
- ];
74645
- /**
74646
- * A stored role, or `null`. An unrecognised value reads as `null` rather than
74647
- * being passed through: a consumer switching on this must not receive a string
74648
- * outside the union just because a row was hand-edited or written by a newer
74649
- * release.
74650
- */
74651
- function readRole(raw) {
74652
- return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
74653
- }
74654
- function readClassName(raw) {
74655
- return typeof raw === "string" && raw.length > 0 ? raw : null;
74656
- }
74657
- /**
74658
- * The key of an open, derived from the birth that founded it.
74659
- *
74660
- * The founding TRACK is part of it, and that is the whole point: the key used
74661
- * to be `(deviceId, openedAt)`, which made an open idempotent — a retry writes
74662
- * the same row — but also made two subjects detected in the SAME frame one
74663
- * group, because they are born in the same millisecond. That is the ordinary
74664
- * case on a gate camera, not a race. With the track in the key a retry still
74665
- * collapses (same birth, same track) and two births stay two groups.
74666
- */
74667
- /** The widest a JS Date can name — the open end of a half-bounded window. */
74668
- var TIME_MIN = -864e13;
74669
- var TIME_MAX = 864e13;
74670
- /**
74671
- * How many rows one paged read may materialise before the merge.
74672
- *
74673
- * The page is cut AFTER ordering, so the read must be able to serve any cursor
74674
- * position — but it must also be bounded, because an unbounded read of this
74675
- * table on a busy hub is the shape D447 measured holding the settings thread
74676
- * for seconds. Summaries are far fewer than tracks by construction (14 940
74677
- * against 30 583 on this hub), so this ceiling is generous.
74678
- */
74679
- var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
74680
- /** How many member rows one filter pass may read per tier. Bounded for the same
74681
- * reason as the scan above, and generous: members are one row per subject. */
74682
- var MEMBER_MATCH_SCAN_LIMIT = 2e4;
74683
- function summaryId(deviceId, openedAt, foundingTrackId) {
74684
- return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
74685
- }
74686
- function memberRowId(summaryIdValue, trackId) {
74687
- return `${summaryIdValue}:${trackId}`;
74688
- }
74689
- function num$1(value, fallback = 0) {
74690
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
74691
- }
74692
- function toRow$1(id, data) {
74693
- return {
74694
- id,
74695
- deviceId: num$1(data["deviceId"]),
74696
- openedAt: num$1(data["openedAt"]),
74697
- lastActiveAt: num$1(data["lastActiveAt"]),
74698
- sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
74699
- memberCount: num$1(data["memberCount"])
74700
- };
74701
- }
74702
- function isUniqueViolation(err) {
74703
- return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
74704
- }
74705
- var SummaryStore = class {
74706
- store;
74707
- logger;
74708
- onEvict;
74709
- trackIdsMatching;
74710
- constructor(deps) {
74711
- this.store = deps.store;
74712
- this.logger = deps.logger;
74713
- this.onEvict = deps.onEvict;
74714
- this.trackIdsMatching = deps.trackIdsMatching;
74715
- }
74716
- static async declare(store) {
74717
- await Promise.all([store.declareCollection.mutate({
74718
- collection: SUMMARIES_COLLECTION,
74719
- columns: [...SUMMARIES_COLUMNS],
74720
- indexes: [...SUMMARIES_INDEXES]
74721
- }), store.declareCollection.mutate({
74722
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74723
- columns: [...SUMMARY_MEMBERS_COLUMNS],
74724
- indexes: [...SUMMARY_MEMBERS_INDEXES]
74725
- })]);
74726
- }
74727
- /** The camera's OPEN summary, from the row — `null` when none. */
74728
- async findOpen(deviceId) {
74729
- const first = (await this.store.query.query({
74730
- collection: SUMMARIES_COLLECTION,
74731
- filter: {
74732
- where: { openDeviceId: deviceId },
74733
- limit: 1
74734
- }
74735
- }))[0];
74736
- return first === void 0 ? null : toRow$1(first.id, first.data);
74737
- }
74738
- /**
74739
- * Every OPEN group, or just one camera's when `deviceId` is given.
74740
- *
74741
- * Plural per camera since 2026-09-13: a summary is a group, not a camera
74742
- * session, so "the open summary of this camera" stopped being a question with
74743
- * an answer. Ordered oldest-first, so a caller with no better rule takes the
74744
- * one that has been there longest — which is what `openOrAbsorb` does, and is
74745
- * byte-for-byte what shipped when only one could exist.
74746
- */
74747
- async listOpen(deviceId) {
74748
- return (await this.store.query.query({
74749
- collection: SUMMARIES_COLLECTION,
74750
- filter: {
74751
- where: deviceId === void 0 ? { sealedAt: null } : { openDeviceId: deviceId },
74752
- limit: 1e4
74753
- }
74754
- })).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data)).toSorted((a, b) => a.openedAt - b.openedAt);
74755
- }
74756
- async membersOf(summaryIdValue) {
74757
- return (await this.store.query.query({
74758
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74759
- filter: {
74760
- where: { summaryId: summaryIdValue },
74761
- limit: 1e4
74762
- }
74763
- })).map((r) => ({
74764
- summaryId: summaryIdValue,
74765
- trackId: String(r.data["trackId"] ?? ""),
74766
- deviceId: num$1(r.data["deviceId"]),
74767
- joinedAt: num$1(r.data["timestamp"]),
74768
- role: readRole(r.data["role"]),
74769
- className: readClassName(r.data["className"])
74770
- }));
74771
- }
74772
- /**
74773
- * Put a track into the camera's open summary, opening one when there is
74774
- * none. Racing opens are settled by the store: the loser's insert hits the
74775
- * unique `openDeviceId` and is re-read as an absorb.
74776
- */
74777
- /**
74778
- * Open a NEW group for this track.
74779
- *
74780
- * The unique-violation branch is kept even though `summaries_open_device` is
74781
- * no longer UNIQUE: the id is `f(deviceId, openedAt)`, so two births in the
74782
- * SAME millisecond still collide on the primary key, and the right answer
74783
- * there is still "join the row that won" — it is the same group.
74784
- */
74785
- async openNew(input) {
74786
- const id = summaryId(input.deviceId, input.at, input.trackId);
74787
- try {
74788
- await this.store.set.mutate({
74789
- collection: SUMMARIES_COLLECTION,
74790
- key: id,
74791
- value: {
74792
- deviceId: input.deviceId,
74793
- openedAt: input.at,
74794
- lastActiveAt: input.at,
74795
- sealedAt: null,
74796
- openDeviceId: input.deviceId,
74797
- memberCount: 1
74798
- }
74799
- });
74800
- } catch (err) {
74801
- if (!isUniqueViolation(err)) throw err;
74802
- const winner = (await this.listOpen(input.deviceId)).find((s) => s.id === id);
74803
- if (winner === void 0) throw err;
74804
- return this.absorbInto(winner, input);
74805
- }
74806
- await this.addMember(id, input, "primary");
74807
- return {
74808
- id,
74809
- deviceId: input.deviceId,
74810
- openedAt: input.at,
74811
- lastActiveAt: input.at,
74812
- sealedAt: null,
74813
- memberCount: 1
74814
- };
74815
- }
74816
- /**
74817
- * Join the camera's oldest open group, or open one.
74818
- *
74819
- * The default policy, for callers with no better rule. The rule that DOES
74820
- * know better — proximity — lives with the caller that can see positions, and
74821
- * reaches the store through {@link listOpen} + {@link absorbInto} instead.
74822
- */
74823
- async openOrAbsorb(input) {
74824
- const first = (await this.listOpen(input.deviceId))[0];
74825
- return first === void 0 ? this.openNew(input) : this.absorbInto(first, input);
74826
- }
74827
- async absorbInto(summary, input) {
74828
- const members = await this.membersOf(summary.id);
74829
- if (members.some((m) => m.trackId === input.trackId)) return summary;
74830
- const joined = {
74831
- trackId: input.trackId,
74832
- className: input.className ?? null,
74833
- joinedAt: input.at
74834
- };
74835
- const primary = primaryTrackId([...members.map((m) => ({
74836
- trackId: m.trackId,
74837
- className: m.className,
74838
- joinedAt: m.joinedAt
74839
- })), joined]);
74840
- await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
74841
- if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
74842
- const memberCount = members.length + 1;
74843
- const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
74844
- await this.store.updateWhere.mutate({
74845
- collection: SUMMARIES_COLLECTION,
74846
- filter: { where: {
74847
- id: summary.id,
74848
- openDeviceId: summary.deviceId
74849
- } },
74850
- data: {
74851
- memberCount,
74852
- lastActiveAt
74853
- }
74854
- });
74855
- return {
74856
- ...summary,
74857
- memberCount,
74858
- lastActiveAt
74859
- };
74860
- }
74861
- /**
74862
- * Move a set of members out of an open group into a NEW open group.
74863
- *
74864
- * The two sides of a split. The departing members keep their join times, so
74865
- * the group that leaves opens at the moment its earliest member joined the
74866
- * old one — it existed then; what changed is that we can now tell.
74867
- *
74868
- * Order is deliberate: the new rows are written BEFORE the old ones are
74869
- * dropped. A failure between the two leaves a member in both groups, which a
74870
- * reader can see and a later pass can repair; the other order loses the
74871
- * membership, and a lost member is the one wrong this store exists to
74872
- * prevent (the same reasoning as {@link demoteOtherPrimaries}).
74873
- *
74874
- * `null` when there is nothing to do: no member matched, or ALL of them did.
74875
- * Moving everybody is not a split — it would leave an empty group behind and
74876
- * a new one holding exactly what the old one held.
74877
- */
74878
- async splitOff(summary, departingTrackIds, at) {
74879
- const members = await this.membersOf(summary.id);
74880
- const leaving = members.filter((m) => departingTrackIds.includes(m.trackId));
74881
- const staying = members.filter((m) => !departingTrackIds.includes(m.trackId));
74882
- if (leaving.length === 0 || staying.length === 0) return null;
74883
- const openedAt = Math.min(...leaving.map((m) => m.joinedAt));
74884
- const founder = leaving.find((m) => m.joinedAt === openedAt) ?? leaving[0];
74885
- const id = summaryId(summary.deviceId, openedAt, founder.trackId);
74886
- await this.store.set.mutate({
74887
- collection: SUMMARIES_COLLECTION,
74888
- key: id,
74889
- value: {
74890
- deviceId: summary.deviceId,
74891
- openedAt,
74892
- lastActiveAt: summary.lastActiveAt,
74893
- sealedAt: null,
74894
- openDeviceId: summary.deviceId,
74895
- memberCount: leaving.length
74896
- }
74897
- });
74898
- const newPrimary = primaryTrackId(leaving);
74899
- await this.store.insertMany.mutate({
74900
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74901
- records: leaving.map((m) => ({
74902
- id: memberRowId(id, m.trackId),
74903
- data: {
74904
- summaryId: id,
74905
- trackId: m.trackId,
74906
- deviceId: m.deviceId,
74907
- timestamp: m.joinedAt,
74908
- role: m.trackId === newPrimary ? "primary" : "sibling",
74909
- className: m.className
74910
- }
74911
- }))
74912
- });
74913
- await this.store.deleteWhere.mutate({
74914
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74915
- filter: {
74916
- where: { summaryId: summary.id },
74917
- whereIn: { trackId: leaving.map((m) => m.trackId) }
74918
- }
74919
- });
74920
- const keptPrimary = primaryTrackId(staying);
74921
- for (const m of staying) {
74922
- const role = m.trackId === keptPrimary ? "primary" : "sibling";
74923
- if (m.role === role) continue;
74924
- await this.store.update.mutate({
74925
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74926
- id: memberRowId(summary.id, m.trackId),
74927
- data: { role }
74928
- });
74929
- }
74930
- await this.store.updateWhere.mutate({
74931
- collection: SUMMARIES_COLLECTION,
74932
- filter: { where: {
74933
- id: summary.id,
74934
- openDeviceId: summary.deviceId
74935
- } },
74936
- data: { memberCount: staying.length }
74937
- });
74938
- this.logger.info("summary: a group walked apart", {
74939
- tags: { deviceId: summary.deviceId },
74940
- meta: {
74941
- summaryId: summary.id,
74942
- bornId: id,
74943
- left: leaving.map((m) => m.trackId),
74944
- stayed: staying.map((m) => m.trackId),
74945
- at
74946
- }
74947
- });
74948
- return {
74949
- id,
74950
- deviceId: summary.deviceId,
74951
- openedAt,
74952
- lastActiveAt: summary.lastActiveAt,
74953
- sealedAt: null,
74954
- memberCount: leaving.length
74955
- };
74956
- }
74957
- /**
74958
- * Put every OTHER member back to `sibling` after a new primary took the role.
74959
- *
74960
- * Best-effort per row and loud about a row it could not move: two primaries
74961
- * is a wrong a consumer can see, but it is smaller than a lost member, so a
74962
- * failure here never propagates to the join.
74963
- */
74964
- async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
74965
- for (const m of previous) {
74966
- if (m.trackId === primaryTrack || m.role !== "primary") continue;
74967
- try {
74968
- await this.store.update.mutate({
74969
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74970
- id: memberRowId(summaryIdValue, m.trackId),
74971
- data: { role: "sibling" }
74972
- });
74973
- } catch (err) {
74974
- this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
74975
- summaryId: summaryIdValue,
74976
- trackId: m.trackId,
74977
- error: String(err)
74978
- } });
74979
- }
74980
- }
74981
- }
74982
- async addMember(summaryIdValue, input, role) {
74983
- await this.store.insertMany.mutate({
74984
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74985
- records: [{
74986
- id: memberRowId(summaryIdValue, input.trackId),
74987
- data: {
74988
- summaryId: summaryIdValue,
74989
- trackId: input.trackId,
74990
- deviceId: input.deviceId,
74991
- timestamp: input.at,
74992
- role,
74993
- className: input.className ?? null
74994
- }
74995
- }]
74996
- });
74997
- }
74998
- /**
74999
- * Seal in place. `true` when THIS call sealed it; `false` when the row was
75000
- * already sealed (or gone) — the second sealer learns, it does not write.
75001
- */
75002
- async seal(summary, sealedAt) {
75003
- const { updated } = await this.store.updateWhere.mutate({
75004
- collection: SUMMARIES_COLLECTION,
75005
- filter: { where: {
75006
- id: summary.id,
75007
- openDeviceId: summary.deviceId
75008
- } },
75009
- data: {
75010
- sealedAt,
75011
- openDeviceId: null
75012
- }
75013
- });
75014
- if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
75015
- tags: { deviceId: summary.deviceId },
75016
- meta: {
75017
- summaryId: summary.id,
75018
- updated
75019
- }
75020
- });
75021
- return updated >= 1;
75022
- }
75023
- /**
75024
- * Track cascade leaf: a member row whose track is gone is gone too, and
75025
- * the summary's count follows. The envelope itself outlives its members
75026
- * — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
75027
- */
75028
- async deleteByTracks(trackIds) {
75029
- if (trackIds.length === 0) return [{
75030
- family: "summaries",
75031
- rows: 0,
75032
- bytes: 0
75033
- }];
75034
- const members = await this.store.query.query({
75035
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75036
- filter: {
75037
- whereIn: { trackId: [...trackIds] },
75038
- limit: 1e4
75039
- }
75040
- });
75041
- if (members.length === 0) return [{
75042
- family: "summaries",
75043
- rows: 0,
75044
- bytes: 0
75045
- }];
75046
- const { deleted } = await this.store.deleteWhere.mutate({
75047
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75048
- filter: { whereIn: { trackId: [...trackIds] } }
75049
- });
75050
- const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
75051
- for (const summaryIdValue of touched) {
75052
- const remaining = await this.membersOf(summaryIdValue);
75053
- const raw = await this.store.get.query({
75054
- collection: SUMMARIES_COLLECTION,
75055
- key: summaryIdValue
75056
- });
75057
- if (typeof raw !== "object" || raw === null) continue;
75058
- const row = raw;
75059
- await this.store.updateWhere.mutate({
75060
- collection: SUMMARIES_COLLECTION,
75061
- filter: { where: {
75062
- deviceId: num$1(row["deviceId"]),
75063
- openedAt: num$1(row["openedAt"])
75064
- } },
75065
- data: { memberCount: remaining.length }
75066
- });
75067
- }
75068
- return [{
75069
- family: "summaries",
75070
- rows: deleted,
75071
- bytes: 0
75072
- }];
75073
- }
75074
- /**
75075
- * The surface's page (D359 phase 4): a camera set, newest first by
75076
- * `openedAt`, inside an optional window, bounded. Ordered and bounded HERE
75077
- * as well as in the query, so a store that ignores `orderBy` (the fake
75078
- * does) cannot hand the surface an unordered page.
75079
- */
75080
- async listRecent(input) {
75081
- if (input.deviceIds.length === 0) return [];
75082
- const matching = await this.summaryIdsMatching(input.classes);
75083
- if (matching !== null && matching.size === 0) return [];
75084
- const limit = Math.max(1, input.limit);
75085
- return (await this.store.query.query({
75086
- collection: SUMMARIES_COLLECTION,
75087
- filter: {
75088
- whereIn: { deviceId: [...input.deviceIds] },
75089
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { openedAt: [input.since ?? 0, input.until ?? Number.MAX_SAFE_INTEGER] } } : {},
75090
- orderBy: {
75091
- field: "openedAt",
75092
- direction: "desc"
75093
- },
75094
- limit
75095
- }
75096
- })).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
75097
- }
75098
- /** One summary with its members, or `null`. */
75099
- /**
75100
- * One PAGE of summaries across a camera set, newest activity first.
75101
- *
75102
- * The same contract as `listRecentTracks` and for the same consumers: the
75103
- * reel on the cameras page and the Events gallery both ride a cursor, so a
75104
- * feed that could only answer "the last N" would make groups available in the
75105
- * timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
75106
- * ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
75107
- * second implementation of that is a second place for two feeds to disagree
75108
- * about what "newest" means.
75109
- *
75110
- * The window is on LAST ACTIVITY, not on the open: a session that started
75111
- * before the window and ran into it belongs to the window, because it
75112
- * happened there.
75113
- *
75114
- * Members for the WHOLE page come back in one read. One query per row is how
75115
- * a page of two hundred becomes two hundred round trips — the shape that made
75116
- * the old digest read two thousand tracks a camera.
75117
- */
75118
- async queryRecent(input) {
75119
- const deviceIds = [...new Set(input.deviceIds)];
75120
- if (deviceIds.length === 0) return {
75121
- summaries: [],
75122
- nextCursor: null
75123
- };
75124
- const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
75125
- const matching = await this.summaryIdsMatching(input.classes);
75126
- if (matching !== null && matching.size === 0) return {
75127
- summaries: [],
75128
- nextCursor: null
75129
- };
75130
- const ordered = (await this.store.query.query({
75131
- collection: SUMMARIES_COLLECTION,
75132
- filter: {
75133
- whereIn: { deviceId: deviceIds },
75134
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
75135
- limit: RECENT_SUMMARY_SCAN_LIMIT
75136
- }
75137
- })).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
75138
- row,
75139
- key: {
75140
- lastSeen: row.lastActiveAt,
75141
- id: row.id
75142
- }
75143
- })).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
75144
- const page = ordered.slice(0, input.limit);
75145
- const last = page.at(-1);
75146
- const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
75147
- lastSeen: last.row.lastActiveAt,
75148
- trackId: last.row.id
75149
- }) : null;
75150
- const members = await this.membersOfMany(page.map((e) => e.row.id));
75151
- return {
75152
- summaries: page.map((e) => ({
75153
- summary: e.row,
75154
- members: members.get(e.row.id) ?? []
75155
- })),
75156
- nextCursor
75157
- };
75158
- }
75159
- /**
75160
- * The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
75161
- *
75162
- * TRACKS first, then the members that name them, then the summaries. The
75163
- * label is a fact about the track and it is asked of the track — a summary
75164
- * that stored its own copy would answer with whatever was true when it
75165
- * sealed, and would never learn that a face was enrolled and a person named
75166
- * an hour later. The operator's rule: *a summary keeps the minimum
75167
- * information necessary*, and a duplicated label is not the minimum, it is a
75168
- * second truth with an expiry date.
75169
- *
75170
- * Narrows before the page is cut. Filtering the other way round — a page of
75171
- * summaries, then their members — returns a short or empty page while matches
75172
- * sit further down the list, and the operator reads that as "there were none".
75173
- */
75174
- async summaryIdsMatching(classes) {
75175
- const wanted = (classes ?? []).filter((t) => t.length > 0);
75176
- if (wanted.length === 0) return null;
75177
- const matchTracks = this.trackIdsMatching;
75178
- if (matchTracks === void 0) {
75179
- this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
75180
- return /* @__PURE__ */ new Set();
75181
- }
75182
- const trackIds = await matchTracks(wanted);
75183
- if (trackIds.length === 0) return /* @__PURE__ */ new Set();
75184
- const ids = /* @__PURE__ */ new Set();
75185
- const PAGE = 500;
75186
- for (let i = 0; i < trackIds.length; i += PAGE) {
75187
- const chunk = trackIds.slice(i, i + PAGE);
75188
- const rows = await this.store.query.query({
75189
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75190
- filter: {
75191
- whereIn: { trackId: [...chunk] },
75192
- limit: MEMBER_MATCH_SCAN_LIMIT
75193
- }
75194
- });
75195
- for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
75196
- }
75197
- ids.delete("");
75198
- return ids;
75199
- }
75200
- /** Members of many summaries, in ONE read, grouped by summary id. */
75201
- async membersOfMany(summaryIds) {
75202
- const out = /* @__PURE__ */ new Map();
75203
- if (summaryIds.length === 0) return out;
75204
- const rows = await this.store.query.query({
75205
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75206
- filter: {
75207
- whereIn: { summaryId: [...summaryIds] },
75208
- limit: 1e4
75209
- }
75210
- });
75211
- for (const r of rows) {
75212
- const summaryIdValue = String(r.data["summaryId"] ?? "");
75213
- const member = {
75214
- summaryId: summaryIdValue,
75215
- trackId: String(r.data["trackId"] ?? ""),
75216
- deviceId: num$1(r.data["deviceId"]),
75217
- joinedAt: num$1(r.data["timestamp"]),
75218
- role: readRole(r.data["role"]),
75219
- className: readClassName(r.data["className"])
75220
- };
75221
- const bucket = out.get(summaryIdValue);
75222
- if (bucket === void 0) out.set(summaryIdValue, [member]);
75223
- else bucket.push(member);
75224
- }
75225
- for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
75226
- return out;
75227
- }
75228
- async get(id) {
75229
- const raw = await this.store.get.query({
75230
- collection: SUMMARIES_COLLECTION,
75231
- key: id
75232
- });
75233
- if (typeof raw !== "object" || raw === null) return null;
75234
- return {
75235
- summary: toRow$1(id, raw),
75236
- members: await this.membersOf(id)
75237
- };
75238
- }
75239
- /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
75240
- async existingIds(ids) {
75241
- const found = /* @__PURE__ */ new Set();
75242
- await Promise.all(ids.map(async (id) => {
75243
- const raw = await this.store.get.query({
75244
- collection: SUMMARIES_COLLECTION,
75245
- key: id
75246
- });
75247
- if (typeof raw === "object" && raw !== null) found.add(id);
75248
- }));
75249
- return found;
75250
- }
75251
- /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
75252
- async evictBefore(deviceId, cutoffMs) {
75253
- const rows = await this.store.query.query({
75254
- collection: SUMMARIES_COLLECTION,
75255
- filter: {
75256
- where: { deviceId },
75257
- whereBetween: { openedAt: [0, cutoffMs - 1] },
75258
- limit: 1e4
75259
- }
75260
- });
75261
- if (rows.length === 0) return 0;
75262
- const ids = rows.map((r) => r.id);
75263
- await this.onEvict?.(ids);
75264
- await this.store.deleteWhere.mutate({
75265
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75266
- filter: { whereIn: { summaryId: ids } }
75267
- });
75268
- const { deleted } = await this.store.deleteWhere.mutate({
75269
- collection: SUMMARIES_COLLECTION,
75270
- filter: {
75271
- where: { deviceId },
75272
- whereBetween: { openedAt: [0, cutoffMs - 1] }
75273
- }
75274
- });
75275
- return deleted;
75276
- }
75277
- };
75278
74206
  /**
75279
74207
  * Where the completion marker lives.
75280
74208
  *
@@ -78548,10 +77476,6 @@ var ANALYTICS_COLLECTIONS = [
78548
77476
  collection: PLATES_COLLECTION$1,
78549
77477
  classification: derived(PLATE_COLUMNS)
78550
77478
  },
78551
- {
78552
- collection: SUMMARY_MEMBERS_COLLECTION$1,
78553
- classification: derived(SUMMARY_MEMBERS_COLUMNS)
78554
- },
78555
77479
  {
78556
77480
  collection: MEDIA_COLLECTION,
78557
77481
  classification: {
@@ -78595,13 +77519,6 @@ var ANALYTICS_COLLECTIONS = [
78595
77519
  rootId: "ops-log"
78596
77520
  }
78597
77521
  },
78598
- {
78599
- collection: SUMMARIES_COLLECTION,
78600
- classification: {
78601
- kind: "retention-root",
78602
- rootId: "summary"
78603
- }
78604
- },
78605
77522
  {
78606
77523
  collection: IDENTITIES_COLLECTION,
78607
77524
  classification: {
@@ -78790,10 +77707,6 @@ function resolveMediaOwner(row) {
78790
77707
  table: ownerKind,
78791
77708
  eventId: ownerId
78792
77709
  };
78793
- case "summary": return {
78794
- kind: "summary",
78795
- summaryId: ownerId
78796
- };
78797
77710
  case "face": return ownerId.startsWith("face-") ? {
78798
77711
  kind: "track",
78799
77712
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -78901,17 +77814,15 @@ var DEFAULT_ORPHAN_LIMITS = {
78901
77814
  function questionsFor(refs) {
78902
77815
  const trackIds = /* @__PURE__ */ new Set();
78903
77816
  const eventIds = /* @__PURE__ */ new Map();
78904
- const summaryIds = /* @__PURE__ */ new Set();
78905
77817
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
78906
77818
  else if (ref.kind === "event") {
78907
77819
  const perTable = eventIds.get(ref.table) ?? /* @__PURE__ */ new Set();
78908
77820
  perTable.add(ref.eventId);
78909
77821
  eventIds.set(ref.table, perTable);
78910
- } else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
77822
+ }
78911
77823
  return {
78912
77824
  trackIds: [...trackIds],
78913
- eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]])),
78914
- summaryIds: [...summaryIds]
77825
+ eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]]))
78915
77826
  };
78916
77827
  }
78917
77828
  /**
@@ -78955,10 +77866,9 @@ async function auditScope(scope, deps, limits, cursor) {
78955
77866
  break;
78956
77867
  }
78957
77868
  const refs = rows.map((row) => scope.resolve(row));
78958
- const { trackIds, eventIdsByTable, summaryIds } = questionsFor(refs);
77869
+ const { trackIds, eventIdsByTable } = questionsFor(refs);
78959
77870
  let liveTracks;
78960
77871
  const liveEvents = /* @__PURE__ */ new Map();
78961
- let liveSummaries;
78962
77872
  let keptMedia = /* @__PURE__ */ new Set();
78963
77873
  try {
78964
77874
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
@@ -78967,7 +77877,6 @@ async function auditScope(scope, deps, limits, cursor) {
78967
77877
  if (ids.length === 0) continue;
78968
77878
  liveEvents.set(table, await deps.liveEventIds(table, ids));
78969
77879
  }
78970
- liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
78971
77880
  } catch (err) {
78972
77881
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
78973
77882
  collection: scope.collection,
@@ -78995,7 +77904,7 @@ async function auditScope(scope, deps, limits, cursor) {
78995
77904
  tally.unresolvable += 1;
78996
77905
  continue;
78997
77906
  }
78998
- if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
77907
+ if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : false) continue;
78999
77908
  const ts = num(row.data, scope.timestampField);
79000
77909
  if (ts !== null && ts > graceFloor) {
79001
77910
  tally.withinGrace += 1;
@@ -79165,11 +78074,7 @@ var ORPHAN_AUDIT_DEFAULTS = {
79165
78074
  intervalHours: OrphanAuditIntervalHoursSchema.parse(void 0),
79166
78075
  pageSize: OrphanAuditPageSizeSchema.parse(void 0)
79167
78076
  };
79168
- var RAW_DELETE_COLLECTIONS = new Set([
79169
- "pipeline-analytics:object-events",
79170
- "pipeline-analytics:plates",
79171
- "pipeline-analytics:summary-members"
79172
- ]);
78077
+ var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
79173
78078
  function buildOrphanReclaimer(ports) {
79174
78079
  return async (collection, key) => {
79175
78080
  if (collection === "pipeline-analytics:media") {
@@ -79310,14 +78215,12 @@ async function sweepTracklessRoots(deps) {
79310
78215
  cutoffMs
79311
78216
  });
79312
78217
  const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
79313
- const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
79314
78218
  const evicted = {
79315
78219
  motion: events.motion,
79316
78220
  audio: events.audio,
79317
- sensor,
79318
- summaries
78221
+ sensor
79319
78222
  };
79320
- if (evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
78223
+ if (evicted.motion.length + evicted.audio.length + evicted.sensor > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
79321
78224
  } catch (err) {
79322
78225
  deps.onError(deviceId, err);
79323
78226
  }
@@ -80094,11 +78997,12 @@ function isFullFrameBbox(bbox) {
80094
78997
  /** Throttle for the native-crop HIT/FALLBACK metric window line. */
80095
78998
  var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
80096
78999
  /**
80097
- * Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (the
80098
- * `keyFrame`/group-shot rung, ~1–1.5 MB on a 4K camera) rather than a boxed
80099
- * timeline tile or a pinned ≤640 detection frame (tens of kB). The two belong
80100
- * in different counters or the window line says "full frames" and means two
80101
- * payload classes three orders of magnitude apart. (D470)
79000
+ * Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (~1–1.5 MB
79001
+ * on a 4K camera) rather than a boxed timeline tile or a pinned ≤640 detection
79002
+ * frame (tens of kB). The two belong in different counters or the window line
79003
+ * says "full frames" and means two payload classes three orders of magnitude
79004
+ * apart. (D470) No caller asks for that rung since D422 (the key frame) and
79005
+ * D680 (the group shot); the counter stays so the next one is seen.
80102
79006
  */
80103
79007
  var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
80104
79008
  function createNativeFrameTransport(deps) {
@@ -80175,8 +79079,8 @@ function createNativeFrameTransport(deps) {
80175
79079
  };
80176
79080
  };
80177
79081
  const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
79082
+ const getNativeFullFrameRgb = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToFullFrame));
80178
79083
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
80179
- const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
80180
79084
  const cropMetricLogger = logger.child("NativeCrop");
80181
79085
  let nativeHits = 0;
80182
79086
  let nativeFallbacks = 0;
@@ -80192,8 +79096,8 @@ function createNativeFrameTransport(deps) {
80192
79096
  /**
80193
79097
  * Publish the window once `request` has SETTLED — never before, or the line
80194
79098
  * carries a window whose own outcome has not been counted yet and throttles
80195
- * away the one that has. `finally`, so the deliberate throw of
80196
- * `fetchNativeFullFrameTiered` still reaches `persistTrackKeyFrame`.
79099
+ * away the one that has. `finally`, so the deliberate throw of the
79100
+ * full-frame fetches still reaches their callers.
80197
79101
  */
80198
79102
  const closeWindowAfter = async (request) => {
80199
79103
  try {
@@ -80250,7 +79154,7 @@ function createNativeFrameTransport(deps) {
80250
79154
  try {
80251
79155
  return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
80252
79156
  } catch (err) {
80253
- cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg$1(err) } });
79157
+ cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
80254
79158
  return null;
80255
79159
  }
80256
79160
  };
@@ -80264,7 +79168,7 @@ function createNativeFrameTransport(deps) {
80264
79168
  bumpCropMetric(false);
80265
79169
  return null;
80266
79170
  }
80267
- if (!keyFrameAcceptsTier(native.tier)) {
79171
+ if (native.tier === "ram-fullframe") {
80268
79172
  bumpCropMetric(false);
80269
79173
  cropMetricLogger.info("subject crop refused: sub-native tier (will retry, no masquerade)", {
80270
79174
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
@@ -80284,7 +79188,6 @@ function createNativeFrameTransport(deps) {
80284
79188
  return {
80285
79189
  getRemoteFrame,
80286
79190
  getNativeFullFrameRgb,
80287
- getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
80288
79191
  getNativeCropJpeg,
80289
79192
  captureCrop: createCaptureCrop({
80290
79193
  tryNativeCrop,
@@ -80298,7 +79201,7 @@ function createNativeFrameTransport(deps) {
80298
79201
  const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH).catch((err) => {
80299
79202
  logger.child("DisplayCrop").debug("fallback full-frame fetch failed — treating as a miss", { meta: {
80300
79203
  nodeId: handle.nodeId,
80301
- error: errMsg$1(err)
79204
+ error: errMsg(err)
80302
79205
  } });
80303
79206
  return null;
80304
79207
  });
@@ -80513,7 +79416,7 @@ function createParkedFrameClient(deps) {
80513
79416
  tags: { deviceId: order.deviceId },
80514
79417
  meta: {
80515
79418
  ...meta,
80516
- error: errMsg$1(err)
79419
+ error: errMsg(err)
80517
79420
  }
80518
79421
  });
80519
79422
  return false;
@@ -80593,7 +79496,7 @@ function createParkedFrameClient(deps) {
80593
79496
  trackId,
80594
79497
  kind,
80595
79498
  nodeId: known.nodeId,
80596
- error: errMsg$1(err)
79499
+ error: errMsg(err)
80597
79500
  }
80598
79501
  });
80599
79502
  return null;
@@ -80615,7 +79518,7 @@ function createParkedFrameClient(deps) {
80615
79518
  meta: {
80616
79519
  trackId,
80617
79520
  nodeId,
80618
- error: errMsg$1(err)
79521
+ error: errMsg(err)
80619
79522
  }
80620
79523
  });
80621
79524
  });
@@ -82730,7 +81633,7 @@ async function projectSensorMarkers(deps, data, timestamp) {
82730
81633
  capName: data.capName,
82731
81634
  kind: descriptor.kind,
82732
81635
  reason: "not-materialized",
82733
- ...outcome.status === "rejected" ? { error: errMsg$1(outcome.reason) } : {}
81636
+ ...outcome.status === "rejected" ? { error: errMsg(outcome.reason) } : {}
82734
81637
  });
82735
81638
  return;
82736
81639
  }
@@ -86360,947 +85263,6 @@ var RawGlobalStoreMirror = class {
86360
85263
  }
86361
85264
  };
86362
85265
  //#endregion
86363
- //#region src/pipeline-analytics/summary/group-crop.ts
86364
- /** Padding around the union, as a fraction of its own size. */
86365
- var GROUP_CROP_PADDING = .12;
86366
- /**
86367
- * Smallest crop, per axis, as a fraction of the frame.
86368
- *
86369
- * A quarter. Below that a thumbnail stops being legible at the sizes the
86370
- * gallery draws it, and the extra context costs nothing — the subjects stay the
86371
- * dominant thing in the picture at this size.
86372
- */
86373
- var MIN_CROP_FRACTION = .25;
86374
- /**
86375
- * The crop, or `null` when there is nothing to cut to — no visible member, or
86376
- * a frame with no dimensions. `null` means the caller keeps the full frame,
86377
- * which is the only honest answer when nobody could say where the group was.
86378
- */
86379
- function groupCropRect(input) {
86380
- const { frameWidth: fw, frameHeight: fh } = input;
86381
- if (fw <= 0 || fh <= 0) return null;
86382
- const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
86383
- if (boxes.length === 0) return null;
86384
- let left = Number.POSITIVE_INFINITY;
86385
- let top = Number.POSITIVE_INFINITY;
86386
- let right = Number.NEGATIVE_INFINITY;
86387
- let bottom = Number.NEGATIVE_INFINITY;
86388
- for (const b of boxes) {
86389
- left = Math.min(left, b.x);
86390
- top = Math.min(top, b.y);
86391
- right = Math.max(right, b.x + b.width);
86392
- bottom = Math.max(bottom, b.y + b.height);
86393
- }
86394
- const padX = (right - left) * GROUP_CROP_PADDING;
86395
- const padY = (bottom - top) * GROUP_CROP_PADDING;
86396
- let x0 = left - padX;
86397
- let y0 = top - padY;
86398
- let x1 = right + padX;
86399
- let y1 = bottom + padY;
86400
- const minW = fw * MIN_CROP_FRACTION;
86401
- const minH = fh * MIN_CROP_FRACTION;
86402
- if (x1 - x0 < minW) {
86403
- const cx = (x0 + x1) / 2;
86404
- x0 = cx - minW / 2;
86405
- x1 = cx + minW / 2;
86406
- }
86407
- if (y1 - y0 < minH) {
86408
- const cy = (y0 + y1) / 2;
86409
- y0 = cy - minH / 2;
86410
- y1 = cy + minH / 2;
86411
- }
86412
- const width = Math.min(x1 - x0, fw);
86413
- const height = Math.min(y1 - y0, fh);
86414
- const x = Math.max(0, Math.min(x0, fw - width));
86415
- const y = Math.max(0, Math.min(y0, fh - height));
86416
- return {
86417
- x: Math.round(x),
86418
- y: Math.round(y),
86419
- width: Math.round(width),
86420
- height: Math.round(height)
86421
- };
86422
- }
86423
- /**
86424
- * Carry a crop computed on the DETECTION frame onto the NATIVE surface.
86425
- *
86426
- * The two are different sizes by construction: detection runs on a downscaled
86427
- * frame and the group shot is fetched at up to 4096 px. Applying the rectangle
86428
- * raw would cut a corner of the picture and call it the group — the failure
86429
- * mode is silent and looks like a framing bug.
86430
- *
86431
- * Scaled per axis rather than by one factor, because the two surfaces are not
86432
- * guaranteed to share an aspect, and clamped to the native frame so rounding
86433
- * can never ask for a pixel that is not there.
86434
- */
86435
- function scaleCropToFrame(crop, from, to) {
86436
- if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
86437
- const sx = to.width / from.width;
86438
- const sy = to.height / from.height;
86439
- const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
86440
- const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
86441
- return {
86442
- x,
86443
- y,
86444
- width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
86445
- height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
86446
- };
86447
- }
86448
- //#endregion
86449
- //#region src/pipeline-analytics/summary/summary-score.ts
86450
- /** A member whose own box is covered this much by others is hidden. */
86451
- var OCCLUDED_COVER_FRACTION = .5;
86452
- /** A box covering this share of the frame scores full marks on area. */
86453
- var FULL_AREA_SHARE = .25;
86454
- var WEIGHT_CONFIDENCE = .3;
86455
- var WEIGHT_AREA = .25;
86456
- var WEIGHT_RECOGNITION = .25;
86457
- var WEIGHT_PLACEMENT = .2;
86458
- function intersectionArea(a, b) {
86459
- const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
86460
- const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
86461
- return w > 0 && h > 0 ? w * h : 0;
86462
- }
86463
- /**
86464
- * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
86465
- * capped at the own area: when two others overlap each other on top of this
86466
- * box the sum over-counts, which only ever calls a member MORE hidden, never
86467
- * less — the safe side for a shot meant to show everyone.
86468
- */
86469
- function coveredFraction(own, others) {
86470
- const area = own.width * own.height;
86471
- if (area <= 0) return 1;
86472
- const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
86473
- return Math.min(1, covered / area);
86474
- }
86475
- function clamp01(value) {
86476
- return Math.max(0, Math.min(1, value));
86477
- }
86478
- function placement(box, frameWidth, frameHeight) {
86479
- const cx = box.x + box.width / 2;
86480
- const cy = box.y + box.height / 2;
86481
- const dx = cx - frameWidth / 2;
86482
- const dy = cy - frameHeight / 2;
86483
- const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
86484
- return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
86485
- }
86486
- function memberQuality(d, frameWidth, frameHeight) {
86487
- const frameArea = frameWidth * frameHeight;
86488
- const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
86489
- return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
86490
- }
86491
- /** Score one frame for one summary. Pure; never throws on empty input. */
86492
- function scoreSummaryFrame(input) {
86493
- const present = input.detections.filter((d) => input.members.has(d.trackId));
86494
- const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
86495
- if (visible.length === 0) return {
86496
- visible: 0,
86497
- quality: 0,
86498
- crop: null
86499
- };
86500
- const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
86501
- const crop = groupCropRect({
86502
- boxes: visible.map((d) => ({
86503
- x: d.bbox.x,
86504
- y: d.bbox.y,
86505
- width: d.bbox.width,
86506
- height: d.bbox.height
86507
- })),
86508
- frameWidth: input.frameWidth,
86509
- frameHeight: input.frameHeight
86510
- });
86511
- return {
86512
- visible: visible.length,
86513
- quality,
86514
- crop
86515
- };
86516
- }
86517
- /**
86518
- * Lexicographic: more visible members wins outright; only equal counts fall
86519
- * through to quality. Positive when `a` is the better shot.
86520
- */
86521
- function compareSummaryScores(a, b) {
86522
- if (a.visible !== b.visible) return a.visible - b.visible;
86523
- return a.quality - b.quality;
86524
- }
86525
- var GroupShotTracker = class {
86526
- states = /* @__PURE__ */ new Map();
86527
- offered = 0;
86528
- materialised = 0;
86529
- byGap = 0;
86530
- byMargin = 0;
86531
- byWorse = 0;
86532
- /**
86533
- * Offer a frame. Returns `true` when it is worth materialising NOW — the
86534
- * caller's cue to fetch the pixels for THIS frame, while its handle is
86535
- * still leased.
86536
- *
86537
- * The clock is `frame.timestamp`; the caller injects no second one.
86538
- */
86539
- offer(summaryId, score, frame) {
86540
- if (score.visible === 0) return false;
86541
- this.offered += 1;
86542
- const state = this.states.get(summaryId);
86543
- if (state === void 0) {
86544
- this.states.set(summaryId, {
86545
- delivered: null,
86546
- lastAttemptAt: frame.timestamp,
86547
- unlanded: 1
86548
- });
86549
- this.materialised += 1;
86550
- return true;
86551
- }
86552
- if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
86553
- this.byGap += 1;
86554
- return false;
86555
- }
86556
- if (!this.isWorthPixels(state, score)) return false;
86557
- state.lastAttemptAt = frame.timestamp;
86558
- state.unlanded += 1;
86559
- this.materialised += 1;
86560
- return true;
86561
- }
86562
- /**
86563
- * Nothing has landed yet → keep trying (D58) until the budget is spent.
86564
- * Something has landed → beat it, by the margin on the quality tiebreak.
86565
- */
86566
- isWorthPixels(state, score) {
86567
- const delivered = state.delivered;
86568
- if (delivered === null) {
86569
- if (state.unlanded >= 4) {
86570
- this.byMargin += 1;
86571
- return false;
86572
- }
86573
- return true;
86574
- }
86575
- if (compareSummaryScores(score, delivered.score) <= 0) {
86576
- this.byWorse += 1;
86577
- return false;
86578
- }
86579
- if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
86580
- this.byMargin += 1;
86581
- return false;
86582
- }
86583
- return true;
86584
- }
86585
- /**
86586
- * A materialisation LANDED. Only this advances the baseline the next
86587
- * candidate must beat — a fetch that missed leaves the summary exactly as
86588
- * picture-less as it was, and the next frame may be worse and still worth
86589
- * taking.
86590
- */
86591
- noteLanded(summaryId, score, frame) {
86592
- const state = this.states.get(summaryId);
86593
- if (state === void 0) return;
86594
- state.delivered = {
86595
- score,
86596
- frame
86597
- };
86598
- state.unlanded = 0;
86599
- }
86600
- /** The shot on disk for this summary, as this process last saw it land. */
86601
- deliveredOf(summaryId) {
86602
- return this.states.get(summaryId)?.delivered ?? null;
86603
- }
86604
- /** A sealed or evicted summary takes no more frames; drop its state. */
86605
- forget(summaryId) {
86606
- this.states.delete(summaryId);
86607
- }
86608
- /**
86609
- * What the gate did since the last read. Shipped WITH the gate: without it
86610
- * the only evidence for a fetch that did NOT happen is hub-main's byte
86611
- * counter, which answers a different question a minute later.
86612
- */
86613
- drainCounts() {
86614
- const counts = {
86615
- offered: this.offered,
86616
- materialised: this.materialised,
86617
- byGap: this.byGap,
86618
- byMargin: this.byMargin,
86619
- byWorse: this.byWorse
86620
- };
86621
- this.offered = 0;
86622
- this.materialised = 0;
86623
- this.byGap = 0;
86624
- this.byMargin = 0;
86625
- this.byWorse = 0;
86626
- return counts;
86627
- }
86628
- get size() {
86629
- return this.states.size;
86630
- }
86631
- };
86632
- //#endregion
86633
- //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
86634
- /**
86635
- * Apply the group crop to the native surface, or hand it back untouched.
86636
- *
86637
- * Every reason to skip is a reason NOT to guess: no rectangle, no detection
86638
- * geometry to scale from, or a cropper this deployment does not have. The full
86639
- * frame is worse than a crop and far better than a wrong crop.
86640
- */
86641
- async function cropNative(native, input, deps) {
86642
- const crop = input.crop;
86643
- if (!crop || !deps.cropFrame) return native;
86644
- const fw = input.cropFrameWidth;
86645
- const fh = input.cropFrameHeight;
86646
- if (fw === void 0 || fh === void 0) return native;
86647
- const scaled = scaleCropToFrame(crop, {
86648
- width: fw,
86649
- height: fh
86650
- }, native);
86651
- if (scaled === null) return native;
86652
- if (scaled.width >= native.width && scaled.height >= native.height) return native;
86653
- try {
86654
- const cut = await deps.cropFrame(native, scaled);
86655
- if (cut === null) return native;
86656
- const expected = cut.width * cut.height * 3;
86657
- if (cut.format === "rgb" && cut.data.length !== expected) {
86658
- deps.logger.warn("group shot crop returned a surface that is not its geometry", {
86659
- tags: { deviceId: input.deviceId },
86660
- meta: {
86661
- summaryId: input.summaryId,
86662
- width: cut.width,
86663
- height: cut.height,
86664
- expectedBytes: expected,
86665
- gotBytes: cut.data.length
86666
- }
86667
- });
86668
- return native;
86669
- }
86670
- return cut;
86671
- } catch (err) {
86672
- deps.logger.warn("group shot crop failed — storing the full frame", {
86673
- tags: { deviceId: input.deviceId },
86674
- meta: {
86675
- summaryId: input.summaryId,
86676
- error: errMsg$1(err)
86677
- }
86678
- });
86679
- return native;
86680
- }
86681
- }
86682
- async function persistSummaryGroupShot(deps, input) {
86683
- const { deviceId, summaryId, frameHandle } = input;
86684
- const drop = {
86685
- tags: { deviceId },
86686
- meta: {
86687
- summaryId,
86688
- nodeId: frameHandle.nodeId
86689
- }
86690
- };
86691
- try {
86692
- const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
86693
- const native = result?.frame;
86694
- if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
86695
- deps.logger.warn("group shot native miss — the previous shot stands", {
86696
- ...drop,
86697
- meta: {
86698
- ...drop.meta,
86699
- shmId: frameHandle.shmId
86700
- }
86701
- });
86702
- return { kind: "native-miss" };
86703
- }
86704
- if (!keyFrameAcceptsTier(result.tier)) {
86705
- deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
86706
- ...drop,
86707
- meta: {
86708
- ...drop.meta,
86709
- shmId: frameHandle.shmId,
86710
- tier: result.tier
86711
- }
86712
- });
86713
- return { kind: "ram-tier" };
86714
- }
86715
- const surface = await cropNative(native, input, deps);
86716
- const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
86717
- const keyFrameMediaId = await deps.media.putReplacing({
86718
- deviceId,
86719
- ownerKind: "summary",
86720
- ownerId: summaryId,
86721
- kind: "keyFrame",
86722
- timestamp: input.timestamp,
86723
- data: keyFrame
86724
- });
86725
- try {
86726
- await deps.media.putReplacing({
86727
- deviceId,
86728
- ownerKind: "summary",
86729
- ownerId: summaryId,
86730
- kind: "keyFrameSmall",
86731
- timestamp: input.timestamp,
86732
- data: keyFrameSmall
86733
- });
86734
- } catch (err) {
86735
- deps.logger.warn("group shot web companion write failed — the native shot is stored", {
86736
- ...drop,
86737
- meta: {
86738
- ...drop.meta,
86739
- error: errMsg$1(err)
86740
- }
86741
- });
86742
- }
86743
- return {
86744
- kind: "landed",
86745
- keyFrameMediaId
86746
- };
86747
- } catch (err) {
86748
- const error = errMsg$1(err);
86749
- deps.logger.warn("group shot capture failed", {
86750
- ...drop,
86751
- meta: {
86752
- ...drop.meta,
86753
- error
86754
- }
86755
- });
86756
- return {
86757
- kind: "error",
86758
- error
86759
- };
86760
- }
86761
- }
86762
- //#endregion
86763
- //#region src/pipeline-analytics/summary/summary-composition.ts
86764
- function summaryComposition(members) {
86765
- const counts = /* @__PURE__ */ new Map();
86766
- for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
86767
- return { classes: [...counts].map(([className, count]) => ({
86768
- className,
86769
- count
86770
- })).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
86771
- }
86772
- //#endregion
86773
- //#region src/pipeline-analytics/summary/summary-member-labels.ts
86774
- function present(value) {
86775
- return typeof value === "string" && value.length > 0;
86776
- }
86777
- function withMemberLabels(members, tracks) {
86778
- const byId = new Map(tracks.map((t) => [t.trackId, t]));
86779
- return members.map((member) => {
86780
- const track = byId.get(member.trackId);
86781
- return {
86782
- ...member,
86783
- className: member.className ?? (present(track?.className) ? track.className : null),
86784
- ...present(track?.label) ? { label: track.label } : {},
86785
- ...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
86786
- };
86787
- });
86788
- }
86789
- //#endregion
86790
- //#region src/pipeline-analytics/summary/summary-proximity.ts
86791
- /**
86792
- * The measured gap, in fractions of the frame's diagonal-free normalisation
86793
- * (x by width, y by height). See the arithmetic above before moving it.
86794
- */
86795
- var DEFAULT_TOGETHER_FRACTION = .15;
86796
- /**
86797
- * Distance between two points as a fraction of the frame, or `null` when the
86798
- * frame is unknown. Each axis is normalised by its OWN dimension, so the answer
86799
- * does not change when a camera's aspect does.
86800
- */
86801
- function normalisedDistance(a, b, frame) {
86802
- if (!(frame.width > 0) || !(frame.height > 0)) return null;
86803
- const dx = (a.x - b.x) / frame.width;
86804
- const dy = (a.y - b.y) / frame.height;
86805
- return Math.sqrt(dx * dx + dy * dy);
86806
- }
86807
- //#endregion
86808
- //#region src/pipeline-analytics/summary/summary-join.ts
86809
- /**
86810
- * Which open group a newborn track joins — or none, and it opens its own.
86811
- *
86812
- * ## Why this is a separate decision from the store
86813
- *
86814
- * The store knows rows; only the caller can see where subjects ARE. Keeping the
86815
- * choice here means the store's `listOpen` + `absorbInto` stay a mechanism and
86816
- * this stays the policy, which is what let the policy change without the store
86817
- * changing underneath it.
86818
- *
86819
- * ## The two cases this exists for
86820
- *
86821
- * A summary used to be one per camera, so a subject at the far end of the drive
86822
- * was absorbed into the group at the door — a group it was never part of — and
86823
- * the halves of a group that split had nowhere to go. Both are the same need:
86824
- * a summary is a GROUP, not a camera session.
86825
- *
86826
- * ## Nearest, not first
86827
- *
86828
- * When two groups are within reach the closest wins. `listOpen` orders by
86829
- * `openedAt`, and which group opened first has nothing to do with who this
86830
- * subject is walking with — taking the first would make the answer depend on
86831
- * list order, which is the kind of tie-break that reads as random in a log.
86832
- *
86833
- * ## Knowing nothing is not a reason to take somebody in
86834
- *
86835
- * A group whose members have no position in RAM — one open across a restart —
86836
- * is skipped rather than joined. Joining on absent evidence is exactly how a
86837
- * far subject ends up in the wrong group, and nothing revisits that decision.
86838
- */
86839
- /**
86840
- * The id of the group to join, or `null` to open a new one.
86841
- *
86842
- * `null` on an unknown frame too: refusing is the recoverable direction, since
86843
- * the subject then gets its own group, while a wrong join merges two strangers.
86844
- */
86845
- function chooseGroupToJoin(subject, groups, frame, maxFraction) {
86846
- let bestId = null;
86847
- let bestDistance = Number.POSITIVE_INFINITY;
86848
- for (const g of groups) for (const m of g.memberPositions) {
86849
- const d = normalisedDistance(subject, m, frame);
86850
- if (d === null || d > maxFraction) continue;
86851
- if (d < bestDistance) {
86852
- bestDistance = d;
86853
- bestId = g.id;
86854
- }
86855
- }
86856
- return bestId;
86857
- }
86858
- //#endregion
86859
- //#region src/pipeline-analytics/summary/summary-split.ts
86860
- /**
86861
- * A group that walked apart — who leaves, and who keeps the summary.
86862
- *
86863
- * ## The case
86864
- *
86865
- * Membership is decided ONCE, at birth, by proximity (D489). That answers "who
86866
- * arrived together" and says nothing about what happens next: two people who
86867
- * meet at the gate and then go separate ways stay one group until the camera
86868
- * falls quiet, and the summary then claims they were together for the whole
86869
- * session. The operator asked for this case by name alongside the concurrent
86870
- * ones — "un gruppo che si divide" — and they are the same need, because the
86871
- * halves of a split have to be able to BE groups.
86872
- *
86873
- * ## Single linkage, deliberately
86874
- *
86875
- * A family in single file is a chain: the last of them is nowhere near the
86876
- * first, and requiring every pair to be close would tear the group apart the
86877
- * moment it grew past two. So two members are together when a chain of
86878
- * within-threshold hops connects them — the same "any, not all" rule
86879
- * {@link isTogetherWith} uses at birth, applied to the whole set.
86880
- *
86881
- * ## What an absent position means
86882
- *
86883
- * Nothing. A member whose track has closed has no live position, and that is
86884
- * not evidence it walked off — it is the ordinary end of a track. Unplaced
86885
- * members are never candidates to leave; they stay with whoever keeps the
86886
- * summary. Likewise an unknown frame yields a `null` distance, and a null is
86887
- * not "far": it produces ONE cluster, never a split (D393).
86888
- *
86889
- * ## Which side keeps the row
86890
- *
86891
- * The primary's cluster, because the summary's identity and its group shot are
86892
- * already the primary's. When the primary cannot be placed — its track closed —
86893
- * the largest cluster keeps it instead; refusing to split there would freeze
86894
- * every group the moment its primary left the frame, which is the common way
86895
- * for a group to break up in the first place.
86896
- */
86897
- /**
86898
- * Members grouped by single-linkage proximity. One cluster when nothing can be
86899
- * measured — an unknown frame is not a reason to tear a group in half.
86900
- */
86901
- function clusterByProximity(members, frame, maxFraction) {
86902
- const parent = members.map((_, i) => i);
86903
- const find = (i) => {
86904
- let root = i;
86905
- while (parent[root] !== root) root = parent[root];
86906
- return root;
86907
- };
86908
- const union = (a, b) => {
86909
- const ra = find(a);
86910
- const rb = find(b);
86911
- if (ra !== rb) parent[rb] = ra;
86912
- };
86913
- let measured = false;
86914
- for (let i = 0; i < members.length; i++) for (let j = i + 1; j < members.length; j++) {
86915
- const d = normalisedDistance(members[i].position, members[j].position, frame);
86916
- if (d === null) continue;
86917
- measured = true;
86918
- if (d <= maxFraction) union(i, j);
86919
- }
86920
- if (!measured && members.length > 1) return [members.map((m) => m.trackId)];
86921
- const byRoot = /* @__PURE__ */ new Map();
86922
- for (let i = 0; i < members.length; i++) {
86923
- const root = find(i);
86924
- const bucket = byRoot.get(root);
86925
- const trackId = members[i].trackId;
86926
- if (bucket === void 0) byRoot.set(root, [trackId]);
86927
- else bucket.push(trackId);
86928
- }
86929
- return [...byRoot.values()];
86930
- }
86931
- /**
86932
- * The departures, or `null` when the group is still whole.
86933
- *
86934
- * Order is the input's: the clusters come out in the order their first member
86935
- * appears, so a caller logging a split reads the same order twice running.
86936
- */
86937
- function planSplit(subject, frame, maxFraction) {
86938
- if (subject.placed.length < 2) return null;
86939
- const clusters = clusterByProximity(subject.placed, frame, maxFraction);
86940
- if (clusters.length < 2) return null;
86941
- const keeper = clusters.find((c) => subject.primaryTrackId !== null && c.includes(subject.primaryTrackId)) ?? largest(clusters);
86942
- return { departures: clusters.filter((c) => c !== keeper) };
86943
- }
86944
- /** The biggest cluster; ties go to the one that appeared first. */
86945
- function largest(clusters) {
86946
- let best = clusters[0];
86947
- for (const c of clusters) if (c.length > best.length) best = c;
86948
- return best;
86949
- }
86950
- function errMsg(err) {
86951
- return err instanceof Error ? err.message : String(err);
86952
- }
86953
- var SummarySealer = class {
86954
- deps;
86955
- sweepInFlight = false;
86956
- /**
86957
- * What the FRAME path may ask at frame-rate: EVERY open group of a camera,
86958
- * with the members it has seen THIS process open. Fed only by this class's
86959
- * own writes, never the authority (D224): a fresh process answers an empty
86960
- * list until a track opens, and a summary open across a restart is scored on
86961
- * the members that open after it — a weaker shot, never a wrong one.
86962
- *
86963
- * Keyed by summary id INSIDE the camera, because a camera holds as many
86964
- * groups as it has (D489). It was one entry per camera for one release, and
86965
- * that silently threw away the group shot of every group but the last to
86966
- * open — the frame path scored the newest group's members and no other.
86967
- */
86968
- openMirror = /* @__PURE__ */ new Map();
86969
- /**
86970
- * When each open group was FIRST seen divided, by summary id. Cleared the
86971
- * moment it is whole again, so a separation has to hold without interruption
86972
- * — a group that oscillates never accumulates its window.
86973
- *
86974
- * RAM only, and deliberately: after a restart a group that is still apart
86975
- * simply waits {@link SPLIT_CONFIRM_MS} again. Persisting it would buy one
86976
- * earlier split and cost a column nothing else reads.
86977
- */
86978
- apartSince = /* @__PURE__ */ new Map();
86979
- constructor(deps) {
86980
- this.deps = deps;
86981
- }
86982
- /** See {@link SUMMARY_GROUPS_ENABLED}; production never overrides it. */
86983
- get groupsEnabled() {
86984
- return this.deps.groupsEnabled ?? false;
86985
- }
86986
- /**
86987
- * The camera's open groups, oldest entry first. Empty when none is mirrored —
86988
- * which means "this process has opened none", not "the camera has none".
86989
- */
86990
- openGroupsFor(deviceId) {
86991
- const groups = this.openMirror.get(deviceId);
86992
- if (groups === void 0) return [];
86993
- return [...groups].map(([id, members]) => ({
86994
- id,
86995
- members
86996
- }));
86997
- }
86998
- /**
86999
- * Join the open group this subject is walking WITH, or open its own.
87000
- *
87001
- * Falls back to the store's own policy (the camera's oldest open group)
87002
- * whenever the evidence is missing — no birth position, no frame size, no
87003
- * member positions. That fallback is today's behaviour, so an unknowable
87004
- * case degrades to what shipped rather than to nothing.
87005
- */
87006
- async joinOrOpen(input, position) {
87007
- const frame = this.deps.frameSize(input.deviceId);
87008
- if (position === void 0 || frame === null) return await this.deps.summaries.openOrAbsorb(input);
87009
- const open = await this.deps.summaries.listOpen(input.deviceId);
87010
- if (open.length === 0) return await this.deps.summaries.openNew(input);
87011
- const groups = [];
87012
- for (const summary of open) {
87013
- const members = await this.deps.summaries.membersOf(summary.id);
87014
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
87015
- groups.push({
87016
- id: summary.id,
87017
- memberPositions: [...positions.values()]
87018
- });
87019
- }
87020
- const chosenId = chooseGroupToJoin(position, groups, frame, this.deps.togetherFraction(input.deviceId));
87021
- const chosen = open.find((s) => s.id === chosenId);
87022
- if (chosen === void 0) {
87023
- this.deps.logger.debug("summary: nobody to walk with — opening a group", {
87024
- tags: { deviceId: input.deviceId },
87025
- meta: {
87026
- trackId: input.trackId,
87027
- openGroups: open.length
87028
- }
87029
- });
87030
- return await this.deps.summaries.openNew(input);
87031
- }
87032
- return await this.deps.summaries.absorbInto(chosen, input);
87033
- }
87034
- async onTrackOpened(input) {
87035
- if (!this.groupsEnabled) return;
87036
- try {
87037
- if (!isSummaryEligible(input.trackId, input.className)) {
87038
- this.deps.logger.debug("summary: track is not a group member — left standalone", {
87039
- tags: { deviceId: input.deviceId },
87040
- meta: {
87041
- trackId: input.trackId,
87042
- className: input.className ?? null
87043
- }
87044
- });
87045
- return;
87046
- }
87047
- const at = this.deps.now();
87048
- const open = {
87049
- deviceId: input.deviceId,
87050
- trackId: input.trackId,
87051
- at,
87052
- className: input.className
87053
- };
87054
- const row = await this.joinOrOpen(open, input.position);
87055
- this.rememberMember(input.deviceId, row.id, input.trackId);
87056
- } catch (err) {
87057
- this.deps.logger.warn("summary: could not open or absorb for a track", {
87058
- tags: { deviceId: input.deviceId },
87059
- meta: {
87060
- trackId: input.trackId,
87061
- error: errMsg(err)
87062
- }
87063
- });
87064
- }
87065
- }
87066
- /** One sweep at a time (THE latch). A tick that finds one in flight returns. */
87067
- async sweep() {
87068
- if (this.sweepInFlight) return;
87069
- this.sweepInFlight = true;
87070
- try {
87071
- const mine = this.deps.analysedDeviceIds();
87072
- const open = [];
87073
- for (const deviceId of mine) open.push(...await this.deps.summaries.listOpen(deviceId));
87074
- for (const summary of open) {
87075
- const born = await this.splitIfApart(summary);
87076
- await this.sealIfDone(summary);
87077
- if (born !== null) await this.sealIfDone(born);
87078
- }
87079
- } catch (err) {
87080
- this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
87081
- } finally {
87082
- this.sweepInFlight = false;
87083
- }
87084
- }
87085
- /**
87086
- * Split an open group whose members have walked apart and STAYED apart.
87087
- *
87088
- * Returns the last group born, so the sweep can consider it for sealing in
87089
- * the same pass — a group that divided as everybody left would otherwise sit
87090
- * open until the next tick with nothing alive in it.
87091
- *
87092
- * Every failure here is swallowed into a log: a split is an improvement to a
87093
- * record that is already correct about WHO was present, and losing it is
87094
- * never worth losing the seal that follows.
87095
- */
87096
- async splitIfApart(summary) {
87097
- const { id: summaryId, deviceId } = summary;
87098
- try {
87099
- const frame = this.deps.frameSize(deviceId);
87100
- if (frame === null) {
87101
- this.apartSince.delete(summaryId);
87102
- return null;
87103
- }
87104
- const members = await this.deps.summaries.membersOf(summaryId);
87105
- if (members.length < 2) {
87106
- this.apartSince.delete(summaryId);
87107
- return null;
87108
- }
87109
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
87110
- const placed = [];
87111
- for (const m of members) {
87112
- const position = positions.get(m.trackId);
87113
- if (position !== void 0) placed.push({
87114
- trackId: m.trackId,
87115
- position
87116
- });
87117
- }
87118
- const plan = planSplit({
87119
- placed,
87120
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null
87121
- }, frame, this.deps.togetherFraction(deviceId));
87122
- if (plan === null) {
87123
- this.apartSince.delete(summaryId);
87124
- return null;
87125
- }
87126
- const firstSeen = this.apartSince.get(summaryId);
87127
- const now = this.deps.now();
87128
- if (firstSeen === void 0) {
87129
- this.apartSince.set(summaryId, now);
87130
- return null;
87131
- }
87132
- if (now - firstSeen < 0) return null;
87133
- let last = null;
87134
- let row = summary;
87135
- for (const departing of plan.departures) {
87136
- const born = await this.deps.summaries.splitOff(row, departing, now);
87137
- if (born === null) continue;
87138
- this.moveMirrorMembers(deviceId, summaryId, born.id, departing);
87139
- row = {
87140
- ...row,
87141
- memberCount: row.memberCount - departing.length
87142
- };
87143
- last = born;
87144
- }
87145
- this.apartSince.delete(summaryId);
87146
- return last;
87147
- } catch (err) {
87148
- this.deps.logger.warn("summary: could not divide a group that walked apart", {
87149
- tags: { deviceId },
87150
- meta: {
87151
- summaryId,
87152
- error: errMsg(err)
87153
- }
87154
- });
87155
- return null;
87156
- }
87157
- }
87158
- /**
87159
- * Is this group finished?
87160
- *
87161
- * Both questions are about THIS group's members, not about the camera. A
87162
- * camera holds as many groups as it has (D489), so a camera-wide gate means
87163
- * no group seals until every subject on the camera has gone — and on a busy
87164
- * camera they never all go at once. Measured: eight summaries open on one
87165
- * camera over seventeen minutes, none sealed, each new subject opening a
87166
- * ninth because a group whose members have closed has no positions left for
87167
- * the proximity rule to judge.
87168
- *
87169
- * `null` means "could not find out", and the caller treats that as NOT done —
87170
- * a read that failed must never seal a live session (D49).
87171
- */
87172
- async isGroupFinished(summary, members) {
87173
- const { id: summaryId, deviceId } = summary;
87174
- let openTracks;
87175
- try {
87176
- openTracks = await this.deps.openTrackIds(deviceId);
87177
- } catch (err) {
87178
- this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
87179
- tags: { deviceId },
87180
- meta: {
87181
- summaryId,
87182
- error: errMsg(err)
87183
- }
87184
- });
87185
- return null;
87186
- }
87187
- const stillOpen = new Set(openTracks);
87188
- if (members.some((m) => stillOpen.has(m))) return false;
87189
- let parked;
87190
- try {
87191
- parked = await this.deps.parkedTrackIds(deviceId);
87192
- } catch (err) {
87193
- this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
87194
- tags: { deviceId },
87195
- meta: {
87196
- summaryId,
87197
- error: errMsg(err)
87198
- }
87199
- });
87200
- return null;
87201
- }
87202
- return !members.some((m) => parked.has(m));
87203
- }
87204
- async sealIfDone(summary) {
87205
- const { id: summaryId, deviceId } = summary;
87206
- let members;
87207
- try {
87208
- members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
87209
- } catch (err) {
87210
- this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
87211
- tags: { deviceId },
87212
- meta: {
87213
- summaryId,
87214
- error: errMsg(err)
87215
- }
87216
- });
87217
- return;
87218
- }
87219
- if (await this.isGroupFinished(summary, members) !== true) return;
87220
- const sealedAt = this.deps.now();
87221
- try {
87222
- if (await this.deps.summaries.seal(summary, sealedAt)) {
87223
- const members = this.forgetGroup(deviceId, summaryId);
87224
- this.deps.onSealed?.(summaryId, members);
87225
- this.deps.logger.info("summary sealed", {
87226
- tags: { deviceId },
87227
- meta: {
87228
- summaryId,
87229
- sealedAt
87230
- }
87231
- });
87232
- }
87233
- } catch (err) {
87234
- this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
87235
- tags: { deviceId },
87236
- meta: {
87237
- summaryId,
87238
- error: errMsg(err)
87239
- }
87240
- });
87241
- }
87242
- }
87243
- rememberMember(deviceId, summaryIdValue, trackId) {
87244
- let groups = this.openMirror.get(deviceId);
87245
- if (groups === void 0) {
87246
- groups = /* @__PURE__ */ new Map();
87247
- this.openMirror.set(deviceId, groups);
87248
- }
87249
- const members = groups.get(summaryIdValue);
87250
- if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
87251
- else members.add(trackId);
87252
- }
87253
- /** Follows a split in the mirror, so the frame path scores each half on its
87254
- * own members from the very next frame. */
87255
- moveMirrorMembers(deviceId, fromId, toId, trackIds) {
87256
- const groups = this.openMirror.get(deviceId);
87257
- const from = groups?.get(fromId);
87258
- for (const trackId of trackIds) {
87259
- from?.delete(trackId);
87260
- this.rememberMember(deviceId, toId, trackId);
87261
- }
87262
- if (from !== void 0 && from.size === 0) groups?.delete(fromId);
87263
- }
87264
- /** Drops one group from the mirror and returns the members it held. */
87265
- forgetGroup(deviceId, summaryIdValue) {
87266
- this.apartSince.delete(summaryIdValue);
87267
- const groups = this.openMirror.get(deviceId);
87268
- const members = groups?.get(summaryIdValue) ?? /* @__PURE__ */ new Set();
87269
- groups?.delete(summaryIdValue);
87270
- if (groups !== void 0 && groups.size === 0) this.openMirror.delete(deviceId);
87271
- return members;
87272
- }
87273
- };
87274
- //#endregion
87275
- //#region src/pipeline-analytics/summary-settings.ts
87276
- /**
87277
- * Per-device summary (group) settings. Cascade: a per-device override on top of
87278
- * the global default, resolved per field — an invalid or missing value falls
87279
- * back to its default and parsing never throws. Mirrors `stationary-settings` /
87280
- * `media-settings`.
87281
- *
87282
- * The default IS the constant the proximity rule shipped with, imported and not
87283
- * copied, so an unset value and a reset value both resolve to exactly today's
87284
- * behaviour. It is per camera because a threshold belongs to whoever generates
87285
- * the event: a gate camera framing three metres of path and a drive camera
87286
- * framing twenty disagree about what "walking together" looks like in a
87287
- * fraction of the frame, and the fleet measurement behind the default is a
87288
- * median across both kinds.
87289
- */
87290
- var SummarySettingsSchema = object({
87291
- /**
87292
- * How close two subjects must be to count as one group, as a fraction of the
87293
- * frame. The floor is not zero: zero means "only at the identical pixel",
87294
- * which ends grouping on that camera while looking like a tuning choice.
87295
- * See `summary/summary-proximity.ts` for where the default came from.
87296
- */
87297
- summaryTogetherFraction: number().min(.02).max(1).default(DEFAULT_TOGETHER_FRACTION) });
87298
- var SUMMARY_DEFAULTS = { togetherFraction: SummarySettingsSchema.parse({}).summaryTogetherFraction };
87299
- function resolveSummarySettings(raw) {
87300
- const parsed = SummarySettingsSchema.shape.summaryTogetherFraction.safeParse(raw["summaryTogetherFraction"]);
87301
- return { togetherFraction: parsed.success ? parsed.data : SUMMARY_DEFAULTS.togetherFraction };
87302
- }
87303
- //#endregion
87304
85266
  //#region src/pipeline-analytics/track-retention-sweep.ts
87305
85267
  /**
87306
85268
  * Sweep every device with persisted tracks: skip retention-disabled devices,
@@ -88306,8 +86268,6 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
88306
86268
  * the others) — a longer drain would be killed mid-way and log nothing.
88307
86269
  */
88308
86270
  var SENSOR_HANDLER_DRAIN_MS = 2700;
88309
- /** Throttle for the group-shot gate's window line (D470). */
88310
- var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
88311
86271
  /**
88312
86272
  * How often the addon asks "is a gallery sample outside the cluster face
88313
86273
  * space?". A flip is rare and the check is two cheap reads — a minute keeps
@@ -88585,21 +86545,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88585
86545
  storageMigrationLeaseId = null;
88586
86546
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
88587
86547
  sensorEventStore = null;
88588
- /** The per-camera session envelope (D359). Bound with the other stores at boot. */
88589
- summaryStore = null;
88590
- /** The ONLY writer of a summary's lifecycle (D359). */
88591
- summarySealer = null;
88592
- /** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
88593
- groupShots = new GroupShotTracker();
88594
- /** Throttle for the group-shot gate's window line (D470). */
88595
- lastGroupShotMetricAt = 0;
88596
- /**
88597
- * Tracks a gallery match has named, for the group-shot score's recognition
88598
- * term. "Recognised so far" rather than "at that instant": the match arrives
88599
- * on the detail plane, after the frame; the frames that follow carry it.
88600
- * Pruned when the track's summary seals, so it is bounded by open summaries.
88601
- */
88602
- recognizedTrackIds = /* @__PURE__ */ new Set();
88603
86548
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
88604
86549
  * so the DeviceStateChanged handler stays cheap. */
88605
86550
  linkedCamerasCache = null;
@@ -88880,7 +86825,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88880
86825
  };
88881
86826
  this.ctx.logger.info("nc-action data-plane served", { meta: { served: served !== null } });
88882
86827
  } catch (err) {
88883
- this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg$1(err) } });
86828
+ this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg(err) } });
88884
86829
  }
88885
86830
  }
88886
86831
  /** Mints a button's callback URL once the action plane is served; null until
@@ -88914,7 +86859,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88914
86859
  await store.sweep();
88915
86860
  await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
88916
86861
  } catch (err) {
88917
- this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg$1(err) } });
86862
+ this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg(err) } });
88918
86863
  }
88919
86864
  };
88920
86865
  run();
@@ -88984,7 +86929,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88984
86929
  retentionDays: store.retentionMs === null ? "forever" : Math.round(store.retentionMs / 864e5)
88985
86930
  } });
88986
86931
  } catch (err) {
88987
- this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg$1(err) } });
86932
+ this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
88988
86933
  }
88989
86934
  }
88990
86935
  /** Public base URL for event thumbnails: `/addon/<addonId>/event-media`.
@@ -89121,11 +87066,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89121
87066
  * the partition + promotion + sweep call sites — an operator change takes
89122
87067
  * effect within one SETTINGS_CACHE_TTL_MS tick without an addon restart. */
89123
87068
  stationarySettingsCache = this.settingsCaches.registerDeviceCache("stationary", SETTINGS_CACHE_TTL_MS);
89124
- /** Per-device summary (group) settings — the proximity threshold. Read
89125
- * SYNCHRONOUSLY by the sealer's birth and split paths, so it is warmed on
89126
- * the frame path like the stationary one and a cold miss answers the
89127
- * measured default. */
89128
- summarySettingsCache = this.settingsCaches.registerDeviceCache("summary", SETTINGS_CACHE_TTL_MS);
89129
87069
  /** Per-device STILLNESS SCALE cache — the measured "how slow is settled" bar
89130
87070
  * (`stillness-settings.ts`). Read once per FrameProcessor build, so this is
89131
87071
  * a cold-path cache; it exists so a calibration applied through
@@ -89255,9 +87195,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89255
87195
  * line/min/device instead of one per §5 cadence tick, see media-capture-log.ts). */
89256
87196
  mediaCaptureLog = new MediaCaptureLogAggregator();
89257
87197
  /** Per-device hit/miss window for the native `keyFrame` path (2026-08-06).
89258
- * Before it there was NO metric here at all — `fetchNativeFullFrameTiered`
89259
- * never touched `bumpCropMetric` — so whether a key-frame fix worked could
89260
- * only be answered by hand-counting Loki lines. See `key-frame-metrics.ts`. */
87198
+ * Before it there was NO metric here at all — the by-handle key-frame fetch
87199
+ * in `runtime/native-frame-transport.ts` (retired by D422) never touched
87200
+ * `bumpCropMetric` — so whether a key-frame fix worked could only be
87201
+ * answered by hand-counting Loki lines. See `key-frame-metrics.ts`. */
89261
87202
  keyFrameCaptureLog = new KeyFrameCaptureLog();
89262
87203
  /** The registry's own liveness answer, mirrored off `ClusterTopologySnapshot`,
89263
87204
  * so a capture is never dispatched at a node the hub already called dead.
@@ -89287,7 +87228,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89287
87228
  tags: { deviceId },
89288
87229
  meta: {
89289
87230
  deviceId,
89290
- error: errMsg$1(error)
87231
+ error: errMsg(error)
89291
87232
  }
89292
87233
  });
89293
87234
  }
@@ -89316,7 +87257,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89316
87257
  tags: { deviceId },
89317
87258
  meta: {
89318
87259
  deviceId,
89319
- error: errMsg$1(error)
87260
+ error: errMsg(error)
89320
87261
  }
89321
87262
  });
89322
87263
  }
@@ -89391,12 +87332,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89391
87332
  });
89392
87333
  }
89393
87334
  captureCrop = null;
89394
- /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
89395
- * to the owning runner, PLUS the source `tier`. Captured in the async init
89396
- * block so `persistKeyFrames` can fetch the native full frame for the
89397
- * `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
89398
- * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
89399
- getNativeKeyFrameRgb = null;
89400
87335
  shuttingDown = false;
89401
87336
  /**
89402
87337
  * Sensor handlers the bus dispatched under `void` that have not finished.
@@ -89896,7 +87831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89896
87831
  logger,
89897
87832
  noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
89898
87833
  });
89899
- this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
89900
87834
  this.parkedFrames = createParkedFrameClient({
89901
87835
  api: api.pipelineRunner,
89902
87836
  logger: logger.child("ParkedFrames"),
@@ -90109,7 +88043,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90109
88043
  */
90110
88044
  armAlarmPanelDeclaration(api) {
90111
88045
  this.declareAlarmPanel(api).catch((err) => {
90112
- 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) } });
88046
+ this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg(err) } });
90113
88047
  });
90114
88048
  }
90115
88049
  async declareAlarmPanel(api) {
@@ -90233,7 +88167,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90233
88167
  MediaStore.declare(api.settingsStore),
90234
88168
  EventStore.declare(api.settingsStore),
90235
88169
  SensorEventStore.declare(api.settingsStore),
90236
- SummaryStore.declare(api.settingsStore),
90237
88170
  IdentityStore.declare(api.settingsStore),
90238
88171
  FaceStore.declare(api.settingsStore),
90239
88172
  PlateStore.declare(api.settingsStore),
@@ -90262,7 +88195,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90262
88195
  logger: this.ctx.logger.child("viewer-settings")
90263
88196
  });
90264
88197
  } catch (err) {
90265
- this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg$1(err) } });
88198
+ this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg(err) } });
90266
88199
  }
90267
88200
  runLabelTierMigration({
90268
88201
  store: api.settingsStore,
@@ -90360,7 +88293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90360
88293
  try {
90361
88294
  locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
90362
88295
  } catch (err) {
90363
- logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg$1(err) } });
88296
+ logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
90364
88297
  this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
90365
88298
  return DEFAULT_MEDIA_LOCATION;
90366
88299
  }
@@ -90541,49 +88474,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90541
88474
  store: api.settingsStore,
90542
88475
  logger: logger.child("SensorEventStore")
90543
88476
  });
90544
- this.summaryStore = new SummaryStore({
90545
- trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
90546
- store: api.settingsStore,
90547
- logger: this.ctx.logger.child("summary"),
90548
- onEvict: async (ids) => {
90549
- await this.mediaStore?.deleteForOwner("summary", ids);
90550
- }
90551
- });
90552
- this.summarySealer = new SummarySealer({
90553
- summaries: this.summaryStore,
90554
- analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
90555
- openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
90556
- parkedTrackIds: async (deviceId) => {
90557
- const ids = /* @__PURE__ */ new Set();
90558
- for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
90559
- return ids;
90560
- },
90561
- now: () => Date.now(),
90562
- logger: this.ctx.logger.child("summary-sealer"),
90563
- frameSize: (deviceId) => {
90564
- const d = this.lastFrameDimsByDevice.get(deviceId);
90565
- return d === void 0 ? null : {
90566
- width: d.w,
90567
- height: d.h
90568
- };
90569
- },
90570
- latestPositions: async (trackIds) => {
90571
- const out = /* @__PURE__ */ new Map();
90572
- for (const id of trackIds) {
90573
- const last = (this.trackStore?.peekActive(id))?.positions.at(-1);
90574
- if (last !== void 0) out.set(id, {
90575
- x: last.x,
90576
- y: last.y
90577
- });
90578
- }
90579
- return out;
90580
- },
90581
- togetherFraction: (deviceId) => this.summarySettingsFromCache(deviceId).togetherFraction,
90582
- onSealed: (summaryId, members) => {
90583
- this.groupShots.forget(summaryId);
90584
- for (const trackId of members) this.recognizedTrackIds.delete(trackId);
90585
- }
90586
- });
90587
88477
  this.packageDropDetector = new PackageDropDetector({
90588
88478
  events: eventStore,
90589
88479
  emit: {
@@ -90621,7 +88511,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90621
88511
  onPersisted: (ev, phase) => this.notificationCenter?.onPackageEventPersisted(ev, phase),
90622
88512
  onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
90623
88513
  scope,
90624
- error: errMsg$1(err)
88514
+ error: errMsg(err)
90625
88515
  } })
90626
88516
  });
90627
88517
  this.linkedCamerasCache = new LinkedCamerasCache({
@@ -90637,7 +88527,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90637
88527
  ...cameraId !== void 0 ? { tags: { deviceId: cameraId } } : {},
90638
88528
  meta: {
90639
88529
  scope,
90640
- error: errMsg$1(err)
88530
+ error: errMsg(err)
90641
88531
  }
90642
88532
  })
90643
88533
  });
@@ -90649,7 +88539,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90649
88539
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90650
88540
  meta: {
90651
88541
  scope,
90652
- error: errMsg$1(err)
88542
+ error: errMsg(err)
90653
88543
  }
90654
88544
  })
90655
88545
  });
@@ -90662,7 +88552,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90662
88552
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90663
88553
  meta: {
90664
88554
  scope,
90665
- error: errMsg$1(err)
88555
+ error: errMsg(err)
90666
88556
  }
90667
88557
  })
90668
88558
  });
@@ -90672,7 +88562,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90672
88562
  logger: logger.child("AudioMarkers"),
90673
88563
  onError: (scope, err) => logger.warn("audio marker error", { meta: {
90674
88564
  scope,
90675
- error: errMsg$1(err)
88565
+ error: errMsg(err)
90676
88566
  } })
90677
88567
  });
90678
88568
  const galleryVectorStore = api.vectorStore;
@@ -90783,7 +88673,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90783
88673
  score: face.matchScore ?? face.confidence,
90784
88674
  confidence: face.confidence,
90785
88675
  ...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
90786
- }, face.identityId !== void 0 ? "identified" : "evidence"),
88676
+ }),
90787
88677
  onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
90788
88678
  faceSourceFrames: new FaceSourceFrameKeeper({
90789
88679
  port: createFaceSourceParkPort({
@@ -91029,7 +88919,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91029
88919
  } catch (err) {
91030
88920
  this.ctx.logger.debug("zone ownership read failed", {
91031
88921
  tags: { deviceId },
91032
- meta: { error: errMsg$1(err) }
88922
+ meta: { error: errMsg(err) }
91033
88923
  });
91034
88924
  }
91035
88925
  return out;
@@ -91089,7 +88979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91089
88979
  } catch (err) {
91090
88980
  return {
91091
88981
  ok: false,
91092
- error: errMsg$1(err)
88982
+ error: errMsg(err)
91093
88983
  };
91094
88984
  }
91095
88985
  },
@@ -91109,7 +88999,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91109
88999
  } catch (err) {
91110
89000
  return {
91111
89001
  ok: false,
91112
- error: errMsg$1(err)
89002
+ error: errMsg(err)
91113
89003
  };
91114
89004
  }
91115
89005
  },
@@ -91311,30 +89201,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91311
89201
  confirmed: t.observations !== void 0 && t.observations >= minHits
91312
89202
  }));
91313
89203
  },
91314
- listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
91315
- const summaryStore = this.summaryStore;
91316
- if (!summaryStore) return [];
91317
- const rows = await summaryStore.listRecent({
91318
- deviceIds: [deviceId],
91319
- since: sinceMs,
91320
- until: untilMs,
91321
- limit: 500
91322
- });
91323
- const out = [];
91324
- for (const row of rows) {
91325
- const members = await summaryStore.membersOf(row.id);
91326
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
91327
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
91328
- out.push({
91329
- summaryId: row.id,
91330
- deviceId: row.deviceId,
91331
- memberTrackIds: members.map((m) => m.trackId),
91332
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
91333
- groupShotMediaId: shot?.mediaId ?? null
91334
- });
91335
- }
91336
- return out;
91337
- },
91338
89204
  listMediaInfo: async (id) => {
91339
89205
  return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
91340
89206
  mediaId: r.mediaId,
@@ -91561,7 +89427,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91561
89427
  } catch (err) {
91562
89428
  this.ctx.logger.warn("event-media resolve failed", { meta: {
91563
89429
  address,
91564
- error: errMsg$1(err)
89430
+ error: errMsg(err)
91565
89431
  } });
91566
89432
  throw err;
91567
89433
  }
@@ -91582,7 +89448,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91582
89448
  access: planeAccess("event-media")
91583
89449
  } });
91584
89450
  } catch (err) {
91585
- this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89451
+ this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg(err) } });
91586
89452
  }
91587
89453
  }
91588
89454
  /**
@@ -91608,7 +89474,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91608
89474
  handler
91609
89475
  }) ?? null;
91610
89476
  } catch (err) {
91611
- this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89477
+ this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91612
89478
  }
91613
89479
  }
91614
89480
  /**
@@ -91900,7 +89766,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91900
89766
  handler
91901
89767
  }) ?? null;
91902
89768
  } catch (err) {
91903
- this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89769
+ this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91904
89770
  }
91905
89771
  }
91906
89772
  /**
@@ -91929,7 +89795,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91929
89795
  handler
91930
89796
  }) ?? null;
91931
89797
  } catch (err) {
91932
- this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89798
+ this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91933
89799
  }
91934
89800
  }
91935
89801
  /**
@@ -91994,7 +89860,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91994
89860
  tags: { deviceId },
91995
89861
  meta: {
91996
89862
  deviceId,
91997
- error: errMsg$1(err)
89863
+ error: errMsg(err)
91998
89864
  }
91999
89865
  });
92000
89866
  return null;
@@ -92113,7 +89979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92113
89979
  }, RETENTION_SWEEP_INTERVAL_MS);
92114
89980
  this.ltsFlushTimer = setInterval(() => {
92115
89981
  this.lts?.flushDue().catch((err) => {
92116
- this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg$1(err) } });
89982
+ this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg(err) } });
92117
89983
  });
92118
89984
  }, LTS_FLUSH_INTERVAL_MS);
92119
89985
  this.startOrphanAuditTimer();
@@ -92134,7 +90000,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92134
90000
  } catch (err) {
92135
90001
  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: {
92136
90002
  location: ARTIFACTS_MEDIA_LOCATION,
92137
- error: errMsg$1(err),
90003
+ error: errMsg(err),
92138
90004
  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)."
92139
90005
  } });
92140
90006
  return;
@@ -92151,7 +90017,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92151
90017
  hint: "the pixels are gone for good; re-enrol these people from a photo"
92152
90018
  } });
92153
90019
  } catch (err) {
92154
- this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg$1(err) } });
90020
+ this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg(err) } });
92155
90021
  }
92156
90022
  try {
92157
90023
  const identityStore = this.identityStore;
@@ -92183,7 +90049,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92183
90049
  maxReclaimPerRun: 16
92184
90050
  })).reclaimed > 0) await this.faceRecognizer?.refreshGallery();
92185
90051
  } catch (err) {
92186
- this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg$1(err) } });
90052
+ this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg(err) } });
92187
90053
  }
92188
90054
  }
92189
90055
  /** Schedule the orphan audit on the operator's interval (hours). */
@@ -92325,7 +90191,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92325
90191
  engine.start();
92326
90192
  this.sceneEngine = engine;
92327
90193
  this.sceneReloadTimer = setInterval(() => {
92328
- store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg$1(err) } }));
90194
+ store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg(err) } }));
92329
90195
  }, SCENE_RELOAD_INTERVAL_MS);
92330
90196
  this.sceneReloadTimer.unref?.();
92331
90197
  }
@@ -92369,7 +90235,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92369
90235
  lng: status.location.longitude
92370
90236
  };
92371
90237
  } catch (err) {
92372
- this.ctx.logger.debug("site location read failed", { meta: { error: errMsg$1(err) } });
90238
+ this.ctx.logger.debug("site location read failed", { meta: { error: errMsg(err) } });
92373
90239
  }
92374
90240
  this.siteCoordsCache = {
92375
90241
  value,
@@ -92388,7 +90254,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92388
90254
  } catch (err) {
92389
90255
  this.ctx.logger.debug("scene slice mirror failed", {
92390
90256
  tags: { deviceId },
92391
- meta: { error: errMsg$1(err) }
90257
+ meta: { error: errMsg(err) }
92392
90258
  });
92393
90259
  }
92394
90260
  }
@@ -92758,7 +90624,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92758
90624
  if (planeRunsStationary(source) && this.stationaryRegistry) {
92759
90625
  await this.resolveDeviceStationarySettings(deviceId);
92760
90626
  await this.resolveDevicePackageDropSettings(deviceId);
92761
- await this.resolveDeviceSummarySettings(deviceId);
92762
90627
  }
92763
90628
  const processor = await this.getOrCreateProcessor(deviceId, source);
92764
90629
  const proxy = await this.ensureProxy(deviceId);
@@ -93134,15 +90999,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93134
90999
  className: t.className
93135
91000
  }
93136
91001
  });
93137
- this.summarySealer?.onTrackOpened({
93138
- deviceId,
93139
- trackId: id,
93140
- className: t.className,
93141
- position: {
93142
- x: t.bbox.x + t.bbox.w / 2,
93143
- y: t.bbox.y + t.bbox.h / 2
93144
- }
93145
- });
93146
91002
  const startPayload = buildTrackLifecyclePayload({
93147
91003
  deviceId,
93148
91004
  trackId: id,
@@ -93378,7 +91234,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93378
91234
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
93379
91235
  else plateCrops += 1;
93380
91236
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
93381
- this.offerGroupShot(deviceId, result, frameHandle);
93382
91237
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
93383
91238
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
93384
91239
  const rasterFallbackCandidates = [];
@@ -93742,7 +91597,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93742
91597
  } catch (err) {
93743
91598
  this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
93744
91599
  nodeId,
93745
- error: errMsg$1(err)
91600
+ error: errMsg(err)
93746
91601
  } });
93747
91602
  }
93748
91603
  this.confirmationDetectorStepByNode.set(nodeId, {
@@ -93786,7 +91641,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93786
91641
  } catch (err) {
93787
91642
  this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
93788
91643
  tags: { deviceId },
93789
- meta: { error: errMsg$1(err) }
91644
+ meta: { error: errMsg(err) }
93790
91645
  });
93791
91646
  return null;
93792
91647
  }
@@ -94094,15 +91949,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94094
91949
  stationarySettingsFromCache(deviceId) {
94095
91950
  return this.stationarySettingsCache.peek(deviceId) ?? STATIONARY_DEFAULTS;
94096
91951
  }
94097
- /** Synchronous read of the per-device summary settings — warmed by
94098
- * `processFrame`. A cold miss is today's constant, never a zero: a zero
94099
- * threshold would quietly end grouping on that camera. */
94100
- summarySettingsFromCache(deviceId) {
94101
- return this.summarySettingsCache.peek(deviceId) ?? SUMMARY_DEFAULTS;
94102
- }
94103
- async resolveDeviceSummarySettings(deviceId) {
94104
- return this.summarySettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveSummarySettings));
94105
- }
94106
91952
  /** Synchronous read of the per-device package-drop settings — warmed by
94107
91953
  * `processFrame` before the promotion loop, exactly like the stationary one. */
94108
91954
  packageDropSettingsFromCache(deviceId) {
@@ -94186,12 +92032,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94186
92032
  } } : {}
94187
92033
  });
94188
92034
  }
94189
- if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
92035
+ if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
94190
92036
  }
94191
92037
  const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
94192
92038
  if (resolved !== null && resolved.text !== void 0) {
94193
92039
  const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
94194
- if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
92040
+ if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
94195
92041
  }
94196
92042
  } else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
94197
92043
  } catch (err) {
@@ -94200,7 +92046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94200
92046
  meta: {
94201
92047
  trackId,
94202
92048
  stepId: d.stepId,
94203
- error: errMsg$1(err)
92049
+ error: errMsg(err)
94204
92050
  }
94205
92051
  });
94206
92052
  }
@@ -94336,7 +92182,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94336
92182
  tags: { deviceId },
94337
92183
  meta: {
94338
92184
  trackId: t.trackId,
94339
- error: errMsg$1(err)
92185
+ error: errMsg(err)
94340
92186
  }
94341
92187
  });
94342
92188
  }
@@ -94384,139 +92230,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94384
92230
  });
94385
92231
  }
94386
92232
  /**
94387
- * Capture the CLEAN best full frame per given track at this best-detection
94388
- * moment and store BOTH variants (`putReplacing` → one row per track, per
94389
- * kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
94390
- * — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
94391
- * from the SAME native surface (no second fetch). No box is drawn — these are
94392
- * the clean hero images (`keyFrameSmall` is the web-friendly one).
94393
- *
94394
- * NATIVE-OR-NOTHING: a miss writes nothing (no upscale, no ≤640 stand-in) and
94395
- * the track's BOUNDED catch-up retry re-fires on a later frame. Until
94396
- * 2026-08-06 the only re-fire was the next genuine new-best PEAK, which for
94397
- * 80% of the tracks that missed never came — see `key-frame-retry.ts`. The
94398
- * `keyFrame` key is recorded on the track's resident state so the face /
94399
- * plate / object-embedding rows LINK the SAME native key frame (Design B).
94400
- * Issued in the live-frame window so the native lease is still held.
94401
- * Best-effort (D8) — a per-track failure is logged and never thrown.
94402
- *
94403
- * The capture BODY lives in `pipeline/key-frame-persist.ts`; what stays here
94404
- * is the scheduling (coalescing, priority, budget) and the bookkeeping the
94405
- * retry gate reads.
94406
- */
94407
- /**
94408
- * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
94409
- * Metadata only, at frame-rate: the frame is scored against the members the
94410
- * sealer has mirrored, and the pixels are fetched — by handle, at the owning
94411
- * node, as a native key frame — only when this frame beats the best so far.
94412
- */
94413
- offerGroupShot(deviceId, result, frameHandle) {
94414
- if (frameHandle === void 0) return;
94415
- const groups = this.summarySealer?.openGroupsFor(deviceId) ?? [];
94416
- for (const open of groups) this.offerGroupShotTo(deviceId, open, result, frameHandle);
94417
- if (groups.length > 0) this.logGroupShotWindow(deviceId);
94418
- }
94419
- /** One group's turn at the frame. Split out of {@link offerGroupShot} so the
94420
- * scoring reads the members of the group it is about, and nothing else. */
94421
- offerGroupShotTo(deviceId, open, result, frameHandle) {
94422
- const score = scoreSummaryFrame({
94423
- members: open.members,
94424
- frameWidth: result.frameWidth,
94425
- frameHeight: result.frameHeight,
94426
- detections: result.tracked.map((t) => ({
94427
- trackId: t.trackId,
94428
- bbox: {
94429
- x: t.bbox.x,
94430
- y: t.bbox.y,
94431
- width: t.bbox.w,
94432
- height: t.bbox.h
94433
- },
94434
- confidence: t.confidence,
94435
- recognized: this.recognizedTrackIds.has(t.trackId)
94436
- }))
94437
- });
94438
- const frameRef = { timestamp: result.timestamp };
94439
- if (!this.groupShots.offer(open.id, score, frameRef)) return;
94440
- const getNative = this.getNativeKeyFrameRgb;
94441
- const mediaStore = this.mediaStore;
94442
- if (!getNative || !mediaStore) return;
94443
- const summaryId = open.id;
94444
- this.captureScheduler.request({
94445
- deviceId,
94446
- kind: "keyFrame",
94447
- coalesceKey: `summary:${summaryId}:keyFrame`,
94448
- exec: () => persistSummaryGroupShot({
94449
- fetchNativeFullFrame: getNative,
94450
- encodeVariants: encodeKeyFrameVariants,
94451
- media: mediaStore,
94452
- logger: this.ctx.logger,
94453
- cropFrame: async (frame, rect) => {
94454
- const out = await extractCropRaw(frame.data, frame.width, frame.height, {
94455
- x: rect.x / frame.width,
94456
- y: rect.y / frame.height,
94457
- w: rect.width / frame.width,
94458
- h: rect.height / frame.height
94459
- });
94460
- return {
94461
- ...frame,
94462
- data: out.data,
94463
- width: out.width,
94464
- height: out.height
94465
- };
94466
- }
94467
- }, {
94468
- deviceId,
94469
- summaryId,
94470
- timestamp: result.timestamp,
94471
- frameHandle,
94472
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
94473
- crop: score.crop,
94474
- cropFrameWidth: result.frameWidth,
94475
- cropFrameHeight: result.frameHeight
94476
- })
94477
- }).then((outcome) => {
94478
- if (outcome?.kind === "landed") {
94479
- this.groupShots.noteLanded(summaryId, score, frameRef);
94480
- this.ctx.logger.debug("summary group shot landed", {
94481
- tags: { deviceId },
94482
- meta: {
94483
- summaryId,
94484
- visible: score.visible,
94485
- quality: Number(score.quality.toFixed(3))
94486
- }
94487
- });
94488
- }
94489
- }).catch(() => {});
94490
- }
94491
- /**
94492
- * The group-shot gate's window, throttled to one line per
94493
- * {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
94494
- * the device that happened to close the window — the counters are the
94495
- * gate's, not the camera's, and the tag is there because every line about a
94496
- * device carries one.
94497
- *
94498
- * `materialised` is the number of 4096-px native full-frame fetches this
94499
- * path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
94500
- */
94501
- logGroupShotWindow(deviceId) {
94502
- const now = Date.now();
94503
- if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
94504
- this.lastGroupShotMetricAt = now;
94505
- const counts = this.groupShots.drainCounts();
94506
- if (counts.offered === 0) return;
94507
- this.ctx.logger.info("group shot window", {
94508
- tags: { deviceId },
94509
- meta: {
94510
- offered: counts.offered,
94511
- materialised: counts.materialised,
94512
- suppressedByGap: counts.byGap,
94513
- suppressedByMargin: counts.byMargin,
94514
- suppressedByWorse: counts.byWorse,
94515
- openSummaries: this.groupShots.size
94516
- }
94517
- });
94518
- }
94519
- /**
94520
92233
  * Order the parcels a best-frame decision wants (D422). Skipped entirely at
94521
92234
  * a node the registry already called offline (D58) — logged on the
94522
92235
  * transition by `noteCapturesGated`, counted in the key-frame window.
@@ -94553,6 +92266,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94553
92266
  timestamp: parcel.timestamp
94554
92267
  };
94555
92268
  }
92269
+ /**
92270
+ * Store each given track's clean native `keyFrame` + `keyFrameSmall` from its
92271
+ * PARKED parcel (D422) — never a by-handle fetch. The body is
92272
+ * `pipeline/key-frame-persist.ts`; what stays here is the scheduling
92273
+ * (coalescing, priority, budget) and the bookkeeping the retry gate reads.
92274
+ * Best-effort (D8): a per-track failure is logged and never thrown.
92275
+ */
94556
92276
  async persistKeyFrames(deviceId, timestamp, captures) {
94557
92277
  const mediaStore = this.mediaStore;
94558
92278
  if (this.parkedFrames === null || !mediaStore) return;
@@ -94653,7 +92373,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94653
92373
  meta: {
94654
92374
  trackId,
94655
92375
  keyFrameMediaId,
94656
- error: errMsg$1(err)
92376
+ error: errMsg(err)
94657
92377
  }
94658
92378
  });
94659
92379
  }
@@ -94720,8 +92440,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94720
92440
  * when it wins and the frame's pixels are still reachable. Callers: the
94721
92441
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
94722
92442
  * fine-classification attribution. */
94723
- promoteSemanticRecognition(input, origin) {
94724
- if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
92443
+ promoteSemanticRecognition(input) {
94725
92444
  const ctx = this.ctxIfReady;
94726
92445
  if (ctx === null) return;
94727
92446
  promoteSemanticBestFrame({
@@ -95262,7 +92981,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95262
92981
  tags: { deviceId: input.deviceId },
95263
92982
  meta: {
95264
92983
  className: ref.className,
95265
- error: errMsg$1(err)
92984
+ error: errMsg(err)
95266
92985
  }
95267
92986
  });
95268
92987
  }
@@ -95294,7 +93013,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95294
93013
  tags: { deviceId: track.deviceId },
95295
93014
  meta: {
95296
93015
  episodeId: ref.episodeId,
95297
- error: errMsg$1(err)
93016
+ error: errMsg(err)
95298
93017
  }
95299
93018
  });
95300
93019
  }
@@ -95551,7 +93270,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95551
93270
  tags: { deviceId: input.deviceId },
95552
93271
  meta: {
95553
93272
  eventId: input.eventId,
95554
- error: errMsg$1(err)
93273
+ error: errMsg(err)
95555
93274
  }
95556
93275
  });
95557
93276
  });
@@ -95618,7 +93337,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95618
93337
  async sweepExpiredTracks() {
95619
93338
  if (this.shuttingDown || !this.trackStore) return;
95620
93339
  await this.trackCloser.sweep();
95621
- await this.summarySealer?.sweep();
95622
93340
  }
95623
93341
  /**
95624
93342
  * The follow-recordings floor per camera, read from the ARCHIVE's hour ledger
@@ -95658,7 +93376,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95658
93376
  listTrackedDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
95659
93377
  }, (half, err) => this.ctx.logger.warn("retention scope half unavailable — sweeping on the other", { meta: {
95660
93378
  half,
95661
- error: errMsg$1(err)
93379
+ error: errMsg(err)
95662
93380
  } }));
95663
93381
  }
95664
93382
  /**
@@ -95705,7 +93423,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95705
93423
  await this.runTrackRetentionSweep(deviceIds, now);
95706
93424
  const eventStore = this.eventStore;
95707
93425
  const sensorEventStore = this.sensorEventStore;
95708
- const summaryStore = this.summaryStore;
95709
93426
  await sweepTracklessRoots({
95710
93427
  deviceIds,
95711
93428
  nowMs: now,
@@ -95715,16 +93432,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95715
93432
  cutoffMs
95716
93433
  }),
95717
93434
  evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
95718
- evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
95719
93435
  onEvicted: async (deviceId, evicted, cutoffs) => {
95720
- const total = evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries;
93436
+ const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
95721
93437
  await this.eventsOpsLog?.append({
95722
93438
  op: "prune",
95723
93439
  reason: "retention",
95724
93440
  deviceId,
95725
93441
  itemsAffected: total,
95726
93442
  bytesReclaimed: 0,
95727
- detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor, ${evicted.summaries} summaries`
93443
+ detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor`
95728
93444
  });
95729
93445
  this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
95730
93446
  tags: { deviceId },
@@ -95732,7 +93448,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95732
93448
  motion: evicted.motion.length,
95733
93449
  audio: evicted.audio.length,
95734
93450
  sensor: evicted.sensor,
95735
- summaries: evicted.summaries,
95736
93451
  mode: cutoffs.effectiveMode,
95737
93452
  cutoffMs: cutoffs.cutoffMs
95738
93453
  }
@@ -96686,118 +94401,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96686
94401
  async listRecentTracks(input) {
96687
94402
  return this.queryFacade.listRecentTracks(input);
96688
94403
  }
96689
- /** Newest first for a camera set; each row carries its group-shot media key. */
96690
- async listSummaries(input) {
96691
- const store = this.summaryStore;
96692
- if (!store) return { summaries: [] };
96693
- const rows = await store.listRecent({
96694
- deviceIds: input.deviceIds,
96695
- ...input.since !== void 0 ? { since: input.since } : {},
96696
- ...input.until !== void 0 ? { until: input.until } : {},
96697
- limit: input.limit ?? 50,
96698
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96699
- });
96700
- return { summaries: await this.listItemsFor(rows) };
96701
- }
96702
- /**
96703
- * Rows → what a LIST draws: the envelope plus the composition.
96704
- *
96705
- * Two bulk reads for the whole page, never per row: the members in one, their
96706
- * tracks (for the names) in one. The MEMBERS themselves stay off the list —
96707
- * a page of two hundred groups would otherwise carry a thousand records so
96708
- * the client could count them. `getSummary` is where the links live.
96709
- */
96710
- async listItemsFor(rows) {
96711
- const store = this.summaryStore;
96712
- if (!store || rows.length === 0) return [];
96713
- const members = await store.membersOfMany(rows.map((r) => r.id));
96714
- return await Promise.all(rows.map(async (row) => ({
96715
- summary: await this.toSummary(row),
96716
- ...summaryComposition(members.get(row.id) ?? [])
96717
- })));
96718
- }
96719
- /** The members of a page of summaries, with the labels a card can show —
96720
- * in ONE read of the members and ONE of their tracks. */
96721
- async labelledMembersFor(rows) {
96722
- const out = /* @__PURE__ */ new Map();
96723
- const store = this.summaryStore;
96724
- if (!store || rows.length === 0) return out;
96725
- const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
96726
- const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
96727
- let tracks = [];
96728
- try {
96729
- tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
96730
- } catch (err) {
96731
- this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
96732
- summaries: rows.length,
96733
- members: trackIds.length,
96734
- error: errMsg$1(err)
96735
- } });
96736
- }
96737
- for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
96738
- return out;
96739
- }
96740
- /**
96741
- * Rows → the shape every summary surface answers with: the envelope, its
96742
- * members, and what a thumbnail can render for each of them.
96743
- *
96744
- * THREE reads for the whole page, never per row: the members in one, their
96745
- * tracks in one, and the group shots through `toSummary`. A read per member
96746
- * is what made a sixty-group page take twenty seconds on the events feed.
96747
- */
96748
- /** ONE summary, with every member and the link each of them carries. */
96749
- async detailFor(row) {
96750
- const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
96751
- return {
96752
- summary: await this.toSummary(row),
96753
- members: labelled.map((m) => ({
96754
- trackId: m.trackId,
96755
- className: m.className,
96756
- role: m.role,
96757
- joinedAt: m.joinedAt,
96758
- ...m.label !== void 0 ? { label: m.label } : {},
96759
- ...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
96760
- }))
96761
- };
96762
- }
96763
- async listRecentSummaries(input) {
96764
- const store = this.summaryStore;
96765
- if (!store) return {
96766
- summaries: [],
96767
- nextCursor: null
96768
- };
96769
- const page = await store.queryRecent({
96770
- deviceIds: input.deviceIds,
96771
- ...input.since !== void 0 ? { since: input.since } : {},
96772
- ...input.until !== void 0 ? { until: input.until } : {},
96773
- limit: input.limit,
96774
- ...input.cursor !== void 0 ? { cursor: input.cursor } : {},
96775
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96776
- });
96777
- return {
96778
- summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
96779
- nextCursor: page.nextCursor
96780
- };
96781
- }
96782
- async getSummary(input) {
96783
- const detail = await this.summaryStore?.get(input.id);
96784
- if (!detail) return null;
96785
- return await this.detailFor(detail.summary);
96786
- }
96787
- /** The row plus its group shot: the web companion when it landed, else the native key frame. */
96788
- async toSummary(row) {
96789
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
96790
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
96791
- return {
96792
- id: row.id,
96793
- deviceId: row.deviceId,
96794
- openedAt: row.openedAt,
96795
- lastActiveAt: row.lastActiveAt,
96796
- sealedAt: row.sealedAt,
96797
- memberCount: row.memberCount,
96798
- groupShotMediaId: shot?.mediaId ?? null
96799
- };
96800
- }
96801
94404
  /**
96802
94405
  * Every event kind the device can produce: built-ins (motion + audio), the
96803
94406
  * detection classes its CONFIGURED pipeline can emit, and sensor kinds from
@@ -96826,7 +94429,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96826
94429
  tags: { deviceId: input.deviceId },
96827
94430
  meta: {
96828
94431
  scope,
96829
- error: errMsg$1(err)
94432
+ error: errMsg(err)
96830
94433
  }
96831
94434
  })
96832
94435
  }, input.deviceId);
@@ -96844,7 +94447,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96844
94447
  } catch (err) {
96845
94448
  this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
96846
94449
  tags: { deviceId },
96847
- meta: { error: errMsg$1(err) }
94450
+ meta: { error: errMsg(err) }
96848
94451
  });
96849
94452
  return NO_EMITTABLE;
96850
94453
  }
@@ -96881,7 +94484,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96881
94484
  }, deviceIds).catch((err) => {
96882
94485
  this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
96883
94486
  deviceCount: deviceIds.length,
96884
- error: errMsg$1(err)
94487
+ error: errMsg(err)
96885
94488
  } });
96886
94489
  return null;
96887
94490
  });
@@ -96899,13 +94502,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96899
94502
  tags: { deviceId },
96900
94503
  meta: {
96901
94504
  scope,
96902
- error: errMsg$1(err)
94505
+ error: errMsg(err)
96903
94506
  }
96904
94507
  })
96905
94508
  }, deviceId);
96906
94509
  }, (deviceId, err) => this.ctx.logger.warn("listEventKindsBatch: compose threw — empty vocabulary", {
96907
94510
  tags: { deviceId },
96908
- meta: { error: errMsg$1(err) }
94511
+ meta: { error: errMsg(err) }
96909
94512
  }));
96910
94513
  }
96911
94514
  /** Per-camera sensor-event history (state changes of linked devices). */
@@ -96935,7 +94538,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96935
94538
  meta: {
96936
94539
  capName: data.capName,
96937
94540
  cameraIds: [...cameras.mediaCameraIds],
96938
- error: errMsg$1(err)
94541
+ error: errMsg(err)
96939
94542
  }
96940
94543
  });
96941
94544
  return;
@@ -96979,7 +94582,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96979
94582
  meta: {
96980
94583
  kind: event.kind,
96981
94584
  linkedCameraIds: [...input.mediaCameraIds],
96982
- error: errMsg$1(err)
94585
+ error: errMsg(err)
96983
94586
  }
96984
94587
  });
96985
94588
  }
@@ -96996,7 +94599,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96996
94599
  tags: { deviceId: data.deviceId },
96997
94600
  meta: {
96998
94601
  capName: data.capName,
96999
- error: errMsg$1(err)
94602
+ error: errMsg(err)
97000
94603
  }
97001
94604
  });
97002
94605
  });
@@ -97052,7 +94655,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97052
94655
  tags: { deviceId: data.deviceId },
97053
94656
  meta: {
97054
94657
  capName: data.capName,
97055
- error: errMsg$1(err)
94658
+ error: errMsg(err)
97056
94659
  }
97057
94660
  });
97058
94661
  return NO_SENSOR_NOTIFICATION_CAMERAS;
@@ -97074,7 +94677,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97074
94677
  tags: { deviceId: data.deviceId },
97075
94678
  meta: {
97076
94679
  capName: data.capName,
97077
- error: errMsg$1(err)
94680
+ error: errMsg(err)
97078
94681
  }
97079
94682
  });
97080
94683
  }
@@ -97146,7 +94749,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97146
94749
  tags: { deviceId: data.deviceId },
97147
94750
  meta: {
97148
94751
  capName: data.capName,
97149
- error: errMsg$1(err)
94752
+ error: errMsg(err)
97150
94753
  }
97151
94754
  });
97152
94755
  }
@@ -97169,7 +94772,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97169
94772
  meta: {
97170
94773
  kind: ev.kind,
97171
94774
  sourceDeviceId: ev.sourceDeviceId,
97172
- error: errMsg$1(err)
94775
+ error: errMsg(err)
97173
94776
  }
97174
94777
  });
97175
94778
  }
@@ -97189,7 +94792,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97189
94792
  meta: {
97190
94793
  kind: ev.kind,
97191
94794
  linkedCameraIds: [...linkedCameraIds],
97192
- error: errMsg$1(err)
94795
+ error: errMsg(err)
97193
94796
  }
97194
94797
  });
97195
94798
  }
@@ -97319,7 +94922,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97319
94922
  } catch (err) {
97320
94923
  this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
97321
94924
  tags: { deviceId },
97322
- meta: { error: errMsg$1(err) }
94925
+ meta: { error: errMsg(err) }
97323
94926
  });
97324
94927
  return;
97325
94928
  }
@@ -97581,11 +95184,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97581
95184
  },
97582
95185
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
97583
95186
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
97584
- liveSummaryIds: (ids) => {
97585
- const summaries = this.summaryStore;
97586
- if (!summaries) throw new Error("summary store not built — probe unavailable");
97587
- return summaries.existingIds(ids);
97588
- },
97589
95187
  keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
97590
95188
  reclaim: buildOrphanReclaimer({
97591
95189
  deleteFace: async (faceId) => {
@@ -98192,7 +95790,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98192
95790
  nodeId
98193
95791
  };
98194
95792
  } catch (err) {
98195
- const reason = errMsg$1(err);
95793
+ const reason = errMsg(err);
98196
95794
  failures.push(`${nodeId}: ${reason}`);
98197
95795
  this.ctx.logger.warn("photo enrollment: a node could not analyse the photograph", { meta: {
98198
95796
  nodeId,
@@ -98918,7 +96516,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98918
96516
  this.unreadableClipIndexes.add(space.index);
98919
96517
  this.ctx.logger.warn("embedding.rebuildStatus: index size unreadable — reported as unknown", { meta: {
98920
96518
  index: space.index,
98921
- error: errMsg$1(err)
96519
+ error: errMsg(err)
98922
96520
  } });
98923
96521
  }
98924
96522
  indexedByIndex[space.index] = null;
@@ -99274,8 +96872,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99274
96872
  mediaStore,
99275
96873
  faceStore: this.faceStore,
99276
96874
  plateStore: this.plateStore,
99277
- objectEmbeddingStore: this.objectEmbeddingStore,
99278
- summaryStore: this.summaryStore
96875
+ objectEmbeddingStore: this.objectEmbeddingStore
99279
96876
  };
99280
96877
  const absent = absentCascadeLegs(stores);
99281
96878
  if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
@@ -99853,7 +97450,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99853
97450
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
99854
97451
  mediaId: media.mediaId,
99855
97452
  size: variant.size,
99856
- error: errMsg$1(err)
97453
+ error: errMsg(err)
99857
97454
  } });
99858
97455
  return media;
99859
97456
  }