camstack 1.2.58 → 1.2.60

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.
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
23633
23633
  }
23634
23634
  });
23635
23635
 
23636
- // ../system/dist/dist-CxgTulEG.js
23637
- var require_dist_CxgTulEG = __commonJS({
23638
- "../system/dist/dist-CxgTulEG.js"(exports) {
23636
+ // ../system/dist/dist-BPlfW-CG.js
23637
+ var require_dist_BPlfW_CG = __commonJS({
23638
+ "../system/dist/dist-BPlfW-CG.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -32718,6 +32718,38 @@ var require_dist_CxgTulEG = __commonJS({
32718
32718
  /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
32719
32719
  uptimeSec: zod.z.number()
32720
32720
  });
32721
+ var ContainerMemoryPointSchema = zod.z.object({
32722
+ /** Which hierarchy answered, so a reading is never ambiguous. */
32723
+ source: zod.z.enum(["cgroup-v2", "cgroup-v1"]),
32724
+ /** `memory.current` (v2) / `memory.usage_in_bytes` (v1). Always known. */
32725
+ currentBytes: zod.z.number(),
32726
+ /** The cgroup's ceiling. `null` = NO LIMIT — never a sentinel, never zero. */
32727
+ limitBytes: zod.z.number().nullable(),
32728
+ /** Anonymous pages: the closest thing to "what the processes allocated". */
32729
+ anonBytes: zod.z.number().nullable(),
32730
+ /** Page cache. Charged to the cgroup, owned by no process. */
32731
+ fileBytes: zod.z.number().nullable(),
32732
+ /**
32733
+ * Shared memory — and the field that explained the largest single surprise.
32734
+ * The i915 driver backs GPU buffers with shmem, so an inference pool or a
32735
+ * hardware-decode session holding DRM objects is charged HERE and appears
32736
+ * nowhere in a `ps` scan.
32737
+ */
32738
+ shmemBytes: zod.z.number().nullable(),
32739
+ /** Kernel slab charged to this cgroup. `null` on v1, which never publishes it. */
32740
+ slabBytes: zod.z.number().nullable(),
32741
+ /**
32742
+ * Shrinkable i915 GEM object bytes, from debugfs.
32743
+ *
32744
+ * **Host-wide across every DRM client, NOT cgroup-scoped.** It is not a
32745
+ * component of `currentBytes` and must not be subtracted from it; it says
32746
+ * what put the shmem there, where `shmemBytes` only says how much.
32747
+ *
32748
+ * `null` wherever debugfs is not mounted — which is inside every camstack
32749
+ * container today — and on any node with no Intel GPU.
32750
+ */
32751
+ gpuShmemBytes: zod.z.number().nullable()
32752
+ }).extend({ atMs: zod.z.number() });
32721
32753
  var DumpHeapSnapshotInputSchema = zod.z.object({
32722
32754
  /** The addon whose runner should dump a heap snapshot. */
32723
32755
  addonId: zod.z.string()
@@ -32767,6 +32799,21 @@ var require_dist_CxgTulEG = __commonJS({
32767
32799
  /** One entry per function seen in the window, heaviest-first. */
32768
32800
  series: zod.z.array(LoadFunctionSeriesSchema).readonly(),
32769
32801
  /**
32802
+ * The CONTAINER's memory over the same window, oldest-first.
32803
+ *
32804
+ * Sits next to `series` rather than in a method of its own because the whole
32805
+ * question is a subtraction: the per-process rows in `series` sum to one
32806
+ * number and this one is another, and an operator who has to issue two calls
32807
+ * to compare them will compare two different instants. Same reader, same
32808
+ * `sinceMs`, same `bucketMs`, same timestamps.
32809
+ *
32810
+ * **EMPTY means ABSENT, never zero.** A node with no cgroup — a developer
32811
+ * Mac, a bare-metal host, a container with the hierarchy hidden — reports no
32812
+ * points at all. A zero here would be indistinguishable from a healthy
32813
+ * container and is precisely the lie this field exists to avoid.
32814
+ */
32815
+ containerMemory: zod.z.array(ContainerMemoryPointSchema).readonly(),
32816
+ /**
32770
32817
  * Width of one returned bucket, in ms. Equals the sampling cadence when no
32771
32818
  * reduction was needed — so a caller can always say what one point covers
32772
32819
  * without having to know whether it was reduced.
@@ -43598,6 +43645,33 @@ var require_dist_CxgTulEG = __commonJS({
43598
43645
  deviceNative: true,
43599
43646
  mode: "singleton",
43600
43647
  deviceTypes: [DeviceType.Camera],
43648
+ /**
43649
+ * **Auth tier: `protected` on every method — deliberate, and load-bearing.**
43650
+ *
43651
+ * Talking through a camera is an OPERATE action, not a CONFIGURE one. This
43652
+ * cap has no configuration surface at all: all six methods open, feed and
43653
+ * close one live audio session against one `deviceId`. That is the same
43654
+ * authority as `ptz.move` or `snapshot.getSnapshot`, both `protected` — and
43655
+ * the opposite of `ptz.savePreset` / `snapshot.invalidateCache`, which are
43656
+ * `admin` because they change what the device IS.
43657
+ *
43658
+ * `protected` does not mean ungated: `protectedProcedure` runs the
43659
+ * `METHOD_ACCESS_MAP` scope check, and every method here is `scope: 'device'`
43660
+ * with `access: 'create'` and a `deviceId` in its input. So a caller needs a
43661
+ * grant that covers THAT camera at `create` — a `camera-viewer` (`view`
43662
+ * only) still cannot talk, and a grant on camera 5 cannot talk through
43663
+ * camera 7.
43664
+ *
43665
+ * Every method was `auth: 'admin'` from the initial commit, which made the
43666
+ * cap unreachable by every non-admin principal — `adminProcedure` throws
43667
+ * `FORBIDDEN: Admin required` BEFORE the scope check runs, so the scope
43668
+ * machinery generated for this cap (`METHOD_ACCESS_MAP`,
43669
+ * `DEVICE_SCOPED_CAPS`, `METHOD_DEVICE_SELECTORS`) was complete and dead. The
43670
+ * `camera-operator` scope preset has promised "PTZ control, intercom,
43671
+ * snapshots" since that same commit; the promise could not be kept. Recorded
43672
+ * as D289; `scripts/check-scope-preset-promises.ts` now fails the build if a
43673
+ * preset promises a cap no row of that preset can reach.
43674
+ */
43601
43675
  methods: {
43602
43676
  /**
43603
43677
  * Open a server-side WebRTC audio-only session. Returns an SDP
@@ -43610,7 +43684,7 @@ var require_dist_CxgTulEG = __commonJS({
43610
43684
  sdpOffer: zod.z.string()
43611
43685
  }), {
43612
43686
  kind: "mutation",
43613
- auth: "admin"
43687
+ auth: "protected"
43614
43688
  }),
43615
43689
  handleAnswer: method(zod.z.object({
43616
43690
  deviceId: zod.z.number(),
@@ -43618,7 +43692,7 @@ var require_dist_CxgTulEG = __commonJS({
43618
43692
  sdpAnswer: zod.z.string()
43619
43693
  }), zod.z.void(), {
43620
43694
  kind: "mutation",
43621
- auth: "admin"
43695
+ auth: "protected"
43622
43696
  }),
43623
43697
  /** Close explicitly. Server also auto-closes on 30s idle. */
43624
43698
  stopSession: method(zod.z.object({
@@ -43626,7 +43700,7 @@ var require_dist_CxgTulEG = __commonJS({
43626
43700
  sessionId: zod.z.string()
43627
43701
  }), zod.z.void(), {
43628
43702
  kind: "mutation",
43629
- auth: "admin"
43703
+ auth: "protected"
43630
43704
  }),
43631
43705
  /**
43632
43706
  * Open a raw-PCM talk session (no WebRTC SDP plumbing). Used by
@@ -43639,7 +43713,7 @@ var require_dist_CxgTulEG = __commonJS({
43639
43713
  */
43640
43714
  startTalkSession: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.object({ sessionId: zod.z.string() }), {
43641
43715
  kind: "mutation",
43642
- auth: "admin"
43716
+ auth: "protected"
43643
43717
  }),
43644
43718
  /**
43645
43719
  * Push one chunk of talk-back audio onto the active talk session.
@@ -43674,12 +43748,12 @@ var require_dist_CxgTulEG = __commonJS({
43674
43748
  sequenceNumber: zod.z.number().int()
43675
43749
  }), zod.z.object({ accepted: zod.z.boolean() }), {
43676
43750
  kind: "mutation",
43677
- auth: "admin"
43751
+ auth: "protected"
43678
43752
  }),
43679
43753
  /** Close the raw-PCM talk session. Idempotent. */
43680
43754
  endTalkSession: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), {
43681
43755
  kind: "mutation",
43682
- auth: "admin"
43756
+ auth: "protected"
43683
43757
  })
43684
43758
  },
43685
43759
  events: { onStatusChanged: { data: zod.z.object({
@@ -58639,7 +58713,7 @@ var require_alerts_addon = __commonJS({
58639
58713
  [Symbol.toStringTag]: { value: "Module" }
58640
58714
  });
58641
58715
  require_chunk_Cek0wNdY();
58642
- var require_dist10 = require_dist_CxgTulEG();
58716
+ var require_dist10 = require_dist_BPlfW_CG();
58643
58717
  function selectExpired(alerts, cutoffMs) {
58644
58718
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
58645
58719
  }
@@ -59458,7 +59532,7 @@ var require_console_logging = __commonJS({
59458
59532
  [Symbol.toStringTag]: { value: "Module" }
59459
59533
  });
59460
59534
  require_chunk_Cek0wNdY();
59461
- var require_dist10 = require_dist_CxgTulEG();
59535
+ var require_dist10 = require_dist_BPlfW_CG();
59462
59536
  var require_formatter = require_formatter_DqAKDlvN();
59463
59537
  var LEVEL_RANK = {
59464
59538
  debug: 0,
@@ -59552,7 +59626,7 @@ var require_core_blocks_addon = __commonJS({
59552
59626
  "use strict";
59553
59627
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
59554
59628
  var require_chunk = require_chunk_Cek0wNdY();
59555
- var require_dist10 = require_dist_CxgTulEG();
59629
+ var require_dist10 = require_dist_BPlfW_CG();
59556
59630
  var node_crypto = __require("crypto");
59557
59631
  var node_fs_promises = __require("fs/promises");
59558
59632
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -60449,11 +60523,11 @@ var require_core_blocks = __commonJS({
60449
60523
  }
60450
60524
  });
60451
60525
 
60452
- // ../system/dist/retired-settings-keys-BbjWjol0.js
60453
- var require_retired_settings_keys_BbjWjol0 = __commonJS({
60454
- "../system/dist/retired-settings-keys-BbjWjol0.js"(exports) {
60526
+ // ../system/dist/retired-settings-keys-DBY6ebwV.js
60527
+ var require_retired_settings_keys_DBY6ebwV = __commonJS({
60528
+ "../system/dist/retired-settings-keys-DBY6ebwV.js"(exports) {
60455
60529
  "use strict";
60456
- var require_dist10 = require_dist_CxgTulEG();
60530
+ var require_dist10 = require_dist_BPlfW_CG();
60457
60531
  function settingsStoreIsAuthoritativeHere(env) {
60458
60532
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
60459
60533
  return raw === "" || raw === "hub";
@@ -62667,8 +62741,8 @@ var require_device_manager_addon = __commonJS({
62667
62741
  [Symbol.toStringTag]: { value: "Module" }
62668
62742
  });
62669
62743
  require_chunk_Cek0wNdY();
62670
- var require_dist10 = require_dist_CxgTulEG();
62671
- var require_retired_settings_keys = require_retired_settings_keys_BbjWjol0();
62744
+ var require_dist10 = require_dist_BPlfW_CG();
62745
+ var require_retired_settings_keys = require_retired_settings_keys_DBY6ebwV();
62672
62746
  var node_crypto = __require("crypto");
62673
62747
  var _camstack_types_node = require_node();
62674
62748
  var JOB_HISTORY = 20;
@@ -67417,7 +67491,7 @@ var require_hub_forwarder = __commonJS({
67417
67491
  [Symbol.toStringTag]: { value: "Module" }
67418
67492
  });
67419
67493
  require_chunk_Cek0wNdY();
67420
- var require_dist10 = require_dist_CxgTulEG();
67494
+ var require_dist10 = require_dist_BPlfW_CG();
67421
67495
  var require_formatter = require_formatter_DqAKDlvN();
67422
67496
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
67423
67497
  var HubForwarderDestination = class {
@@ -67554,7 +67628,7 @@ var require_liveness_monitor_addon = __commonJS({
67554
67628
  "use strict";
67555
67629
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
67556
67630
  require_chunk_Cek0wNdY();
67557
- var require_dist10 = require_dist_CxgTulEG();
67631
+ var require_dist10 = require_dist_BPlfW_CG();
67558
67632
  var NO_DEVICES = "liveness:no-devices";
67559
67633
  var ALL_OFFLINE = "liveness:all-devices-offline";
67560
67634
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -67744,7 +67818,7 @@ var require_local_auth_addon = __commonJS({
67744
67818
  [Symbol.toStringTag]: { value: "Module" }
67745
67819
  });
67746
67820
  var require_chunk = require_chunk_Cek0wNdY();
67747
- var require_dist10 = require_dist_CxgTulEG();
67821
+ var require_dist10 = require_dist_BPlfW_CG();
67748
67822
  var node_crypto = __require("crypto");
67749
67823
  node_crypto = require_chunk.__toESM(node_crypto);
67750
67824
  var crypto$1 = __require("crypto");
@@ -75557,7 +75631,7 @@ var require_loki_logging = __commonJS({
75557
75631
  [Symbol.toStringTag]: { value: "Module" }
75558
75632
  });
75559
75633
  require_chunk_Cek0wNdY();
75560
- var require_dist10 = require_dist_CxgTulEG();
75634
+ var require_dist10 = require_dist_BPlfW_CG();
75561
75635
  function sanitizeLabelName(raw) {
75562
75636
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
75563
75637
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -76122,14 +76196,859 @@ var require_native_metrics_addon = __commonJS({
76122
76196
  [Symbol.toStringTag]: { value: "Module" }
76123
76197
  });
76124
76198
  var require_chunk = require_chunk_Cek0wNdY();
76125
- var require_dist10 = require_dist_CxgTulEG();
76199
+ var require_dist10 = require_dist_BPlfW_CG();
76126
76200
  var node_fs_promises = __require("fs/promises");
76127
76201
  var node_child_process = __require("child_process");
76128
76202
  var node_util = __require("util");
76129
- var node_os = __require("os");
76130
- node_os = require_chunk.__toESM(node_os);
76131
76203
  var node_fs = __require("fs");
76132
76204
  node_fs = require_chunk.__toESM(node_fs);
76205
+ var node_os = __require("os");
76206
+ node_os = require_chunk.__toESM(node_os);
76207
+ var nodeCgroupFileReader = { read(path) {
76208
+ try {
76209
+ return (0, node_fs.readFileSync)(path, "utf8");
76210
+ } catch {
76211
+ return null;
76212
+ }
76213
+ } };
76214
+ var CGROUP_MOUNT_ROOT = "/sys/fs/cgroup";
76215
+ var I915_GEM_OBJECTS_PATH = "/sys/kernel/debug/dri/0/i915_gem_objects";
76216
+ var V1_UNLIMITED_SENTINEL = 9223372036854772e3;
76217
+ function parseCgroupSelf(text) {
76218
+ let v1 = null;
76219
+ for (const rawLine of text.split("\n")) {
76220
+ const line = rawLine.trim();
76221
+ if (line.length === 0) continue;
76222
+ const first = line.indexOf(":");
76223
+ if (first < 0) continue;
76224
+ const second = line.indexOf(":", first + 1);
76225
+ if (second < 0) continue;
76226
+ const controllers = line.slice(first + 1, second);
76227
+ const path = line.slice(second + 1);
76228
+ if (path.length === 0) continue;
76229
+ const normalized = path.startsWith("/") ? path : `/${path}`;
76230
+ if (controllers.length === 0) return {
76231
+ version: "v2",
76232
+ path: normalized
76233
+ };
76234
+ if (v1 === null && controllers.split(",").includes("memory")) v1 = {
76235
+ version: "v1",
76236
+ path: normalized
76237
+ };
76238
+ }
76239
+ return v1;
76240
+ }
76241
+ function parseCgroupStat(text) {
76242
+ const out = /* @__PURE__ */ new Map();
76243
+ for (const rawLine of text.split("\n")) {
76244
+ const line = rawLine.trim();
76245
+ if (line.length === 0) continue;
76246
+ const space = line.indexOf(" ");
76247
+ if (space <= 0) continue;
76248
+ const key = line.slice(0, space);
76249
+ const value = Number(line.slice(space + 1).trim());
76250
+ if (!Number.isFinite(value)) continue;
76251
+ out.set(key, value);
76252
+ }
76253
+ return out;
76254
+ }
76255
+ function parseCgroupScalar(text) {
76256
+ const trimmed = text.trim();
76257
+ if (trimmed.length === 0) return null;
76258
+ if (trimmed === "max") return null;
76259
+ const value = Number(trimmed);
76260
+ if (!Number.isFinite(value) || value < 0) return null;
76261
+ if (value >= V1_UNLIMITED_SENTINEL) return null;
76262
+ return value;
76263
+ }
76264
+ function parseI915GemObjects(text) {
76265
+ const match = text.match(/^\s*\d+\s+shrinkable\b[^,\n]*,\s*(\d+)\s+bytes/m);
76266
+ if (match === null) return null;
76267
+ const bytes = Number(match[1]);
76268
+ return Number.isFinite(bytes) ? bytes : null;
76269
+ }
76270
+ function resolveCgroupMemoryLayout(fs) {
76271
+ const selfText = fs.read("/proc/self/cgroup");
76272
+ if (selfText === null) return null;
76273
+ const entry = parseCgroupSelf(selfText);
76274
+ if (entry === null) return null;
76275
+ const suffix = entry.path === "/" ? "" : entry.path;
76276
+ const candidates = entry.version === "v2" ? [`${CGROUP_MOUNT_ROOT}${suffix}`, CGROUP_MOUNT_ROOT] : [`${CGROUP_MOUNT_ROOT}/memory${suffix}`, `${CGROUP_MOUNT_ROOT}/memory`];
76277
+ const usageFile = entry.version === "v2" ? "memory.current" : "memory.usage_in_bytes";
76278
+ const limitFile = entry.version === "v2" ? "memory.max" : "memory.limit_in_bytes";
76279
+ for (const directory of candidates) {
76280
+ if (fs.read(`${directory}/${usageFile}`) === null) continue;
76281
+ return {
76282
+ source: entry.version === "v2" ? "cgroup-v2" : "cgroup-v1",
76283
+ directory,
76284
+ currentPath: `${directory}/${usageFile}`,
76285
+ limitPath: `${directory}/${limitFile}`,
76286
+ statPath: `${directory}/memory.stat`
76287
+ };
76288
+ }
76289
+ return null;
76290
+ }
76291
+ function statValue(stat, key) {
76292
+ const value = stat.get(key);
76293
+ return value === void 0 ? null : value;
76294
+ }
76295
+ function resolveSlab(stat) {
76296
+ const direct = statValue(stat, "slab");
76297
+ if (direct !== null) return direct;
76298
+ const reclaimable = stat.get("slab_reclaimable");
76299
+ const unreclaimable = stat.get("slab_unreclaimable");
76300
+ if (reclaimable === void 0 || unreclaimable === void 0) return null;
76301
+ return reclaimable + unreclaimable;
76302
+ }
76303
+ function buildContainerMemorySnapshot(input) {
76304
+ const currentBytes = parseCgroupScalar(input.currentText);
76305
+ if (currentBytes === null) return null;
76306
+ const stat = input.statText === null ? /* @__PURE__ */ new Map() : parseCgroupStat(input.statText);
76307
+ const isV2 = input.source === "cgroup-v2";
76308
+ return {
76309
+ source: input.source,
76310
+ currentBytes,
76311
+ limitBytes: input.limitText === null ? null : parseCgroupScalar(input.limitText),
76312
+ anonBytes: statValue(stat, isV2 ? "anon" : "rss"),
76313
+ fileBytes: statValue(stat, isV2 ? "file" : "cache"),
76314
+ shmemBytes: statValue(stat, "shmem"),
76315
+ slabBytes: isV2 ? resolveSlab(stat) : null,
76316
+ gpuShmemBytes: input.gpuShmemBytes
76317
+ };
76318
+ }
76319
+ var ContainerMemoryProbe = class {
76320
+ logger;
76321
+ fs;
76322
+ wantGpu;
76323
+ resolved = false;
76324
+ layout = null;
76325
+ gpuProbed = false;
76326
+ gpuReadable = false;
76327
+ /** `null` until the first sample — so the FIRST outcome is a transition too. */
76328
+ lastReadOk = null;
76329
+ constructor(deps) {
76330
+ this.logger = deps.logger;
76331
+ this.fs = deps.fs ?? nodeCgroupFileReader;
76332
+ this.wantGpu = deps.readGpu ?? true;
76333
+ }
76334
+ /** Did this node resolve a cgroup at all? Absent is a reportable fact. */
76335
+ get available() {
76336
+ return this.layout !== null;
76337
+ }
76338
+ /**
76339
+ * One reading, or `null` when this node has no cgroup or the cgroup stopped
76340
+ * answering. `null` is ABSENT — the caller contributes no point, and no
76341
+ * point is what a chart must show, never a zero.
76342
+ */
76343
+ sample() {
76344
+ const layout = this.resolveOnce();
76345
+ if (layout === null) return null;
76346
+ const currentText = this.fs.read(layout.currentPath);
76347
+ if (currentText === null) {
76348
+ this.noteRead(false, `unreadable: ${layout.currentPath}`);
76349
+ return null;
76350
+ }
76351
+ const snapshot = buildContainerMemorySnapshot({
76352
+ source: layout.source,
76353
+ currentText,
76354
+ limitText: this.fs.read(layout.limitPath),
76355
+ statText: this.fs.read(layout.statPath),
76356
+ gpuShmemBytes: this.readGpuShmem()
76357
+ });
76358
+ if (snapshot === null) {
76359
+ this.noteRead(false, `unparseable: ${layout.currentPath}`);
76360
+ return null;
76361
+ }
76362
+ this.noteRead(true, null);
76363
+ return snapshot;
76364
+ }
76365
+ /** Resolve on first use and never again — a cgroup does not move under a pid. */
76366
+ resolveOnce() {
76367
+ if (this.resolved) return this.layout;
76368
+ this.resolved = true;
76369
+ try {
76370
+ this.layout = resolveCgroupMemoryLayout(this.fs);
76371
+ } catch (err) {
76372
+ this.layout = null;
76373
+ this.logger.warn("cgroup memory resolution failed \u2014 container memory will be ABSENT", { meta: { error: require_dist10.errMsg(err) } });
76374
+ return null;
76375
+ }
76376
+ if (this.layout === null) {
76377
+ this.logger.info("no cgroup on this node \u2014 container memory is ABSENT, not zero, for every sample", { meta: {
76378
+ platform: process.platform,
76379
+ probed: "/proc/self/cgroup"
76380
+ } });
76381
+ return null;
76382
+ }
76383
+ this.logger.info("container memory resolved from the cgroup", { meta: {
76384
+ source: this.layout.source,
76385
+ directory: this.layout.directory
76386
+ } });
76387
+ return this.layout;
76388
+ }
76389
+ /**
76390
+ * The GPU number, probed once.
76391
+ *
76392
+ * It is host-wide and lives in debugfs, so it is absent far more often than
76393
+ * it is present — inside every camstack container today. One `open` decides
76394
+ * it for the life of the process; after that an unreadable file costs
76395
+ * nothing at all, not even a syscall.
76396
+ */
76397
+ readGpuShmem() {
76398
+ if (!this.wantGpu) return null;
76399
+ if (!this.gpuProbed) {
76400
+ this.gpuProbed = true;
76401
+ const text2 = this.fs.read(I915_GEM_OBJECTS_PATH);
76402
+ this.gpuReadable = text2 !== null && parseI915GemObjects(text2) !== null;
76403
+ this.logger.info(this.gpuReadable ? "i915 GEM accounting is readable \u2014 GPU-backed shmem will be reported" : "i915 GEM accounting is NOT readable \u2014 gpuShmemBytes stays UNKNOWN (this is normal: debugfs is not mounted in the container, and a node with no Intel GPU has no such file)", { meta: { path: I915_GEM_OBJECTS_PATH } });
76404
+ if (!this.gpuReadable) return null;
76405
+ return text2 === null ? null : parseI915GemObjects(text2);
76406
+ }
76407
+ if (!this.gpuReadable) return null;
76408
+ const text = this.fs.read(I915_GEM_OBJECTS_PATH);
76409
+ return text === null ? null : parseI915GemObjects(text);
76410
+ }
76411
+ /** Log a read outcome only when it CHANGES. Never once per tick. */
76412
+ noteRead(ok, reason) {
76413
+ if (this.lastReadOk === ok) return;
76414
+ const first = this.lastReadOk === null;
76415
+ this.lastReadOk = ok;
76416
+ if (!ok) {
76417
+ this.logger.warn("cgroup memory stopped answering \u2014 container memory is ABSENT until it returns", { meta: { reason: reason ?? "unknown" } });
76418
+ return;
76419
+ }
76420
+ if (!first) this.logger.info("cgroup memory is answering again");
76421
+ }
76422
+ };
76423
+ var LoadPartition = class {
76424
+ capacity;
76425
+ slots;
76426
+ head = 0;
76427
+ count = 0;
76428
+ rows = 0;
76429
+ /** Newest retained sample's timestamp — the monotonic gate for `push`. */
76430
+ newestAtMs = null;
76431
+ constructor(capacity) {
76432
+ this.capacity = capacity;
76433
+ this.slots = Array.from({ length: capacity });
76434
+ }
76435
+ /**
76436
+ * Accept a sample.
76437
+ *
76438
+ * A sample at or before the newest one already held is REFUSED. The bus drops
76439
+ * a node's own broadcast echo, but a cross-node redelivery or a replayed
76440
+ * subscription must not be able to double a point — and idempotence here is
76441
+ * what lets every reader above treat the series as a set.
76442
+ *
76443
+ * `accepted` is reported separately from `rowDelta` on purpose: a full ring
76444
+ * that evicts a sample of the same size has a delta of zero and is not a
76445
+ * refusal, and conflating the two would silently stop advancing the write
76446
+ * ordinal on a steady-state cluster.
76447
+ */
76448
+ push(sample) {
76449
+ if (this.newestAtMs !== null && sample.atMs <= this.newestAtMs) return {
76450
+ accepted: false,
76451
+ rowDelta: 0
76452
+ };
76453
+ const evicted = this.count === this.capacity ? this.slots[this.head]?.processes.length ?? 0 : 0;
76454
+ this.slots[this.head] = sample;
76455
+ this.head = (this.head + 1) % this.capacity;
76456
+ if (this.count < this.capacity) this.count++;
76457
+ this.newestAtMs = sample.atMs;
76458
+ const rowDelta = sample.processes.length - evicted;
76459
+ this.rows += rowDelta;
76460
+ return {
76461
+ accepted: true,
76462
+ rowDelta
76463
+ };
76464
+ }
76465
+ /** Drop the oldest sample. Returns the rows reclaimed (0 when empty). */
76466
+ dropOldest() {
76467
+ if (this.count === 0) return 0;
76468
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76469
+ const victim = this.slots[index];
76470
+ this.slots[index] = void 0;
76471
+ this.count--;
76472
+ const reclaimed = victim?.processes.length ?? 0;
76473
+ this.rows -= reclaimed;
76474
+ if (this.count === 0) this.newestAtMs = null;
76475
+ return reclaimed;
76476
+ }
76477
+ /** Oldest-first, optionally only what is strictly newer than `sinceMs`. */
76478
+ list(sinceMs) {
76479
+ const out = [];
76480
+ for (let i = 0; i < this.count; i++) {
76481
+ const index = (this.head - this.count + i + this.capacity) % this.capacity;
76482
+ const sample = this.slots[index];
76483
+ if (sample === void 0) continue;
76484
+ if (sinceMs !== void 0 && sample.atMs <= sinceMs) continue;
76485
+ out.push(sample);
76486
+ }
76487
+ return out;
76488
+ }
76489
+ size() {
76490
+ return this.count;
76491
+ }
76492
+ rowCount() {
76493
+ return this.rows;
76494
+ }
76495
+ oldestAtMs() {
76496
+ if (this.count === 0) return null;
76497
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76498
+ return this.slots[index]?.atMs ?? null;
76499
+ }
76500
+ lastWriteAtMs() {
76501
+ return this.newestAtMs;
76502
+ }
76503
+ };
76504
+ var NodeLoadRing = class {
76505
+ partitions = /* @__PURE__ */ new Map();
76506
+ /**
76507
+ * Node id → write ordinal of its last accepted sample. A monotonic counter,
76508
+ * not a clock: partition eviction must follow the order writes actually
76509
+ * happened, and node clocks disagree.
76510
+ */
76511
+ lastWriteSeq = /* @__PURE__ */ new Map();
76512
+ writeSeq = 0;
76513
+ totalRows = 0;
76514
+ samplesPerNode;
76515
+ maxTotalProcessRows;
76516
+ maxNodes;
76517
+ idleEvictionMs;
76518
+ now;
76519
+ constructor(options = {}) {
76520
+ this.samplesPerNode = options.samplesPerNode ?? 180;
76521
+ this.maxTotalProcessRows = options.maxTotalProcessRows ?? 32e3;
76522
+ this.maxNodes = options.maxNodes ?? 16;
76523
+ this.idleEvictionMs = options.idleEvictionMs ?? 36e5;
76524
+ this.now = options.now ?? Date.now;
76525
+ }
76526
+ /**
76527
+ * Retain one snapshot. `processes` is stored by reference — the payload is
76528
+ * already an immutable arrival off the bus, and copying it would double the
76529
+ * measured cost for nothing.
76530
+ *
76531
+ * Returns whether the sample was ACCEPTED — that is, whether it was new
76532
+ * rather than a replay of a timestamp this node has already delivered.
76533
+ *
76534
+ * The return value is not diagnostics. It is the idempotence gate the
76535
+ * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
76536
+ * appended to the table, which is what lets that table carry an `INTEGER`
76537
+ * rowid key instead of a composite unique index over two million rows. The
76538
+ * gate is one monotonic comparison in memory; the index it replaces was
76539
+ * measured at ~40 bytes per row.
76540
+ */
76541
+ record(nodeId, atMs, processes) {
76542
+ if (nodeId.length === 0) return false;
76543
+ this.sweepIdle();
76544
+ let partition = this.partitions.get(nodeId);
76545
+ if (partition === void 0) {
76546
+ partition = new LoadPartition(this.samplesPerNode);
76547
+ this.partitions.set(nodeId, partition);
76548
+ }
76549
+ const outcome = partition.push({
76550
+ atMs,
76551
+ processes
76552
+ });
76553
+ if (!outcome.accepted) {
76554
+ if (partition.size() === 0) this.partitions.delete(nodeId);
76555
+ return false;
76556
+ }
76557
+ this.totalRows += outcome.rowDelta;
76558
+ this.lastWriteSeq.set(nodeId, ++this.writeSeq);
76559
+ if (this.partitions.size > this.maxNodes) this.evictPartitions(this.partitions.size - this.maxNodes, nodeId);
76560
+ this.enforceRowBudget();
76561
+ return true;
76562
+ }
76563
+ /**
76564
+ * Read one node's retained series, oldest-first.
76565
+ *
76566
+ * `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
76567
+ * holds and gets back only what it is missing. That is the whole contract
76568
+ * that lets the admin UI seed from here and then continue live without
76569
+ * doubling a point it already drew.
76570
+ *
76571
+ * A node nobody has recorded answers with an empty series, not an error —
76572
+ * unknown is the truth about a node that has not reported.
76573
+ */
76574
+ read(nodeId, sinceMs) {
76575
+ this.sweepIdle();
76576
+ const partition = this.partitions.get(nodeId);
76577
+ if (partition === void 0) return {
76578
+ nodeId,
76579
+ samples: [],
76580
+ retainedSamples: 0,
76581
+ oldestAtMs: null,
76582
+ capacity: this.samplesPerNode
76583
+ };
76584
+ return {
76585
+ nodeId,
76586
+ samples: partition.list(sinceMs),
76587
+ retainedSamples: partition.size(),
76588
+ oldestAtMs: partition.oldestAtMs(),
76589
+ capacity: this.samplesPerNode
76590
+ };
76591
+ }
76592
+ /** Node ids with a live partition. Observability for the fleet bound. */
76593
+ nodeIds() {
76594
+ return [...this.partitions.keys()];
76595
+ }
76596
+ /** Process rows retained across every partition — the number that IS memory. */
76597
+ rowCount() {
76598
+ return this.totalRows;
76599
+ }
76600
+ /** Samples retained across every partition. */
76601
+ sampleCount() {
76602
+ let total = 0;
76603
+ for (const partition of this.partitions.values()) total += partition.size();
76604
+ return total;
76605
+ }
76606
+ /**
76607
+ * Drop partitions whose newest sample is older than the retention window.
76608
+ *
76609
+ * Lazy, on write and on read — never a timer. A timer would be a new
76610
+ * periodic cost in a subsystem whose entire premise is that it adds none,
76611
+ * and a ring that nobody writes to and nobody reads is not growing either.
76612
+ */
76613
+ sweepIdle() {
76614
+ const cutoff = this.now() - this.idleEvictionMs;
76615
+ for (const [nodeId, partition] of [...this.partitions.entries()]) {
76616
+ const lastWrite = partition.lastWriteAtMs();
76617
+ if (lastWrite !== null && lastWrite > cutoff) continue;
76618
+ this.dropPartition(nodeId);
76619
+ }
76620
+ }
76621
+ /**
76622
+ * Bring the fleet back under the row budget by dropping the OLDEST sample of
76623
+ * the HEAVIEST partition, repeatedly.
76624
+ *
76625
+ * Terminates: every iteration removes one sample from a non-empty partition,
76626
+ * and the population of samples is finite and strictly decreasing.
76627
+ */
76628
+ enforceRowBudget() {
76629
+ while (this.totalRows > this.maxTotalProcessRows) {
76630
+ const victim = this.heaviestPartition();
76631
+ if (victim === null) return;
76632
+ const [nodeId, partition] = victim;
76633
+ this.totalRows -= partition.dropOldest();
76634
+ if (partition.size() === 0) this.dropPartition(nodeId);
76635
+ }
76636
+ }
76637
+ heaviestPartition() {
76638
+ let best = null;
76639
+ for (const entry of this.partitions.entries()) {
76640
+ if (entry[1].size() === 0) continue;
76641
+ if (best === null || entry[1].rowCount() > best[1].rowCount()) best = entry;
76642
+ }
76643
+ return best;
76644
+ }
76645
+ /** Drop `count` whole partitions, least-recently-written first. */
76646
+ evictPartitions(count, protectedNodeId) {
76647
+ const order = [...this.partitions.keys()].filter((nodeId) => nodeId !== protectedNodeId).toSorted((a, b) => (this.lastWriteSeq.get(a) ?? 0) - (this.lastWriteSeq.get(b) ?? 0));
76648
+ let remaining = count;
76649
+ for (const nodeId of order) {
76650
+ if (remaining <= 0) return;
76651
+ this.dropPartition(nodeId);
76652
+ remaining -= 1;
76653
+ }
76654
+ }
76655
+ /** Remove a partition and everything that indexes it. */
76656
+ dropPartition(nodeId) {
76657
+ const partition = this.partitions.get(nodeId);
76658
+ if (partition === void 0) return;
76659
+ this.totalRows -= partition.rowCount();
76660
+ this.partitions.delete(nodeId);
76661
+ this.lastWriteSeq.delete(nodeId);
76662
+ }
76663
+ };
76664
+ var CONTAINER_MEMORY_COLLECTION = "metrics:node-memory-samples";
76665
+ var CONTAINER_MEMORY_MAX_TOTAL_ROWS = 2880;
76666
+ var CONTAINER_MEMORY_MAX_ROWS = 5e4;
76667
+ var CONTAINER_MEMORY_COLUMNS = [
76668
+ {
76669
+ name: "id",
76670
+ type: "INTEGER",
76671
+ primaryKey: true,
76672
+ notNull: true
76673
+ },
76674
+ {
76675
+ name: "nodeId",
76676
+ type: "TEXT",
76677
+ notNull: true
76678
+ },
76679
+ /** Shared with the process rows of the same tick — that is the whole point. */
76680
+ {
76681
+ name: "atMs",
76682
+ type: "INTEGER",
76683
+ notNull: true
76684
+ },
76685
+ {
76686
+ name: "source",
76687
+ type: "TEXT",
76688
+ notNull: true
76689
+ },
76690
+ /**
76691
+ * Whole MiB, like `rssMib` on the process row and for the same reason: a
76692
+ * byte-exact residency on a value in the tens of gigabytes is precision no
76693
+ * chart can draw, and it costs bytes on every row forever.
76694
+ */
76695
+ {
76696
+ name: "currentMib",
76697
+ type: "INTEGER",
76698
+ notNull: true
76699
+ },
76700
+ /** `NULL` = NO LIMIT. Never a sentinel. */
76701
+ {
76702
+ name: "limitMib",
76703
+ type: "INTEGER"
76704
+ },
76705
+ /** `NULL` = UNKNOWN on all four. Never zero. */
76706
+ {
76707
+ name: "anonMib",
76708
+ type: "INTEGER"
76709
+ },
76710
+ {
76711
+ name: "fileMib",
76712
+ type: "INTEGER"
76713
+ },
76714
+ {
76715
+ name: "shmemMib",
76716
+ type: "INTEGER"
76717
+ },
76718
+ {
76719
+ name: "slabMib",
76720
+ type: "INTEGER"
76721
+ },
76722
+ /** Host-wide i915 GEM bytes. `NULL` wherever debugfs is not mounted. */
76723
+ {
76724
+ name: "gpuShmemMib",
76725
+ type: "INTEGER"
76726
+ }
76727
+ ];
76728
+ var CONTAINER_MEMORY_INDEXES = [
76729
+ /** The one index, and it serves both the read and the prune — see the
76730
+ * process table's `idx_load_samples_node_at` for the full reasoning. */
76731
+ {
76732
+ name: "idx_node_memory_node_at",
76733
+ columns: ["nodeId", "atMs"]
76734
+ }
76735
+ ];
76736
+ var BYTES_PER_MIB$1 = 1048576;
76737
+ function toMib(value) {
76738
+ return value === null ? null : Math.round(value / BYTES_PER_MIB$1);
76739
+ }
76740
+ function fromMib(value) {
76741
+ return value === null ? null : value * BYTES_PER_MIB$1;
76742
+ }
76743
+ function snapshotToRow(nodeId, atMs, snapshot) {
76744
+ return {
76745
+ nodeId,
76746
+ atMs,
76747
+ source: snapshot.source,
76748
+ currentMib: Math.round(snapshot.currentBytes / BYTES_PER_MIB$1),
76749
+ limitMib: toMib(snapshot.limitBytes),
76750
+ anonMib: toMib(snapshot.anonBytes),
76751
+ fileMib: toMib(snapshot.fileBytes),
76752
+ shmemMib: toMib(snapshot.shmemBytes),
76753
+ slabMib: toMib(snapshot.slabBytes),
76754
+ gpuShmemMib: toMib(snapshot.gpuShmemBytes)
76755
+ };
76756
+ }
76757
+ function rowToPoint(row) {
76758
+ if (row.source !== "cgroup-v2" && row.source !== "cgroup-v1") return null;
76759
+ return {
76760
+ atMs: row.atMs,
76761
+ source: row.source,
76762
+ currentBytes: row.currentMib * BYTES_PER_MIB$1,
76763
+ limitBytes: fromMib(row.limitMib),
76764
+ anonBytes: fromMib(row.anonMib),
76765
+ fileBytes: fromMib(row.fileMib),
76766
+ shmemBytes: fromMib(row.shmemMib),
76767
+ slabBytes: fromMib(row.slabMib),
76768
+ gpuShmemBytes: fromMib(row.gpuShmemMib)
76769
+ };
76770
+ }
76771
+ function recordToMemoryRow(data) {
76772
+ const nodeId = data["nodeId"];
76773
+ const source = data["source"];
76774
+ if (typeof nodeId !== "string" || typeof source !== "string") return null;
76775
+ const atMs = Number(data["atMs"]);
76776
+ const currentMib = Number(data["currentMib"]);
76777
+ if (!Number.isFinite(atMs) || !Number.isFinite(currentMib)) return null;
76778
+ const optional = (raw) => {
76779
+ if (raw === null || raw === void 0) return null;
76780
+ const n = Number(raw);
76781
+ return Number.isFinite(n) ? n : null;
76782
+ };
76783
+ return {
76784
+ nodeId,
76785
+ atMs,
76786
+ source,
76787
+ currentMib,
76788
+ limitMib: optional(data["limitMib"]),
76789
+ anonMib: optional(data["anonMib"]),
76790
+ fileMib: optional(data["fileMib"]),
76791
+ shmemMib: optional(data["shmemMib"]),
76792
+ slabMib: optional(data["slabMib"]),
76793
+ gpuShmemMib: optional(data["gpuShmemMib"])
76794
+ };
76795
+ }
76796
+ var ContainerMemoryRing = class {
76797
+ ring;
76798
+ constructor(options = {}) {
76799
+ this.ring = new NodeLoadRing({
76800
+ samplesPerNode: options.samplesPerNode ?? 180,
76801
+ maxTotalProcessRows: CONTAINER_MEMORY_MAX_TOTAL_ROWS,
76802
+ ...options.now !== void 0 ? { now: options.now } : {}
76803
+ });
76804
+ }
76805
+ /** Retain one reading. `false` = a timestamp this node already delivered. */
76806
+ record(nodeId, atMs, snapshot) {
76807
+ return this.ring.record(nodeId, atMs, [snapshot]);
76808
+ }
76809
+ /**
76810
+ * Read one node's retained readings, oldest-first. `sinceMs` is EXCLUSIVE,
76811
+ * matching every other reader in this subsystem.
76812
+ *
76813
+ * A node nobody has recorded answers EMPTY — which is what "this node has no
76814
+ * cgroup" looks like, and it must stay distinguishable from a zero.
76815
+ */
76816
+ read(nodeId, sinceMs) {
76817
+ const series = this.ring.read(nodeId, sinceMs);
76818
+ const out = [];
76819
+ for (const sample of series.samples) {
76820
+ const snapshot = sample.processes[0];
76821
+ if (snapshot === void 0) continue;
76822
+ out.push({
76823
+ ...snapshot,
76824
+ atMs: sample.atMs
76825
+ });
76826
+ }
76827
+ return out;
76828
+ }
76829
+ };
76830
+ function reduceContainerMemory(points, bucketMs, origin) {
76831
+ if (bucketMs <= 0 || points.length === 0) return points;
76832
+ const buckets = /* @__PURE__ */ new Map();
76833
+ for (const point of points) {
76834
+ const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
76835
+ const held = buckets.get(start);
76836
+ if (held === void 0 || point.currentBytes > held.currentBytes) buckets.set(start, {
76837
+ ...point,
76838
+ atMs: start
76839
+ });
76840
+ }
76841
+ return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
76842
+ }
76843
+ function mergeContainerMemory(cold, hot) {
76844
+ const byAt = /* @__PURE__ */ new Map();
76845
+ for (const point of cold) byAt.set(point.atMs, point);
76846
+ for (const point of hot) byAt.set(point.atMs, point);
76847
+ return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
76848
+ }
76849
+ var ContainerMemoryStore = class {
76850
+ declared = false;
76851
+ store;
76852
+ logger;
76853
+ nowFn;
76854
+ prunePageRows;
76855
+ constructor(deps) {
76856
+ this.store = deps.store;
76857
+ this.logger = deps.logger;
76858
+ this.nowFn = deps.now ?? (() => Date.now());
76859
+ this.prunePageRows = deps.prunePageRows ?? 2e4;
76860
+ }
76861
+ /** Idempotently declare the collection. `false` when the store refused. */
76862
+ async declare() {
76863
+ if (this.declared) return true;
76864
+ try {
76865
+ await this.store.declareCollection.mutate({
76866
+ collection: CONTAINER_MEMORY_COLLECTION,
76867
+ columns: [...CONTAINER_MEMORY_COLUMNS],
76868
+ indexes: CONTAINER_MEMORY_INDEXES.map((i) => ({
76869
+ name: i.name,
76870
+ columns: [...i.columns]
76871
+ }))
76872
+ });
76873
+ this.declared = true;
76874
+ return true;
76875
+ } catch (err) {
76876
+ this.logger.warn("container memory declareCollection failed \u2014 nothing will be retained on disk", { meta: {
76877
+ collection: CONTAINER_MEMORY_COLLECTION,
76878
+ error: require_dist10.errMsg(err)
76879
+ } });
76880
+ return false;
76881
+ }
76882
+ }
76883
+ /** Append ONE reading. */
76884
+ async append(nodeId, atMs, snapshot) {
76885
+ if (!await this.declare()) return 0;
76886
+ const record = { ...snapshotToRow(nodeId, atMs, snapshot) };
76887
+ try {
76888
+ const { inserted } = await this.store.insertMany.mutate({
76889
+ collection: CONTAINER_MEMORY_COLLECTION,
76890
+ records: [{ data: record }]
76891
+ });
76892
+ return inserted;
76893
+ } catch (err) {
76894
+ this.logger.warn("container memory reading not retained \u2014 this interval will be missing", { meta: {
76895
+ nodeId,
76896
+ atMs,
76897
+ error: require_dist10.errMsg(err)
76898
+ } });
76899
+ return 0;
76900
+ }
76901
+ }
76902
+ /** Read one node's cold readings, oldest-first. `sinceMs` is EXCLUSIVE. */
76903
+ async read(nodeId, sinceMs, limitRows) {
76904
+ if (!await this.declare()) return [];
76905
+ try {
76906
+ const records = await this.store.query.query({
76907
+ collection: CONTAINER_MEMORY_COLLECTION,
76908
+ filter: {
76909
+ where: { nodeId },
76910
+ whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
76911
+ orderBy: {
76912
+ field: "atMs",
76913
+ direction: "asc"
76914
+ },
76915
+ limit: limitRows
76916
+ }
76917
+ });
76918
+ const points = [];
76919
+ for (const record of records) {
76920
+ const row = recordToMemoryRow(record.data);
76921
+ if (row === null) continue;
76922
+ const point = rowToPoint(row);
76923
+ if (point !== null) points.push(point);
76924
+ }
76925
+ return points;
76926
+ } catch (err) {
76927
+ this.logger.warn("container memory cold read failed \u2014 answering from the hot window only", { meta: {
76928
+ nodeId,
76929
+ sinceMs,
76930
+ error: require_dist10.errMsg(err)
76931
+ } });
76932
+ return [];
76933
+ }
76934
+ }
76935
+ /**
76936
+ * Enforce BOTH bounds, oldest-first, through a bounded page each.
76937
+ *
76938
+ * Not rate-limited here: it is driven by the process store's own prune
76939
+ * cadence, so it runs at most once a minute by construction and a second
76940
+ * timer would be a second thing to get wrong.
76941
+ */
76942
+ async prune(nodeIds, retention) {
76943
+ if (!await this.declare()) return null;
76944
+ const ageCutoff = this.nowFn() - retention.retentionHours * 36e5;
76945
+ let deletedByAge = 0;
76946
+ let rowsExamined = 0;
76947
+ for (const nodeId of nodeIds) {
76948
+ const outcome = await this.deleteOldestPage(nodeId, ageCutoff, this.prunePageRows);
76949
+ deletedByAge += outcome.deleted;
76950
+ rowsExamined += outcome.examined;
76951
+ }
76952
+ const total = await this.count();
76953
+ const excess = total === null ? 0 : total - CONTAINER_MEMORY_MAX_ROWS;
76954
+ let deletedByCap = 0;
76955
+ if (excess > 0) {
76956
+ let remaining = Math.min(excess, this.prunePageRows);
76957
+ for (const nodeId of nodeIds) {
76958
+ if (remaining <= 0) break;
76959
+ const outcome = await this.deleteOldestPage(nodeId, null, remaining);
76960
+ deletedByCap += outcome.deleted;
76961
+ remaining -= outcome.deleted;
76962
+ rowsExamined += outcome.examined;
76963
+ }
76964
+ this.logger.warn("container memory ROW CAP bit \u2014 evicting the oldest readings", { meta: {
76965
+ collection: CONTAINER_MEMORY_COLLECTION,
76966
+ rows: total,
76967
+ cap: CONTAINER_MEMORY_MAX_ROWS,
76968
+ over: excess,
76969
+ deleted: deletedByCap,
76970
+ hint: "lower the retention or the sampling cadence \u2014 the cap is the guarantee, not the intention"
76971
+ } });
76972
+ }
76973
+ return {
76974
+ deletedByAge,
76975
+ deletedByCap,
76976
+ rowsExamined,
76977
+ capBit: excess > 0
76978
+ };
76979
+ }
76980
+ /** Total rows, or `null` when the store could not answer. */
76981
+ async count() {
76982
+ if (!await this.declare()) return null;
76983
+ try {
76984
+ return await this.store.count.query({ collection: CONTAINER_MEMORY_COLLECTION });
76985
+ } catch (err) {
76986
+ this.logger.warn("container memory count failed \u2014 the row cap is not enforced this pass", { meta: { error: require_dist10.errMsg(err) } });
76987
+ return null;
76988
+ }
76989
+ }
76990
+ /**
76991
+ * Delete this node's oldest rows, at most one page's worth.
76992
+ *
76993
+ * `cutoff === null` means "the oldest rows, whatever their age" — the
76994
+ * row-cap path, which is bounded by `limitRows` instead. Either way the page
76995
+ * is READ FIRST, ordered by the one index, and its last `atMs` becomes the
76996
+ * effective cutoff for a single bounded delete. An unbounded `deleteWhere`
76997
+ * would be one statement and one stalling transaction the first time a
76998
+ * retention is lowered.
76999
+ */
77000
+ async deleteOldestPage(nodeId, cutoff, limitRows) {
77001
+ if (limitRows <= 0) return {
77002
+ deleted: 0,
77003
+ examined: 0
77004
+ };
77005
+ try {
77006
+ const page = await this.store.query.query({
77007
+ collection: CONTAINER_MEMORY_COLLECTION,
77008
+ filter: {
77009
+ where: { nodeId },
77010
+ ...cutoff === null ? {} : { whereBetween: { atMs: [0, cutoff] } },
77011
+ orderBy: {
77012
+ field: "atMs",
77013
+ direction: "asc"
77014
+ },
77015
+ limit: Math.min(limitRows, this.prunePageRows)
77016
+ },
77017
+ columns: ["atMs"]
77018
+ });
77019
+ if (page.length === 0) return {
77020
+ deleted: 0,
77021
+ examined: 0
77022
+ };
77023
+ const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
77024
+ if (!Number.isFinite(effectiveCutoff)) return {
77025
+ deleted: 0,
77026
+ examined: page.length
77027
+ };
77028
+ const { deleted } = await this.store.deleteWhere.mutate({
77029
+ collection: CONTAINER_MEMORY_COLLECTION,
77030
+ filter: {
77031
+ where: { nodeId },
77032
+ whereBetween: { atMs: [0, effectiveCutoff] }
77033
+ }
77034
+ });
77035
+ return {
77036
+ deleted,
77037
+ examined: page.length
77038
+ };
77039
+ } catch (err) {
77040
+ this.logger.warn("container memory prune failed \u2014 the table keeps growing this pass", { meta: {
77041
+ nodeId,
77042
+ cutoff,
77043
+ error: require_dist10.errMsg(err)
77044
+ } });
77045
+ return {
77046
+ deleted: 0,
77047
+ examined: 0
77048
+ };
77049
+ }
77050
+ }
77051
+ };
76133
77052
  var IS_DARWIN = node_os.platform() === "darwin";
76134
77053
  var IS_LINUX = node_os.platform() === "linux";
76135
77054
  var NativeMetricsProvider = class {
@@ -76594,247 +77513,6 @@ var require_native_metrics_addon = __commonJS({
76594
77513
  });
76595
77514
  });
76596
77515
  }
76597
- var LoadPartition = class {
76598
- capacity;
76599
- slots;
76600
- head = 0;
76601
- count = 0;
76602
- rows = 0;
76603
- /** Newest retained sample's timestamp — the monotonic gate for `push`. */
76604
- newestAtMs = null;
76605
- constructor(capacity) {
76606
- this.capacity = capacity;
76607
- this.slots = Array.from({ length: capacity });
76608
- }
76609
- /**
76610
- * Accept a sample.
76611
- *
76612
- * A sample at or before the newest one already held is REFUSED. The bus drops
76613
- * a node's own broadcast echo, but a cross-node redelivery or a replayed
76614
- * subscription must not be able to double a point — and idempotence here is
76615
- * what lets every reader above treat the series as a set.
76616
- *
76617
- * `accepted` is reported separately from `rowDelta` on purpose: a full ring
76618
- * that evicts a sample of the same size has a delta of zero and is not a
76619
- * refusal, and conflating the two would silently stop advancing the write
76620
- * ordinal on a steady-state cluster.
76621
- */
76622
- push(sample) {
76623
- if (this.newestAtMs !== null && sample.atMs <= this.newestAtMs) return {
76624
- accepted: false,
76625
- rowDelta: 0
76626
- };
76627
- const evicted = this.count === this.capacity ? this.slots[this.head]?.processes.length ?? 0 : 0;
76628
- this.slots[this.head] = sample;
76629
- this.head = (this.head + 1) % this.capacity;
76630
- if (this.count < this.capacity) this.count++;
76631
- this.newestAtMs = sample.atMs;
76632
- const rowDelta = sample.processes.length - evicted;
76633
- this.rows += rowDelta;
76634
- return {
76635
- accepted: true,
76636
- rowDelta
76637
- };
76638
- }
76639
- /** Drop the oldest sample. Returns the rows reclaimed (0 when empty). */
76640
- dropOldest() {
76641
- if (this.count === 0) return 0;
76642
- const index = (this.head - this.count + this.capacity) % this.capacity;
76643
- const victim = this.slots[index];
76644
- this.slots[index] = void 0;
76645
- this.count--;
76646
- const reclaimed = victim?.processes.length ?? 0;
76647
- this.rows -= reclaimed;
76648
- if (this.count === 0) this.newestAtMs = null;
76649
- return reclaimed;
76650
- }
76651
- /** Oldest-first, optionally only what is strictly newer than `sinceMs`. */
76652
- list(sinceMs) {
76653
- const out = [];
76654
- for (let i = 0; i < this.count; i++) {
76655
- const index = (this.head - this.count + i + this.capacity) % this.capacity;
76656
- const sample = this.slots[index];
76657
- if (sample === void 0) continue;
76658
- if (sinceMs !== void 0 && sample.atMs <= sinceMs) continue;
76659
- out.push(sample);
76660
- }
76661
- return out;
76662
- }
76663
- size() {
76664
- return this.count;
76665
- }
76666
- rowCount() {
76667
- return this.rows;
76668
- }
76669
- oldestAtMs() {
76670
- if (this.count === 0) return null;
76671
- const index = (this.head - this.count + this.capacity) % this.capacity;
76672
- return this.slots[index]?.atMs ?? null;
76673
- }
76674
- lastWriteAtMs() {
76675
- return this.newestAtMs;
76676
- }
76677
- };
76678
- var NodeLoadRing = class {
76679
- partitions = /* @__PURE__ */ new Map();
76680
- /**
76681
- * Node id → write ordinal of its last accepted sample. A monotonic counter,
76682
- * not a clock: partition eviction must follow the order writes actually
76683
- * happened, and node clocks disagree.
76684
- */
76685
- lastWriteSeq = /* @__PURE__ */ new Map();
76686
- writeSeq = 0;
76687
- totalRows = 0;
76688
- samplesPerNode;
76689
- maxTotalProcessRows;
76690
- maxNodes;
76691
- idleEvictionMs;
76692
- now;
76693
- constructor(options = {}) {
76694
- this.samplesPerNode = options.samplesPerNode ?? 180;
76695
- this.maxTotalProcessRows = options.maxTotalProcessRows ?? 32e3;
76696
- this.maxNodes = options.maxNodes ?? 16;
76697
- this.idleEvictionMs = options.idleEvictionMs ?? 36e5;
76698
- this.now = options.now ?? Date.now;
76699
- }
76700
- /**
76701
- * Retain one snapshot. `processes` is stored by reference — the payload is
76702
- * already an immutable arrival off the bus, and copying it would double the
76703
- * measured cost for nothing.
76704
- *
76705
- * Returns whether the sample was ACCEPTED — that is, whether it was new
76706
- * rather than a replay of a timestamp this node has already delivered.
76707
- *
76708
- * The return value is not diagnostics. It is the idempotence gate the
76709
- * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
76710
- * appended to the table, which is what lets that table carry an `INTEGER`
76711
- * rowid key instead of a composite unique index over two million rows. The
76712
- * gate is one monotonic comparison in memory; the index it replaces was
76713
- * measured at ~40 bytes per row.
76714
- */
76715
- record(nodeId, atMs, processes) {
76716
- if (nodeId.length === 0) return false;
76717
- this.sweepIdle();
76718
- let partition = this.partitions.get(nodeId);
76719
- if (partition === void 0) {
76720
- partition = new LoadPartition(this.samplesPerNode);
76721
- this.partitions.set(nodeId, partition);
76722
- }
76723
- const outcome = partition.push({
76724
- atMs,
76725
- processes
76726
- });
76727
- if (!outcome.accepted) {
76728
- if (partition.size() === 0) this.partitions.delete(nodeId);
76729
- return false;
76730
- }
76731
- this.totalRows += outcome.rowDelta;
76732
- this.lastWriteSeq.set(nodeId, ++this.writeSeq);
76733
- if (this.partitions.size > this.maxNodes) this.evictPartitions(this.partitions.size - this.maxNodes, nodeId);
76734
- this.enforceRowBudget();
76735
- return true;
76736
- }
76737
- /**
76738
- * Read one node's retained series, oldest-first.
76739
- *
76740
- * `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
76741
- * holds and gets back only what it is missing. That is the whole contract
76742
- * that lets the admin UI seed from here and then continue live without
76743
- * doubling a point it already drew.
76744
- *
76745
- * A node nobody has recorded answers with an empty series, not an error —
76746
- * unknown is the truth about a node that has not reported.
76747
- */
76748
- read(nodeId, sinceMs) {
76749
- this.sweepIdle();
76750
- const partition = this.partitions.get(nodeId);
76751
- if (partition === void 0) return {
76752
- nodeId,
76753
- samples: [],
76754
- retainedSamples: 0,
76755
- oldestAtMs: null,
76756
- capacity: this.samplesPerNode
76757
- };
76758
- return {
76759
- nodeId,
76760
- samples: partition.list(sinceMs),
76761
- retainedSamples: partition.size(),
76762
- oldestAtMs: partition.oldestAtMs(),
76763
- capacity: this.samplesPerNode
76764
- };
76765
- }
76766
- /** Node ids with a live partition. Observability for the fleet bound. */
76767
- nodeIds() {
76768
- return [...this.partitions.keys()];
76769
- }
76770
- /** Process rows retained across every partition — the number that IS memory. */
76771
- rowCount() {
76772
- return this.totalRows;
76773
- }
76774
- /** Samples retained across every partition. */
76775
- sampleCount() {
76776
- let total = 0;
76777
- for (const partition of this.partitions.values()) total += partition.size();
76778
- return total;
76779
- }
76780
- /**
76781
- * Drop partitions whose newest sample is older than the retention window.
76782
- *
76783
- * Lazy, on write and on read — never a timer. A timer would be a new
76784
- * periodic cost in a subsystem whose entire premise is that it adds none,
76785
- * and a ring that nobody writes to and nobody reads is not growing either.
76786
- */
76787
- sweepIdle() {
76788
- const cutoff = this.now() - this.idleEvictionMs;
76789
- for (const [nodeId, partition] of [...this.partitions.entries()]) {
76790
- const lastWrite = partition.lastWriteAtMs();
76791
- if (lastWrite !== null && lastWrite > cutoff) continue;
76792
- this.dropPartition(nodeId);
76793
- }
76794
- }
76795
- /**
76796
- * Bring the fleet back under the row budget by dropping the OLDEST sample of
76797
- * the HEAVIEST partition, repeatedly.
76798
- *
76799
- * Terminates: every iteration removes one sample from a non-empty partition,
76800
- * and the population of samples is finite and strictly decreasing.
76801
- */
76802
- enforceRowBudget() {
76803
- while (this.totalRows > this.maxTotalProcessRows) {
76804
- const victim = this.heaviestPartition();
76805
- if (victim === null) return;
76806
- const [nodeId, partition] = victim;
76807
- this.totalRows -= partition.dropOldest();
76808
- if (partition.size() === 0) this.dropPartition(nodeId);
76809
- }
76810
- }
76811
- heaviestPartition() {
76812
- let best = null;
76813
- for (const entry of this.partitions.entries()) {
76814
- if (entry[1].size() === 0) continue;
76815
- if (best === null || entry[1].rowCount() > best[1].rowCount()) best = entry;
76816
- }
76817
- return best;
76818
- }
76819
- /** Drop `count` whole partitions, least-recently-written first. */
76820
- evictPartitions(count, protectedNodeId) {
76821
- const order = [...this.partitions.keys()].filter((nodeId) => nodeId !== protectedNodeId).toSorted((a, b) => (this.lastWriteSeq.get(a) ?? 0) - (this.lastWriteSeq.get(b) ?? 0));
76822
- let remaining = count;
76823
- for (const nodeId of order) {
76824
- if (remaining <= 0) return;
76825
- this.dropPartition(nodeId);
76826
- remaining -= 1;
76827
- }
76828
- }
76829
- /** Remove a partition and everything that indexes it. */
76830
- dropPartition(nodeId) {
76831
- const partition = this.partitions.get(nodeId);
76832
- if (partition === void 0) return;
76833
- this.totalRows -= partition.rowCount();
76834
- this.partitions.delete(nodeId);
76835
- this.lastWriteSeq.delete(nodeId);
76836
- }
76837
- };
76838
77516
  var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
76839
77517
  var DEFAULT_MAX_ROWS = 5e5;
76840
77518
  var LOAD_SERIES_COLUMNS = [
@@ -77545,6 +78223,20 @@ var require_native_metrics_addon = __commonJS({
77545
78223
  * per sample, to reach that very same table.
77546
78224
  */
77547
78225
  loadStore = null;
78226
+ /**
78227
+ * The container's OWN memory, sampled on the process-snapshot tick.
78228
+ *
78229
+ * It rides that tick rather than a timer of its own so a memory reading and
78230
+ * the process rows it must be compared against carry the SAME timestamp —
78231
+ * the whole question is a subtraction, and two clocks make it an estimate.
78232
+ * Constructed in `onInitialize`, where the logger exists; it resolves the
78233
+ * cgroup once and latches.
78234
+ */
78235
+ memoryProbe = null;
78236
+ /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
78237
+ memoryRing = new ContainerMemoryRing();
78238
+ /** Cold tier, HUB only — same reasoning as `loadStore`. */
78239
+ memoryStore = null;
77548
78240
  /** The resolved knobs. Re-resolved on every settings write. */
77549
78241
  loadConfig = {
77550
78242
  cadenceSec: 10,
@@ -77617,13 +78309,20 @@ var require_native_metrics_addon = __commonJS({
77617
78309
  dumpHeapSnapshot: (params) => this.dumpHeapSnapshot(params)
77618
78310
  };
77619
78311
  this.applyLoadSeriesConfig();
77620
- if (this.isHub) this.loadStore = new LoadSeriesStore({
77621
- store: this.ctx.api.settingsStore,
77622
- logger: this.ctx.logger.child("LoadSeries")
77623
- });
78312
+ if (this.isHub) {
78313
+ this.loadStore = new LoadSeriesStore({
78314
+ store: this.ctx.api.settingsStore,
78315
+ logger: this.ctx.logger.child("LoadSeries")
78316
+ });
78317
+ this.memoryStore = new ContainerMemoryStore({
78318
+ store: this.ctx.api.settingsStore,
78319
+ logger: this.ctx.logger.child("ContainerMemory")
78320
+ });
78321
+ }
78322
+ this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
77624
78323
  this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
77625
78324
  this.startProcessSnapshotTimer();
77626
- this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: require_dist10.EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes)));
78325
+ this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: require_dist10.EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes, event.data.containerMemory)));
77627
78326
  return [{
77628
78327
  capability: require_dist10.metricsProviderCapability,
77629
78328
  provider: composed
@@ -77641,19 +78340,31 @@ var require_native_metrics_addon = __commonJS({
77641
78340
  * The durable append is fire-and-forget: a storage stall must cost a gap in
77642
78341
  * the cold window, never a blocked event-bus handler. Every failure logs.
77643
78342
  */
77644
- retainSnapshot(nodeId, atMs, processes) {
78343
+ retainSnapshot(nodeId, atMs, processes, containerMemory) {
77645
78344
  const retained = processes.map((p) => NativeMetricsAddon2.toRetained(p));
77646
78345
  if (!this.loadRing.record(nodeId, atMs, retained)) return;
77647
78346
  this.observedRowsByNode.set(nodeId, retained.length);
78347
+ if (containerMemory !== null) this.memoryRing.record(nodeId, atMs, containerMemory);
77648
78348
  const store = this.loadStore;
77649
78349
  if (store === null) return;
77650
- store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).catch((err) => {
78350
+ store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).then(async (outcome) => {
78351
+ if (outcome === null) return;
78352
+ await this.memoryStore?.prune([...this.observedRowsByNode.keys()], this.loadConfig);
78353
+ }).catch((err) => {
77651
78354
  this.ctx.logger.warn("durable load series write failed", { meta: {
77652
78355
  nodeId,
77653
78356
  atMs,
77654
78357
  error: err instanceof Error ? err.message : String(err)
77655
78358
  } });
77656
78359
  });
78360
+ if (containerMemory === null) return;
78361
+ this.memoryStore?.append(nodeId, atMs, containerMemory).catch((err) => {
78362
+ this.ctx.logger.warn("durable container memory write failed", { meta: {
78363
+ nodeId,
78364
+ atMs,
78365
+ error: err instanceof Error ? err.message : String(err)
78366
+ } });
78367
+ });
77657
78368
  }
77658
78369
  /**
77659
78370
  * The ONE reader, over both tiers.
@@ -77682,6 +78393,14 @@ var require_native_metrics_addon = __commonJS({
77682
78393
  return {
77683
78394
  nodeId: params.forNodeId,
77684
78395
  series: merged.series,
78396
+ containerMemory: await this.readContainerMemory({
78397
+ forNodeId: params.forNodeId,
78398
+ sinceMs,
78399
+ ...params.sinceMs !== void 0 ? { exclusiveSinceMs: params.sinceMs } : {},
78400
+ bucketMs: merged.bucketMs,
78401
+ cadenceMs,
78402
+ origin: merged.oldestAtMs
78403
+ }),
77685
78404
  bucketMs: merged.bucketMs,
77686
78405
  retainedSamples: merged.retainedSamples,
77687
78406
  oldestAtMs: merged.oldestAtMs,
@@ -77690,6 +78409,24 @@ var require_native_metrics_addon = __commonJS({
77690
78409
  };
77691
78410
  }
77692
78411
  /**
78412
+ * The container's memory over the same window, on the same x-axis.
78413
+ *
78414
+ * Both tiers, merged and deduped on `atMs` like the process series, then
78415
+ * reduced against the SAME `bucketMs` and the SAME origin — otherwise the
78416
+ * two series in one answer would sit on two different axes and the
78417
+ * subtraction they exist for would be an eyeball.
78418
+ *
78419
+ * An EMPTY answer means the node has no cgroup, or nothing has been retained
78420
+ * for it yet. It never means zero.
78421
+ */
78422
+ async readContainerMemory(params) {
78423
+ const hot = this.memoryRing.read(params.forNodeId, params.exclusiveSinceMs);
78424
+ const store = this.memoryStore;
78425
+ const merged = mergeContainerMemory(store === null ? [] : await store.read(params.forNodeId, params.sinceMs, CONTAINER_MEMORY_MAX_ROWS), hot);
78426
+ if (params.bucketMs <= params.cadenceMs) return merged;
78427
+ return reduceContainerMemory(merged, params.bucketMs, params.origin ?? 0);
78428
+ }
78429
+ /**
77693
78430
  * Re-resolve the knobs and restate the fixed cadence.
77694
78431
  *
77695
78432
  * A REFUSED value (out of 5-60 s, or out of 1-72 h) leaves the previous
@@ -77806,6 +78543,7 @@ var require_native_metrics_addon = __commonJS({
77806
78543
  const timestamp = Date.now();
77807
78544
  try {
77808
78545
  const processes = await this.listNodeProcesses();
78546
+ const containerMemory = this.memoryProbe?.sample() ?? null;
77809
78547
  eventBus.emit(require_dist10.createEvent(require_dist10.EventCategory.MetricsNodeProcessesSnapshot, {
77810
78548
  type: "node",
77811
78549
  id: nodeId,
@@ -77813,7 +78551,8 @@ var require_native_metrics_addon = __commonJS({
77813
78551
  }, {
77814
78552
  nodeId,
77815
78553
  processes,
77816
- timestamp
78554
+ timestamp,
78555
+ containerMemory
77817
78556
  }));
77818
78557
  } catch (err) {
77819
78558
  this.ctx.logger.warn("process snapshot skipped \u2014 this interval will be missing", { meta: {
@@ -78079,14 +78818,14 @@ var require_filesystem_storage_addon = __commonJS({
78079
78818
  [Symbol.toStringTag]: { value: "Module" }
78080
78819
  });
78081
78820
  var require_chunk = require_chunk_Cek0wNdY();
78082
- var require_dist10 = require_dist_CxgTulEG();
78821
+ var require_dist10 = require_dist_BPlfW_CG();
78083
78822
  var node_crypto = __require("crypto");
78084
78823
  var node_fs_promises = __require("fs/promises");
78085
78824
  var node_path = __require("path");
78086
78825
  node_path = require_chunk.__toESM(node_path);
78087
- var node_os = __require("os");
78088
78826
  var node_fs = __require("fs");
78089
78827
  node_fs = require_chunk.__toESM(node_fs);
78828
+ var node_os = __require("os");
78090
78829
  function isWithinAllowedRoots(candidate, allowedRoots) {
78091
78830
  const norm = (0, node_path.resolve)(candidate);
78092
78831
  return allowedRoots.some((root) => {
@@ -79195,8 +79934,8 @@ var require_sqlite_settings_addon = __commonJS({
79195
79934
  [Symbol.toStringTag]: { value: "Module" }
79196
79935
  });
79197
79936
  var require_chunk = require_chunk_Cek0wNdY();
79198
- var require_dist10 = require_dist_CxgTulEG();
79199
- var require_retired_settings_keys = require_retired_settings_keys_BbjWjol0();
79937
+ var require_dist10 = require_dist_BPlfW_CG();
79938
+ var require_retired_settings_keys = require_retired_settings_keys_DBY6ebwV();
79200
79939
  var node_crypto = __require("crypto");
79201
79940
  var node_fs = __require("fs");
79202
79941
  var node_module = __require("module");
@@ -81475,7 +82214,7 @@ var require_storage_orchestrator_addon = __commonJS({
81475
82214
  [Symbol.toStringTag]: { value: "Module" }
81476
82215
  });
81477
82216
  var require_chunk = require_chunk_Cek0wNdY();
81478
- var require_dist10 = require_dist_CxgTulEG();
82217
+ var require_dist10 = require_dist_BPlfW_CG();
81479
82218
  var node_crypto = __require("crypto");
81480
82219
  var node_fs_promises = __require("fs/promises");
81481
82220
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -83356,7 +84095,7 @@ var require_system_config_addon = __commonJS({
83356
84095
  [Symbol.toStringTag]: { value: "Module" }
83357
84096
  });
83358
84097
  require_chunk_Cek0wNdY();
83359
- var require_dist10 = require_dist_CxgTulEG();
84098
+ var require_dist10 = require_dist_BPlfW_CG();
83360
84099
  var SECTION_TITLES = {
83361
84100
  server: "Server",
83362
84101
  auth: "Authentication"
@@ -101417,7 +102156,7 @@ var require_winston_logging = __commonJS({
101417
102156
  [Symbol.toStringTag]: { value: "Module" }
101418
102157
  });
101419
102158
  var require_chunk = require_chunk_Cek0wNdY();
101420
- var require_dist10 = require_dist_CxgTulEG();
102159
+ var require_dist10 = require_dist_BPlfW_CG();
101421
102160
  var require_formatter = require_formatter_DqAKDlvN();
101422
102161
  var node_path = __require("path");
101423
102162
  node_path = require_chunk.__toESM(node_path);
@@ -102447,9 +103186,9 @@ var require_file_data_plane_DO8KbxCe = __commonJS({
102447
103186
  }
102448
103187
  });
102449
103188
 
102450
- // ../system/dist/tls-u8QCJCFE.js
102451
- var require_tls_u8QCJCFE = __commonJS({
102452
- "../system/dist/tls-u8QCJCFE.js"(exports) {
103189
+ // ../system/dist/tls-BxQlomxd.js
103190
+ var require_tls_BxQlomxd = __commonJS({
103191
+ "../system/dist/tls-BxQlomxd.js"(exports) {
102453
103192
  "use strict";
102454
103193
  var require_chunk = require_chunk_Cek0wNdY();
102455
103194
  var node_crypto = __require("crypto");
@@ -102457,9 +103196,9 @@ var require_tls_u8QCJCFE = __commonJS({
102457
103196
  var node_path = __require("path");
102458
103197
  var node_child_process = __require("child_process");
102459
103198
  var node_util = __require("util");
103199
+ var node_fs = __require("fs");
102460
103200
  var node_os = __require("os");
102461
103201
  node_os = require_chunk.__toESM(node_os);
102462
- var node_fs = __require("fs");
102463
103202
  var node_http = __require("http");
102464
103203
  var node_net = __require("net");
102465
103204
  var DEFAULT_HTTP_PORT = 4480;
@@ -113905,12 +114644,12 @@ var require_dist2 = __commonJS({
113905
114644
  }
113906
114645
  });
113907
114646
 
113908
- // ../system/dist/manifest-python-deps-CcDe4S2Z.js
113909
- var require_manifest_python_deps_CcDe4S2Z = __commonJS({
113910
- "../system/dist/manifest-python-deps-CcDe4S2Z.js"(exports) {
114647
+ // ../system/dist/manifest-python-deps-COeSr7el.js
114648
+ var require_manifest_python_deps_COeSr7el = __commonJS({
114649
+ "../system/dist/manifest-python-deps-COeSr7el.js"(exports) {
113911
114650
  "use strict";
113912
114651
  var require_chunk = require_chunk_Cek0wNdY();
113913
- require_dist_CxgTulEG();
114652
+ require_dist_BPlfW_CG();
113914
114653
  var node_crypto = __require("crypto");
113915
114654
  node_crypto = require_chunk.__toESM(node_crypto);
113916
114655
  var _camstack_types_node = require_node();
@@ -113919,10 +114658,10 @@ var require_manifest_python_deps_CcDe4S2Z = __commonJS({
113919
114658
  node_path = require_chunk.__toESM(node_path);
113920
114659
  var node_child_process = __require("child_process");
113921
114660
  var node_util = __require("util");
113922
- var node_os = __require("os");
113923
- node_os = require_chunk.__toESM(node_os);
113924
114661
  var node_fs = __require("fs");
113925
114662
  node_fs = require_chunk.__toESM(node_fs);
114663
+ var node_os = __require("os");
114664
+ node_os = require_chunk.__toESM(node_os);
113926
114665
  var node_http = __require("http");
113927
114666
  var node_v8 = __require("v8");
113928
114667
  node_v8 = require_chunk.__toESM(node_v8);
@@ -125356,7 +126095,7 @@ var require_dist3 = __commonJS({
125356
126095
  "use strict";
125357
126096
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
125358
126097
  var require_chunk = require_chunk_Cek0wNdY();
125359
- var require_dist10 = require_dist_CxgTulEG();
126098
+ var require_dist10 = require_dist_BPlfW_CG();
125360
126099
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
125361
126100
  require_alerts();
125362
126101
  var require_formatter = require_formatter_DqAKDlvN();
@@ -125381,8 +126120,8 @@ var require_dist3 = __commonJS({
125381
126120
  require_system_config();
125382
126121
  var require_builtins_winston_logging_index = require_winston_logging();
125383
126122
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
125384
- var require_tls$1 = require_tls_u8QCJCFE();
125385
- var require_manifest_python_deps = require_manifest_python_deps_CcDe4S2Z();
126123
+ var require_tls$1 = require_tls_BxQlomxd();
126124
+ var require_manifest_python_deps = require_manifest_python_deps_COeSr7el();
125386
126125
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
125387
126126
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
125388
126127
  var zod = require_zod();
@@ -125394,11 +126133,11 @@ var require_dist3 = __commonJS({
125394
126133
  var node_child_process = __require("child_process");
125395
126134
  var node_util = __require("util");
125396
126135
  node_util = require_chunk.__toESM(node_util);
125397
- var node_os = __require("os");
125398
- node_os = require_chunk.__toESM(node_os);
125399
126136
  var node_fs = __require("fs");
125400
126137
  var node_fs$1 = require_chunk.__toESM(node_fs, 1);
125401
126138
  node_fs = require_chunk.__toESM(node_fs);
126139
+ var node_os = __require("os");
126140
+ node_os = require_chunk.__toESM(node_os);
125402
126141
  var node_http = __require("http");
125403
126142
  var node_vm = __require("vm");
125404
126143
  node_vm = require_chunk.__toESM(node_vm);
@@ -215080,6 +215819,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
215080
215819
  /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
215081
215820
  uptimeSec: zod.z.number()
215082
215821
  });
215822
+ var ContainerMemoryPointSchema = zod.z.object({
215823
+ /** Which hierarchy answered, so a reading is never ambiguous. */
215824
+ source: zod.z.enum(["cgroup-v2", "cgroup-v1"]),
215825
+ /** `memory.current` (v2) / `memory.usage_in_bytes` (v1). Always known. */
215826
+ currentBytes: zod.z.number(),
215827
+ /** The cgroup's ceiling. `null` = NO LIMIT — never a sentinel, never zero. */
215828
+ limitBytes: zod.z.number().nullable(),
215829
+ /** Anonymous pages: the closest thing to "what the processes allocated". */
215830
+ anonBytes: zod.z.number().nullable(),
215831
+ /** Page cache. Charged to the cgroup, owned by no process. */
215832
+ fileBytes: zod.z.number().nullable(),
215833
+ /**
215834
+ * Shared memory — and the field that explained the largest single surprise.
215835
+ * The i915 driver backs GPU buffers with shmem, so an inference pool or a
215836
+ * hardware-decode session holding DRM objects is charged HERE and appears
215837
+ * nowhere in a `ps` scan.
215838
+ */
215839
+ shmemBytes: zod.z.number().nullable(),
215840
+ /** Kernel slab charged to this cgroup. `null` on v1, which never publishes it. */
215841
+ slabBytes: zod.z.number().nullable(),
215842
+ /**
215843
+ * Shrinkable i915 GEM object bytes, from debugfs.
215844
+ *
215845
+ * **Host-wide across every DRM client, NOT cgroup-scoped.** It is not a
215846
+ * component of `currentBytes` and must not be subtracted from it; it says
215847
+ * what put the shmem there, where `shmemBytes` only says how much.
215848
+ *
215849
+ * `null` wherever debugfs is not mounted — which is inside every camstack
215850
+ * container today — and on any node with no Intel GPU.
215851
+ */
215852
+ gpuShmemBytes: zod.z.number().nullable()
215853
+ }).extend({ atMs: zod.z.number() });
215083
215854
  var DumpHeapSnapshotInputSchema = zod.z.object({
215084
215855
  /** The addon whose runner should dump a heap snapshot. */
215085
215856
  addonId: zod.z.string()
@@ -215129,6 +215900,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
215129
215900
  /** One entry per function seen in the window, heaviest-first. */
215130
215901
  series: zod.z.array(LoadFunctionSeriesSchema).readonly(),
215131
215902
  /**
215903
+ * The CONTAINER's memory over the same window, oldest-first.
215904
+ *
215905
+ * Sits next to `series` rather than in a method of its own because the whole
215906
+ * question is a subtraction: the per-process rows in `series` sum to one
215907
+ * number and this one is another, and an operator who has to issue two calls
215908
+ * to compare them will compare two different instants. Same reader, same
215909
+ * `sinceMs`, same `bucketMs`, same timestamps.
215910
+ *
215911
+ * **EMPTY means ABSENT, never zero.** A node with no cgroup — a developer
215912
+ * Mac, a bare-metal host, a container with the hierarchy hidden — reports no
215913
+ * points at all. A zero here would be indistinguishable from a healthy
215914
+ * container and is precisely the lie this field exists to avoid.
215915
+ */
215916
+ containerMemory: zod.z.array(ContainerMemoryPointSchema).readonly(),
215917
+ /**
215132
215918
  * Width of one returned bucket, in ms. Equals the sampling cadence when no
215133
215919
  * reduction was needed — so a caller can always say what one point covers
215134
215920
  * without having to know whether it was reduced.
@@ -226930,6 +227716,33 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226930
227716
  deviceNative: true,
226931
227717
  mode: "singleton",
226932
227718
  deviceTypes: [require_sleep.DeviceType.Camera],
227719
+ /**
227720
+ * **Auth tier: `protected` on every method — deliberate, and load-bearing.**
227721
+ *
227722
+ * Talking through a camera is an OPERATE action, not a CONFIGURE one. This
227723
+ * cap has no configuration surface at all: all six methods open, feed and
227724
+ * close one live audio session against one `deviceId`. That is the same
227725
+ * authority as `ptz.move` or `snapshot.getSnapshot`, both `protected` — and
227726
+ * the opposite of `ptz.savePreset` / `snapshot.invalidateCache`, which are
227727
+ * `admin` because they change what the device IS.
227728
+ *
227729
+ * `protected` does not mean ungated: `protectedProcedure` runs the
227730
+ * `METHOD_ACCESS_MAP` scope check, and every method here is `scope: 'device'`
227731
+ * with `access: 'create'` and a `deviceId` in its input. So a caller needs a
227732
+ * grant that covers THAT camera at `create` — a `camera-viewer` (`view`
227733
+ * only) still cannot talk, and a grant on camera 5 cannot talk through
227734
+ * camera 7.
227735
+ *
227736
+ * Every method was `auth: 'admin'` from the initial commit, which made the
227737
+ * cap unreachable by every non-admin principal — `adminProcedure` throws
227738
+ * `FORBIDDEN: Admin required` BEFORE the scope check runs, so the scope
227739
+ * machinery generated for this cap (`METHOD_ACCESS_MAP`,
227740
+ * `DEVICE_SCOPED_CAPS`, `METHOD_DEVICE_SELECTORS`) was complete and dead. The
227741
+ * `camera-operator` scope preset has promised "PTZ control, intercom,
227742
+ * snapshots" since that same commit; the promise could not be kept. Recorded
227743
+ * as D289; `scripts/check-scope-preset-promises.ts` now fails the build if a
227744
+ * preset promises a cap no row of that preset can reach.
227745
+ */
226933
227746
  methods: {
226934
227747
  /**
226935
227748
  * Open a server-side WebRTC audio-only session. Returns an SDP
@@ -226942,7 +227755,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226942
227755
  sdpOffer: zod.z.string()
226943
227756
  }), {
226944
227757
  kind: "mutation",
226945
- auth: "admin"
227758
+ auth: "protected"
226946
227759
  }),
226947
227760
  handleAnswer: require_sleep.method(zod.z.object({
226948
227761
  deviceId: zod.z.number(),
@@ -226950,7 +227763,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226950
227763
  sdpAnswer: zod.z.string()
226951
227764
  }), zod.z.void(), {
226952
227765
  kind: "mutation",
226953
- auth: "admin"
227766
+ auth: "protected"
226954
227767
  }),
226955
227768
  /** Close explicitly. Server also auto-closes on 30s idle. */
226956
227769
  stopSession: require_sleep.method(zod.z.object({
@@ -226958,7 +227771,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226958
227771
  sessionId: zod.z.string()
226959
227772
  }), zod.z.void(), {
226960
227773
  kind: "mutation",
226961
- auth: "admin"
227774
+ auth: "protected"
226962
227775
  }),
226963
227776
  /**
226964
227777
  * Open a raw-PCM talk session (no WebRTC SDP plumbing). Used by
@@ -226971,7 +227784,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
226971
227784
  */
226972
227785
  startTalkSession: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.object({ sessionId: zod.z.string() }), {
226973
227786
  kind: "mutation",
226974
- auth: "admin"
227787
+ auth: "protected"
226975
227788
  }),
226976
227789
  /**
226977
227790
  * Push one chunk of talk-back audio onto the active talk session.
@@ -227006,12 +227819,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227006
227819
  sequenceNumber: zod.z.number().int()
227007
227820
  }), zod.z.object({ accepted: zod.z.boolean() }), {
227008
227821
  kind: "mutation",
227009
- auth: "admin"
227822
+ auth: "protected"
227010
227823
  }),
227011
227824
  /** Close the raw-PCM talk session. Idempotent. */
227012
227825
  endTalkSession: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), zod.z.void(), {
227013
227826
  kind: "mutation",
227014
- auth: "admin"
227827
+ auth: "protected"
227015
227828
  })
227016
227829
  },
227017
227830
  events: { onStatusChanged: { data: zod.z.object({
@@ -244269,7 +245082,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244269
245082
  {
244270
245083
  id: "camera-operator",
244271
245084
  label: "Camera operator",
244272
- description: "Everything `camera-viewer` does plus PTZ control, intercom, snapshots and recording start/stop. Cannot delete devices or recordings.",
245085
+ description: "Everything `camera-viewer` does plus live control of every granted camera \u2014 PTZ movement, intercom talk-back and on-demand snapshots. Cannot delete devices, and grants nothing on system-scope caps (recording control, users, addons).",
244273
245086
  rows: [{
244274
245087
  type: "category",
244275
245088
  target: "device",
@@ -400869,6 +401682,7 @@ var require_scope_access = __commonJS({
400869
401682
  "use strict";
400870
401683
  Object.defineProperty(exports, "__esModule", { value: true });
400871
401684
  exports.DEVICE_ENUMERATION_METHODS = void 0;
401685
+ exports.referencedDeviceIds = referencedDeviceIds;
400872
401686
  exports.checkScopeAccess = checkScopeAccess;
400873
401687
  var system_1 = require_dist3();
400874
401688
  var types_1 = require_dist4();
@@ -412794,6 +413608,25 @@ var require_trpc_router = __commonJS({
412794
413608
  }
412795
413609
  });
412796
413610
 
413611
+ // ../../server/backend/dist/api/trpc/trpc-error-device-tags.js
413612
+ var require_trpc_error_device_tags = __commonJS({
413613
+ "../../server/backend/dist/api/trpc/trpc-error-device-tags.js"(exports) {
413614
+ "use strict";
413615
+ Object.defineProperty(exports, "__esModule", { value: true });
413616
+ exports.trpcErrorDeviceTags = trpcErrorDeviceTags;
413617
+ var scope_access_js_1 = require_scope_access();
413618
+ function trpcErrorDeviceTags(path, input) {
413619
+ if (path === void 0)
413620
+ return {};
413621
+ const ids = (0, scope_access_js_1.referencedDeviceIds)(path, input);
413622
+ const first = ids[0];
413623
+ if (first === void 0)
413624
+ return {};
413625
+ return ids.length > 1 ? { deviceId: first, deviceCount: ids.length } : { deviceId: first };
413626
+ }
413627
+ }
413628
+ });
413629
+
412797
413630
  // ../../server/backend/dist/api/trpc/ws-request-census.js
412798
413631
  var require_ws_request_census = __commonJS({
412799
413632
  "../../server/backend/dist/api/trpc/ws-request-census.js"(exports) {
@@ -420876,6 +421709,7 @@ var require_main4 = __commonJS({
420876
421709
  var trpc_context_1 = require_trpc_context();
420877
421710
  var trpc_router_1 = require_trpc_router();
420878
421711
  var trpc_error_principal_1 = require_trpc_error_principal();
421712
+ var trpc_error_device_tags_1 = require_trpc_error_device_tags();
420879
421713
  var ws_request_census_1 = require_ws_request_census();
420880
421714
  var addon_route_jwt_gate_js_1 = require_addon_route_jwt_gate();
420881
421715
  var session_cookie_js_1 = require_session_cookie();
@@ -421174,9 +422008,13 @@ var require_main4 = __commonJS({
421174
422008
  (0, http_request_census_hook_1.recordTrpcHttpRequest)(httpRequestCensus, req, trpcCtx.user);
421175
422009
  return trpcCtx;
421176
422010
  },
421177
- onError: ({ path: trpcPath, error, ctx }) => {
422011
+ onError: ({ path: trpcPath, error, ctx, input }) => {
421178
422012
  const trpcLogger = app.get(logging_service_1.LoggingService).createLogger("tRPC");
421179
422013
  trpcLogger.warn("tRPC error", {
422014
+ // WHICH camera. A refusal on a device-scoped path that names no
422015
+ // deviceId cannot answer "one camera or all of them?" — the
422016
+ // question always asked first (2026-08-29, `intercom.startSession`).
422017
+ tags: (0, trpc_error_device_tags_1.trpcErrorDeviceTags)(trpcPath, input),
421180
422018
  meta: {
421181
422019
  code: error.code,
421182
422020
  path: trpcPath ?? "?",
@@ -421575,9 +422413,12 @@ var require_main4 = __commonJS({
421575
422413
  (0, ws_request_census_1.identifyWsCensusSession)(opts.res, (0, trpc_error_principal_1.describeTrpcPrincipal)(wsCtx.user));
421576
422414
  return wsCtx;
421577
422415
  },
421578
- onError: ({ path: trpcPath, error, ctx }) => {
422416
+ onError: ({ path: trpcPath, error, ctx, input }) => {
421579
422417
  const trpcLogger = app.get(logging_service_1.LoggingService).createLogger("tRPC:ws");
421580
422418
  trpcLogger.warn("tRPC error", {
422419
+ // The socket the viewer uses — this is the transport that logged the
422420
+ // four `intercom.startSession` refusals with no camera on them.
422421
+ tags: (0, trpc_error_device_tags_1.trpcErrorDeviceTags)(trpcPath, input),
421581
422422
  meta: {
421582
422423
  code: error.code,
421583
422424
  path: trpcPath ?? "?",