@agentlensai/server 0.2.0 → 0.5.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 (222) hide show
  1. package/dist/db/embedding-store.d.ts +74 -0
  2. package/dist/db/embedding-store.d.ts.map +1 -0
  3. package/dist/db/embedding-store.js +177 -0
  4. package/dist/db/embedding-store.js.map +1 -0
  5. package/dist/db/lesson-store.d.ts +57 -0
  6. package/dist/db/lesson-store.d.ts.map +1 -0
  7. package/dist/db/lesson-store.js +217 -0
  8. package/dist/db/lesson-store.js.map +1 -0
  9. package/dist/db/migrate.d.ts.map +1 -1
  10. package/dist/db/migrate.js +250 -4
  11. package/dist/db/migrate.js.map +1 -1
  12. package/dist/db/schema.sqlite.d.ts +881 -55
  13. package/dist/db/schema.sqlite.d.ts.map +1 -1
  14. package/dist/db/schema.sqlite.js +82 -4
  15. package/dist/db/schema.sqlite.js.map +1 -1
  16. package/dist/db/session-summary-store.d.ts +45 -0
  17. package/dist/db/session-summary-store.d.ts.map +1 -0
  18. package/dist/db/session-summary-store.js +112 -0
  19. package/dist/db/session-summary-store.js.map +1 -0
  20. package/dist/db/sqlite-store.d.ts +25 -13
  21. package/dist/db/sqlite-store.d.ts.map +1 -1
  22. package/dist/db/sqlite-store.js +224 -47
  23. package/dist/db/sqlite-store.js.map +1 -1
  24. package/dist/db/tenant-scoped-store.d.ts +61 -0
  25. package/dist/db/tenant-scoped-store.d.ts.map +1 -0
  26. package/dist/db/tenant-scoped-store.js +94 -0
  27. package/dist/db/tenant-scoped-store.js.map +1 -0
  28. package/dist/index.d.ts +25 -4
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +99 -5
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/alert-engine.d.ts +72 -0
  33. package/dist/lib/alert-engine.d.ts.map +1 -0
  34. package/dist/lib/alert-engine.js +318 -0
  35. package/dist/lib/alert-engine.js.map +1 -0
  36. package/dist/lib/analysis/cost-analysis.d.ts +20 -0
  37. package/dist/lib/analysis/cost-analysis.d.ts.map +1 -0
  38. package/dist/lib/analysis/cost-analysis.js +158 -0
  39. package/dist/lib/analysis/cost-analysis.js.map +1 -0
  40. package/dist/lib/analysis/error-patterns.d.ts +26 -0
  41. package/dist/lib/analysis/error-patterns.d.ts.map +1 -0
  42. package/dist/lib/analysis/error-patterns.js +155 -0
  43. package/dist/lib/analysis/error-patterns.js.map +1 -0
  44. package/dist/lib/analysis/index.d.ts +23 -0
  45. package/dist/lib/analysis/index.d.ts.map +1 -0
  46. package/dist/lib/analysis/index.js +144 -0
  47. package/dist/lib/analysis/index.js.map +1 -0
  48. package/dist/lib/analysis/performance-trends.d.ts +19 -0
  49. package/dist/lib/analysis/performance-trends.d.ts.map +1 -0
  50. package/dist/lib/analysis/performance-trends.js +118 -0
  51. package/dist/lib/analysis/performance-trends.js.map +1 -0
  52. package/dist/lib/analysis/tool-sequences.d.ts +19 -0
  53. package/dist/lib/analysis/tool-sequences.d.ts.map +1 -0
  54. package/dist/lib/analysis/tool-sequences.js +145 -0
  55. package/dist/lib/analysis/tool-sequences.js.map +1 -0
  56. package/dist/lib/context/index.d.ts +5 -0
  57. package/dist/lib/context/index.d.ts.map +1 -0
  58. package/dist/lib/context/index.js +5 -0
  59. package/dist/lib/context/index.js.map +1 -0
  60. package/dist/lib/context/retrieval.d.ts +56 -0
  61. package/dist/lib/context/retrieval.d.ts.map +1 -0
  62. package/dist/lib/context/retrieval.js +229 -0
  63. package/dist/lib/context/retrieval.js.map +1 -0
  64. package/dist/lib/embeddings/index.d.ts +31 -0
  65. package/dist/lib/embeddings/index.d.ts.map +1 -0
  66. package/dist/lib/embeddings/index.js +31 -0
  67. package/dist/lib/embeddings/index.js.map +1 -0
  68. package/dist/lib/embeddings/local.d.ts +15 -0
  69. package/dist/lib/embeddings/local.d.ts.map +1 -0
  70. package/dist/lib/embeddings/local.js +65 -0
  71. package/dist/lib/embeddings/local.js.map +1 -0
  72. package/dist/lib/embeddings/math.d.ts +13 -0
  73. package/dist/lib/embeddings/math.d.ts.map +1 -0
  74. package/dist/lib/embeddings/math.js +35 -0
  75. package/dist/lib/embeddings/math.js.map +1 -0
  76. package/dist/lib/embeddings/openai.d.ts +15 -0
  77. package/dist/lib/embeddings/openai.d.ts.map +1 -0
  78. package/dist/lib/embeddings/openai.js +58 -0
  79. package/dist/lib/embeddings/openai.js.map +1 -0
  80. package/dist/lib/embeddings/summarizer.d.ts +26 -0
  81. package/dist/lib/embeddings/summarizer.d.ts.map +1 -0
  82. package/dist/lib/embeddings/summarizer.js +181 -0
  83. package/dist/lib/embeddings/summarizer.js.map +1 -0
  84. package/dist/lib/embeddings/types.d.ts +17 -0
  85. package/dist/lib/embeddings/types.d.ts.map +1 -0
  86. package/dist/lib/embeddings/types.js +5 -0
  87. package/dist/lib/embeddings/types.js.map +1 -0
  88. package/dist/lib/embeddings/worker.d.ts +56 -0
  89. package/dist/lib/embeddings/worker.d.ts.map +1 -0
  90. package/dist/lib/embeddings/worker.js +109 -0
  91. package/dist/lib/embeddings/worker.js.map +1 -0
  92. package/dist/lib/event-bus.d.ts +48 -0
  93. package/dist/lib/event-bus.d.ts.map +1 -0
  94. package/dist/lib/event-bus.js +34 -0
  95. package/dist/lib/event-bus.js.map +1 -0
  96. package/dist/lib/retention.d.ts +1 -1
  97. package/dist/lib/retention.d.ts.map +1 -1
  98. package/dist/lib/sse.d.ts +22 -0
  99. package/dist/lib/sse.d.ts.map +1 -0
  100. package/dist/lib/sse.js +121 -0
  101. package/dist/lib/sse.js.map +1 -0
  102. package/dist/middleware/auth.d.ts +1 -0
  103. package/dist/middleware/auth.d.ts.map +1 -1
  104. package/dist/middleware/auth.js +2 -1
  105. package/dist/middleware/auth.js.map +1 -1
  106. package/dist/routes/agents.d.ts +1 -1
  107. package/dist/routes/agents.d.ts.map +1 -1
  108. package/dist/routes/agents.js +6 -3
  109. package/dist/routes/agents.js.map +1 -1
  110. package/dist/routes/alerts.d.ts +17 -0
  111. package/dist/routes/alerts.d.ts.map +1 -0
  112. package/dist/routes/alerts.js +141 -0
  113. package/dist/routes/alerts.js.map +1 -0
  114. package/dist/routes/analytics.d.ts +16 -0
  115. package/dist/routes/analytics.d.ts.map +1 -0
  116. package/dist/routes/analytics.js +307 -0
  117. package/dist/routes/analytics.js.map +1 -0
  118. package/dist/routes/api-keys.d.ts.map +1 -1
  119. package/dist/routes/api-keys.js +30 -5
  120. package/dist/routes/api-keys.js.map +1 -1
  121. package/dist/routes/config.d.ts +5 -0
  122. package/dist/routes/config.d.ts.map +1 -1
  123. package/dist/routes/config.js +27 -7
  124. package/dist/routes/config.js.map +1 -1
  125. package/dist/routes/context.d.ts +23 -0
  126. package/dist/routes/context.d.ts.map +1 -0
  127. package/dist/routes/context.js +52 -0
  128. package/dist/routes/context.js.map +1 -0
  129. package/dist/routes/events.d.ts +7 -2
  130. package/dist/routes/events.d.ts.map +1 -1
  131. package/dist/routes/events.js +76 -8
  132. package/dist/routes/events.js.map +1 -1
  133. package/dist/routes/ingest.d.ts +36 -0
  134. package/dist/routes/ingest.d.ts.map +1 -0
  135. package/dist/routes/ingest.js +280 -0
  136. package/dist/routes/ingest.js.map +1 -0
  137. package/dist/routes/lessons.d.ts +19 -0
  138. package/dist/routes/lessons.d.ts.map +1 -0
  139. package/dist/routes/lessons.js +164 -0
  140. package/dist/routes/lessons.js.map +1 -0
  141. package/dist/routes/recall.d.ts +20 -0
  142. package/dist/routes/recall.d.ts.map +1 -0
  143. package/dist/routes/recall.js +71 -0
  144. package/dist/routes/recall.js.map +1 -0
  145. package/dist/routes/reflect.d.ts +15 -0
  146. package/dist/routes/reflect.d.ts.map +1 -0
  147. package/dist/routes/reflect.js +55 -0
  148. package/dist/routes/reflect.js.map +1 -0
  149. package/dist/routes/sessions.d.ts +1 -1
  150. package/dist/routes/sessions.d.ts.map +1 -1
  151. package/dist/routes/sessions.js +9 -7
  152. package/dist/routes/sessions.js.map +1 -1
  153. package/dist/routes/stats.d.ts +1 -1
  154. package/dist/routes/stats.d.ts.map +1 -1
  155. package/dist/routes/stats.js +3 -1
  156. package/dist/routes/stats.js.map +1 -1
  157. package/dist/routes/stream.d.ts +23 -0
  158. package/dist/routes/stream.d.ts.map +1 -0
  159. package/dist/routes/stream.js +94 -0
  160. package/dist/routes/stream.js.map +1 -0
  161. package/dist/routes/tenant-helper.d.ts +20 -0
  162. package/dist/routes/tenant-helper.d.ts.map +1 -0
  163. package/dist/routes/tenant-helper.js +23 -0
  164. package/dist/routes/tenant-helper.js.map +1 -0
  165. package/package.json +11 -11
  166. package/LICENSE +0 -21
  167. package/dist/__tests__/agents-stats.test.d.ts +0 -5
  168. package/dist/__tests__/agents-stats.test.d.ts.map +0 -1
  169. package/dist/__tests__/agents-stats.test.js +0 -134
  170. package/dist/__tests__/agents-stats.test.js.map +0 -1
  171. package/dist/__tests__/api-keys.test.d.ts +0 -5
  172. package/dist/__tests__/api-keys.test.d.ts.map +0 -1
  173. package/dist/__tests__/api-keys.test.js +0 -118
  174. package/dist/__tests__/api-keys.test.js.map +0 -1
  175. package/dist/__tests__/auth-no-db.test.d.ts +0 -2
  176. package/dist/__tests__/auth-no-db.test.d.ts.map +0 -1
  177. package/dist/__tests__/auth-no-db.test.js +0 -43
  178. package/dist/__tests__/auth-no-db.test.js.map +0 -1
  179. package/dist/__tests__/auth.test.d.ts +0 -5
  180. package/dist/__tests__/auth.test.d.ts.map +0 -1
  181. package/dist/__tests__/auth.test.js +0 -86
  182. package/dist/__tests__/auth.test.js.map +0 -1
  183. package/dist/__tests__/config.test.d.ts +0 -2
  184. package/dist/__tests__/config.test.d.ts.map +0 -1
  185. package/dist/__tests__/config.test.js +0 -37
  186. package/dist/__tests__/config.test.js.map +0 -1
  187. package/dist/__tests__/events-ingest.test.d.ts +0 -5
  188. package/dist/__tests__/events-ingest.test.d.ts.map +0 -1
  189. package/dist/__tests__/events-ingest.test.js +0 -248
  190. package/dist/__tests__/events-ingest.test.js.map +0 -1
  191. package/dist/__tests__/events-query.test.d.ts +0 -5
  192. package/dist/__tests__/events-query.test.d.ts.map +0 -1
  193. package/dist/__tests__/events-query.test.js +0 -205
  194. package/dist/__tests__/events-query.test.js.map +0 -1
  195. package/dist/__tests__/health.test.d.ts +0 -5
  196. package/dist/__tests__/health.test.d.ts.map +0 -1
  197. package/dist/__tests__/health.test.js +0 -40
  198. package/dist/__tests__/health.test.js.map +0 -1
  199. package/dist/__tests__/sessions.test.d.ts +0 -5
  200. package/dist/__tests__/sessions.test.d.ts.map +0 -1
  201. package/dist/__tests__/sessions.test.js +0 -176
  202. package/dist/__tests__/sessions.test.js.map +0 -1
  203. package/dist/__tests__/test-helpers.d.ts +0 -24
  204. package/dist/__tests__/test-helpers.d.ts.map +0 -1
  205. package/dist/__tests__/test-helpers.js +0 -45
  206. package/dist/__tests__/test-helpers.js.map +0 -1
  207. package/dist/db/__tests__/init.test.d.ts +0 -2
  208. package/dist/db/__tests__/init.test.d.ts.map +0 -1
  209. package/dist/db/__tests__/init.test.js +0 -73
  210. package/dist/db/__tests__/init.test.js.map +0 -1
  211. package/dist/db/__tests__/sqlite-store-read.test.d.ts +0 -2
  212. package/dist/db/__tests__/sqlite-store-read.test.d.ts.map +0 -1
  213. package/dist/db/__tests__/sqlite-store-read.test.js +0 -749
  214. package/dist/db/__tests__/sqlite-store-read.test.js.map +0 -1
  215. package/dist/db/__tests__/sqlite-store-write.test.d.ts +0 -2
  216. package/dist/db/__tests__/sqlite-store-write.test.d.ts.map +0 -1
  217. package/dist/db/__tests__/sqlite-store-write.test.js +0 -418
  218. package/dist/db/__tests__/sqlite-store-write.test.js.map +0 -1
  219. package/dist/lib/__tests__/retention.test.d.ts +0 -2
  220. package/dist/lib/__tests__/retention.test.d.ts.map +0 -1
  221. package/dist/lib/__tests__/retention.test.js +0 -238
  222. package/dist/lib/__tests__/retention.test.js.map +0 -1
