@camstack/addon-post-analysis 1.2.297 → 1.2.299

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,6 +1,6 @@
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 partialRecord, $t as isScheduleActive, A as LockStateSchema, At as audioMetricsCapability, B as NC_DEFAULT_SNOOZE_MINUTES, Bn as defineCustomActions, Bt as embeddingRefusalOf, C as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Cn as systemEventFamilyProblems, Ct as TimelapseRuleInputSchema, D as FULL_IMAGE_BBOX, Dn as templateVarsFor, Dt as addonWidgetsSourceCapability, E as FIRST_LEVEL_MACRO_CLASSES, En as templateCarriesSummary, Et as TrackSourceSchema, F as MediaFileKindEnum, Fn as errMsg, Ft as deriveRecordingMode, G as NC_TAXONOMY, Gn as sleep$1, Gt as faceGalleryCapability, Hn as isDeviceScopedCap, I as MotionTriggeredBySchema, In as BaseAddon, It as describeSystemEventDeliveryProblem, J as NcAlarmSettingsPatchSchema, Jn as boolean, Jt as gifFromMp4Args, K as NC_TEMPLATE_VARS, Kn as _enum, Kt as failureContributionCapability, L as NC_ALARM_SYSTEM_EVENT_KINDS, Ln as CamProfileSchema, Lt as describeSystemEventFamilyProblem, M as MAX_BIRTH_DECISION_RECORDS, Mn as evaluateSensorEdge, Mt as buildEventKindDescriptor, Nn as isSourceCap, Nt as consumableLowPercentOf, O as FailureCounters, On as vectorDimFromBase64, Ot as alarmPanelCapability, P as MOTION_CLOSE_AFTER_MS, Pn as DeviceType, Pt as cosineSimilarity$1, Q as NcRulePatchSchema, Qn as object, Qt as isMediaOwnerType, R as NC_CONDITION_CATALOG, Rn as createEvent, S as EMBEDDING_REQUIREMENTS_FAILED, Sn as systemEventDeliveryProblems, St as TIMELAPSE_DENSE_FLOOR_SEC, T as EVENT_OWNER_TYPES, Tn as systemEventSummaryScheduleOf, Tt as TimelapseRuleSchema, U as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, Un as nodePin, Ut as encodeVectorBase64, V as NC_SENSOR_TRANSITION_KINDS, Vn as hydrateSchema, W as NC_TANK_SYSTEM_EVENT_KINDS, Wt as evictionPolicyOfLocation, X as NcConditionsSchema, Xn as literal, Xt as isEventOwnerType, Y as NcConditionDescriptorSchema, Yn as discriminatedUnion, Yt as isDetectionMacroClass, Z as NcRuleInputSchema, Zn as number, Zt as isKnownTemplateVar, _n as sceneMonitorCapability, a as CLIP_EMBEDDING_STEP_ID, an as notificationRulesCapability, at as NcSnoozeSuppressedSchema, b as DeclaredDevices, bn as storageOccupancyCapability, bt as SCENE_DIVERGED, c as resolveClusterModelPin, cn as pipelineAnalyticsCapability, ct as NcTemplatePreviewInputSchema, d as ArchivedDebugNoteSchema, dn as readMotionTriggeredBy, dt as OpsLogEntrySchema, en as isSystemDelivery, er as record, et as NcRuleTargetSchema, f as BaseDevice, fn as readTimelapseGeneratedAt, g as DEFAULT_EVENT_COLOR, gn as ruleBelongsToAlarm$1, gt as RetrainStatusSchema, h as CoverStateSchema, ht as RebuildObjectEmbeddingsRefusalSchema, in as mayWriteToLocation, it as NcSnoozeSchema, j as MAX_ARCHIVED_DEBUG_NOTES, jn as canonicalHash, jt as audioModeOf, k as LabelAttributionSchema, kn as zoneAnalyticsCapability, kt as assertTimelapseCadences, l as AUDIO_MACRO_LABELS, ln as plateGalleryCapability, lt as NcTemplatePreviewSchema, m as COCO_TO_MACRO, mn as resolveLocationMode, mt as RECORDING_SOURCE_CAMSTACK_ADDON, nn as isVectorIndexDimConflict, nr as unknown, nt as NcScheduleSchema, o as FACE_EMBEDDING_STEP_ID, on as occupancyScope, ot as NcSystemEventKindSchema, p as BirthDecisionRecordSchema, pn as resolveClipSearchMinScore, pt as RECORDING_EXPORT_MAX_READ_BYTES, q as NcAlarmConfigSchema, qn as array, qt as gifDeriveSpeed, rn as kebabToCamel, rr as EventCategory, rt as NcSnoozeInputSchema, s as readClusterModelPinStrict, st as NcTaxonomySchema, t as BUILTIN_CLIP_MODELS, tn as isSystemEventSummaryRule, tr as string, tt as NcRuleUpdateInputSchema, u as AlarmArmModeSchema, un as readDeviceStateFrom, ut as NcTemplateVarDescriptorSchema, v as DETECTION_MACRO_CLASSES, vn as sensorRaisedOf, vt as SCENE_DEFAULT_ANCHOR_THRESHOLD, w as EVENT_KIND_BY_CAP, wn as systemEventFilterApplies, wt as TimelapseRulePatchSchema, x as EMBEDDING_MODEL_FAILED, xn as subKindsOf, xt as SceneMonitorSchema, yn as servedTemplateVars, yt as SCENE_DEFAULT_UNCOVERED_POLICY, z as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, zn as customAction } from "../clip-model-registry-DCztUu1G.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
- import * as fs from "node:fs";
3
+ import * as fs$1 from "node:fs";
4
4
  import { promises } from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import path from "node:path";
@@ -8,6 +8,7 @@ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from
8
8
  import { execFile } from "node:child_process";
9
9
  import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
10
10
  import { pipeline } from "node:stream/promises";
11
+ import fs from "node:fs/promises";
11
12
  import sharp from "sharp";
12
13
  import os from "node:os";
13
14
  import { isDeepStrictEqual } from "node:util";
@@ -2708,8 +2709,8 @@ var NC_ARTIFACTS_INDEXES = [
2708
2709
  }
2709
2710
  ];
2710
2711
  /** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
2711
- var TIME_MAX$1 = 864e13;
2712
- var TIME_MIN$1 = -864e13;
2712
+ var TIME_MAX = 864e13;
2713
+ var TIME_MIN = -864e13;
2713
2714
  /** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
2714
2715
  * entries by default), so this only has to be comfortably above it. */
2715
2716
  var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
