@opengeni/db 0.9.3 → 0.12.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 (54) hide show
  1. package/dist/{chunk-4LG5NBTC.js → chunk-VUKRIBO5.js} +577 -12
  2. package/dist/chunk-VUKRIBO5.js.map +1 -0
  3. package/dist/{chunk-KW526IJA.js → chunk-Y5WZZVQK.js} +80 -4
  4. package/dist/chunk-Y5WZZVQK.js.map +1 -0
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.js +6372 -2189
  7. package/dist/index.js.map +1 -1
  8. package/dist/migrate.d.ts +6 -3
  9. package/dist/migrate.js +1 -1
  10. package/dist/provision-roles.d.ts +1122 -91
  11. package/dist/{schema-CdPGTHlD.d.ts → schema-CnpD6BcX.d.ts} +5908 -3626
  12. package/dist/schema.d.ts +1 -1
  13. package/dist/schema.js +19 -1
  14. package/drizzle/0053_codex_credential_leases.sql +2 -2
  15. package/drizzle/0057_durable_queue_control.sql +1 -1
  16. package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
  17. package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
  18. package/drizzle/0063_session_control_mega_foundation.sql +1 -1
  19. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +1 -1
  20. package/drizzle/0065_codex_subscription_overview.sql +168 -0
  21. package/drizzle/0065_session_tool_policy.sql +38 -0
  22. package/drizzle/0067_session_event_payload_bounds.sql +2 -2
  23. package/drizzle/0068_workspace_control_event_bounds.sql +2 -2
  24. package/drizzle/0069_session_event_history_backfill.sql +2 -2
  25. package/drizzle/0074_session_activity_revisions.sql +2 -2
  26. package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
  27. package/drizzle/0107_host_export_lineage_contract.sql +381 -0
  28. package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
  29. package/drizzle/0109_nested_agent_depth_expand.sql +42 -0
  30. package/drizzle/0110_nested_agent_depth_boundary.sql +480 -0
  31. package/drizzle/0111_nested_agent_depth_backfill.sql +49 -0
  32. package/drizzle/0112_nested_agent_depth_contract.sql +38 -0
  33. package/drizzle/0113_nested_agent_depth_validate.sql +13 -0
  34. package/drizzle/0114_nested_agent_depth_contract.sql +49 -0
  35. package/drizzle/0115_nested_agent_depth_validate.sql +11 -0
  36. package/drizzle/0116_nested_agent_depth_index.sql +4 -0
  37. package/drizzle/0117_sandbox_recovery_generations.sql +699 -0
  38. package/drizzle/0118_new_session_drafts.sql +59 -0
  39. package/drizzle/0119_pending_tool_output_policy.sql +5 -0
  40. package/drizzle/0120_durable_goal_wake.sql +360 -0
  41. package/drizzle/0121_goal_update_idempotency.sql +11 -0
  42. package/package.json +5 -4
  43. package/src/codex-token-resolver.ts +175 -14
  44. package/src/connection-token-resolver.ts +143 -120
  45. package/src/event-payload-sanitizer.ts +32 -2
  46. package/src/index.ts +7734 -1330
  47. package/src/migrate.ts +131 -2
  48. package/src/new-session-drafts.ts +144 -0
  49. package/src/schema.ts +626 -16
  50. package/src/session-control.ts +44 -18
  51. package/src/session-queue-commands.ts +94 -21
  52. package/src/session-tool-call-settlement.ts +6 -1
  53. package/dist/chunk-4LG5NBTC.js.map +0 -1
  54. package/dist/chunk-KW526IJA.js.map +0 -1
