@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.
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-BV15Z8DR.mjs";
1
+ import { $ as assertTimelapseCadences, $t as string, A as NcScheduleSchema, At as subKindsOf, Bt as hydrateSchema, Ct as plateGalleryCapability, D as NcRulePatchSchema, E as NcRuleInputSchema, Et as resolveLocationMode, F as NcTaxonomySchema, Ft as errMsg$1, G as TIMELAPSE_DENSE_FLOOR_SEC, Gt as array, H as SCENE_DEFAULT_UNCOVERED_POLICY, Ht as nodePin, I as OpsLogEntrySchema, It as BaseAddon, J as TimelapseRuleSchema, Jt as literal, K as TimelapseRuleInputSchema, Kt as boolean, Lt as CamProfileSchema, M as NcSnoozeSchema, Mt as vectorDimFromBase64, N as NcSnoozeSuppressedSchema, Nt as videoclipsCapability, O as NcRuleSchema, Ot as sceneMonitorCapability, P as NcSystemEventKindSchema, Pt as zoneAnalyticsCapability, Q as alarmPanelCapability, Qt as record, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as DeviceType, S as NC_DEFAULT_SNOOZE_MINUTES, St as pipelineAnalyticsCapability, T as NcConditionDescriptorSchema, Tt as readTimelapseGeneratedAt, U as SCENE_DIVERGED, Ut as sleep$1, V as SCENE_DEFAULT_ANCHOR_THRESHOLD, Vt as isDeviceScopedCap, W as SceneMonitorSchema, Wt as _enum, X as VISIT_MERGE_GAP_MS, Xt as object, Y as TrackSourceSchema, Yt as number, Z as addonWidgetsSourceCapability, Zt as partialRecord, _ as MACRO_LABELS, _t as kebabToCamel, a as COCO_TO_MACRO, at as defineCustomActions, b as NC_ALARM_SYSTEM_EVENT_KINDS, ct as encodeVectorBase64, d as DeclaredDevices, dt as faceGalleryCapability, en as unknown, et as audioMetricsCapability, f as EVENT_KIND_BY_CAP, ft as failureContributionCapability, g as LabelAttributionSchema, gt as isSourceCap, h as FailureCounters, ht as isScheduleActive, i as CLUSTER_MODEL_SCOPED_STEPS, it as customAction, j as NcSnoozeInputSchema, jt as systemEventFilterApplies, k as NcRuleTargetSchema, kt as storageOccupancyCapability, l as DETECTION_MACRO_CLASSES, lt as evaluateSensorEdge, m as FULL_IMAGE_BBOX, mt as isDetectionMacroClass, n as ArchivedDebugNoteSchema, nt as buildEventKindDescriptor, o as DEFAULT_EVENT_COLOR, ot as deriveRecordingMode, p as EVENT_PAD_MS, q as TimelapseRulePatchSchema, qt as discriminatedUnion, r as BaseDevice, rt as cosineSimilarity$1, t as AUDIO_MACRO_LABELS, tn as EventCategory, tt as audioModeOf, ut as evictionPolicyOfLocation, v as MAX_ARCHIVED_DEBUG_NOTES, vt as mayWriteToLocation, w as NC_TAXONOMY, wt as readDeviceStateFrom, x as NC_CONDITION_CATALOG, xt as pickClusterStepModels, y as MediaFileKindEnum, yt as notificationRulesCapability, z as RetrainStatusSchema, zt as createEvent } from "../dist-D2rFaMex.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";
@@ -3445,7 +3445,7 @@ var SceneConfirmGate = class {
3445
3445
  };
3446
3446
  }
3447
3447
  };
