@steadwing/openalerts 0.2.5 → 0.2.6

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.
Files changed (134) hide show
  1. package/README.md +198 -141
  2. package/dist/channels/console.d.ts +6 -0
  3. package/dist/channels/console.d.ts.map +1 -0
  4. package/dist/channels/console.js +10 -0
  5. package/dist/channels/console.js.map +1 -0
  6. package/dist/channels/telegram.d.ts +12 -0
  7. package/dist/channels/telegram.d.ts.map +1 -0
  8. package/dist/channels/telegram.js +28 -0
  9. package/dist/channels/telegram.js.map +1 -0
  10. package/dist/channels/webhook.d.ts +8 -0
  11. package/dist/channels/webhook.d.ts.map +1 -0
  12. package/dist/channels/webhook.js +15 -0
  13. package/dist/channels/webhook.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +234 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +51 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +86 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/core/alert-channel.d.ts +3 -10
  23. package/dist/core/alert-channel.d.ts.map +1 -0
  24. package/dist/core/alert-channel.js +9 -30
  25. package/dist/core/alert-channel.js.map +1 -0
  26. package/dist/core/bounded-map.d.ts +1 -0
  27. package/dist/core/bounded-map.d.ts.map +1 -0
  28. package/dist/core/bounded-map.js +1 -0
  29. package/dist/core/bounded-map.js.map +1 -0
  30. package/dist/core/engine.d.ts +6 -18
  31. package/dist/core/engine.d.ts.map +1 -0
  32. package/dist/core/engine.js +48 -98
  33. package/dist/core/engine.js.map +1 -0
  34. package/dist/core/evaluator.d.ts +1 -0
  35. package/dist/core/evaluator.d.ts.map +1 -0
  36. package/dist/core/evaluator.js +1 -0
  37. package/dist/core/evaluator.js.map +1 -0
  38. package/dist/core/event-bus.d.ts +1 -0
  39. package/dist/core/event-bus.d.ts.map +1 -0
  40. package/dist/core/event-bus.js +1 -0
  41. package/dist/core/event-bus.js.map +1 -0
  42. package/dist/core/formatter.d.ts +1 -0
  43. package/dist/core/formatter.d.ts.map +1 -0
  44. package/dist/core/formatter.js +1 -0
  45. package/dist/core/formatter.js.map +1 -0
  46. package/dist/core/rules.d.ts +1 -0
  47. package/dist/core/rules.d.ts.map +1 -0
  48. package/dist/core/rules.js +1 -0
  49. package/dist/core/rules.js.map +1 -0
  50. package/dist/core/store.d.ts +6 -9
  51. package/dist/core/store.d.ts.map +1 -0
  52. package/dist/core/store.js +43 -96
  53. package/dist/core/store.js.map +1 -0
  54. package/dist/core/types.d.ts +1 -0
  55. package/dist/core/types.d.ts.map +1 -0
  56. package/dist/core/types.js +1 -0
  57. package/dist/core/types.js.map +1 -0
  58. package/dist/db/index.d.ts +6 -0
  59. package/dist/db/index.d.ts.map +1 -0
  60. package/dist/db/index.js +31 -0
  61. package/dist/db/index.js.map +1 -0
  62. package/dist/db/queries.d.ts +157 -0
  63. package/dist/db/queries.d.ts.map +1 -0
  64. package/dist/db/queries.js +221 -0
  65. package/dist/db/queries.js.map +1 -0
  66. package/dist/db/schema.d.ts +5 -0
  67. package/dist/db/schema.d.ts.map +1 -0
  68. package/dist/db/schema.js +177 -0
  69. package/dist/db/schema.js.map +1 -0
  70. package/dist/readers/openclaw.d.ts +11 -0
  71. package/dist/readers/openclaw.d.ts.map +1 -0
  72. package/dist/readers/openclaw.js +267 -0
  73. package/dist/readers/openclaw.js.map +1 -0
  74. package/dist/server/dashboard.d.ts +2 -0
  75. package/dist/server/dashboard.d.ts.map +1 -0
  76. package/dist/server/dashboard.js +765 -0
  77. package/dist/server/dashboard.js.map +1 -0
  78. package/dist/server/index.d.ts +10 -0
  79. package/dist/server/index.d.ts.map +1 -0
  80. package/dist/server/index.js +28 -0
  81. package/dist/server/index.js.map +1 -0
  82. package/dist/server/routes.d.ts +6 -0
  83. package/dist/server/routes.d.ts.map +1 -0
  84. package/dist/server/routes.js +146 -0
  85. package/dist/server/routes.js.map +1 -0
  86. package/dist/server/sse.d.ts +21 -0
  87. package/dist/server/sse.d.ts.map +1 -0
  88. package/dist/server/sse.js +53 -0
  89. package/dist/server/sse.js.map +1 -0
  90. package/dist/watchers/files.d.ts +19 -0
  91. package/dist/watchers/files.d.ts.map +1 -0
  92. package/dist/watchers/files.js +105 -0
  93. package/dist/watchers/files.js.map +1 -0
  94. package/dist/watchers/gateway-adapter.d.ts +18 -0
  95. package/dist/watchers/gateway-adapter.d.ts.map +1 -0
  96. package/dist/watchers/gateway-adapter.js +273 -0
  97. package/dist/watchers/gateway-adapter.js.map +1 -0
  98. package/dist/watchers/gateway.d.ts +27 -0
  99. package/dist/watchers/gateway.d.ts.map +1 -0
  100. package/dist/watchers/gateway.js +131 -0
  101. package/dist/watchers/gateway.js.map +1 -0
  102. package/package.json +29 -43
  103. package/LICENSE +0 -201
  104. package/dist/collections/collection-manager.d.ts +0 -50
  105. package/dist/collections/collection-manager.js +0 -583
  106. package/dist/collections/event-parser.d.ts +0 -27
  107. package/dist/collections/event-parser.js +0 -321
  108. package/dist/collections/index.d.ts +0 -6
  109. package/dist/collections/index.js +0 -6
  110. package/dist/collections/persistence.d.ts +0 -25
  111. package/dist/collections/persistence.js +0 -213
  112. package/dist/collections/types.d.ts +0 -177
  113. package/dist/collections/types.js +0 -15
  114. package/dist/core/index.d.ts +0 -13
  115. package/dist/core/index.js +0 -23
  116. package/dist/core/llm-enrichment.d.ts +0 -21
  117. package/dist/core/llm-enrichment.js +0 -180
  118. package/dist/core/platform.d.ts +0 -17
  119. package/dist/core/platform.js +0 -93
  120. package/dist/index.d.ts +0 -8
  121. package/dist/index.js +0 -600
  122. package/dist/plugin/adapter.d.ts +0 -150
  123. package/dist/plugin/adapter.js +0 -530
  124. package/dist/plugin/commands.d.ts +0 -18
  125. package/dist/plugin/commands.js +0 -103
  126. package/dist/plugin/dashboard-html.d.ts +0 -7
  127. package/dist/plugin/dashboard-html.js +0 -968
  128. package/dist/plugin/dashboard-routes.d.ts +0 -12
  129. package/dist/plugin/dashboard-routes.js +0 -444
  130. package/dist/plugin/gateway-client.d.ts +0 -39
  131. package/dist/plugin/gateway-client.js +0 -200
  132. package/dist/plugin/log-bridge.d.ts +0 -22
  133. package/dist/plugin/log-bridge.js +0 -363
  134. package/openclaw.plugin.json +0 -87
package/dist/index.js DELETED
@@ -1,600 +0,0 @@
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;