@camstack/system 1.2.271 → 1.2.273

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.
Files changed (63) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/addon.d.ts +13 -0
  12. package/dist/builtins/camera-grid/grid-canvas-seed.d.ts +62 -0
  13. package/dist/builtins/camera-grid/grid-canvas.d.ts +4 -2
  14. package/dist/builtins/camera-grid/grid-child.d.ts +10 -0
  15. package/dist/builtins/camera-grid/grid-compositor.d.ts +10 -0
  16. package/dist/builtins/camera-grid/index.js +322 -16
  17. package/dist/builtins/camera-grid/index.mjs +322 -16
  18. package/dist/builtins/console-logging/index.js +1 -1
  19. package/dist/builtins/console-logging/index.mjs +1 -1
  20. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  22. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  23. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  30. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  32. package/dist/builtins/local-network/local-network.addon.js +1 -1
  33. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  34. package/dist/builtins/loki-logging/index.js +1 -1
  35. package/dist/builtins/loki-logging/index.mjs +1 -1
  36. package/dist/builtins/native-metrics/cgroup-pids.d.ts +112 -0
  37. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +1 -0
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +209 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +209 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/system-config/system-config.addon.js +1 -1
  53. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  54. package/dist/builtins/winston-logging/index.js +1 -1
  55. package/dist/builtins/winston-logging/index.mjs +1 -1
  56. package/dist/{dist-_YWIPtUH.js → dist-D5ONNqL6.js} +187 -11
  57. package/dist/{dist-Diz59Dhi.mjs → dist-DrLhdcwI.mjs} +187 -11
  58. package/dist/index.js +12 -5
  59. package/dist/index.mjs +12 -5
  60. package/dist/kernel/moleculer/process-service.d.ts +54 -0
  61. package/dist/{retired-settings-keys-C2ocoH_l.js → retired-settings-keys-1686ou3s.js} +1 -1
  62. package/dist/{retired-settings-keys-C_Hg5d37.mjs → retired-settings-keys-Du_0qedo.mjs} +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,112 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ import { CgroupFileReader } from './cgroup-memory.js';
