@sixb/sqlite 0.1.20 → 0.1.22

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 (126) 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 +8 -2
  6. package/dist/agents/agent-storage.d.ts.map +1 -1
  7. package/dist/agents/approvals.d.ts +51 -0
  8. package/dist/agents/approvals.d.ts.map +1 -0
  9. package/dist/agents/checkpoints.d.ts +1 -0
  10. package/dist/agents/checkpoints.d.ts.map +1 -1
  11. package/dist/agents/index.d.ts +1 -0
  12. package/dist/agents/index.d.ts.map +1 -1
  13. package/dist/agents/memory.d.ts +21 -0
  14. package/dist/agents/memory.d.ts.map +1 -0
  15. package/dist/agents/messages.d.ts.map +1 -1
  16. package/dist/agents/rows.d.ts +15 -1
  17. package/dist/agents/rows.d.ts.map +1 -1
  18. package/dist/agents/runs.d.ts +14 -1
  19. package/dist/agents/runs.d.ts.map +1 -1
  20. package/dist/agents/settings.d.ts +29 -0
  21. package/dist/agents/settings.d.ts.map +1 -0
  22. package/dist/agents/threads.d.ts +16 -1
  23. package/dist/agents/threads.d.ts.map +1 -1
  24. package/dist/ai-cost-groups.d.ts.map +1 -1
  25. package/dist/ai-cost-storage.d.ts.map +1 -1
  26. package/dist/ai-usage-storage.d.ts +4 -1
  27. package/dist/ai-usage-storage.d.ts.map +1 -1
  28. package/dist/artifact-storage.d.ts +31 -0
  29. package/dist/artifact-storage.d.ts.map +1 -0
  30. package/dist/auth-storage/auth-storage.d.ts +3 -2
  31. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  32. package/dist/auth-storage/sessions.d.ts +2 -1
  33. package/dist/auth-storage/sessions.d.ts.map +1 -1
  34. package/dist/auth-storage/users.d.ts +7 -3
  35. package/dist/auth-storage/users.d.ts.map +1 -1
  36. package/dist/execution-storage.d.ts +3 -1
  37. package/dist/execution-storage.d.ts.map +1 -1
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5216 -2517
  41. package/dist/index.js.map +39 -31
  42. package/dist/migrations.d.ts +0 -1
  43. package/dist/migrations.d.ts.map +1 -1
  44. package/dist/objects/conditional-reader.d.ts +8 -0
  45. package/dist/objects/conditional-reader.d.ts.map +1 -0
  46. package/dist/objects/query-compiler.d.ts +26 -1
  47. package/dist/objects/query-compiler.d.ts.map +1 -1
  48. package/dist/objects/read-scope.d.ts +10 -1
  49. package/dist/objects/read-scope.d.ts.map +1 -1
  50. package/dist/objects/reader.d.ts +2 -0
  51. package/dist/objects/reader.d.ts.map +1 -1
  52. package/dist/objects/scoped-reader.d.ts +15 -0
  53. package/dist/objects/scoped-reader.d.ts.map +1 -0
  54. package/dist/objects/selected-reader.d.ts.map +1 -1
  55. package/dist/objects/storage.d.ts +6 -1
  56. package/dist/objects/storage.d.ts.map +1 -1
  57. package/dist/ontology-storage/index.d.ts +2 -0
  58. package/dist/ontology-storage/index.d.ts.map +1 -1
  59. package/dist/ontology-storage/materialization-writer.d.ts +2 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  61. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  62. package/dist/ontology-storage/sources.d.ts +2 -1
  63. package/dist/ontology-storage/sources.d.ts.map +1 -1
  64. package/dist/ontology-storage/unique-values.d.ts +42 -0
  65. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  66. package/dist/share-grant-storage.d.ts +2 -1
  67. package/dist/share-grant-storage.d.ts.map +1 -1
  68. package/dist/workflow-run-storage.d.ts +4 -0
  69. package/dist/workflow-run-storage.d.ts.map +1 -1
  70. package/package.json +3 -3
  71. package/src/action-run-storage.ts +27 -0
  72. package/src/activity-storage.ts +588 -0
  73. package/src/agents/agent-storage.ts +11 -2
  74. package/src/agents/approvals.ts +456 -0
  75. package/src/agents/checkpoints.ts +9 -4
  76. package/src/agents/index.ts +1 -0
  77. package/src/agents/memory.ts +195 -0
  78. package/src/agents/messages.ts +21 -18
  79. package/src/agents/rows.ts +46 -1
  80. package/src/agents/runs.ts +202 -21
  81. package/src/agents/settings.ts +112 -0
  82. package/src/agents/threads.ts +412 -1
  83. package/src/ai-cost-groups.ts +11 -1
  84. package/src/ai-cost-storage.ts +54 -12
  85. package/src/ai-usage-storage.ts +79 -2
  86. package/src/artifact-storage.ts +553 -0
  87. package/src/auth-storage/auth-storage.ts +58 -26
  88. package/src/auth-storage/sessions.ts +27 -0
  89. package/src/auth-storage/users.ts +106 -60
  90. package/src/execution-storage.ts +95 -0
  91. package/src/index.ts +10 -0
  92. package/src/migrations/062-artifacts.sql +71 -0
  93. package/src/migrations/063-drop-output-diagnostics.sql +19 -0
  94. package/src/migrations/064-activity.sql +61 -0
  95. package/src/migrations/065-activity-timeline.sql +19 -0
  96. package/src/migrations/066-activity-interactions.sql +20 -0
  97. package/src/migrations/067-artifact-kinds.sql +32 -0
  98. package/src/migrations/068-artifact-state.sql +15 -0
  99. package/src/migrations/069-agent-thread-participants.sql +16 -0
  100. package/src/migrations/070-forked-context-checkpoints.sql +45 -0
  101. package/src/migrations/071-unique-values.sql +22 -0
  102. package/src/migrations/072-generated-media-usage.sql +32 -0
  103. package/src/migrations/073-model-call-files.sql +11 -0
  104. package/src/migrations/074-agent-memory.sql +32 -0
  105. package/src/migrations/075-artifact-threads.sql +21 -0
  106. package/src/migrations/076-agent-approvals.sql +59 -0
  107. package/src/migrations/077-agent-approval-resume.sql +20 -0
  108. package/src/migrations/078-subagent-threads.sql +241 -0
  109. package/src/migrations/079-subagent-questions.sql +6 -0
  110. package/src/migrations.ts +89 -1
  111. package/src/objects/README.md +7 -0
  112. package/src/objects/conditional-reader.ts +27 -0
  113. package/src/objects/query-compiler.ts +36 -6
  114. package/src/objects/read-scope.ts +140 -23
  115. package/src/objects/reader.ts +47 -14
  116. package/src/objects/scoped-reader.ts +103 -0
  117. package/src/objects/selected-reader.ts +12 -75
  118. package/src/objects/storage.ts +16 -0
  119. package/src/ontology-storage/index.ts +3 -0
  120. package/src/ontology-storage/materialization-writer.ts +14 -0
  121. package/src/ontology-storage/materializations.ts +16 -1
  122. package/src/ontology-storage/sources.ts +41 -0
  123. package/src/ontology-storage/unique-values.ts +187 -0
  124. package/src/share-grant-storage.ts +25 -0
  125. package/src/transactions.ts +1 -1
  126. package/src/workflow-run-storage.ts +27 -0
