@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,1062 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOSSLESS_CONTENT_CODEC_VERSION,
|
|
3
|
+
fromPostgresLosslessText,
|
|
4
|
+
toPostgresLosslessJson,
|
|
5
|
+
toPostgresLosslessText
|
|
6
|
+
} from "./chunk-S2TZ3F4L.js";
|
|
7
|
+
|
|
8
|
+
// src/migrate.ts
|
|
9
|
+
import { readdir, readFile } from "fs/promises";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import postgres from "postgres";
|
|
13
|
+
|
|
14
|
+
// src/skill-metadata-migration.ts
|
|
15
|
+
import { readSkillMetadata } from "@opengeni/contracts";
|
|
16
|
+
var SKILL_METADATA_MIGRATION_MARKER = "-- opengeni:skill-metadata-stage-v1";
|
|
17
|
+
function prepareLegacySkillFolder(files, legacy) {
|
|
18
|
+
const main = files.find((file) => file.path === "SKILL.md");
|
|
19
|
+
if (!main) throw new Error(`Skill ${legacy.id} requires SKILL.md before migration`);
|
|
20
|
+
let content = main.content;
|
|
21
|
+
if (!/^\s*---(?:\r?\n|$)/u.test(content)) {
|
|
22
|
+
const slug = legacy.title.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
23
|
+
const name = slug && slug.length <= 64 ? slug : `legacy-${legacy.id.replaceAll("-", "")}`;
|
|
24
|
+
content = `---
|
|
25
|
+
name: ${JSON.stringify(name)}
|
|
26
|
+
description: ${JSON.stringify(legacy.description)}
|
|
27
|
+
---
|
|
28
|
+
${content}`;
|
|
29
|
+
}
|
|
30
|
+
const metadata = readSkillMetadata(content);
|
|
31
|
+
return {
|
|
32
|
+
files: files.map((file) => file.path === "SKILL.md" ? { ...file, content } : file),
|
|
33
|
+
...metadata
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function createSkillMetadataMigrationStage(tx) {
|
|
37
|
+
await tx`CREATE TEMP TABLE skill_metadata_0426 (
|
|
38
|
+
source_kind text NOT NULL, source_id uuid NOT NULL, account_id uuid NOT NULL,
|
|
39
|
+
workspace_id uuid, files jsonb NOT NULL, name text NOT NULL, description text NOT NULL
|
|
40
|
+
) ON COMMIT DROP`;
|
|
41
|
+
}
|
|
42
|
+
async function stageSkillMetadataMigration(tx) {
|
|
43
|
+
const sources = await tx`
|
|
44
|
+
SELECT 'installed' AS source_kind,sf.facet_id AS source_id,i.account_id,i.workspace_id,
|
|
45
|
+
sf.name AS title,sf.description,
|
|
46
|
+
coalesce(jsonb_agg(jsonb_build_object('path',ff.path,'content',ff.content) ORDER BY ff.path COLLATE "C")
|
|
47
|
+
FILTER (WHERE ff.id IS NOT NULL),'[]'::jsonb) AS files
|
|
48
|
+
FROM capability_plugin_installations i JOIN capability_facets f ON f.plugin_version_id=i.plugin_version_id
|
|
49
|
+
JOIN capability_skill_facets sf ON sf.facet_id=f.id LEFT JOIN capability_skill_files ff ON ff.skill_facet_id=sf.facet_id
|
|
50
|
+
WHERE i.status='active'
|
|
51
|
+
GROUP BY i.account_id,i.workspace_id,sf.facet_id
|
|
52
|
+
UNION ALL
|
|
53
|
+
SELECT 'authored',h.id,h.account_id,h.scope_workspace_id,r.title,r.description,
|
|
54
|
+
coalesce(r.skill_files,jsonb_build_array(jsonb_build_object('path','SKILL.md','content',r.content)))
|
|
55
|
+
FROM preference_registry_preferences h JOIN preference_registry_revisions r
|
|
56
|
+
ON r.id=h.active_revision_id AND r.preference_id=h.id AND r.account_id=h.account_id
|
|
57
|
+
WHERE h.status='active'
|
|
58
|
+
`;
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
let folder;
|
|
61
|
+
try {
|
|
62
|
+
folder = prepareLegacySkillFolder(source.files, {
|
|
63
|
+
id: source.source_id,
|
|
64
|
+
title: source.title,
|
|
65
|
+
description: source.description
|
|
66
|
+
});
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Skill ${source.source_kind}:${source.source_id} needs explicit frontmatter repair before 0433`,
|
|
70
|
+
{ cause }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const [valid] = await tx`SELECT skill_files_valid(${tx.json(folder.files)}) AS valid`;
|
|
74
|
+
if (!valid?.valid)
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Skill ${source.source_id} exceeds the valid folder limits after metadata migration`
|
|
77
|
+
);
|
|
78
|
+
await tx`INSERT INTO pg_temp.skill_metadata_0426(source_kind,source_id,account_id,workspace_id,files,name,description)
|
|
79
|
+
VALUES(${source.source_kind},${source.source_id},${source.account_id},${source.workspace_id},${tx.json(folder.files)},${folder.name},${folder.description})`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/knowledge-migration.ts
|
|
84
|
+
import { createHash } from "crypto";
|
|
85
|
+
import {
|
|
86
|
+
KnowledgeSourceSyncAction,
|
|
87
|
+
knowledgeSourceAgentConfig,
|
|
88
|
+
StoredKnowledgeEntryContent
|
|
89
|
+
} from "@opengeni/contracts";
|
|
90
|
+
var KNOWLEDGE_MIGRATION_MARKER = "-- opengeni:unified-knowledge-copy-v1";
|
|
91
|
+
function knowledgeMigrationId(kind, id) {
|
|
92
|
+
const hash = createHash("sha256").update(`opengeni:knowledge:0461:${kind}:${id}`).digest("hex").slice(0, 32);
|
|
93
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-5${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20)}`;
|
|
94
|
+
}
|
|
95
|
+
async function migrateMemoryEvidence(tx, memory) {
|
|
96
|
+
const evidence = [];
|
|
97
|
+
const references = memory.snapshot.source_refs;
|
|
98
|
+
if (!Array.isArray(references)) return evidence;
|
|
99
|
+
for (const ref of references) {
|
|
100
|
+
if (!ref || typeof ref !== "object" || typeof ref.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref.id))
|
|
101
|
+
continue;
|
|
102
|
+
let entryId;
|
|
103
|
+
let passage;
|
|
104
|
+
if (ref.kind === "memory") {
|
|
105
|
+
const [found] = await tx`SELECT id FROM knowledge_memories WHERE account_id=${memory.account_id} AND id=${ref.id}`;
|
|
106
|
+
if (found && found.id !== memory.id) entryId = String(found.id);
|
|
107
|
+
} else if (ref.kind === "document" || ref.kind === "document_chunk") {
|
|
108
|
+
const [doc] = ref.kind === "document_chunk" ? 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
|
|
109
|
+
WHERE c.account_id=${memory.account_id} AND c.id=${ref.id}` : await tx`SELECT id,status FROM documents WHERE account_id=${memory.account_id} AND id=${ref.id}`;
|
|
110
|
+
if (doc) {
|
|
111
|
+
const [version] = await tx`SELECT id FROM knowledge_document_versions WHERE account_id=${memory.account_id}
|
|
112
|
+
AND document_id=${doc.id} ORDER BY created_at DESC,id DESC LIMIT 1`;
|
|
113
|
+
if (version || doc.status === "ready")
|
|
114
|
+
entryId = knowledgeMigrationId(
|
|
115
|
+
version ? "document-version" : "document",
|
|
116
|
+
String(version?.id ?? doc.id)
|
|
117
|
+
);
|
|
118
|
+
if (ref.kind === "document_chunk") passage = `Legacy passage ${ref.id}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (entryId && !evidence.some((item) => item.entryId === entryId && item.location.passage === passage)) {
|
|
122
|
+
evidence.push({
|
|
123
|
+
entryId,
|
|
124
|
+
revisionId: knowledgeMigrationId("revision", entryId),
|
|
125
|
+
location: passage ? { passage } : {}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return evidence;
|
|
130
|
+
}
|
|
131
|
+
async function importEntry(tx, input) {
|
|
132
|
+
const body = StoredKnowledgeEntryContent.parse(input.body);
|
|
133
|
+
const revisionId = knowledgeMigrationId("revision", input.id);
|
|
134
|
+
const outcome = ["active", "approved", "ready"].includes(input.status) ? "published" : input.status === "proposed" ? "pending" : "rejected";
|
|
135
|
+
const archived = !["active", "approved", "ready", "proposed"].includes(input.status);
|
|
136
|
+
const migrationActor = { kind: "migration", subjectId: "service:knowledge-migration:0461" };
|
|
137
|
+
const actor = input.originalActor ?? migrationActor;
|
|
138
|
+
const batchId = outcome === "pending" ? knowledgeMigrationId(
|
|
139
|
+
"review-batch",
|
|
140
|
+
`${input.accountId}:${input.workspaceId}:${input.scope}:${input.subjectId ?? ""}:${input.sessionId ?? input.id}`
|
|
141
|
+
) : null;
|
|
142
|
+
if (batchId)
|
|
143
|
+
await tx`INSERT INTO knowledge_review_batches(id,account_id,origin_workspace_id,owner_key,session_id,created_at)
|
|
144
|
+
VALUES(${batchId},${input.accountId},${input.workspaceId},${input.scope === "personal" ? `personal:${input.subjectId}` : `${input.scope}:${input.workspaceId}`},
|
|
145
|
+
${input.sessionId ?? null},${input.createdAt}) ON CONFLICT(id) DO NOTHING`;
|
|
146
|
+
await tx`INSERT INTO knowledge_entries(id,account_id,origin_workspace_id,scope,scope_workspace_id,scope_subject_id,
|
|
147
|
+
version,published_revision_id,latest_revision_id,archived,legacy_memory_id,legacy_document_id,
|
|
148
|
+
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)
|
|
149
|
+
VALUES(${input.id},${input.accountId},${input.workspaceId},${input.scope},${input.scope === "workspace" ? input.workspaceId : null},
|
|
150
|
+
${input.subjectId},1,${outcome === "published" ? revisionId : null},${revisionId},${archived},${input.memoryId ?? null},${input.documentId ?? null},
|
|
151
|
+
${input.legacyScopeType ?? null},${input.roleKey ?? null},${input.scopeSessionId ?? null},${input.legacyDocumentVersionId ?? null},${input.legacyClaimId ?? null},${input.legacyScopeWorkspaceId ?? null},${input.validFrom ?? input.createdAt},
|
|
152
|
+
${input.validUntil ?? null},${input.createdAt},${input.createdAt})`;
|
|
153
|
+
await tx`INSERT INTO knowledge_entry_revisions(id,account_id,entry_id,number,body,body_codec_version,preview,preview_codec_version,
|
|
154
|
+
actor,created_by_session_id,review_batch_id,legacy_snapshot,created_at)
|
|
155
|
+
VALUES(${revisionId},${input.accountId},${input.id},1,${tx.json(toPostgresLosslessJson(body))},
|
|
156
|
+
${LOSSLESS_CONTENT_CODEC_VERSION},${toPostgresLosslessText(body.content.slice(0, 512))},${LOSSLESS_CONTENT_CODEC_VERSION},
|
|
157
|
+
${tx.json(actor)},${input.sessionId ?? null},${batchId},${input.legacySnapshot ? tx.json(input.legacySnapshot) : null},${input.createdAt})`;
|
|
158
|
+
await tx`INSERT INTO knowledge_entry_decisions(account_id,entry_id,revision_id,version,outcome,actor,created_at)
|
|
159
|
+
VALUES(${input.accountId},${input.id},${revisionId},1,${outcome},${tx.json(migrationActor)},${input.createdAt})`;
|
|
160
|
+
await tx`SELECT knowledge_index_revision(${input.accountId},${input.id},${revisionId},
|
|
161
|
+
${toPostgresLosslessText(`${body.title}
|
|
162
|
+
${body.content}`)})`;
|
|
163
|
+
}
|
|
164
|
+
async function importGroup(tx, owner, title, sourceKey) {
|
|
165
|
+
const id = knowledgeMigrationId(
|
|
166
|
+
"group",
|
|
167
|
+
`${owner.accountId}:${owner.scope}:${owner.scope === "workspace" ? owner.workspaceId : owner.subjectId ?? ""}:${sourceKey}`
|
|
168
|
+
);
|
|
169
|
+
const [existing] = await tx`SELECT id FROM knowledge_entries WHERE id=${id}`;
|
|
170
|
+
if (!existing)
|
|
171
|
+
await importEntry(tx, {
|
|
172
|
+
...owner,
|
|
173
|
+
id,
|
|
174
|
+
status: "ready",
|
|
175
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
176
|
+
body: { title, kind: "group", content: "", groupIds: [], evidence: [], relationships: [] }
|
|
177
|
+
});
|
|
178
|
+
return id;
|
|
179
|
+
}
|
|
180
|
+
async function migratePendingLegacySkills(tx) {
|
|
181
|
+
let after = null;
|
|
182
|
+
for (; ; ) {
|
|
183
|
+
const candidates = await tx`
|
|
184
|
+
SELECT receipt.id,receipt.workspace_id,receipt.account_id,receipt.preference_id,receipt.revision_id,
|
|
185
|
+
revision.content,revision.title,revision.description,receipt.session_id,head.active_revision_id,head.scope_version
|
|
186
|
+
FROM company_brain_preference_proposal_receipts receipt
|
|
187
|
+
JOIN preference_registry_preferences head ON head.id=receipt.preference_id AND head.account_id=receipt.account_id
|
|
188
|
+
JOIN preference_registry_revisions revision ON revision.id=receipt.revision_id AND revision.preference_id=head.id
|
|
189
|
+
JOIN knowledge_change_proposals proposal ON proposal.id=receipt.knowledge_proposal_id AND proposal.account_id=receipt.account_id
|
|
190
|
+
WHERE (${after}::uuid IS NULL OR receipt.id>${after}::uuid)
|
|
191
|
+
AND revision.skill_files IS NULL AND head.status NOT IN ('rejected','superseded')
|
|
192
|
+
AND proposal.status='proposed' AND proposal.target_kind='preference'
|
|
193
|
+
AND head.scope='workspace' AND head.scope_workspace_id=receipt.workspace_id
|
|
194
|
+
AND proposal.scope_workspace_id=receipt.workspace_id AND proposal.target_scope='workspace'
|
|
195
|
+
AND coalesce((SELECT review.state FROM knowledge_claim_reviews review WHERE review.claim_id=proposal.claim_id
|
|
196
|
+
AND review.account_id=receipt.account_id ORDER BY review.review_revision DESC LIMIT 1),'proposed') IN ('proposed','approved')
|
|
197
|
+
AND NOT EXISTS(SELECT 1 FROM preference_registry_events event WHERE event.preference_id=head.id
|
|
198
|
+
AND event.new_revision_id=revision.id AND event.type IN ('activated','corrected','rejected'))
|
|
199
|
+
AND NOT EXISTS(SELECT 1 FROM preference_registry_revisions newer WHERE newer.preference_id=head.id
|
|
200
|
+
AND newer.revision>revision.revision AND newer.created_by_subject_id<>'service:skill-migration:0433')
|
|
201
|
+
ORDER BY receipt.id LIMIT 100`;
|
|
202
|
+
if (!candidates.length) break;
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const folder = prepareLegacySkillFolder([{ path: "SKILL.md", content: candidate.content }], {
|
|
205
|
+
id: candidate.preference_id,
|
|
206
|
+
title: candidate.title,
|
|
207
|
+
description: candidate.description
|
|
208
|
+
});
|
|
209
|
+
const main = folder.files.find((file) => file.path === "SKILL.md").content;
|
|
210
|
+
const revisionId = knowledgeMigrationId("pending-skill-revision", candidate.id);
|
|
211
|
+
const operationId = knowledgeMigrationId("pending-skill-operation", candidate.id);
|
|
212
|
+
await tx`INSERT INTO preference_registry_revisions(id,account_id,preference_id,title,description,content,content_hash,
|
|
213
|
+
precedence_rank,conflict_strategy,conflicts_with,provenance_source,provenance_source_id,trust,expires_at,
|
|
214
|
+
created_by_subject_id,corrects_revision_id,skill_files,skill_activation_mode)
|
|
215
|
+
SELECT ${revisionId}::uuid,account_id,preference_id,${folder.name},${folder.description},${main},
|
|
216
|
+
${createHash("sha256").update(main, "utf8").digest("hex")},precedence_rank,conflict_strategy,conflicts_with,
|
|
217
|
+
provenance_source,provenance_source_id,trust,expires_at,'service:knowledge-migration:0461',id,
|
|
218
|
+
${tx.json(folder.files)},'workspace_managed'
|
|
219
|
+
FROM preference_registry_revisions WHERE id=${candidate.revision_id}::uuid`;
|
|
220
|
+
await tx`INSERT INTO skill_write_receipts(account_id,workspace_id,operation_id,fingerprint,actor,receipt)
|
|
221
|
+
VALUES(${candidate.account_id}::uuid,${candidate.workspace_id}::uuid,${operationId}::uuid,
|
|
222
|
+
${createHash("sha256").update(operationId).digest("hex")},
|
|
223
|
+
${tx.json({
|
|
224
|
+
kind: "migration",
|
|
225
|
+
subjectId: "service:knowledge-migration:0461",
|
|
226
|
+
legacyProposalReceiptId: candidate.id,
|
|
227
|
+
legacyRevisionId: candidate.revision_id,
|
|
228
|
+
sessionId: candidate.session_id
|
|
229
|
+
})},
|
|
230
|
+
${tx.json({
|
|
231
|
+
operationId,
|
|
232
|
+
skillId: candidate.preference_id,
|
|
233
|
+
revisionId,
|
|
234
|
+
outcome: "pending",
|
|
235
|
+
pendingReason: "approval",
|
|
236
|
+
replayed: false
|
|
237
|
+
})})`;
|
|
238
|
+
}
|
|
239
|
+
after = candidates.at(-1).id;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function migrateRetainedKnowledge(tx) {
|
|
243
|
+
await migratePendingLegacySkills(tx);
|
|
244
|
+
await migrateSourceSchedules(tx);
|
|
245
|
+
let after = null;
|
|
246
|
+
for (; ; ) {
|
|
247
|
+
const memories = await tx`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,
|
|
248
|
+
m.scope_role_key,m.scope_session_id,m.created_by_session_id,
|
|
249
|
+
(SELECT r.claim_id FROM remember_knowledge_memory_materializations map
|
|
250
|
+
JOIN remember_knowledge_confirmation_receipts r ON r.id=map.confirmation_receipt_id
|
|
251
|
+
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,
|
|
252
|
+
(to_jsonb(m)-'embedding'-'text') || jsonb_build_object('relationships',coalesce((SELECT jsonb_agg(to_jsonb(edge) ORDER BY edge.id)
|
|
253
|
+
FROM knowledge_memory_relationships edge WHERE edge.account_id=m.account_id
|
|
254
|
+
AND (edge.source_memory_id=m.id OR edge.target_memory_id=m.id)),'[]'::jsonb),
|
|
255
|
+
'lifecycleEvents',coalesce((SELECT jsonb_agg(to_jsonb(event) ORDER BY event.created_at,event.id)
|
|
256
|
+
FROM knowledge_memory_lifecycle_events event WHERE event.account_id=m.account_id AND event.target_memory_id=m.id),'[]'::jsonb)) AS snapshot,
|
|
257
|
+
coalesce((SELECT jsonb_agg(jsonb_build_object('entryId',edge.target_memory_id,'relation',
|
|
258
|
+
CASE edge.relationship_type WHEN 'derived_from' THEN 'depends_on' WHEN 'corrects' THEN 'supersedes' ELSE edge.relationship_type END)
|
|
259
|
+
ORDER BY edge.id) FROM knowledge_memory_relationships edge
|
|
260
|
+
WHERE edge.account_id=m.account_id AND edge.source_memory_id=m.id AND edge.removed_at IS NULL),'[]'::jsonb) AS relationships
|
|
261
|
+
FROM knowledge_memories m WHERE (${after}::uuid IS NULL OR m.id>${after}::uuid) ORDER BY m.id LIMIT 200`;
|
|
262
|
+
if (!memories.length) break;
|
|
263
|
+
for (const memory of memories) {
|
|
264
|
+
const content = fromPostgresLosslessText(memory.text, memory.text_codec_version);
|
|
265
|
+
const owner = {
|
|
266
|
+
accountId: memory.account_id,
|
|
267
|
+
workspaceId: memory.workspace_id,
|
|
268
|
+
scope: memory.scope_type === "user" ? "personal" : "workspace",
|
|
269
|
+
subjectId: memory.scope_subject_id
|
|
270
|
+
};
|
|
271
|
+
const groupIds = [];
|
|
272
|
+
if (memory.scope_type === "workspace") {
|
|
273
|
+
if (memory.namespace && memory.namespace !== "general")
|
|
274
|
+
groupIds.push(
|
|
275
|
+
await importGroup(tx, owner, memory.namespace, `namespace:${memory.namespace}`)
|
|
276
|
+
);
|
|
277
|
+
for (const label of memory.labels)
|
|
278
|
+
groupIds.push(await importGroup(tx, owner, label, `label:${label}`));
|
|
279
|
+
}
|
|
280
|
+
await importEntry(tx, {
|
|
281
|
+
...owner,
|
|
282
|
+
id: memory.id,
|
|
283
|
+
memoryId: memory.id,
|
|
284
|
+
...memory.confirmed_claim_id ? { legacyClaimId: memory.confirmed_claim_id } : {},
|
|
285
|
+
legacySnapshot: memory.snapshot,
|
|
286
|
+
originalActor: {
|
|
287
|
+
kind: memory.snapshot.created_by_kind,
|
|
288
|
+
subjectId: memory.snapshot.created_by_subject_id,
|
|
289
|
+
context: memory.snapshot.created_by_context
|
|
290
|
+
},
|
|
291
|
+
status: memory.status,
|
|
292
|
+
legacyScopeType: memory.scope_type,
|
|
293
|
+
roleKey: memory.scope_role_key,
|
|
294
|
+
scopeSessionId: memory.scope_session_id,
|
|
295
|
+
sessionId: memory.created_by_session_id,
|
|
296
|
+
createdAt: memory.created_at,
|
|
297
|
+
validFrom: memory.valid_from,
|
|
298
|
+
validUntil: memory.valid_until,
|
|
299
|
+
body: {
|
|
300
|
+
title: content.split(/\r?\n/u, 1)[0]?.slice(0, 160) || "Imported knowledge",
|
|
301
|
+
content,
|
|
302
|
+
kind: memory.kind === "episodic" ? "incident" : memory.kind === "semantic" ? "fact" : memory.kind === "decision" ? "decision" : "note",
|
|
303
|
+
source: { kind: "manual", externalId: `memory:${memory.id}` },
|
|
304
|
+
groupIds: [...new Set(groupIds)],
|
|
305
|
+
evidence: await migrateMemoryEvidence(tx, memory),
|
|
306
|
+
relationships: memory.relationships.map((relationship) => ({
|
|
307
|
+
entryId: relationship.entryId,
|
|
308
|
+
relation: relationship.relation
|
|
309
|
+
}))
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
after = memories.at(-1).id;
|
|
314
|
+
}
|
|
315
|
+
after = null;
|
|
316
|
+
for (; ; ) {
|
|
317
|
+
const documents = await tx`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,
|
|
318
|
+
d.authority_subject_id,d.source_uri,d.source_external_id,d.source_version,d.created_at
|
|
319
|
+
FROM documents d JOIN document_bases b ON b.id=d.base_id AND b.account_id=d.account_id
|
|
320
|
+
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)
|
|
321
|
+
AND (${after}::uuid IS NULL OR d.id>${after}::uuid) ORDER BY d.id LIMIT 50`;
|
|
322
|
+
if (!documents.length) break;
|
|
323
|
+
for (const document of documents) {
|
|
324
|
+
const chunks = await tx`SELECT text FROM document_chunks
|
|
325
|
+
WHERE account_id=${document.account_id} AND document_id=${document.id} ORDER BY chunk_index`;
|
|
326
|
+
const owner = {
|
|
327
|
+
accountId: document.account_id,
|
|
328
|
+
workspaceId: document.workspace_id,
|
|
329
|
+
scope: document.authority_kind,
|
|
330
|
+
subjectId: document.authority_subject_id
|
|
331
|
+
};
|
|
332
|
+
const groupIds = document.authority_kind === "workspace" && document.base_name ? [await importGroup(tx, owner, document.base_name, `document-base:${document.base_id}`)] : [];
|
|
333
|
+
await importEntry(tx, {
|
|
334
|
+
...owner,
|
|
335
|
+
id: knowledgeMigrationId("document", document.id),
|
|
336
|
+
documentId: document.id,
|
|
337
|
+
status: "ready",
|
|
338
|
+
createdAt: document.created_at,
|
|
339
|
+
body: {
|
|
340
|
+
title: document.title,
|
|
341
|
+
kind: "source",
|
|
342
|
+
// Historical chunks may overlap. Preserve every retained passage;
|
|
343
|
+
// never claim to have reconstructed missing original full text.
|
|
344
|
+
content: chunks.map((chunk) => chunk.text).join("\n\n"),
|
|
345
|
+
groupIds,
|
|
346
|
+
evidence: [],
|
|
347
|
+
relationships: [],
|
|
348
|
+
source: {
|
|
349
|
+
kind: "file",
|
|
350
|
+
fileId: document.file_id,
|
|
351
|
+
documentId: document.id,
|
|
352
|
+
retention: "passages",
|
|
353
|
+
...document.source_uri ? { uri: document.source_uri } : {},
|
|
354
|
+
...document.source_external_id ? { externalId: document.source_external_id } : {},
|
|
355
|
+
...document.source_version ? { version: document.source_version } : {}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
after = documents.at(-1).id;
|
|
361
|
+
}
|
|
362
|
+
await importScopedKnowledge(tx);
|
|
363
|
+
await tx`INSERT INTO knowledge_entry_links(account_id,entry_id,revision_id,ordinal,target_entry_id,relation,target_revision_id)
|
|
364
|
+
SELECT r.account_id,r.entry_id,r.id,(g.ordinality-1)::integer,g.value::uuid,'group',NULL::uuid
|
|
365
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements_text(r.body->'groupIds') WITH ORDINALITY g(value,ordinality)
|
|
366
|
+
UNION ALL
|
|
367
|
+
SELECT r.account_id,r.entry_id,r.id,jsonb_array_length(r.body->'groupIds')+(l.ordinality-1)::integer,
|
|
368
|
+
(l.value->>'entryId')::uuid,l.value->>'relation',NULL::uuid
|
|
369
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements(r.body->'relationships') WITH ORDINALITY l(value,ordinality)
|
|
370
|
+
UNION ALL
|
|
371
|
+
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,
|
|
372
|
+
(e.value->>'entryId')::uuid,'evidence',(e.value->>'revisionId')::uuid
|
|
373
|
+
FROM knowledge_entry_revisions r CROSS JOIN LATERAL jsonb_array_elements(r.body->'evidence') WITH ORDINALITY e(value,ordinality)`;
|
|
374
|
+
}
|
|
375
|
+
async function importScopedKnowledge(tx) {
|
|
376
|
+
function owner(row) {
|
|
377
|
+
return {
|
|
378
|
+
accountId: row.account_id,
|
|
379
|
+
// Some organization/user records never had a creating workspace. This
|
|
380
|
+
// deterministic import origin is provenance only and grants no access.
|
|
381
|
+
workspaceId: row.scope_workspace_id ?? knowledgeMigrationId("import-origin", row.account_id),
|
|
382
|
+
scope: row.scope_kind,
|
|
383
|
+
subjectId: row.scope_subject_id
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function actor(row) {
|
|
387
|
+
return {
|
|
388
|
+
kind: row.actor_kind,
|
|
389
|
+
subjectId: row.actor_subject_id,
|
|
390
|
+
initiatingHumanSubjectId: row.initiating_human_subject_id
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
let after = null;
|
|
394
|
+
for (; ; ) {
|
|
395
|
+
const entities = await tx`
|
|
396
|
+
SELECT e.*,to_jsonb(e) AS snapshot,coalesce((SELECT jsonb_agg(a.alias ORDER BY a.id)
|
|
397
|
+
FROM knowledge_entity_aliases a WHERE a.account_id=e.account_id AND a.entity_id=e.id),'[]'::jsonb) AS aliases
|
|
398
|
+
FROM knowledge_entities e WHERE (${after}::uuid IS NULL OR e.id>${after}::uuid)
|
|
399
|
+
AND (NOT EXISTS(SELECT 1 FROM knowledge_facts f JOIN knowledge_claims c ON c.fact_id=f.id
|
|
400
|
+
WHERE f.account_id=e.account_id AND (f.subject_entity_id=e.id OR f.object_entity_id=e.id))
|
|
401
|
+
OR EXISTS(SELECT 1 FROM knowledge_facts f JOIN knowledge_claims c ON c.fact_id=f.id
|
|
402
|
+
WHERE f.account_id=e.account_id AND (f.subject_entity_id=e.id OR f.object_entity_id=e.id)
|
|
403
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_change_proposals p WHERE p.claim_id=c.id AND p.account_id=c.account_id)))
|
|
404
|
+
ORDER BY e.id LIMIT 200`;
|
|
405
|
+
if (!entities.length) break;
|
|
406
|
+
for (const entity of entities)
|
|
407
|
+
await importEntry(tx, {
|
|
408
|
+
...owner(entity),
|
|
409
|
+
id: knowledgeMigrationId("entity", entity.id),
|
|
410
|
+
status: "approved",
|
|
411
|
+
createdAt: entity.created_at,
|
|
412
|
+
legacyScopeWorkspaceId: entity.scope_workspace_id,
|
|
413
|
+
originalActor: actor(entity),
|
|
414
|
+
legacySnapshot: { entity: entity.snapshot, aliases: entity.aliases },
|
|
415
|
+
body: {
|
|
416
|
+
title: entity.display_name,
|
|
417
|
+
kind: "group",
|
|
418
|
+
content: entity.aliases.join("\n"),
|
|
419
|
+
groupIds: [],
|
|
420
|
+
evidence: [],
|
|
421
|
+
relationships: []
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
after = entities.at(-1).id;
|
|
425
|
+
}
|
|
426
|
+
after = null;
|
|
427
|
+
for (; ; ) {
|
|
428
|
+
const versions = await tx`
|
|
429
|
+
SELECT v.*,coalesce(d.title,o.external_object_id) AS title,s.source_uri,
|
|
430
|
+
jsonb_build_object('version',to_jsonb(v),'source',to_jsonb(s),'object',to_jsonb(o),'provider',to_jsonb(p)) AS snapshot
|
|
431
|
+
FROM knowledge_document_versions v JOIN knowledge_source_objects o ON o.account_id=v.account_id AND o.id=v.object_id
|
|
432
|
+
JOIN knowledge_sources s ON s.account_id=v.account_id AND s.id=v.source_id
|
|
433
|
+
JOIN knowledge_providers p ON p.account_id=s.account_id AND p.id=s.provider_id
|
|
434
|
+
LEFT JOIN documents d ON d.account_id=v.account_id AND d.id=v.document_id
|
|
435
|
+
WHERE (${after}::uuid IS NULL OR v.id>${after}::uuid) ORDER BY v.id LIMIT 100`;
|
|
436
|
+
if (!versions.length) break;
|
|
437
|
+
for (const version of versions) {
|
|
438
|
+
const chunks = version.document_id ? await tx`SELECT text FROM document_chunks
|
|
439
|
+
WHERE account_id=${version.account_id} AND document_id=${version.document_id} ORDER BY chunk_index` : [];
|
|
440
|
+
await importEntry(tx, {
|
|
441
|
+
...owner(version),
|
|
442
|
+
id: knowledgeMigrationId("document-version", version.id),
|
|
443
|
+
legacyDocumentVersionId: version.id,
|
|
444
|
+
legacyScopeWorkspaceId: version.scope_workspace_id,
|
|
445
|
+
status: "ready",
|
|
446
|
+
createdAt: version.created_at,
|
|
447
|
+
originalActor: actor(version),
|
|
448
|
+
legacySnapshot: version.snapshot,
|
|
449
|
+
body: {
|
|
450
|
+
title: version.title,
|
|
451
|
+
kind: "source",
|
|
452
|
+
content: chunks.map((c) => c.text).join("\n\n"),
|
|
453
|
+
groupIds: [],
|
|
454
|
+
evidence: [],
|
|
455
|
+
relationships: [],
|
|
456
|
+
source: {
|
|
457
|
+
kind: version.file_id ? "file" : "connector",
|
|
458
|
+
externalId: `knowledge-document-version:${version.id}`,
|
|
459
|
+
version: version.external_version_id,
|
|
460
|
+
retention: chunks.length ? "passages" : "reference",
|
|
461
|
+
...version.file_id ? { fileId: version.file_id } : {},
|
|
462
|
+
...version.document_id ? { documentId: version.document_id } : {},
|
|
463
|
+
...version.source_uri ? { uri: version.source_uri } : {}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
after = versions.at(-1).id;
|
|
469
|
+
}
|
|
470
|
+
after = null;
|
|
471
|
+
for (; ; ) {
|
|
472
|
+
const claims = await tx`
|
|
473
|
+
SELECT c.*,f.subject_entity_id,f.object_entity_id,f.predicate_key,f.object_value,
|
|
474
|
+
subject.display_name AS subject_name,object.display_name AS object_name,
|
|
475
|
+
coalesce((SELECT state FROM knowledge_claim_reviews r WHERE r.account_id=c.account_id AND r.claim_id=c.id
|
|
476
|
+
ORDER BY r.review_revision DESC LIMIT 1),'proposed') AS state,
|
|
477
|
+
jsonb_build_object('claim',to_jsonb(c),'fact',to_jsonb(f),
|
|
478
|
+
'evidence',coalesce((SELECT jsonb_agg(to_jsonb(e) ORDER BY e.id) FROM knowledge_claim_evidence e
|
|
479
|
+
WHERE e.account_id=c.account_id AND e.claim_id=c.id),'[]'::jsonb),
|
|
480
|
+
'reviews',coalesce((SELECT jsonb_agg(to_jsonb(r) ORDER BY r.review_revision) FROM knowledge_claim_reviews r
|
|
481
|
+
WHERE r.account_id=c.account_id AND r.claim_id=c.id),'[]'::jsonb)) AS snapshot
|
|
482
|
+
FROM knowledge_claims c JOIN knowledge_facts f ON f.account_id=c.account_id AND f.id=c.fact_id
|
|
483
|
+
JOIN knowledge_entities subject ON subject.account_id=f.account_id AND subject.id=f.subject_entity_id
|
|
484
|
+
LEFT JOIN knowledge_entities object ON object.account_id=f.account_id AND object.id=f.object_entity_id
|
|
485
|
+
WHERE (${after}::uuid IS NULL OR c.id>${after}::uuid)
|
|
486
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_entries imported WHERE imported.legacy_claim_id=c.id)
|
|
487
|
+
AND NOT EXISTS(SELECT 1 FROM knowledge_change_proposals p WHERE p.account_id=c.account_id AND p.claim_id=c.id
|
|
488
|
+
)
|
|
489
|
+
ORDER BY c.id LIMIT 100`;
|
|
490
|
+
if (!claims.length) break;
|
|
491
|
+
for (const claim of claims) {
|
|
492
|
+
const retained = await tx`
|
|
493
|
+
SELECT document_version_id,locator FROM knowledge_claim_evidence WHERE account_id=${claim.account_id}
|
|
494
|
+
AND claim_id=${claim.id} AND document_version_id IS NOT NULL ORDER BY id`;
|
|
495
|
+
const relations = await tx`
|
|
496
|
+
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
|
|
497
|
+
WHERE r.account_id=${claim.account_id} AND r.from_claim_id=${claim.id}
|
|
498
|
+
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`;
|
|
499
|
+
const objectText = claim.object_name ?? (typeof claim.object_value === "string" ? claim.object_value : JSON.stringify(claim.object_value));
|
|
500
|
+
await importEntry(tx, {
|
|
501
|
+
...owner(claim),
|
|
502
|
+
id: knowledgeMigrationId("claim", claim.id),
|
|
503
|
+
legacyClaimId: claim.id,
|
|
504
|
+
legacyScopeWorkspaceId: claim.scope_workspace_id,
|
|
505
|
+
status: claim.state,
|
|
506
|
+
createdAt: claim.created_at,
|
|
507
|
+
validFrom: claim.effective_at,
|
|
508
|
+
validUntil: claim.expires_at,
|
|
509
|
+
originalActor: actor(claim),
|
|
510
|
+
legacySnapshot: claim.snapshot,
|
|
511
|
+
body: {
|
|
512
|
+
title: `${claim.subject_name}: ${claim.predicate_key}`,
|
|
513
|
+
kind: "fact",
|
|
514
|
+
content: claim.extraction_method === "task-note-promotion-v1" && typeof claim.object_value === "string" ? claim.object_value : `${claim.subject_name} \xB7 ${claim.predicate_key}: ${objectText}`,
|
|
515
|
+
source: { kind: "manual", externalId: `knowledge-claim:${claim.id}` },
|
|
516
|
+
groupIds: [knowledgeMigrationId("entity", claim.subject_entity_id)],
|
|
517
|
+
evidence: retained.map((e) => {
|
|
518
|
+
const id = knowledgeMigrationId("document-version", e.document_version_id);
|
|
519
|
+
return {
|
|
520
|
+
entryId: id,
|
|
521
|
+
revisionId: knowledgeMigrationId("revision", id),
|
|
522
|
+
location: e.locator ? { passage: e.locator } : {}
|
|
523
|
+
};
|
|
524
|
+
}),
|
|
525
|
+
relationships: [
|
|
526
|
+
...relations.map((r) => ({
|
|
527
|
+
entryId: r.imported_entry_id ?? knowledgeMigrationId("claim", r.to_claim_id),
|
|
528
|
+
relation: r.relation_type
|
|
529
|
+
})),
|
|
530
|
+
...claim.object_entity_id ? [
|
|
531
|
+
{
|
|
532
|
+
entryId: knowledgeMigrationId("entity", claim.object_entity_id),
|
|
533
|
+
relation: "related_to"
|
|
534
|
+
}
|
|
535
|
+
] : []
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
after = claims.at(-1).id;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function migrateSourceSchedules(tx) {
|
|
544
|
+
await tx`ALTER TABLE scheduled_tasks NO FORCE ROW LEVEL SECURITY`;
|
|
545
|
+
await tx`ALTER TABLE scheduled_tasks DISABLE TRIGGER scheduled_task_tombstone_immutable`;
|
|
546
|
+
const tasks = await tx`SELECT id,account_id,workspace_id,action,authority_revision FROM scheduled_tasks
|
|
547
|
+
WHERE action->>'kind'='knowledge_source_sync' AND deleted_at IS NULL ORDER BY id`;
|
|
548
|
+
for (const task of tasks) {
|
|
549
|
+
const source = KnowledgeSourceSyncAction.parse(task.action);
|
|
550
|
+
await tx`SELECT set_config('opengeni.account_id',${task.account_id},true),
|
|
551
|
+
set_config('opengeni.workspace_id',${task.workspace_id},true),
|
|
552
|
+
set_config('opengeni.subject_id',${source.initiatingSubjectId},true),
|
|
553
|
+
set_config('opengeni.initiating_human_subject_id',${source.initiatingSubjectId},true)`;
|
|
554
|
+
await tx`SELECT pg_advisory_xact_lock_shared(hashtextextended('session-tenancy:'||${task.workspace_id}::text,0))`;
|
|
555
|
+
await tx`SELECT id FROM workspaces WHERE id=${task.workspace_id} FOR UPDATE`;
|
|
556
|
+
const [authority] = await tx`SELECT id FROM organization_memberships m
|
|
557
|
+
WHERE m.account_id=${task.account_id} AND m.subject_id=${source.initiatingSubjectId}
|
|
558
|
+
AND m.status='active' AND m.revoked_at IS NULL
|
|
559
|
+
AND (m.personal_workspace_id=${task.workspace_id} OR EXISTS(SELECT 1 FROM workspace_memberships w
|
|
560
|
+
WHERE w.account_id=m.account_id AND w.workspace_id=${task.workspace_id} AND w.subject_id=m.subject_id))`;
|
|
561
|
+
await tx`UPDATE scheduled_task_runs SET status='failed',completed_at=clock_timestamp(),
|
|
562
|
+
error='knowledge_source_agent_cutover',updated_at=clock_timestamp()
|
|
563
|
+
WHERE task_id=${task.id} AND action_kind='knowledge_source_sync' AND status IN ('queued','dispatched')`;
|
|
564
|
+
await tx`UPDATE knowledge_source_sync_wakes SET completed_at=clock_timestamp()
|
|
565
|
+
WHERE scheduled_task_id=${task.id} AND completed_at IS NULL`;
|
|
566
|
+
await tx`UPDATE knowledge_source_sync_states SET lease_id=NULL,lease_until=NULL,
|
|
567
|
+
buffered_wake=false,buffered_scheduled_task_run_id=NULL,pending_wake_count=0,updated_at=clock_timestamp()
|
|
568
|
+
WHERE scheduled_task_id=${task.id}`;
|
|
569
|
+
if (!authority)
|
|
570
|
+
await tx`UPDATE scheduled_tasks SET status='paused',
|
|
571
|
+
metadata=jsonb_set(metadata,'{knowledgeSourceSync}',coalesce(metadata->'knowledgeSourceSync','{}'::jsonb)
|
|
572
|
+
|| '{"sourceEnabled":false,"authorityRefreshRequired":true}'::jsonb) WHERE id=${task.id}`;
|
|
573
|
+
const [updated] = await tx`UPDATE scheduled_tasks SET action='{"kind":"agent_turn"}',
|
|
574
|
+
agent_config=${JSON.stringify(knowledgeSourceAgentConfig(source))}::jsonb, authority_revision=authority_revision+1,
|
|
575
|
+
updated_at=clock_timestamp() WHERE id=${task.id} RETURNING authority_revision`;
|
|
576
|
+
if (!updated) throw new Error("Source schedule disappeared during maintenance");
|
|
577
|
+
await tx`SELECT freeze_scheduled_task_personal_resources(${task.account_id}::uuid,
|
|
578
|
+
${task.workspace_id}::uuid,${task.id}::uuid,${updated.authority_revision}::bigint)`;
|
|
579
|
+
if (authority)
|
|
580
|
+
await tx`SELECT record_scheduled_task_revision_authority(${task.account_id}::uuid,
|
|
581
|
+
${task.workspace_id}::uuid,${task.id}::uuid,${updated.authority_revision}::bigint)`;
|
|
582
|
+
}
|
|
583
|
+
await tx`ALTER TABLE scheduled_tasks ENABLE TRIGGER scheduled_task_tombstone_immutable`;
|
|
584
|
+
await tx`ALTER TABLE scheduled_tasks FORCE ROW LEVEL SECURITY`;
|
|
585
|
+
await tx`SELECT set_config('opengeni.account_id','',true),set_config('opengeni.workspace_id','',true),
|
|
586
|
+
set_config('opengeni.subject_id','',true),set_config('opengeni.initiating_human_subject_id','',true)`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/skill-config-migration.ts
|
|
590
|
+
import { createHash as createHash2 } from "crypto";
|
|
591
|
+
import {
|
|
592
|
+
StoredCapabilityPack,
|
|
593
|
+
StoredSessionSkills,
|
|
594
|
+
validateSkillTextFiles
|
|
595
|
+
} from "@opengeni/contracts";
|
|
596
|
+
function object(value) {
|
|
597
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
598
|
+
throw new Error("Expected stored configuration object");
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
function convertLegacyConfigSkills(value, identity) {
|
|
602
|
+
if (!Array.isArray(value)) throw new Error("Expected stored Skill array");
|
|
603
|
+
const names = /* @__PURE__ */ new Set();
|
|
604
|
+
const converted = value.map((entry, index) => {
|
|
605
|
+
const skill = object(entry);
|
|
606
|
+
if (!Array.isArray(skill.files)) throw new Error("Expected stored Skill folder");
|
|
607
|
+
const files = skill.files.map((fileEntry) => {
|
|
608
|
+
const file = object(fileEntry);
|
|
609
|
+
if (typeof file.path !== "string" || typeof file.content !== "string")
|
|
610
|
+
throw new Error("Invalid stored Skill text file");
|
|
611
|
+
if (Object.keys(file).some((key) => key !== "path" && key !== "content"))
|
|
612
|
+
throw new Error("Unexpected stored Skill file fields");
|
|
613
|
+
return { path: file.path, content: file.content };
|
|
614
|
+
});
|
|
615
|
+
const main = files.find((file) => file.path === "SKILL.md");
|
|
616
|
+
const plain = main && !/^\s*---(?:\r?\n|$)/u.test(main.content);
|
|
617
|
+
if (plain && (typeof skill.name !== "string" || typeof skill.description !== "string"))
|
|
618
|
+
throw new Error("Plain Skill requires historical name and description");
|
|
619
|
+
const folder = prepareLegacySkillFolder(files, {
|
|
620
|
+
id: createHash2("sha256").update(`${identity}:${index}`).digest("hex").slice(0, 32),
|
|
621
|
+
title: typeof skill.name === "string" ? skill.name : "",
|
|
622
|
+
description: typeof skill.description === "string" ? skill.description : ""
|
|
623
|
+
});
|
|
624
|
+
validateSkillTextFiles(folder.files);
|
|
625
|
+
if (names.has(folder.name)) throw new Error(`Canonical Skill name collision: ${folder.name}`);
|
|
626
|
+
names.add(folder.name);
|
|
627
|
+
return plain ? { ...skill, ...folder } : entry;
|
|
628
|
+
});
|
|
629
|
+
StoredSessionSkills.parse(converted);
|
|
630
|
+
return converted;
|
|
631
|
+
}
|
|
632
|
+
function convertLegacyPackConfig(value, identity) {
|
|
633
|
+
const pack = object(value);
|
|
634
|
+
const converted = { ...pack };
|
|
635
|
+
if (pack.skills !== void 0)
|
|
636
|
+
converted.skills = convertLegacyConfigSkills(pack.skills, identity);
|
|
637
|
+
if (Array.isArray(pack.automationTemplates))
|
|
638
|
+
converted.automationTemplates = pack.automationTemplates.map((entry, index) => {
|
|
639
|
+
const template = object(entry);
|
|
640
|
+
const sessionTemplate = object(template.sessionTemplate);
|
|
641
|
+
return {
|
|
642
|
+
...template,
|
|
643
|
+
sessionTemplate: {
|
|
644
|
+
...sessionTemplate,
|
|
645
|
+
...sessionTemplate.skills !== void 0 ? {
|
|
646
|
+
skills: convertLegacyConfigSkills(
|
|
647
|
+
sessionTemplate.skills,
|
|
648
|
+
`${identity}:template:${index}`
|
|
649
|
+
)
|
|
650
|
+
} : {}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
});
|
|
654
|
+
StoredCapabilityPack.parse(converted);
|
|
655
|
+
return converted;
|
|
656
|
+
}
|
|
657
|
+
async function assertSkillConfigurationMaintenanceWindow(tx) {
|
|
658
|
+
const rows = await tx`SELECT c.oid FROM pg_class c
|
|
659
|
+
WHERE c.oid='sessions'::regclass AND pg_get_userbyid(c.relowner)=current_user
|
|
660
|
+
AND c.relrowsecurity AND NOT c.relforcerowsecurity
|
|
661
|
+
AND to_regclass('pg_temp.skill_metadata_0426') IS NOT NULL`;
|
|
662
|
+
if (rows.length !== 1)
|
|
663
|
+
throw new Error(
|
|
664
|
+
"Skill configuration conversion requires the parser-backed maintenance owner window"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
async function migrateLegacySkillConfigurations(tx) {
|
|
668
|
+
await assertSkillConfigurationMaintenanceWindow(tx);
|
|
669
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended('session-tenancy:'||workspace_id::text,0))
|
|
670
|
+
FROM (SELECT DISTINCT workspace_id FROM sessions ORDER BY workspace_id) workspaces`;
|
|
671
|
+
await tx`LOCK TABLE sessions, workspace_packs, session_turns, automation_triggers,
|
|
672
|
+
automation_trigger_revisions, automation_runs, automation_trigger_events, automation_run_event_links, pack_installations IN SHARE ROW EXCLUSIVE MODE`;
|
|
673
|
+
const pinned = await tx`
|
|
674
|
+
SELECT 'pack-installation' AS kind,id::text,manifest_snapshot AS value FROM pack_installations
|
|
675
|
+
WHERE status <> 'disabled' AND manifest_snapshot IS NOT NULL
|
|
676
|
+
UNION ALL SELECT 'automation-current',t.id::text,jsonb_build_object('skills',r.session_template->'skills')
|
|
677
|
+
FROM automation_triggers t JOIN automation_trigger_revisions r ON r.trigger_id=t.id AND r.revision=t.current_revision
|
|
678
|
+
WHERE t.status IN ('active','paused') AND r.session_template ? 'skills'
|
|
679
|
+
UNION ALL SELECT 'automation-run',id::text,jsonb_build_object('skills',accepted_execution->'sessionTemplate'->'skills')
|
|
680
|
+
FROM automation_runs WHERE status IN ('queued','dispatching') AND accepted_execution->'sessionTemplate' ? 'skills'
|
|
681
|
+
UNION ALL SELECT 'automation-event',e.id::text,jsonb_build_object('skills',r.session_template->'skills')
|
|
682
|
+
FROM automation_trigger_events e CROSS JOIN LATERAL jsonb_array_elements(e.matched_trigger_revisions) matched
|
|
683
|
+
JOIN automation_trigger_revisions r ON r.trigger_id=(matched->>'triggerId')::uuid AND r.revision=(matched->>'revision')::integer
|
|
684
|
+
WHERE e.status='accepted' AND r.session_template ? 'skills'
|
|
685
|
+
AND NOT EXISTS (SELECT 1 FROM automation_runs run WHERE run.event_id=e.id AND run.trigger_id=r.trigger_id AND run.trigger_revision=r.revision)
|
|
686
|
+
AND NOT EXISTS (SELECT 1 FROM automation_run_event_links link WHERE link.event_id=e.id AND link.trigger_id=r.trigger_id)
|
|
687
|
+
`;
|
|
688
|
+
const blockers = [];
|
|
689
|
+
for (const source of pinned) {
|
|
690
|
+
try {
|
|
691
|
+
const original = object(source.value);
|
|
692
|
+
const converted = source.kind === "pack-installation" ? convertLegacyPackConfig(original, source.id) : { ...original, skills: convertLegacyConfigSkills(original.skills, source.id) };
|
|
693
|
+
if (JSON.stringify(converted) !== JSON.stringify(original))
|
|
694
|
+
blockers.push(`${source.kind}:${source.id} (plain content)`);
|
|
695
|
+
} catch {
|
|
696
|
+
blockers.push(`${source.kind}:${source.id} (invalid content)`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const candidates = await tx`
|
|
700
|
+
SELECT 'session' AS kind,s.id::text,s.account_id,s.workspace_id,s.skills AS original,
|
|
701
|
+
(s.active_turn_id IS NOT NULL OR EXISTS(SELECT 1 FROM session_turns t WHERE t.session_id=s.id AND t.status NOT IN ('completed','failed','cancelled'))) AS pinned
|
|
702
|
+
FROM sessions s WHERE s.skills <> '[]'::jsonb
|
|
703
|
+
UNION ALL SELECT 'workspace-pack',id::text,account_id,workspace_id,manifest,false FROM workspace_packs
|
|
704
|
+
`;
|
|
705
|
+
const changes = [];
|
|
706
|
+
for (const source of candidates) {
|
|
707
|
+
try {
|
|
708
|
+
const converted = source.kind === "session" ? convertLegacyConfigSkills(source.original, source.id) : convertLegacyPackConfig(source.original, source.id);
|
|
709
|
+
if (source.kind === "session") StoredSessionSkills.parse(converted);
|
|
710
|
+
if (JSON.stringify(source.original) !== JSON.stringify(converted)) {
|
|
711
|
+
if (source.pinned) blockers.push(`${source.kind}:${source.id} (runnable accepted turn)`);
|
|
712
|
+
else changes.push({ source, converted });
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
blockers.push(`${source.kind}:${source.id} (invalid content or canonical collision)`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (blockers.length)
|
|
719
|
+
throw new Error(
|
|
720
|
+
`0433 Skill configuration preflight: ${blockers.length} repair-required sources; drain or explicitly replace pinned sources before retry: ${blockers.slice(0, 30).join(", ")}`
|
|
721
|
+
);
|
|
722
|
+
for (const { source, converted } of changes) {
|
|
723
|
+
const original = tx.json(source.original);
|
|
724
|
+
const replacement = tx.json(converted);
|
|
725
|
+
await tx`INSERT INTO skill_config_conversion_receipts(account_id,workspace_id,source_kind,source_id,original_configuration,original_hash,replacement_hash)
|
|
726
|
+
VALUES(${source.account_id},${source.workspace_id},${source.kind},${source.id},${original},
|
|
727
|
+
encode(sha256(convert_to(${original}::jsonb::text,'UTF8')),'hex'),encode(sha256(convert_to(${replacement}::jsonb::text,'UTF8')),'hex'))`;
|
|
728
|
+
const updated = source.kind === "session" ? await tx`UPDATE sessions SET skills=${replacement} WHERE id=${source.id} AND account_id=${source.account_id} AND workspace_id=${source.workspace_id} AND skills=${original} RETURNING id` : await tx`UPDATE workspace_packs SET manifest=${replacement} WHERE id=${source.id} AND account_id=${source.account_id} AND workspace_id=${source.workspace_id} AND manifest=${original} RETURNING id`;
|
|
729
|
+
if (updated.length !== 1)
|
|
730
|
+
throw new Error(`0433 Skill configuration CAS conflict: ${source.kind}:${source.id}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/migration-runner-settings.ts
|
|
735
|
+
function batchedBackfillTransactionLocalSetting(file) {
|
|
736
|
+
if (file === "0411_new_session_draft_project_provenance_backfill.sql") {
|
|
737
|
+
return {
|
|
738
|
+
guc: "opengeni.new_session_draft_project_provenance_backfill_v1",
|
|
739
|
+
value: "1"
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/migrate.ts
|
|
746
|
+
var DEFAULT_DATABASE_URL = "postgres://opengeni:opengeni@127.0.0.1:5432/opengeni";
|
|
747
|
+
var DEFAULT_MAX_NESTED_AGENT_DEPTH = 3;
|
|
748
|
+
var MAX_NESTED_AGENT_DEPTH = 2147483647;
|
|
749
|
+
var DEFAULT_APPLICATION_DATABASE_ROLE = "opengeni_app";
|
|
750
|
+
var MIGRATION_APPLICATION_ROLES_SETTING = "opengeni.migration_application_roles";
|
|
751
|
+
var MAX_MIGRATION_APPLICATION_ROLES = 16;
|
|
752
|
+
var batchedBackfillDirective = /^-- opengeni:batched-backfill batch-size=(\d+) lock-timeout=(\d+(?:ms|s|min)) statement-timeout=(\d+(?:ms|s|min))$/;
|
|
753
|
+
var concurrentIndexDirective = /^-- opengeni:concurrent-index lock-timeout=(\d+(?:ms|s|min))$/;
|
|
754
|
+
var concurrentIndexStatement = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+CONCURRENTLY\s+(?:(IF\s+NOT\s+EXISTS)\s+)?(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_]*))\s+ON\b/is;
|
|
755
|
+
var governedLegacyConcurrentIndexMigrations = /* @__PURE__ */ new Set([
|
|
756
|
+
"0066_session_interruption_attempt_lookup.sql",
|
|
757
|
+
"0070_session_event_type_sequence_lookup.sql",
|
|
758
|
+
"0071_session_event_monitoring_tail.sql",
|
|
759
|
+
"0072_sessions_workspace_created_id_idx.sql",
|
|
760
|
+
"0073_sessions_workspace_updated_id_idx.sql",
|
|
761
|
+
"0075_sessions_workspace_activity_revision_idx.sql",
|
|
762
|
+
"0077_session_attempt_latest_lookup.sql"
|
|
763
|
+
]);
|
|
764
|
+
function assertIdentifier(name, value) {
|
|
765
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
766
|
+
throw new Error(`${name} is not a valid Postgres identifier: ${value}`);
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
function parseConcurrentIndexMigration(file, sqlText) {
|
|
771
|
+
const lines = sqlText.replaceAll("\r\n", "\n").split("\n");
|
|
772
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
773
|
+
const deploymentPrefixed = /^-- deployment-mode: (?:rolling|maintenance)$/.test(firstLine);
|
|
774
|
+
const directiveIndex = deploymentPrefixed ? 1 : 0;
|
|
775
|
+
const directiveLine = lines[directiveIndex]?.trim() ?? "";
|
|
776
|
+
const directive = concurrentIndexDirective.exec(directiveLine);
|
|
777
|
+
if (!directive) {
|
|
778
|
+
if (directiveLine.startsWith("-- opengeni:")) {
|
|
779
|
+
throw new Error(`Unsupported OpenGeni migration directive in ${file}`);
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const lockTimeout = directive[1];
|
|
784
|
+
const statement = lines.slice(directiveIndex + 1).join("\n").trim();
|
|
785
|
+
const withoutTrailingSemicolon = statement.endsWith(";") ? statement.slice(0, -1).trimEnd() : statement;
|
|
786
|
+
const parsedStatement = concurrentIndexStatement.exec(withoutTrailingSemicolon);
|
|
787
|
+
if (!parsedStatement || withoutTrailingSemicolon.includes(";")) {
|
|
788
|
+
throw new Error(
|
|
789
|
+
`${file}: opengeni:concurrent-index requires exactly one CREATE [UNIQUE] INDEX CONCURRENTLY IF NOT EXISTS statement with an unqualified index name`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
const idempotentInSql = parsedStatement[1] !== void 0;
|
|
793
|
+
if (!idempotentInSql && !governedLegacyConcurrentIndexMigrations.has(file)) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`${file}: opengeni:concurrent-index requires IF NOT EXISTS; bare statements are supported only for governed historical migrations`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
indexName: (parsedStatement[2] ?? parsedStatement[3]).replaceAll('""', '"'),
|
|
800
|
+
lockTimeout,
|
|
801
|
+
skipWhenValid: !idempotentInSql,
|
|
802
|
+
statement
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function parseBatchedBackfillMigration(file, sqlText) {
|
|
806
|
+
const lines = sqlText.replaceAll("\r\n", "\n").split("\n");
|
|
807
|
+
const directiveIndex = /^-- deployment-mode: (?:rolling|maintenance)$/.test(
|
|
808
|
+
lines[0]?.trim() ?? ""
|
|
809
|
+
) ? 1 : 0;
|
|
810
|
+
const directive = batchedBackfillDirective.exec(lines[directiveIndex]?.trim() ?? "");
|
|
811
|
+
if (!directive) return null;
|
|
812
|
+
const statement = lines.slice(directiveIndex + 1).filter((line) => !line.trim().startsWith("--")).join("\n").trim();
|
|
813
|
+
const withoutTrailingSemicolon = statement.endsWith(";") ? statement.slice(0, -1).trimEnd() : statement;
|
|
814
|
+
const batchSize = Number(directive[1]);
|
|
815
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1e4 || !/^WITH\b/is.test(withoutTrailingSemicolon) || !/\bUPDATE\b/is.test(withoutTrailingSemicolon) || !/\bRETURNING\b/is.test(withoutTrailingSemicolon) || !new RegExp(`\\bLIMIT\\s+${batchSize}\\b`, "i").test(withoutTrailingSemicolon) || withoutTrailingSemicolon.includes(";")) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`${file}: opengeni:batched-backfill requires one bounded WITH ... UPDATE ... RETURNING statement whose LIMIT matches batch-size`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
batchSize,
|
|
822
|
+
lockTimeout: directive[2],
|
|
823
|
+
statementTimeout: directive[3],
|
|
824
|
+
statement
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function executeMigrationFile(sql, file, sqlText) {
|
|
828
|
+
if (sqlText.includes(SKILL_METADATA_MIGRATION_MARKER)) {
|
|
829
|
+
if (file !== "0433_unified_skill_lifecycle.sql")
|
|
830
|
+
throw new Error("Skill metadata stage is restricted to migration 0433");
|
|
831
|
+
const parts = sqlText.split(SKILL_METADATA_MIGRATION_MARKER);
|
|
832
|
+
if (parts.length !== 2) throw new Error("0433 requires exactly one Skill metadata stage");
|
|
833
|
+
await sql.begin(async (transaction) => {
|
|
834
|
+
await createSkillMetadataMigrationStage(transaction);
|
|
835
|
+
await transaction`SELECT
|
|
836
|
+
pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2','1',true),
|
|
837
|
+
pg_catalog.set_config('opengeni.session_variable_set_attachments_v1','1',true)`;
|
|
838
|
+
await transaction.unsafe(parts[0]);
|
|
839
|
+
await stageSkillMetadataMigration(transaction);
|
|
840
|
+
await migrateLegacySkillConfigurations(transaction);
|
|
841
|
+
await transaction.unsafe(parts[1]);
|
|
842
|
+
await transaction`INSERT INTO schema_migrations(name) VALUES(${file}) ON CONFLICT DO NOTHING`;
|
|
843
|
+
});
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (sqlText.includes(KNOWLEDGE_MIGRATION_MARKER)) {
|
|
847
|
+
if (file !== "0461_unified_knowledge.sql")
|
|
848
|
+
throw new Error("Knowledge conversion is restricted to migration 0461");
|
|
849
|
+
const parts = sqlText.split(KNOWLEDGE_MIGRATION_MARKER);
|
|
850
|
+
if (parts.length !== 2) throw new Error("0461 requires exactly one Knowledge conversion stage");
|
|
851
|
+
await sql.begin(async (transaction) => {
|
|
852
|
+
await transaction`CREATE TEMP TABLE knowledge_conversion_0461(completed boolean NOT NULL) ON COMMIT DROP`;
|
|
853
|
+
await transaction`SELECT
|
|
854
|
+
pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2','1',true),
|
|
855
|
+
pg_catalog.set_config('opengeni.session_variable_set_attachments_v1','1',true)`;
|
|
856
|
+
await transaction.unsafe(parts[0]);
|
|
857
|
+
await migrateRetainedKnowledge(transaction);
|
|
858
|
+
await transaction`INSERT INTO pg_temp.knowledge_conversion_0461 VALUES(true)`;
|
|
859
|
+
await transaction.unsafe(parts[1]);
|
|
860
|
+
await transaction`INSERT INTO schema_migrations(name) VALUES(${file}) ON CONFLICT DO NOTHING`;
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const batchedBackfill = parseBatchedBackfillMigration(file, sqlText);
|
|
865
|
+
if (batchedBackfill) {
|
|
866
|
+
await sql`select set_config('lock_timeout', ${batchedBackfill.lockTimeout}, false)`;
|
|
867
|
+
await sql`select set_config('statement_timeout', ${batchedBackfill.statementTimeout}, false)`;
|
|
868
|
+
try {
|
|
869
|
+
for (; ; ) {
|
|
870
|
+
const result = await sql.begin(async (transaction) => {
|
|
871
|
+
const transactionLocalSetting = batchedBackfillTransactionLocalSetting(file);
|
|
872
|
+
await transaction`select
|
|
873
|
+
pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true),
|
|
874
|
+
pg_catalog.set_config(
|
|
875
|
+
'opengeni.session_variable_set_attachments_v1',
|
|
876
|
+
'1',
|
|
877
|
+
true
|
|
878
|
+
)`;
|
|
879
|
+
if (transactionLocalSetting) {
|
|
880
|
+
await transaction`select pg_catalog.set_config(
|
|
881
|
+
${transactionLocalSetting.guc},
|
|
882
|
+
${transactionLocalSetting.value},
|
|
883
|
+
true
|
|
884
|
+
)`;
|
|
885
|
+
}
|
|
886
|
+
return await transaction.unsafe(batchedBackfill.statement);
|
|
887
|
+
});
|
|
888
|
+
if (result.length === 0) break;
|
|
889
|
+
}
|
|
890
|
+
} finally {
|
|
891
|
+
await sql`select set_config('statement_timeout', '0', false)`;
|
|
892
|
+
await sql`select set_config('lock_timeout', '0', false)`;
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const concurrentIndex = parseConcurrentIndexMigration(file, sqlText);
|
|
897
|
+
if (!concurrentIndex) {
|
|
898
|
+
await sql.unsafe(
|
|
899
|
+
`SELECT
|
|
900
|
+
pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true),
|
|
901
|
+
pg_catalog.set_config('opengeni.session_variable_set_attachments_v1', '1', true);
|
|
902
|
+
${file === "0434_ordered_model_history.sql" ? "SET CONSTRAINTS ALL IMMEDIATE;\n" : ""}${sqlText}`
|
|
903
|
+
);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
await sql`select set_config('lock_timeout', ${concurrentIndex.lockTimeout}, false)`;
|
|
907
|
+
try {
|
|
908
|
+
const [existing] = await sql`
|
|
909
|
+
select i.indisvalid as valid, i.indisready as ready
|
|
910
|
+
from pg_catalog.pg_class c
|
|
911
|
+
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
912
|
+
join pg_catalog.pg_index i on i.indexrelid = c.oid
|
|
913
|
+
where n.nspname = current_schema() and c.relname = ${concurrentIndex.indexName}
|
|
914
|
+
`;
|
|
915
|
+
if (existing && (!existing.valid || !existing.ready)) {
|
|
916
|
+
const quotedIndexName = `"${concurrentIndex.indexName.replaceAll('"', '""')}"`;
|
|
917
|
+
await sql.unsafe(`DROP INDEX CONCURRENTLY ${quotedIndexName}`);
|
|
918
|
+
} else if (existing && concurrentIndex.skipWhenValid) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
await sql.unsafe(concurrentIndex.statement);
|
|
922
|
+
} finally {
|
|
923
|
+
await sql`select set_config('lock_timeout', '0', false)`;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function deploymentDepthPolicy(options) {
|
|
927
|
+
const raw = options === void 0 ? process.env.OPENGENI_MAX_NESTED_AGENT_DEPTH?.trim() || void 0 : options.maxNestedAgentDepth;
|
|
928
|
+
if (raw === void 0) {
|
|
929
|
+
return { maxNestedAgentDepth: DEFAULT_MAX_NESTED_AGENT_DEPTH, source: "default" };
|
|
930
|
+
}
|
|
931
|
+
const value = typeof raw === "number" ? raw : Number(raw);
|
|
932
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_NESTED_AGENT_DEPTH || typeof raw === "string" && !/^(0|[1-9][0-9]*)$/.test(raw)) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`OPENGENI_MAX_NESTED_AGENT_DEPTH must be a non-negative 32-bit integer: ${raw}`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
return { maxNestedAgentDepth: value, source: "deployment" };
|
|
938
|
+
}
|
|
939
|
+
function migrationApplicationRoles(schema, options) {
|
|
940
|
+
const configured = options?.applicationDatabaseRoles;
|
|
941
|
+
const environment = process.env.OPENGENI_MIGRATION_APPLICATION_DATABASE_ROLES?.trim();
|
|
942
|
+
const explicit = configured !== void 0 ? configured : environment ? environment.split(",").map((role) => role.trim()) : void 0;
|
|
943
|
+
if (!explicit) {
|
|
944
|
+
const configuredRuntimeRoles = [
|
|
945
|
+
process.env.OPENGENI_APP_DATABASE_USER?.trim(),
|
|
946
|
+
process.env.OPENGENI_RUNTIME_DATABASE_ROLE?.trim()
|
|
947
|
+
].filter((role) => Boolean(role) && role !== DEFAULT_APPLICATION_DATABASE_ROLE);
|
|
948
|
+
if (schema || configuredRuntimeRoles.length > 0) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
"Pending migrations require the exact application database roles via MigrationRuntimeOptions.applicationDatabaseRoles or OPENGENI_MIGRATION_APPLICATION_DATABASE_ROLES for dedicated-schema or custom-role deployments"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
return [DEFAULT_APPLICATION_DATABASE_ROLE];
|
|
954
|
+
}
|
|
955
|
+
if (configured !== void 0 && configured.some((role) => typeof role !== "string" || role !== role.trim())) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
"MigrationRuntimeOptions.applicationDatabaseRoles must contain canonical Postgres role names without surrounding whitespace"
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (explicit.length < 1 || explicit.length > MAX_MIGRATION_APPLICATION_ROLES) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`Migration application database roles must contain 1-${MAX_MIGRATION_APPLICATION_ROLES} entries`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const normalized = [...new Set(explicit)].sort(
|
|
966
|
+
(left, right) => Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"))
|
|
967
|
+
);
|
|
968
|
+
if (normalized.length !== explicit.length || normalized.some((role) => role.length === 0 || Buffer.byteLength(role, "utf8") > 63)) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
"Migration application database roles must be unique, non-empty Postgres role names of at most 63 UTF-8 bytes"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return normalized;
|
|
974
|
+
}
|
|
975
|
+
async function persistDeploymentDepthPolicy(sql, policy) {
|
|
976
|
+
const [relation] = await sql`
|
|
977
|
+
select to_regclass('nested_agent_depth_configuration') is not null as exists
|
|
978
|
+
`;
|
|
979
|
+
if (!relation?.exists) return;
|
|
980
|
+
await sql`
|
|
981
|
+
insert into "nested_agent_depth_configuration" (
|
|
982
|
+
"singleton", "max_nested_agent_depth", "policy_source", "updated_at"
|
|
983
|
+
) values (true, ${policy.maxNestedAgentDepth}, ${policy.source}, now())
|
|
984
|
+
on conflict ("singleton") do update
|
|
985
|
+
set "max_nested_agent_depth" = excluded."max_nested_agent_depth",
|
|
986
|
+
"policy_source" = excluded."policy_source",
|
|
987
|
+
"updated_at" = now()
|
|
988
|
+
where "nested_agent_depth_configuration"."max_nested_agent_depth"
|
|
989
|
+
is distinct from excluded."max_nested_agent_depth"
|
|
990
|
+
or "nested_agent_depth_configuration"."policy_source"
|
|
991
|
+
is distinct from excluded."policy_source"
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
async function migrate(databaseUrl = process.env.OPENGENI_MIGRATIONS_DATABASE_URL ?? process.env.OPENGENI_DATABASE_URL ?? DEFAULT_DATABASE_URL, schema = process.env.OPENGENI_DB_SCHEMA?.trim() || void 0, runtimeOptions) {
|
|
995
|
+
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), "../drizzle");
|
|
996
|
+
const files = (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort();
|
|
997
|
+
const depthPolicy = deploymentDepthPolicy(runtimeOptions);
|
|
998
|
+
const sql = postgres(databaseUrl, { max: 1 });
|
|
999
|
+
try {
|
|
1000
|
+
await sql`SELECT pg_advisory_lock(727458)`;
|
|
1001
|
+
if (schema) {
|
|
1002
|
+
assertIdentifier("OPENGENI_DB_SCHEMA", schema);
|
|
1003
|
+
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
|
1004
|
+
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS "opengeni_private"`);
|
|
1005
|
+
await sql.unsafe(`SET search_path = "${schema}", "opengeni_private", "public"`);
|
|
1006
|
+
}
|
|
1007
|
+
await sql`select set_config('opengeni.max_nested_agent_depth', ${String(depthPolicy.maxNestedAgentDepth)}, false)`;
|
|
1008
|
+
await sql`select set_config('opengeni.nested_agent_depth_policy_source', ${depthPolicy.source}, false)`;
|
|
1009
|
+
await sql.unsafe(
|
|
1010
|
+
`CREATE TABLE IF NOT EXISTS "schema_migrations" ("name" text PRIMARY KEY, "applied_at" timestamptz NOT NULL DEFAULT now())`
|
|
1011
|
+
);
|
|
1012
|
+
const appliedRows = await sql`SELECT "name" FROM "schema_migrations"`;
|
|
1013
|
+
const applied = new Set(appliedRows.map((row) => row.name));
|
|
1014
|
+
const pendingMigrationSources = /* @__PURE__ */ new Map();
|
|
1015
|
+
for (const file of files) {
|
|
1016
|
+
if (!applied.has(file)) {
|
|
1017
|
+
pendingMigrationSources.set(file, await readFile(join(migrationsDir, file), "utf8"));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (Array.from(pendingMigrationSources.values()).some(
|
|
1021
|
+
(source) => source.includes(MIGRATION_APPLICATION_ROLES_SETTING)
|
|
1022
|
+
)) {
|
|
1023
|
+
const applicationRoles = migrationApplicationRoles(schema, runtimeOptions);
|
|
1024
|
+
await sql`select set_config(
|
|
1025
|
+
${MIGRATION_APPLICATION_ROLES_SETTING},
|
|
1026
|
+
${JSON.stringify(applicationRoles)},
|
|
1027
|
+
false
|
|
1028
|
+
)`;
|
|
1029
|
+
}
|
|
1030
|
+
for (const file of files) {
|
|
1031
|
+
if (applied.has(file)) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const sqlText = pendingMigrationSources.get(file);
|
|
1035
|
+
if (sqlText === void 0) {
|
|
1036
|
+
throw new Error(`Pending migration source was not loaded: ${file}`);
|
|
1037
|
+
}
|
|
1038
|
+
await executeMigrationFile(sql, file, sqlText);
|
|
1039
|
+
await sql`INSERT INTO "schema_migrations" ("name") VALUES (${file}) ON CONFLICT DO NOTHING`;
|
|
1040
|
+
}
|
|
1041
|
+
await persistDeploymentDepthPolicy(sql, depthPolicy);
|
|
1042
|
+
} finally {
|
|
1043
|
+
await sql.end();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function runMigrations(adminConnection, targetSchema, runtimeOptions) {
|
|
1047
|
+
await migrate(adminConnection, targetSchema, runtimeOptions);
|
|
1048
|
+
}
|
|
1049
|
+
if (import.meta.main) {
|
|
1050
|
+
await migrate();
|
|
1051
|
+
console.log("Applied Drizzle SQL migrations.");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export {
|
|
1055
|
+
parseConcurrentIndexMigration,
|
|
1056
|
+
parseBatchedBackfillMigration,
|
|
1057
|
+
executeMigrationFile,
|
|
1058
|
+
migrationApplicationRoles,
|
|
1059
|
+
migrate,
|
|
1060
|
+
runMigrations
|
|
1061
|
+
};
|
|
1062
|
+
//# sourceMappingURL=chunk-45CP2YKY.js.map
|