@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.
- package/dist/api/core/cap-providers.js +1 -0
- package/dist/api/trpc/generated-cap-routers.js +35 -0
- package/dist/core/agent/agent-registry.service.js +24 -1
- package/dist/core/agent/cluster-node-history-mirror.js +52 -0
- package/dist/core/agent/cluster-node-history-store.js +13 -3
- package/dist/core/agent/node-directory.js +48 -0
- package/package.json +8 -8
|
@@ -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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
39
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
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.
|
|
45
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
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.
|
|
48
|
-
"@camstack/types": "1.2.
|
|
49
|
-
"@camstack/ui-library": "1.2.
|
|
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",
|