@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.
@@ -29,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  enumerable: true
30
30
  }) : target, mod));
31
31
  //#endregion
32
- //#region ../types/dist/event-category-C0lyLd5U.mjs
32
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
33
33
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
34
34
  EventCategory["SystemBoot"] = "system.boot";
35
35
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -68,6 +68,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
68
68
  EventCategory["AddonInstalled"] = "addon.installed";
69
69
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
70
70
  EventCategory["AddonCrashed"] = "addon.crashed";
71
+ /**
72
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
73
+ *
74
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
75
+ * respawned"); this is the one that never resolves itself. Emitted exactly
76
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
77
+ * addons it hosted and the crash count that tripped the breaker.
78
+ *
79
+ * It exists because a runner marked terminally `failed` used to be SILENT:
80
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
81
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
82
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
83
+ * operator's phone told him. The Notification Center's system-event intake
84
+ * consumes this category and maps it to the `addon-crash-loop` kind.
85
+ */
86
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
71
87
  EventCategory["AddonError"] = "addon.error";
72
88
  EventCategory["AddonPageReady"] = "addon.page-ready";
73
89
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -14845,6 +14861,7 @@ var NcSystemEventKindSchema = _enum([
14845
14861
  "node-offline",
14846
14862
  "node-inference-unavailable",
14847
14863
  "detection-blind",
14864
+ "addon-crash-loop",
14848
14865
  "addon-update-available",
14849
14866
  "server-update-available",
14850
14867
  "alarm-triggered",
@@ -15970,6 +15987,10 @@ var NC_CONDITION_CATALOG = [
15970
15987
  value: "detection-blind",
15971
15988
  label: "Camera detecting nothing"
15972
15989
  },
15990
+ {
15991
+ value: "addon-crash-loop",
15992
+ label: "Addon stopped after repeated crashes"
15993
+ },
15973
15994
  {
15974
15995
  value: "addon-update-available",
15975
15996
  label: "Addon update available"
@@ -21504,6 +21525,14 @@ var ClipSchema = object({
21504
21525
  endMs: number()
21505
21526
  }),
21506
21527
  /**
21528
+ * Distinct object classes attached to this visit (`person`, `car`, …),
21529
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
21530
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
21531
+ * did not. Absent (never empty) when the visit attached no classified object
21532
+ * event, so a motion/audio-only visit keeps its kind label.
21533
+ */
21534
+ labels: array(string()).max(3).optional(),
21535
+ /**
21507
21536
  * Lazy thumbnail URL, never inlined.
21508
21537
  *
21509
21538
  * Recording-derived clips (events-mode keep-window, and the prepared
@@ -38568,7 +38597,7 @@ function systemEventFilterApplies(kind, filter) {
38568
38597
  switch (filter) {
38569
38598
  case "deviceIds": return kind.startsWith("device-") || kind.startsWith("stream-") || kind === "detection-blind" || kind === "alarm-triggered" || kind === "export-completed";
38570
38599
  case "deviceTypes": return kind.startsWith("device-") || kind === "detection-blind";
38571
- case "nodeIds": return kind.startsWith("node-") || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
38600
+ case "nodeIds": return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
38572
38601
  case "packageNames": return kind.endsWith("update-available") || kind === "addon-updated" || kind === "server-updated";
38573
38602
  }
38574
38603
  }
@@ -39025,6 +39054,303 @@ function cosineSimilarity(a, b) {
39025
39054
  function hfModelUrl(repo, path) {
39026
39055
  return `https://huggingface.co/${repo}/resolve/main/${path}`;
39027
39056
  }
39057
+ var MB = 1024 * 1024;
39058
+ var DEFAULT_POOL_MEMORY_POLICY = {
39059
+ sampleIntervalMs: 6e4,
39060
+ baselineSettleSamples: 5,
39061
+ baselineSampleCount: 3,
39062
+ restartMultiple: 4,
39063
+ floorBytes: 1024 * MB,
39064
+ ceilingBytes: 3072 * MB,
39065
+ cooldownMs: 30 * 6e4,
39066
+ maxRestartsPerWindow: 6,
39067
+ restartWindowMs: 1440 * 6e4
39068
+ };
39069
+ /**
39070
+ * Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
39071
+ * absent values keep the default — an operator typo must never disable the
39072
+ * bound or set it to zero.
39073
+ */
39074
+ function resolvePoolMemoryPolicy(env) {
39075
+ const num = (key, fallback, min) => {
39076
+ const raw = env[key];
39077
+ if (raw === void 0) return fallback;
39078
+ const parsed = Number(raw);
39079
+ return Number.isFinite(parsed) && parsed >= min ? parsed : fallback;
39080
+ };
39081
+ const d = DEFAULT_POOL_MEMORY_POLICY;
39082
+ return {
39083
+ sampleIntervalMs: num("CAMSTACK_POOL_MEM_INTERVAL_MS", d.sampleIntervalMs, 5e3),
39084
+ baselineSettleSamples: d.baselineSettleSamples,
39085
+ baselineSampleCount: d.baselineSampleCount,
39086
+ restartMultiple: num("CAMSTACK_POOL_MEM_MULTIPLE", d.restartMultiple, 1.5),
39087
+ floorBytes: num("CAMSTACK_POOL_MEM_FLOOR_MB", d.floorBytes / MB, 128) * MB,
39088
+ ceilingBytes: num("CAMSTACK_POOL_MEM_CEILING_MB", d.ceilingBytes / MB, 256) * MB,
39089
+ cooldownMs: num("CAMSTACK_POOL_MEM_COOLDOWN_MS", d.cooldownMs, 6e4),
39090
+ maxRestartsPerWindow: num("CAMSTACK_POOL_MEM_MAX_RESTARTS", d.maxRestartsPerWindow, 1),
39091
+ restartWindowMs: num("CAMSTACK_POOL_MEM_RESTART_WINDOW_MS", d.restartWindowMs, 6e4)
39092
+ };
39093
+ }
39094
+ /** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
39095
+ * Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
39096
+ function parseProcStatus(text) {
39097
+ const kb = (label) => {
39098
+ const match = text.match(new RegExp(`^${label}:\\s+(\\d+)\\s*kB`, "m"));
39099
+ return match ? Number(match[1]) * 1024 : null;
39100
+ };
39101
+ const rssBytes = kb("VmRSS");
39102
+ if (rssBytes === null) return null;
39103
+ const threadsMatch = text.match(/^Threads:\s+(\d+)/m);
39104
+ return {
39105
+ rssBytes,
39106
+ vmBytes: kb("VmSize") ?? 0,
39107
+ hwmBytes: kb("VmHWM") ?? 0,
39108
+ swapBytes: kb("VmSwap") ?? 0,
39109
+ threads: threadsMatch ? Number(threadsMatch[1]) : 0
39110
+ };
39111
+ }
39112
+ function initialPoolMemoryState() {
39113
+ return {
39114
+ settleSeen: 0,
39115
+ baselineWindow: [],
39116
+ baselineBytes: null,
39117
+ restartsAt: [],
39118
+ lastRestartAt: null
39119
+ };
39120
+ }
39121
+ function median(values) {
39122
+ const sorted = [...values].sort((a, b) => a - b);
39123
+ return sorted[Math.floor(sorted.length / 2)];
39124
+ }
39125
+ function advanceBaseline(state, rssBytes, policy) {
39126
+ if (state.baselineBytes !== null) return state;
39127
+ if (state.settleSeen < policy.baselineSettleSamples) return {
39128
+ ...state,
39129
+ settleSeen: state.settleSeen + 1
39130
+ };
39131
+ const window = [...state.baselineWindow, rssBytes];
39132
+ if (window.length < policy.baselineSampleCount) return {
39133
+ ...state,
39134
+ baselineWindow: window
39135
+ };
39136
+ return {
39137
+ ...state,
39138
+ baselineWindow: window,
39139
+ baselineBytes: median(window)
39140
+ };
39141
+ }
39142
+ /** The one place a pool's restart threshold is computed. */
39143
+ function poolMemoryThreshold(baselineBytes, policy) {
39144
+ if (baselineBytes === null) return policy.ceilingBytes;
39145
+ return Math.min(policy.ceilingBytes, Math.max(policy.floorBytes, policy.restartMultiple * baselineBytes));
39146
+ }
39147
+ function pruneRestarts(restartsAt, nowMs, policy) {
39148
+ return restartsAt.filter((t) => nowMs - t < policy.restartWindowMs);
39149
+ }
39150
+ /**
39151
+ * Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
39152
+ * The caller performs (and logs) the restart, then commits it via
39153
+ * {@link commitWatchdogRestart}.
39154
+ */
39155
+ function evaluatePoolMemory(state, rssBytes, nowMs, policy) {
39156
+ const next = advanceBaseline(state, rssBytes, policy);
39157
+ const thresholdBytes = poolMemoryThreshold(next.baselineBytes, policy);
39158
+ if (rssBytes <= thresholdBytes) return {
39159
+ state: next,
39160
+ action: next.baselineBytes === null ? "baseline-pending" : "ok",
39161
+ baselineBytes: next.baselineBytes,
39162
+ thresholdBytes
39163
+ };
39164
+ const inWindow = pruneRestarts(next.restartsAt, nowMs, policy);
39165
+ const pruned = {
39166
+ ...next,
39167
+ restartsAt: inWindow
39168
+ };
39169
+ if (inWindow.length >= policy.maxRestartsPerWindow) return {
39170
+ state: pruned,
39171
+ action: "exhausted",
39172
+ baselineBytes: next.baselineBytes,
39173
+ thresholdBytes
39174
+ };
39175
+ if (pruned.lastRestartAt !== null && nowMs - pruned.lastRestartAt < policy.cooldownMs) return {
39176
+ state: pruned,
39177
+ action: "cooldown",
39178
+ baselineBytes: next.baselineBytes,
39179
+ thresholdBytes
39180
+ };
39181
+ return {
39182
+ state: pruned,
39183
+ action: "restart",
39184
+ baselineBytes: next.baselineBytes,
39185
+ thresholdBytes
39186
+ };
39187
+ }
39188
+ /** Record a performed watchdog restart: budget consumed, baseline reset —
39189
+ * the recreated pool is a NEW process and gets a fresh settle window. */
39190
+ function commitWatchdogRestart(state, nowMs) {
39191
+ return {
39192
+ ...initialPoolMemoryState(),
39193
+ restartsAt: [...state.restartsAt, nowMs],
39194
+ lastRestartAt: nowMs
39195
+ };
39196
+ }
39197
+ /** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
39198
+ * kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
39199
+ * restart budget is kept: it bounds the WATCHDOG, not the pool. */
39200
+ function resetPoolBaseline(state) {
39201
+ return {
39202
+ ...initialPoolMemoryState(),
39203
+ restartsAt: state.restartsAt,
39204
+ lastRestartAt: state.lastRestartAt
39205
+ };
39206
+ }
39207
+ /**
39208
+ * Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
39209
+ * several pools at once (e.g. after a model rollout grew every baseline)
39210
+ * would take detection down on every camera simultaneously; one per sweep
39211
+ * means the survivors keep serving while the worst offender reloads, and the
39212
+ * next sweep (one interval later) takes the next one.
39213
+ */
39214
+ function pickRestartCandidate(candidates) {
39215
+ if (candidates.length === 0) return null;
39216
+ let worst = candidates[0];
39217
+ for (const c of candidates) if (c.rssBytes / c.thresholdBytes > worst.rssBytes / worst.thresholdBytes) worst = c;
39218
+ return worst.key;
39219
+ }
39220
+ var PoolMemoryWatchdog = class {
39221
+ pools = /* @__PURE__ */ new Map();
39222
+ timer = null;
39223
+ sweeping = false;
39224
+ stopped = false;
39225
+ opts;
39226
+ now;
39227
+ setTimer;
39228
+ clearTimer;
39229
+ constructor(opts) {
39230
+ this.opts = opts;
39231
+ this.now = opts.now ?? (() => Date.now());
39232
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
39233
+ this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
39234
+ }
39235
+ start() {
39236
+ if (this.timer || this.stopped) return;
39237
+ this.arm();
39238
+ }
39239
+ stop() {
39240
+ this.stopped = true;
39241
+ if (this.timer) {
39242
+ this.clearTimer(this.timer);
39243
+ this.timer = null;
39244
+ }
39245
+ }
39246
+ arm() {
39247
+ const timer = this.setTimer(() => {
39248
+ this.sweep().finally(() => {
39249
+ if (!this.stopped) this.arm();
39250
+ });
39251
+ }, this.opts.policy.sampleIntervalMs);
39252
+ if (typeof timer.unref === "function") timer.unref();
39253
+ this.timer = timer;
39254
+ }
39255
+ /** One sweep: sample → log every pool → restart at most one. Public so the
39256
+ * loop is testable without fake global timers. */
39257
+ async sweep() {
39258
+ if (this.sweeping) return;
39259
+ this.sweeping = true;
39260
+ try {
39261
+ await this.doSweep();
39262
+ } catch (err) {
39263
+ this.opts.log.warn("pool memory sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
39264
+ } finally {
39265
+ this.sweeping = false;
39266
+ }
39267
+ }
39268
+ async doSweep() {
39269
+ const nowMs = this.now();
39270
+ const samples = await this.opts.sample();
39271
+ const candidates = [];
39272
+ const verdictByKey = /* @__PURE__ */ new Map();
39273
+ for (const t of samples) {
39274
+ const tracked = this.pools.get(t.key);
39275
+ let state = tracked?.state ?? initialPoolMemoryState();
39276
+ if (tracked && tracked.pids.length > 0 && !samePids(tracked.pids, t.pids)) {
39277
+ this.opts.log.warn("pool process changed outside the watchdog — re-baselining", { meta: {
39278
+ poolKey: t.key,
39279
+ previousPids: tracked.pids,
39280
+ pids: t.pids
39281
+ } });
39282
+ state = resetPoolBaseline(state);
39283
+ }
39284
+ const verdict = evaluatePoolMemory(state, t.rssBytes, nowMs, this.opts.policy);
39285
+ this.pools.set(t.key, {
39286
+ state: verdict.state,
39287
+ pids: t.pids
39288
+ });
39289
+ verdictByKey.set(t.key, verdict);
39290
+ this.opts.log.info("pool memory", { meta: {
39291
+ poolKey: t.key,
39292
+ pids: t.pids,
39293
+ rssMb: Math.round(t.rssBytes / MB),
39294
+ baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
39295
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
39296
+ action: verdict.action,
39297
+ ...t.meta
39298
+ } });
39299
+ if (verdict.action === "restart") candidates.push({
39300
+ key: t.key,
39301
+ rssBytes: t.rssBytes,
39302
+ thresholdBytes: verdict.thresholdBytes
39303
+ });
39304
+ else if (verdict.action === "cooldown") this.opts.log.warn("pool over memory threshold but inside restart cooldown", { meta: {
39305
+ poolKey: t.key,
39306
+ rssMb: Math.round(t.rssBytes / MB),
39307
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
39308
+ cooldownMs: this.opts.policy.cooldownMs
39309
+ } });
39310
+ else if (verdict.action === "exhausted") this.opts.log.error("pool memory watchdog gave up — restart budget exhausted, pool leaks faster than restarts can pay for; needs a heap profile (py-spy / tracemalloc)", { meta: {
39311
+ poolKey: t.key,
39312
+ rssMb: Math.round(t.rssBytes / MB),
39313
+ thresholdMb: Math.round(verdict.thresholdBytes / MB),
39314
+ maxRestartsPerWindow: this.opts.policy.maxRestartsPerWindow,
39315
+ restartWindowMs: this.opts.policy.restartWindowMs
39316
+ } });
39317
+ }
39318
+ for (const key of [...this.pools.keys()]) if (!samples.some((t) => t.key === key)) this.pools.delete(key);
39319
+ const pickedKey = pickRestartCandidate(candidates);
39320
+ if (pickedKey === null) return;
39321
+ const picked = candidates.find((c) => c.key === pickedKey);
39322
+ const verdict = verdictByKey.get(pickedKey);
39323
+ const tracked = this.pools.get(pickedKey);
39324
+ const restartsInWindow = tracked.state.restartsAt.length + 1;
39325
+ this.opts.log.warn("pool memory watchdog restarting pool — RSS over threshold", { meta: {
39326
+ poolKey: pickedKey,
39327
+ pids: tracked.pids,
39328
+ rssMb: Math.round(picked.rssBytes / MB),
39329
+ thresholdMb: Math.round(picked.thresholdBytes / MB),
39330
+ baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
39331
+ restartsInWindow,
39332
+ deferredCandidates: candidates.filter((c) => c.key !== pickedKey).map((c) => c.key)
39333
+ } });
39334
+ try {
39335
+ await this.opts.restart(pickedKey);
39336
+ this.pools.set(pickedKey, {
39337
+ state: commitWatchdogRestart(tracked.state, this.now()),
39338
+ pids: []
39339
+ });
39340
+ } catch (err) {
39341
+ this.opts.log.error("pool memory watchdog restart failed", { meta: {
39342
+ poolKey: pickedKey,
39343
+ error: err instanceof Error ? err.message : String(err)
39344
+ } });
39345
+ }
39346
+ }
39347
+ };
39348
+ function samePids(a, b) {
39349
+ if (a.length !== b.length) return false;
39350
+ const sortedA = [...a].sort((x, y) => x - y);
39351
+ const sortedB = [...b].sort((x, y) => x - y);
39352
+ return sortedA.every((v, i) => v === sortedB[i]);
39353
+ }
39028
39354
  /**
39029
39355
  * Vector wire codec — base64 of little-endian Float32.
39030
39356
  *
@@ -39234,6 +39560,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
39234
39560
  return OpsLogEntrySchema;
39235
39561
  }
39236
39562
  });
39563
+ Object.defineProperty(exports, "PoolMemoryWatchdog", {
39564
+ enumerable: true,
39565
+ get: function() {
39566
+ return PoolMemoryWatchdog;
39567
+ }
39568
+ });
39237
39569
  Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
39238
39570
  enumerable: true,
39239
39571
  get: function() {
@@ -39504,6 +39836,12 @@ Object.defineProperty(exports, "object", {
39504
39836
  return object;
39505
39837
  }
39506
39838
  });
39839
+ Object.defineProperty(exports, "parseProcStatus", {
39840
+ enumerable: true,
39841
+ get: function() {
39842
+ return parseProcStatus;
39843
+ }
39844
+ });
39507
39845
  Object.defineProperty(exports, "partialRecord", {
39508
39846
  enumerable: true,
39509
39847
  get: function() {
@@ -39540,6 +39878,12 @@ Object.defineProperty(exports, "record", {
39540
39878
  return record;
39541
39879
  }
39542
39880
  });
39881
+ Object.defineProperty(exports, "resolvePoolMemoryPolicy", {
39882
+ enumerable: true,
39883
+ get: function() {
39884
+ return resolvePoolMemoryPolicy;
39885
+ }
39886
+ });
39543
39887
  Object.defineProperty(exports, "sceneMonitorCapability", {
39544
39888
  enumerable: true,
39545
39889
  get: function() {