@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,682 @@
1
+ -- deployment-mode: rolling
2
+ -- Make the provider-teardown ownership point durable across a mixed worker
3
+ -- rollout. Pre-0184 reapers clear archive_capture_id after publication and
4
+ -- before provider termination; new admission must not re-arm that exact box in
5
+ -- the resulting window. The existing rotation columns are already honored by
6
+ -- every supported old/new acquire path, so a typed teardown_claim marker is a
7
+ -- backward-compatible bridge as well as useful durable lifecycle truth.
8
+
9
+ SET LOCAL lock_timeout = '5s';
10
+ SET LOCAL statement_timeout = '5min';
11
+
12
+ -- A logical operation survives Temporal retries. capture_id changes per DB
13
+ -- owner, while provider_request_id remains the external operation lineage.
14
+ -- replay_safe is explicit: old rolling workers are false; only a caller that
15
+ -- actually supplied a provider-defined idempotency key may retry immediately.
16
+ ALTER TABLE "sandbox_leases"
17
+ ADD COLUMN IF NOT EXISTS "archive_capture_operation_id" uuid,
18
+ ADD COLUMN IF NOT EXISTS "archive_capture_provider_request_id" uuid,
19
+ ADD COLUMN IF NOT EXISTS "archive_capture_provider_replay_safe" boolean
20
+ NOT NULL DEFAULT false,
21
+ ADD COLUMN IF NOT EXISTS "archive_capture_attempt" integer,
22
+ ADD COLUMN IF NOT EXISTS "archive_capture_published_at" timestamptz,
23
+ ADD COLUMN IF NOT EXISTS "reaper_hold_id" uuid,
24
+ ADD COLUMN IF NOT EXISTS "reaper_hold_until" timestamptz,
25
+ ADD COLUMN IF NOT EXISTS "reaper_hold_reason" text;
26
+
27
+ ALTER TABLE "sandbox_leases"
28
+ DROP CONSTRAINT IF EXISTS "sandbox_leases_reaper_hold_check",
29
+ ADD CONSTRAINT "sandbox_leases_reaper_hold_check"
30
+ CHECK (
31
+ (
32
+ "reaper_hold_id" IS NULL
33
+ AND "reaper_hold_until" IS NULL
34
+ AND "reaper_hold_reason" IS NULL
35
+ ) OR (
36
+ "reaper_hold_id" IS NOT NULL
37
+ AND "reaper_hold_until" IS NOT NULL
38
+ AND "reaper_hold_reason" IS NOT NULL
39
+ AND length("reaper_hold_reason") BETWEEN 1 AND 500
40
+ )
41
+ ) NOT VALID;
42
+
43
+ ALTER TABLE "sandbox_leases"
44
+ VALIDATE CONSTRAINT "sandbox_leases_reaper_hold_check";
45
+
46
+ ALTER TABLE "sandbox_leases"
47
+ DROP CONSTRAINT IF EXISTS "sandbox_leases_rotation_check",
48
+ ADD CONSTRAINT "sandbox_leases_rotation_check"
49
+ CHECK (
50
+ ("rotation_requested_at" IS NULL AND "rotation_reason" IS NULL)
51
+ OR (
52
+ "rotation_requested_at" IS NOT NULL
53
+ AND "rotation_reason" IS NOT NULL
54
+ AND "rotation_reason" IN ('provider_deadline', 'operator', 'teardown_claim')
55
+ )
56
+ ) NOT VALID;
57
+
58
+ ALTER TABLE "sandbox_leases"
59
+ VALIDATE CONSTRAINT "sandbox_leases_rotation_check";
60
+
61
+ CREATE OR REPLACE FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence()
62
+ RETURNS trigger
63
+ LANGUAGE plpgsql
64
+ SET search_path = pg_catalog
65
+ AS $function$
66
+ BEGIN
67
+ -- The receipt must predate teardown. This blocks both a new drain capture and
68
+ -- an attempt replacement from an old/new worker after an operator acquired a
69
+ -- live hold. Warm turn-end capture remains allowed: the hold governs only
70
+ -- automatic draining teardown.
71
+ IF NEW.liveness = 'draining'
72
+ AND NEW.archive_capture_id IS NOT NULL
73
+ AND NEW.archive_capture_id IS DISTINCT FROM OLD.archive_capture_id
74
+ AND NEW.reaper_hold_id IS NOT NULL
75
+ AND NEW.reaper_hold_until > pg_catalog.clock_timestamp()
76
+ THEN
77
+ RAISE EXCEPTION 'sandbox reaper hold blocks drain capture for lease %', NEW.id
78
+ USING ERRCODE = '55000';
79
+ END IF;
80
+
81
+ -- Protect old confirmDrainCold implementations too. A hold can be acquired
82
+ -- only before the durable teardown claim, so a draining->cold write during an
83
+ -- active hold is always stale/unsafe.
84
+ IF OLD.liveness = 'draining'
85
+ AND NEW.liveness = 'cold'
86
+ AND NEW.reaper_hold_id IS NOT NULL
87
+ AND NEW.reaper_hold_until > pg_catalog.clock_timestamp()
88
+ THEN
89
+ RAISE EXCEPTION 'sandbox reaper hold blocks cold commit for lease %', NEW.id
90
+ USING ERRCODE = '55000';
91
+ END IF;
92
+
93
+ -- A mixed-version race needs a table-boundary ownership check too. An old
94
+ -- reaper publishes its archive by clearing archive_capture_id, then performs
95
+ -- provider teardown and a later draining->cold write. A new reaper can claim
96
+ -- the row in that window. The old cold write must not atomically erase that
97
+ -- newer claim. New confirmDrainCold sets this transaction-local receipt only
98
+ -- after locking and revalidating the exact capture; old binaries set nothing.
99
+ IF OLD.liveness = 'draining'
100
+ AND NEW.liveness = 'cold'
101
+ AND OLD.archive_capture_id IS NOT NULL
102
+ AND NULLIF(
103
+ pg_catalog.current_setting('opengeni.sandbox_drain_capture_id', true),
104
+ ''
105
+ ) IS DISTINCT FROM OLD.archive_capture_id::text
106
+ THEN
107
+ RAISE EXCEPTION 'sandbox drain capture ownership blocks cold commit for lease %', NEW.id
108
+ USING ERRCODE = '55000';
109
+ END IF;
110
+
111
+ -- Old binaries know only archive_capture_id. Normalize their writes at the
112
+ -- table boundary so the rolling deployment always retains a complete receipt.
113
+ IF NEW.archive_capture_id IS NULL THEN
114
+ NEW.archive_capture_operation_id := NULL;
115
+ NEW.archive_capture_provider_request_id := NULL;
116
+ NEW.archive_capture_provider_replay_safe := false;
117
+ NEW.archive_capture_attempt := NULL;
118
+ NEW.archive_capture_published_at := NULL;
119
+ ELSIF NEW.archive_capture_id IS DISTINCT FROM OLD.archive_capture_id THEN
120
+ -- Publication belongs to one exact physical capture. A replacement cannot
121
+ -- inherit permission to skip capture from its predecessor.
122
+ NEW.archive_capture_published_at := NULL;
123
+ IF NEW.archive_capture_operation_id IS NOT DISTINCT FROM OLD.archive_capture_operation_id
124
+ AND NEW.archive_capture_attempt IS NOT DISTINCT FROM OLD.archive_capture_attempt
125
+ THEN
126
+ NEW.archive_capture_operation_id := NEW.archive_capture_id;
127
+ NEW.archive_capture_provider_request_id := NEW.archive_capture_id;
128
+ NEW.archive_capture_provider_replay_safe := false;
129
+ NEW.archive_capture_attempt := 1;
130
+ ELSE
131
+ NEW.archive_capture_operation_id := COALESCE(
132
+ NEW.archive_capture_operation_id,
133
+ NEW.archive_capture_id
134
+ );
135
+ NEW.archive_capture_provider_request_id := COALESCE(
136
+ NEW.archive_capture_provider_request_id,
137
+ NEW.archive_capture_id
138
+ );
139
+ NEW.archive_capture_attempt := COALESCE(NEW.archive_capture_attempt, 1);
140
+ END IF;
141
+ ELSE
142
+ NEW.archive_capture_operation_id := COALESCE(
143
+ NEW.archive_capture_operation_id,
144
+ NEW.archive_capture_id
145
+ );
146
+ NEW.archive_capture_provider_request_id := COALESCE(
147
+ NEW.archive_capture_provider_request_id,
148
+ NEW.archive_capture_id
149
+ );
150
+ NEW.archive_capture_attempt := COALESCE(NEW.archive_capture_attempt, 1);
151
+ END IF;
152
+
153
+ IF OLD.liveness = 'draining'
154
+ AND NEW.liveness = 'draining'
155
+ AND OLD.archive_capture_id IS NULL
156
+ AND NEW.archive_capture_id IS NOT NULL
157
+ AND NEW.rotation_requested_at IS NULL
158
+ THEN
159
+ NEW.rotation_requested_at := COALESCE(
160
+ NEW.archive_capture_started_at,
161
+ pg_catalog.clock_timestamp()
162
+ );
163
+ NEW.rotation_reason := 'teardown_claim';
164
+ END IF;
165
+ RETURN NEW;
166
+ END
167
+ $function$;
168
+
169
+ DROP TRIGGER IF EXISTS sandbox_leases_drain_teardown_fence ON "sandbox_leases";
170
+ CREATE TRIGGER sandbox_leases_drain_teardown_fence
171
+ BEFORE UPDATE OF "archive_capture_id", "liveness" ON "sandbox_leases"
172
+ FOR EACH ROW
173
+ EXECUTE FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence();
174
+
175
+ -- Cover every claim already held when the online migration activates. The
176
+ -- trigger owns every claim that starts after it is installed.
177
+ UPDATE "sandbox_leases"
178
+ SET
179
+ "archive_capture_operation_id" = COALESCE(
180
+ "archive_capture_operation_id",
181
+ "archive_capture_id"
182
+ ),
183
+ "archive_capture_provider_request_id" = "archive_capture_id",
184
+ "archive_capture_provider_replay_safe" = false,
185
+ "archive_capture_attempt" = COALESCE("archive_capture_attempt", 1),
186
+ "updated_at" = pg_catalog.clock_timestamp()
187
+ WHERE "archive_capture_id" IS NOT NULL;
188
+
189
+ -- A pre-migration draining claim also needs the old-reader teardown bridge.
190
+ UPDATE "sandbox_leases"
191
+ SET
192
+ "rotation_requested_at" = COALESCE(
193
+ "rotation_requested_at",
194
+ "archive_capture_started_at",
195
+ pg_catalog.clock_timestamp()
196
+ ),
197
+ "rotation_reason" = COALESCE("rotation_reason", 'teardown_claim'),
198
+ "updated_at" = pg_catalog.clock_timestamp()
199
+ WHERE "liveness" = 'draining'
200
+ AND "archive_capture_id" IS NOT NULL
201
+ AND "rotation_requested_at" IS NULL;
202
+
203
+ ALTER TABLE "sandbox_leases"
204
+ DROP CONSTRAINT IF EXISTS "sandbox_leases_archive_capture_check",
205
+ ADD CONSTRAINT "sandbox_leases_archive_capture_check"
206
+ CHECK (
207
+ (
208
+ "archive_capture_id" IS NULL
209
+ AND "archive_capture_operation_id" IS NULL
210
+ AND "archive_capture_provider_request_id" IS NULL
211
+ AND "archive_capture_provider_replay_safe" = false
212
+ AND "archive_capture_attempt" IS NULL
213
+ AND "archive_capture_generation" IS NULL
214
+ AND "archive_capture_started_at" IS NULL
215
+ AND "archive_capture_deadline_at" IS NULL
216
+ AND "archive_capture_published_at" IS NULL
217
+ ) OR (
218
+ "archive_capture_id" IS NOT NULL
219
+ AND "archive_capture_operation_id" IS NOT NULL
220
+ AND "archive_capture_provider_request_id" IS NOT NULL
221
+ AND (
222
+ "archive_capture_provider_replay_safe" = false
223
+ OR (
224
+ "backend" = 'modal'
225
+ AND COALESCE(
226
+ "resume_state" #>> '{sessionState,providerState,workspacePersistence}',
227
+ "resume_state" #>> '{sessionState,workspacePersistence}'
228
+ ) = 'snapshot_filesystem'
229
+ )
230
+ )
231
+ AND "archive_capture_attempt" IS NOT NULL
232
+ AND "archive_capture_attempt" > 0
233
+ AND "archive_capture_generation" IS NOT NULL
234
+ AND "archive_capture_generation" = "workspace_generation"
235
+ AND "archive_capture_started_at" IS NOT NULL
236
+ AND "archive_capture_deadline_at" IS NOT NULL
237
+ AND "archive_capture_deadline_at" > "archive_capture_started_at"
238
+ AND (
239
+ "archive_capture_published_at" IS NULL
240
+ OR "liveness" = 'draining'
241
+ )
242
+ )
243
+ ) NOT VALID;
244
+
245
+ ALTER TABLE "sandbox_leases"
246
+ VALIDATE CONSTRAINT "sandbox_leases_archive_capture_check";
247
+
248
+ -- The global reaper is database-owned, so replacing it here protects a hold
249
+ -- even while old worker binaries are still running. Held rows may still have
250
+ -- stale holder receipts reconciled, but cannot enter/complete automatic drain.
251
+ DO $hold_aware_reaper$
252
+ DECLARE data_schema text := current_schema();
253
+ BEGIN
254
+ EXECUTE format($create$
255
+ CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
256
+ p_viewer_holder_ttl_ms bigint,
257
+ p_turn_holder_ttl_ms bigint,
258
+ p_idle_grace_ms bigint
259
+ )
260
+ RETURNS TABLE (
261
+ workspace_id uuid,
262
+ sandbox_group_id uuid,
263
+ instance_id text,
264
+ lease_epoch integer
265
+ )
266
+ LANGUAGE plpgsql
267
+ SECURITY DEFINER
268
+ SET search_path = pg_catalog
269
+ AS $function$
270
+ DECLARE locked_ids uuid[];
271
+ BEGIN
272
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
273
+
274
+ SELECT coalesce(
275
+ pg_catalog.array_agg(candidate.id),
276
+ ARRAY[]::uuid[]
277
+ )
278
+ INTO locked_ids
279
+ FROM (
280
+ SELECT lease.id
281
+ FROM %1$I.sandbox_leases lease
282
+ WHERE lease.liveness <> 'cold'
283
+ OR EXISTS (
284
+ SELECT 1
285
+ FROM %1$I.sandbox_lease_holders holder
286
+ WHERE holder.lease_id = lease.id
287
+ )
288
+ ORDER BY lease.updated_at, lease.id
289
+ LIMIT 500
290
+ FOR UPDATE OF lease SKIP LOCKED
291
+ ) candidate;
292
+
293
+ IF pg_catalog.cardinality(locked_ids) = 0 THEN
294
+ RETURN;
295
+ END IF;
296
+
297
+ DELETE FROM %1$I.sandbox_lease_holders holder
298
+ USING (
299
+ SELECT stale.id
300
+ FROM %1$I.sandbox_lease_holders stale
301
+ WHERE stale.lease_id = ANY(locked_ids)
302
+ AND stale.kind IN ('viewer', 'direct')
303
+ AND stale.last_heartbeat_at
304
+ < pg_catalog.now()
305
+ - pg_catalog.make_interval(secs => p_viewer_holder_ttl_ms / 1000.0)
306
+ FOR UPDATE OF stale SKIP LOCKED
307
+ ) victim
308
+ WHERE holder.id = victim.id;
309
+
310
+ IF p_turn_holder_ttl_ms > 0 THEN
311
+ DELETE FROM %1$I.sandbox_lease_holders holder
312
+ USING (
313
+ SELECT stale.id
314
+ FROM %1$I.sandbox_lease_holders stale
315
+ WHERE stale.lease_id = ANY(locked_ids)
316
+ AND stale.kind = 'turn'
317
+ AND stale.last_heartbeat_at
318
+ < pg_catalog.now()
319
+ - pg_catalog.make_interval(secs => p_turn_holder_ttl_ms / 1000.0)
320
+ FOR UPDATE OF stale SKIP LOCKED
321
+ ) victim
322
+ WHERE holder.id = victim.id;
323
+ END IF;
324
+
325
+ UPDATE %1$I.sandbox_leases lease SET
326
+ refcount = counts.total,
327
+ turn_holders = counts.turns,
328
+ viewer_holders = counts.viewers,
329
+ liveness = CASE
330
+ WHEN lease.liveness = 'warm'
331
+ AND counts.total = 0
332
+ AND counts.turns = 0
333
+ AND (
334
+ lease.reaper_hold_id IS NULL
335
+ OR lease.reaper_hold_until <= pg_catalog.now()
336
+ )
337
+ THEN 'draining' ELSE lease.liveness END,
338
+ expires_at = CASE
339
+ WHEN lease.liveness = 'warm'
340
+ AND counts.total = 0
341
+ AND counts.turns = 0
342
+ AND (
343
+ lease.reaper_hold_id IS NULL
344
+ OR lease.reaper_hold_until <= pg_catalog.now()
345
+ )
346
+ THEN CASE
347
+ WHEN lease.rotation_requested_at IS NOT NULL
348
+ THEN pg_catalog.now() - interval '1 millisecond'
349
+ ELSE pg_catalog.now()
350
+ + pg_catalog.make_interval(secs => p_idle_grace_ms / 1000.0)
351
+ END
352
+ ELSE lease.expires_at END,
353
+ updated_at = pg_catalog.now()
354
+ FROM (
355
+ SELECT candidate.id,
356
+ pg_catalog.count(holder.id)::int AS total,
357
+ pg_catalog.count(holder.id)
358
+ FILTER (WHERE holder.kind = 'turn')::int AS turns,
359
+ pg_catalog.count(holder.id)
360
+ FILTER (WHERE holder.kind = 'viewer')::int AS viewers
361
+ FROM pg_catalog.unnest(locked_ids) candidate(id)
362
+ LEFT JOIN %1$I.sandbox_lease_holders holder
363
+ ON holder.lease_id = candidate.id
364
+ GROUP BY candidate.id
365
+ ) counts
366
+ WHERE lease.id = counts.id;
367
+
368
+ UPDATE %1$I.sandbox_leases lease SET
369
+ liveness = 'cold',
370
+ instance_id = null,
371
+ lease_epoch = lease.lease_epoch + 1,
372
+ reaper_hold_id = null,
373
+ reaper_hold_until = null,
374
+ reaper_hold_reason = null,
375
+ resume_state = opengeni_private.warming_reset_resume_state_v2(
376
+ lease.backend,
377
+ lease.resume_backend_id,
378
+ lease.resume_state,
379
+ lease.workspace_generation,
380
+ lease.archive_generation,
381
+ pg_catalog.clock_timestamp()
382
+ ),
383
+ resume_backend_id = CASE
384
+ WHEN coalesce(
385
+ lease.resume_state #>> '{sessionState,workspaceArchive}', ''
386
+ ) <> ''
387
+ OR coalesce(
388
+ lease.resume_state #>> '{sessionState,workspaceArchivePrev}', ''
389
+ ) <> ''
390
+ THEN coalesce(lease.resume_backend_id, lease.backend)
391
+ ELSE null END,
392
+ data_plane_url = null,
393
+ terminal_data_plane_url = null,
394
+ provider_created_at = null,
395
+ provider_deadline_at = null,
396
+ rotation_requested_at = null,
397
+ rotation_reason = null,
398
+ updated_at = pg_catalog.now()
399
+ WHERE lease.id = ANY(locked_ids)
400
+ AND lease.liveness = 'warming'
401
+ AND lease.expires_at < pg_catalog.now()
402
+ AND lease.instance_id IS NULL
403
+ AND (
404
+ lease.reaper_hold_id IS NULL
405
+ OR lease.reaper_hold_until <= pg_catalog.now()
406
+ );
407
+
408
+ UPDATE %1$I.sandbox_leases lease SET
409
+ liveness = 'draining',
410
+ refcount = 0,
411
+ turn_holders = 0,
412
+ viewer_holders = 0,
413
+ data_plane_url = null,
414
+ terminal_data_plane_url = null,
415
+ lease_epoch = lease.lease_epoch + 1,
416
+ reaper_hold_id = null,
417
+ reaper_hold_until = null,
418
+ reaper_hold_reason = null,
419
+ expires_at = pg_catalog.now() - interval '1 millisecond',
420
+ updated_at = pg_catalog.now()
421
+ WHERE lease.id = ANY(locked_ids)
422
+ AND lease.liveness = 'warming'
423
+ AND lease.expires_at < pg_catalog.now()
424
+ AND lease.instance_id IS NOT NULL
425
+ AND (
426
+ lease.reaper_hold_id IS NULL
427
+ OR lease.reaper_hold_until <= pg_catalog.now()
428
+ );
429
+
430
+ RETURN QUERY
431
+ SELECT lease.workspace_id, lease.sandbox_group_id,
432
+ lease.instance_id, lease.lease_epoch
433
+ FROM %1$I.sandbox_leases lease
434
+ WHERE lease.id = ANY(locked_ids)
435
+ AND lease.liveness = 'draining'
436
+ AND lease.expires_at < pg_catalog.now()
437
+ AND lease.refcount = 0
438
+ AND (
439
+ lease.reaper_hold_id IS NULL
440
+ OR lease.reaper_hold_until <= pg_catalog.now()
441
+ );
442
+ END;
443
+ $function$;
444
+ $create$, data_schema);
445
+ END
446
+ $hold_aware_reaper$;
447
+
448
+ -- Provider-deadline admission is also automatic teardown. Keep the exact
449
+ -- provider available while held; release/expiry makes it eligible next scan.
450
+ DO $hold_aware_rotations$
451
+ DECLARE data_schema text := current_schema();
452
+ BEGIN
453
+ EXECUTE format($create$
454
+ CREATE OR REPLACE FUNCTION opengeni_private.request_due_sandbox_rotations(
455
+ p_lead_ms bigint,
456
+ p_limit integer
457
+ )
458
+ RETURNS integer
459
+ LANGUAGE plpgsql
460
+ SECURITY DEFINER
461
+ SET search_path = pg_catalog
462
+ AS $function$
463
+ DECLARE
464
+ requested integer;
465
+ requested_ids uuid[];
466
+ BEGIN
467
+ IF p_lead_ms IS NULL OR p_lead_ms < 0 OR p_lead_ms > 86400000 THEN
468
+ RAISE EXCEPTION 'sandbox rotation lead must be between 0 and 24h'
469
+ USING ERRCODE = '22023';
470
+ END IF;
471
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
472
+ RAISE EXCEPTION 'sandbox rotation batch limit must be between 1 and 500'
473
+ USING ERRCODE = '22023';
474
+ END IF;
475
+
476
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
477
+
478
+ WITH candidates AS MATERIALIZED (
479
+ SELECT lease.id
480
+ FROM %1$I.sandbox_leases lease
481
+ WHERE lease.backend = 'modal'
482
+ AND lease.liveness IN ('warming', 'warm')
483
+ AND lease.provider_deadline_at IS NOT NULL
484
+ AND lease.provider_deadline_at
485
+ <= pg_catalog.now()
486
+ + pg_catalog.make_interval(secs => p_lead_ms / 1000.0)
487
+ AND lease.rotation_requested_at IS NULL
488
+ AND (
489
+ lease.reaper_hold_id IS NULL
490
+ OR lease.reaper_hold_until <= pg_catalog.now()
491
+ )
492
+ ORDER BY lease.provider_deadline_at, lease.id
493
+ FOR UPDATE SKIP LOCKED
494
+ LIMIT p_limit
495
+ ),
496
+ due AS (
497
+ UPDATE %1$I.sandbox_leases lease SET
498
+ rotation_requested_at = pg_catalog.now(),
499
+ rotation_reason = 'provider_deadline',
500
+ updated_at = pg_catalog.now()
501
+ FROM candidates
502
+ WHERE lease.id = candidates.id
503
+ AND (
504
+ lease.reaper_hold_id IS NULL
505
+ OR lease.reaper_hold_until <= pg_catalog.now()
506
+ )
507
+ RETURNING lease.id
508
+ )
509
+ SELECT
510
+ coalesce(pg_catalog.array_agg(due.id), ARRAY[]::uuid[]),
511
+ pg_catalog.count(*)::integer
512
+ INTO requested_ids, requested
513
+ FROM due;
514
+
515
+ DELETE FROM %1$I.sandbox_lease_holders holder
516
+ WHERE holder.lease_id = ANY(requested_ids)
517
+ AND holder.kind = 'viewer';
518
+
519
+ UPDATE %1$I.sandbox_leases lease SET
520
+ refcount = counts.total,
521
+ turn_holders = counts.turns,
522
+ viewer_holders = counts.viewers,
523
+ liveness = CASE
524
+ WHEN lease.liveness = 'warm' AND counts.total = 0 THEN 'draining'
525
+ ELSE lease.liveness
526
+ END,
527
+ expires_at = CASE
528
+ WHEN lease.liveness = 'warm' AND counts.total = 0
529
+ THEN pg_catalog.now() - interval '1 millisecond'
530
+ ELSE lease.expires_at
531
+ END,
532
+ updated_at = pg_catalog.now()
533
+ FROM (
534
+ SELECT candidate.id,
535
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
536
+ WHERE holder.lease_id = candidate.id)::int AS total,
537
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
538
+ WHERE holder.lease_id = candidate.id AND holder.kind = 'turn')::int AS turns,
539
+ (SELECT pg_catalog.count(*) FROM %1$I.sandbox_lease_holders holder
540
+ WHERE holder.lease_id = candidate.id AND holder.kind = 'viewer')::int AS viewers
541
+ FROM unnest(requested_ids) requested_id(id)
542
+ JOIN %1$I.sandbox_leases candidate ON candidate.id = requested_id.id
543
+ ) counts
544
+ WHERE lease.id = counts.id;
545
+
546
+ RETURN requested;
547
+ END;
548
+ $function$;
549
+ $create$, data_schema);
550
+ END
551
+ $hold_aware_rotations$;
552
+
553
+ -- A native checkpoint exists before its candidate receipt can be published to
554
+ -- current/previous. While an archive-capture claim still owns the exact source
555
+ -- lease/epoch/instance/generation, every native candidate produced from that
556
+ -- physical capture is durable recovery state rather than an orphan. Protect it
557
+ -- from generic candidate GC until the claim settles. The provider request id is
558
+ -- deliberately not compared with object_id: Modal's request UUID and returned
559
+ -- Image id are different identifiers.
560
+ DO $capture_aware_checkpoint_gc$
561
+ DECLARE data_schema text := current_schema();
562
+ BEGIN
563
+ EXECUTE format($create$
564
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_sandbox_checkpoint_artifacts(
565
+ p_claim_id uuid,
566
+ p_limit integer,
567
+ p_claim_ttl_ms bigint
568
+ )
569
+ RETURNS TABLE (
570
+ id uuid,
571
+ provider_backend text,
572
+ provider_binding_key text,
573
+ provider_binding jsonb,
574
+ object_kind text,
575
+ object_id text,
576
+ delete_attempts integer
577
+ )
578
+ LANGUAGE plpgsql
579
+ SECURITY DEFINER
580
+ SET search_path = pg_catalog
581
+ AS $function$
582
+ BEGIN
583
+ IF p_claim_id IS NULL THEN
584
+ RAISE EXCEPTION 'checkpoint artifact claim id is required'
585
+ USING ERRCODE = '22023';
586
+ END IF;
587
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 500 THEN
588
+ RAISE EXCEPTION 'checkpoint artifact claim limit must be between 1 and 500'
589
+ USING ERRCODE = '22023';
590
+ END IF;
591
+ IF p_claim_ttl_ms IS NULL
592
+ OR p_claim_ttl_ms < 1000
593
+ OR p_claim_ttl_ms > 3600000
594
+ THEN
595
+ RAISE EXCEPTION 'checkpoint artifact claim TTL must be between 1s and 1h'
596
+ USING ERRCODE = '22023';
597
+ END IF;
598
+
599
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
600
+
601
+ WITH stale_claims AS MATERIALIZED (
602
+ SELECT artifact.id
603
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
604
+ WHERE artifact.state = 'deleting'
605
+ AND artifact.delete_claimed_at
606
+ < pg_catalog.now()
607
+ - pg_catalog.make_interval(secs => p_claim_ttl_ms / 1000.0)
608
+ ORDER BY artifact.delete_claimed_at, artifact.id
609
+ FOR UPDATE SKIP LOCKED
610
+ LIMIT p_limit
611
+ )
612
+ UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
613
+ state = 'delete_failed',
614
+ delete_after = pg_catalog.now(),
615
+ delete_claim_id = null,
616
+ delete_claimed_at = null,
617
+ last_delete_error = coalesce(
618
+ last_delete_error, 'stale delete claim recovered'
619
+ ),
620
+ updated_at = pg_catalog.now()
621
+ FROM stale_claims
622
+ WHERE artifact.id = stale_claims.id;
623
+
624
+ RETURN QUERY
625
+ WITH candidates AS (
626
+ SELECT artifact.id
627
+ FROM %1$I.sandbox_checkpoint_artifacts artifact
628
+ WHERE (
629
+ (
630
+ artifact.state = 'candidate'
631
+ AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
632
+ )
633
+ OR (
634
+ artifact.state IN ('current', 'previous')
635
+ AND artifact.created_at < pg_catalog.now() - interval '15 minutes'
636
+ )
637
+ OR (
638
+ artifact.state IN ('delete_pending', 'delete_failed')
639
+ AND coalesce(artifact.delete_after, artifact.created_at)
640
+ <= pg_catalog.now()
641
+ )
642
+ )
643
+ AND NOT EXISTS (
644
+ SELECT 1 FROM %1$I.sandbox_leases lease
645
+ WHERE lease.current_checkpoint_artifact_id = artifact.id
646
+ OR lease.previous_checkpoint_artifact_id = artifact.id
647
+ OR (
648
+ artifact.provenance = 'native_capture'
649
+ AND lease.id = artifact.source_lease_id
650
+ AND lease.lease_epoch = artifact.source_lease_epoch
651
+ AND lease.instance_id = artifact.source_instance_id
652
+ AND lease.backend = artifact.provider_backend
653
+ AND lease.archive_capture_id IS NOT NULL
654
+ AND lease.archive_capture_generation
655
+ = artifact.source_workspace_generation
656
+ )
657
+ )
658
+ ORDER BY coalesce(artifact.delete_after, artifact.created_at), artifact.id
659
+ FOR UPDATE SKIP LOCKED
660
+ LIMIT p_limit
661
+ )
662
+ UPDATE %1$I.sandbox_checkpoint_artifacts artifact SET
663
+ state = 'deleting',
664
+ delete_attempts = artifact.delete_attempts + 1,
665
+ delete_claim_id = p_claim_id,
666
+ delete_claimed_at = pg_catalog.now(),
667
+ updated_at = pg_catalog.now()
668
+ FROM candidates
669
+ WHERE artifact.id = candidates.id
670
+ RETURNING artifact.id, artifact.provider_backend, artifact.provider_binding_key,
671
+ artifact.provider_binding, artifact.object_kind, artifact.object_id,
672
+ artifact.delete_attempts;
673
+ END;
674
+ $function$;
675
+ $create$, data_schema);
676
+ END
677
+ $capture_aware_checkpoint_gc$;
678
+
679
+ REVOKE ALL ON FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence() FROM PUBLIC;
680
+
681
+ RESET statement_timeout;
682
+ RESET lock_timeout;