@camstack/addon-post-analysis 1.2.92 → 1.2.94

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.
@@ -1,13 +1,13 @@
1
- import { Z as embeddingEncoderCapability, et as hfModelUrl, gt as BaseAddon } from "../dist-VnaTfwAL.mjs";
1
+ import { O as PoolMemoryWatchdog, Q as embeddingEncoderCapability, dt as resolvePoolMemoryPolicy, ot as parseProcStatus, tt as hfModelUrl, yt as BaseAddon } from "../dist-C-_TL-I1.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import "node:path";
7
7
  import "node:crypto";
8
+ import { execFile, spawn } from "node:child_process";
8
9
  import { promisify } from "node:util";
9
10
  import { brotliCompress, gzip } from "node:zlib";
10
- import { spawn } from "node:child_process";
11
11
  //#region \0rolldown/runtime.js
12
12
  var __defProp = Object.defineProperty;
13
13
  var __exportAll = (all, no_symbols) => {
@@ -21,6 +21,53 @@ var __exportAll = (all, no_symbols) => {
21
21
  };
22
22
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
23
23
  //#endregion
24
+ //#region src/embedding-encoder/shared/process-memory.ts
25
+ /**
26
+ * Read a child process's memory footprint for the pool memory watchdog.
27
+ *
28
+ * Deliberate twin of `packages/addon-pipeline/src/shared/process-memory.ts`:
29
+ * addons must not import each other, and `@camstack/system` stays EXTERNAL in
30
+ * addon bundles (a symbol added there would be missing at runtime until the
31
+ * next framework train). The pure parse (`parseProcStatus`) and the whole
32
+ * watchdog policy live once in `@camstack/types` (inlined at build time);
33
+ * only this thin `fs`/`ps` read is duplicated.
34
+ *
35
+ * Linux (the fleet): `/proc/<pid>/status` — RSS, VmSize, VmHWM (peak RSS ≈
36
+ * getrusage maxrss), VmSwap, Threads in one read. macOS (dev): `ps -o rss=`
37
+ * gives RSS alone. Returns null when the pid is gone — the CALLER logs the
38
+ * dropped sample.
39
+ */
40
+ var IS_LINUX = process.platform === "linux";
41
+ async function readProcessMemory(pid) {
42
+ if (IS_LINUX) try {
43
+ return parseProcStatus(await fs.promises.readFile(`/proc/${pid}/status`, "utf8"));
44
+ } catch {
45
+ return null;
46
+ }
47
+ return readViaPs(pid);
48
+ }
49
+ function readViaPs(pid) {
50
+ return new Promise((resolve) => {
51
+ execFile("ps", [
52
+ "-o",
53
+ "rss=",
54
+ "-p",
55
+ String(pid)
56
+ ], { timeout: 2e3 }, (err, stdout) => {
57
+ if (err) return resolve(null);
58
+ const kb = Number(stdout.trim());
59
+ if (!Number.isFinite(kb) || kb <= 0) return resolve(null);
60
+ resolve({
61
+ rssBytes: kb * 1024,
62
+ vmBytes: 0,
63
+ hwmBytes: 0,
64
+ swapBytes: 0,
65
+ threads: 0
66
+ });
67
+ });
68
+ });
69
+ }
70
+ //#endregion
24
71
  //#region ../system/dist/file-data-plane-CuE_hBli.mjs
25
72
  function isNonEmptyFile(filePath) {
26
73
  return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
@@ -507,8 +554,20 @@ var PythonRawTensorEngine = class {
507
554
  if (ready.length !== 1 || ready[0] !== 1) throw new Error("PythonRawTensorEngine: unexpected ready frame");
508
555
  this.log.info("ONNX raw-tensor engine ready (embedded Python)", { meta: { modelPath: this.modelPath } });
509
556
  }
557
+ /** Native pid of the Python subprocess — sampled by the pool memory
558
+ * watchdog (`/proc/<pid>/status`). */
559
+ getPid() {
560
+ return this.process?.pid ?? null;
561
+ }
562
+ /** Inference requests served since spawn — the denominator that separates
563
+ * "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
564
+ getRequestCount() {
565
+ return this.requestCount;
566
+ }
567
+ requestCount = 0;
510
568
  async run(input, inputShape) {
511
569
  if (!this.process?.stdin) throw new Error("PythonRawTensorEngine: not initialized — call initialize() first");
570
+ this.requestCount++;
512
571
  const ndims = inputShape.length;
513
572
  const meta = Buffer.allocUnsafe(1 + ndims * 4);
514
573
  meta.writeUInt8(ndims, 0);
@@ -633,9 +692,21 @@ var PythonTextEncoderEngine = class {
633
692
  tokenizerPath: this.tokenizerPath
634
693
  } });
635
694
  }
695
+ /** Native pid of the Python subprocess — sampled by the pool memory
696
+ * watchdog (`/proc/<pid>/status`). */
697
+ getPid() {
698
+ return this.process?.pid ?? null;
699
+ }
700
+ /** Encode requests served since spawn — the denominator that separates
701
+ * "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
702
+ getRequestCount() {
703
+ return this.requestCount;
704
+ }
705
+ requestCount = 0;
636
706
  /** Tokenize + encode `text` into the model embedding (float32). */
637
707
  async encode(text) {
638
708
  if (!this.process?.stdin) throw new Error("PythonTextEncoderEngine: not initialized — call initialize() first");
709
+ this.requestCount++;
639
710
  const payload = Buffer.from(text, "utf-8");
640
711
  const lenBuf = Buffer.allocUnsafe(4);
641
712
  lenBuf.writeUInt32LE(payload.length, 0);
@@ -761,6 +832,22 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
761
832
  imageRawEngine = null;
762
833
  textEngine = null;
763
834
  models = null;
835
+ /**
836
+ * RSS bound + periodic memory telemetry for the two Python engines
837
+ * (`raw_tensor_inference.py` — one of the four leaking pools in the
838
+ * 2026-08-18 OOM audit — and `text_encoder_inference.py`). Shared
839
+ * watchdog from `@camstack/types`; a restart here is dispose + null, and
840
+ * the next encode lazily re-resolves the engine (the exact recovery an
841
+ * operator model change already uses). Started in `onInitialize`,
842
+ * stopped in `onShutdown`.
843
+ */
844
+ memoryGuard = null;
845
+ /** Single-flight guards for the lazy engine builds — a watchdog restart
846
+ * followed by two concurrent encodes must not spawn the engine twice
847
+ * (the same bug detection-pipeline fixed three times; see its
848
+ * `engineFactoryInflight`). */
849
+ imageEngineInflight = null;
850
+ textEngineInflight = null;
764
851
  constructor() {
765
852
  super({
766
853
  modelId: DEFAULT_CLIP_MODEL,
@@ -771,11 +858,75 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
771
858
  async onInitialize() {
772
859
  const modelsDir = await this.resolveModelsDir();
773
860
  this.models = new ModelDownloadService(modelsDir, [...CLIP_IMAGE_MODELS, ...CLIP_TEXT_MODELS]);
861
+ this.memoryGuard = new PoolMemoryWatchdog({
862
+ policy: resolvePoolMemoryPolicy(process.env),
863
+ log: this.ctx.logger.child("pool-memory"),
864
+ sample: () => this.sampleEngineMemory(),
865
+ restart: (key) => this.restartEngineForMemory(key)
866
+ });
867
+ this.memoryGuard.start();
774
868
  return [{
775
869
  capability: embeddingEncoderCapability,
776
870
  provider: this
777
871
  }];
778
872
  }
873
+ /** Watchdog sample: one telemetry row per live Python engine. A pid the
874
+ * read cannot see is logged, never silently skipped. */
875
+ async sampleEngineMemory() {
876
+ const engines = [{
877
+ key: "clip-image",
878
+ pid: this.imageRawEngine?.getPid() ?? null,
879
+ requests: this.imageRawEngine?.getRequestCount() ?? 0
880
+ }, {
881
+ key: "clip-text",
882
+ pid: this.textEngine?.getPid() ?? null,
883
+ requests: this.textEngine?.getRequestCount() ?? 0
884
+ }];
885
+ const out = [];
886
+ for (const engine of engines) {
887
+ if (engine.pid === null) continue;
888
+ const mem = await readProcessMemory(engine.pid);
889
+ if (!mem) {
890
+ this.ctx.logger.warn("embedding engine memory sample dropped", { meta: {
891
+ poolKey: engine.key,
892
+ pid: engine.pid
893
+ } });
894
+ continue;
895
+ }
896
+ const mb = (bytes) => Math.round(bytes / (1024 * 1024));
897
+ out.push({
898
+ key: engine.key,
899
+ pids: [engine.pid],
900
+ rssBytes: mem.rssBytes,
901
+ meta: {
902
+ modelId: this.config.modelId,
903
+ vmMb: mb(mem.vmBytes),
904
+ peakRssMb: mb(mem.hwmBytes),
905
+ swapMb: mb(mem.swapBytes),
906
+ threads: mem.threads,
907
+ requestsServed: engine.requests
908
+ }
909
+ });
910
+ }
911
+ return out;
912
+ }
913
+ /** Watchdog restart: dispose + null; the next encode re-resolves lazily
914
+ * through the single-flight ensure. An in-flight encode fails once — its
915
+ * callers are the retry-until-landed bulk passes (D58) and RPCs that
916
+ * surface errors, never silent loss. */
917
+ async restartEngineForMemory(key) {
918
+ if (key === "clip-image") {
919
+ const engine = this.imageRawEngine;
920
+ if (!engine) return;
921
+ this.imageRawEngine = null;
922
+ await engine.dispose();
923
+ return;
924
+ }
925
+ const engine = this.textEngine;
926
+ if (!engine) return;
927
+ this.textEngine = null;
928
+ await engine.dispose();
929
+ }
779
930
  async encode(input) {
780
931
  const { crop, width, height } = input;
781
932
  await this.ensureImageEngine();
@@ -819,17 +970,31 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
819
970
  }
820
971
  async ensureImageEngine() {
821
972
  if (this.imageRawEngine) return;
973
+ if (this.imageEngineInflight) return this.imageEngineInflight;
822
974
  const meta = getModelMeta(this.config.modelId);
823
975
  const imageEntry = CLIP_IMAGE_MODELS.find((m) => m.id === meta.imageModelId);
824
976
  if (!imageEntry) throw new Error(`EmbeddingEncoderAddon: unknown image model "${meta.imageModelId}"`);
825
- await this.resolveForEntry(imageEntry, "image");
977
+ const inflight = this.resolveForEntry(imageEntry, "image");
978
+ this.imageEngineInflight = inflight;
979
+ try {
980
+ await inflight;
981
+ } finally {
982
+ this.imageEngineInflight = null;
983
+ }
826
984
  }
827
985
  async ensureTextEngine() {
828
986
  if (this.textEngine) return;
987
+ if (this.textEngineInflight) return this.textEngineInflight;
829
988
  const meta = getModelMeta(this.config.modelId);
830
989
  const textEntry = CLIP_TEXT_MODELS.find((m) => m.id === meta.textModelId);
831
990
  if (!textEntry) throw new Error(`EmbeddingEncoderAddon: unknown text model "${meta.textModelId}"`);
832
- await this.resolveForEntry(textEntry, "text");
991
+ const inflight = this.resolveForEntry(textEntry, "text");
992
+ this.textEngineInflight = inflight;
993
+ try {
994
+ await inflight;
995
+ } finally {
996
+ this.textEngineInflight = null;
997
+ }
833
998
  }
834
999
  async resolveForEntry(entry, target) {
835
1000
  const engineLogger = this.ctx.logger.withTags({
@@ -855,6 +1020,8 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
855
1020
  this.textEngine = textEngine;
856
1021
  }
857
1022
  async onShutdown() {
1023
+ this.memoryGuard?.stop();
1024
+ this.memoryGuard = null;
858
1025
  await this.imageRawEngine?.dispose();
859
1026
  await this.textEngine?.dispose();
860
1027
  }
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.22",
6
+ version: "1.2.24",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.86",
21
+ version: "1.2.88",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.58",
36
+ version: "1.2.60",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.86",
39
+ version: "1.2.88",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.22",
48
+ version: "1.2.24",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.58",
84
+ version: "1.2.60",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-BfrtqGyh.js");
5
+ const require_dist = require("../dist-BdojoP7i.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");
@@ -8289,6 +8289,8 @@ var en_default = {
8289
8289
  "system.node-offline.body.worker": "Worker {{nodeId}} is offline",
8290
8290
  "system.node-inference-unavailable.title": "Node has no usable inference device",
8291
8291
  "system.node-inference-unavailable.body": "Node {{nodeId}} can no longer run inference — every enabled device is unavailable ({{devices}}). It will not receive cameras until one returns.",
8292
+ "system.addon-crash-loop.title": "Addon stopped after repeated crashes",
8293
+ "system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
8292
8294
  "system.detection-blind.title": "Camera detecting nothing",
8293
8295
  "system.detection-blind.body": {
8294
8296
  "one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
@@ -8501,6 +8503,8 @@ var it_default = {
8501
8503
  "system.node-offline.body.worker": "Il worker {{nodeId}} è offline",
8502
8504
  "system.node-inference-unavailable.title": "Nodo senza dispositivi di inferenza utilizzabili",
8503
8505
  "system.node-inference-unavailable.body": "Il nodo {{nodeId}} non può più eseguire inferenza — tutti i dispositivi abilitati non sono disponibili ({{devices}}). Non riceverà camere finché uno non torna disponibile.",
8506
+ "system.addon-crash-loop.title": "Addon fermato dopo ripetuti crash",
8507
+ "system.addon-crash-loop.body": "L'addon {{addons}} sul nodo {{nodeId}} è andato in crash {{count}} volte di seguito e NON verrà riavviato. Tutto ciò che fornisce resta offline finché non viene riparato.",
8504
8508
  "system.detection-blind.title": "Camera che non rileva nulla",
8505
8509
  "system.detection-blind.body": {
8506
8510
  "one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
@@ -10273,6 +10277,27 @@ function incomingFromSystemEvent(event, lookup) {
10273
10277
  nodeId
10274
10278
  });
10275
10279
  }
10280
+ case "addon.runner-failed": {
10281
+ const nodeId = stringField(data, "nodeId");
10282
+ const runnerId = stringField(data, "runnerId");
10283
+ const crashes = numberField(data, "crashesInWindow");
10284
+ if (nodeId === void 0 || runnerId === void 0 || crashes === void 0) return null;
10285
+ const ids = data["addonIds"];
10286
+ const addons = Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
10287
+ return systemSubject({
10288
+ facts,
10289
+ kind: "addon-crash-loop",
10290
+ subject: `addon-crash:${nodeId}:${runnerId}`,
10291
+ titleKey: "system.addon-crash-loop.title",
10292
+ bodyKey: "system.addon-crash-loop.body",
10293
+ textParams: {
10294
+ nodeId,
10295
+ addons: addons.length > 0 ? addons.join(", ") : runnerId,
10296
+ count: String(crashes)
10297
+ },
10298
+ nodeId
10299
+ });
10300
+ }
10276
10301
  case "pipeline.detection-blind": {
10277
10302
  const deviceId = numberField(data, "deviceId");
10278
10303
  const blindForMs = numberField(data, "blindForMs");
@@ -21940,6 +21965,9 @@ function systemEventKeys(kind) {
21940
21965
  }
21941
21966
  /** The arm mode a synthetic alarm event stands in for — see {@link systemEventKeys}. */
21942
21967
  var TEST_ALARM_MODE = "away";
21968
+ /** The runner a synthetic `addon-crash-loop` stands in for. A test fire has no
21969
+ * real crashed runner, and a blank one would render "The addon on node hub". */
21970
+ var TEST_CRASHED_RUNNER = "recorder";
21943
21971
  /** The dedup subject the live intake stamps for this kind — same families. */
21944
21972
  function systemEventSubject(kind, scope) {
21945
21973
  if (kind === "device-disabled" || kind === "device-enabled") return `device-switch:${scope.deviceId ?? 0}`;
@@ -21948,6 +21976,7 @@ function systemEventSubject(kind, scope) {
21948
21976
  if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
21949
21977
  if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
21950
21978
  if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
21979
+ if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
21951
21980
  if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
21952
21981
  if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
21953
21982
  if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
@@ -22064,6 +22093,7 @@ function planRuleTest(input) {
22064
22093
  seconds: "30",
22065
22094
  devices: "cuda:0",
22066
22095
  minutes: "15",
22096
+ addons: TEST_CRASHED_RUNNER,
22067
22097
  list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
22068
22098
  nodes: "hub",
22069
22099
  fromVersion: "0.0.0",
@@ -30017,6 +30047,22 @@ function stillInstantMs(ranges) {
30017
30047
  if (best === void 0) return void 0;
30018
30048
  return Math.floor((best.startMs + best.endMs) / 2);
30019
30049
  }
30050
+ /**
30051
+ * The distinct object classes attached to a visit, most frequent first,
30052
+ * capped at {@link MAX_CLIP_LABELS}. Empty when nothing classified overlapped
30053
+ * — the caller then omits the field so a motion/audio-only visit keeps its
30054
+ * kind label instead of carrying an empty array every consumer length-checks.
30055
+ */
30056
+ function dominantLabels(attached) {
30057
+ const counts = /* @__PURE__ */ new Map();
30058
+ for (const a of attached) {
30059
+ if (a.kind !== "object") continue;
30060
+ const name = a.event.className;
30061
+ if (typeof name !== "string" || name.length === 0) continue;
30062
+ counts.set(name, (counts.get(name) ?? 0) + 1);
30063
+ }
30064
+ return [...counts.entries()].toSorted((x, y) => y[1] - x[1]).slice(0, 3).map(([name]) => name);
30065
+ }
30020
30066
  function visitsToClips(visits, events, pad, vouched) {
30021
30067
  const tagged = [
30022
30068
  ...events.object.map((event) => ({
@@ -30037,6 +30083,7 @@ function visitsToClips(visits, events, pad, vouched) {
30037
30083
  const kind = dominantKind(attached);
30038
30084
  const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
30039
30085
  const stillAtMs = stillInstantMs(visit.ranges);
30086
+ const labels = dominantLabels(attached);
30040
30087
  return {
30041
30088
  id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
30042
30089
  source: "analytics",
@@ -30049,7 +30096,8 @@ function visitsToClips(visits, events, pad, vouched) {
30049
30096
  ...stillAtMs !== void 0 ? { stillAtMs } : {},
30050
30097
  eventIds: attached.slice(0, 24).map((a) => a.event.id),
30051
30098
  eventCount: attached.length,
30052
- holes: [...visit.holes]
30099
+ holes: [...visit.holes],
30100
+ ...labels.length > 0 ? { labels } : {}
30053
30101
  };
30054
30102
  }).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
30055
30103
  }
@@ -46109,6 +46157,7 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
46109
46157
  require_dist.EventCategory.WorkerOffline,
46110
46158
  require_dist.EventCategory.PipelineNodeInferenceUnavailable,
46111
46159
  require_dist.EventCategory.PipelineDetectionBlind,
46160
+ require_dist.EventCategory.AddonRunnerFailed,
46112
46161
  require_dist.EventCategory.UpdateAvailable,
46113
46162
  require_dist.EventCategory.AddonUpdated,
46114
46163
  require_dist.EventCategory.SystemRestartCompleted,
@@ -1,4 +1,4 @@
1
- import { $ as faceGalleryCapability, At as partialRecord, B as VISIT_MERGE_GAP_MS, C as NcSnoozeSchema, Ct as _enum, D as OpsLogEntrySchema, Dt as literal, E as NcTaxonomySchema, Et as discriminatedUnion, F as TIMELAPSE_DENSE_FLOOR_SEC, G as audioModeOf, H as alarmPanelCapability, I as TimelapseRuleInputSchema, J as customAction, K as buildEventKindDescriptor, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as string, N as SCENE_DIVERGED, Nt as unknown, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as number, P as SceneMonitorSchema, Pt as EventCategory, Q as encodeVectorBase64, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as sleep, T as NcSystemEventKindSchema, Tt as boolean, U as assertTimelapseCadences, V as addonWidgetsSourceCapability, W as audioMetricsCapability, X as deriveRecordingMode, Y as defineCustomActions, _ as NcRuleInputSchema, _t as DeviceType, at as pipelineAnalyticsCapability, b as NcRuleTargetSchema, bt as isDeviceScopedCap, c as EVENT_PAD_MS, ct as readTimelapseGeneratedAt, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as systemEventFilterApplies, f as NC_CONDITION_CATALOG, ft as vectorDimFromBase64, g as NcConditionDescriptorSchema, gt as BaseAddon, h as NC_TAXONOMY, ht as errMsg, i as DETECTION_MACRO_CLASSES, it as notificationRulesCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as record, k as RetrainStatusSchema, kt as object, l as LabelAttributionSchema, lt as sceneMonitorCapability, mt as zoneAnalyticsCapability, n as DEFAULT_EVENT_COLOR, nt as isScheduleActive, o as DeclaredDevices, ot as plateGalleryCapability, p as NC_DEFAULT_SNOOZE_MINUTES, pt as videoclipsCapability, q as cosineSimilarity$1, rt as kebabToCamel, s as EVENT_KIND_BY_CAP, st as readDeviceStateFrom, t as BaseDevice, tt as isDetectionMacroClass, u as MACRO_LABELS, ut as subKindsOf, v as NcRulePatchSchema, vt as createEvent, w as NcSnoozeSuppressedSchema, wt as array, x as NcScheduleSchema, xt as nodePin, y as NcRuleSchema, yt as hydrateSchema, z as TrackSourceSchema } from "../dist-VnaTfwAL.mjs";
1
+ import { $ as encodeVectorBase64, A as RetrainStatusSchema, At as literal, B as TrackSourceSchema, C as NcSnoozeSchema, Ct as isDeviceScopedCap, D as OpsLogEntrySchema, Dt as array, E as NcTaxonomySchema, Et as _enum, F as SceneMonitorSchema, Ft as string, G as audioMetricsCapability, H as addonWidgetsSourceCapability, I as TIMELAPSE_DENSE_FLOOR_SEC, It as unknown, J as cosineSimilarity$1, K as audioModeOf, L as TimelapseRuleInputSchema, Lt as EventCategory, M as SCENE_DEFAULT_ANCHOR_THRESHOLD, Mt as object, N as SCENE_DEFAULT_UNCOVERED_POLICY, Nt as partialRecord, Ot as boolean, P as SCENE_DIVERGED, Pt as record, R as TimelapseRulePatchSchema, S as NcSnoozeInputSchema, St as hydrateSchema, T as NcSystemEventKindSchema, Tt as sleep, U as alarmPanelCapability, V as VISIT_MERGE_GAP_MS, W as assertTimelapseCadences, X as defineCustomActions, Y as customAction, Z as deriveRecordingMode, _ as NcRuleInputSchema, _t as zoneAnalyticsCapability, at as notificationRulesCapability, b as NcRuleTargetSchema, bt as DeviceType, c as EVENT_PAD_MS, ct as plateGalleryCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, et as faceGalleryCapability, f as NC_CONDITION_CATALOG, ft as sceneMonitorCapability, g as NcConditionDescriptorSchema, gt as videoclipsCapability, h as NC_TAXONOMY, ht as vectorDimFromBase64, i as DETECTION_MACRO_CLASSES, it as kebabToCamel, jt as number, k as RECORDING_EXPORT_MAX_READ_BYTES, kt as discriminatedUnion, l as LabelAttributionSchema, lt as readDeviceStateFrom, mt as systemEventFilterApplies, n as DEFAULT_EVENT_COLOR, nt as isDetectionMacroClass, o as DeclaredDevices, p as NC_DEFAULT_SNOOZE_MINUTES, pt as subKindsOf, q as buildEventKindDescriptor, rt as isScheduleActive, s as EVENT_KIND_BY_CAP, st as pipelineAnalyticsCapability, t as BaseDevice, u as MACRO_LABELS, ut as readTimelapseGeneratedAt, v as NcRulePatchSchema, vt as errMsg, w as NcSnoozeSuppressedSchema, wt as nodePin, x as NcScheduleSchema, xt as createEvent, y as NcRuleSchema, yt as BaseAddon, z as TimelapseRuleSchema } from "../dist-C-_TL-I1.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";
@@ -8282,6 +8282,8 @@ var en_default = {
8282
8282
  "system.node-offline.body.worker": "Worker {{nodeId}} is offline",
8283
8283
  "system.node-inference-unavailable.title": "Node has no usable inference device",
8284
8284
  "system.node-inference-unavailable.body": "Node {{nodeId}} can no longer run inference — every enabled device is unavailable ({{devices}}). It will not receive cameras until one returns.",
8285
+ "system.addon-crash-loop.title": "Addon stopped after repeated crashes",
8286
+ "system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
8285
8287
  "system.detection-blind.title": "Camera detecting nothing",
8286
8288
  "system.detection-blind.body": {
8287
8289
  "one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
@@ -8494,6 +8496,8 @@ var it_default = {
8494
8496
  "system.node-offline.body.worker": "Il worker {{nodeId}} è offline",
8495
8497
  "system.node-inference-unavailable.title": "Nodo senza dispositivi di inferenza utilizzabili",
8496
8498
  "system.node-inference-unavailable.body": "Il nodo {{nodeId}} non può più eseguire inferenza — tutti i dispositivi abilitati non sono disponibili ({{devices}}). Non riceverà camere finché uno non torna disponibile.",
8499
+ "system.addon-crash-loop.title": "Addon fermato dopo ripetuti crash",
8500
+ "system.addon-crash-loop.body": "L'addon {{addons}} sul nodo {{nodeId}} è andato in crash {{count}} volte di seguito e NON verrà riavviato. Tutto ciò che fornisce resta offline finché non viene riparato.",
8497
8501
  "system.detection-blind.title": "Camera che non rileva nulla",
8498
8502
  "system.detection-blind.body": {
8499
8503
  "one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
@@ -10266,6 +10270,27 @@ function incomingFromSystemEvent(event, lookup) {
10266
10270
  nodeId
10267
10271
  });
10268
10272
  }
10273
+ case "addon.runner-failed": {
10274
+ const nodeId = stringField(data, "nodeId");
10275
+ const runnerId = stringField(data, "runnerId");
10276
+ const crashes = numberField(data, "crashesInWindow");
10277
+ if (nodeId === void 0 || runnerId === void 0 || crashes === void 0) return null;
10278
+ const ids = data["addonIds"];
10279
+ const addons = Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
10280
+ return systemSubject({
10281
+ facts,
10282
+ kind: "addon-crash-loop",
10283
+ subject: `addon-crash:${nodeId}:${runnerId}`,
10284
+ titleKey: "system.addon-crash-loop.title",
10285
+ bodyKey: "system.addon-crash-loop.body",
10286
+ textParams: {
10287
+ nodeId,
10288
+ addons: addons.length > 0 ? addons.join(", ") : runnerId,
10289
+ count: String(crashes)
10290
+ },
10291
+ nodeId
10292
+ });
10293
+ }
10269
10294
  case "pipeline.detection-blind": {
10270
10295
  const deviceId = numberField(data, "deviceId");
10271
10296
  const blindForMs = numberField(data, "blindForMs");
@@ -21918,6 +21943,9 @@ function systemEventKeys(kind) {
21918
21943
  }
21919
21944
  /** The arm mode a synthetic alarm event stands in for — see {@link systemEventKeys}. */
21920
21945
  var TEST_ALARM_MODE = "away";
21946
+ /** The runner a synthetic `addon-crash-loop` stands in for. A test fire has no
21947
+ * real crashed runner, and a blank one would render "The addon on node hub". */
21948
+ var TEST_CRASHED_RUNNER = "recorder";
21921
21949
  /** The dedup subject the live intake stamps for this kind — same families. */
21922
21950
  function systemEventSubject(kind, scope) {
21923
21951
  if (kind === "device-disabled" || kind === "device-enabled") return `device-switch:${scope.deviceId ?? 0}`;
@@ -21926,6 +21954,7 @@ function systemEventSubject(kind, scope) {
21926
21954
  if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
21927
21955
  if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
21928
21956
  if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
21957
+ if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
21929
21958
  if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
21930
21959
  if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
21931
21960
  if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
@@ -22042,6 +22071,7 @@ function planRuleTest(input) {
22042
22071
  seconds: "30",
22043
22072
  devices: "cuda:0",
22044
22073
  minutes: "15",
22074
+ addons: TEST_CRASHED_RUNNER,
22045
22075
  list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
22046
22076
  nodes: "hub",
22047
22077
  fromVersion: "0.0.0",
@@ -29958,6 +29988,22 @@ function stillInstantMs(ranges) {
29958
29988
  if (best === void 0) return void 0;
29959
29989
  return Math.floor((best.startMs + best.endMs) / 2);
29960
29990
  }
29991
+ /**
29992
+ * The distinct object classes attached to a visit, most frequent first,
29993
+ * capped at {@link MAX_CLIP_LABELS}. Empty when nothing classified overlapped
29994
+ * — the caller then omits the field so a motion/audio-only visit keeps its
29995
+ * kind label instead of carrying an empty array every consumer length-checks.
29996
+ */
29997
+ function dominantLabels(attached) {
29998
+ const counts = /* @__PURE__ */ new Map();
29999
+ for (const a of attached) {
30000
+ if (a.kind !== "object") continue;
30001
+ const name = a.event.className;
30002
+ if (typeof name !== "string" || name.length === 0) continue;
30003
+ counts.set(name, (counts.get(name) ?? 0) + 1);
30004
+ }
30005
+ return [...counts.entries()].toSorted((x, y) => y[1] - x[1]).slice(0, 3).map(([name]) => name);
30006
+ }
29961
30007
  function visitsToClips(visits, events, pad, vouched) {
29962
30008
  const tagged = [
29963
30009
  ...events.object.map((event) => ({
@@ -29978,6 +30024,7 @@ function visitsToClips(visits, events, pad, vouched) {
29978
30024
  const kind = dominantKind(attached);
29979
30025
  const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
29980
30026
  const stillAtMs = stillInstantMs(visit.ranges);
30027
+ const labels = dominantLabels(attached);
29981
30028
  return {
29982
30029
  id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
29983
30030
  source: "analytics",
@@ -29990,7 +30037,8 @@ function visitsToClips(visits, events, pad, vouched) {
29990
30037
  ...stillAtMs !== void 0 ? { stillAtMs } : {},
29991
30038
  eventIds: attached.slice(0, 24).map((a) => a.event.id),
29992
30039
  eventCount: attached.length,
29993
- holes: [...visit.holes]
30040
+ holes: [...visit.holes],
30041
+ ...labels.length > 0 ? { labels } : {}
29994
30042
  };
29995
30043
  }).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
29996
30044
  }
@@ -46049,6 +46097,7 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
46049
46097
  EventCategory.WorkerOffline,
46050
46098
  EventCategory.PipelineNodeInferenceUnavailable,
46051
46099
  EventCategory.PipelineDetectionBlind,
46100
+ EventCategory.AddonRunnerFailed,
46052
46101
  EventCategory.UpdateAvailable,
46053
46102
  EventCategory.AddonUpdated,
46054
46103
  EventCategory.SystemRestartCompleted,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CeFPqm3h.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.92",
3
+ "version": "1.2.94",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -66,7 +66,8 @@
66
66
  "description": "Zone analytics + audio metrics aggregator. Consumes pipeline events to surface per-camera zone counters and audio-level histograms.",
67
67
  "entry": "./dist/pipeline-analytics/index.js",
68
68
  "execution": {
69
- "placement": "any-node"
69
+ "placement": "any-node",
70
+ "heapProfile": "heavy"
70
71
  },
71
72
  "protected": true,
72
73
  "capabilities": [