@sixb/sqlite 0.1.21 → 0.1.23

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 (100) hide show
  1. package/dist/agents/agent-storage.d.ts +8 -2
  2. package/dist/agents/agent-storage.d.ts.map +1 -1
  3. package/dist/agents/approvals.d.ts +51 -0
  4. package/dist/agents/approvals.d.ts.map +1 -0
  5. package/dist/agents/checkpoints.d.ts +1 -0
  6. package/dist/agents/checkpoints.d.ts.map +1 -1
  7. package/dist/agents/index.d.ts +1 -0
  8. package/dist/agents/index.d.ts.map +1 -1
  9. package/dist/agents/memory.d.ts +21 -0
  10. package/dist/agents/memory.d.ts.map +1 -0
  11. package/dist/agents/messages.d.ts.map +1 -1
  12. package/dist/agents/rows.d.ts +15 -1
  13. package/dist/agents/rows.d.ts.map +1 -1
  14. package/dist/agents/runs.d.ts +14 -1
  15. package/dist/agents/runs.d.ts.map +1 -1
  16. package/dist/agents/settings.d.ts +29 -0
  17. package/dist/agents/settings.d.ts.map +1 -0
  18. package/dist/agents/threads.d.ts +12 -1
  19. package/dist/agents/threads.d.ts.map +1 -1
  20. package/dist/ai-cost-groups.d.ts.map +1 -1
  21. package/dist/ai-cost-storage.d.ts.map +1 -1
  22. package/dist/ai-usage-storage.d.ts +4 -1
  23. package/dist/ai-usage-storage.d.ts.map +1 -1
  24. package/dist/artifact-storage.d.ts +10 -1
  25. package/dist/artifact-storage.d.ts.map +1 -1
  26. package/dist/auth-storage/auth-storage.d.ts +3 -2
  27. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  28. package/dist/auth-storage/users.d.ts +7 -3
  29. package/dist/auth-storage/users.d.ts.map +1 -1
  30. package/dist/execution-storage.d.ts +2 -1
  31. package/dist/execution-storage.d.ts.map +1 -1
  32. package/dist/index.js +3348 -1515
  33. package/dist/index.js.map +32 -26
  34. package/dist/migrations.d.ts +0 -1
  35. package/dist/migrations.d.ts.map +1 -1
  36. package/dist/objects/conditional-reader.d.ts +8 -0
  37. package/dist/objects/conditional-reader.d.ts.map +1 -0
  38. package/dist/objects/query-compiler.d.ts +26 -1
  39. package/dist/objects/query-compiler.d.ts.map +1 -1
  40. package/dist/objects/read-scope.d.ts +10 -1
  41. package/dist/objects/read-scope.d.ts.map +1 -1
  42. package/dist/objects/reader.d.ts +2 -0
  43. package/dist/objects/reader.d.ts.map +1 -1
  44. package/dist/objects/scoped-reader.d.ts +15 -0
  45. package/dist/objects/scoped-reader.d.ts.map +1 -0
  46. package/dist/objects/selected-reader.d.ts.map +1 -1
  47. package/dist/objects/storage.d.ts +6 -1
  48. package/dist/objects/storage.d.ts.map +1 -1
  49. package/dist/ontology-storage/index.d.ts +2 -0
  50. package/dist/ontology-storage/index.d.ts.map +1 -1
  51. package/dist/ontology-storage/materialization-writer.d.ts +2 -0
  52. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  53. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  54. package/dist/ontology-storage/unique-values.d.ts +42 -0
  55. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  56. package/package.json +3 -3
  57. package/src/agents/agent-storage.ts +11 -2
  58. package/src/agents/approvals.ts +456 -0
  59. package/src/agents/checkpoints.ts +9 -4
  60. package/src/agents/index.ts +1 -0
  61. package/src/agents/memory.ts +195 -0
  62. package/src/agents/messages.ts +21 -18
  63. package/src/agents/rows.ts +46 -1
  64. package/src/agents/runs.ts +202 -21
  65. package/src/agents/settings.ts +112 -0
  66. package/src/agents/threads.ts +385 -1
  67. package/src/ai-cost-groups.ts +11 -1
  68. package/src/ai-cost-storage.ts +54 -12
  69. package/src/ai-usage-storage.ts +79 -2
  70. package/src/artifact-storage.ts +206 -9
  71. package/src/auth-storage/auth-storage.ts +58 -26
  72. package/src/auth-storage/users.ts +106 -60
  73. package/src/execution-storage.ts +39 -0
  74. package/src/migrations/067-artifact-kinds.sql +32 -0
  75. package/src/migrations/068-artifact-state.sql +15 -0
  76. package/src/migrations/069-agent-thread-participants.sql +16 -0
  77. package/src/migrations/070-forked-context-checkpoints.sql +45 -0
  78. package/src/migrations/071-unique-values.sql +22 -0
  79. package/src/migrations/072-generated-media-usage.sql +32 -0
  80. package/src/migrations/073-model-call-files.sql +11 -0
  81. package/src/migrations/074-agent-memory.sql +32 -0
  82. package/src/migrations/075-artifact-threads.sql +21 -0
  83. package/src/migrations/076-agent-approvals.sql +59 -0
  84. package/src/migrations/077-agent-approval-resume.sql +20 -0
  85. package/src/migrations/078-subagent-threads.sql +241 -0
  86. package/src/migrations/079-subagent-questions.sql +6 -0
  87. package/src/migrations.ts +75 -1
  88. package/src/objects/README.md +7 -0
  89. package/src/objects/conditional-reader.ts +27 -0
  90. package/src/objects/query-compiler.ts +36 -6
  91. package/src/objects/read-scope.ts +140 -23
  92. package/src/objects/reader.ts +47 -14
  93. package/src/objects/scoped-reader.ts +103 -0
  94. package/src/objects/selected-reader.ts +12 -75
  95. package/src/objects/storage.ts +16 -0
  96. package/src/ontology-storage/index.ts +3 -0
  97. package/src/ontology-storage/materialization-writer.ts +14 -0
  98. package/src/ontology-storage/materializations.ts +16 -1
  99. package/src/ontology-storage/unique-values.ts +187 -0
  100. package/src/transactions.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import type { Database } from "bun:sqlite"
