@steadwing/openalerts 0.2.6 → 0.2.8
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/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/collections/collection-manager.d.ts +50 -0
- package/dist/collections/collection-manager.js +583 -0
- package/dist/collections/event-parser.d.ts +27 -0
- package/dist/collections/event-parser.js +321 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.js +6 -0
- package/dist/collections/persistence.d.ts +25 -0
- package/dist/collections/persistence.js +213 -0
- package/dist/collections/types.d.ts +177 -0
- package/dist/collections/types.js +15 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +23 -0
- package/dist/core/llm-enrichment.d.ts +21 -0
- package/dist/core/llm-enrichment.js +180 -0
- package/dist/core/platform.d.ts +17 -0
- package/dist/core/platform.js +93 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +11 -5
- package/dist/db/queries.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +600 -0
- package/dist/mcp/client.d.ts +52 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +172 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +164 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/resources.d.ts +13 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +99 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/tools.d.ts +80 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +304 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/plugin/adapter.d.ts +150 -0
- package/dist/plugin/adapter.js +530 -0
- package/dist/plugin/commands.d.ts +18 -0
- package/dist/plugin/commands.js +103 -0
- package/dist/plugin/dashboard-html.d.ts +7 -0
- package/dist/plugin/dashboard-html.js +968 -0
- package/dist/plugin/dashboard-routes.d.ts +12 -0
- package/dist/plugin/dashboard-routes.js +444 -0
- package/dist/plugin/gateway-client.d.ts +39 -0
- package/dist/plugin/gateway-client.js +200 -0
- package/dist/plugin/log-bridge.d.ts +22 -0
- package/dist/plugin/log-bridge.js +363 -0
- package/dist/watchers/gateway-adapter.d.ts.map +1 -1
- package/dist/watchers/gateway-adapter.js +2 -1
- package/dist/watchers/gateway-adapter.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { BoundedMap } from "../core/bounded-map.js";
|
|
2
|
+
import { parseSessionKey, } from "./types.js";
|
|
3
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
4
|
+
const MAX_SESSIONS = 200;
|
|
5
|
+
const MAX_ACTIONS = 2000;
|
|
6
|
+
const MAX_EXECS = 500;
|
|
7
|
+
const MAX_ACTION_HISTORY = 10;
|
|
8
|
+
const SPAWN_INFERENCE_WINDOW_MS = 10000;
|
|
9
|
+
const EXEC_PLACEHOLDER_COMMAND = "Exec";
|
|
10
|
+
const MAX_EXEC_OUTPUT_CHUNKS = 200;
|
|
11
|
+
const MAX_EXEC_OUTPUT_CHARS = 50000;
|
|
12
|
+
const MAX_EXEC_CHUNK_CHARS = 4000;
|
|
13
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
14
|
+
function isSubagentSession(key) {
|
|
15
|
+
return key.includes("subagent");
|
|
16
|
+
}
|
|
17
|
+
function isParentSession(key) {
|
|
18
|
+
return !isSubagentSession(key) && !key.includes("lifecycle");
|
|
19
|
+
}
|
|
20
|
+
function mapExecStatus(exitCode, status) {
|
|
21
|
+
if (typeof exitCode === "number" && exitCode !== 0)
|
|
22
|
+
return "failed";
|
|
23
|
+
if (typeof status === "string") {
|
|
24
|
+
const normalized = status.toLowerCase();
|
|
25
|
+
if (normalized.includes("fail") || normalized.includes("error")) {
|
|
26
|
+
return "failed";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return "completed";
|
|
30
|
+
}
|
|
31
|
+
function capExecOutputs(outputs) {
|
|
32
|
+
let truncated = false;
|
|
33
|
+
const normalized = outputs.map((chunk) => {
|
|
34
|
+
if (chunk.text.length <= MAX_EXEC_CHUNK_CHARS) {
|
|
35
|
+
return chunk;
|
|
36
|
+
}
|
|
37
|
+
truncated = true;
|
|
38
|
+
return {
|
|
39
|
+
...chunk,
|
|
40
|
+
text: chunk.text.slice(0, MAX_EXEC_CHUNK_CHARS) + "\n...[truncated]",
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
let capped = normalized;
|
|
44
|
+
if (capped.length > MAX_EXEC_OUTPUT_CHUNKS) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
capped = capped.slice(-MAX_EXEC_OUTPUT_CHUNKS);
|
|
47
|
+
}
|
|
48
|
+
const totalChars = capped.reduce((sum, chunk) => sum + chunk.text.length, 0);
|
|
49
|
+
if (totalChars > MAX_EXEC_OUTPUT_CHARS) {
|
|
50
|
+
truncated = true;
|
|
51
|
+
let dropped = 0;
|
|
52
|
+
let startIdx = 0;
|
|
53
|
+
for (let i = 0; i < capped.length; i++) {
|
|
54
|
+
if (totalChars - dropped <= MAX_EXEC_OUTPUT_CHARS)
|
|
55
|
+
break;
|
|
56
|
+
dropped += capped[i].text.length;
|
|
57
|
+
startIdx = i + 1;
|
|
58
|
+
}
|
|
59
|
+
capped = capped.slice(startIdx);
|
|
60
|
+
}
|
|
61
|
+
return { outputs: capped, truncated };
|
|
62
|
+
}
|
|
63
|
+
// ─── Collection Manager ──────────────────────────────────────────────────────
|
|
64
|
+
export class CollectionManager {
|
|
65
|
+
sessions;
|
|
66
|
+
actions;
|
|
67
|
+
execs;
|
|
68
|
+
runSessionMap = new Map();
|
|
69
|
+
parentActionHistory = new Map();
|
|
70
|
+
onSessionChange;
|
|
71
|
+
onActionChange;
|
|
72
|
+
onExecChange;
|
|
73
|
+
constructor() {
|
|
74
|
+
this.sessions = new BoundedMap({ maxSize: MAX_SESSIONS });
|
|
75
|
+
this.actions = new BoundedMap({ maxSize: MAX_ACTIONS });
|
|
76
|
+
this.execs = new BoundedMap({ maxSize: MAX_EXECS });
|
|
77
|
+
}
|
|
78
|
+
setCallbacks(callbacks) {
|
|
79
|
+
this.onSessionChange = callbacks.onSessionChange;
|
|
80
|
+
this.onActionChange = callbacks.onActionChange;
|
|
81
|
+
this.onExecChange = callbacks.onExecChange;
|
|
82
|
+
}
|
|
83
|
+
trackParentAction(sessionKey, timestamp) {
|
|
84
|
+
if (!isParentSession(sessionKey))
|
|
85
|
+
return;
|
|
86
|
+
const ts = timestamp ?? Date.now();
|
|
87
|
+
let history = this.parentActionHistory.get(sessionKey);
|
|
88
|
+
if (!history) {
|
|
89
|
+
history = [];
|
|
90
|
+
this.parentActionHistory.set(sessionKey, history);
|
|
91
|
+
}
|
|
92
|
+
history.push(ts);
|
|
93
|
+
if (history.length > MAX_ACTION_HISTORY) {
|
|
94
|
+
history.shift();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
inferSpawnedBy(subagentKey, timestamp) {
|
|
98
|
+
if (!isSubagentSession(subagentKey))
|
|
99
|
+
return undefined;
|
|
100
|
+
const subagentTime = timestamp ?? Date.now();
|
|
101
|
+
const cutoff = subagentTime - SPAWN_INFERENCE_WINDOW_MS;
|
|
102
|
+
let bestParent;
|
|
103
|
+
let bestTime = 0;
|
|
104
|
+
for (const [parentKey, history] of this.parentActionHistory) {
|
|
105
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
106
|
+
const actionTime = history[i];
|
|
107
|
+
if (actionTime <= subagentTime && actionTime >= cutoff) {
|
|
108
|
+
if (actionTime > bestTime) {
|
|
109
|
+
bestTime = actionTime;
|
|
110
|
+
bestParent = parentKey;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return bestParent;
|
|
117
|
+
}
|
|
118
|
+
resolveSessionKey(event) {
|
|
119
|
+
return (event.sessionKey ||
|
|
120
|
+
this.runSessionMap.get(event.runId) ||
|
|
121
|
+
event.sessionId);
|
|
122
|
+
}
|
|
123
|
+
backfillExecSessionKey(runId, sessionKey) {
|
|
124
|
+
for (const [id, exec] of this.execs.entries()) {
|
|
125
|
+
if (exec.runId !== runId)
|
|
126
|
+
continue;
|
|
127
|
+
if (exec.sessionKey && exec.sessionKey !== exec.sessionId)
|
|
128
|
+
continue;
|
|
129
|
+
this.execs.set(id, {
|
|
130
|
+
...exec,
|
|
131
|
+
sessionKey,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
createPlaceholderExec(event, sessionKey) {
|
|
136
|
+
const startedAt = event.startedAt ?? event.timestamp;
|
|
137
|
+
return {
|
|
138
|
+
id: event.execId,
|
|
139
|
+
runId: event.runId,
|
|
140
|
+
pid: event.pid,
|
|
141
|
+
command: event.command || EXEC_PLACEHOLDER_COMMAND,
|
|
142
|
+
sessionId: event.sessionId,
|
|
143
|
+
sessionKey,
|
|
144
|
+
status: event.eventType === "completed"
|
|
145
|
+
? mapExecStatus(event.exitCode, event.status)
|
|
146
|
+
: "running",
|
|
147
|
+
startedAt,
|
|
148
|
+
timestamp: startedAt,
|
|
149
|
+
outputs: [],
|
|
150
|
+
lastActivityAt: event.timestamp,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
154
|
+
upsertSession(session) {
|
|
155
|
+
if (!session.key)
|
|
156
|
+
return;
|
|
157
|
+
const existing = this.sessions.get(session.key);
|
|
158
|
+
if (existing) {
|
|
159
|
+
const preservedSpawnedBy = existing.spawnedBy;
|
|
160
|
+
const accumulatedCost = (existing.totalCostUsd ?? 0) + (session.totalCostUsd ?? 0);
|
|
161
|
+
const accumulatedInput = (existing.totalInputTokens ?? 0) + (session.totalInputTokens ?? 0);
|
|
162
|
+
const accumulatedOutput = (existing.totalOutputTokens ?? 0) + (session.totalOutputTokens ?? 0);
|
|
163
|
+
const updated = {
|
|
164
|
+
...existing,
|
|
165
|
+
...session,
|
|
166
|
+
spawnedBy: preservedSpawnedBy,
|
|
167
|
+
totalCostUsd: session.totalCostUsd !== undefined ? accumulatedCost : existing.totalCostUsd,
|
|
168
|
+
totalInputTokens: session.totalInputTokens !== undefined ? accumulatedInput : existing.totalInputTokens,
|
|
169
|
+
totalOutputTokens: session.totalOutputTokens !== undefined ? accumulatedOutput : existing.totalOutputTokens,
|
|
170
|
+
};
|
|
171
|
+
this.sessions.set(session.key, updated);
|
|
172
|
+
this.onSessionChange?.(updated);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
let spawnedBy = session.spawnedBy;
|
|
176
|
+
if (!spawnedBy && session.key && isSubagentSession(session.key)) {
|
|
177
|
+
spawnedBy = this.inferSpawnedBy(session.key, session.lastActivityAt ?? Date.now());
|
|
178
|
+
}
|
|
179
|
+
const parsed = parseSessionKey(session.key);
|
|
180
|
+
const newSession = {
|
|
181
|
+
key: session.key,
|
|
182
|
+
agentId: session.agentId ?? parsed.agentId,
|
|
183
|
+
platform: session.platform ?? parsed.platform,
|
|
184
|
+
recipient: session.recipient ?? parsed.recipient,
|
|
185
|
+
isGroup: session.isGroup ?? parsed.isGroup,
|
|
186
|
+
lastActivityAt: session.lastActivityAt ?? Date.now(),
|
|
187
|
+
status: session.status ?? "idle",
|
|
188
|
+
spawnedBy,
|
|
189
|
+
messageCount: session.messageCount,
|
|
190
|
+
totalCostUsd: session.totalCostUsd,
|
|
191
|
+
totalInputTokens: session.totalInputTokens,
|
|
192
|
+
totalOutputTokens: session.totalOutputTokens,
|
|
193
|
+
};
|
|
194
|
+
this.sessions.set(session.key, newSession);
|
|
195
|
+
this.onSessionChange?.(newSession);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
updateSessionStatus(key, status) {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const session = this.sessions.get(key);
|
|
201
|
+
if (session) {
|
|
202
|
+
const updated = {
|
|
203
|
+
...session,
|
|
204
|
+
status,
|
|
205
|
+
lastActivityAt: now,
|
|
206
|
+
};
|
|
207
|
+
this.sessions.set(key, updated);
|
|
208
|
+
this.onSessionChange?.(updated);
|
|
209
|
+
}
|
|
210
|
+
else if (isSubagentSession(key)) {
|
|
211
|
+
const spawnedBy = this.inferSpawnedBy(key, now);
|
|
212
|
+
const parsed = parseSessionKey(key);
|
|
213
|
+
const newSession = {
|
|
214
|
+
key,
|
|
215
|
+
agentId: parsed.agentId,
|
|
216
|
+
platform: parsed.platform,
|
|
217
|
+
recipient: parsed.recipient,
|
|
218
|
+
isGroup: parsed.isGroup,
|
|
219
|
+
lastActivityAt: now,
|
|
220
|
+
status,
|
|
221
|
+
spawnedBy,
|
|
222
|
+
};
|
|
223
|
+
this.sessions.set(key, newSession);
|
|
224
|
+
this.onSessionChange?.(newSession);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
addAction(action) {
|
|
228
|
+
if (action.sessionKey && !action.sessionKey.includes("lifecycle")) {
|
|
229
|
+
const previous = this.runSessionMap.get(action.runId);
|
|
230
|
+
this.runSessionMap.set(action.runId, action.sessionKey);
|
|
231
|
+
if (previous !== action.sessionKey) {
|
|
232
|
+
this.backfillExecSessionKey(action.runId, action.sessionKey);
|
|
233
|
+
}
|
|
234
|
+
if (isParentSession(action.sessionKey)) {
|
|
235
|
+
this.trackParentAction(action.sessionKey, action.timestamp);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let sessionKey = action.sessionKey;
|
|
239
|
+
if (!sessionKey || sessionKey === "lifecycle") {
|
|
240
|
+
sessionKey = this.runSessionMap.get(action.runId) || sessionKey;
|
|
241
|
+
}
|
|
242
|
+
const actionNodeId = `${action.runId}-action`;
|
|
243
|
+
if (["start", "streaming", "complete", "error", "aborted"].includes(action.type)) {
|
|
244
|
+
const existing = this.actions.get(actionNodeId);
|
|
245
|
+
if (existing) {
|
|
246
|
+
const updated = {
|
|
247
|
+
...existing,
|
|
248
|
+
type: action.type,
|
|
249
|
+
seq: action.seq,
|
|
250
|
+
timestamp: action.timestamp,
|
|
251
|
+
sessionKey: sessionKey && sessionKey !== "lifecycle"
|
|
252
|
+
? sessionKey
|
|
253
|
+
: existing.sessionKey,
|
|
254
|
+
content: action.content ?? existing.content,
|
|
255
|
+
inputTokens: action.inputTokens ?? existing.inputTokens,
|
|
256
|
+
outputTokens: action.outputTokens ?? existing.outputTokens,
|
|
257
|
+
stopReason: action.stopReason ?? existing.stopReason,
|
|
258
|
+
endedAt: action.endedAt ?? existing.endedAt,
|
|
259
|
+
costUsd: action.costUsd ?? existing.costUsd,
|
|
260
|
+
model: action.model ?? existing.model,
|
|
261
|
+
provider: action.provider ?? existing.provider,
|
|
262
|
+
duration: existing.startedAt && action.endedAt
|
|
263
|
+
? action.endedAt - existing.startedAt
|
|
264
|
+
: existing.duration,
|
|
265
|
+
};
|
|
266
|
+
this.actions.set(actionNodeId, updated);
|
|
267
|
+
this.onActionChange?.(updated);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const newAction = {
|
|
271
|
+
...action,
|
|
272
|
+
id: actionNodeId,
|
|
273
|
+
sessionKey,
|
|
274
|
+
};
|
|
275
|
+
this.actions.set(actionNodeId, newAction);
|
|
276
|
+
this.onActionChange?.(newAction);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const existing = this.actions.get(action.id);
|
|
281
|
+
if (!existing) {
|
|
282
|
+
const newAction = { ...action, sessionKey };
|
|
283
|
+
this.actions.set(action.id, newAction);
|
|
284
|
+
this.onActionChange?.(newAction);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
addExecEvent(event) {
|
|
288
|
+
const sessionKey = this.resolveSessionKey(event);
|
|
289
|
+
const existing = this.execs.get(event.execId);
|
|
290
|
+
if (event.eventType === "started") {
|
|
291
|
+
if (existing) {
|
|
292
|
+
const updated = {
|
|
293
|
+
...existing,
|
|
294
|
+
command: event.command || existing.command || EXEC_PLACEHOLDER_COMMAND,
|
|
295
|
+
sessionId: event.sessionId || existing.sessionId,
|
|
296
|
+
sessionKey: sessionKey || existing.sessionKey,
|
|
297
|
+
status: "running",
|
|
298
|
+
startedAt: event.startedAt ?? existing.startedAt ?? event.timestamp,
|
|
299
|
+
timestamp: event.startedAt ?? existing.startedAt ?? event.timestamp,
|
|
300
|
+
lastActivityAt: event.timestamp,
|
|
301
|
+
};
|
|
302
|
+
this.execs.set(event.execId, updated);
|
|
303
|
+
this.onExecChange?.(updated);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const newExec = {
|
|
307
|
+
...this.createPlaceholderExec(event, sessionKey),
|
|
308
|
+
command: event.command || EXEC_PLACEHOLDER_COMMAND,
|
|
309
|
+
startedAt: event.startedAt ?? event.timestamp,
|
|
310
|
+
timestamp: event.startedAt ?? event.timestamp,
|
|
311
|
+
};
|
|
312
|
+
this.execs.set(event.execId, newExec);
|
|
313
|
+
this.onExecChange?.(newExec);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (event.eventType === "output") {
|
|
317
|
+
const stream = event.stream || "stdout";
|
|
318
|
+
const text = event.output ?? "";
|
|
319
|
+
const chunk = {
|
|
320
|
+
id: event.id,
|
|
321
|
+
stream,
|
|
322
|
+
text,
|
|
323
|
+
timestamp: event.timestamp,
|
|
324
|
+
};
|
|
325
|
+
if (existing) {
|
|
326
|
+
const capped = capExecOutputs([...existing.outputs, chunk]);
|
|
327
|
+
const updated = {
|
|
328
|
+
...existing,
|
|
329
|
+
sessionId: event.sessionId || existing.sessionId,
|
|
330
|
+
sessionKey: sessionKey || existing.sessionKey,
|
|
331
|
+
lastActivityAt: event.timestamp,
|
|
332
|
+
outputs: text ? capped.outputs : existing.outputs,
|
|
333
|
+
outputTruncated: existing.outputTruncated || capped.truncated,
|
|
334
|
+
};
|
|
335
|
+
this.execs.set(event.execId, updated);
|
|
336
|
+
this.onExecChange?.(updated);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const placeholder = this.createPlaceholderExec(event, sessionKey);
|
|
340
|
+
if (text) {
|
|
341
|
+
const capped = capExecOutputs([chunk]);
|
|
342
|
+
placeholder.outputs = capped.outputs;
|
|
343
|
+
placeholder.outputTruncated = capped.truncated;
|
|
344
|
+
}
|
|
345
|
+
this.execs.set(event.execId, placeholder);
|
|
346
|
+
this.onExecChange?.(placeholder);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (event.eventType === "completed") {
|
|
350
|
+
const completedStatus = mapExecStatus(event.exitCode, event.status);
|
|
351
|
+
if (existing) {
|
|
352
|
+
const completedAt = existing.durationMs != null
|
|
353
|
+
? existing.startedAt + existing.durationMs
|
|
354
|
+
: event.timestamp;
|
|
355
|
+
const updated = {
|
|
356
|
+
...existing,
|
|
357
|
+
sessionId: event.sessionId || existing.sessionId,
|
|
358
|
+
sessionKey: sessionKey || existing.sessionKey,
|
|
359
|
+
command: event.command || existing.command || EXEC_PLACEHOLDER_COMMAND,
|
|
360
|
+
exitCode: event.exitCode ?? existing.exitCode,
|
|
361
|
+
durationMs: event.durationMs ?? existing.durationMs,
|
|
362
|
+
completedAt,
|
|
363
|
+
status: completedStatus,
|
|
364
|
+
lastActivityAt: event.timestamp,
|
|
365
|
+
};
|
|
366
|
+
this.execs.set(event.execId, updated);
|
|
367
|
+
this.onExecChange?.(updated);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const placeholder = this.createPlaceholderExec(event, sessionKey);
|
|
371
|
+
placeholder.command = event.command || placeholder.command;
|
|
372
|
+
placeholder.exitCode = event.exitCode;
|
|
373
|
+
placeholder.durationMs = event.durationMs;
|
|
374
|
+
placeholder.completedAt =
|
|
375
|
+
placeholder.durationMs != null
|
|
376
|
+
? placeholder.startedAt + placeholder.durationMs
|
|
377
|
+
: event.timestamp;
|
|
378
|
+
placeholder.status = completedStatus;
|
|
379
|
+
this.execs.set(event.execId, placeholder);
|
|
380
|
+
this.onExecChange?.(placeholder);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ─── Queries ───────────────────────────────────────────────────────────────
|
|
384
|
+
getSessions() {
|
|
385
|
+
return Array.from(this.sessions.values());
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Returns only recently-active sessions for the dashboard.
|
|
389
|
+
* Filters out stale idle sessions loaded from filesystem that are older than windowMs.
|
|
390
|
+
* Active/thinking sessions are always included regardless of age.
|
|
391
|
+
*/
|
|
392
|
+
getActiveSessions(windowMs = 2 * 60 * 60 * 1000) {
|
|
393
|
+
const cutoff = Date.now() - windowMs;
|
|
394
|
+
return Array.from(this.sessions.values())
|
|
395
|
+
.filter(s => s.status !== "idle" || s.lastActivityAt >= cutoff)
|
|
396
|
+
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
397
|
+
}
|
|
398
|
+
getActions(opts) {
|
|
399
|
+
let actions = Array.from(this.actions.values());
|
|
400
|
+
if (opts?.sessionKey) {
|
|
401
|
+
actions = actions.filter((a) => a.sessionKey === opts.sessionKey);
|
|
402
|
+
}
|
|
403
|
+
actions.sort((a, b) => b.timestamp - a.timestamp);
|
|
404
|
+
if (opts?.limit) {
|
|
405
|
+
actions = actions.slice(0, opts.limit);
|
|
406
|
+
}
|
|
407
|
+
return actions;
|
|
408
|
+
}
|
|
409
|
+
getExecs(opts) {
|
|
410
|
+
let execs = Array.from(this.execs.values());
|
|
411
|
+
if (opts?.status) {
|
|
412
|
+
execs = execs.filter((e) => e.status === opts.status);
|
|
413
|
+
}
|
|
414
|
+
if (opts?.sessionKey) {
|
|
415
|
+
execs = execs.filter((e) => e.sessionKey === opts.sessionKey);
|
|
416
|
+
}
|
|
417
|
+
execs.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
418
|
+
return execs;
|
|
419
|
+
}
|
|
420
|
+
getExec(id) {
|
|
421
|
+
return this.execs.get(id);
|
|
422
|
+
}
|
|
423
|
+
getStats() {
|
|
424
|
+
let totalCostUsd = 0;
|
|
425
|
+
for (const session of this.sessions.values()) {
|
|
426
|
+
if (session.totalCostUsd) {
|
|
427
|
+
totalCostUsd += session.totalCostUsd;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
sessions: this.sessions.size,
|
|
432
|
+
actions: this.actions.size,
|
|
433
|
+
execs: this.execs.size,
|
|
434
|
+
runSessionMapSize: this.runSessionMap.size,
|
|
435
|
+
totalCostUsd,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// ─── Cost Tracking ───────────────────────────────────────────────────────────
|
|
439
|
+
updateSessionCost(sessionKey, costUsd, inputTokens, outputTokens) {
|
|
440
|
+
const existing = this.sessions.get(sessionKey);
|
|
441
|
+
if (existing) {
|
|
442
|
+
const updated = {
|
|
443
|
+
...existing,
|
|
444
|
+
totalCostUsd: (existing.totalCostUsd ?? 0) + costUsd,
|
|
445
|
+
totalInputTokens: (existing.totalInputTokens ?? 0) + (inputTokens ?? 0),
|
|
446
|
+
totalOutputTokens: (existing.totalOutputTokens ?? 0) + (outputTokens ?? 0),
|
|
447
|
+
};
|
|
448
|
+
this.sessions.set(sessionKey, updated);
|
|
449
|
+
this.onSessionChange?.(updated);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const parsed = parseSessionKey(sessionKey);
|
|
453
|
+
const spawnedBy = isSubagentSession(sessionKey)
|
|
454
|
+
? this.inferSpawnedBy(sessionKey, Date.now())
|
|
455
|
+
: undefined;
|
|
456
|
+
const newSession = {
|
|
457
|
+
key: sessionKey,
|
|
458
|
+
agentId: parsed.agentId,
|
|
459
|
+
platform: parsed.platform,
|
|
460
|
+
recipient: parsed.recipient,
|
|
461
|
+
isGroup: parsed.isGroup,
|
|
462
|
+
lastActivityAt: Date.now(),
|
|
463
|
+
status: "active",
|
|
464
|
+
spawnedBy,
|
|
465
|
+
totalCostUsd: costUsd,
|
|
466
|
+
totalInputTokens: inputTokens ?? 0,
|
|
467
|
+
totalOutputTokens: outputTokens ?? 0,
|
|
468
|
+
};
|
|
469
|
+
this.sessions.set(sessionKey, newSession);
|
|
470
|
+
this.onSessionChange?.(newSession);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
syncAggregatedCosts(summary) {
|
|
474
|
+
if (summary.bySession) {
|
|
475
|
+
for (const [sessionKey, totals] of Object.entries(summary.bySession)) {
|
|
476
|
+
if (totals.totalCost > 0) {
|
|
477
|
+
const existing = this.sessions.get(sessionKey);
|
|
478
|
+
if (existing) {
|
|
479
|
+
const updated = {
|
|
480
|
+
...existing,
|
|
481
|
+
totalCostUsd: totals.totalCost,
|
|
482
|
+
totalInputTokens: totals.input,
|
|
483
|
+
totalOutputTokens: totals.output,
|
|
484
|
+
};
|
|
485
|
+
this.sessions.set(sessionKey, updated);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
|
492
|
+
clear() {
|
|
493
|
+
this.runSessionMap.clear();
|
|
494
|
+
this.parentActionHistory.clear();
|
|
495
|
+
this.sessions.clear();
|
|
496
|
+
this.actions.clear();
|
|
497
|
+
this.execs.clear();
|
|
498
|
+
}
|
|
499
|
+
hydrate(sessions, actions, execEvents = []) {
|
|
500
|
+
this.clear();
|
|
501
|
+
const sortedActions = [...actions].sort((a, b) => a.timestamp - b.timestamp);
|
|
502
|
+
for (const action of sortedActions) {
|
|
503
|
+
if (action.sessionKey && isParentSession(action.sessionKey)) {
|
|
504
|
+
this.trackParentAction(action.sessionKey, action.timestamp);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (const session of sessions) {
|
|
508
|
+
if (isParentSession(session.key)) {
|
|
509
|
+
this.trackParentAction(session.key, session.lastActivityAt);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const session of sessions) {
|
|
513
|
+
if (isSubagentSession(session.key)) {
|
|
514
|
+
const spawnedBy = session.spawnedBy ||
|
|
515
|
+
this.inferSpawnedBy(session.key, session.lastActivityAt);
|
|
516
|
+
this.sessions.set(session.key, { ...session, spawnedBy });
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
this.sessions.set(session.key, session);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const action of sortedActions) {
|
|
523
|
+
this.addAction(action);
|
|
524
|
+
}
|
|
525
|
+
const sortedExecEvents = [...execEvents].sort((a, b) => a.timestamp - b.timestamp);
|
|
526
|
+
for (const event of sortedExecEvents) {
|
|
527
|
+
this.addExecEvent(event);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
exportSessions() {
|
|
531
|
+
return this.getSessions();
|
|
532
|
+
}
|
|
533
|
+
exportActions() {
|
|
534
|
+
return this.getActions();
|
|
535
|
+
}
|
|
536
|
+
exportExecEvents() {
|
|
537
|
+
const events = [];
|
|
538
|
+
for (const exec of this.execs.values()) {
|
|
539
|
+
events.push({
|
|
540
|
+
id: `${exec.id}-started`,
|
|
541
|
+
execId: exec.id,
|
|
542
|
+
runId: exec.runId,
|
|
543
|
+
pid: exec.pid,
|
|
544
|
+
sessionId: exec.sessionId,
|
|
545
|
+
sessionKey: exec.sessionKey,
|
|
546
|
+
eventType: "started",
|
|
547
|
+
command: exec.command,
|
|
548
|
+
startedAt: exec.startedAt,
|
|
549
|
+
timestamp: exec.startedAt,
|
|
550
|
+
});
|
|
551
|
+
for (const chunk of exec.outputs) {
|
|
552
|
+
events.push({
|
|
553
|
+
id: chunk.id,
|
|
554
|
+
execId: exec.id,
|
|
555
|
+
runId: exec.runId,
|
|
556
|
+
pid: exec.pid,
|
|
557
|
+
sessionId: exec.sessionId,
|
|
558
|
+
sessionKey: exec.sessionKey,
|
|
559
|
+
eventType: "output",
|
|
560
|
+
stream: chunk.stream,
|
|
561
|
+
output: chunk.text,
|
|
562
|
+
timestamp: chunk.timestamp,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (exec.completedAt) {
|
|
566
|
+
events.push({
|
|
567
|
+
id: `${exec.id}-completed`,
|
|
568
|
+
execId: exec.id,
|
|
569
|
+
runId: exec.runId,
|
|
570
|
+
pid: exec.pid,
|
|
571
|
+
sessionId: exec.sessionId,
|
|
572
|
+
sessionKey: exec.sessionKey,
|
|
573
|
+
eventType: "completed",
|
|
574
|
+
durationMs: exec.durationMs,
|
|
575
|
+
exitCode: exec.exitCode,
|
|
576
|
+
status: exec.status,
|
|
577
|
+
timestamp: exec.completedAt,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return events;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ChatEvent, AgentEvent, ExecStartedEvent, ExecOutputEvent, ExecCompletedEvent, MonitorSession, MonitorAction, MonitorExecEvent, DiagnosticUsageEvent } from "./types.js";
|
|
2
|
+
export interface SessionInfo {
|
|
3
|
+
key: string;
|
|
4
|
+
agentId: string;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
lastActivityAt: number;
|
|
7
|
+
messageCount: number;
|
|
8
|
+
lastMessage?: unknown;
|
|
9
|
+
spawnedBy?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function sessionInfoToMonitor(info: SessionInfo): Partial<MonitorSession>;
|
|
12
|
+
export declare function chatEventToAction(event: ChatEvent): MonitorAction;
|
|
13
|
+
export declare function agentEventToAction(event: AgentEvent): MonitorAction;
|
|
14
|
+
export declare function execStartedToEvent(event: ExecStartedEvent, seq?: number): MonitorExecEvent;
|
|
15
|
+
export declare function execOutputToEvent(event: ExecOutputEvent, seq?: number): MonitorExecEvent;
|
|
16
|
+
export declare function execCompletedToEvent(event: ExecCompletedEvent, seq?: number): MonitorExecEvent;
|
|
17
|
+
export interface ParsedGatewayEvent {
|
|
18
|
+
session?: Partial<MonitorSession>;
|
|
19
|
+
action?: MonitorAction;
|
|
20
|
+
execEvent?: MonitorExecEvent;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseGatewayEvent(eventName: string, payload: unknown, seq?: number): ParsedGatewayEvent | null;
|
|
23
|
+
export interface ParsedDiagnosticUsage {
|
|
24
|
+
session?: Partial<MonitorSession>;
|
|
25
|
+
action?: MonitorAction;
|
|
26
|
+
}
|
|
27
|
+
export declare function diagnosticUsageToSessionUpdate(event: DiagnosticUsageEvent): ParsedDiagnosticUsage;
|