@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.
Files changed (87) hide show
  1. package/dist/browser-sessions.d.ts +26 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-reconciliation.js +2 -2
  4. package/dist/{chunk-4RDUZ5UV.js → chunk-4ZQFT5II.js} +2 -2
  5. package/dist/{chunk-6ORJPCYW.js → chunk-5UOWNKPP.js} +2 -2
  6. package/dist/{chunk-4CCFFLDZ.js → chunk-BG46J5HF.js} +13 -2
  7. package/dist/chunk-BG46J5HF.js.map +1 -0
  8. package/dist/{chunk-CSVHCF3R.js → chunk-IB3SW3JW.js} +11 -4
  9. package/dist/{chunk-CSVHCF3R.js.map → chunk-IB3SW3JW.js.map} +1 -1
  10. package/dist/{chunk-GSB4ISHE.js → chunk-IIPWNNVR.js} +4 -1
  11. package/dist/chunk-IIPWNNVR.js.map +1 -0
  12. package/dist/{chunk-MAMBI2HR.js → chunk-JPYN5XKV.js} +39 -3
  13. package/dist/chunk-JPYN5XKV.js.map +1 -0
  14. package/dist/{chunk-LSLNMENV.js → chunk-KZXWUXWV.js} +4 -4
  15. package/dist/{chunk-LO6BVXXH.js → chunk-LD52IFS3.js} +2 -2
  16. package/dist/{chunk-JILAWE26.js → chunk-M5Z6II2P.js} +2 -2
  17. package/dist/{chunk-PGIFHEBA.js → chunk-RVEOGRMF.js} +2 -2
  18. package/dist/{chunk-H7BC444C.js → chunk-SRAMNVHT.js} +17 -1
  19. package/dist/chunk-SRAMNVHT.js.map +1 -0
  20. package/dist/{chunk-XK6HXON2.js → chunk-UXFBXCHM.js} +2 -2
  21. package/dist/{chunk-RXH5CFG3.js → chunk-VE2XK2KD.js} +12 -8
  22. package/dist/{chunk-RXH5CFG3.js.map → chunk-VE2XK2KD.js.map} +1 -1
  23. package/dist/{chunk-FULLE5CF.js → chunk-WT7HG43Y.js} +3 -3
  24. package/dist/{chunk-I7HQL6FK.js → chunk-ZM5BA3TD.js} +3 -3
  25. package/dist/connect-attempts.js +3 -3
  26. package/dist/database.d.ts +8 -0
  27. package/dist/editable-artifact-durable-export.js +2 -2
  28. package/dist/editable-artifacts.js +3 -3
  29. package/dist/index.d.ts +69 -20
  30. package/dist/index.js +633 -202
  31. package/dist/index.js.map +1 -1
  32. package/dist/knowledge-entries.d.ts +2 -0
  33. package/dist/knowledge-indexing.d.ts +17 -0
  34. package/dist/managed-auth-session-sets.js +3 -3
  35. package/dist/managed-user-setup.d.ts +1 -0
  36. package/dist/mcp-operations.js +2 -2
  37. package/dist/migrate.d.ts +10 -1
  38. package/dist/migrate.js +3 -1
  39. package/dist/organization-integration-policy.js +3 -3
  40. package/dist/organization-membership-lifecycle.d.ts +1 -0
  41. package/dist/provision-roles.js +1 -1
  42. package/dist/retained-provider-commands.d.ts +4 -2
  43. package/dist/retained-provider-commands.js +6 -6
  44. package/dist/runtime-posture.d.ts +1 -1
  45. package/dist/schema.d.ts +25 -8
  46. package/dist/schema.js +1 -1
  47. package/dist/session-background-commands.js +4 -4
  48. package/dist/session-command-output.js +5 -5
  49. package/dist/session-event-slices.js +3 -3
  50. package/dist/session-mcp-credential-rotation.js +4 -4
  51. package/dist/session-tenancy.js +3 -3
  52. package/dist/video-generation.js +3 -3
  53. package/dist/workspace-tool-defaults.js +4 -4
  54. package/drizzle/0508_deadline_command_workspace_capture.sql +75 -0
  55. package/drizzle/0509_verified_signup_trial_credits.sql +76 -0
  56. package/drizzle/0510_knowledge_index_funding_wait.sql +174 -0
  57. package/drizzle/0511_knowledge_visible_index_status.sql +56 -0
  58. package/drizzle/0512_insights_fact_reads_hash_join.sql +31 -0
  59. package/drizzle/0513_message_fork_prefix_compaction.sql +85 -0
  60. package/drizzle/0514_failed_session_variable_set_attach.sql +25 -0
  61. package/package.json +5 -5
  62. package/src/browser-sessions.ts +71 -1
  63. package/src/database.ts +18 -0
  64. package/src/index.ts +706 -252
  65. package/src/insights.ts +44 -36
  66. package/src/knowledge-entries.ts +43 -2
  67. package/src/knowledge-indexing.ts +79 -1
  68. package/src/managed-user-setup.ts +18 -1
  69. package/src/migrate.ts +58 -1
  70. package/src/organization-membership-lifecycle.ts +11 -0
  71. package/src/provision-roles.ts +12 -0
  72. package/src/retained-provider-commands.ts +16 -6
  73. package/src/runtime-posture.ts +4 -0
  74. package/src/schema.ts +3 -0
  75. package/dist/chunk-4CCFFLDZ.js.map +0 -1
  76. package/dist/chunk-GSB4ISHE.js.map +0 -1
  77. package/dist/chunk-H7BC444C.js.map +0 -1
  78. package/dist/chunk-MAMBI2HR.js.map +0 -1
  79. /package/dist/{chunk-4RDUZ5UV.js.map → chunk-4ZQFT5II.js.map} +0 -0
  80. /package/dist/{chunk-6ORJPCYW.js.map → chunk-5UOWNKPP.js.map} +0 -0
  81. /package/dist/{chunk-LSLNMENV.js.map → chunk-KZXWUXWV.js.map} +0 -0
  82. /package/dist/{chunk-LO6BVXXH.js.map → chunk-LD52IFS3.js.map} +0 -0
  83. /package/dist/{chunk-JILAWE26.js.map → chunk-M5Z6II2P.js.map} +0 -0
  84. /package/dist/{chunk-PGIFHEBA.js.map → chunk-RVEOGRMF.js.map} +0 -0
  85. /package/dist/{chunk-XK6HXON2.js.map → chunk-UXFBXCHM.js.map} +0 -0
  86. /package/dist/{chunk-FULLE5CF.js.map → chunk-WT7HG43Y.js.map} +0 -0
  87. /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