@@ -0,0 +1,381 @@
1
+ -- deployment-mode: rolling
2
+ -- Forward-only repair for published 0097/0103/0104 histories. Immutable root
3
+ -- capture already exists after 0103; this migration linearizes first-consumer
4
+ -- enablement with deferred producers and validates the session/root invariant.
5
+ -- It never derives or rewrites historical lineage. Rows that may have been
6
+ -- populated by the old 0104 backfill require explicit evidence-backed
7
+ -- disposition before retry.
8
+
9
+ SET LOCAL lock_timeout = '5s';
10
+ SET LOCAL statement_timeout = '5min';
11
+
12
+ -- Replacing these SECURITY DEFINER functions preserves their identity, owner,
13
+ -- and existing exporter ACL while making the config-row lock the commit-order
14
+ -- boundary between first-consumer registration and deferred source capture.
15
+ DO $migration$
16
+ DECLARE target_schema text := current_schema();
17
+ BEGIN
18
+ EXECUTE format($create$
19
+ CREATE OR REPLACE FUNCTION opengeni_private.enqueue_host_session_event_export()
20
+ RETURNS trigger
21
+ LANGUAGE plpgsql
22
+ SECURITY DEFINER
23
+ SET search_path = pg_catalog
24
+ AS $function$
25
+ DECLARE
26
+ v_enabled boolean;
27
+ v_initiator jsonb;
28
+ v_context jsonb := '{}'::jsonb;
29
+ v_origin text;
30
+ v_payload_bytes integer;
31
+ BEGIN
32
+ IF NEW.type IN (
33
+ 'agent.message.delta', 'agent.reasoning.delta',
34
+ 'sandbox.command.output.delta', 'terminal.pty.output.delta'
35
+ ) THEN
36
+ RETURN NEW;
37
+ END IF;
38
+
39
+ SELECT c.session_events_enabled INTO v_enabled
40
+ FROM %1$I.host_export_config c WHERE c.id = 1
41
+ FOR SHARE;
42
+ IF coalesce(v_enabled, false) = false THEN
43
+ RETURN NEW;
44
+ END IF;
45
+
46
+ IF NEW.turn_id IS NOT NULL THEN
47
+ SELECT
48
+ CASE WHEN octet_length(t.initiator_subject_id) <= 1024 THEN
49
+ jsonb_strip_nulls(jsonb_build_object(
50
+ 'kind', t.initiator_kind,
51
+ 'subjectId', t.initiator_subject_id,
52
+ 'label', CASE
53
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
54
+ THEN left(t.initiator_context ->> 'label', 256)
55
+ ELSE NULL
56
+ END
57
+ ))
58
+ ELSE NULL END,
59
+ jsonb_strip_nulls(jsonb_build_object(
60
+ 'label', CASE
61
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
62
+ THEN left(t.initiator_context ->> 'label', 256)
63
+ ELSE NULL
64
+ END,
65
+ 'backfill', CASE
66
+ WHEN jsonb_typeof(t.initiator_context -> 'backfill') = 'boolean'
67
+ THEN t.initiator_context -> 'backfill'
68
+ ELSE NULL
69
+ END,
70
+ 'attributionOmitted', CASE
71
+ WHEN octet_length(t.initiator_subject_id) > 1024 THEN 'subject_id_too_large'
72
+ ELSE NULL
73
+ END
74
+ )),
75
+ t.source
76
+ INTO v_initiator, v_context, v_origin
77
+ FROM %1$I.session_turns t
78
+ WHERE t.workspace_id = NEW.workspace_id AND t.id = NEW.turn_id;
79
+ ELSIF NEW.type = 'session.created' THEN
80
+ SELECT
81
+ CASE WHEN octet_length(s.created_by_subject_id) <= 1024 THEN
82
+ jsonb_strip_nulls(jsonb_build_object(
83
+ 'kind', s.created_by_kind,
84
+ 'subjectId', s.created_by_subject_id,
85
+ 'label', CASE
86
+ WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
87
+ THEN left(s.created_by_context ->> 'label', 256)
88
+ ELSE NULL
89
+ END
90
+ ))
91
+ ELSE NULL END,
92
+ jsonb_strip_nulls(jsonb_build_object(
93
+ 'label', CASE
94
+ WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
95
+ THEN left(s.created_by_context ->> 'label', 256)
96
+ ELSE NULL
97
+ END,
98
+ 'backfill', CASE
99
+ WHEN jsonb_typeof(s.created_by_context -> 'backfill') = 'boolean'
100
+ THEN s.created_by_context -> 'backfill'
101
+ ELSE NULL
102
+ END,
103
+ 'attributionOmitted', CASE
104
+ WHEN octet_length(s.created_by_subject_id) > 1024 THEN 'subject_id_too_large'
105
+ ELSE NULL
106
+ END
107
+ )),
108
+ NULL
109
+ INTO v_initiator, v_context, v_origin
110
+ FROM %1$I.sessions s
111
+ WHERE s.workspace_id = NEW.workspace_id AND s.id = NEW.session_id;
112
+ END IF;
113
+
114
+ v_payload_bytes := octet_length(NEW.payload::text)
115
+ + octet_length(NEW.type)
116
+ + coalesce(octet_length(NEW.client_event_id), 0)
117
+ + coalesce(octet_length(NEW.turn_association), 0)
118
+ + coalesce(octet_length(NEW.duplicate_reason), 0)
119
+ + octet_length(coalesce(v_initiator, 'null'::jsonb)::text)
120
+ + octet_length(v_context::text)
121
+ + 768;
122
+ INSERT INTO %1$I.host_export_outbox (
123
+ export_kind, source_id, account_id, workspace_id, session_id,
124
+ turn_id, turn_generation, turn_attempt_id, session_sequence,
125
+ client_event_id, turn_association, duplicate_of_event_id, duplicate_reason,
126
+ event_type, idempotency_key, initiator, initiator_context, origin,
127
+ payload, envelope_bytes, occurred_at, source_recorded_at, enqueued_at
128
+ ) VALUES (
129
+ 'session_event', NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id,
130
+ NEW.turn_id, NEW.turn_generation, NEW.turn_attempt_id, NEW.sequence,
131
+ NEW.client_event_id, NEW.turn_association, NEW.duplicate_of_event_id,
132
+ NEW.duplicate_reason,
133
+ NEW.type, 'session_event:' || NEW.id::text, v_initiator,
134
+ coalesce(v_context, '{}'::jsonb), v_origin, NEW.payload,
135
+ greatest(1, v_payload_bytes), NEW.occurred_at,
136
+ NEW.created_at, clock_timestamp()
137
+ )
138
+ ON CONFLICT (export_kind, source_id) DO NOTHING;
139
+ RETURN NEW;
140
+ END $function$;
141
+ $create$, target_schema);
142
+
143
+ EXECUTE format($create$
144
+ CREATE OR REPLACE FUNCTION opengeni_private.enqueue_host_usage_event_export()
145
+ RETURNS trigger
146
+ LANGUAGE plpgsql
147
+ SECURITY DEFINER
148
+ SET search_path = pg_catalog
149
+ AS $function$
150
+ DECLARE
151
+ v_enabled boolean;
152
+ v_initiator jsonb := CASE
153
+ WHEN NEW.initiator_kind IS NOT NULL THEN jsonb_strip_nulls(jsonb_build_object(
154
+ 'kind', NEW.initiator_kind,
155
+ 'subjectId', NEW.initiator_subject_id,
156
+ 'label', CASE
157
+ WHEN jsonb_typeof(NEW.initiator_context -> 'label') = 'string'
158
+ THEN left(NEW.initiator_context ->> 'label', 256)
159
+ ELSE NULL
160
+ END
161
+ ))
162
+ ELSE NULL
163
+ END;
164
+ v_context jsonb := jsonb_strip_nulls(jsonb_build_object(
165
+ 'label', CASE
166
+ WHEN jsonb_typeof(NEW.initiator_context -> 'label') = 'string'
167
+ THEN left(NEW.initiator_context ->> 'label', 256)
168
+ ELSE NULL
169
+ END,
170
+ 'backfill', CASE
171
+ WHEN jsonb_typeof(NEW.initiator_context -> 'backfill') = 'boolean'
172
+ THEN NEW.initiator_context -> 'backfill'
173
+ ELSE NULL
174
+ END
175
+ ));
176
+ v_origin text := NEW.origin;
177
+ v_payload jsonb;
178
+ v_payload_bytes integer;
179
+ BEGIN
180
+ SELECT c.usage_events_enabled INTO v_enabled
181
+ FROM %1$I.host_export_config c WHERE c.id = 1
182
+ FOR SHARE;
183
+ IF coalesce(v_enabled, false) = false THEN
184
+ RETURN NEW;
185
+ END IF;
186
+
187
+ -- Export-specific bounds apply only while the optional host stream is
188
+ -- enabled. Standalone/custom usage writers retain their historical
189
+ -- behavior; an embedded writer that cannot be represented fails its
190
+ -- source transaction visibly instead of committing a poison outbox row.
191
+ IF octet_length(NEW.event_type) NOT BETWEEN 1 AND 256
192
+ OR octet_length(NEW.unit) NOT BETWEEN 1 AND 128
193
+ OR (NEW.subject_id IS NOT NULL AND octet_length(NEW.subject_id) > 1024)
194
+ OR (NEW.source_resource_type IS NOT NULL
195
+ AND octet_length(NEW.source_resource_type) > 256)
196
+ OR (NEW.source_resource_id IS NOT NULL
197
+ AND octet_length(NEW.source_resource_id) > 2048)
198
+ OR octet_length(NEW.idempotency_key) NOT BETWEEN 1 AND 2048
199
+ OR (NEW.billing_provider_event_id IS NOT NULL
200
+ AND octet_length(NEW.billing_provider_event_id) > 2048) THEN
201
+ RAISE EXCEPTION 'usage event exceeds the enabled host-export wire bounds'
202
+ USING ERRCODE = '22001';
203
+ END IF;
204
+
205
+ IF NEW.turn_id IS NOT NULL THEN
206
+ SELECT
207
+ CASE WHEN octet_length(t.initiator_subject_id) <= 1024 THEN
208
+ jsonb_strip_nulls(jsonb_build_object(
209
+ 'kind', t.initiator_kind,
210
+ 'subjectId', t.initiator_subject_id,
211
+ 'label', CASE
212
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
213
+ THEN left(t.initiator_context ->> 'label', 256)
214
+ ELSE NULL
215
+ END
216
+ ))
217
+ ELSE NULL END,
218
+ jsonb_strip_nulls(jsonb_build_object(
219
+ 'label', CASE
220
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
221
+ THEN left(t.initiator_context ->> 'label', 256)
222
+ ELSE NULL
223
+ END,
224
+ 'backfill', CASE
225
+ WHEN jsonb_typeof(t.initiator_context -> 'backfill') = 'boolean'
226
+ THEN t.initiator_context -> 'backfill'
227
+ ELSE NULL
228
+ END,
229
+ 'attributionOmitted', CASE
230
+ WHEN octet_length(t.initiator_subject_id) > 1024 THEN 'subject_id_too_large'
231
+ ELSE NULL
232
+ END
233
+ )),
234
+ t.source
235
+ INTO v_initiator, v_context, v_origin
236
+ FROM %1$I.session_turns t
237
+ WHERE t.workspace_id = NEW.workspace_id AND t.id = NEW.turn_id;
238
+ ELSIF v_initiator IS NULL AND NEW.subject_id IS NOT NULL
239
+ AND octet_length(NEW.subject_id) <= 1024 THEN
240
+ v_initiator := jsonb_build_object('kind', 'subject', 'subjectId', NEW.subject_id);
241
+ END IF;
242
+
243
+ v_payload := jsonb_build_object(
244
+ 'id', NEW.id,
245
+ 'workspaceId', NEW.workspace_id,
246
+ 'accountId', NEW.account_id,
247
+ 'subjectId', NEW.subject_id,
248
+ 'eventType', NEW.event_type,
249
+ 'quantity', NEW.quantity,
250
+ 'unit', NEW.unit,
251
+ 'sourceResourceType', NEW.source_resource_type,
252
+ 'sourceResourceId', NEW.source_resource_id,
253
+ 'idempotencyKey', NEW.idempotency_key,
254
+ 'occurredAt', NEW.occurred_at,
255
+ 'recordedAt', NEW.recorded_at,
256
+ 'exportedToBillingAt', NEW.exported_to_billing_at,
257
+ 'billingProviderEventId', NEW.billing_provider_event_id
258
+ );
259
+ v_payload_bytes := octet_length(v_payload::text)
260
+ + octet_length(coalesce(v_initiator, 'null'::jsonb)::text)
261
+ + octet_length(v_context::text)
262
+ + 768;
263
+ INSERT INTO %1$I.host_export_outbox (
264
+ export_kind, source_id, account_id, workspace_id, session_id,
265
+ turn_id, turn_attempt_id, event_type, idempotency_key, initiator,
266
+ initiator_context, origin, payload, envelope_bytes, occurred_at,
267
+ source_recorded_at, enqueued_at
268
+ ) VALUES (
269
+ 'usage_event', NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id,
270
+ NEW.turn_id, NEW.turn_attempt_id, NEW.event_type, NEW.idempotency_key,
271
+ v_initiator, coalesce(v_context, '{}'::jsonb), v_origin, v_payload,
272
+ greatest(1, v_payload_bytes), NEW.occurred_at,
273
+ NEW.recorded_at, clock_timestamp()
274
+ )
275
+ ON CONFLICT (export_kind, source_id) DO NOTHING;
276
+ RETURN NEW;
277
+ END $function$;
278
+ $create$, target_schema);
279
+ END $migration$;
280
+
281
+ REVOKE ALL ON FUNCTION
282
+ opengeni_private.enqueue_host_session_event_export() FROM PUBLIC;
283
+ REVOKE ALL ON FUNCTION
284
+ opengeni_private.enqueue_host_usage_event_export() FROM PUBLIC;
285
+
286
+ -- The old 0104 backfill resolved lineage from then-current session ancestry.
287
+ -- A session-scoped row enqueued before 0103's durable ledger boundary therefore
288
+ -- lacks immutable-capture provenance even when its root is non-null. Reject the
289
+ -- candidate without touching data. The single-row probe is bounded by the
290
+ -- migration statement timeout; operators must disposition suspect history
291
+ -- separately and rerun the same migration.
292
+ DO $migration$
293
+ DECLARE
294
+ capture_applied_at timestamptz;
295
+ capture_ledger_rows integer;
296
+ suspect_source_id uuid;
297
+ BEGIN
298
+ SELECT min(m.applied_at), count(*)::integer
299
+ INTO capture_applied_at, capture_ledger_rows
300
+ FROM "schema_migrations" m
301
+ WHERE m.name = '0103_host_export_root_session.sql';
302
+
303
+ IF capture_ledger_rows <> 1 OR capture_applied_at IS NULL THEN
304
+ RAISE EXCEPTION '0107 requires the exact 0103 schema-migration ledger boundary'
305
+ USING ERRCODE = '55000',
306
+ HINT = 'Apply migrations through immutable 0104 with the canonical runner before retrying 0107.';
307
+ END IF;
308
+
309
+ SELECT o.source_id
310
+ INTO suspect_source_id
311
+ FROM "host_export_outbox" o
312
+ WHERE o.session_id IS NOT NULL
313
+ AND (
314
+ o.root_session_id IS NULL
315
+ OR o.enqueued_at < capture_applied_at
316
+ )
317
+ ORDER BY o.enqueued_at, o.source_id
318
+ LIMIT 1;
319
+
320
+ IF suspect_source_id IS NOT NULL THEN
321
+ RAISE EXCEPTION '0107 found host-export lineage without immutable-capture provenance'
322
+ USING ERRCODE = '23514',
323
+ HINT = 'Do not derive lineage from current sessions; use an evidence-backed maintenance disposition, then retry 0107.';
324
+ END IF;
325
+ END $migration$;
326
+
327
+ DO $migration$
328
+ DECLARE
329
+ existing_expression text;
330
+ normalized_expression text;
331
+ BEGIN
332
+ SELECT pg_get_expr(c.conbin, c.conrelid, true)
333
+ INTO existing_expression
334
+ FROM pg_catalog.pg_constraint c
335
+ WHERE c.conname = 'host_export_outbox_root_session_check'
336
+ AND c.conrelid = 'host_export_outbox'::regclass
337
+ AND c.contype = 'c'
338
+ AND NOT c.connoinherit;
339
+
340
+ IF existing_expression IS NULL THEN
341
+ IF EXISTS (
342
+ SELECT 1
343
+ FROM pg_catalog.pg_constraint c
344
+ WHERE c.conname = 'host_export_outbox_root_session_check'
345
+ AND c.conrelid = 'host_export_outbox'::regclass
346
+ ) THEN
347
+ RAISE EXCEPTION 'host_export_outbox_root_session_check has an incompatible constraint type'
348
+ USING ERRCODE = '55000';
349
+ END IF;
350
+
351
+ ALTER TABLE "host_export_outbox"
352
+ ADD CONSTRAINT "host_export_outbox_root_session_check"
353
+ CHECK ("session_id" IS NULL OR "root_session_id" IS NOT NULL)
354
+ NOT VALID;
355
+ ELSE
356
+ normalized_expression := lower(regexp_replace(
357
+ existing_expression,
358
+ '["()[:space:]]',
359
+ '',
360
+ 'g'
361
+ ));
362
+ IF normalized_expression <> 'session_idisnullorroot_session_idisnotnull' THEN
363
+ RAISE EXCEPTION 'host_export_outbox_root_session_check has an incompatible expression'
364
+ USING ERRCODE = '55000';
365
+ END IF;
366
+ END IF;
367
+ END $migration$;
368
+
369
+ -- VALIDATE takes SHARE UPDATE EXCLUSIVE, which permits ordinary reads/writes.
370
+ -- Both lock acquisition and the read-only table scan are source-bounded above;
371
+ -- timeout is a retryable operator disposition, never permission to rewrite data.
372
+ DO $migration$
373
+ BEGIN
374
+ ALTER TABLE "host_export_outbox"
375
+ VALIDATE CONSTRAINT "host_export_outbox_root_session_check";
376
+ EXCEPTION
377
+ WHEN check_violation THEN
378
+ RAISE EXCEPTION '0107 cannot validate host-export lineage without historical provenance'
379
+ USING ERRCODE = '23514',
380
+ HINT = 'Do not derive lineage from current sessions; use an evidence-backed maintenance disposition, then retry 0107.';
381
+ END $migration$;
@@ -0,0 +1,76 @@
1
+ -- deployment-mode: rolling
2
+ -- Fence provider creates that outlive a failed or expired warming acquisition.
3
+ --
4
+ -- A warming row can be invalidated while a provider create is still
5
+ -- non-abortably in flight. Advancing lease_epoch in the same transaction that
6
+ -- exposes cold/draining means a successor can never reuse the old acquisition
7
+ -- epoch, so late record/commit/fail/cleanup callbacks fail their existing CAS.
8
+
9
+ CREATE OR REPLACE FUNCTION opengeni_private.reap_sandbox_leases(
10
+ p_viewer_holder_ttl_ms bigint,
11
+ p_turn_holder_ttl_ms bigint,
12
+ p_idle_grace_ms bigint
13
+ )
14
+ RETURNS TABLE (workspace_id uuid, sandbox_group_id uuid, instance_id text, lease_epoch integer)
15
+ LANGUAGE plpgsql
16
+ SECURITY DEFINER
17
+ AS $$
18
+ BEGIN
19
+ DELETE FROM sandbox_lease_holders h
20
+ WHERE h.kind = 'viewer'
21
+ AND h.last_heartbeat_at < now() - make_interval(secs => p_viewer_holder_ttl_ms / 1000.0);
22
+
23
+ IF p_turn_holder_ttl_ms > 0 THEN
24
+ DELETE FROM sandbox_lease_holders h
25
+ WHERE h.kind = 'turn'
26
+ AND h.last_heartbeat_at < now() - make_interval(secs => p_turn_holder_ttl_ms / 1000.0);
27
+ END IF;
28
+
29
+ UPDATE sandbox_leases L SET
30
+ refcount = c.total,
31
+ turn_holders = c.turns,
32
+ viewer_holders = c.viewers,
33
+ liveness = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
34
+ THEN 'draining' ELSE L.liveness END,
35
+ expires_at = CASE WHEN L.liveness = 'warm' AND c.total = 0 AND c.turns = 0
36
+ THEN now() + make_interval(secs => p_idle_grace_ms / 1000.0)
37
+ ELSE L.expires_at END,
38
+ updated_at = now()
39
+ FROM (
40
+ SELECT L2.id,
41
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id)::int AS total,
42
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'turn')::int AS turns,
43
+ (SELECT count(*) FROM sandbox_lease_holders h WHERE h.lease_id = L2.id AND h.kind = 'viewer')::int AS viewers
44
+ FROM sandbox_leases L2
45
+ ) c
46
+ WHERE L.id = c.id;
47
+
48
+ -- The old warming epoch is permanently closed before a successor can acquire.
49
+ UPDATE sandbox_leases AS L SET
50
+ liveness = 'cold', instance_id = NULL,
51
+ resume_backend_id = NULL, resume_state = NULL,
52
+ data_plane_url = NULL, terminal_data_plane_url = NULL,
53
+ lease_epoch = L.lease_epoch + 1,
54
+ updated_at = now()
55
+ WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NULL;
56
+
57
+ -- Keep an attributed provider id for the drain/terminate path, but fence the
58
+ -- expired creator before exposing the row as drainable.
59
+ UPDATE sandbox_leases AS L SET
60
+ liveness = 'draining',
61
+ refcount = 0,
62
+ turn_holders = 0,
63
+ viewer_holders = 0,
64
+ data_plane_url = NULL,
65
+ terminal_data_plane_url = NULL,
66
+ lease_epoch = L.lease_epoch + 1,
67
+ expires_at = now() - interval '1 millisecond',
68
+ updated_at = now()
69
+ WHERE L.liveness = 'warming' AND L.expires_at < now() AND L.instance_id IS NOT NULL;
70
+
71
+ RETURN QUERY
72
+ SELECT L.workspace_id, L.sandbox_group_id, L.instance_id, L.lease_epoch
73
+ FROM sandbox_leases L
74
+ WHERE L.liveness = 'draining' AND L.expires_at < now() AND L.refcount = 0;
75
+ END;
76
+ $$;
@@ -0,0 +1,42 @@
1
+ -- deployment-mode: rolling
2
+ -- Expand phase: persist deployment policy and add nullable lineage columns.
3
+ SET lock_timeout = '5s';
4
+ SET statement_timeout = '30s';
5
+
6
+ CREATE TABLE "nested_agent_depth_configuration" (
7
+ "singleton" boolean PRIMARY KEY DEFAULT true NOT NULL,
8
+ "max_nested_agent_depth" integer NOT NULL,
9
+ "policy_source" text NOT NULL,
10
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
11
+ CONSTRAINT "nested_agent_depth_configuration_singleton_check" CHECK ("singleton"),
12
+ CONSTRAINT "nested_agent_depth_configuration_max_check"
13
+ CHECK ("max_nested_agent_depth" >= 0 AND "max_nested_agent_depth" <= 2147483647),
14
+ CONSTRAINT "nested_agent_depth_configuration_source_check"
15
+ CHECK ("policy_source" IN ('deployment', 'default'))
16
+ );
17
+
18
+ DO $grants$
19
+ BEGIN
20
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
21
+ EXECUTE format('GRANT SELECT ON %I."nested_agent_depth_configuration" TO opengeni_app', current_schema());
22
+ END IF;
23
+ END $grants$;
24
+
25
+ INSERT INTO "nested_agent_depth_configuration" (
26
+ "singleton", "max_nested_agent_depth", "policy_source"
27
+ ) VALUES (
28
+ true,
29
+ coalesce(nullif(current_setting('opengeni.max_nested_agent_depth', true), ''), '3')::integer,
30
+ coalesce(nullif(current_setting('opengeni.nested_agent_depth_policy_source', true), ''), 'default')
31
+ ) ON CONFLICT ("singleton") DO NOTHING;
32
+
33
+ ALTER TABLE "sessions"
34
+ ADD COLUMN IF NOT EXISTS "root_session_id" uuid,
35
+ ADD COLUMN IF NOT EXISTS "nested_agent_depth" integer,
36
+ ADD COLUMN IF NOT EXISTS "max_nested_agent_depth_override" integer,
37
+ ADD COLUMN IF NOT EXISTS "effective_max_nested_agent_depth" integer,
38
+ ADD COLUMN IF NOT EXISTS "nested_agent_depth_policy_source" text,
39
+ ADD COLUMN IF NOT EXISTS "nested_agent_depth_policy_session_id" uuid;
40
+
41
+ RESET statement_timeout;
42
+ RESET lock_timeout;