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