@opengeni/db 3.2.0-canary.0 → 3.4.0-canary.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 (81) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-SIBYBA2D.js → chunk-6DPJA36C.js} +30 -11
  3. package/dist/chunk-6DPJA36C.js.map +1 -0
  4. package/dist/{chunk-KRLPEMLH.js → chunk-C2AFP7SB.js} +184 -6
  5. package/dist/chunk-C2AFP7SB.js.map +1 -0
  6. package/dist/{chunk-AZHW7R5V.js → chunk-H7MXYG7R.js} +12 -8
  7. package/dist/chunk-H7MXYG7R.js.map +1 -0
  8. package/dist/{chunk-2PRS4J5Q.js → chunk-HY3F5Q4E.js} +3 -3
  9. package/dist/{chunk-NAB6FKFN.js → chunk-O6FTELKH.js} +3 -3
  10. package/dist/{chunk-UZNXG44K.js → chunk-PYMY3AJW.js} +324 -8
  11. package/dist/chunk-PYMY3AJW.js.map +1 -0
  12. package/dist/{chunk-6HMM2R5T.js → chunk-TRBOWUG3.js} +98 -4
  13. package/dist/chunk-TRBOWUG3.js.map +1 -0
  14. package/dist/{chunk-E6CLMKEE.js → chunk-V3TRUXFJ.js} +2 -2
  15. package/dist/{chunk-2EH4WRHL.js → chunk-ZWRIPCTR.js} +2 -2
  16. package/dist/editable-artifact-durable-export.js +2 -2
  17. package/dist/editable-artifacts.js +3 -3
  18. package/dist/governed-learning-activation.d.ts +16 -0
  19. package/dist/index.d.ts +108 -7
  20. package/dist/index.js +3605 -1417
  21. package/dist/index.js.map +1 -1
  22. package/dist/insights-model-bundle.d.ts +30 -0
  23. package/dist/insights-usage-bundle.d.ts +32 -0
  24. package/dist/insights.d.ts +10 -2
  25. package/dist/lossless-json.d.ts +1 -1
  26. package/dist/managed-user-setup.d.ts +82 -0
  27. package/dist/memory-domain.d.ts +2 -2
  28. package/dist/migrate.d.ts +3 -1
  29. package/dist/migrate.js +1 -1
  30. package/dist/new-session-drafts.d.ts +4 -4
  31. package/dist/organization-membership-lifecycle.d.ts +60 -1
  32. package/dist/pr-review.d.ts +44 -3
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/runtime-posture.d.ts +12 -7
  35. package/dist/schema.d.ts +839 -101
  36. package/dist/schema.js +9 -1
  37. package/dist/session-background-commands.js +3 -3
  38. package/dist/session-tenancy.d.ts +12 -1
  39. package/dist/session-tenancy.js +3 -3
  40. package/dist/video-generation.js +3 -3
  41. package/drizzle/0348_named_signup_and_user_setup.sql +838 -0
  42. package/drizzle/0349_greenfield_session_tenancy_activation.sql +792 -0
  43. package/drizzle/0350_organization_shared_workspace_administration.sql +889 -0
  44. package/drizzle/0351_organization_user_setup_delivery.sql +978 -0
  45. package/drizzle/0352_session_variable_set_attachments.sql +1661 -0
  46. package/drizzle/0353_automatic_session_title_policy_fence.sql +476 -0
  47. package/drizzle/0354_automatic_session_title_quarantine_index.sql +9 -0
  48. package/drizzle/0355_automatic_session_title_quarantine.sql +92 -0
  49. package/drizzle/0356_set_based_insights_session_visibility.sql +237 -0
  50. package/drizzle/0357_rig_platform_base_only.sql +44 -0
  51. package/drizzle/0358_pr_review_managed_github_app.sql +108 -0
  52. package/drizzle/0359_insights_force_rls_read_capability.sql +936 -0
  53. package/drizzle/0360_organization_identity_confirmation_prompt.sql +103 -0
  54. package/drizzle/0361_remember_knowledge_memory_materialization.sql +249 -0
  55. package/package.json +6 -6
  56. package/src/database.ts +12 -8
  57. package/src/governed-learning-activation.ts +74 -2
  58. package/src/index.ts +879 -92
  59. package/src/insights-model-bundle.ts +681 -0
  60. package/src/insights-usage-bundle.ts +240 -0
  61. package/src/insights.ts +265 -152
  62. package/src/lossless-json.ts +2 -1
  63. package/src/managed-user-setup.ts +316 -0
  64. package/src/memory-domain.ts +3 -3
  65. package/src/migrate.ts +38 -14
  66. package/src/new-session-drafts.ts +22 -15
  67. package/src/organization-membership-lifecycle.ts +253 -1
  68. package/src/pr-review.ts +449 -6
  69. package/src/provision-roles.ts +92 -0
  70. package/src/runtime-posture.ts +247 -4
  71. package/src/schema.ts +210 -4
  72. package/src/session-tenancy.ts +120 -2
  73. package/dist/chunk-6HMM2R5T.js.map +0 -1
  74. package/dist/chunk-AZHW7R5V.js.map +0 -1
  75. package/dist/chunk-KRLPEMLH.js.map +0 -1
  76. package/dist/chunk-SIBYBA2D.js.map +0 -1
  77. package/dist/chunk-UZNXG44K.js.map +0 -1
  78. /package/dist/{chunk-2PRS4J5Q.js.map → chunk-HY3F5Q4E.js.map} +0 -0
  79. /package/dist/{chunk-NAB6FKFN.js.map → chunk-O6FTELKH.js.map} +0 -0
  80. /package/dist/{chunk-E6CLMKEE.js.map → chunk-V3TRUXFJ.js.map} +0 -0
  81. /package/dist/{chunk-2EH4WRHL.js.map → chunk-ZWRIPCTR.js.map} +0 -0
