@camstack/addon-post-analysis 1.2.22 → 1.2.24
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-vIJhE1KT.js → dist-MQmqxd7v.js} +326 -107
- package/dist/{dist-CrAkB8NZ.mjs → dist-h7dnARE_.mjs} +326 -107
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-C1I1svLc.js → node-DrRsUzaw.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DM0rHYe_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-arlk__yF.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CXp0RfRy.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-CJueft6_.mjs → hostInit-NxBfUaEi.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +221 -61
- package/dist/pipeline-analytics/index.mjs +220 -60
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-25P5PBxH.mjs +0 -26
|
@@ -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-MQmqxd7v.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_dist.__toESM(node_path);
|
|
@@ -10249,6 +10249,108 @@ var TrackStore = class {
|
|
|
10249
10249
|
}
|
|
10250
10250
|
};
|
|
10251
10251
|
//#endregion
|
|
10252
|
+
//#region src/pipeline-analytics/store/storage-pressure.ts
|
|
10253
|
+
/**
|
|
10254
|
+
* storage-pressure — make a full disk something the system REACTS to, not just
|
|
10255
|
+
* something it logs once per lost file.
|
|
10256
|
+
*
|
|
10257
|
+
* ## What happened
|
|
10258
|
+
*
|
|
10259
|
+
* On 2026-08-02 media writes started failing with
|
|
10260
|
+
* `ENOSPC: no space left on device, mkdir '/recordings/617/events/track/…'`.
|
|
10261
|
+
* For roughly five hours every new track was recorded with positions, a class,
|
|
10262
|
+
* a `bestEventId` and **zero snapshots** — a track that looks complete in the
|
|
10263
|
+
* list and has no picture behind it. The operator found it by noticing the
|
|
10264
|
+
* missing media, not from any alert.
|
|
10265
|
+
*
|
|
10266
|
+
* Two separate defects made that possible:
|
|
10267
|
+
*
|
|
10268
|
+
* 1. **Retention prunes by AGE only.** It has no feedback from the filesystem,
|
|
10269
|
+
* so "the disk is full" is not an input it can respond to. It kept its
|
|
10270
|
+
* configured window while writes failed.
|
|
10271
|
+
* 2. **The failure was logged per KEY.** Every lost frame produced its own
|
|
10272
|
+
* `media put failed` WARN — dozens a minute, each true, none of them saying
|
|
10273
|
+
* "this camera has stopped storing media". A per-item line is a receipt,
|
|
10274
|
+
* not an alarm.
|
|
10275
|
+
*
|
|
10276
|
+
* This module is the missing feedback: it recognises an out-of-space failure,
|
|
10277
|
+
* counts it per device, and hands the caller ONE escalation to act on.
|
|
10278
|
+
*
|
|
10279
|
+
* ## Deliberately not a deleter
|
|
10280
|
+
*
|
|
10281
|
+
* It decides that pressure exists and says so. Freeing space is retention's
|
|
10282
|
+
* job, and giving a write-error handler the power to delete recordings is how
|
|
10283
|
+
* a transient fault becomes data loss. The trigger is separated from the act on
|
|
10284
|
+
* purpose.
|
|
10285
|
+
*/
|
|
10286
|
+
/** Errors that mean "the filesystem refused for want of room". */
|
|
10287
|
+
var OUT_OF_SPACE_CODES = ["ENOSPC", "EDQUOT"];
|
|
10288
|
+
/**
|
|
10289
|
+
* Is this the disk being full, rather than any other write failure?
|
|
10290
|
+
*
|
|
10291
|
+
* Matches the CODE, not a message substring: a permission error or a missing
|
|
10292
|
+
* parent directory must not trigger a retention response, and the message text
|
|
10293
|
+
* is not a contract.
|
|
10294
|
+
*/
|
|
10295
|
+
function isOutOfSpace(err) {
|
|
10296
|
+
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
|
|
10297
|
+
if (OUT_OF_SPACE_CODES.includes(code)) return true;
|
|
10298
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10299
|
+
return OUT_OF_SPACE_CODES.some((c) => msg.includes(`${c}:`));
|
|
10300
|
+
}
|
|
10301
|
+
/** How long between escalations while pressure persists. */
|
|
10302
|
+
var PRESSURE_ESCALATION_INTERVAL_MS = 6e4;
|
|
10303
|
+
/**
|
|
10304
|
+
* Counts out-of-space failures and releases an escalation at most once per
|
|
10305
|
+
* interval — plus immediately on the FIRST one, because the first is the line
|
|
10306
|
+
* that should have existed five hours before anyone noticed.
|
|
10307
|
+
*
|
|
10308
|
+
* Clock injected so the interval is testable without waiting.
|
|
10309
|
+
*/
|
|
10310
|
+
var StoragePressureTracker = class {
|
|
10311
|
+
failures = 0;
|
|
10312
|
+
devices = /* @__PURE__ */ new Set();
|
|
10313
|
+
lastEscalatedAt = null;
|
|
10314
|
+
now;
|
|
10315
|
+
intervalMs;
|
|
10316
|
+
constructor(intervalMs = PRESSURE_ESCALATION_INTERVAL_MS, now = () => Date.now()) {
|
|
10317
|
+
this.intervalMs = intervalMs;
|
|
10318
|
+
this.now = now;
|
|
10319
|
+
}
|
|
10320
|
+
/**
|
|
10321
|
+
* Record a failed write. Returns an escalation when the caller should act
|
|
10322
|
+
* (alert loudly, ask retention to reclaim), or null to stay quiet.
|
|
10323
|
+
*
|
|
10324
|
+
* A non-space failure returns null and is not counted: this tracker exists
|
|
10325
|
+
* for one condition, and diluting it with unrelated errors would make the
|
|
10326
|
+
* escalation mean nothing.
|
|
10327
|
+
*/
|
|
10328
|
+
record(err, deviceId) {
|
|
10329
|
+
if (!isOutOfSpace(err)) return null;
|
|
10330
|
+
this.failures += 1;
|
|
10331
|
+
if (typeof deviceId === "number") this.devices.add(deviceId);
|
|
10332
|
+
const at = this.now();
|
|
10333
|
+
const first = this.lastEscalatedAt === null;
|
|
10334
|
+
if (!first && at - this.lastEscalatedAt < this.intervalMs) return null;
|
|
10335
|
+
const out = {
|
|
10336
|
+
failures: this.failures,
|
|
10337
|
+
deviceIds: [...this.devices],
|
|
10338
|
+
first
|
|
10339
|
+
};
|
|
10340
|
+
this.failures = 0;
|
|
10341
|
+
this.devices.clear();
|
|
10342
|
+
this.lastEscalatedAt = at;
|
|
10343
|
+
return out;
|
|
10344
|
+
}
|
|
10345
|
+
/** Clear the state after a successful write — pressure that has passed must
|
|
10346
|
+
* not make the next isolated failure look like a continuing emergency. */
|
|
10347
|
+
noteSuccess() {
|
|
10348
|
+
if (this.failures === 0 && this.devices.size === 0) return;
|
|
10349
|
+
this.failures = 0;
|
|
10350
|
+
this.devices.clear();
|
|
10351
|
+
}
|
|
10352
|
+
};
|
|
10353
|
+
//#endregion
|
|
10252
10354
|
//#region src/pipeline-analytics/store/media-store.ts
|
|
10253
10355
|
var MEDIA_COLLECTION = "pipeline-analytics:media";
|
|
10254
10356
|
/** Owner-id prefix for a track's buffered FACE crop. Invariant established by
|
|
@@ -10358,6 +10460,14 @@ function buildPath(params) {
|
|
|
10358
10460
|
return isSingleInstanceKind(params.kind) ? `${base}/${params.kind}.jpg` : `${base}/${params.kind}-${params.timestamp}.jpg`;
|
|
10359
10461
|
}
|
|
10360
10462
|
var MediaStore = class {
|
|
10463
|
+
/** Turns repeated out-of-space write failures into ONE escalation with a
|
|
10464
|
+
* count — see `storage-pressure.ts` for what five hours of silent media loss
|
|
10465
|
+
* cost. */
|
|
10466
|
+
pressure = new StoragePressureTracker();
|
|
10467
|
+
/** Optional hook so the owner can ask retention to reclaim space. Left
|
|
10468
|
+
* undefined by default: deciding there IS pressure and DELETING recordings
|
|
10469
|
+
* are separate powers on purpose. */
|
|
10470
|
+
onStoragePressure;
|
|
10361
10471
|
storage;
|
|
10362
10472
|
store;
|
|
10363
10473
|
logger;
|
|
@@ -10406,6 +10516,7 @@ var MediaStore = class {
|
|
|
10406
10516
|
data: record
|
|
10407
10517
|
}
|
|
10408
10518
|
});
|
|
10519
|
+
this.pressure.noteSuccess();
|
|
10409
10520
|
return key;
|
|
10410
10521
|
} catch (err) {
|
|
10411
10522
|
this.logger.warn("media put failed", {
|
|
@@ -10415,6 +10526,19 @@ var MediaStore = class {
|
|
|
10415
10526
|
error: String(err)
|
|
10416
10527
|
}
|
|
10417
10528
|
});
|
|
10529
|
+
const pressure = this.pressure.record(err, params.deviceId);
|
|
10530
|
+
if (pressure) {
|
|
10531
|
+
this.logger.error("media storage is out of space — media is being LOST", {
|
|
10532
|
+
tags: { deviceId: params.deviceId },
|
|
10533
|
+
meta: {
|
|
10534
|
+
failures: pressure.failures,
|
|
10535
|
+
devices: pressure.deviceIds,
|
|
10536
|
+
first: pressure.first,
|
|
10537
|
+
hint: "retention prunes by age and cannot see a full disk — free space or shorten retention"
|
|
10538
|
+
}
|
|
10539
|
+
});
|
|
10540
|
+
this.onStoragePressure?.(pressure);
|
|
10541
|
+
}
|
|
10418
10542
|
throw err;
|
|
10419
10543
|
}
|
|
10420
10544
|
}
|
|
@@ -11024,6 +11148,54 @@ var EventStore = class {
|
|
|
11024
11148
|
});
|
|
11025
11149
|
}
|
|
11026
11150
|
}
|
|
11151
|
+
/**
|
|
11152
|
+
* Collect the ids matching `filter`, then delete every matching row in ONE
|
|
11153
|
+
* statement. The shared body of every retention sweep here.
|
|
11154
|
+
*
|
|
11155
|
+
* The ids are still read because the callers delete each event's media in
|
|
11156
|
+
* lockstep and a bulk delete returns only a count. What is gone is the
|
|
11157
|
+
* per-row delete loop: ~7,200 round trips to prune a day of motion events,
|
|
11158
|
+
* each carrying every column of the row it deleted.
|
|
11159
|
+
*
|
|
11160
|
+
* Three properties, each of which cost a red test to learn:
|
|
11161
|
+
*
|
|
11162
|
+
* - **the filter is used for BOTH halves**, so what is reported and what is
|
|
11163
|
+
* removed cannot drift apart;
|
|
11164
|
+
* - **paging carries an explicit offset over ROWS READ.** Deleting each page
|
|
11165
|
+
* used to advance the cursor implicitly; reading without deleting re-reads
|
|
11166
|
+
* page one forever. And a row with a non-string id yields no id but still
|
|
11167
|
+
* occupies a place, so ids collected is the wrong cursor;
|
|
11168
|
+
* - **only an EMPTY page ends the scan.** The store caps pages on its own
|
|
11169
|
+
* (~500), so "fewer rows than requested" is normal, not the end — treating
|
|
11170
|
+
* it as the end silently deletes just the first page while reporting a
|
|
11171
|
+
* complete sweep;
|
|
11172
|
+
* - **a delete that removed nothing reports NO ids.** Callers remove media
|
|
11173
|
+
* against this list; claiming a deletion that did not happen would orphan
|
|
11174
|
+
* media in the opposite direction.
|
|
11175
|
+
*/
|
|
11176
|
+
async drainMatching(collection, filter) {
|
|
11177
|
+
const ids = [];
|
|
11178
|
+
let offset = 0;
|
|
11179
|
+
for (;;) {
|
|
11180
|
+
const rows = await this.store.query.query({
|
|
11181
|
+
collection,
|
|
11182
|
+
filter: {
|
|
11183
|
+
...filter,
|
|
11184
|
+
limit: 500,
|
|
11185
|
+
offset
|
|
11186
|
+
}
|
|
11187
|
+
});
|
|
11188
|
+
if (rows.length === 0) break;
|
|
11189
|
+
offset += rows.length;
|
|
11190
|
+
for (const row of rows) if (typeof row.id === "string") ids.push(row.id);
|
|
11191
|
+
}
|
|
11192
|
+
if (ids.length === 0) return ids;
|
|
11193
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
11194
|
+
collection,
|
|
11195
|
+
filter
|
|
11196
|
+
});
|
|
11197
|
+
return deleted === 0 ? [] : ids;
|
|
11198
|
+
}
|
|
11027
11199
|
buildFilter(q) {
|
|
11028
11200
|
const filter = {
|
|
11029
11201
|
where: { deviceId: q.deviceId },
|
|
@@ -11344,35 +11516,10 @@ var EventStore = class {
|
|
|
11344
11516
|
* page at a time (PRUNE_PAGE_SIZE) until no stale rows remain.
|
|
11345
11517
|
*/
|
|
11346
11518
|
async evictBefore(params) {
|
|
11347
|
-
const batch = async (collection, cutoffMs) => {
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
collection,
|
|
11352
|
-
filter: {
|
|
11353
|
-
...params.deviceId !== void 0 ? { where: { deviceId: params.deviceId } } : {},
|
|
11354
|
-
whereBetween: { timestamp: [0, cutoffMs] },
|
|
11355
|
-
limit: 500
|
|
11356
|
-
}
|
|
11357
|
-
});
|
|
11358
|
-
if (rows.length === 0) break;
|
|
11359
|
-
let deletedInPage = 0;
|
|
11360
|
-
for (const row of rows) {
|
|
11361
|
-
const id = row.id;
|
|
11362
|
-
if (typeof id !== "string") continue;
|
|
11363
|
-
try {
|
|
11364
|
-
await this.store.delete.mutate({
|
|
11365
|
-
collection,
|
|
11366
|
-
key: id
|
|
11367
|
-
});
|
|
11368
|
-
ids.push(id);
|
|
11369
|
-
deletedInPage++;
|
|
11370
|
-
} catch {}
|
|
11371
|
-
}
|
|
11372
|
-
if (deletedInPage === 0) break;
|
|
11373
|
-
}
|
|
11374
|
-
return ids;
|
|
11375
|
-
};
|
|
11519
|
+
const batch = async (collection, cutoffMs) => this.drainMatching(collection, {
|
|
11520
|
+
...params.deviceId !== void 0 ? { where: { deviceId: params.deviceId } } : {},
|
|
11521
|
+
whereBetween: { timestamp: [0, cutoffMs] }
|
|
11522
|
+
});
|
|
11376
11523
|
return {
|
|
11377
11524
|
motion: await batch(MOTION_EVENTS_COLLECTION, params.motionCutoffMs),
|
|
11378
11525
|
object: await batch(OBJECT_EVENTS_COLLECTION, params.objectCutoffMs),
|
|
@@ -11395,35 +11542,16 @@ var EventStore = class {
|
|
|
11395
11542
|
* so ALL matching rows are deleted regardless of total count.
|
|
11396
11543
|
*/
|
|
11397
11544
|
async pruneBefore(input) {
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
limit: 500
|
|
11407
|
-
}
|
|
11408
|
-
});
|
|
11409
|
-
if (rows.length === 0) break;
|
|
11410
|
-
let deletedInPage = 0;
|
|
11411
|
-
for (const row of rows) {
|
|
11412
|
-
const id = row.id;
|
|
11413
|
-
if (typeof id !== "string") continue;
|
|
11414
|
-
try {
|
|
11415
|
-
await this.store.delete.mutate({
|
|
11416
|
-
collection,
|
|
11417
|
-
key: id
|
|
11418
|
-
});
|
|
11419
|
-
ids.push(id);
|
|
11420
|
-
deletedInPage++;
|
|
11421
|
-
} catch {}
|
|
11422
|
-
}
|
|
11423
|
-
if (deletedInPage === 0) break;
|
|
11424
|
-
}
|
|
11425
|
-
return ids;
|
|
11545
|
+
/**
|
|
11546
|
+
* The filter is written ONCE and used for both halves below, so the rows
|
|
11547
|
+
* reported and the rows deleted cannot drift apart. Inclusive `[0, cutoff-1]`
|
|
11548
|
+
* → selects timestamps strictly < `cutoffMs`.
|
|
11549
|
+
*/
|
|
11550
|
+
const staleForDevice = {
|
|
11551
|
+
where: { deviceId: input.deviceId },
|
|
11552
|
+
whereBetween: { timestamp: [0, input.cutoffMs - 1] }
|
|
11426
11553
|
};
|
|
11554
|
+
const allForDevice = async (collection) => this.drainMatching(collection, staleForDevice);
|
|
11427
11555
|
const [motionIds, objectIds, audioIds] = await Promise.all([
|
|
11428
11556
|
allForDevice(MOTION_EVENTS_COLLECTION),
|
|
11429
11557
|
allForDevice(OBJECT_EVENTS_COLLECTION),
|
|
@@ -17546,6 +17674,22 @@ var OBJECT_EMBEDDING_COLUMNS = [
|
|
|
17546
17674
|
type: "TEXT"
|
|
17547
17675
|
}
|
|
17548
17676
|
];
|
|
17677
|
+
/**
|
|
17678
|
+
* Ceiling on the CANDIDATE pool a semantic search may load.
|
|
17679
|
+
*
|
|
17680
|
+
* `query()` was called with no limit at all, so a default search loaded the
|
|
17681
|
+
* entire collection — 86.5 MB across 7,375 rows on a live hub, every time,
|
|
17682
|
+
* before ranking anything.
|
|
17683
|
+
*
|
|
17684
|
+
* It cannot simply be the caller's result limit: search RANKS the pool and then
|
|
17685
|
+
* takes the top N, so limiting to N up front would return N arbitrary rows
|
|
17686
|
+
* instead of the N best. This bounds the pool well above any result count, and
|
|
17687
|
+
* `query()` orders by `timestamp` DESC whenever a limit applies so "the pool"
|
|
17688
|
+
* means the most recent rows rather than whatever the page order happened to
|
|
17689
|
+
* be. The caller logs when the cap is reached — a silently truncated search
|
|
17690
|
+
* reads as "nothing older matched".
|
|
17691
|
+
*/
|
|
17692
|
+
var EMBEDDING_SEARCH_CANDIDATE_CAP = 5e3;
|
|
17549
17693
|
var ObjectEmbeddingStore = class {
|
|
17550
17694
|
store;
|
|
17551
17695
|
logger;
|
|
@@ -17620,7 +17764,13 @@ var ObjectEmbeddingStore = class {
|
|
|
17620
17764
|
if (input.modelId !== void 0) where["modelId"] = input.modelId;
|
|
17621
17765
|
const filter = { where };
|
|
17622
17766
|
if (input.since !== void 0 || input.until !== void 0) filter["whereBetween"] = { timestamp: [input.since ?? 0, input.until ?? Date.now()] };
|
|
17623
|
-
if (input.limit !== void 0)
|
|
17767
|
+
if (input.limit !== void 0) {
|
|
17768
|
+
filter["limit"] = input.limit;
|
|
17769
|
+
filter["orderBy"] = {
|
|
17770
|
+
field: "timestamp",
|
|
17771
|
+
direction: "desc"
|
|
17772
|
+
};
|
|
17773
|
+
}
|
|
17624
17774
|
return (await this.store.query.query({
|
|
17625
17775
|
collection: OBJECT_EMBEDDINGS_COLLECTION,
|
|
17626
17776
|
filter
|
|
@@ -20735,7 +20885,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20735
20885
|
let storage = this.ctx.kernel.storage;
|
|
20736
20886
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
20737
20887
|
if (mediaRoot) {
|
|
20738
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
20888
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DrRsUzaw.js"));
|
|
20739
20889
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
20740
20890
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
20741
20891
|
}
|
|
@@ -23670,7 +23820,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
23670
23820
|
deviceId: input.deviceId,
|
|
23671
23821
|
since: input.since,
|
|
23672
23822
|
until: input.until,
|
|
23673
|
-
className: input.classFilter
|
|
23823
|
+
className: input.classFilter,
|
|
23824
|
+
limit: EMBEDDING_SEARCH_CANDIDATE_CAP
|
|
23825
|
+
});
|
|
23826
|
+
if (embeddingRows.length >= 5e3) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
|
|
23827
|
+
meta: {
|
|
23828
|
+
cap: EMBEDDING_SEARCH_CANDIDATE_CAP,
|
|
23829
|
+
since: input.since,
|
|
23830
|
+
until: input.until,
|
|
23831
|
+
className: input.classFilter
|
|
23832
|
+
},
|
|
23833
|
+
tags: input.deviceId !== void 0 ? { deviceId: input.deviceId } : void 0
|
|
23674
23834
|
});
|
|
23675
23835
|
if (embeddingRows.length === 0) return [];
|
|
23676
23836
|
const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
|