@opengeni/db 0.18.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/chunk-NK2A36KP.js +5822 -0
  2. package/dist/chunk-NK2A36KP.js.map +1 -0
  3. package/dist/{chunk-T6RSJT6C.js → chunk-OTJHD33E.js} +109 -1
  4. package/dist/chunk-OTJHD33E.js.map +1 -0
  5. package/dist/index.d.ts +25 -1
  6. package/dist/index.js +5761 -2827
  7. package/dist/index.js.map +1 -1
  8. package/dist/memory-domain.d.ts +110 -1
  9. package/dist/memory-governance-schema.d.ts +562 -0
  10. package/dist/memory-governance.d.ts +41 -0
  11. package/dist/provision-roles.js +1 -1
  12. package/dist/runtime-posture.d.ts +3 -3
  13. package/dist/schema.d.ts +425 -0
  14. package/dist/schema.js +37 -1
  15. package/dist/scoped-knowledge-schema.d.ts +5341 -0
  16. package/dist/scoped-knowledge.d.ts +208 -0
  17. package/dist/session-queue-commands.d.ts +2 -1
  18. package/drizzle/0152_hierarchical_memory_foundation.sql +1589 -0
  19. package/drizzle/0153_mcp_personal_connection_delegations.sql +181 -0
  20. package/drizzle/0154_scoped_knowledge_foundation.sql +2292 -0
  21. package/package.json +3 -3
  22. package/src/connection-token-resolver.ts +3 -0
  23. package/src/index.ts +351 -12
  24. package/src/memory-domain.ts +360 -1
  25. package/src/memory-governance-schema.ts +162 -0
  26. package/src/memory-governance.ts +317 -0
  27. package/src/provision-roles.ts +72 -0
  28. package/src/runtime-posture.ts +36 -0
  29. package/src/schema.ts +87 -4
  30. package/src/scoped-knowledge-schema.ts +603 -0
  31. package/src/scoped-knowledge.ts +2891 -0
  32. package/src/session-queue-commands.ts +48 -0
  33. package/dist/chunk-T6RSJT6C.js.map +0 -1
  34. package/dist/chunk-YKWJ7QJ2.js +0 -5000
  35. package/dist/chunk-YKWJ7QJ2.js.map +0 -1
