@opengeni/db 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
  2. package/dist/chunk-7LDU7F5P.js.map +1 -0
  3. package/dist/chunk-B22X3IEZ.js +2634 -0
  4. package/dist/chunk-B22X3IEZ.js.map +1 -0
  5. package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
  6. package/dist/chunk-YFQ7SGE4.js.map +1 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +16571 -5018
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.js +1 -1
  11. package/dist/provision-roles.d.ts +2121 -248
  12. package/dist/provision-roles.js +1 -1
  13. package/dist/{schema-BUbuMteO.d.ts → schema-BN5mB9xZ.d.ts} +8716 -3432
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +43 -5
  16. package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
  17. package/drizzle/0045_workspace_captures.sql +83 -0
  18. package/drizzle/0045_workspace_memory_v1.sql +71 -0
  19. package/drizzle/0046_variable_sets_rename.sql +56 -0
  20. package/drizzle/0047_rigs.sql +151 -0
  21. package/drizzle/0048_rig_runtime.sql +9 -0
  22. package/drizzle/0049_enrollment_went_offline.sql +28 -0
  23. package/drizzle/0050_enrollment_op_stream.sql +1 -0
  24. package/drizzle/0051_codex_pin_source.sql +48 -0
  25. package/drizzle/0052_file_upload_cleanup.sql +91 -0
  26. package/drizzle/0053_codex_credential_leases.sql +230 -0
  27. package/drizzle/0054_session_pins.sql +85 -0
  28. package/drizzle/0055_session_list_snapshots.sql +73 -0
  29. package/drizzle/0056_workspace_model_policies.sql +48 -0
  30. package/drizzle/0057_durable_queue_control.sql +536 -0
  31. package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
  32. package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
  33. package/drizzle/0060_session_system_update_deferral.sql +10 -0
  34. package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
  35. package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
  36. package/drizzle/0063_session_control_mega_foundation.sql +1324 -0
  37. package/package.json +13 -13
  38. package/src/codex-token-resolver.ts +58 -23
  39. package/src/connection-token-resolver.ts +146 -57
  40. package/src/environment-crypto.ts +5 -1
  41. package/src/event-payload-sanitizer.ts +29 -1
  42. package/src/index.ts +20990 -6465
  43. package/src/memory-domain.ts +218 -0
  44. package/src/migrate.ts +58 -3
  45. package/src/provision-roles.ts +46 -17
  46. package/src/schema.ts +2888 -1121
  47. package/src/session-control.ts +1759 -0
  48. package/src/session-queue-commands.ts +1753 -0
  49. package/src/session-tool-call-settlement.ts +269 -0
  50. package/dist/chunk-57MLICFR.js.map +0 -1
  51. package/dist/chunk-OGCE6O2X.js.map +0 -1
  52. package/dist/chunk-ZIUCA2IO.js +0 -1268
  53. package/dist/chunk-ZIUCA2IO.js.map +0 -1