@@ -2949,7 +2950,7 @@ var NcArtifactIndex = class {
2949
2950
  try {
2950
2951
  const expired = await this.store.deleteWhere.mutate({
2951
2952
  collection: NC_ARTIFACTS_COLLECTION,
2952
- filter: { whereBetween: { expiresAt: [TIME_MIN$1, this.now() - 1] } }
2953
+ filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
2953
2954
  });
2954
2955
  removed += expired.deleted;
2955
2956
  if (keptIds === null) {
@@ -2991,8 +2992,8 @@ var NcArtifactIndex = class {
2991
2992
  whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
2992
2993
  if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
2993
2994
  const whereBetween = {
2994
- capturedAt: [query.fromMs ?? TIME_MIN$1, query.toMs ?? TIME_MAX$1],
2995
- expiresAt: [this.now(), TIME_MAX$1]
2995
+ capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
2996
+ expiresAt: [this.now(), TIME_MAX]
2996
2997
  };
2997
2998
  return {
2998
2999
  ...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
@@ -3152,16 +3153,16 @@ function errnoCode(err) {
3152
3153
  async function writeFileAtomic(filePath, data) {
3153
3154
  const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
3154
3155
  try {
3155
- await fs.promises.writeFile(tmp, data);
3156
+ await fs$1.promises.writeFile(tmp, data);
3156
3157
  await renameOver(tmp, filePath);
3157
3158
  } catch (err) {
3158
- await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
3159
+ await fs$1.promises.rm(tmp, { force: true }).catch(() => void 0);
3159
3160
  throw err;
3160
3161
  }
3161
3162
  }
3162
3163
  async function renameOver(tmp, filePath) {
3163
3164
  for (let attempt = 0;; attempt += 1) try {
3164
- await fs.promises.rename(tmp, filePath);
3165
+ await fs$1.promises.rename(tmp, filePath);
3165
3166
  return;
3166
3167
  } catch (err) {
3167
3168
  const code = errnoCode(err);
@@ -3170,8 +3171,8 @@ async function renameOver(tmp, filePath) {
3170
3171
  await new Promise((resolve) => setTimeout(resolve, WINDOWS_RENAME_RETRY_MS));
3171
3172
  continue;
3172
3173
  }
3173
- await fs.promises.copyFile(tmp, filePath);
3174
- await fs.promises.rm(tmp, { force: true });
3174
+ await fs$1.promises.copyFile(tmp, filePath);
3175
+ await fs$1.promises.rm(tmp, { force: true });
3175
3176
  return;
3176
3177
  }
3177
3178
  }
@@ -3246,28 +3247,28 @@ var FilesystemStorageProvider = class {
3246
3247
  location,
3247
3248
  relativePath
3248
3249
  });
3249
- await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
3250
+ await fs$1.promises.mkdir(path$1.dirname(filePath), { recursive: true });
3250
3251
  if (Buffer.isBuffer(data)) await writeFileAtomic(filePath, data);
3251
3252
  else {
3252
3253
  const tmp = path$1.join(path$1.dirname(filePath), `.${path$1.basename(filePath)}.${randomUUID()}.tmp`);
3253
3254
  try {
3254
- await pipeline(data, fs.createWriteStream(tmp));
3255
- await fs.promises.rename(tmp, filePath);
3255
+ await pipeline(data, fs$1.createWriteStream(tmp));
3256
+ await fs$1.promises.rename(tmp, filePath);
3256
3257
  } catch (err) {
3257
- await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
3258
+ await fs$1.promises.rm(tmp, { force: true }).catch(() => void 0);
3258
3259
  throw err;
3259
3260
  }
3260
3261
  }
3261
3262
  }
3262
3263
  async read({ location, relativePath }) {
3263
- return fs.promises.readFile(await this.resolve({
3264
+ return fs$1.promises.readFile(await this.resolve({
3264
3265
  location,
3265
3266
  relativePath
3266
3267
  }));
3267
3268
  }
3268
3269
  async exists({ location, relativePath }) {
3269
3270
  try {
3270
- await fs.promises.access(await this.resolve({
3271
+ await fs$1.promises.access(await this.resolve({
3271
3272
  location,
3272
3273
  relativePath
3273
3274
  }));
@@ -3281,7 +3282,7 @@ var FilesystemStorageProvider = class {
3281
3282
  if (!base) return [];
3282
3283
  const dir = prefix ? path$1.join(base, prefix) : base;
3283
3284
  try {
3284
- return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
3285
+ return (await fs$1.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
3285
3286
  } catch {
3286
3287
  return [];
3287
3288
  }
@@ -3291,19 +3292,19 @@ var FilesystemStorageProvider = class {
3291
3292
  location,
3292
3293
  relativePath
3293
3294
  });
3294
- await fs.promises.rm(filePath, { force: true });
3295
+ await fs$1.promises.rm(filePath, { force: true });
3295
3296
  }
3296
3297
  async getAvailableSpace({ location }) {
3297
3298
  const base = this.locationPaths.get(location);
3298
3299
  if (!base) return null;
3299
3300
  try {
3300
3301
  let target = base;
3301
- while (!fs.existsSync(target)) {
3302
+ while (!fs$1.existsSync(target)) {
3302
3303
  const parent = path$1.dirname(target);
3303
3304
  if (!parent || parent === target) return null;
3304
3305
  target = parent;
3305
3306
  }
3306
- const stats = await fs.promises.statfs(target);
3307
+ const stats = await fs$1.promises.statfs(target);
3307
3308
  return stats.bavail * stats.bsize;
3308
3309
  } catch {
3309
3310
  return null;
@@ -9973,14 +9974,25 @@ function occupancySubjectFromEdge(edge) {
9973
9974
  threshold: edge.threshold
9974
9975
  };
9975
9976
  }
9976
- /** Build the subject for a `package-event` evaluation (a persisted `package`
9977
- * object-event; `phase` derives from the event `state` at the call site). */
9977
+ /**
9978
+ * Build the subject for a `package-event` evaluation (a persisted `package`
9979
+ * object-event; `phase` derives from the event `state` at the call site).
9980
+ *
9981
+ * `observedAt` (D682, D684): a pick-up row's `timestamp` is when the departure
9982
+ * was CONCLUDED — five minutes of unseen observed time after the parcel went —
9983
+ * so the detector STORES the instant it happened on the row, and the footage is
9984
+ * cut around that (`media-anchor.ts`). Read from the row HERE, the one place
9985
+ * every path builds a package subject (live hook, boot reconcile, dry-run), so
9986
+ * none of them can anchor differently. `timestamp` keeps its meaning; a row
9987
+ * written before D684 has none, which the anchor reads as unknown, out loud.
9988
+ */
9978
9989
  function subjectFromPackageEvent(ev, phase) {
9979
9990
  return {
9980
9991
  kind: "package-event",
9981
9992
  recordId: String(ev.id),
9982
9993
  deviceId: ev.deviceId,
9983
9994
  timestamp: ev.timestamp,
9995
+ ...ev.observedAt !== void 0 ? { observedAt: ev.observedAt } : {},
9984
9996
  classNames: [ev.className],
9985
9997
  ...displayLabel(ev) !== void 0 ? { label: displayLabel(ev) } : {},
9986
9998
  ...recognitionPatch(ev.subLabelMeta),
@@ -10911,7 +10923,11 @@ function incomingFromAudioEvent(event, stillOwnerId) {
10911
10923
  })
10912
10924
  };
10913
10925
  }
10914
- /** Package object-event persist (delivered / picked-up). */
10926
+ /**
10927
+ * Package object-event persist (delivered / picked-up) — the live hook and the
10928
+ * boot reconcile alike. A pick-up's instant is read from the row
10929
+ * (`event.observedAt`, D684) inside `subjectFromPackageEvent`.
10930
+ */
10915
10931
  function incomingFromPackageEvent(event, phase) {
10916
10932
  return {
10917
10933
  subject: subjectFromPackageEvent(event, phase),
@@ -10921,7 +10937,8 @@ function incomingFromPackageEvent(event, phase) {
10921
10937
  tags: { deviceId: event.deviceId },
10922
10938
  meta: {
10923
10939
  eventId: event.id,
10924
- phase
10940
+ phase,
10941
+ ...event.observedAt !== void 0 ? { observedAt: event.observedAt } : {}
10925
10942
  }
10926
10943
  })
10927
10944
  };
@@ -15434,21 +15451,6 @@ var NcSummaryRuleInputSchema = object({
15434
15451
  deviceIds: DeviceIdsField,
15435
15452
  window: NcSummaryWindowSchema,
15436
15453
  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
15454
  maxTiles: MaxTilesField.default(12),
15453
15455
  maxAiImages: MaxAiImagesField.default(6),
15454
15456
  rank: NcSummaryRankSchema.default("importance"),
@@ -15483,7 +15485,6 @@ var NcSummaryRulePatchSchema = object({
15483
15485
  maxTiles: MaxTilesField.optional(),
15484
15486
  maxAiImages: MaxAiImagesField.optional(),
15485
15487
  rank: NcSummaryRankSchema.optional(),
15486
- subject: _enum(["tracks", "groups"]).optional(),
15487
15488
  barrierSeconds: BarrierSecondsField.optional(),
15488
15489
  targets: TargetsField.optional(),
15489
15490
  template: NcSummaryTemplateSchema.nullable().optional(),
@@ -19330,9 +19331,14 @@ function mediaAnchor(subject) {
19330
19331
  lagMs: subject.timestamp - observedAt
19331
19332
  };
19332
19333
  }
19334
+ function selfDatingKindOf(subject) {
19335
+ if (subject.occupancy !== void 0) return "occupancy";
19336
+ if (subject.packagePhase === "picked-up") return "package pick-up";
19337
+ return null;
19338
+ }
19333
19339
  /**
19334
19340
  * True when this anchor is the conclusion instant AND the subject was one that
19335
- * should have been able to date itself — today, an occupancy row.
19341
+ * should have been able to date itself — see {@link selfDatingKindOf}.
19336
19342
  *
19337
19343
  * Split from {@link mediaAnchor} because the anchor must stay pure of any
19338
19344
  * notion of subject KIND, while the log line that matters is exactly the one
@@ -20436,7 +20442,8 @@ var NcDispatcher = class {
20436
20442
  };
20437
20443
  const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
20438
20444
  const anchor = mediaAnchor(entry.payload.subject);
20439
- if (anchorFellBack(anchor, entry.payload.subject.occupancy !== void 0)) this.deps.logger.warn("occupancy footage anchored on the conclusion — moment unknown", {
20445
+ const selfDating = selfDatingKindOf(entry.payload.subject);
20446
+ if (anchorFellBack(anchor, selfDating !== null)) this.deps.logger.warn(`${selfDating ?? "occupancy"} footage anchored on the conclusion — moment unknown`, {
20440
20447
  tags: { deviceId: entry.payload.subject.deviceId },
20441
20448
  meta: {
20442
20449
  ruleId: entry.ruleId,
@@ -20464,7 +20471,7 @@ var NcDispatcher = class {
20464
20471
  preRollSec,
20465
20472
  retentionSec: this.ringRetentionSec(entry.payload.subject.deviceId),
20466
20473
  wanted: [...wantGif ? ["gif"] : [], ...wantMp4 ? ["video"] : []],
20467
- next: this.deps.cutRecordedClip !== void 0 ? "recorded-clip-follow-up" : "no-clip"
20474
+ next: this.recordedFollowUp(entry).kinds.length > 0 ? "recorded-clip-follow-up" : "no-clip"
20468
20475
  }
20469
20476
  });
20470
20477
  return {
@@ -20518,6 +20525,13 @@ var NcDispatcher = class {
20518
20525
  * alert for a complete one, which is precisely the wrong trade for the
20519
20526
  * notification that tells an operator someone is at the door.
20520
20527
  *
20528
+ * A GIF rule is served here too (D682): a package pick-up is concluded five
20529
+ * minutes after it happened, so a gif anchored on the pick-up is always
20530
+ * outside the ring. The gif is derived from THIS cut's bytes
20531
+ * (`deriveGifFromClip`) — one cut, never a second renderer — and a moment the
20532
+ * recordings cannot serve ships NO gif, by named reason, rather than a gif of
20533
+ * the wrong time.
20534
+ *
20521
20535
  * So the alert's latency is UNCHANGED, and **the clip is attached later**, as
20522
20536
  * a follow-up send to the same target carrying the same `tag` — a phone
20523
20537
  * collapses the two, so what the operator sees is the notification gaining
@@ -20540,7 +20554,9 @@ var NcDispatcher = class {
20540
20554
  */
20541
20555
  startRecordedFootage(entry, target, notification) {
20542
20556
  const cut = this.deps.cutRecordedClip;
20543
- if (cut === void 0 || entry.payload.mediaClip !== true) return;
20557
+ const plan = this.recordedFollowUp(entry);
20558
+ const wanted = plan.kinds;
20559
+ if (cut === void 0 || wanted.length === 0 && plan.gifRefused === void 0 && plan.videoRefused === void 0) return;
20544
20560
  const preRollSec = entry.payload.mediaClipPreRollSec ?? 4;
20545
20561
  const anchor = mediaAnchor(entry.payload.subject);
20546
20562
  if (footageSourceFor({
@@ -20550,34 +20566,70 @@ var NcDispatcher = class {
20550
20566
  ringRetentionSec: this.ringRetentionSec(entry.payload.subject.deviceId)
20551
20567
  }) !== "recordings") return;
20552
20568
  const deviceId = entry.payload.subject.deviceId;
20569
+ if (plan.gifRefused !== void 0) this.deps.logger.info("recorded gif follow-up: none — the gif is not cut from the recordings", {
20570
+ tags: { deviceId },
20571
+ meta: {
20572
+ ruleId: entry.ruleId,
20573
+ eventId: entry.recordId,
20574
+ targetId: target.id,
20575
+ reason: plan.gifRefused,
20576
+ anchoredOn: anchor.source,
20577
+ ...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
20578
+ next: wanted
20579
+ }
20580
+ });
20581
+ if (plan.videoRefused !== void 0) this.deps.logger.info("recorded video follow-up: none — the video is not cut from the recordings", {
20582
+ tags: { deviceId },
20583
+ meta: {
20584
+ ruleId: entry.ruleId,
20585
+ eventId: entry.recordId,
20586
+ targetId: target.id,
20587
+ reason: plan.videoRefused,
20588
+ anchoredOn: anchor.source,
20589
+ ...anchor.fallback !== void 0 ? { anchorFallback: anchor.fallback } : {},
20590
+ next: wanted
20591
+ }
20592
+ });
20593
+ if (wanted.length === 0) return;
20594
+ const videoSpeed = entry.payload.mediaClipSpeed;
20553
20595
  const pending = (async () => {
20554
20596
  const result = await cut({
20555
20597
  deviceId,
20556
20598
  aroundMs: anchor.atMs,
20557
20599
  preRollSec,
20558
20600
  postRollSec: entry.payload.mediaClipPostRollSec ?? 6,
20559
- ...entry.payload.mediaClipSpeed !== void 0 ? { speed: entry.payload.mediaClipSpeed } : {},
20601
+ ...videoSpeed !== void 0 ? { speed: videoSpeed } : {},
20560
20602
  ruleId: entry.ruleId,
20561
20603
  recordId: entry.recordId
20562
20604
  });
20563
20605
  if (!result.ok) {
20564
- this.deps.logger.info("recorded clip follow-up: none — this delivery keeps no clip", {
20606
+ this.deps.logger.info("recorded footage follow-up: none — the recorded cut failed", {
20565
20607
  tags: { deviceId },
20566
20608
  meta: {
20567
20609
  ruleId: entry.ruleId,
20568
20610
  eventId: entry.recordId,
20569
20611
  targetId: target.id,
20570
- reason: result.reason
20612
+ reason: result.reason,
20613
+ wanted
20571
20614
  }
20572
20615
  });
20573
20616
  return;
20574
20617
  }
20575
- const withUrls = await this.withArtifactUrls(entry, [{
20576
- mediaType: "video",
20577
- bytes: result.mp4,
20578
- mime: "video/mp4",
20579
- name: "event.mp4"
20580
- }]);
20618
+ const attachments = await this.recordedAttachments(entry, wanted, result.mp4);
20619
+ if (attachments.length === 0) {
20620
+ this.deps.logger.info("recorded clip follow-up: none — the gif could not be derived", {
20621
+ tags: { deviceId },
20622
+ meta: {
20623
+ ruleId: entry.ruleId,
20624
+ eventId: entry.recordId,
20625
+ targetId: target.id,
20626
+ reason: "gif-derive-failed",
20627
+ wanted
20628
+ }
20629
+ });
20630
+ return;
20631
+ }
20632
+ const withUrls = await this.withArtifactUrls(entry, attachments);
20581
20633
  const sent = await this.deps.send({
20582
20634
  addonId: target.addonId,
20583
20635
  targetId: target.id,
@@ -20606,6 +20658,8 @@ var NcDispatcher = class {
20606
20658
  target: target.name,
20607
20659
  profile: result.profile,
20608
20660
  bytes: result.mp4.byteLength,
20661
+ attachments: attachments.map((a) => a.mediaType),
20662
+ anchoredOn: anchor.source,
20609
20663
  lagMs: this.now() - entry.updatedAt
20610
20664
  }
20611
20665
  });
@@ -20625,6 +20679,84 @@ var NcDispatcher = class {
20625
20679
  });
20626
20680
  }
20627
20681
  /**
20682
+ * What a recorded follow-up for this row would CARRY — the one answer both
20683
+ * the refusal line (`next`) and the follow-up read, so the log cannot promise
20684
+ * a follow-up the dispatcher will not send.
20685
+ *
20686
+ * A video when the rule asked for one. A gif only for a package PICK-UP
20687
+ * whose moment is KNOWN (D682): the gif is derived from the recorded cut's
20688
+ * own bytes, the same way the broker derives it from a ring cut — one cut,
20689
+ * two containers, never a second renderer.
20690
+ *
20691
+ * Narrow on purpose. A pick-up is the one kind whose moment is ALWAYS outside
20692
+ * the ring; every other kind keeps the gif behaviour it had (the ring's, or
20693
+ * none) rather than silently gaining a recorder export per notification. And
20694
+ * a pick-up that fell back to its conclusion gets no recorded footage at all,
20695
+ * gif OR video — cutting the recordings there is exactly the doormat D682
20696
+ * removed (see {@link NcRecordedGifRefusal}, {@link NcRecordedVideoRefusal}).
20697
+ */
20698
+ recordedFollowUp(entry) {
20699
+ if (this.deps.cutRecordedClip === void 0) return { kinds: [] };
20700
+ const gifRefused = this.recordedGifRefusal(entry);
20701
+ const videoRefused = this.recordedVideoRefusal(entry);
20702
+ const kinds = [];
20703
+ if (entry.payload.mediaGif === true && gifRefused === void 0) kinds.push("gif");
20704
+ if (entry.payload.mediaClip === true && videoRefused === void 0) kinds.push("video");
20705
+ return {
20706
+ kinds,
20707
+ ...gifRefused !== void 0 ? { gifRefused } : {},
20708
+ ...videoRefused !== void 0 ? { videoRefused } : {}
20709
+ };
20710
+ }
20711
+ /** Why this row's video is not cut from the recordings, or `undefined` when
20712
+ * it is (or when the rule asked for no video). */
20713
+ recordedVideoRefusal(entry) {
20714
+ if (entry.payload.mediaClip !== true) return void 0;
20715
+ const subject = entry.payload.subject;
20716
+ if (selfDatingKindOf(subject) !== "package pick-up") return void 0;
20717
+ if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
20718
+ }
20719
+ /** Why this row's gif is not cut from the recordings, or `undefined` when it
20720
+ * is (or when the rule asked for no gif). */
20721
+ recordedGifRefusal(entry) {
20722
+ if (entry.payload.mediaGif !== true) return void 0;
20723
+ if (this.deps.deriveGifFromClip === void 0) return "no-derive";
20724
+ const subject = entry.payload.subject;
20725
+ if (selfDatingKindOf(subject) !== "package pick-up") return "not-a-pick-up";
20726
+ if (anchorFellBack(mediaAnchor(subject), true)) return "moment-unknown";
20727
+ }
20728
+ /**
20729
+ * The follow-up's attachments, gif first and video second — the order the
20730
+ * ring path ships them in. A gif that could not be derived is left out (its
20731
+ * reason already logged); the video, when asked for, still ships.
20732
+ */
20733
+ async recordedAttachments(entry, wanted, mp4) {
20734
+ const out = [];
20735
+ const derive = this.deps.deriveGifFromClip;
20736
+ if (wanted.includes("gif") && derive !== void 0) {
20737
+ const gif = await derive({
20738
+ deviceId: entry.payload.subject.deviceId,
20739
+ mp4,
20740
+ ...entry.payload.mediaClipSpeed !== void 0 ? { videoSpeed: entry.payload.mediaClipSpeed } : {},
20741
+ ruleId: entry.ruleId,
20742
+ recordId: entry.recordId
20743
+ });
20744
+ if (gif.ok) out.push({
20745
+ mediaType: "gif",
20746
+ bytes: gif.gif,
20747
+ mime: "image/gif",
20748
+ name: "event.gif"
20749
+ });
20750
+ }
20751
+ if (wanted.includes("video")) out.push({
20752
+ mediaType: "video",
20753
+ bytes: mp4,
20754
+ mime: "video/mp4",
20755
+ name: "event.mp4"
20756
+ });
20757
+ return out;
20758
+ }
20759
+ /**
20628
20760
  * Settle every recorded-clip follow-up this dispatcher has in flight.
20629
20761
  *
20630
20762
  * A TEST SEAM, and the honest kind: the follow-up is detached by design, so
@@ -26277,52 +26409,6 @@ function buildSummaryOutboxInputs(input) {
26277
26409
  });
26278
26410
  }
26279
26411
  //#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
26412
  //#region src/notification-center/summary/summary-selection.ts
26327
26413
  /**
26328
26414
  * Which tracks become tiles — filter, rank, and the per-camera quota.
@@ -26916,8 +27002,7 @@ var NcSummaryProducer = class {
26916
27002
  try {
26917
27003
  const candidates = await this.collectCandidates(rule, window);
26918
27004
  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, {
27005
+ const selection = selectSummaryTiles(filtered.kept, {
26921
27006
  maxTiles: rule.maxTiles,
26922
27007
  rank: rule.rank
26923
27008
  });
@@ -26946,7 +27031,7 @@ var NcSummaryProducer = class {
26946
27031
  const tileLoad = await this.loadTiles({
26947
27032
  rule,
26948
27033
  preferred: selection.tiles,
26949
- refill: remainingSummaryCandidates(grouped.kept, selection.tiles, rule.rank),
27034
+ refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
26950
27035
  maxTiles: rule.maxTiles
26951
27036
  });
26952
27037
  const loaded = tileLoad.tiles;
@@ -26987,7 +27072,7 @@ var NcSummaryProducer = class {
26987
27072
  priority: rule.priority,
26988
27073
  tileCount: loaded.length,
26989
27074
  cameraCount: new Set(loaded.map((t) => t.candidate.deviceId)).size,
26990
- matchedCount: grouped.kept.length,
27075
+ matchedCount: filtered.kept.length,
26991
27076
  detections,
26992
27077
  ...mosaic.ref !== null ? { mosaic: mosaic.ref } : {},
26993
27078
  generatedAt: this.deps.now(),
@@ -26999,7 +27084,7 @@ var NcSummaryProducer = class {
26999
27084
  this.deps.logger.info("summary produced", { meta: {
27000
27085
  ...meta,
27001
27086
  scanned: candidates.length,
27002
- matched: grouped.kept.length,
27087
+ matched: filtered.kept.length,
27003
27088
  droppedStationary: filtered.droppedStationary,
27004
27089
  droppedUnconfirmed: filtered.droppedUnconfirmed,
27005
27090
  unknownConfirmation: filtered.unknownConfirmation,
@@ -27022,7 +27107,7 @@ var NcSummaryProducer = class {
27022
27107
  window,
27023
27108
  tiles: loaded.length,
27024
27109
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
27025
- matched: grouped.kept.length,
27110
+ matched: filtered.kept.length,
27026
27111
  droppedStationary: filtered.droppedStationary,
27027
27112
  enqueued,
27028
27113
  ...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
@@ -27043,54 +27128,6 @@ var NcSummaryProducer = class {
27043
27128
  };
27044
27129
  }
27045
27130
  }
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
27131
  /** Every camera's tracks in the window, capped per camera and counted. */
27095
27132
  async collectCandidates(rule, window) {
27096
27133
  const all = [];
@@ -27141,25 +27178,6 @@ var NcSummaryProducer = class {
27141
27178
  seenIds.add(candidate.trackId);
27142
27179
  const tags = { deviceId: candidate.deviceId };
27143
27180
  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
27181
  const rows = await this.deps.listMediaInfo(candidate.trackId);
27164
27182
  const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
27165
27183
  if (picked === null) {
@@ -32666,6 +32684,9 @@ var NotificationCenter = class NotificationCenter {
32666
32684
  * delivered/picked-up), in the SAME moment as the durable insert. Feeds the
32667
32685
  * `package-event` trigger. Package events ARE object-event rows, so the boot
32668
32686
  * crash-gap reconcile re-covers them (routed by className in {@link reconcile}).
32687
+ *
32688
+ * When a pick-up HAPPENED is on the row (`event.observedAt`, D684), so this
32689
+ * hook and the reconcile read it from the same place.
32669
32690
  */
32670
32691
  onPackageEventPersisted(event, phase) {
32671
32692
  this.consumeEvent(incomingFromPackageEvent(event, phase));
@@ -35435,7 +35456,6 @@ var StoragePressureTracker = class {
35435
35456
  */
35436
35457
  var OWNER_KINDS = [
35437
35458
  "track",
35438
- "summary",
35439
35459
  "face",
35440
35460
  "identity",
35441
35461
  "plate",
@@ -35497,10 +35517,19 @@ function isWriterBoundedOwnerKind(value) {
35497
35517
  * `{ kind: 'retired-owner' }` and the orphan audit reclaims it under the same
35498
35518
  * grace window and ceilings as any other orphan.
35499
35519
  *
35520
+ * `'summary'` is the same case one entity later: the session summary (the
35521
+ * proximity group, D489/D490) was deleted on 2026-09-29 (D680) with
35522
+ * `pipeline-analytics:summaries` / `:summary-members`. Its group shots were
35523
+ * `summary`-owned media rows; they are reclaimed the same way.
35524
+ *
35500
35525
  * Deliberately NOT a member of {@link OWNER_KINDS}: the type is what stops a
35501
35526
  * new writer, and this list is only what the READ side must still recognise.
35502
35527
  */
35503
- var RETIRED_OWNER_KINDS = ["group", "event"];
35528
+ var RETIRED_OWNER_KINDS = [
35529
+ "group",
35530
+ "summary",
35531
+ "event"
35532
+ ];
35504
35533
  /** Is this an owner kind whose owning code is gone? See {@link RETIRED_OWNER_KINDS}. */
35505
35534
  function isRetiredMediaOwnerKind(value) {
35506
35535
  return RETIRED_OWNER_KINDS.includes(value);
@@ -42869,6 +42898,33 @@ var FaceSourceStats = class {
42869
42898
  };
42870
42899
  //#endregion
42871
42900
  //#region src/pipeline-analytics/pipeline/key-frame-capture.ts
42901
+ /**
42902
+ * Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
42903
+ * (Design B — one native full-frame per track at its best-detection moment).
42904
+ *
42905
+ * ## Why this exists (the missing native keyFrame)
42906
+ *
42907
+ * `keyFrame` was historically captured ONLY inside the CLIP object-embedding
42908
+ * best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
42909
+ * Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
42910
+ * a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
42911
+ * wide), so that gate was never satisfied and the native `keyFrame` was NEVER
42912
+ * produced — every stored frame stayed at the ≤640×360 detection resolution.
42913
+ *
42914
+ * The fix decouples the `keyFrame` from the clip path: it is selected on the
42915
+ * GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
42916
+ * `thumbnail`) by {@link selectKeyFrameCaptures}.
42917
+ *
42918
+ * ## How the pixels arrive, since D422
42919
+ *
42920
+ * Not by handle. At the best instant the owning node PARKS the whole frame at
42921
+ * {@link KEYFRAME_NATIVE_MAX_WIDTH} (`track-park.ts` orders it; the runner
42922
+ * refuses a ≤640 `ram-fullframe` answer at park time), and `persistKeyFrames`
42923
+ * stores that parcel verbatim as `keyFrame` (`key-frame-persist.ts`). The web
42924
+ * companion is {@link deriveKeyFrameSmall} of the same bytes — never a second
42925
+ * fetch. A retrieval keyed by a track never falls back to a handle
42926
+ * (`scripts/check-parked-frame-no-handle-fallback.ts`).
42927
+ */
42872
42928
  /** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
42873
42929
  * Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
42874
42930
  * resize, 3840 ≤ 4096); an 8K cam is bounded to 4096 (still far past the retired
@@ -42894,47 +42950,10 @@ function selectKeyFrameCaptures(targets) {
42894
42950
  return selected;
42895
42951
  }
42896
42952
  /**
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
42953
  * The web companion from the PARKED key frame's own JPEG (D422): the parcel is
42933
42954
  * stored verbatim as `keyFrame`, and `keyFrameSmall` is this — the same bytes
42934
42955
  * downscaled to {@link KEYFRAME_SMALL_MAX_WIDTH}. One decode, one encode,
42935
42956
  * 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
42957
  */
42939
42958
  async function deriveKeyFrameSmall(keyFrameJpeg) {
42940
42959
  return sharp(keyFrameJpeg).resize(960, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
@@ -43157,7 +43176,7 @@ async function mapWithConcurrency(items, limit, mapper) {
43157
43176
  * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
43158
43177
  * all-zero track scores 0.
43159
43178
  */
43160
- var WEIGHT_CONFIDENCE$1 = .3;
43179
+ var WEIGHT_CONFIDENCE = .3;
43161
43180
  var WEIGHT_CLASS = .2;
43162
43181
  var WEIGHT_DWELL = .2;
43163
43182
  var WEIGHT_PROXIMITY = .12;
@@ -43195,7 +43214,7 @@ var ANIMAL_CLASSES = new Set([
43195
43214
  "cat"
43196
43215
  ]);
43197
43216
  /** Clamp to [0,1]. */
43198
- function clamp01$2(x) {
43217
+ function clamp01$1(x) {
43199
43218
  if (Number.isNaN(x)) return 0;
43200
43219
  if (x < 0) return 0;
43201
43220
  if (x > 1) return 1;
@@ -43229,7 +43248,7 @@ function computeImportance(input) {
43229
43248
  const terms = [
43230
43249
  {
43231
43250
  reason: "confidence",
43232
- value: WEIGHT_CONFIDENCE$1 * clamp01$2(input.peakConfidence)
43251
+ value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
43233
43252
  },
43234
43253
  {
43235
43254
  reason: "class",
@@ -43237,15 +43256,15 @@ function computeImportance(input) {
43237
43256
  },
43238
43257
  {
43239
43258
  reason: "dwell",
43240
- value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
43259
+ value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
43241
43260
  },
43242
43261
  {
43243
43262
  reason: "proximity",
43244
- value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
43263
+ value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
43245
43264
  },
43246
43265
  {
43247
43266
  reason: "travel",
43248
- value: WEIGHT_TRAVEL * clamp01$2(input.totalDistance / 2)
43267
+ value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
43249
43268
  },
43250
43269
  {
43251
43270
  reason: "zone",
@@ -43264,11 +43283,11 @@ function computeImportance(input) {
43264
43283
  }
43265
43284
  const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
43266
43285
  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),
43286
+ importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
43268
43287
  reason: best.reason
43269
43288
  };
43270
43289
  return {
43271
- importance: clamp01$2(sum),
43290
+ importance: clamp01$1(sum),
43272
43291
  reason: best.reason
43273
43292
  };
43274
43293
  }
@@ -46258,7 +46277,7 @@ var ClosedRowMediaGate = class {
46258
46277
  * it, so it can be loose enough to see the near misses.
46259
46278
  */
46260
46279
  var CO_MOVING_MIN_CONTAINMENT = .6;
46261
- function intersectionArea$4(a, b) {
46280
+ function intersectionArea$3(a, b) {
46262
46281
  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
46282
  }
46264
46283
  /**
@@ -46278,7 +46297,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
46278
46297
  if (o.className === b.className) continue;
46279
46298
  const areaO = o.bbox.w * o.bbox.h;
46280
46299
  if (areaO <= 0) continue;
46281
- const inter = intersectionArea$4(b.bbox, o.bbox);
46300
+ const inter = intersectionArea$3(b.bbox, o.bbox);
46282
46301
  if (inter <= 0) continue;
46283
46302
  const union = areaB + areaO - inter;
46284
46303
  const containment = inter / Math.min(areaB, areaO);
@@ -47278,7 +47297,7 @@ var LooseBindSampler = class {
47278
47297
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
47279
47298
  */
47280
47299
  var GROWTH_CENTROID_FACTOR = 2.3;
47281
- function intersectionArea$3(a, b) {
47300
+ function intersectionArea$2(a, b) {
47282
47301
  const x1 = Math.max(a.x, b.x);
47283
47302
  const y1 = Math.max(a.y, b.y);
47284
47303
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -47317,7 +47336,7 @@ function growthAdjacentMatch(track, det, ctx) {
47317
47336
  const larger = Math.max(trackArea, detArea);
47318
47337
  const smaller = Math.min(trackArea, detArea);
47319
47338
  if (larger / smaller < 1.5) return false;
47320
- if (intersectionArea$3(track, det) / smaller >= .5) return false;
47339
+ if (intersectionArea$2(track, det) / smaller >= .5) return false;
47321
47340
  const tc = bboxCentroid(track);
47322
47341
  const dc = bboxCentroid(det);
47323
47342
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -50441,7 +50460,6 @@ var EMPTY_CASCADE_TALLY = {
50441
50460
  faces: 0,
50442
50461
  plates: 0,
50443
50462
  embeddings: 0,
50444
- summaries: 0,
50445
50463
  bytesReclaimed: 0
50446
50464
  };
50447
50465
  /** Fold a leaf store's receipts into a running tally. */
@@ -50485,13 +50503,6 @@ function addLeafCounts(tally, counts) {
50485
50503
  bytesReclaimed
50486
50504
  };
50487
50505
  break;
50488
- case "summaries":
50489
- next = {
50490
- ...next,
50491
- summaries: next.summaries + c.rows,
50492
- bytesReclaimed
50493
- };
50494
- break;
50495
50506
  }
50496
50507
  }
50497
50508
  return next;
@@ -50505,7 +50516,6 @@ function mergeCascadeTallies(a, b) {
50505
50516
  faces: a.faces + b.faces,
50506
50517
  plates: a.plates + b.plates,
50507
50518
  embeddings: a.embeddings + b.embeddings,
50508
- summaries: a.summaries + b.summaries,
50509
50519
  bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
50510
50520
  };
50511
50521
  }
@@ -50568,7 +50578,6 @@ function absentCascadeLegs(stores) {
50568
50578
  if (!stores.faceStore) absent.push("faces");
50569
50579
  if (!stores.plateStore) absent.push("plates");
50570
50580
  if (!stores.objectEmbeddingStore) absent.push("embeddings");
50571
- if (!stores.summaryStore) absent.push("summaries");
50572
50581
  return absent;
50573
50582
  }
50574
50583
  function buildAnalyticsCascadeRegistry(stores) {
@@ -50576,7 +50585,6 @@ function buildAnalyticsCascadeRegistry(stores) {
50576
50585
  if (stores.faceStore) registry.push(stores.faceStore);
50577
50586
  if (stores.plateStore) registry.push(stores.plateStore);
50578
50587
  if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
50579
- if (stores.summaryStore) registry.push(stores.summaryStore);
50580
50588
  return registry;
50581
50589
  }
50582
50590
  /**
@@ -53173,7 +53181,9 @@ var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
53173
53181
  * @durable class=ledger owner=pipeline-analytics
53174
53182
  * write="one row per detected object per processed frame — every tracked
53175
53183
  * detection the frame processor emits, plus the appearance events and the
53176
- * synthetic package-drop events. Every row carries the trackId it belongs to."
53184
+ * synthetic package-drop events. Every row carries the trackId it belongs to.
53185
+ * A package PICK-UP row also carries observedAt, when the parcel went,
53186
+ * beside its timestamp, when the departure was concluded (D684)."
53177
53187
  * retention="TRACK-OWNED: it has no clock of its own. Rows go only when their
53178
53188
  * track goes, through EventStore.deleteByTracks — which folds in the deletion
53179
53189
  * of the event's child crops so a caller cannot take the events and leave the
@@ -53380,7 +53390,21 @@ var OBJECT_COLUMNS = [
53380
53390
  {
53381
53391
  name: "importance",
53382
53392
  type: "REAL"
53383
- }
53393
+ },
53394
+ (
53395
+ /**
53396
+ * When a package PICK-UP happened, beside `timestamp` (when it was
53397
+ * CONCLUDED, five minutes later) — see `ObjectEvent.observedAt` (D684).
53398
+ *
53399
+ * DECLARED, or the backend drops it on write (see `frameId`). Additive: the
53400
+ * collection gains the column in place (`ALTER TABLE … ADD COLUMN`), and every
53401
+ * row written before it reads SQL NULL → absent after `stripNulls`, which is
53402
+ * the truth about it: the moment is unknown. Only the package detector sets it.
53403
+ */
53404
+ {
53405
+ name: "observedAt",
53406
+ type: "INTEGER"
53407
+ })
53384
53408
  ];
53385
53409
  var AUDIO_COLUMNS = [
53386
53410
  ...COMMON_BASE_COLUMNS,
@@ -55097,10 +55121,6 @@ function trackColumnsFor(projection, needsPositions = false) {
55097
55121
  * whatever order the engine felt like and a cursor that cannot resume.
55098
55122
  */
55099
55123
  var RECENT_IDS_COLUMNS = ["lastSeen"];
55100
- /** How many track ids one filter pass may collect per tier. Bounded for the
55101
- * same reason every bulk read here is: an unbounded scan of this table is
55102
- * what D447 measured holding the settings thread for seconds. */
55103
- var TRACK_MATCH_SCAN_LIMIT = 2e4;
55104
55124
  /** Spreadable `columns` for a store query — omitted entirely for `full`. */
55105
55125
  function projectionArg(projection, needsPositions = false) {
55106
55126
  const columns = trackColumnsFor(projection, needsPositions);
@@ -56217,42 +56237,6 @@ var TrackStore = class {
56217
56237
  * current subject out from under it. The orphan audit's grace window would
56218
56238
  * hide this for an hour and then stop hiding it.
56219
56239
  */
56220
- /**
56221
- * Track ids whose CLASS, sub-class or instance matches any of `terms`.
56222
- *
56223
- * The summaries' filter asks its question here rather than on a copy: a
56224
- * label is a fact about the TRACK, it is decided while the track lives and
56225
- * can still be re-decided afterwards (class voting, a late face match, an
56226
- * operator enrolling a face and renaming that person). A copy stamped onto
56227
- * the summary would answer with whatever was true at one instant and never
56228
- * learn — which is the whole reason this lookup exists instead.
56229
- *
56230
- * Three queries because the filter compiler has no OR across fields, and one
56231
- * term can be any of the three tiers: `animal` is a class, `dog` is a
56232
- * sub-class, `Roberta` is an instance. Bounded, like every other bulk read
56233
- * here.
56234
- */
56235
- async trackIdsMatching(terms) {
56236
- const wanted = terms.filter((t) => t.length > 0);
56237
- if (wanted.length === 0) return [];
56238
- const ids = /* @__PURE__ */ new Set();
56239
- for (const field of [
56240
- "className",
56241
- "label",
56242
- "subLabel"
56243
- ]) {
56244
- const rows = await this.store.query.query({
56245
- collection: TRACKS_COLLECTION,
56246
- filter: {
56247
- whereIn: { [field]: [...wanted] },
56248
- limit: TRACK_MATCH_SCAN_LIMIT
56249
- },
56250
- columns: ["id"]
56251
- });
56252
- for (const row of rows) ids.add(row.id);
56253
- }
56254
- return [...ids];
56255
- }
56256
56240
  async existingTrackIds(trackIds) {
56257
56241
  const PAGE = 500;
56258
56242
  const found = /* @__PURE__ */ new Set();
@@ -59208,6 +59192,180 @@ function decodeBase64(base64) {
59208
59192
  }
59209
59193
  }
59210
59194
  //#endregion
59195
+ //#region src/notification-center/recorded-gif.ts
59196
+ /**
59197
+ * The notification GIF for a moment only the RECORDINGS still hold — derived
59198
+ * from the recorded clip's own bytes (D682).
59199
+ *
59200
+ * ## Why this exists
59201
+ *
59202
+ * A package pick-up is concluded five minutes after the parcel went
59203
+ * (`PACKAGE_STILLNESS_TTL_MS`). Once its footage is anchored on the pick-up
59204
+ * itself rather than on the conclusion (`media-anchor.ts`), the window is far
59205
+ * older than any clip ring (≤ 60 s, `NC_CLIP_RETENTION_MAX_SEC`), so the ring
59206
+ * refuses it. A gif-only rule then had NOTHING: the recorded follow-up served
59207
+ * only a rule that asked for a video. The honest fix for "the gif shows the
59208
+ * empty doormat" is not "no gif" — it is the gif of the pick-up, from the
59209
+ * recordings.
59210
+ *
59211
+ * ## One cut, and the gif is DERIVED from it
59212
+ *
59213
+ * The recorded clip (`recorded-clip.ts`, one `recordingExport` render) is the
59214
+ * only cut. The gif is derived from ITS bytes with the SAME filter chain the
59215
+ * broker uses on a ring cut (`gifFromMp4Args` in `@camstack/types`), so a gif
59216
+ * of an old moment and a gif of a live one look the same, and a rule that asks
59217
+ * for both a gif and a video gets two containers of ONE window — the guarantee
59218
+ * `NcEventMediaRequest` exists for.
59219
+ *
59220
+ * ## Never throws, every miss named
59221
+ *
59222
+ * `derive-failed` (ffmpeg could not produce it) and `stage-failed` (the clip
59223
+ * could not be written where ffmpeg can read it) are logged with
59224
+ * `tags: { deviceId }` (D400: a missing attachment names its reason).
59225
+ */
59226
+ /** Bound on one derive: a ~10 s window encodes in well under a second; a
59227
+ * derive that has not finished in 20 s is wedged. Same bound as the broker's. */
59228
+ var NC_RECORDED_GIF_TIMEOUT_MS = 2e4;
59229
+ /** A notification gif is a few hundred KB; this is headroom, not a budget. */
59230
+ var RECORDED_GIF_MAX_BUFFER = 32 * 1024 * 1024;
59231
+ /**
59232
+ * Derive the gif. Never throws; every `ok: false` has logged its reason.
59233
+ */
59234
+ async function deriveRecordedGif(ports, req) {
59235
+ const tags = { deviceId: req.deviceId };
59236
+ const common = {
59237
+ ruleId: req.ruleId,
59238
+ eventId: req.recordId,
59239
+ sourceBytes: req.mp4.byteLength
59240
+ };
59241
+ let file;
59242
+ try {
59243
+ file = await ports.writeTemp(req.mp4);
59244
+ } catch (err) {
59245
+ ports.logger.warn("recorded gif: could not stage the recorded clip — no gif", {
59246
+ tags,
59247
+ meta: {
59248
+ ...common,
59249
+ reason: "stage-failed",
59250
+ error: String(err)
59251
+ }
59252
+ });
59253
+ return {
59254
+ ok: false,
59255
+ reason: "stage-failed"
59256
+ };
59257
+ }
59258
+ try {
59259
+ const speed = gifDeriveSpeed(4, req.videoSpeed ?? 1);
59260
+ const args = gifFromMp4Args({
59261
+ maxWidth: 640,
59262
+ fps: 12,
59263
+ speed,
59264
+ input: file
59265
+ });
59266
+ const startedAt = Date.now();
59267
+ let ran;
59268
+ try {
59269
+ ran = await ports.run(await ports.resolveFfmpeg(), args);
59270
+ } catch (err) {
59271
+ ran = {
59272
+ ok: false,
59273
+ error: String(err)
59274
+ };
59275
+ }
59276
+ const out = ran.ok ? ran.stdout : null;
59277
+ if (out === null || out.byteLength === 0) {
59278
+ ports.logger.warn("recorded gif: the derive failed — the follow-up carries no gif", {
59279
+ tags,
59280
+ meta: {
59281
+ ...common,
59282
+ reason: "derive-failed",
59283
+ error: ran.ok ? "ffmpeg produced no bytes" : ran.error.slice(-400)
59284
+ }
59285
+ });
59286
+ return {
59287
+ ok: false,
59288
+ reason: "derive-failed"
59289
+ };
59290
+ }
59291
+ const gif = new Uint8Array(new ArrayBuffer(out.byteLength));
59292
+ gif.set(out);
59293
+ ports.logger.info("recorded gif: derived from the recorded clip", {
59294
+ tags,
59295
+ meta: {
59296
+ ...common,
59297
+ gifBytes: gif.byteLength,
59298
+ width: 640,
59299
+ fps: 12,
59300
+ speed,
59301
+ encodeMs: Date.now() - startedAt
59302
+ }
59303
+ });
59304
+ return {
59305
+ ok: true,
59306
+ gif
59307
+ };
59308
+ } finally {
59309
+ await ports.removeTemp(file).catch((err) => {
59310
+ ports.logger.warn("recorded gif: could not remove the staged clip", {
59311
+ tags,
59312
+ meta: {
59313
+ ...common,
59314
+ file,
59315
+ error: String(err)
59316
+ }
59317
+ });
59318
+ });
59319
+ }
59320
+ }
59321
+ /** The production ports: the pinned ffmpeg, a temp file, a bounded child. */
59322
+ function buildRecordedGifPorts(input) {
59323
+ return {
59324
+ logger: input.logger,
59325
+ resolveFfmpeg: input.resolveFfmpeg,
59326
+ run: (cmd, args) => new Promise((resolve) => {
59327
+ execFile(cmd, [...args], {
59328
+ encoding: "buffer",
59329
+ maxBuffer: RECORDED_GIF_MAX_BUFFER,
59330
+ timeout: NC_RECORDED_GIF_TIMEOUT_MS,
59331
+ killSignal: "SIGKILL"
59332
+ }, (err, stdout) => resolve(err !== null ? {
59333
+ ok: false,
59334
+ error: String(err)
59335
+ } : {
59336
+ ok: true,
59337
+ stdout
59338
+ }));
59339
+ }),
59340
+ writeTemp: (bytes) => stageTempClip(path.join(os.tmpdir(), `camstack-nc-gif-${randomUUID()}.mp4`), bytes, {
59341
+ write: (file, data) => fs.writeFile(file, data),
59342
+ remove: (file) => fs.rm(file, { force: true })
59343
+ }),
59344
+ removeTemp: (file) => fs.rm(file, { force: true })
59345
+ };
59346
+ }
59347
+ /**
59348
+ * Write the clip to `file`, and on a failed write remove whatever landed.
59349
+ *
59350
+ * The path is named BEFORE the write, because a write can create the file and
59351
+ * then fail (ENOSPC half-way): the caller's `finally` only knows a path the
59352
+ * write RETURNED, so a partial `camstack-nc-gif-*.mp4` used to stay in the temp
59353
+ * directory for good. The write's error is re-thrown — the caller names it
59354
+ * (`stage-failed`, tagged per camera). A removal that ALSO fails is a real leak
59355
+ * and rides in that same error's message, so it reaches that one line rather
59356
+ * than a second, untagged one.
59357
+ */
59358
+ async function stageTempClip(file, bytes, io) {
59359
+ try {
59360
+ await io.write(file, bytes);
59361
+ return file;
59362
+ } catch (err) {
59363
+ const leaked = await io.remove(file).then(() => null, (removeErr) => String(removeErr));
59364
+ if (leaked === null) throw err;
59365
+ throw new Error(`${String(err)}; the partial file ${file} was NOT removed: ${leaked}`);
59366
+ }
59367
+ }
59368
+ //#endregion
59211
59369
  //#region src/shared/frame/crop-extractor.ts
59212
59370
  /**
59213
59371
  * The ONE reading of a normalized box against a frame: round, then clamp so
@@ -65140,7 +65298,7 @@ var RetrainAnnotationStore = class {
65140
65298
  };
65141
65299
  //#endregion
65142
65300
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
65143
- function clamp01$1(v) {
65301
+ function clamp01(v) {
65144
65302
  return v < 0 ? 0 : v > 1 ? 1 : v;
65145
65303
  }
65146
65304
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -65216,10 +65374,10 @@ function deriveZoneCropRect(bbox, zone) {
65216
65374
  function deriveSubjectCropRect(bbox, padding) {
65217
65375
  if (!isUsableRect(bbox)) return null;
65218
65376
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
65219
- const cropX = clamp01$1(bbox.x - bbox.w * pad);
65220
- const cropY = clamp01$1(bbox.y - bbox.h * pad);
65221
- const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
65222
- const cropBottom = clamp01$1(bbox.y + bbox.h * (1 + pad));
65377
+ const cropX = clamp01(bbox.x - bbox.w * pad);
65378
+ const cropY = clamp01(bbox.y - bbox.h * pad);
65379
+ const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
65380
+ const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
65223
65381
  const cropW = cropRight - cropX;
65224
65382
  const cropH = cropBottom - cropY;
65225
65383
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -68762,7 +68920,7 @@ async function persistTrackKeyFrame(deps, input) {
68762
68920
  meta: {
68763
68921
  ...drop.meta,
68764
68922
  nodeId: parcel.nodeId,
68765
- error: errMsg$1(err)
68923
+ error: errMsg(err)
68766
68924
  }
68767
68925
  });
68768
68926
  }
@@ -68782,7 +68940,7 @@ async function persistTrackKeyFrame(deps, input) {
68782
68940
  keyFrameMediaId
68783
68941
  };
68784
68942
  } catch (err) {
68785
- const error = errMsg$1(err);
68943
+ const error = errMsg(err);
68786
68944
  deps.logger.warn("key-frame write failed", {
68787
68945
  ...drop,
68788
68946
  meta: {
@@ -69796,7 +69954,7 @@ function isOversizeBox(bbox, frameWidth, frameHeight) {
69796
69954
  const frame = frameWidth * frameHeight;
69797
69955
  return frame > 0 && bbox.w * bbox.h / frame >= .3;
69798
69956
  }
69799
- function intersectionArea$2(a, b) {
69957
+ function intersectionArea$1(a, b) {
69800
69958
  const ix1 = Math.max(a.x, b.x);
69801
69959
  const iy1 = Math.max(a.y, b.y);
69802
69960
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -69809,7 +69967,7 @@ function detectionSwallowsEntry(det, parked) {
69809
69967
  const detArea = det.w * det.h;
69810
69968
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
69811
69969
  if (!centroidInside(det, centroid(parked))) return false;
69812
- return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69970
+ return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
69813
69971
  }
69814
69972
  /**
69815
69973
  * True when `det` is a FRAGMENT of the parked object — much smaller than the
@@ -69832,7 +69990,7 @@ function entrySwallowsDetection(parked, det) {
69832
69990
  const detArea = det.w * det.h;
69833
69991
  if (entryArea <= 0 || detArea <= 0) return false;
69834
69992
  if (detArea > entryArea / 1.35) return false;
69835
- return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69993
+ return intersectionArea$1(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
69836
69994
  }
69837
69995
  function parkedEntryHoldsDetection(entry, detBbox, config) {
69838
69996
  const overlap = iou(entry.bbox, detBbox);
@@ -70681,10 +70839,12 @@ var PackageDropDetector = class {
70681
70839
  pickupAt: entry.lastConfirmedAt
70682
70840
  }
70683
70841
  });
70842
+ const observedAt = collector?.observedAt ?? pickupInstantOf(entry);
70684
70843
  const draft = {
70685
70844
  kind: "object",
70686
70845
  deviceId: entry.deviceId,
70687
70846
  timestamp,
70847
+ observedAt,
70688
70848
  source: "pipeline",
70689
70849
  idempotencyKey: pickedUpKey,
70690
70850
  trackId: mediaTrackId,
@@ -70736,6 +70896,8 @@ var PackageDropDetector = class {
70736
70896
  idempotencyKey: pickedUpKey,
70737
70897
  deliveredEventId: delivered.id,
70738
70898
  mediaTrackId,
70899
+ observedAt,
70900
+ concludedLagMs: timestamp - observedAt,
70739
70901
  ...collector !== void 0 ? {
70740
70902
  collectorTrackId: collector.trackId,
70741
70903
  collectorClassName: collector.className,
@@ -70773,7 +70935,7 @@ var VEHICLE_CLASSES = [
70773
70935
  "vehicle"
70774
70936
  ];
70775
70937
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
70776
- function intersectionArea$1(a, b) {
70938
+ function intersectionArea(a, b) {
70777
70939
  const left = Math.max(a.x, b.x);
70778
70940
  const right = Math.min(a.x + a.w, b.x + b.w);
70779
70941
  const top = Math.max(a.y, b.y);
@@ -70790,7 +70952,7 @@ function intersectionArea$1(a, b) {
70790
70952
  function containedFraction(person, vehicle) {
70791
70953
  const area = person.w * person.h;
70792
70954
  if (area <= 0) return 0;
70793
- return intersectionArea$1(person, vehicle) / area;
70955
+ return intersectionArea(person, vehicle) / area;
70794
70956
  }
70795
70957
  /**
70796
70958
  * Is this person box contained in any vehicle box in the same frame?
@@ -74386,895 +74548,6 @@ async function reconcileGallerySamples(deps, options) {
74386
74548
  stoppedByCeiling
74387
74549
  };
74388
74550
  }
74389
- //#endregion
74390
- //#region src/pipeline-analytics/summary/summary-membership.ts
74391
- /**
74392
- * Who belongs to a summary, and which member is its primary.
74393
- *
74394
- * ## What this corrects
74395
- *
74396
- * `openOrAbsorb` absorbed ANY track born while a summary was open on the
74397
- * camera, and `primary` went to whoever opened it. Both were measured wrong on
74398
- * the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
74399
- * summaries describing "who was here together", and a dog that walked in ahead
74400
- * of its owner outranked the person behind it.
74401
- *
74402
- * The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
74403
- * sono sempre separati come eventi sintetici di sensori e pacchi."
74404
- *
74405
- * ## Why exclusion and priority live together
74406
- *
74407
- * They are the same question asked twice — "what is a subject in a group" —
74408
- * and answering it in two files is how the two drift. A class that stops being
74409
- * a member must not keep a rank, and a class that gains a rank must be a
74410
- * member.
74411
- *
74412
- * Pure, so the surprising cases are pinned rather than discovered: the dog that
74413
- * arrives first, the tie between two people, and the member whose class is not
74414
- * known yet.
74415
- */
74416
- /** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
74417
- * except this — which is the one place it must be parsed. */
74418
- var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
74419
- /**
74420
- * Classes the operator keeps OUT of a group, by name.
74421
- *
74422
- * A delivered package is an event about a PLACE, not a member of something that
74423
- * arrived together — the operator named it alongside the sensors.
74424
- */
74425
- var NON_MEMBER_CLASSES = new Set(["package"]);
74426
- /**
74427
- * Is this track a member of a group at all?
74428
- *
74429
- * An unknown class is NOT a reason to refuse. The grouping is the point and the
74430
- * label is decoration, and nothing revisits this decision — a member dropped
74431
- * here is dropped for the life of the summary.
74432
- */
74433
- function isSummaryEligible(trackId, className) {
74434
- if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
74435
- return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
74436
- }
74437
- /**
74438
- * The operator's order: person › animal › vehicle › everything else.
74439
- *
74440
- * Higher is more important. Everything unnamed — including a class that has not
74441
- * resolved yet — sits below all three rather than above them: promoting an
74442
- * unknown would let a momentary mis-read take the group's identity, and the
74443
- * group shot with it.
74444
- */
74445
- var CLASS_RANK = new Map([
74446
- ["person", 3],
74447
- ["animal", 2],
74448
- ["vehicle", 1]
74449
- ]);
74450
- function classPriority(className) {
74451
- if (className === null || className === void 0) return 0;
74452
- return CLASS_RANK.get(className) ?? 0;
74453
- }
74454
- /**
74455
- * Which member is the primary: highest class rank, ties broken by EARLIEST
74456
- * arrival.
74457
- *
74458
- * The tie-break matters more than it looks. Two people are equal, so without it
74459
- * the primary would flip on every join between equals — and the primary is what
74460
- * a consumer reads to name the group, so it would rename itself for no reason
74461
- * an operator could see.
74462
- *
74463
- * `null` for no members, never a fabricated id.
74464
- */
74465
- function primaryTrackId(members) {
74466
- let best = null;
74467
- for (const m of members) {
74468
- if (best === null) {
74469
- best = m;
74470
- continue;
74471
- }
74472
- const rank = classPriority(m.className);
74473
- const bestRank = classPriority(best.className);
74474
- if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
74475
- }
74476
- return best?.trackId ?? null;
74477
- }
74478
- //#endregion
74479
- //#region src/pipeline-analytics/summary/summary-store.ts
74480
- /**
74481
- * @durable class=ledger owner=pipeline-analytics
74482
- * write="one row per SUMMARY — a window of co-present tracks on one camera
74483
- * group. Opened by `openOrAbsorb` when the first member arrives, updated
74484
- * in place while the window absorbs more, and finalised by `seal` when the
74485
- * window closes. Per WINDOW, never per frame or per track: the sealed
74486
- * summary is what a notification rule reads."
74487
- * retention="age-keyed on `sealedAt`, swept with the rest of the analytics
74488
- * retention. An unsealed row belongs to a window still open and is never
74489
- * swept by age — it is closed or cascaded away with its members."
74490
- */
74491
- var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
74492
- /**
74493
- * @durable class=ledger owner=pipeline-analytics
74494
- * write="one row per (summary, track) membership, written as a window
74495
- * absorbs a track. The join that lets a sealed summary name its subjects
74496
- * without re-reading the track store."
74497
- * retention="cascaded — a member dies with its summary (`delete-track-cascade`
74498
- * counts them as leaves) or with the track it names. It carries no clock of
74499
- * its own: an age sweep here could outlive or predecease the summary the
74500
- * row only means anything inside of."
74501
- */
74502
- var SUMMARY_MEMBERS_COLLECTION$1 = "pipeline-analytics:summary-members";
74503
- var SUMMARIES_COLUMNS = [
74504
- {
74505
- name: "id",
74506
- type: "TEXT",
74507
- primaryKey: true,
74508
- notNull: true
74509
- },
74510
- {
74511
- name: "deviceId",
74512
- type: "INTEGER",
74513
- notNull: true
74514
- },
74515
- {
74516
- name: "openedAt",
74517
- type: "INTEGER",
74518
- notNull: true
74519
- },
74520
- {
74521
- name: "lastActiveAt",
74522
- type: "INTEGER",
74523
- notNull: true
74524
- },
74525
- {
74526
- name: "sealedAt",
74527
- type: "INTEGER"
74528
- },
74529
- (
74530
- /** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
74531
- {
74532
- name: "openDeviceId",
74533
- type: "INTEGER"
74534
- }),
74535
- {
74536
- name: "memberCount",
74537
- type: "INTEGER",
74538
- notNull: true
74539
- }
74540
- ];
74541
- var SUMMARIES_INDEXES = [(
74542
- /**
74543
- * NOT unique since 2026-09-13. A summary is a GROUP, not a camera session, so
74544
- * one camera can hold several open at once — a family at the door while
74545
- * somebody else crosses the far end of the drive, or a group that split and
74546
- * whose halves are now two groups.
74547
- *
74548
- * The uniqueness was D359's third guarantee, and the other two survive it in
74549
- * a stricter form: an open is still idempotent, now keyed on the BIRTH
74550
- * (`deviceId`, `openedAt`, founding track) rather than on the camera and the
74551
- * millisecond — two subjects detected in one frame are two groups, not one —
74552
- * and sealing is still a conditional transition, addressed by id because
74553
- * `(openDeviceId, openedAt)` no longer names one row. Those are what stopped
74554
- * the 2026-08-24 incident of eleven sealed rows for one group; "one open per
74555
- * camera" was the part that also forbade the model the operator asked for.
74556
- *
74557
- * The index itself is create-only in this system — `declareCollection` emits
74558
- * `CREATE ... IF NOT EXISTS` and nothing anywhere drops an index — so on a
74559
- * database that already has the UNIQUE version this declaration changes
74560
- * nothing until that index is dropped by hand. Until then `openOrAbsorb`
74561
- * degrades to exactly the old behaviour (the unique violation is caught and
74562
- * the newcomer joins the winner), which is why the drop is a switch and not a
74563
- * prerequisite.
74564
- */
74565
- {
74566
- name: "summaries_open_device",
74567
- columns: ["openDeviceId"]
74568
- }), {
74569
- name: "summaries_device_opened",
74570
- columns: ["deviceId", "openedAt"]
74571
- }];
74572
- var SUMMARY_MEMBERS_COLUMNS = [
74573
- {
74574
- name: "id",
74575
- type: "TEXT",
74576
- primaryKey: true,
74577
- notNull: true
74578
- },
74579
- {
74580
- name: "summaryId",
74581
- type: "TEXT",
74582
- notNull: true
74583
- },
74584
- {
74585
- name: "trackId",
74586
- type: "TEXT",
74587
- notNull: true
74588
- },
74589
- {
74590
- name: "deviceId",
74591
- type: "INTEGER",
74592
- notNull: true
74593
- },
74594
- (
74595
- /** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
74596
- {
74597
- name: "timestamp",
74598
- type: "INTEGER",
74599
- notNull: true
74600
- }),
74601
- (
74602
- /**
74603
- * What this member IS to the summary: see {@link SummaryMemberRole}.
74604
- *
74605
- * NULLABLE, and not because a role is optional. `declareCollection` is
74606
- * additive — it ALTERs the column in — and SQLite refuses to add a `NOT NULL`
74607
- * column with no default to a table that already has rows. A default would be
74608
- * worse than null: it would stamp `primary` onto every member written before
74609
- * roles existed, including the siblings, and nothing downstream could tell
74610
- * the fabricated ones from the real ones.
74611
- */
74612
- {
74613
- name: "role",
74614
- type: "TEXT"
74615
- }),
74616
- (
74617
- /**
74618
- * The member's subject class, so "which classes were present" is answerable
74619
- * without joining `tracks`. Nullable for the same additive reason, AND
74620
- * because a birth genuinely may not know a class yet — the membership is the
74621
- * point, the label is decoration.
74622
- */
74623
- {
74624
- name: "className",
74625
- type: "TEXT"
74626
- })
74627
- ];
74628
- var SUMMARY_MEMBERS_INDEXES = [
74629
- {
74630
- name: "summary_members_summary",
74631
- columns: ["summaryId"]
74632
- },
74633
- {
74634
- name: "summary_members_track",
74635
- columns: ["trackId"]
74636
- },
74637
- (
74638
- /** The filter's own index: "which groups hold an `animal`" walks members by
74639
- * class, never summaries by hope. */
74640
- {
74641
- name: "summary_members_class",
74642
- columns: ["className"]
74643
- })
74644
- ];
74645
- /**
74646
- * A stored role, or `null`. An unrecognised value reads as `null` rather than
74647
- * being passed through: a consumer switching on this must not receive a string
74648
- * outside the union just because a row was hand-edited or written by a newer
74649
- * release.
74650
- */
74651
- function readRole(raw) {
74652
- return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
74653
- }
74654
- function readClassName(raw) {
74655
- return typeof raw === "string" && raw.length > 0 ? raw : null;
74656
- }
74657
- /**
74658
- * The key of an open, derived from the birth that founded it.
74659
- *
74660
- * The founding TRACK is part of it, and that is the whole point: the key used
74661
- * to be `(deviceId, openedAt)`, which made an open idempotent — a retry writes
74662
- * the same row — but also made two subjects detected in the SAME frame one
74663
- * group, because they are born in the same millisecond. That is the ordinary
74664
- * case on a gate camera, not a race. With the track in the key a retry still
74665
- * collapses (same birth, same track) and two births stay two groups.
74666
- */
74667
- /** The widest a JS Date can name — the open end of a half-bounded window. */
74668
- var TIME_MIN = -864e13;
74669
- var TIME_MAX = 864e13;
74670
- /**
74671
- * How many rows one paged read may materialise before the merge.
74672
- *
74673
- * The page is cut AFTER ordering, so the read must be able to serve any cursor
74674
- * position — but it must also be bounded, because an unbounded read of this
74675
- * table on a busy hub is the shape D447 measured holding the settings thread
74676
- * for seconds. Summaries are far fewer than tracks by construction (14 940
74677
- * against 30 583 on this hub), so this ceiling is generous.
74678
- */
74679
- var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
74680
- /** How many member rows one filter pass may read per tier. Bounded for the same
74681
- * reason as the scan above, and generous: members are one row per subject. */
74682
- var MEMBER_MATCH_SCAN_LIMIT = 2e4;
74683
- function summaryId(deviceId, openedAt, foundingTrackId) {
74684
- return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
74685
- }
74686
- function memberRowId(summaryIdValue, trackId) {
74687
- return `${summaryIdValue}:${trackId}`;
74688
- }
74689
- function num$1(value, fallback = 0) {
74690
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
74691
- }
74692
- function toRow$1(id, data) {
74693
- return {
74694
- id,
74695
- deviceId: num$1(data["deviceId"]),
74696
- openedAt: num$1(data["openedAt"]),
74697
- lastActiveAt: num$1(data["lastActiveAt"]),
74698
- sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
74699
- memberCount: num$1(data["memberCount"])
74700
- };
74701
- }
74702
- function isUniqueViolation(err) {
74703
- return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
74704
- }
74705
- var SummaryStore = class {
74706
- store;
74707
- logger;
74708
- onEvict;
74709
- trackIdsMatching;
74710
- constructor(deps) {
74711
- this.store = deps.store;
74712
- this.logger = deps.logger;
74713
- this.onEvict = deps.onEvict;
74714
- this.trackIdsMatching = deps.trackIdsMatching;
74715
- }
74716
- static async declare(store) {
74717
- await Promise.all([store.declareCollection.mutate({
74718
- collection: SUMMARIES_COLLECTION,
74719
- columns: [...SUMMARIES_COLUMNS],
74720
- indexes: [...SUMMARIES_INDEXES]
74721
- }), store.declareCollection.mutate({
74722
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74723
- columns: [...SUMMARY_MEMBERS_COLUMNS],
74724
- indexes: [...SUMMARY_MEMBERS_INDEXES]
74725
- })]);
74726
- }
74727
- /** The camera's OPEN summary, from the row — `null` when none. */
74728
- async findOpen(deviceId) {
74729
- const first = (await this.store.query.query({
74730
- collection: SUMMARIES_COLLECTION,
74731
- filter: {
74732
- where: { openDeviceId: deviceId },
74733
- limit: 1
74734
- }
74735
- }))[0];
74736
- return first === void 0 ? null : toRow$1(first.id, first.data);
74737
- }
74738
- /**
74739
- * Every OPEN group, or just one camera's when `deviceId` is given.
74740
- *
74741
- * Plural per camera since 2026-09-13: a summary is a group, not a camera
74742
- * session, so "the open summary of this camera" stopped being a question with
74743
- * an answer. Ordered oldest-first, so a caller with no better rule takes the
74744
- * one that has been there longest — which is what `openOrAbsorb` does, and is
74745
- * byte-for-byte what shipped when only one could exist.
74746
- */
74747
- async listOpen(deviceId) {
74748
- return (await this.store.query.query({
74749
- collection: SUMMARIES_COLLECTION,
74750
- filter: {
74751
- where: deviceId === void 0 ? { sealedAt: null } : { openDeviceId: deviceId },
74752
- limit: 1e4
74753
- }
74754
- })).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data)).toSorted((a, b) => a.openedAt - b.openedAt);
74755
- }
74756
- async membersOf(summaryIdValue) {
74757
- return (await this.store.query.query({
74758
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74759
- filter: {
74760
- where: { summaryId: summaryIdValue },
74761
- limit: 1e4
74762
- }
74763
- })).map((r) => ({
74764
- summaryId: summaryIdValue,
74765
- trackId: String(r.data["trackId"] ?? ""),
74766
- deviceId: num$1(r.data["deviceId"]),
74767
- joinedAt: num$1(r.data["timestamp"]),
74768
- role: readRole(r.data["role"]),
74769
- className: readClassName(r.data["className"])
74770
- }));
74771
- }
74772
- /**
74773
- * Put a track into the camera's open summary, opening one when there is
74774
- * none. Racing opens are settled by the store: the loser's insert hits the
74775
- * unique `openDeviceId` and is re-read as an absorb.
74776
- */
74777
- /**
74778
- * Open a NEW group for this track.
74779
- *
74780
- * The unique-violation branch is kept even though `summaries_open_device` is
74781
- * no longer UNIQUE: the id is `f(deviceId, openedAt)`, so two births in the
74782
- * SAME millisecond still collide on the primary key, and the right answer
74783
- * there is still "join the row that won" — it is the same group.
74784
- */
74785
- async openNew(input) {
74786
- const id = summaryId(input.deviceId, input.at, input.trackId);
74787
- try {
74788
- await this.store.set.mutate({
74789
- collection: SUMMARIES_COLLECTION,
74790
- key: id,
74791
- value: {
74792
- deviceId: input.deviceId,
74793
- openedAt: input.at,
74794
- lastActiveAt: input.at,
74795
- sealedAt: null,
74796
- openDeviceId: input.deviceId,
74797
- memberCount: 1
74798
- }
74799
- });
74800
- } catch (err) {
74801
- if (!isUniqueViolation(err)) throw err;
74802
- const winner = (await this.listOpen(input.deviceId)).find((s) => s.id === id);
74803
- if (winner === void 0) throw err;
74804
- return this.absorbInto(winner, input);
74805
- }
74806
- await this.addMember(id, input, "primary");
74807
- return {
74808
- id,
74809
- deviceId: input.deviceId,
74810
- openedAt: input.at,
74811
- lastActiveAt: input.at,
74812
- sealedAt: null,
74813
- memberCount: 1
74814
- };
74815
- }
74816
- /**
74817
- * Join the camera's oldest open group, or open one.
74818
- *
74819
- * The default policy, for callers with no better rule. The rule that DOES
74820
- * know better — proximity — lives with the caller that can see positions, and
74821
- * reaches the store through {@link listOpen} + {@link absorbInto} instead.
74822
- */
74823
- async openOrAbsorb(input) {
74824
- const first = (await this.listOpen(input.deviceId))[0];
74825
- return first === void 0 ? this.openNew(input) : this.absorbInto(first, input);
74826
- }
74827
- async absorbInto(summary, input) {
74828
- const members = await this.membersOf(summary.id);
74829
- if (members.some((m) => m.trackId === input.trackId)) return summary;
74830
- const joined = {
74831
- trackId: input.trackId,
74832
- className: input.className ?? null,
74833
- joinedAt: input.at
74834
- };
74835
- const primary = primaryTrackId([...members.map((m) => ({
74836
- trackId: m.trackId,
74837
- className: m.className,
74838
- joinedAt: m.joinedAt
74839
- })), joined]);
74840
- await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
74841
- if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
74842
- const memberCount = members.length + 1;
74843
- const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
74844
- await this.store.updateWhere.mutate({
74845
- collection: SUMMARIES_COLLECTION,
74846
- filter: { where: {
74847
- id: summary.id,
74848
- openDeviceId: summary.deviceId
74849
- } },
74850
- data: {
74851
- memberCount,
74852
- lastActiveAt
74853
- }
74854
- });
74855
- return {
74856
- ...summary,
74857
- memberCount,
74858
- lastActiveAt
74859
- };
74860
- }
74861
- /**
74862
- * Move a set of members out of an open group into a NEW open group.
74863
- *
74864
- * The two sides of a split. The departing members keep their join times, so
74865
- * the group that leaves opens at the moment its earliest member joined the
74866
- * old one — it existed then; what changed is that we can now tell.
74867
- *
74868
- * Order is deliberate: the new rows are written BEFORE the old ones are
74869
- * dropped. A failure between the two leaves a member in both groups, which a
74870
- * reader can see and a later pass can repair; the other order loses the
74871
- * membership, and a lost member is the one wrong this store exists to
74872
- * prevent (the same reasoning as {@link demoteOtherPrimaries}).
74873
- *
74874
- * `null` when there is nothing to do: no member matched, or ALL of them did.
74875
- * Moving everybody is not a split — it would leave an empty group behind and
74876
- * a new one holding exactly what the old one held.
74877
- */
74878
- async splitOff(summary, departingTrackIds, at) {
74879
- const members = await this.membersOf(summary.id);
74880
- const leaving = members.filter((m) => departingTrackIds.includes(m.trackId));
74881
- const staying = members.filter((m) => !departingTrackIds.includes(m.trackId));
74882
- if (leaving.length === 0 || staying.length === 0) return null;
74883
- const openedAt = Math.min(...leaving.map((m) => m.joinedAt));
74884
- const founder = leaving.find((m) => m.joinedAt === openedAt) ?? leaving[0];
74885
- const id = summaryId(summary.deviceId, openedAt, founder.trackId);
74886
- await this.store.set.mutate({
74887
- collection: SUMMARIES_COLLECTION,
74888
- key: id,
74889
- value: {
74890
- deviceId: summary.deviceId,
74891
- openedAt,
74892
- lastActiveAt: summary.lastActiveAt,
74893
- sealedAt: null,
74894
- openDeviceId: summary.deviceId,
74895
- memberCount: leaving.length
74896
- }
74897
- });
74898
- const newPrimary = primaryTrackId(leaving);
74899
- await this.store.insertMany.mutate({
74900
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74901
- records: leaving.map((m) => ({
74902
- id: memberRowId(id, m.trackId),
74903
- data: {
74904
- summaryId: id,
74905
- trackId: m.trackId,
74906
- deviceId: m.deviceId,
74907
- timestamp: m.joinedAt,
74908
- role: m.trackId === newPrimary ? "primary" : "sibling",
74909
- className: m.className
74910
- }
74911
- }))
74912
- });
74913
- await this.store.deleteWhere.mutate({
74914
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74915
- filter: {
74916
- where: { summaryId: summary.id },
74917
- whereIn: { trackId: leaving.map((m) => m.trackId) }
74918
- }
74919
- });
74920
- const keptPrimary = primaryTrackId(staying);
74921
- for (const m of staying) {
74922
- const role = m.trackId === keptPrimary ? "primary" : "sibling";
74923
- if (m.role === role) continue;
74924
- await this.store.update.mutate({
74925
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74926
- id: memberRowId(summary.id, m.trackId),
74927
- data: { role }
74928
- });
74929
- }
74930
- await this.store.updateWhere.mutate({
74931
- collection: SUMMARIES_COLLECTION,
74932
- filter: { where: {
74933
- id: summary.id,
74934
- openDeviceId: summary.deviceId
74935
- } },
74936
- data: { memberCount: staying.length }
74937
- });
74938
- this.logger.info("summary: a group walked apart", {
74939
- tags: { deviceId: summary.deviceId },
74940
- meta: {
74941
- summaryId: summary.id,
74942
- bornId: id,
74943
- left: leaving.map((m) => m.trackId),
74944
- stayed: staying.map((m) => m.trackId),
74945
- at
74946
- }
74947
- });
74948
- return {
74949
- id,
74950
- deviceId: summary.deviceId,
74951
- openedAt,
74952
- lastActiveAt: summary.lastActiveAt,
74953
- sealedAt: null,
74954
- memberCount: leaving.length
74955
- };
74956
- }
74957
- /**
74958
- * Put every OTHER member back to `sibling` after a new primary took the role.
74959
- *
74960
- * Best-effort per row and loud about a row it could not move: two primaries
74961
- * is a wrong a consumer can see, but it is smaller than a lost member, so a
74962
- * failure here never propagates to the join.
74963
- */
74964
- async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
74965
- for (const m of previous) {
74966
- if (m.trackId === primaryTrack || m.role !== "primary") continue;
74967
- try {
74968
- await this.store.update.mutate({
74969
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74970
- id: memberRowId(summaryIdValue, m.trackId),
74971
- data: { role: "sibling" }
74972
- });
74973
- } catch (err) {
74974
- this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
74975
- summaryId: summaryIdValue,
74976
- trackId: m.trackId,
74977
- error: String(err)
74978
- } });
74979
- }
74980
- }
74981
- }
74982
- async addMember(summaryIdValue, input, role) {
74983
- await this.store.insertMany.mutate({
74984
- collection: SUMMARY_MEMBERS_COLLECTION$1,
74985
- records: [{
74986
- id: memberRowId(summaryIdValue, input.trackId),
74987
- data: {
74988
- summaryId: summaryIdValue,
74989
- trackId: input.trackId,
74990
- deviceId: input.deviceId,
74991
- timestamp: input.at,
74992
- role,
74993
- className: input.className ?? null
74994
- }
74995
- }]
74996
- });
74997
- }
74998
- /**
74999
- * Seal in place. `true` when THIS call sealed it; `false` when the row was
75000
- * already sealed (or gone) — the second sealer learns, it does not write.
75001
- */
75002
- async seal(summary, sealedAt) {
75003
- const { updated } = await this.store.updateWhere.mutate({
75004
- collection: SUMMARIES_COLLECTION,
75005
- filter: { where: {
75006
- id: summary.id,
75007
- openDeviceId: summary.deviceId
75008
- } },
75009
- data: {
75010
- sealedAt,
75011
- openDeviceId: null
75012
- }
75013
- });
75014
- if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
75015
- tags: { deviceId: summary.deviceId },
75016
- meta: {
75017
- summaryId: summary.id,
75018
- updated
75019
- }
75020
- });
75021
- return updated >= 1;
75022
- }
75023
- /**
75024
- * Track cascade leaf: a member row whose track is gone is gone too, and
75025
- * the summary's count follows. The envelope itself outlives its members
75026
- * — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
75027
- */
75028
- async deleteByTracks(trackIds) {
75029
- if (trackIds.length === 0) return [{
75030
- family: "summaries",
75031
- rows: 0,
75032
- bytes: 0
75033
- }];
75034
- const members = await this.store.query.query({
75035
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75036
- filter: {
75037
- whereIn: { trackId: [...trackIds] },
75038
- limit: 1e4
75039
- }
75040
- });
75041
- if (members.length === 0) return [{
75042
- family: "summaries",
75043
- rows: 0,
75044
- bytes: 0
75045
- }];
75046
- const { deleted } = await this.store.deleteWhere.mutate({
75047
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75048
- filter: { whereIn: { trackId: [...trackIds] } }
75049
- });
75050
- const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
75051
- for (const summaryIdValue of touched) {
75052
- const remaining = await this.membersOf(summaryIdValue);
75053
- const raw = await this.store.get.query({
75054
- collection: SUMMARIES_COLLECTION,
75055
- key: summaryIdValue
75056
- });
75057
- if (typeof raw !== "object" || raw === null) continue;
75058
- const row = raw;
75059
- await this.store.updateWhere.mutate({
75060
- collection: SUMMARIES_COLLECTION,
75061
- filter: { where: {
75062
- deviceId: num$1(row["deviceId"]),
75063
- openedAt: num$1(row["openedAt"])
75064
- } },
75065
- data: { memberCount: remaining.length }
75066
- });
75067
- }
75068
- return [{
75069
- family: "summaries",
75070
- rows: deleted,
75071
- bytes: 0
75072
- }];
75073
- }
75074
- /**
75075
- * The surface's page (D359 phase 4): a camera set, newest first by
75076
- * `openedAt`, inside an optional window, bounded. Ordered and bounded HERE
75077
- * as well as in the query, so a store that ignores `orderBy` (the fake
75078
- * does) cannot hand the surface an unordered page.
75079
- */
75080
- async listRecent(input) {
75081
- if (input.deviceIds.length === 0) return [];
75082
- const matching = await this.summaryIdsMatching(input.classes);
75083
- if (matching !== null && matching.size === 0) return [];
75084
- const limit = Math.max(1, input.limit);
75085
- return (await this.store.query.query({
75086
- collection: SUMMARIES_COLLECTION,
75087
- filter: {
75088
- whereIn: { deviceId: [...input.deviceIds] },
75089
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { openedAt: [input.since ?? 0, input.until ?? Number.MAX_SAFE_INTEGER] } } : {},
75090
- orderBy: {
75091
- field: "openedAt",
75092
- direction: "desc"
75093
- },
75094
- limit
75095
- }
75096
- })).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
75097
- }
75098
- /** One summary with its members, or `null`. */
75099
- /**
75100
- * One PAGE of summaries across a camera set, newest activity first.
75101
- *
75102
- * The same contract as `listRecentTracks` and for the same consumers: the
75103
- * reel on the cameras page and the Events gallery both ride a cursor, so a
75104
- * feed that could only answer "the last N" would make groups available in the
75105
- * timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
75106
- * ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
75107
- * second implementation of that is a second place for two feeds to disagree
75108
- * about what "newest" means.
75109
- *
75110
- * The window is on LAST ACTIVITY, not on the open: a session that started
75111
- * before the window and ran into it belongs to the window, because it
75112
- * happened there.
75113
- *
75114
- * Members for the WHOLE page come back in one read. One query per row is how
75115
- * a page of two hundred becomes two hundred round trips — the shape that made
75116
- * the old digest read two thousand tracks a camera.
75117
- */
75118
- async queryRecent(input) {
75119
- const deviceIds = [...new Set(input.deviceIds)];
75120
- if (deviceIds.length === 0) return {
75121
- summaries: [],
75122
- nextCursor: null
75123
- };
75124
- const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
75125
- const matching = await this.summaryIdsMatching(input.classes);
75126
- if (matching !== null && matching.size === 0) return {
75127
- summaries: [],
75128
- nextCursor: null
75129
- };
75130
- const ordered = (await this.store.query.query({
75131
- collection: SUMMARIES_COLLECTION,
75132
- filter: {
75133
- whereIn: { deviceId: deviceIds },
75134
- ...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
75135
- limit: RECENT_SUMMARY_SCAN_LIMIT
75136
- }
75137
- })).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
75138
- row,
75139
- key: {
75140
- lastSeen: row.lastActiveAt,
75141
- id: row.id
75142
- }
75143
- })).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
75144
- const page = ordered.slice(0, input.limit);
75145
- const last = page.at(-1);
75146
- const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
75147
- lastSeen: last.row.lastActiveAt,
75148
- trackId: last.row.id
75149
- }) : null;
75150
- const members = await this.membersOfMany(page.map((e) => e.row.id));
75151
- return {
75152
- summaries: page.map((e) => ({
75153
- summary: e.row,
75154
- members: members.get(e.row.id) ?? []
75155
- })),
75156
- nextCursor
75157
- };
75158
- }
75159
- /**
75160
- * The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
75161
- *
75162
- * TRACKS first, then the members that name them, then the summaries. The
75163
- * label is a fact about the track and it is asked of the track — a summary
75164
- * that stored its own copy would answer with whatever was true when it
75165
- * sealed, and would never learn that a face was enrolled and a person named
75166
- * an hour later. The operator's rule: *a summary keeps the minimum
75167
- * information necessary*, and a duplicated label is not the minimum, it is a
75168
- * second truth with an expiry date.
75169
- *
75170
- * Narrows before the page is cut. Filtering the other way round — a page of
75171
- * summaries, then their members — returns a short or empty page while matches
75172
- * sit further down the list, and the operator reads that as "there were none".
75173
- */
75174
- async summaryIdsMatching(classes) {
75175
- const wanted = (classes ?? []).filter((t) => t.length > 0);
75176
- if (wanted.length === 0) return null;
75177
- const matchTracks = this.trackIdsMatching;
75178
- if (matchTracks === void 0) {
75179
- this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
75180
- return /* @__PURE__ */ new Set();
75181
- }
75182
- const trackIds = await matchTracks(wanted);
75183
- if (trackIds.length === 0) return /* @__PURE__ */ new Set();
75184
- const ids = /* @__PURE__ */ new Set();
75185
- const PAGE = 500;
75186
- for (let i = 0; i < trackIds.length; i += PAGE) {
75187
- const chunk = trackIds.slice(i, i + PAGE);
75188
- const rows = await this.store.query.query({
75189
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75190
- filter: {
75191
- whereIn: { trackId: [...chunk] },
75192
- limit: MEMBER_MATCH_SCAN_LIMIT
75193
- }
75194
- });
75195
- for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
75196
- }
75197
- ids.delete("");
75198
- return ids;
75199
- }
75200
- /** Members of many summaries, in ONE read, grouped by summary id. */
75201
- async membersOfMany(summaryIds) {
75202
- const out = /* @__PURE__ */ new Map();
75203
- if (summaryIds.length === 0) return out;
75204
- const rows = await this.store.query.query({
75205
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75206
- filter: {
75207
- whereIn: { summaryId: [...summaryIds] },
75208
- limit: 1e4
75209
- }
75210
- });
75211
- for (const r of rows) {
75212
- const summaryIdValue = String(r.data["summaryId"] ?? "");
75213
- const member = {
75214
- summaryId: summaryIdValue,
75215
- trackId: String(r.data["trackId"] ?? ""),
75216
- deviceId: num$1(r.data["deviceId"]),
75217
- joinedAt: num$1(r.data["timestamp"]),
75218
- role: readRole(r.data["role"]),
75219
- className: readClassName(r.data["className"])
75220
- };
75221
- const bucket = out.get(summaryIdValue);
75222
- if (bucket === void 0) out.set(summaryIdValue, [member]);
75223
- else bucket.push(member);
75224
- }
75225
- for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
75226
- return out;
75227
- }
75228
- async get(id) {
75229
- const raw = await this.store.get.query({
75230
- collection: SUMMARIES_COLLECTION,
75231
- key: id
75232
- });
75233
- if (typeof raw !== "object" || raw === null) return null;
75234
- return {
75235
- summary: toRow$1(id, raw),
75236
- members: await this.membersOf(id)
75237
- };
75238
- }
75239
- /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
75240
- async existingIds(ids) {
75241
- const found = /* @__PURE__ */ new Set();
75242
- await Promise.all(ids.map(async (id) => {
75243
- const raw = await this.store.get.query({
75244
- collection: SUMMARIES_COLLECTION,
75245
- key: id
75246
- });
75247
- if (typeof raw === "object" && raw !== null) found.add(id);
75248
- }));
75249
- return found;
75250
- }
75251
- /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
75252
- async evictBefore(deviceId, cutoffMs) {
75253
- const rows = await this.store.query.query({
75254
- collection: SUMMARIES_COLLECTION,
75255
- filter: {
75256
- where: { deviceId },
75257
- whereBetween: { openedAt: [0, cutoffMs - 1] },
75258
- limit: 1e4
75259
- }
75260
- });
75261
- if (rows.length === 0) return 0;
75262
- const ids = rows.map((r) => r.id);
75263
- await this.onEvict?.(ids);
75264
- await this.store.deleteWhere.mutate({
75265
- collection: SUMMARY_MEMBERS_COLLECTION$1,
75266
- filter: { whereIn: { summaryId: ids } }
75267
- });
75268
- const { deleted } = await this.store.deleteWhere.mutate({
75269
- collection: SUMMARIES_COLLECTION,
75270
- filter: {
75271
- where: { deviceId },
75272
- whereBetween: { openedAt: [0, cutoffMs - 1] }
75273
- }
75274
- });
75275
- return deleted;
75276
- }
75277
- };
75278
74551
  /**
75279
74552
  * Where the completion marker lives.
75280
74553
  *
@@ -78548,10 +77821,6 @@ var ANALYTICS_COLLECTIONS = [
78548
77821
  collection: PLATES_COLLECTION$1,
78549
77822
  classification: derived(PLATE_COLUMNS)
78550
77823
  },
78551
- {
78552
- collection: SUMMARY_MEMBERS_COLLECTION$1,
78553
- classification: derived(SUMMARY_MEMBERS_COLUMNS)
78554
- },
78555
77824
  {
78556
77825
  collection: MEDIA_COLLECTION,
78557
77826
  classification: {
@@ -78595,13 +77864,6 @@ var ANALYTICS_COLLECTIONS = [
78595
77864
  rootId: "ops-log"
78596
77865
  }
78597
77866
  },
78598
- {
78599
- collection: SUMMARIES_COLLECTION,
78600
- classification: {
78601
- kind: "retention-root",
78602
- rootId: "summary"
78603
- }
78604
- },
78605
77867
  {
78606
77868
  collection: IDENTITIES_COLLECTION,
78607
77869
  classification: {
@@ -78790,10 +78052,6 @@ function resolveMediaOwner(row) {
78790
78052
  table: ownerKind,
78791
78053
  eventId: ownerId
78792
78054
  };
78793
- case "summary": return {
78794
- kind: "summary",
78795
- summaryId: ownerId
78796
- };
78797
78055
  case "face": return ownerId.startsWith("face-") ? {
78798
78056
  kind: "track",
78799
78057
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -78901,17 +78159,15 @@ var DEFAULT_ORPHAN_LIMITS = {
78901
78159
  function questionsFor(refs) {
78902
78160
  const trackIds = /* @__PURE__ */ new Set();
78903
78161
  const eventIds = /* @__PURE__ */ new Map();
78904
- const summaryIds = /* @__PURE__ */ new Set();
78905
78162
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
78906
78163
  else if (ref.kind === "event") {
78907
78164
  const perTable = eventIds.get(ref.table) ?? /* @__PURE__ */ new Set();
78908
78165
  perTable.add(ref.eventId);
78909
78166
  eventIds.set(ref.table, perTable);
78910
- } else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
78167
+ }
78911
78168
  return {
78912
78169
  trackIds: [...trackIds],
78913
- eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]])),
78914
- summaryIds: [...summaryIds]
78170
+ eventIdsByTable: new Map([...eventIds].map(([table, ids]) => [table, [...ids]]))
78915
78171
  };
78916
78172
  }
78917
78173
  /**
@@ -78955,10 +78211,9 @@ async function auditScope(scope, deps, limits, cursor) {
78955
78211
  break;
78956
78212
  }
78957
78213
  const refs = rows.map((row) => scope.resolve(row));
78958
- const { trackIds, eventIdsByTable, summaryIds } = questionsFor(refs);
78214
+ const { trackIds, eventIdsByTable } = questionsFor(refs);
78959
78215
  let liveTracks;
78960
78216
  const liveEvents = /* @__PURE__ */ new Map();
78961
- let liveSummaries;
78962
78217
  let keptMedia = /* @__PURE__ */ new Set();
78963
78218
  try {
78964
78219
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
@@ -78967,7 +78222,6 @@ async function auditScope(scope, deps, limits, cursor) {
78967
78222
  if (ids.length === 0) continue;
78968
78223
  liveEvents.set(table, await deps.liveEventIds(table, ids));
78969
78224
  }
78970
- liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
78971
78225
  } catch (err) {
78972
78226
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
78973
78227
  collection: scope.collection,
@@ -78995,7 +78249,7 @@ async function auditScope(scope, deps, limits, cursor) {
78995
78249
  tally.unresolvable += 1;
78996
78250
  continue;
78997
78251
  }
78998
- if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
78252
+ if (ref.kind === "track" ? liveTracks.has(ref.trackId) || keptMedia.has(row.key) : ref.kind === "event" ? liveEvents.get(ref.table)?.has(ref.eventId) ?? false : false) continue;
78999
78253
  const ts = num(row.data, scope.timestampField);
79000
78254
  if (ts !== null && ts > graceFloor) {
79001
78255
  tally.withinGrace += 1;
@@ -79165,11 +78419,7 @@ var ORPHAN_AUDIT_DEFAULTS = {
79165
78419
  intervalHours: OrphanAuditIntervalHoursSchema.parse(void 0),
79166
78420
  pageSize: OrphanAuditPageSizeSchema.parse(void 0)
79167
78421
  };
79168
- var RAW_DELETE_COLLECTIONS = new Set([
79169
- "pipeline-analytics:object-events",
79170
- "pipeline-analytics:plates",
79171
- "pipeline-analytics:summary-members"
79172
- ]);
78422
+ var RAW_DELETE_COLLECTIONS = new Set(["pipeline-analytics:object-events", "pipeline-analytics:plates"]);
79173
78423
  function buildOrphanReclaimer(ports) {
79174
78424
  return async (collection, key) => {
79175
78425
  if (collection === "pipeline-analytics:media") {
@@ -79310,14 +78560,12 @@ async function sweepTracklessRoots(deps) {
79310
78560
  cutoffMs
79311
78561
  });
79312
78562
  const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
79313
- const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
79314
78563
  const evicted = {
79315
78564
  motion: events.motion,
79316
78565
  audio: events.audio,
79317
- sensor,
79318
- summaries
78566
+ sensor
79319
78567
  };
79320
- if (evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
78568
+ if (evicted.motion.length + evicted.audio.length + evicted.sensor > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
79321
78569
  } catch (err) {
79322
78570
  deps.onError(deviceId, err);
79323
78571
  }
@@ -80094,11 +79342,12 @@ function isFullFrameBbox(bbox) {
80094
79342
  /** Throttle for the native-crop HIT/FALLBACK metric window line. */
80095
79343
  var NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
80096
79344
  /**
80097
- * Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (the
80098
- * `keyFrame`/group-shot rung, ~1–1.5 MB on a 4K camera) rather than a boxed
80099
- * timeline tile or a pinned ≤640 detection frame (tens of kB). The two belong
80100
- * in different counters or the window line says "full frames" and means two
80101
- * payload classes three orders of magnitude apart. (D470)
79345
+ * Above this `maxWidth`, a full-frame fetch is a NATIVE full frame (~1–1.5 MB
79346
+ * on a 4K camera) rather than a boxed timeline tile or a pinned ≤640 detection
79347
+ * frame (tens of kB). The two belong in different counters or the window line
79348
+ * says "full frames" and means two payload classes three orders of magnitude
79349
+ * apart. (D470) No caller asks for that rung since D422 (the key frame) and
79350
+ * D680 (the group shot); the counter stays so the next one is seen.
80102
79351
  */
80103
79352
  var FULL_FRAME_TILE_WIDTH_CEILING = 1920;
80104
79353
  function createNativeFrameTransport(deps) {
@@ -80175,8 +79424,8 @@ function createNativeFrameTransport(deps) {
80175
79424
  };
80176
79425
  };
80177
79426
  const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
79427
+ const getNativeFullFrameRgb = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToFullFrame));
80178
79428
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
80179
- const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
80180
79429
  const cropMetricLogger = logger.child("NativeCrop");
80181
79430
  let nativeHits = 0;
80182
79431
  let nativeFallbacks = 0;
@@ -80192,8 +79441,8 @@ function createNativeFrameTransport(deps) {
80192
79441
  /**
80193
79442
  * Publish the window once `request` has SETTLED — never before, or the line
80194
79443
  * carries a window whose own outcome has not been counted yet and throttles
80195
- * away the one that has. `finally`, so the deliberate throw of
80196
- * `fetchNativeFullFrameTiered` still reaches `persistTrackKeyFrame`.
79444
+ * away the one that has. `finally`, so the deliberate throw of the
79445
+ * full-frame fetches still reaches their callers.
80197
79446
  */
80198
79447
  const closeWindowAfter = async (request) => {
80199
79448
  try {
@@ -80250,7 +79499,7 @@ function createNativeFrameTransport(deps) {
80250
79499
  try {
80251
79500
  return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
80252
79501
  } catch (err) {
80253
- cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg$1(err) } });
79502
+ cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: errMsg(err) } });
80254
79503
  return null;
80255
79504
  }
80256
79505
  };
@@ -80264,7 +79513,7 @@ function createNativeFrameTransport(deps) {
80264
79513
  bumpCropMetric(false);
80265
79514
  return null;
80266
79515
  }
80267
- if (!keyFrameAcceptsTier(native.tier)) {
79516
+ if (native.tier === "ram-fullframe") {
80268
79517
  bumpCropMetric(false);
80269
79518
  cropMetricLogger.info("subject crop refused: sub-native tier (will retry, no masquerade)", {
80270
79519
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
@@ -80284,7 +79533,6 @@ function createNativeFrameTransport(deps) {
80284
79533
  return {
80285
79534
  getRemoteFrame,
80286
79535
  getNativeFullFrameRgb,
80287
- getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
80288
79536
  getNativeCropJpeg,
80289
79537
  captureCrop: createCaptureCrop({
80290
79538
  tryNativeCrop,
@@ -80298,7 +79546,7 @@ function createNativeFrameTransport(deps) {
80298
79546
  const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH).catch((err) => {
80299
79547
  logger.child("DisplayCrop").debug("fallback full-frame fetch failed — treating as a miss", { meta: {
80300
79548
  nodeId: handle.nodeId,
80301
- error: errMsg$1(err)
79549
+ error: errMsg(err)
80302
79550
  } });
80303
79551
  return null;
80304
79552
  });
@@ -80513,7 +79761,7 @@ function createParkedFrameClient(deps) {
80513
79761
  tags: { deviceId: order.deviceId },
80514
79762
  meta: {
80515
79763
  ...meta,
80516
- error: errMsg$1(err)
79764
+ error: errMsg(err)
80517
79765
  }
80518
79766
  });
80519
79767
  return false;
@@ -80593,7 +79841,7 @@ function createParkedFrameClient(deps) {
80593
79841
  trackId,
80594
79842
  kind,
80595
79843
  nodeId: known.nodeId,
80596
- error: errMsg$1(err)
79844
+ error: errMsg(err)
80597
79845
  }
80598
79846
  });
80599
79847
  return null;
@@ -80615,7 +79863,7 @@ function createParkedFrameClient(deps) {
80615
79863
  meta: {
80616
79864
  trackId,
80617
79865
  nodeId,
80618
- error: errMsg$1(err)
79866
+ error: errMsg(err)
80619
79867
  }
80620
79868
  });
80621
79869
  });
@@ -82730,7 +81978,7 @@ async function projectSensorMarkers(deps, data, timestamp) {
82730
81978
  capName: data.capName,
82731
81979
  kind: descriptor.kind,
82732
81980
  reason: "not-materialized",
82733
- ...outcome.status === "rejected" ? { error: errMsg$1(outcome.reason) } : {}
81981
+ ...outcome.status === "rejected" ? { error: errMsg(outcome.reason) } : {}
82734
81982
  });
82735
81983
  return;
82736
81984
  }
@@ -86360,947 +85608,6 @@ var RawGlobalStoreMirror = class {
86360
85608
  }
86361
85609
  };
86362
85610
  //#endregion
86363
- //#region src/pipeline-analytics/summary/group-crop.ts
86364
- /** Padding around the union, as a fraction of its own size. */
86365
- var GROUP_CROP_PADDING = .12;
86366
- /**
86367
- * Smallest crop, per axis, as a fraction of the frame.
86368
- *
86369
- * A quarter. Below that a thumbnail stops being legible at the sizes the
86370
- * gallery draws it, and the extra context costs nothing — the subjects stay the
86371
- * dominant thing in the picture at this size.
86372
- */
86373
- var MIN_CROP_FRACTION = .25;
86374
- /**
86375
- * The crop, or `null` when there is nothing to cut to — no visible member, or
86376
- * a frame with no dimensions. `null` means the caller keeps the full frame,
86377
- * which is the only honest answer when nobody could say where the group was.
86378
- */
86379
- function groupCropRect(input) {
86380
- const { frameWidth: fw, frameHeight: fh } = input;
86381
- if (fw <= 0 || fh <= 0) return null;
86382
- const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
86383
- if (boxes.length === 0) return null;
86384
- let left = Number.POSITIVE_INFINITY;
86385
- let top = Number.POSITIVE_INFINITY;
86386
- let right = Number.NEGATIVE_INFINITY;
86387
- let bottom = Number.NEGATIVE_INFINITY;
86388
- for (const b of boxes) {
86389
- left = Math.min(left, b.x);
86390
- top = Math.min(top, b.y);
86391
- right = Math.max(right, b.x + b.width);
86392
- bottom = Math.max(bottom, b.y + b.height);
86393
- }
86394
- const padX = (right - left) * GROUP_CROP_PADDING;
86395
- const padY = (bottom - top) * GROUP_CROP_PADDING;
86396
- let x0 = left - padX;
86397
- let y0 = top - padY;
86398
- let x1 = right + padX;
86399
- let y1 = bottom + padY;
86400
- const minW = fw * MIN_CROP_FRACTION;
86401
- const minH = fh * MIN_CROP_FRACTION;
86402
- if (x1 - x0 < minW) {
86403
- const cx = (x0 + x1) / 2;
86404
- x0 = cx - minW / 2;
86405
- x1 = cx + minW / 2;
86406
- }
86407
- if (y1 - y0 < minH) {
86408
- const cy = (y0 + y1) / 2;
86409
- y0 = cy - minH / 2;
86410
- y1 = cy + minH / 2;
86411
- }
86412
- const width = Math.min(x1 - x0, fw);
86413
- const height = Math.min(y1 - y0, fh);
86414
- const x = Math.max(0, Math.min(x0, fw - width));
86415
- const y = Math.max(0, Math.min(y0, fh - height));
86416
- return {
86417
- x: Math.round(x),
86418
- y: Math.round(y),
86419
- width: Math.round(width),
86420
- height: Math.round(height)
86421
- };
86422
- }
86423
- /**
86424
- * Carry a crop computed on the DETECTION frame onto the NATIVE surface.
86425
- *
86426
- * The two are different sizes by construction: detection runs on a downscaled
86427
- * frame and the group shot is fetched at up to 4096 px. Applying the rectangle
86428
- * raw would cut a corner of the picture and call it the group — the failure
86429
- * mode is silent and looks like a framing bug.
86430
- *
86431
- * Scaled per axis rather than by one factor, because the two surfaces are not
86432
- * guaranteed to share an aspect, and clamped to the native frame so rounding
86433
- * can never ask for a pixel that is not there.
86434
- */
86435
- function scaleCropToFrame(crop, from, to) {
86436
- if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
86437
- const sx = to.width / from.width;
86438
- const sy = to.height / from.height;
86439
- const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
86440
- const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
86441
- return {
86442
- x,
86443
- y,
86444
- width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
86445
- height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
86446
- };
86447
- }
86448
- //#endregion
86449
- //#region src/pipeline-analytics/summary/summary-score.ts
86450
- /** A member whose own box is covered this much by others is hidden. */
86451
- var OCCLUDED_COVER_FRACTION = .5;
86452
- /** A box covering this share of the frame scores full marks on area. */
86453
- var FULL_AREA_SHARE = .25;
86454
- var WEIGHT_CONFIDENCE = .3;
86455
- var WEIGHT_AREA = .25;
86456
- var WEIGHT_RECOGNITION = .25;
86457
- var WEIGHT_PLACEMENT = .2;
86458
- function intersectionArea(a, b) {
86459
- const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
86460
- const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
86461
- return w > 0 && h > 0 ? w * h : 0;
86462
- }
86463
- /**
86464
- * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
86465
- * capped at the own area: when two others overlap each other on top of this
86466
- * box the sum over-counts, which only ever calls a member MORE hidden, never
86467
- * less — the safe side for a shot meant to show everyone.
86468
- */
86469
- function coveredFraction(own, others) {
86470
- const area = own.width * own.height;
86471
- if (area <= 0) return 1;
86472
- const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
86473
- return Math.min(1, covered / area);
86474
- }
86475
- function clamp01(value) {
86476
- return Math.max(0, Math.min(1, value));
86477
- }
86478
- function placement(box, frameWidth, frameHeight) {
86479
- const cx = box.x + box.width / 2;
86480
- const cy = box.y + box.height / 2;
86481
- const dx = cx - frameWidth / 2;
86482
- const dy = cy - frameHeight / 2;
86483
- const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
86484
- return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
86485
- }
86486
- function memberQuality(d, frameWidth, frameHeight) {
86487
- const frameArea = frameWidth * frameHeight;
86488
- const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
86489
- return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
86490
- }
86491
- /** Score one frame for one summary. Pure; never throws on empty input. */
86492
- function scoreSummaryFrame(input) {
86493
- const present = input.detections.filter((d) => input.members.has(d.trackId));
86494
- const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
86495
- if (visible.length === 0) return {
86496
- visible: 0,
86497
- quality: 0,
86498
- crop: null
86499
- };
86500
- const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
86501
- const crop = groupCropRect({
86502
- boxes: visible.map((d) => ({
86503
- x: d.bbox.x,
86504
- y: d.bbox.y,
86505
- width: d.bbox.width,
86506
- height: d.bbox.height
86507
- })),
86508
- frameWidth: input.frameWidth,
86509
- frameHeight: input.frameHeight
86510
- });
86511
- return {
86512
- visible: visible.length,
86513
- quality,
86514
- crop
86515
- };
86516
- }
86517
- /**
86518
- * Lexicographic: more visible members wins outright; only equal counts fall
86519
- * through to quality. Positive when `a` is the better shot.
86520
- */
86521
- function compareSummaryScores(a, b) {
86522
- if (a.visible !== b.visible) return a.visible - b.visible;
86523
- return a.quality - b.quality;
86524
- }
86525
- var GroupShotTracker = class {
86526
- states = /* @__PURE__ */ new Map();
86527
- offered = 0;
86528
- materialised = 0;
86529
- byGap = 0;
86530
- byMargin = 0;
86531
- byWorse = 0;
86532
- /**
86533
- * Offer a frame. Returns `true` when it is worth materialising NOW — the
86534
- * caller's cue to fetch the pixels for THIS frame, while its handle is
86535
- * still leased.
86536
- *
86537
- * The clock is `frame.timestamp`; the caller injects no second one.
86538
- */
86539
- offer(summaryId, score, frame) {
86540
- if (score.visible === 0) return false;
86541
- this.offered += 1;
86542
- const state = this.states.get(summaryId);
86543
- if (state === void 0) {
86544
- this.states.set(summaryId, {
86545
- delivered: null,
86546
- lastAttemptAt: frame.timestamp,
86547
- unlanded: 1
86548
- });
86549
- this.materialised += 1;
86550
- return true;
86551
- }
86552
- if (state.lastAttemptAt !== void 0 && frame.timestamp - state.lastAttemptAt < 2e3) {
86553
- this.byGap += 1;
86554
- return false;
86555
- }
86556
- if (!this.isWorthPixels(state, score)) return false;
86557
- state.lastAttemptAt = frame.timestamp;
86558
- state.unlanded += 1;
86559
- this.materialised += 1;
86560
- return true;
86561
- }
86562
- /**
86563
- * Nothing has landed yet → keep trying (D58) until the budget is spent.
86564
- * Something has landed → beat it, by the margin on the quality tiebreak.
86565
- */
86566
- isWorthPixels(state, score) {
86567
- const delivered = state.delivered;
86568
- if (delivered === null) {
86569
- if (state.unlanded >= 4) {
86570
- this.byMargin += 1;
86571
- return false;
86572
- }
86573
- return true;
86574
- }
86575
- if (compareSummaryScores(score, delivered.score) <= 0) {
86576
- this.byWorse += 1;
86577
- return false;
86578
- }
86579
- if (score.visible === delivered.score.visible && score.quality - delivered.score.quality < .05) {
86580
- this.byMargin += 1;
86581
- return false;
86582
- }
86583
- return true;
86584
- }
86585
- /**
86586
- * A materialisation LANDED. Only this advances the baseline the next
86587
- * candidate must beat — a fetch that missed leaves the summary exactly as
86588
- * picture-less as it was, and the next frame may be worse and still worth
86589
- * taking.
86590
- */
86591
- noteLanded(summaryId, score, frame) {
86592
- const state = this.states.get(summaryId);
86593
- if (state === void 0) return;
86594
- state.delivered = {
86595
- score,
86596
- frame
86597
- };
86598
- state.unlanded = 0;
86599
- }
86600
- /** The shot on disk for this summary, as this process last saw it land. */
86601
- deliveredOf(summaryId) {
86602
- return this.states.get(summaryId)?.delivered ?? null;
86603
- }
86604
- /** A sealed or evicted summary takes no more frames; drop its state. */
86605
- forget(summaryId) {
86606
- this.states.delete(summaryId);
86607
- }
86608
- /**
86609
- * What the gate did since the last read. Shipped WITH the gate: without it
86610
- * the only evidence for a fetch that did NOT happen is hub-main's byte
86611
- * counter, which answers a different question a minute later.
86612
- */
86613
- drainCounts() {
86614
- const counts = {
86615
- offered: this.offered,
86616
- materialised: this.materialised,
86617
- byGap: this.byGap,
86618
- byMargin: this.byMargin,
86619
- byWorse: this.byWorse
86620
- };
86621
- this.offered = 0;
86622
- this.materialised = 0;
86623
- this.byGap = 0;
86624
- this.byMargin = 0;
86625
- this.byWorse = 0;
86626
- return counts;
86627
- }
86628
- get size() {
86629
- return this.states.size;
86630
- }
86631
- };
86632
- //#endregion
86633
- //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
86634
- /**
86635
- * Apply the group crop to the native surface, or hand it back untouched.
86636
- *
86637
- * Every reason to skip is a reason NOT to guess: no rectangle, no detection
86638
- * geometry to scale from, or a cropper this deployment does not have. The full
86639
- * frame is worse than a crop and far better than a wrong crop.
86640
- */
86641
- async function cropNative(native, input, deps) {
86642
- const crop = input.crop;
86643
- if (!crop || !deps.cropFrame) return native;
86644
- const fw = input.cropFrameWidth;
86645
- const fh = input.cropFrameHeight;
86646
- if (fw === void 0 || fh === void 0) return native;
86647
- const scaled = scaleCropToFrame(crop, {
86648
- width: fw,
86649
- height: fh
86650
- }, native);
86651
- if (scaled === null) return native;
86652
- if (scaled.width >= native.width && scaled.height >= native.height) return native;
86653
- try {
86654
- const cut = await deps.cropFrame(native, scaled);
86655
- if (cut === null) return native;
86656
- const expected = cut.width * cut.height * 3;
86657
- if (cut.format === "rgb" && cut.data.length !== expected) {
86658
- deps.logger.warn("group shot crop returned a surface that is not its geometry", {
86659
- tags: { deviceId: input.deviceId },
86660
- meta: {
86661
- summaryId: input.summaryId,
86662
- width: cut.width,
86663
- height: cut.height,
86664
- expectedBytes: expected,
86665
- gotBytes: cut.data.length
86666
- }
86667
- });
86668
- return native;
86669
- }
86670
- return cut;
86671
- } catch (err) {
86672
- deps.logger.warn("group shot crop failed — storing the full frame", {
86673
- tags: { deviceId: input.deviceId },
86674
- meta: {
86675
- summaryId: input.summaryId,
86676
- error: errMsg$1(err)
86677
- }
86678
- });
86679
- return native;
86680
- }
86681
- }
86682
- async function persistSummaryGroupShot(deps, input) {
86683
- const { deviceId, summaryId, frameHandle } = input;
86684
- const drop = {
86685
- tags: { deviceId },
86686
- meta: {
86687
- summaryId,
86688
- nodeId: frameHandle.nodeId
86689
- }
86690
- };
86691
- try {
86692
- const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
86693
- const native = result?.frame;
86694
- if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
86695
- deps.logger.warn("group shot native miss — the previous shot stands", {
86696
- ...drop,
86697
- meta: {
86698
- ...drop.meta,
86699
- shmId: frameHandle.shmId
86700
- }
86701
- });
86702
- return { kind: "native-miss" };
86703
- }
86704
- if (!keyFrameAcceptsTier(result.tier)) {
86705
- deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
86706
- ...drop,
86707
- meta: {
86708
- ...drop.meta,
86709
- shmId: frameHandle.shmId,
86710
- tier: result.tier
86711
- }
86712
- });
86713
- return { kind: "ram-tier" };
86714
- }
86715
- const surface = await cropNative(native, input, deps);
86716
- const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
86717
- const keyFrameMediaId = await deps.media.putReplacing({
86718
- deviceId,
86719
- ownerKind: "summary",
86720
- ownerId: summaryId,
86721
- kind: "keyFrame",
86722
- timestamp: input.timestamp,
86723
- data: keyFrame
86724
- });
86725
- try {
86726
- await deps.media.putReplacing({
86727
- deviceId,
86728
- ownerKind: "summary",
86729
- ownerId: summaryId,
86730
- kind: "keyFrameSmall",
86731
- timestamp: input.timestamp,
86732
- data: keyFrameSmall
86733
- });
86734
- } catch (err) {
86735
- deps.logger.warn("group shot web companion write failed — the native shot is stored", {
86736
- ...drop,
86737
- meta: {
86738
- ...drop.meta,
86739
- error: errMsg$1(err)
86740
- }
86741
- });
86742
- }
86743
- return {
86744
- kind: "landed",
86745
- keyFrameMediaId
86746
- };
86747
- } catch (err) {
86748
- const error = errMsg$1(err);
86749
- deps.logger.warn("group shot capture failed", {
86750
- ...drop,
86751
- meta: {
86752
- ...drop.meta,
86753
- error
86754
- }
86755
- });
86756
- return {
86757
- kind: "error",
86758
- error
86759
- };
86760
- }
86761
- }
86762
- //#endregion
86763
- //#region src/pipeline-analytics/summary/summary-composition.ts
86764
- function summaryComposition(members) {
86765
- const counts = /* @__PURE__ */ new Map();
86766
- for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
86767
- return { classes: [...counts].map(([className, count]) => ({
86768
- className,
86769
- count
86770
- })).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
86771
- }
86772
- //#endregion
86773
- //#region src/pipeline-analytics/summary/summary-member-labels.ts
86774
- function present(value) {
86775
- return typeof value === "string" && value.length > 0;
86776
- }
86777
- function withMemberLabels(members, tracks) {
86778
- const byId = new Map(tracks.map((t) => [t.trackId, t]));
86779
- return members.map((member) => {
86780
- const track = byId.get(member.trackId);
86781
- return {
86782
- ...member,
86783
- className: member.className ?? (present(track?.className) ? track.className : null),
86784
- ...present(track?.label) ? { label: track.label } : {},
86785
- ...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
86786
- };
86787
- });
86788
- }
86789
- //#endregion
86790
- //#region src/pipeline-analytics/summary/summary-proximity.ts
86791
- /**
86792
- * The measured gap, in fractions of the frame's diagonal-free normalisation
86793
- * (x by width, y by height). See the arithmetic above before moving it.
86794
- */
86795
- var DEFAULT_TOGETHER_FRACTION = .15;
86796
- /**
86797
- * Distance between two points as a fraction of the frame, or `null` when the
86798
- * frame is unknown. Each axis is normalised by its OWN dimension, so the answer
86799
- * does not change when a camera's aspect does.
86800
- */
86801
- function normalisedDistance(a, b, frame) {
86802
- if (!(frame.width > 0) || !(frame.height > 0)) return null;
86803
- const dx = (a.x - b.x) / frame.width;
86804
- const dy = (a.y - b.y) / frame.height;
86805
- return Math.sqrt(dx * dx + dy * dy);
86806
- }
86807
- //#endregion
86808
- //#region src/pipeline-analytics/summary/summary-join.ts
86809
- /**
86810
- * Which open group a newborn track joins — or none, and it opens its own.
86811
- *
86812
- * ## Why this is a separate decision from the store
86813
- *
86814
- * The store knows rows; only the caller can see where subjects ARE. Keeping the
86815
- * choice here means the store's `listOpen` + `absorbInto` stay a mechanism and
86816
- * this stays the policy, which is what let the policy change without the store
86817
- * changing underneath it.
86818
- *
86819
- * ## The two cases this exists for
86820
- *
86821
- * A summary used to be one per camera, so a subject at the far end of the drive
86822
- * was absorbed into the group at the door — a group it was never part of — and
86823
- * the halves of a group that split had nowhere to go. Both are the same need:
86824
- * a summary is a GROUP, not a camera session.
86825
- *
86826
- * ## Nearest, not first
86827
- *
86828
- * When two groups are within reach the closest wins. `listOpen` orders by
86829
- * `openedAt`, and which group opened first has nothing to do with who this
86830
- * subject is walking with — taking the first would make the answer depend on
86831
- * list order, which is the kind of tie-break that reads as random in a log.
86832
- *
86833
- * ## Knowing nothing is not a reason to take somebody in
86834
- *
86835
- * A group whose members have no position in RAM — one open across a restart —
86836
- * is skipped rather than joined. Joining on absent evidence is exactly how a
86837
- * far subject ends up in the wrong group, and nothing revisits that decision.
86838
- */
86839
- /**
86840
- * The id of the group to join, or `null` to open a new one.
86841
- *
86842
- * `null` on an unknown frame too: refusing is the recoverable direction, since
86843
- * the subject then gets its own group, while a wrong join merges two strangers.
86844
- */
86845
- function chooseGroupToJoin(subject, groups, frame, maxFraction) {
86846
- let bestId = null;
86847
- let bestDistance = Number.POSITIVE_INFINITY;
86848
- for (const g of groups) for (const m of g.memberPositions) {
86849
- const d = normalisedDistance(subject, m, frame);
86850
- if (d === null || d > maxFraction) continue;
86851
- if (d < bestDistance) {
86852
- bestDistance = d;
86853
- bestId = g.id;
86854
- }
86855
- }
86856
- return bestId;
86857
- }
86858
- //#endregion
86859
- //#region src/pipeline-analytics/summary/summary-split.ts
86860
- /**
86861
- * A group that walked apart — who leaves, and who keeps the summary.
86862
- *
86863
- * ## The case
86864
- *
86865
- * Membership is decided ONCE, at birth, by proximity (D489). That answers "who
86866
- * arrived together" and says nothing about what happens next: two people who
86867
- * meet at the gate and then go separate ways stay one group until the camera
86868
- * falls quiet, and the summary then claims they were together for the whole
86869
- * session. The operator asked for this case by name alongside the concurrent
86870
- * ones — "un gruppo che si divide" — and they are the same need, because the
86871
- * halves of a split have to be able to BE groups.
86872
- *
86873
- * ## Single linkage, deliberately
86874
- *
86875
- * A family in single file is a chain: the last of them is nowhere near the
86876
- * first, and requiring every pair to be close would tear the group apart the
86877
- * moment it grew past two. So two members are together when a chain of
86878
- * within-threshold hops connects them — the same "any, not all" rule
86879
- * {@link isTogetherWith} uses at birth, applied to the whole set.
86880
- *
86881
- * ## What an absent position means
86882
- *
86883
- * Nothing. A member whose track has closed has no live position, and that is
86884
- * not evidence it walked off — it is the ordinary end of a track. Unplaced
86885
- * members are never candidates to leave; they stay with whoever keeps the
86886
- * summary. Likewise an unknown frame yields a `null` distance, and a null is
86887
- * not "far": it produces ONE cluster, never a split (D393).
86888
- *
86889
- * ## Which side keeps the row
86890
- *
86891
- * The primary's cluster, because the summary's identity and its group shot are
86892
- * already the primary's. When the primary cannot be placed — its track closed —
86893
- * the largest cluster keeps it instead; refusing to split there would freeze
86894
- * every group the moment its primary left the frame, which is the common way
86895
- * for a group to break up in the first place.
86896
- */
86897
- /**
86898
- * Members grouped by single-linkage proximity. One cluster when nothing can be
86899
- * measured — an unknown frame is not a reason to tear a group in half.
86900
- */
86901
- function clusterByProximity(members, frame, maxFraction) {
86902
- const parent = members.map((_, i) => i);
86903
- const find = (i) => {
86904
- let root = i;
86905
- while (parent[root] !== root) root = parent[root];
86906
- return root;
86907
- };
86908
- const union = (a, b) => {
86909
- const ra = find(a);
86910
- const rb = find(b);
86911
- if (ra !== rb) parent[rb] = ra;
86912
- };
86913
- let measured = false;
86914
- for (let i = 0; i < members.length; i++) for (let j = i + 1; j < members.length; j++) {
86915
- const d = normalisedDistance(members[i].position, members[j].position, frame);
86916
- if (d === null) continue;
86917
- measured = true;
86918
- if (d <= maxFraction) union(i, j);
86919
- }
86920
- if (!measured && members.length > 1) return [members.map((m) => m.trackId)];
86921
- const byRoot = /* @__PURE__ */ new Map();
86922
- for (let i = 0; i < members.length; i++) {
86923
- const root = find(i);
86924
- const bucket = byRoot.get(root);
86925
- const trackId = members[i].trackId;
86926
- if (bucket === void 0) byRoot.set(root, [trackId]);
86927
- else bucket.push(trackId);
86928
- }
86929
- return [...byRoot.values()];
86930
- }
86931
- /**
86932
- * The departures, or `null` when the group is still whole.
86933
- *
86934
- * Order is the input's: the clusters come out in the order their first member
86935
- * appears, so a caller logging a split reads the same order twice running.
86936
- */
86937
- function planSplit(subject, frame, maxFraction) {
86938
- if (subject.placed.length < 2) return null;
86939
- const clusters = clusterByProximity(subject.placed, frame, maxFraction);
86940
- if (clusters.length < 2) return null;
86941
- const keeper = clusters.find((c) => subject.primaryTrackId !== null && c.includes(subject.primaryTrackId)) ?? largest(clusters);
86942
- return { departures: clusters.filter((c) => c !== keeper) };
86943
- }
86944
- /** The biggest cluster; ties go to the one that appeared first. */
86945
- function largest(clusters) {
86946
- let best = clusters[0];
86947
- for (const c of clusters) if (c.length > best.length) best = c;
86948
- return best;
86949
- }
86950
- function errMsg(err) {
86951
- return err instanceof Error ? err.message : String(err);
86952
- }
86953
- var SummarySealer = class {
86954
- deps;
86955
- sweepInFlight = false;
86956
- /**
86957
- * What the FRAME path may ask at frame-rate: EVERY open group of a camera,
86958
- * with the members it has seen THIS process open. Fed only by this class's
86959
- * own writes, never the authority (D224): a fresh process answers an empty
86960
- * list until a track opens, and a summary open across a restart is scored on
86961
- * the members that open after it — a weaker shot, never a wrong one.
86962
- *
86963
- * Keyed by summary id INSIDE the camera, because a camera holds as many
86964
- * groups as it has (D489). It was one entry per camera for one release, and
86965
- * that silently threw away the group shot of every group but the last to
86966
- * open — the frame path scored the newest group's members and no other.
86967
- */
86968
- openMirror = /* @__PURE__ */ new Map();
86969
- /**
86970
- * When each open group was FIRST seen divided, by summary id. Cleared the
86971
- * moment it is whole again, so a separation has to hold without interruption
86972
- * — a group that oscillates never accumulates its window.
86973
- *
86974
- * RAM only, and deliberately: after a restart a group that is still apart
86975
- * simply waits {@link SPLIT_CONFIRM_MS} again. Persisting it would buy one
86976
- * earlier split and cost a column nothing else reads.
86977
- */
86978
- apartSince = /* @__PURE__ */ new Map();
86979
- constructor(deps) {
86980
- this.deps = deps;
86981
- }
86982
- /** See {@link SUMMARY_GROUPS_ENABLED}; production never overrides it. */
86983
- get groupsEnabled() {
86984
- return this.deps.groupsEnabled ?? false;
86985
- }
86986
- /**
86987
- * The camera's open groups, oldest entry first. Empty when none is mirrored —
86988
- * which means "this process has opened none", not "the camera has none".
86989
- */
86990
- openGroupsFor(deviceId) {
86991
- const groups = this.openMirror.get(deviceId);
86992
- if (groups === void 0) return [];
86993
- return [...groups].map(([id, members]) => ({
86994
- id,
86995
- members
86996
- }));
86997
- }
86998
- /**
86999
- * Join the open group this subject is walking WITH, or open its own.
87000
- *
87001
- * Falls back to the store's own policy (the camera's oldest open group)
87002
- * whenever the evidence is missing — no birth position, no frame size, no
87003
- * member positions. That fallback is today's behaviour, so an unknowable
87004
- * case degrades to what shipped rather than to nothing.
87005
- */
87006
- async joinOrOpen(input, position) {
87007
- const frame = this.deps.frameSize(input.deviceId);
87008
- if (position === void 0 || frame === null) return await this.deps.summaries.openOrAbsorb(input);
87009
- const open = await this.deps.summaries.listOpen(input.deviceId);
87010
- if (open.length === 0) return await this.deps.summaries.openNew(input);
87011
- const groups = [];
87012
- for (const summary of open) {
87013
- const members = await this.deps.summaries.membersOf(summary.id);
87014
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
87015
- groups.push({
87016
- id: summary.id,
87017
- memberPositions: [...positions.values()]
87018
- });
87019
- }
87020
- const chosenId = chooseGroupToJoin(position, groups, frame, this.deps.togetherFraction(input.deviceId));
87021
- const chosen = open.find((s) => s.id === chosenId);
87022
- if (chosen === void 0) {
87023
- this.deps.logger.debug("summary: nobody to walk with — opening a group", {
87024
- tags: { deviceId: input.deviceId },
87025
- meta: {
87026
- trackId: input.trackId,
87027
- openGroups: open.length
87028
- }
87029
- });
87030
- return await this.deps.summaries.openNew(input);
87031
- }
87032
- return await this.deps.summaries.absorbInto(chosen, input);
87033
- }
87034
- async onTrackOpened(input) {
87035
- if (!this.groupsEnabled) return;
87036
- try {
87037
- if (!isSummaryEligible(input.trackId, input.className)) {
87038
- this.deps.logger.debug("summary: track is not a group member — left standalone", {
87039
- tags: { deviceId: input.deviceId },
87040
- meta: {
87041
- trackId: input.trackId,
87042
- className: input.className ?? null
87043
- }
87044
- });
87045
- return;
87046
- }
87047
- const at = this.deps.now();
87048
- const open = {
87049
- deviceId: input.deviceId,
87050
- trackId: input.trackId,
87051
- at,
87052
- className: input.className
87053
- };
87054
- const row = await this.joinOrOpen(open, input.position);
87055
- this.rememberMember(input.deviceId, row.id, input.trackId);
87056
- } catch (err) {
87057
- this.deps.logger.warn("summary: could not open or absorb for a track", {
87058
- tags: { deviceId: input.deviceId },
87059
- meta: {
87060
- trackId: input.trackId,
87061
- error: errMsg(err)
87062
- }
87063
- });
87064
- }
87065
- }
87066
- /** One sweep at a time (THE latch). A tick that finds one in flight returns. */
87067
- async sweep() {
87068
- if (this.sweepInFlight) return;
87069
- this.sweepInFlight = true;
87070
- try {
87071
- const mine = this.deps.analysedDeviceIds();
87072
- const open = [];
87073
- for (const deviceId of mine) open.push(...await this.deps.summaries.listOpen(deviceId));
87074
- for (const summary of open) {
87075
- const born = await this.splitIfApart(summary);
87076
- await this.sealIfDone(summary);
87077
- if (born !== null) await this.sealIfDone(born);
87078
- }
87079
- } catch (err) {
87080
- this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
87081
- } finally {
87082
- this.sweepInFlight = false;
87083
- }
87084
- }
87085
- /**
87086
- * Split an open group whose members have walked apart and STAYED apart.
87087
- *
87088
- * Returns the last group born, so the sweep can consider it for sealing in
87089
- * the same pass — a group that divided as everybody left would otherwise sit
87090
- * open until the next tick with nothing alive in it.
87091
- *
87092
- * Every failure here is swallowed into a log: a split is an improvement to a
87093
- * record that is already correct about WHO was present, and losing it is
87094
- * never worth losing the seal that follows.
87095
- */
87096
- async splitIfApart(summary) {
87097
- const { id: summaryId, deviceId } = summary;
87098
- try {
87099
- const frame = this.deps.frameSize(deviceId);
87100
- if (frame === null) {
87101
- this.apartSince.delete(summaryId);
87102
- return null;
87103
- }
87104
- const members = await this.deps.summaries.membersOf(summaryId);
87105
- if (members.length < 2) {
87106
- this.apartSince.delete(summaryId);
87107
- return null;
87108
- }
87109
- const positions = await this.deps.latestPositions(members.map((m) => m.trackId));
87110
- const placed = [];
87111
- for (const m of members) {
87112
- const position = positions.get(m.trackId);
87113
- if (position !== void 0) placed.push({
87114
- trackId: m.trackId,
87115
- position
87116
- });
87117
- }
87118
- const plan = planSplit({
87119
- placed,
87120
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null
87121
- }, frame, this.deps.togetherFraction(deviceId));
87122
- if (plan === null) {
87123
- this.apartSince.delete(summaryId);
87124
- return null;
87125
- }
87126
- const firstSeen = this.apartSince.get(summaryId);
87127
- const now = this.deps.now();
87128
- if (firstSeen === void 0) {
87129
- this.apartSince.set(summaryId, now);
87130
- return null;
87131
- }
87132
- if (now - firstSeen < 0) return null;
87133
- let last = null;
87134
- let row = summary;
87135
- for (const departing of plan.departures) {
87136
- const born = await this.deps.summaries.splitOff(row, departing, now);
87137
- if (born === null) continue;
87138
- this.moveMirrorMembers(deviceId, summaryId, born.id, departing);
87139
- row = {
87140
- ...row,
87141
- memberCount: row.memberCount - departing.length
87142
- };
87143
- last = born;
87144
- }
87145
- this.apartSince.delete(summaryId);
87146
- return last;
87147
- } catch (err) {
87148
- this.deps.logger.warn("summary: could not divide a group that walked apart", {
87149
- tags: { deviceId },
87150
- meta: {
87151
- summaryId,
87152
- error: errMsg(err)
87153
- }
87154
- });
87155
- return null;
87156
- }
87157
- }
87158
- /**
87159
- * Is this group finished?
87160
- *
87161
- * Both questions are about THIS group's members, not about the camera. A
87162
- * camera holds as many groups as it has (D489), so a camera-wide gate means
87163
- * no group seals until every subject on the camera has gone — and on a busy
87164
- * camera they never all go at once. Measured: eight summaries open on one
87165
- * camera over seventeen minutes, none sealed, each new subject opening a
87166
- * ninth because a group whose members have closed has no positions left for
87167
- * the proximity rule to judge.
87168
- *
87169
- * `null` means "could not find out", and the caller treats that as NOT done —
87170
- * a read that failed must never seal a live session (D49).
87171
- */
87172
- async isGroupFinished(summary, members) {
87173
- const { id: summaryId, deviceId } = summary;
87174
- let openTracks;
87175
- try {
87176
- openTracks = await this.deps.openTrackIds(deviceId);
87177
- } catch (err) {
87178
- this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
87179
- tags: { deviceId },
87180
- meta: {
87181
- summaryId,
87182
- error: errMsg(err)
87183
- }
87184
- });
87185
- return null;
87186
- }
87187
- const stillOpen = new Set(openTracks);
87188
- if (members.some((m) => stillOpen.has(m))) return false;
87189
- let parked;
87190
- try {
87191
- parked = await this.deps.parkedTrackIds(deviceId);
87192
- } catch (err) {
87193
- this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
87194
- tags: { deviceId },
87195
- meta: {
87196
- summaryId,
87197
- error: errMsg(err)
87198
- }
87199
- });
87200
- return null;
87201
- }
87202
- return !members.some((m) => parked.has(m));
87203
- }
87204
- async sealIfDone(summary) {
87205
- const { id: summaryId, deviceId } = summary;
87206
- let members;
87207
- try {
87208
- members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
87209
- } catch (err) {
87210
- this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
87211
- tags: { deviceId },
87212
- meta: {
87213
- summaryId,
87214
- error: errMsg(err)
87215
- }
87216
- });
87217
- return;
87218
- }
87219
- if (await this.isGroupFinished(summary, members) !== true) return;
87220
- const sealedAt = this.deps.now();
87221
- try {
87222
- if (await this.deps.summaries.seal(summary, sealedAt)) {
87223
- const members = this.forgetGroup(deviceId, summaryId);
87224
- this.deps.onSealed?.(summaryId, members);
87225
- this.deps.logger.info("summary sealed", {
87226
- tags: { deviceId },
87227
- meta: {
87228
- summaryId,
87229
- sealedAt
87230
- }
87231
- });
87232
- }
87233
- } catch (err) {
87234
- this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
87235
- tags: { deviceId },
87236
- meta: {
87237
- summaryId,
87238
- error: errMsg(err)
87239
- }
87240
- });
87241
- }
87242
- }
87243
- rememberMember(deviceId, summaryIdValue, trackId) {
87244
- let groups = this.openMirror.get(deviceId);
87245
- if (groups === void 0) {
87246
- groups = /* @__PURE__ */ new Map();
87247
- this.openMirror.set(deviceId, groups);
87248
- }
87249
- const members = groups.get(summaryIdValue);
87250
- if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
87251
- else members.add(trackId);
87252
- }
87253
- /** Follows a split in the mirror, so the frame path scores each half on its
87254
- * own members from the very next frame. */
87255
- moveMirrorMembers(deviceId, fromId, toId, trackIds) {
87256
- const groups = this.openMirror.get(deviceId);
87257
- const from = groups?.get(fromId);
87258
- for (const trackId of trackIds) {
87259
- from?.delete(trackId);
87260
- this.rememberMember(deviceId, toId, trackId);
87261
- }
87262
- if (from !== void 0 && from.size === 0) groups?.delete(fromId);
87263
- }
87264
- /** Drops one group from the mirror and returns the members it held. */
87265
- forgetGroup(deviceId, summaryIdValue) {
87266
- this.apartSince.delete(summaryIdValue);
87267
- const groups = this.openMirror.get(deviceId);
87268
- const members = groups?.get(summaryIdValue) ?? /* @__PURE__ */ new Set();
87269
- groups?.delete(summaryIdValue);
87270
- if (groups !== void 0 && groups.size === 0) this.openMirror.delete(deviceId);
87271
- return members;
87272
- }
87273
- };
87274
- //#endregion
87275
- //#region src/pipeline-analytics/summary-settings.ts
87276
- /**
87277
- * Per-device summary (group) settings. Cascade: a per-device override on top of
87278
- * the global default, resolved per field — an invalid or missing value falls
87279
- * back to its default and parsing never throws. Mirrors `stationary-settings` /
87280
- * `media-settings`.
87281
- *
87282
- * The default IS the constant the proximity rule shipped with, imported and not
87283
- * copied, so an unset value and a reset value both resolve to exactly today's
87284
- * behaviour. It is per camera because a threshold belongs to whoever generates
87285
- * the event: a gate camera framing three metres of path and a drive camera
87286
- * framing twenty disagree about what "walking together" looks like in a
87287
- * fraction of the frame, and the fleet measurement behind the default is a
87288
- * median across both kinds.
87289
- */
87290
- var SummarySettingsSchema = object({
87291
- /**
87292
- * How close two subjects must be to count as one group, as a fraction of the
87293
- * frame. The floor is not zero: zero means "only at the identical pixel",
87294
- * which ends grouping on that camera while looking like a tuning choice.
87295
- * See `summary/summary-proximity.ts` for where the default came from.
87296
- */
87297
- summaryTogetherFraction: number().min(.02).max(1).default(DEFAULT_TOGETHER_FRACTION) });
87298
- var SUMMARY_DEFAULTS = { togetherFraction: SummarySettingsSchema.parse({}).summaryTogetherFraction };
87299
- function resolveSummarySettings(raw) {
87300
- const parsed = SummarySettingsSchema.shape.summaryTogetherFraction.safeParse(raw["summaryTogetherFraction"]);
87301
- return { togetherFraction: parsed.success ? parsed.data : SUMMARY_DEFAULTS.togetherFraction };
87302
- }
87303
- //#endregion
87304
85611
  //#region src/pipeline-analytics/track-retention-sweep.ts
87305
85612
  /**
87306
85613
  * Sweep every device with persisted tracks: skip retention-disabled devices,
@@ -88306,8 +86613,6 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
88306
86613
  * the others) — a longer drain would be killed mid-way and log nothing.
88307
86614
  */
88308
86615
  var SENSOR_HANDLER_DRAIN_MS = 2700;
88309
- /** Throttle for the group-shot gate's window line (D470). */
88310
- var GROUP_SHOT_METRIC_INTERVAL_MS = 3e4;
88311
86616
  /**
88312
86617
  * How often the addon asks "is a gallery sample outside the cluster face
88313
86618
  * space?". A flip is rare and the check is two cheap reads — a minute keeps
@@ -88585,21 +86890,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88585
86890
  storageMigrationLeaseId = null;
88586
86891
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
88587
86892
  sensorEventStore = null;
88588
- /** The per-camera session envelope (D359). Bound with the other stores at boot. */
88589
- summaryStore = null;
88590
- /** The ONLY writer of a summary's lifecycle (D359). */
88591
- summarySealer = null;
88592
- /** The materialisation gate on every open summary's group shot (D359 phase 3, D470). RAM only. */
88593
- groupShots = new GroupShotTracker();
88594
- /** Throttle for the group-shot gate's window line (D470). */
88595
- lastGroupShotMetricAt = 0;
88596
- /**
88597
- * Tracks a gallery match has named, for the group-shot score's recognition
88598
- * term. "Recognised so far" rather than "at that instant": the match arrives
88599
- * on the detail plane, after the frame; the frames that follow carry it.
88600
- * Pruned when the track's summary seals, so it is bounded by open summaries.
88601
- */
88602
- recognizedTrackIds = /* @__PURE__ */ new Set();
88603
86893
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
88604
86894
  * so the DeviceStateChanged handler stays cheap. */
88605
86895
  linkedCamerasCache = null;
@@ -88880,7 +87170,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88880
87170
  };
88881
87171
  this.ctx.logger.info("nc-action data-plane served", { meta: { served: served !== null } });
88882
87172
  } catch (err) {
88883
- this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg$1(err) } });
87173
+ this.ctx.logger.warn("nc-action data-plane failed to serve", { meta: { error: errMsg(err) } });
88884
87174
  }
88885
87175
  }
88886
87176
  /** Mints a button's callback URL once the action plane is served; null until
@@ -88914,7 +87204,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88914
87204
  await store.sweep();
88915
87205
  await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
88916
87206
  } catch (err) {
88917
- this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg$1(err) } });
87207
+ this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg(err) } });
88918
87208
  }
88919
87209
  };
88920
87210
  run();
@@ -88984,7 +87274,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
88984
87274
  retentionDays: store.retentionMs === null ? "forever" : Math.round(store.retentionMs / 864e5)
88985
87275
  } });
88986
87276
  } catch (err) {
88987
- this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg$1(err) } });
87277
+ this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
88988
87278
  }
88989
87279
  }
88990
87280
  /** Public base URL for event thumbnails: `/addon/<addonId>/event-media`.
@@ -89121,11 +87411,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89121
87411
  * the partition + promotion + sweep call sites — an operator change takes
89122
87412
  * effect within one SETTINGS_CACHE_TTL_MS tick without an addon restart. */
89123
87413
  stationarySettingsCache = this.settingsCaches.registerDeviceCache("stationary", SETTINGS_CACHE_TTL_MS);
89124
- /** Per-device summary (group) settings — the proximity threshold. Read
89125
- * SYNCHRONOUSLY by the sealer's birth and split paths, so it is warmed on
89126
- * the frame path like the stationary one and a cold miss answers the
89127
- * measured default. */
89128
- summarySettingsCache = this.settingsCaches.registerDeviceCache("summary", SETTINGS_CACHE_TTL_MS);
89129
87414
  /** Per-device STILLNESS SCALE cache — the measured "how slow is settled" bar
89130
87415
  * (`stillness-settings.ts`). Read once per FrameProcessor build, so this is
89131
87416
  * a cold-path cache; it exists so a calibration applied through
@@ -89255,9 +87540,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89255
87540
  * line/min/device instead of one per §5 cadence tick, see media-capture-log.ts). */
89256
87541
  mediaCaptureLog = new MediaCaptureLogAggregator();
89257
87542
  /** Per-device hit/miss window for the native `keyFrame` path (2026-08-06).
89258
- * Before it there was NO metric here at all — `fetchNativeFullFrameTiered`
89259
- * never touched `bumpCropMetric` — so whether a key-frame fix worked could
89260
- * only be answered by hand-counting Loki lines. See `key-frame-metrics.ts`. */
87543
+ * Before it there was NO metric here at all — the by-handle key-frame fetch
87544
+ * in `runtime/native-frame-transport.ts` (retired by D422) never touched
87545
+ * `bumpCropMetric` — so whether a key-frame fix worked could only be
87546
+ * answered by hand-counting Loki lines. See `key-frame-metrics.ts`. */
89261
87547
  keyFrameCaptureLog = new KeyFrameCaptureLog();
89262
87548
  /** The registry's own liveness answer, mirrored off `ClusterTopologySnapshot`,
89263
87549
  * so a capture is never dispatched at a node the hub already called dead.
@@ -89287,7 +87573,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89287
87573
  tags: { deviceId },
89288
87574
  meta: {
89289
87575
  deviceId,
89290
- error: errMsg$1(error)
87576
+ error: errMsg(error)
89291
87577
  }
89292
87578
  });
89293
87579
  }
@@ -89316,7 +87602,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89316
87602
  tags: { deviceId },
89317
87603
  meta: {
89318
87604
  deviceId,
89319
- error: errMsg$1(error)
87605
+ error: errMsg(error)
89320
87606
  }
89321
87607
  });
89322
87608
  }
@@ -89391,12 +87677,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89391
87677
  });
89392
87678
  }
89393
87679
  captureCrop = null;
89394
- /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
89395
- * to the owning runner, PLUS the source `tier`. Captured in the async init
89396
- * block so `persistKeyFrames` can fetch the native full frame for the
89397
- * `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
89398
- * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
89399
- getNativeKeyFrameRgb = null;
89400
87680
  shuttingDown = false;
89401
87681
  /**
89402
87682
  * Sensor handlers the bus dispatched under `void` that have not finished.
@@ -89896,7 +88176,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
89896
88176
  logger,
89897
88177
  noteCropOutcome: (deviceId, reason) => this.failureReport.noteEnrichmentCrop(deviceId, reason)
89898
88178
  });
89899
- this.getNativeKeyFrameRgb = transport.getNativeKeyFrameRgb;
89900
88179
  this.parkedFrames = createParkedFrameClient({
89901
88180
  api: api.pipelineRunner,
89902
88181
  logger: logger.child("ParkedFrames"),
@@ -90109,7 +88388,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90109
88388
  */
90110
88389
  armAlarmPanelDeclaration(api) {
90111
88390
  this.declareAlarmPanel(api).catch((err) => {
90112
- this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg$1(err) } });
88391
+ this.ctx.logger.warn("alarm panel declaration could not be armed — the Alarm tab stays empty until this addon restarts", { meta: { error: errMsg(err) } });
90113
88392
  });
90114
88393
  }
