nexus-prime 3.2.0 → 3.2.2
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/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/dashboard/index.html +2301 -444
- package/dist/dashboard/server.d.ts +29 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +566 -43
- package/dist/dashboard/server.js.map +1 -1
- package/dist/engines/client-registry.d.ts +42 -0
- package/dist/engines/client-registry.d.ts.map +1 -0
- package/dist/engines/client-registry.js +319 -0
- package/dist/engines/client-registry.js.map +1 -0
- package/dist/engines/event-bus.d.ts +52 -1
- package/dist/engines/event-bus.d.ts.map +1 -1
- package/dist/engines/event-bus.js.map +1 -1
- package/dist/engines/memory.d.ts +59 -0
- package/dist/engines/memory.d.ts.map +1 -1
- package/dist/engines/memory.js +214 -0
- package/dist/engines/memory.js.map +1 -1
- package/dist/engines/pod-network.d.ts +27 -0
- package/dist/engines/pod-network.d.ts.map +1 -1
- package/dist/engines/pod-network.js +66 -1
- package/dist/engines/pod-network.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/phantom/runtime.d.ts +2 -0
- package/dist/phantom/runtime.d.ts.map +1 -1
- package/dist/phantom/runtime.js +71 -4
- package/dist/phantom/runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard/server.js
CHANGED
|
@@ -3,98 +3,321 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { nexusEventBus } from '../engines/event-bus.js';
|
|
6
|
+
import { podNetwork } from '../engines/pod-network.js';
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
8
|
-
const PORT = parseInt(process.env.NEXUS_DASHBOARD_PORT || '3377', 10);
|
|
9
9
|
const HOST = process.env.NEXUS_DASHBOARD_HOST || '127.0.0.1';
|
|
10
|
+
const DEFAULT_PORT = parseInt(process.env.NEXUS_DASHBOARD_PORT || '3377', 10);
|
|
11
|
+
const MAX_PORT_SCAN = 24;
|
|
12
|
+
const DASHBOARD_API_VERSION = '2';
|
|
13
|
+
const REQUIRED_CAPABILITIES = {
|
|
14
|
+
runs: true,
|
|
15
|
+
memory: true,
|
|
16
|
+
pod: true,
|
|
17
|
+
clients: true,
|
|
18
|
+
events: true,
|
|
19
|
+
stream: true,
|
|
20
|
+
};
|
|
10
21
|
export class DashboardServer {
|
|
11
22
|
server;
|
|
12
23
|
clients = new Set();
|
|
13
24
|
unsubscribeBus = null;
|
|
14
25
|
runtimeProvider;
|
|
26
|
+
memoryProvider;
|
|
27
|
+
adaptersProvider;
|
|
28
|
+
clientRegistryProvider;
|
|
15
29
|
repoRoot;
|
|
30
|
+
dashboardUrl = null;
|
|
31
|
+
dashboardMode = 'idle';
|
|
32
|
+
activePort = null;
|
|
33
|
+
started = false;
|
|
34
|
+
initializePromise = null;
|
|
16
35
|
constructor(options = {}) {
|
|
17
36
|
this.runtimeProvider = options.runtimeProvider;
|
|
37
|
+
this.memoryProvider = options.memoryProvider;
|
|
38
|
+
this.adaptersProvider = options.adaptersProvider;
|
|
39
|
+
this.clientRegistryProvider = options.clientRegistryProvider;
|
|
18
40
|
this.repoRoot = options.repoRoot ?? process.cwd();
|
|
19
|
-
this.server = http.createServer(
|
|
41
|
+
this.server = http.createServer((req, res) => {
|
|
42
|
+
void this.requestHandler(req, res);
|
|
43
|
+
});
|
|
44
|
+
this.server.on('error', (error) => {
|
|
45
|
+
if (this.dashboardMode === 'bound') {
|
|
46
|
+
console.error('[Dashboard] Server error:', error.message);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
20
49
|
}
|
|
21
50
|
start() {
|
|
22
51
|
if (process.env.NEXUS_DASHBOARD_DISABLED === '1') {
|
|
23
52
|
console.error('[Dashboard] Disabled by NEXUS_DASHBOARD_DISABLED=1');
|
|
24
53
|
return;
|
|
25
54
|
}
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.error(`[Dashboard] Server error:`, e.message);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
this.unsubscribeBus = nexusEventBus.onEvent((event) => {
|
|
40
|
-
this.broadcast(event);
|
|
55
|
+
if (this.started) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.started = true;
|
|
59
|
+
this.initializePromise = this.initialize().catch((error) => {
|
|
60
|
+
this.started = false;
|
|
61
|
+
this.dashboardMode = 'idle';
|
|
62
|
+
this.dashboardUrl = null;
|
|
63
|
+
this.activePort = null;
|
|
64
|
+
console.error('[Dashboard] Failed to start dashboard:', error instanceof Error ? error.message : String(error));
|
|
41
65
|
});
|
|
42
|
-
nexusEventBus.startFilePolling(1500);
|
|
43
66
|
}
|
|
44
67
|
stop() {
|
|
45
68
|
if (this.unsubscribeBus) {
|
|
46
69
|
this.unsubscribeBus();
|
|
47
70
|
this.unsubscribeBus = null;
|
|
48
71
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
res.
|
|
72
|
+
if (this.dashboardMode === 'bound') {
|
|
73
|
+
nexusEventBus.stopFilePolling();
|
|
74
|
+
for (const res of this.clients) {
|
|
75
|
+
res.end();
|
|
76
|
+
}
|
|
77
|
+
this.clients.clear();
|
|
78
|
+
this.server.close();
|
|
52
79
|
}
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
80
|
+
this.dashboardMode = 'idle';
|
|
81
|
+
this.dashboardUrl = null;
|
|
82
|
+
this.activePort = null;
|
|
83
|
+
this.started = false;
|
|
55
84
|
}
|
|
56
85
|
getAddress() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
return this.dashboardUrl;
|
|
87
|
+
}
|
|
88
|
+
async initialize() {
|
|
89
|
+
const probe = await this.probeDashboard(DEFAULT_PORT);
|
|
90
|
+
if (probe.status === 'compatible') {
|
|
91
|
+
this.dashboardMode = 'reused';
|
|
92
|
+
this.dashboardUrl = probe.url;
|
|
93
|
+
this.activePort = DEFAULT_PORT;
|
|
94
|
+
console.error(`[Dashboard] Reusing compatible dashboard at ${probe.url}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const startPort = probe.status === 'incompatible' ? DEFAULT_PORT + 1 : DEFAULT_PORT;
|
|
98
|
+
const fallbackPort = await this.bindFirstAvailablePort(startPort, DEFAULT_PORT + MAX_PORT_SCAN);
|
|
99
|
+
this.dashboardMode = 'bound';
|
|
100
|
+
this.activePort = fallbackPort;
|
|
101
|
+
this.dashboardUrl = this.buildUrl(fallbackPort);
|
|
102
|
+
this.unsubscribeBus = nexusEventBus.onEvent((event) => this.broadcast(event));
|
|
103
|
+
nexusEventBus.startFilePolling();
|
|
104
|
+
if (probe.status === 'incompatible') {
|
|
105
|
+
console.error(`[Dashboard] Incompatible dashboard detected at ${probe.url} (${probe.reason || 'missing compatibility contract'}). New dashboard started at ${this.dashboardUrl}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.error(`[Dashboard] Topology console listening at ${this.dashboardUrl}`);
|
|
109
|
+
}
|
|
110
|
+
async requestHandler(req, res) {
|
|
111
|
+
const url = new URL(req.url || '/', this.dashboardUrl ?? this.buildUrl(this.activePort ?? DEFAULT_PORT));
|
|
112
|
+
if (req.method === 'OPTIONS') {
|
|
113
|
+
this.respondOptions(res);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
65
117
|
this.serveDashboard(res);
|
|
66
118
|
return;
|
|
67
119
|
}
|
|
68
|
-
if (url.pathname === '/stream') {
|
|
120
|
+
if (req.method === 'GET' && url.pathname === '/stream') {
|
|
69
121
|
this.serveSSE(req, res);
|
|
70
122
|
return;
|
|
71
123
|
}
|
|
72
|
-
if (url.pathname === '/api/runs') {
|
|
73
|
-
|
|
124
|
+
if (req.method === 'GET' && url.pathname === '/api/runs') {
|
|
125
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
126
|
+
this.respondJson(res, this.getRuntime()?.listRuns(limit) ?? []);
|
|
74
127
|
return;
|
|
75
128
|
}
|
|
76
|
-
if (url.pathname.startsWith('/api/runs/')) {
|
|
129
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/runs/')) {
|
|
77
130
|
const runId = decodeURIComponent(url.pathname.replace('/api/runs/', ''));
|
|
78
131
|
const run = this.getRuntime()?.getRun(runId);
|
|
79
132
|
this.respondJson(res, run ?? { error: 'run-not-found', runId }, run ? 200 : 404);
|
|
80
133
|
return;
|
|
81
134
|
}
|
|
82
|
-
if (url.pathname === '/api/skills') {
|
|
135
|
+
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
83
136
|
this.respondJson(res, this.getRuntime()?.listSkills() ?? []);
|
|
84
137
|
return;
|
|
85
138
|
}
|
|
86
|
-
if (url.pathname === '/api/workflows') {
|
|
139
|
+
if (req.method === 'GET' && url.pathname === '/api/workflows') {
|
|
87
140
|
this.respondJson(res, this.getRuntime()?.listWorkflows() ?? []);
|
|
88
141
|
return;
|
|
89
142
|
}
|
|
90
|
-
if (url.pathname === '/api/backends') {
|
|
143
|
+
if (req.method === 'GET' && url.pathname === '/api/backends') {
|
|
91
144
|
this.respondJson(res, this.getRuntime()?.getBackendCatalog() ?? {});
|
|
92
145
|
return;
|
|
93
146
|
}
|
|
94
|
-
if (url.pathname === '/api/health') {
|
|
147
|
+
if (req.method === 'GET' && url.pathname === '/api/health') {
|
|
95
148
|
this.respondJson(res, this.collectHealth());
|
|
96
149
|
return;
|
|
97
150
|
}
|
|
151
|
+
if (req.method === 'GET' && url.pathname === '/api/memory') {
|
|
152
|
+
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
153
|
+
const tier = url.searchParams.get('tier') ?? undefined;
|
|
154
|
+
const tag = url.searchParams.get('tag') ?? undefined;
|
|
155
|
+
const linkedType = url.searchParams.get('linkedType') ?? undefined;
|
|
156
|
+
const recencyMs = url.searchParams.get('recencyMs');
|
|
157
|
+
const memory = this.getMemory();
|
|
158
|
+
this.respondJson(res, memory?.listSnapshots(limit, {
|
|
159
|
+
tier: tier,
|
|
160
|
+
tag,
|
|
161
|
+
linkedType: linkedType,
|
|
162
|
+
recencyMs: recencyMs ? parseInt(recencyMs, 10) : undefined,
|
|
163
|
+
}) ?? []);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/') && url.pathname.endsWith('/network')) {
|
|
167
|
+
const id = decodeURIComponent(url.pathname.replace('/api/memory/', '').replace('/network', '').replace(/\/$/, ''));
|
|
168
|
+
const depth = parseInt(url.searchParams.get('depth') || '2', 10);
|
|
169
|
+
const limit = parseInt(url.searchParams.get('limit') || '18', 10);
|
|
170
|
+
const memory = this.getMemory();
|
|
171
|
+
this.respondJson(res, memory?.getNetworkSnapshot(id, depth, limit) ?? { focusId: id, nodes: [], links: [] });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/memory/')) {
|
|
175
|
+
const id = decodeURIComponent(url.pathname.replace('/api/memory/', ''));
|
|
176
|
+
const memory = this.getMemory();
|
|
177
|
+
const detail = memory?.getDetail(id);
|
|
178
|
+
this.respondJson(res, detail ?? { error: 'memory-not-found', id }, detail ? 200 : 404);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (req.method === 'GET' && url.pathname === '/api/pod') {
|
|
182
|
+
const limit = parseInt(url.searchParams.get('limit') || '40', 10);
|
|
183
|
+
this.respondJson(res, podNetwork.getDashboardSnapshot(limit));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/pod/')) {
|
|
187
|
+
const workerId = decodeURIComponent(url.pathname.replace('/api/pod/', ''));
|
|
188
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
189
|
+
this.respondJson(res, podNetwork.getWorker(workerId, limit));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (req.method === 'GET' && url.pathname === '/api/clients') {
|
|
193
|
+
this.respondJson(res, this.getClientRegistry()?.listClients(this.getAdapters()) ?? []);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (req.method === 'GET' && url.pathname === '/api/events') {
|
|
197
|
+
const category = url.searchParams.get('category');
|
|
198
|
+
const type = url.searchParams.get('type');
|
|
199
|
+
const limit = parseInt(url.searchParams.get('limit') || '80', 10);
|
|
200
|
+
const events = this.getEventCards()
|
|
201
|
+
.filter((event) => !category || event.category === category)
|
|
202
|
+
.filter((event) => !type || event.type === type)
|
|
203
|
+
.slice(0, Math.max(limit, 1));
|
|
204
|
+
this.respondJson(res, events);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/deploy') {
|
|
208
|
+
const body = await this.readJsonBody(req);
|
|
209
|
+
const runtime = this.getRuntime();
|
|
210
|
+
const deployed = runtime?.deploySkill(String(body.skillId), body.scope);
|
|
211
|
+
if (deployed) {
|
|
212
|
+
nexusEventBus.emit('skill.deploy', {
|
|
213
|
+
skillId: deployed.skillId,
|
|
214
|
+
scope: deployed.scope,
|
|
215
|
+
status: deployed.rolloutStatus,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
this.respondJson(res, deployed ?? { error: 'skill-not-found' }, deployed ? 200 : 404);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (req.method === 'POST' && url.pathname === '/api/skills/revoke') {
|
|
222
|
+
const body = await this.readJsonBody(req);
|
|
223
|
+
const runtime = this.getRuntime();
|
|
224
|
+
const revoked = runtime?.revokeSkill(String(body.skillId));
|
|
225
|
+
if (revoked) {
|
|
226
|
+
nexusEventBus.emit('skill.revoke', {
|
|
227
|
+
skillId: revoked.skillId,
|
|
228
|
+
status: revoked.rolloutStatus,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
this.respondJson(res, revoked ?? { error: 'skill-not-found' }, revoked ? 200 : 404);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (req.method === 'POST' && url.pathname === '/api/workflows/deploy') {
|
|
235
|
+
const body = await this.readJsonBody(req);
|
|
236
|
+
const runtime = this.getRuntime();
|
|
237
|
+
const deployed = runtime?.deployWorkflow(String(body.workflowId), body.scope);
|
|
238
|
+
if (deployed) {
|
|
239
|
+
nexusEventBus.emit('workflow.deploy', {
|
|
240
|
+
workflowId: deployed.workflowId,
|
|
241
|
+
scope: deployed.scope,
|
|
242
|
+
status: deployed.rolloutStatus,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
this.respondJson(res, deployed ?? { error: 'workflow-not-found' }, deployed ? 200 : 404);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (req.method === 'POST' && url.pathname === '/api/workflows/run') {
|
|
249
|
+
const body = await this.readJsonBody(req);
|
|
250
|
+
const runtime = this.getRuntime();
|
|
251
|
+
if (!runtime) {
|
|
252
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const run = await runtime.runWorkflow(String(body.workflowId), body.goal ? String(body.goal) : undefined);
|
|
257
|
+
nexusEventBus.emit('workflow.run', {
|
|
258
|
+
workflowId: String(body.workflowId),
|
|
259
|
+
runId: run.runId,
|
|
260
|
+
status: run.state,
|
|
261
|
+
});
|
|
262
|
+
this.respondJson(res, run);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.respondJson(res, { error: error instanceof Error ? error.message : 'workflow-run-failed' }, 400);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (req.method === 'POST' && url.pathname === '/api/runtime/execute') {
|
|
270
|
+
const body = await this.readJsonBody(req);
|
|
271
|
+
const runtime = this.getRuntime();
|
|
272
|
+
if (!runtime) {
|
|
273
|
+
this.respondJson(res, { error: 'runtime-unavailable' }, 503);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!body.goal || typeof body.goal !== 'string') {
|
|
277
|
+
this.respondJson(res, { error: 'goal-required' }, 400);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const run = await runtime.run(body);
|
|
281
|
+
nexusEventBus.emit('dashboard.action', {
|
|
282
|
+
action: 'runtime.execute',
|
|
283
|
+
status: run.state,
|
|
284
|
+
target: run.runId,
|
|
285
|
+
});
|
|
286
|
+
this.respondJson(res, run, 201);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (req.method === 'POST' && url.pathname.startsWith('/api/clients/') && url.pathname.endsWith('/reconnect')) {
|
|
290
|
+
const clientId = decodeURIComponent(url.pathname.replace('/api/clients/', '').replace('/reconnect', '').replace(/\/$/, ''));
|
|
291
|
+
const registry = this.getClientRegistry();
|
|
292
|
+
if (!registry) {
|
|
293
|
+
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const client = registry.reconnect(clientId);
|
|
297
|
+
nexusEventBus.emit('dashboard.action', {
|
|
298
|
+
action: 'client.reconnect',
|
|
299
|
+
status: 'ok',
|
|
300
|
+
target: clientId,
|
|
301
|
+
});
|
|
302
|
+
this.respondJson(res, client);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (req.method === 'POST' && url.pathname.startsWith('/api/clients/') && url.pathname.endsWith('/clear')) {
|
|
306
|
+
const clientId = decodeURIComponent(url.pathname.replace('/api/clients/', '').replace('/clear', '').replace(/\/$/, ''));
|
|
307
|
+
const registry = this.getClientRegistry();
|
|
308
|
+
if (!registry) {
|
|
309
|
+
this.respondJson(res, { error: 'client-registry-unavailable' }, 503);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
registry.clear(clientId);
|
|
313
|
+
nexusEventBus.emit('dashboard.action', {
|
|
314
|
+
action: 'client.clear',
|
|
315
|
+
status: 'ok',
|
|
316
|
+
target: clientId,
|
|
317
|
+
});
|
|
318
|
+
this.respondJson(res, { ok: true, clientId });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
98
321
|
res.writeHead(404);
|
|
99
322
|
res.end('Not found');
|
|
100
323
|
}
|
|
@@ -114,12 +337,12 @@ export class DashboardServer {
|
|
|
114
337
|
res.writeHead(200, {
|
|
115
338
|
'Content-Type': 'text/event-stream',
|
|
116
339
|
'Cache-Control': 'no-cache',
|
|
117
|
-
|
|
118
|
-
'Access-Control-Allow-Origin': '*'
|
|
340
|
+
Connection: 'keep-alive',
|
|
341
|
+
'Access-Control-Allow-Origin': '*',
|
|
119
342
|
});
|
|
120
343
|
res.write('retry: 3000\n\n');
|
|
121
344
|
res.write(`event: bootstrap\ndata: ${JSON.stringify({ connected: true, timestamp: Date.now() })}\n\n`);
|
|
122
|
-
const history =
|
|
345
|
+
const history = this.getEventCards();
|
|
123
346
|
for (const evt of history) {
|
|
124
347
|
res.write(`data: ${JSON.stringify(evt)}\n\n`);
|
|
125
348
|
}
|
|
@@ -134,11 +357,20 @@ export class DashboardServer {
|
|
|
134
357
|
});
|
|
135
358
|
}
|
|
136
359
|
broadcast(event) {
|
|
137
|
-
const
|
|
360
|
+
const normalized = this.normalizeEvent(event);
|
|
361
|
+
const dataStr = `data: ${JSON.stringify(normalized)}\n\n`;
|
|
138
362
|
for (const res of this.clients) {
|
|
139
363
|
res.write(dataStr);
|
|
140
364
|
}
|
|
141
365
|
}
|
|
366
|
+
respondOptions(res) {
|
|
367
|
+
res.writeHead(204, {
|
|
368
|
+
'Access-Control-Allow-Origin': '*',
|
|
369
|
+
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
370
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
371
|
+
});
|
|
372
|
+
res.end();
|
|
373
|
+
}
|
|
142
374
|
respondJson(res, data, statusCode = 200) {
|
|
143
375
|
res.writeHead(statusCode, {
|
|
144
376
|
'Content-Type': 'application/json',
|
|
@@ -147,14 +379,58 @@ export class DashboardServer {
|
|
|
147
379
|
});
|
|
148
380
|
res.end(JSON.stringify(data, null, 2));
|
|
149
381
|
}
|
|
382
|
+
async readJsonBody(req) {
|
|
383
|
+
const chunks = [];
|
|
384
|
+
for await (const chunk of req) {
|
|
385
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
386
|
+
}
|
|
387
|
+
if (!chunks.length)
|
|
388
|
+
return {};
|
|
389
|
+
try {
|
|
390
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
150
396
|
getRuntime() {
|
|
151
397
|
return this.runtimeProvider?.();
|
|
152
398
|
}
|
|
399
|
+
getMemory() {
|
|
400
|
+
return this.memoryProvider?.();
|
|
401
|
+
}
|
|
402
|
+
getAdapters() {
|
|
403
|
+
return this.adaptersProvider?.() ?? [];
|
|
404
|
+
}
|
|
405
|
+
getClientRegistry() {
|
|
406
|
+
return this.clientRegistryProvider?.();
|
|
407
|
+
}
|
|
408
|
+
getEventCards() {
|
|
409
|
+
return nexusEventBus.getHistory().map((event) => this.normalizeEvent(event)).reverse();
|
|
410
|
+
}
|
|
411
|
+
normalizeEvent(event) {
|
|
412
|
+
const category = mapEventCategory(event.type);
|
|
413
|
+
const severity = mapEventSeverity(event.type, event.data);
|
|
414
|
+
return {
|
|
415
|
+
id: event.id,
|
|
416
|
+
type: event.type,
|
|
417
|
+
title: mapEventTitle(event.type),
|
|
418
|
+
source: mapEventSource(event.type, event.data),
|
|
419
|
+
time: event.timestamp,
|
|
420
|
+
severity,
|
|
421
|
+
category,
|
|
422
|
+
summary: summarizeEvent(event.type, event.data),
|
|
423
|
+
payload: event.data,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
153
426
|
collectHealth() {
|
|
154
427
|
const packageJsonPath = path.join(this.repoRoot, 'package.json');
|
|
155
428
|
const workflowPath = path.join(this.repoRoot, '.github', 'workflows', 'pages.yml');
|
|
156
429
|
const docsDir = path.join(this.repoRoot, 'docs');
|
|
157
430
|
const runtime = this.getRuntime();
|
|
431
|
+
const memory = this.getMemory();
|
|
432
|
+
const clientRegistry = this.getClientRegistry();
|
|
433
|
+
const podSnapshot = podNetwork.getDashboardSnapshot(20);
|
|
158
434
|
let packageVersion = 'unknown';
|
|
159
435
|
if (fs.existsSync(packageJsonPath)) {
|
|
160
436
|
try {
|
|
@@ -169,12 +445,28 @@ export class DashboardServer {
|
|
|
169
445
|
const raw = fs.readFileSync(workflowPath, 'utf-8');
|
|
170
446
|
pagesWorkflowValid = raw.includes('steps.deployment.outputs.page_url');
|
|
171
447
|
}
|
|
448
|
+
const clients = clientRegistry?.listClients(this.getAdapters()) ?? [];
|
|
172
449
|
return {
|
|
450
|
+
dashboardApiVersion: DASHBOARD_API_VERSION,
|
|
451
|
+
capabilities: { ...REQUIRED_CAPABILITIES },
|
|
452
|
+
dashboardUrl: this.getAddress(),
|
|
453
|
+
dashboardMode: this.dashboardMode,
|
|
173
454
|
connection: {
|
|
174
455
|
stream: this.clients.size > 0 ? 'connected' : 'idle',
|
|
175
456
|
subscribers: this.clients.size,
|
|
176
457
|
},
|
|
177
458
|
runtime: runtime?.getHealth() ?? { runtime: 'unavailable' },
|
|
459
|
+
memory: memory?.getStats() ?? { prefrontal: 0, hippocampus: 0, cortex: 0, totalLinks: 0, oldestEntry: null, topTags: [] },
|
|
460
|
+
pod: {
|
|
461
|
+
workers: podSnapshot.activeWorkers.length,
|
|
462
|
+
lastMessageTimestamp: podSnapshot.lastMessageTimestamp,
|
|
463
|
+
confidenceBands: podSnapshot.confidenceBands,
|
|
464
|
+
},
|
|
465
|
+
clients: {
|
|
466
|
+
total: clients.length,
|
|
467
|
+
active: clients.filter((client) => client.state === 'active').length,
|
|
468
|
+
inferred: clients.filter((client) => client.state === 'inferred').length,
|
|
469
|
+
},
|
|
178
470
|
release: {
|
|
179
471
|
packageVersion,
|
|
180
472
|
},
|
|
@@ -188,5 +480,236 @@ export class DashboardServer {
|
|
|
188
480
|
},
|
|
189
481
|
};
|
|
190
482
|
}
|
|
483
|
+
buildUrl(port) {
|
|
484
|
+
return `http://${HOST}:${port}`;
|
|
485
|
+
}
|
|
486
|
+
async probeDashboard(port) {
|
|
487
|
+
const url = this.buildUrl(port);
|
|
488
|
+
try {
|
|
489
|
+
const response = await this.requestProbe(`${url}/api/health`);
|
|
490
|
+
if (response.statusCode !== 200) {
|
|
491
|
+
return {
|
|
492
|
+
status: 'incompatible',
|
|
493
|
+
url,
|
|
494
|
+
reason: `health-status-${response.statusCode}`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
let payload = null;
|
|
498
|
+
try {
|
|
499
|
+
payload = JSON.parse(response.body);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return {
|
|
503
|
+
status: 'incompatible',
|
|
504
|
+
url,
|
|
505
|
+
reason: 'health-invalid-json',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (this.isCompatibleHealth(payload)) {
|
|
509
|
+
return {
|
|
510
|
+
status: 'compatible',
|
|
511
|
+
url,
|
|
512
|
+
health: payload,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
status: 'incompatible',
|
|
517
|
+
url,
|
|
518
|
+
health: payload,
|
|
519
|
+
reason: 'health-incompatible',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (this.isFreePortProbeError(error)) {
|
|
524
|
+
return {
|
|
525
|
+
status: 'free',
|
|
526
|
+
url,
|
|
527
|
+
reason: error instanceof Error ? error.message : 'connection-refused',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
status: 'incompatible',
|
|
532
|
+
url,
|
|
533
|
+
reason: error instanceof Error ? error.message : 'probe-failed',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
isCompatibleHealth(payload) {
|
|
538
|
+
if (!payload || payload.dashboardApiVersion !== DASHBOARD_API_VERSION) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return Object.entries(REQUIRED_CAPABILITIES).every(([key, expected]) => payload.capabilities?.[key] === expected);
|
|
542
|
+
}
|
|
543
|
+
isFreePortProbeError(error) {
|
|
544
|
+
const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
|
|
545
|
+
return code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EHOSTUNREACH' || code === 'ENOTFOUND';
|
|
546
|
+
}
|
|
547
|
+
requestProbe(url) {
|
|
548
|
+
return new Promise((resolve, reject) => {
|
|
549
|
+
const req = http.get(url, { timeout: 1200 }, (res) => {
|
|
550
|
+
const chunks = [];
|
|
551
|
+
res.on('data', (chunk) => {
|
|
552
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
553
|
+
});
|
|
554
|
+
res.on('end', () => {
|
|
555
|
+
resolve({
|
|
556
|
+
statusCode: res.statusCode ?? 0,
|
|
557
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
req.on('timeout', () => {
|
|
562
|
+
req.destroy(new Error('probe-timeout'));
|
|
563
|
+
});
|
|
564
|
+
req.on('error', reject);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async bindFirstAvailablePort(startPort, endPort) {
|
|
568
|
+
let lastError = null;
|
|
569
|
+
for (let port = startPort; port <= endPort; port += 1) {
|
|
570
|
+
try {
|
|
571
|
+
await this.listenOnPort(port);
|
|
572
|
+
return port;
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
lastError = error;
|
|
576
|
+
const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
|
|
577
|
+
if (code !== 'EADDRINUSE') {
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
throw lastError instanceof Error
|
|
583
|
+
? lastError
|
|
584
|
+
: new Error(`No free dashboard port found in range ${startPort}-${endPort}`);
|
|
585
|
+
}
|
|
586
|
+
listenOnPort(port) {
|
|
587
|
+
return new Promise((resolve, reject) => {
|
|
588
|
+
const handleListening = () => {
|
|
589
|
+
cleanup();
|
|
590
|
+
resolve();
|
|
591
|
+
};
|
|
592
|
+
const handleError = (error) => {
|
|
593
|
+
cleanup();
|
|
594
|
+
reject(error);
|
|
595
|
+
};
|
|
596
|
+
const cleanup = () => {
|
|
597
|
+
this.server.off('listening', handleListening);
|
|
598
|
+
this.server.off('error', handleError);
|
|
599
|
+
};
|
|
600
|
+
this.server.once('listening', handleListening);
|
|
601
|
+
this.server.once('error', handleError);
|
|
602
|
+
this.server.listen(port, HOST);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function mapEventCategory(type) {
|
|
607
|
+
if (type.startsWith('memory.'))
|
|
608
|
+
return 'memory';
|
|
609
|
+
if (type.startsWith('pod.'))
|
|
610
|
+
return 'pod';
|
|
611
|
+
if (type.startsWith('phantom.'))
|
|
612
|
+
return 'runtime';
|
|
613
|
+
if (type.startsWith('client.'))
|
|
614
|
+
return 'clients';
|
|
615
|
+
if (type.startsWith('skill.'))
|
|
616
|
+
return 'skills';
|
|
617
|
+
if (type.startsWith('workflow.'))
|
|
618
|
+
return 'workflows';
|
|
619
|
+
if (type.startsWith('tokens.') || type.startsWith('cas.') || type.startsWith('kv.'))
|
|
620
|
+
return 'tokens';
|
|
621
|
+
return 'system';
|
|
622
|
+
}
|
|
623
|
+
function mapEventSeverity(type, payload) {
|
|
624
|
+
if (type === 'guardrail.check') {
|
|
625
|
+
return payload.passed ? 'good' : 'bad';
|
|
626
|
+
}
|
|
627
|
+
if (type === 'phantom.merge' || type === 'phantom.merge.complete' || type === 'workflow.run') {
|
|
628
|
+
return payload.status === 'failed' ? 'bad' : 'good';
|
|
629
|
+
}
|
|
630
|
+
if (type === 'client.inferred')
|
|
631
|
+
return 'warn';
|
|
632
|
+
if (type === 'dashboard.action' && payload.status === 'failed')
|
|
633
|
+
return 'bad';
|
|
634
|
+
if (type === 'pod.signal')
|
|
635
|
+
return 'info';
|
|
636
|
+
return 'info';
|
|
637
|
+
}
|
|
638
|
+
function mapEventTitle(type) {
|
|
639
|
+
return {
|
|
640
|
+
'system.boot': 'Runtime boot',
|
|
641
|
+
'memory.store': 'Memory stored',
|
|
642
|
+
'memory.recall': 'Memory recall',
|
|
643
|
+
'pod.signal': 'POD signal',
|
|
644
|
+
'tokens.optimized': 'Tokens optimized',
|
|
645
|
+
'phantom.worker.start': 'Worker start',
|
|
646
|
+
'phantom.worker.complete': 'Worker complete',
|
|
647
|
+
'phantom.merge.complete': 'Merge complete',
|
|
648
|
+
'phantom.merge': 'Merge decision',
|
|
649
|
+
'guardrail.check': 'Guardrail check',
|
|
650
|
+
'ghost.pass': 'Ghost pass',
|
|
651
|
+
'graph.query': 'Graph query',
|
|
652
|
+
'darwin.cycle': 'Darwin cycle',
|
|
653
|
+
'session.dna': 'Session DNA',
|
|
654
|
+
'skill.register': 'Skill registered',
|
|
655
|
+
'skill.deploy': 'Skill deployed',
|
|
656
|
+
'skill.revoke': 'Skill revoked',
|
|
657
|
+
'workflow.deploy': 'Workflow deployed',
|
|
658
|
+
'workflow.run': 'Workflow run',
|
|
659
|
+
'client.heartbeat': 'Client heartbeat',
|
|
660
|
+
'client.inferred': 'Client inferred',
|
|
661
|
+
'client.status': 'Client status',
|
|
662
|
+
'dashboard.action': 'Dashboard action',
|
|
663
|
+
'nexusnet.publish': 'NexusNet publish',
|
|
664
|
+
'nexusnet.sync': 'NexusNet sync',
|
|
665
|
+
'entanglement.create': 'Entanglement created',
|
|
666
|
+
'entanglement.collapse': 'Entanglement collapsed',
|
|
667
|
+
'entanglement.correlate': 'Entanglement correlated',
|
|
668
|
+
'cas.encode': 'CAS encode',
|
|
669
|
+
'cas.decode': 'CAS decode',
|
|
670
|
+
'cas.pattern_learned': 'CAS pattern',
|
|
671
|
+
'kv.merge': 'KV merge',
|
|
672
|
+
'kv.adapt': 'KV adapt',
|
|
673
|
+
'kv.consensus': 'KV consensus',
|
|
674
|
+
}[type] ?? type;
|
|
675
|
+
}
|
|
676
|
+
function mapEventSource(type, payload) {
|
|
677
|
+
if (type.startsWith('client.'))
|
|
678
|
+
return String(payload.displayName ?? payload.clientId ?? 'client');
|
|
679
|
+
if (type === 'pod.signal')
|
|
680
|
+
return String(payload.workerId ?? 'pod');
|
|
681
|
+
if (type.startsWith('phantom.'))
|
|
682
|
+
return String(payload.workerId ?? payload.winner ?? 'runtime');
|
|
683
|
+
if (type.startsWith('skill.'))
|
|
684
|
+
return String(payload.skillId ?? payload.name ?? 'skill');
|
|
685
|
+
if (type.startsWith('workflow.'))
|
|
686
|
+
return String(payload.workflowId ?? 'workflow');
|
|
687
|
+
return 'nexus-prime';
|
|
688
|
+
}
|
|
689
|
+
function summarizeEvent(type, payload) {
|
|
690
|
+
switch (type) {
|
|
691
|
+
case 'memory.store':
|
|
692
|
+
return `Priority ${payload.priority ?? 'n/a'} · ${payload.tags?.join(', ') ?? 'no tags'}`;
|
|
693
|
+
case 'memory.recall':
|
|
694
|
+
return `Recalled ${payload.count ?? 0} memories for "${payload.query ?? ''}"`;
|
|
695
|
+
case 'pod.signal':
|
|
696
|
+
return String(payload.content ?? 'POD signal received');
|
|
697
|
+
case 'tokens.optimized':
|
|
698
|
+
return `Saved ${payload.savings ?? 0} tokens across ${payload.files ?? 0} files`;
|
|
699
|
+
case 'phantom.worker.start':
|
|
700
|
+
return `${payload.approach ?? 'worker'} started for ${payload.goal ?? 'task'}`;
|
|
701
|
+
case 'phantom.worker.complete':
|
|
702
|
+
return `Confidence ${payload.confidence ?? 0}`;
|
|
703
|
+
case 'phantom.merge':
|
|
704
|
+
return `${payload.action ?? 'merge'} · ${payload.winner ?? 'unknown winner'}`;
|
|
705
|
+
case 'client.heartbeat':
|
|
706
|
+
case 'client.inferred':
|
|
707
|
+
case 'client.status':
|
|
708
|
+
return JSON.stringify(payload);
|
|
709
|
+
case 'dashboard.action':
|
|
710
|
+
return `${payload.action ?? 'action'} → ${payload.status ?? 'unknown'}`;
|
|
711
|
+
default:
|
|
712
|
+
return JSON.stringify(payload);
|
|
713
|
+
}
|
|
191
714
|
}
|
|
192
715
|
//# sourceMappingURL=server.js.map
|