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