@opengeni/db 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) 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-O3D7DABC.js +2314 -0
  4. package/dist/chunk-O3D7DABC.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 +13129 -3496
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.js +1 -1
  11. package/dist/provision-roles.d.ts +1836 -222
  12. package/dist/provision-roles.js +1 -1
  13. package/dist/{schema-Dsz6UHNv.d.ts → schema-Dp2MbBxx.d.ts} +7480 -3305
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +33 -5
  16. package/drizzle/0043_credit_balance_observability.sql +22 -0
  17. package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
  18. package/drizzle/0045_workspace_captures.sql +83 -0
  19. package/drizzle/0045_workspace_memory_v1.sql +71 -0
  20. package/drizzle/0046_variable_sets_rename.sql +56 -0
  21. package/drizzle/0047_rigs.sql +151 -0
  22. package/drizzle/0048_rig_runtime.sql +9 -0
  23. package/drizzle/0049_enrollment_went_offline.sql +28 -0
  24. package/drizzle/0050_enrollment_op_stream.sql +1 -0
  25. package/drizzle/0051_codex_pin_source.sql +48 -0
  26. package/drizzle/0052_file_upload_cleanup.sql +91 -0
  27. package/drizzle/0053_codex_credential_leases.sql +230 -0
  28. package/drizzle/0054_session_pins.sql +85 -0
  29. package/drizzle/0055_session_list_snapshots.sql +73 -0
  30. package/drizzle/0056_workspace_model_policies.sql +48 -0
  31. package/drizzle/0057_durable_queue_control.sql +536 -0
  32. package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
  33. package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
  34. package/drizzle/0060_session_system_update_deferral.sql +10 -0
  35. package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
  36. package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
  37. package/package.json +17 -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 +22460 -6492
  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 +2557 -1121
  47. package/src/session-control-cutover-audit.ts +1203 -0
  48. package/dist/chunk-57MLICFR.js.map +0 -1
  49. package/dist/chunk-OGCE6O2X.js.map +0 -1
  50. package/dist/chunk-ZIUCA2IO.js +0 -1268
  51. package/dist/chunk-ZIUCA2IO.js.map +0 -1
