@camstack/server 1.2.382 → 1.2.384

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.
@@ -465,6 +465,7 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
465
465
  const broker = moleculer.broker;
466
466
  return {
467
467
  topology: async () => computeTopology(agentRegistry, addonRegistry, getNodeRootPackage),
468
+ directory: async () => agentRegistry.listNodeDirectory(),
468
469
  deployAddon: async () => {
469
470
  // Placeholder — actual deployment orchestration TBD
470
471
  return { success: true };
@@ -4573,6 +4573,33 @@ function createCapRouter_llmRuntime(getProvider, createRemoteProxy) {
4573
4573
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4574
4574
  return p.getDiskUsage(methodInput);
4575
4575
  }),
4576
+ cancel: trpc_middleware_js_1.adminProcedure
4577
+ .input(types_58.llmRuntimeCapability.methods.cancel.input.loose())
4578
+ .output(types_58.llmRuntimeCapability.methods.cancel.output)
4579
+ .mutation(async ({ input, ctx }) => {
4580
+ const { nodeId, ...methodInput } = input;
4581
+ const p = resolveProvider('llm-runtime', nodeId, () => getProvider(ctx), createRemoteProxy);
4582
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4583
+ return p.cancel(methodInput);
4584
+ }),
4585
+ appleAvailability: trpc_middleware_js_1.adminProcedure
4586
+ .input(types_58.llmRuntimeCapability.methods.appleAvailability.input.loose())
4587
+ .output(types_58.llmRuntimeCapability.methods.appleAvailability.output)
4588
+ .query(async ({ input, ctx }) => {
4589
+ const { nodeId, ...methodInput } = input;
4590
+ const p = resolveProvider('llm-runtime', nodeId, () => getProvider(ctx), createRemoteProxy);
4591
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4592
+ return p.appleAvailability(methodInput);
4593
+ }),
4594
+ completeApple: trpc_middleware_js_1.adminProcedure
4595
+ .input(types_58.llmRuntimeCapability.methods.completeApple.input.loose())
4596
+ .output(types_58.llmRuntimeCapability.methods.completeApple.output)
4597
+ .mutation(async ({ input, ctx }) => {
4598
+ const { nodeId, ...methodInput } = input;
4599
+ const p = resolveProvider('llm-runtime', nodeId, () => getProvider(ctx), createRemoteProxy);
4600
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4601
+ return p.completeApple(methodInput);
4602
+ }),
4576
4603
  });
4577
4604
  }
4578
4605
  function createCapRouter_loadContribution(getProvider, createRemoteProxy) {
@@ -5532,6 +5559,14 @@ function createCapRouter_nodes(getProvider, _createRemoteProxy) {
5532
5559
  const p = requireCapProvider('nodes', () => getProvider(ctx));
5533
5560
  return p.topology();
5534
5561
  }),
5562
+ directory: trpc_middleware_js_1.adminProcedure
5563
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
5564
+ .output(types_79.nodesCapability.methods.directory.output)
5565
+ .query(async ({ input, ctx }) => {
5566
+ void input;
5567
+ const p = requireCapProvider('nodes', () => getProvider(ctx));
5568
+ return p.directory();
5569
+ }),
5535
5570
  deployAddon: trpc_middleware_js_1.adminProcedure
5536
5571
  .input(types_79.nodesCapability.methods.deployAddon.input.loose())
5537
5572
  .output(types_79.nodesCapability.methods.deployAddon.output)
@@ -42,6 +42,8 @@ const system_1 = require("@camstack/system");
42
42
  const types_1 = require("@camstack/types");
43
43
  const agent_addon_backfill_1 = require("./agent-addon-backfill");
44
44
  const agent_delivery_ledger_1 = require("./agent-delivery-ledger");
45
+ const cluster_node_history_mirror_1 = require("./cluster-node-history-mirror");
46
+ const node_directory_1 = require("./node-directory");
45
47
  /** Turn a forget outcome into the sentence the operator reads. */
