agentflow-dashboard 0.5.0 → 0.6.0
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/README.md +20 -2
- package/dist/{chunk-RWNVLZU7.js → chunk-N6IN5SHX.js} +513 -278
- package/dist/cli.cjs +510 -284
- package/dist/cli.js +1 -1
- package/dist/index.cjs +510 -284
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +1305 -519
- package/dist/public/debug.html +2 -2
- package/dist/server.cjs +510 -284
- package/dist/server.js +1 -1
- package/package.json +3 -3
- package/public/dashboard.js +1305 -519
- package/public/debug.html +2 -2
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
|
-
import express from "express";
|
|
3
2
|
import * as fs3 from "fs";
|
|
4
3
|
import { createServer } from "http";
|
|
5
4
|
import * as path3 from "path";
|
|
6
5
|
import { fileURLToPath } from "url";
|
|
6
|
+
import {
|
|
7
|
+
auditProcesses,
|
|
8
|
+
createExecutionEvent,
|
|
9
|
+
createKnowledgeStore,
|
|
10
|
+
discoverProcess,
|
|
11
|
+
discoverProcessConfig,
|
|
12
|
+
findVariants,
|
|
13
|
+
getBottlenecks,
|
|
14
|
+
loadGraph as loadGraph2
|
|
15
|
+
} from "agentflow-core";
|
|
16
|
+
import express from "express";
|
|
7
17
|
import { WebSocketServer } from "ws";
|
|
8
|
-
import { discoverProcessConfig, auditProcesses } from "agentflow-core";
|
|
9
18
|
|
|
10
19
|
// src/stats.ts
|
|
11
20
|
import { getFailures, getHungNodes, getStats } from "agentflow-core";
|
|
@@ -183,11 +192,161 @@ var AgentStats = class {
|
|
|
183
192
|
};
|
|
184
193
|
|
|
185
194
|
// src/watcher.ts
|
|
186
|
-
import { loadGraph } from "agentflow-core";
|
|
187
|
-
import chokidar from "chokidar";
|
|
188
195
|
import { EventEmitter } from "events";
|
|
189
196
|
import * as fs from "fs";
|
|
190
197
|
import * as path from "path";
|
|
198
|
+
import { loadGraph } from "agentflow-core";
|
|
199
|
+
import chokidar from "chokidar";
|
|
200
|
+
|
|
201
|
+
// src/parsers/log-utils.ts
|
|
202
|
+
function stripAnsi(str) {
|
|
203
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
204
|
+
}
|
|
205
|
+
function parseValue(value) {
|
|
206
|
+
if (value.match(/^\d+$/)) return parseInt(value, 10);
|
|
207
|
+
if (value.match(/^\d+\.\d+$/)) return parseFloat(value);
|
|
208
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
209
|
+
if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
function parseTimestamp(value) {
|
|
213
|
+
if (!value) return null;
|
|
214
|
+
if (typeof value === "number") return value;
|
|
215
|
+
try {
|
|
216
|
+
return new Date(value).getTime();
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function extractTimestamp(line) {
|
|
222
|
+
const clean = stripAnsi(line);
|
|
223
|
+
const isoMatch = clean.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
|
|
224
|
+
if (isoMatch) return new Date(isoMatch[1]).getTime();
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function extractLogLevel(line) {
|
|
228
|
+
const clean = stripAnsi(line);
|
|
229
|
+
const levelMatch = clean.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
|
|
230
|
+
return levelMatch ? levelMatch[1].trim().toLowerCase() : null;
|
|
231
|
+
}
|
|
232
|
+
function extractAction(line) {
|
|
233
|
+
const clean = stripAnsi(line);
|
|
234
|
+
const actionMatch = clean.match(/\]\s+(\S+)/);
|
|
235
|
+
if (actionMatch) return actionMatch[1].trim();
|
|
236
|
+
const afterLevel = clean.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*\]?\s*/i, "");
|
|
237
|
+
return afterLevel.split(/\s+/)[0] || "";
|
|
238
|
+
}
|
|
239
|
+
function extractKeyValuePairs(line) {
|
|
240
|
+
const pairs = {};
|
|
241
|
+
const clean = stripAnsi(line);
|
|
242
|
+
const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
|
|
243
|
+
let match;
|
|
244
|
+
while ((match = kvRegex.exec(clean)) !== null) {
|
|
245
|
+
if (match[1] === "Z" || match[1] === "m") continue;
|
|
246
|
+
pairs[match[1]] = parseValue(match[2]);
|
|
247
|
+
}
|
|
248
|
+
return pairs;
|
|
249
|
+
}
|
|
250
|
+
function detectComponent(action, kvPairs) {
|
|
251
|
+
if (action.includes(".")) return action.split(".")[0];
|
|
252
|
+
if (kvPairs.component) return String(kvPairs.component);
|
|
253
|
+
if (kvPairs.service) return String(kvPairs.service);
|
|
254
|
+
if (kvPairs.module) return String(kvPairs.module);
|
|
255
|
+
if (kvPairs.worker) return String(kvPairs.worker);
|
|
256
|
+
return action || "unknown";
|
|
257
|
+
}
|
|
258
|
+
function detectOperation(action, kvPairs) {
|
|
259
|
+
if (action.includes(".")) return action.split(".").slice(1).join(".");
|
|
260
|
+
if (kvPairs.operation) return String(kvPairs.operation);
|
|
261
|
+
if (kvPairs.method) return String(kvPairs.method);
|
|
262
|
+
if (kvPairs.action) return String(kvPairs.action);
|
|
263
|
+
return action || "activity";
|
|
264
|
+
}
|
|
265
|
+
function detectActivityPattern(line) {
|
|
266
|
+
let timestamp = extractTimestamp(line);
|
|
267
|
+
let level = extractLogLevel(line);
|
|
268
|
+
let action = extractAction(line);
|
|
269
|
+
let kvPairs = extractKeyValuePairs(line);
|
|
270
|
+
if (!timestamp) {
|
|
271
|
+
const jsonMatch = line.match(/\{.*\}/);
|
|
272
|
+
if (jsonMatch) {
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
275
|
+
timestamp = parseTimestamp(parsed.timestamp || parsed.time || parsed.ts) || Date.now();
|
|
276
|
+
level = parsed.level || parsed.severity || "info";
|
|
277
|
+
action = parsed.action || parsed.event || parsed.message || "";
|
|
278
|
+
kvPairs = parsed;
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!timestamp) {
|
|
284
|
+
const kvMatches = line.match(/(\w+)=([^\s]+)/g);
|
|
285
|
+
if (kvMatches && kvMatches.length >= 2) {
|
|
286
|
+
const pairs = {};
|
|
287
|
+
for (const m of kvMatches) {
|
|
288
|
+
const [key, value] = m.split("=", 2);
|
|
289
|
+
pairs[key] = parseValue(value);
|
|
290
|
+
}
|
|
291
|
+
timestamp = parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
|
|
292
|
+
level = String(pairs.level || "info");
|
|
293
|
+
action = String(pairs.action || pairs.event || "");
|
|
294
|
+
kvPairs = pairs;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!timestamp) {
|
|
298
|
+
const logMatch = line.match(
|
|
299
|
+
/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/
|
|
300
|
+
);
|
|
301
|
+
if (logMatch) {
|
|
302
|
+
timestamp = new Date(logMatch[1]).getTime();
|
|
303
|
+
level = logMatch[2] || "info";
|
|
304
|
+
action = logMatch[3] || "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!timestamp) return null;
|
|
308
|
+
return {
|
|
309
|
+
timestamp,
|
|
310
|
+
level: (level == null ? void 0 : level.toLowerCase()) || "info",
|
|
311
|
+
action,
|
|
312
|
+
component: detectComponent(action, kvPairs),
|
|
313
|
+
operation: detectOperation(action, kvPairs),
|
|
314
|
+
...kvPairs
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function extractSessionIdentifier(activity) {
|
|
318
|
+
return String(
|
|
319
|
+
activity.session_id || activity.run_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
function detectTrigger(activity) {
|
|
323
|
+
if (activity.trigger) return String(activity.trigger);
|
|
324
|
+
if (activity.method && activity.url) return "api-call";
|
|
325
|
+
if (typeof activity.operation === "string" && activity.operation.includes("start"))
|
|
326
|
+
return "startup";
|
|
327
|
+
if (typeof activity.operation === "string" && activity.operation.includes("invoke"))
|
|
328
|
+
return "invocation";
|
|
329
|
+
return "event";
|
|
330
|
+
}
|
|
331
|
+
function getUniversalNodeStatus(activity) {
|
|
332
|
+
if (activity.level === "error" || activity.level === "fatal") return "failed";
|
|
333
|
+
if (activity.level === "warn" || activity.level === "warning") return "warning";
|
|
334
|
+
const op = String(activity.operation || "");
|
|
335
|
+
if (op.match(/start|begin|init/i)) return "running";
|
|
336
|
+
if (op.match(/complete|finish|end|done/i)) return "completed";
|
|
337
|
+
return "completed";
|
|
338
|
+
}
|
|
339
|
+
function openClawSessionIdToAgent(sessionId) {
|
|
340
|
+
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
341
|
+
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
342
|
+
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
343
|
+
if (sessionId.startsWith("main-")) return "main";
|
|
344
|
+
const firstSegment = sessionId.split("-")[0];
|
|
345
|
+
if (firstSegment) return firstSegment;
|
|
346
|
+
return "openclaw";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/watcher.ts
|
|
191
350
|
var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
192
351
|
watchers = [];
|
|
193
352
|
traces = /* @__PURE__ */ new Map();
|
|
@@ -227,7 +386,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
227
386
|
console.error(`Error scanning directory ${dir}:`, error);
|
|
228
387
|
}
|
|
229
388
|
}
|
|
230
|
-
console.log(
|
|
389
|
+
console.log(
|
|
390
|
+
`Scanned ${totalDirectories} directories (recursive), loaded ${this.traces.size} items from ${totalFiles} files`
|
|
391
|
+
);
|
|
231
392
|
}
|
|
232
393
|
/** Recursively scan directory for supported file types */
|
|
233
394
|
scanDirectoryRecursive(dir, depth = 0) {
|
|
@@ -269,7 +430,14 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
269
430
|
"models.json",
|
|
270
431
|
"config.json"
|
|
271
432
|
]);
|
|
272
|
-
static SKIP_SUFFIXES = [
|
|
433
|
+
static SKIP_SUFFIXES = [
|
|
434
|
+
"-state.json",
|
|
435
|
+
"-config.json",
|
|
436
|
+
"-watch-state.json",
|
|
437
|
+
".tmp",
|
|
438
|
+
".bak",
|
|
439
|
+
".backup"
|
|
440
|
+
];
|
|
273
441
|
/** Load a .json trace, .jsonl session file, or .log file. */
|
|
274
442
|
loadFile(filePath) {
|
|
275
443
|
const filename = path.basename(filePath);
|
|
@@ -320,16 +488,16 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
320
488
|
const lines = content.split("\n").filter((line) => line.trim());
|
|
321
489
|
const activities = /* @__PURE__ */ new Map();
|
|
322
490
|
for (const line of lines) {
|
|
323
|
-
const activity =
|
|
491
|
+
const activity = detectActivityPattern(line);
|
|
324
492
|
if (!activity) continue;
|
|
325
|
-
const sessionId =
|
|
493
|
+
const sessionId = extractSessionIdentifier(activity);
|
|
326
494
|
if (!activities.has(sessionId)) {
|
|
327
495
|
activities.set(sessionId, {
|
|
328
496
|
id: sessionId,
|
|
329
497
|
rootNodeId: "",
|
|
330
498
|
agentId: this.detectAgentIdentifier(activity, filename, filePath),
|
|
331
499
|
name: this.generateActivityName(activity, sessionId),
|
|
332
|
-
trigger:
|
|
500
|
+
trigger: detectTrigger(activity),
|
|
333
501
|
startTime: activity.timestamp,
|
|
334
502
|
endTime: activity.timestamp,
|
|
335
503
|
status: "completed",
|
|
@@ -352,7 +520,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
352
520
|
(session) => Object.keys(session.nodes).length > 0
|
|
353
521
|
);
|
|
354
522
|
for (const trace of traces) {
|
|
355
|
-
const sortedNodes = Object.values(trace.nodes).sort(
|
|
523
|
+
const sortedNodes = Object.values(trace.nodes).sort(
|
|
524
|
+
(a, b) => a.startTime - b.startTime
|
|
525
|
+
);
|
|
356
526
|
trace.sessionEvents = sortedNodes.map((node) => ({
|
|
357
527
|
type: node.status === "failed" ? "tool_result" : "system",
|
|
358
528
|
timestamp: node.startTime,
|
|
@@ -370,7 +540,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
370
540
|
id: "",
|
|
371
541
|
rootNodeId: "root",
|
|
372
542
|
nodes: {
|
|
373
|
-
|
|
543
|
+
root: {
|
|
374
544
|
id: "root",
|
|
375
545
|
type: "log-file",
|
|
376
546
|
name: filename,
|
|
@@ -393,125 +563,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
393
563
|
}
|
|
394
564
|
return traces;
|
|
395
565
|
}
|
|
396
|
-
|
|
397
|
-
detectActivityPattern(line) {
|
|
398
|
-
let timestamp = this.extractTimestamp(line);
|
|
399
|
-
let level = this.extractLogLevel(line);
|
|
400
|
-
let action = this.extractAction(line);
|
|
401
|
-
let kvPairs = this.extractKeyValuePairs(line);
|
|
402
|
-
if (!timestamp) {
|
|
403
|
-
const jsonMatch = line.match(/\{.*\}/);
|
|
404
|
-
if (jsonMatch) {
|
|
405
|
-
try {
|
|
406
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
407
|
-
timestamp = this.parseTimestamp(parsed.timestamp || parsed.time || parsed.ts) || Date.now();
|
|
408
|
-
level = parsed.level || parsed.severity || "info";
|
|
409
|
-
action = parsed.action || parsed.event || parsed.message || "";
|
|
410
|
-
kvPairs = parsed;
|
|
411
|
-
} catch {
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (!timestamp) {
|
|
416
|
-
const kvMatches = line.match(/(\w+)=([^\s]+)/g);
|
|
417
|
-
if (kvMatches && kvMatches.length >= 2) {
|
|
418
|
-
const pairs = {};
|
|
419
|
-
kvMatches.forEach((match) => {
|
|
420
|
-
const [key, value] = match.split("=", 2);
|
|
421
|
-
pairs[key] = this.parseValue(value);
|
|
422
|
-
});
|
|
423
|
-
timestamp = this.parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
|
|
424
|
-
level = pairs.level || "info";
|
|
425
|
-
action = pairs.action || pairs.event || "";
|
|
426
|
-
kvPairs = pairs;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (!timestamp) {
|
|
430
|
-
const logMatch = line.match(/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/);
|
|
431
|
-
if (logMatch) {
|
|
432
|
-
timestamp = new Date(logMatch[1]).getTime();
|
|
433
|
-
level = logMatch[2] || "info";
|
|
434
|
-
action = logMatch[3] || "";
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
if (!timestamp) return null;
|
|
438
|
-
return {
|
|
439
|
-
timestamp,
|
|
440
|
-
level: (level == null ? void 0 : level.toLowerCase()) || "info",
|
|
441
|
-
action,
|
|
442
|
-
component: this.detectComponent(action, kvPairs),
|
|
443
|
-
operation: this.detectOperation(action, kvPairs),
|
|
444
|
-
...kvPairs
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
/** Strip ANSI escape codes from a string. */
|
|
448
|
-
stripAnsi(str) {
|
|
449
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
450
|
-
}
|
|
451
|
-
extractTimestamp(line) {
|
|
452
|
-
const clean = this.stripAnsi(line);
|
|
453
|
-
const isoMatch = clean.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
|
|
454
|
-
if (isoMatch) return new Date(isoMatch[1]).getTime();
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
extractLogLevel(line) {
|
|
458
|
-
const clean = this.stripAnsi(line);
|
|
459
|
-
const levelMatch = clean.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
|
|
460
|
-
return levelMatch ? levelMatch[1].trim().toLowerCase() : null;
|
|
461
|
-
}
|
|
462
|
-
extractAction(line) {
|
|
463
|
-
const clean = this.stripAnsi(line);
|
|
464
|
-
const actionMatch = clean.match(/\]\s+(\S+)/);
|
|
465
|
-
if (actionMatch) return actionMatch[1].trim();
|
|
466
|
-
const afterLevel = clean.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*\]?\s*/i, "");
|
|
467
|
-
return afterLevel.split(/\s+/)[0] || "";
|
|
468
|
-
}
|
|
469
|
-
extractKeyValuePairs(line) {
|
|
470
|
-
const pairs = {};
|
|
471
|
-
const clean = this.stripAnsi(line);
|
|
472
|
-
const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
|
|
473
|
-
let match;
|
|
474
|
-
while ((match = kvRegex.exec(clean)) !== null) {
|
|
475
|
-
if (match[1] === "Z" || match[1] === "m") continue;
|
|
476
|
-
pairs[match[1]] = this.parseValue(match[2]);
|
|
477
|
-
}
|
|
478
|
-
return pairs;
|
|
479
|
-
}
|
|
480
|
-
parseValue(value) {
|
|
481
|
-
if (value.match(/^\d+$/)) return parseInt(value);
|
|
482
|
-
if (value.match(/^\d+\.\d+$/)) return parseFloat(value);
|
|
483
|
-
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
484
|
-
if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
485
|
-
return value;
|
|
486
|
-
}
|
|
487
|
-
parseTimestamp(value) {
|
|
488
|
-
if (!value) return null;
|
|
489
|
-
if (typeof value === "number") return value;
|
|
490
|
-
try {
|
|
491
|
-
return new Date(value).getTime();
|
|
492
|
-
} catch {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
detectComponent(action, kvPairs) {
|
|
497
|
-
if (action.includes(".")) return action.split(".")[0];
|
|
498
|
-
if (kvPairs.component) return kvPairs.component;
|
|
499
|
-
if (kvPairs.service) return kvPairs.service;
|
|
500
|
-
if (kvPairs.module) return kvPairs.module;
|
|
501
|
-
if (kvPairs.worker) return kvPairs.worker;
|
|
502
|
-
return action || "unknown";
|
|
503
|
-
}
|
|
504
|
-
detectOperation(action, kvPairs) {
|
|
505
|
-
if (action.includes(".")) return action.split(".").slice(1).join(".");
|
|
506
|
-
if (kvPairs.operation) return kvPairs.operation;
|
|
507
|
-
if (kvPairs.method) return kvPairs.method;
|
|
508
|
-
if (kvPairs.action) return kvPairs.action;
|
|
509
|
-
return action || "activity";
|
|
510
|
-
}
|
|
511
|
-
extractSessionIdentifier(activity) {
|
|
512
|
-
return activity.session_id || activity.run_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
|
|
513
|
-
}
|
|
514
|
-
detectAgentIdentifier(activity, filename, filePath) {
|
|
566
|
+
detectAgentIdentifier(activity, _filename, filePath) {
|
|
515
567
|
if (activity.agent_id) {
|
|
516
568
|
const agentId = activity.agent_id;
|
|
517
569
|
if (agentId.startsWith("vault-")) return agentId;
|
|
@@ -555,14 +607,6 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
555
607
|
const operation = activity.operation !== "activity" ? `: ${activity.operation}` : "";
|
|
556
608
|
return `${component}${operation} (${sessionId})`;
|
|
557
609
|
}
|
|
558
|
-
detectTrigger(activity) {
|
|
559
|
-
var _a, _b;
|
|
560
|
-
if (activity.trigger) return activity.trigger;
|
|
561
|
-
if (activity.method && activity.url) return "api-call";
|
|
562
|
-
if ((_a = activity.operation) == null ? void 0 : _a.includes("start")) return "startup";
|
|
563
|
-
if ((_b = activity.operation) == null ? void 0 : _b.includes("invoke")) return "invocation";
|
|
564
|
-
return "event";
|
|
565
|
-
}
|
|
566
610
|
addActivityNode(session, activity) {
|
|
567
611
|
const nodeId = `${activity.component}-${activity.operation}`;
|
|
568
612
|
if (session.nodes[nodeId]) {
|
|
@@ -580,7 +624,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
580
624
|
id: nodeId,
|
|
581
625
|
type: activity.component,
|
|
582
626
|
name: `${activity.component}: ${activity.operation}`,
|
|
583
|
-
status:
|
|
627
|
+
status: getUniversalNodeStatus(activity),
|
|
584
628
|
startTime: activity.timestamp,
|
|
585
629
|
endTime: activity.timestamp,
|
|
586
630
|
children: [],
|
|
@@ -591,17 +635,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
591
635
|
session.rootNodeId = nodeId;
|
|
592
636
|
}
|
|
593
637
|
}
|
|
594
|
-
getUniversalNodeStatus(activity) {
|
|
595
|
-
var _a, _b;
|
|
596
|
-
if (activity.level === "error" || activity.level === "fatal") return "failed";
|
|
597
|
-
if (activity.level === "warn" || activity.level === "warning") return "warning";
|
|
598
|
-
if ((_a = activity.operation) == null ? void 0 : _a.match(/start|begin|init/i)) return "running";
|
|
599
|
-
if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
|
|
600
|
-
return "completed";
|
|
601
|
-
}
|
|
602
638
|
/** Parse OpenClaw tslog-format log files with session run results. */
|
|
603
639
|
loadOpenClawLogFile(content, filename, filePath, stats) {
|
|
604
|
-
var _a, _b, _c, _d;
|
|
640
|
+
var _a, _b, _c, _d, _e, _f;
|
|
605
641
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
606
642
|
const sessions = /* @__PURE__ */ new Map();
|
|
607
643
|
for (const line of lines) {
|
|
@@ -613,13 +649,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
613
649
|
if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
|
|
614
650
|
const agentMeta = inner.meta.agentMeta;
|
|
615
651
|
const sessionId = agentMeta.sessionId || "unknown";
|
|
616
|
-
const agentName =
|
|
652
|
+
const agentName = openClawSessionIdToAgent(sessionId);
|
|
617
653
|
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_b = parsed._meta) == null ? void 0 : _b.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
|
|
618
654
|
const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
619
655
|
if (!sessions.has(sessionId)) {
|
|
620
656
|
sessions.set(sessionId, { entries: [] });
|
|
621
657
|
}
|
|
622
|
-
sessions.get(sessionId).entries.push({
|
|
658
|
+
(_c = sessions.get(sessionId)) == null ? void 0 : _c.entries.push({
|
|
623
659
|
text: texts.join("\n"),
|
|
624
660
|
timestamp,
|
|
625
661
|
sessionId,
|
|
@@ -634,16 +670,16 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
634
670
|
} catch {
|
|
635
671
|
}
|
|
636
672
|
}
|
|
637
|
-
if (parsed.payloads && ((
|
|
673
|
+
if (parsed.payloads && ((_d = parsed.meta) == null ? void 0 : _d.agentMeta)) {
|
|
638
674
|
const agentMeta = parsed.meta.agentMeta;
|
|
639
675
|
const sessionId = agentMeta.sessionId || "unknown";
|
|
640
|
-
const agentName =
|
|
641
|
-
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((
|
|
676
|
+
const agentName = openClawSessionIdToAgent(sessionId);
|
|
677
|
+
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_e = parsed._meta) == null ? void 0 : _e.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
|
|
642
678
|
const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
643
679
|
if (!sessions.has(sessionId)) {
|
|
644
680
|
sessions.set(sessionId, { entries: [] });
|
|
645
681
|
}
|
|
646
|
-
sessions.get(sessionId).entries.push({
|
|
682
|
+
(_f = sessions.get(sessionId)) == null ? void 0 : _f.entries.push({
|
|
647
683
|
text: texts.join("\n"),
|
|
648
684
|
timestamp,
|
|
649
685
|
sessionId,
|
|
@@ -722,7 +758,11 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
722
758
|
content: e.text,
|
|
723
759
|
model: e.model,
|
|
724
760
|
provider: e.provider,
|
|
725
|
-
tokens: {
|
|
761
|
+
tokens: {
|
|
762
|
+
input: e.usage.input || 0,
|
|
763
|
+
output: e.usage.output || 0,
|
|
764
|
+
total: e.usage.total || 0
|
|
765
|
+
},
|
|
726
766
|
duration: e.durationMs,
|
|
727
767
|
id: `entry-${idx}`
|
|
728
768
|
}));
|
|
@@ -743,7 +783,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
743
783
|
sourceType: "session",
|
|
744
784
|
sourceDir: path.dirname(filePath),
|
|
745
785
|
sessionEvents,
|
|
746
|
-
tokenUsage: {
|
|
786
|
+
tokenUsage: {
|
|
787
|
+
input: totalInput,
|
|
788
|
+
output: totalOutput,
|
|
789
|
+
total: totalTokens || totalInput + totalOutput,
|
|
790
|
+
cost: 0
|
|
791
|
+
},
|
|
747
792
|
metadata: {
|
|
748
793
|
provider: firstEntry.provider,
|
|
749
794
|
model: firstEntry.model,
|
|
@@ -757,16 +802,6 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
757
802
|
}
|
|
758
803
|
return traceIndex > 0;
|
|
759
804
|
}
|
|
760
|
-
/** Map OpenClaw sessionId prefix to agent name. */
|
|
761
|
-
openClawSessionIdToAgent(sessionId) {
|
|
762
|
-
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
763
|
-
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
764
|
-
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
765
|
-
if (sessionId.startsWith("main-")) return "main";
|
|
766
|
-
const firstSegment = sessionId.split("-")[0];
|
|
767
|
-
if (firstSegment) return firstSegment;
|
|
768
|
-
return "openclaw";
|
|
769
|
-
}
|
|
770
805
|
loadTraceFile(filePath) {
|
|
771
806
|
try {
|
|
772
807
|
const content = fs.readFileSync(filePath, "utf8");
|
|
@@ -905,9 +940,6 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
905
940
|
} else {
|
|
906
941
|
agentId = parentDir;
|
|
907
942
|
}
|
|
908
|
-
if (filePath.includes(".openclaw/") && !agentId.startsWith("openclaw-")) {
|
|
909
|
-
agentId = `openclaw-${agentId}`;
|
|
910
|
-
}
|
|
911
943
|
if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
|
|
912
944
|
if (!agentId.startsWith("alfred-")) {
|
|
913
945
|
agentId = `alfred-${agentId}`;
|
|
@@ -924,10 +956,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
924
956
|
if (ts > lastTimestamp) lastTimestamp = ts;
|
|
925
957
|
}
|
|
926
958
|
}
|
|
927
|
-
const firstMessage = rawEvents.find(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
959
|
+
const firstMessage = rawEvents.find(
|
|
960
|
+
(e) => {
|
|
961
|
+
var _a2;
|
|
962
|
+
return e.type === "message" && ((_a2 = e.message) == null ? void 0 : _a2.role) === "user";
|
|
963
|
+
}
|
|
964
|
+
);
|
|
931
965
|
const userPrompt = ((_d = (_c = (_b = firstMessage == null ? void 0 : firstMessage.message) == null ? void 0 : _b.content) == null ? void 0 : _c[0]) == null ? void 0 : _d.text) || "";
|
|
932
966
|
const cronMatch = userPrompt.match(/\[cron:(\S+)\s+([^\]]+)\]/);
|
|
933
967
|
const triggerName = cronMatch ? cronMatch[2] : "";
|
|
@@ -1001,7 +1035,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1001
1035
|
nodes.set(spawnId, {
|
|
1002
1036
|
id: spawnId,
|
|
1003
1037
|
type: "subagent",
|
|
1004
|
-
name:
|
|
1038
|
+
name: `Subagent: ${(((_f = evt.data) == null ? void 0 : _f.sessionId) || "").slice(0, 12)}`,
|
|
1005
1039
|
startTime: evtTs,
|
|
1006
1040
|
endTime: evtTs,
|
|
1007
1041
|
status: "completed",
|
|
@@ -1125,7 +1159,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1125
1159
|
id: evt.id,
|
|
1126
1160
|
parentId: toolCallId
|
|
1127
1161
|
});
|
|
1128
|
-
for (const [
|
|
1162
|
+
for (const [_nodeId, node] of nodes) {
|
|
1129
1163
|
if (node.type === "tool" && ((_k = node.metadata) == null ? void 0 : _k.toolCallId) === toolCallId) {
|
|
1130
1164
|
node.endTime = evtTs;
|
|
1131
1165
|
node.status = hasError ? "failed" : "completed";
|
|
@@ -1293,7 +1327,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1293
1327
|
if (filePath.startsWith(dir)) {
|
|
1294
1328
|
const dirParts = dir.split(path.sep).filter(Boolean);
|
|
1295
1329
|
const dirSuffix = dirParts.slice(-2).join("/");
|
|
1296
|
-
return path.relative(dir, filePath).replace(/\\/g, "/")
|
|
1330
|
+
return `${path.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
|
|
1297
1331
|
}
|
|
1298
1332
|
}
|
|
1299
1333
|
return filePath;
|
|
@@ -1362,7 +1396,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1362
1396
|
});
|
|
1363
1397
|
this.watchers.push(watcher);
|
|
1364
1398
|
}
|
|
1365
|
-
console.log(
|
|
1399
|
+
console.log(
|
|
1400
|
+
`Watching ${this.allWatchDirs.length} directories recursively for JSON/JSONL/LOG/TRACE files`
|
|
1401
|
+
);
|
|
1366
1402
|
}
|
|
1367
1403
|
getAllTraces() {
|
|
1368
1404
|
return Array.from(this.traces.values()).sort((a, b) => {
|
|
@@ -1463,7 +1499,8 @@ function printBanner(config, traceCount, stats) {
|
|
|
1463
1499
|
|
|
1464
1500
|
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
1465
1501
|
|
|
1466
|
-
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ?
|
|
1502
|
+
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
1503
|
+
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
1467
1504
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
1468
1505
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
1469
1506
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
@@ -1485,7 +1522,7 @@ async function startDashboard() {
|
|
|
1485
1522
|
switch (args[i]) {
|
|
1486
1523
|
case "--port":
|
|
1487
1524
|
case "-p":
|
|
1488
|
-
config.port = parseInt(args[++i]) || 3e3;
|
|
1525
|
+
config.port = parseInt(args[++i], 10) || 3e3;
|
|
1489
1526
|
break;
|
|
1490
1527
|
case "--traces":
|
|
1491
1528
|
case "-t":
|
|
@@ -1588,23 +1625,41 @@ var DashboardServer = class {
|
|
|
1588
1625
|
dataDirs: config.dataDirs
|
|
1589
1626
|
});
|
|
1590
1627
|
this.stats = new AgentStats();
|
|
1628
|
+
this.knowledgeStore = createKnowledgeStore({
|
|
1629
|
+
baseDir: path3.join(config.tracesDir, "..", ".agentflow", "knowledge")
|
|
1630
|
+
});
|
|
1591
1631
|
this.setupExpress();
|
|
1592
1632
|
this.setupWebSocket();
|
|
1593
1633
|
this.setupTraceWatcher();
|
|
1634
|
+
let knowledgeCount = 0;
|
|
1594
1635
|
for (const trace of this.watcher.getAllTraces()) {
|
|
1595
1636
|
this.stats.processTrace(trace);
|
|
1637
|
+
if (this.isGraphTrace(trace)) {
|
|
1638
|
+
try {
|
|
1639
|
+
const graph = loadGraph2(serializeTrace(trace));
|
|
1640
|
+
const event = createExecutionEvent(graph);
|
|
1641
|
+
this.knowledgeStore.append(event);
|
|
1642
|
+
knowledgeCount++;
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1596
1646
|
}
|
|
1597
1647
|
console.log(`Processed ${this.watcher.getTraceCount()} existing traces for stats`);
|
|
1648
|
+
console.log(`Persisted ${knowledgeCount} graph traces to knowledge store`);
|
|
1598
1649
|
}
|
|
1599
1650
|
app = express();
|
|
1600
1651
|
server = createServer(this.app);
|
|
1601
1652
|
wss = new WebSocketServer({ server: this.server });
|
|
1602
1653
|
watcher;
|
|
1603
1654
|
stats;
|
|
1604
|
-
processHealthCache = {
|
|
1655
|
+
processHealthCache = {
|
|
1656
|
+
result: null,
|
|
1657
|
+
ts: 0
|
|
1658
|
+
};
|
|
1659
|
+
knowledgeStore;
|
|
1605
1660
|
setupExpress() {
|
|
1606
1661
|
if (this.config.enableCors) {
|
|
1607
|
-
this.app.use((
|
|
1662
|
+
this.app.use((_req, res, next) => {
|
|
1608
1663
|
res.header("Access-Control-Allow-Origin", "*");
|
|
1609
1664
|
res.header(
|
|
1610
1665
|
"Access-Control-Allow-Headers",
|
|
@@ -1617,11 +1672,11 @@ var DashboardServer = class {
|
|
|
1617
1672
|
if (fs3.existsSync(publicDir)) {
|
|
1618
1673
|
this.app.use(express.static(publicDir));
|
|
1619
1674
|
}
|
|
1620
|
-
this.app.get("/api/traces", (
|
|
1675
|
+
this.app.get("/api/traces", (_req, res) => {
|
|
1621
1676
|
try {
|
|
1622
1677
|
const traces = this.watcher.getAllTraces().map(serializeTrace);
|
|
1623
1678
|
res.json(traces);
|
|
1624
|
-
} catch (
|
|
1679
|
+
} catch (_error) {
|
|
1625
1680
|
res.status(500).json({ error: "Failed to load traces" });
|
|
1626
1681
|
}
|
|
1627
1682
|
});
|
|
@@ -1632,7 +1687,7 @@ var DashboardServer = class {
|
|
|
1632
1687
|
return res.status(404).json({ error: "Trace not found" });
|
|
1633
1688
|
}
|
|
1634
1689
|
res.json(serializeTrace(trace));
|
|
1635
|
-
} catch (
|
|
1690
|
+
} catch (_error) {
|
|
1636
1691
|
res.status(500).json({ error: "Failed to load trace" });
|
|
1637
1692
|
}
|
|
1638
1693
|
});
|
|
@@ -1647,30 +1702,30 @@ var DashboardServer = class {
|
|
|
1647
1702
|
tokenUsage: trace.tokenUsage || null,
|
|
1648
1703
|
sourceType: trace.sourceType || "trace"
|
|
1649
1704
|
});
|
|
1650
|
-
} catch (
|
|
1705
|
+
} catch (_error) {
|
|
1651
1706
|
res.status(500).json({ error: "Failed to load trace events" });
|
|
1652
1707
|
}
|
|
1653
1708
|
});
|
|
1654
|
-
this.app.get("/api/agents", (
|
|
1709
|
+
this.app.get("/api/agents", (_req, res) => {
|
|
1655
1710
|
try {
|
|
1656
1711
|
const agents = this.stats.getAgentsList();
|
|
1657
1712
|
res.json(agents);
|
|
1658
|
-
} catch (
|
|
1713
|
+
} catch (_error) {
|
|
1659
1714
|
res.status(500).json({ error: "Failed to load agents" });
|
|
1660
1715
|
}
|
|
1661
1716
|
});
|
|
1662
|
-
this.app.get("/api/stats", (
|
|
1717
|
+
this.app.get("/api/stats", (_req, res) => {
|
|
1663
1718
|
try {
|
|
1664
1719
|
const globalStats = this.stats.getGlobalStats();
|
|
1665
1720
|
res.json(globalStats);
|
|
1666
|
-
} catch (
|
|
1721
|
+
} catch (_error) {
|
|
1667
1722
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1668
1723
|
}
|
|
1669
1724
|
});
|
|
1670
1725
|
this.app.get("/api/agents/:agentId/timeline", (req, res) => {
|
|
1671
1726
|
try {
|
|
1672
1727
|
const agentId = req.params.agentId;
|
|
1673
|
-
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
1728
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 50, 200);
|
|
1674
1729
|
const rawTraces = this.watcher.getTracesByAgent(agentId);
|
|
1675
1730
|
if (rawTraces.length === 0) {
|
|
1676
1731
|
return res.status(404).json({ error: "No traces for agent" });
|
|
@@ -1700,7 +1755,9 @@ var DashboardServer = class {
|
|
|
1700
1755
|
});
|
|
1701
1756
|
}
|
|
1702
1757
|
} else {
|
|
1703
|
-
const sorted = Object.values(nodes).sort(
|
|
1758
|
+
const sorted = Object.values(nodes).sort(
|
|
1759
|
+
(a, b) => (a.startTime || 0) - (b.startTime || 0)
|
|
1760
|
+
);
|
|
1704
1761
|
for (const node of sorted) {
|
|
1705
1762
|
activities.push({
|
|
1706
1763
|
id: node.id,
|
|
@@ -1739,95 +1796,69 @@ var DashboardServer = class {
|
|
|
1739
1796
|
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1740
1797
|
try {
|
|
1741
1798
|
const agentId = req.params.agentId;
|
|
1742
|
-
const
|
|
1743
|
-
if (
|
|
1799
|
+
const allTraces = this.watcher.getTracesByAgent(agentId);
|
|
1800
|
+
if (allTraces.length === 0) {
|
|
1744
1801
|
return res.status(404).json({ error: "No traces for agent" });
|
|
1745
1802
|
}
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1750
|
-
let totalTraces = 0;
|
|
1751
|
-
for (const trace of traces) {
|
|
1752
|
-
totalTraces++;
|
|
1753
|
-
const activities = [];
|
|
1754
|
-
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1755
|
-
for (const evt of trace.sessionEvents) {
|
|
1756
|
-
const name = evt.toolName || evt.name || evt.type;
|
|
1757
|
-
if (!name) continue;
|
|
1758
|
-
activities.push({
|
|
1759
|
-
name,
|
|
1760
|
-
type: evt.type,
|
|
1761
|
-
status: evt.toolError ? "failed" : "completed",
|
|
1762
|
-
duration: evt.duration || 0
|
|
1763
|
-
});
|
|
1764
|
-
}
|
|
1765
|
-
} else {
|
|
1766
|
-
const nodes2 = trace.nodes || {};
|
|
1767
|
-
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1768
|
-
for (const node of sorted) {
|
|
1769
|
-
activities.push({
|
|
1770
|
-
name: node.name || node.type || node.id,
|
|
1771
|
-
type: node.type || "unknown",
|
|
1772
|
-
status: node.status || "completed",
|
|
1773
|
-
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1778
|
-
for (let i = 0; i < seq.length; i++) {
|
|
1779
|
-
const act = seq[i];
|
|
1780
|
-
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1781
|
-
if (i < seq.length - 1) {
|
|
1782
|
-
const key = act + " \u2192 " + seq[i + 1];
|
|
1783
|
-
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
for (const act of activities) {
|
|
1787
|
-
if (act.duration > 0) {
|
|
1788
|
-
const durs = activityDurations.get(act.name) || [];
|
|
1789
|
-
durs.push(act.duration);
|
|
1790
|
-
activityDurations.set(act.name, durs);
|
|
1791
|
-
}
|
|
1792
|
-
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1793
|
-
if (act.status === "failed") st.fail++;
|
|
1794
|
-
else st.ok++;
|
|
1795
|
-
activityStatuses.set(act.name, st);
|
|
1796
|
-
}
|
|
1803
|
+
const graphs = this.getGraphTraces(agentId);
|
|
1804
|
+
if (graphs.length > 0) {
|
|
1805
|
+
return res.json(this.buildProcessGraphFromCore(agentId, graphs));
|
|
1797
1806
|
}
|
|
1798
|
-
|
|
1799
|
-
const durs = activityDurations.get(name) || [];
|
|
1800
|
-
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1801
|
-
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1802
|
-
return {
|
|
1803
|
-
id: name,
|
|
1804
|
-
label: name,
|
|
1805
|
-
count,
|
|
1806
|
-
frequency: count / totalTraces,
|
|
1807
|
-
avgDuration,
|
|
1808
|
-
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1809
|
-
isVirtual: name === "[START]" || name === "[END]"
|
|
1810
|
-
};
|
|
1811
|
-
});
|
|
1812
|
-
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1813
|
-
const [source, target] = key.split(" \u2192 ");
|
|
1814
|
-
return { source, target, count, frequency: count / totalTraces };
|
|
1815
|
-
});
|
|
1816
|
-
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1817
|
-
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1818
|
-
res.json({
|
|
1819
|
-
agentId,
|
|
1820
|
-
totalTraces,
|
|
1821
|
-
nodes,
|
|
1822
|
-
edges,
|
|
1823
|
-
maxEdgeCount,
|
|
1824
|
-
maxNodeCount
|
|
1825
|
-
});
|
|
1807
|
+
return res.json(this.buildProcessGraphLegacy(agentId, allTraces));
|
|
1826
1808
|
} catch (error) {
|
|
1827
1809
|
console.error("Process graph error:", error);
|
|
1828
1810
|
res.status(500).json({ error: "Failed to build process graph" });
|
|
1829
1811
|
}
|
|
1830
1812
|
});
|
|
1813
|
+
this.app.get("/api/agents/:agentId/variants", (req, res) => {
|
|
1814
|
+
try {
|
|
1815
|
+
const agentId = req.params.agentId;
|
|
1816
|
+
const graphs = this.getGraphTraces(agentId);
|
|
1817
|
+
if (graphs.length === 0) {
|
|
1818
|
+
return res.json({ agentId, totalTraces: 0, variants: [] });
|
|
1819
|
+
}
|
|
1820
|
+
const variants = findVariants(graphs).map((v) => ({
|
|
1821
|
+
pathSignature: v.pathSignature,
|
|
1822
|
+
count: v.count,
|
|
1823
|
+
percentage: v.percentage
|
|
1824
|
+
}));
|
|
1825
|
+
res.json({ agentId, totalTraces: graphs.length, variants });
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
console.error("Variants error:", error);
|
|
1828
|
+
res.status(500).json({ error: "Failed to compute variants" });
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
this.app.get("/api/agents/:agentId/bottlenecks", (req, res) => {
|
|
1832
|
+
try {
|
|
1833
|
+
const agentId = req.params.agentId;
|
|
1834
|
+
const graphs = this.getGraphTraces(agentId);
|
|
1835
|
+
if (graphs.length === 0) {
|
|
1836
|
+
return res.json({ agentId, bottlenecks: [] });
|
|
1837
|
+
}
|
|
1838
|
+
const bottlenecks = getBottlenecks(graphs).map((b) => ({
|
|
1839
|
+
nodeName: b.nodeName,
|
|
1840
|
+
nodeType: b.nodeType,
|
|
1841
|
+
occurrences: b.occurrences,
|
|
1842
|
+
durations: b.durations
|
|
1843
|
+
}));
|
|
1844
|
+
res.json({ agentId, bottlenecks });
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
console.error("Bottlenecks error:", error);
|
|
1847
|
+
res.status(500).json({ error: "Failed to compute bottlenecks" });
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
this.app.get("/api/agents/:agentId/profile", (req, res) => {
|
|
1851
|
+
try {
|
|
1852
|
+
const profile = this.knowledgeStore.getAgentProfile(req.params.agentId);
|
|
1853
|
+
if (!profile) {
|
|
1854
|
+
return res.status(404).json({ error: "No profile for agent" });
|
|
1855
|
+
}
|
|
1856
|
+
res.json(profile);
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
console.error("Profile error:", error);
|
|
1859
|
+
res.status(500).json({ error: "Failed to load agent profile" });
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1831
1862
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1832
1863
|
try {
|
|
1833
1864
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
|
@@ -1835,11 +1866,11 @@ var DashboardServer = class {
|
|
|
1835
1866
|
return res.status(404).json({ error: "Agent not found" });
|
|
1836
1867
|
}
|
|
1837
1868
|
res.json(agentStats);
|
|
1838
|
-
} catch (
|
|
1869
|
+
} catch (_error) {
|
|
1839
1870
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
1840
1871
|
}
|
|
1841
1872
|
});
|
|
1842
|
-
this.app.get("/api/process-health", (
|
|
1873
|
+
this.app.get("/api/process-health", (_req, res) => {
|
|
1843
1874
|
try {
|
|
1844
1875
|
const now = Date.now();
|
|
1845
1876
|
if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
|
|
@@ -1884,9 +1915,11 @@ var DashboardServer = class {
|
|
|
1884
1915
|
orphans: uniqueProcesses.filter((p) => {
|
|
1885
1916
|
var _a;
|
|
1886
1917
|
const alfredKnownPids = /* @__PURE__ */ new Set();
|
|
1887
|
-
if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale)
|
|
1918
|
+
if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale)
|
|
1919
|
+
alfredKnownPids.add(alfredResult.pidFile.pid);
|
|
1888
1920
|
if (alfredResult.workers) {
|
|
1889
|
-
if (alfredResult.workers.orchestratorPid)
|
|
1921
|
+
if (alfredResult.workers.orchestratorPid)
|
|
1922
|
+
alfredKnownPids.add(alfredResult.workers.orchestratorPid);
|
|
1890
1923
|
for (const w of alfredResult.workers.workers) {
|
|
1891
1924
|
if (w.pid) alfredKnownPids.add(w.pid);
|
|
1892
1925
|
}
|
|
@@ -1905,11 +1938,22 @@ var DashboardServer = class {
|
|
|
1905
1938
|
result.problems = [...alfredResult.problems || [], ...openclawProblems];
|
|
1906
1939
|
this.processHealthCache = { result, ts: now };
|
|
1907
1940
|
res.json(result);
|
|
1908
|
-
} catch (
|
|
1941
|
+
} catch (_error) {
|
|
1909
1942
|
res.status(500).json({ error: "Failed to audit processes" });
|
|
1910
1943
|
}
|
|
1911
1944
|
});
|
|
1912
|
-
this.app.get("
|
|
1945
|
+
this.app.get("/health", (_req, res) => {
|
|
1946
|
+
res.json({
|
|
1947
|
+
status: "ok",
|
|
1948
|
+
uptime: process.uptime(),
|
|
1949
|
+
traceCount: this.watcher.getTraceCount(),
|
|
1950
|
+
agentCount: this.watcher.getAgentIds().length
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
this.app.get("/ready", (_req, res) => {
|
|
1954
|
+
res.json({ status: "ready" });
|
|
1955
|
+
});
|
|
1956
|
+
this.app.get("*", (_req, res) => {
|
|
1913
1957
|
const indexPath = path3.join(__dirname, "../public/index.html");
|
|
1914
1958
|
if (fs3.existsSync(indexPath)) {
|
|
1915
1959
|
res.sendFile(indexPath);
|
|
@@ -1938,6 +1982,189 @@ var DashboardServer = class {
|
|
|
1938
1982
|
});
|
|
1939
1983
|
});
|
|
1940
1984
|
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
|
|
1987
|
+
* Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
|
|
1988
|
+
*/
|
|
1989
|
+
getGraphTraces(agentId) {
|
|
1990
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1991
|
+
const graphs = [];
|
|
1992
|
+
for (const trace of traces) {
|
|
1993
|
+
try {
|
|
1994
|
+
if (trace.sourceType === "session" || trace.sourceType === "log") continue;
|
|
1995
|
+
if (!trace.rootNodeId && !trace.rootId) continue;
|
|
1996
|
+
const nodes = trace.nodes;
|
|
1997
|
+
if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
|
|
1998
|
+
const nodeValues = Object.values(nodes);
|
|
1999
|
+
if (nodeValues.some((n) => n.type === "log-file" || n.type === "log-entry")) continue;
|
|
2000
|
+
graphs.push(loadGraph2(trace));
|
|
2001
|
+
} catch {
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return graphs;
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Build process graph response using core APIs (discoverProcess + getBottlenecks).
|
|
2008
|
+
* Maps core output to the frontend's expected shape with virtual START/END nodes.
|
|
2009
|
+
*/
|
|
2010
|
+
buildProcessGraphFromCore(agentId, graphs) {
|
|
2011
|
+
const model = discoverProcess(graphs);
|
|
2012
|
+
const bottleneckList = getBottlenecks(graphs);
|
|
2013
|
+
const bottleneckMap = /* @__PURE__ */ new Map();
|
|
2014
|
+
for (const b of bottleneckList) {
|
|
2015
|
+
const key = `${b.nodeType}:${b.nodeName}`;
|
|
2016
|
+
bottleneckMap.set(key, {
|
|
2017
|
+
avgDuration: b.durations.median,
|
|
2018
|
+
failRate: 0,
|
|
2019
|
+
// Not directly available from bottleneck data
|
|
2020
|
+
p95: b.durations.p95
|
|
2021
|
+
});
|
|
2022
|
+
bottleneckMap.set(b.nodeName, {
|
|
2023
|
+
avgDuration: b.durations.median,
|
|
2024
|
+
failRate: 0,
|
|
2025
|
+
p95: b.durations.p95
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
const nodes = [];
|
|
2029
|
+
const stepCounts = /* @__PURE__ */ new Map();
|
|
2030
|
+
for (const t of model.transitions) {
|
|
2031
|
+
stepCounts.set(t.from, (stepCounts.get(t.from) ?? 0) + t.count);
|
|
2032
|
+
}
|
|
2033
|
+
for (const step of model.steps) {
|
|
2034
|
+
const count = stepCounts.get(step) ?? model.totalGraphs;
|
|
2035
|
+
const bn = bottleneckMap.get(step);
|
|
2036
|
+
const colonIdx = step.indexOf(":");
|
|
2037
|
+
const label = colonIdx >= 0 ? step.slice(colonIdx + 1) : step;
|
|
2038
|
+
nodes.push({
|
|
2039
|
+
id: step,
|
|
2040
|
+
label,
|
|
2041
|
+
count,
|
|
2042
|
+
frequency: count / model.totalGraphs,
|
|
2043
|
+
avgDuration: (bn == null ? void 0 : bn.avgDuration) ?? 0,
|
|
2044
|
+
failRate: (bn == null ? void 0 : bn.failRate) ?? 0,
|
|
2045
|
+
p95Duration: (bn == null ? void 0 : bn.p95) ?? 0,
|
|
2046
|
+
isVirtual: false
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
const rootSteps = new Set(model.steps);
|
|
2050
|
+
const childSteps = new Set(model.transitions.map((t) => t.to));
|
|
2051
|
+
const leafSteps = new Set(model.steps);
|
|
2052
|
+
for (const t of model.transitions) {
|
|
2053
|
+
}
|
|
2054
|
+
nodes.push({ id: "[START]", label: "[START]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
|
|
2055
|
+
nodes.push({ id: "[END]", label: "[END]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
|
|
2056
|
+
const edges = model.transitions.map((t) => ({
|
|
2057
|
+
source: t.from,
|
|
2058
|
+
target: t.to,
|
|
2059
|
+
count: t.count,
|
|
2060
|
+
frequency: t.count / model.totalGraphs
|
|
2061
|
+
}));
|
|
2062
|
+
const targetSteps = new Set(model.transitions.map((t) => t.to));
|
|
2063
|
+
for (const step of model.steps) {
|
|
2064
|
+
if (!targetSteps.has(step)) {
|
|
2065
|
+
edges.push({ source: "[START]", target: step, count: model.totalGraphs, frequency: 1 });
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const sourceSteps = new Set(model.transitions.map((t) => t.from));
|
|
2069
|
+
for (const step of model.steps) {
|
|
2070
|
+
if (!sourceSteps.has(step)) {
|
|
2071
|
+
edges.push({ source: step, target: "[END]", count: model.totalGraphs, frequency: 1 });
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
2075
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
2076
|
+
return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Legacy process graph computation for session-based traces.
|
|
2080
|
+
* Preserved for backward compatibility with JSONL/LOG traces.
|
|
2081
|
+
*/
|
|
2082
|
+
buildProcessGraphLegacy(agentId, allTraces) {
|
|
2083
|
+
const traces = allTraces.map(serializeTrace);
|
|
2084
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
2085
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
2086
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
2087
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
2088
|
+
let totalTraces = 0;
|
|
2089
|
+
for (const trace of traces) {
|
|
2090
|
+
totalTraces++;
|
|
2091
|
+
const activities = [];
|
|
2092
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
2093
|
+
for (const evt of trace.sessionEvents) {
|
|
2094
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
2095
|
+
if (!name) continue;
|
|
2096
|
+
activities.push({
|
|
2097
|
+
name,
|
|
2098
|
+
type: evt.type,
|
|
2099
|
+
status: evt.toolError ? "failed" : "completed",
|
|
2100
|
+
duration: evt.duration || 0
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
} else {
|
|
2104
|
+
const nodes2 = trace.nodes || {};
|
|
2105
|
+
const sorted = Object.values(nodes2).sort(
|
|
2106
|
+
(a, b) => (a.startTime || 0) - (b.startTime || 0)
|
|
2107
|
+
);
|
|
2108
|
+
for (const node of sorted) {
|
|
2109
|
+
activities.push({
|
|
2110
|
+
name: node.name || node.type || node.id,
|
|
2111
|
+
type: node.type || "unknown",
|
|
2112
|
+
status: node.status || "completed",
|
|
2113
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
2118
|
+
for (let i = 0; i < seq.length; i++) {
|
|
2119
|
+
const act = seq[i];
|
|
2120
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
2121
|
+
if (i < seq.length - 1) {
|
|
2122
|
+
const key = `${act} \u2192 ${seq[i + 1]}`;
|
|
2123
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
for (const act of activities) {
|
|
2127
|
+
if (act.duration > 0) {
|
|
2128
|
+
const durs = activityDurations.get(act.name) || [];
|
|
2129
|
+
durs.push(act.duration);
|
|
2130
|
+
activityDurations.set(act.name, durs);
|
|
2131
|
+
}
|
|
2132
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
2133
|
+
if (act.status === "failed") st.fail++;
|
|
2134
|
+
else st.ok++;
|
|
2135
|
+
activityStatuses.set(act.name, st);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
2139
|
+
const durs = activityDurations.get(name) || [];
|
|
2140
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
2141
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
2142
|
+
return {
|
|
2143
|
+
id: name,
|
|
2144
|
+
label: name,
|
|
2145
|
+
count,
|
|
2146
|
+
frequency: count / totalTraces,
|
|
2147
|
+
avgDuration,
|
|
2148
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
2149
|
+
p95Duration: 0,
|
|
2150
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
2151
|
+
};
|
|
2152
|
+
});
|
|
2153
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
2154
|
+
const [source, target] = key.split(" \u2192 ");
|
|
2155
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
2156
|
+
});
|
|
2157
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
2158
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
2159
|
+
return { agentId, totalTraces, nodes, edges, maxEdgeCount, maxNodeCount };
|
|
2160
|
+
}
|
|
2161
|
+
/** Check if a trace is a proper ExecutionGraph (not a synthetic session-based trace). */
|
|
2162
|
+
isGraphTrace(trace) {
|
|
2163
|
+
if (trace.sourceType === "session" || trace.sourceType === "log") return false;
|
|
2164
|
+
if (!trace.rootNodeId && !trace.rootId) return false;
|
|
2165
|
+
const nodes = trace.nodes instanceof Map ? Object.fromEntries(trace.nodes) : trace.nodes;
|
|
2166
|
+
return nodes && typeof nodes === "object" && Object.keys(nodes).length > 0;
|
|
2167
|
+
}
|
|
1941
2168
|
setupTraceWatcher() {
|
|
1942
2169
|
this.watcher.on("trace-added", (trace) => {
|
|
1943
2170
|
this.stats.processTrace(trace);
|
|
@@ -1945,6 +2172,14 @@ var DashboardServer = class {
|
|
|
1945
2172
|
type: "trace-added",
|
|
1946
2173
|
data: serializeTrace(trace)
|
|
1947
2174
|
});
|
|
2175
|
+
if (this.isGraphTrace(trace)) {
|
|
2176
|
+
try {
|
|
2177
|
+
const graph = loadGraph2(serializeTrace(trace));
|
|
2178
|
+
const event = createExecutionEvent(graph);
|
|
2179
|
+
this.knowledgeStore.append(event);
|
|
2180
|
+
} catch {
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
1948
2183
|
});
|
|
1949
2184
|
this.watcher.on("trace-updated", (trace) => {
|
|
1950
2185
|
this.stats.processTrace(trace);
|