3
+ /** Which hierarchy answered — reported so a reading is never ambiguous. */
4
+ export type CgroupPidsSource = 'cgroup-v2' | 'cgroup-v1';
5
+ /** One reading of the ceiling and our distance from it. */
6
+ export interface ContainerPidsSnapshot {
7
+ readonly source: CgroupPidsSource;
8
+ /** Threads (not processes) charged to this cgroup right now. */
9
+ readonly current: number;
10
+ /** The ceiling, or `null` when there is none. */
11
+ readonly max: number | null;
12
+ /** `current / max`, or `null` without a ceiling. */
13
+ readonly usedFraction: number | null;
14
+ /** Lifetime count of REFUSED forks, or `null` when the kernel does not say. */
15
+ readonly refusals: number | null;
16
+ }
17
+ /** Where the files for one hierarchy live, once resolved. */
18
+ export interface CgroupPidsLayout {
19
+ readonly source: CgroupPidsSource;
20
+ readonly directory: string;
21
+ readonly currentPath: string;
22
+ readonly maxPath: string;
23
+ readonly eventsPath: string;
24
+ }
25
+ /**
26
+ * `pids.max` is either a number or the word `max`.
27
+ *
28
+ * `max` means NO ceiling, and that is `null` — not `Infinity`, not a huge
29
+ * number. A ceiling nothing can reach is not a ceiling, and reporting one
30
+ * would make every utilisation fraction on an unlimited node read as 0 %.
31
+ */
32
+ export declare function parsePidsMax(text: string): number | null;
33
+ /**
34
+ * `pids.events` is `key value` per line; we read exactly one key:
35
+ *
36
+ * ```
37
+ * max 22
38
+ * ```
39
+ *
40
+ * `max` is the number of times a fork was refused because the limit was hit.
41
+ * A kernel that publishes only `pids.events.local`, or a file we cannot make
42
+ * sense of, answers `null` — unknown, never zero, because zero here means "no
43
+ * refusals have ever happened" and that is a claim.
44
+ */
45
+ export declare function parsePidsEvents(text: string): number | null;
46
+ /** Turn one layout's files into a snapshot. Pure over the text. */
47
+ export declare function buildContainerPidsSnapshot(input: {
48
+ readonly source: CgroupPidsSource;
49
+ readonly currentText: string;
50
+ readonly maxText: string | null;
51
+ readonly eventsText: string | null;
52
+ }): ContainerPidsSnapshot | null;
53
+ /**
54
+ * Find the pids cgroup from INSIDE this process, never a hard-coded path —
55
+ * same reasoning as the memory layout: from inside the container the cgroup is
56
+ * at the mount ROOT (`0::/`), from the host it is `<mount>/docker/<id>`, and
57
+ * hard-coding either one breaks the other.
58
+ */
59
+ export declare function resolveCgroupPidsLayout(fs: CgroupFileReader): CgroupPidsLayout | null;
60
+ export interface ContainerPidsProbeDeps {
61
+ readonly logger: IScopedLogger;
62
+ readonly fs?: CgroupFileReader;
63
+ readonly now?: () => number;
64
+ /** How often the heartbeat line is written. Default 60 s. */
65
+ readonly heartbeatMs?: number;
66
+ /** Utilisation at which the heartbeat becomes a WARN. Default 0.8. */
67
+ readonly pressureFraction?: number;
68
+ }
69
+ /**
70
+ * The sampler.
71
+ *
72
+ * Its logging is deliberately different from the memory probe's, because the
73
+ * operator asked for a DAY of regular readings before deciding whether to
74
+ * raise the ceiling: there is a heartbeat, on its own cadence, whatever the
75
+ * sampling tick is. Three lines exist and no more:
76
+ *
77
+ * 1. **Resolution**, once — the hierarchy that answered, or "no cgroup here",
78
+ * which is a fact about the node and not an error.
79
+ * 2. **Heartbeat**, every `heartbeatMs` — current, ceiling, utilisation and
80
+ * the lifetime refusal count. INFO while there is room, WARN while we are
81
+ * above `pressureFraction`, and one INFO when it comes back down, so the
82
+ * log carries BOTH edges of a pressure episode rather than only its start.
83
+ * 3. **A refusal**, immediately — the `pids.events` counter rose, so the
84
+ * kernel said no that many times since the last sample and some subsystem
85
+ * silently did without a thread. This one never waits for the heartbeat.
86
+ */
87
+ export declare class ContainerPidsProbe {
88
+ private readonly logger;
89
+ private readonly fs;
90
+ private readonly now;
91
+ private readonly heartbeatMs;
92
+ private readonly pressureFraction;
93
+ private resolved;
94
+ private layout;
95
+ private lastHeartbeatAt;
96
+ private lastRefusals;
97
+ private underPressure;
98
+ constructor(deps: ContainerPidsProbeDeps);
99
+ /** Did this node resolve a pids cgroup at all? Absent is a reportable fact. */
100
+ get available(): boolean;
101
+ /** One reading, or `null` where there is no cgroup or it stopped answering. */
102
+ sample(): ContainerPidsSnapshot | null;
103
+ private resolveOnce;
104
+ private report;
105
+ /**
106
+ * The counter only ever rises, so a rise is a refusal that happened in this
107
+ * window. Reported the moment it is seen — never held for the heartbeat,
108
+ * because this is the line that says work was lost.
109
+ */
110
+ private reportRefusals;
111
+ private meta;
112
+ }
@@ -58,6 +58,7 @@ export default class NativeMetricsAddon extends BaseAddon<NativeMetricsConfig> {
58
58
  * cgroup once and latches.
59
59
  */
60
60
  private memoryProbe;
61
+ private pidsProbe;
61
62
  /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
62
63
  private readonly memoryRing;
63
64
  /** Cold tier, HUB only — same reasoning as `loadStore`. */
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-_YWIPtUH.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -350,6 +350,211 @@ var ContainerMemoryProbe = class {
350
350
  if (!first) this.logger.info("cgroup memory is answering again");
351
351
  }
352
352
  };
