camstack 1.2.50 → 1.2.52

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-B-mBrEz9.js
23637
+ var require_dist_B_mBrEz9 = __commonJS({
23638
+ "../system/dist/dist-B-mBrEz9.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
  *
@@ -48542,7 +48902,7 @@ var require_dist_BVU5JADq = __commonJS({
48542
48902
  "ffmpeg.binaryPath": "ffmpeg",
48543
48903
  "ffmpeg.hwAccel": "auto",
48544
48904
  "ffmpeg.threadCount": 0,
48545
- "auth.tokenExpiry": "7d"
48905
+ "auth.tokenExpiry": "30d"
48546
48906
  };
48547
48907
  var AccessoryKind = {
48548
48908
  Siren: DeviceRole.Siren,
@@ -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_B_mBrEz9();
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_B_mBrEz9();
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_B_mBrEz9();
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-Davtjo5p.js
60209
+ var require_retired_settings_keys_Davtjo5p = __commonJS({
60210
+ "../system/dist/retired-settings-keys-Davtjo5p.js"(exports) {
59808
60211
  "use strict";
59809
- var require_dist10 = require_dist_BVU5JADq();
60212
+ var require_dist10 = require_dist_B_mBrEz9();
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_B_mBrEz9();
62367
+ var require_retired_settings_keys = require_retired_settings_keys_Davtjo5p();
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_B_mBrEz9();
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_B_mBrEz9();
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_B_mBrEz9();
66930
67444
  var node_crypto = __require("crypto");
66931
67445
  node_crypto = require_chunk.__toESM(node_crypto);
66932
67446
  var crypto$1 = __require("crypto");
@@ -71440,6 +71954,99 @@ var require_local_auth_addon = __commonJS({
71440
71954
  };
71441
71955
  }));
71442
71956
  var import_jsonwebtoken = /* @__PURE__ */ require_chunk.__toESM(require_jsonwebtoken());
71957
+ var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|milliseconds?|msecs?|s|secs?|seconds?|m|mins?|minutes?|h|hrs?|hours?|d|days?|w|weeks?|y|yrs?|years?)$/i;
71958
+ var UNIT_MS = {
71959
+ ms: 1,
71960
+ msec: 1,
71961
+ msecs: 1,
71962
+ millisecond: 1,
71963
+ milliseconds: 1,
71964
+ s: 1e3,
71965
+ sec: 1e3,
71966
+ secs: 1e3,
71967
+ second: 1e3,
71968
+ seconds: 1e3,
71969
+ m: 6e4,
71970
+ min: 6e4,
71971
+ mins: 6e4,
71972
+ minute: 6e4,
71973
+ minutes: 6e4,
71974
+ h: 36e5,
71975
+ hr: 36e5,
71976
+ hrs: 36e5,
71977
+ hour: 36e5,
71978
+ hours: 36e5,
71979
+ d: 864e5,
71980
+ day: 864e5,
71981
+ days: 864e5,
71982
+ w: 6048e5,
71983
+ week: 6048e5,
71984
+ weeks: 6048e5,
71985
+ y: 315576e5,
71986
+ yr: 315576e5,
71987
+ yrs: 315576e5,
71988
+ year: 315576e5,
71989
+ years: 315576e5
71990
+ };
71991
+ var MIN_EXPIRY_MS = 6e4;
71992
+ var MAX_EXPIRY_MS = 365 * 864e5;
71993
+ function durationMs(candidate) {
71994
+ const match = DURATION_PATTERN.exec(candidate);
71995
+ if (match === null) return null;
71996
+ const amount = Number(match[1]);
71997
+ const unitMs = UNIT_MS[match[2].toLowerCase()];
71998
+ if (!Number.isFinite(amount) || unitMs === void 0) return null;
71999
+ return amount * unitMs;
72000
+ }
72001
+ var DEFAULT_SECONDS = (() => {
72002
+ const ms = durationMs("30d");
72003
+ if (ms === null) throw new Error(`DEFAULT_TOKEN_EXPIRY is not a duration: 30d`);
72004
+ return ms / 1e3;
72005
+ })();
72006
+ function refuse(reason) {
72007
+ return {
72008
+ ok: false,
72009
+ reason,
72010
+ seconds: DEFAULT_SECONDS,
72011
+ value: "30d"
72012
+ };
72013
+ }
72014
+ function parseTokenExpiry(configured) {
72015
+ if (configured === void 0 || configured === null) return {
72016
+ ok: true,
72017
+ seconds: DEFAULT_SECONDS,
72018
+ value: "30d"
72019
+ };
72020
+ if (typeof configured !== "string") return refuse("not-a-string");
72021
+ const trimmed = configured.trim();
72022
+ if (trimmed === "") return {
72023
+ ok: true,
72024
+ seconds: DEFAULT_SECONDS,
72025
+ value: "30d"
72026
+ };
72027
+ const totalMs = durationMs(trimmed);
72028
+ if (totalMs === null) return refuse("malformed");
72029
+ if (totalMs < MIN_EXPIRY_MS) return refuse("too-short");
72030
+ if (totalMs > MAX_EXPIRY_MS) return refuse("too-long");
72031
+ return {
72032
+ ok: true,
72033
+ seconds: totalMs / 1e3,
72034
+ value: trimmed
72035
+ };
72036
+ }
72037
+ var REJECTION_DETAIL = {
72038
+ "not-a-string": "value is not a string",
72039
+ malformed: "not a duration string (expected e.g. 24h, 7d, 30d \u2014 a bare number is NOT seconds)",
72040
+ "too-short": `shorter than the ${MIN_EXPIRY_MS / 1e3}s minimum`,
72041
+ "too-long": `longer than the ${MAX_EXPIRY_MS / 864e5}d maximum`
72042
+ };
72043
+ function logTokenExpiryRejection(configured, rejection, logger) {
72044
+ logger.warn(`auth.tokenExpiry REJECTED \u2014 ${REJECTION_DETAIL[rejection.reason]}; signing with the default 30d instead`, { meta: {
72045
+ configured,
72046
+ reason: rejection.reason,
72047
+ applied: rejection.value
72048
+ } });
72049
+ }
71443
72050
  var noopLogger = {
71444
72051
  debug() {
71445
72052
  },
@@ -71460,6 +72067,16 @@ var require_local_auth_addon = __commonJS({
71460
72067
  config;
71461
72068
  jwtSecret;
71462
72069
  logger;
72070
+ /**
72071
+ * Offending `auth.tokenExpiry` values already reported by this instance.
72072
+ *
72073
+ * `signToken` re-reads the setting on EVERY mint so an operator's change
72074
+ * takes effect without a restart — which also means a bad value would log
72075
+ * once per login and once per silent refresh from every viewer. The refusal
72076
+ * must be visible, not repeated: the first mint after a bad save logs, the
72077
+ * rest resolve silently to the same default until the value changes.
72078
+ */
72079
+ reportedInvalidExpiries = /* @__PURE__ */ new Set();
71463
72080
  constructor(config, logger = noopLogger) {
71464
72081
  this.config = config;
71465
72082
  this.logger = logger;
@@ -71474,8 +72091,15 @@ var require_local_auth_addon = __commonJS({
71474
72091
  }
71475
72092
  signToken(payload) {
71476
72093
  const configured = this.config.get("auth.tokenExpiry");
71477
- const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "30d";
71478
- return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn });
72094
+ const parsed = parseTokenExpiry(configured);
72095
+ if (!parsed.ok) {
72096
+ const seen = typeof configured === "string" ? configured : JSON.stringify(configured);
72097
+ if (!this.reportedInvalidExpiries.has(seen)) {
72098
+ this.reportedInvalidExpiries.add(seen);
72099
+ logTokenExpiryRejection(configured, parsed, this.logger);
72100
+ }
72101
+ }
72102
+ return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: parsed.seconds });
71479
72103
  }
71480
72104
  verifyToken(token2) {
71481
72105
  return import_jsonwebtoken.verify(token2, this.jwtSecret);
@@ -73762,6 +74386,30 @@ var require_local_auth_addon = __commonJS({
73762
74386
  return parseRow(results[0].data);
73763
74387
  }
73764
74388
  };
74389
+ function createAuthConfigReader(resolved, writeAuthSection) {
74390
+ return {
74391
+ get(path) {
74392
+ switch (path) {
74393
+ case "auth.jwtSecret":
74394
+ return narrow(resolved.jwtSecret);
74395
+ case "auth.adminUsername":
74396
+ return narrow(resolved.adminUsername);
74397
+ case "auth.adminPassword":
74398
+ return narrow(resolved.adminPassword);
74399
+ case "auth.tokenExpiry":
74400
+ return narrow(resolved.tokenExpiry);
74401
+ default:
74402
+ return;
74403
+ }
74404
+ },
74405
+ update(_section, data) {
74406
+ if (typeof data["jwtSecret"] === "string") writeAuthSection({ jwtSecret: data["jwtSecret"] });
74407
+ }
74408
+ };
74409
+ }
74410
+ function narrow(value) {
74411
+ return value;
74412
+ }
73765
74413
  var USERS_COLLECTION = "users";
73766
74414
  var API_KEYS_COLLECTION = "api_keys";
73767
74415
  var SCOPED_TOKENS_COLLECTION = "scoped_tokens";
@@ -74292,19 +74940,14 @@ var require_local_auth_addon = __commonJS({
74292
74940
  }
74293
74941
  async onInitialize() {
74294
74942
  const authSection = await this.ctx.settings?.getSection("auth") ?? {};
74295
- const resolvedJwtSecret = typeof authSection["jwtSecret"] === "string" ? authSection["jwtSecret"] : this.config.jwtSecret ?? "";
74296
- const resolvedAdminUser = typeof authSection["adminUsername"] === "string" && authSection["adminUsername"] ? authSection["adminUsername"] : this.config.adminUsername ?? "";
74297
- const resolvedAdminPass = typeof authSection["adminPassword"] === "string" && authSection["adminPassword"] ? authSection["adminPassword"] : this.config.adminPassword ?? "";
74298
- const reader = {
74299
- get(path) {
74300
- if (path === "auth.jwtSecret") return resolvedJwtSecret;
74301
- if (path === "auth.adminUsername") return resolvedAdminUser;
74302
- if (path === "auth.adminPassword") return resolvedAdminPass;
74303
- },
74304
- update: (_section, data) => {
74305
- if (typeof data["jwtSecret"] === "string") this.ctx.settings?.setSection("auth", { jwtSecret: data["jwtSecret"] });
74306
- }
74307
- };
74943
+ const reader = createAuthConfigReader({
74944
+ jwtSecret: typeof authSection["jwtSecret"] === "string" ? authSection["jwtSecret"] : this.config.jwtSecret ?? "",
74945
+ adminUsername: typeof authSection["adminUsername"] === "string" && authSection["adminUsername"] ? authSection["adminUsername"] : this.config.adminUsername ?? "",
74946
+ adminPassword: typeof authSection["adminPassword"] === "string" && authSection["adminPassword"] ? authSection["adminPassword"] : this.config.adminPassword ?? "",
74947
+ tokenExpiry: typeof authSection["tokenExpiry"] === "string" && authSection["tokenExpiry"] ? authSection["tokenExpiry"] : void 0
74948
+ }, (patch) => {
74949
+ this.ctx.settings?.setSection("auth", patch);
74950
+ });
74308
74951
  this.authManager = new AuthManager(reader, this.ctx.logger);
74309
74952
  const store = this.ctx.api?.settingsStore;
74310
74953
  if (store) {
@@ -74610,7 +75253,7 @@ var require_loki_logging = __commonJS({
74610
75253
  [Symbol.toStringTag]: { value: "Module" }
74611
75254
  });
74612
75255
  require_chunk_Cek0wNdY();
74613
- var require_dist10 = require_dist_BVU5JADq();
75256
+ var require_dist10 = require_dist_B_mBrEz9();
74614
75257
  function sanitizeLabelName(raw) {
74615
75258
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
74616
75259
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -75175,7 +75818,8 @@ var require_native_metrics_addon = __commonJS({
75175
75818
  [Symbol.toStringTag]: { value: "Module" }
75176
75819
  });
75177
75820
  var require_chunk = require_chunk_Cek0wNdY();
75178
- var require_dist10 = require_dist_BVU5JADq();
75821
+ var require_dist10 = require_dist_B_mBrEz9();
75822
+ var node_fs_promises = __require("fs/promises");
75179
75823
  var node_child_process = __require("child_process");
75180
75824
  var node_util = __require("util");
75181
75825
  var node_os = __require("os");
@@ -75646,11 +76290,893 @@ var require_native_metrics_addon = __commonJS({
75646
76290
  });
75647
76291
  });
75648
76292
  }
75649
- var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
76293
+ var LoadPartition = class {
76294
+ capacity;
76295
+ slots;
76296
+ head = 0;
76297
+ count = 0;
76298
+ rows = 0;
76299
+ /** Newest retained sample's timestamp — the monotonic gate for `push`. */
76300
+ newestAtMs = null;
76301
+ constructor(capacity) {
76302
+ this.capacity = capacity;
76303
+ this.slots = Array.from({ length: capacity });
76304
+ }
76305
+ /**
76306
+ * Accept a sample.
76307
+ *
76308
+ * A sample at or before the newest one already held is REFUSED. The bus drops
76309
+ * a node's own broadcast echo, but a cross-node redelivery or a replayed
76310
+ * subscription must not be able to double a point — and idempotence here is
76311
+ * what lets every reader above treat the series as a set.
76312
+ *
76313
+ * `accepted` is reported separately from `rowDelta` on purpose: a full ring
76314
+ * that evicts a sample of the same size has a delta of zero and is not a
76315
+ * refusal, and conflating the two would silently stop advancing the write
76316
+ * ordinal on a steady-state cluster.
76317
+ */
76318
+ push(sample) {
76319
+ if (this.newestAtMs !== null && sample.atMs <= this.newestAtMs) return {
76320
+ accepted: false,
76321
+ rowDelta: 0
76322
+ };
76323
+ const evicted = this.count === this.capacity ? this.slots[this.head]?.processes.length ?? 0 : 0;
76324
+ this.slots[this.head] = sample;
76325
+ this.head = (this.head + 1) % this.capacity;
76326
+ if (this.count < this.capacity) this.count++;
76327
+ this.newestAtMs = sample.atMs;
76328
+ const rowDelta = sample.processes.length - evicted;
76329
+ this.rows += rowDelta;
76330
+ return {
76331
+ accepted: true,
76332
+ rowDelta
76333
+ };
76334
+ }
76335
+ /** Drop the oldest sample. Returns the rows reclaimed (0 when empty). */
76336
+ dropOldest() {
76337
+ if (this.count === 0) return 0;
76338
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76339
+ const victim = this.slots[index];
76340
+ this.slots[index] = void 0;
76341
+ this.count--;
76342
+ const reclaimed = victim?.processes.length ?? 0;
76343
+ this.rows -= reclaimed;
76344
+ if (this.count === 0) this.newestAtMs = null;
76345
+ return reclaimed;
76346
+ }
76347
+ /** Oldest-first, optionally only what is strictly newer than `sinceMs`. */
76348
+ list(sinceMs) {
76349
+ const out = [];
76350
+ for (let i = 0; i < this.count; i++) {
76351
+ const index = (this.head - this.count + i + this.capacity) % this.capacity;
76352
+ const sample = this.slots[index];
76353
+ if (sample === void 0) continue;
76354
+ if (sinceMs !== void 0 && sample.atMs <= sinceMs) continue;
76355
+ out.push(sample);
76356
+ }
76357
+ return out;
76358
+ }
76359
+ size() {
76360
+ return this.count;
76361
+ }
76362
+ rowCount() {
76363
+ return this.rows;
76364
+ }
76365
+ oldestAtMs() {
76366
+ if (this.count === 0) return null;
76367
+ const index = (this.head - this.count + this.capacity) % this.capacity;
76368
+ return this.slots[index]?.atMs ?? null;
76369
+ }
76370
+ lastWriteAtMs() {
76371
+ return this.newestAtMs;
76372
+ }
76373
+ };
76374
+ var NodeLoadRing = class {
76375
+ partitions = /* @__PURE__ */ new Map();
76376
+ /**
76377
+ * Node id → write ordinal of its last accepted sample. A monotonic counter,
76378
+ * not a clock: partition eviction must follow the order writes actually
76379
+ * happened, and node clocks disagree.
76380
+ */
76381
+ lastWriteSeq = /* @__PURE__ */ new Map();
76382
+ writeSeq = 0;
76383
+ totalRows = 0;
76384
+ samplesPerNode;
76385
+ maxTotalProcessRows;
76386
+ maxNodes;
76387
+ idleEvictionMs;
76388
+ now;
76389
+ constructor(options = {}) {
76390
+ this.samplesPerNode = options.samplesPerNode ?? 180;
76391
+ this.maxTotalProcessRows = options.maxTotalProcessRows ?? 32e3;
76392
+ this.maxNodes = options.maxNodes ?? 16;
76393
+ this.idleEvictionMs = options.idleEvictionMs ?? 36e5;
76394
+ this.now = options.now ?? Date.now;
76395
+ }
76396
+ /**
76397
+ * Retain one snapshot. `processes` is stored by reference — the payload is
76398
+ * already an immutable arrival off the bus, and copying it would double the
76399
+ * measured cost for nothing.
76400
+ *
76401
+ * Returns whether the sample was ACCEPTED — that is, whether it was new
76402
+ * rather than a replay of a timestamp this node has already delivered.
76403
+ *
76404
+ * The return value is not diagnostics. It is the idempotence gate the
76405
+ * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
76406
+ * appended to the table, which is what lets that table carry an `INTEGER`
76407
+ * rowid key instead of a composite unique index over two million rows. The
76408
+ * gate is one monotonic comparison in memory; the index it replaces was
76409
+ * measured at ~40 bytes per row.
76410
+ */
76411
+ record(nodeId, atMs, processes) {
76412
+ if (nodeId.length === 0) return false;
76413
+ this.sweepIdle();
76414
+ let partition = this.partitions.get(nodeId);
76415
+ if (partition === void 0) {
76416
+ partition = new LoadPartition(this.samplesPerNode);
76417
+ this.partitions.set(nodeId, partition);
76418
+ }
76419
+ const outcome = partition.push({
76420
+ atMs,
76421
+ processes
76422
+ });
76423
+ if (!outcome.accepted) {
76424
+ if (partition.size() === 0) this.partitions.delete(nodeId);
76425
+ return false;
76426
+ }
76427
+ this.totalRows += outcome.rowDelta;
76428
+ this.lastWriteSeq.set(nodeId, ++this.writeSeq);
76429
+ if (this.partitions.size > this.maxNodes) this.evictPartitions(this.partitions.size - this.maxNodes, nodeId);
76430
+ this.enforceRowBudget();
76431
+ return true;
76432
+ }
76433
+ /**
76434
+ * Read one node's retained series, oldest-first.
76435
+ *
76436
+ * `sinceMs` is EXCLUSIVE: a caller passes the newest timestamp it already
76437
+ * holds and gets back only what it is missing. That is the whole contract
76438
+ * that lets the admin UI seed from here and then continue live without
76439
+ * doubling a point it already drew.
76440
+ *
76441
+ * A node nobody has recorded answers with an empty series, not an error —
76442
+ * unknown is the truth about a node that has not reported.
76443
+ */
76444
+ read(nodeId, sinceMs) {
76445
+ this.sweepIdle();
76446
+ const partition = this.partitions.get(nodeId);
76447
+ if (partition === void 0) return {
76448
+ nodeId,
76449
+ samples: [],
76450
+ retainedSamples: 0,
76451
+ oldestAtMs: null,
76452
+ capacity: this.samplesPerNode
76453
+ };
76454
+ return {
76455
+ nodeId,
76456
+ samples: partition.list(sinceMs),
76457
+ retainedSamples: partition.size(),
76458
+ oldestAtMs: partition.oldestAtMs(),
76459
+ capacity: this.samplesPerNode
76460
+ };
76461
+ }
76462
+ /** Node ids with a live partition. Observability for the fleet bound. */
76463
+ nodeIds() {
76464
+ return [...this.partitions.keys()];
76465
+ }
76466
+ /** Process rows retained across every partition — the number that IS memory. */
76467
+ rowCount() {
76468
+ return this.totalRows;
76469
+ }
76470
+ /** Samples retained across every partition. */
76471
+ sampleCount() {
76472
+ let total = 0;
76473
+ for (const partition of this.partitions.values()) total += partition.size();
76474
+ return total;
76475
+ }
76476
+ /**
76477
+ * Drop partitions whose newest sample is older than the retention window.
76478
+ *
76479
+ * Lazy, on write and on read — never a timer. A timer would be a new
76480
+ * periodic cost in a subsystem whose entire premise is that it adds none,
76481
+ * and a ring that nobody writes to and nobody reads is not growing either.
76482
+ */
76483
+ sweepIdle() {
76484
+ const cutoff = this.now() - this.idleEvictionMs;
76485
+ for (const [nodeId, partition] of [...this.partitions.entries()]) {
76486
+ const lastWrite = partition.lastWriteAtMs();
76487
+ if (lastWrite !== null && lastWrite > cutoff) continue;
76488
+ this.dropPartition(nodeId);
76489
+ }
76490
+ }
76491
+ /**
76492
+ * Bring the fleet back under the row budget by dropping the OLDEST sample of
76493
+ * the HEAVIEST partition, repeatedly.
76494
+ *
76495
+ * Terminates: every iteration removes one sample from a non-empty partition,
76496
+ * and the population of samples is finite and strictly decreasing.
76497
+ */
76498
+ enforceRowBudget() {
76499
+ while (this.totalRows > this.maxTotalProcessRows) {
76500
+ const victim = this.heaviestPartition();
76501
+ if (victim === null) return;
76502
+ const [nodeId, partition] = victim;
76503
+ this.totalRows -= partition.dropOldest();
76504
+ if (partition.size() === 0) this.dropPartition(nodeId);
76505
+ }
76506
+ }
76507
+ heaviestPartition() {
76508
+ let best = null;
76509
+ for (const entry of this.partitions.entries()) {
76510
+ if (entry[1].size() === 0) continue;
76511
+ if (best === null || entry[1].rowCount() > best[1].rowCount()) best = entry;
76512
+ }
76513
+ return best;
76514
+ }
76515
+ /** Drop `count` whole partitions, least-recently-written first. */
76516
+ evictPartitions(count, protectedNodeId) {
76517
+ const order = [...this.partitions.keys()].filter((nodeId) => nodeId !== protectedNodeId).toSorted((a, b) => (this.lastWriteSeq.get(a) ?? 0) - (this.lastWriteSeq.get(b) ?? 0));
76518
+ let remaining = count;
76519
+ for (const nodeId of order) {
76520
+ if (remaining <= 0) return;
76521
+ this.dropPartition(nodeId);
76522
+ remaining -= 1;
76523
+ }
76524
+ }
76525
+ /** Remove a partition and everything that indexes it. */
76526
+ dropPartition(nodeId) {
76527
+ const partition = this.partitions.get(nodeId);
76528
+ if (partition === void 0) return;
76529
+ this.totalRows -= partition.rowCount();
76530
+ this.partitions.delete(nodeId);
76531
+ this.lastWriteSeq.delete(nodeId);
76532
+ }
76533
+ };
76534
+ var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
76535
+ var DEFAULT_MAX_ROWS = 5e5;
76536
+ var LOAD_SERIES_COLUMNS = [
76537
+ /** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
76538
+ * so it costs no separate index and no stored string. */
76539
+ {
76540
+ name: "id",
76541
+ type: "INTEGER",
76542
+ primaryKey: true,
76543
+ notNull: true
76544
+ },
76545
+ {
76546
+ name: "nodeId",
76547
+ type: "TEXT",
76548
+ notNull: true
76549
+ },
76550
+ /** The EMITTING node's timestamp for the sample this row belongs to. Every
76551
+ * row of one sample shares it — that is what makes a sample reassemblable. */
76552
+ {
76553
+ name: "atMs",
76554
+ type: "INTEGER",
76555
+ notNull: true
76556
+ },
76557
+ {
76558
+ name: "pid",
76559
+ type: "INTEGER",
76560
+ notNull: true
76561
+ },
76562
+ /** `NULL` for a process no addon owns — `root` and `system` both. */
76563
+ {
76564
+ name: "addonId",
76565
+ type: "TEXT"
76566
+ },
76567
+ {
76568
+ name: "classification",
76569
+ type: "TEXT",
76570
+ notNull: true
76571
+ },
76572
+ /** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
76573
+ {
76574
+ name: "cpuDeci",
76575
+ type: "INTEGER",
76576
+ notNull: true
76577
+ },
76578
+ {
76579
+ name: "rssMib",
76580
+ type: "INTEGER",
76581
+ notNull: true
76582
+ },
76583
+ /** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
76584
+ {
76585
+ name: "cpuMainDeci",
76586
+ type: "INTEGER"
76587
+ },
76588
+ /** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
76589
+ {
76590
+ name: "cpuGcDeci",
76591
+ type: "INTEGER"
76592
+ }
76593
+ ];
76594
+ var LOAD_SERIES_INDEXES = [
76595
+ /**
76596
+ * The ONE index, and it serves both jobs.
76597
+ *
76598
+ * Reads are always "this node, newer than T" — `nodeId` leads so the
76599
+ * equality is a range scan and `atMs` supplies the order without a sort.
76600
+ * Prunes are always "this node, oldest first" — the same index, walked from
76601
+ * the other end. A second index on `atMs` alone was measured and rejected:
76602
+ * it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
76603
+ * cross-node prune that a per-node loop over three nodes already serves.
76604
+ */
76605
+ {
76606
+ name: "idx_load_samples_node_at",
76607
+ columns: ["nodeId", "atMs"]
76608
+ }
76609
+ ];
76610
+ var BYTES_PER_MIB = 1048576;
76611
+ function fromDeci(value) {
76612
+ return value === null ? null : Math.round(value) / 10;
76613
+ }
76614
+ function toDeci(value) {
76615
+ return value === null ? null : Math.round(value * 10);
76616
+ }
76617
+ function rowToProcess(row) {
76618
+ return {
76619
+ pid: row.pid,
76620
+ addonId: row.addonId,
76621
+ classification: row.classification,
76622
+ cpuPercent: fromDeci(row.cpuDeci) ?? 0,
76623
+ memoryRssBytes: row.rssMib * BYTES_PER_MIB,
76624
+ cpuMainPercent: fromDeci(row.cpuMainDeci),
76625
+ cpuGcPercent: fromDeci(row.cpuGcDeci)
76626
+ };
76627
+ }
76628
+ function processToRow(nodeId, atMs, process2) {
76629
+ return {
76630
+ nodeId,
76631
+ atMs,
76632
+ pid: process2.pid,
76633
+ addonId: process2.addonId,
76634
+ classification: process2.classification,
76635
+ cpuDeci: toDeci(process2.cpuPercent) ?? 0,
76636
+ rssMib: Math.round(process2.memoryRssBytes / BYTES_PER_MIB),
76637
+ cpuMainDeci: toDeci(process2.cpuMainPercent),
76638
+ cpuGcDeci: toDeci(process2.cpuGcPercent)
76639
+ };
76640
+ }
76641
+ function rowsToSamples(rows) {
76642
+ const byAt = /* @__PURE__ */ new Map();
76643
+ for (const row of rows) {
76644
+ const held = byAt.get(row.atMs);
76645
+ if (held === void 0) byAt.set(row.atMs, [rowToProcess(row)]);
76646
+ else held.push(rowToProcess(row));
76647
+ }
76648
+ return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, processes]) => ({
76649
+ atMs,
76650
+ processes
76651
+ }));
76652
+ }
76653
+ var LoadSeriesStore = class {
76654
+ declared = false;
76655
+ lastPruneAtMs = 0;
76656
+ store;
76657
+ logger;
76658
+ nowFn;
76659
+ prunePageRows;
76660
+ pruneIntervalMs;
76661
+ constructor(deps) {
76662
+ this.store = deps.store;
76663
+ this.logger = deps.logger;
76664
+ this.nowFn = deps.now ?? (() => Date.now());
76665
+ this.prunePageRows = deps.prunePageRows ?? 2e4;
76666
+ this.pruneIntervalMs = deps.pruneIntervalMs ?? 6e4;
76667
+ }
76668
+ /** Idempotently declare the collection. `false` when the store refused. */
76669
+ async declare() {
76670
+ if (this.declared) return true;
76671
+ try {
76672
+ await this.store.declareCollection.mutate({
76673
+ collection: LOAD_SERIES_COLLECTION,
76674
+ columns: [...LOAD_SERIES_COLUMNS],
76675
+ indexes: LOAD_SERIES_INDEXES.map((i) => ({
76676
+ name: i.name,
76677
+ columns: [...i.columns]
76678
+ }))
76679
+ });
76680
+ this.declared = true;
76681
+ return true;
76682
+ } catch (err) {
76683
+ this.logger.warn("load series declareCollection failed \u2014 nothing will be retained on disk", { meta: {
76684
+ collection: LOAD_SERIES_COLLECTION,
76685
+ error: require_dist10.errMsg(err)
76686
+ } });
76687
+ return false;
76688
+ }
76689
+ }
76690
+ /**
76691
+ * Append ONE sample — every process row of it — in ONE transaction.
76692
+ *
76693
+ * Never a write per row. `insertMany` exists for exactly this: at the 10 s
76694
+ * default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
76695
+ * on the connection that also serves every cluster-wide configuration read
76696
+ * is a constant load nobody asked for.
76697
+ */
76698
+ async append(nodeId, atMs, processes) {
76699
+ if (processes.length === 0) return 0;
76700
+ if (!await this.declare()) return 0;
76701
+ const records = processes.map((p) => ({ data: { ...processToRow(nodeId, atMs, p) } }));
76702
+ try {
76703
+ const { inserted } = await this.store.insertMany.mutate({
76704
+ collection: LOAD_SERIES_COLLECTION,
76705
+ records
76706
+ });
76707
+ return inserted;
76708
+ } catch (err) {
76709
+ this.logger.warn("load series sample not retained \u2014 this interval will be missing", { meta: {
76710
+ nodeId,
76711
+ atMs,
76712
+ rows: processes.length,
76713
+ error: require_dist10.errMsg(err)
76714
+ } });
76715
+ return 0;
76716
+ }
76717
+ }
76718
+ /**
76719
+ * Read one node's cold samples, oldest-first.
76720
+ *
76721
+ * `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
76722
+ * timestamp it holds gets only what it is missing. `limitRows` bounds the
76723
+ * read in ROWS (not samples) because rows are what the query costs.
76724
+ */
76725
+ async read(nodeId, sinceMs, limitRows) {
76726
+ if (!await this.declare()) return [];
76727
+ try {
76728
+ const records = await this.store.query.query({
76729
+ collection: LOAD_SERIES_COLLECTION,
76730
+ filter: {
76731
+ where: { nodeId },
76732
+ whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
76733
+ orderBy: {
76734
+ field: "atMs",
76735
+ direction: "asc"
76736
+ },
76737
+ limit: limitRows
76738
+ }
76739
+ });
76740
+ const rows = [];
76741
+ for (const record of records) {
76742
+ const row = recordToRow(record.data);
76743
+ if (row !== null) rows.push(row);
76744
+ }
76745
+ return rowsToSamples(rows);
76746
+ } catch (err) {
76747
+ this.logger.warn("load series cold read failed \u2014 answering from the hot window only", { meta: {
76748
+ nodeId,
76749
+ sinceMs,
76750
+ error: require_dist10.errMsg(err)
76751
+ } });
76752
+ return [];
76753
+ }
76754
+ }
76755
+ /**
76756
+ * Enforce BOTH bounds, oldest-first, through a bounded page each.
76757
+ *
76758
+ * Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
76759
+ * append path must not pay a sweep on every sample.
76760
+ */
76761
+ async prune(nodeIds, retention, force = false) {
76762
+ const now = this.nowFn();
76763
+ if (!force && now - this.lastPruneAtMs < this.pruneIntervalMs) return null;
76764
+ this.lastPruneAtMs = now;
76765
+ if (!await this.declare()) return null;
76766
+ let deletedByAge = 0;
76767
+ let deletedByCap = 0;
76768
+ let rowsExamined = 0;
76769
+ const ageCutoff = now - retention.retentionHours * 36e5;
76770
+ for (const nodeId of nodeIds) {
76771
+ const outcome = await this.pruneNodeToCutoff(nodeId, ageCutoff);
76772
+ deletedByAge += outcome.deleted;
76773
+ rowsExamined += outcome.examined;
76774
+ }
76775
+ const total = await this.count();
76776
+ const excess = total === null ? 0 : total - retention.maxRows;
76777
+ if (excess > 0) {
76778
+ const outcome = await this.pruneOldestRows(nodeIds, excess);
76779
+ deletedByCap = outcome.deleted;
76780
+ rowsExamined += outcome.examined;
76781
+ this.logger.warn("load series ROW CAP bit \u2014 evicting the oldest samples", { meta: {
76782
+ collection: LOAD_SERIES_COLLECTION,
76783
+ rows: total,
76784
+ cap: retention.maxRows,
76785
+ over: excess,
76786
+ deleted: outcome.deleted,
76787
+ retentionHours: retention.retentionHours,
76788
+ hint: "lower the retention or the sampling cadence \u2014 the cap is the guarantee, not the intention"
76789
+ } });
76790
+ }
76791
+ return {
76792
+ deletedByAge,
76793
+ deletedByCap,
76794
+ rowsExamined,
76795
+ capBit: excess > 0
76796
+ };
76797
+ }
76798
+ /** Total rows, or `null` when the store could not answer. */
76799
+ async count() {
76800
+ if (!await this.declare()) return null;
76801
+ try {
76802
+ return await this.store.count.query({ collection: LOAD_SERIES_COLLECTION });
76803
+ } catch (err) {
76804
+ this.logger.warn("load series count failed \u2014 the row cap is not enforced this pass", { meta: { error: require_dist10.errMsg(err) } });
76805
+ return null;
76806
+ }
76807
+ }
76808
+ /**
76809
+ * Delete this node's rows older than `cutoff`, at most one page's worth.
76810
+ *
76811
+ * The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
76812
+ * own is one statement but an UNBOUNDED one — a first pass after a retention
76813
+ * change would delete millions of rows inside a single stalling transaction.
76814
+ * So the page is read first (keys only, ordered by the index), its last
76815
+ * `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
76816
+ */
76817
+ async pruneNodeToCutoff(nodeId, cutoff) {
76818
+ try {
76819
+ const page = await this.store.query.query({
76820
+ collection: LOAD_SERIES_COLLECTION,
76821
+ filter: {
76822
+ where: { nodeId },
76823
+ whereBetween: { atMs: [0, cutoff] },
76824
+ orderBy: {
76825
+ field: "atMs",
76826
+ direction: "asc"
76827
+ },
76828
+ limit: this.prunePageRows
76829
+ },
76830
+ columns: ["atMs"]
76831
+ });
76832
+ if (page.length === 0) return {
76833
+ deleted: 0,
76834
+ examined: 0
76835
+ };
76836
+ const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
76837
+ if (!Number.isFinite(effectiveCutoff)) return {
76838
+ deleted: 0,
76839
+ examined: page.length
76840
+ };
76841
+ const { deleted } = await this.store.deleteWhere.mutate({
76842
+ collection: LOAD_SERIES_COLLECTION,
76843
+ filter: {
76844
+ where: { nodeId },
76845
+ whereBetween: { atMs: [0, effectiveCutoff] }
76846
+ }
76847
+ });
76848
+ return {
76849
+ deleted,
76850
+ examined: page.length
76851
+ };
76852
+ } catch (err) {
76853
+ this.logger.warn("load series age prune failed \u2014 the table keeps growing this pass", { meta: {
76854
+ nodeId,
76855
+ cutoff,
76856
+ error: require_dist10.errMsg(err)
76857
+ } });
76858
+ return {
76859
+ deleted: 0,
76860
+ examined: 0
76861
+ };
76862
+ }
76863
+ }
76864
+ /**
76865
+ * Drop the oldest rows across the known nodes until `excess` is covered.
76866
+ *
76867
+ * Same bounded-page technique, walked per node so the one index serves it.
76868
+ * The node holding the oldest rows pays first, which is also the node
76869
+ * producing the pressure when a runaway process count is the cause.
76870
+ */
76871
+ async pruneOldestRows(nodeIds, excess) {
76872
+ let remaining = Math.min(excess, this.prunePageRows);
76873
+ let deleted = 0;
76874
+ let examined = 0;
76875
+ for (const nodeId of nodeIds) {
76876
+ if (remaining <= 0) break;
76877
+ try {
76878
+ const page = await this.store.query.query({
76879
+ collection: LOAD_SERIES_COLLECTION,
76880
+ filter: {
76881
+ where: { nodeId },
76882
+ orderBy: {
76883
+ field: "atMs",
76884
+ direction: "asc"
76885
+ },
76886
+ limit: remaining
76887
+ },
76888
+ columns: ["atMs"]
76889
+ });
76890
+ examined += page.length;
76891
+ if (page.length === 0) continue;
76892
+ const cutoff = Number(page.at(-1)?.data["atMs"]);
76893
+ if (!Number.isFinite(cutoff)) continue;
76894
+ const result = await this.store.deleteWhere.mutate({
76895
+ collection: LOAD_SERIES_COLLECTION,
76896
+ filter: {
76897
+ where: { nodeId },
76898
+ whereBetween: { atMs: [0, cutoff] }
76899
+ }
76900
+ });
76901
+ deleted += result.deleted;
76902
+ remaining -= result.deleted;
76903
+ } catch (err) {
76904
+ this.logger.warn("load series cap prune failed \u2014 the cap is not enforced this pass", { meta: {
76905
+ nodeId,
76906
+ error: require_dist10.errMsg(err)
76907
+ } });
76908
+ }
76909
+ }
76910
+ return {
76911
+ deleted,
76912
+ examined
76913
+ };
76914
+ }
76915
+ };
76916
+ function recordToRow(data) {
76917
+ const nodeId = data["nodeId"];
76918
+ const classification = data["classification"];
76919
+ if (typeof nodeId !== "string" || typeof classification !== "string") return null;
76920
+ const atMs = Number(data["atMs"]);
76921
+ const pid = Number(data["pid"]);
76922
+ const cpuDeci = Number(data["cpuDeci"]);
76923
+ const rssMib = Number(data["rssMib"]);
76924
+ if (![
76925
+ atMs,
76926
+ pid,
76927
+ cpuDeci,
76928
+ rssMib
76929
+ ].every((n) => Number.isFinite(n))) return null;
76930
+ const rawAddon = data["addonId"];
76931
+ const optional = (raw) => {
76932
+ if (raw === null || raw === void 0) return null;
76933
+ const n = Number(raw);
76934
+ return Number.isFinite(n) ? n : null;
76935
+ };
76936
+ return {
76937
+ nodeId,
76938
+ atMs,
76939
+ pid,
76940
+ addonId: typeof rawAddon === "string" ? rawAddon : null,
76941
+ classification,
76942
+ cpuDeci,
76943
+ rssMib,
76944
+ cpuMainDeci: optional(data["cpuMainDeci"]),
76945
+ cpuGcDeci: optional(data["cpuGcDeci"])
76946
+ };
76947
+ }
76948
+ var LoadSeriesConfigError = class extends Error {
76949
+ constructor(message) {
76950
+ super(message);
76951
+ this.name = "LoadSeriesConfigError";
76952
+ }
76953
+ };
76954
+ function requireInteger(value, field) {
76955
+ const n = typeof value === "number" ? value : Number(value);
76956
+ if (!Number.isFinite(n)) throw new LoadSeriesConfigError(`${field} must be a number, received ${String(value)}`);
76957
+ return Math.round(n);
76958
+ }
76959
+ function resolveLoadSeriesConfig(raw) {
76960
+ let cadenceSec = 10;
76961
+ if (raw.loadSeriesCadenceSec !== void 0 && raw.loadSeriesCadenceSec !== null) {
76962
+ cadenceSec = requireInteger(raw.loadSeriesCadenceSec, "loadSeriesCadenceSec");
76963
+ if (cadenceSec < 5 || cadenceSec > 60) throw new LoadSeriesConfigError(`load series cadence must be between 5 and 60 seconds \u2014 refused ${cadenceSec}`);
76964
+ }
76965
+ let retentionHours = 6;
76966
+ if (raw.loadSeriesRetentionHours !== void 0 && raw.loadSeriesRetentionHours !== null) {
76967
+ retentionHours = requireInteger(raw.loadSeriesRetentionHours, "loadSeriesRetentionHours");
76968
+ if (retentionHours < 1 || retentionHours > 72) throw new LoadSeriesConfigError(`load series retention must be between 1 and 72 hours \u2014 refused ${retentionHours}`);
76969
+ }
76970
+ return {
76971
+ cadenceSec,
76972
+ retentionHours,
76973
+ maxRows: DEFAULT_MAX_ROWS
76974
+ };
76975
+ }
76976
+ function projectLoadSeriesCost(input) {
76977
+ const samplesPerNodeWindow = input.config.retentionHours * 3600 / input.config.cadenceSec;
76978
+ const intendedRows = Math.round(samplesPerNodeWindow * Math.max(input.observedProcessRows, 0));
76979
+ const boundedRows = Math.min(intendedRows, input.config.maxRows);
76980
+ return {
76981
+ intendedRows,
76982
+ boundedRows,
76983
+ estimatedBytes: boundedRows * 87,
76984
+ capBites: intendedRows > input.config.maxRows
76985
+ };
76986
+ }
76987
+ function describeLoadSeriesCost(projection) {
76988
+ const mib = (projection.estimatedBytes / 1048576).toFixed(1);
76989
+ const rows = projection.intendedRows.toLocaleString("en-US");
76990
+ const capped = projection.boundedRows.toLocaleString("en-US");
76991
+ if (!projection.capBites) return `This configuration retains ~${rows} rows \u2248 ${mib} MiB on the hub database (NVMe cache), measured at 87 bytes per row.`;
76992
+ 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.`;
76993
+ }
76994
+ function mergeSamples(cold, hot) {
76995
+ const byAt = /* @__PURE__ */ new Map();
76996
+ for (const sample of cold) byAt.set(sample.atMs, sample);
76997
+ for (const sample of hot) byAt.set(sample.atMs, sample);
76998
+ return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
76999
+ }
77000
+ function toFoldRows(processes) {
77001
+ return processes;
77002
+ }
77003
+ function mergeLoadSeries(input) {
77004
+ const samples = mergeSamples(input.cold, input.hot);
77005
+ if (samples.length === 0) return {
77006
+ series: [],
77007
+ bucketMs: input.cadenceMs,
77008
+ retainedSamples: 0,
77009
+ oldestAtMs: null
77010
+ };
77011
+ const byKey = /* @__PURE__ */ new Map();
77012
+ for (const sample of samples) for (const bucket of require_dist10.foldSnapshotByFunction(toFoldRows(sample.processes), sample.atMs)) {
77013
+ const held = byKey.get(bucket.key);
77014
+ if (held === void 0) byKey.set(bucket.key, {
77015
+ kind: bucket.kind,
77016
+ points: [bucket.point]
77017
+ });
77018
+ else held.points.push(bucket.point);
77019
+ }
77020
+ const oldestAtMs = samples[0]?.atMs ?? null;
77021
+ const newestAtMs = samples.at(-1)?.atMs ?? oldestAtMs;
77022
+ const spanMs = oldestAtMs === null || newestAtMs === null ? 0 : newestAtMs - oldestAtMs + input.cadenceMs;
77023
+ const bucketMs = input.maxPoints === void 0 ? input.cadenceMs : require_dist10.resolveBucketMs(spanMs, input.cadenceMs, input.maxPoints);
77024
+ const origin = oldestAtMs ?? 0;
77025
+ const series = [...byKey.entries()].map(([key, held]) => ({
77026
+ key,
77027
+ kind: held.kind,
77028
+ points: bucketMs > input.cadenceMs ? require_dist10.reducePoints(held.points, bucketMs, origin) : held.points
77029
+ }));
77030
+ const weight = (s) => {
77031
+ const last = s.points.at(-1);
77032
+ if (last === void 0) return -1;
77033
+ if (last.cpuMainPercent !== null && last.cpuGcPercent !== null) return last.cpuMainPercent + last.cpuGcPercent;
77034
+ return last.cpuLifetimePercent;
77035
+ };
77036
+ return {
77037
+ series: series.toSorted((a, b) => {
77038
+ const d = weight(b) - weight(a);
77039
+ return d !== 0 ? d : a.key.localeCompare(b.key);
77040
+ }),
77041
+ bucketMs,
77042
+ retainedSamples: samples.length,
77043
+ oldestAtMs
77044
+ };
77045
+ }
75650
77046
  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|$))/;
77047
+ function classifyProcess(pid, selfPid, managed) {
77048
+ if (pid === selfPid) return "root";
77049
+ if (managed.has(pid)) return "managed";
77050
+ return "system";
77051
+ }
77052
+ function buildNodeProcesses(input) {
77053
+ const out = [];
77054
+ for (const p of input.psRows) {
77055
+ if (!CAMSTACK_CMD_RE.test(p.command)) continue;
77056
+ const managed = input.managed.get(p.pid);
77057
+ const split = input.cpuSplits.get(p.pid);
77058
+ const classification = classifyProcess(p.pid, input.selfPid, input.managed);
77059
+ out.push({
77060
+ pid: p.pid,
77061
+ ppid: p.ppid,
77062
+ pgid: p.pgid,
77063
+ classification,
77064
+ addonId: managed?.addonId ?? null,
77065
+ nodeId: managed?.nodeId ?? (p.pid === input.selfPid ? input.selfNodeId : null),
77066
+ command: p.command,
77067
+ cpuPercent: p.cpuPercent,
77068
+ memoryRssBytes: p.memoryRssBytes,
77069
+ cpuMainPercent: split?.mainPercent ?? null,
77070
+ cpuGcPercent: split?.gcPercent ?? null,
77071
+ threadCount: split?.threadCount ?? null,
77072
+ uptimeSec: p.uptimeSec
77073
+ });
77074
+ }
77075
+ return out;
77076
+ }
77077
+ var CLOCK_TICKS_PER_SEC = 100;
77078
+ var V8_HELPER_THREAD_RE = /^V8Worker/;
77079
+ function parseThreadStat(line) {
77080
+ const close = line.lastIndexOf(")");
77081
+ const open = line.indexOf("(");
77082
+ if (close < 0 || open < 0 || close < open) return null;
77083
+ const comm = line.slice(open + 1, close);
77084
+ const rest = line.slice(close + 1).trim().split(/\s+/);
77085
+ const utime = Number(rest[11]);
77086
+ const stime = Number(rest[12]);
77087
+ const startTicks = Number(rest[19]);
77088
+ if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
77089
+ if (!Number.isFinite(startTicks)) return null;
77090
+ return {
77091
+ comm,
77092
+ ticks: utime + stime,
77093
+ startTicks
77094
+ };
77095
+ }
77096
+ var nodeProcFsReader = {
77097
+ listTaskIds: (pid) => (0, node_fs_promises.readdir)(`/proc/${pid}/task`),
77098
+ readTaskStat: (pid, tid) => (0, node_fs_promises.readFile)(`/proc/${pid}/task/${tid}/stat`, "utf8")
77099
+ };
77100
+ async function readThreadTicks(pid, reader = nodeProcFsReader) {
77101
+ let tids;
77102
+ try {
77103
+ tids = await reader.listTaskIds(pid);
77104
+ } catch {
77105
+ return null;
77106
+ }
77107
+ let mainTicks = 0;
77108
+ let gcTicks = 0;
77109
+ let threadCount = 0;
77110
+ let startTicks = null;
77111
+ const mainThreadTid = String(pid);
77112
+ for (const tid of tids) {
77113
+ let line;
77114
+ try {
77115
+ line = await reader.readTaskStat(pid, tid);
77116
+ } catch {
77117
+ continue;
77118
+ }
77119
+ const parsed = parseThreadStat(line);
77120
+ if (parsed === null) continue;
77121
+ threadCount += 1;
77122
+ if (tid === mainThreadTid) startTicks = parsed.startTicks;
77123
+ if (V8_HELPER_THREAD_RE.test(parsed.comm)) gcTicks += parsed.ticks;
77124
+ else mainTicks += parsed.ticks;
77125
+ }
77126
+ if (threadCount === 0) return null;
77127
+ return {
77128
+ mainTicks,
77129
+ gcTicks,
77130
+ threadCount,
77131
+ atMs: Date.now(),
77132
+ startTicks
77133
+ };
77134
+ }
77135
+ function cpuSplitBetween(prev, next) {
77136
+ const windowMs = next.atMs - prev.atMs;
77137
+ if (windowMs <= 0) return null;
77138
+ if (prev.startTicks === null || next.startTicks === null) return null;
77139
+ if (prev.startTicks !== next.startTicks) return null;
77140
+ const mainDelta = next.mainTicks - prev.mainTicks;
77141
+ const gcDelta = next.gcTicks - prev.gcTicks;
77142
+ if (mainDelta < 0 || gcDelta < 0) return null;
77143
+ const windowTicks = windowMs / 1e3 * CLOCK_TICKS_PER_SEC;
77144
+ const pct = (delta) => Math.round(delta / windowTicks * 1e3) / 10;
77145
+ return {
77146
+ mainPercent: pct(mainDelta),
77147
+ gcPercent: pct(gcDelta),
77148
+ threadCount: next.threadCount
77149
+ };
77150
+ }
77151
+ var ThreadCpuTracker = class {
77152
+ reader;
77153
+ previous = /* @__PURE__ */ new Map();
77154
+ constructor(reader = nodeProcFsReader) {
77155
+ this.reader = reader;
77156
+ }
77157
+ /**
77158
+ * Sample `pids` and return the split for each one that HAS a usable previous
77159
+ * sample. A pid absent from the result has no answer yet — the caller must
77160
+ * report `null`, not `0`.
77161
+ */
77162
+ async sample(pids) {
77163
+ const out = /* @__PURE__ */ new Map();
77164
+ const nextPrevious = /* @__PURE__ */ new Map();
77165
+ for (const pid of pids) {
77166
+ const next = await readThreadTicks(pid, this.reader);
77167
+ if (next === null) continue;
77168
+ nextPrevious.set(pid, next);
77169
+ const prev = this.previous.get(pid);
77170
+ if (prev === void 0) continue;
77171
+ const split = cpuSplitBetween(prev, next);
77172
+ if (split !== null) out.set(pid, split);
77173
+ }
77174
+ this.previous = nextPrevious;
77175
+ return out;
77176
+ }
77177
+ };
77178
+ var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
75652
77179
  var METRICS_SNAPSHOT_INTERVAL_MS = 5e3;
75653
- var PROCESS_SNAPSHOT_INTERVAL_MS = 2e4;
75654
77180
  var METRICS_SNAPSHOT_HEARTBEAT_MS = 6e4;
75655
77181
  function coarsenResourcesSnapshot(snapshot) {
75656
77182
  if (!snapshot || typeof snapshot !== "object") return JSON.stringify(snapshot);
@@ -75669,18 +77195,6 @@ var require_native_metrics_addon = __commonJS({
75669
77195
  };
75670
77196
  return JSON.stringify(round(snapshot));
75671
77197
  }
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
77198
  function narrowWorkerState(state) {
75685
77199
  switch (state) {
75686
77200
  case "starting":
@@ -75693,7 +77207,7 @@ var require_native_metrics_addon = __commonJS({
75693
77207
  return "running";
75694
77208
  }
75695
77209
  }
75696
- var NativeMetricsAddon = class extends require_dist10.BaseAddon {
77210
+ var NativeMetricsAddon = class NativeMetricsAddon2 extends require_dist10.BaseAddon {
75697
77211
  provider = null;
75698
77212
  startedAtMs = Date.now();
75699
77213
  snapshotTimer = null;
@@ -75705,9 +77219,77 @@ var require_native_metrics_addon = __commonJS({
75705
77219
  * elapsed) is skipped.
75706
77220
  */
75707
77221
  lastResourcesEmit = null;
75708
- lastProcessesEmit = null;
77222
+ /**
77223
+ * Holds the previous `/proc/<pid>/task/*` tick counts so each process scan
77224
+ * can turn them into an instantaneous CPU rate split between the process's
77225
+ * own threads and V8's helper pool. Self-bounding — see `ThreadCpuTracker`.
77226
+ */
77227
+ threadCpu = new ThreadCpuTracker();
77228
+ /**
77229
+ * Retention for the snapshots this addon emits. Fed by SUBSCRIBING to
77230
+ * `metrics.node-processes-snapshot`, never by a second sampler — on the hub
77231
+ * that bus carries every node, so the hub's ring is the cluster's. Bounds and
77232
+ * measured cost live in `node-load-ring.ts`.
77233
+ */
77234
+ loadRing = new NodeLoadRing();
77235
+ /**
77236
+ * The COLD tier, and it exists only on the HUB.
77237
+ *
77238
+ * The table is hub-resident and the hub already hears every node's snapshot
77239
+ * on its bus, so the hub's writer is the cluster's. An agent writing through
77240
+ * the `settings-store` singleton would ship its rows over Moleculer, once
77241
+ * per sample, to reach that very same table.
77242
+ */
77243
+ loadStore = null;
77244
+ /** The resolved knobs. Re-resolved on every settings write. */
77245
+ loadConfig = {
77246
+ cadenceSec: 10,
77247
+ retentionHours: 6,
77248
+ maxRows: DEFAULT_MAX_ROWS
77249
+ };
77250
+ /**
77251
+ * Process rows seen in each node's most recent snapshot — the OBSERVED
77252
+ * numbers the settings form projects its cost from. A constant here would be
77253
+ * a projection that stops being true the first time the fleet changes.
77254
+ */
77255
+ observedRowsByNode = /* @__PURE__ */ new Map();
75709
77256
  constructor() {
75710
- super({ samplingIntervalMs: 5e3 });
77257
+ super({
77258
+ samplingIntervalMs: 5e3,
77259
+ loadSeriesCadenceSec: 10,
77260
+ loadSeriesRetentionHours: 6
77261
+ });
77262
+ }
77263
+ /** Is this the hub? The same test every other addon uses (`addon-ai`). */
77264
+ get isHub() {
77265
+ return (this.ctx.kernel.cluster?.broker?.nodeID ?? "hub") === "hub";
77266
+ }
77267
+ /** Process rows observed across the whole fleet, for the cost projection. */
77268
+ observedFleetRows() {
77269
+ let total = 0;
77270
+ for (const rows of this.observedRowsByNode.values()) total += rows;
77271
+ return total;
77272
+ }
77273
+ /**
77274
+ * Project a `NodeProcess` onto the series' own row.
77275
+ *
77276
+ * `command`, `ppid`, `pgid`, `nodeId`, `threadCount` and `uptimeSec` are
77277
+ * dropped here, at the single point both tiers pass through, so the hot ring
77278
+ * and the cold table carry the SAME shape and the merged read cannot tell
77279
+ * them apart. `command` in particular is the fattest field in a snapshot and
77280
+ * the same identical string on every runner — the runner id travels in the
77281
+ * environment, not in argv — and nothing that draws this series reads it.
77282
+ */
77283
+ static toRetained(process2) {
77284
+ return {
77285
+ pid: process2.pid,
77286
+ addonId: process2.addonId,
77287
+ classification: process2.classification,
77288
+ cpuPercent: process2.cpuPercent,
77289
+ memoryRssBytes: process2.memoryRssBytes,
77290
+ cpuMainPercent: process2.cpuMainPercent,
77291
+ cpuGcPercent: process2.cpuGcPercent
77292
+ };
75711
77293
  }
75712
77294
  async onInitialize() {
75713
77295
  const provider = new NativeMetricsProvider();
@@ -75727,16 +77309,127 @@ var require_native_metrics_addon = __commonJS({
75727
77309
  listAddonInstances: () => this.listAddonInstances(),
75728
77310
  getAddonStats: (params) => this.getAddonStats(params.addonId),
75729
77311
  listNodeProcesses: () => this.listNodeProcesses(),
75730
- killProcess: (params) => this.killProcess(params),
77312
+ getLoadSeries: (params) => this.readLoadSeries(params),
75731
77313
  dumpHeapSnapshot: (params) => this.dumpHeapSnapshot(params)
75732
77314
  };
77315
+ this.applyLoadSeriesConfig();
77316
+ if (this.isHub) this.loadStore = new LoadSeriesStore({
77317
+ store: this.ctx.api.settingsStore,
77318
+ logger: this.ctx.logger.child("LoadSeries")
77319
+ });
75733
77320
  this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
75734
- this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), PROCESS_SNAPSHOT_INTERVAL_MS);
77321
+ this.startProcessSnapshotTimer();
77322
+ 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
77323
  return [{
75736
77324
  capability: require_dist10.metricsProviderCapability,
75737
77325
  provider: composed
75738
77326
  }];
75739
77327
  }
77328
+ /**
77329
+ * Retain one arriving snapshot in BOTH tiers.
77330
+ *
77331
+ * The ring is written FIRST and it is the gate: `record` refuses a timestamp
77332
+ * this node has already delivered, and only an accepted sample reaches the
77333
+ * table. That is what makes the append idempotent without a unique index
77334
+ * over two million rows — the composite key it replaces was measured at ~40
77335
+ * bytes per row (`load-series-store.ts`).
77336
+ *
77337
+ * The durable append is fire-and-forget: a storage stall must cost a gap in
77338
+ * the cold window, never a blocked event-bus handler. Every failure logs.
77339
+ */
77340
+ retainSnapshot(nodeId, atMs, processes) {
77341
+ const retained = processes.map((p) => NativeMetricsAddon2.toRetained(p));
77342
+ if (!this.loadRing.record(nodeId, atMs, retained)) return;
77343
+ this.observedRowsByNode.set(nodeId, retained.length);
77344
+ const store = this.loadStore;
77345
+ if (store === null) return;
77346
+ store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).catch((err) => {
77347
+ this.ctx.logger.warn("durable load series write failed", { meta: {
77348
+ nodeId,
77349
+ atMs,
77350
+ error: err instanceof Error ? err.message : String(err)
77351
+ } });
77352
+ });
77353
+ }
77354
+ /**
77355
+ * The ONE reader, over both tiers.
77356
+ *
77357
+ * Cold first, hot second, merged and deduped on `atMs` — see
77358
+ * `load-series-reader.ts`. The cold read is bounded in ROWS, because rows
77359
+ * are what a query costs, and the fold + reduction happen HERE rather than
77360
+ * in the browser: six hours at the 10 s cadence is 2 160 snapshots, and
77361
+ * shipping them to a page that will discard most of them is precisely the
77362
+ * cost this subsystem exists to avoid.
77363
+ */
77364
+ async readLoadSeries(params) {
77365
+ const cadenceMs = this.loadConfig.cadenceSec * 1e3;
77366
+ const sinceMs = params.sinceMs ?? 0;
77367
+ const hotSamples = this.loadRing.read(params.forNodeId, params.sinceMs).samples.map((sample) => ({
77368
+ atMs: sample.atMs,
77369
+ processes: sample.processes
77370
+ }));
77371
+ const store = this.loadStore;
77372
+ const merged = mergeLoadSeries({
77373
+ cold: store === null ? [] : await store.read(params.forNodeId, sinceMs, this.loadConfig.maxRows),
77374
+ hot: hotSamples,
77375
+ cadenceMs,
77376
+ ...params.maxPoints !== void 0 ? { maxPoints: params.maxPoints } : {}
77377
+ });
77378
+ return {
77379
+ nodeId: params.forNodeId,
77380
+ series: merged.series,
77381
+ bucketMs: merged.bucketMs,
77382
+ retainedSamples: merged.retainedSamples,
77383
+ oldestAtMs: merged.oldestAtMs,
77384
+ cadenceMs,
77385
+ durable: store !== null
77386
+ };
77387
+ }
77388
+ /**
77389
+ * Re-resolve the knobs and restate the fixed cadence.
77390
+ *
77391
+ * A REFUSED value (out of 5-60 s, or out of 1-72 h) leaves the previous
77392
+ * configuration in force and says so. Refused, never clamped: storing 10
77393
+ * when the operator typed 2 and reading 10 back is a knob and a readback
77394
+ * agreeing on a value nobody chose.
77395
+ */
77396
+ applyLoadSeriesConfig() {
77397
+ try {
77398
+ const next = resolveLoadSeriesConfig({
77399
+ loadSeriesCadenceSec: this.config.loadSeriesCadenceSec,
77400
+ loadSeriesRetentionHours: this.config.loadSeriesRetentionHours
77401
+ });
77402
+ const changed = next.cadenceSec !== this.loadConfig.cadenceSec || next.retentionHours !== this.loadConfig.retentionHours;
77403
+ this.loadConfig = next;
77404
+ if (changed) {
77405
+ const projection = projectLoadSeriesCost({
77406
+ config: next,
77407
+ observedProcessRows: this.observedFleetRows()
77408
+ });
77409
+ this.ctx.logger.info("load series configuration applied", { meta: {
77410
+ nodeId: this.ctx.kernel.localNodeId ?? this.ctx.id,
77411
+ cadenceSec: next.cadenceSec,
77412
+ retentionHours: next.retentionHours,
77413
+ maxRows: next.maxRows,
77414
+ projectedRows: projection.intendedRows,
77415
+ projectedMib: Math.round(projection.estimatedBytes / 1048576 * 10) / 10,
77416
+ capBites: projection.capBites
77417
+ } });
77418
+ }
77419
+ } catch (err) {
77420
+ this.ctx.logger.warn("load series configuration REFUSED \u2014 keeping the previous values", { meta: {
77421
+ nodeId: this.ctx.kernel.localNodeId ?? this.ctx.id,
77422
+ cadenceSec: this.loadConfig.cadenceSec,
77423
+ retentionHours: this.loadConfig.retentionHours,
77424
+ error: err instanceof Error ? err.message : String(err)
77425
+ } });
77426
+ }
77427
+ }
77428
+ /** (Re)arm the fixed-cadence process-tree timer at the configured interval. */
77429
+ startProcessSnapshotTimer() {
77430
+ if (this.processSnapshotTimer) clearInterval(this.processSnapshotTimer);
77431
+ this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), this.loadConfig.cadenceSec * 1e3);
77432
+ }
75740
77433
  async onShutdown() {
75741
77434
  if (this.snapshotTimer) {
75742
77435
  clearInterval(this.snapshotTimer);
@@ -75793,10 +77486,14 @@ var require_native_metrics_addon = __commonJS({
75793
77486
  }
75794
77487
  }
75795
77488
  /**
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.
77489
+ * Emit one `metrics.node-processes-snapshot` for this node.
77490
+ *
77491
+ * Heavy a full OS `ps -eo` scan plus a `$process.list` broker call — and
77492
+ * UNCONDITIONAL. The change-detection that used to guard it is gone: with a
77493
+ * fixed cadence a missing interval means exactly one thing, and that is the
77494
+ * property an operator investigating a spike is actually looking for.
77495
+ *
77496
+ * A failed scan emits nothing, which is the same signal: nobody reported.
75800
77497
  */
75801
77498
  async emitProcessesSnapshot() {
75802
77499
  const eventBus = this.ctx.eventBus;
@@ -75805,30 +77502,27 @@ var require_native_metrics_addon = __commonJS({
75805
77502
  const timestamp = Date.now();
75806
77503
  try {
75807
77504
  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 {
77505
+ eventBus.emit(require_dist10.createEvent(require_dist10.EventCategory.MetricsNodeProcessesSnapshot, {
77506
+ type: "node",
77507
+ id: nodeId,
77508
+ nodeId
77509
+ }, {
77510
+ nodeId,
77511
+ processes,
77512
+ timestamp
77513
+ }));
77514
+ } catch (err) {
77515
+ this.ctx.logger.warn("process snapshot skipped \u2014 this interval will be missing", { meta: {
77516
+ nodeId,
77517
+ error: err instanceof Error ? err.message : String(err)
77518
+ } });
75827
77519
  }
75828
77520
  }
75829
77521
  async onConfigChanged() {
75830
77522
  this.provider?.stopSampling();
75831
77523
  this.provider?.startSampling(this.config.samplingIntervalMs);
77524
+ this.applyLoadSeriesConfig();
77525
+ this.startProcessSnapshotTimer();
75832
77526
  }
75833
77527
  async listWorkerInstances() {
75834
77528
  const broker = this.ctx.kernel.cluster?.broker;
@@ -75872,128 +77566,29 @@ var require_native_metrics_addon = __commonJS({
75872
77566
  /**
75873
77567
  * Walk the OS process table and classify each camstack-shaped process.
75874
77568
  *
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.
77569
+ * Classification is IDENTITY-driven: `root` is this pid, `managed` is a pid
77570
+ * the kernel's `$process.list` names, and every other camstack-shaped
77571
+ * process is `system`. The rules and why there is no longer an ancestry
77572
+ * walk behind them live in `process-classification.ts`.
75893
77573
  */
75894
77574
  async listNodeProcesses() {
75895
77575
  const ps = await this.runPs();
75896
77576
  if (ps.length === 0) return [];
75897
- const managedPids = /* @__PURE__ */ new Map();
77577
+ const managed = /* @__PURE__ */ new Map();
75898
77578
  const workers = await this.listWorkerInstances();
75899
- for (const w of workers) managedPids.set(w.pid, {
77579
+ for (const w of workers) managed.set(w.pid, {
75900
77580
  addonId: w.addonId,
75901
77581
  nodeId: w.nodeId
75902
77582
  });
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
77583
+ const camstackPids = ps.filter((p) => CAMSTACK_CMD_RE.test(p.command)).map((p) => p.pid);
77584
+ const cpuSplits = await this.threadCpu.sample(camstackPids);
77585
+ return buildNodeProcesses({
77586
+ psRows: ps,
77587
+ selfPid: process.pid,
77588
+ selfNodeId: this.ctx.kernel.cluster?.broker?.nodeID ?? "hub",
77589
+ managed,
77590
+ cpuSplits
75909
77591
  });
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
77592
  }
75998
77593
  /**
75999
77594
  * Ask the addon's forked runner to write a V8 heap snapshot (SIGUSR2 → the
@@ -76070,7 +77665,29 @@ var require_native_metrics_addon = __commonJS({
76070
77665
  return [];
76071
77666
  }
76072
77667
  }
77668
+ /**
77669
+ * The knobs live HERE, on the document this addon already owns.
77670
+ *
77671
+ * Fields on an existing document, never a method per knob —
77672
+ * `system.getLoggingSettings` / `setLoggingSettings` set that precedent.
77673
+ * They are deliberately NOT in the logging document: that one is about
77674
+ * levels and diagnostic windows, and two documents both claiming a knob is
77675
+ * how this repo has already shipped a switch nobody read.
77676
+ *
77677
+ * Cluster-wide, not per-node: every node must emit on the same cadence or
77678
+ * the fleet's series cannot be laid over each other, and the table is single
77679
+ * and hub-resident, so a per-node retention would be a promise nothing could
77680
+ * keep.
77681
+ *
77682
+ * The cost line is computed from OBSERVED numbers — the nodes and process
77683
+ * counts the cluster is actually reporting — so an operator raising the
77684
+ * retention sees what it costs BEFORE applying it, not afterwards.
77685
+ */
76073
77686
  globalSettingsSchema() {
77687
+ const projection = projectLoadSeriesCost({
77688
+ config: this.loadConfig,
77689
+ observedProcessRows: this.observedFleetRows()
77690
+ });
76074
77691
  return this.schema({ sections: [{
76075
77692
  id: "native-metrics-settings",
76076
77693
  title: "System Metrics",
@@ -76085,6 +77702,47 @@ var require_native_metrics_addon = __commonJS({
76085
77702
  default: 5e3,
76086
77703
  unit: "ms"
76087
77704
  })]
77705
+ }, {
77706
+ id: "native-metrics-load-series",
77707
+ title: "Load history",
77708
+ fields: [
77709
+ this.field({
77710
+ type: "number",
77711
+ key: "loadSeriesCadenceSec",
77712
+ label: "Sampling cadence",
77713
+ 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.",
77714
+ min: 5,
77715
+ max: 60,
77716
+ step: 1,
77717
+ default: 10,
77718
+ unit: "s"
77719
+ }),
77720
+ this.field({
77721
+ type: "number",
77722
+ key: "loadSeriesRetentionHours",
77723
+ label: "Retention",
77724
+ 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.",
77725
+ min: 1,
77726
+ max: 72,
77727
+ step: 1,
77728
+ default: 6,
77729
+ unit: "h"
77730
+ }),
77731
+ {
77732
+ type: "info",
77733
+ key: "load-series-cost",
77734
+ label: "What this configuration costs",
77735
+ content: describeLoadSeriesCost(projection),
77736
+ variant: projection.capBites ? "warning" : "info"
77737
+ },
77738
+ {
77739
+ type: "info",
77740
+ key: "load-series-cap",
77741
+ label: "Hard row cap",
77742
+ 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.`,
77743
+ variant: "info"
77744
+ }
77745
+ ]
76088
77746
  }] });
76089
77747
  }
76090
77748
  };
@@ -76117,7 +77775,7 @@ var require_filesystem_storage_addon = __commonJS({
76117
77775
  [Symbol.toStringTag]: { value: "Module" }
76118
77776
  });
76119
77777
  var require_chunk = require_chunk_Cek0wNdY();
76120
- var require_dist10 = require_dist_BVU5JADq();
77778
+ var require_dist10 = require_dist_B_mBrEz9();
76121
77779
  var node_crypto = __require("crypto");
76122
77780
  var node_fs_promises = __require("fs/promises");
76123
77781
  var node_path = __require("path");
@@ -77233,8 +78891,8 @@ var require_sqlite_settings_addon = __commonJS({
77233
78891
  [Symbol.toStringTag]: { value: "Module" }
77234
78892
  });
77235
78893
  var require_chunk = require_chunk_Cek0wNdY();
77236
- var require_dist10 = require_dist_BVU5JADq();
77237
- var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
78894
+ var require_dist10 = require_dist_B_mBrEz9();
78895
+ var require_retired_settings_keys = require_retired_settings_keys_Davtjo5p();
77238
78896
  var node_crypto = __require("crypto");
77239
78897
  var node_fs = __require("fs");
77240
78898
  var node_module = __require("module");
@@ -77861,7 +79519,8 @@ var require_sqlite_settings_addon = __commonJS({
77861
79519
  this.declaredCollections.set(collection, {
77862
79520
  primaryKey: "id",
77863
79521
  columns: /* @__PURE__ */ new Set(["data"]),
77864
- booleanColumns: /* @__PURE__ */ new Set()
79522
+ booleanColumns: /* @__PURE__ */ new Set(),
79523
+ autoPrimaryKey: false
77865
79524
  });
77866
79525
  }
77867
79526
  if (await this.isEmpty({ collection: "system-settings" })) await this.seedDefaults();
@@ -77877,7 +79536,8 @@ var require_sqlite_settings_addon = __commonJS({
77877
79536
  const decl = {
77878
79537
  primaryKey: "id",
77879
79538
  columns: /* @__PURE__ */ new Set(["data"]),
77880
- booleanColumns: /* @__PURE__ */ new Set()
79539
+ booleanColumns: /* @__PURE__ */ new Set(),
79540
+ autoPrimaryKey: false
77881
79541
  };
77882
79542
  this.declaredCollections.set(scoped, decl);
77883
79543
  return decl;
@@ -77981,6 +79641,48 @@ var require_sqlite_settings_addon = __commonJS({
77981
79641
  else for (const [k, v] of Object.entries(record.data)) if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
77982
79642
  await this.tableInsert(scoped, row);
77983
79643
  }
79644
+ /**
79645
+ * Insert a batch in ONE transaction, on ONE prepared statement.
79646
+ *
79647
+ * The write-side twin of {@link deleteWhere}. Every row in the batch shares
79648
+ * a single column list, which is what makes one `prepare` legal: rows are
79649
+ * normalised to the collection's DECLARED column set, so a row that omits a
79650
+ * column binds `null` for it rather than producing a second statement shape.
79651
+ *
79652
+ * All or nothing — `better-sqlite3`'s `transaction()` rolls the whole batch
79653
+ * back on any throw. A caller writing one process sample gets one COMMIT,
79654
+ * which is the entire point: 76 rows every 10 s is 7.6 rows/s, and 7.6
79655
+ * separate commits per second on the connection that also serves every
79656
+ * cluster-wide configuration read is a constant load nobody asked for.
79657
+ */
79658
+ async insertMany({ namespace, collection, records }) {
79659
+ if (records.length === 0) return { inserted: 0 };
79660
+ const scoped = this.scopedName(namespace, collection);
79661
+ const decl = this.requireDeclared(scoped);
79662
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
79663
+ const dataColumns = [...decl.columns];
79664
+ const keys = decl.autoPrimaryKey ? dataColumns : [decl.primaryKey, ...dataColumns];
79665
+ const sql = `INSERT INTO "${scoped}" (${keys.map((k) => `"${k}"`).join(", ")}) VALUES (${keys.map(() => "?").join(", ")})`;
79666
+ const batch = records.map((record) => {
79667
+ const row = {};
79668
+ if (isKvShape) row["data"] = JSON.stringify(record.data);
79669
+ else for (const [k, v] of Object.entries(record.data)) if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
79670
+ if (!decl.autoPrimaryKey) row[decl.primaryKey] = record.id || (0, node_crypto.randomUUID)();
79671
+ return keys.map((k) => row[k] ?? null);
79672
+ });
79673
+ const db = this.getDb();
79674
+ const stmt = db.prepare(sql);
79675
+ const run = db.transaction((rows) => {
79676
+ for (const row of rows) stmt.run(...row);
79677
+ });
79678
+ this.measured({
79679
+ op: "insertMany",
79680
+ collection: scoped,
79681
+ sql,
79682
+ params: batch[0] ?? []
79683
+ }, () => run(batch));
79684
+ return { inserted: batch.length };
79685
+ }
77984
79686
  async update({ namespace, collection, id, data }) {
77985
79687
  const scoped = this.scopedName(namespace, collection);
77986
79688
  const decl = this.requireDeclared(scoped);
@@ -78655,7 +80357,8 @@ var require_sqlite_settings_addon = __commonJS({
78655
80357
  this.declaredCollections.set(table, {
78656
80358
  primaryKey,
78657
80359
  columns: columnNames,
78658
- booleanColumns
80360
+ booleanColumns,
80361
+ autoPrimaryKey: pkCol?.type === "INTEGER"
78659
80362
  });
78660
80363
  }
78661
80364
  /** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
@@ -79468,7 +81171,7 @@ var require_storage_orchestrator_addon = __commonJS({
79468
81171
  [Symbol.toStringTag]: { value: "Module" }
79469
81172
  });
79470
81173
  var require_chunk = require_chunk_Cek0wNdY();
79471
- var require_dist10 = require_dist_BVU5JADq();
81174
+ var require_dist10 = require_dist_B_mBrEz9();
79472
81175
  var node_crypto = __require("crypto");
79473
81176
  var node_fs_promises = __require("fs/promises");
79474
81177
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -80032,6 +81735,7 @@ var require_storage_orchestrator_addon = __commonJS({
80032
81735
  set: async (input) => (await engine()).set(input),
80033
81736
  query: async (input) => (await engine()).query(input),
80034
81737
  insert: async (input) => (await engine()).insert(input),
81738
+ insertMany: async (input) => (await engine()).insertMany(input),
80035
81739
  update: async (input) => (await engine()).update(input),
80036
81740
  delete: async (input) => (await engine()).delete(input),
80037
81741
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
@@ -81348,7 +83052,7 @@ var require_system_config_addon = __commonJS({
81348
83052
  [Symbol.toStringTag]: { value: "Module" }
81349
83053
  });
81350
83054
  require_chunk_Cek0wNdY();
81351
- var require_dist10 = require_dist_BVU5JADq();
83055
+ var require_dist10 = require_dist_B_mBrEz9();
81352
83056
  var SECTION_TITLES = {
81353
83057
  server: "Server",
81354
83058
  auth: "Authentication"
@@ -81413,7 +83117,7 @@ var require_system_config_addon = __commonJS({
81413
83117
  type: "text",
81414
83118
  key: "tokenExpiry",
81415
83119
  label: "Token Expiry",
81416
- description: "JWT token lifetime (e.g. 24h, 7d, 30d)",
83120
+ description: "Lifetime of a session token, as a duration string (e.g. 24h, 7d, 30d). A bare number is rejected \u2014 it would be read as milliseconds. Applies to tokens issued from the next login or silent refresh; tokens already handed out keep the lifetime they were signed with.",
81417
83121
  placeholder: "30d",
81418
83122
  default: "30d"
81419
83123
  }]
@@ -99409,7 +101113,7 @@ var require_winston_logging = __commonJS({
99409
101113
  [Symbol.toStringTag]: { value: "Module" }
99410
101114
  });
99411
101115
  var require_chunk = require_chunk_Cek0wNdY();
99412
- var require_dist10 = require_dist_BVU5JADq();
101116
+ var require_dist10 = require_dist_B_mBrEz9();
99413
101117
  var require_formatter = require_formatter_DqAKDlvN();
99414
101118
  var node_path = __require("path");
99415
101119
  node_path = require_chunk.__toESM(node_path);
@@ -101185,9 +102889,9 @@ ${(0, node_fs.readFileSync)(ca.caCertPath, "utf-8").trimEnd()}
101185
102889
  }
101186
102890
  });
101187
102891
 
101188
- // ../types/dist/event-category-EY0GNjV9.js
101189
- var require_event_category_EY0GNjV9 = __commonJS({
101190
- "../types/dist/event-category-EY0GNjV9.js"(exports) {
102892
+ // ../types/dist/event-category-BaEgqJNv.js
102893
+ var require_event_category_BaEgqJNv = __commonJS({
102894
+ "../types/dist/event-category-BaEgqJNv.js"(exports) {
101191
102895
  "use strict";
101192
102896
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
101193
102897
  EventCategory2["SystemBoot"] = "system.boot";
@@ -101352,11 +103056,11 @@ var require_event_category_EY0GNjV9 = __commonJS({
101352
103056
  }
101353
103057
  });
101354
103058
 
101355
- // ../types/dist/sleep-CSodb2vQ.js
101356
- var require_sleep_CSodb2vQ = __commonJS({
101357
- "../types/dist/sleep-CSodb2vQ.js"(exports) {
103059
+ // ../types/dist/sleep-9d8tJRbO.js
103060
+ var require_sleep_9d8tJRbO = __commonJS({
103061
+ "../types/dist/sleep-9d8tJRbO.js"(exports) {
101358
103062
  "use strict";
101359
- var require_event_category = require_event_category_EY0GNjV9();
103063
+ var require_event_category = require_event_category_BaEgqJNv();
101360
103064
  var zod = require_zod();
101361
103065
  var WELL_KNOWN_TABS = [
101362
103066
  {
@@ -104907,8 +106611,8 @@ var require_addon = __commonJS({
104907
106611
  "../types/dist/addon.js"(exports) {
104908
106612
  "use strict";
104909
106613
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
104910
- var require_event_category = require_event_category_EY0GNjV9();
104911
- var require_sleep = require_sleep_CSodb2vQ();
106614
+ var require_event_category = require_event_category_BaEgqJNv();
106615
+ var require_sleep = require_sleep_9d8tJRbO();
104912
106616
  var require_err_msg = require_err_msg_COpsHMw2();
104913
106617
  var CAP_INPUT_DEFAULTS = Object.freeze({
104914
106618
  "addons": { "getLogs": { "limit": 100 } },
@@ -105197,6 +106901,7 @@ var require_addon = __commonJS({
105197
106901
  "listProfiles",
105198
106902
  "listRuntimeNodes"
105199
106903
  ],
106904
+ "load-contribution": ["list"],
105200
106905
  "log-channels": ["list"],
105201
106906
  "log-destination": ["query"],
105202
106907
  "login-method": ["getLoginMethods"],
@@ -111785,12 +113490,12 @@ var require_dist2 = __commonJS({
111785
113490
  }
111786
113491
  });
111787
113492
 
111788
- // ../system/dist/manifest-python-deps-CktMcXzS.js
111789
- var require_manifest_python_deps_CktMcXzS = __commonJS({
111790
- "../system/dist/manifest-python-deps-CktMcXzS.js"(exports) {
113493
+ // ../system/dist/manifest-python-deps-BV_Cy99l.js
113494
+ var require_manifest_python_deps_BV_Cy99l = __commonJS({
113495
+ "../system/dist/manifest-python-deps-BV_Cy99l.js"(exports) {
111791
113496
  "use strict";
111792
113497
  var require_chunk = require_chunk_Cek0wNdY();
111793
- require_dist_BVU5JADq();
113498
+ require_dist_B_mBrEz9();
111794
113499
  var node_crypto = __require("crypto");
111795
113500
  node_crypto = require_chunk.__toESM(node_crypto);
111796
113501
  var _camstack_types_node = require_node();
@@ -122905,7 +124610,7 @@ var require_dist3 = __commonJS({
122905
124610
  "use strict";
122906
124611
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
122907
124612
  var require_chunk = require_chunk_Cek0wNdY();
122908
- var require_dist10 = require_dist_BVU5JADq();
124613
+ var require_dist10 = require_dist_B_mBrEz9();
122909
124614
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
122910
124615
  require_alerts();
122911
124616
  var require_formatter = require_formatter_DqAKDlvN();
@@ -122931,7 +124636,7 @@ var require_dist3 = __commonJS({
122931
124636
  var require_builtins_winston_logging_index = require_winston_logging();
122932
124637
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
122933
124638
  var require_tls$1 = require_tls_u8QCJCFE();
122934
- var require_manifest_python_deps = require_manifest_python_deps_CktMcXzS();
124639
+ var require_manifest_python_deps = require_manifest_python_deps_BV_Cy99l();
122935
124640
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
122936
124641
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
122937
124642
  var zod = require_zod();
@@ -203626,7 +205331,7 @@ var require_enums = __commonJS({
203626
205331
  "../types/dist/enums.js"(exports) {
203627
205332
  "use strict";
203628
205333
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
203629
- var require_event_category = require_event_category_EY0GNjV9();
205334
+ var require_event_category = require_event_category_BaEgqJNv();
203630
205335
  var EventSourceType = /* @__PURE__ */ (function(EventSourceType2) {
203631
205336
  EventSourceType2["Addon"] = "addon";
203632
205337
  EventSourceType2["Core"] = "core";
@@ -203646,8 +205351,8 @@ var require_dist4 = __commonJS({
203646
205351
  "../types/dist/index.js"(exports) {
203647
205352
  "use strict";
203648
205353
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
203649
- var require_event_category = require_event_category_EY0GNjV9();
203650
- var require_sleep = require_sleep_CSodb2vQ();
205354
+ var require_event_category = require_event_category_BaEgqJNv();
205355
+ var require_sleep = require_sleep_9d8tJRbO();
203651
205356
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
203652
205357
  var require_enums2 = require_enums();
203653
205358
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -205413,6 +207118,116 @@ var require_dist4 = __commonJS({
205413
207118
  }
205414
207119
  return bestAbove?.entry ?? bestBelow?.entry;
205415
207120
  }
207121
+ var UNATTRIBUTED_BUCKET_KEY = "__unattributed__";
207122
+ var ROOT_BUCKET_KEY = "__root__";
207123
+ function bucketFor(row) {
207124
+ if (row.addonId !== null) return {
207125
+ key: row.addonId,
207126
+ kind: "addon"
207127
+ };
207128
+ if (row.classification === "root") return {
207129
+ key: ROOT_BUCKET_KEY,
207130
+ kind: "root"
207131
+ };
207132
+ return {
207133
+ key: UNATTRIBUTED_BUCKET_KEY,
207134
+ kind: "unattributed"
207135
+ };
207136
+ }
207137
+ function deci(value) {
207138
+ return Math.round(value * 10) / 10;
207139
+ }
207140
+ function foldSnapshotByFunction(rows, atMs) {
207141
+ const acc = /* @__PURE__ */ new Map();
207142
+ for (const row of rows) {
207143
+ const { key, kind } = bucketFor(row);
207144
+ const cur = acc.get(key) ?? {
207145
+ kind,
207146
+ main: 0,
207147
+ gc: 0,
207148
+ lifetime: 0,
207149
+ memory: 0,
207150
+ count: 0,
207151
+ splitKnown: true
207152
+ };
207153
+ const known = row.cpuMainPercent !== null && row.cpuGcPercent !== null;
207154
+ acc.set(key, {
207155
+ kind: cur.kind,
207156
+ main: cur.main + (row.cpuMainPercent ?? 0),
207157
+ gc: cur.gc + (row.cpuGcPercent ?? 0),
207158
+ lifetime: cur.lifetime + row.cpuPercent,
207159
+ memory: cur.memory + row.memoryRssBytes,
207160
+ count: cur.count + 1,
207161
+ splitKnown: cur.splitKnown && known
207162
+ });
207163
+ }
207164
+ return [...acc.entries()].map(([key, a]) => {
207165
+ const main = a.splitKnown ? deci(a.main) : null;
207166
+ const gc = a.splitKnown ? deci(a.gc) : null;
207167
+ const lifetime = deci(a.lifetime);
207168
+ return {
207169
+ key,
207170
+ kind: a.kind,
207171
+ point: {
207172
+ atMs,
207173
+ samples: 1,
207174
+ cpuMainPercent: main,
207175
+ cpuMainPercentMin: main,
207176
+ cpuGcPercent: gc,
207177
+ cpuGcPercentMin: gc,
207178
+ cpuLifetimePercent: lifetime,
207179
+ cpuLifetimePercentMin: lifetime,
207180
+ memoryRssBytes: a.memory,
207181
+ memoryRssBytesMin: a.memory,
207182
+ processCount: a.count,
207183
+ processCountMin: a.count
207184
+ }
207185
+ };
207186
+ });
207187
+ }
207188
+ function minNullable(a, b) {
207189
+ if (a === null || b === null) return null;
207190
+ return a < b ? a : b;
207191
+ }
207192
+ function maxNullable(a, b) {
207193
+ if (a === null || b === null) return null;
207194
+ return a > b ? a : b;
207195
+ }
207196
+ function mergePoints(held, next, atMs) {
207197
+ return {
207198
+ atMs,
207199
+ samples: held.samples + next.samples,
207200
+ cpuMainPercent: maxNullable(held.cpuMainPercent, next.cpuMainPercent),
207201
+ cpuMainPercentMin: minNullable(held.cpuMainPercentMin, next.cpuMainPercentMin),
207202
+ cpuGcPercent: maxNullable(held.cpuGcPercent, next.cpuGcPercent),
207203
+ cpuGcPercentMin: minNullable(held.cpuGcPercentMin, next.cpuGcPercentMin),
207204
+ cpuLifetimePercent: Math.max(held.cpuLifetimePercent, next.cpuLifetimePercent),
207205
+ cpuLifetimePercentMin: Math.min(held.cpuLifetimePercentMin, next.cpuLifetimePercentMin),
207206
+ memoryRssBytes: Math.max(held.memoryRssBytes, next.memoryRssBytes),
207207
+ memoryRssBytesMin: Math.min(held.memoryRssBytesMin, next.memoryRssBytesMin),
207208
+ processCount: Math.max(held.processCount, next.processCount),
207209
+ processCountMin: Math.min(held.processCountMin, next.processCountMin)
207210
+ };
207211
+ }
207212
+ function resolveBucketMs(spanMs, cadenceMs, maxPoints) {
207213
+ if (maxPoints <= 0 || cadenceMs <= 0 || spanMs <= 0) return Math.max(cadenceMs, 1);
207214
+ const wanted = spanMs / maxPoints;
207215
+ if (wanted <= cadenceMs) return cadenceMs;
207216
+ return Math.ceil(wanted / cadenceMs) * cadenceMs;
207217
+ }
207218
+ function reducePoints(points, bucketMs, origin) {
207219
+ if (bucketMs <= 0 || points.length === 0) return points;
207220
+ const buckets = /* @__PURE__ */ new Map();
207221
+ for (const point of points) {
207222
+ const start = origin + Math.floor((point.atMs - origin) / bucketMs) * bucketMs;
207223
+ const held = buckets.get(start);
207224
+ buckets.set(start, held === void 0 ? {
207225
+ ...point,
207226
+ atMs: start
207227
+ } : mergePoints(held, point, start));
207228
+ }
207229
+ return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
207230
+ }
205416
207231
  var FORMAT_KEYS = [
205417
207232
  "onnx",
205418
207233
  "coreml",
@@ -209336,6 +211151,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209336
211151
  id: zod.z.string(),
209337
211152
  data: zod.z.record(zod.z.string(), zod.z.unknown())
209338
211153
  });
211154
+ var BulkRecordSchema = zod.z.object({
211155
+ id: zod.z.string().optional(),
211156
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
211157
+ });
209339
211158
  var CollectionColumnSchema = zod.z.object({
209340
211159
  name: zod.z.string(),
209341
211160
  type: zod.z.enum([
@@ -209410,6 +211229,34 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209410
211229
  collection: zod.z.string(),
209411
211230
  record: SettingsRecordSchema
209412
211231
  }), zod.z.void(), { kind: "mutation" }),
211232
+ /**
211233
+ * Insert MANY records in ONE transaction, returning how many landed.
211234
+ *
211235
+ * The write-side twin of {@link deleteWhere}, and it exists for the same
211236
+ * reason: without it, appending a batch is N round trips and N COMMITs on
211237
+ * the single shared connection that also serves every cluster-wide
211238
+ * configuration read. The durable load series writes one process row per
211239
+ * process per sample — 76 rows every 10 s on the live fleet — and the
211240
+ * operator's rule for it is *one transaction per sample, never one per
211241
+ * row*. `insert` cannot express that; nothing else could.
211242
+ *
211243
+ * **All or nothing.** A batch that fails on its fifth row leaves none of
211244
+ * the five behind. A half-written sample is worse than a missing one: the
211245
+ * missing one reads as "nobody reported", which is true, while the half
211246
+ * one reads as "these were the only processes running", which is not.
211247
+ *
211248
+ * `id` is OPTIONAL per record, and that is the difference from
211249
+ * {@link insert}. A collection whose primary key is an `INTEGER` rowid
211250
+ * alias has no id to supply — SQLite assigns it, for free, and inventing a
211251
+ * `randomUUID()` for such a column would write a 36-character string into
211252
+ * an integer key. Omitted on a TEXT key, a uuid is generated exactly as
211253
+ * `insert` does.
211254
+ */
211255
+ insertMany: require_sleep.method(zod.z.object({
211256
+ namespace: zod.z.string().optional(),
211257
+ collection: zod.z.string(),
211258
+ records: zod.z.array(BulkRecordSchema).readonly()
211259
+ }), zod.z.object({ inserted: zod.z.number().int() }), { kind: "mutation" }),
209413
211260
  /** Update an existing record by ID. */
209414
211261
  update: require_sleep.method(zod.z.object({
209415
211262
  namespace: zod.z.string().optional(),
@@ -209599,6 +211446,15 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209599
211446
  kind: "mutation",
209600
211447
  auth: "admin"
209601
211448
  }),
211449
+ /** Insert many records in ONE transaction. All or nothing. */
211450
+ insertMany: require_sleep.method(zod.z.object({
211451
+ namespace: zod.z.string().optional(),
211452
+ collection: zod.z.string(),
211453
+ records: zod.z.array(BulkRecordSchema).readonly()
211454
+ }), zod.z.object({ inserted: zod.z.number().int() }), {
211455
+ kind: "mutation",
211456
+ auth: "admin"
211457
+ }),
209602
211458
  /** Update an existing record by ID. */
209603
211459
  update: require_sleep.method(zod.z.object({
209604
211460
  namespace: zod.z.string().optional(),
@@ -211997,6 +213853,85 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
211997
213853
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
211998
213854
  mount: { kind: "skip" }
211999
213855
  };
213856
+ var LOAD_CONTRIBUTION_ROLES = [
213857
+ "decode",
213858
+ "transcode",
213859
+ "recording",
213860
+ "streaming",
213861
+ "detection"
213862
+ ];
213863
+ var LOAD_CONTRIBUTION_ATTRIBUTIONS = [
213864
+ "measured",
213865
+ "accounted",
213866
+ "unattributable"
213867
+ ];
213868
+ var LoadContributionSchema = zod.z.object({
213869
+ role: zod.z.enum(LOAD_CONTRIBUTION_ROLES),
213870
+ /**
213871
+ * The NUMERIC device id — the same value every log line carries as
213872
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
213873
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
213874
+ * contributor that cannot name its camera must not emit the entry at all,
213875
+ * because an unnamed per-camera entry is indistinguishable from a shared one
213876
+ * and would quietly turn one camera's cost into everybody's.
213877
+ */
213878
+ deviceId: zod.z.number().int().positive().nullable(),
213879
+ attribution: zod.z.enum(LOAD_CONTRIBUTION_ATTRIBUTIONS),
213880
+ /**
213881
+ * What ONE entry is, in the contributor's own words — `615/high`,
213882
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
213883
+ * family and inventing a common one would lose the only information that
213884
+ * makes two entries for the same camera distinguishable.
213885
+ */
213886
+ unit: zod.z.string(),
213887
+ /**
213888
+ * The OS process this cost lives in, when there is one. Present so a
213889
+ * consumer can (a) tell two generations of the same unit apart across a
213890
+ * restart, and (b) subtract claimed processes from the node's process
213891
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
213892
+ * process of its own.
213893
+ */
213894
+ pid: zod.z.number().int().positive().optional(),
213895
+ /**
213896
+ * When this generation started. The pid's incarnation marker: a consumer
213897
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
213898
+ * window when this changes, because the counter restarted from zero in a new
213899
+ * process.
213900
+ */
213901
+ startedAtMs: zod.z.number().optional(),
213902
+ /**
213903
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
213904
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
213905
+ * contribution is asked for.
213906
+ *
213907
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
213908
+ * needs a sampler, and a new per-node sampler is the defect half of
213909
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
213910
+ * by whoever already keeps a history; a rate cannot be un-averaged.
213911
+ *
213912
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
213913
+ * an entry with no process.
213914
+ */
213915
+ cpuSeconds: zod.z.number().optional(),
213916
+ /** Resident bytes of this unit's process, same source and same rules. */
213917
+ rssBytes: zod.z.number().optional()
213918
+ });
213919
+ var loadContributionCapability = {
213920
+ name: "load-contribution",
213921
+ scope: "system",
213922
+ mode: "collection",
213923
+ internal: true,
213924
+ methods: {
213925
+ /**
213926
+ * This addon's own cost entries, computed live from state it already
213927
+ * holds. Inert: no persistence, no sampling, no timer. It is answered on
213928
+ * whatever beat the caller already has.
213929
+ */
213930
+ list: require_sleep.method(zod.z.void(), zod.z.array(LoadContributionSchema).readonly())
213931
+ },
213932
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
213933
+ mount: { kind: "skip" }
213934
+ };
212000
213935
  var LoginStageEnum = zod.z.enum(["primary", "second-factor"]);
212001
213936
  var RedirectLoginMethodSchema = zod.z.object({
212002
213937
  kind: zod.z.literal("redirect"),
@@ -212168,8 +214103,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212168
214103
  classification: zod.z.enum([
212169
214104
  "root",
212170
214105
  "managed",
212171
- "system",
212172
- "ghost"
214106
+ "system"
212173
214107
  ]),
212174
214108
  /** `$process` addon binding when `managed`, else null. */
212175
214109
  addonId: zod.z.string().nullable(),
@@ -212177,22 +214111,39 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212177
214111
  nodeId: zod.z.string().nullable(),
212178
214112
  /** Truncated command line. */
212179
214113
  command: zod.z.string(),
214114
+ /**
214115
+ * `ps pcpu` — CPU averaged over the process's WHOLE LIFETIME, not a rate.
214116
+ * On a runner up for days it barely moves. Fine as a column, useless as a
214117
+ * series: use `cpuMainPercent + cpuGcPercent` for anything time-varying.
214118
+ */
212180
214119
  cpuPercent: zod.z.number(),
212181
214120
  memoryRssBytes: zod.z.number(),
214121
+ /**
214122
+ * Instantaneous CPU% of the process's own threads over the last
214123
+ * process-snapshot window, from a `/proc/<pid>/task/*` tick delta.
214124
+ *
214125
+ * `null` = UNKNOWN, never zero: no previous sample yet (first tick after
214126
+ * boot), the pid was recycled, or this node is not Linux.
214127
+ */
214128
+ cpuMainPercent: zod.z.number().nullable(),
214129
+ /**
214130
+ * Instantaneous CPU% of V8's `V8Worker` platform pool over the same window.
214131
+ *
214132
+ * This is the number that rewrote the 2026-08-27 diagnosis — hub-main 73%,
214133
+ * `stream-broker` 61% (`docs/architecture/load-ledger.md`). A CPU chart that
214134
+ * does not separate it from `cpuMainPercent` shows "busy" where the truth is
214135
+ * "allocating too much".
214136
+ *
214137
+ * Concurrent GC is the dominant tenant of that pool but not the only one
214138
+ * (background compilation runs there too), so it is reported as
214139
+ * "GC / V8 helpers" rather than as pure collection time. `null` has the same
214140
+ * meaning as on `cpuMainPercent`.
214141
+ */
214142
+ cpuGcPercent: zod.z.number().nullable(),
214143
+ /** Threads seen in the tick scan. `null` under the same conditions. */
214144
+ threadCount: zod.z.number().nullable(),
212182
214145
  /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
212183
- uptimeSec: zod.z.number(),
212184
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
212185
- orphaned: zod.z.boolean()
212186
- });
212187
- var KillProcessInputSchema = zod.z.object({
212188
- pid: zod.z.number(),
212189
- /** Force = SIGKILL. Default is SIGTERM. */
212190
- force: zod.z.boolean().optional()
212191
- });
212192
- var KillProcessResultSchema = zod.z.object({
212193
- success: zod.z.boolean(),
212194
- reason: zod.z.string().optional(),
212195
- signal: zod.z.enum(["SIGTERM", "SIGKILL"]).optional()
214146
+ uptimeSec: zod.z.number()
212196
214147
  });
212197
214148
  var DumpHeapSnapshotInputSchema = zod.z.object({
212198
214149
  /** The addon whose runner should dump a heap snapshot. */
@@ -212206,6 +214157,89 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212206
214157
  pid: zod.z.number().optional(),
212207
214158
  reason: zod.z.string().optional()
212208
214159
  });
214160
+ var LoadPointSchema = zod.z.object({
214161
+ /** Bucket START, or the snapshot's own timestamp when unreduced. */
214162
+ atMs: zod.z.number(),
214163
+ /** Raw snapshots in this bucket. Never 0 — AN EMPTY BUCKET IS ABSENT. */
214164
+ samples: zod.z.number().int(),
214165
+ /**
214166
+ * `null` = UNKNOWN and it PROPAGATES: a bucket is null unless every process
214167
+ * of every snapshot in it reported a thread split. A partial sum is a
214168
+ * smaller number that looks exactly as real as a complete one.
214169
+ */
214170
+ cpuMainPercent: zod.z.number().nullable(),
214171
+ cpuMainPercentMin: zod.z.number().nullable(),
214172
+ cpuGcPercent: zod.z.number().nullable(),
214173
+ cpuGcPercentMin: zod.z.number().nullable(),
214174
+ /** Lifetime-average CPU%, summed. Always known — and never a rate. */
214175
+ cpuLifetimePercent: zod.z.number(),
214176
+ cpuLifetimePercentMin: zod.z.number(),
214177
+ memoryRssBytes: zod.z.number(),
214178
+ memoryRssBytesMin: zod.z.number(),
214179
+ processCount: zod.z.number().int(),
214180
+ processCountMin: zod.z.number().int()
214181
+ });
214182
+ var LoadFunctionSeriesSchema = zod.z.object({
214183
+ key: zod.z.string(),
214184
+ kind: zod.z.enum([
214185
+ "addon",
214186
+ "root",
214187
+ "unattributed"
214188
+ ]),
214189
+ /** Oldest-first. A missing interval is MISSING — never zero-filled. */
214190
+ points: zod.z.array(LoadPointSchema).readonly()
214191
+ });
214192
+ var NodeLoadSeriesSchema = zod.z.object({
214193
+ nodeId: zod.z.string(),
214194
+ /** One entry per function seen in the window, heaviest-first. */
214195
+ series: zod.z.array(LoadFunctionSeriesSchema).readonly(),
214196
+ /**
214197
+ * Width of one returned bucket, in ms. Equals the sampling cadence when no
214198
+ * reduction was needed — so a caller can always say what one point covers
214199
+ * without having to know whether it was reduced.
214200
+ */
214201
+ bucketMs: zod.z.number(),
214202
+ /** Raw snapshots that went into this answer, across both tiers. */
214203
+ retainedSamples: zod.z.number(),
214204
+ /** Oldest snapshot represented, or `null` when nothing is retained. */
214205
+ oldestAtMs: zod.z.number().nullable(),
214206
+ /** The fixed sampling cadence in force on the cluster, in ms. */
214207
+ cadenceMs: zod.z.number(),
214208
+ /**
214209
+ * Did the DURABLE tier contribute? `false` means the answer is the hot ring
214210
+ * alone — an agent (which holds no table), or a store that refused.
214211
+ * Reported because "the last hour" and "the last six hours" are different
214212
+ * questions and an operator must not have to guess which was answered.
214213
+ */
214214
+ durable: zod.z.boolean()
214215
+ });
214216
+ var GetLoadSeriesInputSchema = zod.z.object({
214217
+ /**
214218
+ * The node whose series is wanted.
214219
+ *
214220
+ * NOT named `nodeId`: the generated cap router strips a top-level
214221
+ * `nodeId` from every method input and uses it to ROUTE the call to
214222
+ * that node's provider (`generated-cap-routers.ts`). A series target
214223
+ * called `nodeId` would silently become a routing pin and never reach
214224
+ * the provider. The hub holds every node it hears from, so the
214225
+ * ordinary call is unpinned — answered by the hub, for any node.
214226
+ */
214227
+ forNodeId: zod.z.string(),
214228
+ /**
214229
+ * EXCLUSIVE lower bound. A caller passes the newest `atMs` it already
214230
+ * holds and receives only what it is missing, so seeding a live chart
214231
+ * from this method cannot double a point already drawn.
214232
+ */
214233
+ sinceMs: zod.z.number().optional(),
214234
+ /**
214235
+ * Most points the caller wants PER FUNCTION. The window is reduced to fit,
214236
+ * preserving min and max per bucket.
214237
+ *
214238
+ * Absent means NO reduction — legitimate for a short window and a trap for a
214239
+ * long one, which is why a chart passes its own pixel width.
214240
+ */
214241
+ maxPoints: zod.z.number().int().positive().optional()
214242
+ });
212209
214243
  var SystemMetricsSchema = zod.z.object({
212210
214244
  cpuPercent: zod.z.number(),
212211
214245
  memoryPercent: zod.z.number(),
@@ -212251,28 +214285,44 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
212251
214285
  getAddonStats: require_sleep.method(zod.z.object({ addonId: zod.z.string() }), PidResourceStatsSchema.nullable()),
212252
214286
  /**
212253
214287
  * Snapshot of every camstack-related process on this node with a
212254
- * ghost/managed/root classification. Powers the Cluster → Agent →
212255
- * Processes tab: cross-references `$process.list` against a `ps` scan
212256
- * so orphaned trees (PPID=1) or unknown children show up as `ghost`
212257
- * and can be killed from the UI.
214288
+ * root/managed/system classification. Powers the Cluster → Agent →
214289
+ * Processes tab: cross-references `$process.list` against a `ps` scan so
214290
+ * per-addon CPU and RSS can be attributed, and so a process the cluster
214291
+ * does not manage is still visible.
214292
+ *
214293
+ * **Read-only, by design.** This cap once carried a `killProcess`
214294
+ * mutation; it was deleted on 2026-08-27. A runner's lifecycle belongs to
214295
+ * `CrashSupervisor` and is driven through `addons.restartAddon` /
214296
+ * `$process.restart` — signalling a raw pid went around the supervisor
214297
+ * (D6), and the one class it was willing to signal turned out to be the
214298
+ * container's own init and the operator's desktop app.
212258
214299
  */
212259
214300
  listNodeProcesses: require_sleep.method(zod.z.void(), zod.z.array(NodeProcessSchema).readonly()),
212260
214301
  /**
212261
- * Send SIGTERM (or SIGKILL when `force`) to a pid inside this node's
212262
- * process tree. The provider refuses pids that aren't in the live
212263
- * `listNodeProcesses()` snapshot callers can't use this endpoint
212264
- * to kill arbitrary system processes.
214302
+ * The retained per-node load series the ONE reader over BOTH tiers.
214303
+ *
214304
+ * The in-memory ring is the HOT window (the last 180 snapshots, held by
214305
+ * every node's `native-metrics`); the hub's `metrics:node-load-samples`
214306
+ * table is the COLD one (the operator's retention, six hours by default).
214307
+ * This method merges them and DEDUPES on `atMs`, so a snapshot present in
214308
+ * both contributes once and the caller never learns which tier a point
214309
+ * came from. There is deliberately no second read surface: two readers is
214310
+ * how two charts start disagreeing about the same node.
214311
+ *
214312
+ * Reads only; nothing is sampled to answer it. Normally called UNPINNED —
214313
+ * the hub hears every node's snapshot and holds every node's rows — and
214314
+ * answers for any `forNodeId`. Pinned to an agent it answers from that
214315
+ * agent's ring alone (`durable: false`). Empty is a legitimate answer: a
214316
+ * node nobody has heard from has no series, and saying so is the truth.
212265
214317
  */
212266
- killProcess: require_sleep.method(KillProcessInputSchema, KillProcessResultSchema, {
212267
- kind: "mutation",
212268
- auth: "admin"
212269
- }),
214318
+ getLoadSeries: require_sleep.method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema),
212270
214319
  /**
212271
214320
  * Tell the addon's forked runner to write a V8 heap snapshot to disk (via
212272
214321
  * SIGUSR2 — the runner's diagnostic handler). Also logs its
212273
- * `process.memoryUsage()` + heap-space breakdown. Refuses pids not in the
212274
- * live `listNodeProcesses()` snapshot. Use for deep per-addon memory
212275
- * attribution; copy the returned path off the node to analyze.
214322
+ * `process.memoryUsage()` + heap-space breakdown. Resolves the pid from
214323
+ * `$process.list`, so it can only reach a runner this node spawned. Use
214324
+ * for deep per-addon memory attribution; copy the returned path off the
214325
+ * node to analyze.
212276
214326
  */
212277
214327
  dumpHeapSnapshot: require_sleep.method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
212278
214328
  kind: "mutation",
@@ -228367,6 +230417,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
228367
230417
  */
228368
230418
  channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
228369
230419
  });
230420
+ var ReportedLoadContributionSchema = LoadContributionSchema.extend({ addonId: zod.z.string() });
228370
230421
  var GetLoggingSettingsInputSchema = zod.z.object({
228371
230422
  scopeNodeId: zod.z.string().optional(),
228372
230423
  /**
@@ -228460,6 +230511,22 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
228460
230511
  */
228461
230512
  getRequestCensus: require_sleep.method(zod.z.void(), RequestCensusStatusSchema, { auth: "admin" }),
228462
230513
  /**
230514
+ * Every `load-contribution` an addon on this cluster reports — each
230515
+ * addon's OWN cost, already attributed by the addon that owns it.
230516
+ *
230517
+ * There is no central list of what costs what: an addon that spawns a
230518
+ * per-camera child declares it, and one that cannot attribute its cost
230519
+ * (the shared inference pool) declares THAT. So a new cost family appears
230520
+ * here the moment its addon is redeployed, with nobody editing anything.
230521
+ *
230522
+ * What this does NOT do is measure the node. `metrics.node-processes-
230523
+ * snapshot` still does that, and the difference between the two is the
230524
+ * finding: a process no contribution claims is either a leak or a family
230525
+ * nobody has taught to report. Both belong in the unattributed bucket, and
230526
+ * neither may be folded into a camera.
230527
+ */
230528
+ getLoadContributions: require_sleep.method(zod.z.void(), zod.z.array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }),
230529
+ /**
228463
230530
  * The logging settings document — levels and armed diagnostics — resolved
228464
230531
  * for `nodeId`, or for the cluster when `nodeId` is absent.
228465
230532
  *
@@ -229485,6 +231552,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
229485
231552
  }
229486
231553
  var HF_REPO = "camstack/camstack-models";
229487
231554
  var HF_BASE_URL = `https://huggingface.co/${HF_REPO}/resolve/main`;
231555
+ var DEFAULT_TOKEN_EXPIRY = "30d";
229488
231556
  var RUNTIME_DEFAULTS = {
229489
231557
  "features.streaming": true,
229490
231558
  "features.notifications": true,
@@ -229513,7 +231581,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
229513
231581
  "ffmpeg.binaryPath": "ffmpeg",
229514
231582
  "ffmpeg.hwAccel": "auto",
229515
231583
  "ffmpeg.threadCount": 0,
229516
- "auth.tokenExpiry": "7d"
231584
+ "auth.tokenExpiry": "30d"
229517
231585
  };
229518
231586
  var AccessoryKind = {
229519
231587
  Siren: require_sleep.DeviceRole.Siren,
@@ -232031,6 +234099,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232031
234099
  lawnMowerControl: "lawn-mower-control",
232032
234100
  llm: "llm",
232033
234101
  llmRuntime: "llm-runtime",
234102
+ loadContribution: "load-contribution",
232034
234103
  localNetwork: "local-network",
232035
234104
  lockControl: "lock-control",
232036
234105
  logChannels: "log-channels",
@@ -232396,6 +234465,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232396
234465
  key: "llmRuntime",
232397
234466
  name: "llm-runtime"
232398
234467
  },
234468
+ {
234469
+ key: "loadContribution",
234470
+ name: "load-contribution"
234471
+ },
232399
234472
  {
232400
234473
  key: "localNetwork",
232401
234474
  name: "local-network"
@@ -232785,6 +234858,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232785
234858
  lawnMowerControlCapability,
232786
234859
  llmCapability,
232787
234860
  llmRuntimeCapability,
234861
+ loadContributionCapability,
232788
234862
  localNetworkCapability,
232789
234863
  lockControlCapability,
232790
234864
  logChannelsCapability,
@@ -233808,6 +235882,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
233808
235882
  addonId: null,
233809
235883
  access: "create"
233810
235884
  },
235885
+ "dataStoreProvider.insertMany": {
235886
+ capName: "data-store-provider",
235887
+ capScope: "system",
235888
+ addonId: null,
235889
+ access: "create"
235890
+ },
233811
235891
  "dataStoreProvider.isEmpty": {
233812
235892
  capName: "data-store-provider",
233813
235893
  capScope: "system",
@@ -235122,6 +237202,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
235122
237202
  addonId: null,
235123
237203
  access: "create"
235124
237204
  },
237205
+ "loadContribution.list": {
237206
+ capName: "load-contribution",
237207
+ capScope: "system",
237208
+ addonId: null,
237209
+ access: "view"
237210
+ },
235125
237211
  "localNetwork.downloadCa": {
235126
237212
  capName: "local-network",
235127
237213
  capScope: "system",
@@ -235422,17 +237508,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
235422
237508
  addonId: null,
235423
237509
  access: "view"
235424
237510
  },
235425
- "metricsProvider.getProcessStats": {
237511
+ "metricsProvider.getLoadSeries": {
235426
237512
  capName: "metrics-provider",
235427
237513
  capScope: "system",
235428
237514
  addonId: null,
235429
237515
  access: "view"
235430
237516
  },
235431
- "metricsProvider.killProcess": {
237517
+ "metricsProvider.getProcessStats": {
235432
237518
  capName: "metrics-provider",
235433
237519
  capScope: "system",
235434
237520
  addonId: null,
235435
- access: "create"
237521
+ access: "view"
235436
237522
  },
235437
237523
  "metricsProvider.listAddonInstances": {
235438
237524
  capName: "metrics-provider",
@@ -237444,6 +239530,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237444
239530
  addonId: null,
237445
239531
  access: "create"
237446
239532
  },
239533
+ "settingsStore.insertMany": {
239534
+ capName: "settings-store",
239535
+ capScope: "system",
239536
+ addonId: null,
239537
+ access: "create"
239538
+ },
237447
239539
  "settingsStore.isEmpty": {
237448
239540
  capName: "settings-store",
237449
239541
  capScope: "system",
@@ -238056,6 +240148,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238056
240148
  addonId: null,
238057
240149
  access: "create"
238058
240150
  },
240151
+ "system.getLoadContributions": {
240152
+ capName: "system",
240153
+ capScope: "system",
240154
+ addonId: null,
240155
+ access: "view"
240156
+ },
238059
240157
  "system.getLoggingSettings": {
238060
240158
  capName: "system",
238061
240159
  capScope: "system",
@@ -238748,6 +240846,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238748
240846
  "lawn-mower-control",
238749
240847
  "llm",
238750
240848
  "llm-runtime",
240849
+ "load-contribution",
238751
240850
  "local-network",
238752
240851
  "lock-control",
238753
240852
  "log-channels",
@@ -238906,6 +241005,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238906
241005
  "integrations",
238907
241006
  "llm",
238908
241007
  "llm-runtime",
241008
+ "load-contribution",
238909
241009
  "local-network",
238910
241010
  "log-channels",
238911
241011
  "log-destination",
@@ -241538,7 +243638,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241538
243638
  listAddonInstances: (input) => dispatch("metricsProvider", "listAddonInstances", "query", input),
241539
243639
  getAddonStats: (input) => dispatch("metricsProvider", "getAddonStats", "query", input),
241540
243640
  listNodeProcesses: (input) => dispatch("metricsProvider", "listNodeProcesses", "query", input),
241541
- killProcess: (input) => dispatch("metricsProvider", "killProcess", "mutation", input),
243641
+ getLoadSeries: (input) => dispatch("metricsProvider", "getLoadSeries", "query", input),
241542
243642
  dumpHeapSnapshot: (input) => dispatch("metricsProvider", "dumpHeapSnapshot", "mutation", input)
241543
243643
  },
241544
243644
  mqttBroker: {
@@ -241717,6 +243817,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241717
243817
  set: (input) => dispatch("settingsStore", "set", "mutation", input),
241718
243818
  query: (input) => dispatch("settingsStore", "query", "query", input),
241719
243819
  insert: (input) => dispatch("settingsStore", "insert", "mutation", input),
243820
+ insertMany: (input) => dispatch("settingsStore", "insertMany", "mutation", input),
241720
243821
  update: (input) => dispatch("settingsStore", "update", "mutation", input),
241721
243822
  delete: (input) => dispatch("settingsStore", "delete", "mutation", input),
241722
243823
  deleteWhere: (input) => dispatch("settingsStore", "deleteWhere", "mutation", input),
@@ -241797,6 +243898,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241797
243898
  setSiteLocation: (input) => dispatch("system", "setSiteLocation", "mutation", input),
241798
243899
  detectSiteLocation: (input) => dispatch("system", "detectSiteLocation", "mutation", input),
241799
243900
  getRequestCensus: (input) => dispatch("system", "getRequestCensus", "query", input),
243901
+ getLoadContributions: (input) => dispatch("system", "getLoadContributions", "query", input),
241800
243902
  getLoggingSettings: (input) => dispatch("system", "getLoggingSettings", "query", input),
241801
243903
  setLoggingSettings: (input) => dispatch("system", "setLoggingSettings", "mutation", input)
241802
243904
  },
@@ -244610,6 +246712,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244610
246712
  exports.BrokerSubscribeResultSchema = SubscribeResultSchema;
244611
246713
  exports.BrokerTestConnectionResultSchema = TestConnectionResultSchema;
244612
246714
  exports.BrokerUnsubscribeInputSchema = UnsubscribeInputSchema;
246715
+ exports.BulkRecordSchema = BulkRecordSchema;
244613
246716
  exports.CAMERA_SWITCH_CATALOG = CAMERA_SWITCH_CATALOG;
244614
246717
  exports.CAMERA_SWITCH_ORDER = CAMERA_SWITCH_ORDER;
244615
246718
  exports.CAM_PROFILE_ORDER = require_sleep.CAM_PROFILE_ORDER;
@@ -244724,6 +246827,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244724
246827
  exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
244725
246828
  exports.DEFAULT_RUNTIME_STATE_DURABILITY = require_sleep.DEFAULT_RUNTIME_STATE_DURABILITY;
244726
246829
  exports.DEFAULT_TIMELAPSE_PREVIEW_TEXT = DEFAULT_TIMELAPSE_PREVIEW_TEXT;
246830
+ exports.DEFAULT_TOKEN_EXPIRY = DEFAULT_TOKEN_EXPIRY;
244727
246831
  exports.DETAIL_CROP_PADDING_FIELD = DETAIL_CROP_PADDING_FIELD;
244728
246832
  exports.DETAIL_CROP_PADDING_KEY = DETAIL_CROP_PADDING_KEY;
244729
246833
  exports.DETAIL_CROP_SECTION_ID = DETAIL_CROP_SECTION_ID;
@@ -244883,6 +246987,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244883
246987
  exports.IntercomStatusSchema = IntercomStatusSchema;
244884
246988
  exports.KNOWN_CAP_NAMES = KNOWN_CAP_NAMES;
244885
246989
  exports.KeyEventSchema = KeyEventSchema;
246990
+ exports.LOAD_CONTRIBUTION_ATTRIBUTIONS = LOAD_CONTRIBUTION_ATTRIBUTIONS;
246991
+ exports.LOAD_CONTRIBUTION_ROLES = LOAD_CONTRIBUTION_ROLES;
244886
246992
  exports.LOG_CHANNEL_TICK_MS = LOG_CHANNEL_TICK_MS;
244887
246993
  exports.LOG_LEVEL_RANK = LOG_LEVEL_RANK;
244888
246994
  exports.LabelAttributionSchema = LabelAttributionSchema;
@@ -244915,6 +247021,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244915
247021
  exports.LlmTimeoutDefaults = LlmTimeoutDefaults;
244916
247022
  exports.LlmUsageRollupSchema = LlmUsageRollupSchema;
244917
247023
  exports.LlmUsageSchema = LlmUsageSchema;
247024
+ exports.LoadContributionSchema = LoadContributionSchema;
244918
247025
  exports.LocateSegmentResultSchema = LocateSegmentResultSchema;
244919
247026
  exports.LocationStatSchema = LocationStatSchema;
244920
247027
  exports.LockControlStatusSchema = LockControlStatusSchema;
@@ -245197,6 +247304,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245197
247304
  exports.RECORDING_EXPORT_MAX_READ_BYTES = RECORDING_EXPORT_MAX_READ_BYTES;
245198
247305
  exports.RESERVED_BINDING_NAMES = RESERVED_BINDING_NAMES;
245199
247306
  exports.RESTORED_CAP_NAMES = RESTORED_CAP_NAMES;
247307
+ exports.ROOT_BUCKET_KEY = ROOT_BUCKET_KEY;
245200
247308
  exports.RUNTIME_DEFAULTS = RUNTIME_DEFAULTS;
245201
247309
  exports.RUNTIME_STATE_POLICY = RUNTIME_STATE_POLICY;
245202
247310
  exports.RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS;
@@ -245239,6 +247347,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245239
247347
  exports.RelocateMediaInputSchema = RelocateMediaInputSchema;
245240
247348
  exports.RenderedAsSchema = RenderedAsSchema;
245241
247349
  exports.ReportMotionInputSchema = ReportMotionInputSchema;
247350
+ exports.ReportedLoadContributionSchema = ReportedLoadContributionSchema;
245242
247351
  exports.RequestCensusGroupSchema = RequestCensusGroupSchema;
245243
247352
  exports.RequestCensusProcedureSchema = RequestCensusProcedureSchema;
245244
247353
  exports.RequestCensusSnapshotSchema = RequestCensusSnapshotSchema;
@@ -245422,6 +247531,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245422
247531
  exports.TransportPlaneCountsSchema = TransportPlaneCountsSchema;
245423
247532
  exports.TransportPlaneSchema = TransportPlaneSchema;
245424
247533
  exports.TurnServerSchema = TurnServerSchema;
247534
+ exports.UNATTRIBUTED_BUCKET_KEY = UNATTRIBUTED_BUCKET_KEY;
245425
247535
  exports.UNIT_TABLE = UNIT_TABLE;
245426
247536
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema$1;
245427
247537
  exports.UnitConversionError = UnitConversionError;
@@ -245640,6 +247750,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245640
247750
  exports.filesystemBrowseCapability = filesystemBrowseCapability;
245641
247751
  exports.findTimezone = findTimezone;
245642
247752
  exports.floodCapability = floodCapability;
247753
+ exports.foldSnapshotByFunction = foldSnapshotByFunction;
245643
247754
  exports.formatForBackend = formatForBackend;
245644
247755
  exports.formatForRuntime = formatForRuntime;
245645
247756
  exports.gasCapability = gasCapability;
@@ -245697,6 +247808,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245697
247808
  exports.lifecycleTaskSchema = lifecycleTaskSchema;
245698
247809
  exports.llmCapability = llmCapability;
245699
247810
  exports.llmRuntimeCapability = llmRuntimeCapability;
247811
+ exports.loadContributionCapability = loadContributionCapability;
245700
247812
  exports.localNetworkCapability = localNetworkCapability;
245701
247813
  exports.locationSimilarity = locationSimilarity;
245702
247814
  exports.lockControlCapability = lockControlCapability;
@@ -245793,12 +247905,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245793
247905
  exports.recordingCapability = recordingCapability;
245794
247906
  exports.recordingExportCapability = recordingExportCapability;
245795
247907
  exports.rectsToCells = rectsToCells;
247908
+ exports.reducePoints = reducePoints;
245796
247909
  exports.requiresPython = requiresPython;
245797
247910
  exports.resetPoolBaseline = resetPoolBaseline;
245798
247911
  exports.resolveAddonExecution = resolveAddonExecution;
245799
247912
  exports.resolveAddonGroup = resolveAddonGroup;
245800
247913
  exports.resolveAddonPlacement = resolveAddonPlacement;
245801
247914
  exports.resolveAddonRuntime = resolveAddonRuntime;
247915
+ exports.resolveBucketMs = resolveBucketMs;
245802
247916
  exports.resolveCapMount = require_sleep.resolveCapMount;
245803
247917
  exports.resolveClusterStepModelId = resolveClusterStepModelId;
245804
247918
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
@@ -402988,6 +405102,37 @@ var require_collection_preference = __commonJS({
402988
405102
  }
402989
405103
  });
402990
405104
 
405105
+ // ../../server/backend/dist/api/core/load-contributions.js
405106
+ var require_load_contributions = __commonJS({
405107
+ "../../server/backend/dist/api/core/load-contributions.js"(exports) {
405108
+ "use strict";
405109
+ Object.defineProperty(exports, "__esModule", { value: true });
405110
+ exports.EMPTY_LOAD_CONTRIBUTION_PLANE = void 0;
405111
+ exports.buildLoadContributionPlane = buildLoadContributionPlane;
405112
+ var types_1 = require_dist4();
405113
+ exports.EMPTY_LOAD_CONTRIBUTION_PLANE = {
405114
+ contributions: async () => []
405115
+ };
405116
+ function buildLoadContributionPlane(source, onProviderError) {
405117
+ return {
405118
+ contributions: async () => {
405119
+ const out = [];
405120
+ for (const [addonId, provider] of source.entries()) {
405121
+ try {
405122
+ for (const entry of await provider.list()) {
405123
+ out.push({ ...entry, addonId });
405124
+ }
405125
+ } catch (err) {
405126
+ onProviderError?.(addonId, (0, types_1.errMsg)(err));
405127
+ }
405128
+ }
405129
+ return out;
405130
+ }
405131
+ };
405132
+ }
405133
+ }
405134
+ });
405135
+
402991
405136
  // ../../server/backend/dist/api/core/logging-settings.js
402992
405137
  var require_logging_settings = __commonJS({
402993
405138
  "../../server/backend/dist/api/core/logging-settings.js"(exports) {
@@ -404029,6 +406174,7 @@ var require_cap_providers = __commonJS({
404029
406174
  var agent_installed_packages_js_1 = require_agent_installed_packages();
404030
406175
  var http_request_census_singleton_js_1 = require_http_request_census_singleton();
404031
406176
  var collection_preference_js_1 = require_collection_preference();
406177
+ var load_contributions_js_1 = require_load_contributions();
404032
406178
  var logging_settings_js_1 = require_logging_settings();
404033
406179
  var request_census_settings_js_1 = require_request_census_settings();
404034
406180
  var site_location_js_1 = require_site_location();
@@ -404046,6 +406192,9 @@ var require_cap_providers = __commonJS({
404046
406192
  const channels = (0, logging_settings_js_1.buildLogChannelPlane)({ entries: () => registry?.getCollectionEntries("log-channels") ?? [] }, (addonId, phase, error) => {
404047
406193
  logger?.warn("log-channels provider unreachable", { meta: { addonId, phase, error } });
404048
406194
  });
406195
+ const loadContributions = (0, load_contributions_js_1.buildLoadContributionPlane)({ entries: () => registry?.getCollectionEntries("load-contribution") ?? [] }, (addonId, error) => {
406196
+ logger?.warn("load-contribution provider unreachable", { meta: { addonId, error } });
406197
+ });
404049
406198
  const loggingSettings = new logging_settings_js_1.LoggingSettingsService({
404050
406199
  store,
404051
406200
  gate: (0, system_1.getLoggingGate)(),
@@ -404072,6 +406221,7 @@ var require_cap_providers = __commonJS({
404072
406221
  }
404073
406222
  return result;
404074
406223
  },
406224
+ getLoadContributions: async () => loadContributions.contributions(),
404075
406225
  getRetentionConfig: async () => getRetention(registry)?.getConfig() ?? null,
404076
406226
  setRetentionConfig: async (input) => {
404077
406227
  getRetention(registry)?.setConfig(input);