@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Bs6oboqZ.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -4821,6 +4821,7 @@ var SceneMonitorProvider = class {
|
|
|
4821
4821
|
direction,
|
|
4822
4822
|
verdict: result.verdict,
|
|
4823
4823
|
latched,
|
|
4824
|
+
emit: monitor.emit,
|
|
4824
4825
|
condition: result.currentCondition,
|
|
4825
4826
|
confidence: result.lastConfidence,
|
|
4826
4827
|
timestamp: result.checkedAt
|
|
@@ -6485,6 +6486,110 @@ function maskPolygonOverlap(mask, maskWidth, maskHeight, bbox, polygon, _frameWi
|
|
|
6485
6486
|
if (totalMaskPixels === 0) return 0;
|
|
6486
6487
|
return insidePolygon / totalMaskPixels;
|
|
6487
6488
|
}
|
|
6489
|
+
var NcSceneStateCache = class {
|
|
6490
|
+
deps;
|
|
6491
|
+
entries = /* @__PURE__ */ new Map();
|
|
6492
|
+
noted = /* @__PURE__ */ new Map();
|
|
6493
|
+
ids = [];
|
|
6494
|
+
now;
|
|
6495
|
+
constructor(deps) {
|
|
6496
|
+
this.deps = deps;
|
|
6497
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6498
|
+
}
|
|
6499
|
+
/**
|
|
6500
|
+
* The scene ids worth reading — recomputed from the rule set, so a cluster
|
|
6501
|
+
* with no scene-gated rule does no work at all and logs nothing.
|
|
6502
|
+
*/
|
|
6503
|
+
setWatched(ids) {
|
|
6504
|
+
this.ids = [...new Set(ids)].sort();
|
|
6505
|
+
}
|
|
6506
|
+
watched() {
|
|
6507
|
+
return this.ids;
|
|
6508
|
+
}
|
|
6509
|
+
/**
|
|
6510
|
+
* "This scene, in this process, is in this state, NOW."
|
|
6511
|
+
*
|
|
6512
|
+
* Synchronous by contract — the caller is the scene provider's own edge tap
|
|
6513
|
+
* and the next rule evaluation must already see it.
|
|
6514
|
+
*/
|
|
6515
|
+
note(observation) {
|
|
6516
|
+
this.noted.set(observation.sceneId, {
|
|
6517
|
+
observation,
|
|
6518
|
+
at: this.now()
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
6521
|
+
/** Hot-path read. No I/O, no await — this is called per event. */
|
|
6522
|
+
get(sceneId) {
|
|
6523
|
+
const now = this.now();
|
|
6524
|
+
const noted = this.noted.get(sceneId);
|
|
6525
|
+
if (noted !== void 0) {
|
|
6526
|
+
if (now - noted.at <= 3e5) return noted.observation;
|
|
6527
|
+
this.noted.delete(sceneId);
|
|
6528
|
+
}
|
|
6529
|
+
const entry = this.entries.get(sceneId);
|
|
6530
|
+
if (entry === void 0) return void 0;
|
|
6531
|
+
if (now - entry.at > 3e5) return void 0;
|
|
6532
|
+
return entry.observation;
|
|
6533
|
+
}
|
|
6534
|
+
/**
|
|
6535
|
+
* Re-read every watched scene. Never throws: a gate that could fail the
|
|
6536
|
+
* evaluation tick would take the notification pipeline with it.
|
|
6537
|
+
*
|
|
6538
|
+
* On failure the previous entries are KEPT — their timestamps are not
|
|
6539
|
+
* touched, so they age out on their own rather than vanishing at once.
|
|
6540
|
+
*/
|
|
6541
|
+
refresh() {
|
|
6542
|
+
if (this.ids.length === 0) {
|
|
6543
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
6544
|
+
this.noted.clear();
|
|
6545
|
+
return;
|
|
6546
|
+
}
|
|
6547
|
+
let observations;
|
|
6548
|
+
try {
|
|
6549
|
+
observations = this.deps.readScenes();
|
|
6550
|
+
} catch (err) {
|
|
6551
|
+
this.deps.logger.warn("scene-state refresh failed — keeping the previous states until they age out", { meta: {
|
|
6552
|
+
watched: this.ids.length,
|
|
6553
|
+
error: String(err)
|
|
6554
|
+
} });
|
|
6555
|
+
return;
|
|
6556
|
+
}
|
|
6557
|
+
const at = this.now();
|
|
6558
|
+
const wanted = new Set(this.ids);
|
|
6559
|
+
const next = /* @__PURE__ */ new Map();
|
|
6560
|
+
for (const observation of observations) {
|
|
6561
|
+
if (!wanted.has(observation.sceneId)) continue;
|
|
6562
|
+
next.set(observation.sceneId, {
|
|
6563
|
+
observation,
|
|
6564
|
+
at
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6567
|
+
this.entries = next;
|
|
6568
|
+
for (const sceneId of this.ids) {
|
|
6569
|
+
if (next.has(sceneId)) continue;
|
|
6570
|
+
this.deps.logger.info("scene gate: no state for a watched scene", { meta: { sceneId } });
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
};
|
|
6574
|
+
/**
|
|
6575
|
+
* Does this observation satisfy the rule's scene condition?
|
|
6576
|
+
*
|
|
6577
|
+
* Exported and pure so the semantics can be tested without a cache, a provider
|
|
6578
|
+
* or a rule — and so there is exactly ONE place that decides what `latched`
|
|
6579
|
+
* absent means. See `NcSceneConditionSchema` for the reasoning behind each
|
|
6580
|
+
* branch.
|
|
6581
|
+
*/
|
|
6582
|
+
function sceneConditionSatisfied(condition, observation) {
|
|
6583
|
+
if (condition.latched ?? observation.emit === "latched") return observation.latched === (condition.requiredState === "diverged");
|
|
6584
|
+
return observation.verdict === condition.requiredState;
|
|
6585
|
+
}
|
|
6586
|
+
/** One-line "why", for the suppression log. Never a bare boolean: a drop the
|
|
6587
|
+
* operator cannot explain is the failure this whole feature is one misstep
|
|
6588
|
+
* away from. */
|
|
6589
|
+
function sceneGateReason(condition, observation) {
|
|
6590
|
+
if (observation === void 0) return "no mirrored state for this scene";
|
|
6591
|
+
return condition.latched ?? observation.emit === "latched" ? `latched=${observation.latched} (wanted ${condition.requiredState})` : `verdict=${observation.verdict} (wanted ${condition.requiredState})`;
|
|
6592
|
+
}
|
|
6488
6593
|
//#endregion
|
|
6489
6594
|
//#region src/notification-center/zone-ownership.ts
|
|
6490
6595
|
var UNCHANGED = { kind: "unchanged" };
|
|
@@ -6821,6 +6926,7 @@ function presentConditionIds(c) {
|
|
|
6821
6926
|
if (c.packagePhase !== void 0 && c.packagePhase !== "both") ids.push("packagePhase");
|
|
6822
6927
|
if (c.customZones !== void 0 && c.customZones.length > 0) ids.push("customZones");
|
|
6823
6928
|
if (c.occupancy !== void 0) ids.push("occupancy");
|
|
6929
|
+
if (c.scene !== void 0) ids.push("scene");
|
|
6824
6930
|
return ids;
|
|
6825
6931
|
}
|
|
6826
6932
|
/**
|
|
@@ -7000,7 +7106,7 @@ function matchesSystemEvent(condition, subject) {
|
|
|
7000
7106
|
if (condition.packageNames !== void 0 && (subject.packageName === void 0 || !condition.packageNames.includes(subject.packageName))) return false;
|
|
7001
7107
|
return true;
|
|
7002
7108
|
}
|
|
7003
|
-
function evaluateRule(rule, subject, deviceState, zoneOwners) {
|
|
7109
|
+
function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
7004
7110
|
if (subject.kind === "system-event") {
|
|
7005
7111
|
if (rule.delivery !== "system-event") return fail("delivery");
|
|
7006
7112
|
const condition = rule.conditions.systemEvent;
|
|
@@ -7041,6 +7147,11 @@ function evaluateRule(rule, subject, deviceState, zoneOwners) {
|
|
|
7041
7147
|
if (current === void 0) return fail("deviceState");
|
|
7042
7148
|
if (!toLowerSet(c.deviceState.states).has(current.trim().toLowerCase())) return fail("deviceState");
|
|
7043
7149
|
}
|
|
7150
|
+
if (c.scene !== void 0) {
|
|
7151
|
+
const observation = sceneStates?.(c.scene.sceneId);
|
|
7152
|
+
if (observation === void 0) return fail("scene");
|
|
7153
|
+
if (!sceneConditionSatisfied(c.scene, observation)) return fail("scene");
|
|
7154
|
+
}
|
|
7044
7155
|
if (c.devices !== void 0 && c.devices.length > 0 && !matchesDeviceScope(c.devices, subject)) return fail("devices");
|
|
7045
7156
|
if (c.source !== void 0 && c.source !== "any") {
|
|
7046
7157
|
if ((subject.source ?? "pipeline") !== c.source) return fail("source");
|
|
@@ -17644,6 +17755,22 @@ var NotificationCenter = class NotificationCenter {
|
|
|
17644
17755
|
/** States the `deviceState` rule gate reads. Refreshed on the reload tick;
|
|
17645
17756
|
* read with no I/O on the persist hook. */
|
|
17646
17757
|
deviceStates;
|
|
17758
|
+
/** Scene verdicts + latches the `scene` rule gate reads. Same contract as
|
|
17759
|
+
* {@link deviceStates}: refreshed on the reload tick and on the provider's
|
|
17760
|
+
* in-process edge tap, read with no I/O on the persist hook (D49). */
|
|
17761
|
+
sceneStates;
|
|
17762
|
+
/**
|
|
17763
|
+
* The last scene-gate answer REPORTED per rule, so the suppression line is
|
|
17764
|
+
* written on CHANGE rather than on every event.
|
|
17765
|
+
*
|
|
17766
|
+
* A branch that drops work silently must log it, and a gate that logged on
|
|
17767
|
+
* every detection on a busy camera would be a line nobody reads — which is
|
|
17768
|
+
* the same silence by a different route.
|
|
17769
|
+
*/
|
|
17770
|
+
sceneGateReported = /* @__PURE__ */ new Map();
|
|
17771
|
+
/** The watched scene id set, as last REPORTED — so the line is written when it
|
|
17772
|
+
* changes and not on every reload tick. `null` = never reported. */
|
|
17773
|
+
sceneWatchSignature = null;
|
|
17647
17774
|
/** WHO each device id is (name + type). The system-event intake reads it with
|
|
17648
17775
|
* no I/O; the reload tick refreshes it — see `device-directory.ts`. */
|
|
17649
17776
|
deviceDirectory;
|
|
@@ -18043,6 +18170,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18043
18170
|
readStates: deps.readDeviceStates ?? (async () => /* @__PURE__ */ new Map()),
|
|
18044
18171
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
18045
18172
|
});
|
|
18173
|
+
this.sceneStates = new NcSceneStateCache({
|
|
18174
|
+
logger: this.logger.child("scene-state"),
|
|
18175
|
+
readScenes: deps.readSceneStates ?? (() => []),
|
|
18176
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
18177
|
+
});
|
|
18046
18178
|
this.deviceDirectory = new NcDeviceDirectory({
|
|
18047
18179
|
logger: this.logger.child("device-directory"),
|
|
18048
18180
|
listDevices: deps.listDeviceIdentities ?? (async () => {
|
|
@@ -18242,6 +18374,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18242
18374
|
await this.deviceDirectory.refresh();
|
|
18243
18375
|
await this.hydrateLiveness();
|
|
18244
18376
|
await this.zoneOwners.refresh();
|
|
18377
|
+
this.sceneStates.refresh();
|
|
18245
18378
|
this.evaluationActive = true;
|
|
18246
18379
|
await this.outbox.load();
|
|
18247
18380
|
await this.seedCooldowns();
|
|
@@ -19006,6 +19139,73 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19006
19139
|
if (identity.isCamera && identity.detectionActive === false) return "detection-off";
|
|
19007
19140
|
}
|
|
19008
19141
|
/**
|
|
19142
|
+
* SAY that the scene gate suppressed this rule.
|
|
19143
|
+
*
|
|
19144
|
+
* A branch that drops work silently logs it — the three-hour media blackout
|
|
19145
|
+
* produced not one line, and a gate is the same shape of failure: from the
|
|
19146
|
+
* operator's side "the rule stopped notifying" and "the pipeline stopped
|
|
19147
|
+
* working" look identical. `rule did not match` covers it at DEBUG, which is
|
|
19148
|
+
* not a level this installation runs at.
|
|
19149
|
+
*
|
|
19150
|
+
* On CHANGE only, per rule: the alternative is one line per detection on a
|
|
19151
|
+
* busy camera, which is silence by a different route. The `deviceId` tag is
|
|
19152
|
+
* the SUBJECT's — that is the camera whose notification went missing, and the
|
|
19153
|
+
* question is always asked per-camera. The SCENE's own device is named in the
|
|
19154
|
+
* meta beside its id, because the two are frequently different cameras.
|
|
19155
|
+
*/
|
|
19156
|
+
reportSceneSuppression(rule, subject) {
|
|
19157
|
+
const condition = rule.conditions.scene;
|
|
19158
|
+
if (condition === void 0) return;
|
|
19159
|
+
const observation = this.sceneStates.get(condition.sceneId);
|
|
19160
|
+
const reason = sceneGateReason(condition, observation);
|
|
19161
|
+
if (this.sceneGateReported.get(rule.id) === reason) return;
|
|
19162
|
+
this.sceneGateReported.set(rule.id, reason);
|
|
19163
|
+
this.logger.info("rule suppressed by its scene condition", {
|
|
19164
|
+
tags: { deviceId: subject.deviceId },
|
|
19165
|
+
meta: {
|
|
19166
|
+
ruleId: rule.id,
|
|
19167
|
+
rule: rule.name,
|
|
19168
|
+
sceneId: condition.sceneId,
|
|
19169
|
+
sceneLabel: observation?.label,
|
|
19170
|
+
sceneDeviceId: observation?.deviceId ?? condition.deviceId,
|
|
19171
|
+
requiredState: condition.requiredState,
|
|
19172
|
+
reason
|
|
19173
|
+
}
|
|
19174
|
+
});
|
|
19175
|
+
}
|
|
19176
|
+
/**
|
|
19177
|
+
* A confirmed scene transition, straight from the provider in this process.
|
|
19178
|
+
*
|
|
19179
|
+
* Synchronous on purpose and for the same reason {@link onAlarmPublish} is:
|
|
19180
|
+
* the reload tick would otherwise be the only writer, and a rule gated on
|
|
19181
|
+
* "while the bin is out" would keep its old answer for up to thirty seconds
|
|
19182
|
+
* after the bin came back. The tick stays as reconciliation.
|
|
19183
|
+
*
|
|
19184
|
+
* A DIRECT CALL, not a bus subscription. `EnrichmentSceneStateChanged` is
|
|
19185
|
+
* still emitted for the badge and for anyone else, but D8 events are
|
|
19186
|
+
* droppable telemetry and a gate fed by droppable telemetry is a gate that is
|
|
19187
|
+
* sometimes wrong and never says so.
|
|
19188
|
+
*
|
|
19189
|
+
* This never OPENS a gate that the mirror had shut for lack of an entry: it
|
|
19190
|
+
* writes an observation the caller actually made. It is `refresh` that
|
|
19191
|
+
* removes entries, and it never empties on a failed read (D49).
|
|
19192
|
+
*/
|
|
19193
|
+
onSceneEdge(observation) {
|
|
19194
|
+
this.sceneStates.note(observation);
|
|
19195
|
+
}
|
|
19196
|
+
/**
|
|
19197
|
+
* Reconcile the scene mirror now.
|
|
19198
|
+
*
|
|
19199
|
+
* The scene provider is constructed AFTER this centre starts (it needs the
|
|
19200
|
+
* media store and the embedding encoder), so the boot refresh reads an empty
|
|
19201
|
+
* provider. The addon calls this once the provider has hydrated, rather than
|
|
19202
|
+
* leaving every scene-gated rule shut for a whole reload tick after every
|
|
19203
|
+
* respawn.
|
|
19204
|
+
*/
|
|
19205
|
+
refreshSceneStates() {
|
|
19206
|
+
this.sceneStates.refresh();
|
|
19207
|
+
}
|
|
19208
|
+
/**
|
|
19009
19209
|
* Everything the panel reports, on every publish.
|
|
19010
19210
|
*
|
|
19011
19211
|
* Synchronous on purpose. The state half refreshes the `deviceState` gate's
|
|
@@ -19304,8 +19504,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19304
19504
|
let noOwnerReported = false;
|
|
19305
19505
|
const rejections = [];
|
|
19306
19506
|
for (const rule of candidates) {
|
|
19307
|
-
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id), (id) => this.zoneOwners.get(id));
|
|
19507
|
+
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id), (id) => this.zoneOwners.get(id), (sceneId) => this.sceneStates.get(sceneId));
|
|
19308
19508
|
if (!evaluation.matched) {
|
|
19509
|
+
if (evaluation.failedCondition === "scene") this.reportSceneSuppression(rule, subject);
|
|
19309
19510
|
rejections.push(`${rule.name}:${evaluation.failedCondition ?? "unknown"}`);
|
|
19310
19511
|
outcomes.push({
|
|
19311
19512
|
ruleId: rule.id,
|
|
@@ -19330,6 +19531,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19330
19531
|
});
|
|
19331
19532
|
continue;
|
|
19332
19533
|
}
|
|
19534
|
+
this.sceneGateReported.delete(rule.id);
|
|
19333
19535
|
this.runRuleActions(rule, subject, kind, controls);
|
|
19334
19536
|
const admission = this.admitToGroup(rule, subject, now);
|
|
19335
19537
|
if (admission.outcome === "unchanged") {
|
|
@@ -19961,10 +20163,13 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19961
20163
|
this.reportAudioWatch(audioWatch);
|
|
19962
20164
|
const gated = [];
|
|
19963
20165
|
const zoneScoped = [];
|
|
20166
|
+
const sceneIds = [];
|
|
19964
20167
|
for (const rule of this.rules.list()) {
|
|
19965
20168
|
if (!rule.enabled) continue;
|
|
19966
20169
|
const ds = rule.conditions.deviceState;
|
|
19967
20170
|
if (ds !== void 0) gated.push(ds.deviceId);
|
|
20171
|
+
const scene = rule.conditions.scene;
|
|
20172
|
+
if (scene !== void 0) sceneIds.push(scene.sceneId);
|
|
19968
20173
|
const zones = rule.conditions.zones;
|
|
19969
20174
|
const devices = rule.conditions.devices;
|
|
19970
20175
|
if (zones !== void 0 && zones.ids.length > 0 && devices !== void 0) zoneScoped.push(...devices);
|
|
@@ -19973,6 +20178,15 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19973
20178
|
if (panel !== null) gated.push(...alarmContactWatchList(this.rules.list(), panel.deviceId, panel.availableModes()));
|
|
19974
20179
|
this.deviceStates.setWatched(gated);
|
|
19975
20180
|
this.zoneOwners.setWatched(zoneScoped);
|
|
20181
|
+
this.sceneStates.setWatched(sceneIds);
|
|
20182
|
+
const signature = [...new Set(sceneIds)].sort().join(",");
|
|
20183
|
+
if (signature !== this.sceneWatchSignature) {
|
|
20184
|
+
this.sceneWatchSignature = signature;
|
|
20185
|
+
this.logger.info("scene gate watched set", { meta: {
|
|
20186
|
+
scenes: this.sceneStates.watched(),
|
|
20187
|
+
gatedRules: sceneIds.length
|
|
20188
|
+
} });
|
|
20189
|
+
}
|
|
19976
20190
|
}
|
|
19977
20191
|
/**
|
|
19978
20192
|
* Feed every excluded sensor's mirrored state back to the panel.
|
|
@@ -20109,6 +20323,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20109
20323
|
await this.deviceStates.refresh();
|
|
20110
20324
|
await this.deviceDirectory.refresh();
|
|
20111
20325
|
await this.zoneOwners.refresh();
|
|
20326
|
+
this.sceneStates.refresh();
|
|
20112
20327
|
this.observeAlarmContacts();
|
|
20113
20328
|
await this.occupancyStore.pruneExcept(this.activeOccupancyKeys());
|
|
20114
20329
|
}
|
|
@@ -20406,12 +20621,17 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20406
20621
|
const fresh = await this.deps.readDeviceZoneIds(zoneScope);
|
|
20407
20622
|
zoneLookup = (id) => fresh.get(id) ?? this.zoneOwners.get(id);
|
|
20408
20623
|
} catch {}
|
|
20624
|
+
let sceneLookup = (id) => this.sceneStates.get(id);
|
|
20625
|
+
if (rule.conditions.scene !== void 0 && this.deps.readSceneStates !== void 0) try {
|
|
20626
|
+
const fresh = new Map(this.deps.readSceneStates().map((o) => [o.sceneId, o]));
|
|
20627
|
+
sceneLookup = (id) => fresh.get(id) ?? this.sceneStates.get(id);
|
|
20628
|
+
} catch {}
|
|
20409
20629
|
const occupancy = rule.conditions.occupancy;
|
|
20410
|
-
if (rule.delivery === "device-event" && occupancy !== void 0) return this.occupancyDryRun(rule, occupancy, lookup, zoneLookup);
|
|
20630
|
+
if (rule.delivery === "device-event" && occupancy !== void 0) return this.occupancyDryRun(rule, occupancy, lookup, zoneLookup, sceneLookup);
|
|
20411
20631
|
const results = [];
|
|
20412
20632
|
for (const subject of subjects.sort((a, b) => b.timestamp - a.timestamp)) {
|
|
20413
20633
|
if (results.length >= TEST_RULE_MAX_RESULTS) break;
|
|
20414
|
-
const evaluation = evaluateRule(rule, subject, lookup, zoneLookup);
|
|
20634
|
+
const evaluation = evaluateRule(rule, subject, lookup, zoneLookup, sceneLookup);
|
|
20415
20635
|
results.push({
|
|
20416
20636
|
recordId: subject.recordId,
|
|
20417
20637
|
recordKind: subject.kind === "track-end" ? "track" : subject.kind === "audio-event" || subject.kind === "audio-window" ? "object-event" : subject.kind === "occupancy-event" ? "device-event" : subject.kind,
|
|
@@ -20435,7 +20655,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20435
20655
|
* from the ordinary `evaluateRule` on the ordinary subject — this method
|
|
20436
20656
|
* chooses the cameras and writes the sentence, nothing else.
|
|
20437
20657
|
*/
|
|
20438
|
-
async occupancyDryRun(rule, occupancy, lookup, zoneLookup) {
|
|
20658
|
+
async occupancyDryRun(rule, occupancy, lookup, zoneLookup, sceneLookup) {
|
|
20439
20659
|
const read = this.deps.readOccupancySnapshots;
|
|
20440
20660
|
if (read === void 0) return [];
|
|
20441
20661
|
const now = this.now();
|
|
@@ -20478,7 +20698,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20478
20698
|
});
|
|
20479
20699
|
continue;
|
|
20480
20700
|
}
|
|
20481
|
-
const evaluation = evaluateRule(rule, probe.subject, lookup, zoneLookup);
|
|
20701
|
+
const evaluation = evaluateRule(rule, probe.subject, lookup, zoneLookup, sceneLookup);
|
|
20482
20702
|
results.push({
|
|
20483
20703
|
...common,
|
|
20484
20704
|
recordId: probe.subject.recordId,
|
|
@@ -20733,6 +20953,10 @@ function planRuleTest(input) {
|
|
|
20733
20953
|
conditionId: "deviceState",
|
|
20734
20954
|
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.`
|
|
20735
20955
|
});
|
|
20956
|
+
if (c.scene !== void 0) notes.push({
|
|
20957
|
+
conditionId: "scene",
|
|
20958
|
+
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.`
|
|
20959
|
+
});
|
|
20736
20960
|
if (c.customZones !== void 0 && c.customZones.length > 0) notes.push({
|
|
20737
20961
|
conditionId: "customZones",
|
|
20738
20962
|
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."
|
|
@@ -23242,6 +23466,88 @@ var embeddingActions = require_dist.defineCustomActions({
|
|
|
23242
23466
|
"embedding.wipe": require_dist.customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
|
|
23243
23467
|
});
|
|
23244
23468
|
//#endregion
|
|
23469
|
+
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
23470
|
+
/**
|
|
23471
|
+
* The operator button for the orphan audit.
|
|
23472
|
+
*
|
|
23473
|
+
* The audit engine, its five bounds and its resumable cursor already existed
|
|
23474
|
+
* and had run in `report` mode for weeks; what it had no way to be was
|
|
23475
|
+
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
23476
|
+
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
23477
|
+
* six hours, and remembering to flip it back — with nothing returned to look
|
|
23478
|
+
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
23479
|
+
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
23480
|
+
* drain by leaving a switch on.
|
|
23481
|
+
*
|
|
23482
|
+
* Three properties this action does NOT leave to the caller's care:
|
|
23483
|
+
*
|
|
23484
|
+
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
23485
|
+
* logs and returns, and deletes nothing — regardless of what the durable
|
|
23486
|
+
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
23487
|
+
* typed. The engine's own default is the same for the same reason.
|
|
23488
|
+
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
23489
|
+
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
23490
|
+
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
23491
|
+
* hub-main with every live frame.
|
|
23492
|
+
* 3. **It answers.** The report comes back per collection with rows AND
|
|
23493
|
+
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
23494
|
+
* from a log line.
|
|
23495
|
+
*/
|
|
23496
|
+
/**
|
|
23497
|
+
* `report` counts; `reclaim` deletes.
|
|
23498
|
+
*
|
|
23499
|
+
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
23500
|
+
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
23501
|
+
* unstated intent here is the one that destroys nothing.
|
|
23502
|
+
*/
|
|
23503
|
+
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
23504
|
+
var OrphanAuditActionInputSchema = require_dist.object({
|
|
23505
|
+
mode: OrphanAuditActionModeSchema,
|
|
23506
|
+
/**
|
|
23507
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
23508
|
+
*
|
|
23509
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
23510
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
23511
|
+
*/
|
|
23512
|
+
restart: require_dist.boolean().optional(),
|
|
23513
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
23514
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
23515
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
23516
|
+
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
23517
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
23518
|
+
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
23519
|
+
/**
|
|
23520
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
23521
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
23522
|
+
* the one-age-keyed-sweep guard's books.
|
|
23523
|
+
*/
|
|
23524
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
23525
|
+
});
|
|
23526
|
+
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
23527
|
+
collection: require_dist.string(),
|
|
23528
|
+
examined: require_dist.number(),
|
|
23529
|
+
orphans: require_dist.number(),
|
|
23530
|
+
reclaimed: require_dist.number(),
|
|
23531
|
+
bytesReclaimed: require_dist.number(),
|
|
23532
|
+
exempt: require_dist.number(),
|
|
23533
|
+
withinGrace: require_dist.number(),
|
|
23534
|
+
unresolvable: require_dist.number(),
|
|
23535
|
+
stoppedBy: require_dist.string(),
|
|
23536
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
23537
|
+
nextCursor: require_dist.number()
|
|
23538
|
+
});
|
|
23539
|
+
var OrphanAuditActionResultSchema = require_dist.object({
|
|
23540
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
23541
|
+
ran: require_dist.boolean(),
|
|
23542
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
23543
|
+
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
23544
|
+
totalOrphans: require_dist.number(),
|
|
23545
|
+
totalReclaimed: require_dist.number(),
|
|
23546
|
+
totalBytesReclaimed: require_dist.number(),
|
|
23547
|
+
durationMs: require_dist.number()
|
|
23548
|
+
});
|
|
23549
|
+
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
23550
|
+
//#endregion
|
|
23245
23551
|
//#region src/pipeline-analytics/map-with-concurrency.ts
|
|
23246
23552
|
/**
|
|
23247
23553
|
* Apply `mapper` to every item with at most `limit` in flight at once.
|
|
@@ -25978,6 +26284,72 @@ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG)
|
|
|
25978
26284
|
}
|
|
25979
26285
|
//#endregion
|
|
25980
26286
|
//#region src/pipeline-analytics/pipeline/delete-track-cascade.ts
|
|
26287
|
+
var EMPTY_CASCADE_TALLY = {
|
|
26288
|
+
tracks: 0,
|
|
26289
|
+
events: 0,
|
|
26290
|
+
media: 0,
|
|
26291
|
+
faces: 0,
|
|
26292
|
+
plates: 0,
|
|
26293
|
+
embeddings: 0,
|
|
26294
|
+
bytesReclaimed: 0
|
|
26295
|
+
};
|
|
26296
|
+
/** Fold a leaf store's receipts into a running tally. */
|
|
26297
|
+
function addLeafCounts(tally, counts) {
|
|
26298
|
+
let next = tally;
|
|
26299
|
+
for (const c of counts) {
|
|
26300
|
+
const bytesReclaimed = next.bytesReclaimed + c.bytes;
|
|
26301
|
+
switch (c.family) {
|
|
26302
|
+
case "events":
|
|
26303
|
+
next = {
|
|
26304
|
+
...next,
|
|
26305
|
+
events: next.events + c.rows,
|
|
26306
|
+
bytesReclaimed
|
|
26307
|
+
};
|
|
26308
|
+
break;
|
|
26309
|
+
case "media":
|
|
26310
|
+
next = {
|
|
26311
|
+
...next,
|
|
26312
|
+
media: next.media + c.rows,
|
|
26313
|
+
bytesReclaimed
|
|
26314
|
+
};
|
|
26315
|
+
break;
|
|
26316
|
+
case "faces":
|
|
26317
|
+
next = {
|
|
26318
|
+
...next,
|
|
26319
|
+
faces: next.faces + c.rows,
|
|
26320
|
+
bytesReclaimed
|
|
26321
|
+
};
|
|
26322
|
+
break;
|
|
26323
|
+
case "plates":
|
|
26324
|
+
next = {
|
|
26325
|
+
...next,
|
|
26326
|
+
plates: next.plates + c.rows,
|
|
26327
|
+
bytesReclaimed
|
|
26328
|
+
};
|
|
26329
|
+
break;
|
|
26330
|
+
case "embeddings":
|
|
26331
|
+
next = {
|
|
26332
|
+
...next,
|
|
26333
|
+
embeddings: next.embeddings + c.rows,
|
|
26334
|
+
bytesReclaimed
|
|
26335
|
+
};
|
|
26336
|
+
break;
|
|
26337
|
+
}
|
|
26338
|
+
}
|
|
26339
|
+
return next;
|
|
26340
|
+
}
|
|
26341
|
+
/** Sum two tallies (per-track results into a per-device one). */
|
|
26342
|
+
function mergeCascadeTallies(a, b) {
|
|
26343
|
+
return {
|
|
26344
|
+
tracks: a.tracks + b.tracks,
|
|
26345
|
+
events: a.events + b.events,
|
|
26346
|
+
media: a.media + b.media,
|
|
26347
|
+
faces: a.faces + b.faces,
|
|
26348
|
+
plates: a.plates + b.plates,
|
|
26349
|
+
embeddings: a.embeddings + b.embeddings,
|
|
26350
|
+
bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
|
|
26351
|
+
};
|
|
26352
|
+
}
|
|
25981
26353
|
/**
|
|
25982
26354
|
* Extensible whole-track deletion cascade. Runs every registered
|
|
25983
26355
|
* `TrackScopedStore` (each removes its ephemeral rows + owned media for the
|
|
@@ -25994,8 +26366,16 @@ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG)
|
|
|
25994
26366
|
* ]
|
|
25995
26367
|
*/
|
|
25996
26368
|
async function cascadeDeleteTracks(registry, trackStore, trackIds) {
|
|
25997
|
-
|
|
25998
|
-
for (const
|
|
26369
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
26370
|
+
for (const store of registry) tally = addLeafCounts(tally, await store.deleteByTracks(trackIds));
|
|
26371
|
+
for (const trackId of trackIds) {
|
|
26372
|
+
await trackStore.deletePersisted(trackId);
|
|
26373
|
+
tally = {
|
|
26374
|
+
...tally,
|
|
26375
|
+
tracks: tally.tracks + 1
|
|
26376
|
+
};
|
|
26377
|
+
}
|
|
26378
|
+
return tally;
|
|
25999
26379
|
}
|
|
26000
26380
|
/**
|
|
26001
26381
|
* The one registry, in the one order — leaves first, root deleted internally
|
|
@@ -26026,14 +26406,28 @@ function buildAnalyticsCascadeRegistry(stores) {
|
|
|
26026
26406
|
* embeddings) is opted in by the CALLER's registry, not by this legacy shim.
|
|
26027
26407
|
*/
|
|
26028
26408
|
async function cascadeDeleteTrack(stores, trackId) {
|
|
26029
|
-
|
|
26409
|
+
return cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
|
|
26410
|
+
let events = 0;
|
|
26411
|
+
let media = 0;
|
|
26030
26412
|
for (const id of ids) {
|
|
26031
26413
|
const eventIds = await stores.eventStore.deleteByTrack(id);
|
|
26032
|
-
|
|
26414
|
+
events += eventIds.length;
|
|
26415
|
+
if (eventIds.length > 0) media += await stores.mediaStore.deleteForEvents([...eventIds]);
|
|
26033
26416
|
}
|
|
26034
|
-
|
|
26035
|
-
|
|
26036
|
-
|
|
26417
|
+
return [{
|
|
26418
|
+
family: "events",
|
|
26419
|
+
rows: events,
|
|
26420
|
+
bytes: 0
|
|
26421
|
+
}, {
|
|
26422
|
+
family: "media",
|
|
26423
|
+
rows: media,
|
|
26424
|
+
bytes: 0
|
|
26425
|
+
}];
|
|
26426
|
+
} }, { deleteByTracks: async (ids) => [{
|
|
26427
|
+
family: "media",
|
|
26428
|
+
rows: await stores.mediaStore.deleteForTracks([...ids]),
|
|
26429
|
+
bytes: 0
|
|
26430
|
+
}] }], stores.trackStore, [trackId]);
|
|
26037
26431
|
}
|
|
26038
26432
|
/**
|
|
26039
26433
|
* Widened batch whole-track deletion — the ONE engine behind the three §5 entry
|
|
@@ -26044,9 +26438,10 @@ async function cascadeDeleteTrack(stores, trackId) {
|
|
|
26044
26438
|
*/
|
|
26045
26439
|
async function runTrackCascadeBatch(deps, trackIds) {
|
|
26046
26440
|
let deleted = 0;
|
|
26441
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
26047
26442
|
const failed = [];
|
|
26048
26443
|
for (const trackId of trackIds) try {
|
|
26049
|
-
await cascadeDeleteTracks(deps.registry, deps.trackStore, [trackId]);
|
|
26444
|
+
tally = mergeCascadeTallies(tally, await cascadeDeleteTracks(deps.registry, deps.trackStore, [trackId]));
|
|
26050
26445
|
deps.onTrackCleanup(deps.deviceId, trackId);
|
|
26051
26446
|
deleted += 1;
|
|
26052
26447
|
} catch (err) {
|
|
@@ -26055,7 +26450,8 @@ async function runTrackCascadeBatch(deps, trackIds) {
|
|
|
26055
26450
|
}
|
|
26056
26451
|
return {
|
|
26057
26452
|
deleted,
|
|
26058
|
-
failed
|
|
26453
|
+
failed,
|
|
26454
|
+
tally
|
|
26059
26455
|
};
|
|
26060
26456
|
}
|
|
26061
26457
|
//#endregion
|
|
@@ -33678,21 +34074,33 @@ var MediaStore = class {
|
|
|
33678
34074
|
* Best-effort per row; returns the number of index rows removed.
|
|
33679
34075
|
*/
|
|
33680
34076
|
async deleteForOwner(ownerKind, ownerIds) {
|
|
34077
|
+
return (await this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds))).rows;
|
|
34078
|
+
}
|
|
34079
|
+
/**
|
|
34080
|
+
* The same delete, reporting the BYTES it released as well as the row count.
|
|
34081
|
+
*
|
|
34082
|
+
* `sizeBytes` is already on the index row, so the number costs nothing — it
|
|
34083
|
+
* was simply never returned, which is why every retention ops-log row this
|
|
34084
|
+
* addon has ever written says `bytesReclaimed: 0` (D161).
|
|
34085
|
+
*/
|
|
34086
|
+
async deleteForOwnerCounted(ownerKind, ownerIds) {
|
|
33681
34087
|
return this.gated(() => this.deleteForOwnerUnchecked(ownerKind, ownerIds));
|
|
33682
34088
|
}
|
|
33683
34089
|
async deleteForOwnerUnchecked(ownerKind, ownerIds) {
|
|
33684
|
-
let
|
|
34090
|
+
let rows = 0;
|
|
34091
|
+
let bytes = 0;
|
|
33685
34092
|
for (const ownerId of ownerIds) {
|
|
33686
|
-
const
|
|
34093
|
+
const found = await this.store.query.query({
|
|
33687
34094
|
collection: MEDIA_COLLECTION,
|
|
33688
34095
|
filter: { where: {
|
|
33689
34096
|
ownerKind,
|
|
33690
34097
|
ownerId
|
|
33691
34098
|
} }
|
|
33692
34099
|
});
|
|
33693
|
-
for (const row of
|
|
34100
|
+
for (const row of found) {
|
|
33694
34101
|
const rowData = row.data;
|
|
33695
34102
|
const path = String(rowData["path"] ?? "");
|
|
34103
|
+
const sizeBytes = Number(rowData["sizeBytes"]);
|
|
33696
34104
|
try {
|
|
33697
34105
|
if (path) await this.storage.delete({
|
|
33698
34106
|
location: mediaRowLocation(rowData),
|
|
@@ -33704,7 +34112,8 @@ var MediaStore = class {
|
|
|
33704
34112
|
collection: MEDIA_COLLECTION,
|
|
33705
34113
|
key: row.id
|
|
33706
34114
|
});
|
|
33707
|
-
|
|
34115
|
+
rows++;
|
|
34116
|
+
if (Number.isFinite(sizeBytes)) bytes += sizeBytes;
|
|
33708
34117
|
} catch (err) {
|
|
33709
34118
|
this.logger.debug("media delete-for-owner failed", { meta: {
|
|
33710
34119
|
key: row.id,
|
|
@@ -33713,7 +34122,10 @@ var MediaStore = class {
|
|
|
33713
34122
|
}
|
|
33714
34123
|
}
|
|
33715
34124
|
}
|
|
33716
|
-
return
|
|
34125
|
+
return {
|
|
34126
|
+
rows,
|
|
34127
|
+
bytes
|
|
34128
|
+
};
|
|
33717
34129
|
}
|
|
33718
34130
|
/**
|
|
33719
34131
|
* On-demand footprint aggregation for the events-management UI: sum
|
|
@@ -33852,11 +34264,20 @@ var MediaStore = class {
|
|
|
33852
34264
|
* batch.
|
|
33853
34265
|
*/
|
|
33854
34266
|
async deleteByTracks(trackIds) {
|
|
33855
|
-
await this.gated(async () => {
|
|
33856
|
-
await this.deleteForOwnerUnchecked("track", trackIds);
|
|
33857
|
-
await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
|
|
34267
|
+
const own = await this.gated(async () => {
|
|
34268
|
+
const tracks = await this.deleteForOwnerUnchecked("track", trackIds);
|
|
34269
|
+
const faces = await this.deleteForOwnerUnchecked("face", trackIds.map((id) => FACE_MEDIA_OWNER_PREFIX + id));
|
|
34270
|
+
return {
|
|
34271
|
+
rows: tracks.rows + faces.rows,
|
|
34272
|
+
bytes: tracks.bytes + faces.bytes
|
|
34273
|
+
};
|
|
33858
34274
|
});
|
|
33859
|
-
await this.
|
|
34275
|
+
const plates = await this.deleteForOwnerCounted("plate", trackIds.map((id) => PLATE_MEDIA_OWNER_PREFIX + id));
|
|
34276
|
+
return [{
|
|
34277
|
+
family: "media",
|
|
34278
|
+
rows: own.rows + plates.rows,
|
|
34279
|
+
bytes: own.bytes + plates.bytes
|
|
34280
|
+
}];
|
|
33860
34281
|
}
|
|
33861
34282
|
/**
|
|
33862
34283
|
* A stable page of media INDEX rows for the orphan audit — no blobs, no
|
|
@@ -38294,11 +38715,21 @@ var EventStore = class {
|
|
|
38294
38715
|
error: String(err)
|
|
38295
38716
|
} });
|
|
38296
38717
|
}
|
|
38718
|
+
let mediaRows = 0;
|
|
38297
38719
|
if (eventIds.length > 0 && this.media !== void 0) try {
|
|
38298
|
-
await this.media.deleteForEvents(eventIds);
|
|
38720
|
+
mediaRows = await this.media.deleteForEvents(eventIds);
|
|
38299
38721
|
} catch (err) {
|
|
38300
38722
|
this.logger.warn("EventStore.deleteByTracks: event media delete failed", { meta: { error: String(err) } });
|
|
38301
38723
|
}
|
|
38724
|
+
return [{
|
|
38725
|
+
family: "events",
|
|
38726
|
+
rows: eventIds.length,
|
|
38727
|
+
bytes: 0
|
|
38728
|
+
}, {
|
|
38729
|
+
family: "media",
|
|
38730
|
+
rows: mediaRows,
|
|
38731
|
+
bytes: 0
|
|
38732
|
+
}];
|
|
38302
38733
|
}
|
|
38303
38734
|
};
|
|
38304
38735
|
function slimMotion(id, data) {
|
|
@@ -38689,6 +39120,7 @@ var FaceStore = class {
|
|
|
38689
39120
|
* aborts the batch. (Crop media of these rows is removed by `MediaStore`.)
|
|
38690
39121
|
*/
|
|
38691
39122
|
async deleteByTracks(trackIds) {
|
|
39123
|
+
let removed = 0;
|
|
38692
39124
|
for (const trackId of trackIds) try {
|
|
38693
39125
|
const eligible = (await this.store.query.query({
|
|
38694
39126
|
collection: FACES_COLLECTION,
|
|
@@ -38699,6 +39131,7 @@ var FaceStore = class {
|
|
|
38699
39131
|
collection: FACES_COLLECTION,
|
|
38700
39132
|
key: row.id
|
|
38701
39133
|
});
|
|
39134
|
+
removed++;
|
|
38702
39135
|
} catch (err) {
|
|
38703
39136
|
this.logger.warn("FaceStore.deleteByTracks delete failed", { meta: {
|
|
38704
39137
|
faceId: row.id,
|
|
@@ -38711,6 +39144,11 @@ var FaceStore = class {
|
|
|
38711
39144
|
error: String(err)
|
|
38712
39145
|
} });
|
|
38713
39146
|
}
|
|
39147
|
+
return [{
|
|
39148
|
+
family: "faces",
|
|
39149
|
+
rows: removed,
|
|
39150
|
+
bytes: 0
|
|
39151
|
+
}];
|
|
38714
39152
|
}
|
|
38715
39153
|
/** Delete a single buffered face row by id (its crop media is removed by the
|
|
38716
39154
|
* caller — the FaceStore owns rows, not blobs). Best-effort. */
|
|
@@ -39646,6 +40084,7 @@ var PlateStore = class {
|
|
|
39646
40084
|
* the batch. (Crop media of these rows is removed by `MediaStore`.)
|
|
39647
40085
|
*/
|
|
39648
40086
|
async deleteByTracks(trackIds) {
|
|
40087
|
+
let removed = 0;
|
|
39649
40088
|
for (const trackId of trackIds) try {
|
|
39650
40089
|
const eligible = (await this.store.query.query({
|
|
39651
40090
|
collection: PLATES_COLLECTION,
|
|
@@ -39656,6 +40095,7 @@ var PlateStore = class {
|
|
|
39656
40095
|
collection: PLATES_COLLECTION,
|
|
39657
40096
|
key: row.id
|
|
39658
40097
|
});
|
|
40098
|
+
removed++;
|
|
39659
40099
|
} catch (err) {
|
|
39660
40100
|
this.logger.warn("PlateStore.deleteByTracks delete failed", { meta: {
|
|
39661
40101
|
plateId: row.id,
|
|
@@ -39668,6 +40108,11 @@ var PlateStore = class {
|
|
|
39668
40108
|
error: String(err)
|
|
39669
40109
|
} });
|
|
39670
40110
|
}
|
|
40111
|
+
return [{
|
|
40112
|
+
family: "plates",
|
|
40113
|
+
rows: removed,
|
|
40114
|
+
bytes: 0
|
|
40115
|
+
}];
|
|
39671
40116
|
}
|
|
39672
40117
|
/**
|
|
39673
40118
|
* Hold the unassigned plate buffer to `maxPerDevice` newest reads per camera.
|
|
@@ -40789,6 +41234,36 @@ var TrackStore = class {
|
|
|
40789
41234
|
dropActive(trackId) {
|
|
40790
41235
|
this.active.delete(trackId);
|
|
40791
41236
|
}
|
|
41237
|
+
/**
|
|
41238
|
+
* Close a still-ACTIVE track NOW: persist its row, then drop the in-RAM
|
|
41239
|
+
* entry. The early-close counterpart of {@link expireStale}, for a caller
|
|
41240
|
+
* that knows the track is over before its TTL says so.
|
|
41241
|
+
*
|
|
41242
|
+
* **This exists because {@link dropActive} orphans everything the track
|
|
41243
|
+
* owns.** A track's media, object events and CLIP vector are keyed by trackId
|
|
41244
|
+
* and written throughout its life; the ROW is what retention cascades from,
|
|
41245
|
+
* and it is written only here or at expiry. Forgetting a lived-in track
|
|
41246
|
+
* therefore leaves a full media set — key frame, thumbnail, first/last frame,
|
|
41247
|
+
* the snapshot filmstrip — with no owner any sweep can ever reach. Measured on
|
|
41248
|
+
* the live hub 2026-08-14: 74,700 such media rows, 18.9 GB, growing
|
|
41249
|
+
* ~1.15 GB/day, plus 23,296 object events and 4,543 CLIP vectors (D161).
|
|
41250
|
+
*
|
|
41251
|
+
* `dropActive` is still correct for a birth RETRACTED before it lived —
|
|
41252
|
+
* nothing has been written under that id yet. It is never correct for a track
|
|
41253
|
+
* that ran.
|
|
41254
|
+
*
|
|
41255
|
+
* Returns whether an active entry was found. THROWS if the persist fails: the
|
|
41256
|
+
* caller must log it, because a swallowed failure here IS the leak.
|
|
41257
|
+
*/
|
|
41258
|
+
async closeActive(trackId) {
|
|
41259
|
+
const t = this.active.get(trackId);
|
|
41260
|
+
if (t === void 0) return false;
|
|
41261
|
+
t.active = false;
|
|
41262
|
+
const record = cloneTrack(t);
|
|
41263
|
+
await this.persistCompleted(record);
|
|
41264
|
+
this.active.delete(trackId);
|
|
41265
|
+
return true;
|
|
41266
|
+
}
|
|
40792
41267
|
/** Delete the persisted track row (keyed by trackId) and drop the in-RAM
|
|
40793
41268
|
* active entry if present. Used by the whole-track deletion cascade. */
|
|
40794
41269
|
async deletePersisted(trackId) {
|
|
@@ -42139,6 +42614,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
42139
42614
|
} });
|
|
42140
42615
|
}
|
|
42141
42616
|
}
|
|
42617
|
+
if (deps.breathe) await deps.breathe();
|
|
42142
42618
|
if (ceilingHit) {
|
|
42143
42619
|
stoppedBy = "reclaim-ceiling";
|
|
42144
42620
|
offset += rows.length - reclaimedInPage;
|
|
@@ -46914,7 +47390,11 @@ var ObjectEmbeddingStore = class {
|
|
|
46914
47390
|
* breaks recognition (design §2/§4).
|
|
46915
47391
|
*/
|
|
46916
47392
|
async deleteByTracks(trackIds) {
|
|
46917
|
-
if (trackIds.length === 0) return
|
|
47393
|
+
if (trackIds.length === 0) return [{
|
|
47394
|
+
family: "embeddings",
|
|
47395
|
+
rows: 0,
|
|
47396
|
+
bytes: 0
|
|
47397
|
+
}];
|
|
46918
47398
|
try {
|
|
46919
47399
|
await this.vectorStore.deleteByIds.mutate({
|
|
46920
47400
|
index: OBJECT_CLIP_INDEX,
|
|
@@ -46925,7 +47405,17 @@ var ObjectEmbeddingStore = class {
|
|
|
46925
47405
|
count: trackIds.length,
|
|
46926
47406
|
error: String(err)
|
|
46927
47407
|
} });
|
|
47408
|
+
return [{
|
|
47409
|
+
family: "embeddings",
|
|
47410
|
+
rows: 0,
|
|
47411
|
+
bytes: 0
|
|
47412
|
+
}];
|
|
46928
47413
|
}
|
|
47414
|
+
return [{
|
|
47415
|
+
family: "embeddings",
|
|
47416
|
+
rows: trackIds.length,
|
|
47417
|
+
bytes: 0
|
|
47418
|
+
}];
|
|
46929
47419
|
}
|
|
46930
47420
|
};
|
|
46931
47421
|
/** Build the vector-store metadata for one embedding. */
|
|
@@ -47799,7 +48289,8 @@ var NO_PUBLICATION = {
|
|
|
47799
48289
|
var customActions = {
|
|
47800
48290
|
...ncActions,
|
|
47801
48291
|
...embeddingActions,
|
|
47802
|
-
...debugActions
|
|
48292
|
+
...debugActions,
|
|
48293
|
+
...orphanAuditActions
|
|
47803
48294
|
};
|
|
47804
48295
|
/**
|
|
47805
48296
|
* Assist threshold when the caller does not pin one. Packages have their own
|
|
@@ -48777,7 +49268,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
48777
49268
|
customActions: {
|
|
48778
49269
|
...ncActions,
|
|
48779
49270
|
...embeddingActions,
|
|
48780
|
-
...debugActions
|
|
49271
|
+
...debugActions,
|
|
49272
|
+
...orphanAuditActions
|
|
48781
49273
|
},
|
|
48782
49274
|
actionHandlers: {
|
|
48783
49275
|
...ncHandlers,
|
|
@@ -49495,6 +49987,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
49495
49987
|
}
|
|
49496
49988
|
return out;
|
|
49497
49989
|
},
|
|
49990
|
+
readSceneStates: () => (this.sceneProvider?.listAllEnabled() ?? []).map(({ deviceId, monitor }) => ({
|
|
49991
|
+
sceneId: monitor.id,
|
|
49992
|
+
deviceId,
|
|
49993
|
+
label: monitor.label,
|
|
49994
|
+
verdict: monitor.verdict,
|
|
49995
|
+
latched: monitor.latched,
|
|
49996
|
+
emit: monitor.emit
|
|
49997
|
+
})),
|
|
49498
49998
|
readOccupancySnapshots: async (deviceIds) => {
|
|
49499
49999
|
const za = this.zoneAnalytics;
|
|
49500
50000
|
if (za === null) return /* @__PURE__ */ new Map();
|
|
@@ -50293,6 +50793,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50293
50793
|
data: payload
|
|
50294
50794
|
});
|
|
50295
50795
|
},
|
|
50796
|
+
onSceneEdge: (edge) => {
|
|
50797
|
+
this.notificationCenter?.onSceneEdge({
|
|
50798
|
+
sceneId: edge.monitorId,
|
|
50799
|
+
deviceId: edge.deviceId,
|
|
50800
|
+
label: edge.monitorLabel,
|
|
50801
|
+
verdict: edge.verdict,
|
|
50802
|
+
latched: edge.latched,
|
|
50803
|
+
emit: edge.emit
|
|
50804
|
+
});
|
|
50805
|
+
},
|
|
50296
50806
|
currentCondition: async (deviceId) => resolveCondition({
|
|
50297
50807
|
...await this.sceneConditionContext(deviceId),
|
|
50298
50808
|
now: /* @__PURE__ */ new Date()
|
|
@@ -50301,6 +50811,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50301
50811
|
});
|
|
50302
50812
|
provider.hydrate();
|
|
50303
50813
|
this.sceneProvider = provider;
|
|
50814
|
+
this.notificationCenter?.refreshSceneStates();
|
|
50304
50815
|
if (!this.isPostProcessingNode) {
|
|
50305
50816
|
log.info("scene evaluation INERT on this node — CRUD only");
|
|
50306
50817
|
return;
|
|
@@ -52762,7 +53273,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52762
53273
|
};
|
|
52763
53274
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
52764
53275
|
processor.dropTrack(track.trackId);
|
|
52765
|
-
this.trackStore?.
|
|
53276
|
+
this.trackStore?.closeActive(track.trackId).catch((err) => {
|
|
53277
|
+
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
53278
|
+
tags: { deviceId },
|
|
53279
|
+
meta: {
|
|
53280
|
+
deviceId,
|
|
53281
|
+
trackId: track.trackId,
|
|
53282
|
+
entryId: entry.id,
|
|
53283
|
+
error: String(err)
|
|
53284
|
+
}
|
|
53285
|
+
});
|
|
53286
|
+
});
|
|
52766
53287
|
const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, track.trackId);
|
|
52767
53288
|
const dropKeyFrame = () => {
|
|
52768
53289
|
this.residents.dropKeyFrameKey(track.trackId);
|
|
@@ -53352,19 +53873,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53352
53873
|
* stops at a per-scope row ceiling, RESUMES from a persisted cursor next run,
|
|
53353
53874
|
* and refuses to start if the previous run has not finished.
|
|
53354
53875
|
*/
|
|
53355
|
-
async runOrphanAuditPass() {
|
|
53356
|
-
if (this.shuttingDown) return;
|
|
53876
|
+
async runOrphanAuditPass(overrides) {
|
|
53877
|
+
if (this.shuttingDown) return null;
|
|
53357
53878
|
if (this.orphanAuditRunning) {
|
|
53358
53879
|
this.ctx.logger.info("analytics orphan audit skipped — previous run still in flight");
|
|
53359
|
-
return;
|
|
53880
|
+
return null;
|
|
53360
53881
|
}
|
|
53361
53882
|
const trackStore = this.trackStore;
|
|
53362
53883
|
const eventStore = this.eventStore;
|
|
53363
53884
|
const mediaStore = this.mediaStore;
|
|
53364
|
-
if (!trackStore || !eventStore || !mediaStore) return;
|
|
53885
|
+
if (!trackStore || !eventStore || !mediaStore) return null;
|
|
53365
53886
|
this.orphanAuditRunning = true;
|
|
53366
53887
|
try {
|
|
53367
|
-
const limits = await this.orphanAuditLimits();
|
|
53888
|
+
const limits = await this.orphanAuditLimits(overrides);
|
|
53368
53889
|
const deps = {
|
|
53369
53890
|
page: async (collection, offset, limit) => {
|
|
53370
53891
|
if (collection === "pipeline-analytics:media") return mediaStore.pageForAudit(offset, limit);
|
|
@@ -53396,13 +53917,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53396
53917
|
});
|
|
53397
53918
|
},
|
|
53398
53919
|
now: () => Date.now(),
|
|
53399
|
-
logger: this.ctx.logger
|
|
53920
|
+
logger: this.ctx.logger,
|
|
53921
|
+
breathe: () => new Promise((resolve) => setImmediate(resolve))
|
|
53400
53922
|
};
|
|
53401
53923
|
const { report, cursors } = await runOrphanAudit({
|
|
53402
53924
|
scopes: buildOrphanScopes(),
|
|
53403
53925
|
deps,
|
|
53404
53926
|
limits,
|
|
53405
|
-
cursors: this.orphanAuditCursors
|
|
53927
|
+
cursors: overrides?.restart === true ? /* @__PURE__ */ new Map() : this.orphanAuditCursors
|
|
53406
53928
|
});
|
|
53407
53929
|
this.orphanAuditCursors = cursors;
|
|
53408
53930
|
for (const scope of report.scopes) {
|
|
@@ -53437,8 +53959,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53437
53959
|
bytesReclaimed: report.totalBytesReclaimed,
|
|
53438
53960
|
detail: `orphan audit (${report.mode}): ${report.totalOrphans} orphaned rows across ${report.scopes.length} collections, ${report.totalReclaimed} reclaimed in ${report.durationMs}ms`
|
|
53439
53961
|
});
|
|
53962
|
+
return report;
|
|
53440
53963
|
} catch (err) {
|
|
53441
53964
|
this.ctx.logger.warn("analytics orphan audit failed", { meta: { error: String(err) } });
|
|
53965
|
+
return null;
|
|
53442
53966
|
} finally {
|
|
53443
53967
|
this.orphanAuditRunning = false;
|
|
53444
53968
|
}
|
|
@@ -53501,15 +54025,65 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53501
54025
|
* raising them is raising the blast radius of a bug, and that should take a
|
|
53502
54026
|
* release.
|
|
53503
54027
|
*/
|
|
53504
|
-
async orphanAuditLimits() {
|
|
54028
|
+
async orphanAuditLimits(overrides) {
|
|
53505
54029
|
const mode = await this.orphanAuditModeState.get();
|
|
53506
54030
|
const pageSize = await this.orphanAuditPageSizeState.get();
|
|
54031
|
+
const graceMinutes = overrides?.graceMinutes;
|
|
53507
54032
|
return {
|
|
53508
|
-
mode: mode === "reclaim" ? "reclaim" : "report",
|
|
53509
|
-
graceMs: DEFAULT_ORPHAN_LIMITS.graceMs,
|
|
53510
|
-
pageSize,
|
|
53511
|
-
maxRowsPerScope: DEFAULT_ORPHAN_LIMITS.maxRowsPerScope,
|
|
53512
|
-
maxReclaimPerScope: DEFAULT_ORPHAN_LIMITS.maxReclaimPerScope
|
|
54033
|
+
mode: overrides !== void 0 ? overrides.mode ?? "report" : mode === "reclaim" ? "reclaim" : "report",
|
|
54034
|
+
graceMs: graceMinutes === void 0 ? DEFAULT_ORPHAN_LIMITS.graceMs : graceMinutes * 6e4,
|
|
54035
|
+
pageSize: overrides?.pageSize ?? pageSize,
|
|
54036
|
+
maxRowsPerScope: overrides?.maxRowsPerScope ?? DEFAULT_ORPHAN_LIMITS.maxRowsPerScope,
|
|
54037
|
+
maxReclaimPerScope: overrides?.maxReclaimPerScope ?? DEFAULT_ORPHAN_LIMITS.maxReclaimPerScope
|
|
54038
|
+
};
|
|
54039
|
+
}
|
|
54040
|
+
/**
|
|
54041
|
+
* One operator-driven orphan-audit pass — the entry point behind
|
|
54042
|
+
* `retention.orphanAudit`.
|
|
54043
|
+
*
|
|
54044
|
+
* DRY RUN BY DEFAULT: an input with no `mode` reports and deletes nothing,
|
|
54045
|
+
* whatever `orphanAuditMode` is set to. Reclaiming is a word the caller has to
|
|
54046
|
+
* type. Draining the standing 18.9 GB means running this with
|
|
54047
|
+
* `mode: 'reclaim'` repeatedly until `totalOrphans` reaches 0 (D161).
|
|
54048
|
+
*/
|
|
54049
|
+
async runOrphanAuditNow(overrides) {
|
|
54050
|
+
const report = await this.runOrphanAuditPass(overrides);
|
|
54051
|
+
if (report === null) return {
|
|
54052
|
+
ran: false,
|
|
54053
|
+
mode: overrides.mode ?? "report",
|
|
54054
|
+
scopes: [],
|
|
54055
|
+
totalOrphans: 0,
|
|
54056
|
+
totalReclaimed: 0,
|
|
54057
|
+
totalBytesReclaimed: 0,
|
|
54058
|
+
durationMs: 0
|
|
54059
|
+
};
|
|
54060
|
+
this.ctx.logger.info("analytics orphan audit (operator)", { meta: {
|
|
54061
|
+
mode: report.mode,
|
|
54062
|
+
orphans: report.totalOrphans,
|
|
54063
|
+
reclaimed: report.totalReclaimed,
|
|
54064
|
+
bytesReclaimed: report.totalBytesReclaimed,
|
|
54065
|
+
durationMs: report.durationMs,
|
|
54066
|
+
restart: overrides.restart === true
|
|
54067
|
+
} });
|
|
54068
|
+
return {
|
|
54069
|
+
ran: true,
|
|
54070
|
+
mode: report.mode,
|
|
54071
|
+
scopes: report.scopes.map((scope) => ({
|
|
54072
|
+
collection: scope.collection,
|
|
54073
|
+
examined: scope.examined,
|
|
54074
|
+
orphans: scope.orphans,
|
|
54075
|
+
reclaimed: scope.reclaimed,
|
|
54076
|
+
bytesReclaimed: scope.bytesReclaimed,
|
|
54077
|
+
exempt: scope.exempt,
|
|
54078
|
+
withinGrace: scope.withinGrace,
|
|
54079
|
+
unresolvable: scope.unresolvable,
|
|
54080
|
+
stoppedBy: scope.stoppedBy,
|
|
54081
|
+
nextCursor: scope.nextCursor
|
|
54082
|
+
})),
|
|
54083
|
+
totalOrphans: report.totalOrphans,
|
|
54084
|
+
totalReclaimed: report.totalReclaimed,
|
|
54085
|
+
totalBytesReclaimed: report.totalBytesReclaimed,
|
|
54086
|
+
durationMs: report.durationMs
|
|
53513
54087
|
};
|
|
53514
54088
|
}
|
|
53515
54089
|
/**
|
|
@@ -53709,7 +54283,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53709
54283
|
indexed
|
|
53710
54284
|
};
|
|
53711
54285
|
},
|
|
53712
|
-
"embedding.wipe": async () => this.wipeObjectEmbeddings()
|
|
54286
|
+
"embedding.wipe": async () => this.wipeObjectEmbeddings(),
|
|
54287
|
+
"retention.orphanAudit": async (input) => this.runOrphanAuditNow(input)
|
|
53713
54288
|
};
|
|
53714
54289
|
}
|
|
53715
54290
|
/** Progress of the detached rebuild — the only way to observe a long pass. */
|
|
@@ -53968,8 +54543,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53968
54543
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
53969
54544
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
53970
54545
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|
|
53971
|
-
* §4).
|
|
53972
|
-
*
|
|
54546
|
+
* §4).
|
|
54547
|
+
*
|
|
54548
|
+
* **Every family count here is REAL.** It used to return `tracks` and a
|
|
54549
|
+
* literal 0 for every leaf, because `TrackScopedStore.deleteByTracks` returned
|
|
54550
|
+
* `void` — so `bytesReclaimed: 0` went onto all 3,732 ops-log rows this addon
|
|
54551
|
+
* had ever written and 18.9 GB of leaked media read as "nothing to reclaim"
|
|
54552
|
+
* (D161). The bytes ride {@link lastPruneBytes} rather than
|
|
54553
|
+
* `TrackCascadeCounts`, which is a cap output shape.
|
|
53973
54554
|
*/
|
|
53974
54555
|
async pruneTracksBefore(input) {
|
|
53975
54556
|
const empty = {
|
|
@@ -53980,26 +54561,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53980
54561
|
plates: 0,
|
|
53981
54562
|
embeddings: 0
|
|
53982
54563
|
};
|
|
54564
|
+
this.lastPruneBytes = 0;
|
|
53983
54565
|
const cascade = this.buildTrackCascadeRegistry();
|
|
53984
54566
|
if (!cascade) return empty;
|
|
54567
|
+
let tally = EMPTY_CASCADE_TALLY;
|
|
53985
54568
|
const { deleted: totalDeleted, skipped } = await drainTracksBefore({
|
|
53986
54569
|
stopped: () => this.shuttingDown,
|
|
53987
54570
|
listIdsBefore: (offset, limit) => cascade.trackStore.listIdsBefore(input.deviceId, input.cutoffMs, limit, offset),
|
|
53988
|
-
cascade: (ids) =>
|
|
53989
|
-
|
|
53990
|
-
|
|
53991
|
-
|
|
53992
|
-
|
|
53993
|
-
|
|
53994
|
-
|
|
53995
|
-
|
|
53996
|
-
|
|
53997
|
-
|
|
53998
|
-
|
|
53999
|
-
|
|
54000
|
-
|
|
54001
|
-
|
|
54002
|
-
|
|
54571
|
+
cascade: async (ids) => {
|
|
54572
|
+
const result = await runTrackCascadeBatch({
|
|
54573
|
+
registry: cascade.registry,
|
|
54574
|
+
trackStore: cascade.trackStore,
|
|
54575
|
+
deviceId: input.deviceId,
|
|
54576
|
+
onTrackCleanup: (deviceId, trackId) => this.clearLiveTrackState(deviceId, trackId),
|
|
54577
|
+
onFailure: (trackId, err) => {
|
|
54578
|
+
this.ctx.logger.debug("pruneTracksBefore track failed", {
|
|
54579
|
+
tags: { deviceId: input.deviceId },
|
|
54580
|
+
meta: {
|
|
54581
|
+
trackId,
|
|
54582
|
+
error: String(err)
|
|
54583
|
+
}
|
|
54584
|
+
});
|
|
54585
|
+
}
|
|
54586
|
+
}, ids);
|
|
54587
|
+
tally = mergeCascadeTallies(tally, result.tally);
|
|
54588
|
+
return { deleted: result.deleted };
|
|
54589
|
+
}
|
|
54003
54590
|
});
|
|
54004
54591
|
if (skipped > 0) this.ctx.logger.warn("analytics track retention skipped undeletable rows", {
|
|
54005
54592
|
tags: { deviceId: input.deviceId },
|
|
@@ -54010,20 +54597,41 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54010
54597
|
pruned: totalDeleted
|
|
54011
54598
|
}
|
|
54012
54599
|
});
|
|
54600
|
+
this.lastPruneBytes = tally.bytesReclaimed;
|
|
54013
54601
|
if (totalDeleted > 0) this.ctx.logger.info("analytics track retention prune", {
|
|
54014
54602
|
tags: { deviceId: input.deviceId },
|
|
54015
54603
|
meta: {
|
|
54016
54604
|
deviceId: input.deviceId,
|
|
54017
54605
|
cutoffMs: input.cutoffMs,
|
|
54018
|
-
tracks: totalDeleted
|
|
54606
|
+
tracks: totalDeleted,
|
|
54607
|
+
events: tally.events,
|
|
54608
|
+
media: tally.media,
|
|
54609
|
+
faces: tally.faces,
|
|
54610
|
+
plates: tally.plates,
|
|
54611
|
+
embeddings: tally.embeddings,
|
|
54612
|
+
bytesReclaimed: tally.bytesReclaimed
|
|
54019
54613
|
}
|
|
54020
54614
|
});
|
|
54021
54615
|
return {
|
|
54022
54616
|
...empty,
|
|
54023
|
-
tracks: totalDeleted
|
|
54617
|
+
tracks: totalDeleted,
|
|
54618
|
+
events: tally.events,
|
|
54619
|
+
media: tally.media,
|
|
54620
|
+
faces: tally.faces,
|
|
54621
|
+
plates: tally.plates,
|
|
54622
|
+
embeddings: tally.embeddings
|
|
54024
54623
|
};
|
|
54025
54624
|
}
|
|
54026
54625
|
/**
|
|
54626
|
+
* Bytes released by the most recent {@link pruneTracksBefore}.
|
|
54627
|
+
*
|
|
54628
|
+
* A field rather than a return value because `TrackCascadeCounts` is a cap
|
|
54629
|
+
* OUTPUT shape and widening it is a codegen change; the ops-log row already
|
|
54630
|
+
* has a `bytesReclaimed` column and this is what fills it. Reset to 0 at the
|
|
54631
|
+
* top of every prune, so a stale value can never be reported as a fresh one.
|
|
54632
|
+
*/
|
|
54633
|
+
lastPruneBytes = 0;
|
|
54634
|
+
/**
|
|
54027
54635
|
* Operator "clean slate" for a device (design §5.3): prune EVERY persisted
|
|
54028
54636
|
* track via the same cascade as `pruneTracksBefore` with `cutoffMs = now`.
|
|
54029
54637
|
* Enrolled gallery + identity media are exempt.
|
|
@@ -54077,8 +54685,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54077
54685
|
reason: "retention",
|
|
54078
54686
|
deviceId,
|
|
54079
54687
|
itemsAffected: counts.tracks,
|
|
54080
|
-
bytesReclaimed:
|
|
54081
|
-
detail: `track retention cascade: ${counts.tracks} tracks, ${counts.events} events, ${counts.media} media`
|
|
54688
|
+
bytesReclaimed: this.lastPruneBytes,
|
|
54689
|
+
detail: `track retention cascade: ${counts.tracks} tracks, ${counts.events} events, ${counts.media} media, ${counts.faces} faces, ${counts.plates} plates, ${counts.embeddings} vectors`
|
|
54082
54690
|
});
|
|
54083
54691
|
return counts;
|
|
54084
54692
|
},
|