@sixb/sqlite 0.1.20 → 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 (40) 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/threads.d.ts +4 -0
  6. package/dist/agents/threads.d.ts.map +1 -1
  7. package/dist/artifact-storage.d.ts +22 -0
  8. package/dist/artifact-storage.d.ts.map +1 -0
  9. package/dist/auth-storage/sessions.d.ts +2 -1
  10. package/dist/auth-storage/sessions.d.ts.map +1 -1
  11. package/dist/execution-storage.d.ts +2 -1
  12. package/dist/execution-storage.d.ts.map +1 -1
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +880 -14
  16. package/dist/index.js.map +14 -12
  17. package/dist/migrations.d.ts.map +1 -1
  18. package/dist/ontology-storage/sources.d.ts +2 -1
  19. package/dist/ontology-storage/sources.d.ts.map +1 -1
  20. package/dist/share-grant-storage.d.ts +2 -1
  21. package/dist/share-grant-storage.d.ts.map +1 -1
  22. package/dist/workflow-run-storage.d.ts +4 -0
  23. package/dist/workflow-run-storage.d.ts.map +1 -1
  24. package/package.json +3 -3
  25. package/src/action-run-storage.ts +27 -0
  26. package/src/activity-storage.ts +588 -0
  27. package/src/agents/threads.ts +27 -0
  28. package/src/artifact-storage.ts +356 -0
  29. package/src/auth-storage/sessions.ts +27 -0
  30. package/src/execution-storage.ts +56 -0
  31. package/src/index.ts +10 -0
  32. package/src/migrations/062-artifacts.sql +71 -0
  33. package/src/migrations/063-drop-output-diagnostics.sql +19 -0
  34. package/src/migrations/064-activity.sql +61 -0
  35. package/src/migrations/065-activity-timeline.sql +19 -0
  36. package/src/migrations/066-activity-interactions.sql +20 -0
  37. package/src/migrations.ts +14 -0
  38. package/src/ontology-storage/sources.ts +41 -0
  39. package/src/share-grant-storage.ts +25 -0
  40. package/src/workflow-run-storage.ts +27 -0
