@camstack/addon-post-analysis 1.2.296 → 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);
@@ -47028,10 +46896,127 @@ function pairRiders(detections) {
47028
46896
  }
47029
46897
  return pairs;
47030
46898
  }
46899
+ /**
46900
+ * Floor on the predictive-drift clamp, in box sides.
46901
+ *
46902
+ * The scaled term collapses at short intervals — at the 56 ms minimum measured
46903
+ * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
46904
+ * detector box jitter into a refused match. A quarter of a side is slack, not
46905
+ * travel.
46906
+ */
46907
+ var DRIFT_MIN_SIDES = .25;
46908
+ /**
46909
+ * Re-acquisition reach, in box DIAGONALS per second.
46910
+ *
46911
+ * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
46912
+ * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
46913
+ * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
46914
+ * physically bounded — 1.5 of a subject's own body-diagonal per second is a
46915
+ * brisk run, and being scale-free it means the same thing for a far-field
46916
+ * person with a 90-px box as for a near one with a 900-px box.
46917
+ *
46918
+ * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
46919
+ * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
46920
+ * a walking subject, and at a 150 ms burst interval it was loose enough to hand
46921
+ * one subject's id to another 452 px away (track `27855a56`, a second car, at
46922
+ * IoU 0.058). One number, two opposite failures.
46923
+ */
46924
+ var REACH_DIAGONALS_PER_SEC = 1.5;
46925
+ /**
46926
+ * Floor on the re-acquisition reach, in box diagonals.
46927
+ *
46928
+ * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
46929
+ * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
46930
+ * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
46931
+ * bought that track a second car. A short gap is evidence that the subject
46932
+ * cannot have gone far, and the gate now says so.
46933
+ */
46934
+ var REACH_MIN_DIAGONALS = .3;
46935
+ /**
46936
+ * Ceiling on the re-acquisition reach, in box diagonals.
46937
+ *
46938
+ * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
46939
+ * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
46940
+ */
46941
+ var REACH_MAX_DIAGONALS = 1.5;
46942
+ function clampRange(value, min, max) {
46943
+ return Math.max(min, Math.min(max, value));
46944
+ }
46945
+ /**
46946
+ * How far a coasting box's velocity extrapolation may carry it on one axis.
46947
+ *
46948
+ * `side` is the box's width for the x drift and its height for the y drift, so
46949
+ * the allowance keeps the same meaning for a tall person and a wide car.
46950
+ */
46951
+ function driftAllowancePx(side, elapsedMs) {
46952
+ const elapsed = Math.max(0, elapsedMs);
46953
+ return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
46954
+ }
46955
+ /**
46956
+ * How far from where a track is EXPECTED a detection may sit and still be
46957
+ * claimed as that track by the loose (rescue / resurrection) reach clause.
46958
+ *
46959
+ * Scale-free: the budget is a multiple of the track's own diagonal, so it does
46960
+ * not have to know the frame size, the camera, or how far away the subject is.
46961
+ */
46962
+ function reacquireReachPx(bbox, elapsedMs) {
46963
+ const diag = Math.hypot(bbox.w, bbox.h);
46964
+ const elapsed = Math.max(0, elapsedMs);
46965
+ return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
46966
+ }
46967
+ /**
46968
+ * Resurrection reach, in box DIAGONALS per second of gap (D665).
46969
+ *
46970
+ * A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
46971
+ * rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
46972
+ * nothing useful here. A subject the detector has not boxed for four seconds
46973
+ * is, on this fleet's evidence, one that stood still or stood half-hidden,
46974
+ * not one striding across the frame (a striding person is detected): 0.25
46975
+ * diagonals per second is a slow walk for a mid-field person and a pause for
46976
+ * a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
46977
+ * camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
46978
+ * i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
46979
+ */
46980
+ var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
46981
+ /** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
46982
+ var RESURRECTION_REACH_MIN_DIAGONALS = .75;
46983
+ /**
46984
+ * How far from a corpse's last box a detection may sit and still resurrect it
46985
+ * on the proximity clause, for a corpse last seen `gapMs` ago.
46986
+ */
46987
+ function resurrectionReachPx(bbox, gapMs) {
46988
+ const diag = Math.hypot(bbox.w, bbox.h);
46989
+ return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
46990
+ }
46991
+ /**
46992
+ * The classes D665's two relaxations are for: the low-score continuation tier
46993
+ * and the time-scaled resurrection gate. Defined ONCE, here, and read by both
46994
+ * (`low-score-continuation.ts`, `SortTracker.resurrectionTimeScaledFor`),
46995
+ * because the argument for each is the same argument about the same subjects
46996
+ * (D665, D678).
46997
+ *
46998
+ * D665 grew the graveyard reach with the gap on the argument that a subject
46999
+ * nobody has boxed for more than 4 s stood still or stood half-hidden — true
47000
+ * of a person and of an animal, and what the refused returns on camera 590
47001
+ * were. A car does not stand half-hidden for ten seconds and then reappear
47002
+ * one and a half diagonals away: the next car arrives. Camera 617,
47003
+ * 2026-09-28 08:47:32Z: a car at the gate inherited the id of the car that
47004
+ * had left the lot 9.85 s earlier, 1.51 diagonals away. A `vehicle` corpse
47005
+ * keeps the flat `rescueCentroidFactor` reach and the plausible-birth veto
47006
+ * it had before D665 — the same exclusion D665 made for the low-score tier,
47007
+ * where a sub-floor box on a parked car beside a passing one is the D410 hop.
47008
+ */
47009
+ var LINGERING_SUBJECT_CLASSES = new Set(["person", "animal"]);
47010
+ /** Larger area / smaller area; `Infinity` when either box is empty. */
47011
+ function boxAreaRatio(a, b) {
47012
+ const areaA = a.w * a.h;
47013
+ const areaB = b.w * b.h;
47014
+ const smaller = Math.min(areaA, areaB);
47015
+ return smaller > 0 ? Math.max(areaA, areaB) / smaller : Number.POSITIVE_INFINITY;
47016
+ }
47031
47017
  //#endregion
47032
47018
  //#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
47033
47019
  var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
47034
- var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
47035
47020
  /** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
47036
47021
  function toRect(bbox) {
47037
47022
  const [x1, y1, x2, y2] = bbox;
@@ -47052,7 +47037,7 @@ function selectLowScoreContinuations(discarded, floor) {
47052
47037
  for (const d of discarded) {
47053
47038
  if (d.reason !== "below-threshold") continue;
47054
47039
  if (d.score < floor) continue;
47055
- if (!LOW_SCORE_CONTINUATION_CLASSES.has(d.macroClass)) continue;
47040
+ if (!LINGERING_SUBJECT_CLASSES.has(d.macroClass)) continue;
47056
47041
  const bbox = toRect(d.bbox);
47057
47042
  if (bbox.w <= 0 || bbox.h <= 0) continue;
47058
47043
  out.push({
@@ -47133,10 +47118,12 @@ function newTrackId() {
47133
47118
  * the counts are per camera and per detection source. */
