@opengeni/db 6.0.1 → 6.0.3
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/browser-sessions.d.ts +26 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-reconciliation.js +2 -2
- package/dist/{chunk-4RDUZ5UV.js → chunk-4ZQFT5II.js} +2 -2
- package/dist/{chunk-6ORJPCYW.js → chunk-5UOWNKPP.js} +2 -2
- package/dist/{chunk-4CCFFLDZ.js → chunk-BG46J5HF.js} +13 -2
- package/dist/chunk-BG46J5HF.js.map +1 -0
- package/dist/{chunk-CSVHCF3R.js → chunk-IB3SW3JW.js} +11 -4
- package/dist/{chunk-CSVHCF3R.js.map → chunk-IB3SW3JW.js.map} +1 -1
- package/dist/{chunk-GSB4ISHE.js → chunk-IIPWNNVR.js} +4 -1
- package/dist/chunk-IIPWNNVR.js.map +1 -0
- package/dist/{chunk-MAMBI2HR.js → chunk-JPYN5XKV.js} +39 -3
- package/dist/chunk-JPYN5XKV.js.map +1 -0
- package/dist/{chunk-LSLNMENV.js → chunk-KZXWUXWV.js} +4 -4
- package/dist/{chunk-LO6BVXXH.js → chunk-LD52IFS3.js} +2 -2
- package/dist/{chunk-JILAWE26.js → chunk-M5Z6II2P.js} +2 -2
- package/dist/{chunk-PGIFHEBA.js → chunk-RVEOGRMF.js} +2 -2
- package/dist/{chunk-H7BC444C.js → chunk-SRAMNVHT.js} +17 -1
- package/dist/chunk-SRAMNVHT.js.map +1 -0
- package/dist/{chunk-XK6HXON2.js → chunk-UXFBXCHM.js} +2 -2
- package/dist/{chunk-RXH5CFG3.js → chunk-VE2XK2KD.js} +12 -8
- package/dist/{chunk-RXH5CFG3.js.map → chunk-VE2XK2KD.js.map} +1 -1
- package/dist/{chunk-FULLE5CF.js → chunk-WT7HG43Y.js} +3 -3
- package/dist/{chunk-I7HQL6FK.js → chunk-ZM5BA3TD.js} +3 -3
- package/dist/connect-attempts.js +3 -3
- package/dist/database.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +69 -20
- package/dist/index.js +633 -202
- package/dist/index.js.map +1 -1
- package/dist/knowledge-entries.d.ts +2 -0
- package/dist/knowledge-indexing.d.ts +17 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +1 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.d.ts +10 -1
- package/dist/migrate.js +3 -1
- package/dist/organization-integration-policy.js +3 -3
- package/dist/organization-membership-lifecycle.d.ts +1 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +4 -2
- package/dist/retained-provider-commands.js +6 -6
- package/dist/runtime-posture.d.ts +1 -1
- package/dist/schema.d.ts +25 -8
- package/dist/schema.js +1 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +5 -5
- package/dist/session-event-slices.js +3 -3
- package/dist/session-mcp-credential-rotation.js +4 -4
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0508_deadline_command_workspace_capture.sql +75 -0
- package/drizzle/0509_verified_signup_trial_credits.sql +76 -0
- package/drizzle/0510_knowledge_index_funding_wait.sql +174 -0
- package/drizzle/0511_knowledge_visible_index_status.sql +56 -0
- package/drizzle/0512_insights_fact_reads_hash_join.sql +31 -0
- package/drizzle/0513_message_fork_prefix_compaction.sql +85 -0
- package/drizzle/0514_failed_session_variable_set_attach.sql +25 -0
- package/package.json +5 -5
- package/src/browser-sessions.ts +71 -1
- package/src/database.ts +18 -0
- package/src/index.ts +706 -252
- package/src/insights.ts +44 -36
- package/src/knowledge-entries.ts +43 -2
- package/src/knowledge-indexing.ts +79 -1
- package/src/managed-user-setup.ts +18 -1
- package/src/migrate.ts +58 -1
- package/src/organization-membership-lifecycle.ts +11 -0
- package/src/provision-roles.ts +12 -0
- package/src/retained-provider-commands.ts +16 -6
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +3 -0
- package/dist/chunk-4CCFFLDZ.js.map +0 -1
- package/dist/chunk-GSB4ISHE.js.map +0 -1
- package/dist/chunk-H7BC444C.js.map +0 -1
- package/dist/chunk-MAMBI2HR.js.map +0 -1
- /package/dist/{chunk-4RDUZ5UV.js.map → chunk-4ZQFT5II.js.map} +0 -0
- /package/dist/{chunk-6ORJPCYW.js.map → chunk-5UOWNKPP.js.map} +0 -0
- /package/dist/{chunk-LSLNMENV.js.map → chunk-KZXWUXWV.js.map} +0 -0
- /package/dist/{chunk-LO6BVXXH.js.map → chunk-LD52IFS3.js.map} +0 -0
- /package/dist/{chunk-JILAWE26.js.map → chunk-M5Z6II2P.js.map} +0 -0
- /package/dist/{chunk-PGIFHEBA.js.map → chunk-RVEOGRMF.js.map} +0 -0
- /package/dist/{chunk-XK6HXON2.js.map → chunk-UXFBXCHM.js.map} +0 -0
- /package/dist/{chunk-FULLE5CF.js.map → chunk-WT7HG43Y.js.map} +0 -0
- /package/dist/{chunk-I7HQL6FK.js.map → chunk-ZM5BA3TD.js.map} +0 -0
package/src/insights.ts
CHANGED
|
@@ -990,34 +990,45 @@ export async function aggregateSessionDepth(
|
|
|
990
990
|
}> {
|
|
991
991
|
const context = await rlsContextForWorkspace(db, workspaceId);
|
|
992
992
|
return await withRlsContext(db, context, async (scopedDb) => {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
993
|
+
// One grouped pass over the workspace's sessions yields every count the
|
|
994
|
+
// summary needs; the totals, root count, deepest depth, and mean are
|
|
995
|
+
// derived from the buckets instead of rescanning a wide sessions heap.
|
|
996
|
+
const buckets = (
|
|
997
|
+
await scopedDb
|
|
998
|
+
.select({
|
|
999
|
+
depth: schema.sessions.nestedAgentDepth,
|
|
1000
|
+
sessions: sql<number>`count(*)::int`,
|
|
1001
|
+
})
|
|
1002
|
+
.from(schema.sessions)
|
|
1003
|
+
.where(eq(schema.sessions.workspaceId, workspaceId))
|
|
1004
|
+
.groupBy(schema.sessions.nestedAgentDepth)
|
|
1005
|
+
.orderBy(schema.sessions.nestedAgentDepth)
|
|
1006
|
+
).map((row) => ({ depth: row.depth, sessions: Number(row.sessions) }));
|
|
1007
|
+
const sessionsTouched = buckets.reduce((sum, bucket) => sum + bucket.sessions, 0);
|
|
1008
|
+
const rootSessions = buckets.find((bucket) => bucket.depth === 0)?.sessions ?? 0;
|
|
1009
|
+
const deepestDepth = buckets.reduce((max, bucket) => Math.max(max, bucket.depth), 0);
|
|
1010
|
+
const avgDepth =
|
|
1011
|
+
sessionsTouched > 0
|
|
1012
|
+
? buckets.reduce((sum, bucket) => sum + bucket.depth * bucket.sessions, 0) / sessionsTouched
|
|
1013
|
+
: 0;
|
|
1014
|
+
const [deepest] =
|
|
1015
|
+
sessionsTouched > 0
|
|
1016
|
+
? await scopedDb
|
|
1017
|
+
.select({
|
|
1018
|
+
id: schema.sessions.id,
|
|
1019
|
+
title: schema.sessions.title,
|
|
1020
|
+
depth: schema.sessions.nestedAgentDepth,
|
|
1021
|
+
})
|
|
1022
|
+
.from(schema.sessions)
|
|
1023
|
+
.where(
|
|
1024
|
+
and(
|
|
1025
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
1026
|
+
eq(schema.sessions.nestedAgentDepth, deepestDepth),
|
|
1027
|
+
),
|
|
1028
|
+
)
|
|
1029
|
+
.orderBy(desc(schema.sessions.updatedAt))
|
|
1030
|
+
.limit(1)
|
|
1031
|
+
: [];
|
|
1021
1032
|
const [goals] = await scopedDb
|
|
1022
1033
|
.select({
|
|
1023
1034
|
active: sql<number>`count(*) filter (where ${schema.sessionGoals.status} = 'active')::int`,
|
|
@@ -1026,16 +1037,13 @@ export async function aggregateSessionDepth(
|
|
|
1026
1037
|
.from(schema.sessionGoals)
|
|
1027
1038
|
.where(eq(schema.sessionGoals.workspaceId, workspaceId));
|
|
1028
1039
|
return {
|
|
1029
|
-
buckets
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
sessionsTouched: Number(stats?.sessionsTouched ?? 0),
|
|
1034
|
-
rootSessions: Number(stats?.rootSessions ?? 0),
|
|
1035
|
-
deepestDepth: Number(stats?.deepestDepth ?? 0),
|
|
1040
|
+
buckets,
|
|
1041
|
+
sessionsTouched,
|
|
1042
|
+
rootSessions,
|
|
1043
|
+
deepestDepth,
|
|
1036
1044
|
deepestSessionId: deepest?.id ?? null,
|
|
1037
1045
|
deepestSessionTitle: deepest?.title?.trim() || "",
|
|
1038
|
-
avgDepth
|
|
1046
|
+
avgDepth,
|
|
1039
1047
|
goalsActive: Number(goals?.active ?? 0),
|
|
1040
1048
|
goalsCompleted: Number(goals?.completed ?? 0),
|
|
1041
1049
|
};
|
package/src/knowledge-entries.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
KnowledgeEntryRestoreRequest,
|
|
20
20
|
KnowledgeEntrySaveRequest,
|
|
21
21
|
KnowledgeEntrySummary,
|
|
22
|
+
KnowledgeIndexStatus,
|
|
22
23
|
KnowledgeEntryWriteReceipt,
|
|
23
24
|
type AgentLearningContext,
|
|
24
25
|
type KnowledgeEntryScope,
|
|
@@ -319,6 +320,39 @@ function decodeProjection(value: unknown, full: boolean) {
|
|
|
319
320
|
});
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
/** The owner-definer seam rechecks canonical visibility for every exact revision. */
|
|
324
|
+
async function withIndexStatus<T extends KnowledgeEntryRecord | KnowledgeEntrySummary>(
|
|
325
|
+
db: Database,
|
|
326
|
+
context: KnowledgeContext,
|
|
327
|
+
entries: T[],
|
|
328
|
+
view: "published" | "needs_review" | "archived" | "rejected",
|
|
329
|
+
): Promise<T[]> {
|
|
330
|
+
if (view === "archived" || view === "rejected") return entries;
|
|
331
|
+
const items = entries
|
|
332
|
+
.filter(
|
|
333
|
+
(entry) =>
|
|
334
|
+
("entry" in entry.revision ? entry.revision.entry.kind : entry.revision.kind) === "source",
|
|
335
|
+
)
|
|
336
|
+
.map((entry) => ({ entryId: entry.id, revisionId: entry.revision.id }));
|
|
337
|
+
if (!items.length) return entries;
|
|
338
|
+
const statuses = await inKnowledgeContext(db, context, async (tx) => {
|
|
339
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
340
|
+
tx,
|
|
341
|
+
sql`SELECT knowledge_visible_index_status(${context.accountId}::uuid,
|
|
342
|
+
${context.workspaceId}::uuid,${JSON.stringify(context.actor)}::jsonb,
|
|
343
|
+
${JSON.stringify(items)}::jsonb,${view}) AS result`,
|
|
344
|
+
);
|
|
345
|
+
return z
|
|
346
|
+
.array(z.object({ entryId: z.uuid(), revisionId: z.uuid(), status: KnowledgeIndexStatus }))
|
|
347
|
+
.parse(row?.result);
|
|
348
|
+
});
|
|
349
|
+
const byRevision = new Map(statuses.map((item) => [item.revisionId, item.status]));
|
|
350
|
+
return entries.map((entry) => {
|
|
351
|
+
const indexStatus = byRevision.get(entry.revision.id);
|
|
352
|
+
return indexStatus ? { ...entry, indexStatus } : entry;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
322
356
|
async function read(
|
|
323
357
|
db: Database,
|
|
324
358
|
context: KnowledgeContext,
|
|
@@ -353,7 +387,9 @@ export async function getKnowledgeEntry(
|
|
|
353
387
|
.strict()
|
|
354
388
|
.parse({ entryId, ...options });
|
|
355
389
|
const rows = await read(db, context, { ...request, operation: "get", limit: 1 });
|
|
356
|
-
|
|
390
|
+
if (!rows[0]) return null;
|
|
391
|
+
const record = decodeProjection(rows[0], true);
|
|
392
|
+
return (await withIndexStatus(db, context, [record], request.view ?? "published"))[0]!;
|
|
357
393
|
}
|
|
358
394
|
|
|
359
395
|
const Cursor = z
|
|
@@ -400,7 +436,12 @@ export async function listKnowledgeEntries(
|
|
|
400
436
|
}
|
|
401
437
|
: {}),
|
|
402
438
|
});
|
|
403
|
-
const entries =
|
|
439
|
+
const entries = await withIndexStatus(
|
|
440
|
+
db,
|
|
441
|
+
context,
|
|
442
|
+
rows.slice(0, request.limit).map((row) => decodeProjection(row, false)),
|
|
443
|
+
request.view,
|
|
444
|
+
);
|
|
404
445
|
return {
|
|
405
446
|
entries,
|
|
406
447
|
nextCursor:
|
|
@@ -2,7 +2,7 @@ import { namedSubjectPersonalWorkspaceId } from "./slack-routing-personal-worksp
|
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { StoredKnowledgeEntryContent } from "@opengeni/contracts";
|
|
5
|
-
import { rawRows, withRlsContext, type Database } from "./database";
|
|
5
|
+
import { rawRows, withRlsContext, withWorkspaceUsageLock, type Database } from "./database";
|
|
6
6
|
import { fromPostgresLosslessJson, toPostgresLosslessText } from "./lossless-json";
|
|
7
7
|
|
|
8
8
|
const Claim = z.object({
|
|
@@ -17,6 +17,57 @@ const Claim = z.object({
|
|
|
17
17
|
});
|
|
18
18
|
export type KnowledgeIndexClaim = z.infer<typeof Claim>;
|
|
19
19
|
|
|
20
|
+
/** DB-clock cutoff for this worker's first indexing poll, not a host-clock guess. */
|
|
21
|
+
export async function knowledgeIndexBillingActivationTime(db: Database): Promise<Date> {
|
|
22
|
+
const [row] = await rawRows<{ activatedAt: Date }>(
|
|
23
|
+
db,
|
|
24
|
+
sql`SELECT clock_timestamp() AS "activatedAt"`,
|
|
25
|
+
);
|
|
26
|
+
return z.coerce.date().parse(row?.activatedAt);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Persist the chosen mode for exactly this leased generation. Paid review-first
|
|
30
|
+
* revisions release the lease without embedding until published. */
|
|
31
|
+
export async function freezeKnowledgeIndexBillingMode(
|
|
32
|
+
db: Database,
|
|
33
|
+
raw: KnowledgeIndexClaim,
|
|
34
|
+
requested: "usage_only" | "shadow" | "credits",
|
|
35
|
+
activatedAt: Date,
|
|
36
|
+
rateMicrosPerMillionBytes: number,
|
|
37
|
+
) {
|
|
38
|
+
const claim = Claim.parse(raw);
|
|
39
|
+
return withRlsContext(db, { accountId: claim.accountId }, async (tx) => {
|
|
40
|
+
const [row] = await rawRows<{ policy: unknown }>(
|
|
41
|
+
tx,
|
|
42
|
+
sql`SELECT knowledge_index_billing_policy(${claim.accountId}::uuid,
|
|
43
|
+
${claim.revisionId}::uuid,${claim.leaseId}::uuid,${requested},
|
|
44
|
+
${activatedAt.toISOString()}::timestamptz,
|
|
45
|
+
${rateMicrosPerMillionBytes}::bigint) AS policy`,
|
|
46
|
+
);
|
|
47
|
+
return z
|
|
48
|
+
.object({
|
|
49
|
+
mode: z.enum(["usage_only", "shadow", "credits", "awaiting_review", "obsolete"]),
|
|
50
|
+
rateMicrosPerMillionBytes: z.number().int().nonnegative(),
|
|
51
|
+
})
|
|
52
|
+
.parse(row?.policy);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Serialize paid query admission and settlement across an entire account. */
|
|
57
|
+
export async function withKnowledgeQueryAccountLock<T>(
|
|
58
|
+
db: Database,
|
|
59
|
+
accountId: string,
|
|
60
|
+
workspaceId: string,
|
|
61
|
+
fn: (db: Database) => Promise<T>,
|
|
62
|
+
): Promise<T> {
|
|
63
|
+
return withWorkspaceUsageLock(db, workspaceId, async (tx) => {
|
|
64
|
+
await tx.execute(
|
|
65
|
+
sql`SELECT pg_advisory_xact_lock(hashtext(${`knowledge-query-account:${accountId}`}))`,
|
|
66
|
+
);
|
|
67
|
+
return fn(tx);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
20
71
|
/** Internal projection dispatcher. No HTTP, MCP or agent tool exposes this capability. */
|
|
21
72
|
export async function claimKnowledgeIndexJobs(
|
|
22
73
|
db: Database,
|
|
@@ -121,6 +172,33 @@ export async function deferKnowledgeIndexJob(db: Database, claim: KnowledgeIndex
|
|
|
121
172
|
return work(db, claim, { operation: "fail" });
|
|
122
173
|
}
|
|
123
174
|
|
|
175
|
+
/** A funding wait is not a provider failure. Keep the lease checkpoint intact. */
|
|
176
|
+
export async function waitKnowledgeIndexForFunding(db: Database, raw: KnowledgeIndexClaim) {
|
|
177
|
+
const claim = Claim.parse(raw);
|
|
178
|
+
return withRlsContext(db, { accountId: claim.accountId }, async (tx) => {
|
|
179
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
180
|
+
tx,
|
|
181
|
+
sql`SELECT knowledge_index_wait_for_funding(${claim.accountId}::uuid,
|
|
182
|
+
${claim.revisionId}::uuid,${claim.leaseId}::uuid) AS result`,
|
|
183
|
+
);
|
|
184
|
+
return z.object({ status: z.enum(["pending", "obsolete"]) }).parse(row?.result);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Hold publication stable through the paid append + post-use debit commit.
|
|
189
|
+
* Call only after embedding, inside the same transaction as settlement. */
|
|
190
|
+
export async function guardPaidKnowledgeIndexPublication(db: Database, raw: KnowledgeIndexClaim) {
|
|
191
|
+
const claim = Claim.parse(raw);
|
|
192
|
+
return withRlsContext(db, { accountId: claim.accountId }, async (tx) => {
|
|
193
|
+
const [row] = await rawRows<{ status: string }>(
|
|
194
|
+
tx,
|
|
195
|
+
sql`SELECT knowledge_index_paid_publication_guard(${claim.accountId}::uuid,
|
|
196
|
+
${claim.revisionId}::uuid,${claim.leaseId}::uuid) AS status`,
|
|
197
|
+
);
|
|
198
|
+
return z.enum(["published", "awaiting_review", "obsolete"]).parse(row?.status);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
124
202
|
export async function continueKnowledgeIndexJob(db: Database, claim: KnowledgeIndexClaim) {
|
|
125
203
|
return work(db, claim, { operation: "continue" });
|
|
126
204
|
}
|
|
@@ -83,15 +83,32 @@ export async function completeSelfServiceOrganizationSetup(
|
|
|
83
83
|
organizationName: string;
|
|
84
84
|
operationId: string;
|
|
85
85
|
requestFingerprint: string;
|
|
86
|
+
trialCreditsEnabled?: boolean;
|
|
86
87
|
},
|
|
87
88
|
): Promise<CompleteSelfServiceOrganizationSetupResponseType> {
|
|
88
89
|
return await db.transaction(async (tx) => {
|
|
89
90
|
const txDb = tx as unknown as Database;
|
|
90
91
|
await setSubjectRlsContext(txDb, input.actorSubjectId);
|
|
92
|
+
// Transaction-local and set by the trusted API, never the request body.
|
|
93
|
+
// The AFTER INSERT receipt trigger handles the grant atomically with setup;
|
|
94
|
+
// replaying an old receipt cannot create a new grant after launch.
|
|
95
|
+
await rawRows(
|
|
96
|
+
txDb,
|
|
97
|
+
sql`select pg_catalog.set_config(
|
|
98
|
+
'opengeni.verified_signup_trial_enabled',
|
|
99
|
+
${input.trialCreditsEnabled === true ? "on" : "off"}, true
|
|
100
|
+
)`,
|
|
101
|
+
);
|
|
91
102
|
const [row] = await rawRows<{ result: unknown }>(
|
|
92
103
|
txDb,
|
|
93
104
|
sql`select complete_self_service_organization_setup(
|
|
94
|
-
${JSON.stringify(
|
|
105
|
+
${JSON.stringify({
|
|
106
|
+
authUserId: input.authUserId,
|
|
107
|
+
actorSubjectId: input.actorSubjectId,
|
|
108
|
+
organizationName: input.organizationName,
|
|
109
|
+
operationId: input.operationId,
|
|
110
|
+
requestFingerprint: input.requestFingerprint,
|
|
111
|
+
})}::jsonb
|
|
95
112
|
) as result`,
|
|
96
113
|
);
|
|
97
114
|
return CompleteSelfServiceOrganizationSetupResponse.parse(row?.result);
|
package/src/migrate.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface ConcurrentIndexMigration {
|
|
|
40
40
|
|
|
41
41
|
export type MigrationRuntimeOptions = {
|
|
42
42
|
maxNestedAgentDepth?: number;
|
|
43
|
+
/**
|
|
44
|
+
* For managed Postgres where an administrator preinstalls pgvector but the
|
|
45
|
+
* migration owner cannot execute CREATE EXTENSION, verify the public vector
|
|
46
|
+
* type and omit only 0000's exact vector installation statement. The default
|
|
47
|
+
* continues to execute the shipped migration verbatim.
|
|
48
|
+
*/
|
|
49
|
+
preinstalledVector?: boolean;
|
|
43
50
|
/**
|
|
44
51
|
* Exact database login roles that may run an OpenGeni API or worker against
|
|
45
52
|
* this target. Maintenance cutovers use this list to reject a live mixed-
|
|
@@ -173,7 +180,11 @@ export async function executeMigrationFile(
|
|
|
173
180
|
sql: postgres.Sql,
|
|
174
181
|
file: string,
|
|
175
182
|
sqlText: string,
|
|
183
|
+
options?: Pick<MigrationRuntimeOptions, "preinstalledVector">,
|
|
176
184
|
): Promise<void> {
|
|
185
|
+
if (file === "0000_initial.sql" && options?.preinstalledVector) {
|
|
186
|
+
sqlText = await initialMigrationWithPreinstalledVector(sql, sqlText);
|
|
187
|
+
}
|
|
177
188
|
if (sqlText.includes(SKILL_METADATA_MIGRATION_MARKER)) {
|
|
178
189
|
if (file !== "0433_unified_skill_lifecycle.sql")
|
|
179
190
|
throw new Error("Skill metadata stage is restricted to migration 0433");
|
|
@@ -253,8 +264,13 @@ export async function executeMigrationFile(
|
|
|
253
264
|
// makes a fresh database capable of applying maintenance migration 0138
|
|
254
265
|
// and later migrations capable of crossing the 0352 sessions policy
|
|
255
266
|
// without a process-global PGOPTIONS escape hatch.
|
|
267
|
+
// Bound ordinary DDL lock acquisition in that same implicit transaction.
|
|
268
|
+
// The migration body follows this preamble, so its own SET LOCAL can still
|
|
269
|
+
// override the default; a lock timeout aborts the whole body before the
|
|
270
|
+
// caller writes its separate success receipt.
|
|
256
271
|
await sql.unsafe(
|
|
257
272
|
`SELECT
|
|
273
|
+
pg_catalog.set_config('lock_timeout', '5s', true),
|
|
258
274
|
pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true),
|
|
259
275
|
pg_catalog.set_config('opengeni.session_variable_set_attachments_v1', '1', true);\n${file === "0434_ordered_model_history.sql" ? "SET CONSTRAINTS ALL IMMEDIATE;\n" : ""}${sqlText}`,
|
|
260
276
|
);
|
|
@@ -282,6 +298,46 @@ export async function executeMigrationFile(
|
|
|
282
298
|
}
|
|
283
299
|
}
|
|
284
300
|
|
|
301
|
+
const initialExtensionPreamble =
|
|
302
|
+
"CREATE EXTENSION IF NOT EXISTS pgcrypto;\nCREATE EXTENSION IF NOT EXISTS vector;\n";
|
|
303
|
+
|
|
304
|
+
/** Never rewrite arbitrary migrations or trust a same-named type in another schema. */
|
|
305
|
+
export async function initialMigrationWithPreinstalledVector(
|
|
306
|
+
sql: postgres.Sql,
|
|
307
|
+
sqlText: string,
|
|
308
|
+
): Promise<string> {
|
|
309
|
+
if (!sqlText.startsWith(initialExtensionPreamble)) {
|
|
310
|
+
throw new Error("0000 initial extension preamble changed; review managed Postgres admission");
|
|
311
|
+
}
|
|
312
|
+
const [installed] = await sql<Array<{ present: boolean }>>`
|
|
313
|
+
SELECT EXISTS (
|
|
314
|
+
SELECT 1 FROM pg_catalog.pg_extension extension
|
|
315
|
+
JOIN pg_catalog.pg_namespace namespace ON namespace.oid = extension.extnamespace
|
|
316
|
+
JOIN pg_catalog.pg_type type ON type.typnamespace = namespace.oid AND type.typname = 'vector'
|
|
317
|
+
JOIN pg_catalog.pg_depend dependency
|
|
318
|
+
ON dependency.classid = 'pg_catalog.pg_type'::regclass
|
|
319
|
+
AND dependency.objid = type.oid
|
|
320
|
+
AND dependency.refclassid = 'pg_catalog.pg_extension'::regclass
|
|
321
|
+
AND dependency.refobjid = extension.oid AND dependency.deptype = 'e'
|
|
322
|
+
WHERE extension.extname = 'vector' AND namespace.nspname = 'public'
|
|
323
|
+
) AS present
|
|
324
|
+
`;
|
|
325
|
+
if (!installed?.present) {
|
|
326
|
+
throw new Error("Preinstalled public pgvector extension and type are required");
|
|
327
|
+
}
|
|
328
|
+
return (
|
|
329
|
+
"CREATE EXTENSION IF NOT EXISTS pgcrypto;\n" + sqlText.slice(initialExtensionPreamble.length)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function preinstalledVectorPolicy(options: MigrationRuntimeOptions | undefined): boolean {
|
|
334
|
+
if (options !== undefined) return options.preinstalledVector === true;
|
|
335
|
+
const configured = process.env.OPENGENI_MIGRATIONS_PREINSTALLED_VECTOR;
|
|
336
|
+
if (configured === undefined || configured === "false") return false;
|
|
337
|
+
if (configured === "true") return true;
|
|
338
|
+
throw new Error("OPENGENI_MIGRATIONS_PREINSTALLED_VECTOR must be true or false");
|
|
339
|
+
}
|
|
340
|
+
|
|
285
341
|
function deploymentDepthPolicy(
|
|
286
342
|
options: MigrationRuntimeOptions | undefined,
|
|
287
343
|
): DeploymentDepthPolicy {
|
|
@@ -425,6 +481,7 @@ export async function migrate(
|
|
|
425
481
|
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), "../drizzle");
|
|
426
482
|
const files = (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort();
|
|
427
483
|
const depthPolicy = deploymentDepthPolicy(runtimeOptions);
|
|
484
|
+
const preinstalledVector = preinstalledVectorPolicy(runtimeOptions);
|
|
428
485
|
const sql = postgres(databaseUrl, { max: 1 });
|
|
429
486
|
try {
|
|
430
487
|
// Serialize concurrent migrate() runs; the session-level lock is released
|
|
@@ -477,7 +534,7 @@ export async function migrate(
|
|
|
477
534
|
if (sqlText === undefined) {
|
|
478
535
|
throw new Error(`Pending migration source was not loaded: ${file}`);
|
|
479
536
|
}
|
|
480
|
-
await executeMigrationFile(sql, file, sqlText);
|
|
537
|
+
await executeMigrationFile(sql, file, sqlText, { preinstalledVector });
|
|
481
538
|
await sql`INSERT INTO "schema_migrations" ("name") VALUES (${file}) ON CONFLICT DO NOTHING`;
|
|
482
539
|
}
|
|
483
540
|
// Reconcile even when all migration names were already recorded. This is
|
|
@@ -59,10 +59,21 @@ export async function createManagedOrganization(
|
|
|
59
59
|
subjectLabel: string;
|
|
60
60
|
name: string;
|
|
61
61
|
operationId: string;
|
|
62
|
+
trialCreditsEnabled?: boolean;
|
|
62
63
|
},
|
|
63
64
|
): Promise<CreateOrganizationResponseType> {
|
|
64
65
|
return await db.transaction(async (tx) => {
|
|
65
66
|
await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
|
|
67
|
+
// This legacy first-organization route delegates to the same self-service
|
|
68
|
+
// receipt writer. Do not let it consume the one-shot receipt while the
|
|
69
|
+
// trial is enabled without issuing its transaction-atomic grant.
|
|
70
|
+
await rawRows(
|
|
71
|
+
tx,
|
|
72
|
+
sql`select pg_catalog.set_config(
|
|
73
|
+
'opengeni.verified_signup_trial_enabled',
|
|
74
|
+
${input.trialCreditsEnabled === true ? "on" : "off"}, true
|
|
75
|
+
)`,
|
|
76
|
+
);
|
|
66
77
|
const [row] = await rawRows<{ result: unknown }>(
|
|
67
78
|
tx,
|
|
68
79
|
sql`select create_managed_organization(
|
package/src/provision-roles.ts
CHANGED
|
@@ -840,6 +840,18 @@ BEGIN
|
|
|
840
840
|
IF to_regprocedure(format('%I.knowledge_entry_apply(uuid,uuid,jsonb,jsonb)', ${literal(schema)})) IS NOT NULL THEN
|
|
841
841
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_index_claim(text,integer,integer) TO %I', ${literal(schema)}, ${literal(role)});
|
|
842
842
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_index_work(uuid,uuid,uuid,jsonb) TO %I', ${literal(schema)}, ${literal(role)});
|
|
843
|
+
IF to_regprocedure(format('%I.knowledge_index_billing_policy(uuid,uuid,uuid,text,timestamptz,bigint)', ${literal(schema)})) IS NOT NULL THEN
|
|
844
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_index_billing_policy(uuid,uuid,uuid,text,timestamptz,bigint) TO %I', ${literal(schema)}, ${literal(role)});
|
|
845
|
+
END IF;
|
|
846
|
+
IF to_regprocedure(format('%I.knowledge_index_wait_for_funding(uuid,uuid,uuid)', ${literal(schema)})) IS NOT NULL THEN
|
|
847
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_index_wait_for_funding(uuid,uuid,uuid) TO %I', ${literal(schema)}, ${literal(role)});
|
|
848
|
+
END IF;
|
|
849
|
+
IF to_regprocedure(format('%I.knowledge_index_paid_publication_guard(uuid,uuid,uuid)', ${literal(schema)})) IS NOT NULL THEN
|
|
850
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_index_paid_publication_guard(uuid,uuid,uuid) TO %I', ${literal(schema)}, ${literal(role)});
|
|
851
|
+
END IF;
|
|
852
|
+
IF to_regprocedure(format('%I.knowledge_visible_index_status(uuid,uuid,jsonb,jsonb,text)', ${literal(schema)})) IS NOT NULL THEN
|
|
853
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_visible_index_status(uuid,uuid,jsonb,jsonb,text) TO %I', ${literal(schema)}, ${literal(role)});
|
|
854
|
+
END IF;
|
|
843
855
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.agent_instruction_apply(uuid,uuid,jsonb,jsonb) TO %I', ${literal(schema)}, ${literal(role)});
|
|
844
856
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_entry_apply(uuid,uuid,jsonb,jsonb) TO %I', ${literal(schema)}, ${literal(role)});
|
|
845
857
|
EXECUTE format('GRANT EXECUTE ON FUNCTION %I.knowledge_entry_confirm_legacy(uuid,uuid,jsonb,jsonb) TO %I', ${literal(schema)}, ${literal(role)});
|
|
@@ -406,8 +406,10 @@ export async function retainedProcessCancellationRequested(
|
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
/**
|
|
410
|
-
*
|
|
409
|
+
/** Record deadline cancellation for an exact retained Modal command. Legacy
|
|
410
|
+
* PTY callers send Ctrl-C first because cancellation fences stdin; non-PTY
|
|
411
|
+
* callers only probe, since Ctrl-C would be data. An earlier explicit stop
|
|
412
|
+
* remains immutable; the deadline uses its own clock. Lock order: process -> lease. */
|
|
411
413
|
export async function requestRetainedProcessDeadlineCancellation(
|
|
412
414
|
db: Database,
|
|
413
415
|
scope: ProcessScope,
|
|
@@ -422,8 +424,8 @@ export async function requestRetainedProcessDeadlineCancellation(
|
|
|
422
424
|
if (
|
|
423
425
|
!row ||
|
|
424
426
|
row.state !== "active" ||
|
|
425
|
-
|
|
426
|
-
|
|
427
|
+
row.providerBackend !== "modal" ||
|
|
428
|
+
row.routeTargetId !== null
|
|
427
429
|
)
|
|
428
430
|
return false;
|
|
429
431
|
const [lease] = await tx.execute<{ id: string }>(sql`
|
|
@@ -434,11 +436,19 @@ export async function requestRetainedProcessDeadlineCancellation(
|
|
|
434
436
|
and rotation_requested_at is not null and rotation_reason = 'provider_deadline' for update
|
|
435
437
|
`);
|
|
436
438
|
if (!lease) return false;
|
|
437
|
-
if (!row.cancellationRequestedAt)
|
|
439
|
+
if (!row.deadlineCancellationRequestedAt || !row.cancellationRequestedAt) {
|
|
438
440
|
await tx
|
|
439
441
|
.update(schema.sandboxRetainedProcesses)
|
|
440
|
-
.set({
|
|
442
|
+
.set({
|
|
443
|
+
...(!row.cancellationRequestedAt
|
|
444
|
+
? { cancellationRequestedAt: sql`now()`, cancellationReason: "provider_deadline" }
|
|
445
|
+
: {}),
|
|
446
|
+
...(!row.deadlineCancellationRequestedAt
|
|
447
|
+
? { deadlineCancellationRequestedAt: sql`now()` }
|
|
448
|
+
: {}),
|
|
449
|
+
})
|
|
441
450
|
.where(processWhere(scope));
|
|
451
|
+
}
|
|
442
452
|
return true;
|
|
443
453
|
});
|
|
444
454
|
}
|
package/src/runtime-posture.ts
CHANGED
|
@@ -586,6 +586,10 @@ const XAI_AUTHORITY_TABLES = [
|
|
|
586
586
|
const UNIFIED_KNOWLEDGE_ROUTINES = [
|
|
587
587
|
"knowledge_index_claim(text, integer, integer)",
|
|
588
588
|
"knowledge_index_work(uuid, uuid, uuid, jsonb)",
|
|
589
|
+
"knowledge_index_billing_policy(uuid, uuid, uuid, text, timestamp with time zone, bigint)",
|
|
590
|
+
"knowledge_index_wait_for_funding(uuid, uuid, uuid)",
|
|
591
|
+
"knowledge_index_paid_publication_guard(uuid, uuid, uuid)",
|
|
592
|
+
"knowledge_visible_index_status(uuid, uuid, jsonb, jsonb, text)",
|
|
589
593
|
"knowledge_entry_apply(uuid, uuid, jsonb, jsonb)",
|
|
590
594
|
"knowledge_entry_confirm_legacy(uuid, uuid, jsonb, jsonb)",
|
|
591
595
|
"agent_instruction_apply(uuid, uuid, jsonb, jsonb)",
|
package/src/schema.ts
CHANGED
|
@@ -9960,6 +9960,9 @@ export const sandboxRetainedProcesses = pgTable(
|
|
|
9960
9960
|
supervisionOutputCaptured: boolean("supervision_output_captured").notNull().default(false),
|
|
9961
9961
|
cancellationRequestedAt: timestamp("cancellation_requested_at", { withTimezone: true }),
|
|
9962
9962
|
cancellationReason: text("cancellation_reason"),
|
|
9963
|
+
deadlineCancellationRequestedAt: timestamp("deadline_cancellation_requested_at", {
|
|
9964
|
+
withTimezone: true,
|
|
9965
|
+
}),
|
|
9963
9966
|
providerCommandInputIndex: bigint("provider_command_input_index", { mode: "number" })
|
|
9964
9967
|
.notNull()
|
|
9965
9968
|
.default(0),
|