353
+ //#endregion
354
+ //#region src/builtins/native-metrics/cgroup-pids.ts
355
+ /**
356
+ * `pids.max` is either a number or the word `max`.
357
+ *
358
+ * `max` means NO ceiling, and that is `null` — not `Infinity`, not a huge
359
+ * number. A ceiling nothing can reach is not a ceiling, and reporting one
360
+ * would make every utilisation fraction on an unlimited node read as 0 %.
361
+ */
362
+ function parsePidsMax(text) {
363
+ const trimmed = text.trim();
364
+ if (trimmed.length === 0 || trimmed === "max") return null;
365
+ const value = Number(trimmed);
366
+ if (!Number.isFinite(value) || value < 0) return null;
367
+ return value;
368
+ }
369
+ /**
370
+ * `pids.events` is `key value` per line; we read exactly one key:
371
+ *
372
+ * ```
373
+ * max 22
374
+ * ```
375
+ *
376
+ * `max` is the number of times a fork was refused because the limit was hit.
377
+ * A kernel that publishes only `pids.events.local`, or a file we cannot make
378
+ * sense of, answers `null` — unknown, never zero, because zero here means "no
379
+ * refusals have ever happened" and that is a claim.
380
+ */
381
+ function parsePidsEvents(text) {
382
+ for (const rawLine of text.split("\n")) {
383
+ const line = rawLine.trim();
384
+ if (line.length === 0) continue;
385
+ const space = line.indexOf(" ");
386
+ if (space <= 0) continue;
387
+ if (line.slice(0, space) !== "max") continue;
388
+ const value = Number(line.slice(space + 1).trim());
389
+ if (!Number.isFinite(value) || value < 0) return null;
390
+ return value;
391
+ }
392
+ return null;
393
+ }
394
+ /** Turn one layout's files into a snapshot. Pure over the text. */
395
+ function buildContainerPidsSnapshot(input) {
396
+ const current = Number(input.currentText.trim());
397
+ if (!Number.isFinite(current) || current < 0) return null;
398
+ const max = input.maxText === null ? null : parsePidsMax(input.maxText);
399
+ return {
400
+ source: input.source,
401
+ current,
402
+ max,
403
+ usedFraction: max === null || max === 0 ? null : current / max,
404
+ refusals: input.eventsText === null ? null : parsePidsEvents(input.eventsText)
405
+ };
406
+ }
407
+ /**
408
+ * Find the pids cgroup from INSIDE this process, never a hard-coded path —
409
+ * same reasoning as the memory layout: from inside the container the cgroup is
410
+ * at the mount ROOT (`0::/`), from the host it is `<mount>/docker/<id>`, and
411
+ * hard-coding either one breaks the other.
412
+ */
413
+ function resolveCgroupPidsLayout(fs) {
414
+ const selfText = fs.read("/proc/self/cgroup");
415
+ if (selfText === null) return null;
416
+ const entry = parseCgroupSelf(selfText);
417
+ if (entry === null) return null;
418
+ const suffix = entry.path === "/" ? "" : entry.path;
419
+ const candidates = entry.version === "v2" ? [`${CGROUP_MOUNT_ROOT}${suffix}`, CGROUP_MOUNT_ROOT] : [`${CGROUP_MOUNT_ROOT}/pids${suffix}`, `${CGROUP_MOUNT_ROOT}/pids`];
420
+ for (const directory of candidates) {
421
+ if (fs.read(`${directory}/pids.current`) === null) continue;
422
+ return {
423
+ source: entry.version === "v2" ? "cgroup-v2" : "cgroup-v1",
424
+ directory,
425
+ currentPath: `${directory}/pids.current`,
426
+ maxPath: `${directory}/pids.max`,
427
+ eventsPath: `${directory}/pids.events`
428
+ };
429
+ }
430
+ return null;
431
+ }
432
+ var DEFAULT_HEARTBEAT_MS = 6e4;
433
+ var DEFAULT_PRESSURE_FRACTION = .8;
434
+ /**
435
+ * The sampler.
436
+ *
437
+ * Its logging is deliberately different from the memory probe's, because the
438
+ * operator asked for a DAY of regular readings before deciding whether to
439
+ * raise the ceiling: there is a heartbeat, on its own cadence, whatever the
440
+ * sampling tick is. Three lines exist and no more:
441
+ *
442
+ * 1. **Resolution**, once — the hierarchy that answered, or "no cgroup here",
443
+ * which is a fact about the node and not an error.
444
+ * 2. **Heartbeat**, every `heartbeatMs` — current, ceiling, utilisation and
445
+ * the lifetime refusal count. INFO while there is room, WARN while we are
446
+ * above `pressureFraction`, and one INFO when it comes back down, so the
447
+ * log carries BOTH edges of a pressure episode rather than only its start.
448
+ * 3. **A refusal**, immediately — the `pids.events` counter rose, so the
449
+ * kernel said no that many times since the last sample and some subsystem
450
+ * silently did without a thread. This one never waits for the heartbeat.
451
+ */
452
+ var ContainerPidsProbe = class {
453
+ logger;
454
+ fs;
455
+ now;
456
+ heartbeatMs;
457
+ pressureFraction;
458
+ resolved = false;
459
+ layout = null;
460
+ lastHeartbeatAt = null;
461
+ lastRefusals = null;
462
+ underPressure = false;
463
+ constructor(deps) {
464
+ this.logger = deps.logger;
465
+ this.fs = deps.fs ?? nodeCgroupFileReader;
466
+ this.now = deps.now ?? Date.now;
467
+ this.heartbeatMs = deps.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
468
+ this.pressureFraction = deps.pressureFraction ?? DEFAULT_PRESSURE_FRACTION;
469
+ }
470
+ /** Did this node resolve a pids cgroup at all? Absent is a reportable fact. */
471
+ get available() {
472
+ return this.layout !== null;
473
+ }
474
+ /** One reading, or `null` where there is no cgroup or it stopped answering. */
475
+ sample() {
476
+ const layout = this.resolveOnce();
477
+ if (layout === null) return null;
478
+ const currentText = this.fs.read(layout.currentPath);
479
+ if (currentText === null) return null;
480
+ const snapshot = buildContainerPidsSnapshot({
481
+ source: layout.source,
482
+ currentText,
483
+ maxText: this.fs.read(layout.maxPath),
484
+ eventsText: this.fs.read(layout.eventsPath)
485
+ });
486
+ if (snapshot === null) return null;
487
+ this.report(snapshot);
488
+ return snapshot;
489
+ }
490
+ resolveOnce() {
491
+ if (this.resolved) return this.layout;
492
+ this.resolved = true;
493
+ try {
494
+ this.layout = resolveCgroupPidsLayout(this.fs);
495
+ } catch (err) {
496
+ this.layout = null;
497
+ this.logger.warn("cgroup pids resolution failed — the pid ceiling will be ABSENT", { meta: { error: require_dist.errMsg(err) } });
498
+ return null;
499
+ }
500
+ if (this.layout === null) {
501
+ this.logger.info("no pids cgroup on this node — the pid ceiling is ABSENT, not unlimited, for every sample", { meta: {
502
+ platform: process.platform,
503
+ probed: "/proc/self/cgroup"
504
+ } });
505
+ return null;
506
+ }
507
+ this.logger.info("pid ceiling resolved from the cgroup", { meta: {
508
+ source: this.layout.source,
509
+ directory: this.layout.directory
510
+ } });
511
+ return this.layout;
512
+ }
513
+ report(snapshot) {
514
+ this.reportRefusals(snapshot);
515
+ const at = this.now();
516
+ const pressure = snapshot.usedFraction !== null && snapshot.usedFraction >= this.pressureFraction;
517
+ if (this.underPressure && !pressure) {
518
+ this.underPressure = false;
519
+ this.logger.info("pid pressure recovered — back under the guard fraction", { meta: this.meta(snapshot) });
520
+ this.lastHeartbeatAt = at;
521
+ return;
522
+ }
523
+ if (!(this.lastHeartbeatAt === null || at - this.lastHeartbeatAt >= this.heartbeatMs)) return;
524
+ this.lastHeartbeatAt = at;
525
+ this.underPressure = pressure;
526
+ if (pressure) {
527
+ this.logger.warn("pid ceiling is being approached — a refused clone() is silent work loss, and this container counts THREADS", { meta: this.meta(snapshot) });
528
+ return;
529
+ }
530
+ this.logger.info("pid ceiling", { meta: this.meta(snapshot) });
531
+ }
532
+ /**
533
+ * The counter only ever rises, so a rise is a refusal that happened in this
534
+ * window. Reported the moment it is seen — never held for the heartbeat,
535
+ * because this is the line that says work was lost.
536
+ */
537
+ reportRefusals(snapshot) {
538
+ const refusals = snapshot.refusals;
539
+ if (refusals === null) return;
540
+ const previous = this.lastRefusals;
541
+ this.lastRefusals = refusals;
542
+ if (previous === null || refusals <= previous) return;
543
+ this.logger.warn("the kernel REFUSED a fork at the pid ceiling — something asked for a thread and did not get one", { meta: {
544
+ ...this.meta(snapshot),
545
+ refusedSinceLastSample: refusals - previous
546
+ } });
547
+ }
548
+ meta(snapshot) {
549
+ return {
550
+ current: snapshot.current,
551
+ max: snapshot.max,
552
+ usedPct: snapshot.usedFraction === null ? null : Math.round(snapshot.usedFraction * 1e3) / 10,
553
+ refusalsLifetime: snapshot.refusals,
554
+ source: snapshot.source
555
+ };
556
+ }
557
+ };
353
558
  /**
354
559
  * One node's fixed-capacity circular buffer of samples.
355
560
  *
@@ -2809,6 +3014,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
2809
3014
  * cgroup once and latches.
2810
3015
  */
