@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
@@ -1,14 +1,23 @@
1
- import { assertAiUsageExecutionId, normalizeAiModelCallRecord } from "@sixb/core/internal/storage"
1
+ import type { FileRef } from "@sixb/core/blob-storage/server"
2
+ import {
3
+ assertAiUsageExecutionId,
4
+ assertSameCallExecution,
5
+ normalizeAiModelCallFilesRecord,
6
+ normalizeAiModelCallRecord,
7
+ } from "@sixb/core/internal/storage"
2
8
  import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
3
9
  import type { ReadonlyJsonObject } from "@sixb/core/storage"
4
10
  import {
11
+ type AiModelCallFilesRecord,
5
12
  type AiModelCallUsageInput,
6
13
  type AiModelCallUsageRecord,
7
14
  type AiUsageExecutionSummary,
8
15
  type AiUsageStorage,
9
16
  AiUsageStorageError,
10
17
  aggregateAiModelCallUsage,
18
+ type GetAiModelCallFilesInput,
11
19
  type GetLatestAiModelCallForExecutionInput,
20
+ type RecordAiModelCallFilesInput,
12
21
  type RecordAiModelCallInput,
13
22
  type RecordAiModelCallResult,
14
23
  type SummarizeAiUsageExecutionInput,
@@ -110,10 +119,54 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
110
119
  return aggregateExecutionRows(input.executionIds, rows)
111
120
  }
112
121
 
122
+ async recordModelCallFiles(input: RecordAiModelCallFilesInput): Promise<AiModelCallFilesRecord> {
123
+ const record = normalizeAiModelCallFilesRecord(input)
124
+ return runImmediateTransaction(this.connection.db, () => {
125
+ const existing = this.findCallFiles(record.projectId, record.callId)
126
+ if (existing) return assertSameCallExecution(existing, record)
127
+ this.assertExecutionExists(record.projectId, record.executionId)
128
+ this.connection.db
129
+ .query(
130
+ `
131
+ INSERT INTO ai_model_call_files (project_id, call_id, execution_id, files, recorded_at)
132
+ VALUES (?, ?, ?, ?, ?)
133
+ `
134
+ )
135
+ .run(
136
+ record.projectId,
137
+ record.callId,
138
+ record.executionId,
139
+ JSON.stringify(record.files),
140
+ record.recordedAt.toISOString()
141
+ )
142
+ return record
143
+ })
144
+ }
145
+
146
+ async getModelCallFiles(input: GetAiModelCallFilesInput): Promise<AiModelCallFilesRecord | null> {
147
+ assertNonBlankProjectId(input.projectId)
148
+ return this.findCallFiles(input.projectId, input.callId)
149
+ }
150
+
113
151
  close(): void {
114
152
  closeSqliteStoreConnection(this.connection)
115
153
  }
116
154
 
