@camstack/server 1.2.588 → 1.2.589

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.
@@ -258,6 +258,9 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
258
258
  applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
259
259
  env: options.env,
260
260
  now: options.now,
261
+ ...(options.runnerBootCheck !== undefined
262
+ ? { runnerBootCheck: options.runnerBootCheck }
263
+ : {}),
261
264
  });
262
265
  this.nodeId = nodeId;
263
266
  this.restartAnnouncer = restartAnnouncer;
@@ -35,10 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.startAgent = startAgent;
37
37
  const fs = __importStar(require("node:fs"));
38
+ const runner_boot_check_js_1 = require("../core/runner-health/runner-boot-check.js");
38
39
  const path = __importStar(require("node:path"));
39
40
  const system_1 = require("@camstack/system");
40
- // Capability definitions — must be declared before addon registerProvider() calls
41
41
  const types_1 = require("@camstack/types");
42
+ // Capability definitions — must be declared before addon registerProvider() calls
43
+ const types_2 = require("@camstack/types");
42
44
  const server_management_provider_js_1 = require("../api/core/server-management.provider.js");
43
45
  const package_dir_utils_js_1 = require("../core/addon/package-dir-utils.js");
44
46
  const single_copy_cleanup_runner_js_1 = require("../single-copy-cleanup-runner.js");
@@ -83,7 +85,7 @@ const agentLogManager = new system_2.LogManager(5000);
83
85
  * the hub singleton (which reads `nodeId` as data) instead of being pinned to
84
86
  * a node with no provider (dead broker fallback → ServiceNotFoundError).
85
87
  */
86
- const DATA_NODEID_CAPS = new Set(types_1.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.nodeIdMode === 'data').map((c) => c.name));
88
+ const DATA_NODEID_CAPS = new Set(types_2.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.nodeIdMode === 'data').map((c) => c.name));
87
89
  // ---------------------------------------------------------------------------
88
90
  // Main
89
91
  // ---------------------------------------------------------------------------
@@ -417,33 +419,33 @@ async function startAgent(configPath) {
417
419
  const capabilityRegistry = new system_1.CapabilityRegistry(consoleLogger);
418
420
  // Declare all capabilities the agent uses — required before registerProvider()
419
421
  const agentCapabilities = [
420
- types_1.storageCapability,
421
- types_1.storageProviderCapability,
422
+ types_2.storageCapability,
423
+ types_2.storageProviderCapability,
422
424
  // The path browser for "add a storage location on this node" — the agent
423
425
  // registers the provider since day one, but without DECLARING the cap
424
426
  // here the hub's manifest never learns of it and node-routed browse
425
427
  // answers "not available on node X" (2026-07-30 fix).
426
- types_1.filesystemBrowseCapability,
428
+ types_2.filesystemBrowseCapability,
427
429
  // The agent runs the same two-addon data topology as the hub (D44):
428
430
  // `settings-store` is the door served by storage-orchestrator,
429
431
  // `data-store-provider` is the engine sqlite-settings registers. BOTH
430
432
  // must be declared here — this array is hand-curated and nothing else
431
433
  // teaches an agent a capability, so omitting the engine leaves the
432
434
  // agent's door with nothing behind it.
433
- types_1.settingsStoreCapability,
434
- types_1.dataStoreProviderCapability,
435
- types_1.logDestinationCapability,
436
- types_1.metricsProviderCapability,
437
- types_1.decoderCapability,
438
- types_1.motionDetectionCapability,
439
- types_1.pipelineExecutorCapability,
440
- types_1.pipelineRunnerCapability,
441
- types_1.audioAnalyzerCapability,
442
- types_1.platformProbeCapability,
443
- types_1.serverManagementCapability,
435
+ types_2.settingsStoreCapability,
436
+ types_2.dataStoreProviderCapability,
437
+ types_2.logDestinationCapability,
438
+ types_2.metricsProviderCapability,
439
+ types_2.decoderCapability,
440
+ types_2.motionDetectionCapability,
441
+ types_2.pipelineExecutorCapability,
442
+ types_2.pipelineRunnerCapability,
443
+ types_2.audioAnalyzerCapability,
444
+ types_2.platformProbeCapability,
445
+ types_2.serverManagementCapability,
444
446
  // Certificates of THIS node, called by the hub (D856): mesh leaf renewal and the
445
447
  // hub-issued HTTPS leaf. Provided by the runtime, like `server-management`.
446
- types_1.nodeCertificatesCapability,
448
+ types_2.nodeCertificatesCapability,
447
449
  ];
448
450
  for (const cap of agentCapabilities) {
449
451
  capabilityRegistry.declareCapability(cap);
@@ -455,9 +457,14 @@ async function startAgent(configPath) {
455
457
  // `agent-runtime` addonId (no addon owns it); `buildAgentOwnManifest`
456
458
  // appends the matching manifest entry so the hub can route
457
459
  // nodeId-pinned `server-management` calls here.
460
+ // D877: the agent's runner boot check. Only the hub plans which addons an
461
+ // agent runs, so the agent compares what its process service holds (a runner
462
+ // that failed or refused is named; one that never spawned is not knowable here).
463
+ let agentRunnerBootCheck = null;
458
464
  const agentUpdateService = new agent_update_service_js_1.AgentUpdateService({
459
465
  logger: consoleLogger,
460
466
  dataDir: config.dataDir,
467
+ runnerBootCheck: () => agentRunnerBootCheck?.current(),
461
468
  });
462
469
  capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
463
470
  // The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
@@ -781,6 +788,19 @@ async function startAgent(configPath) {
781
788
  // discovery resolve (or time out cleanly via the read-blob fallback)
782
789
  // so agent boot completes instead of hanging on the first infra addon.
783
790
  await broker.start();
791
+ agentRunnerBootCheck = new runner_boot_check_js_1.RunnerBootCheckService({
792
+ logger: consoleLogger,
793
+ nodeId: config.nodeId,
794
+ planned: () => null,
795
+ listProcesses: async () => {
796
+ // Untyped Moleculer boundary; the process service owns the row shape.
797
+ const rows = await broker.call('$process.list', {});
798
+ return rows;
799
+ },
800
+ });
801
+ agentRunnerBootCheck.scheduleAfterBoot();
802
+ // Re-evaluate a verdict that still had runners on their way, on each runner state change.
803
+ (0, system_1.getBrokerEventBus)(broker).subscribe({ category: types_1.EventCategory.ProcessStateChanged }, () => agentRunnerBootCheck?.noteRunnerChange());
784
804
  // C2: wire the UDS ↔ Moleculer event bridge so events emitted by UDS
785
805
  // children fan to siblings and reach the cluster, and cluster / parent-
786
806
  // local events propagate to every UDS child. Inert when agentUdsRegistry
@@ -900,7 +920,7 @@ async function startAgent(configPath) {
900
920
  (0, agent_pairing_store_js_1.forgetPairing)(config.configPath, config.dataDir);
901
921
  pairing.noteUnpaired();
902
922
  hubConnectionStateOverride = null;
903
- await reconnect().catch((err) => consoleLogger.error(`reconnect after unpairing failed: ${(0, types_1.errMsg)(err)}`));
923
+ await reconnect().catch((err) => consoleLogger.error(`reconnect after unpairing failed: ${(0, types_2.errMsg)(err)}`));
904
924
  advertise();
905
925
  };
906
926
  advertise();
@@ -1041,7 +1061,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
1041
1061
  await loader.loadFromAddonDir(dir);
1042
1062
  }
1043
1063
  catch (err) {
1044
- console.warn(`[Agent] Failed to scan ${dir}: ${(0, types_1.errMsg)(err)}`);
1064
+ console.warn(`[Agent] Failed to scan ${dir}: ${(0, types_2.errMsg)(err)}`);
1045
1065
  }
1046
1066
  }
1047
1067
  // Resolution is per-CAPABILITY (the guard names every missing required
@@ -1094,7 +1114,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
1094
1114
  // configured for type addons-data" → fatal crash-loop.
1095
1115
  listStorageLocationDeclarations: () => loader.listStorageLocationDeclarations(),
1096
1116
  });