3448
- function clamp$1(v, lo, hi) {
3448
+ function clamp$2(v, lo, hi) {
3449
3449
  return Math.max(lo, Math.min(v, hi));
3450
3450
  }
3451
3451
  /**
@@ -3460,10 +3460,10 @@ async function cropSnapshotRoi(encoded, roi) {
3460
3460
  const frameW = meta.width ?? 0;
3461
3461
  const frameH = meta.height ?? 0;
3462
3462
  if (frameW <= 0 || frameH <= 0) throw new Error("snapshot has no decodable dimensions");
3463
- const left = clamp$1(Math.round(roi.x * frameW), 0, frameW - 1);
3464
- const top = clamp$1(Math.round(roi.y * frameH), 0, frameH - 1);
3465
- const width = clamp$1(Math.round(roi.width * frameW), 1, frameW - left);
3466
- const height = clamp$1(Math.round(roi.height * frameH), 1, frameH - top);
3463
+ const left = clamp$2(Math.round(roi.x * frameW), 0, frameW - 1);
3464
+ const top = clamp$2(Math.round(roi.y * frameH), 0, frameH - 1);
3465
+ const width = clamp$2(Math.round(roi.width * frameW), 1, frameW - left);
3466
+ const height = clamp$2(Math.round(roi.height * frameH), 1, frameH - top);
3467
3467
  return {
3468
3468
  crop: await sharp(encoded).extract({
3469
3469
  left,
@@ -28611,6 +28611,37 @@ var MediaStore = class {
28611
28611
  return newKey;
28612
28612
  }
28613
28613
  /**
28614
+ * Exchange the bytes+instant held in the `firstFrame` and `lastFrame` slots
28615
+ * (close-time ordering repair, see `pipeline/first-last-frame-order.ts`).
28616
+ *
28617
+ * Both are single-instance kinds, so each `putReplacing` overwrites the slot
28618
+ * it names — the two rows end up holding each other's blob and timestamp.
28619
+ * Nothing is deleted and nothing is re-encoded: both blobs are already the
28620
+ * track's own stored pixels and the only thing wrong with them is which slot
28621
+ * they sit in. The `lastFrame` write lands first so the "ultimo" tile is
28622
+ * never momentarily the older view a viewer just refuted.
28623
+ */
28624
+ async swapFirstAndLastFrame(input) {
28625
+ await this.gated(async () => {
28626
+ await this.putReplacingUnchecked({
28627
+ deviceId: input.deviceId,
28628
+ ownerKind: "track",
28629
+ ownerId: input.trackId,
28630
+ kind: "lastFrame",
28631
+ timestamp: input.firstFrame.timestamp,
28632
+ data: Buffer.from(input.firstFrame.base64, "base64")
28633
+ });
28634
+ await this.putReplacingUnchecked({
28635
+ deviceId: input.deviceId,
28636
+ ownerKind: "track",
28637
+ ownerId: input.trackId,
28638
+ kind: "firstFrame",
28639
+ timestamp: input.lastFrame.timestamp,
28640
+ data: Buffer.from(input.lastFrame.base64, "base64")
28641
+ });
28642
+ });
28643
+ }
28644
+ /**
28614
28645
  * Fetch one media entry by its key (id). Returns null if the key is not
28615
28646
  * found in the index or if the blob is missing from storage.
28616
28647
  */
@@ -30482,6 +30513,17 @@ function trackDurationMs(facts) {
30482
30513
  function hasKind(media, kind) {
30483
30514
  return media.some((m) => m.kind === kind);
30484
30515
  }
30516
+ /** Newest instant stamped on a row of `kind`, or `null` when the track owns
30517
+ * none. Single-instance kinds hold at most one row; the max guards a legacy
30518
+ * duplicate, mirroring `decideFirstLastFrameOrder`. */
30519
+ function newestOfKind(media, kind) {
30520
+ let newest = null;
30521
+ for (const m of media) {
30522
+ if (m.kind !== kind) continue;
30523
+ if (newest === null || m.timestamp > newest) newest = m.timestamp;
30524
+ }
30525
+ return newest;
30526
+ }
30485
30527
  function latestMediaAt(media) {
30486
30528
  let latest = null;
30487
30529
  for (const m of media) if (latest === null || m.timestamp > latest) latest = m.timestamp;
@@ -30529,6 +30571,12 @@ function analyseDebugTrack(inventory) {
30529
30571
  const lag = facts.lastSeen - lastFrameAt;
30530
30572
  if (lag > 2e3) out.push(finding("LASTFRAME_STALE", "warn", `lastFrame is ${String(lag)} ms older than the track's end.`));
30531
30573
  }
30574
+ const firstFrameAt = newestOfKind(media, "firstFrame");
30575
+ if (firstFrameAt !== null) {
30576
+ const lastFrameAt = newestOfKind(media, "lastFrame");
30577
+ 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.`));
30578
+ 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).`));
30579
+ }
30532
30580
  if (closed && durationMs >= 1e4 && media.length > 0) {
30533
30581
  const latest = latestMediaAt(media);
30534
30582
  if (latest !== null) {
@@ -34126,6 +34174,29 @@ function firstFrameRetryBbox(current, birth) {
34126
34174
  return { ...current };
34127
34175
  }
34128
34176
  //#endregion
34177
+ //#region src/pipeline-analytics/pipeline/birth-first-frame-gate.ts
34178
+ /**
34179
+ * Decide whether the confirmed-birth frame may be dispatched as this track's
34180
+ * `firstFrame` capture. Pure; the caller logs the refusal and keeps the
34181
+ * `firstFramePending` mark so a later frame retries.
34182
+ *
34183
+ * `coasted-frame` is reported ahead of `no-frame-handle` when both hold: a
34184
+ * handle-less coasted frame would have been refused for BEING coasted whatever
34185
+ * the decode plane did, and naming the decode is what would send the next
34186
+ * investigation at the wrong subsystem.
34187
+ */
34188
+ function decideBirthFirstFrameCapture(input) {
34189
+ if (input.matchedThisFrame === false) return {
34190
+ capture: false,
34191
+ refusedBecause: "coasted-frame"
34192
+ };
34193
+ if (!input.hasFrameHandle) return {
34194
+ capture: false,
34195
+ refusedBecause: "no-frame-handle"
34196
+ };
34197
+ return { capture: true };
34198
+ }
34199
+ //#endregion
34129
34200
  //#region src/pipeline-analytics/pipeline/events/event-filter.ts
34130
34201
  var DEFAULT_EVENT_EMITTER_CONFIG = {
34131
34202
  minTrackAge: 3,
@@ -34734,7 +34805,7 @@ var DEFAULT_TRACKER_CONFIG = {
34734
34805
  var PERSON_CLASS = "person";
34735
34806
  var ANIMAL_CLASS = "animal";
34736
34807
  var MAX_PATH_LENGTH = 300;
34737
- function clamp(value, min, max) {
34808
+ function clamp$1(value, min, max) {
34738
34809
  return Math.max(min, Math.min(max, value));
34739
34810
  }
34740
34811
  function iou$3(a, b) {
@@ -35174,8 +35245,8 @@ var SortTracker = class SortTracker {
35174
35245
  const perFrameX = t.velocity.dx * t.emitIntervalMs;
35175
35246
  const perFrameY = t.velocity.dy * t.emitIntervalMs;
35176
35247
  return {
35177
- x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
35178
- y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
35248
+ x: t.bbox.x + clamp$1(perFrameX * f, -t.bbox.w, t.bbox.w),
35249
+ y: t.bbox.y + clamp$1(perFrameY * f, -t.bbox.h, t.bbox.h),
35179
35250
  w: t.bbox.w,
35180
35251
  h: t.bbox.h
35181
35252
  };
@@ -35184,8 +35255,8 @@ var SortTracker = class SortTracker {
35184
35255
  const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
35185
35256
  const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
35186
35257
  return {
35187
- x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
35188
- y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
35258
+ x: t.bbox.x + clamp$1(t.velocity.dx * elapsedMs, -allowX, allowX),
35259
+ y: t.bbox.y + clamp$1(t.velocity.dy * elapsedMs, -allowY, allowY),
35189
35260
  w: t.bbox.w,
35190
35261
  h: t.bbox.h
35191
35262
  };
@@ -37262,6 +37333,44 @@ function decideLastFramePromotion(media) {
37262
37333
  };
37263
37334
  }
37264
37335
  //#endregion
37336
+ //#region src/pipeline-analytics/pipeline/first-last-frame-order.ts
37337
+ /**
37338
+ * Decide whether a closing track's `firstFrame` and `lastFrame` slots hold each
37339
+ * other's bytes. Pure — see the module header for the contract.
37340
+ *
37341
+ * Swap when BOTH slots are filled and the `firstFrame` is STRICTLY newer than
37342
+ * the `lastFrame`. Equal timestamps are not an inversion.
37343
+ */
37344
+ function decideFirstLastFrameOrder(media) {
37345
+ let firstFrame;
37346
+ let lastFrame;
37347
+ for (const m of media) if (m.kind === "firstFrame") {
37348
+ if (firstFrame === void 0 || m.timestamp > firstFrame.timestamp) firstFrame = m;
37349
+ } else if (m.kind === "lastFrame") {
37350
+ if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
37351
+ }
37352
+ if (firstFrame === void 0) return {
37353
+ swap: false,
37354
+ skippedBecause: "no-first-frame"
37355
+ };
37356
+ if (lastFrame === void 0) return {
37357
+ swap: false,
37358
+ skippedBecause: "no-last-frame"
37359
+ };
37360
+ if (firstFrame.timestamp <= lastFrame.timestamp) return {
37361
+ swap: false,
37362
+ skippedBecause: "ordered"
37363
+ };
37364
+ return {
37365
+ swap: true,
37366
+ firstFrameKey: firstFrame.key,
37367
+ lastFrameKey: lastFrame.key,
37368
+ firstFrameTimestamp: firstFrame.timestamp,
37369
+ lastFrameTimestamp: lastFrame.timestamp,
37370
+ inversionMs: firstFrame.timestamp - lastFrame.timestamp
37371
+ };
37372
+ }
37373
+ //#endregion
37265
37374
  //#region src/pipeline-analytics/pipeline/static-track-gate.ts
37266
37375
  /**
37267
37376
  * Net displacement + path span for a track's centroid path, normalized to
@@ -37404,6 +37513,7 @@ var TrackCloser = class {
37404
37513
  tile = "raster";
37405
37514
  }
37406
37515
  await this.maybePromoteLastFrame(t, ownedMedia);
37516
+ await this.repairFirstLastFrameOrder(t);
37407
37517
  const hasRasterFallback = closure?.rasterFallback !== void 0;
37408
37518
  let deriveMiss;
37409
37519
  if (!thumbnailLanded) {
@@ -37747,6 +37857,56 @@ var TrackCloser = class {
37747
37857
  }
37748
37858
  }
37749
37859
  /**
37860
+ * A track's "primo" tile is never newer than its "ultimo" tile
37861
+ * (`first-last-frame-order.ts`). Filed by hand three times on device 3829,
37862
+ * 2026-09-08 — `d035f251`, `448678bb`, `7628b19d`.
37863
+ *
37864
+ * The media list is RE-READ rather than reusing the caller's `ownedMedia`:
37865
+ * `maybePromoteLastFrame` has just run and is precisely what may have put the
37866
+ * older bytes in the `lastFrame` slot, so the caller's copy predates the pair
37867
+ * being ordered here. Best-effort — a failure never blocks the close.
37868
+ */
37869
+ async repairFirstLastFrameOrder(t) {
37870
+ const store = this.deps.mediaStore();
37871
+ if (!store) return;
37872
+ const media = await store.listByOwner("track", t.trackId);
37873
+ const decision = decideFirstLastFrameOrder(media);
37874
+ if (!decision.swap) return;
37875
+ const firstFrame = media.find((m) => m.key === decision.firstFrameKey);
37876
+ const lastFrame = media.find((m) => m.key === decision.lastFrameKey);
37877
+ if (!firstFrame || !lastFrame) return;
37878
+ try {
37879
+ await store.swapFirstAndLastFrame({
37880
+ deviceId: t.deviceId,
37881
+ trackId: t.trackId,
37882
+ firstFrame,
37883
+ lastFrame
37884
+ });
37885
+ this.deps.logger.warn("firstFrame/lastFrame were out of order — slots swapped", {
37886
+ tags: { deviceId: t.deviceId },
37887
+ meta: {
37888
+ trackId: t.trackId,
37889
+ className: t.className,
37890
+ inversionMs: decision.inversionMs,
37891
+ firstFrameAt: decision.firstFrameTimestamp,
37892
+ lastFrameAt: decision.lastFrameTimestamp,
37893
+ /** How far the late capture ran past the track itself. Positive means
37894
+ * it was cut from a frame the subject was no longer observed in. */
37895
+ pastTrackEndMs: decision.firstFrameTimestamp - t.lastSeen,
37896
+ durationMs: t.lastSeen - t.firstSeen
37897
+ }
37898
+ });
37899
+ } catch (err) {
37900
+ this.deps.logger.warn("firstFrame/lastFrame order repair failed", {
37901
+ tags: { deviceId: t.deviceId },
37902
+ meta: {
37903
+ trackId: t.trackId,
37904
+ error: String(err)
37905
+ }
37906
+ });
37907
+ }
37908
+ }
37909
+ /**
37750
37910
  * Key-event importance: score the just-expired track and persist. Peak
37751
37911
  * confidence comes from the shared best-detection tracker; peak bbox area
37752
37912
  * + the winning object-event id from one indexed queryObject(trackId) —
@@ -41562,7 +41722,10 @@ var TrackStore = class {
41562
41722
  retrainStatus: active.retrainStatus ?? "none",
41563
41723
  debug: active.debug === true,
41564
41724
  debugNote: active.debugNote ?? "",
41565
- favourited: active.favourited === true
41725
+ favourited: active.favourited === true,
41726
+ startedAt: active.firstSeen,
41727
+ className: active.className,
41728
+ ...displayLabel(active) !== void 0 ? { label: displayLabel(active) } : {}
41566
41729
  };
41567
41730
  const persisted = await this.getPersistedByTrackId(trackId);
41568
41731
  if (!persisted) return null;
@@ -41570,7 +41733,10 @@ var TrackStore = class {
41570
41733
  retrainStatus: persisted.retrainStatus ?? "none",
41571
41734
  debug: persisted.debug === true,
41572
41735
  debugNote: persisted.debugNote ?? "",
41573
- favourited: persisted.favourited === true
41736
+ favourited: persisted.favourited === true,
41737
+ startedAt: persisted.firstSeen,
41738
+ className: persisted.className,
41739
+ ...displayLabel(persisted) !== void 0 ? { label: displayLabel(persisted) } : {}
41574
41740
  };
41575
41741
  }
41576
41742
  /**
@@ -54949,7 +55115,7 @@ var PackageDropDetector = class {
54949
55115
  async onStationaryChange(change) {
54950
55116
  try {
54951
55117
  if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
54952
- else await this.onDeparted(change.entry, change.timestamp);
55118
+ else await this.onDeparted(change);
54953
55119
  } catch (err) {
54954
55120
  this.deps.onError?.("onStationaryChange", err);
54955
55121
  this.deps.logger.warn("package-drop detector failed on change", {
@@ -55127,7 +55293,9 @@ var PackageDropDetector = class {
55127
55293
  }
55128
55294
  });
55129
55295
  }
55130
- async onDeparted(entry, timestamp) {
55296
+ async onDeparted(change) {
55297
+ const entry = change.entry;
55298
+ const timestamp = change.timestamp;
55131
55299
  if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) {
55132
55300
  this.deps.logger.debug("package pick-up skipped — packageDropPickupEnabled is false", {
55133
55301
  tags: { deviceId: entry.deviceId },
@@ -55162,13 +55330,25 @@ var PackageDropDetector = class {
55162
55330
  });
55163
55331
  return;
55164
55332
  }
55333
+ const collector = change.collector;
55334
+ const packageTrackId = entry.sourceTrackId ?? entry.id;
55335
+ const mediaTrackId = collector?.trackId ?? packageTrackId;
55336
+ if (collector === void 0) this.deps.logger.info("package pick-up has no collector track — the notification carries the parcel frame", {
55337
+ tags: { deviceId: entry.deviceId },
55338
+ meta: {
55339
+ entryId: entry.id,
55340
+ eventId: pickedUpId,
55341
+ packageTrackId,
55342
+ pickupAt: entry.lastConfirmedAt
55343
+ }
55344
+ });
55165
55345
  const ev = {
55166
55346
  id: pickedUpId,
55167
55347
  kind: "object",
55168
55348
  deviceId: entry.deviceId,
55169
55349
  timestamp,
55170
55350
  source: "pipeline",
55171
- trackId: entry.sourceTrackId ?? entry.id,
55351
+ trackId: mediaTrackId,
55172
55352
  className: PACKAGE_EVENT_CLASS,
55173
55353
  ...entry.label !== void 0 ? { label: entry.label } : {},
55174
55354
  confidence: PACKAGE_IMPORTANCE,
@@ -55182,6 +55362,7 @@ var PackageDropDetector = class {
55182
55362
  state: PICKED_UP_STATE,
55183
55363
  frameWidth: entry.frameWidth,
55184
55364
  frameHeight: entry.frameHeight,
55365
+ ...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
55185
55366
  importance: PACKAGE_IMPORTANCE
55186
55367
  };
55187
55368
  await this.deps.events.insertObject(ev);
@@ -55191,6 +55372,7 @@ var PackageDropDetector = class {
55191
55372
  entryId: entry.id,
55192
55373
  deliveredEventId: deliveredId,
55193
55374
  className: entry.className,
55375
+ ...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
55194
55376
  timestamp
55195
55377
  });
55196
55378
  this.deps.logger.info("package picked up", {
@@ -55198,11 +55380,139 @@ var PackageDropDetector = class {
55198
55380
  meta: {
55199
55381
  entryId: entry.id,
55200
55382
  eventId: pickedUpId,
55201
- deliveredEventId: deliveredId
55383
+ deliveredEventId: deliveredId,
55384
+ mediaTrackId,
55385
+ ...collector !== void 0 ? {
55386
+ collectorTrackId: collector.trackId,
55387
+ collectorClassName: collector.className,
55388
+ collectorSkewMs: collector.skewMs
55389
+ } : { collectorTrackId: null }
55202
55390
  }
55203
55391
  });
55204
55392
  }
55205
55393
  };
55394
+ /**
55395
+ * How long a presence survives in the mirror, in FRAME time.
55396
+ *
55397
+ * It must outlive the whole detection latency: a pick-up at T is concluded no
55398
+ * earlier than T + `PACKAGE_STILLNESS_TTL_MS` (5 min of observed time), and the
55399
+ * pick is made at conclusion time. 10 minutes is double that, so a collector
55400
+ * noted at the true instant is still there to be named — and a mirror that
55401
+ * expires the answer before the question is asked would be the same silent
55402
+ * hole in a different place.
55403
+ */
55404
+ var PACKAGE_COLLECTOR_RETENTION_MS = 10 * 6e4;
55405
+ /** The pick-up instant a stationary entry implies. One derivation, named. */
55406
+ function pickupInstantOf(entry) {
55407
+ return entry.lastConfirmedAt;
55408
+ }
55409
+ var PackageCollectorMirror = class {
55410
+ /** Per device, per track. Every write replaces the record (immutable). */
55411
+ byDevice = /* @__PURE__ */ new Map();
55412
+ /**
55413
+ * Record one sighting. Extends the track's presence span and refreshes its
55414
+ * key-frame handle (a re-shot key frame is a better one, and the newest is
55415
+ * also the nearest to the pick-up — the same trade as above).
55416
+ */
55417
+ note(obs) {
55418
+ const current = this.byDevice.get(obs.deviceId);
55419
+ const next = /* @__PURE__ */ new Map();
55420
+ const horizon = obs.at - PACKAGE_COLLECTOR_RETENTION_MS;
55421
+ if (current !== void 0) for (const [id, p] of current) {
55422
+ if (p.lastAt < horizon) continue;
55423
+ next.set(id, p);
55424
+ }
55425
+ const prev = next.get(obs.trackId);
55426
+ next.set(obs.trackId, {
55427
+ trackId: obs.trackId,
55428
+ className: obs.className,
55429
+ firstAt: prev === void 0 ? obs.at : Math.min(prev.firstAt, obs.at),
55430
+ lastAt: prev === void 0 ? obs.at : Math.max(prev.lastAt, obs.at),
55431
+ ...obs.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: obs.keyFrameMediaKey } : prev?.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: prev.keyFrameMediaKey } : {}
55432
+ });
55433
+ this.byDevice.set(obs.deviceId, evictOldest(next));
55434
+ }
55435
+ /**
55436
+ * The track nearest the pick-up instant, or `null` when nobody qualifies.
55437
+ *
55438
+ * `null` is a real answer and not an error: a parcel can stop being seen
55439
+ * because the light changed or the detector lost it, and naming a person who
55440
+ * was not there would be worse than naming nobody. The caller degrades to the
55441
+ * parcel's own owner and logs it.
55442
+ */
55443
+ pickCollector(input) {
55444
+ const presences = this.byDevice.get(input.deviceId);
55445
+ if (presences === void 0 || presences.size === 0) return null;
55446
+ const maxSkewMs = input.maxSkewMs ?? 3e4;
55447
+ let best = null;
55448
+ let bestLastAt = Number.NEGATIVE_INFINITY;
55449
+ for (const p of presences.values()) {
55450
+ const observedAt = clamp(input.pickupAt, p.firstAt, p.lastAt);
55451
+ const skewMs = Math.abs(observedAt - input.pickupAt);
55452
+ if (skewMs > maxSkewMs) continue;
55453
+ if (best !== null && (skewMs > best.skewMs || skewMs === best.skewMs && p.lastAt <= bestLastAt)) continue;
55454
+ best = {
55455
+ trackId: p.trackId,
55456
+ className: p.className,
55457
+ observedAt,
55458
+ skewMs,
55459
+ ...p.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: p.keyFrameMediaKey } : {}
55460
+ };
55461
+ bestLastAt = p.lastAt;
55462
+ }
55463
+ return best;
55464
+ }
55465
+ /**
55466
+ * Fold one pipeline frame in. Returns how many presences it touched, so the
55467
+ * caller can say "nobody was ever in the drop-off zone" with a number.
55468
+ *
55469
+ * This is the ONLY place the frame-shaped input is interpreted, so the
55470
+ * production path and the test path read the same rules: a non-package class,
55471
+ * inside a configured drop-off zone, on this frame.
55472
+ */
55473
+ noteFrame(input) {
55474
+ if (input.packageZoneIds.size === 0) return 0;
55475
+ let noted = 0;
55476
+ for (const t of input.tracked) {
55477
+ if (input.packageClasses.has(t.className)) continue;
55478
+ if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
55479
+ const keyFrameMediaKey = input.keyFrameMediaKeyFor(t.trackId);
55480
+ this.note({
55481
+ deviceId: input.deviceId,
55482
+ trackId: t.trackId,
55483
+ className: t.className,
55484
+ at: input.timestamp,
55485
+ ...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
55486
+ });
55487
+ noted += 1;
55488
+ }
55489
+ return noted;
55490
+ }
55491
+ /** Drop a camera's memory (unbind / step disabled / device removal). */
55492
+ forgetDevice(deviceId) {
55493
+ this.byDevice.delete(deviceId);
55494
+ }
55495
+ reset() {
55496
+ this.byDevice.clear();
55497
+ }
55498
+ /** Retained presences for a camera — diagnostics and tests only. */
55499
+ debugPresenceCount(deviceId) {
55500
+ return this.byDevice.get(deviceId)?.size ?? 0;
55501
+ }
55502
+ };
55503
+ function clamp(value, low, high) {
55504
+ if (value < low) return low;
55505
+ if (value > high) return high;
55506
+ return value;
55507
+ }
55508
+ /** Keep the newest {@link PACKAGE_COLLECTOR_MAX_PRESENCES} by `lastAt`. */
55509
+ function evictOldest(presences) {
55510
+ if (presences.size <= 32) return presences;
55511
+ const ordered = [...presences.values()].sort((a, b) => b.lastAt - a.lastAt);
55512
+ const kept = /* @__PURE__ */ new Map();
55513
+ for (const p of ordered.slice(0, 32)) kept.set(p.trackId, p);
55514
+ return kept;
55515
+ }
55206
55516
  //#endregion
55207
55517
  //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
55208
55518
  /**
@@ -56678,6 +56988,61 @@ function classifyTrackAppearance(input) {
56678
56988
  if (input.inPrevActive) return "continuing";
56679
56989
  return input.positionsCount > 1 ? "resurrection" : "birth";
56680
56990
  }
56991
+ //#endregion
56992
+ //#region src/pipeline-analytics/pipeline/archive-debug-note.ts
56993
+ /**
56994
+ * Archive the note a clear is about to destroy. Never throws.
56995
+ *
56996
+ * **Which way this fails, stated once.** The operator pressed a flag button;
56997
+ * the archive is this system's own bookkeeping. So a failing archive does NOT
56998
+ * block the flag write — the button must do what it says even when SQLite is
56999
+ * busy — and it does NOT retry, because the caller is a synchronous cap method
57000
+ * an operator is waiting on.
57001
+ *
57002
+ * What it must never do is lose the sentence quietly. The failure line carries
57003
+ * the NOTE TEXT verbatim, plus `tags: { deviceId }` and the trackId, so the
57004
+ * words are recoverable from the log by hand and the loss is attributable to
57005
+ * one camera. A branch that drops work silently reads as "never happened", and
57006
+ * the 3-hour media blackout produced not one line.
57007
+ *
57008
+ * An EMPTY note archives nothing: there is nothing to keep, and a row of `''`
57009
+ * would be a permanent entry in a corpus meant to be mined for sentences.
57010
+ */
57011
+ async function archiveNoteBeforeClear(input) {
57012
+ const note = input.current.debugNote;
57013
+ if (note.trim() === "") return;
57014
+ if (input.archive === void 0) {
57015
+ input.logger.warn("debug note dropped — no archive is wired on this runner", {
57016
+ tags: { deviceId: input.deviceId },
57017
+ meta: {
57018
+ deviceId: input.deviceId,
57019
+ trackId: input.trackId,
57020
+ note
57021
+ }
57022
+ });
57023
+ return;
57024
+ }
57025
+ try {
57026
+ await input.archive.append({
57027
+ deviceId: input.deviceId,
57028
+ sourceTrackId: input.trackId,
57029
+ note,
57030
+ ...input.current.startedAt !== void 0 ? { trackStartedAt: input.current.startedAt } : {},
57031
+ ...input.current.className !== void 0 ? { trackClass: input.current.className } : {},
57032
+ ...input.current.label !== void 0 ? { trackLabel: input.current.label } : {}
57033
+ });
57034
+ } catch (err) {
57035
+ input.logger.error("failed to archive a debug note — the text is in this line", {
57036
+ tags: { deviceId: input.deviceId },
57037
+ meta: {
57038
+ deviceId: input.deviceId,
57039
+ trackId: input.trackId,
57040
+ note,
57041
+ error: err instanceof Error ? err.message : String(err)
57042
+ }
57043
+ });
57044
+ }
57045
+ }
56681
57046
  /** Thrown when the device is already holding its full staging budget. Distinct
56682
57047
  * type so a surface can tell "you are out of room" from "that track is gone". */
56683
57048
  var StagingBudgetExceededError = class extends Error {
@@ -56718,7 +57083,10 @@ var TrackAlreadyTrainedError = class extends Error {
56718
57083
  * caller sent with it. A note surviving the flag is a leftover that reads
56719
57084
  * as a live request, and the invariant belongs here rather than in the
56720
57085
  * three surfaces that write this patch — one of which is a browser grid
56721
- * that has no note UI at all and would never think to clear it.
57086
+ * that has no note UI at all and would never think to clear it. Since
57087
+ * D405 the clear is not a DELETE: `applyTrackFlags` ARCHIVES the note
57088
+ * first, into a table that outlives the track. The invariant above is
57089
+ * unchanged — only where the words go.
56722
57090
  * 2. **No note in the patch → nothing is written.** This is what makes a
56723
57091
  * cancelled prompt free: the app turns debug on, the operator declines to
56724
57092
  * type, and the flag still lands. Refusing to write a note must never cost
@@ -56812,6 +57180,13 @@ async function applyTrackFlags(deps, input) {
56812
57180
  ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
56813
57181
  ...resolveNotePatch(deps, input, current)
56814
57182
  };
57183
+ if (patch.debugNote === "" && current.debugNote !== "") await archiveNoteBeforeClear({
57184
+ deviceId: input.deviceId,
57185
+ trackId: input.trackId,
57186
+ logger,
57187
+ archive: deps.noteArchive,
57188
+ current
57189
+ });
56815
57190
  if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
56816
57191
  const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
56817
57192
  return {
@@ -59493,6 +59868,261 @@ var IdentityStore = class {
59493
59868
  }
59494
59869
  };
59495
59870
  //#endregion
59871
+ //#region src/pipeline-analytics/store/debug-note-archive-store.ts
59872
+ /**
59873
+ * DebugNoteArchiveStore — the durable corpus of what operators asked to be
59874
+ * checked (D405).
59875
+ *
59876
+ * A declared SQL-backed collection, like the events ops-log next door, and for
59877
+ * the same reason: pipeline-analytics already owns SQLite collections, and an
59878
+ * undeclared collection crash-loops the runner. MUST be declared in
59879
+ * `onInitialize` before the first write.
59880
+ *
59881
+ * Collection name: pipeline-analytics:debug-note-archive
59882
+ *
59883
+ * Two properties separate it from every other table in this addon:
59884
+ *
59885
+ * - **It is not track-owned.** It names a track in `sourceTrackId` and that is
59886
+ * PROVENANCE, never ownership — the whole point is that the row outlives the
59887
+ * track, which `debug` deliberately never pinned (D353). It is classified
59888
+ * `never-orphaned` by hand in `collection-classification.ts` so the
59889
+ * ownership derivation cannot see the column and cascade it away.
59890
+ * - **It is bounded by COUNT, not by age.** An age sweep would delete the
59891
+ * corpus for being old, which is the failure this table exists to fix: a
59892
+ * six-month-old note is the most valuable row in it, because the pattern it
59893
+ * describes has had time to repeat. See {@link MAX_ARCHIVED_DEBUG_NOTES}.
59894
+ */
59895
+ /**
59896
+ * @durable class=ledger owner=pipeline-analytics
59897
+ * write="one row per debug note RETIRED by a review — appended by
59898
+ * `applyTrackFlags` immediately before `debug: false` clears the note off
59899
+ * the track row (D405). Idempotent per (sourceTrackId, note): the row id is
59900
+ * derived from the pair, so reviewing the same track twice cannot double
59901
+ * the corpus. An empty note writes nothing."
59902
+ * retention="NOT track retention and NOT an age clock — deleting a note for
59903
+ * being old is the failure this table fixes. Bounded by ROW COUNT
59904
+ * (MAX_ARCHIVED_DEBUG_NOTES, 5000, fleet-wide), oldest `archivedAt` first,
59905
+ * trimmed on append. ~3 MB of text at the ceiling."
59906
+ */
59907
+ var DEBUG_NOTE_ARCHIVE_COLLECTION = "pipeline-analytics:debug-note-archive";
59908
+ var DEBUG_NOTE_ARCHIVE_COLUMNS = [
59909
+ {
59910
+ name: "id",
59911
+ type: "TEXT",
59912
+ primaryKey: true,
59913
+ notNull: true
59914
+ },
59915
+ {
59916
+ name: "archivedAt",
59917
+ type: "INTEGER",
59918
+ notNull: true
59919
+ },
59920
+ {
59921
+ name: "deviceId",
59922
+ type: "INTEGER",
59923
+ notNull: true
59924
+ },
59925
+ {
59926
+ name: "sourceTrackId",
59927
+ type: "TEXT",
59928
+ notNull: true
59929
+ },
59930
+ {
59931
+ name: "note",
59932
+ type: "TEXT",
59933
+ notNull: true
59934
+ },
59935
+ {
59936
+ name: "trackStartedAt",
59937
+ type: "INTEGER"
59938
+ },
59939
+ {
59940
+ name: "trackClass",
59941
+ type: "TEXT"
59942
+ },
59943
+ {
59944
+ name: "trackLabel",
59945
+ type: "TEXT"
59946
+ }
59947
+ ];
59948
+ var DEBUG_NOTE_ARCHIVE_INDEXES = [{
59949
+ name: "idx_debugnote_archived_at",
59950
+ columns: ["archivedAt"]
59951
+ }, {
59952
+ name: "idx_debugnote_device_at",
59953
+ columns: ["deviceId", "archivedAt"]
59954
+ }];
59955
+ /**
59956
+ * The row id for one `(track, note)` pair.
59957
+ *
59958
+ * Deterministic on purpose: the idempotency the operator needs is "reviewing
59959
+ * twice does not double the row", and a derived primary key gets it from the
59960
+ * database rather than from a read-modify-write that two concurrent reviews
59961
+ * could interleave through. Truncated to 32 hex characters — 128 bits over a
59962
+ * table capped at 5 000 rows.
59963
+ */
59964
+ function archiveRowId(sourceTrackId, note) {
59965
+ return createHash("sha256").update(`${sourceTrackId}${note}`).digest("hex").slice(0, 32);
59966
+ }
59967
+ var DebugNoteArchiveStore = class {
59968
+ store;
59969
+ logger;
59970
+ now;
59971
+ maxRows;
59972
+ constructor(deps) {
59973
+ this.store = deps.store;
59974
+ this.logger = deps.logger;
59975
+ this.now = deps.now ?? (() => Date.now());
59976
+ this.maxRows = deps.maxRows ?? 5e3;
59977
+ }
59978
+ /** The ceiling a store built without an override enforces. */
59979
+ static defaultMaxRows() {
59980
+ return MAX_ARCHIVED_DEBUG_NOTES;
59981
+ }
59982
+ static async declare(store) {
59983
+ await store.declareCollection.mutate({
59984
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
59985
+ columns: [...DEBUG_NOTE_ARCHIVE_COLUMNS],
59986
+ indexes: [...DEBUG_NOTE_ARCHIVE_INDEXES]
59987
+ });
59988
+ }
59989
+ /**
59990
+ * Append one retired note. THROWS if the row cannot be written.
59991
+ *
59992
+ * Deliberately not best-effort, unlike the ops-log's `append`: this is the
59993
+ * only copy of something a person typed, and the caller
59994
+ * (`archiveNoteBeforeClear`) has both the text and the camera and is the
59995
+ * right place to decide what a failure costs — it logs the note verbatim and
59996
+ * lets the operator's flag write proceed. Swallowing here would hide the
59997
+ * failure from the one caller able to preserve the words.
59998
+ *
59999
+ * The TRIM is the exception and is best-effort: the row has already landed,
60000
+ * and failing the append because housekeeping could not run would make the
60001
+ * caller report a note lost that is sitting in the table.
60002
+ */
60003
+ async append(input) {
60004
+ const note = input.note.trim() === "" ? "" : input.note;
60005
+ if (note === "") return {
60006
+ archived: false,
60007
+ id: null
60008
+ };
60009
+ const id = archiveRowId(input.sourceTrackId, note);
60010
+ if ((await this.store.query.query({
60011
+ collection: "pipeline-analytics:debug-note-archive",
60012
+ filter: {
60013
+ where: { id },
60014
+ limit: 1
60015
+ }
60016
+ })).length > 0) return {
60017
+ archived: false,
60018
+ id
60019
+ };
60020
+ const row = ArchivedDebugNoteSchema.parse({
60021
+ id,
60022
+ note,
60023
+ deviceId: input.deviceId,
60024
+ sourceTrackId: input.sourceTrackId,
60025
+ archivedAt: this.now(),
60026
+ trackStartedAt: input.trackStartedAt ?? null,
60027
+ trackClass: input.trackClass ?? null,
60028
+ trackLabel: input.trackLabel ?? null
60029
+ });
60030
+ await this.store.insert.mutate({
60031
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60032
+ record: {
60033
+ id: row.id,
60034
+ data: {
60035
+ note: row.note,
60036
+ deviceId: row.deviceId,
60037
+ sourceTrackId: row.sourceTrackId,
60038
+ archivedAt: row.archivedAt,
60039
+ trackStartedAt: row.trackStartedAt,
60040
+ trackClass: row.trackClass,
60041
+ trackLabel: row.trackLabel
60042
+ }
60043
+ }
60044
+ });
60045
+ await this.trim();
60046
+ return {
60047
+ archived: true,
60048
+ id: row.id
60049
+ };
60050
+ }
60051
+ /**
60052
+ * Hold the table at its ceiling, oldest first. Best-effort by design — see
60053
+ * {@link append}.
60054
+ *
60055
+ * NOT an age-keyed sweep and must never become one: the boundary below is
60056
+ * computed from the POSITION of the excess rows, never from a clock, so a
60057
+ * quiet fleet keeps its whole corpus for ever and a busy one drops its oldest
60058
+ * questions. Rows sharing the boundary timestamp go together; at a
60059
+ * one-per-review write rate that is a rounding error, and the alternative —
60060
+ * deleting by id, one round trip per row — is the N+1 drain this store layer
60061
+ * spent a release removing.
60062
+ */
60063
+ async trim() {
60064
+ try {
60065
+ const held = await this.store.count.query({ collection: DEBUG_NOTE_ARCHIVE_COLLECTION });
60066
+ const excess = held - this.maxRows;
60067
+ if (excess <= 0) return;
60068
+ const boundary = (await this.store.query.query({
60069
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60070
+ filter: {
60071
+ orderBy: {
60072
+ field: "archivedAt",
60073
+ direction: "asc"
60074
+ },
60075
+ limit: excess
60076
+ },
60077
+ columns: ["archivedAt"]
60078
+ })).reduce((acc, r) => {
60079
+ const at = r.data["archivedAt"];
60080
+ return typeof at === "number" && at > acc ? at : acc;
60081
+ }, 0);
60082
+ if (boundary === 0) return;
60083
+ const { deleted } = await this.store.deleteWhere.mutate({
60084
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60085
+ filter: { whereBetween: { archivedAt: [0, boundary] } }
60086
+ });
60087
+ this.logger.info("debug-note archive trimmed to its ceiling", { meta: {
60088
+ held,
60089
+ ceiling: this.maxRows,
60090
+ deleted
60091
+ } });
60092
+ } catch (err) {
60093
+ this.logger.warn("debug-note archive trim failed — the archive will keep growing", { meta: {
60094
+ ceiling: this.maxRows,
60095
+ error: err instanceof Error ? err.message : String(err)
60096
+ } });
60097
+ }
60098
+ }
60099
+ /** Archived notes, newest first, optionally scoped to one camera. */
60100
+ async list(query) {
60101
+ const filter = {
60102
+ orderBy: {
60103
+ field: "archivedAt",
60104
+ direction: "desc"
60105
+ },
60106
+ limit: query.limit ?? 200
60107
+ };
60108
+ if (query.deviceId !== void 0) filter["where"] = { deviceId: query.deviceId };
60109
+ const rows = await this.store.query.query({
60110
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
60111
+ filter
60112
+ });
60113
+ const out = [];
60114
+ for (const r of rows) {
60115
+ const parsed = ArchivedDebugNoteSchema.safeParse({
60116
+ id: r.id,
60117
+ ...r.data
60118
+ });
60119
+ if (parsed.success) out.push(parsed.data);
60120
+ else this.logger.debug("debug-note archive: skipped a malformed row", { meta: { id: r.id } });
60121
+ }
60122
+ return out;
60123
+ }
60124
+ };
60125
+ //#endregion
59496
60126
  //#region src/pipeline-analytics/store/ops-log-store.ts
59497
60127
  /**
59498
60128
  * OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
@@ -60856,6 +61486,13 @@ var ANALYTICS_COLLECTIONS = [
60856
61486
  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."
60857
61487
  }
60858
61488
  },
61489
+ {
61490
+ collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
61491
+ classification: {
61492
+ kind: "never-orphaned",
61493
+ 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."
61494
+ }
61495
+ },
60859
61496
  {
60860
61497
  collection: STATIONARY_COLLECTION,
60861
61498
  classification: {
@@ -68626,6 +69263,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68626
69263
  eventStore = null;
68627
69264
  /** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
68628
69265
  eventsOpsLog = null;
69266
+ /**
69267
+ * The archived operator debug notes (D405). Null until onInitialize — and
69268
+ * `applyTrackFlags` treats null as "no archive wired": the flag write the
69269
+ * operator asked for never depends on this addon's own bookkeeping.
69270
+ */
69271
+ debugNoteArchive = null;
68629
69272
  /** Event-media relocation engine (entity-routing Phase 4). */
68630
69273
  mediaRelocate = null;
68631
69274
  mediaLocationStorage = null;
@@ -69183,6 +69826,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69183
69826
  * which would let the streetlight wedge vouch for its own delivery.
69184
69827
  */
69185
69828
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
69829
+ /**
69830
+ * WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
69831
+ * minutes after the fact, so the collector must be remembered while it is
69832
+ * still true — handles only, never pixels.
69833
+ */
69834
+ packageCollectors = new PackageCollectorMirror();
69186
69835
  /** Last time the oversize-sighting drop was logged per device — one line
69187
69836
  * per 5 minutes, not one per governor round (the wedge re-sights all
69188
69837
  * night; ~300 identical lines would bury the log it exists to serve). */
@@ -69608,6 +70257,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69608
70257
  logger: logger.child("ops-log"),
69609
70258
  nodeId: ownNodeId
69610
70259
  });
70260
+ this.debugNoteArchive = new DebugNoteArchiveStore({
70261
+ store: api.settingsStore,
70262
+ logger: logger.child("debug-note-archive")
70263
+ });
69611
70264
  {
69612
70265
  const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
69613
70266
  this.isPostProcessingNode = ownNodeId === designated;
@@ -69927,6 +70580,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69927
70580
  RetrainFrameStore.declare(api.settingsStore),
69928
70581
  RetrainAnnotationStore.declare(api.settingsStore),
69929
70582
  OpsLogStore.declare(api.settingsStore),
70583
+ DebugNoteArchiveStore.declare(api.settingsStore),
69930
70584
  AnalyticsLts.declare(api.settingsStore),
69931
70585
  SceneStore.declare(api.settingsStore),
69932
70586
  NotificationCenter.declare(api.settingsStore),
@@ -71565,6 +72219,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71565
72219
  this.packageStillness.forgetDevice(data.deviceId);
71566
72220
  this.packageDropDetector?.forgetDevice(data.deviceId);
71567
72221
  this.lastNonPackageActivityAt.delete(data.deviceId);
72222
+ this.packageCollectors.forgetDevice(data.deviceId);
71568
72223
  this.packageOversizeLogAt.delete(data.deviceId);
71569
72224
  this.overlayState.clearDevice(data.deviceId);
71570
72225
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -71584,6 +72239,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71584
72239
  this.packageStillness.forgetDevice(deviceId);
71585
72240
  this.packageDropDetector?.forgetDevice(deviceId);
71586
72241
  this.lastNonPackageActivityAt.delete(deviceId);
72242
+ this.packageCollectors.forgetDevice(deviceId);
71587
72243
  this.packageOversizeLogAt.delete(deviceId);
71588
72244
  this.overlayState.clearDevice(deviceId);
71589
72245
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -72467,12 +73123,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72467
73123
  this.residents.markFirstFramePending(deviceId, id);
72468
73124
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
72469
73125
  this.residents.setLastFrameAt(deviceId, id, result.timestamp);
72470
- if (frameHandle) firstFrameTargets.push({
73126
+ const birthCapture = decideBirthFirstFrameCapture({
73127
+ hasFrameHandle: frameHandle !== void 0,
73128
+ matchedThisFrame: t.matchedThisFrame
73129
+ });
73130
+ if (birthCapture.capture) firstFrameTargets.push({
72471
73131
  trackId: id,
72472
73132
  timestamp: result.timestamp,
72473
73133
  bbox: { ...t.bbox },
72474
73134
  ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {}
72475
73135
  });
73136
+ else log.info("birth firstFrame not captured — retry armed", {
73137
+ tags: { deviceId },
73138
+ meta: {
73139
+ trackId: id,
73140
+ className: t.className,
73141
+ source,
73142
+ refusedBecause: birthCapture.refusedBecause,
73143
+ deferredMs: result.timestamp - (this.trackStore?.peekActive(id)?.firstSeen ?? result.timestamp)
73144
+ }
73145
+ });
72476
73146
  }
72477
73147
  this.ctx.eventBus.emit({
72478
73148
  id: `pa-${randomUUID()}`,
@@ -75182,6 +75852,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75182
75852
  this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
75183
75853
  break;
75184
75854
  }
75855
+ this.packageCollectors.noteFrame({
75856
+ deviceId: input.deviceId,
75857
+ tracked: input.tracked,
75858
+ packageClasses: input.packageClasses,
75859
+ packageZoneIds: input.packageZoneIds,
75860
+ timestamp: input.timestamp,
75861
+ keyFrameMediaKeyFor: (trackId) => this.residents.keyFrameKey(trackId)
75862
+ });
75185
75863
  const sightings = [];
75186
75864
  for (const t of input.tracked) {
75187
75865
  if (!input.packageClasses.has(t.className)) continue;
@@ -75293,10 +75971,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75293
75971
  meta: { entryId: entry.id }
75294
75972
  });
75295
75973
  }).catch(() => void 0);
75974
+ const collector = this.packageCollectors.pickCollector({
75975
+ deviceId: input.deviceId,
75976
+ pickupAt: pickupInstantOf(entry)
75977
+ });
75978
+ if (collector === null) this.ctx.logger.info("package pick-up has no collector in the drop-off zone", {
75979
+ tags,
75980
+ meta: {
75981
+ entryId: entry.id,
75982
+ pickupAt: entry.lastConfirmedAt,
75983
+ departedAt: input.timestamp,
75984
+ presencesKnown: this.packageCollectors.debugPresenceCount(input.deviceId)
75985
+ }
75986
+ });
75296
75987
  this.packageDropDetector.onStationaryChange({
75297
75988
  phase: "departed",
75298
75989
  entry,
75299
- timestamp: input.timestamp
75990
+ timestamp: input.timestamp,
75991
+ ...collector !== null ? { collector } : {}
75300
75992
  });
75301
75993
  }
75302
75994
  for (const entry of outcome.departedWithoutActivity) {
@@ -77421,7 +78113,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77421
78113
  setFlags: (trackId, patch) => trackStore.setFlags(trackId, patch),
77422
78114
  countStaging: (query) => trackStore.countStaging(query)
77423
78115
  },
77424
- logger: this.ctx.logger
78116
+ logger: this.ctx.logger,
78117
+ ...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
77425
78118
  }, input);
77426
78119
  this.ctx.logger.info("track operator flags set", {
77427
78120
  tags: { deviceId: input.deviceId },
@@ -77552,6 +78245,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77552
78245
  return this.queryFacade.listOpsLog(input);
77553
78246
  }
77554
78247
  /**
78248
+ * The archived debug notes, newest first (D405).
78249
+ *
78250
+ * Answers from the archive alone — never from the track rows. The point of
78251
+ * the table is that the tracks these notes were written on are gone, so a
78252
+ * read that joined them would return exactly the rows the archive exists to
78253
+ * replace. An archive that has not been built yet answers EMPTY, which is
78254
+ * true: nothing has been archived on this runner.
78255
+ */
78256
+ async listArchivedDebugNotes(input) {
78257
+ return await this.debugNoteArchive?.list(input) ?? [];
78258
+ }
78259
+ /**
77555
78260
  * Track-centric time-based retention (design §5.1). Drains every persisted
77556
78261
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
77557
78262
  * widened cascade — enrolled faces/plates + identity media are exempt (design