@opengeni/db 0.18.1 → 0.19.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.
- package/dist/{chunk-YKWJ7QJ2.js → chunk-SZP6KRLC.js} +2123 -1932
- package/dist/chunk-SZP6KRLC.js.map +1 -0
- package/dist/{chunk-T6RSJT6C.js → chunk-XOXEQ7HG.js} +29 -1
- package/dist/chunk-XOXEQ7HG.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1023 -561
- package/dist/index.js.map +1 -1
- package/dist/memory-domain.d.ts +110 -1
- package/dist/memory-governance-schema.d.ts +562 -0
- package/dist/memory-governance.d.ts +41 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +199 -0
- package/dist/schema.js +5 -1
- package/drizzle/0152_hierarchical_memory_foundation.sql +1589 -0
- package/package.json +1 -1
- package/src/index.ts +10 -5
- package/src/memory-domain.ts +360 -1
- package/src/memory-governance-schema.ts +162 -0
- package/src/memory-governance.ts +317 -0
- package/src/provision-roles.ts +24 -0
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +40 -2
- package/dist/chunk-T6RSJT6C.js.map +0 -1
- 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$;
|