@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,241 @@
1
+ -- A sub-agent works in a thread of its own: its task and its work are that thread's messages, and
2
+ -- each of its runs has the thread and a trigger message, as a turn does. The thread's parent is the
3
+ -- conversation, which every read of it goes through, and its spawning turn and key name it.
4
+ --
5
+ -- SQLite adds neither a foreign key nor a check with ALTER TABLE: both tables are rebuilt, as 033
6
+ -- rebuilt `agent_runs`. Their references use the final names.
7
+
8
+ -- The children admitted before this have no thread. Each gets one named as a new child's is, with
9
+ -- its task as the only message: what it answered stays in its parent's `wait_agent` output.
10
+ CREATE TEMP TABLE legacy_subagent_threads AS
11
+ SELECT
12
+ child.project_id,
13
+ child.id AS run_id,
14
+ 'agt_thr_child_' || CASE
15
+ WHEN substr(child.id, 1, 14) = 'agt_run_child_' THEN substr(child.id, 15)
16
+ ELSE child.id
17
+ END AS thread_id,
18
+ 'agt_msg_child_' || CASE
19
+ WHEN substr(child.id, 1, 14) = 'agt_run_child_' THEN substr(child.id, 15)
20
+ ELSE child.id
21
+ END AS message_id
22
+ FROM agent_runs AS child
23
+ WHERE child.kind = 'subagent' AND child.thread_id IS NULL;
24
+
25
+ CREATE TABLE agent_threads_v2 (
26
+ project_id TEXT NOT NULL,
27
+ id TEXT NOT NULL,
28
+ kind TEXT NOT NULL DEFAULT 'conversation' CHECK (kind IN ('conversation', 'subagent')),
29
+ parent_thread_id TEXT,
30
+ parent_run_id TEXT,
31
+ spawn_key TEXT,
32
+ owner_principal_type TEXT NOT NULL,
33
+ owner_principal_id TEXT NOT NULL,
34
+ title TEXT,
35
+ status TEXT NOT NULL CHECK (status IN ('active', 'archived')),
36
+ active_run_id TEXT,
37
+ last_message_at TEXT,
38
+ message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0),
39
+ created_at TEXT NOT NULL,
40
+ updated_at TEXT NOT NULL,
41
+ sandbox_params TEXT CHECK (
42
+ sandbox_params IS NULL
43
+ OR (json_valid(sandbox_params) AND json_type(sandbox_params) = 'object')
44
+ ),
45
+ sandbox_state TEXT CHECK (
46
+ sandbox_state IS NULL
47
+ OR (json_valid(sandbox_state) AND json_type(sandbox_state) = 'object')
48
+ ),
49
+ PRIMARY KEY (project_id, id),
50
+ UNIQUE (project_id, parent_run_id, spawn_key),
51
+ -- A conversation's sub-agent threads go with it.
52
+ FOREIGN KEY (project_id, parent_thread_id)
53
+ REFERENCES agent_threads (project_id, id)
54
+ ON DELETE CASCADE,
55
+ FOREIGN KEY (project_id, parent_run_id)
56
+ REFERENCES agent_runs (project_id, id)
57
+ ON DELETE RESTRICT,
58
+ CHECK (
59
+ (
60
+ kind = 'conversation'
61
+ AND parent_thread_id IS NULL
62
+ AND parent_run_id IS NULL
63
+ AND spawn_key IS NULL
64
+ )
65
+ OR
66
+ (
67
+ kind = 'subagent'
68
+ AND parent_thread_id IS NOT NULL
69
+ AND parent_run_id IS NOT NULL
70
+ AND spawn_key IS NOT NULL
71
+ AND sandbox_params IS NULL
72
+ )
73
+ )
74
+ );
75
+
76
+ INSERT INTO agent_threads_v2 (
77
+ project_id, id, owner_principal_type, owner_principal_id, title, status, active_run_id,
78
+ last_message_at, message_count, created_at, updated_at, sandbox_params, sandbox_state
79
+ )
80
+ SELECT
81
+ project_id, id, owner_principal_type, owner_principal_id, title, status, active_run_id,
82
+ last_message_at, message_count, created_at, updated_at, sandbox_params, sandbox_state
83
+ FROM agent_threads;
84
+
85
+ INSERT INTO agent_threads_v2 (
86
+ project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
87
+ owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
88
+ updated_at
89
+ )
90
+ SELECT
91
+ child.project_id,
92
+ legacy.thread_id,
93
+ 'subagent',
94
+ parent.thread_id,
95
+ child.parent_run_id,
96
+ child.spawn_key,
97
+ conversation.owner_principal_type,
98
+ conversation.owner_principal_id,
99
+ 'active',
100
+ CASE WHEN child.status IN ('queued', 'running') THEN child.id END,
101
+ child.created_at,
102
+ 1,
103
+ child.created_at,
104
+ COALESCE(child.completed_at, child.created_at)
105
+ FROM legacy_subagent_threads AS legacy
106
+ JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id
107
+ JOIN agent_runs AS parent
108
+ ON parent.project_id = child.project_id AND parent.id = child.parent_run_id
109
+ JOIN agent_threads AS conversation
110
+ ON conversation.project_id = parent.project_id AND conversation.id = parent.thread_id;
111
+
112
+ DROP TABLE agent_threads;
113
+ ALTER TABLE agent_threads_v2 RENAME TO agent_threads;
114
+
115
+ -- Lists read conversations only, so a conversation's children never weigh on them.
116
+ CREATE INDEX idx_agent_threads_project_activity
117
+ ON agent_threads(project_id, COALESCE(last_message_at, created_at) DESC)
118
+ WHERE kind = 'conversation';
119
+ CREATE INDEX idx_agent_threads_project_status_activity
120
+ ON agent_threads(project_id, status, COALESCE(last_message_at, created_at) DESC)
121
+ WHERE kind = 'conversation';
122
+ CREATE INDEX idx_agent_threads_project_owner_activity
123
+ ON agent_threads(
124
+ project_id,
125
+ owner_principal_type,
126
+ owner_principal_id,
127
+ COALESCE(last_message_at, created_at) DESC
128
+ )
129
+ WHERE kind = 'conversation';
130
+
131
+ -- A conversation's children, in the order they were spawned. Only a child has a parent, so the
132
+ -- index holds children only, and a read by `parent_thread_id` can use it.
133
+ CREATE INDEX idx_agent_threads_children
134
+ ON agent_threads(project_id, parent_thread_id, created_at, id)
135
+ WHERE parent_thread_id IS NOT NULL;
136
+
137
+ INSERT INTO agent_messages (
138
+ project_id, id, thread_id, run_id, role, seq, parts, content_version, created_at
139
+ )
140
+ SELECT
141
+ child.project_id,
142
+ legacy.message_id,
143
+ legacy.thread_id,
144
+ child.id,
145
+ 'user',
146
+ 1,
147
+ json_array(json_object('type', 'text', 'text', json_extract(child.spec, '$.task'))),
148
+ 2,
149
+ child.created_at
150
+ FROM legacy_subagent_threads AS legacy
151
+ JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id;
152
+
153
+ CREATE TABLE agent_runs_v6 (
154
+ project_id TEXT NOT NULL,
155
+ id TEXT NOT NULL,
156
+ execution_id TEXT NOT NULL,
157
+ kind TEXT NOT NULL DEFAULT 'conversation' CHECK (kind IN ('conversation', 'subagent')),
158
+ thread_id TEXT,
159
+ trigger_message_id TEXT,
160
+ parent_run_id TEXT,
161
+ spawn_key TEXT,
162
+ spec TEXT CHECK (spec IS NULL OR json_valid(spec)),
163
+ result TEXT CHECK (result IS NULL OR json_valid(result)),
164
+ status TEXT NOT NULL CHECK (
165
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')
166
+ ),
167
+ model_id TEXT,
168
+ finish_reason TEXT,
169
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
170
+ diagnostics TEXT,
171
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
172
+ execution_token TEXT,
173
+ execution_queue_lease_expires_at TEXT,
174
+ created_at TEXT NOT NULL,
175
+ started_at TEXT,
176
+ completed_at TEXT,
177
+ PRIMARY KEY (project_id, id),
178
+ UNIQUE (project_id, execution_id),
179
+ UNIQUE (project_id, parent_run_id, spawn_key),
180
+ FOREIGN KEY (project_id, execution_id)
181
+ REFERENCES executions (project_id, id)
182
+ ON DELETE RESTRICT,
183
+ FOREIGN KEY (project_id, parent_run_id)
184
+ REFERENCES agent_runs (project_id, id)
185
+ ON DELETE RESTRICT,
186
+ CHECK ((execution_token IS NULL) = (execution_queue_lease_expires_at IS NULL)),
187
+ CHECK (
188
+ (
189
+ kind = 'conversation'
190
+ AND thread_id IS NOT NULL
191
+ AND trigger_message_id IS NOT NULL
192
+ AND parent_run_id IS NULL
193
+ AND spawn_key IS NULL
194
+ AND result IS NULL
195
+ )
196
+ OR
197
+ (
198
+ kind = 'subagent'
199
+ AND thread_id IS NOT NULL
200
+ AND trigger_message_id IS NOT NULL
201
+ AND parent_run_id IS NOT NULL
202
+ AND spawn_key IS NOT NULL
203
+ AND spec IS NOT NULL
204
+ )
205
+ ),
206
+ CHECK (kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL)
207
+ );
208
+
209
+ INSERT INTO agent_runs_v6 (
210
+ project_id, id, execution_id, kind, thread_id, trigger_message_id, parent_run_id, spawn_key,
211
+ spec, result, status, model_id, finish_reason, error, diagnostics, attempt, execution_token,
212
+ execution_queue_lease_expires_at, created_at, started_at, completed_at
213
+ )
214
+ SELECT
215
+ run.project_id, run.id, run.execution_id, run.kind,
216
+ COALESCE(run.thread_id, legacy.thread_id),
217
+ COALESCE(run.trigger_message_id, legacy.message_id),
218
+ run.parent_run_id, run.spawn_key, run.spec, run.result, run.status, run.model_id,
219
+ run.finish_reason, run.error, run.diagnostics, run.attempt, run.execution_token,
220
+ run.execution_queue_lease_expires_at, run.created_at, run.started_at, run.completed_at
221
+ FROM agent_runs AS run
222
+ LEFT JOIN legacy_subagent_threads AS legacy
223
+ ON legacy.project_id = run.project_id AND legacy.run_id = run.id;
224
+
225
+ DROP TABLE agent_runs;
226
+ ALTER TABLE agent_runs_v6 RENAME TO agent_runs;
227
+
228
+ CREATE INDEX idx_agent_runs_project_thread_started
229
+ ON agent_runs (project_id, thread_id, COALESCE(started_at, created_at) DESC, id DESC);
230
+ CREATE INDEX idx_agent_runs_project_status_created
231
+ ON agent_runs (project_id, status, created_at, id);
232
+ CREATE INDEX idx_agent_runs_project_parent_started
233
+ ON agent_runs (
234
+ project_id,
235
+ parent_run_id,
236
+ status,
237
+ COALESCE(started_at, created_at) DESC,
238
+ id DESC
239
+ );
240
+
241
+ DROP TABLE legacy_subagent_threads;
@@ -0,0 +1,6 @@
1
+ -- A child that pauses on its question waits for a later turn of its parent's conversation to
2
+ -- answer it. Each turn reads which of the conversation's children wait so: few do, so a partial
3
+ -- index keeps that read small however many children the conversation had.
4
+ CREATE INDEX idx_agent_runs_paused_on_question
5
+ ON agent_runs (project_id, parent_run_id)
6
+ WHERE kind = 'subagent' AND finish_reason = 'awaiting_answer';
package/src/migrations.ts CHANGED
@@ -273,6 +273,44 @@ export const sqliteStorageMigrations = defineMigrations({
273
273
  sqliteSql("059-drained-source-versions", drainedSourceVersionsSql),
274
274
  sqliteSql("060-terminal-action-runs", terminalActionRunsSql),
275
275
  sqliteSql("061-drop-auth-audience", dropAuthAudienceSql),
276
+ sqliteSql("062-artifacts", artifactsSql),
277
+ sqliteSql("063-drop-output-diagnostics", dropOutputDiagnosticsSql),
278
+ sqliteSql("064-activity", activitySql),
279
+ sqliteSql("065-activity-timeline", activityTimelineSql),
280
+ sqliteSql("066-activity-interactions", activityInteractionsSql),
281
+ sqliteSql("067-artifact-kinds", artifactKindsSql),
282
+ sqliteSql("068-artifact-state", artifactStateSql),
283
+ sqliteSql("069-agent-thread-participants", agentThreadParticipantsSql),
284
+ sqliteSql("070-forked-context-checkpoints", forkedContextCheckpointsSql),
285
+ sqliteSql("071-unique-values", uniqueValuesSql),
286
+ sqliteSql("072-generated-media-usage", generatedMediaUsageSql),
287
+ sqliteSql("073-model-call-files", modelCallFilesSql),
288
+ sqliteSql("074-agent-memory", agentMemorySql),
289
+ sqliteSql("075-artifact-threads", artifactThreadsSql),
290
+ sqliteSql("076-agent-approvals", agentApprovalsSql),
291
+ sqliteSql("077-agent-approval-resume", agentApprovalResumeSql),
292
+ sqliteStep(
293
+ "078-subagent-threads",
294
+ (db) => {
295
+ // Bun runs a script past a statement that fails and reports only the last one's error, so
296
+ // a refused copy would go unnoticed and the rebuild would drop the rows: count around it.
297
+ const before = subagentThreadCounts(db)
298
+ db.run(subagentThreadsSql)
299
+ const after = subagentThreadCounts(db)
300
+ if (
301
+ after.runs !== before.runs ||
302
+ after.threads !== before.threads + before.threadless ||
303
+ after.messages !== before.messages + before.threadless ||
304
+ after.threadless !== 0
305
+ ) {
306
+ throw new Error(
307
+ "[SixbSqliteStorage] Agent runs, threads or messages were lost while giving sub-agents their threads."
308
+ )
309
+ }
310
+ },
311
+ { checksum: checksum(subagentThreadsSql) }
312
+ ),
313
+ sqliteSql("079-subagent-questions", subagentQuestionsSql),
276
314
  ],
277
315
  })