2
+ import { countsAsThreadMessage } from "@sixb/core/internal/agent-run-storage-provider"
2
3
  import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
3
4
  import {
4
5
  type AgentMessageRecord,
@@ -101,11 +102,17 @@ export class SqliteAgentMessageStore implements AgentMessageStore {
101
102
  .query(
102
103
  `
103
104
  UPDATE agent_threads
104
- SET message_count = message_count + 1, last_message_at = ?, updated_at = ?
105
+ SET message_count = message_count + ?, last_message_at = ?, updated_at = ?
105
106
  WHERE project_id = ? AND id = ?
106
107
  `
107
108
  )
108
- .run(createdAt.toISOString(), createdAt.toISOString(), input.projectId, input.threadId)
109
+ .run(
110
+ countsAsThreadMessage(input) ? 1 : 0,
111
+ createdAt.toISOString(),
112
+ createdAt.toISOString(),
113
+ input.projectId,
114
+ input.threadId
115
+ )
109
116
 
110
117
  return this.requireMessage(input.projectId, input.id)
111
118
  })()
@@ -125,11 +132,17 @@ export class SqliteAgentMessageStore implements AgentMessageStore {
125
132
  }
126
133
 
127
134
  const deleted = this.db
128
- .query("DELETE FROM agent_messages WHERE project_id = ? AND thread_id = ? AND run_id = ?")
129
- .run(input.projectId, input.threadId, input.runId)
130
- if (deleted.changes === 0) {
135
+ .query(
136
+ "DELETE FROM agent_messages WHERE project_id = ? AND thread_id = ? AND run_id = ? RETURNING parts"
137
+ )
138
+ .all(input.projectId, input.threadId, input.runId) as { parts: string }[]
139
+ if (deleted.length === 0) {
131
140
  return 0
132
141
  }
142
+ // The thread counts only the messages the conversation shows: take back the ones it counted.
143
+ const counted = deleted.filter((row) =>
144
+ countsAsThreadMessage({ parts: JSON.parse(row.parts) })
145
+ ).length
133
146
 
134
147
  const updatedAt = new Date().toISOString()
135
148
  this.db
@@ -137,9 +150,7 @@ export class SqliteAgentMessageStore implements AgentMessageStore {
137
150
  `
138
151
  UPDATE agent_threads
139
152
  SET
140
- message_count = (
141
- SELECT COUNT(*) FROM agent_messages WHERE project_id = ? AND thread_id = ?
142
- ),
153
+ message_count = MAX(message_count - ?, 0),
143
154
  last_message_at = (
144
155
  SELECT created_at FROM agent_messages
145
156
  WHERE project_id = ? AND thread_id = ?
@@ -150,17 +161,9 @@ export class SqliteAgentMessageStore implements AgentMessageStore {
150
161
  WHERE project_id = ? AND id = ?
151
162
  `
152
163
  )
153
- .run(
154
- input.projectId,
155
- input.threadId,
156
- input.projectId,
157
- input.threadId,
158
- updatedAt,
159
- input.projectId,
160
- input.threadId
161
- )
164
+ .run(counted, input.projectId, input.threadId, updatedAt, input.projectId, input.threadId)
162
165
 
163
- return deleted.changes
166
+ return deleted.length
164
167
  })()