@@ -0,0 +1,1661 @@
1
+ -- deployment-mode: maintenance
2
+ -- Activate ordered, FK-backed session Variable Set attachments. Runtime
3
+ -- injection now consumes the complete ordered selection, so every API/control/
4
+ -- turn worker must be stopped before this cutover and no pre-0352 image may
5
+ -- restart afterwards.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '10min';
9
+
10
+ DO $session_variable_set_writer_drain_before_lock$
11
+ DECLARE
12
+ configured_roles_text text := nullif(
13
+ current_setting('opengeni.migration_application_roles', true), ''
14
+ );
15
+ configured_roles jsonb;
16
+ BEGIN
17
+ IF configured_roles_text IS NULL THEN
18
+ RAISE EXCEPTION
19
+ '0352 session Variable Set attachments require an explicit application database role list'
20
+ USING ERRCODE = '55000';
21
+ END IF;
22
+ BEGIN
23
+ configured_roles := configured_roles_text::jsonb;
24
+ EXCEPTION WHEN OTHERS THEN
25
+ RAISE EXCEPTION
26
+ '0352 session Variable Set attachments received a malformed application database role list'
27
+ USING ERRCODE = '55000';
28
+ END;
29
+ IF jsonb_typeof(configured_roles) <> 'array'
30
+ OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
31
+ OR EXISTS (
32
+ SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
33
+ WHERE jsonb_typeof(value) <> 'string'
34
+ OR btrim(value #>> '{}') = ''
35
+ OR octet_length(value #>> '{}') > 63
36
+ )
37
+ OR (
38
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
39
+ ) <> (
40
+ SELECT count(DISTINCT value)
41
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
42
+ )
43
+ THEN
44
+ RAISE EXCEPTION
45
+ '0352 session Variable Set attachments received an invalid application database role list'
46
+ USING ERRCODE = '55000';
47
+ END IF;
48
+ IF EXISTS (
49
+ SELECT 1 FROM pg_stat_activity activity
50
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
51
+ ON roles.role_name = activity.usename
52
+ WHERE activity.datname = current_database()
53
+ AND activity.pid <> pg_backend_pid()
54
+ )
55
+ THEN
56
+ RAISE EXCEPTION
57
+ '0352 session Variable Set attachments require all configured OpenGeni application database sessions to be stopped'
58
+ USING ERRCODE = '55000';
59
+ END IF;
60
+ END
61
+ $session_variable_set_writer_drain_before_lock$;
62
+
63
+ LOCK TABLE sessions IN ACCESS EXCLUSIVE MODE;
64
+ LOCK TABLE workspace_variable_sets IN ACCESS EXCLUSIVE MODE;
65
+ LOCK TABLE sandbox_leases IN ACCESS EXCLUSIVE MODE;
66
+ LOCK TABLE turn_personal_resource_attachment_receipts IN ACCESS EXCLUSIVE MODE;
67
+ LOCK TABLE turn_personal_resource_snapshots IN ACCESS EXCLUSIVE MODE;
68
+ LOCK TABLE rig_versions IN ACCESS EXCLUSIVE MODE;
69
+ LOCK TABLE session_events IN ACCESS EXCLUSIVE MODE;
70
+ LOCK TABLE audit_events IN ACCESS EXCLUSIVE MODE;
71
+ LOCK TABLE workspace_session_activity_revisions IN ACCESS EXCLUSIVE MODE;
72
+ LOCK TABLE session_command_receipts IN ACCESS EXCLUSIVE MODE;
73
+
74
+ ALTER TABLE sessions
75
+ ADD COLUMN variable_set_ids jsonb NOT NULL DEFAULT '[]'::jsonb;
76
+
77
+ ALTER TABLE sessions NO FORCE ROW LEVEL SECURITY;
78
+ DO $session_variable_set_backfill_workspace_fences$
79
+ DECLARE
80
+ workspace_id_value uuid;
81
+ BEGIN
82
+ FOR workspace_id_value IN
83
+ SELECT DISTINCT session_value.workspace_id
84
+ FROM sessions session_value
85
+ ORDER BY session_value.workspace_id
86
+ LOOP
87
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
88
+ END LOOP;
89
+ END
90
+ $session_variable_set_backfill_workspace_fences$;
91
+ UPDATE sessions
92
+ SET variable_set_ids = CASE
93
+ WHEN variable_set_id IS NULL THEN '[]'::jsonb
94
+ ELSE jsonb_build_array(variable_set_id::text)
95
+ END;
96
+ ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
97
+
98
+ -- A maintenance drain prevents a mixed fleet during the DDL, while this
99
+ -- restrictive policy prevents an old API/control/turn worker from rejoining
100
+ -- afterwards. Current standalone connections use the PgBouncer-supported
101
+ -- application_name startup receipt; injected/embedded handles stamp the
102
+ -- transaction-local GUC in setRlsContext. Pre-0352 binaries carry neither.
103
+ CREATE FUNCTION opengeni_private.session_variable_set_attachments_protocol_v1_active()
104
+ RETURNS boolean
105
+ LANGUAGE plpgsql
106
+ STABLE
107
+ SET search_path = pg_catalog
108
+ AS $function$
109
+ BEGIN
110
+ IF current_setting('opengeni.session_variable_set_attachments_v1', true) = '1'
111
+ OR current_setting('application_name', true) =
112
+ 'opengeni-lossless-v1-session-variable-sets-v1'
113
+ THEN
114
+ RETURN true;
115
+ END IF;
116
+ RAISE EXCEPTION
117
+ 'session Variable Set attachments require an OpenGeni 0352-or-newer runtime'
118
+ USING ERRCODE = '55000';
119
+ END
120
+ $function$;
121
+
122
+ REVOKE ALL ON FUNCTION
123
+ opengeni_private.session_variable_set_attachments_protocol_v1_active()
124
+ FROM PUBLIC;
125
+
126
+ DO $session_variable_set_protocol_grants$
127
+ DECLARE
128
+ configured_role text;
129
+ BEGIN
130
+ FOR configured_role IN
131
+ SELECT role_value.rolname
132
+ FROM jsonb_array_elements_text(
133
+ current_setting('opengeni.migration_application_roles')::jsonb
134
+ ) configured(value)
135
+ JOIN pg_catalog.pg_roles role_value
136
+ ON role_value.rolname = configured.value
137
+ LOOP
138
+ EXECUTE format(
139
+ 'GRANT EXECUTE ON FUNCTION opengeni_private.session_variable_set_attachments_protocol_v1_active() TO %I',
140
+ configured_role
141
+ );
142
+ END LOOP;
143
+ END
144
+ $session_variable_set_protocol_grants$;
145
+
146
+ SELECT set_config('opengeni.session_variable_set_attachments_v1', '1', true);
147
+
148
+ -- Migration 0176 admitted the then-current createDb application_name as a
149
+ -- lossless-content writer. The 0352 startup receipt rotates that name, so keep
150
+ -- both generations lossless-aware while the sessions protocol gate separately
151
+ -- rejects the old generation.
152
+ CREATE OR REPLACE FUNCTION opengeni_private.fence_legacy_lossless_content_update()
153
+ RETURNS trigger
154
+ LANGUAGE plpgsql
155
+ SET search_path = pg_catalog
156
+ AS $function$
157
+ BEGIN
158
+ IF current_setting('opengeni.lossless_content_writer', true) = '1'
159
+ OR current_setting('application_name', true) IN (
160
+ 'opengeni-lossless-v1',
161
+ 'opengeni-lossless-v1-session-variable-sets-v1'
162
+ )
163
+ THEN
164
+ RETURN NEW;
165
+ END IF;
166
+
167
+ CASE TG_TABLE_NAME
168
+ WHEN 'sessions' THEN
169
+ IF NEW.initial_message IS DISTINCT FROM OLD.initial_message
170
+ AND NEW.initial_message_codec_version IS NOT DISTINCT FROM OLD.initial_message_codec_version THEN
171
+ NEW.initial_message_codec_version := NULL;
172
+ END IF;
173
+ WHEN 'session_realtime_entries' THEN
174
+ IF NEW.text IS DISTINCT FROM OLD.text
175
+ AND NEW.text_codec_version IS NOT DISTINCT FROM OLD.text_codec_version THEN
176
+ NEW.text_codec_version := NULL;
177
+ END IF;
178
+ IF NEW.payload IS DISTINCT FROM OLD.payload
179
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
180
+ NEW.payload_codec_version := NULL;
181
+ END IF;
182
+ WHEN 'knowledge_memories' THEN
183
+ IF NEW.text IS DISTINCT FROM OLD.text
184
+ AND NEW.text_codec_version IS NOT DISTINCT FROM OLD.text_codec_version THEN
185
+ NEW.text_codec_version := NULL;
186
+ END IF;
187
+ WHEN 'session_turns' THEN
188
+ IF NEW.prompt IS DISTINCT FROM OLD.prompt
189
+ AND NEW.prompt_codec_version IS NOT DISTINCT FROM OLD.prompt_codec_version THEN
190
+ NEW.prompt_codec_version := NULL;
191
+ END IF;
192
+ WHEN 'session_system_updates' THEN
193
+ IF NEW.summary IS DISTINCT FROM OLD.summary
194
+ AND NEW.summary_codec_version IS NOT DISTINCT FROM OLD.summary_codec_version THEN
195
+ NEW.summary_codec_version := NULL;
196
+ END IF;
197
+ IF NEW.payload IS DISTINCT FROM OLD.payload
198
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
199
+ NEW.payload_codec_version := NULL;
200
+ END IF;
201
+ WHEN 'session_system_update_outbox' THEN
202
+ IF NEW.summary IS DISTINCT FROM OLD.summary
203
+ AND NEW.summary_codec_version IS NOT DISTINCT FROM OLD.summary_codec_version THEN
204
+ NEW.summary_codec_version := NULL;
205
+ END IF;
206
+ IF NEW.payload IS DISTINCT FROM OLD.payload
207
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
208
+ NEW.payload_codec_version := NULL;
209
+ END IF;
210
+ WHEN 'session_events' THEN
211
+ IF NEW.payload IS DISTINCT FROM OLD.payload
212
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
213
+ NEW.payload_codec_version := NULL;
214
+ END IF;
215
+ WHEN 'host_export_outbox' THEN
216
+ IF NEW.payload IS DISTINCT FROM OLD.payload
217
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
218
+ NEW.payload_codec_version := NULL;
219
+ END IF;
220
+ WHEN 'host_export_consumers' THEN
221
+ IF NEW.last_error IS DISTINCT FROM OLD.last_error
222
+ AND NEW.last_error_codec_version IS NOT DISTINCT FROM OLD.last_error_codec_version THEN
223
+ NEW.last_error_codec_version := NULL;
224
+ END IF;
225
+ WHEN 'agent_run_states' THEN
226
+ IF NEW.serialized_run_state IS DISTINCT FROM OLD.serialized_run_state
227
+ AND NEW.serialized_run_state_codec_version IS NOT DISTINCT FROM OLD.serialized_run_state_codec_version THEN
228
+ NEW.serialized_run_state_codec_version := NULL;
229
+ END IF;
230
+ IF NEW.pending_approvals IS DISTINCT FROM OLD.pending_approvals
231
+ AND NEW.pending_approvals_codec_version IS NOT DISTINCT FROM OLD.pending_approvals_codec_version THEN
232
+ NEW.pending_approvals_codec_version := NULL;
233
+ END IF;
234
+ WHEN 'session_history_items' THEN
235
+ IF NEW.item IS DISTINCT FROM OLD.item
236
+ AND NEW.item_codec_version IS NOT DISTINCT FROM OLD.item_codec_version THEN
237
+ NEW.item_codec_version := NULL;
238
+ END IF;
239
+ WHEN 'session_pending_tool_calls' THEN
240
+ IF NEW.call_item IS DISTINCT FROM OLD.call_item
241
+ AND NEW.call_item_codec_version IS NOT DISTINCT FROM OLD.call_item_codec_version THEN
242
+ NEW.call_item_codec_version := NULL;
243
+ END IF;
244
+ IF NEW.result_item IS DISTINCT FROM OLD.result_item
245
+ AND NEW.result_item_codec_version IS NOT DISTINCT FROM OLD.result_item_codec_version THEN
246
+ NEW.result_item_codec_version := NULL;
247
+ END IF;
248
+ WHEN 'sandbox_session_envelopes' THEN
249
+ IF NEW.envelope IS DISTINCT FROM OLD.envelope
250
+ AND NEW.envelope_codec_version IS NOT DISTINCT FROM OLD.envelope_codec_version THEN
251
+ NEW.envelope_codec_version := NULL;
252
+ END IF;
253
+ WHEN 'session_recordings' THEN
254
+ IF NEW.reason IS DISTINCT FROM OLD.reason
255
+ AND NEW.reason_codec_version IS NOT DISTINCT FROM OLD.reason_codec_version THEN
256
+ NEW.reason_codec_version := NULL;
257
+ END IF;
258
+ WHEN 'host_export_dead_letters' THEN
259
+ IF NEW.envelope IS DISTINCT FROM OLD.envelope
260
+ AND NEW.envelope_codec_version IS NOT DISTINCT FROM OLD.envelope_codec_version THEN
261
+ NEW.envelope_codec_version := NULL;
262
+ END IF;
263
+ IF NEW.envelope #> '{event,payload}' IS DISTINCT FROM OLD.envelope #> '{event,payload}'
264
+ AND NEW.event_payload_codec_version IS NOT DISTINCT FROM OLD.event_payload_codec_version THEN
265
+ NEW.event_payload_codec_version := NULL;
266
+ END IF;
267
+ WHEN 'audit_events' THEN
268
+ IF NEW.metadata IS DISTINCT FROM OLD.metadata
269
+ AND NEW.metadata_codec_version IS NOT DISTINCT FROM OLD.metadata_codec_version THEN
270
+ NEW.metadata_codec_version := NULL;
271
+ END IF;
272
+ WHEN 'rig_changes' THEN
273
+ IF NEW.payload IS DISTINCT FROM OLD.payload
274
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
275
+ NEW.payload_codec_version := NULL;
276
+ END IF;
277
+ IF NEW.verification IS DISTINCT FROM OLD.verification
278
+ AND NEW.verification_codec_version IS NOT DISTINCT FROM OLD.verification_codec_version THEN
279
+ NEW.verification_codec_version := NULL;
280
+ END IF;
281
+ WHEN 'transcription_recordings' THEN
282
+ IF NEW.transcript_text IS DISTINCT FROM OLD.transcript_text
283
+ AND NEW.transcript_text_codec_version IS NOT DISTINCT FROM OLD.transcript_text_codec_version THEN
284
+ NEW.transcript_text_codec_version := NULL;
285
+ END IF;
286
+ WHEN 'transcription_recording_segments' THEN
287
+ IF NEW.transcript_text IS DISTINCT FROM OLD.transcript_text
288
+ AND NEW.transcript_text_codec_version IS NOT DISTINCT FROM OLD.transcript_text_codec_version THEN
289
+ NEW.transcript_text_codec_version := NULL;
290
+ END IF;
291
+ ELSE
292
+ RAISE EXCEPTION 'unsupported lossless-content fence table: %', TG_TABLE_NAME;
293
+ END CASE;
294
+ RETURN NEW;
295
+ END
296
+ $function$;
297
+
298
+ CREATE POLICY sessions_variable_set_attachments_protocol_v1
299
+ ON sessions
300
+ AS RESTRICTIVE
301
+ FOR ALL
302
+ TO PUBLIC
303
+ USING (opengeni_private.session_variable_set_attachments_protocol_v1_active())
304
+ WITH CHECK (opengeni_private.session_variable_set_attachments_protocol_v1_active());
305
+
306
+ CREATE TABLE session_variable_set_attachments (
307
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
308
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
309
+ workspace_id uuid NOT NULL,
310
+ session_id uuid NOT NULL,
311
+ variable_set_id uuid NOT NULL REFERENCES workspace_variable_sets(id) ON DELETE CASCADE,
312
+ position integer NOT NULL,
313
+ session_status text NOT NULL,
314
+ created_at timestamptz NOT NULL DEFAULT now(),
315
+ CONSTRAINT session_variable_set_attachments_workspace_account_fk
316
+ FOREIGN KEY (workspace_id, account_id)
317
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
318
+ CONSTRAINT session_variable_set_attachments_session_fk
319
+ FOREIGN KEY (workspace_id, session_id)
320
+ REFERENCES sessions(workspace_id, id) ON DELETE CASCADE,
321
+ CONSTRAINT session_variable_set_attachments_position_check
322
+ CHECK (position >= 0 AND position < 25),
323
+ CONSTRAINT session_variable_set_attachments_status_check
324
+ CHECK (session_status IN (
325
+ 'queued', 'running', 'idle', 'requires_action', 'recovering',
326
+ 'waiting_capacity', 'failed', 'cancelled'
327
+ )),
328
+ CONSTRAINT session_variable_set_attachments_session_position_uq
329
+ UNIQUE (workspace_id, session_id, position),
330
+ CONSTRAINT session_variable_set_attachments_session_set_uq
331
+ UNIQUE (workspace_id, session_id, variable_set_id)
332
+ );
333
+
334
+ CREATE INDEX session_variable_set_attachments_set_sessions_idx
335
+ ON session_variable_set_attachments (
336
+ variable_set_id, session_status, workspace_id, session_id
337
+ );
338
+
339
+ INSERT INTO session_variable_set_attachments (
340
+ account_id, workspace_id, session_id, variable_set_id, position, session_status
341
+ )
342
+ SELECT account_id, workspace_id, id, variable_set_id, 0, status
343
+ FROM sessions
344
+ WHERE variable_set_id IS NOT NULL;
345
+
346
+ CREATE OR REPLACE FUNCTION normalize_session_variable_set_selection()
347
+ RETURNS trigger
348
+ LANGUAGE plpgsql
349
+ AS $body$
350
+ DECLARE
351
+ selected_count integer;
352
+ distinct_count integer;
353
+ last_id uuid;
354
+ BEGIN
355
+ IF TG_OP = 'INSERT'
356
+ AND NEW.variable_set_ids = '[]'::jsonb
357
+ AND NEW.variable_set_id IS NOT NULL
358
+ THEN
359
+ NEW.variable_set_ids := jsonb_build_array(NEW.variable_set_id::text);
360
+ ELSIF TG_OP = 'UPDATE'
361
+ AND NEW.variable_set_ids IS NOT DISTINCT FROM OLD.variable_set_ids
362
+ AND NEW.variable_set_id IS DISTINCT FROM OLD.variable_set_id
363
+ THEN
364
+ NEW.variable_set_ids := CASE
365
+ WHEN NEW.variable_set_id IS NULL THEN '[]'::jsonb
366
+ ELSE jsonb_build_array(NEW.variable_set_id::text)
367
+ END;
368
+ END IF;
369
+
370
+ IF jsonb_typeof(NEW.variable_set_ids) <> 'array' THEN
371
+ RAISE EXCEPTION 'variable_set_ids must be a JSON array' USING ERRCODE = '23514';
372
+ END IF;
373
+ selected_count := jsonb_array_length(NEW.variable_set_ids);
374
+ IF selected_count > 25 THEN
375
+ RAISE EXCEPTION 'variable_set_ids exceeds 25 entries' USING ERRCODE = '23514';
376
+ END IF;
377
+ IF EXISTS (
378
+ SELECT 1
379
+ FROM jsonb_array_elements(NEW.variable_set_ids) item(value)
380
+ WHERE jsonb_typeof(item.value) <> 'string'
381
+ OR (item.value #>> '{}') !~*
382
+ '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
383
+ ) THEN
384
+ RAISE EXCEPTION 'variable_set_ids must contain UUID strings' USING ERRCODE = '23514';
385
+ END IF;
386
+ SELECT count(DISTINCT value) INTO distinct_count
387
+ FROM jsonb_array_elements_text(NEW.variable_set_ids) selected(value);
388
+ IF distinct_count <> selected_count THEN
389
+ RAISE EXCEPTION 'variable_set_ids must not contain duplicates' USING ERRCODE = '23514';
390
+ END IF;
391
+ SELECT value::uuid INTO last_id
392
+ FROM jsonb_array_elements_text(NEW.variable_set_ids) WITH ORDINALITY selected(value, position)
393
+ ORDER BY position DESC
394
+ LIMIT 1;
395
+ IF NEW.variable_set_id IS DISTINCT FROM last_id THEN
396
+ RAISE EXCEPTION 'variable_set_id must match the last variable_set_ids entry'
397
+ USING ERRCODE = '23514';
398
+ END IF;
399
+ RETURN NEW;
400
+ END
401
+ $body$;
402
+
403
+ CREATE OR REPLACE FUNCTION sync_session_variable_set_attachments()
404
+ RETURNS trigger
405
+ LANGUAGE plpgsql
406
+ SECURITY DEFINER
407
+ SET search_path = pg_catalog
408
+ AS $body$
409
+ BEGIN
410
+ IF pg_trigger_depth() > 1 THEN
411
+ RETURN NEW;
412
+ END IF;
413
+ EXECUTE pg_catalog.format(
414
+ 'DELETE FROM %I.session_variable_set_attachments
415
+ WHERE workspace_id = $1 AND session_id = $2',
416
+ TG_TABLE_SCHEMA
417
+ ) USING NEW.workspace_id, NEW.id;
418
+ EXECUTE pg_catalog.format(
419
+ 'INSERT INTO %I.session_variable_set_attachments (
420
+ account_id, workspace_id, session_id, variable_set_id, position, session_status
421
+ )
422
+ SELECT $1, $2, $3, selected.value::uuid,
423
+ selected.position::integer - 1, $5
424
+ FROM pg_catalog.jsonb_array_elements_text($4)
425
+ WITH ORDINALITY selected(value, position)',
426
+ TG_TABLE_SCHEMA
427
+ ) USING NEW.account_id, NEW.workspace_id, NEW.id, NEW.variable_set_ids, NEW.status;
428
+ RETURN NEW;
429
+ END
430
+ $body$;
431
+
432
+ CREATE OR REPLACE FUNCTION sync_session_variable_set_attachment_status()
433
+ RETURNS trigger
434
+ LANGUAGE plpgsql
435
+ SECURITY DEFINER
436
+ SET search_path = pg_catalog
437
+ AS $body$
438
+ BEGIN
439
+ EXECUTE pg_catalog.format(
440
+ 'UPDATE %I.session_variable_set_attachments
441
+ SET session_status = $1
442
+ WHERE workspace_id = $2 AND session_id = $3
443
+ AND session_status IS DISTINCT FROM $1',
444
+ TG_TABLE_SCHEMA
445
+ ) USING NEW.status, NEW.workspace_id, NEW.id;
446
+ RETURN NEW;
447
+ END
448
+ $body$;
449
+
450
+ CREATE OR REPLACE FUNCTION guard_variable_set_session_attachments()
451
+ RETURNS trigger
452
+ LANGUAGE plpgsql
453
+ SECURITY DEFINER
454
+ SET search_path = pg_catalog
455
+ AS $body$
456
+ DECLARE
457
+ attachment_count integer := 0;
458
+ previous_account text := current_setting('opengeni.account_id', true);
459
+ previous_workspace text := current_setting('opengeni.workspace_id', true);
460
+ previous_subject text := current_setting('opengeni.subject_id', true);
461
+ BEGIN
462
+ PERFORM set_config('opengeni.account_id', OLD.account_id::text, true);
463
+ PERFORM set_config('opengeni.workspace_id', '', true);
464
+ PERFORM set_config('opengeni.subject_id', '', true);
465
+ EXECUTE format(
466
+ 'SELECT count(*)::integer
467
+ FROM %I.session_variable_set_attachments
468
+ WHERE variable_set_id = $1',
469
+ TG_TABLE_SCHEMA
470
+ ) INTO attachment_count USING OLD.id;
471
+ IF attachment_count > 0 THEN
472
+ RAISE EXCEPTION 'variable set remains attached to % sessions', attachment_count
473
+ USING ERRCODE = '23503';
474
+ END IF;
475
+ PERFORM set_config('opengeni.account_id', coalesce(previous_account, ''), true);
476
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace, ''), true);
477
+ PERFORM set_config('opengeni.subject_id', coalesce(previous_subject, ''), true);
478
+ RETURN OLD;
479
+ EXCEPTION WHEN OTHERS THEN
480
+ PERFORM set_config('opengeni.account_id', coalesce(previous_account, ''), true);
481
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace, ''), true);
482
+ PERFORM set_config('opengeni.subject_id', coalesce(previous_subject, ''), true);
483
+ RAISE;
484
+ END
485
+ $body$;
486
+
487
+ CREATE TRIGGER sessions_variable_set_selection_normalize
488
+ BEFORE INSERT OR UPDATE OF variable_set_ids, variable_set_id ON sessions
489
+ FOR EACH ROW EXECUTE FUNCTION normalize_session_variable_set_selection();
490
+
491
+ CREATE TRIGGER sessions_variable_set_attachments_sync
492
+ AFTER INSERT OR UPDATE OF variable_set_ids, variable_set_id ON sessions
493
+ FOR EACH ROW EXECUTE FUNCTION sync_session_variable_set_attachments();
494
+
495
+ CREATE TRIGGER sessions_variable_set_attachment_status_sync
496
+ AFTER UPDATE OF status ON sessions
497
+ FOR EACH ROW EXECUTE FUNCTION sync_session_variable_set_attachment_status();
498
+
499
+ CREATE TRIGGER workspace_variable_sets_session_attachment_guard
500
+ BEFORE DELETE ON workspace_variable_sets
501
+ FOR EACH ROW EXECUTE FUNCTION guard_variable_set_session_attachments();
502
+
503
+ REVOKE ALL ON FUNCTION normalize_session_variable_set_selection() FROM PUBLIC;
504
+ REVOKE ALL ON FUNCTION sync_session_variable_set_attachments() FROM PUBLIC;
505
+ REVOKE ALL ON FUNCTION sync_session_variable_set_attachment_status() FROM PUBLIC;
506
+ REVOKE ALL ON FUNCTION guard_variable_set_session_attachments() FROM PUBLIC;
507
+
508
+ ALTER TABLE session_variable_set_attachments ENABLE ROW LEVEL SECURITY;
509
+ ALTER TABLE session_variable_set_attachments FORCE ROW LEVEL SECURITY;
510
+ CREATE POLICY workspace_isolation ON session_variable_set_attachments
511
+ USING (
512
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
513
+ OR opengeni_private.variable_set_authority_capability_active('write')
514
+ )
515
+ WITH CHECK (
516
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
517
+ OR opengeni_private.variable_set_authority_capability_active('write')
518
+ );
519
+ CREATE POLICY session_visibility_isolation
520
+ ON session_variable_set_attachments AS RESTRICTIVE
521
+ USING (
522
+ session_reference_visible(account_id, workspace_id, session_id)
523
+ OR opengeni_private.variable_set_authority_capability_active('write')
524
+ )
525
+ WITH CHECK (
526
+ session_reference_visible(account_id, workspace_id, session_id)
527
+ OR opengeni_private.variable_set_authority_capability_active('write')
528
+ );
529
+
530
+ -- Retention finalization is the one lifecycle that must delete a departed
531
+ -- member's personal Variable Sets even when their already-revoked sessions
532
+ -- still retain an ordered selection. Ordinary deletion remains guarded above.
533
+ -- This private helper is callable only from the claimed retention finalizer;
534
+ -- it admits target-owned private or unowned workspace-shared sessions, refuses
535
+ -- another member's private/non-quiescent sessions, preserves survivor order,
536
+ -- rotates any warm quiescent sandbox group, and commits the selection,
537
+ -- projection, event, audit, and activity-clock writes as one transaction.
538
+ CREATE FUNCTION opengeni_private.detach_retention_variable_set_session_selections(
539
+ p_account_id uuid,
540
+ p_membership_id uuid,
541
+ p_operation_id uuid
542
+ ) RETURNS integer
543
+ LANGUAGE plpgsql
544
+ SECURITY DEFINER
545
+ SET search_path FROM CURRENT
546
+ AS $body$
547
+ DECLARE
548
+ workspace_id_value uuid;
549
+ session_row sessions%ROWTYPE;
550
+ group_member_id uuid;
551
+ previous_ids jsonb;
552
+ next_ids jsonb;
553
+ removed_ids jsonb;
554
+ next_last_id uuid;
555
+ affected_session_ids uuid[];
556
+ activity_revision_value bigint;
557
+ detached_sessions integer := 0;
558
+ previous_workspace text := current_setting('opengeni.workspace_id', true);
559
+ previous_gate_state text := current_setting('opengeni.session_activity_gate_state', true);
560
+ previous_gate_workspace text := current_setting(
561
+ 'opengeni.session_activity_gate_workspace_id', true
562
+ );
563
+ finalized_session_count integer;
564
+ write_capability_preexisting boolean;
565
+ fenced_access_capability_id uuid;
566
+ BEGIN
567
+ IF p_account_id IS NULL OR p_membership_id IS NULL OR p_operation_id IS NULL
568
+ OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
569
+ OR current_setting('opengeni.organization_tenancy_lifecycle', true)
570
+ IS DISTINCT FROM 'organization_membership_lifecycle'
571
+ OR nullif(previous_gate_state, '') IS NOT NULL
572
+ OR nullif(previous_gate_workspace, '') IS NOT NULL
573
+ THEN
574
+ RAISE EXCEPTION 'retention Variable Set detach authority is invalid'
575
+ USING ERRCODE = '42501';
576
+ END IF;
577
+ IF NOT EXISTS (
578
+ SELECT 1
579
+ FROM organization_memberships membership
580
+ JOIN organization_user_retention_deletions deletion
581
+ ON deletion.account_id = membership.account_id
582
+ AND deletion.membership_id = membership.id
583
+ WHERE membership.account_id = p_account_id
584
+ AND membership.id = p_membership_id
585
+ AND membership.status = 'revoked'
586
+ AND membership.personal_retention_until IS NOT NULL
587
+ AND membership.personal_retention_until <= clock_timestamp()
588
+ AND deletion.state = 'claimed'
589
+ AND deletion.claim_operation_id = p_operation_id
590
+ AND deletion.claim_expires_at > clock_timestamp()
591
+ AND deletion.database_finalized_at IS NULL
592
+ ) THEN
593
+ RAISE EXCEPTION 'retention Variable Set detach claim is stale'
594
+ USING ERRCODE = '40001';
595
+ END IF;
596
+ SELECT opengeni_private.variable_set_authority_capability_active('write')
597
+ INTO write_capability_preexisting;
598
+ INSERT INTO opengeni_private.variable_set_authority_capabilities (
599
+ backend_pid, transaction_id, capability_kind
600
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'write')
601
+ ON CONFLICT DO NOTHING;
602
+ fenced_access_capability_id :=
603
+ opengeni_private.open_session_tenancy_fenced_access(
604
+ session_tenancy_fence_target_schema()
605
+ );
606
+
607
+ FOR workspace_id_value IN
608
+ SELECT DISTINCT session_value.workspace_id
609
+ FROM sessions session_value
610
+ WHERE session_value.account_id = p_account_id
611
+ AND EXISTS (
612
+ SELECT 1
613
+ FROM session_variable_set_attachments attachment
614
+ JOIN workspace_variable_sets variable_set
615
+ ON variable_set.account_id = p_account_id
616
+ AND variable_set.id = attachment.variable_set_id
617
+ JOIN organization_user_resource_authorities authority
618
+ ON authority.account_id = variable_set.account_id
619
+ AND authority.id = variable_set.authority_id
620
+ AND authority.organization_membership_id = p_membership_id
621
+ AND authority.resource_kind = 'variable_set'
622
+ AND authority.resource_id = variable_set.id
623
+ WHERE attachment.account_id = p_account_id
624
+ AND attachment.workspace_id = session_value.workspace_id
625
+ AND attachment.session_id = session_value.id
626
+ AND variable_set.owner_organization_membership_id = p_membership_id
627
+ )
628
+ ORDER BY session_value.workspace_id
629
+ LOOP
630
+ PERFORM set_config('opengeni.workspace_id', workspace_id_value::text, true);
631
+ PERFORM 1 FROM workspace_inference_controls control
632
+ WHERE control.account_id = p_account_id
633
+ AND control.workspace_id = workspace_id_value
634
+ FOR SHARE;
635
+ PERFORM 1 FROM workspaces workspace
636
+ WHERE workspace.account_id = p_account_id
637
+ AND workspace.id = workspace_id_value
638
+ FOR KEY SHARE;
639
+ IF NOT FOUND THEN
640
+ RAISE EXCEPTION 'retention Variable Set detach workspace is unavailable'
641
+ USING ERRCODE = '40001';
642
+ END IF;
643
+
644
+ affected_session_ids := ARRAY[]::uuid[];
645
+ FOR session_row IN
646
+ SELECT session_value.*
647
+ FROM sessions session_value
648
+ WHERE session_value.account_id = p_account_id
649
+ AND session_value.workspace_id = workspace_id_value
650
+ AND EXISTS (
651
+ SELECT 1
652
+ FROM session_variable_set_attachments attachment
653
+ JOIN workspace_variable_sets variable_set
654
+ ON variable_set.account_id = p_account_id
655
+ AND variable_set.id = attachment.variable_set_id
656
+ JOIN organization_user_resource_authorities authority
657
+ ON authority.account_id = variable_set.account_id
658
+ AND authority.id = variable_set.authority_id
659
+ AND authority.organization_membership_id = p_membership_id
660
+ AND authority.resource_kind = 'variable_set'
661
+ AND authority.resource_id = variable_set.id
662
+ WHERE attachment.account_id = p_account_id
663
+ AND attachment.workspace_id = session_value.workspace_id
664
+ AND attachment.session_id = session_value.id
665
+ AND variable_set.owner_organization_membership_id = p_membership_id
666
+ )
667
+ ORDER BY session_value.id
668
+ FOR NO KEY UPDATE
669
+ LOOP
670
+ IF (
671
+ session_row.owner_organization_membership_id IS NOT NULL
672
+ AND session_row.owner_organization_membership_id IS DISTINCT FROM p_membership_id
673
+ )
674
+ OR session_row.active_turn_id IS NOT NULL
675
+ OR session_row.status NOT IN ('queued', 'idle', 'failed', 'cancelled')
676
+ THEN
677
+ RAISE EXCEPTION 'retention Variable Set detach requires a quiescent eligible session'
678
+ USING ERRCODE = '55000';
679
+ END IF;
680
+ PERFORM assert_session_tenancy_quiescent(
681
+ p_account_id, workspace_id_value, session_row.id, false
682
+ );
683
+ IF EXISTS (
684
+ SELECT 1 FROM sessions group_member
685
+ WHERE group_member.account_id = p_account_id
686
+ AND group_member.workspace_id = workspace_id_value
687
+ AND group_member.sandbox_group_id = session_row.sandbox_group_id
688
+ AND (
689
+ (
690
+ group_member.owner_organization_membership_id IS NOT NULL
691
+ AND group_member.owner_organization_membership_id
692
+ IS DISTINCT FROM p_membership_id
693
+ )
694
+ OR group_member.active_turn_id IS NOT NULL
695
+ OR group_member.status NOT IN ('queued', 'idle', 'failed', 'cancelled')
696
+ )
697
+ ) THEN
698
+ RAISE EXCEPTION 'retention Variable Set detach cannot rotate a foreign or active sandbox group'
699
+ USING ERRCODE = '55000';
700
+ END IF;
701
+ FOR group_member_id IN
702
+ SELECT group_member.id
703
+ FROM sessions group_member
704
+ WHERE group_member.account_id = p_account_id
705
+ AND group_member.workspace_id = workspace_id_value
706
+ AND group_member.sandbox_group_id = session_row.sandbox_group_id
707
+ ORDER BY group_member.id
708
+ LOOP
709
+ PERFORM assert_session_tenancy_quiescent(
710
+ p_account_id, workspace_id_value, group_member_id, false
711
+ );
712
+ END LOOP;
713
+
714
+ PERFORM pg_advisory_xact_lock(hashtextextended(
715
+ 'sandbox-lease-admission:' || workspace_id_value::text || ':'
716
+ || session_row.sandbox_group_id::text,
717
+ 0
718
+ ));
719
+ PERFORM 1 FROM sandbox_leases lease
720
+ WHERE lease.account_id = p_account_id
721
+ AND lease.workspace_id = workspace_id_value
722
+ AND lease.sandbox_group_id = session_row.sandbox_group_id
723
+ FOR UPDATE;
724
+ IF EXISTS (
725
+ SELECT 1 FROM sandbox_lease_holders holder
726
+ JOIN sandbox_leases lease ON lease.id = holder.lease_id
727
+ WHERE lease.account_id = p_account_id
728
+ AND lease.workspace_id = workspace_id_value
729
+ AND lease.sandbox_group_id = session_row.sandbox_group_id
730
+ ) THEN
731
+ RAISE EXCEPTION 'retention Variable Set detach requires an unheld sandbox group'
732
+ USING ERRCODE = '55000';
733
+ END IF;
734
+
735
+ previous_ids := coalesce(
736
+ session_row.variable_set_ids,
737
+ CASE WHEN session_row.variable_set_id IS NULL THEN '[]'::jsonb
738
+ ELSE jsonb_build_array(session_row.variable_set_id::text) END
739
+ );
740
+ SELECT coalesce(jsonb_agg(selected.value ORDER BY selected.position)
741
+ FILTER (WHERE variable_set.id IS NULL), '[]'::jsonb),
742
+ coalesce(jsonb_agg(selected.value ORDER BY selected.position)
743
+ FILTER (WHERE variable_set.id IS NOT NULL), '[]'::jsonb)
744
+ INTO next_ids, removed_ids
745
+ FROM jsonb_array_elements_text(previous_ids)
746
+ WITH ORDINALITY selected(value, position)
747
+ LEFT JOIN workspace_variable_sets variable_set
748
+ ON variable_set.account_id = p_account_id
749
+ AND variable_set.id = selected.value::uuid
750
+ AND variable_set.owner_organization_membership_id = p_membership_id
751
+ AND EXISTS (
752
+ SELECT 1 FROM organization_user_resource_authorities authority
753
+ WHERE authority.account_id = p_account_id
754
+ AND authority.id = variable_set.authority_id
755
+ AND authority.organization_membership_id = p_membership_id
756
+ AND authority.resource_kind = 'variable_set'
757
+ AND authority.resource_id = variable_set.id
758
+ );
759
+ IF removed_ids = '[]'::jsonb THEN
760
+ RAISE EXCEPTION 'retention Variable Set detach projection drifted from the session selection'
761
+ USING ERRCODE = '55000';
762
+ END IF;
763
+ SELECT selected.value::uuid INTO next_last_id
764
+ FROM jsonb_array_elements_text(next_ids)
765
+ WITH ORDINALITY selected(value, position)
766
+ ORDER BY selected.position DESC
767
+ LIMIT 1;
768
+
769
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
770
+ sessions_activity_update_commit_guard DEFERRED;
771
+ PERFORM set_config('opengeni.session_activity_gate_state', 'open', true);
772
+ PERFORM set_config(
773
+ 'opengeni.session_activity_gate_workspace_id', workspace_id_value::text, true
774
+ );
775
+ UPDATE sessions affected SET
776
+ variable_set_ids = next_ids,
777
+ variable_set_id = next_last_id,
778
+ last_sequence = affected.last_sequence + 1,
779
+ updated_at = clock_timestamp()
780
+ WHERE affected.account_id = p_account_id
781
+ AND affected.workspace_id = workspace_id_value
782
+ AND affected.id = session_row.id;
783
+ INSERT INTO session_events (
784
+ account_id, workspace_id, session_id, sequence, type, payload,
785
+ payload_codec_version, occurred_at
786
+ ) VALUES (
787
+ p_account_id, workspace_id_value, session_row.id,
788
+ session_row.last_sequence + 1, 'session.variable_sets.updated',
789
+ jsonb_build_object(
790
+ 'actor', 'service:organization_retention',
791
+ 'organizationMembershipId', p_membership_id,
792
+ 'operationId', p_operation_id,
793
+ 'variableSetIds', next_ids,
794
+ 'removedIds', removed_ids,
795
+ 'collisionPolicy', 'later_selected_set_wins',
796
+ 'effectiveOn', 'next_turn_or_sandbox_operation',
797
+ 'reason', 'owner_retention_deleted'
798
+ ), 1, clock_timestamp()
799
+ );
800
+ INSERT INTO audit_events (
801
+ account_id, workspace_id, subject_id, action, target_type, target_id,
802
+ metadata, metadata_codec_version
803
+ )
804
+ SELECT p_account_id, workspace_id_value, NULL,
805
+ 'session.variable_set.detached', 'session', session_row.id::text,
806
+ jsonb_build_object(
807
+ 'actorKind', 'service',
808
+ 'organizationMembershipId', p_membership_id,
809
+ 'operationId', p_operation_id,
810
+ 'variableSetId', removed.value,
811
+ 'reason', 'owner_retention_deleted'
812
+ ), 1
813
+ FROM jsonb_array_elements_text(removed_ids) removed(value);
814
+ UPDATE sandbox_leases lease SET
815
+ rotation_requested_at = coalesce(lease.rotation_requested_at, clock_timestamp()),
816
+ rotation_reason = coalesce(lease.rotation_reason, 'operator'),
817
+ expires_at = least(lease.expires_at, clock_timestamp()),
818
+ updated_at = clock_timestamp()
819
+ WHERE lease.account_id = p_account_id
820
+ AND lease.workspace_id = workspace_id_value
821
+ AND lease.sandbox_group_id = session_row.sandbox_group_id
822
+ AND lease.liveness <> 'cold';
823
+
824
+ affected_session_ids := array_append(affected_session_ids, session_row.id);
825
+ detached_sessions := detached_sessions + 1;
826
+ END LOOP;
827
+
828
+ IF coalesce(array_length(affected_session_ids, 1), 0) > 0 THEN
829
+ PERFORM set_config('opengeni.session_activity_gate_state', 'preparing', true);
830
+ SET CONSTRAINTS ALL IMMEDIATE;
831
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
832
+ sessions_activity_update_commit_guard DEFERRED;
833
+ PERFORM set_config('opengeni.session_activity_gate_state', 'finalizing', true);
834
+ UPDATE workspace_session_activity_revisions counter
835
+ SET revision = counter.revision + 1
836
+ WHERE counter.account_id = p_account_id
837
+ AND counter.workspace_id = workspace_id_value
838
+ RETURNING counter.revision INTO activity_revision_value;
839
+ IF activity_revision_value IS NULL THEN
840
+ RAISE EXCEPTION 'retention Variable Set detach activity counter is unavailable'
841
+ USING ERRCODE = '55000';
842
+ END IF;
843
+ UPDATE sessions affected SET
844
+ activity_revision = activity_revision_value,
845
+ activity_revision_pending_xid = NULL
846
+ WHERE affected.account_id = p_account_id
847
+ AND affected.workspace_id = workspace_id_value
848
+ AND affected.id = ANY(affected_session_ids)
849
+ AND affected.activity_revision_pending_xid = pg_current_xact_id()::text::bigint;
850
+ GET DIAGNOSTICS finalized_session_count = ROW_COUNT;
851
+ IF finalized_session_count <> array_length(affected_session_ids, 1) THEN
852
+ RAISE EXCEPTION 'retention Variable Set detach activity was not finalized'
853
+ USING ERRCODE = '55000';
854
+ END IF;
855
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
856
+ sessions_activity_update_commit_guard IMMEDIATE;
857
+ PERFORM set_config('opengeni.session_activity_gate_state', '', true);
858
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id', '', true);
859
+ END IF;
860
+ END LOOP;
861
+ PERFORM opengeni_private.close_session_tenancy_fenced_access(
862
+ fenced_access_capability_id
863
+ );
864
+ IF NOT write_capability_preexisting THEN
865
+ DELETE FROM opengeni_private.variable_set_authority_capabilities
866
+ WHERE backend_pid = pg_backend_pid()
867
+ AND transaction_id = pg_current_xact_id_if_assigned()
868
+ AND capability_kind = 'write';
869
+ END IF;
870
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace, ''), true);
871
+ RETURN detached_sessions;
872
+ EXCEPTION WHEN OTHERS THEN
873
+ PERFORM opengeni_private.close_session_tenancy_fenced_access(
874
+ fenced_access_capability_id
875
+ );
876
+ IF NOT coalesce(write_capability_preexisting, false) THEN
877
+ DELETE FROM opengeni_private.variable_set_authority_capabilities
878
+ WHERE backend_pid = pg_backend_pid()
879
+ AND transaction_id = pg_current_xact_id_if_assigned()
880
+ AND capability_kind = 'write';
881
+ END IF;
882
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace, ''), true);
883
+ PERFORM set_config(
884
+ 'opengeni.session_activity_gate_state', coalesce(previous_gate_state, ''), true
885
+ );
886
+ PERFORM set_config(
887
+ 'opengeni.session_activity_gate_workspace_id',
888
+ coalesce(previous_gate_workspace, ''), true
889
+ );
890
+ RAISE;
891
+ END
892
+ $body$;
893
+
894
+ REVOKE ALL ON FUNCTION
895
+ opengeni_private.detach_retention_variable_set_session_selections(uuid,uuid,uuid)
896
+ FROM PUBLIC;
897
+
898
+ -- The source-definition anchor below contains the retained finalizer's
899
+ -- scheduled_tasks UPDATE and its authority lookup. Keep the migration owner
900
+ -- visible to those FORCE-RLS tables while the definition is inspected and
901
+ -- replaced; application roles remain policy-bound, and the maintenance drain
902
+ -- above excludes live writers.
903
+ ALTER TABLE scheduled_tasks NO FORCE ROW LEVEL SECURITY;
904
+ ALTER TABLE organization_user_resource_authorities NO FORCE ROW LEVEL SECURITY;
905
+ DO $patch_retention_variable_set_detach$
906
+ DECLARE
907
+ definition text;
908
+ patched text;
909
+ occurrences integer;
910
+ anchor constant text :=
911
+ E' UPDATE scheduled_tasks task SET status = ''paused'', variable_set_id = NULL,\n'
912
+ || E' authority_revision = authority_revision + 1, updated_at = clock_timestamp()\n'
913
+ || E' WHERE task.account_id = p_account_id AND task.variable_set_id IN (\n'
914
+ || E' SELECT authority.resource_id FROM organization_user_resource_authorities authority\n'
915
+ || E' WHERE authority.account_id = p_account_id\n'
916
+ || E' AND authority.organization_membership_id = p_membership_id\n'
917
+ || E' AND authority.resource_kind = ''variable_set''\n'
918
+ || E' );';
919
+ replacement constant text :=
920
+ anchor
921
+ || E'\n PERFORM opengeni_private.detach_retention_variable_set_session_selections(\n'
922
+ || E' p_account_id, p_membership_id, p_operation_id\n'
923
+ || E' );';
924
+ BEGIN
925
+ definition := pg_catalog.pg_get_functiondef(
926
+ pg_catalog.to_regprocedure(
927
+ pg_catalog.quote_ident(pg_catalog.current_schema())
928
+ || '.finalize_organization_retention_deletion(uuid,uuid,uuid,text)'
929
+ )
930
+ );
931
+ occurrences := (
932
+ pg_catalog.length(definition)
933
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
934
+ ) / pg_catalog.length(anchor);
935
+ IF definition IS NULL OR occurrences <> 1
936
+ OR pg_catalog.strpos(
937
+ definition,
938
+ 'detach_retention_variable_set_session_selections'
939
+ ) > 0
940
+ THEN
941
+ RAISE EXCEPTION '0352 organization retention Variable Set detach definition drift'
942
+ USING ERRCODE = '55000';
943
+ END IF;
944
+ patched := pg_catalog.replace(definition, anchor, replacement);
945
+ IF pg_catalog.strpos(patched, replacement) = 0 THEN
946
+ RAISE EXCEPTION '0352 organization retention Variable Set detach patch failed'
947
+ USING ERRCODE = '55000';
948
+ END IF;
949
+ EXECUTE patched;
950
+ END
951
+ $patch_retention_variable_set_detach$;
952
+ ALTER TABLE organization_user_resource_authorities FORCE ROW LEVEL SECURITY;
953
+ ALTER TABLE scheduled_tasks FORCE ROW LEVEL SECURITY;
954
+
955
+ -- A restart/fork with runtime setup is one database transaction. The original
956
+ -- fork function still owns destination creation, history copy, receipt, and
957
+ -- source quiescence. This helper configures only that fresh singleton
958
+ -- destination before the outer function can commit, so no caller can observe
959
+ -- a fork that is missing its selected Rig or explicit Variable Sets.
960
+ CREATE FUNCTION opengeni_private.configure_fork_session_runtime(
961
+ p_account_id uuid,
962
+ p_workspace_id uuid,
963
+ p_source_session_id uuid,
964
+ p_destination_session_id uuid,
965
+ p_actor_subject_id text,
966
+ p_variable_set_ids jsonb,
967
+ p_rig_id uuid,
968
+ p_rig_version_id uuid,
969
+ p_configuration_digest text
970
+ ) RETURNS TABLE (
971
+ runtime_event_id uuid,
972
+ runtime_event_sequence integer
973
+ )
974
+ LANGUAGE plpgsql SECURITY DEFINER
975
+ SET search_path FROM CURRENT
976
+ AS $body$
977
+ DECLARE
978
+ destination_session sessions%ROWTYPE;
979
+ runtime_marker jsonb;
980
+ selected_count integer;
981
+ selected_last uuid;
982
+ destination_activity_revision bigint;
983
+ previous_gate_state text := current_setting('opengeni.session_activity_gate_state', true);
984
+ previous_gate_workspace text := current_setting(
985
+ 'opengeni.session_activity_gate_workspace_id', true
986
+ );
987
+ BEGIN
988
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
989
+ OR p_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
990
+ OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
991
+ THEN
992
+ RAISE EXCEPTION 'fork runtime configuration authority is invalid'
993
+ USING ERRCODE = '42501';
994
+ END IF;
995
+ IF p_account_id IS NULL OR p_workspace_id IS NULL
996
+ OR p_source_session_id IS NULL OR p_destination_session_id IS NULL
997
+ OR p_actor_subject_id IS NULL OR p_variable_set_ids IS NULL
998
+ OR p_configuration_digest !~ '^[0-9a-f]{64}$'
999
+ OR nullif(previous_gate_state, '') IS NOT NULL
1000
+ OR nullif(previous_gate_workspace, '') IS NOT NULL
1001
+ THEN
1002
+ RAISE EXCEPTION 'fork runtime configuration request is invalid'
1003
+ USING ERRCODE = '22023';
1004
+ END IF;
1005
+
1006
+ PERFORM acquire_session_tenancy_fence(p_workspace_id);
1007
+ SELECT session_value.* INTO destination_session
1008
+ FROM sessions session_value
1009
+ WHERE session_value.account_id = p_account_id
1010
+ AND session_value.workspace_id = p_workspace_id
1011
+ AND session_value.id = p_destination_session_id
1012
+ FOR UPDATE;
1013
+ IF NOT FOUND
1014
+ OR destination_session.forked_from_session_id IS DISTINCT FROM p_source_session_id
1015
+ OR destination_session.owner_subject_id IS DISTINCT FROM p_actor_subject_id
1016
+ THEN
1017
+ RAISE EXCEPTION 'fork runtime destination is unavailable'
1018
+ USING ERRCODE = '42501';
1019
+ END IF;
1020
+
1021
+ runtime_marker := destination_session.metadata -> 'forkRuntimeConfiguration';
1022
+ IF runtime_marker IS NOT NULL THEN
1023
+ IF runtime_marker ->> 'digest' IS DISTINCT FROM p_configuration_digest THEN
1024
+ RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
1025
+ END IF;
1026
+ runtime_event_id := nullif(runtime_marker ->> 'eventId', '')::uuid;
1027
+ runtime_event_sequence := (runtime_marker ->> 'eventSequence')::integer;
1028
+ IF runtime_event_id IS NULL OR runtime_event_sequence IS NULL THEN
1029
+ RAISE EXCEPTION 'fork runtime configuration marker is incomplete'
1030
+ USING ERRCODE = '23514';
1031
+ END IF;
1032
+ RETURN NEXT;
1033
+ RETURN;
1034
+ END IF;
1035
+
1036
+ IF destination_session.status <> 'idle'
1037
+ OR destination_session.active_turn_id IS NOT NULL
1038
+ OR destination_session.active_sandbox_id IS NOT NULL
1039
+ OR destination_session.sandbox_group_id IS DISTINCT FROM destination_session.id
1040
+ THEN
1041
+ RAISE EXCEPTION 'fork runtime destination is not fresh and quiescent'
1042
+ USING ERRCODE = '23514';
1043
+ END IF;
1044
+ IF jsonb_typeof(p_variable_set_ids) <> 'array' THEN
1045
+ RAISE EXCEPTION 'fork runtime Variable Set ids must be an array'
1046
+ USING ERRCODE = '22023';
1047
+ END IF;
1048
+ selected_count := jsonb_array_length(p_variable_set_ids);
1049
+ IF selected_count > 25
1050
+ OR EXISTS (
1051
+ SELECT 1 FROM jsonb_array_elements(p_variable_set_ids) item(value)
1052
+ WHERE jsonb_typeof(item.value) <> 'string'
1053
+ OR (item.value #>> '{}') !~*
1054
+ '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
1055
+ )
1056
+ OR selected_count <> (
1057
+ SELECT count(DISTINCT value)
1058
+ FROM jsonb_array_elements_text(p_variable_set_ids) selected(value)
1059
+ )
1060
+ THEN
1061
+ RAISE EXCEPTION 'fork runtime Variable Set selection is invalid'
1062
+ USING ERRCODE = '22023';
1063
+ END IF;
1064
+ IF selected_count <> (
1065
+ SELECT count(*)
1066
+ FROM workspace_variable_sets variable_set
1067
+ JOIN jsonb_array_elements_text(p_variable_set_ids) selected(value)
1068
+ ON selected.value::uuid = variable_set.id
1069
+ WHERE variable_set.account_id = p_account_id
1070
+ AND variable_set.status = 'active'
1071
+ ) THEN
1072
+ RAISE EXCEPTION 'fork runtime Variable Set selection is unavailable'
1073
+ USING ERRCODE = '42501';
1074
+ END IF;
1075
+ SELECT selected.value::uuid INTO selected_last
1076
+ FROM jsonb_array_elements_text(p_variable_set_ids)
1077
+ WITH ORDINALITY selected(value, position)
1078
+ ORDER BY selected.position DESC
1079
+ LIMIT 1;
1080
+
1081
+ IF (p_rig_id IS NULL) <> (p_rig_version_id IS NULL)
1082
+ OR (
1083
+ p_rig_id IS NOT NULL
1084
+ AND NOT EXISTS (
1085
+ SELECT 1
1086
+ FROM rig_versions rig_version
1087
+ WHERE rig_version.account_id = p_account_id
1088
+ AND rig_version.rig_id = p_rig_id
1089
+ AND rig_version.id = p_rig_version_id
1090
+ AND rig_version.active
1091
+ AND NOT EXISTS (
1092
+ SELECT 1
1093
+ FROM jsonb_array_elements_text(
1094
+ coalesce(rig_version.default_variable_set_ids, '[]'::jsonb)
1095
+ ) default_set(value)
1096
+ WHERE NOT EXISTS (
1097
+ SELECT 1
1098
+ FROM workspace_variable_sets variable_set
1099
+ WHERE variable_set.account_id = p_account_id
1100
+ AND variable_set.id = default_set.value::uuid
1101
+ AND variable_set.status = 'active'
1102
+ )
1103
+ )
1104
+ )
1105
+ )
1106
+ THEN
1107
+ RAISE EXCEPTION 'fork runtime Rig selection is unavailable'
1108
+ USING ERRCODE = '42501';
1109
+ END IF;
1110
+
1111
+ runtime_event_id := gen_random_uuid();
1112
+ runtime_event_sequence := destination_session.last_sequence + 1;
1113
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
1114
+ sessions_activity_update_commit_guard DEFERRED;
1115
+ PERFORM set_config('opengeni.session_activity_gate_state', 'open', true);
1116
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id', p_workspace_id::text, true);
1117
+ UPDATE sessions session_value SET
1118
+ variable_set_ids = p_variable_set_ids,
1119
+ variable_set_id = selected_last,
1120
+ rig_id = p_rig_id,
1121
+ rig_version_id = p_rig_version_id,
1122
+ metadata = session_value.metadata || jsonb_build_object(
1123
+ 'forkRuntimeConfiguration', jsonb_build_object(
1124
+ 'digest', p_configuration_digest,
1125
+ 'eventId', runtime_event_id,
1126
+ 'eventSequence', runtime_event_sequence,
1127
+ 'variableSetIds', p_variable_set_ids,
1128
+ 'rigId', p_rig_id,
1129
+ 'rigVersionId', p_rig_version_id
1130
+ )
1131
+ ),
1132
+ last_sequence = runtime_event_sequence,
1133
+ updated_at = clock_timestamp()
1134
+ WHERE session_value.account_id = p_account_id
1135
+ AND session_value.workspace_id = p_workspace_id
1136
+ AND session_value.id = p_destination_session_id;
1137
+
1138
+ INSERT INTO session_events (
1139
+ id, account_id, workspace_id, session_id, sequence, type, payload, occurred_at
1140
+ ) VALUES (
1141
+ runtime_event_id, p_account_id, p_workspace_id, p_destination_session_id,
1142
+ runtime_event_sequence, 'session.runtime.configured', jsonb_build_object(
1143
+ 'sourceSessionId', p_source_session_id,
1144
+ 'variableSetIds', p_variable_set_ids,
1145
+ 'rigId', p_rig_id,
1146
+ 'rigVersionId', p_rig_version_id,
1147
+ 'collisionPolicy', 'later_selected_set_wins'
1148
+ ), clock_timestamp()
1149
+ );
1150
+ INSERT INTO audit_events (
1151
+ account_id, workspace_id, subject_id, action, target_type, target_id,
1152
+ metadata, metadata_codec_version
1153
+ ) VALUES (
1154
+ p_account_id, p_workspace_id, p_actor_subject_id,
1155
+ 'session.runtime.configured', 'session', p_destination_session_id::text,
1156
+ jsonb_build_object(
1157
+ 'sourceSessionId', p_source_session_id,
1158
+ 'variableSetIds', p_variable_set_ids,
1159
+ 'rigId', p_rig_id,
1160
+ 'rigVersionId', p_rig_version_id,
1161
+ 'collisionPolicy', 'later_selected_set_wins'
1162
+ ), 1
1163
+ );
1164
+
1165
+ PERFORM set_config('opengeni.session_activity_gate_state', 'preparing', true);
1166
+ SET CONSTRAINTS ALL IMMEDIATE;
1167
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
1168
+ sessions_activity_update_commit_guard DEFERRED;
1169
+ PERFORM set_config('opengeni.session_activity_gate_state', 'finalizing', true);
1170
+ UPDATE workspace_session_activity_revisions counter
1171
+ SET revision = counter.revision + 1
1172
+ WHERE counter.workspace_id = p_workspace_id
1173
+ RETURNING counter.revision INTO destination_activity_revision;
1174
+ IF destination_activity_revision IS NULL THEN
1175
+ RAISE EXCEPTION 'fork runtime destination activity counter is unavailable'
1176
+ USING ERRCODE = '23514';
1177
+ END IF;
1178
+ UPDATE sessions session_value SET
1179
+ activity_revision = destination_activity_revision,
1180
+ activity_revision_pending_xid = NULL
1181
+ WHERE session_value.account_id = p_account_id
1182
+ AND session_value.workspace_id = p_workspace_id
1183
+ AND session_value.id = p_destination_session_id
1184
+ AND session_value.activity_revision_pending_xid = pg_current_xact_id()::text::bigint;
1185
+ IF NOT FOUND THEN
1186
+ RAISE EXCEPTION 'fork runtime destination activity was not finalized'
1187
+ USING ERRCODE = '23514';
1188
+ END IF;
1189
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
1190
+ sessions_activity_update_commit_guard IMMEDIATE;
1191
+ PERFORM set_config('opengeni.session_activity_gate_state', '', true);
1192
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id', '', true);
1193
+ RETURN NEXT;
1194
+ EXCEPTION WHEN OTHERS THEN
1195
+ PERFORM set_config(
1196
+ 'opengeni.session_activity_gate_state', coalesce(previous_gate_state, ''), true
1197
+ );
1198
+ PERFORM set_config(
1199
+ 'opengeni.session_activity_gate_workspace_id',
1200
+ coalesce(previous_gate_workspace, ''), true
1201
+ );
1202
+ RAISE;
1203
+ END
1204
+ $body$;
1205
+
1206
+ CREATE FUNCTION opengeni_private.read_fork_session_runtime(
1207
+ p_account_id uuid,
1208
+ p_workspace_id uuid,
1209
+ p_source_session_id uuid,
1210
+ p_destination_session_id uuid,
1211
+ p_actor_subject_id text,
1212
+ p_configuration_digest text
1213
+ ) RETURNS TABLE (
1214
+ runtime_event_id uuid,
1215
+ runtime_event_sequence integer
1216
+ )
1217
+ LANGUAGE plpgsql SECURITY DEFINER
1218
+ SET search_path FROM CURRENT
1219
+ AS $body$
1220
+ DECLARE
1221
+ runtime_marker jsonb;
1222
+ BEGIN
1223
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
1224
+ OR p_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
1225
+ OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
1226
+ OR p_configuration_digest !~ '^[0-9a-f]{64}$'
1227
+ THEN
1228
+ RAISE EXCEPTION 'fork runtime replay authority is invalid'
1229
+ USING ERRCODE = '42501';
1230
+ END IF;
1231
+ SELECT session_value.metadata -> 'forkRuntimeConfiguration'
1232
+ INTO runtime_marker
1233
+ FROM sessions session_value
1234
+ WHERE session_value.account_id = p_account_id
1235
+ AND session_value.workspace_id = p_workspace_id
1236
+ AND session_value.id = p_destination_session_id
1237
+ AND session_value.forked_from_session_id = p_source_session_id
1238
+ AND session_value.owner_subject_id = p_actor_subject_id;
1239
+ IF runtime_marker IS NULL THEN
1240
+ RAISE EXCEPTION 'fork runtime configuration marker is unavailable'
1241
+ USING ERRCODE = '23514';
1242
+ END IF;
1243
+ IF runtime_marker ->> 'digest' IS DISTINCT FROM p_configuration_digest THEN
1244
+ RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
1245
+ END IF;
1246
+ runtime_event_id := nullif(runtime_marker ->> 'eventId', '')::uuid;
1247
+ runtime_event_sequence := (runtime_marker ->> 'eventSequence')::integer;
1248
+ IF runtime_event_id IS NULL OR runtime_event_sequence IS NULL THEN
1249
+ RAISE EXCEPTION 'fork runtime configuration marker is incomplete'
1250
+ USING ERRCODE = '23514';
1251
+ END IF;
1252
+ RETURN NEXT;
1253
+ END
1254
+ $body$;
1255
+
1256
+ CREATE FUNCTION fork_session_content_with_runtime(
1257
+ p_account_id uuid,
1258
+ p_source_workspace_id uuid,
1259
+ p_source_session_id uuid,
1260
+ p_actor_subject_id text,
1261
+ p_destination_workspace_id uuid,
1262
+ p_destination_visibility text,
1263
+ p_workspace_shared_acknowledged boolean,
1264
+ p_operation_key text,
1265
+ p_canonical_request_hash text,
1266
+ p_activation_version integer,
1267
+ p_variable_set_ids jsonb,
1268
+ p_rig_id uuid,
1269
+ p_rig_version_id uuid,
1270
+ p_configuration_digest text
1271
+ ) RETURNS TABLE (
1272
+ operation_id uuid,
1273
+ event_id uuid,
1274
+ event_sequence integer,
1275
+ session_id uuid,
1276
+ workspace_id uuid,
1277
+ visibility text,
1278
+ authority_epoch integer,
1279
+ copied_history_item_count integer,
1280
+ replay boolean,
1281
+ runtime_event_id uuid,
1282
+ runtime_event_sequence integer
1283
+ )
1284
+ LANGUAGE plpgsql SECURITY DEFINER
1285
+ SET search_path FROM CURRENT
1286
+ AS $body$
1287
+ BEGIN
1288
+ SELECT fork_result.operation_id, fork_result.event_id, fork_result.event_sequence,
1289
+ fork_result.session_id, fork_result.workspace_id, fork_result.visibility,
1290
+ fork_result.authority_epoch, fork_result.copied_history_item_count, fork_result.replay
1291
+ INTO operation_id, event_id, event_sequence, session_id, workspace_id, visibility,
1292
+ authority_epoch, copied_history_item_count, replay
1293
+ FROM fork_session_content(
1294
+ p_account_id, p_source_workspace_id, p_source_session_id, p_actor_subject_id,
1295
+ p_destination_workspace_id, p_destination_visibility,
1296
+ p_workspace_shared_acknowledged, p_operation_key, p_canonical_request_hash,
1297
+ p_activation_version
1298
+ ) fork_result;
1299
+ IF session_id IS NULL THEN
1300
+ RAISE EXCEPTION 'session fork returned no destination' USING ERRCODE = '23514';
1301
+ END IF;
1302
+ SELECT configured.runtime_event_id, configured.runtime_event_sequence
1303
+ INTO runtime_event_id, runtime_event_sequence
1304
+ FROM opengeni_private.configure_fork_session_runtime(
1305
+ p_account_id, p_source_workspace_id, p_source_session_id, session_id,
1306
+ p_actor_subject_id, p_variable_set_ids, p_rig_id, p_rig_version_id,
1307
+ p_configuration_digest
1308
+ ) configured;
1309
+ RETURN NEXT;
1310
+ END
1311
+ $body$;
1312
+
1313
+ CREATE FUNCTION replay_applied_session_fork_with_runtime(
1314
+ p_account_id uuid,
1315
+ p_source_workspace_id uuid,
1316
+ p_source_session_id uuid,
1317
+ p_actor_subject_id text,
1318
+ p_destination_workspace_id uuid,
1319
+ p_destination_visibility text,
1320
+ p_workspace_shared_acknowledged boolean,
1321
+ p_operation_key text,
1322
+ p_canonical_request_hash text,
1323
+ p_activation_version integer,
1324
+ p_configuration_digest text
1325
+ ) RETURNS TABLE (
1326
+ operation_id uuid,
1327
+ event_id uuid,
1328
+ event_sequence integer,
1329
+ session_id uuid,
1330
+ workspace_id uuid,
1331
+ visibility text,
1332
+ authority_epoch integer,
1333
+ copied_history_item_count integer,
1334
+ replay boolean,
1335
+ runtime_event_id uuid,
1336
+ runtime_event_sequence integer
1337
+ )
1338
+ LANGUAGE plpgsql SECURITY DEFINER
1339
+ SET search_path FROM CURRENT
1340
+ AS $body$
1341
+ BEGIN
1342
+ SELECT fork_result.operation_id, fork_result.event_id, fork_result.event_sequence,
1343
+ fork_result.session_id, fork_result.workspace_id, fork_result.visibility,
1344
+ fork_result.authority_epoch, fork_result.copied_history_item_count, fork_result.replay
1345
+ INTO operation_id, event_id, event_sequence, session_id, workspace_id, visibility,
1346
+ authority_epoch, copied_history_item_count, replay
1347
+ FROM replay_applied_session_fork(
1348
+ p_account_id, p_source_workspace_id, p_source_session_id, p_actor_subject_id,
1349
+ p_destination_workspace_id, p_destination_visibility,
1350
+ p_workspace_shared_acknowledged, p_operation_key, p_canonical_request_hash,
1351
+ p_activation_version
1352
+ ) fork_result;
1353
+ IF session_id IS NULL THEN
1354
+ RETURN;
1355
+ END IF;
1356
+ SELECT configured.runtime_event_id, configured.runtime_event_sequence
1357
+ INTO runtime_event_id, runtime_event_sequence
1358
+ FROM opengeni_private.read_fork_session_runtime(
1359
+ p_account_id, p_source_workspace_id, p_source_session_id, session_id,
1360
+ p_actor_subject_id, p_configuration_digest
1361
+ ) configured;
1362
+ RETURN NEXT;
1363
+ END
1364
+ $body$;
1365
+
1366
+ DO $fork_runtime_search_path_and_grants$
1367
+ DECLARE
1368
+ data_schema text := current_schema();
1369
+ application_role text;
1370
+ BEGIN
1371
+ EXECUTE format(
1372
+ 'ALTER FUNCTION opengeni_private.configure_fork_session_runtime('
1373
+ || 'uuid,uuid,uuid,uuid,text,jsonb,uuid,uuid,text) '
1374
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1375
+ data_schema
1376
+ );
1377
+ EXECUTE format(
1378
+ 'ALTER FUNCTION opengeni_private.read_fork_session_runtime('
1379
+ || 'uuid,uuid,uuid,uuid,text,text) '
1380
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1381
+ data_schema
1382
+ );
1383
+ EXECUTE format(
1384
+ 'ALTER FUNCTION %I.fork_session_content_with_runtime('
1385
+ || 'uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer,jsonb,uuid,uuid,text) '
1386
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1387
+ data_schema, data_schema
1388
+ );
1389
+ EXECUTE format(
1390
+ 'ALTER FUNCTION %I.replay_applied_session_fork_with_runtime('
1391
+ || 'uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer,text) '
1392
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1393
+ data_schema, data_schema
1394
+ );
1395
+ FOR application_role IN
1396
+ SELECT role_value.rolname
1397
+ FROM jsonb_array_elements_text(
1398
+ current_setting('opengeni.migration_application_roles')::jsonb
1399
+ ) configured(value)
1400
+ JOIN pg_catalog.pg_roles role_value
1401
+ ON role_value.rolname = configured.value
1402
+ LOOP
1403
+ EXECUTE format(
1404
+ 'GRANT EXECUTE ON FUNCTION %I.fork_session_content_with_runtime('
1405
+ || 'uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer,jsonb,uuid,uuid,text) '
1406
+ || 'TO %I',
1407
+ data_schema, application_role
1408
+ );
1409
+ EXECUTE format(
1410
+ 'GRANT EXECUTE ON FUNCTION %I.replay_applied_session_fork_with_runtime('
1411
+ || 'uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer,text) '
1412
+ || 'TO %I',
1413
+ data_schema, application_role
1414
+ );
1415
+ END LOOP;
1416
+ END
1417
+ $fork_runtime_search_path_and_grants$;
1418
+
1419
+ REVOKE ALL ON FUNCTION opengeni_private.configure_fork_session_runtime(
1420
+ uuid, uuid, uuid, uuid, text, jsonb, uuid, uuid, text
1421
+ ) FROM PUBLIC;
1422
+ REVOKE ALL ON FUNCTION opengeni_private.read_fork_session_runtime(
1423
+ uuid, uuid, uuid, uuid, text, text
1424
+ ) FROM PUBLIC;
1425
+ REVOKE ALL ON FUNCTION fork_session_content_with_runtime(
1426
+ uuid, uuid, uuid, text, uuid, text, boolean, text, text, integer,
1427
+ jsonb, uuid, uuid, text
1428
+ ) FROM PUBLIC;
1429
+ REVOKE ALL ON FUNCTION replay_applied_session_fork_with_runtime(
1430
+ uuid, uuid, uuid, text, uuid, text, boolean, text, text, integer, text
1431
+ ) FROM PUBLIC;
1432
+
1433
+ -- The accepted-turn personal-resource closure must include every explicit
1434
+ -- user-scoped Variable Set, not only the legacy final alias. Patch the exact
1435
+ -- drained function source so the established grant/receipt protocol remains
1436
+ -- byte-for-byte unchanged outside this ordered-selection extension.
1437
+ ALTER TABLE turn_personal_resource_attachment_receipts
1438
+ DROP CONSTRAINT turn_personal_resource_attachment_receipts_identity_chk;
1439
+ ALTER TABLE turn_personal_resource_attachment_receipts
1440
+ ADD CONSTRAINT turn_personal_resource_attachment_receipts_identity_chk CHECK (
1441
+ octet_length(initiating_human_subject_id) BETWEEN 1 AND 512
1442
+ AND membership_authorization_revision > 0
1443
+ AND session_authority_epoch > 0
1444
+ AND grant_mode IN ('once', 'session', 'always')
1445
+ AND session_visibility IN ('user_private', 'workspace_shared')
1446
+ AND shared_output_warning_version = 1
1447
+ AND resource_count BETWEEN 1 AND 52
1448
+ AND (session_visibility <> 'workspace_shared' OR shared_output_acknowledged IS TRUE)
1449
+ );
1450
+ ALTER TABLE turn_personal_resource_snapshots
1451
+ DROP CONSTRAINT turn_personal_resource_snapshots_generation_chk;
1452
+ ALTER TABLE turn_personal_resource_snapshots
1453
+ ADD CONSTRAINT turn_personal_resource_snapshots_generation_chk CHECK (
1454
+ membership_authorization_revision > 0
1455
+ AND authority_generation > 0
1456
+ AND grant_generation > 0
1457
+ AND cardinality(selection_sources) BETWEEN 1 AND 50
1458
+ );
1459
+
1460
+ ALTER TABLE session_variable_set_attachments NO FORCE ROW LEVEL SECURITY;
1461
+ ALTER TABLE workspace_variable_sets NO FORCE ROW LEVEL SECURITY;
1462
+ DO $extend_atomic_attachment_function_for_session_sets$
1463
+ DECLARE
1464
+ data_schema text := current_schema();
1465
+ function_source text;
1466
+ prior_source text;
1467
+ BEGIN
1468
+ SELECT procedure_value.prosrc INTO STRICT function_source
1469
+ FROM pg_proc procedure_value
1470
+ JOIN pg_namespace namespace_value ON namespace_value.oid = procedure_value.pronamespace
1471
+ WHERE namespace_value.nspname = data_schema
1472
+ AND procedure_value.oid = (
1473
+ pg_catalog.format(
1474
+ '%I.accept_turn_personal_resource_attachment(uuid,uuid,uuid,uuid,text,integer,boolean,integer)',
1475
+ data_schema
1476
+ )::regprocedure
1477
+ );
1478
+
1479
+ prior_source := function_source;
1480
+ function_source := replace(function_source,
1481
+ $old$ PERFORM 1 FROM workspace_variable_sets variable_set
1482
+ WHERE variable_set.id = session_row.variable_set_id
1483
+ AND variable_set.account_id = p_account_id
1484
+ FOR SHARE;$old$,
1485
+ $new$ PERFORM variable_set.id
1486
+ FROM session_variable_set_attachments attachment
1487
+ JOIN workspace_variable_sets variable_set
1488
+ ON variable_set.id = attachment.variable_set_id
1489
+ AND variable_set.account_id = p_account_id
1490
+ WHERE attachment.account_id = p_account_id
1491
+ AND attachment.workspace_id = p_workspace_id
1492
+ AND attachment.session_id = p_session_id
1493
+ ORDER BY attachment.position
1494
+ FOR SHARE OF variable_set;$new$
1495
+ );
1496
+ IF function_source = prior_source THEN
1497
+ RAISE EXCEPTION '0352 could not extend the personal-resource attachment lock set'
1498
+ USING ERRCODE = '55000';
1499
+ END IF;
1500
+
1501
+ prior_source := function_source;
1502
+ function_source := replace(function_source,
1503
+ $old$ FROM workspace_variable_sets variable_set
1504
+ WHERE variable_set.id = session_row.variable_set_id
1505
+ AND variable_set.account_id = p_account_id
1506
+ AND variable_set.authority_scope = 'user'$old$,
1507
+ $new$ FROM session_variable_set_attachments attachment
1508
+ JOIN workspace_variable_sets variable_set
1509
+ ON variable_set.id = attachment.variable_set_id
1510
+ AND variable_set.account_id = p_account_id
1511
+ AND variable_set.authority_scope = 'user'
1512
+ WHERE attachment.account_id = p_account_id
1513
+ AND attachment.workspace_id = p_workspace_id
1514
+ AND attachment.session_id = p_session_id$new$
1515
+ );
1516
+ IF function_source = prior_source THEN
1517
+ RAISE EXCEPTION '0352 could not extend the personal-resource attachment closure'
1518
+ USING ERRCODE = '55000';
1519
+ END IF;
1520
+ function_source := replace(
1521
+ function_source,
1522
+ '''session_variable_set''::text AS selection_source',
1523
+ '''session_variable_set:'' || attachment.position::text AS selection_source'
1524
+ );
1525
+ function_source := replace(function_source, 'IF selected_count > 28 THEN',
1526
+ 'IF selected_count > 52 THEN');
1527
+ IF function_source NOT LIKE '%IF selected_count > 52 THEN%'
1528
+ OR function_source LIKE '%IF selected_count > 28 THEN%'
1529
+ THEN
1530
+ RAISE EXCEPTION '0352 could not update the personal-resource attachment bound'
1531
+ USING ERRCODE = '55000';
1532
+ END IF;
1533
+
1534
+ EXECUTE pg_catalog.format(
1535
+ 'CREATE OR REPLACE FUNCTION %I.accept_turn_personal_resource_attachment('
1536
+ || 'p_account_id uuid, p_workspace_id uuid, p_session_id uuid, p_turn_id uuid, '
1537
+ || 'p_mode text, p_expected_authority_epoch integer, '
1538
+ || 'p_workspace_shared_acknowledged boolean, '
1539
+ || 'p_shared_output_warning_version integer) '
1540
+ || 'RETURNS TABLE (grant_mode text, grant_context text, resource_count integer, '
1541
+ || 'resource_kinds text[], shared_output_warning_version integer, replay boolean) '
1542
+ || 'LANGUAGE plpgsql SECURITY DEFINER '
1543
+ || 'SET search_path TO pg_catalog, %I, pg_temp AS %L',
1544
+ data_schema, data_schema, function_source
1545
+ );
1546
+ END
1547
+ $extend_atomic_attachment_function_for_session_sets$;
1548
+ ALTER TABLE workspace_variable_sets FORCE ROW LEVEL SECURITY;
1549
+ ALTER TABLE session_variable_set_attachments FORCE ROW LEVEL SECURITY;
1550
+
1551
+ -- Scheduled attempts freeze every explicit set generation. Extend the existing
1552
+ -- attempt oracle to consume the new plural accepted snapshot while retaining
1553
+ -- the legacy singular fallback for pre-0344 runs.
1554
+ DO $extend_scheduled_variable_set_generation_oracle$
1555
+ DECLARE
1556
+ data_schema text := current_schema();
1557
+ function_oid regprocedure;
1558
+ function_source text;
1559
+ prior_source text;
1560
+ BEGIN
1561
+ function_oid := pg_catalog.to_regprocedure(
1562
+ pg_catalog.format(
1563
+ '%I.scheduled_variable_set_expected_generation_for_attempt(uuid,uuid,uuid,uuid,uuid,integer,uuid)',
1564
+ data_schema
1565
+ )
1566
+ );
1567
+ IF function_oid IS NULL THEN
1568
+ -- Some focused legacy-migration tests deliberately apply a sparse suffix
1569
+ -- that predates the scheduled-attempt oracle. There is nothing to extend
1570
+ -- in that shape; full migration chains always have the function here.
1571
+ RETURN;
1572
+ END IF;
1573
+
1574
+ SELECT procedure_value.prosrc INTO STRICT function_source
1575
+ FROM pg_proc procedure_value
1576
+ JOIN pg_namespace namespace_value ON namespace_value.oid = procedure_value.pronamespace
1577
+ WHERE namespace_value.nspname = data_schema
1578
+ AND procedure_value.oid = function_oid;
1579
+
1580
+ prior_source := function_source;
1581
+ function_source := replace(
1582
+ function_source,
1583
+ 'session_value.variable_set_id, session_value.rig_id,',
1584
+ 'session_value.variable_set_ids, session_value.variable_set_id, session_value.rig_id,'
1585
+ );
1586
+ IF function_source = prior_source THEN
1587
+ RAISE EXCEPTION '0352 could not extend the scheduled session selection snapshot'
1588
+ USING ERRCODE = '55000';
1589
+ END IF;
1590
+
1591
+ prior_source := function_source;
1592
+ function_source := replace(function_source,
1593
+ $old$ SELECT (run_snapshot -> 'targetSessionExecution' ->> 'variableSetGeneration')::bigint
1594
+ WHERE run_snapshot -> 'targetSessionExecution' <> 'null'::jsonb
1595
+ AND run_snapshot -> 'targetSessionExecution' ->> 'variableSetId' = p_variable_set_id::text$old$,
1596
+ $new$ SELECT (item ->> 'generation')::bigint
1597
+ FROM jsonb_array_elements(coalesce(
1598
+ run_snapshot -> 'targetSessionExecution' -> 'variableSets', '[]'::jsonb
1599
+ )) item
1600
+ WHERE item ->> 'id' = p_variable_set_id::text
1601
+ UNION ALL
1602
+ SELECT (run_snapshot -> 'targetSessionExecution' ->> 'variableSetGeneration')::bigint
1603
+ WHERE run_snapshot -> 'targetSessionExecution' <> 'null'::jsonb
1604
+ AND jsonb_array_length(coalesce(
1605
+ run_snapshot -> 'targetSessionExecution' -> 'variableSets', '[]'::jsonb
1606
+ )) = 0
1607
+ AND run_snapshot -> 'targetSessionExecution' ->> 'variableSetId' = p_variable_set_id::text$new$
1608
+ );
1609
+ IF function_source = prior_source THEN
1610
+ RAISE EXCEPTION '0352 could not extend the scheduled accepted generation lookup'
1611
+ USING ERRCODE = '55000';
1612
+ END IF;
1613
+
1614
+ prior_source := function_source;
1615
+ function_source := replace(
1616
+ function_source,
1617
+ 'IF session_row.variable_set_id IS DISTINCT FROM p_variable_set_id',
1618
+ 'IF NOT coalesce(session_row.variable_set_ids, ''[]''::jsonb) ? p_variable_set_id::text'
1619
+ );
1620
+ IF function_source = prior_source THEN
1621
+ RAISE EXCEPTION '0352 could not extend the scheduled exact-session selection check'
1622
+ USING ERRCODE = '55000';
1623
+ END IF;
1624
+
1625
+ EXECUTE pg_catalog.format(
1626
+ 'CREATE OR REPLACE FUNCTION %I.scheduled_variable_set_expected_generation_for_attempt('
1627
+ || 'p_account_id uuid, p_workspace_id uuid, p_session_id uuid, p_turn_id uuid, '
1628
+ || 'p_attempt_id uuid, p_execution_generation integer, p_variable_set_id uuid) '
1629
+ || 'RETURNS bigint LANGUAGE plpgsql SECURITY DEFINER '
1630
+ || 'SET search_path TO pg_catalog, %I, pg_temp AS %L',
1631
+ data_schema, data_schema, function_source
1632
+ );
1633
+ END
1634
+ $extend_scheduled_variable_set_generation_oracle$;
1635
+
1636
+ DO $grants$
1637
+ DECLARE
1638
+ data_schema text := current_schema();
1639
+ configured_roles jsonb := current_setting(
1640
+ 'opengeni.migration_application_roles', false
1641
+ )::jsonb;
1642
+ application_role text;
1643
+ BEGIN
1644
+ FOR application_role IN
1645
+ SELECT roles.role_name
1646
+ FROM jsonb_array_elements_text(configured_roles) roles(role_name)
1647
+ JOIN pg_catalog.pg_roles role_value ON role_value.rolname = roles.role_name
1648
+ LOOP
1649
+ EXECUTE pg_catalog.format(
1650
+ 'REVOKE ALL ON TABLE %I.session_variable_set_attachments FROM %I',
1651
+ data_schema,
1652
+ application_role
1653
+ );
1654
+ END LOOP;
1655
+ END
1656
+ $grants$;
1657
+
1658
+ COMMENT ON COLUMN sessions.variable_set_ids IS
1659
+ 'Ordered low-to-high precedence Variable Set ids; the final entry is mirrored in legacy variable_set_id.';
1660
+ COMMENT ON TABLE session_variable_set_attachments IS
1661
+ 'FK-backed lifecycle-only projection of ordered session Variable Set selection; values never appear here.';