1097
- const initResult = (0, types_1.normalizeAddonInitResult)(await instance.initialize(context));
1117
+ const initResult = (0, types_2.normalizeAddonInitResult)(await instance.initialize(context));
1098
1118
  for (const reg of initResult?.providers ?? []) {
1099
1119
  const capName = reg.capability.name;
1100
1120
  registry.registerProvider(capName, addonId, reg.provider);
@@ -1112,7 +1132,7 @@ async function bootCoreAddons(broker, config, registry, loadedAddons, loggerFact
1112
1132
  console.log(`[Agent] Core addon "${addonId}" initialized`);
1113
1133
  }
1114
1134
  catch (err) {
1115
- const msg = (0, types_1.errMsg)(err);
1135
+ const msg = (0, types_2.errMsg)(err);
1116
1136
  console.error(`[Agent] Failed to initialize core addon "${addonId}": ${msg}`);
1117
1137
  if (step.required) {
1118
1138
  throw new Error(`Required infrastructure addon "${addonId}" failed: ${msg}`, { cause: err });
@@ -1155,7 +1175,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1155
1175
  await loader.loadFromAddonDir(dir);
1156
1176
  }
1157
1177
  catch (err) {
1158
- console.warn(`[Agent] Skipping ${dir}: ${(0, types_1.errMsg)(err)}`);
1178
+ console.warn(`[Agent] Skipping ${dir}: ${(0, types_2.errMsg)(err)}`);
1159
1179
  continue;
1160
1180
  }
1161
1181
  dirToLoader.set(dir, loader);
@@ -1164,11 +1184,11 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1164
1184
  continue;
1165
1185
  if (registered.declaration.execution === undefined)
1166
1186
  continue;
1167
- const placement = (0, types_1.resolveAddonPlacement)(registered.declaration);
1187
+ const placement = (0, types_2.resolveAddonPlacement)(registered.declaration);
1168
1188
  if (placement === 'hub-only')
1169
1189
  continue;
1170
1190
  allGroupCandidates.push({
1171
- groupId: (0, types_1.resolveRunnerId)(registered.declaration, registered.declaration.id),
1191
+ groupId: (0, types_2.resolveRunnerId)(registered.declaration, registered.declaration.id),
1172
1192
  addonId: registered.declaration.id,
1173
1193
  addonDir: dir,
1174
1194
  version: registered.declaration.version ?? '0.0.0',
@@ -1216,7 +1236,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1216
1236
  const addonId = registered.declaration.id;
1217
1237
  if (loadedAddons.has(addonId))
1218
1238
  continue;
1219
- if (!(0, types_1.isDeployableToAgent)(registered.declaration))
1239
+ if (!(0, types_2.isDeployableToAgent)(registered.declaration))
1220
1240
  continue;
1221
1241
  console.warn(`[Agent] Addon "${addonId}" is deployable but missing from any spawned group — verify package.json execution field`);
1222
1242
  }
@@ -1250,11 +1270,11 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
1250
1270
  const addonId = decl.id;
1251
1271
  if (loadedAddons.has(addonId))
1252
1272
  continue;
1253
- if (!(0, types_1.isDeployableToAgent)(decl))
1273
+ if (!(0, types_2.isDeployableToAgent)(decl))
1254
1274
  continue;
1255
1275
  if ((0, agent_group_runner_js_1.isGroupRunnerAddon)(decl)) {
1256
1276
  groupCandidates.push({
1257
- groupId: (0, types_1.resolveRunnerId)(decl, addonId),
1277
+ groupId: (0, types_2.resolveRunnerId)(decl, addonId),
1258
1278
  addonId,
1259
1279
  addonDir: dir,
1260
1280
  version: decl.version ?? '0.0.0',
@@ -1316,14 +1336,14 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
1316
1336
  await loader.loadFromAddonDir(dir);
1317
1337
  }
1318
1338
  catch (err) {
1319
- console.warn(`[Agent] Skipping ${dir}: ${(0, types_1.errMsg)(err)}`);
1339
+ console.warn(`[Agent] Skipping ${dir}: ${(0, types_2.errMsg)(err)}`);
1320
1340
  continue;
1321
1341
  }
1322
1342
  for (const registered of loader.listAddons()) {
1323
1343
  const addonId = registered.declaration.id;
1324
1344
  if (loadedAddons.has(addonId))
1325
1345
  continue;
1326
- if (!(0, types_1.isDeployableToAgent)(registered.declaration))
1346
+ if (!(0, types_2.isDeployableToAgent)(registered.declaration))
1327
1347
  continue;
1328
1348
  // Group-runner addons are dispatched to subprocesses by the caller.
1329
1349
  if ((0, agent_group_runner_js_1.isGroupRunnerAddon)(registered.declaration))
@@ -1345,7 +1365,7 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
1345
1365
  console.log(`[Agent] Deployed addon "${addonId}" loaded in-process`);
1346
1366
  }
1347
1367
  catch (err) {
1348
- const msg = (0, types_1.errMsg)(err);
1368
+ const msg = (0, types_2.errMsg)(err);
1349
1369
  console.error(`[Agent] Failed to load deployed addon "${addonId}": ${msg}`);
1350
1370
  loadedAddons.set(addonId, { id: addonId, status: 'error' });
1351
1371
  }
@@ -586,10 +586,11 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
586
586
  // tab can render the same shape regardless of whether the target is
587
587
  // hub or agent.
588
588
  if (input.nodeId === 'hub') {
589
+ await addonRegistry?.refreshRunnerStates();
589
590
  const rows = addonRegistry?.listAddons() ?? [];
590
591
  return rows.map((r) => ({
591
592
  id: r.manifest.id,
592
- status: r.process?.state ?? 'running',
593
+ status: r.process?.state ?? 'unknown',
593
594
  version: r.manifest.packageVersion,
594
595
  packageName: r.manifest.packageName,
595
596
  }));
@@ -1477,6 +1478,8 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentReg
1477
1478
  for (const h of ar.getAddonHealthSnapshot()) {
1478
1479
  healthByPackage.set(h.packageName, h);
1479
1480
  }
1481
+ // Real runner states (D877) - never the literal `running` the list used to carry.
1482
+ await ar.refreshRunnerStates();
1480
1483
  const hubList = ar.listAllAddons().map((item) => ({
1481
1484
  ...item,
1482
1485
  hasBackup: rollbackable.has(item.manifest.packageName),
@@ -1506,6 +1509,8 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentReg
1506
1509
  },
1507
1510
  hasBackup: false,
1508
1511
  health: null,
1512
+ // The hub's runner state is not this agent's: report none rather than a borrowed one.
1513
+ process: undefined,
1509
1514
  },
1510
1515
  ];
1511
1516
  });
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.buildHubHealth = buildHubHealth;
24
24
  exports.isAuthorizedHealthRequest = isAuthorizedHealthRequest;
25
25
  exports.registerHealthRoutes = registerHealthRoutes;
26
+ const runner_boot_check_js_1 = require("../../core/runner-health/runner-boot-check.js");
26
27
  const session_cookie_js_1 = require("../../auth/session-cookie.js");
27
28
  const AGENT_HEALTH_TIMEOUT_MS = 3_000;
28
29
  function nowIso() {
@@ -35,6 +36,18 @@ async function buildHubHealth(deps, proc = process) {
35
36
  const offlineNodes = remote.filter((n) => !n.isOnline);
36
37
  const total = remote.length;
37
38
  const online = total - offlineNodes.length;
39
+ const check = deps.runnerBootCheck?.();
40
+ const runners = check === undefined
41
+ ? undefined
42
+ : {
43
+ degraded: (0, runner_boot_check_js_1.isRunnerBootCheckDegraded)(check),
44
+ expected: check.expected,
45
+ running: check.running,
46
+ failed: check.failed.length,
47
+ refused: check.refused.length,
48
+ missing: check.missing.length,
49
+ checkedAtMs: check.checkedAtMs,
50
+ };
38
51
  const memUsage = proc.memoryUsage();
39
52
  const totalMem = memUsage.heapTotal + memUsage.external + memUsage.arrayBuffers;
40
53
  const memoryPercent = totalMem > 0 ? Math.round((memUsage.heapUsed / totalMem) * 100) : 0;
@@ -55,6 +68,7 @@ async function buildHubHealth(deps, proc = process) {
55
68
  cpuPercent: 0,
56
69
  memoryPercent,
57
70
  checkedAt: nowIso(),
71
+ ...(runners !== undefined ? { runners } : {}),
58
72
  };
59
73
  }
60
74
  async function fetchAgentHealth(deps, nodeId) {
@@ -4995,6 +4995,14 @@ function createCapRouter_llm(getProvider, _createRemoteProxy) {
4995
4995
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4996
4996
  return p.upsertProfile(input);
4997
4997
  }),
4998
+ migrateRawSecrets: trpc_middleware_js_1.adminProcedure
4999
+ .input(types_67.llmCapability.methods.migrateRawSecrets.input.loose())
5000
+ .output(types_67.llmCapability.methods.migrateRawSecrets.output)
5001
+ .mutation(async ({ input, ctx }) => {
5002
+ const p = requireCapProvider('llm', () => getProvider(ctx));
5003
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5004
+ return p.migrateRawSecrets(input);
5005
+ }),
4998
5006
  deleteProfile: trpc_middleware_js_1.adminProcedure
4999
5007
  .input(types_67.llmCapability.methods.deleteProfile.input.loose())
5000
5008
  .output(types_67.llmCapability.methods.deleteProfile.output)
@@ -11971,6 +11979,15 @@ function createCapRouter_userManagement(getProvider, createRemoteProxy) {
11971
11979
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
11972
11980
  return p.getTotpStatus(methodInput);
11973
11981
  }),
11982
+ migrateRawSecrets: trpc_middleware_js_1.adminProcedure
11983
+ .input(types_138.userManagementCapability.methods.migrateRawSecrets.input.loose())
11984
+ .output(types_138.userManagementCapability.methods.migrateRawSecrets.output)
11985
+ .mutation(async ({ input, ctx }) => {
11986
+ const { nodeId, ...methodInput } = input;
11987
+ const p = resolveProvider('user-management', nodeId, () => getProvider(ctx), createRemoteProxy);
11988
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
11989
+ return p.migrateRawSecrets(methodInput);
11990
+ }),
11974
11991
  verifyTotp: trpc_middleware_js_1.protectedProcedure
11975
11992
  .input(types_138.userManagementCapability.methods.verifyTotp.input.loose())
11976
11993
  .output(types_138.userManagementCapability.methods.verifyTotp.output)
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEVER_LOADED_PROCESS = void 0;
4
+ exports.describeAddonProcess = describeAddonProcess;
5
+ function describeAddonProcess(input) {
6
+ if (!input.forked) {
7
+ // Hosted by hub-main itself: its state is hub-main's, which is running if this code is.
8
+ return input.initialized
9
+ ? { pid: input.hubPid, mode: 'in-process', state: 'running' }
10
+ : undefined;
11
+ }
12
+ if (input.runners === null)
13
+ return { mode: 'forked', state: 'unknown' };
14
+ const row = input.runners.get(input.runnerId);
15
+ if (row === undefined)
16
+ return { mode: 'forked', state: 'missing' };
17
+ return {
18
+ pid: row.pid,
19
+ mode: 'forked',
20
+ state: row.state,
21
+ ...(row.refusal !== undefined ? { reason: row.refusal.reason } : {}),
22
+ };
23
+ }
24
+ /**
25
+ * A package on disk that never loaded: there is no process, and the card must
26
+ * show the diagnostic state distinctly (`scanFailedToLoadPackages`).
27
+ */
28
+ exports.NEVER_LOADED_PROCESS = { mode: 'in-process', state: 'failed' };
@@ -39,6 +39,7 @@ exports.shouldEvictMissingOnDisk = shouldEvictMissingOnDisk;
39
39
  exports.addonUpdatePayload = addonUpdatePayload;
40
40
  exports.parseDataPlaneEndpoints = parseDataPlaneEndpoints;
41
41
  exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
42
+ const addon_process_view_js_1 = require("./addon-process-view.js");
42
43
  const node_crypto_1 = require("node:crypto");
43
44
  const fs = __importStar(require("node:fs"));
44
45
  const os = __importStar(require("node:os"));
@@ -2045,17 +2046,16 @@ class AddonRegistryService {
2045
2046
  return this.isPackageProtected(entry.packageName);
2046
2047
  }
2047
2048
  listAddons() {
2048
- // Build process info map from in-memory data (sync, no stats)
2049
- const processMap = new Map();
2050
- for (const [id, entry] of this.addonEntries) {
2051
- if (entry.initialized) {
2052
- processMap.set(`addon:${id}`, {
2053
- pid: process.pid,
2054
- mode: 'in-process',
2055
- state: 'running',
2056
- });
2057
- }
2058
- }
2049
+ // The real process-service state of each addon's runner (D877) - the last
2050
+ // `refreshRunnerStates()` answer; `unknown` before the first one.
2051
+ const runnerStates = this.runnerStates;
2052
+ const processFor = (id, entry) => (0, addon_process_view_js_1.describeAddonProcess)({
2053
+ forked: this.runsInOwnRunner(entry),
2054
+ initialized: entry.initialized === true,
2055
+ runnerId: entry.declaration ? (0, types_1.resolveRunnerId)(entry.declaration, id) : id,
2056
+ runners: runnerStates,
2057
+ hubPid: process.pid,
2058
+ });
2059
2059
  const live = Array.from(this.addonEntries.values())
2060
2060
  .filter((entry) => entry.addon?.manifest?.id)
2061
2061
  .map((entry) => {
@@ -2098,7 +2098,7 @@ class AddonRegistryService {
2098
2098
  declaration: entry.declaration,
2099
2099
  source: entry.source,
2100
2100
  installSource,
2101
- process: processMap.get(`addon:${entry.addon.manifest.id}`),
2101
+ process: processFor(entry.addon.manifest.id, entry),
2102
2102
  };
2103
2103
  });
2104
2104
  // Surface packages that exist on disk but failed to load — typical
@@ -2194,7 +2194,7 @@ class AddonRegistryService {
2194
2194
  declaration: decl,
2195
2195
  source: 'installed',
2196
2196
  ...(installSource ? { installSource } : {}),
2197
- process: { mode: 'in-process', state: 'failed' },
2197
+ process: addon_process_view_js_1.NEVER_LOADED_PROCESS,
2198
2198
  });
2199
2199
  }
2200
2200
  };
@@ -2228,38 +2228,6 @@ class AddonRegistryService {
2228
2228
  listAllAddons() {
2229
2229
  return this.listAddons();
2230
2230
  }
2231
- /**
2232
- * List all addon processes (in-process addons)
2233
- * as ManagedProcessStatus-compatible entries for the process tree.
2234
- */
2235
- async listAddonProcesses() {
2236
- const result = [];
2237
- // Collect hub PID and fetch stats
2238
- const { getPidStats } = await Promise.resolve().then(() => __importStar(require('@camstack/system')));
2239
- const pidStats = await getPidStats([process.pid]);
2240
- // In-process addons
2241
- const hubStats = pidStats.get(process.pid);
2242
- for (const [id, entry] of this.addonEntries) {
2243
- if (entry.initialized) {
2244
- result.push({
2245
- id: `addon:${id}`,
2246
- label: `Addon: ${id} (in-process)`,
2247
- state: 'running',
2248
- pid: process.pid,
2249
- stats: {
2250
- pid: process.pid,
2251
- cpu: hubStats?.cpu ?? 0,
2252
- memory: hubStats?.memory ?? 0,
2253
- uptime: Math.round(process.uptime()),
2254
- restartCount: 0,
2255
- },
2256
- restartCount: 0,
2257
- mode: 'in-process',
2258
- });
2259
- }
2260
- }
2261
- return result;
2262
- }
2263
2231
  /**
2264
2232
  * Phase 11 (settings redesign): `getAddonConfigSchema`,
2265
2233
  * `getAddonConfig`, and `updateAddonConfig` — the legacy adapter
@@ -3243,6 +3211,45 @@ class AddonRegistryService {
3243
3211
  meta: { runnerId, addonCount: addons.length, addonIds: addons.map((a) => a.addonId) },
3244
3212
  });
3245
3213
  }
3214
+ /**
3215
+ * The last answer of `$process.list`, keyed by runner id (D877); `null` before
3216
+ * the first one or when the process service could not answer. Read by the
3217
+ * synchronous `listAddons()`; refreshed by `refreshRunnerStates()`.
3218
+ */
3219
+ runnerStates = null;
3220
+ /** Every runner row the process service holds right now, or `null` when it cannot answer. */
3221
+ async listRunnerRows() {
3222
+ try {
3223
+ // Untyped Moleculer boundary: the process-service owns `ProcessInfo`.
3224
+ const procs = (await this.broker.call('$process.list'));
3225
+ return procs.map((p) => ({
3226
+ name: p.name,
3227
+ pid: p.pid,
3228
+ addonIds: p.addonIds,
3229
+ state: p.state,
3230
+ ...(p.refusal !== undefined ? { refusal: { reason: p.refusal.reason } } : {}),
3231
+ }));
3232
+ }
3233
+ catch (err) {
3234
+ this.logger.warn('could not list the runner processes - addon process state is unknown', {
3235
+ meta: { error: (0, types_1.errMsg)(err) },
3236
+ });
3237
+ return null;
3238
+ }
3239
+ }
3240
+ /** Re-read the process service so the next `listAddons()` reports real runner states. */
3241
+ async refreshRunnerStates() {
3242
+ const rows = await this.listRunnerRows();
3243
+ this.runnerStates = rows === null ? null : new Map(rows.map((r) => [r.name, r]));
3244
+ }
3245
+ /** The runner ids this node PLANS to run: `resolveRunnerId` over every forked addon. */
3246
+ plannedRunnerIds() {
3247
+ return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).keys()];
3248
+ }
3249
+ /** The forked addon ids (the ones whose Init the D870 guard gates). */
3250
+ forkedAddonIds() {
3251
+ return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).values()].flatMap((roster) => roster.map((r) => r.addonId));
3252
+ }
3246
3253
  /**
3247
3254
  * Snapshot of the live runner subprocesses (`$process.list`), reduced to the
3248
3255
  * `{ runnerId, addonIds }` shape the convergence planner needs. Returns `[]`
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RunnerBootCheckService = exports.RUNNER_BOOT_CHECK_DELAY_MS = void 0;
4
+ exports.evaluateRunnerBootCheck = evaluateRunnerBootCheck;
5
+ exports.isRunnerBootCheckSettled = isRunnerBootCheckSettled;
6
+ exports.isRunnerBootCheckDegraded = isRunnerBootCheckDegraded;
7
+ exports.describeRunnerBootCheck = describeRunnerBootCheck;
8
+ /**
9
+ * Pure verdict. A planned runner is `running`, `refused` (typed Init refusal),
10
+ * `missing` (the process service has no entry), or `failed` - which here means
11
+ * "has an entry and is none of the above": the D6 breaker's terminal state, a
12
+ * crash waiting on its backoff, a stop, or a start that never reached READY.
13
+ */
14
+ function evaluateRunnerBootCheck(input) {
15
+ const byName = new Map(input.processes.map((p) => [p.name, p]));
16
+ const expectedIds = input.planned ?? input.processes.map((p) => p.name);
17
+ const failed = [];
18
+ const refused = [];
19
+ const missing = [];
20
+ const pending = [];
21
+ let running = 0;
22
+ for (const runnerId of expectedIds) {
23
+ const row = byName.get(runnerId);
24
+ if (row === undefined) {
25
+ missing.push(runnerId);
26
+ }
27
+ else if (row.state === 'running') {
28
+ running += 1;
29
+ }
30
+ else if (row.state === 'starting' || row.state === 'crashed') {
31
+ // On its way: a slow boot or a crash waiting on its backoff respawn is not
32
+ // a failure yet - the breaker (`failed`) is what says "gave up".
33
+ pending.push(runnerId);
34
+ }
35
+ else if (row.state === 'refused') {
36
+ refused.push({
37
+ runnerId,
38
+ addonIds: [...row.addonIds],
39
+ reason: row.refusal?.reason ?? 'refused to start (reason not reported)',
40
+ });
41
+ }
42
+ else {
43
+ failed.push(runnerId);
44
+ }
45
+ }
46
+ return {
47
+ checkedAtMs: input.now,
48
+ expected: expectedIds.length,
49
+ running,
50
+ failed,
51
+ refused,
52
+ missing,
53
+ pending,
54
+ planKnown: input.planned !== null,
55
+ };
56
+ }
57
+ /** True when no runner is still on its way - only a settled verdict is pushed. */
58
+ function isRunnerBootCheckSettled(check) {
59
+ return (check.pending ?? []).length === 0;
60
+ }
61
+ /** True when the check found anything an operator has to act on. */
62
+ function isRunnerBootCheckDegraded(check) {
63
+ return (check !== undefined &&
64
+ (check.failed.length > 0 || check.refused.length > 0 || check.missing.length > 0));
65
+ }
66
+ /** One sentence naming the findings, for a log line or an alert. */
67
+ function describeRunnerBootCheck(check) {
68
+ const parts = [];
69
+ if (check.failed.length > 0)
70
+ parts.push(`failed: ${check.failed.join(', ')}`);
71
+ if (check.refused.length > 0) {
72
+ parts.push(`refused: ${check.refused.map((r) => `${r.runnerId} (${r.reason})`).join('; ')}`);
73
+ }
74
+ if (check.missing.length > 0)
75
+ parts.push(`missing: ${check.missing.join(', ')}`);
76
+ if ((check.pending ?? []).length > 0)
77
+ parts.push(`still starting: ${(check.pending ?? []).join(', ')}`);
78
+ return `${check.running}/${check.expected} runners running${parts.length > 0 ? ` - ${parts.join(' - ')}` : ''}`;
79
+ }
80
+ exports.RUNNER_BOOT_CHECK_DELAY_MS = 60_000;
81
+ class RunnerBootCheckService {
82
+ options;
83
+ latest;
84
+ timer = null;
85
+ constructor(options) {
86
+ this.options = options;
87
+ }
88
+ /** The stored verdict; `undefined` until the check has run. */
89
+ current() {
90
+ return this.latest;
91
+ }
92
+ /** Arm the one-shot check. A second call while armed is a no-op. */
93
+ scheduleAfterBoot(delayMs = exports.RUNNER_BOOT_CHECK_DELAY_MS) {
94
+ if (this.timer !== null)
95
+ return;
96
+ this.timer = setTimeout(() => {
97
+ this.timer = null;
98
+ void this.run();
99
+ }, delayMs);
100
+ this.timer.unref();
101
+ }
102
+ reevaluating = null;
103
+ /**
104
+ * A runner settled on a state. While the stored verdict still has pending
105
+ * runners, evaluate again - once at a time, no timer: the process service
106
+ * tells us when something changed (D3).
107
+ */
108
+ noteRunnerChange() {
109
+ if (this.latest === undefined || isRunnerBootCheckSettled(this.latest))
110
+ return;
111
+ if (this.reevaluating !== null) {
112
+ // A change landed while a re-evaluation was reading: read once more after it.
113
+ this.changedDuringRun = true;
114
+ return;
115
+ }
116
+ this.reevaluating = this.run().finally(() => {
117
+ this.reevaluating = null;
118
+ if (this.changedDuringRun) {
119
+ this.changedDuringRun = false;
120
+ this.noteRunnerChange();
121
+ }
122
+ });
123
+ }
124
+ changedDuringRun = false;
125
+ cancel() {
126
+ if (this.timer !== null)
127
+ clearTimeout(this.timer);
128
+ this.timer = null;
129
+ }
130
+ /** Run the check now. A process service that cannot answer leaves the previous verdict. */
131
+ async run() {
132
+ const { logger, nodeId } = this.options;
133
+ let processes;
134
+ try {
135
+ processes = await this.options.listProcesses();
136
+ }
137
+ catch (err) {
138
+ logger.warn('runner boot check could not list the runners - no verdict recorded', {
139
+ tags: { nodeId },
140
+ meta: { error: err instanceof Error ? err.message : String(err) },
141
+ });
142
+ return this.latest;
143
+ }
144
+ const check = evaluateRunnerBootCheck({
145
+ planned: this.options.planned(),
146
+ processes,
147
+ now: (this.options.now ?? Date.now)(),
148
+ });
149
+ this.latest = check;
150
+ const summary = describeRunnerBootCheck(check);
151
+ if (!isRunnerBootCheckSettled(check)) {
152
+ // Not final: nothing is announced until every runner has settled; the next
153
+ // runner state change re-evaluates (`noteRunnerChange`).
154
+ logger.info(`runner boot check not settled: ${summary}`, { tags: { nodeId } });
155
+ return check;
156
+ }
157
+ if (isRunnerBootCheckDegraded(check)) {
158
+ logger.error(`runner boot check DEGRADED: ${summary}`, { tags: { nodeId } });
159
+ }
160
+ else {
161
+ logger.info(`runner boot check: ${summary}`, { tags: { nodeId } });
162
+ }
163
+ try {
164
+ this.options.onResult?.(check);
165
+ }
166
+ catch (err) {
167
+ logger.warn('runner boot check result handler threw', {
168
+ tags: { nodeId },
169
+ meta: { error: err instanceof Error ? err.message : String(err) },
170
+ });
171
+ }
172
+ return check;
173
+ }
174
+ }
175
+ exports.RunnerBootCheckService = RunnerBootCheckService;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LEGACY_BLOB_GUARD_SINCE = void 0;
4
+ exports.buildUpdatePreflight = buildUpdatePreflight;
5
+ exports.legacyGuardClientOf = legacyGuardClientOf;
6
+ /**
7
+ * The `applyServerUpdate` pre-flight (D877).
8
+ *
9
+ * Question: would the TARGET version refuse to start any addon on THIS node's
10
+ * live data? Today there is one deterministic Init refusal the framework can
11
+ * pre-check: the D870 legacy-blob guard (`assertLegacyBlobsMigrated`) - an addon
12
+ * whose settings still sit in its per-addon blob tables while the standard store
13
+ * holds none.
14
+ *
15
+ * The store-shape check is deliberately version-independent: the live store is
16
+ * what it is, so the only version question is "does the target contain the
17
+ * guard?" - answered by {@link LEGACY_BLOB_GUARD_SINCE}. The honest limit: it
18
+ * runs inside the version that ships it, so it protects updates FROM this
19
+ * version onwards; an update from an older hub never ran it. And it knows only
20
+ * the refusals listed here - a new deterministic Init refusal has to be added
21
+ * to this module to be pre-checked.
22
+ */
23
+ const system_1 = require("@camstack/system");
24
+ const index_js_1 = require("../../server-root/index.js");
25
+ /** The first server version whose addons run `assertLegacyBlobsMigrated` at Init. */
26
+ exports.LEGACY_BLOB_GUARD_SINCE = '1.2.585';
27
+ function buildUpdatePreflight(deps) {
28
+ return async (targetVersion) => {
29
+ if ((0, index_js_1.compareSemver)(targetVersion, exports.LEGACY_BLOB_GUARD_SINCE) < 0)
30
+ return [];
31
+ const store = deps.store();
32
+ if (store === null) {
33
+ throw new Error('the settings store is not available to pre-check the update');
34
+ }
35
+ const blockers = [];
36
+ for (const addonId of deps.forkedAddonIds()) {
37
+ try {
38
+ await (0, system_1.assertLegacyBlobsMigrated)(store, addonId, deps.logger);
39
+ }
40
+ catch (err) {
41
+ // A refusal is the finding; any other error is "could not ask" and the
42
+ // caller treats a throw as exactly that (it never blocks on it).
43
+ if (!(err instanceof system_1.UnmigratedSettingsError))
44
+ throw err;
45
+ blockers.push({
46
+ addonId,
47
+ reason: err.message,
48
+ remedy: system_1.UNMIGRATED_SETTINGS_REMEDY,
49
+ });
50
+ }
51
+ }
52
+ return blockers;
53
+ };
54
+ }
55
+ /** The three store calls the guard makes, over the hub's `settings-store` provider. */
56
+ function legacyGuardClientOf(store) {
57
+ return {
58
+ query: { query: (input) => store.query(input) },
59
+ get: { query: (input) => store.get(input) },
60
+ count: { query: (input) => store.count(input) },
61
+ };
62
+ }
@@ -103,6 +103,12 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
103
103
  applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