47134
47119
  var LooseBindSampler = class {
47135
47120
  counts = /* @__PURE__ */ new Map();
47136
- /** Increment the (stage, gate) counter and answer with the running total when
47137
- * this decision should be reported, or `undefined` when it is sampled out. */
47138
- next(stage, gate) {
47139
- const key = `${stage}|${gate}`;
47121
+ /** Increment the (stage, gate, refusal) counter and answer with the running
47122
+ * total when this decision should be reported, or `undefined` when it is
47123
+ * sampled out. The refusal is part of the key so a rare refusal is not
47124
+ * sampled away behind a frequent one on the same gate. */
47125
+ next(stage, gate, refusal) {
47126
+ const key = `${stage}|${gate}|${refusal ?? ""}`;
47140
47127
  const count = (this.counts.get(key) ?? 0) + 1;
47141
47128
  this.counts.set(key, count);
47142
47129
  if (count === 1 || count % 50 === 0) return count;
@@ -47145,98 +47132,6 @@ var LooseBindSampler = class {
47145
47132
  this.counts.clear();
47146
47133
  }
47147
47134
  };
47148
- /**
47149
- * Floor on the predictive-drift clamp, in box sides.
47150
- *
47151
- * The scaled term collapses at short intervals — at the 56 ms minimum measured
47152
- * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
47153
- * detector box jitter into a refused match. A quarter of a side is slack, not
47154
- * travel.
47155
- */
47156
- var DRIFT_MIN_SIDES = .25;
47157
- /**
47158
- * Re-acquisition reach, in box DIAGONALS per second.
47159
- *
47160
- * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
47161
- * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
47162
- * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
47163
- * physically bounded — 1.5 of a subject's own body-diagonal per second is a
47164
- * brisk run, and being scale-free it means the same thing for a far-field
47165
- * person with a 90-px box as for a near one with a 900-px box.
47166
- *
47167
- * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
47168
- * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
47169
- * a walking subject, and at a 150 ms burst interval it was loose enough to hand
47170
- * one subject's id to another 452 px away (track `27855a56`, a second car, at
47171
- * IoU 0.058). One number, two opposite failures.
47172
- */
47173
- var REACH_DIAGONALS_PER_SEC = 1.5;
47174
- /**
47175
- * Floor on the re-acquisition reach, in box diagonals.
47176
- *
47177
- * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
47178
- * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
47179
- * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
47180
- * bought that track a second car. A short gap is evidence that the subject
47181
- * cannot have gone far, and the gate now says so.
47182
- */
47183
- var REACH_MIN_DIAGONALS = .3;
47184
- /**
47185
- * Ceiling on the re-acquisition reach, in box diagonals.
47186
- *
47187
- * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
47188
- * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
47189
- */
47190
- var REACH_MAX_DIAGONALS = 1.5;
47191
- function clampRange(value, min, max) {
47192
- return Math.max(min, Math.min(max, value));
47193
- }
47194
- /**
47195
- * How far a coasting box's velocity extrapolation may carry it on one axis.
47196
- *
47197
- * `side` is the box's width for the x drift and its height for the y drift, so
47198
- * the allowance keeps the same meaning for a tall person and a wide car.
47199
- */
47200
- function driftAllowancePx(side, elapsedMs) {
47201
- const elapsed = Math.max(0, elapsedMs);
47202
- return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
47203
- }
47204
- /**
47205
- * How far from where a track is EXPECTED a detection may sit and still be
47206
- * claimed as that track by the loose (rescue / resurrection) reach clause.
47207
- *
47208
- * Scale-free: the budget is a multiple of the track's own diagonal, so it does
47209
- * not have to know the frame size, the camera, or how far away the subject is.
47210
- */
47211
- function reacquireReachPx(bbox, elapsedMs) {
47212
- const diag = Math.hypot(bbox.w, bbox.h);
47213
- const elapsed = Math.max(0, elapsedMs);
47214
- return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
47215
- }
47216
- /**
47217
- * Resurrection reach, in box DIAGONALS per second of gap (D665).
47218
- *
47219
- * A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
47220
- * rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
47221
- * nothing useful here. A subject the detector has not boxed for four seconds
47222
- * is, on this fleet's evidence, one that stood still or stood half-hidden,
47223
- * not one striding across the frame (a striding person is detected): 0.25
47224
- * diagonals per second is a slow walk for a mid-field person and a pause for
47225
- * a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
47226
- * camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
47227
- * i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
47228
- */
47229
- var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
47230
- /** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
47231
- var RESURRECTION_REACH_MIN_DIAGONALS = .75;
47232
- /**
47233
- * How far from a corpse's last box a detection may sit and still resurrect it
47234
- * on the proximity clause, for a corpse last seen `gapMs` ago.
47235
- */
47236
- function resurrectionReachPx(bbox, gapMs) {
47237
- const diag = Math.hypot(bbox.w, bbox.h);
47238
- return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
47239
- }
47240
47135
  //#endregion
47241
47136
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
47242
47137
  /**
@@ -47251,7 +47146,7 @@ function resurrectionReachPx(bbox, gapMs) {
47251
47146
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
47252
47147
  */
47253
47148
  var GROWTH_CENTROID_FACTOR = 2.3;
47254
- function intersectionArea$3(a, b) {
47149
+ function intersectionArea$2(a, b) {
47255
47150
  const x1 = Math.max(a.x, b.x);
47256
47151
  const y1 = Math.max(a.y, b.y);
47257
47152
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -47290,7 +47185,7 @@ function growthAdjacentMatch(track, det, ctx) {
47290
47185
  const larger = Math.max(trackArea, detArea);
47291
47186
  const smaller = Math.min(trackArea, detArea);
47292
47187
  if (larger / smaller < 1.5) return false;
47293
- if (intersectionArea$3(track, det) / smaller >= .5) return false;
47188
+ if (intersectionArea$2(track, det) / smaller >= .5) return false;
47294
47189
  const tc = bboxCentroid(track);
47295
47190
  const dc = bboxCentroid(det);
47296
47191
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -47689,10 +47584,10 @@ var SortTracker = class SortTracker {
47689
47584
  }
47690
47585
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
47691
47586
  * Nothing is computed when no observer is wired. */
47692
- reportBind(stage, gate, track, det, timestamp) {
47587
+ reportBind(stage, gate, track, det, timestamp, refusal) {
47693
47588
  const observer = this.bindObserver;
47694
47589
  if (!observer) return;
47695
- const bindCount = this.bindSampler.next(stage, gate);
47590
+ const bindCount = this.bindSampler.next(stage, gate, refusal);
47696
47591
  if (bindCount === void 0) return;
47697
47592
  const tc = bboxCentroid(track.bbox);
47698
47593
  const dc = bboxCentroid(det.bbox);
@@ -47726,6 +47621,7 @@ var SortTracker = class SortTracker {
47726
47621
  frameHeight: this.frameHeightPx,
47727
47622
  dtMs: timestamp - track.lastSeen,
47728
47623
  trackAgeMs: timestamp - track.firstSeen,
47624
+ ...refusal !== void 0 ? { refusal } : {},
47729
47625
  bindCount,
47730
47626
  sampledEvery: 50
47731
47627
  });
@@ -48066,15 +47962,45 @@ var SortTracker = class SortTracker {
48066
47962
  * was written for; not of the pass as it runs: on camera 590 (2026-09-27)
48067
47963
  * 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
48068
47964
  * flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
48069
- * off restores the flat gate.
47965
+ * off restores the flat gate — and a `vehicle` corpse always has it (D678,
47966
+ * {@link resurrectionTimeScaledFor}).
48070
47967
  */
48071
47968
  reacquireReach(track, timestamp, pass) {
48072
47969
  const diag = Math.hypot(track.bbox.w, track.bbox.h);
48073
- if (pass === "resurrection") return this.config.resurrectionTimeScaled ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
47970
+ if (pass === "resurrection") return this.resurrectionTimeScaledFor(track.class) ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
48074
47971
  if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
48075
47972
  return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
48076
47973
  }
48077
47974
  /**
47975
+ * Whether D665's graveyard gate — the reach that grows with the gap, and the
47976
+ * plausible-birth veto lifted for a sole subject — applies to a corpse of
47977
+ * `cls`. People and animals only (D678): a car gone for ten seconds did not
47978
+ * stand half-hidden, the next one arrived.
47979
+ */
47980
+ resurrectionTimeScaledFor(cls) {
47981
+ return this.config.resurrectionTimeScaled && LINGERING_SUBJECT_CLASSES.has(cls);
47982
+ }
47983
+ /**
47984
+ * The size check a PROXIMITY-only resurrection needs (D678), for a pair the
47985
+ * loose gate accepted on its `centroid` clause and the plausible-birth veto
47986
+ * let through.
47987
+ *
47988
+ * The `centroid` clause says where a box is and nothing about what it is;
47989
+ * a box more than {@link RESURRECTION_MAX_AREA_RATIO} larger or smaller than
47990
+ * the corpse's last box is refused on it, and the refusal is returned so the
47991
+ * caller can name it. The pair may still bind on `growth`, the partial-crop
47992
+ * rule whose whole subject IS a size change and which `looseMatch` would
47993
+ * have reached had the centroid clause not answered first. With the default
47994
+ * coast budget that never happens — `growth` refuses a pair more than
47995
+ * `GROWTH_MAX_GAP_MS` (1 s) apart and a corpse has been unseen for
47996
+ * `maxMissedMs` (4 s) — but `maxMissedMs` is a per-camera setting down to 0.
47997
+ */
47998
+ proximitySizeVerdict(corpse, det, timestamp) {
47999
+ if (boxAreaRatio(corpse.bbox, det.bbox) <= 2.5) return "centroid";
48000
+ if (growthAdjacentMatch(corpse.bbox, det.bbox, this.growthContext(corpse, timestamp))) return "growth";
48001
+ return "size-incompatible";
48002
+ }
48003
+ /**
48078
48004
  * Whether a REACH bind would be unambiguous: this coasting track is the only
48079
48005
  * subject of its association group the scene holds, and `det` is the only box
48080
48006
  * of that group on the frame.
@@ -48488,13 +48414,26 @@ var SortTracker = class SortTracker {
48488
48414
  let refused;
48489
48415
  let refusedGate = "iou";
48490
48416
  let refusedScore = -1;
48417
+ let refusedBy = "crowd-guard";
48491
48418
  for (const lost of this.lostTracks) {
48492
48419
  if (!lost.resurrectable) continue;
48493
48420
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
48494
- const gate = this.looseMatch(lost, det, timestamp, "resurrection");
48495
- if (gate === null) continue;
48496
- const vetoLifted = gate === "centroid" && this.isPlausibleBirth(det, frameArea);
48497
- if (vetoLifted && !(this.config.resurrectionTimeScaled && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
48421
+ const loose = this.looseMatch(lost, det, timestamp, "resurrection");
48422
+ if (loose === null) continue;
48423
+ const vetoApplies = loose === "centroid" && this.isPlausibleBirth(det, frameArea);
48424
+ if (vetoApplies && !(this.resurrectionTimeScaledFor(lost.class) && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
48425
+ const gate = loose === "centroid" ? this.proximitySizeVerdict(lost, det, timestamp) : loose;
48426
+ if (gate === "size-incompatible") {
48427
+ const score = iou$2(lost.bbox, det.bbox);
48428
+ if (score > refusedScore) {
48429
+ refused = lost;
48430
+ refusedGate = "centroid";
48431
+ refusedScore = score;
48432
+ refusedBy = "size-incompatible";
48433
+ }
48434
+ continue;
48435
+ }
48436
+ const vetoLifted = vetoApplies && gate === "centroid";
48498
48437
  const score = iou$2(lost.bbox, det.bbox);
48499
48438
  const peers = this.sameClassPeerCount(lost, surviving);
48500
48439
  if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
@@ -48502,6 +48441,7 @@ var SortTracker = class SortTracker {
48502
48441
  refused = lost;
48503
48442
  refusedGate = gate;
48504
48443
  refusedScore = score;
48444
+ refusedBy = "crowd-guard";
48505
48445
  }
48506
48446
  continue;
48507
48447
  }
@@ -48514,7 +48454,7 @@ var SortTracker = class SortTracker {
48514
48454
  }
48515
48455
  }
48516
48456
  if (!best) {
48517
- if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
48457
+ if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp, refusedBy);
48518
48458
  continue;
48519
48459
  }
48520
48460
  resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
@@ -50369,7 +50309,6 @@ var EMPTY_CASCADE_TALLY = {
50369
50309
  faces: 0,
50370
50310
  plates: 0,
50371
50311
  embeddings: 0,
50372
- summaries: 0,
50373
50312
  bytesReclaimed: 0
50374
50313
  };
50375
50314
  /** Fold a leaf store's receipts into a running tally. */
@@ -50413,13 +50352,6 @@ function addLeafCounts(tally, counts) {
50413
50352
  bytesReclaimed
50414
50353
  };
50415
50354
  break;
50416
- case "summaries":
50417
- next = {
50418
- ...next,
50419
- summaries: next.summaries + c.rows,
50420
- bytesReclaimed
50421
- };
50422
- break;
50423
50355
  }
50424
50356
  }
50425
50357
  return next;
@@ -50433,7 +50365,6 @@ function mergeCascadeTallies(a, b) {
50433
50365
  faces: a.faces + b.faces,
50434
50366
  plates: a.plates + b.plates,
50435
50367
  embeddings: a.embeddings + b.embeddings,
50436
- summaries: a.summaries + b.summaries,
50437
50368
  bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
50438
50369
  };
50439
50370
  }
@@ -50496,7 +50427,6 @@ function absentCascadeLegs(stores) {
50496
50427
  if (!stores.faceStore) absent.push("faces");
50497
50428
  if (!stores.plateStore) absent.push("plates");
50498
50429
  if (!stores.objectEmbeddingStore) absent.push("embeddings");
50499
- if (!stores.summaryStore) absent.push("summaries");
50500
50430
  return absent;
50501
50431
  }
50502
50432
  function buildAnalyticsCascadeRegistry(stores) {
@@ -50504,7 +50434,6 @@ function buildAnalyticsCascadeRegistry(stores) {
50504
50434
  if (stores.faceStore) registry.push(stores.faceStore);
50505
50435
  if (stores.plateStore) registry.push(stores.plateStore);
50506
50436
  if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
50507
- if (stores.summaryStore) registry.push(stores.summaryStore);
50508
50437
  return registry;
50509
50438
  }
50510
50439
  /**
@@ -55025,10 +54954,6 @@ function trackColumnsFor(projection, needsPositions = false) {
55025
54954
  * whatever order the engine felt like and a cursor that cannot resume.
55026
54955
  */
55027
54956
  var RECENT_IDS_COLUMNS = ["lastSeen"];
55028
- /** How many track ids one filter pass may collect per tier. Bounded for the
55029
- * same reason every bulk read here is: an unbounded scan of this table is
55030
- * what D447 measured holding the settings thread for seconds. */
55031
- var TRACK_MATCH_SCAN_LIMIT = 2e4;
55032
54957
  /** Spreadable `columns` for a store query — omitted entirely for `full`. */
55033
54958
  function projectionArg(projection, needsPositions = false) {
55034
54959
  const columns = trackColumnsFor(projection, needsPositions);
@@ -56145,42 +56070,6 @@ var TrackStore = class {
56145
56070
  * current subject out from under it. The orphan audit's grace window would
56146
56071
  * hide this for an hour and then stop hiding it.
56147
56072
  */
56148
- /**
56149
- * Track ids whose CLASS, sub-class or instance matches any of `terms`.
56150
- *
56151
- * The summaries' filter asks its question here rather than on a copy: a
56152
- * label is a fact about the TRACK, it is decided while the track lives and
56153
- * can still be re-decided afterwards (class voting, a late face match, an
56154
- * operator enrolling a face and renaming that person). A copy stamped onto
56155
- * the summary would answer with whatever was true at one instant and never
56156
- * learn — which is the whole reason this lookup exists instead.
56157
- *
56158
- * Three queries because the filter compiler has no OR across fields, and one
56159
- * term can be any of the three tiers: `animal` is a class, `dog` is a
56160
- * sub-class, `Roberta` is an instance. Bounded, like every other bulk read
56161
- * here.
56162
- */
56163
- async trackIdsMatching(terms) {
56164
- const wanted = terms.filter((t) => t.length > 0);
56165
- if (wanted.length === 0) return [];
56166
- const ids = /* @__PURE__ */ new Set();
56167
- for (const field of [
56168
- "className",
56169
- "label",
56170
- "subLabel"
56171
- ]) {
56172
- const rows = await this.store.query.query({
56173
- collection: TRACKS_COLLECTION,
56174
- filter: {
56175
- whereIn: { [field]: [...wanted] },
56176
- limit: TRACK_MATCH_SCAN_LIMIT
56177
- },
56178
- columns: ["id"]
56179
- });
56180
- for (const row of rows) ids.add(row.id);
56181
- }
56182
- return [...ids];
56183
- }
56184
56073
  async existingTrackIds(trackIds) {
56185
56074
  const PAGE = 500;
56186
56075
  const found = /* @__PURE__ */ new Set();
@@ -65068,7 +64957,7 @@ var RetrainAnnotationStore = class {
65068
64957
  };
65069
64958
  //#endregion
65070
64959
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
65071
- function clamp01$1(v) {
64960
+ function clamp01(v) {
65072
64961
  return v < 0 ? 0 : v > 1 ? 1 : v;
65073
64962
  }
65074
64963
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -65144,10 +65033,10 @@ function deriveZoneCropRect(bbox, zone) {
65144
65033
  function deriveSubjectCropRect(bbox, padding) {
65145
65034
  if (!isUsableRect(bbox)) return null;
65146
65035
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
65147
- const cropX = clamp01$1(bbox.x - bbox.w * pad);
65148
- const cropY = clamp01$1(bbox.y - bbox.h * pad);
65149
- const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
65150
- 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));
65151
65040
  const cropW = cropRight - cropX;
65152
65041
  const cropH = cropBottom - cropY;
65153
65042
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -68690,7 +68579,7 @@ async function persistTrackKeyFrame(deps, input) {
68690
68579
  meta: {
68691
68580
  ...drop.meta,
68692
68581
  nodeId: parcel.nodeId,
68693
- error: errMsg$1(err)
68582
+ error: errMsg(err)
68694
68583
  }
68695
68584
  });
68696
68585
  }
@@ -68710,7 +68599,7 @@ async function persistTrackKeyFrame(deps, input) {
68710
68599
  keyFrameMediaId
68711
68600
  };
68712
68601
  } catch (err) {
68713
- const error = errMsg$1(err);
68602
+ const error = errMsg(err);
68714
68603
  deps.logger.warn("key-frame write failed", {
68715
68604
  ...drop,
68716
68605
  meta: {
@@ -69724,7 +69613,7 @@ function isOversizeBox(bbox, frameWidth, frameHeight) {
69724
69613
  const frame = frameWidth * frameHeight;
69725
69614
  return frame > 0 && bbox.w * bbox.h / frame >= .3;
69726
69615
  }
69727
- function intersectionArea$2(a, b) {
69616
+ function intersectionArea$1(a, b) {
69728
69617
  const ix1 = Math.max(a.x, b.x);
69729
69618
  const iy1 = Math.max(a.y, b.y);
69730
69619
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -69737,7 +69626,7 @@ function detectionSwallowsEntry(det, parked) {
69737
69626
  const detArea = det.w * det.h;
69738
69627
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
69739
69628
  if (!centroidInside(det, centroid(parked))) return false;
69740
- return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69629
+ return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69741
69630
  }
69742
69631
  /**
69743
69632
  * True when `det` is a FRAGMENT of the parked object — much smaller than the
@@ -69760,7 +69649,7 @@ function entrySwallowsDetection(parked, det) {
69760
69649
  const detArea = det.w * det.h;
69761
69650
  if (entryArea <= 0 || detArea <= 0) return false;
69762
69651
  if (detArea > entryArea / 1.35) return false;
69763
- return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69652
+ return intersectionArea$1(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69764
69653
  }
69765
69654
  function parkedEntryHoldsDetection(entry, detBbox, config) {
69766
69655
  const overlap = iou(entry.bbox, detBbox);
@@ -70701,7 +70590,7 @@ var VEHICLE_CLASSES = [
70701
70590
  "vehicle"
70702
70591
  ];
70703
70592
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
70704
- function intersectionArea$1(a, b) {
70593
+ function intersectionArea(a, b) {
70705
70594
  const left = Math.max(a.x, b.x);
70706
70595
  const right = Math.min(a.x + a.w, b.x + b.w);
70707
70596
  const top = Math.max(a.y, b.y);
@@ -70718,7 +70607,7 @@ function intersectionArea$1(a, b) {
70718
70607
  function containedFraction(person, vehicle) {
70719
70608
  const area = person.w * person.h;
70720
70609
  if (area <= 0) return 0;
70721
- return intersectionArea$1(person, vehicle) / area;
70610
+ return intersectionArea(person, vehicle) / area;
70722
70611
  }
70723
70612
  /**
70724
70613
  * Is this person box contained in any vehicle box in the same frame?
@@ -74314,895 +74203,6 @@ async function reconcileGallerySamples(deps, options) {
74314
74203
  stoppedByCeiling
74315
74204
  };
74316
74205
  }
74317
- //#endregion
74318
- //#region src/pipeline-analytics/summary/summary-membership.ts
74319
- /**
74320
- * Who belongs to a summary, and which member is its primary.
74321
- *
74322
- * ## What this corrects
74323
- *
74324
- * `openOrAbsorb` absorbed ANY track born while a summary was open on the
74325
- * camera, and `primary` went to whoever opened it. Both were measured wrong on
74326
- * the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
74327
- * summaries describing "who was here together", and a dog that walked in ahead
74328
- * of its owner outranked the person behind it.
74329
- *
74330
- * The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
74331
- * sono sempre separati come eventi sintetici di sensori e pacchi."
74332
- *
74333
- * ## Why exclusion and priority live together
74334
- *
74335
- * They are the same question asked twice — "what is a subject in a group" —
74336
- * and answering it in two files is how the two drift. A class that stops being
74337
- * a member must not keep a rank, and a class that gains a rank must be a
74338
- * member.
74339
- *
74340
- * Pure, so the surprising cases are pinned rather than discovered: the dog that
74341
- * arrives first, the tie between two people, and the member whose class is not
74342
- * known yet.
74343
- */
74344
- /** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
74345
- * except this — which is the one place it must be parsed. */
74346
- var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
74347
- /**
74348
- * Classes the operator keeps OUT of a group, by name.
74349
- *
74350
- * A delivered package is an event about a PLACE, not a member of something that
74351
- * arrived together — the operator named it alongside the sensors.
74352
- */
74353
- var NON_MEMBER_CLASSES = new Set(["package"]);
74354
- /**
74355
- * Is this track a member of a group at all?
74356
- *
74357
- * An unknown class is NOT a reason to refuse. The grouping is the point and the
74358
- * label is decoration, and nothing revisits this decision — a member dropped
74359
- * here is dropped for the life of the summary.
74360
- */
74361
- function isSummaryEligible(trackId, className) {
74362
- if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
74363
- return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
74364
- }
74365
- /**
74366
- * The operator's order: person › animal › vehicle › everything else.
74367
- *
74368
- * Higher is more important. Everything unnamed — including a class that has not
74369
- * resolved yet — sits below all three rather than above them: promoting an
74370
- * unknown would let a momentary mis-read take the group's identity, and the
74371
- * group shot with it.
74372
- */
74373
- var CLASS_RANK = new Map([
74374
- ["person", 3],
74375
- ["animal", 2],
74376
- ["vehicle", 1]
74377
- ]);
74378
- function classPriority(className) {
74379
- if (className === null || className === void 0) return 0;
74380
- return CLASS_RANK.get(className) ?? 0;
74381
- }
74382
- /**
74383
- * Which member is the primary: highest class rank, ties broken by EARLIEST
74384
- * arrival.
74385
- *
74386
- * The tie-break matters more than it looks. Two people are equal, so without it
74387
- * the primary would flip on every join between equals — and the primary is what
74388
- * a consumer reads to name the group, so it would rename itself for no reason
74389
- * an operator could see.
74390
- *
74391
- * `null` for no members, never a fabricated id.
74392
- */
74393
- function primaryTrackId(members) {
74394
- let best = null;
74395
- for (const m of members) {
74396
- if (best === null) {
74397
- best = m;
74398
- continue;
74399
- }
74400
- const rank = classPriority(m.className);
74401
- const bestRank = classPriority(best.className);
74402
- if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
74403
- }
74404
- return best?.trackId ?? null;
74405
- }
74406
- //#endregion
74407
- //#region src/pipeline-analytics/summary/summary-store.ts
74408
- /**
74409
- * @durable class=ledger owner=pipeline-analytics
74410
- * write="one row per SUMMARY — a window of co-present tracks on one camera
74411
- * group. Opened by `openOrAbsorb` when the first member arrives, updated
74412
- * in place while the window absorbs more, and finalised by `seal` when the
74413
- * window closes. Per WINDOW, never per frame or per track: the sealed
74414
- * summary is what a notification rule reads."
74415
- * retention="age-keyed on `sealedAt`, swept with the rest of the analytics
74416
- * retention. An unsealed row belongs to a window still open and is never
74417
- * swept by age — it is closed or cascaded away with its members."
74418
- */
74419
- var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
74420
- /**
74421
- * @durable class=ledger owner=pipeline-analytics
74422
- * write="one row per (summary, track) membership, written as a window
74423
- * absorbs a track. The join that lets a sealed summary name its subjects
74424
- * without re-reading the track store."
74425
- * retention="cascaded — a member dies with its summary (`delete-track-cascade`
74426
- * counts them as leaves) or with the track it names. It carries no clock of
74427
- * its own: an age sweep here could outlive or predecease the summary the
74428
- * row only means anything inside of."
74429
- */
74430
- var SUMMARY_MEMBERS_COLLECTION$1 = "pipeline-analytics:summary-members";
74431
- var SUMMARIES_COLUMNS = [
74432
- {
74433
- name: "id",
74434
- type: "TEXT",
74435
- primaryKey: true,
74436
- notNull: true
74437
- },
74438
- {
74439
- name: "deviceId",
74440
- type: "INTEGER",
74441
- notNull: true
74442
- },
74443
- {
74444
- name: "openedAt",
74445
- type: "INTEGER",
74446
- notNull: true
74447
- },
74448
- {
74449
- name: "lastActiveAt",
74450
- type: "INTEGER",
74451
- notNull: true
74452
- },
74453
- {
74454
- name: "sealedAt",
74455
- type: "INTEGER"
74456
- },
74457
- (
74458
- /** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
74459
- {
74460
- name: "openDeviceId",
74461
- type: "INTEGER"
74462
- }),
74463
- {
74464
- name: "memberCount",
74465
- type: "INTEGER",
74466
- notNull: true
74467
- }
74468
- ];
74469
- var SUMMARIES_INDEXES = [(
74470
- /**
74471
- * NOT unique since 2026-09-13. A summary is a GROUP, not a camera session, so
74472
- * one camera can hold several open at once — a family at the door while
74473
- * somebody else crosses the far end of the drive, or a group that split and
74474
- * whose halves are now two groups.
74475
- *
74476
- * The uniqueness was D359's third guarantee, and the other two survive it in
74477
- * a stricter form: an open is still idempotent, now keyed on the BIRTH
74478
- * (`deviceId`, `openedAt`, founding track) rather than on the camera and the
74479
- * millisecond — two subjects detected in one frame are two groups, not one —
74480
- * and sealing is still a conditional transition, addressed by id because
74481
- * `(openDeviceId, openedAt)` no longer names one row. Those are what stopped
74482
- * the 2026-08-24 incident of eleven sealed rows for one group; "one open per
74483
- * camera" was the part that also forbade the model the operator asked for.
74484
- *
74485
- * The index itself is create-only in this system — `declareCollection` emits
74486
- * `CREATE ... IF NOT EXISTS` and nothing anywhere drops an index — so on a
74487
- * database that already has the UNIQUE version this declaration changes
74488
- * nothing until that index is dropped by hand. Until then `openOrAbsorb`
74489
- * degrades to exactly the old behaviour (the unique violation is caught and
74490
- * the newcomer joins the winner), which is why the drop is a switch and not a
74491
- * prerequisite.
74492
- */
74493
- {
74494
- name: "summaries_open_device",
74495
- columns: ["openDeviceId"]
74496
- }), {
74497
- name: "summaries_device_opened",
74498
- columns: ["deviceId", "openedAt"]
74499
- }];
74500
- var SUMMARY_MEMBERS_COLUMNS = [
74501
- {
74502
- name: "id",
74503
- type: "TEXT",
74504
- primaryKey: true,
74505
- notNull: true
74506
- },
74507
- {
74508
- name: "summaryId",
74509
- type: "TEXT",
74510
- notNull: true
74511
- },
74512
- {
74513
- name: "trackId",
74514
- type: "TEXT",
74515
- notNull: true
74516
- },
74517
- {
74518
- name: "deviceId",
74519
- type: "INTEGER",
74520
- notNull: true
74521
- },
74522
- (
74523
- /** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
74524
- {
74525
- name: "timestamp",
74526
- type: "INTEGER",
74527
- notNull: true
74528
- }),
74529
- (
74530
- /**
74531
- * What this member IS to the summary: see {@link SummaryMemberRole}.
74532
- *
74533
- * NULLABLE, and not because a role is optional. `declareCollection` is
74534
- * additive — it ALTERs the column in — and SQLite refuses to add a `NOT NULL`
74535
- * column with no default to a table that already has rows. A default would be
74536
- * worse than null: it would stamp `primary` onto every member written before
74537
- * roles existed, including the siblings, and nothing downstream could tell
74538
- * the fabricated ones from the real ones.
74539
- */
74540
- {
74541
- name: "role",
74542
- type: "TEXT"
74543
- }),
74544
- (
74545
- /**
74546
- * The member's subject class, so "which classes were present" is answerable
74547
- * without joining `tracks`. Nullable for the same additive reason, AND
74548
- * because a birth genuinely may not know a class yet — the membership is the
74549
- * point, the label is decoration.
74550
- */
74551
- {
74552
- name: "className",
74553
- type: "TEXT"
74554
- })
74555
- ];
74556
- var SUMMARY_MEMBERS_INDEXES = [
74557
- {
74558
- name: "summary_members_summary",
74559
- columns: ["summaryId"]
74560
- },
74561
- {
74562
- name: "summary_members_track",
74563
- columns: ["trackId"]
74564
- },
74565
- (
74566
- /** The filter's own index: "which groups hold an `animal`" walks members by
74567
- * class, never summaries by hope. */
74568
- {
74569
- name: "summary_members_class",
74570
- columns: ["className"]
74571
- })
74572
- ];
74573
- /**
74574
- * A stored role, or `null`. An unrecognised value reads as `null` rather than
74575
- * being passed through: a consumer switching on this must not receive a string
74576
- * outside the union just because a row was hand-edited or written by a newer
74577
- * release.
74578
- */
74579
- function readRole(raw) {
74580
- return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
74581
- }
74582
- function readClassName(raw) {
74583
- return typeof raw === "string" && raw.length > 0 ? raw : null;
74584
- }
74585
- /**
74586
- * The key of an open, derived from the birth that founded it.
74587
- *
74588
- * The founding TRACK is part of it, and that is the whole point: the key used
74589
- * to be `(deviceId, openedAt)`, which made an open idempotent — a retry writes
74590
- * the same row — but also made two subjects detected in the SAME frame one
74591
- * group, because they are born in the same millisecond. That is the ordinary
74592
- * case on a gate camera, not a race. With the track in the key a retry still
74593
- * collapses (same birth, same track) and two births stay two groups.
74594
- */
74595
- /** The widest a JS Date can name — the open end of a half-bounded window. */
74596
- var TIME_MIN = -864e13;
74597
- var TIME_MAX = 864e13;
74598
- /**
74599
- * How many rows one paged read may materialise before the merge.
74600
- *
74601
- * The page is cut AFTER ordering, so the read must be able to serve any cursor
74602
- * position — but it must also be bounded, because an unbounded read of this
74603
- * table on a busy hub is the shape D447 measured holding the settings thread
74604
- * for seconds. Summaries are far fewer than tracks by construction (14 940
74605
- * against 30 583 on this hub), so this ceiling is generous.
74606
- */
74607
- var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
74608
- /** How many member rows one filter pass may read per tier. Bounded for the same
74609
- * reason as the scan above, and generous: members are one row per subject. */
74610
- var MEMBER_MATCH_SCAN_LIMIT = 2e4;
74611
- function summaryId(deviceId, openedAt, foundingTrackId) {
74612
- return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
74613
- }
74614
- function memberRowId(summaryIdValue, trackId) {
74615
- return `${summaryIdValue}:${trackId}`;
74616
- }
74617
- function num$1(value, fallback = 0) {
74618
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
74619
- }
74620
- function toRow$1(id, data) {
74621
- return {
74622
- id,
74623
- deviceId: num$1(data["deviceId"]),
74624
- openedAt: num$1(data["openedAt"]),
74625
- lastActiveAt: num$1(data["lastActiveAt"]),
74626
- sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
74627
- memberCount: num$1(data["memberCount"])
74628
- };
74629
- }
74630
- function isUniqueViolation(err) {
74631
- return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
74632
- }
74633
- var SummaryStore = class {
74634
- store;
74635
- logger;
74636
- onEvict;
74637
- trackIdsMatching;
74638
- constructor(deps) {
74639
- this.store = deps.store;
74640
- this.logger = deps.logger;
74641
- this.onEvict = deps.onEvict;
74642
- this.trackIdsMatching = deps.trackIdsMatching;
74643
- }
74644
- static async declare(store) {
74645
- await Promise.all([store.declareCollection.mutate({
74646
- collection: SUMMARIES_COLLECTION,
74647
- columns: [...SUMMARIES_COLUMNS],
74648
- indexes: [...SUMMARIES_INDEXES]
74649
- }), store.declareCollection.mutate({
74650
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74651
- columns: [...SUMMARY_MEMBERS_COLUMNS],
74652
- indexes: [...SUMMARY_MEMBERS_INDEXES]
74653
- })]);
74654
- }
74655
- /** The camera's OPEN summary, from the row — `null` when none. */
74656
- async findOpen(deviceId) {
74657
- const first = (await this.store.query.query({
74658
- collection: SUMMARIES_COLLECTION,
74659
- filter: {
74660
- where: { openDeviceId: deviceId },
74661
- limit: 1
74662
- }
74663
- }))[0];
74664
- return first === void 0 ? null : toRow$1(first.id, first.data);
74665
- }
74666
- /**
74667
- * Every OPEN group, or just one camera's when `deviceId` is given.
74668
- *
74669
- * Plural per camera since 2026-09-13: a summary is a group, not a camera
74670
- * session, so "the open summary of this camera" stopped being a question with
74671
- * an answer. Ordered oldest-first, so a caller with no better rule takes the
74672
- * one that has been there longest — which is what `openOrAbsorb` does, and is
74673
- * byte-for-byte what shipped when only one could exist.
74674
- */
74675
- async listOpen(deviceId) {
74676
- return (await this.store.query.query({
74677
- collection: SUMMARIES_COLLECTION,
74678
- filter: {
74679
- where: deviceId === void 0 ? { sealedAt: null } : { openDeviceId: deviceId },
74680
- limit: 1e4
74681
- }
74682
- })).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data)).toSorted((a, b) => a.openedAt - b.openedAt);
74683
- }
74684
- async membersOf(summaryIdValue) {
74685
- return (await this.store.query.query({
74686
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74687
- filter: {
74688
- where: { summaryId: summaryIdValue },
74689
- limit: 1e4
74690
- }
74691
- })).map((r) => ({
74692
- summaryId: summaryIdValue,
74693
- trackId: String(r.data["trackId"] ?? ""),
74694
- deviceId: num$1(r.data["deviceId"]),
74695
- joinedAt: num$1(r.data["timestamp"]),
74696
- role: readRole(r.data["role"]),
74697
- className: readClassName(r.data["className"])
74698
- }));
74699
- }
74700
- /**
74701
- * Put a track into the camera's open summary, opening one when there is
74702
- * none. Racing opens are settled by the store: the loser's insert hits the
74703
- * unique `openDeviceId` and is re-read as an absorb.
74704
- */
74705
- /**
74706
- * Open a NEW group for this track.
74707
- *
74708
- * The unique-violation branch is kept even though `summaries_open_device` is
74709
- * no longer UNIQUE: the id is `f(deviceId, openedAt)`, so two births in the
74710
- * SAME millisecond still collide on the primary key, and the right answer
74711
- * there is still "join the row that won" — it is the same group.
74712
- */
74713
- async openNew(input) {
74714
- const id = summaryId(input.deviceId, input.at, input.trackId);
74715
- try {
74716
- await this.store.set.mutate({
74717
- collection: SUMMARIES_COLLECTION,
74718
- key: id,
74719
- value: {
74720
- deviceId: input.deviceId,
74721
- openedAt: input.at,
74722
- lastActiveAt: input.at,
74723
- sealedAt: null,
74724
- openDeviceId: input.deviceId,
74725
- memberCount: 1
74726
- }
74727
- });
74728
- } catch (err) {
74729
- if (!isUniqueViolation(err)) throw err;
74730
- const winner = (await this.listOpen(input.deviceId)).find((s) => s.id === id);
74731
- if (winner === void 0) throw err;
74732
- return this.absorbInto(winner, input);
74733
- }
74734
- await this.addMember(id, input, "primary");
74735
- return {
74736
- id,
74737
- deviceId: input.deviceId,
74738
- openedAt: input.at,
74739
- lastActiveAt: input.at,
74740
- sealedAt: null,
74741
- memberCount: 1
74742
- };
74743
- }
74744
- /**
74745
- * Join the camera's oldest open group, or open one.
74746
- *
74747
- * The default policy, for callers with no better rule. The rule that DOES
74748
- * know better — proximity — lives with the caller that can see positions, and
74749
- * reaches the store through {@link listOpen} + {@link absorbInto} instead.
74750
- */
74751
- async openOrAbsorb(input) {
74752
- const first = (await this.listOpen(input.deviceId))[0];
74753
- return first === void 0 ? this.openNew(input) : this.absorbInto(first, input);
74754
- }
74755
- async absorbInto(summary, input) {
74756
- const members = await this.membersOf(summary.id);
74757
- if (members.some((m) => m.trackId === input.trackId)) return summary;
74758
- const joined = {
74759
- trackId: input.trackId,
74760
- className: input.className ?? null,
74761
- joinedAt: input.at
74762
- };
74763
- const primary = primaryTrackId([...members.map((m) => ({
74764
- trackId: m.trackId,
74765
- className: m.className,
74766
- joinedAt: m.joinedAt
74767
- })), joined]);
74768
- await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
74769
- if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
74770
- const memberCount = members.length + 1;
74771
- const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
74772
- await this.store.updateWhere.mutate({
74773
- collection: SUMMARIES_COLLECTION,
74774
- filter: { where: {
74775
- id: summary.id,
74776
- openDeviceId: summary.deviceId
74777
- } },
74778
- data: {
74779
- memberCount,
74780
- lastActiveAt
74781
- }
74782
- });
74783
- return {
74784
- ...summary,
74785
- memberCount,
74786
- lastActiveAt
74787
- };
74788
- }
74789
- /**
74790
- * Move a set of members out of an open group into a NEW open group.
74791
- *
74792
- * The two sides of a split. The departing members keep their join times, so
74793
- * the group that leaves opens at the moment its earliest member joined the
74794
- * old one — it existed then; what changed is that we can now tell.
74795
- *
74796
- * Order is deliberate: the new rows are written BEFORE the old ones are
74797
- * dropped. A failure between the two leaves a member in both groups, which a
74798
- * reader can see and a later pass can repair; the other order loses the
74799
- * membership, and a lost member is the one wrong this store exists to
74800
- * prevent (the same reasoning as {@link demoteOtherPrimaries}).
74801
- *
74802
- * `null` when there is nothing to do: no member matched, or ALL of them did.
74803
- * Moving everybody is not a split — it would leave an empty group behind and
74804
- * a new one holding exactly what the old one held.
74805
- */
74806
- async splitOff(summary, departingTrackIds, at) {
74807
- const members = await this.membersOf(summary.id);
74808
- const leaving = members.filter((m) => departingTrackIds.includes(m.trackId));
74809
- const staying = members.filter((m) => !departingTrackIds.includes(m.trackId));
74810
- if (leaving.length === 0 || staying.length === 0) return null;
74811
- const openedAt = Math.min(...leaving.map((m) => m.joinedAt));
74812
- const founder = leaving.find((m) => m.joinedAt === openedAt) ?? leaving[0];
74813
- const id = summaryId(summary.deviceId, openedAt, founder.trackId);
74814
- await this.store.set.mutate({
74815
- collection: SUMMARIES_COLLECTION,
74816
- key: id,
74817
- value: {
74818
- deviceId: summary.deviceId,
74819
- openedAt,
74820
- lastActiveAt: summary.lastActiveAt,
74821
- sealedAt: null,
74822
- openDeviceId: summary.deviceId,
74823
- memberCount: leaving.length
74824
- }
74825
- });
74826
- const newPrimary = primaryTrackId(leaving);
74827
- await this.store.insertMany.mutate({
74828
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74829
- records: leaving.map((m) => ({
74830
- id: memberRowId(id, m.trackId),
74831
- data: {
74832
- summaryId: id,
74833
- trackId: m.trackId,
74834
- deviceId: m.deviceId,
74835
- timestamp: m.joinedAt,
74836
- role: m.trackId === newPrimary ? "primary" : "sibling",
74837
- className: m.className
74838
- }
74839
- }))
74840
- });
74841
- await this.store.deleteWhere.mutate({
74842
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74843
- filter: {
74844
- where: { summaryId: summary.id },
74845
- whereIn: { trackId: leaving.map((m) => m.trackId) }
74846
- }
74847
- });
74848
- const keptPrimary = primaryTrackId(staying);
74849
- for (const m of staying) {
74850
- const role = m.trackId === keptPrimary ? "primary" : "sibling";
74851
- if (m.role === role) continue;
74852
- await this.store.update.mutate({
74853
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74854
- id: memberRowId(summary.id, m.trackId),
74855
- data: { role }
74856
- });
74857
- }
74858
- await this.store.updateWhere.mutate({
74859
- collection: SUMMARIES_COLLECTION,
74860
- filter: { where: {
74861
- id: summary.id,
74862
- openDeviceId: summary.deviceId
74863
- } },
74864
- data: { memberCount: staying.length }
74865
- });
74866
- this.logger.info("summary: a group walked apart", {
74867
- tags: { deviceId: summary.deviceId },
74868
- meta: {
74869
- summaryId: summary.id,
74870
- bornId: id,
74871
- left: leaving.map((m) => m.trackId),
74872
- stayed: staying.map((m) => m.trackId),
74873
- at
74874
- }
74875
- });
74876
- return {
74877
- id,
74878
- deviceId: summary.deviceId,
74879
- openedAt,
74880
- lastActiveAt: summary.lastActiveAt,
74881
- sealedAt: null,
74882
- memberCount: leaving.length
74883
- };
74884
- }
74885
- /**
74886
- * Put every OTHER member back to `sibling` after a new primary took the role.
74887
- *
74888
- * Best-effort per row and loud about a row it could not move: two primaries
74889
- * is a wrong a consumer can see, but it is smaller than a lost member, so a
74890
- * failure here never propagates to the join.
74891
- */
74892
- async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
74893
- for (const m of previous) {
74894
- if (m.trackId === primaryTrack || m.role !== "primary") continue;
74895
- try {
74896
- await this.store.update.mutate({
74897
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74898
- id: memberRowId(summaryIdValue, m.trackId),
74899
- data: { role: "sibling" }
74900
- });
74901
- } catch (err) {
74902
- this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
74903
- summaryId: summaryIdValue,
74904
- trackId: m.trackId,
74905
- error: String(err)
74906
- } });
74907
- }
74908
- }
74909
- }
74910
- async addMember(summaryIdValue, input, role) {
74911
- await this.store.insertMany.mutate({
74912
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74913
- records: [{
74914
- id: memberRowId(summaryIdValue, input.trackId),
74915
- data: {
74916
- summaryId: summaryIdValue,
74917
- trackId: input.trackId,
74918
- deviceId: input.deviceId,
74919
- timestamp: input.at,
74920
- role,
74921
- className: input.className ?? null
74922
- }
74923
- }]
74924
- });
74925
- }
74926
- /**
74927
- * Seal in place. `true` when THIS call sealed it; `false` when the row was
74928
- * already sealed (or gone) — the second sealer learns, it does not write.
74929
- */
74930
- async seal(summary, sealedAt) {
74931
- const { updated } = await this.store.updateWhere.mutate({
74932
- collection: SUMMARIES_COLLECTION,
74933
- filter: { where: {
74934
- id: summary.id,
74935
- openDeviceId: summary.deviceId
74936
- } },
74937
- data: {
74938
- sealedAt,
74939
- openDeviceId: null
74940
- }
74941
- });
74942
- if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
74943
- tags: { deviceId: summary.deviceId },
74944
- meta: {
74945
- summaryId: summary.id,
74946
- updated
74947
- }
74948
- });
74949
- return updated >= 1;
74950
- }
74951
- /**
74952
- * Track cascade leaf: a member row whose track is gone is gone too, and
74953
- * the summary's count follows. The envelope itself outlives its members
74954
- * — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
74955
- */
74956
- async deleteByTracks(trackIds) {
74957
- if (trackIds.length === 0) return [{
74958
- family: "summaries",
74959
- rows: 0,
74960
- bytes: 0
74961
- }];
74962
- const members = await this.store.query.query({
74963
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74964
- filter: {
74965
- whereIn: { trackId: [...trackIds] },
74966
- limit: 1e4
74967
- }
74968
- });
74969
- if (members.length === 0) return [{
74970
- family: "summaries",
74971
- rows: 0,
74972
- bytes: 0
74973
- }];
74974
- const { deleted } = await this.store.deleteWhere.mutate({
74975
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74976
- filter: { whereIn: { trackId: [...trackIds] } }
74977
- });
74978
- const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
74979
- for (const summaryIdValue of touched) {
74980
- const remaining = await this.membersOf(summaryIdValue);
74981
- const raw = await this.store.get.query({
74982
- collection: SUMMARIES_COLLECTION,
74983
- key: summaryIdValue
74984
- });
74985
- if (typeof raw !== "object" || raw === null) continue;
74986
- const row = raw;
74987
- await this.store.updateWhere.mutate({
74988
- collection: SUMMARIES_COLLECTION,
74989
- filter: { where: {
74990
- deviceId: num$1(row["deviceId"]),
74991
- openedAt: num$1(row["openedAt"])
74992
- } },
74993
- data: { memberCount: remaining.length }
74994
- });
74995
- }
74996
- return [{
74997
- family: "summaries",
74998
- rows: deleted,
74999
- bytes: 0
75000
- }];
75001
- }
75002
- /**
75003
- * The surface's page (D359 phase 4): a camera set, newest first by
75004
- * `openedAt`, inside an optional window, bounded. Ordered and bounded HERE
75005
- * as well as in the query, so a store that ignores `orderBy` (the fake
75006
- * does) cannot hand the surface an unordered page.
75007
- */
75008
- async listRecent(input) {
75009
- if (input.deviceIds.length === 0) return [];
75010
- const matching = await this.summaryIdsMatching(input.classes);
75011
- if (matching !== null && matching.size === 0) return [];
75012
- const limit = Math.max(1, input.limit);
75013
- return (await this.store.query.query({
75014
- collection: SUMMARIES_COLLECTION,
75015
- filter: {
75016
- whereIn: { deviceId: [...input.deviceIds] },
75017
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { openedAt: [input.since ?? 0, input.until ?? Number.MAX_SAFE_INTEGER] } } : {},
75018
- orderBy: {
75019
- field: "openedAt",
75020
- direction: "desc"
75021
- },
75022
- limit
75023
- }
75024
- })).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);
75025
- }
75026
- /** One summary with its members, or `null`. */
75027
- /**
75028
- * One PAGE of summaries across a camera set, newest activity first.
75029
- *
75030
- * The same contract as `listRecentTracks` and for the same consumers: the
75031
- * reel on the cameras page and the Events gallery both ride a cursor, so a
75032
- * feed that could only answer "the last N" would make groups available in the
75033
- * timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
75034
- * ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
75035
- * second implementation of that is a second place for two feeds to disagree
75036
- * about what "newest" means.
75037
- *
75038
- * The window is on LAST ACTIVITY, not on the open: a session that started
75039
- * before the window and ran into it belongs to the window, because it
75040
- * happened there.
75041
- *
75042
- * Members for the WHOLE page come back in one read. One query per row is how
75043
- * a page of two hundred becomes two hundred round trips — the shape that made
75044
- * the old digest read two thousand tracks a camera.
75045
- */
75046
- async queryRecent(input) {
75047
- const deviceIds = [...new Set(input.deviceIds)];
75048
- if (deviceIds.length === 0) return {
75049
- summaries: [],
75050
- nextCursor: null
75051
- };
75052
- const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
75053
- const matching = await this.summaryIdsMatching(input.classes);
75054
- if (matching !== null && matching.size === 0) return {
75055
- summaries: [],
75056
- nextCursor: null
75057
- };
75058
- const ordered = (await this.store.query.query({
75059
- collection: SUMMARIES_COLLECTION,
75060
- filter: {
75061
- whereIn: { deviceId: deviceIds },
75062
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
75063
- limit: RECENT_SUMMARY_SCAN_LIMIT
75064
- }
75065
- })).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
75066
- row,
75067
- key: {
75068
- lastSeen: row.lastActiveAt,
75069
- id: row.id
75070
- }
75071
- })).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
75072
- const page = ordered.slice(0, input.limit);
75073
- const last = page.at(-1);
75074
- const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
75075
- lastSeen: last.row.lastActiveAt,
75076
- trackId: last.row.id
75077
- }) : null;
75078
- const members = await this.membersOfMany(page.map((e) => e.row.id));
75079
- return {
75080
- summaries: page.map((e) => ({
75081
- summary: e.row,
75082
- members: members.get(e.row.id) ?? []
75083
- })),
75084
- nextCursor
75085
- };
75086
- }
75087
- /**
75088
- * The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
75089
- *
75090
- * TRACKS first, then the members that name them, then the summaries. The
75091
- * label is a fact about the track and it is asked of the track — a summary
75092
- * that stored its own copy would answer with whatever was true when it
75093
- * sealed, and would never learn that a face was enrolled and a person named
75094
- * an hour later. The operator's rule: *a summary keeps the minimum
75095
- * information necessary*, and a duplicated label is not the minimum, it is a
75096
- * second truth with an expiry date.
75097
- *
75098
- * Narrows before the page is cut. Filtering the other way round — a page of
75099
- * summaries, then their members — returns a short or empty page while matches
75100
- * sit further down the list, and the operator reads that as "there were none".
75101
- */
75102
- async summaryIdsMatching(classes) {
75103
- const wanted = (classes ?? []).filter((t) => t.length > 0);
75104
- if (wanted.length === 0) return null;
75105
- const matchTracks = this.trackIdsMatching;
75106
- if (matchTracks === void 0) {
75107
- this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
75108
- return /* @__PURE__ */ new Set();
75109
- }
75110
- const trackIds = await matchTracks(wanted);
75111
- if (trackIds.length === 0) return /* @__PURE__ */ new Set();
75112
- const ids = /* @__PURE__ */ new Set();
75113
- const PAGE = 500;
75114
- for (let i = 0; i < trackIds.length; i += PAGE) {
75115
- const chunk = trackIds.slice(i, i + PAGE);
75116
- const rows = await this.store.query.query({
75117
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75118
- filter: {
75119
- whereIn: { trackId: [...chunk] },
75120
- limit: MEMBER_MATCH_SCAN_LIMIT
75121
- }
75122
- });
75123
- for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
75124
- }
75125
- ids.delete("");
75126
- return ids;
75127
- }
75128
- /** Members of many summaries, in ONE read, grouped by summary id. */
75129
- async membersOfMany(summaryIds) {
75130
- const out = /* @__PURE__ */ new Map();
75131
- if (summaryIds.length === 0) return out;
75132
- const rows = await this.store.query.query({
75133
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75134
- filter: {
75135
- whereIn: { summaryId: [...summaryIds] },
75136
- limit: 1e4
75137
- }
75138
- });
75139
- for (const r of rows) {
75140
- const summaryIdValue = String(r.data["summaryId"] ?? "");
75141
- const member = {
75142
- summaryId: summaryIdValue,
75143
- trackId: String(r.data["trackId"] ?? ""),
75144
- deviceId: num$1(r.data["deviceId"]),
75145
- joinedAt: num$1(r.data["timestamp"]),
75146
- role: readRole(r.data["role"]),
75147
- className: readClassName(r.data["className"])
75148
- };
75149
- const bucket = out.get(summaryIdValue);
75150
- if (bucket === void 0) out.set(summaryIdValue, [member]);
75151
- else bucket.push(member);
75152
- }
75153
- for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
75154
- return out;
75155
- }
75156
- async get(id) {
75157
- const raw = await this.store.get.query({
75158
- collection: SUMMARIES_COLLECTION,
75159
- key: id
75160
- });
75161
- if (typeof raw !== "object" || raw === null) return null;
75162
- return {
75163
- summary: toRow$1(id, raw),
75164
- members: await this.membersOf(id)
75165
- };
75166
- }
75167
- /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
75168
- async existingIds(ids) {
75169
- const found = /* @__PURE__ */ new Set();
75170
- await Promise.all(ids.map(async (id) => {
75171
- const raw = await this.store.get.query({
75172
- collection: SUMMARIES_COLLECTION,
75173
- key: id
75174
- });
75175
- if (typeof raw === "object" && raw !== null) found.add(id);
75176
- }));
75177
- return found;
75178
- }
75179
- /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
75180
- async evictBefore(deviceId, cutoffMs) {
75181
- const rows = await this.store.query.query({
75182
- collection: SUMMARIES_COLLECTION,
75183
- filter: {
75184
- where: { deviceId },
75185
- whereBetween: { openedAt: [0, cutoffMs - 1] },
75186
- limit: 1e4
75187
- }
75188
- });
75189
- if (rows.length === 0) return 0;
75190
- const ids = rows.map((r) => r.id);
75191
- await this.onEvict?.(ids);
75192
- await this.store.deleteWhere.mutate({
75193
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75194
- filter: { whereIn: { summaryId: ids } }
75195
- });
75196
- const { deleted } = await this.store.deleteWhere.mutate({
75197
- collection: SUMMARIES_COLLECTION,
75198
- filter: {
75199
- where: { deviceId },
75200
- whereBetween: { openedAt: [0, cutoffMs - 1] }
75201
- }
75202
- });
75203
- return deleted;
75204
- }
75205
- };
75206
74206
  /**
75207
74207
  * Where the completion marker lives.
75208
74208
  *
@@ -78476,10 +77476,6 @@ var ANALYTICS_COLLECTIONS = [
78476
77476
  collection: PLATES_COLLECTION$1,
78477
77477
  classification: derived(PLATE_COLUMNS)
78478
77478
  },
78479
- {
78480
- collection: SUMMARY_MEMBERS_COLLECTION$1,
78481
- classification: derived(SUMMARY_MEMBERS_COLUMNS)
78482
- },
78483
77479
  {
78484
77480
  collection: MEDIA_COLLECTION,
78485
77481
  classification: {
@@ -78523,13 +77519,6 @@ var ANALYTICS_COLLECTIONS = [
78523
77519
  rootId: "ops-log"
78524
77520
  }
78525
77521
  },
78526
- {
78527
- collection: SUMMARIES_COLLECTION,
78528
- classification: {
78529
- kind: "retention-root",
78530
- rootId: "summary"
78531
- }
78532
- },
78533
77522
  {
78534
77523
  collection: IDENTITIES_COLLECTION,
78535
77524
  classification: {
@@ -78718,10 +77707,6 @@ function resolveMediaOwner(row) {
78718
77707
  table: ownerKind,
78719
77708
  eventId: ownerId
78720
77709
  };
78721
- case "summary": return {
78722
- kind: "summary",
78723
- summaryId: ownerId
78724
- };
78725
77710
  case "face": return ownerId.startsWith("face-") ? {
78726
77711
  kind: "track",
78727
77712
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -78829,17 +77814,15 @@ var DEFAULT_ORPHAN_LIMITS = {
78829
77814
  function questionsFor(refs) {
78830
77815
  const trackIds = /* @__PURE__ */ new Set();
