@camstack/server 1.2.543 → 1.2.545

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.
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAgentHubRegistration = createAgentHubRegistration;
4
+ /**
5
+ * The agent's upward capability registration (D3), bound to the mesh join.
6
+ *
7
+ * The hub forgets an agent — its join AND its registration — on every
8
+ * `$node.disconnected` the HUB sees. That event is not symmetric: Moleculer
9
+ * marks a node offline on gossip from a third node, on a failed write, on a
10
+ * heartbeat timeout, and the agent, whose own view of the hub never broke,
11
+ * sees none of it. On 2026-10-14 (the D848 rollout) the hub flapped
12
+ * little-unraid and the Mac offline/online every 2–6 s for ~35 min while older
13
+ * agents still gossiped about them; each flap wiped the agent's capabilities
14
+ * on the hub, the agent re-joined (its next call came back "not
15
+ * authenticated") but never re-registered, and both stayed online with ZERO
16
+ * capabilities until a manual restart.
17
+ *
18
+ * So the registration follows the JOIN, not the agent's own `$node.connected`:
19
+ * - every join that settles in a state that lets calls through re-sends the
20
+ * complete manifest — a join runs exactly when the hub's view of this agent
21
+ * was reset (a new connection, or the hub said it lost us);
22
+ * - a registration the hub refuses as "cluster secret mismatch" while THIS
23
+ * connection's join proved the secret is not a mismatch: the hub forgot the
24
+ * join. The agent joins again, and that join re-registers;
25
+ * - the hub may ask for it (`$agent.requestRegistration`, sent on the hub's
26
+ * own `$node.connected`), which closes the window in which the agent makes
27
+ * no call that could tell it the hub forgot.
28
+ *
29
+ * Each send is ONE atomic `$hub.registerNode` carrying the full set; sends are
30
+ * coalesced (one in flight, at most one queued, the manifest built when it
31
+ * goes). Nothing waits on a timer: the triggers are events, the retry is the ack.
32
+ */
33
+ const system_1 = require("@camstack/system");
34
+ /** States in which a call to the hub is refused locally — nothing to send. */
35
+ const BLOCKING = new Set(['secret-mismatch', 'hub-unverified']);
36
+ function createAgentHubRegistration(opts) {
37
+ let waiting = false;
38
+ let sending = false;
39
+ let queuedReason = null;
40
+ const onRefused = (err) => {
41
+ if (!(0, system_1.isClusterSecretMismatchError)(err))
42
+ return; // aborted: shutting down
43
+ if (opts.join.client.state === 'authenticated') {
44
+ // This connection's join PROVED the secret, so the hub did not refuse
45
+ // the secret — it forgot the join (it saw this node disconnect). Join
46
+ // again; the settled join re-registers.
47
+ opts.log('[register-node] the hub refused the registration although this connection proved the cluster secret — it has forgotten the join; joining again');
48
+ opts.join.rejoin();
49
+ return;
50
+ }
51
+ opts.onSecretRefused?.(err);
52
+ };
53
+ const send = (reason) => {
54
+ if (sending) {
55
+ queuedReason = reason;
56
+ return;
57
+ }
58
+ sending = true;
59
+ const manifest = opts.buildManifest();
60
+ const bindings = manifest.addons.reduce((n, a) => n + a.capabilities.length, 0);
61
+ // Each attempt resolves the CURRENT broker: a retry that outlives a
62
+ // `reconnect()` must not keep calling the stopped one.
63
+ const broker = {
64
+ call: (action, params, callOpts) => opts.broker().call(action, params, callOpts),
65
+ };
66
+ (0, system_1.callRegisterNodeWithRetry)(broker, manifest, {
67
+ target: 'hub',
68
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
69
+ log: opts.log,
70
+ })
71
+ .then(() => {
72
+ opts.log(`[register-node] the hub acked this node's capabilities: ${manifest.addons.length} addon(s), ${bindings} binding(s) (${reason})`);
73
+ opts.onAcked?.();
74
+ })
75
+ .catch(onRefused)
76
+ .finally(() => {
77
+ sending = false;
78
+ const next = queuedReason;
79
+ queuedReason = null;
80
+ if (next !== null)
81
+ trigger(next);
82
+ });
83
+ };
84
+ const trigger = (reason) => {
85
+ // A send already waiting for the join builds its manifest when it goes.
86
+ if (waiting)
87
+ return;
88
+ waiting = true;
89
+ opts.join.client.whenCallable().then(() => {
90
+ waiting = false;
91
+ send(reason);
92
+ }, () => {
93
+ // Blocked (mismatch / unverified hub) or the join is still pending:
94
+ // the next settled join sends it (`onJoinSettled`).
95
+ waiting = false;
96
+ });
97
+ };
98
+ return {
99
+ trigger,
100
+ onJoinSettled(state) {
101
+ if (BLOCKING.has(state))
102
+ return;
103
+ // Every settled join means the hub's view of this node was reset (a new
104
+ // connection, or the hub said it lost us): the full set goes again.
105
+ trigger(`join settled: ${state}`);
106
+ },
107
+ };
108
+ }
@@ -49,6 +49,14 @@ function createAgentMeshJoin(opts) {
49
49
  opts.log?.(`mesh inbound: node "${nodeId}" is calling without having authenticated — ` +
50
50
  `tolerated while this agent has no cluster secret (first call: ${action})`);
51
51
  },
