@sixb/sqlite 0.1.19 → 0.1.21

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 (81) hide show
  1. package/dist/action-run-storage.d.ts +4 -0
  2. package/dist/action-run-storage.d.ts.map +1 -1
  3. package/dist/activity-storage.d.ts +20 -0
  4. package/dist/activity-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/threads.d.ts +4 -0
  7. package/dist/agents/threads.d.ts.map +1 -1
  8. package/dist/ai-limit-storage.d.ts.map +1 -1
  9. package/dist/ai-usage-storage.d.ts.map +1 -1
  10. package/dist/artifact-storage.d.ts +22 -0
  11. package/dist/artifact-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  13. package/dist/auth-storage/magic-links.d.ts.map +1 -1
  14. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
  15. package/dist/auth-storage/rows.d.ts +0 -3
  16. package/dist/auth-storage/rows.d.ts.map +1 -1
  17. package/dist/auth-storage/sessions.d.ts +2 -11
  18. package/dist/auth-storage/sessions.d.ts.map +1 -1
  19. package/dist/auth-storage/shared.d.ts +0 -2
  20. package/dist/auth-storage/shared.d.ts.map +1 -1
  21. package/dist/execution-storage.d.ts +2 -1
  22. package/dist/execution-storage.d.ts.map +1 -1
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +933 -128
  26. package/dist/index.js.map +32 -30
  27. package/dist/migrations.d.ts +2 -0
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/dist/objects/storage.d.ts.map +1 -1
  30. package/dist/ontology-storage/sources.d.ts +2 -1
  31. package/dist/ontology-storage/sources.d.ts.map +1 -1
  32. package/dist/pipeline-run-storage.d.ts.map +1 -1
  33. package/dist/projection-run-storage.d.ts.map +1 -1
  34. package/dist/rules-storage.d.ts.map +1 -1
  35. package/dist/share-grant-storage.d.ts +2 -1
  36. package/dist/share-grant-storage.d.ts.map +1 -1
  37. package/dist/share-session-storage.d.ts.map +1 -1
  38. package/dist/sync-run-storage.d.ts.map +1 -1
  39. package/dist/timeseries-storage.d.ts.map +1 -1
  40. package/dist/transactions.d.ts +0 -1
  41. package/dist/transactions.d.ts.map +1 -1
  42. package/dist/webhook-run-storage.d.ts.map +1 -1
  43. package/dist/workflow-intervention-storage.d.ts.map +1 -1
  44. package/dist/workflow-run-storage.d.ts +4 -0
  45. package/dist/workflow-run-storage.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/src/action-run-storage.ts +27 -5
  48. package/src/activity-storage.ts +588 -0
  49. package/src/agents/agent-storage.ts +0 -5
  50. package/src/agents/threads.ts +27 -0
  51. package/src/ai-limit-storage.ts +0 -2
  52. package/src/ai-usage-storage.ts +0 -4
  53. package/src/artifact-storage.ts +356 -0
  54. package/src/auth-storage/auth-storage.ts +0 -23
  55. package/src/auth-storage/magic-links.ts +1 -6
  56. package/src/auth-storage/oidc-attempts.ts +1 -6
  57. package/src/auth-storage/rows.ts +0 -6
  58. package/src/auth-storage/sessions.ts +21 -42
  59. package/src/auth-storage/shared.ts +3 -20
  60. package/src/execution-storage.ts +56 -0
  61. package/src/index.ts +11 -10
  62. package/src/migrations/061-drop-auth-audience.sql +16 -0
  63. package/src/migrations/062-artifacts.sql +71 -0
  64. package/src/migrations/063-drop-output-diagnostics.sql +19 -0
  65. package/src/migrations/064-activity.sql +61 -0
  66. package/src/migrations/065-activity-timeline.sql +19 -0
  67. package/src/migrations/066-activity-interactions.sql +20 -0
  68. package/src/migrations.ts +36 -0
  69. package/src/objects/storage.ts +0 -5
  70. package/src/ontology-storage/sources.ts +41 -0
  71. package/src/pipeline-run-storage.ts +0 -5
  72. package/src/projection-run-storage.ts +0 -5
  73. package/src/rules-storage.ts +0 -5
  74. package/src/share-grant-storage.ts +25 -2
  75. package/src/share-session-storage.ts +0 -2
  76. package/src/sync-run-storage.ts +0 -5
  77. package/src/timeseries-storage.ts +0 -5
  78. package/src/transactions.ts +8 -8
  79. package/src/webhook-run-storage.ts +0 -3
  80. package/src/workflow-intervention-storage.ts +0 -5
  81. package/src/workflow-run-storage.ts +27 -5
