@camstack/addon-post-analysis 1.2.209 → 1.2.210

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-BttKNVmP.js");
5
+ const require_dist = require("../dist-FHSaZkf_.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");
@@ -3452,7 +3452,7 @@ var SceneConfirmGate = class {
3452
3452
  };
3453
3453
  }
3454
3454
  };
3455
- function clamp$1(v, lo, hi) {
3455
+ function clamp$2(v, lo, hi) {
3456
3456
  return Math.max(lo, Math.min(v, hi));
3457
3457
  }
3458
3458
  /**
@@ -3467,10 +3467,10 @@ async function cropSnapshotRoi(encoded, roi) {
3467
3467
  const frameW = meta.width ?? 0;
3468
3468
  const frameH = meta.height ?? 0;
3469
3469
  if (frameW <= 0 || frameH <= 0) throw new Error("snapshot has no decodable dimensions");
3470
- const left = clamp$1(Math.round(roi.x * frameW), 0, frameW - 1);
3471
- const top = clamp$1(Math.round(roi.y * frameH), 0, frameH - 1);
3472
- const width = clamp$1(Math.round(roi.width * frameW), 1, frameW - left);
3473
- const height = clamp$1(Math.round(roi.height * frameH), 1, frameH - top);
3470
+ const left = clamp$2(Math.round(roi.x * frameW), 0, frameW - 1);
3471
+ const top = clamp$2(Math.round(roi.y * frameH), 0, frameH - 1);
3472
+ const width = clamp$2(Math.round(roi.width * frameW), 1, frameW - left);
3473
+ const height = clamp$2(Math.round(roi.height * frameH), 1, frameH - top);
3474
3474
  return {
3475
3475
  crop: await (0, sharp.default)(encoded).extract({
3476
3476
  left,
@@ -28633,6 +28633,37 @@ var MediaStore = class {
28633
28633
  return newKey;
28634
28634
  }
28635
28635
  /**
28636
+ * Exchange the bytes+instant held in the `firstFrame` and `lastFrame` slots
28637
+ * (close-time ordering repair, see `pipeline/first-last-frame-order.ts`).
28638
+ *
28639
+ * Both are single-instance kinds, so each `putReplacing` overwrites the slot
28640
+ * it names — the two rows end up holding each other's blob and timestamp.
28641
+ * Nothing is deleted and nothing is re-encoded: both blobs are already the
28642
+ * track's own stored pixels and the only thing wrong with them is which slot
28643
+ * they sit in. The `lastFrame` write lands first so the "ultimo" tile is
28644
+ * never momentarily the older view a viewer just refuted.
28645
+ */
28646
+ async swapFirstAndLastFrame(input) {
28647
+ await this.gated(async () => {
28648
+ await this.putReplacingUnchecked({
28649
+ deviceId: input.deviceId,
28650
+ ownerKind: "track",
28651
+ ownerId: input.trackId,
28652
+ kind: "lastFrame",
28653
+ timestamp: input.firstFrame.timestamp,
28654
+ data: Buffer.from(input.firstFrame.base64, "base64")
28655
+ });
28656
+ await this.putReplacingUnchecked({
28657
+ deviceId: input.deviceId,
28658
+ ownerKind: "track",
28659
+ ownerId: input.trackId,
28660
+ kind: "firstFrame",
28661
+ timestamp: input.lastFrame.timestamp,
28662
+ data: Buffer.from(input.lastFrame.base64, "base64")
28663
+ });
28664
+ });
28665
+ }
28666
+ /**
28636
28667
  * Fetch one media entry by its key (id). Returns null if the key is not
28637
28668
  * found in the index or if the blob is missing from storage.
28638
28669
  */
@@ -30505,6 +30536,17 @@ function trackDurationMs(facts) {
30505
30536
  function hasKind(media, kind) {
30506
30537
  return media.some((m) => m.kind === kind);
30507
30538
  }
30539
+ /** Newest instant stamped on a row of `kind`, or `null` when the track owns
30540
+ * none. Single-instance kinds hold at most one row; the max guards a legacy
30541
+ * duplicate, mirroring `decideFirstLastFrameOrder`. */
30542
+ function newestOfKind(media, kind) {
30543
+ let newest = null;
30544
+ for (const m of media) {
30545
+ if (m.kind !== kind) continue;
30546
+ if (newest === null || m.timestamp > newest) newest = m.timestamp;
30547
+ }
30548
+ return newest;
30549
+ }
30508
30550
  function latestMediaAt(media) {
30509
30551
  let latest = null;
30510
30552
  for (const m of media) if (latest === null || m.timestamp > latest) latest = m.timestamp;
@@ -30552,6 +30594,12 @@ function analyseDebugTrack(inventory) {
30552
30594
  const lag = facts.lastSeen - lastFrameAt;
30553
30595
  if (lag > 2e3) out.push(finding("LASTFRAME_STALE", "warn", `lastFrame is ${String(lag)} ms older than the track's end.`));
30554
30596
  }
30597
+ const firstFrameAt = newestOfKind(media, "firstFrame");
30598
+ if (firstFrameAt !== null) {
30599
+ const lastFrameAt = newestOfKind(media, "lastFrame");
30600
+ if (lastFrameAt !== null && firstFrameAt > lastFrameAt) out.push(finding("FIRSTFRAME_AFTER_LASTFRAME", "error", `firstFrame is ${String(firstFrameAt - lastFrameAt)} ms NEWER than lastFrame — the "primo" tile post-dates the "ultimo" one. The first view is captured at the CONFIRMED birth, which the confirmation gate can defer by up to its maxDeferralMs, while keyFrame/thumbnail/lastFrame are stamped at the earlier frame they were cut from.`));
30601
+ if (closed && firstFrameAt > facts.lastSeen) out.push(finding("FIRSTFRAME_AFTER_TRACK_END", "error", `firstFrame was captured ${String(firstFrameAt - facts.lastSeen)} ms AFTER the track's last observation — the tile shows a scene the subject had already left (a birth confirmed on a coasted frame).`));
30602
+ }
30555
30603
  if (closed && durationMs >= 1e4 && media.length > 0) {
30556
30604
  const latest = latestMediaAt(media);
30557
30605
  if (latest !== null) {
@@ -34151,6 +34199,29 @@ function firstFrameRetryBbox(current, birth) {
34151
34199
  return { ...current };
34152
34200
  }
34153
34201
  //#endregion
34202
+ //#region src/pipeline-analytics/pipeline/birth-first-frame-gate.ts
34203
+ /**
34204
+ * Decide whether the confirmed-birth frame may be dispatched as this track's
34205
+ * `firstFrame` capture. Pure; the caller logs the refusal and keeps the
34206
+ * `firstFramePending` mark so a later frame retries.
34207
+ *
34208
+ * `coasted-frame` is reported ahead of `no-frame-handle` when both hold: a
34209
+ * handle-less coasted frame would have been refused for BEING coasted whatever
34210
+ * the decode plane did, and naming the decode is what would send the next
34211
+ * investigation at the wrong subsystem.
34212
+ */
34213
+ function decideBirthFirstFrameCapture(input) {
34214
+ if (input.matchedThisFrame === false) return {
34215
+ capture: false,
34216
+ refusedBecause: "coasted-frame"
34217
+ };
34218
+ if (!input.hasFrameHandle) return {
34219
+ capture: false,
34220
+ refusedBecause: "no-frame-handle"
34221
+ };
34222
+ return { capture: true };
34223
+ }
34224
+ //#endregion
34154
34225
  //#region src/pipeline-analytics/pipeline/events/event-filter.ts
34155
34226
  var DEFAULT_EVENT_EMITTER_CONFIG = {
34156
34227
  minTrackAge: 3,
@@ -34759,7 +34830,7 @@ var DEFAULT_TRACKER_CONFIG = {
34759
34830
  var PERSON_CLASS = "person";
34760
34831
  var ANIMAL_CLASS = "animal";
34761
34832
  var MAX_PATH_LENGTH = 300;
34762
- function clamp(value, min, max) {
34833
+ function clamp$1(value, min, max) {
34763
34834
  return Math.max(min, Math.min(max, value));
34764
34835
  }
34765
34836
  function iou$3(a, b) {
@@ -35199,8 +35270,8 @@ var SortTracker = class SortTracker {
35199
35270
  const perFrameX = t.velocity.dx * t.emitIntervalMs;
35200
35271
  const perFrameY = t.velocity.dy * t.emitIntervalMs;
35201
35272
  return {
35202
- x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
35203
- y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
35273
+ x: t.bbox.x + clamp$1(perFrameX * f, -t.bbox.w, t.bbox.w),
35274
+ y: t.bbox.y + clamp$1(perFrameY * f, -t.bbox.h, t.bbox.h),
35204
35275
  w: t.bbox.w,
35205
35276
  h: t.bbox.h
35206
35277
  };
@@ -35209,8 +35280,8 @@ var SortTracker = class SortTracker {
35209
35280
  const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
35210
35281
  const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
35211
35282
  return {
35212
- x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
35213
- y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
35283
+ x: t.bbox.x + clamp$1(t.velocity.dx * elapsedMs, -allowX, allowX),
35284
+ y: t.bbox.y + clamp$1(t.velocity.dy * elapsedMs, -allowY, allowY),
35214
35285
  w: t.bbox.w,
35215
35286
  h: t.bbox.h
35216
35287
  };
@@ -37287,6 +37358,44 @@ function decideLastFramePromotion(media) {
37287
37358
  };
37288
37359
  }
37289
37360
  //#endregion
37361
+ //#region src/pipeline-analytics/pipeline/first-last-frame-order.ts
37362
+ /**
37363
+ * Decide whether a closing track's `firstFrame` and `lastFrame` slots hold each
37364
+ * other's bytes. Pure — see the module header for the contract.
37365
+ *
37366
+ * Swap when BOTH slots are filled and the `firstFrame` is STRICTLY newer than
37367
+ * the `lastFrame`. Equal timestamps are not an inversion.
37368
+ */
37369
+ function decideFirstLastFrameOrder(media) {
37370
+ let firstFrame;
37371
+ let lastFrame;
37372
+ for (const m of media) if (m.kind === "firstFrame") {
37373
+ if (firstFrame === void 0 || m.timestamp > firstFrame.timestamp) firstFrame = m;
37374
+ } else if (m.kind === "lastFrame") {
37375
+ if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
37376
+ }
37377
+ if (firstFrame === void 0) return {
37378
+ swap: false,
37379
+ skippedBecause: "no-first-frame"
37380
+ };
37381
+ if (lastFrame === void 0) return {
37382
+ swap: false,
37383
+ skippedBecause: "no-last-frame"
37384
+ };
37385
+ if (firstFrame.timestamp <= lastFrame.timestamp) return {
37386
+ swap: false,
37387
+ skippedBecause: "ordered"
37388
+ };
37389
+ return {
37390
+ swap: true,
37391
+ firstFrameKey: firstFrame.key,
37392
+ lastFrameKey: lastFrame.key,
37393
+ firstFrameTimestamp: firstFrame.timestamp,
37394
+ lastFrameTimestamp: lastFrame.timestamp,
37395
+ inversionMs: firstFrame.timestamp - lastFrame.timestamp
37396
+ };
37397
+ }
37398
+ //#endregion
37290
37399
  //#region src/pipeline-analytics/pipeline/static-track-gate.ts
37291
37400
  /**
37292
37401
  * Net displacement + path span for a track's centroid path, normalized to
@@ -37429,6 +37538,7 @@ var TrackCloser = class {
37429
37538
  tile = "raster";
37430
37539
  }
37431
37540
  await this.maybePromoteLastFrame(t, ownedMedia);
37541
+ await this.repairFirstLastFrameOrder(t);
37432
37542
  const hasRasterFallback = closure?.rasterFallback !== void 0;
37433
37543
  let deriveMiss;
37434
37544
  if (!thumbnailLanded) {
@@ -37772,6 +37882,56 @@ var TrackCloser = class {
37772
37882
  }
37773
37883
  }
37774
37884
  /**
37885
+ * A track's "primo" tile is never newer than its "ultimo" tile
37886
+ * (`first-last-frame-order.ts`). Filed by hand three times on device 3829,
37887
+ * 2026-09-08 — `d035f251`, `448678bb`, `7628b19d`.
37888
+ *
37889
+ * The media list is RE-READ rather than reusing the caller's `ownedMedia`:
37890
+ * `maybePromoteLastFrame` has just run and is precisely what may have put the
37891
+ * older bytes in the `lastFrame` slot, so the caller's copy predates the pair
37892
+ * being ordered here. Best-effort — a failure never blocks the close.
37893
+ */
37894
+ async repairFirstLastFrameOrder(t) {
37895
+ const store = this.deps.mediaStore();
37896
+ if (!store) return;
37897
+ const media = await store.listByOwner("track", t.trackId);
37898
+ const decision = decideFirstLastFrameOrder(media);
37899
+ if (!decision.swap) return;
37900
+ const firstFrame = media.find((m) => m.key === decision.firstFrameKey);
37901
+ const lastFrame = media.find((m) => m.key === decision.lastFrameKey);
37902
+ if (!firstFrame || !lastFrame) return;
37903
+ try {
37904
+ await store.swapFirstAndLastFrame({
37905
+ deviceId: t.deviceId,
37906
+ trackId: t.trackId,
37907
+ firstFrame,
37908
+ lastFrame
37909
+ });
37910
+ this.deps.logger.warn("firstFrame/lastFrame were out of order — slots swapped", {
37911
+ tags: { deviceId: t.deviceId },
37912
+ meta: {
37913
+ trackId: t.trackId,
37914
+ className: t.className,
37915
+ inversionMs: decision.inversionMs,
37916
+ firstFrameAt: decision.firstFrameTimestamp,
37917
+ lastFrameAt: decision.lastFrameTimestamp,
37918
+ /** How far the late capture ran past the track itself. Positive means
37919
+ * it was cut from a frame the subject was no longer observed in. */
37920
+ pastTrackEndMs: decision.firstFrameTimestamp - t.lastSeen,
37921
+ durationMs: t.lastSeen - t.firstSeen
37922
+ }
37923
+ });
37924
+ } catch (err) {
37925
+ this.deps.logger.warn("firstFrame/lastFrame order repair failed", {
37926
+ tags: { deviceId: t.deviceId },
37927
+ meta: {
37928
+ trackId: t.trackId,
37929
+ error: String(err)
37930
+ }
37931
+ });
37932
+ }
37933
+ }
37934
+ /**
37775
37935
  * Key-event importance: score the just-expired track and persist. Peak
37776
37936
  * confidence comes from the shared best-detection tracker; peak bbox area
37777
37937
  * + the winning object-event id from one indexed queryObject(trackId) —
@@ -41587,7 +41747,10 @@ var TrackStore = class {
41587
41747
  retrainStatus: active.retrainStatus ?? "none",
41588
41748
  debug: active.debug === true,
41589
41749
  debugNote: active.debugNote ?? "",
41590
- favourited: active.favourited === true
41750
+ favourited: active.favourited === true,
41751
+ startedAt: active.firstSeen,
41752
+ className: active.className,
41753
+ ...displayLabel(active) !== void 0 ? { label: displayLabel(active) } : {}
41591
41754
  };
41592
41755
  const persisted = await this.getPersistedByTrackId(trackId);
41593
41756
  if (!persisted) return null;
@@ -41595,7 +41758,10 @@ var TrackStore = class {
41595
41758
  retrainStatus: persisted.retrainStatus ?? "none",
41596
41759
  debug: persisted.debug === true,
41597
41760
  debugNote: persisted.debugNote ?? "",
41598
- favourited: persisted.favourited === true
41761
+ favourited: persisted.favourited === true,
41762
+ startedAt: persisted.firstSeen,
41763
+ className: persisted.className,
41764
+ ...displayLabel(persisted) !== void 0 ? { label: displayLabel(persisted) } : {}
41599
41765
  };
41600
41766
  }
41601
41767
  /**
@@ -55023,7 +55189,7 @@ var PackageDropDetector = class {
55023
55189
  async onStationaryChange(change) {
55024
55190
  try {
55025
55191
  if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
55026
- else await this.onDeparted(change.entry, change.timestamp);
55192
+ else await this.onDeparted(change);
55027
55193
  } catch (err) {
55028
55194
  this.deps.onError?.("onStationaryChange", err);
55029
55195
  this.deps.logger.warn("package-drop detector failed on change", {
@@ -55201,7 +55367,9 @@ var PackageDropDetector = class {
55201
55367
  }
55202
55368
  });
55203
55369
  }
55204
- async onDeparted(entry, timestamp) {
55370
+ async onDeparted(change) {
55371
+ const entry = change.entry;
55372
+ const timestamp = change.timestamp;
55205
55373
  if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) {
55206
55374
  this.deps.logger.debug("package pick-up skipped — packageDropPickupEnabled is false", {
55207
55375
  tags: { deviceId: entry.deviceId },
@@ -55236,13 +55404,25 @@ var PackageDropDetector = class {
55236
55404
  });
55237
55405
  return;
55238
55406
  }
55407
+ const collector = change.collector;
55408
+ const packageTrackId = entry.sourceTrackId ?? entry.id;
55409
+ const mediaTrackId = collector?.trackId ?? packageTrackId;
55410
+ if (collector === void 0) this.deps.logger.info("package pick-up has no collector track — the notification carries the parcel frame", {
55411
+ tags: { deviceId: entry.deviceId },
55412
+ meta: {
55413
+ entryId: entry.id,
55414
+ eventId: pickedUpId,
55415
+ packageTrackId,
55416
+ pickupAt: entry.lastConfirmedAt
55417
+ }
55418
+ });
55239
55419
  const ev = {
55240
55420
  id: pickedUpId,
55241
55421
  kind: "object",
55242
55422
  deviceId: entry.deviceId,
55243
55423
  timestamp,
55244
55424
  source: "pipeline",
55245
- trackId: entry.sourceTrackId ?? entry.id,
55425
+ trackId: mediaTrackId,
55246
55426
  className: PACKAGE_EVENT_CLASS,
55247
55427
  ...entry.label !== void 0 ? { label: entry.label } : {},
55248
55428
  confidence: PACKAGE_IMPORTANCE,
@@ -55256,6 +55436,7 @@ var PackageDropDetector = class {
55256
55436
  state: PICKED_UP_STATE,
55257
55437
  frameWidth: entry.frameWidth,
55258
55438
  frameHeight: entry.frameHeight,
55439
+ ...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
55259
55440
  importance: PACKAGE_IMPORTANCE
55260
55441
  };
55261
55442
  await this.deps.events.insertObject(ev);
@@ -55265,6 +55446,7 @@ var PackageDropDetector = class {
55265
55446
  entryId: entry.id,
55266
55447
  deliveredEventId: deliveredId,
55267
55448
  className: entry.className,
55449
+ ...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
55268
55450
  timestamp
55269
55451
  });
55270
55452
  this.deps.logger.info("package picked up", {
@@ -55272,11 +55454,139 @@ var PackageDropDetector = class {
55272
55454
  meta: {
55273
55455
  entryId: entry.id,
55274
55456
  eventId: pickedUpId,
55275
- deliveredEventId: deliveredId
55457
+ deliveredEventId: deliveredId,
55458
+ mediaTrackId,
55459
+ ...collector !== void 0 ? {
55460
+ collectorTrackId: collector.trackId,
55461
+ collectorClassName: collector.className,
55462
+ collectorSkewMs: collector.skewMs
55463
+ } : { collectorTrackId: null }
55276
55464
  }
55277
55465
  });
55278
55466
  }
55279
55467
  };
55468
+ /**
55469
+ * How long a presence survives in the mirror, in FRAME time.
55470
+ *
55471
+ * It must outlive the whole detection latency: a pick-up at T is concluded no
55472
+ * earlier than T + `PACKAGE_STILLNESS_TTL_MS` (5 min of observed time), and the
55473
+ * pick is made at conclusion time. 10 minutes is double that, so a collector
55474
+ * noted at the true instant is still there to be named — and a mirror that
55475
+ * expires the answer before the question is asked would be the same silent
55476
+ * hole in a different place.
55477
+ */
55478
+ var PACKAGE_COLLECTOR_RETENTION_MS = 10 * 6e4;
55479
+ /** The pick-up instant a stationary entry implies. One derivation, named. */
55480
+ function pickupInstantOf(entry) {
55481
+ return entry.lastConfirmedAt;
55482
+ }
55483
+ var PackageCollectorMirror = class {
55484
+ /** Per device, per track. Every write replaces the record (immutable). */
55485
+ byDevice = /* @__PURE__ */ new Map();
55486
+ /**
55487
+ * Record one sighting. Extends the track's presence span and refreshes its
55488
+ * key-frame handle (a re-shot key frame is a better one, and the newest is
55489
+ * also the nearest to the pick-up — the same trade as above).
55490
+ */
55491
+ note(obs) {
55492
+ const current = this.byDevice.get(obs.deviceId);
55493
+ const next = /* @__PURE__ */ new Map();
55494
+ const horizon = obs.at - PACKAGE_COLLECTOR_RETENTION_MS;
55495
+ if (current !== void 0) for (const [id, p] of current) {
55496
+ if (p.lastAt < horizon) continue;
55497
+ next.set(id, p);
55498
+ }
55499
+ const prev = next.get(obs.trackId);
55500
+ next.set(obs.trackId, {
55501
+ trackId: obs.trackId,
55502
+ className: obs.className,
55503
+ firstAt: prev === void 0 ? obs.at : Math.min(prev.firstAt, obs.at),
55504
+ lastAt: prev === void 0 ? obs.at : Math.max(prev.lastAt, obs.at),
55505
+ ...obs.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: obs.keyFrameMediaKey } : prev?.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: prev.keyFrameMediaKey } : {}
55506
+ });
55507
+ this.byDevice.set(obs.deviceId, evictOldest(next));
55508
+ }
55509
+ /**
55510
+ * The track nearest the pick-up instant, or `null` when nobody qualifies.
55511
+ *
55512
+ * `null` is a real answer and not an error: a parcel can stop being seen
55513
+ * because the light changed or the detector lost it, and naming a person who
55514
+ * was not there would be worse than naming nobody. The caller degrades to the
55515
+ * parcel's own owner and logs it.
55516
+ */
55517
+ pickCollector(input) {
55518
+ const presences = this.byDevice.get(input.deviceId);
55519
+ if (presences === void 0 || presences.size === 0) return null;
55520
+ const maxSkewMs = input.maxSkewMs ?? 3e4;
55521
+ let best = null;
55522
+ let bestLastAt = Number.NEGATIVE_INFINITY;
55523
+ for (const p of presences.values()) {
55524
+ const observedAt = clamp(input.pickupAt, p.firstAt, p.lastAt);
55525
+ const skewMs = Math.abs(observedAt - input.pickupAt);
55526
+ if (skewMs > maxSkewMs) continue;
55527
+ if (best !== null && (skewMs > best.skewMs || skewMs === best.skewMs && p.lastAt <= bestLastAt)) continue;
55528
+ best = {
55529
+ trackId: p.trackId,
55530
+ className: p.className,
55531
+ observedAt,
55532
+ skewMs,
55533
+ ...p.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: p.keyFrameMediaKey } : {}
55534
+ };
55535
+ bestLastAt = p.lastAt;
55536
+ }
55537
+ return best;
55538
+ }
55539
+ /**
55540
+ * Fold one pipeline frame in. Returns how many presences it touched, so the
55541
+ * caller can say "nobody was ever in the drop-off zone" with a number.
55542
+ *
55543
+ * This is the ONLY place the frame-shaped input is interpreted, so the
55544
+ * production path and the test path read the same rules: a non-package class,
55545
+ * inside a configured drop-off zone, on this frame.
55546
+ */
55547
+ noteFrame(input) {
55548
+ if (input.packageZoneIds.size === 0) return 0;
55549
+ let noted = 0;
55550
+ for (const t of input.tracked) {
55551
+ if (input.packageClasses.has(t.className)) continue;
55552
+ if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
55553
+ const keyFrameMediaKey = input.keyFrameMediaKeyFor(t.trackId);
55554
+ this.note({
55555
+ deviceId: input.deviceId,
55556
+ trackId: t.trackId,
55557
+ className: t.className,
55558
+ at: input.timestamp,
55559
+ ...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
55560
+ });
55561
+ noted += 1;
55562
+ }
55563
+ return noted;
55564
+ }
55565
+ /** Drop a camera's memory (unbind / step disabled / device removal). */
55566
+ forgetDevice(deviceId) {
55567
+ this.byDevice.delete(deviceId);
55568
+ }
55569
+ reset() {
55570
+ this.byDevice.clear();
55571
+ }
55572
+ /** Retained presences for a camera — diagnostics and tests only. */
55573
+ debugPresenceCount(deviceId) {
55574
+ return this.byDevice.get(deviceId)?.size ?? 0;
55575
+ }
55576
+ };
55577
+ function clamp(value, low, high) {
55578
+ if (value < low) return low;
55579
+ if (value > high) return high;
55580
+ return value;
55581
+ }
55582
+ /** Keep the newest {@link PACKAGE_COLLECTOR_MAX_PRESENCES} by `lastAt`. */
55583
+ function evictOldest(presences) {
55584
+ if (presences.size <= 32) return presences;
55585
+ const ordered = [...presences.values()].sort((a, b) => b.lastAt - a.lastAt);
55586
+ const kept = /* @__PURE__ */ new Map();
55587
+ for (const p of ordered.slice(0, 32)) kept.set(p.trackId, p);
55588
+ return kept;
55589
+ }
55280
55590
  //#endregion
55281
55591
  //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
55282
55592
  /**
@@ -56752,6 +57062,61 @@ function classifyTrackAppearance(input) {
56752
57062
  if (input.inPrevActive) return "continuing";
56753
57063
  return input.positionsCount > 1 ? "resurrection" : "birth";
56754
57064
  }
57065
+ //#endregion
57066
+ //#region src/pipeline-analytics/pipeline/archive-debug-note.ts
57067
+ /**
57068
+ * Archive the note a clear is about to destroy. Never throws.
57069
+ *
57070
+ * **Which way this fails, stated once.** The operator pressed a flag button;
57071
+ * the archive is this system's own bookkeeping. So a failing archive does NOT
57072
+ * block the flag write — the button must do what it says even when SQLite is
57073
+ * busy — and it does NOT retry, because the caller is a synchronous cap method
57074
+ * an operator is waiting on.
57075
+ *
57076
+ * What it must never do is lose the sentence quietly. The failure line carries
57077
+ * the NOTE TEXT verbatim, plus `tags: { deviceId }` and the trackId, so the
57078
+ * words are recoverable from the log by hand and the loss is attributable to
57079
+ * one camera. A branch that drops work silently reads as "never happened", and
57080
+ * the 3-hour media blackout produced not one line.
57081
+ *
57082
+ * An EMPTY note archives nothing: there is nothing to keep, and a row of `''`
57083
+ * would be a permanent entry in a corpus meant to be mined for sentences.
57084
+ */
57085
+ async function archiveNoteBeforeClear(input) {
57086
+ const note = input.current.debugNote;
57087
+ if (note.trim() === "") return;
57088
+ if (input.archive === void 0) {
57089
+ input.logger.warn("debug note dropped — no archive is wired on this runner", {
57090
+ tags: { deviceId: input.deviceId },
57091
+ meta: {
57092
+ deviceId: input.deviceId,
57093
+ trackId: input.trackId,
57094
+ note
57095
+ }
57096
+ });
57097
+ return;
57098
+ }
57099
+ try {
57100
+ await input.archive.append({
57101
+ deviceId: input.deviceId,
57102
+ sourceTrackId: input.trackId,
57103
+ note,
57104
+ ...input.current.startedAt !== void 0 ? { trackStartedAt: input.current.startedAt } : {},
57105
+ ...input.current.className !== void 0 ? { trackClass: input.current.className } : {},
57106
+ ...input.current.label !== void 0 ? { trackLabel: input.current.label } : {}
57107
+ });
57108
+ } catch (err) {
57109
+ input.logger.error("failed to archive a debug note — the text is in this line", {
57110
+ tags: { deviceId: input.deviceId },
57111
+ meta: {
57112
+ deviceId: input.deviceId,
57113
+ trackId: input.trackId,
57114
+ note,
57115
+ error: err instanceof Error ? err.message : String(err)
57116
+ }
57117
+ });
57118
+ }
57119
+ }
56755
57120
  /** Thrown when the device is already holding its full staging budget. Distinct
56756
57121
  * type so a surface can tell "you are out of room" from "that track is gone". */
56757
57122
  var StagingBudgetExceededError = class extends Error {
@@ -56792,7 +57157,10 @@ var TrackAlreadyTrainedError = class extends Error {
56792
57157
  * caller sent with it. A note surviving the flag is a leftover that reads
56793
57158
  * as a live request, and the invariant belongs here rather than in the
56794
57159
  * three surfaces that write this patch — one of which is a browser grid
56795
- * that has no note UI at all and would never think to clear it.
57160
+ * that has no note UI at all and would never think to clear it. Since
57161
+ * D405 the clear is not a DELETE: `applyTrackFlags` ARCHIVES the note
57162
+ * first, into a table that outlives the track. The invariant above is
57163
+ * unchanged — only where the words go.
56796
57164
  * 2. **No note in the patch → nothing is written.** This is what makes a
56797
57165
  * cancelled prompt free: the app turns debug on, the operator declines to
56798
57166
  * type, and the flag still lands. Refusing to write a note must never cost
@@ -56886,6 +57254,13 @@ async function applyTrackFlags(deps, input) {
56886
57254
  ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
56887
57255
  ...resolveNotePatch(deps, input, current)
56888
57256
  };
57257
+ if (patch.debugNote === "" && current.debugNote !== "") await archiveNoteBeforeClear({
57258
+ deviceId: input.deviceId,
57259
+ trackId: input.trackId,
57260
+ logger,
57261
+ archive: deps.noteArchive,
57262
+ current
57263
+ });
56889
57264
  if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
56890
57265
  const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
56891
57266
  return {
@@ -59567,6 +59942,261 @@ var IdentityStore = class {
59567
59942
  }
59568
59943
  };
59569
59944
  //#endregion
59945
+ //#region src/pipeline-analytics/store/debug-note-archive-store.ts
59946
+ /**
59947
+ * DebugNoteArchiveStore — the durable corpus of what operators asked to be
59948
+ * checked (D405).
59949
+ *
59950
+ * A declared SQL-backed collection, like the events ops-log next door, and for
59951
+ * the same reason: pipeline-analytics already owns SQLite collections, and an
59952
+ * undeclared collection crash-loops the runner. MUST be declared in
59953
+ * `onInitialize` before the first write.
59954
+ *
59955
+ * Collection name: pipeline-analytics:debug-note-archive
59956
+ *
59957
+ * Two properties separate it from every other table in this addon:
59958
+ *
59959
+ * - **It is not track-owned.** It names a track in `sourceTrackId` and that is
59960
+ * PROVENANCE, never ownership — the whole point is that the row outlives the
59961
+ * track, which `debug` deliberately never pinned (D353). It is classified
59962
+ * `never-orphaned` by hand in `collection-classification.ts` so the
59963
+ * ownership derivation cannot see the column and cascade it away.
59964
+ * - **It is bounded by COUNT, not by age.** An age sweep would delete the
59965
+ * corpus for being old, which is the failure this table exists to fix: a
59966
+ * six-month-old note is the most valuable row in it, because the pattern it
59967
+ * describes has had time to repeat. See {@link MAX_ARCHIVED_DEBUG_NOTES}.
59968
+ */
59969
+ /**
59970
+ * @durable class=ledger owner=pipeline-analytics
59971
+ * write="one row per debug note RETIRED by a review — appended by
59972
+ * `applyTrackFlags` immediately before `debug: false` clears the note off
59973
+ * the track row (D405). Idempotent per (sourceTrackId, note): the row id is
59974
+ * derived from the pair, so reviewing the same track twice cannot double
59975
+ * the corpus. An empty note writes nothing."
59976
+ * retention="NOT track retention and NOT an age clock — deleting a note for
59977
+ * being old is the failure this table fixes. Bounded by ROW COUNT
59978
+ * (MAX_ARCHIVED_DEBUG_NOTES, 5000, fleet-wide), oldest `archivedAt` first,
59979
+ * trimmed on append. ~3 MB of text at the ceiling."
59980
+ */
59981
+ var DEBUG_NOTE_ARCHIVE_COLLECTION = "pipeline-analytics:debug-note-archive";
59982
+ var DEBUG_NOTE_ARCHIVE_COLUMNS = [
59983
+ {
59984
+ name: "id",
59985
+ type: "TEXT",
59986
+ primaryKey: true,
59987
+ notNull: true
59988
+ },
59989
+ {
59990
+ name: "archivedAt",
59991
+ type: "INTEGER",
59992
+ notNull: true
59993
+ },
59994
+ {
59995
+ name: "deviceId",
59996
+ type: "INTEGER",
59997
+ notNull: true
59998
+ },
59999
+ {
60000
+ name: "sourceTrackId",
60001
+ type: "TEXT",
60002
+ notNull: true
60003
+ },
60004
+ {
60005
+ name: "note",
60006
+ type: "TEXT",
60007
+ notNull: true
60008
+ },
60009
+ {
60010
+ name: "trackStartedAt",
60011
+ type: "INTEGER"
60012
+ },
60013
+ {
60014
+ name: "trackClass",
60015
+ type: "TEXT"
60016
+ },
60017
+ {
60018
+ name: "trackLabel",
60019
+ type: "TEXT"
60020
+ }
60021
+ ];
60022
+ var DEBUG_NOTE_ARCHIVE_INDEXES = [{
60023
+ name: "idx_debugnote_archived_at",
60024
+ columns: ["archivedAt"]
60025
+ }, {
60026
+ name: "idx_debugnote_device_at",
60027
+ columns: ["deviceId", "archivedAt"]
60028
+ }];
60029
+ /**
60030
+ * The row id for one `(track, note)` pair.
60031
+ *
60032
+ * Deterministic on purpose: the idempotency the operator needs is "reviewing
60033
+ * twice does not double the row", and a derived primary key gets it from the
60034
+ * database rather than from a read-modify-write that two concurrent reviews
60035
+ * could interleave through. Truncated to 32 hex characters — 128 bits over a
60036
+ * table capped at 5 000 rows.
60037
+ */
60038
+ function archiveRowId(sourceTrackId, note) {
60039
+ return (0, node_crypto.createHash)("sha256").update(`${sourceTrackId}${note}`).digest("hex").slice(0, 32);
60040
+ }
60041
+ var DebugNoteArchiveStore = class {
60042
+ store;
60043
+ logger;
60044
+ now;
60045
+ maxRows;
60046
+ constructor(deps) {
60047
+ this.store = deps.store;
60048
+ this.logger = deps.logger;
60049
+ this.now = deps.now ?? (() => Date.now());
60050
+ this.maxRows = deps.maxRows ?? 5e3;
60051
+ }
60052
+ /** The ceiling a store built without an override enforces. */
60053
+ static defaultMaxRows() {
60054
+ return require_dist.MAX_ARCHIVED_DEBUG_NOTES;
60055
+ }
60056
+ static async declare(store) {
60057
+ await store.declareCollection.mutate({
60058
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60059
+ columns: [...DEBUG_NOTE_ARCHIVE_COLUMNS],
60060
+ indexes: [...DEBUG_NOTE_ARCHIVE_INDEXES]
60061
+ });
60062
+ }
60063
+ /**
60064
+ * Append one retired note. THROWS if the row cannot be written.
60065
+ *
60066
+ * Deliberately not best-effort, unlike the ops-log's `append`: this is the
60067
+ * only copy of something a person typed, and the caller
60068
+ * (`archiveNoteBeforeClear`) has both the text and the camera and is the
60069
+ * right place to decide what a failure costs — it logs the note verbatim and
60070
+ * lets the operator's flag write proceed. Swallowing here would hide the
60071
+ * failure from the one caller able to preserve the words.
60072
+ *
60073
+ * The TRIM is the exception and is best-effort: the row has already landed,
60074
+ * and failing the append because housekeeping could not run would make the
60075
+ * caller report a note lost that is sitting in the table.
60076
+ */
60077
+ async append(input) {
60078
+ const note = input.note.trim() === "" ? "" : input.note;
60079
+ if (note === "") return {
60080
+ archived: false,
60081
+ id: null
60082
+ };
60083
+ const id = archiveRowId(input.sourceTrackId, note);
60084
+ if ((await this.store.query.query({
60085
+ collection: "pipeline-analytics:debug-note-archive",
60086
+ filter: {
60087
+ where: { id },
60088
+ limit: 1
60089
+ }
60090
+ })).length > 0) return {
60091
+ archived: false,
60092
+ id
60093
+ };
60094
+ const row = require_dist.ArchivedDebugNoteSchema.parse({
60095
+ id,
60096
+ note,
60097
+ deviceId: input.deviceId,
60098
+ sourceTrackId: input.sourceTrackId,
60099
+ archivedAt: this.now(),
60100
+ trackStartedAt: input.trackStartedAt ?? null,
60101
+ trackClass: input.trackClass ?? null,
60102
+ trackLabel: input.trackLabel ?? null
60103
+ });
60104
+ await this.store.insert.mutate({
60105
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60106
+ record: {
60107
+ id: row.id,
60108
+ data: {
60109
+ note: row.note,
60110
+ deviceId: row.deviceId,
60111
+ sourceTrackId: row.sourceTrackId,
60112
+ archivedAt: row.archivedAt,
60113
+ trackStartedAt: row.trackStartedAt,
60114
+ trackClass: row.trackClass,
60115
+ trackLabel: row.trackLabel
60116
+ }
60117
+ }
60118
+ });
60119
+ await this.trim();
60120
+ return {
60121
+ archived: true,
60122
+ id: row.id
60123
+ };
60124
+ }
60125
+ /**
60126
+ * Hold the table at its ceiling, oldest first. Best-effort by design — see
60127
+ * {@link append}.
60128
+ *
60129
+ * NOT an age-keyed sweep and must never become one: the boundary below is
60130
+ * computed from the POSITION of the excess rows, never from a clock, so a
60131
+ * quiet fleet keeps its whole corpus for ever and a busy one drops its oldest
60132
+ * questions. Rows sharing the boundary timestamp go together; at a
60133
+ * one-per-review write rate that is a rounding error, and the alternative —
60134
+ * deleting by id, one round trip per row — is the N+1 drain this store layer
60135
+ * spent a release removing.
60136
+ */
60137
+ async trim() {
60138
+ try {
60139
+ const held = await this.store.count.query({ collection: DEBUG_NOTE_ARCHIVE_COLLECTION });
60140
+ const excess = held - this.maxRows;
60141
+ if (excess <= 0) return;
60142
+ const boundary = (await this.store.query.query({
60143
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60144
+ filter: {
60145
+ orderBy: {
60146
+ field: "archivedAt",
60147
+ direction: "asc"
60148
+ },
60149
+ limit: excess
60150
+ },
60151
+ columns: ["archivedAt"]
60152
+ })).reduce((acc, r) => {
60153
+ const at = r.data["archivedAt"];
60154
+ return typeof at === "number" && at > acc ? at : acc;
60155
+ }, 0);
60156
+ if (boundary === 0) return;
60157
+ const { deleted } = await this.store.deleteWhere.mutate({
60158
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60159
+ filter: { whereBetween: { archivedAt: [0, boundary] } }
60160
+ });
60161
+ this.logger.info("debug-note archive trimmed to its ceiling", { meta: {
60162
+ held,
60163
+ ceiling: this.maxRows,
60164
+ deleted
60165
+ } });
60166
+ } catch (err) {
60167
+ this.logger.warn("debug-note archive trim failed — the archive will keep growing", { meta: {
60168
+ ceiling: this.maxRows,
60169
+ error: err instanceof Error ? err.message : String(err)
60170
+ } });
60171
+ }
60172
+ }
60173
+ /** Archived notes, newest first, optionally scoped to one camera. */
60174
+ async list(query) {
60175
+ const filter = {
60176
+ orderBy: {
60177
+ field: "archivedAt",
60178
+ direction: "desc"
60179
+ },
60180
+ limit: query.limit ?? 200
60181
+ };
60182
+ if (query.deviceId !== void 0) filter["where"] = { deviceId: query.deviceId };
60183
+ const rows = await this.store.query.query({
60184
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60185
+ filter
60186
+ });
60187
+ const out = [];
60188
+ for (const r of rows) {
60189
+ const parsed = require_dist.ArchivedDebugNoteSchema.safeParse({
60190
+ id: r.id,
60191
+ ...r.data
60192
+ });
60193
+ if (parsed.success) out.push(parsed.data);
60194
+ else this.logger.debug("debug-note archive: skipped a malformed row", { meta: { id: r.id } });
60195
+ }
60196
+ return out;
60197
+ }
60198
+ };
60199
+ //#endregion
59570
60200
  //#region src/pipeline-analytics/store/ops-log-store.ts
59571
60201
  /**
59572
60202
  * OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
@@ -60930,6 +61560,13 @@ var ANALYTICS_COLLECTIONS = [
60930
61560
  why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
60931
61561
  }
60932
61562
  },
61563
+ {
61564
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
61565
+ classification: {
61566
+ kind: "never-orphaned",
61567
+ why: "the archived operator notes (D405) — the ONE table here whose whole purpose is to outlive the track it names. `debug` never pinned a track against retention and D405 does not change that, so the track a note was written on IS expected to be evicted; deleting the note with it is the failure the archive was built to fix (25–27 notes from one sweep unreadable the same afternoon). Its column is called `sourceTrackId` and not `trackId` precisely so the ownership derivation cannot reach it, and its bound is a fleet-wide ROW COUNT trimmed on append — never an age sweep, which would delete the corpus for the one property that makes it valuable."
61568
+ }
61569
+ },
60933
61570
  {
60934
61571
  collection: STATIONARY_COLLECTION,
60935
61572
  classification: {
@@ -68700,6 +69337,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68700
69337
  eventStore = null;
68701
69338
  /** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
68702
69339
  eventsOpsLog = null;
69340
+ /**
69341
+ * The archived operator debug notes (D405). Null until onInitialize — and
69342
+ * `applyTrackFlags` treats null as "no archive wired": the flag write the
69343
+ * operator asked for never depends on this addon's own bookkeeping.
69344
+ */
69345
+ debugNoteArchive = null;
68703
69346
  /** Event-media relocation engine (entity-routing Phase 4). */
68704
69347
  mediaRelocate = null;
68705
69348
  mediaLocationStorage = null;
@@ -69257,6 +69900,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69257
69900
  * which would let the streetlight wedge vouch for its own delivery.
69258
69901
  */
69259
69902
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
69903
+ /**
69904
+ * WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
69905
+ * minutes after the fact, so the collector must be remembered while it is
69906
+ * still true — handles only, never pixels.
69907
+ */
69908
+ packageCollectors = new PackageCollectorMirror();
69260
69909
  /** Last time the oversize-sighting drop was logged per device — one line
69261
69910
  * per 5 minutes, not one per governor round (the wedge re-sights all
69262
69911
  * night; ~300 identical lines would bury the log it exists to serve). */
@@ -69682,6 +70331,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69682
70331
  logger: logger.child("ops-log"),
69683
70332
  nodeId: ownNodeId
69684
70333
  });
70334
+ this.debugNoteArchive = new DebugNoteArchiveStore({
70335
+ store: api.settingsStore,
70336
+ logger: logger.child("debug-note-archive")
70337
+ });
69685
70338
  {
69686
70339
  const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
69687
70340
  this.isPostProcessingNode = ownNodeId === designated;
@@ -70001,6 +70654,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70001
70654
  RetrainFrameStore.declare(api.settingsStore),
70002
70655
  RetrainAnnotationStore.declare(api.settingsStore),
70003
70656
  OpsLogStore.declare(api.settingsStore),
70657
+ DebugNoteArchiveStore.declare(api.settingsStore),
70004
70658
  AnalyticsLts.declare(api.settingsStore),
70005
70659
  SceneStore.declare(api.settingsStore),
70006
70660
  NotificationCenter.declare(api.settingsStore),
@@ -71639,6 +72293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71639
72293
  this.packageStillness.forgetDevice(data.deviceId);
71640
72294
  this.packageDropDetector?.forgetDevice(data.deviceId);
71641
72295
  this.lastNonPackageActivityAt.delete(data.deviceId);
72296
+ this.packageCollectors.forgetDevice(data.deviceId);
71642
72297
  this.packageOversizeLogAt.delete(data.deviceId);
71643
72298
  this.overlayState.clearDevice(data.deviceId);
71644
72299
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -71658,6 +72313,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71658
72313
  this.packageStillness.forgetDevice(deviceId);
71659
72314
  this.packageDropDetector?.forgetDevice(deviceId);
71660
72315
  this.lastNonPackageActivityAt.delete(deviceId);
72316
+ this.packageCollectors.forgetDevice(deviceId);
71661
72317
  this.packageOversizeLogAt.delete(deviceId);
71662
72318
  this.overlayState.clearDevice(deviceId);
71663
72319
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -72541,12 +73197,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72541
73197
  this.residents.markFirstFramePending(deviceId, id);
72542
73198
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
72543
73199
  this.residents.setLastFrameAt(deviceId, id, result.timestamp);
72544
- if (frameHandle) firstFrameTargets.push({
73200
+ const birthCapture = decideBirthFirstFrameCapture({
73201
+ hasFrameHandle: frameHandle !== void 0,
73202
+ matchedThisFrame: t.matchedThisFrame
73203
+ });
73204
+ if (birthCapture.capture) firstFrameTargets.push({
72545
73205
  trackId: id,
72546
73206
  timestamp: result.timestamp,
72547
73207
  bbox: { ...t.bbox },
72548
73208
  ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {}
72549
73209
  });
73210
+ else log.info("birth firstFrame not captured — retry armed", {
73211
+ tags: { deviceId },
73212
+ meta: {
73213
+ trackId: id,
73214
+ className: t.className,
73215
+ source,
73216
+ refusedBecause: birthCapture.refusedBecause,
73217
+ deferredMs: result.timestamp - (this.trackStore?.peekActive(id)?.firstSeen ?? result.timestamp)
73218
+ }
73219
+ });
72550
73220
  }
72551
73221
  this.ctx.eventBus.emit({
72552
73222
  id: `pa-${(0, node_crypto.randomUUID)()}`,
@@ -75256,6 +75926,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75256
75926
  this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
75257
75927
  break;
75258
75928
  }
75929
+ this.packageCollectors.noteFrame({
75930
+ deviceId: input.deviceId,
75931
+ tracked: input.tracked,
75932
+ packageClasses: input.packageClasses,
75933
+ packageZoneIds: input.packageZoneIds,
75934
+ timestamp: input.timestamp,
75935
+ keyFrameMediaKeyFor: (trackId) => this.residents.keyFrameKey(trackId)
75936
+ });
75259
75937
  const sightings = [];
75260
75938
  for (const t of input.tracked) {
75261
75939
  if (!input.packageClasses.has(t.className)) continue;
@@ -75367,10 +76045,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75367
76045
  meta: { entryId: entry.id }
75368
76046
  });
75369
76047
  }).catch(() => void 0);
76048
+ const collector = this.packageCollectors.pickCollector({
76049
+ deviceId: input.deviceId,
76050
+ pickupAt: pickupInstantOf(entry)
76051
+ });
76052
+ if (collector === null) this.ctx.logger.info("package pick-up has no collector in the drop-off zone", {
76053
+ tags,
76054
+ meta: {
76055
+ entryId: entry.id,
76056
+ pickupAt: entry.lastConfirmedAt,
76057
+ departedAt: input.timestamp,
76058
+ presencesKnown: this.packageCollectors.debugPresenceCount(input.deviceId)
76059
+ }
76060
+ });
75370
76061
  this.packageDropDetector.onStationaryChange({
75371
76062
  phase: "departed",
75372
76063
  entry,
75373
- timestamp: input.timestamp
76064
+ timestamp: input.timestamp,
76065
+ ...collector !== null ? { collector } : {}
75374
76066
  });
75375
76067
  }
75376
76068
  for (const entry of outcome.departedWithoutActivity) {
@@ -77495,7 +78187,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77495
78187
  setFlags: (trackId, patch) => trackStore.setFlags(trackId, patch),
77496
78188
  countStaging: (query) => trackStore.countStaging(query)
77497
78189
  },
77498
- logger: this.ctx.logger
78190
+ logger: this.ctx.logger,
78191
+ ...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
77499
78192
  }, input);
77500
78193
  this.ctx.logger.info("track operator flags set", {
77501
78194
  tags: { deviceId: input.deviceId },
@@ -77626,6 +78319,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77626
78319
  return this.queryFacade.listOpsLog(input);
77627
78320
  }
77628
78321
  /**
78322
+ * The archived debug notes, newest first (D405).
78323
+ *
78324
+ * Answers from the archive alone — never from the track rows. The point of
78325
+ * the table is that the tracks these notes were written on are gone, so a
78326
+ * read that joined them would return exactly the rows the archive exists to
78327
+ * replace. An archive that has not been built yet answers EMPTY, which is
78328
+ * true: nothing has been archived on this runner.
78329
+ */
78330
+ async listArchivedDebugNotes(input) {
78331
+ return await this.debugNoteArchive?.list(input) ?? [];
78332
+ }
78333
+ /**
77629
78334
  * Track-centric time-based retention (design §5.1). Drains every persisted
77630
78335
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
77631
78336
  * widened cascade — enrolled faces/plates + identity media are exempt (design