camstack 1.2.49 → 1.2.51

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-BVU5JADq.js
23637
- var require_dist_BVU5JADq = __commonJS({
23638
- "../system/dist/dist-BVU5JADq.js"(exports) {
23636
+ // ../system/dist/dist-Dl6MFXPr.js
23637
+ var require_dist_Dl6MFXPr = __commonJS({
23638
+ "../system/dist/dist-Dl6MFXPr.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -26367,6 +26367,116 @@ var require_dist_BVU5JADq = __commonJS({
26367
26367
  */
26368
26368
  debug: zod.z.boolean().optional()
26369
26369
  });
26370
+ var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
26371
+ var ROOT_BUCKET_KEY = "__root__";
26372
+ function bucketFor(row) {
26373
+ if (row.addonId !== null) return {
26374
+ key: row.addonId,
26375
+ kind: "addon"
26376
+ };
26377
+ if (row.classification === "root") return {
26378
+ key: ROOT_BUCKET_KEY,
26379
+ kind: "root"
26380
+ };
26381
+ return {
26382
+ key: UNATTRIBUTED_BUCKET_KEY,
26383
+ kind: "unattributed"
26384
+ };
26385
+ }
26386
+ function deci(value) {
26387
+ return Math.round(value * 10) / 10;
26388
+ }
26389
+ function foldSnapshotByFunction(rows, atMs) {
26390
+ const acc = /* @__PURE__ */ new Map();
26391
+ for (const row of rows) {
26392
+ const { key, kind } = bucketFor(row);
26393
+ const cur = acc.get(key) ?? {
26394
+ kind,
26395
+ main: 0,
26396
+ gc: 0,
26397
+ lifetime: 0,
26398
+ memory: 0,
26399
+ count: 0,
26400
+ splitKnown: true
26401
+ };
26402
+ const known = row.cpuMainPercent !== null && row.cpuGcPercent !== null;
26403
+ acc.set(key, {
26404
+ kind: cur.kind,
26405
+ main: cur.main + (row.cpuMainPercent ?? 0),
26406
+ gc: cur.gc + (row.cpuGcPercent ?? 0),
26407
+ lifetime: cur.lifetime + row.cpuPercent,
26408
+ memory: cur.memory + row.memoryRssBytes,
26409
+ count: cur.count + 1,
26410
+ splitKnown: cur.splitKnown && known
26411
+ });
26412
+ }
26413
+ return [...acc.entries()].map(([key, a]) => {
26414
+ const main = a.splitKnown ? deci(a.main) : null;
26415
+ const gc = a.splitKnown ? deci(a.gc) : null;
26416
+ const lifetime = deci(a.lifetime);
26417
+ return {
26418
+ key,
26419
+ kind: a.kind,
26420
+ point: {
26421
+ atMs,
26422
+ samples: 1,
26423
+ cpuMainPercent: main,
26424
+ cpuMainPercentMin: main,
26425
+ cpuGcPercent: gc,
26426
+ cpuGcPercentMin: gc,
26427
+ cpuLifetimePercent: lifetime,
26428
+ cpuLifetimePercentMin: lifetime,
26429
+ memoryRssBytes: a.memory,
26430
+ memoryRssBytesMin: a.memory,
26431
+ processCount: a.count,
26432
+ processCountMin: a.count
26433
+ }
26434
+ };
26435
+ });
26436
+ }
26437
+ function minNullable(a, b) {
26438
+ if (a === null || b === null) return null;
26439
+ return a < b ? a : b;
26440
+ }
26441
+ function maxNullable(a, b) {
26442
+ if (a === null || b === null) return null;
26443
+ return a > b ? a : b;
26444
+ }
26445
+ function mergePoints(held, next, atMs) {
26446
+ return {
26447
+ atMs,
26448
+ samples: held.samples + next.samples,
26449
+ cpuMainPercent: maxNullable(held.cpuMainPercent, next.cpuMainPercent),
26450
+ cpuMainPercentMin: minNullable(held.cpuMainPercentMin, next.cpuMainPercentMin),
26451
+ cpuGcPercent: maxNullable(held.cpuGcPercent, next.cpuGcPercent),
26452
+ cpuGcPercentMin: minNullable(held.cpuGcPercentMin, next.cpuGcPercentMin),
26453
+ cpuLifetimePercent: Math.max(held.cpuLifetimePercent, next.cpuLifetimePercent),
26454
+ cpuLifetimePercentMin: Math.min(held.cpuLifetimePercentMin, next.cpuLifetimePercentMin),
26455
+ memoryRssBytes: Math.max(held.memoryRssBytes, next.memoryRssBytes),
26456
+ memoryRssBytesMin: Math.min(held.memoryRssBytesMin, next.memoryRssBytesMin),
26457
+ processCount: Math.max(held.processCount, next.processCount),
26458
+ processCountMin: Math.min(held.processCountMin, next.processCountMin)
26459
+ };
26460
+ }
26461
+ function resolveBucketMs(spanMs, cadenceMs, maxPoints) {
26462
+ if (maxPoints <= 0 || cadenceMs <= 0 || spanMs <= 0) return Math.max(cadenceMs, 1);
26463
+ const wanted = spanMs / maxPoints;
26464
+ if (wanted <= cadenceMs) return cadenceMs;
26465
+ return Math.ceil(wanted / cadenceMs) * cadenceMs;
26466
+ }
26467
+ function reducePoints(points, bucketMs, origin) {
26468
+ if (bucketMs <= 0 || points.length === 0) return points;
26469
+ const buckets = /* @__PURE__ */ new Map();
26470
+ for (const point of points) {
26471
+ const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
26472
+ const held = buckets.get(start);
26473
+ buckets.set(start, held === void 0 ? {
26474
+ ...point,
26475
+ atMs: start
26476
+ } : mergePoints(held, point, start));
26477
+ }
26478
+ return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
26479
+ }
26370
26480
  var MODEL_FORMATS = [
26371
26481
  "onnx",
26372
26482
  "coreml",
@@ -29520,6 +29630,10 @@ var require_dist_BVU5JADq = __commonJS({
29520
29630
  id: zod.z.string(),
29521
29631
  data: zod.z.record(zod.z.string(), zod.z.unknown())
29522
29632
  });
29633
+ var BulkRecordSchema = zod.z.object({
29634
+ id: zod.z.string().optional(),
29635
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
29636
+ });
29523
29637
  var CollectionColumnSchema = zod.z.object({
29524
29638
  name: zod.z.string(),
29525
29639
  type: zod.z.enum([
@@ -29594,6 +29708,34 @@ var require_dist_BVU5JADq = __commonJS({
29594
29708
  collection: zod.z.string(),
29595
29709
  record: SettingsRecordSchema
29596
29710
  }), zod.z.void(), { kind: "mutation" }),
29711
+ /**
29712
+ * Insert MANY records in ONE transaction, returning how many landed.
29713
+ *
29714
+ * The write-side twin of {@link deleteWhere}, and it exists for the same
29715
+ * reason: without it, appending a batch is N round trips and N COMMITs on
29716
+ * the single shared connection that also serves every cluster-wide
29717
+ * configuration read. The durable load series writes one process row per
29718
+ * process per sample — 76 rows every 10 s on the live fleet — and the
29719
+ * operator's rule for it is *one transaction per sample, never one per
29720
+ * row*. `insert` cannot express that; nothing else could.
29721
+ *
29722
+ * **All or nothing.** A batch that fails on its fifth row leaves none of
29723
+ * the five behind. A half-written sample is worse than a missing one: the
29724
+ * missing one reads as "nobody reported", which is true, while the half
29725
+ * one reads as "these were the only processes running", which is not.
29726
+ *
29727
+ * `id` is OPTIONAL per record, and that is the difference from
29728
+ * {@link insert}. A collection whose primary key is an `INTEGER` rowid
29729
+ * alias has no id to supply — SQLite assigns it, for free, and inventing a
29730
+ * `randomUUID()` for such a column would write a 36-character string into
29731
+ * an integer key. Omitted on a TEXT key, a uuid is generated exactly as
29732
+ * `insert` does.
29733
+ */
29734
+ insertMany: method(zod.z.object({
29735
+ namespace: zod.z.string().optional(),
29736
+ collection: zod.z.string(),
29737
+ records: zod.z.array(BulkRecordSchema).readonly()
29738
+ }), zod.z.object({ inserted: zod.z.number().int() }), { kind: "mutation" }),
29597
29739
  /** Update an existing record by ID. */
29598
29740
  update: method(zod.z.object({
29599
29741
  namespace: zod.z.string().optional(),
@@ -29783,6 +29925,15 @@ var require_dist_BVU5JADq = __commonJS({
29783
29925
  kind: "mutation",
29784
29926
  auth: "admin"
29785
29927
  }),
29928
+ /** Insert many records in ONE transaction. All or nothing. */
29929
+ insertMany: method(zod.z.object({
29930
+ namespace: zod.z.string().optional(),
29931
+ collection: zod.z.string(),
29932
+ records: zod.z.array(BulkRecordSchema).readonly()
29933
+ }), zod.z.object({ inserted: zod.z.number().int() }), {
29934
+ kind: "mutation",
29935
+ auth: "admin"
29936
+ }),
29786
29937
  /** Update an existing record by ID. */
29787
29938
  update: method(zod.z.object({
29788
29939
  namespace: zod.z.string().optional(),
@@ -32089,6 +32240,83 @@ var require_dist_BVU5JADq = __commonJS({
32089
32240
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
32090
32241
  mount: { kind: "skip" }
32091
32242
  };
32243
+ var LoadContributionSchema = zod.z.object({
32244
+ role: zod.z.enum([
32245
+ "decode",
32246
+ "transcode",
32247
+ "recording",
32248
+ "streaming",
32249
+ "detection"
32250
+ ]),
32251
+ /**
32252
+ * The NUMERIC device id — the same value every log line carries as
32253
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
32254
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
32255
+ * contributor that cannot name its camera must not emit the entry at all,
32256
+ * because an unnamed per-camera entry is indistinguishable from a shared one
32257
+ * and would quietly turn one camera's cost into everybody's.
32258
+ */
32259
+ deviceId: zod.z.number().int().positive().nullable(),
32260
+ attribution: zod.z.enum([
32261
+ "measured",
32262
+ "accounted",
32263
+ "unattributable"
32264
+ ]),
32265
+ /**
32266
+ * What ONE entry is, in the contributor's own words — `615/high`,
32267
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
32268
+ * family and inventing a common one would lose the only information that
32269
+ * makes two entries for the same camera distinguishable.
32270
+ */
32271
+ unit: zod.z.string(),
32272
+ /**
32273
+ * The OS process this cost lives in, when there is one. Present so a
32274
+ * consumer can (a) tell two generations of the same unit apart across a
32275
+ * restart, and (b) subtract claimed processes from the node's process
32276
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
32277
+ * process of its own.
32278
+ */
32279
+ pid: zod.z.number().int().positive().optional(),
32280
+ /**
32281
+ * When this generation started. The pid's incarnation marker: a consumer
32282
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
32283
+ * window when this changes, because the counter restarted from zero in a new
32284
+ * process.
32285
+ */
32286
+ startedAtMs: zod.z.number().optional(),
32287
+ /**
32288
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
32289
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
32290
+ * contribution is asked for.
32291
+ *
32292
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
32293
+ * needs a sampler, and a new per-node sampler is the defect half of
32294
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
32295
+ * by whoever already keeps a history; a rate cannot be un-averaged.
32296
+ *
32297
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
32298
+ * an entry with no process.
32299
+ */
32300
+ cpuSeconds: zod.z.number().optional(),
32301
+ /** Resident bytes of this unit's process, same source and same rules. */
32302
+ rssBytes: zod.z.number().optional()
32303
+ });
32304
+ var loadContributionCapability = {
32305
+ name: "load-contribution",
32306
+ scope: "system",
32307
+ mode: "collection",
32308
+ internal: true,
32309
+ methods: {
32310
+ /**
32311
+ * This addon's own cost entries, computed live from state it already
32312
+ * holds. Inert: no persistence, no sampling, no timer. It is answered on
32313
+ * whatever beat the caller already has.
32314
+ */
32315
+ list: method(zod.z.void(), zod.z.array(LoadContributionSchema).readonly())
32316
+ },
32317
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
32318
+ mount: { kind: "skip" }
32319
+ };
32092
32320
  var LoginStageEnum = zod.z.enum(["primary", "second-factor"]);
32093
32321
  var RedirectLoginMethodSchema = zod.z.object({
32094
32322
  kind: zod.z.literal("redirect"),
@@ -32260,8 +32488,7 @@ var require_dist_BVU5JADq = __commonJS({
32260
32488
  classification: zod.z.enum([
32261
32489
  "root",
32262
32490
  "managed",
32263
- "system",
32264
- "ghost"
32491
+ "system"
32265
32492
  ]),
32266
32493
  /** `$process` addon binding when `managed`, else null. */
32267
32494
  addonId: zod.z.string().nullable(),
@@ -32269,22 +32496,39 @@ var require_dist_BVU5JADq = __commonJS({
32269
32496
  nodeId: zod.z.string().nullable(),
32270
32497
  /** Truncated command line. */
32271
32498
  command: zod.z.string(),
32499
+ /**
32500
+ * `ps pcpu` — CPU averaged over the process's WHOLE LIFETIME, not a rate.
32501
+ * On a runner up for days it barely moves. Fine as a column, useless as a
32502
+ * series: use `cpuMainPercent + cpuGcPercent` for anything time-varying.
32503
+ */
32272
32504
  cpuPercent: zod.z.number(),
32273
32505
  memoryRssBytes: zod.z.number(),
32506
+ /**
32507
+ * Instantaneous CPU% of the process's own threads over the last
32508
+ * process-snapshot window, from a `/proc/<pid>/task/*` tick delta.
32509
+ *
32510
+ * `null` = UNKNOWN, never zero: no previous sample yet (first tick after
32511
+ * boot), the pid was recycled, or this node is not Linux.
32512
+ */
32513
+ cpuMainPercent: zod.z.number().nullable(),
32514
+ /**
32515
+ * Instantaneous CPU% of V8's `V8Worker` platform pool over the same window.
32516
+ *
32517
+ * This is the number that rewrote the 2026-08-27 diagnosis — hub-main 73%,
32518
+ * `stream-broker` 61% (`docs/architecture/load-ledger.md`). A CPU chart that
32519
+ * does not separate it from `cpuMainPercent` shows "busy" where the truth is
32520
+ * "allocating too much".
32521
+ *
32522
+ * Concurrent GC is the dominant tenant of that pool but not the only one
32523
+ * (background compilation runs there too), so it is reported as
32524
+ * "GC / V8 helpers" rather than as pure collection time. `null` has the same
32525
+ * meaning as on `cpuMainPercent`.
32526
+ */
32527
+ cpuGcPercent: zod.z.number().nullable(),
32528
+ /** Threads seen in the tick scan. `null` under the same conditions. */
32529
+ threadCount: zod.z.number().nullable(),
32274
32530
  /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
32275
- uptimeSec: zod.z.number(),
32276
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
32277
- orphaned: zod.z.boolean()
32278
- });
32279
- var KillProcessInputSchema = zod.z.object({
32280
- pid: zod.z.number(),
32281
- /** Force = SIGKILL. Default is SIGTERM. */
32282
- force: zod.z.boolean().optional()
32283
- });
32284
- var KillProcessResultSchema = zod.z.object({
32285
- success: zod.z.boolean(),
32286
- reason: zod.z.string().optional(),
32287
- signal: zod.z.enum(["SIGTERM", "SIGKILL"]).optional()
32531
+ uptimeSec: zod.z.number()
32288
32532
  });
32289
32533
  var DumpHeapSnapshotInputSchema = zod.z.object({
32290
32534
  /** The addon whose runner should dump a heap snapshot. */
@@ -32298,6 +32542,89 @@ var require_dist_BVU5JADq = __commonJS({
32298
32542
  pid: zod.z.number().optional(),
32299
32543
  reason: zod.z.string().optional()
32300
32544
  });
32545
+ var LoadPointSchema = zod.z.object({
32546
+ /** Bucket START, or the snapshot's own timestamp when unreduced. */
32547
+ atMs: zod.z.number(),
32548
+ /** Raw snapshots in this bucket. Never 0 — AN EMPTY BUCKET IS ABSENT. */
32549
+ samples: zod.z.number().int(),
32550
+ /**
32551
+ * `null` = UNKNOWN and it PROPAGATES: a bucket is null unless every process
32552
+ * of every snapshot in it reported a thread split. A partial sum is a
32553
+ * smaller number that looks exactly as real as a complete one.
32554
+ */
32555
+ cpuMainPercent: zod.z.number().nullable(),
32556
+ cpuMainPercentMin: zod.z.number().nullable(),
32557
+ cpuGcPercent: zod.z.number().nullable(),
32558
+ cpuGcPercentMin: zod.z.number().nullable(),
32559
+ /** Lifetime-average CPU%, summed. Always known — and never a rate. */
32560
+ cpuLifetimePercent: zod.z.number(),
32561
+ cpuLifetimePercentMin: zod.z.number(),
32562
+ memoryRssBytes: zod.z.number(),
32563
+ memoryRssBytesMin: zod.z.number(),
32564
+ processCount: zod.z.number().int(),
32565
+ processCountMin: zod.z.number().int()
32566
+ });
32567
+ var LoadFunctionSeriesSchema = zod.z.object({
32568
+ key: zod.z.string(),
32569
+ kind: zod.z.enum([
32570
+ "addon",
32571
+ "root",
32572
+ "unattributed"
32573
+ ]),
32574
+ /** Oldest-first. A missing interval is MISSING — never zero-filled. */
32575
+ points: zod.z.array(LoadPointSchema).readonly()
32576
+ });
32577
+ var NodeLoadSeriesSchema = zod.z.object({
32578
+ nodeId: zod.z.string(),
32579
+ /** One entry per function seen in the window, heaviest-first. */
32580
+ series: zod.z.array(LoadFunctionSeriesSchema).readonly(),
32581
+ /**
32582
+ * Width of one returned bucket, in ms. Equals the sampling cadence when no
32583
+ * reduction was needed — so a caller can always say what one point covers
32584
+ * without having to know whether it was reduced.
32585
+ */
32586
+ bucketMs: zod.z.number(),
32587
+ /** Raw snapshots that went into this answer, across both tiers. */
32588
+ retainedSamples: zod.z.number(),
32589
+ /** Oldest snapshot represented, or `null` when nothing is retained. */
32590
+ oldestAtMs: zod.z.number().nullable(),
32591
+ /** The fixed sampling cadence in force on the cluster, in ms. */
32592
+ cadenceMs: zod.z.number(),
32593
+ /**
32594
+ * Did the DURABLE tier contribute? `false` means the answer is the hot ring
32595
+ * alone — an agent (which holds no table), or a store that refused.
32596
+ * Reported because "the last hour" and "the last six hours" are different
32597
+ * questions and an operator must not have to guess which was answered.
32598
+ */
32599
+ durable: zod.z.boolean()
32600
+ });
32601
+ var GetLoadSeriesInputSchema = zod.z.object({
32602
+ /**
32603
+ * The node whose series is wanted.
32604
+ *
32605
+ * NOT named `nodeId`: the generated cap router strips a top-level
32606
+ * `nodeId` from every method input and uses it to ROUTE the call to
32607
+ * that node's provider (`generated-cap-routers.ts`). A series target
32608
+ * called `nodeId` would silently become a routing pin and never reach
32609
+ * the provider. The hub holds every node it hears from, so the
32610
+ * ordinary call is unpinned — answered by the hub, for any node.
32611
+ */
32612
+ forNodeId: zod.z.string(),
32613
+ /**
32614
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
32615
+ * holds and receives only what it is missing, so seeding a live chart
32616
+ * from this method cannot double a point already drawn.
32617
+ */
32618
+ sinceMs: zod.z.number().optional(),
32619
+ /**
32620
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
32621
+ * preserving min and max per bucket.
32622
+ *
32623
+ * Absent means NO reduction — legitimate for a short window and a trap for a
32624
+ * long one, which is why a chart passes its own pixel width.
32625
+ */
32626
+ maxPoints: zod.z.number().int().positive().optional()
32627
+ });
32301
32628
  var SystemMetricsSchema = zod.z.object({
32302
32629
  cpuPercent: zod.z.number(),
32303
32630
  memoryPercent: zod.z.number(),
@@ -32343,28 +32670,44 @@ var require_dist_BVU5JADq = __commonJS({
32343
32670
  getAddonStats: method(zod.z.object({ addonId: zod.z.string() }), PidResourceStatsSchema.nullable()),
32344
32671
  /**
32345
32672
  * Snapshot of every camstack-related process on this node with a
32346
- * ghost/managed/root classification. Powers the Cluster → Agent →
32347
- * Processes tab: cross-references `$process.list` against a `ps` scan
32348
- * so orphaned trees (PPID=1) or unknown children show up as `ghost`
32349
- * and can be killed from the UI.
32673
+ * root/managed/system classification. Powers the Cluster → Agent →
32674
+ * Processes tab: cross-references `$process.list` against a `ps` scan so
32675
+ * per-addon CPU and RSS can be attributed, and so a process the cluster
32676
+ * does not manage is still visible.
32677
+ *
32678
+ * **Read-only, by design.** This cap once carried a `killProcess`
32679
+ * mutation; it was deleted on 2026-08-27. A runner's lifecycle belongs to
32680
+ * `CrashSupervisor` and is driven through `addons.restartAddon` /
32681
+ * `$process.restart` — signalling a raw pid went around the supervisor
32682
+ * (D6), and the one class it was willing to signal turned out to be the
32683
+ * container's own init and the operator's desktop app.
32350
32684
  */
32351
32685
  listNodeProcesses: method(zod.z.void(), zod.z.array(NodeProcessSchema).readonly()),
32352
32686
  /**
32353
- * Send SIGTERM (or SIGKILL when `force`) to a pid inside this node's
32354
- * process tree. The provider refuses pids that aren't in the live
32355
- * `listNodeProcesses()` snapshot callers can't use this endpoint
32356
- * to kill arbitrary system processes.
32687
+ * The retained per-node load series the ONE reader over BOTH tiers.
32688
+ *
32689
+ * The in-memory ring is the HOT window (the last 180 snapshots, held by
32690
+ * every node's `native-metrics`); the hub's `metrics:node-load-samples`
32691
+ * table is the COLD one (the operator's retention, six hours by default).
32692
+ * This method merges them and DEDUPES on `atMs`, so a snapshot present in
32693
+ * both contributes once and the caller never learns which tier a point
32694
+ * came from. There is deliberately no second read surface: two readers is
32695
+ * how two charts start disagreeing about the same node.
32696
+ *
32697
+ * Reads only; nothing is sampled to answer it. Normally called UNPINNED —
32698
+ * the hub hears every node's snapshot and holds every node's rows — and
32699
+ * answers for any `forNodeId`. Pinned to an agent it answers from that
32700
+ * agent's ring alone (`durable: false`). Empty is a legitimate answer: a
32701
+ * node nobody has heard from has no series, and saying so is the truth.
32357
32702
  */
32358
- killProcess: method(KillProcessInputSchema, KillProcessResultSchema, {
32359
- kind: "mutation",
32360
- auth: "admin"
32361
- }),
32703
+ getLoadSeries: method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema),
32362
32704
  /**
32363
32705
  * Tell the addon's forked runner to write a V8 heap snapshot to disk (via
32364
32706
  * SIGUSR2 — the runner's diagnostic handler). Also logs its
32365
- * `process.memoryUsage()` + heap-space breakdown. Refuses pids not in the
32366
- * live `listNodeProcesses()` snapshot. Use for deep per-addon memory
32367
- * attribution; copy the returned path off the node to analyze.
32707
+ * `process.memoryUsage()` + heap-space breakdown. Resolves the pid from
32708
+ * `$process.list`, so it can only reach a runner this node spawned. Use
32709
+ * for deep per-addon memory attribution; copy the returned path off the
32710
+ * node to analyze.
32368
32711
  */
32369
32712
  dumpHeapSnapshot: method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
32370
32713
  kind: "mutation",
@@ -47465,6 +47808,7 @@ var require_dist_BVU5JADq = __commonJS({
47465
47808
  */
47466
47809
  channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
47467
47810
  });
47811
+ var ReportedLoadContributionSchema = LoadContributionSchema.extend({ addonId: zod.z.string() });
47468
47812
  var GetLoggingSettingsInputSchema = zod.z.object({
47469
47813
  scopeNodeId: zod.z.string().optional(),
47470
47814
  /**
@@ -47558,6 +47902,22 @@ var require_dist_BVU5JADq = __commonJS({
47558
47902
  */
47559
47903
  getRequestCensus: method(zod.z.void(), RequestCensusStatusSchema, { auth: "admin" }),
47560
47904
  /**
47905
+ * Every `load-contribution` an addon on this cluster reports — each
47906
+ * addon's OWN cost, already attributed by the addon that owns it.
47907
+ *
47908
+ * There is no central list of what costs what: an addon that spawns a
47909
+ * per-camera child declares it, and one that cannot attribute its cost
47910
+ * (the shared inference pool) declares THAT. So a new cost family appears
47911
+ * here the moment its addon is redeployed, with nobody editing anything.
47912
+ *
47913
+ * What this does NOT do is measure the node. `metrics.node-processes-
47914
+ * snapshot` still does that, and the difference between the two is the
47915
+ * finding: a process no contribution claims is either a leak or a family
47916
+ * nobody has taught to report. Both belong in the unattributed bucket, and
47917
+ * neither may be folded into a camera.
47918
+ */
47919
+ getLoadContributions: method(zod.z.void(), zod.z.array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }),
47920
+ /**
47561
47921
  * The logging settings document — levels and armed diagnostics — resolved
47562
47922
  * for `nodeId`, or for the cluster when `nodeId` is absent.
47563
47923
  *
@@ -48976,6 +49336,7 @@ var require_dist_BVU5JADq = __commonJS({
48976
49336
  lawnMowerControlCapability,
48977
49337
  llmCapability,
48978
49338
  llmRuntimeCapability,
49339
+ loadContributionCapability,
48979
49340
  localNetworkCapability,
48980
49341
  lockControlCapability,
48981
49342
  logChannelsCapability,
@@ -49999,6 +50360,12 @@ var require_dist_BVU5JADq = __commonJS({
49999
50360
  addonId: null,
50000
50361
  access: "create"
50001
50362
  },
50363
+ "dataStoreProvider.insertMany": {
50364
+ capName: "data-store-provider",
50365
+ capScope: "system",
50366
+ addonId: null,
50367
+ access: "create"
50368
+ },
50002
50369
  "dataStoreProvider.isEmpty": {
50003
50370
  capName: "data-store-provider",
50004
50371
  capScope: "system",
@@ -51313,6 +51680,12 @@ var require_dist_BVU5JADq = __commonJS({
51313
51680
  addonId: null,
51314
51681
  access: "create"
51315
51682
  },
51683
+ "loadContribution.list": {
51684
+ capName: "load-contribution",
51685
+ capScope: "system",
51686
+ addonId: null,
51687
+ access: "view"
51688
+ },
51316
51689
  "localNetwork.downloadCa": {
51317
51690
  capName: "local-network",
51318
51691
  capScope: "system",
@@ -51613,17 +51986,17 @@ var require_dist_BVU5JADq = __commonJS({
51613
51986
  addonId: null,
51614
51987
  access: "view"
51615
51988
  },
51616
- "metricsProvider.getProcessStats": {
51989
+ "metricsProvider.getLoadSeries": {
51617
51990
  capName: "metrics-provider",
51618
51991
  capScope: "system",
51619
51992
  addonId: null,
51620
51993
  access: "view"
51621
51994
  },
51622
- "metricsProvider.killProcess": {
51995
+ "metricsProvider.getProcessStats": {
51623
51996
  capName: "metrics-provider",
51624
51997
  capScope: "system",
51625
51998
  addonId: null,
51626
- access: "create"
51999
+ access: "view"
51627
52000
  },
51628
52001
  "metricsProvider.listAddonInstances": {
51629
52002
  capName: "metrics-provider",
@@ -53635,6 +54008,12 @@ var require_dist_BVU5JADq = __commonJS({
53635
54008
  addonId: null,
53636
54009
  access: "create"
53637
54010
  },
54011
+ "settingsStore.insertMany": {
54012
+ capName: "settings-store",
54013
+ capScope: "system",
54014
+ addonId: null,
54015
+ access: "create"
54016
+ },
53638
54017
  "settingsStore.isEmpty": {
53639
54018
  capName: "settings-store",
53640
54019
  capScope: "system",
@@ -54247,6 +54626,12 @@ var require_dist_BVU5JADq = __commonJS({
54247
54626
  addonId: null,
54248
54627
  access: "create"
54249
54628
  },
54629
+ "system.getLoadContributions": {
54630
+ capName: "system",
54631
+ capScope: "system",
54632
+ addonId: null,
54633
+ access: "view"
54634
+ },
54250
54635
  "system.getLoggingSettings": {
54251
54636
  capName: "system",
54252
54637
  capScope: "system",
@@ -57734,6 +58119,12 @@ var require_dist_BVU5JADq = __commonJS({
57734
58119
  return filesystemBrowseCapability;
57735
58120
  }
57736
58121
  });
58122
+ Object.defineProperty(exports, "foldSnapshotByFunction", {
58123
+ enumerable: true,
58124
+ get: function() {
58125
+ return foldSnapshotByFunction;
58126
+ }
58127
+ });
57737
58128
  Object.defineProperty(exports, "hydrateSchema", {
57738
58129
  enumerable: true,
57739
58130
  get: function() {
@@ -57878,6 +58269,18 @@ var require_dist_BVU5JADq = __commonJS({
57878
58269
  return readinessKey;
57879
58270
  }
57880
58271
  });
58272
+ Object.defineProperty(exports, "reducePoints", {
58273
+ enumerable: true,
58274
+ get: function() {
58275
+ return reducePoints;
58276
+ }
58277
+ });
58278
+ Object.defineProperty(exports, "resolveBucketMs", {
58279
+ enumerable: true,
58280
+ get: function() {
58281
+ return resolveBucketMs;
58282
+ }
58283
+ });
57881
58284
  Object.defineProperty(exports, "resolveCapMount", {
57882
58285
  enumerable: true,
57883
58286
  get: function() {
@@ -57992,7 +58395,7 @@ var require_alerts_addon = __commonJS({
57992
58395
  [Symbol.toStringTag]: { value: "Module" }
57993
58396
  });
57994
58397
  require_chunk_Cek0wNdY();
57995
- var require_dist10 = require_dist_BVU5JADq();
58398
+ var require_dist10 = require_dist_Dl6MFXPr();
57996
58399
  function selectExpired(alerts, cutoffMs) {
57997
58400
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
57998
58401
  }
@@ -58811,7 +59214,7 @@ var require_console_logging = __commonJS({
58811
59214
  [Symbol.toStringTag]: { value: "Module" }
58812
59215
  });
58813
59216
  require_chunk_Cek0wNdY();
58814
- var require_dist10 = require_dist_BVU5JADq();
59217
+ var require_dist10 = require_dist_Dl6MFXPr();
58815
59218
  var require_formatter = require_formatter_DqAKDlvN();
58816
59219
  var LEVEL_RANK = {
58817
59220
  debug: 0,
@@ -58905,7 +59308,7 @@ var require_core_blocks_addon = __commonJS({
58905
59308
  "use strict";
58906
59309
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
58907
59310
  var require_chunk = require_chunk_Cek0wNdY();
58908
- var require_dist10 = require_dist_BVU5JADq();
59311
+ var require_dist10 = require_dist_Dl6MFXPr();
58909
59312
  var node_crypto = __require("crypto");
58910
59313
  var node_fs_promises = __require("fs/promises");
58911
59314
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -59802,11 +60205,11 @@ var require_core_blocks = __commonJS({
59802
60205
  }
59803
60206
  });
59804
60207
 
59805
- // ../system/dist/retired-settings-keys-_PLI9w0k.js
59806
- var require_retired_settings_keys_PLI9w0k = __commonJS({
59807
- "../system/dist/retired-settings-keys-_PLI9w0k.js"(exports) {
60208
+ // ../system/dist/retired-settings-keys-6w_JOqBg.js
60209
+ var require_retired_settings_keys_6w_JOqBg = __commonJS({
60210
+ "../system/dist/retired-settings-keys-6w_JOqBg.js"(exports) {
59808
60211
  "use strict";
59809
- var require_dist10 = require_dist_BVU5JADq();
60212
+ var require_dist10 = require_dist_Dl6MFXPr();
59810
60213
  function settingsStoreIsAuthoritativeHere(env) {
59811
60214
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
59812
60215
  return raw === "" || raw === "hub";
@@ -60638,6 +61041,7 @@ var require_node = __commonJS({
60638
61041
  var node_stream_promises = __require("stream/promises");
60639
61042
  var node_stream = __require("stream");
60640
61043
  var node_child_process = __require("child_process");
61044
+ var node_fs_promises = __require("fs/promises");
60641
61045
  function findFileByName(dir, name) {
60642
61046
  try {
60643
61047
  for (const entry of (0, node_fs.readdirSync)(dir, { withFileTypes: true })) {
@@ -60998,6 +61402,110 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
60998
61402
  hash
60999
61403
  } });
61000
61404
  }
61405
+ var CLOCK_TICKS_PER_SEC = 100;
61406
+ var PAGE_BYTES = 4096;
61407
+ var NO_COST_CLAIM = { release: () => void 0 };
61408
+ var nodeProcStatReader = {
61409
+ readStat: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "utf8"),
61410
+ readStatm: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/statm`, "utf8")
61411
+ };
61412
+ function parseProcCpuSeconds(line) {
61413
+ const close = line.lastIndexOf(")");
61414
+ if (close < 0) return null;
61415
+ const rest = line.slice(close + 1).trim().split(/\s+/);
61416
+ const utime = Number(rest[11]);
61417
+ const stime = Number(rest[12]);
61418
+ if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
61419
+ return (utime + stime) / CLOCK_TICKS_PER_SEC;
61420
+ }
61421
+ function parseProcRssBytes(line) {
61422
+ const fields = line.trim().split(/\s+/);
61423
+ const residentPages = Number(fields[1]);
61424
+ if (!Number.isFinite(residentPages)) return null;
61425
+ return residentPages * PAGE_BYTES;
61426
+ }
61427
+ async function readProcessCost(pid, reader = nodeProcStatReader) {
61428
+ let cpuSeconds = null;
61429
+ let rssBytes = null;
61430
+ try {
61431
+ cpuSeconds = parseProcCpuSeconds(await reader.readStat(pid));
61432
+ } catch {
61433
+ cpuSeconds = null;
61434
+ }
61435
+ try {
61436
+ rssBytes = parseProcRssBytes(await reader.readStatm(pid));
61437
+ } catch {
61438
+ rssBytes = null;
61439
+ }
61440
+ return {
61441
+ ...cpuSeconds === null ? {} : { cpuSeconds },
61442
+ ...rssBytes === null ? {} : { rssBytes }
61443
+ };
61444
+ }
61445
+ var ChildCostRegistry = class {
61446
+ reader;
61447
+ now;
61448
+ claims = /* @__PURE__ */ new Map();
61449
+ constructor(reader = nodeProcStatReader, now = Date.now) {
61450
+ this.reader = reader;
61451
+ this.now = now;
61452
+ }
61453
+ /**
61454
+ * Record one child. The returned handle is the only way to remove it.
61455
+ *
61456
+ * A claim with no pid is dropped rather than stored: it could carry no
61457
+ * measurement, and an entry with a camera and no numbers reads on a chart as
61458
+ * a camera that cost nothing.
61459
+ */
61460
+ claim(input) {
61461
+ const pid = input.pid;
61462
+ if (pid === void 0 || !Number.isInteger(pid) || pid <= 0) return NO_COST_CLAIM;
61463
+ const key = /* @__PURE__ */ Symbol("child-cost-claim");
61464
+ this.claims.set(key, {
61465
+ role: input.role,
61466
+ deviceId: input.deviceId,
61467
+ unit: input.unit,
61468
+ attribution: input.attribution ?? "measured",
61469
+ pid,
61470
+ startedAtMs: this.now()
61471
+ });
61472
+ return { release: () => {
61473
+ this.claims.delete(key);
61474
+ } };
61475
+ }
61476
+ /** Live claims, in claim order. Diagnostics and tests; not a contribution. */
61477
+ list() {
61478
+ return [...this.claims.values()].map((c) => ({
61479
+ role: c.role,
61480
+ deviceId: c.deviceId,
61481
+ unit: c.unit,
61482
+ attribution: c.attribution,
61483
+ pid: c.pid
61484
+ }));
61485
+ }
61486
+ /**
61487
+ * This addon's contribution: one entry per live claim, with whatever the OS
61488
+ * will tell us about that child right now.
61489
+ *
61490
+ * A child that has exited between the claim and this read contributes an
61491
+ * entry with no numbers rather than no entry — the unit exists, the addon
61492
+ * believes it is running, and hiding it would make a dying writer look like
61493
+ * a writer that was never started.
61494
+ */
61495
+ async contributions() {
61496
+ const out = [];
61497
+ for (const claim of this.claims.values()) out.push({
61498
+ role: claim.role,
61499
+ deviceId: claim.deviceId,
61500
+ attribution: claim.attribution,
61501
+ unit: claim.unit,
61502
+ pid: claim.pid,
61503
+ startedAtMs: claim.startedAtMs,
61504
+ ...await readProcessCost(claim.pid, this.reader)
61505
+ });
61506
+ return out;
61507
+ }
61508
+ };
61001
61509
  var STORAGE_LOCATION_TYPES = [
61002
61510
  "data",
61003
61511
  "media",
@@ -61815,10 +62323,12 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
61815
62323
  }
61816
62324
  }
61817
62325
  };
62326
+ exports.ChildCostRegistry = ChildCostRegistry;
61818
62327
  exports.FfmpegProcess = FfmpegProcess;
61819
62328
  exports.FilesystemStorageProvider = FilesystemStorageProvider;
61820
62329
  exports.Fmp4FragmentChild = Fmp4FragmentChild;
61821
62330
  exports.Fmp4FragmentPlane = Fmp4FragmentPlane;
62331
+ exports.NO_COST_CLAIM = NO_COST_CLAIM;
61822
62332
  exports.PYTHON_VERSION = PYTHON_VERSION;
61823
62333
  exports.buildBinaryPath = buildBinaryPath;
61824
62334
  exports.canonicalDeviceFingerprint = canonicalDeviceFingerprint;
@@ -61834,6 +62344,10 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
61834
62344
  exports.getPythonDownloadUrl = getPythonDownloadUrl;
61835
62345
  exports.installPythonPackages = installPythonPackages;
61836
62346
  exports.installPythonRequirements = installPythonRequirements;
62347
+ exports.nodeProcStatReader = nodeProcStatReader;
62348
+ exports.parseProcCpuSeconds = parseProcCpuSeconds;
62349
+ exports.parseProcRssBytes = parseProcRssBytes;
62350
+ exports.readProcessCost = readProcessCost;
61837
62351
  exports.resolveExportFingerprint = resolveExportFingerprint;
61838
62352
  exports.signExpiringUrl = signExpiringUrl;
61839
62353
  exports.verifyExpiringUrl = verifyExpiringUrl;
@@ -61849,8 +62363,8 @@ var require_device_manager_addon = __commonJS({
61849
62363
  [Symbol.toStringTag]: { value: "Module" }
61850
62364
  });
61851
62365
  require_chunk_Cek0wNdY();
61852
- var require_dist10 = require_dist_BVU5JADq();
61853
- var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
62366
+ var require_dist10 = require_dist_Dl6MFXPr();
62367
+ var require_retired_settings_keys = require_retired_settings_keys_6w_JOqBg();
61854
62368
  var node_crypto = __require("crypto");
61855
62369
  var _camstack_types_node = require_node();
61856
62370
  var JOB_HISTORY = 20;
@@ -66599,7 +67113,7 @@ var require_hub_forwarder = __commonJS({
66599
67113
  [Symbol.toStringTag]: { value: "Module" }
66600
67114
  });
66601
67115
  require_chunk_Cek0wNdY();
66602
- var require_dist10 = require_dist_BVU5JADq();
67116
+ var require_dist10 = require_dist_Dl6MFXPr();
66603
67117
  var require_formatter = require_formatter_DqAKDlvN();
66604
67118
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
66605
67119
  var HubForwarderDestination = class {
@@ -66736,7 +67250,7 @@ var require_liveness_monitor_addon = __commonJS({
66736
67250
  "use strict";
66737
67251
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
66738
67252
  require_chunk_Cek0wNdY();
66739
- var require_dist10 = require_dist_BVU5JADq();
67253
+ var require_dist10 = require_dist_Dl6MFXPr();
66740
67254
  var NO_DEVICES = "liveness:no-devices";
66741
67255
  var ALL_OFFLINE = "liveness:all-devices-offline";
66742
67256
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -66926,7 +67440,7 @@ var require_local_auth_addon = __commonJS({
66926
67440
  [Symbol.toStringTag]: { value: "Module" }
66927
67441
  });
66928
67442
  var require_chunk = require_chunk_Cek0wNdY();
66929
- var require_dist10 = require_dist_BVU5JADq();
67443
+ var require_dist10 = require_dist_Dl6MFXPr();
66930
67444
  var node_crypto = __require("crypto");
66931
67445
  node_crypto = require_chunk.__toESM(node_crypto);
66932
67446
  var crypto$1 = __require("crypto");
@@ -74610,7 +75124,7 @@ var require_loki_logging = __commonJS({
74610
75124
  [Symbol.toStringTag]: { value: "Module" }
74611
75125
  });
74612
75126
  require_chunk_Cek0wNdY();
74613
- var require_dist10 = require_dist_BVU5JADq();
75127
+ var require_dist10 = require_dist_Dl6MFXPr();
74614
75128
  function sanitizeLabelName(raw) {
74615
75129
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
74616
75130
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -75175,7 +75689,8 @@ var require_native_metrics_addon = __commonJS({
75175
75689
  [Symbol.toStringTag]: { value: "Module" }
75176
75690
  });
75177
75691
  var require_chunk = require_chunk_Cek0wNdY();
75178
- var require_dist10 = require_dist_BVU5JADq();
75692
+ var require_dist10 = require_dist_Dl6MFXPr();
75693
+ var node_fs_promises = __require("fs/promises");
75179
75694
  var node_child_process = __require("child_process");
75180
75695
  var node_util = __require("util");
75181
75696
  var node_os = __require("os");
@@ -75646,11 +76161,893 @@ var require_native_metrics_addon = __commonJS({
75646
76161
  });
75647
76162
  });
75648
76163
  }
75649
- var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
76164
+ var LoadPartition = class {
76165
+ capacity;
76166
+ slots;
76167
+ head = 0;
76168
+ count = 0;
76169
+ rows = 0;
76170
+ /** Newest retained sample's timestamp — the monotonic gate for `push`. */
76171
+ newestAtMs = null;
76172
+ constructor(capacity) {
76173
+ this.capacity = capacity;
76174
+ this.slots = Array.from({ length: capacity });
76175
+ }
76176
+ /**
76177
+ * Accept a sample.
76178
+ *
76179
+ * A sample at or before the newest one already held is REFUSED. The bus drops
76180
+ * a node's own broadcast echo, but a cross-node redelivery or a replayed
76181
+ * subscription must not be able to double a point — and idempotence here is
76182
+ * what lets every reader above treat the series as a set.
76183
+ *
76184
+ * `accepted` is reported separately from `rowDelta` on purpose: a full ring
76185
+ * that evicts a sample of the same size has a delta of zero and is not a
76186
+ * refusal, and conflating the two would silently stop advancing the write
76187
+ * ordinal on a steady-state cluster.
76188
+ */
76189
+ push(sample) {
76190
+ if (this.newestAtMs !== null && sample.atMs <= this.newestAtMs) return {
76191
+ accepted: false,
76192
+ rowDelta: 0
76193
+ };
76194
+ const evicted = this.count === this.capacity ? this.slots[this.head]?.processes.length ?? 0 : 0;
76195
+ this.slots[this.head] = sample;
76196
+ this.head = (this.head + 1) % this.capacity;
76197
+ if (this.count < this.capacity) this.count++;
76198
+ this.newestAtMs = sample.atMs;
76199
+ const rowDelta = sample.processes.length - evicted;
76200
+ this.rows += rowDelta;
76201
+ return {
76202
+ accepted: true,
76203
+ rowDelta
76204
+ };
76205
+ }
76206
+ /** Drop the oldest sample. Returns the rows reclaimed (0 when empty). */
76207
+ dropOldest() {
76208
+ if (this.count === 0) return 0;
76209
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76210
+ const victim = this.slots[index];
76211
+ this.slots[index] = void 0;
76212
+ this.count--;
76213
+ const reclaimed = victim?.processes.length ?? 0;
76214
+ this.rows -= reclaimed;
76215
+ if (this.count === 0) this.newestAtMs = null;
76216
+ return reclaimed;
76217
+ }
76218
+ /** Oldest-first, optionally only what is strictly newer than `sinceMs`. */
76219
+ list(sinceMs) {
76220
+ const out = [];
76221
+ for (let i = 0; i < this.count; i++) {
76222
+ const index = (this.head - this.count + i + this.capacity) % this.capacity;
76223
+ const sample = this.slots[index];
76224
+ if (sample === void 0) continue;
76225
+ if (sinceMs !== void 0 && sample.atMs <= sinceMs) continue;
76226
+ out.push(sample);
76227
+ }
76228
+ return out;
76229
+ }
76230
+ size() {
76231
+ return this.count;
76232
+ }
76233
+ rowCount() {
76234
+ return this.rows;
76235
+ }
76236
+ oldestAtMs() {
76237
+ if (this.count === 0) return null;
76238
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76239
+ return this.slots[index]?.atMs ?? null;
76240
+ }
76241
+ lastWriteAtMs() {
76242
+ return this.newestAtMs;
76243
+ }
76244
+ };
76245
+ var NodeLoadRing = class {
76246
+ partitions = /* @__PURE__ */ new Map();
76247
+ /**
76248
+ * Node id → write ordinal of its last accepted sample. A monotonic counter,
76249
+ * not a clock: partition eviction must follow the order writes actually
76250
+ * happened, and node clocks disagree.
76251
+ */
76252
+ lastWriteSeq = /* @__PURE__ */ new Map();
76253
+ writeSeq = 0;
76254
+ totalRows = 0;
76255
+ samplesPerNode;
76256
+ maxTotalProcessRows;
76257
+ maxNodes;
76258
+ idleEvictionMs;
76259
+ now;
76260
+ constructor(options = {}) {
76261
+ this.samplesPerNode = options.samplesPerNode ?? 180;
76262
+ this.maxTotalProcessRows = options.maxTotalProcessRows ?? 32e3;
76263
+ this.maxNodes = options.maxNodes ?? 16;
76264
+ this.idleEvictionMs = options.idleEvictionMs ?? 36e5;
76265
+ this.now = options.now ?? Date.now;
76266
+ }
76267
+ /**
76268
+ * Retain one snapshot. `processes` is stored by reference — the payload is
76269
+ * already an immutable arrival off the bus, and copying it would double the
76270
+ * measured cost for nothing.
76271
+ *
76272
+ * Returns whether the sample was ACCEPTED — that is, whether it was new
76273
+ * rather than a replay of a timestamp this node has already delivered.
76274
+ *
76275
+ * The return value is not diagnostics. It is the idempotence gate the
76276
+ * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
76277
+ * appended to the table, which is what lets that table carry an `INTEGER`
76278
+ * rowid key instead of a composite unique index over two million rows. The
76279
+ * gate is one monotonic comparison in memory; the index it replaces was
76280
+ * measured at ~40 bytes per row.
76281
+ */
76282
+ record(nodeId, atMs, processes) {
76283
+ if (nodeId.length === 0) return false;
76284
+ this.sweepIdle();
76285
+ let partition = this.partitions.get(nodeId);
76286
+ if (partition === void 0) {
76287
+ partition = new LoadPartition(this.samplesPerNode);
76288
+ this.partitions.set(nodeId, partition);
76289
+ }
76290
+ const outcome = partition.push({
76291
+ atMs,
76292
+ processes
76293
+ });
76294
+ if (!outcome.accepted) {
76295
+ if (partition.size() === 0) this.partitions.delete(nodeId);
76296
+ return false;
76297
+ }
76298
+ this.totalRows += outcome.rowDelta;
76299
+ this.lastWriteSeq.set(nodeId, ++this.writeSeq);
76300
+ if (this.partitions.size > this.maxNodes) this.evictPartitions(this.partitions.size - this.maxNodes, nodeId);
76301
+ this.enforceRowBudget();
76302
+ return true;
76303
+ }
76304
+ /**
76305
+ * Read one node's retained series, oldest-first.
76306
+ *
76307
+ * `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
76308
+ * holds and gets back only what it is missing. That is the whole contract
76309
+ * that lets the admin UI seed from here and then continue live without
76310
+ * doubling a point it already drew.
76311
+ *
76312
+ * A node nobody has recorded answers with an empty series, not an error —
76313
+ * unknown is the truth about a node that has not reported.
76314
+ */
76315
+ read(nodeId, sinceMs) {
76316
+ this.sweepIdle();
76317
+ const partition = this.partitions.get(nodeId);
76318
+ if (partition === void 0) return {
76319
+ nodeId,
76320
+ samples: [],
76321
+ retainedSamples: 0,
76322
+ oldestAtMs: null,
76323
+ capacity: this.samplesPerNode
76324
+ };
76325
+ return {
76326
+ nodeId,
76327
+ samples: partition.list(sinceMs),
76328
+ retainedSamples: partition.size(),
76329
+ oldestAtMs: partition.oldestAtMs(),
76330
+ capacity: this.samplesPerNode
76331
+ };
76332
+ }
76333
+ /** Node ids with a live partition. Observability for the fleet bound. */
76334
+ nodeIds() {
76335
+ return [...this.partitions.keys()];
76336
+ }
76337
+ /** Process rows retained across every partition — the number that IS memory. */
76338
+ rowCount() {
76339
+ return this.totalRows;
76340
+ }
76341
+ /** Samples retained across every partition. */
76342
+ sampleCount() {
76343
+ let total = 0;
76344
+ for (const partition of this.partitions.values()) total += partition.size();
76345
+ return total;
76346
+ }
76347
+ /**
76348
+ * Drop partitions whose newest sample is older than the retention window.
76349
+ *
76350
+ * Lazy, on write and on read — never a timer. A timer would be a new
76351
+ * periodic cost in a subsystem whose entire premise is that it adds none,
76352
+ * and a ring that nobody writes to and nobody reads is not growing either.
76353
+ */
76354
+ sweepIdle() {
76355
+ const cutoff = this.now() - this.idleEvictionMs;
76356
+ for (const [nodeId, partition] of [...this.partitions.entries()]) {
76357
+ const lastWrite = partition.lastWriteAtMs();
76358
+ if (lastWrite !== null && lastWrite > cutoff) continue;
76359
+ this.dropPartition(nodeId);
76360
+ }
76361
+ }
76362
+ /**
76363
+ * Bring the fleet back under the row budget by dropping the OLDEST sample of
76364
+ * the HEAVIEST partition, repeatedly.
76365
+ *
76366
+ * Terminates: every iteration removes one sample from a non-empty partition,
76367
+ * and the population of samples is finite and strictly decreasing.
76368
+ */
76369
+ enforceRowBudget() {
76370
+ while (this.totalRows > this.maxTotalProcessRows) {
76371
+ const victim = this.heaviestPartition();
76372
+ if (victim === null) return;
76373
+ const [nodeId, partition] = victim;
76374
+ this.totalRows -= partition.dropOldest();
76375
+ if (partition.size() === 0) this.dropPartition(nodeId);
76376
+ }
76377
+ }
76378
+ heaviestPartition() {
76379
+ let best = null;
76380
+ for (const entry of this.partitions.entries()) {
76381
+ if (entry[1].size() === 0) continue;
76382
+ if (best === null || entry[1].rowCount() > best[1].rowCount()) best = entry;
76383
+ }
76384
+ return best;
76385
+ }
76386
+ /** Drop `count` whole partitions, least-recently-written first. */
76387
+ evictPartitions(count, protectedNodeId) {
76388
+ const order = [...this.partitions.keys()].filter((nodeId) => nodeId !== protectedNodeId).toSorted((a, b) => (this.lastWriteSeq.get(a) ?? 0) - (this.lastWriteSeq.get(b) ?? 0));
76389
+ let remaining = count;
76390
+ for (const nodeId of order) {
76391
+ if (remaining <= 0) return;
76392
+ this.dropPartition(nodeId);
76393
+ remaining -= 1;
76394
+ }
76395
+ }
76396
+ /** Remove a partition and everything that indexes it. */
76397
+ dropPartition(nodeId) {
76398
+ const partition = this.partitions.get(nodeId);
76399
+ if (partition === void 0) return;
76400
+ this.totalRows -= partition.rowCount();
76401
+ this.partitions.delete(nodeId);
76402
+ this.lastWriteSeq.delete(nodeId);
76403
+ }
76404
+ };
76405
+ var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
76406
+ var DEFAULT_MAX_ROWS = 5e5;
76407
+ var LOAD_SERIES_COLUMNS = [
76408
+ /** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
76409
+ * so it costs no separate index and no stored string. */
76410
+ {
76411
+ name: "id",
76412
+ type: "INTEGER",
76413
+ primaryKey: true,
76414
+ notNull: true
76415
+ },
76416
+ {
76417
+ name: "nodeId",
76418
+ type: "TEXT",
76419
+ notNull: true
76420
+ },
76421
+ /** The EMITTING node's timestamp for the sample this row belongs to. Every
76422
+ * row of one sample shares it — that is what makes a sample reassemblable. */
76423
+ {
76424
+ name: "atMs",
76425
+ type: "INTEGER",
76426
+ notNull: true
76427
+ },
76428
+ {
76429
+ name: "pid",
76430
+ type: "INTEGER",
76431
+ notNull: true
76432
+ },
76433
+ /** `NULL` for a process no addon owns — `root` and `system` both. */
76434
+ {
76435
+ name: "addonId",
76436
+ type: "TEXT"
76437
+ },
76438
+ {
76439
+ name: "classification",
76440
+ type: "TEXT",
76441
+ notNull: true
76442
+ },
76443
+ /** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
76444
+ {
76445
+ name: "cpuDeci",
76446
+ type: "INTEGER",
76447
+ notNull: true
76448
+ },
76449
+ {
76450
+ name: "rssMib",
76451
+ type: "INTEGER",
76452
+ notNull: true
76453
+ },
76454
+ /** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
76455
+ {
76456
+ name: "cpuMainDeci",
76457
+ type: "INTEGER"
76458
+ },
76459
+ /** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
76460
+ {
76461
+ name: "cpuGcDeci",
76462
+ type: "INTEGER"
76463
+ }
76464
+ ];
76465
+ var LOAD_SERIES_INDEXES = [
76466
+ /**
76467
+ * The ONE index, and it serves both jobs.
76468
+ *
76469
+ * Reads are always "this node, newer than T" — `nodeId` leads so the
76470
+ * equality is a range scan and `atMs` supplies the order without a sort.
76471
+ * Prunes are always "this node, oldest first" — the same index, walked from
76472
+ * the other end. A second index on `atMs` alone was measured and rejected:
76473
+ * it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
76474
+ * cross-node prune that a per-node loop over three nodes already serves.
76475
+ */
76476
+ {
76477
+ name: "idx_load_samples_node_at",
76478
+ columns: ["nodeId", "atMs"]
76479
+ }
76480
+ ];
76481
+ var BYTES_PER_MIB = 1048576;
76482
+ function fromDeci(value) {
76483
+ return value === null ? null : Math.round(value) / 10;
76484
+ }
76485
+ function toDeci(value) {
76486
+ return value === null ? null : Math.round(value * 10);
76487
+ }
76488
+ function rowToProcess(row) {
76489
+ return {
76490
+ pid: row.pid,
76491
+ addonId: row.addonId,
76492
+ classification: row.classification,
76493
+ cpuPercent: fromDeci(row.cpuDeci) ?? 0,
76494
+ memoryRssBytes: row.rssMib * BYTES_PER_MIB,
76495
+ cpuMainPercent: fromDeci(row.cpuMainDeci),
76496
+ cpuGcPercent: fromDeci(row.cpuGcDeci)
76497
+ };
76498
+ }
76499
+ function processToRow(nodeId, atMs, process2) {
76500
+ return {
76501
+ nodeId,
76502
+ atMs,
76503
+ pid: process2.pid,
76504
+ addonId: process2.addonId,
76505
+ classification: process2.classification,
76506
+ cpuDeci: toDeci(process2.cpuPercent) ?? 0,
76507
+ rssMib: Math.round(process2.memoryRssBytes / BYTES_PER_MIB),
76508
+ cpuMainDeci: toDeci(process2.cpuMainPercent),
76509
+ cpuGcDeci: toDeci(process2.cpuGcPercent)
76510
+ };
76511
+ }
76512
+ function rowsToSamples(rows) {
76513
+ const byAt = /* @__PURE__ */ new Map();
76514
+ for (const row of rows) {
76515
+ const held = byAt.get(row.atMs);
76516
+ if (held === void 0) byAt.set(row.atMs, [rowToProcess(row)]);
76517
+ else held.push(rowToProcess(row));
76518
+ }
76519
+ return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, processes]) => ({
76520
+ atMs,
76521
+ processes
76522
+ }));
76523
+ }
76524
+ var LoadSeriesStore = class {
76525
+ declared = false;
76526
+ lastPruneAtMs = 0;
76527
+ store;
76528
+ logger;
76529
+ nowFn;
76530
+ prunePageRows;
76531
+ pruneIntervalMs;
76532
+ constructor(deps) {
76533
+ this.store = deps.store;
76534
+ this.logger = deps.logger;
76535
+ this.nowFn = deps.now ?? (() => Date.now());
76536
+ this.prunePageRows = deps.prunePageRows ?? 2e4;
76537
+ this.pruneIntervalMs = deps.pruneIntervalMs ?? 6e4;
76538
+ }
76539
+ /** Idempotently declare the collection. `false` when the store refused. */
76540
+ async declare() {
76541
+ if (this.declared) return true;
76542
+ try {
76543
+ await this.store.declareCollection.mutate({
76544
+ collection: LOAD_SERIES_COLLECTION,
76545
+ columns: [...LOAD_SERIES_COLUMNS],
76546
+ indexes: LOAD_SERIES_INDEXES.map((i) => ({
76547
+ name: i.name,
76548
+ columns: [...i.columns]
76549
+ }))
76550
+ });
76551
+ this.declared = true;
76552
+ return true;
76553
+ } catch (err) {
76554
+ this.logger.warn("load series declareCollection failed \u2014 nothing will be retained on disk", { meta: {
76555
+ collection: LOAD_SERIES_COLLECTION,
76556
+ error: require_dist10.errMsg(err)
76557
+ } });
76558
+ return false;
76559
+ }
76560
+ }
76561
+ /**
76562
+ * Append ONE sample — every process row of it — in ONE transaction.
76563
+ *
76564
+ * Never a write per row. `insertMany` exists for exactly this: at the 10 s
76565
+ * default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
76566
+ * on the connection that also serves every cluster-wide configuration read
76567
+ * is a constant load nobody asked for.
76568
+ */
76569
+ async append(nodeId, atMs, processes) {
76570
+ if (processes.length === 0) return 0;
76571
+ if (!await this.declare()) return 0;
76572
+ const records = processes.map((p) => ({ data: { ...processToRow(nodeId, atMs, p) } }));
76573
+ try {
76574
+ const { inserted } = await this.store.insertMany.mutate({
76575
+ collection: LOAD_SERIES_COLLECTION,
76576
+ records
76577
+ });
76578
+ return inserted;
76579
+ } catch (err) {
76580
+ this.logger.warn("load series sample not retained \u2014 this interval will be missing", { meta: {
76581
+ nodeId,
76582
+ atMs,
76583
+ rows: processes.length,
76584
+ error: require_dist10.errMsg(err)
76585
+ } });
76586
+ return 0;
76587
+ }
76588
+ }
76589
+ /**
76590
+ * Read one node's cold samples, oldest-first.
76591
+ *
76592
+ * `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
76593
+ * timestamp it holds gets only what it is missing. `limitRows` bounds the
76594
+ * read in ROWS (not samples) because rows are what the query costs.
76595
+ */
76596
+ async read(nodeId, sinceMs, limitRows) {
76597
+ if (!await this.declare()) return [];
76598
+ try {
76599
+ const records = await this.store.query.query({
76600
+ collection: LOAD_SERIES_COLLECTION,
76601
+ filter: {
76602
+ where: { nodeId },
76603
+ whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
76604
+ orderBy: {
76605
+ field: "atMs",
76606
+ direction: "asc"
76607
+ },
76608
+ limit: limitRows
76609
+ }
76610
+ });
76611
+ const rows = [];
76612
+ for (const record of records) {
76613
+ const row = recordToRow(record.data);
76614
+ if (row !== null) rows.push(row);
76615
+ }
76616
+ return rowsToSamples(rows);
76617
+ } catch (err) {
76618
+ this.logger.warn("load series cold read failed \u2014 answering from the hot window only", { meta: {
76619
+ nodeId,
76620
+ sinceMs,
76621
+ error: require_dist10.errMsg(err)
76622
+ } });
76623
+ return [];
76624
+ }
76625
+ }
76626
+ /**
76627
+ * Enforce BOTH bounds, oldest-first, through a bounded page each.
76628
+ *
76629
+ * Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
76630
+ * append path must not pay a sweep on every sample.
76631
+ */
76632
+ async prune(nodeIds, retention, force = false) {
76633
+ const now = this.nowFn();
76634
+ if (!force && now - this.lastPruneAtMs < this.pruneIntervalMs) return null;
76635
+ this.lastPruneAtMs = now;
76636
+ if (!await this.declare()) return null;
76637
+ let deletedByAge = 0;
76638
+ let deletedByCap = 0;
76639
+ let rowsExamined = 0;
76640
+ const ageCutoff = now - retention.retentionHours * 36e5;
76641
+ for (const nodeId of nodeIds) {
76642
+ const outcome = await this.pruneNodeToCutoff(nodeId, ageCutoff);
76643
+ deletedByAge += outcome.deleted;
76644
+ rowsExamined += outcome.examined;
76645
+ }
76646
+ const total = await this.count();
76647
+ const excess = total === null ? 0 : total - retention.maxRows;
76648
+ if (excess > 0) {
76649
+ const outcome = await this.pruneOldestRows(nodeIds, excess);
76650
+ deletedByCap = outcome.deleted;
76651
+ rowsExamined += outcome.examined;
76652
+ this.logger.warn("load series ROW CAP bit \u2014 evicting the oldest samples", { meta: {
76653
+ collection: LOAD_SERIES_COLLECTION,
76654
+ rows: total,
76655
+ cap: retention.maxRows,
76656
+ over: excess,
76657
+ deleted: outcome.deleted,
76658
+ retentionHours: retention.retentionHours,
76659
+ hint: "lower the retention or the sampling cadence \u2014 the cap is the guarantee, not the intention"
76660
+ } });
76661
+ }
76662
+ return {
76663
+ deletedByAge,
76664
+ deletedByCap,
76665
+ rowsExamined,
76666
+ capBit: excess > 0
76667
+ };
76668
+ }
76669
+ /** Total rows, or `null` when the store could not answer. */
76670
+ async count() {
76671
+ if (!await this.declare()) return null;
76672
+ try {
76673
+ return await this.store.count.query({ collection: LOAD_SERIES_COLLECTION });
76674
+ } catch (err) {
76675
+ this.logger.warn("load series count failed \u2014 the row cap is not enforced this pass", { meta: { error: require_dist10.errMsg(err) } });
76676
+ return null;
76677
+ }
76678
+ }
76679
+ /**
76680
+ * Delete this node's rows older than `cutoff`, at most one page's worth.
76681
+ *
76682
+ * The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
76683
+ * own is one statement but an UNBOUNDED one — a first pass after a retention
76684
+ * change would delete millions of rows inside a single stalling transaction.
76685
+ * So the page is read first (keys only, ordered by the index), its last
76686
+ * `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
76687
+ */
76688
+ async pruneNodeToCutoff(nodeId, cutoff) {
76689
+ try {
76690
+ const page = await this.store.query.query({
76691
+ collection: LOAD_SERIES_COLLECTION,
76692
+ filter: {
76693
+ where: { nodeId },
76694
+ whereBetween: { atMs: [0, cutoff] },
76695
+ orderBy: {
76696
+ field: "atMs",
76697
+ direction: "asc"
76698
+ },
76699
+ limit: this.prunePageRows
76700
+ },
76701
+ columns: ["atMs"]
76702
+ });
76703
+ if (page.length === 0) return {
76704
+ deleted: 0,
76705
+ examined: 0
76706
+ };
76707
+ const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
76708
+ if (!Number.isFinite(effectiveCutoff)) return {
76709
+ deleted: 0,
76710
+ examined: page.length
76711
+ };
76712
+ const { deleted } = await this.store.deleteWhere.mutate({
76713
+ collection: LOAD_SERIES_COLLECTION,
76714
+ filter: {
76715
+ where: { nodeId },
76716
+ whereBetween: { atMs: [0, effectiveCutoff] }
76717
+ }
76718
+ });
76719
+ return {
76720
+ deleted,
76721
+ examined: page.length
76722
+ };
76723
+ } catch (err) {
76724
+ this.logger.warn("load series age prune failed \u2014 the table keeps growing this pass", { meta: {
76725
+ nodeId,
76726
+ cutoff,
76727
+ error: require_dist10.errMsg(err)
76728
+ } });
76729
+ return {
76730
+ deleted: 0,
76731
+ examined: 0
76732
+ };
76733
+ }
76734
+ }
76735
+ /**
76736
+ * Drop the oldest rows across the known nodes until `excess` is covered.
76737
+ *
76738
+ * Same bounded-page technique, walked per node so the one index serves it.
76739
+ * The node holding the oldest rows pays first, which is also the node
76740
+ * producing the pressure when a runaway process count is the cause.
76741
+ */
76742
+ async pruneOldestRows(nodeIds, excess) {
76743
+ let remaining = Math.min(excess, this.prunePageRows);
76744
+ let deleted = 0;
76745
+ let examined = 0;
76746
+ for (const nodeId of nodeIds) {
76747
+ if (remaining <= 0) break;
76748
+ try {
76749
+ const page = await this.store.query.query({
76750
+ collection: LOAD_SERIES_COLLECTION,
76751
+ filter: {
76752
+ where: { nodeId },
76753
+ orderBy: {
76754
+ field: "atMs",
76755
+ direction: "asc"
76756
+ },
76757
+ limit: remaining
76758
+ },
76759
+ columns: ["atMs"]
76760
+ });
76761
+ examined += page.length;
76762
+ if (page.length === 0) continue;
76763
+ const cutoff = Number(page.at(-1)?.data["atMs"]);
76764
+ if (!Number.isFinite(cutoff)) continue;
76765
+ const result = await this.store.deleteWhere.mutate({
76766
+ collection: LOAD_SERIES_COLLECTION,
76767
+ filter: {
76768
+ where: { nodeId },
76769
+ whereBetween: { atMs: [0, cutoff] }
76770
+ }
76771
+ });
76772
+ deleted += result.deleted;
76773
+ remaining -= result.deleted;
76774
+ } catch (err) {
76775
+ this.logger.warn("load series cap prune failed \u2014 the cap is not enforced this pass", { meta: {
76776
+ nodeId,
76777
+ error: require_dist10.errMsg(err)
76778
+ } });
76779
+ }
76780
+ }
76781
+ return {
76782
+ deleted,
76783
+ examined
76784
+ };
76785
+ }
76786
+ };
76787
+ function recordToRow(data) {
76788
+ const nodeId = data["nodeId"];
76789
+ const classification = data["classification"];
76790
+ if (typeof nodeId !== "string" || typeof classification !== "string") return null;
76791
+ const atMs = Number(data["atMs"]);
76792
+ const pid = Number(data["pid"]);
76793
+ const cpuDeci = Number(data["cpuDeci"]);
76794
+ const rssMib = Number(data["rssMib"]);
76795
+ if (![
76796
+ atMs,
76797
+ pid,
76798
+ cpuDeci,
76799
+ rssMib
76800
+ ].every((n) => Number.isFinite(n))) return null;
76801
+ const rawAddon = data["addonId"];
76802
+ const optional = (raw) => {
76803
+ if (raw === null || raw === void 0) return null;
76804
+ const n = Number(raw);
76805
+ return Number.isFinite(n) ? n : null;
76806
+ };
76807
+ return {
76808
+ nodeId,
76809
+ atMs,
76810
+ pid,
76811
+ addonId: typeof rawAddon === "string" ? rawAddon : null,
76812
+ classification,
76813
+ cpuDeci,
76814
+ rssMib,
76815
+ cpuMainDeci: optional(data["cpuMainDeci"]),
76816
+ cpuGcDeci: optional(data["cpuGcDeci"])
76817
+ };
76818
+ }
76819
+ var LoadSeriesConfigError = class extends Error {
76820
+ constructor(message) {
76821
+ super(message);
76822
+ this.name = "LoadSeriesConfigError";
76823
+ }
76824
+ };
76825
+ function requireInteger(value, field) {
76826
+ const n = typeof value === "number" ? value : Number(value);
76827
+ if (!Number.isFinite(n)) throw new LoadSeriesConfigError(`${field} must be a number, received ${String(value)}`);
76828
+ return Math.round(n);
76829
+ }
76830
+ function resolveLoadSeriesConfig(raw) {
76831
+ let cadenceSec = 10;
76832
+ if (raw.loadSeriesCadenceSec !== void 0 && raw.loadSeriesCadenceSec !== null) {
76833
+ cadenceSec = requireInteger(raw.loadSeriesCadenceSec, "loadSeriesCadenceSec");
76834
+ if (cadenceSec < 5 || cadenceSec > 60) throw new LoadSeriesConfigError(`load series cadence must be between 5 and 60 seconds \u2014 refused ${cadenceSec}`);
76835
+ }
76836
+ let retentionHours = 6;
76837
+ if (raw.loadSeriesRetentionHours !== void 0 && raw.loadSeriesRetentionHours !== null) {
76838
+ retentionHours = requireInteger(raw.loadSeriesRetentionHours, "loadSeriesRetentionHours");
76839
+ if (retentionHours < 1 || retentionHours > 72) throw new LoadSeriesConfigError(`load series retention must be between 1 and 72 hours \u2014 refused ${retentionHours}`);
76840
+ }
76841
+ return {
76842
+ cadenceSec,
76843
+ retentionHours,
76844
+ maxRows: DEFAULT_MAX_ROWS
76845
+ };
76846
+ }
76847
+ function projectLoadSeriesCost(input) {
76848
+ const samplesPerNodeWindow = input.config.retentionHours * 3600 / input.config.cadenceSec;
76849
+ const intendedRows = Math.round(samplesPerNodeWindow * Math.max(input.observedProcessRows, 0));
76850
+ const boundedRows = Math.min(intendedRows, input.config.maxRows);
76851
+ return {
76852
+ intendedRows,
76853
+ boundedRows,
76854
+ estimatedBytes: boundedRows * 87,
76855
+ capBites: intendedRows > input.config.maxRows
76856
+ };
76857
+ }
76858
+ function describeLoadSeriesCost(projection) {
76859
+ const mib = (projection.estimatedBytes / 1048576).toFixed(1);
76860
+ const rows = projection.intendedRows.toLocaleString("en-US");
76861
+ const capped = projection.boundedRows.toLocaleString("en-US");
76862
+ if (!projection.capBites) return `This configuration retains ~${rows} rows \u2248 ${mib} MiB on the hub database (NVMe cache), measured at 87 bytes per row.`;
76863
+ return `This configuration WANTS ~${rows} rows, above the ${projection.boundedRows.toLocaleString("en-US")}-row hard cap. The table will hold ~${capped} rows \u2248 ${mib} MiB and the OLDEST samples will be evicted, so the effective window is shorter than the retention you asked for.`;
76864
+ }
76865
+ function mergeSamples(cold, hot) {
76866
+ const byAt = /* @__PURE__ */ new Map();
76867
+ for (const sample of cold) byAt.set(sample.atMs, sample);
76868
+ for (const sample of hot) byAt.set(sample.atMs, sample);
76869
+ return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
76870
+ }
76871
+ function toFoldRows(processes) {
76872
+ return processes;
76873
+ }
76874
+ function mergeLoadSeries(input) {
76875
+ const samples = mergeSamples(input.cold, input.hot);
76876
+ if (samples.length === 0) return {
76877
+ series: [],
76878
+ bucketMs: input.cadenceMs,
76879
+ retainedSamples: 0,
76880
+ oldestAtMs: null
76881
+ };
76882
+ const byKey = /* @__PURE__ */ new Map();
76883
+ for (const sample of samples) for (const bucket of require_dist10.foldSnapshotByFunction(toFoldRows(sample.processes), sample.atMs)) {
76884
+ const held = byKey.get(bucket.key);
76885
+ if (held === void 0) byKey.set(bucket.key, {
76886
+ kind: bucket.kind,
76887
+ points: [bucket.point]
76888
+ });
76889
+ else held.points.push(bucket.point);
76890
+ }
76891
+ const oldestAtMs = samples[0]?.atMs ?? null;
76892
+ const newestAtMs = samples.at(-1)?.atMs ?? oldestAtMs;
76893
+ const spanMs = oldestAtMs === null || newestAtMs === null ? 0 : newestAtMs - oldestAtMs + input.cadenceMs;
76894
+ const bucketMs = input.maxPoints === void 0 ? input.cadenceMs : require_dist10.resolveBucketMs(spanMs, input.cadenceMs, input.maxPoints);
76895
+ const origin = oldestAtMs ?? 0;
76896
+ const series = [...byKey.entries()].map(([key, held]) => ({
76897
+ key,
76898
+ kind: held.kind,
76899
+ points: bucketMs > input.cadenceMs ? require_dist10.reducePoints(held.points, bucketMs, origin) : held.points
76900
+ }));
76901
+ const weight = (s) => {
76902
+ const last = s.points.at(-1);
76903
+ if (last === void 0) return -1;
76904
+ if (last.cpuMainPercent !== null && last.cpuGcPercent !== null) return last.cpuMainPercent + last.cpuGcPercent;
76905
+ return last.cpuLifetimePercent;
76906
+ };
76907
+ return {
76908
+ series: series.toSorted((a, b) => {
76909
+ const d = weight(b) - weight(a);
76910
+ return d !== 0 ? d : a.key.localeCompare(b.key);
76911
+ }),
76912
+ bucketMs,
76913
+ retainedSamples: samples.length,
76914
+ oldestAtMs
76915
+ };
76916
+ }
75650
76917
  var CAMSTACK_CMD_RE = /(camstack|tsx\s+watch\s.*launcher\.ts|packages\/agent\/dist\/cli\.js|inference_pool\.py|bench-(inference-pool|nodeav)|node .*\/packages\/)/;
75651
- var SUPERVISOR_BOUNDARY_RE = /(tsx\s+watch\s.*launcher\.ts|packages\/agent\/dist\/cli\.js|\.bin\/concurrently|\/concurrently\/dist|\bnpm-cli\.js\b|npm exec |\.bin\/vite|\/vite\/bin\/vite\.js|node_modules\/\.bin\/(vite|concurrently|tsup|rollup|esbuild|tsx)(\s|$))/;
76918
+ function classifyProcess(pid, selfPid, managed) {
76919
+ if (pid === selfPid) return "root";
76920
+ if (managed.has(pid)) return "managed";
76921
+ return "system";
76922
+ }
76923
+ function buildNodeProcesses(input) {
76924
+ const out = [];
76925
+ for (const p of input.psRows) {
76926
+ if (!CAMSTACK_CMD_RE.test(p.command)) continue;
76927
+ const managed = input.managed.get(p.pid);
76928
+ const split = input.cpuSplits.get(p.pid);
76929
+ const classification = classifyProcess(p.pid, input.selfPid, input.managed);
76930
+ out.push({
76931
+ pid: p.pid,
76932
+ ppid: p.ppid,
76933
+ pgid: p.pgid,
76934
+ classification,
76935
+ addonId: managed?.addonId ?? null,
76936
+ nodeId: managed?.nodeId ?? (p.pid === input.selfPid ? input.selfNodeId : null),
76937
+ command: p.command,
76938
+ cpuPercent: p.cpuPercent,
76939
+ memoryRssBytes: p.memoryRssBytes,
76940
+ cpuMainPercent: split?.mainPercent ?? null,
76941
+ cpuGcPercent: split?.gcPercent ?? null,
76942
+ threadCount: split?.threadCount ?? null,
76943
+ uptimeSec: p.uptimeSec
76944
+ });
76945
+ }
76946
+ return out;
76947
+ }
76948
+ var CLOCK_TICKS_PER_SEC = 100;
76949
+ var V8_HELPER_THREAD_RE = /^V8Worker/;
76950
+ function parseThreadStat(line) {
76951
+ const close = line.lastIndexOf(")");
76952
+ const open = line.indexOf("(");
76953
+ if (close < 0 || open < 0 || close < open) return null;
76954
+ const comm = line.slice(open + 1, close);
76955
+ const rest = line.slice(close + 1).trim().split(/\s+/);
76956
+ const utime = Number(rest[11]);
76957
+ const stime = Number(rest[12]);
76958
+ const startTicks = Number(rest[19]);
76959
+ if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
76960
+ if (!Number.isFinite(startTicks)) return null;
76961
+ return {
76962
+ comm,
76963
+ ticks: utime + stime,
76964
+ startTicks
76965
+ };
76966
+ }
76967
+ var nodeProcFsReader = {
76968
+ listTaskIds: (pid) => (0, node_fs_promises.readdir)(`/proc/${pid}/task`),
76969
+ readTaskStat: (pid, tid) => (0, node_fs_promises.readFile)(`/proc/${pid}/task/${tid}/stat`, "utf8")
76970
+ };
76971
+ async function readThreadTicks(pid, reader = nodeProcFsReader) {
76972
+ let tids;
76973
+ try {
76974
+ tids = await reader.listTaskIds(pid);
76975
+ } catch {
76976
+ return null;
76977
+ }
76978
+ let mainTicks = 0;
76979
+ let gcTicks = 0;
76980
+ let threadCount = 0;
76981
+ let startTicks = null;
76982
+ const mainThreadTid = String(pid);
76983
+ for (const tid of tids) {
76984
+ let line;
76985
+ try {
76986
+ line = await reader.readTaskStat(pid, tid);
76987
+ } catch {
76988
+ continue;
76989
+ }
76990
+ const parsed = parseThreadStat(line);
76991
+ if (parsed === null) continue;
76992
+ threadCount += 1;
76993
+ if (tid === mainThreadTid) startTicks = parsed.startTicks;
76994
+ if (V8_HELPER_THREAD_RE.test(parsed.comm)) gcTicks += parsed.ticks;
76995
+ else mainTicks += parsed.ticks;
76996
+ }
76997
+ if (threadCount === 0) return null;
76998
+ return {
76999
+ mainTicks,
77000
+ gcTicks,
77001
+ threadCount,
77002
+ atMs: Date.now(),
77003
+ startTicks
77004
+ };
77005
+ }
77006
+ function cpuSplitBetween(prev, next) {
77007
+ const windowMs = next.atMs - prev.atMs;
77008
+ if (windowMs <= 0) return null;
77009
+ if (prev.startTicks === null || next.startTicks === null) return null;
77010
+ if (prev.startTicks !== next.startTicks) return null;
77011
+ const mainDelta = next.mainTicks - prev.mainTicks;
77012
+ const gcDelta = next.gcTicks - prev.gcTicks;
77013
+ if (mainDelta < 0 || gcDelta < 0) return null;
77014
+ const windowTicks = windowMs / 1e3 * CLOCK_TICKS_PER_SEC;
77015
+ const pct = (delta) => Math.round(delta / windowTicks * 1e3) / 10;
77016
+ return {
77017
+ mainPercent: pct(mainDelta),
77018
+ gcPercent: pct(gcDelta),
77019
+ threadCount: next.threadCount
77020
+ };
77021
+ }
77022
+ var ThreadCpuTracker = class {
77023
+ reader;
77024
+ previous = /* @__PURE__ */ new Map();
77025
+ constructor(reader = nodeProcFsReader) {
77026
+ this.reader = reader;
77027
+ }
77028
+ /**
77029
+ * Sample `pids` and return the split for each one that HAS a usable previous
77030
+ * sample. A pid absent from the result has no answer yet — the caller must
77031
+ * report `null`, not `0`.
77032
+ */
77033
+ async sample(pids) {
77034
+ const out = /* @__PURE__ */ new Map();
77035
+ const nextPrevious = /* @__PURE__ */ new Map();
77036
+ for (const pid of pids) {
77037
+ const next = await readThreadTicks(pid, this.reader);
77038
+ if (next === null) continue;
77039
+ nextPrevious.set(pid, next);
77040
+ const prev = this.previous.get(pid);
77041
+ if (prev === void 0) continue;
77042
+ const split = cpuSplitBetween(prev, next);
77043
+ if (split !== null) out.set(pid, split);
77044
+ }
77045
+ this.previous = nextPrevious;
77046
+ return out;
77047
+ }
77048
+ };
77049
+ var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
75652
77050
  var METRICS_SNAPSHOT_INTERVAL_MS = 5e3;
75653
- var PROCESS_SNAPSHOT_INTERVAL_MS = 2e4;
75654
77051
  var METRICS_SNAPSHOT_HEARTBEAT_MS = 6e4;
75655
77052
  function coarsenResourcesSnapshot(snapshot) {
75656
77053
  if (!snapshot || typeof snapshot !== "object") return JSON.stringify(snapshot);
@@ -75669,18 +77066,6 @@ var require_native_metrics_addon = __commonJS({
75669
77066
  };
75670
77067
  return JSON.stringify(round(snapshot));
75671
77068
  }
75672
- function coarsenProcessList(processes) {
75673
- const summary = processes.filter((p) => !!p && typeof p === "object").map((p) => {
75674
- return [
75675
- p["pid"],
75676
- p["addonId"],
75677
- p["state"],
75678
- typeof p["cpuPercent"] === "number" ? Math.round(p["cpuPercent"] / 5) * 5 : null,
75679
- typeof p["memoryRss"] === "number" ? Math.round(p["memoryRss"] / (50 * 1024 * 1024)) : null
75680
- ];
75681
- });
75682
- return JSON.stringify(summary);
75683
- }
75684
77069
  function narrowWorkerState(state) {
75685
77070
  switch (state) {
75686
77071
  case "starting":
@@ -75693,7 +77078,7 @@ var require_native_metrics_addon = __commonJS({
75693
77078
  return "running";
75694
77079
  }
75695
77080
  }
75696
- var NativeMetricsAddon = class extends require_dist10.BaseAddon {
77081
+ var NativeMetricsAddon = class NativeMetricsAddon2 extends require_dist10.BaseAddon {
75697
77082
  provider = null;
75698
77083
  startedAtMs = Date.now();
75699
77084
  snapshotTimer = null;
@@ -75705,9 +77090,77 @@ var require_native_metrics_addon = __commonJS({
75705
77090
  * elapsed) is skipped.
75706
77091
  */
75707
77092
  lastResourcesEmit = null;
75708
- lastProcessesEmit = null;
77093
+ /**
77094
+ * Holds the previous `/proc/<pid>/task/*` tick counts so each process scan
77095
+ * can turn them into an instantaneous CPU rate split between the process's
77096
+ * own threads and V8's helper pool. Self-bounding — see `ThreadCpuTracker`.
77097
+ */
77098
+ threadCpu = new ThreadCpuTracker();
77099
+ /**
77100
+ * Retention for the snapshots this addon emits. Fed by SUBSCRIBING to
77101
+ * `metrics.node-processes-snapshot`, never by a second sampler — on the hub
77102
+ * that bus carries every node, so the hub's ring is the cluster's. Bounds and
77103
+ * measured cost live in `node-load-ring.ts`.
77104
+ */
77105
+ loadRing = new NodeLoadRing();
77106
+ /**
77107
+ * The COLD tier, and it exists only on the HUB.
77108
+ *
77109
+ * The table is hub-resident and the hub already hears every node's snapshot
77110
+ * on its bus, so the hub's writer is the cluster's. An agent writing through
77111
+ * the `settings-store` singleton would ship its rows over Moleculer, once
77112
+ * per sample, to reach that very same table.
77113
+ */
77114
+ loadStore = null;
77115
+ /** The resolved knobs. Re-resolved on every settings write. */
77116
+ loadConfig = {
77117
+ cadenceSec: 10,
77118
+ retentionHours: 6,
77119
+ maxRows: DEFAULT_MAX_ROWS
77120
+ };
77121
+ /**
77122
+ * Process rows seen in each node's most recent snapshot — the OBSERVED
77123
+ * numbers the settings form projects its cost from. A constant here would be
77124
+ * a projection that stops being true the first time the fleet changes.
77125
+ */
77126
+ observedRowsByNode = /* @__PURE__ */ new Map();
75709
77127
  constructor() {
75710
- super({ samplingIntervalMs: 5e3 });
77128
+ super({
77129
+ samplingIntervalMs: 5e3,
77130
+ loadSeriesCadenceSec: 10,
77131
+ loadSeriesRetentionHours: 6
77132
+ });
77133
+ }
77134
+ /** Is this the hub? The same test every other addon uses (`addon-ai`). */
77135
+ get isHub() {
77136
+ return (this.ctx.kernel.cluster?.broker?.nodeID ?? "hub") === "hub";
77137
+ }
77138
+ /** Process rows observed across the whole fleet, for the cost projection. */
77139
+ observedFleetRows() {
77140
+ let total = 0;
77141
+ for (const rows of this.observedRowsByNode.values()) total += rows;
77142
+ return total;
77143
+ }
77144
+ /**
77145
+ * Project a `NodeProcess` onto the series' own row.
77146
+ *
77147
+ * `command`, `ppid`, `pgid`, `nodeId`, `threadCount` and `uptimeSec` are
77148
+ * dropped here, at the single point both tiers pass through, so the hot ring
77149
+ * and the cold table carry the SAME shape and the merged read cannot tell
77150
+ * them apart. `command` in particular is the fattest field in a snapshot and
77151
+ * the same identical string on every runner — the runner id travels in the
77152
+ * environment, not in argv — and nothing that draws this series reads it.
77153
+ */
77154
+ static toRetained(process2) {
77155
+ return {
77156
+ pid: process2.pid,
77157
+ addonId: process2.addonId,
77158
+ classification: process2.classification,
77159
+ cpuPercent: process2.cpuPercent,
77160
+ memoryRssBytes: process2.memoryRssBytes,
77161
+ cpuMainPercent: process2.cpuMainPercent,
77162
+ cpuGcPercent: process2.cpuGcPercent
77163
+ };
75711
77164
  }
75712
77165
  async onInitialize() {
75713
77166
  const provider = new NativeMetricsProvider();
@@ -75727,16 +77180,127 @@ var require_native_metrics_addon = __commonJS({
75727
77180
  listAddonInstances: () => this.listAddonInstances(),
75728
77181
  getAddonStats: (params) => this.getAddonStats(params.addonId),
75729
77182
  listNodeProcesses: () => this.listNodeProcesses(),
75730
- killProcess: (params) => this.killProcess(params),
77183
+ getLoadSeries: (params) => this.readLoadSeries(params),
75731
77184
  dumpHeapSnapshot: (params) => this.dumpHeapSnapshot(params)
75732
77185
  };
77186
+ this.applyLoadSeriesConfig();
77187
+ if (this.isHub) this.loadStore = new LoadSeriesStore({
77188
+ store: this.ctx.api.settingsStore,
77189
+ logger: this.ctx.logger.child("LoadSeries")
77190
+ });
75733
77191
  this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
75734
- this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), PROCESS_SNAPSHOT_INTERVAL_MS);
77192
+ this.startProcessSnapshotTimer();
77193
+ this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: require_dist10.EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes)));
75735
77194
  return [{
75736
77195
  capability: require_dist10.metricsProviderCapability,
75737
77196
  provider: composed
75738
77197
  }];
75739
77198
  }
77199
+ /**
77200
+ * Retain one arriving snapshot in BOTH tiers.
77201
+ *
77202
+ * The ring is written FIRST and it is the gate: `record` refuses a timestamp
77203
+ * this node has already delivered, and only an accepted sample reaches the
77204
+ * table. That is what makes the append idempotent without a unique index
77205
+ * over two million rows — the composite key it replaces was measured at ~40
77206
+ * bytes per row (`load-series-store.ts`).
77207
+ *
77208
+ * The durable append is fire-and-forget: a storage stall must cost a gap in
77209
+ * the cold window, never a blocked event-bus handler. Every failure logs.
77210
+ */
77211
+ retainSnapshot(nodeId, atMs, processes) {
77212
+ const retained = processes.map((p) => NativeMetricsAddon2.toRetained(p));
77213
+ if (!this.loadRing.record(nodeId, atMs, retained)) return;
77214
+ this.observedRowsByNode.set(nodeId, retained.length);
77215
+ const store = this.loadStore;
77216
+ if (store === null) return;
77217
+ store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).catch((err) => {
77218
+ this.ctx.logger.warn("durable load series write failed", { meta: {
77219
+ nodeId,
77220
+ atMs,
77221
+ error: err instanceof Error ? err.message : String(err)
77222
+ } });
77223
+ });
77224
+ }
77225
+ /**
77226
+ * The ONE reader, over both tiers.
77227
+ *
77228
+ * Cold first, hot second, merged and deduped on `atMs` — see
77229
+ * `load-series-reader.ts`. The cold read is bounded in ROWS, because rows
77230
+ * are what a query costs, and the fold + reduction happen HERE rather than
77231
+ * in the browser: six hours at the 10 s cadence is 2 160 snapshots, and
77232
+ * shipping them to a page that will discard most of them is precisely the
77233
+ * cost this subsystem exists to avoid.
77234
+ */
77235
+ async readLoadSeries(params) {
77236
+ const cadenceMs = this.loadConfig.cadenceSec * 1e3;
77237
+ const sinceMs = params.sinceMs ?? 0;
77238
+ const hotSamples = this.loadRing.read(params.forNodeId, params.sinceMs).samples.map((sample) => ({
77239
+ atMs: sample.atMs,
77240
+ processes: sample.processes
77241
+ }));
77242
+ const store = this.loadStore;
77243
+ const merged = mergeLoadSeries({
77244
+ cold: store === null ? [] : await store.read(params.forNodeId, sinceMs, this.loadConfig.maxRows),
77245
+ hot: hotSamples,
77246
+ cadenceMs,
77247
+ ...params.maxPoints !== void 0 ? { maxPoints: params.maxPoints } : {}
77248
+ });
77249
+ return {
77250
+ nodeId: params.forNodeId,
77251
+ series: merged.series,
77252
+ bucketMs: merged.bucketMs,
77253
+ retainedSamples: merged.retainedSamples,
77254
+ oldestAtMs: merged.oldestAtMs,
77255
+ cadenceMs,
77256
+ durable: store !== null
77257
+ };
77258
+ }
77259
+ /**
77260
+ * Re-resolve the knobs and restate the fixed cadence.
77261
+ *
77262
+ * A REFUSED value (out of 5-60 s, or out of 1-72 h) leaves the previous
77263
+ * configuration in force and says so. Refused, never clamped: storing 10
77264
+ * when the operator typed 2 and reading 10 back is a knob and a readback
77265
+ * agreeing on a value nobody chose.
77266
+ */
77267
+ applyLoadSeriesConfig() {
77268
+ try {
77269
+ const next = resolveLoadSeriesConfig({
77270
+ loadSeriesCadenceSec: this.config.loadSeriesCadenceSec,
77271
+ loadSeriesRetentionHours: this.config.loadSeriesRetentionHours
77272
+ });
77273
+ const changed = next.cadenceSec !== this.loadConfig.cadenceSec || next.retentionHours !== this.loadConfig.retentionHours;
77274
+ this.loadConfig = next;
77275
+ if (changed) {
77276
+ const projection = projectLoadSeriesCost({
77277
+ config: next,
77278
+ observedProcessRows: this.observedFleetRows()
77279
+ });
77280
+ this.ctx.logger.info("load series configuration applied", { meta: {
77281
+ nodeId: this.ctx.kernel.localNodeId ?? this.ctx.id,
77282
+ cadenceSec: next.cadenceSec,
77283
+ retentionHours: next.retentionHours,
77284
+ maxRows: next.maxRows,
77285
+ projectedRows: projection.intendedRows,
77286
+ projectedMib: Math.round(projection.estimatedBytes / 1048576 * 10) / 10,
77287
+ capBites: projection.capBites
77288
+ } });
77289
+ }
77290
+ } catch (err) {
77291
+ this.ctx.logger.warn("load series configuration REFUSED \u2014 keeping the previous values", { meta: {
77292
+ nodeId: this.ctx.kernel.localNodeId ?? this.ctx.id,
77293
+ cadenceSec: this.loadConfig.cadenceSec,
77294
+ retentionHours: this.loadConfig.retentionHours,
77295
+ error: err instanceof Error ? err.message : String(err)
77296
+ } });
77297
+ }
77298
+ }
77299
+ /** (Re)arm the fixed-cadence process-tree timer at the configured interval. */
77300
+ startProcessSnapshotTimer() {
77301
+ if (this.processSnapshotTimer) clearInterval(this.processSnapshotTimer);
77302
+ this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), this.loadConfig.cadenceSec * 1e3);
77303
+ }
75740
77304
  async onShutdown() {
75741
77305
  if (this.snapshotTimer) {
75742
77306
  clearInterval(this.snapshotTimer);
@@ -75793,10 +77357,14 @@ var require_native_metrics_addon = __commonJS({
75793
77357
  }
75794
77358
  }
75795
77359
  /**
75796
- * Emit one `metrics.node-processes-snapshot` for this node. Heavy —
75797
- * runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
75798
- * call. Fires on the coarser PROCESS_SNAPSHOT_INTERVAL_MS so an idle
75799
- * node isn't paying a process-table walk every 5s. Skip on failure.
77360
+ * Emit one `metrics.node-processes-snapshot` for this node.
77361
+ *
77362
+ * Heavy a full OS `ps -eo` scan plus a `$process.list` broker call — and
77363
+ * UNCONDITIONAL. The change-detection that used to guard it is gone: with a
77364
+ * fixed cadence a missing interval means exactly one thing, and that is the
77365
+ * property an operator investigating a spike is actually looking for.
77366
+ *
77367
+ * A failed scan emits nothing, which is the same signal: nobody reported.
75800
77368
  */
75801
77369
  async emitProcessesSnapshot() {
75802
77370
  const eventBus = this.ctx.eventBus;
@@ -75805,30 +77373,27 @@ var require_native_metrics_addon = __commonJS({
75805
77373
  const timestamp = Date.now();
75806
77374
  try {
75807
77375
  const processes = await this.listNodeProcesses();
75808
- const coarse = coarsenProcessList(processes);
75809
- const prev = this.lastProcessesEmit;
75810
- const heartbeatDue = !prev || timestamp - prev.emittedAt >= METRICS_SNAPSHOT_HEARTBEAT_MS;
75811
- if (!prev || prev.coarse !== coarse || heartbeatDue) {
75812
- this.lastProcessesEmit = {
75813
- coarse,
75814
- emittedAt: timestamp
75815
- };
75816
- eventBus.emit(require_dist10.createEvent(require_dist10.EventCategory.MetricsNodeProcessesSnapshot, {
75817
- type: "node",
75818
- id: nodeId,
75819
- nodeId
75820
- }, {
75821
- nodeId,
75822
- processes,
75823
- timestamp
75824
- }));
75825
- }
75826
- } catch {
77376
+ eventBus.emit(require_dist10.createEvent(require_dist10.EventCategory.MetricsNodeProcessesSnapshot, {
77377
+ type: "node",
77378
+ id: nodeId,
77379
+ nodeId
77380
+ }, {
77381
+ nodeId,
77382
+ processes,
77383
+ timestamp
77384
+ }));
77385
+ } catch (err) {
77386
+ this.ctx.logger.warn("process snapshot skipped \u2014 this interval will be missing", { meta: {
77387
+ nodeId,
77388
+ error: err instanceof Error ? err.message : String(err)
77389
+ } });
75827
77390
  }
75828
77391
  }
75829
77392
  async onConfigChanged() {
75830
77393
  this.provider?.stopSampling();
75831
77394
  this.provider?.startSampling(this.config.samplingIntervalMs);
77395
+ this.applyLoadSeriesConfig();
77396
+ this.startProcessSnapshotTimer();
75832
77397
  }
75833
77398
  async listWorkerInstances() {
75834
77399
  const broker = this.ctx.kernel.cluster?.broker;
@@ -75872,128 +77437,29 @@ var require_native_metrics_addon = __commonJS({
75872
77437
  /**
75873
77438
  * Walk the OS process table and classify each camstack-shaped process.
75874
77439
  *
75875
- * Classification (ancestry-driven, NOT pattern-driven):
75876
- * - root — the current node's own pid (`process.pid`).
75877
- * - managedpid is registered in the kernel's `$process.list`
75878
- * (forked addon worker spawned by this hub).
75879
- * - system — ancestry walk crosses a SUPERVISOR_BOUNDARY_RE match
75880
- * (tsx-watch launcher, agent CLI, concurrently, vite,
75881
- * npm exec wrapper). The process belongs to the dev
75882
- * tree even if not in `$process.list`. NEVER killable.
75883
- * - ghost — ancestry walk reaches `ppid=1` without crossing any
75884
- * supervisor boundary AND the parent isn't visible in
75885
- * `ps`. A truly orphaned camstack-shaped process. The
75886
- * ONLY classification that's eligible for kill.
75887
- *
75888
- * Old pattern-only ghost detection produced false positives: every
75889
- * monorepo-path process matched CAMSTACK_CMD_RE, ancestry walk
75890
- * stopping at ppid=hub returned false-positive ghosts whenever a
75891
- * concurrently sibling sat above hub. Ancestry-driven classification
75892
- * fixes that.
77440
+ * Classification is IDENTITY-driven: `root` is this pid, `managed` is a pid
77441
+ * the kernel's `$process.list` names, and every other camstack-shaped
77442
+ * process is `system`. The rules and why there is no longer an ancestry
77443
+ * walk behind them live in `process-classification.ts`.
75893
77444
  */
75894
77445
  async listNodeProcesses() {
75895
77446
  const ps = await this.runPs();
75896
77447
  if (ps.length === 0) return [];
75897
- const managedPids = /* @__PURE__ */ new Map();
77448
+ const managed = /* @__PURE__ */ new Map();
75898
77449
  const workers = await this.listWorkerInstances();
75899
- for (const w of workers) managedPids.set(w.pid, {
77450
+ for (const w of workers) managed.set(w.pid, {
75900
77451
  addonId: w.addonId,
75901
77452
  nodeId: w.nodeId
75902
77453
  });
75903
- const hubNodeId = this.ctx.kernel.cluster?.broker?.nodeID ?? "hub";
75904
- const selfPid = process.pid;
75905
- const psIndex = /* @__PURE__ */ new Map();
75906
- for (const p of ps) psIndex.set(p.pid, {
75907
- ppid: p.ppid,
75908
- command: p.command
77454
+ const camstackPids = ps.filter((p) => CAMSTACK_CMD_RE.test(p.command)).map((p) => p.pid);
77455
+ const cpuSplits = await this.threadCpu.sample(camstackPids);
77456
+ return buildNodeProcesses({
77457
+ psRows: ps,
77458
+ selfPid: process.pid,
77459
+ selfNodeId: this.ctx.kernel.cluster?.broker?.nodeID ?? "hub",
77460
+ managed,
77461
+ cpuSplits
75909
77462
  });
75910
- const classifyByAncestry = (startPid) => {
75911
- let cur = startPid;
75912
- for (let depth = 0; depth < 32; depth++) {
75913
- const node = psIndex.get(cur);
75914
- if (!node) return "ghost";
75915
- if (cur === selfPid) return "system";
75916
- if (SUPERVISOR_BOUNDARY_RE.test(node.command)) return "system";
75917
- if (node.ppid === 1) return "ghost";
75918
- if (node.ppid === selfPid) return "system";
75919
- cur = node.ppid;
75920
- }
75921
- return "system";
75922
- };
75923
- const out = [];
75924
- for (const p of ps) {
75925
- if (!CAMSTACK_CMD_RE.test(p.command)) continue;
75926
- const managed = managedPids.get(p.pid);
75927
- let classification;
75928
- if (p.pid === selfPid) classification = "root";
75929
- else if (managed) classification = "managed";
75930
- else classification = classifyByAncestry(p.pid);
75931
- const orphaned = classification === "ghost";
75932
- out.push({
75933
- pid: p.pid,
75934
- ppid: p.ppid,
75935
- pgid: p.pgid,
75936
- classification,
75937
- addonId: managed?.addonId ?? null,
75938
- nodeId: managed?.nodeId ?? (p.pid === selfPid ? hubNodeId : null),
75939
- command: p.command,
75940
- cpuPercent: p.cpuPercent,
75941
- memoryRssBytes: p.memoryRssBytes,
75942
- uptimeSec: p.uptimeSec,
75943
- orphaned
75944
- });
75945
- }
75946
- return out;
75947
- }
75948
- /**
75949
- * Send SIGTERM / SIGKILL to a pid. Refuses pids that don't appear in
75950
- * `listNodeProcesses()` to prevent arbitrary system kills — a dedicated
75951
- * admin-path for resurrected zombies, not a generic shell replacement.
75952
- *
75953
- * `root`-classified pids (the running launcher / agent CLI / hub itself)
75954
- * are also refused: killing them tears down the whole node and the
75955
- * operator's intent is almost always to nuke a leaked child, not the
75956
- * supervisor that keeps the rest alive. Process restart goes through
75957
- * the dedicated `$process.restart` action, not this kill API.
75958
- */
75959
- async killProcess(input) {
75960
- const match = (await this.listNodeProcesses()).find((p) => p.pid === input.pid);
75961
- if (!match) return {
75962
- success: false,
75963
- reason: "pid not in node process table"
75964
- };
75965
- if (match.classification === "root" || match.classification === "system") {
75966
- this.ctx.logger.warn("Refused to kill protected process", { meta: {
75967
- pid: input.pid,
75968
- classification: match.classification,
75969
- addonId: match.addonId,
75970
- command: match.command
75971
- } });
75972
- return {
75973
- success: false,
75974
- reason: match.classification === "root" ? "cannot kill root (current node supervisor)" : "cannot kill system (intentional dev-tree ancestor \u2014 vite, concurrently, npm, etc.)"
75975
- };
75976
- }
75977
- const signal = input.force ? "SIGKILL" : "SIGTERM";
75978
- try {
75979
- process.kill(input.pid, signal);
75980
- this.ctx.logger.info("Killed node process", { meta: {
75981
- pid: input.pid,
75982
- signal,
75983
- classification: match.classification,
75984
- addonId: match.addonId
75985
- } });
75986
- return {
75987
- success: true,
75988
- signal
75989
- };
75990
- } catch (err) {
75991
- return {
75992
- success: false,
75993
- reason: err instanceof Error ? err.message : String(err),
75994
- signal
75995
- };
75996
- }
75997
77463
  }
75998
77464
  /**
75999
77465
  * Ask the addon's forked runner to write a V8 heap snapshot (SIGUSR2 → the
@@ -76070,7 +77536,29 @@ var require_native_metrics_addon = __commonJS({
76070
77536
  return [];
76071
77537
  }
76072
77538
  }
77539
+ /**
77540
+ * The knobs live HERE, on the document this addon already owns.
77541
+ *
77542
+ * Fields on an existing document, never a method per knob —
77543
+ * `system.getLoggingSettings` / `setLoggingSettings` set that precedent.
77544
+ * They are deliberately NOT in the logging document: that one is about
77545
+ * levels and diagnostic windows, and two documents both claiming a knob is
77546
+ * how this repo has already shipped a switch nobody read.
77547
+ *
77548
+ * Cluster-wide, not per-node: every node must emit on the same cadence or
77549
+ * the fleet's series cannot be laid over each other, and the table is single
77550
+ * and hub-resident, so a per-node retention would be a promise nothing could
77551
+ * keep.
77552
+ *
77553
+ * The cost line is computed from OBSERVED numbers — the nodes and process
77554
+ * counts the cluster is actually reporting — so an operator raising the
77555
+ * retention sees what it costs BEFORE applying it, not afterwards.
77556
+ */
76073
77557
  globalSettingsSchema() {
77558
+ const projection = projectLoadSeriesCost({
77559
+ config: this.loadConfig,
77560
+ observedProcessRows: this.observedFleetRows()
77561
+ });
76074
77562
  return this.schema({ sections: [{
76075
77563
  id: "native-metrics-settings",
76076
77564
  title: "System Metrics",
@@ -76085,6 +77573,47 @@ var require_native_metrics_addon = __commonJS({
76085
77573
  default: 5e3,
76086
77574
  unit: "ms"
76087
77575
  })]
77576
+ }, {
77577
+ id: "native-metrics-load-series",
77578
+ title: "Load history",
77579
+ fields: [
77580
+ this.field({
77581
+ type: "number",
77582
+ key: "loadSeriesCadenceSec",
77583
+ label: "Sampling cadence",
77584
+ description: "How often every node reports its process tree. FIXED \u2014 a sample is emitted on every interval whether or not anything changed, so a gap in the chart means one thing only: nobody reported. Outside 5-60 s the value is refused.",
77585
+ min: 5,
77586
+ max: 60,
77587
+ step: 1,
77588
+ default: 10,
77589
+ unit: "s"
77590
+ }),
77591
+ this.field({
77592
+ type: "number",
77593
+ key: "loadSeriesRetentionHours",
77594
+ label: "Retention",
77595
+ description: "How long the load history is kept on disk (the hub database, on the NVMe cache \u2014 never the recordings disk). This is the INTENTION; the row cap below is the guarantee.",
77596
+ min: 1,
77597
+ max: 72,
77598
+ step: 1,
77599
+ default: 6,
77600
+ unit: "h"
77601
+ }),
77602
+ {
77603
+ type: "info",
77604
+ key: "load-series-cost",
77605
+ label: "What this configuration costs",
77606
+ content: describeLoadSeriesCost(projection),
77607
+ variant: projection.capBites ? "warning" : "info"
77608
+ },
77609
+ {
77610
+ type: "info",
77611
+ key: "load-series-cap",
77612
+ label: "Hard row cap",
77613
+ content: `The table never exceeds ${this.loadConfig.maxRows.toLocaleString("en-US")} rows. When it would, the OLDEST samples are evicted and a warning is logged \u2014 so raising the retention can never turn into an incident, only into a shorter effective window.`,
77614
+ variant: "info"
77615
+ }
77616
+ ]
76088
77617
  }] });
76089
77618
  }
76090
77619
  };
@@ -76117,7 +77646,7 @@ var require_filesystem_storage_addon = __commonJS({
76117
77646
  [Symbol.toStringTag]: { value: "Module" }
76118
77647
  });
76119
77648
  var require_chunk = require_chunk_Cek0wNdY();
76120
- var require_dist10 = require_dist_BVU5JADq();
77649
+ var require_dist10 = require_dist_Dl6MFXPr();
76121
77650
  var node_crypto = __require("crypto");
76122
77651
  var node_fs_promises = __require("fs/promises");
76123
77652
  var node_path = __require("path");
@@ -77233,8 +78762,8 @@ var require_sqlite_settings_addon = __commonJS({
77233
78762
  [Symbol.toStringTag]: { value: "Module" }
77234
78763
  });
77235
78764
  var require_chunk = require_chunk_Cek0wNdY();
77236
- var require_dist10 = require_dist_BVU5JADq();
77237
- var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
78765
+ var require_dist10 = require_dist_Dl6MFXPr();
78766
+ var require_retired_settings_keys = require_retired_settings_keys_6w_JOqBg();
77238
78767
  var node_crypto = __require("crypto");
77239
78768
  var node_fs = __require("fs");
77240
78769
  var node_module = __require("module");
@@ -77861,7 +79390,8 @@ var require_sqlite_settings_addon = __commonJS({
77861
79390
  this.declaredCollections.set(collection, {
77862
79391
  primaryKey: "id",
77863
79392
  columns: /* @__PURE__ */ new Set(["data"]),
77864
- booleanColumns: /* @__PURE__ */ new Set()
79393
+ booleanColumns: /* @__PURE__ */ new Set(),
79394
+ autoPrimaryKey: false
77865
79395
  });
77866
79396
  }
77867
79397
  if (await this.isEmpty({ collection: "system-settings" })) await this.seedDefaults();
@@ -77877,7 +79407,8 @@ var require_sqlite_settings_addon = __commonJS({
77877
79407
  const decl = {
77878
79408
  primaryKey: "id",
77879
79409
  columns: /* @__PURE__ */ new Set(["data"]),
77880
- booleanColumns: /* @__PURE__ */ new Set()
79410
+ booleanColumns: /* @__PURE__ */ new Set(),
79411
+ autoPrimaryKey: false
77881
79412
  };
77882
79413
  this.declaredCollections.set(scoped, decl);
77883
79414
  return decl;
@@ -77981,6 +79512,48 @@ var require_sqlite_settings_addon = __commonJS({
77981
79512
  else for (const [k, v] of Object.entries(record.data)) if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
77982
79513
  await this.tableInsert(scoped, row);
77983
79514
  }
79515
+ /**
79516
+ * Insert a batch in ONE transaction, on ONE prepared statement.
79517
+ *
79518
+ * The write-side twin of {@link deleteWhere}. Every row in the batch shares
79519
+ * a single column list, which is what makes one `prepare` legal: rows are
79520
+ * normalised to the collection's DECLARED column set, so a row that omits a
79521
+ * column binds `null` for it rather than producing a second statement shape.
79522
+ *
79523
+ * All or nothing — `better-sqlite3`'s `transaction()` rolls the whole batch
79524
+ * back on any throw. A caller writing one process sample gets one COMMIT,
79525
+ * which is the entire point: 76 rows every 10 s is 7.6 rows/s, and 7.6
79526
+ * separate commits per second on the connection that also serves every
79527
+ * cluster-wide configuration read is a constant load nobody asked for.
79528
+ */
79529
+ async insertMany({ namespace, collection, records }) {
79530
+ if (records.length === 0) return { inserted: 0 };
79531
+ const scoped = this.scopedName(namespace, collection);
79532
+ const decl = this.requireDeclared(scoped);
79533
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
79534
+ const dataColumns = [...decl.columns];
79535
+ const keys = decl.autoPrimaryKey ? dataColumns : [decl.primaryKey, ...dataColumns];
79536
+ const sql = `INSERT INTO "${scoped}" (${keys.map((k) => `"${k}"`).join(", ")}) VALUES (${keys.map(() => "?").join(", ")})`;
79537
+ const batch = records.map((record) => {
79538
+ const row = {};
79539
+ if (isKvShape) row["data"] = JSON.stringify(record.data);
79540
+ else for (const [k, v] of Object.entries(record.data)) if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
79541
+ if (!decl.autoPrimaryKey) row[decl.primaryKey] = record.id || (0, node_crypto.randomUUID)();
79542
+ return keys.map((k) => row[k] ?? null);
79543
+ });
79544
+ const db = this.getDb();
79545
+ const stmt = db.prepare(sql);
79546
+ const run = db.transaction((rows) => {
79547
+ for (const row of rows) stmt.run(...row);
79548
+ });
79549
+ this.measured({
79550
+ op: "insertMany",
79551
+ collection: scoped,
79552
+ sql,
79553
+ params: batch[0] ?? []
79554
+ }, () => run(batch));
79555
+ return { inserted: batch.length };
79556
+ }
77984
79557
  async update({ namespace, collection, id, data }) {
77985
79558
  const scoped = this.scopedName(namespace, collection);
77986
79559
  const decl = this.requireDeclared(scoped);
@@ -78655,7 +80228,8 @@ var require_sqlite_settings_addon = __commonJS({
78655
80228
  this.declaredCollections.set(table, {
78656
80229
  primaryKey,
78657
80230
  columns: columnNames,
78658
- booleanColumns
80231
+ booleanColumns,
80232
+ autoPrimaryKey: pkCol?.type === "INTEGER"
78659
80233
  });
78660
80234
  }
78661
80235
  /** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
@@ -79468,7 +81042,7 @@ var require_storage_orchestrator_addon = __commonJS({
79468
81042
  [Symbol.toStringTag]: { value: "Module" }
79469
81043
  });
79470
81044
  var require_chunk = require_chunk_Cek0wNdY();
79471
- var require_dist10 = require_dist_BVU5JADq();
81045
+ var require_dist10 = require_dist_Dl6MFXPr();
79472
81046
  var node_crypto = __require("crypto");
79473
81047
  var node_fs_promises = __require("fs/promises");
79474
81048
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -80032,6 +81606,7 @@ var require_storage_orchestrator_addon = __commonJS({
80032
81606
  set: async (input) => (await engine()).set(input),
80033
81607
  query: async (input) => (await engine()).query(input),
80034
81608
  insert: async (input) => (await engine()).insert(input),
81609
+ insertMany: async (input) => (await engine()).insertMany(input),
80035
81610
  update: async (input) => (await engine()).update(input),
80036
81611
  delete: async (input) => (await engine()).delete(input),
80037
81612
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
@@ -81348,7 +82923,7 @@ var require_system_config_addon = __commonJS({
81348
82923
  [Symbol.toStringTag]: { value: "Module" }
81349
82924
  });
81350
82925
  require_chunk_Cek0wNdY();
81351
- var require_dist10 = require_dist_BVU5JADq();
82926
+ var require_dist10 = require_dist_Dl6MFXPr();
81352
82927
  var SECTION_TITLES = {
81353
82928
  server: "Server",
81354
82929
  auth: "Authentication"
@@ -99409,7 +100984,7 @@ var require_winston_logging = __commonJS({
99409
100984
  [Symbol.toStringTag]: { value: "Module" }
99410
100985
  });
99411
100986
  var require_chunk = require_chunk_Cek0wNdY();
99412
- var require_dist10 = require_dist_BVU5JADq();
100987
+ var require_dist10 = require_dist_Dl6MFXPr();
99413
100988
  var require_formatter = require_formatter_DqAKDlvN();
99414
100989
  var node_path = __require("path");
99415
100990
  node_path = require_chunk.__toESM(node_path);
@@ -101185,9 +102760,9 @@ ${(0, node_fs.readFileSync)(ca.caCertPath, "utf-8").trimEnd()}
101185
102760
  }
101186
102761
  });
101187
102762
 
101188
- // ../types/dist/event-category-EY0GNjV9.js
101189
- var require_event_category_EY0GNjV9 = __commonJS({
101190
- "../types/dist/event-category-EY0GNjV9.js"(exports) {
102763
+ // ../types/dist/event-category-BaEgqJNv.js
102764
+ var require_event_category_BaEgqJNv = __commonJS({
102765
+ "../types/dist/event-category-BaEgqJNv.js"(exports) {
101191
102766
  "use strict";
101192
102767
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
101193
102768
  EventCategory2["SystemBoot"] = "system.boot";
@@ -101352,11 +102927,11 @@ var require_event_category_EY0GNjV9 = __commonJS({
101352
102927
  }
101353
102928
  });
101354
102929
 
101355
- // ../types/dist/sleep-CSodb2vQ.js
101356
- var require_sleep_CSodb2vQ = __commonJS({
101357
- "../types/dist/sleep-CSodb2vQ.js"(exports) {
102930
+ // ../types/dist/sleep-9d8tJRbO.js
102931
+ var require_sleep_9d8tJRbO = __commonJS({
102932
+ "../types/dist/sleep-9d8tJRbO.js"(exports) {
101358
102933
  "use strict";
101359
- var require_event_category = require_event_category_EY0GNjV9();
102934
+ var require_event_category = require_event_category_BaEgqJNv();
101360
102935
  var zod = require_zod();
101361
102936
  var WELL_KNOWN_TABS = [
101362
102937
  {
@@ -104907,8 +106482,8 @@ var require_addon = __commonJS({
104907
106482
  "../types/dist/addon.js"(exports) {
104908
106483
  "use strict";
104909
106484
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
104910
- var require_event_category = require_event_category_EY0GNjV9();
104911
- var require_sleep = require_sleep_CSodb2vQ();
106485
+ var require_event_category = require_event_category_BaEgqJNv();
106486
+ var require_sleep = require_sleep_9d8tJRbO();
104912
106487
  var require_err_msg = require_err_msg_COpsHMw2();
104913
106488
  var CAP_INPUT_DEFAULTS = Object.freeze({
104914
106489
  "addons": { "getLogs": { "limit": 100 } },
@@ -105197,6 +106772,7 @@ var require_addon = __commonJS({
105197
106772
  "listProfiles",
105198
106773
  "listRuntimeNodes"
105199
106774
  ],
106775
+ "load-contribution": ["list"],
105200
106776
  "log-channels": ["list"],
105201
106777
  "log-destination": ["query"],
105202
106778
  "login-method": ["getLoginMethods"],
@@ -111785,12 +113361,12 @@ var require_dist2 = __commonJS({
111785
113361
  }
111786
113362
  });
111787
113363
 
111788
- // ../system/dist/manifest-python-deps-GjlyPjm0.js
111789
- var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111790
- "../system/dist/manifest-python-deps-GjlyPjm0.js"(exports) {
113364
+ // ../system/dist/manifest-python-deps-FYZBHc3v.js
113365
+ var require_manifest_python_deps_FYZBHc3v = __commonJS({
113366
+ "../system/dist/manifest-python-deps-FYZBHc3v.js"(exports) {
111791
113367
  "use strict";
111792
113368
  var require_chunk = require_chunk_Cek0wNdY();
111793
- require_dist_BVU5JADq();
113369
+ require_dist_Dl6MFXPr();
111794
113370
  var node_crypto = __require("crypto");
111795
113371
  node_crypto = require_chunk.__toESM(node_crypto);
111796
113372
  var _camstack_types_node = require_node();
@@ -111881,6 +113457,34 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111881
113457
  function shouldReclaim(s, triggerMb = HEAP_RECLAIM_TRIGGER_MB) {
111882
113458
  return strandedMb(s) > triggerMb;
111883
113459
  }
113460
+ function share(partMb, rssMb) {
113461
+ return rssMb > 0 ? Math.round(partMb / rssMb * 100) : 0;
113462
+ }
113463
+ function describeRss(s, budgetMb) {
113464
+ const nativeResidueMb = Math.max(0, strandedMb(s));
113465
+ const shape = s.heapUsedMb >= s.externalMb && s.heapUsedMb >= nativeResidueMb ? "v8-heap" : s.externalMb >= nativeResidueMb ? "external" : "native-residue";
113466
+ return {
113467
+ rssMb: s.rssMb,
113468
+ budgetMb,
113469
+ overMb: Math.max(0, s.rssMb - budgetMb),
113470
+ heapUsedMb: s.heapUsedMb,
113471
+ externalMb: s.externalMb,
113472
+ nativeResidueMb,
113473
+ arrayBuffersMb: s.arrayBuffersMb,
113474
+ shape
113475
+ };
113476
+ }
113477
+ var SHAPE_ADVICE = {
113478
+ "v8-heap": "live JS retainers \u2014 bounded by --max-old-space-size; the fix is the retained set",
113479
+ external: "Buffers / native external \u2014 NO V8 flag bounds this, and the old-space ceiling never will",
113480
+ "native-residue": "freed native memory the allocator has not returned \u2014 MALLOC_ARENA_MAX/VIPS_CONCURRENCY are already applied, so this lever is spent"
113481
+ };
113482
+ var RSS_BUDGET_RELEASE_RATIO = 0.9;
113483
+ var RSS_BUDGET_REANNOUNCE_MIN_MS = 9e5;
113484
+ function nextRssBudgetState(current, rssMb, budgetMb, releaseRatio = RSS_BUDGET_RELEASE_RATIO) {
113485
+ if (current === "over") return rssMb < budgetMb * releaseRatio ? "within" : "over";
113486
+ return rssMb > budgetMb ? "over" : "within";
113487
+ }
111884
113488
  function isGcFunction(value) {
111885
113489
  return typeof value === "function";
111886
113490
  }
@@ -111898,12 +113502,15 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111898
113502
  return;
111899
113503
  }
111900
113504
  }
111901
- function format2(label, s, loop) {
111902
- const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
113505
+ function formatOverBudget(label, b, source) {
113506
+ return `[mem] ${label} OVER RSS BUDGET rss=${b.rssMb}MB budget=${b.budgetMb}MB over=+${b.overMb}MB shape=${b.shape} heapUsed=${b.heapUsedMb}MB(${share(b.heapUsedMb, b.rssMb)}%) external=${b.externalMb}MB(${share(b.externalMb, b.rssMb)}%) nativeResidue=${b.nativeResidueMb}MB(${share(b.nativeResidueMb, b.rssMb)}%) arrayBuffers=${b.arrayBuffersMb}MB budgetFrom="${source}" \u2014 ${SHAPE_ADVICE[b.shape]}`;
113507
+ }
113508
+ function format2(label, s, loop, budgetMb) {
113509
+ const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB` + (budgetMb === void 0 ? "" : ` rssBudget=${budgetMb}MB`);
111903
113510
  if (loop === void 0) return line;
111904
113511
  return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms`;
111905
113512
  }
111906
- function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true) {
113513
+ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true, rssBudget) {
111907
113514
  const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
111908
113515
  const now = reclaimOptions?.now ?? (() => Date.now());
111909
113516
  const triggerMb = reclaimOptions?.triggerMb ?? 1024;
@@ -111945,6 +113552,29 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111945
113552
  };
111946
113553
  let mode = "steady";
111947
113554
  let lastLoggedAt = Number.NEGATIVE_INFINITY;
113555
+ const budgetMb = rssBudget?.budgetMb !== void 0 && rssBudget.budgetMb > 0 ? rssBudget.budgetMb : void 0;
113556
+ const budgetSource = rssBudget?.source ?? "unspecified";
113557
+ const releaseRatio = rssBudget?.releaseRatio ?? 0.9;
113558
+ const reannounceMinMs = rssBudget?.reannounceMinMs ?? 9e5;
113559
+ let budgetState = "within";
113560
+ let lastBudgetWarnAt = Number.NEGATIVE_INFINITY;
113561
+ let overBudgetAnnounced = false;
113562
+ const checkRssBudget = (sample, at) => {
113563
+ if (budgetMb === void 0) return;
113564
+ const previous = budgetState;
113565
+ budgetState = nextRssBudgetState(previous, sample.rssMb, budgetMb, releaseRatio);
113566
+ if (budgetState === previous) return;
113567
+ if (budgetState === "over") {
113568
+ if (at - lastBudgetWarnAt < reannounceMinMs) return;
113569
+ lastBudgetWarnAt = at;
113570
+ overBudgetAnnounced = true;
113571
+ sink.warn(formatOverBudget(label, describeRss(sample, budgetMb), budgetSource));
113572
+ return;
113573
+ }
113574
+ if (!overBudgetAnnounced) return;
113575
+ overBudgetAnnounced = false;
113576
+ sink.info(`[mem] ${label} back within its RSS budget \u2014 rss=${sample.rssMb}MB budget=${budgetMb}MB`);
113577
+ };
111948
113578
  const probeIntervalMs = Math.min(fastIntervalMs, intervalMs);
111949
113579
  const tick = () => {
111950
113580
  try {
@@ -111955,12 +113585,13 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111955
113585
  const due = at - lastLoggedAt >= intervalMs;
111956
113586
  if (mode === "escalated" || due) {
111957
113587
  lastLoggedAt = at;
111958
- const line = format2(label, sample, loopDelay?.read());
113588
+ const line = format2(label, sample, loopDelay?.read(), budgetMb);
111959
113589
  if (sample.nearLimit) sink.warn(`${line} \u2014 APPROACHING HEAP LIMIT`);
111960
113590
  else if (mode === "escalated") sink.warn(`${line} \u2014 heap elevated, sampling every ${probeIntervalMs}ms`);
111961
113591
  else sink.info(line);
111962
113592
  }
111963
113593
  if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine \u2014 logging every ${intervalMs}ms`);
113594
+ checkRssBudget(sample, at);
111964
113595
  maybeReclaim(sample);
111965
113596
  } catch {
111966
113597
  }
@@ -111968,6 +113599,7 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111968
113599
  const timer = setInterval(tick, probeIntervalMs);
111969
113600
  timer.unref?.();
111970
113601
  tick();
113602
+ if (rssBudget !== void 0 && budgetMb === void 0) sink.info(`[mem] ${label} is NOT WATCHED against an RSS budget \u2014 no budget declared (${budgetSource}). rss growth outside the V8 heap raises nothing here: --max-old-space-size bounds old space only, and stranded subtracts external by construction. Declare execution.rssBudgetMb once this process has a measured working set.`);
111971
113603
  if (announceCeilingOrigin && heapCeilingOrigin(execArgv) === "v8-default") sink.info(`[mem] ${label} heap ceiling is V8's DEFAULT (${read().heapLimitMb}MB) \u2014 no --max-old-space-size on argv. Nothing here CHOSE that number; it is derived from host RAM and moves with it.`);
111972
113604
  let stopped = false;
111973
113605
  return () => {
@@ -111978,16 +113610,40 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111978
113610
  };
111979
113611
  }
111980
113612
  var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
113613
+ function parseRssBudgetMb(raw) {
113614
+ if (raw === void 0) return void 0;
113615
+ const parsed = Number(raw);
113616
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
113617
+ return Math.floor(parsed);
113618
+ }
113619
+ var RUNNER_RSS_BUDGET_ENV = "CAMSTACK_RUNNER_RSS_BUDGET_MB";
113620
+ var HUB_RSS_BUDGET_ENV = "CAMSTACK_HUB_RSS_BUDGET_MB";
113621
+ var HUB_MAIN_RSS_BUDGET_MB = 4096;
113622
+ function hubMainRssBudget(env = process.env) {
113623
+ const override = env[HUB_RSS_BUDGET_ENV];
113624
+ if (override !== void 0) return {
113625
+ budgetMb: parseRssBudgetMb(override),
113626
+ source: HUB_RSS_BUDGET_ENV
113627
+ };
113628
+ return {
113629
+ budgetMb: HUB_MAIN_RSS_BUDGET_MB,
113630
+ source: "HUB_MAIN_RSS_BUDGET_MB (kernel/heap-watch)"
113631
+ };
113632
+ }
111981
113633
  function startRunnerHeapWatch(options) {
111982
113634
  if (options.heapProfile !== "heavy") return void 0;
111983
113635
  const intervalMs = options.intervalMs ?? 3e5;
111984
- if (options.reclaimSwitch === "off") return startHeapWatch(options.label, options.sink, intervalMs, void 0, void 0, void 0, false);
113636
+ const rssBudget = {
113637
+ budgetMb: parseRssBudgetMb(options.rssBudgetMb),
113638
+ source: `manifest execution.rssBudgetMb (via ${RUNNER_RSS_BUDGET_ENV})`
113639
+ };
113640
+ if (options.reclaimSwitch === "off") return startHeapWatch(options.label, options.sink, intervalMs, void 0, void 0, void 0, false, rssBudget);
111985
113641
  let reclaimOptions = options.reclaimOptions;
111986
113642
  if (reclaimOptions === void 0) {
111987
113643
  const reclaimer = createV8Reclaimer();
111988
113644
  reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
111989
113645
  }
111990
- return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
113646
+ return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true, rssBudget);
111991
113647
  }
111992
113648
  function trimSlashes(s) {
111993
113649
  return s.replace(/^\/+/, "").replace(/\/+$/, "");
@@ -118543,6 +120199,18 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
118543
120199
  return HUB_CAP_FWD_SERVICE;
118544
120200
  }
118545
120201
  });
120202
+ Object.defineProperty(exports, "HUB_MAIN_RSS_BUDGET_MB", {
120203
+ enumerable: true,
120204
+ get: function() {
120205
+ return HUB_MAIN_RSS_BUDGET_MB;
120206
+ }
120207
+ });
120208
+ Object.defineProperty(exports, "HUB_RSS_BUDGET_ENV", {
120209
+ enumerable: true,
120210
+ get: function() {
120211
+ return HUB_RSS_BUDGET_ENV;
120212
+ }
120213
+ });
118546
120214
  Object.defineProperty(exports, "LocalChildClient", {
118547
120215
  enumerable: true,
118548
120216
  get: function() {
@@ -118561,12 +120229,30 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
118561
120229
  return NATIVE_PROVIDER_SERVICE_INFIX;
118562
120230
  }
118563
120231
  });
120232
+ Object.defineProperty(exports, "RSS_BUDGET_REANNOUNCE_MIN_MS", {
120233
+ enumerable: true,
120234
+ get: function() {
120235
+ return RSS_BUDGET_REANNOUNCE_MIN_MS;
120236
+ }
120237
+ });
120238
+ Object.defineProperty(exports, "RSS_BUDGET_RELEASE_RATIO", {
120239
+ enumerable: true,
120240
+ get: function() {
120241
+ return RSS_BUDGET_RELEASE_RATIO;
120242
+ }
120243
+ });
118564
120244
  Object.defineProperty(exports, "RUNNER_HEAP_WATCH_INTERVAL_MS", {
118565
120245
  enumerable: true,
118566
120246
  get: function() {
118567
120247
  return RUNNER_HEAP_WATCH_INTERVAL_MS;
118568
120248
  }
118569
120249
  });
120250
+ Object.defineProperty(exports, "RUNNER_RSS_BUDGET_ENV", {
120251
+ enumerable: true,
120252
+ get: function() {
120253
+ return RUNNER_RSS_BUDGET_ENV;
120254
+ }
120255
+ });
118570
120256
  Object.defineProperty(exports, "SocketChannel", {
118571
120257
  enumerable: true,
118572
120258
  get: function() {
@@ -118771,6 +120457,12 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
118771
120457
  return createV8Reclaimer;
118772
120458
  }
118773
120459
  });
120460
+ Object.defineProperty(exports, "describeRss", {
120461
+ enumerable: true,
120462
+ get: function() {
120463
+ return describeRss;
120464
+ }
120465
+ });
118774
120466
  Object.defineProperty(exports, "deserializeTypedArrays", {
118775
120467
  enumerable: true,
118776
120468
  get: function() {
@@ -118831,6 +120523,12 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
118831
120523
  return getWorkerNativeCapSnapshot;
118832
120524
  }
118833
120525
  });
120526
+ Object.defineProperty(exports, "hubMainRssBudget", {
120527
+ enumerable: true,
120528
+ get: function() {
120529
+ return hubMainRssBudget;
120530
+ }
120531
+ });
118834
120532
  Object.defineProperty(exports, "installManifestNativeDeps", {
118835
120533
  enumerable: true,
118836
120534
  get: function() {
@@ -118867,12 +120565,24 @@ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
118867
120565
  return mountNativeCapService;
118868
120566
  }
118869
120567
  });
120568
+ Object.defineProperty(exports, "nextRssBudgetState", {
120569
+ enumerable: true,
120570
+ get: function() {
120571
+ return nextRssBudgetState;
120572
+ }
120573
+ });
118870
120574
  Object.defineProperty(exports, "parseCapAction", {
118871
120575
  enumerable: true,
118872
120576
  get: function() {
118873
120577
  return parseCapAction;
118874
120578
  }
118875
120579
  });
120580
+ Object.defineProperty(exports, "parseRssBudgetMb", {
120581
+ enumerable: true,
120582
+ get: function() {
120583
+ return parseRssBudgetMb;
120584
+ }
120585
+ });
118876
120586
  Object.defineProperty(exports, "registerEventBusService", {
118877
120587
  enumerable: true,
118878
120588
  get: function() {
@@ -122771,7 +124481,7 @@ var require_dist3 = __commonJS({
122771
124481
  "use strict";
122772
124482
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
122773
124483
  var require_chunk = require_chunk_Cek0wNdY();
122774
- var require_dist10 = require_dist_BVU5JADq();
124484
+ var require_dist10 = require_dist_Dl6MFXPr();
122775
124485
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
122776
124486
  require_alerts();
122777
124487
  var require_formatter = require_formatter_DqAKDlvN();
@@ -122797,7 +124507,7 @@ var require_dist3 = __commonJS({
122797
124507
  var require_builtins_winston_logging_index = require_winston_logging();
122798
124508
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
122799
124509
  var require_tls$1 = require_tls_u8QCJCFE();
122800
- var require_manifest_python_deps = require_manifest_python_deps_GjlyPjm0();
124510
+ var require_manifest_python_deps = require_manifest_python_deps_FYZBHc3v();
122801
124511
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
122802
124512
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
122803
124513
  var zod = require_zod();
@@ -202263,7 +203973,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202263
203973
  }
202264
203974
  var EMPTY_HEAP_DECLARATION = {
202265
203975
  profile: void 0,
202266
- maxOldSpaceMb: void 0
203976
+ maxOldSpaceMb: void 0,
203977
+ rssBudgetMb: void 0
202267
203978
  };
202268
203979
  var heapProfileCache = /* @__PURE__ */ new Map();
202269
203980
  function readAddonHeapDeclaration(spec) {
@@ -202276,10 +203987,12 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202276
203987
  const parsed = JSON.parse(raw);
202277
203988
  const profile = extractHeapProfile(parsed, spec.addonId);
202278
203989
  const maxOldSpaceMb = extractMaxOldSpaceMb(parsed, spec.addonId);
202279
- if (profile !== void 0 || maxOldSpaceMb !== void 0) {
203990
+ const rssBudgetMb = extractRssBudgetMb(parsed, spec.addonId);
203991
+ if (profile !== void 0 || maxOldSpaceMb !== void 0 || rssBudgetMb !== void 0) {
202280
203992
  declaration = {
202281
203993
  profile,
202282
- maxOldSpaceMb
203994
+ maxOldSpaceMb,
203995
+ rssBudgetMb
202283
203996
  };
202284
203997
  break;
202285
203998
  }
@@ -202303,6 +204016,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202303
204016
  const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.maxOldSpaceMb;
202304
204017
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
202305
204018
  }
204019
+ function extractRssBudgetMb(parsed, addonId) {
204020
+ const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.rssBudgetMb;
204021
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
204022
+ }
202306
204023
  function readManifestAddons(parsed) {
202307
204024
  if (typeof parsed !== "object" || parsed === null) return [];
202308
204025
  const camstack = parsed.camstack;
@@ -202321,6 +204038,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202321
204038
  if (declared.includes(0)) return 0;
202322
204039
  return Math.max(...declared);
202323
204040
  }
204041
+ function runnerRssBudgetMb(addons) {
204042
+ const declared = addons.map((a) => readAddonHeapDeclaration(a).rssBudgetMb).filter((mb) => mb !== void 0);
204043
+ return declared.length === 0 ? void 0 : Math.max(...declared);
204044
+ }
202324
204045
  function runnerHeapFlags(addons) {
202325
204046
  if (process.env["CAMSTACK_RUNNER_HEAP_TUNING"] === "off") return [];
202326
204047
  const heavy = isHeavyRunner(addons);
@@ -202377,6 +204098,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202377
204098
  const nodeId = buildNodeId(runnerId);
202378
204099
  const runnerPath = node_path.resolve(__dirname, "addon-runner.js");
202379
204100
  const heavy = isHeavyRunner(addons);
204101
+ const rssBudgetMb = runnerRssBudgetMb(addons);
202380
204102
  const childEnv = {
202381
204103
  ...process.env,
202382
204104
  CAMSTACK_RUNNER_ID: runnerId,
@@ -202390,6 +204112,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202390
204112
  ...applyRunnerNativeAllocator(process.env),
202391
204113
  ...env
202392
204114
  };
204115
+ if (rssBudgetMb === void 0) delete childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV];
204116
+ else childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV] = String(rssBudgetMb);
202393
204117
  const heapFlags = runnerHeapFlags(addons);
202394
204118
  capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
202395
204119
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
@@ -203180,6 +204904,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
203180
204904
  exports.HEAP_WATCH_WARN_RATIO = require_manifest_python_deps.HEAP_WATCH_WARN_RATIO;
203181
204905
  exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
203182
204906
  exports.HUB_CAP_FWD_SERVICE = require_manifest_python_deps.HUB_CAP_FWD_SERVICE;
204907
+ exports.HUB_MAIN_RSS_BUDGET_MB = require_manifest_python_deps.HUB_MAIN_RSS_BUDGET_MB;
204908
+ exports.HUB_RSS_BUDGET_ENV = require_manifest_python_deps.HUB_RSS_BUDGET_ENV;
203183
204909
  exports.HubForwarderAddon = require_builtins_hub_forwarder_index.HubForwarderAddon$1;
203184
204910
  exports.HubForwarderDestination = require_builtins_hub_forwarder_index.HubForwarderDestination$1;
203185
204911
  exports.HubLogForwarder = HubLogForwarder;
@@ -203219,7 +204945,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
203219
204945
  exports.PythonEnvManager = PythonEnvManager;
203220
204946
  exports.QUARANTINE_DIRNAME = QUARANTINE_DIRNAME;
203221
204947
  exports.RESTART_MARKER_FILE = RESTART_MARKER_FILE;
204948
+ exports.RSS_BUDGET_REANNOUNCE_MIN_MS = require_manifest_python_deps.RSS_BUDGET_REANNOUNCE_MIN_MS;
204949
+ exports.RSS_BUDGET_RELEASE_RATIO = require_manifest_python_deps.RSS_BUDGET_RELEASE_RATIO;
203222
204950
  exports.RUNNER_HEAP_WATCH_INTERVAL_MS = require_manifest_python_deps.RUNNER_HEAP_WATCH_INTERVAL_MS;
204951
+ exports.RUNNER_RSS_BUDGET_ENV = require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV;
203223
204952
  exports.RUNTIME_DEFAULTS = require_dist10.RUNTIME_DEFAULTS;
203224
204953
  exports.ReadinessRegistry = require_dist10.ReadinessRegistry;
203225
204954
  exports.ReadinessTimeoutError = require_dist10.ReadinessTimeoutError;
@@ -203316,6 +205045,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
203316
205045
  exports.deleteModelFromDisk = require_file_data_plane.deleteModelFromDisk;
203317
205046
  exports.deriveAgentListenPort = deriveAgentListenPort;
203318
205047
  exports.describeProviderKindDrift = describeProviderKindDrift;
205048
+ exports.describeRss = require_manifest_python_deps.describeRss;
203319
205049
  exports.detectWorkspacePackagesDir = detectWorkspacePackagesDir;
203320
205050
  Object.defineProperty(exports, "downloadBinary", {
203321
205051
  enumerable: true,
@@ -203392,6 +205122,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
203392
205122
  exports.getWorkerDeviceRegistry = require_manifest_python_deps.getWorkerDeviceRegistry;
203393
205123
  exports.hasDotNode = hasDotNode;
203394
205124
  exports.hashClusterSecret = hashClusterSecret;
205125
+ exports.hubMainRssBudget = require_manifest_python_deps.hubMainRssBudget;
203395
205126
  exports.installManifestNativeDeps = require_manifest_python_deps.installManifestNativeDeps;
203396
205127
  exports.installManifestPythonDeps = require_manifest_python_deps.installManifestPythonDeps;
203397
205128
  exports.installPackageFromNpm = installPackageFromNpm;
@@ -203420,8 +205151,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
203420
205151
  exports.localEndpointPath = require_manifest_python_deps.localEndpointPath;
203421
205152
  exports.localProviderLink = require_manifest_python_deps.localProviderLink;
203422
205153
  exports.mountNativeCapService = require_manifest_python_deps.mountNativeCapService;
205154
+ exports.nextRssBudgetState = require_manifest_python_deps.nextRssBudgetState;
203423
205155
  exports.parseCapAction = require_manifest_python_deps.parseCapAction;
203424
205156
  exports.parseRangeHeader = require_file_data_plane.parseRangeHeader;
205157
+ exports.parseRssBudgetMb = require_manifest_python_deps.parseRssBudgetMb;
203425
205158
  exports.parseTokenizedUrl = require_file_data_plane.parseTokenizedUrl;
203426
205159
  exports.partitionIsolatedBuiltinIds = partitionIsolatedBuiltinIds;
203427
205160
  exports.proxyToUpstream = proxyToUpstream;
@@ -203469,7 +205202,7 @@ var require_enums = __commonJS({
203469
205202
  "../types/dist/enums.js"(exports) {
203470
205203
  "use strict";
203471
205204
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
203472
- var require_event_category = require_event_category_EY0GNjV9();
205205
+ var require_event_category = require_event_category_BaEgqJNv();
203473
205206
  var EventSourceType = /* @__PURE__ */ (function(EventSourceType2) {
203474
205207
  EventSourceType2["Addon"] = "addon";
203475
205208
  EventSourceType2["Core"] = "core";
@@ -203489,8 +205222,8 @@ var require_dist4 = __commonJS({
203489
205222
  "../types/dist/index.js"(exports) {
203490
205223
  "use strict";
203491
205224
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
203492
- var require_event_category = require_event_category_EY0GNjV9();
203493
- var require_sleep = require_sleep_CSodb2vQ();
205225
+ var require_event_category = require_event_category_BaEgqJNv();
205226
+ var require_sleep = require_sleep_9d8tJRbO();
203494
205227
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
203495
205228
  var require_enums2 = require_enums();
203496
205229
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -205256,6 +206989,116 @@ var require_dist4 = __commonJS({
205256
206989
  }
205257
206990
  return bestAbove?.entry ?? bestBelow?.entry;
205258
206991
  }
206992
+ var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
206993
+ var ROOT_BUCKET_KEY = "__root__";
206994
+ function bucketFor(row) {
206995
+ if (row.addonId !== null) return {
206996
+ key: row.addonId,
206997
+ kind: "addon"
206998
+ };
206999
+ if (row.classification === "root") return {
207000
+ key: ROOT_BUCKET_KEY,
207001
+ kind: "root"
207002
+ };
207003
+ return {
207004
+ key: UNATTRIBUTED_BUCKET_KEY,
207005
+ kind: "unattributed"
207006
+ };
207007
+ }
207008
+ function deci(value) {
207009
+ return Math.round(value * 10) / 10;
207010
+ }
207011
+ function foldSnapshotByFunction(rows, atMs) {
207012
+ const acc = /* @__PURE__ */ new Map();
207013
+ for (const row of rows) {
207014
+ const { key, kind } = bucketFor(row);
207015
+ const cur = acc.get(key) ?? {
207016
+ kind,
207017
+ main: 0,
207018
+ gc: 0,
207019
+ lifetime: 0,
207020
+ memory: 0,
207021
+ count: 0,
207022
+ splitKnown: true
207023
+ };
207024
+ const known = row.cpuMainPercent !== null && row.cpuGcPercent !== null;
207025
+ acc.set(key, {
207026
+ kind: cur.kind,
207027
+ main: cur.main + (row.cpuMainPercent ?? 0),
207028
+ gc: cur.gc + (row.cpuGcPercent ?? 0),
207029
+ lifetime: cur.lifetime + row.cpuPercent,
207030
+ memory: cur.memory + row.memoryRssBytes,
207031
+ count: cur.count + 1,
207032
+ splitKnown: cur.splitKnown && known
207033
+ });
207034
+ }
207035
+ return [...acc.entries()].map(([key, a]) => {
207036
+ const main = a.splitKnown ? deci(a.main) : null;
207037
+ const gc = a.splitKnown ? deci(a.gc) : null;
207038
+ const lifetime = deci(a.lifetime);
207039
+ return {
207040
+ key,
207041
+ kind: a.kind,
207042
+ point: {
207043
+ atMs,
207044
+ samples: 1,
207045
+ cpuMainPercent: main,
207046
+ cpuMainPercentMin: main,
207047
+ cpuGcPercent: gc,
207048
+ cpuGcPercentMin: gc,
207049
+ cpuLifetimePercent: lifetime,
207050
+ cpuLifetimePercentMin: lifetime,
207051
+ memoryRssBytes: a.memory,
207052
+ memoryRssBytesMin: a.memory,
207053
+ processCount: a.count,
207054
+ processCountMin: a.count
207055
+ }
207056
+ };
207057
+ });
207058
+ }
207059
+ function minNullable(a, b) {
207060
+ if (a === null || b === null) return null;
207061
+ return a < b ? a : b;
207062
+ }
207063
+ function maxNullable(a, b) {
207064
+ if (a === null || b === null) return null;
207065
+ return a > b ? a : b;
207066
+ }
207067
+ function mergePoints(held, next, atMs) {
207068
+ return {
207069
+ atMs,
207070
+ samples: held.samples + next.samples,
207071
+ cpuMainPercent: maxNullable(held.cpuMainPercent, next.cpuMainPercent),
207072
+ cpuMainPercentMin: minNullable(held.cpuMainPercentMin, next.cpuMainPercentMin),
207073
+ cpuGcPercent: maxNullable(held.cpuGcPercent, next.cpuGcPercent),
207074
+ cpuGcPercentMin: minNullable(held.cpuGcPercentMin, next.cpuGcPercentMin),
207075
+ cpuLifetimePercent: Math.max(held.cpuLifetimePercent, next.cpuLifetimePercent),
207076
+ cpuLifetimePercentMin: Math.min(held.cpuLifetimePercentMin, next.cpuLifetimePercentMin),
207077
+ memoryRssBytes: Math.max(held.memoryRssBytes, next.memoryRssBytes),
207078
+ memoryRssBytesMin: Math.min(held.memoryRssBytesMin, next.memoryRssBytesMin),
207079
+ processCount: Math.max(held.processCount, next.processCount),
207080
+ processCountMin: Math.min(held.processCountMin, next.processCountMin)
207081
+ };
207082
+ }
207083
+ function resolveBucketMs(spanMs, cadenceMs, maxPoints) {
207084
+ if (maxPoints <= 0 || cadenceMs <= 0 || spanMs <= 0) return Math.max(cadenceMs, 1);
207085
+ const wanted = spanMs / maxPoints;
207086
+ if (wanted <= cadenceMs) return cadenceMs;
207087
+ return Math.ceil(wanted / cadenceMs) * cadenceMs;
207088
+ }
207089
+ function reducePoints(points, bucketMs, origin) {
207090
+ if (bucketMs <= 0 || points.length === 0) return points;
207091
+ const buckets = /* @__PURE__ */ new Map();
207092
+ for (const point of points) {
207093
+ const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
207094
+ const held = buckets.get(start);
207095
+ buckets.set(start, held === void 0 ? {
207096
+ ...point,
207097
+ atMs: start
207098
+ } : mergePoints(held, point, start));
207099
+ }
207100
+ return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
207101
+ }
205259
207102
  var FORMAT_KEYS = [
205260
207103
  "onnx",
205261
207104
  "coreml",
@@ -209179,6 +211022,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209179
211022
  id: zod.z.string(),
209180
211023
  data: zod.z.record(zod.z.string(), zod.z.unknown())
209181
211024
  });
211025
+ var BulkRecordSchema = zod.z.object({
211026
+ id: zod.z.string().optional(),
211027
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
211028
+ });
209182
211029
  var CollectionColumnSchema = zod.z.object({
209183
211030
  name: zod.z.string(),
209184
211031
  type: zod.z.enum([
@@ -209253,6 +211100,34 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209253
211100
  collection: zod.z.string(),
209254
211101
  record: SettingsRecordSchema
209255
211102
  }), zod.z.void(), { kind: "mutation" }),
211103
+ /**
211104
+ * Insert MANY records in ONE transaction, returning how many landed.
211105
+ *
211106
+ * The write-side twin of {@link deleteWhere}, and it exists for the same
211107
+ * reason: without it, appending a batch is N round trips and N COMMITs on
211108
+ * the single shared connection that also serves every cluster-wide
211109
+ * configuration read. The durable load series writes one process row per
211110
+ * process per sample — 76 rows every 10 s on the live fleet — and the
211111
+ * operator's rule for it is *one transaction per sample, never one per
211112
+ * row*. `insert` cannot express that; nothing else could.
211113
+ *
211114
+ * **All or nothing.** A batch that fails on its fifth row leaves none of
211115
+ * the five behind. A half-written sample is worse than a missing one: the
211116
+ * missing one reads as "nobody reported", which is true, while the half
211117
+ * one reads as "these were the only processes running", which is not.
211118
+ *
211119
+ * `id` is OPTIONAL per record, and that is the difference from
211120
+ * {@link insert}. A collection whose primary key is an `INTEGER` rowid
211121
+ * alias has no id to supply — SQLite assigns it, for free, and inventing a
211122
+ * `randomUUID()` for such a column would write a 36-character string into
211123
+ * an integer key. Omitted on a TEXT key, a uuid is generated exactly as
211124
+ * `insert` does.
211125
+ */
211126
+ insertMany: require_sleep.method(zod.z.object({
211127
+ namespace: zod.z.string().optional(),
211128
+ collection: zod.z.string(),
211129
+ records: zod.z.array(BulkRecordSchema).readonly()
211130
+ }), zod.z.object({ inserted: zod.z.number().int() }), { kind: "mutation" }),
209256
211131
  /** Update an existing record by ID. */
209257
211132
  update: require_sleep.method(zod.z.object({
209258
211133
  namespace: zod.z.string().optional(),
@@ -209442,6 +211317,15 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209442
211317
  kind: "mutation",
209443
211318
  auth: "admin"
209444
211319
  }),
211320
+ /** Insert many records in ONE transaction. All or nothing. */
211321
+ insertMany: require_sleep.method(zod.z.object({
211322
+ namespace: zod.z.string().optional(),
211323
+ collection: zod.z.string(),
211324
+ records: zod.z.array(BulkRecordSchema).readonly()
211325
+ }), zod.z.object({ inserted: zod.z.number().int() }), {
211326
+ kind: "mutation",
211327
+ auth: "admin"
211328
+ }),
209445
211329
  /** Update an existing record by ID. */
209446
211330
  update: require_sleep.method(zod.z.object({
209447
211331
  namespace: zod.z.string().optional(),
@@ -211840,6 +213724,85 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
211840
213724
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
211841
213725
  mount: { kind: "skip" }
211842
213726
  };
213727
+ var LOAD_CONTRIBUTION_ROLES = [
213728
+ "decode",
213729
+ "transcode",
213730
+ "recording",
213731
+ "streaming",
213732
+ "detection"
213733
+ ];
213734
+ var LOAD_CONTRIBUTION_ATTRIBUTIONS = [
213735
+ "measured",
213736
+ "accounted",
213737
+ "unattributable"
213738
+ ];
213739
+ var LoadContributionSchema = zod.z.object({
213740
+ role: zod.z.enum(LOAD_CONTRIBUTION_ROLES),
213741
+ /**
213742
+ * The NUMERIC device id — the same value every log line carries as
213743
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
213744
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
213745
+ * contributor that cannot name its camera must not emit the entry at all,
213746
+ * because an unnamed per-camera entry is indistinguishable from a shared one
213747
+ * and would quietly turn one camera's cost into everybody's.
213748
+ */
213749
+ deviceId: zod.z.number().int().positive().nullable(),
213750
+ attribution: zod.z.enum(LOAD_CONTRIBUTION_ATTRIBUTIONS),
213751
+ /**
213752
+ * What ONE entry is, in the contributor's own words — `615/high`,
213753
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
213754
+ * family and inventing a common one would lose the only information that
213755
+ * makes two entries for the same camera distinguishable.
213756
+ */
213757
+ unit: zod.z.string(),
213758
+ /**
213759
+ * The OS process this cost lives in, when there is one. Present so a
213760
+ * consumer can (a) tell two generations of the same unit apart across a
213761
+ * restart, and (b) subtract claimed processes from the node's process
213762
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
213763
+ * process of its own.
213764
+ */
213765
+ pid: zod.z.number().int().positive().optional(),
213766
+ /**
213767
+ * When this generation started. The pid's incarnation marker: a consumer
213768
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
213769
+ * window when this changes, because the counter restarted from zero in a new
213770
+ * process.
213771
+ */
213772
+ startedAtMs: zod.z.number().optional(),
213773
+ /**
213774
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
213775
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
213776
+ * contribution is asked for.
213777
+ *
213778
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
213779
+ * needs a sampler, and a new per-node sampler is the defect half of
213780
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
213781
+ * by whoever already keeps a history; a rate cannot be un-averaged.
213782
+ *
213783
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
213784
+ * an entry with no process.
213785
+ */
213786
+ cpuSeconds: zod.z.number().optional(),
213787
+ /** Resident bytes of this unit's process, same source and same rules. */
213788
+ rssBytes: zod.z.number().optional()
213789
+ });
213790
+ var loadContributionCapability = {
213791
+ name: "load-contribution",
213792
+ scope: "system",
213793
+ mode: "collection",
213794
+ internal: true,
213795
+ methods: {
213796
+ /**
213797
+ * This addon's own cost entries, computed live from state it already
213798
+ * holds. Inert: no persistence, no sampling, no timer. It is answered on
213799
+ * whatever beat the caller already has.
213800
+ */
213801
+ list: require_sleep.method(zod.z.void(), zod.z.array(LoadContributionSchema).readonly())
213802
+ },
213803
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
213804
+ mount: { kind: "skip" }
213805
+ };
211843
213806
  var LoginStageEnum = zod.z.enum(["primary", "second-factor"]);
211844
213807
  var RedirectLoginMethodSchema = zod.z.object({
211845
213808
  kind: zod.z.literal("redirect"),
@@ -212011,8 +213974,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212011
213974
  classification: zod.z.enum([
212012
213975
  "root",
212013
213976
  "managed",
212014
- "system",
212015
- "ghost"
213977
+ "system"
212016
213978
  ]),
212017
213979
  /** `$process` addon binding when `managed`, else null. */
212018
213980
  addonId: zod.z.string().nullable(),
@@ -212020,22 +213982,39 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212020
213982
  nodeId: zod.z.string().nullable(),
212021
213983
  /** Truncated command line. */
212022
213984
  command: zod.z.string(),
213985
+ /**
213986
+ * `ps pcpu` — CPU averaged over the process's WHOLE LIFETIME, not a rate.
213987
+ * On a runner up for days it barely moves. Fine as a column, useless as a
213988
+ * series: use `cpuMainPercent + cpuGcPercent` for anything time-varying.
213989
+ */
212023
213990
  cpuPercent: zod.z.number(),
212024
213991
  memoryRssBytes: zod.z.number(),
213992
+ /**
213993
+ * Instantaneous CPU% of the process's own threads over the last
213994
+ * process-snapshot window, from a `/proc/<pid>/task/*` tick delta.
213995
+ *
213996
+ * `null` = UNKNOWN, never zero: no previous sample yet (first tick after
213997
+ * boot), the pid was recycled, or this node is not Linux.
213998
+ */
213999
+ cpuMainPercent: zod.z.number().nullable(),
214000
+ /**
214001
+ * Instantaneous CPU% of V8's `V8Worker` platform pool over the same window.
214002
+ *
214003
+ * This is the number that rewrote the 2026-08-27 diagnosis — hub-main 73%,
214004
+ * `stream-broker` 61% (`docs/architecture/load-ledger.md`). A CPU chart that
214005
+ * does not separate it from `cpuMainPercent` shows "busy" where the truth is
214006
+ * "allocating too much".
214007
+ *
214008
+ * Concurrent GC is the dominant tenant of that pool but not the only one
214009
+ * (background compilation runs there too), so it is reported as
214010
+ * "GC / V8 helpers" rather than as pure collection time. `null` has the same
214011
+ * meaning as on `cpuMainPercent`.
214012
+ */
214013
+ cpuGcPercent: zod.z.number().nullable(),
214014
+ /** Threads seen in the tick scan. `null` under the same conditions. */
214015
+ threadCount: zod.z.number().nullable(),
212025
214016
  /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
212026
- uptimeSec: zod.z.number(),
212027
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
212028
- orphaned: zod.z.boolean()
212029
- });
212030
- var KillProcessInputSchema = zod.z.object({
212031
- pid: zod.z.number(),
212032
- /** Force = SIGKILL. Default is SIGTERM. */
212033
- force: zod.z.boolean().optional()
212034
- });
212035
- var KillProcessResultSchema = zod.z.object({
212036
- success: zod.z.boolean(),
212037
- reason: zod.z.string().optional(),
212038
- signal: zod.z.enum(["SIGTERM", "SIGKILL"]).optional()
214017
+ uptimeSec: zod.z.number()
212039
214018
  });
212040
214019
  var DumpHeapSnapshotInputSchema = zod.z.object({
212041
214020
  /** The addon whose runner should dump a heap snapshot. */
@@ -212049,6 +214028,89 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212049
214028
  pid: zod.z.number().optional(),
212050
214029
  reason: zod.z.string().optional()
212051
214030
  });
214031
+ var LoadPointSchema = zod.z.object({
214032
+ /** Bucket START, or the snapshot's own timestamp when unreduced. */
214033
+ atMs: zod.z.number(),
214034
+ /** Raw snapshots in this bucket. Never 0 — AN EMPTY BUCKET IS ABSENT. */
214035
+ samples: zod.z.number().int(),
214036
+ /**
214037
+ * `null` = UNKNOWN and it PROPAGATES: a bucket is null unless every process
214038
+ * of every snapshot in it reported a thread split. A partial sum is a
214039
+ * smaller number that looks exactly as real as a complete one.
214040
+ */
214041
+ cpuMainPercent: zod.z.number().nullable(),
214042
+ cpuMainPercentMin: zod.z.number().nullable(),
214043
+ cpuGcPercent: zod.z.number().nullable(),
214044
+ cpuGcPercentMin: zod.z.number().nullable(),
214045
+ /** Lifetime-average CPU%, summed. Always known — and never a rate. */
214046
+ cpuLifetimePercent: zod.z.number(),
214047
+ cpuLifetimePercentMin: zod.z.number(),
214048
+ memoryRssBytes: zod.z.number(),
214049
+ memoryRssBytesMin: zod.z.number(),
214050
+ processCount: zod.z.number().int(),
214051
+ processCountMin: zod.z.number().int()
214052
+ });
214053
+ var LoadFunctionSeriesSchema = zod.z.object({
214054
+ key: zod.z.string(),
214055
+ kind: zod.z.enum([
214056
+ "addon",
214057
+ "root",
214058
+ "unattributed"
214059
+ ]),
214060
+ /** Oldest-first. A missing interval is MISSING — never zero-filled. */
214061
+ points: zod.z.array(LoadPointSchema).readonly()
214062
+ });
214063
+ var NodeLoadSeriesSchema = zod.z.object({
214064
+ nodeId: zod.z.string(),
214065
+ /** One entry per function seen in the window, heaviest-first. */
214066
+ series: zod.z.array(LoadFunctionSeriesSchema).readonly(),
214067
+ /**
214068
+ * Width of one returned bucket, in ms. Equals the sampling cadence when no
214069
+ * reduction was needed — so a caller can always say what one point covers
214070
+ * without having to know whether it was reduced.
214071
+ */
214072
+ bucketMs: zod.z.number(),
214073
+ /** Raw snapshots that went into this answer, across both tiers. */
214074
+ retainedSamples: zod.z.number(),
214075
+ /** Oldest snapshot represented, or `null` when nothing is retained. */
214076
+ oldestAtMs: zod.z.number().nullable(),
214077
+ /** The fixed sampling cadence in force on the cluster, in ms. */
214078
+ cadenceMs: zod.z.number(),
214079
+ /**
214080
+ * Did the DURABLE tier contribute? `false` means the answer is the hot ring
214081
+ * alone — an agent (which holds no table), or a store that refused.
214082
+ * Reported because "the last hour" and "the last six hours" are different
214083
+ * questions and an operator must not have to guess which was answered.
214084
+ */
214085
+ durable: zod.z.boolean()
214086
+ });
214087
+ var GetLoadSeriesInputSchema = zod.z.object({
214088
+ /**
214089
+ * The node whose series is wanted.
214090
+ *
214091
+ * NOT named `nodeId`: the generated cap router strips a top-level
214092
+ * `nodeId` from every method input and uses it to ROUTE the call to
214093
+ * that node's provider (`generated-cap-routers.ts`). A series target
214094
+ * called `nodeId` would silently become a routing pin and never reach
214095
+ * the provider. The hub holds every node it hears from, so the
214096
+ * ordinary call is unpinned — answered by the hub, for any node.
214097
+ */
214098
+ forNodeId: zod.z.string(),
214099
+ /**
214100
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
214101
+ * holds and receives only what it is missing, so seeding a live chart
214102
+ * from this method cannot double a point already drawn.
214103
+ */
214104
+ sinceMs: zod.z.number().optional(),
214105
+ /**
214106
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
214107
+ * preserving min and max per bucket.
214108
+ *
214109
+ * Absent means NO reduction — legitimate for a short window and a trap for a
214110
+ * long one, which is why a chart passes its own pixel width.
214111
+ */
214112
+ maxPoints: zod.z.number().int().positive().optional()
214113
+ });
212052
214114
  var SystemMetricsSchema = zod.z.object({
212053
214115
  cpuPercent: zod.z.number(),
212054
214116
  memoryPercent: zod.z.number(),
@@ -212094,28 +214156,44 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212094
214156
  getAddonStats: require_sleep.method(zod.z.object({ addonId: zod.z.string() }), PidResourceStatsSchema.nullable()),
212095
214157
  /**
212096
214158
  * Snapshot of every camstack-related process on this node with a
212097
- * ghost/managed/root classification. Powers the Cluster → Agent →
212098
- * Processes tab: cross-references `$process.list` against a `ps` scan
212099
- * so orphaned trees (PPID=1) or unknown children show up as `ghost`
212100
- * and can be killed from the UI.
214159
+ * root/managed/system classification. Powers the Cluster → Agent →
214160
+ * Processes tab: cross-references `$process.list` against a `ps` scan so
214161
+ * per-addon CPU and RSS can be attributed, and so a process the cluster
214162
+ * does not manage is still visible.
214163
+ *
214164
+ * **Read-only, by design.** This cap once carried a `killProcess`
214165
+ * mutation; it was deleted on 2026-08-27. A runner's lifecycle belongs to
214166
+ * `CrashSupervisor` and is driven through `addons.restartAddon` /
214167
+ * `$process.restart` — signalling a raw pid went around the supervisor
214168
+ * (D6), and the one class it was willing to signal turned out to be the
214169
+ * container's own init and the operator's desktop app.
212101
214170
  */
212102
214171
  listNodeProcesses: require_sleep.method(zod.z.void(), zod.z.array(NodeProcessSchema).readonly()),
212103
214172
  /**
212104
- * Send SIGTERM (or SIGKILL when `force`) to a pid inside this node's
212105
- * process tree. The provider refuses pids that aren't in the live
212106
- * `listNodeProcesses()` snapshot callers can't use this endpoint
212107
- * to kill arbitrary system processes.
214173
+ * The retained per-node load series the ONE reader over BOTH tiers.
214174
+ *
214175
+ * The in-memory ring is the HOT window (the last 180 snapshots, held by
214176
+ * every node's `native-metrics`); the hub's `metrics:node-load-samples`
214177
+ * table is the COLD one (the operator's retention, six hours by default).
214178
+ * This method merges them and DEDUPES on `atMs`, so a snapshot present in
214179
+ * both contributes once and the caller never learns which tier a point
214180
+ * came from. There is deliberately no second read surface: two readers is
214181
+ * how two charts start disagreeing about the same node.
214182
+ *
214183
+ * Reads only; nothing is sampled to answer it. Normally called UNPINNED —
214184
+ * the hub hears every node's snapshot and holds every node's rows — and
214185
+ * answers for any `forNodeId`. Pinned to an agent it answers from that
214186
+ * agent's ring alone (`durable: false`). Empty is a legitimate answer: a
214187
+ * node nobody has heard from has no series, and saying so is the truth.
212108
214188
  */
212109
- killProcess: require_sleep.method(KillProcessInputSchema, KillProcessResultSchema, {
212110
- kind: "mutation",
212111
- auth: "admin"
212112
- }),
214189
+ getLoadSeries: require_sleep.method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema),
212113
214190
  /**
212114
214191
  * Tell the addon's forked runner to write a V8 heap snapshot to disk (via
212115
214192
  * SIGUSR2 — the runner's diagnostic handler). Also logs its
212116
- * `process.memoryUsage()` + heap-space breakdown. Refuses pids not in the
212117
- * live `listNodeProcesses()` snapshot. Use for deep per-addon memory
212118
- * attribution; copy the returned path off the node to analyze.
214193
+ * `process.memoryUsage()` + heap-space breakdown. Resolves the pid from
214194
+ * `$process.list`, so it can only reach a runner this node spawned. Use
214195
+ * for deep per-addon memory attribution; copy the returned path off the
214196
+ * node to analyze.
212119
214197
  */
212120
214198
  dumpHeapSnapshot: require_sleep.method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
212121
214199
  kind: "mutation",
@@ -228210,6 +230288,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
228210
230288
  */
228211
230289
  channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
228212
230290
  });
230291
+ var ReportedLoadContributionSchema = LoadContributionSchema.extend({ addonId: zod.z.string() });
228213
230292
  var GetLoggingSettingsInputSchema = zod.z.object({
228214
230293
  scopeNodeId: zod.z.string().optional(),
228215
230294
  /**
@@ -228303,6 +230382,22 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
228303
230382
  */
228304
230383
  getRequestCensus: require_sleep.method(zod.z.void(), RequestCensusStatusSchema, { auth: "admin" }),
228305
230384
  /**
230385
+ * Every `load-contribution` an addon on this cluster reports — each
230386
+ * addon's OWN cost, already attributed by the addon that owns it.
230387
+ *
230388
+ * There is no central list of what costs what: an addon that spawns a
230389
+ * per-camera child declares it, and one that cannot attribute its cost
230390
+ * (the shared inference pool) declares THAT. So a new cost family appears
230391
+ * here the moment its addon is redeployed, with nobody editing anything.
230392
+ *
230393
+ * What this does NOT do is measure the node. `metrics.node-processes-
230394
+ * snapshot` still does that, and the difference between the two is the
230395
+ * finding: a process no contribution claims is either a leak or a family
230396
+ * nobody has taught to report. Both belong in the unattributed bucket, and
230397
+ * neither may be folded into a camera.
230398
+ */
230399
+ getLoadContributions: require_sleep.method(zod.z.void(), zod.z.array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }),
230400
+ /**
228306
230401
  * The logging settings document — levels and armed diagnostics — resolved
228307
230402
  * for `nodeId`, or for the cluster when `nodeId` is absent.
228308
230403
  *
@@ -231874,6 +233969,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
231874
233969
  lawnMowerControl: "lawn-mower-control",
231875
233970
  llm: "llm",
231876
233971
  llmRuntime: "llm-runtime",
233972
+ loadContribution: "load-contribution",
231877
233973
  localNetwork: "local-network",
231878
233974
  lockControl: "lock-control",
231879
233975
  logChannels: "log-channels",
@@ -232239,6 +234335,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232239
234335
  key: "llmRuntime",
232240
234336
  name: "llm-runtime"
232241
234337
  },
234338
+ {
234339
+ key: "loadContribution",
234340
+ name: "load-contribution"
234341
+ },
232242
234342
  {
232243
234343
  key: "localNetwork",
232244
234344
  name: "local-network"
@@ -232628,6 +234728,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232628
234728
  lawnMowerControlCapability,
232629
234729
  llmCapability,
232630
234730
  llmRuntimeCapability,
234731
+ loadContributionCapability,
232631
234732
  localNetworkCapability,
232632
234733
  lockControlCapability,
232633
234734
  logChannelsCapability,
@@ -233651,6 +235752,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
233651
235752
  addonId: null,
233652
235753
  access: "create"
233653
235754
  },
235755
+ "dataStoreProvider.insertMany": {
235756
+ capName: "data-store-provider",
235757
+ capScope: "system",
235758
+ addonId: null,
235759
+ access: "create"
235760
+ },
233654
235761
  "dataStoreProvider.isEmpty": {
233655
235762
  capName: "data-store-provider",
233656
235763
  capScope: "system",
@@ -234965,6 +237072,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234965
237072
  addonId: null,
234966
237073
  access: "create"
234967
237074
  },
237075
+ "loadContribution.list": {
237076
+ capName: "load-contribution",
237077
+ capScope: "system",
237078
+ addonId: null,
237079
+ access: "view"
237080
+ },
234968
237081
  "localNetwork.downloadCa": {
234969
237082
  capName: "local-network",
234970
237083
  capScope: "system",
@@ -235265,17 +237378,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
235265
237378
  addonId: null,
235266
237379
  access: "view"
235267
237380
  },
235268
- "metricsProvider.getProcessStats": {
237381
+ "metricsProvider.getLoadSeries": {
235269
237382
  capName: "metrics-provider",
235270
237383
  capScope: "system",
235271
237384
  addonId: null,
235272
237385
  access: "view"
235273
237386
  },
235274
- "metricsProvider.killProcess": {
237387
+ "metricsProvider.getProcessStats": {
235275
237388
  capName: "metrics-provider",
235276
237389
  capScope: "system",
235277
237390
  addonId: null,
235278
- access: "create"
237391
+ access: "view"
235279
237392
  },
235280
237393
  "metricsProvider.listAddonInstances": {
235281
237394
  capName: "metrics-provider",
@@ -237287,6 +239400,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237287
239400
  addonId: null,
237288
239401
  access: "create"
237289
239402
  },
239403
+ "settingsStore.insertMany": {
239404
+ capName: "settings-store",
239405
+ capScope: "system",
239406
+ addonId: null,
239407
+ access: "create"
239408
+ },
237290
239409
  "settingsStore.isEmpty": {
237291
239410
  capName: "settings-store",
237292
239411
  capScope: "system",
@@ -237899,6 +240018,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237899
240018
  addonId: null,
237900
240019
  access: "create"
237901
240020
  },
240021
+ "system.getLoadContributions": {
240022
+ capName: "system",
240023
+ capScope: "system",
240024
+ addonId: null,
240025
+ access: "view"
240026
+ },
237902
240027
  "system.getLoggingSettings": {
237903
240028
  capName: "system",
237904
240029
  capScope: "system",
@@ -238591,6 +240716,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238591
240716
  "lawn-mower-control",
238592
240717
  "llm",
238593
240718
  "llm-runtime",
240719
+ "load-contribution",
238594
240720
  "local-network",
238595
240721
  "lock-control",
238596
240722
  "log-channels",
@@ -238749,6 +240875,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238749
240875
  "integrations",
238750
240876
  "llm",
238751
240877
  "llm-runtime",
240878
+ "load-contribution",
238752
240879
  "local-network",
238753
240880
  "log-channels",
238754
240881
  "log-destination",
@@ -241381,7 +243508,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241381
243508
  listAddonInstances: (input) => dispatch("metricsProvider", "listAddonInstances", "query", input),
241382
243509
  getAddonStats: (input) => dispatch("metricsProvider", "getAddonStats", "query", input),
241383
243510
  listNodeProcesses: (input) => dispatch("metricsProvider", "listNodeProcesses", "query", input),
241384
- killProcess: (input) => dispatch("metricsProvider", "killProcess", "mutation", input),
243511
+ getLoadSeries: (input) => dispatch("metricsProvider", "getLoadSeries", "query", input),
241385
243512
  dumpHeapSnapshot: (input) => dispatch("metricsProvider", "dumpHeapSnapshot", "mutation", input)
241386
243513
  },
241387
243514
  mqttBroker: {
@@ -241560,6 +243687,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241560
243687
  set: (input) => dispatch("settingsStore", "set", "mutation", input),
241561
243688
  query: (input) => dispatch("settingsStore", "query", "query", input),
241562
243689
  insert: (input) => dispatch("settingsStore", "insert", "mutation", input),
243690
+ insertMany: (input) => dispatch("settingsStore", "insertMany", "mutation", input),
241563
243691
  update: (input) => dispatch("settingsStore", "update", "mutation", input),
241564
243692
  delete: (input) => dispatch("settingsStore", "delete", "mutation", input),
241565
243693
  deleteWhere: (input) => dispatch("settingsStore", "deleteWhere", "mutation", input),
@@ -241640,6 +243768,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241640
243768
  setSiteLocation: (input) => dispatch("system", "setSiteLocation", "mutation", input),
241641
243769
  detectSiteLocation: (input) => dispatch("system", "detectSiteLocation", "mutation", input),
241642
243770
  getRequestCensus: (input) => dispatch("system", "getRequestCensus", "query", input),
243771
+ getLoadContributions: (input) => dispatch("system", "getLoadContributions", "query", input),
241643
243772
  getLoggingSettings: (input) => dispatch("system", "getLoggingSettings", "query", input),
241644
243773
  setLoggingSettings: (input) => dispatch("system", "setLoggingSettings", "mutation", input)
241645
243774
  },
@@ -244453,6 +246582,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244453
246582
  exports.BrokerSubscribeResultSchema = SubscribeResultSchema;
244454
246583
  exports.BrokerTestConnectionResultSchema = TestConnectionResultSchema;
244455
246584
  exports.BrokerUnsubscribeInputSchema = UnsubscribeInputSchema;
246585
+ exports.BulkRecordSchema = BulkRecordSchema;
244456
246586
  exports.CAMERA_SWITCH_CATALOG = CAMERA_SWITCH_CATALOG;
244457
246587
  exports.CAMERA_SWITCH_ORDER = CAMERA_SWITCH_ORDER;
244458
246588
  exports.CAM_PROFILE_ORDER = require_sleep.CAM_PROFILE_ORDER;
@@ -244726,6 +246856,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244726
246856
  exports.IntercomStatusSchema = IntercomStatusSchema;
244727
246857
  exports.KNOWN_CAP_NAMES = KNOWN_CAP_NAMES;
244728
246858
  exports.KeyEventSchema = KeyEventSchema;
246859
+ exports.LOAD_CONTRIBUTION_ATTRIBUTIONS = LOAD_CONTRIBUTION_ATTRIBUTIONS;
246860
+ exports.LOAD_CONTRIBUTION_ROLES = LOAD_CONTRIBUTION_ROLES;
244729
246861
  exports.LOG_CHANNEL_TICK_MS = LOG_CHANNEL_TICK_MS;
244730
246862
  exports.LOG_LEVEL_RANK = LOG_LEVEL_RANK;
244731
246863
  exports.LabelAttributionSchema = LabelAttributionSchema;
@@ -244758,6 +246890,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244758
246890
  exports.LlmTimeoutDefaults = LlmTimeoutDefaults;
244759
246891
  exports.LlmUsageRollupSchema = LlmUsageRollupSchema;
244760
246892
  exports.LlmUsageSchema = LlmUsageSchema;
246893
+ exports.LoadContributionSchema = LoadContributionSchema;
244761
246894
  exports.LocateSegmentResultSchema = LocateSegmentResultSchema;
244762
246895
  exports.LocationStatSchema = LocationStatSchema;
244763
246896
  exports.LockControlStatusSchema = LockControlStatusSchema;
@@ -245040,6 +247173,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245040
247173
  exports.RECORDING_EXPORT_MAX_READ_BYTES = RECORDING_EXPORT_MAX_READ_BYTES;
245041
247174
  exports.RESERVED_BINDING_NAMES = RESERVED_BINDING_NAMES;
245042
247175
  exports.RESTORED_CAP_NAMES = RESTORED_CAP_NAMES;
247176
+ exports.ROOT_BUCKET_KEY = ROOT_BUCKET_KEY;
245043
247177
  exports.RUNTIME_DEFAULTS = RUNTIME_DEFAULTS;
245044
247178
  exports.RUNTIME_STATE_POLICY = RUNTIME_STATE_POLICY;
245045
247179
  exports.RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS;
@@ -245082,6 +247216,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245082
247216
  exports.RelocateMediaInputSchema = RelocateMediaInputSchema;
245083
247217
  exports.RenderedAsSchema = RenderedAsSchema;
245084
247218
  exports.ReportMotionInputSchema = ReportMotionInputSchema;
247219
+ exports.ReportedLoadContributionSchema = ReportedLoadContributionSchema;
245085
247220
  exports.RequestCensusGroupSchema = RequestCensusGroupSchema;
245086
247221
  exports.RequestCensusProcedureSchema = RequestCensusProcedureSchema;
245087
247222
  exports.RequestCensusSnapshotSchema = RequestCensusSnapshotSchema;
@@ -245265,6 +247400,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245265
247400
  exports.TransportPlaneCountsSchema = TransportPlaneCountsSchema;
245266
247401
  exports.TransportPlaneSchema = TransportPlaneSchema;
245267
247402
  exports.TurnServerSchema = TurnServerSchema;
247403
+ exports.UNATTRIBUTED_BUCKET_KEY = UNATTRIBUTED_BUCKET_KEY;
245268
247404
  exports.UNIT_TABLE = UNIT_TABLE;
245269
247405
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema$1;
245270
247406
  exports.UnitConversionError = UnitConversionError;
@@ -245483,6 +247619,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245483
247619
  exports.filesystemBrowseCapability = filesystemBrowseCapability;
245484
247620
  exports.findTimezone = findTimezone;
245485
247621
  exports.floodCapability = floodCapability;
247622
+ exports.foldSnapshotByFunction = foldSnapshotByFunction;
245486
247623
  exports.formatForBackend = formatForBackend;
245487
247624
  exports.formatForRuntime = formatForRuntime;
245488
247625
  exports.gasCapability = gasCapability;
@@ -245540,6 +247677,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245540
247677
  exports.lifecycleTaskSchema = lifecycleTaskSchema;
245541
247678
  exports.llmCapability = llmCapability;
245542
247679
  exports.llmRuntimeCapability = llmRuntimeCapability;
247680
+ exports.loadContributionCapability = loadContributionCapability;
245543
247681
  exports.localNetworkCapability = localNetworkCapability;
245544
247682
  exports.locationSimilarity = locationSimilarity;
245545
247683
  exports.lockControlCapability = lockControlCapability;
@@ -245636,12 +247774,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245636
247774
  exports.recordingCapability = recordingCapability;
245637
247775
  exports.recordingExportCapability = recordingExportCapability;
245638
247776
  exports.rectsToCells = rectsToCells;
247777
+ exports.reducePoints = reducePoints;
245639
247778
  exports.requiresPython = requiresPython;
245640
247779
  exports.resetPoolBaseline = resetPoolBaseline;
245641
247780
  exports.resolveAddonExecution = resolveAddonExecution;
245642
247781
  exports.resolveAddonGroup = resolveAddonGroup;
245643
247782
  exports.resolveAddonPlacement = resolveAddonPlacement;
245644
247783
  exports.resolveAddonRuntime = resolveAddonRuntime;
247784
+ exports.resolveBucketMs = resolveBucketMs;
245645
247785
  exports.resolveCapMount = require_sleep.resolveCapMount;
245646
247786
  exports.resolveClusterStepModelId = resolveClusterStepModelId;
245647
247787
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
@@ -402831,6 +404971,37 @@ var require_collection_preference = __commonJS({
402831
404971
  }
402832
404972
  });
402833
404973
 
404974
+ // ../../server/backend/dist/api/core/load-contributions.js
404975
+ var require_load_contributions = __commonJS({
404976
+ "../../server/backend/dist/api/core/load-contributions.js"(exports) {
404977
+ "use strict";
404978
+ Object.defineProperty(exports, "__esModule", { value: true });
404979
+ exports.EMPTY_LOAD_CONTRIBUTION_PLANE = void 0;
404980
+ exports.buildLoadContributionPlane = buildLoadContributionPlane;
404981
+ var types_1 = require_dist4();
404982
+ exports.EMPTY_LOAD_CONTRIBUTION_PLANE = {
404983
+ contributions: async () => []
404984
+ };
404985
+ function buildLoadContributionPlane(source, onProviderError) {
404986
+ return {
404987
+ contributions: async () => {
404988
+ const out = [];
404989
+ for (const [addonId, provider] of source.entries()) {
404990
+ try {
404991
+ for (const entry of await provider.list()) {
404992
+ out.push({ ...entry, addonId });
404993
+ }
404994
+ } catch (err) {
404995
+ onProviderError?.(addonId, (0, types_1.errMsg)(err));
404996
+ }
404997
+ }
404998
+ return out;
404999
+ }
405000
+ };
405001
+ }
405002
+ }
405003
+ });
405004
+
402834
405005
  // ../../server/backend/dist/api/core/logging-settings.js
402835
405006
  var require_logging_settings = __commonJS({
402836
405007
  "../../server/backend/dist/api/core/logging-settings.js"(exports) {
@@ -403872,6 +406043,7 @@ var require_cap_providers = __commonJS({
403872
406043
  var agent_installed_packages_js_1 = require_agent_installed_packages();
403873
406044
  var http_request_census_singleton_js_1 = require_http_request_census_singleton();
403874
406045
  var collection_preference_js_1 = require_collection_preference();
406046
+ var load_contributions_js_1 = require_load_contributions();
403875
406047
  var logging_settings_js_1 = require_logging_settings();
403876
406048
  var request_census_settings_js_1 = require_request_census_settings();
403877
406049
  var site_location_js_1 = require_site_location();
@@ -403889,6 +406061,9 @@ var require_cap_providers = __commonJS({
403889
406061
  const channels = (0, logging_settings_js_1.buildLogChannelPlane)({ entries: () => registry?.getCollectionEntries("log-channels") ?? [] }, (addonId, phase, error) => {
403890
406062
  logger?.warn("log-channels provider unreachable", { meta: { addonId, phase, error } });
403891
406063
  });
406064
+ const loadContributions = (0, load_contributions_js_1.buildLoadContributionPlane)({ entries: () => registry?.getCollectionEntries("load-contribution") ?? [] }, (addonId, error) => {
406065
+ logger?.warn("load-contribution provider unreachable", { meta: { addonId, error } });
406066
+ });
403892
406067
  const loggingSettings = new logging_settings_js_1.LoggingSettingsService({
403893
406068
  store,
403894
406069
  gate: (0, system_1.getLoggingGate)(),
@@ -403915,6 +406090,7 @@ var require_cap_providers = __commonJS({
403915
406090
  }
403916
406091
  return result;
403917
406092
  },
406093
+ getLoadContributions: async () => loadContributions.contributions(),
403918
406094
  getRetentionConfig: async () => getRetention(registry)?.getConfig() ?? null,
403919
406095
  setRetentionConfig: async (input) => {
403920
406096
  getRetention(registry)?.setConfig(input);
@@ -417610,7 +419786,7 @@ var require_main4 = __commonJS({
417610
419786
  async function bootstrap() {
417611
419787
  const heapReclaimer = (0, system_1.createV8Reclaimer)();
417612
419788
  const heapReclaim = heapReclaimer === void 0 ? void 0 : { reclaim: heapReclaimer };
417613
- (0, system_1.startHeapWatch)("hub-main", void 0, void 0, heapReclaim);
419789
+ (0, system_1.startHeapWatch)("hub-main", void 0, void 0, heapReclaim, void 0, void 0, true, (0, system_1.hubMainRssBudget)());
417614
419790
  cleanupOrphanProcesses();
417615
419791
  let spaIndexHtml = null;
417616
419792
  const configPath = process.env.CONFIG_PATH ?? path.join(process.env.CAMSTACK_DATA ?? path.join(process.cwd(), "camstack-data"), "config.yaml");