@@ -0,0 +1,588 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite"
2
+ import {
3
+ ACTIVITY_MAX_VISIT_MS,
4
+ type ActivitySourceState,
5
+ activityActor,
6
+ activityCalendarDays,
7
+ parseActivityAttribution,
8
+ planActivityAppend,
9
+ type StoredActivityEvent,
10
+ serializeActivityAttribution,
11
+ validateActivityAppend,
12
+ } from "@sixb/core/internal/storage"
13
+ import type {
14
+ ActivityCalendarInput,
15
+ ActivityCursor,
16
+ ActivityDay,
17
+ ActivityInteraction,
18
+ ActivityPageView,
19
+ ActivityPeriodInput,
20
+ ActivitySessionVisits,
21
+ ActivityStorage,
22
+ ActivitySummary,
23
+ ActivityUsersPage,
24
+ ActivityVisit,
25
+ ActivityVisitsPage,
26
+ AppendActivityInput,
27
+ AppendActivityResult,
28
+ ListActivitySessionVisitsInput,
29
+ ListActivityUsersInput,
30
+ ListActivityVisitsInput,
31
+ ReadActivityVisitInput,
32
+ StoredActivityVisitPage,
33
+ } from "@sixb/core/storage"
34
+ import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
35
+
36
+ const VISIT_COLUMNS = `
37
+ v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
38
+ v.interactions, v.attribution
39
+ `
40
+
41
+ // Sorts after every stored timestamp, so a first page starts from the newest row.
42
+ const NO_CURSOR = { at: "9999-12-31T23:59:59.999Z", id: "" }
43
+
44
+ /** SQLite activity. The immediate transaction is the source lock; the policy decides the rest. */
45
+ export class SqliteActivityStorage implements ActivityStorage {
46
+ private readonly db: Database
47
+
48
+ constructor(connection: SqliteStoreConnection) {
49
+ this.db = connection.db
50
+ }
51
+
52
+ async append(input: AppendActivityInput): Promise<AppendActivityResult> {
53
+ validateActivityAppend(input)
54
+
55
+ return runImmediateTransaction(this.db, () => {
56
+ const plan = planActivityAppend(input, this.source(input), this.storedEvents(input))
57
+ if (plan.status !== "append") {
58
+ return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 }
59
+ }
60
+
61
+ const { state } = plan
62
+ const lastSeenAt = iso(state.lastSeenAt)
63
+
64
+ if (plan.newVisit) {
65
+ this.run(
66
+ `
67
+ INSERT INTO activity_visits (
68
+ project_id, id, actor_type, user_id, grant_id, session_id,
69
+ started_at, last_seen_at, page_views, interactions, attribution
70
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
71
+ `,
72
+ input.projectId,
73
+ state.visitId,
74
+ input.actor.type,
75
+ input.actor.type === "user" ? input.actor.userId : null,
76
+ input.actor.type === "share" ? input.actor.grantId : null,
77
+ input.sessionId,
78
+ lastSeenAt,
79
+ lastSeenAt,
80
+ plan.pageViews.length,
81
+ plan.interactions.length,
82
+ plan.newVisit.attribution === null
83
+ ? null
84
+ : serializeActivityAttribution(plan.newVisit.attribution)
85
+ )
86
+ } else {
87
+ this.run(
88
+ `
89
+ UPDATE activity_visits
90
+ SET
91
+ last_seen_at = ?,
92
+ page_views = page_views + ?,
93
+ interactions = interactions + ?
94
+ WHERE project_id = ? AND id = ?
95
+ `,
96
+ lastSeenAt,
97
+ plan.pageViews.length,
98
+ plan.interactions.length,
99
+ input.projectId,
100
+ state.visitId
101
+ )
102
+ }
103
+
104
+ for (const pageView of plan.pageViews) {
105
+ this.run(
106
+ `
107
+ INSERT INTO activity_page_views (
108
+ project_id, visit_id, sequence, id, route, received_at, attribution
109
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
110
+ `,
111
+ input.projectId,
112
+ pageView.visitId,
113
+ pageView.sequence,
114
+ pageView.id,
115
+ pageView.route,
116
+ pageView.receivedAt.toISOString(),
117
+ serializeActivityAttribution(pageView.attribution)
118
+ )
119
+ }
120
+
121
+ for (const interaction of plan.interactions) {
122
+ this.run(
123
+ `
124
+ INSERT INTO activity_interactions (
125
+ project_id, visit_id, sequence, id, name, route, received_at
126
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
127
+ `,
128
+ input.projectId,
129
+ interaction.visitId,
130
+ interaction.sequence,
131
+ interaction.id,
132
+ interaction.name,
133
+ interaction.route,
134
+ interaction.receivedAt.toISOString()
135
+ )
136
+ }
137
+
138
+ this.run(
139
+ `
140
+ INSERT INTO activity_sources (
141
+ project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
142
+ window_start, window_count
143
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
144
+ ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
145
+ visit_id = excluded.visit_id,
146
+ last_seen_at = excluded.last_seen_at,
147
+ sequence = excluded.sequence,
148
+ window_start = excluded.window_start,
149
+ window_count = excluded.window_count
150
+ `,
151
+ input.projectId,
152
+ input.actor.type,
153
+ input.sessionId,
154
+ state.visitId,
155
+ lastSeenAt,
156
+ state.sequence,
157
+ iso(state.windowStart),
158
+ state.windowCount
159
+ )
160
+
161
+ return { status: "accepted", accepted: plan.accepted }
162
+ })
163
+ }
164
+
165
+ async list(input: ListActivityVisitsInput): Promise<ActivityVisitsPage> {
166
+ const period = overlapping(input)
167
+ const before = cursorOf(input.before)
168
+ const rows = this.all<VisitRow>(
169
+ `
170
+ SELECT ${VISIT_COLUMNS}
171
+ FROM activity_visits v
172
+ WHERE ${period.where}
173
+ AND (v.last_seen_at < ? OR (v.last_seen_at = ? AND v.id < ?))
174
+ ORDER BY v.last_seen_at DESC, v.id DESC
175
+ LIMIT ?
176
+ `,
177
+ ...period.params,
178
+ before.at,
179
+ before.at,
180
+ before.id,
181
+ input.limit + 1
182
+ )
183
+
184
+ return {
185
+ visits: rows.slice(0, input.limit).map(visitFromRow),
186
+ hasMore: rows.length > input.limit,
187
+ }
188
+ }
189
+
190
+ async users(input: ListActivityUsersInput): Promise<ActivityUsersPage> {
191
+ const period = overlapping(input)
192
+ const before = cursorOf(input.before)
193
+ const rows = this.all<UserRow>(
194
+ `
195
+ SELECT
196
+ v.user_id,
197
+ MAX(v.last_seen_at) AS last_seen_at,
198
+ COUNT(DISTINCT v.id) AS visits,
199
+ COUNT(p.id) AS page_views
200
+ FROM activity_visits v
201
+ LEFT JOIN activity_page_views p
202
+ ON p.project_id = v.project_id
203
+ AND p.visit_id = v.id
204
+ AND p.received_at >= ?
205
+ AND p.received_at < ?
206
+ WHERE ${period.where} AND v.actor_type = 'user'
207
+ GROUP BY v.user_id
208
+ HAVING MAX(v.last_seen_at) < ? OR (MAX(v.last_seen_at) = ? AND v.user_id < ?)
209
+ ORDER BY last_seen_at DESC, v.user_id DESC
210
+ LIMIT ?
211
+ `,
212
+ input.from.toISOString(),
213
+ input.to.toISOString(),
214
+ ...period.params,
215
+ before.at,
216
+ before.at,
217
+ before.id,
218
+ input.limit + 1
219
+ )
220
+
221
+ return {
222
+ users: rows.slice(0, input.limit).map((row) => ({
223
+ userId: row.user_id,
224
+ lastSeenAt: new Date(row.last_seen_at),
225
+ visits: Number(row.visits),
226
+ pageViews: Number(row.page_views),
227
+ })),
228
+ hasMore: rows.length > input.limit,
229
+ }
230
+ }
231
+
232
+ async summarize(input: ActivityPeriodInput): Promise<ActivitySummary> {
233
+ const period = overlapping(input)
234
+ const [row] = this.all<SummaryRow>(
235
+ `
236
+ SELECT
237
+ COUNT(DISTINCT v.user_id) AS users,
238
+ COUNT(DISTINCT v.id) AS visits,
239
+ COUNT(DISTINCT CASE WHEN v.actor_type = 'share' THEN v.id END) AS shared_visits,
240
+ COUNT(p.id) AS page_views
241
+ FROM activity_visits v
242
+ LEFT JOIN activity_page_views p
243
+ ON p.project_id = v.project_id
244
+ AND p.visit_id = v.id
245
+ AND p.received_at >= ?
246
+ AND p.received_at < ?
247
+ WHERE ${period.where}
248
+ `,
249
+ input.from.toISOString(),
250
+ input.to.toISOString(),
251
+ ...period.params
252
+ )
253
+
254
+ return {
255
+ users: Number(row?.users ?? 0),
256
+ visits: Number(row?.visits ?? 0),
257
+ sharedVisits: Number(row?.shared_visits ?? 0),
258
+ pageViews: Number(row?.page_views ?? 0),
259
+ }
260
+ }
261
+
262
+ async calendar(input: ActivityCalendarInput): Promise<readonly ActivityDay[]> {
263
+ const period = overlapping(input)
264
+ const visits = this.all<{ started_at: string; last_seen_at: string }>(
265
+ `
266
+ SELECT v.started_at, v.last_seen_at
267
+ FROM activity_visits v
268
+ WHERE ${period.where}
269
+ `,
270
+ ...period.params
271
+ )
272
+ const days = this.all<{ day: string; page_views: number }>(
273
+ `
274
+ SELECT substr(p.received_at, 1, 10) AS day, COUNT(*) AS page_views
275
+ FROM activity_visits v
276
+ JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
277
+ WHERE ${period.where}
278
+ AND p.received_at >= ?
279
+ AND p.received_at < ?
280
+ GROUP BY day
281
+ `,
282
+ ...period.params,
283
+ input.from.toISOString(),
284
+ input.to.toISOString()
285
+ )
286
+
287
+ return activityCalendarDays(
288
+ input,
289
+ visits.map((visit) => ({
290
+ startedAt: new Date(visit.started_at),
291
+ lastSeenAt: new Date(visit.last_seen_at),
292
+ })),
293
+ new Map(days.map((row) => [row.day, Number(row.page_views)]))
294
+ )
295
+ }
296
+
297
+ async read(input: ReadActivityVisitInput): Promise<StoredActivityVisitPage | null> {
298
+ const [visit] = this.all<VisitRow & { readonly session_id: string }>(
299
+ `
300
+ SELECT ${VISIT_COLUMNS}, v.session_id
301
+ FROM activity_visits v
302
+ WHERE v.project_id = ? AND v.id = ?
303
+ `,
304
+ input.projectId,
305
+ input.id
306
+ )
307
+ if (!visit) {
308
+ return null
309
+ }
310
+
311
+ // Page views and interactions share the visit's sequence, so one bound covers both.
312
+ const rows = this.all<EventRow>(
313
+ `
314
+ SELECT id, visit_id, sequence, route, NULL AS name, received_at
315
+ FROM activity_page_views
316
+ WHERE project_id = ? AND visit_id = ?
317
+ UNION ALL
318
+ SELECT id, visit_id, sequence, route, name, received_at
319
+ FROM activity_interactions
320
+ WHERE project_id = ? AND visit_id = ?
321
+ ORDER BY sequence
322
+ LIMIT ?
323
+ `,
324
+ input.projectId,
325
+ input.id,
326
+ input.projectId,
327
+ input.id,
328
+ input.limit + 1
329
+ )
330
+
331
+ const pageViews: ActivityPageView[] = []
332
+ const interactions: ActivityInteraction[] = []
333
+ for (const row of rows.slice(0, input.limit)) {
334
+ if (row.name === null) {
335
+ pageViews.push(pageViewFromRow(row))
336
+ } else {
337
+ interactions.push({ ...pageViewFromRow(row), name: row.name })
338
+ }
339
+ }
340
+
341
+ return {
342
+ visit: visitFromRow(visit),
343
+ sessionId: visit.session_id,
344
+ pageViews,
345
+ interactions,
346
+ hasMore: rows.length > input.limit,
347
+ }
348
+ }
349
+
350
+ async sessionVisits(
351
+ input: ListActivitySessionVisitsInput
352
+ ): Promise<readonly ActivitySessionVisits[]> {
353
+ if (input.sessionIds.length === 0) {
354
+ return []
355
+ }
356
+
357
+ // A session opens a visit per return after inactivity, so each has a few to rank.
358
+ const rows = this.all<SessionVisitRow>(
359
+ `
360
+ SELECT *
361
+ FROM (
362
+ SELECT
363
+ ${VISIT_COLUMNS},
364
+ v.session_id,
365
+ ROW_NUMBER() OVER (
366
+ PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
367
+ ) AS position,
368
+ COUNT(*) OVER (PARTITION BY v.session_id) AS total
369
+ FROM activity_visits v
370
+ WHERE v.project_id = ?
371
+ AND v.actor_type = 'user'
372
+ AND v.session_id IN (SELECT value FROM json_each(?))
373
+ AND v.user_id = ?
374
+ )
375
+ WHERE position <= ?
376
+ ORDER BY session_id, position
377
+ `,
378
+ input.projectId,
379
+ JSON.stringify(input.sessionIds),
380
+ input.userId,
381
+ input.limit
382
+ )
383
+
384
+ const bySession = new Map<string, { visits: number; latest: ActivityVisit[] }>()
385
+ for (const row of rows) {
386
+ const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] }
387
+ session.latest.push(visitFromRow(row))
388
+ bySession.set(row.session_id, session)
389
+ }
390
+
391
+ return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }))
392
+ }
393
+
394
+ private source(input: AppendActivityInput): ActivitySourceState | undefined {
395
+ const [row] = this.all<SourceRow>(
396
+ `
397
+ SELECT
398
+ s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
399
+ s.window_start, s.window_count
400
+ FROM activity_sources s
401
+ LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
402
+ WHERE s.project_id = ? AND s.actor_type = ? AND s.session_id = ?
403
+ `,
404
+ input.projectId,
405
+ input.actor.type,
406
+ input.sessionId
407
+ )
408
+ if (!row) {
409
+ return undefined
410
+ }
411
+
412
+ // A deleted visit leaves its source behind; the next batch starts another visit.
413
+ return {
414
+ visitId: row.visit_id,
415
+ visitStartedAt: row.visit_started_at === null ? null : Date.parse(row.visit_started_at),
416
+ lastSeenAt: Date.parse(row.last_seen_at),
417
+ sequence: Number(row.sequence),
418
+ windowStart: Date.parse(row.window_start),
419
+ windowCount: Number(row.window_count),
420
+ }
421
+ }
422
+
423
+ /** The session's page views and interactions with an id of the batch, to recognize retries. */
424
+ private storedEvents(input: AppendActivityInput): StoredActivityEvent[] {
425
+ const ids = JSON.stringify(input.events.map((event) => event.id))
426
+ const rows = this.all<{
427
+ id: string
428
+ route: string
429
+ name: string | null
430
+ attribution: string | null
431
+ }>(
432
+ `
433
+ SELECT p.id, p.route, NULL AS name, p.attribution
434
+ FROM activity_page_views p
435
+ JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
436
+ WHERE p.project_id = ?
437
+ AND p.id IN (SELECT value FROM json_each(?))
438
+ AND v.actor_type = ?
439
+ AND v.session_id = ?
440
+ UNION ALL
441
+ SELECT i.id, i.route, i.name, NULL AS attribution
442
+ FROM activity_interactions i
443
+ JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
444
+ WHERE i.project_id = ?
445
+ AND i.id IN (SELECT value FROM json_each(?))
446
+ AND v.actor_type = ?
447
+ AND v.session_id = ?
448
+ `,
449
+ input.projectId,
450
+ ids,
451
+ input.actor.type,
452
+ input.sessionId,
453
+ input.projectId,
454
+ ids,
455
+ input.actor.type,
456
+ input.sessionId
457
+ )
458
+
459
+ return rows.map((row) => {
460
+ if (row.name !== null) {
461
+ return { id: row.id, route: row.route, name: row.name }
462
+ }
463
+
464
+ const attribution = parseActivityAttribution(row.attribution)
465
+ return { id: row.id, route: row.route, ...(attribution ? { attribution } : {}) }
466
+ })
467
+ }
468
+
469
+ private run(sql: string, ...params: SQLQueryBindings[]): void {
470
+ this.db.query<unknown, SQLQueryBindings[]>(sql).run(...params)
471
+ }
472
+
473
+ private all<T>(sql: string, ...params: SQLQueryBindings[]): T[] {
474
+ return this.db.query<T, SQLQueryBindings[]>(sql).all(...params)
475
+ }
476
+ }
477
+
478
+ interface SourceRow {
479
+ readonly visit_id: string
480
+ readonly visit_started_at: string | null
481
+ readonly last_seen_at: string
482
+ readonly sequence: number
483
+ readonly window_start: string
484
+ readonly window_count: number
485
+ }
486
+
487
+ interface VisitRow {
488
+ readonly id: string
489
+ readonly actor_type: string
490
+ readonly user_id: string | null
491
+ readonly grant_id: string | null
492
+ readonly started_at: string
493
+ readonly last_seen_at: string
494
+ readonly page_views: number
495
+ readonly interactions: number
496
+ readonly attribution: string | null
497
+ }
498
+
499
+ interface SessionVisitRow extends VisitRow {
500
+ readonly session_id: string
501
+ readonly total: number
502
+ }
503
+
504
+ interface UserRow {
505
+ readonly user_id: string
506
+ readonly last_seen_at: string
507
+ readonly visits: number
508
+ readonly page_views: number
509
+ }
510
+
511
+ interface SummaryRow {
512
+ readonly users: number
513
+ readonly visits: number
514
+ readonly shared_visits: number
515
+ readonly page_views: number
516
+ }
517
+
518
+ interface PageViewRow {
519
+ readonly id: string
520
+ readonly visit_id: string
521
+ readonly sequence: number
522
+ readonly route: string
523
+ readonly received_at: string
524
+ }
525
+
526
+ /** A page view or, with a name, an interaction. */
527
+ interface EventRow extends PageViewRow {
528
+ readonly name: string | null
529
+ }
530
+
531
+ /**
532
+ * The visits `v` active in the period, and only the account's when one is asked for. A visit lasts
533
+ * less than a day, so it started at most a day before the period: that bounds the index range.
534
+ */
535
+ function overlapping(input: ActivityPeriodInput | ActivityCalendarInput): {
536
+ readonly where: string
537
+ readonly params: SQLQueryBindings[]
538
+ } {
539
+ const conditions = [
540
+ "v.project_id = ?",
541
+ "v.started_at >= ?",
542
+ "v.started_at < ?",
543
+ "v.last_seen_at >= ?",
544
+ ]
545
+ const params: SQLQueryBindings[] = [
546
+ input.projectId,
547
+ new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS).toISOString(),
548
+ input.to.toISOString(),
549
+ input.from.toISOString(),
550
+ ]
551
+
552
+ if (input.userId !== undefined) {
553
+ conditions.push("v.user_id = ?")
554
+ params.push(input.userId)
555
+ }
556
+
557
+ return { where: conditions.join(" AND "), params }
558
+ }
559
+
560
+ function cursorOf(cursor: ActivityCursor | undefined): { at: string; id: string } {
561
+ return cursor ? { at: cursor.at.toISOString(), id: cursor.id } : NO_CURSOR
562
+ }
563
+
564
+ function visitFromRow(row: VisitRow): ActivityVisit {
565
+ return {
566
+ id: row.id,
567
+ actor: activityActor(row.actor_type, row.user_id, row.grant_id),
568
+ attribution: parseActivityAttribution(row.attribution),
569
+ startedAt: new Date(row.started_at),
570
+ lastSeenAt: new Date(row.last_seen_at),
571
+ pageViews: Number(row.page_views),
572
+ interactions: Number(row.interactions),
573
+ }
574
+ }
575
+
576
+ function pageViewFromRow(row: PageViewRow): ActivityPageView {
577
+ return {
578
+ id: row.id,
579
+ visitId: row.visit_id,
580
+ sequence: Number(row.sequence),
581
+ route: row.route,
582
+ receivedAt: new Date(row.received_at),
583
+ }
584
+ }
585
+
586
+ function iso(epochMs: number): string {
587
+ return new Date(epochMs).toISOString()
588
+ }
@@ -1,5 +1,4 @@
1
1
  import type { AgentStorage, ExecutionStorage } from "@sixb/core/storage"