90115
88394
  async declareAlarmPanel(api) {
@@ -90233,7 +88512,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90233
88512
  MediaStore.declare(api.settingsStore),
90234
88513
  EventStore.declare(api.settingsStore),
90235
88514
  SensorEventStore.declare(api.settingsStore),
90236
- SummaryStore.declare(api.settingsStore),
90237
88515
  IdentityStore.declare(api.settingsStore),
90238
88516
  FaceStore.declare(api.settingsStore),
90239
88517
  PlateStore.declare(api.settingsStore),
@@ -90262,7 +88540,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90262
88540
  logger: this.ctx.logger.child("viewer-settings")
90263
88541
  });
90264
88542
  } catch (err) {
90265
- this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg$1(err) } });
88543
+ this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg(err) } });
90266
88544
  }
90267
88545
  runLabelTierMigration({
90268
88546
  store: api.settingsStore,
@@ -90360,7 +88638,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90360
88638
  try {
90361
88639
  locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
90362
88640
  } catch (err) {
90363
- logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg$1(err) } });
88641
+ logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: errMsg(err) } });
90364
88642
  this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
90365
88643
  return DEFAULT_MEDIA_LOCATION;
90366
88644
  }
@@ -90541,49 +88819,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90541
88819
  store: api.settingsStore,
