@camstack/server 1.2.675 → 1.2.677

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.677] - 2026-10-28
11
+
12
+ ### Changed
13
+
14
+ - An agent now keeps its own server closure current: unpaired it follows the npm latest, paired it follows the hub's version, checked at boot and every 6 hours (turn off with CAMSTACK_AGENT_SELF_UPDATE=off).
15
+
16
+ ## [1.2.676] - 2026-10-28
17
+
18
+ ### Fixed
19
+
20
+ - An agent advertises itself on the network again, so the hub lists a new agent for pairing without typing its address
21
+ - An unpaired agent no longer crash-loops: it stays up to be paired and starts its addons once pairing completes
22
+
10
23
  ## [1.2.675] - 2026-10-28
11
24
 
12
25
  ### Changed
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideAgentBootMode = decideAgentBootMode;
4
+ function decideAgentBootMode(input) {
5
+ return input.paired ? 'full' : 'pairing-only';
6
+ }
@@ -50,9 +50,9 @@ function createAgentHubRegistration(opts) {
50
50
  opts.onCertificateRefused?.(err);
51
51
  },
52
52
  })
53
- .then(() => {
53
+ .then((ack) => {
54
54
  opts.log(`[register-node] the hub acked this node's capabilities: ${manifest.addons.length} addon(s), ${bindings} binding(s) (${reason})`);
55
- opts.onAcked?.();
55
+ opts.onAcked?.((0, system_1.hubServerVersionOf)(ack));
56
56
  })
57
57
  .catch(() => {
58
58
  // Aborted: shutting down.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideAgentSelfUpdate = decideAgentSelfUpdate;
4
+ exports.agentSelfUpdateEnabled = agentSelfUpdateEnabled;
5
+ exports.runAgentSelfUpdate = runAgentSelfUpdate;
6
+ /**
7
+ * An agent keeps its own `@camstack/server` closure current, with nobody driving it.
8
+ *
9
+ * Until now a closure reached an agent only when the HUB called `applyServerUpdate` on it. A
10
+ * freshly installed desktop agent boots the seed closure baked into the app and, being unpaired,
11
+ * had no hub to receive a fix from — not even the fix for its own unpaired boot (D857).
12
+ *
13
+ * The target version:
14
+ * - UNPAIRED: the npm `latest` of `@camstack/server` — there is no hub to follow;
15
+ * - PAIRED: the version the HUB runs (carried on the registration ack), never npm latest, so
16
+ * the cluster has no hub/agent skew.
17
+ *
18
+ * It reuses the one updater (`AgentUpdateService` → stage → `scheduleAgentRestart`, lifecycle =
19
+ * respawn, D5), never downgrades, and treats a failed read as "unknown", never as "up to date"
20
+ * (D49, D393): the reason is logged and nothing changes. The cadence is the update-check
21
+ * constant; there is no retry loop (D3).
22
+ *
23
+ * Off switch: `CAMSTACK_AGENT_SELF_UPDATE=off` (or `0`/`false`).
24
+ */
25
+ const index_js_1 = require("../server-root/index.js");
26
+ function skip(reason, failedRead = false) {
27
+ return { action: 'skip', reason, failedRead };
28
+ }
29
+ function decideAgentSelfUpdate(input) {
30
+ if (input.running === null)
31
+ return skip('the running version is unknown', true);
32
+ let target;
33
+ let source;
34
+ if (input.paired) {
35
+ if (input.hubVersion === null) {
36
+ return skip('paired, but the hub version is unknown (no registration ack carrying it yet, or an older hub)', true);
37
+ }
38
+ target = input.hubVersion;
39
+ source = 'hub';
40
+ }
41
+ else {
42
+ if (!input.npmLatest.ok) {
43
+ return skip(`the registry read failed: ${input.npmLatest.reason}`, true);
44
+ }
45
+ target = input.npmLatest.version;
46
+ source = 'npm-latest';
47
+ }
48
+ const order = (0, index_js_1.compareSemver)(target, input.running);
49
+ if (order === 0)
50
+ return skip(`already running ${target} (${source})`);
51
+ if (order < 0)
52
+ return skip(`target ${target} (${source}) is older than running ${input.running}: never a downgrade`);
53
+ if (input.pendingVersion === target)
54
+ return skip(`${target} is already staged, awaiting restart`);
55
+ return { action: 'stage', version: target, source };
56
+ }
57
+ function agentSelfUpdateEnabled(env) {
58
+ const raw = env['CAMSTACK_AGENT_SELF_UPDATE']?.trim().toLowerCase();
59
+ return !(raw === 'off' || raw === '0' || raw === 'false' || raw === 'no');
60
+ }
61
+ /** One self-update pass. Every decision is logged with the versions; it never throws. */
62
+ async function runAgentSelfUpdate(ports) {
63
+ const { log } = ports;
64
+ try {
65
+ if (!ports.enabled()) {
66
+ log.info('agent self-update: off (CAMSTACK_AGENT_SELF_UPDATE) — nothing checked');
67
+ return;
68
+ }
69
+ const paired = ports.isPaired();
70
+ const running = ports.runningVersion();
71
+ const hubVersion = ports.getHubServerVersion();
72
+ let npmLatest = { ok: false, reason: 'not needed' };
73
+ let pendingVersion = null;
74
+ if (!paired) {
75
+ const check = await ports.checkLatest();
76
+ pendingVersion = check.pendingVersion;
77
+ npmLatest =
78
+ check.error === null && check.latestVersion !== null
79
+ ? { ok: true, version: check.latestVersion }
80
+ : {
81
+ ok: false,
82
+ reason: check.error ?? 'the registry returned no version',
83
+ };
84
+ }
85
+ log.info(`agent self-update: checked (${paired ? 'paired' : 'unpaired'}) running=${running ?? 'unknown'} ` +
86
+ `hub=${hubVersion ?? 'unknown'} npmLatest=${npmLatest.ok ? npmLatest.version : 'n/a'}`);
87
+ const decision = decideAgentSelfUpdate({
88
+ paired,
89
+ running,
90
+ npmLatest,
91
+ hubVersion,
92
+ pendingVersion,
93
+ });
94
+ if (decision.action === 'skip') {
95
+ const line = `agent self-update: skip — ${decision.reason}`;
96
+ if (decision.failedRead)
97
+ log.warn(line);
98
+ else
99
+ log.info(line);
100
+ return;
101
+ }
102
+ log.info(`agent self-update: staging ${decision.version} (${decision.source}) over ${running ?? 'unknown'}`);
103
+ const result = await ports.apply(decision.version);
104
+ if (result.accepted) {
105
+ log.info(`agent self-update: applied ${decision.version} (${result.outcome}) — restarting to swap the closure`);
106
+ }
107
+ else {
108
+ log.warn(`agent self-update: ${decision.version} not applied (${result.outcome}): ${result.message}`);
109
+ }
110
+ }
111
+ catch (err) {
112
+ log.warn(`agent self-update: failed — ${err instanceof Error ? err.message : String(err)}; nothing changed`);
113
+ }
114
+ }
@@ -56,6 +56,9 @@ const agent_http_js_1 = require("./agent-http.js");
56
56
  const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
57
57
  const agent_reconnect_js_1 = require("./agent-reconnect.js");
58
58
  const serial_queue_js_1 = require("./serial-queue.js");
59
+ const agent_boot_mode_js_1 = require("./agent-boot-mode.js");
60
+ const agent_self_update_js_1 = require("./agent-self-update.js");
61
+ const update_check_scheduler_js_1 = require("../core/updates/update-check-scheduler.js");
59
62
  const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
60
63
  const agent_service_js_1 = require("./agent-service.js");
61
64
  const agent_hub_addon_update_js_1 = require("./agent-hub-addon-update.js");
@@ -305,6 +308,9 @@ async function startAgent(configPath) {
305
308
  // AbortController for the upward hub registration retry loop — cancelled on
306
309
  // agent shutdown so we don't leak the retry loop after stop().
307
310
  const registerAbortController = new AbortController();
311
+ // The hub's `@camstack/server` version from the last `registerNode` ack (agent self-update).
312
+ let hubServerVersion = null;
313
+ let selfUpdateOnFirstAck = null;
308
314
  /**
309
315
  * Build the manifest for the agent's OWN in-process addons (those loaded by
310
316
  * bootCoreAddons and loadDeployedAddons that ended up in `loadedAddons` WITH
@@ -394,10 +400,15 @@ async function startAgent(configPath) {
394
400
  log: (msg) => console.log(`[Agent] ${msg}`),
395
401
  // The hub acked the manifest — the agent reached its registered state.
396
402
  // Run the single-copy GC sweep once (first ack only; no-op after).
397
- onAcked: () => {
403
+ onAcked: (ackedHubServerVersion) => {
398
404
  if (hubConnectionStateOverride === 'certificate-rejected')
399
405
  hubConnectionStateOverride = null;
406
+ hubServerVersion = ackedHubServerVersion;
400
407
  confirmAgentBootHealthy();
408
+ // Paired: the first ack is the first moment the hub's version is known.
409
+ const firstAck = selfUpdateOnFirstAck;
410
+ selfUpdateOnFirstAck = null;
411
+ firstAck?.();
401
412
  },
402
413
  onCertificateRefused: () => {
403
414
  if (hubConnectionStateOverride === 'certificate-rejected')
@@ -900,6 +911,44 @@ async function startAgent(configPath) {
900
911
  leafFingerprint: (0, agent_pairing_store_js_1.pairedLeafFingerprint)(config.configPath),
901
912
  });
902
913
  };
914
+ const addonsBooted = (0, agent_boot_mode_js_1.decideAgentBootMode)({ paired: (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath) }) === 'full';
915
+ // ── Self-update (agent-self-update.ts): keep this closure current with nobody driving it.
916
+ // Unpaired -> npm latest, at boot; paired -> the hub's version, at the first registration ack.
917
+ // Then on the update-check cadence (a periodic check, no retry loop). Lifecycle = respawn (D5).
918
+ const selfUpdatePorts = {
919
+ enabled: () => (0, agent_self_update_js_1.agentSelfUpdateEnabled)(process.env),
920
+ isPaired: () => (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath),
921
+ getHubServerVersion: () => hubServerVersion,
922
+ runningVersion: () => agentUpdateService.getRunningRootPackage().version,
923
+ checkLatest: async () => {
924
+ const check = await agentUpdateService.checkServerUpdate();
925
+ return {
926
+ latestVersion: check.latestVersion,
927
+ error: check.error,
928
+ pendingVersion: check.pendingVersion ?? null,
929
+ };
930
+ },
931
+ apply: async (version) => {
932
+ const result = await agentUpdateService.applyServerUpdate({ version });
933
+ return {
934
+ accepted: result.accepted,
935
+ outcome: result.outcome ?? 'unknown',
936
+ message: result.message,
937
+ };
938
+ },
939
+ log: {
940
+ info: (message) => consoleLogger.info(message, { tags: { nodeId: config.nodeId } }),
941
+ warn: (message) => consoleLogger.warn(message, { tags: { nodeId: config.nodeId } }),
942
+ },
943
+ };
944
+ if ((0, agent_pairing_store_js_1.isAgentPaired)(config.configPath)) {
945
+ selfUpdateOnFirstAck = () => void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts);
946
+ }
947
+ else {
948
+ void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts);
949
+ }
950
+ const selfUpdateTimer = setInterval(() => void (0, agent_self_update_js_1.runAgentSelfUpdate)(selfUpdatePorts), update_check_scheduler_js_1.DEFAULT_UPDATE_CHECK_INTERVAL_SECONDS * 1000);
951
+ selfUpdateTimer.unref();
903
952
  const localPairing = (0, agent_pairing_local_js_1.createLocalPairingAnnouncer)(config.dataDir, consoleLogger);
904
953
  const pairing = new system_1.AgentPairing({
905
954
  nodeId: config.nodeId,
@@ -912,7 +961,16 @@ async function startAgent(configPath) {
912
961
  meta: { hubUrl: installed.hubUrl, hubAddress: installed.hubAddress },
913
962
  });
914
963
  // The completion is answered first; the wire is replaced right after.
915
- const timer = setTimeout(() => void reconnect().then(advertise, advertise), 400);
964
+ const timer = setTimeout(() => {
965
+ if (addonsBooted) {
966
+ void reconnect().then(advertise, advertise);
967
+ }
968
+ else {
969
+ // Booted unpaired: no addon was started (agent-boot-mode.ts). Relaunch (D5) so
970
+ // the core addons boot with the hub reachable.
971
+ (0, agent_update_service_js_1.scheduleAgentRestart)(consoleLogger, 'paired with the hub', config.dataDir);
972
+ }
973
+ }, 400);
916
974
  timer.unref?.();
917
975
  },
918
976
  log: (message, meta) => consoleLogger.info(`pairing: ${message}`, meta ? { meta } : undefined),
@@ -951,6 +1009,40 @@ async function startAgent(configPath) {
951
1009
  }).then((listener) => {
952
1010
  agentHttpListener = listener;
953
1011
  });
1012
+ // Graceful shutdown
1013
+ const shutdown = async () => {
1014
+ console.log('[Agent] Shutting down...');
1015
+ // Abort any in-flight upward hub registration retry loop.
1016
+ registerAbortController.abort();
1017
+ // Dispose the UDS event bridge to remove the parent-bus subscriber and
1018
+ // clear the child-event handler, preventing subscriber leaks on shutdown.
1019
+ udsEventBridgeDispose?.();
1020
+ udsEventBridgeDispose = null;
1021
+ for (const [, entry] of loadedAddons) {
1022
+ if (entry.addon?.shutdown) {
1023
+ try {
1024
+ await entry.addon.shutdown();
1025
+ }
1026
+ catch {
1027
+ /* */
1028
+ }
1029
+ }
1030
+ }
1031
+ // Withdraw the mDNS advertisement, then stop accepting new HTTP/HTTPS connections before
1032
+ // the broker goes away.
1033
+ await agentMdns.stop().catch(() => undefined);
1034
+ await agentHttpListener?.close().catch(() => undefined);
1035
+ await broker.stop();
1036
+ process.exit(0);
1037
+ };
1038
+ process.on('SIGTERM', shutdown);
1039
+ process.on('SIGINT', shutdown);
1040
+ // An unpaired agent has no hub to read settings from (D857): serve pairing only and
1041
+ // relaunch after the certificate is installed (D5). See agent-boot-mode.ts.
1042
+ if (!addonsBooted) {
1043
+ console.log(`[Agent] Node "${config.nodeId}" is unpaired - serving pairing only; addons boot after pairing`);
1044
+ return;
1045
+ }
954
1046
  // ── Phase 1: Load core infrastructure addons (in-process) ──
955
1047
  // storage + settings + metrics + hub-forwarder (log destination)
956
1048
  await bootCoreAddons(broker, config, capabilityRegistry, loadedAddons, loggerFactory);
@@ -1003,34 +1095,6 @@ async function startAgent(configPath) {
1003
1095
  }
1004
1096
  });
