@sixb/sqlite 0.1.21 → 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 (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
@@ -5,10 +5,12 @@ import type {
5
5
  ListAuthUsersInput,
6
6
  ListAuthUsersResult,
7
7
  UpdateAuthUserProfileInput,
8
+ UpdateAuthUserResult,
8
9
  UpdateAuthUserStatusInput,
9
10
  UserRecord,
10
11
  } from "@sixb/core/storage"
11
12
  import { AuthStorageError } from "@sixb/core/storage"
13
+ import { runImmediateTransaction } from "../transactions"
12
14
  import type { SqliteAuthUserRow } from "./rows"
13
15
  import { rowToUserRecord } from "./rows"
14
16
  import {
@@ -103,6 +105,22 @@ export class SqliteAuthUserStore implements AuthUserStore {
103
105
  return row ? rowToUserRecord(row) : null
104
106
  }
105
107
 
108
+ async getByIds(params: {
109
+ readonly projectId: string
110
+ readonly ids: readonly string[]
111
+ }): Promise<readonly UserRecord[]> {
112
+ const ids = [...new Set(params.ids)]
113
+ if (ids.length === 0) return []
114
+
115
+ const rows = this.db
116
+ .query(
117
+ `SELECT * FROM auth_users WHERE project_id = ? AND id IN (${ids.map(() => "?").join(", ")})`
118
+ )
119
+ .all(params.projectId, ...ids) as SqliteAuthUserRow[]
120
+
121
+ return rows.map(rowToUserRecord)
122
+ }
123
+
106
124
  async getByEmail(params: {
107
125
  readonly projectId: string
108
126
  readonly email: string
@@ -111,77 +129,87 @@ export class SqliteAuthUserStore implements AuthUserStore {
111
129
  return row ? rowToUserRecord(row) : null
112
130
  }
113
131
 
114
- async updateProfile(input: UpdateAuthUserProfileInput): Promise<UserRecord> {
115
- const existing = getUserRowById(this.db, {
116
- projectId: input.projectId,
117
- id: input.id,
118
- })
132
+ async updateProfile(input: UpdateAuthUserProfileInput): Promise<UpdateAuthUserResult> {
133
+ // One transaction, so the user it returns as previous is the one it changed.
134
+ return runImmediateTransaction(this.db, () => {
135
+ const existing = getUserRowById(this.db, {
136
+ projectId: input.projectId,
137
+ id: input.id,
138
+ })
119
139
 
120
- if (!existing) {
121
- throw new AuthStorageError(
122
- "missing_user",
123
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
124
- )
125
- }
140
+ if (!existing) {
141
+ throw new AuthStorageError(
142
+ "missing_user",
143
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
144
+ )
145
+ }
126
146
 
127
- const updatedAt = dateOrNow(input.updatedAt)
128
- this.db
129
- .query(
147
+ const previousUser = rowToUserRecord(existing)
148
+ if (!changesProfile(previousUser, input)) {
149
+ return { user: previousUser, previousUser }
150
+ }
151
+
152
+ const updatedAt = dateOrNow(input.updatedAt)
153
+ const row = this.db
154
+ .query(
155
+ `
156
+ UPDATE auth_users
157
+ SET display_name = CASE WHEN ? THEN ? ELSE display_name END,
158
+ avatar_url = CASE WHEN ? THEN ? ELSE avatar_url END,
159
+ updated_at = ?
160
+ WHERE project_id = ?
161
+ AND id = ?
162
+ RETURNING *
130
163
  `
131
- UPDATE auth_users
132
- SET display_name = ?,
133
- avatar_url = ?,
134
- updated_at = ?
135
- WHERE project_id = ?
136
- AND id = ?
137
- `
138
- )
139
- .run(
140
- input.displayName ?? null,
141
- input.avatarUrl ?? null,
142
- toIso(updatedAt),
143
- input.projectId,
144
- input.id
145
- )
164
+ )
165
+ .get(
166
+ input.displayName === undefined ? 0 : 1,
167
+ input.displayName ?? null,
168
+ input.avatarUrl === undefined ? 0 : 1,
169
+ input.avatarUrl ?? null,
170
+ toIso(updatedAt),
171
+ input.projectId,
172
+ input.id
173
+ ) as SqliteAuthUserRow
146
174
 
147
- return rowToUserRecord({
148
- ...existing,
149
- display_name: input.displayName ?? null,
150
- avatar_url: input.avatarUrl ?? null,
151
- updated_at: toIso(updatedAt),
175
+ return { user: rowToUserRecord(row), previousUser }
152
176
  })
153
177
  }
154
178
 
155
- async updateStatus(input: UpdateAuthUserStatusInput): Promise<UserRecord> {
156
- const existing = getUserRowById(this.db, {
157
- projectId: input.projectId,
158
- id: input.id,
159
- })
179
+ async updateStatus(input: UpdateAuthUserStatusInput): Promise<UpdateAuthUserResult> {
180
+ // One transaction, so two status changes cannot both report the same previous one.
181
+ return runImmediateTransaction(this.db, () => {
182
+ const existing = getUserRowById(this.db, {
183
+ projectId: input.projectId,
184
+ id: input.id,
185
+ })
160
186
 
161
- if (!existing) {
162
- throw new AuthStorageError(
163
- "missing_user",
164
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
165
- )
166
- }
187
+ if (!existing) {
188
+ throw new AuthStorageError(
189
+ "missing_user",
190
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
191
+ )
192
+ }
167
193
 
168
- const updatedAt = dateOrNow(input.updatedAt)
169
- this.db
170
- .query(
194
+ const updatedAt = dateOrNow(input.updatedAt)
195
+ this.db
196
+ .query(
197
+ `
198
+ UPDATE auth_users
199
+ SET status = ?,
200
+ updated_at = ?
201
+ WHERE project_id = ?
202
+ AND id = ?
171
203
  `
172
- UPDATE auth_users
173
- SET status = ?,
174
- updated_at = ?
175
- WHERE project_id = ?
176
- AND id = ?
177
- `
178
- )
179
- .run(input.status, toIso(updatedAt), input.projectId, input.id)
204
+ )
205
+ .run(input.status, toIso(updatedAt), input.projectId, input.id)
180
206
 
181
- return rowToUserRecord({
182
- ...existing,
183
- status: input.status,
184
- updated_at: toIso(updatedAt),
207
+ const user = rowToUserRecord({
208
+ ...existing,
209
+ status: input.status,
210
+ updated_at: toIso(updatedAt),
211
+ })
212
+ return { user, previousUser: rowToUserRecord(existing) }
185
213
  })
186
214
  }
187
215
 
@@ -198,6 +226,15 @@ export class SqliteAuthUserStore implements AuthUserStore {
198
226
  args.push(...input.statuses)
199
227
  }
200
228
 
229
+ // `instr` matches literally, so `%` and `_` need no escaping. Both sides fold through the same
230
+ // `lower()`, which ignores case for ASCII letters only: "émilie" does not find "Émilie" here.
231
+ if (input.search !== undefined) {
232
+ whereClauses.push(
233
+ "(instr(lower(coalesce(display_name, '')), lower(?)) > 0 OR instr(lower(email), lower(?)) > 0)"
234
+ )
235
+ args.push(input.search, input.search)
236
+ }
237
+
201
238
  const where = `WHERE ${whereClauses.join(" AND ")}`
202
239
  const order = input.order === "asc" ? "ASC" : "DESC"
203
240
  const totalRow = this.db
@@ -224,3 +261,12 @@ export class SqliteAuthUserStore implements AuthUserStore {
224
261
  }
225
262
  }
226
263
  }
264
+
265
+ // Omitted keeps the stored value, and `null` clears it.
266
+ function changesProfile(user: UserRecord, input: UpdateAuthUserProfileInput): boolean {
267
+ const displayName =
268
+ input.displayName === undefined ? user.displayName : (input.displayName ?? undefined)
269
+ const avatarUrl = input.avatarUrl === undefined ? user.avatarUrl : (input.avatarUrl ?? undefined)
270
+
271
+ return displayName !== user.displayName || avatarUrl !== user.avatarUrl
272
+ }
@@ -8,8 +8,10 @@ import {
8
8
  } from "@sixb/core/internal/execution-storage"
9
9
  import type {
10
10
  CreateExecutionInput,
11
+ ExecutionDescendant,
11
12
  ExecutionRecord,
12
13
  ExecutionStorage,
14
+ ListExecutionDescendantsInput,
13
15
  ListSessionExecutionsInput,
14
16
  SessionExecutions,
15
17
  ShareSessionStorage,
@@ -171,6 +173,43 @@ export class SqliteExecutionStorage implements ExecutionStorage {
171
173
  truncated: rows.length > input.limit,
172
174
  }
173
175
  }
176
+
177
+ async listDescendants(
178
+ input: ListExecutionDescendantsInput
179
+ ): Promise<readonly ExecutionDescendant[]> {
180
+ if (input.executionIds.length === 0) return []
181
+ const ids = [...new Set(input.executionIds)]
182
+
183
+ // UNION, not UNION ALL: an execution asked about may sit below another one asked about, and
184
+ // the walk would otherwise reach what it caused twice.
185
+ const rows = this.db
186
+ .query(
187
+ `
188
+ WITH RECURSIVE tree(id, parent_id) AS (
189
+ SELECT child.id, child.parent_execution_id
190
+ FROM executions child
191
+ WHERE child.project_id = ?
192
+ AND child.parent_execution_id IN (SELECT value FROM json_each(?))
193
+ UNION
194
+ SELECT child.id, child.parent_execution_id
195
+ FROM tree
196
+ CROSS JOIN executions child
197
+ ON child.project_id = ?
198
+ AND child.parent_execution_id = tree.id
199
+ )
200
+ SELECT id, parent_id
201
+ FROM tree
202
+ WHERE id NOT IN (SELECT value FROM json_each(?))
203
+ ORDER BY id
204
+ `
205
+ )
206
+ .all(input.projectId, JSON.stringify(ids), input.projectId, JSON.stringify(ids)) as {
207
+ id: string
208
+ parent_id: string
209
+ }[]
210
+
211
+ return rows.map((row) => ({ id: row.id, parentId: row.parent_id }))
212
+ }
174
213
  }
175
214
 
176
215
  /** An `executions` row as its record, for any store that reads executions alongside its own rows. */
@@ -0,0 +1,32 @@
1
+ -- Artifacts take two more kinds: pages, and the canvases that lay pages out. SQLite cannot alter a
2
+ -- CHECK constraint, so the table is rebuilt; its versions and frame grants reference it by name.
3
+ CREATE TABLE artifacts_v2 (
4
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
5
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
6
+ kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'page', 'canvas', 'file')),
7
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
8
+ owner_principal_type TEXT NOT NULL
9
+ CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
10
+ owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
11
+ latest_version INTEGER NOT NULL CHECK (latest_version > 0),
12
+ next_version INTEGER NOT NULL CHECK (next_version > latest_version),
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL,
15
+ PRIMARY KEY (project_id, id)
16
+ );
17
+
18
+ INSERT INTO artifacts_v2 (
19
+ project_id, id, kind, title, owner_principal_type, owner_principal_id, latest_version,
20
+ next_version, created_at, updated_at
21
+ )
22
+ SELECT
23
+ project_id, id, kind, title, owner_principal_type, owner_principal_id, latest_version,
24
+ next_version, created_at, updated_at
25
+ FROM artifacts;
26
+
27
+ DROP TABLE artifacts;
28
+ ALTER TABLE artifacts_v2 RENAME TO artifacts;
29
+
30
+ CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
31
+ CREATE INDEX idx_artifacts_owner_updated
32
+ ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
@@ -0,0 +1,15 @@
1
+ -- What a page keeps for each of its readers with `sixb.state`: JSON text by key, for one artifact
2
+ -- and one principal, across the artifact's versions. `size_bytes` is the value's UTF-8 length,
3
+ -- which the reader's limits sum. Deleting the artifact cascades here.
4
+ CREATE TABLE artifact_state (
5
+ project_id TEXT NOT NULL,
6
+ artifact_id TEXT NOT NULL,
7
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
8
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
9
+ key TEXT NOT NULL CHECK (length(key) > 0),
10
+ value TEXT NOT NULL CHECK (json_valid(value)),
11
+ size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
12
+ updated_at TEXT NOT NULL,
13
+ PRIMARY KEY (project_id, artifact_id, principal_type, principal_id, key),
14
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE
15
+ );
@@ -0,0 +1,16 @@
1
+ -- The users a thread's owner added to the conversation. Only users take part: only a signed-in
2
+ -- user talks to the Agent. Listing the threads shared with someone reads the principal index.
3
+ CREATE TABLE agent_thread_participants (
4
+ project_id TEXT NOT NULL,
5
+ thread_id TEXT NOT NULL,
6
+ principal_type TEXT NOT NULL CHECK (principal_type = 'user'),
7
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
8
+ added_at TEXT NOT NULL,
9
+ added_by_type TEXT NOT NULL CHECK (added_by_type IN ('user', 'serviceAccount', 'system')),
10
+ added_by_id TEXT NOT NULL CHECK (length(added_by_id) > 0),
11
+ PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
12
+ FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
13
+ );
14
+
15
+ CREATE INDEX idx_agent_thread_participants_principal
16
+ ON agent_thread_participants (project_id, principal_type, principal_id, thread_id);
@@ -0,0 +1,45 @@
1
+ -- A forked thread copies the checkpoint its history needs, and the copy keeps the run that wrote
2
+ -- the summary. A run's checkpoint is therefore unique within a thread, not within the project.
3
+ -- SQLite cannot drop a UNIQUE constraint, so the table is rebuilt.
4
+ CREATE TABLE agent_context_checkpoints_v2 (
5
+ project_id TEXT NOT NULL,
6
+ id TEXT NOT NULL,
7
+ thread_id TEXT NOT NULL,
8
+ created_by_run_id TEXT NOT NULL,
9
+ previous_checkpoint_id TEXT,
10
+ reason TEXT NOT NULL CHECK (reason IN ('threshold', 'overflow')),
11
+ summary TEXT NOT NULL CHECK (length(trim(summary)) > 0),
12
+ summary_format_version INTEGER NOT NULL CHECK (summary_format_version = 1),
13
+ summarized_through_seq INTEGER NOT NULL CHECK (summarized_through_seq >= 1),
14
+ observed_head_seq INTEGER NOT NULL,
15
+ estimated_input_tokens_before INTEGER NOT NULL CHECK (estimated_input_tokens_before >= 0),
16
+ estimated_input_tokens_after INTEGER NOT NULL CHECK (estimated_input_tokens_after >= 0),
17
+ summary_model_id TEXT NOT NULL CHECK (length(trim(summary_model_id)) > 0),
18
+ created_at TEXT NOT NULL,
19
+ PRIMARY KEY (project_id, id),
20
+ UNIQUE (project_id, thread_id, created_by_run_id),
21
+ FOREIGN KEY (project_id, thread_id)
22
+ REFERENCES agent_threads(project_id, id) ON DELETE CASCADE,
23
+ FOREIGN KEY (project_id, created_by_run_id)
24
+ REFERENCES agent_runs(project_id, id) ON DELETE RESTRICT,
25
+ FOREIGN KEY (project_id, previous_checkpoint_id)
26
+ REFERENCES agent_context_checkpoints(project_id, id) ON DELETE RESTRICT,
27
+ CHECK (summarized_through_seq < observed_head_seq)
28
+ );
29
+
30
+ INSERT INTO agent_context_checkpoints_v2 (
31
+ project_id, id, thread_id, created_by_run_id, previous_checkpoint_id, reason, summary,
32
+ summary_format_version, summarized_through_seq, observed_head_seq,
33
+ estimated_input_tokens_before, estimated_input_tokens_after, summary_model_id, created_at
34
+ )
35
+ SELECT
36
+ project_id, id, thread_id, created_by_run_id, previous_checkpoint_id, reason, summary,
37
+ summary_format_version, summarized_through_seq, observed_head_seq,
38
+ estimated_input_tokens_before, estimated_input_tokens_after, summary_model_id, created_at
39
+ FROM agent_context_checkpoints;
40
+
41
+ DROP TABLE agent_context_checkpoints;
42
+ ALTER TABLE agent_context_checkpoints_v2 RENAME TO agent_context_checkpoints;
43
+
44
+ CREATE INDEX idx_agent_context_checkpoints_thread_latest
45
+ ON agent_context_checkpoints(project_id, thread_id, summarized_through_seq DESC, created_at DESC, id DESC);
@@ -0,0 +1,22 @@
1
+ -- The values of unique properties, one row per value an object holds. Every object write keeps
2
+ -- them current, so a write that would give a value to a second object of the type is refused.
3
+ CREATE TABLE object_unique_values (
4
+ project_id TEXT NOT NULL,
5
+ object_type_id TEXT NOT NULL,
6
+ property_id TEXT NOT NULL,
7
+ value_key TEXT NOT NULL,
8
+ primary_id TEXT NOT NULL,
9
+ PRIMARY KEY (project_id, object_type_id, property_id, value_key)
10
+ );
11
+
12
+ -- The unique properties whose values were built from the objects already stored, and how their
13
+ -- keys were computed. A claim made with other keys is refused until they are built again.
14
+ CREATE TABLE object_unique_properties (
15
+ project_id TEXT NOT NULL,
16
+ object_type_id TEXT NOT NULL,
17
+ property_id TEXT NOT NULL,
18
+ key_kind TEXT NOT NULL,
19
+ key_version INTEGER NOT NULL,
20
+ prepared_at TEXT NOT NULL,
21
+ PRIMARY KEY (project_id, object_type_id, property_id)
22
+ );
@@ -0,0 +1,32 @@
1
+ ALTER TABLE ai_model_call_usage
2
+ ADD COLUMN image_output_tokens INTEGER
3
+ CHECK (image_output_tokens IS NULL OR image_output_tokens >= 0);
4
+
5
+ ALTER TABLE ai_model_call_usage
6
+ ADD COLUMN audio_output_tokens INTEGER
7
+ CHECK (audio_output_tokens IS NULL OR audio_output_tokens >= 0);
8
+
9
+ ALTER TABLE ai_model_call_usage
10
+ ADD COLUMN output_images INTEGER
11
+ CHECK (output_images IS NULL OR output_images >= 0);
12
+
13
+ ALTER TABLE ai_model_call_usage
14
+ ADD COLUMN input_characters INTEGER
15
+ CHECK (input_characters IS NULL OR input_characters >= 0);
16
+
17
+ -- SQLite cannot alter a CHECK constraint. Swapping the column avoids rebuilding a table that
18
+ -- usage groups, costs and limit reservations reference.
19
+ ALTER TABLE ai_model_call_usage
20
+ ADD COLUMN model_kind_next TEXT
21
+ CHECK (
22
+ model_kind_next IS NULL
23
+ OR model_kind_next IN (
24
+ 'language', 'image', 'video', 'embedding', 'decision', 'transcription', 'speech', 'reranking'
25
+ )
26
+ );
27
+
28
+ UPDATE ai_model_call_usage SET model_kind_next = model_kind;
29
+
30
+ ALTER TABLE ai_model_call_usage DROP COLUMN model_kind;
31
+
32
+ ALTER TABLE ai_model_call_usage RENAME COLUMN model_kind_next TO model_kind;
@@ -0,0 +1,11 @@
1
+ -- The files a model call produced. The execution that made the call decides who may read them.
2
+ CREATE TABLE ai_model_call_files (
3
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
4
+ call_id TEXT NOT NULL CHECK (length(trim(call_id)) > 0),
5
+ execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
6
+ files TEXT NOT NULL CHECK (json_type(files) = 'array' AND json_array_length(files) > 0),
7
+ recorded_at TEXT NOT NULL,
8
+ PRIMARY KEY (project_id, call_id),
9
+ FOREIGN KEY (project_id, execution_id)
10
+ REFERENCES executions(project_id, id)
11
+ );
@@ -0,0 +1,32 @@
1
+ -- Agent memory: each repo, 'user:<userId>' or 'project', is a log of revisions. A save inserts one
2
+ -- revision and the file versions it wrote, and never updates a row. A file's current contents are
3
+ -- its highest-revision version; NULL contents mean that revision deleted it. Timestamps are
4
+ -- ISO-8601 UTC text and compare lexicographically.
5
+ CREATE TABLE agent_memory_revisions (
6
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
7
+ repo_id TEXT NOT NULL CHECK (length(trim(repo_id)) > 0),
8
+ revision INTEGER NOT NULL CHECK (revision > 0),
9
+ message TEXT NOT NULL CHECK (length(trim(message)) > 0),
10
+ author_principal_type TEXT NOT NULL
11
+ CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
12
+ author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
13
+ source TEXT,
14
+ created_at TEXT NOT NULL,
15
+ PRIMARY KEY (project_id, repo_id, revision)
16
+ );
17
+
18
+ -- A save also removes versions older than 90 days that are not their path's latest, so a
19
+ -- revision's rows here can be fewer than the files it wrote.
20
+ CREATE TABLE agent_memory_files (
21
+ project_id TEXT NOT NULL,
22
+ repo_id TEXT NOT NULL,
23
+ path TEXT NOT NULL CHECK (length(path) > 0),
24
+ revision INTEGER NOT NULL,
25
+ contents TEXT,
26
+ PRIMARY KEY (project_id, repo_id, path, revision),
27
+ FOREIGN KEY (project_id, repo_id, revision)
28
+ REFERENCES agent_memory_revisions (project_id, repo_id, revision) ON DELETE CASCADE
29
+ );
30
+
31
+ CREATE INDEX idx_agent_memory_files_revision
32
+ ON agent_memory_files (project_id, repo_id, revision);
@@ -0,0 +1,21 @@
1
+ -- The threads an artifact is shared in: everyone who reads one of them reads the artifact. An
2
+ -- Agent turn publishing a version links its thread, for as long as a version from it remains; a
3
+ -- forked thread links its copies, `copied`, for as long as they last. Existing versions published
4
+ -- from a thread link it here.
5
+ CREATE TABLE artifact_threads (
6
+ project_id TEXT NOT NULL,
7
+ artifact_id TEXT NOT NULL,
8
+ thread_id TEXT NOT NULL CHECK (length(thread_id) > 0),
9
+ shared_at TEXT NOT NULL,
10
+ copied INTEGER NOT NULL DEFAULT 0 CHECK (copied IN (0, 1)),
11
+ PRIMARY KEY (project_id, artifact_id, thread_id),
12
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE
13
+ );
14
+
15
+ CREATE INDEX idx_artifact_threads_thread ON artifact_threads (project_id, thread_id, artifact_id);
16
+
17
+ INSERT INTO artifact_threads (project_id, artifact_id, thread_id, shared_at)
18
+ SELECT project_id, artifact_id, origin_thread_id, MIN(created_at)
19
+ FROM artifact_versions
20
+ WHERE origin_thread_id IS NOT NULL
21
+ GROUP BY project_id, artifact_id, origin_thread_id;
@@ -0,0 +1,59 @@
1
+ -- Calls of the Agent that wait for whoever asked for the turn. A call's run id is its key: a
2
+ -- thread holds at most one pending, approved or denied approval per key, and a cancelled,
3
+ -- superseded or expired one lets the same call ask again. Timestamps are ISO-8601 UTC text and
4
+ -- compare lexicographically.
5
+ CREATE TABLE agent_approvals (
6
+ project_id TEXT NOT NULL,
7
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
8
+ thread_id TEXT NOT NULL,
9
+ run_id TEXT NOT NULL CHECK (length(run_id) > 0),
10
+ caller_run_id TEXT NOT NULL CHECK (length(caller_run_id) > 0),
11
+ requested_by_type TEXT NOT NULL CHECK (requested_by_type IN ('user', 'serviceAccount', 'system')),
12
+ requested_by_id TEXT NOT NULL CHECK (length(requested_by_id) > 0),
13
+ mode TEXT NOT NULL CHECK (mode IN ('manual', 'accept-edits')),
14
+ call_key TEXT NOT NULL CHECK (length(call_key) > 0),
15
+ call TEXT NOT NULL,
16
+ status TEXT NOT NULL
17
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'superseded', 'expired')),
18
+ created_at TEXT NOT NULL,
19
+ expires_at TEXT NOT NULL,
20
+ decided_at TEXT,
21
+ decided_by_type TEXT CHECK (decided_by_type IN ('user', 'serviceAccount', 'system')),
22
+ decided_by_id TEXT,
23
+ outcome TEXT,
24
+ reported_in_message_id TEXT,
25
+ PRIMARY KEY (project_id, id),
26
+ FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
27
+ );
28
+
29
+ CREATE UNIQUE INDEX idx_agent_approvals_live_call
30
+ ON agent_approvals (project_id, thread_id, call_key)
31
+ WHERE status IN ('pending', 'approved', 'denied');
32
+
33
+ CREATE INDEX idx_agent_approvals_thread ON agent_approvals (project_id, thread_id, created_at);
34
+ CREATE INDEX idx_agent_approvals_run ON agent_approvals (project_id, run_id, status);
35
+ CREATE INDEX idx_agent_approvals_caller_run ON agent_approvals (project_id, caller_run_id, status);
36
+ CREATE INDEX idx_agent_approvals_requester
37
+ ON agent_approvals (project_id, requested_by_type, requested_by_id, status);
38
+
39
+ -- What each user lets the Agent change without asking: a default, then their own choice in a
40
+ -- thread. Absent rows fall back, in that order.
41
+ CREATE TABLE agent_user_settings (
42
+ project_id TEXT NOT NULL,
43
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
44
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
45
+ approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
46
+ updated_at TEXT NOT NULL,
47
+ PRIMARY KEY (project_id, principal_type, principal_id)
48
+ );
49
+
50
+ CREATE TABLE agent_thread_user_settings (
51
+ project_id TEXT NOT NULL,
52
+ thread_id TEXT NOT NULL,
53
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
54
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
55
+ approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
56
+ updated_at TEXT NOT NULL,
57
+ PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
58
+ FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
59
+ );
@@ -0,0 +1,20 @@
1
+ -- A paused Agent turn resumes once its requester decided on every call it held. What that needs:
2
+ -- the mode "Always allow" raised the turn to, whether the turn may still resume on a call, and the
3
+ -- claim a process takes before it runs an approved call left without its outcome.
4
+ ALTER TABLE agent_approvals
5
+ ADD COLUMN raised_mode TEXT CHECK (raised_mode IN ('manual', 'accept-edits', 'auto'));
6
+ ALTER TABLE agent_approvals ADD COLUMN turn_stopped_at TEXT;
7
+ ALTER TABLE agent_approvals ADD COLUMN execution_claimed_at TEXT;
8
+
9
+ -- The repair pass: calls past their time, approved calls without an outcome, and decided calls of
10
+ -- turns that may still resume. Each index covers only the rows that are still work.
11
+ CREATE INDEX idx_agent_approvals_due
12
+ ON agent_approvals (project_id, expires_at)
13
+ WHERE status = 'pending';
14
+ CREATE INDEX idx_agent_approvals_unfinished
15
+ ON agent_approvals (project_id, created_at, id)
16
+ WHERE status = 'approved' AND outcome IS NULL;
17
+ CREATE INDEX idx_agent_approvals_resumable
18
+ ON agent_approvals (project_id, created_at, id)
19
+ WHERE status IN ('approved', 'denied') AND reported_in_message_id IS NULL
20
+ AND turn_stopped_at IS NULL;