@opengeni/db 0.6.1 → 0.7.1

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 (53) hide show
  1. package/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
  2. package/dist/chunk-7LDU7F5P.js.map +1 -0
  3. package/dist/chunk-B22X3IEZ.js +2634 -0
  4. package/dist/chunk-B22X3IEZ.js.map +1 -0
  5. package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
  6. package/dist/chunk-YFQ7SGE4.js.map +1 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +16571 -5018
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.js +1 -1
  11. package/dist/provision-roles.d.ts +2121 -248
  12. package/dist/provision-roles.js +1 -1
  13. package/dist/{schema-BUbuMteO.d.ts → schema-BN5mB9xZ.d.ts} +8716 -3432
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +43 -5
  16. package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
  17. package/drizzle/0045_workspace_captures.sql +83 -0
  18. package/drizzle/0045_workspace_memory_v1.sql +71 -0
  19. package/drizzle/0046_variable_sets_rename.sql +56 -0
  20. package/drizzle/0047_rigs.sql +151 -0
  21. package/drizzle/0048_rig_runtime.sql +9 -0
  22. package/drizzle/0049_enrollment_went_offline.sql +28 -0
  23. package/drizzle/0050_enrollment_op_stream.sql +1 -0
  24. package/drizzle/0051_codex_pin_source.sql +48 -0
  25. package/drizzle/0052_file_upload_cleanup.sql +91 -0
  26. package/drizzle/0053_codex_credential_leases.sql +230 -0
  27. package/drizzle/0054_session_pins.sql +85 -0
  28. package/drizzle/0055_session_list_snapshots.sql +73 -0
  29. package/drizzle/0056_workspace_model_policies.sql +48 -0
  30. package/drizzle/0057_durable_queue_control.sql +536 -0
  31. package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
  32. package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
  33. package/drizzle/0060_session_system_update_deferral.sql +10 -0
  34. package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
  35. package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
  36. package/drizzle/0063_session_control_mega_foundation.sql +1324 -0
  37. package/package.json +13 -13
  38. package/src/codex-token-resolver.ts +58 -23
  39. package/src/connection-token-resolver.ts +146 -57
  40. package/src/environment-crypto.ts +5 -1
  41. package/src/event-payload-sanitizer.ts +29 -1
  42. package/src/index.ts +20990 -6465
  43. package/src/memory-domain.ts +218 -0
  44. package/src/migrate.ts +58 -3
  45. package/src/provision-roles.ts +46 -17
  46. package/src/schema.ts +2888 -1121
  47. package/src/session-control.ts +1759 -0
  48. package/src/session-queue-commands.ts +1753 -0
  49. package/src/session-tool-call-settlement.ts +269 -0
  50. package/dist/chunk-57MLICFR.js.map +0 -1
  51. package/dist/chunk-OGCE6O2X.js.map +0 -1
  52. package/dist/chunk-ZIUCA2IO.js +0 -1268
  53. package/dist/chunk-ZIUCA2IO.js.map +0 -1
package/src/schema.ts CHANGED
@@ -1,5 +1,20 @@
1
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";
2
+ import {
3
+ bigint,
4
+ boolean,
5
+ check,
6
+ foreignKey,
7
+ index,
8
+ integer,
9
+ jsonb,
10
+ numeric,
11
+ pgTable,
12
+ text,
13
+ timestamp,
14
+ uniqueIndex,
15
+ uuid,
16
+ customType,
17
+ } from "drizzle-orm/pg-core";
3
18
 
4
19
  const vector = customType<{ data: number[]; driverData: string }>({
5
20
  dataType() {
@@ -10,216 +25,385 @@ const vector = customType<{ data: number[]; driverData: string }>({
10
25
  },
11
26
  });
12
27
 
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
- }));
28
+ export const managedAccounts = pgTable(
29
+ "managed_accounts",
30
+ {
31
+ id: uuid("id").primaryKey().defaultRandom(),
32
+ name: text("name").notNull(),
33
+ externalSource: text("external_source"),
34
+ externalId: text("external_id"),
35
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
36
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
37
+ },
38
+ (table) => ({
39
+ external: uniqueIndex("managed_accounts_external_idx").on(
40
+ table.externalSource,
41
+ table.externalId,
42
+ ),
43
+ }),
44
+ );
45
+
46
+ export const workspaces = pgTable(
47
+ "workspaces",
48
+ {
49
+ id: uuid("id").primaryKey().defaultRandom(),
50
+ accountId: uuid("account_id")
51
+ .notNull()
52
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
53
+ name: text("name").notNull(),
54
+ slug: text("slug"),
55
+ externalSource: text("external_source"),
56
+ externalId: text("external_id"),
57
+ // White-label agent persona template override. NULL means the deployment
58
+ // default (OPENGENI_AGENT_INSTRUCTIONS_TEMPLATE / DEFAULT_AGENT_INSTRUCTIONS).
59
+ agentInstructions: text("agent_instructions"),
60
+ // Growth-ready per-workspace settings bag (migration 0045). Holds memoryEnabled
61
+ // and future workspace-level toggles; validated/merged via WorkspaceSettingsSchema.
62
+ settings: jsonb("settings").$type<Record<string, unknown>>().notNull().default({}),
63
+ // The workspace's default rig (migration 0047). NULL ⇒ no default; sessions
64
+ // created without an explicit rig ride no rig (today's behavior exactly). FK
65
+ // (-> rigs(id) ON DELETE SET NULL) lives in migration 0047, not a Drizzle
66
+ // .references(), because `rigs` is declared later in this file (same
67
+ // forward-reference pattern as sessions.activeSandboxId). Consumed in M3.
68
+ defaultRigId: uuid("default_rig_id"),
69
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
70
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
71
+ },
72
+ (table) => ({
73
+ account: index("workspaces_account_idx").on(table.accountId),
74
+ accountSlug: uniqueIndex("workspaces_account_slug_idx")
75
+ .on(table.accountId, table.slug)
76
+ .where(sql`${table.slug} is not null`),
77
+ external: uniqueIndex("workspaces_external_idx").on(table.externalSource, table.externalId),
78
+ }),
79
+ );
80
+
81
+ // One mandatory workspace-wide admission barrier. Every inference-admitting
82
+ // transaction locks this row before it touches a session; Pause/Resume and
83
+ // foreground Send/Steer advance its monotonic revision under FOR UPDATE.
84
+ export const workspaceInferenceControls = pgTable(
85
+ "workspace_inference_controls",
86
+ {
87
+ workspaceId: uuid("workspace_id").primaryKey(),
88
+ accountId: uuid("account_id").notNull(),
89
+ revision: bigint("revision", { mode: "number" }).notNull().default(0),
90
+ workspaceState: text("workspace_state").notNull().default("active"),
91
+ workspacePauseRevision: bigint("workspace_pause_revision", { mode: "number" }),
92
+ reason: text("reason"),
93
+ changedBy: text("changed_by"),
94
+ changedAt: timestamp("changed_at", { withTimezone: true }),
95
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
96
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
97
+ },
98
+ (table) => ({
99
+ workspaceAccount: foreignKey({
100
+ name: "workspace_inference_controls_workspace_account_fk",
101
+ columns: [table.workspaceId, table.accountId],
102
+ foreignColumns: [workspaces.id, workspaces.accountId],
103
+ }).onDelete("cascade"),
104
+ workspaceAccountIdentity: uniqueIndex("workspace_inference_controls_workspace_account_uq").on(
105
+ table.workspaceId,
106
+ table.accountId,
107
+ ),
108
+ stateValid: check(
109
+ "workspace_inference_controls_state_check",
110
+ sql`${table.workspaceState} in ('active', 'paused')`,
111
+ ),
112
+ pauseRevisionConsistent: check(
113
+ "workspace_inference_controls_pause_revision_check",
114
+ sql`(${table.workspaceState} = 'active' and ${table.workspacePauseRevision} is null)
115
+ or (${table.workspaceState} = 'paused' and ${table.workspacePauseRevision} is not null)`,
116
+ ),
117
+ revisionValid: check(
118
+ "workspace_inference_controls_revision_check",
119
+ sql`${table.revision} >= 0 and (${table.workspacePauseRevision} is null or ${table.workspacePauseRevision} <= ${table.revision})`,
120
+ ),
121
+ }),
122
+ );
123
+
124
+ export const workspaceMemberships = pgTable(
125
+ "workspace_memberships",
126
+ {
127
+ id: uuid("id").primaryKey().defaultRandom(),
128
+ accountId: uuid("account_id")
129
+ .notNull()
130
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
131
+ workspaceId: uuid("workspace_id")
132
+ .notNull()
133
+ .references(() => workspaces.id, { onDelete: "cascade" }),
134
+ subjectId: text("subject_id").notNull(),
135
+ subjectLabel: text("subject_label"),
136
+ role: text("role").notNull().default("member"),
137
+ permissions: jsonb("permissions").$type<string[]>().notNull().default([]),
138
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
139
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
140
+ },
141
+ (table) => ({
142
+ subjectWorkspace: uniqueIndex("workspace_memberships_subject_workspace_idx").on(
143
+ table.subjectId,
144
+ table.workspaceId,
145
+ ),
146
+ subject: index("workspace_memberships_subject_idx").on(table.subjectId),
147
+ account: index("workspace_memberships_account_idx").on(table.accountId),
148
+ }),
149
+ );
150
+
151
+ export const apiKeys = pgTable(
152
+ "api_keys",
153
+ {
154
+ id: uuid("id").primaryKey().defaultRandom(),
155
+ accountId: uuid("account_id")
156
+ .notNull()
157
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
158
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
159
+ name: text("name").notNull(),
160
+ prefix: text("prefix").notNull(),
161
+ keyHash: text("key_hash").notNull(),
162
+ permissions: jsonb("permissions").$type<string[]>().notNull().default([]),
163
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
164
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
165
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
166
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
167
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
168
+ },
169
+ (table) => ({
170
+ prefix: index("api_keys_prefix_idx").on(table.prefix),
171
+ hash: uniqueIndex("api_keys_key_hash_idx").on(table.keyHash),
172
+ account: index("api_keys_account_idx").on(table.accountId),
173
+ workspace: index("api_keys_workspace_idx").on(table.workspaceId),
174
+ }),
175
+ );
176
+
177
+ export const workspaceVariableSets = pgTable(
178
+ "workspace_variable_sets",
179
+ {
180
+ id: uuid("id").primaryKey().defaultRandom(),
181
+ accountId: uuid("account_id")
182
+ .notNull()
183
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
184
+ workspaceId: uuid("workspace_id")
185
+ .notNull()
186
+ .references(() => workspaces.id, { onDelete: "cascade" }),
187
+ name: text("name").notNull(),
188
+ description: text("description"),
189
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
190
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
191
+ },
192
+ (table) => ({
193
+ workspaceName: uniqueIndex("workspace_variable_sets_workspace_name_idx").on(
194
+ table.workspaceId,
195
+ table.name,
196
+ ),
197
+ workspaceCreated: index("workspace_variable_sets_workspace_created_idx").on(
198
+ table.workspaceId,
199
+ table.createdAt,
200
+ ),
201
+ }),
202
+ );
203
+
204
+ export const workspaceVariableSetVariables = pgTable(
205
+ "workspace_variable_set_variables",
206
+ {
207
+ id: uuid("id").primaryKey().defaultRandom(),
208
+ accountId: uuid("account_id")
209
+ .notNull()
210
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
211
+ workspaceId: uuid("workspace_id")
212
+ .notNull()
213
+ .references(() => workspaces.id, { onDelete: "cascade" }),
214
+ variableSetId: uuid("variable_set_id")
215
+ .notNull()
216
+ .references(() => workspaceVariableSets.id, { onDelete: "cascade" }),
217
+ name: text("name").notNull(),
218
+ // Format: v1:<base64 iv>:<base64 ciphertext||gcm-tag>. Never returned by any API.
219
+ valueEncrypted: text("value_encrypted").notNull(),
220
+ version: integer("version").notNull().default(1),
221
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
222
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
223
+ },
224
+ (table) => ({
225
+ variableSetName: uniqueIndex("workspace_variable_set_variables_env_name_idx").on(
226
+ table.workspaceId,
227
+ table.variableSetId,
228
+ table.name,
229
+ ),
230
+ variableSet: index("workspace_variable_set_variables_workspace_env_idx").on(
231
+ table.workspaceId,
232
+ table.variableSetId,
233
+ ),
234
+ }),
235
+ );
106
236
 
107
237
  // Per-workspace ChatGPT/Codex subscription credential. One row per workspace.
108
238
  // access/refresh/id tokens live INSIDE credential_encrypted (v1 AES-256-GCM,
109
- // same envelope as workspace_environment_variables); the other columns are
239
+ // same envelope as workspace_variable_set_variables); the other columns are
110
240
  // 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
- }));
241
+ export const codexSubscriptionCredentials = pgTable(
242
+ "codex_subscription_credentials",
243
+ {
244
+ id: uuid("id").primaryKey().defaultRandom(),
245
+ accountId: uuid("account_id")
246
+ .notNull()
247
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
248
+ workspaceId: uuid("workspace_id")
249
+ .notNull()
250
+ .references(() => workspaces.id, { onDelete: "cascade" }),
251
+ // Format: v1:<base64 iv>:<base64 ciphertext||gcm-tag>. JSON {access_token, refresh_token, id_token}. Never returned by any API.
252
+ credentialEncrypted: text("credential_encrypted").notNull(),
253
+ chatgptAccountId: text("chatgpt_account_id"), // plaintext ChatGPT-Account-ID header value (non-secret)
254
+ scopes: text("scopes"), // space-delimited, as granted
255
+ planType: text("plan_type"),
256
+ isFedramp: boolean("is_fedramp").notNull().default(false),
257
+ expiresAt: timestamp("expires_at", { withTimezone: true }), // derived from access-token JWT exp
258
+ lastRefreshAt: timestamp("last_refresh_at", { withTimezone: true }),
259
+ status: text("status").notNull().default("active"), // active | needs_relogin | error
260
+ lastError: text("last_error"),
261
+ version: integer("version").notNull().default(1),
262
+ label: text("label"), // user-chosen nickname; null ⇒ derive from email/plan/account
263
+ accountEmail: text("account_email"), // email from the id_token (user's own email; non-secret)
264
+ // P2 usage cache (plaintext metadata; NEVER a token). Snapshotted from
265
+ // GET /wham/usage; drives the quota bars + the cache TTL. primary = 5h window
266
+ // (limit_window_seconds 18000), secondary = weekly (604800).
267
+ primaryUsedPercent: integer("primary_used_percent"),
268
+ primaryResetAt: timestamp("primary_reset_at", { withTimezone: true }),
269
+ secondaryUsedPercent: integer("secondary_used_percent"),
270
+ secondaryResetAt: timestamp("secondary_reset_at", { withTimezone: true }),
271
+ usageCheckedAt: timestamp("usage_checked_at", { withTimezone: true }), // snapshot freshness → cache TTL clock
272
+ // P3 rotation cooldown (plaintext metadata; NEVER a token). Set when this account hit its
273
+ // usage cap on a rotation turn; the rotation engine treats `exhausted_until > now()` as
274
+ // capped/skip so it isn't immediately re-picked. Self-clears via the now() comparison.
275
+ exhaustedUntil: timestamp("exhausted_until", { withTimezone: true }),
276
+ // P4 connector-aware rotation cache (plaintext metadata; NEVER a token). The set
277
+ // of ORIGINAL-dotted connector namespaces (github/gmail/linear/…) this account
278
+ // exposes via codex_apps, captured from the per-turn tools/list. null ⇒ never
279
+ // probed (the ranker treats it as unknown: never credited as covering, never
280
+ // excluded). The writer only ever sets a NON-empty set, so a flaky empty turn
281
+ // can't false-drop coverage. connectorsCheckedAt is the freshness clock.
282
+ connectorNamespaces: text("connector_namespaces").array(),
283
+ connectorsCheckedAt: timestamp("connectors_checked_at", { withTimezone: true }),
284
+ // Workspace-local, server-held fairness cursor. Provider usage headers are
285
+ // capacity hints, never the sole allocator: live lease count is ranked first
286
+ // and this cursor deterministically breaks equal-load/equal-capacity ties.
287
+ // This flag controls NEW automatic allocations only. Credential health,
288
+ // refresh, encrypted material, and already-frozen/in-flight turns are
289
+ // intentionally independent. OPE-24 owns toggle OCC/audit and product UI.
290
+ allocatorEnabled: boolean("allocator_enabled").notNull().default(true),
291
+ selectionCount: integer("selection_count").notNull().default(0),
292
+ lastSelectedAt: timestamp("last_selected_at", { withTimezone: true }),
293
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
294
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
295
+ },
296
+ (table) => ({
297
+ // REPLACES codex_subscription_credentials_workspace_idx (the one-per-workspace cap).
298
+ // One row per (workspace, ChatGPT account). Partial WHERE chatgpt_account_id IS NOT NULL
299
+ // so degenerate null-account rows can't collide; the device-grant connect path always
300
+ // populates chatgpt_account_id.
301
+ wsAccount: uniqueIndex("codex_subscription_credentials_ws_account_idx")
302
+ .on(table.workspaceId, table.chatgptAccountId)
303
+ .where(sql`${table.chatgptAccountId} is not null`),
304
+ workspace: index("codex_subscription_credentials_workspace_lookup_idx").on(table.workspaceId),
305
+ // Composite identity is the defense-in-depth FK target for workspace-local
306
+ // lease references in migration 0053.
307
+ workspaceIdentity: uniqueIndex("codex_subscription_credentials_workspace_id_idx").on(
308
+ table.workspaceId,
309
+ table.id,
310
+ ),
311
+ }),
312
+ );
160
313
 
161
314
  // Generic external-service credential spine. credential_encrypted is the ONLY
162
315
  // secret-bearing column; normal API reads use metadata-only helpers below the DB
163
316
  // layer. Runtime token material is decrypted only by the broker accessor.
164
- export const connections = pgTable("connections", {
165
- id: uuid("id").primaryKey().defaultRandom(),
166
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
167
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
168
- subjectId: text("subject_id"),
169
- providerDomain: text("provider_domain").notNull(),
170
- kind: text("kind").notNull(),
171
- status: text("status").notNull().default("active"),
172
- credentialEncrypted: text("credential_encrypted").notNull(),
173
- grantedScopes: jsonb("granted_scopes").$type<string[]>().notNull().default([]),
174
- expiresAt: timestamp("expires_at", { withTimezone: true }),
175
- lastRefreshAt: timestamp("last_refresh_at", { withTimezone: true }),
176
- lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
177
- lastError: text("last_error"),
178
- version: integer("version").notNull().default(1),
179
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
180
- createdBySubjectId: text("created_by_subject_id"),
181
- updatedBySubjectId: text("updated_by_subject_id"),
182
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
183
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
184
- }, (table) => ({
185
- workspaceProviderStatus: index("connections_workspace_provider_status_idx").on(table.workspaceId, table.providerDomain, table.status),
186
- workspaceSubjectProvider: index("connections_workspace_subject_provider_idx").on(table.workspaceId, table.subjectId, table.providerDomain),
187
- workspaceKind: index("connections_workspace_kind_idx").on(table.workspaceId, table.kind),
188
- workspaceExpires: index("connections_workspace_expires_idx").on(table.workspaceId, table.expiresAt),
189
- }));
317
+ export const connections = pgTable(
318
+ "connections",
319
+ {
320
+ id: uuid("id").primaryKey().defaultRandom(),
321
+ accountId: uuid("account_id")
322
+ .notNull()
323
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
324
+ workspaceId: uuid("workspace_id")
325
+ .notNull()
326
+ .references(() => workspaces.id, { onDelete: "cascade" }),
327
+ subjectId: text("subject_id"),
328
+ providerDomain: text("provider_domain").notNull(),
329
+ kind: text("kind").notNull(),
330
+ status: text("status").notNull().default("active"),
331
+ credentialEncrypted: text("credential_encrypted").notNull(),
332
+ grantedScopes: jsonb("granted_scopes").$type<string[]>().notNull().default([]),
333
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
334
+ lastRefreshAt: timestamp("last_refresh_at", { withTimezone: true }),
335
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
336
+ lastError: text("last_error"),
337
+ version: integer("version").notNull().default(1),
338
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
339
+ createdBySubjectId: text("created_by_subject_id"),
340
+ updatedBySubjectId: text("updated_by_subject_id"),
341
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
342
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
343
+ },
344
+ (table) => ({
345
+ workspaceProviderStatus: index("connections_workspace_provider_status_idx").on(
346
+ table.workspaceId,
347
+ table.providerDomain,
348
+ table.status,
349
+ ),
350
+ workspaceSubjectProvider: index("connections_workspace_subject_provider_idx").on(
351
+ table.workspaceId,
352
+ table.subjectId,
353
+ table.providerDomain,
354
+ ),
355
+ workspaceKind: index("connections_workspace_kind_idx").on(table.workspaceId, table.kind),
356
+ workspaceExpires: index("connections_workspace_expires_idx").on(
357
+ table.workspaceId,
358
+ table.expiresAt,
359
+ ),
360
+ }),
361
+ );
190
362
 
191
363
  // OAuth client registrations minted through MCP DCR, keyed by authorization
192
364
  // server issuer. This is deployment-wide client identity, not a workspace
193
365
  // credential; per-user/provider tokens still live only in connections.
194
- export const integrationOauthClients = pgTable("integration_oauth_clients", {
195
- id: uuid("id").primaryKey().defaultRandom(),
196
- issuer: text("issuer").notNull(),
197
- authorizationServer: text("authorization_server").notNull(),
198
- clientId: text("client_id").notNull(),
199
- clientSecretEncrypted: text("client_secret_encrypted"),
200
- tokenEndpointAuthMethod: text("token_endpoint_auth_method").notNull().default("none"),
201
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
202
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
203
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
204
- }, (table) => ({
205
- issuer: uniqueIndex("integration_oauth_clients_issuer_idx").on(table.issuer),
206
- authorizationServer: index("integration_oauth_clients_as_idx").on(table.authorizationServer),
207
- }));
366
+ export const integrationOauthClients = pgTable(
367
+ "integration_oauth_clients",
368
+ {
369
+ id: uuid("id").primaryKey().defaultRandom(),
370
+ issuer: text("issuer").notNull(),
371
+ authorizationServer: text("authorization_server").notNull(),
372
+ clientId: text("client_id").notNull(),
373
+ clientSecretEncrypted: text("client_secret_encrypted"),
374
+ tokenEndpointAuthMethod: text("token_endpoint_auth_method").notNull().default("none"),
375
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
376
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
377
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
378
+ },
379
+ (table) => ({
380
+ issuer: uniqueIndex("integration_oauth_clients_issuer_idx").on(table.issuer),
381
+ authorizationServer: index("integration_oauth_clients_as_idx").on(table.authorizationServer),
382
+ }),
383
+ );
208
384
 
209
385
  // Consumed OAuth state nonces. Rows are inserted only on callback; the primary
210
386
  // key makes a verified state single-use across API instances.
211
- export const integrationOauthStateNonces = pgTable("integration_oauth_state_nonces", {
212
- nonce: text("nonce").primaryKey(),
213
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
214
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
215
- subjectId: text("subject_id").notNull(),
216
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
217
- usedAt: timestamp("used_at", { withTimezone: true }).notNull().defaultNow(),
218
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
219
- }, (table) => ({
220
- workspace: index("integration_oauth_state_nonces_workspace_idx").on(table.workspaceId),
221
- expires: index("integration_oauth_state_nonces_expires_idx").on(table.expiresAt),
222
- }));
387
+ export const integrationOauthStateNonces = pgTable(
388
+ "integration_oauth_state_nonces",
389
+ {
390
+ nonce: text("nonce").primaryKey(),
391
+ accountId: uuid("account_id")
392
+ .notNull()
393
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
394
+ workspaceId: uuid("workspace_id")
395
+ .notNull()
396
+ .references(() => workspaces.id, { onDelete: "cascade" }),
397
+ subjectId: text("subject_id").notNull(),
398
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
399
+ usedAt: timestamp("used_at", { withTimezone: true }).notNull().defaultNow(),
400
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
401
+ },
402
+ (table) => ({
403
+ workspace: index("integration_oauth_state_nonces_workspace_idx").on(table.workspaceId),
404
+ expires: index("integration_oauth_state_nonces_expires_idx").on(table.expiresAt),
405
+ }),
406
+ );
223
407
 
224
408
  // Per-workspace Codex account selection (the ACTIVE pointer) + P3 rotation
225
409
  // forward-compat. One row per workspace. The only P1-load-bearing column is
