@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/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
- workspaceId: workspace.id,
1357
- accountId: account.id,
1358
- subjectId: input.subjectId,
1359
- ...(input.subjectLabel ? { subjectLabel: input.subjectLabel } : {}),
1360
- permissions: workspacePermissions,
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
- const [row] = await scopedDb
4784
- .insert(schema.connections)
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 mapConnectionMetadata(row);
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 withWorkspaceRls(db, workspaceId, async (scopedDb) => {
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 withWorkspaceRls(db, workspaceId, async (scopedDb) => {
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 withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
4857
- const set = {
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
- ...(input.providerDomain !== undefined ? { providerDomain: input.providerDomain } : {}),
4860
- ...(input.subjectId !== undefined ? { subjectId: input.subjectId } : {}),
4861
- ...(input.kind !== undefined ? { kind: input.kind } : {}),
4862
- ...(input.status !== undefined ? { status: input.status } : {}),
4863
- ...(input.credentialEncrypted !== undefined
4864
- ? {
4865
- credentialEncrypted: input.credentialEncrypted,
4866
- version: sql`${schema.connections.version} + 1`,
4867
- lastError: null,
4868
- }
4869
- : {}),
4870
- ...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
4871
- ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
4872
- ...(input.verifiedInstallAt !== undefined
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 withWorkspaceRls(db, workspaceId, async (scopedDb) => {
4908
- const [row] = await scopedDb
4909
- .update(schema.connections)
4910
- .set({
4911
- status: "revoked",
4912
- // The version bump invalidates any in-flight refresh's (id, version) CAS,
4913
- // so a racing refresh cannot commit and flip the row back to active.
4914
- version: sql`${schema.connections.version} + 1`,
4915
- // Status-only revocation does not replace the verified credential or bot
4916
- // identity. Carry the marker to the same new CAS version so the dedicated
4917
- // reinstall path can still recognize (but not use) the inactive row.
4918
- verifiedInstallVersion: sql`case
4919
- when ${schema.connections.verifiedInstallAt} is null then null
4920
- else ${schema.connections.version} + 1
4921
- end`,
4922
- updatedBySubjectId: updatedBySubjectId ?? null,
4923
- updatedAt: new Date(),
4924
- })
4925
- .where(
4926
- and(
4927
- eq(schema.connections.workspaceId, workspaceId),
4928
- eq(schema.connections.id, connectionId),
4929
- // Same visibility rule as get/update: shared rows plus the caller's own
4930
- // subject rows. Cross-subject revocation (admin janitorial) arrives with
4931
- // the subject-connections UX in I5, deliberately not before.
4932
- connectionSubjectVisibility(updatedBySubjectId),
4933
- ),
4934
- )
4935
- .returning(connectionMetadataColumns);
4936
- return row ? mapConnectionMetadata(row) : null;
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
- ? connectionSubjectVisibility(input.subjectId)
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 withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
5240
- // Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
5241
- // freshly revoked connection shadow an active replacement for the provider.
5242
- const [row] = await scopedDb
5243
- .select()
5244
- .from(schema.connections)
5245
- .where(and(...conditions))
5246
- .orderBy(
5247
- desc(sql`(${schema.connections.status} = 'active')`),
5248
- desc(schema.connections.updatedAt),
5249
- )
5250
- .limit(1);
5251
- if (!row) {
5252
- return null;
5253
- }
5254
- let credential: unknown;
5255
- try {
5256
- credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
5257
- } catch (error) {
5258
- throw new Error(
5259
- `connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`,
5260
- { cause: error },
5261
- );
5262
- }
5263
- if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
5264
- throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
5265
- }
5266
- return {
5267
- id: row.id,
5268
- accountId: row.accountId,
5269
- workspaceId: row.workspaceId,
5270
- subjectId: row.subjectId,
5271
- providerDomain: row.providerDomain,
5272
- kind: row.kind as ConnectionKind,
5273
- status: row.status as ConnectionStatus,
5274
- credential: credential as Record<string, unknown>,
5275
- grantedScopes: row.grantedScopes,
5276
- expiresAt: row.expiresAt,
5277
- lastRefreshAt: row.lastRefreshAt,
5278
- version: row.version,
5279
- metadata: row.metadata,
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 withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
5297
- const set = {
5298
- credentialEncrypted: input.credentialEncrypted,
5299
- expiresAt: input.expiresAt,
5300
- lastRefreshAt: input.lastRefreshAt,
5301
- status: "active",
5302
- lastError: null,
5303
- version: sql`${schema.connections.version} + 1`,
5304
- updatedAt: new Date(),
5305
- ...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
5306
- };
5307
- const updated = await scopedDb
5308
- .update(schema.connections)
5309
- .set(set)
5310
- .where(
5311
- and(
5312
- eq(schema.connections.id, input.id),
5313
- eq(schema.connections.workspaceId, input.workspaceId),
5314
- eq(schema.connections.version, input.version),
5315
- // A refresh may only ever renew a live credential; revoked/errored rows
5316
- // stay dead even if a status change somewhere forgot to bump version.
5317
- eq(schema.connections.status, "active"),
5318
- ),
5319
- )
5320
- .returning({ id: schema.connections.id });
5321
- return updated.length > 0;
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 withWorkspaceRls(db, workspaceId, async (scopedDb) => {
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 withWorkspaceRls(db, workspaceId, async (scopedDb) => {
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. Ancestors are capped at 10 and returned root-first.
15980
- * Descendants are capped at depth 5 and 200 total rows, returned as a nested tree.
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 ancestorRows = (await scopedDb.execute(sql<LineageIdRow>`
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 parent.id, parent.parent_session_id, ancestors.depth + 1, ancestors.path || parent.id
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.depth < 10
16014
- and not parent.id = any(ancestors.path)
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
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
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: update.id,
28827
- kind: update.kind,
28828
- classification: update.classification,
28829
- sourceId: update.sourceId,
28830
- summary: update.summary,
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
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
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
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
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 deliverable: typeof updates = [];
29438
- let deliveredBytes = 0;
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
- const updateBytes = Buffer.byteLength(
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: nextSequence,
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: nextSequence,
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
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
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
- const rows = pendingAgentSteer
29883
- ? []
29884
- : await rawRows<{
29885
- id: string;
29886
- trigger_event_id: string;
29887
- metadata: Record<string, unknown>;
29888
- }>(
29889
- tx as unknown as Database,
29890
- sql`select id, trigger_event_id, metadata from session_turns
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.event);
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
- if (delivered.event) {
30409
- await tx.insert(schema.sessionEvents).values(delivered.event);
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 && latestInterruption.quiescedAt === null,
33459
- items: rows.map(mapSessionTurn),
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 Agent Steer. Temporal accepting a signal is transport evidence, not
33809
- * proof that a closing workflow observed Postgres or admitted its pending
33810
- * direction. While active control still has an actionable Agent Steer, retain
33811
- * the revision so the bounded outbox dispatcher retries signalWithStart. The
33812
- * attempt-fenced claim consumes the update once; a real Pause is the typed
33813
- * blocker and may acknowledge this revision because Resume commits a new one.
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
- | { action: "pending_admission"; blocker: "pending_agent_steer" };
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: inserted.id,
34256
- kind: input.kind,
34257
- classification: input.classification,
34258
- sourceId: input.sourceId,
34259
- summary: input.summary,
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
- inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
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,