@camstack/addon-post-analysis 1.2.208 → 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-CoLA3NHh.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,
@@ -7520,7 +7520,8 @@ function cooldownKey(rule, subject) {
7520
7520
  const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
7521
7521
  const scopeRef = subject.systemEvent !== void 0 ? systemEventCooldownScope(subject.systemEvent) : `d:${subject.deviceId}`;
7522
7522
  const kindKey = subject.systemEvent !== void 0 ? `:k:${subject.systemEvent.kind}` : "";
7523
- return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${classKey}` : `r:${rule.id}:${scopeRef}${classKey}`;
7523
+ const phaseKey = subject.packagePhase !== void 0 ? `:p:${subject.packagePhase}` : "";
7524
+ return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${phaseKey}${classKey}` : `r:${rule.id}:${scopeRef}${phaseKey}${classKey}`;
7524
7525
  }
7525
7526
  /** True when the rule fired within its cooldown window before `now`. */
7526
7527
  function isCoolingDown(rule, lastFiredAt, now) {
@@ -9957,6 +9958,10 @@ var en_default = {
9957
9958
  "one": "{{count}} other {{classNoun}}",
9958
9959
  "other": "{{count}} other {{classNoun}}"
9959
9960
  },
9961
+ "package.delivered.title": "Parcel delivered",
9962
+ "package.delivered.body": "Delivered at {{camera}}{{inZones}}",
9963
+ "package.picked-up.title": "Parcel collected",
9964
+ "package.picked-up.body": "Collected from {{camera}}{{inZones}}",
9960
9965
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
9961
9966
  "occupancy.op.occupied": "occupied",
9962
9967
  "occupancy.op.free": "free",
@@ -10208,6 +10213,10 @@ var it_default = {
10208
10213
  "one": "{{count}} altro {{classNoun}}",
10209
10214
  "other": "{{count}} altri {{classNoun}}"
10210
10215
  },
10216
+ "package.delivered.title": "Pacco consegnato",
10217
+ "package.delivered.body": "Consegnato su {{camera}}{{inZones}}",
10218
+ "package.picked-up.title": "Pacco ritirato",
10219
+ "package.picked-up.body": "Ritirato da {{camera}}{{inZones}}",
10211
10220
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
10212
10221
  "occupancy.op.occupied": "occupata",
10213
10222
  "occupancy.op.free": "libera",
@@ -12050,6 +12059,7 @@ function bodyKeyFor(entry) {
12050
12059
  const subject = entry.payload.subject;
12051
12060
  if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
12052
12061
  if (subject.occupancy !== void 0) return "occupancy.body";
12062
+ if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
12053
12063
  if (entry.payload.enhancement === true) return "detection.body.enhanced";
12054
12064
  return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
12055
12065
  }
@@ -12057,6 +12067,8 @@ function bodyKeyFor(entry) {
12057
12067
  function titleKeyFor(entry) {
12058
12068
  const systemEvent = entry.payload.subject.systemEvent;
12059
12069
  if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
12070
+ const packagePhase = entry.payload.subject.packagePhase;
12071
+ if (packagePhase !== void 0) return `package.${packagePhase}.title`;
12060
12072
  return "detection.title";
12061
12073
  }
12062
12074
  /**
@@ -24856,6 +24868,7 @@ var NotificationCenter = class NotificationCenter {
24856
24868
  zones: renderedZones,
24857
24869
  timestamp: subject.timestamp,
24858
24870
  ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
24871
+ ...subject.packagePhase !== void 0 ? { packagePhase: subject.packagePhase } : {},
24859
24872
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
24860
24873
  }
24861
24874
  };
@@ -28620,6 +28633,37 @@ var MediaStore = class {
28620
28633
  return newKey;
28621
28634
  }
28622
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
+ /**
28623
28667
  * Fetch one media entry by its key (id). Returns null if the key is not
28624
28668
  * found in the index or if the blob is missing from storage.
28625
28669
  */
@@ -30492,6 +30536,17 @@ function trackDurationMs(facts) {
30492
30536
  function hasKind(media, kind) {
30493
30537
  return media.some((m) => m.kind === kind);
30494
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
+ }
30495
30550
  function latestMediaAt(media) {
30496
30551
  let latest = null;
30497
30552
  for (const m of media) if (latest === null || m.timestamp > latest) latest = m.timestamp;
@@ -30539,6 +30594,12 @@ function analyseDebugTrack(inventory) {
30539
30594
  const lag = facts.lastSeen - lastFrameAt;
30540
30595
  if (lag > 2e3) out.push(finding("LASTFRAME_STALE", "warn", `lastFrame is ${String(lag)} ms older than the track's end.`));
30541
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
+ }
30542
30603
  if (closed && durationMs >= 1e4 && media.length > 0) {
30543
30604
  const latest = latestMediaAt(media);
30544
30605
  if (latest !== null) {
@@ -32769,9 +32830,14 @@ var FaceStore = class {
32769
32830
  * with that person named. High means "this looks like someone we can name",
32770
32831
  * which is what a mis-enrolment actually looks like.
32771
32832
  *
32772
- * `suspicion` combines them so a sample that looks like somebody else outranks
32773
- * one that merely looks like nobody: the first is a fixable error, the second
32774
- * is often just a difficult photograph.
32833
+ * `suspicion` is the EXCESS `bestForeign selfMean` because that is the
32834
+ * only one of the two that discriminates on real data. Measured on Roberta's
32835
+ * 106 samples, 2026-09-08: `selfMean` has a median of 0.188 and a p90 of 0.235,
32836
+ * so ANY absolute reading of "does not look like itself" fires on the whole
32837
+ * gallery. The first version of this module scored `(1 − selfMean) / 2` and
32838
+ * flagged 56 of 106 — a finding that fires on healthy samples means nothing.
32839
+ * `bestForeign` on the same gallery: median 0.283, p95 0.376, and one single
32840
+ * outlier at 0.612, which is the mis-enrolment.
32775
32841
  *
32776
32842
  * ## What it refuses to do
32777
32843
  *
@@ -32786,8 +32852,20 @@ var FaceStore = class {
32786
32852
  *
32787
32853
  * Pure — the caller loads the vectors and performs the removal.
32788
32854
  */
32789
- /** Above this a row is worth the operator's attention. */
32790
- var SUSPICION_FLAG = .5;
32855
+ /**
32856
+ * A row is FLAGGED when its best foreign match would auto-assign the face to
32857
+ * that other person — i.e. it reaches the recogniser's own
32858
+ * `similarityThreshold`.
32859
+ *
32860
+ * Tied to the live threshold rather than to a constant of its own, because
32861
+ * that is what the flag MEANS: this sample can steal a match. A second number
32862
+ * invented here would drift from the one the recogniser actually uses, and the
32863
+ * panel would accuse samples that cannot hurt anyone while staying quiet about
32864
+ * ones that can. On the live gallery (threshold 0.40) it flags 2 of 106.
32865
+ */
32866
+ function flaggedCount(rows, autoAssignThreshold) {
32867
+ return rows.filter((row) => (row.bestForeign?.score ?? 0) >= autoAssignThreshold).length;
32868
+ }
32791
32869
  /**
32792
32870
  * How far a foreign match has to stand OUT to count as looking like someone
32793
32871
  * else: `bestForeign - selfMean`. A sample that scores 0.6 against a stranger
@@ -32842,8 +32920,7 @@ function auditIdentitySamples(input) {
32842
32920
  };
32843
32921
  const selfMean = meanCosine(sample, comparable);
32844
32922
  const foreign = bestForeignMatch(sample, input.others, input.modelId);
32845
- const excess = foreignExcess(selfMean, foreign);
32846
- const suspicion = selfMean === null ? 0 : (1 - selfMean) / 2 + excess;
32923
+ const suspicion = foreignExcess(selfMean, foreign);
32847
32924
  return {
32848
32925
  sampleId: sample.sampleId,
32849
32926
  enrolledAt: sample.enrolledAt,
@@ -32854,12 +32931,15 @@ function auditIdentitySamples(input) {
32854
32931
  suspicion
32855
32932
  };
32856
32933
  });
32857
- rows.sort((a, b) => b.suspicion - a.suspicion);
32934
+ rows.sort((a, b) => {
32935
+ if (b.suspicion !== a.suspicion) return b.suspicion - a.suspicion;
32936
+ return (a.selfMean ?? Number.POSITIVE_INFINITY) - (b.selfMean ?? Number.POSITIVE_INFINITY);
32937
+ });
32858
32938
  return {
32859
32939
  identityId: input.identityId,
32860
32940
  totalSamples: input.samples.length,
32861
32941
  comparableSamples: comparable.length,
32862
- flagged: rows.filter((r) => r.suspicion >= SUSPICION_FLAG).length,
32942
+ flagged: flaggedCount(rows, input.autoAssignThreshold),
32863
32943
  rows
32864
32944
  };
32865
32945
  }
@@ -32916,6 +32996,7 @@ var FaceGalleryProvider = class {
32916
32996
  logger;
32917
32997
  refreshGallery;
32918
32998
  resolveClusterModelId;
32999
+ resolveAutoAssignThreshold;
32919
33000
  eventMediaBaseUrl;
32920
33001
  eventMediaPathPrefix;
32921
33002
  emitFaceGalleryChanged;
@@ -32928,6 +33009,7 @@ var FaceGalleryProvider = class {
32928
33009
  this.logger = deps.logger;
32929
33010
  this.refreshGallery = deps.refreshGallery;
32930
33011
  this.resolveClusterModelId = deps.resolveClusterModelId;
33012
+ this.resolveAutoAssignThreshold = deps.resolveAutoAssignThreshold;
32931
33013
  this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
32932
33014
  this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
32933
33015
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
@@ -33019,6 +33101,7 @@ var FaceGalleryProvider = class {
33019
33101
  async auditIdentitySamples(input) {
33020
33102
  const modelId = await this.resolveClusterModelId();
33021
33103
  if (modelId === null) throw new Error("auditIdentitySamples: the cluster face-embedding model could not be resolved");
33104
+ const autoAssignThreshold = await this.resolveAutoAssignThreshold();
33022
33105
  const identities = await this.identityStore.listIdentities();
33023
33106
  const target = identities.find((i) => i.id === input.identityId);
33024
33107
  if (target === void 0) throw new Error(`auditIdentitySamples: no identity ${input.identityId}`);
@@ -33044,7 +33127,8 @@ var FaceGalleryProvider = class {
33044
33127
  name: identity.name,
33045
33128
  samples: inputsFor(identity.id)
33046
33129
  })),
33047
- modelId
33130
+ modelId,
33131
+ autoAssignThreshold
33048
33132
  });
33049
33133
  const meta = new Map((samplesByIdentity.get(input.identityId) ?? []).map((sample) => [sample.id, sample]));
33050
33134
  return {
@@ -33054,7 +33138,7 @@ var FaceGalleryProvider = class {
33054
33138
  totalSamples: report.totalSamples,
33055
33139
  comparableSamples: report.comparableSamples,
33056
33140
  flagged: report.flagged,
33057
- flagThreshold: SUSPICION_FLAG,
33141
+ flagThreshold: autoAssignThreshold,
33058
33142
  rows: report.rows.map((row) => {
33059
33143
  const sample = meta.get(row.sampleId);
33060
33144
  return {
@@ -34115,6 +34199,29 @@ function firstFrameRetryBbox(current, birth) {
34115
34199
  return { ...current };
34116
34200
  }
34117
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
34118
34225
  //#region src/pipeline-analytics/pipeline/events/event-filter.ts
34119
34226
  var DEFAULT_EVENT_EMITTER_CONFIG = {
34120
34227
  minTrackAge: 3,
@@ -34723,7 +34830,7 @@ var DEFAULT_TRACKER_CONFIG = {
34723
34830
  var PERSON_CLASS = "person";
34724
34831
  var ANIMAL_CLASS = "animal";
34725
34832
  var MAX_PATH_LENGTH = 300;
34726
- function clamp(value, min, max) {
34833
+ function clamp$1(value, min, max) {
34727
34834
  return Math.max(min, Math.min(max, value));
34728
34835
  }
34729
34836
  function iou$3(a, b) {
@@ -35163,8 +35270,8 @@ var SortTracker = class SortTracker {
35163
35270
  const perFrameX = t.velocity.dx * t.emitIntervalMs;
35164
35271
  const perFrameY = t.velocity.dy * t.emitIntervalMs;
35165
35272
  return {
35166
- x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
35167
- 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),
35168
35275
  w: t.bbox.w,
35169
35276
  h: t.bbox.h
35170
35277
  };
@@ -35173,8 +35280,8 @@ var SortTracker = class SortTracker {
35173
35280
  const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
35174
35281
  const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
35175
35282
  return {
35176
- x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
35177
- 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),
35178
35285
  w: t.bbox.w,
35179
35286
  h: t.bbox.h
35180
35287
  };
@@ -37251,6 +37358,44 @@ function decideLastFramePromotion(media) {
37251
37358
  };
37252
37359
  }
37253
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
37254
37399
  //#region src/pipeline-analytics/pipeline/static-track-gate.ts
37255
37400
  /**
37256
37401
  * Net displacement + path span for a track's centroid path, normalized to
@@ -37393,6 +37538,7 @@ var TrackCloser = class {
37393
37538
  tile = "raster";
37394
37539
  }
37395
37540
  await this.maybePromoteLastFrame(t, ownedMedia);
37541
+ await this.repairFirstLastFrameOrder(t);
37396
37542
  const hasRasterFallback = closure?.rasterFallback !== void 0;
37397
37543
  let deriveMiss;
37398
37544
  if (!thumbnailLanded) {
@@ -37736,6 +37882,56 @@ var TrackCloser = class {
37736
37882
  }
37737
37883
  }
37738
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
+ /**
37739
37935
  * Key-event importance: score the just-expired track and persist. Peak
37740
37936
  * confidence comes from the shared best-detection tracker; peak bbox area
37741
37937
  * + the winning object-event id from one indexed queryObject(trackId) —
@@ -41551,7 +41747,10 @@ var TrackStore = class {
41551
41747
  retrainStatus: active.retrainStatus ?? "none",
41552
41748
  debug: active.debug === true,
41553
41749
  debugNote: active.debugNote ?? "",
41554
- 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) } : {}
41555
41754
  };
41556
41755
  const persisted = await this.getPersistedByTrackId(trackId);
41557
41756
  if (!persisted) return null;
@@ -41559,7 +41758,10 @@ var TrackStore = class {
41559
41758
  retrainStatus: persisted.retrainStatus ?? "none",
41560
41759
  debug: persisted.debug === true,
41561
41760
  debugNote: persisted.debugNote ?? "",
41562
- 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) } : {}
41563
41765
  };
41564
41766
  }
41565
41767
  /**
@@ -54987,7 +55189,7 @@ var PackageDropDetector = class {
54987
55189
  async onStationaryChange(change) {
54988
55190
  try {
54989
55191
  if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
54990
- else await this.onDeparted(change.entry, change.timestamp);
55192
+ else await this.onDeparted(change);
54991
55193
  } catch (err) {
54992
55194
  this.deps.onError?.("onStationaryChange", err);
54993
55195
  this.deps.logger.warn("package-drop detector failed on change", {
@@ -55165,7 +55367,9 @@ var PackageDropDetector = class {
55165
55367
  }
55166
55368
  });
55167
55369
  }
55168
- async onDeparted(entry, timestamp) {
55370
+ async onDeparted(change) {
55371
+ const entry = change.entry;
55372
+ const timestamp = change.timestamp;
55169
55373
  if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) {
55170
55374
  this.deps.logger.debug("package pick-up skipped — packageDropPickupEnabled is false", {
55171
55375
  tags: { deviceId: entry.deviceId },
@@ -55200,13 +55404,25 @@ var PackageDropDetector = class {
55200
55404
  });
55201
55405
  return;
55202
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
+ });
55203
55419
  const ev = {
55204
55420
  id: pickedUpId,
55205
55421
  kind: "object",
55206
55422
  deviceId: entry.deviceId,
55207
55423
  timestamp,
55208
55424
  source: "pipeline",
55209
- trackId: entry.sourceTrackId ?? entry.id,
55425
+ trackId: mediaTrackId,
55210
55426
  className: PACKAGE_EVENT_CLASS,
55211
55427
  ...entry.label !== void 0 ? { label: entry.label } : {},
55212
55428
  confidence: PACKAGE_IMPORTANCE,
@@ -55220,6 +55436,7 @@ var PackageDropDetector = class {
55220
55436
  state: PICKED_UP_STATE,
55221
55437
  frameWidth: entry.frameWidth,
55222
55438
  frameHeight: entry.frameHeight,
55439
+ ...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
55223
55440
  importance: PACKAGE_IMPORTANCE
55224
55441
  };
55225
55442
  await this.deps.events.insertObject(ev);
@@ -55229,6 +55446,7 @@ var PackageDropDetector = class {
55229
55446
  entryId: entry.id,
55230
55447
  deliveredEventId: deliveredId,
55231
55448
  className: entry.className,
55449
+ ...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
55232
55450
  timestamp
55233
55451
  });
55234
55452
  this.deps.logger.info("package picked up", {
@@ -55236,11 +55454,139 @@ var PackageDropDetector = class {
55236
55454
  meta: {
55237
55455
  entryId: entry.id,
55238
55456
  eventId: pickedUpId,
55239
- 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 }
55240
55464
  }
55241
55465
  });
55242
55466
  }
55243
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
+ }
55244
55590
  //#endregion
55245
55591
  //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
55246
55592
  /**
@@ -56716,6 +57062,61 @@ function classifyTrackAppearance(input) {
56716
57062
  if (input.inPrevActive) return "continuing";
56717
57063
  return input.positionsCount > 1 ? "resurrection" : "birth";
56718
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
+ }
56719
57120
  /** Thrown when the device is already holding its full staging budget. Distinct
56720
57121
  * type so a surface can tell "you are out of room" from "that track is gone". */
56721
57122
  var StagingBudgetExceededError = class extends Error {
@@ -56756,7 +57157,10 @@ var TrackAlreadyTrainedError = class extends Error {
56756
57157
  * caller sent with it. A note surviving the flag is a leftover that reads
56757
57158
  * as a live request, and the invariant belongs here rather than in the
56758
57159
  * three surfaces that write this patch — one of which is a browser grid
56759
- * 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.
56760
57164
  * 2. **No note in the patch → nothing is written.** This is what makes a
56761
57165
  * cancelled prompt free: the app turns debug on, the operator declines to
56762
57166
  * type, and the flag still lands. Refusing to write a note must never cost
@@ -56850,6 +57254,13 @@ async function applyTrackFlags(deps, input) {
56850
57254
  ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
56851
57255
  ...resolveNotePatch(deps, input, current)
56852
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
+ });
56853
57264
  if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
56854
57265
  const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
56855
57266
  return {
@@ -57932,6 +58343,9 @@ async function reconcileGallerySamples(deps, options) {
57932
58343
  let keyless = 0;
57933
58344
  let skippedGrace = 0;
57934
58345
  let stoppedByCeiling = false;
58346
+ let missed = 0;
58347
+ /** Verdicts held until there are enough of them to tell rot from a move. */
58348
+ const held = [];
57935
58349
  const identities = await deps.listIdentities();
57936
58350
  for (const identity of identities) {
57937
58351
  const samples = await deps.listSamples(identity.id);
@@ -57976,6 +58390,43 @@ async function reconcileGallerySamples(deps, options) {
57976
58390
  }
57977
58391
  checked += 1;
57978
58392
  if (state === "exists") continue;
58393
+ missed += 1;
58394
+ const minProbes = options.massMissMinProbes ?? 8;
58395
+ const bar = options.massMissFraction ?? .5;
58396
+ if (checked < minProbes) {
58397
+ held.push({
58398
+ identityId: identity.id,
58399
+ sampleId: sample.id,
58400
+ mediaKey: sample.mediaKey
58401
+ });
58402
+ continue;
58403
+ }
58404
+ if (missed / checked >= bar) {
58405
+ const reason = `mass miss: ${String(missed)} of ${String(checked)} probes found no blob`;
58406
+ deps.logger.warn("gallery reconcile: nearly every blob is missing — that is the volume, not the samples. Aborting and keeping every row; check where the gallery location points.", { meta: {
58407
+ checked,
58408
+ missed,
58409
+ held: held.length,
58410
+ identity: identity.name
58411
+ } });
58412
+ return {
58413
+ checked,
58414
+ reclaimed,
58415
+ keyless,
58416
+ skippedGrace,
58417
+ stoppedByCeiling,
58418
+ aborted: reason
58419
+ };
58420
+ }
58421
+ for (const pending of held.splice(0, held.length)) {
58422
+ await deps.removeSample(pending.identityId, pending.sampleId);
58423
+ reclaimed += 1;
58424
+ deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
58425
+ sampleId: pending.sampleId,
58426
+ mediaKey: pending.mediaKey,
58427
+ state: "missing-blob"
58428
+ } });
58429
+ }
57979
58430
  await deps.removeSample(identity.id, sample.id);
57980
58431
  reclaimed += 1;
57981
58432
  deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
@@ -57986,6 +58437,14 @@ async function reconcileGallerySamples(deps, options) {
57986
58437
  } });
57987
58438
  }
57988
58439
  }
58440
+ for (const pending of held.splice(0, held.length)) {
58441
+ if (reclaimed >= options.maxReclaimPerRun) {
58442
+ stoppedByCeiling = true;
58443
+ break;
58444
+ }
58445
+ await deps.removeSample(pending.identityId, pending.sampleId);
58446
+ reclaimed += 1;
58447
+ }
57989
58448
  if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
57990
58449
  checked,
57991
58450
  reclaimed,
@@ -59483,6 +59942,261 @@ var IdentityStore = class {
59483
59942
  }
59484
59943
  };
59485
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
59486
60200
  //#region src/pipeline-analytics/store/ops-log-store.ts
59487
60201
  /**
59488
60202
  * OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
@@ -60846,6 +61560,13 @@ var ANALYTICS_COLLECTIONS = [
60846
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."
60847
61561
  }
60848
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
+ },
60849
61570
  {
60850
61571
  collection: STATIONARY_COLLECTION,
60851
61572
  classification: {
@@ -68616,6 +69337,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68616
69337
  eventStore = null;
68617
69338
  /** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
68618
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;
68619
69346
  /** Event-media relocation engine (entity-routing Phase 4). */
68620
69347
  mediaRelocate = null;
68621
69348
  mediaLocationStorage = null;
@@ -69173,6 +69900,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69173
69900
  * which would let the streetlight wedge vouch for its own delivery.
69174
69901
  */
69175
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();
69176
69909
  /** Last time the oversize-sighting drop was logged per device — one line
69177
69910
  * per 5 minutes, not one per governor round (the wedge re-sights all
69178
69911
  * night; ~300 identical lines would bury the log it exists to serve). */
@@ -69598,6 +70331,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69598
70331
  logger: logger.child("ops-log"),
69599
70332
  nodeId: ownNodeId
69600
70333
  });
70334
+ this.debugNoteArchive = new DebugNoteArchiveStore({
70335
+ store: api.settingsStore,
70336
+ logger: logger.child("debug-note-archive")
70337
+ });
69601
70338
  {
69602
70339
  const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
69603
70340
  this.isPostProcessingNode = ownNodeId === designated;
@@ -69917,6 +70654,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69917
70654
  RetrainFrameStore.declare(api.settingsStore),
69918
70655
  RetrainAnnotationStore.declare(api.settingsStore),
69919
70656
  OpsLogStore.declare(api.settingsStore),
70657
+ DebugNoteArchiveStore.declare(api.settingsStore),
69920
70658
  AnalyticsLts.declare(api.settingsStore),
69921
70659
  SceneStore.declare(api.settingsStore),
69922
70660
  NotificationCenter.declare(api.settingsStore),
@@ -71555,6 +72293,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71555
72293
  this.packageStillness.forgetDevice(data.deviceId);
71556
72294
  this.packageDropDetector?.forgetDevice(data.deviceId);
71557
72295
  this.lastNonPackageActivityAt.delete(data.deviceId);
72296
+ this.packageCollectors.forgetDevice(data.deviceId);
71558
72297
  this.packageOversizeLogAt.delete(data.deviceId);
71559
72298
  this.overlayState.clearDevice(data.deviceId);
71560
72299
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -71574,6 +72313,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71574
72313
  this.packageStillness.forgetDevice(deviceId);
71575
72314
  this.packageDropDetector?.forgetDevice(deviceId);
71576
72315
  this.lastNonPackageActivityAt.delete(deviceId);
72316
+ this.packageCollectors.forgetDevice(deviceId);
71577
72317
  this.packageOversizeLogAt.delete(deviceId);
71578
72318
  this.overlayState.clearDevice(deviceId);
71579
72319
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -71964,6 +72704,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71964
72704
  eventStore: stores.eventStore,
71965
72705
  logger: this.ctx.logger,
71966
72706
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
72707
+ resolveAutoAssignThreshold: async () => (await this.readDeviceSettings(0, resolveFaceSettings)).similarityThreshold,
71967
72708
  resolveClusterModelId: async () => {
71968
72709
  const api = this.ctx.api;
71969
72710
  if (!api) return null;
@@ -72456,12 +73197,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72456
73197
  this.residents.markFirstFramePending(deviceId, id);
72457
73198
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
72458
73199
  this.residents.setLastFrameAt(deviceId, id, result.timestamp);
72459
- if (frameHandle) firstFrameTargets.push({
73200
+ const birthCapture = decideBirthFirstFrameCapture({
73201
+ hasFrameHandle: frameHandle !== void 0,
73202
+ matchedThisFrame: t.matchedThisFrame
73203
+ });
73204
+ if (birthCapture.capture) firstFrameTargets.push({
72460
73205
  trackId: id,
72461
73206
  timestamp: result.timestamp,
72462
73207
  bbox: { ...t.bbox },
72463
73208
  ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {}
72464
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
+ });
72465
73220
  }
72466
73221
  this.ctx.eventBus.emit({
72467
73222
  id: `pa-${(0, node_crypto.randomUUID)()}`,
@@ -75171,6 +75926,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75171
75926
  this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
75172
75927
  break;
75173
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
+ });
75174
75937
  const sightings = [];
75175
75938
  for (const t of input.tracked) {
75176
75939
  if (!input.packageClasses.has(t.className)) continue;
@@ -75282,10 +76045,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75282
76045
  meta: { entryId: entry.id }
75283
76046
  });
75284
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
+ });
75285
76061
  this.packageDropDetector.onStationaryChange({
75286
76062
  phase: "departed",
75287
76063
  entry,
75288
- timestamp: input.timestamp
76064
+ timestamp: input.timestamp,
76065
+ ...collector !== null ? { collector } : {}
75289
76066
  });
75290
76067
  }
75291
76068
  for (const entry of outcome.departedWithoutActivity) {
@@ -77410,7 +78187,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77410
78187
  setFlags: (trackId, patch) => trackStore.setFlags(trackId, patch),
77411
78188
  countStaging: (query) => trackStore.countStaging(query)
77412
78189
  },
77413
- logger: this.ctx.logger
78190
+ logger: this.ctx.logger,
78191
+ ...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
77414
78192
  }, input);
77415
78193
  this.ctx.logger.info("track operator flags set", {
77416
78194
  tags: { deviceId: input.deviceId },
@@ -77541,6 +78319,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77541
78319
  return this.queryFacade.listOpsLog(input);
77542
78320
  }
77543
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
+ /**
77544
78334
  * Track-centric time-based retention (design §5.1). Drains every persisted
77545
78335
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
77546
78336
  * widened cascade — enrolled faces/plates + identity media are exempt (design