@opengeni/db 0.13.1 → 0.14.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-N4WTE6SB.js → chunk-PQKVUWQW.js} +20 -6
- package/dist/chunk-PQKVUWQW.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +817 -288
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +94 -13
- package/dist/{schema-D-hyrsFt.d.ts → schema-FoPub9yt.d.ts} +70 -15
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/drizzle/0132_connection_subject_isolation.sql +100 -0
- package/drizzle/0133_session_skills.sql +3 -0
- package/drizzle/0134_session_first_party_mcp_tools.sql +15 -0
- package/drizzle/0135_durable_machine_input_batches.sql +359 -0
- package/package.json +3 -3
- package/src/connection-token-resolver.ts +17 -7
- package/src/index.ts +1095 -344
- package/src/schema.ts +26 -6
- package/src/session-queue-commands.ts +22 -14
- package/dist/chunk-N4WTE6SB.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
FileAsset,
|
|
17
17
|
FileStatus,
|
|
18
18
|
FileUploadStatus,
|
|
19
|
+
FirstPartyMcpToolName,
|
|
19
20
|
HumanInputAnswer,
|
|
20
21
|
HumanInputQuestion,
|
|
21
22
|
HumanInputResponse,
|
|
@@ -59,6 +60,7 @@ import type {
|
|
|
59
60
|
SessionHumanInputRequest,
|
|
60
61
|
LineageNode,
|
|
61
62
|
SessionMcpApprovalPolicy,
|
|
63
|
+
SessionSkill,
|
|
62
64
|
SessionMcpServerMetadata,
|
|
63
65
|
SessionStatus,
|
|
64
66
|
SessionToolPolicy,
|
|
@@ -112,6 +114,7 @@ import {
|
|
|
112
114
|
RigChange as RigChangeContract,
|
|
113
115
|
SessionGoal as SessionGoalContract,
|
|
114
116
|
SessionSystemUpdatePayload,
|
|
117
|
+
sessionSystemUpdateBatchHistoryItem,
|
|
115
118
|
HostEventExport as HostEventExportContract,
|
|
116
119
|
HostEventExportBatch as HostEventExportBatchContract,
|
|
117
120
|
HostExportConsumerId,
|
|
@@ -1338,6 +1341,24 @@ export async function bootstrapWorkspace(
|
|
|
1338
1341
|
})
|
|
1339
1342
|
.where(eq(schema.workspaceMemberships.id, membership.id));
|
|
1340
1343
|
}
|
|
1344
|
+
// Access refreshes must retain workspaces created or granted after the
|
|
1345
|
+
// default workspace. Restore account-scoped RLS before listing them.
|
|
1346
|
+
await setRlsContext(tx as unknown as Database, {
|
|
1347
|
+
accountId: workspace.accountId,
|
|
1348
|
+
workspaceId: null,
|
|
1349
|
+
});
|
|
1350
|
+
const memberships = await tx
|
|
1351
|
+
.select({
|
|
1352
|
+
membership: schema.workspaceMemberships,
|
|
1353
|
+
workspace: schema.workspaces,
|
|
1354
|
+
})
|
|
1355
|
+
.from(schema.workspaceMemberships)
|
|
1356
|
+
.innerJoin(
|
|
1357
|
+
schema.workspaces,
|
|
1358
|
+
eq(schema.workspaceMemberships.workspaceId, schema.workspaces.id),
|
|
1359
|
+
)
|
|
1360
|
+
.where(eq(schema.workspaceMemberships.subjectId, input.subjectId))
|
|
1361
|
+
.orderBy(desc(schema.workspaces.createdAt));
|
|
1341
1362
|
return {
|
|
1342
1363
|
mode: input.accountExternalSource === "opengeni:local" ? "local" : "configured",
|
|
1343
1364
|
subjectId: input.subjectId,
|
|
@@ -1351,15 +1372,13 @@ export async function bootstrapWorkspace(
|
|
|
1351
1372
|
permissions: input.accountPermissions ?? allAccountPermissions,
|
|
1352
1373
|
},
|
|
1353
1374
|
],
|
|
1354
|
-
workspaceGrants:
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
},
|
|
1362
|
-
],
|
|
1375
|
+
workspaceGrants: memberships.map((row) => ({
|
|
1376
|
+
workspaceId: row.workspace.id,
|
|
1377
|
+
accountId: row.workspace.accountId,
|
|
1378
|
+
subjectId: input.subjectId,
|
|
1379
|
+
...(input.subjectLabel ? { subjectLabel: input.subjectLabel } : {}),
|
|
1380
|
+
permissions: row.membership.permissions as Permission[],
|
|
1381
|
+
})),
|
|
1363
1382
|
defaultAccountId: account.id,
|
|
1364
1383
|
defaultWorkspaceId: workspace.id,
|
|
1365
1384
|
};
|
|
@@ -4772,6 +4791,52 @@ function connectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
|
4772
4791
|
: isNull(schema.connections.subjectId);
|
|
4773
4792
|
}
|
|
4774
4793
|
|
|
4794
|
+
function connectionExactSubject(subjectId?: string | null): SQL {
|
|
4795
|
+
return subjectId
|
|
4796
|
+
? eq(schema.connections.subjectId, subjectId)
|
|
4797
|
+
: isNull(schema.connections.subjectId);
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
async function withConnectionSubjectRls<T>(
|
|
4801
|
+
db: Database,
|
|
4802
|
+
workspaceId: string,
|
|
4803
|
+
subjectId: string | null | undefined,
|
|
4804
|
+
fn: (db: Database) => Promise<T>,
|
|
4805
|
+
): Promise<T> {
|
|
4806
|
+
return subjectId
|
|
4807
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
|
|
4808
|
+
: await withWorkspaceRls(db, workspaceId, fn);
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
async function createConnectionInScope(
|
|
4812
|
+
db: Database,
|
|
4813
|
+
input: CreateConnectionInput,
|
|
4814
|
+
): Promise<ConnectionMetadataWithVerification> {
|
|
4815
|
+
const [row] = await db
|
|
4816
|
+
.insert(schema.connections)
|
|
4817
|
+
.values({
|
|
4818
|
+
accountId: input.accountId,
|
|
4819
|
+
workspaceId: input.workspaceId,
|
|
4820
|
+
subjectId: input.subjectId ?? null,
|
|
4821
|
+
providerDomain: input.providerDomain,
|
|
4822
|
+
kind: input.kind,
|
|
4823
|
+
status: input.status ?? "active",
|
|
4824
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
4825
|
+
grantedScopes: input.grantedScopes ?? [],
|
|
4826
|
+
expiresAt: input.expiresAt ?? null,
|
|
4827
|
+
verifiedInstallAt: input.verifiedInstallAt ?? null,
|
|
4828
|
+
verifiedInstallVersion: input.verifiedInstallVersion ?? null,
|
|
4829
|
+
metadata: input.metadata ?? {},
|
|
4830
|
+
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
4831
|
+
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
4832
|
+
})
|
|
4833
|
+
.returning(connectionMetadataColumns);
|
|
4834
|
+
if (!row) {
|
|
4835
|
+
throw new Error("Failed to create connection");
|
|
4836
|
+
}
|
|
4837
|
+
return mapConnectionMetadata(row);
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4775
4840
|
export async function createConnection(
|
|
4776
4841
|
db: Database,
|
|
4777
4842
|
input: CreateConnectionInput,
|
|
@@ -4780,29 +4845,10 @@ export async function createConnection(
|
|
|
4780
4845
|
db,
|
|
4781
4846
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
4782
4847
|
async (scopedDb) => {
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
.values({
|
|
4786
|
-
accountId: input.accountId,
|
|
4787
|
-
workspaceId: input.workspaceId,
|
|
4788
|
-
subjectId: input.subjectId ?? null,
|
|
4789
|
-
providerDomain: input.providerDomain,
|
|
4790
|
-
kind: input.kind,
|
|
4791
|
-
status: input.status ?? "active",
|
|
4792
|
-
credentialEncrypted: input.credentialEncrypted,
|
|
4793
|
-
grantedScopes: input.grantedScopes ?? [],
|
|
4794
|
-
expiresAt: input.expiresAt ?? null,
|
|
4795
|
-
verifiedInstallAt: input.verifiedInstallAt ?? null,
|
|
4796
|
-
verifiedInstallVersion: input.verifiedInstallVersion ?? null,
|
|
4797
|
-
metadata: input.metadata ?? {},
|
|
4798
|
-
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
4799
|
-
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
4800
|
-
})
|
|
4801
|
-
.returning(connectionMetadataColumns);
|
|
4802
|
-
if (!row) {
|
|
4803
|
-
throw new Error("Failed to create connection");
|
|
4848
|
+
if (input.subjectId) {
|
|
4849
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
4804
4850
|
}
|
|
4805
|
-
return
|
|
4851
|
+
return await createConnectionInScope(scopedDb, input);
|
|
4806
4852
|
},
|
|
4807
4853
|
);
|
|
4808
4854
|
}
|
|
@@ -4812,7 +4858,7 @@ export async function listConnectionsMetadata(
|
|
|
4812
4858
|
workspaceId: string,
|
|
4813
4859
|
subjectId?: string | null,
|
|
4814
4860
|
): Promise<ConnectionMetadataWithVerification[]> {
|
|
4815
|
-
return await
|
|
4861
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4816
4862
|
const rows = await scopedDb
|
|
4817
4863
|
.select(connectionMetadataColumns)
|
|
4818
4864
|
.from(schema.connections)
|
|
@@ -4833,7 +4879,7 @@ export async function getConnectionMetadata(
|
|
|
4833
4879
|
connectionId: string,
|
|
4834
4880
|
subjectId?: string | null,
|
|
4835
4881
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4836
|
-
return await
|
|
4882
|
+
return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
4837
4883
|
const [row] = await scopedDb
|
|
4838
4884
|
.select(connectionMetadataColumns)
|
|
4839
4885
|
.from(schema.connections)
|
|
@@ -4849,53 +4895,102 @@ export async function getConnectionMetadata(
|
|
|
4849
4895
|
});
|
|
4850
4896
|
}
|
|
4851
4897
|
|
|
4898
|
+
async function updateConnectionInScope(
|
|
4899
|
+
db: Database,
|
|
4900
|
+
input: UpdateConnectionInput,
|
|
4901
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4902
|
+
const set = {
|
|
4903
|
+
updatedAt: new Date(),
|
|
4904
|
+
...(input.providerDomain !== undefined ? { providerDomain: input.providerDomain } : {}),
|
|
4905
|
+
...(input.subjectId !== undefined ? { subjectId: input.subjectId } : {}),
|
|
4906
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
4907
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
4908
|
+
...(input.credentialEncrypted !== undefined
|
|
4909
|
+
? {
|
|
4910
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
4911
|
+
version: sql`${schema.connections.version} + 1`,
|
|
4912
|
+
lastError: null,
|
|
4913
|
+
}
|
|
4914
|
+
: {}),
|
|
4915
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
4916
|
+
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
4917
|
+
...(input.verifiedInstallAt !== undefined
|
|
4918
|
+
? { verifiedInstallAt: input.verifiedInstallAt }
|
|
4919
|
+
: {}),
|
|
4920
|
+
...(input.verifiedInstallVersion !== undefined
|
|
4921
|
+
? { verifiedInstallVersion: input.verifiedInstallVersion }
|
|
4922
|
+
: {}),
|
|
4923
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
4924
|
+
...(input.updatedBySubjectId !== undefined
|
|
4925
|
+
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
4926
|
+
: {}),
|
|
4927
|
+
};
|
|
4928
|
+
const [row] = await db
|
|
4929
|
+
.update(schema.connections)
|
|
4930
|
+
.set(set)
|
|
4931
|
+
.where(
|
|
4932
|
+
and(
|
|
4933
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
4934
|
+
eq(schema.connections.id, input.connectionId),
|
|
4935
|
+
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
4936
|
+
...(input.expectedVersion !== undefined
|
|
4937
|
+
? [eq(schema.connections.version, input.expectedVersion)]
|
|
4938
|
+
: []),
|
|
4939
|
+
),
|
|
4940
|
+
)
|
|
4941
|
+
.returning(connectionMetadataColumns);
|
|
4942
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4852
4945
|
export async function updateConnection(
|
|
4853
4946
|
db: Database,
|
|
4854
4947
|
input: UpdateConnectionInput,
|
|
4855
4948
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4856
|
-
return await
|
|
4857
|
-
|
|
4949
|
+
return await withConnectionSubjectRls(
|
|
4950
|
+
db,
|
|
4951
|
+
input.workspaceId,
|
|
4952
|
+
input.visibleToSubjectId,
|
|
4953
|
+
async (scopedDb) => await updateConnectionInScope(scopedDb, input),
|
|
4954
|
+
);
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
async function revokeConnectionInScope(
|
|
4958
|
+
db: Database,
|
|
4959
|
+
workspaceId: string,
|
|
4960
|
+
connectionId: string,
|
|
4961
|
+
updatedBySubjectId?: string | null,
|
|
4962
|
+
expectedVersion?: number,
|
|
4963
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4964
|
+
const [row] = await db
|
|
4965
|
+
.update(schema.connections)
|
|
4966
|
+
.set({
|
|
4967
|
+
status: "revoked",
|
|
4968
|
+
// The version bump invalidates any in-flight refresh's (id, version) CAS,
|
|
4969
|
+
// so a racing refresh cannot commit and flip the row back to active.
|
|
4970
|
+
version: sql`${schema.connections.version} + 1`,
|
|
4971
|
+
// Status-only revocation does not replace the verified credential or bot
|
|
4972
|
+
// identity. Carry the marker to the same new CAS version so the dedicated
|
|
4973
|
+
// reinstall path can still recognize (but not use) the inactive row.
|
|
4974
|
+
verifiedInstallVersion: sql`case
|
|
4975
|
+
when ${schema.connections.verifiedInstallAt} is null then null
|
|
4976
|
+
else ${schema.connections.version} + 1
|
|
4977
|
+
end`,
|
|
4978
|
+
updatedBySubjectId: updatedBySubjectId ?? null,
|
|
4858
4979
|
updatedAt: new Date(),
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
? { verifiedInstallAt: input.verifiedInstallAt }
|
|
4874
|
-
: {}),
|
|
4875
|
-
...(input.verifiedInstallVersion !== undefined
|
|
4876
|
-
? { verifiedInstallVersion: input.verifiedInstallVersion }
|
|
4877
|
-
: {}),
|
|
4878
|
-
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
4879
|
-
...(input.updatedBySubjectId !== undefined
|
|
4880
|
-
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
4881
|
-
: {}),
|
|
4882
|
-
};
|
|
4883
|
-
const [row] = await scopedDb
|
|
4884
|
-
.update(schema.connections)
|
|
4885
|
-
.set(set)
|
|
4886
|
-
.where(
|
|
4887
|
-
and(
|
|
4888
|
-
eq(schema.connections.workspaceId, input.workspaceId),
|
|
4889
|
-
eq(schema.connections.id, input.connectionId),
|
|
4890
|
-
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
4891
|
-
...(input.expectedVersion !== undefined
|
|
4892
|
-
? [eq(schema.connections.version, input.expectedVersion)]
|
|
4893
|
-
: []),
|
|
4894
|
-
),
|
|
4895
|
-
)
|
|
4896
|
-
.returning(connectionMetadataColumns);
|
|
4897
|
-
return row ? mapConnectionMetadata(row) : null;
|
|
4898
|
-
});
|
|
4980
|
+
})
|
|
4981
|
+
.where(
|
|
4982
|
+
and(
|
|
4983
|
+
eq(schema.connections.workspaceId, workspaceId),
|
|
4984
|
+
eq(schema.connections.id, connectionId),
|
|
4985
|
+
// Same visibility rule as get/update: shared rows plus the caller's own
|
|
4986
|
+
// subject rows. Cross-subject revocation (admin janitorial) arrives with
|
|
4987
|
+
// the subject-connections UX in I5, deliberately not before.
|
|
4988
|
+
connectionSubjectVisibility(updatedBySubjectId),
|
|
4989
|
+
...(expectedVersion !== undefined ? [eq(schema.connections.version, expectedVersion)] : []),
|
|
4990
|
+
),
|
|
4991
|
+
)
|
|
4992
|
+
.returning(connectionMetadataColumns);
|
|
4993
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
4899
4994
|
}
|
|
4900
4995
|
|
|
4901
4996
|
export async function revokeConnection(
|
|
@@ -4904,39 +4999,244 @@ export async function revokeConnection(
|
|
|
4904
4999
|
connectionId: string,
|
|
4905
5000
|
updatedBySubjectId?: string | null,
|
|
4906
5001
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
4907
|
-
return await
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
5002
|
+
return await withConnectionSubjectRls(
|
|
5003
|
+
db,
|
|
5004
|
+
workspaceId,
|
|
5005
|
+
updatedBySubjectId,
|
|
5006
|
+
async (scopedDb) =>
|
|
5007
|
+
await revokeConnectionInScope(scopedDb, workspaceId, connectionId, updatedBySubjectId),
|
|
5008
|
+
);
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
export class SlackBotLifecycleSuccessAuditError extends Error {
|
|
5012
|
+
constructor() {
|
|
5013
|
+
super("OpenGeni Slack bot lifecycle success audit failed");
|
|
5014
|
+
this.name = "SlackBotLifecycleSuccessAuditError";
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
type SlackBotLifecycleSuccessAuditInput = {
|
|
5019
|
+
accountId: string;
|
|
5020
|
+
workspaceId: string;
|
|
5021
|
+
subjectId: string;
|
|
5022
|
+
credentialRole: string;
|
|
5023
|
+
credentialLabel: string;
|
|
5024
|
+
slackTeamId: string;
|
|
5025
|
+
};
|
|
5026
|
+
|
|
5027
|
+
async function insertSlackBotLifecycleSuccessAuditInScope(
|
|
5028
|
+
db: Database,
|
|
5029
|
+
input: SlackBotLifecycleSuccessAuditInput & {
|
|
5030
|
+
action: "slack_bot.connected" | "slack_bot.reinstalled" | "slack_bot.disconnected";
|
|
5031
|
+
connectionId: string;
|
|
5032
|
+
},
|
|
5033
|
+
): Promise<void> {
|
|
5034
|
+
try {
|
|
5035
|
+
await db.insert(schema.auditEvents).values({
|
|
5036
|
+
accountId: input.accountId,
|
|
5037
|
+
workspaceId: input.workspaceId,
|
|
5038
|
+
subjectId: input.subjectId,
|
|
5039
|
+
action: input.action,
|
|
5040
|
+
targetType: "connection",
|
|
5041
|
+
targetId: input.connectionId,
|
|
5042
|
+
metadata: {
|
|
5043
|
+
credentialRole: input.credentialRole,
|
|
5044
|
+
credentialLabel: input.credentialLabel,
|
|
5045
|
+
connectionId: input.connectionId,
|
|
5046
|
+
slackTeamId: input.slackTeamId,
|
|
5047
|
+
outcome: "succeeded",
|
|
5048
|
+
},
|
|
5049
|
+
});
|
|
5050
|
+
} catch {
|
|
5051
|
+
// Do not leak a provider/database payload through the callback. Throwing from
|
|
5052
|
+
// the RLS transaction is what rolls the paired connection mutation back.
|
|
5053
|
+
throw new SlackBotLifecycleSuccessAuditError();
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
async function assertWorkspaceAccountPairInScope(
|
|
5058
|
+
db: Database,
|
|
5059
|
+
accountId: string,
|
|
5060
|
+
workspaceId: string,
|
|
5061
|
+
): Promise<void> {
|
|
5062
|
+
const [workspace] = await db
|
|
5063
|
+
.select({ id: schema.workspaces.id })
|
|
5064
|
+
.from(schema.workspaces)
|
|
5065
|
+
.where(and(eq(schema.workspaces.id, workspaceId), eq(schema.workspaces.accountId, accountId)))
|
|
5066
|
+
.limit(1);
|
|
5067
|
+
if (!workspace) {
|
|
5068
|
+
throw new Error("Workspace does not belong to the expected account");
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
async function withSlackBotLifecycleRls<T>(
|
|
5073
|
+
db: Database,
|
|
5074
|
+
input: Pick<SlackBotLifecycleSuccessAuditInput, "accountId" | "workspaceId" | "subjectId">,
|
|
5075
|
+
fn: (db: Database) => Promise<T>,
|
|
5076
|
+
): Promise<T> {
|
|
5077
|
+
return await withRlsContext(
|
|
5078
|
+
db,
|
|
5079
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
5080
|
+
async (scopedDb) => {
|
|
5081
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
5082
|
+
await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
|
|
5083
|
+
return await fn(scopedDb);
|
|
5084
|
+
},
|
|
5085
|
+
);
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
export async function createConnectionWithSlackBotSuccessAudit(
|
|
5089
|
+
db: Database,
|
|
5090
|
+
input: SlackBotLifecycleSuccessAuditInput & { connection: CreateConnectionInput },
|
|
5091
|
+
): Promise<ConnectionMetadataWithVerification> {
|
|
5092
|
+
if (
|
|
5093
|
+
input.connection.accountId !== input.accountId ||
|
|
5094
|
+
input.connection.workspaceId !== input.workspaceId
|
|
5095
|
+
) {
|
|
5096
|
+
throw new Error("Slack bot connection and lifecycle audit tenant must match");
|
|
5097
|
+
}
|
|
5098
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5099
|
+
const connection = await createConnectionInScope(scopedDb, input.connection);
|
|
5100
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5101
|
+
...input,
|
|
5102
|
+
action: "slack_bot.connected",
|
|
5103
|
+
connectionId: connection.id,
|
|
5104
|
+
});
|
|
5105
|
+
return connection;
|
|
4937
5106
|
});
|
|
4938
5107
|
}
|
|
4939
5108
|
|
|
5109
|
+
export async function updateConnectionWithSlackBotSuccessAudit(
|
|
5110
|
+
db: Database,
|
|
5111
|
+
input: SlackBotLifecycleSuccessAuditInput & { connection: UpdateConnectionInput },
|
|
5112
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5113
|
+
if (input.connection.workspaceId !== input.workspaceId) {
|
|
5114
|
+
throw new Error("Slack bot connection and lifecycle audit workspace must match");
|
|
5115
|
+
}
|
|
5116
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5117
|
+
const connection = await updateConnectionInScope(scopedDb, input.connection);
|
|
5118
|
+
if (!connection) return null;
|
|
5119
|
+
if (connection.accountId !== input.accountId) {
|
|
5120
|
+
throw new Error("Slack bot connection and lifecycle audit account must match");
|
|
5121
|
+
}
|
|
5122
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5123
|
+
...input,
|
|
5124
|
+
action: "slack_bot.reinstalled",
|
|
5125
|
+
connectionId: connection.id,
|
|
5126
|
+
});
|
|
5127
|
+
return connection;
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
export async function revokeConnectionWithSlackBotSuccessAudit(
|
|
5132
|
+
db: Database,
|
|
5133
|
+
input: SlackBotLifecycleSuccessAuditInput & {
|
|
5134
|
+
connectionId: string;
|
|
5135
|
+
expectedVersion: number;
|
|
5136
|
+
},
|
|
5137
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5138
|
+
return await withSlackBotLifecycleRls(db, input, async (scopedDb) => {
|
|
5139
|
+
const connection = await revokeConnectionInScope(
|
|
5140
|
+
scopedDb,
|
|
5141
|
+
input.workspaceId,
|
|
5142
|
+
input.connectionId,
|
|
5143
|
+
input.subjectId,
|
|
5144
|
+
input.expectedVersion,
|
|
5145
|
+
);
|
|
5146
|
+
if (!connection) return null;
|
|
5147
|
+
if (connection.accountId !== input.accountId) {
|
|
5148
|
+
throw new Error("Slack bot connection and lifecycle audit account must match");
|
|
5149
|
+
}
|
|
5150
|
+
await insertSlackBotLifecycleSuccessAuditInScope(scopedDb, {
|
|
5151
|
+
...input,
|
|
5152
|
+
action: "slack_bot.disconnected",
|
|
5153
|
+
connectionId: connection.id,
|
|
5154
|
+
});
|
|
5155
|
+
return connection;
|
|
5156
|
+
});
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
export type SlackBotInstallCallbackFailureStage =
|
|
5160
|
+
| "permission_check"
|
|
5161
|
+
| "nonce_consume"
|
|
5162
|
+
| "provider_denial"
|
|
5163
|
+
| "code_exchange"
|
|
5164
|
+
| "credential_verification"
|
|
5165
|
+
| "permission_recheck"
|
|
5166
|
+
| "principal_validation"
|
|
5167
|
+
| "persistence";
|
|
5168
|
+
|
|
5169
|
+
export type SlackBotInstallCallbackFailureReason =
|
|
5170
|
+
| "permission_lost"
|
|
5171
|
+
| "state_replayed"
|
|
5172
|
+
| "provider_denied"
|
|
5173
|
+
| "missing_code"
|
|
5174
|
+
| "exchange_failed"
|
|
5175
|
+
| "scope_mismatch"
|
|
5176
|
+
| "identity_mismatch"
|
|
5177
|
+
| "credential_verification_failed"
|
|
5178
|
+
| "connection_conflict"
|
|
5179
|
+
| "principal_mismatch"
|
|
5180
|
+
| "persistence_failed"
|
|
5181
|
+
| "success_audit_failed";
|
|
5182
|
+
|
|
5183
|
+
export async function recordSlackBotInstallCallbackFailure(
|
|
5184
|
+
db: Database,
|
|
5185
|
+
input: {
|
|
5186
|
+
accountId: string;
|
|
5187
|
+
workspaceId: string;
|
|
5188
|
+
subjectId: string;
|
|
5189
|
+
callbackDigest: string;
|
|
5190
|
+
installMode: "connect" | "reinstall";
|
|
5191
|
+
stage: SlackBotInstallCallbackFailureStage;
|
|
5192
|
+
reason: SlackBotInstallCallbackFailureReason;
|
|
5193
|
+
},
|
|
5194
|
+
): Promise<boolean> {
|
|
5195
|
+
if (!/^[a-f0-9]{64}$/.test(input.callbackDigest)) {
|
|
5196
|
+
throw new Error("Slack callback digest must be a lowercase SHA-256 value");
|
|
5197
|
+
}
|
|
5198
|
+
return await withRlsContext(
|
|
5199
|
+
db,
|
|
5200
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
5201
|
+
async (scopedDb) => {
|
|
5202
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
5203
|
+
await assertWorkspaceAccountPairInScope(scopedDb, input.accountId, input.workspaceId);
|
|
5204
|
+
await scopedDb.execute(
|
|
5205
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`slack-callback-failure:${input.workspaceId}:${input.callbackDigest}`}, 0))`,
|
|
5206
|
+
);
|
|
5207
|
+
const [existing] = await scopedDb
|
|
5208
|
+
.select({ id: schema.auditEvents.id })
|
|
5209
|
+
.from(schema.auditEvents)
|
|
5210
|
+
.where(
|
|
5211
|
+
and(
|
|
5212
|
+
eq(schema.auditEvents.accountId, input.accountId),
|
|
5213
|
+
eq(schema.auditEvents.workspaceId, input.workspaceId),
|
|
5214
|
+
eq(schema.auditEvents.action, "slack_bot.install.callback.failed"),
|
|
5215
|
+
eq(schema.auditEvents.targetType, "slack_oauth_callback"),
|
|
5216
|
+
eq(schema.auditEvents.targetId, input.callbackDigest),
|
|
5217
|
+
),
|
|
5218
|
+
)
|
|
5219
|
+
.limit(1);
|
|
5220
|
+
if (existing) return false;
|
|
5221
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
5222
|
+
accountId: input.accountId,
|
|
5223
|
+
workspaceId: input.workspaceId,
|
|
5224
|
+
subjectId: input.subjectId,
|
|
5225
|
+
action: "slack_bot.install.callback.failed",
|
|
5226
|
+
targetType: "slack_oauth_callback",
|
|
5227
|
+
targetId: input.callbackDigest,
|
|
5228
|
+
metadata: {
|
|
5229
|
+
outcome: "failed",
|
|
5230
|
+
installMode: input.installMode,
|
|
5231
|
+
stage: input.stage,
|
|
5232
|
+
reason: input.reason,
|
|
5233
|
+
},
|
|
5234
|
+
});
|
|
5235
|
+
return true;
|
|
5236
|
+
},
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
|
|
4940
5240
|
export type ClaimSlackBotPostOperationResult =
|
|
4941
5241
|
| { kind: "claimed" | "in_progress" | "completed"; operation: SlackBotPostOperation }
|
|
4942
5242
|
| { kind: "conflict" | "connection_not_found" };
|
|
@@ -5215,6 +5515,9 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5215
5515
|
allowSubjectOwned?: boolean;
|
|
5216
5516
|
},
|
|
5217
5517
|
): Promise<ConnectionCredentialForBroker | null> {
|
|
5518
|
+
if (input.allowSubjectOwned && !input.subjectId) {
|
|
5519
|
+
return null;
|
|
5520
|
+
}
|
|
5218
5521
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
5219
5522
|
if (!key) {
|
|
5220
5523
|
throw new Error(
|
|
@@ -5222,7 +5525,7 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5222
5525
|
);
|
|
5223
5526
|
}
|
|
5224
5527
|
const subjectPredicate = input.allowSubjectOwned
|
|
5225
|
-
?
|
|
5528
|
+
? connectionExactSubject(input.subjectId)
|
|
5226
5529
|
: isNull(schema.connections.subjectId);
|
|
5227
5530
|
const conditions: SQL[] = [
|
|
5228
5531
|
eq(schema.connections.workspaceId, input.workspaceId),
|
|
@@ -5236,49 +5539,54 @@ export async function loadConnectionCredentialForBroker(
|
|
|
5236
5539
|
conditions.push(eq(schema.connections.kind, input.kind));
|
|
5237
5540
|
}
|
|
5238
5541
|
}
|
|
5239
|
-
return await
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
.
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
)
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5542
|
+
return await withConnectionSubjectRls(
|
|
5543
|
+
db,
|
|
5544
|
+
input.workspaceId,
|
|
5545
|
+
input.allowSubjectOwned ? input.subjectId : null,
|
|
5546
|
+
async (scopedDb) => {
|
|
5547
|
+
// Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
|
|
5548
|
+
// freshly revoked connection shadow an active replacement for the provider.
|
|
5549
|
+
const [row] = await scopedDb
|
|
5550
|
+
.select()
|
|
5551
|
+
.from(schema.connections)
|
|
5552
|
+
.where(and(...conditions))
|
|
5553
|
+
.orderBy(
|
|
5554
|
+
desc(sql`(${schema.connections.status} = 'active')`),
|
|
5555
|
+
desc(schema.connections.updatedAt),
|
|
5556
|
+
)
|
|
5557
|
+
.limit(1);
|
|
5558
|
+
if (!row) {
|
|
5559
|
+
return null;
|
|
5560
|
+
}
|
|
5561
|
+
let credential: unknown;
|
|
5562
|
+
try {
|
|
5563
|
+
credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
|
|
5564
|
+
} catch (error) {
|
|
5565
|
+
throw new Error(
|
|
5566
|
+
`connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
5567
|
+
{ cause: error },
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
|
|
5571
|
+
throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
|
|
5572
|
+
}
|
|
5573
|
+
return {
|
|
5574
|
+
id: row.id,
|
|
5575
|
+
accountId: row.accountId,
|
|
5576
|
+
workspaceId: row.workspaceId,
|
|
5577
|
+
subjectId: row.subjectId,
|
|
5578
|
+
providerDomain: row.providerDomain,
|
|
5579
|
+
kind: row.kind as ConnectionKind,
|
|
5580
|
+
status: row.status as ConnectionStatus,
|
|
5581
|
+
credential: credential as Record<string, unknown>,
|
|
5582
|
+
grantedScopes: row.grantedScopes,
|
|
5583
|
+
expiresAt: row.expiresAt,
|
|
5584
|
+
lastRefreshAt: row.lastRefreshAt,
|
|
5585
|
+
version: row.version,
|
|
5586
|
+
metadata: row.metadata,
|
|
5587
|
+
};
|
|
5588
|
+
},
|
|
5589
|
+
);
|
|
5282
5590
|
}
|
|
5283
5591
|
|
|
5284
5592
|
export async function recordConnectionTokenRefresh(
|
|
@@ -5291,35 +5599,42 @@ export async function recordConnectionTokenRefresh(
|
|
|
5291
5599
|
expiresAt: Date | null;
|
|
5292
5600
|
grantedScopes?: string[];
|
|
5293
5601
|
lastRefreshAt: Date;
|
|
5602
|
+
subjectId?: string | null;
|
|
5294
5603
|
},
|
|
5295
5604
|
): Promise<boolean> {
|
|
5296
|
-
return await
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5605
|
+
return await withConnectionSubjectRls(
|
|
5606
|
+
db,
|
|
5607
|
+
input.workspaceId,
|
|
5608
|
+
input.subjectId,
|
|
5609
|
+
async (scopedDb) => {
|
|
5610
|
+
const set = {
|
|
5611
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
5612
|
+
expiresAt: input.expiresAt,
|
|
5613
|
+
lastRefreshAt: input.lastRefreshAt,
|
|
5614
|
+
status: "active",
|
|
5615
|
+
lastError: null,
|
|
5616
|
+
version: sql`${schema.connections.version} + 1`,
|
|
5617
|
+
updatedAt: new Date(),
|
|
5618
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
5619
|
+
};
|
|
5620
|
+
const updated = await scopedDb
|
|
5621
|
+
.update(schema.connections)
|
|
5622
|
+
.set(set)
|
|
5623
|
+
.where(
|
|
5624
|
+
and(
|
|
5625
|
+
eq(schema.connections.id, input.id),
|
|
5626
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
5627
|
+
eq(schema.connections.version, input.version),
|
|
5628
|
+
connectionExactSubject(input.subjectId),
|
|
5629
|
+
// A refresh may only ever renew a live credential; revoked/errored rows
|
|
5630
|
+
// stay dead even if a status change somewhere forgot to bump version.
|
|
5631
|
+
eq(schema.connections.status, "active"),
|
|
5632
|
+
),
|
|
5633
|
+
)
|
|
5634
|
+
.returning({ id: schema.connections.id });
|
|
5635
|
+
return updated.length > 0;
|
|
5636
|
+
},
|
|
5637
|
+
);
|
|
5323
5638
|
}
|
|
5324
5639
|
|
|
5325
5640
|
export async function setConnectionStatus(
|
|
@@ -5327,9 +5642,9 @@ export async function setConnectionStatus(
|
|
|
5327
5642
|
workspaceId: string,
|
|
5328
5643
|
status: ConnectionStatus,
|
|
5329
5644
|
lastError: string | null,
|
|
5330
|
-
guard: { id: string; version: number },
|
|
5645
|
+
guard: { id: string; version: number; subjectId?: string | null },
|
|
5331
5646
|
): Promise<boolean> {
|
|
5332
|
-
return await
|
|
5647
|
+
return await withConnectionSubjectRls(db, workspaceId, guard.subjectId, async (scopedDb) => {
|
|
5333
5648
|
const updated = await scopedDb
|
|
5334
5649
|
.update(schema.connections)
|
|
5335
5650
|
.set({
|
|
@@ -5347,6 +5662,7 @@ export async function setConnectionStatus(
|
|
|
5347
5662
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5348
5663
|
eq(schema.connections.id, guard.id),
|
|
5349
5664
|
eq(schema.connections.version, guard.version),
|
|
5665
|
+
connectionExactSubject(guard.subjectId),
|
|
5350
5666
|
),
|
|
5351
5667
|
)
|
|
5352
5668
|
.returning({ id: schema.connections.id });
|
|
@@ -5358,8 +5674,9 @@ export async function recordConnectionUsed(
|
|
|
5358
5674
|
db: Database,
|
|
5359
5675
|
workspaceId: string,
|
|
5360
5676
|
connectionId: string,
|
|
5677
|
+
subjectId?: string | null,
|
|
5361
5678
|
): Promise<void> {
|
|
5362
|
-
await
|
|
5679
|
+
await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
5363
5680
|
await scopedDb
|
|
5364
5681
|
.update(schema.connections)
|
|
5365
5682
|
.set({
|
|
@@ -5370,6 +5687,7 @@ export async function recordConnectionUsed(
|
|
|
5370
5687
|
and(
|
|
5371
5688
|
eq(schema.connections.workspaceId, workspaceId),
|
|
5372
5689
|
eq(schema.connections.id, connectionId),
|
|
5690
|
+
connectionExactSubject(subjectId),
|
|
5373
5691
|
),
|
|
5374
5692
|
);
|
|
5375
5693
|
});
|
|
@@ -13559,6 +13877,7 @@ export type SessionCreateInput = {
|
|
|
13559
13877
|
initialMessage: string;
|
|
13560
13878
|
initialTurnInstructions?: string | null;
|
|
13561
13879
|
resources: ResourceRef[];
|
|
13880
|
+
skills?: SessionSkill[];
|
|
13562
13881
|
tools?: ToolRef[];
|
|
13563
13882
|
toolPolicy?: SessionToolPolicy | null;
|
|
13564
13883
|
metadata: Record<string, unknown>;
|
|
@@ -13571,6 +13890,7 @@ export type SessionCreateInput = {
|
|
|
13571
13890
|
rigId?: string | null;
|
|
13572
13891
|
rigVersionId?: string | null;
|
|
13573
13892
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
13893
|
+
firstPartyMcpTools?: FirstPartyMcpToolName[] | null;
|
|
13574
13894
|
instructions?: string | null;
|
|
13575
13895
|
parentSessionId?: string | null;
|
|
13576
13896
|
createIdempotencyKey?: string | null;
|
|
@@ -13958,6 +14278,7 @@ async function createSessionInTransaction(
|
|
|
13958
14278
|
initialMessage: input.initialMessage,
|
|
13959
14279
|
initialTurnInstructions: input.initialTurnInstructions ?? null,
|
|
13960
14280
|
resources: input.resources,
|
|
14281
|
+
skills: input.skills ?? [],
|
|
13961
14282
|
tools: input.tools ?? [],
|
|
13962
14283
|
toolPolicy: input.toolPolicy ?? null,
|
|
13963
14284
|
metadata: input.metadata,
|
|
@@ -13970,6 +14291,7 @@ async function createSessionInTransaction(
|
|
|
13970
14291
|
rigId: input.rigId ?? null,
|
|
13971
14292
|
rigVersionId: input.rigVersionId ?? null,
|
|
13972
14293
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
14294
|
+
firstPartyMcpTools: input.firstPartyMcpTools ?? null,
|
|
13973
14295
|
instructions: input.instructions ?? null,
|
|
13974
14296
|
parentSessionId: input.parentSessionId ?? null,
|
|
13975
14297
|
createIdempotencyKey,
|
|
@@ -15971,13 +16293,16 @@ type LineageIdRow = {
|
|
|
15971
16293
|
parentSessionId: string | null;
|
|
15972
16294
|
depth: number;
|
|
15973
16295
|
path: string[];
|
|
16296
|
+
cycle: boolean;
|
|
15974
16297
|
};
|
|
15975
16298
|
|
|
15976
16299
|
/**
|
|
15977
16300
|
* Read the full lineage slice around a session. Every recursive step carries
|
|
15978
16301
|
* workspace_id as a hard predicate; a foreign parent/child id is invisible even
|
|
15979
|
-
* before RLS is considered.
|
|
15980
|
-
*
|
|
16302
|
+
* before RLS is considered. Up to 63 ancestors are returned root-first; an
|
|
16303
|
+
* invalid, cyclic, foreign, or deeper chain fails closed instead of presenting
|
|
16304
|
+
* a partial path as if it were rooted. Descendants are capped at depth 5 and
|
|
16305
|
+
* 200 total rows, returned as a nested tree.
|
|
15981
16306
|
*/
|
|
15982
16307
|
export async function getSessionLineage(
|
|
15983
16308
|
db: Database,
|
|
@@ -15999,25 +16324,39 @@ export async function getSessionLineage(
|
|
|
15999
16324
|
return null;
|
|
16000
16325
|
}
|
|
16001
16326
|
|
|
16002
|
-
const
|
|
16003
|
-
with recursive ancestors(id, parent_session_id, depth, path) as (
|
|
16004
|
-
select ${schema.sessions.id}, ${schema.sessions.parentSessionId}, 0, array[${schema.sessions.id}]
|
|
16327
|
+
const ancestorLineageRows = (await scopedDb.execute(sql<LineageIdRow>`
|
|
16328
|
+
with recursive ancestors(id, parent_session_id, depth, path, cycle) as (
|
|
16329
|
+
select ${schema.sessions.id}, ${schema.sessions.parentSessionId}, 0, array[${schema.sessions.id}], false
|
|
16005
16330
|
from ${schema.sessions}
|
|
16006
16331
|
where ${schema.sessions.workspaceId} = ${workspaceId}
|
|
16007
16332
|
and ${schema.sessions.id} = ${sessionId}
|
|
16008
16333
|
union all
|
|
16009
|
-
select
|
|
16334
|
+
select
|
|
16335
|
+
parent.id,
|
|
16336
|
+
parent.parent_session_id,
|
|
16337
|
+
ancestors.depth + 1,
|
|
16338
|
+
ancestors.path || parent.id,
|
|
16339
|
+
parent.id = any(ancestors.path)
|
|
16010
16340
|
from ${schema.sessions} parent
|
|
16011
16341
|
join ancestors on ancestors.parent_session_id = parent.id
|
|
16012
16342
|
where parent.workspace_id = ${workspaceId}
|
|
16013
|
-
and ancestors.
|
|
16014
|
-
and
|
|
16343
|
+
and not ancestors.cycle
|
|
16344
|
+
and ancestors.depth < 64
|
|
16015
16345
|
)
|
|
16016
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16346
|
+
select id, parent_session_id as "parentSessionId", depth, path, cycle
|
|
16017
16347
|
from ancestors
|
|
16018
|
-
where depth > 0
|
|
16019
16348
|
order by depth desc
|
|
16020
16349
|
`)) as LineageIdRow[];
|
|
16350
|
+
const frontier = ancestorLineageRows[0];
|
|
16351
|
+
if (
|
|
16352
|
+
!frontier ||
|
|
16353
|
+
frontier.cycle ||
|
|
16354
|
+
frontier.parentSessionId !== null ||
|
|
16355
|
+
Number(frontier.depth) >= 64
|
|
16356
|
+
) {
|
|
16357
|
+
throw new Error(`session lineage for ${sessionId} has no valid workspace root`);
|
|
16358
|
+
}
|
|
16359
|
+
const ancestorRows = ancestorLineageRows.filter((row) => row.depth > 0);
|
|
16021
16360
|
|
|
16022
16361
|
const childRows = (await scopedDb.execute(sql<LineageIdRow>`
|
|
16023
16362
|
with recursive descendants(id, parent_session_id, depth, path) as (
|
|
@@ -16033,7 +16372,7 @@ export async function getSessionLineage(
|
|
|
16033
16372
|
and descendants.depth < 5
|
|
16034
16373
|
and not child.id = any(descendants.path)
|
|
16035
16374
|
)
|
|
16036
|
-
select id, parent_session_id as "parentSessionId", depth, path
|
|
16375
|
+
select id, parent_session_id as "parentSessionId", depth, path, false as cycle
|
|
16037
16376
|
from descendants
|
|
16038
16377
|
order by path
|
|
16039
16378
|
limit ${descendantLimit + 1}
|
|
@@ -28650,7 +28989,7 @@ export async function materializeGoalContinuation(
|
|
|
28650
28989
|
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
28651
28990
|
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
28652
28991
|
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
28653
|
-
|
|
28992
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
28654
28993
|
),
|
|
28655
28994
|
)
|
|
28656
28995
|
.limit(1);
|
|
@@ -28813,6 +29152,7 @@ export async function materializeGoalContinuation(
|
|
|
28813
29152
|
})
|
|
28814
29153
|
.onConflictDoNothing({ target: schema.usageEvents.idempotencyKey });
|
|
28815
29154
|
|
|
29155
|
+
const eventPreview = internalUpdateEventMember(update);
|
|
28816
29156
|
const insertedEvents = await tx
|
|
28817
29157
|
.insert(schema.sessionEvents)
|
|
28818
29158
|
.values([
|
|
@@ -28823,11 +29163,13 @@ export async function materializeGoalContinuation(
|
|
|
28823
29163
|
sequence: session.lastSequence + 1,
|
|
28824
29164
|
type: "system.update.pending",
|
|
28825
29165
|
payload: sanitizeEventPayload({
|
|
28826
|
-
updateId:
|
|
28827
|
-
kind:
|
|
28828
|
-
classification:
|
|
28829
|
-
sourceId:
|
|
28830
|
-
|
|
29166
|
+
updateId: eventPreview.id,
|
|
29167
|
+
kind: eventPreview.kind,
|
|
29168
|
+
classification: eventPreview.classification,
|
|
29169
|
+
sourceId: eventPreview.sourceId,
|
|
29170
|
+
sourceIdTruncated: eventPreview.sourceIdTruncated,
|
|
29171
|
+
summary: eventPreview.summary,
|
|
29172
|
+
summaryTruncated: eventPreview.summaryTruncated,
|
|
28831
29173
|
}),
|
|
28832
29174
|
occurredAt: now,
|
|
28833
29175
|
},
|
|
@@ -29328,6 +29670,79 @@ export type SessionWorkTrigger = { kind: "next" } | { kind: "approval"; triggerE
|
|
|
29328
29670
|
export const MAX_INTERNAL_UPDATE_BYTES = 64 * 1024;
|
|
29329
29671
|
export const MAX_INTERNAL_UPDATE_BATCH_MEMBERS = 100;
|
|
29330
29672
|
export const MAX_INTERNAL_UPDATE_BATCH_BYTES = 256 * 1024;
|
|
29673
|
+
const MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES = 512;
|
|
29674
|
+
const MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES = 256;
|
|
29675
|
+
|
|
29676
|
+
type BoundedSystemUpdate = Pick<
|
|
29677
|
+
typeof schema.sessionSystemUpdates.$inferSelect,
|
|
29678
|
+
"id" | "kind" | "classification" | "sourceId" | "summary" | "payload" | "lineage"
|
|
29679
|
+
>;
|
|
29680
|
+
|
|
29681
|
+
function boundedInternalUpdateEventText(
|
|
29682
|
+
value: string,
|
|
29683
|
+
maxBytes: number,
|
|
29684
|
+
): {
|
|
29685
|
+
text: string;
|
|
29686
|
+
truncated: boolean;
|
|
29687
|
+
} {
|
|
29688
|
+
if (Buffer.byteLength(value) <= maxBytes) return { text: value, truncated: false };
|
|
29689
|
+
const suffix = "…";
|
|
29690
|
+
const bodyBudget = maxBytes - Buffer.byteLength(suffix);
|
|
29691
|
+
const bytes = Buffer.from(value);
|
|
29692
|
+
let text = bytes.subarray(0, bodyBudget).toString("utf8");
|
|
29693
|
+
if (text.endsWith("\uFFFD")) text = text.slice(0, -1);
|
|
29694
|
+
return { text: `${text}${suffix}`, truncated: true };
|
|
29695
|
+
}
|
|
29696
|
+
|
|
29697
|
+
function internalUpdateEventMember(update: BoundedSystemUpdate) {
|
|
29698
|
+
const summary = boundedInternalUpdateEventText(
|
|
29699
|
+
update.summary,
|
|
29700
|
+
MAX_INTERNAL_UPDATE_EVENT_SUMMARY_BYTES,
|
|
29701
|
+
);
|
|
29702
|
+
const source = boundedInternalUpdateEventText(
|
|
29703
|
+
update.sourceId,
|
|
29704
|
+
MAX_INTERNAL_UPDATE_EVENT_SOURCE_BYTES,
|
|
29705
|
+
);
|
|
29706
|
+
return {
|
|
29707
|
+
id: update.id,
|
|
29708
|
+
kind: update.kind,
|
|
29709
|
+
classification: update.classification,
|
|
29710
|
+
sourceId: source.text,
|
|
29711
|
+
sourceIdTruncated: source.truncated,
|
|
29712
|
+
summary: summary.text,
|
|
29713
|
+
summaryTruncated: summary.truncated,
|
|
29714
|
+
};
|
|
29715
|
+
}
|
|
29716
|
+
|
|
29717
|
+
function selectBoundedSystemUpdateBatch<T extends BoundedSystemUpdate>(updates: readonly T[]): T[] {
|
|
29718
|
+
const selected: T[] = [];
|
|
29719
|
+
let selectedBytes = 0;
|
|
29720
|
+
for (const update of updates) {
|
|
29721
|
+
const updateBytes = Buffer.byteLength(
|
|
29722
|
+
JSON.stringify({
|
|
29723
|
+
id: update.id,
|
|
29724
|
+
kind: update.kind,
|
|
29725
|
+
classification: update.classification,
|
|
29726
|
+
sourceId: update.sourceId,
|
|
29727
|
+
summary: update.summary,
|
|
29728
|
+
payload: update.payload,
|
|
29729
|
+
lineage: update.lineage,
|
|
29730
|
+
}),
|
|
29731
|
+
);
|
|
29732
|
+
if (
|
|
29733
|
+
selected.length >= MAX_INTERNAL_UPDATE_BATCH_MEMBERS ||
|
|
29734
|
+
// One individually large canonical input must still make progress. The
|
|
29735
|
+
// model/context boundary may reject it explicitly, but the queue cannot
|
|
29736
|
+
// wedge forever merely because the coalescing target is smaller.
|
|
29737
|
+
(selected.length > 0 && selectedBytes + updateBytes > MAX_INTERNAL_UPDATE_BATCH_BYTES)
|
|
29738
|
+
) {
|
|
29739
|
+
break;
|
|
29740
|
+
}
|
|
29741
|
+
selected.push(update);
|
|
29742
|
+
selectedBytes += updateBytes;
|
|
29743
|
+
}
|
|
29744
|
+
return selected;
|
|
29745
|
+
}
|
|
29331
29746
|
|
|
29332
29747
|
export type ClaimSessionWorkForAttemptInput = {
|
|
29333
29748
|
sessionId: string;
|
|
@@ -29373,7 +29788,10 @@ export async function claimSessionWorkForAttempt(
|
|
|
29373
29788
|
count: number;
|
|
29374
29789
|
lastSequence: number;
|
|
29375
29790
|
triggerEventId: string | null;
|
|
29791
|
+
historyItemId: string | null;
|
|
29792
|
+
historyItem: Record<string, unknown> | null;
|
|
29376
29793
|
updates: Array<typeof schema.sessionSystemUpdates.$inferSelect>;
|
|
29794
|
+
events: Array<typeof schema.sessionEvents.$inferInsert>;
|
|
29377
29795
|
event: typeof schema.sessionEvents.$inferInsert | null;
|
|
29378
29796
|
}> => {
|
|
29379
29797
|
const [agentSteer] = await tx
|
|
@@ -29383,7 +29801,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
29383
29801
|
and(
|
|
29384
29802
|
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
29385
29803
|
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
29386
|
-
|
|
29804
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
29387
29805
|
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
29388
29806
|
),
|
|
29389
29807
|
)
|
|
@@ -29393,20 +29811,6 @@ export async function claimSessionWorkForAttempt(
|
|
|
29393
29811
|
)
|
|
29394
29812
|
.limit(1)
|
|
29395
29813
|
.for("update");
|
|
29396
|
-
if (agentSteer) {
|
|
29397
|
-
await tx
|
|
29398
|
-
.update(schema.sessionSystemUpdates)
|
|
29399
|
-
.set({ state: "superseded" })
|
|
29400
|
-
.where(
|
|
29401
|
-
and(
|
|
29402
|
-
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
29403
|
-
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
29404
|
-
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
29405
|
-
inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
|
|
29406
|
-
ne(schema.sessionSystemUpdates.id, agentSteer.id),
|
|
29407
|
-
),
|
|
29408
|
-
);
|
|
29409
|
-
}
|
|
29410
29814
|
const ordinary = await tx
|
|
29411
29815
|
.select()
|
|
29412
29816
|
.from(schema.sessionSystemUpdates)
|
|
@@ -29414,7 +29818,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
29414
29818
|
and(
|
|
29415
29819
|
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
29416
29820
|
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
29417
|
-
|
|
29821
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
29418
29822
|
ne(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
29419
29823
|
),
|
|
29420
29824
|
)
|
|
@@ -29430,12 +29834,15 @@ export async function claimSessionWorkForAttempt(
|
|
|
29430
29834
|
count: 0,
|
|
29431
29835
|
lastSequence: nextSequence - 1,
|
|
29432
29836
|
triggerEventId: null,
|
|
29837
|
+
historyItemId: null,
|
|
29838
|
+
historyItem: null,
|
|
29433
29839
|
updates: [],
|
|
29840
|
+
events: [],
|
|
29434
29841
|
event: null,
|
|
29435
29842
|
};
|
|
29436
29843
|
}
|
|
29437
|
-
const
|
|
29438
|
-
|
|
29844
|
+
const validUpdates: typeof updates = [];
|
|
29845
|
+
const cancelledUpdateIds: string[] = [];
|
|
29439
29846
|
for (const update of updates) {
|
|
29440
29847
|
const payload = update.payload;
|
|
29441
29848
|
if (payload.type === "goal_continuation") {
|
|
@@ -29464,43 +29871,66 @@ export async function claimSessionWorkForAttempt(
|
|
|
29464
29871
|
.update(schema.sessionSystemUpdates)
|
|
29465
29872
|
.set({ state: "cancelled" })
|
|
29466
29873
|
.where(eq(schema.sessionSystemUpdates.id, update.id));
|
|
29874
|
+
cancelledUpdateIds.push(update.id);
|
|
29467
29875
|
continue;
|
|
29468
29876
|
}
|
|
29469
29877
|
}
|
|
29470
|
-
|
|
29471
|
-
JSON.stringify({
|
|
29472
|
-
id: update.id,
|
|
29473
|
-
kind: update.kind,
|
|
29474
|
-
classification: update.classification,
|
|
29475
|
-
sourceId: update.sourceId,
|
|
29476
|
-
summary: update.summary,
|
|
29477
|
-
payload: update.payload,
|
|
29478
|
-
lineage: update.lineage,
|
|
29479
|
-
}),
|
|
29480
|
-
);
|
|
29481
|
-
if (
|
|
29482
|
-
deliverable.length >= MAX_INTERNAL_UPDATE_BATCH_MEMBERS ||
|
|
29483
|
-
deliveredBytes + updateBytes > MAX_INTERNAL_UPDATE_BATCH_BYTES
|
|
29484
|
-
) {
|
|
29485
|
-
break;
|
|
29486
|
-
}
|
|
29487
|
-
deliverable.push(update);
|
|
29488
|
-
deliveredBytes += updateBytes;
|
|
29878
|
+
validUpdates.push(update);
|
|
29489
29879
|
}
|
|
29880
|
+
const deliverable = selectBoundedSystemUpdateBatch(validUpdates);
|
|
29490
29881
|
if (deliverable.length === 0) {
|
|
29882
|
+
const cancellationEvent =
|
|
29883
|
+
cancelledUpdateIds.length > 0
|
|
29884
|
+
? {
|
|
29885
|
+
accountId,
|
|
29886
|
+
workspaceId,
|
|
29887
|
+
sessionId,
|
|
29888
|
+
// No receiving turn exists when every candidate was
|
|
29889
|
+
// cancelled before a model batch could be persisted.
|
|
29890
|
+
turnId: null,
|
|
29891
|
+
turnGeneration: null,
|
|
29892
|
+
turnAttemptId: null,
|
|
29893
|
+
turnAssociation: null,
|
|
29894
|
+
sequence: nextSequence,
|
|
29895
|
+
type: "system.update.cancelled" as const,
|
|
29896
|
+
payload: sanitizeEventPayload({
|
|
29897
|
+
updateIds: cancelledUpdateIds,
|
|
29898
|
+
count: cancelledUpdateIds.length,
|
|
29899
|
+
reason: "stale_goal_continuation",
|
|
29900
|
+
}),
|
|
29901
|
+
occurredAt,
|
|
29902
|
+
}
|
|
29903
|
+
: null;
|
|
29491
29904
|
return {
|
|
29492
29905
|
count: 0,
|
|
29493
|
-
lastSequence: nextSequence - 1,
|
|
29906
|
+
lastSequence: cancellationEvent ? nextSequence : nextSequence - 1,
|
|
29494
29907
|
triggerEventId: null,
|
|
29908
|
+
historyItemId: null,
|
|
29909
|
+
historyItem: null,
|
|
29495
29910
|
updates: [],
|
|
29911
|
+
events: cancellationEvent ? [cancellationEvent] : [],
|
|
29496
29912
|
event: null,
|
|
29497
29913
|
};
|
|
29498
29914
|
}
|
|
29915
|
+
// Inclusion gives the newest Steer first refusal on the bounded
|
|
29916
|
+
// batch. Model ordering is deliberately the opposite: ordinary
|
|
29917
|
+
// updates establish context, then the authoritative replacement
|
|
29918
|
+
// direction is last so it cannot be overridden by an older goal or
|
|
29919
|
+
// lifecycle notice.
|
|
29920
|
+
const modelOrdered = [
|
|
29921
|
+
...deliverable.filter((update) => update.kind !== "agent_steer_instruction"),
|
|
29922
|
+
...deliverable.filter((update) => update.kind === "agent_steer_instruction"),
|
|
29923
|
+
];
|
|
29924
|
+
const historyItemId = crypto.randomUUID();
|
|
29925
|
+
const historyItem = sessionSystemUpdateBatchHistoryItem(
|
|
29926
|
+
modelOrdered.map((update) => mapSessionSystemUpdate(update)),
|
|
29927
|
+
) as Record<string, unknown>;
|
|
29499
29928
|
await tx
|
|
29500
29929
|
.update(schema.sessionSystemUpdates)
|
|
29501
29930
|
.set({
|
|
29502
29931
|
state: "delivered",
|
|
29503
29932
|
deliveredTurnId: turnId,
|
|
29933
|
+
deliveredHistoryItemId: historyItemId,
|
|
29504
29934
|
deliveredAt: occurredAt,
|
|
29505
29935
|
})
|
|
29506
29936
|
.where(
|
|
@@ -29514,6 +29944,27 @@ export async function claimSessionWorkForAttempt(
|
|
|
29514
29944
|
),
|
|
29515
29945
|
);
|
|
29516
29946
|
const eventId = triggerEventId ?? crypto.randomUUID();
|
|
29947
|
+
let sequence = nextSequence - 1;
|
|
29948
|
+
const events: Array<typeof schema.sessionEvents.$inferInsert> = [];
|
|
29949
|
+
if (cancelledUpdateIds.length > 0) {
|
|
29950
|
+
events.push({
|
|
29951
|
+
accountId,
|
|
29952
|
+
workspaceId,
|
|
29953
|
+
sessionId,
|
|
29954
|
+
turnId,
|
|
29955
|
+
turnGeneration,
|
|
29956
|
+
turnAttemptId: input.attemptId,
|
|
29957
|
+
turnAssociation: "current",
|
|
29958
|
+
sequence: ++sequence,
|
|
29959
|
+
type: "system.update.cancelled",
|
|
29960
|
+
payload: sanitizeEventPayload({
|
|
29961
|
+
updateIds: cancelledUpdateIds,
|
|
29962
|
+
count: cancelledUpdateIds.length,
|
|
29963
|
+
reason: "stale_goal_continuation",
|
|
29964
|
+
}),
|
|
29965
|
+
occurredAt,
|
|
29966
|
+
});
|
|
29967
|
+
}
|
|
29517
29968
|
const event: typeof schema.sessionEvents.$inferInsert = {
|
|
29518
29969
|
id: eventId,
|
|
29519
29970
|
accountId,
|
|
@@ -29523,24 +29974,64 @@ export async function claimSessionWorkForAttempt(
|
|
|
29523
29974
|
turnGeneration,
|
|
29524
29975
|
turnAttemptId: input.attemptId,
|
|
29525
29976
|
turnAssociation: "current",
|
|
29526
|
-
sequence:
|
|
29977
|
+
sequence: ++sequence,
|
|
29527
29978
|
type: "system.update.delivered",
|
|
29528
29979
|
payload: sanitizeEventPayload({
|
|
29529
29980
|
updateIds: deliverable.map((update) => update.id),
|
|
29981
|
+
historyItemId,
|
|
29530
29982
|
count: deliverable.length,
|
|
29531
29983
|
classifications: [...new Set(deliverable.map((update) => update.classification))],
|
|
29984
|
+
members: modelOrdered.map(internalUpdateEventMember),
|
|
29532
29985
|
}),
|
|
29533
29986
|
occurredAt,
|
|
29534
29987
|
};
|
|
29988
|
+
events.push(event);
|
|
29535
29989
|
return {
|
|
29536
29990
|
count: deliverable.length,
|
|
29537
|
-
lastSequence:
|
|
29991
|
+
lastSequence: sequence,
|
|
29538
29992
|
triggerEventId: eventId,
|
|
29993
|
+
historyItemId,
|
|
29994
|
+
historyItem,
|
|
29539
29995
|
updates: deliverable,
|
|
29996
|
+
events,
|
|
29540
29997
|
event,
|
|
29541
29998
|
};
|
|
29542
29999
|
};
|
|
29543
30000
|
|
|
30001
|
+
const persistDeliveredUpdateBatch = async (
|
|
30002
|
+
delivered: Awaited<ReturnType<typeof deliverPendingUpdates>>,
|
|
30003
|
+
accountId: string,
|
|
30004
|
+
turnId: string,
|
|
30005
|
+
): Promise<void> => {
|
|
30006
|
+
if (!delivered.historyItemId || !delivered.historyItem) {
|
|
30007
|
+
if (delivered.count !== 0) {
|
|
30008
|
+
throw new Error("Delivered machine-input batch has no model-memory item");
|
|
30009
|
+
}
|
|
30010
|
+
return;
|
|
30011
|
+
}
|
|
30012
|
+
const [{ position } = { position: 0 }] = await tx
|
|
30013
|
+
.select({
|
|
30014
|
+
position: sql<number>`coalesce(max(${schema.sessionHistoryItems.position}), -1) + 1`,
|
|
30015
|
+
})
|
|
30016
|
+
.from(schema.sessionHistoryItems)
|
|
30017
|
+
.where(
|
|
30018
|
+
and(
|
|
30019
|
+
eq(schema.sessionHistoryItems.workspaceId, workspaceId),
|
|
30020
|
+
eq(schema.sessionHistoryItems.sessionId, sessionId),
|
|
30021
|
+
),
|
|
30022
|
+
);
|
|
30023
|
+
await tx.insert(schema.sessionHistoryItems).values({
|
|
30024
|
+
id: delivered.historyItemId,
|
|
30025
|
+
accountId,
|
|
30026
|
+
workspaceId,
|
|
30027
|
+
sessionId,
|
|
30028
|
+
turnId,
|
|
30029
|
+
position: Number(position),
|
|
30030
|
+
item: sanitizeModelPayload(delivered.historyItem),
|
|
30031
|
+
producerCodexCredentialId: null,
|
|
30032
|
+
});
|
|
30033
|
+
};
|
|
30034
|
+
|
|
29544
30035
|
// Capacity settlement and resume use session -> turn after their
|
|
29545
30036
|
// workspace rotation lock. Claiming must preserve that shared order:
|
|
29546
30037
|
// taking a queued turn first can deadlock with a settlement that owns
|
|
@@ -29588,6 +30079,12 @@ export async function claimSessionWorkForAttempt(
|
|
|
29588
30079
|
and(
|
|
29589
30080
|
eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
|
|
29590
30081
|
eq(schema.sessionAttemptInterruptions.sessionId, sessionId),
|
|
30082
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
30083
|
+
"pending",
|
|
30084
|
+
"delivered",
|
|
30085
|
+
"acknowledged",
|
|
30086
|
+
"settled",
|
|
30087
|
+
]),
|
|
29591
30088
|
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
29592
30089
|
),
|
|
29593
30090
|
)
|
|
@@ -29870,7 +30367,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
29870
30367
|
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
29871
30368
|
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
29872
30369
|
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
29873
|
-
|
|
30370
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
29874
30371
|
),
|
|
29875
30372
|
)
|
|
29876
30373
|
.orderBy(
|
|
@@ -29879,20 +30376,27 @@ export async function claimSessionWorkForAttempt(
|
|
|
29879
30376
|
)
|
|
29880
30377
|
.limit(1)
|
|
29881
30378
|
.for("update");
|
|
29882
|
-
|
|
29883
|
-
|
|
29884
|
-
|
|
29885
|
-
|
|
29886
|
-
|
|
29887
|
-
|
|
29888
|
-
|
|
29889
|
-
|
|
29890
|
-
|
|
30379
|
+
// A human/API Steer is the newest explicit replacement direction. It
|
|
30380
|
+
// must claim next even when an older Agent Steer is pending; the
|
|
30381
|
+
// internal instruction is delivered as context on that same human turn
|
|
30382
|
+
// instead of manufacturing another system inference ahead of it.
|
|
30383
|
+
// Ordinary queued sends retain the established Agent-Steer priority.
|
|
30384
|
+
const rows = await rawRows<{
|
|
30385
|
+
id: string;
|
|
30386
|
+
trigger_event_id: string;
|
|
30387
|
+
metadata: Record<string, unknown>;
|
|
30388
|
+
}>(
|
|
30389
|
+
tx as unknown as Database,
|
|
30390
|
+
sql`select id, trigger_event_id, metadata from session_turns
|
|
29891
30391
|
where workspace_id = ${workspaceId} and session_id = ${sessionId}
|
|
29892
30392
|
and status = 'queued' and source in ('user', 'api')
|
|
30393
|
+
and (
|
|
30394
|
+
${Boolean(pendingAgentSteer)} = false
|
|
30395
|
+
or metadata->>'delivery' = 'steer'
|
|
30396
|
+
)
|
|
29893
30397
|
order by position asc, created_at asc, id asc
|
|
29894
30398
|
limit 1`,
|
|
29895
|
-
|
|
30399
|
+
);
|
|
29896
30400
|
const queuedTurnPreview = rows[0];
|
|
29897
30401
|
const queuedLocks = queuedTurnPreview
|
|
29898
30402
|
? await lockSessionEventWriteRows(tx as unknown as Database, {
|
|
@@ -30100,6 +30604,22 @@ export async function claimSessionWorkForAttempt(
|
|
|
30100
30604
|
triggerEventId,
|
|
30101
30605
|
);
|
|
30102
30606
|
if (delivered.count === 0) {
|
|
30607
|
+
if (delivered.events.length > 0) {
|
|
30608
|
+
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
30609
|
+
await tx
|
|
30610
|
+
.update(schema.sessions)
|
|
30611
|
+
.set({
|
|
30612
|
+
status: "idle",
|
|
30613
|
+
lastSequence: delivered.lastSequence,
|
|
30614
|
+
updatedAt: now,
|
|
30615
|
+
})
|
|
30616
|
+
.where(
|
|
30617
|
+
and(
|
|
30618
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
30619
|
+
eq(schema.sessions.id, sessionId),
|
|
30620
|
+
),
|
|
30621
|
+
);
|
|
30622
|
+
}
|
|
30103
30623
|
return { action: "unclaimed", reason: "no-work" };
|
|
30104
30624
|
}
|
|
30105
30625
|
const goalUpdate = delivered.updates.find(
|
|
@@ -30286,9 +30806,10 @@ export async function claimSessionWorkForAttempt(
|
|
|
30286
30806
|
})
|
|
30287
30807
|
.returning();
|
|
30288
30808
|
if (!internalTurn) throw new Error("Failed to create internal update inference");
|
|
30809
|
+
await persistDeliveredUpdateBatch(delivered, session.accountId, internalTurn.id);
|
|
30289
30810
|
await registerAttempt(internalTurn);
|
|
30290
30811
|
if (!delivered.event) throw new Error("Delivered update batch has no durable event");
|
|
30291
|
-
await tx.insert(schema.sessionEvents).values(delivered.
|
|
30812
|
+
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
30292
30813
|
if (goalUpdate && typeof goalUpdate.payload.goalId === "string") {
|
|
30293
30814
|
await tx
|
|
30294
30815
|
.update(schema.sessionGoals)
|
|
@@ -30405,8 +30926,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
30405
30926
|
session.lastSequence + 1,
|
|
30406
30927
|
now,
|
|
30407
30928
|
);
|
|
30408
|
-
|
|
30409
|
-
|
|
30929
|
+
await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
|
|
30930
|
+
if (delivered.events.length > 0) {
|
|
30931
|
+
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
30410
30932
|
}
|
|
30411
30933
|
await tx
|
|
30412
30934
|
.update(schema.sessions)
|
|
@@ -30650,6 +31172,196 @@ export async function markSessionAttemptQuiesced(
|
|
|
30650
31172
|
});
|
|
30651
31173
|
}
|
|
30652
31174
|
|
|
31175
|
+
export type ReconcileSessionAttemptQuiescenceResult =
|
|
31176
|
+
| { action: "quiesced"; events: SessionEvent[] }
|
|
31177
|
+
| { action: "pending"; events: [] }
|
|
31178
|
+
| { action: "stale"; events: [] };
|
|
31179
|
+
|
|
31180
|
+
export type SessionAttemptActivityRef = {
|
|
31181
|
+
workflowId: string;
|
|
31182
|
+
workflowRunId: string;
|
|
31183
|
+
activityId: string;
|
|
31184
|
+
quiesced: boolean;
|
|
31185
|
+
};
|
|
31186
|
+
|
|
31187
|
+
export async function getSessionAttemptActivityRef(
|
|
31188
|
+
db: Database,
|
|
31189
|
+
input: {
|
|
31190
|
+
accountId: string;
|
|
31191
|
+
workspaceId: string;
|
|
31192
|
+
sessionId: string;
|
|
31193
|
+
attemptId: string;
|
|
31194
|
+
temporalWorkflowId: string;
|
|
31195
|
+
},
|
|
31196
|
+
): Promise<SessionAttemptActivityRef | null> {
|
|
31197
|
+
return await withRlsContext(
|
|
31198
|
+
db,
|
|
31199
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
31200
|
+
async (scopedDb) => {
|
|
31201
|
+
const [attempt] = await scopedDb
|
|
31202
|
+
.select({
|
|
31203
|
+
workflowId: schema.sessionTurnAttempts.temporalWorkflowId,
|
|
31204
|
+
workflowRunId: schema.sessionTurnAttempts.temporalWorkflowRunId,
|
|
31205
|
+
activityId: schema.sessionTurnAttempts.temporalActivityId,
|
|
31206
|
+
quiescedAt: schema.sessionTurnAttempts.quiescedAt,
|
|
31207
|
+
})
|
|
31208
|
+
.from(schema.sessionTurnAttempts)
|
|
31209
|
+
.where(
|
|
31210
|
+
and(
|
|
31211
|
+
eq(schema.sessionTurnAttempts.accountId, input.accountId),
|
|
31212
|
+
eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
31213
|
+
eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
|
|
31214
|
+
eq(schema.sessionTurnAttempts.id, input.attemptId),
|
|
31215
|
+
eq(schema.sessionTurnAttempts.temporalWorkflowId, input.temporalWorkflowId),
|
|
31216
|
+
),
|
|
31217
|
+
)
|
|
31218
|
+
.limit(1);
|
|
31219
|
+
return attempt
|
|
31220
|
+
? {
|
|
31221
|
+
workflowId: attempt.workflowId,
|
|
31222
|
+
workflowRunId: attempt.workflowRunId,
|
|
31223
|
+
activityId: attempt.activityId,
|
|
31224
|
+
quiesced: attempt.quiescedAt !== null,
|
|
31225
|
+
}
|
|
31226
|
+
: null;
|
|
31227
|
+
},
|
|
31228
|
+
);
|
|
31229
|
+
}
|
|
31230
|
+
|
|
31231
|
+
/**
|
|
31232
|
+
* Recover the quiescence receipt when the original activity disappeared after
|
|
31233
|
+
* its attempt was durably interrupted. The caller first proves through
|
|
31234
|
+
* Temporal that the exact activity is absent or its server-owned heartbeat
|
|
31235
|
+
* lease expired. The closed attempt then cannot admit another workspace writer,
|
|
31236
|
+
* and every writer it did admit (including retained-process child writes) must
|
|
31237
|
+
* carry a physical settlement before the ordinary receipt transaction is
|
|
31238
|
+
* allowed to run.
|
|
31239
|
+
*/
|
|
31240
|
+
export async function reconcileSessionAttemptQuiescence(
|
|
31241
|
+
db: Database,
|
|
31242
|
+
input: {
|
|
31243
|
+
accountId: string;
|
|
31244
|
+
workspaceId: string;
|
|
31245
|
+
sessionId: string;
|
|
31246
|
+
attemptId: string;
|
|
31247
|
+
temporalWorkflowId: string;
|
|
31248
|
+
temporalWorkflowRunId: string;
|
|
31249
|
+
temporalActivityId: string;
|
|
31250
|
+
activitySettled: boolean;
|
|
31251
|
+
},
|
|
31252
|
+
): Promise<ReconcileSessionAttemptQuiescenceResult> {
|
|
31253
|
+
const eligibility = await withRlsContext(
|
|
31254
|
+
db,
|
|
31255
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
31256
|
+
async (scopedDb) => {
|
|
31257
|
+
const rows = await scopedDb.execute<{
|
|
31258
|
+
account_id: string;
|
|
31259
|
+
state: string;
|
|
31260
|
+
quiesced_at: Date | string | null;
|
|
31261
|
+
temporal_workflow_id: string;
|
|
31262
|
+
temporal_workflow_run_id: string;
|
|
31263
|
+
temporal_activity_id: string;
|
|
31264
|
+
interruption_settled: boolean;
|
|
31265
|
+
interruption_pending: boolean;
|
|
31266
|
+
writer_pending: boolean;
|
|
31267
|
+
}>(sql`
|
|
31268
|
+
select
|
|
31269
|
+
attempt.account_id,
|
|
31270
|
+
attempt.state,
|
|
31271
|
+
attempt.quiesced_at,
|
|
31272
|
+
attempt.temporal_workflow_id,
|
|
31273
|
+
attempt.temporal_workflow_run_id,
|
|
31274
|
+
attempt.temporal_activity_id,
|
|
31275
|
+
exists (
|
|
31276
|
+
select 1
|
|
31277
|
+
from session_attempt_interruptions interruption
|
|
31278
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
31279
|
+
and interruption.session_id = attempt.session_id
|
|
31280
|
+
and interruption.attempt_id = attempt.id
|
|
31281
|
+
and interruption.state in ('settled', 'rejected_stale')
|
|
31282
|
+
) as interruption_settled,
|
|
31283
|
+
exists (
|
|
31284
|
+
select 1
|
|
31285
|
+
from session_attempt_interruptions interruption
|
|
31286
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
31287
|
+
and interruption.session_id = attempt.session_id
|
|
31288
|
+
and interruption.attempt_id = attempt.id
|
|
31289
|
+
and interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
31290
|
+
) as interruption_pending,
|
|
31291
|
+
(
|
|
31292
|
+
exists (
|
|
31293
|
+
select 1
|
|
31294
|
+
from sandbox_workspace_mutation_admissions admission
|
|
31295
|
+
where admission.account_id = attempt.account_id
|
|
31296
|
+
and admission.workspace_id = attempt.workspace_id
|
|
31297
|
+
and admission.session_id = attempt.session_id
|
|
31298
|
+
and admission.settled_at is null
|
|
31299
|
+
and (
|
|
31300
|
+
admission.attempt_id = attempt.id
|
|
31301
|
+
or (
|
|
31302
|
+
admission.actor_kind = 'process'
|
|
31303
|
+
and exists (
|
|
31304
|
+
select 1
|
|
31305
|
+
from sandbox_retained_processes process
|
|
31306
|
+
where process.account_id = attempt.account_id
|
|
31307
|
+
and process.workspace_id = attempt.workspace_id
|
|
31308
|
+
and process.session_id = attempt.session_id
|
|
31309
|
+
and process.id = admission.actor_id
|
|
31310
|
+
and process.owner_attempt_id = attempt.id
|
|
31311
|
+
)
|
|
31312
|
+
)
|
|
31313
|
+
)
|
|
31314
|
+
)
|
|
31315
|
+
or exists (
|
|
31316
|
+
select 1
|
|
31317
|
+
from sandbox_retained_processes process
|
|
31318
|
+
where process.account_id = attempt.account_id
|
|
31319
|
+
and process.workspace_id = attempt.workspace_id
|
|
31320
|
+
and process.session_id = attempt.session_id
|
|
31321
|
+
and process.owner_attempt_id = attempt.id
|
|
31322
|
+
and process.state = 'active'
|
|
31323
|
+
)
|
|
31324
|
+
) as writer_pending
|
|
31325
|
+
from session_turn_attempts attempt
|
|
31326
|
+
where attempt.account_id = ${input.accountId}
|
|
31327
|
+
and attempt.workspace_id = ${input.workspaceId}
|
|
31328
|
+
and attempt.session_id = ${input.sessionId}
|
|
31329
|
+
and attempt.id = ${input.attemptId}
|
|
31330
|
+
limit 1
|
|
31331
|
+
`);
|
|
31332
|
+
return rows[0] ?? null;
|
|
31333
|
+
},
|
|
31334
|
+
);
|
|
31335
|
+
if (
|
|
31336
|
+
!eligibility ||
|
|
31337
|
+
eligibility.account_id !== input.accountId ||
|
|
31338
|
+
eligibility.temporal_workflow_id !== input.temporalWorkflowId ||
|
|
31339
|
+
eligibility.temporal_workflow_run_id !== input.temporalWorkflowRunId ||
|
|
31340
|
+
eligibility.temporal_activity_id !== input.temporalActivityId ||
|
|
31341
|
+
eligibility.state !== "closed" ||
|
|
31342
|
+
!eligibility.interruption_settled ||
|
|
31343
|
+
eligibility.interruption_pending
|
|
31344
|
+
) {
|
|
31345
|
+
return { action: "stale", events: [] };
|
|
31346
|
+
}
|
|
31347
|
+
if (eligibility.quiesced_at) {
|
|
31348
|
+
return { action: "quiesced", events: [] };
|
|
31349
|
+
}
|
|
31350
|
+
if (!input.activitySettled || eligibility.writer_pending) {
|
|
31351
|
+
return { action: "pending", events: [] };
|
|
31352
|
+
}
|
|
31353
|
+
const events = await markSessionAttemptQuiesced(db, {
|
|
31354
|
+
accountId: input.accountId,
|
|
31355
|
+
workspaceId: input.workspaceId,
|
|
31356
|
+
sessionId: input.sessionId,
|
|
31357
|
+
attemptId: input.attemptId,
|
|
31358
|
+
temporalWorkflowId: input.temporalWorkflowId,
|
|
31359
|
+
temporalWorkflowRunId: input.temporalWorkflowRunId,
|
|
31360
|
+
temporalActivityId: input.temporalActivityId,
|
|
31361
|
+
});
|
|
31362
|
+
return { action: "quiesced", events };
|
|
31363
|
+
}
|
|
31364
|
+
|
|
30653
31365
|
/**
|
|
30654
31366
|
* Settle every durable interruption cause for one exact first-class attempt.
|
|
30655
31367
|
* Steer wins the logical-turn fate when causes coexist; effective control after
|
|
@@ -30820,13 +31532,6 @@ export async function settleSessionAttemptInterruptions(
|
|
|
30820
31532
|
outcome,
|
|
30821
31533
|
closedAt: now,
|
|
30822
31534
|
});
|
|
30823
|
-
await requeueInterruptedSessionSystemUpdatesForTurnTx(
|
|
30824
|
-
tx as unknown as Database,
|
|
30825
|
-
workspaceId,
|
|
30826
|
-
sessionId,
|
|
30827
|
-
turn.id,
|
|
30828
|
-
);
|
|
30829
|
-
|
|
30830
31535
|
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = steer
|
|
30831
31536
|
? [
|
|
30832
31537
|
{
|
|
@@ -32111,10 +32816,45 @@ export async function applySessionTurnSettlement(
|
|
|
32111
32816
|
},
|
|
32112
32817
|
}),
|
|
32113
32818
|
);
|
|
32819
|
+
const settledMachineInputs = ["completed", "failed", "cancelled", "superseded"].includes(
|
|
32820
|
+
input.turnStatus,
|
|
32821
|
+
)
|
|
32822
|
+
? await tx
|
|
32823
|
+
.select({
|
|
32824
|
+
id: schema.sessionSystemUpdates.id,
|
|
32825
|
+
historyItemId: schema.sessionSystemUpdates.deliveredHistoryItemId,
|
|
32826
|
+
})
|
|
32827
|
+
.from(schema.sessionSystemUpdates)
|
|
32828
|
+
.where(
|
|
32829
|
+
and(
|
|
32830
|
+
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
32831
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
32832
|
+
eq(schema.sessionSystemUpdates.deliveredTurnId, input.turnId),
|
|
32833
|
+
eq(schema.sessionSystemUpdates.state, "delivered"),
|
|
32834
|
+
),
|
|
32835
|
+
)
|
|
32836
|
+
.orderBy(
|
|
32837
|
+
asc(schema.sessionSystemUpdates.createdAt),
|
|
32838
|
+
asc(schema.sessionSystemUpdates.id),
|
|
32839
|
+
)
|
|
32840
|
+
: [];
|
|
32841
|
+
const machineInputSettlementEvent: AppendEventInput | null =
|
|
32842
|
+
settledMachineInputs.length > 0
|
|
32843
|
+
? {
|
|
32844
|
+
type: "system.update.settled",
|
|
32845
|
+
payload: {
|
|
32846
|
+
updateIds: settledMachineInputs.map((update) => update.id),
|
|
32847
|
+
count: settledMachineInputs.length,
|
|
32848
|
+
historyItemId: settledMachineInputs[0]!.historyItemId,
|
|
32849
|
+
outcome: input.turnStatus,
|
|
32850
|
+
},
|
|
32851
|
+
}
|
|
32852
|
+
: null;
|
|
32114
32853
|
const settlementEvents = [
|
|
32115
32854
|
...(recordingEvent ? [recordingEvent] : []),
|
|
32116
32855
|
...(compactionRequestEvent ? [compactionRequestEvent] : []),
|
|
32117
32856
|
...terminalHumanInputEvents,
|
|
32857
|
+
...(machineInputSettlementEvent ? [machineInputSettlementEvent] : []),
|
|
32118
32858
|
...input.events,
|
|
32119
32859
|
];
|
|
32120
32860
|
const values = settlementEvents.map((event) => {
|
|
@@ -32197,21 +32937,6 @@ export async function applySessionTurnSettlement(
|
|
|
32197
32937
|
turn,
|
|
32198
32938
|
);
|
|
32199
32939
|
}
|
|
32200
|
-
if (input.turnStatus === "failed") {
|
|
32201
|
-
await deferFailedSessionSystemUpdatesForTurnTx(
|
|
32202
|
-
tx as unknown as Database,
|
|
32203
|
-
workspaceId,
|
|
32204
|
-
input.sessionId,
|
|
32205
|
-
input.turnId,
|
|
32206
|
-
);
|
|
32207
|
-
} else if (["cancelled", "superseded"].includes(input.turnStatus)) {
|
|
32208
|
-
await requeueInterruptedSessionSystemUpdatesForTurnTx(
|
|
32209
|
-
tx as unknown as Database,
|
|
32210
|
-
workspaceId,
|
|
32211
|
-
input.sessionId,
|
|
32212
|
-
input.turnId,
|
|
32213
|
-
);
|
|
32214
|
-
}
|
|
32215
32940
|
await tx
|
|
32216
32941
|
.update(schema.sessions)
|
|
32217
32942
|
.set({
|
|
@@ -32495,14 +33220,6 @@ export async function settleCodexCredentialLeaseLoss(
|
|
|
32495
33220
|
eq(schema.sessions.activeTurnId, input.turnId),
|
|
32496
33221
|
),
|
|
32497
33222
|
);
|
|
32498
|
-
if (!input.checkpointDurable) {
|
|
32499
|
-
await deferFailedSessionSystemUpdatesForTurnTx(
|
|
32500
|
-
tx as unknown as Database,
|
|
32501
|
-
input.workspaceId,
|
|
32502
|
-
input.sessionId,
|
|
32503
|
-
input.turnId,
|
|
32504
|
-
);
|
|
32505
|
-
}
|
|
32506
33223
|
await tx.execute(sql`
|
|
32507
33224
|
delete from codex_credential_leases
|
|
32508
33225
|
where account_id = ${input.accountId}
|
|
@@ -33446,17 +34163,61 @@ export async function getSessionQueueSnapshot(
|
|
|
33446
34163
|
),
|
|
33447
34164
|
)
|
|
33448
34165
|
.orderBy(asc(schema.sessionTurns.position), asc(schema.sessionTurns.createdAt));
|
|
34166
|
+
const pendingInputs = await scopedDb
|
|
34167
|
+
.select()
|
|
34168
|
+
.from(schema.sessionSystemUpdates)
|
|
34169
|
+
.where(
|
|
34170
|
+
and(
|
|
34171
|
+
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
34172
|
+
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
34173
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
34174
|
+
),
|
|
34175
|
+
)
|
|
34176
|
+
.orderBy(
|
|
34177
|
+
sql`case when ${schema.sessionSystemUpdates.kind} = 'agent_steer_instruction' then 0 else 1 end`,
|
|
34178
|
+
asc(schema.sessionSystemUpdates.createdAt),
|
|
34179
|
+
asc(schema.sessionSystemUpdates.id),
|
|
34180
|
+
);
|
|
33449
34181
|
const latestInterruption = await latestSessionAttemptInterruption(
|
|
33450
34182
|
scopedDb,
|
|
33451
34183
|
workspaceId,
|
|
33452
34184
|
sessionId,
|
|
33453
34185
|
);
|
|
34186
|
+
const items = rows.map(mapSessionTurn);
|
|
34187
|
+
const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
|
|
34188
|
+
const hasPendingAgentSteer = pendingInputs.some(
|
|
34189
|
+
(update) => update.kind === "agent_steer_instruction",
|
|
34190
|
+
);
|
|
34191
|
+
const attachmentTurn = hasPendingAgentSteer
|
|
34192
|
+
? items.find((turn) => turn.metadata.delivery === "steer")
|
|
34193
|
+
: items[0];
|
|
33454
34194
|
return {
|
|
33455
34195
|
version: session.queueVersion,
|
|
33456
34196
|
effectiveControl: serializeEffectiveSessionControl(effectiveControl),
|
|
33457
34197
|
stoppingPreviousAttempt:
|
|
33458
|
-
latestInterruption !== null &&
|
|
33459
|
-
|
|
34198
|
+
latestInterruption !== null &&
|
|
34199
|
+
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
34200
|
+
latestInterruption.quiescedAt === null,
|
|
34201
|
+
items,
|
|
34202
|
+
pendingInputs: pendingInputs.map((update) => {
|
|
34203
|
+
const canonical = mapSessionSystemUpdate(update);
|
|
34204
|
+
return {
|
|
34205
|
+
id: canonical.id,
|
|
34206
|
+
sessionId: canonical.sessionId,
|
|
34207
|
+
kind: canonical.kind,
|
|
34208
|
+
classification: canonical.classification,
|
|
34209
|
+
sourceId: boundedInternalUpdateEventText(canonical.sourceId, 256).text,
|
|
34210
|
+
summary: boundedInternalUpdateEventText(canonical.summary, 512).text,
|
|
34211
|
+
createdAt: canonical.createdAt,
|
|
34212
|
+
};
|
|
34213
|
+
}),
|
|
34214
|
+
pendingInputAttachment:
|
|
34215
|
+
attachmentTurn && nextInputBatch.length > 0
|
|
34216
|
+
? {
|
|
34217
|
+
turnId: attachmentTurn.id,
|
|
34218
|
+
inputIds: nextInputBatch.map((update) => update.id),
|
|
34219
|
+
}
|
|
34220
|
+
: null,
|
|
33460
34221
|
};
|
|
33461
34222
|
});
|
|
33462
34223
|
}
|
|
@@ -33805,12 +34566,13 @@ export async function claimPendingSessionWorkflowWakes(
|
|
|
33805
34566
|
|
|
33806
34567
|
/**
|
|
33807
34568
|
* Acknowledge an immediate post-commit signal only after it cannot strand an
|
|
33808
|
-
* accepted
|
|
33809
|
-
*
|
|
33810
|
-
*
|
|
33811
|
-
* the revision so the bounded outbox
|
|
33812
|
-
* attempt-fenced claim consumes
|
|
33813
|
-
*
|
|
34569
|
+
* accepted direction or an interrupted attempt awaiting writer-set proof.
|
|
34570
|
+
* Temporal accepting a signal is transport evidence, not proof that a closing
|
|
34571
|
+
* workflow observed Postgres. While active control still has an actionable
|
|
34572
|
+
* Agent Steer or pending quiescence, retain the revision so the bounded outbox
|
|
34573
|
+
* dispatcher retries signalWithStart. The attempt-fenced claim consumes an
|
|
34574
|
+
* Agent Steer once; a real Pause is the typed blocker and may acknowledge this
|
|
34575
|
+
* revision because Resume commits a new one.
|
|
33814
34576
|
*
|
|
33815
34577
|
* An older sender may advance only its own revision; it cannot clear a claim or
|
|
33816
34578
|
* failure state belonging to a newer revision. The control -> workspace ->
|
|
@@ -33819,7 +34581,10 @@ export async function claimPendingSessionWorkflowWakes(
|
|
|
33819
34581
|
*/
|
|
33820
34582
|
export type SessionWorkflowWakeDeliveryResult =
|
|
33821
34583
|
| { action: "acknowledged" }
|
|
33822
|
-
| {
|
|
34584
|
+
| {
|
|
34585
|
+
action: "pending_admission";
|
|
34586
|
+
blocker: "pending_agent_steer" | "pending_quiescence";
|
|
34587
|
+
};
|
|
33823
34588
|
|
|
33824
34589
|
export async function markSessionWorkflowWakeDelivered(
|
|
33825
34590
|
db: Database,
|
|
@@ -33859,6 +34624,35 @@ export async function markSessionWorkflowWakeDelivered(
|
|
|
33859
34624
|
if (pendingAgentSteer) {
|
|
33860
34625
|
return { action: "pending_admission", blocker: "pending_agent_steer" } as const;
|
|
33861
34626
|
}
|
|
34627
|
+
const [pendingQuiescence] = await tx
|
|
34628
|
+
.select({ id: schema.sessionTurnAttempts.id })
|
|
34629
|
+
.from(schema.sessionTurnAttempts)
|
|
34630
|
+
.innerJoin(
|
|
34631
|
+
schema.sessionAttemptInterruptions,
|
|
34632
|
+
and(
|
|
34633
|
+
eq(
|
|
34634
|
+
schema.sessionAttemptInterruptions.workspaceId,
|
|
34635
|
+
schema.sessionTurnAttempts.workspaceId,
|
|
34636
|
+
),
|
|
34637
|
+
eq(
|
|
34638
|
+
schema.sessionAttemptInterruptions.sessionId,
|
|
34639
|
+
schema.sessionTurnAttempts.sessionId,
|
|
34640
|
+
),
|
|
34641
|
+
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
34642
|
+
),
|
|
34643
|
+
)
|
|
34644
|
+
.where(
|
|
34645
|
+
and(
|
|
34646
|
+
eq(schema.sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
34647
|
+
eq(schema.sessionTurnAttempts.sessionId, input.sessionId),
|
|
34648
|
+
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
34649
|
+
inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
|
|
34650
|
+
),
|
|
34651
|
+
)
|
|
34652
|
+
.limit(1);
|
|
34653
|
+
if (pendingQuiescence) {
|
|
34654
|
+
return { action: "pending_admission", blocker: "pending_quiescence" } as const;
|
|
34655
|
+
}
|
|
33862
34656
|
}
|
|
33863
34657
|
const [row] = await tx
|
|
33864
34658
|
.update(schema.sessionWorkflowWakeOutbox)
|
|
@@ -34093,55 +34887,6 @@ export async function addSessionSystemUpdate(
|
|
|
34093
34887
|
return await addSessionSystemUpdateWithSourceMutation(db, input, async () => undefined);
|
|
34094
34888
|
}
|
|
34095
34889
|
|
|
34096
|
-
async function requeueInterruptedSessionSystemUpdatesForTurnTx(
|
|
34097
|
-
tx: Database,
|
|
34098
|
-
workspaceId: string,
|
|
34099
|
-
sessionId: string,
|
|
34100
|
-
turnId: string,
|
|
34101
|
-
): Promise<void> {
|
|
34102
|
-
await tx
|
|
34103
|
-
.update(schema.sessionSystemUpdates)
|
|
34104
|
-
.set({ state: "pending", deliveredTurnId: null, deliveredAt: null })
|
|
34105
|
-
.where(
|
|
34106
|
-
and(
|
|
34107
|
-
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
34108
|
-
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
34109
|
-
eq(schema.sessionSystemUpdates.deliveredTurnId, turnId),
|
|
34110
|
-
eq(schema.sessionSystemUpdates.state, "delivered"),
|
|
34111
|
-
),
|
|
34112
|
-
);
|
|
34113
|
-
}
|
|
34114
|
-
|
|
34115
|
-
/**
|
|
34116
|
-
* A failed internal-only inference must not manufacture another inference by
|
|
34117
|
-
* making its inputs immediately runnable again. Preserve ordinary internal
|
|
34118
|
-
* updates as deferred input for the next real prompt/new update. Goal
|
|
34119
|
-
* continuation notices are derivable from the durable goal and become terminal
|
|
34120
|
-
* so the goal evaluator can pause or synthesize the next valid continuation.
|
|
34121
|
-
*/
|
|
34122
|
-
async function deferFailedSessionSystemUpdatesForTurnTx(
|
|
34123
|
-
tx: Database,
|
|
34124
|
-
workspaceId: string,
|
|
34125
|
-
sessionId: string,
|
|
34126
|
-
turnId: string,
|
|
34127
|
-
): Promise<void> {
|
|
34128
|
-
await tx
|
|
34129
|
-
.update(schema.sessionSystemUpdates)
|
|
34130
|
-
.set({
|
|
34131
|
-
state: sql`case when ${schema.sessionSystemUpdates.payload} ->> 'type' = 'goal_continuation' then 'failed' else 'deferred' end`,
|
|
34132
|
-
deliveredTurnId: null,
|
|
34133
|
-
deliveredAt: null,
|
|
34134
|
-
})
|
|
34135
|
-
.where(
|
|
34136
|
-
and(
|
|
34137
|
-
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
34138
|
-
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
34139
|
-
eq(schema.sessionSystemUpdates.deliveredTurnId, turnId),
|
|
34140
|
-
eq(schema.sessionSystemUpdates.state, "delivered"),
|
|
34141
|
-
),
|
|
34142
|
-
);
|
|
34143
|
-
}
|
|
34144
|
-
|
|
34145
34890
|
/**
|
|
34146
34891
|
* Persist one internal update without fabricating a user prompt or queue row.
|
|
34147
34892
|
* Dedupe and any producer/outbox mutation commit in the same transaction.
|
|
@@ -34243,6 +34988,7 @@ export async function addSessionSystemUpdateWithSourceMutation(
|
|
|
34243
34988
|
}
|
|
34244
34989
|
|
|
34245
34990
|
const now = new Date();
|
|
34991
|
+
const eventPreview = internalUpdateEventMember(inserted);
|
|
34246
34992
|
const [event] = await tx
|
|
34247
34993
|
.insert(schema.sessionEvents)
|
|
34248
34994
|
.values({
|
|
@@ -34252,11 +34998,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
|
|
|
34252
34998
|
sequence: session.lastSequence + 1,
|
|
34253
34999
|
type: "system.update.pending",
|
|
34254
35000
|
payload: sanitizeEventPayload({
|
|
34255
|
-
updateId:
|
|
34256
|
-
kind:
|
|
34257
|
-
classification:
|
|
34258
|
-
sourceId:
|
|
34259
|
-
|
|
35001
|
+
updateId: eventPreview.id,
|
|
35002
|
+
kind: eventPreview.kind,
|
|
35003
|
+
classification: eventPreview.classification,
|
|
35004
|
+
sourceId: eventPreview.sourceId,
|
|
35005
|
+
sourceIdTruncated: eventPreview.sourceIdTruncated,
|
|
35006
|
+
summary: eventPreview.summary,
|
|
35007
|
+
summaryTruncated: eventPreview.summaryTruncated,
|
|
34260
35008
|
}),
|
|
34261
35009
|
occurredAt: now,
|
|
34262
35010
|
})
|
|
@@ -34307,7 +35055,7 @@ export async function listOutstandingSessionSystemUpdates(
|
|
|
34307
35055
|
and(
|
|
34308
35056
|
eq(schema.sessionSystemUpdates.workspaceId, workspaceId),
|
|
34309
35057
|
eq(schema.sessionSystemUpdates.sessionId, sessionId),
|
|
34310
|
-
|
|
35058
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
34311
35059
|
),
|
|
34312
35060
|
)
|
|
34313
35061
|
.orderBy(asc(schema.sessionSystemUpdates.createdAt), asc(schema.sessionSystemUpdates.id));
|
|
@@ -34363,6 +35111,7 @@ function mapSessionSystemUpdate(
|
|
|
34363
35111
|
lineage: row.lineage,
|
|
34364
35112
|
state: row.state as SessionSystemUpdateState,
|
|
34365
35113
|
deliveredTurnId: row.deliveredTurnId,
|
|
35114
|
+
deliveredHistoryItemId: row.deliveredHistoryItemId,
|
|
34366
35115
|
deliveredAt: row.deliveredAt?.toISOString() ?? null,
|
|
34367
35116
|
createdAt: row.createdAt.toISOString(),
|
|
34368
35117
|
};
|
|
@@ -35210,6 +35959,7 @@ function mapSession(
|
|
|
35210
35959
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
35211
35960
|
instructions: row.instructions ?? null,
|
|
35212
35961
|
resources: row.resources as ResourceRef[],
|
|
35962
|
+
skills: (row.skills as SessionSkill[]) ?? [],
|
|
35213
35963
|
tools: row.tools as ToolRef[],
|
|
35214
35964
|
toolPolicy: (row.toolPolicy as SessionToolPolicy | null) ?? {
|
|
35215
35965
|
mode: "legacy",
|
|
@@ -35239,6 +35989,7 @@ function mapSession(
|
|
|
35239
35989
|
rigId: row.rigId ?? null,
|
|
35240
35990
|
rigVersionId: row.rigVersionId ?? null,
|
|
35241
35991
|
firstPartyMcpPermissions: (row.firstPartyMcpPermissions as Permission[] | null) ?? null,
|
|
35992
|
+
firstPartyMcpTools: (row.firstPartyMcpTools as FirstPartyMcpToolName[] | null) ?? null,
|
|
35242
35993
|
mcpServers,
|
|
35243
35994
|
parentSessionId: row.parentSessionId ?? null,
|
|
35244
35995
|
rootSessionId: row.rootSessionId,
|