78831
77816
  const eventIds = /* @__PURE__ */ new Map();
78832
- const summaryIds = /* @__PURE__ */ new Set();
78833
77817
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
78834
77818
  else if (ref.kind === "event") {
78835
77819
  const perTable = eventIds.get(ref.table) ?? /* @__PURE__ */ new Set();
78836
77820
  perTable.add(ref.eventId);
78837
77821
  eventIds.set(ref.table, perTable);
78838
- } else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
77822
+ }
78839
77823
  return {
78840
77824
  trackIds: [...trackIds],
78841
- eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]])),
78842
- summaryIds: [...summaryIds]
77825
+ eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]]))
78843
77826
  };
78844
77827
  }
78845
77828
  /**
@@ -78883,10 +77866,9 @@ async function auditScope(scope, deps, limits, cursor) {
78883
77866
  break;
78884
77867
  }
78885
77868
  const refs = rows.map((row) => scope.resolve(row));
78886
- const { trackIds, eventIdsByTable, summaryIds } = questionsFor(refs);
77869
+ const { trackIds, eventIdsByTable } = questionsFor(refs);
78887
77870
  let liveTracks;
78888
77871
  const liveEvents = /* @__PURE__ */ new Map();
78889
- let liveSummaries;
78890
77872
  let keptMedia = /* @__PURE__ */ new Set();