278
316
 
@@ -318,7 +356,7 @@ export function freshSqliteDatabase(): Database {
318
356
  return Database.deserialize(freshSchema)
319
357
  }
320
358
 
321
- export function installFreshSqliteSchema(db: Database): void {
359
+ function installFreshSqliteSchema(db: Database): void {
322
360
  for (const migration of sqliteStorageMigrations.steps) {
323
361
  const installed = migration.up(db)
324
362
  if (installed instanceof Promise) {
@@ -463,6 +501,26 @@ function sourceStorageCounts(db: Database): {
463
501
  .get() as { readonly versions: number; readonly roots: number; readonly rows: number }
464
502
  }
465
503
 
504
+ function subagentThreadCounts(db: Database): {
505
+ readonly runs: number
506
+ readonly threads: number
507
+ readonly messages: number
508
+ /** Child runs without a thread. */
509
+ readonly threadless: number
510
+ } {
511
+ return db
512
+ .query(`SELECT (SELECT count(*) FROM agent_runs) AS runs,
513
+ (SELECT count(*) FROM agent_threads) AS threads,
514
+ (SELECT count(*) FROM agent_messages) AS messages,
515
+ (SELECT count(*) FROM agent_runs WHERE kind = 'subagent' AND thread_id IS NULL) AS threadless`)
516
+ .get() as {
517
+ readonly runs: number
518
+ readonly threads: number
519
+ readonly messages: number
520
+ readonly threadless: number
521
+ }
522
+ }
523
+
466
524
  /**
467
525
  * Stored events that would not rebuild exactly from their commit and execution. Who asked and what
468
526
  * wrote are derived from the execution exactly as 047 derived the commit's copies; both sides were
@@ -621,3 +679,33 @@ import drainedSourceVersionsSql from "./migrations/059-drained-source-versions.s
621
679
  }
622
680
  import terminalActionRunsSql from "./migrations/060-terminal-action-runs.sql" with { type: "text" }
623
681
  import dropAuthAudienceSql from "./migrations/061-drop-auth-audience.sql" with { type: "text" }
682
+ import artifactsSql from "./migrations/062-artifacts.sql" with { type: "text" }
683
+ import dropOutputDiagnosticsSql from "./migrations/063-drop-output-diagnostics.sql" with {
684
+ type: "text",
685
+ }
686
+ import activitySql from "./migrations/064-activity.sql" with { type: "text" }
687
+ import activityTimelineSql from "./migrations/065-activity-timeline.sql" with { type: "text" }
688
+ import activityInteractionsSql from "./migrations/066-activity-interactions.sql" with {
689
+ type: "text",
690
+ }
691
+ import artifactKindsSql from "./migrations/067-artifact-kinds.sql" with { type: "text" }
692
+ import artifactStateSql from "./migrations/068-artifact-state.sql" with { type: "text" }
693
+ import agentThreadParticipantsSql from "./migrations/069-agent-thread-participants.sql" with {
694
+ type: "text",
695
+ }
696
+ import forkedContextCheckpointsSql from "./migrations/070-forked-context-checkpoints.sql" with {
697
+ type: "text",
698
+ }
699
+ import uniqueValuesSql from "./migrations/071-unique-values.sql" with { type: "text" }
700
+ import generatedMediaUsageSql from "./migrations/072-generated-media-usage.sql" with {
701
+ type: "text",
702
+ }
703
+ import modelCallFilesSql from "./migrations/073-model-call-files.sql" with { type: "text" }
704
+ import agentMemorySql from "./migrations/074-agent-memory.sql" with { type: "text" }
705
+ import artifactThreadsSql from "./migrations/075-artifact-threads.sql" with { type: "text" }
706
+ import agentApprovalsSql from "./migrations/076-agent-approvals.sql" with { type: "text" }
707
+ import agentApprovalResumeSql from "./migrations/077-agent-approval-resume.sql" with {
708
+ type: "text",
709
+ }
710
+ import subagentThreadsSql from "./migrations/078-subagent-threads.sql" with { type: "text" }
711
+ import subagentQuestionsSql from "./migrations/079-subagent-questions.sql" with { type: "text" }
@@ -4,6 +4,13 @@
4
4
  paths. The selected reader holds its traversal-budget probe and terminal statements on one WAL
5
5
  snapshot. SQL must filter by those relations before ranking or limiting results.
6
6
 
7
+ A conditional scope needs no traversal: `_sixb_scope_objects` keeps an object of a conditioned type
8
+ while its condition matches, and `_sixb_scope_links` a link while each conditioned endpoint is such an
9
+ object. Both are `NOT MATERIALIZED`, so statements keep their index seeks and `LIMIT`. A statement
10
+ that reads one type (`start`, `list` of one type, `getByPrimaryId`) reads the stored table with that
11
+ type's own condition instead: the disjunction over every conditioned type would keep unconditioned
12
+ types off covering indexes and the condition out of reach of an index.
13
+
7
14
  SQLite flattens CTEs. Expose identity columns from the grant/visible relation and put authorized
8
15
  parents before indexed lookups with `CROSS JOIN`; exposing stored identities can cause a full
9
16
  grant scan per traversed edge. The query-plan regression test checks those lookups.
@@ -0,0 +1,27 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ CompiledConditionalObjectReadScope,
4
+ ObjectQueryCapabilities,
5
+ ObjectReadStorage,
6
+ } from "@sixb/core/storage"
7
+ import { compileSqliteConditionalObjectReadSource } from "./read-scope"
8
+ import { createSqliteScopedReader } from "./scoped-reader"
9
+
10
+ /** Read objects and links through relations that apply the scope's conditions as they are read. */
11
+ export function createSqliteConditionalReader(
12
+ db: Database,
13
+ params: {
14
+ projectId: string
15
+ scope: CompiledConditionalObjectReadScope
16
+ },
17
+ queryCapabilities: () => ObjectQueryCapabilities
18
+ ): ObjectReadStorage {
19
+ return createSqliteScopedReader(
20
+ db,
21
+ {
22
+ projectId: params.projectId,
23
+ source: compileSqliteConditionalObjectReadSource(params.projectId, params.scope),
24
+ },
25
+ queryCapabilities
26
+ )
27
+ }
@@ -118,6 +118,12 @@ export interface SqliteObjectQuerySource {
118
118
  readonly objectsTable: string
119
119
  readonly linksTable: string
120
120
  readonly objectPropertyPermissionsTable?: string
121
+ /**
122
+ * Where to read one concrete type's objects, when the source can say it more precisely than
123
+ * `objectsTable`: a table and that type's own filter over its `properties`. A statement that
124
+ * reads a single type then holds the filter as a top-level condition, which an index can serve.
125
+ */
126
+ objectsOfType?(objectTypeId: string): SqliteTypedObjects
121
127
  /** Wrap a terminal SELECT without its own WITH clause; the selected source owns the only WITH. */
122
128
  wrapStatement(
123
129
  sql: string,
@@ -129,6 +135,26 @@ export interface SqliteObjectQuerySource {
129
135
  wrapQuery(query: CompiledObjectQuery): CompiledObjectQuery
130
136
  }
131
137
 
138
+ export interface SqliteTypedObjects {
139
+ readonly table: string
140
+ readonly filter?: { readonly sql: string; readonly args: readonly SqliteValue[] }
141
+ }
142
+
143
+ /** The table and `AND ...` filter a statement reads one type's objects with. */
144
+ export function sqliteObjectsOfType(
145
+ source: SqliteObjectQuerySource,
146
+ objectTypeId: string
147
+ ): { table: string; filter: string; args: readonly SqliteValue[] } {
148
+ const typed = source.objectsOfType?.(objectTypeId)
149
+ if (!typed) return { table: source.objectsTable, filter: "", args: [] }
150
+
151
+ return {
152
+ table: typed.table,
153
+ filter: typed.filter ? `AND (${typed.filter.sql})` : "",
154
+ args: typed.filter?.args ?? [],
155
+ }
156
+ }
157
+
132
158
  export const DEFAULT_OBJECT_QUERY_SOURCE: SqliteObjectQuerySource = {
133
159
  objectsTable: "objects",
134
160
  linksTable: "links",
@@ -298,19 +324,20 @@ function compileStart(
298
324
  }
299
325
 
300
326
  const order = compileOrder(identityOrderFields())
327
+ const objects = sqliteObjectsOfType(ctx.source, query.objectTypeId)
301
328
  const sql = `
302
329
  SELECT *, properties AS _cursor_properties
303
- FROM ${ctx.source.objectsTable}
304
- WHERE project_id = ? AND object_type_id = ?
330
+ FROM ${objects.table}
331
+ WHERE project_id = ? AND object_type_id = ? ${objects.filter}
305
332
  ORDER BY ${order.sql}
306
333
  `
307
- const args = [projectId, query.objectTypeId, ...order.args]
334
+ const args = [projectId, query.objectTypeId, ...objects.args, ...order.args]
308
335
 
309
336
  return {
310
337
  sql,
311
338
  args,
312
- totalSql: `SELECT COUNT(*) as total FROM ${ctx.source.objectsTable} WHERE project_id = ? AND object_type_id = ?`,
313
- totalArgs: [projectId, query.objectTypeId],
339
+ totalSql: `SELECT COUNT(*) as total FROM ${objects.table} WHERE project_id = ? AND object_type_id = ? ${objects.filter}`,
340
+ totalArgs: [projectId, query.objectTypeId, ...objects.args],
314
341
  order,
315
342
  hasMore: () => false,
316
343
  trimRows: identityRows,
@@ -974,7 +1001,8 @@ export function sqliteJsonEachValue(alias: string): string {
974
1001
  END`
975
1002
  }
976
1003
 
977
- function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1004
+ /** Compile a predicate over the unqualified `properties` column of the relation it filters. */
1005
+ export function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
978
1006
  if (
979
1007
  "scalarKind" in predicate &&
980
1008
  predicate.scalarKind === "decimal" &&
@@ -1004,6 +1032,8 @@ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
1004
1032
  const item = compilePredicate(predicate.item)
1005
1033
  return negatePredicate(item)
1006
1034
  }
1035
+ case "constant":
1036
+ return { sql: predicate.value ? "1" : "0", args: [] }
1007
1037
  case "eq":
1008
1038
  return predicate.scalarKind === "userRef"
1009
1039
  ? compileUserRefInPredicate(predicate.propertyId, [predicate.value])
@@ -1,6 +1,10 @@
1
- import type { CompiledSelectedObjectReadScope } from "@sixb/core/storage"
1
+ import type {
2
+ CompiledConditionalObjectReadScope,
3
+ CompiledSelectedObjectReadScope,
4
+ } from "@sixb/core/storage"
2
5
  import {
3
6
  type CompiledObjectQuery,
7
+ compilePredicate,
4
8
  type SqliteObjectQuerySource,
5
9
  type SqliteValue,
6
10
  sqliteJsonEachValue,
@@ -35,16 +39,75 @@ export function compileSqliteSelectedObjectReadSource(
35
39
  objects: scope.objects,
36
40
  steps: scope.steps,
37
41
  })
38
- const compiled = compileSelectedReadScopeCte(scopeJson, projectId)
42
+ const source = scopedQuerySource(compileSelectedReadScopeCte(scopeJson, projectId))
43
+
44
+ return Object.freeze({
45
+ ...source,
46
+ objectPropertyPermissionsTable: "_sixb_scope_object_permissions",
47
+ traversalProbe: source.wrapStatement(
48
+ `
49
+ SELECT COUNT(*) AS total
50
+ FROM (
51
+ SELECT 1 AS traversal_fact
52
+ FROM _sixb_scope_root_facts
53
+
54
+ UNION ALL
55
+
56
+ SELECT 1 AS traversal_fact
57
+ FROM _sixb_scope_reached_links
58
+
59
+ LIMIT ?
60
+ ) AS bounded_traversal_facts
61
+ `,
62
+ [BigInt(maxTraversalFacts) + 1n]
63
+ ),
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Prepare a conditional scope as SQLite relations that filter stored rows as they are read.
69
+ *
70
+ * Both relations are NOT MATERIALIZED, so SQLite flattens them into each statement: its filters,
71
+ * index seeks and LIMIT reach the stored tables, and the conditions become one more filter. An
72
+ * object of a conditioned type is visible while its condition matches; a link while each of its
73
+ * conditioned endpoints is a visible object.
74
+ */
75
+ export function compileSqliteConditionalObjectReadSource(
76
+ projectId: string,
77
+ scope: CompiledConditionalObjectReadScope
78
+ ): SqliteObjectQuerySource {
79
+ const filters = new Map(
80
+ scope.conditions.map((condition) => [
81
+ condition.objectTypeId,
82
+ compilePredicate(condition.predicate),
83
+ ])
84
+ )
85
+
86
+ return Object.freeze({
87
+ ...scopedQuerySource(compileConditionalReadScopeCte(projectId, filters)),
88
+ // One type's visible objects are the stored ones its own condition matches.
89
+ objectsOfType: (objectTypeId: string) => {
90
+ const filter = filters.get(objectTypeId)
91
+ return filter ? { table: "objects", filter } : { table: "objects" }
92
+ },
93
+ })
94
+ }
95
+
96
+ interface CompiledReadScopeCte {
97
+ readonly sql: string
98
+ readonly args: readonly SqliteValue[]
99
+ }
100
+
101
+ /** Prefix every statement of a query with the scope's relations, ahead of its own arguments. */
102
+ function scopedQuerySource(compiled: CompiledReadScopeCte): SqliteObjectQuerySource {
39
103
  const wrapStatement = (sql: string, args: readonly SqliteValue[] = []) => ({
40
104
  sql: `${compiled.sql}\n${sql}`,
41
105
  args: [...compiled.args, ...args],
42
106
  })
43
107
 
44
- return Object.freeze({
108
+ return {
45
109
  objectsTable: "_sixb_scope_objects",
46
110
  linksTable: "_sixb_scope_links",
47
- objectPropertyPermissionsTable: "_sixb_scope_object_permissions",
48
111
  wrapStatement,
49
112
  wrapQuery: (query: CompiledObjectQuery): CompiledObjectQuery => ({
50
113
  ...query,
@@ -63,29 +126,83 @@ export function compileSqliteSelectedObjectReadSource(
63
126
  }
64
127
  : {}),
65
128
  }),
66
- traversalProbe: wrapStatement(
67
- `
68
- SELECT COUNT(*) AS total
69
- FROM (
70
- SELECT 1 AS traversal_fact
71
- FROM _sixb_scope_root_facts
72
-
73
- UNION ALL
129
+ }
130
+ }
74
131
 
75
- SELECT 1 AS traversal_fact
76
- FROM _sixb_scope_reached_links
132
+ /** Conditions keyed by their concrete type, each compiled over that type's `properties`. */
133
+ type ConditionFilters = ReadonlyMap<
134
+ string,
135
+ { readonly sql: string; readonly args: readonly SqliteValue[] }
136
+ >
77
137
 
78
- LIMIT ?
79
- ) AS bounded_traversal_facts
138
+ function compileConditionalReadScopeCte(
139
+ projectId: string,
140
+ filters: ConditionFilters
141
+ ): CompiledReadScopeCte {
142
+ if (filters.size === 0) {
143
+ return {
144
+ sql: `
145
+ WITH
146
+ _sixb_scope_objects AS NOT MATERIALIZED (
147
+ SELECT * FROM objects WHERE project_id = ?
148
+ ),
149
+ _sixb_scope_links AS NOT MATERIALIZED (
150
+ SELECT * FROM links WHERE project_id = ?
151
+ )
80
152
  `,
81
- [BigInt(maxTraversalFacts) + 1n]
82
- ),
83
- })
84
- }
153
+ args: [projectId, projectId],
154
+ }
155
+ }
85
156
 
86
- interface CompiledReadScopeCte {
87
- readonly sql: string
88
- readonly args: readonly SqliteValue[]
157
+ const conditionedTypeArgs = [...filters.keys()]
158
+ const conditionedTypes = conditionedTypeArgs.map(() => "?").join(", ")
159
+
160
+ const matches: string[] = []
161
+ const matchArgs: SqliteValue[] = []
162
+ for (const [objectTypeId, filter] of filters) {
163
+ matches.push(`(object_type_id = ? AND ${filter.sql})`)
164
+ matchArgs.push(objectTypeId, ...filter.args)
165
+ }
166
+
167
+ const visibleEndpoint = (end: "source" | "target") => `
168
+ (
169
+ stored.${end}_type_id NOT IN (${conditionedTypes})
170
+ OR EXISTS (
171
+ SELECT 1
172
+ FROM _sixb_scope_objects AS endpoint
173
+ WHERE endpoint.project_id = stored.project_id
174
+ AND endpoint.object_type_id = stored.${end}_type_id
175
+ AND endpoint.primary_id = stored.${end}_id
176
+ )
177
+ )
178
+ `
179
+
180
+ return {
181
+ sql: `
182
+ WITH
183
+ _sixb_scope_objects AS NOT MATERIALIZED (
184
+ SELECT *
185
+ FROM objects
186
+ WHERE project_id = ?
187
+ AND (object_type_id NOT IN (${conditionedTypes}) OR ${matches.join(" OR ")})
188
+ ),
189
+ _sixb_scope_links AS NOT MATERIALIZED (
190
+ SELECT stored.*
191
+ FROM links AS stored
192
+ WHERE stored.project_id = ?
193
+ AND ${visibleEndpoint("source")}
194
+ AND ${visibleEndpoint("target")}
195
+ )
196
+ `,
197
+ args: [
198
+ projectId,
199
+ ...conditionedTypeArgs,
200
+ ...matchArgs,
201
+ projectId,
202
+ ...conditionedTypeArgs,
203
+ ...conditionedTypeArgs,
204
+ ],
205
+ }
89
206
  }
90
207
 
91
208
  function compileSelectedReadScopeCte(scopeJson: string, projectId: string): CompiledReadScopeCte {