@camstack/addon-post-analysis 1.2.116 → 1.2.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as deriveRecordingMode, At as array, B as TimelapseRulePatchSchema, Bt as EventCategory, C as NcScheduleSchema, Ct as DeviceType, D as NcSystemEventKindSchema, Dt as nodePin, E as NcSnoozeSuppressedSchema, Et as isDeviceScopedCap, F as SCENE_DEFAULT_UNCOVERED_POLICY, Ft as object, G as alarmPanelCapability, H as TrackSourceSchema, I as SCENE_DIVERGED, It as partialRecord, J as audioModeOf, K as assertTimelapseCadences, L as SceneMonitorSchema, Lt as record, M as RetrainStatusSchema, Mt as discriminatedUnion, Nt as literal, O as NcTaxonomySchema, Ot as sleep, P as SCENE_DEFAULT_ANCHOR_THRESHOLD, Pt as number, Q as defineCustomActions, R as TIMELAPSE_DENSE_FLOOR_SEC, Rt as string, S as NcRuleTargetSchema, St as BaseAddon, T as NcSnoozeSchema, Tt as hydrateSchema, U as VISIT_MERGE_GAP_MS, V as TimelapseRuleSchema, W as addonWidgetsSourceCapability, X as cosineSimilarity$1, Y as buildEventKindDescriptor, Z as customAction, _ as NC_TAXONOMY, _t as systemEventFilterApplies, a as DETECTION_MACRO_CLASSES, at as isScheduleActive, b as NcRulePatchSchema, bt as zoneAnalyticsCapability, c as EVENT_KIND_BY_CAP, d as LabelAttributionSchema, dt as plateGalleryCapability, f as MACRO_LABELS, ft as readDeviceStateFrom, gt as subKindsOf, h as NC_DEFAULT_SNOOZE_MINUTES, ht as sceneMonitorCapability, it as isDetectionMacroClass, j as RECORDING_EXPORT_MAX_READ_BYTES, jt as boolean, k as OpsLogEntrySchema, kt as _enum, l as EVENT_PAD_MS, lt as pickClusterStepModels, m as NC_CONDITION_CATALOG, n as CLUSTER_MODEL_SCOPED_STEPS, nt as faceGalleryCapability, ot as kebabToCamel, p as NC_ALARM_SYSTEM_EVENT_KINDS, pt as readTimelapseGeneratedAt, q as audioMetricsCapability, r as DEFAULT_EVENT_COLOR, s as DeclaredDevices, st as notificationRulesCapability, t as BaseDevice, tt as encodeVectorBase64, u as FULL_IMAGE_BBOX, ut as pipelineAnalyticsCapability, v as NcConditionDescriptorSchema, vt as vectorDimFromBase64, w as NcSnoozeInputSchema, wt as createEvent, x as NcRuleSchema, xt as errMsg, y as NcRuleInputSchema, yt as videoclipsCapability, z as TimelapseRuleInputSchema, zt as unknown } from "../dist-B13sFjR2.mjs";
1
+ import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-PMOtjzh2.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -15142,26 +15142,28 @@ function remainingSummaryCandidates(kept, taken, rank) {
15142
15142
  /**
15143
15143
  * The media a tile is drawn from, best first.
15144
15144
  *
15145
- * For a group: `groupCrop` (union crop, the group's own blob) then
15146
- * `nativeFrame` (pointer at the anchor's clean keyFrame). A digest of a
15147
- * cohort may show the native still when the crop was not written; a
15148
- * member's `keyFrame` listed as itself stays off this ladder so tracker
15149
- * ID churn cannot refill a mosaic with eleven driveways. Boxed frames stay
15150
- * absent: a drawn rectangle is a permanent lie in a digest tile.
15145
+ * First-level group crop only (`groupCrop`), then the group's native-frame
15146
+ * pointer, then a member's clean full frame. Member `thumbnail` / plate /
15147
+ * face crops stay off this ladder: a successful plate read overwrites the
15148
+ * vehicle first-level crop under the same `thumbnail` kind, and a digest
15149
+ * tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
15150
+ * rectangle is a permanent lie in a digest tile. Duplicate driveways from
15151
+ * tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
15152
+ * full-frame fallback.
15151
15153
  */
15152
15154
  var SUMMARY_MEDIA_LADDER = [
15153
15155
  "groupCrop",
15154
15156
  "nativeFrame",
15155
- "thumbnail",
15156
- "thumbnailSmall",
15157
- "snapshot"
15157
+ "keyFrame",
15158
+ "keyFrameSmall",
15159
+ "fullFrame"
15158
15160
  ];
15159
15161
  /**
15160
15162
  * Pick ONE media row for a track — the cheap question first (index rows only),
15161
15163
  * so exactly one blob is ever read.
15162
15164
  *
15163
- * Within a rung the row CLOSEST to the track's peak is chosen; `snapshot` is a
15164
- * filmstrip and its rows accumulate, so "the first one" would systematically
15165
+ * Within a rung the row CLOSEST to the track's peak is chosen; a member may
15166
+ * carry more than one full-frame still, so "the first one" would systematically
15165
15167
  * pick the moment the subject entered rather than the moment it was best seen.
15166
15168
  */
15167
15169
  function pickSummaryMedia(rows, aroundMs) {
@@ -39873,8 +39875,9 @@ var OverlayDetailStateStore = class {
39873
39875
  *
39874
39876
  * See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
39875
39877
  * docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
39876
- * (surface C). Dwell default is 15s per operator decision (the design's
39877
- * §3.3 draft said 45s).
39878
+ * (surface C). Confirmation is N frames (default 3); dwell default is 0
39879
+ * (an extra bar). The 15 s dwell plus a 5 s round-gap missed a real
39880
+ * parcel on device 615 (2026-08-24 13:40).
39878
39881
  */
39879
39882
  var PackageDropSettingsSchema = object({
39880
39883
  /**
@@ -39895,13 +39898,20 @@ var PackageDropSettingsSchema = object({
39895
39898
  */
39896
39899
  packageDropEnabled: boolean().default(true),
39897
39900
  /**
39898
- * Minimum OBSERVED dwell (seconds, since first-seen) before a newly
39899
- * promoted stationary package counts as a delivery. Kills a bag briefly
39900
- * set down and snatched back. Operator default 15s. Values beyond the
39901
- * stationary promotion window (30s) are satisfied only once the entry
39902
- * has actually been observed that long.
39901
+ * Frames the box must be re-seen on before it is a parked package.
39902
+ * Same bar as tracker `minHits`: one frame is a flicker, two can still
39903
+ * be a passer-by's bag, three is "it stayed". A settle burst of three
39904
+ * frames in ~1 s is enough; no second governor round after the courier
39905
+ * left. Device 615 2026-08-24 13:40 expired at 1 round / 1.5 s dwell.
39906
+ */
39907
+ packageDropMinFrames: number().int().min(1).max(20).default(3),
39908
+ /**
39909
+ * Extra OBSERVED dwell (seconds, since first-seen) on top of N frames.
39910
+ * 0 (the default) means the frame count is enough — a courier who drops
39911
+ * and leaves in 2 s still delivers. Raise this to reject a bag briefly
39912
+ * set down and snatched back.
39903
39913
  */
39904
- packageDropDwellSec: number().int().min(0).max(3600).default(15),
39914
+ packageDropDwellSec: number().int().min(0).max(3600).default(0),
39905
39915
  /** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
39906
39916
  packageDropMinBboxAreaFrac: number().min(0).max(1).default(.004),
39907
39917
  /**
@@ -39919,13 +39929,15 @@ var PackageDropSettingsSchema = object({
39919
39929
  packageDropPickupEnabled: boolean().default(true),
39920
39930
  /**
39921
39931
  * A parcel needs a deliverer: a stillness candidate only ever promotes to
39922
- * `delivered` if a non-package track (person/vehicle/animal) was active on
39923
- * the device within this many seconds BEFORE the box first appeared — and a
39924
- * departure only counts as `picked-up` with somebody around the
39925
- * disappearance. 0 disables both gates. Device 615's streetlight wedge
39926
- * cycled delivered→picked-up ~23×/night with nobody in frame, ever.
39927
- * Default 300 s: wide enough to absorb the motion-settle governor's cadence
39928
- * (the box is often first SEEN minutes after the courier left).
39932
+ * `delivered` if a non-package track (person/vehicle/animal) was active IN
39933
+ * A CONFIGURED PACKAGE DROP-OFF ZONE within this many seconds BEFORE the
39934
+ * box first appeared — and a departure only counts as `picked-up` with
39935
+ * somebody in that zone around the disappearance. Whole-camera motion (a
39936
+ * walker at the far gate) does not count. 0 disables both gates. Device
39937
+ * 615's streetlight wedge cycled delivered→picked-up ~23×/night with
39938
+ * nobody in the drop-off zone. Default 300 s: wide enough to absorb the
39939
+ * motion-settle governor's cadence (the box is often first SEEN minutes
39940
+ * after the courier left).
39929
39941
  */
39930
39942
  packageDropActivityWindowSec: number().int().min(0).max(3600).default(300),
39931
39943
  /**
@@ -39949,6 +39961,7 @@ function resolvePackageDropSettings(raw) {
39949
39961
  };
39950
39962
  return {
39951
39963
  packageDropEnabled: pick("packageDropEnabled"),
39964
+ packageDropMinFrames: pick("packageDropMinFrames"),
39952
39965
  packageDropDwellSec: pick("packageDropDwellSec"),
39953
39966
  packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
39954
39967
  packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
@@ -40206,29 +40219,6 @@ var DeferredBirthRegistry = class {
40206
40219
  this.byDevice.delete(deviceKey);
40207
40220
  }
40208
40221
  };
40209
- var AnalyticsGroupRecordSchema = object({
40210
- id: string(),
40211
- deviceId: number().int(),
40212
- openedAt: number().int(),
40213
- closedAt: number().int(),
40214
- timestamp: number().int(),
40215
- memberCount: number().int(),
40216
- memberTrackIds: array(string()).readonly(),
40217
- className: string(),
40218
- classes: array(string()).readonly(),
40219
- /** Relative event-media path, or null when the group has no picture yet. */
40220
- mediaUrl: string().nullable(),
40221
- singleton: boolean()
40222
- });
40223
- var AnalyticsGroupMemberSchema = object({
40224
- trackId: string(),
40225
- deviceId: number().int(),
40226
- className: string(),
40227
- firstSeen: number().int(),
40228
- lastSeen: number().int(),
40229
- mediaUrl: string().nullable()
40230
- });
40231
- var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
40232
40222
  var groupActions = defineCustomActions({
40233
40223
  "analytics.listGroups": customAction(object({
40234
40224
  deviceId: number().int().optional(),
@@ -40242,61 +40232,6 @@ var groupActions = defineCustomActions({
40242
40232
  })),
40243
40233
  "analytics.getGroup": customAction(object({ groupId: string().min(1) }), object({ group: AnalyticsGroupDetailSchema.nullable() }))
40244
40234
  });
40245
- //#endregion
40246
- //#region src/pipeline-analytics/group/group-query.ts
40247
- /**
40248
- * Stamp a relative event-media URL.
40249
- *
40250
- * Media keys contain `:` and the data-plane serves them by key. Track ids are
40251
- * bare and fall through to the existing event/track thumbnail resolver. Never
40252
- * percent-encode: the handler does not URL-decode the path.
40253
- */
40254
- function stampEventMediaUrl(baseUrl, id) {
40255
- if (baseUrl === null || baseUrl.length === 0) return null;
40256
- if (id === null || id === void 0 || id.length === 0) return null;
40257
- return `${baseUrl}/${id}`;
40258
- }
40259
- /** Union of every class on the members; `className` is the first, stable order. */
40260
- function aggregateGroupClasses(tracks) {
40261
- const seen = /* @__PURE__ */ new Set();
40262
- const classes = [];
40263
- for (const track of tracks) for (const name of [track.className, ...track.classes ?? []]) {
40264
- if (name.length === 0 || seen.has(name)) continue;
40265
- seen.add(name);
40266
- classes.push(name);
40267
- }
40268
- return {
40269
- className: classes[0] ?? "unknown",
40270
- classes
40271
- };
40272
- }
40273
- function toGroupRecord(input) {
40274
- const { className, classes } = aggregateGroupClasses(input.tracks);
40275
- const mediaId = input.group.mediaKey ?? input.group.nativeKey ?? (input.group.memberCount === 1 ? input.memberTrackIds[0] : null);
40276
- return {
40277
- id: input.group.id,
40278
- deviceId: input.group.deviceId,
40279
- openedAt: input.group.openedAt,
40280
- closedAt: input.group.closedAt,
40281
- timestamp: input.group.timestamp,
40282
- memberCount: input.memberTrackIds.length > 0 ? input.memberTrackIds.length : input.group.memberCount,
40283
- memberTrackIds: [...input.memberTrackIds],
40284
- className,
40285
- classes: [...classes],
40286
- mediaUrl: stampEventMediaUrl(input.eventMediaBaseUrl, mediaId),
40287
- singleton: input.memberTrackIds.length <= 1
40288
- };
40289
- }
40290
- function toGroupMember(member, track, eventMediaBaseUrl) {
40291
- return {
40292
- trackId: member.trackId,
40293
- deviceId: member.deviceId,
40294
- className: track?.className ?? "unknown",
40295
- firstSeen: track?.firstSeen ?? member.timestamp,
40296
- lastSeen: track?.lastSeen ?? member.timestamp,
40297
- mediaUrl: stampEventMediaUrl(eventMediaBaseUrl, member.trackId)
40298
- };
40299
- }
40300
40235
  function trackToLite(track) {
40301
40236
  return {
40302
40237
  trackId: track.trackId,
@@ -40306,56 +40241,18 @@ function trackToLite(track) {
40306
40241
  lastSeen: track.lastSeen
40307
40242
  };
40308
40243
  }
40309
- async function listGroupRecords(ports, input) {
40310
- const page = await ports.groupStore.listRecent(input);
40311
- const groups = [];
40312
- for (const group of page.groups) groups.push(await buildGroupRecord(ports, group));
40313
- return {
40314
- groups,
40315
- hasMore: page.hasMore
40316
- };
40317
- }
40318
- async function getGroupDetail(ports, groupId) {
40319
- const group = await ports.groupStore.get(groupId);
40320
- if (group === null) return null;
40321
- const members = await ports.groupStore.listMembers(groupId);
40322
- const lites = [];
40323
- const rows = [];
40324
- for (const member of members) {
40325
- const track = await ports.loadTrack(member.trackId);
40326
- const lite = track === null ? null : trackToLite(track);
40327
- if (lite !== null) lites.push(lite);
40328
- rows.push(toGroupMember(member, lite, ports.eventMediaBaseUrl()));
40329
- }
40330
- return {
40331
- ...toGroupRecord({
40332
- group,
40333
- memberTrackIds: members.map((m) => m.trackId),
40334
- tracks: lites,
40335
- eventMediaBaseUrl: ports.eventMediaBaseUrl()
40336
- }),
40337
- members: rows
40338
- };
40339
- }
40340
- async function buildGroupRecord(ports, group) {
40341
- const members = await ports.groupStore.listMembers(group.id);
40342
- const lites = [];
40343
- for (const member of members) {
40344
- const track = await ports.loadTrack(member.trackId);
40345
- if (track !== null) lites.push(trackToLite(track));
40346
- }
40347
- return toGroupRecord({
40348
- group,
40349
- memberTrackIds: members.map((m) => m.trackId),
40350
- tracks: lites,
40351
- eventMediaBaseUrl: ports.eventMediaBaseUrl()
40352
- });
40353
- }
40354
40244
  /**
40355
40245
  * Gallery and digest read the same ladder: groupCrop, then the native-frame
40356
- * pointer, then member subject crops. A multi with no crop still has a
40357
- * picture if the sealer stored `nativeKey`.
40246
+ * pointer, then a member's clean full frame. Plate / face / member
40247
+ * thumbnails never reach the picker — a plate read overwrites the vehicle
40248
+ * first-level crop under the same `thumbnail` kind. A multi with no crop
40249
+ * still has a picture if the sealer stored `nativeKey`.
40358
40250
  */
40251
+ var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
40252
+ "keyFrame",
40253
+ "keyFrameSmall",
40254
+ "fullFrame"
40255
+ ]);
40359
40256
  function digestMediaRows(input) {
40360
40257
  const owned = [];
40361
40258
  if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
@@ -40369,7 +40266,7 @@ function digestMediaRows(input) {
40369
40266
  timestamp: input.timestamp
40370
40267
  });
40371
40268
  if (owned.length > 0) return owned;
40372
- return input.memberRows;
40269
+ return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
40373
40270
  }
40374
40271
  var MATCH_WINDOW_MS = 1e3;
40375
40272
  var MOVE_EPS = .35;
@@ -40923,6 +40820,34 @@ function toInstant(t) {
40923
40820
  confidence: t.importance
40924
40821
  };
40925
40822
  }
40823
+ function memberBoxesAt(members, at, frame) {
40824
+ const w = Math.max(frame.width, 1);
40825
+ const h = Math.max(frame.height, 1);
40826
+ const boxes = [];
40827
+ for (const member of members) {
40828
+ let best = member.positions[0];
40829
+ let bestDt = Number.POSITIVE_INFINITY;
40830
+ for (const p of member.positions) {
40831
+ const dt = Math.abs(p.timestamp - at);
40832
+ if (dt <= 1e3 && dt < bestDt) {
40833
+ best = p;
40834
+ bestDt = dt;
40835
+ }
40836
+ }
40837
+ if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
40838
+ boxes.push({
40839
+ bbox: {
40840
+ x: best.bbox.x / w,
40841
+ y: best.bbox.y / h,
40842
+ w: best.bbox.w / w,
40843
+ h: best.bbox.h / h
40844
+ },
40845
+ label: member.className,
40846
+ observedAt: best.timestamp
40847
+ });
40848
+ }
40849
+ return boxes;
40850
+ }
40926
40851
  function toSealer(track) {
40927
40852
  return {
40928
40853
  trackId: track.trackId,
@@ -40939,7 +40864,11 @@ var GroupSealer = class {
40939
40864
  constructor(deps) {
40940
40865
  this.deps = deps;
40941
40866
  }
40867
+ get processEnabled() {
40868
+ return this.deps.enabled ?? false;
40869
+ }
40942
40870
  async flushIdle(buffer, now = this.deps.now()) {
40871
+ if (!this.processEnabled) return 0;
40943
40872
  let written = 0;
40944
40873
  for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
40945
40874
  buffer.drop([group.id]);
@@ -40952,6 +40881,7 @@ var GroupSealer = class {
40952
40881
  * into the live buffer, persist whoever is already idle, keep the rest in RAM.
40953
40882
  */
40954
40883
  async recoverCamera(deviceId, buffer) {
40884
+ if (!this.processEnabled) return 0;
40955
40885
  if (buffer.isPrimed(deviceId)) return 0;
40956
40886
  const now = this.deps.now();
40957
40887
  try {
@@ -40980,6 +40910,7 @@ var GroupSealer = class {
40980
40910
  * is what wedged hub-main.
40981
40911
  */
40982
40912
  async sealWindow(deviceId, since, until) {
40913
+ if (!this.processEnabled) return 0;
40983
40914
  try {
40984
40915
  const closed = await this.deps.trackStore.queryHistorical({
40985
40916
  deviceId,
@@ -41094,26 +41025,54 @@ var GroupSealer = class {
41094
41025
  return;
41095
41026
  }
41096
41027
  try {
41097
- const raw = await sharp(source.jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true });
41098
- if (raw.info.channels !== 3) return;
41099
- const union = {
41100
- x: instant.ux * raw.info.width,
41101
- y: instant.uy * raw.info.height,
41102
- w: instant.uw * raw.info.width,
41103
- h: instant.uh * raw.info.height
41104
- };
41105
- const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
41106
- const mediaKey = await this.deps.mediaStore.put({
41028
+ const raw = await sharp(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
41029
+ if (raw.info.channels === 3) {
41030
+ const union = {
41031
+ x: instant.ux * raw.info.width,
41032
+ y: instant.uy * raw.info.height,
41033
+ w: instant.uw * raw.info.width,
41034
+ h: instant.uh * raw.info.height
41035
+ };
41036
+ const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
41037
+ const mediaKey = await this.deps.mediaStore.put({
41038
+ deviceId,
41039
+ ownerKind: "group",
41040
+ ownerId: draft.id,
41041
+ kind: "thumbnail",
41042
+ timestamp,
41043
+ data: crop
41044
+ });
41045
+ await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
41046
+ }
41047
+ } catch (err) {
41048
+ this.deps.logger.warn("group sealer: crop failed", {
41049
+ tags: { deviceId },
41050
+ meta: {
41051
+ groupId: draft.id,
41052
+ error: String(err)
41053
+ }
41054
+ });
41055
+ }
41056
+ try {
41057
+ const composed = await composeGroupFrame({
41058
+ jpeg: source.jpeg,
41059
+ boxes: memberBoxesAt(instantMembers, timestamp, frame),
41060
+ at: timestamp,
41061
+ logger: this.deps.logger,
41062
+ deviceId,
41063
+ groupKey: draft.id
41064
+ });
41065
+ if (composed === null) return;
41066
+ await this.deps.mediaStore.put({
41107
41067
  deviceId,
41108
41068
  ownerKind: "group",
41109
41069
  ownerId: draft.id,
41110
- kind: "thumbnail",
41070
+ kind: "fullFrameBoxed",
41111
41071
  timestamp,
41112
- data: crop
41072
+ data: composed.bytes
41113
41073
  });
41114
- await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
41115
41074
  } catch (err) {
41116
- this.deps.logger.warn("group sealer: crop failed", {
41075
+ this.deps.logger.warn("group sealer: boxed frame failed", {
41117
41076
  tags: { deviceId },
41118
41077
  meta: {
41119
41078
  groupId: draft.id,
@@ -41138,6 +41097,37 @@ var GroupSealer = class {
41138
41097
  }
41139
41098
  };
41140
41099
  //#endregion
41100
+ //#region src/pipeline-analytics/group/group-cursor.ts
41101
+ function encodeGroupCursor(cursor) {
41102
+ return Buffer.from(JSON.stringify({
41103
+ c: cursor.closedAt,
41104
+ i: cursor.groupId
41105
+ }), "utf8").toString("base64url");
41106
+ }
41107
+ function decodeGroupCursor(raw) {
41108
+ let parsed;
41109
+ try {
41110
+ parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
41111
+ } catch {
41112
+ throw new Error("listGroups: malformed cursor");
41113
+ }
41114
+ if (parsed === null || typeof parsed !== "object" || !("c" in parsed) || !("i" in parsed) || typeof parsed.c !== "number" || !Number.isFinite(parsed.c) || typeof parsed.i !== "string" || parsed.i.length === 0) throw new Error("listGroups: malformed cursor");
41115
+ return {
41116
+ closedAt: parsed.c,
41117
+ groupId: parsed.i
41118
+ };
41119
+ }
41120
+ function compareGroupDesc(a, b) {
41121
+ if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
41122
+ if (a.id === b.id) return 0;
41123
+ return a.id < b.id ? 1 : -1;
41124
+ }
41125
+ /** True when `row` sits strictly AFTER the cursor in DESC order. */
41126
+ function isAfterGroupCursor(row, cursor) {
41127
+ if (row.closedAt < cursor.closedAt) return true;
41128
+ return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
41129
+ }
41130
+ //#endregion
41141
41131
  //#region src/pipeline-analytics/group/group-store.ts
41142
41132
  /**
41143
41133
  * The group ENTITY store — a co-moving partition of one session, with one
@@ -41399,6 +41389,24 @@ var TrackGroupStore = class {
41399
41389
  data: r.data
41400
41390
  }));
41401
41391
  }
41392
+ /**
41393
+ * Membership for a page of groups in ONE query. `listGroupRecords` used to
41394
+ * call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
41395
+ * ~20 s, and the viewer feed times out into a retry loop.
41396
+ */
41397
+ async listMembersForGroups(groupIds) {
41398
+ if (groupIds.length === 0) return [];
41399
+ return (await this.store.query.query({
41400
+ collection: TRACK_GROUP_MEMBERS_COLLECTION,
41401
+ filter: {
41402
+ whereIn: { groupId: [...groupIds] },
41403
+ limit: 1e4
41404
+ }
41405
+ })).map((r) => this.normalizeMember({
41406
+ id: r.id,
41407
+ data: r.data
41408
+ }));
41409
+ }
41402
41410
  async membersOfTracks(trackIds) {
41403
41411
  const out = [];
41404
41412
  for (const trackId of trackIds) {
@@ -41414,20 +41422,29 @@ var TrackGroupStore = class {
41414
41422
  return out;
41415
41423
  }
41416
41424
  /**
41417
- * Newest-closed page for the Library Groups tab.
41425
+ * Newest-closed page for the Groups feed.
41418
41426
  *
41419
41427
  * Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
41420
41428
  * same as {@link listOverlapping}. Pagination is applied AFTER the window
41421
41429
  * so a day cursor is honest rather than a raw-row offset that skips live
41422
- * groups on a busy camera.
41430
+ * groups on a busy camera. `deviceIds: []` is an empty page (same as
41431
+ * `listRecentTracks`); omit the field for every camera. `deviceId` stays
41432
+ * as the single-camera alias the custom-action bridge still sends.
41423
41433
  */
41424
41434
  async listRecent(input) {
41425
41435
  const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
41426
41436
  const offset = Math.max(input.offset ?? 0, 0);
41427
- const slice = (await this.store.query.query({
41437
+ const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
41438
+ if (deviceIds !== void 0 && deviceIds.length === 0) return {
41439
+ groups: [],
41440
+ hasMore: false,
41441
+ nextCursor: null
41442
+ };
41443
+ const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
41444
+ const filtered = (await this.store.query.query({
41428
41445
  collection: TRACK_GROUPS_COLLECTION,
41429
41446
  filter: {
41430
- ...input.deviceId !== void 0 ? { where: { deviceId: input.deviceId } } : {},
41447
+ ...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
41431
41448
  orderBy: {
41432
41449
  field: "closedAt",
41433
41450
  direction: "desc"
@@ -41440,11 +41457,20 @@ var TrackGroupStore = class {
41440
41457
  })).filter((g) => {
41441
41458
  if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
41442
41459
  if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
41460
+ if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
41443
41461
  return true;
41444
- }).slice(offset, offset + limit + 1);
41462
+ }).toSorted(compareGroupDesc);
41463
+ const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
41464
+ const groups = slice.slice(0, limit);
41465
+ const hasMore = slice.length > limit;
41466
+ const last = groups[groups.length - 1];
41445
41467
  return {
41446
- groups: slice.slice(0, limit),
41447
- hasMore: slice.length > limit
41468
+ groups,
41469
+ hasMore,
41470
+ nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
41471
+ closedAt: last.closedAt,
41472
+ groupId: last.id
41473
+ }) : null
41448
41474
  };
41449
41475
  }
41450
41476
  /** Groups whose life overlaps `[since, until]` on one camera. */
@@ -42776,7 +42802,7 @@ function evaluateStationaryPromotion(input) {
42776
42802
  };
42777
42803
  }
42778
42804
  var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
42779
- minRounds: 2,
42805
+ minHits: 3,
42780
42806
  roundGapMs: 5e3,
42781
42807
  ttlMs: 5 * 6e4,
42782
42808
  assocIou: .3
@@ -42809,8 +42835,9 @@ var PackageStillnessTracker = class {
42809
42835
  const prev = next[index];
42810
42836
  if (prev === void 0) continue;
42811
42837
  consumed.add(prev.id);
42812
- const updated = confirm(prev, input, s, this.config.roundGapMs);
42813
- if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.rounds >= this.config.minRounds && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
42838
+ const updated = confirm(prev, input, s);
42839
+ const minHits = input.minHits ?? this.config.minHits;
42840
+ if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
42814
42841
  next[index] = updated;
42815
42842
  continue;
42816
42843
  }
@@ -42834,7 +42861,7 @@ var PackageStillnessTracker = class {
42834
42861
  id: c.id,
42835
42862
  className: c.className,
42836
42863
  bbox: { ...c.bbox },
42837
- rounds: c.rounds,
42864
+ hits: c.hits,
42838
42865
  firstSeenAt: c.firstSeenAt,
42839
42866
  lastSeenAt: c.lastSeenAt,
42840
42867
  activityBackedAtOpen: c.activityBackedAtOpen
@@ -42892,7 +42919,7 @@ var PackageStillnessTracker = class {
42892
42919
  frameHeight: input.frameHeight,
42893
42920
  firstSeenAt: input.timestamp,
42894
42921
  lastSeenAt: input.timestamp,
42895
- rounds: 1,
42922
+ hits: 1,
42896
42923
  promotedAt: null,
42897
42924
  activityBackedAtOpen,
42898
42925
  ...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
@@ -42902,18 +42929,16 @@ var PackageStillnessTracker = class {
42902
42929
  };
42903
42930
  }
42904
42931
  };
42905
- /** Re-confirm a candidate with this frame's box. A new ROUND is only counted
42906
- * when the gap since the last sighting clears `roundGapMs`, so a burst of
42907
- * frames inside one governor round counts once. */
42908
- function confirm(prev, input, s, roundGapMs) {
42909
- const newRound = input.timestamp - prev.lastSeenAt >= roundGapMs;
42932
+ /** Re-confirm a candidate with this frame's box. Every sighting is a hit —
42933
+ * a settle burst of three frames in one second is three confirmations. */
42934
+ function confirm(prev, input, s) {
42910
42935
  return {
42911
42936
  ...prev,
42912
42937
  bbox: { ...s.bbox },
42913
42938
  frameWidth: input.frameWidth,
42914
42939
  frameHeight: input.frameHeight,
42915
42940
  lastSeenAt: input.timestamp,
42916
- rounds: newRound ? prev.rounds + 1 : prev.rounds,
42941
+ hits: prev.hits + 1,
42917
42942
  ...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
42918
42943
  ...s.label !== void 0 ? { label: s.label } : {},
42919
42944
  ...s.confidence !== void 0 ? { confidence: s.confidence } : {},
@@ -42938,6 +42963,31 @@ function toEntry(c) {
42938
42963
  };
42939
42964
  }
42940
42965
  //#endregion
42966
+ //#region src/pipeline-analytics/pipeline/package/package-zone-activity.ts
42967
+ /**
42968
+ * Whether a live track is motion inside a configured package drop-off zone.
42969
+ *
42970
+ * The stillness activity gate used to stamp ANY non-package track on the
42971
+ * camera. A walker at the far gate then "delivered" the streetlight wedge
42972
+ * on the doormat. The operator's prerequisite is motion in the drop-off
42973
+ * zone the package rule points at — the same zones `PackageDropDetector`
42974
+ * uses for the centroid gate.
42975
+ */
42976
+ function isDelivererInPackageZone(trackZones, packageZoneIds) {
42977
+ if (packageZoneIds.size === 0 || trackZones.length === 0) return false;
42978
+ for (const z of trackZones) if (packageZoneIds.has(z)) return true;
42979
+ return false;
42980
+ }
42981
+ /** Zone ids referenced by enabled `package`-stage rules. Disabled rules do not count. */
42982
+ function collectPackageZoneIds(rules) {
42983
+ const ids = /* @__PURE__ */ new Set();
42984
+ for (const r of rules) {
42985
+ if (r.enabled === false) continue;
42986
+ for (const z of r.zoneIds) ids.add(z);
42987
+ }
42988
+ return ids;
42989
+ }
42990
+ //#endregion
42941
42991
  //#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
42942
42992
  /**
42943
42993
  * Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
@@ -49841,6 +49891,28 @@ var TrackStore = class {
49841
49891
  return this.rowToTrack(row.id, row.data);
49842
49892
  }
49843
49893
  /**
49894
+ * Batch read of persisted tracks. `listGroupRecords` cannot call
49895
+ * {@link getPersistedByTrackId} per member — that is one settings-store RPC
49896
+ * per row and a 60-group page takes ~20 s, which the viewer feed times out.
49897
+ */
49898
+ async getPersistedByTrackIds(trackIds) {
49899
+ if (trackIds.length === 0) return [];
49900
+ const PAGE = 500;
49901
+ const out = [];
49902
+ for (let i = 0; i < trackIds.length; i += PAGE) {
49903
+ const chunk = trackIds.slice(i, i + PAGE);
49904
+ const rows = await this.store.query.query({
49905
+ collection: TRACKS_COLLECTION,
49906
+ filter: {
49907
+ whereIn: { id: [...chunk] },
49908
+ limit: chunk.length
49909
+ }
49910
+ });
49911
+ for (const row of rows) out.push(this.rowToTrack(row.id, row.data));
49912
+ }
49913
+ return out;
49914
+ }
49915
+ /**
49844
49916
  * Persist a SYNTHETIC track (a sensor/control event projected into the
49845
49917
  * unified track store). Unlike `persistCompleted` there is no live in-RAM
49846
49918
  * state and no trajectory envelope (synthetic tracks carry no positions) —
@@ -54913,11 +54985,22 @@ function buildGlobalSettingsSchema(cap) {
54913
54985
  description: "Also emit a package-picked-up event when the delivered package leaves.",
54914
54986
  default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
54915
54987
  },
54988
+ {
54989
+ type: "slider",
54990
+ key: "packageDropMinFrames",
54991
+ label: "Confirmation frames",
54992
+ description: "How many frames the box must be seen on before it counts as a delivery. One is a flicker; two can still be a passer-by; three is the default bar against false positives. Independent of dwell.",
54993
+ min: 1,
54994
+ max: 10,
54995
+ step: 1,
54996
+ default: PACKAGE_DROP_DEFAULTS.packageDropMinFrames,
54997
+ showValue: true
54998
+ },
54916
54999
  {
54917
55000
  type: "slider",
54918
55001
  key: "packageDropDwellSec",
54919
55002
  label: "Minimum dwell",
54920
- description: "How long a package must stay parked before it counts as a delivery. Higher = fewer false positives from bags briefly set down.",
55003
+ description: "Extra time the package must stay parked after the confirmation frames. 0 (default) means N frames is enough — a courier who drops and leaves in a couple of seconds still delivers. Raise to reject a bag briefly set down.",
54921
55004
  min: 0,
54922
55005
  max: 300,
54923
55006
  step: 5,
@@ -54951,7 +55034,7 @@ function buildGlobalSettingsSchema(cap) {
54951
55034
  type: "slider",
54952
55035
  key: "packageDropActivityWindowSec",
54953
55036
  label: "Deliverer window",
54954
- description: "A delivery only counts if a person or vehicle was active on this camera within this many seconds before the package appeared — and a pick-up only with somebody around when it vanished. 0 disables the requirement.",
55037
+ description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Motion elsewhere on the camera does not count. 0 disables the requirement.",
54955
55038
  min: 0,
54956
55039
  max: 1800,
54957
55040
  step: 30,
@@ -57376,11 +57459,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
57376
57459
  */
57377
57460
  packageStillness = new PackageStillnessTracker({ newId: () => randomUUID() });
57378
57461
  /**
57379
- * Frame-clock timestamp of the last NON-package track seen per device — the
57380
- * activity mirror behind the package activity gate ("a parcel needs a
57381
- * deliverer"). Deliberately not {@link lastTrackActivityMs}: that map is
57382
- * stamped by ANY live track, including the package-class FP itself, which
57383
- * would let the streetlight wedge vouch for its own delivery.
57462
+ * Frame-clock timestamp of the last NON-package track seen IN A PACKAGE
57463
+ * DROP-OFF ZONE per device — the activity mirror behind the package
57464
+ * activity gate ("a parcel needs a deliverer"). A walker at the far gate
57465
+ * does not stamp this. Deliberately not {@link lastTrackActivityMs}: that
57466
+ * map is stamped by ANY live track, including the package-class FP itself,
57467
+ * which would let the streetlight wedge vouch for its own delivery.
57384
57468
  */
57385
57469
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
57386
57470
  /** Last time the oversize-sighting drop was logged per device — one line
@@ -58747,13 +58831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
58747
58831
  buildSummaryPorts(stores) {
58748
58832
  return {
58749
58833
  listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
58750
- await this.groupSealer?.sealWindow(deviceId, sinceMs, untilMs);
58751
- const groups = await stores.groupStore.listOverlapping({
58752
- deviceId,
58753
- sinceMs,
58754
- untilMs,
58755
- limit
58756
- });
58757
58834
  const windowTracks = await stores.trackStore.queryHistorical({
58758
58835
  deviceId,
58759
58836
  since: sinceMs,
@@ -58761,33 +58838,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
58761
58838
  projection: "slim",
58762
58839
  limit: Math.max(limit, 2e3)
58763
58840
  });
58764
- const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
58765
58841
  const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
58766
- const out = [];
58767
- for (const group of groups) {
58768
- const members = await stores.groupStore.listMembers(group.id);
58769
- const tracks = members.map((m) => trackById.get(m.trackId)).filter((t) => t !== void 0);
58770
- if (tracks.length === 0) continue;
58771
- const classes = [...new Set(tracks.flatMap((t) => [t.className, ...t.classes ?? []]))];
58772
- const zones = [...new Set(tracks.flatMap((t) => [...t.zonesVisited]))];
58773
- const importance = Math.max(0, ...tracks.map((t) => t.importance ?? 0));
58774
- const primary = tracks.reduce((best, t) => (t.importance ?? 0) > (best.importance ?? 0) ? t : best);
58775
- out.push({
58776
- trackId: group.id,
58777
- groupId: group.id,
58778
- memberTrackIds: members.map((m) => m.trackId),
58779
- deviceId,
58780
- className: primary.className,
58781
- firstSeen: group.openedAt,
58782
- lastSeen: group.closedAt,
58783
- importance,
58784
- zones,
58785
- classes,
58786
- ...tracks.every((t) => t.closedReason === "stationary-promotion") ? { stationary: true } : {},
58787
- confirmed: tracks.some((t) => t.observations !== void 0 && t.observations >= minHits)
58788
- });
58789
- }
58790
- return out;
58842
+ return windowTracks.map((t) => ({
58843
+ trackId: t.trackId,
58844
+ deviceId,
58845
+ className: t.className,
58846
+ firstSeen: t.firstSeen,
58847
+ lastSeen: t.lastSeen,
58848
+ zones: [...t.zonesVisited],
58849
+ ...t.importance !== void 0 ? { importance: t.importance } : {},
58850
+ ...t.classes !== void 0 && t.classes.length > 0 ? { classes: t.classes } : {},
58851
+ ...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
58852
+ confirmed: t.observations !== void 0 && t.observations >= minHits
58853
+ }));
58791
58854
  },
58792
58855
  listMediaInfo: async (id) => {
58793
58856
  const group = await stores.groupStore.get(id);
@@ -60270,6 +60333,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
60270
60333
  deviceId,
60271
60334
  tracked: result.tracked,
60272
60335
  packageClasses,
60336
+ packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
60273
60337
  settings: packageSettings,
60274
60338
  frameWidth: result.frameWidth,
60275
60339
  frameHeight: result.frameHeight,
@@ -61984,25 +62048,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
61984
62048
  }
61985
62049
  /** Hub-only: assign this sighting in RAM. First activity on a camera
61986
62050
  * replays 2 minutes of that camera (crash recover), then observe is sync. */
61987
- enqueueLiveGroup(track, now) {
61988
- if (!this.isPostProcessingNode || !this.groupLive) return;
61989
- const deviceId = track.deviceId;
61990
- const snapshot = toGroupableTrack(track);
61991
- if (this.groupLive.isPrimed(deviceId)) {
61992
- this.groupLive.observe(snapshot, now);
61993
- return;
61994
- }
61995
- const next = (this.groupObserveTail.get(deviceId) ?? Promise.resolve()).then(async () => {
61996
- if (this.groupLive && this.groupSealer && !this.groupLive.isPrimed(deviceId)) await this.groupSealer.recoverCamera(deviceId, this.groupLive);
61997
- this.groupLive?.observe(snapshot, now);
61998
- }).catch((err) => {
61999
- this.ctx.logger.warn("group live observe failed", {
62000
- tags: { deviceId },
62001
- meta: { error: String(err) }
62002
- });
62003
- });
62004
- this.groupObserveTail.set(deviceId, next);
62005
- }
62051
+ enqueueLiveGroup(track, now) {}
62006
62052
  /** Unified retention (Phase 1): earliest retained footage per device — the
62007
62053
  * follow-recordings cutoff source. Cached 10 min: availability answers in
62008
62054
  * ~70 ms but the sweep asks once per device per pass. `null` = no footage
@@ -62448,7 +62494,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62448
62494
  runPackageStillness(input) {
62449
62495
  if (this.packageDropDetector === null) return;
62450
62496
  const tags = { deviceId: input.deviceId };
62451
- for (const t of input.tracked) if (!input.packageClasses.has(t.className)) {
62497
+ for (const t of input.tracked) {
62498
+ if (input.packageClasses.has(t.className)) continue;
62499
+ if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
62452
62500
  this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
62453
62501
  break;
62454
62502
  }
@@ -62495,6 +62543,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62495
62543
  frameWidth: input.frameWidth,
62496
62544
  frameHeight: input.frameHeight,
62497
62545
  timestamp: input.timestamp,
62546
+ minHits: input.settings.packageDropMinFrames,
62498
62547
  minDwellMs: input.settings.packageDropDwellSec * 1e3,
62499
62548
  activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
62500
62549
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {}
@@ -62598,9 +62647,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62598
62647
  for (const ex of outcome.expired) {
62599
62648
  const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
62600
62649
  const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
62650
+ const requiredHits = input.settings.packageDropMinFrames;
62601
62651
  const reasons = [];
62602
62652
  if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
62603
- if (ex.rounds < 2) reasons.push("rounds");
62653
+ if (ex.hits < requiredHits) reasons.push("hits");
62604
62654
  if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
62605
62655
  this.ctx.logger.info("package candidate expired without promotion", {
62606
62656
  tags,
@@ -62608,8 +62658,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62608
62658
  candidateId: ex.id,
62609
62659
  className: ex.className,
62610
62660
  bbox: ex.bbox,
62611
- rounds: ex.rounds,
62612
- minRounds: 2,
62661
+ hits: ex.hits,
62662
+ minHits: requiredHits,
62613
62663
  observedDwellMs,
62614
62664
  requiredDwellMs,
62615
62665
  activityBackedAtOpen: ex.activityBackedAtOpen,
@@ -62871,6 +62921,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62871
62921
  async listRecentTracks(input) {
62872
62922
  return this.queryFacade.listRecentTracks(input);
62873
62923
  }
62924
+ async listGroups(input) {
62925
+ return {
62926
+ groups: [],
62927
+ nextCursor: null
62928
+ };
62929
+ }
62930
+ async getGroup(input) {
62931
+ return null;
62932
+ }
62874
62933
  /**
62875
62934
  * Every event kind the device can produce: built-ins (motion + audio),
62876
62935
  * detection classes actually observed on the device, and sensor kinds
@@ -64015,33 +64074,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
64015
64074
  buildFaceRescoreActionHandlers() {
64016
64075
  return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
64017
64076
  }
64018
- buildGroupActionHandlers() {
64019
- const ports = () => {
64020
- const groupStore = this.groupStore;
64021
- if (groupStore === null) return null;
64022
- return {
64023
- groupStore,
64024
- loadTrack: async (trackId) => {
64025
- const tracks = this.trackStore;
64026
- if (!tracks) return null;
64027
- return tracks.getActiveByTrack(trackId) ?? await tracks.getPersistedByTrackId(trackId);
64028
- },
64029
- eventMediaBaseUrl: () => this.eventMediaBaseUrl
64030
- };
64077
+ groupQueryPorts() {
64078
+ const groupStore = this.groupStore;
64079
+ if (groupStore === null) return null;
64080
+ return {
64081
+ groupStore,
64082
+ loadTracks: async (trackIds) => {
64083
+ const map = /* @__PURE__ */ new Map();
64084
+ const tracks = this.trackStore;
64085
+ if (!tracks || trackIds.length === 0) return map;
64086
+ const missing = [];
64087
+ for (const id of trackIds) {
64088
+ const active = tracks.getActiveByTrack(id);
64089
+ if (active !== null) map.set(id, trackToLite(active));
64090
+ else missing.push(id);
64091
+ }
64092
+ if (missing.length === 0) return map;
64093
+ for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
64094
+ return map;
64095
+ },
64096
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl
64031
64097
  };
64098
+ }
64099
+ buildGroupActionHandlers() {
64032
64100
  return {
64033
64101
  "analytics.listGroups": async (input) => {
64034
- const ready = ports();
64035
- if (ready === null) return {
64102
+ return {
64036
64103
  groups: [],
64037
64104
  hasMore: false
64038
64105
  };
64039
- return listGroupRecords(ready, input);
64040
64106
  },
64041
64107
  "analytics.getGroup": async (input) => {
64042
- const ready = ports();
64043
- if (ready === null) return { group: null };
64044
- return { group: await getGroupDetail(ready, input.groupId) };
64108
+ return { group: null };
64045
64109
  }
64046
64110
  };
64047
64111
  }