@opengeni/db 3.2.0-canary.0 → 3.4.0-canary.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/canonical-human-identities.js +3 -3
- package/dist/{chunk-SIBYBA2D.js → chunk-6DPJA36C.js} +30 -11
- package/dist/chunk-6DPJA36C.js.map +1 -0
- package/dist/{chunk-KRLPEMLH.js → chunk-C2AFP7SB.js} +184 -6
- package/dist/chunk-C2AFP7SB.js.map +1 -0
- package/dist/{chunk-AZHW7R5V.js → chunk-H7MXYG7R.js} +12 -8
- package/dist/chunk-H7MXYG7R.js.map +1 -0
- package/dist/{chunk-2PRS4J5Q.js → chunk-HY3F5Q4E.js} +3 -3
- package/dist/{chunk-NAB6FKFN.js → chunk-O6FTELKH.js} +3 -3
- package/dist/{chunk-UZNXG44K.js → chunk-PYMY3AJW.js} +324 -8
- package/dist/chunk-PYMY3AJW.js.map +1 -0
- package/dist/{chunk-6HMM2R5T.js → chunk-TRBOWUG3.js} +98 -4
- package/dist/chunk-TRBOWUG3.js.map +1 -0
- package/dist/{chunk-E6CLMKEE.js → chunk-V3TRUXFJ.js} +2 -2
- package/dist/{chunk-2EH4WRHL.js → chunk-ZWRIPCTR.js} +2 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation.d.ts +16 -0
- package/dist/index.d.ts +108 -7
- package/dist/index.js +3605 -1417
- package/dist/index.js.map +1 -1
- package/dist/insights-model-bundle.d.ts +30 -0
- package/dist/insights-usage-bundle.d.ts +32 -0
- package/dist/insights.d.ts +10 -2
- package/dist/lossless-json.d.ts +1 -1
- package/dist/managed-user-setup.d.ts +82 -0
- package/dist/memory-domain.d.ts +2 -2
- package/dist/migrate.d.ts +3 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +4 -4
- package/dist/organization-membership-lifecycle.d.ts +60 -1
- package/dist/pr-review.d.ts +44 -3
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +12 -7
- package/dist/schema.d.ts +839 -101
- package/dist/schema.js +9 -1
- package/dist/session-background-commands.js +3 -3
- package/dist/session-tenancy.d.ts +12 -1
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/drizzle/0348_named_signup_and_user_setup.sql +838 -0
- package/drizzle/0349_greenfield_session_tenancy_activation.sql +792 -0
- package/drizzle/0350_organization_shared_workspace_administration.sql +889 -0
- package/drizzle/0351_organization_user_setup_delivery.sql +978 -0
- package/drizzle/0352_session_variable_set_attachments.sql +1661 -0
- package/drizzle/0353_automatic_session_title_policy_fence.sql +476 -0
- package/drizzle/0354_automatic_session_title_quarantine_index.sql +9 -0
- package/drizzle/0355_automatic_session_title_quarantine.sql +92 -0
- package/drizzle/0356_set_based_insights_session_visibility.sql +237 -0
- package/drizzle/0357_rig_platform_base_only.sql +44 -0
- package/drizzle/0358_pr_review_managed_github_app.sql +108 -0
- package/drizzle/0359_insights_force_rls_read_capability.sql +936 -0
- package/drizzle/0360_organization_identity_confirmation_prompt.sql +103 -0
- package/drizzle/0361_remember_knowledge_memory_materialization.sql +249 -0
- package/package.json +6 -6
- package/src/database.ts +12 -8
- package/src/governed-learning-activation.ts +74 -2
- package/src/index.ts +879 -92
- package/src/insights-model-bundle.ts +681 -0
- package/src/insights-usage-bundle.ts +240 -0
- package/src/insights.ts +265 -152
- package/src/lossless-json.ts +2 -1
- package/src/managed-user-setup.ts +316 -0
- package/src/memory-domain.ts +3 -3
- package/src/migrate.ts +38 -14
- package/src/new-session-drafts.ts +22 -15
- package/src/organization-membership-lifecycle.ts +253 -1
- package/src/pr-review.ts +449 -6
- package/src/provision-roles.ts +92 -0
- package/src/runtime-posture.ts +247 -4
- package/src/schema.ts +210 -4
- package/src/session-tenancy.ts +120 -2
- package/dist/chunk-6HMM2R5T.js.map +0 -1
- package/dist/chunk-AZHW7R5V.js.map +0 -1
- package/dist/chunk-KRLPEMLH.js.map +0 -1
- package/dist/chunk-SIBYBA2D.js.map +0 -1
- package/dist/chunk-UZNXG44K.js.map +0 -1
- /package/dist/{chunk-2PRS4J5Q.js.map → chunk-HY3F5Q4E.js.map} +0 -0
- /package/dist/{chunk-NAB6FKFN.js.map → chunk-O6FTELKH.js.map} +0 -0
- /package/dist/{chunk-E6CLMKEE.js.map → chunk-V3TRUXFJ.js.map} +0 -0
- /package/dist/{chunk-2EH4WRHL.js.map → chunk-ZWRIPCTR.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
OrganizationMember,
|
|
19
19
|
WorkspaceMember as WorkspaceMemberContract,
|
|
20
20
|
SessionGoalSnapshot,
|
|
21
|
+
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
21
22
|
ScheduledTaskRunAcceptedExecution,
|
|
22
23
|
SessionGoalRootConstraintsWrite,
|
|
23
24
|
normalizeSessionGoalRootConstraints,
|
|
25
|
+
normalizeAutomaticSessionTitle,
|
|
24
26
|
sessionVisibilityToPublic,
|
|
25
27
|
sessionGoalUtf8Bytes,
|
|
26
28
|
} from "@opengeni/contracts";
|
|
@@ -425,6 +427,7 @@ export * from "./company-brain-context-selection";
|
|
|
425
427
|
export * from "./knowledge-source-sync";
|
|
426
428
|
export * from "./task-notes";
|
|
427
429
|
export * from "./managed-human-provisioning";
|
|
430
|
+
export * from "./managed-user-setup";
|
|
428
431
|
export * from "./organization-membership-backfill";
|
|
429
432
|
export * from "./connection-tenancy-backfill";
|
|
430
433
|
export * from "./generated-images";
|
|
@@ -451,6 +454,8 @@ export * from "./capability-integrations";
|
|
|
451
454
|
export * from "./integration-bindings";
|
|
452
455
|
export * from "./integration-facets";
|
|
453
456
|
export * from "./insights";
|
|
457
|
+
export * from "./insights-model-bundle";
|
|
458
|
+
export * from "./insights-usage-bundle";
|
|
454
459
|
export * from "./organization-membership-lifecycle";
|
|
455
460
|
import { assertActiveManagedHumanOrganizationMembership } from "./organization-membership-lifecycle";
|
|
456
461
|
// Deliberately NOT `export *`: `accountIdInRlsScope` is an internal convenience
|
|
@@ -1502,6 +1507,7 @@ export async function ensureManagedAccessForUser(
|
|
|
1502
1507
|
email: string;
|
|
1503
1508
|
name: string;
|
|
1504
1509
|
emailVerified?: boolean;
|
|
1510
|
+
provisionFallbackOrganization?: boolean;
|
|
1505
1511
|
},
|
|
1506
1512
|
): Promise<AccessContext> {
|
|
1507
1513
|
return (await ensureManagedAccessForUserWithOrganizationMemberships(db, input)).accessContext;
|
|
@@ -1646,6 +1652,7 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1646
1652
|
email: string;
|
|
1647
1653
|
name: string;
|
|
1648
1654
|
emailVerified?: boolean;
|
|
1655
|
+
provisionFallbackOrganization?: boolean;
|
|
1649
1656
|
},
|
|
1650
1657
|
): Promise<ManagedAccessProvisioningResult> {
|
|
1651
1658
|
const subjectId = `user:${input.userId}`;
|
|
@@ -1710,18 +1717,6 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1710
1717
|
(membership) => membership.status === "suspended" || membership.status === "revoked",
|
|
1711
1718
|
)
|
|
1712
1719
|
) {
|
|
1713
|
-
if (
|
|
1714
|
-
fallbackAccount &&
|
|
1715
|
-
existingOrganizationMemberships.some(
|
|
1716
|
-
(membership) => membership.organizationId === fallbackAccount.id,
|
|
1717
|
-
)
|
|
1718
|
-
) {
|
|
1719
|
-
return await projectManagedOrganizationAccess(txDb, {
|
|
1720
|
-
subjectId,
|
|
1721
|
-
subjectLabel,
|
|
1722
|
-
organizationMemberships: existingOrganizationMemberships,
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
1720
|
return {
|
|
1726
1721
|
accessContext: {
|
|
1727
1722
|
mode: "managed",
|
|
@@ -1755,6 +1750,20 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1755
1750
|
organizationMemberships: [],
|
|
1756
1751
|
};
|
|
1757
1752
|
}
|
|
1753
|
+
if (input.provisionFallbackOrganization === false) {
|
|
1754
|
+
return {
|
|
1755
|
+
accessContext: {
|
|
1756
|
+
mode: "managed",
|
|
1757
|
+
subjectId,
|
|
1758
|
+
subjectLabel,
|
|
1759
|
+
accountGrants: [],
|
|
1760
|
+
workspaceGrants: [],
|
|
1761
|
+
defaultAccountId: null,
|
|
1762
|
+
defaultWorkspaceId: null,
|
|
1763
|
+
},
|
|
1764
|
+
organizationMemberships: [],
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1758
1767
|
const accountName = input.name || input.email;
|
|
1759
1768
|
let [account] = await tx
|
|
1760
1769
|
.select()
|
|
@@ -1776,7 +1785,9 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1776
1785
|
})
|
|
1777
1786
|
.onConflictDoUpdate({
|
|
1778
1787
|
target: [schema.managedAccounts.externalSource, schema.managedAccounts.externalId],
|
|
1779
|
-
|
|
1788
|
+
// The insert race may converge here, but later access refreshes must
|
|
1789
|
+
// never turn one-shot signup intent into an organization rename.
|
|
1790
|
+
set: { updatedAt: new Date() },
|
|
1780
1791
|
})
|
|
1781
1792
|
.returning();
|
|
1782
1793
|
}
|
|
@@ -1810,7 +1821,8 @@ export async function ensureManagedAccessForUserWithOrganizationMemberships(
|
|
|
1810
1821
|
})
|
|
1811
1822
|
.onConflictDoUpdate({
|
|
1812
1823
|
target: [schema.workspaces.externalSource, schema.workspaces.externalId],
|
|
1813
|
-
|
|
1824
|
+
// Preserve a later administrator rename on legacy fallback workspaces.
|
|
1825
|
+
set: { updatedAt: new Date() },
|
|
1814
1826
|
})
|
|
1815
1827
|
.returning();
|
|
1816
1828
|
}
|
|
@@ -2050,7 +2062,13 @@ export async function getWorkspace(db: Database, workspaceId: string): Promise<W
|
|
|
2050
2062
|
.from(schema.workspaces)
|
|
2051
2063
|
.where(eq(schema.workspaces.id, workspaceId))
|
|
2052
2064
|
.limit(1);
|
|
2053
|
-
return row
|
|
2065
|
+
return row
|
|
2066
|
+
? mapWorkspace(
|
|
2067
|
+
row,
|
|
2068
|
+
await workspaceControlProjection(db, row.id),
|
|
2069
|
+
await workspaceKindProjection(db, row),
|
|
2070
|
+
)
|
|
2071
|
+
: null;
|
|
2054
2072
|
}
|
|
2055
2073
|
|
|
2056
2074
|
export async function getManagedAccount(
|
|
@@ -2087,7 +2105,11 @@ export async function listWorkspacesForSubject(
|
|
|
2087
2105
|
.limit(limit);
|
|
2088
2106
|
return await Promise.all(
|
|
2089
2107
|
rows.map(async (row) =>
|
|
2090
|
-
mapWorkspace(
|
|
2108
|
+
mapWorkspace(
|
|
2109
|
+
row.workspace,
|
|
2110
|
+
await workspaceControlProjection(db, row.workspace.id),
|
|
2111
|
+
await workspaceKindProjection(db, row.workspace),
|
|
2112
|
+
),
|
|
2091
2113
|
),
|
|
2092
2114
|
);
|
|
2093
2115
|
}
|
|
@@ -2136,13 +2158,17 @@ export async function createWorkspace(
|
|
|
2136
2158
|
workspaceId: row.id,
|
|
2137
2159
|
accountId: row.accountId,
|
|
2138
2160
|
});
|
|
2139
|
-
return mapWorkspace(
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2161
|
+
return mapWorkspace(
|
|
2162
|
+
row,
|
|
2163
|
+
{
|
|
2164
|
+
state: "active",
|
|
2165
|
+
revision: 0,
|
|
2166
|
+
reason: null,
|
|
2167
|
+
changedBy: null,
|
|
2168
|
+
changedAt: null,
|
|
2169
|
+
},
|
|
2170
|
+
"shared",
|
|
2171
|
+
);
|
|
2146
2172
|
});
|
|
2147
2173
|
}
|
|
2148
2174
|
|
|
@@ -2356,7 +2382,11 @@ export async function updateWorkspace(
|
|
|
2356
2382
|
if (!row) {
|
|
2357
2383
|
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
2358
2384
|
}
|
|
2359
|
-
return mapWorkspace(
|
|
2385
|
+
return mapWorkspace(
|
|
2386
|
+
row,
|
|
2387
|
+
await workspaceControlProjection(db, workspaceId),
|
|
2388
|
+
await workspaceKindProjection(db, row),
|
|
2389
|
+
);
|
|
2360
2390
|
}
|
|
2361
2391
|
|
|
2362
2392
|
// Deep-merge (top-level) a settings patch into workspaces.settings, atomically.
|
|
@@ -2417,6 +2447,7 @@ export async function updateWorkspaceSettings(
|
|
|
2417
2447
|
return mapWorkspace(
|
|
2418
2448
|
row,
|
|
2419
2449
|
await workspaceControlProjection(tx as unknown as Database, workspaceId),
|
|
2450
|
+
await workspaceKindProjection(tx as unknown as Database, row),
|
|
2420
2451
|
);
|
|
2421
2452
|
}),
|
|
2422
2453
|
);
|
|
@@ -2432,7 +2463,11 @@ export async function updateWorkspaceSettings(
|
|
|
2432
2463
|
if (!row) {
|
|
2433
2464
|
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
2434
2465
|
}
|
|
2435
|
-
return mapWorkspace(
|
|
2466
|
+
return mapWorkspace(
|
|
2467
|
+
row,
|
|
2468
|
+
await workspaceControlProjection(db, workspaceId),
|
|
2469
|
+
await workspaceKindProjection(db, row),
|
|
2470
|
+
);
|
|
2436
2471
|
}
|
|
2437
2472
|
|
|
2438
2473
|
export async function setWorkspaceDefaultRig(
|
|
@@ -2451,7 +2486,11 @@ export async function setWorkspaceDefaultRig(
|
|
|
2451
2486
|
if (!row) {
|
|
2452
2487
|
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
2453
2488
|
}
|
|
2454
|
-
return mapWorkspace(
|
|
2489
|
+
return mapWorkspace(
|
|
2490
|
+
row,
|
|
2491
|
+
await workspaceControlProjection(db, workspaceId),
|
|
2492
|
+
await workspaceKindProjection(db, row),
|
|
2493
|
+
);
|
|
2455
2494
|
}
|
|
2456
2495
|
|
|
2457
2496
|
export async function getWorkspaceGrant(
|
|
@@ -15899,17 +15938,31 @@ async function validateScheduledTargetExecutionAtClaim(
|
|
|
15899
15938
|
),
|
|
15900
15939
|
)
|
|
15901
15940
|
.orderBy(asc(schema.sessionMcpServers.serverId));
|
|
15902
|
-
const
|
|
15903
|
-
|
|
15941
|
+
const targetVariableSets =
|
|
15942
|
+
target.variableSets.length > 0
|
|
15943
|
+
? target.variableSets
|
|
15944
|
+
: target.variableSetId && target.variableSetGeneration
|
|
15945
|
+
? [
|
|
15946
|
+
{
|
|
15947
|
+
id: target.variableSetId,
|
|
15948
|
+
generation: target.variableSetGeneration,
|
|
15949
|
+
},
|
|
15950
|
+
]
|
|
15951
|
+
: [];
|
|
15952
|
+
const liveVariableSets = await Promise.all(
|
|
15953
|
+
targetVariableSets.map(async (selected) => {
|
|
15954
|
+
const variableSet = await getVariableSet(
|
|
15904
15955
|
tx,
|
|
15905
15956
|
{
|
|
15906
15957
|
accountId: input.accountId,
|
|
15907
15958
|
workspaceId: input.workspaceId,
|
|
15908
15959
|
subjectId: accepted.personalResourceAuthoritySubjectId ?? "scheduled-task",
|
|
15909
15960
|
},
|
|
15910
|
-
|
|
15911
|
-
)
|
|
15912
|
-
|
|
15961
|
+
selected.id,
|
|
15962
|
+
);
|
|
15963
|
+
return variableSet ? { id: variableSet.id, generation: variableSet.generation } : null;
|
|
15964
|
+
}),
|
|
15965
|
+
);
|
|
15913
15966
|
const [rigVersion] =
|
|
15914
15967
|
target.rigId && target.rigVersionId
|
|
15915
15968
|
? await tx
|
|
@@ -15950,8 +16003,10 @@ async function validateScheduledTargetExecutionAtClaim(
|
|
|
15950
16003
|
stableJson(target.toolPolicy) !== stableJson(input.session.toolPolicy) ||
|
|
15951
16004
|
stableJson(target.mcpServerIds) !== stableJson(mcpServers.map((server) => server.id)) ||
|
|
15952
16005
|
target.toolPolicyVersion !== input.session.toolPolicyVersion ||
|
|
15953
|
-
|
|
15954
|
-
|
|
16006
|
+
stableJson(targetVariableSets) !==
|
|
16007
|
+
stableJson(liveVariableSets.filter((value) => value !== null)) ||
|
|
16008
|
+
stableJson(targetVariableSets.map((variableSet) => variableSet.id)) !==
|
|
16009
|
+
stableJson((input.session.variableSetIds as string[]) ?? []) ||
|
|
15955
16010
|
target.rigId !== (input.session.rigId ?? null) ||
|
|
15956
16011
|
target.rigVersionId !== (input.session.rigVersionId ?? null) ||
|
|
15957
16012
|
stableJson(target.rigDefaultVariableSets) !==
|
|
@@ -16339,7 +16394,7 @@ export class VariableSetAttachedError extends Error {
|
|
|
16339
16394
|
|
|
16340
16395
|
constructor(cause?: unknown) {
|
|
16341
16396
|
super(
|
|
16342
|
-
"variable set remains attached to a scheduled task or
|
|
16397
|
+
"variable set remains attached to a scheduled task or session",
|
|
16343
16398
|
cause === undefined ? undefined : { cause },
|
|
16344
16399
|
);
|
|
16345
16400
|
}
|
|
@@ -16422,8 +16477,14 @@ export async function countActiveSessionsUsingVariableSet(
|
|
|
16422
16477
|
.where(
|
|
16423
16478
|
and(
|
|
16424
16479
|
eq(schema.sessions.workspaceId, workspaceId),
|
|
16425
|
-
|
|
16426
|
-
inArray(schema.sessions.status, [
|
|
16480
|
+
sql`${schema.sessions.variableSetIds} ? ${variableSetId}`,
|
|
16481
|
+
inArray(schema.sessions.status, [
|
|
16482
|
+
"queued",
|
|
16483
|
+
"running",
|
|
16484
|
+
"requires_action",
|
|
16485
|
+
"recovering",
|
|
16486
|
+
"waiting_capacity",
|
|
16487
|
+
]),
|
|
16427
16488
|
),
|
|
16428
16489
|
);
|
|
16429
16490
|
return Number(count);
|
|
@@ -16528,7 +16589,15 @@ export class RigChangeAlreadyVerifyingError extends Error {
|
|
|
16528
16589
|
}
|
|
16529
16590
|
}
|
|
16530
16591
|
|
|
16592
|
+
export class RigImageOverrideUnsupportedError extends Error {
|
|
16593
|
+
constructor() {
|
|
16594
|
+
super("Rig image overrides are unsupported; Rigs use the deployment platform sandbox image");
|
|
16595
|
+
this.name = "RigImageOverrideUnsupportedError";
|
|
16596
|
+
}
|
|
16597
|
+
}
|
|
16598
|
+
|
|
16531
16599
|
export type RigVersionContentInput = {
|
|
16600
|
+
/** @deprecated Rig versions always use the deployment platform sandbox image. */
|
|
16532
16601
|
image?: string | null;
|
|
16533
16602
|
setupScript?: string | null;
|
|
16534
16603
|
checks?: RigCheck[];
|
|
@@ -16538,6 +16607,12 @@ export type RigVersionContentInput = {
|
|
|
16538
16607
|
createdBy?: string | null;
|
|
16539
16608
|
};
|
|
16540
16609
|
|
|
16610
|
+
function assertRigUsesPlatformImage(input: RigVersionContentInput | undefined): void {
|
|
16611
|
+
if (input?.image != null) {
|
|
16612
|
+
throw new RigImageOverrideUnsupportedError();
|
|
16613
|
+
}
|
|
16614
|
+
}
|
|
16615
|
+
|
|
16541
16616
|
function mapRigVersion(row: typeof schema.rigVersions.$inferSelect): RigVersion {
|
|
16542
16617
|
return {
|
|
16543
16618
|
id: row.id,
|
|
@@ -16773,6 +16848,7 @@ export async function createRig(
|
|
|
16773
16848
|
initialVersion?: RigVersionContentInput;
|
|
16774
16849
|
},
|
|
16775
16850
|
): Promise<Rig> {
|
|
16851
|
+
assertRigUsesPlatformImage(input.initialVersion);
|
|
16776
16852
|
if (input.scope !== undefined || input.subjectId !== undefined) {
|
|
16777
16853
|
return await withRlsContext(
|
|
16778
16854
|
db,
|
|
@@ -16786,7 +16862,7 @@ export async function createRig(
|
|
|
16786
16862
|
${input.accountId}::uuid, ${input.workspaceId}::uuid,
|
|
16787
16863
|
${input.scope ?? "workspace"}, ${input.name}, ${input.description ?? null},
|
|
16788
16864
|
${input.createdBy ?? null}, ${JSON.stringify({
|
|
16789
|
-
image:
|
|
16865
|
+
image: null,
|
|
16790
16866
|
setupScript: content.setupScript ?? null,
|
|
16791
16867
|
checks: content.checks ?? [],
|
|
16792
16868
|
credentialHooks: content.credentialHooks ?? [],
|
|
@@ -16826,7 +16902,7 @@ export async function createRig(
|
|
|
16826
16902
|
workspaceId: input.workspaceId,
|
|
16827
16903
|
rigId: rigRow.id,
|
|
16828
16904
|
version: 1,
|
|
16829
|
-
image:
|
|
16905
|
+
image: null,
|
|
16830
16906
|
setupScript: content.setupScript ?? null,
|
|
16831
16907
|
checks: content.checks ?? [],
|
|
16832
16908
|
credentialHooks: content.credentialHooks ?? [],
|
|
@@ -17180,6 +17256,66 @@ function isSessionChannelFkViolation(error: unknown): boolean {
|
|
|
17180
17256
|
);
|
|
17181
17257
|
}
|
|
17182
17258
|
|
|
17259
|
+
const sessionVariableSetSelectionFkConstraints = new Set([
|
|
17260
|
+
// Legacy final-entry mirror on sessions.variable_set_id.
|
|
17261
|
+
"sessions_environment_id_fkey",
|
|
17262
|
+
// Ordered attachment projection populated by the session INSERT/UPDATE trigger.
|
|
17263
|
+
"session_variable_set_attachments_variable_set_id_fkey",
|
|
17264
|
+
]);
|
|
17265
|
+
|
|
17266
|
+
/** A selected Variable Set disappeared after validation; unrelated FKs stay loud. */
|
|
17267
|
+
function isSessionVariableSetSelectionFkViolation(error: unknown): boolean {
|
|
17268
|
+
return (
|
|
17269
|
+
nestedPostgresSqlState(error) === "23503" &&
|
|
17270
|
+
sessionVariableSetSelectionFkConstraints.has(safeDatabaseErrorFacts(error).constraint ?? "")
|
|
17271
|
+
);
|
|
17272
|
+
}
|
|
17273
|
+
|
|
17274
|
+
export class SessionVariableSetSelectionUnavailableError extends Error {
|
|
17275
|
+
readonly variableSetIds: string[];
|
|
17276
|
+
|
|
17277
|
+
constructor(variableSetIds: string[], cause?: unknown) {
|
|
17278
|
+
super(
|
|
17279
|
+
"One or more selected Variable Sets are no longer available.",
|
|
17280
|
+
cause === undefined ? undefined : { cause },
|
|
17281
|
+
);
|
|
17282
|
+
this.name = "SessionVariableSetSelectionUnavailableError";
|
|
17283
|
+
this.variableSetIds = variableSetIds;
|
|
17284
|
+
}
|
|
17285
|
+
}
|
|
17286
|
+
|
|
17287
|
+
async function translateSessionVariableSetSelectionCreateError(
|
|
17288
|
+
db: Database,
|
|
17289
|
+
input: SessionCreateInput,
|
|
17290
|
+
error: unknown,
|
|
17291
|
+
): Promise<never> {
|
|
17292
|
+
if (!isSessionVariableSetSelectionFkViolation(error) || !input.subjectId) throw error;
|
|
17293
|
+
|
|
17294
|
+
// The failed create transaction, including its session row and projection
|
|
17295
|
+
// trigger, has rolled back. Resolve the exact requested identities under the
|
|
17296
|
+
// original account/workspace/subject RLS scope before translating the known
|
|
17297
|
+
// FK race. If every identity is still available, preserve the original
|
|
17298
|
+
// integrity failure so an unrelated defect cannot be hidden.
|
|
17299
|
+
const variableSetIds = [
|
|
17300
|
+
...new Set(input.variableSetIds ?? (input.variableSetId ? [input.variableSetId] : [])),
|
|
17301
|
+
];
|
|
17302
|
+
const unavailableVariableSetIds: string[] = [];
|
|
17303
|
+
for (const variableSetId of variableSetIds) {
|
|
17304
|
+
const current = await getVariableSet(
|
|
17305
|
+
db,
|
|
17306
|
+
{
|
|
17307
|
+
accountId: input.accountId,
|
|
17308
|
+
workspaceId: input.workspaceId,
|
|
17309
|
+
subjectId: input.subjectId,
|
|
17310
|
+
},
|
|
17311
|
+
variableSetId,
|
|
17312
|
+
);
|
|
17313
|
+
if (!current) unavailableVariableSetIds.push(variableSetId);
|
|
17314
|
+
}
|
|
17315
|
+
if (unavailableVariableSetIds.length === 0) throw error;
|
|
17316
|
+
throw new SessionVariableSetSelectionUnavailableError(unavailableVariableSetIds, error);
|
|
17317
|
+
}
|
|
17318
|
+
|
|
17183
17319
|
function mapChannel(row: typeof schema.channels.$inferSelect): Channel {
|
|
17184
17320
|
return {
|
|
17185
17321
|
id: row.id,
|
|
@@ -17422,6 +17558,311 @@ export async function setSessionChannel(
|
|
|
17422
17558
|
});
|
|
17423
17559
|
}
|
|
17424
17560
|
|
|
17561
|
+
export type UpdateSessionVariableSetsResult =
|
|
17562
|
+
| { status: "updated"; event: SessionEvent }
|
|
17563
|
+
| { status: "unchanged" }
|
|
17564
|
+
| { status: "invalid_variable_sets"; variableSetIds: string[] }
|
|
17565
|
+
| {
|
|
17566
|
+
status: "blocked";
|
|
17567
|
+
reason: "turn_in_flight" | "shared_sandbox_group" | "live_sandbox_holders";
|
|
17568
|
+
}
|
|
17569
|
+
| { status: "not_found" };
|
|
17570
|
+
|
|
17571
|
+
/**
|
|
17572
|
+
* Replace the complete ordered session Variable Set selection at a quiescent
|
|
17573
|
+
* turn boundary. The session row + FK-backed attachment trigger + audit/event
|
|
17574
|
+
* evidence commit together. A live managed box is fenced for rotation so its
|
|
17575
|
+
* baked environment can never serve the next turn after a detach.
|
|
17576
|
+
*/
|
|
17577
|
+
export async function updateSessionVariableSets(
|
|
17578
|
+
db: Database,
|
|
17579
|
+
input: {
|
|
17580
|
+
accountId: string;
|
|
17581
|
+
workspaceId: string;
|
|
17582
|
+
sessionId: string;
|
|
17583
|
+
subjectId: string;
|
|
17584
|
+
variableSets: Array<{
|
|
17585
|
+
id: string;
|
|
17586
|
+
name: string;
|
|
17587
|
+
scope: VariableSet["scope"];
|
|
17588
|
+
}>;
|
|
17589
|
+
},
|
|
17590
|
+
): Promise<UpdateSessionVariableSetsResult> {
|
|
17591
|
+
return await withWorkspaceSubjectSessionActivityRls<UpdateSessionVariableSetsResult>(
|
|
17592
|
+
db,
|
|
17593
|
+
input.workspaceId,
|
|
17594
|
+
input.subjectId,
|
|
17595
|
+
async (scopedDb) =>
|
|
17596
|
+
await scopedDb.transaction(async (rawTx) => {
|
|
17597
|
+
const tx = rawTx as unknown as Database;
|
|
17598
|
+
const locks = await lockSessionEventWriteRows(tx, {
|
|
17599
|
+
workspaceId: input.workspaceId,
|
|
17600
|
+
controlLock: "share",
|
|
17601
|
+
sessionIds: [input.sessionId],
|
|
17602
|
+
});
|
|
17603
|
+
const session = locks.sessions[0];
|
|
17604
|
+
if (!session || session.accountId !== input.accountId) return { status: "not_found" };
|
|
17605
|
+
// Prompt admission and turn claim both take the same session-row lock
|
|
17606
|
+
// before creating or claiming work. Inspect the complete tenancy
|
|
17607
|
+
// quiescence set while that lock is held so an accepted queued turn can
|
|
17608
|
+
// neither slip through this mutation nor be admitted against the old
|
|
17609
|
+
// Variable Set selection after this transaction commits.
|
|
17610
|
+
const [pendingWork] = await rawRows<{ blocker: string | null }>(
|
|
17611
|
+
tx,
|
|
17612
|
+
sql`select case
|
|
17613
|
+
when ${session.activeTurnId}::uuid is not null
|
|
17614
|
+
or exists (select 1 from ${schema.sessionTurns} turn_row
|
|
17615
|
+
where turn_row.workspace_id = ${input.workspaceId}
|
|
17616
|
+
and turn_row.session_id = ${input.sessionId}
|
|
17617
|
+
and turn_row.status in (
|
|
17618
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
17619
|
+
))
|
|
17620
|
+
then 'nonterminal_turn'
|
|
17621
|
+
when exists (select 1 from ${schema.sessionTurnAttempts} attempt_row
|
|
17622
|
+
where attempt_row.workspace_id = ${input.workspaceId}
|
|
17623
|
+
and attempt_row.session_id = ${input.sessionId}
|
|
17624
|
+
and attempt_row.state <> 'closed')
|
|
17625
|
+
then 'nonterminal_attempt'
|
|
17626
|
+
when exists (select 1 from ${schema.sessionAttemptInterruptions} interruption_row
|
|
17627
|
+
where interruption_row.workspace_id = ${input.workspaceId}
|
|
17628
|
+
and interruption_row.session_id = ${input.sessionId}
|
|
17629
|
+
and interruption_row.state in ('pending', 'delivered', 'acknowledged'))
|
|
17630
|
+
then 'unsettled_interruption'
|
|
17631
|
+
when exists (select 1 from ${schema.sessionSystemUpdates} update_row
|
|
17632
|
+
where update_row.workspace_id = ${input.workspaceId}
|
|
17633
|
+
and update_row.session_id = ${input.sessionId}
|
|
17634
|
+
and update_row.state = 'pending')
|
|
17635
|
+
then 'pending_system_update'
|
|
17636
|
+
when exists (select 1 from ${schema.sessionHumanInputRequests} request_row
|
|
17637
|
+
where request_row.workspace_id = ${input.workspaceId}
|
|
17638
|
+
and request_row.session_id = ${input.sessionId}
|
|
17639
|
+
and request_row.status = 'pending')
|
|
17640
|
+
then 'pending_human_input'
|
|
17641
|
+
when exists (select 1 from ${schema.sessionPendingToolCalls} tool_row
|
|
17642
|
+
where tool_row.workspace_id = ${input.workspaceId}
|
|
17643
|
+
and tool_row.session_id = ${input.sessionId})
|
|
17644
|
+
then 'pending_tool_receipt'
|
|
17645
|
+
when exists (select 1 from ${schema.agentRunStates} run_state
|
|
17646
|
+
where run_state.workspace_id = ${input.workspaceId}
|
|
17647
|
+
and run_state.session_id = ${input.sessionId})
|
|
17648
|
+
then 'run_state'
|
|
17649
|
+
when exists (select 1 from ${schema.sessionGoals} goal_row
|
|
17650
|
+
where goal_row.workspace_id = ${input.workspaceId}
|
|
17651
|
+
and goal_row.session_id = ${input.sessionId}
|
|
17652
|
+
and goal_row.status = 'active')
|
|
17653
|
+
then 'active_goal'
|
|
17654
|
+
when exists (select 1 from ${schema.codexCapacityWaiters} codex_waiter
|
|
17655
|
+
where codex_waiter.workspace_id = ${input.workspaceId}
|
|
17656
|
+
and codex_waiter.session_id = ${input.sessionId}
|
|
17657
|
+
and codex_waiter.status = 'waiting')
|
|
17658
|
+
or exists (select 1 from ${schema.xaiCapacityWaiters} xai_waiter
|
|
17659
|
+
where xai_waiter.workspace_id = ${input.workspaceId}
|
|
17660
|
+
and xai_waiter.session_id = ${input.sessionId}
|
|
17661
|
+
and xai_waiter.status = 'waiting')
|
|
17662
|
+
then 'capacity_waiter'
|
|
17663
|
+
when exists (select 1 from ${schema.sessionRealtimeModes} realtime_mode
|
|
17664
|
+
where realtime_mode.workspace_id = ${input.workspaceId}
|
|
17665
|
+
and realtime_mode.session_id = ${input.sessionId}
|
|
17666
|
+
and realtime_mode.state = 'active')
|
|
17667
|
+
or exists (select 1 from ${schema.sessionRealtimeConnections} realtime_connection
|
|
17668
|
+
where realtime_connection.workspace_id = ${input.workspaceId}
|
|
17669
|
+
and realtime_connection.session_id = ${input.sessionId}
|
|
17670
|
+
and realtime_connection.state in ('negotiating', 'ready', 'active'))
|
|
17671
|
+
then 'active_realtime'
|
|
17672
|
+
when exists (select 1 from ${schema.scheduledTasks} scheduled_task
|
|
17673
|
+
where scheduled_task.workspace_id = ${input.workspaceId}
|
|
17674
|
+
and scheduled_task.reusable_session_id = ${input.sessionId}
|
|
17675
|
+
and scheduled_task.status = 'active')
|
|
17676
|
+
then 'active_scheduled_task'
|
|
17677
|
+
when exists (select 1 from ${schema.sandboxWorkspaceMutationAdmissions} admission_row
|
|
17678
|
+
where admission_row.workspace_id = ${input.workspaceId}
|
|
17679
|
+
and admission_row.session_id = ${input.sessionId}
|
|
17680
|
+
and admission_row.settled_at is null)
|
|
17681
|
+
then 'workspace_mutation_admission'
|
|
17682
|
+
when exists (select 1 from ${schema.sandboxRetainedProcesses} process_row
|
|
17683
|
+
where process_row.workspace_id = ${input.workspaceId}
|
|
17684
|
+
and process_row.session_id = ${input.sessionId}
|
|
17685
|
+
and process_row.state = 'active')
|
|
17686
|
+
then 'retained_process'
|
|
17687
|
+
else null
|
|
17688
|
+
end as blocker`,
|
|
17689
|
+
);
|
|
17690
|
+
if (pendingWork?.blocker !== null && pendingWork?.blocker !== undefined) {
|
|
17691
|
+
return { status: "blocked", reason: "turn_in_flight" };
|
|
17692
|
+
}
|
|
17693
|
+
const [groupCount] = await rawRows<{ count: number }>(
|
|
17694
|
+
tx,
|
|
17695
|
+
sql`select count(*)::int as count
|
|
17696
|
+
from sessions
|
|
17697
|
+
where workspace_id = ${input.workspaceId}
|
|
17698
|
+
and sandbox_group_id = ${session.sandboxGroupId}`,
|
|
17699
|
+
);
|
|
17700
|
+
if (Number(groupCount?.count ?? 0) > 1) {
|
|
17701
|
+
return { status: "blocked", reason: "shared_sandbox_group" };
|
|
17702
|
+
}
|
|
17703
|
+
// Serialize with the get-or-create admission path even when this group
|
|
17704
|
+
// has never materialized a lease row. Once a row exists, lock it too:
|
|
17705
|
+
// every ordinary holder acquire and retained-process promotion takes
|
|
17706
|
+
// that row lock before publishing its holder.
|
|
17707
|
+
await lockSandboxLeaseAdmission(tx, input.workspaceId, session.sandboxGroupId);
|
|
17708
|
+
await tx.execute(sql`select id
|
|
17709
|
+
from sandbox_leases
|
|
17710
|
+
where workspace_id = ${input.workspaceId}
|
|
17711
|
+
and sandbox_group_id = ${session.sandboxGroupId}
|
|
17712
|
+
for update`);
|
|
17713
|
+
const [holderCount] = await rawRows<{ count: number }>(
|
|
17714
|
+
tx,
|
|
17715
|
+
sql`select count(*)::int as count
|
|
17716
|
+
from sandbox_lease_holders holder
|
|
17717
|
+
join sandbox_leases lease on lease.id = holder.lease_id
|
|
17718
|
+
where lease.workspace_id = ${input.workspaceId}
|
|
17719
|
+
and lease.sandbox_group_id = ${session.sandboxGroupId}`,
|
|
17720
|
+
);
|
|
17721
|
+
if (Number(holderCount?.count ?? 0) > 0) {
|
|
17722
|
+
return { status: "blocked", reason: "live_sandbox_holders" };
|
|
17723
|
+
}
|
|
17724
|
+
|
|
17725
|
+
const previousIds =
|
|
17726
|
+
(session.variableSetIds as string[]) ??
|
|
17727
|
+
(session.variableSetId ? [session.variableSetId] : []);
|
|
17728
|
+
const nextIds = input.variableSets.map((variableSet) => variableSet.id);
|
|
17729
|
+
if (stableJson(previousIds) === stableJson(nextIds)) {
|
|
17730
|
+
return { status: "unchanged" };
|
|
17731
|
+
}
|
|
17732
|
+
|
|
17733
|
+
const now = new Date();
|
|
17734
|
+
await tx
|
|
17735
|
+
.update(schema.sessions)
|
|
17736
|
+
.set({
|
|
17737
|
+
variableSetIds: nextIds,
|
|
17738
|
+
variableSetId: nextIds.at(-1) ?? null,
|
|
17739
|
+
updatedAt: now,
|
|
17740
|
+
})
|
|
17741
|
+
.where(
|
|
17742
|
+
and(
|
|
17743
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
17744
|
+
eq(schema.sessions.id, input.sessionId),
|
|
17745
|
+
),
|
|
17746
|
+
);
|
|
17747
|
+
await tx.execute(sql`update sandbox_leases set
|
|
17748
|
+
rotation_requested_at = coalesce(rotation_requested_at, now()),
|
|
17749
|
+
rotation_reason = coalesce(rotation_reason, 'operator'),
|
|
17750
|
+
expires_at = least(expires_at, now()),
|
|
17751
|
+
updated_at = now()
|
|
17752
|
+
where workspace_id = ${input.workspaceId}
|
|
17753
|
+
and sandbox_group_id = ${session.sandboxGroupId}
|
|
17754
|
+
and liveness <> 'cold'`);
|
|
17755
|
+
|
|
17756
|
+
const previous = new Set(previousIds);
|
|
17757
|
+
const next = new Set(nextIds);
|
|
17758
|
+
const addedIds = nextIds.filter((id) => !previous.has(id));
|
|
17759
|
+
const removedIds = previousIds.filter((id) => !next.has(id));
|
|
17760
|
+
const sequence = session.lastSequence + 1;
|
|
17761
|
+
const [eventRow] = await tx
|
|
17762
|
+
.insert(schema.sessionEvents)
|
|
17763
|
+
.values(
|
|
17764
|
+
withLosslessContentWriteVersion(
|
|
17765
|
+
{
|
|
17766
|
+
accountId: input.accountId,
|
|
17767
|
+
workspaceId: input.workspaceId,
|
|
17768
|
+
sessionId: input.sessionId,
|
|
17769
|
+
sequence,
|
|
17770
|
+
type: "session.variable_sets.updated",
|
|
17771
|
+
payload: {
|
|
17772
|
+
actor: input.subjectId,
|
|
17773
|
+
variableSets: input.variableSets,
|
|
17774
|
+
addedIds,
|
|
17775
|
+
removedIds,
|
|
17776
|
+
collisionPolicy: "later_selected_set_wins",
|
|
17777
|
+
effectiveOn: "next_turn_or_sandbox_operation",
|
|
17778
|
+
},
|
|
17779
|
+
occurredAt: now,
|
|
17780
|
+
},
|
|
17781
|
+
"payload",
|
|
17782
|
+
"payloadCodecVersion",
|
|
17783
|
+
),
|
|
17784
|
+
)
|
|
17785
|
+
.returning();
|
|
17786
|
+
if (!eventRow) throw new Error("Failed to append session Variable Set update event");
|
|
17787
|
+
await tx
|
|
17788
|
+
.update(schema.sessions)
|
|
17789
|
+
.set({ lastSequence: sequence })
|
|
17790
|
+
.where(
|
|
17791
|
+
and(
|
|
17792
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
17793
|
+
eq(schema.sessions.id, input.sessionId),
|
|
17794
|
+
),
|
|
17795
|
+
);
|
|
17796
|
+
const auditChanges = [
|
|
17797
|
+
...addedIds.map((id) => ({
|
|
17798
|
+
action: "session.variable_set.attached",
|
|
17799
|
+
id,
|
|
17800
|
+
})),
|
|
17801
|
+
...removedIds.map((id) => ({
|
|
17802
|
+
action: "session.variable_set.detached",
|
|
17803
|
+
id,
|
|
17804
|
+
})),
|
|
17805
|
+
...(addedIds.length === 0 && removedIds.length === 0
|
|
17806
|
+
? [
|
|
17807
|
+
{
|
|
17808
|
+
action: "session.variable_sets.reordered",
|
|
17809
|
+
id: input.sessionId,
|
|
17810
|
+
},
|
|
17811
|
+
]
|
|
17812
|
+
: []),
|
|
17813
|
+
];
|
|
17814
|
+
await tx.insert(schema.auditEvents).values(
|
|
17815
|
+
auditChanges.map(({ action, id }) =>
|
|
17816
|
+
withLosslessContentWriteVersion(
|
|
17817
|
+
{
|
|
17818
|
+
accountId: input.accountId,
|
|
17819
|
+
workspaceId: input.workspaceId,
|
|
17820
|
+
subjectId: input.subjectId,
|
|
17821
|
+
action,
|
|
17822
|
+
targetType: "session",
|
|
17823
|
+
targetId: input.sessionId,
|
|
17824
|
+
metadata: {
|
|
17825
|
+
sessionId: input.sessionId,
|
|
17826
|
+
...(action === "session.variable_sets.reordered"
|
|
17827
|
+
? { variableSetIds: nextIds }
|
|
17828
|
+
: { variableSetId: id }),
|
|
17829
|
+
},
|
|
17830
|
+
},
|
|
17831
|
+
"metadata",
|
|
17832
|
+
"metadataCodecVersion",
|
|
17833
|
+
),
|
|
17834
|
+
),
|
|
17835
|
+
);
|
|
17836
|
+
return { status: "updated", event: mapEvent(eventRow) };
|
|
17837
|
+
}),
|
|
17838
|
+
).catch(async (error: unknown) => {
|
|
17839
|
+
if (!isSessionVariableSetSelectionFkViolation(error)) throw error;
|
|
17840
|
+
|
|
17841
|
+
// The failed transaction, including the session row, projection trigger,
|
|
17842
|
+
// lease rotation, event, and audit writes, has rolled back. Re-resolve the
|
|
17843
|
+
// exact requested identities under the same tenant/subject authority so a
|
|
17844
|
+
// concurrent revoke/delete is translated without masking any other FK bug.
|
|
17845
|
+
const unavailableVariableSetIds: string[] = [];
|
|
17846
|
+
for (const variableSet of input.variableSets) {
|
|
17847
|
+
const current = await getVariableSet(
|
|
17848
|
+
db,
|
|
17849
|
+
{
|
|
17850
|
+
accountId: input.accountId,
|
|
17851
|
+
workspaceId: input.workspaceId,
|
|
17852
|
+
subjectId: input.subjectId,
|
|
17853
|
+
},
|
|
17854
|
+
variableSet.id,
|
|
17855
|
+
);
|
|
17856
|
+
if (!current) unavailableVariableSetIds.push(variableSet.id);
|
|
17857
|
+
}
|
|
17858
|
+
if (unavailableVariableSetIds.length === 0) throw error;
|
|
17859
|
+
return {
|
|
17860
|
+
status: "invalid_variable_sets",
|
|
17861
|
+
variableSetIds: unavailableVariableSetIds,
|
|
17862
|
+
};
|
|
17863
|
+
});
|
|
17864
|
+
}
|
|
17865
|
+
|
|
17425
17866
|
export async function countRigs(
|
|
17426
17867
|
db: Database,
|
|
17427
17868
|
access: string | RigAccessContext,
|
|
@@ -17473,6 +17914,7 @@ export async function createRigVersion(
|
|
|
17473
17914
|
input: RigVersionContentInput,
|
|
17474
17915
|
options: { activate?: boolean } = {},
|
|
17475
17916
|
): Promise<RigVersion> {
|
|
17917
|
+
assertRigUsesPlatformImage(input);
|
|
17476
17918
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
17477
17919
|
const [rig] = await scopedDb
|
|
17478
17920
|
.select({ id: schema.rigs.id, accountId: schema.rigs.accountId })
|
|
@@ -17511,7 +17953,7 @@ export async function createRigVersion(
|
|
|
17511
17953
|
workspaceId,
|
|
17512
17954
|
rigId,
|
|
17513
17955
|
version: nextVersion,
|
|
17514
|
-
image:
|
|
17956
|
+
image: null,
|
|
17515
17957
|
setupScript: input.setupScript ?? null,
|
|
17516
17958
|
checks: input.checks ?? [],
|
|
17517
17959
|
credentialHooks: input.credentialHooks ?? [],
|
|
@@ -17543,6 +17985,7 @@ export async function createRigVersionForChangePromotion(
|
|
|
17543
17985
|
providerImages?: RigProviderImages;
|
|
17544
17986
|
},
|
|
17545
17987
|
): Promise<{ version: RigVersion; change: RigChange }> {
|
|
17988
|
+
assertRigUsesPlatformImage(input);
|
|
17546
17989
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
17547
17990
|
const [rig] = await scopedDb
|
|
17548
17991
|
.select({ id: schema.rigs.id, accountId: schema.rigs.accountId })
|
|
@@ -17627,7 +18070,7 @@ export async function createRigVersionForChangePromotion(
|
|
|
17627
18070
|
workspaceId,
|
|
17628
18071
|
rigId,
|
|
17629
18072
|
version: nextVersion,
|
|
17630
|
-
image:
|
|
18073
|
+
image: null,
|
|
17631
18074
|
setupScript: input.setupScript ?? null,
|
|
17632
18075
|
checks: input.checks ?? [],
|
|
17633
18076
|
credentialHooks: input.credentialHooks ?? [],
|
|
@@ -17948,9 +18391,9 @@ async function retainRigProviderImageArtifacts(
|
|
|
17948
18391
|
|
|
17949
18392
|
/**
|
|
17950
18393
|
* Claim the one provider-image build slot for an existing immutable rig
|
|
17951
|
-
* version.
|
|
17952
|
-
*
|
|
17953
|
-
*
|
|
18394
|
+
* version. The setup hash fences definition drift. Deployment base-image and
|
|
18395
|
+
* provider-image protocol rotations may replace operational metadata for the
|
|
18396
|
+
* same setup; late finalizers remain fenced by the build request id.
|
|
17954
18397
|
*/
|
|
17955
18398
|
export async function claimRigVersionProviderImageBuild(
|
|
17956
18399
|
db: Database,
|
|
@@ -17987,22 +18430,24 @@ export async function claimRigVersionProviderImageBuild(
|
|
|
17987
18430
|
existing.contentHash === candidate.contentHash &&
|
|
17988
18431
|
existing.setupHash === candidate.setupHash &&
|
|
17989
18432
|
existing.sourceImage === candidate.sourceImage;
|
|
18433
|
+
const sameSetup = existing.setupHash === candidate.setupHash;
|
|
17990
18434
|
if (existing.status === "ready") {
|
|
17991
|
-
if (
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
18435
|
+
if (sameContent) {
|
|
18436
|
+
const retained = await retainRigProviderImageArtifacts(scopedDb, input.workspaceId, {
|
|
18437
|
+
[existing.backend]: existing,
|
|
18438
|
+
});
|
|
18439
|
+
if (retained[existing.backend] && existing.coldBootValidation?.version === 1) {
|
|
18440
|
+
return { status: "ready", image: existing };
|
|
18441
|
+
}
|
|
17997
18442
|
}
|
|
17998
18443
|
}
|
|
17999
18444
|
if (existing.status === "unsupported" && sameContent && !input.retryUnsupported) {
|
|
18000
18445
|
return { status: "unsupported", image: existing };
|
|
18001
18446
|
}
|
|
18002
|
-
if (!sameContent) {
|
|
18447
|
+
if (!sameContent && !sameSetup) {
|
|
18003
18448
|
return { status: "conflict", image: existing };
|
|
18004
18449
|
}
|
|
18005
|
-
if (existing.status === "building") {
|
|
18450
|
+
if (sameContent && existing.status === "building") {
|
|
18006
18451
|
const startedAtMs = Date.parse(existing.startedAt);
|
|
18007
18452
|
if (
|
|
18008
18453
|
Number.isFinite(startedAtMs) &&
|
|
@@ -26912,6 +27357,7 @@ export type SessionCreateInput = {
|
|
|
26912
27357
|
reasoningEffort: ReasoningEffort;
|
|
26913
27358
|
latencyMode: LatencyMode;
|
|
26914
27359
|
sandboxBackend: SandboxBackend;
|
|
27360
|
+
variableSetIds?: string[];
|
|
26915
27361
|
variableSetId?: string | null;
|
|
26916
27362
|
rigId?: string | null;
|
|
26917
27363
|
rigVersionId?: string | null;
|
|
@@ -27292,6 +27738,8 @@ async function createSessionInTransaction(
|
|
|
27292
27738
|
input: SessionCreateInput,
|
|
27293
27739
|
id: string,
|
|
27294
27740
|
): Promise<SessionCreateResult> {
|
|
27741
|
+
const variableSetIds = input.variableSetIds ?? (input.variableSetId ? [input.variableSetId] : []);
|
|
27742
|
+
const variableSetId = variableSetIds.at(-1) ?? null;
|
|
27295
27743
|
const createIdempotencyKey = input.createIdempotencyKey ?? null;
|
|
27296
27744
|
const createRequestedVisibility = input.visibility ?? "workspace_shared";
|
|
27297
27745
|
if (createRequestedVisibility === "user_private") {
|
|
@@ -27347,6 +27795,9 @@ async function createSessionInTransaction(
|
|
|
27347
27795
|
) {
|
|
27348
27796
|
throw new SessionCreateIdempotencyConflictError();
|
|
27349
27797
|
}
|
|
27798
|
+
if (stableJson(existing.variableSetIds ?? []) !== stableJson(variableSetIds)) {
|
|
27799
|
+
throw new SessionCreateIdempotencyConflictError();
|
|
27800
|
+
}
|
|
27350
27801
|
if (existing.createRequestedVisibility !== createRequestedVisibility) {
|
|
27351
27802
|
throw new SessionCreateIdempotencyConflictError();
|
|
27352
27803
|
}
|
|
@@ -27457,6 +27908,8 @@ async function createSessionInTransaction(
|
|
|
27457
27908
|
workspaceId: input.workspaceId,
|
|
27458
27909
|
initialMessage: input.initialMessage,
|
|
27459
27910
|
initialModelContext: input.initialModelContext ?? null,
|
|
27911
|
+
title: AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
27912
|
+
titleSource: "agent",
|
|
27460
27913
|
resources: input.resources,
|
|
27461
27914
|
skills: input.skills ?? [],
|
|
27462
27915
|
tools: input.tools ?? [],
|
|
@@ -27478,7 +27931,8 @@ async function createSessionInTransaction(
|
|
|
27478
27931
|
sandboxBackend: input.sandboxBackend,
|
|
27479
27932
|
sandboxOs: input.sandboxOs ?? "linux",
|
|
27480
27933
|
sandboxGroupId: input.sandboxGroupId ?? id,
|
|
27481
|
-
|
|
27934
|
+
variableSetIds,
|
|
27935
|
+
variableSetId,
|
|
27482
27936
|
rigId: input.rigId ?? null,
|
|
27483
27937
|
rigVersionId: input.rigVersionId ?? null,
|
|
27484
27938
|
channelId: input.channelId ?? null,
|
|
@@ -27546,6 +28000,9 @@ async function createSessionInTransaction(
|
|
|
27546
28000
|
) {
|
|
27547
28001
|
throw new SessionCreateIdempotencyConflictError();
|
|
27548
28002
|
}
|
|
28003
|
+
if (stableJson(existing.variableSetIds ?? []) !== stableJson(variableSetIds)) {
|
|
28004
|
+
throw new SessionCreateIdempotencyConflictError();
|
|
28005
|
+
}
|
|
27549
28006
|
if (existing.createRequestedVisibility !== createRequestedVisibility) {
|
|
27550
28007
|
throw new SessionCreateIdempotencyConflictError();
|
|
27551
28008
|
}
|
|
@@ -27597,15 +28054,20 @@ export async function createSession(db: Database, input: SessionCreateInput): Pr
|
|
|
27597
28054
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
27598
28055
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
27599
28056
|
const id = input.requestedSessionId ?? crypto.randomUUID();
|
|
27600
|
-
|
|
27601
|
-
|
|
27602
|
-
|
|
27603
|
-
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
28057
|
+
let result: SessionCreateResult;
|
|
28058
|
+
try {
|
|
28059
|
+
result = await withSessionActivityRlsContext(
|
|
28060
|
+
db,
|
|
28061
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
28062
|
+
async (scopedDb) =>
|
|
28063
|
+
await withSessionActivitySavepoint(
|
|
28064
|
+
scopedDb,
|
|
28065
|
+
async (tx) => await createSessionInTransaction(tx, input, id),
|
|
28066
|
+
),
|
|
28067
|
+
);
|
|
28068
|
+
} catch (error) {
|
|
28069
|
+
return await translateSessionVariableSetSelectionCreateError(db, input, error);
|
|
28070
|
+
}
|
|
27609
28071
|
if (result.denied) {
|
|
27610
28072
|
// Throw only after withRlsContext's outer transaction commits the denial.
|
|
27611
28073
|
throw new SessionSpawnDeniedDbError(result.denial);
|
|
@@ -27630,15 +28092,19 @@ export async function createSessionWithIdempotencyKeyResult(
|
|
|
27630
28092
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
27631
28093
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
27632
28094
|
const id = input.requestedSessionId ?? crypto.randomUUID();
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
|
|
27636
|
-
|
|
27637
|
-
|
|
27638
|
-
|
|
27639
|
-
|
|
27640
|
-
|
|
27641
|
-
|
|
28095
|
+
try {
|
|
28096
|
+
return await withSessionActivityRlsContext(
|
|
28097
|
+
db,
|
|
28098
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
28099
|
+
async (scopedDb) =>
|
|
28100
|
+
await withSessionActivitySavepoint(
|
|
28101
|
+
scopedDb,
|
|
28102
|
+
async (tx) => await createSessionInTransaction(tx, input, id),
|
|
28103
|
+
),
|
|
28104
|
+
);
|
|
28105
|
+
} catch (error) {
|
|
28106
|
+
return await translateSessionVariableSetSelectionCreateError(db, input, error);
|
|
28107
|
+
}
|
|
27642
28108
|
}
|
|
27643
28109
|
|
|
27644
28110
|
/**
|
|
@@ -28322,6 +28788,26 @@ export async function listDistinctVariableSetIdsInGroup(
|
|
|
28322
28788
|
});
|
|
28323
28789
|
}
|
|
28324
28790
|
|
|
28791
|
+
/** Ordered Variable Set selections across every member of a sandbox group. */
|
|
28792
|
+
export async function listDistinctVariableSetSelectionsInGroup(
|
|
28793
|
+
db: Database,
|
|
28794
|
+
workspaceId: string,
|
|
28795
|
+
sandboxGroupId: string,
|
|
28796
|
+
): Promise<string[][]> {
|
|
28797
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
28798
|
+
const rows = await scopedDb
|
|
28799
|
+
.selectDistinct({ variableSetIds: schema.sessions.variableSetIds })
|
|
28800
|
+
.from(schema.sessions)
|
|
28801
|
+
.where(
|
|
28802
|
+
and(
|
|
28803
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
28804
|
+
eq(schema.sessions.sandboxGroupId, sandboxGroupId),
|
|
28805
|
+
),
|
|
28806
|
+
);
|
|
28807
|
+
return rows.map((row) => (row.variableSetIds as string[]) ?? []);
|
|
28808
|
+
});
|
|
28809
|
+
}
|
|
28810
|
+
|
|
28325
28811
|
// M3 rig sharing gate: the distinct frozen rig_version_ids across a group's
|
|
28326
28812
|
// sessions. Mirrors listDistinctVariableSetIdsInGroup — the box's rig-baked
|
|
28327
28813
|
// setup is fixed at cold-create, so a session joining a group must carry the
|
|
@@ -36282,6 +36768,24 @@ async function upsertLeaseHolder(
|
|
|
36282
36768
|
`);
|
|
36283
36769
|
}
|
|
36284
36770
|
|
|
36771
|
+
/**
|
|
36772
|
+
* Fence first lease materialization against control-plane mutations that must
|
|
36773
|
+
* prove the sandbox group has no holders. The lease-row lock remains the
|
|
36774
|
+
* canonical holder lock once the row exists; this advisory lock covers only
|
|
36775
|
+
* the otherwise-unlockable absent-row window.
|
|
36776
|
+
*/
|
|
36777
|
+
async function lockSandboxLeaseAdmission(
|
|
36778
|
+
tx: Database,
|
|
36779
|
+
workspaceId: string,
|
|
36780
|
+
sandboxGroupId: string,
|
|
36781
|
+
): Promise<void> {
|
|
36782
|
+
await tx.execute(
|
|
36783
|
+
sql`select pg_advisory_xact_lock(hashtextextended(
|
|
36784
|
+
${`sandbox-lease-admission:${workspaceId}:${sandboxGroupId}`}, 0
|
|
36785
|
+
))`,
|
|
36786
|
+
);
|
|
36787
|
+
}
|
|
36788
|
+
|
|
36285
36789
|
// §4.1 — the get-or-create critical section. ONE transaction:
|
|
36286
36790
|
// insert-or-nothing -> SELECT … FOR UPDATE (block, not skip) -> branch -> bump.
|
|
36287
36791
|
// The single most load-bearing function: the sole double-spawn guard.
|
|
@@ -36304,6 +36808,7 @@ async function acquireLeaseOnce(
|
|
|
36304
36808
|
const tx = txRaw as unknown as Database;
|
|
36305
36809
|
const image = input.image ?? null;
|
|
36306
36810
|
const rigVersionId = input.rigVersionId ?? null;
|
|
36811
|
+
await lockSandboxLeaseAdmission(tx, workspaceId, sandboxGroupId);
|
|
36307
36812
|
// (1) Materialize the singleton row if absent. ON CONFLICT DO NOTHING + the
|
|
36308
36813
|
// unique index = idempotent under a race; concurrent inserts collapse to
|
|
36309
36814
|
// one row. expires_at seeded so a never-warmed cold row has a valid TTL. The
|
|
@@ -52777,10 +53282,21 @@ export async function recordSessionGoalProgressWithEvent(
|
|
|
52777
53282
|
}
|
|
52778
53283
|
|
|
52779
53284
|
/**
|
|
52780
|
-
*
|
|
52781
|
-
*
|
|
52782
|
-
*
|
|
52783
|
-
*
|
|
53285
|
+
* Low-level row-only session-title update used by database-specific callers and
|
|
53286
|
+
* tests. Public product writers should use `updateSessionTitleWithEvent` so the
|
|
53287
|
+
* observable event commits atomically with this state transition. The clobber
|
|
53288
|
+
* guard lives entirely in this single atomic UPDATE: a user-set title is
|
|
53289
|
+
* permanent, so agent/auto writes
|
|
53290
|
+
* are first normalized through the shared short/sensitive-safe automatic-title
|
|
53291
|
+
* policy, then carry an `AND title_source IS DISTINCT FROM 'user'` guard
|
|
53292
|
+
* (NULL-safe in Postgres) while user writes are unconditional. Automatic
|
|
53293
|
+
* writes also bind the exact normalized candidate to a transaction-local
|
|
53294
|
+
* database capability, so a pre-policy binary cannot bypass normalization
|
|
53295
|
+
* during a rolling deployment. Never read-modify-write.
|
|
53296
|
+
*
|
|
53297
|
+
* `expectedCurrent`, when supplied, is an additional NULL-safe compare-and-set
|
|
53298
|
+
* fence. It is used by stale/retry-prone producers such as scheduled dispatch:
|
|
53299
|
+
* the write succeeds only if both the durable title and source still match.
|
|
52784
53300
|
* Re-applying the exact title is also a no-op so a confused agent cannot churn
|
|
52785
53301
|
* `updated_at` every turn. Returns `{ updated, title }`: `updated` is false when
|
|
52786
53302
|
* the value was unchanged or an agent write was skipped because a user title
|
|
@@ -52794,13 +53310,37 @@ export async function updateSessionTitle(
|
|
|
52794
53310
|
sessionId: string;
|
|
52795
53311
|
title: string;
|
|
52796
53312
|
source: "user" | "agent";
|
|
53313
|
+
expectedCurrent?: {
|
|
53314
|
+
title: string | null;
|
|
53315
|
+
source: "user" | "agent" | null;
|
|
53316
|
+
};
|
|
52797
53317
|
},
|
|
52798
53318
|
): Promise<{ updated: boolean; title: string | null }> {
|
|
52799
53319
|
return await withWorkspaceSessionActivityRls(db, input.workspaceId, async (scopedDb) => {
|
|
53320
|
+
const title =
|
|
53321
|
+
input.source === "agent" ? normalizeAutomaticSessionTitle(input.title) : input.title;
|
|
53322
|
+
if (title === null) {
|
|
53323
|
+
const [existing] = await scopedDb
|
|
53324
|
+
.select({ title: schema.sessions.title })
|
|
53325
|
+
.from(schema.sessions)
|
|
53326
|
+
.where(
|
|
53327
|
+
and(
|
|
53328
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
53329
|
+
eq(schema.sessions.id, input.sessionId),
|
|
53330
|
+
),
|
|
53331
|
+
)
|
|
53332
|
+
.limit(1);
|
|
53333
|
+
return { updated: false, title: existing?.title ?? null };
|
|
53334
|
+
}
|
|
53335
|
+
if (input.source === "agent") {
|
|
53336
|
+
await scopedDb.execute(
|
|
53337
|
+
sql`select pg_catalog.set_config('opengeni.automatic_session_title_v1_candidate', ${title}, true)`,
|
|
53338
|
+
);
|
|
53339
|
+
}
|
|
52800
53340
|
const [row] = await scopedDb
|
|
52801
53341
|
.update(schema.sessions)
|
|
52802
53342
|
.set({
|
|
52803
|
-
title
|
|
53343
|
+
title,
|
|
52804
53344
|
titleSource: input.source,
|
|
52805
53345
|
updatedAt: new Date(),
|
|
52806
53346
|
})
|
|
@@ -52808,10 +53348,16 @@ export async function updateSessionTitle(
|
|
|
52808
53348
|
and(
|
|
52809
53349
|
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
52810
53350
|
eq(schema.sessions.id, input.sessionId),
|
|
52811
|
-
sql`${schema.sessions.title} is distinct from ${
|
|
53351
|
+
sql`${schema.sessions.title} is distinct from ${title}`,
|
|
52812
53352
|
...(input.source === "agent"
|
|
52813
53353
|
? [sql`${schema.sessions.titleSource} is distinct from 'user'`]
|
|
52814
53354
|
: []),
|
|
53355
|
+
...(input.expectedCurrent
|
|
53356
|
+
? [
|
|
53357
|
+
sql`${schema.sessions.title} is not distinct from ${input.expectedCurrent.title}`,
|
|
53358
|
+
sql`${schema.sessions.titleSource} is not distinct from ${input.expectedCurrent.source}`,
|
|
53359
|
+
]
|
|
53360
|
+
: []),
|
|
52815
53361
|
),
|
|
52816
53362
|
)
|
|
52817
53363
|
.returning({ title: schema.sessions.title });
|
|
@@ -52832,6 +53378,113 @@ export async function updateSessionTitle(
|
|
|
52832
53378
|
});
|
|
52833
53379
|
}
|
|
52834
53380
|
|
|
53381
|
+
/**
|
|
53382
|
+
* Atomically set a session title and append its public `session.title_set`
|
|
53383
|
+
* event under the canonical workspace/session event-write lock. The row CAS,
|
|
53384
|
+
* durable sequence, and event either all commit or all roll back, so a newer
|
|
53385
|
+
* title writer can never commit between the authoritative update and an older
|
|
53386
|
+
* event append.
|
|
53387
|
+
*
|
|
53388
|
+
* Exact retries are naturally idempotent: the unchanged-title predicate (and,
|
|
53389
|
+
* for stale producers, `expectedCurrent`) makes a committed retry a no-op with
|
|
53390
|
+
* no duplicate event. Automatic titles retain the rolling-deployment trigger's
|
|
53391
|
+
* exact transaction-local candidate capability.
|
|
53392
|
+
*/
|
|
53393
|
+
export async function updateSessionTitleWithEvent(
|
|
53394
|
+
db: Database,
|
|
53395
|
+
input: {
|
|
53396
|
+
workspaceId: string;
|
|
53397
|
+
sessionId: string;
|
|
53398
|
+
title: string;
|
|
53399
|
+
source: "user" | "agent";
|
|
53400
|
+
expectedCurrent?: {
|
|
53401
|
+
title: string | null;
|
|
53402
|
+
source: "user" | "agent" | null;
|
|
53403
|
+
};
|
|
53404
|
+
},
|
|
53405
|
+
): Promise<{ updated: boolean; title: string | null; events: SessionEvent[] }> {
|
|
53406
|
+
const title =
|
|
53407
|
+
input.source === "agent" ? normalizeAutomaticSessionTitle(input.title) : input.title;
|
|
53408
|
+
return await retryWorkspaceSessionEventActivityPersistence(
|
|
53409
|
+
db,
|
|
53410
|
+
input.workspaceId,
|
|
53411
|
+
true,
|
|
53412
|
+
{
|
|
53413
|
+
stage: "session_title.update_with_event",
|
|
53414
|
+
eventTypes: ["session.title_set"],
|
|
53415
|
+
maxAttempts: 3,
|
|
53416
|
+
},
|
|
53417
|
+
async (scopedDb) =>
|
|
53418
|
+
await scopedDb.transaction(async (tx) => {
|
|
53419
|
+
const locks = await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
53420
|
+
workspaceId: input.workspaceId,
|
|
53421
|
+
controlLock: "none",
|
|
53422
|
+
sessionIds: [input.sessionId],
|
|
53423
|
+
});
|
|
53424
|
+
const session = locks.sessions[0];
|
|
53425
|
+
if (!session) return { updated: false, title: null, events: [] };
|
|
53426
|
+
if (title === null) {
|
|
53427
|
+
return { updated: false, title: session.title, events: [] };
|
|
53428
|
+
}
|
|
53429
|
+
if (input.source === "agent") {
|
|
53430
|
+
await tx.execute(
|
|
53431
|
+
sql`select pg_catalog.set_config('opengeni.automatic_session_title_v1_candidate', ${title}, true)`,
|
|
53432
|
+
);
|
|
53433
|
+
}
|
|
53434
|
+
const now = new Date();
|
|
53435
|
+
const sequence = session.lastSequence + 1;
|
|
53436
|
+
const [updated] = await tx
|
|
53437
|
+
.update(schema.sessions)
|
|
53438
|
+
.set({
|
|
53439
|
+
title,
|
|
53440
|
+
titleSource: input.source,
|
|
53441
|
+
lastSequence: sequence,
|
|
53442
|
+
updatedAt: now,
|
|
53443
|
+
})
|
|
53444
|
+
.where(
|
|
53445
|
+
and(
|
|
53446
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
53447
|
+
eq(schema.sessions.id, input.sessionId),
|
|
53448
|
+
sql`${schema.sessions.title} is distinct from ${title}`,
|
|
53449
|
+
...(input.source === "agent"
|
|
53450
|
+
? [sql`${schema.sessions.titleSource} is distinct from 'user'`]
|
|
53451
|
+
: []),
|
|
53452
|
+
...(input.expectedCurrent
|
|
53453
|
+
? [
|
|
53454
|
+
sql`${schema.sessions.title} is not distinct from ${input.expectedCurrent.title}`,
|
|
53455
|
+
sql`${schema.sessions.titleSource} is not distinct from ${input.expectedCurrent.source}`,
|
|
53456
|
+
]
|
|
53457
|
+
: []),
|
|
53458
|
+
),
|
|
53459
|
+
)
|
|
53460
|
+
.returning({ title: schema.sessions.title });
|
|
53461
|
+
if (!updated) {
|
|
53462
|
+
return { updated: false, title: session.title, events: [] };
|
|
53463
|
+
}
|
|
53464
|
+
const [event] = await tx
|
|
53465
|
+
.insert(schema.sessionEvents)
|
|
53466
|
+
.values(
|
|
53467
|
+
withLosslessContentWriteVersion(
|
|
53468
|
+
{
|
|
53469
|
+
accountId: session.accountId,
|
|
53470
|
+
workspaceId: input.workspaceId,
|
|
53471
|
+
sessionId: input.sessionId,
|
|
53472
|
+
sequence,
|
|
53473
|
+
type: "session.title_set",
|
|
53474
|
+
payload: { title: updated.title ?? title, source: input.source },
|
|
53475
|
+
occurredAt: now,
|
|
53476
|
+
},
|
|
53477
|
+
"payload",
|
|
53478
|
+
"payloadCodecVersion",
|
|
53479
|
+
),
|
|
53480
|
+
)
|
|
53481
|
+
.returning();
|
|
53482
|
+
if (!event) throw new Error("Failed to append session.title_set event");
|
|
53483
|
+
return { updated: true, title: updated.title, events: [mapEvent(event)] };
|
|
53484
|
+
}),
|
|
53485
|
+
);
|
|
53486
|
+
}
|
|
53487
|
+
|
|
52835
53488
|
/**
|
|
52836
53489
|
* Status transition helper. Idempotent: requesting the current status returns
|
|
52837
53490
|
* `changed: false` so callers can skip emitting a duplicate event. `completed`
|
|
@@ -56986,17 +57639,33 @@ export async function claimSessionWorkForAttempt(
|
|
|
56986
57639
|
),
|
|
56987
57640
|
)
|
|
56988
57641
|
.orderBy(asc(schema.sessionMcpServers.serverId));
|
|
56989
|
-
const
|
|
56990
|
-
|
|
57642
|
+
const targetVariableSets =
|
|
57643
|
+
targetPolicy.variableSets.length > 0
|
|
57644
|
+
? targetPolicy.variableSets
|
|
57645
|
+
: targetPolicy.variableSetId && targetPolicy.variableSetGeneration
|
|
57646
|
+
? [
|
|
57647
|
+
{
|
|
57648
|
+
id: targetPolicy.variableSetId,
|
|
57649
|
+
generation: targetPolicy.variableSetGeneration,
|
|
57650
|
+
},
|
|
57651
|
+
]
|
|
57652
|
+
: [];
|
|
57653
|
+
const liveVariableSets = await Promise.all(
|
|
57654
|
+
targetVariableSets.map(async (selected) => {
|
|
57655
|
+
const variableSet = await getVariableSet(
|
|
56991
57656
|
tx as unknown as Database,
|
|
56992
57657
|
{
|
|
56993
57658
|
accountId: session.accountId,
|
|
56994
57659
|
workspaceId,
|
|
56995
57660
|
subjectId: accepted.personalResourceAuthoritySubjectId ?? "scheduled-task",
|
|
56996
57661
|
},
|
|
56997
|
-
|
|
56998
|
-
)
|
|
56999
|
-
|
|
57662
|
+
selected.id,
|
|
57663
|
+
);
|
|
57664
|
+
return variableSet
|
|
57665
|
+
? { id: variableSet.id, generation: variableSet.generation }
|
|
57666
|
+
: null;
|
|
57667
|
+
}),
|
|
57668
|
+
);
|
|
57000
57669
|
if (
|
|
57001
57670
|
targetPolicy.sessionId !== sessionId ||
|
|
57002
57671
|
targetPolicy.visibility !== session.visibility ||
|
|
@@ -57009,8 +57678,10 @@ export async function claimSessionWorkForAttempt(
|
|
|
57009
57678
|
stableJson(targetPolicy.mcpServerIds) !==
|
|
57010
57679
|
stableJson(targetMcpServers.map((server) => server.id)) ||
|
|
57011
57680
|
targetPolicy.toolPolicyVersion !== session.toolPolicyVersion ||
|
|
57012
|
-
|
|
57013
|
-
|
|
57681
|
+
stableJson(targetVariableSets) !==
|
|
57682
|
+
stableJson(liveVariableSets.filter((value) => value !== null)) ||
|
|
57683
|
+
stableJson(targetVariableSets.map((variableSet) => variableSet.id)) !==
|
|
57684
|
+
stableJson((session.variableSetIds as string[]) ?? []) ||
|
|
57014
57685
|
targetPolicy.rigId !== (session.rigId ?? null) ||
|
|
57015
57686
|
targetPolicy.rigVersionId !== (session.rigVersionId ?? null) ||
|
|
57016
57687
|
targetPolicy.maxNestedAgentDepthOverride !==
|
|
@@ -62478,6 +63149,7 @@ export async function getScheduledTargetSessionExecution(
|
|
|
62478
63149
|
firstPartyMcpPermissions: schema.sessions.firstPartyMcpPermissions,
|
|
62479
63150
|
toolPolicy: schema.sessions.toolPolicy,
|
|
62480
63151
|
toolPolicyVersion: schema.sessions.toolPolicyVersion,
|
|
63152
|
+
variableSetIds: schema.sessions.variableSetIds,
|
|
62481
63153
|
variableSetId: schema.sessions.variableSetId,
|
|
62482
63154
|
rigId: schema.sessions.rigId,
|
|
62483
63155
|
rigVersionId: schema.sessions.rigVersionId,
|
|
@@ -62500,8 +63172,9 @@ export async function getScheduledTargetSessionExecution(
|
|
|
62500
63172
|
)
|
|
62501
63173
|
.orderBy(asc(schema.sessionMcpServers.serverId));
|
|
62502
63174
|
const latestStarted = await latestStartedSessionTurnRow(scopedDb, workspaceId, sessionId);
|
|
62503
|
-
const
|
|
62504
|
-
|
|
63175
|
+
const variableSets = await Promise.all(
|
|
63176
|
+
((session.variableSetIds as string[]) ?? []).map(async (variableSetId) => {
|
|
63177
|
+
const variableSet = await getVariableSet(
|
|
62505
63178
|
scopedDb,
|
|
62506
63179
|
{
|
|
62507
63180
|
accountId: session.accountId,
|
|
@@ -62510,12 +63183,14 @@ export async function getScheduledTargetSessionExecution(
|
|
|
62510
63183
|
// user-owned sets resolve only for the frozen causal subject.
|
|
62511
63184
|
subjectId: authoritySubjectId ?? "scheduled-task",
|
|
62512
63185
|
},
|
|
62513
|
-
|
|
62514
|
-
)
|
|
62515
|
-
|
|
62516
|
-
|
|
62517
|
-
|
|
62518
|
-
|
|
63186
|
+
variableSetId,
|
|
63187
|
+
);
|
|
63188
|
+
if (!variableSet) {
|
|
63189
|
+
throw new Error(`scheduled target session Variable Set is unavailable: ${variableSetId}`);
|
|
63190
|
+
}
|
|
63191
|
+
return { id: variableSet.id, generation: variableSet.generation };
|
|
63192
|
+
}),
|
|
63193
|
+
);
|
|
62519
63194
|
const rigMetadata =
|
|
62520
63195
|
session.rigId && session.rigVersionId
|
|
62521
63196
|
? await getScheduledScopedRigVersionMetadata(
|
|
@@ -62573,8 +63248,9 @@ export async function getScheduledTargetSessionExecution(
|
|
|
62573
63248
|
mcpServerIds: targetMcpServers.map((server) => server.id),
|
|
62574
63249
|
effectiveMcpServerIds: targetMcpServers.map((server) => server.id),
|
|
62575
63250
|
toolPolicyVersion: session.toolPolicyVersion,
|
|
63251
|
+
variableSets,
|
|
62576
63252
|
variableSetId: session.variableSetId ?? null,
|
|
62577
|
-
variableSetGeneration:
|
|
63253
|
+
variableSetGeneration: variableSets.at(-1)?.generation ?? null,
|
|
62578
63254
|
rigId: session.rigId ?? null,
|
|
62579
63255
|
rigVersionId: session.rigVersionId ?? null,
|
|
62580
63256
|
rigDefaultVariableSets,
|
|
@@ -63357,6 +64033,93 @@ export async function claimPendingSessionSystemUpdateOutbox(
|
|
|
63357
64033
|
return deliveries;
|
|
63358
64034
|
}
|
|
63359
64035
|
|
|
64036
|
+
export type AutomaticSessionTitleFanoutDelivery = {
|
|
64037
|
+
outboxId: string;
|
|
64038
|
+
event: SessionEvent;
|
|
64039
|
+
};
|
|
64040
|
+
|
|
64041
|
+
/**
|
|
64042
|
+
* Claim migration-created title events for deployment-wide NATS fanout. The
|
|
64043
|
+
* function is SECURITY DEFINER because one control worker drains obligations
|
|
64044
|
+
* across workspaces; callers receive only the exact already-public event
|
|
64045
|
+
* projection, never arbitrary session rows.
|
|
64046
|
+
*/
|
|
64047
|
+
export async function claimAutomaticSessionTitleFanout(
|
|
64048
|
+
db: Database,
|
|
64049
|
+
limit = 100,
|
|
64050
|
+
): Promise<AutomaticSessionTitleFanoutDelivery[]> {
|
|
64051
|
+
const rows = await rawRows<{
|
|
64052
|
+
outbox_id: string;
|
|
64053
|
+
workspace_id: string;
|
|
64054
|
+
session_id: string;
|
|
64055
|
+
event_id: string;
|
|
64056
|
+
sequence: number;
|
|
64057
|
+
type: string;
|
|
64058
|
+
payload: unknown;
|
|
64059
|
+
payload_codec_version: number | null;
|
|
64060
|
+
occurred_at: Date | string;
|
|
64061
|
+
client_event_id: string | null;
|
|
64062
|
+
turn_id: string | null;
|
|
64063
|
+
turn_generation: number | null;
|
|
64064
|
+
turn_attempt_id: string | null;
|
|
64065
|
+
turn_association: string | null;
|
|
64066
|
+
duplicate_of_event_id: string | null;
|
|
64067
|
+
duplicate_reason: string | null;
|
|
64068
|
+
}>(db, sql`select * from opengeni_private.claim_automatic_session_title_fanout_v1(${limit})`);
|
|
64069
|
+
return rows.map((row) => ({
|
|
64070
|
+
outboxId: row.outbox_id,
|
|
64071
|
+
event: {
|
|
64072
|
+
id: row.event_id,
|
|
64073
|
+
workspaceId: row.workspace_id,
|
|
64074
|
+
sessionId: row.session_id,
|
|
64075
|
+
sequence: row.sequence,
|
|
64076
|
+
type: row.type as SessionEventType,
|
|
64077
|
+
payload: fromPostgresLosslessJson(row.payload, row.payload_codec_version),
|
|
64078
|
+
occurredAt:
|
|
64079
|
+
row.occurred_at instanceof Date
|
|
64080
|
+
? row.occurred_at.toISOString()
|
|
64081
|
+
: new Date(row.occurred_at).toISOString(),
|
|
64082
|
+
clientEventId: row.client_event_id,
|
|
64083
|
+
turnId: row.turn_id,
|
|
64084
|
+
turnGeneration: row.turn_generation,
|
|
64085
|
+
turnAttemptId: row.turn_attempt_id,
|
|
64086
|
+
turnAssociation: row.turn_association as SessionEvent["turnAssociation"],
|
|
64087
|
+
duplicateOfEventId: row.duplicate_of_event_id,
|
|
64088
|
+
duplicateReason: row.duplicate_reason,
|
|
64089
|
+
},
|
|
64090
|
+
}));
|
|
64091
|
+
}
|
|
64092
|
+
|
|
64093
|
+
export async function markAutomaticSessionTitleFanoutDelivered(
|
|
64094
|
+
db: Database,
|
|
64095
|
+
delivery: AutomaticSessionTitleFanoutDelivery,
|
|
64096
|
+
): Promise<boolean> {
|
|
64097
|
+
const [row] = await rawRows<{ changed: boolean }>(
|
|
64098
|
+
db,
|
|
64099
|
+
sql`select opengeni_private.mark_automatic_session_title_fanout_delivered_v1(
|
|
64100
|
+
${delivery.outboxId}::uuid,
|
|
64101
|
+
${delivery.event.id}::uuid
|
|
64102
|
+
) as changed`,
|
|
64103
|
+
);
|
|
64104
|
+
return row?.changed === true;
|
|
64105
|
+
}
|
|
64106
|
+
|
|
64107
|
+
export async function markAutomaticSessionTitleFanoutFailed(
|
|
64108
|
+
db: Database,
|
|
64109
|
+
delivery: AutomaticSessionTitleFanoutDelivery,
|
|
64110
|
+
error: string,
|
|
64111
|
+
): Promise<boolean> {
|
|
64112
|
+
const [row] = await rawRows<{ changed: boolean }>(
|
|
64113
|
+
db,
|
|
64114
|
+
sql`select opengeni_private.mark_automatic_session_title_fanout_failed_v1(
|
|
64115
|
+
${delivery.outboxId}::uuid,
|
|
64116
|
+
${delivery.event.id}::uuid,
|
|
64117
|
+
${error}
|
|
64118
|
+
) as changed`,
|
|
64119
|
+
);
|
|
64120
|
+
return row?.changed === true;
|
|
64121
|
+
}
|
|
64122
|
+
|
|
63360
64123
|
export type SessionWorkflowWake = {
|
|
63361
64124
|
accountId: string;
|
|
63362
64125
|
workspaceId: string;
|
|
@@ -65909,6 +66672,8 @@ function mapSession(
|
|
|
65909
66672
|
activeSandboxId: row.activeSandboxId ?? null,
|
|
65910
66673
|
activeEpoch: Number(row.activeEpoch),
|
|
65911
66674
|
workingDir: row.workingDir ?? null,
|
|
66675
|
+
variableSetIds:
|
|
66676
|
+
(row.variableSetIds as string[]) ?? (row.variableSetId ? [row.variableSetId] : []),
|
|
65912
66677
|
variableSetId: row.variableSetId,
|
|
65913
66678
|
environmentId: row.variableSetId,
|
|
65914
66679
|
// The rig + frozen rig version the session rides (M3). Both null for a
|
|
@@ -66420,13 +67185,35 @@ async function workspaceControlProjection(
|
|
|
66420
67185
|
});
|
|
66421
67186
|
}
|
|
66422
67187
|
|
|
67188
|
+
async function workspaceKindProjection(
|
|
67189
|
+
db: Database,
|
|
67190
|
+
row: Pick<typeof schema.workspaces.$inferSelect, "id" | "accountId">,
|
|
67191
|
+
): Promise<Workspace["kind"]> {
|
|
67192
|
+
return await withRlsContext(
|
|
67193
|
+
db,
|
|
67194
|
+
{ accountId: row.accountId, workspaceId: row.id },
|
|
67195
|
+
async (scopedDb) => {
|
|
67196
|
+
const [kind] = await rawRows<{ kind: string }>(
|
|
67197
|
+
scopedDb,
|
|
67198
|
+
sql`select get_workspace_kind(${row.accountId}::uuid, ${row.id}::uuid) as kind`,
|
|
67199
|
+
);
|
|
67200
|
+
if (kind?.kind !== "personal" && kind?.kind !== "shared") {
|
|
67201
|
+
throw new Error(`Workspace ${row.id} has no canonical kind`);
|
|
67202
|
+
}
|
|
67203
|
+
return kind.kind;
|
|
67204
|
+
},
|
|
67205
|
+
);
|
|
67206
|
+
}
|
|
67207
|
+
|
|
66423
67208
|
function mapWorkspace(
|
|
66424
67209
|
row: typeof schema.workspaces.$inferSelect,
|
|
66425
67210
|
inferenceControl: Workspace["inferenceControl"],
|
|
67211
|
+
kind: Workspace["kind"],
|
|
66426
67212
|
): Workspace {
|
|
66427
67213
|
return {
|
|
66428
67214
|
id: row.id,
|
|
66429
67215
|
accountId: row.accountId,
|
|
67216
|
+
kind,
|
|
66430
67217
|
name: row.name,
|
|
66431
67218
|
slug: row.slug,
|
|
66432
67219
|
externalSource: row.externalSource,
|