@camstack/addon-post-analysis 1.2.78 → 1.2.79

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CFOT1Yoz.js");
5
+ const require_dist = require("../dist-CWoaZE47.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -14144,12 +14144,15 @@ function dwellSeconds(candidate) {
14144
14144
  * branch that discards work silently reads as "it never happened".
14145
14145
  */
14146
14146
  function filterSummaryCandidates(candidates, filters, zoneOwner) {
14147
+ const moving = filters?.includeStationary === true ? candidates : candidates.filter((candidate) => candidate.stationary !== true);
14148
+ const droppedStationary = candidates.length - moving.length;
14147
14149
  if (filters === void 0) return {
14148
- kept: [...candidates],
14150
+ kept: [...moving],
14149
14151
  droppedByClass: 0,
14150
14152
  droppedByZone: 0,
14151
14153
  droppedByImportance: 0,
14152
- droppedByDwell: 0
14154
+ droppedByDwell: 0,
14155
+ droppedStationary
14153
14156
  };
14154
14157
  const include = filters.classes;
14155
14158
  const exclude = new Set(filters.classesExclude ?? []);
@@ -14158,7 +14161,7 @@ function filterSummaryCandidates(candidates, filters, zoneOwner) {
14158
14161
  let droppedByZone = 0;
14159
14162
  let droppedByImportance = 0;
14160
14163
  let droppedByDwell = 0;
14161
- for (const candidate of candidates) {
14164
+ for (const candidate of moving) {
14162
14165
  const own = classesOf(candidate);
14163
14166
  if (own.some((c) => exclude.has(c))) {
14164
14167
  droppedByClass += 1;
@@ -14194,7 +14197,8 @@ function filterSummaryCandidates(candidates, filters, zoneOwner) {
14194
14197
  droppedByClass,
14195
14198
  droppedByZone,
14196
14199
  droppedByImportance,
14197
- droppedByDwell
14200
+ droppedByDwell,
14201
+ droppedStationary
14198
14202
  };
14199
14203
  }
14200
14204
  /**
@@ -14569,7 +14573,8 @@ var NcSummaryProducer = class {
14569
14573
  droppedByClass: filtered.droppedByClass,
14570
14574
  droppedByZone: filtered.droppedByZone,
14571
14575
  droppedByImportance: filtered.droppedByImportance,
14572
- droppedByDwell: filtered.droppedByDwell
14576
+ droppedByDwell: filtered.droppedByDwell,
14577
+ droppedStationary: filtered.droppedStationary
14573
14578
  } });
14574
14579
  if (opts.stamp) await this.deps.store.markGenerated(rule.id, window.endMs);
14575
14580
  return {
@@ -14577,7 +14582,8 @@ var NcSummaryProducer = class {
14577
14582
  window,
14578
14583
  tiles: 0,
14579
14584
  cameras: 0,
14580
- matched: 0
14585
+ matched: 0,
14586
+ droppedStationary: filtered.droppedStationary
14581
14587
  };
14582
14588
  }
14583
14589
  const loaded = await this.loadTiles(rule, selection.tiles);
@@ -14600,6 +14606,7 @@ var NcSummaryProducer = class {
14600
14606
  produced: 0,
14601
14607
  window,
14602
14608
  tiles: loaded.length,
14609
+ droppedStationary: filtered.droppedStationary,
14603
14610
  skippedByAi: true,
14604
14611
  ...ai.aiFailure !== void 0 ? { aiFailure: ai.aiFailure } : {},
14605
14612
  ...ai.aiImages !== void 0 ? { aiImages: ai.aiImages } : {}
@@ -14630,6 +14637,8 @@ var NcSummaryProducer = class {
14630
14637
  ...meta,
14631
14638
  scanned: candidates.length,
14632
14639
  matched: filtered.kept.length,
14640
+ droppedStationary: filtered.droppedStationary,
14641
+ droppedByClass: filtered.droppedByClass,
14633
14642
  tiles: loaded.length,
14634
14643
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
14635
14644
  camerasDropped: selection.camerasDropped,
@@ -14648,6 +14657,7 @@ var NcSummaryProducer = class {
14648
14657
  tiles: loaded.length,
14649
14658
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
14650
14659
  matched: filtered.kept.length,
14660
+ droppedStationary: filtered.droppedStationary,
14651
14661
  enqueued,
14652
14662
  ...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
14653
14663
  ...mosaic.ref !== null ? { mosaicUrl: mosaic.ref.url } : {},
@@ -15020,7 +15030,26 @@ var NcSummaryFiltersSchema = require_dist.object({
15020
15030
  classesExclude: require_dist.array(require_dist.string().min(1).max(40)).max(20).optional(),
15021
15031
  zones: require_dist.array(require_dist.string()).max(40).optional(),
15022
15032
  minImportance: require_dist.number().min(0).max(1).optional(),
15023
- minDwellSeconds: require_dist.number().min(0).max(3600).optional()
15033
+ minDwellSeconds: require_dist.number().min(0).max(3600).optional(),
15034
+ /**
15035
+ * Put PARKED objects in the digest too. Off unless explicitly turned on, and
15036
+ * an ABSENT field reads as off — the soft migration, no backfill.
15037
+ *
15038
+ * A digest is a record of PASSAGES: the operator's words are "the last
15039
+ * summaries still include an enormous number of stationary tracks". Since
15040
+ * D161 a track promoted to the stationary registry is persisted rather than
15041
+ * dropped (it owns media the retention sweep has to reach), so the car parked
15042
+ * in front of camera 617 now enters every window it overlaps and, ranked on
15043
+ * importance and dwell, wins tiles from the passages the digest is about. The
15044
+ * measured effect on the live hub was 1-2 tiles becoming 10.
15045
+ *
15046
+ * `.optional()` and NOT `.default(false)`: a `.default()` here would be
15047
+ * materialised by `NcSummaryRulePatchSchema` on any patch that named
15048
+ * `filters`, which is the `.partial()` trap this module's docblock describes.
15049
+ * `filterSummaryCandidates` reads `=== true`, so absent and false are the
15050
+ * same answer and neither needs a stored value.
15051
+ */
15052
+ includeStationary: require_dist.boolean().optional()
15024
15053
  });
15025
15054
  /**
15026
15055
  * How tiles are chosen when the window holds more tracks than the budget.
@@ -21819,6 +21848,14 @@ var ncActions = require_dist.defineCustomActions({
21819
21848
  tiles: require_dist.number().int().optional(),
21820
21849
  cameras: require_dist.number().int().optional(),
21821
21850
  matched: require_dist.number().int().optional(),
21851
+ /**
21852
+ * PARKED objects the window held and the rule dropped (`filters
21853
+ * .includeStationary`). Without it a Test that returns two tiles out of
21854
+ * forty tracks cannot tell the operator whether the night was quiet or
21855
+ * whether it was the car park — and that is the exact question this
21856
+ * option exists to answer.
21857
+ */
21858
+ droppedStationary: require_dist.number().int().optional(),
21822
21859
  enqueued: require_dist.number().int().optional(),
21823
21860
  mosaicBytes: require_dist.number().int().optional(),
21824
21861
  mosaicUrl: require_dist.string().optional(),
@@ -22285,6 +22322,7 @@ function makeNcActionHandlers(deps) {
22285
22322
  ...out.tiles !== void 0 ? { tiles: out.tiles } : {},
22286
22323
  ...out.cameras !== void 0 ? { cameras: out.cameras } : {},
22287
22324
  ...out.matched !== void 0 ? { matched: out.matched } : {},
22325
+ ...out.droppedStationary !== void 0 ? { droppedStationary: out.droppedStationary } : {},
22288
22326
  ...out.enqueued !== void 0 ? { enqueued: out.enqueued } : {},
22289
22327
  ...out.mosaicBytes !== void 0 ? { mosaicBytes: out.mosaicBytes } : {},
22290
22328
  ...out.mosaicUrl !== void 0 ? { mosaicUrl: out.mosaicUrl } : {},
@@ -26288,7 +26326,9 @@ var FrameProcessor = class {
26288
26326
  }) : {
26289
26327
  suppressedIndices: /* @__PURE__ */ new Set(),
26290
26328
  confirmed: [],
26291
- wokenEntryIds: []
26329
+ wokenEntryIds: [],
26330
+ armedEntryIds: [],
26331
+ disarmedEntryIds: []
26292
26332
  };
26293
26333
  const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
26294
26334
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
@@ -26498,6 +26538,8 @@ var FrameProcessor = class {
26498
26538
  rawTrackedDetections: trackedDetections,
26499
26539
  stationaryConfirmed: gate.confirmed,
26500
26540
  stationaryWoken: gate.wokenEntryIds,
26541
+ stationaryArmed: gate.armedEntryIds,
26542
+ stationaryDisarmed: gate.disarmedEntryIds,
26501
26543
  riderFolds
26502
26544
  };
26503
26545
  }
@@ -36062,20 +36104,38 @@ function centroidInside(b, p) {
36062
36104
  * box → SUPPRESS (detector box jitter — the box shrank/grew around the same
36063
36105
  * centre; still the parked object, must not spawn a duplicate track).
36064
36106
  * - `wakeAssocIou` ≤ best IoU < `suppressIou`, centroid OUTSIDE the parked box
36065
- * → WAKE (the object's box slid off its spot → it is departing). Not
36066
- * suppressed, so it spawns a fresh moving track.
36107
+ * → DEPARTING (the object's box slid off its spot).
36067
36108
  * - best IoU < `wakeAssocIou` → MISS: no detection overlaps this entry (a
36068
- * different object, or the parked object is momentarily undetected). Neither
36069
- * suppress nor wake; the TTL sweep retires it only if the absence persists.
36109
+ * different object, or the parked object is momentarily undetected). The TTL
36110
+ * sweep retires it only if the absence persists.
36111
+ *
36112
+ * ## A departure is never acted on from ONE read (D49)
36113
+ *
36114
+ * Retiring an entry DESTROYS work: the object's registry identity, and with it
36115
+ * the suppression that stops it re-spawning a track every 30 s. So a single
36116
+ * departing read only ARMS the entry — the detection is still suppressed and
36117
+ * the entry survives. The entry is retired only when a SECOND read agrees:
36118
+ * departing again, or missing entirely (the object really has left the spot).
36119
+ * A confirming read DISARMS it.
36120
+ *
36121
+ * The caller owns the armed set and passes it back in; this function stays
36122
+ * pure. Cost of the extra read: a genuinely departing object spawns its track
36123
+ * one frame later. Cost of NOT having it, measured on the live hub over 24 h:
36124
+ * 65% of all stationary entries were retired within 300 s — below the TTL, so
36125
+ * by a wake — and 38% of the tracks that promotion then re-created had never
36126
+ * moved in their entire life. One bad box per parked object was enough.
36070
36127
  *
36071
36128
  * A detection can suppress at most one spawn even if it overlaps two entries
36072
36129
  * (`suppressedIndices` is a set).
36073
36130
  */
36074
36131
  function partitionDetectionsAgainstRegistry(input) {
36075
36132
  const { entries, detections, config } = input;
36133
+ const armed = input.armedEntryIds ?? EMPTY_ARMED;
36076
36134
  const suppressed = /* @__PURE__ */ new Set();
36077
36135
  const confirmed = [];
36078
36136
  const woken = [];
36137
+ const toArm = [];
36138
+ const toDisarm = [];
36079
36139
  for (const entry of entries) {
36080
36140
  let bestIou = 0;
36081
36141
  let bestIdx = -1;
@@ -36088,23 +36148,37 @@ function partitionDetectionsAgainstRegistry(input) {
36088
36148
  bestIdx = di;
36089
36149
  }
36090
36150
  }
36091
- if (bestIdx < 0 || bestIou < config.wakeAssocIou) continue;
36151
+ if (bestIdx < 0 || bestIou < config.wakeAssocIou) {
36152
+ if (armed.has(entry.id)) woken.push(entry.id);
36153
+ continue;
36154
+ }
36092
36155
  const detCentroid = centroid(detections[bestIdx].bbox);
36093
- if (bestIou >= config.suppressIou || centroidInside(entry.bbox, detCentroid)) {
36156
+ const strong = bestIou >= config.suppressIou;
36157
+ if (strong || centroidInside(entry.bbox, detCentroid)) {
36094
36158
  suppressed.add(bestIdx);
36095
36159
  confirmed.push({
36096
36160
  entryId: entry.id,
36097
36161
  className: entry.className,
36098
- bbox: { ...entry.bbox }
36162
+ bbox: { ...entry.bbox },
36163
+ observedBbox: { ...detections[bestIdx].bbox },
36164
+ strong
36099
36165
  });
36100
- } else woken.push(entry.id);
36166
+ if (armed.has(entry.id)) toDisarm.push(entry.id);
36167
+ } else if (armed.has(entry.id)) woken.push(entry.id);
36168
+ else {
36169
+ toArm.push(entry.id);
36170
+ suppressed.add(bestIdx);
36171
+ }
36101
36172
  }
36102
36173
  return {
36103
36174
  suppressedIndices: suppressed,
36104
36175
  confirmed,
36105
- wokenEntryIds: woken
36176
+ wokenEntryIds: woken,
36177
+ armedEntryIds: toArm,
36178
+ disarmedEntryIds: toDisarm
36106
36179
  };
36107
36180
  }
36181
+ var EMPTY_ARMED = /* @__PURE__ */ new Set();
36108
36182
  /**
36109
36183
  * A track is promoted when, over the recent `windowMs`, its centroid barely
36110
36184
  * moved (both net displacement and path span below the static thresholds) AND
@@ -36677,6 +36751,21 @@ var StationaryObjectRegistry = class {
36677
36751
  * between motion sessions; that silence is not evidence the object left,
36678
36752
  * so quiet minutes must not age the entries (see {@link sweep}). */
36679
36753
  lastFrameAtByDevice = /* @__PURE__ */ new Map();
36754
+ /**
36755
+ * Entries carrying ONE unconfirmed departure read, per device (D49, D174).
36756
+ *
36757
+ * Retiring an entry destroys work — the object's registry identity and the
36758
+ * suppression that keeps it from re-spawning a track every promotion window.
36759
+ * A single frame whose box slid off the parked spot is a fallible read (a
36760
+ * partial occlusion, a shadow, one bad box), so it only ARMS the entry here.
36761
+ * The retirement happens on the next frame that AGREES; a confirming frame
36762
+ * clears the strike.
36763
+ */
36764
+ departureStrikes = /* @__PURE__ */ new Map();
36765
+ /** Detections absorbed by an existing entry since the last sweep, per device.
36766
+ * Rolled up into ONE log line per sweep — this fires on every frame of every
36767
+ * parked object, so it can never be a line of its own. */
36768
+ absorbedSinceSweep = /* @__PURE__ */ new Map();
36680
36769
  constructor(deps) {
36681
36770
  this.logger = deps.logger;
36682
36771
  this.ledger = new DurableLedger({
@@ -36749,12 +36838,15 @@ var StationaryObjectRegistry = class {
36749
36838
  if (entries.length === 0) return {
36750
36839
  suppressedIndices: /* @__PURE__ */ new Set(),
36751
36840
  confirmed: [],
36752
- wokenEntryIds: []
36841
+ wokenEntryIds: [],
36842
+ armedEntryIds: [],
36843
+ disarmedEntryIds: []
36753
36844
  };
36754
36845
  return partitionDetectionsAgainstRegistry({
36755
36846
  entries,
36756
36847
  detections: input.detections,
36757
- config: config ?? this.matchConfig
36848
+ config: config ?? this.matchConfig,
36849
+ ...this.departureStrikes.has(input.deviceId) ? { armedEntryIds: this.departureStrikes.get(input.deviceId) } : {}
36758
36850
  });
36759
36851
  }
36760
36852
  /** Fold a frame's gate result back into state: advance confirmed entries'
@@ -36771,15 +36863,41 @@ var StationaryObjectRegistry = class {
36771
36863
  if (!e) continue;
36772
36864
  this.ledger.stage({
36773
36865
  ...e,
36774
- lastConfirmedAt: input.timestamp
36866
+ lastConfirmedAt: input.timestamp,
36867
+ ...c.strong ? { bbox: { ...c.observedBbox } } : {}
36775
36868
  });
36776
36869
  this.dirty.add(c.entryId);
36777
36870
  }
36871
+ this.absorbedSinceSweep.set(input.deviceId, (this.absorbedSinceSweep.get(input.deviceId) ?? 0) + input.confirmed.length);
36872
+ if (input.armedEntryIds && input.armedEntryIds.length > 0) {
36873
+ let armed = this.departureStrikes.get(input.deviceId);
36874
+ if (!armed) {
36875
+ armed = /* @__PURE__ */ new Set();
36876
+ this.departureStrikes.set(input.deviceId, armed);
36877
+ }
36878
+ for (const id of input.armedEntryIds) if (ids.has(id)) armed.add(id);
36879
+ }
36880
+ const armed = this.departureStrikes.get(input.deviceId);
36881
+ if (armed && input.disarmedEntryIds) {
36882
+ for (const id of input.disarmedEntryIds) armed.delete(id);
36883
+ if (armed.size === 0) this.departureStrikes.delete(input.deviceId);
36884
+ }
36778
36885
  for (const id of input.wokenEntryIds) {
36779
36886
  if (!ids.has(id)) continue;
36780
36887
  const e = this.ledger.get(id);
36781
36888
  if (!e) continue;
36782
36889
  this.dropEntry(input.deviceId, id);
36890
+ this.logger.info("stationary entry retired", {
36891
+ tags: { deviceId: input.deviceId },
36892
+ meta: {
36893
+ deviceId: input.deviceId,
36894
+ entryId: id,
36895
+ className: e.className,
36896
+ reason: "departed",
36897
+ parkedForMs: input.timestamp - e.becameStationaryAt,
36898
+ ...e.sourceTrackId !== void 0 ? { sourceTrackId: e.sourceTrackId } : {}
36899
+ }
36900
+ });
36783
36901
  this.onChange?.({
36784
36902
  phase: "departed",
36785
36903
  entry: e,
@@ -36826,6 +36944,18 @@ var StationaryObjectRegistry = class {
36826
36944
  if (observedAt - e.lastConfirmedAt > ttl) {
36827
36945
  this.dropEntry(deviceId, id);
36828
36946
  retired.push(e);
36947
+ this.logger.info("stationary entry retired", {
36948
+ tags: { deviceId },
36949
+ meta: {
36950
+ deviceId,
36951
+ entryId: id,
36952
+ className: e.className,
36953
+ reason: "ttl",
36954
+ ttlMs: ttl,
36955
+ unconfirmedForMs: observedAt - e.lastConfirmedAt,
36956
+ parkedForMs: observedAt - e.becameStationaryAt
36957
+ }
36958
+ });
36829
36959
  this.onChange?.({
36830
36960
  phase: "departed",
36831
36961
  entry: e,
@@ -36835,6 +36965,18 @@ var StationaryObjectRegistry = class {
36835
36965
  }
36836
36966
  if (ids.size === 0) this.idsByDevice.delete(deviceId);
36837
36967
  }
36968
+ for (const [deviceId, absorbed] of this.absorbedSinceSweep) {
36969
+ if (absorbed === 0) continue;
36970
+ this.logger.info("stationary registry absorbed detections", {
36971
+ tags: { deviceId },
36972
+ meta: {
36973
+ deviceId,
36974
+ absorbed,
36975
+ entries: this.count(deviceId)
36976
+ }
36977
+ });
36978
+ }
36979
+ this.absorbedSinceSweep.clear();
36838
36980
  for (const id of [...this.dirty]) {
36839
36981
  this.dirty.delete(id);
36840
36982
  const entry = this.ledger.get(id);
@@ -36847,6 +36989,8 @@ var StationaryObjectRegistry = class {
36847
36989
  * Used on device unbind; a rebind reloads from the store. */
36848
36990
  forgetDevice(deviceId) {
36849
36991
  this.lastFrameAtByDevice.delete(deviceId);
36992
+ this.departureStrikes.delete(deviceId);
36993
+ this.absorbedSinceSweep.delete(deviceId);
36850
36994
  const ids = this.idsByDevice.get(deviceId);
36851
36995
  if (!ids) return;
36852
36996
  for (const id of ids) {
@@ -36858,6 +37002,8 @@ var StationaryObjectRegistry = class {
36858
37002
  /** Delete every persisted + in-memory entry for a device (operator wipe). */
36859
37003
  async clearDevice(deviceId) {
36860
37004
  this.lastFrameAtByDevice.delete(deviceId);
37005
+ this.departureStrikes.delete(deviceId);
37006
+ this.absorbedSinceSweep.delete(deviceId);
36861
37007
  const ids = this.idsByDevice.get(deviceId);
36862
37008
  if (ids) {
36863
37009
  for (const id of [...ids]) {
@@ -36875,10 +37021,17 @@ var StationaryObjectRegistry = class {
36875
37021
  }
36876
37022
  set.add(id);
36877
37023
  }
36878
- /** Retire one entry: index, staged advance and durable row all go. */
37024
+ /** Retire one entry: index, staged advance, pending strike and durable row
37025
+ * all go. A strike that outlived its entry would arm the NEXT entry to reuse
37026
+ * the id — the registry mints UUIDs, but the invariant is cheap to keep. */
36879
37027
  dropEntry(deviceId, id) {
36880
37028
  this.idsByDevice.get(deviceId)?.delete(id);
36881
37029
  this.dirty.delete(id);
37030
+ const armed = this.departureStrikes.get(deviceId);
37031
+ if (armed) {
37032
+ armed.delete(id);
37033
+ if (armed.size === 0) this.departureStrikes.delete(deviceId);
37034
+ }
36882
37035
  this.ledger.forget(id);
36883
37036
  }
36884
37037
  };
@@ -40922,6 +41075,26 @@ function isAfterCursor(row, cursor) {
40922
41075
  }
40923
41076
  //#endregion
40924
41077
  //#region src/pipeline-analytics/store/track-store.ts
41078
+ /**
41079
+ * WHY a track's row was written EARLY — i.e. by {@link TrackStore.closeActive}
41080
+ * rather than by the ordinary TTL expiry.
41081
+ *
41082
+ * `'stationary-promotion'` is the only member and means: the tracker decided
41083
+ * this subject had PARKED, handed it to the stationary registry, and the row
41084
+ * was written so its media, events and vector keep an owner (D161). It is not a
41085
+ * quality signal and it never hides the track — a parked car is still a track,
41086
+ * still on the events page, still swept by the same retention. It exists so a
41087
+ * consumer that wants PASSAGES can say so exactly, instead of re-deriving
41088
+ * "did it move" from a trajectory downstream and getting it wrong on the
41089
+ * subject that stood still for one honest reason.
41090
+ *
41091
+ * An ABSENT value means "expired normally, or written before this marker
41092
+ * existed". Never read an absence as a positive — same rule as `hasFace`.
41093
+ */
41094
+ var TRACK_CLOSE_REASONS = ["stationary-promotion"];
41095
+ function parseCloseReason(value) {
41096
+ return TRACK_CLOSE_REASONS.find((reason) => reason === value);
41097
+ }
40925
41098
  var DEFAULT_CONFIG = {
40926
41099
  ttlMs: 3e4,
40927
41100
  maxPositionHistory: 300
@@ -41144,6 +41317,10 @@ var TRACKS_COLUMNS = [
41144
41317
  {
41145
41318
  name: "frameHeight",
41146
41319
  type: "INTEGER"
41320
+ },
41321
+ {
41322
+ name: "closedReason",
41323
+ type: "TEXT"
41147
41324
  }
41148
41325
  ];
41149
41326
  var TRACKS_INDEXES = [{
@@ -41203,7 +41380,8 @@ function cloneTrack(t) {
41203
41380
  } : {},
41204
41381
  ...t.debug !== void 0 ? { debug: t.debug } : {},
41205
41382
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
41206
- ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {}
41383
+ ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
41384
+ ...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
41207
41385
  };
41208
41386
  }
41209
41387
  var TrackStore = class {
@@ -41772,13 +41950,22 @@ var TrackStore = class {
41772
41950
  * nothing has been written under that id yet. It is never correct for a track
41773
41951
  * that ran.
41774
41952
  *
41953
+ * **The row SAYS why it was closed early** (`reason` → the `closedReason`
41954
+ * column). Persisting a promoted track put its footprint back under the
41955
+ * retention sweep, and it also put the parked car back in front of every
41956
+ * consumer that reads the window's tracks: the operator's nightly digest went
41957
+ * from 1-2 tiles to 10, most of them the same stationary car. The marker is
41958
+ * written HERE, at the one place that knows, so a consumer can ask for
41959
+ * passages exactly instead of guessing at movement from a stored trajectory.
41960
+ *
41775
41961
  * Returns whether an active entry was found. THROWS if the persist fails: the
41776
41962
  * caller must log it, because a swallowed failure here IS the leak.
41777
41963
  */
41778
- async closeActive(trackId) {
41964
+ async closeActive(trackId, reason) {
41779
41965
  const t = this.active.get(trackId);
41780
41966
  if (t === void 0) return false;
41781
41967
  t.active = false;
41968
+ t.closedReason = reason;
41782
41969
  const record = cloneTrack(t);
41783
41970
  await this.persistCompleted(record);
41784
41971
  this.active.delete(trackId);
@@ -42360,6 +42547,7 @@ var TrackStore = class {
42360
42547
  ...t.debug !== void 0 ? { debug: t.debug } : {},
42361
42548
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
42362
42549
  ...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
42550
+ ...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
42363
42551
  ...envelope !== null && dims !== void 0 ? {
42364
42552
  envMinX: envelope.minX,
42365
42553
  envMinY: envelope.minY,
@@ -42397,6 +42585,7 @@ var TrackStore = class {
42397
42585
  const hasFace = data["hasFace"];
42398
42586
  /** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`. */
42399
42587
  const hasRider = data["hasRider"];
42588
+ const closedReason = parseCloseReason(data["closedReason"]);
42400
42589
  const envMinX = data["envMinX"];
42401
42590
  const envMinY = data["envMinY"];
42402
42591
  const envMaxX = data["envMaxX"];
@@ -42434,6 +42623,7 @@ var TrackStore = class {
42434
42623
  ...typeof debug === "boolean" ? { debug } : {},
42435
42624
  ...typeof hasFace === "boolean" ? { hasFace } : {},
42436
42625
  ...typeof hasRider === "boolean" ? { hasRider } : {},
42626
+ ...closedReason !== void 0 ? { closedReason } : {},
42437
42627
  ...envelope !== null ? { envelope } : {}
42438
42628
  };
42439
42629
  }
@@ -50621,7 +50811,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
50621
50811
  lastSeen: t.lastSeen,
50622
50812
  ...t.importance !== void 0 ? { importance: t.importance } : {},
50623
50813
  zones: [...t.zonesVisited],
50624
- ...t.classes !== void 0 ? { classes: [...t.classes] } : {}
50814
+ ...t.classes !== void 0 ? { classes: [...t.classes] } : {},
50815
+ ...t.closedReason === "stationary-promotion" ? { stationary: true } : {}
50625
50816
  }));
50626
50817
  },
50627
50818
  listMediaInfo: async (trackId) => {
@@ -51725,10 +51916,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
51725
51916
  this.trackStore?.markRiderFolded(fold.trackId);
51726
51917
  }
51727
51918
  if (source === "pipeline") this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
51728
- if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0)) this.stationaryRegistry.applyFrameOutcome({
51919
+ if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0 || result.stationaryArmed.length > 0 || result.stationaryDisarmed.length > 0)) this.stationaryRegistry.applyFrameOutcome({
51729
51920
  deviceId,
51730
51921
  confirmed: result.stationaryConfirmed,
51731
51922
  wokenEntryIds: result.stationaryWoken,
51923
+ armedEntryIds: result.stationaryArmed,
51924
+ disarmedEntryIds: result.stationaryDisarmed,
51732
51925
  timestamp: result.timestamp
51733
51926
  });
51734
51927
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
@@ -53785,7 +53978,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
53785
53978
  };
53786
53979
  if (plausible) this.stationaryRegistry?.promote(entry);
53787
53980
  processor.dropTrack(track.trackId);
53788
- this.trackStore?.closeActive(track.trackId).catch((err) => {
53981
+ this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
53789
53982
  this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
53790
53983
  tags: { deviceId },
53791
53984
  meta: {
@@ -53888,7 +54081,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
53888
54081
  if (!s.enabled) return {
53889
54082
  suppressedIndices: /* @__PURE__ */ new Set(),
53890
54083
  confirmed: [],
53891
- wokenEntryIds: []
54084
+ wokenEntryIds: [],
54085
+ armedEntryIds: [],
54086
+ disarmedEntryIds: []
53892
54087
  };
53893
54088
  const matchConfig = {
53894
54089
  suppressIou: s.suppressIou,