@camstack/server 1.2.125 → 1.2.127

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.
@@ -60,11 +60,15 @@ exports.agentRuntimeManifestEntry = agentRuntimeManifestEntry;
60
60
  *
61
61
  * Spec: docs/superpowers/specs/2026-07-18-single-framework-copy-collapse-design.md
62
62
  */
63
+ const node_crypto_1 = require("node:crypto");
63
64
  const fs = __importStar(require("node:fs"));
64
65
  const path = __importStar(require("node:path"));
65
66
  const index_js_1 = require("../server-root/index.js");
66
67
  const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
67
68
  const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
69
+ const system_1 = require("@camstack/system");
70
+ const types_1 = require("@camstack/types");
71
+ const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
68
72
  /**
69
73
  * The synthetic addonId the agent's OWN runtime registers infra providers
70
74
  * under (no addon owns them — the bootstrap does). Appears in the agent's
@@ -143,7 +147,13 @@ function scheduleAgentRestart(logger, requestedBy, dataDir) {
143
147
  grace.unref();
144
148
  }
145
149
  class AgentUpdateService extends index_js_1.RootUpdateService {
150
+ updateAvailability;
151
+ eventBus;
152
+ nodeId;
153
+ agentDataDir;
146
154
  constructor(options) {
155
+ const restartAgent = options.restartAgent ??
156
+ ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir));
147
157
  super({
148
158
  spec: index_js_1.HUB_ROOT_SPEC,
149
159
  // The agent's on-disk root IS `@camstack/server` (booted via
@@ -155,8 +165,10 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
155
165
  seedDir: 'CAMSTACK_SEED_SERVER_DIR',
156
166
  },
157
167
  logger: options.logger,
158
- restartServer: options.restartAgent ??
159
- ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
168
+ restartServer: (requestedBy) => {
169
+ writeAgentRestartMarker(options.dataDir, requestedBy);
170
+ restartAgent(requestedBy);
171
+ },
160
172
  dataDir: options.dataDir,
161
173
  runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
162
174
  workspaceProbeDir: __dirname,
@@ -175,9 +187,88 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
175
187
  env: options.env,
176
188
  now: options.now,
177
189
  });
190
+ this.nodeId = options.nodeId ?? process.env['CAMSTACK_NODE_ID'] ?? 'agent';
191
+ this.agentDataDir = options.dataDir;
192
+ this.eventBus = options.eventBus ?? null;
193
+ this.updateAvailability =
194
+ options.eventBus !== undefined
195
+ ? new update_availability_emitter_js_1.UpdateAvailabilityEmitter(options.eventBus, {
196
+ type: 'core',
197
+ id: 'agent-update-service',
198
+ })
199
+ : null;
200
+ }
201
+ bindAvailability(eventBus, nodeId) {
202
+ this.eventBus = eventBus;
203
+ this.nodeId = nodeId;
204
+ this.updateAvailability = new update_availability_emitter_js_1.UpdateAvailabilityEmitter(eventBus, {
205
+ type: 'core',
206
+ id: 'agent-update-service',
207
+ });
208
+ }
209
+ async checkServerUpdate() {
210
+ const result = await super.checkServerUpdate();
211
+ if (result.error !== null)
212
+ return result;
213
+ this.updateAvailability?.publishSnapshot('server', result.updateAvailable && result.runningVersion !== null && result.latestVersion !== null
214
+ ? [
215
+ {
216
+ target: 'server',
217
+ packageName: result.packageName,
218
+ currentVersion: result.runningVersion,
219
+ latestVersion: result.latestVersion,
220
+ nodeId: this.nodeId,
221
+ },
222
+ ]
223
+ : [], this.nodeId);
224
+ return result;
225
+ }
226
+ /** Consume `.restart-pending` and emit `system.restart-completed` if present. */
227
+ emitRestartCompletedIfPending() {
228
+ const bus = this.eventBus;
229
+ if (bus === null)
230
+ return;
231
+ const marker = (0, system_1.readPendingRestart)(this.agentDataDir);
232
+ if (marker === null)
233
+ return;
234
+ const payload = {
235
+ kind: marker.kind,
236
+ requestedAt: marker.requestedAt,
237
+ nodeId: this.nodeId,
238
+ ...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
239
+ ...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
240
+ ...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
241
+ ...(marker.requestedBy !== undefined ? { requestedBy: marker.requestedBy } : {}),
242
+ };
243
+ bus.emit({
244
+ id: (0, node_crypto_1.randomUUID)(),
245
+ timestamp: new Date(),
246
+ source: { type: 'core', id: 'agent-update-service' },
247
+ category: types_1.EventCategory.SystemRestartCompleted,
248
+ data: payload,
249
+ });
178
250
  }
179
251
  }
180
252
  exports.AgentUpdateService = AgentUpdateService;
