@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.
- package/dist/canonical-human-identities.js +3 -3
- package/dist/{chunk-SIBYBA2D.js → chunk-6DPJA36C.js} +30 -11
- package/dist/chunk-6DPJA36C.js.map +1 -0
- package/dist/{chunk-KRLPEMLH.js → chunk-C2AFP7SB.js} +184 -6
- package/dist/chunk-C2AFP7SB.js.map +1 -0
- package/dist/{chunk-AZHW7R5V.js → chunk-H7MXYG7R.js} +12 -8
- package/dist/chunk-H7MXYG7R.js.map +1 -0
- package/dist/{chunk-2PRS4J5Q.js → chunk-HY3F5Q4E.js} +3 -3
- package/dist/{chunk-NAB6FKFN.js → chunk-O6FTELKH.js} +3 -3
- package/dist/{chunk-UZNXG44K.js → chunk-PYMY3AJW.js} +324 -8
- package/dist/chunk-PYMY3AJW.js.map +1 -0
- package/dist/{chunk-6HMM2R5T.js → chunk-TRBOWUG3.js} +98 -4
- package/dist/chunk-TRBOWUG3.js.map +1 -0
- package/dist/{chunk-E6CLMKEE.js → chunk-V3TRUXFJ.js} +2 -2
- package/dist/{chunk-2EH4WRHL.js → chunk-ZWRIPCTR.js} +2 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation.d.ts +16 -0
- package/dist/index.d.ts +108 -7
- package/dist/index.js +3605 -1417
- package/dist/index.js.map +1 -1
- package/dist/insights-model-bundle.d.ts +30 -0
- package/dist/insights-usage-bundle.d.ts +32 -0
- package/dist/insights.d.ts +10 -2
- package/dist/lossless-json.d.ts +1 -1
- package/dist/managed-user-setup.d.ts +82 -0
- package/dist/memory-domain.d.ts +2 -2
- package/dist/migrate.d.ts +3 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +4 -4
- package/dist/organization-membership-lifecycle.d.ts +60 -1
- package/dist/pr-review.d.ts +44 -3
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +12 -7
- package/dist/schema.d.ts +839 -101
- package/dist/schema.js +9 -1
- package/dist/session-background-commands.js +3 -3
- package/dist/session-tenancy.d.ts +12 -1
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/drizzle/0348_named_signup_and_user_setup.sql +838 -0
- package/drizzle/0349_greenfield_session_tenancy_activation.sql +792 -0
- package/drizzle/0350_organization_shared_workspace_administration.sql +889 -0
- package/drizzle/0351_organization_user_setup_delivery.sql +978 -0
- package/drizzle/0352_session_variable_set_attachments.sql +1661 -0
- package/drizzle/0353_automatic_session_title_policy_fence.sql +476 -0
- package/drizzle/0354_automatic_session_title_quarantine_index.sql +9 -0
- package/drizzle/0355_automatic_session_title_quarantine.sql +92 -0
- package/drizzle/0356_set_based_insights_session_visibility.sql +237 -0
- package/drizzle/0357_rig_platform_base_only.sql +44 -0
- package/drizzle/0358_pr_review_managed_github_app.sql +108 -0
- package/drizzle/0359_insights_force_rls_read_capability.sql +936 -0
- package/drizzle/0360_organization_identity_confirmation_prompt.sql +103 -0
- package/drizzle/0361_remember_knowledge_memory_materialization.sql +249 -0
- package/package.json +6 -6
- package/src/database.ts +12 -8
- package/src/governed-learning-activation.ts +74 -2
- package/src/index.ts +879 -92
- package/src/insights-model-bundle.ts +681 -0
- package/src/insights-usage-bundle.ts +240 -0
- package/src/insights.ts +265 -152
- package/src/lossless-json.ts +2 -1
- package/src/managed-user-setup.ts +316 -0
- package/src/memory-domain.ts +3 -3
- package/src/migrate.ts +38 -14
- package/src/new-session-drafts.ts +22 -15
- package/src/organization-membership-lifecycle.ts +253 -1
- package/src/pr-review.ts +449 -6
- package/src/provision-roles.ts +92 -0
- package/src/runtime-posture.ts +247 -4
- package/src/schema.ts +210 -4
- package/src/session-tenancy.ts +120 -2
- package/dist/chunk-6HMM2R5T.js.map +0 -1
- package/dist/chunk-AZHW7R5V.js.map +0 -1
- package/dist/chunk-KRLPEMLH.js.map +0 -1
- package/dist/chunk-SIBYBA2D.js.map +0 -1
- package/dist/chunk-UZNXG44K.js.map +0 -1
- /package/dist/{chunk-2PRS4J5Q.js.map → chunk-HY3F5Q4E.js.map} +0 -0
- /package/dist/{chunk-NAB6FKFN.js.map → chunk-O6FTELKH.js.map} +0 -0
- /package/dist/{chunk-E6CLMKEE.js.map → chunk-V3TRUXFJ.js.map} +0 -0
- /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;
|