@opengeni/db 0.2.0

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 (66) hide show
  1. package/dist/chunk-57MLICFR.js +121 -0
  2. package/dist/chunk-57MLICFR.js.map +1 -0
  3. package/dist/chunk-OGCE6O2X.js +52 -0
  4. package/dist/chunk-OGCE6O2X.js.map +1 -0
  5. package/dist/chunk-PSX56ZTL.js +1093 -0
  6. package/dist/chunk-PSX56ZTL.js.map +1 -0
  7. package/dist/chunk-PZ5AY32C.js +10 -0
  8. package/dist/chunk-PZ5AY32C.js.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +5165 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/migrate.d.ts +40 -0
  13. package/dist/migrate.js +10 -0
  14. package/dist/migrate.js.map +1 -0
  15. package/dist/provision-roles.d.ts +2063 -0
  16. package/dist/provision-roles.js +8 -0
  17. package/dist/provision-roles.js.map +1 -0
  18. package/dist/schema-CaeZQAJQ.d.ts +9705 -0
  19. package/dist/schema.d.ts +3 -0
  20. package/dist/schema.js +110 -0
  21. package/dist/schema.js.map +1 -0
  22. package/drizzle/0000_initial.sql +179 -0
  23. package/drizzle/0001_workspace_auth_billing.sql +590 -0
  24. package/drizzle/0002_packs_and_social.sql +99 -0
  25. package/drizzle/0003_capability_catalog.sql +73 -0
  26. package/drizzle/0004_workspace_environments.sql +65 -0
  27. package/drizzle/0005_session_goals.sql +45 -0
  28. package/drizzle/0006_workspace_packs.sql +31 -0
  29. package/drizzle/0007_session_history_items.sql +66 -0
  30. package/drizzle/0008_session_first_party_mcp_permissions.sql +5 -0
  31. package/drizzle/0009_goal_sessions_first_party_goals_manage.sql +34 -0
  32. package/drizzle/0010_session_parent_linkage.sql +30 -0
  33. package/drizzle/0011_context_compaction.sql +33 -0
  34. package/drizzle/0012_compaction_summary_fractional_position.sql +19 -0
  35. package/drizzle/0013_session_compact_requested.sql +16 -0
  36. package/drizzle/0014_repair_orphaned_function_call_results.sql +125 -0
  37. package/drizzle/0015_workspace_agent_instructions.sql +17 -0
  38. package/drizzle/0016_session_create_idempotency.sql +27 -0
  39. package/drizzle/0017_sandbox_leases.sql +313 -0
  40. package/drizzle/0018_sandbox_os.sql +89 -0
  41. package/drizzle/0019_session_stream_acknowledgments.sql +94 -0
  42. package/drizzle/0020_session_recordings.sql +88 -0
  43. package/drizzle/0021_sandbox_pty_sessions.sql +70 -0
  44. package/drizzle/0022_sandbox_lease_terminal_url.sql +32 -0
  45. package/drizzle/0023_session_title.sql +19 -0
  46. package/drizzle/0024_codex_subscription_credentials.sql +51 -0
  47. package/drizzle/0024_sandboxes_enrollments_metrics.sql +262 -0
  48. package/drizzle/0025_device_enrollment_requests.sql +142 -0
  49. package/drizzle/0026_device_enrollment_user_code_resolver.sql +47 -0
  50. package/drizzle/0027_session_working_dir.sql +24 -0
  51. package/drizzle/0028_codex_multi_account.sql +85 -0
  52. package/drizzle/0029_session_history_item_producer.sql +31 -0
  53. package/drizzle/0030_agent_run_state_frozen_codex.sql +35 -0
  54. package/drizzle/0031_codex_usage_cache.sql +21 -0
  55. package/drizzle/0032_codex_account_cooldown.sql +18 -0
  56. package/drizzle/0033_codex_connector_cache.sql +20 -0
  57. package/drizzle/0034_sandbox_lease_image.sql +21 -0
  58. package/drizzle/meta/_journal.json +167 -0
  59. package/package.json +66 -0
  60. package/src/codex-token-resolver.ts +247 -0
  61. package/src/environment-crypto.ts +51 -0
  62. package/src/event-payload-sanitizer.ts +89 -0
  63. package/src/index.ts +7776 -0
  64. package/src/migrate.ts +95 -0
  65. package/src/provision-roles.ts +198 -0
  66. package/src/schema.ts +1110 -0