90542
88820
  logger: logger.child("SensorEventStore")
90543
88821
  });
90544
- this.summaryStore = new SummaryStore({
90545
- trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
90546
- store: api.settingsStore,
90547
- logger: this.ctx.logger.child("summary"),
90548
- onEvict: async (ids) => {
90549
- await this.mediaStore?.deleteForOwner("summary", ids);
90550
- }
90551
- });
90552
- this.summarySealer = new SummarySealer({
90553
- summaries: this.summaryStore,
90554
- analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
90555
- openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
90556
- parkedTrackIds: async (deviceId) => {
90557
- const ids = /* @__PURE__ */ new Set();
90558
- for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
90559
- return ids;
90560
- },
90561
- now: () => Date.now(),
90562
- logger: this.ctx.logger.child("summary-sealer"),
90563
- frameSize: (deviceId) => {
90564
- const d = this.lastFrameDimsByDevice.get(deviceId);
90565
- return d === void 0 ? null : {
90566
- width: d.w,
90567
- height: d.h
90568
- };
90569
- },
90570
- latestPositions: async (trackIds) => {
90571
- const out = /* @__PURE__ */ new Map();
90572
- for (const id of trackIds) {
90573
- const last = (this.trackStore?.peekActive(id))?.positions.at(-1);
90574
- if (last !== void 0) out.set(id, {
90575
- x: last.x,
90576
- y: last.y
90577
- });
90578
- }
90579
- return out;
90580
- },
90581
- togetherFraction: (deviceId) => this.summarySettingsFromCache(deviceId).togetherFraction,
90582
- onSealed: (summaryId, members) => {
90583
- this.groupShots.forget(summaryId);
90584
- for (const trackId of members) this.recognizedTrackIds.delete(trackId);
90585
- }
90586
- });
90587
88822
  this.packageDropDetector = new PackageDropDetector({
90588
88823
  events: eventStore,
90589
88824
  emit: {
@@ -90621,7 +88856,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90621
88856
  onPersisted: (ev, phase) => this.notificationCenter?.onPackageEventPersisted(ev, phase),
90622
88857
  onError: (scope, err) => logger.warn("package-drop detector error", { meta: {
90623
88858
  scope,
90624
- error: errMsg$1(err)
88859
+ error: errMsg(err)
90625
88860
  } })
90626
88861
  });
90627
88862
  this.linkedCamerasCache = new LinkedCamerasCache({
@@ -90637,7 +88872,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90637
88872
  ...cameraId !== void 0 ? { tags: { deviceId: cameraId } } : {},
90638
88873
  meta: {
90639
88874
  scope,
90640
- error: errMsg$1(err)
88875
+ error: errMsg(err)
90641
88876
  }
90642
88877
  })
90643
88878
  });