78891
77873
  try {
78892
77874
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
@@ -78895,7 +77877,6 @@ async function auditScope(scope, deps, limits, cursor) {
78895
77877
  if (ids.length === 0) continue;
78896
77878
  liveEvents.set(table, await deps.liveEventIds(table, ids));
78897
77879
  }
78898
- liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
78899
77880
  } catch (err) {
78900
77881
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
78901
77882
  collection: scope.collection,
@@ -78923,7 +77904,7 @@ async function auditScope(scope, deps, limits, cursor) {
78923
77904
  tally.unresolvable += 1;
78924
77905
  continue;
78925
77906
  }
78926
- 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;
78927
77908
  const ts = num(row.data, scope.timestampField);
78928
77909
  if (ts !== null && ts > graceFloor) {
78929
77910
  tally.withinGrace += 1;
@@ -79093,11 +78074,7 @@ var ORPHAN_AUDIT_DEFAULTS = {
79093
78074
  intervalHours: OrphanAuditIntervalHoursSchema.parse(void 0),
79094
78075
  pageSize: OrphanAuditPageSizeSchema.parse(void 0)
79095
78076
  };
79096
- var RAW_DELETE_COLLECTIONS = new Set([
79097
- "pipeline-analytics:object-events",
79098
- "pipeline-analytics:plates",
79099
- "pipeline-analytics:summary-members"
79100
- ]);
78077
+ var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
79101
78078
  function buildOrphanReclaimer(ports) {
79102
78079
  return async (collection, key) => {
79103
78080
  if (collection === "pipeline-analytics:media") {
@@ -79238,14 +78215,12 @@ async function sweepTracklessRoots(deps) {
79238
78215
  cutoffMs
79239
78216
  });
79240
78217
  const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
79241
- const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
79242
78218
  const evicted = {
79243
78219
  motion: events.motion,
79244
78220
  audio: events.audio,
79245
- sensor,
79246
- summaries
78221
+ sensor
79247
78222
  };
79248
- 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);
79249
78224
  } catch (err) {
79250
78225
  deps.onError(deviceId, err);
79251
78226
  }
@@ -80022,11 +78997,12 @@ function isFullFrameBbox(bbox) {
80022
78997
  /** Throttle for the native-crop HIT/FALLBACK metric window line. */
80023
78998
  var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
80024
78999
  /**
80025
- * Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (the
80026
- * `keyFrame`/group-shot rung, ~1–1.5 MB on a 4K camera) rather than a boxed
80027
- * timeline tile or a pinned ≤640 detection frame (tens of kB). The two belong
80028
- * in different counters or the window line says "full frames" and means two
80029
- * 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.
80030
79006
  */
80031
79007
  var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
80032
79008
  function createNativeFrameTransport(deps) {
@@ -80103,8 +79079,8 @@ function createNativeFrameTransport(deps) {
80103
79079
  };
80104
79080
  };
80105
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));
80106
79083
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
80107
- const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
80108
79084
  const cropMetricLogger = logger.child("NativeCrop");
80109
79085
  let nativeHits = 0;
80110
79086
  let nativeFallbacks = 0;
@@ -80120,8 +79096,8 @@ function createNativeFrameTransport(deps) {
80120
79096
  /**
80121
79097
  * Publish the window once `request` has SETTLED — never before, or the line
80122
79098
  * carries a window whose own outcome has not been counted yet and throttles
80123
- * away the one that has. `finally`, so the deliberate throw of
80124
- * `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.
80125
79101
  */
80126
79102
  const closeWindowAfter = async (request) => {
80127
79103
  try {
@@ -80178,7 +79154,7 @@ function createNativeFrameTransport(deps) {
80178
79154
  try {
80179
79155
  return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
80180
79156
  } catch (err) {
80181
- 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) } });
80182
79158
  return null;
80183
79159
  }
80184
79160
  };
@@ -80192,7 +79168,7 @@ function createNativeFrameTransport(deps) {
80192
79168
  bumpCropMetric(false);
80193
79169
  return null;
80194
79170
  }
80195
- if (!keyFrameAcceptsTier(native.tier)) {
79171
+ if (native.tier === "ram-fullframe") {
80196
79172
  bumpCropMetric(false);
80197
79173
  cropMetricLogger.info("subject crop refused: sub-native tier (will retry, no masquerade)", {
80198
79174
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
@@ -80212,7 +79188,6 @@ function createNativeFrameTransport(deps) {
80212
79188
  return {
80213
79189
  getRemoteFrame,
80214
79190
  getNativeFullFrameRgb,
80215
- getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
80216
79191
  getNativeCropJpeg,
80217
79192
  captureCrop: createCaptureCrop({
80218
79193
  tryNativeCrop,
@@ -80226,7 +79201,7 @@ function createNativeFrameTransport(deps) {
80226
79201
  const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH).catch((err) => {
80227
79202
  logger.child("DisplayCrop").debug("fallback full-frame fetch failed — treating as a miss", { meta: {
80228
79203
  nodeId: handle.nodeId,
80229
- error: errMsg$1(err)
79204
+ error: errMsg(err)
80230
79205
  } });
80231
79206
  return null;
80232
79207
  });
@@ -80441,7 +79416,7 @@ function createParkedFrameClient(deps) {
80441
79416
  tags: { deviceId: order.deviceId },
80442
79417
  meta: {
80443
79418
  ...meta,
80444
- error: errMsg$1(err)
79419
+ error: errMsg(err)
80445
79420
  }
80446
79421
  });
80447
79422
  return false;
@@ -80521,7 +79496,7 @@ function createParkedFrameClient(deps) {
80521
79496
  trackId,
80522
79497
  kind,
80523
79498
  nodeId: known.nodeId,
80524
- error: errMsg$1(err)
79499
+ error: errMsg(err)
80525
79500
  }
80526
79501
  });
80527
79502
  return null;
@@ -80543,7 +79518,7 @@ function createParkedFrameClient(deps) {
80543
79518
  meta: {
80544
79519
  trackId,
80545
79520
  nodeId,
80546
- error: errMsg$1(err)
79521
+ error: errMsg(err)
80547
79522
  }
80548
79523
  });
80549
79524
  });
@@ -82658,7 +81633,7 @@ async function projectSensorMarkers(deps, data, timestamp) {
82658
81633
  capName: data.capName,
82659
81634
  kind: descriptor.kind,
82660
81635
  reason: "not-materialized",
82661
- ...outcome.status === "rejected" ? { error: errMsg$1(outcome.reason) } : {}
81636
+ ...outcome.status === "rejected" ? { error: errMsg(outcome.reason) } : {}
82662
81637
  });
82663
81638
  return;
82664
81639
  }
@@ -86288,947 +85263,6 @@ var RawGlobalStoreMirror = class {
86288
85263
  }
86289
85264
  };
86290
85265
  //#endregion
