@agentlensai/server 0.2.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/agents-stats.test.d.ts +5 -0
  3. package/dist/__tests__/agents-stats.test.d.ts.map +1 -0
  4. package/dist/__tests__/agents-stats.test.js +134 -0
  5. package/dist/__tests__/agents-stats.test.js.map +1 -0
  6. package/dist/__tests__/api-keys.test.d.ts +5 -0
  7. package/dist/__tests__/api-keys.test.d.ts.map +1 -0
  8. package/dist/__tests__/api-keys.test.js +118 -0
  9. package/dist/__tests__/api-keys.test.js.map +1 -0
  10. package/dist/__tests__/auth-no-db.test.d.ts +2 -0
  11. package/dist/__tests__/auth-no-db.test.d.ts.map +1 -0
  12. package/dist/__tests__/auth-no-db.test.js +43 -0
  13. package/dist/__tests__/auth-no-db.test.js.map +1 -0
  14. package/dist/__tests__/auth.test.d.ts +5 -0
  15. package/dist/__tests__/auth.test.d.ts.map +1 -0
  16. package/dist/__tests__/auth.test.js +86 -0
  17. package/dist/__tests__/auth.test.js.map +1 -0
  18. package/dist/__tests__/config.test.d.ts +2 -0
  19. package/dist/__tests__/config.test.d.ts.map +1 -0
  20. package/dist/__tests__/config.test.js +37 -0
  21. package/dist/__tests__/config.test.js.map +1 -0
  22. package/dist/__tests__/events-ingest.test.d.ts +5 -0
  23. package/dist/__tests__/events-ingest.test.d.ts.map +1 -0
  24. package/dist/__tests__/events-ingest.test.js +248 -0
  25. package/dist/__tests__/events-ingest.test.js.map +1 -0
  26. package/dist/__tests__/events-query.test.d.ts +5 -0
  27. package/dist/__tests__/events-query.test.d.ts.map +1 -0
  28. package/dist/__tests__/events-query.test.js +205 -0
  29. package/dist/__tests__/events-query.test.js.map +1 -0
  30. package/dist/__tests__/health.test.d.ts +5 -0
  31. package/dist/__tests__/health.test.d.ts.map +1 -0
  32. package/dist/__tests__/health.test.js +40 -0
  33. package/dist/__tests__/health.test.js.map +1 -0
  34. package/dist/__tests__/sessions.test.d.ts +5 -0
  35. package/dist/__tests__/sessions.test.d.ts.map +1 -0
  36. package/dist/__tests__/sessions.test.js +176 -0
  37. package/dist/__tests__/sessions.test.js.map +1 -0
  38. package/dist/__tests__/test-helpers.d.ts +24 -0
  39. package/dist/__tests__/test-helpers.d.ts.map +1 -0
  40. package/dist/__tests__/test-helpers.js +45 -0
  41. package/dist/__tests__/test-helpers.js.map +1 -0
  42. package/dist/config.d.ts +20 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +20 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/db/__tests__/init.test.d.ts +2 -0
  47. package/dist/db/__tests__/init.test.d.ts.map +1 -0
  48. package/dist/db/__tests__/init.test.js +73 -0
  49. package/dist/db/__tests__/init.test.js.map +1 -0
  50. package/dist/db/__tests__/sqlite-store-read.test.d.ts +2 -0
  51. package/dist/db/__tests__/sqlite-store-read.test.d.ts.map +1 -0
  52. package/dist/db/__tests__/sqlite-store-read.test.js +749 -0
  53. package/dist/db/__tests__/sqlite-store-read.test.js.map +1 -0
  54. package/dist/db/__tests__/sqlite-store-write.test.d.ts +2 -0
  55. package/dist/db/__tests__/sqlite-store-write.test.d.ts.map +1 -0
  56. package/dist/db/__tests__/sqlite-store-write.test.js +418 -0
  57. package/dist/db/__tests__/sqlite-store-write.test.js.map +1 -0
  58. package/dist/db/errors.d.ts +16 -0
  59. package/dist/db/errors.d.ts.map +1 -0
  60. package/dist/db/errors.js +22 -0
  61. package/dist/db/errors.js.map +1 -0
  62. package/dist/db/index.d.ts +33 -0
  63. package/dist/db/index.d.ts.map +1 -0
  64. package/dist/db/index.js +44 -0
  65. package/dist/db/index.js.map +1 -0
  66. package/dist/db/migrate.d.ts +26 -0
  67. package/dist/db/migrate.d.ts.map +1 -0
  68. package/dist/db/migrate.js +128 -0
  69. package/dist/db/migrate.js.map +1 -0
  70. package/dist/db/schema.sqlite.d.ts +1009 -0
  71. package/dist/db/schema.sqlite.d.ts.map +1 -0
  72. package/dist/db/schema.sqlite.js +96 -0
  73. package/dist/db/schema.sqlite.js.map +1 -0
  74. package/dist/db/sqlite-store.d.ts +68 -0
  75. package/dist/db/sqlite-store.d.ts.map +1 -0
  76. package/dist/db/sqlite-store.js +753 -0
  77. package/dist/db/sqlite-store.js.map +1 -0
  78. package/dist/index.d.ts +45 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +182 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/lib/__tests__/retention.test.d.ts +2 -0
  83. package/dist/lib/__tests__/retention.test.d.ts.map +1 -0
  84. package/dist/lib/__tests__/retention.test.js +238 -0
  85. package/dist/lib/__tests__/retention.test.js.map +1 -0
  86. package/dist/lib/retention.d.ts +31 -0
  87. package/dist/lib/retention.d.ts.map +1 -0
  88. package/dist/lib/retention.js +47 -0
  89. package/dist/lib/retention.js.map +1 -0
  90. package/dist/middleware/auth.d.ts +37 -0
  91. package/dist/middleware/auth.d.ts.map +1 -0
  92. package/dist/middleware/auth.js +78 -0
  93. package/dist/middleware/auth.js.map +1 -0
  94. package/dist/routes/agents.d.ts +13 -0
  95. package/dist/routes/agents.d.ts.map +1 -0
  96. package/dist/routes/agents.js +34 -0
  97. package/dist/routes/agents.js.map +1 -0
  98. package/dist/routes/api-keys.d.ts +14 -0
  99. package/dist/routes/api-keys.d.ts.map +1 -0
  100. package/dist/routes/api-keys.js +81 -0
  101. package/dist/routes/api-keys.js.map +1 -0
  102. package/dist/routes/config.d.ts +39 -0
  103. package/dist/routes/config.d.ts.map +1 -0
  104. package/dist/routes/config.js +97 -0
  105. package/dist/routes/config.js.map +1 -0
  106. package/dist/routes/events.d.ts +14 -0
  107. package/dist/routes/events.d.ts.map +1 -0
  108. package/dist/routes/events.js +164 -0
  109. package/dist/routes/events.js.map +1 -0
  110. package/dist/routes/sessions.d.ts +14 -0
  111. package/dist/routes/sessions.d.ts.map +1 -0
  112. package/dist/routes/sessions.js +72 -0
  113. package/dist/routes/sessions.js.map +1 -0
  114. package/dist/routes/stats.d.ts +12 -0
  115. package/dist/routes/stats.d.ts.map +1 -0
  116. package/dist/routes/stats.js +16 -0
  117. package/dist/routes/stats.js.map +1 -0
  118. package/package.json +61 -0