1005
1097
  console.log(`[Agent] Node "${config.nodeId}" (name="${config.name}") ready — ${loadedAddons.size} addon(s) loaded`);
1006
- // Graceful shutdown
1007
- const shutdown = async () => {
1008
- console.log('[Agent] Shutting down...');
1009
- // Abort any in-flight upward hub registration retry loop.
1010
- registerAbortController.abort();
1011
- // Dispose the UDS event bridge to remove the parent-bus subscriber and
1012
- // clear the child-event handler, preventing subscriber leaks on shutdown.
1013
- udsEventBridgeDispose?.();
1014
- udsEventBridgeDispose = null;
1015
- for (const [, entry] of loadedAddons) {
1016
- if (entry.addon?.shutdown) {
1017
- try {
1018
- await entry.addon.shutdown();
1019
- }
1020
- catch {
1021
- /* */
1022
- }
1023
- }
1024
- }
1025
- // Withdraw the mDNS advertisement, then stop accepting new HTTP/HTTPS connections before
1026
- // the broker goes away.
1027
- await agentMdns.stop().catch(() => undefined);
1028
- await agentHttpListener?.close().catch(() => undefined);
1029
- await broker.stop();
1030
- process.exit(0);
1031
- };
1032
- process.on('SIGTERM', shutdown);
1033
- process.on('SIGINT', shutdown);
1034
1098
  }