@@ -90649,7 +88884,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90649
88884
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90650
88885
  meta: {
90651
88886
  scope,
90652
- error: errMsg$1(err)
88887
+ error: errMsg(err)
90653
88888
  }
90654
88889
  })
90655
88890
  });
@@ -90662,7 +88897,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90662
88897
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
90663
88898
  meta: {
90664
88899
  scope,
90665
- error: errMsg$1(err)
88900
+ error: errMsg(err)
90666
88901
  }
90667
88902
  })
90668
88903
  });
@@ -90672,7 +88907,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90672
88907
  logger: logger.child("AudioMarkers"),
90673
88908
  onError: (scope, err) => logger.warn("audio marker error", { meta: {
90674
88909
  scope,
90675
- error: errMsg$1(err)
88910
+ error: errMsg(err)
90676
88911
  } })
90677
88912
  });
90678
88913
  const galleryVectorStore = api.vectorStore;
@@ -90783,7 +89018,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
90783
89018
  score: face.matchScore ?? face.confidence,
90784
89019
  confidence: face.confidence,
90785
89020
  ...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
90786
- }, face.identityId !== void 0 ? "identified" : "evidence"),
89021
+ }),
90787
89022
  onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
90788
89023
  faceSourceFrames: new FaceSourceFrameKeeper({
90789
89024
  port: createFaceSourceParkPort({
@@ -91029,7 +89264,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91029
89264
  } catch (err) {
91030
89265
  this.ctx.logger.debug("zone ownership read failed", {
91031
89266
  tags: { deviceId },
91032
- meta: { error: errMsg$1(err) }
89267
+ meta: { error: errMsg(err) }
91033
89268
  });
91034
89269
  }
91035
89270
  return out;
@@ -91089,7 +89324,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91089
89324
  } catch (err) {
91090
89325
  return {
91091
89326
  ok: false,
91092
- error: errMsg$1(err)
89327
+ error: errMsg(err)
91093
89328
  };
91094
89329
  }
91095
89330
  },
@@ -91109,7 +89344,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91109
89344
  } catch (err) {
91110
89345
  return {
91111
89346
  ok: false,
91112
- error: errMsg$1(err)
89347
+ error: errMsg(err)
91113
89348
  };
91114
89349
  }
