@sixb/pg 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 (139) hide show
  1. package/README.md +1 -0
  2. package/dist/agents/agent-storage.d.ts +8 -2
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/agents/approvals.d.ts +48 -0
  5. package/dist/agents/approvals.d.ts.map +1 -0
  6. package/dist/agents/checkpoints.d.ts +1 -0
  7. package/dist/agents/checkpoints.d.ts.map +1 -1
  8. package/dist/agents/index.d.ts +1 -0
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/memory.d.ts +19 -0
  11. package/dist/agents/memory.d.ts.map +1 -0
  12. package/dist/agents/messages.d.ts.map +1 -1
  13. package/dist/agents/rows.d.ts +15 -1
  14. package/dist/agents/rows.d.ts.map +1 -1
  15. package/dist/agents/runs.d.ts +3 -1
  16. package/dist/agents/runs.d.ts.map +1 -1
  17. package/dist/agents/settings.d.ts +29 -0
  18. package/dist/agents/settings.d.ts.map +1 -0
  19. package/dist/agents/threads.d.ts +10 -1
  20. package/dist/agents/threads.d.ts.map +1 -1
  21. package/dist/ai-cost-groups.d.ts.map +1 -1
  22. package/dist/auth-storage/auth-storage.d.ts +3 -2
  23. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  24. package/dist/auth-storage/sessions.d.ts +2 -1
  25. package/dist/auth-storage/sessions.d.ts.map +1 -1
  26. package/dist/auth-storage/users.d.ts +8 -4
  27. package/dist/auth-storage/users.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6001 -2923
  31. package/dist/index.js.map +45 -37
  32. package/dist/migrations.d.ts.map +1 -1
  33. package/dist/objects/conditional-reader.d.ts +17 -0
  34. package/dist/objects/conditional-reader.d.ts.map +1 -0
  35. package/dist/objects/query-compiler.d.ts +27 -1
  36. package/dist/objects/query-compiler.d.ts.map +1 -1
  37. package/dist/objects/query-indexes.d.ts.map +1 -1
  38. package/dist/objects/read-scope.d.ts +10 -1
  39. package/dist/objects/read-scope.d.ts.map +1 -1
  40. package/dist/objects/reader.d.ts +2 -0
  41. package/dist/objects/reader.d.ts.map +1 -1
  42. package/dist/objects/scoped-reader.d.ts +12 -0
  43. package/dist/objects/scoped-reader.d.ts.map +1 -0
  44. package/dist/objects/selected-reader.d.ts +1 -0
  45. package/dist/objects/selected-reader.d.ts.map +1 -1
  46. package/dist/objects/storage.d.ts +7 -1
  47. package/dist/objects/storage.d.ts.map +1 -1
  48. package/dist/ontology-storage/index.d.ts +2 -0
  49. package/dist/ontology-storage/index.d.ts.map +1 -1
  50. package/dist/ontology-storage/materialization-session.d.ts +6 -0
  51. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  52. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  53. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  54. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  55. package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
  56. package/dist/ontology-storage/sources.d.ts +2 -1
  57. package/dist/ontology-storage/sources.d.ts.map +1 -1
  58. package/dist/ontology-storage/unique-values.d.ts +60 -0
  59. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  60. package/dist/pg-action-run-storage.d.ts +4 -0
  61. package/dist/pg-action-run-storage.d.ts.map +1 -1
  62. package/dist/pg-activity-storage.d.ts +22 -0
  63. package/dist/pg-activity-storage.d.ts.map +1 -0
  64. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  65. package/dist/pg-ai-usage-storage.d.ts +8 -1
  66. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  67. package/dist/pg-artifact-storage.d.ts +26 -0
  68. package/dist/pg-artifact-storage.d.ts.map +1 -0
  69. package/dist/pg-execution-storage.d.ts +3 -1
  70. package/dist/pg-execution-storage.d.ts.map +1 -1
  71. package/dist/pg-workflow-run-storage.d.ts +4 -0
  72. package/dist/pg-workflow-run-storage.d.ts.map +1 -1
  73. package/dist/share-grant-storage.d.ts +2 -1
  74. package/dist/share-grant-storage.d.ts.map +1 -1
  75. package/dist/storage-errors.d.ts +2 -0
  76. package/dist/storage-errors.d.ts.map +1 -1
  77. package/package.json +3 -3
  78. package/src/agents/agent-storage.ts +11 -2
  79. package/src/agents/approvals.ts +400 -0
  80. package/src/agents/checkpoints.ts +8 -4
  81. package/src/agents/index.ts +1 -0
  82. package/src/agents/memory.ts +175 -0
  83. package/src/agents/messages.ts +14 -7
  84. package/src/agents/rows.ts +46 -1
  85. package/src/agents/runs.ts +154 -22
  86. package/src/agents/settings.ts +105 -0
  87. package/src/agents/threads.ts +379 -3
  88. package/src/ai-cost-groups.ts +11 -1
  89. package/src/auth-storage/auth-storage.ts +45 -13
  90. package/src/auth-storage/sessions.ts +20 -0
  91. package/src/auth-storage/users.ts +105 -43
  92. package/src/index.ts +10 -0
  93. package/src/migrations/063-artifacts.sql +67 -0
  94. package/src/migrations/064-drop-output-diagnostics.sql +14 -0
  95. package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
  96. package/src/migrations/066-activity.sql +61 -0
  97. package/src/migrations/067-activity-timeline.sql +15 -0
  98. package/src/migrations/068-activity-interactions.sql +20 -0
  99. package/src/migrations/069-artifact-kinds.sql +4 -0
  100. package/src/migrations/070-artifact-state.sql +16 -0
  101. package/src/migrations/071-agent-thread-participants.sql +16 -0
  102. package/src/migrations/072-forked-context-checkpoints.sql +23 -0
  103. package/src/migrations/073-unique-values.sql +25 -0
  104. package/src/migrations/074-generated-media-usage.sql +21 -0
  105. package/src/migrations/075-model-call-files.sql +11 -0
  106. package/src/migrations/076-agent-memory.sql +33 -0
  107. package/src/migrations/077-artifact-threads.sql +21 -0
  108. package/src/migrations/078-agent-approvals.sql +58 -0
  109. package/src/migrations/079-agent-approval-resume.sql +20 -0
  110. package/src/migrations/080-subagent-threads.sql +145 -0
  111. package/src/migrations/081-subagent-questions.sql +6 -0
  112. package/src/migrations.ts +52 -0
  113. package/src/objects/conditional-reader.ts +44 -0
  114. package/src/objects/prepare-queries.ts +1 -1
  115. package/src/objects/query-compiler.ts +96 -33
  116. package/src/objects/query-indexes.ts +29 -10
  117. package/src/objects/read-scope.ts +140 -25
  118. package/src/objects/reader.ts +50 -6
  119. package/src/objects/scoped-reader.ts +101 -0
  120. package/src/objects/selected-reader.ts +16 -76
  121. package/src/objects/storage.ts +24 -5
  122. package/src/objects/text-count-index.ts +1 -0
  123. package/src/ontology-storage/index.ts +3 -0
  124. package/src/ontology-storage/materialization-session.ts +8 -0
  125. package/src/ontology-storage/materialization-writer.ts +76 -1
  126. package/src/ontology-storage/materializations.ts +20 -1
  127. package/src/ontology-storage/replacement-plans.ts +25 -7
  128. package/src/ontology-storage/sources.ts +37 -0
  129. package/src/ontology-storage/unique-values.ts +274 -0
  130. package/src/pg-action-run-storage.ts +23 -0
  131. package/src/pg-activity-storage.ts +486 -0
  132. package/src/pg-ai-cost-storage.ts +62 -12
  133. package/src/pg-ai-usage-storage.ts +90 -1
  134. package/src/pg-artifact-storage.ts +521 -0
  135. package/src/pg-execution-storage.ts +83 -0
  136. package/src/pg-workflow-run-storage.ts +23 -0
  137. package/src/share-grant-storage.ts +21 -0
  138. package/src/storage-errors.ts +7 -0
  139. package/src/transactions.ts +1 -1
