@camstack/server 1.2.688 → 1.2.690

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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.690] - 2026-10-29
11
+
12
+ ### Changed
13
+
14
+ - 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
15
+
16
+ ## [1.2.689] - 2026-10-29
17
+
18
+ ### Added
19
+
20
+ - A node's free memory comes from the metrics provider instead of os.freemem(), which undercounts on macOS
21
+ - device-export gains optional methods to list, expose and unexpose actions and notification targets beside devices.
22
+ - Forget node now works on an online node too: its certificates are revoked, its connection is cut and calls pinned to it fail at once.
23
+ - Generated routers for the pet-fountain capability
24
+ - Agents report their own process memory in the topology (the core process showed 0), and an unreadable memory shows as unknown
25
+
26
+ ### Changed
27
+
28
+ - The agent now reports its own [mem] agent-main heartbeat and reclaims stranded memory, as the hub does
29
+ - The hub tells its addon runners which telemetry categories anyone consumes, so unused ones stop crossing the process boundary
30
+ - 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
31
+ - A node's heap-ceiling override can be read and set through server-management (applies at the next restart)
32
+
10
33
  ## [1.2.688] - 2026-10-29
11
34
 
12
35
  ### 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
+ }
@@ -240,7 +240,8 @@ function createAgentService(deps) {
240
240
  const { broker } = ctx;
241
241
  const cpus = os.cpus();
242
242
  let cpuPercent = 0;
243
- let memoryPercent = 0;
243
+ let memoryPercent = null;
244
+ let freeMemoryBytes = null;
244
245
  const metrics = deps.getMetricsProvider?.();
245
246
  if (metrics) {
246
247
  // Bounded so $agent.status never hangs (and drops the node from
@@ -249,6 +250,7 @@ function createAgentService(deps) {
249
250
  if (snapshot) {
250
251
  cpuPercent = snapshot.cpu.total;
251
252
  memoryPercent = snapshot.memory.percent;
253
+ freeMemoryBytes = snapshot.memory.availableBytes;
252
254
  }
253
255
  }
254
256
  const mem = process.memoryUsage();
@@ -262,9 +264,12 @@ function createAgentService(deps) {
262
264
  cpuCores: cpus.length,
263
265
  cpuModel: cpus[0]?.model,
264
266
  totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
265
- freeMemoryMB: Math.round(os.freemem() / 1024 / 1024),
267
+ freeMemoryMB: freeMemoryBytes === null ? null : Math.round(freeMemoryBytes / 1024 / 1024),
266
268
  cpuPercent,
267
269
  memoryPercent,
270
+ // The agent's own (core) process, so a topology row can attribute
271
+ // the largest process on the node instead of showing 0.
272
+ processRssBytes: process.memoryUsage().rss,
268
273
  uptime: os.uptime(),
269
274
  // The agent *process* itself (distinct from the host `uptime` /
270
275
  // memory above) — surfaced so the UI can show how long THIS agent
@@ -298,7 +303,7 @@ function createAgentService(deps) {
298
303
  handler: async (ctx) => {
299
304
  const { broker } = ctx;
300
305
  let cpuPercent = 0;
301
- let memoryPercent = 0;
306
+ let memoryPercent = null;
302
307
  const metrics = deps.getMetricsProvider?.();
303
308
  if (metrics) {
304
309
  try {
@@ -76,6 +76,7 @@ const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.
76
76
  // entries flow through it (console output + hub forwarding).
77
77
  // ---------------------------------------------------------------------------
78
78
  const system_2 = require("@camstack/system");
79
+ const agent_heap_watch_js_1 = require("./agent-heap-watch.js");
79
80
  /** A reconnect (stop + rebuild + start) that takes longer than this is abandoned. */
80
81
  const RECONNECT_DEADLINE_MS = 90_000;
81
82
  /** How often the agent re-evaluates whether to ask its hub "am I still a member?" (D857). */
@@ -97,6 +98,11 @@ const DATA_NODEID_CAPS = new Set(types_2.ALL_CAPABILITY_DEFINITIONS.filter((c) =
97
98
  // Main
98
99
  // ---------------------------------------------------------------------------
99
100
  async function startAgent(configPath) {
101
+ // The agent root's own memory: `[mem] agent-main` heartbeat + the stranded-page
102
+ // reclaim hub-main has had since 2026-08. Started first so a slow boot is on the
103
+ // record; lines reach the hub through the same LogManager the addons use.
104
+ const heapWatchLogger = agentLogManager.createLogger().withTags({ addonId: 'heap-watch' });
105
+ (0, agent_heap_watch_js_1.startAgentHeapWatch)(heapWatchLogger);
100
106
  const config = (0, agent_config_js_1.loadAgentConfig)(configPath);
101
107
  // Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
102
108
  // no longer need a second, redundant env var set by hand (the cross-node
@@ -480,7 +486,7 @@ async function startAgent(configPath) {
480
486
  dataDir: config.dataDir,
481
487
  runnerBootCheck: () => agentRunnerBootCheck?.current(),
482
488
  });
483
- capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
489
+ capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService, config.dataDir));
484
490
  // The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
485
491
  // hub-issued HTTPS leaf hot-swapped into the sniffing listener. The listener is
486
492
  // created later in boot, so it is read through a getter.
@@ -349,7 +349,9 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
349
349
  name: `${node.info.id} (core)`,
350
350
  state: 'running',
351
351
  cpuPercent: node.status?.cpuPercent ?? 0,
352
- memoryRss: 0,
352
+ // The agent's own process RSS from $agent.status; 0 only for an agent
353
+ // that predates the field.
354
+ memoryRss: node.status?.processRssBytes ?? 0,
353
355
  uptimeSeconds: Math.floor((Date.now() - node.connectedSince) / 1000),
354
356
  services: mainProcessServices,
355
357
  };
@@ -414,7 +416,7 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
414
416
  isHub: node.isHub,
415
417
  isOnline: node.isOnline !== false,
416
418
  cpuPercent: node.status?.cpuPercent ?? 0,
417
- memoryPercent: node.status?.memoryPercent ?? 0,
419
+ memoryPercent: node.status?.memoryPercent ?? null,
418
420
  uptime: Date.now() - node.connectedSince,
419
421
  // Fix (was hardcoded `new Date().toISOString()` — always "now", a bug
420
422
  // even for online nodes): report the persisted `lastActive` when we have
@@ -456,7 +458,7 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
456
458
  isHub: persisted.isHub,
457
459
  isOnline: false,
458
460
  cpuPercent: 0,
459
- memoryPercent: 0,
461
+ memoryPercent: null,
460
462
  uptime: 0,
461
463
  lastSeen: new Date(persisted.lastActive).toISOString(),
462
464
  localIps: [...persisted.localIps],
@@ -69,9 +69,9 @@ function createClusterNodesRouter(agentRegistry) {
69
69
  await agentRegistry.repairNodeAddons(input.nodeId);
70
70
  return { success: true };
71
71
  }),
72
- // Purge one node's persisted offline history. Pairs with
73
- // `pipelineOrchestrator.removeAgentSettings` on the frontend to fully
74
- // forget an offline node (history row + per-node pipeline assignments).
72
+ // Forget a node, online or offline: revoke its certificates, cut it from routing, purge its
73
+ // persisted history. Pairs with `pipelineOrchestrator.removeAgentSettings` on the frontend
74
+ // (per-node pipeline assignments).
75
75
  forgetNode: trpc_middleware_js_1.adminProcedure
76
76
  .input(ForgetNodeInputSchema)
77
77
  .output(ForgetNodeResultSchema)
@@ -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
  }
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 188 Skipped (legacy): 3
5
+ // Mounted: 189 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -231,6 +231,8 @@ function mountAllCaps(services) {
231
231
  ...first,
232
232
  listSupportedDeviceKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listSupportedDeviceKinds'),
233
233
  listExposedDevices: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listExposedDevices'),
234
+ listSupportedEntityKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listSupportedEntityKinds'),
235
+ listExposedEntities: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listExposedEntities'),
234
236
  };
235
237
  }, remoteCapProxy),
236
238
  deviceExtension: (0, generated_cap_routers_js_1.createCapRouter_deviceExtension)((_ctx) => (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, 'device-extension'), remoteCapProxy),
@@ -501,6 +503,7 @@ function mountAllCaps(services) {
501
503
  osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
502
504
  osdManager: (0, generated_cap_routers_js_1.createCapRouter_osdManager)((_ctx) => reg?.getSingleton('osd-manager') ?? null, remoteCapProxy),
503
505
  petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
506
+ petFountain: (0, generated_cap_routers_js_1.createCapRouter_petFountain)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-fountain'), remoteCapProxy),
504
507
  pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pipeline-analytics'), remoteCapProxy),
505
508
  pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??
506
509
  null, remoteCapProxy),