@opengeni/db 0.7.3 → 0.10.7

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 (70) hide show
  1. package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
  2. package/dist/chunk-BMFDXFPA.js.map +1 -0
  3. package/dist/chunk-KW526IJA.js +127 -0
  4. package/dist/chunk-KW526IJA.js.map +1 -0
  5. package/dist/{chunk-B22X3IEZ.js → chunk-P6PKXY5W.js} +604 -32
  6. package/dist/chunk-P6PKXY5W.js.map +1 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +7408 -2276
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.d.ts +29 -4
  11. package/dist/migrate.js +3 -1
  12. package/dist/provision-roles.d.ts +1122 -73
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/{schema-BN5mB9xZ.d.ts → schema-CqkzrBRS.d.ts} +6967 -4457
  15. package/dist/schema.d.ts +1 -1
  16. package/dist/schema.js +19 -1
  17. package/drizzle/0053_codex_credential_leases.sql +2 -2
  18. package/drizzle/0057_durable_queue_control.sql +1 -1
  19. package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
  20. package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
  21. package/drizzle/0063_session_control_mega_foundation.sql +1 -1
  22. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
  23. package/drizzle/0065_codex_subscription_overview.sql +168 -0
  24. package/drizzle/0065_session_attempt_quiescence.sql +26 -0
  25. package/drizzle/0065_session_tool_policy.sql +38 -0
  26. package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
  27. package/drizzle/0067_session_event_payload_bounds.sql +209 -0
  28. package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
  29. package/drizzle/0069_session_event_history_backfill.sql +32 -0
  30. package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
  31. package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
  32. package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
  33. package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
  34. package/drizzle/0074_session_activity_revisions.sql +72 -0
  35. package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
  36. package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
  37. package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
  38. package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
  39. package/drizzle/0095_github_existing_installations.sql +84 -0
  40. package/drizzle/0096_session_turn_initiators.sql +97 -0
  41. package/drizzle/0097_host_export_outbox.sql +1220 -0
  42. package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
  43. package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
  44. package/drizzle/0100_session_human_input_requests.sql +89 -0
  45. package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
  46. package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
  47. package/drizzle/0103_host_export_root_session.sql +166 -0
  48. package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
  49. package/drizzle/0105_session_turn_instructions.sql +9 -0
  50. package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
  51. package/drizzle/0107_host_export_lineage_contract.sql +381 -0
  52. package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
  53. package/package.json +5 -4
  54. package/src/codex-token-resolver.ts +175 -14
  55. package/src/connection-token-resolver.ts +428 -119
  56. package/src/event-payload-sanitizer.ts +88 -20
  57. package/src/index.ts +7208 -1273
  58. package/src/memory-domain.ts +1 -1
  59. package/src/migrate.ts +86 -23
  60. package/src/persistence-errors.ts +252 -0
  61. package/src/provision-roles.ts +42 -0
  62. package/src/schema.ts +658 -34
  63. package/src/session-control.ts +520 -38
  64. package/src/session-queue-commands.ts +385 -61
  65. package/src/session-tool-call-settlement.ts +58 -7
  66. package/src/turn-initiator.ts +155 -0
  67. package/dist/chunk-7LDU7F5P.js +0 -80
  68. package/dist/chunk-7LDU7F5P.js.map +0 -1
  69. package/dist/chunk-B22X3IEZ.js.map +0 -1
  70. package/dist/chunk-YFQ7SGE4.js.map +0 -1
package/src/schema.ts CHANGED
@@ -1,4 +1,7 @@
1
+ import type { McpServerConnectionRef, SessionMcpApprovalPolicy } from "@opengeni/contracts";
1
2
  import { sql } from "drizzle-orm";
3
+ import type { SessionToolPolicy } from "@opengeni/contracts";
4
+ import type { HumanInputQuestion, HumanInputResponse } from "@opengeni/contracts";
2
5
  import {
3
6
  bigint,
4
7
  boolean,
@@ -88,7 +91,9 @@ export const workspaceInferenceControls = pgTable(
88
91
  accountId: uuid("account_id").notNull(),
89
92
  revision: bigint("revision", { mode: "number" }).notNull().default(0),
90
93
  workspaceState: text("workspace_state").notNull().default("active"),
91
- workspacePauseRevision: bigint("workspace_pause_revision", { mode: "number" }),
94
+ workspacePauseRevision: bigint("workspace_pause_revision", {
95
+ mode: "number",
96
+ }),
92
97
  reason: text("reason"),
93
98
  changedBy: text("changed_by"),
94
99
  changedAt: timestamp("changed_at", { withTimezone: true }),
@@ -121,6 +126,30 @@ export const workspaceInferenceControls = pgTable(
121
126
  }),
122
127
  );
123
128
 