@@ -0,0 +1,317 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { Database } from "./index";
3
+ import { setSubjectRlsContext, withWorkspaceRls } from "./index";
4
+ import {
5
+ hashMemoryOperationPlan,
6
+ hashMemoryRevertPlan,
7
+ normalizeMemoryOperationPlan,
8
+ normalizeMemoryRevertPlan,
9
+ normalizeMemoryRoleKey,
10
+ type MemoryOperationPlanInput,
11
+ type MemoryRevertPlanInput,
12
+ } from "./memory-domain";
13
+
14
+ export class MemoryGovernanceAuthorityError extends Error {
15
+ readonly name = "MemoryGovernanceAuthorityError";
16
+ }
17
+
18
+ export type MemoryGovernanceDirectSubjectAuthority = {
19
+ kind: "subject";
20
+ accountId: string;
21
+ workspaceId: string;
22
+ subjectId: string;
23
+ };
24
+
25
+ export type MemoryGovernanceDirectServiceAuthority = {
26
+ kind: "service";
27
+ accountId: string;
28
+ workspaceId: string;
29
+ serviceId: string;
30
+ };
31
+
32
+ export type MemoryGovernanceAttemptAuthority = {
33
+ kind: "attempt";
34
+ accountId: string;
35
+ workspaceId: string;
36
+ sessionId: string;
37
+ turnId: string;
38
+ attemptId: string;
39
+ executionGeneration: number;
40
+ };
41
+
42
+ export type MemoryGovernanceAuthority =
43
+ | MemoryGovernanceDirectSubjectAuthority
44
+ | MemoryGovernanceDirectServiceAuthority
45
+ | MemoryGovernanceAttemptAuthority;
46
+
47
+ type ResolvedMemoryGovernanceAuthority = {
48
+ accountId: string;
49
+ workspaceId: string;
50
+ actorKind: "subject" | "service";
51
+ actorSubjectId: string;
52
+ sessionId: string | null;
53
+ turnId: string | null;
54
+ attemptId: string | null;
55
+ executionGeneration: number | null;
56
+ roleKey: string | null;
57
+ };
58
+
59
+ function requireBoundedActorId(value: string, label: string): string {
60
+ const normalized = value.trim();
61
+ if (!normalized || normalized.length > 1024) {
62
+ throw new MemoryGovernanceAuthorityError(`${label} must be a non-empty bounded identifier`);
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ async function setAndVerifyMemoryGovernanceContext(
68
+ db: Database,
69
+ authority: ResolvedMemoryGovernanceAuthority,
70
+ ): Promise<void> {
71
+ if (authority.actorKind === "subject") {
72
+ await setSubjectRlsContext(db, authority.actorSubjectId);
73
+ } else {
74
+ await db.execute(sql`select set_config('opengeni.subject_id', '', true)`);
75
+ }
76
+ await db.execute(sql`
77
+ select
78
+ set_config('opengeni.memory_actor_kind', ${authority.actorKind}, true),
79
+ set_config('opengeni.memory_actor_id', ${authority.actorSubjectId}, true),
80
+ set_config('opengeni.memory_session_id', ${authority.sessionId ?? ""}, true),
81
+ set_config('opengeni.memory_role_key', ${authority.roleKey ?? ""}, true)
82
+ `);
83
+ const rows = (await db.execute(sql`
84
+ select
85
+ nullif(current_setting('opengeni.account_id', true), '') as account_id,
86
+ nullif(current_setting('opengeni.workspace_id', true), '') as workspace_id,
87
+ nullif(current_setting('opengeni.subject_id', true), '') as subject_id,
88
+ nullif(current_setting('opengeni.memory_actor_kind', true), '') as actor_kind,
89
+ nullif(current_setting('opengeni.memory_actor_id', true), '') as actor_id,
90
+ nullif(current_setting('opengeni.memory_session_id', true), '') as session_id,
91
+ nullif(current_setting('opengeni.memory_role_key', true), '') as role_key
92
+ `)) as unknown as Array<{
93
+ account_id: string | null;
94
+ workspace_id: string | null;
95
+ subject_id: string | null;
96
+ actor_kind: string | null;
97
+ actor_id: string | null;
98
+ session_id: string | null;
99
+ role_key: string | null;
100
+ }>;
101
+ const applied = rows[0];
102
+ if (
103
+ !applied ||
104
+ applied.account_id !== authority.accountId ||
105
+ applied.workspace_id !== authority.workspaceId ||
106
+ applied.subject_id !== (authority.actorKind === "subject" ? authority.actorSubjectId : null) ||
107
+ applied.actor_kind !== authority.actorKind ||
108
+ applied.actor_id !== authority.actorSubjectId ||
109
+ applied.session_id !== authority.sessionId ||
110
+ applied.role_key !== authority.roleKey
111
+ ) {
112
+ throw new MemoryGovernanceAuthorityError(
113
+ "Memory governance authority was not applied on the active database backend",
114
+ );
115
+ }
116
+ }
117
+
118
+ async function resolveAttemptAuthority(
119
+ db: Database,
120
+ input: MemoryGovernanceAttemptAuthority,
121
+ ): Promise<ResolvedMemoryGovernanceAuthority> {
122
+ if (!Number.isSafeInteger(input.executionGeneration) || input.executionGeneration <= 0) {
123
+ throw new MemoryGovernanceAuthorityError(
124
+ "Memory governance attempt requires a positive execution generation",
125
+ );
126
+ }
127
+ const rows = (await db.execute(sql`
128
+ with locked_workspace as materialized (
129
+ select workspace.id, workspace.account_id
130
+ from workspaces workspace
131
+ where workspace.id = ${input.workspaceId}::uuid
132
+ and workspace.account_id = ${input.accountId}::uuid
133
+ for key share of workspace
134
+ ), locked_session as materialized (
135
+ select session.id, session.account_id, session.workspace_id,
136
+ session.active_turn_id, session.metadata ->> 'memoryRoleKey' as memory_role_key
137
+ from sessions session
138
+ join locked_workspace workspace
139
+ on workspace.id = session.workspace_id
140
+ and workspace.account_id = session.account_id
141
+ where session.id = ${input.sessionId}::uuid
142
+ and session.active_turn_id = ${input.turnId}::uuid
143
+ for share of session
144
+ ), locked_turn as materialized (
145
+ select turn.id, turn.account_id, turn.workspace_id, turn.session_id,
146
+ turn.active_attempt_id, turn.execution_generation,
147
+ turn.initiator_kind, turn.initiator_subject_id
148
+ from session_turns turn
149
+ join locked_session session
150
+ on session.id = turn.session_id
151
+ and session.workspace_id = turn.workspace_id
152
+ and session.account_id = turn.account_id
153
+ where turn.id = ${input.turnId}::uuid
154
+ and turn.active_attempt_id = ${input.attemptId}::uuid
155
+ and turn.execution_generation = ${input.executionGeneration}
156
+ and turn.status in ('running', 'requires_action', 'recovering', 'waiting_capacity')
157
+ and turn.initiator_kind in ('subject', 'service')
158
+ and length(btrim(turn.initiator_subject_id)) between 1 and 1024
159
+ for share of turn
160
+ ), locked_attempt as materialized (
161
+ select attempt.id, attempt.account_id, attempt.workspace_id,
162
+ attempt.session_id, attempt.turn_id, attempt.execution_generation
163
+ from session_turn_attempts attempt
164
+ join locked_turn turn
165
+ on turn.id = attempt.turn_id
166
+ and turn.session_id = attempt.session_id
167
+ and turn.workspace_id = attempt.workspace_id
168
+ and turn.account_id = attempt.account_id
169
+ where attempt.id = ${input.attemptId}::uuid
170
+ and attempt.execution_generation = ${input.executionGeneration}
171
+ and attempt.state in ('claimed', 'running')
172
+ and not exists (
173
+ select 1
174
+ from session_attempt_interruptions interruption
175
+ where interruption.workspace_id = attempt.workspace_id
176
+ and interruption.attempt_id = attempt.id
177
+ and interruption.state in ('pending', 'delivered', 'acknowledged')
178
+ )
179
+ for share of attempt
180
+ )
181
+ select turn.initiator_kind, turn.initiator_subject_id, session.memory_role_key
182
+ from locked_workspace workspace
183
+ join locked_session session on true
184
+ join locked_turn turn on true
185
+ join locked_attempt attempt on true
186
+ where workspace.account_id = attempt.account_id
187
+ and workspace.id = attempt.workspace_id
188
+ and session.id = attempt.session_id
189
+ and turn.id = attempt.turn_id
190
+ `)) as unknown as Array<{
191
+ initiator_kind: "subject" | "service";
192
+ initiator_subject_id: string;
193
+ memory_role_key: string | null;
194
+ }>;
195
+ const row = rows[0];
196
+ if (!row) {
197
+ throw new MemoryGovernanceAuthorityError(
198
+ "Memory governance requires the exact current attempt, generation, and immutable initiator",
199
+ );
200
+ }
201
+ let roleKey: string | null = null;
202
+ if (row.memory_role_key !== null) {
203
+ try {
204
+ roleKey = normalizeMemoryRoleKey(row.memory_role_key);
205
+ } catch {
206
+ throw new MemoryGovernanceAuthorityError(
207
+ "Persisted session memoryRoleKey is invalid; role-scoped authority fails closed",
208
+ );
209
+ }
210
+ if (roleKey !== row.memory_role_key) {
211
+ throw new MemoryGovernanceAuthorityError(
212
+ "Persisted session memoryRoleKey is not canonical; role-scoped authority fails closed",
213
+ );
214
+ }
215
+ }
216
+ return {
217
+ accountId: input.accountId,
218
+ workspaceId: input.workspaceId,
219
+ actorKind: row.initiator_kind,
220
+ actorSubjectId: row.initiator_subject_id,
221
+ sessionId: input.sessionId,
222
+ turnId: input.turnId,
223
+ attemptId: input.attemptId,
224
+ executionGeneration: input.executionGeneration,
225
+ roleKey,
226
+ };
227
+ }
228
+
229
+ async function withMemoryGovernanceAuthority<T>(
230
+ db: Database,
231
+ authority: MemoryGovernanceAuthority,
232
+ fn: (db: Database, authority: ResolvedMemoryGovernanceAuthority) => Promise<T>,
233
+ ): Promise<T> {
234
+ return await withWorkspaceRls(db, authority.workspaceId, async (scopedDb) => {
235
+ let resolved: ResolvedMemoryGovernanceAuthority;
236
+ if (authority.kind === "attempt") {
237
+ resolved = await resolveAttemptAuthority(scopedDb, authority);
238
+ } else {
239
+ const actorSubjectId = requireBoundedActorId(
240
+ authority.kind === "subject" ? authority.subjectId : authority.serviceId,
241
+ authority.kind === "subject" ? "subject id" : "service id",
242
+ );
243
+ resolved = {
244
+ accountId: authority.accountId,
245
+ workspaceId: authority.workspaceId,
246
+ actorKind: authority.kind,
247
+ actorSubjectId,
248
+ sessionId: null,
249
+ turnId: null,
250
+ attemptId: null,
251
+ executionGeneration: null,
252
+ roleKey: null,
253
+ };
254
+ }
255
+ await setAndVerifyMemoryGovernanceContext(scopedDb, resolved);
256
+ return await fn(scopedDb, resolved);
257
+ });
258
+ }
259
+
260
+ export async function applyKnowledgeMemoryOperation(
261
+ db: Database,
262
+ input: {
263
+ authority: MemoryGovernanceAuthority;
264
+ plan: MemoryOperationPlanInput;
265
+ },
266
+ ): Promise<{ eventId: string; planHash: string }> {
267
+ const plan = normalizeMemoryOperationPlan(input.plan);
268
+ const planHash = hashMemoryOperationPlan(plan);
269
+ return await withMemoryGovernanceAuthority(db, input.authority, async (scopedDb, authority) => {
270
+ const rows = (await scopedDb.execute(sql`
271
+ select event_id
272
+ from knowledge_memory_apply_operation(
273
+ ${JSON.stringify(plan)}::jsonb,
274
+ ${planHash},
275
+ ${authority.actorKind},
276
+ ${authority.actorSubjectId},
277
+ ${authority.sessionId}::uuid,
278
+ ${authority.turnId}::uuid,
279
+ ${authority.attemptId}::uuid,
280
+ ${authority.executionGeneration}::integer
281
+ )
282
+ `)) as unknown as Array<{ event_id: string }>;
283
+ const eventId = rows[0]?.event_id;
284
+ if (!eventId) throw new Error("Memory governance apply operation returned no event");
285
+ return { eventId, planHash };
286
+ });
287
+ }
288
+
289
+ export async function revertKnowledgeMemoryOperation(
290
+ db: Database,
291
+ input: {
292
+ authority: MemoryGovernanceAuthority;
293
+ plan: MemoryRevertPlanInput;
294
+ },
295
+ ): Promise<{ eventId: string; planHash: string }> {
296
+ const plan = normalizeMemoryRevertPlan(input.plan);
297
+ const planHash = hashMemoryRevertPlan(plan);
298
+ return await withMemoryGovernanceAuthority(db, input.authority, async (scopedDb, authority) => {
299
+ const rows = (await scopedDb.execute(sql`
300
+ select event_id
301
+ from knowledge_memory_revert_operation(
302
+ ${plan.operationId}::uuid,
303
+ ${plan.appliedOperationId}::uuid,
304
+ ${planHash},
305
+ ${authority.actorKind},
306
+ ${authority.actorSubjectId},
307
+ ${authority.sessionId}::uuid,
308
+ ${authority.turnId}::uuid,
309
+ ${authority.attemptId}::uuid,
310
+ ${authority.executionGeneration}::integer
311
+ )
312
+ `)) as unknown as Array<{ event_id: string }>;
313
+ const eventId = rows[0]?.event_id;
314
+ if (!eventId) throw new Error("Memory governance revert operation returned no event");
315
+ return { eventId, planHash };
316
+ });
317
+ }
@@ -433,6 +433,30 @@ BEGIN
433
433
  ${literal(role)}
434
434
  );
435
435
  END IF;
436
+ IF to_regprocedure(
437
+ format(
438
+ '%I.knowledge_memory_apply_operation(jsonb,text,text,text,uuid,uuid,uuid,integer)',
439
+ ${literal(schema)}
440
+ )
441
+ ) IS NOT NULL THEN
442
+ EXECUTE format(
443
+ 'GRANT EXECUTE ON FUNCTION %I.knowledge_memory_apply_operation(jsonb, text, text, text, uuid, uuid, uuid, integer) TO %I',
444
+ ${literal(schema)},
445
+ ${literal(role)}
446
+ );
447
+ END IF;
448
+ IF to_regprocedure(
449
+ format(
450
+ '%I.knowledge_memory_revert_operation(uuid,uuid,text,text,text,uuid,uuid,uuid,integer)',
451
+ ${literal(schema)}
452
+ )
453
+ ) IS NOT NULL THEN
454
+ EXECUTE format(
455
+ 'GRANT EXECUTE ON FUNCTION %I.knowledge_memory_revert_operation(uuid, uuid, text, text, text, uuid, uuid, uuid, integer) TO %I',
456
+ ${literal(schema)},
457
+ ${literal(role)}
458
+ );
459
+ END IF;
436
460
  IF to_regprocedure(
437
461
  format(
438
462
  '%I.preference_registry_get_or_create_snapshot(uuid,uuid,uuid,uuid,uuid,integer)',
@@ -445,6 +469,54 @@ BEGIN
445
469
  ${literal(role)}
446
470
  );
447
471
  END IF;
472
+ IF to_regprocedure(
473
+ format(
474
+ '%I.scoped_knowledge_apply_lifecycle(uuid,text,uuid,text,bigint,text,text,text,text,text,text)',
475
+ ${literal(schema)}
476
+ )
477
+ ) IS NOT NULL THEN
478
+ EXECUTE format(
479
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_apply_lifecycle(uuid, text, uuid, text, bigint, text, text, text, text, text, text) TO %I',
480
+ ${literal(schema)},
481
+ ${literal(role)}
482
+ );
483
+ END IF;
484
+ IF to_regprocedure(
485
+ format(
486
+ '%I.scoped_knowledge_advance_source_acl(uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text)',
487
+ ${literal(schema)}
488
+ )
489
+ ) IS NOT NULL THEN
490
+ EXECUTE format(
491
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_advance_source_acl(uuid, uuid, bigint, bigint, uuid, text, text, text, text, text, text) TO %I',
492
+ ${literal(schema)},
493
+ ${literal(role)}
494
+ );
495
+ END IF;
496
+ IF to_regprocedure(
497
+ format(
498
+ '%I.scoped_knowledge_complete_sync(uuid,uuid,text,text,timestamptz,jsonb,text,text,text)',
499
+ ${literal(schema)}
500
+ )
501
+ ) IS NOT NULL THEN
502
+ EXECUTE format(
503
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_complete_sync(uuid, uuid, text, text, timestamptz, jsonb, text, text, text) TO %I',
504
+ ${literal(schema)},
505
+ ${literal(role)}
506
+ );
507
+ END IF;
508
+ IF to_regprocedure(
509
+ format(
510
+ '%I.scoped_knowledge_advance_object_version(uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text)',
511
+ ${literal(schema)}
512
+ )
513
+ ) IS NOT NULL THEN
514
+ EXECUTE format(
515
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_advance_object_version(uuid, uuid, bigint, bigint, uuid, text, text, text, text, text, text) TO %I',
516
+ ${literal(schema)},
517
+ ${literal(role)}
518
+ );
519
+ END IF;
448
520
  EXECUTE format('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});
449
521
  EXECUTE format(
450
522
  'ALTER DEFAULT PRIVILEGES FOR ROLE %I IN SCHEMA %I REVOKE ALL PRIVILEGES ON TABLES FROM %I',
@@ -42,7 +42,25 @@ export const FORCE_RLS_TABLES = [
42
42
  "host_export_outbox",
43
43
  "import_batches",
44
44
  "integration_oauth_state_nonces",
45
+ "knowledge_change_proposals",
46
+ "knowledge_claim_evidence",
47
+ "knowledge_claim_relations",
48
+ "knowledge_claim_reviews",
49
+ "knowledge_claims",
50
+ "knowledge_document_versions",
51
+ "knowledge_entities",
52
+ "knowledge_entity_aliases",
53
+ "knowledge_facts",
54
+ "knowledge_lifecycle_events",
45
55
  "knowledge_memories",
56
+ "knowledge_memory_lifecycle_events",
57
+ "knowledge_memory_relationships",
58
+ "knowledge_operation_receipts",
59
+ "knowledge_providers",
60
+ "knowledge_source_acl_versions",
61
+ "knowledge_source_objects",
62
+ "knowledge_sources",
63
+ "knowledge_sync_runs",
46
64
  "machine_metrics_latest",
47
65
  "machine_metrics_series",
48
66
  "model_call_facts",
@@ -229,6 +247,9 @@ export const RUNTIME_FULL_DML_TABLES = [
229
247
 
230
248
  /** Configuration and lifecycle-owned audit rows are read-only at runtime. */
231
249
  export const RUNTIME_READ_ONLY_TABLES = [
250
+ "knowledge_lifecycle_events",
251
+ "knowledge_memory_lifecycle_events",
252
+ "knowledge_memory_relationships",
232
253
  "nested_agent_depth_configuration",
233
254
  "preference_registry_events",
234
255
  "preference_registry_snapshots",
@@ -236,6 +257,21 @@ export const RUNTIME_READ_ONLY_TABLES = [
236
257
 
237
258
  /** Append-only evidence/revision tables are insertable and queryable, never mutable. */
238
259
  export const RUNTIME_READ_INSERT_TABLES = [
260
+ "knowledge_change_proposals",
261
+ "knowledge_claim_evidence",
262
+ "knowledge_claim_relations",
263
+ "knowledge_claim_reviews",
264
+ "knowledge_claims",
265
+ "knowledge_document_versions",
266
+ "knowledge_entities",
267
+ "knowledge_entity_aliases",
268
+ "knowledge_facts",
269
+ "knowledge_operation_receipts",
270
+ "knowledge_providers",
271
+ "knowledge_source_acl_versions",
272
+ "knowledge_source_objects",
273
+ "knowledge_sources",
274
+ "knowledge_sync_runs",
239
275
  "preference_registry_preferences",
240
276
  "preference_registry_revisions",
241
277
  "session_spawn_denials",
package/src/schema.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  FirstPartyMcpToolName,
3
+ McpPersonalConnectionDelegation,
3
4
  McpServerConnectionRef,
4
5
  SessionMcpApprovalPolicy,
5
6
  } from "@opengeni/contracts";
@@ -1316,6 +1317,14 @@ export const sessions = pgTable(
1316
1317
  // Exact model-visible first-party tool selection. All catalogued tools are
1317
1318
  // selected by default; [] intentionally selects no broad-server tools.
1318
1319
  firstPartyMcpTools: jsonb("first_party_mcp_tools").$type<FirstPartyMcpToolName[]>().notNull(),
1320
+ // Initial-command staging only. initializeSessionStartAtomically copies
1321
+ // this immutable snapshot onto the first turn so create repair survives a
1322
+ // crash between the session insert and first-turn transaction. Runtime
1323
+ // credential authority must never read this session field.
1324
+ initialPersonalConnectionDelegations: jsonb("initial_personal_connection_delegations")
1325
+ .$type<McpPersonalConnectionDelegation[]>()
1326
+ .notNull()
1327
+ .default([]),
1319
1328
  // Durable tool-policy origin. Migration 0136 removes the old null/legacy
1320
1329
  // representation so every session has one explicit policy mode.
1321
1330
  toolPolicy: jsonb("tool_policy").$type<SessionToolPolicy>().notNull(),
@@ -1325,9 +1334,15 @@ export const sessions = pgTable(
1325
1334
  // when the creating grant carried a worker-signed sessionId claim (a session
1326
1335
  // spawning a worker); null for direct API creates and scheduled-task runs.
1327
1336
  // When set, this worker's terminal-for-now transitions wake the parent so a
1328
- // manager can orchestrate workers without busy-polling. Self-referencing FK,
1329
- // ON DELETE SET NULL so deleting a manager never cascades into its workers.
1337
+ // manager can orchestrate workers without busy-polling. The migration-owned
1338
+ // self-reference uses ON DELETE RESTRICT so immutable hierarchy and
1339
+ // authority lineage cannot be silently orphaned by a parent hard delete.
1330
1340
  parentSessionId: uuid("parent_session_id"),
1341
+ // Exact parent turn whose worker-signed attempt created this child. This is
1342
+ // private immutable authority lineage: child completion copies personal MCP
1343
+ // authority from that turn, never from whichever parent turn ran most
1344
+ // recently. Null for top-level and legacy child sessions.
1345
+ parentTurnId: uuid("parent_turn_id"),
1331
1346
  // Workspace-scoped CREATE idempotency key. NULL means the create carried no
1332
1347
  // key (each such create is independent). When set, the partial unique index
1333
1348
  // below collapses concurrent/retried creates with the same key in the same
@@ -1885,6 +1900,21 @@ export const knowledgeMemories = pgTable(
1885
1900
  supersededById: uuid("superseded_by_id"),
1886
1901
  validFrom: timestamp("valid_from", { withTimezone: true }).notNull().defaultNow(),
1887
1902
  validUntil: timestamp("valid_until", { withTimezone: true }),
1903
+ // Hierarchical memory foundation (migration 0152). `scope` remains the V1
1904
+ // compatibility projection; typed selectors are the fail-closed authority.
1905
+ scopeType: text("scope_type").notNull().default("workspace"),
1906
+ scopeSubjectId: text("scope_subject_id"),
1907
+ scopeRoleKey: text("scope_role_key"),
1908
+ scopeSessionId: uuid("scope_session_id"),
1909
+ namespace: text("namespace_key").notNull().default("general"),
1910
+ labels: text("labels").array().notNull().default([]),
1911
+ memoryVersion: integer("memory_version").notNull().default(1),
1912
+ createdByKind: text("created_by_kind").notNull().default("service"),
1913
+ createdBySubjectId: text("created_by_subject_id").notNull().default("unattributed-legacy"),
1914
+ createdByContext: jsonb("created_by_context")
1915
+ .$type<Record<string, unknown>>()
1916
+ .notNull()
1917
+ .default({ backfill: true }),
1888
1918
  // sha256(normalizeMemoryText(text)) — exact-dedup key; see memory-domain.
1889
1919
  textHash: text("text_hash"),
1890
1920
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -1901,6 +1931,18 @@ export const knowledgeMemories = pgTable(
1901
1931
  table.workspaceId,
1902
1932
  table.scope,
1903
1933
  ),
1934
+ workspaceTypedScope: index("knowledge_memories_workspace_typed_scope_idx").on(
1935
+ table.workspaceId,
1936
+ table.scopeType,
1937
+ table.scopeSubjectId,
1938
+ table.scopeRoleKey,
1939
+ table.scopeSessionId,
1940
+ ),
1941
+ workspaceNamespace: index("knowledge_memories_workspace_namespace_idx").on(
1942
+ table.workspaceId,
1943
+ table.namespace,
1944
+ ),
1945
+ labelsGin: index("knowledge_memories_labels_idx").using("gin", table.labels),
1904
1946
  createdBySession: index("knowledge_memories_workspace_created_by_session_idx").on(
1905
1947
  table.workspaceId,
1906
1948
  table.createdBySessionId,
@@ -1913,8 +1955,18 @@ export const knowledgeMemories = pgTable(
1913
1955
  table.workspaceId,
1914
1956
  table.textHash,
1915
1957
  ),
1916
- workspaceVisibleTextHashUnique: uniqueIndex("knowledge_memories_workspace_visible_text_hash_uq")
1917
- .on(table.workspaceId, table.textHash)
1958
+ // Drizzle 0.45 cannot encode PostgreSQL `NULLS NOT DISTINCT`; migration
1959
+ // 0152 owns that option so nullable scope selectors remain one identity.
1960
+ scopeVisibleTextHashUnique: uniqueIndex("knowledge_memories_scope_visible_text_hash_uq")
1961
+ .on(
1962
+ table.workspaceId,
1963
+ table.scopeType,
1964
+ table.scopeSubjectId,
1965
+ table.scopeRoleKey,
1966
+ table.scopeSessionId,
1967
+ table.namespace,
1968
+ table.textHash,
1969
+ )
1918
1970
  .where(sql`${table.status} in ('active', 'approved') and ${table.textHash} is not null`),
1919
1971
  }),
1920
1972
  );
@@ -1971,6 +2023,13 @@ export const sessionTurns = pgTable(
1971
2023
  .$type<Record<string, unknown>>()
1972
2024
  .notNull()
1973
2025
  .default({ backfill: true }),
2026
+ // Immutable exact personal MCP authority for this logical turn. Recovery,
2027
+ // approval, retries, and Toolspace reuse this row; no runtime may infer
2028
+ // broader authority from the session creator or mutable session state.
2029
+ personalConnectionDelegations: jsonb("personal_connection_delegations")
2030
+ .$type<McpPersonalConnectionDelegation[]>()
2031
+ .notNull()
2032
+ .default([]),
1974
2033
  cancelledBy: text("cancelled_by"),
1975
2034
  cancelReason: text("cancel_reason"),
1976
2035
  // Atomic per-turn toolspace call budget counter (migration 0043). Incremented
@@ -2410,6 +2469,12 @@ export const sessionSystemUpdates = pgTable(
2410
2469
  summary: text("summary").notNull(),
2411
2470
  payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
2412
2471
  lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
2472
+ // Private immutable authority frozen when this machine input is accepted.
2473
+ // Public projections intentionally omit connection ids and owner subjects.
2474
+ personalConnectionDelegations: jsonb("personal_connection_delegations")
2475
+ .$type<McpPersonalConnectionDelegation[]>()
2476
+ .notNull()
2477
+ .default([]),
2413
2478
  // pending is visible queue truth; delivered means its exact model-memory
2414
2479
  // batch was durably claimed. Terminal cancellation/supersession is explicit.
2415
2480
  state: text("state").notNull().default("pending"),
@@ -2489,6 +2554,12 @@ export const sessionSystemUpdateOutbox = pgTable(
2489
2554
  summary: text("summary").notNull(),
2490
2555
  payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default({}),
2491
2556
  lineage: jsonb("lineage").$type<Record<string, unknown>>().notNull().default({}),
2557
+ // Exact private authority copied from the causal parent turn in the source
2558
+ // terminal transaction. Delivery retries cannot replace this snapshot.
2559
+ personalConnectionDelegations: jsonb("personal_connection_delegations")
2560
+ .$type<McpPersonalConnectionDelegation[]>()
2561
+ .notNull()
2562
+ .default([]),
2492
2563
  status: text("status").notNull().default("pending"),
2493
2564
  attempts: integer("attempts").notNull().default(0),
2494
2565
  updateId: uuid("update_id"),
@@ -4288,6 +4359,16 @@ export const scheduledTasks = pgTable(
4288
4359
  runMode: text("run_mode").notNull().default("new_session_per_run"),
4289
4360
  overlapPolicy: text("overlap_policy").notNull().default("allow_concurrent"),
4290
4361
  agentConfig: jsonb("agent_config").$type<unknown>().notNull(),
4362
+ createdByKind: text("created_by_kind").notNull().default("service"),
4363
+ createdBySubjectId: text("created_by_subject_id").notNull().default("unattributed-legacy"),
4364
+ createdByContext: jsonb("created_by_context")
4365
+ .$type<Record<string, unknown>>()
4366
+ .notNull()
4367
+ .default({ backfill: true }),
4368
+ personalConnectionDelegations: jsonb("personal_connection_delegations")
4369
+ .$type<McpPersonalConnectionDelegation[]>()
4370
+ .notNull()
4371
+ .default([]),
4291
4372
  reusableSessionId: uuid("reusable_session_id").references(() => sessions.id, {
4292
4373
  onDelete: "set null",
4293
4374
  }),
@@ -5305,3 +5386,5 @@ export const rigChanges = pgTable(
5305
5386
 
5306
5387
  export * from "./workspace-instruction-policies-schema";
5307
5388
  export * from "./preference-registry-schema";
5389
+ export * from "./memory-governance-schema";
5390
+ export * from "./scoped-knowledge-schema";