155
+ private findCallFiles(projectId: string, callId: string): AiModelCallFilesRecord | null {
156
+ const row = this.connection.db
157
+ .query("SELECT * FROM ai_model_call_files WHERE project_id = ? AND call_id = ?")
158
+ .get(projectId, callId) as CallFilesRow | null
159
+ return row
160
+ ? {
161
+ projectId: row.project_id,
162
+ executionId: row.execution_id,
163
+ callId: row.call_id,
164
+ files: JSON.parse(row.files) as FileRef[],
165
+ recordedAt: new Date(row.recorded_at),
166
+ }
167
+ : null
168
+ }
169
+
117
170
  private insertRecord(record: AiModelCallUsageRecord): void {
118
171
  this.connection.db
119
172
  .query(
@@ -140,12 +193,16 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
140
193
  text_output_tokens,
141
194
  reasoning_output_tokens,
142
195
  audio_duration_ms,
196
+ image_output_tokens,
197
+ audio_output_tokens,
198
+ output_images,
199
+ input_characters,
143
200
  reporting_status,
144
201
  raw_usage,
145
202
  occurred_at,
146
203
  recorded_at
147
204
  ) VALUES (
148
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
205
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
149
206
  )
150
207
  `
151
208
  )
@@ -171,6 +228,10 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
171
228
  record.usage.textOutputTokens ?? null,
172
229
  record.usage.reasoningOutputTokens ?? null,
173
230
  record.usage.audioDurationMs ?? null,
231
+ record.usage.imageOutputTokens ?? null,
232
+ record.usage.audioOutputTokens ?? null,
233
+ record.usage.outputImages ?? null,
234
+ record.usage.inputCharacters ?? null,
174
235
  record.usage.reportingStatus,
175
236
  record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage),
176
237
  record.occurredAt.toISOString(),
@@ -285,6 +346,14 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
285
346
  }
286
347
  }
287
348
 
349
+ interface CallFilesRow {
350
+ readonly project_id: string
351
+ readonly call_id: string
352
+ readonly execution_id: string
353
+ readonly files: string
354
+ readonly recorded_at: string
355
+ }
356
+
288
357
  interface AiUsageRow {
289
358
  readonly project_id: string
290
359
  readonly id: string
@@ -307,6 +376,10 @@ interface AiUsageRow {
307
376
  readonly text_output_tokens: number | null
308
377
  readonly reasoning_output_tokens: number | null
309
378
  readonly audio_duration_ms: number | null
379
+ readonly image_output_tokens: number | null
380
+ readonly audio_output_tokens: number | null
381
+ readonly output_images: number | null
382
+ readonly input_characters: number | null
310
383
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
311
384
  readonly raw_usage: string | null
312
385
  readonly occurred_at: string
@@ -349,6 +422,10 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
349
422
  ? {}
350
423
  : { reasoningOutputTokens: row.reasoning_output_tokens }),
351
424
  ...(row.audio_duration_ms === null ? {} : { audioDurationMs: row.audio_duration_ms }),
425
+ ...(row.image_output_tokens === null ? {} : { imageOutputTokens: row.image_output_tokens }),
426
+ ...(row.audio_output_tokens === null ? {} : { audioOutputTokens: row.audio_output_tokens }),
427
+ ...(row.output_images === null ? {} : { outputImages: row.output_images }),
428
+ ...(row.input_characters === null ? {} : { inputCharacters: row.input_characters }),
352
429
  }
353
430
  }
354
431
 
@@ -0,0 +1,553 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite"
2
+ import {
3
+ artifactStateBytes,
4
+ artifactStorageErrors,
5
+ checkArtifactStateWrite,
6
+ parseArtifactFrameGrantRow,
7
+ parseArtifactRow,
8
+ parseArtifactStateRow,
9
+ parseArtifactVersionRow,
10
+ } from "@sixb/core/internal/storage"
11
+ import type {
12
+ ArtifactFrameGrantRecord,
13
+ ArtifactRecord,
14
+ ArtifactStateEntry,
15
+ ArtifactStateKeyInput,
16
+ ArtifactStateScope,
17
+ ArtifactStorage,
18
+ ArtifactVersionInput,
19
+ ArtifactVersionRecord,
20
+ CopyArtifactInput,
21
+ CreateArtifactInput,
22
+ DeleteArtifactInput,
23
+ DeleteArtifactVersionsByOriginInput,
24
+ DeleteExpiredArtifactFrameGrantsInput,
25
+ FindArtifactFrameGrantInput,
26
+ GetArtifactInput,
27
+ GetArtifactVersionInput,
28
+ ListArtifactsInput,
29
+ ListArtifactsResult,
30
+ ListArtifactVersionsInput,
31
+ ListArtifactVersionsResult,
32
+ ListReferencingVersionsInput,
33
+ PublishArtifactVersionInput,
34
+ PublishedArtifactVersion,
35
+ SetArtifactStateInput,
36
+ } from "@sixb/core/storage"
37
+ import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
38
+
39
+ const ARTIFACT_COLUMNS = `
40
+ project_id AS projectId, id, kind, title,
41
+ owner_principal_type AS ownerPrincipalType, owner_principal_id AS ownerPrincipalId,
42
+ latest_version AS latestVersion, created_at AS createdAt, updated_at AS updatedAt
43
+ `
44
+
45
+ const VERSION_COLUMNS = `
46
+ project_id AS projectId, artifact_id AS artifactId, version, title, entry, files, outline, refs,
47
+ author_principal_type AS authorPrincipalType, author_principal_id AS authorPrincipalId,
48
+ origin_thread_id AS originThreadId, origin_run_id AS originRunId, created_at AS createdAt
49
+ `
50
+
51
+ const FRAME_GRANT_COLUMNS = `
52
+ project_id AS projectId, token_hash AS tokenHash, artifact_id AS artifactId, version, targets,
53
+ created_at AS createdAt, expires_at AS expiresAt
54
+ `
55
+
56
+ const STATE_COLUMNS = "key, value, size_bytes AS sizeBytes, updated_at AS updatedAt"
57
+
58
+ /** One reader of one artifact: the bindings that follow `STATE_SCOPE`. */
59
+ const STATE_SCOPE = "project_id = ? AND artifact_id = ? AND principal_type = ? AND principal_id = ?"
60
+
61
+ function stateScope(input: ArtifactStateScope): SQLQueryBindings[] {
62
+ return [input.projectId, input.artifactId, input.principal.type, input.principal.id]
63
+ }
64
+
65
+ type Row = Record<string, unknown>
66
+
67
+ export class SqliteArtifactStorage implements ArtifactStorage {
68
+ private readonly db: Database
69
+
70
+ constructor(connection: SqliteStoreConnection) {
71
+ this.db = connection.db
72
+ }
73
+
74
+ async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
75
+ return runImmediateTransaction(this.db, () => {
76
+ const createdAt = input.version.createdAt.toISOString()
77
+ const artifact = this.db
78
+ .query(
79
+ `INSERT INTO artifacts (
80
+ project_id, id, kind, title, owner_principal_type, owner_principal_id,
81
+ latest_version, next_version, created_at, updated_at
82
+ ) VALUES (?, ?, ?, ?, ?, ?, 1, 2, ?, ?)
83
+ ON CONFLICT (project_id, id) DO NOTHING
84
+ RETURNING ${ARTIFACT_COLUMNS}`
85
+ )
86
+ .get(
87
+ input.projectId,
88
+ input.id,
89
+ input.kind,
90
+ input.version.title,
91
+ input.owner.type,
92
+ input.owner.id,
93
+ createdAt,
94
+ createdAt
95
+ ) as Row | null
96
+ if (!artifact) throw artifactStorageErrors.exists(input.id)
97
+ return {
98
+ artifact: parseArtifactRow(artifact),
99
+ version: this.insertVersion(input.projectId, input.id, 1, input.version),
100
+ }
101
+ })
102
+ }
103
+
104
+ async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
105
+ return runImmediateTransaction(this.db, () => {
106
+ const current = this.db
107
+ .query(
108
+ `SELECT kind, next_version AS nextVersion, latest_version AS latestVersion
109
+ FROM artifacts WHERE project_id = ? AND id = ?`
110
+ )
111
+ .get(input.projectId, input.artifactId) as {
112
+ readonly kind: ArtifactRecord["kind"]
113
+ readonly nextVersion: number
114
+ readonly latestVersion: number
115
+ } | null
116
+ if (!current) throw artifactStorageErrors.notFound(input.artifactId)
117
+ if (current.kind !== input.kind) {
118
+ throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
119
+ }
120
+ // Inside the write transaction: a publish from the same base after this one finds it moved.
121
+ if (input.baseVersion !== undefined && current.latestVersion !== input.baseVersion) {
122
+ const latest = parseArtifactVersionRow(
123
+ this.db
124
+ .query(
125
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
126
+ WHERE project_id = ? AND artifact_id = ? AND version = ?`
127
+ )
128
+ .get(input.projectId, input.artifactId, current.latestVersion) as Row
129
+ )
130
+ throw artifactStorageErrors.moved(input.artifactId, input.baseVersion, {
131
+ version: latest.version,
132
+ author: latest.author,
133
+ })
134
+ }
135
+ const version = this.insertVersion(
136
+ input.projectId,
137
+ input.artifactId,
138
+ current.nextVersion,
139
+ input.version
140
+ )
141
+ const artifact = this.db
142
+ .query(
143
+ `UPDATE artifacts
144
+ SET title = ?, latest_version = ?, next_version = ?, updated_at = ?
145
+ WHERE project_id = ? AND id = ?
146
+ RETURNING ${ARTIFACT_COLUMNS}`
147
+ )
148
+ .get(
149
+ input.version.title,
150
+ version.version,
151
+ version.version + 1,
152
+ input.version.createdAt.toISOString(),
153
+ input.projectId,
154
+ input.artifactId
155
+ ) as Row
156
+ return { artifact: parseArtifactRow(artifact), version }
157
+ })
158
+ }
159
+
160
+ async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
161
+ const row = this.db
162
+ .query(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE project_id = ? AND id = ?`)
163
+ .get(input.projectId, input.id) as Row | null
164
+ return row ? parseArtifactRow(row) : null
165
+ }
166
+
167
+ async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
168
+ const where = ["a.project_id = ?"]
169
+ const params: SQLQueryBindings[] = [input.projectId]
170
+ if (input.owner) {
171
+ where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
172
+ params.push(input.owner.type, input.owner.id)
173
+ }
174
+ if (input.threadId !== undefined) {
175
+ where.push(
176
+ `EXISTS (
177
+ SELECT 1 FROM artifact_threads t
178
+ WHERE t.project_id = a.project_id AND t.artifact_id = a.id AND t.thread_id = ?
179
+ )`
180
+ )
181
+ params.push(input.threadId)
182
+ }
183
+ if (input.kind !== undefined) {
184
+ where.push("a.kind = ?")
185
+ params.push(input.kind)
186
+ }
187
+ const clause = where.join(" AND ")
188
+ const { total } = this.db
189
+ .query(`SELECT COUNT(*) AS total FROM artifacts a WHERE ${clause}`)
190
+ .get(...params) as { readonly total: number }
191
+ const offset = input.offset ?? 0
192
+ const rows = this.db
193
+ .query(
194
+ `SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
195
+ ORDER BY a.updated_at DESC, a.id ASC LIMIT ? OFFSET ?`
196
+ )
197
+ .all(...params, input.limit ?? -1, offset) as Row[]
198
+ return {
199
+ artifacts: rows.map(parseArtifactRow),
200
+ hasMore: offset + rows.length < total,
201
+ total,
202
+ }
203
+ }
204
+
205
+ async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
206
+ const row = this.db
207
+ .query(
208
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
209
+ WHERE project_id = ? AND artifact_id = ? AND version = ?`
210
+ )
211
+ .get(input.projectId, input.artifactId, input.version) as Row | null
212
+ return row ? parseArtifactVersionRow(row) : null
213
+ }
214
+
215
+ async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
216
+ const { total } = this.db
217
+ .query(
218
+ "SELECT COUNT(*) AS total FROM artifact_versions WHERE project_id = ? AND artifact_id = ?"
219
+ )
220
+ .get(input.projectId, input.artifactId) as { readonly total: number }
221
+ const offset = input.offset ?? 0
222
+ const rows = this.db
223
+ .query(
224
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
225
+ WHERE project_id = ? AND artifact_id = ?
226
+ ORDER BY version DESC LIMIT ? OFFSET ?`
227
+ )
228
+ .all(input.projectId, input.artifactId, input.limit ?? -1, offset) as Row[]
229
+ return {
230
+ versions: rows.map(parseArtifactVersionRow),
231
+ hasMore: offset + rows.length < total,
232
+ total,
233
+ }
234
+ }
235
+
236
+ async listReferencingVersions(
237
+ input: ListReferencingVersionsInput
238
+ ): Promise<ArtifactVersionRecord[]> {
239
+ // An address names the artifact when the id is followed by nothing, a version, a file, or a
240
+ // part.
241
+ const prefix = `artifact:${input.artifactId}`
242
+ const rows = this.db
243
+ .query(
244
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions v
245
+ WHERE (project_id, artifact_id, version) IN (
246
+ SELECT project_id, id, latest_version FROM artifacts WHERE project_id = ?
247
+ ) AND EXISTS (
248
+ SELECT 1 FROM json_each(v.refs) r
249
+ WHERE substr(json_extract(r.value, '$.address'), 1, ?) = ?
250
+ AND substr(json_extract(r.value, '$.address'), ? + 1, 1) IN ('', '@', '/', '#')
251
+ )
252
+ ORDER BY artifact_id`
253
+ )
254
+ .all(input.projectId, prefix.length, prefix, prefix.length) as Row[]
255
+ return rows.map(parseArtifactVersionRow)
256
+ }
257
+
258
+ async listThreadIds(input: GetArtifactInput): Promise<string[]> {
259
+ const rows = this.db
260
+ .query(
261
+ `SELECT thread_id AS threadId FROM artifact_threads
262
+ WHERE project_id = ? AND artifact_id = ? ORDER BY thread_id`
263
+ )
264
+ .all(input.projectId, input.id) as { readonly threadId: string }[]
265
+ return rows.map((row) => row.threadId)
266
+ }
267
+
268
+ async copy(input: CopyArtifactInput): Promise<ArtifactRecord> {
269
+ return runImmediateTransaction(this.db, () => {
270
+ const source = this.db
271
+ .query("SELECT id FROM artifacts WHERE project_id = ? AND id = ?")
272
+ .get(input.projectId, input.sourceId)
273
+ if (!source) throw artifactStorageErrors.notFound(input.sourceId)
274
+
275
+ const artifact = this.db
276
+ .query(
277
+ `INSERT INTO artifacts (
278
+ project_id, id, kind, title, owner_principal_type, owner_principal_id,
279
+ latest_version, next_version, created_at, updated_at
280
+ )
281
+ SELECT project_id, ?, kind, title, ?, ?, latest_version, next_version, created_at,
282
+ updated_at
283
+ FROM artifacts WHERE project_id = ? AND id = ?
284
+ ON CONFLICT (project_id, id) DO NOTHING
285
+ RETURNING ${ARTIFACT_COLUMNS}`
286
+ )
287
+ .get(
288
+ input.id,
289
+ input.owner.type,
290
+ input.owner.id,
291
+ input.projectId,
292
+ input.sourceId
293
+ ) as Row | null
294
+ if (!artifact) throw artifactStorageErrors.exists(input.id)
295
+
296
+ this.db
297
+ .query(
298
+ `INSERT INTO artifact_versions (
299
+ project_id, artifact_id, version, title, entry, files, outline, refs,
300
+ author_principal_type, author_principal_id, created_at
301
+ )
302
+ SELECT project_id, ?, version, title, entry, files, outline, refs,
303
+ author_principal_type, author_principal_id, created_at
304
+ FROM artifact_versions WHERE project_id = ? AND artifact_id = ?`
305
+ )
306
+ .run(input.id, input.projectId, input.sourceId)
307
+ this.db
308
+ .query(
309
+ `INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at, copied)
310
+ VALUES (?, ?, ?, ?, 1)`
311
+ )
312
+ .run(
313
+ input.projectId,
314
+ input.id,
315
+ input.threadId,
316
+ (input.copiedAt ?? new Date()).toISOString()
317
+ )
318
+ return parseArtifactRow(artifact)
319
+ })
320
+ }
321
+
322
+ async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
323
+ runImmediateTransaction(this.db, () => {
324
+ const exists = this.db
325
+ .query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
326
+ .get(record.projectId, record.artifactId)
327
+ if (!exists) throw artifactStorageErrors.notFound(record.artifactId)
328
+ this.db
329
+ .query(
330
+ `INSERT INTO artifact_frame_grants
331
+ (project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
332
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
333
+ )
334
+ .run(
335
+ record.projectId,
336
+ record.tokenHash,
337
+ record.artifactId,
338
+ record.version,
339
+ JSON.stringify(record.targets),
340
+ record.createdAt.toISOString(),
341
+ record.expiresAt.toISOString()
342
+ )
343
+ })
344
+ }
345
+
346
+ async findFrameGrant(
347
+ input: FindArtifactFrameGrantInput
348
+ ): Promise<ArtifactFrameGrantRecord | null> {
349
+ const row = this.db
350
+ .query(
351
+ `SELECT ${FRAME_GRANT_COLUMNS} FROM artifact_frame_grants
352
+ WHERE project_id = ? AND token_hash = ?`
353
+ )
354
+ .get(input.projectId, input.tokenHash) as Row | null
355
+ return row ? parseArtifactFrameGrantRow(row) : null
356
+ }
357
+
358
+ async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
359
+ return this.db
360
+ .query(
361
+ `DELETE FROM artifact_frame_grants WHERE rowid IN (
362
+ SELECT rowid FROM artifact_frame_grants
363
+ WHERE project_id = ? AND expires_at < ?
364
+ ORDER BY expires_at LIMIT ?
365
+ )`
366
+ )
367
+ .run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
368
+ }
369
+
370
+ async getState(input: ArtifactStateKeyInput): Promise<ArtifactStateEntry | null> {
371
+ const row = this.db
372
+ .query(`SELECT ${STATE_COLUMNS} FROM artifact_state WHERE ${STATE_SCOPE} AND key = ?`)
373
+ .get(...stateScope(input), input.key) as Row | null
374
+ return row ? parseArtifactStateRow(row) : null
375
+ }
376
+
377
+ async setState(input: SetArtifactStateInput): Promise<void> {
378
+ runImmediateTransaction(this.db, () => {
379
+ const artifact = this.db
380
+ .query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
381
+ .get(input.projectId, input.artifactId)
382
+ if (!artifact) throw artifactStorageErrors.notFound(input.artifactId)
383
+ // In the write transaction: two writes at once cannot both fit under the limits.
384
+ const others = this.db
385
+ .query(
386
+ `SELECT count(*) AS keys, coalesce(sum(size_bytes), 0) AS bytes FROM artifact_state
387
+ WHERE ${STATE_SCOPE} AND key <> ?`
388
+ )
389
+ .get(...stateScope(input), input.key) as { readonly keys: number; readonly bytes: number }
390
+ const sizeBytes = artifactStateBytes(input.value)
391
+ checkArtifactStateWrite({ key: input.key, valueBytes: sizeBytes, others })
392
+ this.db
393
+ .query(
394
+ `INSERT INTO artifact_state (
395
+ project_id, artifact_id, principal_type, principal_id, key, value, size_bytes,
396
+ updated_at
397
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
398
+ ON CONFLICT (project_id, artifact_id, principal_type, principal_id, key) DO UPDATE SET
399
+ value = excluded.value, size_bytes = excluded.size_bytes,
400
+ updated_at = excluded.updated_at`
401
+ )
402
+ .run(...stateScope(input), input.key, input.value, sizeBytes, input.updatedAt.toISOString())
403
+ })
404
+ }
405
+
406
+ async deleteState(input: ArtifactStateKeyInput): Promise<boolean> {
407
+ return (
408
+ this.db
409
+ .query(`DELETE FROM artifact_state WHERE ${STATE_SCOPE} AND key = ?`)
410
+ .run(...stateScope(input), input.key).changes > 0
411
+ )
412
+ }
413
+
414
+ async listState(input: ArtifactStateScope): Promise<ArtifactStateEntry[]> {
415
+ const rows = this.db
416
+ .query(`SELECT ${STATE_COLUMNS} FROM artifact_state WHERE ${STATE_SCOPE} ORDER BY key`)
417
+ .all(...stateScope(input)) as Row[]
418
+ return rows.map(parseArtifactStateRow)
419
+ }
420
+
421
+ async clearState(input: ArtifactStateScope): Promise<number> {
422
+ return this.db
423
+ .query(`DELETE FROM artifact_state WHERE ${STATE_SCOPE}`)
424
+ .run(...stateScope(input)).changes
425
+ }
426
+
427
+ async delete(input: DeleteArtifactInput): Promise<boolean> {
428
+ // Versions, frame grants, and state go with the artifact through ON DELETE CASCADE.
429
+ return (
430
+ this.db
431
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
432
+ .run(input.projectId, input.id).changes > 0
433
+ )
434
+ }
435
+
436
+ async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
437
+ return runImmediateTransaction(this.db, () => {
438
+ const affected = this.db
439
+ .query(
440
+ `SELECT DISTINCT artifact_id AS artifactId FROM artifact_versions
441
+ WHERE project_id = ? AND origin_run_id = ?`
442
+ )
443
+ .all(input.projectId, input.runId) as { readonly artifactId: string }[]
444
+ const deleted = this.db
445
+ .query(
446
+ `DELETE FROM artifact_versions WHERE project_id = ? AND origin_run_id = ?
447
+ RETURNING origin_thread_id AS threadId`
448
+ )
449
+ .all(input.projectId, input.runId) as { readonly threadId: string }[]
450
+ // A run publishes from one thread: its artifacts stay shared there only while a version
451
+ // from it remains, or when they were copied for it.
452
+ const threadId = deleted[0]?.threadId
453
+ for (const { artifactId } of affected) {
454
+ this.db
455
+ .query(
456
+ `DELETE FROM artifact_threads
457
+ WHERE project_id = ? AND artifact_id = ? AND thread_id = ? AND copied = 0
458
+ AND NOT EXISTS (
459
+ SELECT 1 FROM artifact_versions
460
+ WHERE project_id = ? AND artifact_id = ? AND origin_thread_id = ?
461
+ )`
462
+ )
463
+ .run(
464
+ input.projectId,
465
+ artifactId,
466
+ threadId ?? null,
467
+ input.projectId,
468
+ artifactId,
469
+ threadId ?? null
470
+ )
471
+ this.repointLatest(input.projectId, artifactId)
472
+ }
473
+ return deleted.length
474
+ })
475
+ }
476
+
477
+ /** Point an artifact at its newest remaining version, or delete it when none remains. */
478
+ private repointLatest(projectId: string, artifactId: string): void {
479
+ const latest = this.db
480
+ .query(
481
+ `SELECT version, title, created_at AS createdAt FROM artifact_versions
482
+ WHERE project_id = ? AND artifact_id = ? ORDER BY version DESC LIMIT 1`
483
+ )
484
+ .get(projectId, artifactId) as {
485
+ readonly version: number
486
+ readonly title: string
487
+ readonly createdAt: string
488
+ } | null
489
+ if (!latest) {
490
+ this.db
491
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
492
+ .run(projectId, artifactId)
493
+ return
494
+ }
495
+ this.db
496
+ .query(
497
+ `UPDATE artifacts SET title = ?, latest_version = ?, updated_at = ?
498
+ WHERE project_id = ? AND id = ?`
499
+ )
500
+ .run(latest.title, latest.version, latest.createdAt, projectId, artifactId)
501
+ }
502
+
503
+ private insertVersion(
504
+ projectId: string,
505
+ artifactId: string,
506
+ version: number,
507
+ input: ArtifactVersionInput
508
+ ): ArtifactVersionRecord {
509
+ const row = this.db
510
+ .query(
511
+ `INSERT INTO artifact_versions (
512
+ project_id, artifact_id, version, title, entry, files, outline, refs,
513
+ author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
514
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
515
+ RETURNING ${VERSION_COLUMNS}`
516
+ )
517
+ .get(
518
+ projectId,
519
+ artifactId,
520
+ version,
521
+ input.title,
522
+ input.entry,
523
+ JSON.stringify(input.files),
524
+ JSON.stringify(input.outline),
525
+ JSON.stringify(input.references),
526
+ input.author.type,
527
+ input.author.id,
528
+ input.origin?.threadId ?? null,
529
+ input.origin?.runId ?? null,
530
+ input.createdAt.toISOString()
531
+ ) as Row
532
+ if (input.origin) {
533
+ this.linkThread(projectId, artifactId, input.origin.threadId, input.createdAt)
534
+ }
535
+ return parseArtifactVersionRow(row)
536
+ }
537
+
538
+ /** Shares an artifact in a thread, keeping when it was first shared there. */
539
+ private linkThread(
540
+ projectId: string,
541
+ artifactId: string,
542
+ threadId: string,
543
+ sharedAt: Date
544
+ ): void {
545
+ this.db
546
+ .query(
547
+ `INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at)
548
+ VALUES (?, ?, ?, ?)
549
+ ON CONFLICT (project_id, artifact_id, thread_id) DO NOTHING`
550
+ )
551
+ .run(projectId, artifactId, threadId, sharedAt.toISOString())
552
+ }
553
+ }