@@ -0,0 +1,536 @@
1
+ -- OPE-18/OPE-21/OPE-22: one prompt queue, one current inference, portable
2
+ -- compaction, attempt-fenced publication, coalesced internal updates, and
3
+ -- session/workspace Pause.
4
+ --
5
+ -- This is a one-way maintenance migration. The production release gate stops
6
+ -- admission, pauses Temporal Schedules, drains workers, terminates the old
7
+ -- session workflows, and scales the old worker fleet to zero before applying
8
+ -- it. No old worker or old workflow is allowed to execute after this boundary.
9
+
10
+ ALTER TABLE "workspaces" ADD COLUMN "inference_state" text NOT NULL DEFAULT 'active';
11
+ ALTER TABLE "workspaces" ADD COLUMN "inference_generation" integer NOT NULL DEFAULT 0;
12
+ ALTER TABLE "workspaces" ADD COLUMN "inference_reason" text;
13
+ ALTER TABLE "workspaces" ADD COLUMN "inference_changed_by" text;
14
+ ALTER TABLE "workspaces" ADD COLUMN "inference_changed_at" timestamptz;
15
+
16
+ ALTER TABLE "sessions" ADD COLUMN "queue_version" integer NOT NULL DEFAULT 0;
17
+ ALTER TABLE "sessions" ADD COLUMN "queue_head_position" bigint NOT NULL DEFAULT 0;
18
+ ALTER TABLE "sessions" ADD COLUMN "queue_tail_position" bigint NOT NULL DEFAULT 0;
19
+ ALTER TABLE "sessions" ADD COLUMN "control_state" text NOT NULL DEFAULT 'active';
20
+ ALTER TABLE "sessions" ADD COLUMN "control_generation" integer NOT NULL DEFAULT 0;
21
+ ALTER TABLE "sessions" ADD COLUMN "control_reason" text;
22
+ ALTER TABLE "sessions" ADD COLUMN "control_changed_by" text;
23
+ ALTER TABLE "sessions" ADD COLUMN "control_changed_at" timestamptz;
24
+ ALTER TABLE "sessions" ADD COLUMN "pending_control_event_id" uuid;
25
+ ALTER TABLE "sessions" ADD COLUMN "pending_control_kind" text;
26
+ ALTER TABLE "sessions" ADD COLUMN "pending_control_expected_turn_id" uuid;
27
+ ALTER TABLE "sessions" ADD COLUMN "pending_control_expected_generation" integer;
28
+ ALTER TABLE "sessions" ADD COLUMN "pending_control_expected_attempt_id" uuid;
29
+ ALTER TABLE "sessions" ADD COLUMN "workspace_run_exception_generation" integer;
30
+
31
+ ALTER TABLE "session_turns" ALTER COLUMN "position" TYPE bigint USING "position"::bigint;
32
+ ALTER TABLE "session_turns" ADD COLUMN "version" integer NOT NULL DEFAULT 1;
33
+ ALTER TABLE "session_turns" ADD COLUMN "execution_generation" integer NOT NULL DEFAULT 0;
34
+ ALTER TABLE "session_turns" ADD COLUMN "active_attempt_id" uuid;
35
+ ALTER TABLE "session_turns" ADD COLUMN "lineage" jsonb NOT NULL DEFAULT '{}'::jsonb;
36
+ ALTER TABLE "session_turns" ADD COLUMN "cancelled_by" text;
37
+ ALTER TABLE "session_turns" ADD COLUMN "cancel_reason" text;
38
+
39
+ ALTER TABLE "session_events" ADD COLUMN "turn_generation" integer;
40
+ ALTER TABLE "session_events" ADD COLUMN "turn_attempt_id" uuid;
41
+ ALTER TABLE "session_events" ADD COLUMN "turn_association" text;
42
+
43
+ ALTER TABLE "scheduled_task_runs" ADD COLUMN "producer_key" text;
44
+
45
+ CREATE TABLE "session_system_updates" (
46
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
47
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
48
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
49
+ "session_id" uuid NOT NULL,
50
+ "kind" text NOT NULL,
51
+ "classification" text NOT NULL DEFAULT 'info',
52
+ "source_id" text NOT NULL,
53
+ "dedupe_key" text NOT NULL,
54
+ "summary" text NOT NULL,
55
+ "payload" jsonb NOT NULL DEFAULT '{}'::jsonb,
56
+ "lineage" jsonb NOT NULL DEFAULT '{}'::jsonb,
57
+ "state" text NOT NULL DEFAULT 'pending',
58
+ "delivered_turn_id" uuid,
59
+ "delivered_at" timestamptz,
60
+ "created_at" timestamptz NOT NULL DEFAULT now()
61
+ );
62
+
63
+ -- Capacity becoming available is an internal update, never a queued goal turn.
64
+ -- Retarget the durable waiter receipt from the removed queue representation to
65
+ -- the typed update that will drive the next internal inference.
66
+ ALTER TABLE "codex_capacity_waiters"
67
+ DROP CONSTRAINT "codex_capacity_waiters_workspace_resumed_turn_fk";
68
+ ALTER TABLE "codex_capacity_waiters"
69
+ RENAME COLUMN "resumed_turn_id" TO "resumed_update_id";
70
+ UPDATE "codex_capacity_waiters" SET "resumed_update_id" = NULL;
71
+
72
+ CREATE TABLE "runtime_control_operations" (
73
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
74
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
75
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
76
+ "scope" text NOT NULL,
77
+ "target_id" uuid NOT NULL,
78
+ "client_event_id" text NOT NULL,
79
+ "requested_state" text NOT NULL,
80
+ "expected_state" text,
81
+ "expected_generation" integer,
82
+ "result" jsonb NOT NULL DEFAULT '{}'::jsonb,
83
+ "created_at" timestamptz NOT NULL DEFAULT now(),
84
+ "updated_at" timestamptz NOT NULL DEFAULT now()
85
+ );
86
+
87
+ CREATE TABLE "session_system_update_outbox" (
88
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
89
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
90
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
91
+ "source_session_id" uuid NOT NULL,
92
+ "target_session_id" uuid NOT NULL,
93
+ "dedupe_key" text NOT NULL,
94
+ "kind" text NOT NULL,
95
+ "classification" text NOT NULL,
96
+ "source_id" text NOT NULL,
97
+ "summary" text NOT NULL,
98
+ "payload" jsonb NOT NULL DEFAULT '{}'::jsonb,
99
+ "lineage" jsonb NOT NULL DEFAULT '{}'::jsonb,
100
+ "status" text NOT NULL DEFAULT 'pending',
101
+ "attempts" integer NOT NULL DEFAULT 0,
102
+ "update_id" uuid,
103
+ "last_error" text,
104
+ "delivered_at" timestamptz,
105
+ "created_at" timestamptz NOT NULL DEFAULT now(),
106
+ "updated_at" timestamptz NOT NULL DEFAULT now()
107
+ );
108
+
109
+ CREATE TABLE "session_pending_tool_calls" (
110
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
111
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
112
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
113
+ "session_id" uuid NOT NULL,
114
+ "turn_id" uuid NOT NULL,
115
+ "execution_generation" integer NOT NULL,
116
+ "attempt_id" uuid NOT NULL,
117
+ "call_id" text NOT NULL,
118
+ "call_type" text NOT NULL,
119
+ "call_item" jsonb NOT NULL,
120
+ "result_item" jsonb,
121
+ "result_recorded_at" timestamptz,
122
+ "created_at" timestamptz NOT NULL DEFAULT now()
123
+ );
124
+
125
+ -- Composite identities make every new foreign key workspace-scoped.
126
+ CREATE UNIQUE INDEX "sessions_workspace_id_uq" ON "sessions" ("workspace_id", "id");
127
+ CREATE UNIQUE INDEX "session_turns_workspace_id_uq" ON "session_turns" ("workspace_id", "id");
128
+ CREATE UNIQUE INDEX "session_events_workspace_id_uq" ON "session_events" ("workspace_id", "id");
129
+ CREATE UNIQUE INDEX "session_system_updates_workspace_id_uq"
130
+ ON "session_system_updates" ("workspace_id", "id");
131
+
132
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_pending_control_event_fk"
133
+ FOREIGN KEY ("workspace_id", "pending_control_event_id")
134
+ REFERENCES "session_events"("workspace_id", "id") ON DELETE SET NULL;
135
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_pending_control_turn_fk"
136
+ FOREIGN KEY ("workspace_id", "pending_control_expected_turn_id")
137
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE SET NULL;
138
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_workspace_account_fk"
139
+ FOREIGN KEY ("workspace_id", "account_id")
140
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE;
141
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_workspace_session_fk"
142
+ FOREIGN KEY ("workspace_id", "session_id")
143
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE;
144
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_workspace_turn_fk"
145
+ FOREIGN KEY ("workspace_id", "delivered_turn_id")
146
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE SET NULL
147
+ DEFERRABLE INITIALLY DEFERRED;
148
+ ALTER TABLE "codex_capacity_waiters" ADD CONSTRAINT "codex_capacity_waiters_workspace_resumed_update_fk"
149
+ FOREIGN KEY ("workspace_id", "resumed_update_id")
150
+ REFERENCES "session_system_updates"("workspace_id", "id") ON DELETE SET NULL;
151
+ ALTER TABLE "runtime_control_operations" ADD CONSTRAINT "runtime_control_operations_workspace_account_fk"
152
+ FOREIGN KEY ("workspace_id", "account_id")
153
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE;
154
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_workspace_account_fk"
155
+ FOREIGN KEY ("workspace_id", "account_id")
156
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE;
157
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_source_session_fk"
158
+ FOREIGN KEY ("workspace_id", "source_session_id")
159
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE;
160
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_target_session_fk"
161
+ FOREIGN KEY ("workspace_id", "target_session_id")
162
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE;
163
+ ALTER TABLE "session_pending_tool_calls" ADD CONSTRAINT "pending_tool_calls_workspace_account_fk"
164
+ FOREIGN KEY ("workspace_id", "account_id")
165
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE;
166
+ ALTER TABLE "session_pending_tool_calls" ADD CONSTRAINT "pending_tool_calls_workspace_session_fk"
167
+ FOREIGN KEY ("workspace_id", "session_id")
168
+ REFERENCES "sessions"("workspace_id", "id") ON DELETE CASCADE;
169
+ ALTER TABLE "session_pending_tool_calls" ADD CONSTRAINT "pending_tool_calls_workspace_turn_fk"
170
+ FOREIGN KEY ("workspace_id", "turn_id")
171
+ REFERENCES "session_turns"("workspace_id", "id") ON DELETE CASCADE;
172
+
173
+ -- Preserve historical evidence while normalizing every event name to the new
174
+ -- vocabulary before new workers parse it.
175
+ UPDATE "session_events" SET "type" = CASE "type"
176
+ WHEN 'user.interrupt' THEN 'user.pause'
177
+ WHEN 'turn.preempted' THEN 'turn.recovery.requested'
178
+ WHEN 'turn.updated' THEN 'session.queue.history'
179
+ WHEN 'turn.queue_drained' THEN 'session.queue.history'
180
+ WHEN 'system.update.bundle' THEN 'system.update.pending'
181
+ WHEN 'system.update.bundle.updated' THEN 'system.update.pending'
182
+ WHEN 'session.control.stopped' THEN 'session.control.paused'
183
+ WHEN 'session.control.interrupt_requested' THEN 'session.control.paused'
184
+ WHEN 'workspace.inference.killed' THEN 'workspace.inference.paused'
185
+ WHEN 'session.queue.item.edited' THEN 'session.queue.history'
186
+ WHEN 'session.queue.item.promoted' THEN 'session.queue.history'
187
+ WHEN 'session.queue.reordered' THEN 'session.queue.history'
188
+ WHEN 'session.queue.item.cancelled' THEN 'session.queue.prompt.cancelled'
189
+ ELSE "type"
190
+ END
191
+ WHERE "type" IN (
192
+ 'user.interrupt','turn.preempted','turn.updated','turn.queue_drained','system.update.bundle',
193
+ 'system.update.bundle.updated','session.control.stopped',
194
+ 'session.control.interrupt_requested','workspace.inference.killed',
195
+ 'session.queue.item.edited','session.queue.item.promoted',
196
+ 'session.queue.reordered','session.queue.item.cancelled'
197
+ );
198
+
199
+ UPDATE "session_goals"
200
+ SET "paused_reason" = 'user_pause'
201
+ WHERE "paused_reason" = 'user_interrupt';
202
+
203
+ -- A drained turn that started before and was put back into the old queue is
204
+ -- the same current inference in recovery, not a waiting prompt. The drain gate
205
+ -- guarantees no old attempt still owns it here.
206
+ UPDATE "session_turns" t
207
+ SET "status" = 'recovering',
208
+ "active_attempt_id" = NULL,
209
+ "updated_at" = now()
210
+ WHERE t."status" = 'running'
211
+ OR (
212
+ t."status" = 'queued'
213
+ AND EXISTS (
214
+ SELECT 1 FROM "session_events" e
215
+ WHERE e."workspace_id" = t."workspace_id"
216
+ AND e."session_id" = t."session_id"
217
+ AND e."turn_id" = t."id"
218
+ AND e."type" = 'turn.started'
219
+ )
220
+ );
221
+
222
+ -- Unstarted reusable-session machine work moves to the typed update plane.
223
+ INSERT INTO "session_system_updates" (
224
+ "account_id", "workspace_id", "session_id", "kind", "classification",
225
+ "source_id", "dedupe_key", "summary", "payload", "lineage"
226
+ )
227
+ SELECT
228
+ t."account_id", t."workspace_id", t."session_id",
229
+ CASE WHEN t."source" = 'scheduled_task' THEN 'scheduled_wake' ELSE 'runtime_notice' END,
230
+ 'info', t."id"::text, 'migrated-turn:' || t."id"::text, t."prompt",
231
+ jsonb_build_object('migratedTurnId', t."id", 'source', t."source"),
232
+ jsonb_build_object('migratedTurnId', t."id")
233
+ FROM "session_turns" t
234
+ WHERE t."status" = 'queued' AND t."source" IN ('scheduled_task','system');
235
+
236
+ -- The delivered-turn FK above is INITIALLY DEFERRED because normal runtime
237
+ -- fan-in may preallocate an update and its delivered turn in one transaction.
238
+ -- This migration still has table-level DDL below (RLS, constraints, indexes).
239
+ -- PostgreSQL refuses ALTER TABLE while a table has pending deferred constraint
240
+ -- triggers, even when every inserted delivered_turn_id is NULL. Settle the
241
+ -- migration's own inserts now; this changes only this transaction's constraint
242
+ -- mode and leaves the declared runtime default deferred.
243
+ SET CONSTRAINTS ALL IMMEDIATE;
244
+
245
+ UPDATE "session_turns"
246
+ SET "status" = 'superseded',
247
+ "cancelled_by" = 'control-plane-cutover',
248
+ "cancel_reason" = 'migrated_to_internal_update',
249
+ "finished_at" = now(),
250
+ "updated_at" = now()
251
+ WHERE "status" = 'queued' AND "source" IN ('scheduled_task','system');
252
+
253
+ -- An unstarted goal row carries no truth that is not already in session_goals;
254
+ -- the generalized wake scan will continue the active goal after cutover.
255
+ UPDATE "session_turns"
256
+ SET "status" = 'superseded',
257
+ "cancelled_by" = 'control-plane-cutover',
258
+ "cancel_reason" = 'goal_continuation_is_not_queue_work',
259
+ "finished_at" = now(),
260
+ "updated_at" = now()
261
+ WHERE "status" = 'queued' AND "source" = 'goal';
262
+
263
+ DO $$
264
+ BEGIN
265
+ IF EXISTS (
266
+ SELECT 1 FROM "session_turns"
267
+ WHERE "status" = 'queued' AND "source" NOT IN ('user','api')
268
+ ) THEN
269
+ RAISE EXCEPTION 'unclassified queued machine turn remains at control-plane cutover';
270
+ END IF;
271
+
272
+ IF EXISTS (
273
+ SELECT 1 FROM "session_turns"
274
+ WHERE "status" NOT IN (
275
+ 'queued','running','requires_action','recovering','waiting_capacity',
276
+ 'completed','failed','cancelled','superseded'
277
+ )
278
+ ) THEN
279
+ RAISE EXCEPTION 'unknown session turn status at control-plane cutover';
280
+ END IF;
281
+
282
+ IF EXISTS (
283
+ SELECT 1
284
+ FROM "session_turns"
285
+ WHERE "status" IN ('running','requires_action','recovering','waiting_capacity')
286
+ GROUP BY "workspace_id", "session_id"
287
+ HAVING count(*) > 1
288
+ ) THEN
289
+ RAISE EXCEPTION 'multiple current inferences found for one session at control-plane cutover';
290
+ END IF;
291
+
292
+ IF EXISTS (
293
+ SELECT 1 FROM "session_history_items"
294
+ WHERE "active" = true AND "item" ->> 'type' IN ('compaction','compaction_summary')
295
+ ) THEN
296
+ RAISE EXCEPTION 'active opaque remote compaction item found at control-plane cutover';
297
+ END IF;
298
+ END $$;
299
+
300
+ UPDATE "sessions" s
301
+ SET "active_turn_id" = current_turn."id"
302
+ FROM (
303
+ SELECT "workspace_id", "session_id", max("id"::text)::uuid AS "id"
304
+ FROM "session_turns"
305
+ WHERE "status" IN ('running','requires_action','recovering','waiting_capacity')
306
+ GROUP BY "workspace_id", "session_id"
307
+ ) current_turn
308
+ WHERE s."workspace_id" = current_turn."workspace_id"
309
+ AND s."id" = current_turn."session_id";
310
+
311
+ UPDATE "sessions" s
312
+ SET "active_turn_id" = NULL
313
+ WHERE NOT EXISTS (
314
+ SELECT 1 FROM "session_turns" t
315
+ WHERE t."workspace_id" = s."workspace_id"
316
+ AND t."session_id" = s."id"
317
+ AND t."status" IN ('running','requires_action','recovering','waiting_capacity')
318
+ );
319
+
320
+ UPDATE "sessions" s
321
+ SET "queue_head_position" = bounds."head",
322
+ "queue_tail_position" = bounds."tail"
323
+ FROM (
324
+ SELECT "workspace_id", "session_id",
325
+ coalesce(min("position"), 0) AS "head",
326
+ coalesce(max("position"), 0) AS "tail"
327
+ FROM "session_turns"
328
+ GROUP BY "workspace_id", "session_id"
329
+ ) bounds
330
+ WHERE s."workspace_id" = bounds."workspace_id"
331
+ AND s."id" = bounds."session_id";
332
+
333
+ UPDATE "sessions" s
334
+ SET "status" = CASE
335
+ WHEN s."control_state" = 'paused' THEN 'paused'
336
+ WHEN t."status" = 'requires_action' THEN 'requires_action'
337
+ WHEN t."status" = 'recovering' THEN 'recovering'
338
+ WHEN t."status" = 'waiting_capacity' THEN 'waiting_capacity'
339
+ WHEN t."status" = 'running' THEN 'running'
340
+ WHEN EXISTS (
341
+ SELECT 1 FROM "session_turns" q
342
+ WHERE q."workspace_id" = s."workspace_id"
343
+ AND q."session_id" = s."id" AND q."status" = 'queued'
344
+ ) THEN 'queued'
345
+ ELSE 'idle'
346
+ END
347
+ FROM "session_turns" t
348
+ WHERE t."workspace_id" = s."workspace_id"
349
+ AND t."id" = s."active_turn_id";
350
+
351
+ UPDATE "sessions" s
352
+ SET "status" = CASE
353
+ WHEN s."control_state" = 'paused' THEN 'paused'
354
+ WHEN EXISTS (
355
+ SELECT 1 FROM "session_turns" q
356
+ WHERE q."workspace_id" = s."workspace_id"
357
+ AND q."session_id" = s."id" AND q."status" = 'queued'
358
+ ) THEN 'queued'
359
+ ELSE 'idle'
360
+ END
361
+ WHERE s."active_turn_id" IS NULL;
362
+
363
+ ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_inference_state_check"
364
+ CHECK ("inference_state" IN ('active','paused'));
365
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_control_state_check"
366
+ CHECK ("control_state" IN ('active','paused'));
367
+ ALTER TABLE "sessions" ADD CONSTRAINT "sessions_pending_control_kind_check"
368
+ CHECK ("pending_control_kind" IS NULL OR "pending_control_kind" IN ('pause','steer'));
369
+ ALTER TABLE "session_turns" ADD CONSTRAINT "session_turns_status_check"
370
+ CHECK ("status" IN (
371
+ 'queued','running','requires_action','recovering','waiting_capacity',
372
+ 'completed','failed','cancelled','superseded'
373
+ ));
374
+ ALTER TABLE "session_turns" ADD CONSTRAINT "session_turns_queue_purity_check"
375
+ CHECK ("status" <> 'queued' OR "source" IN ('user','api'));
376
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_kind_check"
377
+ CHECK ("kind" IN ('child_session_update','scheduled_wake','lifecycle_event','runtime_notice'));
378
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_classification_check"
379
+ CHECK ("classification" IN ('success','failure','action_required','info'));
380
+ ALTER TABLE "session_system_updates" ADD CONSTRAINT "system_updates_state_check"
381
+ CHECK ("state" IN ('pending','delivered','cancelled','failed'));
382
+ ALTER TABLE "runtime_control_operations" ADD CONSTRAINT "runtime_control_operations_scope_check"
383
+ CHECK ("scope" IN ('session','workspace'));
384
+ ALTER TABLE "session_system_update_outbox" ADD CONSTRAINT "system_update_outbox_status_check"
385
+ CHECK ("status" IN ('pending','delivered','failed'));
386
+ ALTER TABLE "session_events" ADD CONSTRAINT "session_events_turn_association_check"
387
+ CHECK ("turn_association" IS NULL OR "turn_association" IN ('current','late_rejected'));
388
+
389
+ CREATE UNIQUE INDEX "session_turns_one_current_inference_uq"
390
+ ON "session_turns" ("workspace_id", "session_id")
391
+ WHERE "status" IN ('running','requires_action','recovering','waiting_capacity');
392
+ CREATE INDEX "session_turns_prompt_queue_idx"
393
+ ON "session_turns" ("workspace_id", "session_id", "position")
394
+ WHERE "status" = 'queued';
395
+ CREATE UNIQUE INDEX "session_system_updates_dedupe_uq"
396
+ ON "session_system_updates" ("workspace_id", "session_id", "dedupe_key");
397
+ CREATE INDEX "session_system_updates_pending_idx"
398
+ ON "session_system_updates" ("workspace_id", "session_id", "state", "created_at");
399
+ CREATE UNIQUE INDEX "runtime_control_operations_client_uq"
400
+ ON "runtime_control_operations" ("workspace_id", "client_event_id");
401
+ CREATE INDEX "runtime_control_operations_target_idx"
402
+ ON "runtime_control_operations" ("workspace_id", "scope", "target_id", "created_at");
403
+ CREATE UNIQUE INDEX "session_system_update_outbox_dedupe_uq"
404
+ ON "session_system_update_outbox" ("workspace_id", "dedupe_key");
405
+ CREATE INDEX "session_system_update_outbox_pending_idx"
406
+ ON "session_system_update_outbox" ("status", "created_at");
407
+ CREATE UNIQUE INDEX "session_pending_tool_calls_turn_call_idx"
408
+ ON "session_pending_tool_calls" ("workspace_id", "turn_id", "call_id");
409
+ CREATE INDEX "session_pending_tool_calls_session_turn_idx"
410
+ ON "session_pending_tool_calls" ("workspace_id", "session_id", "turn_id");
411
+ CREATE UNIQUE INDEX "scheduled_task_runs_producer_key_uq"
412
+ ON "scheduled_task_runs" ("workspace_id", "producer_key")
413
+ WHERE "producer_key" IS NOT NULL;
414
+
415
+ CREATE OR REPLACE FUNCTION opengeni_private.enforce_session_inference_claim()
416
+ RETURNS trigger
417
+ LANGUAGE plpgsql
418
+ SECURITY DEFINER
419
+ SET search_path = pg_catalog
420
+ AS $$
421
+ BEGIN
422
+ IF NEW.status = 'running' AND OLD.status <> 'running'
423
+ AND coalesce(current_setting('opengeni.session_inference_claim', true), '') <> '1' THEN
424
+ RAISE EXCEPTION 'session inference must be claimed through the fenced claim transaction'
425
+ USING ERRCODE = '55000';
426
+ END IF;
427
+ RETURN NEW;
428
+ END $$;
429
+ CREATE TRIGGER session_turns_inference_claim_guard
430
+ BEFORE UPDATE OF status ON "session_turns" FOR EACH ROW
431
+ EXECUTE FUNCTION opengeni_private.enforce_session_inference_claim();
432
+
433
+ ALTER TABLE "session_system_updates" ENABLE ROW LEVEL SECURITY;
434
+ ALTER TABLE "session_system_updates" FORCE ROW LEVEL SECURITY;
435
+ ALTER TABLE "runtime_control_operations" ENABLE ROW LEVEL SECURITY;
436
+ ALTER TABLE "runtime_control_operations" FORCE ROW LEVEL SECURITY;
437
+ ALTER TABLE "session_system_update_outbox" ENABLE ROW LEVEL SECURITY;
438
+ ALTER TABLE "session_system_update_outbox" FORCE ROW LEVEL SECURITY;
439
+ ALTER TABLE "session_pending_tool_calls" ENABLE ROW LEVEL SECURITY;
440
+ ALTER TABLE "session_pending_tool_calls" FORCE ROW LEVEL SECURITY;
441
+ CREATE POLICY workspace_isolation ON "session_system_updates"
442
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
443
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
444
+ CREATE POLICY workspace_isolation ON "runtime_control_operations"
445
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
446
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
447
+ CREATE POLICY workspace_isolation ON "session_system_update_outbox"
448
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
449
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
450
+ CREATE POLICY workspace_isolation ON "session_pending_tool_calls"
451
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
452
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
453
+
454
+ -- Bounded cross-workspace recovery claim for the worker outbox reconciler.
455
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_session_system_update_outbox(p_limit integer)
456
+ RETURNS TABLE (
457
+ id uuid, account_id uuid, workspace_id uuid, source_session_id uuid,
458
+ target_session_id uuid, dedupe_key text, kind text, classification text,
459
+ source_id text, summary text, payload jsonb, lineage jsonb
460
+ )
461
+ LANGUAGE plpgsql
462
+ SECURITY DEFINER
463
+ AS $$
464
+ BEGIN
465
+ RETURN QUERY
466
+ WITH claimed AS (
467
+ SELECT o.id FROM session_system_update_outbox o
468
+ WHERE o.status = 'pending'
469
+ ORDER BY o.created_at, o.id
470
+ FOR UPDATE SKIP LOCKED
471
+ LIMIT greatest(1, least(coalesce(p_limit, 100), 100))
472
+ )
473
+ UPDATE session_system_update_outbox o
474
+ SET attempts = o.attempts + 1, updated_at = now()
475
+ FROM claimed c WHERE o.id = c.id
476
+ RETURNING o.id, o.account_id, o.workspace_id, o.source_session_id,
477
+ o.target_session_id, o.dedupe_key, o.kind, o.classification,
478
+ o.source_id, o.summary, o.payload, o.lineage;
479
+ END $$;
480
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_system_update_outbox(integer) FROM PUBLIC;
481
+
482
+ -- Generalized post-cutover repair scan. It returns each runnable session once;
483
+ -- the workflow claim transaction chooses recovery, prompt, then update/goal.
484
+ CREATE OR REPLACE FUNCTION opengeni_private.list_claimable_sessions(p_limit integer)
485
+ RETURNS TABLE (
486
+ account_id uuid, workspace_id uuid, session_id uuid, temporal_workflow_id text
487
+ )
488
+ LANGUAGE sql
489
+ SECURITY DEFINER
490
+ AS $$
491
+ SELECT DISTINCT s.account_id, s.workspace_id, s.id,
492
+ coalesce(s.temporal_workflow_id, 'session-' || s.id::text)
493
+ FROM sessions s
494
+ JOIN workspaces w ON w.id = s.workspace_id
495
+ WHERE s.control_state = 'active'
496
+ AND (
497
+ w.inference_state = 'active'
498
+ OR s.workspace_run_exception_generation = w.inference_generation
499
+ )
500
+ AND s.pending_control_event_id IS NULL
501
+ AND (
502
+ EXISTS (
503
+ SELECT 1 FROM session_turns t
504
+ WHERE t.workspace_id = s.workspace_id AND t.session_id = s.id
505
+ AND t.status IN ('queued','recovering','waiting_capacity')
506
+ )
507
+ OR EXISTS (
508
+ SELECT 1 FROM session_system_updates u
509
+ WHERE u.workspace_id = s.workspace_id AND u.session_id = s.id
510
+ AND u.state = 'pending'
511
+ )
512
+ OR EXISTS (
513
+ SELECT 1 FROM session_goals g
514
+ WHERE g.workspace_id = s.workspace_id AND g.session_id = s.id
515
+ AND g.status = 'active'
516
+ )
517
+ )
518
+ ORDER BY s.id
519
+ LIMIT greatest(1, least(coalesce(p_limit, 1000), 10000));
520
+ $$;
521
+ REVOKE ALL ON FUNCTION opengeni_private.list_claimable_sessions(integer) FROM PUBLIC;
522
+
523
+ DO $$
524
+ DECLARE target_schema text := current_schema();
525
+ BEGIN
526
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
527
+ EXECUTE format(
528
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO opengeni_app',
529
+ target_schema
530
+ );
531
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_system_update_outbox(integer)
532
+ TO opengeni_app;
533
+ GRANT EXECUTE ON FUNCTION opengeni_private.list_claimable_sessions(integer)
534
+ TO opengeni_app;
535
+ END IF;
536
+ END $$;