104
104
  env: options.env,
105
105
  now: options.now,
106
+ ...(options.updatePreflight !== undefined
107
+ ? { updatePreflight: options.updatePreflight }
108
+ : {}),
109
+ ...(options.runnerBootCheck !== undefined
110
+ ? { runnerBootCheck: options.runnerBootCheck }
111
+ : {}),
106
112
  });
107
113
  this.updateAvailability =
108
114
  options.eventBus !== undefined
package/dist/main.js CHANGED
@@ -67,6 +67,7 @@ const request_census_settings_1 = require("./api/core/request-census-settings");
67
67
  const register_cors_policy_1 = require("./api/cors/register-cors-policy");
68
68
  const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
69
69
  const health_routes_1 = require("./api/health/health.routes");
70
+ const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
70
71
  const hub_cluster_url_1 = require("./api/hub-cluster-url");
71
72
  const model_distributor_js_1 = require("./api/model-distributor.js");
72
73
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
@@ -729,6 +730,7 @@ async function bootstrap() {
729
730
  // router finishes registering (see `trpcRegistered = true` below).
730
731
  isReady: () => trpcRegistered,
731
732
  verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
733
+ runnerBootCheck: () => app.get(runner_boot_check_1.RunnerBootCheckService).current(),
732
734
  });
