@opengeni/db 0.18.1 → 0.21.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-NK2A36KP.js +5822 -0
  2. package/dist/chunk-NK2A36KP.js.map +1 -0
  3. package/dist/{chunk-T6RSJT6C.js → chunk-OTJHD33E.js} +109 -1
  4. package/dist/chunk-OTJHD33E.js.map +1 -0
  5. package/dist/index.d.ts +25 -1
  6. package/dist/index.js +5761 -2827
  7. package/dist/index.js.map +1 -1
  8. package/dist/memory-domain.d.ts +110 -1
  9. package/dist/memory-governance-schema.d.ts +562 -0
  10. package/dist/memory-governance.d.ts +41 -0
  11. package/dist/provision-roles.js +1 -1
  12. package/dist/runtime-posture.d.ts +3 -3
  13. package/dist/schema.d.ts +425 -0
  14. package/dist/schema.js +37 -1
  15. package/dist/scoped-knowledge-schema.d.ts +5341 -0
  16. package/dist/scoped-knowledge.d.ts +208 -0
  17. package/dist/session-queue-commands.d.ts +2 -1
  18. package/drizzle/0152_hierarchical_memory_foundation.sql +1589 -0
  19. package/drizzle/0153_mcp_personal_connection_delegations.sql +181 -0
  20. package/drizzle/0154_scoped_knowledge_foundation.sql +2292 -0
  21. package/package.json +3 -3
  22. package/src/connection-token-resolver.ts +3 -0
  23. package/src/index.ts +351 -12
  24. package/src/memory-domain.ts +360 -1
  25. package/src/memory-governance-schema.ts +162 -0
  26. package/src/memory-governance.ts +317 -0
  27. package/src/provision-roles.ts +72 -0
  28. package/src/runtime-posture.ts +36 -0
  29. package/src/schema.ts +87 -4
  30. package/src/scoped-knowledge-schema.ts +603 -0
  31. package/src/scoped-knowledge.ts +2891 -0
  32. package/src/session-queue-commands.ts +48 -0
  33. package/dist/chunk-T6RSJT6C.js.map +0 -1
  34. package/dist/chunk-YKWJ7QJ2.js +0 -5000
  35. package/dist/chunk-YKWJ7QJ2.js.map +0 -1
