@opengeni/db 0.10.7 → 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 (35) hide show
  1. package/dist/{chunk-P6PKXY5W.js → chunk-VUKRIBO5.js} +485 -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 +3 -2
  6. package/dist/index.js +5114 -2085
  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 +720 -63
  11. package/dist/{schema-CqkzrBRS.d.ts → schema-CnpD6BcX.d.ts} +4684 -2913
  12. package/dist/schema.d.ts +1 -1
  13. package/dist/schema.js +17 -1
  14. package/drizzle/0109_nested_agent_depth_expand.sql +42 -0
  15. package/drizzle/0110_nested_agent_depth_boundary.sql +480 -0
  16. package/drizzle/0111_nested_agent_depth_backfill.sql +49 -0
  17. package/drizzle/0112_nested_agent_depth_contract.sql +38 -0
  18. package/drizzle/0113_nested_agent_depth_validate.sql +13 -0
  19. package/drizzle/0114_nested_agent_depth_contract.sql +49 -0
  20. package/drizzle/0115_nested_agent_depth_validate.sql +11 -0
  21. package/drizzle/0116_nested_agent_depth_index.sql +4 -0
  22. package/drizzle/0117_sandbox_recovery_generations.sql +699 -0
  23. package/drizzle/0118_new_session_drafts.sql +59 -0
  24. package/drizzle/0119_pending_tool_output_policy.sql +5 -0
  25. package/drizzle/0120_durable_goal_wake.sql +360 -0
  26. package/drizzle/0121_goal_update_idempotency.sql +11 -0
  27. package/package.json +3 -3
  28. package/src/index.ts +5961 -1240
  29. package/src/migrate.ts +131 -2
  30. package/src/new-session-drafts.ts +144 -0
  31. package/src/schema.ts +519 -15
  32. package/src/session-control.ts +42 -18
  33. package/src/session-tool-call-settlement.ts +6 -1
  34. package/dist/chunk-KW526IJA.js.map +0 -1
  35. package/dist/chunk-P6PKXY5W.js.map +0 -1
package/dist/schema.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import 'drizzle-orm';
2
2
  import 'drizzle-orm/pg-core';
