@opengeni/db 0.22.2 → 0.26.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-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
- package/dist/chunk-7WTDI7Y3.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
- package/dist/chunk-KW6U54V2.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +107 -5
- package/dist/index.js +7857 -3898
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1659 -77
- package/dist/schema.js +13 -1
- package/dist/session-control.d.ts +7 -1
- package/dist/session-queue-commands.d.ts +8 -0
- package/dist/session-realtime-context.d.ts +56 -0
- package/dist/session-realtime-ledger.d.ts +188 -0
- package/dist/session-realtime-mirror.d.ts +51 -0
- package/dist/session-realtime-state.d.ts +2 -0
- package/dist/session-realtime-terminal.d.ts +40 -0
- package/dist/session-realtime.d.ts +59 -0
- package/dist/workspace-instruction-policies-schema.d.ts +307 -0
- package/dist/workspace-instruction-policies.d.ts +97 -7
- package/drizzle/0156_slack_reaction_trigger.sql +49 -0
- package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
- package/drizzle/0158_session_realtime_mode.sql +88 -0
- package/drizzle/0159_session_realtime_ledger.sql +198 -0
- package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
- package/drizzle/0161_session_realtime_context_projection.sql +82 -0
- package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
- package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
- package/drizzle/0164_session_realtime_models.sql +28 -0
- package/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/package.json +4 -4
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +1033 -101
- package/src/preference-registry.ts +114 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +12 -0
- package/src/schema.ts +486 -43
- package/src/session-control.ts +643 -21
- package/src/session-queue-commands.ts +121 -18
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +276 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +659 -0
- package/src/workspace-instruction-policies-schema.ts +71 -0
- package/src/workspace-instruction-policies.ts +568 -25
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- package/dist/chunk-CYGFLLMN.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ConnectionKind,
|
|
15
15
|
ConnectionMetadata,
|
|
16
16
|
ConnectionStatus,
|
|
17
|
+
DocumentAuthorityKind,
|
|
17
18
|
McpServerConnectionRef,
|
|
18
19
|
FileAsset,
|
|
19
20
|
FileStatus,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
decodeNativeSnapshotRef,
|
|
112
113
|
parseWorkspaceArchiveDescriptor,
|
|
113
114
|
stableJson,
|
|
115
|
+
MODEL_ATTACHMENT_REFS_FIELD,
|
|
114
116
|
} from "@opengeni/contracts";
|
|
115
117
|
|
|
116
118
|
import {
|
|
@@ -217,11 +219,30 @@ import {
|
|
|
217
219
|
serializeEffectiveSessionControl,
|
|
218
220
|
type SessionDiscoveryControl,
|
|
219
221
|
type SessionCommandActor,
|
|
222
|
+
SessionControlConflictError,
|
|
220
223
|
SessionControlInvariantError,
|
|
221
224
|
updateSessionCommandReceiptResult,
|
|
222
225
|
type SessionTurnAttemptOutcome,
|
|
223
226
|
type WorkspaceControlRow,
|
|
224
227
|
} from "./session-control";
|
|
228
|
+
import {
|
|
229
|
+
sessionRealtimeIsActiveInTransaction,
|
|
230
|
+
settleExpiredSessionRealtimeInTransaction,
|
|
231
|
+
} from "./session-realtime";
|
|
232
|
+
import {
|
|
233
|
+
isSessionRealtimeDelegationTurnMetadata,
|
|
234
|
+
projectSessionRealtimeDelegationProgressInTransaction,
|
|
235
|
+
projectSessionRealtimeDelegationTerminalInTransaction,
|
|
236
|
+
} from "./session-realtime-ledger";
|
|
237
|
+
import {
|
|
238
|
+
mirrorSessionRealtimeContextInTransaction,
|
|
239
|
+
renderRealtimeHumanInputRequestContext,
|
|
240
|
+
renderRealtimeHumanInputResponseContext,
|
|
241
|
+
} from "./session-realtime-mirror";
|
|
242
|
+
import {
|
|
243
|
+
listSessionRealtimeContinuityEntriesInTransaction,
|
|
244
|
+
type SessionRealtimeContinuityEntry,
|
|
245
|
+
} from "./session-realtime-context";
|
|
225
246
|
import * as schema from "./schema";
|
|
226
247
|
import {
|
|
227
248
|
AGENT_VISIBLE_MEMORY_STATUSES,
|
|
@@ -243,6 +264,9 @@ import {
|
|
|
243
264
|
export { sql as dbSql } from "drizzle-orm";
|
|
244
265
|
export * from "./session-control";
|
|
245
266
|
export * from "./session-queue-commands";
|
|
267
|
+
export * from "./session-realtime";
|
|
268
|
+
export * from "./session-realtime-context";
|
|
269
|
+
export * from "./session-realtime-ledger";
|
|
246
270
|
export * from "./new-session-drafts";
|
|
247
271
|
export * from "./workspace-instruction-policies";
|
|
248
272
|
export * from "./preference-registry";
|
|
@@ -1180,6 +1204,55 @@ export async function withWorkspaceUsageLock<T>(
|
|
|
1180
1204
|
});
|
|
1181
1205
|
}
|
|
1182
1206
|
|
|
1207
|
+
export type DocumentIndexAuthority = {
|
|
1208
|
+
authorityKind: DocumentAuthorityKind;
|
|
1209
|
+
authorityWorkspaceId: string | null;
|
|
1210
|
+
authoritySubjectId: string | null;
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Read the immutable authority tuple used by document indexing compatibility.
|
|
1215
|
+
* The database capability returns no document content and independently
|
|
1216
|
+
* verifies that this transaction already carries the exact account/workspace
|
|
1217
|
+
* RLS context. It is intentionally subject-free so a historical three-field
|
|
1218
|
+
* Temporal payload can recover a personal document's frozen subject without
|
|
1219
|
+
* weakening ordinary document visibility.
|
|
1220
|
+
*/
|
|
1221
|
+
export async function resolveDocumentIndexAuthority(
|
|
1222
|
+
db: Database,
|
|
1223
|
+
input: { accountId: string; workspaceId: string; documentId: string },
|
|
1224
|
+
): Promise<DocumentIndexAuthority | null> {
|
|
1225
|
+
const rows = await rawRows<{
|
|
1226
|
+
authority_kind: string;
|
|
1227
|
+
authority_workspace_id: string | null;
|
|
1228
|
+
authority_subject_id: string | null;
|
|
1229
|
+
}>(
|
|
1230
|
+
db,
|
|
1231
|
+
sql`
|
|
1232
|
+
select authority_kind, authority_workspace_id, authority_subject_id
|
|
1233
|
+
from opengeni_private.resolve_document_index_authority(
|
|
1234
|
+
${input.accountId},
|
|
1235
|
+
${input.workspaceId},
|
|
1236
|
+
${input.documentId}
|
|
1237
|
+
)
|
|
1238
|
+
`,
|
|
1239
|
+
);
|
|
1240
|
+
const row = rows[0];
|
|
1241
|
+
if (!row) return null;
|
|
1242
|
+
if (
|
|
1243
|
+
row.authority_kind !== "organization" &&
|
|
1244
|
+
row.authority_kind !== "workspace" &&
|
|
1245
|
+
row.authority_kind !== "personal"
|
|
1246
|
+
) {
|
|
1247
|
+
throw new Error(`Stored document authority kind is invalid: ${row.authority_kind}`);
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
authorityKind: row.authority_kind,
|
|
1251
|
+
authorityWorkspaceId: row.authority_workspace_id,
|
|
1252
|
+
authoritySubjectId: row.authority_subject_id,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1183
1256
|
export async function withAccountRls<T>(
|
|
1184
1257
|
db: Database,
|
|
1185
1258
|
accountId: string,
|
|
@@ -3481,6 +3554,47 @@ export type UpdateConnectionInput = {
|
|
|
3481
3554
|
updatedBySubjectId?: string | null;
|
|
3482
3555
|
};
|
|
3483
3556
|
|
|
3557
|
+
export type TransitionConnectionStateInput = {
|
|
3558
|
+
workspaceId: string;
|
|
3559
|
+
connectionId: string;
|
|
3560
|
+
visibleToSubjectId?: string | null;
|
|
3561
|
+
expectedVersion: number;
|
|
3562
|
+
status?: ConnectionStatus;
|
|
3563
|
+
metadata: Record<string, unknown>;
|
|
3564
|
+
lastError?: string | null;
|
|
3565
|
+
updatedBySubjectId?: string | null;
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
export type DisconnectConnectionIdempotentlyInput = {
|
|
3569
|
+
accountId: string;
|
|
3570
|
+
workspaceId: string;
|
|
3571
|
+
subjectId: string;
|
|
3572
|
+
connectionId: string;
|
|
3573
|
+
expectedVersion: number;
|
|
3574
|
+
idempotencyKey: string;
|
|
3575
|
+
metadata: Record<string, unknown>;
|
|
3576
|
+
lastError?: string | null;
|
|
3577
|
+
updatedBySubjectId?: string | null;
|
|
3578
|
+
};
|
|
3579
|
+
|
|
3580
|
+
export class ConnectionDisconnectIdempotencyError extends Error {
|
|
3581
|
+
readonly code = "IDEMPOTENCY_KEY_REUSED";
|
|
3582
|
+
|
|
3583
|
+
constructor() {
|
|
3584
|
+
super("The disconnect idempotency key was already used with different input");
|
|
3585
|
+
this.name = "ConnectionDisconnectIdempotencyError";
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
export class ConnectionDisconnectGenerationError extends Error {
|
|
3590
|
+
readonly code = "CONNECTION_GENERATION_CHANGED";
|
|
3591
|
+
|
|
3592
|
+
constructor() {
|
|
3593
|
+
super("The disconnect operation belongs to an older connection generation");
|
|
3594
|
+
this.name = "ConnectionDisconnectGenerationError";
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3484
3598
|
/** Server-owned verification facts; public schemas expose them read-only and nullable. */
|
|
3485
3599
|
export type ConnectionMetadataWithVerification = ConnectionMetadata & {
|
|
3486
3600
|
verifiedInstallAt: string | null;
|
|
@@ -3863,6 +3977,39 @@ export async function requireFile(
|
|
|
3863
3977
|
return file;
|
|
3864
3978
|
}
|
|
3865
3979
|
|
|
3980
|
+
/** One RLS-scoped query for all attachment metadata needed by a model turn. */
|
|
3981
|
+
export async function getFiles(
|
|
3982
|
+
db: Database,
|
|
3983
|
+
workspaceId: string,
|
|
3984
|
+
fileIds: readonly string[],
|
|
3985
|
+
): Promise<FileAsset[]> {
|
|
3986
|
+
const ids = [...new Set(fileIds)];
|
|
3987
|
+
if (ids.length === 0) return [];
|
|
3988
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3989
|
+
const rows = await scopedDb
|
|
3990
|
+
.select()
|
|
3991
|
+
.from(schema.files)
|
|
3992
|
+
.where(and(eq(schema.files.workspaceId, workspaceId), inArray(schema.files.id, ids)));
|
|
3993
|
+
return rows.map(mapFile);
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
/** Build the canonical user row with stable attachment refs, never inline bytes. */
|
|
3998
|
+
export function durableUserHistoryItem(
|
|
3999
|
+
prompt: string,
|
|
4000
|
+
resources: readonly ResourceRef[],
|
|
4001
|
+
): Record<string, unknown> {
|
|
4002
|
+
const attachmentRefs = resources.filter(
|
|
4003
|
+
(resource): resource is Extract<ResourceRef, { kind: "file" }> => resource.kind === "file",
|
|
4004
|
+
);
|
|
4005
|
+
return sanitizeModelPayload({
|
|
4006
|
+
type: "message",
|
|
4007
|
+
role: "user",
|
|
4008
|
+
content: prompt,
|
|
4009
|
+
...(attachmentRefs.length > 0 ? { [MODEL_ATTACHMENT_REFS_FIELD]: attachmentRefs } : {}),
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
|
|
3866
4013
|
export type RetainedFileArtifact = {
|
|
3867
4014
|
file: FileAsset;
|
|
3868
4015
|
uploadStatus: FileUploadStatus | null;
|
|
@@ -5310,6 +5457,191 @@ export async function updateConnection(
|
|
|
5310
5457
|
);
|
|
5311
5458
|
}
|
|
5312
5459
|
|
|
5460
|
+
async function transitionConnectionStateInScope(
|
|
5461
|
+
db: Database,
|
|
5462
|
+
input: TransitionConnectionStateInput,
|
|
5463
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5464
|
+
const [row] = await db
|
|
5465
|
+
.update(schema.connections)
|
|
5466
|
+
.set({
|
|
5467
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
5468
|
+
metadata: input.metadata,
|
|
5469
|
+
...(input.lastError !== undefined ? { lastError: input.lastError } : {}),
|
|
5470
|
+
version: sql`${schema.connections.version} + 1`,
|
|
5471
|
+
...(input.updatedBySubjectId !== undefined
|
|
5472
|
+
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
5473
|
+
: {}),
|
|
5474
|
+
updatedAt: new Date(),
|
|
5475
|
+
})
|
|
5476
|
+
.where(
|
|
5477
|
+
and(
|
|
5478
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
5479
|
+
eq(schema.connections.id, input.connectionId),
|
|
5480
|
+
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
5481
|
+
eq(schema.connections.version, input.expectedVersion),
|
|
5482
|
+
),
|
|
5483
|
+
)
|
|
5484
|
+
.returning(connectionMetadataColumns);
|
|
5485
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
/**
|
|
5489
|
+
* Applies a metadata/status lifecycle transition behind the connection's
|
|
5490
|
+
* existing `(id, version)` fence. Unlike ordinary metadata edits, every
|
|
5491
|
+
* transition advances the version so concurrent refresh, disconnect,
|
|
5492
|
+
* reconnect, source-selection, and lifecycle operations cannot overwrite one
|
|
5493
|
+
* another from stale snapshots.
|
|
5494
|
+
*/
|
|
5495
|
+
export async function transitionConnectionState(
|
|
5496
|
+
db: Database,
|
|
5497
|
+
input: TransitionConnectionStateInput,
|
|
5498
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5499
|
+
return await withConnectionSubjectRls(
|
|
5500
|
+
db,
|
|
5501
|
+
input.workspaceId,
|
|
5502
|
+
input.visibleToSubjectId,
|
|
5503
|
+
async (scopedDb) => await transitionConnectionStateInScope(scopedDb, input),
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
const connectionDisconnectOperationColumns = {
|
|
5508
|
+
accountId: schema.connectionDisconnectOperations.accountId,
|
|
5509
|
+
workspaceId: schema.connectionDisconnectOperations.workspaceId,
|
|
5510
|
+
connectionId: schema.connectionDisconnectOperations.connectionId,
|
|
5511
|
+
subjectId: schema.connectionDisconnectOperations.subjectId,
|
|
5512
|
+
idempotencyKey: schema.connectionDisconnectOperations.idempotencyKey,
|
|
5513
|
+
expectedVersion: schema.connectionDisconnectOperations.expectedVersion,
|
|
5514
|
+
resultVersion: schema.connectionDisconnectOperations.resultVersion,
|
|
5515
|
+
};
|
|
5516
|
+
|
|
5517
|
+
/**
|
|
5518
|
+
* Disconnects one exact subject-owned connection generation and records the
|
|
5519
|
+
* caller's durable operation identity in the same transaction. Exact retries
|
|
5520
|
+
* converge only while the produced generation remains current; a reconnect or
|
|
5521
|
+
* any other later transition fences the old operation permanently.
|
|
5522
|
+
*/
|
|
5523
|
+
export async function disconnectConnectionIdempotently(
|
|
5524
|
+
db: Database,
|
|
5525
|
+
input: DisconnectConnectionIdempotentlyInput,
|
|
5526
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5527
|
+
return await withConnectionSubjectRls(
|
|
5528
|
+
db,
|
|
5529
|
+
input.workspaceId,
|
|
5530
|
+
input.subjectId,
|
|
5531
|
+
async (scopedDb) => {
|
|
5532
|
+
const [connectionRow] = await scopedDb
|
|
5533
|
+
.select(connectionMetadataColumns)
|
|
5534
|
+
.from(schema.connections)
|
|
5535
|
+
.where(
|
|
5536
|
+
and(
|
|
5537
|
+
eq(schema.connections.accountId, input.accountId),
|
|
5538
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
5539
|
+
eq(schema.connections.id, input.connectionId),
|
|
5540
|
+
eq(schema.connections.subjectId, input.subjectId),
|
|
5541
|
+
),
|
|
5542
|
+
)
|
|
5543
|
+
.for("update");
|
|
5544
|
+
if (!connectionRow) return null;
|
|
5545
|
+
const connection = mapConnectionMetadata(connectionRow);
|
|
5546
|
+
|
|
5547
|
+
const [replayed] = await scopedDb
|
|
5548
|
+
.select(connectionDisconnectOperationColumns)
|
|
5549
|
+
.from(schema.connectionDisconnectOperations)
|
|
5550
|
+
.where(
|
|
5551
|
+
and(
|
|
5552
|
+
eq(schema.connectionDisconnectOperations.workspaceId, input.workspaceId),
|
|
5553
|
+
eq(schema.connectionDisconnectOperations.subjectId, input.subjectId),
|
|
5554
|
+
eq(schema.connectionDisconnectOperations.idempotencyKey, input.idempotencyKey),
|
|
5555
|
+
),
|
|
5556
|
+
)
|
|
5557
|
+
.limit(1);
|
|
5558
|
+
if (replayed) {
|
|
5559
|
+
if (
|
|
5560
|
+
replayed.accountId !== input.accountId ||
|
|
5561
|
+
replayed.connectionId !== input.connectionId ||
|
|
5562
|
+
replayed.expectedVersion !== input.expectedVersion
|
|
5563
|
+
) {
|
|
5564
|
+
throw new ConnectionDisconnectIdempotencyError();
|
|
5565
|
+
}
|
|
5566
|
+
if (connection.version !== replayed.resultVersion) {
|
|
5567
|
+
throw new ConnectionDisconnectGenerationError();
|
|
5568
|
+
}
|
|
5569
|
+
return connection;
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5572
|
+
if (connection.version !== input.expectedVersion) {
|
|
5573
|
+
throw new ConnectionDisconnectGenerationError();
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
const [inserted] = await scopedDb
|
|
5577
|
+
.insert(schema.connectionDisconnectOperations)
|
|
5578
|
+
.values({
|
|
5579
|
+
accountId: input.accountId,
|
|
5580
|
+
workspaceId: input.workspaceId,
|
|
5581
|
+
connectionId: input.connectionId,
|
|
5582
|
+
subjectId: input.subjectId,
|
|
5583
|
+
idempotencyKey: input.idempotencyKey,
|
|
5584
|
+
expectedVersion: input.expectedVersion,
|
|
5585
|
+
resultVersion: input.expectedVersion + 1,
|
|
5586
|
+
})
|
|
5587
|
+
.onConflictDoNothing()
|
|
5588
|
+
.returning(connectionDisconnectOperationColumns);
|
|
5589
|
+
if (!inserted) {
|
|
5590
|
+
const [conflictingKey] = await scopedDb
|
|
5591
|
+
.select(connectionDisconnectOperationColumns)
|
|
5592
|
+
.from(schema.connectionDisconnectOperations)
|
|
5593
|
+
.where(
|
|
5594
|
+
and(
|
|
5595
|
+
eq(schema.connectionDisconnectOperations.workspaceId, input.workspaceId),
|
|
5596
|
+
eq(schema.connectionDisconnectOperations.subjectId, input.subjectId),
|
|
5597
|
+
eq(schema.connectionDisconnectOperations.idempotencyKey, input.idempotencyKey),
|
|
5598
|
+
),
|
|
5599
|
+
)
|
|
5600
|
+
.limit(1);
|
|
5601
|
+
if (
|
|
5602
|
+
!conflictingKey ||
|
|
5603
|
+
conflictingKey.accountId !== input.accountId ||
|
|
5604
|
+
conflictingKey.connectionId !== input.connectionId ||
|
|
5605
|
+
conflictingKey.expectedVersion !== input.expectedVersion
|
|
5606
|
+
) {
|
|
5607
|
+
throw new ConnectionDisconnectIdempotencyError();
|
|
5608
|
+
}
|
|
5609
|
+
if (connection.version !== conflictingKey.resultVersion) {
|
|
5610
|
+
throw new ConnectionDisconnectGenerationError();
|
|
5611
|
+
}
|
|
5612
|
+
return connection;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
const [updated] = await scopedDb
|
|
5616
|
+
.update(schema.connections)
|
|
5617
|
+
.set({
|
|
5618
|
+
status: "revoked",
|
|
5619
|
+
metadata: input.metadata,
|
|
5620
|
+
...(input.lastError !== undefined ? { lastError: input.lastError } : {}),
|
|
5621
|
+
version: sql`${schema.connections.version} + 1`,
|
|
5622
|
+
...(input.updatedBySubjectId !== undefined
|
|
5623
|
+
? { updatedBySubjectId: input.updatedBySubjectId }
|
|
5624
|
+
: {}),
|
|
5625
|
+
updatedAt: new Date(),
|
|
5626
|
+
})
|
|
5627
|
+
.where(
|
|
5628
|
+
and(
|
|
5629
|
+
eq(schema.connections.accountId, input.accountId),
|
|
5630
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
5631
|
+
eq(schema.connections.id, input.connectionId),
|
|
5632
|
+
eq(schema.connections.subjectId, input.subjectId),
|
|
5633
|
+
eq(schema.connections.version, input.expectedVersion),
|
|
5634
|
+
),
|
|
5635
|
+
)
|
|
5636
|
+
.returning(connectionMetadataColumns);
|
|
5637
|
+
if (!updated) {
|
|
5638
|
+
throw new ConnectionDisconnectGenerationError();
|
|
5639
|
+
}
|
|
5640
|
+
return mapConnectionMetadata(updated);
|
|
5641
|
+
},
|
|
5642
|
+
);
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5313
5645
|
async function revokeConnectionInScope(
|
|
5314
5646
|
db: Database,
|
|
5315
5647
|
workspaceId: string,
|
|
@@ -5354,13 +5686,20 @@ export async function revokeConnection(
|
|
|
5354
5686
|
workspaceId: string,
|
|
5355
5687
|
connectionId: string,
|
|
5356
5688
|
updatedBySubjectId?: string | null,
|
|
5689
|
+
expectedVersion?: number,
|
|
5357
5690
|
): Promise<ConnectionMetadataWithVerification | null> {
|
|
5358
5691
|
return await withConnectionSubjectRls(
|
|
5359
5692
|
db,
|
|
5360
5693
|
workspaceId,
|
|
5361
5694
|
updatedBySubjectId,
|
|
5362
5695
|
async (scopedDb) =>
|
|
5363
|
-
await revokeConnectionInScope(
|
|
5696
|
+
await revokeConnectionInScope(
|
|
5697
|
+
scopedDb,
|
|
5698
|
+
workspaceId,
|
|
5699
|
+
connectionId,
|
|
5700
|
+
updatedBySubjectId,
|
|
5701
|
+
expectedVersion,
|
|
5702
|
+
),
|
|
5364
5703
|
);
|
|
5365
5704
|
}
|
|
5366
5705
|
|
|
@@ -5388,6 +5727,7 @@ export type SlackBotUserLink = {
|
|
|
5388
5727
|
export type SlackInteractionTriggerKind =
|
|
5389
5728
|
| "app_mention"
|
|
5390
5729
|
| "dm"
|
|
5730
|
+
| "reaction"
|
|
5391
5731
|
| "slash_command"
|
|
5392
5732
|
| "message_shortcut"
|
|
5393
5733
|
| "thread_reply";
|
|
@@ -5412,6 +5752,7 @@ export type SlackInteractionInboxEntry = {
|
|
|
5412
5752
|
attemptCount: number;
|
|
5413
5753
|
retryAt: Date | null;
|
|
5414
5754
|
lastErrorCode: string | null;
|
|
5755
|
+
reactionContextCheckpoint: unknown | null;
|
|
5415
5756
|
processedAt: Date | null;
|
|
5416
5757
|
createdAt: Date;
|
|
5417
5758
|
updatedAt: Date;
|
|
@@ -5557,6 +5898,7 @@ export async function enqueueSlackInteractionInbox(
|
|
|
5557
5898
|
| "attemptCount"
|
|
5558
5899
|
| "retryAt"
|
|
5559
5900
|
| "lastErrorCode"
|
|
5901
|
+
| "reactionContextCheckpoint"
|
|
5560
5902
|
| "processedAt"
|
|
5561
5903
|
| "createdAt"
|
|
5562
5904
|
| "updatedAt"
|
|
@@ -5619,6 +5961,7 @@ export async function settleSlackInteractionInbox(
|
|
|
5619
5961
|
claimHolderId: null,
|
|
5620
5962
|
claimExpiresAt: null,
|
|
5621
5963
|
retryAt: null,
|
|
5964
|
+
reactionContextCheckpoint: null,
|
|
5622
5965
|
processedAt: sql`now()`,
|
|
5623
5966
|
lastErrorCode: input.errorCode ?? null,
|
|
5624
5967
|
updatedAt: sql`now()`,
|
|
@@ -5635,6 +5978,42 @@ export async function settleSlackInteractionInbox(
|
|
|
5635
5978
|
});
|
|
5636
5979
|
}
|
|
5637
5980
|
|
|
5981
|
+
export async function saveSlackInteractionInboxReactionCheckpoint(
|
|
5982
|
+
db: Database,
|
|
5983
|
+
input: {
|
|
5984
|
+
entry: Pick<
|
|
5985
|
+
SlackInteractionInboxEntry,
|
|
5986
|
+
"id" | "accountId" | "workspaceId" | "connectionId" | "providerEventId" | "providerMessageId"
|
|
5987
|
+
>;
|
|
5988
|
+
claimHolderId: string;
|
|
5989
|
+
checkpoint: unknown;
|
|
5990
|
+
},
|
|
5991
|
+
): Promise<boolean> {
|
|
5992
|
+
return await withRlsContext(db, input.entry, async (scopedDb) => {
|
|
5993
|
+
const rows = await scopedDb
|
|
5994
|
+
.update(schema.slackInteractionInbox)
|
|
5995
|
+
.set({
|
|
5996
|
+
reactionContextCheckpoint: input.checkpoint,
|
|
5997
|
+
updatedAt: sql`now()`,
|
|
5998
|
+
})
|
|
5999
|
+
.where(
|
|
6000
|
+
and(
|
|
6001
|
+
eq(schema.slackInteractionInbox.id, input.entry.id),
|
|
6002
|
+
eq(schema.slackInteractionInbox.accountId, input.entry.accountId),
|
|
6003
|
+
eq(schema.slackInteractionInbox.workspaceId, input.entry.workspaceId),
|
|
6004
|
+
eq(schema.slackInteractionInbox.connectionId, input.entry.connectionId),
|
|
6005
|
+
eq(schema.slackInteractionInbox.providerEventId, input.entry.providerEventId),
|
|
6006
|
+
eq(schema.slackInteractionInbox.providerMessageId, input.entry.providerMessageId),
|
|
6007
|
+
eq(schema.slackInteractionInbox.triggerKind, "reaction"),
|
|
6008
|
+
eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
|
|
6009
|
+
eq(schema.slackInteractionInbox.status, "processing"),
|
|
6010
|
+
),
|
|
6011
|
+
)
|
|
6012
|
+
.returning({ id: schema.slackInteractionInbox.id });
|
|
6013
|
+
return rows.length === 1;
|
|
6014
|
+
});
|
|
6015
|
+
}
|
|
6016
|
+
|
|
5638
6017
|
export async function releaseSlackInteractionInbox(
|
|
5639
6018
|
db: Database,
|
|
5640
6019
|
input: {
|
|
@@ -5731,6 +6110,57 @@ export async function getSlackInteractionByRoute(
|
|
|
5731
6110
|
});
|
|
5732
6111
|
}
|
|
5733
6112
|
|
|
6113
|
+
/**
|
|
6114
|
+
* Resolve an already-durable Slack task to its canonical interaction.
|
|
6115
|
+
*
|
|
6116
|
+
* Client event ids are unique only inside one session, so this lookup also
|
|
6117
|
+
* fences the exact workspace and Slack connection. The reservation id is the
|
|
6118
|
+
* canonical Slack session identity both before and after binding; joining it
|
|
6119
|
+
* lets a retry repair a crash after the task committed but before the route was
|
|
6120
|
+
* bound. Two matches indicate a cross-session collision and fail closed rather
|
|
6121
|
+
* than choosing an arbitrary route.
|
|
6122
|
+
*/
|
|
6123
|
+
export async function getSlackInteractionByClientEventId(
|
|
6124
|
+
db: Database,
|
|
6125
|
+
workspaceId: string,
|
|
6126
|
+
connectionId: string,
|
|
6127
|
+
clientEventId: string,
|
|
6128
|
+
): Promise<{ interaction: SlackInteraction; eventSessionId: string } | null> {
|
|
6129
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
6130
|
+
const rows = await scopedDb
|
|
6131
|
+
.select({
|
|
6132
|
+
interaction: schema.slackInteractions,
|
|
6133
|
+
eventSessionId: schema.sessionEvents.sessionId,
|
|
6134
|
+
})
|
|
6135
|
+
.from(schema.slackInteractions)
|
|
6136
|
+
.innerJoin(
|
|
6137
|
+
schema.sessionEvents,
|
|
6138
|
+
and(
|
|
6139
|
+
eq(schema.sessionEvents.workspaceId, schema.slackInteractions.workspaceId),
|
|
6140
|
+
eq(schema.sessionEvents.sessionId, schema.slackInteractions.sessionReservationId),
|
|
6141
|
+
),
|
|
6142
|
+
)
|
|
6143
|
+
.where(
|
|
6144
|
+
and(
|
|
6145
|
+
eq(schema.slackInteractions.workspaceId, workspaceId),
|
|
6146
|
+
eq(schema.slackInteractions.connectionId, connectionId),
|
|
6147
|
+
eq(schema.sessionEvents.clientEventId, clientEventId),
|
|
6148
|
+
eq(schema.sessionEvents.type, "user.message"),
|
|
6149
|
+
),
|
|
6150
|
+
)
|
|
6151
|
+
.limit(2);
|
|
6152
|
+
if (rows.length > 1) {
|
|
6153
|
+
throw new Error("Slack client event id resolved to multiple canonical interactions");
|
|
6154
|
+
}
|
|
6155
|
+
const row = rows[0];
|
|
6156
|
+
if (!row) return null;
|
|
6157
|
+
return {
|
|
6158
|
+
interaction: mapSlackInteraction(row.interaction),
|
|
6159
|
+
eventSessionId: row.eventSessionId,
|
|
6160
|
+
};
|
|
6161
|
+
});
|
|
6162
|
+
}
|
|
6163
|
+
|
|
5734
6164
|
export async function getSlackInteractionSessionAccess(
|
|
5735
6165
|
db: Database,
|
|
5736
6166
|
workspaceId: string,
|
|
@@ -6178,6 +6608,8 @@ function mapSlackInteractionInbox(
|
|
|
6178
6608
|
attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
|
|
6179
6609
|
retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
|
|
6180
6610
|
lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
|
|
6611
|
+
reactionContextCheckpoint:
|
|
6612
|
+
slackRowValue(row, "reactionContextCheckpoint", "reaction_context_checkpoint") ?? null,
|
|
6181
6613
|
processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
|
|
6182
6614
|
createdAt: slackRowDate(row, "createdAt", "created_at"),
|
|
6183
6615
|
updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
|
|
@@ -16693,6 +17125,7 @@ export type SessionCreateInput = {
|
|
|
16693
17125
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
16694
17126
|
firstPartyMcpTools?: FirstPartyMcpToolName[];
|
|
16695
17127
|
instructions?: string | null;
|
|
17128
|
+
policyRole?: string | null;
|
|
16696
17129
|
parentSessionId?: string | null;
|
|
16697
17130
|
createIdempotencyKey?: string | null;
|
|
16698
17131
|
sandboxGroupId?: string | null;
|
|
@@ -16849,6 +17282,9 @@ async function resolveSessionDepthDecision(
|
|
|
16849
17282
|
if (!parent) {
|
|
16850
17283
|
throw new Error(`Parent session not found: ${parentSessionId}`);
|
|
16851
17284
|
}
|
|
17285
|
+
if (parent.status === "cancelled") {
|
|
17286
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot create children");
|
|
17287
|
+
}
|
|
16852
17288
|
}
|
|
16853
17289
|
|
|
16854
17290
|
const currentParentDepth = parent?.nestedAgentDepth ?? 0;
|
|
@@ -17104,6 +17540,7 @@ async function createSessionInTransaction(
|
|
|
17104
17540
|
firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
|
|
17105
17541
|
initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
17106
17542
|
instructions: input.instructions ?? null,
|
|
17543
|
+
policyRole: input.policyRole ?? null,
|
|
17107
17544
|
parentSessionId: input.parentSessionId ?? null,
|
|
17108
17545
|
parentTurnId,
|
|
17109
17546
|
createIdempotencyKey,
|
|
@@ -20249,21 +20686,9 @@ function validateAnsweredHumanInput(
|
|
|
20249
20686
|
`Text question ${question.id} accepts one value`,
|
|
20250
20687
|
);
|
|
20251
20688
|
}
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
if (minLength != null && value.length < minLength) {
|
|
20256
|
-
throw new HumanInputResponseValidationError(
|
|
20257
|
-
"INVALID_RESPONSE",
|
|
20258
|
-
`Question ${question.id} is shorter than its minimum length`,
|
|
20259
|
-
);
|
|
20260
|
-
}
|
|
20261
|
-
if (maxLength != null && value.length > maxLength) {
|
|
20262
|
-
throw new HumanInputResponseValidationError(
|
|
20263
|
-
"INVALID_RESPONSE",
|
|
20264
|
-
`Question ${question.id} exceeds its maximum length`,
|
|
20265
|
-
);
|
|
20266
|
-
}
|
|
20689
|
+
// Text answers have no agent-chosen char bounds — only the platform
|
|
20690
|
+
// string cap on HumanInputAnswer.values. Ignore legacy minLength/maxLength
|
|
20691
|
+
// still present on older persisted question JSON.
|
|
20267
20692
|
continue;
|
|
20268
20693
|
}
|
|
20269
20694
|
if (other && !question.allowOther) {
|
|
@@ -20574,6 +20999,27 @@ export async function acceptSessionHumanInputResponse(
|
|
|
20574
20999
|
})
|
|
20575
21000
|
.returning();
|
|
20576
21001
|
if (!event) throw new Error("Failed to append human-input response");
|
|
21002
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
21003
|
+
accountId: session.accountId,
|
|
21004
|
+
workspaceId: input.workspaceId,
|
|
21005
|
+
sessionId: input.sessionId,
|
|
21006
|
+
sourceKind: "human_input_response",
|
|
21007
|
+
sourceId: event.id,
|
|
21008
|
+
turnId: turn.id,
|
|
21009
|
+
channel:
|
|
21010
|
+
response.outcome === "answered" || response.outcome === "skipped" ? "speakable" : null,
|
|
21011
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
21012
|
+
requestId: request.id,
|
|
21013
|
+
questions: request.questions,
|
|
21014
|
+
response,
|
|
21015
|
+
}),
|
|
21016
|
+
payload: {
|
|
21017
|
+
requestId: request.id,
|
|
21018
|
+
outcome: response.outcome,
|
|
21019
|
+
sourceEventId: event.id,
|
|
21020
|
+
},
|
|
21021
|
+
now,
|
|
21022
|
+
});
|
|
20577
21023
|
await tx
|
|
20578
21024
|
.update(schema.sessions)
|
|
20579
21025
|
.set({ lastSequence: session.lastSequence + 1, updatedAt: now })
|
|
@@ -21298,6 +21744,29 @@ export async function getActiveSessionHistoryItems(
|
|
|
21298
21744
|
});
|
|
21299
21745
|
}
|
|
21300
21746
|
|
|
21747
|
+
/** Bounded finalized voice turns used only to resume a later realtime call. */
|
|
21748
|
+
export async function getSessionRealtimeContinuityEntries(
|
|
21749
|
+
db: Database,
|
|
21750
|
+
workspaceId: string,
|
|
21751
|
+
sessionId: string,
|
|
21752
|
+
maximumEntries = 20,
|
|
21753
|
+
): Promise<SessionRealtimeContinuityEntry[]> {
|
|
21754
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
21755
|
+
const [session] = await scopedDb
|
|
21756
|
+
.select({ accountId: schema.sessions.accountId })
|
|
21757
|
+
.from(schema.sessions)
|
|
21758
|
+
.where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
|
|
21759
|
+
.limit(1);
|
|
21760
|
+
if (!session) return [];
|
|
21761
|
+
return await listSessionRealtimeContinuityEntriesInTransaction(scopedDb, {
|
|
21762
|
+
accountId: session.accountId,
|
|
21763
|
+
workspaceId,
|
|
21764
|
+
sessionId,
|
|
21765
|
+
maximumEntries,
|
|
21766
|
+
});
|
|
21767
|
+
});
|
|
21768
|
+
}
|
|
21769
|
+
|
|
21301
21770
|
/**
|
|
21302
21771
|
* Count of ACTIVE (live, model-facing) history rows for a session. This is the
|
|
21303
21772
|
* length of the history the next turn is seeded from — the dual-write slice
|
|
@@ -36074,6 +36543,8 @@ export type InitializeSessionStartInput = {
|
|
|
36074
36543
|
subjectId: string;
|
|
36075
36544
|
expectedRevision: number;
|
|
36076
36545
|
} | null;
|
|
36546
|
+
/** Persist session.created only; realtime will supply the first human turn. */
|
|
36547
|
+
deferInitialTurn?: boolean;
|
|
36077
36548
|
};
|
|
36078
36549
|
|
|
36079
36550
|
export type InitializeSessionStartResult = {
|
|
@@ -36159,6 +36630,95 @@ export async function initializeSessionStartAtomically(
|
|
|
36159
36630
|
if (!goal) throw new Error("Failed to create initial session goal");
|
|
36160
36631
|
}
|
|
36161
36632
|
|
|
36633
|
+
if (input.deferInitialTurn) {
|
|
36634
|
+
const [existingCreatedEvent] = await tx
|
|
36635
|
+
.select({ id: schema.sessionEvents.id })
|
|
36636
|
+
.from(schema.sessionEvents)
|
|
36637
|
+
.where(
|
|
36638
|
+
and(
|
|
36639
|
+
eq(schema.sessionEvents.workspaceId, input.workspaceId),
|
|
36640
|
+
eq(schema.sessionEvents.sessionId, session.id),
|
|
36641
|
+
eq(schema.sessionEvents.type, "session.created"),
|
|
36642
|
+
),
|
|
36643
|
+
)
|
|
36644
|
+
.limit(1);
|
|
36645
|
+
let sequence = session.lastSequence;
|
|
36646
|
+
let initializedNow = false;
|
|
36647
|
+
let insertedEvents: Array<typeof schema.sessionEvents.$inferSelect> = [];
|
|
36648
|
+
if (!existingCreatedEvent) {
|
|
36649
|
+
insertedEvents = await tx
|
|
36650
|
+
.insert(schema.sessionEvents)
|
|
36651
|
+
.values([
|
|
36652
|
+
{
|
|
36653
|
+
accountId: session.accountId,
|
|
36654
|
+
workspaceId: input.workspaceId,
|
|
36655
|
+
sessionId: session.id,
|
|
36656
|
+
sequence: ++sequence,
|
|
36657
|
+
type: "session.created",
|
|
36658
|
+
payload: sanitizeEventPayload({
|
|
36659
|
+
...input.createdEventPayload,
|
|
36660
|
+
status: "idle",
|
|
36661
|
+
createdBy: creator.initiator,
|
|
36662
|
+
}),
|
|
36663
|
+
},
|
|
36664
|
+
...(goal
|
|
36665
|
+
? [
|
|
36666
|
+
{
|
|
36667
|
+
accountId: session.accountId,
|
|
36668
|
+
workspaceId: input.workspaceId,
|
|
36669
|
+
sessionId: session.id,
|
|
36670
|
+
sequence: ++sequence,
|
|
36671
|
+
type: "goal.set" as const,
|
|
36672
|
+
payload: sanitizeEventPayload({
|
|
36673
|
+
goalId: goal.id,
|
|
36674
|
+
text: goal.text,
|
|
36675
|
+
...(goal.successCriteria
|
|
36676
|
+
? { successCriteria: goal.successCriteria }
|
|
36677
|
+
: {}),
|
|
36678
|
+
version: goal.version,
|
|
36679
|
+
actor: "api",
|
|
36680
|
+
replaced: false,
|
|
36681
|
+
}),
|
|
36682
|
+
},
|
|
36683
|
+
]
|
|
36684
|
+
: []),
|
|
36685
|
+
])
|
|
36686
|
+
.returning();
|
|
36687
|
+
initializedNow = true;
|
|
36688
|
+
}
|
|
36689
|
+
await tx
|
|
36690
|
+
.update(schema.sessions)
|
|
36691
|
+
.set({
|
|
36692
|
+
temporalWorkflowId,
|
|
36693
|
+
lastSequence: sequence,
|
|
36694
|
+
...(initializedNow && session.status === "queued" ? { status: "idle" } : {}),
|
|
36695
|
+
updatedAt: new Date(),
|
|
36696
|
+
})
|
|
36697
|
+
.where(
|
|
36698
|
+
and(
|
|
36699
|
+
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
36700
|
+
eq(schema.sessions.id, session.id),
|
|
36701
|
+
),
|
|
36702
|
+
);
|
|
36703
|
+
if (initializedNow && input.consumeNewSessionDraft) {
|
|
36704
|
+
await setSubjectRlsContext(
|
|
36705
|
+
tx as unknown as Database,
|
|
36706
|
+
input.consumeNewSessionDraft.subjectId,
|
|
36707
|
+
);
|
|
36708
|
+
await seedNewSessionDraftInTransaction(tx as unknown as Database, {
|
|
36709
|
+
workspaceId: input.workspaceId,
|
|
36710
|
+
subjectId: input.consumeNewSessionDraft.subjectId,
|
|
36711
|
+
expectedRevision: input.consumeNewSessionDraft.expectedRevision,
|
|
36712
|
+
});
|
|
36713
|
+
}
|
|
36714
|
+
return {
|
|
36715
|
+
events: insertedEvents.map(mapEvent),
|
|
36716
|
+
turn: null,
|
|
36717
|
+
temporalWorkflowId,
|
|
36718
|
+
workflowWakeRevision: null,
|
|
36719
|
+
};
|
|
36720
|
+
}
|
|
36721
|
+
|
|
36162
36722
|
const existingUserEvents = await tx
|
|
36163
36723
|
.select()
|
|
36164
36724
|
.from(schema.sessionEvents)
|
|
@@ -36262,6 +36822,7 @@ export async function initializeSessionStartAtomically(
|
|
|
36262
36822
|
let queueTailPosition = Number(session.queueTailPosition);
|
|
36263
36823
|
if (!turn) {
|
|
36264
36824
|
queueTailPosition += 1;
|
|
36825
|
+
const acceptedAt = new Date();
|
|
36265
36826
|
[turn] = await tx
|
|
36266
36827
|
.insert(schema.sessionTurns)
|
|
36267
36828
|
.values({
|
|
@@ -36293,10 +36854,14 @@ export async function initializeSessionStartAtomically(
|
|
|
36293
36854
|
: {},
|
|
36294
36855
|
lineage: {},
|
|
36295
36856
|
...initiatorColumns(creator),
|
|
36857
|
+
initiatingHumanSubjectId:
|
|
36858
|
+
creator.initiator.kind === "subject" ? creator.initiator.subjectId : null,
|
|
36296
36859
|
personalConnectionDelegations: parsedPersonalConnectionDelegations(
|
|
36297
36860
|
session.initialPersonalConnectionDelegations,
|
|
36298
36861
|
`sessions:${session.workspaceId}:${session.id}:initial`,
|
|
36299
36862
|
),
|
|
36863
|
+
createdAt: acceptedAt,
|
|
36864
|
+
updatedAt: acceptedAt,
|
|
36300
36865
|
})
|
|
36301
36866
|
.returning();
|
|
36302
36867
|
if (!turn) throw new Error("Failed to create initial session turn");
|
|
@@ -36430,6 +36995,7 @@ export async function enqueueSessionTurn(
|
|
|
36430
36995
|
const position = atHead
|
|
36431
36996
|
? Number(lockedSession.queueHeadPosition) - 1
|
|
36432
36997
|
: Number(lockedSession.queueTailPosition) + 1;
|
|
36998
|
+
const acceptedAt = new Date();
|
|
36433
36999
|
const [row] = await tx
|
|
36434
37000
|
.insert(schema.sessionTurns)
|
|
36435
37001
|
.values({
|
|
@@ -36457,7 +37023,11 @@ export async function enqueueSessionTurn(
|
|
|
36457
37023
|
initiator: input.initiator,
|
|
36458
37024
|
context: input.initiatorContext ?? {},
|
|
36459
37025
|
}),
|
|
37026
|
+
initiatingHumanSubjectId:
|
|
37027
|
+
input.initiator.kind === "subject" ? input.initiator.subjectId : null,
|
|
36460
37028
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
37029
|
+
createdAt: acceptedAt,
|
|
37030
|
+
updatedAt: acceptedAt,
|
|
36461
37031
|
})
|
|
36462
37032
|
.returning();
|
|
36463
37033
|
if (!row) {
|
|
@@ -36893,8 +37463,14 @@ export async function claimSessionWorkForAttempt(
|
|
|
36893
37463
|
sessionId,
|
|
36894
37464
|
{ workspaceControl },
|
|
36895
37465
|
);
|
|
36896
|
-
|
|
37466
|
+
let session = prefix.sessions[0];
|
|
36897
37467
|
if (!session) return { action: "unclaimed", reason: "no-work" };
|
|
37468
|
+
// Work and realtime may coexist, but claim remains the lazy lifecycle
|
|
37469
|
+
// cleanup point for an expired voice lease.
|
|
37470
|
+
session = await settleExpiredSessionRealtimeInTransaction(
|
|
37471
|
+
tx as unknown as Database,
|
|
37472
|
+
session,
|
|
37473
|
+
);
|
|
36898
37474
|
if (effectiveControl.state !== "active") {
|
|
36899
37475
|
return { action: "unclaimed", reason: "gate-closed" };
|
|
36900
37476
|
}
|
|
@@ -36974,7 +37550,37 @@ export async function claimSessionWorkForAttempt(
|
|
|
36974
37550
|
if (connectorPolicyRows.length > 2048) {
|
|
36975
37551
|
throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
|
|
36976
37552
|
}
|
|
36977
|
-
|
|
37553
|
+
// New queued, compaction, and internal-update executions are born
|
|
37554
|
+
// while the locked session still has no active-turn pointer. Publish
|
|
37555
|
+
// that ownership edge before freezing governance so the database
|
|
37556
|
+
// functions can require the complete exact-current-attempt chain.
|
|
37557
|
+
// The enclosing transaction keeps this invisible unless every later
|
|
37558
|
+
// claim/event/session settlement step succeeds.
|
|
37559
|
+
if (session.activeTurnId !== turn.id) {
|
|
37560
|
+
if (session.activeTurnId !== null) {
|
|
37561
|
+
throw new SessionControlInvariantError(
|
|
37562
|
+
`Attempt ${input.attemptId} cannot replace active turn ${session.activeTurnId}`,
|
|
37563
|
+
);
|
|
37564
|
+
}
|
|
37565
|
+
const [claimedSession] = await tx
|
|
37566
|
+
.update(schema.sessions)
|
|
37567
|
+
.set({ activeTurnId: turn.id })
|
|
37568
|
+
.where(
|
|
37569
|
+
and(
|
|
37570
|
+
eq(schema.sessions.accountId, session.accountId),
|
|
37571
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
37572
|
+
eq(schema.sessions.id, sessionId),
|
|
37573
|
+
isNull(schema.sessions.activeTurnId),
|
|
37574
|
+
),
|
|
37575
|
+
)
|
|
37576
|
+
.returning({ id: schema.sessions.id });
|
|
37577
|
+
if (!claimedSession) {
|
|
37578
|
+
throw new SessionControlInvariantError(
|
|
37579
|
+
`Attempt ${input.attemptId} could not establish active turn ${turn.id}`,
|
|
37580
|
+
);
|
|
37581
|
+
}
|
|
37582
|
+
}
|
|
37583
|
+
const attempt = await registerSessionTurnAttemptClaim(tx as unknown as Database, {
|
|
36978
37584
|
id: input.attemptId,
|
|
36979
37585
|
accountId: session.accountId,
|
|
36980
37586
|
workspaceId,
|
|
@@ -36988,6 +37594,42 @@ export async function claimSessionWorkForAttempt(
|
|
|
36988
37594
|
mcpApprovalPolicies,
|
|
36989
37595
|
connectorActionPolicies: connectorPolicyRows,
|
|
36990
37596
|
});
|
|
37597
|
+
// Freeze governance only after this exact attempt is durably claimed,
|
|
37598
|
+
// while the claim transaction still owns the session/turn/attempt
|
|
37599
|
+
// locks. Later policy or preference activation applies to a future
|
|
37600
|
+
// attempt and can never move accepted queued/in-flight work.
|
|
37601
|
+
await tx.execute(sql`
|
|
37602
|
+
SELECT snapshot.id
|
|
37603
|
+
FROM workspace_instruction_policy_get_or_create_snapshot(
|
|
37604
|
+
${session.accountId}::uuid,
|
|
37605
|
+
${workspaceId}::uuid,
|
|
37606
|
+
${sessionId}::uuid,
|
|
37607
|
+
${turn.id}::uuid,
|
|
37608
|
+
${input.attemptId}::uuid,
|
|
37609
|
+
${turn.executionGeneration}
|
|
37610
|
+
) snapshot
|
|
37611
|
+
`);
|
|
37612
|
+
const initiatingHumanSubjectId =
|
|
37613
|
+
turn.initiatingHumanSubjectId ??
|
|
37614
|
+
(turn.initiatorKind === "subject" ? turn.initiatorSubjectId : null);
|
|
37615
|
+
if (initiatingHumanSubjectId) {
|
|
37616
|
+
await tx.execute(sql`
|
|
37617
|
+
SELECT snapshot.id
|
|
37618
|
+
FROM preference_registry_get_or_create_snapshot(
|
|
37619
|
+
${session.accountId}::uuid,
|
|
37620
|
+
${workspaceId}::uuid,
|
|
37621
|
+
${sessionId}::uuid,
|
|
37622
|
+
${turn.id}::uuid,
|
|
37623
|
+
${input.attemptId}::uuid,
|
|
37624
|
+
${turn.executionGeneration}
|
|
37625
|
+
) snapshot
|
|
37626
|
+
`);
|
|
37627
|
+
// The preference function applies the immutable initiating human
|
|
37628
|
+
// only transaction-locally. Clear it before the rest of claim
|
|
37629
|
+
// settlement so no unrelated subject-scoped read inherits it.
|
|
37630
|
+
await tx.execute(sql`SELECT set_config('opengeni.subject_id', '', true)`);
|
|
37631
|
+
}
|
|
37632
|
+
return attempt;
|
|
36991
37633
|
};
|
|
36992
37634
|
if (session.activeTurnId !== null) {
|
|
36993
37635
|
const [activeTurnPreview] = await tx
|
|
@@ -37335,6 +37977,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37335
37977
|
latencyMode: schema.sessionTurns.latencyMode,
|
|
37336
37978
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37337
37979
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
37980
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
37981
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
37982
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37338
37983
|
})
|
|
37339
37984
|
.from(schema.sessionTurns)
|
|
37340
37985
|
.where(
|
|
@@ -37384,8 +38029,15 @@ export async function claimSessionWorkForAttempt(
|
|
|
37384
38029
|
},
|
|
37385
38030
|
),
|
|
37386
38031
|
...initiatorColumns(compactionInitiator),
|
|
38032
|
+
initiatingHumanSubjectId:
|
|
38033
|
+
latestStarted?.initiatingHumanSubjectId ??
|
|
38034
|
+
(latestStarted?.initiatorKind === "subject"
|
|
38035
|
+
? latestStarted.initiatorSubjectId
|
|
38036
|
+
: null),
|
|
37387
38037
|
personalConnectionDelegations: [],
|
|
37388
38038
|
startedAt: now,
|
|
38039
|
+
createdAt: now,
|
|
38040
|
+
updatedAt: now,
|
|
37389
38041
|
})
|
|
37390
38042
|
.returning();
|
|
37391
38043
|
if (!compactionTurn) throw new Error("Failed to create context compaction execution");
|
|
@@ -37634,6 +38286,9 @@ export async function claimSessionWorkForAttempt(
|
|
|
37634
38286
|
tools: schema.sessionTurns.tools,
|
|
37635
38287
|
sandboxBackend: schema.sessionTurns.sandboxBackend,
|
|
37636
38288
|
sandboxOs: schema.sessionTurns.sandboxOs,
|
|
38289
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
38290
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
38291
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
37637
38292
|
})
|
|
37638
38293
|
.from(schema.sessionTurns)
|
|
37639
38294
|
.where(
|
|
@@ -37668,6 +38323,39 @@ export async function claimSessionWorkForAttempt(
|
|
|
37668
38323
|
typeof goalPolicy?.sandboxBackend === "string"
|
|
37669
38324
|
? goalPolicy.sandboxBackend
|
|
37670
38325
|
: (latestStarted?.sandboxBackend ?? session.sandboxBackend);
|
|
38326
|
+
let initiatingHumanSubjectId =
|
|
38327
|
+
internalInitiator.initiator.kind === "subject"
|
|
38328
|
+
? internalInitiator.initiator.subjectId
|
|
38329
|
+
: null;
|
|
38330
|
+
if (!initiatingHumanSubjectId && routingGoalUpdate) {
|
|
38331
|
+
const causalTurnIdValue =
|
|
38332
|
+
routingGoalUpdate.lineage &&
|
|
38333
|
+
typeof routingGoalUpdate.lineage === "object" &&
|
|
38334
|
+
!Array.isArray(routingGoalUpdate.lineage)
|
|
38335
|
+
? (routingGoalUpdate.lineage as Record<string, unknown>).causalTurnId
|
|
38336
|
+
: null;
|
|
38337
|
+
const causalTurnId = typeof causalTurnIdValue === "string" ? causalTurnIdValue : null;
|
|
38338
|
+
if (causalTurnId) {
|
|
38339
|
+
const [causalTurn] = await tx
|
|
38340
|
+
.select({
|
|
38341
|
+
initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
|
|
38342
|
+
initiatorKind: schema.sessionTurns.initiatorKind,
|
|
38343
|
+
initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
|
|
38344
|
+
})
|
|
38345
|
+
.from(schema.sessionTurns)
|
|
38346
|
+
.where(
|
|
38347
|
+
and(
|
|
38348
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
38349
|
+
eq(schema.sessionTurns.sessionId, sessionId),
|
|
38350
|
+
eq(schema.sessionTurns.id, causalTurnId),
|
|
38351
|
+
),
|
|
38352
|
+
)
|
|
38353
|
+
.limit(1);
|
|
38354
|
+
initiatingHumanSubjectId =
|
|
38355
|
+
causalTurn?.initiatingHumanSubjectId ??
|
|
38356
|
+
(causalTurn?.initiatorKind === "subject" ? causalTurn.initiatorSubjectId : null);
|
|
38357
|
+
}
|
|
38358
|
+
}
|
|
37671
38359
|
await tx.execute(sql`set local opengeni.session_inference_claim = '1'`);
|
|
37672
38360
|
const [internalTurn] = await tx
|
|
37673
38361
|
.insert(schema.sessionTurns)
|
|
@@ -37699,8 +38387,11 @@ export async function claimSessionWorkForAttempt(
|
|
|
37699
38387
|
{ id: input.dispatchId, generation: 1, triggerEventId },
|
|
37700
38388
|
),
|
|
37701
38389
|
...initiatorColumns(internalInitiator),
|
|
38390
|
+
initiatingHumanSubjectId,
|
|
37702
38391
|
personalConnectionDelegations: internalPersonalConnectionDelegations,
|
|
37703
38392
|
startedAt: now,
|
|
38393
|
+
createdAt: now,
|
|
38394
|
+
updatedAt: now,
|
|
37704
38395
|
})
|
|
37705
38396
|
.returning();
|
|
37706
38397
|
if (!internalTurn) throw new Error("Failed to create internal update inference");
|
|
@@ -37813,20 +38504,34 @@ export async function claimSessionWorkForAttempt(
|
|
|
37813
38504
|
sessionId,
|
|
37814
38505
|
turnId: row.id,
|
|
37815
38506
|
position: Number(historyPosition),
|
|
37816
|
-
item:
|
|
37817
|
-
|
|
37818
|
-
|
|
37819
|
-
|
|
37820
|
-
}),
|
|
38507
|
+
item: durableUserHistoryItem(
|
|
38508
|
+
row.prompt,
|
|
38509
|
+
Array.isArray(row.resources) ? (row.resources as ResourceRef[]) : [],
|
|
38510
|
+
),
|
|
37821
38511
|
producerCodexCredentialId: null,
|
|
37822
38512
|
});
|
|
37823
|
-
const
|
|
37824
|
-
|
|
37825
|
-
|
|
37826
|
-
|
|
37827
|
-
|
|
37828
|
-
|
|
37829
|
-
|
|
38513
|
+
const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
|
|
38514
|
+
// Cross-session updates are already projected through
|
|
38515
|
+
// delegation.context.append. Keep them pending instead of consuming
|
|
38516
|
+
// them as hidden context on the provider-delegated ordinary turn.
|
|
38517
|
+
const delivered = providerDelegatedTurn
|
|
38518
|
+
? {
|
|
38519
|
+
count: 0,
|
|
38520
|
+
lastSequence: session.lastSequence,
|
|
38521
|
+
triggerEventId: null,
|
|
38522
|
+
historyItemId: null,
|
|
38523
|
+
historyItem: null,
|
|
38524
|
+
updates: [] as Array<typeof schema.sessionSystemUpdates.$inferSelect>,
|
|
38525
|
+
events: [] as Array<typeof schema.sessionEvents.$inferInsert>,
|
|
38526
|
+
event: null,
|
|
38527
|
+
}
|
|
38528
|
+
: await deliverPendingUpdates(
|
|
38529
|
+
session.accountId,
|
|
38530
|
+
row.id,
|
|
38531
|
+
row.executionGeneration,
|
|
38532
|
+
session.lastSequence + 1,
|
|
38533
|
+
now,
|
|
38534
|
+
);
|
|
37830
38535
|
await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
|
|
37831
38536
|
if (delivered.events.length > 0) {
|
|
37832
38537
|
await tx.insert(schema.sessionEvents).values(delivered.events);
|
|
@@ -38400,15 +39105,23 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38400
39105
|
throw new Error(`Live interrupted attempt ${attemptId} lost its exact turn ownership`);
|
|
38401
39106
|
}
|
|
38402
39107
|
|
|
38403
|
-
const
|
|
38404
|
-
const
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
38411
|
-
|
|
39108
|
+
const terminalCancel = session.status === "cancelled";
|
|
39109
|
+
const steer =
|
|
39110
|
+
!terminalCancel && interruptions.some((interruption) => interruption.kind === "steer");
|
|
39111
|
+
const outcome: SessionTurnAttemptOutcome = terminalCancel
|
|
39112
|
+
? "cancelled"
|
|
39113
|
+
: steer
|
|
39114
|
+
? "superseded"
|
|
39115
|
+
: "interrupted_recoverable";
|
|
39116
|
+
const reason = terminalCancel
|
|
39117
|
+
? "session_cancelled"
|
|
39118
|
+
: steer
|
|
39119
|
+
? "steer"
|
|
39120
|
+
: interruptions.some((interruption) => interruption.kind === "workspace_pause")
|
|
39121
|
+
? "workspace_pause"
|
|
39122
|
+
: interruptions.some((interruption) => interruption.kind === "maintenance")
|
|
39123
|
+
? "maintenance"
|
|
39124
|
+
: "session_pause";
|
|
38412
39125
|
let sequence = session.lastSequence;
|
|
38413
39126
|
const closedTools = await closePendingSessionToolCallsInTransaction(
|
|
38414
39127
|
tx as unknown as Database,
|
|
@@ -38433,38 +39146,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38433
39146
|
outcome,
|
|
38434
39147
|
closedAt: now,
|
|
38435
39148
|
});
|
|
38436
|
-
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> =
|
|
39149
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = terminalCancel
|
|
38437
39150
|
? [
|
|
38438
39151
|
{
|
|
38439
39152
|
accountId: session.accountId,
|
|
38440
39153
|
workspaceId,
|
|
38441
39154
|
sessionId,
|
|
38442
39155
|
sequence: ++sequence,
|
|
38443
|
-
type: "turn.
|
|
38444
|
-
turnId: turn.id,
|
|
38445
|
-
turnGeneration: turn.executionGeneration,
|
|
38446
|
-
turnAttemptId: attemptId,
|
|
38447
|
-
turnAssociation: "current",
|
|
38448
|
-
payload: sanitizeEventPayload({ reason: "steer" }),
|
|
38449
|
-
occurredAt: now,
|
|
38450
|
-
},
|
|
38451
|
-
{
|
|
38452
|
-
accountId: session.accountId,
|
|
38453
|
-
workspaceId,
|
|
38454
|
-
sessionId,
|
|
38455
|
-
sequence: ++sequence,
|
|
38456
|
-
type: "session.status.changed",
|
|
38457
|
-
payload: sanitizeEventPayload({ status: "queued" }),
|
|
38458
|
-
occurredAt: now,
|
|
38459
|
-
},
|
|
38460
|
-
]
|
|
38461
|
-
: [
|
|
38462
|
-
{
|
|
38463
|
-
accountId: session.accountId,
|
|
38464
|
-
workspaceId,
|
|
38465
|
-
sessionId,
|
|
38466
|
-
sequence: ++sequence,
|
|
38467
|
-
type: "turn.recovery.requested",
|
|
39156
|
+
type: "turn.cancelled",
|
|
38468
39157
|
turnId: turn.id,
|
|
38469
39158
|
turnGeneration: turn.executionGeneration,
|
|
38470
39159
|
turnAttemptId: attemptId,
|
|
@@ -38472,50 +39161,101 @@ export async function settleSessionAttemptInterruptions(
|
|
|
38472
39161
|
payload: sanitizeEventPayload({ reason }),
|
|
38473
39162
|
occurredAt: now,
|
|
38474
39163
|
},
|
|
38475
|
-
|
|
38476
|
-
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38483
|
-
|
|
38484
|
-
|
|
38485
|
-
|
|
38486
|
-
|
|
38487
|
-
|
|
38488
|
-
|
|
39164
|
+
]
|
|
39165
|
+
: steer
|
|
39166
|
+
? [
|
|
39167
|
+
{
|
|
39168
|
+
accountId: session.accountId,
|
|
39169
|
+
workspaceId,
|
|
39170
|
+
sessionId,
|
|
39171
|
+
sequence: ++sequence,
|
|
39172
|
+
type: "turn.superseded",
|
|
39173
|
+
turnId: turn.id,
|
|
39174
|
+
turnGeneration: turn.executionGeneration,
|
|
39175
|
+
turnAttemptId: attemptId,
|
|
39176
|
+
turnAssociation: "current",
|
|
39177
|
+
payload: sanitizeEventPayload({ reason: "steer" }),
|
|
39178
|
+
occurredAt: now,
|
|
39179
|
+
},
|
|
39180
|
+
{
|
|
39181
|
+
accountId: session.accountId,
|
|
39182
|
+
workspaceId,
|
|
39183
|
+
sessionId,
|
|
39184
|
+
sequence: ++sequence,
|
|
39185
|
+
type: "session.status.changed",
|
|
39186
|
+
payload: sanitizeEventPayload({ status: "queued" }),
|
|
39187
|
+
occurredAt: now,
|
|
39188
|
+
},
|
|
39189
|
+
]
|
|
39190
|
+
: [
|
|
39191
|
+
{
|
|
39192
|
+
accountId: session.accountId,
|
|
39193
|
+
workspaceId,
|
|
39194
|
+
sessionId,
|
|
39195
|
+
sequence: ++sequence,
|
|
39196
|
+
type: "turn.recovery.requested",
|
|
39197
|
+
turnId: turn.id,
|
|
39198
|
+
turnGeneration: turn.executionGeneration,
|
|
39199
|
+
turnAttemptId: attemptId,
|
|
39200
|
+
turnAssociation: "current",
|
|
39201
|
+
payload: sanitizeEventPayload({ reason }),
|
|
39202
|
+
occurredAt: now,
|
|
39203
|
+
},
|
|
39204
|
+
{
|
|
39205
|
+
accountId: session.accountId,
|
|
39206
|
+
workspaceId,
|
|
39207
|
+
sessionId,
|
|
39208
|
+
sequence: ++sequence,
|
|
39209
|
+
type: "session.status.changed",
|
|
39210
|
+
turnId: turn.id,
|
|
39211
|
+
turnGeneration: turn.executionGeneration,
|
|
39212
|
+
turnAttemptId: attemptId,
|
|
39213
|
+
turnAssociation: "current",
|
|
39214
|
+
payload: sanitizeEventPayload({ status: "recovering" }),
|
|
39215
|
+
occurredAt: now,
|
|
39216
|
+
},
|
|
39217
|
+
];
|
|
38489
39218
|
const eventRows = await tx.insert(schema.sessionEvents).values(eventValues).returning();
|
|
38490
39219
|
await tx
|
|
38491
39220
|
.update(schema.sessionTurns)
|
|
38492
39221
|
.set(
|
|
38493
|
-
|
|
39222
|
+
terminalCancel
|
|
38494
39223
|
? {
|
|
38495
|
-
status: "
|
|
39224
|
+
status: "cancelled",
|
|
38496
39225
|
activeAttemptId: null,
|
|
38497
39226
|
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
39227
|
+
cancelledBy: "system:session_cancelled",
|
|
39228
|
+
cancelReason: reason,
|
|
38498
39229
|
version: turn.version + 1,
|
|
38499
39230
|
finishedAt: turn.finishedAt ?? now,
|
|
38500
39231
|
updatedAt: now,
|
|
38501
39232
|
}
|
|
38502
|
-
:
|
|
38503
|
-
|
|
38504
|
-
|
|
38505
|
-
|
|
38506
|
-
|
|
38507
|
-
|
|
38508
|
-
|
|
38509
|
-
|
|
38510
|
-
|
|
38511
|
-
|
|
39233
|
+
: steer
|
|
39234
|
+
? {
|
|
39235
|
+
status: "superseded",
|
|
39236
|
+
activeAttemptId: null,
|
|
39237
|
+
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
39238
|
+
version: turn.version + 1,
|
|
39239
|
+
finishedAt: turn.finishedAt ?? now,
|
|
39240
|
+
updatedAt: now,
|
|
39241
|
+
}
|
|
39242
|
+
: {
|
|
39243
|
+
status: "recovering",
|
|
39244
|
+
activeAttemptId: null,
|
|
39245
|
+
metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
39246
|
+
cancelledBy: null,
|
|
39247
|
+
cancelReason: null,
|
|
39248
|
+
version: turn.version + 1,
|
|
39249
|
+
finishedAt: null,
|
|
39250
|
+
updatedAt: now,
|
|
39251
|
+
},
|
|
38512
39252
|
)
|
|
38513
39253
|
.where(eq(schema.sessionTurns.id, turn.id));
|
|
38514
39254
|
await tx
|
|
38515
39255
|
.update(schema.sessions)
|
|
38516
39256
|
.set({
|
|
38517
|
-
status: steer ? "queued" : "recovering",
|
|
38518
|
-
activeTurnId: steer ? null : turn.id,
|
|
39257
|
+
status: terminalCancel ? "cancelled" : steer ? "queued" : "recovering",
|
|
39258
|
+
activeTurnId: terminalCancel || steer ? null : turn.id,
|
|
38519
39259
|
lastSequence: sequence,
|
|
38520
39260
|
updatedAt: now,
|
|
38521
39261
|
})
|
|
@@ -39348,6 +40088,13 @@ export type ApplySessionTurnSettlementResult =
|
|
|
39348
40088
|
activeTurnId: string | null;
|
|
39349
40089
|
};
|
|
39350
40090
|
|
|
40091
|
+
export type ApplySessionTurnSettlementHooks = {
|
|
40092
|
+
/** Test-only failure injection after terminal projection but before commit. */
|
|
40093
|
+
afterRealtimeDelegationProjection?:
|
|
40094
|
+
| ((projection: { entryId: string; turnId: string }) => void | Promise<void>)
|
|
40095
|
+
| undefined;
|
|
40096
|
+
};
|
|
40097
|
+
|
|
39351
40098
|
function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAttemptOutcome | null {
|
|
39352
40099
|
switch (status) {
|
|
39353
40100
|
case "completed":
|
|
@@ -39361,6 +40108,40 @@ function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAtte
|
|
|
39361
40108
|
}
|
|
39362
40109
|
}
|
|
39363
40110
|
|
|
40111
|
+
type TerminalSessionTurnStatus = "completed" | "failed" | "cancelled" | "superseded";
|
|
40112
|
+
type TerminalSessionTurnEventType =
|
|
40113
|
+
| "turn.completed"
|
|
40114
|
+
| "turn.failed"
|
|
40115
|
+
| "turn.cancelled"
|
|
40116
|
+
| "turn.superseded";
|
|
40117
|
+
|
|
40118
|
+
function terminalSessionTurnEventType(
|
|
40119
|
+
status: TerminalSessionTurnStatus,
|
|
40120
|
+
): TerminalSessionTurnEventType {
|
|
40121
|
+
switch (status) {
|
|
40122
|
+
case "completed":
|
|
40123
|
+
return "turn.completed";
|
|
40124
|
+
case "failed":
|
|
40125
|
+
return "turn.failed";
|
|
40126
|
+
case "cancelled":
|
|
40127
|
+
return "turn.cancelled";
|
|
40128
|
+
case "superseded":
|
|
40129
|
+
return "turn.superseded";
|
|
40130
|
+
}
|
|
40131
|
+
}
|
|
40132
|
+
|
|
40133
|
+
function isTerminalSessionTurnStatus(
|
|
40134
|
+
status: SessionTurnStatus,
|
|
40135
|
+
): status is TerminalSessionTurnStatus {
|
|
40136
|
+
return ["completed", "failed", "cancelled", "superseded"].includes(status);
|
|
40137
|
+
}
|
|
40138
|
+
|
|
40139
|
+
function sessionEventPayloadRecord(payload: unknown): Record<string, unknown> {
|
|
40140
|
+
return payload && typeof payload === "object" && !Array.isArray(payload)
|
|
40141
|
+
? (payload as Record<string, unknown>)
|
|
40142
|
+
: {};
|
|
40143
|
+
}
|
|
40144
|
+
|
|
39364
40145
|
/**
|
|
39365
40146
|
* Atomically append terminal/requires-action truth, update the exact turn, and
|
|
39366
40147
|
* transition the owning session. A superseded dispatch or closed control gate
|
|
@@ -39371,6 +40152,7 @@ export async function applySessionTurnSettlement(
|
|
|
39371
40152
|
db: Database,
|
|
39372
40153
|
workspaceId: string,
|
|
39373
40154
|
input: ApplySessionTurnSettlementInput,
|
|
40155
|
+
hooks: ApplySessionTurnSettlementHooks = {},
|
|
39374
40156
|
): Promise<ApplySessionTurnSettlementResult> {
|
|
39375
40157
|
const fromStatuses = input.fromStatuses ?? ["running", "requires_action"];
|
|
39376
40158
|
const eventTypes = [
|
|
@@ -39460,11 +40242,11 @@ export async function applySessionTurnSettlement(
|
|
|
39460
40242
|
};
|
|
39461
40243
|
}
|
|
39462
40244
|
|
|
40245
|
+
const humanInputRequests = input.runState?.humanInputRequests ?? [];
|
|
39463
40246
|
if (input.runState) {
|
|
39464
40247
|
if (input.turnStatus !== "requires_action" || input.sessionStatus !== "requires_action") {
|
|
39465
40248
|
throw new Error("A frozen run state requires a requires_action settlement");
|
|
39466
40249
|
}
|
|
39467
|
-
const humanInputRequests = input.runState.humanInputRequests ?? [];
|
|
39468
40250
|
for (const request of humanInputRequests) {
|
|
39469
40251
|
const parsedQuestions = request.questions.map((question) =>
|
|
39470
40252
|
HumanInputQuestionContract.parse(question),
|
|
@@ -39716,7 +40498,10 @@ export async function applySessionTurnSettlement(
|
|
|
39716
40498
|
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
39717
40499
|
),
|
|
39718
40500
|
)
|
|
39719
|
-
.returning({
|
|
40501
|
+
.returning({
|
|
40502
|
+
id: schema.sessionHumanInputRequests.id,
|
|
40503
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
40504
|
+
})
|
|
39720
40505
|
: [];
|
|
39721
40506
|
const terminalHumanInputEvents: AppendEventInput[] = terminalHumanInputRows.map(
|
|
39722
40507
|
(request) => ({
|
|
@@ -39799,11 +40584,100 @@ export async function applySessionTurnSettlement(
|
|
|
39799
40584
|
});
|
|
39800
40585
|
const inserted =
|
|
39801
40586
|
values.length > 0 ? await tx.insert(schema.sessionEvents).values(values).returning() : [];
|
|
39802
|
-
const
|
|
39803
|
-
|
|
39804
|
-
|
|
39805
|
-
|
|
39806
|
-
|
|
40587
|
+
const requestedEvents = inserted.filter(
|
|
40588
|
+
(event) => event.type === "session.humanInput.requested",
|
|
40589
|
+
);
|
|
40590
|
+
if (requestedEvents.length > 0) {
|
|
40591
|
+
const requestedIds = new Set(
|
|
40592
|
+
requestedEvents.flatMap((event) => {
|
|
40593
|
+
const request = sessionEventPayloadRecord(event.payload).request;
|
|
40594
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) return [];
|
|
40595
|
+
const id = (request as Record<string, unknown>).id;
|
|
40596
|
+
return typeof id === "string" ? [id] : [];
|
|
40597
|
+
}),
|
|
40598
|
+
);
|
|
40599
|
+
const requests = humanInputRequests.filter((request) => requestedIds.has(request.id));
|
|
40600
|
+
if (requests.length !== requestedIds.size) {
|
|
40601
|
+
throw new Error("Human-input realtime projection lost its durable request contract");
|
|
40602
|
+
}
|
|
40603
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
40604
|
+
accountId: session.accountId,
|
|
40605
|
+
workspaceId,
|
|
40606
|
+
sessionId: input.sessionId,
|
|
40607
|
+
sourceKind: "human_input_request",
|
|
40608
|
+
sourceId: requestedEvents.map((event) => event.id).join(":"),
|
|
40609
|
+
turnId: input.turnId,
|
|
40610
|
+
channel: "speakable",
|
|
40611
|
+
text: renderRealtimeHumanInputRequestContext({ requests }),
|
|
40612
|
+
payload: {
|
|
40613
|
+
status: "waiting_for_user",
|
|
40614
|
+
requestIds: requests.map((request) => request.id),
|
|
40615
|
+
sourceEventIds: requestedEvents.map((event) => event.id),
|
|
40616
|
+
},
|
|
40617
|
+
now,
|
|
40618
|
+
});
|
|
40619
|
+
}
|
|
40620
|
+
const terminalHumanInputById = new Map(
|
|
40621
|
+
terminalHumanInputRows.map((request) => [request.id, request]),
|
|
40622
|
+
);
|
|
40623
|
+
for (const event of inserted) {
|
|
40624
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
40625
|
+
const payload = sessionEventPayloadRecord(event.payload);
|
|
40626
|
+
const requestId = typeof payload.requestId === "string" ? payload.requestId : null;
|
|
40627
|
+
const request = requestId ? terminalHumanInputById.get(requestId) : null;
|
|
40628
|
+
if (!request) continue;
|
|
40629
|
+
await mirrorSessionRealtimeContextInTransaction(tx as unknown as Database, {
|
|
40630
|
+
accountId: session.accountId,
|
|
40631
|
+
workspaceId,
|
|
40632
|
+
sessionId: input.sessionId,
|
|
40633
|
+
sourceKind: "human_input_response",
|
|
40634
|
+
sourceId: event.id,
|
|
40635
|
+
turnId: input.turnId,
|
|
40636
|
+
channel: null,
|
|
40637
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
40638
|
+
requestId: request.id,
|
|
40639
|
+
questions: request.questions,
|
|
40640
|
+
response: { outcome: "cancelled" },
|
|
40641
|
+
}),
|
|
40642
|
+
payload: {
|
|
40643
|
+
requestId: request.id,
|
|
40644
|
+
outcome: "cancelled",
|
|
40645
|
+
sourceEventId: event.id,
|
|
40646
|
+
},
|
|
40647
|
+
now,
|
|
40648
|
+
});
|
|
40649
|
+
}
|
|
40650
|
+
const terminal = isTerminalSessionTurnStatus(input.turnStatus);
|
|
40651
|
+
if (isTerminalSessionTurnStatus(input.turnStatus)) {
|
|
40652
|
+
const terminalType = terminalSessionTurnEventType(input.turnStatus);
|
|
40653
|
+
const persistedTerminal = inserted.find((event) => event.type === terminalType);
|
|
40654
|
+
const projection = await projectSessionRealtimeDelegationTerminalInTransaction(
|
|
40655
|
+
tx as unknown as Database,
|
|
40656
|
+
{
|
|
40657
|
+
accountId: session.accountId,
|
|
40658
|
+
workspaceId,
|
|
40659
|
+
sessionId: input.sessionId,
|
|
40660
|
+
turnId: input.turnId,
|
|
40661
|
+
turnStatus: input.turnStatus,
|
|
40662
|
+
terminalEvent: {
|
|
40663
|
+
type: terminalType,
|
|
40664
|
+
payload: persistedTerminal
|
|
40665
|
+
? sessionEventPayloadRecord(persistedTerminal.payload)
|
|
40666
|
+
: {
|
|
40667
|
+
code: "delegation_terminal_event_missing",
|
|
40668
|
+
error: `Delegated turn reached ${input.turnStatus} without its canonical terminal event.`,
|
|
40669
|
+
},
|
|
40670
|
+
},
|
|
40671
|
+
now,
|
|
40672
|
+
},
|
|
40673
|
+
);
|
|
40674
|
+
if (projection) {
|
|
40675
|
+
await hooks.afterRealtimeDelegationProjection?.({
|
|
40676
|
+
entryId: projection.entry.id,
|
|
40677
|
+
turnId: input.turnId,
|
|
40678
|
+
});
|
|
40679
|
+
}
|
|
40680
|
+
}
|
|
39807
40681
|
if (input.turnStatus === "running") {
|
|
39808
40682
|
// Approval resume re-enters the same logical turn after a new fenced
|
|
39809
40683
|
// dispatch has advanced its trigger. It is an authorized inference
|
|
@@ -40092,6 +40966,20 @@ export async function settleCodexCredentialLeaseLoss(
|
|
|
40092
40966
|
if (!settlementEvent) {
|
|
40093
40967
|
throw new Error("Codex lease-loss settlement did not persist its checkpoint event");
|
|
40094
40968
|
}
|
|
40969
|
+
if (!input.checkpointDurable) {
|
|
40970
|
+
await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
|
|
40971
|
+
accountId: input.accountId,
|
|
40972
|
+
workspaceId: input.workspaceId,
|
|
40973
|
+
sessionId: input.sessionId,
|
|
40974
|
+
turnId: input.turnId,
|
|
40975
|
+
turnStatus: "failed",
|
|
40976
|
+
terminalEvent: {
|
|
40977
|
+
type: "turn.failed",
|
|
40978
|
+
payload: sessionEventPayloadRecord(settlementEvent.payload),
|
|
40979
|
+
},
|
|
40980
|
+
now,
|
|
40981
|
+
});
|
|
40982
|
+
}
|
|
40095
40983
|
|
|
40096
40984
|
await tx
|
|
40097
40985
|
.update(schema.sessionTurns)
|
|
@@ -40821,6 +41709,22 @@ export async function recoverSessionDispatch(
|
|
|
40821
41709
|
},
|
|
40822
41710
|
])
|
|
40823
41711
|
.returning();
|
|
41712
|
+
const failedEvent = inserted.find((event) => event.type === "turn.failed");
|
|
41713
|
+
if (!failedEvent) {
|
|
41714
|
+
throw new Error("Worker-death exhaustion did not persist its terminal event");
|
|
41715
|
+
}
|
|
41716
|
+
await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
|
|
41717
|
+
accountId: session.accountId,
|
|
41718
|
+
workspaceId,
|
|
41719
|
+
sessionId: input.sessionId,
|
|
41720
|
+
turnId: turn.id,
|
|
41721
|
+
turnStatus: "failed",
|
|
41722
|
+
terminalEvent: {
|
|
41723
|
+
type: "turn.failed",
|
|
41724
|
+
payload: sessionEventPayloadRecord(failedEvent.payload),
|
|
41725
|
+
},
|
|
41726
|
+
now,
|
|
41727
|
+
});
|
|
40824
41728
|
await tx
|
|
40825
41729
|
.update(schema.sessionTurns)
|
|
40826
41730
|
.set({
|
|
@@ -41559,8 +42463,14 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
|
|
|
41559
42463
|
.for("update")
|
|
41560
42464
|
.limit(1);
|
|
41561
42465
|
if (!workspace || !session) throw new Error(`Session not found: ${input.sessionId}`);
|
|
42466
|
+
const realtimeActive = await sessionRealtimeIsActiveInTransaction(
|
|
42467
|
+
tx as unknown as Database,
|
|
42468
|
+
input.workspaceId,
|
|
42469
|
+
input.sessionId,
|
|
42470
|
+
);
|
|
41562
42471
|
const runnable =
|
|
41563
42472
|
session.status !== "cancelled" &&
|
|
42473
|
+
!realtimeActive &&
|
|
41564
42474
|
session.activeTurnId === null &&
|
|
41565
42475
|
effectiveControl.state === "active";
|
|
41566
42476
|
return runnable
|
|
@@ -42053,7 +42963,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
|
|
|
42053
42963
|
.returning();
|
|
42054
42964
|
if (!event) throw new Error("Failed to create system-update pending event");
|
|
42055
42965
|
await mutateSource(tx as unknown as Database, event.id);
|
|
42056
|
-
const
|
|
42966
|
+
const realtimeActive = await sessionRealtimeIsActiveInTransaction(
|
|
42967
|
+
tx as unknown as Database,
|
|
42968
|
+
input.workspaceId,
|
|
42969
|
+
input.sessionId,
|
|
42970
|
+
);
|
|
42971
|
+
const shouldWake =
|
|
42972
|
+
!realtimeActive && session.activeTurnId === null && effectiveControl.state === "active";
|
|
42057
42973
|
const wake = shouldWake
|
|
42058
42974
|
? await registerInternalUpdateWakeInTransaction(tx as unknown as Database, {
|
|
42059
42975
|
accountId: session.accountId,
|
|
@@ -42617,6 +43533,21 @@ export async function appendSessionEventsForTurnAttempt(
|
|
|
42617
43533
|
};
|
|
42618
43534
|
});
|
|
42619
43535
|
const inserted = await tx.insert(schema.sessionEvents).values(values).returning();
|
|
43536
|
+
if (fence.allowed) {
|
|
43537
|
+
await projectSessionRealtimeDelegationProgressInTransaction(tx as unknown as Database, {
|
|
43538
|
+
accountId: session.accountId,
|
|
43539
|
+
workspaceId,
|
|
43540
|
+
sessionId,
|
|
43541
|
+
turnId,
|
|
43542
|
+
events: inserted.map((event) => ({
|
|
43543
|
+
id: event.id,
|
|
43544
|
+
sequence: event.sequence,
|
|
43545
|
+
type: event.type,
|
|
43546
|
+
payload: event.payload,
|
|
43547
|
+
})),
|
|
43548
|
+
now,
|
|
43549
|
+
});
|
|
43550
|
+
}
|
|
42620
43551
|
await tx
|
|
42621
43552
|
.update(schema.sessions)
|
|
42622
43553
|
.set({
|
|
@@ -43041,6 +43972,7 @@ function mapSession(
|
|
|
43041
43972
|
title: row.title ?? null,
|
|
43042
43973
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
43043
43974
|
instructions: row.instructions ?? null,
|
|
43975
|
+
policyRole: row.policyRole ?? null,
|
|
43044
43976
|
resources: row.resources as ResourceRef[],
|
|
43045
43977
|
skills: (row.skills as SessionSkill[]) ?? [],
|
|
43046
43978
|
tools: row.tools as ToolRef[],
|