@@ -12,6 +12,7 @@ import type {
12
12
  InvitationRecord,
13
13
  SessionRecord,
14
14
  SuspendUserAndRevokeSessionsInput,
15
+ UpdateAuthUserResult,
15
16
  UserIdentityRecord,
16
17
  UserRecord,
17
18
  } from "@sixb/core/storage"
@@ -32,7 +33,7 @@ import { SqliteAuthUserIdentityStore } from "./identities"
32
33
  import { SqliteAuthInvitationStore } from "./invitations"
33
34
  import { SqliteAuthMagicLinkStore } from "./magic-links"
34
35
  import { SqliteAuthOidcAuthorizationAttemptStore } from "./oidc-attempts"
35
- import type { SqliteAuthMagicLinkRow, SqliteAuthOidcAttemptRow } from "./rows"
36
+ import type { SqliteAuthMagicLinkRow, SqliteAuthOidcAttemptRow, SqliteAuthUserRow } from "./rows"
36
37
  import { rowToIdentityRecord, rowToUserRecord, serializeOptionalRecord } from "./rows"
37
38
  import { SqliteAuthServiceAccountGroupMembershipStore } from "./service-account-group-memberships"
38
39
  import { SqliteAuthServiceAccountStore } from "./service-accounts"
@@ -216,16 +217,17 @@ export class SqliteAuthStorage implements AuthStorage {
216
217
  consumedAt: completedAt,
217
218
  })
218
219
 
219
- const user = existingUserRow
220
- ? rowToUserRecord(existingUserRow)
221
- : this.insertUser({
222
- id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
223
- projectId,
224
- email: magicLink.email,
225
- displayName: input.newUserDisplayName,
226
- avatarUrl: input.newUserAvatarUrl,
227
- createdAt: completedAt,
228
- })
220
+ const previousUser = existingUserRow ? rowToUserRecord(existingUserRow) : null
221
+ const user =
222
+ previousUser ??
223
+ this.insertUser({
224
+ id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
225
+ projectId,
226
+ email: magicLink.email,
227
+ displayName: input.newUserDisplayName,
228
+ avatarUrl: input.newUserAvatarUrl,
229
+ createdAt: completedAt,
230
+ })
229
231
 