package/src/schema.ts ADDED
@@ -0,0 +1,1110 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { bigint, boolean, index, integer, jsonb, numeric, pgTable, text, timestamp, uniqueIndex, uuid, customType } from "drizzle-orm/pg-core";
3
+
4
+ const vector = customType<{ data: number[]; driverData: string }>({
5
+ dataType() {
6
+ return "vector(3072)";
7
+ },
8
+ toDriver(value) {
9
+ return `[${value.join(",")}]`;
10
+ },
11
+ });
12
+
13
+ export const managedAccounts = pgTable("managed_accounts", {
14
+ id: uuid("id").primaryKey().defaultRandom(),
15
+ name: text("name").notNull(),
16
+ externalSource: text("external_source"),
17
+ externalId: text("external_id"),
18
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
20
+ }, (table) => ({
21
+ external: uniqueIndex("managed_accounts_external_idx").on(table.externalSource, table.externalId),
22
+ }));
23
+
24
+ export const workspaces = pgTable("workspaces", {
25
+ id: uuid("id").primaryKey().defaultRandom(),
26
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
27
+ name: text("name").notNull(),
28
+ slug: text("slug"),
29
+ externalSource: text("external_source"),
30
+ externalId: text("external_id"),
31
+ // White-label agent persona template override. NULL means the deployment
32
+ // default (OPENGENI_AGENT_INSTRUCTIONS_TEMPLATE / DEFAULT_AGENT_INSTRUCTIONS).
33
+ agentInstructions: text("agent_instructions"),
34
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
35
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
36
+ }, (table) => ({
37
+ account: index("workspaces_account_idx").on(table.accountId),
38
+ accountSlug: uniqueIndex("workspaces_account_slug_idx").on(table.accountId, table.slug).where(sql`${table.slug} is not null`),
39
+ external: uniqueIndex("workspaces_external_idx").on(table.externalSource, table.externalId),
40
+ }));
41
+
42
+ export const workspaceMemberships = pgTable("workspace_memberships", {
43
+ id: uuid("id").primaryKey().defaultRandom(),
44
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
45
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
46
+ subjectId: text("subject_id").notNull(),
47
+ subjectLabel: text("subject_label"),
48
+ role: text("role").notNull().default("member"),
49
+ permissions: jsonb("permissions").$type<string[]>().notNull().default([]),
50
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
51
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
52
+ }, (table) => ({
53
+ subjectWorkspace: uniqueIndex("workspace_memberships_subject_workspace_idx").on(table.subjectId, table.workspaceId),
54
+ subject: index("workspace_memberships_subject_idx").on(table.subjectId),
55
+ account: index("workspace_memberships_account_idx").on(table.accountId),
56
+ }));
57
+
58
+ export const apiKeys = pgTable("api_keys", {
59
+ id: uuid("id").primaryKey().defaultRandom(),
60
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
61
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
62
+ name: text("name").notNull(),
63
+ prefix: text("prefix").notNull(),
64
+ keyHash: text("key_hash").notNull(),
65
+ permissions: jsonb("permissions").$type<string[]>().notNull().default([]),
66
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
67
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
68
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
69
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
70
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
71
+ }, (table) => ({
72
+ prefix: index("api_keys_prefix_idx").on(table.prefix),
73
+ hash: uniqueIndex("api_keys_key_hash_idx").on(table.keyHash),
74
+ account: index("api_keys_account_idx").on(table.accountId),
75
+ workspace: index("api_keys_workspace_idx").on(table.workspaceId),
76
+ }));
77
+
78
+ export const workspaceEnvironments = pgTable("workspace_environments", {
79
+ id: uuid("id").primaryKey().defaultRandom(),
80
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
81
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
82
+ name: text("name").notNull(),
83
+ description: text("description"),
84
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
85
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
86
+ }, (table) => ({
87
+ workspaceName: uniqueIndex("workspace_environments_workspace_name_idx").on(table.workspaceId, table.name),
88
+ workspaceCreated: index("workspace_environments_workspace_created_idx").on(table.workspaceId, table.createdAt),
89
+ }));
90
+
91
+ export const workspaceEnvironmentVariables = pgTable("workspace_environment_variables", {
92
+ id: uuid("id").primaryKey().defaultRandom(),
93
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
94
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
95
+ environmentId: uuid("environment_id").notNull().references(() => workspaceEnvironments.id, { onDelete: "cascade" }),
96
+ name: text("name").notNull(),
97
+ // Format: v1:<base64 iv>:<base64 ciphertext||gcm-tag>. Never returned by any API.
98
+ valueEncrypted: text("value_encrypted").notNull(),
99
+ version: integer("version").notNull().default(1),
100
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
101
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
102
+ }, (table) => ({
103
+ environmentName: uniqueIndex("workspace_environment_variables_env_name_idx").on(table.workspaceId, table.environmentId, table.name),
104
+ environment: index("workspace_environment_variables_workspace_env_idx").on(table.workspaceId, table.environmentId),
105
+ }));
106
+
107
+ // Per-workspace ChatGPT/Codex subscription credential. One row per workspace.
108
+ // access/refresh/id tokens live INSIDE credential_encrypted (v1 AES-256-GCM,
109
+ // same envelope as workspace_environment_variables); the other columns are
110
+ // plaintext metadata (header value + UI). RLS-isolated per workspace.
111
+ export const codexSubscriptionCredentials = pgTable("codex_subscription_credentials", {
112
+ id: uuid("id").primaryKey().defaultRandom(),
113
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
114
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
115
+ // Format: v1:<base64 iv>:<base64 ciphertext||gcm-tag>. JSON {access_token, refresh_token, id_token}. Never returned by any API.
116
+ credentialEncrypted: text("credential_encrypted").notNull(),
117
+ chatgptAccountId: text("chatgpt_account_id"), // plaintext ChatGPT-Account-ID header value (non-secret)
118
+ scopes: text("scopes"), // space-delimited, as granted
119
+ planType: text("plan_type"),
120
+ isFedramp: boolean("is_fedramp").notNull().default(false),
121
+ expiresAt: timestamp("expires_at", { withTimezone: true }), // derived from access-token JWT exp
122
+ lastRefreshAt: timestamp("last_refresh_at", { withTimezone: true }),
123
+ status: text("status").notNull().default("active"), // active | needs_relogin | error
124
+ lastError: text("last_error"),
125
+ version: integer("version").notNull().default(1),
126
+ label: text("label"), // user-chosen nickname; null ⇒ derive from email/plan/account
127
+ accountEmail: text("account_email"), // email from the id_token (user's own email; non-secret)
128
+ // P2 usage cache (plaintext metadata; NEVER a token). Snapshotted from
129
+ // GET /wham/usage; drives the quota bars + the cache TTL. primary = 5h window
130
+ // (limit_window_seconds 18000), secondary = weekly (604800).
131
+ primaryUsedPercent: integer("primary_used_percent"),
132
+ primaryResetAt: timestamp("primary_reset_at", { withTimezone: true }),
133
+ secondaryUsedPercent: integer("secondary_used_percent"),
134
+ secondaryResetAt: timestamp("secondary_reset_at", { withTimezone: true }),
135
+ usageCheckedAt: timestamp("usage_checked_at", { withTimezone: true }), // snapshot freshness → cache TTL clock
136
+ // P3 rotation cooldown (plaintext metadata; NEVER a token). Set when this account hit its
137
+ // usage cap on a rotation turn; the rotation engine treats `exhausted_until > now()` as
138
+ // capped/skip so it isn't immediately re-picked. Self-clears via the now() comparison.
139
+ exhaustedUntil: timestamp("exhausted_until", { withTimezone: true }),
140
+ // P4 connector-aware rotation cache (plaintext metadata; NEVER a token). The set
141
+ // of ORIGINAL-dotted connector namespaces (github/gmail/linear/…) this account
142
+ // exposes via codex_apps, captured from the per-turn tools/list. null ⇒ never
143
+ // probed (the ranker treats it as unknown: never credited as covering, never
144
+ // excluded). The writer only ever sets a NON-empty set, so a flaky empty turn
145
+ // can't false-drop coverage. connectorsCheckedAt is the freshness clock.
146
+ connectorNamespaces: text("connector_namespaces").array(),
147
+ connectorsCheckedAt: timestamp("connectors_checked_at", { withTimezone: true }),
148
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
149
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
150
+ }, (table) => ({
151
+ // REPLACES codex_subscription_credentials_workspace_idx (the one-per-workspace cap).
152
+ // One row per (workspace, ChatGPT account). Partial WHERE chatgpt_account_id IS NOT NULL
153
+ // so degenerate null-account rows can't collide; the device-grant connect path always
154
+ // populates chatgpt_account_id.
155
+ wsAccount: uniqueIndex("codex_subscription_credentials_ws_account_idx")
156
+ .on(table.workspaceId, table.chatgptAccountId)
157
+ .where(sql`${table.chatgptAccountId} is not null`),
158
+ workspace: index("codex_subscription_credentials_workspace_lookup_idx").on(table.workspaceId),
159
+ }));
160
+
161
+ // Per-workspace Codex account selection (the ACTIVE pointer) + P3 rotation
162
+ // forward-compat. One row per workspace. The only P1-load-bearing column is
163
+ // activeCredentialId — the account a session runs on when it has no pin. NULL ⇒
164
+ // none selected (e.g. the active one was just disconnected). The
165
+ // (account_id, workspace_id) pair inherits the verbatim workspace_rls_visible
166
+ // policy. active_credential_id's FK is declared in the MIGRATION (not
167
+ // .references()) to avoid a forward-reference on the const ordering, exactly like
168
+ // sessions.activeSandboxId; ON DELETE SET NULL.
169
+ export const codexRotationSettings = pgTable("codex_rotation_settings", {
170
+ id: uuid("id").primaryKey().defaultRandom(),
171
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
172
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
173
+ activeCredentialId: uuid("active_credential_id"),
174
+ rotationEnabled: boolean("rotation_enabled").notNull().default(false), // P3, inert in P1
175
+ rotationStrategy: text("rotation_strategy").notNull().default("most_remaining"), // P3, inert
176
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
177
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
178
+ }, (table) => ({
179
+ workspace: uniqueIndex("codex_rotation_settings_workspace_idx").on(table.workspaceId),
180
+ }));
181
+
182
+ export const sessions = pgTable("sessions", {
183
+ id: uuid("id").primaryKey().defaultRandom(),
184
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
185
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
186
+ status: text("status").notNull().default("queued"),
187
+ initialMessage: text("initial_message").notNull(),
188
+ title: text("title"),
189
+ titleSource: text("title_source"),
190
+ resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
191
+ tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
192
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
193
+ model: text("model").notNull(),
194
+ sandboxBackend: text("sandbox_backend").notNull(),
195
+ // The OS this session's box runs. Defaults to 'linux' (today's only OS, so
196
+ // every existing + new row is a behavior-preserving no-op). CHECK-constrained
197
+ // to the SandboxOs enum (linux|macos|windows) in migration 0018.
198
+ sandboxOs: text("sandbox_os").notNull().default("linux"),
199
+ // The shared-sandbox group this session's box belongs to. Defaults to the
200
+ // session's OWN id (a singleton group: group === session — today's 1:1
201
+ // behavior). When spawned shared via session_create, set to the PARENT's
202
+ // sandboxGroupId so both run in ONE box. Immutable once set. NOT an FK (the
203
+ // value is this row's id or an ancestor session's id in the same workspace;
204
+ // the live lease row, not a sandbox_groups table, materializes the group).
205
+ // The app generates the uuid and uses it for both id and sandbox_group_id in
206
+ // one insert — it cannot SQL-default to id (id is defaultRandom()).
207
+ sandboxGroupId: uuid("sandbox_group_id").notNull(),
208
+ // The first-class swappable-sandbox POINTER (bring-your-own-compute M2,
209
+ // dossier §10.3). NULL == "use the session's own group sandbox" (the
210
+ // backward-compat default — every existing/new row is a behavior-preserving
211
+ // no-op). The routing proxy re-reads (active_sandbox_id, active_epoch) PER
212
+ // TOOL CALL to make a Modal<->selfhosted hot-swap seamless. The FK
213
+ // (-> sandboxes(id) ON DELETE SET NULL — a deleted sandbox degrades the
214
+ // pointer to the group default, never dangles) lives in migration 0024, NOT a
215
+ // Drizzle .references() — exactly like parentSessionId below, so the const
216
+ // ordering imposes no forward-reference.
217
+ activeSandboxId: uuid("active_sandbox_id"),
218
+ // The SECOND epoch ABOVE sandbox_leases.lease_epoch, bumped on every swap; an
219
+ // in-flight op fenced by a stale active_epoch retries against the new active
220
+ // sandbox. integer (NOT bigint) — the lease-epoch spike: int8 reads back as a
221
+ // JS string and breaks the strict fence; int4 returns a number.
222
+ activeEpoch: integer("active_epoch").notNull().default(0),
223
+ // The session's WORKING DIRECTORY — the path/cwd base the (selfhosted) box's
224
+ // agent/terminal/file-dock operate under. A launch-workspace_root-relative
225
+ // subdir or an absolute machine path; surfaced alongside the active-sandbox
226
+ // pointer (readActiveSandbox) and written through the epoch-fenced
227
+ // setActiveSandbox CAS, NOT the row INSERT. NULL (the default) ⇒ today's
228
+ // behavior exactly — the agent substitutes its workspace_root for an empty cwd,
229
+ // so an unset working_dir is a byte-identical no-op. Create-time only (Stage A).
230
+ workingDir: text("working_dir"),
231
+ environmentId: uuid("environment_id").references(() => workspaceEnvironments.id, { onDelete: "set null" }),
232
+ // Non-default first-party MCP token permissions (manager-style sessions);
233
+ // null means the fixed worker default set in @opengeni/runtime.
234
+ firstPartyMcpPermissions: jsonb("first_party_mcp_permissions").$type<string[]>(),
235
+ // The manager session that spawned this one via session_create. Set only
236
+ // when the creating grant carried a worker-signed sessionId claim (a session
237
+ // spawning a worker); null for direct API creates and scheduled-task runs.
238
+ // When set, this worker's terminal-for-now transitions wake the parent so a
239
+ // manager can orchestrate workers without busy-polling. Self-referencing FK,
240
+ // ON DELETE SET NULL so deleting a manager never cascades into its workers.
241
+ parentSessionId: uuid("parent_session_id"),
242
+ // Workspace-scoped CREATE idempotency key. NULL means the create carried no
243
+ // key (each such create is independent). When set, the partial unique index
244
+ // below collapses concurrent/retried creates with the same key in the same
245
+ // workspace to a single session row — the dedup that closes the
246
+ // double-submit/double-dispatch stuck-queued bug.
247
+ createIdempotencyKey: text("create_idempotency_key"),
248
+ temporalWorkflowId: text("temporal_workflow_id"),
249
+ activeTurnId: uuid("active_turn_id"),
250
+ // Actual input tokens reported for the last model call of the most recent
251
+ // turn. The pre-turn client-side compaction trigger reads this as its budget
252
+ // signal (char/4 estimate is the same-turn fallback). Null until a turn with
253
+ // usage has completed.
254
+ lastInputTokens: integer("last_input_tokens"),
255
+ // Operator /compact request flag (client-side compaction path). The API sets
256
+ // it true; the worker honors it BEFORE the next turn's model call by forcing
257
+ // a compaction, then clears it. A durable flag (not a transient signal) so
258
+ // the trigger survives a worker restart and converges before the next turn.
259
+ compactRequested: boolean("compact_requested").notNull().default(false),
260
+ lastSequence: integer("last_sequence").notNull().default(0),
261
+ // The session's PINNED Codex account (manual override from the in-session
262
+ // switcher). NULL ⇒ follow the workspace active pointer. FK declared in the
263
+ // migration with ON DELETE SET NULL (a disconnected pin degrades to "follow
264
+ // active", never dangles), same pattern as activeSandboxId.
265
+ codexPinnedCredentialId: uuid("codex_pinned_credential_id"),
266
+ // The Codex account the session's most recent turn ACTUALLY ran on — drives
267
+ // the "Running on:" indicator. Written by the worker at the turn boundary. FK
268
+ // ON DELETE SET NULL (migration).
269
+ codexLastCredentialId: uuid("codex_last_credential_id"),
270
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
271
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
272
+ }, (table) => ({
273
+ workspaceCreated: index("sessions_workspace_created_idx").on(table.workspaceId, table.createdAt),
274
+ environment: index("sessions_environment_idx").on(table.workspaceId, table.environmentId),
275
+ parent: index("sessions_parent_idx").on(table.workspaceId, table.parentSessionId),
276
+ // Routing index: resolve session_id -> sandbox_group_id at every lease entry
277
+ // point and enumerate all sessions in a group for attribution/disclosure.
278
+ sandboxGroup: index("sessions_sandbox_group_idx").on(table.workspaceId, table.sandboxGroupId),
279
+ // Partial unique index: one session per (workspace, create_idempotency_key)
280
+ // when a key is present. Concurrent creates racing on the same key see a
281
+ // unique violation on all but one; the domain layer catches it and returns
282
+ // the winning row instead of erroring.
283
+ createIdempotency: uniqueIndex("sessions_workspace_create_idempotency_idx").on(table.workspaceId, table.createIdempotencyKey).where(sql`${table.createIdempotencyKey} is not null`),
284
+ }));
285
+
286
+ export const files = pgTable("files", {
287
+ id: uuid("id").primaryKey().defaultRandom(),
288
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
289
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
290
+ status: text("status").notNull().default("pending_upload"),
291
+ filename: text("filename").notNull(),
292
+ safeFilename: text("safe_filename").notNull(),
293
+ contentType: text("content_type").notNull(),
294
+ sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
295
+ sha256: text("sha256"),
296
+ bucket: text("bucket").notNull(),
297
+ objectKey: text("object_key").notNull(),
298
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
299
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
300
+ }, (table) => ({
301
+ workspaceCreated: index("files_workspace_created_idx").on(table.workspaceId, table.createdAt),
302
+ objectKey: uniqueIndex("files_object_key_idx").on(table.objectKey),
303
+ status: index("files_status_idx").on(table.status),
304
+ }));
305
+
306
+ export const fileUploads = pgTable("file_uploads", {
307
+ id: uuid("id").primaryKey().defaultRandom(),
308
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
309
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
310
+ fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "cascade" }),
311
+ status: text("status").notNull().default("pending"),
312
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
313
+ completedAt: timestamp("completed_at", { withTimezone: true }),
314
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
315
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
316
+ }, (table) => ({
317
+ workspace: index("file_uploads_workspace_idx").on(table.workspaceId),
318
+ fileId: index("file_uploads_file_id_idx").on(table.fileId),
319
+ status: index("file_uploads_status_idx").on(table.status),
320
+ }));
321
+
322
+ export const documentBases = pgTable("document_bases", {
323
+ id: uuid("id").primaryKey().defaultRandom(),
324
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
325
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
326
+ name: text("name").notNull(),
327
+ description: text("description"),
328
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
329
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
330
+ }, (table) => ({
331
+ workspaceCreated: index("document_bases_workspace_created_idx").on(table.workspaceId, table.createdAt),
332
+ }));
333
+
334
+ export const documents = pgTable("documents", {
335
+ id: uuid("id").primaryKey().defaultRandom(),
336
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
337
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
338
+ baseId: uuid("base_id").notNull().references(() => documentBases.id, { onDelete: "cascade" }),
339
+ fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "restrict" }),
340
+ status: text("status").notNull().default("queued"),
341
+ title: text("title").notNull(),
342
+ parser: text("parser").notNull().default("liteparse"),
343
+ chunkCount: integer("chunk_count").notNull().default(0),
344
+ error: text("error"),
345
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
346
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
347
+ }, (table) => ({
348
+ baseFile: uniqueIndex("documents_workspace_base_file_idx").on(table.workspaceId, table.baseId, table.fileId),
349
+ baseStatus: index("documents_workspace_base_status_idx").on(table.workspaceId, table.baseId, table.status),
350
+ }));
351
+
352
+ export const documentChunks = pgTable("document_chunks", {
353
+ id: uuid("id").primaryKey().defaultRandom(),
354
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
355
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
356
+ documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
357
+ baseId: uuid("base_id").notNull().references(() => documentBases.id, { onDelete: "cascade" }),
358
+ fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "restrict" }),
359
+ chunkIndex: integer("chunk_index").notNull(),
360
+ text: text("text").notNull(),
361
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
362
+ embedding: vector("embedding").notNull(),
363
+ embeddingModel: text("embedding_model").notNull(),
364
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
365
+ }, (table) => ({
366
+ documentIndex: uniqueIndex("document_chunks_workspace_document_index_idx").on(table.workspaceId, table.documentId, table.chunkIndex),
367
+ base: index("document_chunks_workspace_base_idx").on(table.workspaceId, table.baseId),
368
+ }));
369
+
370
+ export const sessionTurns = pgTable("session_turns", {
371
+ id: uuid("id").primaryKey().defaultRandom(),
372
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
373
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
374
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
375
+ triggerEventId: uuid("trigger_event_id").notNull(),
376
+ temporalWorkflowId: text("temporal_workflow_id").notNull(),
377
+ status: text("status").notNull(),
378
+ source: text("source").notNull().default("user"),
379
+ position: integer("position").notNull(),
380
+ prompt: text("prompt").notNull(),
381
+ resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
382
+ tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
383
+ model: text("model").notNull(),
384
+ reasoningEffort: text("reasoning_effort").notNull(),
385
+ sandboxBackend: text("sandbox_backend").notNull(),
386
+ // Per-turn OS override. NULL = inherit the session's sandbox_os. CHECK-
387
+ // constrained to the SandboxOs enum (or NULL) in migration 0018.
388
+ sandboxOs: text("sandbox_os"),
389
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
390
+ startedAt: timestamp("started_at", { withTimezone: true }),
391
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
392
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
393
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
394
+ }, (table) => ({
395
+ queue: index("session_turns_workspace_queue_idx").on(table.workspaceId, table.sessionId, table.status, table.position),
396
+ }));
397
+
398
+ export const sessionGoals = pgTable("session_goals", {
399
+ id: uuid("id").primaryKey().defaultRandom(),
400
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
401
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
402
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
403
+ status: text("status").notNull().default("active"), // active | paused | completed
404
+ text: text("text").notNull(),
405
+ successCriteria: text("success_criteria"),
406
+ evidence: text("evidence"), // set by goal_complete
407
+ rationale: text("rationale"), // set by goal_pause
408
+ pausedReason: text("paused_reason"), // agent | user_interrupt | api | no_progress | max_auto_continuations | limits
409
+ createdBy: text("created_by").notNull().default("api"), // api | agent | scheduled_task
410
+ version: integer("version").notNull().default(1), // bumped on every set/update; progress signal
411
+ autoContinuations: integer("auto_continuations").notNull().default(0),
412
+ noProgressStreak: integer("no_progress_streak").notNull().default(0),
413
+ maxAutoContinuations: integer("max_auto_continuations"), // per-goal override; a configured settings cap (if any) remains the hard ceiling
414
+ lastContinuationTurnId: uuid("last_continuation_turn_id"),
415
+ versionAtLastContinuation: integer("version_at_last_continuation"),
416
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
417
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
418
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
419
+ }, (table) => ({
420
+ workspaceSession: uniqueIndex("session_goals_workspace_session_idx").on(table.workspaceId, table.sessionId),
421
+ status: index("session_goals_workspace_status_idx").on(table.workspaceId, table.status),
422
+ }));
423
+
424
+ export const sessionEvents = pgTable("session_events", {
425
+ id: uuid("id").primaryKey().defaultRandom(),
426
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
427
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
428
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
429
+ turnId: uuid("turn_id"),
430
+ sequence: integer("sequence").notNull(),
431
+ type: text("type").notNull(),
432
+ payload: jsonb("payload").$type<unknown>().notNull().default({}),
433
+ clientEventId: text("client_event_id"),
434
+ producerId: text("producer_id"),
435
+ producerSeq: integer("producer_seq"),
436
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
437
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
438
+ }, (table) => ({
439
+ sessionSequence: uniqueIndex("session_events_workspace_session_sequence_idx").on(table.workspaceId, table.sessionId, table.sequence),
440
+ clientEvent: uniqueIndex("session_events_workspace_client_event_idx").on(table.workspaceId, table.sessionId, table.clientEventId).where(sql`${table.clientEventId} is not null`),
441
+ producer: uniqueIndex("session_events_workspace_producer_idx").on(table.workspaceId, table.sessionId, table.producerId, table.producerSeq).where(sql`${table.producerId} is not null and ${table.producerSeq} is not null`),
442
+ sessionCreated: index("session_events_workspace_session_created_idx").on(table.workspaceId, table.sessionId, table.createdAt),
443
+ }));
444
+
445
+ export const agentRunStates = pgTable("agent_run_states", {
446
+ id: uuid("id").primaryKey().defaultRandom(),
447
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
448
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
449
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
450
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
451
+ stateVersion: integer("state_version").notNull(),
452
+ serializedRunState: text("serialized_run_state").notNull(),
453
+ pendingApprovals: jsonb("pending_approvals").$type<unknown[]>().notNull().default([]),
454
+ // The Codex account that FROZE this run state: the turn's resolved codex
455
+ // credential id (pin > workspace-active), or NULL when frozen on the
456
+ // non-codex / Azure path (or before this column existed). The serialized
457
+ // RunState blob round-trips `reasoning.encrypted_content` minted by the
458
+ // ChatGPT/Codex backend — account/org-bound, so a foreign blob 400s — and the
459
+ // foreign reasoning ids the Responses backend validates; but the blob carries
460
+ // NO per-item producer tag (those live only on session_history_items). So we
461
+ // stamp the freezing account here: on a resume (approval decision, or the
462
+ // items-mode run-state fallback) whose codex account DIFFERS from this value,
463
+ // the replay path neutralizes every reasoning item's account-bound identity
464
+ // (encrypted_content + provider id) in the blob before it reaches the model.
465
+ // Deliberately NO FK: provenance must OUTLIVE the account's hard-disconnect (a
466
+ // stale-but-null tag still mismatches a live codex id, so the strip stays
467
+ // correct either way). NULL on both sides (non-codex freeze + non-codex
468
+ // resume) is a no-op, so single-account and non-codex sessions are unchanged.
469
+ frozenCodexCredentialId: uuid("frozen_codex_credential_id"),
470
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
471
+ });
472
+
473
+ // Conversation truth: ordered, verbatim SDK input items (issue #35). The
474
+ // model-facing memory store — unredacted and replay-ready. session_events
475
+ // remains the redacted human/audit timeline.
476
+ export const sessionHistoryItems = pgTable("session_history_items", {
477
+ id: uuid("id").primaryKey().defaultRandom(),
478
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
479
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
480
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
481
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
482
+ // Numeric (not integer) so the synthetic compaction-summary row can be
483
+ // inserted at a FRACTIONAL position (boundaryPosition - 0.5) that sorts ahead
484
+ // of the kept tail without colliding with — and thus overwriting — the real
485
+ // prefix row at boundaryPosition - 1. Normally-appended rows keep whole-number
486
+ // positions; only the summary uses the half-step. `mode: "number"` maps the
487
+ // postgres.js string back to a JS number so every reader stays numeric.
488
+ position: numeric("position", { mode: "number" }).notNull(),
489
+ item: jsonb("item").$type<Record<string, unknown>>().notNull(),
490
+ // Live-row flag for client-side context compaction. The read path selects
491
+ // only active rows; a compaction supersedes the summarized prefix (sets this
492
+ // false — never deletes, so the full transcript stays as an audit trail) and
493
+ // inserts ONE synthetic active summary row at the boundary. Defaults true so
494
+ // every existing and normally-appended row is live.
495
+ active: boolean("active").notNull().default(true),
496
+ // The Codex account that PRODUCED these items: the per-turn resolved codex
497
+ // credential id (pin > workspace-active), or NULL when produced on the
498
+ // non-codex / Azure path (or before this column existed). Used to strip
499
+ // cross-account `reasoning.encrypted_content` blobs — those are account/org-
500
+ // bound, minted by the ChatGPT/Codex backend, so replaying account A's blob
501
+ // into a turn running on account B 400s. The read path drops the encrypted
502
+ // reasoning of any item whose producer != the turn's current codex account.
503
+ // Deliberately NO FK: provenance must OUTLIVE the account's hard-disconnect
504
+ // (an ON DELETE SET NULL would erase the tag, and a stale-but-null tag still
505
+ // mismatches a live codex id so the strip stays correct either way).
506
+ producerCodexCredentialId: uuid("producer_codex_credential_id"),
507
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
508
+ }, (table) => ({
509
+ positionIdx: uniqueIndex("session_history_items_position_idx").on(table.workspaceId, table.sessionId, table.position),
510
+ }));
511
+
512
+ // Sandbox recovery descriptor, decoupled from the RunState blob: the small
513
+ // versioned envelope (provider handle / snapshot ref / manifest) needed to
514
+ // reattach, restore, or rebuild the session's sandbox on its next turn.
515
+ export const sandboxSessionEnvelopes = pgTable("sandbox_session_envelopes", {
516
+ id: uuid("id").primaryKey().defaultRandom(),
517
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
518
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
519
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
520
+ envelope: jsonb("envelope").$type<Record<string, unknown>>().notNull(),
521
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
522
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
523
+ }, (table) => ({
524
+ sessionIdx: uniqueIndex("sandbox_session_envelopes_session_idx").on(table.workspaceId, table.sessionId),
525
+ }));
526
+
527
+ // The 4 liveness states of the singleton lease. Exported so the query layer and
528
+ // the stateless resume-by-id path share one source of truth for the domain.
529
+ export const sandboxLeaseLivenessValues = ["cold", "warming", "warm", "draining"] as const;
530
+
531
+ // One row per GROUP: the SOLE enforcer of the strict-singleton-box invariant.
532
+ // uniqueIndex(workspaceId, sandboxGroupId) + SELECT…FOR UPDATE + cold->warming
533
+ // CAS + integer lease_epoch fence. Re-keyed to sandboxGroupId from the start
534
+ // (addendum B.2) so today's 1:1 world (sandboxGroupId == session id, set in
535
+ // 0018) is a behavior-preserving no-op. Mirrors the account/workspace FK chain
536
+ // of sandboxSessionEnvelopes; sandboxGroupId is a BARE uuid (NOT an FK — the
537
+ // value is a session id or an ancestor's, and an FK would let a founder's
538
+ // deletion cascade-kill a box still in use by a spawned session).
539
+ export const sandboxLeases = pgTable("sandbox_leases", {
540
+ id: uuid("id").primaryKey().defaultRandom(),
541
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
542
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
543
+ sandboxGroupId: uuid("sandbox_group_id").notNull(),
544
+
545
+ liveness: text("liveness", { enum: sandboxLeaseLivenessValues }).notNull().default("cold"),
546
+ refcount: integer("refcount").notNull().default(0),
547
+ turnHolders: integer("turn_holders").notNull().default(0),
548
+ viewerHolders: integer("viewer_holders").notNull().default(0),
549
+
550
+ instanceId: text("instance_id"),
551
+ backend: text("backend").notNull(),
552
+ os: text("os").notNull().default("linux"),
553
+ // The container IMAGE the group box runs (Modal image ref / docker image). A shared
554
+ // box is SHARED STATE: all its sessions run the SAME filesystem, so they must run the
555
+ // same image. This column stamps the image the live box was created with; a resume
556
+ // whose resolved image DIFFERS is a conflict (B3): a solo holder recreates the box on
557
+ // the new image, N-holders are rejected (SandboxImageConflictError). Nullable — a
558
+ // legacy/cold row reads NULL = "image unknown", which never conflicts.
559
+ image: text("image"),
560
+ dataPlaneUrl: text("data_plane_url"),
561
+ // The REAL PTY terminal (ttyd pty-ws) rides a SEPARATE provider tunnel (7681)
562
+ // from the desktop noVNC (6080), so its resolved URL is cached independently.
563
+ // Recorded under the epoch fence by recordLeaseTerminalDataPlaneUrl; reset to
564
+ // null on every box re-key (warm-commit / fail / drain), symmetric with
565
+ // data_plane_url.
566
+ terminalDataPlaneUrl: text("terminal_data_plane_url"),
567
+
568
+ // integer (NOT bigint): the lease-epoch spike proved a raw int8 read returns a
569
+ // JS STRING from postgres-js, breaking the strict epoch-fence comparison (it
570
+ // was always-true → every turn fenced); int4 returns a JS number, the fix.
571
+ // Epochs never approach 2^31, so the narrower type loses nothing.
572
+ leaseEpoch: integer("lease_epoch").notNull().default(0),
573
+
574
+ // The group box-envelope (the "envelope split" Critical): the small recovery
575
+ // descriptor to resume()-by-id the group's box without a per-session join.
576
+ resumeBackendId: text("resume_backend_id"),
577
+ resumeState: jsonb("resume_state").$type<Record<string, unknown>>(),
578
+
579
+ // Warm-time billing cursor: last_meter_at = accrual cursor; last_meter_tick =
580
+ // idempotency tick (warm_seconds accrued idempotent on
581
+ // (sandbox_group_id, lease_epoch, last_meter_tick) in P2.1).
582
+ lastMeterAt: timestamp("last_meter_at", { withTimezone: true }),
583
+ lastMeterTick: integer("last_meter_tick").notNull().default(0),
584
+
585
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
586
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
587
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
588
+ }, (table) => ({
589
+ groupIdx: uniqueIndex("sandbox_leases_group_idx").on(table.workspaceId, table.sandboxGroupId),
590
+ reaperIdx: index("sandbox_leases_reaper_idx").on(table.expiresAt)
591
+ .where(sql`${table.liveness} in ('warming','warm','draining')`),
592
+ }));
593
+
594
+ // N rows per group: one per live holder. Makes release idempotent
595
+ // (delete-my-row, never blind decrement) and lets the reaper recompute refcount.
596
+ export const sandboxLeaseHolders = pgTable("sandbox_lease_holders", {
597
+ id: uuid("id").primaryKey().defaultRandom(),
598
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
599
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
600
+ leaseId: uuid("lease_id").notNull().references(() => sandboxLeases.id, { onDelete: "cascade" }),
601
+ kind: text("kind", { enum: ["turn", "viewer"] }).notNull(),
602
+ holderId: text("holder_id").notNull(),
603
+ // The attributing session within the (possibly shared) group.
604
+ subjectId: uuid("subject_id"),
605
+ lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }).notNull().defaultNow(),
606
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
607
+ }, (table) => ({
608
+ holderIdx: uniqueIndex("sandbox_lease_holders_holder_idx").on(table.leaseId, table.kind, table.holderId),
609
+ staleIdx: index("sandbox_lease_holders_stale_idx").on(table.kind, table.lastHeartbeatAt),
610
+ leaseIdx: index("sandbox_lease_holders_lease_idx").on(table.leaseId),
611
+ }));
612
+
613
+ // The recording lifecycle states (P4.3). Exported so the activity + the query
614
+ // layer share one source of truth for the §3.1 state machine.
615
+ export const sessionRecordingStateValues = ["recording", "finalizing", "available", "failed"] as const;
616
+ export const sessionRecordingModeValues = ["manual", "on-turn", "on-verify"] as const;
617
+ export const sessionRecordingCodecValues = ["h264-mp4", "vp9-webm"] as const;
618
+
619
+ // One row per recording — the durable index for the "agent films itself proving
620
+ // the fix" loop. ffmpeg x11grab of the SAME :0 humans watch, finalized by
621
+ // reading the bytes off the box and PUTting them to @opengeni/storage in the
622
+ // process that holds the resumed-by-id handle (never a Temporal payload, F10).
623
+ // Mirrors the account/workspace/session FK chain of sandboxSessionEnvelopes;
624
+ // turnId is ON DELETE SET NULL (a deleted turn must not kill the artifact row).
625
+ export const sessionRecordings = pgTable("session_recordings", {
626
+ id: uuid("id").primaryKey().defaultRandom(),
627
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
628
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
629
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
630
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
631
+
632
+ state: text("state", { enum: sessionRecordingStateValues }).notNull(),
633
+ mode: text("mode", { enum: sessionRecordingModeValues }).notNull(),
634
+ codec: text("codec", { enum: sessionRecordingCodecValues }).notNull(),
635
+
636
+ storageKey: text("storage_key"),
637
+ sizeBytes: bigint("size_bytes", { mode: "number" }),
638
+ durationSeconds: numeric("duration_seconds").$type<number>(),
639
+
640
+ width: integer("width").notNull(),
641
+ height: integer("height").notNull(),
642
+
643
+ reason: text("reason"),
644
+
645
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
646
+ finalizedAt: timestamp("finalized_at", { withTimezone: true }),
647
+ }, (table) => ({
648
+ sessionIdx: index("session_recordings_session_idx").on(table.workspaceId, table.sessionId, table.createdAt),
649
+ }));
650
+
651
+ // Channel-A interactive PTY sessions (P4.4 / modules/08-channel-a.md §3.1). The
652
+ // ONLY new persistent state Channel A needs — FS/Git reads are stateless point
653
+ // queries; an interactive PTY is a live in-box process keyed by the SDK's numeric
654
+ // exec-session id (writeStdin({sessionId})). We map our UUID ptyId <-> that id,
655
+ // the owning workspace/session, the lease_epoch that fences it to the box it was
656
+ // opened on (a box re-key strands the PTY -> reaped with reason owner_gone), and
657
+ // a last_input_at heartbeat so the reaper can kill idle/orphaned PTYs. Mirrors
658
+ // the account/workspace/session FK chain of sandboxSessionEnvelopes.
659
+ export const sandboxPtySessions = pgTable("sandbox_pty_sessions", {
660
+ id: uuid("id").primaryKey().defaultRandom(), // == ptyId on the wire
661
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
662
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
663
+ sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
664
+ // The SDK numeric exec-session id used by writeStdin({ sessionId }). Null until
665
+ // the open exec yields a still-running process (a fast-exiting shell has none).
666
+ execSessionId: integer("exec_session_id"),
667
+ leaseEpoch: integer("lease_epoch").notNull(), // fenced to the box that opened it
668
+ cols: integer("cols").notNull(),
669
+ rows: integer("rows").notNull(),
670
+ shell: text("shell").notNull(),
671
+ cwd: text("cwd").notNull(),
672
+ status: text("status").notNull().default("open"), // 'open' | 'closed'
673
+ // The viewer grant/subject that opened it (free-text — access subjects are not
674
+ // always UUIDs, M5; so a text column, never a uuid NOT NULL).
675
+ openedBy: text("opened_by").notNull(),
676
+ lastInputAt: timestamp("last_input_at", { withTimezone: true }).notNull().defaultNow(),
677
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
678
+ closedAt: timestamp("closed_at", { withTimezone: true }),
679
+ }, (table) => ({
680
+ openIdx: index("sandbox_pty_sessions_session_idx")
681
+ .on(table.workspaceId, table.sessionId)
682
+ .where(sql`${table.status} = 'open'`),
683
+ }));
684
+
685
+ // ============================================================================
686
+ // Bring-your-own-compute (M2): first-class swappable sandboxes + enrollment +
687
+ // metrics (migration 0024 / dossier §10.3 + §10.7 + §23). The session→box
688
+ // binding becomes a per-session mutable, epoch-fenced active_sandbox_id pointer
689
+ // (declared on sessions above) that the routing proxy resolves PER TOOL CALL.
690
+
691
+ // The lifecycle/enum domains, exported so the query layer + the migration share
692
+ // ONE source of truth for each CHECK.
693
+ export const enrollmentExposureValues = ["whole-machine"] as const;
694
+ export const enrollmentStatusValues = ["active", "revoked"] as const;
695
+ export const enrollmentOsValues = ["linux", "macos", "windows"] as const;
696
+ export const sandboxKindValues = ["modal", "selfhosted"] as const;
697
+
698
+ // One row per registered machine. The agent's ed25519 PUBLIC key IS the machine
699
+ // identity (the NATS control-plane subject the agent subscribes to maps to it).
700
+ // exposure is the loudly-consented access mode; has_display/allow_screen_control
701
+ // are the desktop/computer-use consent bits (default false — opt-in). status is
702
+ // the active|revoked lifecycle; last_seen_at the heartbeat liveness cursor the
703
+ // Machines dashboard renders online/reconnecting/offline from.
704
+ export const enrollments = pgTable("enrollments", {
705
+ id: uuid("id").primaryKey().defaultRandom(),
706
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
707
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
708
+ // The agent's ed25519 public key (the machine identity).
709
+ pubkey: text("pubkey").notNull(),
710
+ exposure: text("exposure", { enum: enrollmentExposureValues }).notNull().default("whole-machine"),
711
+ hasDisplay: boolean("has_display").notNull().default(false),
712
+ allowScreenControl: boolean("allow_screen_control").notNull().default(false),
713
+ status: text("status", { enum: enrollmentStatusValues }).notNull().default("active"),
714
+ os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
715
+ arch: text("arch").notNull().default("x86_64"),
716
+ // Heartbeat liveness cursor. Null until the first connect.
717
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }),
718
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
719
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
720
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
721
+ }, (table) => ({
722
+ // One enrollment per (workspace, pubkey): a re-enroll is an idempotent upsert.
723
+ workspacePubkey: uniqueIndex("enrollments_workspace_pubkey_idx").on(table.workspaceId, table.pubkey),
724
+ // List a workspace's ACTIVE machines without scanning revoked rows.
725
+ workspaceStatus: index("enrollments_workspace_status_idx").on(table.workspaceId, table.status),
726
+ }));
727
+
728
+ // The OAuth 2.0 device-authorization (RFC 8628) PENDING request (M5, migration
729
+ // 0025 / dossier §10.2 enrollment + §18 LOUD consent). An agent's `enroll` starts
730
+ // a flow (POST /enrollments/device/start) → one short-TTL, single-use row keyed by
731
+ // an opaque `device_code` (the agent polls with) + a short `user_code` (the user
732
+ // types at the approve page). The user (workspace-membership / workspace:admin
733
+ // gated) approves it (POST /enrollments/device/approve), which records WHO
734
+ // (subject + label) consented WHEN (approved_at) to WHAT (whole-machine mandatory +
735
+ // screen-control per allow_screen_control) and stamps the resulting enrollment_id /
736
+ // sandbox_id. The agent then polls (POST /enrollments/device/poll) and the approved
737
+ // row yields the EnrollmentCredentials. State machine: pending → approved | denied;
738
+ // a pending row past expires_at is EXPIRED; once the agent has polled an approved
739
+ // row its credentials, the row flips to consumed (single-use). NOT a long-lived
740
+ // record — a retention sweep prunes terminal rows; the durable identity is the
741
+ // `enrollments` row the approve produced.
742
+ export const deviceEnrollmentStatusValues = ["pending", "approved", "denied", "consumed"] as const;
743
+
744
+ export const deviceEnrollmentRequests = pgTable("device_enrollment_requests", {
745
+ id: uuid("id").primaryKey().defaultRandom(),
746
+ // The opaque code the agent polls with (unguessable, single-use). Unique.
747
+ deviceCode: text("device_code").notNull(),
748
+ // The short human-typed code (e.g. "WDJB-MJHT"). Unique among LIVE (pending)
749
+ // rows via a partial unique index so a recycled code never collides with a
750
+ // terminal row.
751
+ userCode: text("user_code").notNull(),
752
+ // The workspace this request was started for (resolved from the deployment-edge
753
+ // request context — the agent presents the access key, the flow binds to the
754
+ // single managed workspace OR a workspace hint). account_id rides along for RLS.
755
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
756
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
757
+ // The agent's ed25519 public key (the machine identity the enrollment binds to).
758
+ pubkey: text("pubkey").notNull(),
759
+ os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
760
+ arch: text("arch").notNull().default("x86_64"),
761
+ machineName: text("machine_name"),
762
+ // The exposure the agent REQUESTED (whole-machine in v1; loudly consented at
763
+ // approve). Mirrors the enrollment column domain.
764
+ requestedExposure: text("requested_exposure", { enum: enrollmentExposureValues }).notNull().default("whole-machine"),
765
+ // The agent CAN offer a display (a real screen / Xvfb is available) — gates
766
+ // whether screen-control consent is even meaningful. has_display on the
767
+ // resulting enrollment is derived from this.
768
+ canOfferDisplay: boolean("can_offer_display").notNull().default(false),
769
+ // The agent REQUESTS screen control (computer-use). The user's allow_screen_control
770
+ // at approve is the AUTHORITATIVE consent; this is only the agent's request.
771
+ requestsScreenControl: boolean("requests_screen_control").notNull().default(false),
772
+ status: text("status", { enum: deviceEnrollmentStatusValues }).notNull().default("pending"),
773
+ // ── LOUD CONSENT capture (who/when/what), stamped at approve ──────────────
774
+ approvedBySubjectId: text("approved_by_subject_id"),
775
+ approvedBySubjectLabel: text("approved_by_subject_label"),
776
+ // The user's screen-control consent decision (whole-machine is mandatory at
777
+ // approve; screen-control is opt-in per this flag).
778
+ allowScreenControl: boolean("allow_screen_control").notNull().default(false),
779
+ approvedAt: timestamp("approved_at", { withTimezone: true }),
780
+ // The enrollment + sandbox the approve produced (acceptance #2: an enrollment
781
+ // row AND a sandbox row appear). Null until approved.
782
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
783
+ sandboxId: uuid("sandbox_id").references(() => sandboxes.id, { onDelete: "set null" }),
784
+ // The short-TTL expiry; a pending row past this is EXPIRED on poll.
785
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
786
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
787
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
788
+ }, (table) => ({
789
+ // The device_code is the agent's poll key — globally unique + indexed.
790
+ deviceCode: uniqueIndex("device_enrollment_requests_device_code_idx").on(table.deviceCode),
791
+ // The user_code must be unique among LIVE (pending) rows so the approve lookup
792
+ // is unambiguous; a terminal row's code may be recycled.
793
+ userCodePending: uniqueIndex("device_enrollment_requests_user_code_pending_idx")
794
+ .on(table.userCode)
795
+ .where(sql`${table.status} = 'pending'`),
796
+ workspaceCreated: index("device_enrollment_requests_workspace_created_idx").on(table.workspaceId, table.createdAt),
797
+ expires: index("device_enrollment_requests_expires_idx").on(table.expiresAt),
798
+ }));
799
+
800
+ // The first-class NAMED sandbox a session's active_sandbox_id points AT. kind
801
+ // discriminates the backend the routing proxy resolves to: 'modal' (cloud box,
802
+ // NULL enrollment_id) or 'selfhosted' (a user's machine, enrollment_id -> the
803
+ // enrollment it lives on). The selfhosted-needs-enrollment invariant is pinned by
804
+ // the sandboxes_selfhosted_enrollment_chk CHECK in migration 0024. enrollment_id
805
+ // is ON DELETE SET NULL so deleting an enrollment never cascade-kills a sandbox a
806
+ // session might still point at (the routing layer surfaces agent_offline instead).
807
+ export const sandboxes = pgTable("sandboxes", {
808
+ id: uuid("id").primaryKey().defaultRandom(),
809
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
810
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
811
+ kind: text("kind", { enum: sandboxKindValues }).notNull(),
812
+ name: text("name").notNull(),
813
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
814
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
815
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
816
+ }, (table) => ({
817
+ workspaceCreated: index("sandboxes_workspace_created_idx").on(table.workspaceId, table.createdAt),
818
+ enrollment: index("sandboxes_enrollment_idx").on(table.enrollmentId).where(sql`${table.enrollmentId} is not null`),
819
+ }));
820
+
821
+ // Last-sample upsert: ONE row per enrollment, overwritten on every sample (the
822
+ // PK on enrollment_id is the ON CONFLICT target). The §10.7 signals; nullable
823
+ // where a platform/sample may not provide it (no GPU, headless).
824
+ export const machineMetricsLatest = pgTable("machine_metrics_latest", {
825
+ enrollmentId: uuid("enrollment_id").primaryKey().references(() => enrollments.id, { onDelete: "cascade" }),
826
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
827
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
828
+ cpuPercent: numeric("cpu_percent").$type<number>(),
829
+ load1: numeric("load1").$type<number>(),
830
+ load5: numeric("load5").$type<number>(),
831
+ load15: numeric("load15").$type<number>(),
832
+ memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
833
+ memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
834
+ diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
835
+ diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
836
+ gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
837
+ gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
838
+ gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
839
+ contention: numeric("contention").$type<number>(),
840
+ sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
841
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
842
+ }, (table) => ({
843
+ workspace: index("machine_metrics_latest_workspace_idx").on(table.workspaceId),
844
+ }));
845
+
846
+ // Append-only downsampled history (~1/min per enrollment, retained N days). Same
847
+ // signal columns as _latest. The (enrollment_id, sampled_at) index serves the
848
+ // dashboard time-range read AND the (later) retention sweep.
849
+ export const machineMetricsSeries = pgTable("machine_metrics_series", {
850
+ id: uuid("id").primaryKey().defaultRandom(),
851
+ enrollmentId: uuid("enrollment_id").notNull().references(() => enrollments.id, { onDelete: "cascade" }),
852
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
853
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
854
+ cpuPercent: numeric("cpu_percent").$type<number>(),
855
+ load1: numeric("load1").$type<number>(),
856
+ load5: numeric("load5").$type<number>(),
857
+ load15: numeric("load15").$type<number>(),
858
+ memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
859
+ memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
860
+ diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
861
+ diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
862
+ gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
863
+ gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
864
+ gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
865
+ contention: numeric("contention").$type<number>(),
866
+ sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
867
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
868
+ }, (table) => ({
869
+ enrollmentSampled: index("machine_metrics_series_enrollment_sampled_idx").on(table.enrollmentId, table.sampledAt),
870
+ sampled: index("machine_metrics_series_sampled_idx").on(table.sampledAt),
871
+ }));
872
+
873
+ export const scheduledTasks = pgTable("scheduled_tasks", {
874
+ id: uuid("id").primaryKey().defaultRandom(),
875
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
876
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
877
+ name: text("name").notNull(),
878
+ status: text("status").notNull().default("active"),
879
+ schedule: jsonb("schedule").$type<unknown>().notNull(),
880
+ temporalScheduleId: text("temporal_schedule_id").notNull(),
881
+ runMode: text("run_mode").notNull().default("new_session_per_run"),
882
+ overlapPolicy: text("overlap_policy").notNull().default("allow_concurrent"),
883
+ agentConfig: jsonb("agent_config").$type<unknown>().notNull(),
884
+ reusableSessionId: uuid("reusable_session_id").references(() => sessions.id, { onDelete: "set null" }),
885
+ environmentId: uuid("environment_id").references(() => workspaceEnvironments.id, { onDelete: "restrict" }),
886
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
887
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
888
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
889
+ }, (table) => ({
890
+ temporalScheduleId: uniqueIndex("scheduled_tasks_workspace_temporal_schedule_id_idx").on(table.workspaceId, table.temporalScheduleId),
891
+ status: index("scheduled_tasks_workspace_status_idx").on(table.workspaceId, table.status),
892
+ environment: index("scheduled_tasks_environment_idx").on(table.workspaceId, table.environmentId),
893
+ }));
894
+
895
+ export const scheduledTaskRuns = pgTable("scheduled_task_runs", {
896
+ id: uuid("id").primaryKey().defaultRandom(),
897
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
898
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
899
+ taskId: uuid("task_id").notNull().references(() => scheduledTasks.id, { onDelete: "cascade" }),
900
+ status: text("status").notNull().default("queued"),
901
+ triggerType: text("trigger_type").notNull(),
902
+ scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
903
+ firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(),
904
+ sessionId: uuid("session_id").references(() => sessions.id, { onDelete: "set null" }),
905
+ triggerEventId: uuid("trigger_event_id"),
906
+ error: text("error"),
907
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
908
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
909
+ }, (table) => ({
910
+ taskCreated: index("scheduled_task_runs_workspace_task_created_idx").on(table.workspaceId, table.taskId, table.createdAt),
911
+ session: index("scheduled_task_runs_workspace_session_idx").on(table.workspaceId, table.sessionId),
912
+ }));
913
+
914
+ export const githubInstallations = pgTable("github_installations", {
915
+ id: uuid("id").primaryKey().defaultRandom(),
916
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
917
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
918
+ installationId: integer("installation_id").notNull(),
919
+ accountLogin: text("account_login"),
920
+ accountType: text("account_type"),
921
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
922
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
923
+ }, (table) => ({
924
+ workspaceInstallation: uniqueIndex("github_installations_workspace_installation_idx").on(table.workspaceId, table.installationId),
925
+ installation: index("github_installations_installation_idx").on(table.installationId),
926
+ workspace: index("github_installations_workspace_idx").on(table.workspaceId),
927
+ }));
928
+
929
+ export const usageEvents = pgTable("usage_events", {
930
+ id: uuid("id").primaryKey().defaultRandom(),
931
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
932
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
933
+ subjectId: text("subject_id"),
934
+ eventType: text("event_type").notNull(),
935
+ quantity: bigint("quantity", { mode: "number" }).notNull(),
936
+ unit: text("unit").notNull(),
937
+ sourceResourceType: text("source_resource_type"),
938
+ sourceResourceId: text("source_resource_id"),
939
+ idempotencyKey: text("idempotency_key").notNull(),
940
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
941
+ recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(),
942
+ exportedToBillingAt: timestamp("exported_to_billing_at", { withTimezone: true }),
943
+ billingProviderEventId: text("billing_provider_event_id"),
944
+ }, (table) => ({
945
+ idempotency: uniqueIndex("usage_events_idempotency_idx").on(table.idempotencyKey),
946
+ workspaceMetric: index("usage_events_workspace_metric_idx").on(table.workspaceId, table.eventType, table.occurredAt),
947
+ accountMetric: index("usage_events_account_metric_idx").on(table.accountId, table.eventType, table.occurredAt),
948
+ }));
949
+
950
+ export const creditLedgerEntries = pgTable("credit_ledger_entries", {
951
+ id: uuid("id").primaryKey().defaultRandom(),
952
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
953
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
954
+ type: text("type").notNull(),
955
+ amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
956
+ currency: text("currency").notNull().default("usd"),
957
+ sourceType: text("source_type"),
958
+ sourceId: text("source_id"),
959
+ idempotencyKey: text("idempotency_key").notNull(),
960
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
961
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
962
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
963
+ }, (table) => ({
964
+ idempotency: uniqueIndex("credit_ledger_entries_idempotency_idx").on(table.idempotencyKey),
965
+ accountCreated: index("credit_ledger_entries_account_created_idx").on(table.accountId, table.createdAt),
966
+ }));
967
+
968
+ export const billingCustomers = pgTable("billing_customers", {
969
+ id: uuid("id").primaryKey().defaultRandom(),
970
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
971
+ provider: text("provider").notNull().default("stripe"),
972
+ providerCustomerId: text("provider_customer_id").notNull(),
973
+ email: text("email"),
974
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
975
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
976
+ }, (table) => ({
977
+ accountProvider: uniqueIndex("billing_customers_account_provider_idx").on(table.accountId, table.provider),
978
+ providerCustomer: uniqueIndex("billing_customers_provider_customer_idx").on(table.provider, table.providerCustomerId),
979
+ }));
980
+
981
+ export const stripeWebhookEvents = pgTable("stripe_webhook_events", {
982
+ id: text("id").primaryKey(),
983
+ type: text("type").notNull(),
984
+ livemode: text("livemode").notNull().default("false"),
985
+ payload: jsonb("payload").$type<unknown>().notNull(),
986
+ processedAt: timestamp("processed_at", { withTimezone: true }),
987
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
988
+ });
989
+
990
+ export const auditEvents = pgTable("audit_events", {
991
+ id: uuid("id").primaryKey().defaultRandom(),
992
+ accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "set null" }),
993
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
994
+ subjectId: text("subject_id"),
995
+ action: text("action").notNull(),
996
+ targetType: text("target_type"),
997
+ targetId: text("target_id"),
998
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
999
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1000
+ }, (table) => ({
1001
+ accountCreated: index("audit_events_account_created_idx").on(table.accountId, table.occurredAt),
1002
+ workspaceCreated: index("audit_events_workspace_created_idx").on(table.workspaceId, table.occurredAt),
1003
+ }));
1004
+
1005
+ export const packInstallations = pgTable("pack_installations", {
1006
+ id: uuid("id").primaryKey().defaultRandom(),
1007
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1008
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1009
+ packId: text("pack_id").notNull(),
1010
+ status: text("status").notNull().default("active"),
1011
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1012
+ enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
1013
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1014
+ }, (table) => ({
1015
+ workspacePack: uniqueIndex("pack_installations_workspace_pack_idx").on(table.workspaceId, table.packId),
1016
+ status: index("pack_installations_workspace_status_idx").on(table.workspaceId, table.status),
1017
+ }));
1018
+
1019
+ export const workspacePacks = pgTable("workspace_packs", {
1020
+ id: uuid("id").primaryKey().defaultRandom(),
1021
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1022
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1023
+ packId: text("pack_id").notNull(),
1024
+ manifest: jsonb("manifest").$type<Record<string, unknown>>().notNull(),
1025
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1026
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1027
+ }, (table) => ({
1028
+ workspacePack: uniqueIndex("workspace_packs_workspace_pack_idx").on(table.workspaceId, table.packId),
1029
+ }));
1030
+
1031
+ export const capabilityCatalogItems = pgTable("capability_catalog_items", {
1032
+ id: text("id").notNull(),
1033
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1034
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1035
+ kind: text("kind").notNull(),
1036
+ source: text("source").notNull().default("manual"),
1037
+ name: text("name").notNull(),
1038
+ description: text("description"),
1039
+ category: text("category").notNull().default("custom"),
1040
+ tags: jsonb("tags").$type<string[]>().notNull().default([]),
1041
+ homepageUrl: text("homepage_url"),
1042
+ endpointUrl: text("endpoint_url"),
1043
+ installUrl: text("install_url"),
1044
+ authModel: text("auth_model"),
1045
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1046
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1047
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1048
+ }, (table) => ({
1049
+ workspaceCapability: uniqueIndex("capability_catalog_items_workspace_capability_idx").on(table.workspaceId, table.id),
1050
+ kind: index("capability_catalog_items_workspace_kind_idx").on(table.workspaceId, table.kind),
1051
+ category: index("capability_catalog_items_workspace_category_idx").on(table.workspaceId, table.category),
1052
+ source: index("capability_catalog_items_workspace_source_idx").on(table.workspaceId, table.source),
1053
+ }));
1054
+
1055
+ export const capabilityInstallations = pgTable("capability_installations", {
1056
+ id: uuid("id").primaryKey().defaultRandom(),
1057
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1058
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1059
+ capabilityId: text("capability_id").notNull(),
1060
+ kind: text("kind").notNull(),
1061
+ status: text("status").notNull().default("active"),
1062
+ config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
1063
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1064
+ enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
1065
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1066
+ }, (table) => ({
1067
+ workspaceCapability: uniqueIndex("capability_installations_workspace_capability_idx").on(table.workspaceId, table.capabilityId),
1068
+ kind: index("capability_installations_workspace_kind_idx").on(table.workspaceId, table.kind),
1069
+ status: index("capability_installations_workspace_status_idx").on(table.workspaceId, table.status),
1070
+ }));
1071
+
1072
+ export const socialConnections = pgTable("social_connections", {
1073
+ id: uuid("id").primaryKey().defaultRandom(),
1074
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1075
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1076
+ provider: text("provider").notNull(),
1077
+ accountHandle: text("account_handle").notNull(),
1078
+ accountName: text("account_name"),
1079
+ externalAccountId: text("external_account_id"),
1080
+ status: text("status").notNull().default("connected"),
1081
+ scopes: jsonb("scopes").$type<string[]>().notNull().default([]),
1082
+ credentialRef: text("credential_ref"),
1083
+ tokenMetadata: jsonb("token_metadata").$type<Record<string, unknown>>().notNull().default({}),
1084
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1085
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1086
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1087
+ }, (table) => ({
1088
+ workspaceProviderHandle: uniqueIndex("social_connections_workspace_provider_handle_idx").on(table.workspaceId, table.provider, table.accountHandle),
1089
+ providerStatus: index("social_connections_workspace_provider_status_idx").on(table.workspaceId, table.provider, table.status),
1090
+ }));
1091
+
1092
+ export const socialPosts = pgTable("social_posts", {
1093
+ id: uuid("id").primaryKey().defaultRandom(),
1094
+ accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1095
+ workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1096
+ connectionId: uuid("connection_id").notNull().references(() => socialConnections.id, { onDelete: "cascade" }),
1097
+ provider: text("provider").notNull(),
1098
+ externalPostId: text("external_post_id"),
1099
+ url: text("url"),
1100
+ authorHandle: text("author_handle"),
1101
+ text: text("text").notNull(),
1102
+ publishedAt: timestamp("published_at", { withTimezone: true }).notNull(),
1103
+ metrics: jsonb("metrics").$type<Record<string, number>>().notNull().default({}),
1104
+ raw: jsonb("raw").$type<Record<string, unknown>>().notNull().default({}),
1105
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1106
+ }, (table) => ({
1107
+ connectionExternalPost: uniqueIndex("social_posts_workspace_connection_external_post_idx").on(table.workspaceId, table.connectionId, table.externalPostId),
1108
+ connectionPublished: index("social_posts_workspace_connection_published_idx").on(table.workspaceId, table.connectionId, table.publishedAt),
1109
+ providerPublished: index("social_posts_workspace_provider_published_idx").on(table.workspaceId, table.provider, table.publishedAt),
1110
+ }));