@opengeni/db 0.28.9 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,262 @@
1
+ -- deployment-mode: maintenance
2
+ -- Replace the hidden per-row activity-counter allocator with one explicit
3
+ -- once-per-transaction commit gate. Readers become ordinary MVCC reads; writers
4
+ -- settle every other constraint before briefly advancing the workspace clock.
5
+ -- The durable ledger remains the source of truth; no application-local cache participates.
6
+
7
+ SET lock_timeout = '5s';
8
+ SET statement_timeout = '10min';
9
+
10
+ ALTER TABLE "sessions"
11
+ ADD COLUMN "activity_revision_pending_xid" bigint;
12
+
13
+ CREATE INDEX "sessions_workspace_activity_pending_idx"
14
+ ON "sessions" ("workspace_id", "activity_revision_pending_xid", "id")
15
+ WHERE "activity_revision_pending_xid" IS NOT NULL;
16
+
17
+ CREATE OR REPLACE FUNCTION opengeni_private.ensure_workspace_session_activity_revision(
18
+ target_schema text,
19
+ target_workspace_id uuid,
20
+ target_account_id uuid
21
+ )
22
+ RETURNS void
23
+ LANGUAGE plpgsql
24
+ SECURITY DEFINER
25
+ SET search_path = pg_catalog
26
+ AS $function$
27
+ DECLARE
28
+ previous_account_id text := current_setting('opengeni.account_id', true);
29
+ previous_workspace_id text := current_setting('opengeni.workspace_id', true);
30
+ BEGIN
31
+ -- FORCE RLS applies to supported non-bypass table owners too. Establish only
32
+ -- the row's own trusted scope, then restore the caller's transaction-local
33
+ -- context so structural initialization cannot leak authority downstream.
34
+ PERFORM set_config('opengeni.account_id', target_account_id::text, true);
35
+ PERFORM set_config('opengeni.workspace_id', target_workspace_id::text, true);
36
+ EXECUTE format(
37
+ 'INSERT INTO %I.workspace_session_activity_revisions '
38
+ || '(workspace_id, account_id, revision) VALUES ($1, $2, 0) '
39
+ || 'ON CONFLICT (workspace_id) DO NOTHING',
40
+ target_schema
41
+ )
42
+ USING target_workspace_id, target_account_id;
43
+ PERFORM set_config('opengeni.account_id', coalesce(previous_account_id, ''), true);
44
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace_id, ''), true);
45
+ EXCEPTION WHEN OTHERS THEN
46
+ PERFORM set_config('opengeni.account_id', coalesce(previous_account_id, ''), true);
47
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace_id, ''), true);
48
+ RAISE;
49
+ END;
50
+ $function$;
51
+
52
+ REVOKE ALL ON FUNCTION opengeni_private.ensure_workspace_session_activity_revision(
53
+ text, uuid, uuid
54
+ ) FROM PUBLIC;
55
+
56
+ CREATE OR REPLACE FUNCTION opengeni_private.initialize_workspace_session_activity_revision()
57
+ RETURNS trigger
58
+ LANGUAGE plpgsql
59
+ SECURITY DEFINER
60
+ SET search_path = pg_catalog
61
+ AS $function$
62
+ BEGIN
63
+ PERFORM opengeni_private.ensure_workspace_session_activity_revision(
64
+ TG_TABLE_SCHEMA,
65
+ NEW.id,
66
+ NEW.account_id
67
+ );
68
+ RETURN NEW;
69
+ END;
70
+ $function$;
71
+
72
+ REVOKE ALL ON FUNCTION opengeni_private.initialize_workspace_session_activity_revision()
73
+ FROM PUBLIC;
74
+
75
+ CREATE TRIGGER workspaces_initialize_session_activity_revision
76
+ AFTER INSERT ON "workspaces"
77
+ FOR EACH ROW
78
+ EXECUTE FUNCTION opengeni_private.initialize_workspace_session_activity_revision();
79
+
80
+ -- Install the structural initializer before taking the existing-workspace
81
+ -- snapshot. CREATE TRIGGER fences concurrent workspace writers; the subsequent
82
+ -- conflict-safe backfill covers every row that committed before the trigger,
83
+ -- while later rows are covered by the trigger itself. The same private helper
84
+ -- keeps this path valid when the schema owner is subject to FORCE RLS.
85
+ SELECT opengeni_private.ensure_workspace_session_activity_revision(
86
+ current_schema(),
87
+ "id",
88
+ "account_id"
89
+ )
90
+ FROM "workspaces";
91
+
92
+ -- Counter lifecycle is now structural: the workspace trigger creates rows and
93
+ -- the foreign-key cascade removes them. The application may only observe and
94
+ -- advance an existing counter at the explicit commit gate.
95
+ DO $grants$
96
+ DECLARE target_schema text := current_schema();
97
+ BEGIN
98
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
99
+ EXECUTE format(
100
+ 'REVOKE INSERT, DELETE ON TABLE %I.workspace_session_activity_revisions FROM opengeni_app',
101
+ target_schema
102
+ );
103
+ END IF;
104
+ END $grants$;
105
+
106
+ -- UPDATE remains necessary for the runtime finalizer, but the table itself
107
+ -- rejects every other write shape. This catches a forgotten wrapper even when
108
+ -- application code reaches for the counter directly instead of a session row.
109
+ CREATE OR REPLACE FUNCTION opengeni_private.guard_workspace_session_activity_revision()
110
+ RETURNS trigger
111
+ LANGUAGE plpgsql
112
+ SET search_path = pg_catalog
113
+ AS $function$
114
+ DECLARE
115
+ gate_state text := nullif(current_setting('opengeni.session_activity_gate_state', true), '');
116
+ gate_workspace_id uuid := nullif(
117
+ current_setting('opengeni.session_activity_gate_workspace_id', true),
118
+ ''
119
+ )::uuid;
120
+ BEGIN
121
+ IF gate_state <> 'finalizing'
122
+ OR gate_workspace_id IS DISTINCT FROM OLD.workspace_id
123
+ OR NEW.workspace_id IS DISTINCT FROM OLD.workspace_id
124
+ OR NEW.account_id IS DISTINCT FROM OLD.account_id
125
+ OR NEW.revision IS DISTINCT FROM OLD.revision + 1
126
+ THEN
127
+ RAISE EXCEPTION USING
128
+ ERRCODE = '55000',
129
+ MESSAGE = 'Workspace session activity revision requires the finalizing commit gate';
130
+ END IF;
131
+ RETURN NEW;
132
+ END;
133
+ $function$;
134
+
135
+ REVOKE ALL ON FUNCTION opengeni_private.guard_workspace_session_activity_revision()
136
+ FROM PUBLIC;
137
+
138
+ CREATE TRIGGER workspace_session_activity_revision_commit_gate
139
+ BEFORE UPDATE ON "workspace_session_activity_revisions"
140
+ FOR EACH ROW
141
+ EXECUTE FUNCTION opengeni_private.guard_workspace_session_activity_revision();
142
+
143
+ DROP TRIGGER sessions_assign_activity_revision ON "sessions";
144
+ DROP FUNCTION opengeni_private.assign_session_activity_revision();
145
+
146
+ CREATE OR REPLACE FUNCTION opengeni_private.mark_session_activity_pending()
147
+ RETURNS trigger
148
+ LANGUAGE plpgsql
149
+ SET search_path = pg_catalog
150
+ AS $function$
151
+ DECLARE
152
+ gate_state text := nullif(current_setting('opengeni.session_activity_gate_state', true), '');
153
+ gate_workspace_id uuid := nullif(
154
+ current_setting('opengeni.session_activity_gate_workspace_id', true),
155
+ ''
156
+ )::uuid;
157
+ current_xid bigint := pg_current_xact_id()::text::bigint;
158
+ BEGIN
159
+ IF gate_state = 'finalizing' THEN
160
+ IF TG_OP <> 'UPDATE'
161
+ OR NEW.workspace_id IS DISTINCT FROM OLD.workspace_id
162
+ OR NEW.updated_at IS DISTINCT FROM OLD.updated_at
163
+ OR OLD.activity_revision_pending_xid IS DISTINCT FROM current_xid
164
+ OR NEW.activity_revision_pending_xid IS NOT NULL
165
+ OR NEW.activity_revision <= OLD.activity_revision
166
+ THEN
167
+ RAISE EXCEPTION USING
168
+ ERRCODE = '55000',
169
+ MESSAGE = 'Invalid session activity finalization';
170
+ END IF;
171
+ RETURN NEW;
172
+ END IF;
173
+
174
+ IF gate_state <> 'open' OR gate_workspace_id IS DISTINCT FROM NEW.workspace_id THEN
175
+ RAISE EXCEPTION USING
176
+ ERRCODE = '55000',
177
+ MESSAGE = 'Session activity write requires an open workspace commit gate';
178
+ END IF;
179
+
180
+ IF TG_OP = 'INSERT' THEN
181
+ IF NEW.activity_revision <> 0 OR NEW.activity_revision_pending_xid IS NOT NULL THEN
182
+ RAISE EXCEPTION USING
183
+ ERRCODE = '55000',
184
+ MESSAGE = 'Session activity revision is managed by the commit gate';
185
+ END IF;
186
+ ELSIF NEW.activity_revision IS DISTINCT FROM OLD.activity_revision
187
+ OR NEW.activity_revision_pending_xid IS DISTINCT FROM OLD.activity_revision_pending_xid
188
+ THEN
189
+ RAISE EXCEPTION USING
190
+ ERRCODE = '55000',
191
+ MESSAGE = 'Session activity revision is managed by the commit gate';
192
+ END IF;
193
+
194
+ NEW.activity_revision_pending_xid := current_xid;
195
+ RETURN NEW;
196
+ END;
197
+ $function$;
198
+
199
+ REVOKE ALL ON FUNCTION opengeni_private.mark_session_activity_pending()
200
+ FROM PUBLIC;
201
+
202
+ CREATE TRIGGER sessions_mark_activity_pending
203
+ BEFORE INSERT OR UPDATE OF "updated_at", "activity_revision", "activity_revision_pending_xid"
204
+ ON "sessions"
205
+ FOR EACH ROW
206
+ EXECUTE FUNCTION opengeni_private.mark_session_activity_pending();
207
+
208
+ -- A transaction that opens the gate manually but omits the finalizer still
209
+ -- cannot commit. The application gate switches to `preparing` before forcing
210
+ -- all deferred checks, then clears every pending marker in its final write.
211
+ CREATE OR REPLACE FUNCTION opengeni_private.reject_unfinalized_session_activity()
212
+ RETURNS trigger
213
+ LANGUAGE plpgsql
214
+ SET search_path = pg_catalog
215
+ AS $function$
216
+ DECLARE
217
+ gate_state text := nullif(current_setting('opengeni.session_activity_gate_state', true), '');
218
+ still_pending_xid bigint;
219
+ BEGIN
220
+ IF gate_state = 'preparing' THEN
221
+ RETURN NULL;
222
+ END IF;
223
+ EXECUTE format(
224
+ 'SELECT activity_revision_pending_xid FROM %I.sessions WHERE id = $1',
225
+ TG_TABLE_SCHEMA
226
+ )
227
+ INTO still_pending_xid
228
+ USING NEW.id;
229
+ IF still_pending_xid IS NOT NULL THEN
230
+ RAISE EXCEPTION USING
231
+ ERRCODE = '55000',
232
+ MESSAGE = 'Session activity transaction reached commit without finalization';
233
+ END IF;
234
+ RETURN NULL;
235
+ END;
236
+ $function$;
237
+
238
+ REVOKE ALL ON FUNCTION opengeni_private.reject_unfinalized_session_activity()
239
+ FROM PUBLIC;
240
+
241
+ CREATE CONSTRAINT TRIGGER sessions_activity_insert_commit_guard
242
+ AFTER INSERT ON "sessions"
243
+ DEFERRABLE INITIALLY DEFERRED
244
+ FOR EACH ROW
245
+ WHEN (NEW.activity_revision_pending_xid IS NOT NULL)
246
+ EXECUTE FUNCTION opengeni_private.reject_unfinalized_session_activity();
247
+
248
+ -- The clearing write must queue a second check as well. The application first
249
+ -- flushes all existing deferred constraints, re-defers these two guards, stamps
250
+ -- every pending row, and then forces both guards against the final row state.
251
+ CREATE CONSTRAINT TRIGGER sessions_activity_update_commit_guard
252
+ AFTER UPDATE ON "sessions"
253
+ DEFERRABLE INITIALLY DEFERRED
254
+ FOR EACH ROW
255
+ WHEN (
256
+ NEW.activity_revision_pending_xid IS NOT NULL
257
+ OR OLD.activity_revision_pending_xid IS NOT NULL
258
+ )
259
+ EXECUTE FUNCTION opengeni_private.reject_unfinalized_session_activity();
260
+
261
+ RESET statement_timeout;
262
+ RESET lock_timeout;
@@ -0,0 +1,385 @@
1
+ -- deployment-mode: maintenance
2
+ -- Separate a remote browser's logical placement from the sandbox that hosts
3
+ -- its sole browserd mutation controller. Existing managed sessions preserve
4
+ -- their current home placement; remote rows are backfilled from their immutable
5
+ -- creation association.
6
+ SET LOCAL lock_timeout = '5s';
7
+ SET LOCAL statement_timeout = '30s';
8
+
9
+ ALTER TABLE "browser_sessions"
10
+ ADD COLUMN "controller_host_sandbox_group_id" uuid;
11
+
12
+ UPDATE "browser_sessions"
13
+ SET "controller_host_sandbox_group_id" = "sandbox_group_id"
14
+ WHERE "placement_kind" = 'sandbox_group';
15
+
16
+ UPDATE "browser_sessions" browser
17
+ SET "controller_host_sandbox_group_id" = source."sandbox_group_id"
18
+ FROM "browser_session_associations" association
19
+ JOIN "sessions" source
20
+ ON source."workspace_id" = association."workspace_id"
21
+ AND source."id" = association."session_id"
22
+ WHERE browser."workspace_id" = association."workspace_id"
23
+ AND browser."id" = association."browser_session_id"
24
+ AND browser."placement_kind" = 'external_provider'
25
+ AND association."relationship" = 'created';
26
+
27
+ ALTER TABLE "browser_sessions"
28
+ ADD CONSTRAINT "browser_sessions_controller_host_check" CHECK (
29
+ ("placement_kind" = 'sandbox_group'
30
+ AND "controller_host_sandbox_group_id" = "sandbox_group_id")
31
+ OR
32
+ ("placement_kind" = 'external_provider'
33
+ AND "controller_host_sandbox_group_id" IS NOT NULL)
34
+ OR
35
+ ("placement_kind" IN ('connected_machine', 'attached_device')
36
+ AND "controller_host_sandbox_group_id" IS NULL)
37
+ );
38
+
39
+ CREATE INDEX "browser_sessions_controller_host_sandbox_group_idx"
40
+ ON "browser_sessions"
41
+ ("workspace_id", "controller_host_sandbox_group_id", "lifecycle");
42
+
43
+ -- Reinstall the interaction-aware lease reaper against controller locality,
44
+ -- not logical browser locality. This keeps a remote browser alive exactly while
45
+ -- its home browserd lease and holder remain authoritative.
46
+ DO $interaction_aware_reaper$
47
+ DECLARE data_schema text := current_schema();
48
+ BEGIN
49
+ EXECUTE format($create$
50
+ CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
51
+ p_viewer_holder_ttl_ms bigint,
52
+ p_turn_holder_ttl_ms bigint,
53
+ p_interaction_holder_ttl_ms bigint,
54
+ p_idle_grace_ms bigint
55
+ )
56
+ RETURNS TABLE (
57
+ workspace_id uuid,
58
+ sandbox_group_id uuid,
59
+ instance_id text,
60
+ lease_epoch integer
61
+ )
62
+ LANGUAGE plpgsql
63
+ SECURITY DEFINER
64
+ SET search_path = pg_catalog
65
+ AS $function$
66
+ DECLARE
67
+ locked_ids uuid[];
68
+ stale_interaction_ids uuid[];
69
+ changed_interaction_workspaces uuid[];
70
+ BEGIN
71
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
72
+
73
+ SELECT coalesce(pg_catalog.array_agg(candidate.id), ARRAY[]::uuid[])
74
+ INTO locked_ids
75
+ FROM (
76
+ SELECT lease.id
77
+ FROM %1$I.sandbox_leases lease
78
+ WHERE lease.liveness <> 'cold'
79
+ OR EXISTS (
80
+ SELECT 1
81
+ FROM %1$I.sandbox_lease_holders holder
82
+ WHERE holder.lease_id = lease.id
83
+ )
84
+ ORDER BY lease.updated_at, lease.id
85
+ LIMIT 500
86
+ FOR UPDATE OF lease SKIP LOCKED
87
+ ) candidate;
88
+
89
+ IF pg_catalog.cardinality(locked_ids) = 0 THEN
90
+ RETURN;
91
+ END IF;
92
+
93
+ DELETE FROM %1$I.sandbox_lease_holders holder
94
+ USING (
95
+ SELECT stale.id
96
+ FROM %1$I.sandbox_lease_holders stale
97
+ WHERE stale.lease_id = ANY(locked_ids)
98
+ AND stale.kind IN ('viewer', 'direct')
99
+ AND stale.last_heartbeat_at
100
+ < pg_catalog.now()
101
+ - pg_catalog.make_interval(secs => p_viewer_holder_ttl_ms / 1000.0)
102
+ FOR UPDATE OF stale SKIP LOCKED
103
+ ) victim
104
+ WHERE holder.id = victim.id;
105
+
106
+ IF p_turn_holder_ttl_ms > 0 THEN
107
+ DELETE FROM %1$I.sandbox_lease_holders holder
108
+ USING (
109
+ SELECT stale.id
110
+ FROM %1$I.sandbox_lease_holders stale
111
+ WHERE stale.lease_id = ANY(locked_ids)
112
+ AND stale.kind = 'turn'
113
+ AND (
114
+ stale.last_heartbeat_at
115
+ < pg_catalog.now()
116
+ - pg_catalog.make_interval(secs => p_turn_holder_ttl_ms / 1000.0)
117
+ OR (
118
+ stale.holder_id ~* '^turn-attempt:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
119
+ AND NOT EXISTS (
120
+ SELECT 1
121
+ FROM %1$I.session_turn_attempts attempt
122
+ JOIN %1$I.session_turns turn
123
+ ON turn.account_id = attempt.account_id
124
+ AND turn.workspace_id = attempt.workspace_id
125
+ AND turn.session_id = attempt.session_id
126
+ AND turn.id = attempt.turn_id
127
+ AND turn.execution_generation = attempt.execution_generation
128
+ AND turn.active_attempt_id = attempt.id
129
+ WHERE stale.holder_id = ('turn-attempt:' || attempt.id::text)
130
+ AND attempt.account_id = stale.account_id
131
+ AND attempt.workspace_id = stale.workspace_id
132
+ AND attempt.session_id = stale.subject_id
133
+ AND attempt.state IN ('claimed', 'running')
134
+ )
135
+ )
136
+ )
137
+ FOR UPDATE OF stale SKIP LOCKED
138
+ ) victim
139
+ WHERE holder.id = victim.id;
140
+ END IF;
141
+
142
+ IF p_interaction_holder_ttl_ms > 0 THEN
143
+ SELECT coalesce(pg_catalog.array_agg(candidate.id), ARRAY[]::uuid[])
144
+ INTO stale_interaction_ids
145
+ FROM (
146
+ SELECT stale.id
147
+ FROM %1$I.sandbox_lease_holders stale
148
+ JOIN %1$I.sandbox_leases lease ON lease.id = stale.lease_id
149
+ WHERE stale.lease_id = ANY(locked_ids)
150
+ AND stale.kind = 'interaction'
151
+ AND (
152
+ stale.last_heartbeat_at
153
+ < pg_catalog.now()
154
+ - pg_catalog.make_interval(
155
+ secs => p_interaction_holder_ttl_ms / 1000.0
156
+ )
157
+ OR NOT (
158
+ EXISTS (
159
+ SELECT 1
160
+ FROM %1$I.browser_sessions browser
161
+ WHERE stale.holder_id = ('browser-session:' || browser.id::text)
162
+ AND browser.account_id = stale.account_id
163
+ AND browser.workspace_id = stale.workspace_id
164
+ AND browser.controller_host_sandbox_group_id = lease.sandbox_group_id
165
+ AND browser.lifecycle IN (
166
+ 'starting', 'active', 'suspending', 'restoring', 'ending'
167
+ )
168
+ )
169
+ OR EXISTS (
170
+ SELECT 1
171
+ FROM %1$I.computer_sessions computer
172
+ WHERE stale.holder_id = ('computer-session:' || computer.id::text)
173
+ AND computer.account_id = stale.account_id
174
+ AND computer.workspace_id = stale.workspace_id
175
+ AND computer.sandbox_group_id = lease.sandbox_group_id
176
+ AND computer.lifecycle IN (
177
+ 'starting', 'active', 'suspending', 'restoring', 'ending'
178
+ )
179
+ )
180
+ )
181
+ )
182
+ FOR UPDATE OF stale SKIP LOCKED
183
+ ) candidate;
184
+
185
+ IF pg_catalog.cardinality(stale_interaction_ids) > 0 THEN
186
+ WITH updated_browser AS (
187
+ UPDATE %1$I.browser_sessions browser
188
+ SET lifecycle = 'lost',
189
+ controller_id = null,
190
+ controller_generation = null,
191
+ placement_instance_id = null,
192
+ controller_heartbeat_at = null,
193
+ failure_code = 'controller_heartbeat_expired',
194
+ updated_at = pg_catalog.now()
195
+ FROM %1$I.sandbox_lease_holders holder
196
+ WHERE holder.id = ANY(stale_interaction_ids)
197
+ AND holder.holder_id = ('browser-session:' || browser.id::text)
198
+ AND holder.account_id = browser.account_id
199
+ AND holder.workspace_id = browser.workspace_id
200
+ AND browser.lifecycle IN (
201
+ 'starting', 'active', 'suspending', 'restoring', 'ending'
202
+ )
203
+ RETURNING browser.workspace_id
204
+ ), updated_computer AS (
205
+ UPDATE %1$I.computer_sessions computer
206
+ SET lifecycle = 'lost',
207
+ controller_id = null,
208
+ controller_generation = null,
209
+ placement_instance_id = null,
210
+ controller_heartbeat_at = null,
211
+ failure_code = 'controller_heartbeat_expired',
212
+ updated_at = pg_catalog.now()
213
+ FROM %1$I.sandbox_lease_holders holder
214
+ WHERE holder.id = ANY(stale_interaction_ids)
215
+ AND holder.holder_id = ('computer-session:' || computer.id::text)
216
+ AND holder.account_id = computer.account_id
217
+ AND holder.workspace_id = computer.workspace_id
218
+ AND computer.lifecycle IN (
219
+ 'starting', 'active', 'suspending', 'restoring', 'ending'
220
+ )
221
+ RETURNING computer.workspace_id
222
+ ), updated AS (
223
+ SELECT updated_browser.workspace_id AS interaction_workspace_id
224
+ FROM updated_browser
225
+ UNION
226
+ SELECT updated_computer.workspace_id AS interaction_workspace_id
227
+ FROM updated_computer
228
+ )
229
+ SELECT coalesce(
230
+ pg_catalog.array_agg(DISTINCT updated.interaction_workspace_id),
231
+ ARRAY[]::uuid[]
232
+ )
233
+ INTO changed_interaction_workspaces
234
+ FROM updated;
235
+
236
+ DELETE FROM %1$I.sandbox_lease_holders holder
237
+ WHERE holder.id = ANY(stale_interaction_ids);
238
+
239
+ UPDATE %1$I.workspace_interaction_revisions revision
240
+ SET revision = revision.revision + 1,
241
+ updated_at = pg_catalog.now()
242
+ WHERE revision.workspace_id = ANY(changed_interaction_workspaces);
243
+ END IF;
244
+ END IF;
245
+
246
+ UPDATE %1$I.sandbox_leases lease SET
247
+ refcount = counts.total,
248
+ turn_holders = counts.turns,
249
+ viewer_holders = counts.viewers,
250
+ liveness = CASE
251
+ WHEN lease.liveness = 'warm'
252
+ AND counts.total = 0
253
+ AND counts.turns = 0
254
+ AND (
255
+ lease.reaper_hold_id IS NULL
256
+ OR lease.reaper_hold_until <= pg_catalog.now()
257
+ )
258
+ THEN 'draining' ELSE lease.liveness END,
259
+ expires_at = CASE
260
+ WHEN lease.liveness = 'warm'
261
+ AND counts.total = 0
262
+ AND counts.turns = 0
263
+ AND (
264
+ lease.reaper_hold_id IS NULL
265
+ OR lease.reaper_hold_until <= pg_catalog.now()
266
+ )
267
+ THEN CASE
268
+ WHEN lease.rotation_requested_at IS NOT NULL
269
+ OR lease.archive_capture_id IS NOT NULL
270
+ THEN pg_catalog.now() - interval '1 millisecond'
271
+ ELSE pg_catalog.now()
272
+ + pg_catalog.make_interval(secs => p_idle_grace_ms / 1000.0)
273
+ END
274
+ ELSE lease.expires_at END,
275
+ updated_at = pg_catalog.now()
276
+ FROM (
277
+ SELECT candidate.id,
278
+ pg_catalog.count(holder.id)::int AS total,
279
+ pg_catalog.count(holder.id)
280
+ FILTER (WHERE holder.kind = 'turn')::int AS turns,
281
+ pg_catalog.count(holder.id)
282
+ FILTER (WHERE holder.kind = 'viewer')::int AS viewers
283
+ FROM pg_catalog.unnest(locked_ids) candidate(id)
284
+ LEFT JOIN %1$I.sandbox_lease_holders holder
285
+ ON holder.lease_id = candidate.id
286
+ GROUP BY candidate.id
287
+ ) counts
288
+ WHERE lease.id = counts.id;
289
+
290
+ UPDATE %1$I.sandbox_leases lease SET
291
+ liveness = 'cold',
292
+ instance_id = null,
293
+ lease_epoch = lease.lease_epoch + 1,
294
+ reaper_hold_id = null,
295
+ reaper_hold_until = null,
296
+ reaper_hold_reason = null,
297
+ resume_state = opengeni_private.warming_reset_resume_state_v2(
298
+ lease.backend,
299
+ lease.resume_backend_id,
300
+ lease.resume_state,
301
+ lease.workspace_generation,
302
+ lease.archive_generation,
303
+ pg_catalog.clock_timestamp()
304
+ ),
305
+ resume_backend_id = CASE
306
+ WHEN coalesce(
307
+ lease.resume_state #>> '{sessionState,workspaceArchive}', ''
308
+ ) <> ''
309
+ OR coalesce(
310
+ lease.resume_state #>> '{sessionState,workspaceArchivePrev}', ''
311
+ ) <> ''
312
+ THEN coalesce(lease.resume_backend_id, lease.backend)
313
+ ELSE null END,
314
+ data_plane_url = null,
315
+ terminal_data_plane_url = null,
316
+ provider_created_at = null,
317
+ provider_deadline_at = null,
318
+ rotation_requested_at = null,
319
+ rotation_reason = null,
320
+ updated_at = pg_catalog.now()
321
+ WHERE lease.id = ANY(locked_ids)
322
+ AND lease.liveness = 'warming'
323
+ AND lease.expires_at < pg_catalog.now()
324
+ AND lease.instance_id IS NULL
325
+ AND (
326
+ lease.reaper_hold_id IS NULL
327
+ OR lease.reaper_hold_until <= pg_catalog.now()
328
+ );
329
+
330
+ UPDATE %1$I.sandbox_leases lease SET
331
+ liveness = 'draining',
332
+ refcount = 0,
333
+ turn_holders = 0,
334
+ viewer_holders = 0,
335
+ data_plane_url = null,
336
+ terminal_data_plane_url = null,
337
+ lease_epoch = lease.lease_epoch + 1,
338
+ reaper_hold_id = null,
339
+ reaper_hold_until = null,
340
+ reaper_hold_reason = null,
341
+ expires_at = pg_catalog.now() - interval '1 millisecond',
342
+ updated_at = pg_catalog.now()
343
+ WHERE lease.id = ANY(locked_ids)
344
+ AND lease.liveness = 'warming'
345
+ AND lease.expires_at < pg_catalog.now()
346
+ AND lease.instance_id IS NOT NULL
347
+ AND (
348
+ lease.reaper_hold_id IS NULL
349
+ OR lease.reaper_hold_until <= pg_catalog.now()
350
+ );
351
+
352
+ RETURN QUERY
353
+ SELECT lease.workspace_id, lease.sandbox_group_id,
354
+ lease.instance_id, lease.lease_epoch
355
+ FROM %1$I.sandbox_leases lease
356
+ WHERE lease.id = ANY(locked_ids)
357
+ AND lease.liveness = 'draining'
358
+ AND lease.expires_at < pg_catalog.now()
359
+ AND lease.refcount = 0
360
+ AND (
361
+ lease.reaper_hold_id IS NULL
362
+ OR lease.reaper_hold_until <= pg_catalog.now()
363
+ );
364
+ END;
365
+ $function$;
366
+ $create$, data_schema);
367
+ END
368
+ $interaction_aware_reaper$;
369
+
370
+ REVOKE ALL ON FUNCTION opengeni_private.reap_sandbox_leases(
371
+ bigint, bigint, bigint, bigint
372
+ ) FROM PUBLIC;
373
+
374
+ DO $reaper_grant$
375
+ BEGIN
376
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
377
+ GRANT EXECUTE ON FUNCTION opengeni_private.reap_sandbox_leases(
378
+ bigint, bigint, bigint, bigint
379
+ ) TO opengeni_app;
380
+ END IF;
381
+ END
382
+ $reaper_grant$;
383
+
384
+ RESET statement_timeout;
385
+ RESET lock_timeout;