129
+ // One transactionally allocated activity clock per workspace. An updated-order
130
+ // first page takes a SHARE lock on this row after the workspace-control lock;
131
+ // semantic writers allocate after UUID-ordered session locks. Plain MVCC page
132
+ // reads never lock session rows, so that ordering cannot form a cycle.
133
+ export const workspaceSessionActivityRevisions = pgTable(
134
+ "workspace_session_activity_revisions",
135
+ {
136
+ workspaceId: uuid("workspace_id").primaryKey(),
137
+ accountId: uuid("account_id").notNull(),
138
+ revision: bigint("revision", { mode: "number" }).notNull().default(0),
139
+ },
140
+ (table) => ({
141
+ workspaceAccount: foreignKey({
142
+ name: "workspace_session_activity_revisions_workspace_account_fk",
143
+ columns: [table.workspaceId, table.accountId],
144
+ foreignColumns: [workspaces.id, workspaces.accountId],
145
+ }).onDelete("cascade"),
146
+ revisionValid: check(
147
+ "workspace_session_activity_revisions_revision_check",
148
+ sql`${table.revision} >= 0`,
149
+ ),
150
+ }),
151
+ );
152
+
124
153
  export const workspaceMemberships = pgTable(
125
154
  "workspace_memberships",
126
155
  {
@@ -155,7 +184,9 @@ export const apiKeys = pgTable(
155
184
  accountId: uuid("account_id")
156
185
  .notNull()
157
186
  .references(() => managedAccounts.id, { onDelete: "cascade" }),
158
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
187
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, {
188
+ onDelete: "cascade",
189
+ }),
159
190
  name: text("name").notNull(),
160
191
  prefix: text("prefix").notNull(),
161
192
  keyHash: text("key_hash").notNull(),
@@ -280,14 +311,29 @@ export const codexSubscriptionCredentials = pgTable(
280
311
  // excluded). The writer only ever sets a NON-empty set, so a flaky empty turn
281
312
  // can't false-drop coverage. connectorsCheckedAt is the freshness clock.
282
313
  connectorNamespaces: text("connector_namespaces").array(),
283
- connectorsCheckedAt: timestamp("connectors_checked_at", { withTimezone: true }),
314
+ connectorsCheckedAt: timestamp("connectors_checked_at", {
315
+ withTimezone: true,
316
+ }),
284
317
  // Workspace-local, server-held fairness cursor. Provider usage headers are
285
318
  // capacity hints, never the sole allocator: live lease count is ranked first
286
319
  // and this cursor deterministically breaks equal-load/equal-capacity ties.
287
320
  // This flag controls NEW automatic allocations only. Credential health,
288
321
  // refresh, encrypted material, and already-frozen/in-flight turns are
289
- // intentionally independent. OPE-24 owns toggle OCC/audit and product UI.
322
+ // intentionally independent. account eligibility policy owns toggle OCC/audit and product UI.
290
323
  allocatorEnabled: boolean("allocator_enabled").notNull().default(true),
324
+ // Independent OCC/audit sequence for the allocator toggle. Token refresh
325
+ // continues to own `version`; quota/cache writes own neither counter.
326
+ allocatorVersion: integer("allocator_version").notNull().default(1),
327
+ allocatorUpdatedBySubjectId: text("allocator_updated_by_subject_id"),
328
+ allocatorUpdatedAt: timestamp("allocator_updated_at", { withTimezone: true }),
329
+ // Authoritative count-only summary cached from /wham/usage. Detailed rows
330
+ // are never persisted as redemption authority; every first POST preflights
331
+ // the provider's fresh detail endpoint.
332
+ resetCreditAvailableCount: integer("reset_credit_available_count"),
333
+ resetCreditsCheckedAt: timestamp("reset_credits_checked_at", { withTimezone: true }),
334
+ // Set only by a direct Better Auth cookie connection/reconnection. Legacy,
335
+ // configured, delegated, API-key, and agent-created rows remain view-only.
336
+ connectedBySubjectId: text("connected_by_subject_id"),
291
337
  selectionCount: integer("selection_count").notNull().default(0),
292
338
  lastSelectedAt: timestamp("last_selected_at", { withTimezone: true }),
293
339
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -311,6 +357,82 @@ export const codexSubscriptionCredentials = pgTable(
311
357
  }),
312
358
  );
313
359
 
360
+ // One durable logical human redemption. `processing` means the fresh provider
361
+ // detail preflight is still owed; `provider_started` means the POST may have
362
+ // reached upstream and every retry must reuse upstreamIdempotencyKey without
363
+ // requiring the credit to remain visible as available.
364
+ export const codexResetRedemptionAttempts = pgTable(
365
+ "codex_reset_redemption_attempts",
366
+ {
367
+ id: uuid("id").primaryKey(),
368
+ accountId: uuid("account_id")
369
+ .notNull()
370
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
371
+ workspaceId: uuid("workspace_id")
372
+ .notNull()
373
+ .references(() => workspaces.id, { onDelete: "cascade" }),
374
+ credentialId: uuid("credential_id").notNull(),
375
+ subjectId: text("subject_id").notNull(),
376
+ browserSessionHash: text("browser_session_hash").notNull(),
377
+ creditId: text("credit_id").notNull(),
378
+ upstreamIdempotencyKey: uuid("upstream_idempotency_key").notNull().defaultRandom(),
379
+ status: text("status").notNull().default("processing"),
380
+ outcome: text("outcome"),
381
+ claimHolderId: uuid("claim_holder_id"),
382
+ claimExpiresAt: timestamp("claim_expires_at", { withTimezone: true }),
383
+ confirmationExpiresAt: timestamp("confirmation_expires_at", { withTimezone: true }).notNull(),
384
+ providerStartedAt: timestamp("provider_started_at", { withTimezone: true }),
385
+ completedAt: timestamp("completed_at", { withTimezone: true }),
386
+ lastFailureKind: text("last_failure_kind"),
387
+ retryCount: integer("retry_count").notNull().default(0),
388
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
389
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
390
+ },
391
+ (table) => ({
392
+ workspaceAccount: foreignKey({
393
+ name: "codex_reset_redemption_workspace_account_fk",
394
+ columns: [table.workspaceId, table.accountId],
395
+ foreignColumns: [workspaces.id, workspaces.accountId],
396
+ }).onDelete("cascade"),
397
+ upstreamKey: uniqueIndex("codex_reset_redemption_upstream_key_idx").on(
398
+ table.upstreamIdempotencyKey,
399
+ ),
400
+ credentialCredit: uniqueIndex("codex_reset_redemption_credential_credit_idx")
401
+ .on(table.workspaceId, table.credentialId, table.creditId)
402
+ .where(
403
+ sql`${table.status} <> 'completed' or ${table.outcome} in ('reset', 'alreadyRedeemed')`,
404
+ ),
405
+ workspaceCredential: index("codex_reset_redemption_workspace_credential_idx").on(
406
+ table.workspaceId,
407
+ table.credentialId,
408
+ table.createdAt,
409
+ ),
410
+ claimExpiry: index("codex_reset_redemption_claim_expiry_idx")
411
+ .on(table.claimExpiresAt)
412
+ .where(sql`${table.status} <> 'completed'`),
413
+ statusValid: check(
414
+ "codex_reset_redemption_status_check",
415
+ sql`${table.status} in ('processing', 'provider_started', 'completed')`,
416
+ ),
417
+ outcomeValid: check(
418
+ "codex_reset_redemption_outcome_check",
419
+ sql`${table.outcome} is null or ${table.outcome} in ('reset', 'nothingToReset', 'noCredit', 'alreadyRedeemed')`,
420
+ ),
421
+ completionConsistent: check(
422
+ "codex_reset_redemption_completed_check",
423
+ sql`(${table.status} = 'completed') = (${table.outcome} is not null and ${table.completedAt} is not null)`,
424
+ ),
425
+ retryCountValid: check(
426
+ "codex_reset_redemption_retry_count_check",
427
+ sql`${table.retryCount} >= 0`,
428
+ ),
429
+ humanSubjectValid: check(
430
+ "codex_reset_redemption_human_subject_check",
431
+ sql`${table.subjectId} like 'user:_%'`,
432
+ ),
433
+ }),
434
+ );
435
+
314
436
  // Generic external-service credential spine. credential_encrypted is the ONLY
315
437
  // secret-bearing column; normal API reads use metadata-only helpers below the DB
316
438
  // layer. Runtime token material is decrypted only by the broker accessor.
@@ -432,7 +554,7 @@ export const codexRotationSettings = pgTable(
432
554
  // code reads this bit; old binaries safely ignore it and keep the legacy
433
555
  // pin/rotation policy.
434
556
  leaseRotationEnabled: boolean("lease_rotation_enabled").notNull().default(false),
435
- rotationStrategy: text("rotation_strategy").notNull().default("most_remaining"), // P3, inert
557
+ rotationStrategy: text("rotation_strategy").notNull().default("sharded"), // sharded-rotation policy: legacy residue; behavior is always sharded
436
558
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
437
559
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
438
560
  },
@@ -527,6 +649,10 @@ export const sessions = pgTable(
527
649
  .references(() => workspaces.id, { onDelete: "cascade" }),
528
650
  status: text("status").notNull().default("queued"),
529
651
  initialMessage: text("initial_message").notNull(),
652
+ // Invisible host context frozen with the winning session create. The
653
+ // initial turn copies this value so an idempotent repair can never adopt a
654
+ // retrying caller's different instructions.
655
+ initialTurnInstructions: text("initial_turn_instructions"),
530
656
  title: text("title"),
531
657
  titleSource: text("title_source"),
532
658
  // Per-session agent persona/system instructions supplied at create (the
@@ -539,6 +665,14 @@ export const sessions = pgTable(
539
665
  resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
540
666
  tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
541
667
  metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
668
+ // Frozen creator fact. This is used for creation attribution and for the
669
+ // idempotent first-turn repair only; later turns capture their own actor.
670
+ createdByKind: text("created_by_kind").notNull().default("service"),
671
+ createdBySubjectId: text("created_by_subject_id").notNull().default("unattributed-legacy"),
672
+ createdByContext: jsonb("created_by_context")
673
+ .$type<Record<string, unknown>>()
674
+ .notNull()
675
+ .default({ backfill: true }),
542
676
  model: text("model").notNull(),
543
677
  sandboxBackend: text("sandbox_backend").notNull(),
544
678
  // The OS this session's box runs. Defaults to 'linux' (today's only OS, so
@@ -554,8 +688,8 @@ export const sessions = pgTable(
554
688
  // The app generates the uuid and uses it for both id and sandbox_group_id in
555
689
  // one insert — it cannot SQL-default to id (id is defaultRandom()).
556
690
  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
691
+ // The first-class swappable-sandbox POINTER (bring-your-own-compute M2).
692
+ // NULL == "use the session's own group sandbox" (the
559
693
  // backward-compat default — every existing/new row is a behavior-preserving
560
694
  // no-op). The routing proxy re-reads (active_sandbox_id, active_epoch) PER
561
695
  // TOOL CALL to make a Modal<->selfhosted hot-swap seamless. The FK
@@ -590,6 +724,10 @@ export const sessions = pgTable(
590
724
  // Non-default first-party MCP token permissions (manager-style sessions);
591
725
  // null means the fixed worker default set in @opengeni/runtime.
592
726
  firstPartyMcpPermissions: jsonb("first_party_mcp_permissions").$type<string[]>(),
727
+ // Durable tool-policy origin. NULL is retained for pre-migration rows;
728
+ // mapSession exposes those rows as `legacy` instead of guessing omitted vs
729
+ // explicit [].
730
+ toolPolicy: jsonb("tool_policy").$type<SessionToolPolicy>(),
593
731
  // The manager session that spawned this one via session_create. Set only
594
732
  // when the creating grant carried a worker-signed sessionId claim (a session
595
733
  // spawning a worker); null for direct API creates and scheduled-task runs.
@@ -620,11 +758,15 @@ export const sessions = pgTable(
620
758
  queueTailPosition: bigint("queue_tail_position", { mode: "number" }).notNull().default(0),
621
759
  directControlState: text("direct_control_state").notNull().default("active"),
622
760
  directPauseRevision: bigint("direct_pause_revision", { mode: "number" }),
623
- subtreeRunOverrideRevision: bigint("subtree_run_override_revision", { mode: "number" }),
761
+ subtreeRunOverrideRevision: bigint("subtree_run_override_revision", {
762
+ mode: "number",
763
+ }),
624
764
  controlVersion: bigint("control_version", { mode: "number" }).notNull().default(0),
625
765
  directControlReason: text("direct_control_reason"),
626
766
  directControlChangedBy: text("direct_control_changed_by"),
627
- directControlChangedAt: timestamp("direct_control_changed_at", { withTimezone: true }),
767
+ directControlChangedAt: timestamp("direct_control_changed_at", {
768
+ withTimezone: true,
769
+ }),
628
770
  lastSequence: integer("last_sequence").notNull().default(0),
629
771
  // The session's PINNED Codex account (manual override from the in-session
630
772
  // switcher). NULL ⇒ follow the workspace active pointer. FK declared in the
@@ -645,6 +787,10 @@ export const sessions = pgTable(
645
787
  codexPinSource: text("codex_pin_source"),
646
788
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
647
789
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
790
+ // Assigned by the database trigger whenever canonical updated_at activity
791
+ // advances. Legacy rows remain zero until touched; raw delta-only writers
792
+ // intentionally omit updated_at and therefore do not allocate revisions.
793
+ activityRevision: bigint("activity_revision", { mode: "number" }).notNull().default(0),
648
794
  },
649
795
  (table) => ({
650
796
  workspaceIdentity: uniqueIndex("sessions_workspace_id_idx").on(table.workspaceId, table.id),
@@ -652,6 +798,25 @@ export const sessions = pgTable(
652
798
  table.workspaceId,
653
799
  table.createdAt,
654
800
  ),
801
+ // Model-facing monitoring pages use exact (timestamp,id) keysets. Keep the
802
+ // older prefix index during rolling deploys; these composites serve both
803
+ // deterministic traversal and updatedAfter change scans.
804
+ workspaceCreatedId: index("sessions_workspace_created_id_idx").on(
805
+ table.workspaceId,
806
+ table.createdAt.desc(),
807
+ table.id.desc(),
808
+ ),
809
+ workspaceUpdatedId: index("sessions_workspace_updated_id_idx").on(
810
+ table.workspaceId,
811
+ table.updatedAt.desc(),
812
+ table.id.desc(),
813
+ ),
814
+ workspaceActivityRevision: index("sessions_workspace_activity_revision_idx").on(
815
+ table.workspaceId,
816
+ table.activityRevision.desc(),
817
+ table.updatedAt.desc(),
818
+ table.id.desc(),
819
+ ),
655
820
  variableSet: index("sessions_variable_set_idx").on(table.workspaceId, table.variableSetId),
656
821
  parent: index("sessions_parent_idx").on(table.workspaceId, table.parentSessionId),
657
822
  // Routing index: resolve session_id -> sandbox_group_id at every lease entry
@@ -801,6 +966,9 @@ export const sessionMcpServers = pgTable(
801
966
  // Human-approval policy: `true` = every tool requires approval, a string[] of
802
967
  // UNPREFIXED tool names = only those require it, null/absent = auto-run.
803
968
  requireApproval: jsonb("require_approval").$type<boolean | string[]>(),
969
+ // Non-secret pointer resolved at request time by the standalone broker or
970
+ // an embedding host. Unlike headersEncrypted, this is safe to project.
971
+ connectionRef: jsonb("connection_ref").$type<McpServerConnectionRef>(),
804
972
  // Map of header name -> AES-GCM ciphertext. Values are decrypted only by the
805
973
  // worker's run-preparation path and never returned by API helpers.
806
974
  headersEncrypted: jsonb("headers_encrypted")
@@ -1077,8 +1245,14 @@ export const sessionTurns = pgTable(
1077
1245
  source: text("source").notNull().default("user"),
1078
1246
  position: bigint("position", { mode: "number" }).notNull(),
1079
1247
  prompt: text("prompt").notNull(),
1248
+ // Host context for this exact turn. System-level at runtime and deliberately
1249
+ // separate from the visible prompt/event payload.
1250
+ turnInstructions: text("turn_instructions"),
1080
1251
  resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1081
1252
  tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1253
+ // false = inherit the durable session policy; true = this turn explicitly
1254
+ // replaces it with `tools` after the core subset fence.
1255
+ toolsProvided: boolean("tools_provided").notNull().default(false),
1082
1256
  model: text("model").notNull(),
1083
1257
  reasoningEffort: text("reasoning_effort").notNull(),
1084
1258
  sandboxBackend: text("sandbox_backend").notNull(),
@@ -1094,6 +1268,14 @@ export const sessionTurns = pgTable(
1094
1268
  // the SQL constraint is therefore DEFERRABLE INITIALLY DEFERRED.
1095
1269
  activeAttemptId: uuid("active_attempt_id"),
1096
1270
  lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
1271
+ // Required immutable authority captured when the turn is accepted. These
1272
+ // columns are deliberately outside mutable metadata/lineage.
1273
+ initiatorKind: text("initiator_kind").notNull().default("service"),
1274
+ initiatorSubjectId: text("initiator_subject_id").notNull().default("unattributed-legacy"),
1275
+ initiatorContext: jsonb("initiator_context")
1276
+ .$type<Record<string, unknown>>()
1277
+ .notNull()
1278
+ .default({ backfill: true }),
1097
1279
  cancelledBy: text("cancelled_by"),
1098
1280
  cancelReason: text("cancel_reason"),
1099
1281
  // Atomic per-turn toolspace call budget counter (migration 0043). Incremented
@@ -1142,10 +1324,22 @@ export const sessionTurnAttempts = pgTable(
1142
1324
  workerId: text("worker_id"),
1143
1325
  leaseId: text("lease_id"),
1144
1326
  leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
1145
- verifiedControlRevision: bigint("verified_control_revision", { mode: "number" }).notNull(),
1327
+ verifiedControlRevision: bigint("verified_control_revision", {
1328
+ mode: "number",
1329
+ }).notNull(),
1330
+ // Immutable policy snapshot captured under the session lock at claim.
1331
+ mcpApprovalPolicies: jsonb("mcp_approval_policies")
1332
+ .$type<Record<string, SessionMcpApprovalPolicy>>()
1333
+ .notNull(),
1146
1334
  startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
1147
1335
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1148
1336
  closedAt: timestamp("closed_at", { withTimezone: true }),
1337
+ // The cancelled activity writes this after losing inference, user-visible
1338
+ // output, and workspace-persistence authority. Fenced/idempotent cleanup
1339
+ // and telemetry may still finish. Temporal activity cancellation or
1340
+ // terminalization is transport state only; queue admission and clients use
1341
+ // this durable receipt as the physical-quiescence authority.
1342
+ quiescedAt: timestamp("quiesced_at", { withTimezone: true }),
1149
1343
  },
1150
1344
  (table) => ({
1151
1345
  workspaceAccount: foreignKey({
@@ -1167,12 +1361,25 @@ export const sessionTurnAttempts = pgTable(
1167
1361
  table.workspaceId,
1168
1362
  table.id,
1169
1363
  ),
1364
+ ownershipIdentity: uniqueIndex("session_turn_attempts_human_input_owner_uq").on(
1365
+ table.accountId,
1366
+ table.workspaceId,
1367
+ table.sessionId,
1368
+ table.turnId,
1369
+ table.id,
1370
+ ),
1170
1371
  liveTurn: uniqueIndex("session_turn_attempts_live_turn_uq")
1171
1372
  .on(table.workspaceId, table.turnId)
1172
1373
  .where(sql`${table.state} in ('claimed', 'running')`),
1173
1374
  liveSession: uniqueIndex("session_turn_attempts_live_session_uq")
1174
1375
  .on(table.workspaceId, table.sessionId)
1175
1376
  .where(sql`${table.state} in ('claimed', 'running')`),
1377
+ latestSessionAttempt: index("session_turn_attempts_latest_session_idx").on(
1378
+ table.workspaceId,
1379
+ table.sessionId,
1380
+ table.startedAt.desc(),
1381
+ table.id.desc(),
1382
+ ),
1176
1383
  dispatch: uniqueIndex("session_turn_attempts_dispatch_uq").on(
1177
1384
  table.workspaceId,
1178
1385
  table.temporalWorkflowRunId,
@@ -1217,7 +1424,9 @@ export const sessionCommandReceipts = pgTable(
1217
1424
  targetTurnId: uuid("target_turn_id"),
1218
1425
  operationKey: text("operation_key").notNull(),
1219
1426
  canonicalRequestHash: text("canonical_request_hash").notNull(),
1220
- appliedControlRevision: bigint("applied_control_revision", { mode: "number" }),
1427
+ appliedControlRevision: bigint("applied_control_revision", {
1428
+ mode: "number",
1429
+ }),
1221
1430
  appliedQueueVersion: integer("applied_queue_version"),
1222
1431
  appliedTurnVersion: integer("applied_turn_version"),
1223
1432
  appliedDraftRevision: bigint("applied_draft_revision", { mode: "number" }),
@@ -1262,7 +1471,7 @@ export const sessionCommandReceipts = pgTable(
1262
1471
  and ${table.actorAttemptId} is not null
1263
1472
  and ${table.actorSubjectId} is null
1264
1473
  ) or (
1265
- ${table.actorType} in ('human', 'operator')
1474
+ ${table.actorType} in ('human', 'operator', 'service')
1266
1475
  and ${table.actorSubjectId} is not null
1267
1476
  and ${table.actorAttemptId} is null
1268
1477
  )`,
@@ -1286,7 +1495,11 @@ export const workspaceControlEvents = pgTable(
1286
1495
  action: text("action").notNull(),
1287
1496
  automatic: boolean("automatic").notNull().default(false),
1288
1497
  reason: text("reason"),
1498
+ reasonOriginalBytes: integer("reason_original_bytes"),
1289
1499
  actor: text("actor").notNull(),
1500
+ // Null is the rolling-upgrade shape for untouched, already-bounded legacy
1501
+ // rows. New writes and rewritten poison rows carry exact source byte facts.
1502
+ actorOriginalBytes: integer("actor_original_bytes"),
1290
1503
  occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
1291
1504
  },
1292
1505
  (table) => ({
@@ -1390,6 +1603,7 @@ export const composerDrafts = pgTable(
1390
1603
  text: text("text").notNull().default(""),
1391
1604
  resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1392
1605
  tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1606
+ toolsProvided: boolean("tools_provided").notNull().default(false),
1393
1607
  model: text("model").notNull(),
1394
1608
  reasoningEffort: text("reasoning_effort").notNull(),
1395
1609
  sourceTurnId: uuid("source_turn_id"),
@@ -1628,7 +1842,7 @@ export const sessionGoals = pgTable(
1628
1842
  }),
1629
1843
  );
1630
1844
 
1631
- // OPE-21: one durable, coalescing capacity waiter per session. The row is both
1845
+ // credential allocator: one durable, coalescing capacity waiter per session. The row is both
1632
1846
  // the wait state and the commit->signal outbox: capacity mutations increment
1633
1847
  // wakeRevision in the SAME transaction as the mutation, while the session
1634
1848
  // workflow advances observedWakeRevision only after it has re-evaluated the
@@ -1638,7 +1852,7 @@ export const sessionGoals = pgTable(
1638
1852
  // The session/goal/turn foreign keys are declared in migration 0053 so the
1639
1853
  // table keeps the same composite workspace-integrity posture as credential
1640
1854
  // leases. Control is evaluated independently at admission and never changes a
1641
- // capacity waiter's identity. OPE-32 supplies policyHash when accepted-turn
1855
+ // capacity waiter's identity. policy filter supplies policyHash when accepted-turn
1642
1856
  // pool routing lands.
1643
1857
  export const codexCapacityWaiters = pgTable(
1644
1858
  "codex_capacity_waiters",
@@ -1744,6 +1958,41 @@ export const sessionEvents = pgTable(
1744
1958
  table.sessionId,
1745
1959
  table.createdAt,
1746
1960
  ),
1961
+ sessionTypeSequence: index("session_events_workspace_session_type_sequence_idx").on(
1962
+ table.workspaceId,
1963
+ table.sessionId,
1964
+ table.type,
1965
+ table.sequence,
1966
+ ),
1967
+ monitoringTail: index("session_events_workspace_session_monitoring_tail_idx")
1968
+ .on(table.workspaceId, table.sessionId, table.sequence)
1969
+ .where(
1970
+ sql`${table.type} not in ('agent.message.delta', 'agent.reasoning.delta', 'sandbox.command.output.delta', 'terminal.pty.output.delta')`,
1971
+ ),
1972
+ payloadBytes: check(
1973
+ "session_events_payload_bytes_check",
1974
+ sql`octet_length(${table.payload}::text) <= 65536`,
1975
+ ),
1976
+ typeBytes: check(
1977
+ "session_events_type_bytes_check",
1978
+ sql`octet_length(${table.type}) <= 256 and position(E'\n' in ${table.type}) = 0 and position(E'\r' in ${table.type}) = 0`,
1979
+ ),
1980
+ clientEventIdBytes: check(
1981
+ "session_events_client_event_id_bytes_check",
1982
+ sql`${table.clientEventId} is null or octet_length(${table.clientEventId}) <= 1024`,
1983
+ ),
1984
+ producerIdBytes: check(
1985
+ "session_events_producer_id_bytes_check",
1986
+ sql`${table.producerId} is null or octet_length(${table.producerId}) <= 1024`,
1987
+ ),
1988
+ turnAssociationBytes: check(
1989
+ "session_events_turn_association_bytes_check",
1990
+ sql`${table.turnAssociation} is null or octet_length(${table.turnAssociation}) <= 64`,
1991
+ ),
1992
+ duplicateReasonBytes: check(
1993
+ "session_events_duplicate_reason_bytes_check",
1994
+ sql`${table.duplicateReason} is null or octet_length(${table.duplicateReason}) <= 4096`,
1995
+ ),
1747
1996
  }),
1748
1997
  );
1749
1998
 
@@ -1758,7 +2007,9 @@ export const agentRunStates = pgTable("agent_run_states", {
1758
2007
  sessionId: uuid("session_id")
1759
2008
  .notNull()
1760
2009
  .references(() => sessions.id, { onDelete: "cascade" }),
1761
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2010
+ turnId: uuid("turn_id").references(() => sessionTurns.id, {
2011
+ onDelete: "set null",
2012
+ }),
1762
2013
  stateVersion: integer("state_version").notNull(),
1763
2014
  serializedRunState: text("serialized_run_state").notNull(),
1764
2015
  pendingApprovals: jsonb("pending_approvals").$type<unknown[]>().notNull().default([]),
@@ -1781,6 +2032,88 @@ export const agentRunStates = pgTable("agent_run_states", {
1781
2032
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1782
2033
  });
1783
2034
 
2035
+ // Durable structured human-input requests. The creation attempt is immutable
2036
+ // provenance; legitimate resume attempts are newer owners of the SAME logical
2037
+ // turn. Settlement therefore fences on the active session/turn plus the
2038
+ // request's turn generation, never on creationAttemptId.
2039
+ export const sessionHumanInputRequests = pgTable(
2040
+ "session_human_input_requests",
2041
+ {
2042
+ id: uuid("id").primaryKey().defaultRandom(),
2043
+ accountId: uuid("account_id").notNull(),
2044
+ workspaceId: uuid("workspace_id").notNull(),
2045
+ sessionId: uuid("session_id").notNull(),
2046
+ turnId: uuid("turn_id").notNull(),
2047
+ turnGeneration: integer("turn_generation").notNull(),
2048
+ creationAttemptId: uuid("creation_attempt_id").notNull(),
2049
+ toolCallId: text("tool_call_id").notNull(),
2050
+ status: text("status").notNull().default("pending"),
2051
+ questions: jsonb("questions").$type<HumanInputQuestion[]>().notNull(),
2052
+ allowSkip: boolean("allow_skip").notNull().default(false),
2053
+ response: jsonb("response").$type<HumanInputResponse>(),
2054
+ respondedBy: text("responded_by"),
2055
+ respondedAt: timestamp("responded_at", { withTimezone: true }),
2056
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
2057
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2058
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2059
+ },
2060
+ (table) => ({
2061
+ creationAttemptOwner: foreignKey({
2062
+ name: "session_human_input_requests_creation_attempt_fk",
2063
+ columns: [
2064
+ table.accountId,
2065
+ table.workspaceId,
2066
+ table.sessionId,
2067
+ table.turnId,
2068
+ table.creationAttemptId,
2069
+ ],
2070
+ foreignColumns: [
2071
+ sessionTurnAttempts.accountId,
2072
+ sessionTurnAttempts.workspaceId,
2073
+ sessionTurnAttempts.sessionId,
2074
+ sessionTurnAttempts.turnId,
2075
+ sessionTurnAttempts.id,
2076
+ ],
2077
+ }).onDelete("cascade"),
2078
+ toolCall: uniqueIndex("session_human_input_requests_tool_call_uq").on(
2079
+ table.workspaceId,
2080
+ table.sessionId,
2081
+ table.turnId,
2082
+ table.toolCallId,
2083
+ ),
2084
+ pendingSession: index("session_human_input_requests_pending_session_idx")
2085
+ .on(table.workspaceId, table.sessionId, table.createdAt, table.id)
2086
+ .where(sql`${table.status} = 'pending'`),
2087
+ pendingExpiry: index("session_human_input_requests_pending_expiry_idx")
2088
+ .on(table.expiresAt, table.id)
2089
+ .where(sql`${table.status} = 'pending' and ${table.expiresAt} is not null`),
2090
+ status: check(
2091
+ "session_human_input_requests_status_check",
2092
+ sql`${table.status} in ('pending','answered','skipped','expired','cancelled')`,
2093
+ ),
2094
+ generation: check(
2095
+ "session_human_input_requests_generation_check",
2096
+ sql`${table.turnGeneration} > 0`,
2097
+ ),
2098
+ toolCallBytes: check(
2099
+ "session_human_input_requests_tool_call_bytes_check",
2100
+ sql`octet_length(${table.toolCallId}) between 1 and 1024`,
2101
+ ),
2102
+ questionsBytes: check(
2103
+ "session_human_input_requests_questions_bytes_check",
2104
+ sql`octet_length(${table.questions}::text) <= 49152`,
2105
+ ),
2106
+ responseBytes: check(
2107
+ "session_human_input_requests_response_bytes_check",
2108
+ sql`${table.response} is null or octet_length(${table.response}::text) <= 49152`,
2109
+ ),
2110
+ actorBytes: check(
2111
+ "session_human_input_requests_actor_bytes_check",
2112
+ sql`${table.respondedBy} is null or octet_length(${table.respondedBy}) <= 1024`,
2113
+ ),
2114
+ }),
2115
+ );
2116
+
1784
2117
  // Conversation truth: ordered, verbatim SDK input items (issue #35). The
1785
2118
  // model-facing memory store — unredacted and replay-ready. session_events
1786
2119
  // remains the redacted human/audit timeline.
@@ -1797,7 +2130,9 @@ export const sessionHistoryItems = pgTable(
1797
2130
  sessionId: uuid("session_id")
1798
2131
  .notNull()
1799
2132
  .references(() => sessions.id, { onDelete: "cascade" }),
1800
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2133
+ turnId: uuid("turn_id").references(() => sessionTurns.id, {
2134
+ onDelete: "set null",
2135
+ }),
1801
2136
  // Numeric (not integer) so the synthetic compaction-summary row can be
1802
2137
  // inserted at a FRACTIONAL position (boundaryPosition - 0.5) that sorts ahead
1803
2138
  // of the kept tail without colliding with — and thus overwriting — the real
@@ -2061,7 +2396,9 @@ export const sessionRecordings = pgTable(
2061
2396
  sessionId: uuid("session_id")
2062
2397
  .notNull()
2063
2398
  .references(() => sessions.id, { onDelete: "cascade" }),
2064
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2399
+ turnId: uuid("turn_id").references(() => sessionTurns.id, {
2400
+ onDelete: "set null",
2401
+ }),
2065
2402
 
2066
2403
  state: text("state", { enum: sessionRecordingStateValues }).notNull(),
2067
2404
  mode: text("mode", { enum: sessionRecordingModeValues }).notNull(),
@@ -2088,7 +2425,7 @@ export const sessionRecordings = pgTable(
2088
2425
  }),
2089
2426
  );
2090
2427
 
2091
- // Workbench v2 turn-end workspace capture (dossier §10.2; model: sessionRecordings).
2428
+ // Workbench v2 turn-end workspace capture (model: sessionRecordings).
2092
2429
  // One row per capture revision — a point-in-time snapshot of a session's changed
2093
2430
  // files, probed off the live box at turn end. The manifest (tree index + per-repo
2094
2431
  // status/diff + file index) and each after-image blob live in @opengeni/storage;
@@ -2111,7 +2448,9 @@ export const workspaceCaptures = pgTable(
2111
2448
  sessionId: uuid("session_id")
2112
2449
  .notNull()
2113
2450
  .references(() => sessions.id, { onDelete: "cascade" }),
2114
- turnId: uuid("turn_id").references(() => sessionTurns.id, { onDelete: "set null" }),
2451
+ turnId: uuid("turn_id").references(() => sessionTurns.id, {
2452
+ onDelete: "set null",
2453
+ }),
2115
2454
 
2116
2455
  revision: bigint("revision", { mode: "number" }).notNull(),
2117
2456
  leaseEpoch: integer("lease_epoch").notNull(),
@@ -2147,8 +2486,8 @@ export const workspaceCaptures = pgTable(
2147
2486
  }),
2148
2487
  );
2149
2488
 
2150
- // Channel-A interactive PTY sessions (P4.4 / modules/08-channel-a.md §3.1). The
2151
- // ONLY new persistent state Channel A needs — FS/Git reads are stateless point
2489
+ // Interactive PTY sessions. This is the persistent state needed for a live
2490
+ // terminal; file and Git reads remain stateless point
2152
2491
  // queries; an interactive PTY is a live in-box process keyed by the SDK's numeric
2153
2492
  // exec-session id (writeStdin({sessionId})). We map our UUID ptyId <-> that id,
2154
2493
  // the owning workspace/session, the lease_epoch that fences it to the box it was
@@ -2193,7 +2532,7 @@ export const sandboxPtySessions = pgTable(
2193
2532
 
2194
2533
  // ============================================================================
2195
2534
  // Bring-your-own-compute (M2): first-class swappable sandboxes + enrollment +
2196
- // metrics (migration 0024 / dossier §10.3 + §10.7 + §23). The session→box
2535
+ // metrics (migration 0024). The session→box
2197
2536
  // binding becomes a per-session mutable, epoch-fenced active_sandbox_id pointer
2198
2537
  // (declared on sessions above) that the routing proxy resolves PER TOOL CALL.
2199
2538
 
@@ -2267,7 +2606,7 @@ export const enrollments = pgTable(
2267
2606
  );
2268
2607
 
2269
2608
  // The OAuth 2.0 device-authorization (RFC 8628) PENDING request (M5, migration
2270
- // 0025 / dossier §10.2 enrollment + §18 LOUD consent). An agent's `enroll` starts
2609
+ // 0025 / enrollment + §18 LOUD consent). An agent's `enroll` starts
2271
2610
  // a flow (POST /enrollments/device/start) → one short-TTL, single-use row keyed by
2272
2611
  // an opaque `device_code` (the agent polls with) + a short `user_code` (the user
2273
2612
  // types at the approve page). The user (workspace-membership / workspace:admin
@@ -2308,7 +2647,9 @@ export const deviceEnrollmentRequests = pgTable(
2308
2647
  machineName: text("machine_name"),
2309
2648
  // The exposure the agent REQUESTED (whole-machine in v1; loudly consented at
2310
2649
  // approve). Mirrors the enrollment column domain.
2311
- requestedExposure: text("requested_exposure", { enum: enrollmentExposureValues })
2650
+ requestedExposure: text("requested_exposure", {
2651
+ enum: enrollmentExposureValues,
2652
+ })
2312
2653
  .notNull()
2313
2654
  .default("whole-machine"),
2314
2655
  // The agent CAN offer a display (a real screen / Xvfb is available) — gates
@@ -2328,8 +2669,12 @@ export const deviceEnrollmentRequests = pgTable(
2328
2669
  approvedAt: timestamp("approved_at", { withTimezone: true }),
2329
2670
  // The enrollment + sandbox the approve produced (acceptance #2: an enrollment
2330
2671
  // 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" }),
2672
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, {
2673
+ onDelete: "set null",
2674
+ }),
2675
+ sandboxId: uuid("sandbox_id").references(() => sandboxes.id, {
2676
+ onDelete: "set null",
2677
+ }),
2333
2678
  // The short-TTL expiry; a pending row past this is EXPIRED on poll.
2334
2679
  expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2335
2680
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -2370,7 +2715,9 @@ export const sandboxes = pgTable(
2370
2715
  .references(() => workspaces.id, { onDelete: "cascade" }),
2371
2716
  kind: text("kind", { enum: sandboxKindValues }).notNull(),
2372
2717
  name: text("name").notNull(),
2373
- enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "set null" }),
2718
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, {
2719
+ onDelete: "set null",
2720
+ }),
2374
2721
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2375
2722
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2376
2723
  },
@@ -2521,7 +2868,9 @@ export const scheduledTaskRuns = pgTable(
2521
2868
  triggerType: text("trigger_type").notNull(),
2522
2869
  scheduledAt: timestamp("scheduled_at", { withTimezone: true }),
2523
2870
  firedAt: timestamp("fired_at", { withTimezone: true }).notNull().defaultNow(),
2524
- sessionId: uuid("session_id").references(() => sessions.id, { onDelete: "set null" }),
2871
+ sessionId: uuid("session_id").references(() => sessions.id, {
2872
+ onDelete: "set null",
2873
+ }),
2525
2874
  triggerEventId: uuid("trigger_event_id"),
2526
2875
  // Stable Temporal producer identity. Activity replay/re-dispatch returns
2527
2876
  // the exact run instead of allocating a second schedule source row.
@@ -2559,6 +2908,8 @@ export const githubInstallations = pgTable(
2559
2908
  installationId: integer("installation_id").notNull(),
2560
2909
  accountLogin: text("account_login"),
2561
2910
  accountType: text("account_type"),
2911
+ repositoryScope: text("repository_scope").notNull().default("all"),
2912
+ linkedBySubjectId: text("linked_by_subject_id"),
2562
2913
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2563
2914
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
2564
2915
  },
@@ -2569,6 +2920,47 @@ export const githubInstallations = pgTable(
2569
2920
  ),
2570
2921
  installation: index("github_installations_installation_idx").on(table.installationId),
2571
2922
  workspace: index("github_installations_workspace_idx").on(table.workspaceId),
2923
+ repositoryScopeCheck: check(
2924
+ "github_installations_repository_scope_check",
2925
+ sql`${table.repositoryScope} in ('all', 'selected')`,
2926
+ ),
2927
+ }),
2928
+ );
2929
+
2930
+ export const githubInstallationRepositories = pgTable(
2931
+ "github_installation_repositories",
2932
+ {
2933
+ id: uuid("id").primaryKey().defaultRandom(),
2934
+ accountId: uuid("account_id")
2935
+ .notNull()
2936
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
2937
+ workspaceId: uuid("workspace_id")
2938
+ .notNull()
2939
+ .references(() => workspaces.id, { onDelete: "cascade" }),
2940
+ installationId: integer("installation_id").notNull(),
2941
+ repositoryId: bigint("repository_id", { mode: "number" }).notNull(),
2942
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
2943
+ },
2944
+ (table) => ({
2945
+ installationRepository: uniqueIndex("github_install_repo_workspace_installation_repo_idx").on(
2946
+ table.workspaceId,
2947
+ table.installationId,
2948
+ table.repositoryId,
2949
+ ),
2950
+ workspaceInstallation: index("github_install_repo_workspace_installation_idx").on(
2951
+ table.workspaceId,
2952
+ table.installationId,
2953
+ ),
2954
+ workspaceAccount: foreignKey({
2955
+ columns: [table.workspaceId, table.accountId],
2956
+ foreignColumns: [workspaces.id, workspaces.accountId],
2957
+ name: "github_installation_repositories_workspace_account_fk",
2958
+ }).onDelete("cascade"),
2959
+ installationBinding: foreignKey({
2960
+ columns: [table.workspaceId, table.installationId],
2961
+ foreignColumns: [githubInstallations.workspaceId, githubInstallations.installationId],
2962
+ name: "github_installation_repositories_installation_fk",
2963
+ }).onDelete("cascade"),
2572
2964
  }),
2573
2965
  );
2574
2966
 
@@ -2588,10 +2980,26 @@ export const usageEvents = pgTable(
2588
2980
  unit: text("unit").notNull(),
2589
2981
  sourceResourceType: text("source_resource_type"),
2590
2982
  sourceResourceId: text("source_resource_id"),
2983
+ // Exact execution source for host usage export. These are deliberately
2984
+ // validated soft references rather than cascading foreign keys: usage is
2985
+ // an immutable billing/audit fact and must retain its source identity after
2986
+ // a session or turn is deleted.
2987
+ sessionId: uuid("session_id"),
2988
+ turnId: uuid("turn_id"),
2989
+ turnAttemptId: uuid("turn_attempt_id"),
2990
+ initiatorKind: text("initiator_kind"),
2991
+ initiatorSubjectId: text("initiator_subject_id"),
2992
+ initiatorContext: jsonb("initiator_context")
2993
+ .$type<Record<string, unknown>>()
2994
+ .notNull()
2995
+ .default({}),
2996
+ origin: text("origin"),
2591
2997
  idempotencyKey: text("idempotency_key").notNull(),
2592
2998
  occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
2593
2999
  recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(),
2594
- exportedToBillingAt: timestamp("exported_to_billing_at", { withTimezone: true }),
3000
+ exportedToBillingAt: timestamp("exported_to_billing_at", {
3001
+ withTimezone: true,
3002
+ }),
2595
3003
  billingProviderEventId: text("billing_provider_event_id"),
2596
3004
  },
2597
3005
  (table) => ({
@@ -2606,6 +3014,212 @@ export const usageEvents = pgTable(
2606
3014
  table.eventType,
2607
3015
  table.occurredAt,
2608
3016
  ),
3017
+ workspaceSession: index("usage_events_workspace_session_idx").on(
3018
+ table.workspaceId,
3019
+ table.sessionId,
3020
+ table.occurredAt,
3021
+ ),
3022
+ contextHierarchy: check(
3023
+ "usage_events_context_hierarchy_check",
3024
+ sql`(${table.turnId} is null or ${table.sessionId} is not null)
3025
+ and (${table.turnAttemptId} is null or ${table.turnId} is not null)`,
3026
+ ),
3027
+ initiatorConsistent: check(
3028
+ "usage_events_initiator_check",
3029
+ sql`(${table.initiatorKind} is null and ${table.initiatorSubjectId} is null)
3030
+ or (${table.initiatorKind} in ('subject', 'service')
3031
+ and ${table.initiatorSubjectId} is not null
3032
+ and octet_length(${table.initiatorSubjectId}) between 1 and 1024)`,
3033
+ ),
3034
+ attributionContextBytes: check(
3035
+ "usage_events_initiator_context_bytes_check",
3036
+ sql`octet_length(${table.initiatorContext}::text) <= 4096`,
3037
+ ),
3038
+ originValid: check(
3039
+ "usage_events_origin_check",
3040
+ sql`${table.origin} is null or ${table.origin} in (
3041
+ 'user', 'scheduled_task', 'api', 'goal', 'system', 'compaction'
3042
+ )`,
3043
+ ),
3044
+ }),
3045
+ );
3046
+
3047
+ /** Singleton, migration-installed gate. Standalone defaults keep both off. */
3048
+ export const hostExportConfig = pgTable(
3049
+ "host_export_config",
3050
+ {
3051
+ id: integer("id").primaryKey().default(1),
3052
+ sessionEventsEnabled: boolean("session_events_enabled").notNull().default(false),
3053
+ usageEventsEnabled: boolean("usage_events_enabled").notNull().default(false),
3054
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
3055
+ },
3056
+ (table) => ({
3057
+ singleton: check("host_export_config_singleton_check", sql`${table.id} = 1`),
3058
+ }),
3059
+ );
3060
+
3061
+ /**
3062
+ * Transactional delivery buffer. It intentionally has no tenant/source FKs:
3063
+ * a workspace deletion must not erase an already-committed, unacknowledged
3064
+ * host fact. Payloads are sanitized/bounded before they reach this table.
3065
+ */
3066
+ export const hostExportOutbox = pgTable(
3067
+ "host_export_outbox",
3068
+ {
3069
+ id: uuid("id").primaryKey().defaultRandom(),
3070
+ exportKind: text("export_kind").notNull(),
3071
+ exportCursor: bigint("export_cursor", { mode: "bigint" }),
3072
+ sourceId: uuid("source_id").notNull(),
3073
+ accountId: uuid("account_id").notNull(),
3074
+ workspaceId: uuid("workspace_id").notNull(),
3075
+ sessionId: uuid("session_id"),
3076
+ rootSessionId: uuid("root_session_id"),
3077
+ turnId: uuid("turn_id"),
3078
+ turnGeneration: integer("turn_generation"),
3079
+ turnAttemptId: uuid("turn_attempt_id"),
3080
+ sessionSequence: integer("session_sequence"),
3081
+ clientEventId: text("client_event_id"),
3082
+ turnAssociation: text("turn_association"),
3083
+ duplicateOfEventId: uuid("duplicate_of_event_id"),
3084
+ duplicateReason: text("duplicate_reason"),
3085
+ eventType: text("event_type").notNull(),
3086
+ idempotencyKey: text("idempotency_key").notNull(),
3087
+ initiator: jsonb("initiator").$type<Record<string, unknown> | null>(),
3088
+ initiatorContext: jsonb("initiator_context")
3089
+ .$type<Record<string, unknown>>()
3090
+ .notNull()
3091
+ .default({}),
3092
+ origin: text("origin"),
3093
+ payload: jsonb("payload").$type<unknown>().notNull(),
3094
+ envelopeBytes: integer("envelope_bytes").notNull(),
3095
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
3096
+ sourceRecordedAt: timestamp("source_recorded_at", { withTimezone: true }).notNull(),
3097
+ enqueuedAt: timestamp("enqueued_at", { withTimezone: true }).notNull(),
3098
+ },
3099
+ (table) => ({
3100
+ kindValid: check(
3101
+ "host_export_outbox_kind_check",
3102
+ sql`${table.exportKind} in ('session_event', 'usage_event')`,
3103
+ ),
3104
+ rootSessionCaptured: check(
3105
+ "host_export_outbox_root_session_check",
3106
+ sql`${table.sessionId} is null or ${table.rootSessionId} is not null`,
3107
+ ),
3108
+ sourceUnique: uniqueIndex("host_export_outbox_source_uq").on(table.exportKind, table.sourceId),
3109
+ cursorUnique: uniqueIndex("host_export_outbox_cursor_uq")
3110
+ .on(table.exportKind, table.exportCursor)
3111
+ .where(sql`${table.exportCursor} is not null`),
3112
+ unassigned: index("host_export_outbox_unassigned_idx")
3113
+ .on(table.exportKind, table.enqueuedAt, table.id)
3114
+ .where(sql`${table.exportCursor} is null`),
3115
+ unassignedSession: index("host_export_outbox_unassigned_session_idx")
3116
+ .on(table.exportKind, table.sessionId, table.sessionSequence)
3117
+ .where(sql`${table.exportCursor} is null and ${table.sessionId} is not null`),
3118
+ contextBytes: check(
3119
+ "host_export_outbox_context_bytes_check",
3120
+ sql`octet_length(${table.initiatorContext}::text) <= 4096`,
3121
+ ),
3122
+ originValid: check(
3123
+ "host_export_outbox_origin_check",
3124
+ sql`${table.origin} is null or ${table.origin} in (
3125
+ 'user', 'scheduled_task', 'api', 'goal', 'system', 'compaction'
3126
+ )`,
3127
+ ),
3128
+ }),
3129
+ );
3130
+
3131
+ export const hostExportCursorState = pgTable(
3132
+ "host_export_cursor_state",
3133
+ {
3134
+ exportKind: text("export_kind").primaryKey(),
3135
+ nextCursor: bigint("next_cursor", { mode: "bigint" }).notNull().default(1n),
3136
+ prunedThrough: bigint("pruned_through", { mode: "bigint" }).notNull().default(0n),
3137
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
3138
+ },
3139
+ (table) => ({
3140
+ kindValid: check(
3141
+ "host_export_cursor_state_kind_check",
3142
+ sql`${table.exportKind} in ('session_event', 'usage_event')`,
3143
+ ),
3144
+ cursorValid: check(
3145
+ "host_export_cursor_state_next_check",
3146
+ sql`${table.nextCursor} > 0 and ${table.prunedThrough} >= 0
3147
+ and ${table.prunedThrough} < ${table.nextCursor}`,
3148
+ ),
3149
+ }),
3150
+ );
3151
+
3152
+ /** Named at-least-once consumer checkpoint and one-batch lease. */
3153
+ export const hostExportConsumers = pgTable(
3154
+ "host_export_consumers",
3155
+ {
3156
+ id: uuid("id").primaryKey().defaultRandom(),
3157
+ consumerId: text("consumer_id").notNull(),
3158
+ exportKind: text("export_kind").notNull(),
3159
+ checkpoint: bigint("checkpoint", { mode: "bigint" }).notNull().default(0n),
3160
+ enabled: boolean("enabled").notNull().default(true),
3161
+ leaseToken: uuid("lease_token"),
3162
+ leaseHolderId: text("lease_holder_id"),
3163
+ leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
3164
+ leaseFrom: bigint("lease_from", { mode: "bigint" }),
3165
+ leaseThrough: bigint("lease_through", { mode: "bigint" }),
3166
+ consecutiveFailures: integer("consecutive_failures").notNull().default(0),
3167
+ nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
3168
+ lastError: text("last_error"),
3169
+ lastErrorAt: timestamp("last_error_at", { withTimezone: true }),
3170
+ blockedAt: timestamp("blocked_at", { withTimezone: true }),
3171
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3172
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
3173
+ },
3174
+ (table) => ({
3175
+ consumerKind: uniqueIndex("host_export_consumers_kind_id_uq").on(
3176
+ table.exportKind,
3177
+ table.consumerId,
3178
+ ),
3179
+ kindValid: check(
3180
+ "host_export_consumers_kind_check",
3181
+ sql`${table.exportKind} in ('session_event', 'usage_event')`,
3182
+ ),
3183
+ checkpointValid: check("host_export_consumers_checkpoint_check", sql`${table.checkpoint} >= 0`),
3184
+ due: index("host_export_consumers_due_idx").on(
3185
+ table.enabled,
3186
+ table.blockedAt,
3187
+ table.nextAttemptAt,
3188
+ ),
3189
+ }),
3190
+ );
3191
+
3192
+ /** Explicit poison-row disposition; transient failures never write here. */
3193
+ export const hostExportDeadLetters = pgTable(
3194
+ "host_export_dead_letters",
3195
+ {
3196
+ id: uuid("id").primaryKey().defaultRandom(),
3197
+ consumerId: text("consumer_id").notNull(),
3198
+ exportKind: text("export_kind").notNull(),
3199
+ exportCursor: bigint("export_cursor", { mode: "bigint" }).notNull(),
3200
+ sourceId: uuid("source_id").notNull(),
3201
+ reason: text("reason").notNull(),
3202
+ envelope: jsonb("envelope").$type<Record<string, unknown>>().notNull(),
3203
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3204
+ },
3205
+ (table) => ({
3206
+ consumerCursor: uniqueIndex("host_export_dead_letters_consumer_cursor_uq").on(
3207
+ table.exportKind,
3208
+ table.consumerId,
3209
+ table.exportCursor,
3210
+ ),
3211
+ kindValid: check(
3212
+ "host_export_dead_letters_kind_check",
3213
+ sql`${table.exportKind} in ('session_event', 'usage_event')`,
3214
+ ),
3215
+ reasonValid: check(
3216
+ "host_export_dead_letters_reason_check",
3217
+ sql`length(${table.reason}) between 1 and 500`,
3218
+ ),
3219
+ envelopeBytes: check(
3220
+ "host_export_dead_letters_envelope_bytes_check",
3221
+ sql`pg_column_size(${table.envelope}) <= 114688`,
3222
+ ),
2609
3223
  }),
2610
3224
  );
2611
3225
 
@@ -2616,7 +3230,9 @@ export const creditLedgerEntries = pgTable(
2616
3230
  accountId: uuid("account_id")
2617
3231
  .notNull()
2618
3232
  .references(() => managedAccounts.id, { onDelete: "cascade" }),
2619
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "set null" }),
3233
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, {
3234
+ onDelete: "set null",
3235
+ }),
2620
3236
  type: text("type").notNull(),
2621
3237
  amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
2622
3238
  currency: text("currency").notNull().default("usd"),
@@ -2674,8 +3290,12 @@ export const auditEvents = pgTable(
2674
3290
  "audit_events",
2675
3291
  {
2676
3292
  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" }),
3293
+ accountId: uuid("account_id").references(() => managedAccounts.id, {
3294
+ onDelete: "set null",
3295
+ }),
3296
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, {
3297
+ onDelete: "set null",
3298
+ }),
2679
3299
  subjectId: text("subject_id"),
2680
3300
  action: text("action").notNull(),
2681
3301
  targetType: text("target_type"),
@@ -2770,8 +3390,12 @@ export const capabilityCatalogItems = pgTable(
2770
3390
  "capability_catalog_items",
2771
3391
  {
2772
3392
  id: text("id").notNull(),
2773
- accountId: uuid("account_id").references(() => managedAccounts.id, { onDelete: "cascade" }),
2774
- workspaceId: uuid("workspace_id").references(() => workspaces.id, { onDelete: "cascade" }),
3393
+ accountId: uuid("account_id").references(() => managedAccounts.id, {
3394
+ onDelete: "cascade",
3395
+ }),
3396
+ workspaceId: uuid("workspace_id").references(() => workspaces.id, {
3397
+ onDelete: "cascade",
3398
+ }),
2775
3399
  kind: text("kind").notNull(),
2776
3400
  source: text("source").notNull().default("manual"),
2777
3401
  name: text("name").notNull(),