@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -238,6 +238,7 @@ export * from "./session-queue-commands";
238
238
  export * from "./new-session-drafts";
239
239
  export * from "./workspace-instruction-policies";
240
240
  export * from "./preference-registry";
241
+ export * from "./memory-governance";
241
242
  export { interruptedToolCallResult } from "./session-tool-call-settlement";
242
243
  export { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
243
244
  export {
@@ -7888,7 +7889,10 @@ function memoryVectorLiteral(values: number[]): string {
7888
7889
  }
7889
7890
 
7890
7891
  const agentVisibleMemoryStatuses = [...AGENT_VISIBLE_MEMORY_STATUSES];
7891
- const visibleTextHashUniqueIndexName = "knowledge_memories_workspace_visible_text_hash_uq";
7892
+ const visibleTextHashUniqueIndexNames = new Set([
7893
+ "knowledge_memories_workspace_visible_text_hash_uq",
7894
+ "knowledge_memories_scope_visible_text_hash_uq",
7895
+ ]);
7892
7896
 
7893
7897
  function isVisibleTextHashUniqueViolation(error: unknown): boolean {
7894
7898
  const candidate = error as {
@@ -7902,14 +7906,15 @@ function isVisibleTextHashUniqueViolation(error: unknown): boolean {
7902
7906
  return false;
7903
7907
  }
7904
7908
  const constraint = candidate.constraint ?? candidate.constraint_name;
7905
- if (candidate.code === "23505" && constraint === visibleTextHashUniqueIndexName) {
7909
+ if (candidate.code === "23505" && visibleTextHashUniqueIndexNames.has(String(constraint))) {
7906
7910
  return true;
7907
7911
  }
7912
+ const message = typeof candidate.message === "string" ? candidate.message : null;
7908
7913
  if (
7909
- typeof candidate.message === "string" &&
7910
- candidate.message.includes(visibleTextHashUniqueIndexName) &&
7914
+ message !== null &&
7915
+ [...visibleTextHashUniqueIndexNames].some((name) => message.includes(name)) &&
7911
7916
  (candidate.code === "23505" ||
7912
- candidate.message.includes("duplicate key value violates unique constraint"))
7917
+ message.includes("duplicate key value violates unique constraint"))
7913
7918
  ) {
7914
7919
  return true;
7915
7920
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { KnowledgeMemoryKind } from "@opengeni/contracts";
2
+ import { stableJson, type KnowledgeMemoryKind } from "@opengeni/contracts";
3
3
 
4
4
  // Workspace Memory V1 — pure domain logic (gates + render + canonical prompt
5
5
  // text). No database access: everything here is unit-testable in isolation and
@@ -32,6 +32,365 @@ export const MEMORY_SEARCH_MAX_LIMIT = 20;
32
32
  /** Statuses an agent may see: active (agent-written) ∪ approved (curated). */
33
33
  export const AGENT_VISIBLE_MEMORY_STATUSES = ["active", "approved"] as const;
34
34
 
35
+ /** Maximum normalized label length. Labels are relevance hints, never authority. */
36
+ export const MEMORY_LABEL_MAX_CHARS = 64;
37
+ /** Maximum labels stored on one memory. */
38
+ export const MEMORY_LABEL_MAX_COUNT = 16;
39
+ /** Maximum normalized namespace length, including hierarchy separators. */
40
+ export const MEMORY_NAMESPACE_MAX_CHARS = 128;
41
+ /** Maximum normalized durable-role key length. */
42
+ export const MEMORY_ROLE_KEY_MAX_CHARS = 64;
43
+ /** Maximum subject identifier length stored in typed scope/authority columns. */
44
+ export const MEMORY_SUBJECT_ID_MAX_CHARS = 1024;
45
+
46
+ const MEMORY_SELECTOR_SEGMENT_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
47
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
48
+
49
+ export type MemoryScopeType = "workspace" | "user" | "role" | "session" | "ephemeral" | "legacy";
50
+
51
+ export type MemoryScopeSpec =
52
+ | { type: "workspace" }
53
+ | { type: "user"; subjectId: string }
54
+ | { type: "role"; roleKey: string }
55
+ | { type: "session"; sessionId: string }
56
+ | { type: "ephemeral"; sessionId: string; validUntil: string }
57
+ | { type: "legacy"; legacyScope: string };
58
+
59
+ export type MemoryRelationshipType =
60
+ | "derived_from"
61
+ | "supersedes"
62
+ | "corrects"
63
+ | "conflicts_with"
64
+ | "related_to"
65
+ | "depends_on"
66
+ | "applies_to";
67
+
68
+ export const MEMORY_RELATIONSHIP_TYPES = [
69
+ "derived_from",
70
+ "supersedes",
71
+ "corrects",
72
+ "conflicts_with",
73
+ "related_to",
74
+ "depends_on",
75
+ "applies_to",
76
+ ] as const satisfies readonly MemoryRelationshipType[];
77
+
78
+ const SYMMETRIC_MEMORY_RELATIONSHIPS = new Set<MemoryRelationshipType>([
79
+ "conflicts_with",
80
+ "related_to",
81
+ ]);
82
+
83
+ export type MemoryOperationType =
84
+ | "reclassify"
85
+ | "archive"
86
+ | "relationship_add"
87
+ | "relationship_remove"
88
+ | "supersede"
89
+ | "correct";
90
+
91
+ type MemoryOperationPlanBase = {
92
+ operationId: string;
93
+ operationType: MemoryOperationType;
94
+ targetMemoryId: string;
95
+ expectedTargetVersion: number;
96
+ };
97
+
98
+ export type MemoryOperationPlan =
99
+ | (MemoryOperationPlanBase & {
100
+ operationType: "reclassify";
101
+ scope: MemoryScopeSpec;
102
+ namespace: string;
103
+ labels: string[];
104
+ })
105
+ | (MemoryOperationPlanBase & { operationType: "archive" })
106
+ | (MemoryOperationPlanBase & {
107
+ operationType: "relationship_add" | "relationship_remove";
108
+ relatedMemoryId: string;
109
+ expectedRelatedVersion: number;
110
+ relationshipType: MemoryRelationshipType;
111
+ })
112
+ | (MemoryOperationPlanBase & {
113
+ operationType: "supersede" | "correct";
114
+ relatedMemoryId: string;
115
+ expectedRelatedVersion: number;
116
+ });
117
+
118
+ export type MemoryOperationPlanInput =
119
+ | (Omit<MemoryOperationPlanBase, "operationType"> & {
120
+ operationType: "reclassify";
121
+ scope: MemoryScopeSpec;
122
+ namespace?: string | null;
123
+ labels?: readonly string[] | null;
124
+ })
125
+ | (Omit<MemoryOperationPlanBase, "operationType"> & { operationType: "archive" })
126
+ | (Omit<MemoryOperationPlanBase, "operationType"> & {
127
+ operationType: "relationship_add" | "relationship_remove";
128
+ relatedMemoryId: string;
129
+ expectedRelatedVersion: number;
130
+ relationshipType: MemoryRelationshipType;
131
+ })
132
+ | (Omit<MemoryOperationPlanBase, "operationType"> & {
133
+ operationType: "supersede" | "correct";
134
+ relatedMemoryId: string;
135
+ expectedRelatedVersion: number;
136
+ });
137
+
138
+ export type MemoryRevertPlan = {
139
+ operationId: string;
140
+ appliedOperationId: string;
141
+ };
142
+
143
+ export type MemoryRevertPlanInput = MemoryRevertPlan;
144
+
145
+ function normalizeSelectorSegment(value: string, label: string): string {
146
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
147
+ if (!MEMORY_SELECTOR_SEGMENT_PATTERN.test(normalized)) {
148
+ throw new Error(
149
+ `${label} must be a lowercase slug using letters, numbers, dot, underscore, or dash`,
150
+ );
151
+ }
152
+ return normalized;
153
+ }
154
+
155
+ function normalizeUuid(value: string, label: string): string {
156
+ const normalized = value.trim().toLowerCase();
157
+ if (!UUID_PATTERN.test(normalized)) {
158
+ throw new Error(`${label} must be a UUID`);
159
+ }
160
+ return normalized;
161
+ }
162
+
163
+ function normalizePositiveVersion(value: number, label: string): number {
164
+ if (!Number.isSafeInteger(value) || value <= 0) {
165
+ throw new Error(`${label} must be a positive safe integer`);
166
+ }
167
+ return value;
168
+ }
169
+
170
+ /** Normalize one bounded memory label. Invalid selectors fail closed. */
171
+ export function normalizeMemoryLabel(label: string): string {
172
+ const normalized = normalizeSelectorSegment(label, "memory label");
173
+ if (normalized.length > MEMORY_LABEL_MAX_CHARS) {
174
+ throw new Error(`memory label exceeds ${MEMORY_LABEL_MAX_CHARS} characters`);
175
+ }
176
+ return normalized;
177
+ }
178
+
179
+ /** Normalize, de-duplicate, sort, and bound memory labels deterministically. */
180
+ export function normalizeMemoryLabels(labels: readonly string[] | null | undefined): string[] {
181
+ const normalized = new Set((labels ?? []).map(normalizeMemoryLabel));
182
+ if (normalized.size > MEMORY_LABEL_MAX_COUNT) {
183
+ throw new Error(`memory labels exceed the ${MEMORY_LABEL_MAX_COUNT}-label limit`);
184
+ }
185
+ return [...normalized].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
186
+ }
187
+
188
+ /** Normalize a hierarchical namespace such as `engineering/backend`. */
189
+ export function normalizeMemoryNamespace(namespace: string | null | undefined): string {
190
+ const raw = namespace?.trim() || "general";
191
+ const normalized = raw
192
+ .split("/")
193
+ .map((segment) => normalizeSelectorSegment(segment, "memory namespace segment"))
194
+ .join("/");
195
+ if (normalized.length > MEMORY_NAMESPACE_MAX_CHARS) {
196
+ throw new Error(`memory namespace exceeds ${MEMORY_NAMESPACE_MAX_CHARS} characters`);
197
+ }
198
+ return normalized;
199
+ }
200
+
201
+ export function normalizeMemoryRoleKey(roleKey: string): string {
202
+ const normalized = normalizeSelectorSegment(roleKey, "memory role key");
203
+ if (normalized.length > MEMORY_ROLE_KEY_MAX_CHARS) {
204
+ throw new Error(`memory role key exceeds ${MEMORY_ROLE_KEY_MAX_CHARS} characters`);
205
+ }
206
+ return normalized;
207
+ }
208
+
209
+ export function normalizeMemoryScope(scope: MemoryScopeSpec): MemoryScopeSpec {
210
+ switch (scope.type) {
211
+ case "workspace":
212
+ return { type: "workspace" };
213
+ case "user": {
214
+ const subjectId = scope.subjectId.trim();
215
+ if (!subjectId || subjectId.length > MEMORY_SUBJECT_ID_MAX_CHARS) {
216
+ throw new Error(
217
+ `memory user scope requires a subject id of at most ${MEMORY_SUBJECT_ID_MAX_CHARS} characters`,
218
+ );
219
+ }
220
+ return { type: "user", subjectId };
221
+ }
222
+ case "role":
223
+ return { type: "role", roleKey: normalizeMemoryRoleKey(scope.roleKey) };
224
+ case "session":
225
+ return { type: "session", sessionId: normalizeUuid(scope.sessionId, "memory session id") };
226
+ case "ephemeral": {
227
+ const validUntil = new Date(scope.validUntil);
228
+ if (!Number.isFinite(validUntil.getTime())) {
229
+ throw new Error("ephemeral memory scope requires a valid expiry timestamp");
230
+ }
231
+ return {
232
+ type: "ephemeral",
233
+ sessionId: normalizeUuid(scope.sessionId, "ephemeral memory session id"),
234
+ validUntil: validUntil.toISOString(),
235
+ };
236
+ }
237
+ case "legacy": {
238
+ const legacyScope = scope.legacyScope.trim();
239
+ if (!legacyScope || legacyScope.length > MEMORY_NAMESPACE_MAX_CHARS) {
240
+ throw new Error("legacy memory scope must be a non-empty bounded string");
241
+ }
242
+ return { type: "legacy", legacyScope };
243
+ }
244
+ }
245
+ }
246
+
247
+ export function isMemoryScopeApplicable(
248
+ scope: MemoryScopeSpec,
249
+ context: {
250
+ subjectId?: string | null;
251
+ roleKey?: string | null;
252
+ sessionId?: string | null;
253
+ now?: Date | string;
254
+ },
255
+ ): boolean {
256
+ const normalized = normalizeMemoryScope(scope);
257
+ switch (normalized.type) {
258
+ case "workspace":
259
+ return true;
260
+ case "user":
261
+ return Boolean(context.subjectId) && normalized.subjectId === context.subjectId;
262
+ case "role":
263
+ return (
264
+ Boolean(context.roleKey) && normalized.roleKey === normalizeMemoryRoleKey(context.roleKey!)
265
+ );
266
+ case "session":
267
+ return Boolean(context.sessionId) && normalized.sessionId === context.sessionId;
268
+ case "ephemeral": {
269
+ if (!context.sessionId || normalized.sessionId !== context.sessionId) return false;
270
+ const now = context.now instanceof Date ? context.now : new Date(context.now ?? Date.now());
271
+ return Number.isFinite(now.getTime()) && now.getTime() < Date.parse(normalized.validUntil);
272
+ }
273
+ case "legacy":
274
+ return false;
275
+ }
276
+ }
277
+
278
+ export function canonicalMemoryRelationship(input: {
279
+ sourceMemoryId: string;
280
+ targetMemoryId: string;
281
+ relationshipType: MemoryRelationshipType;
282
+ }): {
283
+ sourceMemoryId: string;
284
+ targetMemoryId: string;
285
+ relationshipType: MemoryRelationshipType;
286
+ } {
287
+ const sourceMemoryId = normalizeUuid(input.sourceMemoryId, "source memory id");
288
+ const targetMemoryId = normalizeUuid(input.targetMemoryId, "target memory id");
289
+ if (sourceMemoryId === targetMemoryId) {
290
+ throw new Error("a memory relationship must connect two distinct memories");
291
+ }
292
+ if (!(MEMORY_RELATIONSHIP_TYPES as readonly string[]).includes(input.relationshipType)) {
293
+ throw new Error(`unsupported memory relationship type: ${input.relationshipType}`);
294
+ }
295
+ if (
296
+ SYMMETRIC_MEMORY_RELATIONSHIPS.has(input.relationshipType) &&
297
+ targetMemoryId < sourceMemoryId
298
+ ) {
299
+ return {
300
+ sourceMemoryId: targetMemoryId,
301
+ targetMemoryId: sourceMemoryId,
302
+ relationshipType: input.relationshipType,
303
+ };
304
+ }
305
+ return { sourceMemoryId, targetMemoryId, relationshipType: input.relationshipType };
306
+ }
307
+
308
+ /** Build the canonical, bounded operation plan consumed by the database lifecycle function. */
309
+ export function normalizeMemoryOperationPlan(input: MemoryOperationPlanInput): MemoryOperationPlan {
310
+ const base = {
311
+ operationId: normalizeUuid(input.operationId, "memory operation id"),
312
+ operationType: input.operationType,
313
+ targetMemoryId: normalizeUuid(input.targetMemoryId, "target memory id"),
314
+ expectedTargetVersion: normalizePositiveVersion(
315
+ input.expectedTargetVersion,
316
+ "expected target memory version",
317
+ ),
318
+ } as const;
319
+
320
+ switch (input.operationType) {
321
+ case "reclassify":
322
+ return {
323
+ ...base,
324
+ operationType: "reclassify",
325
+ scope: normalizeMemoryScope(input.scope),
326
+ namespace: normalizeMemoryNamespace(input.namespace),
327
+ labels: normalizeMemoryLabels(input.labels),
328
+ };
329
+ case "archive":
330
+ return { ...base, operationType: "archive" };
331
+ case "relationship_add":
332
+ case "relationship_remove": {
333
+ const relatedMemoryId = normalizeUuid(input.relatedMemoryId, "related memory id");
334
+ const expectedRelatedVersion = normalizePositiveVersion(
335
+ input.expectedRelatedVersion,
336
+ "expected related memory version",
337
+ );
338
+ const relationship = canonicalMemoryRelationship({
339
+ sourceMemoryId: base.targetMemoryId,
340
+ targetMemoryId: relatedMemoryId,
341
+ relationshipType: input.relationshipType,
342
+ });
343
+ const endpointsWereSwapped = relationship.sourceMemoryId !== base.targetMemoryId;
344
+ return {
345
+ ...base,
346
+ operationType: input.operationType,
347
+ targetMemoryId: relationship.sourceMemoryId,
348
+ relatedMemoryId: relationship.targetMemoryId,
349
+ expectedTargetVersion: endpointsWereSwapped
350
+ ? expectedRelatedVersion
351
+ : base.expectedTargetVersion,
352
+ expectedRelatedVersion: endpointsWereSwapped
353
+ ? base.expectedTargetVersion
354
+ : expectedRelatedVersion,
355
+ relationshipType: relationship.relationshipType,
356
+ };
357
+ }
358
+ case "supersede":
359
+ case "correct": {
360
+ const relatedMemoryId = normalizeUuid(input.relatedMemoryId, "replacement memory id");
361
+ if (relatedMemoryId === base.targetMemoryId) {
362
+ throw new Error("a memory cannot supersede or correct itself");
363
+ }
364
+ return {
365
+ ...base,
366
+ operationType: input.operationType,
367
+ relatedMemoryId,
368
+ expectedRelatedVersion: normalizePositiveVersion(
369
+ input.expectedRelatedVersion,
370
+ "expected replacement memory version",
371
+ ),
372
+ };
373
+ }
374
+ }
375
+ }
376
+
377
+ /** Stable SHA-256 identity for one already-normalized operation plan. */
378
+ export function hashMemoryOperationPlan(plan: MemoryOperationPlan): string {
379
+ return createHash("sha256").update(stableJson(plan), "utf8").digest("hex");
380
+ }
381
+
382
+ export function normalizeMemoryRevertPlan(input: MemoryRevertPlanInput): MemoryRevertPlan {
383
+ return {
384
+ operationId: normalizeUuid(input.operationId, "memory revert operation id"),
385
+ appliedOperationId: normalizeUuid(input.appliedOperationId, "applied memory operation id"),
386
+ };
387
+ }
388
+
389
+ /** Stable SHA-256 identity for one already-normalized revert plan. */
390
+ export function hashMemoryRevertPlan(plan: MemoryRevertPlan): string {
391
+ return createHash("sha256").update(stableJson(plan), "utf8").digest("hex");
392
+ }
393
+
35
394
  // ---------------------------------------------------------------------------
36
395
  // Kinds → block sections
37
396
  // ---------------------------------------------------------------------------
@@ -0,0 +1,162 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ check,
4
+ index,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ text,
9
+ timestamp,
10
+ uniqueIndex,
11
+ uuid,
12
+ } from "drizzle-orm/pg-core";
13
+
14
+ // Foreign keys, immutable-history triggers, and lifecycle-only mutation guards
15
+ // live in migration 0152. Keeping this leaf cycle-free lets schema.ts expose the
16
+ // additive memory governance tables without importing knowledgeMemories here.
17
+ export const knowledgeMemoryRelationships = pgTable(
18
+ "knowledge_memory_relationships",
19
+ {
20
+ id: uuid("id").primaryKey().defaultRandom(),
21
+ accountId: uuid("account_id").notNull(),
22
+ workspaceId: uuid("workspace_id").notNull(),
23
+ sourceMemoryId: uuid("source_memory_id").notNull(),
24
+ targetMemoryId: uuid("target_memory_id").notNull(),
25
+ relationshipType: text("relationship_type").notNull(),
26
+ version: integer("version").notNull().default(1),
27
+ createdByEventId: uuid("created_by_event_id").notNull(),
28
+ removedByEventId: uuid("removed_by_event_id"),
29
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
30
+ removedAt: timestamp("removed_at", { withTimezone: true }),
31
+ },
32
+ (table) => ({
33
+ activeDirectedEdge: uniqueIndex("knowledge_memory_relationships_active_edge_uq")
34
+ .on(table.workspaceId, table.sourceMemoryId, table.targetMemoryId, table.relationshipType)
35
+ .where(sql`${table.removedByEventId} is null`),
36
+ activeSymmetricEdge: uniqueIndex("knowledge_memory_relationships_active_symmetric_edge_uq")
37
+ .on(
38
+ table.workspaceId,
39
+ table.relationshipType,
40
+ sql`least(${table.sourceMemoryId}, ${table.targetMemoryId})`,
41
+ sql`greatest(${table.sourceMemoryId}, ${table.targetMemoryId})`,
42
+ )
43
+ .where(
44
+ sql`${table.removedByEventId} is null and ${table.relationshipType} in ('conflicts_with', 'related_to')`,
45
+ ),
46
+ source: index("knowledge_memory_relationships_source_idx").on(
47
+ table.workspaceId,
48
+ table.sourceMemoryId,
49
+ table.createdAt.desc(),
50
+ table.id,
51
+ ),
52
+ target: index("knowledge_memory_relationships_target_idx").on(
53
+ table.workspaceId,
54
+ table.targetMemoryId,
55
+ table.createdAt.desc(),
56
+ table.id,
57
+ ),
58
+ typeValid: check(
59
+ "knowledge_memory_relationships_type_chk",
60
+ sql`${table.relationshipType} in ('derived_from', 'supersedes', 'corrects', 'conflicts_with', 'related_to', 'depends_on', 'applies_to')`,
61
+ ),
62
+ distinctEndpoints: check(
63
+ "knowledge_memory_relationships_distinct_chk",
64
+ sql`${table.sourceMemoryId} <> ${table.targetMemoryId}`,
65
+ ),
66
+ versionPositive: check("knowledge_memory_relationships_version_chk", sql`${table.version} > 0`),
67
+ removedShape: check(
68
+ "knowledge_memory_relationships_removed_shape_chk",
69
+ sql`(${table.removedByEventId} is null and ${table.removedAt} is null) or (${table.removedByEventId} is not null and ${table.removedAt} is not null)`,
70
+ ),
71
+ workspaceIdentity: uniqueIndex("knowledge_memory_relationships_workspace_id_uq").on(
72
+ table.workspaceId,
73
+ table.id,
74
+ ),
75
+ }),
76
+ );
77
+
78
+ export const knowledgeMemoryLifecycleEvents = pgTable(
79
+ "knowledge_memory_lifecycle_events",
80
+ {
81
+ id: uuid("id").primaryKey().defaultRandom(),
82
+ accountId: uuid("account_id").notNull(),
83
+ workspaceId: uuid("workspace_id").notNull(),
84
+ operationId: uuid("operation_id").notNull(),
85
+ action: text("action").notNull(),
86
+ operationType: text("operation_type").notNull(),
87
+ targetMemoryId: uuid("target_memory_id").notNull(),
88
+ relatedMemoryId: uuid("related_memory_id"),
89
+ relationshipId: uuid("relationship_id"),
90
+ relationshipType: text("relationship_type"),
91
+ actorKind: text("actor_kind").notNull(),
92
+ actorSubjectId: text("actor_subject_id").notNull(),
93
+ actorSessionId: uuid("actor_session_id"),
94
+ actorTurnId: uuid("actor_turn_id"),
95
+ actorAttemptId: uuid("actor_attempt_id"),
96
+ actorExecutionGeneration: integer("actor_execution_generation"),
97
+ planHash: text("plan_hash").notNull(),
98
+ beforeState: jsonb("before_state").$type<Record<string, unknown>>().notNull(),
99
+ afterState: jsonb("after_state").$type<Record<string, unknown>>().notNull(),
100
+ revertsEventId: uuid("reverts_event_id"),
101
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
102
+ },
103
+ (table) => ({
104
+ operationAction: uniqueIndex("knowledge_memory_lifecycle_events_operation_action_uq").on(
105
+ table.workspaceId,
106
+ table.operationId,
107
+ table.action,
108
+ ),
109
+ oneRevert: uniqueIndex("knowledge_memory_lifecycle_events_one_revert_uq")
110
+ .on(table.revertsEventId)
111
+ .where(sql`${table.revertsEventId} is not null`),
112
+ targetTimeline: index("knowledge_memory_lifecycle_events_target_timeline_idx").on(
113
+ table.workspaceId,
114
+ table.targetMemoryId,
115
+ table.createdAt.desc(),
116
+ table.id,
117
+ ),
118
+ actorTimeline: index("knowledge_memory_lifecycle_events_actor_timeline_idx").on(
119
+ table.workspaceId,
120
+ table.actorKind,
121
+ table.actorSubjectId,
122
+ table.createdAt.desc(),
123
+ table.id,
124
+ ),
125
+ actionValid: check(
126
+ "knowledge_memory_lifecycle_events_action_chk",
127
+ sql`${table.action} in ('apply', 'revert')`,
128
+ ),
129
+ operationValid: check(
130
+ "knowledge_memory_lifecycle_events_operation_chk",
131
+ sql`${table.operationType} in ('reclassify', 'archive', 'relationship_add', 'relationship_remove', 'supersede', 'correct')`,
132
+ ),
133
+ relationshipShape: check(
134
+ "knowledge_memory_lifecycle_events_relationship_chk",
135
+ sql`(${table.operationType} in ('relationship_add', 'relationship_remove', 'supersede', 'correct') and ${table.relatedMemoryId} is not null and ${table.relationshipId} is not null and ${table.relationshipType} is not null) or (${table.operationType} in ('reclassify', 'archive') and ${table.relatedMemoryId} is null and ${table.relationshipId} is null and ${table.relationshipType} is null)`,
136
+ ),
137
+ actorValid: check(
138
+ "knowledge_memory_lifecycle_events_actor_chk",
139
+ sql`${table.actorKind} in ('subject', 'service') and length(btrim(${table.actorSubjectId})) between 1 and 1024`,
140
+ ),
141
+ attemptShape: check(
142
+ "knowledge_memory_lifecycle_events_attempt_shape_chk",
143
+ sql`(${table.actorSessionId} is null and ${table.actorTurnId} is null and ${table.actorAttemptId} is null and ${table.actorExecutionGeneration} is null) or (${table.actorSessionId} is not null and ${table.actorTurnId} is not null and ${table.actorAttemptId} is not null and ${table.actorExecutionGeneration} > 0)`,
144
+ ),
145
+ planHashShape: check(
146
+ "knowledge_memory_lifecycle_events_plan_hash_chk",
147
+ sql`${table.planHash} ~ '^[a-f0-9]{64}$'`,
148
+ ),
149
+ stateShape: check(
150
+ "knowledge_memory_lifecycle_events_state_chk",
151
+ sql`jsonb_typeof(${table.beforeState}) = 'object' and jsonb_typeof(${table.afterState}) = 'object' and not (${table.beforeState} ?| array['text', 'sourceRefs', 'source_refs', 'metadata', 'embedding']) and not (${table.afterState} ?| array['text', 'sourceRefs', 'source_refs', 'metadata', 'embedding'])`,
152
+ ),
153
+ revertShape: check(
154
+ "knowledge_memory_lifecycle_events_revert_shape_chk",
155
+ sql`(${table.action} = 'apply' and ${table.revertsEventId} is null) or (${table.action} = 'revert' and ${table.revertsEventId} is not null)`,
156
+ ),
157
+ workspaceIdentity: uniqueIndex("knowledge_memory_lifecycle_events_workspace_id_uq").on(
158
+ table.workspaceId,
159
+ table.id,
160
+ ),
161
+ }),
162
+ );