86291
- //#region src/pipeline-analytics/summary/group-crop.ts
86292
- /** Padding around the union, as a fraction of its own size. */
86293
- var GROUP_CROP_PADDING = .12;
86294
- /**
86295
- * Smallest crop, per axis, as a fraction of the frame.
86296
- *
86297
- * A quarter. Below that a thumbnail stops being legible at the sizes the
86298
- * gallery draws it, and the extra context costs nothing — the subjects stay the
86299
- * dominant thing in the picture at this size.
86300
- */
86301
- var MIN_CROP_FRACTION = .25;
86302
- /**
86303
- * The crop, or `null` when there is nothing to cut to — no visible member, or
86304
- * a frame with no dimensions. `null` means the caller keeps the full frame,
86305
- * which is the only honest answer when nobody could say where the group was.
86306
- */
86307
- function groupCropRect(input) {
86308
- const { frameWidth: fw, frameHeight: fh } = input;
86309
- if (fw <= 0 || fh <= 0) return null;
86310
- const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
86311
- if (boxes.length === 0) return null;
86312
- let left = Number.POSITIVE_INFINITY;
86313
- let top = Number.POSITIVE_INFINITY;
86314
- let right = Number.NEGATIVE_INFINITY;
86315
- let bottom = Number.NEGATIVE_INFINITY;
86316
- for (const b of boxes) {
86317
- left = Math.min(left, b.x);
86318
- top = Math.min(top, b.y);
86319
- right = Math.max(right, b.x + b.width);
86320
- bottom = Math.max(bottom, b.y + b.height);
86321
- }
86322
- const padX = (right - left) * GROUP_CROP_PADDING;
86323
- const padY = (bottom - top) * GROUP_CROP_PADDING;
86324
- let x0 = left - padX;
86325
- let y0 = top - padY;
86326
- let x1 = right + padX;
86327
- let y1 = bottom + padY;
86328
- const minW = fw * MIN_CROP_FRACTION;
86329
- const minH = fh * MIN_CROP_FRACTION;
86330
- if (x1 - x0 < minW) {
86331
- const cx = (x0 + x1) / 2;
86332
- x0 = cx - minW / 2;
86333
- x1 = cx + minW / 2;
86334
- }
86335
- if (y1 - y0 < minH) {
86336
- const cy = (y0 + y1) / 2;
86337
- y0 = cy - minH / 2;
86338
- y1 = cy + minH / 2;
86339
- }
86340
- const width = Math.min(x1 - x0, fw);
86341
- const height = Math.min(y1 - y0, fh);
86342
- const x = Math.max(0, Math.min(x0, fw - width));
86343
- const y = Math.max(0, Math.min(y0, fh - height));
86344
- return {
86345
- x: Math.round(x),
86346
- y: Math.round(y),
86347
- width: Math.round(width),
86348
- height: Math.round(height)
86349
- };
86350
- }
86351
- /**
86352
- * Carry a crop computed on the DETECTION frame onto the NATIVE surface.
86353
- *
86354
- * The two are different sizes by construction: detection runs on a downscaled
86355
- * frame and the group shot is fetched at up to 4096 px. Applying the rectangle
86356
- * raw would cut a corner of the picture and call it the group — the failure
86357
- * mode is silent and looks like a framing bug.
86358
- *
86359
- * Scaled per axis rather than by one factor, because the two surfaces are not
86360
- * guaranteed to share an aspect, and clamped to the native frame so rounding
86361
- * can never ask for a pixel that is not there.
86362
- */
86363
- function scaleCropToFrame(crop, from, to) {
86364
- if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
86365
- const sx = to.width / from.width;
86366
- const sy = to.height / from.height;
86367
- const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
86368
- const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
86369
- return {
86370
- x,
86371
- y,
86372
- width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
86373
- height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
86374
- };
86375
- }
86376
- //#endregion
86377
- //#region src/pipeline-analytics/summary/summary-score.ts
86378
- /** A member whose own box is covered this much by others is hidden. */
86379
- var OCCLUDED_COVER_FRACTION = .5;
86380
- /** A box covering this share of the frame scores full marks on area. */
86381
- var FULL_AREA_SHARE = .25;
86382
- var WEIGHT_CONFIDENCE = .3;
86383
- var WEIGHT_AREA = .25;
86384
- var WEIGHT_RECOGNITION = .25;
86385
- var WEIGHT_PLACEMENT = .2;
86386
- function intersectionArea(a, b) {
86387
- const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
86388
- const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
86389
- return w > 0 && h > 0 ? w * h : 0;
86390
- }
86391
- /**
86392
- * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
86393
- * capped at the own area: when two others overlap each other on top of this
86394
- * box the sum over-counts, which only ever calls a member MORE hidden, never
86395
- * less — the safe side for a shot meant to show everyone.
86396
- */
86397
- function coveredFraction(own, others) {
86398
- const area = own.width * own.height;
86399
- if (area <= 0) return 1;
86400
- const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
86401
- return Math.min(1, covered / area);
86402
- }
86403
- function clamp01(value) {
86404
- return Math.max(0, Math.min(1, value));
86405
- }
86406
- function placement(box, frameWidth, frameHeight) {
86407
- const cx = box.x + box.width / 2;
86408
- const cy = box.y + box.height / 2;
86409
- const dx = cx - frameWidth / 2;
86410
- const dy = cy - frameHeight / 2;
86411
- const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
86412
- return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
86413
- }
86414
- function memberQuality(d, frameWidth, frameHeight) {
86415
- const frameArea = frameWidth * frameHeight;
86416
- const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
86417
- return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
86418
- }
86419
- /** Score one frame for one summary. Pure; never throws on empty input. */
86420
- function scoreSummaryFrame(input) {
86421
- const present = input.detections.filter((d) => input.members.has(d.trackId));
86422
- const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
86423
- if (visible.length === 0) return {
86424
- visible: 0,
86425
- quality: 0,
86426
- crop: null
86427
- };
86428
- const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
86429
- const crop = groupCropRect({
86430
- boxes: visible.map((d) => ({
86431
- x: d.bbox.x,
86432
- y: d.bbox.y,
86433
- width: d.bbox.width,
86434
- height: d.bbox.height
86435
- })),
86436
- frameWidth: input.frameWidth,
86437
- frameHeight: input.frameHeight
86438
- });
86439
- return {
86440
- visible: visible.length,
86441
- quality,
86442
- crop
86443
- };
86444
- }
86445
- /**
86446
- * Lexicographic: more visible members wins outright; only equal counts fall
86447
- * through to quality. Positive when `a` is the better shot.
86448
- */
86449
- function compareSummaryScores(a, b) {
86450
- if (a.visible !== b.visible) return a.visible - b.visible;
86451
- return a.quality - b.quality;
86452
- }
86453
- var GroupShotTracker = class {
86454
- states = /* @__PURE__ */ new Map();
86455
- offered = 0;
86456
- materialised = 0;
86457
- byGap = 0;
86458
- byMargin = 0;
86459
- byWorse = 0;
86460
- /**
86461
- * Offer a frame. Returns `true` when it is worth materialising NOW — the
86462
- * caller's cue to fetch the pixels for THIS frame, while its handle is
86463
- * still leased.
86464
- *
86465
- * The clock is `frame.timestamp`; the caller injects no second one.
86466
- */
86467
- offer(summaryId, score, frame) {
86468
- if (score.visible === 0) return false;
86469
- this.offered += 1;
86470
- const state = this.states.get(summaryId);
86471
- if (state === void 0) {
86472
- this.states.set(summaryId, {
86473
- delivered: null,
86474
- lastAttemptAt: frame.timestamp,
86475
- unlanded: 1
86476
- });
86477
- this.materialised += 1;
86478
- return true;
86479
- }
86480
- if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
86481
- this.byGap += 1;
86482
- return false;
86483
- }
86484
- if (!this.isWorthPixels(state, score)) return false;
86485
- state.lastAttemptAt = frame.timestamp;
86486
- state.unlanded += 1;
86487
- this.materialised += 1;
86488
- return true;
86489
- }
86490
- /**
86491
- * Nothing has landed yet → keep trying (D58) until the budget is spent.
86492
- * Something has landed → beat it, by the margin on the quality tiebreak.
86493
- */
86494
- isWorthPixels(state, score) {
86495
- const delivered = state.delivered;
86496
- if (delivered === null) {
86497
- if (state.unlanded >= 4) {
86498
- this.byMargin += 1;
86499
- return false;
86500
- }
86501
- return true;
86502
- }
86503
- if (compareSummaryScores(score, delivered.score) <= 0) {
86504
- this.byWorse += 1;
86505
- return false;
86506
- }
86507
- if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
86508
- this.byMargin += 1;
86509
- return false;
86510
- }
86511
- return true;
86512
- }
86513
- /**
86514
- * A materialisation LANDED. Only this advances the baseline the next
86515
- * candidate must beat — a fetch that missed leaves the summary exactly as
86516
- * picture-less as it was, and the next frame may be worse and still worth
86517
- * taking.
86518
- */
86519
- noteLanded(summaryId, score, frame) {
86520
- const state = this.states.get(summaryId);
86521
- if (state === void 0) return;
86522
- state.delivered = {
86523
- score,
86524
- frame
86525
- };
86526
- state.unlanded = 0;
86527
- }
86528
- /** The shot on disk for this summary, as this process last saw it land. */
86529
- deliveredOf(summaryId) {
86530
- return this.states.get(summaryId)?.delivered ?? null;
86531
- }
86532
- /** A sealed or evicted summary takes no more frames; drop its state. */
86533
- forget(summaryId) {
86534
- this.states.delete(summaryId);
86535
- }
86536
- /**
86537
- * What the gate did since the last read. Shipped WITH the gate: without it
86538
- * the only evidence for a fetch that did NOT happen is hub-main's byte
86539
- * counter, which answers a different question a minute later.
86540
- */
86541
- drainCounts() {
86542
- const counts = {
86543
- offered: this.offered,
86544
- materialised: this.materialised,
86545
- byGap: this.byGap,
86546
- byMargin: this.byMargin,
86547
- byWorse: this.byWorse
86548
- };
86549
- this.offered = 0;
86550
- this.materialised = 0;
86551
- this.byGap = 0;
86552
- this.byMargin = 0;
86553
- this.byWorse = 0;
86554
- return counts;
86555
- }
86556
- get size() {
86557
- return this.states.size;
86558
- }
86559
- };
86560
- //#endregion
86561
- //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
86562
- /**
86563
- * Apply the group crop to the native surface, or hand it back untouched.
86564
- *
86565
- * Every reason to skip is a reason NOT to guess: no rectangle, no detection
86566
- * geometry to scale from, or a cropper this deployment does not have. The full
86567
- * frame is worse than a crop and far better than a wrong crop.
86568
- */
86569
- async function cropNative(native, input, deps) {
86570
- const crop = input.crop;
86571
- if (!crop || !deps.cropFrame) return native;
86572
- const fw = input.cropFrameWidth;
86573
- const fh = input.cropFrameHeight;
86574
- if (fw === void 0 || fh === void 0) return native;
86575
- const scaled = scaleCropToFrame(crop, {
86576
- width: fw,
86577
- height: fh
86578
- }, native);
86579
- if (scaled === null) return native;
86580
- if (scaled.width >= native.width && scaled.height >= native.height) return native;
86581
- try {
86582
- const cut = await deps.cropFrame(native, scaled);
86583
- if (cut === null) return native;
86584
- const expected = cut.width * cut.height * 3;
86585
- if (cut.format === "rgb" && cut.data.length !== expected) {
86586
- deps.logger.warn("group shot crop returned a surface that is not its geometry", {
86587
- tags: { deviceId: input.deviceId },
86588
- meta: {
86589
- summaryId: input.summaryId,
86590
- width: cut.width,
86591
- height: cut.height,
86592
- expectedBytes: expected,
86593
- gotBytes: cut.data.length
86594
- }
86595
- });
86596
- return native;
86597
- }
86598
- return cut;
86599
- } catch (err) {
86600
- deps.logger.warn("group shot crop failed — storing the full frame", {
86601
- tags: { deviceId: input.deviceId },
86602
- meta: {
86603
- summaryId: input.summaryId,
86604
- error: errMsg$1(err)
86605
- }
86606
- });
86607
- return native;
86608
- }
86609
- }
86610
- async function persistSummaryGroupShot(deps, input) {
86611
- const { deviceId, summaryId, frameHandle } = input;
86612
- const drop = {
86613
- tags: { deviceId },
86614
- meta: {
86615
- summaryId,
86616
- nodeId: frameHandle.nodeId
86617
- }
86618
- };
86619
- try {
86620
- const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
86621
- const native = result?.frame;
86622
- if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
86623
- deps.logger.warn("group shot native miss — the previous shot stands", {
86624
- ...drop,
86625
- meta: {
86626
- ...drop.meta,
86627
- shmId: frameHandle.shmId
86628
- }
86629
- });
86630
- return { kind: "native-miss" };
86631
- }
86632
- if (!keyFrameAcceptsTier(result.tier)) {
86633
- deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
86634
- ...drop,
86635
- meta: {
86636
- ...drop.meta,
86637
- shmId: frameHandle.shmId,
86638
- tier: result.tier
86639
- }
86640
- });
86641
- return { kind: "ram-tier" };
86642
- }
86643
- const surface = await cropNative(native, input, deps);
86644
- const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
86645
- const keyFrameMediaId = await deps.media.putReplacing({
86646
- deviceId,
86647
- ownerKind: "summary",
86648
- ownerId: summaryId,
86649
- kind: "keyFrame",
86650
- timestamp: input.timestamp,
86651
- data: keyFrame
86652
- });
86653
- try {
86654
- await deps.media.putReplacing({
86655
- deviceId,
86656
- ownerKind: "summary",
86657
- ownerId: summaryId,
86658
- kind: "keyFrameSmall",
86659
- timestamp: input.timestamp,
86660
- data: keyFrameSmall
86661
- });
86662
- } catch (err) {
86663
- deps.logger.warn("group shot web companion write failed — the native shot is stored", {
86664
- ...drop,
86665
- meta: {
86666
- ...drop.meta,
86667
- error: errMsg$1(err)
86668
- }
86669
- });
86670
- }
86671
- return {
86672
- kind: "landed",
86673
- keyFrameMediaId
86674
- };
86675
- } catch (err) {
86676
- const error = errMsg$1(err);
86677
- deps.logger.warn("group shot capture failed", {
86678
- ...drop,
86679
- meta: {
86680
- ...drop.meta,
86681
- error
86682
- }
86683
- });
86684
- return {
86685
- kind: "error",
86686
- error
86687
- };
86688
- }
86689
- }
86690
- //#endregion
86691
- //#region src/pipeline-analytics/summary/summary-composition.ts
86692
- function summaryComposition(members) {
86693
- const counts = /* @__PURE__ */ new Map();
86694
- for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
86695
- return { classes: [...counts].map(([className, count]) => ({
86696
- className,
86697
- count
86698
- })).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
86699
- }
86700
- //#endregion
86701
- //#region src/pipeline-analytics/summary/summary-member-labels.ts
86702
- function present(value) {
86703
- return typeof value === "string" && value.length > 0;
86704
- }
86705
- function withMemberLabels(members, tracks) {
86706
- const byId = new Map(tracks.map((t) => [t.trackId, t]));
86707
- return members.map((member) => {
86708
- const track = byId.get(member.trackId);
86709
- return {
86710
- ...member,
86711
- className: member.className ?? (present(track?.className) ? track.className : null),
86712
- ...present(track?.label) ? { label: track.label } : {},
86713
- ...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
86714
- };
86715
- });
86716
- }
86717
- //#endregion
86718
- //#region src/pipeline-analytics/summary/summary-proximity.ts
86719
- /**
86720
- * The measured gap, in fractions of the frame's diagonal-free normalisation
86721
- * (x by width, y by height). See the arithmetic above before moving it.
86722
- */
86723
- var DEFAULT_TOGETHER_FRACTION = .15;
86724
- /**
86725
- * Distance between two points as a fraction of the frame, or `null` when the
86726
- * frame is unknown. Each axis is normalised by its OWN dimension, so the answer
86727
- * does not change when a camera's aspect does.
86728
- */
86729
- function normalisedDistance(a, b, frame) {
86730
- if (!(frame.width > 0) || !(frame.height > 0)) return null;
86731
- const dx = (a.x - b.x) / frame.width;
86732
- const dy = (a.y - b.y) / frame.height;
86733
- return Math.sqrt(dx * dx + dy * dy);
86734
- }
86735
- //#endregion
86736
- //#region src/pipeline-analytics/summary/summary-join.ts
86737
- /**
86738
- * Which open group a newborn track joins — or none, and it opens its own.
86739
- *
86740
- * ## Why this is a separate decision from the store
86741
- *
86742
- * The store knows rows; only the caller can see where subjects ARE. Keeping the
86743
- * choice here means the store's `listOpen` + `absorbInto` stay a mechanism and
86744
- * this stays the policy, which is what let the policy change without the store
86745
- * changing underneath it.
86746
- *
86747
- * ## The two cases this exists for
86748
- *
86749
- * A summary used to be one per camera, so a subject at the far end of the drive
86750
- * was absorbed into the group at the door — a group it was never part of — and
86751
- * the halves of a group that split had nowhere to go. Both are the same need:
86752
- * a summary is a GROUP, not a camera session.
86753
- *
86754
- * ## Nearest, not first
86755
- *
86756
- * When two groups are within reach the closest wins. `listOpen` orders by
86757
- * `openedAt`, and which group opened first has nothing to do with who this
86758
- * subject is walking with — taking the first would make the answer depend on
86759
- * list order, which is the kind of tie-break that reads as random in a log.
86760
- *
86761
- * ## Knowing nothing is not a reason to take somebody in
86762
- *
86763
- * A group whose members have no position in RAM — one open across a restart —
86764
- * is skipped rather than joined. Joining on absent evidence is exactly how a
86765
- * far subject ends up in the wrong group, and nothing revisits that decision.
86766
- */
86767
- /**
86768
- * The id of the group to join, or `null` to open a new one.
86769
- *
86770
- * `null` on an unknown frame too: refusing is the recoverable direction, since
86771
- * the subject then gets its own group, while a wrong join merges two strangers.
86772
- */
86773
- function chooseGroupToJoin(subject, groups, frame, maxFraction) {
86774
- let bestId = null;
86775
- let bestDistance = Number.POSITIVE_INFINITY;
86776
- for (const g of groups) for (const m of g.memberPositions) {
86777
- const d = normalisedDistance(subject, m, frame);
86778
- if (d === null || d > maxFraction) continue;
86779
- if (d < bestDistance) {
86780
- bestDistance = d;
86781
- bestId = g.id;
86782
- }
86783
- }
86784
- return bestId;
86785
- }
86786
- //#endregion
86787
- //#region src/pipeline-analytics/summary/summary-split.ts
86788
- /**
86789
- * A group that walked apart — who leaves, and who keeps the summary.
86790
- *
86791
- * ## The case
86792
- *
86793
- * Membership is decided ONCE, at birth, by proximity (D489). That answers "who
86794
- * arrived together" and says nothing about what happens next: two people who
86795
- * meet at the gate and then go separate ways stay one group until the camera
86796
- * falls quiet, and the summary then claims they were together for the whole
86797
- * session. The operator asked for this case by name alongside the concurrent
86798
- * ones — "un gruppo che si divide" — and they are the same need, because the
86799
- * halves of a split have to be able to BE groups.
86800
- *
86801
- * ## Single linkage, deliberately
86802
- *
86803
- * A family in single file is a chain: the last of them is nowhere near the
86804
- * first, and requiring every pair to be close would tear the group apart the
86805
- * moment it grew past two. So two members are together when a chain of
86806
- * within-threshold hops connects them — the same "any, not all" rule
86807
- * {@link isTogetherWith} uses at birth, applied to the whole set.
86808
- *
86809
- * ## What an absent position means
86810
- *
86811
- * Nothing. A member whose track has closed has no live position, and that is
86812
- * not evidence it walked off — it is the ordinary end of a track. Unplaced
86813
- * members are never candidates to leave; they stay with whoever keeps the
86814
- * summary. Likewise an unknown frame yields a `null` distance, and a null is
86815
- * not "far": it produces ONE cluster, never a split (D393).
86816
- *
86817
- * ## Which side keeps the row
86818
- *
86819
- * The primary's cluster, because the summary's identity and its group shot are
86820
- * already the primary's. When the primary cannot be placed — its track closed —
86821
- * the largest cluster keeps it instead; refusing to split there would freeze
86822
- * every group the moment its primary left the frame, which is the common way
86823
- * for a group to break up in the first place.
86824
- */
86825
- /**
86826
- * Members grouped by single-linkage proximity. One cluster when nothing can be
86827
- * measured — an unknown frame is not a reason to tear a group in half.
86828
- */
86829
- function clusterByProximity(members, frame, maxFraction) {
86830
- const parent = members.map((_, i) => i);
86831
- const find = (i) => {
86832
- let root = i;
86833
- while (parent[root] !== root) root = parent[root];
86834
- return root;
86835
- };
86836
- const union = (a, b) => {
86837
- const ra = find(a);
86838
- const rb = find(b);
86839
- if (ra !== rb) parent[rb] = ra;
86840
- };
86841
- let measured = false;
86842
- for (let i = 0; i < members.length; i++) for (let j = i + 1; j < members.length; j++) {
86843
- const d = normalisedDistance(members[i].position, members[j].position, frame);
86844
- if (d === null) continue;
86845
- measured = true;
86846
- if (d <= maxFraction) union(i, j);
86847
- }
86848
- if (!measured && members.length > 1) return [members.map((m) => m.trackId)];
86849
- const byRoot = /* @__PURE__ */ new Map();
86850
- for (let i = 0; i < members.length; i++) {
86851
- const root = find(i);
86852
- const bucket = byRoot.get(root);
86853
- const trackId = members[i].trackId;
86854
- if (bucket === void 0) byRoot.set(root, [trackId]);
86855
- else bucket.push(trackId);
86856
- }
86857
- return [...byRoot.values()];
86858
- }
86859
- /**
86860
- * The departures, or `null` when the group is still whole.
86861
- *
86862
- * Order is the input's: the clusters come out in the order their first member
86863
- * appears, so a caller logging a split reads the same order twice running.
86864
- */
86865
- function planSplit(subject, frame, maxFraction) {
86866
- if (subject.placed.length < 2) return null;
86867
- const clusters = clusterByProximity(subject.placed, frame, maxFraction);
86868
- if (clusters.length < 2) return null;
86869
- const keeper = clusters.find((c) => subject.primaryTrackId !== null && c.includes(subject.primaryTrackId)) ?? largest(clusters);
86870
- return { departures: clusters.filter((c) => c !== keeper) };
86871
- }
86872
- /** The biggest cluster; ties go to the one that appeared first. */
86873
- function largest(clusters) {
86874
- let best = clusters[0];
86875
- for (const c of clusters) if (c.length > best.length) best = c;
86876
- return best;
86877
- }
86878
- function errMsg(err) {
86879
- return err instanceof Error ? err.message : String(err);
86880
- }
86881
- var SummarySealer = class {
86882
- deps;
86883
- sweepInFlight = false;
86884
- /**
86885
- * What the FRAME path may ask at frame-rate: EVERY open group of a camera,
86886
- * with the members it has seen THIS process open. Fed only by this class's
86887
- * own writes, never the authority (D224): a fresh process answers an empty
86888
- * list until a track opens, and a summary open across a restart is scored on
86889
- * the members that open after it — a weaker shot, never a wrong one.
86890
- *
86891
- * Keyed by summary id INSIDE the camera, because a camera holds as many
86892
- * groups as it has (D489). It was one entry per camera for one release, and
86893
- * that silently threw away the group shot of every group but the last to
86894
- * open — the frame path scored the newest group's members and no other.
86895
- */
86896
- openMirror = /* @__PURE__ */ new Map();
86897
- /**
86898
- * When each open group was FIRST seen divided, by summary id. Cleared the
86899
- * moment it is whole again, so a separation has to hold without interruption
86900
- * — a group that oscillates never accumulates its window.
86901
- *
86902
- * RAM only, and deliberately: after a restart a group that is still apart
86903
- * simply waits {@link SPLIT_CONFIRM_MS} again. Persisting it would buy one
86904
- * earlier split and cost a column nothing else reads.
86905
- */
86906
- apartSince = /* @__PURE__ */ new Map();
86907
- constructor(deps) {
86908
- this.deps = deps;
86909
- }
86910
- /** See {@link SUMMARY_GROUPS_ENABLED}; production never overrides it. */
86911
- get groupsEnabled() {
86912
- return this.deps.groupsEnabled ?? false;
86913
- }
86914
- /**
86915
- * The camera's open groups, oldest entry first. Empty when none is mirrored —
86916
- * which means "this process has opened none", not "the camera has none".
86917
- */
86918
- openGroupsFor(deviceId) {
86919
- const groups = this.openMirror.get(deviceId);
86920
- if (groups === void 0) return [];
86921
- return [...groups].map(([id, members]) => ({
86922
- id,
86923
- members
86924
- }));
86925
- }
86926
- /**
86927
- * Join the open group this subject is walking WITH, or open its own.
86928
- *
86929
- * Falls back to the store's own policy (the camera's oldest open group)
86930
- * whenever the evidence is missing — no birth position, no frame size, no
86931
- * member positions. That fallback is today's behaviour, so an unknowable
86932
- * case degrades to what shipped rather than to nothing.
86933
- */
86934
- async joinOrOpen(input, position) {
86935
- const frame = this.deps.frameSize(input.deviceId);
86936
- if (position === void 0 || frame === null) return await this.deps.summaries.openOrAbsorb(input);
86937
- const open = await this.deps.summaries.listOpen(input.deviceId);
86938
- if (open.length === 0) return await this.deps.summaries.openNew(input);
86939
- const groups = [];
86940
- for (const summary of open) {
86941
- const members = await this.deps.summaries.membersOf(summary.id);
86942
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
86943
- groups.push({
86944
- id: summary.id,
86945
- memberPositions: [...positions.values()]
86946
- });
86947
- }
86948
- const chosenId = chooseGroupToJoin(position, groups, frame, this.deps.togetherFraction(input.deviceId));
86949
- const chosen = open.find((s) => s.id === chosenId);
86950
- if (chosen === void 0) {
86951
- this.deps.logger.debug("summary: nobody to walk with — opening a group", {
86952
- tags: { deviceId: input.deviceId },
86953
- meta: {
86954
- trackId: input.trackId,
86955
- openGroups: open.length
86956
- }
86957
- });
86958
- return await this.deps.summaries.openNew(input);
86959
- }
86960
- return await this.deps.summaries.absorbInto(chosen, input);
86961
- }
86962
- async onTrackOpened(input) {
86963
- if (!this.groupsEnabled) return;
86964
- try {
86965
- if (!isSummaryEligible(input.trackId, input.className)) {
86966
- this.deps.logger.debug("summary: track is not a group member — left standalone", {
86967
- tags: { deviceId: input.deviceId },
86968
- meta: {
86969
- trackId: input.trackId,
86970
- className: input.className ?? null
86971
- }
86972
- });
86973
- return;
86974
- }
86975
- const at = this.deps.now();
86976
- const open = {
86977
- deviceId: input.deviceId,
86978
- trackId: input.trackId,
86979
- at,
86980
- className: input.className
86981
- };
86982
- const row = await this.joinOrOpen(open, input.position);
86983
- this.rememberMember(input.deviceId, row.id, input.trackId);
86984
- } catch (err) {
86985
- this.deps.logger.warn("summary: could not open or absorb for a track", {
86986
- tags: { deviceId: input.deviceId },
86987
- meta: {
86988
- trackId: input.trackId,
86989
- error: errMsg(err)
86990
- }
86991
- });
86992
- }
86993
- }
86994
- /** One sweep at a time (THE latch). A tick that finds one in flight returns. */
86995
- async sweep() {
86996
- if (this.sweepInFlight) return;
86997
- this.sweepInFlight = true;
86998
- try {
86999
- const mine = this.deps.analysedDeviceIds();
87000
- const open = [];
87001
- for (const deviceId of mine) open.push(...await this.deps.summaries.listOpen(deviceId));
87002
- for (const summary of open) {
87003
- const born = await this.splitIfApart(summary);
87004
- await this.sealIfDone(summary);
87005
- if (born !== null) await this.sealIfDone(born);
87006
- }
87007
- } catch (err) {
87008
- this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
87009
- } finally {
87010
- this.sweepInFlight = false;
87011
- }
87012
- }
87013
- /**
87014
- * Split an open group whose members have walked apart and STAYED apart.
87015
- *
87016
- * Returns the last group born, so the sweep can consider it for sealing in
87017
- * the same pass — a group that divided as everybody left would otherwise sit
87018
- * open until the next tick with nothing alive in it.
87019
- *
87020
- * Every failure here is swallowed into a log: a split is an improvement to a
87021
- * record that is already correct about WHO was present, and losing it is
87022
- * never worth losing the seal that follows.
87023
- */
87024
- async splitIfApart(summary) {
87025
- const { id: summaryId, deviceId } = summary;
87026
- try {
87027
- const frame = this.deps.frameSize(deviceId);
87028
- if (frame === null) {
87029
- this.apartSince.delete(summaryId);
87030
- return null;
87031
- }
87032
- const members = await this.deps.summaries.membersOf(summaryId);
87033
- if (members.length < 2) {
87034
- this.apartSince.delete(summaryId);
87035
- return null;
87036
- }
87037
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
87038
- const placed = [];
87039
- for (const m of members) {
87040
- const position = positions.get(m.trackId);
87041
- if (position !== void 0) placed.push({
87042
- trackId: m.trackId,
87043
- position
87044
- });
87045
- }
87046
- const plan = planSplit({
87047
- placed,
87048
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null
87049
- }, frame, this.deps.togetherFraction(deviceId));
87050
- if (plan === null) {
87051
- this.apartSince.delete(summaryId);
87052
- return null;
87053
- }
87054
- const firstSeen = this.apartSince.get(summaryId);
87055
- const now = this.deps.now();
87056
- if (firstSeen === void 0) {
87057
- this.apartSince.set(summaryId, now);
87058
- return null;
87059
- }
87060
- if (now - firstSeen < 0) return null;
87061
- let last = null;
87062
- let row = summary;
87063
- for (const departing of plan.departures) {
87064
- const born = await this.deps.summaries.splitOff(row, departing, now);
87065
- if (born === null) continue;
87066
- this.moveMirrorMembers(deviceId, summaryId, born.id, departing);
87067
- row = {
87068
- ...row,
87069
- memberCount: row.memberCount - departing.length
87070
- };
87071
- last = born;
87072
- }
87073
- this.apartSince.delete(summaryId);
87074
- return last;
87075
- } catch (err) {
87076
- this.deps.logger.warn("summary: could not divide a group that walked apart", {
87077
- tags: { deviceId },
87078
- meta: {
87079
- summaryId,
87080
- error: errMsg(err)
87081
- }
87082
- });
87083
- return null;
87084
- }
87085
- }
87086
- /**
87087
- * Is this group finished?
87088
- *
87089
- * Both questions are about THIS group's members, not about the camera. A
87090
- * camera holds as many groups as it has (D489), so a camera-wide gate means
87091
- * no group seals until every subject on the camera has gone — and on a busy
87092
- * camera they never all go at once. Measured: eight summaries open on one
87093
- * camera over seventeen minutes, none sealed, each new subject opening a
87094
- * ninth because a group whose members have closed has no positions left for
87095
- * the proximity rule to judge.
87096
- *
87097
- * `null` means "could not find out", and the caller treats that as NOT done —
87098
- * a read that failed must never seal a live session (D49).
87099
- */
87100
- async isGroupFinished(summary, members) {
87101
- const { id: summaryId, deviceId } = summary;
87102
- let openTracks;
87103
- try {
87104
- openTracks = await this.deps.openTrackIds(deviceId);
87105
- } catch (err) {
87106
- this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
87107
- tags: { deviceId },
87108
- meta: {
87109
- summaryId,
87110
- error: errMsg(err)
87111
- }
87112
- });
87113
- return null;
87114
- }
87115
- const stillOpen = new Set(openTracks);
87116
- if (members.some((m) => stillOpen.has(m))) return false;
87117
- let parked;
87118
- try {
87119
- parked = await this.deps.parkedTrackIds(deviceId);
87120
- } catch (err) {
87121
- this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
87122
- tags: { deviceId },
87123
- meta: {
87124
- summaryId,
87125
- error: errMsg(err)
87126
- }
87127
- });
87128
- return null;
87129
- }
87130
- return !members.some((m) => parked.has(m));
87131
- }
87132
- async sealIfDone(summary) {
87133
- const { id: summaryId, deviceId } = summary;
87134
- let members;
87135
- try {
87136
- members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
87137
- } catch (err) {
87138
- this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
87139
- tags: { deviceId },
87140
- meta: {
87141
- summaryId,
87142
- error: errMsg(err)
87143
- }
87144
- });
87145
- return;
87146
- }
87147
- if (await this.isGroupFinished(summary, members) !== true) return;
87148
- const sealedAt = this.deps.now();
87149
- try {
87150
- if (await this.deps.summaries.seal(summary, sealedAt)) {
87151
- const members = this.forgetGroup(deviceId, summaryId);
87152
- this.deps.onSealed?.(summaryId, members);
87153
- this.deps.logger.info("summary sealed", {
87154
- tags: { deviceId },
87155
- meta: {
87156
- summaryId,
87157
- sealedAt
87158
- }
87159
- });
87160
- }
87161
- } catch (err) {
87162
- this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
87163
- tags: { deviceId },
87164
- meta: {
87165
- summaryId,
87166
- error: errMsg(err)
87167
- }
87168
- });
87169
- }
87170
- }
87171
- rememberMember(deviceId, summaryIdValue, trackId) {
87172
- let groups = this.openMirror.get(deviceId);
87173
- if (groups === void 0) {
87174
- groups = /* @__PURE__ */ new Map();
87175
- this.openMirror.set(deviceId, groups);
87176
- }
87177
- const members = groups.get(summaryIdValue);
87178
- if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
87179
- else members.add(trackId);
87180
- }
87181
- /** Follows a split in the mirror, so the frame path scores each half on its
87182
- * own members from the very next frame. */
87183
- moveMirrorMembers(deviceId, fromId, toId, trackIds) {
87184
- const groups = this.openMirror.get(deviceId);
87185
- const from = groups?.get(fromId);
87186
- for (const trackId of trackIds) {
87187
- from?.delete(trackId);
87188
- this.rememberMember(deviceId, toId, trackId);
87189
- }
87190
- if (from !== void 0 && from.size === 0) groups?.delete(fromId);
87191
- }
87192
- /** Drops one group from the mirror and returns the members it held. */
87193
- forgetGroup(deviceId, summaryIdValue) {
87194
- this.apartSince.delete(summaryIdValue);
87195
- const groups = this.openMirror.get(deviceId);
87196
- const members = groups?.get(summaryIdValue) ?? /* @__PURE__ */ new Set();
87197
- groups?.delete(summaryIdValue);
87198
- if (groups !== void 0 && groups.size === 0) this.openMirror.delete(deviceId);
87199
- return members;
87200
- }
87201
- };
87202
- //#endregion
87203
- //#region src/pipeline-analytics/summary-settings.ts
87204
- /**
87205
- * Per-device summary (group) settings. Cascade: a per-device override on top of
87206
- * the global default, resolved per field — an invalid or missing value falls
87207
- * back to its default and parsing never throws. Mirrors `stationary-settings` /
87208
- * `media-settings`.
87209
- *
87210
- * The default IS the constant the proximity rule shipped with, imported and not
87211
- * copied, so an unset value and a reset value both resolve to exactly today's
87212
- * behaviour. It is per camera because a threshold belongs to whoever generates
87213
- * the event: a gate camera framing three metres of path and a drive camera
87214
- * framing twenty disagree about what "walking together" looks like in a
87215
- * fraction of the frame, and the fleet measurement behind the default is a
87216
- * median across both kinds.
87217
- */
87218
- var SummarySettingsSchema = object({
87219
- /**
87220
- * How close two subjects must be to count as one group, as a fraction of the
87221
- * frame. The floor is not zero: zero means "only at the identical pixel",
87222
- * which ends grouping on that camera while looking like a tuning choice.
87223
- * See `summary/summary-proximity.ts` for where the default came from.
87224
- */
87225
- summaryTogetherFraction: number().min(.02).max(1).default(DEFAULT_TOGETHER_FRACTION) });
87226
- var SUMMARY_DEFAULTS = { togetherFraction: SummarySettingsSchema.parse({}).summaryTogetherFraction };
87227
- function resolveSummarySettings(raw) {
87228
- const parsed = SummarySettingsSchema.shape.summaryTogetherFraction.safeParse(raw["summaryTogetherFraction"]);
87229
- return { togetherFraction: parsed.success ? parsed.data : SUMMARY_DEFAULTS.togetherFraction };
87230
- }
87231
- //#endregion
87232
85266
  //#region src/pipeline-analytics/track-retention-sweep.ts
