@opengeni/db 0.28.1 → 0.28.9

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 (52) hide show
  1. package/dist/{chunk-HT5T62CC.js → chunk-P6CUHXQZ.js} +651 -206
  2. package/dist/chunk-P6CUHXQZ.js.map +1 -0
  3. package/dist/{chunk-DBS5HPEW.js → chunk-VHBFHMPC.js} +7 -1
  4. package/dist/chunk-VHBFHMPC.js.map +1 -0
  5. package/dist/environment-crypto.d.ts +8 -4
  6. package/dist/index.d.ts +265 -20
  7. package/dist/index.js +3966 -2185
  8. package/dist/index.js.map +1 -1
  9. package/dist/lossless-columns.d.ts +58 -0
  10. package/dist/lossless-json.d.ts +39 -0
  11. package/dist/memory-domain.d.ts +3 -6
  12. package/dist/persistence-errors.d.ts +7 -14
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/runtime-posture.d.ts +2 -2
  15. package/dist/schema.d.ts +1419 -271
  16. package/dist/schema.js +9 -1
  17. package/dist/session-realtime-terminal.d.ts +7 -0
  18. package/dist/transcription-recordings-schema.d.ts +44 -6
  19. package/dist/transcription-recordings.d.ts +1 -1
  20. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  21. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  22. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  23. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  24. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  25. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  26. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  27. package/drizzle/0181_connected_machine_removal.sql +52 -0
  28. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  29. package/package.json +4 -4
  30. package/src/database.ts +7 -1
  31. package/src/environment-crypto.ts +22 -9
  32. package/src/index.ts +4253 -1601
  33. package/src/lossless-columns.ts +35 -0
  34. package/src/lossless-json.ts +322 -0
  35. package/src/memory-domain.ts +5 -44
  36. package/src/persistence-errors.ts +29 -34
  37. package/src/runtime-posture.ts +6 -0
  38. package/src/schema.ts +260 -38
  39. package/src/session-control.ts +125 -76
  40. package/src/session-queue-commands.ts +325 -234
  41. package/src/session-realtime-context.ts +18 -5
  42. package/src/session-realtime-ledger.ts +29 -7
  43. package/src/session-realtime-mirror.ts +4 -2
  44. package/src/session-realtime-terminal.ts +89 -21
  45. package/src/session-realtime.ts +3 -2
  46. package/src/session-tool-call-settlement.ts +29 -15
  47. package/src/transcription-recordings-schema.ts +5 -2
  48. package/src/transcription-recordings.ts +70 -40
  49. package/dist/chunk-DBS5HPEW.js.map +0 -1
  50. package/dist/chunk-HT5T62CC.js.map +0 -1
  51. package/dist/event-payload-sanitizer.d.ts +0 -32
  52. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,975 @@
