@opengeni/db 0.2.2 → 0.3.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-PSX56ZTL.js → chunk-NZA6YVN7.js} +24 -1
- package/dist/chunk-NZA6YVN7.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +237 -13
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +68 -9
- package/dist/{schema-fwrPBw5T.d.ts → schema-BI0iqN9U.d.ts} +252 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -1
- package/drizzle/0035_session_mcp_servers.sql +50 -0
- package/package.json +3 -3
- package/src/event-payload-sanitizer.ts +58 -1
- package/src/index.ts +297 -17
- package/src/schema.ts +22 -0
- package/dist/chunk-PSX56ZTL.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
SessionGoal,
|
|
35
35
|
SessionGoalCreatedBy,
|
|
36
36
|
SessionGoalStatus,
|
|
37
|
+
SessionMcpServerMetadata,
|
|
37
38
|
SessionStatus,
|
|
38
39
|
SessionTurn,
|
|
39
40
|
SessionTurnSource,
|
|
@@ -348,6 +349,7 @@ export const allWorkspacePermissions: Permission[] = [
|
|
|
348
349
|
"api_keys:manage",
|
|
349
350
|
"environments:manage",
|
|
350
351
|
"environments:use",
|
|
352
|
+
"mcp_servers:attach",
|
|
351
353
|
"goals:manage",
|
|
352
354
|
"enrollments:read",
|
|
353
355
|
"enrollments:manage",
|
|
@@ -1200,6 +1202,33 @@ export type EnabledMcpCapabilityServer = {
|
|
|
1200
1202
|
headersEncrypted?: Record<string, string>;
|
|
1201
1203
|
};
|
|
1202
1204
|
|
|
1205
|
+
export type CreateSessionMcpServerInput = {
|
|
1206
|
+
id: string;
|
|
1207
|
+
name?: string | null;
|
|
1208
|
+
url: string;
|
|
1209
|
+
allowedTools?: string[] | null;
|
|
1210
|
+
timeoutMs?: number | null;
|
|
1211
|
+
cacheToolsList?: boolean | null;
|
|
1212
|
+
headersEncrypted?: Record<string, string>;
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
export type UpdateSessionMcpServerCredentialsInput = {
|
|
1216
|
+
id: string;
|
|
1217
|
+
headersEncrypted: Record<string, string>;
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
export type UpdateSessionMcpServerCredentialsResult = {
|
|
1221
|
+
servers: SessionMcpServerMetadata[];
|
|
1222
|
+
missingIds: string[];
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
export type SessionMcpServerForRun = SessionMcpServerMetadata & {
|
|
1226
|
+
allowedTools?: string[];
|
|
1227
|
+
timeoutMs?: number;
|
|
1228
|
+
cacheToolsList?: boolean;
|
|
1229
|
+
headers: Record<string, string>;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1203
1232
|
export type EnqueueSessionTurnInput = {
|
|
1204
1233
|
accountId: string;
|
|
1205
1234
|
workspaceId: string;
|
|
@@ -3085,6 +3114,155 @@ function mapWorkspaceEnvironmentVariableMetadata(row: {
|
|
|
3085
3114
|
};
|
|
3086
3115
|
}
|
|
3087
3116
|
|
|
3117
|
+
function mapSessionMcpServerMetadata(row: typeof schema.sessionMcpServers.$inferSelect): SessionMcpServerMetadata {
|
|
3118
|
+
return {
|
|
3119
|
+
id: row.serverId,
|
|
3120
|
+
name: row.name ?? null,
|
|
3121
|
+
url: row.url,
|
|
3122
|
+
headerNames: Object.keys(row.headersEncrypted ?? {}).sort(),
|
|
3123
|
+
credentialVersion: Number(row.credentialVersion),
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
async function sessionMcpServerMetadataForSessions(
|
|
3128
|
+
db: Database,
|
|
3129
|
+
workspaceId: string,
|
|
3130
|
+
sessionIds: string[],
|
|
3131
|
+
): Promise<Map<string, SessionMcpServerMetadata[]>> {
|
|
3132
|
+
const grouped = new Map<string, SessionMcpServerMetadata[]>();
|
|
3133
|
+
if (sessionIds.length === 0) {
|
|
3134
|
+
return grouped;
|
|
3135
|
+
}
|
|
3136
|
+
const rows = await db.select().from(schema.sessionMcpServers)
|
|
3137
|
+
.where(and(
|
|
3138
|
+
eq(schema.sessionMcpServers.workspaceId, workspaceId),
|
|
3139
|
+
inArray(schema.sessionMcpServers.sessionId, sessionIds),
|
|
3140
|
+
))
|
|
3141
|
+
.orderBy(asc(schema.sessionMcpServers.createdAt), asc(schema.sessionMcpServers.serverId));
|
|
3142
|
+
for (const row of rows) {
|
|
3143
|
+
const list = grouped.get(row.sessionId) ?? [];
|
|
3144
|
+
list.push(mapSessionMcpServerMetadata(row));
|
|
3145
|
+
grouped.set(row.sessionId, list);
|
|
3146
|
+
}
|
|
3147
|
+
return grouped;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
async function insertSessionMcpServers(db: Database, input: {
|
|
3151
|
+
accountId: string;
|
|
3152
|
+
workspaceId: string;
|
|
3153
|
+
sessionId: string;
|
|
3154
|
+
servers: CreateSessionMcpServerInput[];
|
|
3155
|
+
}): Promise<SessionMcpServerMetadata[]> {
|
|
3156
|
+
if (input.servers.length === 0) {
|
|
3157
|
+
return [];
|
|
3158
|
+
}
|
|
3159
|
+
const rows = await db.insert(schema.sessionMcpServers).values(input.servers.map((server) => ({
|
|
3160
|
+
accountId: input.accountId,
|
|
3161
|
+
workspaceId: input.workspaceId,
|
|
3162
|
+
sessionId: input.sessionId,
|
|
3163
|
+
serverId: server.id,
|
|
3164
|
+
name: server.name ?? null,
|
|
3165
|
+
url: server.url,
|
|
3166
|
+
allowedTools: server.allowedTools ?? null,
|
|
3167
|
+
timeoutMs: server.timeoutMs ?? null,
|
|
3168
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
3169
|
+
headersEncrypted: server.headersEncrypted ?? {},
|
|
3170
|
+
}))).returning();
|
|
3171
|
+
return rows.map(mapSessionMcpServerMetadata);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
export async function createSessionMcpServers(db: Database, input: {
|
|
3175
|
+
accountId: string;
|
|
3176
|
+
workspaceId: string;
|
|
3177
|
+
sessionId: string;
|
|
3178
|
+
servers: CreateSessionMcpServerInput[];
|
|
3179
|
+
}): Promise<SessionMcpServerMetadata[]> {
|
|
3180
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) =>
|
|
3181
|
+
await insertSessionMcpServers(scopedDb, input)
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
export async function listSessionMcpServerMetadata(db: Database, workspaceId: string, sessionId: string): Promise<SessionMcpServerMetadata[]> {
|
|
3186
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3187
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [sessionId]);
|
|
3188
|
+
return grouped.get(sessionId) ?? [];
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
export async function updateSessionMcpServerCredentials(db: Database, input: {
|
|
3193
|
+
workspaceId: string;
|
|
3194
|
+
sessionId: string;
|
|
3195
|
+
updates: UpdateSessionMcpServerCredentialsInput[];
|
|
3196
|
+
}): Promise<UpdateSessionMcpServerCredentialsResult> {
|
|
3197
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => await scopedDb.transaction(async (tx) =>
|
|
3198
|
+
await updateSessionMcpServerCredentialsInTransaction(tx, input)
|
|
3199
|
+
));
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
async function updateSessionMcpServerCredentialsInTransaction(
|
|
3203
|
+
tx: Pick<Database, "update">,
|
|
3204
|
+
input: {
|
|
3205
|
+
workspaceId: string;
|
|
3206
|
+
sessionId: string;
|
|
3207
|
+
updates: UpdateSessionMcpServerCredentialsInput[];
|
|
3208
|
+
},
|
|
3209
|
+
): Promise<UpdateSessionMcpServerCredentialsResult> {
|
|
3210
|
+
const servers: SessionMcpServerMetadata[] = [];
|
|
3211
|
+
const missingIds: string[] = [];
|
|
3212
|
+
for (const update of input.updates) {
|
|
3213
|
+
const [row] = await tx.update(schema.sessionMcpServers)
|
|
3214
|
+
.set({
|
|
3215
|
+
headersEncrypted: update.headersEncrypted,
|
|
3216
|
+
credentialVersion: sql`${schema.sessionMcpServers.credentialVersion} + 1`,
|
|
3217
|
+
updatedAt: new Date(),
|
|
3218
|
+
})
|
|
3219
|
+
.where(and(
|
|
3220
|
+
eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
|
|
3221
|
+
eq(schema.sessionMcpServers.sessionId, input.sessionId),
|
|
3222
|
+
eq(schema.sessionMcpServers.serverId, update.id),
|
|
3223
|
+
))
|
|
3224
|
+
.returning();
|
|
3225
|
+
if (!row) {
|
|
3226
|
+
missingIds.push(update.id);
|
|
3227
|
+
} else {
|
|
3228
|
+
servers.push(mapSessionMcpServerMetadata(row));
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return { servers, missingIds };
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
export async function listSessionMcpServersForRun(
|
|
3235
|
+
db: Database,
|
|
3236
|
+
workspaceId: string,
|
|
3237
|
+
sessionId: string,
|
|
3238
|
+
encryptionKey: Uint8Array,
|
|
3239
|
+
): Promise<SessionMcpServerForRun[]> {
|
|
3240
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3241
|
+
const rows = await scopedDb.select().from(schema.sessionMcpServers)
|
|
3242
|
+
.where(and(
|
|
3243
|
+
eq(schema.sessionMcpServers.workspaceId, workspaceId),
|
|
3244
|
+
eq(schema.sessionMcpServers.sessionId, sessionId),
|
|
3245
|
+
))
|
|
3246
|
+
.orderBy(asc(schema.sessionMcpServers.createdAt), asc(schema.sessionMcpServers.serverId));
|
|
3247
|
+
return rows.map((row) => {
|
|
3248
|
+
let headers: Record<string, string>;
|
|
3249
|
+
try {
|
|
3250
|
+
headers = Object.fromEntries(Object.entries(row.headersEncrypted ?? {})
|
|
3251
|
+
.map(([name, stored]) => [name, decryptEnvironmentValue(encryptionKey, stored)]));
|
|
3252
|
+
} catch {
|
|
3253
|
+
throw new Error("session MCP server credential decryption failed");
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
...mapSessionMcpServerMetadata(row),
|
|
3257
|
+
...(row.allowedTools ? { allowedTools: row.allowedTools } : {}),
|
|
3258
|
+
...(row.timeoutMs ? { timeoutMs: row.timeoutMs } : {}),
|
|
3259
|
+
...(row.cacheToolsList ? { cacheToolsList: row.cacheToolsList } : {}),
|
|
3260
|
+
headers,
|
|
3261
|
+
};
|
|
3262
|
+
});
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3088
3266
|
export async function createSession(db: Database, input: {
|
|
3089
3267
|
accountId: string;
|
|
3090
3268
|
workspaceId: string;
|
|
@@ -3103,6 +3281,7 @@ export async function createSession(db: Database, input: {
|
|
|
3103
3281
|
// shared spawn passes the parent's sandboxGroupId so both run in ONE box.
|
|
3104
3282
|
sandboxGroupId?: string | null;
|
|
3105
3283
|
sandboxOs?: SandboxOs;
|
|
3284
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
3106
3285
|
}): Promise<Session> {
|
|
3107
3286
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
3108
3287
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
@@ -3129,7 +3308,13 @@ export async function createSession(db: Database, input: {
|
|
|
3129
3308
|
if (!row) {
|
|
3130
3309
|
throw new Error("Failed to create session");
|
|
3131
3310
|
}
|
|
3132
|
-
|
|
3311
|
+
const mcpServers = await insertSessionMcpServers(scopedDb, {
|
|
3312
|
+
accountId: input.accountId,
|
|
3313
|
+
workspaceId: input.workspaceId,
|
|
3314
|
+
sessionId: row.id,
|
|
3315
|
+
servers: input.mcpServers ?? [],
|
|
3316
|
+
});
|
|
3317
|
+
return mapSession(row, mcpServers);
|
|
3133
3318
|
});
|
|
3134
3319
|
}
|
|
3135
3320
|
|
|
@@ -3160,6 +3345,7 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3160
3345
|
// (group === the new row's own id); a shared spawn passes the parent's group.
|
|
3161
3346
|
sandboxGroupId?: string | null;
|
|
3162
3347
|
sandboxOs?: SandboxOs;
|
|
3348
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
3163
3349
|
}): Promise<{ session: Session; created: boolean }> {
|
|
3164
3350
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
3165
3351
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
@@ -3187,7 +3373,13 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3187
3373
|
where: sql`${schema.sessions.createIdempotencyKey} is not null`,
|
|
3188
3374
|
}).returning();
|
|
3189
3375
|
if (inserted) {
|
|
3190
|
-
|
|
3376
|
+
const mcpServers = await insertSessionMcpServers(scopedDb, {
|
|
3377
|
+
accountId: input.accountId,
|
|
3378
|
+
workspaceId: input.workspaceId,
|
|
3379
|
+
sessionId: inserted.id,
|
|
3380
|
+
servers: input.mcpServers ?? [],
|
|
3381
|
+
});
|
|
3382
|
+
return { session: mapSession(inserted, mcpServers), created: true };
|
|
3191
3383
|
}
|
|
3192
3384
|
const [existing] = await scopedDb.select().from(schema.sessions).where(and(
|
|
3193
3385
|
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
@@ -3199,7 +3391,8 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3199
3391
|
// than silently returning a phantom.
|
|
3200
3392
|
throw new Error("Failed to create session under idempotency key");
|
|
3201
3393
|
}
|
|
3202
|
-
|
|
3394
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, input.workspaceId, [existing.id]);
|
|
3395
|
+
return { session: mapSession(existing, grouped.get(existing.id) ?? []), created: false };
|
|
3203
3396
|
});
|
|
3204
3397
|
}
|
|
3205
3398
|
|
|
@@ -3209,14 +3402,18 @@ export async function getSessionByCreateIdempotencyKey(db: Database, workspaceId
|
|
|
3209
3402
|
eq(schema.sessions.workspaceId, workspaceId),
|
|
3210
3403
|
eq(schema.sessions.createIdempotencyKey, createIdempotencyKey),
|
|
3211
3404
|
)).limit(1);
|
|
3212
|
-
|
|
3405
|
+
if (!row) return null;
|
|
3406
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [row.id]);
|
|
3407
|
+
return mapSession(row, grouped.get(row.id) ?? []);
|
|
3213
3408
|
});
|
|
3214
3409
|
}
|
|
3215
3410
|
|
|
3216
3411
|
export async function getSession(db: Database, workspaceId: string, sessionId: string): Promise<Session | null> {
|
|
3217
3412
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3218
3413
|
const [row] = await scopedDb.select().from(schema.sessions).where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId))).limit(1);
|
|
3219
|
-
|
|
3414
|
+
if (!row) return null;
|
|
3415
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [row.id]);
|
|
3416
|
+
return mapSession(row, grouped.get(row.id) ?? []);
|
|
3220
3417
|
});
|
|
3221
3418
|
}
|
|
3222
3419
|
|
|
@@ -3262,7 +3459,8 @@ export async function listSessions(db: Database, workspaceId: string, limit = 50
|
|
|
3262
3459
|
.where(eq(schema.sessions.workspaceId, workspaceId))
|
|
3263
3460
|
.orderBy(desc(schema.sessions.createdAt), desc(schema.sessions.id))
|
|
3264
3461
|
.limit(limit);
|
|
3265
|
-
|
|
3462
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, rows.map((row) => row.id));
|
|
3463
|
+
return rows.map((row) => mapSession(row, grouped.get(row.id) ?? []));
|
|
3266
3464
|
});
|
|
3267
3465
|
}
|
|
3268
3466
|
|
|
@@ -3294,16 +3492,63 @@ export async function requireSession(db: Database, workspaceId: string, sessionI
|
|
|
3294
3492
|
return session;
|
|
3295
3493
|
}
|
|
3296
3494
|
|
|
3297
|
-
export
|
|
3495
|
+
export type ListSessionEventsOptions = {
|
|
3496
|
+
after?: number;
|
|
3497
|
+
before?: number;
|
|
3498
|
+
limit?: number;
|
|
3499
|
+
};
|
|
3500
|
+
|
|
3501
|
+
const POSTGRES_INT_MAX = 2_147_483_647;
|
|
3502
|
+
|
|
3503
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string): Promise<SessionEvent[]>;
|
|
3504
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string, after: number, limit?: number): Promise<SessionEvent[]>;
|
|
3505
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string, options: ListSessionEventsOptions): Promise<SessionEvent[]>;
|
|
3506
|
+
export async function listSessionEvents(
|
|
3507
|
+
db: Database,
|
|
3508
|
+
workspaceId: string,
|
|
3509
|
+
sessionId: string,
|
|
3510
|
+
afterOrOptions: number | ListSessionEventsOptions = 0,
|
|
3511
|
+
legacyLimit = 500,
|
|
3512
|
+
): Promise<SessionEvent[]> {
|
|
3513
|
+
const options = typeof afterOrOptions === "number"
|
|
3514
|
+
? { after: afterOrOptions, limit: legacyLimit }
|
|
3515
|
+
: afterOrOptions;
|
|
3516
|
+
const after = normalizeEventSequence(options.after, 0);
|
|
3517
|
+
const limit = normalizeEventLimit(options.limit, 500);
|
|
3518
|
+
const hasBefore = options.before !== undefined && Number.isFinite(options.before);
|
|
3519
|
+
const before = hasBefore ? Math.floor(options.before as number) : undefined;
|
|
3520
|
+
|
|
3298
3521
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3522
|
+
const filters: SQL[] = [
|
|
3523
|
+
eq(schema.sessionEvents.workspaceId, workspaceId),
|
|
3524
|
+
eq(schema.sessionEvents.sessionId, sessionId),
|
|
3525
|
+
gt(schema.sessionEvents.sequence, after),
|
|
3526
|
+
];
|
|
3527
|
+
if (before !== undefined && before <= POSTGRES_INT_MAX) {
|
|
3528
|
+
filters.push(lt(schema.sessionEvents.sequence, before));
|
|
3529
|
+
}
|
|
3299
3530
|
const rows = await scopedDb.select().from(schema.sessionEvents)
|
|
3300
|
-
.where(and(
|
|
3301
|
-
.orderBy(asc(schema.sessionEvents.sequence))
|
|
3531
|
+
.where(and(...filters))
|
|
3532
|
+
.orderBy(hasBefore ? desc(schema.sessionEvents.sequence) : asc(schema.sessionEvents.sequence))
|
|
3302
3533
|
.limit(limit);
|
|
3303
|
-
return rows.map(mapEvent);
|
|
3534
|
+
return (hasBefore ? rows.reverse() : rows).map(mapEvent);
|
|
3304
3535
|
});
|
|
3305
3536
|
}
|
|
3306
3537
|
|
|
3538
|
+
function normalizeEventSequence(value: number | undefined, fallback: number): number {
|
|
3539
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
3540
|
+
return fallback;
|
|
3541
|
+
}
|
|
3542
|
+
return Math.floor(value);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
function normalizeEventLimit(value: number | undefined, fallback: number): number {
|
|
3546
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
3547
|
+
return fallback;
|
|
3548
|
+
}
|
|
3549
|
+
return Math.max(0, Math.floor(value));
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3307
3552
|
export async function getSessionEvent(db: Database, workspaceId: string, eventId: string): Promise<SessionEvent | null> {
|
|
3308
3553
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3309
3554
|
const [row] = await scopedDb.select().from(schema.sessionEvents).where(and(eq(schema.sessionEvents.workspaceId, workspaceId), eq(schema.sessionEvents.id, eventId))).limit(1);
|
|
@@ -3384,8 +3629,8 @@ export async function getSessionHistoryItems(db: Database, workspaceId: string,
|
|
|
3384
3629
|
|
|
3385
3630
|
/**
|
|
3386
3631
|
* The LIVE conversation-truth read path: only active rows, position-ordered.
|
|
3387
|
-
* After a client-side context compaction this returns [
|
|
3388
|
-
*
|
|
3632
|
+
* After a client-side context compaction this returns [retained user messages,
|
|
3633
|
+
* active summary]; with no compaction yet it equals
|
|
3389
3634
|
* getSessionHistoryItems. The model-facing read path uses this so superseded
|
|
3390
3635
|
* (summarized-away) prefix rows are excluded while the full transcript stays in
|
|
3391
3636
|
* the table as an audit trail.
|
|
@@ -3555,8 +3800,15 @@ export async function applyContextCompaction(db: Database, input: {
|
|
|
3555
3800
|
turnId?: string | null;
|
|
3556
3801
|
/** Active prefix rows with position < boundaryPosition get superseded. */
|
|
3557
3802
|
boundaryPosition: number;
|
|
3558
|
-
/**
|
|
3803
|
+
/** Position for the new summary row. Old boundary mode uses a fractional half-step before the kept tail. */
|
|
3559
3804
|
summaryPosition: number;
|
|
3805
|
+
/**
|
|
3806
|
+
* Optional replacement rows inserted after superseding the old active set.
|
|
3807
|
+
* Used by Codex-parity client compaction to rebuild active history as retained
|
|
3808
|
+
* user messages plus one summary. These rows are synthetic replay rows, so
|
|
3809
|
+
* they intentionally do not inherit the current compaction turn id.
|
|
3810
|
+
*/
|
|
3811
|
+
replacementItems?: Array<{ position: number; item: Record<string, unknown> }>;
|
|
3560
3812
|
summaryItem: Record<string, unknown>;
|
|
3561
3813
|
}): Promise<void> {
|
|
3562
3814
|
await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
@@ -3569,6 +3821,20 @@ export async function applyContextCompaction(db: Database, input: {
|
|
|
3569
3821
|
eq(schema.sessionHistoryItems.active, true),
|
|
3570
3822
|
lt(schema.sessionHistoryItems.position, input.boundaryPosition),
|
|
3571
3823
|
));
|
|
3824
|
+
if (input.replacementItems && input.replacementItems.length > 0) {
|
|
3825
|
+
await tx.insert(schema.sessionHistoryItems).values(input.replacementItems.map((entry) => ({
|
|
3826
|
+
accountId: input.accountId,
|
|
3827
|
+
workspaceId: input.workspaceId,
|
|
3828
|
+
sessionId: input.sessionId,
|
|
3829
|
+
turnId: null,
|
|
3830
|
+
position: entry.position,
|
|
3831
|
+
item: sanitizeEventPayload(entry.item),
|
|
3832
|
+
active: true,
|
|
3833
|
+
}))).onConflictDoUpdate({
|
|
3834
|
+
target: [schema.sessionHistoryItems.workspaceId, schema.sessionHistoryItems.sessionId, schema.sessionHistoryItems.position],
|
|
3835
|
+
set: { active: true },
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3572
3838
|
// Insert the summary at its FRACTIONAL position. The supersede step above
|
|
3573
3839
|
// also sets active=false for any rows with position < boundaryPosition —
|
|
3574
3840
|
// which on a RETRY includes the summary itself (it sits below the
|
|
@@ -7275,7 +7541,11 @@ export async function appendSessionEventsAndUpdateSession(db: Database, workspac
|
|
|
7275
7541
|
}));
|
|
7276
7542
|
}
|
|
7277
7543
|
|
|
7278
|
-
|
|
7544
|
+
type LockedSessionUpdateContext = {
|
|
7545
|
+
updateSessionMcpServerCredentials: (updates: UpdateSessionMcpServerCredentialsInput[]) => Promise<UpdateSessionMcpServerCredentialsResult>;
|
|
7546
|
+
};
|
|
7547
|
+
|
|
7548
|
+
type LockedSessionUpdateResult = {
|
|
7279
7549
|
events: AppendEventInput[];
|
|
7280
7550
|
update?: {
|
|
7281
7551
|
resources?: ResourceRef[];
|
|
@@ -7285,13 +7555,22 @@ export async function appendSessionEventsWithLockedSessionUpdate(db: Database, w
|
|
|
7285
7555
|
status?: SessionStatus;
|
|
7286
7556
|
activeTurnId?: string | null;
|
|
7287
7557
|
};
|
|
7288
|
-
}
|
|
7558
|
+
};
|
|
7559
|
+
|
|
7560
|
+
export async function appendSessionEventsWithLockedSessionUpdate(
|
|
7561
|
+
db: Database,
|
|
7562
|
+
workspaceId: string,
|
|
7563
|
+
sessionId: string,
|
|
7564
|
+
build: (session: Session, context: LockedSessionUpdateContext) => LockedSessionUpdateResult | Promise<LockedSessionUpdateResult>,
|
|
7565
|
+
): Promise<SessionEvent[]> {
|
|
7289
7566
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => await scopedDb.transaction(async (tx) => {
|
|
7290
7567
|
const [sessionRow] = await tx.select().from(schema.sessions).where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId))).for("update").limit(1);
|
|
7291
7568
|
if (!sessionRow) {
|
|
7292
7569
|
throw new Error(`Session not found: ${sessionId}`);
|
|
7293
7570
|
}
|
|
7294
|
-
const built = build(mapSession(sessionRow)
|
|
7571
|
+
const built = await build(mapSession(sessionRow), {
|
|
7572
|
+
updateSessionMcpServerCredentials: async (updates) => await updateSessionMcpServerCredentialsInTransaction(tx, { workspaceId, sessionId, updates }),
|
|
7573
|
+
});
|
|
7295
7574
|
if (built.events.length === 0) {
|
|
7296
7575
|
return [];
|
|
7297
7576
|
}
|
|
@@ -7330,7 +7609,7 @@ export function sessionSubject(workspaceId: string, sessionId: string): string {
|
|
|
7330
7609
|
return `workspaces.${workspaceId}.sessions.${sessionId}.events`;
|
|
7331
7610
|
}
|
|
7332
7611
|
|
|
7333
|
-
function mapSession(row: typeof schema.sessions.$inferSelect): Session {
|
|
7612
|
+
function mapSession(row: typeof schema.sessions.$inferSelect, mcpServers: SessionMcpServerMetadata[] = []): Session {
|
|
7334
7613
|
return {
|
|
7335
7614
|
id: row.id,
|
|
7336
7615
|
accountId: row.accountId,
|
|
@@ -7353,6 +7632,7 @@ function mapSession(row: typeof schema.sessions.$inferSelect): Session {
|
|
|
7353
7632
|
activeEpoch: Number(row.activeEpoch),
|
|
7354
7633
|
environmentId: row.environmentId,
|
|
7355
7634
|
firstPartyMcpPermissions: (row.firstPartyMcpPermissions as Permission[] | null) ?? null,
|
|
7635
|
+
mcpServers,
|
|
7356
7636
|
parentSessionId: row.parentSessionId ?? null,
|
|
7357
7637
|
createIdempotencyKey: row.createIdempotencyKey ?? null,
|
|
7358
7638
|
temporalWorkflowId: row.temporalWorkflowId,
|
package/src/schema.ts
CHANGED
|
@@ -283,6 +283,28 @@ export const sessions = pgTable("sessions", {
|
|
|
283
283
|
createIdempotency: uniqueIndex("sessions_workspace_create_idempotency_idx").on(table.workspaceId, table.createIdempotencyKey).where(sql`${table.createIdempotencyKey} is not null`),
|
|
284
284
|
}));
|
|
285
285
|
|
|
286
|
+
export const sessionMcpServers = pgTable("session_mcp_servers", {
|
|
287
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
288
|
+
accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
|
|
289
|
+
workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
|
|
290
|
+
sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
291
|
+
serverId: text("server_id").notNull(),
|
|
292
|
+
name: text("name"),
|
|
293
|
+
url: text("url").notNull(),
|
|
294
|
+
allowedTools: jsonb("allowed_tools").$type<string[]>(),
|
|
295
|
+
timeoutMs: integer("timeout_ms"),
|
|
296
|
+
cacheToolsList: boolean("cache_tools_list").notNull().default(false),
|
|
297
|
+
// Map of header name -> AES-GCM ciphertext. Values are decrypted only by the
|
|
298
|
+
// worker's run-preparation path and never returned by API helpers.
|
|
299
|
+
headersEncrypted: jsonb("headers_encrypted").$type<Record<string, string>>().notNull().default({}),
|
|
300
|
+
credentialVersion: integer("credential_version").notNull().default(1),
|
|
301
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
302
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
303
|
+
}, (table) => ({
|
|
304
|
+
sessionServer: uniqueIndex("session_mcp_servers_session_server_idx").on(table.workspaceId, table.sessionId, table.serverId),
|
|
305
|
+
session: index("session_mcp_servers_session_idx").on(table.workspaceId, table.sessionId),
|
|
306
|
+
}));
|
|
307
|
+
|
|
286
308
|
export const files = pgTable("files", {
|
|
287
309
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
288
310
|
accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
|