52
+ // D848: what the hub sent before the join settled is held, then decided — said here.
53
+ onHoldReport: (r) => {
54
+ const what = `${r.events} hub event(s) and ${r.calls} hub call(s) held ${r.heldForMs} ms before the mesh join settled`;
55
+ const overflow = r.overflow > 0 ? `; ${r.overflow} more refused because the hold was full` : '';
56
+ opts.log?.(r.outcome === 'delivered'
57
+ ? `[mesh-join] ${what}: delivered${overflow}`
58
+ : `[mesh-join] ${what}: REFUSED (join ${r.reason})${overflow}`);
59
+ },
52
60
  });
53
61
  const client = new system_1.MeshJoinClient({
54
62
  nodeId: opts.nodeId,
@@ -77,6 +85,10 @@ function createAgentMeshJoin(opts) {
77
85
  void runJoin();
78
86
  };
79
87
  const joinMiddleware = client.middleware(rejoin);
88
+ const eventScope = (0, system_1.createAgentEventScope)({
89
+ hubNodeId: HUB_NODE_ID,
90
+ ...(opts.log !== undefined ? { log: opts.log } : {}),
91
+ });
80
92
  const inboundMiddleware = (0, system_1.createMeshAccessMiddleware)({
81
93
  ownNodeId: opts.nodeId,
82
94
  authenticator,
@@ -95,7 +107,12 @@ function createAgentMeshJoin(opts) {
95
107
  client,
96
108
  authenticator,
97
109
  middleware: joinMiddleware,
98
- middlewares: [inboundMiddleware, joinMiddleware],
110
+ eventScope,
111
+ middlewares: [inboundMiddleware, joinMiddleware, eventScope.middleware],
112
+ rejoin() {
113
+ if (connected)
114
+ rejoin();
115
+ },
99
116
  attach(broker) {
100
117
  broker.localBus.on('$node.connected', (data) => {
101
118
  if (nodeIdOf(data) !== HUB_NODE_ID)
@@ -584,6 +584,20 @@ function createAgentService(deps) {
584
584
  return { success: true, addonId, pid: result.pid };
585
585
  },
586
586
  },
587
+ /**
588
+ * The hub asks for this node's complete capability set (sent on the
589
+ * hub's `$node.connected`). Answers at once; the registration itself
590
+ * goes out through the atomic `$hub.registerNode` (D3), after a fresh
591
+ * mesh join when the hub has forgotten this node's.
592
+ */
593
+ requestRegistration: {
594
+ handler() {
595
+ if (deps.requestRegistration === undefined)
596
+ return { ok: false };
597
+ deps.requestRegistration();
598
+ return { ok: true };
599
+ },
600
+ },
587
601
  /**
588
602
  * D3 subtree aggregation: a forked group-runner child calls this action
589
603
  * to deliver its complete capability manifest to the agent. The agent
@@ -44,6 +44,7 @@ const package_dir_utils_js_1 = require("../core/addon/package-dir-utils.js");
44
44
  const single_copy_cleanup_runner_js_1 = require("../single-copy-cleanup-runner.js");
45
45
  const addon_scan_js_1 = require("./addon-scan.js");
46
46
  const agent_addon_updates_js_1 = require("./agent-addon-updates.js");
47
+ const agent_hub_registration_js_1 = require("./agent-hub-registration.js");
47
48
  const agent_mesh_join_js_1 = require("./agent-mesh-join.js");
48
49
  const agent_config_js_1 = require("./agent-config.js");
49
50
  const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
@@ -177,8 +178,6 @@ async function startAgent(configPath) {
177
178
  // effect through `reconnect()` below, which re-reads the file.
178
179
  let currentSecret = config.secret ?? null;
179
180
  let joinState = 'pending';
180
- /** A registration waited for a join that had not settled; the join re-fires it. */
181
- let registrationDeferred = false;
182
181
  const meshJoin = (0, agent_mesh_join_js_1.createAgentMeshJoin)({
183
182
  nodeId: config.nodeId,
184
183
  secret: () => currentSecret,
@@ -195,9 +194,9 @@ async function startAgent(configPath) {
195
194
  else if (hubConnectionStateOverride === 'secret-mismatch') {
196
195
  hubConnectionStateOverride = null;
197
196
  }
198
- const blocking = state === 'secret-mismatch' || state === 'hub-unverified';
199
- if (!blocking && registrationDeferred)
200
- triggerUpwardRegistration();
197
+ // Every settled join re-sends the full manifest (D3, 2026-10-14): a join
198
+ // runs exactly when the hub's view of this node was reset.
199
+ registration.onJoinSettled(state);
201
200
  if (state === 'no-secret') {
202
201
  consoleLogger.warn("no cluster secret configured — the hub tolerates this agent only while its System → Cluster is in Transition. Copy the hub's cluster secret into this agent's setup");
203
202
  }
@@ -255,9 +254,8 @@ async function startAgent(configPath) {
255
254
  ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
256
255
  });
257
256
  meshJoin.attach(broker);
258
- // The boot-time `$node.connected` listeners belong to the old broker: let
259
- // the next settled join on this one re-send the manifest (D802).
260
- registrationDeferred = true;
257
+ // The new broker's hub connection runs a join, and its settle re-sends
258
+ // the manifest (`registration.onJoinSettled`).
261
259
  await broker.start();
262
260
  console.log('[Agent] Reconnected successfully');
263
261
  };
@@ -385,46 +383,32 @@ async function startAgent(configPath) {
385
383
  : { name: rootPackage.name, version: rootPackage.version });
386
384
  }
387
385
  /**
388
- * Fire (or re-fire) the upward `$hub.registerNode` registration carrying
389
- * the agent's complete subtree union. Called:
390
- * - after initial addon loading completes
391
- * - on every `$agent.registerNode` from a child
392
- * - on hub reconnect (via `$node.connected`)
386
+ * The upward `$hub.registerNode` registration carrying the agent's complete
387
+ * subtree union (`agent-hub-registration.ts`). Sent:
388
+ * - once boot has loaded every addon (`registration.ready()`);
389
+ * - on every `$agent.registerNode` from a child, and on `$agent.reload`;
390
+ * - on every mesh join that settles — a new hub connection, or the hub
391
+ * saying it lost this node — and when the hub asks for it
392
+ * (`$agent.requestRegistration`, on the hub's own `$node.connected`).
393
+ * D802: never before the join lets calls to the hub through, and the
394
+ * manifest is built AFTER the join, so it carries the legacy hash only for a
395
+ * hub that needs it.
393
396
  */
394
- function triggerUpwardRegistration() {
395
- // D802: the manifest goes to the hub only once the mesh join has settled —
396
- // never to a node that could not prove it is the hub — and it is built
397
- // AFTER the join, so it carries the legacy hash only for a hub that needs it.
398
- meshJoin.client.whenCallable().then(() => {
399
- registrationDeferred = false;
400
- sendUpwardRegistration();
401
- }, () => {
402
- // Blocked (mismatch / unverified hub) or the join is still pending:
403
- // the next settled join re-fires it (`onState`).
404
- registrationDeferred = true;
405
- });
406
- }
407
- function sendUpwardRegistration() {
408
- (0, system_1.callRegisterNodeWithRetry)(broker, aggregateManifest(), {
409
- target: 'hub',
410
- signal: registerAbortController.signal,
411
- log: (msg) => console.log(`[Agent] ${msg}`),
412
- })
413
- .then(() => {
414
- // The hub acked the manifest — the agent reached its registered state.
415
- // Run the single-copy GC sweep once (first ack only; no-op after).
416
- confirmAgentBootHealthy();
417
- })
418
- .catch((err) => {
419
- if ((0, system_1.isClusterSecretMismatchError)(err)) {
420
- consoleLogger.error("hub registration rejected: cluster secret mismatch — enter the hub's cluster secret (System → Cluster) in this agent's setup (agent.json) and restart the agent");
421
- // Surface the mismatch in the UI status.
422
- hubConnectionStateOverride = 'secret-mismatch';
423
- return;
424
- }
425
- // abort (shutdown) — preserve prior void behaviour: ignore.
426
- });
427
- }
397
+ const registration = (0, agent_hub_registration_js_1.createAgentHubRegistration)({
398
+ join: meshJoin,
399
+ buildManifest: () => aggregateManifest(),
400
+ broker: () => broker,
401
+ signal: registerAbortController.signal,
402
+ log: (msg) => console.log(`[Agent] ${msg}`),
403
+ // The hub acked the manifest — the agent reached its registered state.
404
+ // Run the single-copy GC sweep once (first ack only; no-op after).
405
+ onAcked: () => confirmAgentBootHealthy(),
406
+ onSecretRefused: () => {
407
+ consoleLogger.error("hub registration rejected: cluster secret mismatch — enter the hub's cluster secret (System → Cluster) in this agent's setup (agent.json) and restart the agent");
408
+ // Surface the mismatch in the UI status.
409
+ hubConnectionStateOverride = 'secret-mismatch';
410
+ },
411
+ });
428
412
  const consoleLogger = {
429
413
  info: (msg) => console.log(`[Agent] ${msg}`),
430
414
  warn: (msg) => console.warn(`[Agent] ${msg}`),
@@ -543,14 +527,19 @@ async function startAgent(configPath) {
543
527
  // so the hub's `CapRouteResolver` can route to what this node now has.
544
528
  // Hung on the existing `$agent.reload` trigger; no timer (D3).
545
529
  reconcileNodeRegistration: () => {
546
- triggerUpwardRegistration();
530
+ registration.trigger('addons reloaded');
531
+ },
532
+ // The hub saw this node (re)connect and asks for its capability set —
533
+ // the hub may have dropped it on a disconnect this agent never saw.
534
+ requestRegistration: () => {
535
+ registration.trigger('the hub asked');
547
536
  },
548
537
  // D3 subtree aggregation: when a group-runner child delivers its manifest
549
538
  // via `$agent.registerNode`, merge it into the local subtree registry and
550
539
  // immediately re-register the complete union with the hub.
551
540
  onChildRegistered: (params) => {
552
541
  subtree.registerNode(params);
553
- triggerUpwardRegistration();
542
+ registration.trigger(`child ${params.nodeId} registered`);
554
543
  },
555
544
  expectedClusterSecretHash: config.secret ? (0, system_1.hashClusterSecret)(config.secret) : undefined,
556
545
  meshAuth: meshJoin.authenticator,
@@ -686,13 +675,13 @@ async function startAgent(configPath) {
686
675
  const childNodeId = `${agentNodeId}/${child.childId}`;
687
676
  const childParams = buildAgentChildUdsManifest(childNodeId, child.childId, child.caps);
688
677
  subtree.registerNode(childParams);
689
- triggerUpwardRegistration();
678
+ registration.trigger(`child ${childNodeId} connected`);
690
679
  consoleLogger.info(`UDS child registered — subtree updated: ${childNodeId}`);
691
680
  });
692
681
  agentUdsRegistry.onChildGone((childId) => {
693
682
  const childNodeId = `${agentNodeId}/${childId}`;
694
683
  subtree.removeNode(childNodeId);
695
- triggerUpwardRegistration();
684
+ registration.trigger(`child ${childNodeId} gone`);
696
685
  consoleLogger.info(`UDS child gone — subtree updated: ${childNodeId}`);
697
686
  });
698
687
  // B2: forward UDS child logs onward to the hub's log-receiver service so
@@ -883,8 +872,9 @@ async function startAgent(configPath) {
883
872
  // ── D3: Fire the initial upward hub registration carrying the agent's
884
873
  // complete in-process manifest (+ any children that already registered).
885
874
  // Group-runner children call `$agent.registerNode` and trigger a re-fire
886
- // via `onChildRegistered`; hub reconnects re-fire via `$node.connected`.
887
- triggerUpwardRegistration();
875
+ // via `onChildRegistered`; every settled mesh join re-fires it
876
+ // (`registration.onJoinSettled`), and so does the hub's ask.
877
+ registration.trigger('boot');
888
878
  // Fire `onHubReachable()` on every in-process addon as soon as the hub
889
879
  // node connects to our broker — this is the safe point for ctx.api.* calls
890
880
  // into hub-provided capabilities. Forked children get the same hook via
@@ -899,9 +889,8 @@ async function startAgent(configPath) {
899
889
  if (hubConnectionStateOverride === 'secret-mismatch') {
900
890
  hubConnectionStateOverride = null;
901
891
  }
902
- // Re-register with the hub on reconnect — the hub may have restarted and
903
- // lost the agent's manifest. D3 idempotent-replace: safe to re-fire.
904
- triggerUpwardRegistration();
892
+ // No re-registration here: this connection runs a mesh join, and the
893
+ // join's settle re-sends the manifest (`registration.onJoinSettled`).
905
894
  if (hubReachableFired)
906
895
  return;
907
896
  hubReachableFired = true;
@@ -265,15 +265,25 @@ function getLocalIps() {
265
265
  return ips;
266
266
  }
267
267
  /** {@link computeTopology}, keeping the live entries the projection consumed. */
268
- async function computeTopologyPass(agentRegistry, addonRegistry, getNodeRootPackage) {
269
- const [nodes, liveNodes] = await computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage);
268
+ async function computeTopologyPass(agentRegistry, addonRegistry, getNodeRootPackage, isNodeRegistered) {
269
+ const [nodes, liveNodes] = await computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage, isNodeRegistered);
270
270
  return { nodes, liveNodes };
271
271
  }
272
- async function computeTopology(agentRegistry, addonRegistry, getNodeRootPackage) {
273
- const [nodes] = await computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage);
272
+ async function computeTopology(agentRegistry, addonRegistry, getNodeRootPackage, isNodeRegistered) {
273
+ const [nodes] = await computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage, isNodeRegistered);
274
274
  return nodes;
275
275
  }
276
- async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage) {
276
+ /**
277
+ * The `capabilitiesRegistered` field of an ONLINE agent row: whether the hub
278
+ * holds that agent's `registerNode` manifest. Absent for the hub, offline rows
279
+ * and when no lookup is wired.
280
+ */
281
+ function registrationField(nodeId, isHub, isOnline, isNodeRegistered) {
282
+ if (isHub || !isOnline || isNodeRegistered === undefined)
283
+ return {};
284
+ return { capabilitiesRegistered: isNodeRegistered(nodeId) };
285
+ }
286
+ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRootPackage, isNodeRegistered) {
277
287
  // The live cluster walk and the durable offline-node history read NOTHING
278
288
  // from each other, and the history read is one settings-store round trip —
279
289
  // measured on the live hub at 14.8–18.3 s while the store is stalled. Awaited
@@ -419,6 +429,10 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
419
429
  // Mesh mTLS Release A: observed by the hub's transporter (certificate CN),
420
430
  // never self-reported by the agent. Hub row and unknown: no badge.
421
431
  ...(node.isHub ? {} : meshTransportField(node.info.id)),
432
+ // An agent online in the mesh whose capability registration the hub
433
+ // dropped (a disconnect only the hub saw) is reported, never silently
434
+ // capless (2026-10-14).
435
+ ...registrationField(node.info.id, node.isHub, node.isOnline !== false, isNodeRegistered),
422
436
  };
423
437
  });
424
438
  // A2: union in OFFLINE rows for every persisted node no longer present in the
@@ -474,7 +488,7 @@ function createNodeRootPackageLookup(moleculer, serverUpdate) {
474
488
  function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRootPackage, logger) {
475
489
  const broker = moleculer.broker;
476
490
  return {
477
- topology: async () => computeTopology(agentRegistry, addonRegistry, getNodeRootPackage),
491
+ topology: async () => computeTopology(agentRegistry, addonRegistry, getNodeRootPackage, (nodeId) => moleculer.isNodeRegistered(nodeId)),
478
492
  directory: async () => agentRegistry.listNodeDirectory(),
479
493
  deployAddon: async () => {
480
494
  // Placeholder — actual deployment orchestration TBD
@@ -8280,6 +8280,22 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
8280
8280
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8281
8281
  return p.setCameraStepToggle(input);
8282
8282
  }),
8283
+ getCameraDetectionSource: trpc_middleware_js_1.protectedProcedure
8284
+ .input(types_97.pipelineOrchestratorCapability.methods.getCameraDetectionSource.input.loose())
8285
+ .output(types_97.pipelineOrchestratorCapability.methods.getCameraDetectionSource.output)
8286
+ .query(async ({ input, ctx }) => {
8287
+ const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8288
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8289
+ return p.getCameraDetectionSource(input);
8290
+ }),
8291
+ setCameraDetectionSource: trpc_middleware_js_1.adminProcedure
8292
+ .input(types_97.pipelineOrchestratorCapability.methods.setCameraDetectionSource.input.loose())
8293
+ .output(types_97.pipelineOrchestratorCapability.methods.setCameraDetectionSource.output)
8294
+ .mutation(async ({ input, ctx }) => {
8295
+ const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8296
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8297
+ return p.setCameraDetectionSource(input);
8298
+ }),
8283
8299
  getCameraStepOverrides: trpc_middleware_js_1.protectedProcedure
8284
8300
  .input(types_97.pipelineOrchestratorCapability.methods.getCameraStepOverrides.input.loose())
8285
8301
  .output(types_97.pipelineOrchestratorCapability.methods.getCameraStepOverrides.output)
@@ -10565,6 +10581,13 @@ function createCapRouter_streamBroker(getProvider, createRemoteProxy) {
10565
10581
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10566
10582
  return p.listClients(methodInput);
10567
10583
  }),
10584
+ getLiveViewerCounts: trpc_middleware_js_1.protectedProcedure
10585
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
10586
+ .output(types_121.streamBrokerCapability.methods.getLiveViewerCounts.output)
10587
+ .query(async ({ input, ctx }) => {
10588
+ const p = resolveProvider('stream-broker', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
10589
+ return p.getLiveViewerCounts();
10590
+ }),
10568
10591
  killClient: trpc_middleware_js_1.adminProcedure
10569
10592
  .input(types_121.streamBrokerCapability.methods.killClient.input.loose())
10570
10593
  .output(types_121.streamBrokerCapability.methods.killClient.output)
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * The hub asks a (re)connected agent for its capability set.
4
+ *
5
+ * The hub drops an agent's registration — and its mesh join — on every
6
+ * `$node.disconnected` it sees (`removeNodeFromRegistry`, `meshGuard.forget`).
7
+ * Moleculer raises that event on the HUB alone when a third node gossips the
8
+ * agent offline, when a write to it fails, or when its heartbeat is late; the
9
+ * agent's own view of the hub never breaks, so nothing on the agent says "send
10
+ * your manifest again". When the hub then sees the agent again
11
+ * (`$node.connected`), it holds an online node with NO capabilities. On
12
+ * 2026-10-14 little-unraid and the Mac sat that way for 50 minutes.
13
+ *
14
+ * So on every `$node.connected` of an agent the hub calls
15
+ * `$agent.requestRegistration`: one RPC, on the event, no timer (D3). The
16
+ * agent answers through the one atomic `$hub.registerNode`, joining again
17
+ * first when the hub forgot its join. An agent that predates the action cannot
18
+ * be asked: that is said, with whether the hub holds its registration, so an
19
+ * online-but-capless node is never silent.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.AGENT_REQUEST_REGISTRATION_ACTION = void 0;
23
+ exports.requestAgentRegistration = requestAgentRegistration;
24
+ exports.AGENT_REQUEST_REGISTRATION_ACTION = '$agent.requestRegistration';
25
+ function errorType(err) {
26
+ if (typeof err !== 'object' || err === null)
27
+ return null;
28
+ const type = Reflect.get(err, 'type');
29
+ return typeof type === 'string' ? type : null;
30
+ }
31
+ function errorText(err) {
32
+ return err instanceof Error ? err.message : String(err);
33
+ }
34
+ /** Ask `nodeId` to re-send its complete capability manifest. Never throws. */
35
+ async function requestAgentRegistration(deps, nodeId) {
36
+ try {
37
+ await deps.call(exports.AGENT_REQUEST_REGISTRATION_ACTION, {}, { nodeID: nodeId });
38
+ return 'requested';
39
+ }
40
+ catch (err) {
41
+ const type = errorType(err);
42
+ const unsupported = type === 'SERVICE_NOT_FOUND' || type === 'SERVICE_NOT_AVAILABLE';
43
+ const registered = deps.isRegistered(nodeId);
44
+ const meta = { registered, error: errorText(err) };
45
+ if (registered) {
46
+ deps.info(unsupported
47
+ ? 'agent cannot be asked for its capabilities (it predates $agent.requestRegistration) — the hub holds its registration'
48
+ : 'asking the agent for its capabilities failed — the hub holds its registration', nodeId, meta);
49
+ }
50
+ else {
51
+ deps.warn(unsupported
52
+ ? 'agent is online and the hub holds NO capability registration for it; it predates $agent.requestRegistration and cannot be asked — if it stays without capabilities, restart it'
53
+ : 'agent is online and the hub holds NO capability registration for it; asking it to re-register failed', nodeId, meta);
54
+ }
55
+ return unsupported ? 'unsupported' : 'failed';
56
+ }
57
+ }
@@ -9,6 +9,7 @@ const types_1 = require("@camstack/types");
9
9
  const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
10
10
  const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
11
11
  const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
12
+ const agent_registration_request_js_1 = require("./agent-registration-request.js");
12
13
  const hub_mesh_guard_js_1 = require("./hub-mesh-guard.js");
13
14
  const cap_call_fn_js_1 = require("./cap-call-fn.js");
14
15
  const cap_route_authority_js_1 = require("./cap-route-authority.js");
@@ -656,6 +657,15 @@ class MoleculerService {
656
657
  if (nodeId === this.brokerSafe.nodeID || nodeId.includes('/'))
657
658
  return;
658
659
  void this.pullAgentReadinessSnapshot(nodeId);
660
+ // The hub may have dropped this agent's registration on a disconnect
661
+ // only the hub saw (gossip, a failed write): ask it for its full set
662
+ // (`agent-registration-request.ts`, 2026-10-14).
663
+ void (0, agent_registration_request_js_1.requestAgentRegistration)({
664
+ call: (action, params, opts) => this.brokerSafe.call(action, params, opts),
665
+ isRegistered: (id) => this.isNodeRegistered(id),
666
+ warn: (message, id, meta) => this.logger.warn(message, { tags: { nodeId: id }, meta }),
667
+ info: (message, id, meta) => this.logger.info(message, { tags: { nodeId: id }, meta }),
668
+ }, nodeId);
659
669
  });
660
670
  this.agentReadinessPullTimer = setInterval(() => {
661
671
  for (const nodeId of this.nodeRegistry.listNodeIds()) {
@@ -1275,6 +1285,15 @@ class MoleculerService {
1275
1285
  listClusterNativeCapsForDevice(deviceId) {
1276
1286
  return this.nodeRegistry.listNativeCapEntriesForDevice(deviceId);
1277
1287
  }
1288
+ /**
1289
+ * Does the hub hold a capability registration for `nodeId`? `false` for an
1290
+ * agent that is online in the mesh but whose manifest was dropped (a
1291
+ * disconnect only the hub saw) and not yet re-sent — reported as such by
1292
+ * `nodes.topology` (`capabilitiesRegistered`), never silently capless.
1293
+ */
1294
+ isNodeRegistered(nodeId) {
1295
+ return this.nodeRegistry.getNodeManifest(nodeId) !== undefined;
1296
+ }
1278
1297
  /**
1279
1298
  * The root-package identity a node reported in its `registerNode`
1280
1299
  * manifest (runtime-updatable node packages: version visibility), or null
@@ -39,17 +39,21 @@ class TopologyEmitterService {
39
39
  agentRegistry;
40
40
  addonRegistry;
41
41
  getNodeRootPackage;
42
+ isNodeRegistered;
42
43
  unsubscribers = [];
43
44
  debounceTimer = null;
44
45
  heartbeatTimer = null;
45
46
  emitting = false;
46
47
  constructor(eventBus, agentRegistry, addonRegistry,
47
48
  /** Root-package identity per node (version visibility) — optional. */
48
- getNodeRootPackage) {
49
+ getNodeRootPackage,
50
+ /** Whether the hub holds each agent's capability registration — optional. */
51
+ isNodeRegistered) {
49
52
  this.eventBus = eventBus;
50
53
  this.agentRegistry = agentRegistry;
51
54
  this.addonRegistry = addonRegistry;
52
55
  this.getNodeRootPackage = getNodeRootPackage;
56
+ this.isNodeRegistered = isNodeRegistered;
53
57
  }
54
58
  onModuleInit() {
55
59
  for (const category of LIFECYCLE_CATEGORIES) {
@@ -89,7 +93,7 @@ class TopologyEmitterService {
89
93
  return;
90
94
  this.emitting = true;
91
95
  try {
92
- const { nodes, liveNodes } = await (0, cap_providers_1.computeTopologyPass)(this.agentRegistry, this.addonRegistry, this.getNodeRootPackage);
96
+ const { nodes, liveNodes } = await (0, cap_providers_1.computeTopologyPass)(this.agentRegistry, this.addonRegistry, this.getNodeRootPackage, this.isNodeRegistered);
93
97
  this.eventBus.emit({
94
98
  id: (0, node_crypto_1.randomUUID)(),
95
99
  timestamp: new Date(),
@@ -257,7 +257,7 @@ async function bootManual(opts) {
257
257
  eventBus: eventBusService,
258
258
  updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, 'server-update', loggingService.createLogger('UpdateAvailability')),
259
259
  });
260
- const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService, (0, cap_providers_1.createNodeRootPackageLookup)(moleculerService, serverUpdateService));
260
+ const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService, (0, cap_providers_1.createNodeRootPackageLookup)(moleculerService, serverUpdateService), (nodeId) => moleculerService.isNodeRegistered(nodeId));
261
261
  const postBootService = new post_boot_service_1.PostBootService(addonRegistryService, eventBusService, loggingService);
262
262
  // ---- Update availability poller ---------------------------------------
263
263
  // "Tell me, don't install." Deliberately NOT gated on the auto-update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.543",
3
+ "version": "1.2.545",
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.409",
42
- "@camstack/addon-agent-ui": "1.2.224",
41
+ "@camstack/addon-admin-ui": "1.2.410",
42
+ "@camstack/addon-agent-ui": "1.2.225",
43
43
  "@camstack/addon-auth": "1.2.141",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
45
45
  "@camstack/addon-notifiers": "1.2.134",
46
- "@camstack/addon-pipeline": "1.2.361",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.311",
48
- "@camstack/addon-post-analysis": "1.2.360",
46
+ "@camstack/addon-pipeline": "1.2.362",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.312",
48
+ "@camstack/addon-post-analysis": "1.2.361",
49
49
  "@camstack/develop": "0.1.5",
50
50
  "@camstack/sdk": "1.2.131",
51
- "@camstack/system": "1.2.419",
52
- "@camstack/types": "1.2.357",
53
- "@camstack/ui-library": "1.2.281",
51
+ "@camstack/system": "1.2.421",
52
+ "@camstack/types": "1.2.358",
53
+ "@camstack/ui-library": "1.2.282",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",