@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,476 @@
1
+ -- deployment-mode: rolling
2
+ -- Automatic session titles are normalized in application code. Trigger DDL is
3
+ -- committed before the separately batched quarantine starts, so no heavyweight
4
+ -- table lock is held across a row scan. Unfenced old UPDATE writers are converted
5
+ -- to a no-op rather than raising, so a pre-policy scheduler still reaches
6
+ -- scheduled-occurrence delivery. Unfenced old create/fork/clone INSERTs receive
7
+ -- the neutral fallback.
8
+
9
+ SET LOCAL lock_timeout = '5s';
10
+
11
+ DROP TRIGGER IF EXISTS sessions_automatic_title_policy_v1_fence ON sessions;
12
+
13
+ -- FORCE RLS also binds the non-superuser table owner used by production
14
+ -- migrations. This persistent policy opens only an exact owner + transaction-
15
+ -- local capability seam; ordinary application roles cannot activate it. The
16
+ -- following migrations use that seam in one bounded statement per transaction.
17
+ DROP POLICY IF EXISTS sessions_automatic_title_quarantine_v1 ON sessions;
18
+ CREATE POLICY sessions_automatic_title_quarantine_v1 ON sessions
19
+ FOR ALL
20
+ USING (
21
+ current_user = (
22
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
23
+ FROM pg_catalog.pg_class relation
24
+ WHERE relation.oid = 'sessions'::regclass
25
+ )
26
+ AND pg_catalog.current_setting(
27
+ 'opengeni.automatic_session_title_quarantine_v1',
28
+ true
29
+ ) = '1'
30
+ )
31
+ WITH CHECK (
32
+ current_user = (
33
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
34
+ FROM pg_catalog.pg_class relation
35
+ WHERE relation.oid = 'sessions'::regclass
36
+ )
37
+ AND pg_catalog.current_setting(
38
+ 'opengeni.automatic_session_title_quarantine_v1',
39
+ true
40
+ ) = '1'
41
+ );
42
+
43
+ -- The quarantine advances the authoritative session sequence and appends a
44
+ -- safe title event in the same transaction. Give that INSERT the identical
45
+ -- owner/capability fence so rolling clients receive a superseding projection.
46
+ DROP POLICY IF EXISTS session_events_automatic_title_quarantine_v1 ON session_events;
47
+ CREATE POLICY session_events_automatic_title_quarantine_v1 ON session_events
48
+ FOR ALL
49
+ USING (
50
+ current_user = (
51
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
52
+ FROM pg_catalog.pg_class relation
53
+ WHERE relation.oid = 'session_events'::regclass
54
+ )
55
+ AND pg_catalog.current_setting(
56
+ 'opengeni.automatic_session_title_quarantine_v1',
57
+ true
58
+ ) = '1'
59
+ )
60
+ WITH CHECK (
61
+ current_user = (
62
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
63
+ FROM pg_catalog.pg_class relation
64
+ WHERE relation.oid = 'session_events'::regclass
65
+ )
66
+ AND pg_catalog.current_setting(
67
+ 'opengeni.automatic_session_title_quarantine_v1',
68
+ true
69
+ ) = '1'
70
+ );
71
+
72
+ -- Migration-created title events have no application process available to
73
+ -- perform the ordinary post-commit NATS publish. Persist that obligation next
74
+ -- to the event so the existing deployment-wide workflow-wake dispatcher can
75
+ -- fan it out after any migration/worker crash. This is deliberately one
76
+ -- bounded global outbox, not one durable poll per open SSE connection.
77
+ CREATE TABLE opengeni_private.automatic_session_title_fanout_outbox_v1 (
78
+ id uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid(),
79
+ account_id uuid NOT NULL,
80
+ workspace_id uuid NOT NULL,
81
+ session_id uuid NOT NULL,
82
+ event_id uuid NOT NULL,
83
+ attempts integer NOT NULL DEFAULT 0,
84
+ last_error text,
85
+ delivered_at timestamptz,
86
+ created_at timestamptz NOT NULL DEFAULT pg_catalog.now(),
87
+ updated_at timestamptz NOT NULL DEFAULT pg_catalog.now(),
88
+ CONSTRAINT automatic_title_fanout_attempts_chk CHECK (attempts >= 0),
89
+ CONSTRAINT automatic_title_fanout_workspace_account_fk
90
+ FOREIGN KEY (workspace_id, account_id)
91
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
92
+ CONSTRAINT automatic_title_fanout_workspace_session_fk
93
+ FOREIGN KEY (workspace_id, session_id)
94
+ REFERENCES sessions(workspace_id, id) ON DELETE CASCADE,
95
+ CONSTRAINT automatic_title_fanout_workspace_event_fk
96
+ FOREIGN KEY (workspace_id, event_id)
97
+ REFERENCES session_events(workspace_id, id) ON DELETE CASCADE
98
+ );
99
+
100
+ CREATE UNIQUE INDEX automatic_title_fanout_event_uq
101
+ ON opengeni_private.automatic_session_title_fanout_outbox_v1(event_id);
102
+ CREATE INDEX automatic_title_fanout_pending_idx
103
+ ON opengeni_private.automatic_session_title_fanout_outbox_v1(created_at, id)
104
+ WHERE delivered_at IS NULL;
105
+
106
+ ALTER TABLE opengeni_private.automatic_session_title_fanout_outbox_v1
107
+ ENABLE ROW LEVEL SECURITY;
108
+ ALTER TABLE opengeni_private.automatic_session_title_fanout_outbox_v1
109
+ FORCE ROW LEVEL SECURITY;
110
+ CREATE POLICY automatic_title_fanout_owner_v1
111
+ ON opengeni_private.automatic_session_title_fanout_outbox_v1
112
+ FOR ALL
113
+ USING (
114
+ current_user = (
115
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
116
+ FROM pg_catalog.pg_class relation
117
+ WHERE relation.oid =
118
+ 'opengeni_private.automatic_session_title_fanout_outbox_v1'::regclass
119
+ )
120
+ )
121
+ WITH CHECK (
122
+ current_user = (
123
+ SELECT pg_catalog.pg_get_userbyid(relation.relowner)
124
+ FROM pg_catalog.pg_class relation
125
+ WHERE relation.oid =
126
+ 'opengeni_private.automatic_session_title_fanout_outbox_v1'::regclass
127
+ )
128
+ );
129
+
130
+ REVOKE ALL ON TABLE
131
+ opengeni_private.automatic_session_title_fanout_outbox_v1
132
+ FROM PUBLIC;
133
+
134
+ -- Keep the batched backfill as one WITH statement while preserving the old
135
+ -- runtime posture's all-private-routines EXECUTE rule. This helper is
136
+ -- deliberately SECURITY INVOKER: the application role may execute it but the
137
+ -- table ACL and owner-only FORCE-RLS policy deny its INSERT, while the
138
+ -- non-superuser table owner running migration 0355 can enqueue the exact event
139
+ -- identity.
140
+ CREATE OR REPLACE FUNCTION opengeni_private.enqueue_automatic_session_title_fanout_v1(
141
+ p_account_id uuid,
142
+ p_workspace_id uuid,
143
+ p_session_id uuid,
144
+ p_event_id uuid
145
+ )
146
+ RETURNS boolean
147
+ LANGUAGE sql
148
+ SECURITY INVOKER
149
+ SET search_path FROM CURRENT
150
+ AS $automatic_title_fanout_enqueue$
151
+ WITH enqueued AS (
152
+ INSERT INTO opengeni_private.automatic_session_title_fanout_outbox_v1 (
153
+ account_id,
154
+ workspace_id,
155
+ session_id,
156
+ event_id
157
+ ) VALUES (
158
+ p_account_id,
159
+ p_workspace_id,
160
+ p_session_id,
161
+ p_event_id
162
+ )
163
+ ON CONFLICT (event_id) DO NOTHING
164
+ RETURNING true AS changed
165
+ )
166
+ SELECT coalesce((SELECT changed FROM enqueued), false);
167
+ $automatic_title_fanout_enqueue$;
168
+
169
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_automatic_session_title_fanout_v1(
170
+ p_limit integer
171
+ )
172
+ RETURNS TABLE (
173
+ outbox_id uuid,
174
+ account_id uuid,
175
+ workspace_id uuid,
176
+ session_id uuid,
177
+ event_id uuid,
178
+ sequence integer,
179
+ type text,
180
+ payload jsonb,
181
+ payload_codec_version integer,
182
+ occurred_at timestamptz,
183
+ client_event_id text,
184
+ turn_id uuid,
185
+ turn_generation integer,
186
+ turn_attempt_id uuid,
187
+ turn_association text,
188
+ duplicate_of_event_id uuid,
189
+ duplicate_reason text
190
+ )
191
+ LANGUAGE sql
192
+ SECURITY DEFINER
193
+ SET search_path FROM CURRENT
194
+ AS $automatic_title_fanout_claim$
195
+ WITH candidates AS MATERIALIZED (
196
+ SELECT pending.id
197
+ FROM opengeni_private.automatic_session_title_fanout_outbox_v1 pending
198
+ WHERE pending.delivered_at IS NULL
199
+ ORDER BY pending.created_at, pending.id
200
+ FOR UPDATE SKIP LOCKED
201
+ LIMIT greatest(1, least(coalesce(p_limit, 100), 1000))
202
+ ),
203
+ claimed AS (
204
+ UPDATE opengeni_private.automatic_session_title_fanout_outbox_v1 pending
205
+ SET attempts = pending.attempts + 1,
206
+ updated_at = pg_catalog.clock_timestamp()
207
+ FROM candidates
208
+ WHERE pending.id = candidates.id
209
+ AND pending.delivered_at IS NULL
210
+ RETURNING pending.id, pending.event_id
211
+ )
212
+ SELECT
213
+ claimed.id,
214
+ event.account_id,
215
+ event.workspace_id,
216
+ event.session_id,
217
+ event.id,
218
+ event.sequence,
219
+ event.type,
220
+ event.payload,
221
+ event.payload_codec_version,
222
+ event.occurred_at,
223
+ event.client_event_id,
224
+ event.turn_id,
225
+ event.turn_generation,
226
+ event.turn_attempt_id,
227
+ event.turn_association,
228
+ event.duplicate_of_event_id,
229
+ event.duplicate_reason
230
+ FROM claimed
231
+ JOIN session_events event ON event.id = claimed.event_id
232
+ ORDER BY event.workspace_id, event.session_id, event.sequence;
233
+ $automatic_title_fanout_claim$;
234
+
235
+ CREATE OR REPLACE FUNCTION opengeni_private.mark_automatic_session_title_fanout_delivered_v1(
236
+ p_outbox_id uuid,
237
+ p_event_id uuid
238
+ )
239
+ RETURNS boolean
240
+ LANGUAGE sql
241
+ SECURITY DEFINER
242
+ SET search_path FROM CURRENT
243
+ AS $automatic_title_fanout_delivered$
244
+ WITH delivered AS (
245
+ UPDATE opengeni_private.automatic_session_title_fanout_outbox_v1 pending
246
+ SET delivered_at = pg_catalog.clock_timestamp(),
247
+ last_error = NULL,
248
+ updated_at = pg_catalog.clock_timestamp()
249
+ WHERE pending.id = p_outbox_id
250
+ AND pending.event_id = p_event_id
251
+ AND pending.delivered_at IS NULL
252
+ RETURNING true AS changed
253
+ )
254
+ SELECT coalesce((SELECT changed FROM delivered), false);
255
+ $automatic_title_fanout_delivered$;
256
+
257
+ CREATE OR REPLACE FUNCTION opengeni_private.mark_automatic_session_title_fanout_failed_v1(
258
+ p_outbox_id uuid,
259
+ p_event_id uuid,
260
+ p_error text
261
+ )
262
+ RETURNS boolean
263
+ LANGUAGE sql
264
+ SECURITY DEFINER
265
+ SET search_path FROM CURRENT
266
+ AS $automatic_title_fanout_failed$
267
+ WITH failed AS (
268
+ UPDATE opengeni_private.automatic_session_title_fanout_outbox_v1 pending
269
+ SET last_error = pg_catalog.left(coalesce(p_error, 'unknown failure'), 500),
270
+ updated_at = pg_catalog.clock_timestamp()
271
+ WHERE pending.id = p_outbox_id
272
+ AND pending.event_id = p_event_id
273
+ AND pending.delivered_at IS NULL
274
+ RETURNING true AS changed
275
+ )
276
+ SELECT coalesce((SELECT changed FROM failed), false);
277
+ $automatic_title_fanout_failed$;
278
+
279
+ REVOKE ALL ON FUNCTION
280
+ opengeni_private.enqueue_automatic_session_title_fanout_v1(uuid, uuid, uuid, uuid)
281
+ FROM PUBLIC;
282
+ REVOKE ALL ON FUNCTION
283
+ opengeni_private.claim_automatic_session_title_fanout_v1(integer)
284
+ FROM PUBLIC;
285
+ REVOKE ALL ON FUNCTION
286
+ opengeni_private.mark_automatic_session_title_fanout_delivered_v1(uuid, uuid)
287
+ FROM PUBLIC;
288
+ REVOKE ALL ON FUNCTION
289
+ opengeni_private.mark_automatic_session_title_fanout_failed_v1(uuid, uuid, text)
290
+ FROM PUBLIC;
291
+
292
+ -- The 0345 session-tenancy trigger requires every non-superuser migration
293
+ -- owner to hold the affected workspace fence before PostgreSQL takes a session
294
+ -- row lock. Discover only the workspaces represented by the next bounded id
295
+ -- batch, then acquire their shared fences in canonical UUID order. Return the
296
+ -- exact locked set so the later row-locking query cannot race into a newly
297
+ -- eligible workspace that this invocation did not fence.
298
+ DROP FUNCTION IF EXISTS acquire_automatic_session_title_quarantine_fences_v1(integer);
299
+ CREATE FUNCTION acquire_automatic_session_title_quarantine_fences_v1(
300
+ p_batch_size integer
301
+ )
302
+ RETURNS uuid[]
303
+ LANGUAGE plpgsql
304
+ SECURITY DEFINER
305
+ SET search_path FROM CURRENT
306
+ AS $automatic_title_quarantine_fences$
307
+ DECLARE
308
+ workspace_id_value uuid;
309
+ workspace_ids uuid[] := ARRAY[]::uuid[];
310
+ BEGIN
311
+ IF p_batch_size IS NULL OR p_batch_size <= 0 THEN
312
+ RETURN workspace_ids;
313
+ END IF;
314
+
315
+ PERFORM pg_catalog.set_config(
316
+ 'opengeni.automatic_session_title_quarantine_v1',
317
+ '1',
318
+ true
319
+ );
320
+ FOR workspace_id_value IN
321
+ SELECT DISTINCT bounded.workspace_id
322
+ FROM (
323
+ SELECT session.id, session.workspace_id
324
+ FROM sessions session
325
+ WHERE session.title_source IS DISTINCT FROM 'user'
326
+ AND (
327
+ session.title IS DISTINCT FROM 'New conversation'
328
+ OR session.title_source IS DISTINCT FROM 'agent'
329
+ )
330
+ ORDER BY session.id
331
+ LIMIT p_batch_size
332
+ ) bounded
333
+ WHERE bounded.workspace_id IS NOT NULL
334
+ ORDER BY bounded.workspace_id
335
+ LOOP
336
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
337
+ workspace_ids := pg_catalog.array_append(workspace_ids, workspace_id_value);
338
+ END LOOP;
339
+
340
+ RETURN workspace_ids;
341
+ END
342
+ $automatic_title_quarantine_fences$;
343
+
344
+ REVOKE ALL ON FUNCTION
345
+ acquire_automatic_session_title_quarantine_fences_v1(integer)
346
+ FROM PUBLIC;
347
+
348
+ CREATE OR REPLACE FUNCTION opengeni_private.enforce_automatic_session_title_policy_v1()
349
+ RETURNS trigger
350
+ LANGUAGE plpgsql
351
+ SET search_path = pg_catalog
352
+ AS $$
353
+ DECLARE
354
+ candidate text := nullif(
355
+ pg_catalog.current_setting('opengeni.automatic_session_title_v1_candidate', true),
356
+ ''
357
+ );
358
+ BEGIN
359
+ IF TG_OP = 'INSERT'
360
+ AND NEW.title_source IS DISTINCT FROM 'user'
361
+ AND (
362
+ NEW.title IS DISTINCT FROM 'New conversation'
363
+ OR NEW.title_source IS DISTINCT FROM 'agent'
364
+ )
365
+ AND (candidate IS NULL OR candidate IS DISTINCT FROM NEW.title)
366
+ THEN
367
+ -- Old create/fork/clone paths remain rolling- and rollback-compatible, but
368
+ -- no unversioned automatic title crosses the new display boundary.
369
+ NEW.title := 'New conversation';
370
+ NEW.title_source := 'agent';
371
+ ELSIF TG_OP = 'UPDATE'
372
+ AND (OLD.title IS DISTINCT FROM NEW.title
373
+ OR OLD.title_source IS DISTINCT FROM NEW.title_source)
374
+ AND NEW.title_source = 'agent'
375
+ AND (NEW.title IS DISTINCT FROM 'New conversation' OR OLD.title_source = 'user')
376
+ AND (candidate IS NULL OR candidate IS DISTINCT FROM NEW.title)
377
+ THEN
378
+ -- Cancel the row operation instead of returning a rewritten success. The
379
+ -- pre-policy helper therefore receives no UPDATE ... RETURNING row, reports
380
+ -- updated=false, and skips its separate title-event transaction. Its caller
381
+ -- still continues scheduled dispatch, while a newer title can no longer be
382
+ -- followed by a higher-sequence stale legacy event.
383
+ RETURN NULL;
384
+ END IF;
385
+
386
+ RETURN NEW;
387
+ END;
388
+ $$;
389
+
390
+ REVOKE ALL ON FUNCTION opengeni_private.enforce_automatic_session_title_policy_v1()
391
+ FROM PUBLIC;
392
+
393
+ -- Pre-policy API and worker binaries require EXECUTE on every non-artifact
394
+ -- opengeni_private routine during startup/readiness. Preserve that rolling and
395
+ -- rollback posture for every exact runtime login supplied to the migration
396
+ -- runner. PUBLIC remains revoked, the app roles receive no direct outbox table
397
+ -- privileges, the enqueue helper remains SECURITY INVOKER, and PostgreSQL
398
+ -- trigger functions cannot be called as ordinary functions.
399
+ DO $automatic_title_rolling_compatibility_grants$
400
+ DECLARE
401
+ configured_roles_text text := nullif(
402
+ pg_catalog.current_setting('opengeni.migration_application_roles', true), ''
403
+ );
404
+ configured_roles jsonb;
405
+ role_name text;
406
+ routine_signature text;
407
+ BEGIN
408
+ IF configured_roles_text IS NULL THEN
409
+ RAISE EXCEPTION
410
+ '0353 automatic session title policy requires an explicit application database role list'
411
+ USING ERRCODE = '55000';
412
+ END IF;
413
+ BEGIN
414
+ configured_roles := configured_roles_text::jsonb;
415
+ EXCEPTION WHEN OTHERS THEN
416
+ RAISE EXCEPTION
417
+ '0353 automatic session title policy received a malformed application database role list'
418
+ USING ERRCODE = '55000';
419
+ END;
420
+ IF pg_catalog.jsonb_typeof(configured_roles) <> 'array'
421
+ OR pg_catalog.jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
422
+ OR EXISTS (
423
+ SELECT 1
424
+ FROM pg_catalog.jsonb_array_elements(configured_roles) AS roles(value)
425
+ WHERE pg_catalog.jsonb_typeof(value) <> 'string'
426
+ OR pg_catalog.btrim(value #>> '{}') = ''
427
+ OR pg_catalog.octet_length(value #>> '{}') > 63
428
+ )
429
+ OR (
430
+ SELECT pg_catalog.count(*)
431
+ FROM pg_catalog.jsonb_array_elements_text(configured_roles)
432
+ ) <> (
433
+ SELECT pg_catalog.count(DISTINCT value)
434
+ FROM pg_catalog.jsonb_array_elements_text(configured_roles) AS roles(value)
435
+ )
436
+ THEN
437
+ RAISE EXCEPTION
438
+ '0353 automatic session title policy received an invalid application database role list'
439
+ USING ERRCODE = '55000';
440
+ END IF;
441
+
442
+ FOR role_name IN
443
+ SELECT value
444
+ FROM pg_catalog.jsonb_array_elements_text(configured_roles) AS roles(value)
445
+ ORDER BY value COLLATE "C"
446
+ LOOP
447
+ IF EXISTS (
448
+ SELECT 1 FROM pg_catalog.pg_roles role_row WHERE role_row.rolname = role_name
449
+ ) THEN
450
+ EXECUTE pg_catalog.format(
451
+ 'REVOKE ALL PRIVILEGES ON TABLE opengeni_private.automatic_session_title_fanout_outbox_v1 FROM %I',
452
+ role_name
453
+ );
454
+ FOREACH routine_signature IN ARRAY ARRAY[
455
+ 'enqueue_automatic_session_title_fanout_v1(uuid, uuid, uuid, uuid)',
456
+ 'claim_automatic_session_title_fanout_v1(integer)',
457
+ 'mark_automatic_session_title_fanout_delivered_v1(uuid, uuid)',
458
+ 'mark_automatic_session_title_fanout_failed_v1(uuid, uuid, text)',
459
+ 'enforce_automatic_session_title_policy_v1()'
460
+ ]
461
+ LOOP
462
+ EXECUTE pg_catalog.format(
463
+ 'GRANT EXECUTE ON FUNCTION opengeni_private.%s TO %I',
464
+ routine_signature,
465
+ role_name
466
+ );
467
+ END LOOP;
468
+ END IF;
469
+ END LOOP;
470
+ END
471
+ $automatic_title_rolling_compatibility_grants$;
472
+
473
+ CREATE TRIGGER sessions_automatic_title_policy_v1_fence
474
+ BEFORE INSERT OR UPDATE OF title, title_source ON sessions
475
+ FOR EACH ROW
476
+ EXECUTE FUNCTION opengeni_private.enforce_automatic_session_title_policy_v1();
@@ -0,0 +1,9 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS sessions_automatic_title_quarantine_v1_idx
4
+ ON sessions (id)
5
+ WHERE title_source IS DISTINCT FROM 'user'
6
+ AND (
7
+ title IS DISTINCT FROM 'New conversation'
8
+ OR title_source IS DISTINCT FROM 'agent'
9
+ );
@@ -0,0 +1,92 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:batched-backfill batch-size=500 lock-timeout=1s statement-timeout=10s
3
+ -- Existing non-user titles predate the normalization policy and have no durable
4
+ -- proof that they are safe. Preserve explicit human edits and quarantine the
5
+ -- legacy rows in independently committed batches. The preceding concurrent
6
+ -- partial index keeps every batch and the final empty probe bounded to the
7
+ -- remaining unsafe rows instead of repeatedly scanning the cleaned prefix. A
8
+ -- contended writer aborts a batch quickly and leaves the migration retryable;
9
+ -- ordinary readers remain compatible with the row-level locks throughout.
10
+ WITH settings AS MATERIALIZED (
11
+ SELECT 500::integer AS batch_size
12
+ ),
13
+ quarantine_capability AS MATERIALIZED (
14
+ SELECT pg_catalog.set_config(
15
+ 'opengeni.automatic_session_title_quarantine_v1',
16
+ '1',
17
+ true
18
+ ) AS enabled
19
+ ),
20
+ quarantine_scope AS MATERIALIZED (
21
+ SELECT acquire_automatic_session_title_quarantine_fences_v1(
22
+ settings.batch_size
23
+ ) AS workspace_ids
24
+ FROM settings
25
+ CROSS JOIN quarantine_capability capability
26
+ WHERE capability.enabled = '1'
27
+ ),
28
+ candidates AS MATERIALIZED (
29
+ SELECT session.id
30
+ FROM sessions session
31
+ CROSS JOIN quarantine_scope scope
32
+ WHERE session.workspace_id = ANY(scope.workspace_ids)
33
+ AND session.title_source IS DISTINCT FROM 'user'
34
+ AND (
35
+ session.title IS DISTINCT FROM 'New conversation'
36
+ OR session.title_source IS DISTINCT FROM 'agent'
37
+ )
38
+ ORDER BY session.id
39
+ LIMIT 500
40
+ FOR UPDATE OF session
41
+ ),
42
+ quarantined AS (
43
+ UPDATE sessions session
44
+ SET title = 'New conversation',
45
+ title_source = 'agent',
46
+ last_sequence = session.last_sequence + 1
47
+ FROM candidates
48
+ WHERE session.id = candidates.id
49
+ RETURNING
50
+ session.id,
51
+ session.account_id,
52
+ session.workspace_id,
53
+ session.last_sequence
54
+ ),
55
+ inserted_events AS (
56
+ INSERT INTO session_events (
57
+ account_id,
58
+ workspace_id,
59
+ session_id,
60
+ sequence,
61
+ type,
62
+ payload,
63
+ occurred_at
64
+ )
65
+ SELECT
66
+ quarantined.account_id,
67
+ quarantined.workspace_id,
68
+ quarantined.id,
69
+ quarantined.last_sequence,
70
+ 'session.title_set',
71
+ pg_catalog.jsonb_build_object(
72
+ 'title', 'New conversation',
73
+ 'source', 'agent'
74
+ ),
75
+ pg_catalog.clock_timestamp()
76
+ FROM quarantined
77
+ RETURNING id, account_id, workspace_id, session_id
78
+ ),
79
+ enqueued AS (
80
+ SELECT
81
+ inserted.session_id,
82
+ opengeni_private.enqueue_automatic_session_title_fanout_v1(
83
+ inserted.account_id,
84
+ inserted.workspace_id,
85
+ inserted.session_id,
86
+ inserted.id
87
+ ) AS accepted
88
+ FROM inserted_events inserted
89
+ )
90
+ SELECT session_id AS id
91
+ FROM enqueued
92
+ WHERE accepted;