agentflow-dashboard 0.3.1 → 0.4.1
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 +116 -314
- package/dist/{chunk-EDHK4NJD.js → chunk-YDFLDRWO.js} +673 -56
- package/dist/cli.cjs +673 -56
- package/dist/cli.js +1 -1
- package/dist/index.cjs +673 -56
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +217 -0
- package/dist/public/index.html +20 -1
- package/dist/server.cjs +673 -56
- package/dist/server.js +1 -1
- package/package.json +20 -4
- package/public/dashboard.js +217 -0
- package/public/index.html +20 -1
package/dist/server.cjs
CHANGED
|
@@ -221,7 +221,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
221
221
|
var import_events = require("events");
|
|
222
222
|
var fs = __toESM(require("fs"), 1);
|
|
223
223
|
var path = __toESM(require("path"), 1);
|
|
224
|
-
var TraceWatcher = class extends import_events.EventEmitter {
|
|
224
|
+
var TraceWatcher = class _TraceWatcher extends import_events.EventEmitter {
|
|
225
225
|
watchers = [];
|
|
226
226
|
traces = /* @__PURE__ */ new Map();
|
|
227
227
|
tracesDir;
|
|
@@ -249,22 +249,65 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
249
249
|
}
|
|
250
250
|
loadExistingFiles() {
|
|
251
251
|
let totalFiles = 0;
|
|
252
|
+
let totalDirectories = 0;
|
|
252
253
|
for (const dir of this.allWatchDirs) {
|
|
253
254
|
if (!fs.existsSync(dir)) continue;
|
|
254
255
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.loadFile(path.join(dir, file));
|
|
259
|
-
}
|
|
256
|
+
totalDirectories++;
|
|
257
|
+
const loadedFiles = this.scanDirectoryRecursive(dir);
|
|
258
|
+
totalFiles += loadedFiles;
|
|
260
259
|
} catch (error) {
|
|
261
260
|
console.error(`Error scanning directory ${dir}:`, error);
|
|
262
261
|
}
|
|
263
262
|
}
|
|
264
|
-
console.log(`Scanned ${
|
|
263
|
+
console.log(`Scanned ${totalDirectories} directories (recursive), loaded ${this.traces.size} items from ${totalFiles} files`);
|
|
264
|
+
}
|
|
265
|
+
/** Recursively scan directory for supported file types */
|
|
266
|
+
scanDirectoryRecursive(dir, depth = 0) {
|
|
267
|
+
if (depth > 10) return 0;
|
|
268
|
+
let fileCount = 0;
|
|
269
|
+
try {
|
|
270
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.name.startsWith(".")) continue;
|
|
273
|
+
const fullPath = path.join(dir, entry.name);
|
|
274
|
+
if (entry.isFile()) {
|
|
275
|
+
if (this.isSupportedFile(entry.name)) {
|
|
276
|
+
if (this.loadFile(fullPath)) {
|
|
277
|
+
fileCount++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (entry.isDirectory()) {
|
|
281
|
+
fileCount += this.scanDirectoryRecursive(fullPath, depth + 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn(`Cannot read directory ${dir}:`, error.message);
|
|
286
|
+
}
|
|
287
|
+
return fileCount;
|
|
288
|
+
}
|
|
289
|
+
/** Check if file type is supported */
|
|
290
|
+
isSupportedFile(filename) {
|
|
291
|
+
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
265
292
|
}
|
|
293
|
+
/** File names that are config/state, not traces — skip them. */
|
|
294
|
+
static SKIP_FILES = /* @__PURE__ */ new Set([
|
|
295
|
+
"workers.json",
|
|
296
|
+
"package.json",
|
|
297
|
+
"package-lock.json",
|
|
298
|
+
"tsconfig.json",
|
|
299
|
+
"biome.json",
|
|
300
|
+
"jobs.json",
|
|
301
|
+
"auth.json",
|
|
302
|
+
"models.json",
|
|
303
|
+
"config.json"
|
|
304
|
+
]);
|
|
305
|
+
static SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
|
|
266
306
|
/** Load a .json trace, .jsonl session file, or .log file. */
|
|
267
307
|
loadFile(filePath) {
|
|
308
|
+
const filename = path.basename(filePath);
|
|
309
|
+
if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
|
|
310
|
+
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
268
311
|
if (filePath.endsWith(".jsonl")) {
|
|
269
312
|
return this.loadSessionFile(filePath);
|
|
270
313
|
}
|
|
@@ -278,12 +321,23 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
278
321
|
const content = fs.readFileSync(filePath, "utf8");
|
|
279
322
|
const filename = path.basename(filePath);
|
|
280
323
|
const stats = fs.statSync(filePath);
|
|
324
|
+
if (filename.startsWith("openclaw-") || filePath.includes("openclaw")) {
|
|
325
|
+
const result = this.loadOpenClawLogFile(content, filename, filePath, stats);
|
|
326
|
+
if (result) return true;
|
|
327
|
+
}
|
|
281
328
|
const traces = this.parseUniversalLog(content, filename, filePath);
|
|
282
329
|
for (let i = 0; i < traces.length; i++) {
|
|
283
330
|
const trace = traces[i];
|
|
331
|
+
if (trace.nodes && !(trace.nodes instanceof Map)) {
|
|
332
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const [key2, value] of Object.entries(trace.nodes)) {
|
|
334
|
+
nodeMap.set(key2, value);
|
|
335
|
+
}
|
|
336
|
+
trace.nodes = nodeMap;
|
|
337
|
+
}
|
|
284
338
|
trace.filename = filename;
|
|
285
339
|
trace.lastModified = stats.mtime.getTime();
|
|
286
|
-
trace.sourceType = "trace";
|
|
340
|
+
trace.sourceType = trace.sourceType || "trace";
|
|
287
341
|
trace.sourceDir = path.dirname(filePath);
|
|
288
342
|
const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
|
|
289
343
|
this.traces.set(key, trace);
|
|
@@ -323,10 +377,26 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
323
377
|
if (activity.timestamp > session.endTime) {
|
|
324
378
|
session.endTime = activity.timestamp;
|
|
325
379
|
}
|
|
380
|
+
if (activity.level === "error" || activity.level === "fatal") {
|
|
381
|
+
session.status = "failed";
|
|
382
|
+
}
|
|
326
383
|
}
|
|
327
384
|
const traces = Array.from(activities.values()).filter(
|
|
328
385
|
(session) => Object.keys(session.nodes).length > 0
|
|
329
386
|
);
|
|
387
|
+
for (const trace of traces) {
|
|
388
|
+
const sortedNodes = Object.values(trace.nodes).sort((a, b) => a.startTime - b.startTime);
|
|
389
|
+
trace.sessionEvents = sortedNodes.map((node) => ({
|
|
390
|
+
type: node.status === "failed" ? "tool_result" : "system",
|
|
391
|
+
timestamp: node.startTime,
|
|
392
|
+
name: node.name,
|
|
393
|
+
content: node.metadata.count > 1 ? `${node.name} (${node.metadata.count} occurrences, ${node.metadata.errorCount || 0} errors)` : node.name,
|
|
394
|
+
duration: node.endTime - node.startTime,
|
|
395
|
+
toolError: node.status === "failed" ? `${node.metadata.errorCount || 1} error(s)` : void 0,
|
|
396
|
+
id: node.id
|
|
397
|
+
}));
|
|
398
|
+
trace.sourceType = "session";
|
|
399
|
+
}
|
|
330
400
|
if (traces.length === 0) {
|
|
331
401
|
const stats = fs.statSync(filePath);
|
|
332
402
|
traces.push({
|
|
@@ -340,6 +410,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
340
410
|
status: "completed",
|
|
341
411
|
startTime: stats.mtime.getTime(),
|
|
342
412
|
endTime: stats.mtime.getTime(),
|
|
413
|
+
children: [],
|
|
343
414
|
metadata: { lineCount: lines.length, path: filePath }
|
|
344
415
|
}
|
|
345
416
|
},
|
|
@@ -406,38 +477,37 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
406
477
|
...kvPairs
|
|
407
478
|
};
|
|
408
479
|
}
|
|
480
|
+
/** Strip ANSI escape codes from a string. */
|
|
481
|
+
stripAnsi(str) {
|
|
482
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
483
|
+
}
|
|
409
484
|
extractTimestamp(line) {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
|
|
485
|
+
const clean = this.stripAnsi(line);
|
|
486
|
+
const isoMatch = clean.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
|
|
413
487
|
if (isoMatch) return new Date(isoMatch[1]).getTime();
|
|
414
488
|
return null;
|
|
415
489
|
}
|
|
416
490
|
extractLogLevel(line) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return levelMatch ? levelMatch[1].toLowerCase() : null;
|
|
491
|
+
const clean = this.stripAnsi(line);
|
|
492
|
+
const levelMatch = clean.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
|
|
493
|
+
return levelMatch ? levelMatch[1].trim().toLowerCase() : null;
|
|
421
494
|
}
|
|
422
495
|
extractAction(line) {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
const clean = this.stripAnsi(line);
|
|
497
|
+
const actionMatch = clean.match(/\]\s+(\S+)/);
|
|
498
|
+
if (actionMatch) return actionMatch[1].trim();
|
|
499
|
+
const afterLevel = clean.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*\]?\s*/i, "");
|
|
500
|
+
return afterLevel.split(/\s+/)[0] || "";
|
|
427
501
|
}
|
|
428
502
|
extractKeyValuePairs(line) {
|
|
429
503
|
const pairs = {};
|
|
430
|
-
const
|
|
504
|
+
const clean = this.stripAnsi(line);
|
|
505
|
+
const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
|
|
431
506
|
let match;
|
|
432
|
-
while ((match =
|
|
507
|
+
while ((match = kvRegex.exec(clean)) !== null) {
|
|
508
|
+
if (match[1] === "Z" || match[1] === "m") continue;
|
|
433
509
|
pairs[match[1]] = this.parseValue(match[2]);
|
|
434
510
|
}
|
|
435
|
-
if (Object.keys(pairs).length === 0) {
|
|
436
|
-
const kvRegex = /(\w+)=([^\s]+)/g;
|
|
437
|
-
while ((match = kvRegex.exec(line)) !== null) {
|
|
438
|
-
pairs[match[1]] = this.parseValue(match[2]);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
511
|
return pairs;
|
|
442
512
|
}
|
|
443
513
|
parseValue(value) {
|
|
@@ -461,30 +531,51 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
461
531
|
if (kvPairs.component) return kvPairs.component;
|
|
462
532
|
if (kvPairs.service) return kvPairs.service;
|
|
463
533
|
if (kvPairs.module) return kvPairs.module;
|
|
464
|
-
return
|
|
534
|
+
if (kvPairs.worker) return kvPairs.worker;
|
|
535
|
+
return action || "unknown";
|
|
465
536
|
}
|
|
466
537
|
detectOperation(action, kvPairs) {
|
|
467
538
|
if (action.includes(".")) return action.split(".").slice(1).join(".");
|
|
468
539
|
if (kvPairs.operation) return kvPairs.operation;
|
|
469
540
|
if (kvPairs.method) return kvPairs.method;
|
|
541
|
+
if (kvPairs.action) return kvPairs.action;
|
|
470
542
|
return action || "activity";
|
|
471
543
|
}
|
|
472
544
|
extractSessionIdentifier(activity) {
|
|
473
|
-
return activity.
|
|
545
|
+
return activity.session_id || activity.run_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
|
|
474
546
|
}
|
|
475
547
|
detectAgentIdentifier(activity, filename, filePath) {
|
|
476
|
-
if (activity.
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
|
|
548
|
+
if (activity.agent_id) {
|
|
549
|
+
const agentId = activity.agent_id;
|
|
550
|
+
if (agentId.startsWith("vault-")) return agentId;
|
|
551
|
+
if (agentId === "main" && filePath.includes(".alfred/")) return "alfred-main";
|
|
552
|
+
return agentId;
|
|
553
|
+
}
|
|
554
|
+
const pathAgent = this.extractAgentFromPath(filePath);
|
|
555
|
+
if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
|
|
556
|
+
const basename2 = path.basename(filePath, path.extname(filePath));
|
|
557
|
+
if (basename2.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
|
|
558
|
+
return basename2 === "alfred" ? "alfred" : `alfred-${basename2}`;
|
|
480
559
|
}
|
|
481
|
-
return activity.component;
|
|
482
560
|
}
|
|
483
|
-
return
|
|
561
|
+
return pathAgent;
|
|
484
562
|
}
|
|
485
563
|
extractAgentFromPath(filePath) {
|
|
486
564
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
487
565
|
const pathParts = filePath.split(path.sep);
|
|
566
|
+
if (filePath.includes(".openclaw/")) {
|
|
567
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
568
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
569
|
+
return `openclaw-${pathParts[agentsIndex + 1]}`;
|
|
570
|
+
}
|
|
571
|
+
if (filename.startsWith("openclaw-")) {
|
|
572
|
+
return "openclaw-gateway";
|
|
573
|
+
}
|
|
574
|
+
return "openclaw";
|
|
575
|
+
}
|
|
576
|
+
if (filePath.includes(".alfred/") || filename.includes("alfred")) {
|
|
577
|
+
return "alfred";
|
|
578
|
+
}
|
|
488
579
|
for (const part of pathParts.reverse()) {
|
|
489
580
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
490
581
|
return part;
|
|
@@ -506,7 +597,18 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
506
597
|
return "event";
|
|
507
598
|
}
|
|
508
599
|
addActivityNode(session, activity) {
|
|
509
|
-
const nodeId = `${activity.component}-${activity.operation}
|
|
600
|
+
const nodeId = `${activity.component}-${activity.operation}`;
|
|
601
|
+
if (session.nodes[nodeId]) {
|
|
602
|
+
const node2 = session.nodes[nodeId];
|
|
603
|
+
node2.endTime = Math.max(node2.endTime, activity.timestamp);
|
|
604
|
+
node2.startTime = Math.min(node2.startTime, activity.timestamp);
|
|
605
|
+
node2.metadata.count = (node2.metadata.count || 1) + 1;
|
|
606
|
+
if (activity.level === "error" || activity.level === "fatal") {
|
|
607
|
+
node2.status = "failed";
|
|
608
|
+
node2.metadata.errorCount = (node2.metadata.errorCount || 0) + 1;
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
510
612
|
const node = {
|
|
511
613
|
id: nodeId,
|
|
512
614
|
type: activity.component,
|
|
@@ -514,7 +616,8 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
514
616
|
status: this.getUniversalNodeStatus(activity),
|
|
515
617
|
startTime: activity.timestamp,
|
|
516
618
|
endTime: activity.timestamp,
|
|
517
|
-
|
|
619
|
+
children: [],
|
|
620
|
+
metadata: { ...activity, count: 1 }
|
|
518
621
|
};
|
|
519
622
|
session.nodes[nodeId] = node;
|
|
520
623
|
if (!session.rootNodeId) {
|
|
@@ -529,22 +632,275 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
529
632
|
if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
|
|
530
633
|
return "completed";
|
|
531
634
|
}
|
|
635
|
+
/** Parse OpenClaw tslog-format log files with session run results. */
|
|
636
|
+
loadOpenClawLogFile(content, filename, filePath, stats) {
|
|
637
|
+
var _a, _b, _c, _d;
|
|
638
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
639
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const line of lines) {
|
|
641
|
+
try {
|
|
642
|
+
const parsed = JSON.parse(line);
|
|
643
|
+
if (parsed["0"] && typeof parsed["0"] === "string") {
|
|
644
|
+
try {
|
|
645
|
+
const inner = typeof parsed["0"] === "string" && parsed["0"].startsWith("{") ? JSON.parse(parsed["0"]) : null;
|
|
646
|
+
if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
|
|
647
|
+
const agentMeta = inner.meta.agentMeta;
|
|
648
|
+
const sessionId = agentMeta.sessionId || "unknown";
|
|
649
|
+
const agentName = this.openClawSessionIdToAgent(sessionId);
|
|
650
|
+
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();
|
|
651
|
+
const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
652
|
+
if (!sessions.has(sessionId)) {
|
|
653
|
+
sessions.set(sessionId, { entries: [] });
|
|
654
|
+
}
|
|
655
|
+
sessions.get(sessionId).entries.push({
|
|
656
|
+
text: texts.join("\n"),
|
|
657
|
+
timestamp,
|
|
658
|
+
sessionId,
|
|
659
|
+
provider: agentMeta.provider || "",
|
|
660
|
+
model: agentMeta.model || "",
|
|
661
|
+
usage: agentMeta.usage || {},
|
|
662
|
+
durationMs: inner.meta.durationMs || 0,
|
|
663
|
+
agentName
|
|
664
|
+
});
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (parsed.payloads && ((_c = parsed.meta) == null ? void 0 : _c.agentMeta)) {
|
|
671
|
+
const agentMeta = parsed.meta.agentMeta;
|
|
672
|
+
const sessionId = agentMeta.sessionId || "unknown";
|
|
673
|
+
const agentName = this.openClawSessionIdToAgent(sessionId);
|
|
674
|
+
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_d = parsed._meta) == null ? void 0 : _d.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
|
|
675
|
+
const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
676
|
+
if (!sessions.has(sessionId)) {
|
|
677
|
+
sessions.set(sessionId, { entries: [] });
|
|
678
|
+
}
|
|
679
|
+
sessions.get(sessionId).entries.push({
|
|
680
|
+
text: texts.join("\n"),
|
|
681
|
+
timestamp,
|
|
682
|
+
sessionId,
|
|
683
|
+
provider: agentMeta.provider || "",
|
|
684
|
+
model: agentMeta.model || "",
|
|
685
|
+
usage: agentMeta.usage || {},
|
|
686
|
+
durationMs: parsed.meta.durationMs || 0,
|
|
687
|
+
agentName
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (sessions.size === 0) return false;
|
|
694
|
+
let traceIndex = 0;
|
|
695
|
+
for (const [sessionId, session] of sessions) {
|
|
696
|
+
const entries = session.entries;
|
|
697
|
+
if (entries.length === 0) continue;
|
|
698
|
+
const firstEntry = entries[0];
|
|
699
|
+
const lastEntry = entries[entries.length - 1];
|
|
700
|
+
const agentId = firstEntry.agentName;
|
|
701
|
+
let totalInput = 0, totalOutput = 0, totalTokens = 0;
|
|
702
|
+
let totalDuration = 0;
|
|
703
|
+
for (const entry of entries) {
|
|
704
|
+
totalInput += entry.usage.input || 0;
|
|
705
|
+
totalOutput += entry.usage.output || 0;
|
|
706
|
+
totalTokens += entry.usage.total || 0;
|
|
707
|
+
totalDuration += entry.durationMs;
|
|
708
|
+
}
|
|
709
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
710
|
+
const rootId = `openclaw-${sessionId.slice(0, 12)}`;
|
|
711
|
+
for (let j = 0; j < entries.length; j++) {
|
|
712
|
+
const e = entries[j];
|
|
713
|
+
const nodeId = `entry-${j}`;
|
|
714
|
+
nodes.set(nodeId, {
|
|
715
|
+
id: nodeId,
|
|
716
|
+
type: "tool",
|
|
717
|
+
name: `${e.model}: ${e.sessionId}`,
|
|
718
|
+
startTime: e.timestamp - e.durationMs,
|
|
719
|
+
endTime: e.timestamp,
|
|
720
|
+
status: "completed",
|
|
721
|
+
parentId: rootId,
|
|
722
|
+
children: [],
|
|
723
|
+
metadata: {
|
|
724
|
+
provider: e.provider,
|
|
725
|
+
model: e.model,
|
|
726
|
+
durationMs: e.durationMs,
|
|
727
|
+
usage: e.usage,
|
|
728
|
+
preview: e.text.slice(0, 200)
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
nodes.set(rootId, {
|
|
733
|
+
id: rootId,
|
|
734
|
+
type: "agent",
|
|
735
|
+
name: sessionId,
|
|
736
|
+
startTime: firstEntry.timestamp - (firstEntry.durationMs || 0),
|
|
737
|
+
endTime: lastEntry.timestamp,
|
|
738
|
+
status: "completed",
|
|
739
|
+
parentId: void 0,
|
|
740
|
+
children: Array.from(nodes.keys()).filter((k) => k !== rootId),
|
|
741
|
+
metadata: {
|
|
742
|
+
provider: firstEntry.provider,
|
|
743
|
+
model: firstEntry.model,
|
|
744
|
+
sessionId,
|
|
745
|
+
totalTokens,
|
|
746
|
+
inputTokens: totalInput,
|
|
747
|
+
outputTokens: totalOutput,
|
|
748
|
+
durationMs: totalDuration
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
const sessionEvents = entries.map((e, idx) => ({
|
|
752
|
+
type: "assistant",
|
|
753
|
+
timestamp: e.timestamp,
|
|
754
|
+
name: e.model,
|
|
755
|
+
content: e.text,
|
|
756
|
+
model: e.model,
|
|
757
|
+
provider: e.provider,
|
|
758
|
+
tokens: { input: e.usage.input || 0, output: e.usage.output || 0, total: e.usage.total || 0 },
|
|
759
|
+
duration: e.durationMs,
|
|
760
|
+
id: `entry-${idx}`
|
|
761
|
+
}));
|
|
762
|
+
const trace = {
|
|
763
|
+
id: sessionId,
|
|
764
|
+
nodes,
|
|
765
|
+
edges: [],
|
|
766
|
+
events: [],
|
|
767
|
+
startTime: firstEntry.timestamp - (firstEntry.durationMs || 0),
|
|
768
|
+
endTime: lastEntry.timestamp,
|
|
769
|
+
agentId,
|
|
770
|
+
trigger: "cron",
|
|
771
|
+
name: sessionId,
|
|
772
|
+
traceId: sessionId,
|
|
773
|
+
spanId: sessionId,
|
|
774
|
+
filename,
|
|
775
|
+
lastModified: stats.mtime.getTime(),
|
|
776
|
+
sourceType: "session",
|
|
777
|
+
sourceDir: path.dirname(filePath),
|
|
778
|
+
sessionEvents,
|
|
779
|
+
tokenUsage: { input: totalInput, output: totalOutput, total: totalTokens || totalInput + totalOutput, cost: 0 },
|
|
780
|
+
metadata: {
|
|
781
|
+
provider: firstEntry.provider,
|
|
782
|
+
model: firstEntry.model,
|
|
783
|
+
durationMs: totalDuration,
|
|
784
|
+
source: "openclaw-log"
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
const key = sessions.size === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${traceIndex}`;
|
|
788
|
+
this.traces.set(key, trace);
|
|
789
|
+
traceIndex++;
|
|
790
|
+
}
|
|
791
|
+
return traceIndex > 0;
|
|
792
|
+
}
|
|
793
|
+
/** Map OpenClaw sessionId prefix to agent name. */
|
|
794
|
+
openClawSessionIdToAgent(sessionId) {
|
|
795
|
+
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
796
|
+
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
797
|
+
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
798
|
+
if (sessionId.startsWith("main-")) return "main";
|
|
799
|
+
const firstSegment = sessionId.split("-")[0];
|
|
800
|
+
if (firstSegment) return firstSegment;
|
|
801
|
+
return "openclaw";
|
|
802
|
+
}
|
|
532
803
|
loadTraceFile(filePath) {
|
|
533
804
|
try {
|
|
534
805
|
const content = fs.readFileSync(filePath, "utf8");
|
|
535
|
-
const graph = (0, import_agentflow_core2.loadGraph)(content);
|
|
536
806
|
const filename = path.basename(filePath);
|
|
807
|
+
if (filename === "sessions.json") {
|
|
808
|
+
return this.loadSessionsIndex(filePath, content);
|
|
809
|
+
}
|
|
810
|
+
const graph = (0, import_agentflow_core2.loadGraph)(content);
|
|
537
811
|
const stats = fs.statSync(filePath);
|
|
538
812
|
graph.filename = filename;
|
|
539
813
|
graph.lastModified = stats.mtime.getTime();
|
|
540
814
|
graph.sourceType = "trace";
|
|
541
815
|
graph.sourceDir = path.dirname(filePath);
|
|
816
|
+
if (graph.nodes instanceof Map) {
|
|
817
|
+
for (const node of graph.nodes.values()) {
|
|
818
|
+
if (!node.children) node.children = [];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
542
821
|
this.traces.set(this.traceKey(filePath), graph);
|
|
543
822
|
return true;
|
|
544
823
|
} catch {
|
|
545
824
|
return false;
|
|
546
825
|
}
|
|
547
826
|
}
|
|
827
|
+
/** Parse sessions.json index to discover agents and their sessions. */
|
|
828
|
+
loadSessionsIndex(filePath, content) {
|
|
829
|
+
try {
|
|
830
|
+
const data = JSON.parse(content);
|
|
831
|
+
if (typeof data !== "object" || data === null) return false;
|
|
832
|
+
const stats = fs.statSync(filePath);
|
|
833
|
+
const pathParts = filePath.split(path.sep);
|
|
834
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
835
|
+
if (agentsIndex === -1 || agentsIndex + 1 >= pathParts.length) return false;
|
|
836
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
837
|
+
const agentId = filePath.includes(".openclaw/") ? `openclaw-${agentName}` : agentName;
|
|
838
|
+
let loaded = 0;
|
|
839
|
+
for (const [sessionKey, sessionData] of Object.entries(data)) {
|
|
840
|
+
if (!sessionData || typeof sessionData !== "object") continue;
|
|
841
|
+
const session = sessionData;
|
|
842
|
+
const sessionId = session.sessionId;
|
|
843
|
+
if (!sessionId) continue;
|
|
844
|
+
const existingKey = Array.from(this.traces.keys()).find((k) => {
|
|
845
|
+
const t = this.traces.get(k);
|
|
846
|
+
return (t == null ? void 0 : t.id) === sessionId || (t == null ? void 0 : t.traceId) === sessionId;
|
|
847
|
+
});
|
|
848
|
+
if (existingKey) continue;
|
|
849
|
+
const updatedAt = session.updatedAt || stats.mtime.getTime();
|
|
850
|
+
const label = session.label || sessionKey.split(":").pop() || sessionId;
|
|
851
|
+
const chatType = session.chatType || (sessionKey.includes("cron") ? "cron" : "direct");
|
|
852
|
+
const trigger = sessionKey.includes("cron") ? "cron" : "message";
|
|
853
|
+
const rootId = `idx-${sessionId.slice(0, 12)}`;
|
|
854
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
855
|
+
nodes.set(rootId, {
|
|
856
|
+
id: rootId,
|
|
857
|
+
type: "agent",
|
|
858
|
+
name: label,
|
|
859
|
+
startTime: updatedAt,
|
|
860
|
+
endTime: updatedAt,
|
|
861
|
+
status: "completed",
|
|
862
|
+
parentId: void 0,
|
|
863
|
+
children: [],
|
|
864
|
+
metadata: {
|
|
865
|
+
sessionId,
|
|
866
|
+
sessionKey,
|
|
867
|
+
chatType,
|
|
868
|
+
source: "sessions-index"
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
const trace = {
|
|
872
|
+
id: sessionId,
|
|
873
|
+
nodes,
|
|
874
|
+
edges: [],
|
|
875
|
+
events: [],
|
|
876
|
+
startTime: updatedAt,
|
|
877
|
+
agentId,
|
|
878
|
+
trigger,
|
|
879
|
+
name: label,
|
|
880
|
+
traceId: sessionId,
|
|
881
|
+
spanId: sessionId,
|
|
882
|
+
filename: `${agentName}-${sessionId.slice(0, 8)}.index`,
|
|
883
|
+
lastModified: updatedAt,
|
|
884
|
+
sourceType: "session",
|
|
885
|
+
sourceDir: path.dirname(filePath),
|
|
886
|
+
sessionEvents: [],
|
|
887
|
+
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
888
|
+
metadata: {
|
|
889
|
+
sessionKey,
|
|
890
|
+
chatType,
|
|
891
|
+
source: "sessions-index",
|
|
892
|
+
agentName
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const key = `${this.traceKey(filePath)}-${sessionId.slice(0, 12)}`;
|
|
896
|
+
this.traces.set(key, trace);
|
|
897
|
+
loaded++;
|
|
898
|
+
}
|
|
899
|
+
return loaded > 0;
|
|
900
|
+
} catch {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
548
904
|
/** Parse a JSONL session log into a WatchedTrace (best-effort). */
|
|
549
905
|
loadSessionFile(filePath) {
|
|
550
906
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
@@ -560,6 +916,10 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
560
916
|
}
|
|
561
917
|
}
|
|
562
918
|
if (rawEvents.length === 0) return false;
|
|
919
|
+
const firstEvent = rawEvents[0];
|
|
920
|
+
if ((firstEvent == null ? void 0 : firstEvent.jobId) && (firstEvent == null ? void 0 : firstEvent.action) && !(firstEvent == null ? void 0 : firstEvent.type)) {
|
|
921
|
+
return this.loadCronRunFile(rawEvents, filePath);
|
|
922
|
+
}
|
|
563
923
|
const sessionEvent = rawEvents.find((e) => e.type === "session");
|
|
564
924
|
const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
|
|
565
925
|
const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
|
|
@@ -567,7 +927,25 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
567
927
|
if (!startTime) return false;
|
|
568
928
|
const parentDir = path.basename(path.dirname(filePath));
|
|
569
929
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
570
|
-
const
|
|
930
|
+
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
931
|
+
let agentId;
|
|
932
|
+
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
933
|
+
agentId = grandParentDir;
|
|
934
|
+
} else if (grandParentDir === "agents") {
|
|
935
|
+
agentId = parentDir;
|
|
936
|
+
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
937
|
+
agentId = "openclaw-cron";
|
|
938
|
+
} else {
|
|
939
|
+
agentId = parentDir;
|
|
940
|
+
}
|
|
941
|
+
if (filePath.includes(".openclaw/") && !agentId.startsWith("openclaw-")) {
|
|
942
|
+
agentId = `openclaw-${agentId}`;
|
|
943
|
+
}
|
|
944
|
+
if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
|
|
945
|
+
if (!agentId.startsWith("alfred-")) {
|
|
946
|
+
agentId = `alfred-${agentId}`;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
571
949
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
572
950
|
const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
|
|
573
951
|
const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
|
|
@@ -880,11 +1258,75 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
880
1258
|
return false;
|
|
881
1259
|
}
|
|
882
1260
|
}
|
|
1261
|
+
/** Parse cron run JSONL files (ts, jobId, action, status format). */
|
|
1262
|
+
loadCronRunFile(rawEvents, filePath) {
|
|
1263
|
+
var _a, _b, _c;
|
|
1264
|
+
try {
|
|
1265
|
+
const filename = path.basename(filePath);
|
|
1266
|
+
const jobId = ((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path.basename(filePath, ".jsonl");
|
|
1267
|
+
const fileStat = fs.statSync(filePath);
|
|
1268
|
+
const sessionEvents = [];
|
|
1269
|
+
let lastStatus = "completed";
|
|
1270
|
+
for (const evt of rawEvents) {
|
|
1271
|
+
const ts = evt.ts || Date.now();
|
|
1272
|
+
const action = evt.action || "unknown";
|
|
1273
|
+
const status = evt.status || "ok";
|
|
1274
|
+
if (status !== "ok") lastStatus = "failed";
|
|
1275
|
+
sessionEvents.push({
|
|
1276
|
+
type: action === "finished" ? "assistant" : "system",
|
|
1277
|
+
timestamp: ts,
|
|
1278
|
+
name: `${jobId}: ${action}`,
|
|
1279
|
+
content: evt.summary || evt.error || `${action} (${status})`,
|
|
1280
|
+
id: `cron-${ts}`
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const firstTs = ((_b = rawEvents[0]) == null ? void 0 : _b.ts) || fileStat.mtime.getTime();
|
|
1284
|
+
const lastTs = ((_c = rawEvents[rawEvents.length - 1]) == null ? void 0 : _c.ts) || fileStat.mtime.getTime();
|
|
1285
|
+
const rootId = `cron-${jobId.slice(0, 12)}`;
|
|
1286
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1287
|
+
nodes.set(rootId, {
|
|
1288
|
+
id: rootId,
|
|
1289
|
+
type: "agent",
|
|
1290
|
+
name: jobId,
|
|
1291
|
+
startTime: firstTs,
|
|
1292
|
+
endTime: lastTs,
|
|
1293
|
+
status: lastStatus,
|
|
1294
|
+
parentId: void 0,
|
|
1295
|
+
children: [],
|
|
1296
|
+
metadata: { jobId, runs: rawEvents.length }
|
|
1297
|
+
});
|
|
1298
|
+
const trace = {
|
|
1299
|
+
id: jobId,
|
|
1300
|
+
nodes,
|
|
1301
|
+
edges: [],
|
|
1302
|
+
events: [],
|
|
1303
|
+
startTime: firstTs,
|
|
1304
|
+
agentId: "openclaw-cron",
|
|
1305
|
+
trigger: "cron",
|
|
1306
|
+
name: jobId,
|
|
1307
|
+
traceId: jobId,
|
|
1308
|
+
spanId: jobId,
|
|
1309
|
+
filename,
|
|
1310
|
+
lastModified: fileStat.mtime.getTime(),
|
|
1311
|
+
sourceType: "session",
|
|
1312
|
+
sourceDir: path.dirname(filePath),
|
|
1313
|
+
sessionEvents,
|
|
1314
|
+
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
1315
|
+
metadata: { jobId, source: "cron-run" }
|
|
1316
|
+
};
|
|
1317
|
+
this.traces.set(this.traceKey(filePath), trace);
|
|
1318
|
+
return true;
|
|
1319
|
+
} catch {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
883
1323
|
/** Unique key for a file across directories. */
|
|
884
1324
|
traceKey(filePath) {
|
|
885
1325
|
for (const dir of this.allWatchDirs) {
|
|
886
1326
|
if (filePath.startsWith(dir)) {
|
|
887
|
-
|
|
1327
|
+
const dirParts = dir.split(path.sep).filter(Boolean);
|
|
1328
|
+
const dirSuffix = dirParts.slice(-2).join("/");
|
|
1329
|
+
return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + dirSuffix;
|
|
888
1330
|
}
|
|
889
1331
|
}
|
|
890
1332
|
return filePath;
|
|
@@ -892,16 +1334,35 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
892
1334
|
startWatching() {
|
|
893
1335
|
for (const dir of this.allWatchDirs) {
|
|
894
1336
|
if (!fs.existsSync(dir)) continue;
|
|
895
|
-
const
|
|
896
|
-
|
|
1337
|
+
const patterns = [
|
|
1338
|
+
path.join(dir, "**/*.json"),
|
|
1339
|
+
path.join(dir, "**/*.jsonl"),
|
|
1340
|
+
path.join(dir, "**/*.log"),
|
|
1341
|
+
path.join(dir, "**/*.trace")
|
|
1342
|
+
];
|
|
1343
|
+
const watcher = import_chokidar.default.watch(patterns, {
|
|
1344
|
+
ignored: [
|
|
1345
|
+
/^\./,
|
|
1346
|
+
// Ignore hidden files
|
|
1347
|
+
/node_modules/,
|
|
1348
|
+
// Ignore node_modules
|
|
1349
|
+
/\.git/,
|
|
1350
|
+
// Ignore git directories
|
|
1351
|
+
/\.vscode/,
|
|
1352
|
+
// Ignore vscode
|
|
1353
|
+
/\.idea/
|
|
1354
|
+
// Ignore idea
|
|
1355
|
+
],
|
|
897
1356
|
persistent: true,
|
|
898
1357
|
ignoreInitial: true,
|
|
899
|
-
|
|
900
|
-
|
|
1358
|
+
followSymlinks: false,
|
|
1359
|
+
depth: 10
|
|
1360
|
+
// Allow deep nesting for OpenClaw agents/*/sessions/
|
|
901
1361
|
});
|
|
902
1362
|
watcher.on("add", (filePath) => {
|
|
903
|
-
if (
|
|
904
|
-
|
|
1363
|
+
if (this.isSupportedFile(path.basename(filePath))) {
|
|
1364
|
+
const relativePath = path.relative(dir, filePath);
|
|
1365
|
+
console.log(`New file: ${relativePath} (in ${path.basename(dir)})`);
|
|
905
1366
|
if (this.loadFile(filePath)) {
|
|
906
1367
|
const key = this.traceKey(filePath);
|
|
907
1368
|
const trace = this.traces.get(key);
|
|
@@ -912,7 +1373,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
912
1373
|
}
|
|
913
1374
|
});
|
|
914
1375
|
watcher.on("change", (filePath) => {
|
|
915
|
-
if (
|
|
1376
|
+
if (this.isSupportedFile(path.basename(filePath))) {
|
|
916
1377
|
if (this.loadFile(filePath)) {
|
|
917
1378
|
const key = this.traceKey(filePath);
|
|
918
1379
|
const trace = this.traces.get(key);
|
|
@@ -923,7 +1384,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
923
1384
|
}
|
|
924
1385
|
});
|
|
925
1386
|
watcher.on("unlink", (filePath) => {
|
|
926
|
-
if (
|
|
1387
|
+
if (this.isSupportedFile(path.basename(filePath))) {
|
|
927
1388
|
const key = this.traceKey(filePath);
|
|
928
1389
|
this.traces.delete(key);
|
|
929
1390
|
this.emit("trace-removed", key);
|
|
@@ -934,7 +1395,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
934
1395
|
});
|
|
935
1396
|
this.watchers.push(watcher);
|
|
936
1397
|
}
|
|
937
|
-
console.log(`Watching ${this.allWatchDirs.length} directories for JSON/JSONL files`);
|
|
1398
|
+
console.log(`Watching ${this.allWatchDirs.length} directories recursively for JSON/JSONL/LOG/TRACE files`);
|
|
938
1399
|
}
|
|
939
1400
|
getAllTraces() {
|
|
940
1401
|
return Array.from(this.traces.values()).sort((a, b) => {
|
|
@@ -996,7 +1457,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
|
|
|
996
1457
|
var fs2 = __toESM(require("fs"), 1);
|
|
997
1458
|
var os = __toESM(require("os"), 1);
|
|
998
1459
|
var path2 = __toESM(require("path"), 1);
|
|
999
|
-
var VERSION = "0.
|
|
1460
|
+
var VERSION = "0.4.0";
|
|
1000
1461
|
function getLanAddress() {
|
|
1001
1462
|
const interfaces = os.networkInterfaces();
|
|
1002
1463
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -1141,6 +1602,18 @@ Tabs:
|
|
|
1141
1602
|
var import_meta = {};
|
|
1142
1603
|
var __filename = (0, import_url.fileURLToPath)(import_meta.url);
|
|
1143
1604
|
var __dirname = path3.dirname(__filename);
|
|
1605
|
+
function serializeTrace(trace) {
|
|
1606
|
+
if (!trace) return trace;
|
|
1607
|
+
const obj = { ...trace };
|
|
1608
|
+
if (obj.nodes instanceof Map) {
|
|
1609
|
+
const nodesObj = {};
|
|
1610
|
+
for (const [key, value] of obj.nodes) {
|
|
1611
|
+
nodesObj[key] = value;
|
|
1612
|
+
}
|
|
1613
|
+
obj.nodes = nodesObj;
|
|
1614
|
+
}
|
|
1615
|
+
return obj;
|
|
1616
|
+
}
|
|
1144
1617
|
var DashboardServer = class {
|
|
1145
1618
|
constructor(config) {
|
|
1146
1619
|
this.config = config;
|
|
@@ -1152,6 +1625,10 @@ var DashboardServer = class {
|
|
|
1152
1625
|
this.setupExpress();
|
|
1153
1626
|
this.setupWebSocket();
|
|
1154
1627
|
this.setupTraceWatcher();
|
|
1628
|
+
for (const trace of this.watcher.getAllTraces()) {
|
|
1629
|
+
this.stats.processTrace(trace);
|
|
1630
|
+
}
|
|
1631
|
+
console.log(`Processed ${this.watcher.getTraceCount()} existing traces for stats`);
|
|
1155
1632
|
}
|
|
1156
1633
|
app = (0, import_express.default)();
|
|
1157
1634
|
server = (0, import_http.createServer)(this.app);
|
|
@@ -1176,7 +1653,7 @@ var DashboardServer = class {
|
|
|
1176
1653
|
}
|
|
1177
1654
|
this.app.get("/api/traces", (req, res) => {
|
|
1178
1655
|
try {
|
|
1179
|
-
const traces = this.watcher.getAllTraces();
|
|
1656
|
+
const traces = this.watcher.getAllTraces().map(serializeTrace);
|
|
1180
1657
|
res.json(traces);
|
|
1181
1658
|
} catch (error) {
|
|
1182
1659
|
res.status(500).json({ error: "Failed to load traces" });
|
|
@@ -1188,7 +1665,7 @@ var DashboardServer = class {
|
|
|
1188
1665
|
if (!trace) {
|
|
1189
1666
|
return res.status(404).json({ error: "Trace not found" });
|
|
1190
1667
|
}
|
|
1191
|
-
res.json(trace);
|
|
1668
|
+
res.json(serializeTrace(trace));
|
|
1192
1669
|
} catch (error) {
|
|
1193
1670
|
res.status(500).json({ error: "Failed to load trace" });
|
|
1194
1671
|
}
|
|
@@ -1224,6 +1701,98 @@ var DashboardServer = class {
|
|
|
1224
1701
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1225
1702
|
}
|
|
1226
1703
|
});
|
|
1704
|
+
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const agentId = req.params.agentId;
|
|
1707
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1708
|
+
if (traces.length === 0) {
|
|
1709
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
1710
|
+
}
|
|
1711
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
1712
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
1713
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
1714
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1715
|
+
let totalTraces = 0;
|
|
1716
|
+
for (const trace of traces) {
|
|
1717
|
+
totalTraces++;
|
|
1718
|
+
const activities = [];
|
|
1719
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1720
|
+
for (const evt of trace.sessionEvents) {
|
|
1721
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
1722
|
+
if (!name) continue;
|
|
1723
|
+
activities.push({
|
|
1724
|
+
name,
|
|
1725
|
+
type: evt.type,
|
|
1726
|
+
status: evt.toolError ? "failed" : "completed",
|
|
1727
|
+
duration: evt.duration || 0
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
} else {
|
|
1731
|
+
const nodes2 = trace.nodes || {};
|
|
1732
|
+
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1733
|
+
for (const node of sorted) {
|
|
1734
|
+
activities.push({
|
|
1735
|
+
name: node.name || node.type || node.id,
|
|
1736
|
+
type: node.type || "unknown",
|
|
1737
|
+
status: node.status || "completed",
|
|
1738
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1743
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1744
|
+
const act = seq[i];
|
|
1745
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1746
|
+
if (i < seq.length - 1) {
|
|
1747
|
+
const key = act + " \u2192 " + seq[i + 1];
|
|
1748
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const act of activities) {
|
|
1752
|
+
if (act.duration > 0) {
|
|
1753
|
+
const durs = activityDurations.get(act.name) || [];
|
|
1754
|
+
durs.push(act.duration);
|
|
1755
|
+
activityDurations.set(act.name, durs);
|
|
1756
|
+
}
|
|
1757
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1758
|
+
if (act.status === "failed") st.fail++;
|
|
1759
|
+
else st.ok++;
|
|
1760
|
+
activityStatuses.set(act.name, st);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
1764
|
+
const durs = activityDurations.get(name) || [];
|
|
1765
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1766
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1767
|
+
return {
|
|
1768
|
+
id: name,
|
|
1769
|
+
label: name,
|
|
1770
|
+
count,
|
|
1771
|
+
frequency: count / totalTraces,
|
|
1772
|
+
avgDuration,
|
|
1773
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1774
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
1775
|
+
};
|
|
1776
|
+
});
|
|
1777
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1778
|
+
const [source, target] = key.split(" \u2192 ");
|
|
1779
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
1780
|
+
});
|
|
1781
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1782
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1783
|
+
res.json({
|
|
1784
|
+
agentId,
|
|
1785
|
+
totalTraces,
|
|
1786
|
+
nodes,
|
|
1787
|
+
edges,
|
|
1788
|
+
maxEdgeCount,
|
|
1789
|
+
maxNodeCount
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
console.error("Process graph error:", error);
|
|
1793
|
+
res.status(500).json({ error: "Failed to build process graph" });
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1227
1796
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1228
1797
|
try {
|
|
1229
1798
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
|
@@ -1250,7 +1819,55 @@ var DashboardServer = class {
|
|
|
1250
1819
|
if (!processConfig) {
|
|
1251
1820
|
return res.json(null);
|
|
1252
1821
|
}
|
|
1253
|
-
const
|
|
1822
|
+
const alfredResult = (0, import_agentflow_core3.auditProcesses)(processConfig);
|
|
1823
|
+
const openclawConfig = {
|
|
1824
|
+
processName: "openclaw",
|
|
1825
|
+
pidFile: void 0,
|
|
1826
|
+
workersFile: void 0,
|
|
1827
|
+
systemdUnit: null
|
|
1828
|
+
};
|
|
1829
|
+
const openclawResult = (0, import_agentflow_core3.auditProcesses)(openclawConfig);
|
|
1830
|
+
const clawmetryConfig = {
|
|
1831
|
+
processName: "clawmetry",
|
|
1832
|
+
pidFile: void 0,
|
|
1833
|
+
workersFile: void 0,
|
|
1834
|
+
systemdUnit: null
|
|
1835
|
+
};
|
|
1836
|
+
const clawmetryResult = (0, import_agentflow_core3.auditProcesses)(clawmetryConfig);
|
|
1837
|
+
const allOsProcesses = [
|
|
1838
|
+
...alfredResult.osProcesses,
|
|
1839
|
+
...openclawResult.osProcesses,
|
|
1840
|
+
...clawmetryResult.osProcesses
|
|
1841
|
+
];
|
|
1842
|
+
const uniqueProcesses = allOsProcesses.filter(
|
|
1843
|
+
(proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
|
|
1844
|
+
);
|
|
1845
|
+
const result = {
|
|
1846
|
+
...alfredResult,
|
|
1847
|
+
osProcesses: uniqueProcesses,
|
|
1848
|
+
// Recalculate orphans based on all processes
|
|
1849
|
+
orphans: uniqueProcesses.filter((p) => {
|
|
1850
|
+
var _a;
|
|
1851
|
+
const alfredKnownPids = /* @__PURE__ */ new Set();
|
|
1852
|
+
if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale) alfredKnownPids.add(alfredResult.pidFile.pid);
|
|
1853
|
+
if (alfredResult.workers) {
|
|
1854
|
+
if (alfredResult.workers.orchestratorPid) alfredKnownPids.add(alfredResult.workers.orchestratorPid);
|
|
1855
|
+
for (const w of alfredResult.workers.workers) {
|
|
1856
|
+
if (w.pid) alfredKnownPids.add(w.pid);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const isOpenClawProcess = p.cmdline.includes("openclaw") || p.cmdline.includes("clawmetry");
|
|
1860
|
+
return !alfredKnownPids.has(p.pid) && !isOpenClawProcess && p.pid !== process.pid && p.pid !== process.ppid;
|
|
1861
|
+
})
|
|
1862
|
+
};
|
|
1863
|
+
const openclawProblems = [];
|
|
1864
|
+
if (openclawResult.osProcesses.length === 0) {
|
|
1865
|
+
openclawProblems.push("No OpenClaw gateway processes detected");
|
|
1866
|
+
}
|
|
1867
|
+
if (clawmetryResult.osProcesses.length === 0) {
|
|
1868
|
+
openclawProblems.push("No clawmetry processes detected");
|
|
1869
|
+
}
|
|
1870
|
+
result.problems = [...alfredResult.problems || [], ...openclawProblems];
|
|
1254
1871
|
this.processHealthCache = { result, ts: now };
|
|
1255
1872
|
res.json(result);
|
|
1256
1873
|
} catch (error) {
|
|
@@ -1273,7 +1890,7 @@ var DashboardServer = class {
|
|
|
1273
1890
|
JSON.stringify({
|
|
1274
1891
|
type: "init",
|
|
1275
1892
|
data: {
|
|
1276
|
-
traces: this.watcher.getAllTraces(),
|
|
1893
|
+
traces: this.watcher.getAllTraces().map(serializeTrace),
|
|
1277
1894
|
stats: this.stats.getGlobalStats()
|
|
1278
1895
|
}
|
|
1279
1896
|
})
|
|
@@ -1291,14 +1908,14 @@ var DashboardServer = class {
|
|
|
1291
1908
|
this.stats.processTrace(trace);
|
|
1292
1909
|
this.broadcast({
|
|
1293
1910
|
type: "trace-added",
|
|
1294
|
-
data: trace
|
|
1911
|
+
data: serializeTrace(trace)
|
|
1295
1912
|
});
|
|
1296
1913
|
});
|
|
1297
1914
|
this.watcher.on("trace-updated", (trace) => {
|
|
1298
1915
|
this.stats.processTrace(trace);
|
|
1299
1916
|
this.broadcast({
|
|
1300
1917
|
type: "trace-updated",
|
|
1301
|
-
data: trace
|
|
1918
|
+
data: serializeTrace(trace)
|
|
1302
1919
|
});
|
|
1303
1920
|
});
|
|
1304
1921
|
this.watcher.on("stats-updated", () => {
|