@@ -229,357 +413,1351 @@ export const integrationOauthStateNonces = pgTable("integration_oauth_state_nonc
229
413
  // policy. active_credential_id's FK is declared in the MIGRATION (not
230
414
  // .references()) to avoid a forward-reference on the const ordering, exactly like
231
415
  // sessions.activeSandboxId; ON DELETE SET NULL.
232
- export const codexRotationSettings = pgTable("codex_rotation_settings", {
233
- id: uuid("id").primaryKey().defaultRandom(),
234
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
235
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
236
- activeCredentialId: uuid("active_credential_id"),
237
- rotationEnabled: boolean("rotation_enabled").notNull().default(false), // P3, inert in P1
238
- rotationStrategy: text("rotation_strategy").notNull().default("most_remaining"), // P3, inert
239
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
240
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
241
- }, (table) => ({
242
- workspace: uniqueIndex("codex_rotation_settings_workspace_idx").on(table.workspaceId),
243
- }));
244
-
245
- export const sessions = pgTable("sessions", {
246
- id: uuid("id").primaryKey().defaultRandom(),
247
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
248
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
249
- status: text("status").notNull().default("queued"),
250
- initialMessage: text("initial_message").notNull(),
251
- title: text("title"),
252
- titleSource: text("title_source"),
253
- // Per-session agent persona/system instructions supplied at create (the
254
- // per-agent-type prompt lever for embedding hosts). NULL ⇒ the session
255
- // carried none, so the composed agent instructions are byte-identical to a
256
- // workspace-only persona (no backfill, no behavior change for existing rows).
257
- // Composed system-level AFTER the workspace agentInstructions; never emitted
258
- // as a timeline event.
259
- instructions: text("instructions"),
260
- resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
261
- tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
262
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
263
- model: text("model").notNull(),
264
- sandboxBackend: text("sandbox_backend").notNull(),
265
- // The OS this session's box runs. Defaults to 'linux' (today's only OS, so
266
- // every existing + new row is a behavior-preserving no-op). CHECK-constrained
267
- // to the SandboxOs enum (linux|macos|windows) in migration 0018.
268
- sandboxOs: text("sandbox_os").notNull().default("linux"),
269
- // The shared-sandbox group this session's box belongs to. Defaults to the
270
- // session's OWN id (a singleton group: group === session — today's 1:1
271
- // behavior). When spawned shared via session_create, set to the PARENT's
272
- // sandboxGroupId so both run in ONE box. Immutable once set. NOT an FK (the
273
- // value is this row's id or an ancestor session's id in the same workspace;
274
- // the live lease row, not a sandbox_groups table, materializes the group).
275
- // The app generates the uuid and uses it for both id and sandbox_group_id in
276
- // one insert — it cannot SQL-default to id (id is defaultRandom()).
277
- sandboxGroupId: uuid("sandbox_group_id").notNull(),
278
- // The first-class swappable-sandbox POINTER (bring-your-own-compute M2,
279
- // dossier §10.3). NULL == "use the session's own group sandbox" (the
280
- // backward-compat default — every existing/new row is a behavior-preserving
281
- // no-op). The routing proxy re-reads (active_sandbox_id, active_epoch) PER
282
- // TOOL CALL to make a Modal<->selfhosted hot-swap seamless. The FK
283
- // (-> sandboxes(id) ON DELETE SET NULL — a deleted sandbox degrades the
284
- // pointer to the group default, never dangles) lives in migration 0024, NOT a
285
- // Drizzle .references() — exactly like parentSessionId below, so the const
286
- // ordering imposes no forward-reference.
287
- activeSandboxId: uuid("active_sandbox_id"),
288
- // The SECOND epoch ABOVE sandbox_leases.lease_epoch, bumped on every swap; an
289
- // in-flight op fenced by a stale active_epoch retries against the new active
290
- // sandbox. integer (NOT bigint) — the lease-epoch spike: int8 reads back as a
291
- // JS string and breaks the strict fence; int4 returns a number.
292
- activeEpoch: integer("active_epoch").notNull().default(0),
293
- // The session's WORKING DIRECTORY — the path/cwd base the (selfhosted) box's
294
- // agent/terminal/file-dock operate under. A launch-workspace_root-relative
295
- // subdir or an absolute machine path; surfaced alongside the active-sandbox
296
- // pointer (readActiveSandbox) and written through the epoch-fenced
297
- // setActiveSandbox CAS, NOT the row INSERT. NULL (the default) ⇒ today's
298
- // behavior exactly — the agent substitutes its workspace_root for an empty cwd,
299
- // so an unset working_dir is a byte-identical no-op. Create-time only (Stage A).
300
- workingDir: text("working_dir"),
301
- environmentId: uuid("environment_id").references(() => workspaceEnvironments.id, { onDelete: "set null" }),
302
- // Non-default first-party MCP token permissions (manager-style sessions);
303
- // null means the fixed worker default set in @opengeni/runtime.
304
- firstPartyMcpPermissions: jsonb("first_party_mcp_permissions").$type<string[]>(),
305
- // The manager session that spawned this one via session_create. Set only
306
- // when the creating grant carried a worker-signed sessionId claim (a session
307
- // spawning a worker); null for direct API creates and scheduled-task runs.
308
- // When set, this worker's terminal-for-now transitions wake the parent so a
309
- // manager can orchestrate workers without busy-polling. Self-referencing FK,
310
- // ON DELETE SET NULL so deleting a manager never cascades into its workers.
311
- parentSessionId: uuid("parent_session_id"),
312
- // Workspace-scoped CREATE idempotency key. NULL means the create carried no
313
- // key (each such create is independent). When set, the partial unique index
314
- // below collapses concurrent/retried creates with the same key in the same
315
- // workspace to a single session row — the dedup that closes the
316
- // double-submit/double-dispatch stuck-queued bug.
317
- createIdempotencyKey: text("create_idempotency_key"),
318
- temporalWorkflowId: text("temporal_workflow_id"),
319
- activeTurnId: uuid("active_turn_id"),
320
- // Actual input tokens reported for the last model call of the most recent
321
- // turn. The pre-turn client-side compaction trigger reads this as its budget
322
- // signal (char/4 estimate is the same-turn fallback). Null until a turn with
323
- // usage has completed.
324
- lastInputTokens: integer("last_input_tokens"),
325
- // Operator /compact request flag (client-side compaction path). The API sets
326
- // it true; the worker honors it BEFORE the next turn's model call by forcing
327
- // a compaction, then clears it. A durable flag (not a transient signal) so
328
- // the trigger survives a worker restart and converges before the next turn.
329
- compactRequested: boolean("compact_requested").notNull().default(false),
330
- lastSequence: integer("last_sequence").notNull().default(0),
331
- // The session's PINNED Codex account (manual override from the in-session
332
- // switcher). NULL ⇒ follow the workspace active pointer. FK declared in the
333
- // migration with ON DELETE SET NULL (a disconnected pin degrades to "follow
334
- // active", never dangles), same pattern as activeSandboxId.
335
- codexPinnedCredentialId: uuid("codex_pinned_credential_id"),
336
- // The Codex account the session's most recent turn ACTUALLY ran on — drives
337
- // the "Running on:" indicator. Written by the worker at the turn boundary. FK
338
- // ON DELETE SET NULL (migration).
339
- codexLastCredentialId: uuid("codex_last_credential_id"),
340
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
341
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
342
- }, (table) => ({
343
- workspaceCreated: index("sessions_workspace_created_idx").on(table.workspaceId, table.createdAt),
344
- environment: index("sessions_environment_idx").on(table.workspaceId, table.environmentId),
345
- parent: index("sessions_parent_idx").on(table.workspaceId, table.parentSessionId),
346
- // Routing index: resolve session_id -> sandbox_group_id at every lease entry
347
- // point and enumerate all sessions in a group for attribution/disclosure.
348
- sandboxGroup: index("sessions_sandbox_group_idx").on(table.workspaceId, table.sandboxGroupId),
349
- // Partial unique index: one session per (workspace, create_idempotency_key)
350
- // when a key is present. Concurrent creates racing on the same key see a
351
- // unique violation on all but one; the domain layer catches it and returns
352
- // the winning row instead of erroring.
353
- createIdempotency: uniqueIndex("sessions_workspace_create_idempotency_idx").on(table.workspaceId, table.createIdempotencyKey).where(sql`${table.createIdempotencyKey} is not null`),
354
- }));
355
-
356
- export const sessionMcpServers = pgTable("session_mcp_servers", {
357
- id: uuid("id").primaryKey().defaultRandom(),
358
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
359
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
360
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
361
- serverId: text("server_id").notNull(),
362
- name: text("name"),
363
- url: text("url").notNull(),
364
- allowedTools: jsonb("allowed_tools").$type<string[]>(),
365
- timeoutMs: integer("timeout_ms"),
366
- cacheToolsList: boolean("cache_tools_list").notNull().default(false),
367
- // Human-approval policy: `true` = every tool requires approval, a string[] of
368
- // UNPREFIXED tool names = only those require it, null/absent = auto-run.
369
- requireApproval: jsonb("require_approval").$type<boolean | string[]>(),
370
- // Map of header name -> AES-GCM ciphertext. Values are decrypted only by the
371
- // worker's run-preparation path and never returned by API helpers.
372
- headersEncrypted: jsonb("headers_encrypted").$type<Record<string, string>>().notNull().default({}),
373
- credentialVersion: integer("credential_version").notNull().default(1),
374
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
375
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
376
- }, (table) => ({
377
- sessionServer: uniqueIndex("session_mcp_servers_session_server_idx").on(table.workspaceId, table.sessionId, table.serverId),
378
- session: index("session_mcp_servers_session_idx").on(table.workspaceId, table.sessionId),
379
- }));
380
-
381
- export const files = pgTable("files", {
382
- id: uuid("id").primaryKey().defaultRandom(),
383
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
384
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
385
- status: text("status").notNull().default("pending_upload"),
386
- filename: text("filename").notNull(),
387
- safeFilename: text("safe_filename").notNull(),
388
- contentType: text("content_type").notNull(),
389
- sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
390
- sha256: text("sha256"),
391
- bucket: text("bucket").notNull(),
392
- objectKey: text("object_key").notNull(),
393
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
394
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
395
- }, (table) => ({
396
- workspaceCreated: index("files_workspace_created_idx").on(table.workspaceId, table.createdAt),
397
- objectKey: uniqueIndex("files_object_key_idx").on(table.objectKey),
398
- status: index("files_status_idx").on(table.status),
399
- }));
400
-
401
- export const fileUploads = pgTable("file_uploads", {
402
- id: uuid("id").primaryKey().defaultRandom(),
403
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
404
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
405
- fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "cascade" }),
406
- status: text("status").notNull().default("pending"),
407
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
408
- completedAt: timestamp("completed_at", { withTimezone: true }),
409
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
410
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
411
- }, (table) => ({
412
- workspace: index("file_uploads_workspace_idx").on(table.workspaceId),
413
- fileId: index("file_uploads_file_id_idx").on(table.fileId),
414
- status: index("file_uploads_status_idx").on(table.status),
415
- }));
416
-
417
- export const documentBases = pgTable("document_bases", {
418
- id: uuid("id").primaryKey().defaultRandom(),
419
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
420
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
421
- name: text("name").notNull(),
422
- description: text("description"),
423
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
424
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
425
- }, (table) => ({
426
- workspaceCreated: index("document_bases_workspace_created_idx").on(table.workspaceId, table.createdAt),
427
- }));
428
-
429
- export const documents = pgTable("documents", {
430
- id: uuid("id").primaryKey().defaultRandom(),
431
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
432
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
433
- baseId: uuid("base_id").notNull().references(() => documentBases.id, { onDelete: "cascade" }),
434
- fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "restrict" }),
435
- status: text("status").notNull().default("queued"),
436
- title: text("title").notNull(),
437
- parser: text("parser").notNull().default("liteparse"),
438
- chunkCount: integer("chunk_count").notNull().default(0),
439
- error: text("error"),
440
- sourceKind: text("source_kind").notNull().default("manual_upload"),
441
- sourceUri: text("source_uri"),
442
- sourceExternalId: text("source_external_id"),
443
- sourceTitle: text("source_title"),
444
- sourceAuthor: text("source_author"),
445
- sourceCreatedAt: timestamp("source_created_at", { withTimezone: true }),
446
- sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
447
- sourceVersion: text("source_version"),
448
- aclTags: jsonb("acl_tags").$type<string[]>().notNull().default([]),
449
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
450
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
451
- }, (table) => ({
452
- baseFile: uniqueIndex("documents_workspace_base_file_idx").on(table.workspaceId, table.baseId, table.fileId),
453
- baseStatus: index("documents_workspace_base_status_idx").on(table.workspaceId, table.baseId, table.status),
454
- sourceKind: index("documents_workspace_source_kind_idx").on(table.workspaceId, table.sourceKind),
455
- sourceExternalId: index("documents_workspace_source_external_id_idx").on(table.workspaceId, table.sourceExternalId),
456
- }));
457
-
458
- export const documentChunks = pgTable("document_chunks", {
459
- id: uuid("id").primaryKey().defaultRandom(),
460
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
461
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
462
- documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
463
- baseId: uuid("base_id").notNull().references(() => documentBases.id, { onDelete: "cascade" }),
464
- fileId: uuid("file_id").notNull().references(() => files.id, { onDelete: "restrict" }),
465
- chunkIndex: integer("chunk_index").notNull(),
466
- text: text("text").notNull(),
467
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
468
- embedding: vector("embedding").notNull(),
469
- embeddingModel: text("embedding_model").notNull(),
470
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
471
- }, (table) => ({
472
- documentIndex: uniqueIndex("document_chunks_workspace_document_index_idx").on(table.workspaceId, table.documentId, table.chunkIndex),
473
- base: index("document_chunks_workspace_base_idx").on(table.workspaceId, table.baseId),
474
- }));
475
-
476
- export const knowledgeMemories = pgTable("knowledge_memories", {
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
- status: text("status").notNull().default("proposed"),
481
- kind: text("kind").notNull().default("semantic"),
482
- scope: text("scope").notNull().default("workspace"),
483
- text: text("text").notNull(),
484
- sourceRefs: jsonb("source_refs").$type<unknown[]>().notNull().default([]),
485
- confidence: integer("confidence").notNull().default(50),
486
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
487
- createdBySessionId: uuid("created_by_session_id").references(() => sessions.id, { onDelete: "set null" }),
488
- reviewedBy: text("reviewed_by"),
489
- reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
490
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
491
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
492
- }, (table) => ({
493
- workspaceStatus: index("knowledge_memories_workspace_status_idx").on(table.workspaceId, table.status, table.updatedAt),
494
- workspaceKind: index("knowledge_memories_workspace_kind_idx").on(table.workspaceId, table.kind),
495
- workspaceScope: index("knowledge_memories_workspace_scope_idx").on(table.workspaceId, table.scope),
496
- createdBySession: index("knowledge_memories_workspace_created_by_session_idx").on(table.workspaceId, table.createdBySessionId),
497
- }));
498
-
499
- export const sessionTurns = pgTable("session_turns", {
500
- id: uuid("id").primaryKey().defaultRandom(),
501
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
502
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
503
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
504
- triggerEventId: uuid("trigger_event_id").notNull(),
505
- temporalWorkflowId: text("temporal_workflow_id").notNull(),
506
- status: text("status").notNull(),
507
- source: text("source").notNull().default("user"),
508
- position: integer("position").notNull(),
509
- prompt: text("prompt").notNull(),
510
- resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
511
- tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
512
- model: text("model").notNull(),
513
- reasoningEffort: text("reasoning_effort").notNull(),
514
- sandboxBackend: text("sandbox_backend").notNull(),
515
- // Per-turn OS override. NULL = inherit the session's sandbox_os. CHECK-
516
- // constrained to the SandboxOs enum (or NULL) in migration 0018.
517
- sandboxOs: text("sandbox_os"),
518
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
519
- // Atomic per-turn toolspace call budget counter (migration 0043). Incremented
520
- // by a single conditional UPDATE at tools/call time; the row lock serializes
521
- // concurrent reservations so exactly `toolspaceMaxCallsPerTurn` succeed.
522
- toolspaceCallCount: integer("toolspace_call_count").notNull().default(0),
523
- startedAt: timestamp("started_at", { withTimezone: true }),
524
- finishedAt: timestamp("finished_at", { withTimezone: true }),
525
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
526
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
527
- }, (table) => ({
528
- queue: index("session_turns_workspace_queue_idx").on(table.workspaceId, table.sessionId, table.status, table.position),
529
- }));
530
-
531
- export const sessionGoals = pgTable("session_goals", {
532
- id: uuid("id").primaryKey().defaultRandom(),
533
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
534
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
535
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
536
- status: text("status").notNull().default("active"), // active | paused | completed
537
- text: text("text").notNull(),
538
- successCriteria: text("success_criteria"),
539
- evidence: text("evidence"), // set by goal_complete
540
- rationale: text("rationale"), // set by goal_pause
541
- pausedReason: text("paused_reason"), // agent | user_interrupt | api | no_progress | max_auto_continuations | limits
542
- createdBy: text("created_by").notNull().default("api"), // api | agent | scheduled_task
543
- version: integer("version").notNull().default(1), // bumped on every set/update; progress signal
544
- autoContinuations: integer("auto_continuations").notNull().default(0),
545
- noProgressStreak: integer("no_progress_streak").notNull().default(0),
546
- maxAutoContinuations: integer("max_auto_continuations"), // per-goal override; a configured settings cap (if any) remains the hard ceiling
547
- lastContinuationTurnId: uuid("last_continuation_turn_id"),
548
- versionAtLastContinuation: integer("version_at_last_continuation"),
549
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
550
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
551
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
552
- }, (table) => ({
553
- workspaceSession: uniqueIndex("session_goals_workspace_session_idx").on(table.workspaceId, table.sessionId),
554
- status: index("session_goals_workspace_status_idx").on(table.workspaceId, table.status),
555
- }));
556
-
557
- export const sessionEvents = pgTable("session_events", {
558
- id: uuid("id").primaryKey().defaultRandom(),
559
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
560
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
561
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
562
- turnId: uuid("turn_id"),
563
- sequence: integer("sequence").notNull(),
564
- type: text("type").notNull(),
565
- payload: jsonb("payload").$type<unknown>().notNull().default({}),
566
- clientEventId: text("client_event_id"),
567
- producerId: text("producer_id"),
568
- producerSeq: integer("producer_seq"),
569
- occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
570
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
571
- }, (table) => ({
572
- sessionSequence: uniqueIndex("session_events_workspace_session_sequence_idx").on(table.workspaceId, table.sessionId, table.sequence),
573
- clientEvent: uniqueIndex("session_events_workspace_client_event_idx").on(table.workspaceId, table.sessionId, table.clientEventId).where(sql`${table.clientEventId} is not null`),
574
- 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`),
575
- sessionCreated: index("session_events_workspace_session_created_idx").on(table.workspaceId, table.sessionId, table.createdAt),
576
- }));
416
+ export const codexRotationSettings = pgTable(
417
+ "codex_rotation_settings",
418
+ {
419
+ id: uuid("id").primaryKey().defaultRandom(),
420
+ accountId: uuid("account_id")
421
+ .notNull()
422
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
423
+ workspaceId: uuid("workspace_id")
424
+ .notNull()
425
+ .references(() => workspaces.id, { onDelete: "cascade" }),
426
+ activeCredentialId: uuid("active_credential_id"),
427
+ // Legacy selector bit. Keep false as the DB default forever: an old worker
428
+ // only understands this column, so a schema-first rollout or binary
429
+ // rollback must never make it enter the non-atomic rotation path.
430
+ rotationEnabled: boolean("rotation_enabled").notNull().default(false),
431
+ // Revision-aware allocator cutover. Only migration-compatible API/worker
432
+ // code reads this bit; old binaries safely ignore it and keep the legacy
433
+ // pin/rotation policy.
434
+ leaseRotationEnabled: boolean("lease_rotation_enabled").notNull().default(false),
435
+ rotationStrategy: text("rotation_strategy").notNull().default("most_remaining"), // P3, inert
436
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
437
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
438
+ },
439
+ (table) => ({
440
+ workspace: uniqueIndex("codex_rotation_settings_workspace_idx").on(table.workspaceId),
441
+ }),
442
+ );
443
+
444
+ // Per-workspace model/provider availability policy — the HARD blocker deciding
445
+ // which providers/models may serve a turn in this workspace AT ALL. NULL columns
446
+ // mean unrestricted (today's behavior for every workspace without a row). A
447
+ // non-null allowed_providers is a strict allowlist over provider identities
448
+ // (the same identities the model router resolves to, with the built-in
449
+ // OpenAI/Azure client — including the legacy resolveTurnModel-null fallback —
450
+ // mapped to one well-known id); a non-null allowed_models is an additional
451
+ // exact-model-id allowlist. Enforced at the API model-choke points (422) and,
452
+ // authoritatively, in the worker immediately after turn model resolution: a
453
+ // blocked resolution NEVER reaches a model call and NEVER silently remaps.
454
+ // This exists so a codex-subscription workspace can be fail-closed to codex —
455
+ // a turn can wait/fail loud, but can never fall through to the paid built-in.
456
+ export const workspaceModelPolicies = pgTable(
457
+ "workspace_model_policies",
458
+ {
459
+ id: uuid("id").primaryKey().defaultRandom(),
460
+ accountId: uuid("account_id")
461
+ .notNull()
462
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
463
+ workspaceId: uuid("workspace_id")
464
+ .notNull()
465
+ .references(() => workspaces.id, { onDelete: "cascade" }),
466
+ allowedProviders: text("allowed_providers").array(),
467
+ allowedModels: text("allowed_models").array(),
468
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
469
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
470
+ },
471
+ (table) => ({
472
+ workspace: uniqueIndex("workspace_model_policies_workspace_idx").on(table.workspaceId),
473
+ }),
474
+ );
475
+
476
+ // One workspace-local short-lived holder per running Codex turn. Selection and
477
+ // insertion happen atomically while codex_rotation_settings is locked FOR
478
+ // UPDATE, so concurrent replicas in the SAME workspace see one another's
479
+ // assignments before choosing. Workspaces never share or correlate lease state.
480
+ // The composite (workspace, account), (workspace, credential), and
481
+ // (workspace, turn) FKs are declared in migration 0053 (sessionTurns is defined
482
+ // later in this module).
483
+ export const codexCredentialLeases = pgTable(
484
+ "codex_credential_leases",
485
+ {
486
+ id: uuid("id").primaryKey().defaultRandom(),
487
+ accountId: uuid("account_id")
488
+ .notNull()
489
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
490
+ workspaceId: uuid("workspace_id")
491
+ .notNull()
492
+ .references(() => workspaces.id, { onDelete: "cascade" }),
493
+ credentialId: uuid("credential_id").notNull(),
494
+ turnId: uuid("turn_id").notNull(),
495
+ // Temporal activity execution fence. A successor dispatch for the same
496
+ // durable turn replaces holderId and increments generation atomically;
497
+ // stale/zombie heartbeats and releases must match both values.
498
+ holderId: text("holder_id").notNull(),
499
+ generation: integer("generation").notNull().default(1),
500
+ leasedUntil: timestamp("leased_until", { withTimezone: true }).notNull(),
501
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
502
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
503
+ },
504
+ (table) => ({
505
+ turn: uniqueIndex("codex_credential_leases_workspace_turn_idx").on(
506
+ table.workspaceId,
507
+ table.turnId,
508
+ ),
509
+ activeCredential: index("codex_credential_leases_active_credential_idx").on(
510
+ table.workspaceId,
511
+ table.credentialId,
512
+ table.leasedUntil,
513
+ ),
514
+ expiry: index("codex_credential_leases_expiry_idx").on(table.leasedUntil),
515
+ }),
516
+ );
517
+
518
+ export const sessions = pgTable(
519
+ "sessions",
520
+ {
521
+ id: uuid("id").primaryKey().defaultRandom(),
522
+ accountId: uuid("account_id")
523
+ .notNull()
524
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
525
+ workspaceId: uuid("workspace_id")
526
+ .notNull()
527
+ .references(() => workspaces.id, { onDelete: "cascade" }),
528
+ status: text("status").notNull().default("queued"),
529
+ initialMessage: text("initial_message").notNull(),
530
+ title: text("title"),
531
+ titleSource: text("title_source"),
532
+ // Per-session agent persona/system instructions supplied at create (the
533
+ // per-agent-type prompt lever for embedding hosts). NULL ⇒ the session
534
+ // carried none, so the composed agent instructions are byte-identical to a
535
+ // workspace-only persona (no backfill, no behavior change for existing rows).
536
+ // Composed system-level AFTER the workspace agentInstructions; never emitted
537
+ // as a timeline event.
538
+ instructions: text("instructions"),
539
+ resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
540
+ tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
541
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
542
+ model: text("model").notNull(),
543
+ sandboxBackend: text("sandbox_backend").notNull(),
544
+ // The OS this session's box runs. Defaults to 'linux' (today's only OS, so
545
+ // every existing + new row is a behavior-preserving no-op). CHECK-constrained
546
+ // to the SandboxOs enum (linux|macos|windows) in migration 0018.
547
+ sandboxOs: text("sandbox_os").notNull().default("linux"),
548
+ // The shared-sandbox group this session's box belongs to. Defaults to the
549
+ // session's OWN id (a singleton group: group === session — today's 1:1
550
+ // behavior). When spawned shared via session_create, set to the PARENT's
551
+ // sandboxGroupId so both run in ONE box. Immutable once set. NOT an FK (the
552
+ // value is this row's id or an ancestor session's id in the same workspace;
553
+ // the live lease row, not a sandbox_groups table, materializes the group).
554
+ // The app generates the uuid and uses it for both id and sandbox_group_id in
555
+ // one insert — it cannot SQL-default to id (id is defaultRandom()).
556
+ sandboxGroupId: uuid("sandbox_group_id").notNull(),
557
+ // The first-class swappable-sandbox POINTER (bring-your-own-compute M2,
558
+ // dossier §10.3). NULL == "use the session's own group sandbox" (the
559
+ // backward-compat default — every existing/new row is a behavior-preserving
560
+ // no-op). The routing proxy re-reads (active_sandbox_id, active_epoch) PER
561
+ // TOOL CALL to make a Modal<->selfhosted hot-swap seamless. The FK
562
+ // (-> sandboxes(id) ON DELETE SET NULL — a deleted sandbox degrades the
563
+ // pointer to the group default, never dangles) lives in migration 0024, NOT a
564
+ // Drizzle .references() — exactly like parentSessionId below, so the const
565
+ // ordering imposes no forward-reference.
566
+ activeSandboxId: uuid("active_sandbox_id"),
567
+ // The SECOND epoch ABOVE sandbox_leases.lease_epoch, bumped on every swap; an
568
+ // in-flight op fenced by a stale active_epoch retries against the new active
569
+ // sandbox. integer (NOT bigint) — the lease-epoch spike: int8 reads back as a
570
+ // JS string and breaks the strict fence; int4 returns a number.
571
+ activeEpoch: integer("active_epoch").notNull().default(0),
572
+ // The session's WORKING DIRECTORY — the path/cwd base the (selfhosted) box's
573
+ // agent/terminal/file-dock operate under. A launch-workspace_root-relative
574
+ // subdir or an absolute machine path; surfaced alongside the active-sandbox
575
+ // pointer (readActiveSandbox) and written through the epoch-fenced
576
+ // setActiveSandbox CAS, NOT the row INSERT. NULL (the default) ⇒ today's
577
+ // behavior exactly — the agent substitutes its workspace_root for an empty cwd,
578
+ // so an unset working_dir is a byte-identical no-op. Create-time only (Stage A).
579
+ workingDir: text("working_dir"),
580
+ variableSetId: uuid("variable_set_id").references(() => workspaceVariableSets.id, {
581
+ onDelete: "set null",
582
+ }),
583
+ // The rig this session rides + the exact rig version frozen at create time
584
+ // (migration 0047). NULL ⇒ the session rides no rig (today's behavior). FKs
585
+ // (-> rigs(id)/rig_versions(id) ON DELETE SET NULL) live in migration 0047,
586
+ // not Drizzle .references(), because those tables are declared later in this
587
+ // file (forward-reference pattern, same as activeSandboxId). Consumed in M3.
588
+ rigId: uuid("rig_id"),
589
+ rigVersionId: uuid("rig_version_id"),
590
+ // Non-default first-party MCP token permissions (manager-style sessions);
591
+ // null means the fixed worker default set in @opengeni/runtime.
592
+ firstPartyMcpPermissions: jsonb("first_party_mcp_permissions").$type<string[]>(),
593
+ // The manager session that spawned this one via session_create. Set only
594
+ // when the creating grant carried a worker-signed sessionId claim (a session
595
+ // spawning a worker); null for direct API creates and scheduled-task runs.
596
+ // When set, this worker's terminal-for-now transitions wake the parent so a
597
+ // manager can orchestrate workers without busy-polling. Self-referencing FK,
598
+ // ON DELETE SET NULL so deleting a manager never cascades into its workers.
599
+ parentSessionId: uuid("parent_session_id"),
600
+ // Workspace-scoped CREATE idempotency key. NULL means the create carried no
601
+ // key (each such create is independent). When set, the partial unique index
602
+ // below collapses concurrent/retried creates with the same key in the same
603
+ // workspace to a single session row — the dedup that closes the
604
+ // double-submit/double-dispatch stuck-queued bug.
605
+ createIdempotencyKey: text("create_idempotency_key"),
606
+ temporalWorkflowId: text("temporal_workflow_id"),
607
+ activeTurnId: uuid("active_turn_id"),
608
+ // Actual input tokens reported for the last model call of the most recent
609
+ // turn. The pre-turn portable compaction trigger reads this as its budget
610
+ // signal (char/4 estimate is the same-turn fallback). Null until a turn with
611
+ // usage has completed.
612
+ lastInputTokens: integer("last_input_tokens"),
613
+ // Operator /compact request flag. The API sets
614
+ // it true; the worker honors it BEFORE the next turn's model call by forcing
615
+ // a compaction, then clears it. A durable flag (not a transient signal) so
616
+ // the trigger survives a worker restart and converges before the next turn.
617
+ compactRequested: boolean("compact_requested").notNull().default(false),
618
+ queueVersion: integer("queue_version").notNull().default(0),
619
+ queueHeadPosition: bigint("queue_head_position", { mode: "number" }).notNull().default(0),
620
+ queueTailPosition: bigint("queue_tail_position", { mode: "number" }).notNull().default(0),
621
+ directControlState: text("direct_control_state").notNull().default("active"),
622
+ directPauseRevision: bigint("direct_pause_revision", { mode: "number" }),
623
+ subtreeRunOverrideRevision: bigint("subtree_run_override_revision", { mode: "number" }),
624
+ controlVersion: bigint("control_version", { mode: "number" }).notNull().default(0),
625
+ directControlReason: text("direct_control_reason"),
626
+ directControlChangedBy: text("direct_control_changed_by"),
627
+ directControlChangedAt: timestamp("direct_control_changed_at", { withTimezone: true }),
628
+ lastSequence: integer("last_sequence").notNull().default(0),
629
+ // The session's PINNED Codex account (manual override from the in-session
630
+ // switcher). NULL ⇒ follow the workspace active pointer. FK declared in the
631
+ // migration with ON DELETE SET NULL (a disconnected pin degrades to "follow
632
+ // active", never dangles), same pattern as activeSandboxId.
633
+ codexPinnedCredentialId: uuid("codex_pinned_credential_id"),
634
+ // The Codex account the session's most recent turn ACTUALLY ran on — drives
635
+ // the "Running on:" indicator. Written by the worker at the turn boundary. FK
636
+ // ON DELETE SET NULL (migration).
637
+ codexLastCredentialId: uuid("codex_last_credential_id"),
638
+ // The SOURCE of codex_pinned_credential_id (AM-2): 'manual' — the user's
639
+ // in-session account switcher, which is SACRED and never moved by any policy —
640
+ // or 'policy' — the sharded rotation strategy's deterministic per-session home
641
+ // assignment, which MAY be re-sharded to another account when its own account
642
+ // caps. NULL when there is no pin (and for every pre-existing row). CHECK
643
+ // (manual|policy) lives in the migration; no FK (it describes the pin, not an
644
+ // account).
645
+ codexPinSource: text("codex_pin_source"),
646
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
647
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
648
+ },
649
+ (table) => ({
650
+ workspaceIdentity: uniqueIndex("sessions_workspace_id_idx").on(table.workspaceId, table.id),
651
+ workspaceCreated: index("sessions_workspace_created_idx").on(
652
+ table.workspaceId,
653
+ table.createdAt,
654
+ ),
655
+ variableSet: index("sessions_variable_set_idx").on(table.workspaceId, table.variableSetId),
656
+ parent: index("sessions_parent_idx").on(table.workspaceId, table.parentSessionId),
657
+ // Routing index: resolve session_id -> sandbox_group_id at every lease entry
658
+ // point and enumerate all sessions in a group for attribution/disclosure.
659
+ sandboxGroup: index("sessions_sandbox_group_idx").on(table.workspaceId, table.sandboxGroupId),
660
+ // Partial unique index: one session per (workspace, create_idempotency_key)
661
+ // when a key is present. Concurrent creates racing on the same key see a
662
+ // unique violation on all but one; the domain layer catches it and returns
663
+ // the winning row instead of erroring.
664
+ createIdempotency: uniqueIndex("sessions_workspace_create_idempotency_idx")
665
+ .on(table.workspaceId, table.createIdempotencyKey)
666
+ .where(sql`${table.createIdempotencyKey} is not null`),
667
+ }),
668
+ );
669
+
670
+ // Per-authenticated-subject session organization. This is deliberately a
671
+ // relation instead of a session column: one member's pin must never reorder a
672
+ // shared workspace for another member, and a session's own activity timestamps
673
+ // must remain agent/runtime truth. `subjectId` is the trusted AccessGrant
674
+ // subject, which is text because configured/delegated principals are not always
675
+ // UUIDs. The account/workspace pair carries the standard forced-RLS boundary.
676
+ export const sessionPins = pgTable(
677
+ "session_pins",
678
+ {
679
+ id: uuid("id").primaryKey().defaultRandom(),
680
+ accountId: uuid("account_id")
681
+ .notNull()
682
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
683
+ workspaceId: uuid("workspace_id")
684
+ .notNull()
685
+ .references(() => workspaces.id, { onDelete: "cascade" }),
686
+ subjectId: text("subject_id").notNull(),
687
+ sessionId: uuid("session_id")
688
+ .notNull()
689
+ .references(() => sessions.id, { onDelete: "cascade" }),
690
+ // Keep an unpinned tombstone (pinned=false, pinned_at=null) rather than
691
+ // deleting it. That preserves a monotonic version and prevents an ABA race:
692
+ // a stale client that saw pin version 1 cannot silently overwrite a later
693
+ // unpin+re-pin that would otherwise recreate version 1.
694
+ pinned: boolean("pinned").notNull().default(true),
695
+ pinnedAt: timestamp("pinned_at", { withTimezone: true }).defaultNow(),
696
+ version: integer("version").notNull().default(1),
697
+ },
698
+ (table) => ({
699
+ subjectNonempty: check(
700
+ "session_pins_subject_nonempty",
701
+ sql`length(btrim(${table.subjectId})) > 0`,
702
+ ),
703
+ versionPositive: check("session_pins_version_positive", sql`${table.version} >= 1`),
704
+ stateConsistent: check(
705
+ "session_pins_state_consistent",
706
+ sql`((${table.pinned}) and (${table.pinnedAt}) is not null) or ((not ${table.pinned}) and (${table.pinnedAt}) is null)`,
707
+ ),
708
+ workspaceAccount: foreignKey({
709
+ name: "session_pins_workspace_account_fk",
710
+ columns: [table.workspaceId, table.accountId],
711
+ foreignColumns: [workspaces.id, workspaces.accountId],
712
+ }).onDelete("cascade"),
713
+ workspaceSession: foreignKey({
714
+ name: "session_pins_workspace_session_fk",
715
+ columns: [table.workspaceId, table.sessionId],
716
+ foreignColumns: [sessions.workspaceId, sessions.id],
717
+ }).onDelete("cascade"),
718
+ subjectSession: uniqueIndex("session_pins_subject_workspace_session_idx").on(
719
+ table.subjectId,
720
+ table.workspaceId,
721
+ table.sessionId,
722
+ ),
723
+ subjectPinned: index("session_pins_workspace_subject_pinned_idx").on(
724
+ table.workspaceId,
725
+ table.subjectId,
726
+ table.pinned,
727
+ table.pinnedAt.desc(),
728
+ table.sessionId.desc(),
729
+ ),
730
+ }),
731
+ );
732
+
733
+ // A short-lived server-owned continuation snapshot for the pin-aware session
734
+ // list. The ordinary list is ordered by mutable activity, so a cursor cannot
735
+ // safely replay that order from updated_at alone across HTTP requests. The
736
+ // snapshot stores the already-ordered ordinary ids; the list query still joins
737
+ // live session rows for current lifecycle/title data and expires snapshots
738
+ // opportunistically.
739
+ export const sessionListSnapshots = pgTable(
740
+ "session_list_snapshots",
741
+ {
742
+ id: uuid("id").primaryKey().defaultRandom(),
743
+ accountId: uuid("account_id").notNull(),
744
+ workspaceId: uuid("workspace_id").notNull(),
745
+ subjectId: text("subject_id").notNull(),
746
+ parentSessionFilter: text("parent_session_filter").notNull().default("all"),
747
+ search: text("search"),
748
+ ordinarySessionIds: uuid("ordinary_session_ids")
749
+ .array()
750
+ .notNull()
751
+ .default(sql`'{}'::uuid[]`),
752
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
753
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
754
+ },
755
+ (table) => ({
756
+ workspaceAccount: foreignKey({
757
+ name: "session_list_snapshots_workspace_account_fk",
758
+ columns: [table.workspaceId, table.accountId],
759
+ foreignColumns: [workspaces.id, workspaces.accountId],
760
+ }).onDelete("cascade"),
761
+ subjectNonempty: check(
762
+ "session_list_snapshots_subject_nonempty",
763
+ sql`length(btrim(${table.subjectId})) > 0`,
764
+ ),
765
+ parentFilterValid: check(
766
+ "session_list_snapshots_parent_filter_valid",
767
+ sql`${table.parentSessionFilter} = 'all' or ${table.parentSessionFilter} = 'null' or ${table.parentSessionFilter} ~ '^[0-9a-fA-F-]{36}$'`,
768
+ ),
769
+ searchLength: check(
770
+ "session_list_snapshots_search_length",
771
+ sql`${table.search} is null or length(${table.search}) <= 200`,
772
+ ),
773
+ workspaceExpiry: index("session_list_snapshots_workspace_expiry_idx").on(
774
+ table.workspaceId,
775
+ table.subjectId,
776
+ table.expiresAt,
777
+ ),
778
+ expiryReaper: index("session_list_snapshots_expiry_reaper_idx").on(table.expiresAt, table.id),
779
+ }),
780
+ );
781
+
782
+ export const sessionMcpServers = pgTable(
783
+ "session_mcp_servers",
784
+ {
785
+ id: uuid("id").primaryKey().defaultRandom(),
786
+ accountId: uuid("account_id")
787
+ .notNull()
788
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
789
+ workspaceId: uuid("workspace_id")
790
+ .notNull()
791
+ .references(() => workspaces.id, { onDelete: "cascade" }),
792
+ sessionId: uuid("session_id")
793
+ .notNull()
794
+ .references(() => sessions.id, { onDelete: "cascade" }),
795
+ serverId: text("server_id").notNull(),
796
+ name: text("name"),
797
+ url: text("url").notNull(),
798
+ allowedTools: jsonb("allowed_tools").$type<string[]>(),
799
+ timeoutMs: integer("timeout_ms"),
800
+ cacheToolsList: boolean("cache_tools_list").notNull().default(false),
801
+ // Human-approval policy: `true` = every tool requires approval, a string[] of
802
+ // UNPREFIXED tool names = only those require it, null/absent = auto-run.
803
+ requireApproval: jsonb("require_approval").$type<boolean | string[]>(),
804
+ // Map of header name -> AES-GCM ciphertext. Values are decrypted only by the
805
+ // worker's run-preparation path and never returned by API helpers.
806
+ headersEncrypted: jsonb("headers_encrypted")
807
+ .$type<Record<string, string>>()
808
+ .notNull()
809
+ .default({}),
810
+ credentialVersion: integer("credential_version").notNull().default(1),
811
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
812
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
813
+ },
814
+ (table) => ({
815
+ sessionServer: uniqueIndex("session_mcp_servers_session_server_idx").on(
816
+ table.workspaceId,
817
+ table.sessionId,
818
+ table.serverId,
819
+ ),
820
+ session: index("session_mcp_servers_session_idx").on(table.workspaceId, table.sessionId),
821
+ }),
822
+ );
823
+
824
+ export const files = pgTable(
825
+ "files",
826
+ {
827
+ id: uuid("id").primaryKey().defaultRandom(),
828
+ accountId: uuid("account_id")
829
+ .notNull()
830
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
831
+ workspaceId: uuid("workspace_id")
832
+ .notNull()
833
+ .references(() => workspaces.id, { onDelete: "cascade" }),
834
+ status: text("status").notNull().default("pending_upload"),
835
+ filename: text("filename").notNull(),
836
+ safeFilename: text("safe_filename").notNull(),
837
+ contentType: text("content_type").notNull(),
838
+ sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
839
+ sha256: text("sha256"),
840
+ bucket: text("bucket").notNull(),
841
+ objectKey: text("object_key").notNull(),
842
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
843
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
844
+ },
845
+ (table) => ({
846
+ workspaceCreated: index("files_workspace_created_idx").on(table.workspaceId, table.createdAt),
847
+ objectKey: uniqueIndex("files_object_key_idx").on(table.objectKey),
848
+ status: index("files_status_idx").on(table.status),
849
+ }),
850
+ );
851
+
852
+ export const fileUploads = pgTable(
853
+ "file_uploads",
854
+ {
855
+ id: uuid("id").primaryKey().defaultRandom(),
856
+ accountId: uuid("account_id")
857
+ .notNull()
858
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
859
+ workspaceId: uuid("workspace_id")
860
+ .notNull()
861
+ .references(() => workspaces.id, { onDelete: "cascade" }),
862
+ fileId: uuid("file_id")
863
+ .notNull()
864
+ .references(() => files.id, { onDelete: "cascade" }),
865
+ status: text("status").notNull().default("pending"),
866
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
867
+ completedAt: timestamp("completed_at", { withTimezone: true }),
868
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
869
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
870
+ },
871
+ (table) => ({
872
+ workspace: index("file_uploads_workspace_idx").on(table.workspaceId),
873
+ fileId: index("file_uploads_file_id_idx").on(table.fileId),
874
+ status: index("file_uploads_status_idx").on(table.status),
875
+ }),
876
+ );
877
+
878
+ export const documentBases = pgTable(
879
+ "document_bases",
880
+ {
881
+ id: uuid("id").primaryKey().defaultRandom(),
882
+ accountId: uuid("account_id")
883
+ .notNull()
884
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
885
+ workspaceId: uuid("workspace_id")
886
+ .notNull()
887
+ .references(() => workspaces.id, { onDelete: "cascade" }),
888
+ name: text("name").notNull(),
889
+ description: text("description"),
890
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
891
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
892
+ },
893
+ (table) => ({
894
+ workspaceCreated: index("document_bases_workspace_created_idx").on(
895
+ table.workspaceId,
896
+ table.createdAt,
897
+ ),
898
+ }),
899
+ );
900
+
901
+ export const documents = pgTable(
902
+ "documents",
903
+ {
904
+ id: uuid("id").primaryKey().defaultRandom(),
905
+ accountId: uuid("account_id")
906
+ .notNull()
907
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
908
+ workspaceId: uuid("workspace_id")
909
+ .notNull()
910
+ .references(() => workspaces.id, { onDelete: "cascade" }),
911
+ baseId: uuid("base_id")
912
+ .notNull()
913
+ .references(() => documentBases.id, { onDelete: "cascade" }),
914
+ fileId: uuid("file_id")
915
+ .notNull()
916
+ .references(() => files.id, { onDelete: "restrict" }),
917
+ status: text("status").notNull().default("queued"),
918
+ title: text("title").notNull(),
919
+ parser: text("parser").notNull().default("liteparse"),
920
+ chunkCount: integer("chunk_count").notNull().default(0),
921
+ error: text("error"),
922
+ sourceKind: text("source_kind").notNull().default("manual_upload"),
923
+ sourceUri: text("source_uri"),
924
+ sourceExternalId: text("source_external_id"),
925
+ sourceTitle: text("source_title"),
926
+ sourceAuthor: text("source_author"),
927
+ sourceCreatedAt: timestamp("source_created_at", { withTimezone: true }),
928
+ sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
929
+ sourceVersion: text("source_version"),
930
+ aclTags: jsonb("acl_tags").$type<string[]>().notNull().default([]),
931
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
932
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
933
+ },
934
+ (table) => ({
935
+ baseFile: uniqueIndex("documents_workspace_base_file_idx").on(
936
+ table.workspaceId,
937
+ table.baseId,
938
+ table.fileId,
939
+ ),
940
+ baseStatus: index("documents_workspace_base_status_idx").on(
941
+ table.workspaceId,
942
+ table.baseId,
943
+ table.status,
944
+ ),
945
+ sourceKind: index("documents_workspace_source_kind_idx").on(
946
+ table.workspaceId,
947
+ table.sourceKind,
948
+ ),
949
+ sourceExternalId: index("documents_workspace_source_external_id_idx").on(
950
+ table.workspaceId,
951
+ table.sourceExternalId,
952
+ ),
953
+ }),
954
+ );
955
+
956
+ export const documentChunks = pgTable(
957
+ "document_chunks",
958
+ {
959
+ id: uuid("id").primaryKey().defaultRandom(),
960
+ accountId: uuid("account_id")
961
+ .notNull()
962
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
963
+ workspaceId: uuid("workspace_id")
964
+ .notNull()
965
+ .references(() => workspaces.id, { onDelete: "cascade" }),
966
+ documentId: uuid("document_id")
967
+ .notNull()
968
+ .references(() => documents.id, { onDelete: "cascade" }),
969
+ baseId: uuid("base_id")
970
+ .notNull()
971
+ .references(() => documentBases.id, { onDelete: "cascade" }),
972
+ fileId: uuid("file_id")
973
+ .notNull()
974
+ .references(() => files.id, { onDelete: "restrict" }),
975
+ chunkIndex: integer("chunk_index").notNull(),
976
+ text: text("text").notNull(),
977
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
978
+ embedding: vector("embedding").notNull(),
979
+ embeddingModel: text("embedding_model").notNull(),
980
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
981
+ },
982
+ (table) => ({
983
+ documentIndex: uniqueIndex("document_chunks_workspace_document_index_idx").on(
984
+ table.workspaceId,
985
+ table.documentId,
986
+ table.chunkIndex,
987
+ ),
988
+ base: index("document_chunks_workspace_base_idx").on(table.workspaceId, table.baseId),
989
+ }),
990
+ );
991
+
992
+ export const knowledgeMemories = pgTable(
993
+ "knowledge_memories",
994
+ {
995
+ id: uuid("id").primaryKey().defaultRandom(),
996
+ accountId: uuid("account_id")
997
+ .notNull()
998
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
999
+ workspaceId: uuid("workspace_id")
1000
+ .notNull()
1001
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1002
+ status: text("status").notNull().default("proposed"),
1003
+ kind: text("kind").notNull().default("semantic"),
1004
+ scope: text("scope").notNull().default("workspace"),
1005
+ text: text("text").notNull(),
1006
+ sourceRefs: jsonb("source_refs").$type<unknown[]>().notNull().default([]),
1007
+ confidence: integer("confidence").notNull().default(50),
1008
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1009
+ createdBySessionId: uuid("created_by_session_id").references(() => sessions.id, {
1010
+ onDelete: "set null",
1011
+ }),
1012
+ reviewedBy: text("reviewed_by"),
1013
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
1014
+ // Workspace Memory V1 (migration 0045). Embedding is nullable: fail-soft writes
1015
+ // (embedder unavailable) persist keyword-searchable rows without a vector.
1016
+ embedding: vector("embedding"),
1017
+ embeddingModel: text("embedding_model"),
1018
+ pinned: boolean("pinned").notNull().default(false),
1019
+ usageCount: integer("usage_count").notNull().default(0),
1020
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
1021
+ // Self-referential supersession chain. FKs live in migration 0045 (ON DELETE SET
1022
+ // NULL); declared here as plain columns like the migration-only composite FK.
1023
+ supersedesId: uuid("supersedes_id"),
1024
+ supersededById: uuid("superseded_by_id"),
1025
+ validFrom: timestamp("valid_from", { withTimezone: true }).notNull().defaultNow(),
1026
+ validUntil: timestamp("valid_until", { withTimezone: true }),
1027
+ // sha256(normalizeMemoryText(text)) — exact-dedup key; see memory-domain.
1028
+ textHash: text("text_hash"),
1029
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1030
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1031
+ },
1032
+ (table) => ({
1033
+ workspaceStatus: index("knowledge_memories_workspace_status_idx").on(
1034
+ table.workspaceId,
1035
+ table.status,
1036
+ table.updatedAt,
1037
+ ),
1038
+ workspaceKind: index("knowledge_memories_workspace_kind_idx").on(table.workspaceId, table.kind),
1039
+ workspaceScope: index("knowledge_memories_workspace_scope_idx").on(
1040
+ table.workspaceId,
1041
+ table.scope,
1042
+ ),
1043
+ createdBySession: index("knowledge_memories_workspace_created_by_session_idx").on(
1044
+ table.workspaceId,
1045
+ table.createdBySessionId,
1046
+ ),
1047
+ // Working-set selection (partial index mirrors migration 0045).
1048
+ workspaceVisible: index("knowledge_memories_workspace_visible_idx")
1049
+ .on(table.workspaceId, table.pinned, table.updatedAt)
1050
+ .where(sql`${table.status} in ('active', 'approved')`),
1051
+ workspaceTextHash: index("knowledge_memories_workspace_text_hash_idx").on(
1052
+ table.workspaceId,
1053
+ table.textHash,
1054
+ ),
1055
+ workspaceVisibleTextHashUnique: uniqueIndex("knowledge_memories_workspace_visible_text_hash_uq")
1056
+ .on(table.workspaceId, table.textHash)
1057
+ .where(sql`${table.status} in ('active', 'approved') and ${table.textHash} is not null`),
1058
+ }),
1059
+ );
1060
+
1061
+ export const sessionTurns = pgTable(
1062
+ "session_turns",
1063
+ {
1064
+ id: uuid("id").primaryKey().defaultRandom(),
1065
+ accountId: uuid("account_id")
1066
+ .notNull()
1067
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1068
+ workspaceId: uuid("workspace_id")
1069
+ .notNull()
1070
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1071
+ sessionId: uuid("session_id")
1072
+ .notNull()
1073
+ .references(() => sessions.id, { onDelete: "cascade" }),
1074
+ triggerEventId: uuid("trigger_event_id").notNull(),
1075
+ temporalWorkflowId: text("temporal_workflow_id").notNull(),
1076
+ status: text("status").notNull(),
1077
+ source: text("source").notNull().default("user"),
1078
+ position: bigint("position", { mode: "number" }).notNull(),
1079
+ prompt: text("prompt").notNull(),
1080
+ resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1081
+ tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1082
+ model: text("model").notNull(),
1083
+ reasoningEffort: text("reasoning_effort").notNull(),
1084
+ sandboxBackend: text("sandbox_backend").notNull(),
1085
+ // Per-turn OS override. NULL = inherit the session's sandbox_os. CHECK-
1086
+ // constrained to the SandboxOs enum (or NULL) in migration 0018.
1087
+ sandboxOs: text("sandbox_os"),
1088
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1089
+ version: integer("version").notNull().default(1),
1090
+ executionGeneration: integer("execution_generation").notNull().default(0),
1091
+ // Composite FK to session_turn_attempts is installed by migration 0063.
1092
+ // It lives in SQL because attempts carry the reciprocal turn FK and because
1093
+ // the claim transaction preallocates this ID before inserting the attempt;
1094
+ // the SQL constraint is therefore DEFERRABLE INITIALLY DEFERRED.
1095
+ activeAttemptId: uuid("active_attempt_id"),
1096
+ lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
1097
+ cancelledBy: text("cancelled_by"),
1098
+ cancelReason: text("cancel_reason"),
1099
+ // Atomic per-turn toolspace call budget counter (migration 0043). Incremented
1100
+ // by a single conditional UPDATE at tools/call time; the row lock serializes
1101
+ // concurrent reservations so exactly `toolspaceMaxCallsPerTurn` succeed.
1102
+ toolspaceCallCount: integer("toolspace_call_count").notNull().default(0),
1103
+ startedAt: timestamp("started_at", { withTimezone: true }),
1104
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
1105
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1106
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1107
+ },
1108
+ (table) => ({
1109
+ workspaceIdentity: uniqueIndex("session_turns_workspace_id_idx").on(
1110
+ table.workspaceId,
1111
+ table.id,
1112
+ ),
1113
+ queue: index("session_turns_workspace_queue_idx").on(
1114
+ table.workspaceId,
1115
+ table.sessionId,
1116
+ table.status,
1117
+ table.position,
1118
+ ),
1119
+ oneCurrentInference: uniqueIndex("session_turns_one_current_inference_uq")
1120
+ .on(table.workspaceId, table.sessionId)
1121
+ .where(sql`${table.status} in ('running','requires_action','recovering','waiting_capacity')`),
1122
+ }),
1123
+ );
1124
+
1125
+ // First-class ownership for one accepted execution attempt. A workflow may
1126
+ // preallocate id, but this row is inserted only by the activity transaction
1127
+ // that actually claims the logical turn and registers its exact dispatch.
1128
+ export const sessionTurnAttempts = pgTable(
1129
+ "session_turn_attempts",
1130
+ {
1131
+ id: uuid("id").primaryKey(),
1132
+ accountId: uuid("account_id").notNull(),
1133
+ workspaceId: uuid("workspace_id").notNull(),
1134
+ sessionId: uuid("session_id").notNull(),
1135
+ turnId: uuid("turn_id").notNull(),
1136
+ executionGeneration: integer("execution_generation").notNull(),
1137
+ state: text("state").notNull().default("claimed"),
1138
+ outcome: text("outcome"),
1139
+ temporalWorkflowId: text("temporal_workflow_id").notNull(),
1140
+ temporalWorkflowRunId: text("temporal_workflow_run_id").notNull(),
1141
+ temporalActivityId: text("temporal_activity_id").notNull(),
1142
+ workerId: text("worker_id"),
1143
+ leaseId: text("lease_id"),
1144
+ leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
1145
+ verifiedControlRevision: bigint("verified_control_revision", { mode: "number" }).notNull(),
1146
+ startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
1147
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1148
+ closedAt: timestamp("closed_at", { withTimezone: true }),
1149
+ },
1150
+ (table) => ({
1151
+ workspaceAccount: foreignKey({
1152
+ name: "session_turn_attempts_workspace_account_fk",
1153
+ columns: [table.workspaceId, table.accountId],
1154
+ foreignColumns: [workspaces.id, workspaces.accountId],
1155
+ }).onDelete("cascade"),
1156
+ workspaceSession: foreignKey({
1157
+ name: "session_turn_attempts_workspace_session_fk",
1158
+ columns: [table.workspaceId, table.sessionId],
1159
+ foreignColumns: [sessions.workspaceId, sessions.id],
1160
+ }).onDelete("restrict"),
1161
+ workspaceTurn: foreignKey({
1162
+ name: "session_turn_attempts_workspace_turn_fk",
1163
+ columns: [table.workspaceId, table.turnId],
1164
+ foreignColumns: [sessionTurns.workspaceId, sessionTurns.id],
1165
+ }).onDelete("restrict"),
1166
+ workspaceIdentity: uniqueIndex("session_turn_attempts_workspace_id_uq").on(
1167
+ table.workspaceId,
1168
+ table.id,
1169
+ ),
1170
+ liveTurn: uniqueIndex("session_turn_attempts_live_turn_uq")
1171
+ .on(table.workspaceId, table.turnId)
1172
+ .where(sql`${table.state} in ('claimed', 'running')`),
1173
+ liveSession: uniqueIndex("session_turn_attempts_live_session_uq")
1174
+ .on(table.workspaceId, table.sessionId)
1175
+ .where(sql`${table.state} in ('claimed', 'running')`),
1176
+ dispatch: uniqueIndex("session_turn_attempts_dispatch_uq").on(
1177
+ table.workspaceId,
1178
+ table.temporalWorkflowRunId,
1179
+ table.temporalActivityId,
1180
+ ),
1181
+ leaseExpiry: index("session_turn_attempts_lease_expiry_idx")
1182
+ .on(table.leaseExpiresAt, table.workspaceId, table.sessionId)
1183
+ .where(sql`${table.state} in ('claimed', 'running')`),
1184
+ stateValid: check(
1185
+ "session_turn_attempts_state_check",
1186
+ sql`${table.state} in ('claimed', 'running', 'closed')`,
1187
+ ),
1188
+ outcomeValid: check(
1189
+ "session_turn_attempts_outcome_check",
1190
+ sql`${table.outcome} is null or ${table.outcome} in (
1191
+ 'completed', 'failed', 'cancelled', 'superseded', 'requires_action',
1192
+ 'interrupted_recoverable', 'lease_lost_recoverable', 'pre_cutover_closed'
1193
+ )`,
1194
+ ),
1195
+ closedConsistent: check(
1196
+ "session_turn_attempts_closed_check",
1197
+ sql`(${table.state} = 'closed' and ${table.outcome} is not null and ${table.closedAt} is not null)
1198
+ or (${table.state} <> 'closed' and ${table.outcome} is null and ${table.closedAt} is null)`,
1199
+ ),
1200
+ }),
1201
+ );
1202
+
1203
+ // One durable idempotency/operation record for every queue, control,
1204
+ // foreground Send/Steer, and Agent MCP mutation. The database migration owns
1205
+ // the NULLS NOT DISTINCT uniqueness form because Drizzle does not model it.
1206
+ export const sessionCommandReceipts = pgTable(
1207
+ "session_command_receipts",
1208
+ {
1209
+ id: uuid("id").primaryKey().defaultRandom(),
1210
+ accountId: uuid("account_id").notNull(),
1211
+ workspaceId: uuid("workspace_id").notNull(),
1212
+ actorType: text("actor_type").notNull(),
1213
+ actorSubjectId: text("actor_subject_id"),
1214
+ actorAttemptId: uuid("actor_attempt_id"),
1215
+ action: text("action").notNull(),
1216
+ targetSessionId: uuid("target_session_id"),
1217
+ targetTurnId: uuid("target_turn_id"),
1218
+ operationKey: text("operation_key").notNull(),
1219
+ canonicalRequestHash: text("canonical_request_hash").notNull(),
1220
+ appliedControlRevision: bigint("applied_control_revision", { mode: "number" }),
1221
+ appliedQueueVersion: integer("applied_queue_version"),
1222
+ appliedTurnVersion: integer("applied_turn_version"),
1223
+ appliedDraftRevision: bigint("applied_draft_revision", { mode: "number" }),
1224
+ result: jsonb("result").$type<Record<string, unknown>>().notNull().default({}),
1225
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1226
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1227
+ },
1228
+ (table) => ({
1229
+ workspaceAccount: foreignKey({
1230
+ name: "session_command_receipts_workspace_account_fk",
1231
+ columns: [table.workspaceId, table.accountId],
1232
+ foreignColumns: [workspaces.id, workspaces.accountId],
1233
+ }).onDelete("cascade"),
1234
+ actorAttempt: foreignKey({
1235
+ name: "session_command_receipts_actor_attempt_fk",
1236
+ columns: [table.workspaceId, table.actorAttemptId],
1237
+ foreignColumns: [sessionTurnAttempts.workspaceId, sessionTurnAttempts.id],
1238
+ }).onDelete("restrict"),
1239
+ targetSession: foreignKey({
1240
+ name: "session_command_receipts_target_session_fk",
1241
+ columns: [table.workspaceId, table.targetSessionId],
1242
+ foreignColumns: [sessions.workspaceId, sessions.id],
1243
+ }).onDelete("restrict"),
1244
+ targetTurn: foreignKey({
1245
+ name: "session_command_receipts_target_turn_fk",
1246
+ columns: [table.workspaceId, table.targetTurnId],
1247
+ foreignColumns: [sessionTurns.workspaceId, sessionTurns.id],
1248
+ }).onDelete("restrict"),
1249
+ workspaceIdentity: uniqueIndex("session_command_receipts_workspace_id_uq").on(
1250
+ table.workspaceId,
1251
+ table.id,
1252
+ ),
1253
+ targetCreated: index("session_command_receipts_target_created_idx").on(
1254
+ table.workspaceId,
1255
+ table.targetSessionId,
1256
+ table.createdAt,
1257
+ ),
1258
+ actorValid: check(
1259
+ "session_command_receipts_actor_check",
1260
+ sql`(
1261
+ ${table.actorType} = 'agent_attempt'
1262
+ and ${table.actorAttemptId} is not null
1263
+ and ${table.actorSubjectId} is null
1264
+ ) or (
1265
+ ${table.actorType} in ('human', 'operator')
1266
+ and ${table.actorSubjectId} is not null
1267
+ and ${table.actorAttemptId} is null
1268
+ )`,
1269
+ ),
1270
+ }),
1271
+ );
1272
+
1273
+ // One workspace-scoped durable invalidation per committed control revision.
1274
+ // This is deliberately separate from conversation/session events: a parent or
1275
+ // workspace Pause can change thousands of effective projections without
1276
+ // manufacturing one event (or queue row) per descendant.
1277
+ export const workspaceControlEvents = pgTable(
1278
+ "workspace_control_events",
1279
+ {
1280
+ id: uuid("id").primaryKey().defaultRandom(),
1281
+ accountId: uuid("account_id").notNull(),
1282
+ workspaceId: uuid("workspace_id").notNull(),
1283
+ revision: bigint("revision", { mode: "number" }).notNull(),
1284
+ scope: text("scope").notNull(),
1285
+ rootSessionId: uuid("root_session_id"),
1286
+ action: text("action").notNull(),
1287
+ automatic: boolean("automatic").notNull().default(false),
1288
+ reason: text("reason"),
1289
+ actor: text("actor").notNull(),
1290
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1291
+ },
1292
+ (table) => ({
1293
+ workspaceAccount: foreignKey({
1294
+ name: "workspace_control_events_workspace_account_fk",
1295
+ columns: [table.workspaceId, table.accountId],
1296
+ foreignColumns: [workspaces.id, workspaces.accountId],
1297
+ }).onDelete("cascade"),
1298
+ rootSession: foreignKey({
1299
+ name: "workspace_control_events_root_session_fk",
1300
+ columns: [table.workspaceId, table.rootSessionId],
1301
+ foreignColumns: [sessions.workspaceId, sessions.id],
1302
+ }).onDelete("restrict"),
1303
+ workspaceRevision: uniqueIndex("workspace_control_events_workspace_revision_uq").on(
1304
+ table.workspaceId,
1305
+ table.revision,
1306
+ ),
1307
+ revisionValid: check("workspace_control_events_revision_check", sql`${table.revision} > 0`),
1308
+ shapeValid: check(
1309
+ "workspace_control_events_shape_check",
1310
+ sql`(${table.scope} = 'workspace' and ${table.rootSessionId} is null)
1311
+ or (${table.scope} = 'session' and ${table.rootSessionId} is not null)`,
1312
+ ),
1313
+ actionValid: check(
1314
+ "workspace_control_events_action_check",
1315
+ sql`${table.action} in ('pause', 'resume')`,
1316
+ ),
1317
+ }),
1318
+ );
1319
+
1320
+ // An interruption is an independently durable request against an exact live
1321
+ // attempt. Multiple Pause/Steer causes coexist; no scalar session field owns
1322
+ // delivery or settlement.
1323
+ export const sessionAttemptInterruptions = pgTable(
1324
+ "session_attempt_interruptions",
1325
+ {
1326
+ id: uuid("id").primaryKey().defaultRandom(),
1327
+ accountId: uuid("account_id").notNull(),
1328
+ workspaceId: uuid("workspace_id").notNull(),
1329
+ sessionId: uuid("session_id").notNull(),
1330
+ operationId: uuid("operation_id").notNull(),
1331
+ attemptId: uuid("attempt_id").notNull(),
1332
+ kind: text("kind").notNull(),
1333
+ controlRevision: bigint("control_revision", { mode: "number" }).notNull(),
1334
+ state: text("state").notNull().default("pending"),
1335
+ requestedAt: timestamp("requested_at", { withTimezone: true }).notNull().defaultNow(),
1336
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
1337
+ acknowledgedAt: timestamp("acknowledged_at", { withTimezone: true }),
1338
+ settledAt: timestamp("settled_at", { withTimezone: true }),
1339
+ },
1340
+ (table) => ({
1341
+ workspaceAccount: foreignKey({
1342
+ name: "session_attempt_interruptions_workspace_account_fk",
1343
+ columns: [table.workspaceId, table.accountId],
1344
+ foreignColumns: [workspaces.id, workspaces.accountId],
1345
+ }).onDelete("cascade"),
1346
+ workspaceSession: foreignKey({
1347
+ name: "session_attempt_interruptions_workspace_session_fk",
1348
+ columns: [table.workspaceId, table.sessionId],
1349
+ foreignColumns: [sessions.workspaceId, sessions.id],
1350
+ }).onDelete("restrict"),
1351
+ operation: foreignKey({
1352
+ name: "session_attempt_interruptions_operation_fk",
1353
+ columns: [table.workspaceId, table.operationId],
1354
+ foreignColumns: [sessionCommandReceipts.workspaceId, sessionCommandReceipts.id],
1355
+ }).onDelete("restrict"),
1356
+ attempt: foreignKey({
1357
+ name: "session_attempt_interruptions_attempt_fk",
1358
+ columns: [table.workspaceId, table.attemptId],
1359
+ foreignColumns: [sessionTurnAttempts.workspaceId, sessionTurnAttempts.id],
1360
+ }).onDelete("restrict"),
1361
+ operationAttempt: uniqueIndex("session_attempt_interruptions_operation_attempt_uq").on(
1362
+ table.operationId,
1363
+ table.attemptId,
1364
+ ),
1365
+ unsettled: index("session_attempt_interruptions_unsettled_idx")
1366
+ .on(table.workspaceId, table.sessionId, table.requestedAt)
1367
+ .where(sql`${table.state} in ('pending', 'delivered', 'acknowledged')`),
1368
+ kindValid: check(
1369
+ "session_attempt_interruptions_kind_check",
1370
+ sql`${table.kind} in ('session_pause', 'workspace_pause', 'steer', 'maintenance')`,
1371
+ ),
1372
+ stateValid: check(
1373
+ "session_attempt_interruptions_state_check",
1374
+ sql`${table.state} in ('pending', 'delivered', 'acknowledged', 'settled', 'rejected_stale')`,
1375
+ ),
1376
+ }),
1377
+ );
1378
+
1379
+ // Private, authenticated-subject composer truth. Editing a queued prompt and
1380
+ // restoring it here is one transaction; human drafts are never agent-visible.
1381
+ export const composerDrafts = pgTable(
1382
+ "composer_drafts",
1383
+ {
1384
+ id: uuid("id").primaryKey().defaultRandom(),
1385
+ accountId: uuid("account_id").notNull(),
1386
+ workspaceId: uuid("workspace_id").notNull(),
1387
+ sessionId: uuid("session_id").notNull(),
1388
+ subjectId: text("subject_id").notNull(),
1389
+ revision: bigint("revision", { mode: "number" }).notNull().default(1),
1390
+ text: text("text").notNull().default(""),
1391
+ resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1392
+ tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1393
+ model: text("model").notNull(),
1394
+ reasoningEffort: text("reasoning_effort").notNull(),
1395
+ sourceTurnId: uuid("source_turn_id"),
1396
+ sourceTurnVersion: integer("source_turn_version"),
1397
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1398
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1399
+ },
1400
+ (table) => ({
1401
+ workspaceAccount: foreignKey({
1402
+ name: "composer_drafts_workspace_account_fk",
1403
+ columns: [table.workspaceId, table.accountId],
1404
+ foreignColumns: [workspaces.id, workspaces.accountId],
1405
+ }).onDelete("cascade"),
1406
+ workspaceSession: foreignKey({
1407
+ name: "composer_drafts_workspace_session_fk",
1408
+ columns: [table.workspaceId, table.sessionId],
1409
+ foreignColumns: [sessions.workspaceId, sessions.id],
1410
+ }).onDelete("cascade"),
1411
+ sourceTurn: foreignKey({
1412
+ name: "composer_drafts_source_turn_fk",
1413
+ columns: [table.workspaceId, table.sourceTurnId],
1414
+ foreignColumns: [sessionTurns.workspaceId, sessionTurns.id],
1415
+ }).onDelete("restrict"),
1416
+ subjectSession: uniqueIndex("composer_drafts_subject_session_uq").on(
1417
+ table.workspaceId,
1418
+ table.sessionId,
1419
+ table.subjectId,
1420
+ ),
1421
+ subjectValid: check(
1422
+ "composer_drafts_subject_check",
1423
+ sql`length(btrim(${table.subjectId})) > 0`,
1424
+ ),
1425
+ revisionValid: check("composer_drafts_revision_check", sql`${table.revision} >= 1`),
1426
+ }),
1427
+ );
1428
+
1429
+ export const sessionSystemUpdates = pgTable(
1430
+ "session_system_updates",
1431
+ {
1432
+ id: uuid("id").primaryKey().defaultRandom(),
1433
+ accountId: uuid("account_id")
1434
+ .notNull()
1435
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1436
+ workspaceId: uuid("workspace_id")
1437
+ .notNull()
1438
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1439
+ sessionId: uuid("session_id")
1440
+ .notNull()
1441
+ .references(() => sessions.id, { onDelete: "cascade" }),
1442
+ kind: text("kind").notNull(),
1443
+ classification: text("classification").notNull().default("info"),
1444
+ sourceId: text("source_id").notNull(),
1445
+ dedupeKey: text("dedupe_key").notNull(),
1446
+ summary: text("summary").notNull(),
1447
+ payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
1448
+ lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
1449
+ // pending: eligible to start/attach to an inference; deferred: preserved
1450
+ // after a failed internal-only inference but dormant until a real prompt or
1451
+ // a genuinely new pending update arrives; delivered/cancelled/failed are
1452
+ // terminal for that delivery attempt.
1453
+ state: text("state").notNull().default("pending"),
1454
+ deliveredTurnId: uuid("delivered_turn_id").references(() => sessionTurns.id, {
1455
+ onDelete: "set null",
1456
+ }),
1457
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
1458
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1459
+ },
1460
+ (table) => ({
1461
+ kindValid: check(
1462
+ "system_updates_kind_check",
1463
+ sql`${table.kind} in ('scheduled_occurrence', 'goal_continuation', 'agent_message', 'agent_steer_instruction', 'child_terminal_result')`,
1464
+ ),
1465
+ payloadKindValid: check(
1466
+ "system_updates_payload_kind_check",
1467
+ sql`${table.payload} ->> 'type' = ${table.kind}`,
1468
+ ),
1469
+ stateValid: check(
1470
+ "system_updates_state_check",
1471
+ sql`${table.state} in ('pending', 'deferred', 'delivered', 'cancelled', 'superseded', 'failed')`,
1472
+ ),
1473
+ dedupe: uniqueIndex("session_system_updates_dedupe_uq").on(
1474
+ table.workspaceId,
1475
+ table.sessionId,
1476
+ table.dedupeKey,
1477
+ ),
1478
+ pending: index("session_system_updates_pending_idx").on(
1479
+ table.workspaceId,
1480
+ table.sessionId,
1481
+ table.state,
1482
+ table.createdAt,
1483
+ ),
1484
+ }),
1485
+ );
1486
+
1487
+ /**
1488
+ * Durable child-terminal producer outbox. The source terminal transaction
1489
+ * inserts this row; fan-in delivery marks it delivered inside
1490
+ * addSessionSystemUpdateWithSourceMutation. A bounded reconciler may retry a
1491
+ * committed row after any worker/process death without duplicating a member.
1492
+ */
1493
+ export const sessionSystemUpdateOutbox = pgTable(
1494
+ "session_system_update_outbox",
1495
+ {
1496
+ id: uuid("id").primaryKey().defaultRandom(),
1497
+ accountId: uuid("account_id")
1498
+ .notNull()
1499
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1500
+ workspaceId: uuid("workspace_id")
1501
+ .notNull()
1502
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1503
+ sourceSessionId: uuid("source_session_id")
1504
+ .notNull()
1505
+ .references(() => sessions.id, { onDelete: "cascade" }),
1506
+ targetSessionId: uuid("target_session_id")
1507
+ .notNull()
1508
+ .references(() => sessions.id, { onDelete: "cascade" }),
1509
+ dedupeKey: text("dedupe_key").notNull(),
1510
+ kind: text("kind").notNull(),
1511
+ classification: text("classification").notNull(),
1512
+ sourceId: text("source_id").notNull(),
1513
+ summary: text("summary").notNull(),
1514
+ payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
1515
+ lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
1516
+ status: text("status").notNull().default("pending"),
1517
+ attempts: integer("attempts").notNull().default(0),
1518
+ updateId: uuid("update_id"),
1519
+ lastError: text("last_error"),
1520
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
1521
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1522
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1523
+ },
1524
+ (table) => ({
1525
+ kindValid: check(
1526
+ "system_update_outbox_kind_check",
1527
+ sql`${table.kind} = 'child_terminal_result'`,
1528
+ ),
1529
+ payloadKindValid: check(
1530
+ "system_update_outbox_payload_kind_check",
1531
+ sql`${table.payload} ->> 'type' = 'child_terminal_result'`,
1532
+ ),
1533
+ dedupe: uniqueIndex("session_system_update_outbox_dedupe_uq").on(
1534
+ table.workspaceId,
1535
+ table.dedupeKey,
1536
+ ),
1537
+ pending: index("session_system_update_outbox_pending_idx").on(table.status, table.createdAt),
1538
+ }),
1539
+ );
1540
+
1541
+ /**
1542
+ * Transactional delivery ledger for session-workflow wakeups. Postgres owns
1543
+ * work eligibility; Temporal signals are only nudges. One coalescing row per
1544
+ * session makes a committed mutation repairable without periodically scanning
1545
+ * every session that happens to look runnable.
1546
+ */
1547
+ export const sessionWorkflowWakeOutbox = pgTable(
1548
+ "session_workflow_wake_outbox",
1549
+ {
1550
+ sessionId: uuid("session_id").primaryKey(),
1551
+ accountId: uuid("account_id").notNull(),
1552
+ workspaceId: uuid("workspace_id").notNull(),
1553
+ temporalWorkflowId: text("temporal_workflow_id").notNull(),
1554
+ wakeRevision: bigint("wake_revision", { mode: "number" }).notNull().default(1),
1555
+ deliveredRevision: bigint("delivered_revision", { mode: "number" }).notNull().default(0),
1556
+ reason: text("reason").notNull(),
1557
+ attempts: integer("attempts").notNull().default(0),
1558
+ nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
1559
+ lastError: text("last_error"),
1560
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1561
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1562
+ },
1563
+ (table) => ({
1564
+ revisionValid: check(
1565
+ "session_workflow_wake_outbox_revision_check",
1566
+ sql`${table.wakeRevision} > 0 and ${table.deliveredRevision} >= 0 and ${table.deliveredRevision} <= ${table.wakeRevision}`,
1567
+ ),
1568
+ workspaceAccount: foreignKey({
1569
+ name: "session_workflow_wake_outbox_workspace_account_fk",
1570
+ columns: [table.workspaceId, table.accountId],
1571
+ foreignColumns: [workspaces.id, workspaces.accountId],
1572
+ }).onDelete("cascade"),
1573
+ workspaceSessionFk: foreignKey({
1574
+ name: "session_workflow_wake_outbox_workspace_session_fk",
1575
+ columns: [table.workspaceId, table.sessionId],
1576
+ foreignColumns: [sessions.workspaceId, sessions.id],
1577
+ }).onDelete("cascade"),
1578
+ workspaceSession: uniqueIndex("session_workflow_wake_outbox_workspace_session_uq").on(
1579
+ table.workspaceId,
1580
+ table.sessionId,
1581
+ ),
1582
+ pending: index("session_workflow_wake_outbox_pending_idx")
1583
+ .on(table.nextAttemptAt, table.updatedAt, table.sessionId)
1584
+ .where(sql`${table.wakeRevision} > ${table.deliveredRevision}`),
1585
+ }),
1586
+ );
1587
+
1588
+ export const sessionGoals = pgTable(
1589
+ "session_goals",
1590
+ {
1591
+ id: uuid("id").primaryKey().defaultRandom(),
1592
+ accountId: uuid("account_id")
1593
+ .notNull()
1594
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1595
+ workspaceId: uuid("workspace_id")
1596
+ .notNull()
1597
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1598
+ sessionId: uuid("session_id")
1599
+ .notNull()
1600
+ .references(() => sessions.id, { onDelete: "cascade" }),
1601
+ status: text("status").notNull().default("active"), // active | paused | completed
1602
+ text: text("text").notNull(),
1603
+ successCriteria: text("success_criteria"),
1604
+ evidence: text("evidence"), // set by goal_complete
1605
+ rationale: text("rationale"), // set by goal_pause
1606
+ pausedReason: text("paused_reason"), // agent | user_pause | api | no_progress | max_auto_continuations | limits
1607
+ createdBy: text("created_by").notNull().default("api"), // api | agent | scheduled_task
1608
+ version: integer("version").notNull().default(1), // bumped on every set/update; progress signal
1609
+ autoContinuations: integer("auto_continuations").notNull().default(0),
1610
+ noProgressStreak: integer("no_progress_streak").notNull().default(0),
1611
+ maxAutoContinuations: integer("max_auto_continuations"), // per-goal override; a configured settings cap (if any) remains the hard ceiling
1612
+ lastContinuationTurnId: uuid("last_continuation_turn_id"),
1613
+ versionAtLastContinuation: integer("version_at_last_continuation"),
1614
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1615
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1616
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1617
+ },
1618
+ (table) => ({
1619
+ workspaceIdentity: uniqueIndex("session_goals_workspace_id_idx").on(
1620
+ table.workspaceId,
1621
+ table.id,
1622
+ ),
1623
+ workspaceSession: uniqueIndex("session_goals_workspace_session_idx").on(
1624
+ table.workspaceId,
1625
+ table.sessionId,
1626
+ ),
1627
+ status: index("session_goals_workspace_status_idx").on(table.workspaceId, table.status),
1628
+ }),
1629
+ );
1630
+
1631
+ // OPE-21: one durable, coalescing capacity waiter per session. The row is both
1632
+ // the wait state and the commit->signal outbox: capacity mutations increment
1633
+ // wakeRevision in the SAME transaction as the mutation, while the session
1634
+ // workflow advances observedWakeRevision only after it has re-evaluated the
1635
+ // allocator. Temporal signals are therefore repairable nudges rather than the
1636
+ // source of truth. No credential material or provider response is stored here.
1637
+ //
1638
+ // The session/goal/turn foreign keys are declared in migration 0053 so the
1639
+ // table keeps the same composite workspace-integrity posture as credential
1640
+ // leases. Control is evaluated independently at admission and never changes a
1641
+ // capacity waiter's identity. OPE-32 supplies policyHash when accepted-turn
1642
+ // pool routing lands.
1643
+ export const codexCapacityWaiters = pgTable(
1644
+ "codex_capacity_waiters",
1645
+ {
1646
+ id: uuid("id").primaryKey().defaultRandom(),
1647
+ accountId: uuid("account_id")
1648
+ .notNull()
1649
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1650
+ workspaceId: uuid("workspace_id")
1651
+ .notNull()
1652
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1653
+ sessionId: uuid("session_id").notNull(),
1654
+ goalId: uuid("goal_id").notNull(),
1655
+ blockedTurnId: uuid("blocked_turn_id").notNull(),
1656
+ workflowId: text("workflow_id").notNull(),
1657
+ generation: integer("generation").notNull().default(1),
1658
+ status: text("status").notNull().default("waiting"), // waiting | resumed | superseded
1659
+ goalVersion: integer("goal_version").notNull(),
1660
+ policyHash: text("policy_hash"),
1661
+ earliestResetAt: timestamp("earliest_reset_at", { withTimezone: true }),
1662
+ nextCheckAt: timestamp("next_check_at", { withTimezone: true }).notNull(),
1663
+ resetKind: text("reset_kind").notNull(), // authoritative | bounded_refresh
1664
+ refreshAttempt: integer("refresh_attempt").notNull().default(0),
1665
+ // Coalescing outbox generation. Every eligibility-affecting mutation bumps
1666
+ // wakeRevision. Duplicate/lost Temporal signals are harmless because only
1667
+ // the row-locked evaluator moves observedWakeRevision and may enqueue work.
1668
+ wakeRevision: integer("wake_revision").notNull().default(1),
1669
+ observedWakeRevision: integer("observed_wake_revision").notNull().default(0),
1670
+ lastWakeReason: text("last_wake_reason").notNull().default("capacity_wait_armed"),
1671
+ resumedUpdateId: uuid("resumed_update_id"),
1672
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1673
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1674
+ },
1675
+ (table) => ({
1676
+ workspaceSession: uniqueIndex("codex_capacity_waiters_workspace_session_idx").on(
1677
+ table.workspaceId,
1678
+ table.sessionId,
1679
+ ),
1680
+ workspaceId: uniqueIndex("codex_capacity_waiters_workspace_id_idx").on(
1681
+ table.workspaceId,
1682
+ table.id,
1683
+ ),
1684
+ pending: index("codex_capacity_waiters_pending_idx").on(
1685
+ table.workspaceId,
1686
+ table.status,
1687
+ table.nextCheckAt,
1688
+ ),
1689
+ wakeRepair: index("codex_capacity_waiters_wake_repair_idx").on(
1690
+ table.status,
1691
+ table.wakeRevision,
1692
+ table.observedWakeRevision,
1693
+ ),
1694
+ }),
1695
+ );
1696
+
1697
+ export const sessionEvents = pgTable(
1698
+ "session_events",
1699
+ {
1700
+ id: uuid("id").primaryKey().defaultRandom(),
1701
+ accountId: uuid("account_id")
1702
+ .notNull()
1703
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1704
+ workspaceId: uuid("workspace_id")
1705
+ .notNull()
1706
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1707
+ sessionId: uuid("session_id")
1708
+ .notNull()
1709
+ .references(() => sessions.id, { onDelete: "cascade" }),
1710
+ turnId: uuid("turn_id"),
1711
+ turnGeneration: integer("turn_generation"),
1712
+ turnAttemptId: uuid("turn_attempt_id"),
1713
+ turnAssociation: text("turn_association"),
1714
+ duplicateOfEventId: uuid("duplicate_of_event_id"),
1715
+ duplicateReason: text("duplicate_reason"),
1716
+ sequence: integer("sequence").notNull(),
1717
+ type: text("type").notNull(),
1718
+ payload: jsonb("payload").$type<unknown>().notNull().default({}),
1719
+ clientEventId: text("client_event_id"),
1720
+ producerId: text("producer_id"),
1721
+ producerSeq: integer("producer_seq"),
1722
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1723
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1724
+ },
1725
+ (table) => ({
1726
+ workspaceAttempt: foreignKey({
1727
+ name: "session_events_workspace_attempt_fk",
1728
+ columns: [table.workspaceId, table.turnAttemptId],
1729
+ foreignColumns: [sessionTurnAttempts.workspaceId, sessionTurnAttempts.id],
1730
+ }).onDelete("restrict"),
1731
+ sessionSequence: uniqueIndex("session_events_workspace_session_sequence_idx").on(
1732
+ table.workspaceId,
1733
+ table.sessionId,
1734
+ table.sequence,
1735
+ ),
1736
+ clientEvent: uniqueIndex("session_events_workspace_client_event_idx")
1737
+ .on(table.workspaceId, table.sessionId, table.clientEventId)
1738
+ .where(sql`${table.clientEventId} is not null`),
1739
+ producer: uniqueIndex("session_events_workspace_producer_idx")
1740
+ .on(table.workspaceId, table.sessionId, table.producerId, table.producerSeq)
1741
+ .where(sql`${table.producerId} is not null and ${table.producerSeq} is not null`),
1742
+ sessionCreated: index("session_events_workspace_session_created_idx").on(
1743
+ table.workspaceId,
1744
+ table.sessionId,
1745
+ table.createdAt,
1746
+ ),
1747
+ }),
1748
+ );
577
1749
 
578
1750
  export const agentRunStates = pgTable("agent_run_states", {
579
1751
  id: uuid("id").primaryKey().defaultRandom(),
580
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
581
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
582
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
1752
+ accountId: uuid("account_id")
1753
+ .notNull()
1754
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1755
+ workspaceId: uuid("workspace_id")
1756
+ .notNull()
1757
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1758
+ sessionId: uuid("session_id")
1759
+ .notNull()
1760
+ .references(() => sessions.id, { onDelete: "cascade" }),
583
1761
  turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
584
1762
  stateVersion: integer("state_version").notNull(),
585
1763
  serializedRunState: text("serialized_run_state").notNull(),
@@ -591,8 +1769,8 @@ export const agentRunStates = pgTable("agent_run_states", {
591
1769
  // ChatGPT/Codex backend — account/org-bound, so a foreign blob 400s — and the
592
1770
  // foreign reasoning ids the Responses backend validates; but the blob carries
593
1771
  // NO per-item producer tag (those live only on session_history_items). So we
594
- // stamp the freezing account here: on a resume (approval decision, or the
595
- // items-mode run-state fallback) whose codex account DIFFERS from this value,
1772
+ // stamp the freezing account here: on an approval resume whose codex account
1773
+ // DIFFERS from this value,
596
1774
  // the replay path neutralizes every reasoning item's account-bound identity
597
1775
  // (encrypted_content + provider id) in the blob before it reaches the model.
598
1776
  // Deliberately NO FK: provenance must OUTLIVE the account's hard-disconnect (a
@@ -606,56 +1784,135 @@ export const agentRunStates = pgTable("agent_run_states", {
606
1784
  // Conversation truth: ordered, verbatim SDK input items (issue #35). The
607
1785
  // model-facing memory store — unredacted and replay-ready. session_events
608
1786
  // remains the redacted human/audit timeline.
609
- export const sessionHistoryItems = pgTable("session_history_items", {
610
- id: uuid("id").primaryKey().defaultRandom(),
611
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
612
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
613
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
614
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
615
- // Numeric (not integer) so the synthetic compaction-summary row can be
616
- // inserted at a FRACTIONAL position (boundaryPosition - 0.5) that sorts ahead
617
- // of the kept tail without colliding with — and thus overwriting — the real
618
- // prefix row at boundaryPosition - 1. Normally-appended rows keep whole-number
619
- // positions; only the summary uses the half-step. `mode: "number"` maps the
620
- // postgres.js string back to a JS number so every reader stays numeric.
621
- position: numeric("position", { mode: "number" }).notNull(),
622
- item: jsonb("item").$type<Record<string, unknown>>().notNull(),
623
- // Live-row flag for client-side context compaction. The read path selects
624
- // only active rows; a compaction supersedes the summarized prefix (sets this
625
- // false — never deletes, so the full transcript stays as an audit trail) and
626
- // inserts ONE synthetic active summary row at the boundary. Defaults true so
627
- // every existing and normally-appended row is live.
628
- active: boolean("active").notNull().default(true),
629
- // The Codex account that PRODUCED these items: the per-turn resolved codex
630
- // credential id (pin > workspace-active), or NULL when produced on the
631
- // non-codex / Azure path (or before this column existed). Used to strip
632
- // cross-account `reasoning.encrypted_content` blobs — those are account/org-
633
- // bound, minted by the ChatGPT/Codex backend, so replaying account A's blob
634
- // into a turn running on account B 400s. The read path drops the encrypted
635
- // reasoning of any item whose producer != the turn's current codex account.
636
- // Deliberately NO FK: provenance must OUTLIVE the account's hard-disconnect
637
- // (an ON DELETE SET NULL would erase the tag, and a stale-but-null tag still
638
- // mismatches a live codex id so the strip stays correct either way).
639
- producerCodexCredentialId: uuid("producer_codex_credential_id"),
640
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
641
- }, (table) => ({
642
- positionIdx: uniqueIndex("session_history_items_position_idx").on(table.workspaceId, table.sessionId, table.position),
643
- }));
1787
+ export const sessionHistoryItems = pgTable(
1788
+ "session_history_items",
1789
+ {
1790
+ id: uuid("id").primaryKey().defaultRandom(),
1791
+ accountId: uuid("account_id")
1792
+ .notNull()
1793
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1794
+ workspaceId: uuid("workspace_id")
1795
+ .notNull()
1796
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1797
+ sessionId: uuid("session_id")
1798
+ .notNull()
1799
+ .references(() => sessions.id, { onDelete: "cascade" }),
1800
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
1801
+ // Numeric (not integer) so the synthetic compaction-summary row can be
1802
+ // inserted at a FRACTIONAL position (boundaryPosition - 0.5) that sorts ahead
1803
+ // of the kept tail without colliding with — and thus overwriting — the real
1804
+ // prefix row at boundaryPosition - 1. Normally-appended rows keep whole-number
1805
+ // positions; only the summary uses the half-step. `mode: "number"` maps the
1806
+ // postgres.js string back to a JS number so every reader stays numeric.
1807
+ position: numeric("position", { mode: "number" }).notNull(),
1808
+ item: jsonb("item").$type<Record<string, unknown>>().notNull(),
1809
+ // Live-row flag for client-side context compaction. The read path selects
1810
+ // only active rows; a compaction supersedes the summarized prefix (sets this
1811
+ // false — never deletes, so the full transcript stays as an audit trail) and
1812
+ // inserts ONE synthetic active summary row at the boundary. Defaults true so
1813
+ // every existing and normally-appended row is live.
1814
+ active: boolean("active").notNull().default(true),
1815
+ // The Codex account that PRODUCED these items: the per-turn resolved codex
1816
+ // credential id (pin > workspace-active), or NULL when produced on the
1817
+ // non-codex / Azure path (or before this column existed). Used to strip
1818
+ // cross-account `reasoning.encrypted_content` blobs — those are account/org-
1819
+ // bound, minted by the ChatGPT/Codex backend, so replaying account A's blob
1820
+ // into a turn running on account B 400s. The read path drops the encrypted
1821
+ // reasoning of any item whose producer != the turn's current codex account.
1822
+ // Deliberately NO FK: provenance must OUTLIVE the account's hard-disconnect
1823
+ // (an ON DELETE SET NULL would erase the tag, and a stale-but-null tag still
1824
+ // mismatches a live codex id so the strip stays correct either way).
1825
+ producerCodexCredentialId: uuid("producer_codex_credential_id"),
1826
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1827
+ },
1828
+ (table) => ({
1829
+ positionIdx: uniqueIndex("session_history_items_position_idx").on(
1830
+ table.workspaceId,
1831
+ table.sessionId,
1832
+ table.position,
1833
+ ),
1834
+ }),
1835
+ );
1836
+
1837
+ // Turn-lineage ledger for a tool call that the SDK emitted but has not yet
1838
+ // produced a durably reconciled result. The raw call item is model-facing truth
1839
+ // (not the redacted session-event projection). The attempt/generation identify
1840
+ // where the call originated, but the receipt survives an approval resume into a
1841
+ // newer attempt of the same logical turn. Turn-ending transactions
1842
+ // consume these rows atomically and append a valid interrupted result so a
1843
+ // recovered model sees an explicit unknown outcome instead of a silently
1844
+ // dropped call.
1845
+ export const sessionPendingToolCalls = pgTable(
1846
+ "session_pending_tool_calls",
1847
+ {
1848
+ id: uuid("id").primaryKey().defaultRandom(),
1849
+ accountId: uuid("account_id")
1850
+ .notNull()
1851
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1852
+ workspaceId: uuid("workspace_id")
1853
+ .notNull()
1854
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1855
+ sessionId: uuid("session_id")
1856
+ .notNull()
1857
+ .references(() => sessions.id, { onDelete: "cascade" }),
1858
+ turnId: uuid("turn_id")
1859
+ .notNull()
1860
+ .references(() => sessionTurns.id, { onDelete: "cascade" }),
1861
+ executionGeneration: integer("execution_generation").notNull(),
1862
+ attemptId: uuid("attempt_id").notNull(),
1863
+ callId: text("call_id").notNull(),
1864
+ callType: text("call_type").notNull(),
1865
+ callItem: jsonb("call_item").$type<Record<string, unknown>>().notNull(),
1866
+ resultItem: jsonb("result_item").$type<Record<string, unknown>>(),
1867
+ resultRecordedAt: timestamp("result_recorded_at", { withTimezone: true }),
1868
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1869
+ },
1870
+ (table) => ({
1871
+ workspaceAttempt: foreignKey({
1872
+ name: "pending_tool_calls_workspace_attempt_fk",
1873
+ columns: [table.workspaceId, table.attemptId],
1874
+ foreignColumns: [sessionTurnAttempts.workspaceId, sessionTurnAttempts.id],
1875
+ }).onDelete("restrict"),
1876
+ turnCall: uniqueIndex("session_pending_tool_calls_turn_call_idx").on(
1877
+ table.workspaceId,
1878
+ table.turnId,
1879
+ table.callId,
1880
+ ),
1881
+ sessionTurn: index("session_pending_tool_calls_session_turn_idx").on(
1882
+ table.workspaceId,
1883
+ table.sessionId,
1884
+ table.turnId,
1885
+ ),
1886
+ }),
1887
+ );
644
1888
 
645
1889
  // Sandbox recovery descriptor, decoupled from the RunState blob: the small
646
1890
  // versioned envelope (provider handle / snapshot ref / manifest) needed to
647
1891
  // reattach, restore, or rebuild the session's sandbox on its next turn.
648
- export const sandboxSessionEnvelopes = pgTable("sandbox_session_envelopes", {
649
- id: uuid("id").primaryKey().defaultRandom(),
650
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
651
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
652
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
653
- envelope: jsonb("envelope").$type<Record<string, unknown>>().notNull(),
654
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
655
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
656
- }, (table) => ({
657
- sessionIdx: uniqueIndex("sandbox_session_envelopes_session_idx").on(table.workspaceId, table.sessionId),
658
- }));
1892
+ export const sandboxSessionEnvelopes = pgTable(
1893
+ "sandbox_session_envelopes",
1894
+ {
1895
+ id: uuid("id").primaryKey().defaultRandom(),
1896
+ accountId: uuid("account_id")
1897
+ .notNull()
1898
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1899
+ workspaceId: uuid("workspace_id")
1900
+ .notNull()
1901
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1902
+ sessionId: uuid("session_id")
1903
+ .notNull()
1904
+ .references(() => sessions.id, { onDelete: "cascade" }),
1905
+ envelope: jsonb("envelope").$type<Record<string, unknown>>().notNull(),
1906
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1907
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1908
+ },
1909
+ (table) => ({
1910
+ sessionIdx: uniqueIndex("sandbox_session_envelopes_session_idx").on(
1911
+ table.workspaceId,
1912
+ table.sessionId,
1913
+ ),
1914
+ }),
1915
+ );
659
1916
 
660
1917
  // The 4 liveness states of the singleton lease. Exported so the query layer and
661
1918
  // the stateless resume-by-id path share one source of truth for the domain.
@@ -669,83 +1926,119 @@ export const sandboxLeaseLivenessValues = ["cold", "warming", "warm", "draining"
669
1926
  // of sandboxSessionEnvelopes; sandboxGroupId is a BARE uuid (NOT an FK — the
670
1927
  // value is a session id or an ancestor's, and an FK would let a founder's
671
1928
  // deletion cascade-kill a box still in use by a spawned session).
672
- export const sandboxLeases = pgTable("sandbox_leases", {
673
- id: uuid("id").primaryKey().defaultRandom(),
674
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
675
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
676
- sandboxGroupId: uuid("sandbox_group_id").notNull(),
677
-
678
- liveness: text("liveness", { enum: sandboxLeaseLivenessValues }).notNull().default("cold"),
679
- refcount: integer("refcount").notNull().default(0),
680
- turnHolders: integer("turn_holders").notNull().default(0),
681
- viewerHolders: integer("viewer_holders").notNull().default(0),
682
-
683
- instanceId: text("instance_id"),
684
- backend: text("backend").notNull(),
685
- os: text("os").notNull().default("linux"),
686
- // The container IMAGE the group box runs (Modal image ref / docker image). A shared
687
- // box is SHARED STATE: all its sessions run the SAME filesystem, so they must run the
688
- // same image. This column stamps the image the live box was created with; a resume
689
- // whose resolved image DIFFERS is a conflict (B3): a solo holder recreates the box on
690
- // the new image, N-holders are rejected (SandboxImageConflictError). Nullable — a
691
- // legacy/cold row reads NULL = "image unknown", which never conflicts.
692
- image: text("image"),
693
- dataPlaneUrl: text("data_plane_url"),
694
- // The REAL PTY terminal (ttyd pty-ws) rides a SEPARATE provider tunnel (7681)
695
- // from the desktop noVNC (6080), so its resolved URL is cached independently.
696
- // Recorded under the epoch fence by recordLeaseTerminalDataPlaneUrl; reset to
697
- // null on every box re-key (warm-commit / fail / drain), symmetric with
698
- // data_plane_url.
699
- terminalDataPlaneUrl: text("terminal_data_plane_url"),
700
-
701
- // integer (NOT bigint): the lease-epoch spike proved a raw int8 read returns a
702
- // JS STRING from postgres-js, breaking the strict epoch-fence comparison (it
703
- // was always-true → every turn fenced); int4 returns a JS number, the fix.
704
- // Epochs never approach 2^31, so the narrower type loses nothing.
705
- leaseEpoch: integer("lease_epoch").notNull().default(0),
706
-
707
- // The group box-envelope (the "envelope split" Critical): the small recovery
708
- // descriptor to resume()-by-id the group's box without a per-session join.
709
- resumeBackendId: text("resume_backend_id"),
710
- resumeState: jsonb("resume_state").$type<Record<string, unknown>>(),
711
-
712
- // Warm-time billing cursor: last_meter_at = accrual cursor; last_meter_tick =
713
- // idempotency tick (warm_seconds accrued idempotent on
714
- // (sandbox_group_id, lease_epoch, last_meter_tick) in P2.1).
715
- lastMeterAt: timestamp("last_meter_at", { withTimezone: true }),
716
- lastMeterTick: integer("last_meter_tick").notNull().default(0),
717
-
718
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
719
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
720
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
721
- }, (table) => ({
722
- groupIdx: uniqueIndex("sandbox_leases_group_idx").on(table.workspaceId, table.sandboxGroupId),
723
- reaperIdx: index("sandbox_leases_reaper_idx").on(table.expiresAt)
724
- .where(sql`${table.liveness} in ('warming','warm','draining')`),
725
- }));
1929
+ export const sandboxLeases = pgTable(
1930
+ "sandbox_leases",
1931
+ {
1932
+ id: uuid("id").primaryKey().defaultRandom(),
1933
+ accountId: uuid("account_id")
1934
+ .notNull()
1935
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
1936
+ workspaceId: uuid("workspace_id")
1937
+ .notNull()
1938
+ .references(() => workspaces.id, { onDelete: "cascade" }),
1939
+ sandboxGroupId: uuid("sandbox_group_id").notNull(),
1940
+
1941
+ liveness: text("liveness", { enum: sandboxLeaseLivenessValues }).notNull().default("cold"),
1942
+ refcount: integer("refcount").notNull().default(0),
1943
+ turnHolders: integer("turn_holders").notNull().default(0),
1944
+ viewerHolders: integer("viewer_holders").notNull().default(0),
1945
+
1946
+ instanceId: text("instance_id"),
1947
+ backend: text("backend").notNull(),
1948
+ os: text("os").notNull().default("linux"),
1949
+ // The container IMAGE the group box runs (Modal image ref / docker image). A shared
1950
+ // box is SHARED STATE: all its sessions run the SAME filesystem, so they must run the
1951
+ // same image. This column stamps the image the live box was created with; a resume
1952
+ // whose resolved image DIFFERS is a conflict (B3): a solo holder recreates the box on
1953
+ // the new image, N-holders are rejected (SandboxImageConflictError). Nullable — a
1954
+ // legacy/cold row reads NULL = "image unknown", which never conflicts.
1955
+ image: text("image"),
1956
+ // The frozen rig version the live box was created under (M3). Like `image`,
1957
+ // this is SHARED STATE: all the box's sessions run the same rig-baked setup,
1958
+ // so a resume resolving a DIFFERENT rig_version_id conflicts (solo holder
1959
+ // recreates cold on the new rig; N-holders throw SandboxRigConflictError).
1960
+ // Nullable — a legacy/cold row or a rig-less session reads NULL = "rig
1961
+ // unknown", which never conflicts. No FK (symmetric with sandbox_group_id's
1962
+ // bare-uuid rationale: this lease outlives no single rig_versions row's RLS).
1963
+ rigVersionId: uuid("rig_version_id"),
1964
+ dataPlaneUrl: text("data_plane_url"),
1965
+ // The REAL PTY terminal (ttyd pty-ws) rides a SEPARATE provider tunnel (7681)
1966
+ // from the desktop noVNC (6080), so its resolved URL is cached independently.
1967
+ // Recorded under the epoch fence by recordLeaseTerminalDataPlaneUrl; reset to
1968
+ // null on every box re-key (warm-commit / fail / drain), symmetric with
1969
+ // data_plane_url.
1970
+ terminalDataPlaneUrl: text("terminal_data_plane_url"),
1971
+
1972
+ // integer (NOT bigint): the lease-epoch spike proved a raw int8 read returns a
1973
+ // JS STRING from postgres-js, breaking the strict epoch-fence comparison (it
1974
+ // was always-true → every turn fenced); int4 returns a JS number, the fix.
1975
+ // Epochs never approach 2^31, so the narrower type loses nothing.
1976
+ leaseEpoch: integer("lease_epoch").notNull().default(0),
1977
+
1978
+ // The group box-envelope (the "envelope split" Critical): the small recovery
1979
+ // descriptor to resume()-by-id the group's box without a per-session join.
1980
+ resumeBackendId: text("resume_backend_id"),
1981
+ resumeState: jsonb("resume_state").$type<Record<string, unknown>>(),
1982
+
1983
+ // Warm-time billing cursor: last_meter_at = accrual cursor; last_meter_tick =
1984
+ // idempotency tick (warm_seconds accrued idempotent on
1985
+ // (sandbox_group_id, lease_epoch, last_meter_tick) in P2.1).
1986
+ lastMeterAt: timestamp("last_meter_at", { withTimezone: true }),
1987
+ lastMeterTick: integer("last_meter_tick").notNull().default(0),
1988
+
1989
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
1990
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1991
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1992
+ },
1993
+ (table) => ({
1994
+ groupIdx: uniqueIndex("sandbox_leases_group_idx").on(table.workspaceId, table.sandboxGroupId),
1995
+ reaperIdx: index("sandbox_leases_reaper_idx")
1996
+ .on(table.expiresAt)
1997
+ .where(sql`${table.liveness} in ('warming','warm','draining')`),
1998
+ }),
1999
+ );
726
2000
 
727
2001
  // N rows per group: one per live holder. Makes release idempotent
728
2002
  // (delete-my-row, never blind decrement) and lets the reaper recompute refcount.
729
- export const sandboxLeaseHolders = pgTable("sandbox_lease_holders", {
730
- id: uuid("id").primaryKey().defaultRandom(),
731
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
732
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
733
- leaseId: uuid("lease_id").notNull().references(() => sandboxLeases.id, { onDelete: "cascade" }),
734
- kind: text("kind", { enum: ["turn", "viewer"] }).notNull(),
735
- holderId: text("holder_id").notNull(),
736
- // The attributing session within the (possibly shared) group.
737
- subjectId: uuid("subject_id"),
738
- lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }).notNull().defaultNow(),
739
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
740
- }, (table) => ({
741
- holderIdx: uniqueIndex("sandbox_lease_holders_holder_idx").on(table.leaseId, table.kind, table.holderId),
742
- staleIdx: index("sandbox_lease_holders_stale_idx").on(table.kind, table.lastHeartbeatAt),
743
- leaseIdx: index("sandbox_lease_holders_lease_idx").on(table.leaseId),
744
- }));
2003
+ export const sandboxLeaseHolders = pgTable(
2004
+ "sandbox_lease_holders",
2005
+ {
2006
+ id: uuid("id").primaryKey().defaultRandom(),
2007
+ accountId: uuid("account_id")
2008
+ .notNull()
2009
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2010
+ workspaceId: uuid("workspace_id")
2011
+ .notNull()
2012
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2013
+ leaseId: uuid("lease_id")
2014
+ .notNull()
2015
+ .references(() => sandboxLeases.id, { onDelete: "cascade" }),
2016
+ kind: text("kind", { enum: ["turn", "viewer"] }).notNull(),
2017
+ holderId: text("holder_id").notNull(),
2018
+ // The attributing session within the (possibly shared) group.
2019
+ subjectId: uuid("subject_id"),
2020
+ lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }).notNull().defaultNow(),
2021
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2022
+ },
2023
+ (table) => ({
2024
+ holderIdx: uniqueIndex("sandbox_lease_holders_holder_idx").on(
2025
+ table.leaseId,
2026
+ table.kind,
2027
+ table.holderId,
2028
+ ),
2029
+ staleIdx: index("sandbox_lease_holders_stale_idx").on(table.kind, table.lastHeartbeatAt),
2030
+ leaseIdx: index("sandbox_lease_holders_lease_idx").on(table.leaseId),
2031
+ }),
2032
+ );
745
2033
 
746
2034
  // The recording lifecycle states (P4.3). Exported so the activity + the query
747
2035
  // layer share one source of truth for the §3.1 state machine.
748
- export const sessionRecordingStateValues = ["recording", "finalizing", "available", "failed"] as const;
2036
+ export const sessionRecordingStateValues = [
2037
+ "recording",
2038
+ "finalizing",
2039
+ "available",
2040
+ "failed",
2041
+ ] as const;
749
2042
  export const sessionRecordingModeValues = ["manual", "on-turn", "on-verify"] as const;
750
2043
  export const sessionRecordingCodecValues = ["h264-mp4", "vp9-webm"] as const;
751
2044
 
@@ -755,31 +2048,104 @@ export const sessionRecordingCodecValues = ["h264-mp4", "vp9-webm"] as const;
755
2048
  // process that holds the resumed-by-id handle (never a Temporal payload, F10).
756
2049
  // Mirrors the account/workspace/session FK chain of sandboxSessionEnvelopes;
757
2050
  // turnId is ON DELETE SET NULL (a deleted turn must not kill the artifact row).
758
- export const sessionRecordings = pgTable("session_recordings", {
759
- id: uuid("id").primaryKey().defaultRandom(),
760
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
761
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
762
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
763
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
764
-
765
- state: text("state", { enum: sessionRecordingStateValues }).notNull(),
766
- mode: text("mode", { enum: sessionRecordingModeValues }).notNull(),
767
- codec: text("codec", { enum: sessionRecordingCodecValues }).notNull(),
768
-
769
- storageKey: text("storage_key"),
770
- sizeBytes: bigint("size_bytes", { mode: "number" }),
771
- durationSeconds: numeric("duration_seconds").$type<number>(),
772
-
773
- width: integer("width").notNull(),
774
- height: integer("height").notNull(),
775
-
776
- reason: text("reason"),
777
-
778
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
779
- finalizedAt: timestamp("finalized_at", { withTimezone: true }),
780
- }, (table) => ({
781
- sessionIdx: index("session_recordings_session_idx").on(table.workspaceId, table.sessionId, table.createdAt),
782
- }));
2051
+ export const sessionRecordings = pgTable(
2052
+ "session_recordings",
2053
+ {
2054
+ id: uuid("id").primaryKey().defaultRandom(),
2055
+ accountId: uuid("account_id")
2056
+ .notNull()
2057
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2058
+ workspaceId: uuid("workspace_id")
2059
+ .notNull()
2060
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2061
+ sessionId: uuid("session_id")
2062
+ .notNull()
2063
+ .references(() => sessions.id, { onDelete: "cascade" }),
2064
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2065
+
2066
+ state: text("state", { enum: sessionRecordingStateValues }).notNull(),
2067
+ mode: text("mode", { enum: sessionRecordingModeValues }).notNull(),
2068
+ codec: text("codec", { enum: sessionRecordingCodecValues }).notNull(),
2069
+
2070
+ storageKey: text("storage_key"),
2071
+ sizeBytes: bigint("size_bytes", { mode: "number" }),
2072
+ durationSeconds: numeric("duration_seconds").$type<number>(),
2073
+
2074
+ width: integer("width").notNull(),
2075
+ height: integer("height").notNull(),
2076
+
2077
+ reason: text("reason"),
2078
+
2079
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2080
+ finalizedAt: timestamp("finalized_at", { withTimezone: true }),
2081
+ },
2082
+ (table) => ({
2083
+ sessionIdx: index("session_recordings_session_idx").on(
2084
+ table.workspaceId,
2085
+ table.sessionId,
2086
+ table.createdAt,
2087
+ ),
2088
+ }),
2089
+ );
2090
+
2091
+ // Workbench v2 turn-end workspace capture (dossier §10.2; model: sessionRecordings).
2092
+ // One row per capture revision — a point-in-time snapshot of a session's changed
2093
+ // files, probed off the live box at turn end. The manifest (tree index + per-repo
2094
+ // status/diff + file index) and each after-image blob live in @opengeni/storage;
2095
+ // this row is the durable index the read routes serve from. `revision` is
2096
+ // monotonic per session (unique (session_id, revision)); `blob_keys` records the
2097
+ // content-addressed after-image keys this revision references so the keep-latest-10
2098
+ // GC can delete only blobs no surviving revision shares (set-difference GC without
2099
+ // a storage read). `lease_epoch` fences a write: an insert whose lease was
2100
+ // superseded writes zero rows (see insertWorkspaceCapture).
2101
+ export const workspaceCaptures = pgTable(
2102
+ "workspace_captures",
2103
+ {
2104
+ id: uuid("id").primaryKey().defaultRandom(),
2105
+ accountId: uuid("account_id")
2106
+ .notNull()
2107
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2108
+ workspaceId: uuid("workspace_id")
2109
+ .notNull()
2110
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2111
+ sessionId: uuid("session_id")
2112
+ .notNull()
2113
+ .references(() => sessions.id, { onDelete: "cascade" }),
2114
+ turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2115
+
2116
+ revision: bigint("revision", { mode: "number" }).notNull(),
2117
+ leaseEpoch: integer("lease_epoch").notNull(),
2118
+ // 'available' on a committed capture. 'failed' reserved for a future two-phase
2119
+ // write; the current synchronous capture only ever inserts 'available'.
2120
+ state: text("state", { enum: ["available", "failed"] })
2121
+ .notNull()
2122
+ .default("available"),
2123
+
2124
+ // Single JSON manifest blob (tree index + repos + file refs) — the cold-paint payload.
2125
+ manifestKey: text("manifest_key"),
2126
+ // The fs tree index blob, kept separate from the manifest so the API can inline
2127
+ // or sign it independently of the (usually small) manifest metadata.
2128
+ treeIndexKey: text("tree_index_key"),
2129
+ // Content-addressed after-image blob keys this revision references (GC input).
2130
+ blobKeys: jsonb("blob_keys").$type<string[]>().notNull().default([]),
2131
+
2132
+ sizeBytes: bigint("size_bytes", { mode: "number" }),
2133
+ stats: jsonb("stats").$type<Record<string, unknown>>().notNull().default({}),
2134
+
2135
+ capturedAt: timestamp("captured_at", { withTimezone: true }).notNull().defaultNow(),
2136
+ },
2137
+ (table) => ({
2138
+ sessionRevision: uniqueIndex("workspace_captures_session_revision_idx").on(
2139
+ table.sessionId,
2140
+ table.revision,
2141
+ ),
2142
+ latest: index("workspace_captures_latest_idx").on(
2143
+ table.workspaceId,
2144
+ table.sessionId,
2145
+ table.revision,
2146
+ ),
2147
+ }),
2148
+ );
783
2149
 
784
2150
  // Channel-A interactive PTY sessions (P4.4 / modules/08-channel-a.md §3.1). The
785
2151
  // ONLY new persistent state Channel A needs — FS/Git reads are stateless point
@@ -789,31 +2155,41 @@ export const sessionRecordings = pgTable("session_recordings", {
789
2155
  // opened on (a box re-key strands the PTY -> reaped with reason owner_gone), and
790
2156
  // a last_input_at heartbeat so the reaper can kill idle/orphaned PTYs. Mirrors
791
2157
  // the account/workspace/session FK chain of sandboxSessionEnvelopes.
792
- export const sandboxPtySessions = pgTable("sandbox_pty_sessions", {
793
- id: uuid("id").primaryKey().defaultRandom(), // == ptyId on the wire
794
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
795
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
796
- sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
797
- // The SDK numeric exec-session id used by writeStdin({ sessionId }). Null until
798
- // the open exec yields a still-running process (a fast-exiting shell has none).
799
- execSessionId: integer("exec_session_id"),
800
- leaseEpoch: integer("lease_epoch").notNull(), // fenced to the box that opened it
801
- cols: integer("cols").notNull(),
802
- rows: integer("rows").notNull(),
803
- shell: text("shell").notNull(),
804
- cwd: text("cwd").notNull(),
805
- status: text("status").notNull().default("open"), // 'open' | 'closed'
806
- // The viewer grant/subject that opened it (free-text — access subjects are not
807
- // always UUIDs, M5; so a text column, never a uuid NOT NULL).
808
- openedBy: text("opened_by").notNull(),
809
- lastInputAt: timestamp("last_input_at", { withTimezone: true }).notNull().defaultNow(),
810
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
811
- closedAt: timestamp("closed_at", { withTimezone: true }),
812
- }, (table) => ({
813
- openIdx: index("sandbox_pty_sessions_session_idx")
814
- .on(table.workspaceId, table.sessionId)
815
- .where(sql`${table.status} = 'open'`),
816
- }));
2158
+ export const sandboxPtySessions = pgTable(
2159
+ "sandbox_pty_sessions",
2160
+ {
2161
+ id: uuid("id").primaryKey().defaultRandom(), // == ptyId on the wire
2162
+ accountId: uuid("account_id")
2163
+ .notNull()
2164
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2165
+ workspaceId: uuid("workspace_id")
2166
+ .notNull()
2167
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2168
+ sessionId: uuid("session_id")
2169
+ .notNull()
2170
+ .references(() => sessions.id, { onDelete: "cascade" }),
2171
+ // The SDK numeric exec-session id used by writeStdin({ sessionId }). Null until
2172
+ // the open exec yields a still-running process (a fast-exiting shell has none).
2173
+ execSessionId: integer("exec_session_id"),
2174
+ leaseEpoch: integer("lease_epoch").notNull(), // fenced to the box that opened it
2175
+ cols: integer("cols").notNull(),
2176
+ rows: integer("rows").notNull(),
2177
+ shell: text("shell").notNull(),
2178
+ cwd: text("cwd").notNull(),
2179
+ status: text("status").notNull().default("open"), // 'open' | 'closed'
2180
+ // The viewer grant/subject that opened it (free-text — access subjects are not
2181
+ // always UUIDs, M5; so a text column, never a uuid NOT NULL).
2182
+ openedBy: text("opened_by").notNull(),
2183
+ lastInputAt: timestamp("last_input_at", { withTimezone: true }).notNull().defaultNow(),
2184
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2185
+ closedAt: timestamp("closed_at", { withTimezone: true }),
2186
+ },
2187
+ (table) => ({
2188
+ openIdx: index("sandbox_pty_sessions_session_idx")
2189
+ .on(table.workspaceId, table.sessionId)
2190
+ .where(sql`${table.status} = 'open'`),
2191
+ }),
2192
+ );
817
2193
 
818
2194
  // ============================================================================
819
2195
  // Bring-your-own-compute (M2): first-class swappable sandboxes + enrollment +
@@ -834,37 +2210,61 @@ export const sandboxKindValues = ["modal", "selfhosted"] as const;
834
2210
  // are the desktop/computer-use consent bits (default false — opt-in). status is
835
2211
  // the active|revoked lifecycle; last_seen_at the heartbeat liveness cursor the
836
2212
  // Machines dashboard renders online/reconnecting/offline from.
837
- export const enrollments = pgTable("enrollments", {
838
- id: uuid("id").primaryKey().defaultRandom(),
839
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
840
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
841
- // The agent's ed25519 public key (the machine identity).
842
- pubkey: text("pubkey").notNull(),
843
- exposure: text("exposure", { enum: enrollmentExposureValues }).notNull().default("whole-machine"),
844
- hasDisplay: boolean("has_display").notNull().default(false),
845
- // When the machine has a display it CANNOT capture (macOS Screen Recording / TCC
846
- // not granted), the agent reports has_display=false AND a human, actionable reason
847
- // here (e.g. "grant Screen Recording in System Settings"). NULL means capture is
848
- // permitted (has_display=true) or the machine is genuinely headless — the reason
849
- // distinguishes "display present but capture not granted" from plain "no display"
850
- // so the Machines dashboard / VM picker can surface the specific hint. Refreshed
851
- // from every connect Hello alongside has_display.
852
- desktopUnavailableReason: text("desktop_unavailable_reason"),
853
- allowScreenControl: boolean("allow_screen_control").notNull().default(false),
854
- status: text("status", { enum: enrollmentStatusValues }).notNull().default("active"),
855
- os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
856
- arch: text("arch").notNull().default("x86_64"),
857
- // Heartbeat liveness cursor. Null until the first connect.
858
- lastSeenAt: timestamp("last_seen_at", { withTimezone: true }),
859
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
860
- revokedAt: timestamp("revoked_at", { withTimezone: true }),
861
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
862
- }, (table) => ({
863
- // One enrollment per (workspace, pubkey): a re-enroll is an idempotent upsert.
864
- workspacePubkey: uniqueIndex("enrollments_workspace_pubkey_idx").on(table.workspaceId, table.pubkey),
865
- // List a workspace's ACTIVE machines without scanning revoked rows.
866
- workspaceStatus: index("enrollments_workspace_status_idx").on(table.workspaceId, table.status),
867
- }));
2213
+ export const enrollments = pgTable(
2214
+ "enrollments",
2215
+ {
2216
+ id: uuid("id").primaryKey().defaultRandom(),
2217
+ accountId: uuid("account_id")
2218
+ .notNull()
2219
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2220
+ workspaceId: uuid("workspace_id")
2221
+ .notNull()
2222
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2223
+ // The agent's ed25519 public key (the machine identity).
2224
+ pubkey: text("pubkey").notNull(),
2225
+ exposure: text("exposure", { enum: enrollmentExposureValues })
2226
+ .notNull()
2227
+ .default("whole-machine"),
2228
+ hasDisplay: boolean("has_display").notNull().default(false),
2229
+ // Refreshed from every connect Hello (Capabilities.op_stream); false for
2230
+ // agents predating the op-stream engine.
2231
+ opStream: boolean("op_stream").notNull().default(false),
2232
+ // When the machine has a display it CANNOT capture (macOS Screen Recording / TCC
2233
+ // not granted), the agent reports has_display=false AND a human, actionable reason
2234
+ // here (e.g. "grant Screen Recording in System Settings"). NULL means capture is
2235
+ // permitted (has_display=true) or the machine is genuinely headless — the reason
2236
+ // distinguishes "display present but capture not granted" from plain "no display"
2237
+ // so the Machines dashboard / VM picker can surface the specific hint. Refreshed
2238
+ // from every connect Hello alongside has_display.
2239
+ desktopUnavailableReason: text("desktop_unavailable_reason"),
2240
+ allowScreenControl: boolean("allow_screen_control").notNull().default(false),
2241
+ status: text("status", { enum: enrollmentStatusValues }).notNull().default("active"),
2242
+ os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
2243
+ arch: text("arch").notNull().default("x86_64"),
2244
+ // Heartbeat liveness cursor. Null until the first connect.
2245
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }),
2246
+ // Clean going-offline marker (migration 0049). Set when the machine announces a
2247
+ // typed GoingOffline (user-stop / self-update / host-shutdown); the liveness
2248
+ // derivation reads an un-cleared marker as OFFLINE immediately, regardless of a
2249
+ // still-fresh last_seen. Any newer liveness signal (a reconnect Hello or a
2250
+ // fresher heartbeat via touchEnrollmentLastSeen) clears BOTH back to NULL. NULL
2251
+ // (the default) ⇒ no goodbye pending — today's last_seen-aging behavior.
2252
+ wentOfflineAt: timestamp("went_offline_at", { withTimezone: true }),
2253
+ wentOfflineReason: text("went_offline_reason"),
2254
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2255
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
2256
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2257
+ },
2258
+ (table) => ({
2259
+ // One enrollment per (workspace, pubkey): a re-enroll is an idempotent upsert.
2260
+ workspacePubkey: uniqueIndex("enrollments_workspace_pubkey_idx").on(
2261
+ table.workspaceId,
2262
+ table.pubkey,
2263
+ ),
2264
+ // List a workspace's ACTIVE machines without scanning revoked rows.
2265
+ workspaceStatus: index("enrollments_workspace_status_idx").on(table.workspaceId, table.status),
2266
+ }),
2267
+ );
868
2268
 
869
2269
  // The OAuth 2.0 device-authorization (RFC 8628) PENDING request (M5, migration
870
2270
  // 0025 / dossier §10.2 enrollment + §18 LOUD consent). An agent's `enroll` starts
@@ -882,61 +2282,74 @@ export const enrollments = pgTable("enrollments", {
882
2282
  // `enrollments` row the approve produced.
883
2283
  export const deviceEnrollmentStatusValues = ["pending", "approved", "denied", "consumed"] as const;
884
2284
 
885
- export const deviceEnrollmentRequests = pgTable("device_enrollment_requests", {
886
- id: uuid("id").primaryKey().defaultRandom(),
887
- // The opaque code the agent polls with (unguessable, single-use). Unique.
888
- deviceCode: text("device_code").notNull(),
889
- // The short human-typed code (e.g. "WDJB-MJHT"). Unique among LIVE (pending)
890
- // rows via a partial unique index so a recycled code never collides with a
891
- // terminal row.
892
- userCode: text("user_code").notNull(),
893
- // The workspace this request was started for (resolved from the deployment-edge
894
- // request context — the agent presents the access key, the flow binds to the
895
- // single managed workspace OR a workspace hint). account_id rides along for RLS.
896
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
897
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
898
- // The agent's ed25519 public key (the machine identity the enrollment binds to).
899
- pubkey: text("pubkey").notNull(),
900
- os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
901
- arch: text("arch").notNull().default("x86_64"),
902
- machineName: text("machine_name"),
903
- // The exposure the agent REQUESTED (whole-machine in v1; loudly consented at
904
- // approve). Mirrors the enrollment column domain.
905
- requestedExposure: text("requested_exposure", { enum: enrollmentExposureValues }).notNull().default("whole-machine"),
906
- // The agent CAN offer a display (a real screen / Xvfb is available) — gates
907
- // whether screen-control consent is even meaningful. has_display on the
908
- // resulting enrollment is derived from this.
909
- canOfferDisplay: boolean("can_offer_display").notNull().default(false),
910
- // The agent REQUESTS screen control (computer-use). The user's allow_screen_control
911
- // at approve is the AUTHORITATIVE consent; this is only the agent's request.
912
- requestsScreenControl: boolean("requests_screen_control").notNull().default(false),
913
- status: text("status", { enum: deviceEnrollmentStatusValues }).notNull().default("pending"),
914
- // ── LOUD CONSENT capture (who/when/what), stamped at approve ──────────────
915
- approvedBySubjectId: text("approved_by_subject_id"),
916
- approvedBySubjectLabel: text("approved_by_subject_label"),
917
- // The user's screen-control consent decision (whole-machine is mandatory at
918
- // approve; screen-control is opt-in per this flag).
919
- allowScreenControl: boolean("allow_screen_control").notNull().default(false),
920
- approvedAt: timestamp("approved_at", { withTimezone: true }),
921
- // The enrollment + sandbox the approve produced (acceptance #2: an enrollment
922
- // row AND a sandbox row appear). Null until approved.
923
- enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
924
- sandboxId: uuid("sandbox_id").references(() => sandboxes.id, { onDelete: "set null" }),
925
- // The short-TTL expiry; a pending row past this is EXPIRED on poll.
926
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
927
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
928
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
929
- }, (table) => ({
930
- // The device_code is the agent's poll key — globally unique + indexed.
931
- deviceCode: uniqueIndex("device_enrollment_requests_device_code_idx").on(table.deviceCode),
932
- // The user_code must be unique among LIVE (pending) rows so the approve lookup
933
- // is unambiguous; a terminal row's code may be recycled.
934
- userCodePending: uniqueIndex("device_enrollment_requests_user_code_pending_idx")
935
- .on(table.userCode)
936
- .where(sql`${table.status} = 'pending'`),
937
- workspaceCreated: index("device_enrollment_requests_workspace_created_idx").on(table.workspaceId, table.createdAt),
938
- expires: index("device_enrollment_requests_expires_idx").on(table.expiresAt),
939
- }));
2285
+ export const deviceEnrollmentRequests = pgTable(
2286
+ "device_enrollment_requests",
2287
+ {
2288
+ id: uuid("id").primaryKey().defaultRandom(),
2289
+ // The opaque code the agent polls with (unguessable, single-use). Unique.
2290
+ deviceCode: text("device_code").notNull(),
2291
+ // The short human-typed code (e.g. "WDJB-MJHT"). Unique among LIVE (pending)
2292
+ // rows via a partial unique index so a recycled code never collides with a
2293
+ // terminal row.
2294
+ userCode: text("user_code").notNull(),
2295
+ // The workspace this request was started for (resolved from the deployment-edge
2296
+ // request context — the agent presents the access key, the flow binds to the
2297
+ // single managed workspace OR a workspace hint). account_id rides along for RLS.
2298
+ accountId: uuid("account_id")
2299
+ .notNull()
2300
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2301
+ workspaceId: uuid("workspace_id")
2302
+ .notNull()
2303
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2304
+ // The agent's ed25519 public key (the machine identity the enrollment binds to).
2305
+ pubkey: text("pubkey").notNull(),
2306
+ os: text("os", { enum: enrollmentOsValues }).notNull().default("linux"),
2307
+ arch: text("arch").notNull().default("x86_64"),
2308
+ machineName: text("machine_name"),
2309
+ // The exposure the agent REQUESTED (whole-machine in v1; loudly consented at
2310
+ // approve). Mirrors the enrollment column domain.
2311
+ requestedExposure: text("requested_exposure", { enum: enrollmentExposureValues })
2312
+ .notNull()
2313
+ .default("whole-machine"),
2314
+ // The agent CAN offer a display (a real screen / Xvfb is available) — gates
2315
+ // whether screen-control consent is even meaningful. has_display on the
2316
+ // resulting enrollment is derived from this.
2317
+ canOfferDisplay: boolean("can_offer_display").notNull().default(false),
2318
+ // The agent REQUESTS screen control (computer-use). The user's allow_screen_control
2319
+ // at approve is the AUTHORITATIVE consent; this is only the agent's request.
2320
+ requestsScreenControl: boolean("requests_screen_control").notNull().default(false),
2321
+ status: text("status", { enum: deviceEnrollmentStatusValues }).notNull().default("pending"),
2322
+ // ── LOUD CONSENT capture (who/when/what), stamped at approve ──────────────
2323
+ approvedBySubjectId: text("approved_by_subject_id"),
2324
+ approvedBySubjectLabel: text("approved_by_subject_label"),
2325
+ // The user's screen-control consent decision (whole-machine is mandatory at
2326
+ // approve; screen-control is opt-in per this flag).
2327
+ allowScreenControl: boolean("allow_screen_control").notNull().default(false),
2328
+ approvedAt: timestamp("approved_at", { withTimezone: true }),
2329
+ // The enrollment + sandbox the approve produced (acceptance #2: an enrollment
2330
+ // row AND a sandbox row appear). Null until approved.
2331
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
2332
+ sandboxId: uuid("sandbox_id").references(() => sandboxes.id, { onDelete: "set null" }),
2333
+ // The short-TTL expiry; a pending row past this is EXPIRED on poll.
2334
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2335
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2336
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2337
+ },
2338
+ (table) => ({
2339
+ // The device_code is the agent's poll key — globally unique + indexed.
2340
+ deviceCode: uniqueIndex("device_enrollment_requests_device_code_idx").on(table.deviceCode),
2341
+ // The user_code must be unique among LIVE (pending) rows so the approve lookup
2342
+ // is unambiguous; a terminal row's code may be recycled.
2343
+ userCodePending: uniqueIndex("device_enrollment_requests_user_code_pending_idx")
2344
+ .on(table.userCode)
2345
+ .where(sql`${table.status} = 'pending'`),
2346
+ workspaceCreated: index("device_enrollment_requests_workspace_created_idx").on(
2347
+ table.workspaceId,
2348
+ table.createdAt,
2349
+ ),
2350
+ expires: index("device_enrollment_requests_expires_idx").on(table.expiresAt),
2351
+ }),
2352
+ );
940
2353
 
941
2354
  // The first-class NAMED sandbox a session's active_sandbox_id points AT. kind
942
2355
  // discriminates the backend the routing proxy resolves to: 'modal' (cloud box,
@@ -945,179 +2358,308 @@ export const deviceEnrollmentRequests = pgTable("device_enrollment_requests", {
945
2358
  // the sandboxes_selfhosted_enrollment_chk CHECK in migration 0024. enrollment_id
946
2359
  // is ON DELETE SET NULL so deleting an enrollment never cascade-kills a sandbox a
947
2360
  // session might still point at (the routing layer surfaces agent_offline instead).
948
- export const sandboxes = pgTable("sandboxes", {
949
- id: uuid("id").primaryKey().defaultRandom(),
950
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
951
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
952
- kind: text("kind", { enum: sandboxKindValues }).notNull(),
953
- name: text("name").notNull(),
954
- enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
955
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
956
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
957
- }, (table) => ({
958
- workspaceCreated: index("sandboxes_workspace_created_idx").on(table.workspaceId, table.createdAt),
959
- enrollment: index("sandboxes_enrollment_idx").on(table.enrollmentId).where(sql`${table.enrollmentId} is not null`),
960
- }));
2361
+ export const sandboxes = pgTable(
2362
+ "sandboxes",
2363
+ {
2364
+ id: uuid("id").primaryKey().defaultRandom(),
2365
+ accountId: uuid("account_id")
2366
+ .notNull()
2367
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2368
+ workspaceId: uuid("workspace_id")
2369
+ .notNull()
2370
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2371
+ kind: text("kind", { enum: sandboxKindValues }).notNull(),
2372
+ name: text("name").notNull(),
2373
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
2374
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2375
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2376
+ },
2377
+ (table) => ({
2378
+ workspaceCreated: index("sandboxes_workspace_created_idx").on(
2379
+ table.workspaceId,
2380
+ table.createdAt,
2381
+ ),
2382
+ enrollment: index("sandboxes_enrollment_idx")
2383
+ .on(table.enrollmentId)
2384
+ .where(sql`${table.enrollmentId} is not null`),
2385
+ }),
2386
+ );
961
2387
 
962
2388
  // Last-sample upsert: ONE row per enrollment, overwritten on every sample (the
963
2389
  // PK on enrollment_id is the ON CONFLICT target). The §10.7 signals; nullable
964
2390
  // where a platform/sample may not provide it (no GPU, headless).
965
- export const machineMetricsLatest = pgTable("machine_metrics_latest", {
966
- enrollmentId: uuid("enrollment_id").primaryKey().references(() => enrollments.id, { onDelete: "cascade" }),
967
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
968
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
969
- cpuPercent: numeric("cpu_percent").$type<number>(),
970
- load1: numeric("load1").$type<number>(),
971
- load5: numeric("load5").$type<number>(),
972
- load15: numeric("load15").$type<number>(),
973
- memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
974
- memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
975
- diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
976
- diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
977
- gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
978
- gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
979
- gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
980
- contention: numeric("contention").$type<number>(),
981
- sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
982
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
983
- }, (table) => ({
984
- workspace: index("machine_metrics_latest_workspace_idx").on(table.workspaceId),
985
- }));
2391
+ export const machineMetricsLatest = pgTable(
2392
+ "machine_metrics_latest",
2393
+ {
2394
+ enrollmentId: uuid("enrollment_id")
2395
+ .primaryKey()
2396
+ .references(() => enrollments.id, { onDelete: "cascade" }),
2397
+ accountId: uuid("account_id")
2398
+ .notNull()
2399
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2400
+ workspaceId: uuid("workspace_id")
2401
+ .notNull()
2402
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2403
+ cpuPercent: numeric("cpu_percent").$type<number>(),
2404
+ load1: numeric("load1").$type<number>(),
2405
+ load5: numeric("load5").$type<number>(),
2406
+ load15: numeric("load15").$type<number>(),
2407
+ memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
2408
+ memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
2409
+ diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
2410
+ diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
2411
+ gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
2412
+ gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
2413
+ gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
2414
+ contention: numeric("contention").$type<number>(),
2415
+ sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
2416
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2417
+ },
2418
+ (table) => ({
2419
+ workspace: index("machine_metrics_latest_workspace_idx").on(table.workspaceId),
2420
+ }),
2421
+ );
986
2422
 
987
2423
  // Append-only downsampled history (~1/min per enrollment, retained N days). Same
988
2424
  // signal columns as _latest. The (enrollment_id, sampled_at) index serves the
989
2425
  // dashboard time-range read AND the (later) retention sweep.
990
- export const machineMetricsSeries = pgTable("machine_metrics_series", {
991
- id: uuid("id").primaryKey().defaultRandom(),
992
- enrollmentId: uuid("enrollment_id").notNull().references(() => enrollments.id, { onDelete: "cascade" }),
993
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
994
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
995
- cpuPercent: numeric("cpu_percent").$type<number>(),
996
- load1: numeric("load1").$type<number>(),
997
- load5: numeric("load5").$type<number>(),
998
- load15: numeric("load15").$type<number>(),
999
- memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
1000
- memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
1001
- diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
1002
- diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
1003
- gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
1004
- gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
1005
- gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
1006
- contention: numeric("contention").$type<number>(),
1007
- sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
1008
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1009
- }, (table) => ({
1010
- enrollmentSampled: index("machine_metrics_series_enrollment_sampled_idx").on(table.enrollmentId, table.sampledAt),
1011
- sampled: index("machine_metrics_series_sampled_idx").on(table.sampledAt),
1012
- }));
1013
-
1014
- export const scheduledTasks = pgTable("scheduled_tasks", {
1015
- id: uuid("id").primaryKey().defaultRandom(),
1016
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1017
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1018
- name: text("name").notNull(),
1019
- status: text("status").notNull().default("active"),
1020
- schedule: jsonb("schedule").$type<unknown>().notNull(),
1021
- temporalScheduleId: text("temporal_schedule_id").notNull(),
1022
- runMode: text("run_mode").notNull().default("new_session_per_run"),
1023
- overlapPolicy: text("overlap_policy").notNull().default("allow_concurrent"),
1024
- agentConfig: jsonb("agent_config").$type<unknown>().notNull(),
1025
- reusableSessionId: uuid("reusable_session_id").references(() => sessions.id, { onDelete: "set null" }),
1026
- environmentId: uuid("environment_id").references(() => workspaceEnvironments.id, { onDelete: "restrict" }),
1027
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1028
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1029
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1030
- }, (table) => ({
1031
- temporalScheduleId: uniqueIndex("scheduled_tasks_workspace_temporal_schedule_id_idx").on(table.workspaceId, table.temporalScheduleId),
1032
- status: index("scheduled_tasks_workspace_status_idx").on(table.workspaceId, table.status),
1033
- environment: index("scheduled_tasks_environment_idx").on(table.workspaceId, table.environmentId),
1034
- }));
1035
-
1036
- export const scheduledTaskRuns = pgTable("scheduled_task_runs", {
1037
- id: uuid("id").primaryKey().defaultRandom(),
1038
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1039
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1040
- taskId: uuid("task_id").notNull().references(() => scheduledTasks.id, { onDelete: "cascade" }),
1041
- status: text("status").notNull().default("queued"),
1042
- triggerType: text("trigger_type").notNull(),
1043
- scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
1044
- firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(),
1045
- sessionId: uuid("session_id").references(() => sessions.id, { onDelete: "set null" }),
1046
- triggerEventId: uuid("trigger_event_id"),
1047
- error: text("error"),
1048
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1049
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1050
- }, (table) => ({
1051
- taskCreated: index("scheduled_task_runs_workspace_task_created_idx").on(table.workspaceId, table.taskId, table.createdAt),
1052
- session: index("scheduled_task_runs_workspace_session_idx").on(table.workspaceId, table.sessionId),
1053
- }));
1054
-
1055
- export const githubInstallations = pgTable("github_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
- installationId: integer("installation_id").notNull(),
1060
- accountLogin: text("account_login"),
1061
- accountType: text("account_type"),
1062
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1063
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1064
- }, (table) => ({
1065
- workspaceInstallation: uniqueIndex("github_installations_workspace_installation_idx").on(table.workspaceId, table.installationId),
1066
- installation: index("github_installations_installation_idx").on(table.installationId),
1067
- workspace: index("github_installations_workspace_idx").on(table.workspaceId),
1068
- }));
1069
-
1070
- export const usageEvents = pgTable("usage_events", {
1071
- id: uuid("id").primaryKey().defaultRandom(),
1072
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1073
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1074
- subjectId: text("subject_id"),
1075
- eventType: text("event_type").notNull(),
1076
- quantity: bigint("quantity", { mode: "number" }).notNull(),
1077
- unit: text("unit").notNull(),
1078
- sourceResourceType: text("source_resource_type"),
1079
- sourceResourceId: text("source_resource_id"),
1080
- idempotencyKey: text("idempotency_key").notNull(),
1081
- occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
1082
- recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(),
1083
- exportedToBillingAt: timestamp("exported_to_billing_at", { withTimezone: true }),
1084
- billingProviderEventId: text("billing_provider_event_id"),
1085
- }, (table) => ({
1086
- idempotency: uniqueIndex("usage_events_idempotency_idx").on(table.idempotencyKey),
1087
- workspaceMetric: index("usage_events_workspace_metric_idx").on(table.workspaceId, table.eventType, table.occurredAt),
1088
- accountMetric: index("usage_events_account_metric_idx").on(table.accountId, table.eventType, table.occurredAt),
1089
- }));
1090
-
1091
- export const creditLedgerEntries = pgTable("credit_ledger_entries", {
1092
- id: uuid("id").primaryKey().defaultRandom(),
1093
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1094
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
1095
- type: text("type").notNull(),
1096
- amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
1097
- currency: text("currency").notNull().default("usd"),
1098
- sourceType: text("source_type"),
1099
- sourceId: text("source_id"),
1100
- idempotencyKey: text("idempotency_key").notNull(),
1101
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1102
- occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1103
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1104
- }, (table) => ({
1105
- idempotency: uniqueIndex("credit_ledger_entries_idempotency_idx").on(table.idempotencyKey),
1106
- accountCreated: index("credit_ledger_entries_account_created_idx").on(table.accountId, table.createdAt),
1107
- }));
1108
-
1109
- export const billingCustomers = pgTable("billing_customers", {
1110
- id: uuid("id").primaryKey().defaultRandom(),
1111
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1112
- provider: text("provider").notNull().default("stripe"),
1113
- providerCustomerId: text("provider_customer_id").notNull(),
1114
- email: text("email"),
1115
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1116
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1117
- }, (table) => ({
1118
- accountProvider: uniqueIndex("billing_customers_account_provider_idx").on(table.accountId, table.provider),
1119
- providerCustomer: uniqueIndex("billing_customers_provider_customer_idx").on(table.provider, table.providerCustomerId),
1120
- }));
2426
+ export const machineMetricsSeries = pgTable(
2427
+ "machine_metrics_series",
2428
+ {
2429
+ id: uuid("id").primaryKey().defaultRandom(),
2430
+ enrollmentId: uuid("enrollment_id")
2431
+ .notNull()
2432
+ .references(() => enrollments.id, { onDelete: "cascade" }),
2433
+ accountId: uuid("account_id")
2434
+ .notNull()
2435
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2436
+ workspaceId: uuid("workspace_id")
2437
+ .notNull()
2438
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2439
+ cpuPercent: numeric("cpu_percent").$type<number>(),
2440
+ load1: numeric("load1").$type<number>(),
2441
+ load5: numeric("load5").$type<number>(),
2442
+ load15: numeric("load15").$type<number>(),
2443
+ memUsedBytes: bigint("mem_used_bytes", { mode: "number" }),
2444
+ memTotalBytes: bigint("mem_total_bytes", { mode: "number" }),
2445
+ diskUsedBytes: bigint("disk_used_bytes", { mode: "number" }),
2446
+ diskTotalBytes: bigint("disk_total_bytes", { mode: "number" }),
2447
+ gpuUtilPercent: numeric("gpu_util_percent").$type<number>(),
2448
+ gpuMemUsedBytes: bigint("gpu_mem_used_bytes", { mode: "number" }),
2449
+ gpuMemTotalBytes: bigint("gpu_mem_total_bytes", { mode: "number" }),
2450
+ contention: numeric("contention").$type<number>(),
2451
+ sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull(),
2452
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2453
+ },
2454
+ (table) => ({
2455
+ enrollmentSampled: index("machine_metrics_series_enrollment_sampled_idx").on(
2456
+ table.enrollmentId,
2457
+ table.sampledAt,
2458
+ ),
2459
+ sampled: index("machine_metrics_series_sampled_idx").on(table.sampledAt),
2460
+ }),
2461
+ );
2462
+
2463
+ export const scheduledTasks = pgTable(
2464
+ "scheduled_tasks",
2465
+ {
2466
+ id: uuid("id").primaryKey().defaultRandom(),
2467
+ accountId: uuid("account_id")
2468
+ .notNull()
2469
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2470
+ workspaceId: uuid("workspace_id")
2471
+ .notNull()
2472
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2473
+ name: text("name").notNull(),
2474
+ status: text("status").notNull().default("active"),
2475
+ schedule: jsonb("schedule").$type<unknown>().notNull(),
2476
+ temporalScheduleId: text("temporal_schedule_id").notNull(),
2477
+ runMode: text("run_mode").notNull().default("new_session_per_run"),
2478
+ overlapPolicy: text("overlap_policy").notNull().default("allow_concurrent"),
2479
+ agentConfig: jsonb("agent_config").$type<unknown>().notNull(),
2480
+ reusableSessionId: uuid("reusable_session_id").references(() => sessions.id, {
2481
+ onDelete: "set null",
2482
+ }),
2483
+ variableSetId: uuid("variable_set_id").references(() => workspaceVariableSets.id, {
2484
+ onDelete: "restrict",
2485
+ }),
2486
+ // The rig this task's runs ride; the active version is resolved per fire
2487
+ // (migration 0047). NULL ⇒ no rig. FK (-> rigs(id) ON DELETE SET NULL) lives
2488
+ // in migration 0047 (forward-reference pattern). Consumed in M3.
2489
+ rigId: uuid("rig_id"),
2490
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2491
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2492
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2493
+ },
2494
+ (table) => ({
2495
+ temporalScheduleId: uniqueIndex("scheduled_tasks_workspace_temporal_schedule_id_idx").on(
2496
+ table.workspaceId,
2497
+ table.temporalScheduleId,
2498
+ ),
2499
+ status: index("scheduled_tasks_workspace_status_idx").on(table.workspaceId, table.status),
2500
+ variableSet: index("scheduled_tasks_variable_set_idx").on(
2501
+ table.workspaceId,
2502
+ table.variableSetId,
2503
+ ),
2504
+ }),
2505
+ );
2506
+
2507
+ export const scheduledTaskRuns = pgTable(
2508
+ "scheduled_task_runs",
2509
+ {
2510
+ id: uuid("id").primaryKey().defaultRandom(),
2511
+ accountId: uuid("account_id")
2512
+ .notNull()
2513
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2514
+ workspaceId: uuid("workspace_id")
2515
+ .notNull()
2516
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2517
+ taskId: uuid("task_id")
2518
+ .notNull()
2519
+ .references(() => scheduledTasks.id, { onDelete: "cascade" }),
2520
+ status: text("status").notNull().default("queued"),
2521
+ triggerType: text("trigger_type").notNull(),
2522
+ scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
2523
+ firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(),
2524
+ sessionId: uuid("session_id").references(() => sessions.id, { onDelete: "set null" }),
2525
+ triggerEventId: uuid("trigger_event_id"),
2526
+ // Stable Temporal producer identity. Activity replay/re-dispatch returns
2527
+ // the exact run instead of allocating a second schedule source row.
2528
+ producerKey: text("producer_key"),
2529
+ error: text("error"),
2530
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2531
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2532
+ },
2533
+ (table) => ({
2534
+ taskCreated: index("scheduled_task_runs_workspace_task_created_idx").on(
2535
+ table.workspaceId,
2536
+ table.taskId,
2537
+ table.createdAt,
2538
+ ),
2539
+ session: index("scheduled_task_runs_workspace_session_idx").on(
2540
+ table.workspaceId,
2541
+ table.sessionId,
2542
+ ),
2543
+ producer: uniqueIndex("scheduled_task_runs_producer_key_uq")
2544
+ .on(table.workspaceId, table.producerKey)
2545
+ .where(sql`${table.producerKey} is not null`),
2546
+ }),
2547
+ );
2548
+
2549
+ export const githubInstallations = pgTable(
2550
+ "github_installations",
2551
+ {
2552
+ id: uuid("id").primaryKey().defaultRandom(),
2553
+ accountId: uuid("account_id")
2554
+ .notNull()
2555
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2556
+ workspaceId: uuid("workspace_id")
2557
+ .notNull()
2558
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2559
+ installationId: integer("installation_id").notNull(),
2560
+ accountLogin: text("account_login"),
2561
+ accountType: text("account_type"),
2562
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2563
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2564
+ },
2565
+ (table) => ({
2566
+ workspaceInstallation: uniqueIndex("github_installations_workspace_installation_idx").on(
2567
+ table.workspaceId,
2568
+ table.installationId,
2569
+ ),
2570
+ installation: index("github_installations_installation_idx").on(table.installationId),
2571
+ workspace: index("github_installations_workspace_idx").on(table.workspaceId),
2572
+ }),
2573
+ );
2574
+
2575
+ export const usageEvents = pgTable(
2576
+ "usage_events",
2577
+ {
2578
+ id: uuid("id").primaryKey().defaultRandom(),
2579
+ accountId: uuid("account_id")
2580
+ .notNull()
2581
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2582
+ workspaceId: uuid("workspace_id")
2583
+ .notNull()
2584
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2585
+ subjectId: text("subject_id"),
2586
+ eventType: text("event_type").notNull(),
2587
+ quantity: bigint("quantity", { mode: "number" }).notNull(),
2588
+ unit: text("unit").notNull(),
2589
+ sourceResourceType: text("source_resource_type"),
2590
+ sourceResourceId: text("source_resource_id"),
2591
+ idempotencyKey: text("idempotency_key").notNull(),
2592
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
2593
+ recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(),
2594
+ exportedToBillingAt: timestamp("exported_to_billing_at", { withTimezone: true }),
2595
+ billingProviderEventId: text("billing_provider_event_id"),
2596
+ },
2597
+ (table) => ({
2598
+ idempotency: uniqueIndex("usage_events_idempotency_idx").on(table.idempotencyKey),
2599
+ workspaceMetric: index("usage_events_workspace_metric_idx").on(
2600
+ table.workspaceId,
2601
+ table.eventType,
2602
+ table.occurredAt,
2603
+ ),
2604
+ accountMetric: index("usage_events_account_metric_idx").on(
2605
+ table.accountId,
2606
+ table.eventType,
2607
+ table.occurredAt,
2608
+ ),
2609
+ }),
2610
+ );
2611
+
2612
+ export const creditLedgerEntries = pgTable(
2613
+ "credit_ledger_entries",
2614
+ {
2615
+ id: uuid("id").primaryKey().defaultRandom(),
2616
+ accountId: uuid("account_id")
2617
+ .notNull()
2618
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2619
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
2620
+ type: text("type").notNull(),
2621
+ amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
2622
+ currency: text("currency").notNull().default("usd"),
2623
+ sourceType: text("source_type"),
2624
+ sourceId: text("source_id"),
2625
+ idempotencyKey: text("idempotency_key").notNull(),
2626
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2627
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
2628
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2629
+ },
2630
+ (table) => ({
2631
+ idempotency: uniqueIndex("credit_ledger_entries_idempotency_idx").on(table.idempotencyKey),
2632
+ accountCreated: index("credit_ledger_entries_account_created_idx").on(
2633
+ table.accountId,
2634
+ table.createdAt,
2635
+ ),
2636
+ }),
2637
+ );
2638
+
2639
+ export const billingCustomers = pgTable(
2640
+ "billing_customers",
2641
+ {
2642
+ id: uuid("id").primaryKey().defaultRandom(),
2643
+ accountId: uuid("account_id")
2644
+ .notNull()
2645
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2646
+ provider: text("provider").notNull().default("stripe"),
2647
+ providerCustomerId: text("provider_customer_id").notNull(),
2648
+ email: text("email"),
2649
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2650
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2651
+ },
2652
+ (table) => ({
2653
+ accountProvider: uniqueIndex("billing_customers_account_provider_idx").on(
2654
+ table.accountId,
2655
+ table.provider,
2656
+ ),
2657
+ providerCustomer: uniqueIndex("billing_customers_provider_customer_idx").on(
2658
+ table.provider,
2659
+ table.providerCustomerId,
2660
+ ),
2661
+ }),
2662
+ );
1121
2663
 
1122
2664
  export const stripeWebhookEvents = pgTable("stripe_webhook_events", {
1123
2665
  id: text("id").primaryKey(),
@@ -1128,160 +2670,385 @@ export const stripeWebhookEvents = pgTable("stripe_webhook_events", {
1128
2670
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1129
2671
  });
1130
2672
 
1131
- export const auditEvents = pgTable("audit_events", {
1132
- id: uuid("id").primaryKey().defaultRandom(),
1133
- accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "set null" }),
1134
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
1135
- subjectId: text("subject_id"),
1136
- action: text("action").notNull(),
1137
- targetType: text("target_type"),
1138
- targetId: text("target_id"),
1139
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1140
- occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1141
- }, (table) => ({
1142
- accountCreated: index("audit_events_account_created_idx").on(table.accountId, table.occurredAt),
1143
- workspaceCreated: index("audit_events_workspace_created_idx").on(table.workspaceId, table.occurredAt),
1144
- }));
1145
-
1146
- export const packInstallations = pgTable("pack_installations", {
1147
- id: uuid("id").primaryKey().defaultRandom(),
1148
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1149
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1150
- packId: text("pack_id").notNull(),
1151
- status: text("status").notNull().default("active"),
1152
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1153
- enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
1154
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1155
- }, (table) => ({
1156
- workspacePack: uniqueIndex("pack_installations_workspace_pack_idx").on(table.workspaceId, table.packId),
1157
- status: index("pack_installations_workspace_status_idx").on(table.workspaceId, table.status),
1158
- }));
1159
-
1160
- export const workspacePacks = pgTable("workspace_packs", {
1161
- id: uuid("id").primaryKey().defaultRandom(),
1162
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1163
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1164
- packId: text("pack_id").notNull(),
1165
- manifest: jsonb("manifest").$type<Record<string, unknown>>().notNull(),
1166
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1167
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1168
- }, (table) => ({
1169
- workspacePack: uniqueIndex("workspace_packs_workspace_pack_idx").on(table.workspaceId, table.packId),
1170
- }));
1171
-
1172
- export const importBatches = pgTable("import_batches", {
1173
- id: uuid("id").primaryKey().defaultRandom(),
1174
- source: text("source").notNull(),
1175
- snapshotDate: timestamp("snapshot_date", { withTimezone: true }).notNull(),
1176
- snapshotRef: text("snapshot_ref"),
1177
- attributionNote: text("attribution_note").notNull(),
1178
- importedCount: integer("imported_count").notNull().default(0),
1179
- skippedCount: integer("skipped_count").notNull().default(0),
1180
- quarantinedCount: integer("quarantined_count").notNull().default(0),
1181
- logoFailureCount: integer("logo_failure_count").notNull().default(0),
1182
- staleCount: integer("stale_count").notNull().default(0),
1183
- details: jsonb("details").$type<Record<string, unknown>>().notNull().default({}),
1184
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1185
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1186
- }, (table) => ({
1187
- sourceSnapshot: index("import_batches_source_snapshot_idx").on(table.source, table.snapshotDate),
1188
- createdAt: index("import_batches_created_at_idx").on(table.createdAt),
1189
- }));
1190
-
1191
- export const capabilityCatalogItems = pgTable("capability_catalog_items", {
1192
- id: text("id").notNull(),
1193
- accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "cascade" }),
1194
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
1195
- kind: text("kind").notNull(),
1196
- source: text("source").notNull().default("manual"),
1197
- name: text("name").notNull(),
1198
- description: text("description"),
1199
- category: text("category").notNull().default("custom"),
1200
- tags: jsonb("tags").$type<string[]>().notNull().default([]),
1201
- homepageUrl: text("homepage_url"),
1202
- endpointUrl: text("endpoint_url"),
1203
- installUrl: text("install_url"),
1204
- authModel: text("auth_model"),
1205
- providerDomain: text("provider_domain"),
1206
- surfaceType: text("surface_type"),
1207
- transport: text("transport"),
1208
- mcpUrl: text("mcp_url"),
1209
- authKind: text("auth_kind"),
1210
- credentialFacts: jsonb("credential_facts").$type<Array<Record<string, unknown>>>().notNull().default([]),
1211
- tier: text("tier"),
1212
- provenance: text("provenance"),
1213
- logoAssetPath: text("logo_asset_path"),
1214
- importBatchId: uuid("import_batch_id").references(() => importBatches.id, { onDelete: "set null" }),
1215
- stale: boolean("stale").notNull().default(false),
1216
- staleAt: timestamp("stale_at", { withTimezone: true }),
1217
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1218
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1219
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1220
- }, (table) => ({
1221
- workspaceCapability: uniqueIndex("capability_catalog_items_workspace_capability_idx").on(table.workspaceId, table.id),
1222
- registrySurface: uniqueIndex("capability_catalog_items_registry_surface_idx").on(table.source, table.providerDomain, table.mcpUrl),
1223
- globalCapability: uniqueIndex("capability_catalog_items_global_capability_idx").on(table.id).where(sql`${table.workspaceId} is null`),
1224
- kind: index("capability_catalog_items_workspace_kind_idx").on(table.workspaceId, table.kind),
1225
- category: index("capability_catalog_items_workspace_category_idx").on(table.workspaceId, table.category),
1226
- source: index("capability_catalog_items_workspace_source_idx").on(table.workspaceId, table.source),
1227
- providerDomain: index("capability_catalog_items_provider_domain_idx").on(table.providerDomain),
1228
- importBatch: index("capability_catalog_items_import_batch_idx").on(table.importBatchId),
1229
- stale: index("capability_catalog_items_source_stale_idx").on(table.source, table.stale),
1230
- }));
1231
-
1232
- export const capabilityInstallations = pgTable("capability_installations", {
1233
- id: uuid("id").primaryKey().defaultRandom(),
1234
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1235
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1236
- capabilityId: text("capability_id").notNull(),
1237
- kind: text("kind").notNull(),
1238
- status: text("status").notNull().default("active"),
1239
- config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
1240
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1241
- enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
1242
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1243
- }, (table) => ({
1244
- workspaceCapability: uniqueIndex("capability_installations_workspace_capability_idx").on(table.workspaceId, table.capabilityId),
1245
- kind: index("capability_installations_workspace_kind_idx").on(table.workspaceId, table.kind),
1246
- status: index("capability_installations_workspace_status_idx").on(table.workspaceId, table.status),
1247
- }));
1248
-
1249
- export const socialConnections = pgTable("social_connections", {
1250
- id: uuid("id").primaryKey().defaultRandom(),
1251
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1252
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1253
- provider: text("provider").notNull(),
1254
- accountHandle: text("account_handle").notNull(),
1255
- accountName: text("account_name"),
1256
- externalAccountId: text("external_account_id"),
1257
- status: text("status").notNull().default("connected"),
1258
- scopes: jsonb("scopes").$type<string[]>().notNull().default([]),
1259
- credentialRef: text("credential_ref"),
1260
- tokenMetadata: jsonb("token_metadata").$type<Record<string, unknown>>().notNull().default({}),
1261
- metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
1262
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1263
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1264
- }, (table) => ({
1265
- workspaceProviderHandle: uniqueIndex("social_connections_workspace_provider_handle_idx").on(table.workspaceId, table.provider, table.accountHandle),
1266
- providerStatus: index("social_connections_workspace_provider_status_idx").on(table.workspaceId, table.provider, table.status),
1267
- }));
1268
-
1269
- export const socialPosts = pgTable("social_posts", {
1270
- id: uuid("id").primaryKey().defaultRandom(),
1271
- accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
1272
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
1273
- connectionId: uuid("connection_id").notNull().references(() => socialConnections.id, { onDelete: "cascade" }),
1274
- provider: text("provider").notNull(),
1275
- externalPostId: text("external_post_id"),
1276
- url: text("url"),
1277
- authorHandle: text("author_handle"),
1278
- text: text("text").notNull(),
1279
- publishedAt: timestamp("published_at", { withTimezone: true }).notNull(),
1280
- metrics: jsonb("metrics").$type<Record<string, number>>().notNull().default({}),
1281
- raw: jsonb("raw").$type<Record<string, unknown>>().notNull().default({}),
1282
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1283
- }, (table) => ({
1284
- connectionExternalPost: uniqueIndex("social_posts_workspace_connection_external_post_idx").on(table.workspaceId, table.connectionId, table.externalPostId),
1285
- connectionPublished: index("social_posts_workspace_connection_published_idx").on(table.workspaceId, table.connectionId, table.publishedAt),
1286
- providerPublished: index("social_posts_workspace_provider_published_idx").on(table.workspaceId, table.provider, table.publishedAt),
1287
- }));
2673
+ export const auditEvents = pgTable(
2674
+ "audit_events",
2675
+ {
2676
+ id: uuid("id").primaryKey().defaultRandom(),
2677
+ accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "set null" }),
2678
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
2679
+ subjectId: text("subject_id"),
2680
+ action: text("action").notNull(),
2681
+ targetType: text("target_type"),
2682
+ targetId: text("target_id"),
2683
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2684
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
2685
+ },
2686
+ (table) => ({
2687
+ accountCreated: index("audit_events_account_created_idx").on(table.accountId, table.occurredAt),
2688
+ workspaceCreated: index("audit_events_workspace_created_idx").on(
2689
+ table.workspaceId,
2690
+ table.occurredAt,
2691
+ ),
2692
+ }),
2693
+ );
2694
+
2695
+ export const packInstallations = pgTable(
2696
+ "pack_installations",
2697
+ {
2698
+ id: uuid("id").primaryKey().defaultRandom(),
2699
+ accountId: uuid("account_id")
2700
+ .notNull()
2701
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2702
+ workspaceId: uuid("workspace_id")
2703
+ .notNull()
2704
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2705
+ packId: text("pack_id").notNull(),
2706
+ status: text("status").notNull().default("active"),
2707
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2708
+ enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
2709
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2710
+ },
2711
+ (table) => ({
2712
+ workspacePack: uniqueIndex("pack_installations_workspace_pack_idx").on(
2713
+ table.workspaceId,
2714
+ table.packId,
2715
+ ),
2716
+ status: index("pack_installations_workspace_status_idx").on(table.workspaceId, table.status),
2717
+ }),
2718
+ );
2719
+
2720
+ export const workspacePacks = pgTable(
2721
+ "workspace_packs",
2722
+ {
2723
+ id: uuid("id").primaryKey().defaultRandom(),
2724
+ accountId: uuid("account_id")
2725
+ .notNull()
2726
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2727
+ workspaceId: uuid("workspace_id")
2728
+ .notNull()
2729
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2730
+ packId: text("pack_id").notNull(),
2731
+ manifest: jsonb("manifest").$type<Record<string, unknown>>().notNull(),
2732
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2733
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2734
+ },
2735
+ (table) => ({
2736
+ workspacePack: uniqueIndex("workspace_packs_workspace_pack_idx").on(
2737
+ table.workspaceId,
2738
+ table.packId,
2739
+ ),
2740
+ }),
2741
+ );
2742
+
2743
+ export const importBatches = pgTable(
2744
+ "import_batches",
2745
+ {
2746
+ id: uuid("id").primaryKey().defaultRandom(),
2747
+ source: text("source").notNull(),
2748
+ snapshotDate: timestamp("snapshot_date", { withTimezone: true }).notNull(),
2749
+ snapshotRef: text("snapshot_ref"),
2750
+ attributionNote: text("attribution_note").notNull(),
2751
+ importedCount: integer("imported_count").notNull().default(0),
2752
+ skippedCount: integer("skipped_count").notNull().default(0),
2753
+ quarantinedCount: integer("quarantined_count").notNull().default(0),
2754
+ logoFailureCount: integer("logo_failure_count").notNull().default(0),
2755
+ staleCount: integer("stale_count").notNull().default(0),
2756
+ details: jsonb("details").$type<Record<string, unknown>>().notNull().default({}),
2757
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2758
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2759
+ },
2760
+ (table) => ({
2761
+ sourceSnapshot: index("import_batches_source_snapshot_idx").on(
2762
+ table.source,
2763
+ table.snapshotDate,
2764
+ ),
2765
+ createdAt: index("import_batches_created_at_idx").on(table.createdAt),
2766
+ }),
2767
+ );
2768
+
2769
+ export const capabilityCatalogItems = pgTable(
2770
+ "capability_catalog_items",
2771
+ {
2772
+ id: text("id").notNull(),
2773
+ accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "cascade" }),
2774
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
2775
+ kind: text("kind").notNull(),
2776
+ source: text("source").notNull().default("manual"),
2777
+ name: text("name").notNull(),
2778
+ description: text("description"),
2779
+ category: text("category").notNull().default("custom"),
2780
+ tags: jsonb("tags").$type<string[]>().notNull().default([]),
2781
+ homepageUrl: text("homepage_url"),
2782
+ endpointUrl: text("endpoint_url"),
2783
+ installUrl: text("install_url"),
2784
+ authModel: text("auth_model"),
2785
+ providerDomain: text("provider_domain"),
2786
+ surfaceType: text("surface_type"),
2787
+ transport: text("transport"),
2788
+ mcpUrl: text("mcp_url"),
2789
+ authKind: text("auth_kind"),
2790
+ credentialFacts: jsonb("credential_facts")
2791
+ .$type<Array<Record<string, unknown>>>()
2792
+ .notNull()
2793
+ .default([]),
2794
+ tier: text("tier"),
2795
+ provenance: text("provenance"),
2796
+ logoAssetPath: text("logo_asset_path"),
2797
+ importBatchId: uuid("import_batch_id").references(() => importBatches.id, {
2798
+ onDelete: "set null",
2799
+ }),
2800
+ stale: boolean("stale").notNull().default(false),
2801
+ staleAt: timestamp("stale_at", { withTimezone: true }),
2802
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2803
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2804
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2805
+ },
2806
+ (table) => ({
2807
+ workspaceCapability: uniqueIndex("capability_catalog_items_workspace_capability_idx").on(
2808
+ table.workspaceId,
2809
+ table.id,
2810
+ ),
2811
+ registrySurface: uniqueIndex("capability_catalog_items_registry_surface_idx").on(
2812
+ table.source,
2813
+ table.providerDomain,
2814
+ table.mcpUrl,
2815
+ ),
2816
+ globalCapability: uniqueIndex("capability_catalog_items_global_capability_idx")
2817
+ .on(table.id)
2818
+ .where(sql`${table.workspaceId} is null`),
2819
+ kind: index("capability_catalog_items_workspace_kind_idx").on(table.workspaceId, table.kind),
2820
+ category: index("capability_catalog_items_workspace_category_idx").on(
2821
+ table.workspaceId,
2822
+ table.category,
2823
+ ),
2824
+ source: index("capability_catalog_items_workspace_source_idx").on(
2825
+ table.workspaceId,
2826
+ table.source,
2827
+ ),
2828
+ providerDomain: index("capability_catalog_items_provider_domain_idx").on(table.providerDomain),
2829
+ importBatch: index("capability_catalog_items_import_batch_idx").on(table.importBatchId),
2830
+ stale: index("capability_catalog_items_source_stale_idx").on(table.source, table.stale),
2831
+ }),
2832
+ );
2833
+
2834
+ export const capabilityInstallations = pgTable(
2835
+ "capability_installations",
2836
+ {
2837
+ id: uuid("id").primaryKey().defaultRandom(),
2838
+ accountId: uuid("account_id")
2839
+ .notNull()
2840
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2841
+ workspaceId: uuid("workspace_id")
2842
+ .notNull()
2843
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2844
+ capabilityId: text("capability_id").notNull(),
2845
+ kind: text("kind").notNull(),
2846
+ status: text("status").notNull().default("active"),
2847
+ config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
2848
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2849
+ enabledAt: timestamp("enabled_at", { withTimezone: true }).notNull().defaultNow(),
2850
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2851
+ },
2852
+ (table) => ({
2853
+ workspaceCapability: uniqueIndex("capability_installations_workspace_capability_idx").on(
2854
+ table.workspaceId,
2855
+ table.capabilityId,
2856
+ ),
2857
+ kind: index("capability_installations_workspace_kind_idx").on(table.workspaceId, table.kind),
2858
+ status: index("capability_installations_workspace_status_idx").on(
2859
+ table.workspaceId,
2860
+ table.status,
2861
+ ),
2862
+ }),
2863
+ );
2864
+
2865
+ export const socialConnections = pgTable(
2866
+ "social_connections",
2867
+ {
2868
+ id: uuid("id").primaryKey().defaultRandom(),
2869
+ accountId: uuid("account_id")
2870
+ .notNull()
2871
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2872
+ workspaceId: uuid("workspace_id")
2873
+ .notNull()
2874
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2875
+ provider: text("provider").notNull(),
2876
+ accountHandle: text("account_handle").notNull(),
2877
+ accountName: text("account_name"),
2878
+ externalAccountId: text("external_account_id"),
2879
+ status: text("status").notNull().default("connected"),
2880
+ scopes: jsonb("scopes").$type<string[]>().notNull().default([]),
2881
+ credentialRef: text("credential_ref"),
2882
+ tokenMetadata: jsonb("token_metadata").$type<Record<string, unknown>>().notNull().default({}),
2883
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
2884
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2885
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2886
+ },
2887
+ (table) => ({
2888
+ workspaceProviderHandle: uniqueIndex("social_connections_workspace_provider_handle_idx").on(
2889
+ table.workspaceId,
2890
+ table.provider,
2891
+ table.accountHandle,
2892
+ ),
2893
+ providerStatus: index("social_connections_workspace_provider_status_idx").on(
2894
+ table.workspaceId,
2895
+ table.provider,
2896
+ table.status,
2897
+ ),
2898
+ }),
2899
+ );
2900
+
2901
+ export const socialPosts = pgTable(
2902
+ "social_posts",
2903
+ {
2904
+ id: uuid("id").primaryKey().defaultRandom(),
2905
+ accountId: uuid("account_id")
2906
+ .notNull()
2907
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2908
+ workspaceId: uuid("workspace_id")
2909
+ .notNull()
2910
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2911
+ connectionId: uuid("connection_id")
2912
+ .notNull()
2913
+ .references(() => socialConnections.id, { onDelete: "cascade" }),
2914
+ provider: text("provider").notNull(),
2915
+ externalPostId: text("external_post_id"),
2916
+ url: text("url"),
2917
+ authorHandle: text("author_handle"),
2918
+ text: text("text").notNull(),
2919
+ publishedAt: timestamp("published_at", { withTimezone: true }).notNull(),
2920
+ metrics: jsonb("metrics").$type<Record<string, number>>().notNull().default({}),
2921
+ raw: jsonb("raw").$type<Record<string, unknown>>().notNull().default({}),
2922
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2923
+ },
2924
+ (table) => ({
2925
+ connectionExternalPost: uniqueIndex("social_posts_workspace_connection_external_post_idx").on(
2926
+ table.workspaceId,
2927
+ table.connectionId,
2928
+ table.externalPostId,
2929
+ ),
2930
+ connectionPublished: index("social_posts_workspace_connection_published_idx").on(
2931
+ table.workspaceId,
2932
+ table.connectionId,
2933
+ table.publishedAt,
2934
+ ),
2935
+ providerPublished: index("social_posts_workspace_provider_published_idx").on(
2936
+ table.workspaceId,
2937
+ table.provider,
2938
+ table.publishedAt,
2939
+ ),
2940
+ }),
2941
+ );
2942
+
2943
+ // Rigs (migration 0047): workspace-scoped, versioned sandbox machine definitions.
2944
+ // A rig is the named truth; each sandbox is a disposable fork of a rig version.
2945
+ export const rigs = pgTable(
2946
+ "rigs",
2947
+ {
2948
+ id: uuid("id").primaryKey().defaultRandom(),
2949
+ accountId: uuid("account_id")
2950
+ .notNull()
2951
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2952
+ workspaceId: uuid("workspace_id")
2953
+ .notNull()
2954
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2955
+ name: text("name").notNull(),
2956
+ description: text("description"),
2957
+ // Attribution string: 'user:<subject>' | 'session:<id>' | 'system'.
2958
+ createdBy: text("created_by"),
2959
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2960
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2961
+ },
2962
+ (table) => ({
2963
+ workspaceName: uniqueIndex("rigs_workspace_name_idx").on(table.workspaceId, table.name),
2964
+ workspaceCreated: index("rigs_workspace_created_idx").on(table.workspaceId, table.createdAt),
2965
+ }),
2966
+ );
2967
+
2968
+ // Append-only, content-immutable rig versions. Exactly one active per rig
2969
+ // (partial unique index). The domain layer never UPDATEs a content column; only
2970
+ // the `active` flag flips (activateRigVersion).
2971
+ export const rigVersions = pgTable(
2972
+ "rig_versions",
2973
+ {
2974
+ id: uuid("id").primaryKey().defaultRandom(),
2975
+ accountId: uuid("account_id")
2976
+ .notNull()
2977
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2978
+ workspaceId: uuid("workspace_id")
2979
+ .notNull()
2980
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2981
+ rigId: uuid("rig_id")
2982
+ .notNull()
2983
+ .references(() => rigs.id, { onDelete: "cascade" }),
2984
+ version: integer("version").notNull(),
2985
+ image: text("image"),
2986
+ setupScript: text("setup_script"),
2987
+ // Self-declared health checks: [{ name, command }].
2988
+ checks: jsonb("checks").$type<Array<{ name: string; command: string }>>().notNull().default([]),
2989
+ // Registered credential-hook names (resolved to hook implementations in M3).
2990
+ credentialHooks: jsonb("credential_hooks").$type<string[]>().notNull().default([]),
2991
+ // Variable-set ids layered below the session's variable set at run time (M3).
2992
+ defaultVariableSetIds: jsonb("default_variable_set_ids")
2993
+ .$type<string[]>()
2994
+ .notNull()
2995
+ .default([]),
2996
+ changelog: text("changelog"),
2997
+ createdBy: text("created_by"),
2998
+ active: boolean("active").notNull().default(false),
2999
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3000
+ },
3001
+ (table) => ({
3002
+ rigVersion: uniqueIndex("rig_versions_rig_version_idx").on(table.rigId, table.version),
3003
+ // At most one active version per rig — the single-active invariant, in the DB.
3004
+ rigActive: uniqueIndex("rig_versions_rig_active_idx")
3005
+ .on(table.rigId)
3006
+ .where(sql`${table.active}`),
3007
+ workspaceRig: index("rig_versions_workspace_rig_idx").on(
3008
+ table.workspaceId,
3009
+ table.rigId,
3010
+ table.version,
3011
+ ),
3012
+ }),
3013
+ );
3014
+
3015
+ // Proposed/verified rig changes (M4 substrate). M2 creates the table + CRUD only;
3016
+ // verification/auto-merge/promotion land in M4.
3017
+ export const rigChanges = pgTable(
3018
+ "rig_changes",
3019
+ {
3020
+ id: uuid("id").primaryKey().defaultRandom(),
3021
+ accountId: uuid("account_id")
3022
+ .notNull()
3023
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
3024
+ workspaceId: uuid("workspace_id")
3025
+ .notNull()
3026
+ .references(() => workspaces.id, { onDelete: "cascade" }),
3027
+ rigId: uuid("rig_id")
3028
+ .notNull()
3029
+ .references(() => rigs.id, { onDelete: "cascade" }),
3030
+ baseVersionId: uuid("base_version_id").references(() => rigVersions.id, {
3031
+ onDelete: "set null",
3032
+ }),
3033
+ // 'setup_append' | 'definition_edit' (CHECK in migration 0047).
3034
+ kind: text("kind").notNull(),
3035
+ payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
3036
+ // 'proposed' | 'verifying' | 'merged' | 'rejected' | 'failed' (CHECK in 0047).
3037
+ status: text("status").notNull().default("proposed"),
3038
+ proposedBy: text("proposed_by"),
3039
+ verification: jsonb("verification").$type<Record<string, unknown>>(),
3040
+ resultVersionId: uuid("result_version_id").references(() => rigVersions.id, {
3041
+ onDelete: "set null",
3042
+ }),
3043
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3044
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
3045
+ },
3046
+ (table) => ({
3047
+ workspaceRig: index("rig_changes_workspace_rig_idx").on(
3048
+ table.workspaceId,
3049
+ table.rigId,
3050
+ table.createdAt,
3051
+ ),
3052
+ workspaceStatus: index("rig_changes_workspace_status_idx").on(table.workspaceId, table.status),
3053
+ }),
3054
+ );