46
48
  function describeForgetOutcome(nodeId, history, registryRemoved, stillListed) {
47
49
  const parts = [];
@@ -194,6 +196,11 @@ class AgentRegistryService {
194
196
  * fakes) stay untouched; a null store means "history disabled" (best-effort).
195
197
  */
196
198
  historyStore = null;
199
+ /**
200
+ * The history as last read successfully — fed by every topology pass
201
+ * (`getClusterNodeHistory`), served to `nodes.directory` (D637).
202
+ */
203
+ historyMirror = null;
197
204
  /**
198
205
  * Scoped logger, wired post-construction from `manual-boot` for the same
199
206
  * reason as the seams above (the logging service is built earlier but the
@@ -240,6 +247,7 @@ class AgentRegistryService {
240
247
  /** Wire the durable offline-node history store (A1). */
241
248
  setClusterNodeHistoryStore(store) {
242
249
  this.historyStore = store;
250
+ this.historyMirror = new cluster_node_history_mirror_1.ClusterNodeHistoryMirror(() => store.tryGetAll());
243
251
  }
244
252
  /** Wire the scoped logger (boot order — see the field). */
245
253
  setLogger(logger) {
@@ -291,7 +299,9 @@ class AgentRegistryService {
291
299
  * Returns `[]` when no history store is wired (unit fakes).
292
300
  */
293
301
  async getClusterNodeHistory() {
294
- return this.historyStore ? this.historyStore.getAll() : [];
302
+ // A fresh read each pass (topology semantics unchanged), and it feeds the
303
+ // mirror `listNodeDirectory` serves.
304
+ return this.historyMirror ? ((await this.historyMirror.refresh()) ?? []) : [];
295
305
  }
296
306
  /**
297
307
  * Permanently forget a node (inline "Forget node") — and report what that
@@ -335,6 +345,8 @@ class AgentRegistryService {
335
345
  const historyOutcome = this.historyStore
336
346
  ? await this.historyStore.forget(nodeId)
337
347
  : 'store-unavailable';
348
+ // The mirror still lists the forgotten node: the next directory re-reads.
349
+ this.historyMirror?.invalidate();
338
350
  const registryRemoved = this.removeFromNodeCatalog(nodeId);
339
351
  // Its intended set is gone with the row, so the back-fill breaker's memory
340
352
  // of failed deliveries to it is meaningless — drop it too.
@@ -890,6 +902,17 @@ class AgentRegistryService {
890
902
  listNodeLiveness() {
891
903
  return toNodeLiveness(this.moleculer.broker.registry.getNodeList({ onlyAvailable: false }));
892
904
  }
905
+ /**
906
+ * The cheap node list for pickers (D637): the in-memory liveness above plus
907
+ * the durable history — no per-node RPC, unlike `computeTopology` (32–51 s).
908
+ * The history comes from the mirror the topology pass keeps fresh, so a
909
+ * picker open normally costs no settings round trip either. No history
910
+ * store wired, or no read that ever answered, is `complete: false`.
911
+ */
912
+ async listNodeDirectory() {
913
+ const history = this.historyMirror ? await this.historyMirror.current() : null;
914
+ return (0, node_directory_1.toNodeDirectory)(this.listNodeLiveness(), history);
915
+ }
893
916
  async listNodes() {
894
917
  const registry = this.moleculer.broker.registry;
895
918
  // `onlyAvailable: false` intentionally includes nodes Moleculer still
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClusterNodeHistoryMirror = exports.HISTORY_MIRROR_MAX_AGE_MS = void 0;
4
+ /** Two topology heartbeats: a mirror older than this was not being fed. */
5
+ exports.HISTORY_MIRROR_MAX_AGE_MS = 60_000;
6
+ class ClusterNodeHistoryMirror {
7
+ read;
8
+ now;
9
+ maxAgeMs;
10
+ entry = null;
11
+ inFlight = null;
12
+ /** Bumped by `invalidate()`: a read started before it must not land. */
13
+ generation = 0;
14
+ constructor(read, now = Date.now, maxAgeMs = exports.HISTORY_MIRROR_MAX_AGE_MS) {
15
+ this.read = read;
16
+ this.now = now;
17
+ this.maxAgeMs = maxAgeMs;
18
+ }
19
+ /** Read the store now (sharing any read in flight). Returns THIS read's answer. */
20
+ refresh() {
21
+ if (this.inFlight !== null)
22
+ return this.inFlight;
23
+ const generation = this.generation;
24
+ const flight = this.read().then((rows) => {
25
+ if (rows !== null && generation === this.generation) {
26
+ this.entry = { rows, readAt: this.now() };
27
+ }
28
+ return rows;
29
+ });
30
+ const settled = flight.finally(() => {
31
+ if (this.inFlight === settled)
32
+ this.inFlight = null;
33
+ });
34
+ this.inFlight = settled;
35
+ return settled;
36
+ }
37
+ /** The mirror if fresh; otherwise a read, falling back to the last successful one. */
38
+ async current() {
39
+ const entry = this.entry;
40
+ if (entry !== null && this.now() - entry.readAt <= this.maxAgeMs)
41
+ return entry.rows;
42
+ const rows = await this.refresh();
43
+ return rows ?? this.entry?.rows ?? null;
44
+ }
45
+ /** Drop the mirror (a node was forgotten): the next `current()` must re-read. */
46
+ invalidate() {
47
+ this.generation += 1;
48
+ this.entry = null;
49
+ this.inFlight = null;
50
+ }
51
+ }
52
+ exports.ClusterNodeHistoryMirror = ClusterNodeHistoryMirror;
@@ -321,9 +321,19 @@ class ClusterNodeHistoryStore {
321
321
  }
322
322
  /** Read the full persisted history, validated. Malformed rows are dropped. */
323
323
  async getAll() {
324
+ return (await this.tryGetAll()) ?? [];
325
+ }
326
+ /**
327
+ * Read the full persisted history, validated, or `null` when the store did
328
+ * not ANSWER (unavailable, or the query threw). A caller that must not claim
329
+ * a node absent from an unanswered read (D315) uses this; `getAll` folds the
330
+ * `null` into `[]` for the callers that only render what exists. Malformed
331
+ * rows are dropped (and logged) — the rest of the answer still stands.
332
+ */
333
+ async tryGetAll() {
324
334
  const store = await this.ensureDeclared();
325
335
  if (!store)
326
- return [];
336
+ return null;
327
337
  try {
328
338
  const rows = await store.query({ collection: COLLECTION });
329
339
  const result = [];
@@ -340,8 +350,8 @@ class ClusterNodeHistoryStore {
340
350
  return result;
341
351
  }
342
352
  catch (err) {
343
- this.logger.warn('getAll failed (best-effort)', { meta: { error: (0, types_1.errMsg)(err) } });
344
- return [];
353
+ this.logger.warn('history read failed (best-effort)', { meta: { error: (0, types_1.errMsg)(err) } });
354
+ return null;
345
355
  }
346
356
  }
347
357
  /**
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toNodeDirectory = toNodeDirectory;
4
+ /**
5
+ * The cheap node list (D637): live liveness plus the durable history, merged.
6
+ *
7
+ * - A node Moleculer still lists keeps Moleculer's liveness verdict, named
8
+ * from the history when the history knows it.
9
+ * - A node known ONLY from the history (Moleculer already purged it) is
10
+ * listed OFFLINE — never dropped.
11
+ * - `history === null` = unreadable, so the answer is INCOMPLETE and a picker
12
+ * must not call a missing id removed (D315).
13
+ */
14
+ function toNodeDirectory(live, history) {
15
+ const rows = history ?? [];
16
+ const names = new Map(rows.map((row) => [row.id, row.name]));
17
+ const hubs = new Set(rows.filter((row) => row.isHub).map((row) => row.id));
18
+ const entries = new Map();
19
+ for (const node of live) {
20
+ entries.set(node.id, {
21
+ id: node.id,
22
+ name: displayName(node.id, names.get(node.id)),
23
+ isHub: node.isHub || hubs.has(node.id),
24
+ isOnline: node.isOnline,
25
+ });
26
+ }
27
+ for (const row of rows) {
28
+ if (entries.has(row.id))
29
+ continue;
30
+ entries.set(row.id, {
31
+ id: row.id,
32
+ name: displayName(row.id, row.name),
33
+ isHub: row.isHub,
34
+ isOnline: false,
35
+ });
36
+ }
37
+ const nodes = [...entries.values()].sort(compareDirectoryEntries);
38
+ return { nodes, complete: history !== null };
39
+ }
40
+ function displayName(id, name) {
41
+ return name !== undefined && name.length > 0 ? name : id;
42
+ }
43
+ /** Hub first, then online before offline, then by name. */
44
+ function compareDirectoryEntries(a, b) {
45
+ return (Number(b.isHub) - Number(a.isHub) ||
46
+ Number(b.isOnline) - Number(a.isOnline) ||
47
+ a.name.localeCompare(b.name));
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.382",
3
+ "version": "1.2.384",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,18 +35,18 @@
35
35
  ]
36
36
  },
37
37
  "dependencies": {
38
- "@camstack/addon-admin-ui": "1.2.264",
39
- "@camstack/addon-agent-ui": "1.2.145",
38
+ "@camstack/addon-admin-ui": "1.2.266",
39
+ "@camstack/addon-agent-ui": "1.2.146",
40
40
  "@camstack/addon-auth": "1.2.123",
41
41
  "@camstack/addon-decoder-nodeav": "1.2.115",
42
42
  "@camstack/addon-notifiers": "1.2.120",
43
43
  "@camstack/addon-pipeline": "1.2.292",
44
- "@camstack/addon-pipeline-orchestrator": "1.2.249",
45
- "@camstack/addon-post-analysis": "1.2.279",
44
+ "@camstack/addon-pipeline-orchestrator": "1.2.251",
45
+ "@camstack/addon-post-analysis": "1.2.281",
46
46
  "@camstack/sdk": "1.2.118",
47
- "@camstack/system": "1.2.297",
48
- "@camstack/types": "1.2.253",
49
- "@camstack/ui-library": "1.2.203",
47
+ "@camstack/system": "1.2.299",
48
+ "@camstack/types": "1.2.255",
49
+ "@camstack/ui-library": "1.2.204",
50
50
  "@fastify/compress": "^9.0.0",
51
51
  "@fastify/cookie": "^11.0.2",
52
52
  "@fastify/cors": "^11.2.0",