165
168
  }
166
169
 
@@ -8,6 +8,7 @@ import {
8
8
  type AgentMessageRecord,
9
9
  type AgentRunDiagnostic,
10
10
  type AgentRunRecord,
11
+ type AgentThreadParticipantRecord,
11
12
  type AgentThreadRecord,
12
13
  type AgentThreadSandboxState,
13
14
  type ConversationAgentRunSpec,
@@ -22,6 +23,10 @@ import type { SqliteValue } from "../run-list-query"
22
23
  export interface AgentThreadRow {
23
24
  project_id: string
24
25
  id: string
26
+ kind: AgentThreadRecord["kind"]
27
+ parent_thread_id: string | null
28
+ parent_run_id: string | null
29
+ spawn_key: string | null
25
30
  owner_principal_type: Principal["type"]
26
31
  owner_principal_id: string
27
32
  title: string | null
@@ -35,6 +40,16 @@ export interface AgentThreadRow {
35
40
  updated_at: string
36
41
  }
37
42
 
43
+ export interface AgentThreadParticipantRow {
44
+ project_id: string
45
+ thread_id: string
46
+ principal_type: "user"
47
+ principal_id: string
48
+ added_at: string
49
+ added_by_type: Principal["type"]
50
+ added_by_id: string
51
+ }
52
+
38
53
  export interface AgentRunRow {
39
54
  project_id: string
40
55
  id: string
@@ -98,6 +113,16 @@ export function rowToThreadRecord(row: AgentThreadRow): AgentThreadRecord {
98
113
  return {
99
114
  id: row.id,
100
115
  projectId: row.project_id,
116
+ kind: row.kind,
117
+ ...(row.parent_thread_id && row.parent_run_id && row.spawn_key
118
+ ? {
119
+ parent: {
120
+ threadId: row.parent_thread_id,
121
+ spawnedBy: row.parent_run_id,
122
+ key: row.spawn_key,
123
+ },
124
+ }
125
+ : {}),
101
126
  ownerPrincipal: { type: row.owner_principal_type, id: row.owner_principal_id },
102
127
  title: row.title ?? undefined,
103
128
  ...(row.sandbox_state == null
@@ -154,12 +179,20 @@ export function rowToRunRecord(row: AgentRunRow): AgentRunRecord {
154
179
  ...(row.spec === null ? {} : { spec: JSON.parse(row.spec) as ConversationAgentRunSpec }),
155
180
  }
156
181
  }
157
- if (!row.parent_run_id || !row.spawn_key || row.spec === null) {
182
+ if (
183
+ !row.thread_id ||
184
+ !row.trigger_message_id ||
185
+ !row.parent_run_id ||
186
+ !row.spawn_key ||
187
+ row.spec === null
188
+ ) {
158
189
  throw new Error(`[SixbSqlite] Subagent run '${row.id}' has invalid persisted fields.`)
159
190
  }
160
191
  return {
161
192
  ...base,
162
193
  kind: "subagent",
194
+ threadId: row.thread_id,
195
+ triggerMessageId: row.trigger_message_id,
163
196
  parentRunId: row.parent_run_id,
164
197
  spawnKey: row.spawn_key,
165
198
  spec: JSON.parse(row.spec) as SubagentRunSpec,
@@ -212,6 +245,18 @@ export function rowToContextCheckpointRecord(
212
245
  }
213
246
  }
214
247
 
248
+ export function rowToParticipantRecord(
249
+ row: AgentThreadParticipantRow
250
+ ): AgentThreadParticipantRecord {
251
+ return {
252
+ projectId: row.project_id,
253
+ threadId: row.thread_id,
254
+ principal: { type: "user", id: row.principal_id },
255
+ addedAt: new Date(row.added_at),
256
+ addedBy: { type: row.added_by_type, id: row.added_by_id },
257
+ }
258
+ }
259
+
215
260
  function principalFromColumns(
216
261
  type: Principal["type"] | null | undefined,
217
262
  id: string | null | undefined
@@ -1,11 +1,17 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import {
3
+ agentRunMayHoldCalls,
3
4
  assertAgentRunExecution,
5
+ assertAgentRunFinishPayload,
6
+ assertAwaitingApprovalFinish,
4
7
  assertConversationAgentRunSpec,
8
+ assertConversationThreadRow,
5
9
  assertCreateSubagentRunInput,
6
- assertSubagentRunResult,
10
+ assertSubagentThreadTakesRun,
11
+ newSubagentThreadRecord,
7
12
  subagentRunMatchesCreateInput,
8
13
  } from "@sixb/core/internal/agent-run-storage-provider"
14
+ import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
9
15
  import { serializeSixbFailure } from "@sixb/core/internal/errors"
10
16
  import {
11
17
  AGENT_RUN_FAILURE_CODES,
@@ -21,6 +27,7 @@ import {
21
27
  type FinishQueuedAgentRunInput,
22
28
  type ListAgentRunsInput,
23
29
  type ListAgentRunsResult,
30
+ type ListThreadSubagentsInput,
24
31
  type ReclaimAgentRunInput,
25
32
  type StartAgentRunInput,
26
33
  type SubagentRunRecord,
@@ -32,7 +39,8 @@ import {
32
39
  type SqliteValue,
33
40
  } from "../run-list-query"
34
41
  import { isUniqueConstraintError } from "../storage-errors"
35
- import { type AgentRunRow, rowToRunRecord } from "./rows"
42
+ import { sqliteHoldsRun } from "./approvals"
43
+ import { type AgentRunRow, type AgentThreadRow, rowToRunRecord, rowToThreadRecord } from "./rows"
36
44
 
37
45
  const SQLITE_RUN_ID_BATCH_SIZE = 500
38
46
 
@@ -54,8 +62,11 @@ export class SqliteAgentRunStore implements AgentRunStore {
54
62
 
55
63
  return this.db.transaction(() => {
56
64
  const thread = this.db
57
- .query("SELECT active_run_id FROM agent_threads WHERE project_id = ? AND id = ?")
58
- .get(input.projectId, input.threadId) as { active_run_id: string | null } | null
65
+ .query("SELECT id, kind, active_run_id FROM agent_threads WHERE project_id = ? AND id = ?")
66
+ .get(input.projectId, input.threadId) as Pick<
67
+ AgentThreadRow,
68
+ "id" | "kind" | "active_run_id"
69
+ > | null
59
70
 
60
71
  if (!thread) {
61
72
  throw new AgentStorageError(
@@ -63,6 +74,7 @@ export class SqliteAgentRunStore implements AgentRunStore {
63
74
  `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
64
75
  )
65
76
  }
77
+ assertConversationThreadRow(thread, "takes turns", "SixbSqlite")
66
78
  if (thread.active_run_id !== null) {
67
79
  throw new AgentStorageError(
68
80
  "active_run_exists",
@@ -188,24 +200,54 @@ export class SqliteAgentRunStore implements AgentRunStore {
188
200
  )
189
201
  }
190
202
 
203
+ const createdAt = input.createdAt ?? new Date()
191
204
  try {
205
+ this.claimChildThread(input, parentRow, createdAt)
192
206
  this.db
193
207
  .query(
194
208
  `
195
209
  INSERT INTO agent_runs (
196
- project_id, id, execution_id, kind, parent_run_id, spawn_key, spec,
197
- status, attempt, created_at
198
- ) VALUES (?, ?, ?, 'subagent', ?, ?, ?, 'queued', 0, ?)
210
+ project_id, id, execution_id, kind, thread_id, trigger_message_id, parent_run_id,
211
+ spawn_key, spec, status, attempt, created_at
212
+ ) VALUES (?, ?, ?, 'subagent', ?, ?, ?, ?, ?, 'queued', 0, ?)
199
213
  `
200
214
  )
201
215
  .run(
202
216
  input.projectId,
203
217
  input.id,
204
218
  input.executionId,
219
+ input.thread.id,
220
+ input.trigger.id,
205
221
  input.parentRunId,
206
222
  input.spawnKey,
207
223
  JSON.stringify(input.spec),
208
- (input.createdAt ?? new Date()).toISOString()
224
+ createdAt.toISOString()
225
+ )
226
+ this.db
227
+ .query(
228
+ `
229
+ INSERT INTO agent_messages (
230
+ project_id, id, thread_id, run_id, role, seq, parts, metadata, content_version,
231
+ created_at
232
+ ) VALUES (
233
+ ?, ?, ?, ?, 'user',
234
+ (SELECT COALESCE(MAX(seq), 0) + 1 FROM agent_messages
235
+ WHERE project_id = ? AND thread_id = ?),
236
+ ?, ?, ?, ?
237
+ )
238
+ `
239
+ )
240
+ .run(
241
+ input.projectId,
242
+ input.trigger.id,
243
+ input.thread.id,
244
+ input.id,
245
+ input.projectId,
246
+ input.thread.id,
247
+ JSON.stringify(input.trigger.parts),
248
+ input.trigger.metadata === undefined ? null : JSON.stringify(input.trigger.metadata),
249
+ AGENT_MESSAGE_CONTENT_VERSION,
250
+ createdAt.toISOString()
209
251
  )
210
252
  } catch (error) {
211
253
  if (isUniqueConstraintError(error)) {
@@ -347,18 +389,14 @@ export class SqliteAgentRunStore implements AgentRunStore {
347
389
  `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`
348
390
  )
349
391
  }
350
- if (input.status === "succeeded" && run.kind === "subagent") {
351
- assertSubagentRunResult(input.result, run.id, "SixbSqlite")
352
- }
353
- if (
354
- input.status === "succeeded" &&
355
- run.kind === "conversation" &&
356
- input.result !== undefined
357
- ) {
358
- throw new AgentStorageError(
359
- "invalid_input",
360
- `[SixbSqlite] Conversational Agent run '${run.id}' cannot persist a subagent result.`
361
- )
392
+ assertAgentRunFinishPayload(run.kind, input, run.id, "SixbSqlite")
393
+ if (input.finishReason === "awaiting_approval") {
394
+ assertAwaitingApprovalFinish({
395
+ runId: input.id,
396
+ status: input.status,
397
+ holdsApproval: sqliteHoldsRun(this.db, input.projectId, input.id),
398
+ prefix: "SixbSqlite",
399
+ })
362
400
  }
363
401
 
364
402
  this.db
@@ -393,11 +431,43 @@ export class SqliteAgentRunStore implements AgentRunStore {
393
431
  )
394
432
 
395
433
  this.releaseThread(run, completedAt)
434
+ if (agentRunMayHoldCalls(rowToRunRecord(run)))
435
+ this.settleApprovals(run.kind, input, completedAt)
396
436
 
397
437
  return this.requireRun(input.projectId, input.id)
398
438
  })()
399
439
  }
400
440
 
441
+ /**
442
+ * A turn that pauses keeps its calls for their decision. A turn that ends any other way cancels
443
+ * the ones still waiting, a late one included, and never resumes on the rest. A child that fails
444
+ * or is cancelled takes its own pending calls with it.
445
+ */
446
+ private settleApprovals(
447
+ kind: AgentRunRecord["kind"],
448
+ input: FinishAgentRunInput,
449
+ completedAt: Date
450
+ ): void {
451
+ if (input.finishReason === "awaiting_approval") return
452
+ const turn = kind === "conversation"
453
+ if (!turn && input.status === "succeeded") return
454
+
455
+ this.db
456
+ .query(
457
+ `UPDATE agent_approvals SET status = 'cancelled'
458
+ WHERE project_id = ? AND status = 'pending' AND (run_id = ? OR caller_run_id = ?)`
459
+ )
460
+ .run(input.projectId, input.id, input.id)
461
+ if (!turn) return
462
+ this.db
463
+ .query(
464
+ `UPDATE agent_approvals SET turn_stopped_at = ?
465
+ WHERE project_id = ? AND run_id = ?
466
+ AND reported_in_message_id IS NULL AND turn_stopped_at IS NULL`
467
+ )
468
+ .run(completedAt.toISOString(), input.projectId, input.id)
469
+ }
470
+
401
471
  async getById(params: { projectId: string; id: string }): Promise<AgentRunRecord | null> {
402
472
  const row = this.db
403
473
  .query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?")
@@ -432,6 +502,41 @@ export class SqliteAgentRunStore implements AgentRunStore {
432
502
  })
433
503
  }
434
504
 
505
+ async listThreadSubagents(
506
+ input: ListThreadSubagentsInput
507
+ ): Promise<readonly SubagentRunRecord[]> {
508
+ // Reached through the turns of the conversation, by thread, then each turn's children by key
509
+ // or, for a finish reason, by `idx_agent_runs_paused_on_question`.
510
+ const filters: string[] = []
511
+ const args: SqliteValue[] = [input.projectId, input.threadId]
512
+ if (input.spawnKey !== undefined) {
513
+ filters.push("AND child.spawn_key = ?")
514
+ args.push(input.spawnKey)
515
+ }
516
+ if (input.finishReason !== undefined) {
517
+ filters.push("AND child.finish_reason = ?")
518
+ args.push(input.finishReason)
519
+ }
520
+ args.push(input.limit ?? 100)
521
+
522
+ const rows = this.db
523
+ .query(
524
+ `SELECT child.* FROM agent_runs turn
525
+ JOIN agent_runs child
526
+ ON child.project_id = turn.project_id AND child.parent_run_id = turn.id
527
+ WHERE turn.project_id = ? AND turn.thread_id = ? AND child.kind = 'subagent'
528
+ ${filters.join(" ")}
529
+ ORDER BY COALESCE(turn.started_at, turn.created_at) DESC, turn.id DESC,
530
+ child.created_at DESC, child.id DESC
531
+ LIMIT ?`
532
+ )
533
+ .all(...args) as AgentRunRow[]
534
+ return rows.flatMap((row) => {
535
+ const run = rowToRunRecord(row)
536
+ return run.kind === "subagent" ? [run] : []
537
+ })
538
+ }
539
+
435
540
  async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
436
541
  if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
437
542
  return { runs: [], hasMore: false, total: 0 }
@@ -500,8 +605,84 @@ export class SqliteAgentRunStore implements AgentRunStore {
500
605
  return row
501
606
  }
502
607
 
608
+ /**
609
+ * The child's thread: its first run creates it, owned by the conversation's owner; a later run
610
+ * takes it while it is idle. Either way the run is its active run.
611
+ */
612
+ private claimChildThread(
613
+ input: CreateSubagentRunInput,
614
+ parent: AgentRunRow,
615
+ createdAt: Date
616
+ ): void {
617
+ if (!input.thread.create) {
618
+ const row = this.db
619
+ .query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?")
620
+ .get(input.projectId, input.thread.id) as AgentThreadRow | null
621
+ assertSubagentThreadTakesRun(
622
+ row ? rowToThreadRecord(row) : null,
623
+ input,
624
+ parent.thread_id ?? "",
625
+ "SixbSqlite"
626
+ )
627
+ this.db
628
+ .query(
629
+ `
630
+ UPDATE agent_threads
631
+ SET active_run_id = ?, message_count = message_count + 1, last_message_at = ?,
632
+ updated_at = ?
633
+ WHERE project_id = ? AND id = ?
634
+ `
635
+ )
636
+ .run(
637
+ input.id,
638
+ createdAt.toISOString(),
639
+ createdAt.toISOString(),
640
+ input.projectId,
641
+ input.thread.id
642
+ )
643
+ return
644
+ }
645
+
646
+ const conversation = this.db
647
+ .query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?")
648
+ .get(input.projectId, parent.thread_id) as AgentThreadRow | null
649
+ if (!conversation) {
650
+ throw new AgentStorageError(
651
+ "thread_not_found",
652
+ `[SixbSqlite] Agent thread '${parent.thread_id}' not found for project '${input.projectId}'.`
653
+ )
654
+ }
655
+
656
+ const thread = newSubagentThreadRecord(input, rowToThreadRecord(conversation), createdAt)
657
+ this.db
658
+ .query(
659
+ `
660
+ INSERT INTO agent_threads (
661
+ project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
662
+ owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
663
+ updated_at
664
+ ) VALUES (?, ?, 'subagent', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
665
+ `
666
+ )
667
+ .run(
668
+ thread.projectId,
669
+ thread.id,
670
+ conversation.id,
671
+ input.parentRunId,
672
+ input.spawnKey,
673
+ thread.ownerPrincipal.type,
674
+ thread.ownerPrincipal.id,
675
+ thread.status,
676
+ input.id,
677
+ createdAt.toISOString(),
678
+ thread.messageCount,
679
+ createdAt.toISOString(),
680
+ createdAt.toISOString()
681
+ )
682
+ }
683
+
684
+ /** A turn releases its conversation, a child run its own thread. */
503
685
  private releaseThread(run: AgentRunRow, completedAt: Date): void {
504
- if (run.kind !== "conversation") return
505
686
  this.db
506
687
  .query(
507
688
  `
@@ -0,0 +1,112 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { assertAgentApprovalModeInput } from "@sixb/core/internal/agent-run-storage-provider"
3
+ import {
4
+ type AgentApprovalMode,
5
+ type AgentApprovalModeStore,
6
+ AgentStorageError,
7
+ type Principal,
8
+ } from "@sixb/core/storage"
9
+
10
+ export class SqliteAgentApprovalModeStore implements AgentApprovalModeStore {
11
+ constructor(private readonly db: Database) {}
12
+
13
+ async getDefault(input: {
14
+ readonly projectId: string
15
+ readonly principal: Principal
16
+ }): Promise<AgentApprovalMode | null> {
17
+ const row = this.db
18
+ .query(
19
+ `SELECT approval_mode FROM agent_user_settings
20
+ WHERE project_id = ? AND principal_type = ? AND principal_id = ?`
21
+ )
22
+ .get(input.projectId, input.principal.type, input.principal.id) as {
23
+ approval_mode: AgentApprovalMode
24
+ } | null
25
+ return row?.approval_mode ?? null
26
+ }
27
+
28
+ async setDefault(input: {
29
+ readonly projectId: string
30
+ readonly principal: Principal
31
+ readonly mode: AgentApprovalMode
32
+ readonly updatedAt?: Date
33
+ }): Promise<void> {
34
+ assertAgentApprovalModeInput(input.mode, "SixbSqlite")
35
+ this.db
36
+ .query(
37
+ `INSERT INTO agent_user_settings (
38
+ project_id, principal_type, principal_id, approval_mode, updated_at
39
+ ) VALUES (?, ?, ?, ?, ?)
40
+ ON CONFLICT (project_id, principal_type, principal_id)
41
+ DO UPDATE SET approval_mode = excluded.approval_mode, updated_at = excluded.updated_at`
42
+ )
43
+ .run(
44
+ input.projectId,
45
+ input.principal.type,
46
+ input.principal.id,
47
+ input.mode,
48
+ (input.updatedAt ?? new Date()).toISOString()
49
+ )
50
+ }
51
+
52
+ async getForThread(input: {
53
+ readonly projectId: string
54
+ readonly threadId: string
55
+ readonly principal: Principal
56
+ }): Promise<AgentApprovalMode | null> {
57
+ const row = this.db
58
+ .query(
59
+ `SELECT approval_mode FROM agent_thread_user_settings
60
+ WHERE project_id = ? AND thread_id = ? AND principal_type = ? AND principal_id = ?`
61
+ )
62
+ .get(input.projectId, input.threadId, input.principal.type, input.principal.id) as {
63
+ approval_mode: AgentApprovalMode
64
+ } | null
65
+ return row?.approval_mode ?? null
66
+ }
67
+
68
+ async setForThread(input: {
69
+ readonly projectId: string
70
+ readonly threadId: string
71
+ readonly principal: Principal
72
+ readonly mode: AgentApprovalMode | null
73
+ readonly updatedAt?: Date
74
+ }): Promise<void> {
75
+ if (input.mode !== null) assertAgentApprovalModeInput(input.mode, "SixbSqlite")
76
+ const thread = this.db
77
+ .query("SELECT id FROM agent_threads WHERE project_id = ? AND id = ?")
78
+ .get(input.projectId, input.threadId)
79
+ if (!thread) {
80
+ throw new AgentStorageError(
81
+ "thread_not_found",
82
+ `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
83
+ )
84
+ }
85
+
86
+ if (input.mode === null) {
87
+ this.db
88
+ .query(
89
+ `DELETE FROM agent_thread_user_settings
90
+ WHERE project_id = ? AND thread_id = ? AND principal_type = ? AND principal_id = ?`
91
+ )
92
+ .run(input.projectId, input.threadId, input.principal.type, input.principal.id)
93
+ return
94
+ }
95
+ this.db
96
+ .query(
97
+ `INSERT INTO agent_thread_user_settings (
98
+ project_id, thread_id, principal_type, principal_id, approval_mode, updated_at
99
+ ) VALUES (?, ?, ?, ?, ?, ?)
100
+ ON CONFLICT (project_id, thread_id, principal_type, principal_id)
101
+ DO UPDATE SET approval_mode = excluded.approval_mode, updated_at = excluded.updated_at`
102
+ )
103
+ .run(
104
+ input.projectId,
105
+ input.threadId,
106
+ input.principal.type,
107
+ input.principal.id,
108
+ input.mode,
109
+ (input.updatedAt ?? new Date()).toISOString()
110
+ )
111
+ }
112
+ }