91115
89350
  },
@@ -91177,6 +89412,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91177
89412
  readExportBytes: (input) => api.recordingExport.readExportBytes.query(input),
91178
89413
  deleteExport: (input) => api.recordingExport.deleteExport.mutate(input)
91179
89414
  }, req),
89415
+ deriveGifFromClip: (req) => deriveRecordedGif(buildRecordedGifPorts({
89416
+ logger: logger.child("RecordedGif"),
89417
+ resolveFfmpeg: () => this.ctx.deps.ensureFfmpeg()
89418
+ }), req),
91180
89419
  publishArtifact: async (bytes, mime, deviceId) => await this.ncArtifactPlane?.publish(bytes, mime, deviceId) ?? null,
91181
89420
  viewerUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
91182
89421
  indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
@@ -91311,30 +89550,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91311
89550
  confirmed: t.observations !== void 0 && t.observations >= minHits
91312
89551
  }));
91313
89552
  },
91314
- listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
91315
- const summaryStore = this.summaryStore;
91316
- if (!summaryStore) return [];
91317
- const rows = await summaryStore.listRecent({
91318
- deviceIds: [deviceId],
91319
- since: sinceMs,
91320
- until: untilMs,
91321
- limit: 500
91322
- });
91323
- const out = [];
91324
- for (const row of rows) {
91325
- const members = await summaryStore.membersOf(row.id);
91326
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
91327
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
91328
- out.push({
91329
- summaryId: row.id,
91330
- deviceId: row.deviceId,
91331
- memberTrackIds: members.map((m) => m.trackId),
91332
- primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
91333
- groupShotMediaId: shot?.mediaId ?? null
91334
- });
91335
- }
91336
- return out;
91337
- },
91338
89553
  listMediaInfo: async (id) => {
91339
89554
  return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
91340
89555
  mediaId: r.mediaId,
@@ -91561,7 +89776,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91561
89776
  } catch (err) {
91562
89777
  this.ctx.logger.warn("event-media resolve failed", { meta: {
91563
89778
  address,
91564
- error: errMsg$1(err)
89779
+ error: errMsg(err)
91565
89780
  } });
91566
89781
  throw err;
91567
89782
  }
