@camstack/server 1.2.689 → 1.2.691

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.691] - 2026-10-29
11
+
12
+ ### Changed
13
+
14
+ - An unpaired agent now shows its real memory on its own page instead of unknown.
15
+
16
+ ## [1.2.690] - 2026-10-29
17
+
18
+ ### Changed
19
+
20
+ - The agent reports its memory, reclaims stranded pages and gets a heap ceiling sized to the host; a node's launch parameters (heap ceiling, allocator) come from the server and can be overridden per node from the hub
21
+
10
22
  ## [1.2.689] - 2026-10-29
11
23
 
12
24
  ### Added
@@ -17,6 +29,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
29
  - Generated routers for the pet-fountain capability
18
30
  - Agents report their own process memory in the topology (the core process showed 0), and an unreadable memory shows as unknown
19
31
 
32
+ ### Changed
33
+
34
+ - The agent now reports its own [mem] agent-main heartbeat and reclaims stranded memory, as the hub does
35
+ - The hub tells its addon runners which telemetry categories anyone consumes, so unused ones stop crossing the process boundary
36
+ - Node launch parameters (heap ceiling, allocator env) are decided by a closure module, with a per-node override and a source line in the boot log
37
+ - A node's heap-ceiling override can be read and set through server-management (applies at the next restart)
38
+
20
39
  ## [1.2.688] - 2026-10-29
21
40
 