3
- export { n as agentRunStates, o as apiKeys, p as auditEvents, q as billingCustomers, r as capabilityCatalogItems, t as capabilityInstallations, u as codexCapacityWaiters, v as codexCredentialLeases, x as codexResetRedemptionAttempts, y as codexRotationSettings, z as codexSubscriptionCredentials, d as composerDrafts, A as connections, B as creditLedgerEntries, C as deviceEnrollmentRequests, h as deviceEnrollmentStatusValues, D as documentBases, E as documentChunks, F as documents, g as enrollmentExposureValues, f as enrollmentOsValues, i as enrollmentStatusValues, G as enrollments, H as fileUploads, I as files, J as githubInstallationRepositories, K as githubInstallations, L as hostExportConfig, M as hostExportConsumers, N as hostExportCursorState, O as hostExportDeadLetters, P as hostExportOutbox, Q as importBatches, R as integrationOauthClients, S as integrationOauthStateNonces, T as knowledgeMemories, U as machineMetricsLatest, V as machineMetricsSeries, W as managedAccounts, X as packInstallations, Y as rigChanges, Z as rigVersions, _ as rigs, j as sandboxKindValues, $ as sandboxLeaseHolders, a0 as sandboxLeaseLivenessValues, a1 as sandboxLeases, a2 as sandboxPtySessions, a3 as sandboxSessionEnvelopes, a4 as sandboxes, a5 as scheduledTaskRuns, a6 as scheduledTasks, a7 as sessionAttemptInterruptions, s as sessionCommandReceipts, a8 as sessionEvents, a9 as sessionGoals, aa as sessionHistoryItems, ab as sessionHumanInputRequests, ac as sessionListSnapshots, ad as sessionMcpServers, ae as sessionPendingToolCalls, af as sessionPins, k as sessionRecordingCodecValues, l as sessionRecordingModeValues, m as sessionRecordingStateValues, ag as sessionRecordings, ah as sessionSystemUpdateOutbox, ai as sessionSystemUpdates, c as sessionTurnAttempts, b as sessionTurns, aj as sessionWorkflowWakeOutbox, a as sessions, ak as socialConnections, al as socialPosts, am as stripeWebhookEvents, an as usageEvents, ao as workspaceCaptures, ap as workspaceControlEvents, aq as workspaceInferenceControls, ar as workspaceMemberships, as as workspaceModelPolicies, at as workspacePacks, au as workspaceSessionActivityRevisions, av as workspaceVariableSetVariables, aw as workspaceVariableSets, w as workspaces } from './schema-CqkzrBRS.js';
3
+ export { o as agentRunStates, p as apiKeys, q as auditEvents, r as billingCustomers, t as capabilityCatalogItems, u as capabilityInstallations, v as codexCapacityWaiters, x as codexCredentialLeases, y as codexResetRedemptionAttempts, z as codexRotationSettings, A as codexSubscriptionCredentials, d as composerDrafts, B as connections, C as creditLedgerEntries, D as deviceEnrollmentRequests, h as deviceEnrollmentStatusValues, E as documentBases, F as documentChunks, G as documents, g as enrollmentExposureValues, f as enrollmentOsValues, i as enrollmentStatusValues, H as enrollments, I as fileUploads, J as files, K as githubInstallationRepositories, L as githubInstallations, M as hostExportConfig, N as hostExportConsumers, O as hostExportCursorState, P as hostExportDeadLetters, Q as hostExportOutbox, R as importBatches, S as integrationOauthClients, T as integrationOauthStateNonces, U as knowledgeMemories, V as machineMetricsLatest, W as machineMetricsSeries, X as managedAccounts, Y as nestedAgentDepthConfiguration, n as newSessionDrafts, Z as packInstallations, _ as rigChanges, $ as rigVersions, a0 as rigs, j as sandboxKindValues, a1 as sandboxLeaseHolders, a2 as sandboxLeaseLivenessValues, a3 as sandboxLeases, a4 as sandboxPtySessions, a5 as sandboxRetainedProcessStateValues, a6 as sandboxRetainedProcesses, a7 as sandboxSessionEnvelopes, a8 as sandboxWorkspaceMutationActorKindValues, a9 as sandboxWorkspaceMutationAdmissions, aa as sandboxWorkspaceMutationHolderKindValues, ab as sandboxes, ac as scheduledTaskRuns, ad as scheduledTasks, ae as sessionAttemptInterruptions, s as sessionCommandReceipts, af as sessionEvents, ag as sessionGoals, ah as sessionHistoryItems, ai as sessionHumanInputRequests, aj as sessionListSnapshots, ak as sessionMcpServers, al as sessionPendingToolCalls, am as sessionPins, k as sessionRecordingCodecValues, l as sessionRecordingModeValues, m as sessionRecordingStateValues, an as sessionRecordings, ao as sessionSpawnDenials, ap as sessionSystemUpdateOutbox, aq as sessionSystemUpdates, c as sessionTurnAttempts, b as sessionTurns, ar as sessionWorkflowWakeOutbox, a as sessions, as as socialConnections, at as socialPosts, au as stripeWebhookEvents, av as usageEvents, aw as workspaceCaptures, ax as workspaceControlEvents, ay as workspaceInferenceControls, az as workspaceMemberships, aA as workspaceModelPolicies, aB as workspacePacks, aC as workspaceSessionActivityRevisions, aD as workspaceVariableSetVariables, aE as workspaceVariableSets, w as workspaces } from './schema-CnpD6BcX.js';
package/dist/schema.js CHANGED
@@ -38,6 +38,8 @@ import {
38
38
  machineMetricsLatest,
39
39
  machineMetricsSeries,
40
40
  managedAccounts,
41
+ nestedAgentDepthConfiguration,
42
+ newSessionDrafts,
41
43
  packInstallations,
42
44
  rigChanges,
43
45
  rigVersions,
@@ -47,7 +49,12 @@ import {
47
49
  sandboxLeaseLivenessValues,
48
50
  sandboxLeases,
49
51
  sandboxPtySessions,
52
+ sandboxRetainedProcessStateValues,
53
+ sandboxRetainedProcesses,
50
54
  sandboxSessionEnvelopes,
55
+ sandboxWorkspaceMutationActorKindValues,
56
+ sandboxWorkspaceMutationAdmissions,
57
+ sandboxWorkspaceMutationHolderKindValues,
51
58
  sandboxes,
52
59
  scheduledTaskRuns,
53
60
  scheduledTasks,
@@ -65,6 +72,7 @@ import {
65
72
  sessionRecordingModeValues,
66
73
  sessionRecordingStateValues,
67
74
  sessionRecordings,
75
+ sessionSpawnDenials,
68
76
  sessionSystemUpdateOutbox,
69
77
  sessionSystemUpdates,
70
78
  sessionTurnAttempts,
@@ -85,7 +93,7 @@ import {
85
93
  workspaceVariableSetVariables,
86
94
  workspaceVariableSets,
87
95
  workspaces
88
- } from "./chunk-P6PKXY5W.js";
96
+ } from "./chunk-VUKRIBO5.js";
89
97
  import "./chunk-PZ5AY32C.js";
90
98
  export {
91
99
  agentRunStates,
@@ -127,6 +135,8 @@ export {
127
135
  machineMetricsLatest,
128
136
  machineMetricsSeries,
129
137
  managedAccounts,
138
+ nestedAgentDepthConfiguration,
139
+ newSessionDrafts,
130
140
  packInstallations,
131
141
  rigChanges,
132
142
  rigVersions,
@@ -136,7 +146,12 @@ export {
136
146
  sandboxLeaseLivenessValues,
137
147
  sandboxLeases,
138
148
  sandboxPtySessions,
149
+ sandboxRetainedProcessStateValues,
150
+ sandboxRetainedProcesses,
139
151
  sandboxSessionEnvelopes,
152
+ sandboxWorkspaceMutationActorKindValues,
153
+ sandboxWorkspaceMutationAdmissions,
154
+ sandboxWorkspaceMutationHolderKindValues,
140
155
  sandboxes,
141
156
  scheduledTaskRuns,
142
157
  scheduledTasks,
@@ -154,6 +169,7 @@ export {
154
169
  sessionRecordingModeValues,
155
170
  sessionRecordingStateValues,
156
171
  sessionRecordings,
172
+ sessionSpawnDenials,
157
173
  sessionSystemUpdateOutbox,
158
174
  sessionSystemUpdates,
159
175
  sessionTurnAttempts,
@@ -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;
@@ -0,0 +1,480 @@
1
+ -- deployment-mode: rolling
2
+ -- Mixed-version admission boundary, denial evidence, idempotency fencing,
3
+ -- policy lock/read, and RLS. This boundary must be installed before the
4
+ -- batched lineage backfill: old writers remain allowed to insert sessions and
5
+ -- this trigger supplies the new snapshot while the backfill is in flight.
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '30s';
8
+
9
+ -- A partial unique index on sessions and a separate partial unique index on
10
+ -- denials cannot prevent one old writer from committing a success while a
11
+ -- newer writer commits a denial for the same workspace/key. This small
12
+ -- cross-outcome ledger is the database authority for that decision. It is
13
+ -- intentionally private to the trigger functions; callers replay the outcome
14
+ -- from the durable source row.
15
+ CREATE TABLE "session_create_idempotency_guard" (
16
+ "workspace_id" uuid NOT NULL,
17
+ "idempotency_key" text NOT NULL,
18
+ "outcome" text NOT NULL,
19
+ "session_id" uuid,
20
+ "denial_id" uuid,
21
+ "created_at" timestamptz NOT NULL DEFAULT now(),
22
+ CONSTRAINT "session_create_idempotency_guard_pk"
23
+ PRIMARY KEY ("workspace_id", "idempotency_key"),
24
+ CONSTRAINT "session_create_idempotency_guard_workspace_fk"
25
+ FOREIGN KEY ("workspace_id")
26
+ REFERENCES "workspaces"("id") ON DELETE CASCADE,
27
+ CONSTRAINT "session_create_idempotency_guard_outcome_check"
28
+ CHECK ("outcome" IN ('session', 'denial')),
29
+ CONSTRAINT "session_create_idempotency_guard_target_check"
30
+ CHECK (("outcome" = 'session' AND "session_id" IS NOT NULL AND "denial_id" IS NULL)
31
+ OR ("outcome" = 'denial' AND "session_id" IS NULL AND "denial_id" IS NOT NULL))
32
+ );
33
+
34
+ CREATE TABLE "session_spawn_denials" (
35
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
36
+ "account_id" uuid NOT NULL,
37
+ "workspace_id" uuid NOT NULL,
38
+ "parent_session_id" uuid,
39
+ "root_session_id" uuid,
40
+ "current_depth" integer NOT NULL,
41
+ "attempted_depth" bigint NOT NULL,
42
+ "effective_max_nested_agent_depth" integer NOT NULL,
43
+ "requested_max_nested_agent_depth_override" integer,
44
+ "policy_source" text NOT NULL,
45
+ "policy_session_id" uuid,
46
+ "subject_id" text,
47
+ "code" text NOT NULL,
48
+ "idempotency_key" text,
49
+ "created_at" timestamptz NOT NULL DEFAULT now(),
50
+ CONSTRAINT "session_spawn_denials_workspace_account_fk"
51
+ FOREIGN KEY ("workspace_id", "account_id")
52
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
53
+ CONSTRAINT "session_spawn_denials_depth_check"
54
+ CHECK ("current_depth" >= 0 AND "attempted_depth" >= 0
55
+ AND "effective_max_nested_agent_depth" >= 0
56
+ AND ("requested_max_nested_agent_depth_override" IS NULL
57
+ OR "requested_max_nested_agent_depth_override" >= 0)),
58
+ CONSTRAINT "session_spawn_denials_policy_source_check"
59
+ CHECK ("policy_source" IN ('session', 'workspace', 'deployment', 'default')),
60
+ CONSTRAINT "session_spawn_denials_code_check"
61
+ CHECK ("code" IN ('nested_agent_depth_exceeded', 'nested_agent_depth_override_forbidden'))
62
+ );
63
+ CREATE UNIQUE INDEX "session_spawn_denials_workspace_id_uq"
64
+ ON "session_spawn_denials" ("workspace_id", "id");
65
+ CREATE INDEX "session_spawn_denials_workspace_created_idx"
66
+ ON "session_spawn_denials" ("workspace_id", "created_at");
67
+ CREATE INDEX "session_spawn_denials_parent_idx"
68
+ ON "session_spawn_denials" ("workspace_id", "parent_session_id", "created_at");
69
+ CREATE UNIQUE INDEX "session_spawn_denials_workspace_idempotency_idx"
70
+ ON "session_spawn_denials" ("workspace_id", "idempotency_key")
71
+ WHERE "idempotency_key" IS NOT NULL;
72
+
73
+ CREATE OR REPLACE FUNCTION opengeni_private.session_create_idempotency_guard_insert()
74
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
75
+ AS $function$
76
+ DECLARE
77
+ inserted_rows integer;
78
+ new_outcome text;
79
+ new_session_id uuid;
80
+ new_denial_id uuid;
81
+ new_idempotency_key text;
82
+ BEGIN
83
+ IF TG_TABLE_NAME = 'sessions' THEN
84
+ new_idempotency_key := NEW.create_idempotency_key;
85
+ new_outcome := 'session';
86
+ new_session_id := NEW.id;
87
+ new_denial_id := NULL;
88
+ ELSE
89
+ new_idempotency_key := NEW.idempotency_key;
90
+ new_outcome := 'denial';
91
+ new_session_id := NULL;
92
+ new_denial_id := NEW.id;
93
+ END IF;
94
+ IF new_idempotency_key IS NULL THEN
95
+ RETURN NEW;
96
+ END IF;
97
+
98
+ -- This is the exact lock used by the application admission path. The
99
+ -- trigger must take it before reserving the cross-outcome ledger row so an
100
+ -- old writer and a current writer cannot choose different winners for the
101
+ -- same workspace/key. The depth trigger already took this same advisory
102
+ -- lock and checked for an existing winner; taking it again is
103
+ -- transaction-reentrant and keeps this guard safe for direct old writers as
104
+ -- well as the current application.
105
+ PERFORM pg_advisory_xact_lock(
106
+ hashtext('session-create:' || NEW.workspace_id::text || ':' || new_idempotency_key)
107
+ );
108
+
109
+ EXECUTE format($sql$
110
+ INSERT INTO %I."session_create_idempotency_guard"
111
+ ("workspace_id", "idempotency_key", "outcome", "session_id", "denial_id")
112
+ VALUES ($1, $2, $3, $4, $5)
113
+ ON CONFLICT ("workspace_id", "idempotency_key") DO NOTHING
114
+ $sql$, TG_TABLE_SCHEMA)
115
+ USING NEW.workspace_id, new_idempotency_key, new_outcome, new_session_id, new_denial_id;
116
+ GET DIAGNOSTICS inserted_rows = ROW_COUNT;
117
+ IF inserted_rows = 1 THEN
118
+ RETURN NEW;
119
+ END IF;
120
+
121
+ -- The durable ledger row is the winner. Returning NULL from a BEFORE INSERT
122
+ -- trigger suppresses this source row without aborting the transaction, so
123
+ -- old writers and current writers both receive an empty RETURNING result and
124
+ -- can replay the winner. Raising 23505 here would abort the transaction
125
+ -- before the application could inspect the durable winner.
126
+ RETURN NULL;
127
+ END
128
+ $function$;
129
+
130
+ DO $lock_function$
131
+ DECLARE target_schema text := current_schema();
132
+ BEGIN
133
+ EXECUTE format($ddl$
134
+ CREATE OR REPLACE FUNCTION %I."lock_nested_agent_depth_configuration"()
135
+ RETURNS TABLE ("max_nested_agent_depth" integer, "policy_source" text)
136
+ LANGUAGE sql SECURITY DEFINER SET search_path = %I, pg_catalog
137
+ AS $body$
138
+ SELECT "max_nested_agent_depth", "policy_source"
139
+ FROM %I."nested_agent_depth_configuration"
140
+ WHERE "singleton" FOR SHARE
141
+ $body$
142
+ $ddl$, target_schema, target_schema, target_schema);
143
+ EXECUTE format('REVOKE ALL ON FUNCTION %I."lock_nested_agent_depth_configuration"() FROM PUBLIC', target_schema);
144
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
145
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %I."lock_nested_agent_depth_configuration"() TO opengeni_app', target_schema);
146
+ EXECUTE format('GRANT SELECT, INSERT ON %I."session_spawn_denials" TO opengeni_app', target_schema);
147
+ END IF;
148
+ END $lock_function$;
149
+
150
+ CREATE OR REPLACE FUNCTION opengeni_private.session_depth_policy_defaults()
151
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
152
+ AS $function$
153
+ DECLARE
154
+ workspace_policy integer;
155
+ workspace_has_policy boolean;
156
+ workspace_account_id uuid;
157
+ control_account_id uuid;
158
+ deployment_policy integer;
159
+ deployment_source text;
160
+ parent_root uuid;
161
+ parent_depth integer;
162
+ parent_effective integer;
163
+ parent_source text;
164
+ parent_policy_session uuid;
165
+ expected_root uuid;
166
+ expected_depth bigint;
167
+ expected_effective integer;
168
+ expected_source text;
169
+ expected_policy_session uuid;
170
+ new_idempotency_key text;
171
+ ledger_winner_exists boolean;
172
+ BEGIN
173
+ -- Keep direct/older writers on the same control -> advisory -> workspace
174
+ -- ordering as pre-boundary application admission. The control prefix is
175
+ -- required for rolling compatibility: an old binary can already hold this
176
+ -- row before its insert reaches this trigger. The keyed advisory lock below
177
+ -- still fences the cross-outcome ledger before policy evaluation.
178
+ EXECUTE format('SELECT account_id FROM %I.workspace_inference_controls '
179
+ 'WHERE workspace_id = $1 FOR SHARE', TG_TABLE_SCHEMA)
180
+ INTO control_account_id USING NEW.workspace_id;
181
+ IF control_account_id IS NULL THEN
182
+ RAISE EXCEPTION 'workspace has no mandatory inference-control row' USING ERRCODE = '23503';
183
+ END IF;
184
+
185
+ -- Take the same transaction advisory lock as current and old application
186
+ -- admission, then check the durable outcome before policy/control suffix
187
+ -- reads. A retry must replay its winner even if the retry's parent would now
188
+ -- exceed the effective depth policy. Waiting on the advisory lock also makes
189
+ -- an uncommitted winner visible before this check, instead of allowing a
190
+ -- mixed-version loser to evaluate policy first.
191
+ IF TG_TABLE_NAME = 'sessions' THEN
192
+ new_idempotency_key := NEW.create_idempotency_key;
193
+ IF new_idempotency_key IS NOT NULL THEN
194
+ PERFORM pg_advisory_xact_lock(
195
+ hashtext('session-create:' || NEW.workspace_id::text || ':' || new_idempotency_key)
196
+ );
197
+ EXECUTE format('SELECT EXISTS (
198
+ SELECT 1
199
+ FROM %I.session_create_idempotency_guard
200
+ WHERE workspace_id = $1 AND idempotency_key = $2
201
+ )', TG_TABLE_SCHEMA)
202
+ INTO ledger_winner_exists
203
+ USING NEW.workspace_id, new_idempotency_key;
204
+ IF ledger_winner_exists THEN
205
+ RETURN NULL;
206
+ END IF;
207
+ END IF;
208
+ END IF;
209
+
210
+ EXECUTE format('SELECT CASE WHEN jsonb_typeof(settings -> ''maxNestedAgentDepth'') = ''number''
211
+ AND settings ->> ''maxNestedAgentDepth'' ~ ''^(0|[1-9][0-9]{0,9})$''
212
+ AND (settings ->> ''maxNestedAgentDepth'')::numeric BETWEEN 0 AND 2147483647
213
+ THEN (settings ->> ''maxNestedAgentDepth'')::integer ELSE NULL END,
214
+ coalesce((jsonb_typeof(settings -> ''maxNestedAgentDepth'') = ''number''
215
+ AND settings ->> ''maxNestedAgentDepth'' ~ ''^(0|[1-9][0-9]{0,9})$''
216
+ AND (settings ->> ''maxNestedAgentDepth'')::numeric BETWEEN 0 AND 2147483647), false),
217
+ account_id
218
+ FROM %I.workspaces WHERE id = $1', TG_TABLE_SCHEMA)
219
+ INTO workspace_policy, workspace_has_policy, workspace_account_id USING NEW.workspace_id;
220
+ IF workspace_account_id IS NULL THEN
221
+ RAISE EXCEPTION 'workspace not found for session insert' USING ERRCODE = '23503';
222
+ END IF;
223
+ EXECUTE format('SELECT max_nested_agent_depth, policy_source FROM %I.lock_nested_agent_depth_configuration()', TG_TABLE_SCHEMA)
224
+ INTO deployment_policy, deployment_source;
225
+ IF deployment_policy IS NULL THEN
226
+ RAISE EXCEPTION 'nested-agent deployment policy is not configured' USING ERRCODE = '23514';
227
+ END IF;
228
+
229
+ IF NEW.parent_session_id IS NULL THEN
230
+ expected_root := NEW.id;
231
+ expected_depth := 0;
232
+ ELSE
233
+ EXECUTE format('SELECT root_session_id, nested_agent_depth, effective_max_nested_agent_depth,
234
+ nested_agent_depth_policy_source, nested_agent_depth_policy_session_id
235
+ FROM %I.sessions WHERE workspace_id = $1 AND id = $2 FOR SHARE', TG_TABLE_SCHEMA)
236
+ INTO parent_root, parent_depth, parent_effective, parent_source, parent_policy_session
237
+ USING NEW.workspace_id, NEW.parent_session_id;
238
+ IF parent_root IS NULL OR parent_depth IS NULL THEN
239
+ -- During 0111, a legacy parent can still have nullable lineage columns.
240
+ -- Walk that existing tree and derive the missing snapshot from the
241
+ -- nearest materialized ancestor, or from the legacy root and workspace
242
+ -- policy when the whole chain predates this boundary. This keeps old
243
+ -- trees writable while the bounded backfill catches up; it does not
244
+ -- bypass the effective-depth check below.
245
+ EXECUTE format($sql$
246
+ WITH RECURSIVE lineage AS (
247
+ SELECT
248
+ node.id,
249
+ node.parent_session_id,
250
+ node.root_session_id,
251
+ node.nested_agent_depth,
252
+ node.effective_max_nested_agent_depth,
253
+ node.nested_agent_depth_policy_source,
254
+ node.nested_agent_depth_policy_session_id,
255
+ 0::bigint AS distance,
256
+ ARRAY[node.id]::uuid[] AS path
257
+ FROM %I.sessions AS node
258
+ WHERE node.workspace_id = $1 AND node.id = $2
259
+ UNION ALL
260
+ SELECT
261
+ ancestor.id,
262
+ ancestor.parent_session_id,
263
+ ancestor.root_session_id,
264
+ ancestor.nested_agent_depth,
265
+ ancestor.effective_max_nested_agent_depth,
266
+ ancestor.nested_agent_depth_policy_source,
267
+ ancestor.nested_agent_depth_policy_session_id,
268
+ lineage.distance + 1,
269
+ lineage.path || ancestor.id
270
+ FROM %I.sessions AS ancestor
271
+ JOIN lineage
272
+ ON ancestor.workspace_id = $1
273
+ AND ancestor.id = lineage.parent_session_id
274
+ WHERE NOT ancestor.id = ANY(lineage.path)
275
+ ),
276
+ complete AS (
277
+ SELECT *
278
+ FROM lineage
279
+ WHERE root_session_id IS NOT NULL
280
+ AND nested_agent_depth IS NOT NULL
281
+ AND effective_max_nested_agent_depth IS NOT NULL
282
+ AND nested_agent_depth_policy_source IS NOT NULL
283
+ )
284
+ SELECT
285
+ COALESCE(
286
+ (SELECT root_session_id FROM lineage
287
+ WHERE root_session_id IS NOT NULL
288
+ ORDER BY distance LIMIT 1),
289
+ (SELECT id FROM lineage
290
+ WHERE parent_session_id IS NULL
291
+ ORDER BY distance DESC LIMIT 1)
292
+ ),
293
+ COALESCE(
294
+ (SELECT nested_agent_depth::bigint + distance FROM lineage
295
+ WHERE nested_agent_depth IS NOT NULL
296
+ ORDER BY distance LIMIT 1),
297
+ (SELECT max(distance) FROM lineage)
298
+ ),
299
+ (SELECT effective_max_nested_agent_depth FROM complete
300
+ ORDER BY distance LIMIT 1),
301
+ (SELECT nested_agent_depth_policy_source FROM complete
302
+ ORDER BY distance LIMIT 1),
303
+ (SELECT nested_agent_depth_policy_session_id FROM complete
304
+ ORDER BY distance LIMIT 1)
305
+ $sql$, TG_TABLE_SCHEMA, TG_TABLE_SCHEMA)
306
+ INTO parent_root, parent_depth, parent_effective, parent_source, parent_policy_session
307
+ USING NEW.workspace_id, NEW.parent_session_id;
308
+ END IF;
309
+ IF parent_root IS NULL OR parent_depth IS NULL THEN
310
+ RAISE EXCEPTION 'parent session not found for nested insert' USING ERRCODE = '23503';
311
+ END IF;
312
+ expected_root := parent_root;
313
+ expected_depth := parent_depth + 1;
314
+ END IF;
315
+
316
+ IF NEW.max_nested_agent_depth_override IS NOT NULL THEN
317
+ expected_effective := NEW.max_nested_agent_depth_override;
318
+ expected_source := 'session';
319
+ expected_policy_session := NEW.id;
320
+ ELSIF NEW.parent_session_id IS NOT NULL AND parent_source = 'session' THEN
321
+ expected_effective := parent_effective;
322
+ expected_source := 'session';
323
+ expected_policy_session := parent_policy_session;
324
+ ELSIF workspace_has_policy THEN
325
+ expected_effective := workspace_policy;
326
+ expected_source := 'workspace';
327
+ expected_policy_session := NULL;
328
+ ELSE
329
+ expected_effective := deployment_policy;
330
+ expected_source := deployment_source;
331
+ expected_policy_session := NULL;
332
+ END IF;
333
+
334
+ IF expected_depth > expected_effective THEN
335
+ RAISE EXCEPTION 'nested agent depth exceeds effective maximum' USING ERRCODE = '23514';
336
+ END IF;
337
+ NEW.root_session_id := expected_root;
338
+ NEW.nested_agent_depth := expected_depth;
339
+ NEW.effective_max_nested_agent_depth := expected_effective;
340
+ NEW.nested_agent_depth_policy_source := expected_source;
341
+ NEW.nested_agent_depth_policy_session_id := expected_policy_session;
342
+ RETURN NEW;
343
+ END
344
+ $function$;
345
+
346
+ CREATE OR REPLACE FUNCTION opengeni_private.session_spawn_denials_append_only()
347
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
348
+ AS $function$
349
+ DECLARE
350
+ workspace_exists boolean;
351
+ BEGIN
352
+ IF TG_OP = 'DELETE' THEN
353
+ -- Workspace/account deletion is intentionally allowed to cascade through
354
+ -- evidence rows. A direct row delete while its workspace still exists is
355
+ -- never allowed, preserving append-only audit evidence for live tenants.
356
+ IF pg_trigger_depth() > 1 THEN
357
+ RETURN OLD;
358
+ END IF;
359
+ -- The FK cascade removes the parent workspace before its child rows are
360
+ -- deleted. Resolve the target schema dynamically because embedded hosts
361
+ -- run the same migration chain outside public.
362
+ EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I.workspaces WHERE id = $1)', TG_TABLE_SCHEMA)
363
+ INTO workspace_exists
364
+ USING OLD.workspace_id;
365
+ IF NOT workspace_exists THEN
366
+ RETURN OLD;
367
+ END IF;
368
+ END IF;
369
+ RAISE EXCEPTION 'session spawn denial evidence is append-only' USING ERRCODE = '55000';
370
+ END
371
+ $function$;
372
+
373
+ CREATE OR REPLACE FUNCTION opengeni_private.lock_nested_agent_workspace_policy_update()
374
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog
375
+ AS $function$
376
+ DECLARE
377
+ control_account_id uuid;
378
+ BEGIN
379
+ IF OLD.settings -> 'maxNestedAgentDepth'
380
+ IS NOT DISTINCT FROM NEW.settings -> 'maxNestedAgentDepth' THEN
381
+ RETURN NEW;
382
+ END IF;
383
+ EXECUTE format('SELECT account_id FROM %I.workspace_inference_controls '
384
+ 'WHERE workspace_id = $1 FOR UPDATE', TG_TABLE_SCHEMA)
385
+ INTO control_account_id USING NEW.id;
386
+ IF control_account_id IS NULL THEN
387
+ RAISE EXCEPTION 'workspace has no mandatory inference-control row' USING ERRCODE = '23503';
388
+ END IF;
389
+ RETURN NEW;
390
+ END
391
+ $function$;
392
+
393
+ -- Fence the source tables before installing any source-row guard. A SHARE
394
+ -- lock waits for every pre-boundary writer already in flight, and prevents
395
+ -- new INSERT/UPDATE/DELETE writers from committing while the trigger boundary
396
+ -- and initial ledger reconciliation are installed. Without this fence an old
397
+ -- writer can commit after the trigger install decision but before its source
398
+ -- row is backfilled into the ledger.
399
+ LOCK TABLE "workspaces", "sessions", "session_spawn_denials" IN SHARE MODE;
400
+
401
+ DO $triggers$
402
+ DECLARE target_schema text := current_schema();
403
+ BEGIN
404
+ EXECUTE format('DROP TRIGGER IF EXISTS session_depth_policy_defaults ON %I.sessions', target_schema);
405
+ EXECUTE format('CREATE TRIGGER session_depth_policy_defaults BEFORE INSERT ON %I.sessions FOR EACH ROW EXECUTE FUNCTION opengeni_private.session_depth_policy_defaults()', target_schema);
406
+ EXECUTE format('DROP TRIGGER IF EXISTS session_create_idempotency_guard ON %I.sessions', target_schema);
407
+ EXECUTE format('DROP TRIGGER IF EXISTS session_idempotency_guard ON %I.sessions', target_schema);
408
+ EXECUTE format('CREATE TRIGGER session_idempotency_guard BEFORE INSERT ON %I.sessions FOR EACH ROW WHEN (NEW.create_idempotency_key IS NOT NULL) EXECUTE FUNCTION opengeni_private.session_create_idempotency_guard_insert()', target_schema);
409
+ EXECUTE format('DROP TRIGGER IF EXISTS session_spawn_denials_append_only ON %I.session_spawn_denials', target_schema);
410
+ EXECUTE format('CREATE TRIGGER session_spawn_denials_append_only BEFORE UPDATE OR DELETE ON %I.session_spawn_denials FOR EACH ROW EXECUTE FUNCTION opengeni_private.session_spawn_denials_append_only()', target_schema);
411
+ EXECUTE format('DROP TRIGGER IF EXISTS session_spawn_denials_idempotency_guard ON %I.session_spawn_denials', target_schema);
412
+ EXECUTE format('CREATE TRIGGER session_spawn_denials_idempotency_guard BEFORE INSERT ON %I.session_spawn_denials FOR EACH ROW WHEN (NEW.idempotency_key IS NOT NULL) EXECUTE FUNCTION opengeni_private.session_create_idempotency_guard_insert()', target_schema);
413
+ EXECUTE format('DROP TRIGGER IF EXISTS lock_nested_agent_workspace_policy_update ON %I.workspaces', target_schema);
414
+ EXECUTE format('CREATE TRIGGER lock_nested_agent_workspace_policy_update BEFORE UPDATE OF settings ON %I.workspaces FOR EACH ROW EXECUTE FUNCTION opengeni_private.lock_nested_agent_workspace_policy_update()', target_schema);
415
+ END $triggers$;
416
+
417
+ -- Preserve source rows created before this boundary. The shared source-table
418
+ -- lock remains held, so this reconciliation is fenced against both old and
419
+ -- new writers and cannot miss a source row between the scan and ledger insert.
420
+ INSERT INTO "session_create_idempotency_guard" (
421
+ "workspace_id", "idempotency_key", "outcome", "session_id"
422
+ )
423
+ SELECT "workspace_id", "create_idempotency_key", 'session', "id"
424
+ FROM "sessions"
425
+ WHERE "create_idempotency_key" IS NOT NULL
426
+ ON CONFLICT ("workspace_id", "idempotency_key") DO NOTHING;
427
+
428
+ -- Keep a final reconciliation for both outcomes while the source-table locks
429
+ -- are still held. The checks make a migration failure explicit rather than
430
+ -- publishing a source row with no ledger entry.
431
+ INSERT INTO "session_create_idempotency_guard" (
432
+ "workspace_id", "idempotency_key", "outcome", "session_id"
433
+ )
434
+ SELECT "workspace_id", "create_idempotency_key", 'session', "id"
435
+ FROM "sessions"
436
+ WHERE "create_idempotency_key" IS NOT NULL
437
+ ON CONFLICT ("workspace_id", "idempotency_key") DO NOTHING;
438
+ INSERT INTO "session_create_idempotency_guard" (
439
+ "workspace_id", "idempotency_key", "outcome", "denial_id"
440
+ )
441
+ SELECT "workspace_id", "idempotency_key", 'denial', "id"
442
+ FROM "session_spawn_denials"
443
+ WHERE "idempotency_key" IS NOT NULL
444
+ ON CONFLICT ("workspace_id", "idempotency_key") DO NOTHING;
445
+
446
+ DO $reconcile$
447
+ DECLARE
448
+ missing_source_rows integer;
449
+ BEGIN
450
+ SELECT count(*)::integer
451
+ INTO missing_source_rows
452
+ FROM (
453
+ SELECT "workspace_id", "create_idempotency_key" AS "idempotency_key"
454
+ FROM "sessions"
455
+ WHERE "create_idempotency_key" IS NOT NULL
456
+ UNION ALL
457
+ SELECT "workspace_id", "idempotency_key"
458
+ FROM "session_spawn_denials"
459
+ WHERE "idempotency_key" IS NOT NULL
460
+ ) source_rows
461
+ WHERE NOT EXISTS (
462
+ SELECT 1
463
+ FROM "session_create_idempotency_guard" guard
464
+ WHERE guard."workspace_id" = source_rows."workspace_id"
465
+ AND guard."idempotency_key" = source_rows."idempotency_key"
466
+ );
467
+ IF missing_source_rows <> 0 THEN
468
+ RAISE EXCEPTION 'session create idempotency ledger reconciliation missed % source rows', missing_source_rows
469
+ USING ERRCODE = '23514';
470
+ END IF;
471
+ END $reconcile$;
472
+
473
+ ALTER TABLE "session_spawn_denials" ENABLE ROW LEVEL SECURITY;
474
+ ALTER TABLE "session_spawn_denials" FORCE ROW LEVEL SECURITY;
475
+ CREATE POLICY workspace_isolation ON "session_spawn_denials"
476
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
477
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
478
+
479
+ RESET statement_timeout;
480
+ RESET lock_timeout;
@@ -0,0 +1,49 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:batched-backfill batch-size=1000 lock-timeout=5s statement-timeout=30s
3
+ WITH candidates AS (
4
+ SELECT
5
+ session.ctid AS target_ctid,
6
+ CASE WHEN session."parent_session_id" IS NULL THEN session."id"
7
+ ELSE parent."root_session_id" END AS root_session_id,
8
+ CASE WHEN session."parent_session_id" IS NULL THEN 0
9
+ ELSE parent."nested_agent_depth"::bigint + 1 END AS nested_agent_depth,
10
+ CASE
11
+ WHEN jsonb_typeof(workspace."settings" -> 'maxNestedAgentDepth') = 'number'
12
+ AND workspace."settings" ->> 'maxNestedAgentDepth' ~ '^(0|[1-9][0-9]{0,9})$'
13
+ AND (workspace."settings" ->> 'maxNestedAgentDepth')::numeric BETWEEN 0 AND 2147483647
14
+ THEN (workspace."settings" ->> 'maxNestedAgentDepth')::integer
15
+ ELSE configuration."max_nested_agent_depth"
16
+ END AS effective_max_nested_agent_depth,
17
+ CASE
18
+ WHEN jsonb_typeof(workspace."settings" -> 'maxNestedAgentDepth') = 'number'
19
+ AND workspace."settings" ->> 'maxNestedAgentDepth' ~ '^(0|[1-9][0-9]{0,9})$'
20
+ AND (workspace."settings" ->> 'maxNestedAgentDepth')::numeric BETWEEN 0 AND 2147483647
21
+ THEN 'workspace'
22
+ ELSE configuration."policy_source"
23
+ END AS nested_agent_depth_policy_source
24
+ FROM "sessions" session
25
+ LEFT JOIN "sessions" parent
26
+ ON parent."workspace_id" = session."workspace_id"
27
+ AND parent."id" = session."parent_session_id"
28
+ JOIN "workspaces" workspace ON workspace."id" = session."workspace_id"
29
+ JOIN "nested_agent_depth_configuration" configuration ON configuration."singleton"
30
+ WHERE (session."root_session_id" IS NULL
31
+ OR session."nested_agent_depth" IS NULL
32
+ OR session."effective_max_nested_agent_depth" IS NULL
33
+ OR session."nested_agent_depth_policy_source" IS NULL)
34
+ AND (session."parent_session_id" IS NULL
35
+ OR (parent."root_session_id" IS NOT NULL AND parent."nested_agent_depth" IS NOT NULL))
36
+ ORDER BY session."created_at", session."id"
37
+ LIMIT 1000
38
+ FOR UPDATE OF session
39
+ )
40
+ UPDATE "sessions" session
41
+ SET "root_session_id" = candidates.root_session_id,
42
+ "nested_agent_depth" = candidates.nested_agent_depth,
43
+ "max_nested_agent_depth_override" = NULL,
44
+ "effective_max_nested_agent_depth" = candidates.effective_max_nested_agent_depth,
45
+ "nested_agent_depth_policy_source" = candidates.nested_agent_depth_policy_source,
46
+ "nested_agent_depth_policy_session_id" = NULL
47
+ FROM candidates
48
+ WHERE session.ctid = candidates.target_ctid
49
+ RETURNING session."id";