@@ -13,9 +13,9 @@
13
13
  * - listAgents(), getAgent()
14
14
  * - getAnalytics(), getStats()
15
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';
16
+ import { eq, and, gte, lte, inArray, desc, asc, sql, count as drizzleCount } from 'drizzle-orm';
17
+ import { computeEventHash } from '@agentlensai/core';
18
+ import { events, sessions, agents, alertRules, alertHistory } from './schema.sqlite.js';
19
19
  import { HashChainError, NotFoundError } from './errors.js';
20
20
  // ─── Helpers ───────────────────────────────────────────────
21
21
  /**
@@ -42,6 +42,7 @@ export class SqliteEventStore {
42
42
  this.db.transaction((tx) => {
43
43
  // CRITICAL 3: Validate hash chain before inserting
44
44
  const firstEvent = eventList[0];
45
+ const tenantId = firstEvent.tenantId ?? 'default';
45
46
  // Check if the first event already exists (idempotent re-insert)
46
47
  const existingFirst = tx
47
48
  .select({ id: events.id })
@@ -53,7 +54,7 @@ export class SqliteEventStore {
53
54
  const lastStoredEvent = tx
54
55
  .select({ hash: events.hash })
55
56
  .from(events)
56
- .where(eq(events.sessionId, firstEvent.sessionId))
57
+ .where(and(eq(events.sessionId, firstEvent.sessionId), eq(events.tenantId, tenantId)))
57
58
  .orderBy(desc(events.timestamp), desc(events.id))
58
59
  .limit(1)
59
60
  .get();
@@ -103,17 +104,18 @@ export class SqliteEventStore {
103
104
  metadata: JSON.stringify(event.metadata),
104
105
  prevHash: event.prevHash,
105
106
  hash: event.hash,
107
+ tenantId,
106
108
  })
107
109
  .onConflictDoNothing({ target: events.id })
108
110
  .run();
109
111
  // Handle session management
110
- this._handleSessionUpdate(tx, event);
112
+ this._handleSessionUpdate(tx, event, tenantId);
111
113
  // Handle agent auto-creation
112
- this._handleAgentUpsert(tx, event);
114
+ this._handleAgentUpsert(tx, event, tenantId);
113
115
  }
114
116
  });
115
117
  }
116
- _handleSessionUpdate(tx, event) {
118
+ _handleSessionUpdate(tx, event, tenantId) {
117
119
  if (event.eventType === 'session_started') {
118
120
  // Create new session
119
121
  const payload = event.payload;
@@ -132,9 +134,10 @@ export class SqliteEventStore {
132
134
  errorCount: 0,
133
135
  totalCostUsd: 0,
134
136
  tags: JSON.stringify(tags),
137
+ tenantId,
135
138
  })
136
139
  .onConflictDoUpdate({
137
- target: sessions.id,
140
+ target: [sessions.id, sessions.tenantId],
138
141
  set: {
139
142
  agentName: agentName ?? sql `coalesce(${sessions.agentName}, NULL)`,
140
143
  status: 'active',
@@ -157,8 +160,9 @@ export class SqliteEventStore {
157
160
  errorCount: 0,
158
161
  totalCostUsd: 0,
159
162
  tags: '[]',
163
+ tenantId,
160
164
  })
161
- .onConflictDoNothing({ target: sessions.id })
165
+ .onConflictDoNothing({ target: [sessions.id, sessions.tenantId] })
162
166
  .run();
163
167
  // Build incremental updates
164
168
  const isToolCall = event.eventType === 'tool_call';
@@ -166,8 +170,13 @@ export class SqliteEventStore {
166
170
  event.severity === 'critical' ||
167
171
  event.eventType === 'tool_error';
168
172
  const isCost = event.eventType === 'cost_tracked';
173
+ const isLlmResponse = event.eventType === 'llm_response';
169
174
  const costPayload = event.payload;
170
175
  const costUsd = isCost ? (Number(costPayload.costUsd) || 0) : 0;
176
+ const llmCostUsd = isLlmResponse ? (Number(costPayload.costUsd) || 0) : 0;
177
+ const llmUsage = isLlmResponse ? costPayload.usage : undefined;
178
+ const llmInputTokens = llmUsage ? (Number(llmUsage.inputTokens) || 0) : 0;
179
+ const llmOutputTokens = llmUsage ? (Number(llmUsage.outputTokens) || 0) : 0;
171
180
  if (event.eventType === 'session_ended') {
172
181
  const payload = event.payload;
173
182
  const reason = payload.reason;
@@ -181,7 +190,7 @@ export class SqliteEventStore {
181
190
  ? sql `${sessions.errorCount} + 1`
182
191
  : sessions.errorCount,
183
192
  })
184
- .where(eq(sessions.id, event.sessionId))
193
+ .where(and(eq(sessions.id, event.sessionId), eq(sessions.tenantId, tenantId)))
185
194
  .run();
186
195
  return;
187
196
  }
@@ -197,13 +206,25 @@ export class SqliteEventStore {
197
206
  : sessions.errorCount,
198
207
  totalCostUsd: isCost
199
208
  ? sql `${sessions.totalCostUsd} + ${costUsd}`
200
- : sessions.totalCostUsd,
209
+ : isLlmResponse
210
+ ? sql `${sessions.totalCostUsd} + ${llmCostUsd}`
211
+ : sessions.totalCostUsd,
212
+ llmCallCount: isLlmResponse
213
+ ? sql `${sessions.llmCallCount} + 1`
214
+ : sessions.llmCallCount,
215
+ totalInputTokens: isLlmResponse
216
+ ? sql `${sessions.totalInputTokens} + ${llmInputTokens}`
217
+ : sessions.totalInputTokens,
218
+ totalOutputTokens: isLlmResponse
219
+ ? sql `${sessions.totalOutputTokens} + ${llmOutputTokens}`
220
+ : sessions.totalOutputTokens,
201
221
  })
202
- .where(eq(sessions.id, event.sessionId))
222
+ .where(and(eq(sessions.id, event.sessionId), eq(sessions.tenantId, tenantId)))
203
223
  .run();
204
224
  }
205
- _handleAgentUpsert(tx, event) {
225
+ _handleAgentUpsert(tx, event, tenantId) {
206
226
  // CRITICAL 4: Use INSERT ... ON CONFLICT DO UPDATE for agents
227
+ // Agents are scoped per tenant — use composite key (id + tenant_id)
207
228
  const payload = event.payload;
208
229
  const agentName = payload.agentName ?? event.agentId;
209
230
  tx.insert(agents)
@@ -213,9 +234,10 @@ export class SqliteEventStore {
213
234
  firstSeenAt: event.timestamp,
214
235
  lastSeenAt: event.timestamp,
215
236
  sessionCount: event.eventType === 'session_started' ? 1 : 0,
237
+ tenantId,
216
238
  })
217
239
  .onConflictDoUpdate({
218
- target: agents.id,
240
+ target: [agents.id, agents.tenantId],
219
241
  set: {
220
242
  lastSeenAt: event.timestamp,
221
243
  sessionCount: event.eventType === 'session_started'
@@ -227,10 +249,11 @@ export class SqliteEventStore {
227
249
  }
228
250
  // ─── Sessions — Write ──────────────────────────────────────
229
251
  async upsertSession(session) {
252
+ const tenantId = session.tenantId ?? 'default';
230
253
  const existing = this.db
231
254
  .select()
232
255
  .from(sessions)
233
- .where(eq(sessions.id, session.id))
256
+ .where(and(eq(sessions.id, session.id), eq(sessions.tenantId, tenantId)))
234
257
  .get();
235
258
  if (existing) {
236
259
  const updates = {};
@@ -252,13 +275,19 @@ export class SqliteEventStore {
252
275
  updates.errorCount = session.errorCount;
253
276
  if (session.totalCostUsd !== undefined)
254
277
  updates.totalCostUsd = session.totalCostUsd;
278
+ if (session.llmCallCount !== undefined)
279
+ updates.llmCallCount = session.llmCallCount;
280
+ if (session.totalInputTokens !== undefined)
281
+ updates.totalInputTokens = session.totalInputTokens;
282
+ if (session.totalOutputTokens !== undefined)
283
+ updates.totalOutputTokens = session.totalOutputTokens;
255
284
  if (session.tags !== undefined)
256
285
  updates.tags = JSON.stringify(session.tags);
257
286
  if (Object.keys(updates).length > 0) {
258
287
  this.db
259
288
  .update(sessions)
260
289
  .set(updates)
261
- .where(eq(sessions.id, session.id))
290
+ .where(and(eq(sessions.id, session.id), eq(sessions.tenantId, tenantId)))
262
291
  .run();
263
292
  }
264
293
  }
@@ -276,17 +305,22 @@ export class SqliteEventStore {
276
305
  toolCallCount: session.toolCallCount ?? 0,
277
306
  errorCount: session.errorCount ?? 0,
278
307
  totalCostUsd: session.totalCostUsd ?? 0,
308
+ llmCallCount: session.llmCallCount ?? 0,
309
+ totalInputTokens: session.totalInputTokens ?? 0,
310
+ totalOutputTokens: session.totalOutputTokens ?? 0,
279
311
  tags: JSON.stringify(session.tags ?? []),
312
+ tenantId,
280
313
  })
281
314
  .run();
282
315
  }
283
316
  }
284
317
  // ─── Agents — Write ────────────────────────────────────────
285
318
  async upsertAgent(agent) {
319
+ const tenantId = agent.tenantId ?? 'default';
286
320
  const existing = this.db
287
321
  .select()
288
322
  .from(agents)
289
- .where(eq(agents.id, agent.id))
323
+ .where(and(eq(agents.id, agent.id), eq(agents.tenantId, tenantId)))
290
324
  .get();
291
325
  if (existing) {
292
326
  const updates = {};
@@ -302,7 +336,7 @@ export class SqliteEventStore {
302
336
  this.db
303
337
  .update(agents)
304
338
  .set(updates)
305
- .where(eq(agents.id, agent.id))
339
+ .where(and(eq(agents.id, agent.id), eq(agents.tenantId, tenantId)))
306
340
  .run();
307
341
  }
308
342
  }
@@ -317,6 +351,7 @@ export class SqliteEventStore {
317
351
  firstSeenAt: agent.firstSeenAt ?? now,
318
352
  lastSeenAt: agent.lastSeenAt ?? now,
319
353
  sessionCount: agent.sessionCount ?? 0,
354
+ tenantId,
320
355
  })
321
356
  .run();
322
357
  }
@@ -342,23 +377,42 @@ export class SqliteEventStore {
342
377
  hasMore: offset + rows.length < total,
343
378
  };
344
379
  }
345
- async getEvent(id) {
380
+ async getEvent(id, tenantId) {
381
+ const conditions = [eq(events.id, id)];
382
+ if (tenantId)
383
+ conditions.push(eq(events.tenantId, tenantId));
346
384
  const row = this.db
347
385
  .select()
348
386
  .from(events)
349
- .where(eq(events.id, id))
387
+ .where(and(...conditions))
350
388
  .get();
351
389
  return row ? this._mapEventRow(row) : null;
352
390
  }
353
- async getSessionTimeline(sessionId) {
391
+ async getSessionTimeline(sessionId, tenantId) {
392
+ const conditions = [eq(events.sessionId, sessionId)];
393
+ if (tenantId)
394
+ conditions.push(eq(events.tenantId, tenantId));
354
395
  const rows = this.db
355
396
  .select()
356
397
  .from(events)
357
- .where(eq(events.sessionId, sessionId))
398
+ .where(and(...conditions))
358
399
  .orderBy(asc(events.timestamp))
359
400
  .all();
360
401
  return rows.map(this._mapEventRow);
361
402
  }
403
+ async getLastEventHash(sessionId, tenantId) {
404
+ const conditions = [eq(events.sessionId, sessionId)];
405
+ if (tenantId)
406
+ conditions.push(eq(events.tenantId, tenantId));
407
+ const row = this.db
408
+ .select({ hash: events.hash })
409
+ .from(events)
410
+ .where(and(...conditions))
411
+ .orderBy(desc(events.timestamp), desc(events.id))
412
+ .limit(1)
413
+ .get();
414
+ return row?.hash ?? null;
415
+ }
362
416
  async countEvents(query) {
363
417
  const conditions = this._buildEventConditions(query);
364
418
  const result = this.db
@@ -391,28 +445,36 @@ export class SqliteEventStore {
391
445
  total: totalResult?.count ?? 0,
392
446
  };
393
447
  }
394
- async getSession(id) {
448
+ async getSession(id, tenantId) {
449
+ const conditions = [eq(sessions.id, id)];
450
+ if (tenantId)
451
+ conditions.push(eq(sessions.tenantId, tenantId));
395
452
  const row = this.db
396
453
  .select()
397
454
  .from(sessions)
398
- .where(eq(sessions.id, id))
455
+ .where(and(...conditions))
399
456
  .get();
400
457
  return row ? this._mapSessionRow(row) : null;
401
458
  }
402
459
  // ─── Agents — Read ─────────────────────────────────────────
403
- async listAgents() {
460
+ async listAgents(tenantId) {
461
+ const conditions = tenantId ? [eq(agents.tenantId, tenantId)] : [];
404
462
  const rows = this.db
405
463
  .select()
406
464
  .from(agents)
465
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
407
466
  .orderBy(desc(agents.lastSeenAt))
408
467
  .all();
409
468
  return rows.map(this._mapAgentRow);
410
469
  }
411
- async getAgent(id) {
470
+ async getAgent(id, tenantId) {
471
+ const conditions = [eq(agents.id, id)];
472
+ if (tenantId)
473
+ conditions.push(eq(agents.tenantId, tenantId));
412
474
  const row = this.db
413
475
  .select()
414
476
  .from(agents)
415
- .where(eq(agents.id, id))
477
+ .where(and(...conditions))
416
478
  .get();
417
479
  return row ? this._mapAgentRow(row) : null;
418
480
  }
@@ -447,6 +509,7 @@ export class SqliteEventStore {
447
509
  WHERE timestamp >= ${params.from}
448
510
  AND timestamp <= ${params.to}
449
511
  ${params.agentId ? sql `AND agent_id = ${params.agentId}` : sql ``}
512
+ ${params.tenantId ? sql `AND tenant_id = ${params.tenantId}` : sql ``}
450
513
  GROUP BY bucket
451
514
  ORDER BY bucket ASC
452
515
  `);
@@ -464,6 +527,7 @@ export class SqliteEventStore {
464
527
  WHERE timestamp >= ${params.from}
465
528
  AND timestamp <= ${params.to}
466
529
  ${params.agentId ? sql `AND agent_id = ${params.agentId}` : sql ``}
530
+ ${params.tenantId ? sql `AND tenant_id = ${params.tenantId}` : sql ``}
467
531
  `);
468
532
  return {
469
533
  buckets: bucketRows.map((row) => ({
@@ -501,11 +565,12 @@ export class SqliteEventStore {
501
565
  notifyChannels: JSON.stringify(rule.notifyChannels),
502
566
  createdAt: rule.createdAt,
503
567
  updatedAt: rule.updatedAt,
568
+ tenantId: rule.tenantId ?? 'default',
504
569
  })
505
570
  .run();
506
571
  }
507
572
  // HIGH 8: Check affected row count and throw NotFoundError when zero
508
- async updateAlertRule(id, updates) {
573
+ async updateAlertRule(id, updates, tenantId) {
509
574
  const setValues = {};
510
575
  if (updates.name !== undefined)
511
576
  setValues.name = updates.name;
@@ -523,12 +588,15 @@ export class SqliteEventStore {
523
588
  setValues.notifyChannels = JSON.stringify(updates.notifyChannels);
524
589
  if (updates.updatedAt !== undefined)
525
590
  setValues.updatedAt = updates.updatedAt;
591
+ const whereConditions = [eq(alertRules.id, id)];
592
+ if (tenantId)
593
+ whereConditions.push(eq(alertRules.tenantId, tenantId));
526
594
  if (Object.keys(setValues).length === 0) {
527
595
  // Even with no actual updates, verify the rule exists
528
596
  const existing = this.db
529
597
  .select({ id: alertRules.id })
530
598
  .from(alertRules)
531
- .where(eq(alertRules.id, id))
599
+ .where(and(...whereConditions))
532
600
  .get();
533
601
  if (!existing) {
534
602
  throw new NotFoundError(`Alert rule not found: ${id}`);
@@ -538,74 +606,165 @@ export class SqliteEventStore {
538
606
  const result = this.db
539
607
  .update(alertRules)
540
608
  .set(setValues)
541
- .where(eq(alertRules.id, id))
609
+ .where(and(...whereConditions))
542
610
  .run();
543
611
  if (result.changes === 0) {
544
612
  throw new NotFoundError(`Alert rule not found: ${id}`);
545
613
  }
546
614
  }
547
- async deleteAlertRule(id) {
548
- const result = this.db.delete(alertRules).where(eq(alertRules.id, id)).run();
615
+ async deleteAlertRule(id, tenantId) {
616
+ const whereConditions = [eq(alertRules.id, id)];
617
+ if (tenantId)
618
+ whereConditions.push(eq(alertRules.tenantId, tenantId));
619
+ const result = this.db.delete(alertRules).where(and(...whereConditions)).run();
549
620
  if (result.changes === 0) {
550
621
  throw new NotFoundError(`Alert rule not found: ${id}`);
551
622
  }
552
623
  }
553
- async listAlertRules() {
554
- const rows = this.db.select().from(alertRules).all();
624
+ async listAlertRules(tenantId) {
625
+ const conditions = tenantId ? [eq(alertRules.tenantId, tenantId)] : [];
626
+ const rows = this.db
627
+ .select()
628
+ .from(alertRules)
629
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
630
+ .all();
555
631
  return rows.map(this._mapAlertRuleRow);
556
632
  }
557
- async getAlertRule(id) {
633
+ async getAlertRule(id, tenantId) {
634
+ const conditions = [eq(alertRules.id, id)];
635
+ if (tenantId)
636
+ conditions.push(eq(alertRules.tenantId, tenantId));
558
637
  const row = this.db
559
638
  .select()
560
639
  .from(alertRules)
561
- .where(eq(alertRules.id, id))
640
+ .where(and(...conditions))
562
641
  .get();
563
642
  return row ? this._mapAlertRuleRow(row) : null;
564
643
  }
644
+ // ─── Alert History ─────────────────────────────────────────
645
+ async insertAlertHistory(entry) {
646
+ this.db
647
+ .insert(alertHistory)
648
+ .values({
649
+ id: entry.id,
650
+ ruleId: entry.ruleId,
651
+ triggeredAt: entry.triggeredAt,
652
+ resolvedAt: entry.resolvedAt ?? null,
653
+ currentValue: entry.currentValue,
654
+ threshold: entry.threshold,
655
+ message: entry.message,
656
+ tenantId: entry.tenantId ?? 'default',
657
+ })
658
+ .run();
659
+ }
660
+ async listAlertHistory(opts) {
661
+ const limit = Math.min(opts?.limit ?? 50, 500);
662
+ const offset = opts?.offset ?? 0;
663
+ const conditions = [];
664
+ if (opts?.ruleId) {
665
+ conditions.push(eq(alertHistory.ruleId, opts.ruleId));
666
+ }
667
+ if (opts?.tenantId) {
668
+ conditions.push(eq(alertHistory.tenantId, opts.tenantId));
669
+ }
670
+ const rows = this.db
671
+ .select()
672
+ .from(alertHistory)
673
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
674
+ .orderBy(desc(alertHistory.triggeredAt))
675
+ .limit(limit)
676
+ .offset(offset)
677
+ .all();
678
+ const totalResult = this.db
679
+ .select({ count: drizzleCount() })
680
+ .from(alertHistory)
681
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
682
+ .get();
683
+ return {
684
+ entries: rows.map((row) => ({
685
+ id: row.id,
686
+ ruleId: row.ruleId,
687
+ triggeredAt: row.triggeredAt,
688
+ resolvedAt: row.resolvedAt ?? undefined,
689
+ currentValue: row.currentValue,
690
+ threshold: row.threshold,
691
+ message: row.message,
692
+ tenantId: row.tenantId,
693
+ })),
694
+ total: totalResult?.count ?? 0,
695
+ };
696
+ }
565
697
  // ─── Maintenance ───────────────────────────────────────────
566
- async applyRetention(olderThan) {
698
+ async applyRetention(olderThan, tenantId) {
699
+ // Build conditions — always filter by timestamp, optionally by tenant
700
+ const countConditions = [lte(events.timestamp, olderThan)];
701
+ if (tenantId)
702
+ countConditions.push(eq(events.tenantId, tenantId));
567
703
  // Count events to be deleted
568
704
  const countResult = this.db
569
705
  .select({ count: drizzleCount() })
570
706
  .from(events)
571
- .where(lte(events.timestamp, olderThan))
707
+ .where(and(...countConditions))
572
708
  .get();
573
709
  const deletedCount = countResult?.count ?? 0;
574
710
  if (deletedCount === 0)
575
711
  return { deletedCount: 0 };
576
712
  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
- `);
713
+ if (tenantId) {
714
+ // Tenant-scoped retention: only delete this tenant's old events
715
+ tx.delete(events)
716
+ .where(and(lte(events.timestamp, olderThan), eq(events.tenantId, tenantId)))
717
+ .run();
718
+ // Clean up this tenant's sessions that have no remaining events
719
+ tx.run(sql `
720
+ DELETE FROM sessions
721
+ WHERE tenant_id = ${tenantId}
722
+ AND id NOT IN (
723
+ SELECT DISTINCT session_id FROM events WHERE tenant_id = ${tenantId}
724
+ )
725
+ `);
726
+ }
727
+ else {
728
+ // Global retention (system-level, not exposed through TenantScopedStore)
729
+ tx.delete(events).where(lte(events.timestamp, olderThan)).run();
730
+ tx.run(sql `
731
+ DELETE FROM sessions
732
+ WHERE id NOT IN (SELECT DISTINCT session_id FROM events)
733
+ `);
734
+ }
584
735
  });
585
736
  return { deletedCount };
586
737
  }
587
- async getStats() {
738
+ async getStats(tenantId) {
739
+ const eventConditions = tenantId ? [eq(events.tenantId, tenantId)] : [];
740
+ const sessionConditions = tenantId ? [eq(sessions.tenantId, tenantId)] : [];
741
+ const agentConditions = tenantId ? [eq(agents.tenantId, tenantId)] : [];
588
742
  const eventCount = this.db
589
743
  .select({ count: drizzleCount() })
590
744
  .from(events)
745
+ .where(eventConditions.length > 0 ? and(...eventConditions) : undefined)
591
746
  .get()?.count ?? 0;
592
747
  const sessionCount = this.db
593
748
  .select({ count: drizzleCount() })
594
749
  .from(sessions)
750
+ .where(sessionConditions.length > 0 ? and(...sessionConditions) : undefined)
595
751
  .get()?.count ?? 0;
596
752
  const agentCount = this.db
597
753
  .select({ count: drizzleCount() })
598
754
  .from(agents)
755
+ .where(agentConditions.length > 0 ? and(...agentConditions) : undefined)
599
756
  .get()?.count ?? 0;
600
757
  const oldest = this.db
601
758
  .select({ timestamp: events.timestamp })
602
759
  .from(events)
760
+ .where(eventConditions.length > 0 ? and(...eventConditions) : undefined)
603
761
  .orderBy(asc(events.timestamp))
604
762
  .limit(1)
605
763
  .get();
606
764
  const newest = this.db
607
765
  .select({ timestamp: events.timestamp })
608
766
  .from(events)
767
+ .where(eventConditions.length > 0 ? and(...eventConditions) : undefined)
609
768
  .orderBy(desc(events.timestamp))
610
769
  .limit(1)
611
770
  .get();
@@ -626,6 +785,9 @@ export class SqliteEventStore {
626
785
  // ─── Private Helpers ───────────────────────────────────────
627
786
  _buildEventConditions(query) {
628
787
  const conditions = [];
788
+ if (query.tenantId) {
789
+ conditions.push(eq(events.tenantId, query.tenantId));
790
+ }
629
791
  if (query.sessionId) {
630
792
  conditions.push(eq(events.sessionId, query.sessionId));
631
793
  }
@@ -655,13 +817,21 @@ export class SqliteEventStore {
655
817
  conditions.push(lte(events.timestamp, query.to));
656
818
  }
657
819
  if (query.search) {
658
- conditions.push(like(events.payload, `%${query.search}%`));
820
+ // H1 fix: Escape LIKE wildcards to prevent wildcard abuse
821
+ const escaped = query.search
822
+ .replace(/\\/g, '\\\\')
823
+ .replace(/%/g, '\\%')
824
+ .replace(/_/g, '\\_');
825
+ conditions.push(sql `${events.payload} LIKE ${'%' + escaped + '%'} ESCAPE '\\'`);
659
826
  }
660
827
  return conditions;
661
828
  }
662
829
  // HIGH 4: Use json_each() for exact tag matching with OR semantics
663
830
  _buildSessionConditions(query) {
664
831
  const conditions = [];
832
+ if (query.tenantId) {
833
+ conditions.push(eq(sessions.tenantId, query.tenantId));
834
+ }
665
835
  if (query.agentId) {
666
836
  conditions.push(eq(sessions.agentId, query.agentId));
667
837
  }
@@ -708,6 +878,7 @@ export class SqliteEventStore {
708
878
  metadata: safeJsonParse(row.metadata, {}),
709
879
  prevHash: row.prevHash,
710
880
  hash: row.hash,
881
+ tenantId: row.tenantId,
711
882
  };
712
883
  }
713
884
  _mapSessionRow(row) {
@@ -722,7 +893,11 @@ export class SqliteEventStore {
722
893
  toolCallCount: row.toolCallCount,
723
894
  errorCount: row.errorCount,
724
895
  totalCostUsd: row.totalCostUsd,
896
+ llmCallCount: row.llmCallCount,
897
+ totalInputTokens: row.totalInputTokens,
898
+ totalOutputTokens: row.totalOutputTokens,
725
899
  tags: safeJsonParse(row.tags, []),
900
+ tenantId: row.tenantId,
726
901
  };
727
902
  }
728
903
  _mapAgentRow(row) {
@@ -733,6 +908,7 @@ export class SqliteEventStore {
733
908
  firstSeenAt: row.firstSeenAt,
734
909
  lastSeenAt: row.lastSeenAt,
735
910
  sessionCount: row.sessionCount,
911
+ tenantId: row.tenantId,
736
912
  };
737
913
  }
738
914
  _mapAlertRuleRow(row) {
@@ -747,6 +923,7 @@ export class SqliteEventStore {
747
923
  notifyChannels: safeJsonParse(row.notifyChannels, []),
748
924
  createdAt: row.createdAt,
749
925
  updatedAt: row.updatedAt,
926
+ tenantId: row.tenantId,
750
927
  };
751
928
  }
752
929
  }