@opengeni/db 0.7.3 → 0.9.3

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 (59) hide show
  1. package/dist/{chunk-B22X3IEZ.js → chunk-4LG5NBTC.js} +512 -32
  2. package/dist/chunk-4LG5NBTC.js.map +1 -0
  3. package/dist/{chunk-YFQ7SGE4.js → chunk-BMFDXFPA.js} +23 -1
  4. package/dist/chunk-BMFDXFPA.js.map +1 -0
  5. package/dist/chunk-KW526IJA.js +127 -0
  6. package/dist/chunk-KW526IJA.js.map +1 -0
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +6068 -2090
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.d.ts +29 -4
  11. package/dist/migrate.js +3 -1
  12. package/dist/provision-roles.d.ts +721 -46
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/{schema-BN5mB9xZ.d.ts → schema-CdPGTHlD.d.ts} +2118 -119
  15. package/dist/schema.d.ts +1 -1
  16. package/dist/schema.js +17 -1
  17. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +15 -0
  18. package/drizzle/0065_session_attempt_quiescence.sql +26 -0
  19. package/drizzle/0066_session_interruption_attempt_lookup.sql +4 -0
  20. package/drizzle/0067_session_event_payload_bounds.sql +209 -0
  21. package/drizzle/0068_workspace_control_event_bounds.sql +134 -0
  22. package/drizzle/0069_session_event_history_backfill.sql +32 -0
  23. package/drizzle/0070_session_event_type_sequence_lookup.sql +4 -0
  24. package/drizzle/0071_session_event_monitoring_tail.sql +10 -0
  25. package/drizzle/0072_sessions_workspace_created_id_idx.sql +4 -0
  26. package/drizzle/0073_sessions_workspace_updated_id_idx.sql +4 -0
  27. package/drizzle/0074_session_activity_revisions.sql +72 -0
  28. package/drizzle/0075_sessions_workspace_activity_revision_idx.sql +4 -0
  29. package/drizzle/0076_session_workflow_wake_acl.sql +13 -0
  30. package/drizzle/0077_session_attempt_latest_lookup.sql +4 -0
  31. package/drizzle/0094_quarantine_credential_bearing_catalog_urls.sql +51 -0
  32. package/drizzle/0095_github_existing_installations.sql +84 -0
  33. package/drizzle/0096_session_turn_initiators.sql +97 -0
  34. package/drizzle/0097_host_export_outbox.sql +1220 -0
  35. package/drizzle/0098_usage_events_workspace_session_idx.sql +4 -0
  36. package/drizzle/0099_session_human_input_attempt_owner_index.sql +6 -0
  37. package/drizzle/0100_session_human_input_requests.sql +89 -0
  38. package/drizzle/0101_session_mcp_connection_refs.sql +30 -0
  39. package/drizzle/0102_session_command_receipt_service_actor.sql +16 -0
  40. package/drizzle/0103_host_export_root_session.sql +166 -0
  41. package/drizzle/0104_host_export_root_session_backfill.sql +27 -0
  42. package/drizzle/0105_session_turn_instructions.sql +9 -0
  43. package/package.json +4 -4
  44. package/src/connection-token-resolver.ts +287 -1
  45. package/src/event-payload-sanitizer.ts +57 -19
  46. package/src/index.ts +5377 -1125
  47. package/src/memory-domain.ts +1 -1
  48. package/src/migrate.ts +86 -23
  49. package/src/persistence-errors.ts +252 -0
  50. package/src/provision-roles.ts +42 -0
  51. package/src/schema.ts +552 -34
  52. package/src/session-control.ts +518 -38
  53. package/src/session-queue-commands.ts +308 -57
  54. package/src/session-tool-call-settlement.ts +58 -7
  55. package/src/turn-initiator.ts +155 -0
  56. package/dist/chunk-7LDU7F5P.js +0 -80
  57. package/dist/chunk-7LDU7F5P.js.map +0 -1
  58. package/dist/chunk-B22X3IEZ.js.map +0 -1
  59. package/dist/chunk-YFQ7SGE4.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 { m as agentRunStates, n as apiKeys, o as auditEvents, p as billingCustomers, q as capabilityCatalogItems, r as capabilityInstallations, t as codexCapacityWaiters, u as codexCredentialLeases, v as codexRotationSettings, w as codexSubscriptionCredentials, c as composerDrafts, x as connections, y as creditLedgerEntries, z as deviceEnrollmentRequests, g as deviceEnrollmentStatusValues, A as documentBases, B as documentChunks, C as documents, f as enrollmentExposureValues, e as enrollmentOsValues, h as enrollmentStatusValues, D as enrollments, E as fileUploads, F as files, G as githubInstallations, H as importBatches, I as integrationOauthClients, J as integrationOauthStateNonces, K as knowledgeMemories, L as machineMetricsLatest, M as machineMetricsSeries, N as managedAccounts, O as packInstallations, P as rigChanges, Q as rigVersions, R as rigs, i as sandboxKindValues, S as sandboxLeaseHolders, T as sandboxLeaseLivenessValues, U as sandboxLeases, V as sandboxPtySessions, W as sandboxSessionEnvelopes, X as sandboxes, Y as scheduledTaskRuns, Z as scheduledTasks, _ as sessionAttemptInterruptions, s as sessionCommandReceipts, $ as sessionEvents, a0 as sessionGoals, a1 as sessionHistoryItems, a2 as sessionListSnapshots, a3 as sessionMcpServers, a4 as sessionPendingToolCalls, a5 as sessionPins, j as sessionRecordingCodecValues, k as sessionRecordingModeValues, l as sessionRecordingStateValues, a6 as sessionRecordings, a7 as sessionSystemUpdateOutbox, a8 as sessionSystemUpdates, a as sessionTurnAttempts, b as sessionTurns, a9 as sessionWorkflowWakeOutbox, aa as sessions, ab as socialConnections, ac as socialPosts, ad as stripeWebhookEvents, ae as usageEvents, af as workspaceCaptures, ag as workspaceControlEvents, ah as workspaceInferenceControls, ai as workspaceMemberships, aj as workspaceModelPolicies, ak as workspacePacks, al as workspaceVariableSetVariables, am as workspaceVariableSets, an as workspaces } from './schema-BN5mB9xZ.js';
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 codexRotationSettings, y as codexSubscriptionCredentials, d as composerDrafts, z as connections, A as creditLedgerEntries, B as deviceEnrollmentRequests, h as deviceEnrollmentStatusValues, C as documentBases, D as documentChunks, E as documents, g as enrollmentExposureValues, f as enrollmentOsValues, i as enrollmentStatusValues, F as enrollments, G as fileUploads, H as files, I as githubInstallationRepositories, J as githubInstallations, K as hostExportConfig, L as hostExportConsumers, M as hostExportCursorState, N as hostExportDeadLetters, O as hostExportOutbox, P as importBatches, Q as integrationOauthClients, R as integrationOauthStateNonces, S as knowledgeMemories, T as machineMetricsLatest, U as machineMetricsSeries, V as managedAccounts, W as packInstallations, X as rigChanges, Y as rigVersions, Z as rigs, j as sandboxKindValues, _ as sandboxLeaseHolders, $ as sandboxLeaseLivenessValues, a0 as sandboxLeases, a1 as sandboxPtySessions, a2 as sandboxSessionEnvelopes, a3 as sandboxes, a4 as scheduledTaskRuns, a5 as scheduledTasks, a6 as sessionAttemptInterruptions, s as sessionCommandReceipts, a7 as sessionEvents, a8 as sessionGoals, a9 as sessionHistoryItems, aa as sessionHumanInputRequests, ab as sessionListSnapshots, ac as sessionMcpServers, ad as sessionPendingToolCalls, ae as sessionPins, k as sessionRecordingCodecValues, l as sessionRecordingModeValues, m as sessionRecordingStateValues, af as sessionRecordings, ag as sessionSystemUpdateOutbox, ah as sessionSystemUpdates, c as sessionTurnAttempts, b as sessionTurns, ai as sessionWorkflowWakeOutbox, a as sessions, aj as socialConnections, ak as socialPosts, al as stripeWebhookEvents, am as usageEvents, an as workspaceCaptures, ao as workspaceControlEvents, ap as workspaceInferenceControls, aq as workspaceMemberships, ar as workspaceModelPolicies, as as workspacePacks, at as workspaceSessionActivityRevisions, au as workspaceVariableSetVariables, av as workspaceVariableSets, w as workspaces } from './schema-CdPGTHlD.js';
package/dist/schema.js CHANGED
@@ -23,7 +23,13 @@ import {
23
23
  enrollments,
24
24
  fileUploads,
25
25
  files,
26
+ githubInstallationRepositories,
26
27
  githubInstallations,
28
+ hostExportConfig,
29
+ hostExportConsumers,
30
+ hostExportCursorState,
31
+ hostExportDeadLetters,
32
+ hostExportOutbox,
27
33
  importBatches,
28
34
  integrationOauthClients,
29
35
  integrationOauthStateNonces,
@@ -49,6 +55,7 @@ import {
49
55
  sessionEvents,
50
56
  sessionGoals,
51
57
  sessionHistoryItems,
58
+ sessionHumanInputRequests,
52
59
  sessionListSnapshots,
53
60
  sessionMcpServers,
54
61
  sessionPendingToolCalls,
@@ -73,10 +80,11 @@ import {
73
80
  workspaceMemberships,
74
81
  workspaceModelPolicies,
75
82
  workspacePacks,
83
+ workspaceSessionActivityRevisions,
76
84
  workspaceVariableSetVariables,
77
85
  workspaceVariableSets,
78
86
  workspaces
79
- } from "./chunk-B22X3IEZ.js";
87
+ } from "./chunk-4LG5NBTC.js";
80
88
  import "./chunk-PZ5AY32C.js";
81
89
  export {
82
90
  agentRunStates,
@@ -103,7 +111,13 @@ export {
103
111
  enrollments,
104
112
  fileUploads,
105
113
  files,
114
+ githubInstallationRepositories,
106
115
  githubInstallations,
116
+ hostExportConfig,
117
+ hostExportConsumers,
118
+ hostExportCursorState,
119
+ hostExportDeadLetters,
120
+ hostExportOutbox,
107
121
  importBatches,
108
122
  integrationOauthClients,
109
123
  integrationOauthStateNonces,
@@ -129,6 +143,7 @@ export {
129
143
  sessionEvents,
130
144
  sessionGoals,
131
145
  sessionHistoryItems,
146
+ sessionHumanInputRequests,
132
147
  sessionListSnapshots,
133
148
  sessionMcpServers,
134
149
  sessionPendingToolCalls,
@@ -153,6 +168,7 @@ export {
153
168
  workspaceMemberships,
154
169
  workspaceModelPolicies,
155
170
  workspacePacks,
171
+ workspaceSessionActivityRevisions,
156
172
  workspaceVariableSetVariables,
157
173
  workspaceVariableSets,
158
174
  workspaces
@@ -0,0 +1,15 @@
1
+ -- deployment-mode: rolling
2
+ -- OPE-36: the strategy picker is gone — rotation-enabled always behaves as
3
+ -- sticky-sharded (worker-side effectiveRotationStrategy normalizes every read).
4
+ -- Backfill stored legacy values so the column tells the truth going forward.
5
+ -- The column itself is KEPT (old-binary rollback safety): an old worker reading
6
+ -- 'sharded' runs the strategy that was already live on every real workspace.
7
+
8
+ SET lock_timeout = '5s';
9
+ SET statement_timeout = '10min';
10
+
11
+ UPDATE "codex_rotation_settings"
12
+ SET "rotation_strategy" = 'sharded'
13
+ WHERE "rotation_strategy" IS DISTINCT FROM 'sharded';
14
+
15
+ ALTER TABLE "codex_rotation_settings" ALTER COLUMN "rotation_strategy" SET DEFAULT 'sharded';
@@ -0,0 +1,26 @@
1
+ -- deployment-mode: maintenance
2
+ -- A cancelled attempt records the exact boundary after which it has no
3
+ -- inference, user-visible output, or workspace-persistence authority.
4
+ -- Fenced/idempotent cleanup and telemetry may still finish. Temporal
5
+ -- independently waits for the activity to terminate; this receipt lets queue
6
+ -- admission and UI state use durable truth.
7
+
8
+ SET lock_timeout = '5s';
9
+ SET statement_timeout = '5min';
10
+
11
+ ALTER TABLE "session_turn_attempts"
12
+ ADD COLUMN "quiesced_at" timestamptz;
13
+
14
+ -- Maintenance mode drains the pre-cutover workers before this backfill, so an
15
+ -- interrupted attempt that is already closed has also left its physical
16
+ -- activity. Seed only that historical population; new activity/workflow code
17
+ -- owns all later receipts.
18
+ UPDATE "session_turn_attempts" AS attempt
19
+ SET "quiesced_at" = COALESCE(attempt."closed_at", attempt."updated_at", now())
20
+ WHERE attempt."state" = 'closed'
21
+ AND EXISTS (
22
+ SELECT 1
23
+ FROM "session_attempt_interruptions" AS interruption
24
+ WHERE interruption."workspace_id" = attempt."workspace_id"
25
+ AND interruption."attempt_id" = attempt."id"
26
+ );
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_attempt_interruptions_attempt_lookup_idx"
4
+ ON "session_attempt_interruptions" ("workspace_id", "session_id", "attempt_id", "requested_at" DESC);
@@ -0,0 +1,209 @@
1
+ -- deployment-mode: rolling
2
+ -- OPE-64: session_events is the lossy human/audit projection, not a blob store.
3
+ -- Install the BEFORE trigger before the NOT VALID check so an older pod in the
4
+ -- same rolling deployment is normalized instead of having an oversized write
5
+ -- rejected. Historical oversized rows stay readable and can be defensively
6
+ -- bounded by NATS/SSE; NOT VALID enforces the invariant for every new row.
7
+
8
+ SET lock_timeout = '5s';
9
+ SET statement_timeout = '10min';
10
+
11
+ CREATE OR REPLACE FUNCTION opengeni_private.project_session_event_payload(source_payload jsonb)
12
+ RETURNS jsonb
13
+ LANGUAGE plpgsql
14
+ IMMUTABLE
15
+ SET search_path = pg_catalog
16
+ AS $function$
17
+ DECLARE
18
+ original_bytes integer;
19
+ delivered_bytes integer;
20
+ bounded jsonb;
21
+ BEGIN
22
+ original_bytes := octet_length(source_payload::text);
23
+ IF original_bytes <= 65536 THEN
24
+ RETURN source_payload;
25
+ END IF;
26
+
27
+ -- This is intentionally a small content-free fallback. Normal application
28
+ -- writers produce richer deterministic head/tail previews before INSERT; the
29
+ -- trigger exists for rolling old binaries and unforeseen direct writers.
30
+ bounded := jsonb_strip_nulls(jsonb_build_object(
31
+ 'id', left(source_payload ->> 'id', 256),
32
+ 'name', left(coalesce(source_payload ->> 'name', source_payload ->> 'toolName'), 256),
33
+ 'type', left(source_payload ->> 'type', 128),
34
+ 'status', left(source_payload ->> 'status', 128),
35
+ 'code', left(source_payload ->> 'code', 128),
36
+ 'isError', source_payload -> 'isError',
37
+ 'preview', '[event payload omitted by durable audit storage guard]',
38
+ 'truncation', jsonb_build_object(
39
+ 'truncated', true,
40
+ 'surface', 'database_guard',
41
+ 'reason', 'database_guard',
42
+ 'originalBytes', original_bytes,
43
+ 'deliveredBytes', 0,
44
+ 'omittedBytes', original_bytes,
45
+ 'estimatedOriginalTokens', ceil(original_bytes / 4.0)::integer,
46
+ 'estimatedDeliveredTokens', 0,
47
+ 'fullEvidence', jsonb_build_object('available', false, 'reason', 'not_retained'),
48
+ 'details', jsonb_build_array(jsonb_build_object(
49
+ 'path', '$',
50
+ 'kind', 'object',
51
+ 'originalBytes', original_bytes
52
+ ))
53
+ )
54
+ ));
55
+
56
+ -- Size fields affect their own JSON width. Converge to an exact fixed point
57
+ -- without retaining any omitted content; fail the write rather than publish
58
+ -- false accounting if PostgreSQL's representation ever stops converging.
59
+ FOR pass IN 1..8 LOOP
60
+ delivered_bytes := octet_length(bounded::text);
61
+ EXIT WHEN (bounded #>> '{truncation,deliveredBytes}')::integer = delivered_bytes
62
+ AND (bounded #>> '{truncation,omittedBytes}')::integer
63
+ = greatest(0, original_bytes - delivered_bytes)
64
+ AND (bounded #>> '{truncation,estimatedDeliveredTokens}')::integer
65
+ = ceil(delivered_bytes / 4.0)::integer;
66
+ bounded := jsonb_set(bounded, '{truncation,deliveredBytes}', to_jsonb(delivered_bytes), false);
67
+ bounded := jsonb_set(
68
+ bounded,
69
+ '{truncation,omittedBytes}',
70
+ to_jsonb(greatest(0, original_bytes - delivered_bytes)),
71
+ false
72
+ );
73
+ bounded := jsonb_set(
74
+ bounded,
75
+ '{truncation,estimatedDeliveredTokens}',
76
+ to_jsonb(ceil(delivered_bytes / 4.0)::integer),
77
+ false
78
+ );
79
+ END LOOP;
80
+
81
+ delivered_bytes := octet_length(bounded::text);
82
+ IF (bounded #>> '{truncation,deliveredBytes}')::integer <> delivered_bytes
83
+ OR (bounded #>> '{truncation,omittedBytes}')::integer
84
+ <> greatest(0, original_bytes - delivered_bytes)
85
+ OR (bounded #>> '{truncation,estimatedDeliveredTokens}')::integer
86
+ <> ceil(delivered_bytes / 4.0)::integer THEN
87
+ RAISE EXCEPTION 'session event payload accounting failed to converge';
88
+ END IF;
89
+
90
+ RETURN bounded;
91
+ END;
92
+ $function$;
93
+
94
+ CREATE OR REPLACE FUNCTION opengeni_private.bound_session_event_payload()
95
+ RETURNS trigger
96
+ LANGUAGE plpgsql
97
+ SET search_path = pg_catalog
98
+ AS $function$
99
+ DECLARE
100
+ original_type text := NEW.type;
101
+ original_event_bytes integer;
102
+ fields jsonb := '[]'::jsonb;
103
+ delivered text;
104
+ BEGIN
105
+ original_event_bytes := octet_length(row_to_json(NEW)::text);
106
+ -- Bound every free-form envelope string by UTF-8 bytes. left() uses
107
+ -- characters, so one quarter of each byte limit is a conservative bound for
108
+ -- both ASCII and four-byte Unicode without ever splitting a code point.
109
+ IF octet_length(NEW.type) > 256 OR position(E'\n' in NEW.type) > 0
110
+ OR position(E'\r' in NEW.type) > 0 THEN
111
+ delivered := 'session.event.envelope_omitted';
112
+ fields := fields || jsonb_build_array(jsonb_build_object(
113
+ 'field', 'type',
114
+ 'originalBytes', octet_length(NEW.type),
115
+ 'deliveredBytes', octet_length(delivered)
116
+ ));
117
+ NEW.type := delivered;
118
+ END IF;
119
+
120
+ IF NEW.client_event_id IS NOT NULL AND octet_length(NEW.client_event_id) > 1024 THEN
121
+ delivered := left(NEW.client_event_id, 256);
122
+ fields := fields || jsonb_build_array(jsonb_build_object(
123
+ 'field', 'clientEventId',
124
+ 'originalBytes', octet_length(NEW.client_event_id),
125
+ 'deliveredBytes', octet_length(delivered)
126
+ ));
127
+ NEW.client_event_id := delivered;
128
+ END IF;
129
+
130
+ IF NEW.producer_id IS NOT NULL AND octet_length(NEW.producer_id) > 1024 THEN
131
+ delivered := left(NEW.producer_id, 256);
132
+ fields := fields || jsonb_build_array(jsonb_build_object(
133
+ 'field', 'producerId',
134
+ 'originalBytes', octet_length(NEW.producer_id),
135
+ 'deliveredBytes', octet_length(delivered)
136
+ ));
137
+ NEW.producer_id := delivered;
138
+ END IF;
139
+
140
+ IF NEW.turn_association IS NOT NULL AND NEW.turn_association NOT IN (
141
+ 'current', 'late_rejected', 'duplicate'
142
+ ) THEN
143
+ fields := fields || jsonb_build_array(jsonb_build_object(
144
+ 'field', 'turnAssociation',
145
+ 'originalBytes', octet_length(NEW.turn_association),
146
+ 'deliveredBytes', 0
147
+ ));
148
+ NEW.turn_association := NULL;
149
+ END IF;
150
+
151
+ IF NEW.duplicate_reason IS NOT NULL AND octet_length(NEW.duplicate_reason) > 4096 THEN
152
+ delivered := left(NEW.duplicate_reason, 1024);
153
+ fields := fields || jsonb_build_array(jsonb_build_object(
154
+ 'field', 'duplicateReason',
155
+ 'originalBytes', octet_length(NEW.duplicate_reason),
156
+ 'deliveredBytes', octet_length(delivered)
157
+ ));
158
+ NEW.duplicate_reason := delivered;
159
+ END IF;
160
+
161
+ IF jsonb_array_length(fields) > 0 THEN
162
+ NEW.payload := jsonb_build_object(
163
+ 'preview', '[legacy event envelope normalized by durable audit storage guard]',
164
+ 'originalEventBytes', original_event_bytes,
165
+ 'originalType', left(original_type, 64),
166
+ 'envelopeProjection', jsonb_build_object(
167
+ 'truncated', true,
168
+ 'surface', 'database_guard',
169
+ 'fields', fields
170
+ ),
171
+ 'fullEvidence', jsonb_build_object('available', false, 'reason', 'not_retained')
172
+ );
173
+ END IF;
174
+
175
+ NEW.payload := opengeni_private.project_session_event_payload(NEW.payload);
176
+ RETURN NEW;
177
+ END;
178
+ $function$;
179
+
180
+ DROP TRIGGER IF EXISTS session_events_bound_payload_before_insert ON session_events;
181
+ CREATE TRIGGER session_events_bound_payload_before_insert
182
+ BEFORE INSERT OR UPDATE OF payload, type, client_event_id, producer_id, turn_association, duplicate_reason
183
+ ON session_events
184
+ FOR EACH ROW
185
+ EXECUTE FUNCTION opengeni_private.bound_session_event_payload();
186
+
187
+ ALTER TABLE session_events
188
+ ADD CONSTRAINT session_events_payload_bytes_check
189
+ CHECK (octet_length(payload::text) <= 65536)
190
+ NOT VALID,
191
+ ADD CONSTRAINT session_events_type_bytes_check
192
+ CHECK (
193
+ octet_length(type) <= 256
194
+ AND position(E'\n' in type) = 0
195
+ AND position(E'\r' in type) = 0
196
+ )
197
+ NOT VALID,
198
+ ADD CONSTRAINT session_events_client_event_id_bytes_check
199
+ CHECK (client_event_id IS NULL OR octet_length(client_event_id) <= 1024)
200
+ NOT VALID,
201
+ ADD CONSTRAINT session_events_producer_id_bytes_check
202
+ CHECK (producer_id IS NULL OR octet_length(producer_id) <= 1024)
203
+ NOT VALID,
204
+ ADD CONSTRAINT session_events_turn_association_bytes_check
205
+ CHECK (turn_association IS NULL OR octet_length(turn_association) <= 64)
206
+ NOT VALID,
207
+ ADD CONSTRAINT session_events_duplicate_reason_bytes_check
208
+ CHECK (duplicate_reason IS NULL OR octet_length(duplicate_reason) <= 4096)
209
+ NOT VALID;
@@ -0,0 +1,134 @@
1
+ -- deployment-mode: rolling
2
+ -- OPE-64: workspace-control rows are compact invalidations, not evidence blobs.
3
+ -- Preserve exact source byte counts while bounding the two free-form fields so
4
+ -- old binaries and direct writers cannot create a durable replay poison row.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ ALTER TABLE workspace_control_events
10
+ ADD COLUMN reason_original_bytes integer,
11
+ ADD COLUMN actor_original_bytes integer;
12
+
13
+ CREATE OR REPLACE FUNCTION opengeni_private.truncate_workspace_control_text(
14
+ source_text text,
15
+ max_bytes integer
16
+ )
17
+ RETURNS text
18
+ LANGUAGE plpgsql
19
+ IMMUTABLE
20
+ STRICT
21
+ SET search_path = pg_catalog
22
+ AS $function$
23
+ DECLARE
24
+ marker constant text := '…[truncated]';
25
+ prefix_budget integer := greatest(0, max_bytes - octet_length(marker));
26
+ candidate text;
27
+ excess integer;
28
+ BEGIN
29
+ IF octet_length(source_text) <= max_bytes THEN
30
+ RETURN source_text;
31
+ END IF;
32
+
33
+ -- left() is character-safe. Starting at at most prefix_budget characters
34
+ -- caps allocation even for a multi-megabyte legacy value; each pass removes
35
+ -- enough characters for the worst-case four-byte UTF-8 encoding.
36
+ candidate := left(source_text, prefix_budget);
37
+ WHILE octet_length(candidate) > prefix_budget LOOP
38
+ excess := octet_length(candidate) - prefix_budget;
39
+ candidate := left(candidate, greatest(0, char_length(candidate) - ceil(excess / 4.0)::integer));
40
+ END LOOP;
41
+ RETURN candidate || marker;
42
+ END;
43
+ $function$;
44
+
45
+ CREATE OR REPLACE FUNCTION opengeni_private.bound_workspace_control_event()
46
+ RETURNS trigger
47
+ LANGUAGE plpgsql
48
+ SET search_path = pg_catalog
49
+ AS $function$
50
+ DECLARE
51
+ source_bytes integer;
52
+ BEGIN
53
+ IF NEW.reason IS NULL THEN
54
+ NEW.reason_original_bytes := NULL;
55
+ ELSE
56
+ source_bytes := octet_length(NEW.reason);
57
+ IF TG_OP = 'INSERT' THEN
58
+ NEW.reason_original_bytes := greatest(source_bytes, coalesce(NEW.reason_original_bytes, 0));
59
+ ELSIF NEW.reason IS DISTINCT FROM OLD.reason THEN
60
+ -- An update from an old binary carries the old metadata column even
61
+ -- though it knows only `reason`. A genuinely changed raw value starts a
62
+ -- new byte fact instead of inheriting the previous value's larger count.
63
+ NEW.reason_original_bytes := source_bytes;
64
+ ELSE
65
+ NEW.reason_original_bytes := greatest(
66
+ source_bytes,
67
+ coalesce(NEW.reason_original_bytes, OLD.reason_original_bytes, 0)
68
+ );
69
+ END IF;
70
+ NEW.reason := opengeni_private.truncate_workspace_control_text(NEW.reason, 8192);
71
+ END IF;
72
+
73
+ source_bytes := octet_length(NEW.actor);
74
+ IF TG_OP = 'INSERT' THEN
75
+ NEW.actor_original_bytes := greatest(source_bytes, coalesce(NEW.actor_original_bytes, 0));
76
+ ELSIF NEW.actor IS DISTINCT FROM OLD.actor THEN
77
+ NEW.actor_original_bytes := source_bytes;
78
+ ELSE
79
+ NEW.actor_original_bytes := greatest(
80
+ source_bytes,
81
+ coalesce(NEW.actor_original_bytes, OLD.actor_original_bytes, 0)
82
+ );
83
+ END IF;
84
+ NEW.actor := opengeni_private.truncate_workspace_control_text(NEW.actor, 1024);
85
+ RETURN NEW;
86
+ END;
87
+ $function$;
88
+
89
+ CREATE TRIGGER workspace_control_events_bound_fields
90
+ BEFORE INSERT OR UPDATE OF reason, actor, reason_original_bytes, actor_original_bytes
91
+ ON workspace_control_events
92
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.bound_workspace_control_event();
93
+
94
+ -- Rewrite only historical rows that violate the new durable caps. Already
95
+ -- bounded rows are safe to serve as-is and retain null metadata as the explicit
96
+ -- rolling-upgrade legacy shape; avoiding a blanket UPDATE keeps installation
97
+ -- work proportional to poison rows instead of table cardinality. The original
98
+ -- counts are supplied in the same statement so truncation never erases truth.
99
+ UPDATE workspace_control_events
100
+ SET
101
+ reason_original_bytes = CASE WHEN reason IS NULL THEN NULL ELSE octet_length(reason) END,
102
+ actor_original_bytes = octet_length(actor),
103
+ reason = reason,
104
+ actor = actor
105
+ WHERE octet_length(reason) > 8192
106
+ OR octet_length(actor) > 1024;
107
+
108
+ ALTER TABLE workspace_control_events
109
+ ADD CONSTRAINT workspace_control_events_reason_bytes_check
110
+ CHECK (reason IS NULL OR octet_length(reason) <= 8192) NOT VALID,
111
+ ADD CONSTRAINT workspace_control_events_actor_bytes_check
112
+ CHECK (octet_length(actor) <= 1024) NOT VALID,
113
+ ADD CONSTRAINT workspace_control_events_original_bytes_check
114
+ CHECK (
115
+ (reason IS NULL AND reason_original_bytes IS NULL)
116
+ OR (
117
+ reason IS NOT NULL
118
+ AND (
119
+ reason_original_bytes IS NULL
120
+ OR reason_original_bytes >= octet_length(reason)
121
+ )
122
+ )
123
+ ) NOT VALID,
124
+ ADD CONSTRAINT workspace_control_events_actor_original_bytes_check
125
+ CHECK (
126
+ actor_original_bytes IS NULL
127
+ OR actor_original_bytes >= octet_length(actor)
128
+ ) NOT VALID;
129
+
130
+ ALTER TABLE workspace_control_events
131
+ VALIDATE CONSTRAINT workspace_control_events_reason_bytes_check,
132
+ VALIDATE CONSTRAINT workspace_control_events_actor_bytes_check,
133
+ VALIDATE CONSTRAINT workspace_control_events_original_bytes_check,
134
+ VALIDATE CONSTRAINT workspace_control_events_actor_original_bytes_check;
@@ -0,0 +1,32 @@
1
+ -- deployment-mode: rolling
2
+ -- OPE-64: finish the 0067 expand/contract sequence. The trigger has bounded
3
+ -- every new/updated row since 0067; now rewrite only legacy violations through
4
+ -- that trigger and validate the durable storage invariant.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ UPDATE session_events
10
+ SET
11
+ payload = payload,
12
+ type = type,
13
+ client_event_id = client_event_id,
14
+ producer_id = producer_id,
15
+ turn_association = turn_association,
16
+ duplicate_reason = duplicate_reason
17
+ WHERE octet_length(payload::text) > 65536
18
+ OR octet_length(type) > 256
19
+ OR position(E'\n' in type) > 0
20
+ OR position(E'\r' in type) > 0
21
+ OR (client_event_id IS NOT NULL AND octet_length(client_event_id) > 1024)
22
+ OR (producer_id IS NOT NULL AND octet_length(producer_id) > 1024)
23
+ OR (turn_association IS NOT NULL AND octet_length(turn_association) > 64)
24
+ OR (duplicate_reason IS NOT NULL AND octet_length(duplicate_reason) > 4096);
25
+
26
+ ALTER TABLE session_events
27
+ VALIDATE CONSTRAINT session_events_payload_bytes_check,
28
+ VALIDATE CONSTRAINT session_events_type_bytes_check,
29
+ VALIDATE CONSTRAINT session_events_client_event_id_bytes_check,
30
+ VALIDATE CONSTRAINT session_events_producer_id_bytes_check,
31
+ VALIDATE CONSTRAINT session_events_turn_association_bytes_check,
32
+ VALIDATE CONSTRAINT session_events_duplicate_reason_bytes_check;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_events_workspace_session_type_sequence_idx"
4
+ ON "session_events" ("workspace_id", "session_id", "type", "sequence" DESC);
@@ -0,0 +1,10 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_events_workspace_session_monitoring_tail_idx"
4
+ ON "session_events" ("workspace_id", "session_id", "sequence" DESC)
5
+ WHERE "type" NOT IN (
6
+ 'agent.message.delta',
7
+ 'agent.reasoning.delta',
8
+ 'sandbox.command.output.delta',
9
+ 'terminal.pty.output.delta'
10
+ );
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_created_id_idx"
4
+ ON "sessions" ("workspace_id", "created_at" DESC, "id" DESC);
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_updated_id_idx"
4
+ ON "sessions" ("workspace_id", "updated_at" DESC, "id" DESC);
@@ -0,0 +1,72 @@
1
+ -- deployment-mode: rolling
2
+ -- OPE-65: transactional workspace revisions make updated-order discovery and
3
+ -- its next incremental scan one gap-free handoff independent of application
4
+ -- clocks. Existing sessions remain revision zero until their next activity.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ CREATE TABLE "workspace_session_activity_revisions" (
10
+ "workspace_id" uuid PRIMARY KEY,
11
+ "account_id" uuid NOT NULL,
12
+ "revision" bigint NOT NULL DEFAULT 0,
13
+ CONSTRAINT "workspace_session_activity_revisions_workspace_account_fk"
14
+ FOREIGN KEY ("workspace_id", "account_id")
15
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
16
+ CONSTRAINT "workspace_session_activity_revisions_revision_check"
17
+ CHECK ("revision" >= 0)
18
+ );
19
+
20
+ ALTER TABLE "workspace_session_activity_revisions" ENABLE ROW LEVEL SECURITY;
21
+ ALTER TABLE "workspace_session_activity_revisions" FORCE ROW LEVEL SECURITY;
22
+ CREATE POLICY workspace_isolation ON "workspace_session_activity_revisions"
23
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
24
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
25
+
26
+ -- PostgreSQL installs this constant default without rewriting existing rows.
27
+ -- Revision zero is an intentional legacy bucket ordered by (updated_at, id).
28
+ ALTER TABLE "sessions"
29
+ ADD COLUMN "activity_revision" bigint NOT NULL DEFAULT 0;
30
+
31
+ CREATE OR REPLACE FUNCTION opengeni_private.assign_session_activity_revision()
32
+ RETURNS trigger
33
+ LANGUAGE plpgsql
34
+ SET search_path = pg_catalog
35
+ AS $function$
36
+ DECLARE
37
+ next_revision bigint;
38
+ BEGIN
39
+ EXECUTE format(
40
+ 'INSERT INTO %I.workspace_session_activity_revisions AS counter '
41
+ || '(workspace_id, account_id, revision) VALUES ($1, $2, 1) '
42
+ || 'ON CONFLICT (workspace_id) DO UPDATE SET '
43
+ || 'account_id = excluded.account_id, revision = counter.revision + 1 '
44
+ || 'RETURNING revision',
45
+ TG_TABLE_SCHEMA
46
+ )
47
+ INTO next_revision
48
+ USING NEW.workspace_id, NEW.account_id;
49
+
50
+ NEW.activity_revision := next_revision;
51
+ RETURN NEW;
52
+ END;
53
+ $function$;
54
+
55
+ CREATE TRIGGER sessions_assign_activity_revision
56
+ BEFORE INSERT OR UPDATE OF updated_at, activity_revision
57
+ ON "sessions"
58
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.assign_session_activity_revision();
59
+
60
+ DO $grants$
61
+ DECLARE target_schema text := current_schema();
62
+ BEGIN
63
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
64
+ EXECUTE format(
65
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.workspace_session_activity_revisions TO opengeni_app',
66
+ target_schema
67
+ );
68
+ END IF;
69
+ END $grants$;
70
+
71
+ RESET statement_timeout;
72
+ RESET lock_timeout;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "sessions_workspace_activity_revision_idx"
4
+ ON "sessions" ("workspace_id", "activity_revision" DESC, "updated_at" DESC, "id" DESC);
@@ -0,0 +1,13 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0063 recreates this SECURITY DEFINER function and revokes its
3
+ -- default PUBLIC authority. Restore only the runtime dispatcher's exact grant;
4
+ -- production Helm runs migrations without a separate provision-roles pass.
5
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer) FROM PUBLIC;
6
+
7
+ DO $$
8
+ BEGIN
9
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
10
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_workflow_wakes(integer)
11
+ TO opengeni_app;
12
+ END IF;
13
+ END $$;
@@ -0,0 +1,4 @@
1
+ -- deployment-mode: rolling
2
+ -- opengeni:concurrent-index lock-timeout=5s
3
+ CREATE INDEX CONCURRENTLY "session_turn_attempts_latest_session_idx"
4
+ ON "session_turn_attempts" ("workspace_id", "session_id", "started_at" DESC, "id" DESC);