@@ -91582,7 +89797,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91582
89797
  access: planeAccess("event-media")
91583
89798
  } });
91584
89799
  } catch (err) {
91585
- this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89800
+ this.ctx.logger.warn("event-media data-plane failed to serve", { meta: { error: errMsg(err) } });
91586
89801
  }
91587
89802
  }
91588
89803
  /**
@@ -91608,7 +89823,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91608
89823
  handler
91609
89824
  }) ?? null;
91610
89825
  } catch (err) {
91611
- this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
89826
+ this.ctx.logger.warn("training-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91612
89827
  }
91613
89828
  }
91614
89829
  /**
@@ -91900,7 +90115,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91900
90115
  handler
91901
90116
  }) ?? null;
91902
90117
  } catch (err) {
91903
- this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
90118
+ this.ctx.logger.warn("debug-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91904
90119
  }
91905
90120
  }
91906
90121
  /**
@@ -91929,7 +90144,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91929
90144
  handler
91930
90145
  }) ?? null;
91931
90146
  } catch (err) {
91932
- this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg$1(err) } });
90147
+ this.ctx.logger.warn("retrain-export data-plane failed to serve", { meta: { error: errMsg(err) } });
91933
90148
  }
91934
90149
  }
91935
90150
  /**
@@ -91994,7 +90209,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
91994
90209
  tags: { deviceId },
91995
90210
  meta: {
91996
90211
  deviceId,
91997
- error: errMsg$1(err)
90212
+ error: errMsg(err)
91998
90213
  }
91999
90214
  });
92000
90215
  return null;
@@ -92113,7 +90328,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92113
90328
  }, RETENTION_SWEEP_INTERVAL_MS);
92114
90329
  this.ltsFlushTimer = setInterval(() => {
92115
90330
  this.lts?.flushDue().catch((err) => {
92116
- this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg$1(err) } });
90331
+ this.ctx.logger.warn("lts flush failed", { meta: { error: errMsg(err) } });
92117
90332
  });
92118
90333
  }, LTS_FLUSH_INTERVAL_MS);
92119
90334
  this.startOrphanAuditTimer();
@@ -92134,7 +90349,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92134
90349
  } catch (err) {
92135
90350
  this.ctx.logger.error("artifacts storage location is NOT available — enrolling a face, a plate or a scene reference will FAIL until it is", { meta: {
92136
90351
  location: ARTIFACTS_MEDIA_LOCATION,
92137
- error: errMsg$1(err),
90352
+ error: errMsg(err),
92138
90353
  hint: "the declaration is seeded when a storage-provider registers; restart this node so the manifest is re-read, or create an artifacts location. Refusing the write is deliberate — the footage volume is where the last gallery was lost (D197), and the addon data dir carries no media (D341)."
92139
90354
  } });
92140
90355
  return;
@@ -92151,7 +90366,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92151
90366
  hint: "the pixels are gone for good; re-enrol these people from a photo"
92152
90367
  } });
92153
90368
  } catch (err) {
92154
- this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg$1(err) } });
90369
+ this.ctx.logger.warn("enrolled gallery media reconcile failed", { meta: { error: errMsg(err) } });
92155
90370
  }
92156
90371
  try {
92157
90372
  const identityStore = this.identityStore;
@@ -92183,7 +90398,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92183
90398
  maxReclaimPerRun: 16
92184
90399
  })).reclaimed > 0) await this.faceRecognizer?.refreshGallery();
92185
90400
  } catch (err) {
92186
- this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg$1(err) } });
90401
+ this.ctx.logger.warn("gallery sample reconcile failed", { meta: { error: errMsg(err) } });
92187
90402
  }
92188
90403
  }
92189
90404
  /** Schedule the orphan audit on the operator's interval (hours). */
@@ -92325,7 +90540,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92325
90540
  engine.start();
92326
90541
  this.sceneEngine = engine;
92327
90542
  this.sceneReloadTimer = setInterval(() => {
92328
- store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg$1(err) } }));
90543
+ store.load().then(() => store.pruneLatches()).catch((err) => log.debug("scene reload failed", { meta: { error: errMsg(err) } }));
92329
90544
  }, SCENE_RELOAD_INTERVAL_MS);
92330
90545
  this.sceneReloadTimer.unref?.();
92331
90546
  }
@@ -92369,7 +90584,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92369
90584
  lng: status.location.longitude
92370
90585
  };
92371
90586
  } catch (err) {
92372
- this.ctx.logger.debug("site location read failed", { meta: { error: errMsg$1(err) } });
90587
+ this.ctx.logger.debug("site location read failed", { meta: { error: errMsg(err) } });
92373
90588
  }
92374
90589
  this.siteCoordsCache = {
92375
90590
  value,
@@ -92388,7 +90603,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92388
90603
  } catch (err) {
92389
90604
  this.ctx.logger.debug("scene slice mirror failed", {
92390
90605
  tags: { deviceId },
92391
- meta: { error: errMsg$1(err) }
90606
+ meta: { error: errMsg(err) }
92392
90607
  });
92393
90608
  }
92394
90609
  }
@@ -92758,7 +90973,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
92758
90973
  if (planeRunsStationary(source) && this.stationaryRegistry) {
92759
90974
  await this.resolveDeviceStationarySettings(deviceId);
92760
90975
  await this.resolveDevicePackageDropSettings(deviceId);
92761
- await this.resolveDeviceSummarySettings(deviceId);
92762
90976
  }
92763
90977
  const processor = await this.getOrCreateProcessor(deviceId, source);
92764
90978
  const proxy = await this.ensureProxy(deviceId);
@@ -93134,15 +91348,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93134
91348
  className: t.className
93135
91349
  }
93136
91350
  });
93137
- this.summarySealer?.onTrackOpened({
93138
- deviceId,
93139
- trackId: id,
93140
- className: t.className,
93141
- position: {
93142
- x: t.bbox.x + t.bbox.w / 2,
93143
- y: t.bbox.y + t.bbox.h / 2
93144
- }
93145
- });
93146
91351
  const startPayload = buildTrackLifecyclePayload({
93147
91352
  deviceId,
93148
91353
  trackId: id,
@@ -93378,7 +91583,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93378
91583
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
93379
91584
  else plateCrops += 1;
93380
91585
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight, frameHandle);
93381
- this.offerGroupShot(deviceId, result, frameHandle);
93382
91586
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
93383
91587
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
93384
91588
  const rasterFallbackCandidates = [];
@@ -93742,7 +91946,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93742
91946
  } catch (err) {
93743
91947
  this.ctx.logger.debug("confirmation gate: getGlobalSteps failed", { meta: {
93744
91948
  nodeId,
93745
- error: errMsg$1(err)
91949
+ error: errMsg(err)
93746
91950
  } });
93747
91951
  }
93748
91952
  this.confirmationDetectorStepByNode.set(nodeId, {
@@ -93786,7 +91990,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
93786
91990
  } catch (err) {
93787
91991
  this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
93788
91992
  tags: { deviceId },
93789
- meta: { error: errMsg$1(err) }
91993
+ meta: { error: errMsg(err) }
93790
91994
  });
93791
91995
  return null;
93792
91996
  }
@@ -94094,15 +92298,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94094
92298
  stationarySettingsFromCache(deviceId) {
94095
92299
  return this.stationarySettingsCache.peek(deviceId) ?? STATIONARY_DEFAULTS;
94096
92300
  }