@@ -0,0 +1,1324 @@
1
+ -- deployment-mode: maintenance
2
+ -- OPE-18/OPE-54: one-way queue/control maintenance cutover. Old workers are
3
+ -- drained before this migration; obsolete columns are removed in this same
4
+ -- transaction so no mixed runtime architecture can survive deployment.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '30min';
8
+
9
+ -- Queue Edit has a distinct terminal fate: it atomically checks the prompt out
10
+ -- into the private composer rather than pretending the human deleted it.
11
+ ALTER TABLE "session_turns" DROP CONSTRAINT "session_turns_status_check";
12
+ ALTER TABLE "session_turns" ADD CONSTRAINT "session_turns_status_check"
13
+ CHECK ("status" IN (
14
+ 'queued','running','requires_action','recovering','waiting_capacity',
15
+ 'completed','failed','cancelled','superseded','withdrawn_for_edit'
16
+ ));
17
+
18
+ -- Parentage is immutable, workspace-scoped, and structurally complete. Fail
19
+ -- before changing the FK if historical data violates the new ownership model.
20
+ DO $audit$
21
+ BEGIN
22
+ IF EXISTS (
23
+ SELECT 1
24
+ FROM "sessions" child
25
+ JOIN "sessions" parent ON parent."id" = child."parent_session_id"
26
+ WHERE child."parent_session_id" IS NOT NULL
27
+ AND child."workspace_id" <> parent."workspace_id"
28
+ ) THEN
29
+ RAISE EXCEPTION 'session-control cutover: cross-workspace parent link';
30
+ END IF;
31
+ IF EXISTS (
32
+ SELECT 1 FROM "sessions" WHERE "parent_session_id" = "id"
33
+ ) THEN
34
+ RAISE EXCEPTION 'session-control cutover: self-parent session';
35
+ END IF;
36
+ IF EXISTS (
37
+ WITH RECURSIVE ancestry AS (
38
+ SELECT s."workspace_id", s."id" AS origin_id, s."parent_session_id" AS next_id,
39
+ ARRAY[s."id"]::uuid[] AS path
40
+ FROM "sessions" s
41
+ WHERE s."parent_session_id" IS NOT NULL
42
+ UNION ALL
43
+ SELECT a."workspace_id", a.origin_id, parent."parent_session_id",
44
+ a.path || parent."id"
45
+ FROM ancestry a
46
+ JOIN "sessions" parent
47
+ ON parent."workspace_id" = a."workspace_id" AND parent."id" = a.next_id
48
+ WHERE a.next_id IS NOT NULL
49
+ AND NOT parent."id" = ANY(a.path)
50
+ AND cardinality(a.path) <= 10000
51
+ )
52
+ SELECT 1
53
+ FROM ancestry a
54
+ WHERE a.next_id = ANY(a.path)
55
+ ) THEN
56
+ RAISE EXCEPTION 'session-control cutover: cyclic session ancestry';
57
+ END IF;
58
+ END $audit$;
59
+
60
+ ALTER TABLE "sessions" DROP CONSTRAINT IF EXISTS "sessions_parent_session_id_fkey";
61
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_parent_not_self_check"
62
+ CHECK ("parent_session_id" IS NULL OR "parent_session_id" <> "id");
63
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_workspace_parent_fk"
64
+ FOREIGN KEY ("workspace_id", "parent_session_id")
65
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE RESTRICT;
66
+
67
+ CREATE TABLE "workspace_inference_controls" (
68
+ "workspace_id" uuid PRIMARY KEY,
69
+ "account_id" uuid NOT NULL,
70
+ "revision" bigint NOT NULL DEFAULT 0,
71
+ "workspace_state" text NOT NULL DEFAULT 'active',
72
+ "workspace_pause_revision" bigint,
73
+ "reason" text,
74
+ "changed_by" text,
75
+ "changed_at" timestamptz,
76
+ "created_at" timestamptz NOT NULL DEFAULT now(),
77
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
78
+ CONSTRAINT "workspace_inference_controls_workspace_account_fk"
79
+ FOREIGN KEY ("workspace_id", "account_id")
80
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
81
+ CONSTRAINT "workspace_inference_controls_state_check"
82
+ CHECK ("workspace_state" IN ('active', 'paused')),
83
+ CONSTRAINT "workspace_inference_controls_pause_revision_check"
84
+ CHECK (
85
+ ("workspace_state" = 'active' AND "workspace_pause_revision" IS NULL)
86
+ OR ("workspace_state" = 'paused' AND "workspace_pause_revision" IS NOT NULL)
87
+ ),
88
+ CONSTRAINT "workspace_inference_controls_revision_check"
89
+ CHECK (
90
+ "revision" >= 0
91
+ AND ("workspace_pause_revision" IS NULL OR "workspace_pause_revision" <= "revision")
92
+ )
93
+ );
94
+ CREATE UNIQUE INDEX "workspace_inference_controls_workspace_account_uq"
95
+ ON "workspace_inference_controls" ("workspace_id", "account_id");
96
+
97
+ ALTER TABLE "sessions" ADD COLUMN "direct_control_state" text NOT NULL DEFAULT 'active';
98
+ ALTER TABLE "sessions" ADD COLUMN "direct_pause_revision" bigint;
99
+ ALTER TABLE "sessions" ADD COLUMN "subtree_run_override_revision" bigint;
100
+ ALTER TABLE "sessions" ADD COLUMN "control_version" bigint NOT NULL DEFAULT 0;
101
+ ALTER TABLE "sessions" ADD COLUMN "direct_control_reason" text;
102
+ ALTER TABLE "sessions" ADD COLUMN "direct_control_changed_by" text;
103
+ ALTER TABLE "sessions" ADD COLUMN "direct_control_changed_at" timestamptz;
104
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_direct_control_state_check"
105
+ CHECK ("direct_control_state" IN ('active', 'paused'));
106
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_direct_pause_revision_check"
107
+ CHECK (
108
+ ("direct_control_state" = 'active' AND "direct_pause_revision" IS NULL)
109
+ OR ("direct_control_state" = 'paused' AND "direct_pause_revision" IS NOT NULL)
110
+ );
111
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_control_revision_order_check"
112
+ CHECK (
113
+ "control_version" >= 0
114
+ AND ("direct_pause_revision" IS NULL OR "direct_pause_revision" <= "control_version")
115
+ AND (
116
+ "subtree_run_override_revision" IS NULL
117
+ OR "subtree_run_override_revision" <= "control_version"
118
+ )
119
+ );
120
+
121
+ CREATE TABLE "session_turn_attempts" (
122
+ "id" uuid PRIMARY KEY NOT NULL,
123
+ "account_id" uuid NOT NULL,
124
+ "workspace_id" uuid NOT NULL,
125
+ "session_id" uuid NOT NULL,
126
+ "turn_id" uuid NOT NULL,
127
+ "execution_generation" integer NOT NULL,
128
+ "state" text NOT NULL DEFAULT 'claimed',
129
+ "outcome" text,
130
+ "temporal_workflow_id" text NOT NULL,
131
+ "temporal_workflow_run_id" text NOT NULL,
132
+ "temporal_activity_id" text NOT NULL,
133
+ "worker_id" text,
134
+ "lease_id" text,
135
+ "lease_expires_at" timestamptz,
136
+ "verified_control_revision" bigint NOT NULL,
137
+ "started_at" timestamptz NOT NULL DEFAULT now(),
138
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
139
+ "closed_at" timestamptz,
140
+ CONSTRAINT "session_turn_attempts_workspace_account_fk"
141
+ FOREIGN KEY ("workspace_id", "account_id")
142
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
143
+ CONSTRAINT "session_turn_attempts_workspace_session_fk"
144
+ FOREIGN KEY ("workspace_id", "session_id")
145
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE RESTRICT,
146
+ CONSTRAINT "session_turn_attempts_workspace_turn_fk"
147
+ FOREIGN KEY ("workspace_id", "turn_id")
148
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE RESTRICT,
149
+ CONSTRAINT "session_turn_attempts_state_check"
150
+ CHECK ("state" IN ('claimed', 'running', 'closed')),
151
+ CONSTRAINT "session_turn_attempts_outcome_check"
152
+ CHECK (
153
+ "outcome" IS NULL OR "outcome" IN (
154
+ 'completed', 'failed', 'cancelled', 'superseded', 'requires_action',
155
+ 'interrupted_recoverable', 'lease_lost_recoverable', 'pre_cutover_closed'
156
+ )
157
+ ),
158
+ CONSTRAINT "session_turn_attempts_closed_check"
159
+ CHECK (
160
+ ("state" = 'closed' AND "outcome" IS NOT NULL AND "closed_at" IS NOT NULL)
161
+ OR ("state" <> 'closed' AND "outcome" IS NULL AND "closed_at" IS NULL)
162
+ )
163
+ );
164
+ CREATE UNIQUE INDEX "session_turn_attempts_workspace_id_uq"
165
+ ON "session_turn_attempts" ("workspace_id", "id");
166
+ CREATE UNIQUE INDEX "session_turn_attempts_live_turn_uq"
167
+ ON "session_turn_attempts" ("workspace_id", "turn_id")
168
+ WHERE "state" IN ('claimed', 'running');
169
+ CREATE UNIQUE INDEX "session_turn_attempts_live_session_uq"
170
+ ON "session_turn_attempts" ("workspace_id", "session_id")
171
+ WHERE "state" IN ('claimed', 'running');
172
+ CREATE UNIQUE INDEX "session_turn_attempts_dispatch_uq"
173
+ ON "session_turn_attempts" ("workspace_id", "temporal_workflow_run_id", "temporal_activity_id");
174
+ CREATE INDEX "session_turn_attempts_lease_expiry_idx"
175
+ ON "session_turn_attempts" ("lease_expires_at", "workspace_id", "session_id")
176
+ WHERE "state" IN ('claimed', 'running');
177
+
178
+ -- Every historical UUID that still participates in attempt lineage becomes one
179
+ -- closed first-class attempt before any new FK is installed. The maintenance
180
+ -- drain guarantees none of these is a live owner. Conflicting reuse of one UUID
181
+ -- across two turns/generations is corruption and aborts the whole migration.
182
+ CREATE TEMP TABLE "cutover_attempt_ownership" (
183
+ "attempt_id" uuid NOT NULL,
184
+ "account_id" uuid NOT NULL,
185
+ "workspace_id" uuid NOT NULL,
186
+ "session_id" uuid NOT NULL,
187
+ "turn_id" uuid NOT NULL,
188
+ "execution_generation" integer NOT NULL,
189
+ "temporal_workflow_id" text NOT NULL,
190
+ "started_at" timestamptz
191
+ ) ON COMMIT DROP;
192
+
193
+ DO $attempt_preflight$
194
+ BEGIN
195
+ IF EXISTS (
196
+ SELECT 1
197
+ FROM "session_events" event
198
+ LEFT JOIN "session_turns" turn
199
+ ON turn."workspace_id" = event."workspace_id" AND turn."id" = event."turn_id"
200
+ WHERE event."turn_attempt_id" IS NOT NULL
201
+ AND (
202
+ turn."id" IS NULL
203
+ OR turn."session_id" <> event."session_id"
204
+ OR event."turn_generation" IS NULL
205
+ )
206
+ ) THEN
207
+ RAISE EXCEPTION 'session-control cutover: unclassifiable event attempt ownership';
208
+ END IF;
209
+ IF EXISTS (
210
+ SELECT 1
211
+ FROM "sessions" session
212
+ LEFT JOIN "session_turns" turn
213
+ ON turn."workspace_id" = session."workspace_id"
214
+ AND turn."id" = session."pending_control_expected_turn_id"
215
+ WHERE session."pending_control_expected_attempt_id" IS NOT NULL
216
+ AND (
217
+ turn."id" IS NULL
218
+ OR turn."session_id" <> session."id"
219
+ OR session."pending_control_expected_generation" IS NULL
220
+ )
221
+ ) THEN
222
+ RAISE EXCEPTION 'session-control cutover: unclassifiable pending-control attempt ownership';
223
+ END IF;
224
+ END $attempt_preflight$;
225
+
226
+ INSERT INTO "cutover_attempt_ownership"
227
+ SELECT turn."active_attempt_id", turn."account_id", turn."workspace_id", turn."session_id",
228
+ turn."id", turn."execution_generation", turn."temporal_workflow_id", turn."started_at"
229
+ FROM "session_turns" turn
230
+ WHERE turn."active_attempt_id" IS NOT NULL
231
+ UNION ALL
232
+ SELECT event."turn_attempt_id", event."account_id", event."workspace_id", event."session_id",
233
+ turn."id", event."turn_generation", turn."temporal_workflow_id", event."occurred_at"
234
+ FROM "session_events" event
235
+ JOIN "session_turns" turn
236
+ ON turn."workspace_id" = event."workspace_id" AND turn."id" = event."turn_id"
237
+ WHERE event."turn_attempt_id" IS NOT NULL
238
+ UNION ALL
239
+ SELECT call."attempt_id", call."account_id", call."workspace_id", call."session_id",
240
+ call."turn_id", call."execution_generation", turn."temporal_workflow_id", call."created_at"
241
+ FROM "session_pending_tool_calls" call
242
+ JOIN "session_turns" turn
243
+ ON turn."workspace_id" = call."workspace_id" AND turn."id" = call."turn_id"
244
+ UNION ALL
245
+ SELECT session."pending_control_expected_attempt_id", session."account_id", session."workspace_id",
246
+ session."id", turn."id", session."pending_control_expected_generation",
247
+ turn."temporal_workflow_id", session."control_changed_at"
248
+ FROM "sessions" session
249
+ JOIN "session_turns" turn
250
+ ON turn."workspace_id" = session."workspace_id"
251
+ AND turn."id" = session."pending_control_expected_turn_id"
252
+ WHERE session."pending_control_expected_attempt_id" IS NOT NULL;
253
+
254
+ DO $attempt_identity$
255
+ BEGIN
256
+ IF EXISTS (
257
+ SELECT attempt_id
258
+ FROM "cutover_attempt_ownership"
259
+ GROUP BY attempt_id
260
+ HAVING count(DISTINCT (
261
+ account_id, workspace_id, session_id, turn_id, execution_generation,
262
+ temporal_workflow_id
263
+ )) <> 1
264
+ ) THEN
265
+ RAISE EXCEPTION 'session-control cutover: one attempt UUID maps to conflicting ownership';
266
+ END IF;
267
+ END $attempt_identity$;
268
+
269
+ INSERT INTO "session_turn_attempts" (
270
+ "id", "account_id", "workspace_id", "session_id", "turn_id",
271
+ "execution_generation", "state", "outcome", "temporal_workflow_id",
272
+ "temporal_workflow_run_id", "temporal_activity_id", "verified_control_revision",
273
+ "started_at", "closed_at"
274
+ )
275
+ SELECT ownership."attempt_id", ownership."account_id", ownership."workspace_id",
276
+ ownership."session_id", ownership."turn_id", ownership."execution_generation",
277
+ 'closed', 'pre_cutover_closed', ownership."temporal_workflow_id",
278
+ 'pre-cutover:' || ownership."attempt_id"::text,
279
+ 'pre-cutover:' || ownership."attempt_id"::text, 0,
280
+ min(coalesce(ownership."started_at", now())), now()
281
+ FROM "cutover_attempt_ownership" ownership
282
+ GROUP BY ownership."attempt_id", ownership."account_id", ownership."workspace_id",
283
+ ownership."session_id", ownership."turn_id", ownership."execution_generation",
284
+ ownership."temporal_workflow_id";
285
+
286
+ -- Maintenance must have converted every executing logical turn into an
287
+ -- ownerless recovery state before schema cutover. A closed historical attempt
288
+ -- is evidence only; silently clearing ownership from a still-running turn
289
+ -- would strand it and let the new workflow misclassify the state.
290
+ DO $drained_attempts$
291
+ BEGIN
292
+ IF EXISTS (SELECT 1 FROM "session_turns" WHERE "status" = 'running') THEN
293
+ RAISE EXCEPTION 'session-control cutover: running turn survived maintenance drain';
294
+ END IF;
295
+ END $drained_attempts$;
296
+
297
+ -- A drained logical turn is not owned after the cutover. Historical events and
298
+ -- unresolved tool-call receipts keep their immutable reference to the closed
299
+ -- evidence row; a new worker creates a new attempt only when it actually claims.
300
+ UPDATE "session_turns" SET "active_attempt_id" = NULL
301
+ WHERE "active_attempt_id" IS NOT NULL;
302
+
303
+ ALTER TABLE "session_turns" ADD CONSTRAINT "session_turns_workspace_active_attempt_fk"
304
+ FOREIGN KEY ("workspace_id", "active_attempt_id")
305
+ REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT
306
+ DEFERRABLE INITIALLY DEFERRED;
307
+ ALTER TABLE "session_events" ADD CONSTRAINT "session_events_workspace_attempt_fk"
308
+ FOREIGN KEY ("workspace_id", "turn_attempt_id")
309
+ REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT
310
+ DEFERRABLE INITIALLY DEFERRED;
311
+ ALTER TABLE "session_pending_tool_calls" ADD CONSTRAINT "pending_tool_calls_workspace_attempt_fk"
312
+ FOREIGN KEY ("workspace_id", "attempt_id")
313
+ REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT
314
+ DEFERRABLE INITIALLY DEFERRED;
315
+
316
+ CREATE TABLE "session_command_receipts" (
317
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
318
+ "account_id" uuid NOT NULL,
319
+ "workspace_id" uuid NOT NULL,
320
+ "actor_type" text NOT NULL,
321
+ "actor_subject_id" text,
322
+ "actor_attempt_id" uuid,
323
+ "action" text NOT NULL,
324
+ "target_session_id" uuid,
325
+ "target_turn_id" uuid,
326
+ "operation_key" text NOT NULL,
327
+ "canonical_request_hash" text NOT NULL,
328
+ "applied_control_revision" bigint,
329
+ "applied_queue_version" integer,
330
+ "applied_turn_version" integer,
331
+ "applied_draft_revision" bigint,
332
+ "result" jsonb NOT NULL DEFAULT '{}'::jsonb,
333
+ "created_at" timestamptz NOT NULL DEFAULT now(),
334
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
335
+ CONSTRAINT "session_command_receipts_workspace_account_fk"
336
+ FOREIGN KEY ("workspace_id", "account_id")
337
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
338
+ CONSTRAINT "session_command_receipts_actor_attempt_fk"
339
+ FOREIGN KEY ("workspace_id", "actor_attempt_id")
340
+ REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT,
341
+ CONSTRAINT "session_command_receipts_target_session_fk"
342
+ FOREIGN KEY ("workspace_id", "target_session_id")
343
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE RESTRICT,
344
+ CONSTRAINT "session_command_receipts_target_turn_fk"
345
+ FOREIGN KEY ("workspace_id", "target_turn_id")
346
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE RESTRICT
347
+ DEFERRABLE INITIALLY DEFERRED,
348
+ CONSTRAINT "session_command_receipts_actor_check"
349
+ CHECK (
350
+ ("actor_type" = 'agent_attempt' AND "actor_attempt_id" IS NOT NULL
351
+ AND "actor_subject_id" IS NULL)
352
+ OR ("actor_type" IN ('human', 'operator') AND "actor_subject_id" IS NOT NULL
353
+ AND "actor_attempt_id" IS NULL)
354
+ )
355
+ );
356
+ CREATE UNIQUE INDEX "session_command_receipts_workspace_id_uq"
357
+ ON "session_command_receipts" ("workspace_id", "id");
358
+ CREATE UNIQUE INDEX "session_command_receipts_idempotency_uq"
359
+ ON "session_command_receipts" (
360
+ "workspace_id", "actor_type", "actor_subject_id", "actor_attempt_id",
361
+ "action", "target_session_id", "target_turn_id", "operation_key"
362
+ ) NULLS NOT DISTINCT;
363
+ CREATE INDEX "session_command_receipts_target_created_idx"
364
+ ON "session_command_receipts" ("workspace_id", "target_session_id", "created_at");
365
+
366
+ CREATE TABLE "workspace_control_events" (
367
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
368
+ "account_id" uuid NOT NULL,
369
+ "workspace_id" uuid NOT NULL,
370
+ "revision" bigint NOT NULL,
371
+ "scope" text NOT NULL,
372
+ "root_session_id" uuid,
373
+ "action" text NOT NULL,
374
+ "automatic" boolean NOT NULL DEFAULT false,
375
+ "reason" text,
376
+ "actor" text NOT NULL,
377
+ "occurred_at" timestamptz NOT NULL DEFAULT now(),
378
+ CONSTRAINT "workspace_control_events_workspace_account_fk"
379
+ FOREIGN KEY ("workspace_id", "account_id")
380
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
381
+ CONSTRAINT "workspace_control_events_root_session_fk"
382
+ FOREIGN KEY ("workspace_id", "root_session_id")
383
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE RESTRICT,
384
+ CONSTRAINT "workspace_control_events_revision_check" CHECK ("revision" > 0),
385
+ CONSTRAINT "workspace_control_events_shape_check" CHECK (
386
+ ("scope" = 'workspace' AND "root_session_id" IS NULL)
387
+ OR ("scope" = 'session' AND "root_session_id" IS NOT NULL)
388
+ ),
389
+ CONSTRAINT "workspace_control_events_action_check" CHECK ("action" IN ('pause', 'resume'))
390
+ );
391
+ CREATE UNIQUE INDEX "workspace_control_events_workspace_revision_uq"
392
+ ON "workspace_control_events" ("workspace_id", "revision");
393
+
394
+ CREATE TABLE "session_attempt_interruptions" (
395
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
396
+ "account_id" uuid NOT NULL,
397
+ "workspace_id" uuid NOT NULL,
398
+ "session_id" uuid NOT NULL,
399
+ "operation_id" uuid NOT NULL,
400
+ "attempt_id" uuid NOT NULL,
401
+ "kind" text NOT NULL,
402
+ "control_revision" bigint NOT NULL,
403
+ "state" text NOT NULL DEFAULT 'pending',
404
+ "requested_at" timestamptz NOT NULL DEFAULT now(),
405
+ "delivered_at" timestamptz,
406
+ "acknowledged_at" timestamptz,
407
+ "settled_at" timestamptz,
408
+ CONSTRAINT "session_attempt_interruptions_workspace_account_fk"
409
+ FOREIGN KEY ("workspace_id", "account_id")
410
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
411
+ CONSTRAINT "session_attempt_interruptions_workspace_session_fk"
412
+ FOREIGN KEY ("workspace_id", "session_id")
413
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE RESTRICT,
414
+ CONSTRAINT "session_attempt_interruptions_operation_fk"
415
+ FOREIGN KEY ("workspace_id", "operation_id")
416
+ REFERENCES "session_command_receipts"("workspace_id", "id") ON DELETE RESTRICT,
417
+ CONSTRAINT "session_attempt_interruptions_attempt_fk"
418
+ FOREIGN KEY ("workspace_id", "attempt_id")
419
+ REFERENCES "session_turn_attempts"("workspace_id", "id") ON DELETE RESTRICT,
420
+ CONSTRAINT "session_attempt_interruptions_kind_check"
421
+ CHECK ("kind" IN ('session_pause', 'workspace_pause', 'steer', 'maintenance')),
422
+ CONSTRAINT "session_attempt_interruptions_state_check"
423
+ CHECK ("state" IN ('pending', 'delivered', 'acknowledged', 'settled', 'rejected_stale')),
424
+ CONSTRAINT "session_attempt_interruptions_operation_attempt_uq"
425
+ UNIQUE ("operation_id", "attempt_id")
426
+ );
427
+ CREATE INDEX "session_attempt_interruptions_unsettled_idx"
428
+ ON "session_attempt_interruptions" ("workspace_id", "session_id", "requested_at")
429
+ WHERE "state" IN ('pending', 'delivered', 'acknowledged');
430
+
431
+ CREATE TABLE "composer_drafts" (
432
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
433
+ "account_id" uuid NOT NULL,
434
+ "workspace_id" uuid NOT NULL,
435
+ "session_id" uuid NOT NULL,
436
+ "subject_id" text NOT NULL,
437
+ "revision" bigint NOT NULL DEFAULT 1,
438
+ "text" text NOT NULL DEFAULT '',
439
+ "resources" jsonb NOT NULL DEFAULT '[]'::jsonb,
440
+ "tools" jsonb NOT NULL DEFAULT '[]'::jsonb,
441
+ "model" text NOT NULL,
442
+ "reasoning_effort" text NOT NULL,
443
+ "source_turn_id" uuid,
444
+ "source_turn_version" integer,
445
+ "created_at" timestamptz NOT NULL DEFAULT now(),
446
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
447
+ CONSTRAINT "composer_drafts_workspace_account_fk"
448
+ FOREIGN KEY ("workspace_id", "account_id")
449
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
450
+ CONSTRAINT "composer_drafts_workspace_session_fk"
451
+ FOREIGN KEY ("workspace_id", "session_id")
452
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE,
453
+ CONSTRAINT "composer_drafts_source_turn_fk"
454
+ FOREIGN KEY ("workspace_id", "source_turn_id")
455
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE RESTRICT,
456
+ CONSTRAINT "composer_drafts_subject_check" CHECK (length(btrim("subject_id")) > 0),
457
+ CONSTRAINT "composer_drafts_revision_check" CHECK ("revision" >= 1)
458
+ );
459
+ CREATE UNIQUE INDEX "composer_drafts_subject_session_uq"
460
+ ON "composer_drafts" ("workspace_id", "session_id", "subject_id");
461
+
462
+ -- Derive one deterministic per-workspace revision order for historical direct
463
+ -- barriers. There are intentionally zero migration-created subtree overrides.
464
+ WITH paused_sessions AS (
465
+ SELECT s."workspace_id", s."id",
466
+ row_number() OVER (
467
+ PARTITION BY s."workspace_id"
468
+ ORDER BY s."control_changed_at" NULLS FIRST, s."created_at", s."id"
469
+ )::bigint AS pause_revision
470
+ FROM "sessions" s
471
+ WHERE s."control_state" = 'paused'
472
+ ), workspace_seed AS (
473
+ SELECT w."id" AS workspace_id, w."account_id", w."inference_state",
474
+ w."inference_reason", w."inference_changed_by", w."inference_changed_at",
475
+ count(p."id")::bigint AS direct_pause_count
476
+ FROM "workspaces" w
477
+ LEFT JOIN paused_sessions p ON p."workspace_id" = w."id"
478
+ GROUP BY w."id", w."account_id", w."inference_state", w."inference_reason",
479
+ w."inference_changed_by", w."inference_changed_at"
480
+ )
481
+ INSERT INTO "workspace_inference_controls" (
482
+ "workspace_id", "account_id", "revision", "workspace_state",
483
+ "workspace_pause_revision", "reason", "changed_by", "changed_at"
484
+ )
485
+ SELECT workspace_id, account_id,
486
+ direct_pause_count + CASE WHEN inference_state = 'paused' THEN 1 ELSE 0 END,
487
+ inference_state,
488
+ CASE WHEN inference_state = 'paused' THEN direct_pause_count + 1 ELSE NULL END,
489
+ inference_reason, inference_changed_by, inference_changed_at
490
+ FROM workspace_seed;
491
+
492
+ WITH paused_sessions AS (
493
+ SELECT s."workspace_id", s."id",
494
+ row_number() OVER (
495
+ PARTITION BY s."workspace_id"
496
+ ORDER BY s."control_changed_at" NULLS FIRST, s."created_at", s."id"
497
+ )::bigint AS pause_revision
498
+ FROM "sessions" s
499
+ WHERE s."control_state" = 'paused'
500
+ ), session_seed AS (
501
+ SELECT s."workspace_id", s."id",
502
+ CASE WHEN p."id" IS NULL THEN 'active' ELSE 'paused' END AS direct_state,
503
+ p.pause_revision
504
+ FROM "sessions" s
505
+ LEFT JOIN paused_sessions p
506
+ ON p."workspace_id" = s."workspace_id" AND p."id" = s."id"
507
+ )
508
+ UPDATE "sessions" s
509
+ SET "direct_control_state" = seed.direct_state,
510
+ "direct_pause_revision" = seed.pause_revision,
511
+ "subtree_run_override_revision" = NULL,
512
+ "control_version" = coalesce(seed.pause_revision, 0),
513
+ "direct_control_reason" = s."control_reason",
514
+ "direct_control_changed_by" = s."control_changed_by",
515
+ "direct_control_changed_at" = s."control_changed_at"
516
+ FROM session_seed seed
517
+ WHERE s."workspace_id" = seed."workspace_id" AND s."id" = seed."id";
518
+
519
+ DO $verify$
520
+ BEGIN
521
+ IF (SELECT count(*) FROM "workspace_inference_controls") <>
522
+ (SELECT count(*) FROM "workspaces") THEN
523
+ RAISE EXCEPTION 'session-control cutover: missing workspace control row';
524
+ END IF;
525
+ IF EXISTS (
526
+ SELECT 1 FROM "sessions"
527
+ WHERE ("control_state" = 'paused') <> ("direct_control_state" = 'paused')
528
+ OR "subtree_run_override_revision" IS NOT NULL
529
+ ) THEN
530
+ RAISE EXCEPTION 'session-control cutover: direct barrier seed mismatch';
531
+ END IF;
532
+ END $verify$;
533
+
534
+ -- Record every discarded exact-session workspace exception and every intentional
535
+ -- hold-only delta without prompt or model content. Migration creates barriers
536
+ -- only: no historical exception is reinterpreted as a new branch override.
537
+ INSERT INTO "audit_events" (
538
+ "account_id", "workspace_id", "subject_id", "action", "target_type", "target_id", "metadata"
539
+ )
540
+ SELECT session."account_id", session."workspace_id", 'control-mega-migration',
541
+ 'session.control.migration.workspace_exception_dropped', 'session', session."id"::text,
542
+ jsonb_build_object(
543
+ 'oldExceptionGeneration', session."workspace_run_exception_generation",
544
+ 'oldWorkspaceGeneration', workspace."inference_generation",
545
+ 'wasCurrent',
546
+ session."workspace_run_exception_generation" = workspace."inference_generation"
547
+ )
548
+ FROM "sessions" session
549
+ JOIN "workspaces" workspace ON workspace."id" = session."workspace_id"
550
+ WHERE session."workspace_run_exception_generation" IS NOT NULL;
551
+
552
+ WITH RECURSIVE ancestry AS (
553
+ SELECT session."account_id", session."workspace_id", session."id" AS target_id,
554
+ session."id" AS ancestor_id, session."parent_session_id", 0 AS depth
555
+ FROM "sessions" session
556
+ UNION ALL
557
+ SELECT ancestry."account_id", ancestry."workspace_id", ancestry.target_id,
558
+ parent."id", parent."parent_session_id", ancestry.depth + 1
559
+ FROM ancestry
560
+ JOIN "sessions" parent
561
+ ON parent."workspace_id" = ancestry."workspace_id"
562
+ AND parent."id" = ancestry."parent_session_id"
563
+ WHERE ancestry.depth < 10000
564
+ ), fate AS (
565
+ SELECT session."account_id", session."workspace_id", session."id",
566
+ (
567
+ session."control_state" = 'paused'
568
+ OR (
569
+ workspace."inference_state" = 'paused'
570
+ AND session."workspace_run_exception_generation" IS DISTINCT FROM
571
+ workspace."inference_generation"
572
+ )
573
+ ) AS old_blocked,
574
+ (
575
+ workspace."inference_state" = 'paused'
576
+ OR EXISTS (
577
+ SELECT 1
578
+ FROM ancestry
579
+ JOIN "sessions" ancestor
580
+ ON ancestor."workspace_id" = ancestry."workspace_id"
581
+ AND ancestor."id" = ancestry."ancestor_id"
582
+ WHERE ancestry."workspace_id" = session."workspace_id"
583
+ AND ancestry.target_id = session."id"
584
+ AND ancestor."control_state" = 'paused'
585
+ )
586
+ ) AS new_blocked,
587
+ workspace."inference_state" = 'paused'
588
+ AND session."workspace_run_exception_generation" = workspace."inference_generation"
589
+ AS dropped_current_workspace_exception
590
+ FROM "sessions" session
591
+ JOIN "workspaces" workspace ON workspace."id" = session."workspace_id"
592
+ )
593
+ INSERT INTO "audit_events" (
594
+ "account_id", "workspace_id", "subject_id", "action", "target_type", "target_id", "metadata"
595
+ )
596
+ SELECT fate."account_id", fate."workspace_id", 'control-mega-migration',
597
+ 'session.control.migration.hold_only_delta', 'session', fate."id"::text,
598
+ jsonb_build_object(
599
+ 'classification', CASE
600
+ WHEN fate.dropped_current_workspace_exception THEN 'dropped_workspace_exception'
601
+ ELSE 'recursive_pause_descendant'
602
+ END,
603
+ 'oldBlocked', fate.old_blocked,
604
+ 'newBlocked', fate.new_blocked
605
+ )
606
+ FROM fate
607
+ WHERE NOT fate.old_blocked AND fate.new_blocked;
608
+
609
+ DO $hold_only_verify$
610
+ BEGIN
611
+ IF EXISTS (
612
+ WITH RECURSIVE ancestry AS (
613
+ SELECT session."workspace_id", session."id" AS target_id,
614
+ session."id" AS ancestor_id, session."parent_session_id", 0 AS depth
615
+ FROM "sessions" session
616
+ UNION ALL
617
+ SELECT ancestry."workspace_id", ancestry.target_id,
618
+ parent."id", parent."parent_session_id", ancestry.depth + 1
619
+ FROM ancestry
620
+ JOIN "sessions" parent
621
+ ON parent."workspace_id" = ancestry."workspace_id"
622
+ AND parent."id" = ancestry."parent_session_id"
623
+ WHERE ancestry.depth < 10000
624
+ )
625
+ SELECT 1
626
+ FROM "sessions" session
627
+ JOIN "workspaces" workspace ON workspace."id" = session."workspace_id"
628
+ WHERE (
629
+ session."control_state" = 'paused'
630
+ OR (
631
+ workspace."inference_state" = 'paused'
632
+ AND session."workspace_run_exception_generation" IS DISTINCT FROM
633
+ workspace."inference_generation"
634
+ )
635
+ )
636
+ AND NOT (
637
+ workspace."inference_state" = 'paused'
638
+ OR EXISTS (
639
+ SELECT 1
640
+ FROM ancestry
641
+ JOIN "sessions" ancestor
642
+ ON ancestor."workspace_id" = ancestry."workspace_id"
643
+ AND ancestor."id" = ancestry."ancestor_id"
644
+ WHERE ancestry."workspace_id" = session."workspace_id"
645
+ AND ancestry.target_id = session."id"
646
+ AND ancestor."control_state" = 'paused'
647
+ )
648
+ )
649
+ ) THEN
650
+ RAISE EXCEPTION 'session-control cutover: old-blocked session became runnable';
651
+ END IF;
652
+ END $hold_only_verify$;
653
+
654
+ -- Old Session Pause silently paused an otherwise-active goal with the private
655
+ -- reason user_pause and emitted no goal.paused event. Migration 0057 also
656
+ -- normalized the older *explicit* user-goal reason user_interrupt to the same
657
+ -- database value, while correctly preserving its goal.paused event. Classify
658
+ -- the current goal by its latest status-changing event so an explicit user
659
+ -- pause stays sacred and only the eventless Session-Pause coupling is removed.
660
+ CREATE TEMP TABLE "cutover_user_pause_goal_fates" (
661
+ "workspace_id" uuid NOT NULL,
662
+ "goal_id" uuid PRIMARY KEY,
663
+ "source" text NOT NULL CHECK ("source" IN ('explicit_goal_pause', 'session_pause'))
664
+ ) ON COMMIT DROP;
665
+
666
+ INSERT INTO "cutover_user_pause_goal_fates" ("workspace_id", "goal_id", "source")
667
+ SELECT goal."workspace_id", goal."id",
668
+ CASE
669
+ WHEN latest."type" = 'goal.paused'
670
+ AND latest."reason" IN ('user_interrupt', 'user_pause')
671
+ THEN 'explicit_goal_pause'
672
+ ELSE 'session_pause'
673
+ END
674
+ FROM "session_goals" goal
675
+ LEFT JOIN LATERAL (
676
+ SELECT event."type", event."payload" ->> 'reason' AS reason
677
+ FROM "session_events" event
678
+ WHERE event."workspace_id" = goal."workspace_id"
679
+ AND event."session_id" = goal."session_id"
680
+ AND event."payload" ->> 'goalId' = goal."id"::text
681
+ AND event."type" IN ('goal.set', 'goal.paused', 'goal.resumed', 'goal.completed', 'goal.cleared')
682
+ ORDER BY event."sequence" DESC, event."id" DESC
683
+ LIMIT 1
684
+ ) latest ON TRUE
685
+ WHERE goal."status" = 'paused'
686
+ AND goal."paused_reason" = 'user_pause';
687
+
688
+ DO $goal_preflight$
689
+ BEGIN
690
+ IF EXISTS (
691
+ SELECT 1
692
+ FROM "cutover_user_pause_goal_fates" fate
693
+ JOIN "session_goals" goal
694
+ ON goal."workspace_id" = fate."workspace_id" AND goal."id" = fate."goal_id"
695
+ JOIN "sessions" session
696
+ ON session."workspace_id" = goal."workspace_id" AND session."id" = goal."session_id"
697
+ WHERE fate."source" = 'session_pause'
698
+ AND session."control_state" <> 'paused'
699
+ ) THEN
700
+ RAISE EXCEPTION 'session-control cutover: eventless user_pause goal outside paused session';
701
+ END IF;
702
+ END $goal_preflight$;
703
+
704
+ INSERT INTO "audit_events" (
705
+ "account_id", "workspace_id", "subject_id", "action", "target_type", "target_id", "metadata"
706
+ )
707
+ SELECT goal."account_id", goal."workspace_id", 'control-mega-migration',
708
+ 'session.goal.migration.restored_from_session_pause', 'session_goal', goal."id"::text,
709
+ jsonb_build_object('oldStatus', goal."status", 'newStatus', 'active', 'oldVersion', goal."version")
710
+ FROM "session_goals" goal
711
+ JOIN "cutover_user_pause_goal_fates" fate
712
+ ON fate."workspace_id" = goal."workspace_id" AND fate."goal_id" = goal."id"
713
+ JOIN "sessions" session
714
+ ON session."workspace_id" = goal."workspace_id" AND session."id" = goal."session_id"
715
+ WHERE fate."source" = 'session_pause'
716
+ AND session."control_state" = 'paused';
717
+
718
+ UPDATE "session_goals" goal
719
+ SET "status" = 'active',
720
+ "paused_reason" = NULL,
721
+ "rationale" = NULL,
722
+ "auto_continuations" = 0,
723
+ "no_progress_streak" = 0,
724
+ "last_continuation_turn_id" = NULL,
725
+ "version_at_last_continuation" = NULL,
726
+ "version" = goal."version" + 1,
727
+ "updated_at" = now()
728
+ FROM "sessions" session
729
+ JOIN "cutover_user_pause_goal_fates" fate
730
+ ON fate."workspace_id" = session."workspace_id"
731
+ WHERE session."workspace_id" = goal."workspace_id"
732
+ AND session."id" = goal."session_id"
733
+ AND fate."goal_id" = goal."id"
734
+ AND fate."source" = 'session_pause'
735
+ AND session."control_state" = 'paused';
736
+
737
+ -- Pause was previously overloaded into session lifecycle. Recover canonical
738
+ -- lifecycle from the owned turn first, then visible queued work, then the last
739
+ -- non-pause lifecycle event. Unknown active-turn shapes fail closed rather than
740
+ -- being guessed into an idle session.
741
+ DO $lifecycle_preflight$
742
+ BEGIN
743
+ IF EXISTS (
744
+ SELECT 1
745
+ FROM "sessions" s
746
+ LEFT JOIN "session_turns" t
747
+ ON t."workspace_id" = s."workspace_id" AND t."id" = s."active_turn_id"
748
+ WHERE s."status" = 'paused'
749
+ AND s."active_turn_id" IS NOT NULL
750
+ AND (
751
+ t."id" IS NULL
752
+ OR t."session_id" <> s."id"
753
+ OR t."status" NOT IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
754
+ )
755
+ ) THEN
756
+ RAISE EXCEPTION 'session-control cutover: paused lifecycle has unclassifiable active turn';
757
+ END IF;
758
+ END $lifecycle_preflight$;
759
+
760
+ WITH reconstructed AS (
761
+ SELECT s."workspace_id", s."id",
762
+ CASE
763
+ WHEN s."status" <> 'paused' THEN s."status"
764
+ WHEN active_turn."status" IS NOT NULL THEN active_turn."status"
765
+ WHEN EXISTS (
766
+ SELECT 1 FROM "session_turns" queued
767
+ WHERE queued."workspace_id" = s."workspace_id"
768
+ AND queued."session_id" = s."id"
769
+ AND queued."status" = 'queued'
770
+ ) THEN 'queued'
771
+ ELSE coalesce((
772
+ SELECT event."payload" ->> 'status'
773
+ FROM "session_events" event
774
+ WHERE event."workspace_id" = s."workspace_id"
775
+ AND event."session_id" = s."id"
776
+ AND event."type" = 'session.status.changed'
777
+ AND event."payload" ->> 'status' IN (
778
+ 'queued', 'running', 'idle', 'requires_action', 'recovering',
779
+ 'waiting_capacity', 'failed', 'cancelled'
780
+ )
781
+ ORDER BY event."sequence" DESC, event."id" DESC
782
+ LIMIT 1
783
+ ), 'idle')
784
+ END AS lifecycle_status
785
+ FROM "sessions" s
786
+ LEFT JOIN "session_turns" active_turn
787
+ ON active_turn."workspace_id" = s."workspace_id"
788
+ AND active_turn."id" = s."active_turn_id"
789
+ )
790
+ UPDATE "sessions" s
791
+ SET "status" = reconstructed.lifecycle_status,
792
+ "updated_at" = greatest(s."updated_at", now())
793
+ FROM reconstructed
794
+ WHERE s."workspace_id" = reconstructed."workspace_id"
795
+ AND s."id" = reconstructed."id"
796
+ AND s."status" IS DISTINCT FROM reconstructed.lifecycle_status;
797
+
798
+ -- The former passive child-notification preference is not part of the closed
799
+ -- internal-update contract. Every child terminal result is now a coalescible,
800
+ -- actionable update, so remove the obsolete metadata rather than leaving a
801
+ -- setting that current code cannot and must not honor.
802
+ UPDATE "sessions"
803
+ SET "metadata" = "metadata" - 'childNotificationsMode',
804
+ "updated_at" = greatest("updated_at", now())
805
+ WHERE "metadata" ? 'childNotificationsMode';
806
+
807
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_lifecycle_status_check"
808
+ CHECK ("status" IN (
809
+ 'queued', 'running', 'idle', 'requires_action', 'recovering',
810
+ 'waiting_capacity', 'failed', 'cancelled'
811
+ ));
812
+
813
+ DO $lifecycle_verify$
814
+ BEGIN
815
+ IF EXISTS (SELECT 1 FROM "sessions" WHERE "status" = 'paused') THEN
816
+ RAISE EXCEPTION 'session-control cutover: paused lifecycle survived reconstruction';
817
+ END IF;
818
+ END $lifecycle_verify$;
819
+
820
+ -- Destructive half of the same maintenance cutover. New code cannot compile
821
+ -- against these columns, and the database cannot accept writes through them.
822
+ ALTER TABLE "sessions"
823
+ DROP COLUMN "control_state",
824
+ DROP COLUMN "control_generation",
825
+ DROP COLUMN "control_reason",
826
+ DROP COLUMN "control_changed_by",
827
+ DROP COLUMN "control_changed_at",
828
+ DROP COLUMN "pending_control_event_id",
829
+ DROP COLUMN "pending_control_kind",
830
+ DROP COLUMN "pending_control_expected_turn_id",
831
+ DROP COLUMN "pending_control_expected_generation",
832
+ DROP COLUMN "pending_control_expected_attempt_id",
833
+ DROP COLUMN "workspace_run_exception_generation";
834
+
835
+ ALTER TABLE "workspaces"
836
+ DROP COLUMN "inference_state",
837
+ DROP COLUMN "inference_generation",
838
+ DROP COLUMN "inference_reason",
839
+ DROP COLUMN "inference_changed_by",
840
+ DROP COLUMN "inference_changed_at";
841
+
842
+ ALTER TABLE "codex_capacity_waiters" DROP COLUMN "control_generation";
843
+ DROP TABLE "runtime_control_operations";
844
+
845
+ -- Workflow signals are replaceable wake hints. The dispatcher derives whether
846
+ -- a wake must cancel an executing activity from the durable interruption
847
+ -- ledger at claim time; a later ordinary queue wake therefore cannot erase or
848
+ -- downgrade an already-pending Pause/Steer request.
849
+ DROP FUNCTION opengeni_private.claim_session_workflow_wakes(integer);
850
+ DO $migration$
851
+ DECLARE target_schema text := current_schema();
852
+ BEGIN
853
+ EXECUTE format($create$
854
+ CREATE FUNCTION opengeni_private.claim_session_workflow_wakes(p_limit integer)
855
+ RETURNS TABLE (
856
+ account_id uuid,
857
+ workspace_id uuid,
858
+ session_id uuid,
859
+ temporal_workflow_id text,
860
+ wake_revision bigint,
861
+ interruption_requested boolean
862
+ )
863
+ LANGUAGE plpgsql
864
+ SECURITY DEFINER
865
+ SET search_path = pg_catalog
866
+ AS $function$
867
+ BEGIN
868
+ RETURN QUERY
869
+ WITH due AS (
870
+ SELECT o.session_id
871
+ FROM %1$I.session_workflow_wake_outbox o
872
+ WHERE o.wake_revision > o.delivered_revision
873
+ AND o.next_attempt_at <= now()
874
+ ORDER BY o.next_attempt_at, o.updated_at, o.session_id
875
+ FOR UPDATE SKIP LOCKED
876
+ LIMIT greatest(1, least(coalesce(p_limit, 100), 1000))
877
+ )
878
+ UPDATE %1$I.session_workflow_wake_outbox o
879
+ SET attempts = o.attempts + 1,
880
+ next_attempt_at = now() + make_interval(
881
+ secs => least(300, greatest(1, power(2, least(o.attempts, 8))::integer))
882
+ ),
883
+ updated_at = now()
884
+ FROM due
885
+ WHERE o.session_id = due.session_id
886
+ RETURNING o.account_id, o.workspace_id, o.session_id,
887
+ o.temporal_workflow_id, o.wake_revision,
888
+ EXISTS (
889
+ SELECT 1
890
+ FROM %1$I.session_attempt_interruptions interruption
891
+ WHERE interruption.workspace_id = o.workspace_id
892
+ AND interruption.session_id = o.session_id
893
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
894
+ ) AS interruption_requested;
895
+ END $function$;
896
+ $create$, target_schema);
897
+ END $migration$;
898
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer) FROM PUBLIC;
899
+
900
+ -- Clean internal-update cutover: preserve every durable row identity while
901
+ -- replacing the four open-ended legacy buckets with the five closed actionable
902
+ -- producer contracts. Unknown lifecycle shapes fail the migration below.
903
+ ALTER TABLE "session_system_updates" DROP CONSTRAINT "system_updates_kind_check";
904
+ ALTER TABLE "session_system_updates" DROP CONSTRAINT "system_updates_state_check";
905
+
906
+ -- Migration 0057 converted unstarted scheduled queue rows before scheduled
907
+ -- updates had a closed payload contract. Rebind those exact durable rows to the
908
+ -- owning scheduled-task occurrence; an orphan/ambiguous occurrence is not safe
909
+ -- to invent and fails the classifier below.
910
+ UPDATE "session_system_updates" update_row
911
+ SET "source_id" = run."id"::text,
912
+ "dedupe_key" = 'scheduled-occurrence:' || run."id"::text,
913
+ "payload" = update_row."payload" || jsonb_build_object(
914
+ 'text', update_row."summary",
915
+ 'scheduledTaskId', run."task_id",
916
+ 'scheduledTaskRunId', run."id"
917
+ ),
918
+ "lineage" = update_row."lineage" || jsonb_build_object(
919
+ 'scheduledTaskId', run."task_id",
920
+ 'scheduledTaskRunId', run."id"
921
+ )
922
+ FROM "session_turns" turn
923
+ JOIN "scheduled_task_runs" run
924
+ ON run."workspace_id" = turn."workspace_id"
925
+ AND run."trigger_event_id" = turn."trigger_event_id"
926
+ WHERE update_row."workspace_id" = turn."workspace_id"
927
+ AND update_row."payload" ->> 'migratedTurnId' = turn."id"::text
928
+ AND update_row."kind" = 'scheduled_wake';
929
+
930
+ UPDATE "session_system_updates"
931
+ SET kind = 'scheduled_occurrence',
932
+ payload = payload || jsonb_build_object(
933
+ 'type', 'scheduled_occurrence',
934
+ 'text', coalesce(nullif(payload ->> 'text', ''), summary)
935
+ )
936
+ WHERE kind = 'scheduled_wake';
937
+
938
+ UPDATE "session_system_updates"
939
+ SET kind = 'goal_continuation',
940
+ payload = payload || jsonb_build_object(
941
+ 'type', 'goal_continuation',
942
+ 'prompt', coalesce(nullif(payload ->> 'prompt', ''), summary)
943
+ )
944
+ WHERE kind = 'lifecycle_event' AND payload ->> 'type' = 'goal_continuation';
945
+
946
+ UPDATE "session_system_updates"
947
+ SET kind = 'agent_message',
948
+ payload = payload || jsonb_build_object(
949
+ 'type', 'agent_message',
950
+ 'text', coalesce(nullif(payload ->> 'text', ''), summary),
951
+ 'operationId', id
952
+ )
953
+ WHERE kind = 'runtime_notice';
954
+
955
+ UPDATE "session_system_updates"
956
+ SET kind = 'child_terminal_result',
957
+ payload = payload || jsonb_build_object(
958
+ 'type', 'child_terminal_result',
959
+ 'childSessionId', coalesce(nullif(payload ->> 'childSessionId', ''), source_id),
960
+ 'status', CASE
961
+ WHEN coalesce(payload ->> 'status', payload ->> 'terminalStatus') = 'failed'
962
+ THEN 'failed'
963
+ ELSE 'idle'
964
+ END
965
+ )
966
+ WHERE kind = 'child_session_update';
967
+
968
+ -- The durable child-delivery outbox is the upstream half of the same producer.
969
+ -- Convert it before a new worker can retry a pending pre-cutover row.
970
+ UPDATE "session_system_update_outbox"
971
+ SET "kind" = 'child_terminal_result',
972
+ "payload" = "payload" || jsonb_build_object(
973
+ 'type', 'child_terminal_result',
974
+ 'childSessionId', coalesce(nullif("payload" ->> 'childSessionId', ''), "source_id"),
975
+ 'status', CASE
976
+ WHEN coalesce("payload" ->> 'status', "payload" ->> 'terminalStatus') = 'failed'
977
+ THEN 'failed'
978
+ ELSE 'idle'
979
+ END
980
+ )
981
+ WHERE "kind" = 'child_session_update';
982
+
983
+ DO $internal_updates$
984
+ BEGIN
985
+ IF EXISTS (
986
+ SELECT 1 FROM "session_system_updates"
987
+ WHERE kind NOT IN (
988
+ 'scheduled_occurrence', 'goal_continuation', 'agent_message',
989
+ 'agent_steer_instruction', 'child_terminal_result'
990
+ ) OR payload ->> 'type' IS DISTINCT FROM kind
991
+ OR CASE kind
992
+ WHEN 'scheduled_occurrence' THEN NOT (
993
+ nullif(payload ->> 'text', '') IS NOT NULL
994
+ AND (payload ->> 'scheduledTaskId') ~
995
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
996
+ AND (payload ->> 'scheduledTaskRunId') ~
997
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
998
+ )
999
+ WHEN 'goal_continuation' THEN NOT (
1000
+ nullif(payload ->> 'prompt', '') IS NOT NULL
1001
+ AND (payload ->> 'goalId') ~
1002
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
1003
+ AND (payload ->> 'goalVersion') ~ '^[1-9][0-9]*$'
1004
+ )
1005
+ WHEN 'agent_message' THEN NOT (
1006
+ nullif(payload ->> 'text', '') IS NOT NULL
1007
+ AND (payload ->> 'operationId') ~
1008
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
1009
+ )
1010
+ WHEN 'agent_steer_instruction' THEN NOT (
1011
+ nullif(payload ->> 'instruction', '') IS NOT NULL
1012
+ AND (payload ->> 'operationId') ~
1013
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
1014
+ )
1015
+ WHEN 'child_terminal_result' THEN NOT (
1016
+ (payload ->> 'childSessionId') ~
1017
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
1018
+ AND payload ->> 'status' IN ('idle', 'failed')
1019
+ )
1020
+ ELSE true
1021
+ END
1022
+ ) THEN
1023
+ RAISE EXCEPTION 'unclassified session_system_updates row blocks canonical cutover';
1024
+ END IF;
1025
+ IF EXISTS (
1026
+ SELECT 1 FROM "session_system_update_outbox"
1027
+ WHERE "kind" <> 'child_terminal_result'
1028
+ OR "payload" ->> 'type' <> 'child_terminal_result'
1029
+ OR NOT (
1030
+ ("payload" ->> 'childSessionId') ~
1031
+ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
1032
+ AND "payload" ->> 'status' IN ('idle', 'failed')
1033
+ )
1034
+ ) THEN
1035
+ RAISE EXCEPTION 'unclassified session_system_update_outbox row blocks canonical cutover';
1036
+ END IF;
1037
+ END $internal_updates$;
1038
+
1039
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_kind_check"
1040
+ CHECK (kind IN (
1041
+ 'scheduled_occurrence', 'goal_continuation', 'agent_message',
1042
+ 'agent_steer_instruction', 'child_terminal_result'
1043
+ ));
1044
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_payload_kind_check"
1045
+ CHECK (payload ->> 'type' = kind);
1046
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_state_check"
1047
+ CHECK (state IN ('pending', 'deferred', 'delivered', 'cancelled', 'superseded', 'failed'));
1048
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_kind_check"
1049
+ CHECK (kind = 'child_terminal_result');
1050
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_payload_kind_check"
1051
+ CHECK (payload ->> 'type' = 'child_terminal_result');
1052
+
1053
+ -- One canonical continuability projection owns both runtime Pause/Resume wake
1054
+ -- registration and maintenance reconstruction. It returns one row per session
1055
+ -- plus every durable reason that makes a fresh Temporal workflow necessary.
1056
+ -- Control blocks ordinary work but never blocks settlement of an already
1057
+ -- committed interruption. Malformed ancestry is omitted (held fail-closed) and
1058
+ -- is separately rejected by the migration ancestry preflight above.
1059
+ -- Propagate the greatest Pause and Resume revisions once from each root instead
1060
+ -- of rescanning every target's ancestry. Pause wins an impossible equal-revision
1061
+ -- tie, matching the strict "override revision must be newer" control rule.
1062
+ DO $continuability$
1063
+ DECLARE target_schema text := current_schema();
1064
+ BEGIN
1065
+ EXECUTE format($create$
1066
+ CREATE FUNCTION opengeni_private.list_continuable_sessions(
1067
+ p_workspace_id uuid,
1068
+ p_root_session_id uuid
1069
+ )
1070
+ RETURNS TABLE (
1071
+ account_id uuid,
1072
+ workspace_id uuid,
1073
+ session_id uuid,
1074
+ temporal_workflow_id text,
1075
+ reasons text[]
1076
+ )
1077
+ LANGUAGE sql
1078
+ STABLE
1079
+ SET search_path = pg_catalog
1080
+ AS $function$
1081
+ WITH RECURSIVE control_tree AS (
1082
+ SELECT session.workspace_id, session.id, session.parent_session_id,
1083
+ greatest(control.workspace_pause_revision, session.direct_pause_revision)
1084
+ AS max_pause_revision,
1085
+ session.subtree_run_override_revision AS max_override_revision,
1086
+ 0::integer AS depth
1087
+ FROM %1$I.sessions session
1088
+ JOIN %1$I.workspace_inference_controls control
1089
+ ON control.workspace_id = session.workspace_id
1090
+ WHERE session.parent_session_id IS NULL
1091
+ AND (p_workspace_id IS NULL OR session.workspace_id = p_workspace_id)
1092
+ UNION ALL
1093
+ SELECT child.workspace_id, child.id, child.parent_session_id,
1094
+ greatest(parent.max_pause_revision, child.direct_pause_revision),
1095
+ greatest(parent.max_override_revision, child.subtree_run_override_revision),
1096
+ parent.depth + 1
1097
+ FROM control_tree parent
1098
+ JOIN %1$I.sessions child
1099
+ ON child.workspace_id = parent.workspace_id
1100
+ AND child.parent_session_id = parent.id
1101
+ WHERE parent.depth < 10000
1102
+ ), descendants AS (
1103
+ SELECT tree.workspace_id, tree.id
1104
+ FROM control_tree tree
1105
+ WHERE p_root_session_id IS NOT NULL AND tree.id = p_root_session_id
1106
+ UNION ALL
1107
+ SELECT child.workspace_id, child.id
1108
+ FROM descendants parent
1109
+ JOIN control_tree child
1110
+ ON child.workspace_id = parent.workspace_id
1111
+ AND child.parent_session_id = parent.id
1112
+ ), scope_sessions AS (
1113
+ SELECT session.*, tree.max_pause_revision, tree.max_override_revision
1114
+ FROM %1$I.sessions session
1115
+ JOIN control_tree tree
1116
+ ON tree.workspace_id = session.workspace_id AND tree.id = session.id
1117
+ WHERE (
1118
+ p_root_session_id IS NULL
1119
+ OR EXISTS (
1120
+ SELECT 1 FROM descendants descendant
1121
+ WHERE descendant.workspace_id = session.workspace_id
1122
+ AND descendant.id = session.id
1123
+ )
1124
+ )
1125
+ ), control_state AS (
1126
+ SELECT session.workspace_id, session.id AS session_id,
1127
+ session.max_pause_revision IS NOT NULL
1128
+ AND (
1129
+ session.max_override_revision IS NULL
1130
+ OR session.max_pause_revision >= session.max_override_revision
1131
+ ) AS blocked
1132
+ FROM scope_sessions session
1133
+ ), unsettled_interruptions AS (
1134
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1135
+ FROM scope_sessions session
1136
+ JOIN %1$I.session_attempt_interruptions interruption
1137
+ ON interruption.workspace_id = session.workspace_id
1138
+ AND interruption.session_id = session.id
1139
+ WHERE interruption.state IN ('pending', 'delivered', 'acknowledged')
1140
+ ), queued_human AS (
1141
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1142
+ FROM scope_sessions session
1143
+ JOIN %1$I.session_turns turn
1144
+ ON turn.workspace_id = session.workspace_id AND turn.session_id = session.id
1145
+ WHERE turn.status = 'queued' AND turn.source IN ('user', 'api')
1146
+ ), recovering_turn AS (
1147
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1148
+ FROM scope_sessions session
1149
+ JOIN %1$I.session_turns turn
1150
+ ON turn.workspace_id = session.workspace_id
1151
+ AND turn.session_id = session.id
1152
+ AND turn.id = session.active_turn_id
1153
+ WHERE turn.status = 'recovering'
1154
+ ), capacity_wait AS (
1155
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1156
+ FROM scope_sessions session
1157
+ JOIN %1$I.codex_capacity_waiters waiter
1158
+ ON waiter.workspace_id = session.workspace_id AND waiter.session_id = session.id
1159
+ WHERE waiter.status = 'waiting'
1160
+ ), decided_approval AS (
1161
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1162
+ FROM scope_sessions session
1163
+ JOIN %1$I.session_turns turn
1164
+ ON turn.workspace_id = session.workspace_id
1165
+ AND turn.session_id = session.id
1166
+ AND turn.id = session.active_turn_id
1167
+ JOIN %1$I.session_events trigger_event
1168
+ ON trigger_event.workspace_id = turn.workspace_id
1169
+ AND trigger_event.id = turn.trigger_event_id
1170
+ JOIN %1$I.session_events decision
1171
+ ON decision.workspace_id = turn.workspace_id
1172
+ AND decision.session_id = turn.session_id
1173
+ AND decision.sequence > trigger_event.sequence
1174
+ AND decision.type = 'user.approvalDecision'
1175
+ WHERE turn.status = 'requires_action'
1176
+ ), active_goal AS (
1177
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1178
+ FROM scope_sessions session
1179
+ JOIN %1$I.session_goals goal
1180
+ ON goal.workspace_id = session.workspace_id AND goal.session_id = session.id
1181
+ WHERE goal.status = 'active'
1182
+ ), pending_internal_updates AS (
1183
+ SELECT DISTINCT session.workspace_id, session.id AS session_id
1184
+ FROM scope_sessions session
1185
+ JOIN %1$I.session_system_updates update_row
1186
+ ON update_row.workspace_id = session.workspace_id
1187
+ AND update_row.session_id = session.id
1188
+ WHERE update_row.state = 'pending'
1189
+ ), classified AS (
1190
+ SELECT session.account_id, session.workspace_id, session.id AS session_id,
1191
+ coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
1192
+ AS temporal_workflow_id,
1193
+ array_remove(ARRAY[
1194
+ CASE WHEN interruption.session_id IS NOT NULL
1195
+ THEN 'interruption_settlement' END,
1196
+ CASE WHEN NOT control.blocked AND queued.session_id IS NOT NULL
1197
+ THEN 'queued_human' END,
1198
+ CASE WHEN NOT control.blocked AND recovering.session_id IS NOT NULL
1199
+ THEN 'recovering_turn' END,
1200
+ CASE WHEN NOT control.blocked AND capacity.session_id IS NOT NULL
1201
+ THEN 'capacity_wait' END,
1202
+ CASE WHEN NOT control.blocked AND approval.session_id IS NOT NULL
1203
+ THEN 'decided_approval' END,
1204
+ CASE WHEN NOT control.blocked AND goal.session_id IS NOT NULL
1205
+ THEN 'active_goal' END,
1206
+ CASE WHEN NOT control.blocked AND updates.session_id IS NOT NULL
1207
+ THEN 'pending_internal_updates' END,
1208
+ CASE WHEN NOT control.blocked AND session.compact_requested
1209
+ THEN 'compaction_requested' END
1210
+ ]::text[], NULL) AS reasons
1211
+ FROM scope_sessions session
1212
+ JOIN control_state control
1213
+ ON control.workspace_id = session.workspace_id AND control.session_id = session.id
1214
+ LEFT JOIN unsettled_interruptions interruption
1215
+ ON interruption.workspace_id = session.workspace_id
1216
+ AND interruption.session_id = session.id
1217
+ LEFT JOIN queued_human queued
1218
+ ON queued.workspace_id = session.workspace_id AND queued.session_id = session.id
1219
+ LEFT JOIN recovering_turn recovering
1220
+ ON recovering.workspace_id = session.workspace_id AND recovering.session_id = session.id
1221
+ LEFT JOIN capacity_wait capacity
1222
+ ON capacity.workspace_id = session.workspace_id AND capacity.session_id = session.id
1223
+ LEFT JOIN decided_approval approval
1224
+ ON approval.workspace_id = session.workspace_id AND approval.session_id = session.id
1225
+ LEFT JOIN active_goal goal
1226
+ ON goal.workspace_id = session.workspace_id AND goal.session_id = session.id
1227
+ LEFT JOIN pending_internal_updates updates
1228
+ ON updates.workspace_id = session.workspace_id AND updates.session_id = session.id
1229
+ )
1230
+ SELECT classified.account_id, classified.workspace_id, classified.session_id,
1231
+ classified.temporal_workflow_id, classified.reasons
1232
+ FROM classified
1233
+ WHERE cardinality(classified.reasons) > 0
1234
+ ORDER BY classified.workspace_id, classified.session_id
1235
+ $function$;
1236
+ $create$, target_schema);
1237
+ END $continuability$;
1238
+
1239
+ -- Old workflow histories are unconditionally terminated during maintenance.
1240
+ -- Bump one durable revision for every continuable session even when an older
1241
+ -- wake row says it was delivered; the new workflow receives the complete set
1242
+ -- of reasons from current PostgreSQL truth and no prompt is manufactured.
1243
+ WITH continuable AS (
1244
+ SELECT * FROM opengeni_private.list_continuable_sessions(NULL, NULL)
1245
+ ), seeded AS (
1246
+ INSERT INTO "session_workflow_wake_outbox" (
1247
+ "session_id", "account_id", "workspace_id", "temporal_workflow_id", "reason"
1248
+ )
1249
+ SELECT session_id, account_id, workspace_id, temporal_workflow_id,
1250
+ 'control_mega_cutover:' || array_to_string(reasons, ',')
1251
+ FROM continuable
1252
+ ON CONFLICT ("session_id") DO UPDATE SET
1253
+ "wake_revision" = "session_workflow_wake_outbox"."wake_revision" + 1,
1254
+ "temporal_workflow_id" = excluded."temporal_workflow_id",
1255
+ "reason" = excluded."reason",
1256
+ "attempts" = 0,
1257
+ "next_attempt_at" = now(),
1258
+ "last_error" = NULL,
1259
+ "updated_at" = now()
1260
+ RETURNING "account_id", "workspace_id", "session_id", "wake_revision", "reason"
1261
+ )
1262
+ INSERT INTO "audit_events" (
1263
+ "account_id", "workspace_id", "subject_id", "action", "target_type", "target_id", "metadata"
1264
+ )
1265
+ SELECT seeded.account_id, seeded.workspace_id, 'control-mega-migration',
1266
+ 'session.workflow.migration.wake_seeded', 'session', seeded.session_id::text,
1267
+ jsonb_build_object('wakeRevision', seeded.wake_revision, 'reason', seeded.reason)
1268
+ FROM seeded;
1269
+
1270
+ REVOKE ALL ON FUNCTION opengeni_private.list_continuable_sessions(uuid, uuid) FROM PUBLIC;
1271
+
1272
+ ALTER TABLE "workspace_inference_controls" ENABLE ROW LEVEL SECURITY;
1273
+ ALTER TABLE "workspace_inference_controls" FORCE ROW LEVEL SECURITY;
1274
+ ALTER TABLE "session_turn_attempts" ENABLE ROW LEVEL SECURITY;
1275
+ ALTER TABLE "session_turn_attempts" FORCE ROW LEVEL SECURITY;
1276
+ ALTER TABLE "session_command_receipts" ENABLE ROW LEVEL SECURITY;
1277
+ ALTER TABLE "session_command_receipts" FORCE ROW LEVEL SECURITY;
1278
+ ALTER TABLE "workspace_control_events" ENABLE ROW LEVEL SECURITY;
1279
+ ALTER TABLE "workspace_control_events" FORCE ROW LEVEL SECURITY;
1280
+ ALTER TABLE "session_attempt_interruptions" ENABLE ROW LEVEL SECURITY;
1281
+ ALTER TABLE "session_attempt_interruptions" FORCE ROW LEVEL SECURITY;
1282
+ ALTER TABLE "composer_drafts" ENABLE ROW LEVEL SECURITY;
1283
+ ALTER TABLE "composer_drafts" FORCE ROW LEVEL SECURITY;
1284
+
1285
+ CREATE POLICY workspace_isolation ON "workspace_inference_controls"
1286
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
1287
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
1288
+ CREATE POLICY workspace_isolation ON "session_turn_attempts"
1289
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
1290
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
1291
+ CREATE POLICY workspace_isolation ON "session_command_receipts"
1292
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
1293
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
1294
+ CREATE POLICY workspace_isolation ON "workspace_control_events"
1295
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
1296
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
1297
+ CREATE POLICY workspace_isolation ON "session_attempt_interruptions"
1298
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
1299
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
1300
+ CREATE POLICY workspace_isolation ON "composer_drafts"
1301
+ USING (
1302
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
1303
+ AND subject_id = opengeni_private.current_subject_id()
1304
+ )
1305
+ WITH CHECK (
1306
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
1307
+ AND subject_id = opengeni_private.current_subject_id()
1308
+ );
1309
+
1310
+ DO $grants$
1311
+ DECLARE target_schema text := current_schema();
1312
+ BEGIN
1313
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1314
+ EXECUTE format(
1315
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO opengeni_app',
1316
+ target_schema
1317
+ );
1318
+ GRANT EXECUTE ON FUNCTION opengeni_private.list_continuable_sessions(uuid, uuid)
1319
+ TO opengeni_app;
1320
+ END IF;
1321
+ END $grants$;
1322
+
1323
+ RESET statement_timeout;
1324
+ RESET lock_timeout;