253
+ function writeAgentRestartMarker(dataDir, requestedBy) {
254
+ const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
255
+ try {
256
+ (0, system_1.writePendingRestart)(dataDir, {
257
+ kind: requestedBy.startsWith('server-update:') ? 'framework-update' : 'manual',
258
+ requestedAt: Date.now(),
259
+ requestedBy,
260
+ ...(requestedBy.startsWith('server-update:')
261
+ ? {
262
+ packageName: '@camstack/server',
263
+ ...(toVersion !== undefined ? { toVersion } : {}),
264
+ }
265
+ : {}),
266
+ });
267
+ }
268
+ catch {
269
+ // Marker write must never block the restart.
270
+ }
271
+ }
181
272
  /**
182
273
  * The manifest entry for the agent runtime's own providers. Appended to
183
274
  * `buildAgentOwnManifest`'s output so `server-management` reaches the hub's
@@ -378,6 +378,7 @@ async function startAgent(configPath) {
378
378
  agentBootConfirmed = true;
379
379
  try {
380
380
  agentUpdateService.confirmBootHealthy();
381
+ agentUpdateService.emitRestartCompletedIfPending();
381
382
  }
382
383
  catch (err) {
383
384
  consoleLogger.warn(`agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -655,9 +656,10 @@ async function startAgent(configPath) {
655
656
  // local events propagate to every UDS child. Inert when agentUdsRegistry
656
657
  // was not started (no UDS children). Wired after broker.start() so
657
658
  // getBrokerEventBus returns the fully operational shared bus.
659
+ const agentBrokerEventBus = (0, system_1.getBrokerEventBus)(broker);
660
+ agentUpdateService.bindAvailability(agentBrokerEventBus, broker.nodeID);
658
661
  let udsEventBridgeDispose = null;
659
662
  if (agentUdsRegistry !== undefined) {
660
- const agentBrokerEventBus = (0, system_1.getBrokerEventBus)(broker);
661
663
  udsEventBridgeDispose = (0, system_1.createUdsEventBridge)({
662
664
  registry: agentUdsRegistry,
663
665
  parentBus: agentBrokerEventBus,
@@ -5794,6 +5794,15 @@ function createCapRouter_pipelineAnalytics(getProvider, createRemoteProxy) {
5794
5794
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5795
5795
  return p.wipeAllAnalytics(methodInput);
5796
5796
  }),
5797
+ reconcileFromDisk: trpc_middleware_js_1.adminProcedure
5798
+ .input(types_82.pipelineAnalyticsCapability.methods.reconcileFromDisk.input.loose())
5799
+ .output(types_82.pipelineAnalyticsCapability.methods.reconcileFromDisk.output)
5800
+ .mutation(async ({ input, ctx }) => {
5801
+ const { nodeId, ...methodInput } = input;
5802
+ const p = resolveProvider('pipeline-analytics', nodeId, () => getProvider(ctx), createRemoteProxy);
5803
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5804
+ return p.reconcileFromDisk(methodInput);
5805
+ }),
5797
5806
  deleteTracks: trpc_middleware_js_1.adminProcedure
5798
5807
  .input(types_82.pipelineAnalyticsCapability.methods.deleteTracks.input.loose())
5799
5808
  .output(types_82.pipelineAnalyticsCapability.methods.deleteTracks.output)
@@ -6728,6 +6737,22 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
6728
6737
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
6729
6738
  return p.getCameraStatuses(input);
6730
6739
  }),
6740
+ reconcileFromDisk: trpc_middleware_js_1.adminProcedure
6741
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
6742
+ .output(types_84.pipelineOrchestratorCapability.methods.reconcileFromDisk.output)
6743
+ .mutation(async ({ input, ctx }) => {
6744
+ void input;
6745
+ const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
6746
+ return p.reconcileFromDisk();
6747
+ }),
6748
+ getReconcileFromDiskStatus: trpc_middleware_js_1.adminProcedure
6749
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
6750
+ .output(types_84.pipelineOrchestratorCapability.methods.getReconcileFromDiskStatus.output)
6751
+ .query(async ({ input, ctx }) => {
6752
+ void input;
6753
+ const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
6754
+ return p.getReconcileFromDiskStatus();
6755
+ }),
6731
6756
  listTemplates: trpc_middleware_js_1.protectedProcedure
6732
6757
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
6733
6758
  .output(types_84.pipelineOrchestratorCapability.methods.listTemplates.output)
@@ -75,6 +75,7 @@ class PostBootService {
75
75
  const payload = {
76
76
  kind: marker.kind,
77
77
  requestedAt: marker.requestedAt,
78
+ nodeId: process.env['CAMSTACK_NODE_ID'] ?? 'hub',
78
79
  ...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
79
80
  ...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
80
81
  ...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
@@ -1103,10 +1103,17 @@ class AddonPackageService {
1103
1103
  */