230
232
  const invitation = this.acceptInvitationAndApplyGroups({
231
233
  activeInvitation,
@@ -253,6 +255,7 @@ export class SqliteAuthStorage implements AuthStorage {
253
255
  session,
254
256
  invitation: invitation.invitation,
255
257
  groupMemberships,
258
+ previousUser,
256
259
  }
257
260
  }
258
261
  )
@@ -297,7 +300,7 @@ export class SqliteAuthStorage implements AuthStorage {
297
300
  email,
298
301
  now: completedAt,
299
302
  })
300
- let userRow = identity
303
+ const userRow = identity
301
304
  ? getUserRowById(this.db, { projectId, id: identity.user_id })
302
305
  : getUserRowByEmail(this.db, { projectId, email })
303
306
  const shouldCreateUser = !identity && !userRow
@@ -348,8 +351,10 @@ export class SqliteAuthStorage implements AuthStorage {
348
351
 
349
352
  this.consumeOidcAttempt(input, completedAt, projectId)
350
353
 
354
+ const previousUser = userRow ? rowToUserRecord(userRow) : null
355
+ let user: UserRecord
351
356
  if (!userRow) {
352
- const user = this.insertUser({
357
+ user = this.insertUser({
353
358
  id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
354
359
  projectId,
355
360
  email,
@@ -357,19 +362,10 @@ export class SqliteAuthStorage implements AuthStorage {
357
362
  avatarUrl: input.avatarUrl,
358
363
  createdAt: completedAt,
359
364
  })
360
- userRow = {
361
- project_id: user.projectId,
362
- id: user.id,
363
- email: user.email,
364
- display_name: user.displayName ?? null,
365
- avatar_url: user.avatarUrl ?? null,
366
- status: user.status,
367
- created_at: toIso(user.createdAt),
368
- updated_at: toIso(user.updatedAt),
369
- }
365
+ } else {
366
+ user = this.refreshProviderProfile(userRow, input, completedAt)
370
367
  }
371
368
 
372
- const user = rowToUserRecord(userRow)
373
369
  const invitation = this.acceptInvitationAndApplyGroups({
374
370
  activeInvitation,
375
371
  completedAt,
@@ -412,6 +408,7 @@ export class SqliteAuthStorage implements AuthStorage {
412
408
  identity: nextIdentity,
413
409
  invitation: invitation.invitation,
414
410
  groupMemberships,
411
+ previousUser,
415
412
  }
416
413
  }
417
414
  )
@@ -425,7 +422,7 @@ export class SqliteAuthStorage implements AuthStorage {
425
422
 
426
423
  async suspendUserAndRevokeSessions(
427
424
  input: SuspendUserAndRevokeSessionsInput
428
- ): Promise<UserRecord> {
425
+ ): Promise<UpdateAuthUserResult> {
429
426
  return runImmediateTransaction(this.db, () => {
430
427
  const existing = getUserRowById(this.db, {
431
428
  projectId: input.projectId,
@@ -456,11 +453,12 @@ export class SqliteAuthStorage implements AuthStorage {
456
453
  revokedAt: input.suspendedAt,
457
454
  })
458
455
 
459
- return rowToUserRecord({
456
+ const user = rowToUserRecord({
460
457
  ...existing,
461
458
  status: "suspended",
462
459
  updated_at: toIso(input.suspendedAt),
463
460
  })
461
+ return { user, previousUser: rowToUserRecord(existing) }
464
462
  })
465
463
  }
466
464
 
@@ -526,6 +524,40 @@ export class SqliteAuthStorage implements AuthStorage {
526
524
  }
527
525
  }
528
526
 
527
+ // The identity provider owns the name and picture it sends: every sign-in overwrites them, and
528
+ // one it leaves out keeps the stored value.
529
+ private refreshProviderProfile(
530
+ row: SqliteAuthUserRow,
531
+ input: CompleteOidcSignInInput,
532
+ updatedAt: Date
533
+ ): UserRecord {
534
+ const displayName = input.displayName ?? row.display_name
535
+ const avatarUrl = input.avatarUrl ?? row.avatar_url
536
+ if (displayName === row.display_name && avatarUrl === row.avatar_url) {
537
+ return rowToUserRecord(row)
538
+ }
539
+
540
+ this.db
541
+ .query(
542
+ `
543
+ UPDATE auth_users
544
+ SET display_name = ?,
545
+ avatar_url = ?,
546
+ updated_at = ?
547
+ WHERE project_id = ?
548
+ AND id = ?
549
+ `
550
+ )
551
+ .run(displayName, avatarUrl, toIso(updatedAt), row.project_id, row.id)
552
+
553
+ return rowToUserRecord({
554
+ ...row,
555
+ display_name: displayName,
556
+ avatar_url: avatarUrl,
557
+ updated_at: toIso(updatedAt),
558
+ })
559
+ }
560
+
529
561
  private insertUser(input: {
530
562
  readonly id: string
531
563
  readonly projectId: string
@@ -2,6 +2,8 @@ import type { Database } from "bun:sqlite"
2
2
  import type {
3
3
  AuthSessionStore,
4
4
  CreateAuthSessionInput,
5
+ ListAuthSessionsInput,
6
+ ListAuthSessionsResult,
5
7
  RotateSessionRefreshTokenResult,
6
8
  SessionRecord,
7
9
  } from "@sixb/core/storage"
@@ -60,6 +62,31 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
60
62
  return rows.map(rowToSessionRecord)
61
63
  }
62
64
 
65
+ async listByUserId(params: ListAuthSessionsInput): Promise<ListAuthSessionsResult> {
66
+ const rows = this.db
67
+ .query(
68
+ `
69
+ SELECT *
70
+ FROM auth_sessions
71
+ WHERE project_id = ? AND user_id = ? AND created_at <= ?
72
+ ORDER BY created_at DESC, id DESC
73
+ LIMIT ? OFFSET ?
74
+ `
75
+ )
76
+ .all(
77
+ params.projectId,
78
+ params.userId,
79
+ params.before ? toIso(params.before.createdAt) : "9999-12-31T23:59:59.999Z",
80
+ params.limit + 1,
81
+ params.before?.skip ?? 0
82
+ ) as SqliteAuthSessionRow[]
83
+
84
+ return {
85
+ sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
86
+ hasMore: rows.length > params.limit,
87
+ }
88
+ }
89
+
63
90
  async findValidByTokenHash(params: {
64
91
  readonly projectId: string
65
92
  readonly id: string
@@ -5,10 +5,12 @@ import type {
5
5
  ListAuthUsersInput,
6
6
  ListAuthUsersResult,
7
7
  UpdateAuthUserProfileInput,
8
+ UpdateAuthUserResult,
8
9
  UpdateAuthUserStatusInput,
9
10
  UserRecord,
10
11
  } from "@sixb/core/storage"
11
12
  import { AuthStorageError } from "@sixb/core/storage"
13
+ import { runImmediateTransaction } from "../transactions"
12
14
  import type { SqliteAuthUserRow } from "./rows"
13
15
  import { rowToUserRecord } from "./rows"
14
16
  import {
@@ -103,6 +105,22 @@ export class SqliteAuthUserStore implements AuthUserStore {
103
105
  return row ? rowToUserRecord(row) : null
104
106
  }
105
107
 
108
+ async getByIds(params: {
109
+ readonly projectId: string
110
+ readonly ids: readonly string[]
111
+ }): Promise<readonly UserRecord[]> {
112
+ const ids = [...new Set(params.ids)]
113
+ if (ids.length === 0) return []
114
+
115
+ const rows = this.db
116
+ .query(
117
+ `SELECT * FROM auth_users WHERE project_id = ? AND id IN (${ids.map(() => "?").join(", ")})`
118
+ )
119
+ .all(params.projectId, ...ids) as SqliteAuthUserRow[]
120
+
121
+ return rows.map(rowToUserRecord)
122
+ }
123
+
106
124
  async getByEmail(params: {
107
125
  readonly projectId: string
108
126
  readonly email: string
@@ -111,77 +129,87 @@ export class SqliteAuthUserStore implements AuthUserStore {
111
129
  return row ? rowToUserRecord(row) : null
112
130
  }
113
131
 
114
- async updateProfile(input: UpdateAuthUserProfileInput): Promise<UserRecord> {
115
- const existing = getUserRowById(this.db, {
116
- projectId: input.projectId,
117
- id: input.id,
118
- })
132
+ async updateProfile(input: UpdateAuthUserProfileInput): Promise<UpdateAuthUserResult> {
133
+ // One transaction, so the user it returns as previous is the one it changed.
134
+ return runImmediateTransaction(this.db, () => {
135
+ const existing = getUserRowById(this.db, {
136
+ projectId: input.projectId,
137
+ id: input.id,
138
+ })
119
139
 
120
- if (!existing) {
121
- throw new AuthStorageError(
122
- "missing_user",
123
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
124
- )
125
- }
140
+ if (!existing) {
141
+ throw new AuthStorageError(
142
+ "missing_user",
143
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
144
+ )
145
+ }
126
146
 
127
- const updatedAt = dateOrNow(input.updatedAt)
128
- this.db
129
- .query(
147
+ const previousUser = rowToUserRecord(existing)
148
+ if (!changesProfile(previousUser, input)) {
149
+ return { user: previousUser, previousUser }
150
+ }
151
+
152
+ const updatedAt = dateOrNow(input.updatedAt)
153
+ const row = this.db
154
+ .query(
155
+ `
156
+ UPDATE auth_users
157
+ SET display_name = CASE WHEN ? THEN ? ELSE display_name END,
158
+ avatar_url = CASE WHEN ? THEN ? ELSE avatar_url END,
159
+ updated_at = ?
160
+ WHERE project_id = ?
161
+ AND id = ?
162
+ RETURNING *
130
163
  `
131
- UPDATE auth_users
132
- SET display_name = ?,
133
- avatar_url = ?,
134
- updated_at = ?
135
- WHERE project_id = ?
136
- AND id = ?
137
- `
138
- )
139
- .run(
140
- input.displayName ?? null,
141
- input.avatarUrl ?? null,
142
- toIso(updatedAt),
143
- input.projectId,
144
- input.id
145
- )
164
+ )
165
+ .get(
166
+ input.displayName === undefined ? 0 : 1,
167
+ input.displayName ?? null,
168
+ input.avatarUrl === undefined ? 0 : 1,
169
+ input.avatarUrl ?? null,
170
+ toIso(updatedAt),
171
+ input.projectId,
172
+ input.id
173
+ ) as SqliteAuthUserRow
146
174
 
147
- return rowToUserRecord({
148
- ...existing,
149
- display_name: input.displayName ?? null,
150
- avatar_url: input.avatarUrl ?? null,
151
- updated_at: toIso(updatedAt),
175
+ return { user: rowToUserRecord(row), previousUser }
152
176
  })
153
177
  }
154
178
 
155
- async updateStatus(input: UpdateAuthUserStatusInput): Promise<UserRecord> {
156
- const existing = getUserRowById(this.db, {
157
- projectId: input.projectId,
158
- id: input.id,
159
- })
179
+ async updateStatus(input: UpdateAuthUserStatusInput): Promise<UpdateAuthUserResult> {
180
+ // One transaction, so two status changes cannot both report the same previous one.
181
+ return runImmediateTransaction(this.db, () => {
182
+ const existing = getUserRowById(this.db, {
183
+ projectId: input.projectId,
184
+ id: input.id,
185
+ })
160
186
 
161
- if (!existing) {
162
- throw new AuthStorageError(
163
- "missing_user",
164
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
165
- )
166
- }
187
+ if (!existing) {
188
+ throw new AuthStorageError(
189
+ "missing_user",
190
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
191
+ )
192
+ }
167
193
 
168
- const updatedAt = dateOrNow(input.updatedAt)
169
- this.db
170
- .query(
194
+ const updatedAt = dateOrNow(input.updatedAt)
195
+ this.db
196
+ .query(
197
+ `
198
+ UPDATE auth_users
199
+ SET status = ?,
200
+ updated_at = ?
201
+ WHERE project_id = ?
202
+ AND id = ?
171
203
  `
172
- UPDATE auth_users
173
- SET status = ?,
174
- updated_at = ?
175
- WHERE project_id = ?
176
- AND id = ?
177
- `
178
- )
179
- .run(input.status, toIso(updatedAt), input.projectId, input.id)
204
+ )
205
+ .run(input.status, toIso(updatedAt), input.projectId, input.id)
180
206
 
181
- return rowToUserRecord({
182
- ...existing,
183
- status: input.status,
184
- updated_at: toIso(updatedAt),
207
+ const user = rowToUserRecord({
208
+ ...existing,
209
+ status: input.status,
210
+ updated_at: toIso(updatedAt),
211
+ })
212
+ return { user, previousUser: rowToUserRecord(existing) }
185
213
  })
186
214
  }
187
215
 
@@ -198,6 +226,15 @@ export class SqliteAuthUserStore implements AuthUserStore {
198
226
  args.push(...input.statuses)
199
227
  }
200
228
 
229
+ // `instr` matches literally, so `%` and `_` need no escaping. Both sides fold through the same
230
+ // `lower()`, which ignores case for ASCII letters only: "émilie" does not find "Émilie" here.
231
+ if (input.search !== undefined) {
232
+ whereClauses.push(
233
+ "(instr(lower(coalesce(display_name, '')), lower(?)) > 0 OR instr(lower(email), lower(?)) > 0)"
234
+ )
235
+ args.push(input.search, input.search)
236
+ }
237
+
201
238
  const where = `WHERE ${whereClauses.join(" AND ")}`
202
239
  const order = input.order === "asc" ? "ASC" : "DESC"
203
240
  const totalRow = this.db
@@ -224,3 +261,12 @@ export class SqliteAuthUserStore implements AuthUserStore {
224
261
  }
225
262
  }
226
263
  }
264
+
265
+ // Omitted keeps the stored value, and `null` clears it.
266
+ function changesProfile(user: UserRecord, input: UpdateAuthUserProfileInput): boolean {
267
+ const displayName =
268
+ input.displayName === undefined ? user.displayName : (input.displayName ?? undefined)
269
+ const avatarUrl = input.avatarUrl === undefined ? user.avatarUrl : (input.avatarUrl ?? undefined)
270
+
271
+ return displayName !== user.displayName || avatarUrl !== user.avatarUrl
272
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import {
3
+ compareExecutionCreation,
3
4
  type ExecutionStorageRow,
4
5
  executionRecordFromStorageRow,
5
6
  executionRecordToStorageRow,
@@ -7,8 +8,12 @@ import {
7
8
  } from "@sixb/core/internal/execution-storage"
8
9
  import type {
9
10
  CreateExecutionInput,
11
+ ExecutionDescendant,
10
12
  ExecutionRecord,
11
13
  ExecutionStorage,
14
+ ListExecutionDescendantsInput,
15
+ ListSessionExecutionsInput,
16
+ SessionExecutions,
12
17
  ShareSessionStorage,
13
18
  } from "@sixb/core/storage"
14
19
  import { ExecutionStorageError } from "@sixb/core/storage"
@@ -115,6 +120,96 @@ export class SqliteExecutionStorage implements ExecutionStorage {
115
120
 
116
121
  return row ? sqliteExecutionRecord(row) : null
117
122
  }
123
+
124
+ async listBySession(input: ListSessionExecutionsInput): Promise<SessionExecutions> {
125
+ const sessionColumn =
126
+ input.session.type === "user" ? "authority_session_id" : "authority_delegation_session_id"
127
+
128
+ // The queue is ordered by depth, so the walk takes roots, then their children level by level,
129
+ // the earliest first, and the limit stops it: a large tree costs no more than the rows
130
+ // returned. CROSS JOIN keeps the walk driven by the rows found: statistics taken on a small
131
+ // ledger choose to scan every child.
132
+ const rows = this.db
133
+ .query(
134
+ `
135
+ WITH RECURSIVE tree AS (
136
+ SELECT roots.*, 0 AS depth
137
+ FROM (
138
+ SELECT *
139
+ FROM executions
140
+ WHERE project_id = ?
141
+ AND ${sessionColumn} = ?
142
+ AND parent_execution_id IS NULL
143
+ AND created_at >= ?
144
+ AND created_at < ?
145
+ ORDER BY created_at, id
146
+ LIMIT ?
147
+ ) roots
148
+ UNION ALL
149
+ SELECT child.*, tree.depth + 1
150
+ FROM tree
151
+ CROSS JOIN executions child
152
+ ON child.project_id = tree.project_id
153
+ AND child.parent_execution_id = tree.id
154
+ ORDER BY depth, created_at, id
155
+ LIMIT ?
156
+ )
157
+ SELECT * FROM tree
158
+ `
159
+ )
160
+ .all(
161
+ input.projectId,
162
+ input.session.id,
163
+ input.from.toISOString(),
164
+ input.to.toISOString(),
165
+ input.limit + 1,
166
+ input.limit + 1
167
+ ) as SqliteExecutionRow[]
168
+
169
+ // Rows come out in walk order, so the first `limit` are the ones the contract keeps.
170
+ const kept = rows.slice(0, input.limit).map(sqliteExecutionRecord)
171
+ return {
172
+ executions: kept.sort(compareExecutionCreation),
173
+ truncated: rows.length > input.limit,
174
+ }
175
+ }
176
+
177
+ async listDescendants(
178
+ input: ListExecutionDescendantsInput
179
+ ): Promise<readonly ExecutionDescendant[]> {
180
+ if (input.executionIds.length === 0) return []
181
+ const ids = [...new Set(input.executionIds)]
182
+
183
+ // UNION, not UNION ALL: an execution asked about may sit below another one asked about, and
184
+ // the walk would otherwise reach what it caused twice.
185
+ const rows = this.db
186
+ .query(
187
+ `
188
+ WITH RECURSIVE tree(id, parent_id) AS (
189
+ SELECT child.id, child.parent_execution_id
190
+ FROM executions child
191
+ WHERE child.project_id = ?
192
+ AND child.parent_execution_id IN (SELECT value FROM json_each(?))
193
+ UNION
194
+ SELECT child.id, child.parent_execution_id
195
+ FROM tree
196
+ CROSS JOIN executions child
197
+ ON child.project_id = ?
198
+ AND child.parent_execution_id = tree.id
199
+ )
200
+ SELECT id, parent_id
201
+ FROM tree
202
+ WHERE id NOT IN (SELECT value FROM json_each(?))
203
+ ORDER BY id
204
+ `
205
+ )
206
+ .all(input.projectId, JSON.stringify(ids), input.projectId, JSON.stringify(ids)) as {
207
+ id: string
208
+ parent_id: string
209
+ }[]
210
+
211
+ return rows.map((row) => ({ id: row.id, parentId: row.parent_id }))
212
+ }
118
213
  }
119
214
 
120
215
  /** An `executions` row as its record, for any store that reads executions alongside its own rows. */
package/src/index.ts CHANGED
@@ -32,10 +32,12 @@ import {
32
32
  StorageTransactionError,
33
33
  } from "@sixb/core/storage"
34
34
  import { SqliteActionRunStorage } from "./action-run-storage"
35
+ import { SqliteActivityStorage } from "./activity-storage"
35
36
  import { SqliteAgentStorage } from "./agents"
36
37
  import { SqliteAiCostStorage } from "./ai-cost-storage"
37
38
  import { SqliteAiLimitStorage } from "./ai-limit-storage"
38
39
  import { SqliteAiUsageStorage } from "./ai-usage-storage"
40
+ import { SqliteArtifactStorage } from "./artifact-storage"
39
41
  import { SqliteAuthStorage } from "./auth-storage"
40
42
  import { SqliteConnectorConnectionStorage } from "./connector-connection-storage"
41
43
  import { SqliteExecutionStorage } from "./execution-storage"
@@ -89,6 +91,7 @@ export class SqliteStorage implements MigrationCapableStorage {
89
91
  readonly auth: SqliteAuthStorage
90
92
  readonly executions: SqliteExecutionStorage
91
93
  readonly agents: SqliteAgentStorage
94
+ readonly artifacts: SqliteArtifactStorage
92
95
  readonly aiUsage: AiUsageStorage
93
96
  readonly aiCosts: AiCostStorage
94
97
  readonly aiLimits: AiLimitStorage
@@ -106,6 +109,7 @@ export class SqliteStorage implements MigrationCapableStorage {
106
109
  readonly connectorConnections: SqliteConnectorConnectionStorage
107
110
  readonly fileUploadSessions: SqliteFileUploadSessionStorage
108
111
  readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
112
+ readonly activity: SqliteActivityStorage
109
113
  readonly migrators: readonly StorageMigrator[]
110
114
 
111
115
  private readonly connection: SqliteStoreConnection
@@ -153,6 +157,7 @@ export class SqliteStorage implements MigrationCapableStorage {
153
157
  this.auth = createAuthOperationScope(stores.auth, scope)
154
158
  this.executions = createOperationScopedFacade(stores.executions, scope)
155
159
  this.agents = createAgentOperationScope(stores.agents, scope)
160
+ this.artifacts = createOperationScopedFacade(stores.artifacts, scope)
156
161
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
157
162
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
158
163
  this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
@@ -170,6 +175,7 @@ export class SqliteStorage implements MigrationCapableStorage {
170
175
  this.connectorConnections = createOperationScopedFacade(stores.connectorConnections, scope)
171
176
  this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
172
177
  this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope)
178
+ this.activity = createOperationScopedFacade(stores.activity, scope)
173
179
  this.migrators = options.path ? createSqliteStorageMigrators(options.path) : []
174
180
  registerSqliteStorageTestingAdapter(this, {
175
181
  advanceConnectorConnectionTime: (durationMs) =>
@@ -317,6 +323,7 @@ function createSqliteStores(
317
323
  auth,
318
324
  executions,
319
325
  agents: new SqliteAgentStorage({ connection, executions }),
326
+ artifacts: new SqliteArtifactStorage(connection),
320
327
  aiUsage: new SqliteAiUsageStorage({ connection }),
321
328
  aiCosts: new SqliteAiCostStorage(connection),
322
329
  aiLimits: new SqliteAiLimitStorage({ connection }),
@@ -334,6 +341,7 @@ function createSqliteStores(
334
341
  connectorConnections: new SqliteConnectorConnectionStorage(connection),
335
342
  fileUploadSessions: new SqliteFileUploadSessionStorage(connection),
336
343
  fileDownloadGrants: new SqliteFileDownloadGrantStorage(connection),
344
+ activity: new SqliteActivityStorage(connection),
337
345
  }
338
346
  }
339
347
 
@@ -343,6 +351,7 @@ interface SqliteStoreSet {
343
351
  readonly auth: SqliteAuthStorage
344
352
  readonly executions: SqliteExecutionStorage
345
353
  readonly agents: SqliteAgentStorage
354
+ readonly artifacts: SqliteArtifactStorage
346
355
  readonly aiUsage: SqliteAiUsageStorage
347
356
  readonly aiCosts: SqliteAiCostStorage
348
357
  readonly aiLimits: SqliteAiLimitStorage
@@ -360,6 +369,7 @@ interface SqliteStoreSet {
360
369
  readonly connectorConnections: SqliteConnectorConnectionStorage
361
370
  readonly fileUploadSessions: SqliteFileUploadSessionStorage
362
371
  readonly fileDownloadGrants: SqliteFileDownloadGrantStorage
372
+ readonly activity: SqliteActivityStorage
363
373
  }
364
374
 
365
375
  export { migrateSqliteStorage } from "./migrations"
@@ -0,0 +1,71 @@
1
+ -- Artifacts: versioned documents, sheets, decks, and files. An artifact's kind never
2
+ -- changes. Versions take `next_version`, which only grows, so a number is never reused after its
3
+ -- version is deleted. Timestamps are ISO-8601 UTC text and compare lexicographically.
4
+ CREATE TABLE artifacts (
5
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
6
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
7
+ kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'file')),
8
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
9
+ owner_principal_type TEXT NOT NULL
10
+ CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
11
+ owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
12
+ latest_version INTEGER NOT NULL CHECK (latest_version > 0),
13
+ next_version INTEGER NOT NULL CHECK (next_version > latest_version),
14
+ created_at TEXT NOT NULL,
15
+ updated_at TEXT NOT NULL,
16
+ PRIMARY KEY (project_id, id)
17
+ );
18
+
19
+ CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
20
+ CREATE INDEX idx_artifacts_owner_updated
21
+ ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
22
+
23
+ -- `refs`, not `references`: that word is reserved in SQL.
24
+ CREATE TABLE artifact_versions (
25
+ project_id TEXT NOT NULL,
26
+ artifact_id TEXT NOT NULL,
27
+ version INTEGER NOT NULL CHECK (version > 0),
28
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
29
+ entry TEXT NOT NULL CHECK (length(entry) > 0),
30
+ files TEXT NOT NULL CHECK (json_type(files) = 'array'),
31
+ outline TEXT NOT NULL CHECK (json_type(outline) = 'object'),
32
+ refs TEXT NOT NULL CHECK (json_type(refs) = 'array'),
33
+ author_principal_type TEXT NOT NULL
34
+ CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
35
+ author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
36
+ origin_thread_id TEXT,
37
+ origin_run_id TEXT,
38
+ created_at TEXT NOT NULL,
39
+ PRIMARY KEY (project_id, artifact_id, version),
40
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
41
+ CHECK ((origin_thread_id IS NULL) = (origin_run_id IS NULL))
42
+ );
43
+
44
+ CREATE INDEX idx_artifact_versions_origin_run
45
+ ON artifact_versions (project_id, origin_run_id) WHERE origin_run_id IS NOT NULL;
46
+ CREATE INDEX idx_artifact_versions_origin_thread
47
+ ON artifact_versions (project_id, origin_thread_id, artifact_id) WHERE origin_thread_id IS NOT NULL;
48
+
49
+ -- Short-lived links that a sandboxed frame loads a deck's files through. Only the SHA-256 of each
50
+ -- link's token is stored. `targets` is what the frame may show from other artifacts, decided for
51
+ -- its reader when the link was issued: a JSON array of { address, artifactId, version }.
52
+ CREATE TABLE artifact_frame_grants (
53
+ project_id TEXT NOT NULL,
54
+ token_hash TEXT NOT NULL CHECK (
55
+ length(token_hash) = 64
56
+ AND token_hash = lower(token_hash)
57
+ AND token_hash NOT GLOB '*[^0-9a-f]*'
58
+ ),
59
+ artifact_id TEXT NOT NULL,
60
+ version INTEGER NOT NULL CHECK (version > 0),
61
+ targets TEXT NOT NULL CHECK (json_type(targets) = 'array'),
62
+ created_at TEXT NOT NULL,
63
+ expires_at TEXT NOT NULL,
64
+ PRIMARY KEY (project_id, token_hash),
65
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
66
+ CHECK (expires_at > created_at)
67
+ );
68
+
69
+ CREATE INDEX idx_artifact_frame_grants_expiry ON artifact_frame_grants (project_id, expires_at);
70
+ -- Deleting an artifact cascades here.
71
+ CREATE INDEX idx_artifact_frame_grants_artifact ON artifact_frame_grants (project_id, artifact_id);