@@ -0,0 +1,1589 @@
1
+ -- deployment-mode: maintenance
2
+ -- First slice: typed/composable memory scope, namespace/labels, typed
3
+ -- relationships, and immutable apply/revert evidence. Old memory readers and
4
+ -- writers must not overlap this cutover because they ignore typed selectors and
5
+ -- recognize only the V1 workspace-wide dedup constraint.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '30min';
9
+
10
+ DO $maintenance_preflight_guard$
11
+ BEGIN
12
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
13
+ AND EXISTS (
14
+ SELECT 1
15
+ FROM pg_stat_activity
16
+ WHERE datname = current_database()
17
+ AND usename = 'opengeni_app'
18
+ AND pid <> pg_backend_pid()
19
+ )
20
+ THEN
21
+ RAISE EXCEPTION
22
+ 'hierarchical memory activation requires all opengeni_app sessions to be stopped'
23
+ USING ERRCODE = '55000';
24
+ END IF;
25
+ END
26
+ $maintenance_preflight_guard$;
27
+
28
+ LOCK TABLE knowledge_memories IN ACCESS EXCLUSIVE MODE;
29
+ LOCK TABLE sessions IN SHARE MODE;
30
+ LOCK TABLE session_turns IN SHARE MODE;
31
+ LOCK TABLE session_turn_attempts IN SHARE MODE;
32
+ LOCK TABLE session_attempt_interruptions IN SHARE MODE;
33
+
34
+ DO $maintenance_guard$
35
+ BEGIN
36
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
37
+ AND EXISTS (
38
+ SELECT 1
39
+ FROM pg_stat_activity
40
+ WHERE datname = current_database()
41
+ AND usename = 'opengeni_app'
42
+ AND pid <> pg_backend_pid()
43
+ )
44
+ THEN
45
+ RAISE EXCEPTION
46
+ 'hierarchical memory activation requires all opengeni_app sessions to be stopped'
47
+ USING ERRCODE = '55000';
48
+ END IF;
49
+ END
50
+ $maintenance_guard$;
51
+
52
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS scope_type text;
53
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS scope_subject_id text;
54
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS scope_role_key text;
55
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS scope_session_id uuid;
56
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS namespace_key text NOT NULL DEFAULT 'general';
57
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS labels text[] NOT NULL DEFAULT '{}'::text[];
58
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS memory_version integer NOT NULL DEFAULT 1;
59
+ ALTER TABLE knowledge_memories ADD COLUMN IF NOT EXISTS created_by_kind text NOT NULL DEFAULT 'service';
60
+ ALTER TABLE knowledge_memories
61
+ ADD COLUMN IF NOT EXISTS created_by_subject_id text NOT NULL DEFAULT 'unattributed-legacy';
62
+ ALTER TABLE knowledge_memories
63
+ ADD COLUMN IF NOT EXISTS created_by_context jsonb NOT NULL DEFAULT '{"backfill":true}'::jsonb;
64
+
65
+ -- Existing `workspace` rows retain their behavior. Every unknown historical
66
+ -- convention becomes audit-only `legacy`; it is never widened into workspace
67
+ -- visibility by inference.
68
+ UPDATE knowledge_memories
69
+ SET scope_type = CASE WHEN scope = 'workspace' THEN 'workspace' ELSE 'legacy' END
70
+ WHERE scope_type IS NULL;
71
+ ALTER TABLE knowledge_memories ALTER COLUMN scope_type SET DEFAULT 'workspace';
72
+ ALTER TABLE knowledge_memories ALTER COLUMN scope_type SET NOT NULL;
73
+
74
+ -- Migration 0041 installed a global creator FK. Replace it with a workspace-
75
+ -- qualified provenance fence; never attach a valid foreign-workspace session.
76
+ DO $creator_fk$
77
+ DECLARE legacy_constraint_name text;
78
+ BEGIN
79
+ FOR legacy_constraint_name IN
80
+ SELECT constraint_row.conname
81
+ FROM pg_constraint AS constraint_row
82
+ JOIN pg_attribute AS local_column
83
+ ON local_column.attrelid = constraint_row.conrelid
84
+ AND local_column.attnum = constraint_row.conkey[1]
85
+ WHERE constraint_row.conrelid = 'knowledge_memories'::regclass
86
+ AND constraint_row.contype = 'f'
87
+ AND cardinality(constraint_row.conkey) = 1
88
+ AND local_column.attname = 'created_by_session_id'
89
+ LOOP
90
+ EXECUTE format('ALTER TABLE knowledge_memories DROP CONSTRAINT %I', legacy_constraint_name);
91
+ END LOOP;
92
+
93
+ IF NOT EXISTS (
94
+ SELECT 1 FROM pg_constraint
95
+ WHERE conname = 'knowledge_memories_created_by_workspace_session_fk'
96
+ AND conrelid = 'knowledge_memories'::regclass
97
+ ) THEN
98
+ ALTER TABLE knowledge_memories
99
+ ADD CONSTRAINT knowledge_memories_created_by_workspace_session_fk
100
+ FOREIGN KEY (workspace_id, created_by_session_id)
101
+ REFERENCES sessions(workspace_id, id)
102
+ ON DELETE SET NULL (created_by_session_id);
103
+ END IF;
104
+ END
105
+ $creator_fk$;
106
+
107
+ CREATE OR REPLACE FUNCTION opengeni_private.memory_labels_valid(value text[])
108
+ RETURNS boolean
109
+ LANGUAGE sql
110
+ IMMUTABLE
111
+ AS $$
112
+ SELECT coalesce(cardinality(value), 0) <= 16
113
+ AND NOT EXISTS (
114
+ SELECT 1 FROM unnest(value) AS label
115
+ WHERE label IS NULL
116
+ OR length(label) > 64
117
+ OR label !~ '^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$'
118
+ )
119
+ AND value = ARRAY(SELECT label FROM unnest(value) AS label ORDER BY label)
120
+ AND cardinality(value) = cardinality(ARRAY(SELECT DISTINCT label FROM unnest(value) AS label));
121
+ $$;
122
+
123
+ DO $memory_constraints$
124
+ BEGIN
125
+ IF NOT EXISTS (
126
+ SELECT 1 FROM pg_constraint
127
+ WHERE conname = 'knowledge_memories_scope_selector_chk'
128
+ AND conrelid = 'knowledge_memories'::regclass
129
+ ) THEN
130
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_scope_selector_chk CHECK (
131
+ (scope_type = 'workspace'
132
+ AND scope_subject_id IS NULL AND scope_role_key IS NULL AND scope_session_id IS NULL)
133
+ OR (scope_type = 'user'
134
+ AND length(btrim(scope_subject_id)) BETWEEN 1 AND 1024
135
+ AND scope_role_key IS NULL AND scope_session_id IS NULL)
136
+ OR (scope_type = 'role'
137
+ AND scope_subject_id IS NULL
138
+ AND scope_role_key ~ '^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$'
139
+ AND scope_session_id IS NULL)
140
+ OR (scope_type = 'session'
141
+ AND scope_subject_id IS NULL AND scope_role_key IS NULL AND scope_session_id IS NOT NULL)
142
+ OR (scope_type = 'ephemeral'
143
+ AND scope_subject_id IS NULL AND scope_role_key IS NULL
144
+ AND scope_session_id IS NOT NULL AND valid_until IS NOT NULL)
145
+ OR (scope_type = 'legacy'
146
+ AND scope_subject_id IS NULL AND scope_role_key IS NULL AND scope_session_id IS NULL)
147
+ );
148
+ END IF;
149
+
150
+ IF NOT EXISTS (
151
+ SELECT 1 FROM pg_constraint
152
+ WHERE conname = 'knowledge_memories_valid_window_chk'
153
+ AND conrelid = 'knowledge_memories'::regclass
154
+ ) THEN
155
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_valid_window_chk
156
+ CHECK (valid_until IS NULL OR valid_from <= valid_until);
157
+ END IF;
158
+
159
+ IF NOT EXISTS (
160
+ SELECT 1 FROM pg_constraint
161
+ WHERE conname = 'knowledge_memories_namespace_chk'
162
+ AND conrelid = 'knowledge_memories'::regclass
163
+ ) THEN
164
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_namespace_chk CHECK (
165
+ length(namespace_key) BETWEEN 1 AND 128
166
+ AND namespace_key ~ '^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?(?:/[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?)*$'
167
+ );
168
+ END IF;
169
+
170
+ IF NOT EXISTS (
171
+ SELECT 1 FROM pg_constraint
172
+ WHERE conname = 'knowledge_memories_labels_chk'
173
+ AND conrelid = 'knowledge_memories'::regclass
174
+ ) THEN
175
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_labels_chk
176
+ CHECK (opengeni_private.memory_labels_valid(labels));
177
+ END IF;
178
+
179
+ IF NOT EXISTS (
180
+ SELECT 1 FROM pg_constraint
181
+ WHERE conname = 'knowledge_memories_version_chk'
182
+ AND conrelid = 'knowledge_memories'::regclass
183
+ ) THEN
184
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_version_chk
185
+ CHECK (memory_version > 0);
186
+ END IF;
187
+
188
+ IF NOT EXISTS (
189
+ SELECT 1 FROM pg_constraint
190
+ WHERE conname = 'knowledge_memories_creator_chk'
191
+ AND conrelid = 'knowledge_memories'::regclass
192
+ ) THEN
193
+ ALTER TABLE knowledge_memories ADD CONSTRAINT knowledge_memories_creator_chk CHECK (
194
+ created_by_kind IN ('subject', 'service')
195
+ AND length(btrim(created_by_subject_id)) BETWEEN 1 AND 1024
196
+ AND jsonb_typeof(created_by_context) = 'object'
197
+ AND created_by_context - ARRAY[
198
+ 'backfill', 'legacyWriter', 'sessionId', 'turnId', 'attemptId',
199
+ 'executionGeneration'
200
+ ]::text[] = '{}'::jsonb
201
+ );
202
+ END IF;
203
+
204
+ IF NOT EXISTS (
205
+ SELECT 1 FROM pg_constraint
206
+ WHERE conname = 'knowledge_memories_scope_session_fk'
207
+ AND conrelid = 'knowledge_memories'::regclass
208
+ ) THEN
209
+ ALTER TABLE knowledge_memories
210
+ ADD CONSTRAINT knowledge_memories_scope_session_fk
211
+ FOREIGN KEY (workspace_id, scope_session_id)
212
+ REFERENCES sessions(workspace_id, id) ON DELETE CASCADE;
213
+ END IF;
214
+ END
215
+ $memory_constraints$;
216
+
217
+ CREATE UNIQUE INDEX IF NOT EXISTS knowledge_memories_workspace_id_uq
218
+ ON knowledge_memories(workspace_id, id);
219
+ CREATE INDEX IF NOT EXISTS knowledge_memories_workspace_typed_scope_idx
220
+ ON knowledge_memories(workspace_id, scope_type, scope_subject_id, scope_role_key, scope_session_id);
221
+ CREATE INDEX IF NOT EXISTS knowledge_memories_workspace_namespace_idx
222
+ ON knowledge_memories(workspace_id, namespace_key, updated_at DESC, id);
223
+ CREATE INDEX IF NOT EXISTS knowledge_memories_labels_idx
224
+ ON knowledge_memories USING gin(labels);
225
+
226
+ -- Exact dedup is local to the complete typed target. A user's private fact must
227
+ -- not collide with a different user, role, or session carrying the same text.
228
+ DROP INDEX IF EXISTS knowledge_memories_workspace_visible_text_hash_uq;
229
+ CREATE UNIQUE INDEX IF NOT EXISTS knowledge_memories_scope_visible_text_hash_uq
230
+ ON knowledge_memories(
231
+ workspace_id, scope_type, scope_subject_id, scope_role_key, scope_session_id,
232
+ namespace_key, text_hash
233
+ ) NULLS NOT DISTINCT
234
+ WHERE status IN ('active', 'approved') AND text_hash IS NOT NULL;
235
+
236
+ CREATE OR REPLACE FUNCTION opengeni_private.knowledge_memory_row_guard()
237
+ RETURNS trigger
238
+ LANGUAGE plpgsql
239
+ AS $$
240
+ DECLARE
241
+ typed_change boolean;
242
+ lifecycle_writer boolean;
243
+ BEGIN
244
+ IF TG_OP = 'INSERT' THEN
245
+ IF NEW.scope_type IS NULL
246
+ OR (NEW.scope_type = 'workspace' AND NEW.scope <> 'workspace'
247
+ AND NEW.scope_subject_id IS NULL AND NEW.scope_role_key IS NULL
248
+ AND NEW.scope_session_id IS NULL)
249
+ THEN
250
+ NEW.scope_type := CASE WHEN NEW.scope = 'workspace' THEN 'workspace' ELSE 'legacy' END;
251
+ NEW.scope_subject_id := NULL;
252
+ NEW.scope_role_key := NULL;
253
+ NEW.scope_session_id := NULL;
254
+ END IF;
255
+ IF opengeni_private.current_memory_actor_kind() IN ('subject', 'service')
256
+ AND length(btrim(opengeni_private.current_memory_actor_id())) BETWEEN 1 AND 1024
257
+ THEN
258
+ NEW.created_by_kind := opengeni_private.current_memory_actor_kind();
259
+ NEW.created_by_subject_id := opengeni_private.current_memory_actor_id();
260
+ NEW.created_by_context := jsonb_strip_nulls(jsonb_build_object(
261
+ 'sessionId', opengeni_private.current_memory_session_id()
262
+ ));
263
+ ELSE
264
+ NEW.created_by_kind := 'service';
265
+ NEW.created_by_subject_id := 'unattributed-legacy';
266
+ NEW.created_by_context := '{"legacyWriter":true}'::jsonb;
267
+ END IF;
268
+ NEW.memory_version := 1;
269
+ RETURN NEW;
270
+ END IF;
271
+
272
+ IF ROW(
273
+ NEW.created_by_kind, NEW.created_by_subject_id, NEW.created_by_context, NEW.created_at
274
+ ) IS DISTINCT FROM ROW(
275
+ OLD.created_by_kind, OLD.created_by_subject_id, OLD.created_by_context, OLD.created_at
276
+ )
277
+ OR (NEW.created_by_session_id IS DISTINCT FROM OLD.created_by_session_id
278
+ AND (NEW.created_by_session_id IS NOT NULL OR pg_trigger_depth() <= 1))
279
+ THEN
280
+ RAISE EXCEPTION 'knowledge memory creator authority is immutable'
281
+ USING ERRCODE = '55000';
282
+ END IF;
283
+
284
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
285
+ INTO lifecycle_writer
286
+ FROM pg_proc procedure_row
287
+ WHERE procedure_row.oid = to_regprocedure(format(
288
+ '%I.knowledge_memory_apply_operation(jsonb,text,text,text,uuid,uuid,uuid,integer)',
289
+ TG_TABLE_SCHEMA
290
+ ));
291
+ lifecycle_writer := coalesce(lifecycle_writer, false)
292
+ AND nullif(current_setting('opengeni.memory_lifecycle_operation', true), '') = '1';
293
+
294
+ IF NEW.scope IS DISTINCT FROM OLD.scope
295
+ AND ROW(NEW.scope_type, NEW.scope_subject_id, NEW.scope_role_key, NEW.scope_session_id)
296
+ IS NOT DISTINCT FROM ROW(OLD.scope_type, OLD.scope_subject_id, OLD.scope_role_key, OLD.scope_session_id)
297
+ AND NOT lifecycle_writer
298
+ THEN
299
+ NEW.scope_type := CASE WHEN NEW.scope = 'workspace' THEN 'workspace' ELSE 'legacy' END;
300
+ NEW.scope_subject_id := NULL;
301
+ NEW.scope_role_key := NULL;
302
+ NEW.scope_session_id := NULL;
303
+ END IF;
304
+
305
+ IF ROW(
306
+ NEW.scope_type, NEW.scope_subject_id, NEW.scope_role_key,
307
+ NEW.scope_session_id, NEW.namespace_key, NEW.labels
308
+ ) IS DISTINCT FROM ROW(
309
+ OLD.scope_type, OLD.scope_subject_id, OLD.scope_role_key,
310
+ OLD.scope_session_id, OLD.namespace_key, OLD.labels
311
+ ) AND NOT lifecycle_writer
312
+ THEN
313
+ RAISE EXCEPTION 'typed memory governance fields mutate only through lifecycle operations'
314
+ USING ERRCODE = '55000';
315
+ END IF;
316
+
317
+ typed_change := ROW(
318
+ NEW.status, NEW.scope_type, NEW.scope_subject_id, NEW.scope_role_key,
319
+ NEW.scope_session_id, NEW.namespace_key, NEW.labels, NEW.valid_from,
320
+ NEW.valid_until, NEW.supersedes_id, NEW.superseded_by_id
321
+ ) IS DISTINCT FROM ROW(
322
+ OLD.status, OLD.scope_type, OLD.scope_subject_id, OLD.scope_role_key,
323
+ OLD.scope_session_id, OLD.namespace_key, OLD.labels, OLD.valid_from,
324
+ OLD.valid_until, OLD.supersedes_id, OLD.superseded_by_id
325
+ );
326
+ NEW.memory_version := OLD.memory_version + CASE WHEN typed_change THEN 1 ELSE 0 END;
327
+ RETURN NEW;
328
+ END;
329
+ $$;
330
+
331
+ DROP TRIGGER IF EXISTS knowledge_memories_row_guard ON knowledge_memories;
332
+ CREATE TRIGGER knowledge_memories_row_guard
333
+ BEFORE INSERT OR UPDATE ON knowledge_memories
334
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.knowledge_memory_row_guard();
335
+
336
+ CREATE TABLE knowledge_memory_lifecycle_events (
337
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
338
+ account_id uuid NOT NULL,
339
+ workspace_id uuid NOT NULL,
340
+ operation_id uuid NOT NULL,
341
+ action text NOT NULL,
342
+ operation_type text NOT NULL,
343
+ target_memory_id uuid NOT NULL,
344
+ related_memory_id uuid,
345
+ relationship_id uuid,
346
+ relationship_type text,
347
+ actor_kind text NOT NULL,
348
+ actor_subject_id text NOT NULL,
349
+ actor_session_id uuid,
350
+ actor_turn_id uuid,
351
+ actor_attempt_id uuid,
352
+ actor_execution_generation integer,
353
+ plan_hash text NOT NULL,
354
+ before_state jsonb NOT NULL,
355
+ after_state jsonb NOT NULL,
356
+ reverts_event_id uuid,
357
+ created_at timestamptz NOT NULL DEFAULT transaction_timestamp(),
358
+ CONSTRAINT knowledge_memory_lifecycle_events_workspace_account_fk
359
+ FOREIGN KEY (workspace_id, account_id)
360
+ REFERENCES workspaces(id, account_id) ON DELETE RESTRICT,
361
+ CONSTRAINT knowledge_memory_lifecycle_events_target_fk
362
+ FOREIGN KEY (workspace_id, target_memory_id)
363
+ REFERENCES knowledge_memories(workspace_id, id) ON DELETE RESTRICT,
364
+ CONSTRAINT knowledge_memory_lifecycle_events_related_fk
365
+ FOREIGN KEY (workspace_id, related_memory_id)
366
+ REFERENCES knowledge_memories(workspace_id, id) ON DELETE RESTRICT,
367
+ CONSTRAINT knowledge_memory_lifecycle_events_actor_session_fk
368
+ FOREIGN KEY (workspace_id, actor_session_id)
369
+ REFERENCES sessions(workspace_id, id) ON DELETE RESTRICT,
370
+ CONSTRAINT knowledge_memory_lifecycle_events_actor_turn_fk
371
+ FOREIGN KEY (workspace_id, actor_turn_id)
372
+ REFERENCES session_turns(workspace_id, id) ON DELETE RESTRICT,
373
+ CONSTRAINT knowledge_memory_lifecycle_events_actor_attempt_fk
374
+ FOREIGN KEY (workspace_id, actor_attempt_id)
375
+ REFERENCES session_turn_attempts(workspace_id, id) ON DELETE RESTRICT,
376
+ CONSTRAINT knowledge_memory_lifecycle_events_reverts_fk
377
+ FOREIGN KEY (workspace_id, reverts_event_id)
378
+ REFERENCES knowledge_memory_lifecycle_events(workspace_id, id) ON DELETE RESTRICT
379
+ DEFERRABLE INITIALLY DEFERRED,
380
+ CONSTRAINT knowledge_memory_lifecycle_events_action_chk
381
+ CHECK (action IN ('apply', 'revert')),
382
+ CONSTRAINT knowledge_memory_lifecycle_events_operation_chk
383
+ CHECK (operation_type IN (
384
+ 'reclassify', 'archive', 'relationship_add', 'relationship_remove',
385
+ 'supersede', 'correct'
386
+ )),
387
+ CONSTRAINT knowledge_memory_lifecycle_events_relationship_chk CHECK (
388
+ (operation_type IN ('relationship_add', 'relationship_remove', 'supersede', 'correct')
389
+ AND related_memory_id IS NOT NULL AND relationship_id IS NOT NULL
390
+ AND relationship_type IS NOT NULL)
391
+ OR (operation_type IN ('reclassify', 'archive')
392
+ AND related_memory_id IS NULL AND relationship_id IS NULL
393
+ AND relationship_type IS NULL)
394
+ ),
395
+ CONSTRAINT knowledge_memory_lifecycle_events_actor_chk CHECK (
396
+ actor_kind IN ('subject', 'service')
397
+ AND length(btrim(actor_subject_id)) BETWEEN 1 AND 1024
398
+ ),
399
+ CONSTRAINT knowledge_memory_lifecycle_events_attempt_shape_chk CHECK (
400
+ (actor_session_id IS NULL AND actor_turn_id IS NULL AND actor_attempt_id IS NULL
401
+ AND actor_execution_generation IS NULL)
402
+ OR (actor_session_id IS NOT NULL AND actor_turn_id IS NOT NULL AND actor_attempt_id IS NOT NULL
403
+ AND actor_execution_generation > 0)
404
+ ),
405
+ CONSTRAINT knowledge_memory_lifecycle_events_plan_hash_chk
406
+ CHECK (plan_hash ~ '^[a-f0-9]{64}$'),
407
+ CONSTRAINT knowledge_memory_lifecycle_events_state_chk CHECK (
408
+ jsonb_typeof(before_state) = 'object' AND jsonb_typeof(after_state) = 'object'
409
+ AND NOT (before_state ?| ARRAY['text', 'sourceRefs', 'source_refs', 'metadata', 'embedding'])
410
+ AND NOT (after_state ?| ARRAY['text', 'sourceRefs', 'source_refs', 'metadata', 'embedding'])
411
+ ),
412
+ CONSTRAINT knowledge_memory_lifecycle_events_revert_shape_chk CHECK (
413
+ (action = 'apply' AND reverts_event_id IS NULL)
414
+ OR (action = 'revert' AND reverts_event_id IS NOT NULL)
415
+ ),
416
+ CONSTRAINT knowledge_memory_lifecycle_events_operation_action_uq
417
+ UNIQUE (workspace_id, operation_id, action),
418
+ CONSTRAINT knowledge_memory_lifecycle_events_workspace_id_uq
419
+ UNIQUE (workspace_id, id)
420
+ );
421
+
422
+ CREATE UNIQUE INDEX knowledge_memory_lifecycle_events_one_revert_uq
423
+ ON knowledge_memory_lifecycle_events(reverts_event_id)
424
+ WHERE reverts_event_id IS NOT NULL;
425
+ CREATE INDEX knowledge_memory_lifecycle_events_target_timeline_idx
426
+ ON knowledge_memory_lifecycle_events(workspace_id, target_memory_id, created_at DESC, id);
427
+ CREATE INDEX knowledge_memory_lifecycle_events_actor_timeline_idx
428
+ ON knowledge_memory_lifecycle_events(
429
+ workspace_id, actor_kind, actor_subject_id, created_at DESC, id
430
+ );
431
+
432
+ CREATE TABLE knowledge_memory_relationships (
433
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
434
+ account_id uuid NOT NULL,
435
+ workspace_id uuid NOT NULL,
436
+ source_memory_id uuid NOT NULL,
437
+ target_memory_id uuid NOT NULL,
438
+ relationship_type text NOT NULL,
439
+ version integer NOT NULL DEFAULT 1,
440
+ created_by_event_id uuid NOT NULL,
441
+ removed_by_event_id uuid,
442
+ created_at timestamptz NOT NULL DEFAULT transaction_timestamp(),
443
+ removed_at timestamptz,
444
+ CONSTRAINT knowledge_memory_relationships_workspace_account_fk
445
+ FOREIGN KEY (workspace_id, account_id)
446
+ REFERENCES workspaces(id, account_id) ON DELETE RESTRICT,
447
+ CONSTRAINT knowledge_memory_relationships_source_fk
448
+ FOREIGN KEY (workspace_id, source_memory_id)
449
+ REFERENCES knowledge_memories(workspace_id, id) ON DELETE RESTRICT,
450
+ CONSTRAINT knowledge_memory_relationships_target_fk
451
+ FOREIGN KEY (workspace_id, target_memory_id)
452
+ REFERENCES knowledge_memories(workspace_id, id) ON DELETE RESTRICT,
453
+ CONSTRAINT knowledge_memory_relationships_created_event_fk
454
+ FOREIGN KEY (workspace_id, created_by_event_id)
455
+ REFERENCES knowledge_memory_lifecycle_events(workspace_id, id) ON DELETE RESTRICT
456
+ DEFERRABLE INITIALLY DEFERRED,
457
+ CONSTRAINT knowledge_memory_relationships_removed_event_fk
458
+ FOREIGN KEY (workspace_id, removed_by_event_id)
459
+ REFERENCES knowledge_memory_lifecycle_events(workspace_id, id) ON DELETE RESTRICT
460
+ DEFERRABLE INITIALLY DEFERRED,
461
+ CONSTRAINT knowledge_memory_relationships_type_chk CHECK (
462
+ relationship_type IN (
463
+ 'derived_from', 'supersedes', 'corrects', 'conflicts_with',
464
+ 'related_to', 'depends_on', 'applies_to'
465
+ )
466
+ ),
467
+ CONSTRAINT knowledge_memory_relationships_distinct_chk
468
+ CHECK (source_memory_id <> target_memory_id),
469
+ CONSTRAINT knowledge_memory_relationships_version_chk CHECK (version > 0),
470
+ CONSTRAINT knowledge_memory_relationships_removed_shape_chk CHECK (
471
+ (removed_by_event_id IS NULL AND removed_at IS NULL)
472
+ OR (removed_by_event_id IS NOT NULL AND removed_at IS NOT NULL)
473
+ ),
474
+ CONSTRAINT knowledge_memory_relationships_workspace_id_uq
475
+ UNIQUE (workspace_id, id)
476
+ );
477
+
478
+ ALTER TABLE knowledge_memory_lifecycle_events
479
+ ADD CONSTRAINT knowledge_memory_lifecycle_events_relationship_fk
480
+ FOREIGN KEY (workspace_id, relationship_id)
481
+ REFERENCES knowledge_memory_relationships(workspace_id, id) ON DELETE RESTRICT
482
+ DEFERRABLE INITIALLY DEFERRED;
483
+
484
+ CREATE UNIQUE INDEX knowledge_memory_relationships_active_edge_uq
485
+ ON knowledge_memory_relationships(
486
+ workspace_id, source_memory_id, target_memory_id, relationship_type
487
+ ) WHERE removed_by_event_id IS NULL;
488
+ CREATE UNIQUE INDEX knowledge_memory_relationships_active_symmetric_edge_uq
489
+ ON knowledge_memory_relationships(
490
+ workspace_id, relationship_type,
491
+ LEAST(source_memory_id, target_memory_id),
492
+ GREATEST(source_memory_id, target_memory_id)
493
+ ) WHERE removed_by_event_id IS NULL
494
+ AND relationship_type IN ('conflicts_with', 'related_to');
495
+ CREATE INDEX knowledge_memory_relationships_source_idx
496
+ ON knowledge_memory_relationships(workspace_id, source_memory_id, created_at DESC, id);
497
+ CREATE INDEX knowledge_memory_relationships_target_idx
498
+ ON knowledge_memory_relationships(workspace_id, target_memory_id, created_at DESC, id);
499
+
500
+ CREATE OR REPLACE FUNCTION opengeni_private.immutable_memory_lifecycle_event()
501
+ RETURNS trigger
502
+ LANGUAGE plpgsql
503
+ AS $$
504
+ BEGIN
505
+ RAISE EXCEPTION 'knowledge memory lifecycle events are immutable'
506
+ USING ERRCODE = '55000';
507
+ END;
508
+ $$;
509
+
510
+ CREATE TRIGGER knowledge_memory_lifecycle_events_immutable
511
+ BEFORE UPDATE OR DELETE ON knowledge_memory_lifecycle_events
512
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.immutable_memory_lifecycle_event();
513
+
514
+ CREATE OR REPLACE FUNCTION opengeni_private.knowledge_memory_relationship_guard()
515
+ RETURNS trigger
516
+ LANGUAGE plpgsql
517
+ AS $$
518
+ DECLARE
519
+ lifecycle_writer boolean;
520
+ BEGIN
521
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
522
+ INTO lifecycle_writer
523
+ FROM pg_proc procedure_row
524
+ WHERE procedure_row.oid = to_regprocedure(format(
525
+ '%I.knowledge_memory_apply_operation(jsonb,text,text,text,uuid,uuid,uuid,integer)',
526
+ TG_TABLE_SCHEMA
527
+ ));
528
+ lifecycle_writer := coalesce(lifecycle_writer, false)
529
+ AND nullif(current_setting('opengeni.memory_lifecycle_operation', true), '') = '1';
530
+ IF NOT lifecycle_writer THEN
531
+ RAISE EXCEPTION 'knowledge memory relationships mutate only through lifecycle operations'
532
+ USING ERRCODE = '55000';
533
+ END IF;
534
+ IF TG_OP = 'DELETE' THEN
535
+ RAISE EXCEPTION 'knowledge memory relationships are retained and retired, never deleted'
536
+ USING ERRCODE = '55000';
537
+ END IF;
538
+ IF ROW(
539
+ NEW.id, NEW.account_id, NEW.workspace_id, NEW.source_memory_id,
540
+ NEW.target_memory_id, NEW.relationship_type, NEW.created_by_event_id, NEW.created_at
541
+ ) IS DISTINCT FROM ROW(
542
+ OLD.id, OLD.account_id, OLD.workspace_id, OLD.source_memory_id,
543
+ OLD.target_memory_id, OLD.relationship_type, OLD.created_by_event_id, OLD.created_at
544
+ ) THEN
545
+ RAISE EXCEPTION 'knowledge memory relationship identity is immutable'
546
+ USING ERRCODE = '55000';
547
+ END IF;
548
+ NEW.version := OLD.version + 1;
549
+ RETURN NEW;
550
+ END;
551
+ $$;
552
+
553
+ CREATE TRIGGER knowledge_memory_relationships_guard
554
+ BEFORE UPDATE OR DELETE ON knowledge_memory_relationships
555
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.knowledge_memory_relationship_guard();
556
+
557
+ CREATE OR REPLACE FUNCTION opengeni_private.current_memory_role_key()
558
+ RETURNS text LANGUAGE sql STABLE AS $$
559
+ SELECT nullif(current_setting('opengeni.memory_role_key', true), '');
560
+ $$;
561
+
562
+ CREATE OR REPLACE FUNCTION opengeni_private.current_memory_session_id()
563
+ RETURNS uuid LANGUAGE sql STABLE AS $$
564
+ SELECT nullif(current_setting('opengeni.memory_session_id', true), '')::uuid;
565
+ $$;
566
+
567
+ CREATE OR REPLACE FUNCTION opengeni_private.current_memory_actor_kind()
568
+ RETURNS text LANGUAGE sql STABLE AS $$
569
+ SELECT nullif(current_setting('opengeni.memory_actor_kind', true), '');
570
+ $$;
571
+
572
+ CREATE OR REPLACE FUNCTION opengeni_private.current_memory_actor_id()
573
+ RETURNS text LANGUAGE sql STABLE AS $$
574
+ SELECT nullif(current_setting('opengeni.memory_actor_id', true), '');
575
+ $$;
576
+
577
+ -- Stable JSON bytes must match packages/contracts stableJson(): recursively
578
+ -- sort object keys, preserve array order, and emit compact JSON. Recompute plan
579
+ -- hashes inside PostgreSQL so the runtime role cannot forge audit identity.
580
+ DO $plan_hash_functions$
581
+ DECLARE target_schema text := current_schema();
582
+ BEGIN
583
+ EXECUTE format($ddl$
584
+ CREATE OR REPLACE FUNCTION %1$I.knowledge_memory_stable_jsonb(p_value jsonb)
585
+ RETURNS text
586
+ LANGUAGE plpgsql IMMUTABLE STRICT SET search_path = pg_catalog
587
+ AS $body$
588
+ DECLARE rendered text;
589
+ BEGIN
590
+ CASE jsonb_typeof(p_value)
591
+ WHEN 'object' THEN
592
+ SELECT '{' || coalesce(string_agg(
593
+ to_jsonb(entry.key)::text || ':' || %1$I.knowledge_memory_stable_jsonb(entry.value),
594
+ ',' ORDER BY entry.key COLLATE "C"
595
+ ), '') || '}'
596
+ INTO rendered
597
+ FROM jsonb_each(p_value) AS entry;
598
+ WHEN 'array' THEN
599
+ SELECT '[' || coalesce(string_agg(
600
+ %1$I.knowledge_memory_stable_jsonb(entry.value),
601
+ ',' ORDER BY entry.ordinality
602
+ ), '') || ']'
603
+ INTO rendered
604
+ FROM jsonb_array_elements(p_value) WITH ORDINALITY AS entry(value, ordinality);
605
+ ELSE
606
+ rendered := p_value::text;
607
+ END CASE;
608
+ RETURN rendered;
609
+ END;
610
+ $body$;
611
+
612
+ CREATE OR REPLACE FUNCTION %1$I.knowledge_memory_plan_hash(p_value jsonb)
613
+ RETURNS text
614
+ LANGUAGE sql IMMUTABLE STRICT SET search_path = pg_catalog
615
+ AS $body$
616
+ SELECT encode(sha256(convert_to(
617
+ %1$I.knowledge_memory_stable_jsonb(p_value), 'UTF8'
618
+ )), 'hex');
619
+ $body$;
620
+ $ddl$, target_schema);
621
+ EXECUTE format(
622
+ 'REVOKE ALL ON FUNCTION %I.knowledge_memory_stable_jsonb(jsonb) FROM PUBLIC',
623
+ target_schema
624
+ );
625
+ EXECUTE format(
626
+ 'REVOKE ALL ON FUNCTION %I.knowledge_memory_plan_hash(jsonb) FROM PUBLIC',
627
+ target_schema
628
+ );
629
+ END
630
+ $plan_hash_functions$;
631
+
632
+ CREATE OR REPLACE FUNCTION opengeni_private.memory_scope_authorized(
633
+ row_scope_type text,
634
+ row_subject_id text,
635
+ row_role_key text,
636
+ row_session_id uuid
637
+ ) RETURNS boolean LANGUAGE sql STABLE AS $$
638
+ SELECT coalesce(CASE row_scope_type
639
+ WHEN 'workspace' THEN true
640
+ WHEN 'user' THEN row_subject_id = opengeni_private.current_subject_id()
641
+ WHEN 'role' THEN row_role_key = opengeni_private.current_memory_role_key()
642
+ WHEN 'session' THEN row_session_id = opengeni_private.current_memory_session_id()
643
+ WHEN 'ephemeral' THEN row_session_id = opengeni_private.current_memory_session_id()
644
+ WHEN 'legacy' THEN false
645
+ ELSE false
646
+ END, false);
647
+ $$;
648
+
649
+ CREATE OR REPLACE FUNCTION opengeni_private.memory_scope_visible(
650
+ row_scope_type text,
651
+ row_subject_id text,
652
+ row_role_key text,
653
+ row_session_id uuid,
654
+ row_valid_from timestamptz,
655
+ row_valid_until timestamptz
656
+ ) RETURNS boolean LANGUAGE sql STABLE AS $$
657
+ SELECT opengeni_private.memory_scope_authorized(
658
+ row_scope_type, row_subject_id, row_role_key, row_session_id
659
+ ) AND (
660
+ row_scope_type <> 'ephemeral'
661
+ OR (
662
+ row_valid_from <= transaction_timestamp()
663
+ AND row_valid_until > transaction_timestamp()
664
+ )
665
+ );
666
+ $$;
667
+
668
+ DROP POLICY IF EXISTS workspace_isolation ON knowledge_memories;
669
+ CREATE POLICY workspace_isolation ON knowledge_memories
670
+ USING (
671
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
672
+ AND opengeni_private.memory_scope_visible(
673
+ scope_type, scope_subject_id, scope_role_key, scope_session_id, valid_from, valid_until
674
+ )
675
+ )
676
+ WITH CHECK (
677
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
678
+ AND opengeni_private.memory_scope_visible(
679
+ scope_type, scope_subject_id, scope_role_key, scope_session_id, valid_from, valid_until
680
+ )
681
+ );
682
+
683
+ ALTER TABLE knowledge_memory_relationships ENABLE ROW LEVEL SECURITY;
684
+ ALTER TABLE knowledge_memory_relationships FORCE ROW LEVEL SECURITY;
685
+ CREATE POLICY workspace_isolation ON knowledge_memory_relationships
686
+ USING (
687
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
688
+ AND EXISTS (
689
+ SELECT 1 FROM knowledge_memories source
690
+ WHERE source.workspace_id = knowledge_memory_relationships.workspace_id
691
+ AND source.id = knowledge_memory_relationships.source_memory_id
692
+ )
693
+ AND EXISTS (
694
+ SELECT 1 FROM knowledge_memories target
695
+ WHERE target.workspace_id = knowledge_memory_relationships.workspace_id
696
+ AND target.id = knowledge_memory_relationships.target_memory_id
697
+ )
698
+ );
699
+
700
+ ALTER TABLE knowledge_memory_lifecycle_events ENABLE ROW LEVEL SECURITY;
701
+ ALTER TABLE knowledge_memory_lifecycle_events FORCE ROW LEVEL SECURITY;
702
+ CREATE POLICY workspace_isolation ON knowledge_memory_lifecycle_events
703
+ USING (
704
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
705
+ AND actor_kind = opengeni_private.current_memory_actor_kind()
706
+ AND actor_subject_id = opengeni_private.current_memory_actor_id()
707
+ );
708
+
709
+ DO $apply_function$
710
+ DECLARE target_schema text := current_schema();
711
+ BEGIN
712
+ EXECUTE format($ddl$
713
+ CREATE OR REPLACE FUNCTION %I.knowledge_memory_apply_operation(
714
+ p_plan jsonb,
715
+ p_plan_hash text,
716
+ p_actor_kind text,
717
+ p_actor_subject_id text,
718
+ p_actor_session_id uuid,
719
+ p_actor_turn_id uuid,
720
+ p_actor_attempt_id uuid,
721
+ p_actor_execution_generation integer
722
+ ) RETURNS TABLE (event_id uuid)
723
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %I, pg_catalog
724
+ AS $body$
725
+ DECLARE
726
+ context_account_id uuid := nullif(current_setting('opengeni.account_id', true), '')::uuid;
727
+ context_workspace_id uuid := nullif(current_setting('opengeni.workspace_id', true), '')::uuid;
728
+ v_operation_id uuid;
729
+ operation_type text;
730
+ target_id uuid;
731
+ related_id uuid;
732
+ expected_target_version integer;
733
+ expected_related_version integer;
734
+ v_relationship_type text;
735
+ relationship_id uuid;
736
+ new_scope jsonb;
737
+ new_scope_type text;
738
+ new_scope_subject_id text;
739
+ new_scope_role_key text;
740
+ new_scope_session_id uuid;
741
+ new_valid_until timestamptz;
742
+ new_namespace text;
743
+ new_labels text[];
744
+ target record;
745
+ related record;
746
+ relationship record;
747
+ before_state jsonb;
748
+ after_state jsonb;
749
+ new_event_id uuid := gen_random_uuid();
750
+ authority_role_key text;
751
+ existing_event record;
752
+ BEGIN
753
+ IF jsonb_typeof(p_plan) IS DISTINCT FROM 'object'
754
+ OR p_plan_hash !~ '^[a-f0-9]{64}$'
755
+ OR p_plan_hash IS DISTINCT FROM knowledge_memory_plan_hash(p_plan)
756
+ OR p_actor_kind NOT IN ('subject', 'service')
757
+ OR length(btrim(p_actor_subject_id)) NOT BETWEEN 1 AND 1024
758
+ THEN
759
+ RAISE EXCEPTION 'memory operation plan or actor is invalid' USING ERRCODE = '22023';
760
+ END IF;
761
+ IF context_account_id IS NULL OR context_workspace_id IS NULL
762
+ OR opengeni_private.current_memory_actor_kind() IS DISTINCT FROM p_actor_kind
763
+ OR opengeni_private.current_memory_actor_id() IS DISTINCT FROM p_actor_subject_id
764
+ OR (p_actor_kind = 'subject'
765
+ AND opengeni_private.current_subject_id() IS DISTINCT FROM p_actor_subject_id)
766
+ OR (p_actor_kind = 'service' AND opengeni_private.current_subject_id() IS NOT NULL)
767
+ THEN
768
+ RAISE EXCEPTION 'memory operation requires exact transaction-local actor authority'
769
+ USING ERRCODE = '42501';
770
+ END IF;
771
+
772
+ IF (p_actor_session_id IS NULL) <> (p_actor_turn_id IS NULL)
773
+ OR (p_actor_session_id IS NULL) <> (p_actor_attempt_id IS NULL)
774
+ OR (p_actor_session_id IS NULL) <> (p_actor_execution_generation IS NULL)
775
+ THEN
776
+ RAISE EXCEPTION 'memory operation attempt provenance is partial' USING ERRCODE = '22023';
777
+ END IF;
778
+ IF p_actor_session_id IS NOT NULL THEN
779
+ SELECT session.metadata ->> 'memoryRoleKey' INTO authority_role_key
780
+ FROM workspaces workspace
781
+ JOIN sessions session
782
+ ON session.account_id = workspace.account_id
783
+ AND session.workspace_id = workspace.id
784
+ JOIN session_turns turn
785
+ ON turn.account_id = session.account_id
786
+ AND turn.workspace_id = session.workspace_id
787
+ AND turn.session_id = session.id
788
+ JOIN session_turn_attempts attempt
789
+ ON attempt.account_id = turn.account_id
790
+ AND attempt.workspace_id = turn.workspace_id
791
+ AND attempt.session_id = turn.session_id
792
+ AND attempt.turn_id = turn.id
793
+ WHERE workspace.account_id = context_account_id
794
+ AND workspace.id = context_workspace_id
795
+ AND session.id = p_actor_session_id
796
+ AND session.active_turn_id = p_actor_turn_id
797
+ AND turn.id = p_actor_turn_id
798
+ AND turn.active_attempt_id = p_actor_attempt_id
799
+ AND turn.execution_generation = p_actor_execution_generation
800
+ AND turn.status IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
801
+ AND turn.initiator_kind = p_actor_kind
802
+ AND turn.initiator_subject_id = p_actor_subject_id
803
+ AND attempt.id = p_actor_attempt_id
804
+ AND attempt.execution_generation = p_actor_execution_generation
805
+ AND attempt.state IN ('claimed', 'running')
806
+ AND NOT EXISTS (
807
+ SELECT 1 FROM session_attempt_interruptions interruption
808
+ WHERE interruption.workspace_id = attempt.workspace_id
809
+ AND interruption.attempt_id = attempt.id
810
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
811
+ )
812
+ FOR KEY SHARE OF workspace
813
+ FOR SHARE OF session, turn
814
+ FOR UPDATE OF attempt;
815
+ IF NOT FOUND THEN
816
+ RAISE EXCEPTION 'memory operation attempt authority is stale or invalid'
817
+ USING ERRCODE = '42501';
818
+ END IF;
819
+ IF authority_role_key IS NOT NULL AND (
820
+ authority_role_key IS DISTINCT FROM lower(btrim(authority_role_key))
821
+ OR length(authority_role_key) NOT BETWEEN 1 AND 64
822
+ OR authority_role_key !~ '^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$'
823
+ ) THEN
824
+ RAISE EXCEPTION 'persisted memory role authority is invalid' USING ERRCODE = '42501';
825
+ END IF;
826
+ IF opengeni_private.current_memory_session_id() IS DISTINCT FROM p_actor_session_id
827
+ OR opengeni_private.current_memory_role_key() IS DISTINCT FROM authority_role_key
828
+ THEN
829
+ RAISE EXCEPTION 'memory operation session or role context is not exact'
830
+ USING ERRCODE = '42501';
831
+ END IF;
832
+ ELSIF opengeni_private.current_memory_session_id() IS NOT NULL
833
+ OR opengeni_private.current_memory_role_key() IS NOT NULL
834
+ THEN
835
+ RAISE EXCEPTION 'direct memory authority cannot carry session or role context'
836
+ USING ERRCODE = '42501';
837
+ END IF;
838
+ IF p_actor_session_id IS NOT NULL AND p_actor_execution_generation < 1 THEN
839
+ RAISE EXCEPTION 'memory operation attempt generation is invalid' USING ERRCODE = '22023';
840
+ END IF;
841
+ IF p_actor_session_id IS NULL AND (
842
+ p_actor_turn_id IS NOT NULL OR p_actor_attempt_id IS NOT NULL
843
+ OR p_actor_execution_generation IS NOT NULL
844
+ ) THEN
845
+ RAISE EXCEPTION 'memory operation direct authority has attempt provenance'
846
+ USING ERRCODE = '22023';
847
+ END IF;
848
+ IF p_actor_session_id IS NULL AND p_actor_kind = 'service'
849
+ AND opengeni_private.current_subject_id() IS NOT NULL
850
+ THEN
851
+ RAISE EXCEPTION 'service memory authority cannot carry human subject context'
852
+ USING ERRCODE = '42501';
853
+ END IF;
854
+ IF p_actor_session_id IS NULL AND p_actor_kind = 'subject'
855
+ AND opengeni_private.current_subject_id() IS DISTINCT FROM p_actor_subject_id
856
+ THEN
857
+ RAISE EXCEPTION 'subject memory authority is not exact' USING ERRCODE = '42501';
858
+ END IF;
859
+
860
+ v_operation_id := (p_plan->>'operationId')::uuid;
861
+ operation_type := p_plan->>'operationType';
862
+ target_id := (p_plan->>'targetMemoryId')::uuid;
863
+ expected_target_version := (p_plan->>'expectedTargetVersion')::integer;
864
+ IF operation_type NOT IN (
865
+ 'reclassify', 'archive', 'relationship_add', 'relationship_remove',
866
+ 'supersede', 'correct'
867
+ ) OR expected_target_version < 1 THEN
868
+ RAISE EXCEPTION 'memory operation type or target version is invalid'
869
+ USING ERRCODE = '22023';
870
+ END IF;
871
+
872
+ SELECT event.* INTO existing_event
873
+ FROM knowledge_memory_lifecycle_events event
874
+ WHERE event.workspace_id = context_workspace_id
875
+ AND event.operation_id = v_operation_id
876
+ AND event.action = 'apply';
877
+ IF FOUND THEN
878
+ IF existing_event.plan_hash IS DISTINCT FROM p_plan_hash
879
+ OR existing_event.actor_kind IS DISTINCT FROM p_actor_kind
880
+ OR existing_event.actor_subject_id IS DISTINCT FROM p_actor_subject_id
881
+ OR existing_event.actor_session_id IS DISTINCT FROM p_actor_session_id
882
+ OR existing_event.actor_turn_id IS DISTINCT FROM p_actor_turn_id
883
+ OR existing_event.actor_attempt_id IS DISTINCT FROM p_actor_attempt_id
884
+ OR existing_event.actor_execution_generation
885
+ IS DISTINCT FROM p_actor_execution_generation
886
+ THEN
887
+ RAISE EXCEPTION 'memory operation id already identifies different immutable evidence'
888
+ USING ERRCODE = '23505';
889
+ END IF;
890
+ RETURN QUERY SELECT existing_event.id;
891
+ RETURN;
892
+ END IF;
893
+
894
+ IF operation_type IN ('relationship_add', 'relationship_remove', 'supersede', 'correct') THEN
895
+ related_id := (p_plan->>'relatedMemoryId')::uuid;
896
+ expected_related_version := (p_plan->>'expectedRelatedVersion')::integer;
897
+ IF related_id IS NULL OR related_id = target_id OR expected_related_version < 1 THEN
898
+ RAISE EXCEPTION 'memory relationship operation requires a distinct related memory'
899
+ USING ERRCODE = '22023';
900
+ END IF;
901
+ PERFORM 1 FROM knowledge_memories candidate
902
+ WHERE candidate.id IN (target_id, related_id)
903
+ AND candidate.account_id = context_account_id
904
+ AND candidate.workspace_id = context_workspace_id
905
+ ORDER BY candidate.id FOR UPDATE;
906
+ ELSE
907
+ PERFORM 1 FROM knowledge_memories candidate
908
+ WHERE candidate.id = target_id
909
+ AND candidate.account_id = context_account_id
910
+ AND candidate.workspace_id = context_workspace_id
911
+ FOR UPDATE;
912
+ END IF;
913
+
914
+ SELECT * INTO target FROM knowledge_memories
915
+ WHERE account_id = context_account_id AND workspace_id = context_workspace_id AND id = target_id;
916
+ IF NOT FOUND OR NOT opengeni_private.memory_scope_visible(
917
+ target.scope_type, target.scope_subject_id, target.scope_role_key, target.scope_session_id,
918
+ target.valid_from, target.valid_until
919
+ ) THEN
920
+ RAISE EXCEPTION 'target memory is not visible to this authority' USING ERRCODE = '42501';
921
+ END IF;
922
+ IF target.memory_version <> expected_target_version THEN
923
+ RAISE EXCEPTION 'target memory version changed' USING ERRCODE = '40001';
924
+ END IF;
925
+
926
+ IF related_id IS NOT NULL THEN
927
+ SELECT * INTO related FROM knowledge_memories
928
+ WHERE account_id = context_account_id AND workspace_id = context_workspace_id AND id = related_id;
929
+ IF NOT FOUND OR NOT opengeni_private.memory_scope_visible(
930
+ related.scope_type, related.scope_subject_id, related.scope_role_key,
931
+ related.scope_session_id, related.valid_from, related.valid_until
932
+ ) THEN
933
+ RAISE EXCEPTION 'related memory is not visible to this authority' USING ERRCODE = '42501';
934
+ END IF;
935
+ IF related.memory_version <> expected_related_version THEN
936
+ RAISE EXCEPTION 'related memory version changed' USING ERRCODE = '40001';
937
+ END IF;
938
+ END IF;
939
+
940
+ before_state := jsonb_build_object(
941
+ 'memoryVersion', target.memory_version,
942
+ 'status', target.status,
943
+ 'scopeType', target.scope_type,
944
+ 'scopeSubjectId', target.scope_subject_id,
945
+ 'scopeRoleKey', target.scope_role_key,
946
+ 'scopeSessionId', target.scope_session_id,
947
+ 'namespace', target.namespace_key,
948
+ 'labels', to_jsonb(target.labels),
949
+ 'validUntil', target.valid_until,
950
+ 'supersedesId', target.supersedes_id,
951
+ 'supersededById', target.superseded_by_id
952
+ );
953
+
954
+ PERFORM set_config('opengeni.memory_lifecycle_operation', '1', true);
955
+
956
+ IF operation_type = 'reclassify' THEN
957
+ new_scope := p_plan->'scope';
958
+ new_scope_type := new_scope->>'type';
959
+ new_scope_subject_id := nullif(new_scope->>'subjectId', '');
960
+ new_scope_role_key := nullif(new_scope->>'roleKey', '');
961
+ new_scope_session_id := nullif(new_scope->>'sessionId', '')::uuid;
962
+ new_valid_until := nullif(new_scope->>'validUntil', '')::timestamptz;
963
+ new_namespace := p_plan->>'namespace';
964
+ SELECT coalesce(array_agg(label ORDER BY label), '{}'::text[]) INTO new_labels
965
+ FROM jsonb_array_elements_text(coalesce(p_plan->'labels', '[]'::jsonb)) AS label;
966
+ IF new_scope_type = 'legacy' OR NOT coalesce((CASE new_scope_type
967
+ WHEN 'workspace' THEN true
968
+ WHEN 'user' THEN new_scope_subject_id = opengeni_private.current_subject_id()
969
+ WHEN 'role' THEN new_scope_role_key = opengeni_private.current_memory_role_key()
970
+ WHEN 'session' THEN new_scope_session_id = opengeni_private.current_memory_session_id()
971
+ WHEN 'ephemeral' THEN
972
+ new_scope_session_id = opengeni_private.current_memory_session_id()
973
+ AND new_valid_until > transaction_timestamp()
974
+ ELSE false
975
+ END), false)
976
+ THEN
977
+ RAISE EXCEPTION 'new memory scope is not writable by this authority'
978
+ USING ERRCODE = '42501';
979
+ END IF;
980
+ UPDATE knowledge_memories SET
981
+ scope = new_scope_type,
982
+ scope_type = new_scope_type,
983
+ scope_subject_id = new_scope_subject_id,
984
+ scope_role_key = new_scope_role_key,
985
+ scope_session_id = new_scope_session_id,
986
+ namespace_key = new_namespace,
987
+ labels = new_labels,
988
+ valid_until = CASE WHEN new_scope_type = 'ephemeral' THEN new_valid_until ELSE NULL END,
989
+ updated_at = transaction_timestamp()
990
+ WHERE id = target_id;
991
+ ELSIF operation_type = 'archive' THEN
992
+ IF target.status NOT IN ('proposed', 'approved', 'active') THEN
993
+ RAISE EXCEPTION 'only a live memory may be archived' USING ERRCODE = '22023';
994
+ END IF;
995
+ UPDATE knowledge_memories SET
996
+ status = 'archived',
997
+ valid_until = coalesce(valid_until, transaction_timestamp()),
998
+ updated_at = transaction_timestamp()
999
+ WHERE id = target_id;
1000
+ ELSIF operation_type IN ('relationship_add', 'relationship_remove') THEN
1001
+ v_relationship_type := p_plan->>'relationshipType';
1002
+ IF v_relationship_type NOT IN (
1003
+ 'derived_from', 'supersedes', 'corrects', 'conflicts_with',
1004
+ 'related_to', 'depends_on', 'applies_to'
1005
+ ) THEN
1006
+ RAISE EXCEPTION 'memory relationship type is invalid' USING ERRCODE = '22023';
1007
+ END IF;
1008
+ IF v_relationship_type IN ('conflicts_with', 'related_to') AND related_id < target_id THEN
1009
+ RAISE EXCEPTION 'symmetric memory relationship endpoints are not canonical'
1010
+ USING ERRCODE = '22023';
1011
+ END IF;
1012
+ IF operation_type = 'relationship_add' THEN
1013
+ relationship_id := gen_random_uuid();
1014
+ INSERT INTO knowledge_memory_relationships (
1015
+ id, account_id, workspace_id, source_memory_id, target_memory_id,
1016
+ relationship_type, created_by_event_id
1017
+ ) VALUES (
1018
+ relationship_id, context_account_id, context_workspace_id, target_id, related_id,
1019
+ v_relationship_type, new_event_id
1020
+ );
1021
+ before_state := jsonb_build_object(
1022
+ 'active', false, 'relationshipId', relationship_id,
1023
+ 'targetMemoryVersion', target.memory_version,
1024
+ 'relatedMemoryVersion', related.memory_version
1025
+ );
1026
+ after_state := jsonb_build_object(
1027
+ 'active', true, 'relationshipId', relationship_id, 'relationshipVersion', 1,
1028
+ 'targetMemoryVersion', target.memory_version,
1029
+ 'relatedMemoryVersion', related.memory_version
1030
+ );
1031
+ ELSE
1032
+ SELECT * INTO relationship
1033
+ FROM knowledge_memory_relationships edge
1034
+ WHERE edge.workspace_id = context_workspace_id
1035
+ AND edge.source_memory_id = target_id
1036
+ AND edge.target_memory_id = related_id
1037
+ AND edge.relationship_type = v_relationship_type
1038
+ AND edge.removed_by_event_id IS NULL
1039
+ FOR UPDATE;
1040
+ IF NOT FOUND THEN
1041
+ RAISE EXCEPTION 'active memory relationship was not found' USING ERRCODE = '02000';
1042
+ END IF;
1043
+ relationship_id := relationship.id;
1044
+ before_state := jsonb_build_object(
1045
+ 'active', true, 'relationshipId', relationship.id,
1046
+ 'relationshipVersion', relationship.version,
1047
+ 'targetMemoryVersion', target.memory_version,
1048
+ 'relatedMemoryVersion', related.memory_version
1049
+ );
1050
+ UPDATE knowledge_memory_relationships SET
1051
+ removed_by_event_id = new_event_id,
1052
+ removed_at = transaction_timestamp()
1053
+ WHERE id = relationship.id
1054
+ RETURNING * INTO relationship;
1055
+ after_state := jsonb_build_object(
1056
+ 'active', false, 'relationshipId', relationship.id,
1057
+ 'relationshipVersion', relationship.version,
1058
+ 'targetMemoryVersion', target.memory_version,
1059
+ 'relatedMemoryVersion', related.memory_version
1060
+ );
1061
+ END IF;
1062
+ ELSE
1063
+ v_relationship_type := CASE
1064
+ WHEN operation_type = 'correct' THEN 'corrects'
1065
+ ELSE 'supersedes'
1066
+ END;
1067
+ IF target.status NOT IN ('proposed', 'approved', 'active')
1068
+ OR related.status NOT IN ('approved', 'active')
1069
+ THEN
1070
+ RAISE EXCEPTION 'supersession requires a live source and visible replacement'
1071
+ USING ERRCODE = '22023';
1072
+ END IF;
1073
+ IF ROW(target.scope_type, target.scope_subject_id, target.scope_role_key, target.scope_session_id)
1074
+ IS DISTINCT FROM ROW(
1075
+ related.scope_type, related.scope_subject_id, related.scope_role_key, related.scope_session_id
1076
+ )
1077
+ THEN
1078
+ RAISE EXCEPTION 'supersession cannot widen or change memory scope'
1079
+ USING ERRCODE = '42501';
1080
+ END IF;
1081
+ before_state := jsonb_build_object(
1082
+ 'target', before_state,
1083
+ 'related', jsonb_build_object(
1084
+ 'memoryVersion', related.memory_version,
1085
+ 'status', related.status,
1086
+ 'scopeType', related.scope_type,
1087
+ 'scopeSubjectId', related.scope_subject_id,
1088
+ 'scopeRoleKey', related.scope_role_key,
1089
+ 'scopeSessionId', related.scope_session_id,
1090
+ 'namespace', related.namespace_key,
1091
+ 'labels', to_jsonb(related.labels),
1092
+ 'validUntil', related.valid_until,
1093
+ 'supersedesId', related.supersedes_id,
1094
+ 'supersededById', related.superseded_by_id
1095
+ )
1096
+ );
1097
+ UPDATE knowledge_memories SET
1098
+ status = 'superseded',
1099
+ superseded_by_id = related_id,
1100
+ valid_until = coalesce(valid_until, transaction_timestamp()),
1101
+ updated_at = transaction_timestamp()
1102
+ WHERE id = target_id;
1103
+ UPDATE knowledge_memories SET
1104
+ supersedes_id = target_id,
1105
+ updated_at = transaction_timestamp()
1106
+ WHERE id = related_id;
1107
+ relationship_id := gen_random_uuid();
1108
+ INSERT INTO knowledge_memory_relationships (
1109
+ id, account_id, workspace_id, source_memory_id, target_memory_id,
1110
+ relationship_type, created_by_event_id
1111
+ ) VALUES (
1112
+ relationship_id, context_account_id, context_workspace_id, related_id, target_id,
1113
+ v_relationship_type, new_event_id
1114
+ );
1115
+ END IF;
1116
+
1117
+ IF operation_type NOT IN ('relationship_add', 'relationship_remove') THEN
1118
+ SELECT * INTO target FROM knowledge_memories WHERE id = target_id;
1119
+ after_state := jsonb_build_object(
1120
+ 'memoryVersion', target.memory_version,
1121
+ 'status', target.status,
1122
+ 'scopeType', target.scope_type,
1123
+ 'scopeSubjectId', target.scope_subject_id,
1124
+ 'scopeRoleKey', target.scope_role_key,
1125
+ 'scopeSessionId', target.scope_session_id,
1126
+ 'namespace', target.namespace_key,
1127
+ 'labels', to_jsonb(target.labels),
1128
+ 'validUntil', target.valid_until,
1129
+ 'supersedesId', target.supersedes_id,
1130
+ 'supersededById', target.superseded_by_id
1131
+ );
1132
+ IF operation_type IN ('supersede', 'correct') THEN
1133
+ SELECT * INTO related FROM knowledge_memories WHERE id = related_id;
1134
+ after_state := jsonb_build_object(
1135
+ 'target', after_state,
1136
+ 'related', jsonb_build_object(
1137
+ 'memoryVersion', related.memory_version,
1138
+ 'status', related.status,
1139
+ 'scopeType', related.scope_type,
1140
+ 'scopeSubjectId', related.scope_subject_id,
1141
+ 'scopeRoleKey', related.scope_role_key,
1142
+ 'scopeSessionId', related.scope_session_id,
1143
+ 'namespace', related.namespace_key,
1144
+ 'labels', to_jsonb(related.labels),
1145
+ 'validUntil', related.valid_until,
1146
+ 'supersedesId', related.supersedes_id,
1147
+ 'supersededById', related.superseded_by_id
1148
+ ),
1149
+ 'relationship', jsonb_build_object(
1150
+ 'active', true, 'relationshipId', relationship_id, 'relationshipVersion', 1
1151
+ )
1152
+ );
1153
+ before_state := before_state || jsonb_build_object('relationship', NULL);
1154
+ END IF;
1155
+ END IF;
1156
+
1157
+ INSERT INTO knowledge_memory_lifecycle_events (
1158
+ id, account_id, workspace_id, operation_id, action, operation_type,
1159
+ target_memory_id, related_memory_id, relationship_id, relationship_type,
1160
+ actor_kind, actor_subject_id, actor_session_id, actor_turn_id,
1161
+ actor_attempt_id, actor_execution_generation, plan_hash, before_state, after_state
1162
+ ) VALUES (
1163
+ new_event_id, context_account_id, context_workspace_id, v_operation_id, 'apply', operation_type,
1164
+ target_id, related_id, relationship_id, v_relationship_type,
1165
+ p_actor_kind, p_actor_subject_id, p_actor_session_id, p_actor_turn_id,
1166
+ p_actor_attempt_id, p_actor_execution_generation, p_plan_hash, before_state, after_state
1167
+ );
1168
+
1169
+ PERFORM set_config('opengeni.memory_lifecycle_operation', '', true);
1170
+ RETURN QUERY SELECT new_event_id;
1171
+ END;
1172
+ $body$;
1173
+ $ddl$, target_schema, target_schema);
1174
+ EXECUTE format(
1175
+ 'REVOKE ALL ON FUNCTION %I.knowledge_memory_apply_operation(jsonb, text, text, text, uuid, uuid, uuid, integer) FROM PUBLIC',
1176
+ target_schema
1177
+ );
1178
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1179
+ EXECUTE format(
1180
+ 'GRANT EXECUTE ON FUNCTION %I.knowledge_memory_apply_operation(jsonb, text, text, text, uuid, uuid, uuid, integer) TO opengeni_app',
1181
+ target_schema
1182
+ );
1183
+ END IF;
1184
+ END
1185
+ $apply_function$;
1186
+
1187
+ DO $revert_function$
1188
+ DECLARE target_schema text := current_schema();
1189
+ BEGIN
1190
+ EXECUTE format($ddl$
1191
+ CREATE OR REPLACE FUNCTION %I.knowledge_memory_revert_operation(
1192
+ p_operation_id uuid,
1193
+ p_applied_operation_id uuid,
1194
+ p_plan_hash text,
1195
+ p_actor_kind text,
1196
+ p_actor_subject_id text,
1197
+ p_actor_session_id uuid,
1198
+ p_actor_turn_id uuid,
1199
+ p_actor_attempt_id uuid,
1200
+ p_actor_execution_generation integer
1201
+ ) RETURNS TABLE (event_id uuid)
1202
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %I, pg_catalog
1203
+ AS $body$
1204
+ DECLARE
1205
+ context_account_id uuid := nullif(current_setting('opengeni.account_id', true), '')::uuid;
1206
+ context_workspace_id uuid := nullif(current_setting('opengeni.workspace_id', true), '')::uuid;
1207
+ applied record;
1208
+ existing_event record;
1209
+ target record;
1210
+ related record;
1211
+ relationship record;
1212
+ restored jsonb;
1213
+ revert_before jsonb;
1214
+ revert_after jsonb;
1215
+ new_event_id uuid := gen_random_uuid();
1216
+ authority_role_key text;
1217
+ BEGIN
1218
+ IF p_operation_id IS NULL OR p_applied_operation_id IS NULL
1219
+ OR p_plan_hash !~ '^[a-f0-9]{64}$'
1220
+ OR p_plan_hash IS DISTINCT FROM knowledge_memory_plan_hash(jsonb_build_object(
1221
+ 'operationId', p_operation_id,
1222
+ 'appliedOperationId', p_applied_operation_id
1223
+ ))
1224
+ THEN
1225
+ RAISE EXCEPTION 'memory revert plan or hash is invalid' USING ERRCODE = '22023';
1226
+ END IF;
1227
+ IF context_account_id IS NULL OR context_workspace_id IS NULL
1228
+ OR p_actor_kind NOT IN ('subject', 'service')
1229
+ OR length(btrim(p_actor_subject_id)) NOT BETWEEN 1 AND 1024
1230
+ OR opengeni_private.current_memory_actor_kind() IS DISTINCT FROM p_actor_kind
1231
+ OR opengeni_private.current_memory_actor_id() IS DISTINCT FROM p_actor_subject_id
1232
+ OR (p_actor_kind = 'subject'
1233
+ AND opengeni_private.current_subject_id() IS DISTINCT FROM p_actor_subject_id)
1234
+ OR (p_actor_kind = 'service' AND opengeni_private.current_subject_id() IS NOT NULL)
1235
+ THEN
1236
+ RAISE EXCEPTION 'memory revert requires exact transaction-local actor authority'
1237
+ USING ERRCODE = '42501';
1238
+ END IF;
1239
+
1240
+ IF (p_actor_session_id IS NULL) <> (p_actor_turn_id IS NULL)
1241
+ OR (p_actor_session_id IS NULL) <> (p_actor_attempt_id IS NULL)
1242
+ OR (p_actor_session_id IS NULL) <> (p_actor_execution_generation IS NULL)
1243
+ THEN
1244
+ RAISE EXCEPTION 'memory revert attempt provenance is partial' USING ERRCODE = '22023';
1245
+ END IF;
1246
+ IF p_actor_session_id IS NOT NULL THEN
1247
+ SELECT session.metadata ->> 'memoryRoleKey' INTO authority_role_key
1248
+ FROM workspaces workspace
1249
+ JOIN sessions session
1250
+ ON session.account_id = workspace.account_id
1251
+ AND session.workspace_id = workspace.id
1252
+ JOIN session_turns turn
1253
+ ON turn.account_id = session.account_id
1254
+ AND turn.workspace_id = session.workspace_id
1255
+ AND turn.session_id = session.id
1256
+ JOIN session_turn_attempts attempt
1257
+ ON attempt.account_id = turn.account_id
1258
+ AND attempt.workspace_id = turn.workspace_id
1259
+ AND attempt.session_id = turn.session_id
1260
+ AND attempt.turn_id = turn.id
1261
+ WHERE workspace.account_id = context_account_id
1262
+ AND workspace.id = context_workspace_id
1263
+ AND session.id = p_actor_session_id
1264
+ AND session.active_turn_id = p_actor_turn_id
1265
+ AND turn.id = p_actor_turn_id
1266
+ AND turn.active_attempt_id = p_actor_attempt_id
1267
+ AND turn.execution_generation = p_actor_execution_generation
1268
+ AND turn.status IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
1269
+ AND turn.initiator_kind = p_actor_kind
1270
+ AND turn.initiator_subject_id = p_actor_subject_id
1271
+ AND attempt.id = p_actor_attempt_id
1272
+ AND attempt.execution_generation = p_actor_execution_generation
1273
+ AND attempt.state IN ('claimed', 'running')
1274
+ AND NOT EXISTS (
1275
+ SELECT 1 FROM session_attempt_interruptions interruption
1276
+ WHERE interruption.workspace_id = attempt.workspace_id
1277
+ AND interruption.attempt_id = attempt.id
1278
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
1279
+ )
1280
+ FOR KEY SHARE OF workspace
1281
+ FOR SHARE OF session, turn
1282
+ FOR UPDATE OF attempt;
1283
+ IF NOT FOUND OR p_actor_execution_generation < 1 THEN
1284
+ RAISE EXCEPTION 'memory revert attempt authority is stale or invalid'
1285
+ USING ERRCODE = '42501';
1286
+ END IF;
1287
+ IF authority_role_key IS NOT NULL AND (
1288
+ authority_role_key IS DISTINCT FROM lower(btrim(authority_role_key))
1289
+ OR length(authority_role_key) NOT BETWEEN 1 AND 64
1290
+ OR authority_role_key !~ '^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$'
1291
+ ) THEN
1292
+ RAISE EXCEPTION 'persisted memory role authority is invalid' USING ERRCODE = '42501';
1293
+ END IF;
1294
+ IF opengeni_private.current_memory_session_id() IS DISTINCT FROM p_actor_session_id
1295
+ OR opengeni_private.current_memory_role_key() IS DISTINCT FROM authority_role_key
1296
+ THEN
1297
+ RAISE EXCEPTION 'memory revert session or role context is not exact'
1298
+ USING ERRCODE = '42501';
1299
+ END IF;
1300
+ ELSIF opengeni_private.current_memory_session_id() IS NOT NULL
1301
+ OR opengeni_private.current_memory_role_key() IS NOT NULL
1302
+ THEN
1303
+ RAISE EXCEPTION 'direct memory revert authority cannot carry session or role context'
1304
+ USING ERRCODE = '42501';
1305
+ END IF;
1306
+
1307
+ SELECT event.* INTO existing_event
1308
+ FROM knowledge_memory_lifecycle_events event
1309
+ WHERE event.workspace_id = context_workspace_id
1310
+ AND event.operation_id = p_operation_id
1311
+ AND event.action = 'revert';
1312
+ IF FOUND THEN
1313
+ IF existing_event.plan_hash IS DISTINCT FROM p_plan_hash
1314
+ OR existing_event.actor_kind IS DISTINCT FROM p_actor_kind
1315
+ OR existing_event.actor_subject_id IS DISTINCT FROM p_actor_subject_id
1316
+ OR existing_event.actor_session_id IS DISTINCT FROM p_actor_session_id
1317
+ OR existing_event.actor_turn_id IS DISTINCT FROM p_actor_turn_id
1318
+ OR existing_event.actor_attempt_id IS DISTINCT FROM p_actor_attempt_id
1319
+ OR existing_event.actor_execution_generation
1320
+ IS DISTINCT FROM p_actor_execution_generation
1321
+ OR existing_event.reverts_event_id IS DISTINCT FROM (
1322
+ SELECT event.id FROM knowledge_memory_lifecycle_events event
1323
+ WHERE event.workspace_id = context_workspace_id
1324
+ AND event.operation_id = p_applied_operation_id
1325
+ AND event.action = 'apply'
1326
+ )
1327
+ THEN
1328
+ RAISE EXCEPTION 'memory revert id already identifies different immutable evidence'
1329
+ USING ERRCODE = '23505';
1330
+ END IF;
1331
+ RETURN QUERY SELECT existing_event.id;
1332
+ RETURN;
1333
+ END IF;
1334
+
1335
+ SELECT event.* INTO applied
1336
+ FROM knowledge_memory_lifecycle_events event
1337
+ WHERE event.account_id = context_account_id
1338
+ AND event.workspace_id = context_workspace_id
1339
+ AND event.operation_id = p_applied_operation_id
1340
+ AND event.action = 'apply'
1341
+ FOR UPDATE;
1342
+ IF NOT FOUND THEN
1343
+ RAISE EXCEPTION 'applied memory operation was not found' USING ERRCODE = '02000';
1344
+ END IF;
1345
+ IF applied.actor_kind IS DISTINCT FROM p_actor_kind
1346
+ OR applied.actor_subject_id IS DISTINCT FROM p_actor_subject_id
1347
+ THEN
1348
+ RAISE EXCEPTION 'memory revert requires the same immutable actor authority'
1349
+ USING ERRCODE = '42501';
1350
+ END IF;
1351
+ IF EXISTS (
1352
+ SELECT 1 FROM knowledge_memory_lifecycle_events event
1353
+ WHERE event.reverts_event_id = applied.id
1354
+ ) THEN
1355
+ RAISE EXCEPTION 'memory operation was already reverted' USING ERRCODE = '23505';
1356
+ END IF;
1357
+
1358
+ SELECT * INTO target FROM knowledge_memories
1359
+ WHERE id = applied.target_memory_id
1360
+ AND account_id = context_account_id
1361
+ AND workspace_id = context_workspace_id;
1362
+ IF NOT FOUND OR NOT opengeni_private.memory_scope_authorized(
1363
+ target.scope_type, target.scope_subject_id, target.scope_role_key, target.scope_session_id
1364
+ ) THEN
1365
+ RAISE EXCEPTION 'reverted target memory is not authorized for this actor'
1366
+ USING ERRCODE = '42501';
1367
+ END IF;
1368
+ IF applied.related_memory_id IS NOT NULL THEN
1369
+ SELECT * INTO related FROM knowledge_memories
1370
+ WHERE id = applied.related_memory_id
1371
+ AND account_id = context_account_id
1372
+ AND workspace_id = context_workspace_id;
1373
+ IF NOT FOUND OR NOT opengeni_private.memory_scope_authorized(
1374
+ related.scope_type, related.scope_subject_id,
1375
+ related.scope_role_key, related.scope_session_id
1376
+ ) THEN
1377
+ RAISE EXCEPTION 'reverted related memory is not authorized for this actor'
1378
+ USING ERRCODE = '42501';
1379
+ END IF;
1380
+ END IF;
1381
+
1382
+ PERFORM set_config('opengeni.memory_lifecycle_operation', '1', true);
1383
+
1384
+ IF applied.operation_type IN ('reclassify', 'archive') THEN
1385
+ SELECT * INTO target FROM knowledge_memories
1386
+ WHERE id = applied.target_memory_id AND account_id = context_account_id
1387
+ AND workspace_id = context_workspace_id FOR UPDATE;
1388
+ IF target.memory_version <> (applied.after_state->>'memoryVersion')::integer THEN
1389
+ RAISE EXCEPTION 'memory changed after the applied operation' USING ERRCODE = '40001';
1390
+ END IF;
1391
+ revert_before := applied.after_state;
1392
+ restored := applied.before_state;
1393
+ UPDATE knowledge_memories SET
1394
+ status = restored->>'status',
1395
+ scope = restored->>'scopeType',
1396
+ scope_type = restored->>'scopeType',
1397
+ scope_subject_id = nullif(restored->>'scopeSubjectId', ''),
1398
+ scope_role_key = nullif(restored->>'scopeRoleKey', ''),
1399
+ scope_session_id = nullif(restored->>'scopeSessionId', '')::uuid,
1400
+ namespace_key = restored->>'namespace',
1401
+ labels = ARRAY(SELECT jsonb_array_elements_text(restored->'labels')),
1402
+ valid_until = nullif(restored->>'validUntil', '')::timestamptz,
1403
+ supersedes_id = nullif(restored->>'supersedesId', '')::uuid,
1404
+ superseded_by_id = nullif(restored->>'supersededById', '')::uuid,
1405
+ updated_at = transaction_timestamp()
1406
+ WHERE id = target.id
1407
+ RETURNING * INTO target;
1408
+ revert_after := jsonb_build_object(
1409
+ 'memoryVersion', target.memory_version,
1410
+ 'status', target.status,
1411
+ 'scopeType', target.scope_type,
1412
+ 'scopeSubjectId', target.scope_subject_id,
1413
+ 'scopeRoleKey', target.scope_role_key,
1414
+ 'scopeSessionId', target.scope_session_id,
1415
+ 'namespace', target.namespace_key,
1416
+ 'labels', to_jsonb(target.labels),
1417
+ 'validUntil', target.valid_until,
1418
+ 'supersedesId', target.supersedes_id,
1419
+ 'supersededById', target.superseded_by_id
1420
+ );
1421
+ ELSIF applied.operation_type IN ('relationship_add', 'relationship_remove') THEN
1422
+ PERFORM 1 FROM knowledge_memories candidate
1423
+ WHERE candidate.id IN (applied.target_memory_id, applied.related_memory_id)
1424
+ AND candidate.account_id = context_account_id
1425
+ AND candidate.workspace_id = context_workspace_id
1426
+ ORDER BY candidate.id FOR UPDATE;
1427
+ SELECT * INTO target FROM knowledge_memories
1428
+ WHERE id = applied.target_memory_id
1429
+ AND account_id = context_account_id
1430
+ AND workspace_id = context_workspace_id;
1431
+ SELECT * INTO related FROM knowledge_memories
1432
+ WHERE id = applied.related_memory_id
1433
+ AND account_id = context_account_id
1434
+ AND workspace_id = context_workspace_id;
1435
+ IF target.memory_version <>
1436
+ (applied.after_state->>'targetMemoryVersion')::integer
1437
+ OR related.memory_version <>
1438
+ (applied.after_state->>'relatedMemoryVersion')::integer
1439
+ THEN
1440
+ RAISE EXCEPTION 'relationship endpoints changed after apply'
1441
+ USING ERRCODE = '40001';
1442
+ END IF;
1443
+ SELECT * INTO relationship FROM knowledge_memory_relationships
1444
+ WHERE id = applied.relationship_id AND workspace_id = context_workspace_id FOR UPDATE;
1445
+ revert_before := jsonb_build_object(
1446
+ 'active', relationship.removed_by_event_id IS NULL,
1447
+ 'relationshipId', relationship.id,
1448
+ 'relationshipVersion', relationship.version,
1449
+ 'targetMemoryVersion', target.memory_version,
1450
+ 'relatedMemoryVersion', related.memory_version
1451
+ );
1452
+ IF applied.operation_type = 'relationship_add' THEN
1453
+ IF relationship.created_by_event_id <> applied.id
1454
+ OR relationship.removed_by_event_id IS NOT NULL
1455
+ OR relationship.version <> (applied.after_state->>'relationshipVersion')::integer
1456
+ THEN
1457
+ RAISE EXCEPTION 'relationship add can no longer be reverted' USING ERRCODE = '40001';
1458
+ END IF;
1459
+ UPDATE knowledge_memory_relationships SET
1460
+ removed_by_event_id = new_event_id,
1461
+ removed_at = transaction_timestamp()
1462
+ WHERE id = relationship.id RETURNING * INTO relationship;
1463
+ ELSE
1464
+ IF relationship.removed_by_event_id <> applied.id
1465
+ OR relationship.version <> (applied.after_state->>'relationshipVersion')::integer
1466
+ THEN
1467
+ RAISE EXCEPTION 'relationship removal can no longer be reverted' USING ERRCODE = '40001';
1468
+ END IF;
1469
+ UPDATE knowledge_memory_relationships SET
1470
+ removed_by_event_id = NULL,
1471
+ removed_at = NULL
1472
+ WHERE id = relationship.id RETURNING * INTO relationship;
1473
+ END IF;
1474
+ revert_after := jsonb_build_object(
1475
+ 'active', relationship.removed_by_event_id IS NULL,
1476
+ 'relationshipId', relationship.id,
1477
+ 'relationshipVersion', relationship.version,
1478
+ 'targetMemoryVersion', target.memory_version,
1479
+ 'relatedMemoryVersion', related.memory_version
1480
+ );
1481
+ ELSE
1482
+ PERFORM 1 FROM knowledge_memories candidate
1483
+ WHERE candidate.id IN (applied.target_memory_id, applied.related_memory_id)
1484
+ AND candidate.account_id = context_account_id
1485
+ AND candidate.workspace_id = context_workspace_id
1486
+ ORDER BY candidate.id FOR UPDATE;
1487
+ SELECT * INTO target FROM knowledge_memories WHERE id = applied.target_memory_id;
1488
+ SELECT * INTO related FROM knowledge_memories WHERE id = applied.related_memory_id;
1489
+ IF target.memory_version <> (applied.after_state#>>'{target,memoryVersion}')::integer
1490
+ OR related.memory_version <> (applied.after_state#>>'{related,memoryVersion}')::integer
1491
+ THEN
1492
+ RAISE EXCEPTION 'supersession participants changed after apply' USING ERRCODE = '40001';
1493
+ END IF;
1494
+ revert_before := applied.after_state;
1495
+ restored := applied.before_state->'target';
1496
+ UPDATE knowledge_memories SET
1497
+ status = restored->>'status',
1498
+ valid_until = nullif(restored->>'validUntil', '')::timestamptz,
1499
+ supersedes_id = nullif(restored->>'supersedesId', '')::uuid,
1500
+ superseded_by_id = nullif(restored->>'supersededById', '')::uuid,
1501
+ updated_at = transaction_timestamp()
1502
+ WHERE id = target.id RETURNING * INTO target;
1503
+ restored := applied.before_state->'related';
1504
+ UPDATE knowledge_memories SET
1505
+ status = restored->>'status',
1506
+ valid_until = nullif(restored->>'validUntil', '')::timestamptz,
1507
+ supersedes_id = nullif(restored->>'supersedesId', '')::uuid,
1508
+ superseded_by_id = nullif(restored->>'supersededById', '')::uuid,
1509
+ updated_at = transaction_timestamp()
1510
+ WHERE id = related.id RETURNING * INTO related;
1511
+ SELECT * INTO relationship FROM knowledge_memory_relationships
1512
+ WHERE id = applied.relationship_id AND removed_by_event_id IS NULL FOR UPDATE;
1513
+ IF NOT FOUND OR relationship.version <>
1514
+ (applied.after_state#>>'{relationship,relationshipVersion}')::integer
1515
+ THEN
1516
+ RAISE EXCEPTION 'supersession relationship changed after apply' USING ERRCODE = '40001';
1517
+ END IF;
1518
+ UPDATE knowledge_memory_relationships SET
1519
+ removed_by_event_id = new_event_id,
1520
+ removed_at = transaction_timestamp()
1521
+ WHERE id = relationship.id RETURNING * INTO relationship;
1522
+ revert_after := jsonb_build_object(
1523
+ 'target', jsonb_build_object(
1524
+ 'memoryVersion', target.memory_version, 'status', target.status,
1525
+ 'scopeType', target.scope_type, 'scopeSubjectId', target.scope_subject_id,
1526
+ 'scopeRoleKey', target.scope_role_key, 'scopeSessionId', target.scope_session_id,
1527
+ 'namespace', target.namespace_key, 'labels', to_jsonb(target.labels),
1528
+ 'validUntil', target.valid_until, 'supersedesId', target.supersedes_id,
1529
+ 'supersededById', target.superseded_by_id
1530
+ ),
1531
+ 'related', jsonb_build_object(
1532
+ 'memoryVersion', related.memory_version, 'status', related.status,
1533
+ 'scopeType', related.scope_type, 'scopeSubjectId', related.scope_subject_id,
1534
+ 'scopeRoleKey', related.scope_role_key, 'scopeSessionId', related.scope_session_id,
1535
+ 'namespace', related.namespace_key, 'labels', to_jsonb(related.labels),
1536
+ 'validUntil', related.valid_until, 'supersedesId', related.supersedes_id,
1537
+ 'supersededById', related.superseded_by_id
1538
+ ),
1539
+ 'relationship', jsonb_build_object(
1540
+ 'active', false, 'relationshipId', relationship.id,
1541
+ 'relationshipVersion', relationship.version
1542
+ )
1543
+ );
1544
+ END IF;
1545
+
1546
+ INSERT INTO knowledge_memory_lifecycle_events (
1547
+ id, account_id, workspace_id, operation_id, action, operation_type,
1548
+ target_memory_id, related_memory_id, relationship_id, relationship_type,
1549
+ actor_kind, actor_subject_id, actor_session_id, actor_turn_id,
1550
+ actor_attempt_id, actor_execution_generation, plan_hash,
1551
+ before_state, after_state, reverts_event_id
1552
+ ) VALUES (
1553
+ new_event_id, context_account_id, context_workspace_id, p_operation_id, 'revert',
1554
+ applied.operation_type, applied.target_memory_id, applied.related_memory_id,
1555
+ applied.relationship_id, applied.relationship_type, p_actor_kind, p_actor_subject_id,
1556
+ p_actor_session_id, p_actor_turn_id, p_actor_attempt_id,
1557
+ p_actor_execution_generation, p_plan_hash, revert_before, revert_after, applied.id
1558
+ );
1559
+ PERFORM set_config('opengeni.memory_lifecycle_operation', '', true);
1560
+ RETURN QUERY SELECT new_event_id;
1561
+ END;
1562
+ $body$;
1563
+ $ddl$, target_schema, target_schema);
1564
+ EXECUTE format(
1565
+ 'REVOKE ALL ON FUNCTION %I.knowledge_memory_revert_operation(uuid, uuid, text, text, text, uuid, uuid, uuid, integer) FROM PUBLIC',
1566
+ target_schema
1567
+ );
1568
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1569
+ EXECUTE format(
1570
+ 'GRANT EXECUTE ON FUNCTION %I.knowledge_memory_revert_operation(uuid, uuid, text, text, text, uuid, uuid, uuid, integer) TO opengeni_app',
1571
+ target_schema
1572
+ );
1573
+ END IF;
1574
+ END
1575
+ $revert_function$;
1576
+
1577
+ REVOKE ALL ON TABLE knowledge_memory_relationships FROM PUBLIC;
1578
+ REVOKE ALL ON TABLE knowledge_memory_lifecycle_events FROM PUBLIC;
1579
+
1580
+ DO $runtime_grants$
1581
+ BEGIN
1582
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1583
+ EXECUTE format(
1584
+ 'GRANT SELECT ON TABLE %I.knowledge_memory_relationships, %I.knowledge_memory_lifecycle_events TO opengeni_app',
1585
+ current_schema(), current_schema()
1586
+ );
1587
+ END IF;
1588
+ END
1589
+ $runtime_grants$;