1104
1104
  restartServer(requestedBy) {
1105
1105
  this.logger.info('Server restart requested -- initiating graceful shutdown');
1106
+ const toVersion = toVersionFromRestartReason(requestedBy);
1106
1107
  const payload = {
1107
- kind: 'manual',
1108
+ kind: isServerUpdateRequest(requestedBy) ? 'framework-update' : 'manual',
1108
1109
  requestedAt: Date.now(),
1109
1110
  ...(requestedBy !== undefined ? { requestedBy } : {}),
1111
+ ...(isServerUpdateRequest(requestedBy)
1112
+ ? {
1113
+ packageName: '@camstack/server',
1114
+ ...(toVersion !== undefined ? { toVersion } : {}),
1115
+ }
1116
+ : {}),
1110
1117
  };
1111
1118
  try {
1112
1119
  (0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
@@ -1710,6 +1717,15 @@ class AddonPackageService {
1710
1717
  }
1711
1718
  }
1712
1719
  exports.AddonPackageService = AddonPackageService;
1720
+ function isServerUpdateRequest(requestedBy) {
1721
+ return requestedBy !== undefined && requestedBy.startsWith('server-update:');
1722
+ }
1723
+ function toVersionFromRestartReason(requestedBy) {
1724
+ if (requestedBy === undefined)
1725
+ return undefined;
1726
+ const match = /@([^@\s]+)$/.exec(requestedBy);
1727
+ return match?.[1];
1728
+ }
1713
1729
  // ---------------------------------------------------------------------------
1714
1730
  // Tarball extraction helper (exported for regression tests)
1715
1731
  // ---------------------------------------------------------------------------
@@ -58,6 +58,7 @@ const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
58
58
  const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
59
59
  class ServerUpdateService extends index_js_1.RootUpdateService {
60
60
  updateAvailability;
61
+ nodeId;
61
62
  constructor(options) {
62
63
  const dataDir = path.resolve(options.dataDir ??
63
64
  options.env?.['CAMSTACK_DATA'] ??
@@ -99,11 +100,13 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
99
100
  id: 'server-update-service',
100
101
  })
101
102
  : null;
103
+ this.nodeId = options.nodeId ?? 'hub';
102
104
  }
103
105
  async checkServerUpdate() {
104
106
  const result = await super.checkServerUpdate();
105
107
  if (result.error !== null)
106
108
  return result;
109
+ const nodeId = this.nodeId;
107
110
  this.updateAvailability?.publishSnapshot('server', result.updateAvailable && result.runningVersion !== null && result.latestVersion !== null
108
111
  ? [
109
112
  {
@@ -111,9 +114,10 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
111
114
  packageName: result.packageName,
112
115
  currentVersion: result.runningVersion,
113
116
  latestVersion: result.latestVersion,
117
+ ...(nodeId !== undefined ? { nodeId } : {}),
114
118
  },
115
119
  ]
116
- : []);
120
+ : [], nodeId);
117
121
  return result;
118
122
  }
119
123
  }
@@ -3,14 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UpdateAvailabilityEmitter = void 0;
4
4
  const types_1 = require("@camstack/types");
5
5
  /**
6
- * Emits update availability only when a successful check changes the observed
7
- * `(currentVersion, latestVersion)` pair. A successful empty snapshot clears
8
- * prior state; failed checks should not call this method.
6
+ * Emits update availability only when a successful check changes an observed
7
+ * `latestVersion` (or first-sees a package/node that has one). Repeated polls
8
+ * of the same latests are silent.
9
+ *
10
+ * Addon snapshots emit ONE event carrying the **entire** currently-available
11
+ * list. Server snapshots do the same across nodes, so the Notification Center
12
+ * can say "1.0.3 is available on hub and little-unraid" instead of one push
13
+ * per package or per node.
14
+ *
15
+ * A successful empty snapshot clears prior state for that scope; failed checks
16
+ * should not call this method.
9
17
  */
