@camstack/addon-post-analysis 1.2.78 → 1.2.80

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 { At as string, B as addonWidgetsSourceCapability, C as NcSnoozeSchema, Ct as boolean, D as OpsLogEntrySchema, Dt as object, E as NcTaxonomySchema, Et as number, F as TIMELAPSE_DENSE_FLOOR_SEC, G as buildEventKindDescriptor, H as assertTimelapseCadences, I as TimelapseRuleInputSchema, J as defineCustomActions, K as cosineSimilarity$1, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as EventCategory, N as SCENE_DIVERGED, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as partialRecord, P as SceneMonitorSchema, Q as faceGalleryCapability, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as array, T as NcSystemEventKindSchema, Tt as literal, U as audioMetricsCapability, V as alarmPanelCapability, W as audioModeOf, Y as deriveRecordingMode, Z as encodeVectorBase64, _ as NcRuleInputSchema, _t as hydrateSchema, at as plateGalleryCapability, b as NcRuleTargetSchema, bt as sleep, c as EVENT_PAD_MS, ct as sceneMonitorCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as videoclipsCapability, et as isDetectionMacroClass, f as NC_CONDITION_CATALOG, ft as zoneAnalyticsCapability, g as NcConditionDescriptorSchema, gt as createEvent, h as NC_TAXONOMY, ht as DeviceType, i as DETECTION_MACRO_CLASSES, it as pipelineAnalyticsCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as unknown, k as RetrainStatusSchema, kt as record, l as LabelAttributionSchema, lt as subKindsOf, mt as BaseAddon, n as DEFAULT_EVENT_COLOR, nt as kebabToCamel, o as DeclaredDevices, ot as readDeviceStateFrom, p as NC_DEFAULT_SNOOZE_MINUTES, pt as errMsg, q as customAction, rt as notificationRulesCapability, s as EVENT_KIND_BY_CAP, st as readTimelapseGeneratedAt, t as BaseDevice, tt as isScheduleActive, u as MACRO_LABELS, ut as vectorDimFromBase64, v as NcRulePatchSchema, vt as isDeviceScopedCap, w as NcSnoozeSuppressedSchema, wt as discriminatedUnion, x as NcScheduleSchema, xt as _enum, y as NcRuleSchema, yt as nodePin, z as TrackSourceSchema } from "../dist-CA2KdMY9.mjs";
1
+ import { At as string, B as addonWidgetsSourceCapability, C as NcSnoozeSchema, Ct as boolean, D as OpsLogEntrySchema, Dt as object, E as NcTaxonomySchema, Et as number, F as TIMELAPSE_DENSE_FLOOR_SEC, G as buildEventKindDescriptor, H as assertTimelapseCadences, I as TimelapseRuleInputSchema, J as defineCustomActions, K as cosineSimilarity$1, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as EventCategory, N as SCENE_DIVERGED, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as partialRecord, P as SceneMonitorSchema, Q as faceGalleryCapability, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as array, T as NcSystemEventKindSchema, Tt as literal, U as audioMetricsCapability, V as alarmPanelCapability, W as audioModeOf, Y as deriveRecordingMode, Z as encodeVectorBase64, _ as NcRuleInputSchema, _t as hydrateSchema, at as plateGalleryCapability, b as NcRuleTargetSchema, bt as sleep, c as EVENT_PAD_MS, ct as sceneMonitorCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as videoclipsCapability, et as isDetectionMacroClass, f as NC_CONDITION_CATALOG, ft as zoneAnalyticsCapability, g as NcConditionDescriptorSchema, gt as createEvent, h as NC_TAXONOMY, ht as DeviceType, i as DETECTION_MACRO_CLASSES, it as pipelineAnalyticsCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as unknown, k as RetrainStatusSchema, kt as record, l as LabelAttributionSchema, lt as subKindsOf, mt as BaseAddon, n as DEFAULT_EVENT_COLOR, nt as kebabToCamel, o as DeclaredDevices, ot as readDeviceStateFrom, p as NC_DEFAULT_SNOOZE_MINUTES, pt as errMsg, q as customAction, rt as notificationRulesCapability, s as EVENT_KIND_BY_CAP, st as readTimelapseGeneratedAt, t as BaseDevice, tt as isScheduleActive, u as MACRO_LABELS, ut as vectorDimFromBase64, v as NcRulePatchSchema, vt as isDeviceScopedCap, w as NcSnoozeSuppressedSchema, wt as discriminatedUnion, x as NcScheduleSchema, xt as _enum, y as NcRuleSchema, yt as nodePin, z as TrackSourceSchema } from "../dist-D9Z7czrz.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";
@@ -14138,12 +14138,15 @@ function dwellSeconds(candidate) {
14138
14138
  * branch that discards work silently reads as "it never happened".
14139
14139
  */
14140
14140
  function filterSummaryCandidates(candidates, filters, zoneOwner) {
14141
+ const moving = filters?.includeStationary === true ? candidates : candidates.filter((candidate) => candidate.stationary !== true);
14142
+ const droppedStationary = candidates.length - moving.length;
14141
14143
  if (filters === void 0) return {
14142
- kept: [...candidates],
14144
+ kept: [...moving],
14143
14145
  droppedByClass: 0,
14144
14146
  droppedByZone: 0,
14145
14147
  droppedByImportance: 0,
14146
- droppedByDwell: 0
14148
+ droppedByDwell: 0,
14149
+ droppedStationary
14147
14150
  };
14148
14151
  const include = filters.classes;
14149
14152
  const exclude = new Set(filters.classesExclude ?? []);
@@ -14152,7 +14155,7 @@ function filterSummaryCandidates(candidates, filters, zoneOwner) {
14152
14155
  let droppedByZone = 0;
14153
14156
  let droppedByImportance = 0;
14154
14157
  let droppedByDwell = 0;
14155
- for (const candidate of candidates) {
14158
+ for (const candidate of moving) {
14156
14159
  const own = classesOf(candidate);
14157
14160
  if (own.some((c) => exclude.has(c))) {
14158
14161
  droppedByClass += 1;
@@ -14188,7 +14191,8 @@ function filterSummaryCandidates(candidates, filters, zoneOwner) {
14188
14191
  droppedByClass,
14189
14192
  droppedByZone,
14190
14193
  droppedByImportance,
14191
- droppedByDwell
14194
+ droppedByDwell,
14195
+ droppedStationary
14192
14196
  };
14193
14197
  }
14194
14198
  /**
@@ -14563,7 +14567,8 @@ var NcSummaryProducer = class {
14563
14567
  droppedByClass: filtered.droppedByClass,
14564
14568
  droppedByZone: filtered.droppedByZone,
14565
14569
  droppedByImportance: filtered.droppedByImportance,
14566
- droppedByDwell: filtered.droppedByDwell
14570
+ droppedByDwell: filtered.droppedByDwell,
14571
+ droppedStationary: filtered.droppedStationary
14567
14572
  } });
14568
14573
  if (opts.stamp) await this.deps.store.markGenerated(rule.id, window.endMs);
14569
14574
  return {
@@ -14571,7 +14576,8 @@ var NcSummaryProducer = class {
14571
14576
  window,
14572
14577
  tiles: 0,
14573
14578
  cameras: 0,
14574
- matched: 0
14579
+ matched: 0,
14580
+ droppedStationary: filtered.droppedStationary
14575
14581
  };
14576
14582
  }
14577
14583
  const loaded = await this.loadTiles(rule, selection.tiles);
@@ -14594,6 +14600,7 @@ var NcSummaryProducer = class {
14594
14600
  produced: 0,
14595
14601
  window,
14596
14602
  tiles: loaded.length,
14603
+ droppedStationary: filtered.droppedStationary,
14597
14604
  skippedByAi: true,
14598
14605
  ...ai.aiFailure !== void 0 ? { aiFailure: ai.aiFailure } : {},
14599
14606
  ...ai.aiImages !== void 0 ? { aiImages: ai.aiImages } : {}
@@ -14624,6 +14631,8 @@ var NcSummaryProducer = class {
14624
14631
  ...meta,
14625
14632
  scanned: candidates.length,
14626
14633
  matched: filtered.kept.length,
14634
+ droppedStationary: filtered.droppedStationary,
14635
+ droppedByClass: filtered.droppedByClass,
14627
14636
  tiles: loaded.length,
14628
14637
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
14629
14638
  camerasDropped: selection.camerasDropped,
@@ -14642,6 +14651,7 @@ var NcSummaryProducer = class {
14642
14651
  tiles: loaded.length,
14643
14652
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
14644
14653
  matched: filtered.kept.length,
14654
+ droppedStationary: filtered.droppedStationary,
14645
14655
  enqueued,
14646
14656
  ...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
14647
14657
  ...mosaic.ref !== null ? { mosaicUrl: mosaic.ref.url } : {},
@@ -15014,7 +15024,26 @@ var NcSummaryFiltersSchema = object({
15014
15024
  classesExclude: array(string().min(1).max(40)).max(20).optional(),
15015
15025
  zones: array(string()).max(40).optional(),
15016
15026
  minImportance: number().min(0).max(1).optional(),
15017
- minDwellSeconds: number().min(0).max(3600).optional()
15027
+ minDwellSeconds: number().min(0).max(3600).optional(),
15028
+ /**
15029
+ * Put PARKED objects in the digest too. Off unless explicitly turned on, and
15030
+ * an ABSENT field reads as off — the soft migration, no backfill.
15031
+ *
15032
+ * A digest is a record of PASSAGES: the operator's words are "the last
15033
+ * summaries still include an enormous number of stationary tracks". Since
15034
+ * D161 a track promoted to the stationary registry is persisted rather than
15035
+ * dropped (it owns media the retention sweep has to reach), so the car parked
15036
+ * in front of camera 617 now enters every window it overlaps and, ranked on
15037
+ * importance and dwell, wins tiles from the passages the digest is about. The
15038
+ * measured effect on the live hub was 1-2 tiles becoming 10.
15039
+ *
15040
+ * `.optional()` and NOT `.default(false)`: a `.default()` here would be
15041
+ * materialised by `NcSummaryRulePatchSchema` on any patch that named
15042
+ * `filters`, which is the `.partial()` trap this module's docblock describes.
15043
+ * `filterSummaryCandidates` reads `=== true`, so absent and false are the
15044
+ * same answer and neither needs a stored value.
15045
+ */
15046
+ includeStationary: boolean().optional()
15018
15047
  });
15019
15048
  /**
15020
15049
  * How tiles are chosen when the window holds more tracks than the budget.
@@ -21798,6 +21827,14 @@ var ncActions = defineCustomActions({
21798
21827
  tiles: number().int().optional(),
21799
21828
  cameras: number().int().optional(),
21800
21829
  matched: number().int().optional(),
21830
+ /**
21831
+ * PARKED objects the window held and the rule dropped (`filters
21832
+ * .includeStationary`). Without it a Test that returns two tiles out of
21833
+ * forty tracks cannot tell the operator whether the night was quiet or
21834
+ * whether it was the car park — and that is the exact question this
21835
+ * option exists to answer.
21836
+ */
21837
+ droppedStationary: number().int().optional(),
21801
21838
  enqueued: number().int().optional(),
21802
21839
  mosaicBytes: number().int().optional(),
21803
21840
  mosaicUrl: string().optional(),
@@ -22264,6 +22301,7 @@ function makeNcActionHandlers(deps) {
22264
22301
  ...out.tiles !== void 0 ? { tiles: out.tiles } : {},
22265
22302
  ...out.cameras !== void 0 ? { cameras: out.cameras } : {},
22266
22303
  ...out.matched !== void 0 ? { matched: out.matched } : {},
22304
+ ...out.droppedStationary !== void 0 ? { droppedStationary: out.droppedStationary } : {},
22267
22305
  ...out.enqueued !== void 0 ? { enqueued: out.enqueued } : {},
22268
22306
  ...out.mosaicBytes !== void 0 ? { mosaicBytes: out.mosaicBytes } : {},
22269
22307
  ...out.mosaicUrl !== void 0 ? { mosaicUrl: out.mosaicUrl } : {},
@@ -26267,7 +26305,9 @@ var FrameProcessor = class {
26267
26305
  }) : {
26268
26306
  suppressedIndices: /* @__PURE__ */ new Set(),
26269
26307
  confirmed: [],
26270
- wokenEntryIds: []
26308
+ wokenEntryIds: [],
26309
+ armedEntryIds: [],
26310
+ disarmedEntryIds: []
26271
26311
  };
26272
26312
  const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
26273
26313
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
@@ -26477,6 +26517,8 @@ var FrameProcessor = class {
26477
26517
  rawTrackedDetections: trackedDetections,
26478
26518
  stationaryConfirmed: gate.confirmed,
26479
26519
  stationaryWoken: gate.wokenEntryIds,
26520
+ stationaryArmed: gate.armedEntryIds,
26521
+ stationaryDisarmed: gate.disarmedEntryIds,
26480
26522
  riderFolds
26481
26523
  };
26482
26524
  }
@@ -36004,20 +36046,38 @@ function centroidInside(b, p) {
36004
36046
  * box → SUPPRESS (detector box jitter — the box shrank/grew around the same
36005
36047
  * centre; still the parked object, must not spawn a duplicate track).
36006
36048
  * - `wakeAssocIou` ≤ best IoU < `suppressIou`, centroid OUTSIDE the parked box
36007
- * → WAKE (the object's box slid off its spot → it is departing). Not
36008
- * suppressed, so it spawns a fresh moving track.
36049
+ * → DEPARTING (the object's box slid off its spot).
36009
36050
  * - best IoU < `wakeAssocIou` → MISS: no detection overlaps this entry (a
36010
- * different object, or the parked object is momentarily undetected). Neither
36011
- * suppress nor wake; the TTL sweep retires it only if the absence persists.
36051
+ * different object, or the parked object is momentarily undetected). The TTL
36052
+ * sweep retires it only if the absence persists.
36053
+ *
36054
+ * ## A departure is never acted on from ONE read (D49)
36055
+ *
36056
+ * Retiring an entry DESTROYS work: the object's registry identity, and with it
36057
+ * the suppression that stops it re-spawning a track every 30 s. So a single
36058
+ * departing read only ARMS the entry — the detection is still suppressed and
36059
+ * the entry survives. The entry is retired only when a SECOND read agrees:
36060
+ * departing again, or missing entirely (the object really has left the spot).
36061
+ * A confirming read DISARMS it.
36062
+ *
36063
+ * The caller owns the armed set and passes it back in; this function stays
36064
+ * pure. Cost of the extra read: a genuinely departing object spawns its track
36065
+ * one frame later. Cost of NOT having it, measured on the live hub over 24 h:
36066
+ * 65% of all stationary entries were retired within 300 s — below the TTL, so
36067
+ * by a wake — and 38% of the tracks that promotion then re-created had never
36068
+ * moved in their entire life. One bad box per parked object was enough.
36012
36069
  *
36013
36070
  * A detection can suppress at most one spawn even if it overlaps two entries
36014
36071
  * (`suppressedIndices` is a set).
36015
36072
  */
36016
36073
  function partitionDetectionsAgainstRegistry(input) {
36017
36074
  const { entries, detections, config } = input;
36075
+ const armed = input.armedEntryIds ?? EMPTY_ARMED;
36018
36076
  const suppressed = /* @__PURE__ */ new Set();
36019
36077
  const confirmed = [];
36020
36078
  const woken = [];
36079
+ const toArm = [];
36080
+ const toDisarm = [];
36021
36081
  for (const entry of entries) {
36022
36082
  let bestIou = 0;
36023
36083
  let bestIdx = -1;
@@ -36030,23 +36090,37 @@ function partitionDetectionsAgainstRegistry(input) {
36030
36090
  bestIdx = di;
36031
36091
  }
36032
36092
  }
36033
- if (bestIdx < 0 || bestIou < config.wakeAssocIou) continue;
36093
+ if (bestIdx < 0 || bestIou < config.wakeAssocIou) {
36094
+ if (armed.has(entry.id)) woken.push(entry.id);
36095
+ continue;
36096
+ }
36034
36097
  const detCentroid = centroid(detections[bestIdx].bbox);
36035
- if (bestIou >= config.suppressIou || centroidInside(entry.bbox, detCentroid)) {
36098
+ const strong = bestIou >= config.suppressIou;
36099
+ if (strong || centroidInside(entry.bbox, detCentroid)) {
36036
36100
  suppressed.add(bestIdx);
36037
36101
  confirmed.push({
36038
36102
  entryId: entry.id,
36039
36103
  className: entry.className,
36040
- bbox: { ...entry.bbox }
36104
+ bbox: { ...entry.bbox },
36105
+ observedBbox: { ...detections[bestIdx].bbox },
36106
+ strong
36041
36107
  });
36042
- } else woken.push(entry.id);
36108
+ if (armed.has(entry.id)) toDisarm.push(entry.id);
36109
+ } else if (armed.has(entry.id)) woken.push(entry.id);
36110
+ else {
36111
+ toArm.push(entry.id);
36112
+ suppressed.add(bestIdx);
36113
+ }
36043
36114
  }
36044
36115
  return {
36045
36116
  suppressedIndices: suppressed,
36046
36117
  confirmed,
36047
- wokenEntryIds: woken
36118
+ wokenEntryIds: woken,
36119
+ armedEntryIds: toArm,
36120
+ disarmedEntryIds: toDisarm
36048
36121
  };
36049
36122
  }
36123
+ var EMPTY_ARMED = /* @__PURE__ */ new Set();
36050
36124
  /**
36051
36125
  * A track is promoted when, over the recent `windowMs`, its centroid barely
36052
36126
  * moved (both net displacement and path span below the static thresholds) AND
@@ -36619,6 +36693,21 @@ var StationaryObjectRegistry = class {
36619
36693
  * between motion sessions; that silence is not evidence the object left,
36620
36694
  * so quiet minutes must not age the entries (see {@link sweep}). */
36621
36695
  lastFrameAtByDevice = /* @__PURE__ */ new Map();
36696
+ /**
36697
+ * Entries carrying ONE unconfirmed departure read, per device (D49, D174).
36698
+ *
36699
+ * Retiring an entry destroys work — the object's registry identity and the
36700
+ * suppression that keeps it from re-spawning a track every promotion window.
36701
+ * A single frame whose box slid off the parked spot is a fallible read (a
36702
+ * partial occlusion, a shadow, one bad box), so it only ARMS the entry here.
36703
+ * The retirement happens on the next frame that AGREES; a confirming frame
36704
+ * clears the strike.
36705
+ */
36706
+ departureStrikes = /* @__PURE__ */ new Map();
36707
+ /** Detections absorbed by an existing entry since the last sweep, per device.
36708
+ * Rolled up into ONE log line per sweep — this fires on every frame of every
36709
+ * parked object, so it can never be a line of its own. */
36710
+ absorbedSinceSweep = /* @__PURE__ */ new Map();
36622
36711
  constructor(deps) {
36623
36712
  this.logger = deps.logger;
36624
36713
  this.ledger = new DurableLedger({
@@ -36691,12 +36780,15 @@ var StationaryObjectRegistry = class {
36691
36780
  if (entries.length === 0) return {
36692
36781
  suppressedIndices: /* @__PURE__ */ new Set(),
36693
36782
  confirmed: [],
36694
- wokenEntryIds: []
36783
+ wokenEntryIds: [],
36784
+ armedEntryIds: [],
36785
+ disarmedEntryIds: []
36695
36786
  };
36696
36787
  return partitionDetectionsAgainstRegistry({
36697
36788
  entries,
36698
36789
  detections: input.detections,
36699
- config: config ?? this.matchConfig
36790
+ config: config ?? this.matchConfig,
36791
+ ...this.departureStrikes.has(input.deviceId) ? { armedEntryIds: this.departureStrikes.get(input.deviceId) } : {}
36700
36792
  });
36701
36793
  }
36702
36794
  /** Fold a frame's gate result back into state: advance confirmed entries'
@@ -36713,15 +36805,41 @@ var StationaryObjectRegistry = class {
36713
36805
  if (!e) continue;
36714
36806
  this.ledger.stage({
36715
36807
  ...e,
36716
- lastConfirmedAt: input.timestamp
36808
+ lastConfirmedAt: input.timestamp,
36809
+ ...c.strong ? { bbox: { ...c.observedBbox } } : {}
36717
36810
  });
36718
36811
  this.dirty.add(c.entryId);
36719
36812
  }
36813
+ this.absorbedSinceSweep.set(input.deviceId, (this.absorbedSinceSweep.get(input.deviceId) ?? 0) + input.confirmed.length);
36814
+ if (input.armedEntryIds && input.armedEntryIds.length > 0) {
36815
+ let armed = this.departureStrikes.get(input.deviceId);
36816
+ if (!armed) {
36817
+ armed = /* @__PURE__ */ new Set();
36818
+ this.departureStrikes.set(input.deviceId, armed);
36819
+ }
36820
+ for (const id of input.armedEntryIds) if (ids.has(id)) armed.add(id);
36821
+ }
36822
+ const armed = this.departureStrikes.get(input.deviceId);
36823
+ if (armed && input.disarmedEntryIds) {
36824
+ for (const id of input.disarmedEntryIds) armed.delete(id);
36825
+ if (armed.size === 0) this.departureStrikes.delete(input.deviceId);
36826
+ }
36720
36827
  for (const id of input.wokenEntryIds) {
36721
36828
  if (!ids.has(id)) continue;
36722
36829
  const e = this.ledger.get(id);
36723
36830
  if (!e) continue;
36724
36831
  this.dropEntry(input.deviceId, id);
36832
+ this.logger.info("stationary entry retired", {
36833
+ tags: { deviceId: input.deviceId },
36834
+ meta: {
36835
+ deviceId: input.deviceId,
36836
+ entryId: id,
36837
+ className: e.className,
36838
+ reason: "departed",
36839
+ parkedForMs: input.timestamp - e.becameStationaryAt,
36840
+ ...e.sourceTrackId !== void 0 ? { sourceTrackId: e.sourceTrackId } : {}
36841
+ }
36842
+ });
36725
36843
  this.onChange?.({
36726
36844
  phase: "departed",
36727
36845
  entry: e,
@@ -36768,6 +36886,18 @@ var StationaryObjectRegistry = class {
36768
36886
  if (observedAt - e.lastConfirmedAt > ttl) {
36769
36887
  this.dropEntry(deviceId, id);
36770
36888
  retired.push(e);
36889
+ this.logger.info("stationary entry retired", {
36890
+ tags: { deviceId },
36891
+ meta: {
36892
+ deviceId,
36893
+ entryId: id,
36894
+ className: e.className,
36895
+ reason: "ttl",
36896
+ ttlMs: ttl,
36897
+ unconfirmedForMs: observedAt - e.lastConfirmedAt,
36898
+ parkedForMs: observedAt - e.becameStationaryAt
36899
+ }
36900
+ });
36771
36901
  this.onChange?.({
36772
36902
  phase: "departed",
36773
36903
  entry: e,
@@ -36777,6 +36907,18 @@ var StationaryObjectRegistry = class {
36777
36907
  }
36778
36908
  if (ids.size === 0) this.idsByDevice.delete(deviceId);
36779
36909
  }
36910
+ for (const [deviceId, absorbed] of this.absorbedSinceSweep) {
36911
+ if (absorbed === 0) continue;
36912
+ this.logger.info("stationary registry absorbed detections", {
36913
+ tags: { deviceId },
36914
+ meta: {
36915
+ deviceId,
36916
+ absorbed,
36917
+ entries: this.count(deviceId)
36918
+ }
36919
+ });
36920
+ }
36921
+ this.absorbedSinceSweep.clear();
36780
36922
  for (const id of [...this.dirty]) {
36781
36923
  this.dirty.delete(id);
36782
36924
  const entry = this.ledger.get(id);
@@ -36789,6 +36931,8 @@ var StationaryObjectRegistry = class {
36789
36931
  * Used on device unbind; a rebind reloads from the store. */
36790
36932
  forgetDevice(deviceId) {
36791
36933
  this.lastFrameAtByDevice.delete(deviceId);
36934
+ this.departureStrikes.delete(deviceId);
36935
+ this.absorbedSinceSweep.delete(deviceId);
36792
36936
  const ids = this.idsByDevice.get(deviceId);
36793
36937
  if (!ids) return;
36794
36938
  for (const id of ids) {
@@ -36800,6 +36944,8 @@ var StationaryObjectRegistry = class {
36800
36944
  /** Delete every persisted + in-memory entry for a device (operator wipe). */
36801
36945
  async clearDevice(deviceId) {
36802
36946
  this.lastFrameAtByDevice.delete(deviceId);
36947
+ this.departureStrikes.delete(deviceId);
36948
+ this.absorbedSinceSweep.delete(deviceId);
36803
36949
  const ids = this.idsByDevice.get(deviceId);
36804
36950
  if (ids) {
36805
36951
  for (const id of [...ids]) {
@@ -36817,10 +36963,17 @@ var StationaryObjectRegistry = class {
36817
36963
  }
36818
36964
  set.add(id);
36819
36965
  }
36820
- /** Retire one entry: index, staged advance and durable row all go. */
36966
+ /** Retire one entry: index, staged advance, pending strike and durable row
36967
+ * all go. A strike that outlived its entry would arm the NEXT entry to reuse
36968
+ * the id — the registry mints UUIDs, but the invariant is cheap to keep. */
36821
36969
  dropEntry(deviceId, id) {
36822
36970
  this.idsByDevice.get(deviceId)?.delete(id);
36823
36971
  this.dirty.delete(id);
36972
+ const armed = this.departureStrikes.get(deviceId);
36973
+ if (armed) {
36974
+ armed.delete(id);
36975
+ if (armed.size === 0) this.departureStrikes.delete(deviceId);
36976
+ }
36824
36977
  this.ledger.forget(id);
36825
36978
  }
36826
36979
  };
@@ -40864,6 +41017,26 @@ function isAfterCursor(row, cursor) {
40864
41017
  }
40865
41018
  //#endregion
40866
41019
  //#region src/pipeline-analytics/store/track-store.ts
41020
+ /**
41021
+ * WHY a track's row was written EARLY — i.e. by {@link TrackStore.closeActive}
41022
+ * rather than by the ordinary TTL expiry.
41023
+ *
41024
+ * `'stationary-promotion'` is the only member and means: the tracker decided
41025
+ * this subject had PARKED, handed it to the stationary registry, and the row
41026
+ * was written so its media, events and vector keep an owner (D161). It is not a
41027
+ * quality signal and it never hides the track — a parked car is still a track,
41028
+ * still on the events page, still swept by the same retention. It exists so a
41029
+ * consumer that wants PASSAGES can say so exactly, instead of re-deriving
41030
+ * "did it move" from a trajectory downstream and getting it wrong on the
41031
+ * subject that stood still for one honest reason.
41032
+ *
41033
+ * An ABSENT value means "expired normally, or written before this marker
41034
+ * existed". Never read an absence as a positive — same rule as `hasFace`.
41035
+ */
41036
+ var TRACK_CLOSE_REASONS = ["stationary-promotion"];
41037
+ function parseCloseReason(value) {
41038
+ return TRACK_CLOSE_REASONS.find((reason) => reason === value);
41039
+ }
40867
41040
  var DEFAULT_CONFIG = {
40868
41041
  ttlMs: 3e4,
40869
41042
  maxPositionHistory: 300
@@ -41086,6 +41259,10 @@ var TRACKS_COLUMNS = [
41086
41259
  {
41087
41260
  name: "frameHeight",
41088
41261
  type: "INTEGER"
41262
+ },
41263
+ {
41264
+ name: "closedReason",
41265
+ type: "TEXT"
41089
41266
  }
41090
41267
  ];
41091
41268
  var TRACKS_INDEXES = [{
@@ -41145,7 +41322,8 @@ function cloneTrack(t) {
41145
41322
  } : {},
41146
41323
  ...t.debug !== void 0 ? { debug: t.debug } : {},
41147
41324
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
41148
- ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {}
41325
+ ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
41326
+ ...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
41149
41327
  };
41150
41328
  }
41151
41329
  var TrackStore = class {
@@ -41714,13 +41892,22 @@ var TrackStore = class {
41714
41892
  * nothing has been written under that id yet. It is never correct for a track
41715
41893
  * that ran.
41716
41894
  *
41895
+ * **The row SAYS why it was closed early** (`reason` → the `closedReason`
41896
+ * column). Persisting a promoted track put its footprint back under the
41897
+ * retention sweep, and it also put the parked car back in front of every
41898
+ * consumer that reads the window's tracks: the operator's nightly digest went
41899
+ * from 1-2 tiles to 10, most of them the same stationary car. The marker is
41900
+ * written HERE, at the one place that knows, so a consumer can ask for
41901
+ * passages exactly instead of guessing at movement from a stored trajectory.
41902
+ *
41717
41903
  * Returns whether an active entry was found. THROWS if the persist fails: the
41718
41904
  * caller must log it, because a swallowed failure here IS the leak.
41719
41905
  */
41720
- async closeActive(trackId) {
41906
+ async closeActive(trackId, reason) {
41721
41907
  const t = this.active.get(trackId);
41722
41908
  if (t === void 0) return false;
41723
41909
  t.active = false;
41910
+ t.closedReason = reason;
41724
41911
  const record = cloneTrack(t);
41725
41912
  await this.persistCompleted(record);
41726
41913
  this.active.delete(trackId);
@@ -42302,6 +42489,7 @@ var TrackStore = class {
42302
42489
  ...t.debug !== void 0 ? { debug: t.debug } : {},
42303
42490
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
42304
42491
  ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
42492
+ ...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
42305
42493
  ...envelope !== null && dims !== void 0 ? {
42306
42494
  envMinX: envelope.minX,
42307
42495
  envMinY: envelope.minY,
@@ -42339,6 +42527,7 @@ var TrackStore = class {
42339
42527
  const hasFace = data["hasFace"];
42340
42528
  /** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`. */
42341
42529
  const hasRider = data["hasRider"];
42530
+ const closedReason = parseCloseReason(data["closedReason"]);
42342
42531
  const envMinX = data["envMinX"];
42343
42532
  const envMinY = data["envMinY"];
42344
42533
  const envMaxX = data["envMaxX"];
@@ -42376,6 +42565,7 @@ var TrackStore = class {
42376
42565
  ...typeof debug === "boolean" ? { debug } : {},
42377
42566
  ...typeof hasFace === "boolean" ? { hasFace } : {},
42378
42567
  ...typeof hasRider === "boolean" ? { hasRider } : {},
42568
+ ...closedReason !== void 0 ? { closedReason } : {},
42379
42569
  ...envelope !== null ? { envelope } : {}
42380
42570
  };
42381
42571
  }
@@ -49651,11 +49841,22 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
49651
49841
  async onInitialize() {
49652
49842
  const api = this.ctx.api;
49653
49843
  if (!api) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
49654
- await this.declareCollections(api);
49655
- await this.declareAlarmPanel(api);
49844
+ const bootT0 = Date.now();
49845
+ const step = async (name, fn) => {
49846
+ const t0 = Date.now();
49847
+ const out = await fn();
49848
+ this.ctx.logger.info("pipeline-analytics boot step", { meta: {
49849
+ step: name,
49850
+ ms: Date.now() - t0,
49851
+ sinceBootMs: Date.now() - bootT0
49852
+ } });
49853
+ return out;
49854
+ };
49855
+ await step("declareCollections", () => this.declareCollections(api));
49856
+ await step("declareAlarmPanel", () => this.declareAlarmPanel(api));
49656
49857
  const logger = this.ctx.logger;
49657
- const storage = await this.resolveMediaStorage(logger);
49658
- const stores = await this.buildStores(api, logger, storage);
49858
+ const storage = await step("resolveMediaStorage", () => this.resolveMediaStorage(logger));
49859
+ const stores = await step("buildStores", () => this.buildStores(api, logger, storage));
49659
49860
  const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
49660
49861
  const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
49661
49862
  this.eventsOpsLog = new OpsLogStore({
@@ -49664,7 +49865,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
49664
49865
  nodeId: ownNodeId
49665
49866
  });
49666
49867
  {
49667
- const designated = await this.postProcessingNodeState.get();
49868
+ const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
49668
49869
  this.isPostProcessingNode = ownNodeId === designated;
49669
49870
  if (!this.isPostProcessingNode) logger.info("pipeline-analytics INERT on this node — post-processing assigned elsewhere", { meta: {
49670
49871
  ownNodeId,
@@ -49688,8 +49889,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
49688
49889
  this.buildRecognizers(api, logger, stores, transport);
49689
49890
  this.buildDetailDispatcher(api, logger, transport);
49690
49891
  const capProviders = this.buildAnalyticsProviders(api, logger);
49691
- await this.buildNotificationCenter(api, logger, stores);
49692
- await this.serveEventMediaDataPlane();
49892
+ await step("buildNotificationCenter", () => this.buildNotificationCenter(api, logger, stores));
49893
+ await step("serveEventMediaDataPlane", () => this.serveEventMediaDataPlane());
49693
49894
  this.subscriptionUnsubs = wireSubscriptions({
49694
49895
  eventBus: this.ctx.eventBus,
49695
49896
  isPostProcessingNode: this.isPostProcessingNode,
@@ -49705,11 +49906,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
49705
49906
  onClusterTopology: (nodes) => this.nodeLiveness.applySnapshot(nodes, Date.now()),
49706
49907
  onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event)
49707
49908
  });
49708
- if (this.isPostProcessingNode) await this.startEmbeddingDispatcher(logger, transport);
49709
- await this.buildSceneMonitor(api, logger);
49909
+ if (this.isPostProcessingNode) await step("startEmbeddingDispatcher", () => this.startEmbeddingDispatcher(logger, transport));
49910
+ await step("buildSceneMonitor", () => this.buildSceneMonitor(api, logger));
49710
49911
  this.startSweepTimers();
49711
49912
  this.ctx.logger.info("pipeline-analytics subscribers installed");
49712
- const providers = this.buildProviderRegistrations(api, stores, capProviders);
49913
+ const providers = await step("buildProviderRegistrations", () => this.buildProviderRegistrations(api, stores, capProviders));
49713
49914
  const isHub = ownNodeId === "hub";
49714
49915
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
49715
49916
  return {
@@ -49884,26 +50085,30 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
49884
50085
  }
49885
50086
  async declareCollections(api) {
49886
50087
  this.settingsStore = api.settingsStore;
49887
- await TrackStore.declare(api.settingsStore);
49888
- await MediaStore.declare(api.settingsStore);
49889
- await EventStore.declare(api.settingsStore);
49890
- await SensorEventStore.declare(api.settingsStore);
49891
- await IdentityStore.declare(api.settingsStore);
49892
- await FaceStore.declare(api.settingsStore);
49893
- await PlateStore.declare(api.settingsStore);
49894
- await VehicleStore.declare(api.settingsStore);
49895
- await StationaryObjectRegistry.declare(api.settingsStore);
49896
- await RetrainFrameStore.declare(api.settingsStore);
49897
- await RetrainAnnotationStore.declare(api.settingsStore);
49898
- await OpsLogStore.declare(api.settingsStore);
49899
- await AnalyticsLts.declare(api.settingsStore);
49900
- await SceneStore.declare(api.settingsStore);
49901
- await NotificationCenter.declare(api.settingsStore);
49902
- await runLabelTierMigration({
50088
+ await Promise.all([
50089
+ TrackStore.declare(api.settingsStore),
50090
+ MediaStore.declare(api.settingsStore),
50091
+ EventStore.declare(api.settingsStore),
50092
+ SensorEventStore.declare(api.settingsStore),
50093
+ IdentityStore.declare(api.settingsStore),
50094
+ FaceStore.declare(api.settingsStore),
50095
+ PlateStore.declare(api.settingsStore),
50096
+ VehicleStore.declare(api.settingsStore),
50097
+ StationaryObjectRegistry.declare(api.settingsStore),
50098
+ RetrainFrameStore.declare(api.settingsStore),
50099
+ RetrainAnnotationStore.declare(api.settingsStore),
50100
+ OpsLogStore.declare(api.settingsStore),
50101
+ AnalyticsLts.declare(api.settingsStore),
50102
+ SceneStore.declare(api.settingsStore),
50103
+ NotificationCenter.declare(api.settingsStore)
50104
+ ]);
50105
+ runLabelTierMigration({
49903
50106
  store: api.settingsStore,
49904
50107
  logger: this.ctx.logger,
49905
50108
  now: () => Date.now()
49906
- }, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]);
50109
+ }, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
50110
+ this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
50111
+ });
49907
50112
  }
49908
50113
  /** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
49909
50114
  * the node data dir (appData) → heavy thumbnail/track-frame churn hammers
@@ -50563,7 +50768,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
50563
50768
  lastSeen: t.lastSeen,
50564
50769
  ...t.importance !== void 0 ? { importance: t.importance } : {},
50565
50770
  zones: [...t.zonesVisited],
50566
- ...t.classes !== void 0 ? { classes: [...t.classes] } : {}
50771
+ ...t.classes !== void 0 ? { classes: [...t.classes] } : {},
50772
+ ...t.closedReason === "stationary-promotion" ? { stationary: true } : {}
50567
50773
  }));
50568
50774
  },
50569
50775
  listMediaInfo: async (trackId) => {
@@ -51667,10 +51873,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
51667
51873
  this.trackStore?.markRiderFolded(fold.trackId);
51668
51874
  }
51669
51875
  if (source === "pipeline") this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
51670
- if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0)) this.stationaryRegistry.applyFrameOutcome({
51876
+ if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0 || result.stationaryArmed.length > 0 || result.stationaryDisarmed.length > 0)) this.stationaryRegistry.applyFrameOutcome({
51671
51877
  deviceId,
51672
51878
  confirmed: result.stationaryConfirmed,
51673
51879
  wokenEntryIds: result.stationaryWoken,
51880
+ armedEntryIds: result.stationaryArmed,
51881
+ disarmedEntryIds: result.stationaryDisarmed,
51674
51882
  timestamp: result.timestamp
51675
51883
  });
51676
51884
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
@@ -53727,7 +53935,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
53727
53935
  };
53728
53936
  if (plausible) this.stationaryRegistry?.promote(entry);
53729
53937
  processor.dropTrack(track.trackId);
53730
- this.trackStore?.closeActive(track.trackId).catch((err) => {
53938
+ this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
53731
53939
  this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
53732
53940
  tags: { deviceId },
53733
53941
  meta: {
@@ -53830,7 +54038,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
53830
54038
  if (!s.enabled) return {
53831
54039
  suppressedIndices: /* @__PURE__ */ new Set(),
53832
54040
  confirmed: [],
53833
- wokenEntryIds: []
54041
+ wokenEntryIds: [],
54042
+ armedEntryIds: [],
54043
+ disarmedEntryIds: []
53834
54044
  };
53835
54045
  const matchConfig = {
53836
54046
  suppressIou: s.suppressIou,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DteUZxxp.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DHm8hb9G.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.78",
3
+ "version": "1.2.80",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",