@steadwing/openalerts 0.2.1 → 0.2.3

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.
@@ -32,7 +32,7 @@ function isRuleEnabled(ctx, ruleId) {
32
32
  const infraErrors = {
33
33
  id: "infra-errors",
34
34
  defaultCooldownMs: 15 * 60 * 1000,
35
- defaultThreshold: 3,
35
+ defaultThreshold: 1,
36
36
  evaluate(event, ctx) {
37
37
  if (event.type !== "infra.error")
38
38
  return null;
@@ -40,8 +40,8 @@ const infraErrors = {
40
40
  return null;
41
41
  const channel = event.channel ?? "unknown";
42
42
  pushWindow(ctx, "infra-errors", { ts: ctx.now });
43
- const threshold = getRuleThreshold(ctx, "infra-errors", 3);
44
- const windowMs = 5 * 60 * 1000; // 5 minutes
43
+ const threshold = getRuleThreshold(ctx, "infra-errors", 1);
44
+ const windowMs = 60 * 1000; // 1 minute
45
45
  const count = countInWindow(ctx, "infra-errors", windowMs);
46
46
  if (count < threshold)
47
47
  return null;
@@ -52,7 +52,7 @@ const infraErrors = {
52
52
  ruleId: "infra-errors",
53
53
  severity: "error",
54
54
  title: "Infrastructure errors spike",
55
- detail: `${count} infra errors on ${channel} in the last 5 minutes.`,
55
+ detail: `${count} infra error(s) on ${channel} in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
56
56
  ts: ctx.now,
57
57
  fingerprint,
58
58
  };
@@ -62,32 +62,37 @@ const infraErrors = {
62
62
  const llmErrors = {
63
63
  id: "llm-errors",
64
64
  defaultCooldownMs: 15 * 60 * 1000,
65
- defaultThreshold: 3,
65
+ defaultThreshold: 1,
66
66
  evaluate(event, ctx) {
67
- if (event.type !== "llm.call")
67
+ // Trigger on LLM call/error events AND agent errors (agent failing before/during LLM call)
68
+ if (event.type !== "llm.call" && event.type !== "llm.error" && event.type !== "agent.error")
68
69
  return null;
69
70
  if (!isRuleEnabled(ctx, "llm-errors"))
70
71
  return null;
71
- // Track all LLM calls for stats
72
- ctx.state.stats.messagesProcessed++;
73
- if (event.outcome !== "error")
74
- return null;
75
- ctx.state.stats.messageErrors++;
72
+ // Stats are tracked in the evaluator (independent of rule state).
73
+ // Only proceed for actual errors:
74
+ if (event.type === "llm.call") {
75
+ // Only explicit error/timeout outcomes trigger alerting; undefined = OK
76
+ if (event.outcome !== "error" && event.outcome !== "timeout")
77
+ return null;
78
+ }
79
+ // llm.error and agent.error are always errors — no outcome check needed
76
80
  const channel = event.channel ?? "unknown";
77
81
  pushWindow(ctx, "llm-errors", { ts: ctx.now });
78
- const threshold = getRuleThreshold(ctx, "llm-errors", 3);
79
- const windowMs = 5 * 60 * 1000;
82
+ const threshold = getRuleThreshold(ctx, "llm-errors", 1);
83
+ const windowMs = 60 * 1000; // 1 minute
80
84
  const count = countInWindow(ctx, "llm-errors", windowMs);
81
85
  if (count < threshold)
82
86
  return null;
83
87
  const fingerprint = `llm-errors:${channel}`;
88
+ const label = event.type === "agent.error" ? "agent error(s)" : "LLM error(s)";
84
89
  return {
85
90
  type: "alert",
86
91
  id: makeAlertId("llm-errors", fingerprint, ctx.now),
87
92
  ruleId: "llm-errors",
88
93
  severity: "error",
89
94
  title: "LLM call errors",
90
- detail: `${count} LLM errors on ${channel} in the last 5 minutes.`,
95
+ detail: `${count} ${label} on ${channel} in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
91
96
  ts: ctx.now,
92
97
  fingerprint,
93
98
  };
@@ -103,7 +108,7 @@ const sessionStuck = {
103
108
  return null;
104
109
  if (!isRuleEnabled(ctx, "session-stuck"))
105
110
  return null;
106
- ctx.state.stats.stuckSessions++;
111
+ // Stats tracked in evaluator (independent of rule state)
107
112
  const ageMs = event.ageMs ?? 0;
108
113
  const threshold = getRuleThreshold(ctx, "session-stuck", 120_000);
109
114
  if (ageMs < threshold)
@@ -153,10 +158,8 @@ const heartbeatFail = {
153
158
  fingerprint,
154
159
  };
155
160
  }
156
- // Reset on success
157
- if (event.outcome === "success") {
158
- ctx.state.consecutives.set(counterKey, 0);
159
- }
161
+ // Reset on any non-error (success, undefined, etc.)
162
+ ctx.state.consecutives.set(counterKey, 0);
160
163
  return null;
161
164
  },
162
165
  };
@@ -169,12 +172,12 @@ const queueDepth = {
169
172
  // Fire on heartbeat (which carries queue depth) and dedicated queue_depth events
170
173
  if (event.type !== "infra.heartbeat" && event.type !== "infra.queue_depth")
171
174
  return null;
172
- if (!isRuleEnabled(ctx, "queue-depth"))
173
- return null;
174
- // Update last heartbeat timestamp (used by gateway-down rule)
175
+ // Always update heartbeat timestamp regardless of rule state (gateway-down depends on it)
175
176
  if (event.type === "infra.heartbeat") {
176
177
  ctx.state.lastHeartbeatTs = ctx.now;
177
178
  }
179
+ if (!isRuleEnabled(ctx, "queue-depth"))
180
+ return null;
178
181
  const queued = event.queueDepth ?? 0;
179
182
  const threshold = getRuleThreshold(ctx, "queue-depth", 10);
180
183
  if (queued < threshold)
@@ -198,11 +201,15 @@ const highErrorRate = {
198
201
  defaultCooldownMs: 30 * 60 * 1000,
199
202
  defaultThreshold: 50, // percent
200
203
  evaluate(event, ctx) {
201
- if (event.type !== "llm.call")
204
+ if (event.type !== "llm.call" && event.type !== "llm.error" && event.type !== "agent.error")
202
205
  return null;
203
206
  if (!isRuleEnabled(ctx, "high-error-rate"))
204
207
  return null;
205
- const isError = event.outcome === "error";
208
+ // agent.error and llm.error are always errors; llm.call checks outcome (timeout counts as error)
209
+ const isError = event.type === "agent.error" ||
210
+ event.type === "llm.error" ||
211
+ event.outcome === "error" ||
212
+ event.outcome === "timeout";
206
213
  pushWindow(ctx, "msg-outcomes", { ts: ctx.now, value: isError ? 1 : 0 });
207
214
  const window = ctx.state.windows.get("msg-outcomes");
208
215
  if (!window || window.length < 20)
@@ -227,11 +234,41 @@ const highErrorRate = {
227
234
  };
228
235
  },
229
236
  };
237
+ // ─── Rule: tool-errors ───────────────────────────────────────────────────
238
+ const toolErrors = {
239
+ id: "tool-errors",
240
+ defaultCooldownMs: 15 * 60 * 1000,
241
+ defaultThreshold: 1, // 1 tool error in 1 minute
242
+ evaluate(event, ctx) {
243
+ if (event.type !== "tool.error")
244
+ return null;
245
+ if (!isRuleEnabled(ctx, "tool-errors"))
246
+ return null;
247
+ pushWindow(ctx, "tool-errors", { ts: ctx.now });
248
+ const threshold = getRuleThreshold(ctx, "tool-errors", 1);
249
+ const windowMs = 60 * 1000; // 1 minute
250
+ const count = countInWindow(ctx, "tool-errors", windowMs);
251
+ if (count < threshold)
252
+ return null;
253
+ const toolName = event.meta?.toolName ?? "unknown";
254
+ const fingerprint = `tool-errors:${toolName}`;
255
+ return {
256
+ type: "alert",
257
+ id: makeAlertId("tool-errors", fingerprint, ctx.now),
258
+ ruleId: "tool-errors",
259
+ severity: "warn",
260
+ title: "Tool errors spike",
261
+ detail: `${count} tool error(s) in the last minute.${event.error ? ` Last: ${event.error}` : ""}`,
262
+ ts: ctx.now,
263
+ fingerprint,
264
+ };
265
+ },
266
+ };
230
267
  // ─── Rule: gateway-down ──────────────────────────────────────────────────────
231
268
  const gatewayDown = {
232
269
  id: "gateway-down",
233
270
  defaultCooldownMs: 60 * 60 * 1000,
234
- defaultThreshold: 90_000, // 90 seconds
271
+ defaultThreshold: 30_000, // 30 seconds
235
272
  evaluate(event, ctx) {
236
273
  // This rule is called by the watchdog timer, not by events directly.
237
274
  if (event.type !== "watchdog.tick")
@@ -270,5 +307,6 @@ export const ALL_RULES = [
270
307
  heartbeatFail,
271
308
  queueDepth,
272
309
  highErrorRate,
310
+ toolErrors,
273
311
  gatewayDown,
274
312
  ];
@@ -51,6 +51,8 @@ export type AlertTarget = {
51
51
  to: string;
52
52
  accountId?: string;
53
53
  };
54
+ /** Enriches an alert with LLM-generated summary/action. Returns enriched alert or null to skip. */
55
+ export type AlertEnricher = (alert: AlertEvent) => Promise<AlertEvent | null>;
54
56
  export type RuleOverride = {
55
57
  enabled?: boolean;
56
58
  threshold?: number;
@@ -65,6 +67,7 @@ export type MonitorConfig = {
65
67
  maxLogSizeKb?: number;
66
68
  maxLogAgeDays?: number;
67
69
  quiet?: boolean;
70
+ llmEnriched?: boolean;
68
71
  rules?: Record<string, RuleOverride>;
69
72
  };
70
73
  export type OpenAlertsInitOptions = {
@@ -80,6 +83,8 @@ export type OpenAlertsInitOptions = {
80
83
  logPrefix?: string;
81
84
  /** Diagnosis hint shown in critical alerts (e.g., 'Run "openclaw doctor"') */
82
85
  diagnosisHint?: string;
86
+ /** Optional LLM enricher — adds smart summaries to alerts before dispatch */
87
+ enricher?: AlertEnricher;
83
88
  };
84
89
  export type OpenAlertsLogger = {
85
90
  info: (msg: string) => void;
@@ -148,5 +153,5 @@ export declare const DEFAULTS: {
148
153
  readonly pruneIntervalMs: number;
149
154
  readonly platformFlushIntervalMs: number;
150
155
  readonly platformBatchSize: 100;
151
- readonly gatewayDownThresholdMs: 90000;
156
+ readonly gatewayDownThresholdMs: 30000;
152
157
  };
@@ -13,5 +13,5 @@ export const DEFAULTS = {
13
13
  pruneIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
14
14
  platformFlushIntervalMs: 5 * 60 * 1000, // 5 minutes
15
15
  platformBatchSize: 100,
16
- gatewayDownThresholdMs: 90_000, // 90 seconds
16
+ gatewayDownThresholdMs: 30_000, // 30 seconds
17
17
  };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { 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 { OpenClawAlertChannel, parseConfig, resolveAlertTarget, translateOpenClawEvent, translateToolCallHook, translateAgentStartHook, translateAgentEndHook, translateSessionStartHook, translateSessionEndHook, translateMessageSentHook, translateMessageReceivedHook, translateBeforeToolCallHook, translateBeforeCompactionHook, translateAfterCompactionHook, translateMessageSendingHook, translateToolResultPersistHook, translateGatewayStartHook, translateGatewayStopHook, } from "./plugin/adapter.js";
4
+ 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
5
  import { bindEngine, createMonitorCommands } from "./plugin/commands.js";
6
6
  import { createDashboardHandler, closeDashboardConnections, } from "./plugin/dashboard-routes.js";
7
7
  const PLUGIN_ID = "openalerts";
@@ -13,12 +13,16 @@ let logBridgeCleanup = null;
13
13
  function createMonitorService(api) {
14
14
  return {
15
15
  id: PLUGIN_ID,
16
- start(ctx) {
16
+ async start(ctx) {
17
17
  const logger = ctx.logger;
18
18
  const config = parseConfig(api.pluginConfig);
19
19
  // Resolve alert target + create OpenClaw alert channel
20
- const target = resolveAlertTarget(api, config);
20
+ const target = await resolveAlertTarget(api, config);
21
21
  const channels = target ? [new OpenClawAlertChannel(api, target)] : [];
22
+ // Create LLM enricher if enabled (default: false)
23
+ const enricher = config.llmEnriched === true
24
+ ? createOpenClawEnricher(api, logger)
25
+ : null;
22
26
  // Create and start the universal engine
23
27
  engine = new OpenAlertsEngine({
24
28
  stateDir: ctx.stateDir,
@@ -27,6 +31,7 @@ function createMonitorService(api) {
27
31
  logger,
28
32
  logPrefix: LOG_PREFIX,
29
33
  diagnosisHint: 'Run "openclaw doctor" to diagnose.',
34
+ enricher: enricher ?? undefined,
30
35
  });
31
36
  engine.start();
32
37
  // Wire commands to engine
@@ -169,7 +174,7 @@ function createMonitorService(api) {
169
174
  const targetDesc = target
170
175
  ? `alerting to ${target.channel}:${target.to}`
171
176
  : "log-only (no alert channel detected)";
172
- logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active, 7 rules active`);
177
+ logger.info(`${LOG_PREFIX}: started, ${targetDesc}, log-bridge active, 8 rules active`);
173
178
  },
174
179
  stop() {
175
180
  closeDashboardConnections();
@@ -1,4 +1,4 @@
1
- import type { AlertChannel, AlertEvent, AlertTarget, MonitorConfig, OpenAlertsEvent } from "../core/index.js";
1
+ import type { AlertChannel, AlertEnricher, AlertEvent, AlertTarget, MonitorConfig, OpenAlertsEvent } from "../core/index.js";
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
3
  /**
4
4
  * Translate an OpenClaw diagnostic event into a universal OpenAlertsEvent.
@@ -128,11 +128,23 @@ export declare class OpenClawAlertChannel implements AlertChannel {
128
128
  readonly name: string;
129
129
  private api;
130
130
  private target;
131
+ private warnedMissing;
131
132
  constructor(api: OpenClawPluginApi, target: AlertTarget);
132
133
  send(alert: AlertEvent, formatted: string): Promise<void>;
133
134
  }
134
135
  /**
135
136
  * Resolve the alert target from plugin config or by auto-detecting from OpenClaw config.
136
137
  */
137
- export declare function resolveAlertTarget(api: OpenClawPluginApi, pluginConfig: MonitorConfig): AlertTarget | null;
138
+ export declare function resolveAlertTarget(api: OpenClawPluginApi, pluginConfig: MonitorConfig): Promise<AlertTarget | null>;
138
139
  export declare function parseConfig(raw: Record<string, unknown> | undefined): MonitorConfig;
140
+ /**
141
+ * Create an AlertEnricher from the OpenClaw plugin API.
142
+ * Reads the model from api.config.agents.defaults.model.primary (e.g. "openai/gpt-5-nano")
143
+ * and resolves the API key from process.env.
144
+ * Returns null if no model is configured or enricher can't be created.
145
+ */
146
+ export declare function createOpenClawEnricher(api: OpenClawPluginApi, logger?: {
147
+ info: (msg: string) => void;
148
+ warn: (msg: string) => void;
149
+ error: (msg: string) => void;
150
+ }): AlertEnricher | null;
@@ -1,3 +1,7 @@
1
+ import { createLlmEnricher, resolveApiKeyEnvVar } from "../core/llm-enrichment.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
1
5
  // ─── Diagnostic Event Translation ───────────────────────────────────────────
2
6
  //
3
7
  // OpenClaw emits 12 diagnostic event types through onDiagnosticEvent():
@@ -230,7 +234,7 @@ export function translateMessageSentHook(data, context) {
230
234
  sessionKey: context.sessionId,
231
235
  outcome: data.success ? "success" : "error",
232
236
  error: data.error,
233
- meta: { to: data.to, source: "hook:message_sent" },
237
+ meta: { to: data.to, content: data.content, source: "hook:message_sent" },
234
238
  };
235
239
  }
236
240
  /** Translate gateway_start hook data into OpenAlertsEvent. */
@@ -261,6 +265,7 @@ export function translateMessageReceivedHook(data, context) {
261
265
  outcome: "success",
262
266
  meta: {
263
267
  from: data.from,
268
+ content: data.content,
264
269
  accountId: context.accountId,
265
270
  openclawHook: "message_received",
266
271
  source: "hook:message_received",
@@ -310,6 +315,7 @@ export function translateAfterCompactionHook(data, context) {
310
315
  messageCount: data.messageCount,
311
316
  tokenCount: data.tokenCount,
312
317
  compactedCount: data.compactedCount,
318
+ compaction: true,
313
319
  openclawHook: "after_compaction",
314
320
  source: "hook:after_compaction",
315
321
  },
@@ -324,6 +330,7 @@ export function translateMessageSendingHook(data, context) {
324
330
  outcome: "success",
325
331
  meta: {
326
332
  to: data.to,
333
+ content: data.content,
327
334
  accountId: context.accountId,
328
335
  openclawHook: "message_sending",
329
336
  source: "hook:message_sending",
@@ -356,6 +363,7 @@ export class OpenClawAlertChannel {
356
363
  name;
357
364
  api;
358
365
  target;
366
+ warnedMissing = false;
359
367
  constructor(api, target) {
360
368
  this.api = api;
361
369
  this.target = target;
@@ -364,8 +372,13 @@ export class OpenClawAlertChannel {
364
372
  async send(alert, formatted) {
365
373
  const runtime = this.api.runtime;
366
374
  const channel = runtime.channel;
367
- if (!channel)
375
+ if (!channel) {
376
+ if (!this.warnedMissing) {
377
+ this.warnedMissing = true;
378
+ throw new Error(`runtime.channel not available — alert dropped`);
379
+ }
368
380
  return;
381
+ }
369
382
  const opts = this.target.accountId
370
383
  ? { accountId: this.target.accountId }
371
384
  : {};
@@ -377,20 +390,22 @@ export class OpenClawAlertChannel {
377
390
  signal: "sendMessageSignal",
378
391
  };
379
392
  const methodName = channelMethods[this.target.channel];
380
- if (!methodName)
381
- return;
393
+ if (!methodName) {
394
+ throw new Error(`unsupported channel "${this.target.channel}" — no send method mapped`);
395
+ }
382
396
  const channelMod = channel[this.target.channel];
383
397
  const sendFn = channelMod?.[methodName];
384
- if (sendFn) {
385
- await sendFn(this.target.to, formatted, opts);
398
+ if (!sendFn) {
399
+ throw new Error(`${this.target.channel}.${methodName} not found on runtime — alert dropped`);
386
400
  }
401
+ await sendFn(this.target.to, formatted, opts);
387
402
  }
388
403
  }
389
404
  // ─── Alert Target Resolution ────────────────────────────────────────────────
390
405
  /**
391
406
  * Resolve the alert target from plugin config or by auto-detecting from OpenClaw config.
392
407
  */
393
- export function resolveAlertTarget(api, pluginConfig) {
408
+ export async function resolveAlertTarget(api, pluginConfig) {
394
409
  // 1. Explicit config
395
410
  if (pluginConfig.alertChannel && pluginConfig.alertTo) {
396
411
  return {
@@ -400,7 +415,8 @@ export function resolveAlertTarget(api, pluginConfig) {
400
415
  };
401
416
  }
402
417
  const cfg = api.config;
403
- // 2. Auto-detect from configured channels
418
+ const channelsCfg = cfg.channels ??
419
+ {};
404
420
  const channelKeys = [
405
421
  "telegram",
406
422
  "discord",
@@ -408,14 +424,33 @@ export function resolveAlertTarget(api, pluginConfig) {
408
424
  "whatsapp",
409
425
  "signal",
410
426
  ];
427
+ // 2. Auto-detect from static allowFrom in channel config
411
428
  for (const channelKey of channelKeys) {
412
- const channelConfig = cfg[channelKey];
429
+ const channelConfig = channelsCfg[channelKey];
413
430
  if (!channelConfig || typeof channelConfig !== "object")
414
431
  continue;
415
432
  const target = extractFirstAllowFrom(channelKey, channelConfig);
416
433
  if (target)
417
434
  return target;
418
435
  }
436
+ // 3. Auto-detect from pairing store (runtime-paired users)
437
+ // The store lives at ~/.openclaw/credentials/<channel>-allowFrom.json
438
+ const credDir = join(process.env.OPENCLAW_HOME ?? join(homedir(), ".openclaw"), "credentials");
439
+ for (const channelKey of channelKeys) {
440
+ const channelConfig = channelsCfg[channelKey];
441
+ if (!channelConfig || typeof channelConfig !== "object")
442
+ continue;
443
+ try {
444
+ const raw = await readFile(join(credDir, `${channelKey}-allowFrom.json`), "utf-8");
445
+ const data = JSON.parse(raw);
446
+ if (Array.isArray(data.allowFrom) && data.allowFrom.length > 0) {
447
+ return { channel: channelKey, to: String(data.allowFrom[0]) };
448
+ }
449
+ }
450
+ catch {
451
+ // File doesn't exist or isn't valid — skip this channel
452
+ }
453
+ }
419
454
  return null;
420
455
  }
421
456
  function extractFirstAllowFrom(channel, channelConfig) {
@@ -451,8 +486,45 @@ export function parseConfig(raw) {
451
486
  maxLogSizeKb: typeof raw.maxLogSizeKb === "number" ? raw.maxLogSizeKb : undefined,
452
487
  maxLogAgeDays: typeof raw.maxLogAgeDays === "number" ? raw.maxLogAgeDays : undefined,
453
488
  quiet: typeof raw.quiet === "boolean" ? raw.quiet : undefined,
489
+ llmEnriched: typeof raw.llmEnriched === "boolean" ? raw.llmEnriched : undefined,
454
490
  rules: raw.rules && typeof raw.rules === "object"
455
491
  ? raw.rules
456
492
  : undefined,
457
493
  };
458
494
  }
495
+ // ─── LLM Enricher Factory ───────────────────────────────────────────────────
496
+ /**
497
+ * Create an AlertEnricher from the OpenClaw plugin API.
498
+ * Reads the model from api.config.agents.defaults.model.primary (e.g. "openai/gpt-5-nano")
499
+ * and resolves the API key from process.env.
500
+ * Returns null if no model is configured or enricher can't be created.
501
+ */
502
+ export function createOpenClawEnricher(api, logger) {
503
+ try {
504
+ const cfg = api.config;
505
+ const agents = cfg.agents;
506
+ const defaults = agents?.defaults;
507
+ const model = defaults?.model;
508
+ const primary = model?.primary;
509
+ if (typeof primary !== "string" || !primary.includes("/")) {
510
+ logger?.warn("openalerts: llm-enrichment skipped — no model configured at agents.defaults.model.primary");
511
+ return null;
512
+ }
513
+ // Resolve API key here (in adapter) to keep env access separate from network calls
514
+ const envVar = resolveApiKeyEnvVar(primary);
515
+ if (!envVar) {
516
+ logger?.warn("openalerts: llm-enrichment skipped — unknown provider");
517
+ return null;
518
+ }
519
+ const apiKey = process.env[envVar];
520
+ if (!apiKey) {
521
+ logger?.warn(`openalerts: llm-enrichment skipped — ${envVar} not set in environment`);
522
+ return null;
523
+ }
524
+ return createLlmEnricher({ modelString: primary, apiKey, logger });
525
+ }
526
+ catch (err) {
527
+ logger?.warn(`openalerts: llm-enrichment setup failed: ${String(err)}`);
528
+ return null;
529
+ }
530
+ }
@@ -29,7 +29,7 @@ export function createMonitorCommands(api) {
29
29
  handler: () => handleDashboard(),
30
30
  },
31
31
  {
32
- name: "test-alert",
32
+ name: "test_alert",
33
33
  description: "Send a test alert to verify alert delivery",
34
34
  acceptsArgs: false,
35
35
  handler: () => handleTestAlert(),
@@ -72,26 +72,7 @@ function handleTestAlert() {
72
72
  if (!_engine) {
73
73
  return { text: "OpenAlerts not initialized yet. Wait for gateway startup." };
74
74
  }
75
- // Ingest a synthetic infra.error to trigger the infra-errors rule evaluation.
76
- // This won't fire an actual alert unless the threshold (3 errors) is reached,
77
- // so we fire a one-off test alert directly through the engine.
78
- const testEvent = {
79
- type: "alert",
80
- id: `test:manual:${Date.now()}`,
81
- ruleId: "test",
82
- severity: "info",
83
- title: "Test alert — delivery verified",
84
- detail: "This is a test alert from /test-alert. If you see this, alert delivery is working.",
85
- ts: Date.now(),
86
- fingerprint: `test:manual`,
87
- };
88
- // Ingest as a custom event so it appears in the dashboard
89
- _engine.ingest({
90
- type: "custom",
91
- ts: Date.now(),
92
- outcome: "success",
93
- meta: { openclawLog: "test_alert", source: "command:test-alert" },
94
- });
75
+ _engine.sendTestAlert();
95
76
  return {
96
77
  text: "Test alert sent. Check your alert channel (Telegram/Discord/etc) for delivery confirmation.\n\nIf you don't receive it, check /health for channel status.",
97
78
  };