agmon 0.0.0-stage → 0.1.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 +81 -2
- package/bin/cli.js +366 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +6 -0
- package/package.json +50 -4
- package/postcss.config.mjs +5 -0
- package/public/favicon.svg +48 -0
- package/public/icons/icon.svg +48 -0
- package/public/manifest.json +23 -0
- package/public/sw.js +42 -0
- package/src/app/api/traces/stream/route.js +89 -0
- package/src/app/api/version/route.js +23 -0
- package/src/app/globals.css +15 -0
- package/src/app/layout.js +42 -0
- package/src/app/page.js +174 -0
- package/src/components/PwaRegister.js +31 -0
- package/src/components/UpdateNotification.js +79 -0
- package/src/components/factory/AgentPanel.js +92 -0
- package/src/components/factory/office-canvas.js +95 -0
- package/src/components/factory/office-scene.js +519 -0
- package/src/components/factory/scene/characters.js +335 -0
- package/src/components/factory/scene/office-layout.js +188 -0
- package/src/components/factory/scene/token-streams.js +85 -0
- package/src/components/factory/scene/tool-icons.js +233 -0
- package/src/lib/mockTraces.js +171 -0
- package/src/lib/traceContract.js +46 -0
- package/src/lib/useFactoryTraces.js +142 -0
- package/src/lib/watchers/antigravityWatcher.js +180 -0
- package/src/lib/watchers/claudeWatcher.js +190 -0
- package/src/lib/watchers/watcherManager.js +41 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { normalizeTrace } from "../traceContract.js";
|
|
5
|
+
|
|
6
|
+
const TOOL_MAP = {
|
|
7
|
+
view_file: "read",
|
|
8
|
+
read_file: "read",
|
|
9
|
+
replace_file_content: "edit",
|
|
10
|
+
write_to_file: "edit",
|
|
11
|
+
call_mcp_tool: "mcp",
|
|
12
|
+
list_resources: "mcp",
|
|
13
|
+
invoke_subagent: "agent",
|
|
14
|
+
grep_search: "search",
|
|
15
|
+
list_dir: "read",
|
|
16
|
+
find_by_name: "read",
|
|
17
|
+
define_subagent: "agent",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function normalizeToolCall(tc) {
|
|
21
|
+
if (!tc || !tc.name) return { type: "mcp", detail: null };
|
|
22
|
+
const name = tc.name;
|
|
23
|
+
const args = tc.args || {};
|
|
24
|
+
|
|
25
|
+
// 1. MCP Tools inspection
|
|
26
|
+
if (name === "call_mcp_tool") {
|
|
27
|
+
const srv = String(args.ServerName || "").toLowerCase();
|
|
28
|
+
const tool = String(args.ToolName || "").toLowerCase();
|
|
29
|
+
if (srv.includes("gitnexus") || tool.includes("cypher") || tool.includes("pdg_query") || tool.includes("impact")) {
|
|
30
|
+
return { type: "gitnexus", detail: `GitNexus: ${tool}` };
|
|
31
|
+
}
|
|
32
|
+
if (srv.includes("playwright") || srv.includes("browser") || tool.startsWith("browser_")) {
|
|
33
|
+
return { type: "browser", detail: `Browser: ${tool}` };
|
|
34
|
+
}
|
|
35
|
+
return { type: "mcp", detail: `MCP: ${srv}/${tool}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Terminal commands inspection
|
|
39
|
+
if (name === "run_command") {
|
|
40
|
+
const cmd = String(args.CommandLine || "").trim();
|
|
41
|
+
const cmdLower = cmd.toLowerCase();
|
|
42
|
+
if (cmdLower.startsWith("docker") || cmdLower.includes("docker exec") || cmdLower.includes("docker-compose")) {
|
|
43
|
+
return { type: "docker", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
44
|
+
}
|
|
45
|
+
if (cmdLower.startsWith("git ") || cmdLower.startsWith("gh ") || cmdLower.includes("git commit") || cmdLower.includes("git push")) {
|
|
46
|
+
return { type: "git", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
47
|
+
}
|
|
48
|
+
if (cmdLower.includes("gitnexus")) {
|
|
49
|
+
return { type: "gitnexus", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
50
|
+
}
|
|
51
|
+
return { type: "bash", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Native tools mapping
|
|
55
|
+
const mapped = TOOL_MAP[name];
|
|
56
|
+
const target = args.TargetFile || args.AbsolutePath || args.query || "";
|
|
57
|
+
const detail = target ? `${name}: ${path.basename(target)}` : name;
|
|
58
|
+
|
|
59
|
+
if (mapped) return { type: mapped, detail };
|
|
60
|
+
if (name.includes("read") || name.includes("find") || name.includes("dir") || name.includes("list")) return { type: "read", detail };
|
|
61
|
+
if (name.includes("edit") || name.includes("write") || name.includes("patch")) return { type: "edit", detail };
|
|
62
|
+
if (name.includes("search") || name.includes("grep")) return { type: "search", detail };
|
|
63
|
+
if (name.includes("agent") || name.includes("message")) return { type: "agent", detail };
|
|
64
|
+
|
|
65
|
+
return { type: "mcp", detail };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getAntigravityTraces(maxAgeMs = 2 * 60 * 60 * 1000) {
|
|
69
|
+
const homeDir = os.homedir();
|
|
70
|
+
const brainDir = path.join(homeDir, ".gemini", "antigravity-cli", "brain");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const entries = await fs.readdir(brainDir, { withFileTypes: true });
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const traces = [];
|
|
76
|
+
|
|
77
|
+
for (const ent of entries) {
|
|
78
|
+
if (!ent.isDirectory()) continue;
|
|
79
|
+
const convId = ent.name;
|
|
80
|
+
const transcriptPath = path.join(brainDir, convId, ".system_generated", "logs", "transcript.jsonl");
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const stat = await fs.stat(transcriptPath);
|
|
84
|
+
const mtime = stat.mtimeMs;
|
|
85
|
+
if (now - mtime > maxAgeMs) continue;
|
|
86
|
+
|
|
87
|
+
const content = await fs.readFile(transcriptPath, "utf-8");
|
|
88
|
+
const lines = content.trim().split("\n");
|
|
89
|
+
if (lines.length === 0) continue;
|
|
90
|
+
|
|
91
|
+
let totalIn = 0;
|
|
92
|
+
let totalOut = 0;
|
|
93
|
+
let totalCached = 0;
|
|
94
|
+
let lastToolEntry = null;
|
|
95
|
+
let lastStep = null;
|
|
96
|
+
const toolSet = new Set();
|
|
97
|
+
let firstTime = null;
|
|
98
|
+
let lastTime = null;
|
|
99
|
+
let activeCommandDetail = null;
|
|
100
|
+
let detectedCwd = null;
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (!line) continue;
|
|
104
|
+
try {
|
|
105
|
+
const entry = JSON.parse(line);
|
|
106
|
+
if (!firstTime && entry.created_at) firstTime = new Date(entry.created_at).getTime();
|
|
107
|
+
if (entry.created_at) lastTime = new Date(entry.created_at).getTime();
|
|
108
|
+
|
|
109
|
+
if (entry.input_tokens) totalIn += entry.input_tokens;
|
|
110
|
+
if (entry.output_tokens) totalOut += entry.output_tokens;
|
|
111
|
+
if (entry.cache_read_tokens) totalCached += entry.cache_read_tokens;
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
|
|
114
|
+
lastToolEntry = entry;
|
|
115
|
+
for (const tc of entry.tool_calls) {
|
|
116
|
+
if (tc.args?.Cwd && !detectedCwd) detectedCwd = tc.args.Cwd;
|
|
117
|
+
if (!detectedCwd && (tc.args?.TargetFile || tc.args?.AbsolutePath)) {
|
|
118
|
+
const p = tc.args.TargetFile || tc.args.AbsolutePath;
|
|
119
|
+
if (p.includes("/Projects/")) detectedCwd = p.split("/").slice(0, 5).join("/");
|
|
120
|
+
}
|
|
121
|
+
const { type, detail } = normalizeToolCall(tc);
|
|
122
|
+
if (type) toolSet.add(type);
|
|
123
|
+
if (detail) activeCommandDetail = detail;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lastStep = entry;
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore malformed line
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isRecent = (now - mtime) < 45 * 1000;
|
|
133
|
+
let state = "done";
|
|
134
|
+
if (isRecent) {
|
|
135
|
+
if (lastStep && lastStep.type === "USER_INPUT") state = "pending";
|
|
136
|
+
else if (lastToolEntry && lastToolEntry.status === "RUNNING") state = "streaming";
|
|
137
|
+
else state = "streaming";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let activeTool = null;
|
|
141
|
+
if (lastToolEntry && Array.isArray(lastToolEntry.tool_calls) && lastToolEntry.tool_calls.length > 0) {
|
|
142
|
+
const lastTc = lastToolEntry.tool_calls[lastToolEntry.tool_calls.length - 1];
|
|
143
|
+
activeTool = normalizeToolCall(lastTc).type;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const elapsedMs = (lastTime && firstTime) ? Math.max(0, lastTime - firstTime) : (now - mtime);
|
|
147
|
+
const rawFolder = detectedCwd ? path.basename(detectedCwd) : `agy-${convId.slice(0, 6)}`;
|
|
148
|
+
const folderName = rawFolder.replace(/["'\\]/g, "").trim();
|
|
149
|
+
|
|
150
|
+
traces.push(normalizeTrace({
|
|
151
|
+
traceId: `agy-${convId.slice(0, 8)}`,
|
|
152
|
+
cli: "antigravity",
|
|
153
|
+
connectionId: `Antigravity (${convId.slice(0, 6)})`,
|
|
154
|
+
account: folderName,
|
|
155
|
+
model: "gemini-2.5-pro",
|
|
156
|
+
provider: "gemini",
|
|
157
|
+
state,
|
|
158
|
+
startedAt: firstTime || (now - elapsedMs),
|
|
159
|
+
elapsedMs,
|
|
160
|
+
tokens: {
|
|
161
|
+
input: totalIn || 2500,
|
|
162
|
+
output: totalOut || 850,
|
|
163
|
+
cached: totalCached,
|
|
164
|
+
},
|
|
165
|
+
cost: 0,
|
|
166
|
+
status: "200",
|
|
167
|
+
tools: Array.from(toolSet),
|
|
168
|
+
activeTool,
|
|
169
|
+
currentCommand: activeCommandDetail,
|
|
170
|
+
}));
|
|
171
|
+
} catch {
|
|
172
|
+
// file missing or unreadable
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return traces;
|
|
177
|
+
} catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { normalizeTrace } from "../traceContract.js";
|
|
5
|
+
|
|
6
|
+
function normalizeClaudeTool(name, input = {}) {
|
|
7
|
+
const n = String(name || "").toLowerCase();
|
|
8
|
+
const cmd = String(input.command || "").trim();
|
|
9
|
+
const cmdLower = cmd.toLowerCase();
|
|
10
|
+
|
|
11
|
+
if (n === "bash") {
|
|
12
|
+
if (cmdLower.startsWith("docker") || cmdLower.includes("docker exec")) {
|
|
13
|
+
return { type: "docker", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
14
|
+
}
|
|
15
|
+
if (cmdLower.startsWith("git ") || cmdLower.startsWith("gh ")) {
|
|
16
|
+
return { type: "git", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
17
|
+
}
|
|
18
|
+
if (cmdLower.includes("gitnexus")) {
|
|
19
|
+
return { type: "gitnexus", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
20
|
+
}
|
|
21
|
+
return { type: "bash", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (n.includes("read") || n.includes("view") || n.includes("glob")) {
|
|
25
|
+
const file = input.file_path || input.path || "";
|
|
26
|
+
return { type: "read", detail: file ? `Read: ${path.basename(file)}` : "Read file" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("replace")) {
|
|
30
|
+
const file = input.file_path || input.path || "";
|
|
31
|
+
return { type: "edit", detail: file ? `Edit: ${path.basename(file)}` : "Edit file" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (n.includes("grep") || n.includes("search")) {
|
|
35
|
+
return { type: "search", detail: input.pattern ? `Search: ${input.pattern}` : "Search" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (n.includes("agent") || n.includes("task")) {
|
|
39
|
+
return { type: "agent", detail: "Subagent Task" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (n.includes("browser") || n.includes("playwright")) {
|
|
43
|
+
return { type: "browser", detail: "Browser automation" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (n.includes("gitnexus")) {
|
|
47
|
+
return { type: "gitnexus", detail: "GitNexus query" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { type: "mcp", detail: name };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detectProvider(modelName) {
|
|
54
|
+
const m = String(modelName || "").toLowerCase();
|
|
55
|
+
if (m.includes("claude")) return "anthropic";
|
|
56
|
+
if (m.includes("gemini")) return "gemini";
|
|
57
|
+
if (m.includes("gpt") || m.includes("o1") || m.includes("o3")) return "openai";
|
|
58
|
+
if (m.includes("minimax")) return "minimax";
|
|
59
|
+
if (m.includes("deepseek")) return "deepseek";
|
|
60
|
+
return "anthropic";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getClaudeTraces(maxAgeMs = 3 * 60 * 60 * 1000) {
|
|
64
|
+
const homeDir = os.homedir();
|
|
65
|
+
const sessionsDir = path.join(homeDir, ".claude", "sessions");
|
|
66
|
+
const projectsDir = path.join(homeDir, ".claude", "projects");
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const sessionFiles = await fs.readdir(sessionsDir).catch(() => []);
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const traces = [];
|
|
72
|
+
|
|
73
|
+
for (const f of sessionFiles) {
|
|
74
|
+
if (!f.endsWith(".json")) continue;
|
|
75
|
+
try {
|
|
76
|
+
const rawMeta = await fs.readFile(path.join(sessionsDir, f), "utf-8");
|
|
77
|
+
const meta = JSON.parse(rawMeta);
|
|
78
|
+
const { pid, sessionId, cwd, startedAt, updatedAt, status, name } = meta;
|
|
79
|
+
|
|
80
|
+
// Skip very old sessions
|
|
81
|
+
const lastActive = updatedAt || startedAt || 0;
|
|
82
|
+
if (now - lastActive > maxAgeMs) continue;
|
|
83
|
+
|
|
84
|
+
// Find associated jsonl transcript in projects folder
|
|
85
|
+
let transcriptPath = null;
|
|
86
|
+
try {
|
|
87
|
+
const projectFolders = await fs.readdir(projectsDir);
|
|
88
|
+
for (const pf of projectFolders) {
|
|
89
|
+
const candidate = path.join(projectsDir, pf, `${sessionId}.jsonl`);
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(candidate);
|
|
92
|
+
transcriptPath = candidate;
|
|
93
|
+
break;
|
|
94
|
+
} catch {
|
|
95
|
+
// try next
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let totalIn = 0;
|
|
103
|
+
let totalOut = 0;
|
|
104
|
+
let totalCached = 0;
|
|
105
|
+
let model = "claude-3-7-sonnet";
|
|
106
|
+
let activeTool = null;
|
|
107
|
+
let activeCommand = null;
|
|
108
|
+
const toolSet = new Set();
|
|
109
|
+
let lastTimestamp = lastActive;
|
|
110
|
+
|
|
111
|
+
if (transcriptPath) {
|
|
112
|
+
try {
|
|
113
|
+
const content = await fs.readFile(transcriptPath, "utf-8");
|
|
114
|
+
const lines = content.trim().split("\n");
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (!line) continue;
|
|
117
|
+
try {
|
|
118
|
+
const entry = JSON.parse(line);
|
|
119
|
+
if (entry.timestamp) lastTimestamp = new Date(entry.timestamp).getTime();
|
|
120
|
+
|
|
121
|
+
const msg = entry.message;
|
|
122
|
+
if (msg) {
|
|
123
|
+
if (msg.model) model = msg.model;
|
|
124
|
+
const u = msg.usage;
|
|
125
|
+
if (u) {
|
|
126
|
+
if (u.input_tokens) totalIn += u.input_tokens;
|
|
127
|
+
if (u.output_tokens) totalOut += u.output_tokens;
|
|
128
|
+
if (u.cache_read_input_tokens) totalCached += u.cache_read_input_tokens;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (Array.isArray(msg.content)) {
|
|
132
|
+
for (const block of msg.content) {
|
|
133
|
+
if (block.type === "tool_use") {
|
|
134
|
+
const { type, detail } = normalizeClaudeTool(block.name, block.input);
|
|
135
|
+
toolSet.add(type);
|
|
136
|
+
activeTool = type;
|
|
137
|
+
if (detail) activeCommand = detail;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore malformed line
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// unreadable transcript
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const isRecent = (now - lastActive) < 60 * 1000;
|
|
152
|
+
let state = "done";
|
|
153
|
+
if (status === "running" || status === "working") state = "streaming";
|
|
154
|
+
else if (isRecent) state = "streaming";
|
|
155
|
+
else if (status === "idle") state = "pending";
|
|
156
|
+
|
|
157
|
+
const workspaceName = cwd ? path.basename(cwd) : (name || "Claude Workspace");
|
|
158
|
+
const elapsedMs = Math.max(0, (lastTimestamp || now) - (startedAt || now));
|
|
159
|
+
|
|
160
|
+
traces.push(normalizeTrace({
|
|
161
|
+
traceId: `claude-${pid || sessionId.slice(0, 6)}`,
|
|
162
|
+
cli: "claude",
|
|
163
|
+
connectionId: `Claude (${name || pid})`,
|
|
164
|
+
account: workspaceName,
|
|
165
|
+
model,
|
|
166
|
+
provider: detectProvider(model),
|
|
167
|
+
state,
|
|
168
|
+
startedAt: startedAt || now,
|
|
169
|
+
elapsedMs,
|
|
170
|
+
tokens: {
|
|
171
|
+
input: totalIn || 3200,
|
|
172
|
+
output: totalOut || 1100,
|
|
173
|
+
cached: totalCached,
|
|
174
|
+
},
|
|
175
|
+
cost: 0,
|
|
176
|
+
status: "200",
|
|
177
|
+
tools: Array.from(toolSet),
|
|
178
|
+
activeTool,
|
|
179
|
+
currentCommand: activeCommand,
|
|
180
|
+
}));
|
|
181
|
+
} catch {
|
|
182
|
+
// ignore single session error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return traces;
|
|
187
|
+
} catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getAntigravityTraces } from "./antigravityWatcher.js";
|
|
2
|
+
import { getClaudeTraces } from "./claudeWatcher.js";
|
|
3
|
+
import { extractProviders } from "../traceContract.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WatcherManager aggregates traces across all installed CLI watchers
|
|
7
|
+
* (Antigravity, Claude Code, etc.) with automatic fallback and caching.
|
|
8
|
+
*/
|
|
9
|
+
let cachedTraces = [];
|
|
10
|
+
let lastFetchTime = 0;
|
|
11
|
+
const CACHE_TTL_MS = 1500; // 1.5s cache to avoid reading disks too frequently
|
|
12
|
+
|
|
13
|
+
export async function getAllLiveTraces(force = false) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
if (!force && now - lastFetchTime < CACHE_TTL_MS && cachedTraces.length > 0) {
|
|
16
|
+
return cachedTraces;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const [agyList, claudeList] = await Promise.all([
|
|
20
|
+
getAntigravityTraces().catch(() => []),
|
|
21
|
+
getClaudeTraces().catch(() => []),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const combined = [...agyList, ...claudeList];
|
|
25
|
+
|
|
26
|
+
// Sort: active/streaming/pending first, then by most recent startedAt
|
|
27
|
+
combined.sort((a, b) => {
|
|
28
|
+
const activeA = (a.state === "streaming" || a.state === "pending") ? 1 : 0;
|
|
29
|
+
const activeB = (b.state === "streaming" || b.state === "pending") ? 1 : 0;
|
|
30
|
+
if (activeA !== activeB) return activeB - activeA;
|
|
31
|
+
return (b.startedAt || 0) - (a.startedAt || 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
cachedTraces = combined;
|
|
35
|
+
lastFetchTime = now;
|
|
36
|
+
return combined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getProvidersFromTraces(traces) {
|
|
40
|
+
return extractProviders(traces);
|
|
41
|
+
}
|