87233
85267
  /**
87234
85268
  * Sweep every device with persisted tracks: skip retention-disabled devices,
@@ -88234,8 +86268,6 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
88234
86268
  * the others) — a longer drain would be killed mid-way and log nothing.
88235
86269
  */
88236
86270
  var SENSOR_HANDLER_DRAIN_MS = 2700;
88237
- /** Throttle for the group-shot gate's window line (D470). */
88238
- var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
88239
86271
  /**
88240
86272
  * How often the addon asks "is a gallery sample outside the cluster face
88241
86273
  * space?". A flip is rare and the check is two cheap reads — a minute keeps
@@ -88513,21 +86545,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88513
86545
  storageMigrationLeaseId = null;
88514
86546
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
88515
86547
  sensorEventStore = null;
88516
- /** The per-camera session envelope (D359). Bound with the other stores at boot. */
88517
- summaryStore = null;
88518
- /** The ONLY writer of a summary's lifecycle (D359). */
88519
- summarySealer = null;
88520
- /** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
88521
- groupShots = new GroupShotTracker();
88522
- /** Throttle for the group-shot gate's window line (D470). */
88523
- lastGroupShotMetricAt = 0;
88524
- /**
88525
- * Tracks a gallery match has named, for the group-shot score's recognition
88526
- * term. "Recognised so far" rather than "at that instant": the match arrives
88527
- * on the detail plane, after the frame; the frames that follow carry it.
88528
- * Pruned when the track's summary seals, so it is bounded by open summaries.
88529
- */
88530
- recognizedTrackIds = /* @__PURE__ */ new Set();
88531
86548
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
88532
86549
  * so the DeviceStateChanged handler stays cheap. */
88533
86550
  linkedCamerasCache = null;
@@ -88808,7 +86825,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88808
86825
  };
88809
86826
  this.ctx.logger.info("nc-action data-plane served", { meta: { served: served !== null } });
88810
86827
  } catch (err) {
88811
- 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) } });
88812
86829
  }
88813
86830
  }
88814
86831
  /** Mints a button's callback URL once the action plane is served; null until
@@ -88842,7 +86859,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88842
86859
  await store.sweep();
88843
86860
  await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
88844
86861
  } catch (err) {
88845
- 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) } });
88846
86863
  }
88847
86864
  };
88848
86865
  run();
@@ -88912,7 +86929,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88912
86929
  retentionDays: store.retentionMs === null ? "forever" : Math.round(store.retentionMs / 864e5)
88913
86930
  } });
88914
86931
  } catch (err) {
88915
- 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) } });
88916
86933
  }
88917
86934
  }
88918
86935
  /** Public base URL for event thumbnails: `/addon/<addonId>/event-media`.
@@ -89049,11 +87066,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89049
87066
  * the partition + promotion + sweep call sites — an operator change takes
89050
87067
  * effect within one SETTINGS_CACHE_TTL_MS tick without an addon restart. */
89051
87068
  stationarySettingsCache = this.settingsCaches.registerDeviceCache("stationary", SETTINGS_CACHE_TTL_MS);
89052
- /** Per-device summary (group) settings — the proximity threshold. Read
89053
- * SYNCHRONOUSLY by the sealer's birth and split paths, so it is warmed on
89054
- * the frame path like the stationary one and a cold miss answers the
89055
- * measured default. */
89056
- summarySettingsCache = this.settingsCaches.registerDeviceCache("summary", SETTINGS_CACHE_TTL_MS);
89057
87069
  /** Per-device STILLNESS SCALE cache — the measured "how slow is settled" bar
89058
87070
  * (`stillness-settings.ts`). Read once per FrameProcessor build, so this is
89059
87071
  * a cold-path cache; it exists so a calibration applied through
@@ -89183,9 +87195,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89183
87195
  * line/min/device instead of one per §5 cadence tick, see media-capture-log.ts). */
89184
87196
  mediaCaptureLog = new MediaCaptureLogAggregator();
89185
87197
  /** Per-device hit/miss window for the native `keyFrame` path (2026-08-06).
89186
- * Before it there was NO metric here at all — `fetchNativeFullFrameTiered`
89187
- * never touched `bumpCropMetric` — so whether a key-frame fix worked could
89188
- * 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`. */
89189
87202
  keyFrameCaptureLog = new KeyFrameCaptureLog();
