@camstack/server 1.2.138 → 1.2.140

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.
@@ -70,6 +70,7 @@ const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js")
70
70
  const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
71
71
  const update_availability_store_js_1 = require("../core/update-availability-store.js");
72
72
  const index_js_1 = require("../server-root/index.js");
73
+ const restart_marker_js_1 = require("../core/updates/restart-marker.js");
73
74
  /**
74
75
  * The synthetic addonId the agent's OWN runtime registers infra providers
75
76
  * under (no addon owns them — the bootstrap does). Appears in the agent's
@@ -168,8 +169,8 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
168
169
  seedDir: 'CAMSTACK_SEED_SERVER_DIR',
169
170
  },
170
171
  logger: options.logger,
171
- restartServer: (requestedBy) => {
172
- writeAgentRestartMarker(options.dataDir, requestedBy);
172
+ restartServer: (requestedBy, update) => {
173
+ writeAgentRestartMarker(options.dataDir, requestedBy, update);
173
174
  restartAgent(requestedBy);
174
175
  },
175
176
  dataDir: options.dataDir,
@@ -253,20 +254,15 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
253
254
  }
254
255
  }
255
256
  exports.AgentUpdateService = AgentUpdateService;
256
- function writeAgentRestartMarker(dataDir, requestedBy) {
257
- const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
257
+ function writeAgentRestartMarker(dataDir, requestedBy, update) {
258
258
  try {
259
- (0, system_1.writePendingRestart)(dataDir, {
260
- kind: requestedBy.startsWith('server-update:') ? 'framework-update' : 'manual',
259
+ // Same builder as the hub: an agent's "Server aggiornato" notification must
260
+ // name both versions too — see `core/updates/restart-marker.ts`.
261
+ (0, system_1.writePendingRestart)(dataDir, (0, restart_marker_js_1.buildRestartMarker)({
261
262
  requestedAt: Date.now(),
262
263
  requestedBy,
263
- ...(requestedBy.startsWith('server-update:')
264
- ? {
265
- packageName: '@camstack/server',
266
- ...(toVersion !== undefined ? { toVersion } : {}),
267
- }
268
- : {}),
269
- });
264
+ ...(update !== undefined ? { update } : {}),
265
+ }));
270
266
  }
271
267
  catch {
272
268
  // Marker write must never block the restart.
@@ -47,6 +47,7 @@ const system_1 = require("@camstack/system");
47
47
  const types_1 = require("@camstack/types");
48
48
  const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
49
49
  const update_check_scheduler_js_1 = require("../updates/update-check-scheduler.js");
50
+ const restart_marker_js_1 = require("../updates/restart-marker.js");
50
51
  const package_dir_utils_js_1 = require("./package-dir-utils.js");
51
52
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
52
53
  /**
@@ -1105,28 +1106,25 @@ class AddonPackageService {
1105
1106
  /**
1106
1107
  * Gracefully restart the server process.
1107
1108
  *
1108
- * Writes a `manual` restart marker so the post-boot service can
1109
- * emit `system.restart-completed` on the next boot (powers the
1110
- * admin-UI "Server restarted" toast). Emits `system.restarting`
1109
+ * Writes the restart marker so the post-boot service can emit
1110
+ * `system.restart-completed` on the next boot (powers the admin-UI
1111
+ * "Server restarted" toast AND the operator's "Server aggiornato"
1112
+ * notification). `update` is passed by `RootUpdateService` for an
1113
+ * update-driven restart and is the ONLY source of the version being left
1114
+ * behind — see `updates/restart-marker.ts`. Emits `system.restarting`
1111
1115
  * with the same payload so subscribers can show a reconnect overlay
1112
1116
  * immediately. Then delegates to `scheduleSelfRestart` which picks
1113
1117
  * the right exit path (Electron `app.relaunch` vs supervisor-driven
1114
1118
  * `process.exit`).
1115
1119
  */