10
18
  class UpdateAvailabilityEmitter {
11
19
  eventBus;
12
20
  source;
13
- signatures = new Map();
21
+ latestByKey = new Map();
22
+ held = new Map();
14
23
  constructor(eventBus, source) {
15
24
  this.eventBus = eventBus;
16
25
  this.source = source;
@@ -18,36 +27,78 @@ class UpdateAvailabilityEmitter {
18
27
  publishSnapshot(target, candidates, nodeId) {
19
28
  const scope = nodeId ?? 'hub';
20
29
  const present = new Set();
30
+ let latestChanged = false;
21
31
  for (const candidate of candidates) {
22
32
  if (candidate.target !== target)
23
33
  continue;
24
- const key = this.key(candidate);
34
+ const stamped = stampNode(candidate, nodeId);
35
+ const key = this.key(stamped);
25
36
  present.add(key);
26
- this.publish(candidate);
37
+ if (this.note(stamped))
38
+ latestChanged = true;
27
39
  }
28
- for (const key of this.signatures.keys()) {
29
- if (key.startsWith(`${target}:${scope}:`) && !present.has(key)) {
30
- this.signatures.delete(key);
31
- }
40
+ for (const key of [...this.held.keys()]) {
41
+ if (!key.startsWith(`${target}:${scope}:`))
42
+ continue;
43
+ if (present.has(key))
44
+ continue;
45
+ this.held.delete(key);
46
+ this.latestByKey.delete(key);
32
47
  }
48
+ if (!latestChanged)
49
+ return;
50
+ this.emitList(target);
33
51
  }
34
52
  /** Publish a partial candidate set without treating omissions as up-to-date. */
35
53
  publishCandidates(candidates) {
36
- for (const candidate of candidates)
37
- this.publish(candidate);
54
+ let latestChanged = false;
55
+ const targets = new Set();
56
+ for (const candidate of candidates) {
57
+ targets.add(candidate.target);
58
+ if (this.note(candidate))
59
+ latestChanged = true;
60
+ }
61
+ if (!latestChanged)
62
+ return;
63
+ for (const target of targets)
64
+ this.emitList(target);
38
65
  }
39
- publish(candidate) {
66
+ note(candidate) {
40
67
  const key = this.key(candidate);
41
- const signature = `${candidate.currentVersion}->${candidate.latestVersion}`;
42
- if (this.signatures.get(key) === signature)
68
+ const previous = this.latestByKey.get(key);
69
+ this.held.set(key, candidate);
70
+ this.latestByKey.set(key, candidate.latestVersion);
71
+ return previous !== candidate.latestVersion;
72
+ }
73
+ emitList(target) {
74
+ const list = [...this.held.values()].filter((candidate) => candidate.target === target);
75
+ if (list.length === 0)
43
76
  return;
44
- this.signatures.set(key, signature);
77
+ const packages = list.map((candidate) => ({
78
+ packageName: candidate.packageName,
79
+ currentVersion: candidate.currentVersion,
80
+ latestVersion: candidate.latestVersion,
81
+ ...(candidate.nodeId !== undefined ? { nodeId: candidate.nodeId } : {}),
82
+ }));
83
+ const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== undefined));
84
+ const head = list[0];
45
85
  this.eventBus.emit({
46
- id: `update.available:${key}:${signature}`,
86
+ id: `update.available:${target}:${packages
87
+ .map((pkg) => `${pkg.nodeId ?? 'hub'}:${pkg.packageName}@${pkg.latestVersion}`)
88
+ .toSorted()
89
+ .join(',')}`,
47
90
  timestamp: new Date(),
48
91
  source: this.source,
49
92
  category: types_1.EventCategory.UpdateAvailable,
50
- data: candidate,
93
+ data: {
94
+ target,
95
+ packageName: head.packageName,
96
+ currentVersion: head.currentVersion,
97
+ latestVersion: head.latestVersion,
98
+ packages,
99
+ ...(head.nodeId !== undefined ? { nodeId: head.nodeId } : {}),
100
+ ...(nodeIds.length > 0 ? { nodeIds } : {}),
101
+ },
51
102
  });
52
103
  }
53
104
  key(candidate) {
@@ -55,3 +106,11 @@ class UpdateAvailabilityEmitter {
55
106
  }
56
107
  }
57
108
  exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
109
+ function stampNode(candidate, nodeId) {
110
+ if (nodeId === undefined || candidate.nodeId !== undefined)
111
+ return candidate;
112
+ return { ...candidate, nodeId };
113
+ }
114
+ function unique(values) {
115
+ return [...new Set(values)];
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.125",
3
+ "version": "1.2.127",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.67",
36
+ "@camstack/addon-admin-ui": "1.2.76",
37
37
  "@camstack/addon-agent-ui": "1.2.20",
38
38
  "@camstack/addon-auth": "1.2.22",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.19",
40
40
  "@camstack/addon-notifiers": "1.2.24",
41
- "@camstack/addon-pipeline": "1.2.88",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.67",
43
- "@camstack/addon-post-analysis": "1.2.84",
41
+ "@camstack/addon-pipeline": "1.2.93",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.75",
43
+ "@camstack/addon-post-analysis": "1.2.90",
44
44
  "@camstack/sdk": "1.2.22",
45
45
  "@camstack/shm-ring": "1.1.19",
46
- "@camstack/system": "1.2.101",
47
- "@camstack/types": "1.2.82",
48
- "@camstack/ui-library": "1.2.57",
46
+ "@camstack/system": "1.2.102",
47
+ "@camstack/types": "1.2.84",
48
+ "@camstack/ui-library": "1.2.58",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",