@@ -0,0 +1,356 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite"
2
+ import {
3
+ artifactStorageErrors,
4
+ parseArtifactFrameGrantRow,
5
+ parseArtifactRow,
6
+ parseArtifactVersionRow,
7
+ } from "@sixb/core/internal/storage"
8
+ import type {
9
+ ArtifactFrameGrantRecord,
10
+ ArtifactRecord,
11
+ ArtifactStorage,
12
+ ArtifactVersionInput,
13
+ ArtifactVersionRecord,
14
+ CreateArtifactInput,
15
+ DeleteArtifactInput,
16
+ DeleteArtifactVersionsByOriginInput,
17
+ DeleteExpiredArtifactFrameGrantsInput,
18
+ FindArtifactFrameGrantInput,
19
+ GetArtifactInput,
20
+ GetArtifactVersionInput,
21
+ ListArtifactsInput,
22
+ ListArtifactsResult,
23
+ ListArtifactVersionsInput,
24
+ ListArtifactVersionsResult,
25
+ ListReferencingVersionsInput,
26
+ PublishArtifactVersionInput,
27
+ PublishedArtifactVersion,
28
+ } from "@sixb/core/storage"
29
+ import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
30
+
31
+ const ARTIFACT_COLUMNS = `
32
+ project_id AS projectId, id, kind, title,
33
+ owner_principal_type AS ownerPrincipalType, owner_principal_id AS ownerPrincipalId,
34
+ latest_version AS latestVersion, created_at AS createdAt, updated_at AS updatedAt
35
+ `
36
+
37
+ const VERSION_COLUMNS = `
38
+ project_id AS projectId, artifact_id AS artifactId, version, title, entry, files, outline, refs,
39
+ author_principal_type AS authorPrincipalType, author_principal_id AS authorPrincipalId,
40
+ origin_thread_id AS originThreadId, origin_run_id AS originRunId, created_at AS createdAt
41
+ `
42
+
43
+ const FRAME_GRANT_COLUMNS = `
44
+ project_id AS projectId, token_hash AS tokenHash, artifact_id AS artifactId, version, targets,
45
+ created_at AS createdAt, expires_at AS expiresAt
46
+ `
47
+
48
+ type Row = Record<string, unknown>
49
+
50
+ export class SqliteArtifactStorage implements ArtifactStorage {
51
+ private readonly db: Database
52
+
53
+ constructor(connection: SqliteStoreConnection) {
54
+ this.db = connection.db
55
+ }
56
+
57
+ async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
58
+ return runImmediateTransaction(this.db, () => {
59
+ const createdAt = input.version.createdAt.toISOString()
60
+ const artifact = this.db
61
+ .query(
62
+ `INSERT INTO artifacts (
63
+ project_id, id, kind, title, owner_principal_type, owner_principal_id,
64
+ latest_version, next_version, created_at, updated_at
65
+ ) VALUES (?, ?, ?, ?, ?, ?, 1, 2, ?, ?)
66
+ ON CONFLICT (project_id, id) DO NOTHING
67
+ RETURNING ${ARTIFACT_COLUMNS}`
68
+ )
69
+ .get(
70
+ input.projectId,
71
+ input.id,
72
+ input.kind,
73
+ input.version.title,
74
+ input.owner.type,
75
+ input.owner.id,
76
+ createdAt,
77
+ createdAt
78
+ ) as Row | null
79
+ if (!artifact) throw artifactStorageErrors.exists(input.id)
80
+ return {
81
+ artifact: parseArtifactRow(artifact),
82
+ version: this.insertVersion(input.projectId, input.id, 1, input.version),
83
+ }
84
+ })
85
+ }
86
+
87
+ async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
88
+ return runImmediateTransaction(this.db, () => {
89
+ const current = this.db
90
+ .query(
91
+ "SELECT kind, next_version AS nextVersion FROM artifacts WHERE project_id = ? AND id = ?"
92
+ )
93
+ .get(input.projectId, input.artifactId) as {
94
+ readonly kind: ArtifactRecord["kind"]
95
+ readonly nextVersion: number
96
+ } | null
97
+ if (!current) throw artifactStorageErrors.notFound(input.artifactId)
98
+ if (current.kind !== input.kind) {
99
+ throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
100
+ }
101
+ const version = this.insertVersion(
102
+ input.projectId,
103
+ input.artifactId,
104
+ current.nextVersion,
105
+ input.version
106
+ )
107
+ const artifact = this.db
108
+ .query(
109
+ `UPDATE artifacts
110
+ SET title = ?, latest_version = ?, next_version = ?, updated_at = ?
111
+ WHERE project_id = ? AND id = ?
112
+ RETURNING ${ARTIFACT_COLUMNS}`
113
+ )
114
+ .get(
115
+ input.version.title,
116
+ version.version,
117
+ version.version + 1,
118
+ input.version.createdAt.toISOString(),
119
+ input.projectId,
120
+ input.artifactId
121
+ ) as Row
122
+ return { artifact: parseArtifactRow(artifact), version }
123
+ })
124
+ }
125
+
126
+ async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
127
+ const row = this.db
128
+ .query(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE project_id = ? AND id = ?`)
129
+ .get(input.projectId, input.id) as Row | null
130
+ return row ? parseArtifactRow(row) : null
131
+ }
132
+
133
+ async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
134
+ const where = ["a.project_id = ?"]
135
+ const params: SQLQueryBindings[] = [input.projectId]
136
+ if (input.owner) {
137
+ where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
138
+ params.push(input.owner.type, input.owner.id)
139
+ }
140
+ if (input.originThreadId !== undefined) {
141
+ where.push(
142
+ `EXISTS (
143
+ SELECT 1 FROM artifact_versions v
144
+ WHERE v.project_id = a.project_id AND v.artifact_id = a.id AND v.origin_thread_id = ?
145
+ )`
146
+ )
147
+ params.push(input.originThreadId)
148
+ }
149
+ if (input.kind !== undefined) {
150
+ where.push("a.kind = ?")
151
+ params.push(input.kind)
152
+ }
153
+ const clause = where.join(" AND ")
154
+ const { total } = this.db
155
+ .query(`SELECT COUNT(*) AS total FROM artifacts a WHERE ${clause}`)
156
+ .get(...params) as { readonly total: number }
157
+ const offset = input.offset ?? 0
158
+ const rows = this.db
159
+ .query(
160
+ `SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
161
+ ORDER BY a.updated_at DESC, a.id ASC LIMIT ? OFFSET ?`
162
+ )
163
+ .all(...params, input.limit ?? -1, offset) as Row[]
164
+ return {
165
+ artifacts: rows.map(parseArtifactRow),
166
+ hasMore: offset + rows.length < total,
167
+ total,
168
+ }
169
+ }
170
+
171
+ async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
172
+ const row = this.db
173
+ .query(
174
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
175
+ WHERE project_id = ? AND artifact_id = ? AND version = ?`
176
+ )
177
+ .get(input.projectId, input.artifactId, input.version) as Row | null
178
+ return row ? parseArtifactVersionRow(row) : null
179
+ }
180
+
181
+ async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
182
+ const { total } = this.db
183
+ .query(
184
+ "SELECT COUNT(*) AS total FROM artifact_versions WHERE project_id = ? AND artifact_id = ?"
185
+ )
186
+ .get(input.projectId, input.artifactId) as { readonly total: number }
187
+ const offset = input.offset ?? 0
188
+ const rows = this.db
189
+ .query(
190
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
191
+ WHERE project_id = ? AND artifact_id = ?
192
+ ORDER BY version DESC LIMIT ? OFFSET ?`
193
+ )
194
+ .all(input.projectId, input.artifactId, input.limit ?? -1, offset) as Row[]
195
+ return {
196
+ versions: rows.map(parseArtifactVersionRow),
197
+ hasMore: offset + rows.length < total,
198
+ total,
199
+ }
200
+ }
201
+
202
+ async listReferencingVersions(
203
+ input: ListReferencingVersionsInput
204
+ ): Promise<ArtifactVersionRecord[]> {
205
+ // An address names the artifact when the id is followed by nothing, a version, a file, or a
206
+ // part.
207
+ const prefix = `artifact:${input.artifactId}`
208
+ const rows = this.db
209
+ .query(
210
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions v
211
+ WHERE (project_id, artifact_id, version) IN (
212
+ SELECT project_id, id, latest_version FROM artifacts WHERE project_id = ?
213
+ ) AND EXISTS (
214
+ SELECT 1 FROM json_each(v.refs) r
215
+ WHERE substr(json_extract(r.value, '$.address'), 1, ?) = ?
216
+ AND substr(json_extract(r.value, '$.address'), ? + 1, 1) IN ('', '@', '/', '#')
217
+ )
218
+ ORDER BY artifact_id`
219
+ )
220
+ .all(input.projectId, prefix.length, prefix, prefix.length) as Row[]
221
+ return rows.map(parseArtifactVersionRow)
222
+ }
223
+
224
+ async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
225
+ runImmediateTransaction(this.db, () => {
226
+ const exists = this.db
227
+ .query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
228
+ .get(record.projectId, record.artifactId)
229
+ if (!exists) throw artifactStorageErrors.notFound(record.artifactId)
230
+ this.db
231
+ .query(
232
+ `INSERT INTO artifact_frame_grants
233
+ (project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
234
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
235
+ )
236
+ .run(
237
+ record.projectId,
238
+ record.tokenHash,
239
+ record.artifactId,
240
+ record.version,
241
+ JSON.stringify(record.targets),
242
+ record.createdAt.toISOString(),
243
+ record.expiresAt.toISOString()
244
+ )
245
+ })
246
+ }
247
+
248
+ async findFrameGrant(
249
+ input: FindArtifactFrameGrantInput
250
+ ): Promise<ArtifactFrameGrantRecord | null> {
251
+ const row = this.db
252
+ .query(
253
+ `SELECT ${FRAME_GRANT_COLUMNS} FROM artifact_frame_grants
254
+ WHERE project_id = ? AND token_hash = ?`
255
+ )
256
+ .get(input.projectId, input.tokenHash) as Row | null
257
+ return row ? parseArtifactFrameGrantRow(row) : null
258
+ }
259
+
260
+ async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
261
+ return this.db
262
+ .query(
263
+ `DELETE FROM artifact_frame_grants WHERE rowid IN (
264
+ SELECT rowid FROM artifact_frame_grants
265
+ WHERE project_id = ? AND expires_at < ?
266
+ ORDER BY expires_at LIMIT ?
267
+ )`
268
+ )
269
+ .run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
270
+ }
271
+
272
+ async delete(input: DeleteArtifactInput): Promise<boolean> {
273
+ // Versions and frame grants go with the artifact through ON DELETE CASCADE.
274
+ return (
275
+ this.db
276
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
277
+ .run(input.projectId, input.id).changes > 0
278
+ )
279
+ }
280
+
281
+ async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
282
+ return runImmediateTransaction(this.db, () => {
283
+ const affected = this.db
284
+ .query(
285
+ `SELECT DISTINCT artifact_id AS artifactId FROM artifact_versions
286
+ WHERE project_id = ? AND origin_run_id = ?`
287
+ )
288
+ .all(input.projectId, input.runId) as { readonly artifactId: string }[]
289
+ const deleted = this.db
290
+ .query("DELETE FROM artifact_versions WHERE project_id = ? AND origin_run_id = ?")
291
+ .run(input.projectId, input.runId).changes
292
+ for (const { artifactId } of affected) {
293
+ this.repointLatest(input.projectId, artifactId)
294
+ }
295
+ return deleted
296
+ })
297
+ }
298
+
299
+ /** Point an artifact at its newest remaining version, or delete it when none remains. */
300
+ private repointLatest(projectId: string, artifactId: string): void {
301
+ const latest = this.db
302
+ .query(
303
+ `SELECT version, title, created_at AS createdAt FROM artifact_versions
304
+ WHERE project_id = ? AND artifact_id = ? ORDER BY version DESC LIMIT 1`
305
+ )
306
+ .get(projectId, artifactId) as {
307
+ readonly version: number
308
+ readonly title: string
309
+ readonly createdAt: string
310
+ } | null
311
+ if (!latest) {
312
+ this.db
313
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
314
+ .run(projectId, artifactId)
315
+ return
316
+ }
317
+ this.db
318
+ .query(
319
+ `UPDATE artifacts SET title = ?, latest_version = ?, updated_at = ?
320
+ WHERE project_id = ? AND id = ?`
321
+ )
322
+ .run(latest.title, latest.version, latest.createdAt, projectId, artifactId)
323
+ }
324
+
325
+ private insertVersion(
326
+ projectId: string,
327
+ artifactId: string,
328
+ version: number,
329
+ input: ArtifactVersionInput
330
+ ): ArtifactVersionRecord {
331
+ const row = this.db
332
+ .query(
333
+ `INSERT INTO artifact_versions (
334
+ project_id, artifact_id, version, title, entry, files, outline, refs,
335
+ author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
336
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
337
+ RETURNING ${VERSION_COLUMNS}`
338
+ )
339
+ .get(
340
+ projectId,
341
+ artifactId,
342
+ version,
343
+ input.title,
344
+ input.entry,
345
+ JSON.stringify(input.files),
346
+ JSON.stringify(input.outline),
347
+ JSON.stringify(input.references),
348
+ input.author.type,
349
+ input.author.id,
350
+ input.origin?.threadId ?? null,
351
+ input.origin?.runId ?? null,
352
+ input.createdAt.toISOString()
353
+ ) as Row
354
+ return parseArtifactVersionRow(row)
355
+ }
356
+ }
@@ -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
@@ -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,
@@ -9,6 +10,8 @@ import type {
9
10
  CreateExecutionInput,
10
11
  ExecutionRecord,
11
12
  ExecutionStorage,
13
+ ListSessionExecutionsInput,
14
+ SessionExecutions,
12
15
  ShareSessionStorage,
13
16
  } from "@sixb/core/storage"
14
17
  import { ExecutionStorageError } from "@sixb/core/storage"
@@ -115,6 +118,59 @@ export class SqliteExecutionStorage implements ExecutionStorage {
115
118
 
116
119
  return row ? sqliteExecutionRecord(row) : null
117
120
  }
121
+
122
+ async listBySession(input: ListSessionExecutionsInput): Promise<SessionExecutions> {
123
+ const sessionColumn =
124
+ input.session.type === "user" ? "authority_session_id" : "authority_delegation_session_id"
125
+
126
+ // The queue is ordered by depth, so the walk takes roots, then their children level by level,
127
+ // the earliest first, and the limit stops it: a large tree costs no more than the rows
128
+ // returned. CROSS JOIN keeps the walk driven by the rows found: statistics taken on a small
129
+ // ledger choose to scan every child.
130
+ const rows = this.db
131
+ .query(
132
+ `
133
+ WITH RECURSIVE tree AS (
134
+ SELECT roots.*, 0 AS depth
135
+ FROM (
136
+ SELECT *
137
+ FROM executions
138
+ WHERE project_id = ?
139
+ AND ${sessionColumn} = ?
140
+ AND parent_execution_id IS NULL
141
+ AND created_at >= ?
142
+ AND created_at < ?
143
+ ORDER BY created_at, id
144
+ LIMIT ?
145
+ ) roots
146
+ UNION ALL
147
+ SELECT child.*, tree.depth + 1
148
+ FROM tree
149
+ CROSS JOIN executions child
150
+ ON child.project_id = tree.project_id
151
+ AND child.parent_execution_id = tree.id
152
+ ORDER BY depth, created_at, id
153
+ LIMIT ?
154
+ )
155
+ SELECT * FROM tree
156
+ `
157
+ )
158
+ .all(
159
+ input.projectId,
160
+ input.session.id,
161
+ input.from.toISOString(),
162
+ input.to.toISOString(),
163
+ input.limit + 1,
164
+ input.limit + 1
165
+ ) as SqliteExecutionRow[]
166
+
167
+ // Rows come out in walk order, so the first `limit` are the ones the contract keeps.
168
+ const kept = rows.slice(0, input.limit).map(sqliteExecutionRecord)
169
+ return {
170
+ executions: kept.sort(compareExecutionCreation),
171
+ truncated: rows.length > input.limit,
172
+ }
173
+ }
118
174
  }
119
175
 
120
176
  /** 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);
@@ -0,0 +1,19 @@
1
+ -- The Agent delivers files by publishing artifacts, so a run no longer collects an output
2
+ -- directory or reports on it. Stored `output_*` diagnostics go; a run left with none stores none.
3
+ UPDATE agent_runs
4
+ SET diagnostics = (
5
+ SELECT CASE WHEN count(*) = 0 THEN NULL ELSE json_group_array(json(value)) END
6
+ FROM (
7
+ SELECT value
8
+ FROM json_each(agent_runs.diagnostics)
9
+ WHERE json_extract(value, '$.code') NOT LIKE 'output\_%' ESCAPE '\'
10
+ ORDER BY key
11
+ )
12
+ )
13
+ WHERE diagnostics IS NOT NULL
14
+ AND json_valid(diagnostics)
15
+ AND EXISTS (
16
+ SELECT 1
17
+ FROM json_each(agent_runs.diagnostics)
18
+ WHERE json_extract(value, '$.code') LIKE 'output\_%' ESCAPE '\'
19
+ );