@steadwing/openalerts 0.2.6 → 0.2.7
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/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/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 -10
package/dist/index.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { ALL_RULES, OpenAlertsEngine } from "./core/index.js";
|
|
2
|
+
import { onDiagnosticEvent, registerLogTransport } from "openclaw/plugin-sdk";
|
|
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";
|
|
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";
|
|
9
|
+
import { bindEngine, createMonitorCommands } from "./plugin/commands.js";
|
|
10
|
+
import { createDashboardHandler, closeDashboardConnections, emitAgentMonitorEvent, recordGatewayEvent, } from "./plugin/dashboard-routes.js";
|
|
11
|
+
import { CollectionManager, CollectionPersistence, parseGatewayEvent, diagnosticUsageToSessionUpdate, } from "./collections/index.js";
|
|
12
|
+
const PLUGIN_ID = "openalerts";
|
|
13
|
+
const LOG_PREFIX = "openalerts";
|
|
14
|
+
let engine = null;
|
|
15
|
+
let unsubDiagnostic = null;
|
|
16
|
+
let unsubLogTransport = null;
|
|
17
|
+
let logBridgeCleanup = null;
|
|
18
|
+
let gatewayClient = null;
|
|
19
|
+
let collections = null;
|
|
20
|
+
let collectionsPersistence = null;
|
|
21
|
+
let sessionSyncInterval = null;
|
|
22
|
+
function createMonitorService(api) {
|
|
23
|
+
return {
|
|
24
|
+
id: PLUGIN_ID,
|
|
25
|
+
async start(ctx) {
|
|
26
|
+
const logger = ctx.logger;
|
|
27
|
+
const config = parseConfig(api.pluginConfig);
|
|
28
|
+
// Resolve alert target + create OpenClaw alert channel
|
|
29
|
+
const target = await resolveAlertTarget(api, config);
|
|
30
|
+
const channels = target ? [new OpenClawAlertChannel(api, target)] : [];
|
|
31
|
+
// Create LLM enricher if enabled (default: false)
|
|
32
|
+
const enricher = config.llmEnriched === true
|
|
33
|
+
? createOpenClawEnricher(api, logger)
|
|
34
|
+
: null;
|
|
35
|
+
// Create and start the universal engine
|
|
36
|
+
engine = new OpenAlertsEngine({
|
|
37
|
+
stateDir: ctx.stateDir,
|
|
38
|
+
config,
|
|
39
|
+
channels,
|
|
40
|
+
logger,
|
|
41
|
+
logPrefix: LOG_PREFIX,
|
|
42
|
+
diagnosisHint: 'Run "openclaw doctor" to diagnose.',
|
|
43
|
+
enricher: enricher ?? undefined,
|
|
44
|
+
});
|
|
45
|
+
engine.start();
|
|
46
|
+
// Wire commands to engine
|
|
47
|
+
bindEngine(engine, api);
|
|
48
|
+
// ── Bridge 1: Diagnostic events → engine + collections ───────────────────
|
|
49
|
+
// Covers: webhook.*, message.*, session.stuck, session.state,
|
|
50
|
+
// model.usage, queue.lane.*, diagnostic.heartbeat, run.attempt
|
|
51
|
+
unsubDiagnostic = onDiagnosticEvent((event) => {
|
|
52
|
+
const translated = translateOpenClawEvent(event);
|
|
53
|
+
if (translated) {
|
|
54
|
+
engine.ingest(translated);
|
|
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
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// ── Bridge 2: Log transport → engine (fills gaps from non-firing hooks) ─
|
|
69
|
+
// Parses structured log records to synthesize tool.call, session.start/end,
|
|
70
|
+
// and run duration events that hooks don't provide in long-polling mode.
|
|
71
|
+
const logBridge = createLogBridge(engine);
|
|
72
|
+
unsubLogTransport = registerLogTransport(logBridge.transport);
|
|
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
|
+
};
|
|
95
|
+
// ── Bridge 3: Plugin hooks → engine ───────────────────────────────────
|
|
96
|
+
// Covers: tool calls, agent lifecycle, session lifecycle, gateway, messages
|
|
97
|
+
// These events are NOT emitted as diagnostic events — they come through
|
|
98
|
+
// the plugin hook system (api.on).
|
|
99
|
+
const apiOn = api.on?.bind(api);
|
|
100
|
+
if (apiOn) {
|
|
101
|
+
// Tool execution tracking
|
|
102
|
+
apiOn("after_tool_call", (data, hookCtx) => {
|
|
103
|
+
if (!engine)
|
|
104
|
+
return;
|
|
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
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// Agent lifecycle
|
|
142
|
+
apiOn("before_agent_start", (data, hookCtx) => {
|
|
143
|
+
if (!engine)
|
|
144
|
+
return;
|
|
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
|
+
}
|
|
179
|
+
});
|
|
180
|
+
apiOn("agent_end", (data, hookCtx) => {
|
|
181
|
+
if (!engine)
|
|
182
|
+
return;
|
|
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
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// Session lifecycle
|
|
212
|
+
apiOn("session_start", (data, hookCtx) => {
|
|
213
|
+
if (!engine)
|
|
214
|
+
return;
|
|
215
|
+
engine.ingest(translateSessionStartHook(data));
|
|
216
|
+
// Track in collections
|
|
217
|
+
const ctx = hookCtx;
|
|
218
|
+
trackAction("start", "system", { sessionId: data.sessionId }, ctx);
|
|
219
|
+
});
|
|
220
|
+
apiOn("session_end", (data, hookCtx) => {
|
|
221
|
+
if (!engine)
|
|
222
|
+
return;
|
|
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);
|
|
230
|
+
});
|
|
231
|
+
// Message delivery tracking (all messages — success and failure)
|
|
232
|
+
apiOn("message_sent", (data, hookCtx) => {
|
|
233
|
+
if (!engine)
|
|
234
|
+
return;
|
|
235
|
+
const d = data;
|
|
236
|
+
engine.ingest(translateMessageSentHook(d, {
|
|
237
|
+
channel: hookCtx.channel,
|
|
238
|
+
sessionId: hookCtx.sessionId,
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
241
|
+
// Inbound message tracking (fires reliably in all modes)
|
|
242
|
+
// Inbound message tracking (fires reliably in all modes)
|
|
243
|
+
apiOn("message_received", (data, hookCtx) => {
|
|
244
|
+
if (!engine)
|
|
245
|
+
return;
|
|
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
|
+
});
|
|
272
|
+
});
|
|
273
|
+
// Tool start tracking (fires reliably — complements log-bridge tool end)
|
|
274
|
+
apiOn("before_tool_call", (data, hookCtx) => {
|
|
275
|
+
if (!engine)
|
|
276
|
+
return;
|
|
277
|
+
engine.ingest(translateBeforeToolCallHook(data, {
|
|
278
|
+
sessionId: hookCtx.sessionId,
|
|
279
|
+
agentId: hookCtx.agentId,
|
|
280
|
+
}));
|
|
281
|
+
});
|
|
282
|
+
// Tool result persistence tracking
|
|
283
|
+
apiOn("tool_result_persist", (data, hookCtx) => {
|
|
284
|
+
if (!engine)
|
|
285
|
+
return;
|
|
286
|
+
engine.ingest(translateToolResultPersistHook(data, {
|
|
287
|
+
sessionKey: hookCtx.sessionKey,
|
|
288
|
+
agentId: hookCtx.agentId,
|
|
289
|
+
toolName: hookCtx.toolName,
|
|
290
|
+
}));
|
|
291
|
+
});
|
|
292
|
+
// Compaction lifecycle (before + after)
|
|
293
|
+
apiOn("before_compaction", (data, hookCtx) => {
|
|
294
|
+
if (!engine)
|
|
295
|
+
return;
|
|
296
|
+
engine.ingest(translateBeforeCompactionHook(data, {
|
|
297
|
+
sessionKey: hookCtx.sessionKey,
|
|
298
|
+
agentId: hookCtx.agentId,
|
|
299
|
+
}));
|
|
300
|
+
});
|
|
301
|
+
apiOn("after_compaction", (data, hookCtx) => {
|
|
302
|
+
if (!engine)
|
|
303
|
+
return;
|
|
304
|
+
engine.ingest(translateAfterCompactionHook(data, {
|
|
305
|
+
sessionKey: hookCtx.sessionKey,
|
|
306
|
+
agentId: hookCtx.agentId,
|
|
307
|
+
}));
|
|
308
|
+
});
|
|
309
|
+
// Pre-send message tracking (fires before message_sent)
|
|
310
|
+
apiOn("message_sending", (data, hookCtx) => {
|
|
311
|
+
if (!engine)
|
|
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
|
+
}
|
|
332
|
+
engine.ingest(translateMessageSendingHook(data, {
|
|
333
|
+
channelId: hookCtx.channelId,
|
|
334
|
+
accountId: hookCtx.accountId,
|
|
335
|
+
}));
|
|
336
|
+
});
|
|
337
|
+
// Gateway lifecycle
|
|
338
|
+
apiOn("gateway_start", (data) => {
|
|
339
|
+
if (!engine)
|
|
340
|
+
return;
|
|
341
|
+
engine.ingest(translateGatewayStartHook(data));
|
|
342
|
+
});
|
|
343
|
+
apiOn("gateway_stop", (data) => {
|
|
344
|
+
if (!engine)
|
|
345
|
+
return;
|
|
346
|
+
engine.ingest(translateGatewayStopHook(data));
|
|
347
|
+
});
|
|
348
|
+
logger.info(`${LOG_PREFIX}: subscribed to 13 plugin hooks (100% coverage: tool, agent, session, gateway, message, compaction)`);
|
|
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
|
+
}
|
|
545
|
+
const targetDesc = target
|
|
546
|
+
? `alerting to ${target.channel}:${target.to}`
|
|
547
|
+
: "log-only (no alert channel detected)";
|
|
548
|
+
logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active, ${ALL_RULES.length} rules active`);
|
|
549
|
+
},
|
|
550
|
+
stop() {
|
|
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
|
+
}
|
|
568
|
+
if (unsubLogTransport) {
|
|
569
|
+
unsubLogTransport();
|
|
570
|
+
unsubLogTransport = null;
|
|
571
|
+
}
|
|
572
|
+
if (logBridgeCleanup) {
|
|
573
|
+
logBridgeCleanup();
|
|
574
|
+
logBridgeCleanup = null;
|
|
575
|
+
}
|
|
576
|
+
if (unsubDiagnostic) {
|
|
577
|
+
unsubDiagnostic();
|
|
578
|
+
unsubDiagnostic = null;
|
|
579
|
+
}
|
|
580
|
+
if (engine) {
|
|
581
|
+
engine.stop();
|
|
582
|
+
engine = null;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const plugin = {
|
|
588
|
+
id: PLUGIN_ID,
|
|
589
|
+
name: "OpenAlerts",
|
|
590
|
+
description: "Alerting & monitoring — texts you when your bot is sick",
|
|
591
|
+
register(api) {
|
|
592
|
+
api.registerService(createMonitorService(api));
|
|
593
|
+
for (const cmd of createMonitorCommands(api)) {
|
|
594
|
+
api.registerCommand(cmd);
|
|
595
|
+
}
|
|
596
|
+
// Register dashboard HTTP routes under /openalerts*
|
|
597
|
+
api.registerHttpHandler(createDashboardHandler(() => engine, () => collections));
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
export default plugin;
|