22
41
  ### Changed
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startAgentHeapWatch = startAgentHeapWatch;
4
+ /**
5
+ * The agent root's own memory, on the record and returned to the OS.
6
+ *
7
+ * hub-main has had the `[mem]` heartbeat and the stranded-page reclaimer since
8
+ * 2026-08; the agent root process (container AND desktop — both reach
9
+ * `startAgent` through the same launcher) had neither, so a measured 0.8–1.4 GB
10
+ * agent root emitted no line at all and never gave a page back. This is the same
11
+ * mechanism, not a second one: `startHeapWatch` with the V8 reclaimer, labelled
12
+ * `agent-main`.
13
+ */
14
+ const system_1 = require("@camstack/system");
15
+ const DEFAULT_DEPS = {
16
+ start: system_1.startHeapWatch,
17
+ createReclaimer: system_1.createV8Reclaimer,
18
+ };
19
+ /**
20
+ * Start the heartbeat + reclaim for the agent root. Returns the stop function.
21
+ * `createV8Reclaimer()` returns undefined when V8 declines; the heartbeat then
22
+ * runs report-only, exactly as hub-main does.
23
+ */
24
+ function startAgentHeapWatch(logger, deps = DEFAULT_DEPS) {
25
+ const reclaimer = deps.createReclaimer();
26
+ const sink = {
27
+ info: (line) => {
28
+ logger.info(line);
29
+ },
30
+ warn: (line) => {
31
+ logger.warn(line);
32
+ },
33
+ };
34
+ return deps.start(system_1.AGENT_MAIN_HEAP_WATCH_LABEL, sink, undefined, reclaimer === undefined ? undefined : { reclaim: reclaimer }, undefined, undefined,
35
+ // Say who chose the ceiling: the default ceiling below is applied by the
36
+ // launcher's argv, and an absent flag means the image/desktop predates it.
37
+ true);
38
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveStatusMetrics = resolveStatusMetrics;
4
+ /**
5
+ * The metrics source `$agent.status` reads (and so the agent's own local page).
6
+ *
7
+ * A PAIRED agent boots the `metrics-provider` addon, so the registry answers. An
8
+ * UNPAIRED agent boots no addon at all (agent-boot-mode.ts), the registry has no
9
+ * provider, and the page read memory as unknown although the host reader works. The
10
+ * fallback is the SAME native reader the addon registers - never a second formula -
11
+ * and it is asked only when the registry has nothing.
12
+ */
13
+ function resolveStatusMetrics(fromRegistry, fallback) {
14
+ return () => fromRegistry() ?? fallback();
15
+ }
@@ -59,6 +59,7 @@ const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
59
59
  const agent_reconnect_js_1 = require("./agent-reconnect.js");
60
60
  const serial_queue_js_1 = require("./serial-queue.js");
61
61
  const agent_boot_mode_js_1 = require("./agent-boot-mode.js");
62
+ const agent_status_metrics_js_1 = require("./agent-status-metrics.js");
62
63
  const agent_self_update_js_1 = require("./agent-self-update.js");
63
64
  const update_check_scheduler_js_1 = require("../core/updates/update-check-scheduler.js");
64
65
  const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
@@ -76,6 +77,7 @@ const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.
76
77
  // entries flow through it (console output + hub forwarding).
77
78
  // ---------------------------------------------------------------------------
78
79
  const system_2 = require("@camstack/system");
80
+ const agent_heap_watch_js_1 = require("./agent-heap-watch.js");
79
81
  /** A reconnect (stop + rebuild + start) that takes longer than this is abandoned. */
80
82
  const RECONNECT_DEADLINE_MS = 90_000;
81
83
  /** How often the agent re-evaluates whether to ask its hub "am I still a member?" (D857). */
@@ -97,6 +99,11 @@ const DATA_NODEID_CAPS = new Set(types_2.ALL_CAPABILITY_DEFINITIONS.filter((c) =
97
99
  // Main
98
100
  // ---------------------------------------------------------------------------
99
101
  async function startAgent(configPath) {
102
+ // The agent root's own memory: `[mem] agent-main` heartbeat + the stranded-page
103
+ // reclaim hub-main has had since 2026-08. Started first so a slow boot is on the
104
+ // record; lines reach the hub through the same LogManager the addons use.
105
+ const heapWatchLogger = agentLogManager.createLogger().withTags({ addonId: 'heap-watch' });
106
+ (0, agent_heap_watch_js_1.startAgentHeapWatch)(heapWatchLogger);
100
107
  const config = (0, agent_config_js_1.loadAgentConfig)(configPath);
101
108
  // Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
102
109
  // no longer need a second, redundant env var set by hand (the cross-node
@@ -431,6 +438,7 @@ async function startAgent(configPath) {
431
438
  withTags: () => consoleLogger,
432
439
  };
433
440
  const capabilityRegistry = new system_1.CapabilityRegistry(consoleLogger);
441
+ let unpairedMetrics = null;
434
442
  // Declare all capabilities the agent uses — required before registerProvider()
435
443
  const agentCapabilities = [
436
444
  types_2.storageCapability,
@@ -480,7 +488,7 @@ async function startAgent(configPath) {
480
488
  dataDir: config.dataDir,
481
489
  runnerBootCheck: () => agentRunnerBootCheck?.current(),
482
490
  });
483
- capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
491
+ capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService, config.dataDir));
484
492
  // The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
485
493
  // hub-issued HTTPS leaf hot-swapped into the sniffing listener. The listener is
486
494
  // created later in boot, so it is read through a getter.
@@ -547,7 +555,17 @@ async function startAgent(configPath) {
547
555
  // Resolve the current metrics-provider cap lazily from the registry.
548
556
  // The cap is registered during bootCoreAddons but may not exist in test
549
557
  // scenarios — `null` is a documented fallback for both.
550
- getMetricsProvider: () => capabilityRegistry.getSingleton('metrics-provider'),
558
+ // An UNPAIRED agent boots no addon, so the registry has no provider: its own page
559
+ // reads the same native reader the addon would register (see agent-status-metrics.ts).
560
+ getMetricsProvider: (0, agent_status_metrics_js_1.resolveStatusMetrics)(() => capabilityRegistry.getSingleton('metrics-provider'), () => {
561
+ if ((0, agent_boot_mode_js_1.decideAgentBootMode)({ paired: (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath) }) === 'full')
562
+ return null;
563
+ if (unpairedMetrics === null) {
564
+ unpairedMetrics = new system_1.NativeMetricsProvider();
565
+ unpairedMetrics.startSampling(5000);
566
+ }
567
+ return unpairedMetrics;
568
+ }),
551
569
  agentVersion: readAgentVersion(),
552
570
  // Drives `$agent.reload` — re-runs the same discovery pass the bootstrap
553
571
  // does at startup, picking up tarballs just landed via `$agent.deploy`.
@@ -1,7 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildLaunchOverrideMethods = buildLaunchOverrideMethods;
3
4
  exports.buildServerManagementProvider = buildServerManagementProvider;
4
- function buildServerManagementProvider(service) {
5
+ const launch_args_js_1 = require("../../launch-args.js");
6
+ /** The per-node launch override methods (ADR 0970), backed by the node's data dir. */
7
+ function buildLaunchOverrideMethods(dataDir) {
8
+ return {
9
+ getLaunchOverrides: () => Promise.resolve({ heapMb: (0, launch_args_js_1.readHeapOverrideMb)(dataDir) }),
10
+ setLaunchOverrides: (input) => {
11
+ (0, launch_args_js_1.writeHeapOverrideMb)(dataDir, input.heapMb);
12
+ return Promise.resolve({ heapMb: input.heapMb, restartRequired: true });
13
+ },
14
+ };
15
+ }
16
+ function buildServerManagementProvider(service, dataDir) {
5
17
  if (service === null)
6
18
  return null;
7
19
  return {
@@ -10,5 +22,6 @@ function buildServerManagementProvider(service) {
10
22
  applyServerUpdate: (input) => service.applyServerUpdate(input),
11
23
  rollbackServerUpdate: () => service.rollbackServerUpdate(),
12
24
  restartServer: () => Promise.resolve(service.restartNode()),
25
+ ...buildLaunchOverrideMethods(dataDir),
13
26
  };
14
27
  }
@@ -11047,6 +11047,22 @@ function createCapRouter_serverManagement(getProvider, createRemoteProxy) {
11047
11047
  const p = resolveProvider('server-management', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
11048
11048
  return p.restartServer();
11049
11049
  }),
11050
+ getLaunchOverrides: trpc_middleware_js_1.adminProcedure
11051
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
11052
+ .output(types_126.serverManagementCapability.methods.getLaunchOverrides.output)
11053
+ .query(async ({ input, ctx }) => {
11054
+ const p = resolveProvider('server-management', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
11055
+ return p.getLaunchOverrides();
11056
+ }),
11057
+ setLaunchOverrides: trpc_middleware_js_1.adminProcedure
11058
+ .input(types_126.serverManagementCapability.methods.setLaunchOverrides.input.loose())
11059
+ .output(types_126.serverManagementCapability.methods.setLaunchOverrides.output)
11060
+ .mutation(async ({ input, ctx }) => {
11061
+ const { nodeId, ...methodInput } = input;
11062
+ const p = resolveProvider('server-management', nodeId, () => getProvider(ctx), createRemoteProxy);
11063
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
11064
+ return p.setLaunchOverrides(methodInput);
11065
+ }),
11050
11066
  });
11051
11067
  }
11052
11068
  function createCapRouter_settingsStore(getProvider, createRemoteProxy) {
@@ -254,7 +254,7 @@ function buildServerProviders(services) {
254
254
  integrations: wrap(() => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
255
255
  nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry, (0, cap_providers_js_1.createNodeRootPackageLookup)(services.moleculer, services.serverUpdateService), services.loggingService.createLogger('nodes'))),
256
256
  // server-management — runtime-updatable root package (phase 1: hub only).
257
- 'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService)),
257
+ 'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService, path.resolve(process.env['CAMSTACK_DATA'] ?? 'camstack-data'))),
258
258
  addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.agentRegistry)),
259
259
  // webrtc-session — the resolved broker singleton, wrapped per request to
260
260
  // inject the server-read User-Agent into signaling attribution (the broker
@@ -759,6 +759,14 @@ class MoleculerService {
759
759
  // agents (non-retaining nodes).
760
760
  const hubChildRegistry = this.localChildRegistry;
761
761
  (0, system_1.setNodeEventInterest)(this.broker, () => hubChildRegistry.aggregateEventInterest());
762
+ // Producer-side suppression (D8): tell each forked child which categories
763
+ // someone else wants — siblings plus the hub's own in-process subscribers
764
+ // (the admin live stream is one) — so a high-frequency category nobody
765
+ // wants never leaves its process. Hub only: an agent cannot see the hub's
766
+ // subscribers, so its children keep sending everything. A remote node's
767
+ // interest in a ring-denied category is NOT visible here (ADR 0969).
768
+ hubChildRegistry.setEventInterestSource(() => (0, system_1.getLocalEventInterest)(this.broker));
769
+ (0, system_1.onLocalEventInterestChange)(this.broker, () => hubChildRegistry.refreshEventInterest());
762
770
  }
763
771
  }
764
772
  /**
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ALLOCATOR_ENV_DEFAULTS = exports.OVERRIDE_MAX_MB = exports.OVERRIDE_MIN_MB = exports.AGENT_HEAP_CAP_MB = exports.AGENT_HEAP_FLOOR_MB = exports.HEAP_CEILING_SOURCE_ENV = exports.LAUNCH_OVERRIDES_FILE = void 0;
37
+ exports.computedAgentHeapMb = computedAgentHeapMb;
38
+ exports.planLaunch = planLaunch;
39
+ exports.renderLaunchPlan = renderLaunchPlan;
40
+ exports.overridesPath = overridesPath;
41
+ exports.readHeapOverrideMb = readHeapOverrideMb;
42
+ exports.writeHeapOverrideMb = writeHeapOverrideMb;
43
+ exports.readCgroupLimitBytes = readCgroupLimitBytes;
44
+ exports.runLaunchCli = runLaunchCli;
45
+ /**
46
+ * NODE LAUNCH ARGUMENTS — decided by the server closure, executed by the image.
47
+ *
48
+ * `node <closure>/dist/launch-args.js --role hub|agent [--allocator-env]` prints
49
+ * what the launcher should add to the node's command line, so the Docker
50
+ * entrypoint, the Electron shell and the Windows service supervisor carry NO copy
51
+ * of the formula: changing it (or a node's override) needs a closure update and a
52
+ * restart, never a new image or app.
53
+ *
54
+ * Zero dependencies (node built-ins only): it runs BEFORE the closure's own
55
+ * `node_modules` resolution, from either the active closure or the baked seed.
56
+ *
57
+ * ## Output (stdout, one record per line, `\n` separated)
58
+ *
59
+ * arg --max-old-space-size=2048 an extra node argv entry (flags only)
60
+ * env NAME=value an environment variable to export
61
+ * source override|legacy-env|computed|v8-default
62
+ * warn <free text> something the launcher should log
63
+ *
64
+ * Nothing else is printed. A launcher treats any line it does not recognise as
65
+ * absent, and a non-zero exit, a timeout or empty output as "no extra args".
66
+ *
67
+ * ## The heap ceiling
68
+ *
69
+ * - `override` — `<dataDir>/launch-overrides.json` `{ "heapMb": N }`, set from
70
+ * the hub (`serverManagement.setLaunchOverrides`) and persisted on the node.
71
+ * - `legacy-env` — `CAMSTACK_HUB_HEAP_MB` (hub) / `CAMSTACK_AGENT_HEAP_MB`
72
+ * (agent), still honoured when no override is set.
73
+ * - `computed` — AGENT only: clamp(min(RAM, cgroup limit) / 4, 1536, 4096) MB.
74
+ * V8's own quarter-of-RAM rule with a floor above every measured agent RSS
75
+ * (0.8-1.4 GB), so it bounds a runaway and never a healthy root.
76
+ * - `v8-default` — HUB only, nothing set: no flag. A hardcoded 1536 killed the
77
+ * hub four times in 15 h (see the history in the Dockerfile); V8's default
78
+ * here was 4288 MB and is still bounded.
79
+ *
80
+ * Set on argv, never NODE_OPTIONS: forked addon runners are sized separately.
81
+ */
82
+ const fs = __importStar(require("node:fs"));
83
+ const os = __importStar(require("node:os"));
84
+ const path = __importStar(require("node:path"));
85
+ exports.LAUNCH_OVERRIDES_FILE = 'launch-overrides.json';
86
+ /** Env var carrying {@link CeilingSource} to the node so its heartbeat can say who chose. */
87
+ exports.HEAP_CEILING_SOURCE_ENV = 'CAMSTACK_HEAP_CEILING_SOURCE';
88
+ exports.AGENT_HEAP_FLOOR_MB = 1536;
89
+ exports.AGENT_HEAP_CAP_MB = 4096;
90
+ /** An override outside this range is ignored with a warning, never applied. */
91
+ exports.OVERRIDE_MIN_MB = 512;
92
+ exports.OVERRIDE_MAX_MB = 65536;
93
+ /**
94
+ * The native allocator pair, welded: glibc reads MALLOC_ARENA_MAX once at malloc
95
+ * init, so it must exist before `exec node`; sharp restores libvips concurrency
96
+ * from 1 to `availableParallelism()` the moment it appears, so the pair travels
97
+ * together or not at all. Defaults only: an operator value already in the
98
+ * environment wins (the launcher applies them with `: "${NAME:=value}"` semantics).
99
+ */
100
+ exports.ALLOCATOR_ENV_DEFAULTS = {
101
+ MALLOC_ARENA_MAX: '2',
102
+ VIPS_CONCURRENCY: '1',
103
+ };
104
+ const BYTES_PER_MB = 1024 * 1024;
105
+ /** cgroup v1 reports "unlimited" as a value near 2^63. */
106
+ const CGROUP_UNLIMITED_BYTES = 2 ** 60;
107
+ function validMb(value) {
108
+ return Number.isInteger(value) && value >= exports.OVERRIDE_MIN_MB && value <= exports.OVERRIDE_MAX_MB;
109
+ }
110
+ /** The agent's computed ceiling for `host`, in MB. */
111
+ function computedAgentHeapMb(host) {
112
+ const limits = [host.totalMemBytes, host.cgroupLimitBytes ?? Number.POSITIVE_INFINITY].filter((n) => Number.isFinite(n) && n > 0);
113
+ if (limits.length === 0)
114
+ return exports.AGENT_HEAP_FLOOR_MB;
115
+ const quarterMb = Math.floor(Math.min(...limits) / BYTES_PER_MB / 4);
116
+ return Math.min(exports.AGENT_HEAP_CAP_MB, Math.max(exports.AGENT_HEAP_FLOOR_MB, quarterMb));
117
+ }
118
+ function planLaunch(input) {
119
+ const warnings = [];
120
+ let source = input.role === 'agent' ? 'computed' : 'v8-default';
121
+ let heapMb = input.role === 'agent' ? computedAgentHeapMb(input.host) : null;
122
+ const legacy = input.legacyEnv;
123
+ if (legacy !== undefined && legacy !== '') {
124
+ const mb = /^[1-9][0-9]*$/.test(legacy) ? Number(legacy) : Number.NaN;
125
+ if (validMb(mb)) {
126
+ source = 'legacy-env';
127
+ heapMb = mb;
128
+ }
129
+ else {
130
+ warnings.push(`ignoring the legacy heap env "${legacy}": not an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB`);
131
+ }
132
+ }
133
+ if (input.overrideMb !== null) {
134
+ if (validMb(input.overrideMb)) {
135
+ source = 'override';
136
+ heapMb = input.overrideMb;
137
+ }
138
+ else {
139
+ warnings.push(`ignoring the heap override ${input.overrideMb}: not an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB`);
140
+ }
141
+ }
142
+ return {
143
+ args: heapMb === null ? [] : [`--max-old-space-size=${heapMb}`],
144
+ env: {
145
+ [exports.HEAP_CEILING_SOURCE_ENV]: source,
146
+ ...(input.allocatorEnv ? exports.ALLOCATOR_ENV_DEFAULTS : {}),
147
+ },
148
+ source,
149
+ warnings,
150
+ };
151
+ }
152
+ function renderLaunchPlan(plan) {
153
+ const lines = [
154
+ ...plan.args.map((a) => `arg ${a}`),
155
+ ...Object.entries(plan.env).map(([k, v]) => `env ${k}=${v}`),
156
+ `source ${plan.source}`,
157
+ ...plan.warnings.map((w) => `warn ${w.replace(/[\r\n]+/g, ' ')}`),
158
+ ];
159
+ return `${lines.join('\n')}\n`;
160
+ }
161
+ const realFs = {
162
+ readFile(file) {
163
+ try {
164
+ return fs.readFileSync(file, 'utf8');
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ },
170
+ };
171
+ function overridesPath(dataDir) {
172
+ return path.join(dataDir, exports.LAUNCH_OVERRIDES_FILE);
173
+ }
174
+ /** The persisted heap override in MB; `null` when absent, unreadable or not a number. */
175
+ function readHeapOverrideMb(dataDir, io = realFs) {
176
+ const raw = io.readFile(overridesPath(dataDir));
177
+ if (raw === null)
178
+ return null;
179
+ try {
180
+ const parsed = JSON.parse(raw);
181
+ if (typeof parsed !== 'object' || parsed === null || !('heapMb' in parsed))
182
+ return null;
183
+ const value = parsed.heapMb;
184
+ return typeof value === 'number' ? value : null;
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ /**
191
+ * Persist (or, with `null`, clear) the heap override. Validated against the same
192
+ * range the launcher enforces, so a value the next launch would ignore is refused
193
+ * here instead of silently doing nothing. Written atomically (temp + rename).
194
+ */
195
+ function writeHeapOverrideMb(dataDir, heapMb) {
196
+ const file = overridesPath(dataDir);
197
+ if (heapMb === null) {
198
+ fs.rmSync(file, { force: true });
199
+ return;
200
+ }
201
+ if (!validMb(heapMb)) {
202
+ throw new Error(`heap override must be an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB, got ${heapMb}`);
203
+ }
204
+ fs.mkdirSync(dataDir, { recursive: true });
205
+ const tmp = `${file}.${process.pid}.tmp`;
206
+ fs.writeFileSync(tmp, `${JSON.stringify({ heapMb })}\n`, { mode: 0o600 });
207
+ fs.renameSync(tmp, file);
208
+ }
209
+ function readCgroupLimitBytes(io = realFs, files) {
210
+ for (const file of files ?? [
211
+ '/sys/fs/cgroup/memory.max',
212
+ '/sys/fs/cgroup/memory/memory.limit_in_bytes',
213
+ ]) {
214
+ const raw = io.readFile(file)?.trim();
215
+ if (raw === undefined || !/^[0-9]+$/.test(raw))
216
+ continue;
217
+ const bytes = Number(raw);
218
+ if (bytes > 0 && bytes < CGROUP_UNLIMITED_BYTES)
219
+ return bytes;
220
+ }
221
+ return null;
222
+ }
223
+ function parseRole(argv) {
224
+ const i = argv.indexOf('--role');
225
+ const value = i >= 0 ? argv[i + 1] : undefined;
226
+ return value === 'hub' || value === 'agent' ? value : null;
227
+ }
228
+ /** Run the CLI; returns the stdout text and the exit code. Never throws. */
229
+ function runLaunchCli(argv, deps) {
230
+ const role = parseRole(argv);
231
+ if (role === null)
232
+ return { stdout: '', code: 2 };
233
+ const dataDir = path.resolve(deps.env['CAMSTACK_DATA'] ?? 'camstack-data');
234
+ const legacyName = role === 'hub' ? 'CAMSTACK_HUB_HEAP_MB' : 'CAMSTACK_AGENT_HEAP_MB';
235
+ const plan = planLaunch({
236
+ role,
237
+ host: {
238
+ totalMemBytes: deps.totalMemBytes,
239
+ cgroupLimitBytes: readCgroupLimitBytes(deps.io, deps.cgroupFiles),
240
+ },
241
+ overrideMb: readHeapOverrideMb(dataDir, deps.io),
242
+ legacyEnv: deps.env[legacyName],
243
+ allocatorEnv: argv.includes('--allocator-env'),
244
+ });
245
+ return { stdout: renderLaunchPlan(plan), code: 0 };
246
+ }
247
+ if (require.main === module) {
248
+ const result = runLaunchCli(process.argv.slice(2), {
249
+ env: process.env,
250
+ io: realFs,
251
+ totalMemBytes: os.totalmem(),
252
+ });
253
+ process.stdout.write(result.stdout);
254
+ process.exitCode = result.code;
255
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.689",
3
+ "version": "1.2.691",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,7 +24,8 @@
24
24
  "./package.json": "./package.json",
25
25
  "./main.js": "./dist/main.js",
26
26
  "./launcher.js": "./dist/launcher.js",
27
- "./starter.js": "./dist/starter.js"
27
+ "./starter.js": "./dist/starter.js",
28
+ "./launch-args.js": "./dist/launch-args.js"
28
29
  },
29
30
  "scripts": {
30
31
  "build": "../../scripts/tsc7 -p tsconfig.build.json && tsup && node scripts/copy-closure-patches.mjs",
@@ -49,10 +50,10 @@
49
50
  ]
50
51
  },
51
52
  "dependencies": {
52
- "@camstack/addon-admin-ui": "1.2.517",
53
- "@camstack/addon-agent-ui": "1.2.292",
53
+ "@camstack/addon-admin-ui": "1.2.518",
54
+ "@camstack/addon-agent-ui": "1.2.293",
54
55
  "@camstack/addon-auth": "1.2.170",
55
- "@camstack/addon-benchmark": "1.2.291",
56
+ "@camstack/addon-benchmark": "1.2.292",
56
57
  "@camstack/addon-decoder-nodeav": "1.2.144",
57
58
  "@camstack/addon-notifiers": "1.2.623",
58
59
  "@camstack/addon-pipeline": "1.2.634",
@@ -60,9 +61,9 @@
60
61
  "@camstack/addon-post-analysis": "1.2.651",
61
62
  "@camstack/develop": "0.1.39",
62
63
  "@camstack/sdk": "1.2.154",
63
- "@camstack/system": "1.2.518",
64
- "@camstack/types": "1.2.444",
65
- "@camstack/ui-library": "1.2.350",
64
+ "@camstack/system": "1.2.519",
65
+ "@camstack/types": "1.2.445",
66
+ "@camstack/ui-library": "1.2.351",
66
67
  "@fastify/compress": "^9.0.0",
67
68
  "@fastify/cookie": "^11.0.2",
68
69
  "@fastify/cors": "^11.2.0",