1035
1099
  // ---------------------------------------------------------------------------
1036
1100
  // Phase 1: Boot core addons (storage, settings, metrics — NO winston)
@@ -1217,7 +1281,10 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1217
1281
  try {
1218
1282
  await broker.call('$process.spawnRunner', {
1219
1283
  runnerId: groupId,
1220
- addons: addons.map((a) => ({ addonId: a.addonId, addonDir: a.addonDir })),
1284
+ addons: addons.map((a) => ({
1285
+ addonId: a.addonId,
1286
+ addonDir: a.addonDir,
1287
+ })),
1221
1288
  });
1222
1289
  // Shared with the reload path (`ensureGroupRunner`) so the boot and
1223
1290
  // reload registrations can never drift — the drift was the in-process
@@ -0,0 +1,68 @@
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.readServerVersionFrom = readServerVersionFrom;
37
+ exports.readHubServerVersion = readHubServerVersion;
38
+ /**
39
+ * The `@camstack/server` version this hub runs, carried on every `$hub.registerNode` ack so an
40
+ * agent can align its own closure to it (agent self-update). Read once from the running
41
+ * package.json (`dist/core/moleculer/` -> `../../../package.json`); null when it cannot be read —
42
+ * the ack then simply omits it and the agent says "hub version unknown" instead of guessing.
43
+ */
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const index_js_1 = require("../../server-root/index.js");
47
+ function readServerVersionFrom(packageJsonPath) {
48
+ try {
49
+ const raw = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
50
+ if (typeof raw !== 'object' || raw === null)
51
+ return null;
52
+ const name = Reflect.get(raw, 'name');
53
+ const version = Reflect.get(raw, 'version');
54
+ if (name !== index_js_1.SERVER_ROOT_PACKAGE_NAME)
55
+ return null;
56
+ return typeof version === 'string' && version.length > 0 ? version : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ let cached;
63
+ function readHubServerVersion() {
64
+ if (cached === undefined) {
65
+ cached = readServerVersionFrom(path.resolve(__dirname, '..', '..', '..', 'package.json'));
66
+ }
67
+ return cached;
68
+ }
@@ -11,6 +11,7 @@ const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
11
11
  const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
12
12
  const agent_registration_request_js_1 = require("./agent-registration-request.js");
13
13
  const hub_mesh_guard_js_1 = require("./hub-mesh-guard.js");
14
+ const hub_server_version_js_1 = require("./hub-server-version.js");
14
15
  const cap_call_fn_js_1 = require("./cap-call-fn.js");
15
16
  const cap_route_authority_js_1 = require("./cap-route-authority.js");
16
17
  class MoleculerService {
@@ -270,6 +271,8 @@ class MoleculerService {
270
271
  onUnregisterNode: (nodeId) => {
271
272
  this.removeNodeFromRegistry(nodeId);
272
273
  },
274
+ // The ack carries the hub's closure version so an agent aligns to it (agent self-update).
275
+ getServerVersion: () => (0, hub_server_version_js_1.readHubServerVersion)(),
273
276
  // D857: a node registers only itself, and only with a certificate identity.
274
277
  meshIdentity: this.meshGuard.gate,
275
278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.675",
3
+ "version": "1.2.677",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  "@camstack/addon-post-analysis": "1.2.649",
61
61
  "@camstack/develop": "0.1.38",
62
62
  "@camstack/sdk": "1.2.154",
63
- "@camstack/system": "1.2.506",
63
+ "@camstack/system": "1.2.508",
64
64
  "@camstack/types": "1.2.436",
65
65
  "@camstack/ui-library": "1.2.345",
66
66
  "@fastify/compress": "^9.0.0",