89190
87203
  /** The registry's own liveness answer, mirrored off `ClusterTopologySnapshot`,
89191
87204
  * so a capture is never dispatched at a node the hub already called dead.
@@ -89215,7 +87228,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89215
87228
  tags: { deviceId },
89216
87229
  meta: {
89217
87230
  deviceId,
89218
- error: errMsg$1(error)
87231
+ error: errMsg(error)
89219
87232
  }
89220
87233
  });
89221
87234
  }
@@ -89244,7 +87257,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89244
87257
  tags: { deviceId },
89245
87258
  meta: {
89246
87259
  deviceId,
89247
- error: errMsg$1(error)
87260
+ error: errMsg(error)
89248
87261
  }
89249
87262
  });
89250
87263
  }
@@ -89319,12 +87332,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89319
87332
  });
89320
87333
  }
89321
87334
  captureCrop = null;
89322
- /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
89323
- * to the owning runner, PLUS the source `tier`. Captured in the async init
89324
- * block so `persistKeyFrames` can fetch the native full frame for the
89325
- * `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
89326
- * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
89327
- getNativeKeyFrameRgb = null;
89328
87335
  shuttingDown = false;
89329
87336
  /**
89330
87337
  * Sensor handlers the bus dispatched under `void` that have not finished.
@@ -89824,7 +87831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89824
87831
  logger,
89825
87832
  noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
89826
87833
  });
89827
- this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
89828
87834
  this.parkedFrames = createParkedFrameClient({
89829
87835
  api: api.pipelineRunner,
89830
87836
  logger: logger.child("ParkedFrames"),
@@ -90037,7 +88043,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90037
88043
  */
90038
88044
  armAlarmPanelDeclaration(api) {
90039
88045
  this.declareAlarmPanel(api).catch((err) => {
90040
- 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) } });
90041
88047
  });
90042
88048
  }
90043
88049
  async declareAlarmPanel(api) {
@@ -90161,7 +88167,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90161
88167
  MediaStore.declare(api.settingsStore),
90162
88168
  EventStore.declare(api.settingsStore),
90163
88169
  SensorEventStore.declare(api.settingsStore),
90164
- SummaryStore.declare(api.settingsStore),
90165
88170
  IdentityStore.declare(api.settingsStore),
90166
88171
  FaceStore.declare(api.settingsStore),
90167
88172
  PlateStore.declare(api.settingsStore),
@@ -90190,7 +88195,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90190
88195
  logger: this.ctx.logger.child("viewer-settings")
90191
88196
  });
90192
88197
  } catch (err) {
90193
- 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) } });
90194
88199
  }
90195
88200
  runLabelTierMigration({
90196
88201
  store: api.settingsStore,
@@ -90288,7 +88293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90288
88293
  try {
90289
88294
  locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
90290
88295
  } catch (err) {
90291
- 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) } });
90292
88297
  this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
90293
88298
  return DEFAULT_MEDIA_LOCATION;
90294
88299
  }
@@ -90469,49 +88474,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90469
88474
  store: api.settingsStore,
90470
88475
  logger: logger.child("SensorEventStore")
90471
88476
  });
90472
- this.summaryStore = new SummaryStore({
90473
- trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
90474
- store: api.settingsStore,
90475
- logger: this.ctx.logger.child("summary"),
90476
- onEvict: async (ids) => {
90477
- await this.mediaStore?.deleteForOwner("summary", ids);
90478
- }
90479
- });
90480
- this.summarySealer = new SummarySealer({
90481
- summaries: this.summaryStore,
90482
- analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
90483
- openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
90484
- parkedTrackIds: async (deviceId) => {
90485
- const ids = /* @__PURE__ */ new Set();
90486
- for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
90487
- return ids;
90488
- },
90489
- now: () => Date.now(),
90490
- logger: this.ctx.logger.child("summary-sealer"),
90491
- frameSize: (deviceId) => {
90492
- const d = this.lastFrameDimsByDevice.get(deviceId);
90493
- return d === void 0 ? null : {
90494
- width: d.w,
90495
- height: d.h
90496
- };
90497
- },
90498
- latestPositions: async (trackIds) => {
90499
- const out = /* @__PURE__ */ new Map();
90500
- for (const id of trackIds) {
90501
- const last = (this.trackStore?.peekActive(id))?.positions.at(-1);
90502
- if (last !== void 0) out.set(id, {
90503
- x: last.x,
90504
- y: last.y
90505
- });
90506
- }
90507
- return out;
90508
- },
90509
- togetherFraction: (deviceId) => this.summarySettingsFromCache(deviceId).togetherFraction,
90510
- onSealed: (summaryId, members) => {
90511
- this.groupShots.forget(summaryId);
90512
- for (const trackId of members) this.recognizedTrackIds.delete(trackId);
90513
- }
90514
- });
90515
88477
  this.packageDropDetector = new PackageDropDetector({
90516
88478
  events: eventStore,
90517
88479
  emit: {
@@ -90549,7 +88511,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90549
88511
  onPersisted: (ev, phase) => this.notificationCenter?.onPackageEventPersisted(ev, phase),
90550
88512
  onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
90551
88513
  scope,
90552
- error: errMsg$1(err)
88514
+ error: errMsg(err)
90553
88515
  } })
90554
88516
  });
90555
88517
  this.linkedCamerasCache = new LinkedCamerasCache({
@@ -90565,7 +88527,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90565
88527
  ...cameraId !== void 0 ? { tags: { deviceId: cameraId } } : {},
90566
88528
  meta: {
90567
88529
  scope,
90568
- error: errMsg$1(err)
88530
+ error: errMsg(err)
90569
88531
  }
90570
88532
  })
90571
88533
  });
@@ -90577,7 +88539,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90577
88539
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90578
88540
  meta: {
90579
88541
  scope,
90580
- error: errMsg$1(err)
88542
+ error: errMsg(err)
90581
88543
  }
90582
88544
  })
90583
88545
  });
@@ -90590,7 +88552,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90590
88552
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90591
88553
  meta: {
90592
88554
  scope,
90593
- error: errMsg$1(err)
88555
+ error: errMsg(err)
90594
88556
  }
90595
88557
  })
90596
88558
  });
@@ -90600,7 +88562,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90600
88562
  logger: logger.child("AudioMarkers"),
90601
88563
  onError: (scope, err) => logger.warn("audio marker error", { meta: {
90602
88564
  scope,
90603
- error: errMsg$1(err)
88565
+ error: errMsg(err)
90604
88566
  } })
90605
88567
  });
90606
88568
  const galleryVectorStore = api.vectorStore;
@@ -90711,7 +88673,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90711
88673
  score: face.matchScore ?? face.confidence,
90712
88674
  confidence: face.confidence,
90713
88675
  ...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
90714
- }, face.identityId !== void 0 ? "identified" : "evidence"),
88676
+ }),
90715
88677
  onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
90716
88678
  faceSourceFrames: new FaceSourceFrameKeeper({
90717
88679
  port: createFaceSourceParkPort({
@@ -90957,7 +88919,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90957
88919
  } catch (err) {
90958
88920
  this.ctx.logger.debug("zone ownership read failed", {
90959
88921
  tags: { deviceId },
90960
- meta: { error: errMsg$1(err) }
88922
+ meta: { error: errMsg(err) }
90961
88923
  });
90962
88924
  }
90963
88925
  return out;
@@ -91017,7 +88979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91017
88979
  } catch (err) {
91018
88980
  return {
91019
88981
  ok: false,
91020
- error: errMsg$1(err)
88982
+ error: errMsg(err)
91021
88983
  };
91022
88984
  }
91023
88985
  },
@@ -91037,7 +88999,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91037
88999
  } catch (err) {
91038
89000
  return {
91039
89001
  ok: false,
91040
- error: errMsg$1(err)
89002
+ error: errMsg(err)
91041
89003
  };
91042
89004
  }
91043
89005
  },
@@ -91239,30 +89201,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91239
89201
  confirmed: t.observations !== void 0 && t.observations >= minHits
91240
89202
  }));
91241
89203
  },
91242
- listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
91243
- const summaryStore = this.summaryStore;
91244
- if (!summaryStore) return [];
91245
- const rows = await summaryStore.listRecent({
91246
- deviceIds: [deviceId],
91247
- since: sinceMs,
91248
- until: untilMs,
91249
- limit: 500
91250
- });
91251
- const out = [];
91252
- for (const row of rows) {
91253
- const members = await summaryStore.membersOf(row.id);
91254
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
91255
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
91256
- out.push({
91257
- summaryId: row.id,
91258
- deviceId: row.deviceId,
91259
- memberTrackIds: members.map((m) => m.trackId),
91260
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
91261
- groupShotMediaId: shot?.mediaId ?? null
91262
- });
91263
- }
91264
- return out;
91265
- },
91266
89204
  listMediaInfo: async (id) => {
91267
89205
  return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
91268
89206
  mediaId: r.mediaId,
@@ -91489,7 +89427,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91489
89427
  } catch (err) {
91490
89428
  this.ctx.logger.warn("event-media resolve failed", { meta: {
91491
89429
  address,
91492
- error: errMsg$1(err)
89430
+ error: errMsg(err)
91493
89431
  } });
91494
89432
  throw err;
91495
89433
  }
@@ -91510,7 +89448,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91510
89448
  access: planeAccess("event-media")
91511
89449
  } });
91512
89450
  } catch (err) {
91513
- 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) } });
91514
89452
  }
91515
89453
  }
91516
89454
  /**
@@ -91536,7 +89474,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91536
89474
  handler
91537
89475
  }) ?? null;
91538
89476
  } catch (err) {
91539
- 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) } });
91540
89478
  }
91541
89479
  }
91542
89480
  /**
@@ -91828,7 +89766,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91828
89766
  handler
91829
89767
  }) ?? null;
91830
89768
  } catch (err) {
91831
- 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) } });
91832
89770
  }
91833
89771
  }
91834
89772
  /**
@@ -91857,7 +89795,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91857
89795
  handler
91858
89796
  }) ?? null;
91859
89797
  } catch (err) {
91860
- 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) } });
91861
89799
  }
91862
89800
  }
91863
89801
  /**
@@ -91922,7 +89860,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91922
89860
  tags: { deviceId },
91923
89861
  meta: {
91924
89862
  deviceId,
91925
- error: errMsg$1(err)
89863
+ error: errMsg(err)
91926
89864
  }
91927
89865
  });
91928
89866
  return null;
@@ -92041,7 +89979,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92041
89979
  }, RETENTION_SWEEP_INTERVAL_MS);
92042
89980
  this.ltsFlushTimer = setInterval(() => {
92043
89981
  this.lts?.flushDue().catch((err) => {
92044
- this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg$1(err) } });
89982
+ this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg(err) } });
92045
89983
  });
92046
89984
  }, LTS_FLUSH_INTERVAL_MS);
92047
89985
  this.startOrphanAuditTimer();
@@ -92062,7 +90000,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92062
90000
  } catch (err) {
92063
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: {
92064
90002
  location: ARTIFACTS_MEDIA_LOCATION,
92065
- error: errMsg$1(err),
90003
+ error: errMsg(err),
92066
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)."
92067
90005
  } });
92068
90006
  return;
@@ -92079,7 +90017,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92079
90017
  hint: "the pixels are gone for good; re-enrol these people from a photo"
92080
90018
  } });
92081
90019
  } catch (err) {
92082
- 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) } });
92083
90021
  }
92084
90022
  try {
92085
90023
  const identityStore = this.identityStore;
@@ -92111,7 +90049,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92111
90049
  maxReclaimPerRun: 16
92112
90050
  })).reclaimed > 0) await this.faceRecognizer?.refreshGallery();
92113
90051
  } catch (err) {
92114
- 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) } });
92115
90053
  }
92116
90054
  }
92117
90055
  /** Schedule the orphan audit on the operator's interval (hours). */
@@ -92253,7 +90191,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92253
90191
  engine.start();
92254
90192
  this.sceneEngine = engine;
92255
90193
  this.sceneReloadTimer = setInterval(() => {
92256
- 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) } }));
92257
90195
  }, SCENE_RELOAD_INTERVAL_MS);
92258
90196
  this.sceneReloadTimer.unref?.();
92259
90197
  }
@@ -92297,7 +90235,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92297
90235
  lng: status.location.longitude
92298
90236
  };
92299
90237
  } catch (err) {
92300
- 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) } });
92301
90239
  }
92302
90240
  this.siteCoordsCache = {
92303
90241
  value,
@@ -92316,7 +90254,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92316
90254
  } catch (err) {
92317
90255
  this.ctx.logger.debug("scene slice mirror failed", {
92318
90256
  tags: { deviceId },
92319
- meta: { error: errMsg$1(err) }
90257
+ meta: { error: errMsg(err) }
92320
90258
  });
92321
90259
  }
92322
90260
  }
@@ -92686,7 +90624,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92686
90624
  if (planeRunsStationary(source) && this.stationaryRegistry) {
92687
90625
  await this.resolveDeviceStationarySettings(deviceId);
92688
90626
  await this.resolveDevicePackageDropSettings(deviceId);
92689
- await this.resolveDeviceSummarySettings(deviceId);
92690
90627
  }
92691
90628
  const processor = await this.getOrCreateProcessor(deviceId, source);
92692
90629
  const proxy = await this.ensureProxy(deviceId);
@@ -93062,15 +90999,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93062
90999
  className: t.className
93063
91000
  }
93064
91001
  });
93065
- this.summarySealer?.onTrackOpened({
93066
- deviceId,
93067
- trackId: id,
93068
- className: t.className,
93069
- position: {
93070
- x: t.bbox.x + t.bbox.w / 2,
93071
- y: t.bbox.y + t.bbox.h / 2
93072
- }
93073
- });
93074
91002
  const startPayload = buildTrackLifecyclePayload({
93075
91003
  deviceId,
93076
91004
  trackId: id,
@@ -93306,7 +91234,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93306
91234
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
93307
91235
  else plateCrops += 1;
93308
91236
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
93309
- this.offerGroupShot(deviceId, result, frameHandle);
93310
91237
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
93311
91238
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
93312
91239
  const rasterFallbackCandidates = [];
@@ -93670,7 +91597,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93670
91597
  } catch (err) {
93671
91598
  this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
93672
91599
  nodeId,
93673
- error: errMsg$1(err)
91600
+ error: errMsg(err)
93674
91601
  } });
93675
91602
  }
93676
91603
  this.confirmationDetectorStepByNode.set(nodeId, {
@@ -93714,7 +91641,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93714
91641
  } catch (err) {
93715
91642
  this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
93716
91643
  tags: { deviceId },
93717
- meta: { error: errMsg$1(err) }
91644
+ meta: { error: errMsg(err) }
93718
91645
  });
93719
91646
  return null;
93720
91647
  }
@@ -94022,15 +91949,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94022
91949
  stationarySettingsFromCache(deviceId) {
94023
91950
  return this.stationarySettingsCache.peek(deviceId) ?? STATIONARY_DEFAULTS;
94024
91951
  }
94025
- /** Synchronous read of the per-device summary settings — warmed by
94026
- * `processFrame`. A cold miss is today's constant, never a zero: a zero
94027
- * threshold would quietly end grouping on that camera. */
94028
- summarySettingsFromCache(deviceId) {
94029
- return this.summarySettingsCache.peek(deviceId) ?? SUMMARY_DEFAULTS;
94030
- }
94031
- async resolveDeviceSummarySettings(deviceId) {
94032
- return this.summarySettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveSummarySettings));
94033
- }
94034
91952
  /** Synchronous read of the per-device package-drop settings — warmed by
94035
91953
  * `processFrame` before the promotion loop, exactly like the stationary one. */
94036
91954
  packageDropSettingsFromCache(deviceId) {
@@ -94114,12 +92032,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94114
92032
  } } : {}
94115
92033
  });
94116
92034
  }
94117
- 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));
94118
92036
  }
94119
92037
  const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
94120
92038
  if (resolved !== null && resolved.text !== void 0) {
94121
92039
  const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
94122
- 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));
94123
92041
  }
94124
92042
  } else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
94125
92043
  } catch (err) {
@@ -94128,7 +92046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94128
92046
  meta: {
94129
92047
  trackId,
94130
92048
  stepId: d.stepId,
94131
- error: errMsg$1(err)
92049
+ error: errMsg(err)
94132
92050
  }
94133
92051
  });
94134
92052
  }
@@ -94264,7 +92182,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94264
92182
  tags: { deviceId },
94265
92183
  meta: {
94266
92184
  trackId: t.trackId,
94267
- error: errMsg$1(err)
92185
+ error: errMsg(err)
94268
92186
  }
94269
92187
  });
94270
92188
  }
@@ -94312,139 +92230,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94312
92230
  });
94313
92231
  }
