@steadwing/openalerts 0.2.3 → 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 +63 -53
- 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.d.ts +39 -0
- package/dist/plugin/gateway-client.js +200 -0
- package/openclaw.plugin.json +30 -0
- package/package.json +18 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { OpenAlertsEngine } from "./core/index.js";
|
|
1
|
+
import { ALL_RULES, OpenAlertsEngine } from "./core/index.js";
|
|
2
2
|
import { onDiagnosticEvent, registerLogTransport } from "openclaw/plugin-sdk";
|
|
3
3
|
import { createLogBridge } from "./plugin/log-bridge.js";
|
|
4
|
+
import { GatewayClient } from "./plugin/gateway-client.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
4
8
|
import { OpenClawAlertChannel, createOpenClawEnricher, parseConfig, resolveAlertTarget, translateOpenClawEvent, translateToolCallHook, translateAgentStartHook, translateAgentEndHook, translateSessionStartHook, translateSessionEndHook, translateMessageSentHook, translateMessageReceivedHook, translateBeforeToolCallHook, translateBeforeCompactionHook, translateAfterCompactionHook, translateMessageSendingHook, translateToolResultPersistHook, translateGatewayStartHook, translateGatewayStopHook, } from "./plugin/adapter.js";
|
|
5
9
|
import { bindEngine, createMonitorCommands } from "./plugin/commands.js";
|
|
6
|
-
import { createDashboardHandler, closeDashboardConnections, } from "./plugin/dashboard-routes.js";
|
|
10
|
+
import { createDashboardHandler, closeDashboardConnections, emitAgentMonitorEvent, recordGatewayEvent, } from "./plugin/dashboard-routes.js";
|
|
11
|
+
import { CollectionManager, CollectionPersistence, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./collections/index.js";
|
|
7
12
|
const PLUGIN_ID = "openalerts";
|
|
8
13
|
const LOG_PREFIX = "openalerts";
|
|
9
14
|
let engine = null;
|
|
10
15
|
let unsubDiagnostic = null;
|
|
11
16
|
let unsubLogTransport = null;
|
|
12
17
|
let logBridgeCleanup = null;
|
|
18
|
+
let gatewayClient = null;
|
|
19
|
+
let collections = null;
|
|
20
|
+
let collectionsPersistence = null;
|
|
21
|
+
let sessionSyncInterval = null;
|
|
13
22
|
function createMonitorService(api) {
|
|
14
23
|
return {
|
|
15
24
|
id: PLUGIN_ID,
|
|
@@ -36,7 +45,7 @@ function createMonitorService(api) {
|
|
|
36
45
|
engine.start();
|
|
37
46
|
// Wire commands to engine
|
|
38
47
|
bindEngine(engine, api);
|
|
39
|
-
// ── Bridge 1: Diagnostic events → engine
|
|
48
|
+
// ── Bridge 1: Diagnostic events → engine + collections ───────────────────
|
|
40
49
|
// Covers: webhook.*, message.*, session.stuck, session.state,
|
|
41
50
|
// model.usage, queue.lane.*, diagnostic.heartbeat, run.attempt
|
|
42
51
|
unsubDiagnostic = onDiagnosticEvent((event) => {
|
|
@@ -44,6 +53,17 @@ function createMonitorService(api) {
|
|
|
44
53
|
if (translated) {
|
|
45
54
|
engine.ingest(translated);
|
|
46
55
|
}
|
|
56
|
+
// Feed model.usage to collections for cost tracking
|
|
57
|
+
if (event.type === "model.usage" && collections) {
|
|
58
|
+
const parsed = diagnosticUsageToSessionUpdate(event);
|
|
59
|
+
if (parsed.session) {
|
|
60
|
+
collections.upsertSession(parsed.session);
|
|
61
|
+
}
|
|
62
|
+
if (parsed.action && collectionsPersistence) {
|
|
63
|
+
collections.addAction(parsed.action);
|
|
64
|
+
collectionsPersistence.queueAction(parsed.action);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
47
67
|
});
|
|
48
68
|
// ── Bridge 2: Log transport → engine (fills gaps from non-firing hooks) ─
|
|
49
69
|
// Parses structured log records to synthesize tool.call, session.start/end,
|
|
@@ -51,6 +71,27 @@ function createMonitorService(api) {
|
|
|
51
71
|
const logBridge = createLogBridge(engine);
|
|
52
72
|
unsubLogTransport = registerLogTransport(logBridge.transport);
|
|
53
73
|
logBridgeCleanup = logBridge.cleanup;
|
|
74
|
+
// Helper to track actions in collections (for sessions, tools, messages)
|
|
75
|
+
const trackAction = (type, eventType, data, context) => {
|
|
76
|
+
if (!collections || !collectionsPersistence)
|
|
77
|
+
return;
|
|
78
|
+
const action = {
|
|
79
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
80
|
+
runId: data.runId || `run-${Date.now()}`,
|
|
81
|
+
sessionKey: context.sessionKey ||
|
|
82
|
+
context.sessionId ||
|
|
83
|
+
"unknown",
|
|
84
|
+
seq: Date.now() % 1000000,
|
|
85
|
+
type,
|
|
86
|
+
eventType,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
content: typeof data.content === "string"
|
|
89
|
+
? data.content
|
|
90
|
+
: JSON.stringify(data).slice(0, 500),
|
|
91
|
+
};
|
|
92
|
+
collections.addAction(action);
|
|
93
|
+
collectionsPersistence.queueAction(action);
|
|
94
|
+
};
|
|
54
95
|
// ── Bridge 3: Plugin hooks → engine ───────────────────────────────────
|
|
55
96
|
// Covers: tool calls, agent lifecycle, session lifecycle, gateway, messages
|
|
56
97
|
// These events are NOT emitted as diagnostic events — they come through
|
|
@@ -61,38 +102,131 @@ function createMonitorService(api) {
|
|
|
61
102
|
apiOn("after_tool_call", (data, hookCtx) => {
|
|
62
103
|
if (!engine)
|
|
63
104
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
const d = data;
|
|
106
|
+
const ctx = hookCtx;
|
|
107
|
+
const sessionId = ctx.sessionKey ||
|
|
108
|
+
ctx.sessionId ||
|
|
109
|
+
undefined;
|
|
110
|
+
const agentId = ctx.agentId;
|
|
111
|
+
const runId = ctx.runId;
|
|
112
|
+
engine.ingest(translateToolCallHook(d, { sessionId, agentId }));
|
|
113
|
+
const _toolKey = sessionId && isRealSessionKey(sessionId)
|
|
114
|
+
? sessionId
|
|
115
|
+
: runSessionMap.get(runId || "");
|
|
116
|
+
const _toolParamSummary = d.params
|
|
117
|
+
? JSON.stringify(d.params).slice(0, 200)
|
|
118
|
+
: undefined;
|
|
119
|
+
const _toolResultSummary = d.result
|
|
120
|
+
? JSON.stringify(d.result).slice(0, 300)
|
|
121
|
+
: undefined;
|
|
122
|
+
if (_toolKey)
|
|
123
|
+
emitAgentMonitorEvent({
|
|
124
|
+
type: "agent",
|
|
125
|
+
data: {
|
|
126
|
+
ts: Date.now(),
|
|
127
|
+
type: "agent",
|
|
128
|
+
sessionKey: _toolKey,
|
|
129
|
+
runId: runId || `tool-${Date.now()}`,
|
|
130
|
+
data: {
|
|
131
|
+
phase: "tool",
|
|
132
|
+
toolName: d.toolName,
|
|
133
|
+
durationMs: d.durationMs,
|
|
134
|
+
error: d.error,
|
|
135
|
+
params: _toolParamSummary,
|
|
136
|
+
result: _toolResultSummary,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
68
140
|
});
|
|
69
141
|
// Agent lifecycle
|
|
70
142
|
apiOn("before_agent_start", (data, hookCtx) => {
|
|
71
143
|
if (!engine)
|
|
72
144
|
return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
145
|
+
const ctx = hookCtx;
|
|
146
|
+
const sessionId = ctx.sessionKey ||
|
|
147
|
+
ctx.sessionId ||
|
|
148
|
+
undefined;
|
|
149
|
+
const agentId = ctx.agentId;
|
|
150
|
+
const runId = ctx.runId;
|
|
151
|
+
engine.ingest(translateAgentStartHook(data, { sessionId, agentId }));
|
|
152
|
+
const _startKey = sessionId && isRealSessionKey(sessionId)
|
|
153
|
+
? sessionId
|
|
154
|
+
: runSessionMap.get(runId || "");
|
|
155
|
+
const _msgs = data.messages;
|
|
156
|
+
const _lastUMsg = Array.isArray(_msgs)
|
|
157
|
+
? _msgs
|
|
158
|
+
.filter((m) => m.role === "user")
|
|
159
|
+
.pop()
|
|
160
|
+
: null;
|
|
161
|
+
const _startCtx = _lastUMsg
|
|
162
|
+
? (typeof _lastUMsg.content === "string"
|
|
163
|
+
? _lastUMsg.content
|
|
164
|
+
: JSON.stringify(_lastUMsg.content)).slice(0, 300)
|
|
165
|
+
: undefined;
|
|
166
|
+
if (_startKey) {
|
|
167
|
+
runSessionMap.set(runId || "", _startKey);
|
|
168
|
+
emitAgentMonitorEvent({
|
|
169
|
+
type: "agent",
|
|
170
|
+
data: {
|
|
171
|
+
ts: Date.now(),
|
|
172
|
+
type: "agent",
|
|
173
|
+
sessionKey: _startKey,
|
|
174
|
+
runId: runId || `run-${Date.now()}`,
|
|
175
|
+
data: { phase: "start", agentId, content: _startCtx },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
77
179
|
});
|
|
78
180
|
apiOn("agent_end", (data, hookCtx) => {
|
|
79
181
|
if (!engine)
|
|
80
182
|
return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
183
|
+
const d = data;
|
|
184
|
+
const ctx = hookCtx;
|
|
185
|
+
const sessionId = ctx.sessionKey ||
|
|
186
|
+
ctx.sessionId ||
|
|
187
|
+
undefined;
|
|
188
|
+
const agentId = ctx.agentId;
|
|
189
|
+
const runId = ctx.runId;
|
|
190
|
+
engine.ingest(translateAgentEndHook(d, { sessionId, agentId }));
|
|
191
|
+
const _endKey = sessionId && isRealSessionKey(sessionId)
|
|
192
|
+
? sessionId
|
|
193
|
+
: runSessionMap.get(runId || "");
|
|
194
|
+
if (_endKey)
|
|
195
|
+
emitAgentMonitorEvent({
|
|
196
|
+
type: "agent",
|
|
197
|
+
data: {
|
|
198
|
+
ts: Date.now(),
|
|
199
|
+
type: "agent",
|
|
200
|
+
sessionKey: _endKey,
|
|
201
|
+
runId: runId || `run-${Date.now()}`,
|
|
202
|
+
data: {
|
|
203
|
+
phase: "end",
|
|
204
|
+
durationMs: d.durationMs,
|
|
205
|
+
success: d.success,
|
|
206
|
+
error: d.error,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
85
210
|
});
|
|
86
211
|
// Session lifecycle
|
|
87
|
-
apiOn("session_start", (data) => {
|
|
212
|
+
apiOn("session_start", (data, hookCtx) => {
|
|
88
213
|
if (!engine)
|
|
89
214
|
return;
|
|
90
215
|
engine.ingest(translateSessionStartHook(data));
|
|
216
|
+
// Track in collections
|
|
217
|
+
const ctx = hookCtx;
|
|
218
|
+
trackAction("start", "system", { sessionId: data.sessionId }, ctx);
|
|
91
219
|
});
|
|
92
|
-
apiOn("session_end", (data) => {
|
|
220
|
+
apiOn("session_end", (data, hookCtx) => {
|
|
93
221
|
if (!engine)
|
|
94
222
|
return;
|
|
95
223
|
engine.ingest(translateSessionEndHook(data));
|
|
224
|
+
// Track in collections
|
|
225
|
+
const ctx = hookCtx;
|
|
226
|
+
trackAction("complete", "system", {
|
|
227
|
+
sessionId: data.sessionId,
|
|
228
|
+
messageCount: data.messageCount,
|
|
229
|
+
}, ctx);
|
|
96
230
|
});
|
|
97
231
|
// Message delivery tracking (all messages — success and failure)
|
|
98
232
|
apiOn("message_sent", (data, hookCtx) => {
|
|
@@ -105,13 +239,36 @@ function createMonitorService(api) {
|
|
|
105
239
|
}));
|
|
106
240
|
});
|
|
107
241
|
// Inbound message tracking (fires reliably in all modes)
|
|
242
|
+
// Inbound message tracking (fires reliably in all modes)
|
|
108
243
|
apiOn("message_received", (data, hookCtx) => {
|
|
109
244
|
if (!engine)
|
|
110
245
|
return;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
246
|
+
const d = data;
|
|
247
|
+
const ctx = hookCtx;
|
|
248
|
+
const channelId = ctx.channelId;
|
|
249
|
+
const accountId = ctx.accountId;
|
|
250
|
+
engine.ingest(translateMessageReceivedHook(d, { channelId, accountId }));
|
|
251
|
+
// Emit inbound message to agent monitor live view
|
|
252
|
+
const _inboundKey = channelId || "unknown";
|
|
253
|
+
// Register channelId as a known session so subsequent agent events can link to it
|
|
254
|
+
if (_inboundKey !== "unknown" && isRealSessionKey(_inboundKey)) {
|
|
255
|
+
// Will be linked by runId once agent starts
|
|
256
|
+
}
|
|
257
|
+
emitAgentMonitorEvent({
|
|
258
|
+
type: "chat",
|
|
259
|
+
data: {
|
|
260
|
+
ts: d.timestamp || Date.now(),
|
|
261
|
+
type: "chat",
|
|
262
|
+
sessionKey: _inboundKey,
|
|
263
|
+
runId: "inbound",
|
|
264
|
+
data: {
|
|
265
|
+
state: "inbound",
|
|
266
|
+
content: d.content,
|
|
267
|
+
from: d.from,
|
|
268
|
+
isInbound: true,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
115
272
|
});
|
|
116
273
|
// Tool start tracking (fires reliably — complements log-bridge tool end)
|
|
117
274
|
apiOn("before_tool_call", (data, hookCtx) => {
|
|
@@ -153,6 +310,25 @@ function createMonitorService(api) {
|
|
|
153
310
|
apiOn("message_sending", (data, hookCtx) => {
|
|
154
311
|
if (!engine)
|
|
155
312
|
return;
|
|
313
|
+
const _sendCtx = hookCtx;
|
|
314
|
+
const _sendChannelId = _sendCtx.channelId;
|
|
315
|
+
const _sendData = data;
|
|
316
|
+
if (_sendChannelId) {
|
|
317
|
+
emitAgentMonitorEvent({
|
|
318
|
+
type: "chat",
|
|
319
|
+
data: {
|
|
320
|
+
ts: Date.now(),
|
|
321
|
+
type: "chat",
|
|
322
|
+
sessionKey: _sendChannelId,
|
|
323
|
+
runId: "outbound",
|
|
324
|
+
data: {
|
|
325
|
+
state: "outbound",
|
|
326
|
+
content: _sendData.content,
|
|
327
|
+
to: _sendData.to,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
156
332
|
engine.ingest(translateMessageSendingHook(data, {
|
|
157
333
|
channelId: hookCtx.channelId,
|
|
158
334
|
accountId: hookCtx.accountId,
|
|
@@ -171,13 +347,224 @@ function createMonitorService(api) {
|
|
|
171
347
|
});
|
|
172
348
|
logger.info(`${LOG_PREFIX}: subscribed to 13 plugin hooks (100% coverage: tool, agent, session, gateway, message, compaction)`);
|
|
173
349
|
}
|
|
350
|
+
// ── Collections: Session/Action/Exec tracking ───────────────────────────
|
|
351
|
+
// NOTE: Direct filesystem reading for sessions (no gateway pairing needed)
|
|
352
|
+
// Sessions are read from ~/.openclaw/agents/<agent>/sessions/sessions.json
|
|
353
|
+
collections = new CollectionManager();
|
|
354
|
+
collectionsPersistence = new CollectionPersistence(ctx.stateDir);
|
|
355
|
+
collectionsPersistence.start();
|
|
356
|
+
// Read sessions from filesystem
|
|
357
|
+
const agentsDir = path.join(process.env.OPENCLAW_HOME ?? path.join(os.homedir(), ".openclaw"), "agents");
|
|
358
|
+
const loadSessionsFromFilesystem = () => {
|
|
359
|
+
try {
|
|
360
|
+
if (!fs.existsSync(agentsDir))
|
|
361
|
+
return;
|
|
362
|
+
const agentDirs = fs
|
|
363
|
+
.readdirSync(agentsDir, { withFileTypes: true })
|
|
364
|
+
.filter((d) => d.isDirectory());
|
|
365
|
+
let loadedCount = 0;
|
|
366
|
+
for (const agentDir of agentDirs) {
|
|
367
|
+
const sessionsFile = path.join(agentsDir, agentDir.name, "sessions", "sessions.json");
|
|
368
|
+
if (fs.existsSync(sessionsFile)) {
|
|
369
|
+
const content = fs.readFileSync(sessionsFile, "utf-8");
|
|
370
|
+
const sessionsObj = JSON.parse(content);
|
|
371
|
+
// sessions.json is an object with keys like "agent:main:main"
|
|
372
|
+
for (const [key, session] of Object.entries(sessionsObj)) {
|
|
373
|
+
const s = session;
|
|
374
|
+
if (s.sessionId) {
|
|
375
|
+
collections?.upsertSession({
|
|
376
|
+
key: key,
|
|
377
|
+
agentId: s.agentId || agentDir.name,
|
|
378
|
+
platform: s.platform || "unknown",
|
|
379
|
+
recipient: s.recipient || "",
|
|
380
|
+
isGroup: s.isGroup || false,
|
|
381
|
+
lastActivityAt: s.updatedAt || Date.now(),
|
|
382
|
+
status: "idle",
|
|
383
|
+
messageCount: s.messageCount,
|
|
384
|
+
});
|
|
385
|
+
loadedCount++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
logger.info(`${LOG_PREFIX}: loaded ${loadedCount} sessions from filesystem`);
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
logger.warn(`${LOG_PREFIX}: failed to load sessions from filesystem: ${err}`);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
loadSessionsFromFilesystem();
|
|
397
|
+
// Persist sessions immediately after loading
|
|
398
|
+
if (collections && collectionsPersistence) {
|
|
399
|
+
collectionsPersistence.saveSessions(collections.getSessions());
|
|
400
|
+
}
|
|
401
|
+
// Persist sessions periodically
|
|
402
|
+
sessionSyncInterval = setInterval(() => {
|
|
403
|
+
if (collections && collectionsPersistence) {
|
|
404
|
+
collectionsPersistence.saveSessions(collections.getSessions());
|
|
405
|
+
}
|
|
406
|
+
}, 30000);
|
|
407
|
+
// Hydrate from persisted data
|
|
408
|
+
const hydrated = collectionsPersistence.hydrate();
|
|
409
|
+
if (hydrated.sessions.length > 0 || hydrated.actions.length > 0) {
|
|
410
|
+
collections.hydrate(hydrated.sessions, hydrated.actions, hydrated.execEvents);
|
|
411
|
+
logger.info(`${LOG_PREFIX}: hydrated ${hydrated.sessions.length} sessions, ${hydrated.actions.length} actions, ${hydrated.execEvents.length} exec events`);
|
|
412
|
+
}
|
|
413
|
+
// Wire collection changes to persistence
|
|
414
|
+
collections.setCallbacks({
|
|
415
|
+
onSessionChange: (session) => {
|
|
416
|
+
if (collections && collectionsPersistence) {
|
|
417
|
+
collectionsPersistence.saveSessions(collections.getSessions());
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
onActionChange: (action) => {
|
|
421
|
+
collectionsPersistence?.queueAction(action);
|
|
422
|
+
},
|
|
423
|
+
onExecChange: (exec) => {
|
|
424
|
+
// Exec changes tracked via hooks
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
// ── runId → real session key map ─────────────────────────────────
|
|
428
|
+
// Chat events always carry the real sessionKey (agent:agentId:platform:recipient).
|
|
429
|
+
// Agent events often omit it, falling back to stream name ("assistant", "lifecycle").
|
|
430
|
+
// We build this map from chat events and use it to normalize agent events.
|
|
431
|
+
const runSessionMap = new Map();
|
|
432
|
+
const isRealSessionKey = (k) => k.split(":")[0] === "agent" && k.split(":").length >= 3;
|
|
433
|
+
// ── Bridge 4: Gateway WebSocket → engine + collections ─────────────────
|
|
434
|
+
// Connects to gateway WS for real-time session/action/exec tracking.
|
|
435
|
+
// Falls back gracefully if not paired (NOT_PAIRED error is handled).
|
|
436
|
+
// Use token from config or fall back to empty (will show pairing warning)
|
|
437
|
+
// Bug 4 fix: read token from config instead of hardcoding
|
|
438
|
+
const _gatewayCfg = api.config.gateway;
|
|
439
|
+
const _gatewayAuth = _gatewayCfg?.auth;
|
|
440
|
+
const gatewayToken = _gatewayAuth?.token || "";
|
|
441
|
+
if (gatewayToken) {
|
|
442
|
+
gatewayClient = new GatewayClient({
|
|
443
|
+
token: gatewayToken,
|
|
444
|
+
});
|
|
445
|
+
gatewayClient.on("ready", () => {
|
|
446
|
+
logger.info(`${LOG_PREFIX}: gateway client connected`);
|
|
447
|
+
});
|
|
448
|
+
gatewayClient.on("error", (err) => {
|
|
449
|
+
// Ignore pairing errors - plugin works without full gateway client
|
|
450
|
+
if (err.message.includes("NOT_PAIRED") ||
|
|
451
|
+
err.message.includes("device identity")) {
|
|
452
|
+
logger.warn(`${LOG_PREFIX}: gateway pairing not configured (optional)`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
logger.warn(`${LOG_PREFIX}: gateway client error: ${err.message}`);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
gatewayClient.on("disconnected", () => {
|
|
459
|
+
logger.info(`${LOG_PREFIX}: gateway client disconnected`);
|
|
460
|
+
});
|
|
461
|
+
// Wire gateway events to collections
|
|
462
|
+
const gatewayEventNames = ["chat", "agent", "health", "tick"];
|
|
463
|
+
for (const eventName of gatewayEventNames) {
|
|
464
|
+
gatewayClient.on(eventName, (payload) => {
|
|
465
|
+
// Record for test endpoint
|
|
466
|
+
recordGatewayEvent(eventName, payload);
|
|
467
|
+
if (collections) {
|
|
468
|
+
const parsed = parseGatewayEvent(eventName, payload);
|
|
469
|
+
if (parsed) {
|
|
470
|
+
if (parsed.session)
|
|
471
|
+
collections.upsertSession(parsed.session);
|
|
472
|
+
if (parsed.action) {
|
|
473
|
+
// Register real keys in runSessionMap
|
|
474
|
+
if (parsed.action.sessionKey &&
|
|
475
|
+
!parsed.action.sessionKey.includes("lifecycle") &&
|
|
476
|
+
isRealSessionKey(parsed.action.sessionKey)) {
|
|
477
|
+
runSessionMap.set(parsed.action.runId, parsed.action.sessionKey);
|
|
478
|
+
}
|
|
479
|
+
// Route to real key via runId; fall back to original key — never filter (frontend handles grouping)
|
|
480
|
+
const realKey = isRealSessionKey(parsed.action.sessionKey)
|
|
481
|
+
? parsed.action.sessionKey
|
|
482
|
+
: runSessionMap.get(parsed.action.runId) ||
|
|
483
|
+
parsed.action.sessionKey;
|
|
484
|
+
collections.addAction(parsed.action);
|
|
485
|
+
// Forward parsed WS actions to the live agent monitor SSE stream
|
|
486
|
+
const action = parsed.action;
|
|
487
|
+
const sseType = action.eventType === "chat" ? "chat" : "agent";
|
|
488
|
+
const sseData = {
|
|
489
|
+
ts: action.timestamp,
|
|
490
|
+
sessionKey: realKey,
|
|
491
|
+
runId: action.runId,
|
|
492
|
+
};
|
|
493
|
+
if (action.eventType === "chat") {
|
|
494
|
+
const state = action.type === "complete"
|
|
495
|
+
? "final"
|
|
496
|
+
: action.type === "streaming"
|
|
497
|
+
? "delta"
|
|
498
|
+
: action.type === "error"
|
|
499
|
+
? "error"
|
|
500
|
+
: "final";
|
|
501
|
+
sseData.data = {
|
|
502
|
+
state,
|
|
503
|
+
content: action.content,
|
|
504
|
+
inputTokens: action.inputTokens,
|
|
505
|
+
outputTokens: action.outputTokens,
|
|
506
|
+
stopReason: action.stopReason,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
const phase = action.type === "start"
|
|
511
|
+
? "start"
|
|
512
|
+
: action.type === "complete"
|
|
513
|
+
? "end"
|
|
514
|
+
: action.type === "error"
|
|
515
|
+
? "error"
|
|
516
|
+
: action.type === "tool_call"
|
|
517
|
+
? "tool"
|
|
518
|
+
: action.type === "streaming"
|
|
519
|
+
? "streaming"
|
|
520
|
+
: action.type;
|
|
521
|
+
sseData.data = {
|
|
522
|
+
phase,
|
|
523
|
+
content: action.content,
|
|
524
|
+
toolName: action.toolName,
|
|
525
|
+
toolArgs: action.toolArgs,
|
|
526
|
+
durationMs: action.endedAt && action.startedAt
|
|
527
|
+
? action.endedAt - action.startedAt
|
|
528
|
+
: undefined,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
emitAgentMonitorEvent({ type: sseType, data: sseData });
|
|
532
|
+
}
|
|
533
|
+
if (parsed.execEvent) {
|
|
534
|
+
collections.addExecEvent(parsed.execEvent);
|
|
535
|
+
collectionsPersistence?.queueExecEvent(parsed.execEvent);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
gatewayClient.start();
|
|
542
|
+
// Bug 1 fix: cost sync via usage.cost RPC removed
|
|
543
|
+
// Gateway denies operator.read scope; cost flows via model.usage diagnostic events
|
|
544
|
+
}
|
|
174
545
|
const targetDesc = target
|
|
175
546
|
? `alerting to ${target.channel}:${target.to}`
|
|
176
547
|
: "log-only (no alert channel detected)";
|
|
177
|
-
logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active,
|
|
548
|
+
logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active, ${ALL_RULES.length} rules active`);
|
|
178
549
|
},
|
|
179
550
|
stop() {
|
|
180
551
|
closeDashboardConnections();
|
|
552
|
+
if (sessionSyncInterval) {
|
|
553
|
+
clearInterval(sessionSyncInterval);
|
|
554
|
+
sessionSyncInterval = null;
|
|
555
|
+
}
|
|
556
|
+
if (gatewayClient) {
|
|
557
|
+
gatewayClient.stop();
|
|
558
|
+
gatewayClient = null;
|
|
559
|
+
}
|
|
560
|
+
if (collectionsPersistence) {
|
|
561
|
+
collectionsPersistence.stop();
|
|
562
|
+
collectionsPersistence = null;
|
|
563
|
+
}
|
|
564
|
+
if (collections) {
|
|
565
|
+
collections.clear();
|
|
566
|
+
collections = null;
|
|
567
|
+
}
|
|
181
568
|
if (unsubLogTransport) {
|
|
182
569
|
unsubLogTransport();
|
|
183
570
|
unsubLogTransport = null;
|
|
@@ -207,7 +594,7 @@ const plugin = {
|
|
|
207
594
|
api.registerCommand(cmd);
|
|
208
595
|
}
|
|
209
596
|
// Register dashboard HTTP routes under /openalerts*
|
|
210
|
-
api.registerHttpHandler(createDashboardHandler(() => engine));
|
|
597
|
+
api.registerHttpHandler(createDashboardHandler(() => engine, () => collections));
|
|
211
598
|
},
|
|
212
599
|
};
|
|
213
600
|
export default plugin;
|
|
@@ -194,6 +194,28 @@ export function getDashboardHtml() {
|
|
|
194
194
|
.log-detail .ld-grid .lk{color:#8b949e;text-align:right}
|
|
195
195
|
.log-detail .ld-grid .lv{color:#c9d1d9;word-break:break-all}
|
|
196
196
|
.log-detail .ld-file{color:#484f58;font-size:10px;margin-top:3px}
|
|
197
|
+
|
|
198
|
+
/* ── Graph View ──────────────────── */
|
|
199
|
+
.view-toggle{display:flex;gap:2px;background:#0d1117;border:1px solid #30363d;border-radius:4px;padding:2px;font-size:11px}
|
|
200
|
+
.view-toggle button{background:transparent;border:none;color:#8b949e;padding:3px 10px;border-radius:3px;cursor:pointer;font-family:inherit;font-size:11px}
|
|
201
|
+
.view-toggle button:hover{color:#c9d1d9;background:#21262d}
|
|
202
|
+
.view-toggle button.active{background:#21262d;color:#58a6ff}
|
|
203
|
+
#graphView{display:none;flex:1;overflow:auto;position:relative;background:#0d1117}
|
|
204
|
+
#graphView.active{display:block}
|
|
205
|
+
.graph-svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
|
|
206
|
+
.graph-node{position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#161b22;border:2px solid #30363d;border-radius:8px;padding:8px 12px;min-width:60px;cursor:pointer;transition:all 0.15s}
|
|
207
|
+
.graph-node:hover{border-color:#58a6ff;background:#1c2129}
|
|
208
|
+
.graph-node.session{border-color:#58a6ff;background:#0d1a2d}
|
|
209
|
+
.graph-node.action{border-radius:6px;padding:6px 10px}
|
|
210
|
+
.graph-node.tool{border-color:#bc8cff;background:#1a0d2d;border-radius:4px;padding:4px 8px;font-size:11px}
|
|
211
|
+
.graph-node.exec{border-color:#d29922;background:#2d1a0d;border-radius:4px;padding:4px 8px;font-size:11px}
|
|
212
|
+
.node-icon{font-size:20px;margin-bottom:2px}
|
|
213
|
+
.node-label{font-size:11px;color:#c9d1d9;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center}
|
|
214
|
+
.node-status{font-size:9px;color:#8b949e;margin-top:2px}
|
|
215
|
+
.node-status.running{color:#58a6ff}
|
|
216
|
+
.node-status.completed{color:#3fb950}
|
|
217
|
+
.node-status.error{color:#f85149}
|
|
218
|
+
|
|
197
219
|
</style>
|
|
198
220
|
</head>
|
|
199
221
|
<body>
|
|
@@ -218,7 +240,10 @@ export function getDashboardHtml() {
|
|
|
218
240
|
<div class="tab-content active" id="tab-activity">
|
|
219
241
|
<div class="activity-panels">
|
|
220
242
|
<div class="panel">
|
|
221
|
-
<div class="panel-header"
|
|
243
|
+
<div class="panel-header">
|
|
244
|
+
<span>Live Timeline</span>
|
|
245
|
+
<span style="color:#484f58;font-weight:400" id="evCnt">0</span>
|
|
246
|
+
</div>
|
|
222
247
|
<div class="scroll" id="evList"><div class="empty-msg" id="emptyMsg">Waiting for events... send a message to your bot.</div></div>
|
|
223
248
|
</div>
|
|
224
249
|
<div class="panel">
|
|
@@ -626,8 +651,13 @@ export function getDashboardHtml() {
|
|
|
626
651
|
evSrc.addEventListener('history',function(e){try{var evs=JSON.parse(e.data);for(var i=0;i<evs.length;i++)addEvent(evs[i])}catch(_){}});
|
|
627
652
|
evSrc.addEventListener('oclog',function(e){try{addLogEntry(JSON.parse(e.data))}catch(_){}});
|
|
628
653
|
evSrc.onopen=function(){$('sDot').className='dot live';$('sConn').textContent='live'};
|
|
629
|
-
evSrc.onerror=function(e){
|
|
630
|
-
|
|
654
|
+
evSrc.onerror=function(e){
|
|
655
|
+
$('sDot').className='dot dead';
|
|
656
|
+
$('sConn').textContent='reconnecting...';
|
|
657
|
+
evSrc.close();
|
|
658
|
+
setTimeout(connectSSE,3000);
|
|
659
|
+
};
|
|
660
|
+
}catch(e){$('sConn').textContent='SSE fail:'+e.message;setTimeout(connectSSE,3000);}
|
|
631
661
|
}
|
|
632
662
|
|
|
633
663
|
// ─── State polling ──────────────────────
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { OpenAlertsEngine } from "../core/index.js";
|
|
3
3
|
type HttpHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean> | boolean;
|
|
4
|
-
|
|
4
|
+
type AgentMonitorEvent = {
|
|
5
|
+
type: string;
|
|
6
|
+
data: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare function emitAgentMonitorEvent(event: AgentMonitorEvent): void;
|
|
9
|
+
export declare function recordGatewayEvent(eventName: string, payload: unknown): void;
|
|
5
10
|
export declare function closeDashboardConnections(): void;
|
|
6
|
-
export declare function createDashboardHandler(getEngine: () => OpenAlertsEngine | null): HttpHandler;
|
|
11
|
+
export declare function createDashboardHandler(getEngine: () => OpenAlertsEngine | null, getCollections: () => unknown): HttpHandler;
|
|
7
12
|
export {};
|