@opengeni/db 4.4.0-canary.0 → 4.4.0-canary.2
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/canonical-human-identities.js +4 -3
- package/dist/{chunk-2ZMTQVTR.js → chunk-2KOLL33E.js} +2 -2
- package/dist/chunk-45CP2YKY.js +1062 -0
- package/dist/chunk-45CP2YKY.js.map +1 -0
- package/dist/{chunk-6TKL3GQO.js → chunk-5UUWZDAV.js} +6531 -6480
- package/dist/chunk-5UUWZDAV.js.map +1 -0
- package/dist/{chunk-QLA3UQNX.js → chunk-6IJD3XXF.js} +17 -3
- package/dist/chunk-6IJD3XXF.js.map +1 -0
- package/dist/{chunk-232VC2LW.js → chunk-6V3HDEMR.js} +2 -2
- package/dist/{chunk-M7PSMVFF.js → chunk-CHBLBEU6.js} +2 -2
- package/dist/chunk-M4PKBJRS.js +20318 -0
- package/dist/chunk-M4PKBJRS.js.map +1 -0
- package/dist/chunk-S2TZ3F4L.js +237 -0
- package/dist/chunk-S2TZ3F4L.js.map +1 -0
- package/dist/{chunk-UK57QA2H.js → chunk-UOXWVP3T.js} +2 -2
- package/dist/{chunk-GVZJ7XSR.js → chunk-WFU6PS54.js} +11 -4
- package/dist/chunk-WFU6PS54.js.map +1 -0
- package/dist/{chunk-THJGMEK5.js → chunk-XJP7KJ6U.js} +121 -66
- package/dist/chunk-XJP7KJ6U.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +2 -0
- package/dist/database.d.ts +2 -0
- package/dist/editable-artifact-durable-export.js +3 -2
- package/dist/editable-artifact-durable-export.js.map +1 -1
- package/dist/editable-artifacts.js +4 -3
- package/dist/external-identities.d.ts +1 -0
- package/dist/external-membership-operations.d.ts +44 -0
- package/dist/governed-learning-evaluator.d.ts +1 -14
- package/dist/host-mcp-bindings.d.ts +4 -0
- package/dist/host-mcp-resolvers.d.ts +47 -0
- package/dist/host-mcp-task-authority.d.ts +2 -6
- package/dist/index.d.ts +29 -31
- package/dist/index.js +8616 -8321
- package/dist/index.js.map +1 -1
- package/dist/knowledge-document-preparation.d.ts +56 -0
- package/dist/knowledge-entries-schema.d.ts +2479 -0
- package/dist/knowledge-entries.d.ts +421 -0
- package/dist/knowledge-indexing.d.ts +99 -0
- package/dist/knowledge-migration.d.ts +6 -0
- package/dist/managed-auth-session-sets.js +4 -3
- package/dist/mcp-operations.js +3 -2
- package/dist/mcp-operations.js.map +1 -1
- package/dist/migrate.js +2 -1
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +3 -2
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/runtime-posture.d.ts +7 -7
- package/dist/schema.d.ts +115 -12
- package/dist/schema.js +26 -1
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -6
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-event-slices.js +6 -4
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-tenancy.js +4 -3
- package/dist/video-generation.js +4 -3
- package/dist/workspace-artifacts.d.ts +1 -0
- package/dist/workspace-instruction-policies-schema.d.ts +21 -2
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-membership-access.d.ts +11 -0
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0461_unified_knowledge.sql +2859 -0
- package/drizzle/0462_agent_instruction_non_destructive_edits.sql +212 -0
- package/drizzle/0463_host_mcp_resolver_registration.sql +111 -0
- package/drizzle/0464_external_workspace_membership_operations.sql +166 -0
- package/package.json +6 -6
- package/src/connection-token-resolver.ts +75 -6
- package/src/database.ts +12 -1
- package/src/external-identities.ts +1 -0
- package/src/external-membership-operations.ts +134 -0
- package/src/governed-learning-evaluator.ts +0 -69
- package/src/host-mcp-binding-match.ts +1 -0
- package/src/host-mcp-bindings.ts +24 -1
- package/src/host-mcp-resolvers.ts +266 -0
- package/src/host-mcp-task-authority.ts +11 -10
- package/src/index.ts +307 -128
- package/src/knowledge-document-preparation.ts +90 -0
- package/src/knowledge-entries-schema.ts +264 -0
- package/src/knowledge-entries.ts +860 -0
- package/src/knowledge-indexing.ts +126 -0
- package/src/knowledge-migration.ts +761 -0
- package/src/knowledge-source-sync.ts +46 -7
- package/src/migrate.ts +19 -0
- package/src/provision-roles.ts +23 -57
- package/src/runtime-posture.ts +103 -7
- package/src/schema.ts +15 -3
- package/src/skills.ts +4 -0
- package/src/video-generation.ts +14 -0
- package/src/workspace-artifacts.ts +2 -0
- package/src/workspace-instruction-policies-schema.ts +1 -0
- package/src/workspace-membership-access.ts +61 -0
- package/dist/chunk-6TKL3GQO.js.map +0 -1
- package/dist/chunk-EYF6MH6C.js +0 -4772
- package/dist/chunk-EYF6MH6C.js.map +0 -1
- package/dist/chunk-GSBONN55.js +0 -533
- package/dist/chunk-GSBONN55.js.map +0 -1
- package/dist/chunk-GVZJ7XSR.js.map +0 -1
- package/dist/chunk-QLA3UQNX.js.map +0 -1
- package/dist/chunk-THJGMEK5.js.map +0 -1
- /package/dist/{chunk-2ZMTQVTR.js.map → chunk-2KOLL33E.js.map} +0 -0
- /package/dist/{chunk-232VC2LW.js.map → chunk-6V3HDEMR.js.map} +0 -0
- /package/dist/{chunk-M7PSMVFF.js.map → chunk-CHBLBEU6.js.map} +0 -0
- /package/dist/{chunk-UK57QA2H.js.map → chunk-UOXWVP3T.js.map} +0 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import { prepareLegacySkillFolder } from "./skill-metadata-migration";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type postgres from "postgres";
|
|
4
|
+
import {
|
|
5
|
+
KnowledgeEntryContent,
|
|
6
|
+
KnowledgeSourceSyncAction,
|
|
7
|
+
knowledgeSourceAgentConfig,
|
|
8
|
+
StoredKnowledgeEntryContent,
|
|
9
|
+
type KnowledgeEntryScope,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import {
|
|
12
|
+
fromPostgresLosslessText,
|
|
13
|
+
toPostgresLosslessJson,
|
|
14
|
+
toPostgresLosslessText,
|
|
15
|
+
LOSSLESS_CONTENT_CODEC_VERSION,
|
|
16
|
+
} from "./lossless-json";
|
|
17
|
+
|
|
18
|
+
/** Maintenance-only conversion. It is not a runtime fallback or a second writer. */
|
|
19
|
+
export const KNOWLEDGE_MIGRATION_MARKER = "-- opengeni:unified-knowledge-copy-v1";
|
|
20
|
+
export function knowledgeMigrationId(kind: string, id: string): string {
|
|
21
|
+
const hash = createHash("sha256")
|
|
22
|
+
.update(`opengeni:knowledge:0461:${kind}:${id}`)
|
|
23
|
+
.digest("hex")
|
|
24
|
+
.slice(0, 32);
|
|
25
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-5${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20)}`;
|
|
26
|
+
}
|
|
27
|
+
type Owner = {
|
|
28
|
+
accountId: string;
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
scope: KnowledgeEntryScope;
|
|
31
|
+
subjectId: string | null;
|
|
32
|
+
};
|
|
33
|
+
type Imported = Owner & {
|
|
34
|
+
id: string;
|
|
35
|
+
body: KnowledgeEntryContent;
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
status: string;
|
|
38
|
+
sessionId?: string | null;
|
|
39
|
+
memoryId?: string;
|
|
40
|
+
documentId?: string;
|
|
41
|
+
legacySnapshot?: Record<string, unknown>;
|
|
42
|
+
legacyDocumentVersionId?: string;
|
|
43
|
+
legacyClaimId?: string;
|
|
44
|
+
legacyScopeWorkspaceId?: string | null;
|
|
45
|
+
originalActor?: Record<string, unknown>;
|
|
46
|
+
legacyScopeType?: string;
|
|
47
|
+
roleKey?: string | null;
|
|
48
|
+
scopeSessionId?: string | null;
|
|
49
|
+
validFrom?: Date;
|
|
50
|
+
validUntil?: Date | null;
|
|
51
|
+
};
|
|
52
|
+
type LegacyMemory = {
|
|
53
|
+
id: string;
|
|
54
|
+
account_id: string;
|
|
55
|
+
workspace_id: string;
|
|
56
|
+
text: string;
|
|
57
|
+
text_codec_version: number | null;
|
|
58
|
+
kind: string;
|
|
59
|
+
status: string;
|
|
60
|
+
scope_type: string;
|
|
61
|
+
scope_subject_id: string | null;
|
|
62
|
+
scope_role_key: string | null;
|
|
63
|
+
scope_session_id: string | null;
|
|
64
|
+
created_by_session_id: string | null;
|
|
65
|
+
namespace: string;
|
|
66
|
+
labels: string[];
|
|
67
|
+
created_at: Date;
|
|
68
|
+
valid_from: Date;
|
|
69
|
+
valid_until: Date | null;
|
|
70
|
+
snapshot: Record<string, unknown>;
|
|
71
|
+
relationships: Array<{ entryId: string; relation: string }>;
|
|
72
|
+
confirmed_claim_id: string | null;
|
|
73
|
+
};
|
|
74
|
+
type LegacyDocument = {
|
|
75
|
+
id: string;
|
|
76
|
+
account_id: string;
|
|
77
|
+
workspace_id: string;
|
|
78
|
+
file_id: string;
|
|
79
|
+
base_id: string;
|
|
80
|
+
base_name: string;
|
|
81
|
+
title: string;
|
|
82
|
+
authority_kind: KnowledgeEntryScope;
|
|
83
|
+
authority_subject_id: string | null;
|
|
84
|
+
source_uri: string | null;
|
|
85
|
+
source_external_id: string | null;
|
|
86
|
+
source_version: string | null;
|
|
87
|
+
created_at: Date;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Resolve only identities that still exist; missing evidence stays in the audit snapshot. */
|
|
91
|
+
async function migrateMemoryEvidence(
|
|
92
|
+
tx: postgres.TransactionSql,
|
|
93
|
+
memory: LegacyMemory,
|
|
94
|
+
): Promise<KnowledgeEntryContent["evidence"]> {
|
|
95
|
+
const evidence: KnowledgeEntryContent["evidence"] = [];
|
|
96
|
+
const references = memory.snapshot.source_refs;
|
|
97
|
+
if (!Array.isArray(references)) return evidence;
|
|
98
|
+
for (const ref of references) {
|
|
99
|
+
if (
|
|
100
|
+
!ref ||
|
|
101
|
+
typeof ref !== "object" ||
|
|
102
|
+
typeof ref.id !== "string" ||
|
|
103
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref.id)
|
|
104
|
+
)
|
|
105
|
+
continue;
|
|
106
|
+
let entryId: string | undefined;
|
|
107
|
+
let passage: string | undefined;
|
|
108
|
+
if (ref.kind === "memory") {
|
|
109
|
+
const [found] =
|
|
110
|
+
await tx`SELECT id FROM knowledge_memories WHERE account_id=${memory.account_id} AND id=${ref.id}`;
|
|
111
|
+
if (found && found.id !== memory.id) entryId = String(found.id);
|
|
112
|
+
} else if (ref.kind === "document" || ref.kind === "document_chunk") {
|
|
113
|
+
const [doc] =
|
|
114
|
+
ref.kind === "document_chunk"
|
|
115
|
+
? await tx`SELECT d.id,d.status FROM document_chunks c JOIN documents d ON d.id=c.document_id AND d.account_id=c.account_id
|
|
116
|
+
WHERE c.account_id=${memory.account_id} AND c.id=${ref.id}`
|
|
117
|
+
: await tx`SELECT id,status FROM documents WHERE account_id=${memory.account_id} AND id=${ref.id}`;
|
|
118
|
+
if (doc) {
|
|
119
|
+
const [version] =
|
|
120
|
+
await tx`SELECT id FROM knowledge_document_versions WHERE account_id=${memory.account_id}
|
|
121
|
+
AND document_id=${doc.id} ORDER BY created_at DESC,id DESC LIMIT 1`;
|
|
122
|
+
// Only these identities are imported below. Failed/queued ordinary
|
|
123
|
+
// documents have no canonical source yet; preserve their exact reference
|
|
124
|
+
// in legacySnapshot instead of creating a dangling evidence edge.
|
|
125
|
+
if (version || doc.status === "ready")
|
|
126
|
+
entryId = knowledgeMigrationId(
|
|
127
|
+
version ? "document-version" : "document",
|
|
128
|
+
String(version?.id ?? doc.id),
|
|
129
|
+
);
|
|
130
|
+
if (ref.kind === "document_chunk") passage = `Legacy passage ${ref.id}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
entryId &&
|
|
135
|
+
!evidence.some((item) => item.entryId === entryId && item.location.passage === passage)
|
|
136
|
+
) {
|
|
137
|
+
evidence.push({
|
|
138
|
+
entryId,
|
|
139
|
+
revisionId: knowledgeMigrationId("revision", entryId),
|
|
140
|
+
location: passage ? { passage } : {},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return evidence;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function importEntry(tx: postgres.TransactionSql, input: Imported) {
|
|
148
|
+
const body = StoredKnowledgeEntryContent.parse(input.body);
|
|
149
|
+
const revisionId = knowledgeMigrationId("revision", input.id);
|
|
150
|
+
const outcome = ["active", "approved", "ready"].includes(input.status)
|
|
151
|
+
? "published"
|
|
152
|
+
: input.status === "proposed"
|
|
153
|
+
? "pending"
|
|
154
|
+
: "rejected";
|
|
155
|
+
const archived = !["active", "approved", "ready", "proposed"].includes(input.status);
|
|
156
|
+
const migrationActor = { kind: "migration", subjectId: "service:knowledge-migration:0461" };
|
|
157
|
+
const actor = input.originalActor ?? migrationActor;
|
|
158
|
+
const batchId =
|
|
159
|
+
outcome === "pending"
|
|
160
|
+
? knowledgeMigrationId(
|
|
161
|
+
"review-batch",
|
|
162
|
+
`${input.accountId}:${input.workspaceId}:${input.scope}:${input.subjectId ?? ""}:${input.sessionId ?? input.id}`,
|
|
163
|
+
)
|
|
164
|
+
: null;
|
|
165
|
+
if (batchId)
|
|
166
|
+
await tx`INSERT INTO knowledge_review_batches(id,account_id,origin_workspace_id,owner_key,session_id,created_at)
|
|
167
|
+
VALUES(${batchId},${input.accountId},${input.workspaceId},${input.scope === "personal" ? `personal:${input.subjectId}` : `${input.scope}:${input.workspaceId}`},
|
|
168
|
+
${input.sessionId ?? null},${input.createdAt}) ON CONFLICT(id) DO NOTHING`;
|
|
169
|
+
await tx`INSERT INTO knowledge_entries(id,account_id,origin_workspace_id,scope,scope_workspace_id,scope_subject_id,
|
|
170
|
+
version,published_revision_id,latest_revision_id,archived,legacy_memory_id,legacy_document_id,
|
|
171
|
+
legacy_scope_type,legacy_scope_role_key,legacy_scope_session_id,legacy_document_version_id,legacy_claim_id,legacy_scope_workspace_id,valid_from,valid_until,created_at,updated_at)
|
|
172
|
+
VALUES(${input.id},${input.accountId},${input.workspaceId},${input.scope},${input.scope === "workspace" ? input.workspaceId : null},
|
|
173
|
+
${input.subjectId},1,${outcome === "published" ? revisionId : null},${revisionId},${archived},${input.memoryId ?? null},${input.documentId ?? null},
|
|
174
|
+
${input.legacyScopeType ?? null},${input.roleKey ?? null},${input.scopeSessionId ?? null},${input.legacyDocumentVersionId ?? null},${input.legacyClaimId ?? null},${input.legacyScopeWorkspaceId ?? null},${input.validFrom ?? input.createdAt},
|
|
175
|
+
${input.validUntil ?? null},${input.createdAt},${input.createdAt})`;
|
|
176
|
+
await tx`INSERT INTO knowledge_entry_revisions(id,account_id,entry_id,number,body,body_codec_version,preview,preview_codec_version,
|
|
177
|
+
actor,created_by_session_id,review_batch_id,legacy_snapshot,created_at)
|
|
178
|
+
VALUES(${revisionId},${input.accountId},${input.id},1,${tx.json(toPostgresLosslessJson(body) as postgres.JSONValue)},
|
|
179
|
+
${LOSSLESS_CONTENT_CODEC_VERSION},${toPostgresLosslessText(body.content.slice(0, 512))},${LOSSLESS_CONTENT_CODEC_VERSION},
|
|
180
|
+
${tx.json(actor as postgres.JSONValue)},${input.sessionId ?? null},${batchId},${input.legacySnapshot ? tx.json(input.legacySnapshot as postgres.JSONValue) : null},${input.createdAt})`;
|
|
181
|
+
await tx`INSERT INTO knowledge_entry_decisions(account_id,entry_id,revision_id,version,outcome,actor,created_at)
|
|
182
|
+
VALUES(${input.accountId},${input.id},${revisionId},1,${outcome},${tx.json(migrationActor)},${input.createdAt})`;
|
|
183
|
+
await tx`SELECT knowledge_index_revision(${input.accountId},${input.id},${revisionId},
|
|
184
|
+
${toPostgresLosslessText(`${body.title}\n${body.content}`)})`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function importGroup(
|
|
188
|
+
tx: postgres.TransactionSql,
|
|
189
|
+
owner: Owner,
|
|
190
|
+
title: string,
|
|
191
|
+
sourceKey: string,
|
|
192
|
+
) {
|
|
193
|
+
const id = knowledgeMigrationId(
|
|
194
|
+
"group",
|
|
195
|
+
`${owner.accountId}:${owner.scope}:${owner.scope === "workspace" ? owner.workspaceId : (owner.subjectId ?? "")}:${sourceKey}`,
|
|
196
|
+
);
|
|
197
|
+
const [existing] = await tx`SELECT id FROM knowledge_entries WHERE id=${id}`;
|
|
198
|
+
if (!existing)
|
|
199
|
+
await importEntry(tx, {
|
|
200
|
+
...owner,
|
|
201
|
+
id,
|
|
202
|
+
status: "ready",
|
|
203
|
+
createdAt: new Date(),
|
|
204
|
+
body: { title, kind: "group", content: "", groupIds: [], evidence: [], relationships: [] },
|
|
205
|
+
});
|
|
206
|
+
return id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Pending pre-Skill preference proposals stay in their native authority. A new
|
|
210
|
+
* folder revision preserves the exact original body and links to the old revision;
|
|
211
|
+
* conversion never activates a proposal or reuses its old human confirmation. */
|
|
212
|
+
async function migratePendingLegacySkills(tx: postgres.TransactionSql): Promise<void> {
|
|
213
|
+
let after: string | null = null;
|
|
214
|
+
for (;;) {
|
|
215
|
+
const candidates: Array<{
|
|
216
|
+
id: string;
|
|
217
|
+
workspace_id: string;
|
|
218
|
+
account_id: string;
|
|
219
|
+
preference_id: string;
|
|
220
|
+
revision_id: string;
|
|
221
|
+
content: string;
|
|
222
|
+
title: string;
|
|
223
|
+
description: string;
|
|
224
|
+
session_id: string;
|
|
225
|
+
active_revision_id: string | null;
|
|
226
|
+
scope_version: number;
|
|
227
|
+
}> = await tx<
|
|
228
|
+
Array<{
|
|
229
|
+
id: string;
|
|
230
|
+
workspace_id: string;
|
|
231
|
+
account_id: string;
|
|
232
|
+
preference_id: string;
|
|
233
|
+
revision_id: string;
|
|
234
|
+
content: string;
|
|
235
|
+
title: string;
|
|
236
|
+
description: string;
|
|
237
|
+
session_id: string;
|
|
238
|
+
active_revision_id: string | null;
|
|
239
|
+
scope_version: number;
|
|
240
|
+
}>
|
|
241
|
+
>`
|
|
242
|
+
SELECT receipt.id,receipt.workspace_id,receipt.account_id,receipt.preference_id,receipt.revision_id,
|
|
243
|
+
revision.content,revision.title,revision.description,receipt.session_id,head.active_revision_id,head.scope_version
|
|
244
|
+
FROM company_brain_preference_proposal_receipts receipt
|
|
245
|
+
JOIN preference_registry_preferences head ON head.id=receipt.preference_id AND head.account_id=receipt.account_id
|
|
246
|
+
JOIN preference_registry_revisions revision ON revision.id=receipt.revision_id AND revision.preference_id=head.id
|
|
247
|
+
JOIN knowledge_change_proposals proposal ON proposal.id=receipt.knowledge_proposal_id AND proposal.account_id=receipt.account_id
|
|
248
|
+
WHERE (${after}::uuid IS NULL OR receipt.id>${after}::uuid)
|
|
249
|
+
AND revision.skill_files IS NULL AND head.status NOT IN ('rejected','superseded')
|
|
250
|
+
AND proposal.status='proposed' AND proposal.target_kind='preference'
|
|
251
|
+
AND head.scope='workspace' AND head.scope_workspace_id=receipt.workspace_id
|
|
252
|
+
AND proposal.scope_workspace_id=receipt.workspace_id AND proposal.target_scope='workspace'
|
|
253
|
+
AND coalesce((SELECT review.state FROM knowledge_claim_reviews review WHERE review.claim_id=proposal.claim_id
|
|
254
|
+
AND review.account_id=receipt.account_id ORDER BY review.review_revision DESC LIMIT 1),'proposed') IN ('proposed','approved')
|
|
255
|
+
AND NOT EXISTS(SELECT 1 FROM preference_registry_events event WHERE event.preference_id=head.id
|
|
256
|
+
AND event.new_revision_id=revision.id AND event.type IN ('activated','corrected','rejected'))
|
|
257
|
+
AND NOT EXISTS(SELECT 1 FROM preference_registry_revisions newer WHERE newer.preference_id=head.id
|
|
258
|
+
AND newer.revision>revision.revision AND newer.created_by_subject_id<>'service:skill-migration:0433')
|
|
259
|
+
ORDER BY receipt.id LIMIT 100`;
|
|
260
|
+
if (!candidates.length) break;
|
|
261
|
+
for (const candidate of candidates) {
|
|
262
|
+
const folder = prepareLegacySkillFolder([{ path: "SKILL.md", content: candidate.content }], {
|
|
263
|
+
id: candidate.preference_id,
|
|
264
|
+
title: candidate.title,
|
|
265
|
+
description: candidate.description,
|
|
266
|
+
});
|
|
267
|
+
const main = folder.files.find((file) => file.path === "SKILL.md")!.content;
|
|
268
|
+
const revisionId = knowledgeMigrationId("pending-skill-revision", candidate.id);
|
|
269
|
+
const operationId = knowledgeMigrationId("pending-skill-operation", candidate.id);
|
|
270
|
+
await tx`INSERT INTO preference_registry_revisions(id,account_id,preference_id,title,description,content,content_hash,
|
|
271
|
+
precedence_rank,conflict_strategy,conflicts_with,provenance_source,provenance_source_id,trust,expires_at,
|
|
272
|
+
created_by_subject_id,corrects_revision_id,skill_files,skill_activation_mode)
|
|
273
|
+
SELECT ${revisionId}::uuid,account_id,preference_id,${folder.name},${folder.description},${main},
|
|
274
|
+
${createHash("sha256").update(main, "utf8").digest("hex")},precedence_rank,conflict_strategy,conflicts_with,
|
|
275
|
+
provenance_source,provenance_source_id,trust,expires_at,'service:knowledge-migration:0461',id,
|
|
276
|
+
${tx.json(folder.files)},'workspace_managed'
|
|
277
|
+
FROM preference_registry_revisions WHERE id=${candidate.revision_id}::uuid`;
|
|
278
|
+
await tx`INSERT INTO skill_write_receipts(account_id,workspace_id,operation_id,fingerprint,actor,receipt)
|
|
279
|
+
VALUES(${candidate.account_id}::uuid,${candidate.workspace_id}::uuid,${operationId}::uuid,
|
|
280
|
+
${createHash("sha256").update(operationId).digest("hex")},
|
|
281
|
+
${tx.json({
|
|
282
|
+
kind: "migration",
|
|
283
|
+
subjectId: "service:knowledge-migration:0461",
|
|
284
|
+
legacyProposalReceiptId: candidate.id,
|
|
285
|
+
legacyRevisionId: candidate.revision_id,
|
|
286
|
+
sessionId: candidate.session_id,
|
|
287
|
+
})},
|
|
288
|
+
${tx.json({
|
|
289
|
+
operationId,
|
|
290
|
+
skillId: candidate.preference_id,
|
|
291
|
+
revisionId,
|
|
292
|
+
outcome: "pending",
|
|
293
|
+
pendingReason: "approval",
|
|
294
|
+
replayed: false,
|
|
295
|
+
})})`;
|
|
296
|
+
}
|
|
297
|
+
after = candidates.at(-1)!.id;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** All source and destination FORCE-RLS windows are explicit in the SQL file. */
|
|
302
|
+
export async function migrateRetainedKnowledge(tx: postgres.TransactionSql): Promise<void> {
|
|
303
|
+
await migratePendingLegacySkills(tx);
|
|
304
|
+
await migrateSourceSchedules(tx);
|
|
305
|
+
let after: string | null = null;
|
|
306
|
+
for (;;) {
|
|
307
|
+
const memories: LegacyMemory[] = await tx<
|
|
308
|
+
LegacyMemory[]
|
|
309
|
+
>`SELECT m.id,m.account_id,m.workspace_id,m.text,m.text_codec_version,m.kind,m.status,m.scope_type,m.scope_subject_id,
|
|
310
|
+
m.scope_role_key,m.scope_session_id,m.created_by_session_id,
|
|
311
|
+
(SELECT r.claim_id FROM remember_knowledge_memory_materializations map
|
|
312
|
+
JOIN remember_knowledge_confirmation_receipts r ON r.id=map.confirmation_receipt_id
|
|
313
|
+
WHERE map.account_id=m.account_id AND map.memory_id=m.id) AS confirmed_claim_id,m.namespace_key AS namespace,m.labels,m.created_at,m.valid_from,m.valid_until,
|
|
314
|
+
(to_jsonb(m)-'embedding'-'text') || jsonb_build_object('relationships',coalesce((SELECT jsonb_agg(to_jsonb(edge) ORDER BY edge.id)
|
|
315
|
+
FROM knowledge_memory_relationships edge WHERE edge.account_id=m.account_id
|
|
316
|
+
AND (edge.source_memory_id=m.id OR edge.target_memory_id=m.id)),'[]'::jsonb),
|
|
317
|
+
'lifecycleEvents',coalesce((SELECT jsonb_agg(to_jsonb(event) ORDER BY event.created_at,event.id)
|
|
318
|
+
FROM knowledge_memory_lifecycle_events event WHERE event.account_id=m.account_id AND event.target_memory_id=m.id),'[]'::jsonb)) AS snapshot,
|
|
319
|
+
coalesce((SELECT jsonb_agg(jsonb_build_object('entryId',edge.target_memory_id,'relation',
|
|
320
|
+
CASE edge.relationship_type WHEN 'derived_from' THEN 'depends_on' WHEN 'corrects' THEN 'supersedes' ELSE edge.relationship_type END)
|
|
321
|
+
ORDER BY edge.id) FROM knowledge_memory_relationships edge
|
|
322
|
+
WHERE edge.account_id=m.account_id AND edge.source_memory_id=m.id AND edge.removed_at IS NULL),'[]'::jsonb) AS relationships
|
|
323
|
+
FROM knowledge_memories m WHERE (${after}::uuid IS NULL OR m.id>${after}::uuid) ORDER BY m.id LIMIT 200`;
|
|
324
|
+
if (!memories.length) break;
|
|
325
|
+
for (const memory of memories) {
|
|
326
|
+
const content = fromPostgresLosslessText(memory.text, memory.text_codec_version);
|
|
327
|
+
const owner: Owner = {
|
|
328
|
+
accountId: memory.account_id,
|
|
329
|
+
workspaceId: memory.workspace_id,
|
|
330
|
+
scope: memory.scope_type === "user" ? "personal" : "workspace",
|
|
331
|
+
subjectId: memory.scope_subject_id,
|
|
332
|
+
};
|
|
333
|
+
// Scope-specific legacy records retain their old selectors; do not put
|
|
334
|
+
// their labels or names in a more broadly visible group during migration.
|
|
335
|
+
const groupIds: string[] = [];
|
|
336
|
+
if (memory.scope_type === "workspace") {
|
|
337
|
+
if (memory.namespace && memory.namespace !== "general")
|
|
338
|
+
groupIds.push(
|
|
339
|
+
await importGroup(tx, owner, memory.namespace, `namespace:${memory.namespace}`),
|
|
340
|
+
);
|
|
341
|
+
for (const label of memory.labels)
|
|
342
|
+
groupIds.push(await importGroup(tx, owner, label, `label:${label}`));
|
|
343
|
+
}
|
|
344
|
+
await importEntry(tx, {
|
|
345
|
+
...owner,
|
|
346
|
+
id: memory.id,
|
|
347
|
+
memoryId: memory.id,
|
|
348
|
+
...(memory.confirmed_claim_id ? { legacyClaimId: memory.confirmed_claim_id } : {}),
|
|
349
|
+
legacySnapshot: memory.snapshot,
|
|
350
|
+
originalActor: {
|
|
351
|
+
kind: memory.snapshot.created_by_kind,
|
|
352
|
+
subjectId: memory.snapshot.created_by_subject_id,
|
|
353
|
+
context: memory.snapshot.created_by_context,
|
|
354
|
+
},
|
|
355
|
+
status: memory.status,
|
|
356
|
+
legacyScopeType: memory.scope_type,
|
|
357
|
+
roleKey: memory.scope_role_key,
|
|
358
|
+
scopeSessionId: memory.scope_session_id,
|
|
359
|
+
sessionId: memory.created_by_session_id,
|
|
360
|
+
createdAt: memory.created_at,
|
|
361
|
+
validFrom: memory.valid_from,
|
|
362
|
+
validUntil: memory.valid_until,
|
|
363
|
+
body: {
|
|
364
|
+
title: content.split(/\r?\n/u, 1)[0]?.slice(0, 160) || "Imported knowledge",
|
|
365
|
+
content,
|
|
366
|
+
kind:
|
|
367
|
+
memory.kind === "episodic"
|
|
368
|
+
? "incident"
|
|
369
|
+
: memory.kind === "semantic"
|
|
370
|
+
? "fact"
|
|
371
|
+
: memory.kind === "decision"
|
|
372
|
+
? "decision"
|
|
373
|
+
: "note",
|
|
374
|
+
source: { kind: "manual", externalId: `memory:${memory.id}` },
|
|
375
|
+
groupIds: [...new Set(groupIds)],
|
|
376
|
+
evidence: await migrateMemoryEvidence(tx, memory),
|
|
377
|
+
relationships: memory.relationships.map((relationship) => ({
|
|
378
|
+
entryId: relationship.entryId,
|
|
379
|
+
relation:
|
|
380
|
+
relationship.relation as KnowledgeEntryContent["relationships"][number]["relation"],
|
|
381
|
+
})),
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
after = memories.at(-1)!.id;
|
|
386
|
+
}
|
|
387
|
+
after = null;
|
|
388
|
+
for (;;) {
|
|
389
|
+
const documents: LegacyDocument[] = await tx<
|
|
390
|
+
LegacyDocument[]
|
|
391
|
+
>`SELECT d.id,d.account_id,d.workspace_id,d.file_id,d.base_id,b.name AS base_name,d.title,d.authority_kind,
|
|
392
|
+
d.authority_subject_id,d.source_uri,d.source_external_id,d.source_version,d.created_at
|
|
393
|
+
FROM documents d JOIN document_bases b ON b.id=d.base_id AND b.account_id=d.account_id
|
|
394
|
+
WHERE d.status='ready' AND NOT EXISTS(SELECT 1 FROM knowledge_document_versions v WHERE v.account_id=d.account_id AND v.document_id=d.id)
|
|
395
|
+
AND (${after}::uuid IS NULL OR d.id>${after}::uuid) ORDER BY d.id LIMIT 50`;
|
|
396
|
+
if (!documents.length) break;
|
|
397
|
+
for (const document of documents) {
|
|
398
|
+
const chunks = await tx<Array<{ text: string }>>`SELECT text FROM document_chunks
|
|
399
|
+
WHERE account_id=${document.account_id} AND document_id=${document.id} ORDER BY chunk_index`;
|
|
400
|
+
const owner: Owner = {
|
|
401
|
+
accountId: document.account_id,
|
|
402
|
+
workspaceId: document.workspace_id,
|
|
403
|
+
scope: document.authority_kind,
|
|
404
|
+
subjectId: document.authority_subject_id,
|
|
405
|
+
};
|
|
406
|
+
const groupIds =
|
|
407
|
+
document.authority_kind === "workspace" && document.base_name
|
|
408
|
+
? [await importGroup(tx, owner, document.base_name, `document-base:${document.base_id}`)]
|
|
409
|
+
: [];
|
|
410
|
+
await importEntry(tx, {
|
|
411
|
+
...owner,
|
|
412
|
+
id: knowledgeMigrationId("document", document.id),
|
|
413
|
+
documentId: document.id,
|
|
414
|
+
status: "ready",
|
|
415
|
+
createdAt: document.created_at,
|
|
416
|
+
body: {
|
|
417
|
+
title: document.title,
|
|
418
|
+
kind: "source",
|
|
419
|
+
// Historical chunks may overlap. Preserve every retained passage;
|
|
420
|
+
// never claim to have reconstructed missing original full text.
|
|
421
|
+
content: chunks.map((chunk) => chunk.text).join("\n\n"),
|
|
422
|
+
groupIds,
|
|
423
|
+
evidence: [],
|
|
424
|
+
relationships: [],
|
|
425
|
+
source: {
|
|
426
|
+
kind: "file",
|
|
427
|
+
fileId: document.file_id,
|
|
428
|
+
documentId: document.id,
|
|
429
|
+
retention: "passages",
|
|
430
|
+
...(document.source_uri ? { uri: document.source_uri } : {}),
|
|
431
|
+
...(document.source_external_id ? { externalId: document.source_external_id } : {}),
|
|
432
|
+
...(document.source_version ? { version: document.source_version } : {}),
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
after = documents.at(-1)!.id;
|
|
438
|
+
}
|
|
439
|
+
await importScopedKnowledge(tx);
|
|
440
|
+
// Every endpoint exists before relationship constraints are checked.
|
|
441
|
+
await tx`INSERT INTO knowledge_entry_links(account_id,entry_id,revision_id,ordinal,target_entry_id,relation,target_revision_id)
|
|
442
|
+
SELECT r.account_id,r.entry_id,r.id,(g.ordinality-1)::integer,g.value::uuid,'group',NULL::uuid
|
|
443
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements_text(r.body->'groupIds') WITH ORDINALITY g(value,ordinality)
|
|
444
|
+
UNION ALL
|
|
445
|
+
SELECT r.account_id,r.entry_id,r.id,jsonb_array_length(r.body->'groupIds')+(l.ordinality-1)::integer,
|
|
446
|
+
(l.value->>'entryId')::uuid,l.value->>'relation',NULL::uuid
|
|
447
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements(r.body->'relationships') WITH ORDINALITY l(value,ordinality)
|
|
448
|
+
UNION ALL
|
|
449
|
+
SELECT r.account_id,r.entry_id,r.id,jsonb_array_length(r.body->'groupIds')+jsonb_array_length(r.body->'relationships')+(e.ordinality-1)::integer,
|
|
450
|
+
(e.value->>'entryId')::uuid,'evidence',(e.value->>'revisionId')::uuid
|
|
451
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements(r.body->'evidence') WITH ORDINALITY e(value,ordinality)`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Import the previously separate entity/claim graph into ordinary entries. */
|
|
455
|
+
async function importScopedKnowledge(tx: postgres.TransactionSql) {
|
|
456
|
+
type ScopedRow = {
|
|
457
|
+
id: string;
|
|
458
|
+
account_id: string;
|
|
459
|
+
scope_kind: KnowledgeEntryScope;
|
|
460
|
+
scope_workspace_id: string | null;
|
|
461
|
+
scope_subject_id: string | null;
|
|
462
|
+
created_at: Date;
|
|
463
|
+
actor_kind: string;
|
|
464
|
+
actor_subject_id: string;
|
|
465
|
+
initiating_human_subject_id: string | null;
|
|
466
|
+
};
|
|
467
|
+
function owner(row: ScopedRow): Owner {
|
|
468
|
+
return {
|
|
469
|
+
accountId: row.account_id,
|
|
470
|
+
// Some organization/user records never had a creating workspace. This
|
|
471
|
+
// deterministic import origin is provenance only and grants no access.
|
|
472
|
+
workspaceId: row.scope_workspace_id ?? knowledgeMigrationId("import-origin", row.account_id),
|
|
473
|
+
scope: row.scope_kind,
|
|
474
|
+
subjectId: row.scope_subject_id,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function actor(row: ScopedRow) {
|
|
478
|
+
return {
|
|
479
|
+
kind: row.actor_kind,
|
|
480
|
+
subjectId: row.actor_subject_id,
|
|
481
|
+
initiatingHumanSubjectId: row.initiating_human_subject_id,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
let after: string | null = null;
|
|
485
|
+
for (;;) {
|
|
486
|
+
const entities: Array<
|
|
487
|
+
ScopedRow & { display_name: string; aliases: string[]; snapshot: Record<string, unknown> }
|
|
488
|
+
> = await tx<
|
|
489
|
+
Array<
|
|
490
|
+
ScopedRow & { display_name: string; aliases: string[]; snapshot: Record<string, unknown> }
|
|
491
|
+
>
|
|
492
|
+
>`
|
|
493
|
+
SELECT e.*,to_jsonb(e) AS snapshot,coalesce((SELECT jsonb_agg(a.alias ORDER BY a.id)
|
|
494
|
+
FROM knowledge_entity_aliases a WHERE a.account_id=e.account_id AND a.entity_id=e.id),'[]'::jsonb) AS aliases
|
|
495
|
+
FROM knowledge_entities e WHERE (${after}::uuid IS NULL OR e.id>${after}::uuid)
|
|
496
|
+
AND (NOT EXISTS(SELECT 1 FROM knowledge_facts f JOIN knowledge_claims c ON c.fact_id=f.id
|
|
497
|
+
WHERE f.account_id=e.account_id AND (f.subject_entity_id=e.id OR f.object_entity_id=e.id))
|
|
498
|
+
OR EXISTS(SELECT 1 FROM knowledge_facts f JOIN knowledge_claims c ON c.fact_id=f.id
|
|
499
|
+
WHERE f.account_id=e.account_id AND (f.subject_entity_id=e.id OR f.object_entity_id=e.id)
|
|
500
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_change_proposals p WHERE p.claim_id=c.id AND p.account_id=c.account_id)))
|
|
501
|
+
ORDER BY e.id LIMIT 200`;
|
|
502
|
+
if (!entities.length) break;
|
|
503
|
+
for (const entity of entities)
|
|
504
|
+
await importEntry(tx, {
|
|
505
|
+
...owner(entity),
|
|
506
|
+
id: knowledgeMigrationId("entity", entity.id),
|
|
507
|
+
status: "approved",
|
|
508
|
+
createdAt: entity.created_at,
|
|
509
|
+
legacyScopeWorkspaceId: entity.scope_workspace_id,
|
|
510
|
+
originalActor: actor(entity),
|
|
511
|
+
legacySnapshot: { entity: entity.snapshot, aliases: entity.aliases },
|
|
512
|
+
body: {
|
|
513
|
+
title: entity.display_name,
|
|
514
|
+
kind: "group",
|
|
515
|
+
content: entity.aliases.join("\n"),
|
|
516
|
+
groupIds: [],
|
|
517
|
+
evidence: [],
|
|
518
|
+
relationships: [],
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
after = entities.at(-1)!.id;
|
|
522
|
+
}
|
|
523
|
+
after = null;
|
|
524
|
+
for (;;) {
|
|
525
|
+
const versions: Array<
|
|
526
|
+
ScopedRow & {
|
|
527
|
+
document_id: string | null;
|
|
528
|
+
file_id: string | null;
|
|
529
|
+
external_version_id: string;
|
|
530
|
+
title: string;
|
|
531
|
+
snapshot: Record<string, unknown>;
|
|
532
|
+
source_uri: string | null;
|
|
533
|
+
}
|
|
534
|
+
> = await tx<
|
|
535
|
+
Array<
|
|
536
|
+
ScopedRow & {
|
|
537
|
+
document_id: string | null;
|
|
538
|
+
file_id: string | null;
|
|
539
|
+
external_version_id: string;
|
|
540
|
+
title: string;
|
|
541
|
+
snapshot: Record<string, unknown>;
|
|
542
|
+
source_uri: string | null;
|
|
543
|
+
}
|
|
544
|
+
>
|
|
545
|
+
>`
|
|
546
|
+
SELECT v.*,coalesce(d.title,o.external_object_id) AS title,s.source_uri,
|
|
547
|
+
jsonb_build_object('version',to_jsonb(v),'source',to_jsonb(s),'object',to_jsonb(o),'provider',to_jsonb(p)) AS snapshot
|
|
548
|
+
FROM knowledge_document_versions v JOIN knowledge_source_objects o ON o.account_id=v.account_id AND o.id=v.object_id
|
|
549
|
+
JOIN knowledge_sources s ON s.account_id=v.account_id AND s.id=v.source_id
|
|
550
|
+
JOIN knowledge_providers p ON p.account_id=s.account_id AND p.id=s.provider_id
|
|
551
|
+
LEFT JOIN documents d ON d.account_id=v.account_id AND d.id=v.document_id
|
|
552
|
+
WHERE (${after}::uuid IS NULL OR v.id>${after}::uuid) ORDER BY v.id LIMIT 100`;
|
|
553
|
+
if (!versions.length) break;
|
|
554
|
+
for (const version of versions) {
|
|
555
|
+
const chunks = version.document_id
|
|
556
|
+
? await tx<Array<{ text: string }>>`SELECT text FROM document_chunks
|
|
557
|
+
WHERE account_id=${version.account_id} AND document_id=${version.document_id} ORDER BY chunk_index`
|
|
558
|
+
: [];
|
|
559
|
+
await importEntry(tx, {
|
|
560
|
+
...owner(version),
|
|
561
|
+
id: knowledgeMigrationId("document-version", version.id),
|
|
562
|
+
legacyDocumentVersionId: version.id,
|
|
563
|
+
legacyScopeWorkspaceId: version.scope_workspace_id,
|
|
564
|
+
status: "ready",
|
|
565
|
+
createdAt: version.created_at,
|
|
566
|
+
originalActor: actor(version),
|
|
567
|
+
legacySnapshot: version.snapshot,
|
|
568
|
+
body: {
|
|
569
|
+
title: version.title,
|
|
570
|
+
kind: "source",
|
|
571
|
+
content: chunks.map((c) => c.text).join("\n\n"),
|
|
572
|
+
groupIds: [],
|
|
573
|
+
evidence: [],
|
|
574
|
+
relationships: [],
|
|
575
|
+
source: {
|
|
576
|
+
kind: version.file_id ? "file" : "connector",
|
|
577
|
+
externalId: `knowledge-document-version:${version.id}`,
|
|
578
|
+
version: version.external_version_id,
|
|
579
|
+
retention: chunks.length ? "passages" : "reference",
|
|
580
|
+
...(version.file_id ? { fileId: version.file_id } : {}),
|
|
581
|
+
...(version.document_id ? { documentId: version.document_id } : {}),
|
|
582
|
+
...(version.source_uri ? { uri: version.source_uri } : {}),
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
after = versions.at(-1)!.id;
|
|
588
|
+
}
|
|
589
|
+
after = null;
|
|
590
|
+
for (;;) {
|
|
591
|
+
const claims: Array<
|
|
592
|
+
ScopedRow & {
|
|
593
|
+
subject_entity_id: string;
|
|
594
|
+
object_entity_id: string | null;
|
|
595
|
+
subject_name: string;
|
|
596
|
+
object_name: string | null;
|
|
597
|
+
predicate_key: string;
|
|
598
|
+
object_value: unknown;
|
|
599
|
+
state: string;
|
|
600
|
+
extraction_method: string;
|
|
601
|
+
effective_at: Date;
|
|
602
|
+
expires_at: Date | null;
|
|
603
|
+
snapshot: Record<string, unknown>;
|
|
604
|
+
}
|
|
605
|
+
> = await tx<
|
|
606
|
+
Array<
|
|
607
|
+
ScopedRow & {
|
|
608
|
+
subject_entity_id: string;
|
|
609
|
+
object_entity_id: string | null;
|
|
610
|
+
subject_name: string;
|
|
611
|
+
object_name: string | null;
|
|
612
|
+
predicate_key: string;
|
|
613
|
+
object_value: unknown;
|
|
614
|
+
state: string;
|
|
615
|
+
extraction_method: string;
|
|
616
|
+
effective_at: Date;
|
|
617
|
+
expires_at: Date | null;
|
|
618
|
+
snapshot: Record<string, unknown>;
|
|
619
|
+
}
|
|
620
|
+
>
|
|
621
|
+
>`
|
|
622
|
+
SELECT c.*,f.subject_entity_id,f.object_entity_id,f.predicate_key,f.object_value,
|
|
623
|
+
subject.display_name AS subject_name,object.display_name AS object_name,
|
|
624
|
+
coalesce((SELECT state FROM knowledge_claim_reviews r WHERE r.account_id=c.account_id AND r.claim_id=c.id
|
|
625
|
+
ORDER BY r.review_revision DESC LIMIT 1),'proposed') AS state,
|
|
626
|
+
jsonb_build_object('claim',to_jsonb(c),'fact',to_jsonb(f),
|
|
627
|
+
'evidence',coalesce((SELECT jsonb_agg(to_jsonb(e) ORDER BY e.id) FROM knowledge_claim_evidence e
|
|
628
|
+
WHERE e.account_id=c.account_id AND e.claim_id=c.id),'[]'::jsonb),
|
|
629
|
+
'reviews',coalesce((SELECT jsonb_agg(to_jsonb(r) ORDER BY r.review_revision) FROM knowledge_claim_reviews r
|
|
630
|
+
WHERE r.account_id=c.account_id AND r.claim_id=c.id),'[]'::jsonb)) AS snapshot
|
|
631
|
+
FROM knowledge_claims c JOIN knowledge_facts f ON f.account_id=c.account_id AND f.id=c.fact_id
|
|
632
|
+
JOIN knowledge_entities subject ON subject.account_id=f.account_id AND subject.id=f.subject_entity_id
|
|
633
|
+
LEFT JOIN knowledge_entities object ON object.account_id=f.account_id AND object.id=f.object_entity_id
|
|
634
|
+
WHERE (${after}::uuid IS NULL OR c.id>${after}::uuid)
|
|
635
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_entries imported WHERE imported.legacy_claim_id=c.id)
|
|
636
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_change_proposals p WHERE p.account_id=c.account_id AND p.claim_id=c.id
|
|
637
|
+
)
|
|
638
|
+
ORDER BY c.id LIMIT 100`;
|
|
639
|
+
if (!claims.length) break;
|
|
640
|
+
for (const claim of claims) {
|
|
641
|
+
const retained = await tx<Array<{ document_version_id: string; locator: string | null }>>`
|
|
642
|
+
SELECT document_version_id,locator FROM knowledge_claim_evidence WHERE account_id=${claim.account_id}
|
|
643
|
+
AND claim_id=${claim.id} AND document_version_id IS NOT NULL ORDER BY id`;
|
|
644
|
+
const relations = await tx<
|
|
645
|
+
Array<{
|
|
646
|
+
to_claim_id: string;
|
|
647
|
+
imported_entry_id: string | null;
|
|
648
|
+
relation_type: "supersedes" | "conflicts_with";
|
|
649
|
+
}>
|
|
650
|
+
>`
|
|
651
|
+
SELECT r.to_claim_id,r.relation_type,(SELECT e.id FROM knowledge_entries e WHERE e.legacy_claim_id=r.to_claim_id) AS imported_entry_id FROM knowledge_claim_relations r
|
|
652
|
+
WHERE r.account_id=${claim.account_id} AND r.from_claim_id=${claim.id}
|
|
653
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_change_proposals p WHERE p.account_id=r.account_id AND p.claim_id=r.to_claim_id ) ORDER BY r.id`;
|
|
654
|
+
const objectText =
|
|
655
|
+
claim.object_name ??
|
|
656
|
+
(typeof claim.object_value === "string"
|
|
657
|
+
? claim.object_value
|
|
658
|
+
: JSON.stringify(claim.object_value));
|
|
659
|
+
await importEntry(tx, {
|
|
660
|
+
...owner(claim),
|
|
661
|
+
id: knowledgeMigrationId("claim", claim.id),
|
|
662
|
+
legacyClaimId: claim.id,
|
|
663
|
+
legacyScopeWorkspaceId: claim.scope_workspace_id,
|
|
664
|
+
status: claim.state,
|
|
665
|
+
createdAt: claim.created_at,
|
|
666
|
+
validFrom: claim.effective_at,
|
|
667
|
+
validUntil: claim.expires_at,
|
|
668
|
+
originalActor: actor(claim),
|
|
669
|
+
legacySnapshot: claim.snapshot,
|
|
670
|
+
body: {
|
|
671
|
+
title: `${claim.subject_name}: ${claim.predicate_key}`,
|
|
672
|
+
kind: "fact",
|
|
673
|
+
content:
|
|
674
|
+
claim.extraction_method === "task-note-promotion-v1" &&
|
|
675
|
+
typeof claim.object_value === "string"
|
|
676
|
+
? claim.object_value
|
|
677
|
+
: `${claim.subject_name} · ${claim.predicate_key}: ${objectText}`,
|
|
678
|
+
source: { kind: "manual", externalId: `knowledge-claim:${claim.id}` },
|
|
679
|
+
groupIds: [knowledgeMigrationId("entity", claim.subject_entity_id)],
|
|
680
|
+
evidence: retained.map((e) => {
|
|
681
|
+
const id = knowledgeMigrationId("document-version", e.document_version_id);
|
|
682
|
+
return {
|
|
683
|
+
entryId: id,
|
|
684
|
+
revisionId: knowledgeMigrationId("revision", id),
|
|
685
|
+
location: e.locator ? { passage: e.locator } : {},
|
|
686
|
+
};
|
|
687
|
+
}),
|
|
688
|
+
relationships: [
|
|
689
|
+
...relations.map((r) => ({
|
|
690
|
+
entryId: r.imported_entry_id ?? knowledgeMigrationId("claim", r.to_claim_id),
|
|
691
|
+
relation: r.relation_type,
|
|
692
|
+
})),
|
|
693
|
+
...(claim.object_entity_id
|
|
694
|
+
? [
|
|
695
|
+
{
|
|
696
|
+
entryId: knowledgeMigrationId("entity", claim.object_entity_id),
|
|
697
|
+
relation: "related_to" as const,
|
|
698
|
+
},
|
|
699
|
+
]
|
|
700
|
+
: []),
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
after = claims.at(-1)!.id;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Preserve source selection and schedule ids, but create a new ordinary task
|
|
710
|
+
* revision. Old occurrence/history rows remain exact and never become agents. */
|
|
711
|
+
async function migrateSourceSchedules(tx: postgres.TransactionSql) {
|
|
712
|
+
await tx`ALTER TABLE scheduled_tasks NO FORCE ROW LEVEL SECURITY`;
|
|
713
|
+
await tx`ALTER TABLE scheduled_tasks DISABLE TRIGGER scheduled_task_tombstone_immutable`;
|
|
714
|
+
const tasks =
|
|
715
|
+
await tx`SELECT id,account_id,workspace_id,action,authority_revision FROM scheduled_tasks
|
|
716
|
+
WHERE action->>'kind'='knowledge_source_sync' AND deleted_at IS NULL ORDER BY id`;
|
|
717
|
+
for (const task of tasks) {
|
|
718
|
+
const source = KnowledgeSourceSyncAction.parse(task.action);
|
|
719
|
+
await tx`SELECT set_config('opengeni.account_id',${task.account_id},true),
|
|
720
|
+
set_config('opengeni.workspace_id',${task.workspace_id},true),
|
|
721
|
+
set_config('opengeni.subject_id',${source.initiatingSubjectId},true),
|
|
722
|
+
set_config('opengeni.initiating_human_subject_id',${source.initiatingSubjectId},true)`;
|
|
723
|
+
await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('session-tenancy:'||${task.workspace_id}::text,0))`;
|
|
724
|
+
await tx`SELECT id FROM workspaces WHERE id=${task.workspace_id} FOR UPDATE`;
|
|
725
|
+
const [authority] = await tx`SELECT id FROM organization_memberships m
|
|
726
|
+
WHERE m.account_id=${task.account_id} AND m.subject_id=${source.initiatingSubjectId}
|
|
727
|
+
AND m.status='active' AND m.revoked_at IS NULL
|
|
728
|
+
AND (m.personal_workspace_id=${task.workspace_id} OR EXISTS(SELECT 1 FROM workspace_memberships w
|
|
729
|
+
WHERE w.account_id=m.account_id AND w.workspace_id=${task.workspace_id} AND w.subject_id=m.subject_id))`;
|
|
730
|
+
// Old deterministic occurrences cannot resume as agent work. Preserve their
|
|
731
|
+
// audit rows and checkpoints, close their wakes, and let the next ordinary
|
|
732
|
+
// scheduled run continue from the retained provider cursor.
|
|
733
|
+
await tx`UPDATE scheduled_task_runs SET status='failed',completed_at=clock_timestamp(),
|
|
734
|
+
error='knowledge_source_agent_cutover',updated_at=clock_timestamp()
|
|
735
|
+
WHERE task_id=${task.id} AND action_kind='knowledge_source_sync' AND status IN ('queued','dispatched')`;
|
|
736
|
+
await tx`UPDATE knowledge_source_sync_wakes SET completed_at=clock_timestamp()
|
|
737
|
+
WHERE scheduled_task_id=${task.id} AND completed_at IS NULL`;
|
|
738
|
+
await tx`UPDATE knowledge_source_sync_states SET lease_id=NULL,lease_until=NULL,
|
|
739
|
+
buffered_wake=false,buffered_scheduled_task_run_id=NULL,pending_wake_count=0,updated_at=clock_timestamp()
|
|
740
|
+
WHERE scheduled_task_id=${task.id}`;
|
|
741
|
+
// A former owner's schedule is still useful configuration, but migration
|
|
742
|
+
// cannot invent authority for it. Re-enabling requires a current owner edit.
|
|
743
|
+
if (!authority)
|
|
744
|
+
await tx`UPDATE scheduled_tasks SET status='paused',
|
|
745
|
+
metadata=jsonb_set(metadata,'{knowledgeSourceSync}',coalesce(metadata->'knowledgeSourceSync','{}'::jsonb)
|
|
746
|
+
|| '{"sourceEnabled":false,"authorityRefreshRequired":true}'::jsonb) WHERE id=${task.id}`;
|
|
747
|
+
const [updated] = await tx`UPDATE scheduled_tasks SET action='{"kind":"agent_turn"}',
|
|
748
|
+
agent_config=${JSON.stringify(knowledgeSourceAgentConfig(source))}::jsonb, authority_revision=authority_revision+1,
|
|
749
|
+
updated_at=clock_timestamp() WHERE id=${task.id} RETURNING authority_revision`;
|
|
750
|
+
if (!updated) throw new Error("Source schedule disappeared during maintenance");
|
|
751
|
+
await tx`SELECT freeze_scheduled_task_personal_resources(${task.account_id}::uuid,
|
|
752
|
+
${task.workspace_id}::uuid,${task.id}::uuid,${updated.authority_revision}::bigint)`;
|
|
753
|
+
if (authority)
|
|
754
|
+
await tx`SELECT record_scheduled_task_revision_authority(${task.account_id}::uuid,
|
|
755
|
+
${task.workspace_id}::uuid,${task.id}::uuid,${updated.authority_revision}::bigint)`;
|
|
756
|
+
}
|
|
757
|
+
await tx`ALTER TABLE scheduled_tasks ENABLE TRIGGER scheduled_task_tombstone_immutable`;
|
|
758
|
+
await tx`ALTER TABLE scheduled_tasks FORCE ROW LEVEL SECURITY`;
|
|
759
|
+
await tx`SELECT set_config('opengeni.account_id','',true),set_config('opengeni.workspace_id','',true),
|
|
760
|
+
set_config('opengeni.subject_id','',true),set_config('opengeni.initiating_human_subject_id','',true)`;
|
|
761
|
+
}
|