94314
92232
  /**
94315
- * Capture the CLEAN best full frame per given track at this best-detection
94316
- * moment and store BOTH variants (`putReplacing` → one row per track, per
94317
- * kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
94318
- * — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
94319
- * from the SAME native surface (no second fetch). No box is drawn — these are
94320
- * the clean hero images (`keyFrameSmall` is the web-friendly one).
94321
- *
94322
- * NATIVE-OR-NOTHING: a miss writes nothing (no upscale, no ≤640 stand-in) and
94323
- * the track's BOUNDED catch-up retry re-fires on a later frame. Until
94324
- * 2026-08-06 the only re-fire was the next genuine new-best PEAK, which for
94325
- * 80% of the tracks that missed never came — see `key-frame-retry.ts`. The
94326
- * `keyFrame` key is recorded on the track's resident state so the face /
94327
- * plate / object-embedding rows LINK the SAME native key frame (Design B).
94328
- * Issued in the live-frame window so the native lease is still held.
94329
- * Best-effort (D8) — a per-track failure is logged and never thrown.
94330
- *
94331
- * The capture BODY lives in `pipeline/key-frame-persist.ts`; what stays here
94332
- * is the scheduling (coalescing, priority, budget) and the bookkeeping the
94333
- * retry gate reads.
94334
- */
94335
- /**
94336
- * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
94337
- * Metadata only, at frame-rate: the frame is scored against the members the
94338
- * sealer has mirrored, and the pixels are fetched — by handle, at the owning
94339
- * node, as a native key frame — only when this frame beats the best so far.
94340
- */
94341
- offerGroupShot(deviceId, result, frameHandle) {
94342
- if (frameHandle === void 0) return;
94343
- const groups = this.summarySealer?.openGroupsFor(deviceId) ?? [];
94344
- for (const open of groups) this.offerGroupShotTo(deviceId, open, result, frameHandle);
94345
- if (groups.length > 0) this.logGroupShotWindow(deviceId);
94346
- }
94347
- /** One group's turn at the frame. Split out of {@link offerGroupShot} so the
94348
- * scoring reads the members of the group it is about, and nothing else. */
94349
- offerGroupShotTo(deviceId, open, result, frameHandle) {
94350
- const score = scoreSummaryFrame({
94351
- members: open.members,
94352
- frameWidth: result.frameWidth,
94353
- frameHeight: result.frameHeight,
94354
- detections: result.tracked.map((t) => ({
94355
- trackId: t.trackId,
94356
- bbox: {
94357
- x: t.bbox.x,
94358
- y: t.bbox.y,
94359
- width: t.bbox.w,
94360
- height: t.bbox.h
94361
- },
94362
- confidence: t.confidence,
94363
- recognized: this.recognizedTrackIds.has(t.trackId)
94364
- }))
94365
- });
94366
- const frameRef = { timestamp: result.timestamp };
94367
- if (!this.groupShots.offer(open.id, score, frameRef)) return;
94368
- const getNative = this.getNativeKeyFrameRgb;
94369
- const mediaStore = this.mediaStore;
94370
- if (!getNative || !mediaStore) return;
94371
- const summaryId = open.id;
94372
- this.captureScheduler.request({
94373
- deviceId,
94374
- kind: "keyFrame",
94375
- coalesceKey: `summary:${summaryId}:keyFrame`,
94376
- exec: () => persistSummaryGroupShot({
94377
- fetchNativeFullFrame: getNative,
94378
- encodeVariants: encodeKeyFrameVariants,
94379
- media: mediaStore,
94380
- logger: this.ctx.logger,
94381
- cropFrame: async (frame, rect) => {
94382
- const out = await extractCropRaw(frame.data, frame.width, frame.height, {
94383
- x: rect.x / frame.width,
94384
- y: rect.y / frame.height,
94385
- w: rect.width / frame.width,
94386
- h: rect.height / frame.height
94387
- });
94388
- return {
94389
- ...frame,
94390
- data: out.data,
94391
- width: out.width,
94392
- height: out.height
94393
- };
94394
- }
94395
- }, {
94396
- deviceId,
94397
- summaryId,
94398
- timestamp: result.timestamp,
94399
- frameHandle,
94400
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
94401
- crop: score.crop,
94402
- cropFrameWidth: result.frameWidth,
94403
- cropFrameHeight: result.frameHeight
94404
- })
94405
- }).then((outcome) => {
94406
- if (outcome?.kind === "landed") {
94407
- this.groupShots.noteLanded(summaryId, score, frameRef);
94408
- this.ctx.logger.debug("summary group shot landed", {
94409
- tags: { deviceId },
94410
- meta: {
94411
- summaryId,
94412
- visible: score.visible,
94413
- quality: Number(score.quality.toFixed(3))
94414
- }
94415
- });
94416
- }
94417
- }).catch(() => {});
94418
- }
94419
- /**
94420
- * The group-shot gate's window, throttled to one line per
94421
- * {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
94422
- * the device that happened to close the window — the counters are the
94423
- * gate's, not the camera's, and the tag is there because every line about a
94424
- * device carries one.
94425
- *
94426
- * `materialised` is the number of 4096-px native full-frame fetches this
94427
- * path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
94428
- */
94429
- logGroupShotWindow(deviceId) {
94430
- const now = Date.now();
94431
- if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
94432
- this.lastGroupShotMetricAt = now;
94433
- const counts = this.groupShots.drainCounts();
94434
- if (counts.offered === 0) return;
94435
- this.ctx.logger.info("group shot window", {
94436
- tags: { deviceId },
94437
- meta: {
94438
- offered: counts.offered,
94439
- materialised: counts.materialised,
94440
- suppressedByGap: counts.byGap,
94441
- suppressedByMargin: counts.byMargin,
94442
- suppressedByWorse: counts.byWorse,
94443
- openSummaries: this.groupShots.size
94444
- }
94445
- });
94446
- }
94447
- /**
94448
92233
  * Order the parcels a best-frame decision wants (D422). Skipped entirely at
94449
92234
  * a node the registry already called offline (D58) — logged on the
94450
92235
  * transition by `noteCapturesGated`, counted in the key-frame window.
@@ -94481,6 +92266,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94481
92266
  timestamp: parcel.timestamp
94482
92267
  };
94483
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
+ */
94484
92276
  async persistKeyFrames(deviceId, timestamp, captures) {
94485
92277
  const mediaStore = this.mediaStore;
94486
92278
  if (this.parkedFrames === null || !mediaStore) return;
@@ -94581,7 +92373,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94581
92373
  meta: {
94582
92374
  trackId,
94583
92375
  keyFrameMediaId,
94584
- error: errMsg$1(err)
92376
+ error: errMsg(err)
94585
92377
  }
94586
92378
  });
94587
92379
  }
@@ -94648,8 +92440,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94648
92440
  * when it wins and the frame's pixels are still reachable. Callers: the
94649
92441
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
94650
92442
  * fine-classification attribution. */
94651
- promoteSemanticRecognition(input, origin) {
94652
- if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
92443
+ promoteSemanticRecognition(input) {
94653
92444
  const ctx = this.ctxIfReady;
94654
92445
  if (ctx === null) return;
94655
92446
  promoteSemanticBestFrame({
@@ -95190,7 +92981,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95190
92981
  tags: { deviceId: input.deviceId },
95191
92982
  meta: {
95192
92983
  className: ref.className,
95193
- error: errMsg$1(err)
92984
+ error: errMsg(err)
95194
92985
  }
95195
92986
  });
95196
92987
  }
@@ -95222,7 +93013,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95222
93013
  tags: { deviceId: track.deviceId },
95223
93014
  meta: {
95224
93015
  episodeId: ref.episodeId,
95225
- error: errMsg$1(err)
93016
+ error: errMsg(err)
95226
93017
  }
95227
93018
  });
95228
93019
  }
@@ -95479,7 +93270,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95479
93270
  tags: { deviceId: input.deviceId },
95480
93271
  meta: {
95481
93272
  eventId: input.eventId,
95482
- error: errMsg$1(err)
93273
+ error: errMsg(err)
95483
93274
  }
95484
93275
  });
95485
93276
  });
@@ -95546,7 +93337,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95546
93337
  async sweepExpiredTracks() {
95547
93338
  if (this.shuttingDown || !this.trackStore) return;
95548
93339
  await this.trackCloser.sweep();
95549
- await this.summarySealer?.sweep();
95550
93340
  }
95551
93341
  /**
95552
93342
  * The follow-recordings floor per camera, read from the ARCHIVE's hour ledger
@@ -95586,7 +93376,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95586
93376
  listTrackedDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
95587
93377
  }, (half, err) => this.ctx.logger.warn("retention scope half unavailable — sweeping on the other", { meta: {
95588
93378
  half,
95589
- error: errMsg$1(err)
93379
+ error: errMsg(err)
95590
93380
  } }));
95591
93381
  }
95592
93382
  /**
@@ -95633,7 +93423,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95633
93423
  await this.runTrackRetentionSweep(deviceIds, now);
95634
93424
  const eventStore = this.eventStore;
95635
93425
  const sensorEventStore = this.sensorEventStore;
95636
- const summaryStore = this.summaryStore;
95637
93426
  await sweepTracklessRoots({
95638
93427
  deviceIds,
95639
93428
  nowMs: now,
@@ -95643,16 +93432,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95643
93432
  cutoffMs
95644
93433
  }),
95645
93434
  evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
95646
- evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
95647
93435
  onEvicted: async (deviceId, evicted, cutoffs) => {
95648
- const total = evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries;
93436
+ const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
95649
93437
  await this.eventsOpsLog?.append({
95650
93438
  op: "prune",
95651
93439
  reason: "retention",
95652
93440
  deviceId,
95653
93441
  itemsAffected: total,
95654
93442
  bytesReclaimed: 0,
95655
- 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`
95656
93444
  });
95657
93445
  this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
95658
93446
  tags: { deviceId },
@@ -95660,7 +93448,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95660
93448
  motion: evicted.motion.length,
95661
93449
  audio: evicted.audio.length,
95662
93450
  sensor: evicted.sensor,
95663
- summaries: evicted.summaries,
95664
93451
  mode: cutoffs.effectiveMode,
95665
93452
  cutoffMs: cutoffs.cutoffMs
95666
93453
  }
@@ -96614,118 +94401,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96614
94401
  async listRecentTracks(input) {
96615
94402
  return this.queryFacade.listRecentTracks(input);
96616
94403
  }
96617
- /** Newest first for a camera set; each row carries its group-shot media key. */
96618
- async listSummaries(input) {
96619
- const store = this.summaryStore;
96620
- if (!store) return { summaries: [] };
96621
- const rows = await store.listRecent({
96622
- deviceIds: input.deviceIds,
96623
- ...input.since !== void 0 ? { since: input.since } : {},
96624
- ...input.until !== void 0 ? { until: input.until } : {},
96625
- limit: input.limit ?? 50,
96626
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96627
- });
96628
- return { summaries: await this.listItemsFor(rows) };
96629
- }
96630
- /**
96631
- * Rows → what a LIST draws: the envelope plus the composition.
96632
- *
96633
- * Two bulk reads for the whole page, never per row: the members in one, their
96634
- * tracks (for the names) in one. The MEMBERS themselves stay off the list —
96635
- * a page of two hundred groups would otherwise carry a thousand records so
96636
- * the client could count them. `getSummary` is where the links live.
96637
- */
96638
- async listItemsFor(rows) {
96639
- const store = this.summaryStore;
96640
- if (!store || rows.length === 0) return [];
96641
- const members = await store.membersOfMany(rows.map((r) => r.id));
96642
- return await Promise.all(rows.map(async (row) => ({
96643
- summary: await this.toSummary(row),
96644
- ...summaryComposition(members.get(row.id) ?? [])
96645
- })));
96646
- }
96647
- /** The members of a page of summaries, with the labels a card can show —
96648
- * in ONE read of the members and ONE of their tracks. */
96649
- async labelledMembersFor(rows) {
96650
- const out = /* @__PURE__ */ new Map();
96651
- const store = this.summaryStore;
96652
- if (!store || rows.length === 0) return out;
96653
- const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
96654
- const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
96655
- let tracks = [];
96656
- try {
96657
- tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
96658
- } catch (err) {
96659
- this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
96660
- summaries: rows.length,
96661
- members: trackIds.length,
96662
- error: errMsg$1(err)
96663
- } });
96664
- }
96665
- for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
96666
- return out;
96667
- }
96668
- /**
96669
- * Rows → the shape every summary surface answers with: the envelope, its
96670
- * members, and what a thumbnail can render for each of them.
96671
- *
96672
- * THREE reads for the whole page, never per row: the members in one, their
96673
- * tracks in one, and the group shots through `toSummary`. A read per member
96674
- * is what made a sixty-group page take twenty seconds on the events feed.
96675
- */
96676
- /** ONE summary, with every member and the link each of them carries. */
96677
- async detailFor(row) {
96678
- const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
96679
- return {
96680
- summary: await this.toSummary(row),
96681
- members: labelled.map((m) => ({
96682
- trackId: m.trackId,
96683
- className: m.className,
96684
- role: m.role,
96685
- joinedAt: m.joinedAt,
96686
- ...m.label !== void 0 ? { label: m.label } : {},
96687
- ...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
96688
- }))
96689
- };
96690
- }
96691
- async listRecentSummaries(input) {
96692
- const store = this.summaryStore;
96693
- if (!store) return {
96694
- summaries: [],
96695
- nextCursor: null
96696
- };
96697
- const page = await store.queryRecent({
96698
- deviceIds: input.deviceIds,
96699
- ...input.since !== void 0 ? { since: input.since } : {},
96700
- ...input.until !== void 0 ? { until: input.until } : {},
96701
- limit: input.limit,
96702
- ...input.cursor !== void 0 ? { cursor: input.cursor } : {},
96703
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96704
- });
96705
- return {
96706
- summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
96707
- nextCursor: page.nextCursor
96708
- };
96709
- }
96710
- async getSummary(input) {
96711
- const detail = await this.summaryStore?.get(input.id);
96712
- if (!detail) return null;
96713
- return await this.detailFor(detail.summary);
96714
- }
96715
- /** The row plus its group shot: the web companion when it landed, else the native key frame. */
96716
- async toSummary(row) {
96717
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
96718
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
96719
- return {
96720
- id: row.id,
96721
- deviceId: row.deviceId,
96722
- openedAt: row.openedAt,
96723
- lastActiveAt: row.lastActiveAt,
96724
- sealedAt: row.sealedAt,
96725
- memberCount: row.memberCount,
96726
- groupShotMediaId: shot?.mediaId ?? null
96727
- };
96728
- }
96729
94404
  /**
96730
94405
  * Every event kind the device can produce: built-ins (motion + audio), the
96731
94406
  * detection classes its CONFIGURED pipeline can emit, and sensor kinds from
@@ -96754,7 +94429,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96754
94429
  tags: { deviceId: input.deviceId },
96755
94430
  meta: {
96756
94431
  scope,
96757
- error: errMsg$1(err)
94432
+ error: errMsg(err)
96758
94433
  }
96759
94434
  })
96760
94435
  }, input.deviceId);
@@ -96772,7 +94447,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96772
94447
  } catch (err) {
96773
94448
  this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
96774
94449
  tags: { deviceId },
96775
- meta: { error: errMsg$1(err) }
94450
+ meta: { error: errMsg(err) }
96776
94451
  });
96777
94452
  return NO_EMITTABLE;
96778
94453
  }
@@ -96809,7 +94484,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96809
94484
  }, deviceIds).catch((err) => {
96810
94485
  this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
96811
94486
  deviceCount: deviceIds.length,
96812
- error: errMsg$1(err)
94487
+ error: errMsg(err)
96813
94488
  } });
96814
94489
  return null;
96815
94490
  });
@@ -96827,13 +94502,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96827
94502
  tags: { deviceId },
96828
94503
  meta: {
96829
94504
  scope,
96830
- error: errMsg$1(err)
94505
+ error: errMsg(err)
96831
94506
  }
96832
94507
  })
96833
94508
  }, deviceId);
96834
94509
  }, (deviceId, err) => this.ctx.logger.warn("listEventKindsBatch: compose threw — empty vocabulary", {
96835
94510
  tags: { deviceId },
96836
- meta: { error: errMsg$1(err) }
94511
+ meta: { error: errMsg(err) }
96837
94512
  }));
96838
94513
  }
96839
94514
  /** Per-camera sensor-event history (state changes of linked devices). */
@@ -96863,7 +94538,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96863
94538
  meta: {
96864
94539
  capName: data.capName,
96865
94540
  cameraIds: [...cameras.mediaCameraIds],
96866
- error: errMsg$1(err)
94541
+ error: errMsg(err)
96867
94542
  }
96868
94543
  });
96869
94544
  return;
@@ -96907,7 +94582,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96907
94582
  meta: {
96908
94583
  kind: event.kind,
96909
94584
  linkedCameraIds: [...input.mediaCameraIds],
96910
- error: errMsg$1(err)
94585
+ error: errMsg(err)
96911
94586
  }
96912
94587
  });
96913
94588
  }
@@ -96924,7 +94599,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96924
94599
  tags: { deviceId: data.deviceId },
96925
94600
  meta: {
96926
94601
  capName: data.capName,
96927
- error: errMsg$1(err)
94602
+ error: errMsg(err)
96928
94603
  }
96929
94604
  });
96930
94605
  });
@@ -96980,7 +94655,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96980
94655
  tags: { deviceId: data.deviceId },
96981
94656
  meta: {
96982
94657
  capName: data.capName,
96983
- error: errMsg$1(err)
94658
+ error: errMsg(err)
96984
94659
  }
96985
94660
  });
96986
94661
  return NO_SENSOR_NOTIFICATION_CAMERAS;
@@ -97002,7 +94677,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97002
94677
  tags: { deviceId: data.deviceId },
97003
94678
  meta: {
97004
94679
  capName: data.capName,
97005
- error: errMsg$1(err)
94680
+ error: errMsg(err)
97006
94681
  }
97007
94682
  });
97008
94683
  }
@@ -97074,7 +94749,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97074
94749
  tags: { deviceId: data.deviceId },
97075
94750
  meta: {
97076
94751
  capName: data.capName,
97077
- error: errMsg$1(err)
94752
+ error: errMsg(err)
97078
94753
  }
97079
94754
  });
97080
94755
  }
@@ -97097,7 +94772,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97097
94772
  meta: {
97098
94773
  kind: ev.kind,
97099
94774
  sourceDeviceId: ev.sourceDeviceId,
97100
- error: errMsg$1(err)
94775
+ error: errMsg(err)
97101
94776
  }
97102
94777
  });
97103
94778
  }
@@ -97117,7 +94792,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97117
94792
  meta: {
97118
94793
  kind: ev.kind,
97119
94794
  linkedCameraIds: [...linkedCameraIds],
97120
- error: errMsg$1(err)
94795
+ error: errMsg(err)
97121
94796
  }
97122
94797
  });
97123
94798
  }
@@ -97247,7 +94922,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97247
94922
  } catch (err) {
97248
94923
  this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
97249
94924
  tags: { deviceId },
97250
- meta: { error: errMsg$1(err) }
94925
+ meta: { error: errMsg(err) }
97251
94926
  });
97252
94927
  return;
97253
94928
  }
@@ -97509,11 +95184,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97509
95184
  },
97510
95185
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
97511
95186
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
97512
- liveSummaryIds: (ids) => {
97513
- const summaries = this.summaryStore;
97514
- if (!summaries) throw new Error("summary store not built — probe unavailable");
97515
- return summaries.existingIds(ids);
97516
- },
97517
95187
  keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
97518
95188
  reclaim: buildOrphanReclaimer({
97519
95189
  deleteFace: async (faceId) => {
@@ -98120,7 +95790,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98120
95790
  nodeId
98121
95791
  };
98122
95792
  } catch (err) {
98123
- const reason = errMsg$1(err);
95793
+ const reason = errMsg(err);
98124
95794
  failures.push(`${nodeId}: ${reason}`);
98125
95795
  this.ctx.logger.warn("photo enrollment: a node could not analyse the photograph", { meta: {
98126
95796
  nodeId,
@@ -98846,7 +96516,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98846
96516
  this.unreadableClipIndexes.add(space.index);
98847
96517
  this.ctx.logger.warn("embedding.rebuildStatus: index size unreadable — reported as unknown", { meta: {
98848
96518
  index: space.index,
98849
- error: errMsg$1(err)
96519
+ error: errMsg(err)
98850
96520
  } });
98851
96521
  }
98852
96522
  indexedByIndex[space.index] = null;
@@ -99202,8 +96872,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99202
96872
  mediaStore,
99203
96873
  faceStore: this.faceStore,
99204
96874
  plateStore: this.plateStore,
99205
- objectEmbeddingStore: this.objectEmbeddingStore,
99206
- summaryStore: this.summaryStore
96875
+ objectEmbeddingStore: this.objectEmbeddingStore
99207
96876
  };
99208
96877
  const absent = absentCascadeLegs(stores);
99209
96878
  if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
@@ -99781,7 +97450,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99781
97450
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
99782
97451
  mediaId: media.mediaId,
99783
97452
  size: variant.size,
99784
- error: errMsg$1(err)
97453
+ error: errMsg(err)
99785
97454
  } });
99786
97455
  return media;
99787
97456
  }