2811
3016
  memoryProbe = null;
3017
+ pidsProbe = null;
2812
3018
  /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
2813
3019
  memoryRing = new ContainerMemoryRing();
2814
3020
  /** Cold tier, HUB only — same reasoning as `loadStore`. */
@@ -2895,6 +3101,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
2895
3101
  });
2896
3102
  }
2897
3103
  this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
3104
+ this.pidsProbe = new ContainerPidsProbe({ logger: this.ctx.logger.child("ContainerPids") });
2898
3105
  this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
2899
3106
  this.startProcessSnapshotTimer();
2900
3107
  this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes, event.data.containerMemory)));
@@ -3135,6 +3342,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3135
3342
  try {
3136
3343
  const processes = await this.listNodeProcesses();
3137
3344
  const containerMemory = this.memoryProbe?.sample() ?? null;
3345
+ this.pidsProbe?.sample();
3138
3346
  eventBus.emit(require_dist.createEvent(require_dist.EventCategory.MetricsNodeProcessesSnapshot, {
3139
3347
  type: "node",
3140
3348
  id: nodeId,
@@ -1,4 +1,4 @@
1
- import { Lt as reducePoints, Ot as metricsProviderCapability, Pn as EventCategory, Rt as resolveBucketMs, an as errMsg, bn as createEvent, lt as foldSnapshotByFunction, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Lt as reducePoints, Ot as metricsProviderCapability, Pn as EventCategory, Rt as resolveBucketMs, an as errMsg, bn as createEvent, lt as foldSnapshotByFunction, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";
@@ -345,6 +345,211 @@ var ContainerMemoryProbe = class {
345
345
  if (!first) this.logger.info("cgroup memory is answering again");
346
346
  }
347
347
  };
348
+ //#endregion
349
+ //#region src/builtins/native-metrics/cgroup-pids.ts
350
+ /**
351
+ * `pids.max` is either a number or the word `max`.
352
+ *
353
+ * `max` means NO ceiling, and that is `null` — not `Infinity`, not a huge
354
+ * number. A ceiling nothing can reach is not a ceiling, and reporting one
355
+ * would make every utilisation fraction on an unlimited node read as 0 %.
356
+ */
357
+ function parsePidsMax(text) {
358
+ const trimmed = text.trim();
359
+ if (trimmed.length === 0 || trimmed === "max") return null;
360
+ const value = Number(trimmed);
361
+ if (!Number.isFinite(value) || value < 0) return null;
362
+ return value;
363
+ }
364
+ /**
365
+ * `pids.events` is `key value` per line; we read exactly one key:
366
+ *
367
+ * ```
368
+ * max 22
369
+ * ```
370
+ *
371
+ * `max` is the number of times a fork was refused because the limit was hit.
372
+ * A kernel that publishes only `pids.events.local`, or a file we cannot make
373
+ * sense of, answers `null` — unknown, never zero, because zero here means "no
374
+ * refusals have ever happened" and that is a claim.
375
+ */
376
+ function parsePidsEvents(text) {
377
+ for (const rawLine of text.split("\n")) {
378
+ const line = rawLine.trim();
379
+ if (line.length === 0) continue;
380
+ const space = line.indexOf(" ");
381
+ if (space <= 0) continue;
382
+ if (line.slice(0, space) !== "max") continue;
383
+ const value = Number(line.slice(space + 1).trim());
384
+ if (!Number.isFinite(value) || value < 0) return null;
385
+ return value;
386
+ }
387
+ return null;
388
+ }
389
+ /** Turn one layout's files into a snapshot. Pure over the text. */
390
+ function buildContainerPidsSnapshot(input) {
391
+ const current = Number(input.currentText.trim());
392
+ if (!Number.isFinite(current) || current < 0) return null;
393
+ const max = input.maxText === null ? null : parsePidsMax(input.maxText);
394
+ return {
395
+ source: input.source,
396
+ current,
397
+ max,
398
+ usedFraction: max === null || max === 0 ? null : current / max,
399
+ refusals: input.eventsText === null ? null : parsePidsEvents(input.eventsText)
400
+ };
401
+ }
402
+ /**
403
+ * Find the pids cgroup from INSIDE this process, never a hard-coded path —
404
+ * same reasoning as the memory layout: from inside the container the cgroup is
405
+ * at the mount ROOT (`0::/`), from the host it is `<mount>/docker/<id>`, and
406
+ * hard-coding either one breaks the other.
407
+ */
408
+ function resolveCgroupPidsLayout(fs) {
409
+ const selfText = fs.read("/proc/self/cgroup");
410
+ if (selfText === null) return null;
411
+ const entry = parseCgroupSelf(selfText);
412
+ if (entry === null) return null;
413
+ const suffix = entry.path === "/" ? "" : entry.path;
414
+ const candidates = entry.version === "v2" ? [`${CGROUP_MOUNT_ROOT}${suffix}`, CGROUP_MOUNT_ROOT] : [`${CGROUP_MOUNT_ROOT}/pids${suffix}`, `${CGROUP_MOUNT_ROOT}/pids`];
415
+ for (const directory of candidates) {
416
+ if (fs.read(`${directory}/pids.current`) === null) continue;
417
+ return {
418
+ source: entry.version === "v2" ? "cgroup-v2" : "cgroup-v1",
419
+ directory,
420
+ currentPath: `${directory}/pids.current`,
421
+ maxPath: `${directory}/pids.max`,
422
+ eventsPath: `${directory}/pids.events`
423
+ };
424
+ }
425
+ return null;
426
+ }
427
+ var DEFAULT_HEARTBEAT_MS = 6e4;
428
+ var DEFAULT_PRESSURE_FRACTION = .8;
429
+ /**
430
+ * The sampler.
431
+ *
432
+ * Its logging is deliberately different from the memory probe's, because the
433
+ * operator asked for a DAY of regular readings before deciding whether to
434
+ * raise the ceiling: there is a heartbeat, on its own cadence, whatever the
435
+ * sampling tick is. Three lines exist and no more:
436
+ *
437
+ * 1. **Resolution**, once — the hierarchy that answered, or "no cgroup here",
438
+ * which is a fact about the node and not an error.
439
+ * 2. **Heartbeat**, every `heartbeatMs` — current, ceiling, utilisation and
440
+ * the lifetime refusal count. INFO while there is room, WARN while we are
441
+ * above `pressureFraction`, and one INFO when it comes back down, so the
442
+ * log carries BOTH edges of a pressure episode rather than only its start.
443
+ * 3. **A refusal**, immediately — the `pids.events` counter rose, so the
444
+ * kernel said no that many times since the last sample and some subsystem
445
+ * silently did without a thread. This one never waits for the heartbeat.
446
+ */
447
+ var ContainerPidsProbe = class {
448
+ logger;
449
+ fs;
450
+ now;
451
+ heartbeatMs;
452
+ pressureFraction;
453
+ resolved = false;
454
+ layout = null;
455
+ lastHeartbeatAt = null;
456
+ lastRefusals = null;
457
+ underPressure = false;
458
+ constructor(deps) {
459
+ this.logger = deps.logger;
460
+ this.fs = deps.fs ?? nodeCgroupFileReader;
461
+ this.now = deps.now ?? Date.now;
462
+ this.heartbeatMs = deps.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
463
+ this.pressureFraction = deps.pressureFraction ?? DEFAULT_PRESSURE_FRACTION;
464
+ }
465
+ /** Did this node resolve a pids cgroup at all? Absent is a reportable fact. */
466
+ get available() {
467
+ return this.layout !== null;
468
+ }
469
+ /** One reading, or `null` where there is no cgroup or it stopped answering. */
470
+ sample() {
471
+ const layout = this.resolveOnce();
472
+ if (layout === null) return null;
473
+ const currentText = this.fs.read(layout.currentPath);
474
+ if (currentText === null) return null;
475
+ const snapshot = buildContainerPidsSnapshot({
476
+ source: layout.source,
477
+ currentText,
478
+ maxText: this.fs.read(layout.maxPath),
479
+ eventsText: this.fs.read(layout.eventsPath)
480
+ });
481
+ if (snapshot === null) return null;
482
+ this.report(snapshot);
483
+ return snapshot;
484
+ }
485
+ resolveOnce() {
486
+ if (this.resolved) return this.layout;
487
+ this.resolved = true;
488
+ try {
489
+ this.layout = resolveCgroupPidsLayout(this.fs);
490
+ } catch (err) {
491
+ this.layout = null;
492
+ this.logger.warn("cgroup pids resolution failed — the pid ceiling will be ABSENT", { meta: { error: errMsg(err) } });
493
+ return null;
494
+ }
495
+ if (this.layout === null) {
496
+ this.logger.info("no pids cgroup on this node — the pid ceiling is ABSENT, not unlimited, for every sample", { meta: {
497
+ platform: process.platform,
498
+ probed: "/proc/self/cgroup"
499
+ } });
500
+ return null;
501
+ }
502
+ this.logger.info("pid ceiling resolved from the cgroup", { meta: {
503
+ source: this.layout.source,
504
+ directory: this.layout.directory
505
+ } });
506
+ return this.layout;
507
+ }
508
+ report(snapshot) {
509
+ this.reportRefusals(snapshot);
510
+ const at = this.now();
511
+ const pressure = snapshot.usedFraction !== null && snapshot.usedFraction >= this.pressureFraction;
512
+ if (this.underPressure && !pressure) {
513
+ this.underPressure = false;
514
+ this.logger.info("pid pressure recovered — back under the guard fraction", { meta: this.meta(snapshot) });
515
+ this.lastHeartbeatAt = at;
516
+ return;
517
+ }
518
+ if (!(this.lastHeartbeatAt === null || at - this.lastHeartbeatAt >= this.heartbeatMs)) return;
519
+ this.lastHeartbeatAt = at;
520
+ this.underPressure = pressure;
521
+ if (pressure) {
522
+ this.logger.warn("pid ceiling is being approached — a refused clone() is silent work loss, and this container counts THREADS", { meta: this.meta(snapshot) });
523
+ return;
524
+ }
525
+ this.logger.info("pid ceiling", { meta: this.meta(snapshot) });
526
+ }
527
+ /**
528
+ * The counter only ever rises, so a rise is a refusal that happened in this
529
+ * window. Reported the moment it is seen — never held for the heartbeat,
530
+ * because this is the line that says work was lost.
531
+ */
532
+ reportRefusals(snapshot) {
533
+ const refusals = snapshot.refusals;
534
+ if (refusals === null) return;
535
+ const previous = this.lastRefusals;
536
+ this.lastRefusals = refusals;
537
+ if (previous === null || refusals <= previous) return;
538
+ this.logger.warn("the kernel REFUSED a fork at the pid ceiling — something asked for a thread and did not get one", { meta: {
539
+ ...this.meta(snapshot),
540
+ refusedSinceLastSample: refusals - previous
541
+ } });
542
+ }
543
+ meta(snapshot) {
544
+ return {
545
+ current: snapshot.current,
546
+ max: snapshot.max,
547
+ usedPct: snapshot.usedFraction === null ? null : Math.round(snapshot.usedFraction * 1e3) / 10,
548
+ refusalsLifetime: snapshot.refusals,
549
+ source: snapshot.source
550
+ };
551
+ }
552
+ };
348
553
  /**
349
554
  * One node's fixed-capacity circular buffer of samples.
350
555
  *
@@ -2804,6 +3009,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends BaseAddon {
2804
3009
  * cgroup once and latches.
2805
3010
  */
2806
3011
  memoryProbe = null;
3012
+ pidsProbe = null;
2807
3013
  /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
2808
3014
  memoryRing = new ContainerMemoryRing();
2809
3015
  /** Cold tier, HUB only — same reasoning as `loadStore`. */
@@ -2890,6 +3096,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends BaseAddon {
2890
3096
  });
2891
3097
  }
2892
3098
  this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
3099
+ this.pidsProbe = new ContainerPidsProbe({ logger: this.ctx.logger.child("ContainerPids") });
2893
3100
  this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
2894
3101
  this.startProcessSnapshotTimer();
2895
3102
  this.ctx.addDisposer(this.ctx.eventBus.subscribe({ category: EventCategory.MetricsNodeProcessesSnapshot }, (event) => this.retainSnapshot(event.data.nodeId, event.data.timestamp, event.data.processes, event.data.containerMemory)));
@@ -3130,6 +3337,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends BaseAddon {
3130
3337
  try {
3131
3338
  const processes = await this.listNodeProcesses();
3132
3339
  const containerMemory = this.memoryProbe?.sample() ?? null;
3340
+ this.pidsProbe?.sample();
3133
3341
  eventBus.emit(createEvent(EventCategory.MetricsNodeProcessesSnapshot, {
3134
3342
  type: "node",
3135
3343
  id: nodeId,
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-_YWIPtUH.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Gt as scoreRuntimes, Pn as EventCategory, Pt as platformProbeCapability, Sn as emitReadiness, an as errMsg, nt as enumerateInferenceDevices, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
2
+ import { Gt as scoreRuntimes, Pn as EventCategory, Pt as platformProbeCapability, Sn as emitReadiness, an as errMsg, nt as enumerateInferenceDevices, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-_YWIPtUH.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Pn as EventCategory, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Pn as EventCategory, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-_YWIPtUH.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
7
  const require_snapshot_courtesy = require("../../snapshot-courtesy-Cqs7GkOV.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { B as bareAddonId, Dn as nodePin, Jt as snapshotCapability, Pn as EventCategory, V as batteryCapability, Z as deriveBatteryPresence, a as BatteryStatusSchema, an as errMsg, dn as DeviceFeature, en as streamQualityLabel, pn as DeviceType, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { B as bareAddonId, Dn as nodePin, Jt as snapshotCapability, Pn as EventCategory, V as batteryCapability, Z as deriveBatteryPresence, a as BatteryStatusSchema, an as errMsg, dn as DeviceFeature, en as streamQualityLabel, pn as DeviceType, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg } from "../../snapshot-courtesy-CZPvteq2.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-_YWIPtUH.js");
6
+ const require_dist = require("../../dist-D5ONNqL6.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Qt as storageProviderCapability, ct as filesystemBrowseCapability, sn as BaseAddon } from "../../dist-Diz59Dhi.mjs";
1
+ import { Qt as storageProviderCapability, ct as filesystemBrowseCapability, sn as BaseAddon } from "../../dist-DrLhdcwI.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";