1116
- restartServer(requestedBy) {
1120
+ restartServer(requestedBy, update) {
1117
1121
  this.logger.info('Server restart requested -- initiating graceful shutdown');
1118
- const toVersion = toVersionFromRestartReason(requestedBy);
1119
- const payload = {
1120
- kind: isServerUpdateRequest(requestedBy) ? 'framework-update' : 'manual',
1122
+ // Both versions, one builder, shared with the agent — see `restart-marker.ts`.
1123
+ const payload = (0, restart_marker_js_1.buildRestartMarker)({
1121
1124
  requestedAt: Date.now(),
1122
1125
  ...(requestedBy !== undefined ? { requestedBy } : {}),
1123
- ...(isServerUpdateRequest(requestedBy)
1124
- ? {
1125
- packageName: '@camstack/server',
1126
- ...(toVersion !== undefined ? { toVersion } : {}),
1127
- }
1128
- : {}),
1129
- };
1126
+ ...(update !== undefined ? { update } : {}),
1127
+ });
1130
1128
  try {
1131
1129
  (0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
1132
1130
  }
@@ -1682,22 +1680,26 @@ class AddonPackageService {
1682
1680
  * re-announces them on the next good poll.
1683
1681
  */
1684
1682
  publishAddonAvailability(updates, failures, nodeId) {
1683
+ // The hub's own sweep passes no nodeId — but its rows are about a NODE
1684
+ // like any other, and a list whose lines cannot be attributed is what made
1685
+ // three announcements look like one.
1686
+ const scope = nodeId ?? this.resolveNodeId();
1685
1687
  const candidates = updates.map((update) => ({
1686
1688
  target: 'addon',
1687
1689
  packageName: update.name,
1688
1690
  currentVersion: update.currentVersion,
1689
1691
  latestVersion: update.latestVersion,
1690
- ...(nodeId !== undefined ? { nodeId } : {}),
1692
+ nodeId: scope,
1691
1693
  }));
1692
1694
  if (failures > 0) {
1693
1695
  this.logger.warn('Addon update check incomplete — availability published as partial', {
1694
- tags: { nodeId: nodeId ?? 'hub' },
1696
+ tags: { nodeId: scope },
1695
1697
  meta: { failures, published: candidates.length },
1696
1698
  });
1697
1699
  this.updateAvailability.publishCandidates(candidates);
1698
1700
  return;
1699
1701
  }
1700
- this.updateAvailability.publishSnapshot('addon', candidates, nodeId);
1702
+ this.updateAvailability.publishSnapshot('addon', candidates, scope);
1701
1703
  }
1702
1704
  /** Fetch npm search results for camstack addon packages (cached 5 min) */
1703
1705
  async fetchSearchFromNpm() {
@@ -1860,15 +1862,6 @@ class AddonPackageService {
1860
1862
  }
1861
1863
  }
1862
1864
  exports.AddonPackageService = AddonPackageService;
1863
- function isServerUpdateRequest(requestedBy) {
1864
- return requestedBy !== undefined && requestedBy.startsWith('server-update:');
1865
- }
1866
- function toVersionFromRestartReason(requestedBy) {
1867
- if (requestedBy === undefined)
1868
- return undefined;
1869
- const match = /@([^@\s]+)$/.exec(requestedBy);
1870
- return match?.[1];
1871
- }
1872
1865
  // ---------------------------------------------------------------------------
1873
1866
  // Tarball extraction helper (exported for regression tests)
1874
1867
  // ---------------------------------------------------------------------------
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MoleculerService = void 0;
4
+ exports.childOwnerToken = childOwnerToken;
4
5
  exports.buildChildUdsManifest = buildChildUdsManifest;
5
6
  const node_crypto_1 = require("node:crypto");
6
7
  const system_1 = require("@camstack/system");
@@ -27,6 +28,21 @@ class MoleculerService {
27
28
  * `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
28
29
  */
29
30
  nodeRegistry = new system_1.HubNodeRegistry();
31
+ /**
32
+ * nodeId → the owner token of the INCARNATION whose manifest currently backs
33
+ * that node's capability registrations.
34
+ *
35
+ * A node id is not unique over time: `resolveRunnerId` is deterministic, so a
36
+ * runner replaced by an addon update reconnects as the same `hub/<runner>`
37
+ * node. Without an incarnation the predecessor's teardown and the successor's
38
+ * registration are the same `(cap, addonId)` key, and the teardown wins by
39
+ * arriving last — `recording`, `storage-evictable` and `recording-export`
40
+ * were unregistered from a healthy process for 7 hours on 2026-08-20.
41
+ * Populated only for transports that can observe an incarnation (UDS
42
+ * children); remote nodes stay ownerless and behave exactly as before.
43
+ * See `docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md`.
44
+ */
45
+ nodeOwners = new Map();
30
46
  /**
31
47
  * Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
32
48
  * agent-origin readiness deltas lost while the agent stayed connected.
@@ -405,16 +421,23 @@ class MoleculerService {
405
421
  const hubNodeId = this.brokerSafe.nodeID;
406
422
  const childNodeId = `${hubNodeId}/${child.childId}`;
407
423
  const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
408
- this.onRegisterNode(params);
409
- logger.info('UDS child registered manifest applied', { meta: { nodeId: childNodeId } });
424
+ // The UDS connection is the only thing that can tell two generations of
425
+ // the same runner apart carry its incarnation into the registry as
426
+ // the owner of every registration this manifest makes.
427
+ this.onRegisterNode(params, childOwnerToken(childNodeId, child.incarnation));
428
+ logger.info('UDS child registered — manifest applied', {
429
+ meta: { nodeId: childNodeId, incarnation: child.incarnation },
430
+ });
410
431
  });
411
432
  // E1: cleanup on child disconnect — same effect as `$node.disconnected`
412
433
  // for hub-local children. The Moleculer path stays for AGENT nodes.
413
- registry.onChildGone((childId) => {
434
+ registry.onChildGone((childId, incarnation) => {
414
435
  const hubNodeId = this.brokerSafe.nodeID;
415
436
  const childNodeId = `${hubNodeId}/${childId}`;
416
- logger.info('UDS child gone — removing from registry', { meta: { childId } });
417
- this.removeNodeFromRegistry(childNodeId);
437
+ logger.info('UDS child gone — removing from registry', {
438
+ meta: { childId, incarnation },
439
+ });
440
+ this.removeNodeFromRegistry(childNodeId, childOwnerToken(childNodeId, incarnation));
418
441
  });
419
442
  // B2: ingest UDS child logs into the hub's LoggingService so they appear
420
443
  // in the LogManager / admin-UI log stream alongside broker-forwarded logs.
@@ -449,6 +472,10 @@ class MoleculerService {
449
472
  // sent a $hub.registerNode manifest get cleaned up on disconnect.
450
473
  const bridgeBus = this.broker;
451
474
  bridgeBus.localBus.on('$node.disconnected', ({ node }) => {
475
+ // No owner token: this listener cannot observe WHICH incarnation died.
476
+ // For a hub-local child the UDS `onChildGone` is the authority (it holds
477
+ // the incarnation) and this call is refused if the id has been reclaimed;
478
+ // remote nodes register no owner at all, so they clean up as before.
452
479
  this.removeNodeFromRegistry(node.id);
453
480
  });
454
481
  // D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
@@ -682,10 +709,15 @@ class MoleculerService {
682
709
  * CapabilityRegistry update is a diff (atomic replace) rather than an
683
710
  * unconditional re-register — see `applyNodeManifest` for the rationale.
684
711
  */
685
- onRegisterNode(params) {
712
+ onRegisterNode(params, owner) {
686
713
  const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId);
714
+ const previousOwner = this.nodeOwners.get(params.nodeId);
687
715
  this.nodeRegistry.registerNode(params);
688
- this.applyNodeManifest(params, previousManifest);
716
+ if (owner === undefined)
717
+ this.nodeOwners.delete(params.nodeId);
718
+ else
719
+ this.nodeOwners.set(params.nodeId, owner);
720
+ this.applyNodeManifest(params, previousManifest, owner, previousOwner);
689
721
  // Notify AgentRegistryService to reconcile placement for bare-ID
690
722
  // agent nodes (no '/' = not a hub child worker, not the hub itself).
691
723
  // The handshake is the authoritative completeness signal — the full
@@ -717,7 +749,7 @@ class MoleculerService {
717
749
  * already; this method handles only `params.addons` (system caps).
718
750
  * Native-cap wiring into device-manager is done in a later task.
719
751
  */
720
- applyNodeManifest(params, previousManifest) {
752
+ applyNodeManifest(params, previousManifest, owner, previousOwner) {
721
753
  const { nodeId, addons } = params;
722
754
  const hubNodeId = this.brokerSafe.nodeID;
723
755
  const isLocalChild = nodeId.startsWith(hubNodeId + '/');
@@ -763,9 +795,23 @@ class MoleculerService {
763
795
  for (const [key, { addonId, capName }] of previous) {
764
796
  if (desired.has(key))
765
797
  continue;
766
- registry.unregisterProvider(capName, registryKeyFor(addonId));
798
+ // Present the token the DROPPED registration was made under — this node
799
+ // is dropping its own cap, and the registry refuses anything else.
800
+ registry.unregisterProvider(capName, registryKeyFor(addonId), previousOwner);
767
801
  this.nodeCallFns.delete(`${nodeId}::${capName}`);
768
802
  }
803
+ // ── TAKE OVER ── caps present in BOTH manifests when the node id has been
804
+ // reclaimed by a NEW incarnation. The registration is correct and stays
805
+ // (zero churn), but it is still stamped with the dead generation's token —
806
+ // leave it there and nothing the live generation can present would ever
807
+ // clean it up when IT dies. Re-stamp instead.
808
+ if (owner !== undefined && previousOwner !== undefined && owner !== previousOwner) {
809
+ for (const [key, { addonId, capName }] of desired) {
810
+ if (!previous.has(key))
811
+ continue;
812
+ registry.reassignProviderOwner(capName, registryKeyFor(addonId), owner);
813
+ }
814
+ }
769
815
  // ── REGISTER ── caps the new manifest applies that the previous one
770
816
  // did not. Caps present in BOTH sets are left untouched — zero churn,
771
817
  // no duplicate `registerProvider`, no spurious page/widget re-emit.
@@ -810,7 +856,7 @@ class MoleculerService {
810
856
  for (const methodName of Object.keys((0, system_1.expandCapMethods)(capDef))) {
811
857
  proxy[methodName] = (methodParams) => callFn(methodName, methodParams);
812
858
  }
813
- registry.registerProvider(capName, registryKey, proxy);
859
+ registry.registerProvider(capName, registryKey, proxy, owner);
814
860
  // Local-first singleton preference (UDS regression fix). A
815
861
  // `placement: 'any-node'` singleton (e.g. `pipeline-executor`) can
816
862
  // register on BOTH the hub-local forked child and a remote agent.
@@ -857,7 +903,17 @@ class MoleculerService {
857
903
  * Unregisters every cap the node's last manifest declared and emits
858
904
  * synthetic readiness-down events for each.
859
905
  */
860
- removeNodeFromRegistry(nodeId) {
906
+ removeNodeFromRegistry(nodeId, owner) {
907
+ const currentOwner = this.nodeOwners.get(nodeId);
908
+ if (currentOwner !== undefined && currentOwner !== owner) {
909
+ // The node id has been reclaimed by a newer incarnation since this
910
+ // teardown was scheduled. Tearing down here would unregister the LIVE
911
+ // process's capabilities and remove a node that is still connected.
912
+ this.logger.info('Node teardown ignored — node id reclaimed by a newer incarnation', {
913
+ meta: { nodeId, currentOwner, presentedOwner: owner ?? null },
914
+ });
915
+ return;
916
+ }
861
917
  const manifest = this.nodeRegistry.getNodeManifest(nodeId);
862
918
  if (!manifest)
863
919
  return; // node never sent a handshake — nothing to do
@@ -871,7 +927,7 @@ class MoleculerService {
871
927
  const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`;
872
928
  if (registry) {
873
929
  for (const capName of capabilities) {
874
- registry.unregisterProvider(capName, registryKey);
930
+ registry.unregisterProvider(capName, registryKey, owner);
875
931
  }
876
932
  }
877
933
  for (const capName of capabilities) {
@@ -893,6 +949,7 @@ class MoleculerService {
893
949
  }
894
950
  }
895
951
  }
952
+ this.nodeOwners.delete(nodeId);
896
953
  this.nodeRegistry.removeNode(nodeId);
897
954
  }
898
955
  findCallFn(nodeId, capabilityName) {
@@ -1083,6 +1140,18 @@ exports.MoleculerService = MoleculerService;
1083
1140
  // ---------------------------------------------------------------------------
1084
1141
  // Module-level helpers
1085
1142
  // ---------------------------------------------------------------------------
1143
+ /**
1144
+ * The owner token a hub-local UDS child's capability registrations carry.
1145
+ *
1146
+ * `nodeId` alone is NOT an identity over time — a runner replaced by an addon
1147
+ * update reconnects under the same deterministic `hub/<runner>` id — so the
1148
+ * connection's incarnation is what makes the pair unique. Exported for the
1149
+ * regression suite: the rule is only worth anything if register and unregister
1150
+ * derive the token the SAME way.
1151
+ */
1152
+ function childOwnerToken(childNodeId, incarnation) {
1153
+ return `${childNodeId}#${incarnation}`;
1154
+ }
1086
1155
  /**
1087
1156
  * E1: Adapt a child's UDS `ChildCapDescriptor[]` into a `RegisterNodeParams`
1088
1157
  * that `onRegisterNode` / `applyNodeManifest` can consume.
@@ -7,10 +7,16 @@ const types_1 = require("@camstack/types");
7
7
  * `latestVersion` (or first-sees a package/node that has one). Repeated polls
8
8
  * of the same latests are silent.
9
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.
10
+ * One event carries the entire currently-available list **of one node** — the
11
+ * scope that changed, never the union across nodes. The Notification Center
12
+ * turns that into "2 aggiornamenti addon disponibili su little-unraid" with
13
+ * that node's packages under it.
14
+ *
15
+ * The union was the shape until 2026-08-19, and on a three-node cluster one
16
+ * 6-hourly sweep sent the operator THREE pushes that each listed hub +
17
+ * MinidiGianluca + little-unraid and named no node: every scope's change
18
+ * re-emitted the whole held map, so the notifications differed by one line and
19
+ * read as identical. A list is only actionable if it belongs to somebody.
14
20
  *
15
21
  * A successful empty snapshot clears prior state for that scope; failed checks
16
22
  * should not call this method.
@@ -67,22 +73,22 @@ class UpdateAvailabilityEmitter {
67
73
  this.persistIfDirty();
68
74
  if (!latestChanged)
69
75
  return;
70
- this.emitList(target);
76
+ this.emitList(target, scope);
71
77
  }
72
78
  /** Publish a partial candidate set without treating omissions as up-to-date. */
73
79
  publishCandidates(candidates) {
74
- let latestChanged = false;
75
- const targets = new Set();
80
+ // Only the (target, node) pairs that actually moved are announced — a
81
+ // partial sweep of one node must not re-announce another node's list.
82
+ const changed = new Map();
76
83
  for (const candidate of candidates) {
77
- targets.add(candidate.target);
78
- if (this.note(candidate))
79
- latestChanged = true;
84
+ if (!this.note(candidate))
85
+ continue;
86
+ const scope = scopeOf(candidate);
87
+ changed.set(`${candidate.target}:${scope}`, { target: candidate.target, scope });
80
88
  }
81
89
  this.persistIfDirty();
82
- if (!latestChanged)
83
- return;
84
- for (const target of targets)
85
- this.emitList(target);
90
+ for (const { target, scope } of changed.values())
91
+ this.emitList(target, scope);
86
92
  }
87
93
  note(candidate) {
88
94
  const key = this.key(candidate);
@@ -111,21 +117,24 @@ class UpdateAvailabilityEmitter {
111
117
  held: Object.fromEntries(this.held),
112
118
  });
113
119
  }
114
- emitList(target) {
115
- const list = [...this.held.values()].filter((candidate) => candidate.target === target);
120
+ /** Announce ONE scope's full list. `scope` is the node id, `hub` for the hub. */
121
+ emitList(target, scope) {
122
+ const list = [...this.held.values()].filter((candidate) => candidate.target === target && scopeOf(candidate) === scope);
116
123
  if (list.length === 0)
117
124
  return;
125
+ // Every row names its node, the hub's included: the row a caller published
126
+ // without a nodeId is still ABOUT a node, and an unlabelled line in a
127
+ // cluster-wide list is unreadable.
118
128
  const packages = list.map((candidate) => ({
119
129
  packageName: candidate.packageName,
120
130
  currentVersion: candidate.currentVersion,
121
131
  latestVersion: candidate.latestVersion,
122
- ...(candidate.nodeId !== undefined ? { nodeId: candidate.nodeId } : {}),
132
+ nodeId: scope,
123
133
  }));
124
- const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== undefined));
125
134
  const head = list[0];
126
135
  this.eventBus.emit({
127
- id: `update.available:${target}:${packages
128
- .map((pkg) => `${pkg.nodeId ?? 'hub'}:${pkg.packageName}@${pkg.latestVersion}`)
136
+ id: `update.available:${target}:${scope}:${packages
137
+ .map((pkg) => `${pkg.packageName}@${pkg.latestVersion}`)
129
138
  .toSorted()
130
139
  .join(',')}`,
131
140
  timestamp: new Date(),
@@ -137,21 +146,22 @@ class UpdateAvailabilityEmitter {
137
146
  currentVersion: head.currentVersion,
138
147
  latestVersion: head.latestVersion,
139
148
  packages,
140
- ...(head.nodeId !== undefined ? { nodeId: head.nodeId } : {}),
141
- ...(nodeIds.length > 0 ? { nodeIds } : {}),
149
+ nodeId: scope,
150
+ nodeIds: [scope],
142
151
  },
143
152
  });
144
153
  }
145
154
  key(candidate) {
146
- return `${candidate.target}:${candidate.nodeId ?? 'hub'}:${candidate.packageName}`;
155
+ return `${candidate.target}:${scopeOf(candidate)}:${candidate.packageName}`;
147
156
  }
148
157
  }
149
158
  exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
159
+ /** The node an availability row belongs to. A row without one is the hub's. */
160
+ function scopeOf(candidate) {
161
+ return candidate.nodeId ?? 'hub';
162
+ }
150
163
  function stampNode(candidate, nodeId) {
151
164
  if (nodeId === undefined || candidate.nodeId !== undefined)
152
165
  return candidate;
153
166
  return { ...candidate, nodeId };
154
167
  }
155
- function unique(values) {
156
- return [...new Set(values)];
157
- }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildRestartMarker = buildRestartMarker;
4
+ exports.isServerUpdateReason = isServerUpdateReason;
5
+ exports.versionFromReason = versionFromReason;
6
+ /** Reason-string prefix every `RootUpdateService`-driven restart carries. */
7
+ const SERVER_UPDATE_PREFIX = 'server-update:';
8
+ /** The root package a version-less `server-update:` reason is about. */
9
+ const ROOT_PACKAGE_NAME = '@camstack/server';
10
+ function buildRestartMarker(input) {
11
+ const { requestedBy, update, requestedAt } = input;
12
+ const toVersion = update?.toVersion ?? versionFromReason(requestedBy);
13
+ const isUpdate = update !== undefined || (isServerUpdateReason(requestedBy) && toVersion !== undefined);
14
+ if (!isUpdate) {
15
+ return {
16
+ kind: 'manual',
17
+ requestedAt,
18
+ ...(requestedBy !== undefined ? { requestedBy } : {}),
19
+ };
20
+ }
21
+ return {
22
+ kind: 'framework-update',
23
+ requestedAt,
24
+ ...(requestedBy !== undefined ? { requestedBy } : {}),
25
+ packageName: update?.packageName ?? ROOT_PACKAGE_NAME,
26
+ ...(update?.fromVersion !== undefined ? { fromVersion: update.fromVersion } : {}),
27
+ ...(toVersion !== undefined ? { toVersion } : {}),
28
+ };
29
+ }
30
+ function isServerUpdateReason(requestedBy) {
31
+ return requestedBy !== undefined && requestedBy.startsWith(SERVER_UPDATE_PREFIX);
32
+ }
33
+ /** `server-update: @camstack/server@1.2.138` → `1.2.138`; `… manual restart` → none. */
34
+ function versionFromReason(requestedBy) {
35
+ if (requestedBy === undefined)
36
+ return undefined;
37
+ return /@([^@\s]+)$/.exec(requestedBy)?.[1];
38
+ }
@@ -238,7 +238,7 @@ async function bootManual(opts) {
238
238
  // AddonPackageService.restartServer for the marker + reconnect toast.
239
239
  const serverUpdateService = new server_update_service_1.ServerUpdateService({
240
240
  logger: loggingService.createLogger('ServerUpdate'),
241
- restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
241
+ restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
242
242
  eventBus: eventBusService,
243
243
  updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'server-update', loggingService.createLogger('UpdateAvailability')),
244
244
  });
@@ -1216,7 +1216,13 @@ var require_dist = __commonJS({
1216
1216
  fromVersion: runningVersion
1217
1217
  }
1218
1218
  });
1219
- this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
1219
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`, {
1220
+ packageName: this.spec.packageName,
1221
+ ...runningVersion !== null ? {
1222
+ fromVersion: runningVersion
1223
+ } : {},
1224
+ toVersion: target
1225
+ });
1220
1226
  return {
1221
1227
  accepted: true,
1222
1228
  targetVersion: target,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.138",
3
+ "version": "1.2.140",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,18 +33,18 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.83",
36
+ "@camstack/addon-admin-ui": "1.2.84",
37
37
  "@camstack/addon-agent-ui": "1.2.24",
38
38
  "@camstack/addon-auth": "1.2.26",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.22",
40
40
  "@camstack/addon-notifiers": "1.2.27",
41
- "@camstack/addon-pipeline": "1.2.101",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.84",
43
- "@camstack/addon-post-analysis": "1.2.101",
41
+ "@camstack/addon-pipeline": "1.2.103",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.85",
43
+ "@camstack/addon-post-analysis": "1.2.103",
44
44
  "@camstack/sdk": "1.2.25",
45
- "@camstack/system": "1.2.109",
46
- "@camstack/types": "1.2.91",
47
- "@camstack/ui-library": "1.2.62",
45
+ "@camstack/system": "1.2.111",
46
+ "@camstack/types": "1.2.92",
47
+ "@camstack/ui-library": "1.2.63",
48
48
  "@fastify/compress": "^9.0.0",
49
49
  "@fastify/cookie": "^11.0.2",
50
50
  "@fastify/cors": "^11.2.0",