@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
@@ -0,0 +1,456 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { Principal } from "@sixb/core"
3
+ import {
4
+ agentApprovalCallKey,
5
+ assertAgentApprovalCaller,
6
+ assertRequestAgentApprovalInput,
7
+ newAgentApprovalRecord,
8
+ } from "@sixb/core/internal/agent-run-storage-provider"
9
+ import {
10
+ type AgentApprovalCall,
11
+ type AgentApprovalCursor,
12
+ type AgentApprovalMode,
13
+ type AgentApprovalOutcome,
14
+ type AgentApprovalRecord,
15
+ type AgentApprovalStore,
16
+ AgentStorageError,
17
+ type DecideAgentApprovalInput,
18
+ type DecideAgentApprovalResult,
19
+ type ListAgentApprovalsInput,
20
+ type ListAgentApprovalsResult,
21
+ type MarkAgentApprovalsReportedInput,
22
+ type RequestAgentApprovalInput,
23
+ type RequestAgentApprovalResult,
24
+ type SettleAgentApprovalsInput,
25
+ type StopAgentApprovalsInput,
26
+ type StopAgentApprovalsResult,
27
+ } from "@sixb/core/storage"
28
+ import { runImmediateTransaction } from "../transactions"
29
+ import { type AgentRunRow, rowToRunRecord } from "./rows"
30
+
31
+ interface AgentApprovalRow {
32
+ readonly project_id: string
33
+ readonly id: string
34
+ readonly thread_id: string
35
+ readonly run_id: string
36
+ readonly caller_run_id: string
37
+ readonly requested_by_type: Principal["type"]
38
+ readonly requested_by_id: string
39
+ readonly mode: AgentApprovalRecord["mode"]
40
+ readonly call_key: string
41
+ readonly call: string
42
+ readonly status: AgentApprovalRecord["status"]
43
+ readonly created_at: string
44
+ readonly expires_at: string
45
+ readonly decided_at: string | null
46
+ readonly decided_by_type: Principal["type"] | null
47
+ readonly decided_by_id: string | null
48
+ readonly outcome: string | null
49
+ readonly reported_in_message_id: string | null
50
+ readonly raised_mode: AgentApprovalMode | null
51
+ readonly turn_stopped_at: string | null
52
+ readonly execution_claimed_at: string | null
53
+ }
54
+
55
+ const LIVE_CALL = `
56
+ SELECT * FROM agent_approvals
57
+ WHERE project_id = ? AND thread_id = ? AND call_key = ?
58
+ AND status IN ('pending', 'approved', 'denied')
59
+ `
60
+
61
+ export class SqliteAgentApprovalStore implements AgentApprovalStore {
62
+ constructor(private readonly db: Database) {}
63
+
64
+ async request(input: RequestAgentApprovalInput): Promise<RequestAgentApprovalResult> {
65
+ assertRequestAgentApprovalInput(input, "SixbSqlite")
66
+
67
+ return runImmediateTransaction(this.db, () => {
68
+ const threadId = assertAgentApprovalCaller({
69
+ request: input,
70
+ turn: this.run(input.projectId, input.runId),
71
+ caller: this.run(input.projectId, input.callerRunId),
72
+ prefix: "SixbSqlite",
73
+ })
74
+ const callKey = agentApprovalCallKey(input.call)
75
+ const live = this.db
76
+ .query(LIVE_CALL)
77
+ .get(input.projectId, threadId, callKey) as AgentApprovalRow | null
78
+ if (live) return { approval: rowToApproval(live), created: false }
79
+
80
+ if (this.row(input.projectId, input.id)) {
81
+ throw new AgentStorageError(
82
+ "duplicate_id",
83
+ `[SixbSqlite] Agent approval '${input.id}' already exists for project '${input.projectId}'.`
84
+ )
85
+ }
86
+ const approval = newAgentApprovalRecord(input, threadId)
87
+ this.db
88
+ .query(
89
+ `INSERT INTO agent_approvals (
90
+ project_id, id, thread_id, run_id, caller_run_id, requested_by_type, requested_by_id,
91
+ mode, call_key, call, status, created_at, expires_at
92
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`
93
+ )
94
+ .run(
95
+ approval.projectId,
96
+ approval.id,
97
+ approval.threadId,
98
+ approval.runId,
99
+ approval.callerRunId,
100
+ approval.requestedBy.type,
101
+ approval.requestedBy.id,
102
+ approval.mode,
103
+ approval.callKey,
104
+ JSON.stringify(approval.call),
105
+ approval.createdAt.toISOString(),
106
+ approval.expiresAt.toISOString()
107
+ )
108
+ return { approval, created: true }
109
+ })
110
+ }
111
+
112
+ async decide(input: DecideAgentApprovalInput): Promise<DecideAgentApprovalResult | null> {
113
+ const decidedAt = (input.decidedAt ?? new Date()).toISOString()
114
+
115
+ return runImmediateTransaction(this.db, () => {
116
+ const current = this.row(input.projectId, input.id)
117
+ if (!current) return null
118
+ if (current.status !== "pending") return { approval: rowToApproval(current), decided: false }
119
+
120
+ if (current.expires_at <= decidedAt) {
121
+ this.db
122
+ .query("UPDATE agent_approvals SET status = 'expired' WHERE project_id = ? AND id = ?")
123
+ .run(input.projectId, input.id)
124
+ } else {
125
+ this.db
126
+ .query(
127
+ `UPDATE agent_approvals
128
+ SET status = ?, decided_at = ?, decided_by_type = ?, decided_by_id = ?, raised_mode = ?
129
+ WHERE project_id = ? AND id = ?`
130
+ )
131
+ .run(
132
+ input.decision,
133
+ decidedAt,
134
+ input.decidedBy.type,
135
+ input.decidedBy.id,
136
+ input.decision === "approved" ? (input.raisedMode ?? null) : null,
137
+ input.projectId,
138
+ input.id
139
+ )
140
+ }
141
+ const approval = this.require(input.projectId, input.id)
142
+ return { approval, decided: approval.status === input.decision }
143
+ })
144
+ }
145
+
146
+ async recordOutcome(input: {
147
+ readonly projectId: string
148
+ readonly id: string
149
+ readonly outcome: AgentApprovalOutcome
150
+ }): Promise<AgentApprovalRecord> {
151
+ return runImmediateTransaction(this.db, () => {
152
+ const current = this.row(input.projectId, input.id)
153
+ if (current?.status !== "approved") {
154
+ throw new AgentStorageError(
155
+ "invalid_state",
156
+ current
157
+ ? `[SixbSqlite] Agent approval '${input.id}' is not approved (status '${current.status}').`
158
+ : `[SixbSqlite] Agent approval '${input.id}' not found for project '${input.projectId}'.`
159
+ )
160
+ }
161
+ if (current.outcome !== null) return rowToApproval(current)
162
+
163
+ this.db
164
+ .query("UPDATE agent_approvals SET outcome = ? WHERE project_id = ? AND id = ?")
165
+ .run(JSON.stringify(input.outcome), input.projectId, input.id)
166
+ return this.require(input.projectId, input.id)
167
+ })
168
+ }
169
+
170
+ async settle(input: SettleAgentApprovalsInput): Promise<readonly AgentApprovalRecord[]> {
171
+ const rows = this.db
172
+ .query(
173
+ `UPDATE agent_approvals SET status = ?
174
+ WHERE project_id = ? AND thread_id = ? AND status = 'pending'
175
+ RETURNING *`
176
+ )
177
+ .all(input.status, input.projectId, input.threadId) as AgentApprovalRow[]
178
+ return rows.map(rowToApproval).sort(compareApprovals)
179
+ }
180
+
181
+ async stop(input: StopAgentApprovalsInput): Promise<StopAgentApprovalsResult> {
182
+ const stoppedAt = (input.stoppedAt ?? new Date()).toISOString()
183
+ const clauses = [
184
+ "project_id = ?",
185
+ "thread_id = ?",
186
+ "reported_in_message_id IS NULL",
187
+ "(status = 'pending' OR turn_stopped_at IS NULL)",
188
+ ]
189
+ const params: string[] = [input.projectId, input.threadId]
190
+ if (input.runId !== undefined) {
191
+ clauses.push("run_id = ?")
192
+ params.push(input.runId)
193
+ }
194
+ if (input.requestedBy !== undefined) {
195
+ clauses.push("requested_by_type = ? AND requested_by_id = ?")
196
+ params.push(input.requestedBy.type, input.requestedBy.id)
197
+ }
198
+
199
+ return runImmediateTransaction(this.db, () => {
200
+ const matched = this.db
201
+ .query(`SELECT * FROM agent_approvals WHERE ${clauses.join(" AND ")}`)
202
+ .all(...params) as AgentApprovalRow[]
203
+ const update = this.db.query(
204
+ `UPDATE agent_approvals
205
+ SET status = CASE WHEN status = 'pending' THEN 'cancelled' ELSE status END,
206
+ turn_stopped_at = COALESCE(turn_stopped_at, ?)
207
+ WHERE project_id = ? AND id = ?`
208
+ )
209
+ for (const row of matched) update.run(stoppedAt, input.projectId, row.id)
210
+
211
+ const cancelled = matched
212
+ .filter((row) => row.status === "pending")
213
+ .map((row) => this.require(input.projectId, row.id))
214
+ const stopped = matched.filter((row) => row.turn_stopped_at === null).length
215
+ return { cancelled: cancelled.sort(compareApprovals), stopped }
216
+ })
217
+ }
218
+
219
+ async holdsRun(input: { readonly projectId: string; readonly runId: string }): Promise<boolean> {
220
+ return sqliteHoldsRun(this.db, input.projectId, input.runId)
221
+ }
222
+
223
+ async claimExecution(input: {
224
+ readonly projectId: string
225
+ readonly id: string
226
+ readonly claimedAt: Date
227
+ readonly staleBefore: Date
228
+ }): Promise<AgentApprovalRecord | null> {
229
+ const row = this.db
230
+ .query(
231
+ `UPDATE agent_approvals SET execution_claimed_at = ?
232
+ WHERE project_id = ? AND id = ? AND status = 'approved' AND outcome IS NULL
233
+ AND COALESCE(execution_claimed_at, decided_at) <= ?
234
+ RETURNING *`
235
+ )
236
+ .get(
237
+ input.claimedAt.toISOString(),
238
+ input.projectId,
239
+ input.id,
240
+ input.staleBefore.toISOString()
241
+ ) as AgentApprovalRow | null
242
+ return row ? rowToApproval(row) : null
243
+ }
244
+
245
+ async listRepairable(input: {
246
+ readonly projectId: string
247
+ readonly claimableBefore: Date
248
+ readonly after?: AgentApprovalCursor
249
+ readonly limit: number
250
+ }): Promise<readonly AgentApprovalRecord[]> {
251
+ const afterAt = input.after?.createdAt.toISOString() ?? null
252
+ const afterId = input.after?.id ?? ""
253
+ // Each half of the OR matches one partial index.
254
+ const rows = this.db
255
+ .query(
256
+ `SELECT * FROM agent_approvals
257
+ WHERE project_id = ?
258
+ AND (
259
+ (status = 'approved' AND outcome IS NULL
260
+ AND COALESCE(execution_claimed_at, decided_at) <= ?)
261
+ OR (status IN ('approved', 'denied') AND reported_in_message_id IS NULL
262
+ AND turn_stopped_at IS NULL)
263
+ )
264
+ AND (? IS NULL OR created_at > ? OR (created_at = ? AND id > ?))
265
+ ORDER BY created_at, id
266
+ LIMIT ?`
267
+ )
268
+ .all(
269
+ input.projectId,
270
+ input.claimableBefore.toISOString(),
271
+ afterAt,
272
+ afterAt,
273
+ afterAt,
274
+ afterId,
275
+ input.limit
276
+ ) as AgentApprovalRow[]
277
+ return rows.map(rowToApproval)
278
+ }
279
+
280
+ async expireDue(input: {
281
+ readonly projectId: string
282
+ readonly now: Date
283
+ readonly limit?: number
284
+ }): Promise<readonly AgentApprovalRecord[]> {
285
+ const rows = this.db
286
+ .query(
287
+ `UPDATE agent_approvals SET status = 'expired'
288
+ WHERE project_id = ? AND id IN (
289
+ SELECT id FROM agent_approvals
290
+ WHERE project_id = ? AND status = 'pending' AND expires_at <= ?
291
+ ORDER BY created_at, id
292
+ LIMIT ?
293
+ )
294
+ RETURNING *`
295
+ )
296
+ .all(
297
+ input.projectId,
298
+ input.projectId,
299
+ input.now.toISOString(),
300
+ input.limit ?? -1
301
+ ) as AgentApprovalRow[]
302
+ return rows.map(rowToApproval).sort(compareApprovals)
303
+ }
304
+
305
+ async markReported(input: MarkAgentApprovalsReportedInput): Promise<readonly string[]> {
306
+ const ids = [...new Set(input.ids)]
307
+ if (ids.length === 0) return []
308
+
309
+ const rows = this.db
310
+ .query(
311
+ `UPDATE agent_approvals SET reported_in_message_id = ?
312
+ WHERE project_id = ? AND thread_id = ? AND reported_in_message_id IS NULL
313
+ AND (? = 0 OR turn_stopped_at IS NULL)
314
+ AND id IN (${ids.map(() => "?").join(", ")})
315
+ RETURNING id`
316
+ )
317
+ .all(
318
+ input.messageId,
319
+ input.projectId,
320
+ input.threadId,
321
+ input.resuming === true ? 1 : 0,
322
+ ...ids
323
+ ) as { id: string }[]
324
+ const marked = new Set(rows.map((row) => row.id))
325
+ return ids.filter((id) => marked.has(id))
326
+ }
327
+
328
+ async getById(params: {
329
+ readonly projectId: string
330
+ readonly id: string
331
+ }): Promise<AgentApprovalRecord | null> {
332
+ const row = this.row(params.projectId, params.id)
333
+ return row ? rowToApproval(row) : null
334
+ }
335
+
336
+ async list(input: ListAgentApprovalsInput): Promise<ListAgentApprovalsResult> {
337
+ if (input.statuses?.length === 0) return { approvals: [], hasMore: false, total: 0 }
338
+
339
+ const { where, params } = listFilter(input)
340
+ const offset = input.offset ?? 0
341
+ return this.db.transaction(() => {
342
+ const counted = this.db
343
+ .query(`SELECT COUNT(*) AS total FROM agent_approvals WHERE ${where}`)
344
+ .get(...params) as { total: number }
345
+ const rows = this.db
346
+ .query(
347
+ `SELECT * FROM agent_approvals WHERE ${where}
348
+ ORDER BY created_at, id LIMIT ? OFFSET ?`
349
+ )
350
+ .all(...params, input.limit ?? -1, offset) as AgentApprovalRow[]
351
+ return {
352
+ approvals: rows.map(rowToApproval),
353
+ hasMore: offset + rows.length < counted.total,
354
+ total: counted.total,
355
+ }
356
+ })()
357
+ }
358
+
359
+ private run(projectId: string, id: string) {
360
+ const row = this.db
361
+ .query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?")
362
+ .get(projectId, id) as AgentRunRow | null
363
+ return row ? rowToRunRecord(row) : null
364
+ }
365
+
366
+ private row(projectId: string, id: string): AgentApprovalRow | null {
367
+ return this.db
368
+ .query("SELECT * FROM agent_approvals WHERE project_id = ? AND id = ?")
369
+ .get(projectId, id) as AgentApprovalRow | null
370
+ }
371
+
372
+ private require(projectId: string, id: string): AgentApprovalRecord {
373
+ const row = this.row(projectId, id)
374
+ if (!row) throw new Error(`[SixbSqlite] Agent approval '${id}' disappeared.`)
375
+ return rowToApproval(row)
376
+ }
377
+ }
378
+
379
+ /** The `list` predicate, as the in-memory store's `agentApprovalMatches` reads it. */
380
+ function listFilter(input: ListAgentApprovalsInput): {
381
+ readonly where: string
382
+ readonly params: readonly (string | number)[]
383
+ } {
384
+ const clauses = ["project_id = ?"]
385
+ const params: (string | number)[] = [input.projectId]
386
+ if (input.threadId !== undefined) {
387
+ clauses.push("thread_id = ?")
388
+ params.push(input.threadId)
389
+ }
390
+ if (input.runId !== undefined) {
391
+ clauses.push("run_id = ?")
392
+ params.push(input.runId)
393
+ }
394
+ if (input.requestedBy !== undefined) {
395
+ clauses.push("requested_by_type = ? AND requested_by_id = ?")
396
+ params.push(input.requestedBy.type, input.requestedBy.id)
397
+ }
398
+ if (input.statuses !== undefined) {
399
+ clauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
400
+ params.push(...input.statuses)
401
+ }
402
+ if (input.reported !== undefined) {
403
+ clauses.push(`reported_in_message_id IS ${input.reported ? "NOT NULL" : "NULL"}`)
404
+ }
405
+ return { where: clauses.join(" AND "), params }
406
+ }
407
+
408
+ function rowToApproval(row: AgentApprovalRow): AgentApprovalRecord {
409
+ return {
410
+ id: row.id,
411
+ projectId: row.project_id,
412
+ threadId: row.thread_id,
413
+ runId: row.run_id,
414
+ callerRunId: row.caller_run_id,
415
+ requestedBy: { type: row.requested_by_type, id: row.requested_by_id },
416
+ mode: row.mode,
417
+ callKey: row.call_key,
418
+ call: JSON.parse(row.call) as AgentApprovalCall,
419
+ status: row.status,
420
+ createdAt: new Date(row.created_at),
421
+ expiresAt: new Date(row.expires_at),
422
+ ...(row.decided_at === null ? {} : { decidedAt: new Date(row.decided_at) }),
423
+ ...(row.decided_by_type === null || row.decided_by_id === null
424
+ ? {}
425
+ : { decidedBy: { type: row.decided_by_type, id: row.decided_by_id } }),
426
+ ...(row.outcome === null ? {} : { outcome: JSON.parse(row.outcome) as AgentApprovalOutcome }),
427
+ ...(row.reported_in_message_id === null
428
+ ? {}
429
+ : { reportedInMessageId: row.reported_in_message_id }),
430
+ ...(row.raised_mode === null ? {} : { raisedMode: row.raised_mode }),
431
+ ...(row.turn_stopped_at === null ? {} : { turnStoppedAt: new Date(row.turn_stopped_at) }),
432
+ ...(row.execution_claimed_at === null
433
+ ? {}
434
+ : { executionClaimedAt: new Date(row.execution_claimed_at) }),
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Whether a run holds a call: an approval of its, or one it made for its turn, pending or decided
440
+ * and not reported yet. `holdsRun` and the paused finish of `runs.finish` both ask this.
441
+ */
442
+ export function sqliteHoldsRun(db: Database, projectId: string, runId: string): boolean {
443
+ const held = db
444
+ .query(
445
+ `SELECT id FROM agent_approvals
446
+ WHERE project_id = ? AND (run_id = ? OR caller_run_id = ?)
447
+ AND status IN ('pending', 'approved', 'denied') AND reported_in_message_id IS NULL
448
+ LIMIT 1`
449
+ )
450
+ .get(projectId, runId, runId)
451
+ return held !== null
452
+ }
453
+
454
+ function compareApprovals(left: AgentApprovalRecord, right: AgentApprovalRecord): number {
455
+ return left.createdAt.getTime() - right.createdAt.getTime() || left.id.localeCompare(right.id)
456
+ }
@@ -54,9 +54,13 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
54
54
 
55
55
  const existingForRun = this.db
56
56
  .query(
57
- "SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id = ?"
57
+ "SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND thread_id = ? AND created_by_run_id = ?"
58
58
  )
59
- .get(input.projectId, input.createdByRunId) as AgentContextCheckpointRow | null
59
+ .get(
60
+ input.projectId,
61
+ input.threadId,
62
+ input.createdByRunId
63
+ ) as AgentContextCheckpointRow | null
60
64
  if (existingForRun) {
61
65
  const record = rowToContextCheckpointRecord(existingForRun)
62
66
  if (agentContextCheckpointMatchesCreateInput(record, input)) {
@@ -150,6 +154,7 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
150
154
 
151
155
  async getByRunIds(input: {
152
156
  readonly projectId: string
157
+ readonly threadId: string
153
158
  readonly runIds: readonly string[]
154
159
  }): Promise<readonly AgentContextCheckpointRecord[]> {
155
160
  const runIds = [...new Set(input.runIds)]
@@ -162,9 +167,9 @@ export class SqliteAgentContextCheckpointStore implements AgentContextCheckpoint
162
167
  rows.push(
163
168
  ...(this.db
164
169
  .query(
165
- `SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id IN (${placeholders})`
170
+ `SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND thread_id = ? AND created_by_run_id IN (${placeholders})`
166
171
  )
167
- .all(input.projectId, ...batch) as AgentContextCheckpointRow[])
172
+ .all(input.projectId, input.threadId, ...batch) as AgentContextCheckpointRow[])
168
173
  )
169
174
  }
170
175
  const byRunId = new Map(
@@ -1,6 +1,7 @@
1
1
  export type { SqliteAgentStorageOptions } from "./agent-storage"
2
2
  export { SqliteAgentStorage } from "./agent-storage"
3
3
  export { SqliteAgentContextCheckpointStore } from "./checkpoints"
4
+ export { SqliteAgentMemoryStore } from "./memory"
4
5
  export { SqliteAgentMessageStore } from "./messages"
5
6
  export { SqliteAgentRunStore } from "./runs"
6
7
  export { SqliteAgentThreadStore } from "./threads"
@@ -0,0 +1,195 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type { Principal } from "@sixb/core"
3
+ import {
4
+ agentMemoryConflicts,
5
+ agentMemoryHistoryCutoff,
6
+ assertSaveAgentMemoryInput,
7
+ type LatestAgentMemoryFile,
8
+ } from "@sixb/core/internal/agent-run-storage-provider"
9
+ import type {
10
+ AgentMemoryRepoRecord,
11
+ AgentMemoryRevisionRecord,
12
+ AgentMemoryStore,
13
+ ListAgentMemoryRevisionsInput,
14
+ ListAgentMemoryRevisionsResult,
15
+ SaveAgentMemoryInput,
16
+ SaveAgentMemoryResult,
17
+ } from "@sixb/core/storage"
18
+ import { runImmediateTransaction } from "../transactions"
19
+
20
+ interface RevisionRow {
21
+ readonly revision: number
22
+ readonly message: string
23
+ readonly author_principal_type: Principal["type"]
24
+ readonly author_principal_id: string
25
+ readonly source: string | null
26
+ readonly created_at: string
27
+ }
28
+
29
+ /** Each path's highest-revision version, the deleted ones included. */
30
+ const LATEST_FILES = `
31
+ SELECT f.path, f.revision, f.contents FROM agent_memory_files f
32
+ WHERE f.project_id = ? AND f.repo_id = ? AND f.revision = (
33
+ SELECT MAX(l.revision) FROM agent_memory_files l
34
+ WHERE l.project_id = f.project_id AND l.repo_id = f.repo_id AND l.path = f.path
35
+ )
36
+ `
37
+
38
+ export class SqliteAgentMemoryStore implements AgentMemoryStore {
39
+ constructor(private readonly db: Database) {}
40
+
41
+ async get(input: {
42
+ projectId: string
43
+ repoId: string
44
+ asOf?: Date
45
+ revision?: number
46
+ }): Promise<AgentMemoryRepoRecord> {
47
+ return this.db.transaction(() => {
48
+ const revision = this.head(input.projectId, input.repoId, input.asOf, input.revision)
49
+ const files = this.db
50
+ .query(
51
+ `SELECT f.path, f.contents FROM agent_memory_files f
52
+ WHERE f.project_id = ? AND f.repo_id = ? AND f.contents IS NOT NULL AND f.revision = (
53
+ SELECT MAX(l.revision) FROM agent_memory_files l
54
+ WHERE l.project_id = f.project_id AND l.repo_id = f.repo_id AND l.path = f.path
55
+ AND l.revision <= ?
56
+ )
57
+ ORDER BY f.path`
58
+ )
59
+ .all(input.projectId, input.repoId, revision) as { path: string; contents: string }[]
60
+ return { revision, files: files.map(({ path, contents }) => ({ path, contents })) }
61
+ })()
62
+ }
63
+
64
+ async save(input: SaveAgentMemoryInput): Promise<SaveAgentMemoryResult> {
65
+ assertSaveAgentMemoryInput(input, "SixbSqlite")
66
+ return runImmediateTransaction(this.db, () => {
67
+ const head = this.head(input.projectId, input.repoId)
68
+ const paths = input.changes.map((change) => change.path)
69
+ const latest = this.db
70
+ .query(`${LATEST_FILES} AND f.path IN (${paths.map(() => "?").join(", ")})`)
71
+ .all(input.projectId, input.repoId, ...paths) as (LatestAgentMemoryFile & {
72
+ path: string
73
+ })[]
74
+ const conflicts = agentMemoryConflicts(
75
+ input,
76
+ head,
77
+ new Map(latest.map((file) => [file.path, file]))
78
+ )
79
+ if (conflicts.length > 0) return { saved: false, revision: head, conflicts }
80
+
81
+ const revision = head + 1
82
+ const createdAt = input.createdAt ?? new Date()
83
+ this.db
84
+ .query(
85
+ `INSERT INTO agent_memory_revisions (
86
+ project_id, repo_id, revision, message, author_principal_type, author_principal_id,
87
+ source, created_at
88
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
89
+ )
90
+ .run(
91
+ input.projectId,
92
+ input.repoId,
93
+ revision,
94
+ input.message,
95
+ input.author.type,
96
+ input.author.id,
97
+ input.source ?? null,
98
+ createdAt.toISOString()
99
+ )
100
+ const insertFile = this.db.query(
101
+ `INSERT INTO agent_memory_files (project_id, repo_id, path, revision, contents)
102
+ VALUES (?, ?, ?, ?, ?)`
103
+ )
104
+ for (const change of input.changes) {
105
+ insertFile.run(input.projectId, input.repoId, change.path, revision, change.contents)
106
+ }
107
+ this.db
108
+ .query(
109
+ `DELETE FROM agent_memory_files AS f
110
+ WHERE f.project_id = ? AND f.repo_id = ? AND f.revision IN (
111
+ SELECT revision FROM agent_memory_revisions
112
+ WHERE project_id = ? AND repo_id = ? AND created_at < ?
113
+ ) AND EXISTS (
114
+ SELECT 1 FROM agent_memory_files n
115
+ WHERE n.project_id = f.project_id AND n.repo_id = f.repo_id AND n.path = f.path
116
+ AND n.revision > f.revision
117
+ )`
118
+ )
119
+ .run(
120
+ input.projectId,
121
+ input.repoId,
122
+ input.projectId,
123
+ input.repoId,
124
+ agentMemoryHistoryCutoff(createdAt).toISOString()
125
+ )
126
+ return { saved: true, revision }
127
+ })
128
+ }
129
+
130
+ async listRevisions(
131
+ input: ListAgentMemoryRevisionsInput
132
+ ): Promise<ListAgentMemoryRevisionsResult> {
133
+ return this.db.transaction(() => {
134
+ const { total } = this.db
135
+ .query(
136
+ "SELECT COUNT(*) AS total FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?"
137
+ )
138
+ .get(input.projectId, input.repoId) as { total: number }
139
+ const offset = input.offset ?? 0
140
+ const rows = this.db
141
+ .query(
142
+ `SELECT revision, message, author_principal_type, author_principal_id, source, created_at
143
+ FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?
144
+ ORDER BY revision DESC LIMIT ? OFFSET ?`
145
+ )
146
+ .all(input.projectId, input.repoId, input.limit ?? -1, offset) as RevisionRow[]
147
+ const changes = this.db
148
+ .query(
149
+ `SELECT revision, path, contents IS NULL AS deleted FROM agent_memory_files
150
+ WHERE project_id = ? AND repo_id = ? AND revision IN (${rows.map(() => "?").join(", ")})
151
+ ORDER BY path`
152
+ )
153
+ .all(input.projectId, input.repoId, ...rows.map((row) => row.revision)) as {
154
+ revision: number
155
+ path: string
156
+ deleted: number
157
+ }[]
158
+ const revisions = rows.map(
159
+ (row): AgentMemoryRevisionRecord => ({
160
+ revision: row.revision,
161
+ message: row.message,
162
+ author: { type: row.author_principal_type, id: row.author_principal_id },
163
+ ...(row.source === null ? {} : { source: row.source }),
164
+ changes: changes
165
+ .filter((change) => change.revision === row.revision)
166
+ .map((change) => ({ path: change.path, deleted: change.deleted === 1 })),
167
+ createdAt: new Date(row.created_at),
168
+ })
169
+ )
170
+ return { revisions, hasMore: offset + rows.length < total, total }
171
+ })()
172
+ }
173
+
174
+ async delete(input: { projectId: string; repoId: string }): Promise<void> {
175
+ // File versions go with their revisions through ON DELETE CASCADE.
176
+ this.db
177
+ .query("DELETE FROM agent_memory_revisions WHERE project_id = ? AND repo_id = ?")
178
+ .run(input.projectId, input.repoId)
179
+ }
180
+
181
+ /** The highest revision, or the highest saved by `asOf` and up to `upTo`. */
182
+ private head(projectId: string, repoId: string, asOf?: Date, upTo?: number): number {
183
+ const at = asOf?.toISOString() ?? null
184
+ const row = this.db
185
+ .query(
186
+ `SELECT COALESCE(MAX(revision), 0) AS revision FROM agent_memory_revisions
187
+ WHERE project_id = ? AND repo_id = ? AND (? IS NULL OR created_at <= ?)
188
+ AND (? IS NULL OR revision <= ?)`
189
+ )
190
+ .get(projectId, repoId, at, at, upTo ?? null, upTo ?? null) as {
191
+ revision: number
192
+ }
193
+ return row.revision
194
+ }
195
+ }