@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.
- package/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
- package/dist/chunk-7LDU7F5P.js.map +1 -0
- package/dist/chunk-B22X3IEZ.js +2634 -0
- package/dist/chunk-B22X3IEZ.js.map +1 -0
- package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
- package/dist/chunk-YFQ7SGE4.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16571 -5018
- package/dist/index.js.map +1 -1
- package/dist/migrate.js +1 -1
- package/dist/provision-roles.d.ts +2121 -248
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-BUbuMteO.d.ts → schema-BN5mB9xZ.d.ts} +8716 -3432
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +43 -5
- package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
- package/drizzle/0045_workspace_captures.sql +83 -0
- package/drizzle/0045_workspace_memory_v1.sql +71 -0
- package/drizzle/0046_variable_sets_rename.sql +56 -0
- package/drizzle/0047_rigs.sql +151 -0
- package/drizzle/0048_rig_runtime.sql +9 -0
- package/drizzle/0049_enrollment_went_offline.sql +28 -0
- package/drizzle/0050_enrollment_op_stream.sql +1 -0
- package/drizzle/0051_codex_pin_source.sql +48 -0
- package/drizzle/0052_file_upload_cleanup.sql +91 -0
- package/drizzle/0053_codex_credential_leases.sql +230 -0
- package/drizzle/0054_session_pins.sql +85 -0
- package/drizzle/0055_session_list_snapshots.sql +73 -0
- package/drizzle/0056_workspace_model_policies.sql +48 -0
- package/drizzle/0057_durable_queue_control.sql +536 -0
- package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
- package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
- package/drizzle/0060_session_system_update_deferral.sql +10 -0
- package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
- package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
- package/drizzle/0063_session_control_mega_foundation.sql +1324 -0
- package/package.json +13 -13
- package/src/codex-token-resolver.ts +58 -23
- package/src/connection-token-resolver.ts +146 -57
- package/src/environment-crypto.ts +5 -1
- package/src/event-payload-sanitizer.ts +29 -1
- package/src/index.ts +20990 -6465
- package/src/memory-domain.ts +218 -0
- package/src/migrate.ts +58 -3
- package/src/provision-roles.ts +46 -17
- package/src/schema.ts +2888 -1121
- package/src/session-control.ts +1759 -0
- package/src/session-queue-commands.ts +1753 -0
- package/src/session-tool-call-settlement.ts +269 -0
- package/dist/chunk-57MLICFR.js.map +0 -1
- package/dist/chunk-OGCE6O2X.js.map +0 -1
- package/dist/chunk-ZIUCA2IO.js +0 -1268
- 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;
|