openclaw-observability 1.0.0

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 (66) hide show
  1. package/dist/config.d.ts +60 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/dist/config.js +140 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/index.d.ts +37 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1114 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/redaction.d.ts +20 -0
  10. package/dist/redaction.d.ts.map +1 -0
  11. package/dist/redaction.js +93 -0
  12. package/dist/redaction.js.map +1 -0
  13. package/dist/security/chain-detector.d.ts +37 -0
  14. package/dist/security/chain-detector.d.ts.map +1 -0
  15. package/dist/security/chain-detector.js +187 -0
  16. package/dist/security/chain-detector.js.map +1 -0
  17. package/dist/security/rules.d.ts +22 -0
  18. package/dist/security/rules.d.ts.map +1 -0
  19. package/dist/security/rules.js +479 -0
  20. package/dist/security/rules.js.map +1 -0
  21. package/dist/security/scanner.d.ts +47 -0
  22. package/dist/security/scanner.d.ts.map +1 -0
  23. package/dist/security/scanner.js +150 -0
  24. package/dist/security/scanner.js.map +1 -0
  25. package/dist/security/types.d.ts +47 -0
  26. package/dist/security/types.d.ts.map +1 -0
  27. package/dist/security/types.js +23 -0
  28. package/dist/security/types.js.map +1 -0
  29. package/dist/storage/buffer.d.ts +64 -0
  30. package/dist/storage/buffer.d.ts.map +1 -0
  31. package/dist/storage/buffer.js +120 -0
  32. package/dist/storage/buffer.js.map +1 -0
  33. package/dist/storage/duckdb-local-writer.d.ts +26 -0
  34. package/dist/storage/duckdb-local-writer.d.ts.map +1 -0
  35. package/dist/storage/duckdb-local-writer.js +454 -0
  36. package/dist/storage/duckdb-local-writer.js.map +1 -0
  37. package/dist/storage/mysql-writer.d.ts +55 -0
  38. package/dist/storage/mysql-writer.d.ts.map +1 -0
  39. package/dist/storage/mysql-writer.js +287 -0
  40. package/dist/storage/mysql-writer.js.map +1 -0
  41. package/dist/storage/schema.d.ts +13 -0
  42. package/dist/storage/schema.d.ts.map +1 -0
  43. package/dist/storage/schema.js +94 -0
  44. package/dist/storage/schema.js.map +1 -0
  45. package/dist/storage/writer.d.ts +31 -0
  46. package/dist/storage/writer.d.ts.map +1 -0
  47. package/dist/storage/writer.js +7 -0
  48. package/dist/storage/writer.js.map +1 -0
  49. package/dist/types.d.ts +72 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +44 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/web/api.d.ts +115 -0
  54. package/dist/web/api.d.ts.map +1 -0
  55. package/dist/web/api.js +219 -0
  56. package/dist/web/api.js.map +1 -0
  57. package/dist/web/routes.d.ts +20 -0
  58. package/dist/web/routes.d.ts.map +1 -0
  59. package/dist/web/routes.js +175 -0
  60. package/dist/web/routes.js.map +1 -0
  61. package/dist/web/ui.d.ts +9 -0
  62. package/dist/web/ui.d.ts.map +1 -0
  63. package/dist/web/ui.js +1327 -0
  64. package/dist/web/ui.js.map +1 -0
  65. package/openclaw.plugin.json +231 -0
  66. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,1114 @@