- const buckets = await scopedDb
994
- .select({
995
- depth: schema.sessions.nestedAgentDepth,
996
- sessions: sql<number>`count(*)::int`,
997
- })
998
- .from(schema.sessions)
999
- .where(eq(schema.sessions.workspaceId, workspaceId))
1000
- .groupBy(schema.sessions.nestedAgentDepth)
1001
- .orderBy(schema.sessions.nestedAgentDepth);
1002
- const [stats] = await scopedDb
1003
- .select({
1004
- sessionsTouched: sql<number>`count(*)::int`,
1005
- rootSessions: sql<number>`count(*) filter (where ${schema.sessions.nestedAgentDepth} = 0)::int`,
1006
- deepestDepth: sql<number>`coalesce(max(${schema.sessions.nestedAgentDepth}), 0)`,
1007
- avgDepth: sql<number>`coalesce(avg(${schema.sessions.nestedAgentDepth}), 0)`,
1008
- })
1009
- .from(schema.sessions)
1010
- .where(eq(schema.sessions.workspaceId, workspaceId));
1011
- const [deepest] = await scopedDb
1012
- .select({
1013
- id: schema.sessions.id,
1014
- title: schema.sessions.title,
1015
- depth: schema.sessions.nestedAgentDepth,
1016
- })
1017
- .from(schema.sessions)
1018
- .where(eq(schema.sessions.workspaceId, workspaceId))
1019
- .orderBy(desc(schema.sessions.nestedAgentDepth), desc(schema.sessions.updatedAt))
1020
- .limit(1);
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: buckets.map((row) => ({
1030
- depth: row.depth,
1031
- sessions: Number(row.sessions),
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: Number(stats?.avgDepth ?? 0),
1046
+ avgDepth,
1039
1047
  goalsActive: Number(goals?.active ?? 0),
1040
1048
  goalsCompleted: Number(goals?.completed ?? 0),
1041
1049
  };
@@ -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
- return rows[0] ? decodeProjection(rows[0], true) : null;
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 = rows.slice(0, request.limit).map((row) => decodeProjection(row, false));
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(input)}::jsonb
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(
@@ -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
- /** Called by the reaper for an exact retained command. Ordinary completed turns
410
- * are intentionally irrelevant. Lock order remains process -> lease. */
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
- !row.providerCommand ||
426
- !supervisionDescriptor(row.providerCommand)
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({ cancellationRequestedAt: new Date(), cancellationReason: "provider_deadline" })
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
  }
@@ -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),