@opengeni/db 0.28.1 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) 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 +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,168 @@
1
+ -- deployment-mode: rolling
2
+ -- A workspace cascade cannot atomically delete its external Temporal schedules.
3
+ -- Persist that ownership before the cascade, then let any API replica claim and
4
+ -- settle the idempotent delete. The row intentionally has no workspace/account
5
+ -- FK: surviving deletion of those parents is its entire purpose.
6
+
7
+ CREATE TABLE "temporal_schedule_cleanup_outbox" (
8
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
9
+ "account_id" uuid NOT NULL,
10
+ "workspace_id" uuid NOT NULL,
11
+ "temporal_schedule_id" text NOT NULL,
12
+ "claim_id" uuid,
13
+ "claim_until" timestamptz,
14
+ "attempt_count" integer NOT NULL DEFAULT 0,
15
+ "next_attempt_at" timestamptz NOT NULL DEFAULT now(),
16
+ "last_error" text,
17
+ "created_at" timestamptz NOT NULL DEFAULT now(),
18
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
19
+ CONSTRAINT "temporal_schedule_cleanup_outbox_valid_chk" CHECK (
20
+ length("temporal_schedule_id") BETWEEN 1 AND 512
21
+ AND "attempt_count" >= 0
22
+ AND (("claim_id" IS NULL AND "claim_until" IS NULL)
23
+ OR ("claim_id" IS NOT NULL AND "claim_until" IS NOT NULL))
24
+ AND ("last_error" IS NULL OR length("last_error") <= 2000)
25
+ )
26
+ );
27
+
28
+ CREATE UNIQUE INDEX "temporal_schedule_cleanup_outbox_schedule_uq"
29
+ ON "temporal_schedule_cleanup_outbox" ("temporal_schedule_id");
30
+ CREATE INDEX "temporal_schedule_cleanup_outbox_due_idx"
31
+ ON "temporal_schedule_cleanup_outbox" (
32
+ "next_attempt_at", "claim_until", "id"
33
+ );
34
+
35
+ ALTER TABLE "temporal_schedule_cleanup_outbox" ENABLE ROW LEVEL SECURITY;
36
+ ALTER TABLE "temporal_schedule_cleanup_outbox" FORCE ROW LEVEL SECURITY;
37
+ CREATE POLICY workspace_isolation ON "temporal_schedule_cleanup_outbox"
38
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
39
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
40
+
41
+ DO $migration$
42
+ DECLARE target_schema text := current_schema();
43
+ BEGIN
44
+ EXECUTE format($create$
45
+ CREATE FUNCTION opengeni_private.claim_temporal_schedule_cleanups(
46
+ p_claim_id uuid,
47
+ p_limit integer,
48
+ p_claim_seconds integer
49
+ )
50
+ RETURNS TABLE (
51
+ id uuid,
52
+ account_id uuid,
53
+ workspace_id uuid,
54
+ temporal_schedule_id text,
55
+ attempt_count integer
56
+ )
57
+ LANGUAGE plpgsql
58
+ SECURITY DEFINER
59
+ SET search_path = pg_catalog
60
+ AS $function$
61
+ BEGIN
62
+ IF p_claim_id IS NULL THEN
63
+ RAISE EXCEPTION 'temporal schedule cleanup claim id is required'
64
+ USING ERRCODE = '22023';
65
+ END IF;
66
+
67
+ RETURN QUERY
68
+ WITH due AS (
69
+ SELECT cleanup.id
70
+ FROM %1$I.temporal_schedule_cleanup_outbox cleanup
71
+ WHERE cleanup.next_attempt_at <= pg_catalog.now()
72
+ AND (
73
+ cleanup.claim_until IS NULL
74
+ OR cleanup.claim_until <= pg_catalog.now()
75
+ )
76
+ ORDER BY cleanup.next_attempt_at, cleanup.created_at, cleanup.id
77
+ FOR UPDATE SKIP LOCKED
78
+ LIMIT greatest(1, least(coalesce(p_limit, 32), 100))
79
+ )
80
+ UPDATE %1$I.temporal_schedule_cleanup_outbox cleanup
81
+ SET claim_id = p_claim_id,
82
+ claim_until = pg_catalog.now() + pg_catalog.make_interval(
83
+ secs => greatest(
84
+ 5,
85
+ least(coalesce(p_claim_seconds, 15), 300)
86
+ )
87
+ ),
88
+ attempt_count = cleanup.attempt_count + 1,
89
+ updated_at = pg_catalog.now()
90
+ FROM due
91
+ WHERE cleanup.id = due.id
92
+ RETURNING cleanup.id, cleanup.account_id, cleanup.workspace_id,
93
+ cleanup.temporal_schedule_id, cleanup.attempt_count;
94
+ END $function$;
95
+ $create$, target_schema);
96
+ END $migration$;
97
+
98
+ REVOKE ALL ON FUNCTION opengeni_private.claim_temporal_schedule_cleanups(uuid, integer, integer)
99
+ FROM PUBLIC;
100
+
101
+ DO $migration$
102
+ DECLARE target_schema text := current_schema();
103
+ BEGIN
104
+ EXECUTE format($create$
105
+ CREATE FUNCTION opengeni_private.settle_temporal_schedule_cleanup(
106
+ p_id uuid,
107
+ p_claim_id uuid,
108
+ p_error text
109
+ )
110
+ RETURNS boolean
111
+ LANGUAGE plpgsql
112
+ SECURITY DEFINER
113
+ SET search_path = pg_catalog
114
+ AS $function$
115
+ DECLARE settled_rows bigint := 0;
116
+ BEGIN
117
+ IF p_id IS NULL OR p_claim_id IS NULL THEN
118
+ RAISE EXCEPTION 'temporal schedule cleanup id and claim id are required'
119
+ USING ERRCODE = '22023';
120
+ END IF;
121
+
122
+ IF p_error IS NULL THEN
123
+ DELETE FROM %1$I.temporal_schedule_cleanup_outbox cleanup
124
+ WHERE cleanup.id = p_id AND cleanup.claim_id = p_claim_id;
125
+ GET DIAGNOSTICS settled_rows = ROW_COUNT;
126
+ RETURN settled_rows = 1;
127
+ END IF;
128
+
129
+ UPDATE %1$I.temporal_schedule_cleanup_outbox cleanup
130
+ SET claim_id = NULL,
131
+ claim_until = NULL,
132
+ next_attempt_at = pg_catalog.now() + pg_catalog.make_interval(
133
+ secs => least(
134
+ 300,
135
+ greatest(
136
+ 1,
137
+ pg_catalog.power(2, least(cleanup.attempt_count - 1, 8))::integer
138
+ )
139
+ )
140
+ ),
141
+ last_error = pg_catalog.left(p_error, 2000),
142
+ updated_at = pg_catalog.now()
143
+ WHERE cleanup.id = p_id AND cleanup.claim_id = p_claim_id;
144
+ GET DIAGNOSTICS settled_rows = ROW_COUNT;
145
+ RETURN settled_rows = 1;
146
+ END $function$;
147
+ $create$, target_schema);
148
+ END $migration$;
149
+
150
+ REVOKE ALL ON FUNCTION opengeni_private.settle_temporal_schedule_cleanup(uuid, uuid, text)
151
+ FROM PUBLIC;
152
+
153
+ DO $$
154
+ DECLARE target_schema text := current_schema();
155
+ BEGIN
156
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
157
+ EXECUTE format(
158
+ 'GRANT SELECT, INSERT ON TABLE %I.temporal_schedule_cleanup_outbox TO opengeni_app',
159
+ target_schema
160
+ );
161
+ GRANT EXECUTE ON FUNCTION
162
+ opengeni_private.claim_temporal_schedule_cleanups(uuid, integer, integer)
163
+ TO opengeni_app;
164
+ GRANT EXECUTE ON FUNCTION
165
+ opengeni_private.settle_temporal_schedule_cleanup(uuid, uuid, text)
166
+ TO opengeni_app;
167
+ END IF;
168
+ END $$;
@@ -0,0 +1,400 @@
1
+ -- deployment-mode: rolling
2
+ -- Make provider capture takeover semantics explicit and provider-neutral. The
3
+ -- durable bit records whether a successor may safely overlap an ambiguous
4
+ -- predecessor. It covers both same-request provider idempotency and independent
5
+ -- read-only captures; replay_safe remains the narrower wire-idempotency proof.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '5min';
9
+
10
+ ALTER TABLE "sandbox_leases"
11
+ ADD COLUMN IF NOT EXISTS "archive_capture_takeover_safe" boolean
12
+ NOT NULL DEFAULT false;
13
+
14
+ -- Claims created before this migration had only the narrower Modal replay
15
+ -- receipt. Preserve exactly that proven subset; all other legacy claims remain
16
+ -- exclusive until their deadline plus provider-specific proof.
17
+ UPDATE "sandbox_leases"
18
+ SET
19
+ "archive_capture_takeover_safe" = "archive_capture_provider_replay_safe",
20
+ "updated_at" = pg_catalog.clock_timestamp()
21
+ WHERE "archive_capture_id" IS NOT NULL
22
+ AND "archive_capture_takeover_safe" IS DISTINCT FROM
23
+ "archive_capture_provider_replay_safe";
24
+
25
+ CREATE OR REPLACE FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence()
26
+ RETURNS trigger
27
+ LANGUAGE plpgsql
28
+ SET search_path = pg_catalog
29
+ AS $function$
30
+ BEGIN
31
+ IF NEW.liveness = 'draining'
32
+ AND NEW.archive_capture_id IS NOT NULL
33
+ AND NEW.archive_capture_id IS DISTINCT FROM OLD.archive_capture_id
34
+ AND NEW.reaper_hold_id IS NOT NULL
35
+ AND NEW.reaper_hold_until > pg_catalog.clock_timestamp()
36
+ THEN
37
+ RAISE EXCEPTION 'sandbox reaper hold blocks drain capture for lease %', NEW.id
38
+ USING ERRCODE = '55000';
39
+ END IF;
40
+
41
+ IF OLD.liveness = 'draining'
42
+ AND NEW.liveness = 'cold'
43
+ AND NEW.reaper_hold_id IS NOT NULL
44
+ AND NEW.reaper_hold_until > pg_catalog.clock_timestamp()
45
+ THEN
46
+ RAISE EXCEPTION 'sandbox reaper hold blocks cold commit for lease %', NEW.id
47
+ USING ERRCODE = '55000';
48
+ END IF;
49
+
50
+ IF OLD.liveness = 'draining'
51
+ AND NEW.liveness = 'cold'
52
+ AND OLD.archive_capture_id IS NOT NULL
53
+ AND NULLIF(
54
+ pg_catalog.current_setting('opengeni.sandbox_drain_capture_id', true),
55
+ ''
56
+ ) IS DISTINCT FROM OLD.archive_capture_id::text
57
+ THEN
58
+ RAISE EXCEPTION 'sandbox drain capture ownership blocks cold commit for lease %', NEW.id
59
+ USING ERRCODE = '55000';
60
+ END IF;
61
+
62
+ IF NEW.archive_capture_id IS NULL THEN
63
+ NEW.archive_capture_operation_id := NULL;
64
+ NEW.archive_capture_provider_request_id := NULL;
65
+ NEW.archive_capture_provider_replay_safe := false;
66
+ NEW.archive_capture_takeover_safe := false;
67
+ NEW.archive_capture_attempt := NULL;
68
+ NEW.archive_capture_published_at := NULL;
69
+ ELSIF NEW.archive_capture_id IS DISTINCT FROM OLD.archive_capture_id THEN
70
+ NEW.archive_capture_published_at := NULL;
71
+ IF NEW.archive_capture_operation_id IS NOT DISTINCT FROM OLD.archive_capture_operation_id
72
+ AND NEW.archive_capture_attempt IS NOT DISTINCT FROM OLD.archive_capture_attempt
73
+ THEN
74
+ -- A pre-0186 writer knows neither takeover contract. Never inherit the
75
+ -- predecessor's permission merely because the new column is invisible to it.
76
+ NEW.archive_capture_operation_id := NEW.archive_capture_id;
77
+ NEW.archive_capture_provider_request_id := NEW.archive_capture_id;
78
+ NEW.archive_capture_provider_replay_safe := false;
79
+ NEW.archive_capture_takeover_safe := false;
80
+ NEW.archive_capture_attempt := 1;
81
+ ELSE
82
+ NEW.archive_capture_operation_id := COALESCE(
83
+ NEW.archive_capture_operation_id,
84
+ NEW.archive_capture_id
85
+ );
86
+ NEW.archive_capture_provider_request_id := COALESCE(
87
+ NEW.archive_capture_provider_request_id,
88
+ NEW.archive_capture_id
89
+ );
90
+ NEW.archive_capture_attempt := COALESCE(NEW.archive_capture_attempt, 1);
91
+ END IF;
92
+ ELSE
93
+ NEW.archive_capture_operation_id := COALESCE(
94
+ NEW.archive_capture_operation_id,
95
+ NEW.archive_capture_id
96
+ );
97
+ NEW.archive_capture_provider_request_id := COALESCE(
98
+ NEW.archive_capture_provider_request_id,
99
+ NEW.archive_capture_id
100
+ );
101
+ NEW.archive_capture_attempt := COALESCE(NEW.archive_capture_attempt, 1);
102
+ END IF;
103
+
104
+ IF NEW.archive_capture_provider_replay_safe
105
+ AND NOT NEW.archive_capture_takeover_safe
106
+ THEN
107
+ RAISE EXCEPTION 'provider-replay-safe capture must be takeover-safe for lease %', NEW.id
108
+ USING ERRCODE = '23514';
109
+ END IF;
110
+
111
+ IF OLD.liveness = 'draining'
112
+ AND NEW.liveness = 'draining'
113
+ AND OLD.archive_capture_id IS NULL
114
+ AND NEW.archive_capture_id IS NOT NULL
115
+ AND NEW.rotation_requested_at IS NULL
116
+ THEN
117
+ NEW.rotation_requested_at := COALESCE(
118
+ NEW.archive_capture_started_at,
119
+ pg_catalog.clock_timestamp()
120
+ );
121
+ NEW.rotation_reason := 'teardown_claim';
122
+ END IF;
123
+ RETURN NEW;
124
+ END
125
+ $function$;
126
+
127
+ DROP TRIGGER IF EXISTS sandbox_leases_drain_teardown_fence ON "sandbox_leases";
128
+ CREATE TRIGGER sandbox_leases_drain_teardown_fence
129
+ BEFORE UPDATE OF "archive_capture_id", "liveness" ON "sandbox_leases"
130
+ FOR EACH ROW
131
+ EXECUTE FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence();
132
+
133
+ ALTER TABLE "sandbox_leases"
134
+ DROP CONSTRAINT IF EXISTS "sandbox_leases_archive_capture_check",
135
+ ADD CONSTRAINT "sandbox_leases_archive_capture_check"
136
+ CHECK (
137
+ (
138
+ "archive_capture_id" IS NULL
139
+ AND "archive_capture_operation_id" IS NULL
140
+ AND "archive_capture_provider_request_id" IS NULL
141
+ AND "archive_capture_provider_replay_safe" = false
142
+ AND "archive_capture_takeover_safe" = false
143
+ AND "archive_capture_attempt" IS NULL
144
+ AND "archive_capture_generation" IS NULL
145
+ AND "archive_capture_started_at" IS NULL
146
+ AND "archive_capture_deadline_at" IS NULL
147
+ AND "archive_capture_published_at" IS NULL
148
+ ) OR (
149
+ "archive_capture_id" IS NOT NULL
150
+ AND "archive_capture_operation_id" IS NOT NULL
151
+ AND "archive_capture_provider_request_id" IS NOT NULL
152
+ AND (
153
+ "archive_capture_provider_replay_safe" = false
154
+ OR "archive_capture_takeover_safe" = true
155
+ )
156
+ AND "archive_capture_attempt" IS NOT NULL
157
+ AND "archive_capture_attempt" > 0
158
+ AND "archive_capture_generation" IS NOT NULL
159
+ AND "archive_capture_generation" = "workspace_generation"
160
+ AND "archive_capture_started_at" IS NOT NULL
161
+ AND "archive_capture_deadline_at" IS NOT NULL
162
+ AND "archive_capture_deadline_at" > "archive_capture_started_at"
163
+ AND (
164
+ "archive_capture_published_at" IS NULL
165
+ OR "liveness" = 'draining'
166
+ )
167
+ )
168
+ ) NOT VALID;
169
+
170
+ ALTER TABLE "sandbox_leases"
171
+ VALIDATE CONSTRAINT "sandbox_leases_archive_capture_check";
172
+
173
+ -- Keep the global DB-owned sweep aligned with the per-workspace implementation.
174
+ -- A live turn heartbeats every 10s. A canonical holder whose authoritative turn
175
+ -- is already closed is dead immediately; every other turn holder gets the normal
176
+ -- lease-TTL crash horizon. Once a capture claim exists, holder-zero dispatch is
177
+ -- immediate because that claim already is the durable admission/teardown fence.
178
+ DO $capture_aware_reaper$
179
+ DECLARE data_schema text := current_schema();
180
+ BEGIN
181
+ EXECUTE format($create$
182
+ CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
183
+ p_viewer_holder_ttl_ms bigint,
184
+ p_turn_holder_ttl_ms bigint,
185
+ p_idle_grace_ms bigint
186
+ )
187
+ RETURNS TABLE (
188
+ workspace_id uuid,
189
+ sandbox_group_id uuid,
190
+ instance_id text,
191
+ lease_epoch integer
192
+ )
193
+ LANGUAGE plpgsql
194
+ SECURITY DEFINER
195
+ SET search_path = pg_catalog
196
+ AS $function$
197
+ DECLARE locked_ids uuid[];
198
+ BEGIN
199
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
200
+
201
+ SELECT coalesce(
202
+ pg_catalog.array_agg(candidate.id),
203
+ ARRAY[]::uuid[]
204
+ )
205
+ INTO locked_ids
206
+ FROM (
207
+ SELECT lease.id
208
+ FROM %1$I.sandbox_leases lease
209
+ WHERE lease.liveness <> 'cold'
210
+ OR EXISTS (
211
+ SELECT 1
212
+ FROM %1$I.sandbox_lease_holders holder
213
+ WHERE holder.lease_id = lease.id
214
+ )
215
+ ORDER BY lease.updated_at, lease.id
216
+ LIMIT 500
217
+ FOR UPDATE OF lease SKIP LOCKED
218
+ ) candidate;
219
+
220
+ IF pg_catalog.cardinality(locked_ids) = 0 THEN
221
+ RETURN;
222
+ END IF;
223
+
224
+ DELETE FROM %1$I.sandbox_lease_holders holder
225
+ USING (
226
+ SELECT stale.id
227
+ FROM %1$I.sandbox_lease_holders stale
228
+ WHERE stale.lease_id = ANY(locked_ids)
229
+ AND stale.kind IN ('viewer', 'direct')
230
+ AND stale.last_heartbeat_at
231
+ < pg_catalog.now()
232
+ - pg_catalog.make_interval(secs => p_viewer_holder_ttl_ms / 1000.0)
233
+ FOR UPDATE OF stale SKIP LOCKED
234
+ ) victim
235
+ WHERE holder.id = victim.id;
236
+
237
+ IF p_turn_holder_ttl_ms > 0 THEN
238
+ DELETE FROM %1$I.sandbox_lease_holders holder
239
+ USING (
240
+ SELECT stale.id
241
+ FROM %1$I.sandbox_lease_holders stale
242
+ WHERE stale.lease_id = ANY(locked_ids)
243
+ AND stale.kind = 'turn'
244
+ AND (
245
+ stale.last_heartbeat_at
246
+ < pg_catalog.now()
247
+ - pg_catalog.make_interval(secs => p_turn_holder_ttl_ms / 1000.0)
248
+ OR (
249
+ 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}$'
250
+ AND NOT EXISTS (
251
+ SELECT 1
252
+ FROM %1$I.session_turn_attempts attempt
253
+ JOIN %1$I.session_turns turn
254
+ ON turn.account_id = attempt.account_id
255
+ AND turn.workspace_id = attempt.workspace_id
256
+ AND turn.session_id = attempt.session_id
257
+ AND turn.id = attempt.turn_id
258
+ AND turn.execution_generation = attempt.execution_generation
259
+ AND turn.active_attempt_id = attempt.id
260
+ WHERE stale.holder_id = ('turn-attempt:' || attempt.id::text)
261
+ AND attempt.account_id = stale.account_id
262
+ AND attempt.workspace_id = stale.workspace_id
263
+ AND attempt.session_id = stale.subject_id
264
+ AND attempt.state IN ('claimed', 'running')
265
+ )
266
+ )
267
+ )
268
+ FOR UPDATE OF stale SKIP LOCKED
269
+ ) victim
270
+ WHERE holder.id = victim.id;
271
+ END IF;
272
+
273
+ UPDATE %1$I.sandbox_leases lease SET
274
+ refcount = counts.total,
275
+ turn_holders = counts.turns,
276
+ viewer_holders = counts.viewers,
277
+ liveness = CASE
278
+ WHEN lease.liveness = 'warm'
279
+ AND counts.total = 0
280
+ AND counts.turns = 0
281
+ AND (
282
+ lease.reaper_hold_id IS NULL
283
+ OR lease.reaper_hold_until <= pg_catalog.now()
284
+ )
285
+ THEN 'draining' ELSE lease.liveness END,
286
+ expires_at = CASE
287
+ WHEN lease.liveness = 'warm'
288
+ AND counts.total = 0
289
+ AND counts.turns = 0
290
+ AND (
291
+ lease.reaper_hold_id IS NULL
292
+ OR lease.reaper_hold_until <= pg_catalog.now()
293
+ )
294
+ THEN CASE
295
+ WHEN lease.rotation_requested_at IS NOT NULL
296
+ OR lease.archive_capture_id IS NOT NULL
297
+ THEN pg_catalog.now() - interval '1 millisecond'
298
+ ELSE pg_catalog.now()
299
+ + pg_catalog.make_interval(secs => p_idle_grace_ms / 1000.0)
300
+ END
301
+ ELSE lease.expires_at END,
302
+ updated_at = pg_catalog.now()
303
+ FROM (
304
+ SELECT candidate.id,
305
+ pg_catalog.count(holder.id)::int AS total,
306
+ pg_catalog.count(holder.id)
307
+ FILTER (WHERE holder.kind = 'turn')::int AS turns,
308
+ pg_catalog.count(holder.id)
309
+ FILTER (WHERE holder.kind = 'viewer')::int AS viewers
310
+ FROM pg_catalog.unnest(locked_ids) candidate(id)
311
+ LEFT JOIN %1$I.sandbox_lease_holders holder
312
+ ON holder.lease_id = candidate.id
313
+ GROUP BY candidate.id
314
+ ) counts
315
+ WHERE lease.id = counts.id;
316
+
317
+ UPDATE %1$I.sandbox_leases lease SET
318
+ liveness = 'cold',
319
+ instance_id = null,
320
+ lease_epoch = lease.lease_epoch + 1,
321
+ reaper_hold_id = null,
322
+ reaper_hold_until = null,
323
+ reaper_hold_reason = null,
324
+ resume_state = opengeni_private.warming_reset_resume_state_v2(
325
+ lease.backend,
326
+ lease.resume_backend_id,
327
+ lease.resume_state,
328
+ lease.workspace_generation,
329
+ lease.archive_generation,
330
+ pg_catalog.clock_timestamp()
331
+ ),
332
+ resume_backend_id = CASE
333
+ WHEN coalesce(
334
+ lease.resume_state #>> '{sessionState,workspaceArchive}', ''
335
+ ) <> ''
336
+ OR coalesce(
337
+ lease.resume_state #>> '{sessionState,workspaceArchivePrev}', ''
338
+ ) <> ''
339
+ THEN coalesce(lease.resume_backend_id, lease.backend)
340
+ ELSE null END,
341
+ data_plane_url = null,
342
+ terminal_data_plane_url = null,
343
+ provider_created_at = null,
344
+ provider_deadline_at = null,
345
+ rotation_requested_at = null,
346
+ rotation_reason = null,
347
+ updated_at = pg_catalog.now()
348
+ WHERE lease.id = ANY(locked_ids)
349
+ AND lease.liveness = 'warming'
350
+ AND lease.expires_at < pg_catalog.now()
351
+ AND lease.instance_id IS NULL
352
+ AND (
353
+ lease.reaper_hold_id IS NULL
354
+ OR lease.reaper_hold_until <= pg_catalog.now()
355
+ );
356
+
357
+ UPDATE %1$I.sandbox_leases lease SET
358
+ liveness = 'draining',
359
+ refcount = 0,
360
+ turn_holders = 0,
361
+ viewer_holders = 0,
362
+ data_plane_url = null,
363
+ terminal_data_plane_url = null,
364
+ lease_epoch = lease.lease_epoch + 1,
365
+ reaper_hold_id = null,
366
+ reaper_hold_until = null,
367
+ reaper_hold_reason = null,
368
+ expires_at = pg_catalog.now() - interval '1 millisecond',
369
+ updated_at = pg_catalog.now()
370
+ WHERE lease.id = ANY(locked_ids)
371
+ AND lease.liveness = 'warming'
372
+ AND lease.expires_at < pg_catalog.now()
373
+ AND lease.instance_id IS NOT NULL
374
+ AND (
375
+ lease.reaper_hold_id IS NULL
376
+ OR lease.reaper_hold_until <= pg_catalog.now()
377
+ );
378
+
379
+ RETURN QUERY
380
+ SELECT lease.workspace_id, lease.sandbox_group_id,
381
+ lease.instance_id, lease.lease_epoch
382
+ FROM %1$I.sandbox_leases lease
383
+ WHERE lease.id = ANY(locked_ids)
384
+ AND lease.liveness = 'draining'
385
+ AND lease.expires_at < pg_catalog.now()
386
+ AND lease.refcount = 0
387
+ AND (
388
+ lease.reaper_hold_id IS NULL
389
+ OR lease.reaper_hold_until <= pg_catalog.now()
390
+ );
391
+ END;
392
+ $function$;
393
+ $create$, data_schema);
394
+ END
395
+ $capture_aware_reaper$;
396
+
397
+ REVOKE ALL ON FUNCTION opengeni_private.stamp_sandbox_drain_teardown_fence() FROM PUBLIC;
398
+
399
+ RESET statement_timeout;
400
+ RESET lock_timeout;