94097
- /** Synchronous read of the per-device summary settings — warmed by
94098
- * `processFrame`. A cold miss is today's constant, never a zero: a zero
94099
- * threshold would quietly end grouping on that camera. */
94100
- summarySettingsFromCache(deviceId) {
94101
- return this.summarySettingsCache.peek(deviceId) ?? SUMMARY_DEFAULTS;
94102
- }
94103
- async resolveDeviceSummarySettings(deviceId) {
94104
- return this.summarySettingsCache.get(deviceId, (id) => this.readDeviceSettings(id, resolveSummarySettings));
94105
- }
94106
92301
  /** Synchronous read of the per-device package-drop settings — warmed by
94107
92302
  * `processFrame` before the promotion loop, exactly like the stationary one. */
94108
92303
  packageDropSettingsFromCache(deviceId) {
@@ -94186,12 +92381,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94186
92381
  } } : {}
94187
92382
  });
94188
92383
  }
94189
- if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
92384
+ if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
94190
92385
  }
94191
92386
  const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
94192
92387
  if (resolved !== null && resolved.text !== void 0) {
94193
92388
  const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
94194
- if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
92389
+ if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
94195
92390
  }
94196
92391
  } else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
94197
92392
  } catch (err) {
@@ -94200,7 +92395,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94200
92395
  meta: {
94201
92396
  trackId,
94202
92397
  stepId: d.stepId,
94203
- error: errMsg$1(err)
92398
+ error: errMsg(err)
94204
92399
  }
94205
92400
  });
94206
92401
  }
@@ -94336,7 +92531,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94336
92531
  tags: { deviceId },
94337
92532
  meta: {
94338
92533
  trackId: t.trackId,
94339
- error: errMsg$1(err)
92534
+ error: errMsg(err)
94340
92535
  }
94341
92536
  });
94342
92537
  }
@@ -94384,139 +92579,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94384
92579
  });
94385
92580
  }
94386
92581
  /**
94387
- * Capture the CLEAN best full frame per given track at this best-detection
94388
- * moment and store BOTH variants (`putReplacing` → one row per track, per
94389
- * kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
94390
- * — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
94391
- * from the SAME native surface (no second fetch). No box is drawn — these are
94392
- * the clean hero images (`keyFrameSmall` is the web-friendly one).
94393
- *
94394
- * NATIVE-OR-NOTHING: a miss writes nothing (no upscale, no ≤640 stand-in) and
94395
- * the track's BOUNDED catch-up retry re-fires on a later frame. Until
94396
- * 2026-08-06 the only re-fire was the next genuine new-best PEAK, which for
94397
- * 80% of the tracks that missed never came — see `key-frame-retry.ts`. The
94398
- * `keyFrame` key is recorded on the track's resident state so the face /
94399
- * plate / object-embedding rows LINK the SAME native key frame (Design B).
94400
- * Issued in the live-frame window so the native lease is still held.
94401
- * Best-effort (D8) — a per-track failure is logged and never thrown.
94402
- *
94403
- * The capture BODY lives in `pipeline/key-frame-persist.ts`; what stays here
94404
- * is the scheduling (coalescing, priority, budget) and the bookkeeping the
94405
- * retry gate reads.
94406
- */
94407
- /**
94408
- * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
94409
- * Metadata only, at frame-rate: the frame is scored against the members the
94410
- * sealer has mirrored, and the pixels are fetched — by handle, at the owning
94411
- * node, as a native key frame — only when this frame beats the best so far.
94412
- */
94413
- offerGroupShot(deviceId, result, frameHandle) {
94414
- if (frameHandle === void 0) return;
94415
- const groups = this.summarySealer?.openGroupsFor(deviceId) ?? [];
94416
- for (const open of groups) this.offerGroupShotTo(deviceId, open, result, frameHandle);
94417
- if (groups.length > 0) this.logGroupShotWindow(deviceId);
94418
- }
94419
- /** One group's turn at the frame. Split out of {@link offerGroupShot} so the
94420
- * scoring reads the members of the group it is about, and nothing else. */
94421
- offerGroupShotTo(deviceId, open, result, frameHandle) {
94422
- const score = scoreSummaryFrame({
94423
- members: open.members,
94424
- frameWidth: result.frameWidth,
94425
- frameHeight: result.frameHeight,
94426
- detections: result.tracked.map((t) => ({
94427
- trackId: t.trackId,
94428
- bbox: {
94429
- x: t.bbox.x,
94430
- y: t.bbox.y,
94431
- width: t.bbox.w,
94432
- height: t.bbox.h
94433
- },
94434
- confidence: t.confidence,
94435
- recognized: this.recognizedTrackIds.has(t.trackId)
94436
- }))
94437
- });
94438
- const frameRef = { timestamp: result.timestamp };
94439
- if (!this.groupShots.offer(open.id, score, frameRef)) return;
94440
- const getNative = this.getNativeKeyFrameRgb;
94441
- const mediaStore = this.mediaStore;
94442
- if (!getNative || !mediaStore) return;
94443
- const summaryId = open.id;
94444
- this.captureScheduler.request({
94445
- deviceId,
94446
- kind: "keyFrame",
94447
- coalesceKey: `summary:${summaryId}:keyFrame`,
94448
- exec: () => persistSummaryGroupShot({
94449
- fetchNativeFullFrame: getNative,
94450
- encodeVariants: encodeKeyFrameVariants,
94451
- media: mediaStore,
94452
- logger: this.ctx.logger,
94453
- cropFrame: async (frame, rect) => {
94454
- const out = await extractCropRaw(frame.data, frame.width, frame.height, {
94455
- x: rect.x / frame.width,
94456
- y: rect.y / frame.height,
94457
- w: rect.width / frame.width,
94458
- h: rect.height / frame.height
94459
- });
94460
- return {
94461
- ...frame,
94462
- data: out.data,
94463
- width: out.width,
94464
- height: out.height
94465
- };
94466
- }
94467
- }, {
94468
- deviceId,
94469
- summaryId,
94470
- timestamp: result.timestamp,
94471
- frameHandle,
94472
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
94473
- crop: score.crop,
94474
- cropFrameWidth: result.frameWidth,
94475
- cropFrameHeight: result.frameHeight
94476
- })
94477
- }).then((outcome) => {
94478
- if (outcome?.kind === "landed") {
94479
- this.groupShots.noteLanded(summaryId, score, frameRef);
94480
- this.ctx.logger.debug("summary group shot landed", {
94481
- tags: { deviceId },
94482
- meta: {
94483
- summaryId,
94484
- visible: score.visible,
94485
- quality: Number(score.quality.toFixed(3))
94486
- }
94487
- });
94488
- }
94489
- }).catch(() => {});
94490
- }
94491
- /**
94492
- * The group-shot gate's window, throttled to one line per
94493
- * {@link GROUP_SHOT_METRIC_INTERVAL_MS}. Process-global counts, tagged with
94494
- * the device that happened to close the window — the counters are the
94495
- * gate's, not the camera's, and the tag is there because every line about a
94496
- * device carries one.
94497
- *
94498
- * `materialised` is the number of 4096-px native full-frame fetches this
94499
- * path actually spent; `byGap` + `byMargin` + `byWorse` is what it did not.
94500
- */
94501
- logGroupShotWindow(deviceId) {
94502
- const now = Date.now();
94503
- if (now - this.lastGroupShotMetricAt < GROUP_SHOT_METRIC_INTERVAL_MS) return;
94504
- this.lastGroupShotMetricAt = now;
94505
- const counts = this.groupShots.drainCounts();
94506
- if (counts.offered === 0) return;
94507
- this.ctx.logger.info("group shot window", {
94508
- tags: { deviceId },
94509
- meta: {
94510
- offered: counts.offered,
94511
- materialised: counts.materialised,
94512
- suppressedByGap: counts.byGap,
94513
- suppressedByMargin: counts.byMargin,
94514
- suppressedByWorse: counts.byWorse,
94515
- openSummaries: this.groupShots.size
94516
- }
94517
- });
94518
- }
94519
- /**
94520
92582
  * Order the parcels a best-frame decision wants (D422). Skipped entirely at
94521
92583
  * a node the registry already called offline (D58) — logged on the
94522
92584
  * transition by `noteCapturesGated`, counted in the key-frame window.
@@ -94553,6 +92615,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94553
92615
  timestamp: parcel.timestamp
94554
92616
  };
94555
92617
  }
92618
+ /**
92619
+ * Store each given track's clean native `keyFrame` + `keyFrameSmall` from its
92620
+ * PARKED parcel (D422) — never a by-handle fetch. The body is
92621
+ * `pipeline/key-frame-persist.ts`; what stays here is the scheduling
92622
+ * (coalescing, priority, budget) and the bookkeeping the retry gate reads.
92623
+ * Best-effort (D8): a per-track failure is logged and never thrown.
92624
+ */
94556
92625
  async persistKeyFrames(deviceId, timestamp, captures) {
94557
92626
  const mediaStore = this.mediaStore;
94558
92627
  if (this.parkedFrames === null || !mediaStore) return;
@@ -94653,7 +92722,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94653
92722
  meta: {
94654
92723
  trackId,
94655
92724
  keyFrameMediaId,
94656
- error: errMsg$1(err)
92725
+ error: errMsg(err)
94657
92726
  }
94658
92727
  });
94659
92728
  }
@@ -94720,8 +92789,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
94720
92789
  * when it wins and the frame's pixels are still reachable. Callers: the
94721
92790
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
94722
92791
  * fine-classification attribution. */
94723
- promoteSemanticRecognition(input, origin) {
94724
- if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
92792
+ promoteSemanticRecognition(input) {
94725
92793
  const ctx = this.ctxIfReady;
94726
92794
  if (ctx === null) return;
94727
92795
  promoteSemanticBestFrame({
@@ -95262,7 +93330,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95262
93330
  tags: { deviceId: input.deviceId },
95263
93331
  meta: {
95264
93332
  className: ref.className,
95265
- error: errMsg$1(err)
93333
+ error: errMsg(err)
95266
93334
  }
95267
93335
  });
95268
93336
  }
@@ -95294,7 +93362,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95294
93362
  tags: { deviceId: track.deviceId },
95295
93363
  meta: {
95296
93364
  episodeId: ref.episodeId,
95297
- error: errMsg$1(err)
93365
+ error: errMsg(err)
95298
93366
  }
95299
93367
  });
95300
93368
  }
@@ -95551,7 +93619,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95551
93619
  tags: { deviceId: input.deviceId },
95552
93620
  meta: {
95553
93621
  eventId: input.eventId,
95554
- error: errMsg$1(err)
93622
+ error: errMsg(err)
95555
93623
  }
95556
93624
  });
95557
93625
  });
@@ -95618,7 +93686,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95618
93686
  async sweepExpiredTracks() {
95619
93687
  if (this.shuttingDown || !this.trackStore) return;
95620
93688
  await this.trackCloser.sweep();
95621
- await this.summarySealer?.sweep();
95622
93689
  }
95623
93690
  /**
95624
93691
  * The follow-recordings floor per camera, read from the ARCHIVE's hour ledger
@@ -95658,7 +93725,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95658
93725
  listTrackedDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
95659
93726
  }, (half, err) => this.ctx.logger.warn("retention scope half unavailable — sweeping on the other", { meta: {
95660
93727
  half,
95661
- error: errMsg$1(err)
93728
+ error: errMsg(err)
95662
93729
  } }));
95663
93730
  }
95664
93731
  /**
@@ -95705,7 +93772,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95705
93772
  await this.runTrackRetentionSweep(deviceIds, now);
95706
93773
  const eventStore = this.eventStore;
95707
93774
  const sensorEventStore = this.sensorEventStore;
95708
- const summaryStore = this.summaryStore;
95709
93775
  await sweepTracklessRoots({
95710
93776
  deviceIds,
95711
93777
  nowMs: now,
@@ -95715,16 +93781,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95715
93781
  cutoffMs
95716
93782
  }),
95717
93783
  evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
95718
- evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
95719
93784
  onEvicted: async (deviceId, evicted, cutoffs) => {
95720
- const total = evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries;
93785
+ const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
95721
93786
  await this.eventsOpsLog?.append({
95722
93787
  op: "prune",
95723
93788
  reason: "retention",
95724
93789
  deviceId,
95725
93790
  itemsAffected: total,
95726
93791
  bytesReclaimed: 0,
95727
- detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor, ${evicted.summaries} summaries`
93792
+ detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor`
95728
93793
  });
95729
93794
  this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
95730
93795
  tags: { deviceId },
@@ -95732,7 +93797,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
95732
93797
  motion: evicted.motion.length,
95733
93798
  audio: evicted.audio.length,
95734
93799
  sensor: evicted.sensor,
95735
- summaries: evicted.summaries,
95736
93800
  mode: cutoffs.effectiveMode,
95737
93801
  cutoffMs: cutoffs.cutoffMs
95738
93802
  }
@@ -96686,118 +94750,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96686
94750
  async listRecentTracks(input) {
96687
94751
  return this.queryFacade.listRecentTracks(input);
96688
94752
  }
96689
- /** Newest first for a camera set; each row carries its group-shot media key. */
96690
- async listSummaries(input) {
96691
- const store = this.summaryStore;
96692
- if (!store) return { summaries: [] };
96693
- const rows = await store.listRecent({
96694
- deviceIds: input.deviceIds,
96695
- ...input.since !== void 0 ? { since: input.since } : {},
96696
- ...input.until !== void 0 ? { until: input.until } : {},
96697
- limit: input.limit ?? 50,
96698
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96699
- });
96700
- return { summaries: await this.listItemsFor(rows) };
96701
- }
96702
- /**
96703
- * Rows → what a LIST draws: the envelope plus the composition.
96704
- *
96705
- * Two bulk reads for the whole page, never per row: the members in one, their
96706
- * tracks (for the names) in one. The MEMBERS themselves stay off the list —
96707
- * a page of two hundred groups would otherwise carry a thousand records so
96708
- * the client could count them. `getSummary` is where the links live.
96709
- */
96710
- async listItemsFor(rows) {
96711
- const store = this.summaryStore;
96712
- if (!store || rows.length === 0) return [];
96713
- const members = await store.membersOfMany(rows.map((r) => r.id));
96714
- return await Promise.all(rows.map(async (row) => ({
96715
- summary: await this.toSummary(row),
96716
- ...summaryComposition(members.get(row.id) ?? [])
96717
- })));
96718
- }
96719
- /** The members of a page of summaries, with the labels a card can show —
96720
- * in ONE read of the members and ONE of their tracks. */
96721
- async labelledMembersFor(rows) {
96722
- const out = /* @__PURE__ */ new Map();
96723
- const store = this.summaryStore;
96724
- if (!store || rows.length === 0) return out;
96725
- const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
96726
- const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
96727
- let tracks = [];
96728
- try {
96729
- tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
96730
- } catch (err) {
96731
- this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
96732
- summaries: rows.length,
96733
- members: trackIds.length,
96734
- error: errMsg$1(err)
96735
- } });
96736
- }
96737
- for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
96738
- return out;
96739
- }
96740
- /**
96741
- * Rows → the shape every summary surface answers with: the envelope, its
96742
- * members, and what a thumbnail can render for each of them.
96743
- *
96744
- * THREE reads for the whole page, never per row: the members in one, their
96745
- * tracks in one, and the group shots through `toSummary`. A read per member
96746
- * is what made a sixty-group page take twenty seconds on the events feed.
96747
- */
96748
- /** ONE summary, with every member and the link each of them carries. */
96749
- async detailFor(row) {
96750
- const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
96751
- return {
96752
- summary: await this.toSummary(row),
96753
- members: labelled.map((m) => ({
96754
- trackId: m.trackId,
96755
- className: m.className,
96756
- role: m.role,
96757
- joinedAt: m.joinedAt,
96758
- ...m.label !== void 0 ? { label: m.label } : {},
96759
- ...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
96760
- }))
96761
- };
96762
- }
96763
- async listRecentSummaries(input) {
96764
- const store = this.summaryStore;
96765
- if (!store) return {
96766
- summaries: [],
96767
- nextCursor: null
96768
- };
96769
- const page = await store.queryRecent({
96770
- deviceIds: input.deviceIds,
96771
- ...input.since !== void 0 ? { since: input.since } : {},
96772
- ...input.until !== void 0 ? { until: input.until } : {},
96773
- limit: input.limit,
96774
- ...input.cursor !== void 0 ? { cursor: input.cursor } : {},
96775
- ...input.classes !== void 0 ? { classes: input.classes } : {}
96776
- });
96777
- return {
96778
- summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
96779
- nextCursor: page.nextCursor
96780
- };
96781
- }
96782
- async getSummary(input) {
96783
- const detail = await this.summaryStore?.get(input.id);
96784
- if (!detail) return null;
96785
- return await this.detailFor(detail.summary);
96786
- }
96787
- /** The row plus its group shot: the web companion when it landed, else the native key frame. */
96788
- async toSummary(row) {
96789
- const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
96790
- const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
96791
- return {
96792
- id: row.id,
96793
- deviceId: row.deviceId,
96794
- openedAt: row.openedAt,
96795
- lastActiveAt: row.lastActiveAt,
96796
- sealedAt: row.sealedAt,
96797
- memberCount: row.memberCount,
96798
- groupShotMediaId: shot?.mediaId ?? null
96799
- };
96800
- }
96801
94753
  /**
96802
94754
  * Every event kind the device can produce: built-ins (motion + audio), the
96803
94755
  * detection classes its CONFIGURED pipeline can emit, and sensor kinds from
@@ -96826,7 +94778,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96826
94778
  tags: { deviceId: input.deviceId },
96827
94779
  meta: {
96828
94780
  scope,
96829
- error: errMsg$1(err)
94781
+ error: errMsg(err)
96830
94782
  }
96831
94783
  })
96832
94784
  }, input.deviceId);
@@ -96844,7 +94796,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96844
94796
  } catch (err) {
96845
94797
  this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
96846
94798
  tags: { deviceId },
96847
- meta: { error: errMsg$1(err) }
94799
+ meta: { error: errMsg(err) }
96848
94800
  });
96849
94801
  return NO_EMITTABLE;
96850
94802
  }
@@ -96881,7 +94833,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96881
94833
  }, deviceIds).catch((err) => {
96882
94834
  this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
96883
94835
  deviceCount: deviceIds.length,
96884
- error: errMsg$1(err)
94836
+ error: errMsg(err)
96885
94837
  } });
96886
94838
  return null;
96887
94839
  });
@@ -96899,13 +94851,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96899
94851
  tags: { deviceId },
96900
94852
  meta: {
96901
94853
  scope,
96902
- error: errMsg$1(err)
94854
+ error: errMsg(err)
96903
94855
  }
96904
94856
  })
96905
94857
  }, deviceId);
96906
94858
  }, (deviceId, err) => this.ctx.logger.warn("listEventKindsBatch: compose threw — empty vocabulary", {
96907
94859
  tags: { deviceId },
96908
- meta: { error: errMsg$1(err) }
94860
+ meta: { error: errMsg(err) }
96909
94861
  }));
96910
94862
  }
96911
94863
  /** Per-camera sensor-event history (state changes of linked devices). */
@@ -96935,7 +94887,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96935
94887
  meta: {
96936
94888
  capName: data.capName,
96937
94889
  cameraIds: [...cameras.mediaCameraIds],
96938
- error: errMsg$1(err)
94890
+ error: errMsg(err)
96939
94891
  }
96940
94892
  });
96941
94893
  return;
@@ -96979,7 +94931,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96979
94931
  meta: {
96980
94932
  kind: event.kind,
96981
94933
  linkedCameraIds: [...input.mediaCameraIds],
96982
- error: errMsg$1(err)
94934
+ error: errMsg(err)
96983
94935
  }
96984
94936
  });
96985
94937
  }
@@ -96996,7 +94948,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
96996
94948
  tags: { deviceId: data.deviceId },
96997
94949
  meta: {
96998
94950
  capName: data.capName,
96999
- error: errMsg$1(err)
94951
+ error: errMsg(err)
97000
94952
  }
97001
94953
  });
97002
94954
  });
@@ -97052,7 +95004,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97052
95004
  tags: { deviceId: data.deviceId },
97053
95005
  meta: {
97054
95006
  capName: data.capName,
97055
- error: errMsg$1(err)
95007
+ error: errMsg(err)
97056
95008
  }
97057
95009
  });
97058
95010
  return NO_SENSOR_NOTIFICATION_CAMERAS;
@@ -97074,7 +95026,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97074
95026
  tags: { deviceId: data.deviceId },
97075
95027
  meta: {
97076
95028
  capName: data.capName,
97077
- error: errMsg$1(err)
95029
+ error: errMsg(err)
97078
95030
  }
97079
95031
  });
97080
95032
  }
@@ -97146,7 +95098,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97146
95098
  tags: { deviceId: data.deviceId },
97147
95099
  meta: {
97148
95100
  capName: data.capName,
97149
- error: errMsg$1(err)
95101
+ error: errMsg(err)
97150
95102
  }
97151
95103
  });
97152
95104
  }
@@ -97169,7 +95121,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97169
95121
  meta: {
97170
95122
  kind: ev.kind,
97171
95123
  sourceDeviceId: ev.sourceDeviceId,
97172
- error: errMsg$1(err)
95124
+ error: errMsg(err)
97173
95125
  }
97174
95126
  });
97175
95127
  }
@@ -97189,7 +95141,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97189
95141
  meta: {
97190
95142
  kind: ev.kind,
97191
95143
  linkedCameraIds: [...linkedCameraIds],
97192
- error: errMsg$1(err)
95144
+ error: errMsg(err)
97193
95145
  }
97194
95146
  });
97195
95147
  }
@@ -97319,7 +95271,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97319
95271
  } catch (err) {
97320
95272
  this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
97321
95273
  tags: { deviceId },
97322
- meta: { error: errMsg$1(err) }
95274
+ meta: { error: errMsg(err) }
97323
95275
  });
97324
95276
  return;
97325
95277
  }
@@ -97581,11 +95533,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
97581
95533
  },
97582
95534
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
97583
95535
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
97584
- liveSummaryIds: (ids) => {
97585
- const summaries = this.summaryStore;
97586
- if (!summaries) throw new Error("summary store not built — probe unavailable");
97587
- return summaries.existingIds(ids);
97588
- },
97589
95536
  keptMediaKeys: async (keys) => this.keptStationaryMediaKeys(keys),
97590
95537
  reclaim: buildOrphanReclaimer({
97591
95538
  deleteFace: async (faceId) => {
@@ -98192,7 +96139,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98192
96139
  nodeId
98193
96140
  };
98194
96141
  } catch (err) {
98195
- const reason = errMsg$1(err);
96142
+ const reason = errMsg(err);
98196
96143
  failures.push(`${nodeId}: ${reason}`);
98197
96144
  this.ctx.logger.warn("photo enrollment: a node could not analyse the photograph", { meta: {
98198
96145
  nodeId,
@@ -98918,7 +96865,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
98918
96865
  this.unreadableClipIndexes.add(space.index);
98919
96866
  this.ctx.logger.warn("embedding.rebuildStatus: index size unreadable — reported as unknown", { meta: {
98920
96867
  index: space.index,
98921
- error: errMsg$1(err)
96868
+ error: errMsg(err)
98922
96869
  } });
98923
96870
  }
98924
96871
  indexedByIndex[space.index] = null;
@@ -99274,8 +97221,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99274
97221
  mediaStore,
99275
97222
  faceStore: this.faceStore,
99276
97223
  plateStore: this.plateStore,
99277
- objectEmbeddingStore: this.objectEmbeddingStore,
99278
- summaryStore: this.summaryStore
97224
+ objectEmbeddingStore: this.objectEmbeddingStore
99279
97225
  };
99280
97226
  const absent = absentCascadeLegs(stores);
99281
97227
  if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
@@ -99853,7 +97799,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
99853
97799
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
99854
97800
  mediaId: media.mediaId,
99855
97801
  size: variant.size,
99856
- error: errMsg$1(err)
97802
+ error: errMsg(err)
99857
97803
  } });
99858
97804
  return media;
99859
97805
  }