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/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
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"));
256
- totalFiles += files.length;
257
- for (const file of files) {
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 ${this.allWatchDirs.length} directories, loaded ${this.traces.size} items from ${totalFiles} files`);
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 coloredMatch = line.match(/^\[2m(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\[0m/);
411
- if (coloredMatch) return new Date(coloredMatch[1]).getTime();
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 coloredMatch = line.match(/\[\[(\d+)m\[\[1m(\w+)\s*\[0m\]/);
418
- if (coloredMatch) return coloredMatch[2].toLowerCase();
419
- const levelMatch = line.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
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 coloredMatch = line.match(/\[1m([^\[]+?)\s*\[0m/);
424
- if (coloredMatch) return coloredMatch[1].trim();
425
- const afterLevel = line.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*:?\s*/i, "");
426
- return afterLevel.split(" ")[0] || "";
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 coloredRegex = /\[36m(\w+)\[0m=\[35m([^\[]+?)\[0m/g;
504
+ const clean = this.stripAnsi(line);
505
+ const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
431
506
  let match;
432
- while ((match = coloredRegex.exec(line)) !== null) {
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 "unknown";
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.run_id || activity.session_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
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.component !== "unknown") {
477
- const pathAgent = this.extractAgentFromPath(filePath);
478
- if (pathAgent !== activity.component) {
479
- return `${pathAgent}-${activity.component}`;
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 this.extractAgentFromPath(filePath);
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}-${activity.timestamp}`;
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
- metadata: activity
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 agentId = grandParentDir === "agents" ? parentDir : parentDir;
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
- return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + path.basename(dir);
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 watcher = import_chokidar.default.watch(dir, {
896
- ignored: /^\./,
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
- depth: 0
900
- // don't recurse into subdirectories
1358
+ followSymlinks: false,
1359
+ depth: 10
1360
+ // Allow deep nesting for OpenClaw agents/*/sessions/
901
1361
  });
902
1362
  watcher.on("add", (filePath) => {
903
- if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
904
- console.log(`New file: ${path.basename(filePath)}`);
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 (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
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 (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
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.3.1";
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 result = (0, import_agentflow_core3.auditProcesses)(processConfig);
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", () => {