@camstack/addon-post-analysis 1.2.76 → 1.2.77
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-BsBjSNIo.js → dist-Bs6oboqZ.js} +89 -5
- package/dist/{dist-CYfG8ulU.mjs → dist-CxeFHSSV.mjs} +89 -5
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DCcRtqVf.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BP6Na43Q.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-sdxJJeWR.mjs → hostInit-DTI0jC8p.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +673 -65
- package/dist/pipeline-analytics/index.mjs +636 -65
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isDetectionMacroClass, At as unknown, B as alarmPanelCapability, C as NcSnoozeSchema, Ct as discriminatedUnion, D as OpsLogEntrySchema, Dt as partialRecord, E as NcTaxonomySchema, Et as object, F as TimelapseRuleInputSchema, G as cosineSimilarity$1, H as audioMetricsCapability, I as TimelapseRulePatchSchema, J as deriveRecordingMode, K as customAction, L as TimelapseRuleSchema, M as SCENE_DIVERGED, N as SceneMonitorSchema, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as record, P as TIMELAPSE_DENSE_FLOOR_SEC, R as TrackSourceSchema, S as NcSnoozeInputSchema, St as boolean, T as NcSystemEventKindSchema, Tt as number, U as audioModeOf, V as assertTimelapseCadences, W as buildEventKindDescriptor, X as encodeVectorBase64, Z as faceGalleryCapability, _ as NcRuleInputSchema, _t as isDeviceScopedCap, at as readDeviceStateFrom, b as NcRuleTargetSchema, bt as _enum, c as EVENT_PAD_MS, ct as subKindsOf, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as zoneAnalyticsCapability, et as isScheduleActive, f as NC_CONDITION_CATALOG, ft as errMsg, g as NcConditionDescriptorSchema, gt as hydrateSchema, h as NC_TAXONOMY, ht as createEvent, i as DETECTION_MACRO_CLASSES, it as plateGalleryCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as EventCategory, k as RetrainStatusSchema, kt as string, l as LabelAttributionSchema, lt as vectorDimFromBase64, mt as DeviceType, n as DEFAULT_EVENT_COLOR, nt as notificationRulesCapability, o as DeclaredDevices, ot as readTimelapseGeneratedAt, p as NC_DEFAULT_SNOOZE_MINUTES, pt as BaseAddon, q as defineCustomActions, rt as pipelineAnalyticsCapability, s as EVENT_KIND_BY_CAP, st as sceneMonitorCapability, t as BaseDevice, tt as kebabToCamel, u as MACRO_LABELS, ut as videoclipsCapability, v as NcRulePatchSchema, vt as nodePin, w as NcSnoozeSuppressedSchema, wt as literal, x as NcScheduleSchema, xt as array, y as NcRuleSchema, yt as sleep, z as addonWidgetsSourceCapability } from "../dist-
|
|
1
|
+
import { $ as isDetectionMacroClass, At as unknown, B as alarmPanelCapability, C as NcSnoozeSchema, Ct as discriminatedUnion, D as OpsLogEntrySchema, Dt as partialRecord, E as NcTaxonomySchema, Et as object, F as TimelapseRuleInputSchema, G as cosineSimilarity$1, H as audioMetricsCapability, I as TimelapseRulePatchSchema, J as deriveRecordingMode, K as customAction, L as TimelapseRuleSchema, M as SCENE_DIVERGED, N as SceneMonitorSchema, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as record, P as TIMELAPSE_DENSE_FLOOR_SEC, R as TrackSourceSchema, S as NcSnoozeInputSchema, St as boolean, T as NcSystemEventKindSchema, Tt as number, U as audioModeOf, V as assertTimelapseCadences, W as buildEventKindDescriptor, X as encodeVectorBase64, Z as faceGalleryCapability, _ as NcRuleInputSchema, _t as isDeviceScopedCap, at as readDeviceStateFrom, b as NcRuleTargetSchema, bt as _enum, c as EVENT_PAD_MS, ct as subKindsOf, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as zoneAnalyticsCapability, et as isScheduleActive, f as NC_CONDITION_CATALOG, ft as errMsg, g as NcConditionDescriptorSchema, gt as hydrateSchema, h as NC_TAXONOMY, ht as createEvent, i as DETECTION_MACRO_CLASSES, it as plateGalleryCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as EventCategory, k as RetrainStatusSchema, kt as string, l as LabelAttributionSchema, lt as vectorDimFromBase64, mt as DeviceType, n as DEFAULT_EVENT_COLOR, nt as notificationRulesCapability, o as DeclaredDevices, ot as readTimelapseGeneratedAt, p as NC_DEFAULT_SNOOZE_MINUTES, pt as BaseAddon, q as defineCustomActions, rt as pipelineAnalyticsCapability, s as EVENT_KIND_BY_CAP, st as sceneMonitorCapability, t as BaseDevice, tt as kebabToCamel, u as MACRO_LABELS, ut as videoclipsCapability, v as NcRulePatchSchema, vt as nodePin, w as NcSnoozeSuppressedSchema, wt as literal, x as NcScheduleSchema, xt as array, y as NcRuleSchema, yt as sleep, z as addonWidgetsSourceCapability } from "../dist-CxeFHSSV.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";
|
|
@@ -4815,6 +4815,7 @@ var SceneMonitorProvider = class {
|
|
|
4815
4815
|
direction,
|
|
4816
4816
|
verdict: result.verdict,
|
|
4817
4817
|
latched,
|
|
4818
|
+
emit: monitor.emit,
|
|
4818
4819
|
condition: result.currentCondition,
|
|
4819
4820
|
confidence: result.lastConfidence,
|
|
4820
4821
|
timestamp: result.checkedAt
|
|
@@ -6479,6 +6480,110 @@ function maskPolygonOverlap(mask, maskWidth, maskHeight, bbox, polygon, _frameWi
|
|
|
6479
6480
|
if (totalMaskPixels === 0) return 0;
|
|
6480
6481
|
return insidePolygon / totalMaskPixels;
|
|
6481
6482
|
}
|
|
6483
|
+
var NcSceneStateCache = class {
|
|
6484
|
+
deps;
|
|
6485
|
+
entries = /* @__PURE__ */ new Map();
|
|
6486
|
+
noted = /* @__PURE__ */ new Map();
|
|
6487
|
+
ids = [];
|
|
6488
|
+
now;
|
|
6489
|
+
constructor(deps) {
|
|
6490
|
+
this.deps = deps;
|
|
6491
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6492
|
+
}
|
|
6493
|
+
/**
|
|
6494
|
+
* The scene ids worth reading — recomputed from the rule set, so a cluster
|
|
6495
|
+
* with no scene-gated rule does no work at all and logs nothing.
|
|
6496
|
+
*/
|
|
6497
|
+
setWatched(ids) {
|
|
6498
|
+
this.ids = [...new Set(ids)].sort();
|
|
6499
|
+
}
|
|
6500
|
+
watched() {
|
|
6501
|
+
return this.ids;
|
|
6502
|
+
}
|
|
6503
|
+
/**
|
|
6504
|
+
* "This scene, in this process, is in this state, NOW."
|
|
6505
|
+
*
|
|
6506
|
+
* Synchronous by contract — the caller is the scene provider's own edge tap
|
|
6507
|
+
* and the next rule evaluation must already see it.
|
|
6508
|
+
*/
|
|
6509
|
+
note(observation) {
|
|
6510
|
+
this.noted.set(observation.sceneId, {
|
|
6511
|
+
observation,
|
|
6512
|
+
at: this.now()
|
|
6513
|
+
});
|
|
6514
|
+
}
|
|
6515
|
+
/** Hot-path read. No I/O, no await — this is called per event. */
|
|
6516
|
+
get(sceneId) {
|
|
6517
|
+
const now = this.now();
|
|
6518
|
+
const noted = this.noted.get(sceneId);
|
|
6519
|
+
if (noted !== void 0) {
|
|
6520
|
+
if (now - noted.at <= 3e5) return noted.observation;
|
|
6521
|
+
this.noted.delete(sceneId);
|
|
6522
|
+
}
|
|
6523
|
+
const entry = this.entries.get(sceneId);
|
|
6524
|
+
if (entry === void 0) return void 0;
|
|
6525
|
+
if (now - entry.at > 3e5) return void 0;
|
|
6526
|
+
return entry.observation;
|
|
6527
|
+
}
|
|
6528
|
+
/**
|
|
6529
|
+
* Re-read every watched scene. Never throws: a gate that could fail the
|
|
6530
|
+
* evaluation tick would take the notification pipeline with it.
|
|
6531
|
+
*
|
|
6532
|
+
* On failure the previous entries are KEPT — their timestamps are not
|
|
6533
|
+
* touched, so they age out on their own rather than vanishing at once.
|
|
6534
|
+
*/
|
|
6535
|
+
refresh() {
|
|
6536
|
+
if (this.ids.length === 0) {
|
|
6537
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
6538
|
+
this.noted.clear();
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
let observations;
|
|
6542
|
+
try {
|
|
6543
|
+
observations = this.deps.readScenes();
|
|
6544
|
+
} catch (err) {
|
|
6545
|
+
this.deps.logger.warn("scene-state refresh failed — keeping the previous states until they age out", { meta: {
|
|
6546
|
+
watched: this.ids.length,
|
|
6547
|
+
error: String(err)
|
|
6548
|
+
} });
|
|
6549
|
+
return;
|
|
6550
|
+
}
|
|
6551
|
+
const at = this.now();
|
|
6552
|
+
const wanted = new Set(this.ids);
|
|
6553
|
+
const next = /* @__PURE__ */ new Map();
|
|
6554
|
+
for (const observation of observations) {
|
|
6555
|
+
if (!wanted.has(observation.sceneId)) continue;
|
|
6556
|
+
next.set(observation.sceneId, {
|
|
6557
|
+
observation,
|
|
6558
|
+
at
|
|
6559
|
+
});
|
|
6560
|
+
}
|
|
6561
|
+
this.entries = next;
|
|
6562
|
+
for (const sceneId of this.ids) {
|
|
6563
|
+
if (next.has(sceneId)) continue;
|
|
6564
|
+
this.deps.logger.info("scene gate: no state for a watched scene", { meta: { sceneId } });
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
};
|
|
6568
|
+
/**
|
|
6569
|
+
* Does this observation satisfy the rule's scene condition?
|
|
6570
|
+
*
|
|
6571
|
+
* Exported and pure so the semantics can be tested without a cache, a provider
|
|
6572
|
+
* or a rule — and so there is exactly ONE place that decides what `latched`
|
|
6573
|
+
* absent means. See `NcSceneConditionSchema` for the reasoning behind each
|
|
6574
|
+
* branch.
|
|
6575
|
+
*/
|
|
6576
|
+
function sceneConditionSatisfied(condition, observation) {
|
|
6577
|
+
if (condition.latched ?? observation.emit === "latched") return observation.latched === (condition.requiredState === "diverged");
|
|
6578
|
+
return observation.verdict === condition.requiredState;
|
|
6579
|
+
}
|
|
6580
|
+
/** One-line "why", for the suppression log. Never a bare boolean: a drop the
|
|
6581
|
+
* operator cannot explain is the failure this whole feature is one misstep
|
|
6582
|
+
* away from. */
|
|
6583
|
+
function sceneGateReason(condition, observation) {
|
|
6584
|
+
if (observation === void 0) return "no mirrored state for this scene";
|
|
6585
|
+
return condition.latched ?? observation.emit === "latched" ? `latched=${observation.latched} (wanted ${condition.requiredState})` : `verdict=${observation.verdict} (wanted ${condition.requiredState})`;
|
|
6586
|
+
}
|
|
6482
6587
|
//#endregion
|
|
6483
6588
|
//#region src/notification-center/zone-ownership.ts
|
|
6484
6589
|
var UNCHANGED = { kind: "unchanged" };
|
|
@@ -6815,6 +6920,7 @@ function presentConditionIds(c) {
|
|
|
6815
6920
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") ids.push("packagePhase");
|
|
6816
6921
|
if (c.customZones !== void 0 && c.customZones.length > 0) ids.push("customZones");
|
|
6817
6922
|
if (c.occupancy !== void 0) ids.push("occupancy");
|
|
6923
|
+
if (c.scene !== void 0) ids.push("scene");
|
|
6818
6924
|
return ids;
|
|
6819
6925
|
}
|
|
6820
6926
|
/**
|
|
@@ -6994,7 +7100,7 @@ function matchesSystemEvent(condition, subject) {
|
|
|
6994
7100
|
if (condition.packageNames !== void 0 && (subject.packageName === void 0 || !condition.packageNames.includes(subject.packageName))) return false;
|
|
6995
7101
|
return true;
|
|
6996
7102
|
}
|
|
6997
|
-
function evaluateRule(rule, subject, deviceState, zoneOwners) {
|
|
7103
|
+
function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
6998
7104
|
if (subject.kind === "system-event") {
|
|
6999
7105
|
if (rule.delivery !== "system-event") return fail("delivery");
|
|
7000
7106
|
const condition = rule.conditions.systemEvent;
|
|
@@ -7035,6 +7141,11 @@ function evaluateRule(rule, subject, deviceState, zoneOwners) {
|
|
|
7035
7141
|
if (current === void 0) return fail("deviceState");
|
|
7036
7142
|
if (!toLowerSet(c.deviceState.states).has(current.trim().toLowerCase())) return fail("deviceState");
|
|
7037
7143
|
}
|
|
7144
|
+
if (c.scene !== void 0) {
|
|
7145
|
+
const observation = sceneStates?.(c.scene.sceneId);
|
|
7146
|
+
if (observation === void 0) return fail("scene");
|
|
7147
|
+
if (!sceneConditionSatisfied(c.scene, observation)) return fail("scene");
|
|
7148
|
+
}
|
|
7038
7149
|
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
7039
7150
|
if (c.source !== void 0 && c.source !== "any") {
|
|
7040
7151
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
@@ -17623,6 +17734,22 @@ var NotificationCenter = class NotificationCenter {
|
|
|
17623
17734
|
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
17624
17735
|
* read with no I/O on the persist hook. */
|
|
17625
17736
|
deviceStates;
|
|
17737
|
+
/** Scene verdicts + latches the `scene` rule gate reads. Same contract as
|
|
17738
|
+
* {@link deviceStates}: refreshed on the reload tick and on the provider's
|
|
17739
|
+
* in-process edge tap, read with no I/O on the persist hook (D49). */
|
|
17740
|
+
sceneStates;
|
|
17741
|
+
/**
|
|
17742
|
+
* The last scene-gate answer REPORTED per rule, so the suppression line is
|
|
17743
|
+
* written on CHANGE rather than on every event.
|
|
17744
|
+
*
|
|
17745
|
+
* A branch that drops work silently must log it, and a gate that logged on
|
|
17746
|
+
* every detection on a busy camera would be a line nobody reads — which is
|
|
17747
|
+
* the same silence by a different route.
|
|
17748
|
+
*/
|
|
17749
|
+
sceneGateReported = /* @__PURE__ */ new Map();
|
|
17750
|
+
/** The watched scene id set, as last REPORTED — so the line is written when it
|
|
17751
|
+
* changes and not on every reload tick. `null` = never reported. */
|
|
17752
|
+
sceneWatchSignature = null;
|
|
17626
17753
|
/** WHO each device id is (name + type). The system-event intake reads it with
|
|
17627
17754
|
* no I/O; the reload tick refreshes it — see `device-directory.ts`. */
|
|
17628
17755
|
deviceDirectory;
|
|
@@ -18022,6 +18149,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18022
18149
|
readStates: deps.readDeviceStates ?? (async () => /* @__PURE__ */ new Map()),
|
|
18023
18150
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
18024
18151
|
});
|
|
18152
|
+
this.sceneStates = new NcSceneStateCache({
|
|
18153
|
+
logger: this.logger.child("scene-state"),
|
|
18154
|
+
readScenes: deps.readSceneStates ?? (() => []),
|
|
18155
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
18156
|
+
});
|
|
18025
18157
|
this.deviceDirectory = new NcDeviceDirectory({
|
|
18026
18158
|
logger: this.logger.child("device-directory"),
|
|
18027
18159
|
listDevices: deps.listDeviceIdentities ?? (async () => {
|
|
@@ -18221,6 +18353,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18221
18353
|
await this.deviceDirectory.refresh();
|
|
18222
18354
|
await this.hydrateLiveness();
|
|
18223
18355
|
await this.zoneOwners.refresh();
|
|
18356
|
+
this.sceneStates.refresh();
|
|
18224
18357
|
this.evaluationActive = true;
|
|
18225
18358
|
await this.outbox.load();
|
|
18226
18359
|
await this.seedCooldowns();
|
|
@@ -18985,6 +19118,73 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18985
19118
|
if (identity.isCamera && identity.detectionActive === false) return "detection-off";
|
|
18986
19119
|
}
|
|
18987
19120
|
/**
|
|
19121
|
+
* SAY that the scene gate suppressed this rule.
|
|
19122
|
+
*
|
|
19123
|
+
* A branch that drops work silently logs it — the three-hour media blackout
|
|
19124
|
+
* produced not one line, and a gate is the same shape of failure: from the
|
|
19125
|
+
* operator's side "the rule stopped notifying" and "the pipeline stopped
|
|
19126
|
+
* working" look identical. `rule did not match` covers it at DEBUG, which is
|
|
19127
|
+
* not a level this installation runs at.
|
|
19128
|
+
*
|
|
19129
|
+
* On CHANGE only, per rule: the alternative is one line per detection on a
|
|
19130
|
+
* busy camera, which is silence by a different route. The `deviceId` tag is
|
|
19131
|
+
* the SUBJECT's — that is the camera whose notification went missing, and the
|
|
19132
|
+
* question is always asked per-camera. The SCENE's own device is named in the
|
|
19133
|
+
* meta beside its id, because the two are frequently different cameras.
|
|
19134
|
+
*/
|
|
19135
|
+
reportSceneSuppression(rule, subject) {
|
|
19136
|
+
const condition = rule.conditions.scene;
|
|
19137
|
+
if (condition === void 0) return;
|
|
19138
|
+
const observation = this.sceneStates.get(condition.sceneId);
|
|
19139
|
+
const reason = sceneGateReason(condition, observation);
|
|
19140
|
+
if (this.sceneGateReported.get(rule.id) === reason) return;
|
|
19141
|
+
this.sceneGateReported.set(rule.id, reason);
|
|
19142
|
+
this.logger.info("rule suppressed by its scene condition", {
|
|
19143
|
+
tags: { deviceId: subject.deviceId },
|
|
19144
|
+
meta: {
|
|
19145
|
+
ruleId: rule.id,
|
|
19146
|
+
rule: rule.name,
|
|
19147
|
+
sceneId: condition.sceneId,
|
|
19148
|
+
sceneLabel: observation?.label,
|
|
19149
|
+
sceneDeviceId: observation?.deviceId ?? condition.deviceId,
|
|
19150
|
+
requiredState: condition.requiredState,
|
|
19151
|
+
reason
|
|
19152
|
+
}
|
|
19153
|
+
});
|
|
19154
|
+
}
|
|
19155
|
+
/**
|
|
19156
|
+
* A confirmed scene transition, straight from the provider in this process.
|
|
19157
|
+
*
|
|
19158
|
+
* Synchronous on purpose and for the same reason {@link onAlarmPublish} is:
|
|
19159
|
+
* the reload tick would otherwise be the only writer, and a rule gated on
|
|
19160
|
+
* "while the bin is out" would keep its old answer for up to thirty seconds
|
|
19161
|
+
* after the bin came back. The tick stays as reconciliation.
|
|
19162
|
+
*
|
|
19163
|
+
* A DIRECT CALL, not a bus subscription. `EnrichmentSceneStateChanged` is
|
|
19164
|
+
* still emitted for the badge and for anyone else, but D8 events are
|
|
19165
|
+
* droppable telemetry and a gate fed by droppable telemetry is a gate that is
|
|
19166
|
+
* sometimes wrong and never says so.
|
|
19167
|
+
*
|
|
19168
|
+
* This never OPENS a gate that the mirror had shut for lack of an entry: it
|
|
19169
|
+
* writes an observation the caller actually made. It is `refresh` that
|
|
19170
|
+
* removes entries, and it never empties on a failed read (D49).
|
|
19171
|
+
*/
|
|
19172
|
+
onSceneEdge(observation) {
|
|
19173
|
+
this.sceneStates.note(observation);
|
|
19174
|
+
}
|
|
19175
|
+
/**
|
|
19176
|
+
* Reconcile the scene mirror now.
|
|
19177
|
+
*
|
|
19178
|
+
* The scene provider is constructed AFTER this centre starts (it needs the
|
|
19179
|
+
* media store and the embedding encoder), so the boot refresh reads an empty
|
|
19180
|
+
* provider. The addon calls this once the provider has hydrated, rather than
|
|
19181
|
+
* leaving every scene-gated rule shut for a whole reload tick after every
|
|
19182
|
+
* respawn.
|
|
19183
|
+
*/
|
|
19184
|
+
refreshSceneStates() {
|
|
19185
|
+
this.sceneStates.refresh();
|
|
19186
|
+
}
|
|
19187
|
+
/**
|
|
18988
19188
|
* Everything the panel reports, on every publish.
|
|
18989
19189
|
*
|
|
18990
19190
|
* Synchronous on purpose. The state half refreshes the `deviceState` gate's
|
|
@@ -19283,8 +19483,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19283
19483
|
let noOwnerReported = false;
|
|
19284
19484
|
const rejections = [];
|
|
19285
19485
|
for (const rule of candidates) {
|
|
19286
|
-
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id), (id) => this.zoneOwners.get(id));
|
|
19486
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id), (id) => this.zoneOwners.get(id), (sceneId) => this.sceneStates.get(sceneId));
|
|
19287
19487
|
if (!evaluation.matched) {
|
|
19488
|
+
if (evaluation.failedCondition === "scene") this.reportSceneSuppression(rule, subject);
|
|
19288
19489
|
rejections.push(`${rule.name}:${evaluation.failedCondition ?? "unknown"}`);
|
|
19289
19490
|
outcomes.push({
|
|
19290
19491
|
ruleId: rule.id,
|
|
@@ -19309,6 +19510,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19309
19510
|
});
|
|
19310
19511
|
continue;
|
|
19311
19512
|
}
|
|
19513
|
+
this.sceneGateReported.delete(rule.id);
|
|
19312
19514
|
this.runRuleActions(rule, subject, kind, controls);
|
|
19313
19515
|
const admission = this.admitToGroup(rule, subject, now);
|
|
19314
19516
|
if (admission.outcome === "unchanged") {
|
|
@@ -19940,10 +20142,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19940
20142
|
this.reportAudioWatch(audioWatch);
|
|
19941
20143
|
const gated = [];
|
|
19942
20144
|
const zoneScoped = [];
|
|
20145
|
+
const sceneIds = [];
|
|
19943
20146
|
for (const rule of this.rules.list()) {
|
|
19944
20147
|
if (!rule.enabled) continue;
|
|
19945
20148
|
const ds = rule.conditions.deviceState;
|
|
19946
20149
|
if (ds !== void 0) gated.push(ds.deviceId);
|
|
20150
|
+
const scene = rule.conditions.scene;
|
|
20151
|
+
if (scene !== void 0) sceneIds.push(scene.sceneId);
|
|
19947
20152
|
const zones = rule.conditions.zones;
|
|
19948
20153
|
const devices = rule.conditions.devices;
|
|
19949
20154
|
if (zones !== void 0 && zones.ids.length > 0 && devices !== void 0) zoneScoped.push(...devices);
|
|
@@ -19952,6 +20157,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19952
20157
|
if (panel !== null) gated.push(...alarmContactWatchList(this.rules.list(), panel.deviceId, panel.availableModes()));
|
|
19953
20158
|
this.deviceStates.setWatched(gated);
|
|
19954
20159
|
this.zoneOwners.setWatched(zoneScoped);
|
|
20160
|
+
this.sceneStates.setWatched(sceneIds);
|
|
20161
|
+
const signature = [...new Set(sceneIds)].sort().join(",");
|
|
20162
|
+
if (signature !== this.sceneWatchSignature) {
|
|
20163
|
+
this.sceneWatchSignature = signature;
|
|
20164
|
+
this.logger.info("scene gate watched set", { meta: {
|
|
20165
|
+
scenes: this.sceneStates.watched(),
|
|
20166
|
+
gatedRules: sceneIds.length
|
|
20167
|
+
} });
|
|
20168
|
+
}
|
|
19955
20169
|
}
|
|
19956
20170
|
/**
|
|
19957
20171
|
* Feed every excluded sensor's mirrored state back to the panel.
|
|
@@ -20088,6 +20302,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20088
20302
|
await this.deviceStates.refresh();
|
|
20089
20303
|
await this.deviceDirectory.refresh();
|
|
20090
20304
|
await this.zoneOwners.refresh();
|
|
20305
|
+
this.sceneStates.refresh();
|
|
20091
20306
|
this.observeAlarmContacts();
|
|
20092
20307
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
20093
20308
|
}
|
|
@@ -20385,12 +20600,17 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20385
20600
|
const fresh = await this.deps.readDeviceZoneIds(zoneScope);
|
|
20386
20601
|
zoneLookup = (id) => fresh.get(id) ?? this.zoneOwners.get(id);
|
|
20387
20602
|
} catch {}
|
|
20603
|
+
let sceneLookup = (id) => this.sceneStates.get(id);
|
|
20604
|
+
if (rule.conditions.scene !== void 0 && this.deps.readSceneStates !== void 0) try {
|
|
20605
|
+
const fresh = new Map(this.deps.readSceneStates().map((o) => [o.sceneId, o]));
|
|
20606
|
+
sceneLookup = (id) => fresh.get(id) ?? this.sceneStates.get(id);
|
|
20607
|
+
} catch {}
|
|
20388
20608
|
const occupancy = rule.conditions.occupancy;
|
|
20389
|
-
if (rule.delivery === "device-event" && occupancy !== void 0) return this.occupancyDryRun(rule, occupancy, lookup, zoneLookup);
|
|
20609
|
+
if (rule.delivery === "device-event" && occupancy !== void 0) return this.occupancyDryRun(rule, occupancy, lookup, zoneLookup, sceneLookup);
|
|
20390
20610
|
const results = [];
|
|
20391
20611
|
for (const subject of subjects.sort((a, b) => b.timestamp - a.timestamp)) {
|
|
20392
20612
|
if (results.length >= TEST_RULE_MAX_RESULTS) break;
|
|
20393
|
-
const evaluation = evaluateRule(rule, subject, lookup, zoneLookup);
|
|
20613
|
+
const evaluation = evaluateRule(rule, subject, lookup, zoneLookup, sceneLookup);
|
|
20394
20614
|
results.push({
|
|
20395
20615
|
recordId: subject.recordId,
|
|
20396
20616
|
recordKind: subject.kind === "track-end" ? "track" : subject.kind === "audio-event" || subject.kind === "audio-window" ? "object-event" : subject.kind === "occupancy-event" ? "device-event" : subject.kind,
|
|
@@ -20414,7 +20634,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20414
20634
|
* from the ordinary `evaluateRule` on the ordinary subject — this method
|
|
20415
20635
|
* chooses the cameras and writes the sentence, nothing else.
|
|
20416
20636
|
*/
|
|
20417
|
-
async occupancyDryRun(rule, occupancy, lookup, zoneLookup) {
|
|
20637
|
+
async occupancyDryRun(rule, occupancy, lookup, zoneLookup, sceneLookup) {
|
|
20418
20638
|
const read = this.deps.readOccupancySnapshots;
|
|
20419
20639
|
if (read === void 0) return [];
|
|
20420
20640
|
const now = this.now();
|
|
@@ -20457,7 +20677,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20457
20677
|
});
|
|
20458
20678
|
continue;
|
|
20459
20679
|
}
|
|
20460
|
-
const evaluation = evaluateRule(rule, probe.subject, lookup, zoneLookup);
|
|
20680
|
+
const evaluation = evaluateRule(rule, probe.subject, lookup, zoneLookup, sceneLookup);
|
|
20461
20681
|
results.push({
|
|
20462
20682
|
...common,
|
|
20463
20683
|
recordId: probe.subject.recordId,
|
|
@@ -20712,6 +20932,10 @@ function planRuleTest(input) {
|
|
|
20712
20932
|
conditionId: "deviceState",
|
|
20713
20933
|
note: `This rule only fires while device #${c.deviceState.deviceId} is in one of its chosen states. That gate reads the REAL device and cannot be simulated — put the device in that state first, or the test fails closed exactly as production would.`
|
|
20714
20934
|
});
|
|
20935
|
+
if (c.scene !== void 0) notes.push({
|
|
20936
|
+
conditionId: "scene",
|
|
20937
|
+
note: `This rule only fires while scene ${c.scene.sceneId} is ${c.scene.requiredState}. That gate reads the scene's REAL state and cannot be simulated — the test answers with whatever the scene is right now, and fails closed while it cannot judge or has not been read yet, exactly as production would.`
|
|
20938
|
+
});
|
|
20715
20939
|
if (c.customZones !== void 0 && c.customZones.length > 0) notes.push({
|
|
20716
20940
|
conditionId: "customZones",
|
|
20717
20941
|
note: "The subject is placed over the first drawn polygon so the condition passes. A rule that needs a specific polygon should be checked in the simulator, where the box can be moved."
|
|
@@ -23220,6 +23444,51 @@ var embeddingActions = defineCustomActions({
|
|
|
23220
23444
|
"embedding.rebuildStatus": customAction(NoInputSchema, RebuildStatusSchema),
|
|
23221
23445
|
"embedding.wipe": customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
|
|
23222
23446
|
});
|
|
23447
|
+
var OrphanAuditActionInputSchema = object({
|
|
23448
|
+
mode: _enum(["report", "reclaim"]).default("report"),
|
|
23449
|
+
/**
|
|
23450
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
23451
|
+
*
|
|
23452
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
23453
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
23454
|
+
*/
|
|
23455
|
+
restart: boolean().optional(),
|
|
23456
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
23457
|
+
pageSize: number().int().min(50).max(5e3).optional(),
|
|
23458
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
23459
|
+
maxRowsPerScope: number().int().min(1).max(2e5).optional(),
|
|
23460
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
23461
|
+
maxReclaimPerScope: number().int().min(1).max(1e5).optional(),
|
|
23462
|
+
/**
|
|
23463
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
23464
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
23465
|
+
* the one-age-keyed-sweep guard's books.
|
|
23466
|
+
*/
|
|
23467
|
+
graceMinutes: number().int().min(1).max(10080).optional()
|
|
23468
|
+
});
|
|
23469
|
+
var OrphanAuditScopeReportSchema = object({
|
|
23470
|
+
collection: string(),
|
|
23471
|
+
examined: number(),
|
|
23472
|
+
orphans: number(),
|
|
23473
|
+
reclaimed: number(),
|
|
23474
|
+
bytesReclaimed: number(),
|
|
23475
|
+
exempt: number(),
|
|
23476
|
+
withinGrace: number(),
|
|
23477
|
+
unresolvable: number(),
|
|
23478
|
+
stoppedBy: string(),
|
|
23479
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
23480
|
+
nextCursor: number()
|
|
23481
|
+
});
|
|
23482
|
+
var orphanAuditActions = defineCustomActions({ "retention.orphanAudit": customAction(OrphanAuditActionInputSchema, object({
|
|
23483
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
23484
|
+
ran: boolean(),
|
|
23485
|
+
mode: _enum(["report", "reclaim"]),
|
|
23486
|
+
scopes: array(OrphanAuditScopeReportSchema),
|
|
23487
|
+
totalOrphans: number(),
|
|
23488
|
+
totalReclaimed: number(),
|
|
23489
|
+
totalBytesReclaimed: number(),
|
|
23490
|
+
durationMs: number()
|
|
23491
|
+
}), { auth: "admin" }) });
|
|
23223
23492
|
//#endregion
|
|
23224
23493
|
//#region src/pipeline-analytics/map-with-concurrency.ts
|
|
23225
23494
|
/**
|
|
@@ -25957,6 +26226,72 @@ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG)
|
|
|
25957
26226
|
}
|
|
25958
26227
|
//#endregion
|
|
25959
26228
|
//#region src/pipeline-analytics/pipeline/delete-track-cascade.ts
|
|
26229
|
+
var EMPTY_CASCADE_TALLY = {
|
|
26230
|
+
tracks: 0,
|
|
26231
|
+
events: 0,
|
|
26232
|
+
media: 0,
|
|
26233
|
+
faces: 0,
|
|
26234
|
+
plates: 0,
|
|
26235
|
+
embeddings: 0,
|
|
26236
|
+
bytesReclaimed: 0
|
|
26237
|
+
};
|
|
26238
|
+
/** Fold a leaf store's receipts into a running tally. */
|
|
26239
|
+
function addLeafCounts(tally, counts) {
|
|
26240
|
+
let next = tally;
|
|
26241
|
+
for (const c of counts) {
|
|
26242
|
+
const bytesReclaimed = next.bytesReclaimed + c.bytes;
|
|
26243
|
+
switch (c.family) {
|
|
26244
|
+
case "events":
|
|
26245
|
+
next = {
|
|
26246
|
+
...next,
|
|
26247
|
+
events: next.events + c.rows,
|
|
26248
|
+
bytesReclaimed
|
|
26249
|
+
};
|
|
26250
|
+
break;
|
|
26251
|
+
case "media":
|
|
26252
|
+
next = {
|
|
26253
|
+
...next,
|
|
26254
|
+
media: next.media + c.rows,
|
|
26255
|
+
bytesReclaimed
|
|
26256
|
+
};
|
|
26257
|
+
break;
|
|
26258
|
+
case "faces":
|
|
26259
|
+
next = {
|
|
26260
|
+
...next,
|
|
26261
|
+
faces: next.faces + c.rows,
|
|
26262
|
+
bytesReclaimed
|
|
26263
|
+
};
|
|
26264
|
+
break;
|
|
26265
|
+
case "plates":
|
|
26266
|
+
next = {
|
|
26267
|
+
...next,
|
|
26268
|
+
plates: next.plates + c.rows,
|
|
26269
|
+
bytesReclaimed
|
|
26270
|
+
};
|
|
26271
|
+
break;
|
|
26272
|
+
case "embeddings":
|
|
26273
|
+
next = {
|
|
26274
|
+
...next,
|
|
26275
|
+
embeddings: next.embeddings + c.rows,
|
|
26276
|
+
bytesReclaimed
|
|
26277
|
+
};
|
|
26278
|
+
break;
|
|
26279
|
+
}
|
|
26280
|
+
}
|
|
26281
|
+
return next;
|
|
26282
|
+
}
|
|
26283
|
+
/** Sum two tallies (per-track results into a per-device one). */
|
|
26284
|
+
function mergeCascadeTallies(a, b) {
|
|
26285
|
+
return {
|
|
26286
|
+
tracks: a.tracks + b.tracks,
|
|
26287
|
+
events: a.events + b.events,
|
|
26288
|
+
media: a.media + b.media,
|
|
26289
|
+
faces: a.faces + b.faces,
|
|
26290
|
+
plates: a.plates + b.plates,
|
|
26291
|
+
embeddings: a.embeddings + b.embeddings,
|
|
26292
|
+
bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
|
|
26293
|
+
};
|
|
26294
|
+
}
|
|
25960
26295
|
/**
|
|
25961
26296
|
* Extensible whole-track deletion cascade. Runs every registered
|
|
25962
26297
|
* `TrackScopedStore` (each removes its ephemeral rows + owned media for the
|
|
@@ -25973,8 +26308,16 @@ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG)
|
|
|
25973
26308
|
* ]
|
|
25974
26309
|
*/
|
|
25975
26310
|
async function cascadeDeleteTracks(registry, trackStore, trackIds) {
|
|
25976
|
-
|
|
25977
|
-
for (const
|
|
26311
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
26312
|
+
for (const store of registry) tally = addLeafCounts(tally, await store.deleteByTracks(trackIds));
|
|
26313
|
+
for (const trackId of trackIds) {
|
|
26314
|
+
await trackStore.deletePersisted(trackId);
|
|
26315
|
+
tally = {
|
|
26316
|
+
...tally,
|
|
26317
|
+
tracks: tally.tracks + 1
|
|
26318
|
+
};
|
|
26319
|
+
}
|
|
26320
|
+
return tally;
|
|
25978
26321
|
}
|
|
25979
26322
|
/**
|
|
25980
26323
|
* The one registry, in the one order — leaves first, root deleted internally
|
|
@@ -26005,14 +26348,28 @@ function buildAnalyticsCascadeRegistry(stores) {
|
|
|
26005
26348
|
* embeddings) is opted in by the CALLER's registry, not by this legacy shim.
|
|
26006
26349
|
*/
|
|
26007
26350
|
async function cascadeDeleteTrack(stores, trackId) {
|
|
26008
|
-
|
|
26351
|
+
return cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
|
|
26352
|
+
let events = 0;
|
|
26353
|
+
let media = 0;
|
|
26009
26354
|
for (const id of ids) {
|
|
26010
26355
|
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
26011
|
-
|
|
26356
|
+
events += eventIds.length;
|
|
26357
|
+
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
26012
26358
|
}
|
|
26013
|
-
|
|
26014
|
-
|
|
26015
|
-
|
|
26359
|
+
return [{
|
|
26360
|
+
family: "events",
|
|
26361
|
+
rows: events,
|
|
26362
|
+
bytes: 0
|
|
26363
|
+
}, {
|
|
26364
|
+
family: "media",
|
|
26365
|
+
rows: media,
|
|
26366
|
+
bytes: 0
|
|
26367
|
+
}];
|
|
26368
|
+
} }, { deleteByTracks: async (ids) => [{
|
|
26369
|
+
family: "media",
|
|
26370
|
+
rows: await stores.mediaStore.deleteForTracks([...ids]),
|
|
26371
|
+
bytes: 0
|
|
26372
|
+
}] }], stores.trackStore, [trackId]);
|
|
26016
26373
|
}
|
|
26017
26374
|
/**
|
|
26018
26375
|
* Widened batch whole-track deletion — the ONE engine behind the three §5 entry
|
|
@@ -26023,9 +26380,10 @@ async function cascadeDeleteTrack(stores, trackId) {
|
|
|
26023
26380
|
*/
|
|
26024
26381
|
async function runTrackCascadeBatch(deps, trackIds) {
|
|
26025
26382
|
let deleted = 0;
|
|
26383
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
26026
26384
|
const failed = [];
|
|
26027
26385
|
for (const trackId of trackIds) try {
|
|
26028
|
-
await cascadeDeleteTracks(deps.registry, deps.trackStore, [trackId]);
|
|
26386
|
+
tally = mergeCascadeTallies(tally, await cascadeDeleteTracks(deps.registry, deps.trackStore, [trackId]));
|
|
26029
26387
|
deps.onTrackCleanup(deps.deviceId, trackId);
|
|
26030
26388
|
deleted += 1;
|
|
26031
26389
|
} catch (err) {
|
|
@@ -26034,7 +26392,8 @@ async function runTrackCascadeBatch(deps, trackIds) {
|
|
|
26034
26392
|
}
|
|
26035
26393
|
return {
|
|
26036
26394
|
deleted,
|
|
26037
|
-
failed
|
|
26395
|
+
failed,
|
|
26396
|
+
tally
|
|
26038
26397
|
};
|
|
26039
26398
|
}
|
|
26040
26399
|
//#endregion
|
|
@@ -33657,21 +34016,33 @@ var MediaStore = class {
|
|
|
33657
34016
|
* Best-effort per row; returns the number of index rows removed.
|
|
33658
34017
|
*/
|
|
33659
34018
|
async deleteForOwner(ownerKind, ownerIds) {
|
|
34019
|
+
return (await this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds))).rows;
|
|
34020
|
+
}
|
|
34021
|
+
/**
|
|
34022
|
+
* The same delete, reporting the BYTES it released as well as the row count.
|
|
34023
|
+
*
|
|
34024
|
+
* `sizeBytes` is already on the index row, so the number costs nothing — it
|
|
34025
|
+
* was simply never returned, which is why every retention ops-log row this
|
|
34026
|
+
* addon has ever written says `bytesReclaimed: 0` (D161).
|
|
34027
|
+
*/
|
|
34028
|
+
async deleteForOwnerCounted(ownerKind, ownerIds) {
|
|
33660
34029
|
return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
|
|
33661
34030
|
}
|
|
33662
34031
|
async deleteForOwnerUnchecked(ownerKind, ownerIds) {
|
|
33663
|
-
let
|
|
34032
|
+
let rows = 0;
|
|
34033
|
+
let bytes = 0;
|
|
33664
34034
|
for (const ownerId of ownerIds) {
|
|
33665
|
-
const
|
|
34035
|
+
const found = await this.store.query.query({
|
|
33666
34036
|
collection: MEDIA_COLLECTION,
|
|
33667
34037
|
filter: { where: {
|
|
33668
34038
|
ownerKind,
|
|
33669
34039
|
ownerId
|
|
33670
34040
|
} }
|
|
33671
34041
|
});
|
|
33672
|
-
for (const row of
|
|
34042
|
+
for (const row of found) {
|
|
33673
34043
|
const rowData = row.data;
|
|
33674
34044
|
const path = String(rowData["path"] ?? "");
|
|
34045
|
+
const sizeBytes = Number(rowData["sizeBytes"]);
|
|
33675
34046
|
try {
|
|
33676
34047
|
if (path) await this.storage.delete({
|
|
33677
34048
|
location: mediaRowLocation(rowData),
|
|
@@ -33683,7 +34054,8 @@ var MediaStore = class {
|
|
|
33683
34054
|
collection: MEDIA_COLLECTION,
|
|
33684
34055
|
key: row.id
|
|
33685
34056
|
});
|
|
33686
|
-
|
|
34057
|
+
rows++;
|
|
34058
|
+
if (Number.isFinite(sizeBytes)) bytes += sizeBytes;
|
|
33687
34059
|
} catch (err) {
|
|
33688
34060
|
this.logger.debug("media delete-for-owner failed", { meta: {
|
|
33689
34061
|
key: row.id,
|
|
@@ -33692,7 +34064,10 @@ var MediaStore = class {
|
|
|
33692
34064
|
}
|
|
33693
34065
|
}
|
|
33694
34066
|
}
|
|
33695
|
-
return
|
|
34067
|
+
return {
|
|
34068
|
+
rows,
|
|
34069
|
+
bytes
|
|
34070
|
+
};
|
|
33696
34071
|
}
|
|
33697
34072
|
/**
|
|
33698
34073
|
* On-demand footprint aggregation for the events-management UI: sum
|
|
@@ -33831,11 +34206,20 @@ var MediaStore = class {
|
|
|
33831
34206
|
* batch.
|
|
33832
34207
|
*/
|
|
33833
34208
|
async deleteByTracks(trackIds) {
|
|
33834
|
-
await this.gated(async () => {
|
|
33835
|
-
await this.deleteForOwnerUnchecked("track", trackIds);
|
|
33836
|
-
await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
|
|
34209
|
+
const own = await this.gated(async () => {
|
|
34210
|
+
const tracks = await this.deleteForOwnerUnchecked("track", trackIds);
|
|
34211
|
+
const faces = await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
|
|
34212
|
+
return {
|
|
34213
|
+
rows: tracks.rows + faces.rows,
|
|
34214
|
+
bytes: tracks.bytes + faces.bytes
|
|
34215
|
+
};
|
|
33837
34216
|
});
|
|
33838
|
-
await this.
|
|
34217
|
+
const plates = await this.deleteForOwnerCounted("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
|
|
34218
|
+
return [{
|
|
34219
|
+
family: "media",
|
|
34220
|
+
rows: own.rows + plates.rows,
|
|
34221
|
+
bytes: own.bytes + plates.bytes
|
|
34222
|
+
}];
|
|
33839
34223
|
}
|
|
33840
34224
|
/**
|
|
33841
34225
|
* A stable page of media INDEX rows for the orphan audit — no blobs, no
|
|
@@ -38273,11 +38657,21 @@ var EventStore = class {
|
|
|
38273
38657
|
error: String(err)
|
|
38274
38658
|
} });
|
|
38275
38659
|
}
|
|
38660
|
+
let mediaRows = 0;
|
|
38276
38661
|
if (eventIds.length > 0 && this.media !== void 0) try {
|
|
38277
|
-
await this.media.deleteForEvents(eventIds);
|
|
38662
|
+
mediaRows = await this.media.deleteForEvents(eventIds);
|
|
38278
38663
|
} catch (err) {
|
|
38279
38664
|
this.logger.warn("EventStore.deleteByTracks: event media delete failed", { meta: { error: String(err) } });
|
|
38280
38665
|
}
|
|
38666
|
+
return [{
|
|
38667
|
+
family: "events",
|
|
38668
|
+
rows: eventIds.length,
|
|
38669
|
+
bytes: 0
|
|
38670
|
+
}, {
|
|
38671
|
+
family: "media",
|
|
38672
|
+
rows: mediaRows,
|
|
38673
|
+
bytes: 0
|
|
38674
|
+
}];
|
|
38281
38675
|
}
|
|
38282
38676
|
};
|
|
38283
38677
|
function slimMotion(id, data) {
|
|
@@ -38668,6 +39062,7 @@ var FaceStore = class {
|
|
|
38668
39062
|
* aborts the batch. (Crop media of these rows is removed by `MediaStore`.)
|
|
38669
39063
|
*/
|
|
38670
39064
|
async deleteByTracks(trackIds) {
|
|
39065
|
+
let removed = 0;
|
|
38671
39066
|
for (const trackId of trackIds) try {
|
|
38672
39067
|
const eligible = (await this.store.query.query({
|
|
38673
39068
|
collection: FACES_COLLECTION,
|
|
@@ -38678,6 +39073,7 @@ var FaceStore = class {
|
|
|
38678
39073
|
collection: FACES_COLLECTION,
|
|
38679
39074
|
key: row.id
|
|
38680
39075
|
});
|
|
39076
|
+
removed++;
|
|
38681
39077
|
} catch (err) {
|
|
38682
39078
|
this.logger.warn("FaceStore.deleteByTracks delete failed", { meta: {
|
|
38683
39079
|
faceId: row.id,
|
|
@@ -38690,6 +39086,11 @@ var FaceStore = class {
|
|
|
38690
39086
|
error: String(err)
|
|
38691
39087
|
} });
|
|
38692
39088
|
}
|
|
39089
|
+
return [{
|
|
39090
|
+
family: "faces",
|
|
39091
|
+
rows: removed,
|
|
39092
|
+
bytes: 0
|
|
39093
|
+
}];
|
|
38693
39094
|
}
|
|
38694
39095
|
/** Delete a single buffered face row by id (its crop media is removed by the
|
|
38695
39096
|
* caller — the FaceStore owns rows, not blobs). Best-effort. */
|
|
@@ -39625,6 +40026,7 @@ var PlateStore = class {
|
|
|
39625
40026
|
* the batch. (Crop media of these rows is removed by `MediaStore`.)
|
|
39626
40027
|
*/
|
|
39627
40028
|
async deleteByTracks(trackIds) {
|
|
40029
|
+
let removed = 0;
|
|
39628
40030
|
for (const trackId of trackIds) try {
|
|
39629
40031
|
const eligible = (await this.store.query.query({
|
|
39630
40032
|
collection: PLATES_COLLECTION,
|
|
@@ -39635,6 +40037,7 @@ var PlateStore = class {
|
|
|
39635
40037
|
collection: PLATES_COLLECTION,
|
|
39636
40038
|
key: row.id
|
|
39637
40039
|
});
|
|
40040
|
+
removed++;
|
|
39638
40041
|
} catch (err) {
|
|
39639
40042
|
this.logger.warn("PlateStore.deleteByTracks delete failed", { meta: {
|
|
39640
40043
|
plateId: row.id,
|
|
@@ -39647,6 +40050,11 @@ var PlateStore = class {
|
|
|
39647
40050
|
error: String(err)
|
|
39648
40051
|
} });
|
|
39649
40052
|
}
|
|
40053
|
+
return [{
|
|
40054
|
+
family: "plates",
|
|
40055
|
+
rows: removed,
|
|
40056
|
+
bytes: 0
|
|
40057
|
+
}];
|
|
39650
40058
|
}
|
|
39651
40059
|
/**
|
|
39652
40060
|
* Hold the unassigned plate buffer to `maxPerDevice` newest reads per camera.
|
|
@@ -40768,6 +41176,36 @@ var TrackStore = class {
|
|
|
40768
41176
|
dropActive(trackId) {
|
|
40769
41177
|
this.active.delete(trackId);
|
|
40770
41178
|
}
|
|
41179
|
+
/**
|
|
41180
|
+
* Close a still-ACTIVE track NOW: persist its row, then drop the in-RAM
|
|
41181
|
+
* entry. The early-close counterpart of {@link expireStale}, for a caller
|
|
41182
|
+
* that knows the track is over before its TTL says so.
|
|
41183
|
+
*
|
|
41184
|
+
* **This exists because {@link dropActive} orphans everything the track
|
|
41185
|
+
* owns.** A track's media, object events and CLIP vector are keyed by trackId
|
|
41186
|
+
* and written throughout its life; the ROW is what retention cascades from,
|
|
41187
|
+
* and it is written only here or at expiry. Forgetting a lived-in track
|
|
41188
|
+
* therefore leaves a full media set — key frame, thumbnail, first/last frame,
|
|
41189
|
+
* the snapshot filmstrip — with no owner any sweep can ever reach. Measured on
|
|
41190
|
+
* the live hub 2026-08-14: 74,700 such media rows, 18.9 GB, growing
|
|
41191
|
+
* ~1.15 GB/day, plus 23,296 object events and 4,543 CLIP vectors (D161).
|
|
41192
|
+
*
|
|
41193
|
+
* `dropActive` is still correct for a birth RETRACTED before it lived —
|
|
41194
|
+
* nothing has been written under that id yet. It is never correct for a track
|
|
41195
|
+
* that ran.
|
|
41196
|
+
*
|
|
41197
|
+
* Returns whether an active entry was found. THROWS if the persist fails: the
|
|
41198
|
+
* caller must log it, because a swallowed failure here IS the leak.
|
|
41199
|
+
*/
|
|
41200
|
+
async closeActive(trackId) {
|
|
41201
|
+
const t = this.active.get(trackId);
|
|
41202
|
+
if (t === void 0) return false;
|
|
41203
|
+
t.active = false;
|
|
41204
|
+
const record = cloneTrack(t);
|
|
41205
|
+
await this.persistCompleted(record);
|
|
41206
|
+
this.active.delete(trackId);
|
|
41207
|
+
return true;
|
|
41208
|
+
}
|
|
40771
41209
|
/** Delete the persisted track row (keyed by trackId) and drop the in-RAM
|
|
40772
41210
|
* active entry if present. Used by the whole-track deletion cascade. */
|
|
40773
41211
|
async deletePersisted(trackId) {
|
|
@@ -42118,6 +42556,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
42118
42556
|
} });
|
|
42119
42557
|
}
|
|
42120
42558
|
}
|
|
42559
|
+
if (deps.breathe) await deps.breathe();
|
|
42121
42560
|
if (ceilingHit) {
|
|
42122
42561
|
stoppedBy = "reclaim-ceiling";
|
|
42123
42562
|
offset += rows.length - reclaimedInPage;
|
|
@@ -46893,7 +47332,11 @@ var ObjectEmbeddingStore = class {
|
|
|
46893
47332
|
* breaks recognition (design §2/§4).
|
|
46894
47333
|
*/
|
|
46895
47334
|
async deleteByTracks(trackIds) {
|
|
46896
|
-
if (trackIds.length === 0) return
|
|
47335
|
+
if (trackIds.length === 0) return [{
|
|
47336
|
+
family: "embeddings",
|
|
47337
|
+
rows: 0,
|
|
47338
|
+
bytes: 0
|
|
47339
|
+
}];
|
|
46897
47340
|
try {
|
|
46898
47341
|
await this.vectorStore.deleteByIds.mutate({
|
|
46899
47342
|
index: OBJECT_CLIP_INDEX,
|
|
@@ -46904,7 +47347,17 @@ var ObjectEmbeddingStore = class {
|
|
|
46904
47347
|
count: trackIds.length,
|
|
46905
47348
|
error: String(err)
|
|
46906
47349
|
} });
|
|
47350
|
+
return [{
|
|
47351
|
+
family: "embeddings",
|
|
47352
|
+
rows: 0,
|
|
47353
|
+
bytes: 0
|
|
47354
|
+
}];
|
|
46907
47355
|
}
|
|
47356
|
+
return [{
|
|
47357
|
+
family: "embeddings",
|
|
47358
|
+
rows: trackIds.length,
|
|
47359
|
+
bytes: 0
|
|
47360
|
+
}];
|
|
46908
47361
|
}
|
|
46909
47362
|
};
|
|
46910
47363
|
/** Build the vector-store metadata for one embedding. */
|
|
@@ -47778,7 +48231,8 @@ var NO_PUBLICATION = {
|
|
|
47778
48231
|
var customActions = {
|
|
47779
48232
|
...ncActions,
|
|
47780
48233
|
...embeddingActions,
|
|
47781
|
-
...debugActions
|
|
48234
|
+
...debugActions,
|
|
48235
|
+
...orphanAuditActions
|
|
47782
48236
|
};
|
|
47783
48237
|
/**
|
|
47784
48238
|
* Assist threshold when the caller does not pin one. Packages have their own
|
|
@@ -48756,7 +49210,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
48756
49210
|
customActions: {
|
|
48757
49211
|
...ncActions,
|
|
48758
49212
|
...embeddingActions,
|
|
48759
|
-
...debugActions
|
|
49213
|
+
...debugActions,
|
|
49214
|
+
...orphanAuditActions
|
|
48760
49215
|
},
|
|
48761
49216
|
actionHandlers: {
|
|
48762
49217
|
...ncHandlers,
|
|
@@ -49474,6 +49929,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
49474
49929
|
}
|
|
49475
49930
|
return out;
|
|
49476
49931
|
},
|
|
49932
|
+
readSceneStates: () => (this.sceneProvider?.listAllEnabled() ?? []).map(({ deviceId, monitor }) => ({
|
|
49933
|
+
sceneId: monitor.id,
|
|
49934
|
+
deviceId,
|
|
49935
|
+
label: monitor.label,
|
|
49936
|
+
verdict: monitor.verdict,
|
|
49937
|
+
latched: monitor.latched,
|
|
49938
|
+
emit: monitor.emit
|
|
49939
|
+
})),
|
|
49477
49940
|
readOccupancySnapshots: async (deviceIds) => {
|
|
49478
49941
|
const za = this.zoneAnalytics;
|
|
49479
49942
|
if (za === null) return /* @__PURE__ */ new Map();
|
|
@@ -50272,6 +50735,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
50272
50735
|
data: payload
|
|
50273
50736
|
});
|
|
50274
50737
|
},
|
|
50738
|
+
onSceneEdge: (edge) => {
|
|
50739
|
+
this.notificationCenter?.onSceneEdge({
|
|
50740
|
+
sceneId: edge.monitorId,
|
|
50741
|
+
deviceId: edge.deviceId,
|
|
50742
|
+
label: edge.monitorLabel,
|
|
50743
|
+
verdict: edge.verdict,
|
|
50744
|
+
latched: edge.latched,
|
|
50745
|
+
emit: edge.emit
|
|
50746
|
+
});
|
|
50747
|
+
},
|
|
50275
50748
|
currentCondition: async (deviceId) => resolveCondition({
|
|
50276
50749
|
...await this.sceneConditionContext(deviceId),
|
|
50277
50750
|
now: /* @__PURE__ */ new Date()
|
|
@@ -50280,6 +50753,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
50280
50753
|
});
|
|
50281
50754
|
provider.hydrate();
|
|
50282
50755
|
this.sceneProvider = provider;
|
|
50756
|
+
this.notificationCenter?.refreshSceneStates();
|
|
50283
50757
|
if (!this.isPostProcessingNode) {
|
|
50284
50758
|
log.info("scene evaluation INERT on this node — CRUD only");
|
|
50285
50759
|
return;
|
|
@@ -52741,7 +53215,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52741
53215
|
};
|
|
52742
53216
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
52743
53217
|
processor.dropTrack(track.trackId);
|
|
52744
|
-
this.trackStore?.
|
|
53218
|
+
this.trackStore?.closeActive(track.trackId).catch((err) => {
|
|
53219
|
+
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
53220
|
+
tags: { deviceId },
|
|
53221
|
+
meta: {
|
|
53222
|
+
deviceId,
|
|
53223
|
+
trackId: track.trackId,
|
|
53224
|
+
entryId: entry.id,
|
|
53225
|
+
error: String(err)
|
|
53226
|
+
}
|
|
53227
|
+
});
|
|
53228
|
+
});
|
|
52745
53229
|
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, track.trackId);
|
|
52746
53230
|
const dropKeyFrame = () => {
|
|
52747
53231
|
this.residents.dropKeyFrameKey(track.trackId);
|
|
@@ -53331,19 +53815,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53331
53815
|
* stops at a per-scope row ceiling, RESUMES from a persisted cursor next run,
|
|
53332
53816
|
* and refuses to start if the previous run has not finished.
|
|
53333
53817
|
*/
|
|
53334
|
-
async runOrphanAuditPass() {
|
|
53335
|
-
if (this.shuttingDown) return;
|
|
53818
|
+
async runOrphanAuditPass(overrides) {
|
|
53819
|
+
if (this.shuttingDown) return null;
|
|
53336
53820
|
if (this.orphanAuditRunning) {
|
|
53337
53821
|
this.ctx.logger.info("analytics orphan audit skipped — previous run still in flight");
|
|
53338
|
-
return;
|
|
53822
|
+
return null;
|
|
53339
53823
|
}
|
|
53340
53824
|
const trackStore = this.trackStore;
|
|
53341
53825
|
const eventStore = this.eventStore;
|
|
53342
53826
|
const mediaStore = this.mediaStore;
|
|
53343
|
-
if (!trackStore || !eventStore || !mediaStore) return;
|
|
53827
|
+
if (!trackStore || !eventStore || !mediaStore) return null;
|
|
53344
53828
|
this.orphanAuditRunning = true;
|
|
53345
53829
|
try {
|
|
53346
|
-
const limits = await this.orphanAuditLimits();
|
|
53830
|
+
const limits = await this.orphanAuditLimits(overrides);
|
|
53347
53831
|
const deps = {
|
|
53348
53832
|
page: async (collection, offset, limit) => {
|
|
53349
53833
|
if (collection === "pipeline-analytics:media") return mediaStore.pageForAudit(offset, limit);
|
|
@@ -53375,13 +53859,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53375
53859
|
});
|
|
53376
53860
|
},
|
|
53377
53861
|
now: () => Date.now(),
|
|
53378
|
-
logger: this.ctx.logger
|
|
53862
|
+
logger: this.ctx.logger,
|
|
53863
|
+
breathe: () => new Promise((resolve) => setImmediate(resolve))
|
|
53379
53864
|
};
|
|
53380
53865
|
const { report, cursors } = await runOrphanAudit({
|
|
53381
53866
|
scopes: buildOrphanScopes(),
|
|
53382
53867
|
deps,
|
|
53383
53868
|
limits,
|
|
53384
|
-
cursors: this.orphanAuditCursors
|
|
53869
|
+
cursors: overrides?.restart === true ? /* @__PURE__ */ new Map() : this.orphanAuditCursors
|
|
53385
53870
|
});
|
|
53386
53871
|
this.orphanAuditCursors = cursors;
|
|
53387
53872
|
for (const scope of report.scopes) {
|
|
@@ -53416,8 +53901,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53416
53901
|
bytesReclaimed: report.totalBytesReclaimed,
|
|
53417
53902
|
detail: `orphan audit (${report.mode}): ${report.totalOrphans} orphaned rows across ${report.scopes.length} collections, ${report.totalReclaimed} reclaimed in ${report.durationMs}ms`
|
|
53418
53903
|
});
|
|
53904
|
+
return report;
|
|
53419
53905
|
} catch (err) {
|
|
53420
53906
|
this.ctx.logger.warn("analytics orphan audit failed", { meta: { error: String(err) } });
|
|
53907
|
+
return null;
|
|
53421
53908
|
} finally {
|
|
53422
53909
|
this.orphanAuditRunning = false;
|
|
53423
53910
|
}
|
|
@@ -53480,15 +53967,65 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53480
53967
|
* raising them is raising the blast radius of a bug, and that should take a
|
|
53481
53968
|
* release.
|
|
53482
53969
|
*/
|
|
53483
|
-
async orphanAuditLimits() {
|
|
53970
|
+
async orphanAuditLimits(overrides) {
|
|
53484
53971
|
const mode = await this.orphanAuditModeState.get();
|
|
53485
53972
|
const pageSize = await this.orphanAuditPageSizeState.get();
|
|
53973
|
+
const graceMinutes = overrides?.graceMinutes;
|
|
53974
|
+
return {
|
|
53975
|
+
mode: overrides !== void 0 ? overrides.mode ?? "report" : mode === "reclaim" ? "reclaim" : "report",
|
|
53976
|
+
graceMs: graceMinutes === void 0 ? DEFAULT_ORPHAN_LIMITS.graceMs : graceMinutes * 6e4,
|
|
53977
|
+
pageSize: overrides?.pageSize ?? pageSize,
|
|
53978
|
+
maxRowsPerScope: overrides?.maxRowsPerScope ?? DEFAULT_ORPHAN_LIMITS.maxRowsPerScope,
|
|
53979
|
+
maxReclaimPerScope: overrides?.maxReclaimPerScope ?? DEFAULT_ORPHAN_LIMITS.maxReclaimPerScope
|
|
53980
|
+
};
|
|
53981
|
+
}
|
|
53982
|
+
/**
|
|
53983
|
+
* One operator-driven orphan-audit pass — the entry point behind
|
|
53984
|
+
* `retention.orphanAudit`.
|
|
53985
|
+
*
|
|
53986
|
+
* DRY RUN BY DEFAULT: an input with no `mode` reports and deletes nothing,
|
|
53987
|
+
* whatever `orphanAuditMode` is set to. Reclaiming is a word the caller has to
|
|
53988
|
+
* type. Draining the standing 18.9 GB means running this with
|
|
53989
|
+
* `mode: 'reclaim'` repeatedly until `totalOrphans` reaches 0 (D161).
|
|
53990
|
+
*/
|
|
53991
|
+
async runOrphanAuditNow(overrides) {
|
|
53992
|
+
const report = await this.runOrphanAuditPass(overrides);
|
|
53993
|
+
if (report === null) return {
|
|
53994
|
+
ran: false,
|
|
53995
|
+
mode: overrides.mode ?? "report",
|
|
53996
|
+
scopes: [],
|
|
53997
|
+
totalOrphans: 0,
|
|
53998
|
+
totalReclaimed: 0,
|
|
53999
|
+
totalBytesReclaimed: 0,
|
|
54000
|
+
durationMs: 0
|
|
54001
|
+
};
|
|
54002
|
+
this.ctx.logger.info("analytics orphan audit (operator)", { meta: {
|
|
54003
|
+
mode: report.mode,
|
|
54004
|
+
orphans: report.totalOrphans,
|
|
54005
|
+
reclaimed: report.totalReclaimed,
|
|
54006
|
+
bytesReclaimed: report.totalBytesReclaimed,
|
|
54007
|
+
durationMs: report.durationMs,
|
|
54008
|
+
restart: overrides.restart === true
|
|
54009
|
+
} });
|
|
53486
54010
|
return {
|
|
53487
|
-
|
|
53488
|
-
|
|
53489
|
-
|
|
53490
|
-
|
|
53491
|
-
|
|
54011
|
+
ran: true,
|
|
54012
|
+
mode: report.mode,
|
|
54013
|
+
scopes: report.scopes.map((scope) => ({
|
|
54014
|
+
collection: scope.collection,
|
|
54015
|
+
examined: scope.examined,
|
|
54016
|
+
orphans: scope.orphans,
|
|
54017
|
+
reclaimed: scope.reclaimed,
|
|
54018
|
+
bytesReclaimed: scope.bytesReclaimed,
|
|
54019
|
+
exempt: scope.exempt,
|
|
54020
|
+
withinGrace: scope.withinGrace,
|
|
54021
|
+
unresolvable: scope.unresolvable,
|
|
54022
|
+
stoppedBy: scope.stoppedBy,
|
|
54023
|
+
nextCursor: scope.nextCursor
|
|
54024
|
+
})),
|
|
54025
|
+
totalOrphans: report.totalOrphans,
|
|
54026
|
+
totalReclaimed: report.totalReclaimed,
|
|
54027
|
+
totalBytesReclaimed: report.totalBytesReclaimed,
|
|
54028
|
+
durationMs: report.durationMs
|
|
53492
54029
|
};
|
|
53493
54030
|
}
|
|
53494
54031
|
/**
|
|
@@ -53688,7 +54225,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53688
54225
|
indexed
|
|
53689
54226
|
};
|
|
53690
54227
|
},
|
|
53691
|
-
"embedding.wipe": async () => this.wipeObjectEmbeddings()
|
|
54228
|
+
"embedding.wipe": async () => this.wipeObjectEmbeddings(),
|
|
54229
|
+
"retention.orphanAudit": async (input) => this.runOrphanAuditNow(input)
|
|
53692
54230
|
};
|
|
53693
54231
|
}
|
|
53694
54232
|
/** Progress of the detached rebuild — the only way to observe a long pass. */
|
|
@@ -53947,8 +54485,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53947
54485
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
53948
54486
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
53949
54487
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|
|
53950
|
-
* §4).
|
|
53951
|
-
*
|
|
54488
|
+
* §4).
|
|
54489
|
+
*
|
|
54490
|
+
* **Every family count here is REAL.** It used to return `tracks` and a
|
|
54491
|
+
* literal 0 for every leaf, because `TrackScopedStore.deleteByTracks` returned
|
|
54492
|
+
* `void` — so `bytesReclaimed: 0` went onto all 3,732 ops-log rows this addon
|
|
54493
|
+
* had ever written and 18.9 GB of leaked media read as "nothing to reclaim"
|
|
54494
|
+
* (D161). The bytes ride {@link lastPruneBytes} rather than
|
|
54495
|
+
* `TrackCascadeCounts`, which is a cap output shape.
|
|
53952
54496
|
*/
|
|
53953
54497
|
async pruneTracksBefore(input) {
|
|
53954
54498
|
const empty = {
|
|
@@ -53959,26 +54503,32 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53959
54503
|
plates: 0,
|
|
53960
54504
|
embeddings: 0
|
|
53961
54505
|
};
|
|
54506
|
+
this.lastPruneBytes = 0;
|
|
53962
54507
|
const cascade = this.buildTrackCascadeRegistry();
|
|
53963
54508
|
if (!cascade) return empty;
|
|
54509
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
53964
54510
|
const { deleted: totalDeleted, skipped } = await drainTracksBefore({
|
|
53965
54511
|
stopped: () => this.shuttingDown,
|
|
53966
54512
|
listIdsBefore: (offset, limit) => cascade.trackStore.listIdsBefore(input.deviceId, input.cutoffMs, limit, offset),
|
|
53967
|
-
cascade: (ids) =>
|
|
53968
|
-
|
|
53969
|
-
|
|
53970
|
-
|
|
53971
|
-
|
|
53972
|
-
|
|
53973
|
-
|
|
53974
|
-
|
|
53975
|
-
|
|
53976
|
-
|
|
53977
|
-
|
|
53978
|
-
|
|
53979
|
-
|
|
53980
|
-
|
|
53981
|
-
|
|
54513
|
+
cascade: async (ids) => {
|
|
54514
|
+
const result = await runTrackCascadeBatch({
|
|
54515
|
+
registry: cascade.registry,
|
|
54516
|
+
trackStore: cascade.trackStore,
|
|
54517
|
+
deviceId: input.deviceId,
|
|
54518
|
+
onTrackCleanup: (deviceId, trackId) => this.clearLiveTrackState(deviceId, trackId),
|
|
54519
|
+
onFailure: (trackId, err) => {
|
|
54520
|
+
this.ctx.logger.debug("pruneTracksBefore track failed", {
|
|
54521
|
+
tags: { deviceId: input.deviceId },
|
|
54522
|
+
meta: {
|
|
54523
|
+
trackId,
|
|
54524
|
+
error: String(err)
|
|
54525
|
+
}
|
|
54526
|
+
});
|
|
54527
|
+
}
|
|
54528
|
+
}, ids);
|
|
54529
|
+
tally = mergeCascadeTallies(tally, result.tally);
|
|
54530
|
+
return { deleted: result.deleted };
|
|
54531
|
+
}
|
|
53982
54532
|
});
|
|
53983
54533
|
if (skipped > 0) this.ctx.logger.warn("analytics track retention skipped undeletable rows", {
|
|
53984
54534
|
tags: { deviceId: input.deviceId },
|
|
@@ -53989,20 +54539,41 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
53989
54539
|
pruned: totalDeleted
|
|
53990
54540
|
}
|
|
53991
54541
|
});
|
|
54542
|
+
this.lastPruneBytes = tally.bytesReclaimed;
|
|
53992
54543
|
if (totalDeleted > 0) this.ctx.logger.info("analytics track retention prune", {
|
|
53993
54544
|
tags: { deviceId: input.deviceId },
|
|
53994
54545
|
meta: {
|
|
53995
54546
|
deviceId: input.deviceId,
|
|
53996
54547
|
cutoffMs: input.cutoffMs,
|
|
53997
|
-
tracks: totalDeleted
|
|
54548
|
+
tracks: totalDeleted,
|
|
54549
|
+
events: tally.events,
|
|
54550
|
+
media: tally.media,
|
|
54551
|
+
faces: tally.faces,
|
|
54552
|
+
plates: tally.plates,
|
|
54553
|
+
embeddings: tally.embeddings,
|
|
54554
|
+
bytesReclaimed: tally.bytesReclaimed
|
|
53998
54555
|
}
|
|
53999
54556
|
});
|
|
54000
54557
|
return {
|
|
54001
54558
|
...empty,
|
|
54002
|
-
tracks: totalDeleted
|
|
54559
|
+
tracks: totalDeleted,
|
|
54560
|
+
events: tally.events,
|
|
54561
|
+
media: tally.media,
|
|
54562
|
+
faces: tally.faces,
|
|
54563
|
+
plates: tally.plates,
|
|
54564
|
+
embeddings: tally.embeddings
|
|
54003
54565
|
};
|
|
54004
54566
|
}
|
|
54005
54567
|
/**
|
|
54568
|
+
* Bytes released by the most recent {@link pruneTracksBefore}.
|
|
54569
|
+
*
|
|
54570
|
+
* A field rather than a return value because `TrackCascadeCounts` is a cap
|
|
54571
|
+
* OUTPUT shape and widening it is a codegen change; the ops-log row already
|
|
54572
|
+
* has a `bytesReclaimed` column and this is what fills it. Reset to 0 at the
|
|
54573
|
+
* top of every prune, so a stale value can never be reported as a fresh one.
|
|
54574
|
+
*/
|
|
54575
|
+
lastPruneBytes = 0;
|
|
54576
|
+
/**
|
|
54006
54577
|
* Operator "clean slate" for a device (design §5.3): prune EVERY persisted
|
|
54007
54578
|
* track via the same cascade as `pruneTracksBefore` with `cutoffMs = now`.
|
|
54008
54579
|
* Enrolled gallery + identity media are exempt.
|
|
@@ -54056,8 +54627,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
54056
54627
|
reason: "retention",
|
|
54057
54628
|
deviceId,
|
|
54058
54629
|
itemsAffected: counts.tracks,
|
|
54059
|
-
bytesReclaimed:
|
|
54060
|
-
detail: `track retention cascade: ${counts.tracks} tracks, ${counts.events} events, ${counts.media} media`
|
|
54630
|
+
bytesReclaimed: this.lastPruneBytes,
|
|
54631
|
+
detail: `track retention cascade: ${counts.tracks} tracks, ${counts.events} events, ${counts.media} media, ${counts.faces} faces, ${counts.plates} plates, ${counts.embeddings} vectors`
|
|
54061
54632
|
});
|
|
54062
54633
|
return counts;
|
|
54063
54634
|
},
|