1
+ -- deployment-mode: rolling
2
+ -- Canonical OpenGeni JSON is stored in full. Bounded payloads are derived only
3
+ -- for explicit monitoring/public projections; the write boundary must not
4
+ -- replace source bytes merely to satisfy a timeline preview envelope.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ -- Codec/version truth is out-of-band. Existing rows and writes from an older
10
+ -- application remain NULL and are therefore literal data, even when a value is
11
+ -- byte-for-byte identical to a valid active marker. The new application writes
12
+ -- version 1 only in the same statement as its encoded value; there is
13
+ -- deliberately no database or application default during the rolling window.
14
+ ALTER TABLE sessions
15
+ ADD COLUMN IF NOT EXISTS initial_message_codec_version integer;
16
+ ALTER TABLE session_realtime_entries
17
+ ADD COLUMN IF NOT EXISTS text_codec_version integer,
18
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer;
19
+ ALTER TABLE knowledge_memories
20
+ ADD COLUMN IF NOT EXISTS text_codec_version integer;
21
+ ALTER TABLE session_turns
22
+ ADD COLUMN IF NOT EXISTS prompt_codec_version integer;
23
+ ALTER TABLE session_system_updates
24
+ ADD COLUMN IF NOT EXISTS summary_codec_version integer,
25
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer;
26
+ ALTER TABLE session_system_update_outbox
27
+ ADD COLUMN IF NOT EXISTS summary_codec_version integer,
28
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer;
29
+ ALTER TABLE session_events
30
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer;
31
+ ALTER TABLE host_export_outbox
32
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer;
33
+ ALTER TABLE host_export_consumers
34
+ ADD COLUMN IF NOT EXISTS last_error_codec_version integer;
35
+ ALTER TABLE agent_run_states
36
+ ADD COLUMN IF NOT EXISTS serialized_run_state_codec_version integer,
37
+ ADD COLUMN IF NOT EXISTS pending_approvals_codec_version integer;
38
+ ALTER TABLE session_history_items
39
+ ADD COLUMN IF NOT EXISTS item_codec_version integer;
40
+ ALTER TABLE session_pending_tool_calls
41
+ ADD COLUMN IF NOT EXISTS call_item_codec_version integer,
42
+ ADD COLUMN IF NOT EXISTS result_item_codec_version integer;
43
+ ALTER TABLE sandbox_session_envelopes
44
+ ADD COLUMN IF NOT EXISTS envelope_codec_version integer;
45
+ ALTER TABLE session_recordings
46
+ ADD COLUMN IF NOT EXISTS reason_codec_version integer;
47
+ ALTER TABLE host_export_dead_letters
48
+ ADD COLUMN IF NOT EXISTS envelope_codec_version integer,
49
+ ADD COLUMN IF NOT EXISTS event_payload_codec_version integer;
50
+ ALTER TABLE audit_events
51
+ ADD COLUMN IF NOT EXISTS metadata_codec_version integer;
52
+ ALTER TABLE rig_changes
53
+ ADD COLUMN IF NOT EXISTS payload_codec_version integer,
54
+ ADD COLUMN IF NOT EXISTS verification_codec_version integer;
55
+ ALTER TABLE transcription_recordings
56
+ ADD COLUMN IF NOT EXISTS transcript_text_codec_version integer;
57
+ ALTER TABLE transcription_recording_segments
58
+ ADD COLUMN IF NOT EXISTS transcript_text_codec_version integer;
59
+
60
+ ALTER TABLE sessions
61
+ ADD CONSTRAINT sessions_initial_message_codec_version_chk
62
+ CHECK (initial_message_codec_version IS NULL OR initial_message_codec_version = 1) NOT VALID;
63
+ ALTER TABLE session_realtime_entries
64
+ ADD CONSTRAINT session_realtime_entries_text_codec_version_chk
65
+ CHECK (text_codec_version IS NULL OR text_codec_version = 1) NOT VALID,
66
+ ADD CONSTRAINT session_realtime_entries_payload_codec_version_chk
67
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID;
68
+ ALTER TABLE knowledge_memories
69
+ ADD CONSTRAINT knowledge_memories_text_codec_version_chk
70
+ CHECK (text_codec_version IS NULL OR text_codec_version = 1) NOT VALID;
71
+ ALTER TABLE session_turns
72
+ ADD CONSTRAINT session_turns_prompt_codec_version_chk
73
+ CHECK (prompt_codec_version IS NULL OR prompt_codec_version = 1) NOT VALID;
74
+ ALTER TABLE session_system_updates
75
+ ADD CONSTRAINT session_system_updates_summary_codec_version_chk
76
+ CHECK (summary_codec_version IS NULL OR summary_codec_version = 1) NOT VALID,
77
+ ADD CONSTRAINT session_system_updates_payload_codec_version_chk
78
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID;
79
+ ALTER TABLE session_system_update_outbox
80
+ ADD CONSTRAINT session_system_update_outbox_summary_codec_version_chk
81
+ CHECK (summary_codec_version IS NULL OR summary_codec_version = 1) NOT VALID,
82
+ ADD CONSTRAINT session_system_update_outbox_payload_codec_version_chk
83
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID;
84
+ ALTER TABLE session_events
85
+ ADD CONSTRAINT session_events_payload_codec_version_chk
86
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID;
87
+ ALTER TABLE host_export_outbox
88
+ ADD CONSTRAINT host_export_outbox_payload_codec_version_chk
89
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID;
90
+ ALTER TABLE host_export_consumers
91
+ ADD CONSTRAINT host_export_consumers_last_error_codec_version_chk
92
+ CHECK (last_error_codec_version IS NULL OR last_error_codec_version = 1) NOT VALID;
93
+ ALTER TABLE agent_run_states
94
+ ADD CONSTRAINT agent_run_states_serialized_codec_version_chk
95
+ CHECK (serialized_run_state_codec_version IS NULL OR serialized_run_state_codec_version = 1) NOT VALID,
96
+ ADD CONSTRAINT agent_run_states_pending_codec_version_chk
97
+ CHECK (pending_approvals_codec_version IS NULL OR pending_approvals_codec_version = 1) NOT VALID;
98
+ ALTER TABLE session_history_items
99
+ ADD CONSTRAINT session_history_items_item_codec_version_chk
100
+ CHECK (item_codec_version IS NULL OR item_codec_version = 1) NOT VALID;
101
+ ALTER TABLE session_pending_tool_calls
102
+ ADD CONSTRAINT session_pending_tool_calls_call_codec_version_chk
103
+ CHECK (call_item_codec_version IS NULL OR call_item_codec_version = 1) NOT VALID,
104
+ ADD CONSTRAINT session_pending_tool_calls_result_codec_version_chk
105
+ CHECK (result_item_codec_version IS NULL OR result_item_codec_version = 1) NOT VALID;
106
+ ALTER TABLE sandbox_session_envelopes
107
+ ADD CONSTRAINT sandbox_session_envelopes_codec_version_chk
108
+ CHECK (envelope_codec_version IS NULL OR envelope_codec_version = 1) NOT VALID;
109
+ ALTER TABLE session_recordings
110
+ ADD CONSTRAINT session_recordings_reason_codec_version_chk
111
+ CHECK (reason_codec_version IS NULL OR reason_codec_version = 1) NOT VALID;
112
+ ALTER TABLE host_export_dead_letters
113
+ ADD CONSTRAINT host_export_dead_letters_codec_version_chk
114
+ CHECK (envelope_codec_version IS NULL OR envelope_codec_version = 1) NOT VALID,
115
+ ADD CONSTRAINT host_export_dead_letters_event_payload_codec_version_chk
116
+ CHECK (event_payload_codec_version IS NULL OR event_payload_codec_version = 1) NOT VALID;
117
+ ALTER TABLE audit_events
118
+ ADD CONSTRAINT audit_events_metadata_codec_version_chk
119
+ CHECK (metadata_codec_version IS NULL OR metadata_codec_version = 1) NOT VALID;
120
+ ALTER TABLE rig_changes
121
+ ADD CONSTRAINT rig_changes_payload_codec_version_chk
122
+ CHECK (payload_codec_version IS NULL OR payload_codec_version = 1) NOT VALID,
123
+ ADD CONSTRAINT rig_changes_verification_codec_version_chk
124
+ CHECK (verification_codec_version IS NULL OR verification_codec_version = 1) NOT VALID;
125
+ ALTER TABLE transcription_recordings
126
+ ADD CONSTRAINT transcription_recordings_text_codec_version_chk
127
+ CHECK (transcript_text_codec_version IS NULL OR transcript_text_codec_version = 1) NOT VALID;
128
+ ALTER TABLE transcription_recording_segments
129
+ ADD CONSTRAINT transcription_recording_segments_text_codec_version_chk
130
+ CHECK (transcript_text_codec_version IS NULL OR transcript_text_codec_version = 1) NOT VALID;
131
+
132
+ -- During the rolling window, an old application can update a protected value
133
+ -- on a row whose companion is already 1 while omitting the unknown companion
134
+ -- column. Preserve unrelated updates, but reset codec truth to literal NULL
135
+ -- when such an identified old writer changes protected content without also
136
+ -- changing its companion. New createDb connections carry a distinct startup
137
+ -- application_name; supported injected/embedded handles set the transaction-
138
+ -- local writer GUC from the shared RLS context hook.
139
+ CREATE OR REPLACE FUNCTION opengeni_private.fence_legacy_lossless_content_update()
140
+ RETURNS trigger
141
+ LANGUAGE plpgsql
142
+ SET search_path = pg_catalog
143
+ AS $function$
144
+ BEGIN
145
+ IF current_setting('opengeni.lossless_content_writer', true) = '1'
146
+ OR current_setting('application_name', true) = 'opengeni-lossless-v1' THEN
147
+ RETURN NEW;
148
+ END IF;
149
+
150
+ CASE TG_TABLE_NAME
151
+ WHEN 'sessions' THEN
152
+ IF NEW.initial_message IS DISTINCT FROM OLD.initial_message
153
+ AND NEW.initial_message_codec_version IS NOT DISTINCT FROM OLD.initial_message_codec_version THEN
154
+ NEW.initial_message_codec_version := NULL;
155
+ END IF;
156
+ WHEN 'session_realtime_entries' THEN
157
+ IF NEW.text IS DISTINCT FROM OLD.text
158
+ AND NEW.text_codec_version IS NOT DISTINCT FROM OLD.text_codec_version THEN
159
+ NEW.text_codec_version := NULL;
160
+ END IF;
161
+ IF NEW.payload IS DISTINCT FROM OLD.payload
162
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
163
+ NEW.payload_codec_version := NULL;
164
+ END IF;
165
+ WHEN 'knowledge_memories' THEN
166
+ IF NEW.text IS DISTINCT FROM OLD.text
167
+ AND NEW.text_codec_version IS NOT DISTINCT FROM OLD.text_codec_version THEN
168
+ NEW.text_codec_version := NULL;
169
+ END IF;
170
+ WHEN 'session_turns' THEN
171
+ IF NEW.prompt IS DISTINCT FROM OLD.prompt
172
+ AND NEW.prompt_codec_version IS NOT DISTINCT FROM OLD.prompt_codec_version THEN
173
+ NEW.prompt_codec_version := NULL;
174
+ END IF;
175
+ WHEN 'session_system_updates' THEN
176
+ IF NEW.summary IS DISTINCT FROM OLD.summary
177
+ AND NEW.summary_codec_version IS NOT DISTINCT FROM OLD.summary_codec_version THEN
178
+ NEW.summary_codec_version := NULL;
179
+ END IF;
180
+ IF NEW.payload IS DISTINCT FROM OLD.payload
181
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
182
+ NEW.payload_codec_version := NULL;
183
+ END IF;
184
+ WHEN 'session_system_update_outbox' THEN
185
+ IF NEW.summary IS DISTINCT FROM OLD.summary
186
+ AND NEW.summary_codec_version IS NOT DISTINCT FROM OLD.summary_codec_version THEN
187
+ NEW.summary_codec_version := NULL;
188
+ END IF;
189
+ IF NEW.payload IS DISTINCT FROM OLD.payload
190
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
191
+ NEW.payload_codec_version := NULL;
192
+ END IF;
193
+ WHEN 'session_events' THEN
194
+ IF NEW.payload IS DISTINCT FROM OLD.payload
195
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
196
+ NEW.payload_codec_version := NULL;
197
+ END IF;
198
+ WHEN 'host_export_outbox' THEN
199
+ IF NEW.payload IS DISTINCT FROM OLD.payload
200
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
201
+ NEW.payload_codec_version := NULL;
202
+ END IF;
203
+ WHEN 'host_export_consumers' THEN
204
+ IF NEW.last_error IS DISTINCT FROM OLD.last_error
205
+ AND NEW.last_error_codec_version IS NOT DISTINCT FROM OLD.last_error_codec_version THEN
206
+ NEW.last_error_codec_version := NULL;
207
+ END IF;
208
+ WHEN 'agent_run_states' THEN
209
+ IF NEW.serialized_run_state IS DISTINCT FROM OLD.serialized_run_state
210
+ AND NEW.serialized_run_state_codec_version IS NOT DISTINCT FROM OLD.serialized_run_state_codec_version THEN
211
+ NEW.serialized_run_state_codec_version := NULL;
212
+ END IF;
213
+ IF NEW.pending_approvals IS DISTINCT FROM OLD.pending_approvals
214
+ AND NEW.pending_approvals_codec_version IS NOT DISTINCT FROM OLD.pending_approvals_codec_version THEN
215
+ NEW.pending_approvals_codec_version := NULL;
216
+ END IF;
217
+ WHEN 'session_history_items' THEN
218
+ IF NEW.item IS DISTINCT FROM OLD.item
219
+ AND NEW.item_codec_version IS NOT DISTINCT FROM OLD.item_codec_version THEN
220
+ NEW.item_codec_version := NULL;
221
+ END IF;
222
+ WHEN 'session_pending_tool_calls' THEN
223
+ IF NEW.call_item IS DISTINCT FROM OLD.call_item
224
+ AND NEW.call_item_codec_version IS NOT DISTINCT FROM OLD.call_item_codec_version THEN
225
+ NEW.call_item_codec_version := NULL;
226
+ END IF;
227
+ IF NEW.result_item IS DISTINCT FROM OLD.result_item
228
+ AND NEW.result_item_codec_version IS NOT DISTINCT FROM OLD.result_item_codec_version THEN
229
+ NEW.result_item_codec_version := NULL;
230
+ END IF;
231
+ WHEN 'sandbox_session_envelopes' THEN
232
+ IF NEW.envelope IS DISTINCT FROM OLD.envelope
233
+ AND NEW.envelope_codec_version IS NOT DISTINCT FROM OLD.envelope_codec_version THEN
234
+ NEW.envelope_codec_version := NULL;
235
+ END IF;
236
+ WHEN 'session_recordings' THEN
237
+ IF NEW.reason IS DISTINCT FROM OLD.reason
238
+ AND NEW.reason_codec_version IS NOT DISTINCT FROM OLD.reason_codec_version THEN
239
+ NEW.reason_codec_version := NULL;
240
+ END IF;
241
+ WHEN 'host_export_dead_letters' THEN
242
+ IF NEW.envelope IS DISTINCT FROM OLD.envelope
243
+ AND NEW.envelope_codec_version IS NOT DISTINCT FROM OLD.envelope_codec_version THEN
244
+ NEW.envelope_codec_version := NULL;
245
+ END IF;
246
+ IF NEW.envelope #> '{event,payload}' IS DISTINCT FROM OLD.envelope #> '{event,payload}'
247
+ AND NEW.event_payload_codec_version IS NOT DISTINCT FROM OLD.event_payload_codec_version THEN
248
+ NEW.event_payload_codec_version := NULL;
249
+ END IF;
250
+ WHEN 'audit_events' THEN
251
+ IF NEW.metadata IS DISTINCT FROM OLD.metadata
252
+ AND NEW.metadata_codec_version IS NOT DISTINCT FROM OLD.metadata_codec_version THEN
253
+ NEW.metadata_codec_version := NULL;
254
+ END IF;
255
+ WHEN 'rig_changes' THEN
256
+ IF NEW.payload IS DISTINCT FROM OLD.payload
257
+ AND NEW.payload_codec_version IS NOT DISTINCT FROM OLD.payload_codec_version THEN
258
+ NEW.payload_codec_version := NULL;
259
+ END IF;
260
+ IF NEW.verification IS DISTINCT FROM OLD.verification
261
+ AND NEW.verification_codec_version IS NOT DISTINCT FROM OLD.verification_codec_version THEN
262
+ NEW.verification_codec_version := NULL;
263
+ END IF;
264
+ WHEN 'transcription_recordings' THEN
265
+ IF NEW.transcript_text IS DISTINCT FROM OLD.transcript_text
266
+ AND NEW.transcript_text_codec_version IS NOT DISTINCT FROM OLD.transcript_text_codec_version THEN
267
+ NEW.transcript_text_codec_version := NULL;
268
+ END IF;
269
+ WHEN 'transcription_recording_segments' THEN
270
+ IF NEW.transcript_text IS DISTINCT FROM OLD.transcript_text
271
+ AND NEW.transcript_text_codec_version IS NOT DISTINCT FROM OLD.transcript_text_codec_version THEN
272
+ NEW.transcript_text_codec_version := NULL;
273
+ END IF;
274
+ ELSE
275
+ RAISE EXCEPTION 'unsupported lossless-content fence table: %', TG_TABLE_NAME;
276
+ END CASE;
277
+ RETURN NEW;
278
+ END
279
+ $function$;
280
+
281
+ DROP TRIGGER IF EXISTS sessions_lossless_legacy_update_fence ON sessions;
282
+ CREATE TRIGGER sessions_lossless_legacy_update_fence
283
+ BEFORE UPDATE OF initial_message ON sessions
284
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
285
+ DROP TRIGGER IF EXISTS session_realtime_entries_lossless_legacy_update_fence ON session_realtime_entries;
286
+ CREATE TRIGGER session_realtime_entries_lossless_legacy_update_fence
287
+ BEFORE UPDATE OF text, payload ON session_realtime_entries
288
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
289
+ DROP TRIGGER IF EXISTS knowledge_memories_lossless_legacy_update_fence ON knowledge_memories;
290
+ CREATE TRIGGER knowledge_memories_lossless_legacy_update_fence
291
+ BEFORE UPDATE OF text ON knowledge_memories
292
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
293
+ DROP TRIGGER IF EXISTS session_turns_lossless_legacy_update_fence ON session_turns;
294
+ CREATE TRIGGER session_turns_lossless_legacy_update_fence
295
+ BEFORE UPDATE OF prompt ON session_turns
296
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
297
+ DROP TRIGGER IF EXISTS session_system_updates_lossless_legacy_update_fence ON session_system_updates;
298
+ CREATE TRIGGER session_system_updates_lossless_legacy_update_fence
299
+ BEFORE UPDATE OF summary, payload ON session_system_updates
300
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
301
+ DROP TRIGGER IF EXISTS session_system_update_outbox_lossless_legacy_update_fence ON session_system_update_outbox;
302
+ CREATE TRIGGER session_system_update_outbox_lossless_legacy_update_fence
303
+ BEFORE UPDATE OF summary, payload ON session_system_update_outbox
304
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
305
+ DROP TRIGGER IF EXISTS session_events_lossless_legacy_update_fence ON session_events;
306
+ CREATE TRIGGER session_events_lossless_legacy_update_fence
307
+ BEFORE UPDATE OF payload ON session_events
308
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
309
+ DROP TRIGGER IF EXISTS host_export_outbox_lossless_legacy_update_fence ON host_export_outbox;
310
+ CREATE TRIGGER host_export_outbox_lossless_legacy_update_fence
311
+ BEFORE UPDATE OF payload ON host_export_outbox
312
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
313
+ DROP TRIGGER IF EXISTS host_export_consumers_lossless_legacy_update_fence ON host_export_consumers;
314
+ CREATE TRIGGER host_export_consumers_lossless_legacy_update_fence
315
+ BEFORE UPDATE OF last_error ON host_export_consumers
316
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
317
+ DROP TRIGGER IF EXISTS agent_run_states_lossless_legacy_update_fence ON agent_run_states;
318
+ CREATE TRIGGER agent_run_states_lossless_legacy_update_fence
319
+ BEFORE UPDATE OF serialized_run_state, pending_approvals ON agent_run_states
320
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
321
+ DROP TRIGGER IF EXISTS session_history_items_lossless_legacy_update_fence ON session_history_items;
322
+ CREATE TRIGGER session_history_items_lossless_legacy_update_fence
323
+ BEFORE UPDATE OF item ON session_history_items
324
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
325
+ DROP TRIGGER IF EXISTS session_pending_tool_calls_lossless_legacy_update_fence ON session_pending_tool_calls;
326
+ CREATE TRIGGER session_pending_tool_calls_lossless_legacy_update_fence
327
+ BEFORE UPDATE OF call_item, result_item ON session_pending_tool_calls
328
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
329
+ DROP TRIGGER IF EXISTS sandbox_session_envelopes_lossless_legacy_update_fence ON sandbox_session_envelopes;
330
+ CREATE TRIGGER sandbox_session_envelopes_lossless_legacy_update_fence
331
+ BEFORE UPDATE OF envelope ON sandbox_session_envelopes
332
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
333
+ DROP TRIGGER IF EXISTS session_recordings_lossless_legacy_update_fence ON session_recordings;
334
+ CREATE TRIGGER session_recordings_lossless_legacy_update_fence
335
+ BEFORE UPDATE OF reason ON session_recordings
336
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
337
+ DROP TRIGGER IF EXISTS host_export_dead_letters_lossless_legacy_update_fence ON host_export_dead_letters;
338
+ CREATE TRIGGER host_export_dead_letters_lossless_legacy_update_fence
339
+ BEFORE UPDATE OF envelope ON host_export_dead_letters
340
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
341
+ DROP TRIGGER IF EXISTS audit_events_lossless_legacy_update_fence ON audit_events;
342
+ CREATE TRIGGER audit_events_lossless_legacy_update_fence
343
+ BEFORE UPDATE OF metadata ON audit_events
344
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
345
+ DROP TRIGGER IF EXISTS rig_changes_lossless_legacy_update_fence ON rig_changes;
346
+ CREATE TRIGGER rig_changes_lossless_legacy_update_fence
347
+ BEFORE UPDATE OF payload, verification ON rig_changes
348
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
349
+ DROP TRIGGER IF EXISTS transcription_recordings_lossless_legacy_update_fence ON transcription_recordings;
350
+ CREATE TRIGGER transcription_recordings_lossless_legacy_update_fence
351
+ BEFORE UPDATE OF transcript_text ON transcription_recordings
352
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
353
+ DROP TRIGGER IF EXISTS transcription_recording_segments_lossless_legacy_update_fence ON transcription_recording_segments;
354
+ CREATE TRIGGER transcription_recording_segments_lossless_legacy_update_fence
355
+ BEFORE UPDATE OF transcript_text ON transcription_recording_segments
356
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_legacy_lossless_content_update();
357
+
358
+ REVOKE ALL ON FUNCTION opengeni_private.fence_legacy_lossless_content_update() FROM PUBLIC;
359
+
360
+ -- The claim function has an explicit return shape, so carry the out-of-band
361
+ -- versions with the claimed canonical fields.
362
+ DROP FUNCTION opengeni_private.claim_session_system_update_outbox(integer);
363
+ CREATE FUNCTION opengeni_private.claim_session_system_update_outbox(p_limit integer)
364
+ RETURNS TABLE (
365
+ id uuid, account_id uuid, workspace_id uuid, source_session_id uuid,
366
+ target_session_id uuid, dedupe_key text, kind text, classification text,
367
+ source_id text, summary text, summary_codec_version integer,
368
+ payload jsonb, payload_codec_version integer, lineage jsonb,
369
+ personal_connection_delegations jsonb
370
+ )
371
+ LANGUAGE plpgsql
372
+ SECURITY DEFINER
373
+ AS $function$
374
+ BEGIN
375
+ RETURN QUERY
376
+ WITH claimed AS (
377
+ SELECT o.id FROM session_system_update_outbox o
378
+ WHERE o.status = 'pending'
379
+ ORDER BY o.created_at, o.id
380
+ FOR UPDATE SKIP LOCKED
381
+ LIMIT greatest(1, least(coalesce(p_limit, 100), 100))
382
+ )
383
+ UPDATE session_system_update_outbox o
384
+ SET attempts = o.attempts + 1, updated_at = now()
385
+ FROM claimed c WHERE o.id = c.id
386
+ RETURNING o.id, o.account_id, o.workspace_id, o.source_session_id,
387
+ o.target_session_id, o.dedupe_key, o.kind, o.classification,
388
+ o.source_id, o.summary, o.summary_codec_version,
389
+ o.payload, o.payload_codec_version, o.lineage,
390
+ o.personal_connection_delegations;
391
+ END
392
+ $function$;
393
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_system_update_outbox(integer) FROM PUBLIC;
394
+ DO $block$
395
+ BEGIN
396
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
397
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_session_system_update_outbox(integer)
398
+ TO opengeni_app;
399
+ END IF;
400
+ END
401
+ $block$;
402
+
403
+ -- Keep the published claim_host_export_batch signature and ACL stable. The
404
+ -- deferred producer writes the session-event storage payload and its out-of-
405
+ -- band truth atomically; the application reads immutable sidecars under the
406
+ -- exact current lease in the same transaction as the existing claim call.
407
+ DO $migration$
408
+ DECLARE target_schema text := current_schema();
409
+ BEGIN
410
+ EXECUTE format($create$
411
+ CREATE OR REPLACE FUNCTION opengeni_private.enqueue_host_session_event_export()
412
+ RETURNS trigger
413
+ LANGUAGE plpgsql
414
+ SECURITY DEFINER
415
+ SET search_path = pg_catalog
416
+ AS $function$
417
+ DECLARE
418
+ v_enabled boolean;
419
+ v_initiator jsonb;
420
+ v_context jsonb := '{}'::jsonb;
421
+ v_origin text;
422
+ v_payload_bytes integer;
423
+ BEGIN
424
+ IF NEW.type IN (
425
+ 'agent.message.delta', 'agent.reasoning.delta',
426
+ 'sandbox.command.output.delta', 'terminal.pty.output.delta'
427
+ ) THEN
428
+ RETURN NEW;
429
+ END IF;
430
+
431
+ SELECT c.session_events_enabled INTO v_enabled
432
+ FROM %1$I.host_export_config c WHERE c.id = 1
433
+ FOR SHARE;
434
+ IF coalesce(v_enabled, false) = false THEN
435
+ RETURN NEW;
436
+ END IF;
437
+
438
+ IF NEW.turn_id IS NOT NULL THEN
439
+ SELECT
440
+ CASE WHEN octet_length(t.initiator_subject_id) <= 1024 THEN
441
+ jsonb_strip_nulls(jsonb_build_object(
442
+ 'kind', t.initiator_kind,
443
+ 'subjectId', t.initiator_subject_id,
444
+ 'label', CASE
445
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
446
+ THEN left(t.initiator_context ->> 'label', 256)
447
+ ELSE NULL
448
+ END
449
+ ))
450
+ ELSE NULL END,
451
+ jsonb_strip_nulls(jsonb_build_object(
452
+ 'label', CASE
453
+ WHEN jsonb_typeof(t.initiator_context -> 'label') = 'string'
454
+ THEN left(t.initiator_context ->> 'label', 256)
455
+ ELSE NULL
456
+ END,
457
+ 'backfill', CASE
458
+ WHEN jsonb_typeof(t.initiator_context -> 'backfill') = 'boolean'
459
+ THEN t.initiator_context -> 'backfill'
460
+ ELSE NULL
461
+ END,
462
+ 'attributionOmitted', CASE
463
+ WHEN octet_length(t.initiator_subject_id) > 1024 THEN 'subject_id_too_large'
464
+ ELSE NULL
465
+ END
466
+ )),
467
+ t.source
468
+ INTO v_initiator, v_context, v_origin
469
+ FROM %1$I.session_turns t
470
+ WHERE t.workspace_id = NEW.workspace_id AND t.id = NEW.turn_id;
471
+ ELSIF NEW.type = 'session.created' THEN
472
+ SELECT
473
+ CASE WHEN octet_length(s.created_by_subject_id) <= 1024 THEN
474
+ jsonb_strip_nulls(jsonb_build_object(
475
+ 'kind', s.created_by_kind,
476
+ 'subjectId', s.created_by_subject_id,
477
+ 'label', CASE
478
+ WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
479
+ THEN left(s.created_by_context ->> 'label', 256)
480
+ ELSE NULL
481
+ END
482
+ ))
483
+ ELSE NULL END,
484
+ jsonb_strip_nulls(jsonb_build_object(
485
+ 'label', CASE
486
+ WHEN jsonb_typeof(s.created_by_context -> 'label') = 'string'
487
+ THEN left(s.created_by_context ->> 'label', 256)
488
+ ELSE NULL
489
+ END,
490
+ 'backfill', CASE
491
+ WHEN jsonb_typeof(s.created_by_context -> 'backfill') = 'boolean'
492
+ THEN s.created_by_context -> 'backfill'
493
+ ELSE NULL
494
+ END,
495
+ 'attributionOmitted', CASE
496
+ WHEN octet_length(s.created_by_subject_id) > 1024 THEN 'subject_id_too_large'
497
+ ELSE NULL
498
+ END
499
+ )),
500
+ NULL
501
+ INTO v_initiator, v_context, v_origin
502
+ FROM %1$I.sessions s
503
+ WHERE s.workspace_id = NEW.workspace_id AND s.id = NEW.session_id;
504
+ END IF;
505
+
506
+ v_payload_bytes := octet_length(NEW.payload::text)
507
+ + octet_length(NEW.type)
508
+ + coalesce(octet_length(NEW.client_event_id), 0)
509
+ + coalesce(octet_length(NEW.turn_association), 0)
510
+ + coalesce(octet_length(NEW.duplicate_reason), 0)
511
+ + octet_length(coalesce(v_initiator, 'null'::jsonb)::text)
512
+ + octet_length(v_context::text)
513
+ + 768;
514
+ INSERT INTO %1$I.host_export_outbox (
515
+ export_kind, source_id, account_id, workspace_id, session_id,
516
+ turn_id, turn_generation, turn_attempt_id, session_sequence,
517
+ client_event_id, turn_association, duplicate_of_event_id, duplicate_reason,
518
+ event_type, idempotency_key, initiator, initiator_context, origin,
519
+ payload, payload_codec_version, envelope_bytes, occurred_at,
520
+ source_recorded_at, enqueued_at
521
+ ) VALUES (
522
+ 'session_event', NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id,
523
+ NEW.turn_id, NEW.turn_generation, NEW.turn_attempt_id, NEW.sequence,
524
+ NEW.client_event_id, NEW.turn_association, NEW.duplicate_of_event_id,
525
+ NEW.duplicate_reason,
526
+ NEW.type, 'session_event:' || NEW.id::text, v_initiator,
527
+ coalesce(v_context, '{}'::jsonb), v_origin, NEW.payload,
528
+ NEW.payload_codec_version, greatest(1, v_payload_bytes), NEW.occurred_at,
529
+ NEW.created_at, clock_timestamp()
530
+ )
531
+ ON CONFLICT (export_kind, source_id) DO NOTHING;
532
+ RETURN NEW;
533
+ END $function$;
534
+ $create$, target_schema);
535
+
536
+ EXECUTE format($create$
537
+ CREATE OR REPLACE FUNCTION opengeni_host_export.ack_host_export_batch(
538
+ p_export_kind text, p_consumer_id text, p_lease_token uuid
539
+ ) RETURNS bigint
540
+ LANGUAGE plpgsql
541
+ SECURITY DEFINER
542
+ SET search_path = pg_catalog
543
+ AS $function$
544
+ DECLARE v_checkpoint bigint;
545
+ BEGIN
546
+ UPDATE %1$I.host_export_consumers c
547
+ SET checkpoint = c.lease_through,
548
+ lease_token = NULL, lease_holder_id = NULL, lease_expires_at = NULL,
549
+ lease_from = NULL, lease_through = NULL,
550
+ consecutive_failures = 0, next_attempt_at = now(),
551
+ last_error = NULL, last_error_codec_version = NULL,
552
+ last_error_at = NULL, updated_at = now()
553
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id
554
+ AND c.lease_token = p_lease_token AND c.lease_through IS NOT NULL
555
+ RETURNING c.checkpoint INTO v_checkpoint;
556
+ IF v_checkpoint IS NULL THEN
557
+ RAISE EXCEPTION 'host export lease is stale' USING ERRCODE = '40001';
558
+ END IF;
559
+ RETURN v_checkpoint;
560
+ END $function$;
561
+ $create$, target_schema);
562
+
563
+ -- Keep the published five-argument function as the rolling old-writer
564
+ -- protocol. Its input is literal PostgreSQL text, so its companion remains
565
+ -- NULL even when the content happens to match the active codec marker.
566
+ EXECUTE format($create$
567
+ CREATE OR REPLACE FUNCTION opengeni_host_export.fail_host_export_batch(
568
+ p_export_kind text, p_consumer_id text, p_lease_token uuid,
569
+ p_error text, p_max_failures integer
570
+ ) RETURNS integer
571
+ LANGUAGE plpgsql
572
+ SECURITY DEFINER
573
+ SET search_path = pg_catalog
574
+ AS $function$
575
+ DECLARE v_failures integer;
576
+ BEGIN
577
+ UPDATE %1$I.host_export_consumers c
578
+ SET consecutive_failures = c.consecutive_failures + 1,
579
+ lease_token = NULL, lease_holder_id = NULL, lease_expires_at = NULL,
580
+ lease_from = NULL, lease_through = NULL,
581
+ next_attempt_at = now() + make_interval(
582
+ secs => least(300, greatest(1, power(2, least(c.consecutive_failures, 8))::integer))
583
+ ),
584
+ last_error = coalesce(p_error, 'host sink failed'),
585
+ last_error_codec_version = NULL,
586
+ last_error_at = now(),
587
+ blocked_at = CASE
588
+ WHEN c.consecutive_failures + 1 >= greatest(1, least(coalesce(p_max_failures, 20), 1000))
589
+ THEN now() ELSE NULL END,
590
+ updated_at = now()
591
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id
592
+ AND c.lease_token = p_lease_token
593
+ RETURNING c.consecutive_failures INTO v_failures;
594
+ IF v_failures IS NULL THEN
595
+ RAISE EXCEPTION 'host export lease is stale' USING ERRCODE = '40001';
596
+ END IF;
597
+ RETURN v_failures;
598
+ END $function$;
599
+ $create$, target_schema);
600
+
601
+ -- New writers encode the entire error in application code and stamp the
602
+ -- companion in this same SQL statement. PostgreSQL never implements or
603
+ -- infers the JavaScript lossless-text codec.
604
+ EXECUTE format($create$
605
+ CREATE OR REPLACE FUNCTION opengeni_host_export.fail_host_export_batch(
606
+ p_export_kind text, p_consumer_id text, p_lease_token uuid,
607
+ p_error text, p_max_failures integer, p_error_codec_version integer
608
+ ) RETURNS integer
609
+ LANGUAGE plpgsql
610
+ SECURITY DEFINER
611
+ SET search_path = pg_catalog
612
+ AS $function$
613
+ DECLARE v_failures integer;
614
+ BEGIN
615
+ IF p_error_codec_version IS DISTINCT FROM 1 THEN
616
+ RAISE EXCEPTION 'host export error codec version must be 1'
617
+ USING ERRCODE = '22023';
618
+ END IF;
619
+ UPDATE %1$I.host_export_consumers c
620
+ SET consecutive_failures = c.consecutive_failures + 1,
621
+ lease_token = NULL, lease_holder_id = NULL, lease_expires_at = NULL,
622
+ lease_from = NULL, lease_through = NULL,
623
+ next_attempt_at = now() + make_interval(
624
+ secs => least(300, greatest(1, power(2, least(c.consecutive_failures, 8))::integer))
625
+ ),
626
+ last_error = coalesce(p_error, 'host sink failed'),
627
+ last_error_codec_version = p_error_codec_version,
628
+ last_error_at = now(),
629
+ blocked_at = CASE
630
+ WHEN c.consecutive_failures + 1 >= greatest(1, least(coalesce(p_max_failures, 20), 1000))
631
+ THEN now() ELSE NULL END,
632
+ updated_at = now()
633
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id
634
+ AND c.lease_token = p_lease_token
635
+ RETURNING c.consecutive_failures INTO v_failures;
636
+ IF v_failures IS NULL THEN
637
+ RAISE EXCEPTION 'host export lease is stale' USING ERRCODE = '40001';
638
+ END IF;
639
+ RETURN v_failures;
640
+ END $function$;
641
+ $create$, target_schema);
642
+
643
+ EXECUTE format($create$
644
+ CREATE OR REPLACE FUNCTION opengeni_host_export.resume_host_export_consumer(
645
+ p_export_kind text, p_consumer_id text
646
+ ) RETURNS void
647
+ LANGUAGE plpgsql
648
+ SECURITY DEFINER
649
+ SET search_path = pg_catalog
650
+ AS $function$
651
+ BEGIN
652
+ UPDATE %1$I.host_export_consumers c
653
+ SET blocked_at = NULL, consecutive_failures = 0, next_attempt_at = now(),
654
+ lease_token = NULL, lease_holder_id = NULL, lease_expires_at = NULL,
655
+ lease_from = NULL, lease_through = NULL,
656
+ last_error = NULL, last_error_codec_version = NULL,
657
+ last_error_at = NULL, updated_at = now()
658
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id;
659
+ IF NOT FOUND THEN
660
+ RAISE EXCEPTION 'host export consumer not found' USING ERRCODE = 'P0002';
661
+ END IF;
662
+ END $function$;
663
+ $create$, target_schema);
664
+
665
+ EXECUTE format($create$
666
+ CREATE OR REPLACE FUNCTION opengeni_host_export.host_export_claim_sidecars(
667
+ p_export_kind text,
668
+ p_consumer_id text,
669
+ p_lease_token uuid
670
+ ) RETURNS TABLE (
671
+ export_cursor bigint,
672
+ root_session_id uuid,
673
+ payload_codec_version integer
674
+ )
675
+ LANGUAGE plpgsql
676
+ SECURITY DEFINER
677
+ SET search_path = pg_catalog
678
+ AS $function$
679
+ DECLARE
680
+ v_consumer %1$I.host_export_consumers%%ROWTYPE;
681
+ BEGIN
682
+ SELECT * INTO v_consumer
683
+ FROM %1$I.host_export_consumers c
684
+ WHERE c.export_kind = p_export_kind
685
+ AND c.consumer_id = p_consumer_id
686
+ FOR UPDATE;
687
+ IF NOT FOUND
688
+ OR v_consumer.lease_token IS DISTINCT FROM p_lease_token
689
+ OR v_consumer.lease_from IS NULL
690
+ OR v_consumer.lease_through IS NULL
691
+ OR v_consumer.lease_expires_at IS NULL
692
+ OR v_consumer.lease_expires_at <= now() THEN
693
+ RAISE EXCEPTION 'host export lease is not current'
694
+ USING ERRCODE = '55000';
695
+ END IF;
696
+
697
+ RETURN QUERY
698
+ SELECT o.export_cursor, o.root_session_id, o.payload_codec_version
699
+ FROM %1$I.host_export_outbox o
700
+ WHERE o.export_kind = p_export_kind
701
+ AND o.export_cursor > v_consumer.lease_from
702
+ AND o.export_cursor <= v_consumer.lease_through
703
+ ORDER BY o.export_cursor;
704
+ END $function$;
705
+ $create$, target_schema);
706
+
707
+ -- Preserve the published host_export_consumer_status return signature. The
708
+ -- application joins this one-column companion helper in the same statement
709
+ -- and decodes the returned last_error only when the sidecar is 1.
710
+ EXECUTE format($create$
711
+ CREATE OR REPLACE FUNCTION opengeni_host_export.host_export_consumer_status_sidecar(
712
+ p_export_kind text,
713
+ p_consumer_id text
714
+ ) RETURNS TABLE (
715
+ last_error_codec_version integer
716
+ )
717
+ LANGUAGE sql
718
+ SECURITY DEFINER
719
+ SET search_path = pg_catalog
720
+ AS $function$
721
+ SELECT c.last_error_codec_version
722
+ FROM %1$I.host_export_consumers c
723
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id;
724
+ $function$;
725
+ $create$, target_schema);
726
+
727
+ EXECUTE format($create$
728
+ CREATE OR REPLACE FUNCTION opengeni_host_export.dead_letter_host_export_head(
729
+ p_export_kind text, p_consumer_id text, p_lease_token uuid,
730
+ p_export_cursor bigint, p_reason text
731
+ ) RETURNS bigint
732
+ LANGUAGE plpgsql
733
+ SECURITY DEFINER
734
+ SET search_path = pg_catalog
735
+ AS $function$
736
+ DECLARE
737
+ v_consumer %1$I.host_export_consumers%%ROWTYPE;
738
+ v_row %1$I.host_export_outbox%%ROWTYPE;
739
+ v_envelope jsonb;
740
+ BEGIN
741
+ IF p_reason IS NULL OR length(p_reason) NOT BETWEEN 1 AND 500 THEN
742
+ RAISE EXCEPTION 'dead-letter reason must contain 1 to 500 characters'
743
+ USING ERRCODE = '22023';
744
+ END IF;
745
+ SELECT * INTO v_consumer
746
+ FROM %1$I.host_export_consumers c
747
+ WHERE c.export_kind = p_export_kind AND c.consumer_id = p_consumer_id
748
+ FOR UPDATE;
749
+ IF NOT FOUND OR v_consumer.lease_token IS DISTINCT FROM p_lease_token
750
+ OR v_consumer.lease_through IS NULL THEN
751
+ RAISE EXCEPTION 'host export lease is stale' USING ERRCODE = '40001';
752
+ END IF;
753
+ IF p_export_cursor IS NULL OR p_export_cursor <> v_consumer.checkpoint + 1
754
+ OR p_export_cursor > v_consumer.lease_through THEN
755
+ RAISE EXCEPTION 'only the leased head event may be dead-lettered'
756
+ USING ERRCODE = '22023';
757
+ END IF;
758
+
759
+ SELECT * INTO STRICT v_row FROM %1$I.host_export_outbox o
760
+ WHERE o.export_kind = p_export_kind AND o.export_cursor = p_export_cursor;
761
+ IF p_export_kind = 'session_event' THEN
762
+ v_envelope := jsonb_build_object(
763
+ 'schemaRevision', '2026-07-host-export-v1',
764
+ 'cursor', v_row.export_cursor::text,
765
+ 'idempotencyKey', v_row.idempotency_key,
766
+ 'accountId', v_row.account_id,
767
+ 'workspaceId', v_row.workspace_id,
768
+ 'initiator', v_row.initiator,
769
+ 'initiatorContext', v_row.initiator_context,
770
+ 'origin', v_row.origin,
771
+ 'event', jsonb_strip_nulls(jsonb_build_object(
772
+ 'id', v_row.source_id,
773
+ 'workspaceId', v_row.workspace_id,
774
+ 'sessionId', v_row.session_id,
775
+ 'sequence', v_row.session_sequence,
776
+ 'type', v_row.event_type,
777
+ 'payload', v_row.payload,
778
+ 'occurredAt', v_row.occurred_at,
779
+ 'clientEventId', v_row.client_event_id,
780
+ 'turnId', v_row.turn_id,
781
+ 'turnGeneration', v_row.turn_generation,
782
+ 'turnAttemptId', v_row.turn_attempt_id,
783
+ 'turnAssociation', v_row.turn_association,
784
+ 'duplicateOfEventId', v_row.duplicate_of_event_id,
785
+ 'duplicateReason', v_row.duplicate_reason
786
+ ))
787
+ );
788
+ ELSE
789
+ v_envelope := jsonb_build_object(
790
+ 'schemaRevision', '2026-07-host-export-v1',
791
+ 'cursor', v_row.export_cursor::text,
792
+ 'accountId', v_row.account_id,
793
+ 'workspaceId', v_row.workspace_id,
794
+ 'sessionId', v_row.session_id,
795
+ 'turnId', v_row.turn_id,
796
+ 'turnAttemptId', v_row.turn_attempt_id,
797
+ 'initiator', v_row.initiator,
798
+ 'initiatorContext', v_row.initiator_context,
799
+ 'origin', v_row.origin,
800
+ 'usage', v_row.payload
801
+ );
802
+ END IF;
803
+
804
+ INSERT INTO %1$I.host_export_dead_letters (
805
+ consumer_id, export_kind, export_cursor, source_id, reason, envelope,
806
+ envelope_codec_version, event_payload_codec_version
807
+ ) VALUES (
808
+ p_consumer_id, p_export_kind, p_export_cursor, v_row.source_id,
809
+ p_reason, v_envelope, NULL,
810
+ CASE WHEN p_export_kind = 'session_event' THEN v_row.payload_codec_version ELSE NULL END
811
+ ) ON CONFLICT (export_kind, consumer_id, export_cursor) DO NOTHING;
812
+
813
+ UPDATE %1$I.host_export_consumers c
814
+ SET (
815
+ checkpoint, lease_token, lease_holder_id, lease_expires_at,
816
+ lease_from, lease_through, consecutive_failures, next_attempt_at,
817
+ last_error, last_error_codec_version, last_error_at, blocked_at, updated_at
818
+ ) = (
819
+ p_export_cursor, NULL::uuid, NULL::text, NULL::timestamptz,
820
+ NULL::bigint, NULL::bigint, 0, now(),
821
+ 'dead-lettered: ' || p_reason, NULL::integer, now(), NULL::timestamptz, now()
822
+ )
823
+ WHERE c.id = v_consumer.id;
824
+ RETURN p_export_cursor;
825
+ END $function$;
826
+ $create$, target_schema);
827
+ END $migration$;
828
+
829
+ REVOKE ALL ON FUNCTION
830
+ opengeni_private.enqueue_host_session_event_export() FROM PUBLIC;
831
+ REVOKE ALL ON FUNCTION
832
+ opengeni_host_export.fail_host_export_batch(text, text, uuid, text, integer) FROM PUBLIC;
833
+ REVOKE ALL ON FUNCTION
834
+ opengeni_host_export.fail_host_export_batch(text, text, uuid, text, integer, integer) FROM PUBLIC;
835
+ REVOKE ALL ON FUNCTION
836
+ opengeni_host_export.host_export_claim_sidecars(text, text, uuid) FROM PUBLIC;
837
+ REVOKE ALL ON FUNCTION
838
+ opengeni_host_export.host_export_consumer_status_sidecar(text, text) FROM PUBLIC;
839
+ REVOKE ALL ON FUNCTION
840
+ opengeni_host_export.dead_letter_host_export_head(text, text, uuid, bigint, text) FROM PUBLIC;
841
+
842
+ -- Preserve existing host-configured exporter grants for the new sidecar
843
+ -- helper without guessing a role name. The published claim function remains
844
+ -- the ACL authority and is otherwise unchanged.
845
+ DO $migration$
846
+ DECLARE v_role name;
847
+ BEGIN
848
+ FOR v_role IN
849
+ SELECT grantee.rolname
850
+ FROM pg_catalog.pg_proc proc
851
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
852
+ coalesce(proc.proacl, pg_catalog.acldefault('f', proc.proowner))
853
+ ) privilege
854
+ JOIN pg_catalog.pg_roles grantee ON grantee.oid = privilege.grantee
855
+ WHERE proc.oid =
856
+ 'opengeni_host_export.claim_host_export_batch(text, text, uuid, text, integer, integer, integer)'::regprocedure
857
+ AND privilege.grantee <> proc.proowner
858
+ AND privilege.privilege_type = 'EXECUTE'
859
+ LOOP
860
+ EXECUTE format(
861
+ 'GRANT EXECUTE ON FUNCTION opengeni_host_export.host_export_claim_sidecars(text, text, uuid) TO %I',
862
+ v_role
863
+ );
864
+ END LOOP;
865
+ END $migration$;
866
+
867
+ -- Preserve the old failure-settlement ACL on the new codec-aware overload.
868
+ DO $migration$
869
+ DECLARE v_role name;
870
+ BEGIN
871
+ FOR v_role IN
872
+ SELECT grantee.rolname
873
+ FROM pg_catalog.pg_proc proc
874
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
875
+ coalesce(proc.proacl, pg_catalog.acldefault('f', proc.proowner))
876
+ ) privilege
877
+ JOIN pg_catalog.pg_roles grantee ON grantee.oid = privilege.grantee
878
+ WHERE proc.oid =
879
+ 'opengeni_host_export.fail_host_export_batch(text, text, uuid, text, integer)'::regprocedure
880
+ AND privilege.grantee <> proc.proowner
881
+ AND privilege.privilege_type = 'EXECUTE'
882
+ LOOP
883
+ EXECUTE format(
884
+ 'GRANT EXECUTE ON FUNCTION opengeni_host_export.fail_host_export_batch(text, text, uuid, text, integer, integer) TO %I',
885
+ v_role
886
+ );
887
+ END LOOP;
888
+ END $migration$;
889
+
890
+ -- Preserve the published consumer-status ACL on its out-of-band companion.
891
+ DO $migration$
892
+ DECLARE v_role name;
893
+ BEGIN
894
+ FOR v_role IN
895
+ SELECT grantee.rolname
896
+ FROM pg_catalog.pg_proc proc
897
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
898
+ coalesce(proc.proacl, pg_catalog.acldefault('f', proc.proowner))
899
+ ) privilege
900
+ JOIN pg_catalog.pg_roles grantee ON grantee.oid = privilege.grantee
901
+ WHERE proc.oid =
902
+ 'opengeni_host_export.host_export_consumer_status(text, text)'::regprocedure
903
+ AND privilege.grantee <> proc.proowner
904
+ AND privilege.privilege_type = 'EXECUTE'
905
+ LOOP
906
+ EXECUTE format(
907
+ 'GRANT EXECUTE ON FUNCTION opengeni_host_export.host_export_consumer_status_sidecar(text, text) TO %I',
908
+ v_role
909
+ );
910
+ END LOOP;
911
+ END $migration$;
912
+
913
+ DROP TRIGGER IF EXISTS session_events_bound_payload_before_insert ON session_events;
914
+ DROP FUNCTION IF EXISTS opengeni_private.bound_session_event_payload();
915
+
916
+ -- Retain a bounded, explicit monitoring projection without mutating the
917
+ -- canonical row. Unlike the retired write guard, this read helper accurately
918
+ -- reports that authorized callers can request the full retained payload.
919
+ CREATE OR REPLACE FUNCTION opengeni_private.project_session_event_payload(source_payload jsonb)
920
+ RETURNS jsonb
921
+ LANGUAGE sql
922
+ IMMUTABLE
923
+ SET search_path = pg_catalog
924
+ AS $function$
925
+ SELECT CASE
926
+ WHEN octet_length(source_payload::text) <= 65536 THEN source_payload
927
+ ELSE jsonb_build_object(
928
+ '_monitoring', jsonb_build_object(
929
+ 'payloadMode', 'summary',
930
+ 'payloadTruncated', true,
931
+ 'originalBytes', octet_length(source_payload::text),
932
+ 'fullPayload', 'request payloadMode=full explicitly'
933
+ ),
934
+ 'preview', left(source_payload::text, 2048)
935
+ )
936
+ END
937
+ $function$;
938
+
939
+ ALTER TABLE session_events
940
+ DROP CONSTRAINT IF EXISTS session_events_payload_bytes_check;
941
+
942
+ ALTER TABLE session_realtime_entries
943
+ DROP CONSTRAINT IF EXISTS session_realtime_entries_text_check,
944
+ DROP CONSTRAINT IF EXISTS session_realtime_entries_payload_check;
945
+
946
+ COMMENT ON COLUMN session_events.payload IS
947
+ 'Canonical lossless event payload. Explicit read/transport projections apply their own byte bounds.';
948
+ COMMENT ON COLUMN session_history_items.item IS
949
+ 'Canonical model/history item. PostgreSQL-unsafe string code units use a reversible per-string encoding that leaves structural JSON keys queryable.';
950
+ COMMENT ON COLUMN rig_changes.payload IS
951
+ 'Canonical lossless rig-change payload; public summaries are separate projections.';
952
+ COMMENT ON COLUMN rig_changes.verification IS
953
+ 'Canonical lossless rig-verification result; public summaries are separate projections.';
954
+ COMMENT ON COLUMN session_pending_tool_calls.call_item IS
955
+ 'Canonical pending SDK call item; structural JSON keys remain SQL-queryable.';
956
+ COMMENT ON COLUMN session_pending_tool_calls.result_item IS
957
+ 'Canonical pending SDK result item; structural JSON keys remain SQL-queryable.';
958
+ COMMENT ON COLUMN session_system_updates.payload IS
959
+ 'Canonical machine-input payload; public monitoring projections are separate.';
960
+ COMMENT ON COLUMN host_export_outbox.payload IS
961
+ 'Original storage payload. Session-event rows carry exact codec truth in payload_codec_version; usage rows remain ordinary JSON with NULL codec version.';
962
+ COMMENT ON COLUMN host_export_outbox.payload_codec_version IS
963
+ 'Out-of-band codec truth for the complete outbox payload. NULL is literal legacy or ordinary usage JSON; 1 means the whole payload used the application lossless codec.';
964
+ COMMENT ON COLUMN host_export_consumers.last_error IS
965
+ 'Exact internal host-sink failure storage. Public logs use a separate fixed structural projection.';
966
+ COMMENT ON COLUMN host_export_consumers.last_error_codec_version IS
967
+ 'Out-of-band codec truth for the complete last_error value. NULL is literal old-writer data; 1 means the application encoded the whole value.';
968
+ COMMENT ON COLUMN host_export_dead_letters.envelope IS
969
+ 'Original mixed dead-letter envelope. SQL-built envelopes remain literal with NULL envelope_codec_version; nested session-event payload codec truth is separate.';
970
+ COMMENT ON COLUMN host_export_dead_letters.event_payload_codec_version IS
971
+ 'Out-of-band codec truth only for envelope.event.payload. It never implies that the surrounding mixed envelope is encoded.';
972
+ COMMENT ON COLUMN knowledge_memories.text IS
973
+ 'Canonical exact memory text; PostgreSQL-unsafe code units use the application text codec.';
974
+ COMMENT ON COLUMN audit_events.metadata IS
975
+ 'Canonical audit metadata; value-bearing secret audit fields remain prohibited by their domain contract.';