@opengeni/db 0.9.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4LG5NBTC.js → chunk-VUKRIBO5.js} +577 -12
- package/dist/chunk-VUKRIBO5.js.map +1 -0
- package/dist/{chunk-KW526IJA.js → chunk-Y5WZZVQK.js} +80 -4
- package/dist/chunk-Y5WZZVQK.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6372 -2189
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +6 -3
- package/dist/migrate.js +1 -1
- package/dist/provision-roles.d.ts +1122 -91
- package/dist/{schema-CdPGTHlD.d.ts → schema-CnpD6BcX.d.ts} +5908 -3626
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +19 -1
- package/drizzle/0053_codex_credential_leases.sql +2 -2
- package/drizzle/0057_durable_queue_control.sql +1 -1
- package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
- package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
- package/drizzle/0063_session_control_mega_foundation.sql +1 -1
- package/drizzle/0064_rotation_strategy_sharded_backfill.sql +1 -1
- package/drizzle/0065_codex_subscription_overview.sql +168 -0
- package/drizzle/0065_session_tool_policy.sql +38 -0
- package/drizzle/0067_session_event_payload_bounds.sql +2 -2
- package/drizzle/0068_workspace_control_event_bounds.sql +2 -2
- package/drizzle/0069_session_event_history_backfill.sql +2 -2
- package/drizzle/0074_session_activity_revisions.sql +2 -2
- package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
- package/drizzle/0107_host_export_lineage_contract.sql +381 -0
- package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
- package/drizzle/0109_nested_agent_depth_expand.sql +42 -0
- package/drizzle/0110_nested_agent_depth_boundary.sql +480 -0
- package/drizzle/0111_nested_agent_depth_backfill.sql +49 -0
- package/drizzle/0112_nested_agent_depth_contract.sql +38 -0
- package/drizzle/0113_nested_agent_depth_validate.sql +13 -0
- package/drizzle/0114_nested_agent_depth_contract.sql +49 -0
- package/drizzle/0115_nested_agent_depth_validate.sql +11 -0
- package/drizzle/0116_nested_agent_depth_index.sql +4 -0
- package/drizzle/0117_sandbox_recovery_generations.sql +699 -0
- package/drizzle/0118_new_session_drafts.sql +59 -0
- package/drizzle/0119_pending_tool_output_policy.sql +5 -0
- package/drizzle/0120_durable_goal_wake.sql +360 -0
- package/drizzle/0121_goal_update_idempotency.sql +11 -0
- package/package.json +5 -4
- package/src/codex-token-resolver.ts +175 -14
- package/src/connection-token-resolver.ts +143 -120
- package/src/event-payload-sanitizer.ts +32 -2
- package/src/index.ts +7734 -1330
- package/src/migrate.ts +131 -2
- package/src/new-session-drafts.ts +144 -0
- package/src/schema.ts +626 -16
- package/src/session-control.ts +44 -18
- package/src/session-queue-commands.ts +94 -21
- package/src/session-tool-call-settlement.ts +6 -1
- package/dist/chunk-4LG5NBTC.js.map +0 -1
- package/dist/chunk-KW526IJA.js.map +0 -1
package/src/migrate.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import postgres from "postgres";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_DATABASE_URL = "postgres://opengeni:opengeni@127.0.0.1:5432/opengeni";
|
|
7
|
+
const DEFAULT_MAX_NESTED_AGENT_DEPTH = 3;
|
|
8
|
+
const MAX_NESTED_AGENT_DEPTH = 2_147_483_647;
|
|
9
|
+
const batchedBackfillDirective =
|
|
10
|
+
/^-- opengeni:batched-backfill batch-size=(\d+) lock-timeout=(\d+(?:ms|s|min)) statement-timeout=(\d+(?:ms|s|min))$/;
|
|
7
11
|
const concurrentIndexDirective = /^-- opengeni:concurrent-index lock-timeout=(\d+(?:ms|s|min))$/;
|
|
8
12
|
const concurrentIndexStatement =
|
|
9
13
|
/^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;
|
|
@@ -24,6 +28,15 @@ export interface ConcurrentIndexMigration {
|
|
|
24
28
|
statement: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export type MigrationRuntimeOptions = {
|
|
32
|
+
maxNestedAgentDepth?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DeploymentDepthPolicy = {
|
|
36
|
+
maxNestedAgentDepth: number;
|
|
37
|
+
source: "deployment" | "default";
|
|
38
|
+
};
|
|
39
|
+
|
|
27
40
|
/** A bare Postgres identifier (schema/role name) safe to interpolate into DDL. */
|
|
28
41
|
function assertIdentifier(name: string, value: string): string {
|
|
29
42
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
@@ -95,11 +108,69 @@ export function parseConcurrentIndexMigration(
|
|
|
95
108
|
};
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
function parseBatchedBackfillMigration(
|
|
112
|
+
file: string,
|
|
113
|
+
sqlText: string,
|
|
114
|
+
): { batchSize: number; lockTimeout: string; statementTimeout: string; statement: string } | null {
|
|
115
|
+
const lines = sqlText.replaceAll("\r\n", "\n").split("\n");
|
|
116
|
+
const directiveIndex = /^-- deployment-mode: (?:rolling|maintenance)$/.test(
|
|
117
|
+
lines[0]?.trim() ?? "",
|
|
118
|
+
)
|
|
119
|
+
? 1
|
|
120
|
+
: 0;
|
|
121
|
+
const directive = batchedBackfillDirective.exec(lines[directiveIndex]?.trim() ?? "");
|
|
122
|
+
if (!directive) return null;
|
|
123
|
+
const statement = lines
|
|
124
|
+
.slice(directiveIndex + 1)
|
|
125
|
+
.filter((line) => !line.trim().startsWith("--"))
|
|
126
|
+
.join("\n")
|
|
127
|
+
.trim();
|
|
128
|
+
const withoutTrailingSemicolon = statement.endsWith(";")
|
|
129
|
+
? statement.slice(0, -1).trimEnd()
|
|
130
|
+
: statement;
|
|
131
|
+
const batchSize = Number(directive[1]!);
|
|
132
|
+
if (
|
|
133
|
+
!Number.isSafeInteger(batchSize) ||
|
|
134
|
+
batchSize < 1 ||
|
|
135
|
+
batchSize > 10_000 ||
|
|
136
|
+
!/^WITH\b/is.test(withoutTrailingSemicolon) ||
|
|
137
|
+
!/\bUPDATE\b/is.test(withoutTrailingSemicolon) ||
|
|
138
|
+
!/\bRETURNING\b/is.test(withoutTrailingSemicolon) ||
|
|
139
|
+
!new RegExp(`\\bLIMIT\\s+${batchSize}\\b`, "i").test(withoutTrailingSemicolon) ||
|
|
140
|
+
withoutTrailingSemicolon.includes(";")
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`${file}: opengeni:batched-backfill requires one bounded WITH ... UPDATE ... RETURNING statement whose LIMIT matches batch-size`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
batchSize,
|
|
148
|
+
lockTimeout: directive[2]!,
|
|
149
|
+
statementTimeout: directive[3]!,
|
|
150
|
+
statement,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
async function executeMigrationFile(
|
|
99
155
|
sql: postgres.Sql,
|
|
100
156
|
file: string,
|
|
101
157
|
sqlText: string,
|
|
102
158
|
): Promise<void> {
|
|
159
|
+
const batchedBackfill = parseBatchedBackfillMigration(file, sqlText);
|
|
160
|
+
if (batchedBackfill) {
|
|
161
|
+
await sql`select set_config('lock_timeout', ${batchedBackfill.lockTimeout}, false)`;
|
|
162
|
+
await sql`select set_config('statement_timeout', ${batchedBackfill.statementTimeout}, false)`;
|
|
163
|
+
try {
|
|
164
|
+
for (;;) {
|
|
165
|
+
const result = await sql.unsafe(batchedBackfill.statement);
|
|
166
|
+
if (result.length === 0) break;
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
await sql`select set_config('statement_timeout', '0', false)`;
|
|
170
|
+
await sql`select set_config('lock_timeout', '0', false)`;
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
103
174
|
const concurrentIndex = parseConcurrentIndexMigration(file, sqlText);
|
|
104
175
|
if (!concurrentIndex) {
|
|
105
176
|
await sql.unsafe(sqlText);
|
|
@@ -127,6 +198,53 @@ async function executeMigrationFile(
|
|
|
127
198
|
}
|
|
128
199
|
}
|
|
129
200
|
|
|
201
|
+
function deploymentDepthPolicy(
|
|
202
|
+
options: MigrationRuntimeOptions | undefined,
|
|
203
|
+
): DeploymentDepthPolicy {
|
|
204
|
+
const raw =
|
|
205
|
+
options === undefined
|
|
206
|
+
? process.env.OPENGENI_MAX_NESTED_AGENT_DEPTH?.trim() || undefined
|
|
207
|
+
: options.maxNestedAgentDepth;
|
|
208
|
+
if (raw === undefined) {
|
|
209
|
+
return { maxNestedAgentDepth: DEFAULT_MAX_NESTED_AGENT_DEPTH, source: "default" };
|
|
210
|
+
}
|
|
211
|
+
const value = typeof raw === "number" ? raw : Number(raw);
|
|
212
|
+
if (
|
|
213
|
+
!Number.isSafeInteger(value) ||
|
|
214
|
+
value < 0 ||
|
|
215
|
+
value > MAX_NESTED_AGENT_DEPTH ||
|
|
216
|
+
(typeof raw === "string" && !/^(0|[1-9][0-9]*)$/.test(raw))
|
|
217
|
+
) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`OPENGENI_MAX_NESTED_AGENT_DEPTH must be a non-negative 32-bit integer: ${raw}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return { maxNestedAgentDepth: value, source: "deployment" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function persistDeploymentDepthPolicy(
|
|
226
|
+
sql: postgres.Sql,
|
|
227
|
+
policy: DeploymentDepthPolicy,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const [relation] = await sql<{ exists: boolean }[]>`
|
|
230
|
+
select to_regclass('nested_agent_depth_configuration') is not null as exists
|
|
231
|
+
`;
|
|
232
|
+
if (!relation?.exists) return;
|
|
233
|
+
await sql`
|
|
234
|
+
insert into "nested_agent_depth_configuration" (
|
|
235
|
+
"singleton", "max_nested_agent_depth", "policy_source", "updated_at"
|
|
236
|
+
) values (true, ${policy.maxNestedAgentDepth}, ${policy.source}, now())
|
|
237
|
+
on conflict ("singleton") do update
|
|
238
|
+
set "max_nested_agent_depth" = excluded."max_nested_agent_depth",
|
|
239
|
+
"policy_source" = excluded."policy_source",
|
|
240
|
+
"updated_at" = now()
|
|
241
|
+
where "nested_agent_depth_configuration"."max_nested_agent_depth"
|
|
242
|
+
is distinct from excluded."max_nested_agent_depth"
|
|
243
|
+
or "nested_agent_depth_configuration"."policy_source"
|
|
244
|
+
is distinct from excluded."policy_source"
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
130
248
|
/**
|
|
131
249
|
* Apply the OpenGeni SQL migration chain.
|
|
132
250
|
*
|
|
@@ -160,9 +278,11 @@ export async function migrate(
|
|
|
160
278
|
process.env.OPENGENI_DATABASE_URL ??
|
|
161
279
|
DEFAULT_DATABASE_URL,
|
|
162
280
|
schema: string | undefined = process.env.OPENGENI_DB_SCHEMA?.trim() || undefined,
|
|
281
|
+
runtimeOptions?: MigrationRuntimeOptions,
|
|
163
282
|
): Promise<void> {
|
|
164
283
|
const migrationsDir = join(dirname(fileURLToPath(import.meta.url)), "../drizzle");
|
|
165
284
|
const files = (await readdir(migrationsDir)).filter((file) => file.endsWith(".sql")).sort();
|
|
285
|
+
const depthPolicy = deploymentDepthPolicy(runtimeOptions);
|
|
166
286
|
const sql = postgres(databaseUrl, { max: 1 });
|
|
167
287
|
try {
|
|
168
288
|
// Serialize concurrent migrate() runs; the session-level lock is released
|
|
@@ -177,6 +297,8 @@ export async function migrate(
|
|
|
177
297
|
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS "opengeni_private"`);
|
|
178
298
|
await sql.unsafe(`SET search_path = "${schema}", "opengeni_private", "public"`);
|
|
179
299
|
}
|
|
300
|
+
await sql`select set_config('opengeni.max_nested_agent_depth', ${String(depthPolicy.maxNestedAgentDepth)}, false)`;
|
|
301
|
+
await sql`select set_config('opengeni.nested_agent_depth_policy_source', ${depthPolicy.source}, false)`;
|
|
180
302
|
await sql.unsafe(
|
|
181
303
|
`CREATE TABLE IF NOT EXISTS "schema_migrations" ("name" text PRIMARY KEY, "applied_at" timestamptz NOT NULL DEFAULT now())`,
|
|
182
304
|
);
|
|
@@ -190,6 +312,9 @@ export async function migrate(
|
|
|
190
312
|
await executeMigrationFile(sql, file, sqlText);
|
|
191
313
|
await sql`INSERT INTO "schema_migrations" ("name") VALUES (${file}) ON CONFLICT DO NOTHING`;
|
|
192
314
|
}
|
|
315
|
+
// Reconcile even when all migration names were already recorded. This is
|
|
316
|
+
// the only supported way to change deployment policy in a live database.
|
|
317
|
+
await persistDeploymentDepthPolicy(sql, depthPolicy);
|
|
193
318
|
} finally {
|
|
194
319
|
await sql.end();
|
|
195
320
|
}
|
|
@@ -203,8 +328,12 @@ export async function migrate(
|
|
|
203
328
|
* `targetSchema` undefined → `public` → standalone behavior. Thin wrapper over
|
|
204
329
|
* `migrate` so there is one migration engine.
|
|
205
330
|
*/
|
|
206
|
-
export async function runMigrations(
|
|
207
|
-
|
|
331
|
+
export async function runMigrations(
|
|
332
|
+
adminConnection: string,
|
|
333
|
+
targetSchema?: string,
|
|
334
|
+
runtimeOptions?: MigrationRuntimeOptions,
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
await migrate(adminConnection, targetSchema, runtimeOptions);
|
|
208
337
|
}
|
|
209
338
|
|
|
210
339
|
if (import.meta.main) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NewSessionDraftOptions,
|
|
3
|
+
ReasoningEffort,
|
|
4
|
+
ResourceRef,
|
|
5
|
+
ToolRef,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
8
|
+
import type { Database } from "./index";
|
|
9
|
+
import * as schema from "./schema";
|
|
10
|
+
|
|
11
|
+
export type NewSessionDraftRow = typeof schema.newSessionDrafts.$inferSelect;
|
|
12
|
+
|
|
13
|
+
export class NewSessionDraftConflictError extends Error {
|
|
14
|
+
readonly name = "NewSessionDraftConflictError";
|
|
15
|
+
|
|
16
|
+
constructor(readonly currentRevision: number) {
|
|
17
|
+
super("New-session draft changed in another client");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class NewSessionDraftAccessError extends Error {
|
|
22
|
+
readonly name = "NewSessionDraftAccessError";
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super("New-session draft access changed");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getNewSessionDraftInTransaction(
|
|
30
|
+
db: Database,
|
|
31
|
+
input: { workspaceId: string; subjectId: string; lock?: boolean },
|
|
32
|
+
): Promise<NewSessionDraftRow | null> {
|
|
33
|
+
const query = db
|
|
34
|
+
.select()
|
|
35
|
+
.from(schema.newSessionDrafts)
|
|
36
|
+
.where(
|
|
37
|
+
and(
|
|
38
|
+
eq(schema.newSessionDrafts.workspaceId, input.workspaceId),
|
|
39
|
+
eq(schema.newSessionDrafts.subjectId, input.subjectId),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
.limit(1);
|
|
43
|
+
const rows = input.lock ? await query.for("update") : await query;
|
|
44
|
+
return rows[0] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function saveNewSessionDraftInTransaction(
|
|
48
|
+
db: Database,
|
|
49
|
+
input: {
|
|
50
|
+
accountId: string;
|
|
51
|
+
workspaceId: string;
|
|
52
|
+
subjectId: string;
|
|
53
|
+
expectedRevision: number;
|
|
54
|
+
text: string;
|
|
55
|
+
resources: ResourceRef[];
|
|
56
|
+
tools: ToolRef[];
|
|
57
|
+
model: string;
|
|
58
|
+
reasoningEffort: ReasoningEffort;
|
|
59
|
+
options: NewSessionDraftOptions;
|
|
60
|
+
/** API-key and delegated service subjects have no workspace-membership row. */
|
|
61
|
+
requireWorkspaceMembership?: boolean;
|
|
62
|
+
},
|
|
63
|
+
): Promise<NewSessionDraftRow> {
|
|
64
|
+
if (input.requireWorkspaceMembership !== false) {
|
|
65
|
+
// Serialize with removeWorkspaceMember(), which takes FOR UPDATE before it
|
|
66
|
+
// deletes private rows and the membership. A save that wins first commits
|
|
67
|
+
// before removal's cleanup; a removal that wins first leaves no membership
|
|
68
|
+
// for a stale, already-authorized request to recreate after re-invitation.
|
|
69
|
+
const [membership] = await db
|
|
70
|
+
.select({ id: schema.workspaceMemberships.id })
|
|
71
|
+
.from(schema.workspaceMemberships)
|
|
72
|
+
.where(
|
|
73
|
+
and(
|
|
74
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
75
|
+
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
.for("key share")
|
|
79
|
+
.limit(1);
|
|
80
|
+
if (!membership) throw new NewSessionDraftAccessError();
|
|
81
|
+
}
|
|
82
|
+
const current = await getNewSessionDraftInTransaction(db, { ...input, lock: true });
|
|
83
|
+
const currentRevision = current?.revision ?? 0;
|
|
84
|
+
if (currentRevision !== input.expectedRevision) {
|
|
85
|
+
throw new NewSessionDraftConflictError(currentRevision);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const revision = currentRevision + 1;
|
|
89
|
+
const values = {
|
|
90
|
+
accountId: input.accountId,
|
|
91
|
+
workspaceId: input.workspaceId,
|
|
92
|
+
subjectId: input.subjectId,
|
|
93
|
+
revision,
|
|
94
|
+
text: input.text,
|
|
95
|
+
resources: input.resources,
|
|
96
|
+
tools: input.tools,
|
|
97
|
+
model: input.model,
|
|
98
|
+
reasoningEffort: input.reasoningEffort,
|
|
99
|
+
sessionOptions: input.options,
|
|
100
|
+
updatedAt: new Date(),
|
|
101
|
+
};
|
|
102
|
+
if (current) {
|
|
103
|
+
const [saved] = await db
|
|
104
|
+
.update(schema.newSessionDrafts)
|
|
105
|
+
.set(values)
|
|
106
|
+
.where(eq(schema.newSessionDrafts.id, current.id))
|
|
107
|
+
.returning();
|
|
108
|
+
if (!saved) throw new Error("New-session draft did not save");
|
|
109
|
+
return saved;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// SELECT FOR UPDATE cannot lock an absent key. Two first saves may therefore
|
|
113
|
+
// race; ON CONFLICT keeps the loser transaction usable so it can report the
|
|
114
|
+
// winner's authoritative revision instead of leaking a unique violation.
|
|
115
|
+
const [inserted] = await db
|
|
116
|
+
.insert(schema.newSessionDrafts)
|
|
117
|
+
.values(values)
|
|
118
|
+
.onConflictDoNothing({
|
|
119
|
+
target: [schema.newSessionDrafts.workspaceId, schema.newSessionDrafts.subjectId],
|
|
120
|
+
})
|
|
121
|
+
.returning();
|
|
122
|
+
if (inserted) return inserted;
|
|
123
|
+
const raced = await getNewSessionDraftInTransaction(db, { ...input, lock: true });
|
|
124
|
+
throw new NewSessionDraftConflictError(raced?.revision ?? 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Delete only the submitted revision; a newer sibling-tab revision survives. */
|
|
128
|
+
export async function consumeNewSessionDraftInTransaction(
|
|
129
|
+
db: Database,
|
|
130
|
+
input: { workspaceId: string; subjectId: string; expectedRevision: number },
|
|
131
|
+
): Promise<boolean> {
|
|
132
|
+
if (input.expectedRevision === 0) return false;
|
|
133
|
+
const deleted = await db
|
|
134
|
+
.delete(schema.newSessionDrafts)
|
|
135
|
+
.where(
|
|
136
|
+
and(
|
|
137
|
+
eq(schema.newSessionDrafts.workspaceId, input.workspaceId),
|
|
138
|
+
eq(schema.newSessionDrafts.subjectId, input.subjectId),
|
|
139
|
+
eq(schema.newSessionDrafts.revision, input.expectedRevision),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
.returning({ id: schema.newSessionDrafts.id });
|
|
143
|
+
return deleted.length > 0;
|
|
144
|
+
}
|