2
- import { installFreshSqliteSchema } from "../migrations"
3
2
  import {
4
3
  closeSqliteStoreConnection,
5
4
  openSqliteStoreConnection,
@@ -35,10 +34,6 @@ export class SqliteAgentStorage implements AgentStorage {
35
34
  constructor(options: SqliteAgentStorageOptions) {
36
35
  this.connection = openSqliteStoreConnection(options)
37
36
 
38
- if (this.connection.installFreshSchema) {
39
- installFreshSqliteSchema(this.connection.db)
40
- }
41
-
42
37
  this.threads = new SqliteAgentThreadStore(this.connection.db)
43
38
  this.runs = new SqliteAgentRunStore(this.connection.db, options.executions)
44
39
  this.messages = new SqliteAgentMessageStore(this.connection.db)
@@ -108,6 +108,33 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
108
108
  return row ? rowToThreadRecord(row) : null
109
109
  }
110
110
 
111
+ async getByIds(params: {
112
+ readonly projectId: string
113
+ readonly ids: readonly string[]
114
+ }): Promise<readonly AgentThreadRecord[]> {
115
+ const ids = [...new Set(params.ids)]
116
+ if (ids.length === 0) {
117
+ return []
118
+ }
119
+
120
+ const rows = this.db
121
+ .query(
122
+ `
123
+ SELECT *
124
+ FROM agent_threads
125
+ WHERE project_id = ?
126
+ AND id IN (SELECT value FROM json_each(?))
127
+ `
128
+ )
129
+ .all(params.projectId, JSON.stringify(ids)) as AgentThreadRow[]
130
+
131
+ const byId = new Map(rows.map((row) => [row.id, row]))
132
+ return ids.flatMap((id) => {
133
+ const row = byId.get(id)
134
+ return row ? [rowToThreadRecord(row)] : []
135
+ })
136
+ }
137
+
111
138
  async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
112
139
  if (input.statuses !== undefined && input.statuses.length === 0) {
113
140
  return { threads: [], hasMore: false, total: 0 }
@@ -45,7 +45,6 @@ import type {
45
45
  UpdateAiLimitPolicyInput,
46
46
  } from "@sixb/core/storage"
47
47
  import { AiLimitStorageError, aiLimitCalendarMonth } from "@sixb/core/storage"
48
- import { installFreshSqliteSchema } from "./migrations"
49
48
  import {
50
49
  closeSqliteStoreConnection,
51
50
  openSqliteStoreConnection,
@@ -64,7 +63,6 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
64
63
 
65
64
  constructor(options: SqliteAiLimitStorageOptions = {}) {
66
65
  this.connection = openSqliteStoreConnection(options)
67
- if (this.connection.installFreshSchema) installFreshSqliteSchema(this.connection.db)
68
66
  }
69
67
 
70
68
  async createPolicy(input: CreateAiLimitPolicyInput): Promise<AiLimitPolicy> {
@@ -14,7 +14,6 @@ import {
14
14
  type SummarizeAiUsageExecutionInput,
15
15
  type SummarizeAiUsageExecutionsInput,
16
16
  } from "@sixb/core/storage"
17
- import { installFreshSqliteSchema } from "./migrations"
18
17
  import { isUniqueConstraintError } from "./storage-errors"
19
18
  import {
20
19
  closeSqliteStoreConnection,
@@ -34,9 +33,6 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
34
33
 
35
34
  constructor(options: SqliteAiUsageStorageOptions = {}) {
36
35
  this.connection = openSqliteStoreConnection(options)
37
- if (this.connection.installFreshSchema) {
38
- installFreshSqliteSchema(this.connection.db)
39
- }
40
36
  }
41
37
 
42
38
  async recordModelCall(input: RecordAiModelCallInput): Promise<RecordAiModelCallResult> {