@@ -4,11 +4,12 @@ import type {
4
4
  ListAuthUsersInput,
5
5
  ListAuthUsersResult,
6
6
  UpdateAuthUserProfileInput,
7
+ UpdateAuthUserResult,
7
8
  UpdateAuthUserStatusInput,
8
9
  UserRecord,
9
10
  } from "@sixb/core/storage"
10
11
  import { AuthStorageError } from "@sixb/core/storage"
11
- import type { PgStoreClient } from "../transactions"
12
+ import { type PgStoreClient, runPgTransaction } from "../transactions"
12
13
  import type { PgAuthUserRow } from "./rows"
13
14
  import { rowToUserRecord } from "./rows"
14
15
  import {
@@ -90,6 +91,23 @@ export class PgAuthUserStore implements AuthUserStore {
90
91
  return row ? rowToUserRecord(row) : null
91
92
  }
92
93
 
94
+ async getByIds(params: {
95
+ readonly projectId: string
96
+ readonly ids: readonly string[]
97
+ }): Promise<readonly UserRecord[]> {
98
+ const ids = [...new Set(params.ids)]
99
+ if (ids.length === 0) return []
100
+
101
+ const rows = await this.sql<PgAuthUserRow[]>`
102
+ SELECT *
103
+ FROM auth_users
104
+ WHERE project_id = ${params.projectId}
105
+ AND id IN ${this.sql(ids)}
106
+ `
107
+
108
+ return rows.map(rowToUserRecord)
109
+ }
110
+
93
111
  async getByEmail(params: {
94
112
  readonly projectId: string
95
113
  readonly email: string
@@ -98,57 +116,83 @@ export class PgAuthUserStore implements AuthUserStore {
98
116
  return row ? rowToUserRecord(row) : null
99
117
  }
100
118
 
101
- async updateProfile(input: UpdateAuthUserProfileInput): Promise<UserRecord> {
102
- const existing = await getUserRowById(this.sql, {
103
- projectId: input.projectId,
104
- id: input.id,
105
- })
106
-
107
- if (!existing) {
108
- throw new AuthStorageError(
109
- "missing_user",
110
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
119
+ async updateProfile(input: UpdateAuthUserProfileInput): Promise<UpdateAuthUserResult> {
120
+ // The row stays locked until the update, so the user it returns as previous is the one it
121
+ // changed, and an omitted field keeps whatever a concurrent update wrote to it.
122
+ return runPgTransaction(this.sql, async (tx) => {
123
+ const existing = await getUserRowById(
124
+ tx,
125
+ {
126
+ projectId: input.projectId,
127
+ id: input.id,
128
+ },
129
+ { forUpdate: true }
111
130
  )
112
- }
113
131
 
114
- const updatedAt = dateOrNow(input.updatedAt)
115
- const [row] = await this.sql<PgAuthUserRow[]>`
116
- UPDATE auth_users
117
- SET display_name = ${input.displayName ?? null},
118
- avatar_url = ${input.avatarUrl ?? null},
119
- updated_at = ${updatedAt}
120
- WHERE project_id = ${input.projectId}
121
- AND id = ${input.id}
122
- RETURNING *
123
- `
132
+ if (!existing) {
133
+ throw new AuthStorageError(
134
+ "missing_user",
135
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
136
+ )
137
+ }
124
138
 
125
- return rowToUserRecord(row)
126
- }
139
+ const previousUser = rowToUserRecord(existing)
140
+ if (!changesProfile(previousUser, input)) {
141
+ return { user: previousUser, previousUser }
142
+ }
143
+
144
+ const updatedAt = dateOrNow(input.updatedAt)
145
+ const [row] = await tx<PgAuthUserRow[]>`
146
+ UPDATE auth_users
147
+ SET display_name = CASE
148
+ WHEN ${input.displayName !== undefined}::boolean THEN ${input.displayName ?? null}::text
149
+ ELSE display_name
150
+ END,
151
+ avatar_url = CASE
152
+ WHEN ${input.avatarUrl !== undefined}::boolean THEN ${input.avatarUrl ?? null}::text
153
+ ELSE avatar_url
154
+ END,
155
+ updated_at = ${updatedAt}
156
+ WHERE project_id = ${input.projectId}
157
+ AND id = ${input.id}
158
+ RETURNING *
159
+ `
127
160
 
128
- async updateStatus(input: UpdateAuthUserStatusInput): Promise<UserRecord> {
129
- const existing = await getUserRowById(this.sql, {
130
- projectId: input.projectId,
131
- id: input.id,
161
+ return { user: rowToUserRecord(row), previousUser }
132
162
  })
163
+ }
133
164
 
134
- if (!existing) {
135
- throw new AuthStorageError(
136
- "missing_user",
137
- `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
165
+ async updateStatus(input: UpdateAuthUserStatusInput): Promise<UpdateAuthUserResult> {
166
+ // Locked like a profile update, so two status changes cannot both report the same previous one.
167
+ return runPgTransaction(this.sql, async (tx) => {
168
+ const existing = await getUserRowById(
169
+ tx,
170
+ {
171
+ projectId: input.projectId,
172
+ id: input.id,
173
+ },
174
+ { forUpdate: true }
138
175
  )
139
- }
140
176
 
141
- const updatedAt = dateOrNow(input.updatedAt)
142
- const [row] = await this.sql<PgAuthUserRow[]>`
143
- UPDATE auth_users
144
- SET status = ${input.status},
145
- updated_at = ${updatedAt}
146
- WHERE project_id = ${input.projectId}
147
- AND id = ${input.id}
148
- RETURNING *
149
- `
177
+ if (!existing) {
178
+ throw new AuthStorageError(
179
+ "missing_user",
180
+ `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`
181
+ )
182
+ }
183
+
184
+ const updatedAt = dateOrNow(input.updatedAt)
185
+ const [row] = await tx<PgAuthUserRow[]>`
186
+ UPDATE auth_users
187
+ SET status = ${input.status},
188
+ updated_at = ${updatedAt}
189
+ WHERE project_id = ${input.projectId}
190
+ AND id = ${input.id}
191
+ RETURNING *
192
+ `
150
193
 
151
- return rowToUserRecord(row)
194
+ return { user: rowToUserRecord(row), previousUser: rowToUserRecord(existing) }
195
+ })
152
196
  }
153
197
 
154
198
  async list(input: ListAuthUsersInput): Promise<ListAuthUsersResult> {
@@ -165,6 +209,15 @@ export class PgAuthUserStore implements AuthUserStore {
165
209
  params.push(...input.statuses)
166
210
  }
167
211
 
212
+ // `strpos` matches literally, so `%` and `_` need no escaping. Both sides fold through `lower()`.
213
+ if (input.search !== undefined) {
214
+ const index = nextIndex++
215
+ whereClauses.push(
216
+ `(strpos(lower(coalesce(display_name, '')), lower($${index})) > 0 OR strpos(lower(email), lower($${index})) > 0)`
217
+ )
218
+ params.push(input.search)
219
+ }
220
+
168
221
  const where = `WHERE ${whereClauses.join(" AND ")}`
169
222
  const order = input.order === "asc" ? "ASC" : "DESC"
170
223
  const [totalRow] = await this.sql.unsafe<{ readonly count: string | number }[]>(
@@ -194,3 +247,12 @@ export class PgAuthUserStore implements AuthUserStore {
194
247
  }
195
248
  }
196
249
  }
250
+
251
+ // Omitted keeps the stored value, and `null` clears it.
252
+ function changesProfile(user: UserRecord, input: UpdateAuthUserProfileInput): boolean {
253
+ const displayName =
254
+ input.displayName === undefined ? user.displayName : (input.displayName ?? undefined)
255
+ const avatarUrl = input.avatarUrl === undefined ? user.avatarUrl : (input.avatarUrl ?? undefined)
256
+
257
+ return displayName !== user.displayName || avatarUrl !== user.avatarUrl
258
+ }
package/src/index.ts CHANGED
@@ -49,9 +49,11 @@ export type { PgObjectTextCountIndex } from "./objects/text-count-index"
49
49
 
50
50
  import { PgOntologyStorage, type PgOntologyTransactionContext } from "./ontology-storage"
51
51
  import { PgActionRunStorage } from "./pg-action-run-storage"
52
+ import { PgActivityStorage } from "./pg-activity-storage"
52
53
  import { PgAiCostStorage } from "./pg-ai-cost-storage"
53
54
  import { PgAiLimitStorage } from "./pg-ai-limit-storage"
54
55
  import { PgAiUsageStorage } from "./pg-ai-usage-storage"
56
+ import { PgArtifactStorage } from "./pg-artifact-storage"
55
57
  import { createPgClient, type SQL, type SQLClient } from "./pg-client"
56
58
  import { PgExecutionStorage } from "./pg-execution-storage"
57
59
  import { PgFileDownloadGrantStorage } from "./pg-file-download-grant-storage"
@@ -166,6 +168,7 @@ export class PostgresStorage implements MigrationCapableStorage, QueryPreparatio
166
168
  readonly auth: PgAuthStorage
167
169
  readonly executions: PgExecutionStorage
168
170
  readonly agents: PgAgentStorage
171
+ readonly artifacts: PgArtifactStorage
169
172
  readonly aiUsage: AiUsageStorage
170
173
  readonly aiCosts: AiCostStorage
171
174
  readonly aiLimits: AiLimitStorage
@@ -183,6 +186,7 @@ export class PostgresStorage implements MigrationCapableStorage, QueryPreparatio
183
186
  readonly shareSessions: PgShareSessionStorage
184
187
  readonly fileUploadSessions: PgFileUploadSessionStorage
185
188
  readonly fileDownloadGrants: PgFileDownloadGrantStorage
189
+ readonly activity: PgActivityStorage
186
190
  readonly migrators: readonly StorageMigrator[]
187
191
 
188
192
  private readonly sql: SQL
@@ -252,6 +256,7 @@ export class PostgresStorage implements MigrationCapableStorage, QueryPreparatio
252
256
  this.auth = createAuthOperationScope(stores.auth, scope)
253
257
  this.executions = createOperationScopedFacade(stores.executions, scope)
254
258
  this.agents = createAgentOperationScope(stores.agents, scope)
259
+ this.artifacts = createOperationScopedFacade(stores.artifacts, scope)
255
260
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
256
261
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
257
262
  this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
@@ -269,6 +274,7 @@ export class PostgresStorage implements MigrationCapableStorage, QueryPreparatio
269
274
  this.shareSessions = createOperationScopedFacade(stores.shareSessions, scope)
270
275
  this.fileUploadSessions = createOperationScopedFacade(stores.fileUploadSessions, scope)
271
276
  this.fileDownloadGrants = createOperationScopedFacade(stores.fileDownloadGrants, scope)
277
+ this.activity = createOperationScopedFacade(stores.activity, scope)
272
278
  registerPostgresStorageTestingAdapter(this, (durationMs) =>
273
279
  stores.connectorConnections.advanceTimeForTesting(durationMs)
274
280
  )
@@ -414,6 +420,7 @@ function createPostgresStores(
414
420
  auth,
415
421
  executions,
416
422
  agents: new PgAgentStorage({ sql, executions }),
423
+ artifacts: new PgArtifactStorage(sql),
417
424
  aiUsage: new PgAiUsageStorage(sql),
418
425
  aiCosts: new PgAiCostStorage(sql),
419
426
  aiLimits: new PgAiLimitStorage(sql),
@@ -431,6 +438,7 @@ function createPostgresStores(
431
438
  shareSessions,
432
439
  fileUploadSessions: new PgFileUploadSessionStorage(sql),
433
440
  fileDownloadGrants: new PgFileDownloadGrantStorage(sql),
441
+ activity: new PgActivityStorage(sql),
434
442
  }
435
443
  }
436
444
 
@@ -440,6 +448,7 @@ interface PostgresStoreSet {
440
448
  readonly auth: PgAuthStorage
441
449
  readonly executions: PgExecutionStorage
442
450
  readonly agents: PgAgentStorage
451
+ readonly artifacts: PgArtifactStorage
443
452
  readonly aiUsage: PgAiUsageStorage
444
453
  readonly aiCosts: PgAiCostStorage
445
454
  readonly aiLimits: PgAiLimitStorage
@@ -457,6 +466,7 @@ interface PostgresStoreSet {
457
466
  readonly shareSessions: PgShareSessionStorage
458
467
  readonly fileUploadSessions: PgFileUploadSessionStorage
459
468
  readonly fileDownloadGrants: PgFileDownloadGrantStorage
469
+ readonly activity: PgActivityStorage
460
470
  }
461
471
 
462
472
  function resolveTimeoutMillis(value: number | undefined, label: string): number | undefined {
@@ -0,0 +1,67 @@
1
+ -- Artifacts: versioned documents, sheets, decks, and files. An artifact's kind never
2
+ -- changes. Versions take `next_version`, which only grows, so a number is never reused after its
3
+ -- version is deleted.
4
+ CREATE TABLE artifacts (
5
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
6
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
7
+ kind TEXT NOT NULL CHECK (kind IN ('document', 'sheet', 'slides', 'file')),
8
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
9
+ owner_principal_type TEXT NOT NULL
10
+ CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
11
+ owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
12
+ latest_version INTEGER NOT NULL CHECK (latest_version > 0),
13
+ next_version INTEGER NOT NULL CHECK (next_version > latest_version),
14
+ created_at TIMESTAMPTZ NOT NULL,
15
+ updated_at TIMESTAMPTZ NOT NULL,
16
+ PRIMARY KEY (project_id, id)
17
+ );
18
+
19
+ CREATE INDEX idx_artifacts_project_updated ON artifacts (project_id, updated_at DESC, id);
20
+ CREATE INDEX idx_artifacts_owner_updated
21
+ ON artifacts (project_id, owner_principal_type, owner_principal_id, updated_at DESC, id);
22
+
23
+ -- `refs`, not `references`: that word is reserved in SQL.
24
+ CREATE TABLE artifact_versions (
25
+ project_id TEXT NOT NULL,
26
+ artifact_id TEXT NOT NULL,
27
+ version INTEGER NOT NULL CHECK (version > 0),
28
+ title TEXT NOT NULL CHECK (length(trim(title)) > 0),
29
+ entry TEXT NOT NULL CHECK (length(entry) > 0),
30
+ files JSONB NOT NULL CHECK (jsonb_typeof(files) = 'array'),
31
+ outline JSONB NOT NULL CHECK (jsonb_typeof(outline) = 'object'),
32
+ refs JSONB NOT NULL CHECK (jsonb_typeof(refs) = 'array'),
33
+ author_principal_type TEXT NOT NULL
34
+ CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
35
+ author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
36
+ origin_thread_id TEXT,
37
+ origin_run_id TEXT,
38
+ created_at TIMESTAMPTZ NOT NULL,
39
+ PRIMARY KEY (project_id, artifact_id, version),
40
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
41
+ CHECK ((origin_thread_id IS NULL) = (origin_run_id IS NULL))
42
+ );
43
+
44
+ CREATE INDEX idx_artifact_versions_origin_run
45
+ ON artifact_versions (project_id, origin_run_id) WHERE origin_run_id IS NOT NULL;
46
+ CREATE INDEX idx_artifact_versions_origin_thread
47
+ ON artifact_versions (project_id, origin_thread_id, artifact_id) WHERE origin_thread_id IS NOT NULL;
48
+
49
+ -- Short-lived links that a sandboxed frame loads a deck's files through. Only the SHA-256 of each
50
+ -- link's token is stored. `targets` is what the frame may show from other artifacts, decided for
51
+ -- its reader when the link was issued: a JSON array of { address, artifactId, version }.
52
+ CREATE TABLE artifact_frame_grants (
53
+ project_id TEXT NOT NULL,
54
+ token_hash TEXT NOT NULL CHECK (token_hash ~ '^[0-9a-f]{64}$'),
55
+ artifact_id TEXT NOT NULL,
56
+ version INTEGER NOT NULL CHECK (version > 0),
57
+ targets JSONB NOT NULL CHECK (jsonb_typeof(targets) = 'array'),
58
+ created_at TIMESTAMPTZ NOT NULL,
59
+ expires_at TIMESTAMPTZ NOT NULL,
60
+ PRIMARY KEY (project_id, token_hash),
61
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE,
62
+ CHECK (expires_at > created_at)
63
+ );
64
+
65
+ CREATE INDEX idx_artifact_frame_grants_expiry ON artifact_frame_grants (project_id, expires_at);
66
+ -- Deleting an artifact cascades here, and Postgres does not index a foreign key by itself.
67
+ CREATE INDEX idx_artifact_frame_grants_artifact ON artifact_frame_grants (project_id, artifact_id);
@@ -0,0 +1,14 @@
1
+ -- The Agent delivers files by publishing artifacts, so a run no longer collects an output
2
+ -- directory or reports on it. Stored `output_*` diagnostics go; a run left with none stores none.
3
+ UPDATE agent_runs
4
+ SET diagnostics = (
5
+ SELECT jsonb_agg(item.value ORDER BY item.position)
6
+ FROM jsonb_array_elements(agent_runs.diagnostics) WITH ORDINALITY AS item(value, position)
7
+ WHERE item.value->>'code' NOT LIKE 'output\_%'
8
+ )
9
+ WHERE jsonb_typeof(diagnostics) = 'array'
10
+ AND EXISTS (
11
+ SELECT 1
12
+ FROM jsonb_array_elements(agent_runs.diagnostics) AS item(value)
13
+ WHERE item.value->>'code' LIKE 'output\_%'
14
+ );
@@ -0,0 +1,5 @@
1
+ -- A plan's work is analyzed whenever a stream has staged enough of it, and most of that ANALYZE
2
+ -- went into the statistics of `payload`, which no query reads: the plan queries filter on the
3
+ -- typed columns and only extract values from `payload`, and an extracted value has no statistics.
4
+ -- A query that filters `payload` itself (`@>`, `?`) would need them back.
5
+ ALTER TABLE ontology_replacement_plan_work ALTER COLUMN payload SET STATISTICS 0;
@@ -0,0 +1,61 @@
1
+ -- App activity: one source per verified session, the visits it opened, and their page views.
2
+ -- Foreground activity moves a visit's `last_seen_at` and stores no row, unless it opens a visit.
3
+ -- A session id names an `auth_sessions` row for a user and a `share_sessions` row for a share; it
4
+ -- is not a secret, and no foreign key holds it, so history outlives its sessions. Ids compare
5
+ -- byte by byte, as the other providers sort them.
6
+ CREATE TABLE activity_sources (
7
+ project_id TEXT NOT NULL,
8
+ actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
9
+ session_id TEXT NOT NULL CHECK (length(session_id) > 0),
10
+ visit_id TEXT NOT NULL,
11
+ last_seen_at TIMESTAMPTZ NOT NULL,
12
+ sequence INTEGER NOT NULL CHECK (sequence >= 0),
13
+ window_start TIMESTAMPTZ NOT NULL,
14
+ window_count INTEGER NOT NULL CHECK (window_count >= 0),
15
+ PRIMARY KEY (project_id, actor_type, session_id)
16
+ );
17
+
18
+ CREATE TABLE activity_visits (
19
+ project_id TEXT NOT NULL,
20
+ id TEXT COLLATE "C" NOT NULL,
21
+ actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'share')),
22
+ user_id TEXT COLLATE "C",
23
+ grant_id TEXT,
24
+ session_id TEXT NOT NULL CHECK (length(session_id) > 0),
25
+ started_at TIMESTAMPTZ NOT NULL,
26
+ last_seen_at TIMESTAMPTZ NOT NULL CHECK (last_seen_at >= started_at),
27
+ page_views INTEGER NOT NULL CHECK (page_views >= 0),
28
+ attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
29
+ PRIMARY KEY (project_id, id),
30
+ CHECK (
31
+ (actor_type = 'user' AND user_id IS NOT NULL AND grant_id IS NULL)
32
+ OR (actor_type = 'share' AND user_id IS NULL AND grant_id IS NOT NULL)
33
+ )
34
+ );
35
+
36
+ -- A period reads the visits it overlaps. A visit lasts less than a day, so they started in
37
+ -- `[from - 1 day, to)`: both period indexes bound `started_at` on both sides. `last_seen_at` is
38
+ -- left out of every index, so the update each batch makes touches none of them. The user index
39
+ -- also serves deleting one user's history.
40
+ CREATE INDEX idx_activity_visits_started
41
+ ON activity_visits (project_id, started_at, id);
42
+ CREATE INDEX idx_activity_visits_user
43
+ ON activity_visits (project_id, user_id, started_at, id);
44
+ -- A session's visits, to join them with the session and the executions it authorized.
45
+ CREATE INDEX idx_activity_visits_session
46
+ ON activity_visits (project_id, actor_type, session_id);
47
+
48
+ CREATE TABLE activity_page_views (
49
+ project_id TEXT NOT NULL,
50
+ visit_id TEXT COLLATE "C" NOT NULL,
51
+ sequence INTEGER NOT NULL CHECK (sequence > 0),
52
+ id TEXT NOT NULL,
53
+ route TEXT NOT NULL,
54
+ received_at TIMESTAMPTZ NOT NULL,
55
+ attribution JSONB CHECK (attribution IS NULL OR jsonb_typeof(attribution) = 'object'),
56
+ PRIMARY KEY (project_id, visit_id, sequence),
57
+ FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
58
+ );
59
+
60
+ -- Finds a retried page view by the id its browser chose.
61
+ CREATE INDEX idx_activity_page_views_id ON activity_page_views (project_id, id);
@@ -0,0 +1,15 @@
1
+ -- A visit's timeline reads what its session started: the roots it authorized in a window, then
2
+ -- everything they caused. A signed-in session is the credential of its roots, a shared link's
3
+ -- session their delegation; a child names its parent. Agent executions copy their parent's
4
+ -- session: the root indexes hold only executions without a parent, so they never see those, and
5
+ -- end with `id` so the root read's order needs no sort. A user's connection history reads
6
+ -- `idx_auth_sessions_user_created`, which already exists.
7
+ CREATE INDEX idx_executions_session_roots
8
+ ON executions (project_id, authority_session_id, created_at, id)
9
+ WHERE authority_session_id IS NOT NULL AND parent_execution_id IS NULL;
10
+ CREATE INDEX idx_executions_delegation_roots
11
+ ON executions (project_id, authority_delegation_session_id, created_at, id)
12
+ WHERE authority_delegation_session_id IS NOT NULL AND parent_execution_id IS NULL;
13
+ CREATE INDEX idx_executions_parent
14
+ ON executions (project_id, parent_execution_id)
15
+ WHERE parent_execution_id IS NOT NULL;
@@ -0,0 +1,20 @@
1
+ -- Interactions an app marks, such as a button: their name and route, never a value. They share
2
+ -- the visit's sequence with its page views, so a visit reads both in the order they happened, and
3
+ -- cascade with the visit, which counts them as it counts its page views.
4
+ ALTER TABLE activity_visits
5
+ ADD COLUMN interactions INTEGER NOT NULL DEFAULT 0 CHECK (interactions >= 0);
6
+
7
+ CREATE TABLE activity_interactions (
8
+ project_id TEXT NOT NULL,
9
+ visit_id TEXT COLLATE "C" NOT NULL,
10
+ sequence INTEGER NOT NULL CHECK (sequence > 0),
11
+ id TEXT NOT NULL,
12
+ name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 100),
13
+ route TEXT NOT NULL,
14
+ received_at TIMESTAMPTZ NOT NULL,
15
+ PRIMARY KEY (project_id, visit_id, sequence),
16
+ FOREIGN KEY (project_id, visit_id) REFERENCES activity_visits (project_id, id) ON DELETE CASCADE
17
+ );
18
+
19
+ -- Finds a retried interaction by the id its browser chose.
20
+ CREATE INDEX idx_activity_interactions_id ON activity_interactions (project_id, id);
@@ -0,0 +1,4 @@
1
+ -- Artifacts take two more kinds: pages, and the canvases that lay pages out.
2
+ ALTER TABLE artifacts DROP CONSTRAINT artifacts_kind_check;
3
+ ALTER TABLE artifacts ADD CONSTRAINT artifacts_kind_check
4
+ CHECK (kind IN ('document', 'sheet', 'slides', 'page', 'canvas', 'file'));
@@ -0,0 +1,16 @@
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. The value stays text, as written, so its
3
+ -- size is the one the reader's limits count; `size_bytes` is its UTF-8 length. Deleting the
4
+ -- artifact cascades here.
5
+ CREATE TABLE artifact_state (
6
+ project_id TEXT NOT NULL,
7
+ artifact_id TEXT NOT NULL,
8
+ principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
9
+ principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
10
+ key TEXT NOT NULL CHECK (length(key) > 0),
11
+ value TEXT NOT NULL,
12
+ size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
13
+ updated_at TIMESTAMPTZ NOT NULL,
14
+ PRIMARY KEY (project_id, artifact_id, principal_type, principal_id, key),
15
+ FOREIGN KEY (project_id, artifact_id) REFERENCES artifacts (project_id, id) ON DELETE CASCADE
16
+ );
@@ -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 TIMESTAMPTZ 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,23 @@
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
+ -- The project-wide constraint was unnamed: drop the one unique on exactly those two columns, and
4
+ -- stop if it is not there rather than leave the old rule in place.
5
+ DO $$
6
+ DECLARE run_constraint TEXT;
7
+ BEGIN
8
+ SELECT conname INTO run_constraint
9
+ FROM pg_constraint
10
+ WHERE conrelid = 'agent_context_checkpoints'::regclass
11
+ AND contype = 'u'
12
+ AND pg_get_constraintdef(oid) = 'UNIQUE (project_id, created_by_run_id)';
13
+
14
+ IF run_constraint IS NULL THEN
15
+ RAISE EXCEPTION 'agent_context_checkpoints has no UNIQUE (project_id, created_by_run_id) to drop';
16
+ END IF;
17
+
18
+ EXECUTE format('ALTER TABLE agent_context_checkpoints DROP CONSTRAINT %I', run_constraint);
19
+ END $$;
20
+
21
+ ALTER TABLE agent_context_checkpoints
22
+ ADD CONSTRAINT uq_agent_context_checkpoints_thread_run
23
+ UNIQUE (project_id, thread_id, created_by_run_id);
@@ -0,0 +1,25 @@
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
+ -- Values are keyed by their SHA-256: a long string stays under the btree key limit, and a lookup
4
+ -- still compares `value_key` itself.
5
+ CREATE TABLE object_unique_values (
6
+ project_id TEXT NOT NULL,
7
+ object_type_id TEXT NOT NULL,
8
+ property_id TEXT NOT NULL,
9
+ value_hash BYTEA NOT NULL,
10
+ value_key TEXT NOT NULL,
11
+ primary_id TEXT NOT NULL,
12
+ PRIMARY KEY (project_id, object_type_id, property_id, value_hash)
13
+ );
14
+
15
+ -- The unique properties whose values were built from the objects already stored, and how their
16
+ -- keys were computed. A claim made with other keys is refused until they are built again.
17
+ CREATE TABLE object_unique_properties (
18
+ project_id TEXT NOT NULL,
19
+ object_type_id TEXT NOT NULL,
20
+ property_id TEXT NOT NULL,
21
+ key_kind TEXT NOT NULL,
22
+ key_version INTEGER NOT NULL,
23
+ prepared_at TIMESTAMPTZ NOT NULL,
24
+ PRIMARY KEY (project_id, object_type_id, property_id)
25
+ );
@@ -0,0 +1,21 @@
1
+ ALTER TABLE ai_model_call_usage
2
+ ADD COLUMN image_output_tokens BIGINT
3
+ CHECK (image_output_tokens IS NULL OR image_output_tokens >= 0),
4
+ ADD COLUMN audio_output_tokens BIGINT
5
+ CHECK (audio_output_tokens IS NULL OR audio_output_tokens >= 0),
6
+ ADD COLUMN output_images BIGINT
7
+ CHECK (output_images IS NULL OR output_images >= 0),
8
+ ADD COLUMN input_characters BIGINT
9
+ CHECK (input_characters IS NULL OR input_characters >= 0);
10
+
11
+ ALTER TABLE ai_model_call_usage
12
+ DROP CONSTRAINT ai_model_call_usage_model_kind_check;
13
+
14
+ ALTER TABLE ai_model_call_usage
15
+ ADD CONSTRAINT ai_model_call_usage_model_kind_check
16
+ CHECK (
17
+ model_kind IS NULL
18
+ OR model_kind IN (
19
+ 'language', 'image', 'video', 'embedding', 'decision', 'transcription', 'speech', 'reranking'
20
+ )
21
+ );
@@ -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 JSONB NOT NULL CHECK (jsonb_typeof(files) = 'array' AND jsonb_array_length(files) > 0),
7
+ recorded_at TIMESTAMPTZ 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,33 @@
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.
4
+ CREATE TABLE agent_memory_revisions (
5
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
6
+ repo_id TEXT NOT NULL CHECK (length(trim(repo_id)) > 0),
7
+ revision INTEGER NOT NULL CHECK (revision > 0),
8
+ message TEXT NOT NULL CHECK (length(trim(message)) > 0),
9
+ author_principal_type TEXT NOT NULL
10
+ CHECK (author_principal_type IN ('user', 'serviceAccount', 'system')),
11
+ author_principal_id TEXT NOT NULL CHECK (length(author_principal_id) > 0),
12
+ source TEXT,
13
+ created_at TIMESTAMPTZ NOT NULL,
14
+ PRIMARY KEY (project_id, repo_id, revision)
15
+ );
16
+
17
+ -- A save also removes versions older than 90 days that are not their path's latest, so a
18
+ -- revision's rows here can be fewer than the files it wrote. Paths compare by code unit, as they
19
+ -- do in SQLite.
20
+ CREATE TABLE agent_memory_files (
21
+ project_id TEXT NOT NULL,
22
+ repo_id TEXT NOT NULL,
23
+ path TEXT COLLATE "C" 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
+ -- Deleting a repo cascades here, and Postgres does not index a foreign key by itself.
32
+ CREATE INDEX idx_agent_memory_files_revision
33
+ 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 TIMESTAMPTZ NOT NULL,
10
+ copied BOOLEAN NOT NULL DEFAULT FALSE,
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;