@steadwing/openalerts 0.2.4 → 0.2.5
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 +6 -2
- 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/engine.js +2 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/rules.js +97 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/index.js +410 -23
- package/dist/plugin/dashboard-html.js +33 -3
- package/dist/plugin/dashboard-routes.d.ts +7 -2
- package/dist/plugin/dashboard-routes.js +111 -3
- package/dist/plugin/gateway-client.js +15 -8
- package/openclaw.plugin.json +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/@steadwing/openalerts"><img src="https://img.shields.io/npm/v/@steadwing/openalerts?style=flat&color=blue" alt="npm"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@steadwing/openalerts"><img src="https://img.shields.io/npm/dt/@steadwing/openalerts?style=flat&color=blue" alt="npm"></a>
|
|
10
11
|
<a href="https://github.com/steadwing/openalerts/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green" alt="License"></a>
|
|
11
12
|
<a href="https://github.com/steadwing/openalerts/stargazers"><img src="https://img.shields.io/github/stars/steadwing/openalerts?style=flat" alt="GitHub stars"></a>
|
|
12
13
|
<a href="https://discord.gg/4rUP86tSXn"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat" alt="Discord"></a>
|
|
@@ -83,13 +84,16 @@ A real-time web dashboard is embedded in the gateway at:
|
|
|
83
84
|
http://127.0.0.1:18789/openalerts
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
- **Activity** —
|
|
87
|
+
- **Activity** — Step-by-step execution timeline with tool calls, LLM usage, costs
|
|
88
|
+
- **Sessions** — Active sessions with cost/token aggregation
|
|
89
|
+
- **Execs** — Shell command executions with output capture
|
|
87
90
|
- **System Logs** — Filtered, structured logs with search
|
|
88
91
|
- **Health** — Rule status, alert history, system stats
|
|
92
|
+
- **Debug** — State snapshot for troubleshooting
|
|
89
93
|
|
|
90
94
|
## Alert Rules
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
Ten rules run against every event in real-time. All thresholds and cooldowns are configurable.
|
|
93
97
|
|
|
94
98
|
| Rule | Watches for | Severity | Threshold (default) |
|
|
95
99
|
| ----------------- | ------------------------------------- | -------- | ------------------- |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type MonitorSession, type MonitorAction, type MonitorExecEvent, type MonitorExecProcess, type MonitorExecProcessStatus, type CollectionStats, type CostUsageSummary } from "./types.js";
|
|
2
|
+
export declare class CollectionManager {
|
|
3
|
+
private sessions;
|
|
4
|
+
private actions;
|
|
5
|
+
private execs;
|
|
6
|
+
private runSessionMap;
|
|
7
|
+
private parentActionHistory;
|
|
8
|
+
private onSessionChange?;
|
|
9
|
+
private onActionChange?;
|
|
10
|
+
private onExecChange?;
|
|
11
|
+
constructor();
|
|
12
|
+
setCallbacks(callbacks: {
|
|
13
|
+
onSessionChange?: (session: MonitorSession) => void;
|
|
14
|
+
onActionChange?: (action: MonitorAction) => void;
|
|
15
|
+
onExecChange?: (exec: MonitorExecProcess) => void;
|
|
16
|
+
}): void;
|
|
17
|
+
private trackParentAction;
|
|
18
|
+
private inferSpawnedBy;
|
|
19
|
+
private resolveSessionKey;
|
|
20
|
+
private backfillExecSessionKey;
|
|
21
|
+
private createPlaceholderExec;
|
|
22
|
+
upsertSession(session: Partial<MonitorSession>): void;
|
|
23
|
+
updateSessionStatus(key: string, status: MonitorSession["status"]): void;
|
|
24
|
+
addAction(action: MonitorAction): void;
|
|
25
|
+
addExecEvent(event: MonitorExecEvent): void;
|
|
26
|
+
getSessions(): MonitorSession[];
|
|
27
|
+
/**
|
|
28
|
+
* Returns only recently-active sessions for the dashboard.
|
|
29
|
+
* Filters out stale idle sessions loaded from filesystem that are older than windowMs.
|
|
30
|
+
* Active/thinking sessions are always included regardless of age.
|
|
31
|
+
*/
|
|
32
|
+
getActiveSessions(windowMs?: number): MonitorSession[];
|
|
33
|
+
getActions(opts?: {
|
|
34
|
+
sessionKey?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}): MonitorAction[];
|
|
37
|
+
getExecs(opts?: {
|
|
38
|
+
status?: MonitorExecProcessStatus;
|
|
39
|
+
sessionKey?: string;
|
|
40
|
+
}): MonitorExecProcess[];
|
|
41
|
+
getExec(id: string): MonitorExecProcess | undefined;
|
|
42
|
+
getStats(): CollectionStats;
|
|
43
|
+
updateSessionCost(sessionKey: string, costUsd: number, inputTokens?: number, outputTokens?: number): void;
|
|
44
|
+
syncAggregatedCosts(summary: CostUsageSummary): void;
|
|
45
|
+
clear(): void;
|
|
46
|
+
hydrate(sessions: MonitorSession[], actions: MonitorAction[], execEvents?: MonitorExecEvent[]): void;
|
|
47
|
+
exportSessions(): MonitorSession[];
|
|
48
|
+
exportActions(): MonitorAction[];
|
|
49
|
+
exportExecEvents(): MonitorExecEvent[];
|
|
50
|
+
}
|
|
@@ -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;
|