1
+ "use strict";
2
+ /**
3
+ * OpenClaw audit plugin entry point
4
+ * Registers all 24 Plugin Hooks, assembles capture -> buffer -> writer pipeline
5
+ *
6
+ * OpenClaw hooks:
7
+ * Agent: before_model_resolve, before_prompt_build, before_agent_start, agent_end
8
+ * LLM: llm_input, llm_output
9
+ * Tool: before_tool_call, after_tool_call, tool_result_persist
10
+ * Msg: message_received, message_sending, message_sent, before_message_write
11
+ * Ctx: before_compaction, after_compaction, before_reset
12
+ * Session: session_start, session_end
13
+ * Subagent: subagent_spawning, subagent_delivery_target, subagent_spawned, subagent_ended
14
+ * Gateway: gateway_start, gateway_stop
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.activate = activate;
18
+ const config_1 = require("./config");
19
+ const types_1 = require("./types");
20
+ const redaction_1 = require("./redaction");
21
+ const mysql_writer_1 = require("./storage/mysql-writer");
22
+ const duckdb_local_writer_1 = require("./storage/duckdb-local-writer");
23
+ const buffer_1 = require("./storage/buffer");
24
+ const routes_1 = require("./web/routes");
25
+ const scanner_1 = require("./security/scanner");
26
+ /* ------------------------------------------------------------------ */
27
+ /* Runtime state */
28
+ /* ------------------------------------------------------------------ */
29
+ /** Max map entries (prevent memory leaks) */
30
+ const MAX_MAP_ENTRIES = 1000;
31
+ /** Map entry TTL (5 minutes) */
32
+ const MAP_ENTRY_TTL_MS = 300_000;
33
+ /** Max alert buffer size */
34
+ const MAX_ALERT_BUFFER = 500;
35
+ /** Track start time of each LLM/tool call */
36
+ const runStartTimes = new Map();
37
+ /**
38
+ * Map: requestUrl → usage from SSE final chunk
39
+ * We key by a monotonically increasing request ID since we can't know
40
+ * the sessionId at fetch time.
41
+ */
42
+ const sseUsageCache = new Map();
43
+ let sseRequestCounter = 0;
44
+ /**
45
+ * Most recent SSE usage entry (LIFO — the llm_output hook fires shortly
46
+ * after the stream completes, so the latest entry is the one we want)
47
+ */
48
+ let latestSseUsage = null;
49
+ /** Original fetch reference */
50
+ let originalFetch = null;
51
+ /** Whether the fetch interceptor is installed */
52
+ let fetchInterceptorInstalled = false;
53
+ function installFetchInterceptor() {
54
+ if (fetchInterceptorInstalled)
55
+ return;
56
+ originalFetch = globalThis.fetch;
57
+ const origFetch = originalFetch;
58
+ globalThis.fetch = async function patchedFetch(input, init) {
59
+ // Only intercept POST requests with JSON body containing stream: true
60
+ if (!init?.body || init.method?.toUpperCase() !== 'POST') {
61
+ return origFetch(input, init);
62
+ }
63
+ let bodyStr;
64
+ try {
65
+ bodyStr = typeof init.body === 'string' ? init.body : undefined;
66
+ }
67
+ catch {
68
+ // Not a string body, skip
69
+ }
70
+ if (!bodyStr) {
71
+ return origFetch(input, init);
72
+ }
73
+ // Check if this is an OpenAI-compatible streaming request
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(bodyStr);
77
+ }
78
+ catch {
79
+ return origFetch(input, init);
80
+ }
81
+ if (!parsed || parsed.stream !== true || !parsed.model || !parsed.messages) {
82
+ return origFetch(input, init);
83
+ }
84
+ // Inject stream_options if not already present
85
+ if (!parsed.stream_options) {
86
+ parsed.stream_options = { include_usage: true };
87
+ init = { ...init, body: JSON.stringify(parsed) };
88
+ }
89
+ // Make the actual request
90
+ const response = await origFetch(input, init);
91
+ // Clone the response so we can read the body without consuming it for OpenClaw
92
+ // We use a TransformStream to tee the body and parse SSE chunks
93
+ if (!response.body) {
94
+ return response;
95
+ }
96
+ const requestId = ++sseRequestCounter;
97
+ const reader = response.body.getReader();
98
+ const decoder = new TextDecoder();
99
+ let sseBuffer = '';
100
+ let extractedUsage = null;
101
+ /** Parse a single SSE data line for usage info */
102
+ function tryExtractUsage(dataStr) {
103
+ try {
104
+ const evt = JSON.parse(dataStr);
105
+ if (evt.usage && (evt.usage.prompt_tokens || evt.usage.input_tokens)) {
106
+ const u = evt.usage;
107
+ extractedUsage = {
108
+ input: u.prompt_tokens ?? u.input_tokens ?? 0,
109
+ output: u.completion_tokens ?? u.output_tokens ?? 0,
110
+ cacheRead: u.cache_read_input_tokens ?? 0,
111
+ cacheWrite: u.cache_creation_input_tokens ?? 0,
112
+ total: u.total_tokens ?? 0,
113
+ timestamp: Date.now(),
114
+ };
115
+ }
116
+ }
117
+ catch { /* ignore parse errors */ }
118
+ }
119
+ /** Process buffered SSE lines */
120
+ function processSseLines() {
121
+ const lines = sseBuffer.split('\n');
122
+ sseBuffer = lines.pop() || ''; // keep last incomplete line
123
+ for (const line of lines) {
124
+ if (!line.startsWith('data: '))
125
+ continue;
126
+ const data = line.slice(6).trim();
127
+ if (data === '[DONE]')
128
+ continue;
129
+ tryExtractUsage(data);
130
+ }
131
+ }
132
+ // Pipe response body through a pass-through ReadableStream
133
+ // that also extracts usage from SSE chunks
134
+ const newBody = new ReadableStream({
135
+ async start(controller) {
136
+ try {
137
+ while (true) {
138
+ const { done, value } = await reader.read();
139
+ if (done) {
140
+ // Process any remaining data in the buffer
141
+ if (sseBuffer.trim())
142
+ processSseLines();
143
+ if (extractedUsage) {
144
+ sseUsageCache.set(requestId, extractedUsage);
145
+ latestSseUsage = extractedUsage;
146
+ }
147
+ controller.close();
148
+ break;
149
+ }
150
+ controller.enqueue(value);
151
+ sseBuffer += decoder.decode(value, { stream: true });
152
+ processSseLines();
153
+ }
154
+ }
155
+ catch (err) {
156
+ controller.error(err);
157
+ }
158
+ },
159
+ });
160
+ return new Response(newBody, {
161
+ status: response.status,
162
+ statusText: response.statusText,
163
+ headers: response.headers,
164
+ });
165
+ };
166
+ fetchInterceptorInstalled = true;
167
+ console.log('[audit-duckdb] Fetch interceptor installed for token usage tracking');
168
+ }
169
+ function uninstallFetchInterceptor() {
170
+ if (originalFetch) {
171
+ globalThis.fetch = originalFetch;
172
+ originalFetch = null;
173
+ fetchInterceptorInstalled = false;
174
+ console.log('[audit-duckdb] Fetch interceptor uninstalled');
175
+ }
176
+ }
177
+ /**
178
+ * Pop the most recent SSE usage (consume once per llm_output call)
179
+ */
180
+ function popLatestSseUsage() {
181
+ const usage = latestSseUsage;
182
+ latestSseUsage = null;
183
+ // Also trim old cache entries
184
+ if (sseUsageCache.size > 100) {
185
+ const keys = Array.from(sseUsageCache.keys()).sort((a, b) => a - b);
186
+ for (let i = 0; i < keys.length - 50; i++) {
187
+ sseUsageCache.delete(keys[i]);
188
+ }
189
+ }
190
+ return usage;
191
+ }
192
+ const runInputs = new Map();
193
+ /**
194
+ * Periodically clean up stale Map entries to prevent memory leaks when llm_input fires but llm_output does not
195
+ */
196
+ function cleanupStaleMaps() {
197
+ const now = Date.now();
198
+ for (const [key, startTime] of runStartTimes) {
199
+ if (now - startTime > MAP_ENTRY_TTL_MS) {
200
+ runStartTimes.delete(key);
201
+ }
202
+ }
203
+ // runInputs has no timestamps; trim oldest entries when exceeding size limit
204
+ if (runInputs.size > MAX_MAP_ENTRIES) {
205
+ const keys = Array.from(runInputs.keys());
206
+ for (let i = 0; i < keys.length - MAX_MAP_ENTRIES; i++) {
207
+ runInputs.delete(keys[i]);
208
+ }
209
+ }
210
+ // sseUsageCache: trim old entries
211
+ if (sseUsageCache.size > 100) {
212
+ const keys = Array.from(sseUsageCache.keys()).sort((a, b) => a - b);
213
+ for (let i = 0; i < keys.length - 50; i++) {
214
+ sseUsageCache.delete(keys[i]);
215
+ }
216
+ }
217
+ // sessionContextMap: trim oversized context Map
218
+ if (sessionContextMap.size > MAX_MAP_ENTRIES) {
219
+ const keys = Array.from(sessionContextMap.keys());
220
+ for (let i = 0; i < keys.length - MAX_MAP_ENTRIES; i++) {
221
+ sessionContextMap.delete(keys[i]);
222
+ }
223
+ }
224
+ // sessionStatsMap: evict oldest entries when exceeding limit
225
+ if (sessionStatsMap.size > MAX_SESSION_STATS) {
226
+ const entries = Array.from(sessionStatsMap.entries());
227
+ entries.sort((a, b) => a[1].lastSeen.getTime() - b[1].lastSeen.getTime());
228
+ const toRemove = entries.length - MAX_SESSION_STATS;
229
+ for (let i = 0; i < toRemove; i++) {
230
+ sessionStatsMap.delete(entries[i][0]);
231
+ }
232
+ }
233
+ }
234
+ /**
235
+ * Extract image/media metadata from historyMessages
236
+ * OpenClaw message format:
237
+ * UserMessage.content = string | (TextContent | ImageContent)[]
238
+ * ToolResultMessage.content = (TextContent | ImageContent)[]
239
+ * ImageContent = { type: "image", data: base64string, mimeType: string }
240
+ */
241
+ function extractMediaMeta(historyMessages) {
242
+ const media = [];
243
+ if (!Array.isArray(historyMessages))
244
+ return media;
245
+ for (const msg of historyMessages) {
246
+ if (!msg || typeof msg !== 'object')
247
+ continue;
248
+ const m = msg;
249
+ const role = m.role;
250
+ const content = m.content;
251
+ // content can be string (plain text) or array (multimodal)
252
+ if (!Array.isArray(content))
253
+ continue;
254
+ const source = role === 'toolResult' ? 'tool' : 'user';
255
+ for (const part of content) {
256
+ if (!part || typeof part !== 'object')
257
+ continue;
258
+ const p = part;
259
+ if (p.type === 'image' && typeof p.data === 'string') {
260
+ const base64Len = p.data.length;
261
+ // base64 encoded size is ~4/3 of original, so multiply by 0.75 to approximate original bytes
262
+ const sizeBytes = Math.round(base64Len * 0.75);
263
+ media.push({
264
+ mimeType: p.mimeType || 'unknown',
265
+ sizeBytes,
266
+ source,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ return media;
272
+ }
273
+ const sessionContextMap = new Map();
274
+ /** Most recently active sessionId (ultimate fallback when hook does not carry sessionId) */
275
+ let lastFallbackSessionId = 'unknown';
276
+ /** Get or create session context */
277
+ function getSessionCtx(sessionId) {
278
+ let ctx = sessionContextMap.get(sessionId);
279
+ if (!ctx) {
280
+ ctx = { sessionId, userId: '', modelName: '' };
281
+ sessionContextMap.set(sessionId, ctx);
282
+ }
283
+ return ctx;
284
+ }
285
+ /** Resolve sessionId from hook parameters (priority: event → ctx → fallback) */
286
+ function resolveSessionId(eventSessionId, ctxSessionId) {
287
+ if (eventSessionId && eventSessionId !== 'unknown') {
288
+ lastFallbackSessionId = eventSessionId;
289
+ return eventSessionId;
290
+ }
291
+ if (ctxSessionId && ctxSessionId !== 'unknown') {
292
+ lastFallbackSessionId = ctxSessionId;
293
+ return ctxSessionId;
294
+ }
295
+ return lastFallbackSessionId;
296
+ }
297
+ const sessionStatsMap = new Map();
298
+ /** Max session stats entries */
299
+ const MAX_SESSION_STATS = 200;
300
+ /** Update session statistics and return AuditSession */
301
+ function updateSessionStats(sessionId, modelName, userId, tokens) {
302
+ let stats = sessionStatsMap.get(sessionId);
303
+ if (!stats) {
304
+ stats = {
305
+ firstSeen: new Date(),
306
+ lastSeen: new Date(),
307
+ modelName,
308
+ userId,
309
+ totalActions: 0,
310
+ totalTokens: 0,
311
+ };
312
+ sessionStatsMap.set(sessionId, stats);
313
+ }
314
+ stats.lastSeen = new Date();
315
+ stats.totalActions += 1;
316
+ stats.totalTokens += tokens;
317
+ if (modelName)
318
+ stats.modelName = modelName;
319
+ if (userId)
320
+ stats.userId = userId;
321
+ return {
322
+ sessionId,
323
+ userId: stats.userId,
324
+ modelName: stats.modelName,
325
+ startTime: stats.firstSeen,
326
+ endTime: stats.lastSeen,
327
+ totalActions: stats.totalActions,
328
+ totalTokens: stats.totalTokens,
329
+ };
330
+ }
331
+ /** Helper to build an AuditAction (auto-fills defaults) */
332
+ function makeAction(sessionId, overrides) {
333
+ const sctx = getSessionCtx(sessionId);
334
+ return {
335
+ sessionId,
336
+ actionType: overrides.actionType,
337
+ actionName: overrides.actionName,
338
+ modelName: overrides.modelName ?? sctx.modelName,
339
+ inputParams: overrides.inputParams ?? null,
340
+ outputResult: overrides.outputResult ?? null,
341
+ promptTokens: overrides.promptTokens ?? null,
342
+ completionTokens: overrides.completionTokens ?? null,
343
+ durationMs: overrides.durationMs ?? null,
344
+ userId: overrides.userId ?? sctx.userId,
345
+ createdAt: overrides.createdAt ?? new Date(),
346
+ };
347
+ }
348
+ /* ------------------------------------------------------------------ */
349
+ /* Plugin registration */
350
+ /* ------------------------------------------------------------------ */
351
+ function activate(api) {
352
+ const rawConfig = (api.pluginConfig || api.config || {});
353
+ const config = (0, config_1.resolveConfig)(rawConfig);
354
+ console.log(`[audit-duckdb] Config resolved: mode=${config.mode} ` +
355
+ (config.mode === 'remote'
356
+ ? `mysql.host=${config.mysql.host} mysql.database=${config.mysql.database}`
357
+ : `duckdb.path=${config.duckdb.path}`));
358
+ const redactor = new redaction_1.Redactor(config.redaction);
359
+ // Select storage backend based on mode
360
+ let writer;
361
+ if (config.mode === 'remote') {
362
+ writer = new mysql_writer_1.MySQLWriter(config.mysql);
363
+ }
364
+ else {
365
+ writer = new duckdb_local_writer_1.DuckDBLocalWriter(config.duckdb);
366
+ }
367
+ const buffer = new buffer_1.AsyncBatchBuffer(config.buffer, (entries) => writer.writeBatch(entries));
368
+ // Install fetch interceptor for token usage tracking
369
+ installFetchInterceptor();
370
+ // Security scanner
371
+ const securityConfig = (0, scanner_1.resolveSecurityConfig)(config.security);
372
+ const securityScanner = new scanner_1.SecurityScanner(securityConfig);
373
+ /** Security alert buffer (batch write) */
374
+ const alertBuffer = [];
375
+ let alertFlushTimer = null;
376
+ let mapCleanupTimer = null;
377
+ // Check whether database can be connected
378
+ // - local mode: always available (local file, zero config)
379
+ // - remote mode: requires user-configured connection info
380
+ const dbConfigured = config.mode === 'local'
381
+ || (config.mysql.host !== 'localhost' || config.mysql.password !== '');
382
+ if (dbConfigured) {
383
+ // Async background initialization
384
+ writer
385
+ .initialize()
386
+ .then(() => {
387
+ buffer.start();
388
+ // Start security alert flush timer (every 30s)
389
+ alertFlushTimer = setInterval(() => void flushAlerts(), 30000);
390
+ if (alertFlushTimer && typeof alertFlushTimer === 'object' && 'unref' in alertFlushTimer) {
391
+ alertFlushTimer.unref();
392
+ }
393
+ // Start Map cleanup timer (every 60s)
394
+ mapCleanupTimer = setInterval(cleanupStaleMaps, 60000);
395
+ if (mapCleanupTimer && typeof mapCleanupTimer === 'object' && 'unref' in mapCleanupTimer) {
396
+ mapCleanupTimer.unref();
397
+ }
398
+ console.log(`[audit-duckdb] Plugin activated (mode=${config.mode}, security scanner enabled)`);
399
+ })
400
+ .catch((error) => {
401
+ console.error('[audit-duckdb] Failed to initialize database:', error);
402
+ });
403
+ }
404
+ else {
405
+ console.log('[audit-duckdb] MySQL not configured yet — skipping database connection. ' +
406
+ 'Please configure via Dashboard (Settings → Plugins → audit-duckdb Config) then restart.');
407
+ }
408
+ // Register audit panel Web UI (mounted on Gateway HTTP server)
409
+ if (typeof api.registerHttpRoute === 'function') {
410
+ (0, routes_1.registerAuditRoutes)(api.registerHttpRoute.bind(api), writer);
411
+ }
412
+ else {
413
+ console.warn('[audit-duckdb] registerHttpRoute not available — audit UI disabled');
414
+ }
415
+ // ====================== Helper functions ======================
416
+ /** Record action, update session stats, and run security scan */
417
+ function recordAction(action, tokens = 0) {
418
+ void buffer.addAction(action);
419
+ const session = updateSessionStats(action.sessionId, action.modelName, action.userId, tokens);
420
+ void buffer.addSession(session);
421
+ // --- Security scan ---
422
+ if (securityConfig.enabled) {
423
+ try {
424
+ const alerts = securityScanner.scan(action);
425
+ if (alerts.length > 0) {
426
+ // Prevent alertBuffer from growing unbounded
427
+ if (alertBuffer.length + alerts.length > MAX_ALERT_BUFFER) {
428
+ const overflow = alertBuffer.length + alerts.length - MAX_ALERT_BUFFER;
429
+ alertBuffer.splice(0, overflow);
430
+ console.warn(`[audit-security] Alert buffer overflow, dropped ${overflow} oldest alerts`);
431
+ }
432
+ alertBuffer.push(...alerts);
433
+ for (const alert of alerts) {
434
+ const icon = alert.severity === 'critical' ? '🔴' : alert.severity === 'warn' ? '🟡' : 'ℹ️';
435
+ console.log(`[audit-security] ${icon} ${alert.severity.toUpperCase()} ${alert.ruleId}: ${alert.ruleName} — ${alert.finding} (session=${alert.sessionId})`);
436
+ }
437
+ // Flush immediately on CRITICAL alerts
438
+ if (alerts.some(a => a.severity === 'critical')) {
439
+ void flushAlerts();
440
+ }
441
+ }
442
+ }
443
+ catch (err) {
444
+ console.error('[audit-security] Scan error:', err);
445
+ }
446
+ }
447
+ }
448
+ /** Batch-write security alerts */
449
+ async function flushAlerts() {
450
+ if (alertBuffer.length === 0)
451
+ return;
452
+ const batch = alertBuffer.splice(0, alertBuffer.length);
453
+ try {
454
+ await writer.writeAlerts(batch);
455
+ }
456
+ catch (err) {
457
+ console.error('[audit-security] Failed to write alerts:', err);
458
+ // Put back into buffer
459
+ alertBuffer.unshift(...batch);
460
+ }
461
+ }
462
+ /** Safely truncate a string */
463
+ function truncate(s, max = 500) {
464
+ if (typeof s !== 'string')
465
+ return '';
466
+ return s.length > max ? s.substring(0, max) + '…' : s;
467
+ }
468
+ /** Convert bytes to human-readable format */
469
+ function formatBytes(bytes) {
470
+ if (bytes < 1024)
471
+ return `${bytes} B`;
472
+ if (bytes < 1024 * 1024)
473
+ return `${(bytes / 1024).toFixed(1)} KB`;
474
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
475
+ }
476
+ // =====================================================================
477
+ // 1. Agent lifecycle
478
+ // =====================================================================
479
+ // before_model_resolve: before model selection
480
+ api.on('before_model_resolve', (event, ctx) => {
481
+ try {
482
+ const e = event;
483
+ const c = ctx;
484
+ const sid = resolveSessionId(undefined, c?.sessionId);
485
+ const sctx = getSessionCtx(sid);
486
+ if (c?.agentId)
487
+ sctx.userId = c.agentId;
488
+ recordAction(makeAction(sid, {
489
+ actionType: types_1.ActionType.ModelResolve,
490
+ actionName: 'before_model_resolve',
491
+ userId: c?.agentId,
492
+ inputParams: redactor.redact({ prompt: truncate(e.prompt) }),
493
+ }));
494
+ console.log(`[audit-duckdb] before_model_resolve: session=${sid}`);
495
+ }
496
+ catch (err) {
497
+ console.error('[audit-duckdb] Error in before_model_resolve:', err);
498
+ }
499
+ });
500
+ // before_prompt_build: before prompt construction
501
+ api.on('before_prompt_build', (event, ctx) => {
502
+ try {
503
+ const e = event;
504
+ const c = ctx;
505
+ const sid = resolveSessionId(undefined, c?.sessionId);
506
+ recordAction(makeAction(sid, {
507
+ actionType: types_1.ActionType.PromptBuild,
508
+ actionName: 'before_prompt_build',
509
+ userId: c?.agentId,
510
+ inputParams: redactor.redact({
511
+ prompt: truncate(e.prompt),
512
+ messageCount: Array.isArray(e.messages) ? e.messages.length : 0,
513
+ }),
514
+ }));
515
+ console.log(`[audit-duckdb] before_prompt_build: session=${sid} msgs=${Array.isArray(e.messages) ? e.messages.length : '?'}`);
516
+ }
517
+ catch (err) {
518
+ console.error('[audit-duckdb] Error in before_prompt_build:', err);
519
+ }
520
+ });
521
+ // before_agent_start: skipped — legacy hook that fires twice
522
+ // and fully overlaps with before_model_resolve + before_prompt_build
523
+ // agent_end: Agent run finished
524
+ api.on('agent_end', (event, ctx) => {
525
+ try {
526
+ const e = event;
527
+ const c = ctx;
528
+ const sid = resolveSessionId(undefined, c?.sessionId);
529
+ recordAction(makeAction(sid, {
530
+ actionType: types_1.ActionType.AgentEnd,
531
+ actionName: 'agent_end',
532
+ userId: c?.agentId,
533
+ outputResult: redactor.redact({
534
+ success: e.success,
535
+ error: e.error,
536
+ messageCount: Array.isArray(e.messages) ? e.messages.length : 0,
537
+ }),
538
+ durationMs: e.durationMs ?? null,
539
+ }));
540
+ console.log(`[audit-duckdb] agent_end: session=${sid} success=${e.success} duration=${e.durationMs}ms`);
541
+ }
542
+ catch (err) {
543
+ console.error('[audit-duckdb] Error in agent_end:', err);
544
+ }
545
+ });
546
+ // =====================================================================
547
+ // 2. LLM calls
548
+ // =====================================================================
549
+ // llm_input: before LLM call — record start time + cache user input
550
+ api.on('llm_input', (event, ctx) => {
551
+ try {
552
+ const e = event;
553
+ const c = ctx;
554
+ const sid = resolveSessionId(e.sessionId, c?.sessionId);
555
+ const sctx = getSessionCtx(sid);
556
+ // Parse image/media metadata from historyMessages
557
+ const media = extractMediaMeta(e.historyMessages ?? []);
558
+ if (e.runId) {
559
+ runStartTimes.set(e.runId, Date.now());
560
+ runInputs.set(e.runId, {
561
+ prompt: e.prompt,
562
+ imagesCount: e.imagesCount,
563
+ media: media.length > 0 ? media : undefined,
564
+ });
565
+ }
566
+ if (c?.agentId)
567
+ sctx.userId = c.agentId;
568
+ sctx.modelName = `${e.provider}/${e.model}`;
569
+ console.log(`[audit-duckdb] llm_input: session=${sid} model=${e.provider}/${e.model} runId=${e.runId} images=${e.imagesCount ?? 0} mediaParts=${media.length}`);
570
+ }
571
+ catch (err) {
572
+ console.error('[audit-duckdb] Error in llm_input:', err);
573
+ }
574
+ });
575
+ // llm_output: after LLM call — primary audit record point
576
+ api.on('llm_output', (event, ctx) => {
577
+ try {
578
+ const e = event;
579
+ const c = ctx;
580
+ // Calculate duration
581
+ let durationMs = null;
582
+ if (e.runId && runStartTimes.has(e.runId)) {
583
+ durationMs = Date.now() - runStartTimes.get(e.runId);
584
+ runStartTimes.delete(e.runId);
585
+ }
586
+ // Retrieve cached user input
587
+ const cachedInput = e.runId ? runInputs.get(e.runId) : undefined;
588
+ if (e.runId)
589
+ runInputs.delete(e.runId);
590
+ // Token usage: multiple extraction strategies
591
+ const sid = resolveSessionId(e.sessionId, c?.sessionId);
592
+ let promptTokens = e.usage?.input ?? null;
593
+ let completionTokens = e.usage?.output ?? null;
594
+ let cacheRead = e.usage?.cacheRead ?? null;
595
+ let cacheWrite = e.usage?.cacheWrite ?? null;
596
+ // Strategy 2: extract from lastAssistant message (OpenClaw stores usage in assistant message)
597
+ if (promptTokens === null && completionTokens === null && e.lastAssistant) {
598
+ const la = e.lastAssistant;
599
+ const rawUsage = la.usage;
600
+ if (rawUsage) {
601
+ const inp = rawUsage.input ?? rawUsage.inputTokens ?? rawUsage.prompt_tokens ?? rawUsage.promptTokens;
602
+ const out = rawUsage.output ?? rawUsage.outputTokens ?? rawUsage.completion_tokens ?? rawUsage.completionTokens;
603
+ if (typeof inp === 'number' && inp > 0)
604
+ promptTokens = inp;
605
+ if (typeof out === 'number' && out > 0)
606
+ completionTokens = out;
607
+ if (typeof rawUsage.cacheRead === 'number')
608
+ cacheRead = rawUsage.cacheRead;
609
+ if (typeof rawUsage.cacheWrite === 'number')
610
+ cacheWrite = rawUsage.cacheWrite;
611
+ }
612
+ }
613
+ // Strategy 3: SSE usage captured by fetch interceptor (stream_options injection)
614
+ if (promptTokens === null && completionTokens === null) {
615
+ const sseUsage = popLatestSseUsage();
616
+ if (sseUsage) {
617
+ promptTokens = sseUsage.input || null;
618
+ completionTokens = sseUsage.output || null;
619
+ cacheRead = sseUsage.cacheRead || null;
620
+ cacheWrite = sseUsage.cacheWrite || null;
621
+ }
622
+ }
623
+ const totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
624
+ const modelName = `${e.provider}/${e.model}`;
625
+ // Build inputParams with media metadata
626
+ const inputData = {
627
+ userMessage: cachedInput?.prompt ?? '',
628
+ imagesCount: cachedInput?.imagesCount ?? 0,
629
+ };
630
+ if (cachedInput?.media && cachedInput.media.length > 0) {
631
+ inputData.media = cachedInput.media.map((m) => ({
632
+ mimeType: m.mimeType,
633
+ sizeBytes: m.sizeBytes,
634
+ sizeHuman: formatBytes(m.sizeBytes),
635
+ source: m.source,
636
+ }));
637
+ inputData.mediaTotalBytes = cachedInput.media.reduce((sum, m) => sum + m.sizeBytes, 0);
638
+ }
639
+ const sctx = getSessionCtx(sid);
640
+ if (c?.agentId)
641
+ sctx.userId = c.agentId;
642
+ sctx.modelName = modelName;
643
+ recordAction(makeAction(sid, {
644
+ actionType: types_1.ActionType.Message,
645
+ actionName: `llm_call:${modelName}`,
646
+ modelName,
647
+ inputParams: redactor.redact(inputData),
648
+ outputResult: redactor.redact({
649
+ assistantTexts: e.assistantTexts ?? [],
650
+ }),
651
+ promptTokens,
652
+ completionTokens,
653
+ durationMs,
654
+ userId: c?.agentId,
655
+ }), totalTokens);
656
+ console.log(`[audit-duckdb] llm_output: session=${e.sessionId} model=${e.model} duration=${durationMs}ms tokens=${promptTokens ?? '?'}/${completionTokens ?? '?'} cache_r=${cacheRead ?? '-'} cache_w=${cacheWrite ?? '-'}`);
657
+ }
658
+ catch (err) {
659
+ console.error('[audit-duckdb] Error in llm_output:', err);
660
+ }
661
+ });
662
+ // =====================================================================
663
+ // 3. Context management
664
+ // =====================================================================
665
+ // before_compaction: before context compaction
666
+ api.on('before_compaction', (event, ctx) => {
667
+ try {
668
+ const e = event;
669
+ const c = ctx;
670
+ const sid = resolveSessionId(undefined, c?.sessionId);
671
+ recordAction(makeAction(sid, {
672
+ actionType: types_1.ActionType.Compaction,
673
+ actionName: 'before_compaction',
674
+ userId: c?.agentId,
675
+ inputParams: redactor.redact({
676
+ messageCount: e.messageCount,
677
+ compactingCount: e.compactingCount,
678
+ tokenCount: e.tokenCount,
679
+ }),
680
+ }));
681
+ console.log(`[audit-duckdb] before_compaction: session=${sid} msgs=${e.messageCount} tokens=${e.tokenCount}`);
682
+ }
683
+ catch (err) {
684
+ console.error('[audit-duckdb] Error in before_compaction:', err);
685
+ }
686
+ });
687
+ // after_compaction: after context compaction
688
+ api.on('after_compaction', (event, ctx) => {
689
+ try {
690
+ const e = event;
691
+ const c = ctx;
692
+ const sid = resolveSessionId(undefined, c?.sessionId);
693
+ recordAction(makeAction(sid, {
694
+ actionType: types_1.ActionType.Compaction,
695
+ actionName: 'after_compaction',
696
+ userId: c?.agentId,
697
+ outputResult: redactor.redact({
698
+ messageCount: e.messageCount,
699
+ compactedCount: e.compactedCount,
700
+ tokenCount: e.tokenCount,
701
+ }),
702
+ }));
703
+ console.log(`[audit-duckdb] after_compaction: session=${sid} compacted=${e.compactedCount}`);
704
+ }
705
+ catch (err) {
706
+ console.error('[audit-duckdb] Error in after_compaction:', err);
707
+ }
708
+ });
709
+ // before_reset: before session reset
710
+ api.on('before_reset', (event, ctx) => {
711
+ try {
712
+ const e = event;
713
+ const c = ctx;
714
+ const sid = resolveSessionId(undefined, c?.sessionId);
715
+ recordAction(makeAction(sid, {
716
+ actionType: types_1.ActionType.Reset,
717
+ actionName: 'before_reset',
718
+ userId: c?.agentId,
719
+ inputParams: redactor.redact({
720
+ reason: e.reason,
721
+ messageCount: Array.isArray(e.messages) ? e.messages.length : 0,
722
+ }),
723
+ }));
724
+ console.log(`[audit-duckdb] before_reset: session=${sid} reason=${e.reason}`);
725
+ }
726
+ catch (err) {
727
+ console.error('[audit-duckdb] Error in before_reset:', err);
728
+ }
729
+ });
730
+ // =====================================================================
731
+ // 4. Message channel
732
+ // =====================================================================
733
+ // message_received: user message arrived
734
+ api.on('message_received', (event, ctx) => {
735
+ try {
736
+ const e = event;
737
+ const c = ctx;
738
+ const sid = lastFallbackSessionId;
739
+ if (sid === 'unknown')
740
+ return; // No valid session yet, skip (content will be recorded in llm_input)
741
+ recordAction(makeAction(sid, {
742
+ actionType: types_1.ActionType.UserMessage,
743
+ actionName: 'message_received',
744
+ modelName: '',
745
+ inputParams: redactor.redact({
746
+ from: e.from,
747
+ content: e.content,
748
+ channelId: c?.channelId,
749
+ metadata: e.metadata,
750
+ }),
751
+ createdAt: new Date(e.timestamp || Date.now()),
752
+ }));
753
+ console.log(`[audit-duckdb] message_received: from=${e.from} channel=${c?.channelId} len=${e.content?.length}`);
754
+ }
755
+ catch (err) {
756
+ console.error('[audit-duckdb] Error in message_received:', err);
757
+ }
758
+ });
759
+ // message_sending: before message send (interceptable/modifiable)
760
+ api.on('message_sending', (event, ctx) => {
761
+ try {
762
+ const e = event;
763
+ const c = ctx;
764
+ const sid = lastFallbackSessionId;
765
+ if (sid === 'unknown')
766
+ return;
767
+ recordAction(makeAction(sid, {
768
+ actionType: types_1.ActionType.MsgSending,
769
+ actionName: 'message_sending',
770
+ modelName: '',
771
+ inputParams: redactor.redact({
772
+ to: e.to,
773
+ content: truncate(e.content, 1000),
774
+ channelId: c?.channelId,
775
+ }),
776
+ }));
777
+ console.log(`[audit-duckdb] message_sending: to=${e.to} channel=${c?.channelId}`);
778
+ }
779
+ catch (err) {
780
+ console.error('[audit-duckdb] Error in message_sending:', err);
781
+ }
782
+ });
783
+ // message_sent: message sent
784
+ api.on('message_sent', (event, ctx) => {
785
+ try {
786
+ const e = event;
787
+ const c = ctx;
788
+ const sid = lastFallbackSessionId;
789
+ if (sid === 'unknown')
790
+ return;
791
+ recordAction(makeAction(sid, {
792
+ actionType: types_1.ActionType.AssistantMsg,
793
+ actionName: 'message_sent',
794
+ modelName: '',
795
+ outputResult: redactor.redact({
796
+ to: e.to,
797
+ content: e.content,
798
+ success: e.success,
799
+ error: e.error,
800
+ channelId: c?.channelId,
801
+ }),
802
+ }));
803
+ console.log(`[audit-duckdb] message_sent: to=${e.to} success=${e.success} channel=${c?.channelId}`);
804
+ }
805
+ catch (err) {
806
+ console.error('[audit-duckdb] Error in message_sent:', err);
807
+ }
808
+ });
809
+ // before_message_write: skipped — message content already fully recorded in llm_input/llm_output, no additional audit value
810
+ // =====================================================================
811
+ // 5. Tool calls
812
+ // =====================================================================
813
+ // before_tool_call: before tool call — record start time
814
+ api.on('before_tool_call', (event, ctx) => {
815
+ try {
816
+ const e = event;
817
+ const c = ctx;
818
+ // Use toolCallId as timing key to avoid collision with llm_input runId
819
+ // Note: cannot use Date.now() as fallback, otherwise before and after generate inconsistent keys
820
+ const callId = e.toolCallId || c?.toolCallId || '';
821
+ const timingKey = callId ? `tc:${callId}` : `tc:${e.toolName}`;
822
+ runStartTimes.set(timingKey, Date.now());
823
+ // Update per-session context
824
+ const sid = resolveSessionId(undefined, c?.sessionId);
825
+ const sctx = getSessionCtx(sid);
826
+ if (c?.agentId)
827
+ sctx.userId = c.agentId;
828
+ console.log(`[audit-duckdb] before_tool_call: tool=${e.toolName} session=${sid} callId=${callId}`);
829
+ }
830
+ catch (err) {
831
+ console.error('[audit-duckdb] Error in before_tool_call:', err);
832
+ }
833
+ });
834
+ // after_tool_call: after tool call — generate audit record
835
+ api.on('after_tool_call', (event, ctx) => {
836
+ try {
837
+ const e = event;
838
+ const c = ctx;
839
+ const toolName = e.toolName;
840
+ // Get sessionId from ctx (event does not have this field)
841
+ const sessionId = resolveSessionId(undefined, c?.sessionId);
842
+ // Calculate duration — use toolCallId as key to not interfere with LLM runId timing
843
+ const callId = e.toolCallId || c?.toolCallId || '';
844
+ const timingKey = callId ? `tc:${callId}` : `tc:${e.toolName}`;
845
+ let durationMs = e.durationMs ?? null;
846
+ if (durationMs === null && runStartTimes.has(timingKey)) {
847
+ durationMs = Date.now() - runStartTimes.get(timingKey);
848
+ }
849
+ runStartTimes.delete(timingKey);
850
+ // BUG FIX: use correct field name params (not parameters/input/args)
851
+ const inputParams = e.params ? redactor.redact(e.params) : null;
852
+ // Detect image content in tool results
853
+ let outputData = null;
854
+ if (e.error) {
855
+ outputData = redactor.redact({ error: e.error });
856
+ }
857
+ else if (e.result !== undefined) {
858
+ const raw = typeof e.result === 'object' && e.result !== null
859
+ ? e.result
860
+ : { value: e.result };
861
+ // If tool result contains content array, check for images
862
+ const resultContent = raw.content;
863
+ if (Array.isArray(resultContent)) {
864
+ const toolMedia = [];
865
+ for (const part of resultContent) {
866
+ if (part && typeof part === 'object' && part.type === 'image' && typeof part.data === 'string') {
867
+ toolMedia.push({
868
+ mimeType: part.mimeType || 'unknown',
869
+ sizeBytes: Math.round(part.data.length * 0.75),
870
+ source: 'tool',
871
+ });
872
+ }
873
+ }
874
+ if (toolMedia.length > 0) {
875
+ // Don't write full base64 to audit record, only record metadata
876
+ const sanitized = { ...raw };
877
+ sanitized.content = resultContent.map((part) => {
878
+ if (part && typeof part === 'object' && part.type === 'image') {
879
+ return {
880
+ type: 'image',
881
+ mimeType: part.mimeType || 'unknown',
882
+ sizeBytes: Math.round((part.data?.length ?? 0) * 0.75),
883
+ sizeHuman: formatBytes(Math.round((part.data?.length ?? 0) * 0.75)),
884
+ data: '[base64 omitted]',
885
+ };
886
+ }
887
+ return part;
888
+ });
889
+ sanitized._mediaCount = toolMedia.length;
890
+ sanitized._mediaTotalBytes = toolMedia.reduce((s, m) => s + m.sizeBytes, 0);
891
+ outputData = redactor.redact(sanitized);
892
+ }
893
+ else {
894
+ outputData = redactor.redact(raw);
895
+ }
896
+ }
897
+ else {
898
+ outputData = redactor.redact(raw);
899
+ }
900
+ }
901
+ recordAction(makeAction(sessionId, {
902
+ actionType: types_1.ActionType.ToolCall,
903
+ actionName: `tool_call:${toolName}`,
904
+ inputParams,
905
+ outputResult: outputData,
906
+ durationMs,
907
+ userId: c?.agentId,
908
+ }));
909
+ console.log(`[audit-duckdb] after_tool_call: tool=${toolName} session=${sessionId} duration=${durationMs}ms`);
910
+ }
911
+ catch (err) {
912
+ console.error('[audit-duckdb] Error in after_tool_call:', err);
913
+ }
914
+ });
915
+ // tool_result_persist: tool result persistence (sync hook)
916
+ api.on('tool_result_persist', (event, ctx) => {
917
+ try {
918
+ const e = event;
919
+ const c = ctx;
920
+ const name = e.toolName || c?.toolName || 'unknown';
921
+ const sid = lastFallbackSessionId;
922
+ if (sid === 'unknown')
923
+ return;
924
+ recordAction(makeAction(sid, {
925
+ actionType: types_1.ActionType.ToolPersist,
926
+ actionName: `tool_persist:${name}`,
927
+ inputParams: redactor.redact({
928
+ toolName: name,
929
+ toolCallId: e.toolCallId,
930
+ isSynthetic: e.isSynthetic,
931
+ }),
932
+ }));
933
+ }
934
+ catch (err) {
935
+ console.error('[audit-duckdb] Error in tool_result_persist:', err);
936
+ }
937
+ });
938
+ // =====================================================================
939
+ // 6. Session lifecycle
940
+ // =====================================================================
941
+ // session_start: session started
942
+ api.on('session_start', (event, ctx) => {
943
+ try {
944
+ const e = event;
945
+ const c = ctx;
946
+ const sid = resolveSessionId(e.sessionId, c?.sessionId);
947
+ const sctx = getSessionCtx(sid);
948
+ if (c?.agentId)
949
+ sctx.userId = c.agentId;
950
+ recordAction(makeAction(sid, {
951
+ actionType: types_1.ActionType.SessionStart,
952
+ actionName: 'session_start',
953
+ userId: c?.agentId,
954
+ inputParams: redactor.redact({
955
+ resumedFrom: e.resumedFrom,
956
+ }),
957
+ }));
958
+ console.log(`[audit-duckdb] session_start: session=${sid} resumed=${e.resumedFrom}`);
959
+ }
960
+ catch (err) {
961
+ console.error('[audit-duckdb] Error in session_start:', err);
962
+ }
963
+ });
964
+ // session_end: session ended — force flush
965
+ api.on('session_end', (event, ctx) => {
966
+ try {
967
+ const e = event;
968
+ const c = ctx;
969
+ const sid = resolveSessionId(e.sessionId, c?.sessionId);
970
+ recordAction(makeAction(sid, {
971
+ actionType: types_1.ActionType.SessionEnd,
972
+ actionName: 'session_end',
973
+ userId: c?.agentId,
974
+ outputResult: redactor.redact({
975
+ messageCount: e.messageCount,
976
+ }),
977
+ durationMs: e.durationMs ?? null,
978
+ }));
979
+ // Force flush to ensure session data is not lost
980
+ void buffer.flush();
981
+ // Clean up session context
982
+ sessionContextMap.delete(sid);
983
+ console.log(`[audit-duckdb] session_end: session=${sid} msgs=${e.messageCount} duration=${e.durationMs}ms`);
984
+ }
985
+ catch (err) {
986
+ console.error('[audit-duckdb] Error in session_end:', err);
987
+ }
988
+ });
989
+ // =====================================================================
990
+ // 7. Sub-agents
991
+ // =====================================================================
992
+ // subagent_spawned: sub-agent created
993
+ api.on('subagent_spawned', (event, ctx) => {
994
+ try {
995
+ const e = event;
996
+ const c = ctx;
997
+ const sid = lastFallbackSessionId;
998
+ if (sid === 'unknown')
999
+ return;
1000
+ recordAction(makeAction(sid, {
1001
+ actionType: types_1.ActionType.SubagentSpawn,
1002
+ actionName: `subagent_spawned:${e.agentId}`,
1003
+ inputParams: redactor.redact({
1004
+ childSessionKey: e.childSessionKey,
1005
+ agentId: e.agentId,
1006
+ label: e.label,
1007
+ mode: e.mode,
1008
+ runId: e.runId,
1009
+ threadRequested: e.threadRequested,
1010
+ requesterSessionKey: c?.requesterSessionKey,
1011
+ }),
1012
+ }));
1013
+ console.log(`[audit-duckdb] subagent_spawned: agent=${e.agentId} child=${e.childSessionKey}`);
1014
+ }
1015
+ catch (err) {
1016
+ console.error('[audit-duckdb] Error in subagent_spawned:', err);
1017
+ }
1018
+ });
1019
+ // subagent_ended: sub-agent ended
1020
+ api.on('subagent_ended', (event, ctx) => {
1021
+ try {
1022
+ const e = event;
1023
+ const sid = lastFallbackSessionId;
1024
+ if (sid === 'unknown')
1025
+ return;
1026
+ recordAction(makeAction(sid, {
1027
+ actionType: types_1.ActionType.SubagentEnd,
1028
+ actionName: `subagent_ended:${e.targetKind}`,
1029
+ outputResult: redactor.redact({
1030
+ targetSessionKey: e.targetSessionKey,
1031
+ reason: e.reason,
1032
+ outcome: e.outcome,
1033
+ error: e.error,
1034
+ }),
1035
+ }));
1036
+ console.log(`[audit-duckdb] subagent_ended: target=${e.targetSessionKey} outcome=${e.outcome}`);
1037
+ }
1038
+ catch (err) {
1039
+ console.error('[audit-duckdb] Error in subagent_ended:', err);
1040
+ }
1041
+ });
1042
+ // =====================================================================
1043
+ // 8. Gateway
1044
+ // =====================================================================
1045
+ // gateway_start: gateway started
1046
+ api.on('gateway_start', (event) => {
1047
+ try {
1048
+ const e = event;
1049
+ recordAction(makeAction('system', {
1050
+ actionType: types_1.ActionType.GatewayStart,
1051
+ actionName: 'gateway_start',
1052
+ modelName: '',
1053
+ userId: 'system',
1054
+ inputParams: { port: e.port },
1055
+ }));
1056
+ console.log(`[audit-duckdb] gateway_start: port=${e.port}`);
1057
+ }
1058
+ catch (err) {
1059
+ console.error('[audit-duckdb] Error in gateway_start:', err);
1060
+ }
1061
+ });
1062
+ // gateway_stop: gateway stopped
1063
+ api.on('gateway_stop', (event) => {
1064
+ try {
1065
+ const e = event;
1066
+ recordAction(makeAction('system', {
1067
+ actionType: types_1.ActionType.GatewayStop,
1068
+ actionName: 'gateway_stop',
1069
+ modelName: '',
1070
+ userId: 'system',
1071
+ outputResult: { reason: e.reason },
1072
+ }));
1073
+ // Force flush on gateway stop
1074
+ void buffer.flush();
1075
+ console.log(`[audit-duckdb] gateway_stop: reason=${e.reason}`);
1076
+ }
1077
+ catch (err) {
1078
+ console.error('[audit-duckdb] Error in gateway_stop:', err);
1079
+ }
1080
+ });
1081
+ // =====================================================================
1082
+ // Return deactivate function for cleanup
1083
+ // =====================================================================
1084
+ return {
1085
+ deactivate: async () => {
1086
+ console.log('[audit-duckdb] Deactivating plugin...');
1087
+ // Stop timers
1088
+ if (alertFlushTimer) {
1089
+ clearInterval(alertFlushTimer);
1090
+ alertFlushTimer = null;
1091
+ }
1092
+ if (mapCleanupTimer) {
1093
+ clearInterval(mapCleanupTimer);
1094
+ mapCleanupTimer = null;
1095
+ }
1096
+ await flushAlerts();
1097
+ await buffer.shutdown();
1098
+ await writer.close();
1099
+ // Uninstall fetch interceptor
1100
+ uninstallFetchInterceptor();
1101
+ runStartTimes.clear();
1102
+ runInputs.clear();
1103
+ sseUsageCache.clear();
1104
+ latestSseUsage = null;
1105
+ sessionStatsMap.clear();
1106
+ sessionContextMap.clear();
1107
+ securityScanner.reset();
1108
+ lastFallbackSessionId = 'unknown';
1109
+ console.log('[audit-duckdb] Plugin deactivated');
1110
+ },
1111
+ };
1112
+ }
1113
+ exports.default = activate;
1114
+ //# sourceMappingURL=index.js.map