@@ -0,0 +1,753 @@
1
+ /**
2
+ * SQLite implementation of IEventStore.
3
+ *
4
+ * Write operations (Story 3.4):
5
+ * - insertEvents() — batch insert in single transaction
6
+ * - Auto-create/update session records on event insert
7
+ * - Auto-create agent records on first event
8
+ * - Session status management
9
+ *
10
+ * Read operations (Story 3.5):
11
+ * - queryEvents(), getEvent(), getSessionTimeline(), countEvents()
12
+ * - querySessions(), getSession()
13
+ * - listAgents(), getAgent()
14
+ * - getAnalytics(), getStats()
15
+ */
16
+ import { eq, and, gte, lte, inArray, desc, asc, sql, like, count as drizzleCount } from 'drizzle-orm';
17
+ import { computeEventHash } from '@agentlens/core';
18
+ import { events, sessions, agents, alertRules } from './schema.sqlite.js';
19
+ import { HashChainError, NotFoundError } from './errors.js';
20
+ // ─── Helpers ───────────────────────────────────────────────
21
+ /**
22
+ * Safe JSON.parse that returns a fallback on any error instead of throwing.
23
+ */
24
+ export function safeJsonParse(raw, fallback) {
25
+ try {
26
+ return JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return fallback;
30
+ }
31
+ }
32
+ export class SqliteEventStore {
33
+ db;
34
+ constructor(db) {
35
+ this.db = db;
36
+ }
37
+ // ─── Events — Write ────────────────────────────────────────
38
+ async insertEvents(eventList) {
39
+ if (eventList.length === 0)
40
+ return;
41
+ // Batch insert in a single transaction for atomicity and performance
42
+ this.db.transaction((tx) => {
43
+ // CRITICAL 3: Validate hash chain before inserting
44
+ const firstEvent = eventList[0];
45
+ // Check if the first event already exists (idempotent re-insert)
46
+ const existingFirst = tx
47
+ .select({ id: events.id })
48
+ .from(events)
49
+ .where(eq(events.id, firstEvent.id))
50
+ .get();
51
+ if (!existingFirst) {
52
+ // Fresh insert — validate chain continuity against stored events
53
+ const lastStoredEvent = tx
54
+ .select({ hash: events.hash })
55
+ .from(events)
56
+ .where(eq(events.sessionId, firstEvent.sessionId))
57
+ .orderBy(desc(events.timestamp), desc(events.id))
58
+ .limit(1)
59
+ .get();
60
+ const lastStoredHash = lastStoredEvent?.hash ?? null;
61
+ // Validate that the first event's prevHash matches the latest stored hash
62
+ if (firstEvent.prevHash !== lastStoredHash) {
63
+ throw new HashChainError(`Chain continuity broken: event ${firstEvent.id} has prevHash=${firstEvent.prevHash} but last stored hash is ${lastStoredHash}`);
64
+ }
65
+ }
66
+ // Validate each event's hash and intra-batch chain links
67
+ for (let i = 0; i < eventList.length; i++) {
68
+ const event = eventList[i];
69
+ // Verify intra-batch chain continuity (for events after the first)
70
+ if (i > 0) {
71
+ const prevEvent = eventList[i - 1];
72
+ if (event.prevHash !== prevEvent.hash) {
73
+ throw new HashChainError(`Chain continuity broken within batch: event ${event.id} has prevHash=${event.prevHash} but previous event hash is ${prevEvent.hash}`);
74
+ }
75
+ }
76
+ // Recompute hash server-side and verify
77
+ const recomputedHash = computeEventHash({
78
+ id: event.id,
79
+ timestamp: event.timestamp,
80
+ sessionId: event.sessionId,
81
+ agentId: event.agentId,
82
+ eventType: event.eventType,
83
+ severity: event.severity,
84
+ payload: event.payload,
85
+ metadata: event.metadata,
86
+ prevHash: event.prevHash,
87
+ });
88
+ if (event.hash !== recomputedHash) {
89
+ throw new HashChainError(`Hash mismatch for event ${event.id}: supplied=${event.hash}, computed=${recomputedHash}`);
90
+ }
91
+ }
92
+ for (const event of eventList) {
93
+ // CRITICAL 4: Idempotent event insertion — ON CONFLICT DO NOTHING
94
+ tx.insert(events)
95
+ .values({
96
+ id: event.id,
97
+ timestamp: event.timestamp,
98
+ sessionId: event.sessionId,
99
+ agentId: event.agentId,
100
+ eventType: event.eventType,
101
+ severity: event.severity,
102
+ payload: JSON.stringify(event.payload),
103
+ metadata: JSON.stringify(event.metadata),
104
+ prevHash: event.prevHash,
105
+ hash: event.hash,
106
+ })
107
+ .onConflictDoNothing({ target: events.id })
108
+ .run();
109
+ // Handle session management
110
+ this._handleSessionUpdate(tx, event);
111
+ // Handle agent auto-creation
112
+ this._handleAgentUpsert(tx, event);
113
+ }
114
+ });
115
+ }
116
+ _handleSessionUpdate(tx, event) {
117
+ if (event.eventType === 'session_started') {
118
+ // Create new session
119
+ const payload = event.payload;
120
+ const tags = payload.tags ?? [];
121
+ const agentName = payload.agentName ?? undefined;
122
+ // CRITICAL 4: Use INSERT ... ON CONFLICT DO UPDATE for sessions
123
+ tx.insert(sessions)
124
+ .values({
125
+ id: event.sessionId,
126
+ agentId: event.agentId,
127
+ agentName: agentName,
128
+ startedAt: event.timestamp,
129
+ status: 'active',
130
+ eventCount: 1,
131
+ toolCallCount: 0,
132
+ errorCount: 0,
133
+ totalCostUsd: 0,
134
+ tags: JSON.stringify(tags),
135
+ })
136
+ .onConflictDoUpdate({
137
+ target: sessions.id,
138
+ set: {
139
+ agentName: agentName ?? sql `coalesce(${sessions.agentName}, NULL)`,
140
+ status: 'active',
141
+ eventCount: sql `${sessions.eventCount} + 1`,
142
+ tags: tags.length > 0 ? JSON.stringify(tags) : sql `${sessions.tags}`,
143
+ },
144
+ })
145
+ .run();
146
+ return;
147
+ }
148
+ // For non-session_started events, ensure a session exists first (upsert)
149
+ tx.insert(sessions)
150
+ .values({
151
+ id: event.sessionId,
152
+ agentId: event.agentId,
153
+ startedAt: event.timestamp,
154
+ status: 'active',
155
+ eventCount: 0,
156
+ toolCallCount: 0,
157
+ errorCount: 0,
158
+ totalCostUsd: 0,
159
+ tags: '[]',
160
+ })
161
+ .onConflictDoNothing({ target: sessions.id })
162
+ .run();
163
+ // Build incremental updates
164
+ const isToolCall = event.eventType === 'tool_call';
165
+ const isError = event.severity === 'error' ||
166
+ event.severity === 'critical' ||
167
+ event.eventType === 'tool_error';
168
+ const isCost = event.eventType === 'cost_tracked';
169
+ const costPayload = event.payload;
170
+ const costUsd = isCost ? (Number(costPayload.costUsd) || 0) : 0;
171
+ if (event.eventType === 'session_ended') {
172
+ const payload = event.payload;
173
+ const reason = payload.reason;
174
+ const status = reason === 'error' ? 'error' : 'completed';
175
+ tx.update(sessions)
176
+ .set({
177
+ endedAt: event.timestamp,
178
+ status: status,
179
+ eventCount: sql `${sessions.eventCount} + 1`,
180
+ errorCount: isError
181
+ ? sql `${sessions.errorCount} + 1`
182
+ : sessions.errorCount,
183
+ })
184
+ .where(eq(sessions.id, event.sessionId))
185
+ .run();
186
+ return;
187
+ }
188
+ // Regular event — increment counters
189
+ tx.update(sessions)
190
+ .set({
191
+ eventCount: sql `${sessions.eventCount} + 1`,
192
+ toolCallCount: isToolCall
193
+ ? sql `${sessions.toolCallCount} + 1`
194
+ : sessions.toolCallCount,
195
+ errorCount: isError
196
+ ? sql `${sessions.errorCount} + 1`
197
+ : sessions.errorCount,
198
+ totalCostUsd: isCost
199
+ ? sql `${sessions.totalCostUsd} + ${costUsd}`
200
+ : sessions.totalCostUsd,
201
+ })
202
+ .where(eq(sessions.id, event.sessionId))
203
+ .run();
204
+ }
205
+ _handleAgentUpsert(tx, event) {
206
+ // CRITICAL 4: Use INSERT ... ON CONFLICT DO UPDATE for agents
207
+ const payload = event.payload;
208
+ const agentName = payload.agentName ?? event.agentId;
209
+ tx.insert(agents)
210
+ .values({
211
+ id: event.agentId,
212
+ name: agentName,
213
+ firstSeenAt: event.timestamp,
214
+ lastSeenAt: event.timestamp,
215
+ sessionCount: event.eventType === 'session_started' ? 1 : 0,
216
+ })
217
+ .onConflictDoUpdate({
218
+ target: agents.id,
219
+ set: {
220
+ lastSeenAt: event.timestamp,
221
+ sessionCount: event.eventType === 'session_started'
222
+ ? sql `${agents.sessionCount} + 1`
223
+ : agents.sessionCount,
224
+ },
225
+ })
226
+ .run();
227
+ }
228
+ // ─── Sessions — Write ──────────────────────────────────────
229
+ async upsertSession(session) {
230
+ const existing = this.db
231
+ .select()
232
+ .from(sessions)
233
+ .where(eq(sessions.id, session.id))
234
+ .get();
235
+ if (existing) {
236
+ const updates = {};
237
+ if (session.agentId !== undefined)
238
+ updates.agentId = session.agentId;
239
+ if (session.agentName !== undefined)
240
+ updates.agentName = session.agentName;
241
+ if (session.startedAt !== undefined)
242
+ updates.startedAt = session.startedAt;
243
+ if (session.endedAt !== undefined)
244
+ updates.endedAt = session.endedAt;
245
+ if (session.status !== undefined)
246
+ updates.status = session.status;
247
+ if (session.eventCount !== undefined)
248
+ updates.eventCount = session.eventCount;
249
+ if (session.toolCallCount !== undefined)
250
+ updates.toolCallCount = session.toolCallCount;
251
+ if (session.errorCount !== undefined)
252
+ updates.errorCount = session.errorCount;
253
+ if (session.totalCostUsd !== undefined)
254
+ updates.totalCostUsd = session.totalCostUsd;
255
+ if (session.tags !== undefined)
256
+ updates.tags = JSON.stringify(session.tags);
257
+ if (Object.keys(updates).length > 0) {
258
+ this.db
259
+ .update(sessions)
260
+ .set(updates)
261
+ .where(eq(sessions.id, session.id))
262
+ .run();
263
+ }
264
+ }
265
+ else {
266
+ this.db
267
+ .insert(sessions)
268
+ .values({
269
+ id: session.id,
270
+ agentId: session.agentId ?? '',
271
+ startedAt: session.startedAt ?? new Date().toISOString(),
272
+ status: session.status ?? 'active',
273
+ agentName: session.agentName,
274
+ endedAt: session.endedAt,
275
+ eventCount: session.eventCount ?? 0,
276
+ toolCallCount: session.toolCallCount ?? 0,
277
+ errorCount: session.errorCount ?? 0,
278
+ totalCostUsd: session.totalCostUsd ?? 0,
279
+ tags: JSON.stringify(session.tags ?? []),
280
+ })
281
+ .run();
282
+ }
283
+ }
284
+ // ─── Agents — Write ────────────────────────────────────────
285
+ async upsertAgent(agent) {
286
+ const existing = this.db
287
+ .select()
288
+ .from(agents)
289
+ .where(eq(agents.id, agent.id))
290
+ .get();
291
+ if (existing) {
292
+ const updates = {};
293
+ if (agent.name !== undefined)
294
+ updates.name = agent.name;
295
+ if (agent.description !== undefined)
296
+ updates.description = agent.description;
297
+ if (agent.lastSeenAt !== undefined)
298
+ updates.lastSeenAt = agent.lastSeenAt;
299
+ if (agent.sessionCount !== undefined)
300
+ updates.sessionCount = agent.sessionCount;
301
+ if (Object.keys(updates).length > 0) {
302
+ this.db
303
+ .update(agents)
304
+ .set(updates)
305
+ .where(eq(agents.id, agent.id))
306
+ .run();
307
+ }
308
+ }
309
+ else {
310
+ const now = new Date().toISOString();
311
+ this.db
312
+ .insert(agents)
313
+ .values({
314
+ id: agent.id,
315
+ name: agent.name ?? agent.id,
316
+ description: agent.description,
317
+ firstSeenAt: agent.firstSeenAt ?? now,
318
+ lastSeenAt: agent.lastSeenAt ?? now,
319
+ sessionCount: agent.sessionCount ?? 0,
320
+ })
321
+ .run();
322
+ }
323
+ }
324
+ // ─── Events — Read (Story 3.5 stubs, implemented later) ────
325
+ async queryEvents(query) {
326
+ const limit = Math.min(query.limit ?? 50, 500);
327
+ const offset = query.offset ?? 0;
328
+ const orderDir = query.order === 'asc' ? asc : desc;
329
+ const conditions = this._buildEventConditions(query);
330
+ const rows = this.db
331
+ .select()
332
+ .from(events)
333
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
334
+ .orderBy(orderDir(events.timestamp))
335
+ .limit(limit)
336
+ .offset(offset)
337
+ .all();
338
+ const total = await this.countEvents(query);
339
+ return {
340
+ events: rows.map(this._mapEventRow),
341
+ total,
342
+ hasMore: offset + rows.length < total,
343
+ };
344
+ }
345
+ async getEvent(id) {
346
+ const row = this.db
347
+ .select()
348
+ .from(events)
349
+ .where(eq(events.id, id))
350
+ .get();
351
+ return row ? this._mapEventRow(row) : null;
352
+ }
353
+ async getSessionTimeline(sessionId) {
354
+ const rows = this.db
355
+ .select()
356
+ .from(events)
357
+ .where(eq(events.sessionId, sessionId))
358
+ .orderBy(asc(events.timestamp))
359
+ .all();
360
+ return rows.map(this._mapEventRow);
361
+ }
362
+ async countEvents(query) {
363
+ const conditions = this._buildEventConditions(query);
364
+ const result = this.db
365
+ .select({ count: drizzleCount() })
366
+ .from(events)
367
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
368
+ .get();
369
+ return result?.count ?? 0;
370
+ }
371
+ // ─── Sessions — Read ───────────────────────────────────────
372
+ async querySessions(query) {
373
+ const limit = Math.min(query.limit ?? 50, 500);
374
+ const offset = query.offset ?? 0;
375
+ const conditions = this._buildSessionConditions(query);
376
+ const rows = this.db
377
+ .select()
378
+ .from(sessions)
379
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
380
+ .orderBy(desc(sessions.startedAt))
381
+ .limit(limit)
382
+ .offset(offset)
383
+ .all();
384
+ const totalResult = this.db
385
+ .select({ count: drizzleCount() })
386
+ .from(sessions)
387
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
388
+ .get();
389
+ return {
390
+ sessions: rows.map(this._mapSessionRow),
391
+ total: totalResult?.count ?? 0,
392
+ };
393
+ }
394
+ async getSession(id) {
395
+ const row = this.db
396
+ .select()
397
+ .from(sessions)
398
+ .where(eq(sessions.id, id))
399
+ .get();
400
+ return row ? this._mapSessionRow(row) : null;
401
+ }
402
+ // ─── Agents — Read ─────────────────────────────────────────
403
+ async listAgents() {
404
+ const rows = this.db
405
+ .select()
406
+ .from(agents)
407
+ .orderBy(desc(agents.lastSeenAt))
408
+ .all();
409
+ return rows.map(this._mapAgentRow);
410
+ }
411
+ async getAgent(id) {
412
+ const row = this.db
413
+ .select()
414
+ .from(agents)
415
+ .where(eq(agents.id, id))
416
+ .get();
417
+ return row ? this._mapAgentRow(row) : null;
418
+ }
419
+ // ─── Analytics ─────────────────────────────────────────────
420
+ async getAnalytics(params) {
421
+ // HIGH 5: Build the time-bucket format string for SQLite strftime
422
+ const formatStr = params.granularity === 'hour'
423
+ ? '%Y-%m-%dT%H:00:00Z'
424
+ : params.granularity === 'day'
425
+ ? '%Y-%m-%dT00:00:00Z'
426
+ : '%Y-%W'; // week — true ISO week bucketing
427
+ const conditions = [
428
+ gte(events.timestamp, params.from),
429
+ lte(events.timestamp, params.to),
430
+ ];
431
+ if (params.agentId) {
432
+ conditions.push(eq(events.agentId, params.agentId));
433
+ }
434
+ // HIGH 6: Compute avgLatencyMs and totalCostUsd from payload JSON
435
+ // Bucketed query
436
+ const bucketRows = this.db
437
+ .all(sql `
438
+ SELECT
439
+ strftime(${formatStr}, timestamp) as bucket,
440
+ COUNT(*) as eventCount,
441
+ SUM(CASE WHEN event_type = 'tool_call' THEN 1 ELSE 0 END) as toolCallCount,
442
+ SUM(CASE WHEN severity IN ('error', 'critical') OR event_type = 'tool_error' THEN 1 ELSE 0 END) as errorCount,
443
+ COUNT(DISTINCT session_id) as uniqueSessions,
444
+ COALESCE(AVG(CASE WHEN event_type = 'tool_response' THEN json_extract(payload, '$.durationMs') ELSE NULL END), 0) as avgLatencyMs,
445
+ COALESCE(SUM(CASE WHEN event_type = 'cost_tracked' THEN json_extract(payload, '$.costUsd') ELSE 0 END), 0) as totalCostUsd
446
+ FROM events
447
+ WHERE timestamp >= ${params.from}
448
+ AND timestamp <= ${params.to}
449
+ ${params.agentId ? sql `AND agent_id = ${params.agentId}` : sql ``}
450
+ GROUP BY bucket
451
+ ORDER BY bucket ASC
452
+ `);
453
+ // Totals query
454
+ const totalsRow = this.db.get(sql `
455
+ SELECT
456
+ COUNT(*) as eventCount,
457
+ SUM(CASE WHEN event_type = 'tool_call' THEN 1 ELSE 0 END) as toolCallCount,
458
+ SUM(CASE WHEN severity IN ('error', 'critical') OR event_type = 'tool_error' THEN 1 ELSE 0 END) as errorCount,
459
+ COUNT(DISTINCT session_id) as uniqueSessions,
460
+ COUNT(DISTINCT agent_id) as uniqueAgents,
461
+ COALESCE(AVG(CASE WHEN event_type = 'tool_response' THEN json_extract(payload, '$.durationMs') ELSE NULL END), 0) as avgLatencyMs,
462
+ COALESCE(SUM(CASE WHEN event_type = 'cost_tracked' THEN json_extract(payload, '$.costUsd') ELSE 0 END), 0) as totalCostUsd
463
+ FROM events
464
+ WHERE timestamp >= ${params.from}
465
+ AND timestamp <= ${params.to}
466
+ ${params.agentId ? sql `AND agent_id = ${params.agentId}` : sql ``}
467
+ `);
468
+ return {
469
+ buckets: bucketRows.map((row) => ({
470
+ timestamp: row.bucket,
471
+ eventCount: Number(row.eventCount),
472
+ toolCallCount: Number(row.toolCallCount),
473
+ errorCount: Number(row.errorCount),
474
+ avgLatencyMs: Number(row.avgLatencyMs),
475
+ totalCostUsd: Number(row.totalCostUsd),
476
+ uniqueSessions: Number(row.uniqueSessions),
477
+ })),
478
+ totals: {
479
+ eventCount: Number(totalsRow?.eventCount ?? 0),
480
+ toolCallCount: Number(totalsRow?.toolCallCount ?? 0),
481
+ errorCount: Number(totalsRow?.errorCount ?? 0),
482
+ avgLatencyMs: Number(totalsRow?.avgLatencyMs ?? 0),
483
+ totalCostUsd: Number(totalsRow?.totalCostUsd ?? 0),
484
+ uniqueSessions: Number(totalsRow?.uniqueSessions ?? 0),
485
+ uniqueAgents: Number(totalsRow?.uniqueAgents ?? 0),
486
+ },
487
+ };
488
+ }
489
+ // ─── Alert Rules ───────────────────────────────────────────
490
+ async createAlertRule(rule) {
491
+ this.db
492
+ .insert(alertRules)
493
+ .values({
494
+ id: rule.id,
495
+ name: rule.name,
496
+ enabled: rule.enabled,
497
+ condition: rule.condition,
498
+ threshold: rule.threshold,
499
+ windowMinutes: rule.windowMinutes,
500
+ scope: JSON.stringify(rule.scope),
501
+ notifyChannels: JSON.stringify(rule.notifyChannels),
502
+ createdAt: rule.createdAt,
503
+ updatedAt: rule.updatedAt,
504
+ })
505
+ .run();
506
+ }
507
+ // HIGH 8: Check affected row count and throw NotFoundError when zero
508
+ async updateAlertRule(id, updates) {
509
+ const setValues = {};
510
+ if (updates.name !== undefined)
511
+ setValues.name = updates.name;
512
+ if (updates.enabled !== undefined)
513
+ setValues.enabled = updates.enabled;
514
+ if (updates.condition !== undefined)
515
+ setValues.condition = updates.condition;
516
+ if (updates.threshold !== undefined)
517
+ setValues.threshold = updates.threshold;
518
+ if (updates.windowMinutes !== undefined)
519
+ setValues.windowMinutes = updates.windowMinutes;
520
+ if (updates.scope !== undefined)
521
+ setValues.scope = JSON.stringify(updates.scope);
522
+ if (updates.notifyChannels !== undefined)
523
+ setValues.notifyChannels = JSON.stringify(updates.notifyChannels);
524
+ if (updates.updatedAt !== undefined)
525
+ setValues.updatedAt = updates.updatedAt;
526
+ if (Object.keys(setValues).length === 0) {
527
+ // Even with no actual updates, verify the rule exists
528
+ const existing = this.db
529
+ .select({ id: alertRules.id })
530
+ .from(alertRules)
531
+ .where(eq(alertRules.id, id))
532
+ .get();
533
+ if (!existing) {
534
+ throw new NotFoundError(`Alert rule not found: ${id}`);
535
+ }
536
+ return;
537
+ }
538
+ const result = this.db
539
+ .update(alertRules)
540
+ .set(setValues)
541
+ .where(eq(alertRules.id, id))
542
+ .run();
543
+ if (result.changes === 0) {
544
+ throw new NotFoundError(`Alert rule not found: ${id}`);
545
+ }
546
+ }
547
+ async deleteAlertRule(id) {
548
+ const result = this.db.delete(alertRules).where(eq(alertRules.id, id)).run();
549
+ if (result.changes === 0) {
550
+ throw new NotFoundError(`Alert rule not found: ${id}`);
551
+ }
552
+ }
553
+ async listAlertRules() {
554
+ const rows = this.db.select().from(alertRules).all();
555
+ return rows.map(this._mapAlertRuleRow);
556
+ }
557
+ async getAlertRule(id) {
558
+ const row = this.db
559
+ .select()
560
+ .from(alertRules)
561
+ .where(eq(alertRules.id, id))
562
+ .get();
563
+ return row ? this._mapAlertRuleRow(row) : null;
564
+ }
565
+ // ─── Maintenance ───────────────────────────────────────────
566
+ async applyRetention(olderThan) {
567
+ // Count events to be deleted
568
+ const countResult = this.db
569
+ .select({ count: drizzleCount() })
570
+ .from(events)
571
+ .where(lte(events.timestamp, olderThan))
572
+ .get();
573
+ const deletedCount = countResult?.count ?? 0;
574
+ if (deletedCount === 0)
575
+ return { deletedCount: 0 };
576
+ this.db.transaction((tx) => {
577
+ // Delete old events
578
+ tx.delete(events).where(lte(events.timestamp, olderThan)).run();
579
+ // Clean up sessions with no remaining events
580
+ tx.run(sql `
581
+ DELETE FROM sessions
582
+ WHERE id NOT IN (SELECT DISTINCT session_id FROM events)
583
+ `);
584
+ });
585
+ return { deletedCount };
586
+ }
587
+ async getStats() {
588
+ const eventCount = this.db
589
+ .select({ count: drizzleCount() })
590
+ .from(events)
591
+ .get()?.count ?? 0;
592
+ const sessionCount = this.db
593
+ .select({ count: drizzleCount() })
594
+ .from(sessions)
595
+ .get()?.count ?? 0;
596
+ const agentCount = this.db
597
+ .select({ count: drizzleCount() })
598
+ .from(agents)
599
+ .get()?.count ?? 0;
600
+ const oldest = this.db
601
+ .select({ timestamp: events.timestamp })
602
+ .from(events)
603
+ .orderBy(asc(events.timestamp))
604
+ .limit(1)
605
+ .get();
606
+ const newest = this.db
607
+ .select({ timestamp: events.timestamp })
608
+ .from(events)
609
+ .orderBy(desc(events.timestamp))
610
+ .limit(1)
611
+ .get();
612
+ // SQLite page_count * page_size for storage size
613
+ const pageCount = this.db.get(sql `PRAGMA page_count`)
614
+ ?.page_count ?? 0;
615
+ const pageSize = this.db.get(sql `PRAGMA page_size`)
616
+ ?.page_size ?? 0;
617
+ return {
618
+ totalEvents: eventCount,
619
+ totalSessions: sessionCount,
620
+ totalAgents: agentCount,
621
+ oldestEvent: oldest?.timestamp,
622
+ newestEvent: newest?.timestamp,
623
+ storageSizeBytes: pageCount * pageSize,
624
+ };
625
+ }
626
+ // ─── Private Helpers ───────────────────────────────────────
627
+ _buildEventConditions(query) {
628
+ const conditions = [];
629
+ if (query.sessionId) {
630
+ conditions.push(eq(events.sessionId, query.sessionId));
631
+ }
632
+ if (query.agentId) {
633
+ conditions.push(eq(events.agentId, query.agentId));
634
+ }
635
+ if (query.eventType) {
636
+ if (Array.isArray(query.eventType)) {
637
+ conditions.push(inArray(events.eventType, query.eventType));
638
+ }
639
+ else {
640
+ conditions.push(eq(events.eventType, query.eventType));
641
+ }
642
+ }
643
+ if (query.severity) {
644
+ if (Array.isArray(query.severity)) {
645
+ conditions.push(inArray(events.severity, query.severity));
646
+ }
647
+ else {
648
+ conditions.push(eq(events.severity, query.severity));
649
+ }
650
+ }
651
+ if (query.from) {
652
+ conditions.push(gte(events.timestamp, query.from));
653
+ }
654
+ if (query.to) {
655
+ conditions.push(lte(events.timestamp, query.to));
656
+ }
657
+ if (query.search) {
658
+ conditions.push(like(events.payload, `%${query.search}%`));
659
+ }
660
+ return conditions;
661
+ }
662
+ // HIGH 4: Use json_each() for exact tag matching with OR semantics
663
+ _buildSessionConditions(query) {
664
+ const conditions = [];
665
+ if (query.agentId) {
666
+ conditions.push(eq(sessions.agentId, query.agentId));
667
+ }
668
+ if (query.status) {
669
+ if (Array.isArray(query.status)) {
670
+ if (query.status.length === 1) {
671
+ conditions.push(eq(sessions.status, query.status[0]));
672
+ }
673
+ else if (query.status.length > 1) {
674
+ conditions.push(inArray(sessions.status, query.status));
675
+ }
676
+ }
677
+ else {
678
+ conditions.push(eq(sessions.status, query.status));
679
+ }
680
+ }
681
+ if (query.from) {
682
+ conditions.push(gte(sessions.startedAt, query.from));
683
+ }
684
+ if (query.to) {
685
+ conditions.push(lte(sessions.startedAt, query.to));
686
+ }
687
+ if (query.tags && query.tags.length > 0) {
688
+ // Use json_each for exact tag matching with OR semantics
689
+ // A session matches if it contains ANY of the specified tags
690
+ const tagPlaceholders = query.tags.map((tag) => sql `${tag}`);
691
+ conditions.push(sql `EXISTS (
692
+ SELECT 1 FROM json_each(${sessions.tags}) AS je
693
+ WHERE je.value IN (${sql.join(tagPlaceholders, sql `, `)})
694
+ )`);
695
+ }
696
+ return conditions;
697
+ }
698
+ // HIGH 7: Use safeJsonParse everywhere
699
+ _mapEventRow(row) {
700
+ return {
701
+ id: row.id,
702
+ timestamp: row.timestamp,
703
+ sessionId: row.sessionId,
704
+ agentId: row.agentId,
705
+ eventType: row.eventType,
706
+ severity: row.severity,
707
+ payload: safeJsonParse(row.payload, {}),
708
+ metadata: safeJsonParse(row.metadata, {}),
709
+ prevHash: row.prevHash,
710
+ hash: row.hash,
711
+ };
712
+ }
713
+ _mapSessionRow(row) {
714
+ return {
715
+ id: row.id,
716
+ agentId: row.agentId,
717
+ agentName: row.agentName ?? undefined,
718
+ startedAt: row.startedAt,
719
+ endedAt: row.endedAt ?? undefined,
720
+ status: row.status,
721
+ eventCount: row.eventCount,
722
+ toolCallCount: row.toolCallCount,
723
+ errorCount: row.errorCount,
724
+ totalCostUsd: row.totalCostUsd,
725
+ tags: safeJsonParse(row.tags, []),
726
+ };
727
+ }
728
+ _mapAgentRow(row) {
729
+ return {
730
+ id: row.id,
731
+ name: row.name,
732
+ description: row.description ?? undefined,
733
+ firstSeenAt: row.firstSeenAt,
734
+ lastSeenAt: row.lastSeenAt,
735
+ sessionCount: row.sessionCount,
736
+ };
737
+ }
738
+ _mapAlertRuleRow(row) {
739
+ return {
740
+ id: row.id,
741
+ name: row.name,
742
+ enabled: row.enabled,
743
+ condition: row.condition,
744
+ threshold: row.threshold,
745
+ windowMinutes: row.windowMinutes,
746
+ scope: safeJsonParse(row.scope, {}),
747
+ notifyChannels: safeJsonParse(row.notifyChannels, []),
748
+ createdAt: row.createdAt,
749
+ updatedAt: row.updatedAt,
750
+ };
751
+ }
752
+ }
753
+ //# sourceMappingURL=sqlite-store.js.map