733
735
  console.log(`[bootstrap] Health routes registered (hub v${hubVersion})`);
734
736
  (0, adaptive_probe_routes_1.registerAdaptiveProbeRoute)(fastify, {
@@ -1693,6 +1695,10 @@ async function bootstrap() {
1693
1695
  // Post-boot: fork workers, register device streams, emit system.boot
1694
1696
  const postBoot = app.get(post_boot_service_1.PostBootService);
1695
1697
  await postBoot.run({ port, host, dataPath, trpcRegistered, tlsCert });
1698
+ // D877: ONE check, ~60 s after the boot sequence, of the runners this node
1699
+ // planned against what its process service holds. Stored in memory and served
1700
+ // on `getServerPackageStatus` / `/health`; not a timer loop.
1701
+ app.get(runner_boot_check_1.RunnerBootCheckService).scheduleAfterBoot();
1696
1702
  // The hub reached ready — when this boot is the PROBATION boot of a freshly
1697
1703
  // staged @camstack/server root, promote it (currentVersion ← pending, N-1
1698
1704
  // retained for rollback) and disarm the starter's crash-loop auto-rollback.
@@ -75,6 +75,8 @@ const notification_wrapper_service_1 = require("./core/notification/notification
75
75
  const toast_wrapper_service_1 = require("./core/notification/toast-wrapper.service");
76
76
  const repl_engine_service_1 = require("./core/repl/repl-engine.service");
77
77
  const server_update_service_1 = require("./core/server-update/server-update.service");
78
+ const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
79
+ const update_preflight_1 = require("./core/runner-health/update-preflight");
78
80
  const storage_service_1 = require("./core/storage/storage.service");
79
81
  const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
80
82
  const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
@@ -251,7 +253,49 @@ async function bootManual(opts) {
251
253
  // Runtime-updatable root package (phase 1: hub only). The service owns the
252
254
  // data-dir server-root staging/rollback lifecycle; restarts route through
253
255
  // AddonPackageService.restartServer for the marker + reconnect toast.
256
+ // D877: the runner boot check (one-shot after boot, scheduled by main.ts) and
257
+ // the update pre-flight (would the target refuse addons on the live store?).
258
+ const hubRunnerBootCheck = new runner_boot_check_1.RunnerBootCheckService({
259
+ logger: loggingService.createLogger('RunnerBootCheck'),
260
+ nodeId: 'hub',
261
+ planned: () => addonRegistryService.plannedRunnerIds(),
262
+ listProcesses: async () => {
263
+ const rows = await addonRegistryService.listRunnerRows();
264
+ if (rows === null)
265
+ throw new Error('the process service did not answer');
266
+ return rows;
267
+ },
268
+ // The verdict reaches the alert centre (one alert + the direct critical push).
269
+ onResult: (check) => {
270
+ eventBusService.emit({
271
+ id: (0, node_crypto_1.randomUUID)(),
272
+ timestamp: new Date(),
273
+ source: { type: 'core', id: 'runner-boot-check' },
274
+ category: types_1.EventCategory.SystemRunnerCheck,
275
+ data: {
276
+ nodeId: 'hub',
277
+ degraded: (0, runner_boot_check_1.isRunnerBootCheckDegraded)(check),
278
+ summary: (0, runner_boot_check_1.describeRunnerBootCheck)(check),
279
+ expected: check.expected,
280
+ running: check.running,
281
+ failed: [...check.failed],
282
+ refused: check.refused.map((r) => ({ runnerId: r.runnerId, reason: r.reason })),
283
+ missing: [...check.missing],
284
+ },
285
+ });
286
+ },
287
+ });
288
+ const settingsStoreOrNull = () => capabilityService.getRegistry()?.getSingleton('settings-store') ?? null;
254
289
  const serverUpdateService = new server_update_service_1.ServerUpdateService({
290
+ updatePreflight: (0, update_preflight_1.buildUpdatePreflight)({
291
+ logger: loggingService.createLogger('UpdatePreflight'),
292
+ forkedAddonIds: () => addonRegistryService.forkedAddonIds(),
293
+ store: () => {
294
+ const store = settingsStoreOrNull();
295
+ return store === null ? null : (0, update_preflight_1.legacyGuardClientOf)(store);
296
+ },
297
+ }),
298
+ runnerBootCheck: () => hubRunnerBootCheck.current(),
255
299
  logger: loggingService.createLogger('ServerUpdate'),
256
300
  restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
257
301
  eventBus: eventBusService,
@@ -337,6 +381,10 @@ async function bootManual(opts) {
337
381
  container.register(addon_package_service_1.AddonPackageService, addonPackageService);
338
382
  container.register(repl_engine_service_1.ReplEngineService, replEngineService);
339
383
  container.register(server_update_service_1.ServerUpdateService, serverUpdateService);
384
+ container.register(runner_boot_check_1.RunnerBootCheckService, hubRunnerBootCheck);
385
+ // A runner settling (READY / crashed / failed / refused) re-evaluates a verdict
386
+ // that still had runners on their way (D877).
387
+ eventBusService.subscribe({ category: types_1.EventCategory.ProcessStateChanged }, () => hubRunnerBootCheck.noteRunnerChange());
340
388
  container.register(topology_emitter_service_1.TopologyEmitterService, topologyEmitterService);
341
389
  container.register(post_boot_service_1.PostBootService, postBootService);
342
390
  container.register(update_check_scheduler_js_1.UpdateCheckScheduler, updateCheckScheduler);
@@ -1141,6 +1141,8 @@ var require_dist = __commonJS({
1141
1141
  logTags;
1142
1142
  restartServerFn;
1143
1143
  dataDir;
1144
+ updatePreflightFn;
1145
+ runnerBootCheckFn;
1144
1146
  execNpm;
1145
1147
  ensureNativePrebuildsFn;
1146
1148
  applyClosurePatchesFn;
@@ -1172,6 +1174,8 @@ var require_dist = __commonJS({
1172
1174
  };
1173
1175
  this.restartServerFn = options.restartServer;
1174
1176
  this.dataDir = path6.resolve(options.dataDir);
1177
+ this.updatePreflightFn = options.updatePreflight ?? null;
1178
+ this.runnerBootCheckFn = options.runnerBootCheck ?? (() => void 0);
1175
1179
  this.execNpm = options.execNpm ?? (async (args, opts) => {
1176
1180
  const { stdout } = await execFileAsync("npm", [
1177
1181
  ...args
@@ -1320,7 +1324,12 @@ var require_dist = __commonJS({
1320
1324
  // The other half of the same question. `imageContract` says the shell is
1321
1325
  // BEHIND; this says WHAT the shell is, which is the only thing that can
1322
1326
  // turn that verdict into an instruction the operator can act on.
1323
- wrapper: this.wrapperIdentity()
1327
+ wrapper: this.wrapperIdentity(),
1328
+ // Planned runners against the process service's states (D877); absent
1329
+ // until the one-shot boot check has run.
1330
+ ...this.runnerBootCheckFn() !== void 0 ? {
1331
+ runners: this.runnerBootCheckFn()
1332
+ } : {}
1324
1333
  };
1325
1334
  }
1326
1335
  // ── Check ─────────────────────────────────────────────────────────────
@@ -1440,6 +1449,21 @@ var require_dist = __commonJS({
1440
1449
  error: null
1441
1450
  };
1442
1451
  }
1452
+ async runPreflight(target) {
1453
+ if (this.updatePreflightFn === null) return [];
1454
+ try {
1455
+ return await this.updatePreflightFn(target);
1456
+ } catch (err) {
1457
+ this.logger.warn("update pre-flight could not run - proceeding without it", {
1458
+ tags: this.logTags,
1459
+ meta: {
1460
+ targetVersion: target,
1461
+ error: err instanceof Error ? err.message : String(err)
1462
+ }
1463
+ });
1464
+ return [];
1465
+ }
1466
+ }
1443
1467
  async applyServerUpdate(input) {
1444
1468
  this.logger.info("root package update requested", {
1445
1469
  tags: this.logTags,
@@ -1477,6 +1501,29 @@ var require_dist = __commonJS({
1477
1501
  return this.refuseUpdate("dev-uploads-missing", target, `Refused: dev-channel version ${target} has no uploaded tarballs on this node (${uploadsDir} is missing). On the hub: \`camstack deploy-server\`. On an agent: \`camstack deploy-server -n <nodeId>\` (or \`--all\`) so the hub can POST /api/server-upload/propagate first.`);
1478
1502
  }
1479
1503
  }
1504
+ const blockers = await this.runPreflight(target);
1505
+ if (blockers.length > 0 && input.acknowledgeRefusals !== true) {
1506
+ const names = blockers.map((b) => b.addonId).join(", ");
1507
+ const remedies = [
1508
+ ...new Set(blockers.map((b) => b.remedy))
1509
+ ].join(" ");
1510
+ const refused = this.refuseUpdate("addon-would-refuse", target, `Refused: ${this.spec.packageName}@${target} would refuse to start ${blockers.length} addon(s) on this node's data: ${names}. ${remedies} Then retry, or pass acknowledgeRefusals to apply anyway (those addons will stay down).`);
1511
+ return {
1512
+ ...refused,
1513
+ blockers: [
1514
+ ...blockers
1515
+ ]
1516
+ };
1517
+ }
1518
+ if (blockers.length > 0) {
1519
+ this.logger.warn("root package update applied over acknowledged addon refusals", {
1520
+ tags: this.logTags,
1521
+ meta: {
1522
+ targetVersion: target,
1523
+ addonIds: blockers.map((b) => b.addonId)
1524
+ }
1525
+ });
1526
+ }
1480
1527
  this.inFlight = "staging";
1481
1528
  let stagingPath;
1482
1529
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.588",
3
+ "version": "1.2.589",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.432",
42
- "@camstack/addon-agent-ui": "1.2.239",
41
+ "@camstack/addon-admin-ui": "1.2.433",
42
+ "@camstack/addon-agent-ui": "1.2.240",
43
43
  "@camstack/addon-auth": "1.2.148",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
45
45
  "@camstack/addon-notifiers": "1.2.135",
46
46
  "@camstack/addon-pipeline": "1.2.374",
47
47
  "@camstack/addon-pipeline-orchestrator": "1.2.329",
48
- "@camstack/addon-post-analysis": "1.2.372",
48
+ "@camstack/addon-post-analysis": "1.2.373",
49
49
  "@camstack/develop": "0.1.11",
50
50
  "@camstack/sdk": "1.2.132",
51
- "@camstack/system": "1.2.441",
52
- "@camstack/types": "1.2.377",
53
- "@camstack/ui-library": "1.2.295",
51
+ "@camstack/system": "1.2.442",
52
+ "@camstack/types": "1.2.378",
53
+ "@camstack/ui-library": "1.2.296",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",