@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.
Files changed (55) hide show
  1. package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
  2. package/dist/chunk-7WTDI7Y3.js.map +1 -0
  3. package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
  4. package/dist/chunk-KW6U54V2.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +107 -5
  7. package/dist/index.js +7857 -3898
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +1659 -77
  13. package/dist/schema.js +13 -1
  14. package/dist/session-control.d.ts +7 -1
  15. package/dist/session-queue-commands.d.ts +8 -0
  16. package/dist/session-realtime-context.d.ts +56 -0
  17. package/dist/session-realtime-ledger.d.ts +188 -0
  18. package/dist/session-realtime-mirror.d.ts +51 -0
  19. package/dist/session-realtime-state.d.ts +2 -0
  20. package/dist/session-realtime-terminal.d.ts +40 -0
  21. package/dist/session-realtime.d.ts +59 -0
  22. package/dist/workspace-instruction-policies-schema.d.ts +307 -0
  23. package/dist/workspace-instruction-policies.d.ts +97 -7
  24. package/drizzle/0156_slack_reaction_trigger.sql +49 -0
  25. package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
  26. package/drizzle/0158_session_realtime_mode.sql +88 -0
  27. package/drizzle/0159_session_realtime_ledger.sql +198 -0
  28. package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
  29. package/drizzle/0161_session_realtime_context_projection.sql +82 -0
  30. package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
  31. package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
  32. package/drizzle/0164_session_realtime_models.sql +28 -0
  33. package/drizzle/0165_document_authority_foundation.sql +259 -0
  34. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  35. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  36. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  37. package/package.json +4 -4
  38. package/src/connection-token-resolver.ts +79 -16
  39. package/src/index.ts +1033 -101
  40. package/src/preference-registry.ts +114 -6
  41. package/src/provision-roles.ts +12 -0
  42. package/src/runtime-posture.ts +12 -0
  43. package/src/schema.ts +486 -43
  44. package/src/session-control.ts +643 -21
  45. package/src/session-queue-commands.ts +121 -18
  46. package/src/session-realtime-context.ts +393 -0
  47. package/src/session-realtime-ledger.ts +1790 -0
  48. package/src/session-realtime-mirror.ts +276 -0
  49. package/src/session-realtime-state.ts +25 -0
  50. package/src/session-realtime-terminal.ts +306 -0
  51. package/src/session-realtime.ts +659 -0
  52. package/src/workspace-instruction-policies-schema.ts +71 -0
  53. package/src/workspace-instruction-policies.ts +568 -25
  54. package/dist/chunk-BNGEN5QZ.js.map +0 -1
  55. 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(scopedDb, workspaceId, connectionId, updatedBySubjectId),
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
- const value = values[0] ?? "";
20253
- const minLength = question.validation?.minLength;
20254
- const maxLength = question.validation?.maxLength;
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
- const session = prefix.sessions[0];
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
- return await registerSessionTurnAttemptClaim(tx as unknown as Database, {
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: sanitizeModelPayload({
37817
- type: "message",
37818
- role: "user",
37819
- content: row.prompt,
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 delivered = await deliverPendingUpdates(
37824
- session.accountId,
37825
- row.id,
37826
- row.executionGeneration,
37827
- session.lastSequence + 1,
37828
- now,
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 steer = interruptions.some((interruption) => interruption.kind === "steer");
38404
- const outcome: SessionTurnAttemptOutcome = steer ? "superseded" : "interrupted_recoverable";
38405
- const reason = steer
38406
- ? "steer"
38407
- : interruptions.some((interruption) => interruption.kind === "workspace_pause")
38408
- ? "workspace_pause"
38409
- : interruptions.some((interruption) => interruption.kind === "maintenance")
38410
- ? "maintenance"
38411
- : "session_pause";
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> = steer
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.superseded",
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
- accountId: session.accountId,
38477
- workspaceId,
38478
- sessionId,
38479
- sequence: ++sequence,
38480
- type: "session.status.changed",
38481
- turnId: turn.id,
38482
- turnGeneration: turn.executionGeneration,
38483
- turnAttemptId: attemptId,
38484
- turnAssociation: "current",
38485
- payload: sanitizeEventPayload({ status: "recovering" }),
38486
- occurredAt: now,
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
- steer
39222
+ terminalCancel
38494
39223
  ? {
38495
- status: "superseded",
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
- status: "recovering",
38504
- activeAttemptId: null,
38505
- metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
38506
- cancelledBy: null,
38507
- cancelReason: null,
38508
- version: turn.version + 1,
38509
- finishedAt: null,
38510
- updatedAt: now,
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({ id: schema.sessionHumanInputRequests.id })
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 terminal =
39803
- input.turnStatus === "completed" ||
39804
- input.turnStatus === "cancelled" ||
39805
- input.turnStatus === "failed" ||
39806
- input.turnStatus === "superseded";
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 shouldWake = session.activeTurnId === null && effectiveControl.state === "active";
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[],