@opengeni/db 0.22.1 → 0.23.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 (48) hide show
  1. package/dist/{chunk-CYGFLLMN.js → chunk-3UCHDMKG.js} +656 -282
  2. package/dist/chunk-3UCHDMKG.js.map +1 -0
  3. package/dist/{chunk-BNGEN5QZ.js → chunk-L6ADMZHE.js} +24 -2
  4. package/dist/chunk-L6ADMZHE.js.map +1 -0
  5. package/dist/index.d.ts +42 -3
  6. package/dist/index.js +6919 -3760
  7. package/dist/index.js.map +1 -1
  8. package/dist/provision-roles.js +1 -1
  9. package/dist/runtime-posture.d.ts +3 -3
  10. package/dist/schema.d.ts +1416 -92
  11. package/dist/schema.js +11 -1
  12. package/dist/session-control.d.ts +7 -1
  13. package/dist/session-queue-commands.d.ts +8 -0
  14. package/dist/session-realtime-context.d.ts +56 -0
  15. package/dist/session-realtime-ledger.d.ts +188 -0
  16. package/dist/session-realtime-mirror.d.ts +30 -0
  17. package/dist/session-realtime-state.d.ts +2 -0
  18. package/dist/session-realtime-terminal.d.ts +40 -0
  19. package/dist/session-realtime.d.ts +59 -0
  20. package/dist/workspace-instruction-policies-schema.d.ts +239 -0
  21. package/dist/workspace-instruction-policies.d.ts +44 -0
  22. package/drizzle/0156_slack_reaction_trigger.sql +49 -0
  23. package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
  24. package/drizzle/0158_session_realtime_mode.sql +88 -0
  25. package/drizzle/0159_session_realtime_ledger.sql +198 -0
  26. package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
  27. package/drizzle/0161_session_realtime_context_projection.sql +82 -0
  28. package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
  29. package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
  30. package/drizzle/0164_session_realtime_models.sql +28 -0
  31. package/package.json +4 -4
  32. package/src/index.ts +670 -79
  33. package/src/preference-registry.ts +11 -6
  34. package/src/provision-roles.ts +12 -0
  35. package/src/runtime-posture.ts +10 -0
  36. package/src/schema.ts +400 -43
  37. package/src/session-control.ts +596 -21
  38. package/src/session-queue-commands.ts +76 -7
  39. package/src/session-realtime-context.ts +393 -0
  40. package/src/session-realtime-ledger.ts +1790 -0
  41. package/src/session-realtime-mirror.ts +160 -0
  42. package/src/session-realtime-state.ts +25 -0
  43. package/src/session-realtime-terminal.ts +306 -0
  44. package/src/session-realtime.ts +611 -0
  45. package/src/workspace-instruction-policies-schema.ts +41 -0
  46. package/src/workspace-instruction-policies.ts +131 -2
  47. package/dist/chunk-BNGEN5QZ.js.map +0 -1
  48. package/dist/chunk-CYGFLLMN.js.map +0 -1
package/src/index.ts CHANGED
@@ -146,7 +146,12 @@ import {
146
146
  SubmitHumanInputResponseRequest,
147
147
  TurnExecutionPolicyV1,
148
148
  } from "@opengeni/contracts";
149
- import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
149
+ import {
150
+ environmentsEncryptionKeyBytes,
151
+ VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
152
+ VERCEL_AI_GATEWAY_CONNECTION_ROLE,
153
+ type Settings,
154
+ } from "@opengeni/config";
150
155
  import { boundModelToolOutputItem, isCodexBilledModel } from "@opengeni/codex";
151
156
  // Re-exported so consumers get the whole codex-billed detection surface (the pure
152
157
  // prefix test + the credential-aware predicates below) from a single import.
@@ -212,11 +217,25 @@ import {
212
217
  serializeEffectiveSessionControl,
213
218
  type SessionDiscoveryControl,
214
219
  type SessionCommandActor,
220
+ SessionControlConflictError,
215
221
  SessionControlInvariantError,
216
222
  updateSessionCommandReceiptResult,
217
223
  type SessionTurnAttemptOutcome,
218
224
  type WorkspaceControlRow,
219
225
  } from "./session-control";
226
+ import {
227
+ sessionRealtimeIsActiveInTransaction,
228
+ settleExpiredSessionRealtimeInTransaction,
229
+ } from "./session-realtime";
230
+ import {
231
+ isSessionRealtimeDelegationTurnMetadata,
232
+ projectSessionRealtimeDelegationProgressInTransaction,
233
+ projectSessionRealtimeDelegationTerminalInTransaction,
234
+ } from "./session-realtime-ledger";
235
+ import {
236
+ listSessionRealtimeContinuityEntriesInTransaction,
237
+ type SessionRealtimeContinuityEntry,
238
+ } from "./session-realtime-context";
220
239
  import * as schema from "./schema";
221
240
  import {
222
241
  AGENT_VISIBLE_MEMORY_STATUSES,
@@ -238,6 +257,9 @@ import {
238
257
  export { sql as dbSql } from "drizzle-orm";
239
258
  export * from "./session-control";
240
259
  export * from "./session-queue-commands";
260
+ export * from "./session-realtime";
261
+ export * from "./session-realtime-context";
262
+ export * from "./session-realtime-ledger";
241
263
  export * from "./new-session-drafts";
242
264
  export * from "./workspace-instruction-policies";
243
265
  export * from "./preference-registry";
@@ -5383,6 +5405,7 @@ export type SlackBotUserLink = {
5383
5405
  export type SlackInteractionTriggerKind =
5384
5406
  | "app_mention"
5385
5407
  | "dm"
5408
+ | "reaction"
5386
5409
  | "slash_command"
5387
5410
  | "message_shortcut"
5388
5411
  | "thread_reply";
@@ -5407,6 +5430,7 @@ export type SlackInteractionInboxEntry = {
5407
5430
  attemptCount: number;
5408
5431
  retryAt: Date | null;
5409
5432
  lastErrorCode: string | null;
5433
+ reactionContextCheckpoint: unknown | null;
5410
5434
  processedAt: Date | null;
5411
5435
  createdAt: Date;
5412
5436
  updatedAt: Date;
@@ -5552,6 +5576,7 @@ export async function enqueueSlackInteractionInbox(
5552
5576
  | "attemptCount"
5553
5577
  | "retryAt"
5554
5578
  | "lastErrorCode"
5579
+ | "reactionContextCheckpoint"
5555
5580
  | "processedAt"
5556
5581
  | "createdAt"
5557
5582
  | "updatedAt"
@@ -5614,6 +5639,7 @@ export async function settleSlackInteractionInbox(
5614
5639
  claimHolderId: null,
5615
5640
  claimExpiresAt: null,
5616
5641
  retryAt: null,
5642
+ reactionContextCheckpoint: null,
5617
5643
  processedAt: sql`now()`,
5618
5644
  lastErrorCode: input.errorCode ?? null,
5619
5645
  updatedAt: sql`now()`,
@@ -5630,6 +5656,42 @@ export async function settleSlackInteractionInbox(
5630
5656
  });
5631
5657
  }
5632
5658
 
5659
+ export async function saveSlackInteractionInboxReactionCheckpoint(
5660
+ db: Database,
5661
+ input: {
5662
+ entry: Pick<
5663
+ SlackInteractionInboxEntry,
5664
+ "id" | "accountId" | "workspaceId" | "connectionId" | "providerEventId" | "providerMessageId"
5665
+ >;
5666
+ claimHolderId: string;
5667
+ checkpoint: unknown;
5668
+ },
5669
+ ): Promise<boolean> {
5670
+ return await withRlsContext(db, input.entry, async (scopedDb) => {
5671
+ const rows = await scopedDb
5672
+ .update(schema.slackInteractionInbox)
5673
+ .set({
5674
+ reactionContextCheckpoint: input.checkpoint,
5675
+ updatedAt: sql`now()`,
5676
+ })
5677
+ .where(
5678
+ and(
5679
+ eq(schema.slackInteractionInbox.id, input.entry.id),
5680
+ eq(schema.slackInteractionInbox.accountId, input.entry.accountId),
5681
+ eq(schema.slackInteractionInbox.workspaceId, input.entry.workspaceId),
5682
+ eq(schema.slackInteractionInbox.connectionId, input.entry.connectionId),
5683
+ eq(schema.slackInteractionInbox.providerEventId, input.entry.providerEventId),
5684
+ eq(schema.slackInteractionInbox.providerMessageId, input.entry.providerMessageId),
5685
+ eq(schema.slackInteractionInbox.triggerKind, "reaction"),
5686
+ eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
5687
+ eq(schema.slackInteractionInbox.status, "processing"),
5688
+ ),
5689
+ )
5690
+ .returning({ id: schema.slackInteractionInbox.id });
5691
+ return rows.length === 1;
5692
+ });
5693
+ }
5694
+
5633
5695
  export async function releaseSlackInteractionInbox(
5634
5696
  db: Database,
5635
5697
  input: {
@@ -5726,6 +5788,57 @@ export async function getSlackInteractionByRoute(
5726
5788
  });
5727
5789
  }
5728
5790
 
5791
+ /**
5792
+ * Resolve an already-durable Slack task to its canonical interaction.
5793
+ *
5794
+ * Client event ids are unique only inside one session, so this lookup also
5795
+ * fences the exact workspace and Slack connection. The reservation id is the
5796
+ * canonical Slack session identity both before and after binding; joining it
5797
+ * lets a retry repair a crash after the task committed but before the route was
5798
+ * bound. Two matches indicate a cross-session collision and fail closed rather
5799
+ * than choosing an arbitrary route.
5800
+ */
5801
+ export async function getSlackInteractionByClientEventId(
5802
+ db: Database,
5803
+ workspaceId: string,
5804
+ connectionId: string,
5805
+ clientEventId: string,
5806
+ ): Promise<{ interaction: SlackInteraction; eventSessionId: string } | null> {
5807
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5808
+ const rows = await scopedDb
5809
+ .select({
5810
+ interaction: schema.slackInteractions,
5811
+ eventSessionId: schema.sessionEvents.sessionId,
5812
+ })
5813
+ .from(schema.slackInteractions)
5814
+ .innerJoin(
5815
+ schema.sessionEvents,
5816
+ and(
5817
+ eq(schema.sessionEvents.workspaceId, schema.slackInteractions.workspaceId),
5818
+ eq(schema.sessionEvents.sessionId, schema.slackInteractions.sessionReservationId),
5819
+ ),
5820
+ )
5821
+ .where(
5822
+ and(
5823
+ eq(schema.slackInteractions.workspaceId, workspaceId),
5824
+ eq(schema.slackInteractions.connectionId, connectionId),
5825
+ eq(schema.sessionEvents.clientEventId, clientEventId),
5826
+ eq(schema.sessionEvents.type, "user.message"),
5827
+ ),
5828
+ )
5829
+ .limit(2);
5830
+ if (rows.length > 1) {
5831
+ throw new Error("Slack client event id resolved to multiple canonical interactions");
5832
+ }
5833
+ const row = rows[0];
5834
+ if (!row) return null;
5835
+ return {
5836
+ interaction: mapSlackInteraction(row.interaction),
5837
+ eventSessionId: row.eventSessionId,
5838
+ };
5839
+ });
5840
+ }
5841
+
5729
5842
  export async function getSlackInteractionSessionAccess(
5730
5843
  db: Database,
5731
5844
  workspaceId: string,
@@ -6173,6 +6286,8 @@ function mapSlackInteractionInbox(
6173
6286
  attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
6174
6287
  retryAt: slackRowNullableDate(row, "retryAt", "retry_at"),
6175
6288
  lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
6289
+ reactionContextCheckpoint:
6290
+ slackRowValue(row, "reactionContextCheckpoint", "reaction_context_checkpoint") ?? null,
6176
6291
  processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
6177
6292
  createdAt: slackRowDate(row, "createdAt", "created_at"),
6178
6293
  updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
@@ -7237,6 +7352,56 @@ export async function loadConnectionCredentialForBroker(
7237
7352
  );
7238
7353
  }
7239
7354
 
7355
+ export async function workspaceVercelAiGatewayConnectionActive(
7356
+ db: Database,
7357
+ workspaceId: string,
7358
+ ): Promise<boolean> {
7359
+ const connections = await listConnectionsMetadata(db, workspaceId, null);
7360
+ return connections.some(
7361
+ (connection) =>
7362
+ connection.subjectId === null &&
7363
+ connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
7364
+ connection.kind === "api_key" &&
7365
+ connection.status === "active" &&
7366
+ connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
7367
+ );
7368
+ }
7369
+
7370
+ /** Resolve only the reviewed workspace-shared AI Gateway credential shape. */
7371
+ export async function loadWorkspaceVercelAiGatewayApiKey(
7372
+ db: Database,
7373
+ settings: Settings,
7374
+ workspaceId: string,
7375
+ ): Promise<string | null> {
7376
+ const metadata = (await listConnectionsMetadata(db, workspaceId, null)).find(
7377
+ (connection) =>
7378
+ connection.subjectId === null &&
7379
+ connection.providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
7380
+ connection.kind === "api_key" &&
7381
+ connection.status === "active" &&
7382
+ connection.metadata.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE,
7383
+ );
7384
+ if (!metadata) {
7385
+ return null;
7386
+ }
7387
+ const connection = await loadConnectionCredentialForBroker(db, settings, {
7388
+ workspaceId,
7389
+ connectionId: metadata.id,
7390
+ providerDomain: VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
7391
+ kind: "api_key",
7392
+ allowSubjectOwned: false,
7393
+ });
7394
+ if (
7395
+ !connection ||
7396
+ connection.status !== "active" ||
7397
+ connection.metadata.credentialRole !== VERCEL_AI_GATEWAY_CONNECTION_ROLE
7398
+ ) {
7399
+ return null;
7400
+ }
7401
+ const apiKey = connection.credential.apiKey;
7402
+ return typeof apiKey === "string" && apiKey.trim().length > 0 ? apiKey : null;
7403
+ }
7404
+
7240
7405
  export async function recordConnectionTokenRefresh(
7241
7406
  db: Database,
7242
7407
  input: {
@@ -16638,6 +16803,7 @@ export type SessionCreateInput = {
16638
16803
  firstPartyMcpPermissions?: Permission[] | null;
16639
16804
  firstPartyMcpTools?: FirstPartyMcpToolName[];
16640
16805
  instructions?: string | null;
16806
+ policyRole?: string | null;
16641
16807
  parentSessionId?: string | null;
16642
16808
  createIdempotencyKey?: string | null;
16643
16809
  sandboxGroupId?: string | null;
@@ -16794,6 +16960,9 @@ async function resolveSessionDepthDecision(
16794
16960
  if (!parent) {
16795
16961
  throw new Error(`Parent session not found: ${parentSessionId}`);
16796
16962
  }
16963
+ if (parent.status === "cancelled") {
16964
+ throw new SessionControlConflictError("Cancelled session subtree cannot create children");
16965
+ }
16797
16966
  }
16798
16967
 
16799
16968
  const currentParentDepth = parent?.nestedAgentDepth ?? 0;
@@ -17049,6 +17218,7 @@ async function createSessionInTransaction(
17049
17218
  firstPartyMcpTools: input.firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS],
17050
17219
  initialPersonalConnectionDelegations: input.personalConnectionDelegations ?? [],
17051
17220
  instructions: input.instructions ?? null,
17221
+ policyRole: input.policyRole ?? null,
17052
17222
  parentSessionId: input.parentSessionId ?? null,
17053
17223
  parentTurnId,
17054
17224
  createIdempotencyKey,
@@ -21243,6 +21413,29 @@ export async function getActiveSessionHistoryItems(
21243
21413
  });
21244
21414
  }
21245
21415
 
21416
+ /** Bounded finalized voice turns used only to resume a later realtime call. */
21417
+ export async function getSessionRealtimeContinuityEntries(
21418
+ db: Database,
21419
+ workspaceId: string,
21420
+ sessionId: string,
21421
+ maximumEntries = 20,
21422
+ ): Promise<SessionRealtimeContinuityEntry[]> {
21423
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
21424
+ const [session] = await scopedDb
21425
+ .select({ accountId: schema.sessions.accountId })
21426
+ .from(schema.sessions)
21427
+ .where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
21428
+ .limit(1);
21429
+ if (!session) return [];
21430
+ return await listSessionRealtimeContinuityEntriesInTransaction(scopedDb, {
21431
+ accountId: session.accountId,
21432
+ workspaceId,
21433
+ sessionId,
21434
+ maximumEntries,
21435
+ });
21436
+ });
21437
+ }
21438
+
21246
21439
  /**
21247
21440
  * Count of ACTIVE (live, model-facing) history rows for a session. This is the
21248
21441
  * length of the history the next turn is seeded from — the dual-write slice
@@ -36019,6 +36212,8 @@ export type InitializeSessionStartInput = {
36019
36212
  subjectId: string;
36020
36213
  expectedRevision: number;
36021
36214
  } | null;
36215
+ /** Persist session.created only; realtime will supply the first human turn. */
36216
+ deferInitialTurn?: boolean;
36022
36217
  };
36023
36218
 
36024
36219
  export type InitializeSessionStartResult = {
@@ -36104,6 +36299,95 @@ export async function initializeSessionStartAtomically(
36104
36299
  if (!goal) throw new Error("Failed to create initial session goal");
36105
36300
  }
36106
36301
 
36302
+ if (input.deferInitialTurn) {
36303
+ const [existingCreatedEvent] = await tx
36304
+ .select({ id: schema.sessionEvents.id })
36305
+ .from(schema.sessionEvents)
36306
+ .where(
36307
+ and(
36308
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
36309
+ eq(schema.sessionEvents.sessionId, session.id),
36310
+ eq(schema.sessionEvents.type, "session.created"),
36311
+ ),
36312
+ )
36313
+ .limit(1);
36314
+ let sequence = session.lastSequence;
36315
+ let initializedNow = false;
36316
+ let insertedEvents: Array<typeof schema.sessionEvents.$inferSelect> = [];
36317
+ if (!existingCreatedEvent) {
36318
+ insertedEvents = await tx
36319
+ .insert(schema.sessionEvents)
36320
+ .values([
36321
+ {
36322
+ accountId: session.accountId,
36323
+ workspaceId: input.workspaceId,
36324
+ sessionId: session.id,
36325
+ sequence: ++sequence,
36326
+ type: "session.created",
36327
+ payload: sanitizeEventPayload({
36328
+ ...input.createdEventPayload,
36329
+ status: "idle",
36330
+ createdBy: creator.initiator,
36331
+ }),
36332
+ },
36333
+ ...(goal
36334
+ ? [
36335
+ {
36336
+ accountId: session.accountId,
36337
+ workspaceId: input.workspaceId,
36338
+ sessionId: session.id,
36339
+ sequence: ++sequence,
36340
+ type: "goal.set" as const,
36341
+ payload: sanitizeEventPayload({
36342
+ goalId: goal.id,
36343
+ text: goal.text,
36344
+ ...(goal.successCriteria
36345
+ ? { successCriteria: goal.successCriteria }
36346
+ : {}),
36347
+ version: goal.version,
36348
+ actor: "api",
36349
+ replaced: false,
36350
+ }),
36351
+ },
36352
+ ]
36353
+ : []),
36354
+ ])
36355
+ .returning();
36356
+ initializedNow = true;
36357
+ }
36358
+ await tx
36359
+ .update(schema.sessions)
36360
+ .set({
36361
+ temporalWorkflowId,
36362
+ lastSequence: sequence,
36363
+ ...(initializedNow && session.status === "queued" ? { status: "idle" } : {}),
36364
+ updatedAt: new Date(),
36365
+ })
36366
+ .where(
36367
+ and(
36368
+ eq(schema.sessions.workspaceId, input.workspaceId),
36369
+ eq(schema.sessions.id, session.id),
36370
+ ),
36371
+ );
36372
+ if (initializedNow && input.consumeNewSessionDraft) {
36373
+ await setSubjectRlsContext(
36374
+ tx as unknown as Database,
36375
+ input.consumeNewSessionDraft.subjectId,
36376
+ );
36377
+ await seedNewSessionDraftInTransaction(tx as unknown as Database, {
36378
+ workspaceId: input.workspaceId,
36379
+ subjectId: input.consumeNewSessionDraft.subjectId,
36380
+ expectedRevision: input.consumeNewSessionDraft.expectedRevision,
36381
+ });
36382
+ }
36383
+ return {
36384
+ events: insertedEvents.map(mapEvent),
36385
+ turn: null,
36386
+ temporalWorkflowId,
36387
+ workflowWakeRevision: null,
36388
+ };
36389
+ }
36390
+
36107
36391
  const existingUserEvents = await tx
36108
36392
  .select()
36109
36393
  .from(schema.sessionEvents)
@@ -36207,6 +36491,7 @@ export async function initializeSessionStartAtomically(
36207
36491
  let queueTailPosition = Number(session.queueTailPosition);
36208
36492
  if (!turn) {
36209
36493
  queueTailPosition += 1;
36494
+ const acceptedAt = new Date();
36210
36495
  [turn] = await tx
36211
36496
  .insert(schema.sessionTurns)
36212
36497
  .values({
@@ -36238,10 +36523,14 @@ export async function initializeSessionStartAtomically(
36238
36523
  : {},
36239
36524
  lineage: {},
36240
36525
  ...initiatorColumns(creator),
36526
+ initiatingHumanSubjectId:
36527
+ creator.initiator.kind === "subject" ? creator.initiator.subjectId : null,
36241
36528
  personalConnectionDelegations: parsedPersonalConnectionDelegations(
36242
36529
  session.initialPersonalConnectionDelegations,
36243
36530
  `sessions:${session.workspaceId}:${session.id}:initial`,
36244
36531
  ),
36532
+ createdAt: acceptedAt,
36533
+ updatedAt: acceptedAt,
36245
36534
  })
36246
36535
  .returning();
36247
36536
  if (!turn) throw new Error("Failed to create initial session turn");
@@ -36375,6 +36664,7 @@ export async function enqueueSessionTurn(
36375
36664
  const position = atHead
36376
36665
  ? Number(lockedSession.queueHeadPosition) - 1
36377
36666
  : Number(lockedSession.queueTailPosition) + 1;
36667
+ const acceptedAt = new Date();
36378
36668
  const [row] = await tx
36379
36669
  .insert(schema.sessionTurns)
36380
36670
  .values({
@@ -36402,7 +36692,11 @@ export async function enqueueSessionTurn(
36402
36692
  initiator: input.initiator,
36403
36693
  context: input.initiatorContext ?? {},
36404
36694
  }),
36695
+ initiatingHumanSubjectId:
36696
+ input.initiator.kind === "subject" ? input.initiator.subjectId : null,
36405
36697
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
36698
+ createdAt: acceptedAt,
36699
+ updatedAt: acceptedAt,
36406
36700
  })
36407
36701
  .returning();
36408
36702
  if (!row) {
@@ -36838,8 +37132,14 @@ export async function claimSessionWorkForAttempt(
36838
37132
  sessionId,
36839
37133
  { workspaceControl },
36840
37134
  );
36841
- const session = prefix.sessions[0];
37135
+ let session = prefix.sessions[0];
36842
37136
  if (!session) return { action: "unclaimed", reason: "no-work" };
37137
+ // Work and realtime may coexist, but claim remains the lazy lifecycle
37138
+ // cleanup point for an expired voice lease.
37139
+ session = await settleExpiredSessionRealtimeInTransaction(
37140
+ tx as unknown as Database,
37141
+ session,
37142
+ );
36843
37143
  if (effectiveControl.state !== "active") {
36844
37144
  return { action: "unclaimed", reason: "gate-closed" };
36845
37145
  }
@@ -36919,7 +37219,37 @@ export async function claimSessionWorkForAttempt(
36919
37219
  if (connectorPolicyRows.length > 2048) {
36920
37220
  throw new Error("Connector action policy snapshot exceeds the 2048-row bound");
36921
37221
  }
36922
- return await registerSessionTurnAttemptClaim(tx as unknown as Database, {
37222
+ // New queued, compaction, and internal-update executions are born
37223
+ // while the locked session still has no active-turn pointer. Publish
37224
+ // that ownership edge before freezing governance so the database
37225
+ // functions can require the complete exact-current-attempt chain.
37226
+ // The enclosing transaction keeps this invisible unless every later
37227
+ // claim/event/session settlement step succeeds.
37228
+ if (session.activeTurnId !== turn.id) {
37229
+ if (session.activeTurnId !== null) {
37230
+ throw new SessionControlInvariantError(
37231
+ `Attempt ${input.attemptId} cannot replace active turn ${session.activeTurnId}`,
37232
+ );
37233
+ }
37234
+ const [claimedSession] = await tx
37235
+ .update(schema.sessions)
37236
+ .set({ activeTurnId: turn.id })
37237
+ .where(
37238
+ and(
37239
+ eq(schema.sessions.accountId, session.accountId),
37240
+ eq(schema.sessions.workspaceId, workspaceId),
37241
+ eq(schema.sessions.id, sessionId),
37242
+ isNull(schema.sessions.activeTurnId),
37243
+ ),
37244
+ )
37245
+ .returning({ id: schema.sessions.id });
37246
+ if (!claimedSession) {
37247
+ throw new SessionControlInvariantError(
37248
+ `Attempt ${input.attemptId} could not establish active turn ${turn.id}`,
37249
+ );
37250
+ }
37251
+ }
37252
+ const attempt = await registerSessionTurnAttemptClaim(tx as unknown as Database, {
36923
37253
  id: input.attemptId,
36924
37254
  accountId: session.accountId,
36925
37255
  workspaceId,
@@ -36933,6 +37263,42 @@ export async function claimSessionWorkForAttempt(
36933
37263
  mcpApprovalPolicies,
36934
37264
  connectorActionPolicies: connectorPolicyRows,
36935
37265
  });
37266
+ // Freeze governance only after this exact attempt is durably claimed,
37267
+ // while the claim transaction still owns the session/turn/attempt
37268
+ // locks. Later policy or preference activation applies to a future
37269
+ // attempt and can never move accepted queued/in-flight work.
37270
+ await tx.execute(sql`
37271
+ SELECT snapshot.id
37272
+ FROM workspace_instruction_policy_get_or_create_snapshot(
37273
+ ${session.accountId}::uuid,
37274
+ ${workspaceId}::uuid,
37275
+ ${sessionId}::uuid,
37276
+ ${turn.id}::uuid,
37277
+ ${input.attemptId}::uuid,
37278
+ ${turn.executionGeneration}
37279
+ ) snapshot
37280
+ `);
37281
+ const initiatingHumanSubjectId =
37282
+ turn.initiatingHumanSubjectId ??
37283
+ (turn.initiatorKind === "subject" ? turn.initiatorSubjectId : null);
37284
+ if (initiatingHumanSubjectId) {
37285
+ await tx.execute(sql`
37286
+ SELECT snapshot.id
37287
+ FROM preference_registry_get_or_create_snapshot(
37288
+ ${session.accountId}::uuid,
37289
+ ${workspaceId}::uuid,
37290
+ ${sessionId}::uuid,
37291
+ ${turn.id}::uuid,
37292
+ ${input.attemptId}::uuid,
37293
+ ${turn.executionGeneration}
37294
+ ) snapshot
37295
+ `);
37296
+ // The preference function applies the immutable initiating human
37297
+ // only transaction-locally. Clear it before the rest of claim
37298
+ // settlement so no unrelated subject-scoped read inherits it.
37299
+ await tx.execute(sql`SELECT set_config('opengeni.subject_id', '', true)`);
37300
+ }
37301
+ return attempt;
36936
37302
  };
36937
37303
  if (session.activeTurnId !== null) {
36938
37304
  const [activeTurnPreview] = await tx
@@ -37280,6 +37646,9 @@ export async function claimSessionWorkForAttempt(
37280
37646
  latencyMode: schema.sessionTurns.latencyMode,
37281
37647
  sandboxBackend: schema.sessionTurns.sandboxBackend,
37282
37648
  sandboxOs: schema.sessionTurns.sandboxOs,
37649
+ initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
37650
+ initiatorKind: schema.sessionTurns.initiatorKind,
37651
+ initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
37283
37652
  })
37284
37653
  .from(schema.sessionTurns)
37285
37654
  .where(
@@ -37329,8 +37698,15 @@ export async function claimSessionWorkForAttempt(
37329
37698
  },
37330
37699
  ),
37331
37700
  ...initiatorColumns(compactionInitiator),
37701
+ initiatingHumanSubjectId:
37702
+ latestStarted?.initiatingHumanSubjectId ??
37703
+ (latestStarted?.initiatorKind === "subject"
37704
+ ? latestStarted.initiatorSubjectId
37705
+ : null),
37332
37706
  personalConnectionDelegations: [],
37333
37707
  startedAt: now,
37708
+ createdAt: now,
37709
+ updatedAt: now,
37334
37710
  })
37335
37711
  .returning();
37336
37712
  if (!compactionTurn) throw new Error("Failed to create context compaction execution");
@@ -37579,6 +37955,9 @@ export async function claimSessionWorkForAttempt(
37579
37955
  tools: schema.sessionTurns.tools,
37580
37956
  sandboxBackend: schema.sessionTurns.sandboxBackend,
37581
37957
  sandboxOs: schema.sessionTurns.sandboxOs,
37958
+ initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
37959
+ initiatorKind: schema.sessionTurns.initiatorKind,
37960
+ initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
37582
37961
  })
37583
37962
  .from(schema.sessionTurns)
37584
37963
  .where(
@@ -37613,6 +37992,39 @@ export async function claimSessionWorkForAttempt(
37613
37992
  typeof goalPolicy?.sandboxBackend === "string"
37614
37993
  ? goalPolicy.sandboxBackend
37615
37994
  : (latestStarted?.sandboxBackend ?? session.sandboxBackend);
37995
+ let initiatingHumanSubjectId =
37996
+ internalInitiator.initiator.kind === "subject"
37997
+ ? internalInitiator.initiator.subjectId
37998
+ : null;
37999
+ if (!initiatingHumanSubjectId && routingGoalUpdate) {
38000
+ const causalTurnIdValue =
38001
+ routingGoalUpdate.lineage &&
38002
+ typeof routingGoalUpdate.lineage === "object" &&
38003
+ !Array.isArray(routingGoalUpdate.lineage)
38004
+ ? (routingGoalUpdate.lineage as Record<string, unknown>).causalTurnId
38005
+ : null;
38006
+ const causalTurnId = typeof causalTurnIdValue === "string" ? causalTurnIdValue : null;
38007
+ if (causalTurnId) {
38008
+ const [causalTurn] = await tx
38009
+ .select({
38010
+ initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
38011
+ initiatorKind: schema.sessionTurns.initiatorKind,
38012
+ initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
38013
+ })
38014
+ .from(schema.sessionTurns)
38015
+ .where(
38016
+ and(
38017
+ eq(schema.sessionTurns.workspaceId, workspaceId),
38018
+ eq(schema.sessionTurns.sessionId, sessionId),
38019
+ eq(schema.sessionTurns.id, causalTurnId),
38020
+ ),
38021
+ )
38022
+ .limit(1);
38023
+ initiatingHumanSubjectId =
38024
+ causalTurn?.initiatingHumanSubjectId ??
38025
+ (causalTurn?.initiatorKind === "subject" ? causalTurn.initiatorSubjectId : null);
38026
+ }
38027
+ }
37616
38028
  await tx.execute(sql`set local opengeni.session_inference_claim = '1'`);
37617
38029
  const [internalTurn] = await tx
37618
38030
  .insert(schema.sessionTurns)
@@ -37644,8 +38056,11 @@ export async function claimSessionWorkForAttempt(
37644
38056
  { id: input.dispatchId, generation: 1, triggerEventId },
37645
38057
  ),
37646
38058
  ...initiatorColumns(internalInitiator),
38059
+ initiatingHumanSubjectId,
37647
38060
  personalConnectionDelegations: internalPersonalConnectionDelegations,
37648
38061
  startedAt: now,
38062
+ createdAt: now,
38063
+ updatedAt: now,
37649
38064
  })
37650
38065
  .returning();
37651
38066
  if (!internalTurn) throw new Error("Failed to create internal update inference");
@@ -37765,13 +38180,28 @@ export async function claimSessionWorkForAttempt(
37765
38180
  }),
37766
38181
  producerCodexCredentialId: null,
37767
38182
  });
37768
- const delivered = await deliverPendingUpdates(
37769
- session.accountId,
37770
- row.id,
37771
- row.executionGeneration,
37772
- session.lastSequence + 1,
37773
- now,
37774
- );
38183
+ const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
38184
+ // Cross-session updates are already projected through
38185
+ // delegation.context.append. Keep them pending instead of consuming
38186
+ // them as hidden context on the provider-delegated ordinary turn.
38187
+ const delivered = providerDelegatedTurn
38188
+ ? {
38189
+ count: 0,
38190
+ lastSequence: session.lastSequence,
38191
+ triggerEventId: null,
38192
+ historyItemId: null,
38193
+ historyItem: null,
38194
+ updates: [] as Array<typeof schema.sessionSystemUpdates.$inferSelect>,
38195
+ events: [] as Array<typeof schema.sessionEvents.$inferInsert>,
38196
+ event: null,
38197
+ }
38198
+ : await deliverPendingUpdates(
38199
+ session.accountId,
38200
+ row.id,
38201
+ row.executionGeneration,
38202
+ session.lastSequence + 1,
38203
+ now,
38204
+ );
37775
38205
  await persistDeliveredUpdateBatch(delivered, session.accountId, row.id);
37776
38206
  if (delivered.events.length > 0) {
37777
38207
  await tx.insert(schema.sessionEvents).values(delivered.events);
@@ -38345,15 +38775,23 @@ export async function settleSessionAttemptInterruptions(
38345
38775
  throw new Error(`Live interrupted attempt ${attemptId} lost its exact turn ownership`);
38346
38776
  }
38347
38777
 
38348
- const steer = interruptions.some((interruption) => interruption.kind === "steer");
38349
- const outcome: SessionTurnAttemptOutcome = steer ? "superseded" : "interrupted_recoverable";
38350
- const reason = steer
38351
- ? "steer"
38352
- : interruptions.some((interruption) => interruption.kind === "workspace_pause")
38353
- ? "workspace_pause"
38354
- : interruptions.some((interruption) => interruption.kind === "maintenance")
38355
- ? "maintenance"
38356
- : "session_pause";
38778
+ const terminalCancel = session.status === "cancelled";
38779
+ const steer =
38780
+ !terminalCancel && interruptions.some((interruption) => interruption.kind === "steer");
38781
+ const outcome: SessionTurnAttemptOutcome = terminalCancel
38782
+ ? "cancelled"
38783
+ : steer
38784
+ ? "superseded"
38785
+ : "interrupted_recoverable";
38786
+ const reason = terminalCancel
38787
+ ? "session_cancelled"
38788
+ : steer
38789
+ ? "steer"
38790
+ : interruptions.some((interruption) => interruption.kind === "workspace_pause")
38791
+ ? "workspace_pause"
38792
+ : interruptions.some((interruption) => interruption.kind === "maintenance")
38793
+ ? "maintenance"
38794
+ : "session_pause";
38357
38795
  let sequence = session.lastSequence;
38358
38796
  const closedTools = await closePendingSessionToolCallsInTransaction(
38359
38797
  tx as unknown as Database,
@@ -38378,38 +38816,14 @@ export async function settleSessionAttemptInterruptions(
38378
38816
  outcome,
38379
38817
  closedAt: now,
38380
38818
  });
38381
- const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = steer
38819
+ const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = terminalCancel
38382
38820
  ? [
38383
38821
  {
38384
38822
  accountId: session.accountId,
38385
38823
  workspaceId,
38386
38824
  sessionId,
38387
38825
  sequence: ++sequence,
38388
- type: "turn.superseded",
38389
- turnId: turn.id,
38390
- turnGeneration: turn.executionGeneration,
38391
- turnAttemptId: attemptId,
38392
- turnAssociation: "current",
38393
- payload: sanitizeEventPayload({ reason: "steer" }),
38394
- occurredAt: now,
38395
- },
38396
- {
38397
- accountId: session.accountId,
38398
- workspaceId,
38399
- sessionId,
38400
- sequence: ++sequence,
38401
- type: "session.status.changed",
38402
- payload: sanitizeEventPayload({ status: "queued" }),
38403
- occurredAt: now,
38404
- },
38405
- ]
38406
- : [
38407
- {
38408
- accountId: session.accountId,
38409
- workspaceId,
38410
- sessionId,
38411
- sequence: ++sequence,
38412
- type: "turn.recovery.requested",
38826
+ type: "turn.cancelled",
38413
38827
  turnId: turn.id,
38414
38828
  turnGeneration: turn.executionGeneration,
38415
38829
  turnAttemptId: attemptId,
@@ -38417,50 +38831,101 @@ export async function settleSessionAttemptInterruptions(
38417
38831
  payload: sanitizeEventPayload({ reason }),
38418
38832
  occurredAt: now,
38419
38833
  },
38420
- {
38421
- accountId: session.accountId,
38422
- workspaceId,
38423
- sessionId,
38424
- sequence: ++sequence,
38425
- type: "session.status.changed",
38426
- turnId: turn.id,
38427
- turnGeneration: turn.executionGeneration,
38428
- turnAttemptId: attemptId,
38429
- turnAssociation: "current",
38430
- payload: sanitizeEventPayload({ status: "recovering" }),
38431
- occurredAt: now,
38432
- },
38433
- ];
38834
+ ]
38835
+ : steer
38836
+ ? [
38837
+ {
38838
+ accountId: session.accountId,
38839
+ workspaceId,
38840
+ sessionId,
38841
+ sequence: ++sequence,
38842
+ type: "turn.superseded",
38843
+ turnId: turn.id,
38844
+ turnGeneration: turn.executionGeneration,
38845
+ turnAttemptId: attemptId,
38846
+ turnAssociation: "current",
38847
+ payload: sanitizeEventPayload({ reason: "steer" }),
38848
+ occurredAt: now,
38849
+ },
38850
+ {
38851
+ accountId: session.accountId,
38852
+ workspaceId,
38853
+ sessionId,
38854
+ sequence: ++sequence,
38855
+ type: "session.status.changed",
38856
+ payload: sanitizeEventPayload({ status: "queued" }),
38857
+ occurredAt: now,
38858
+ },
38859
+ ]
38860
+ : [
38861
+ {
38862
+ accountId: session.accountId,
38863
+ workspaceId,
38864
+ sessionId,
38865
+ sequence: ++sequence,
38866
+ type: "turn.recovery.requested",
38867
+ turnId: turn.id,
38868
+ turnGeneration: turn.executionGeneration,
38869
+ turnAttemptId: attemptId,
38870
+ turnAssociation: "current",
38871
+ payload: sanitizeEventPayload({ reason }),
38872
+ occurredAt: now,
38873
+ },
38874
+ {
38875
+ accountId: session.accountId,
38876
+ workspaceId,
38877
+ sessionId,
38878
+ sequence: ++sequence,
38879
+ type: "session.status.changed",
38880
+ turnId: turn.id,
38881
+ turnGeneration: turn.executionGeneration,
38882
+ turnAttemptId: attemptId,
38883
+ turnAssociation: "current",
38884
+ payload: sanitizeEventPayload({ status: "recovering" }),
38885
+ occurredAt: now,
38886
+ },
38887
+ ];
38434
38888
  const eventRows = await tx.insert(schema.sessionEvents).values(eventValues).returning();
38435
38889
  await tx
38436
38890
  .update(schema.sessionTurns)
38437
38891
  .set(
38438
- steer
38892
+ terminalCancel
38439
38893
  ? {
38440
- status: "superseded",
38894
+ status: "cancelled",
38441
38895
  activeAttemptId: null,
38442
38896
  metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
38897
+ cancelledBy: "system:session_cancelled",
38898
+ cancelReason: reason,
38443
38899
  version: turn.version + 1,
38444
38900
  finishedAt: turn.finishedAt ?? now,
38445
38901
  updatedAt: now,
38446
38902
  }
38447
- : {
38448
- status: "recovering",
38449
- activeAttemptId: null,
38450
- metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
38451
- cancelledBy: null,
38452
- cancelReason: null,
38453
- version: turn.version + 1,
38454
- finishedAt: null,
38455
- updatedAt: now,
38456
- },
38903
+ : steer
38904
+ ? {
38905
+ status: "superseded",
38906
+ activeAttemptId: null,
38907
+ metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
38908
+ version: turn.version + 1,
38909
+ finishedAt: turn.finishedAt ?? now,
38910
+ updatedAt: now,
38911
+ }
38912
+ : {
38913
+ status: "recovering",
38914
+ activeAttemptId: null,
38915
+ metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
38916
+ cancelledBy: null,
38917
+ cancelReason: null,
38918
+ version: turn.version + 1,
38919
+ finishedAt: null,
38920
+ updatedAt: now,
38921
+ },
38457
38922
  )
38458
38923
  .where(eq(schema.sessionTurns.id, turn.id));
38459
38924
  await tx
38460
38925
  .update(schema.sessions)
38461
38926
  .set({
38462
- status: steer ? "queued" : "recovering",
38463
- activeTurnId: steer ? null : turn.id,
38927
+ status: terminalCancel ? "cancelled" : steer ? "queued" : "recovering",
38928
+ activeTurnId: terminalCancel || steer ? null : turn.id,
38464
38929
  lastSequence: sequence,
38465
38930
  updatedAt: now,
38466
38931
  })
@@ -39293,6 +39758,13 @@ export type ApplySessionTurnSettlementResult =
39293
39758
  activeTurnId: string | null;
39294
39759
  };
39295
39760
 
39761
+ export type ApplySessionTurnSettlementHooks = {
39762
+ /** Test-only failure injection after terminal projection but before commit. */
39763
+ afterRealtimeDelegationProjection?:
39764
+ | ((projection: { entryId: string; turnId: string }) => void | Promise<void>)
39765
+ | undefined;
39766
+ };
39767
+
39296
39768
  function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAttemptOutcome | null {
39297
39769
  switch (status) {
39298
39770
  case "completed":
@@ -39306,6 +39778,40 @@ function attemptOutcomeForTurnStatus(status: SessionTurnStatus): SessionTurnAtte
39306
39778
  }
39307
39779
  }
39308
39780
 
39781
+ type TerminalSessionTurnStatus = "completed" | "failed" | "cancelled" | "superseded";
39782
+ type TerminalSessionTurnEventType =
39783
+ | "turn.completed"
39784
+ | "turn.failed"
39785
+ | "turn.cancelled"
39786
+ | "turn.superseded";
39787
+
39788
+ function terminalSessionTurnEventType(
39789
+ status: TerminalSessionTurnStatus,
39790
+ ): TerminalSessionTurnEventType {
39791
+ switch (status) {
39792
+ case "completed":
39793
+ return "turn.completed";
39794
+ case "failed":
39795
+ return "turn.failed";
39796
+ case "cancelled":
39797
+ return "turn.cancelled";
39798
+ case "superseded":
39799
+ return "turn.superseded";
39800
+ }
39801
+ }
39802
+
39803
+ function isTerminalSessionTurnStatus(
39804
+ status: SessionTurnStatus,
39805
+ ): status is TerminalSessionTurnStatus {
39806
+ return ["completed", "failed", "cancelled", "superseded"].includes(status);
39807
+ }
39808
+
39809
+ function sessionEventPayloadRecord(payload: unknown): Record<string, unknown> {
39810
+ return payload && typeof payload === "object" && !Array.isArray(payload)
39811
+ ? (payload as Record<string, unknown>)
39812
+ : {};
39813
+ }
39814
+
39309
39815
  /**
39310
39816
  * Atomically append terminal/requires-action truth, update the exact turn, and
39311
39817
  * transition the owning session. A superseded dispatch or closed control gate
@@ -39316,6 +39822,7 @@ export async function applySessionTurnSettlement(
39316
39822
  db: Database,
39317
39823
  workspaceId: string,
39318
39824
  input: ApplySessionTurnSettlementInput,
39825
+ hooks: ApplySessionTurnSettlementHooks = {},
39319
39826
  ): Promise<ApplySessionTurnSettlementResult> {
39320
39827
  const fromStatuses = input.fromStatuses ?? ["running", "requires_action"];
39321
39828
  const eventTypes = [
@@ -39744,11 +40251,37 @@ export async function applySessionTurnSettlement(
39744
40251
  });
39745
40252
  const inserted =
39746
40253
  values.length > 0 ? await tx.insert(schema.sessionEvents).values(values).returning() : [];
39747
- const terminal =
39748
- input.turnStatus === "completed" ||
39749
- input.turnStatus === "cancelled" ||
39750
- input.turnStatus === "failed" ||
39751
- input.turnStatus === "superseded";
40254
+ const terminal = isTerminalSessionTurnStatus(input.turnStatus);
40255
+ if (isTerminalSessionTurnStatus(input.turnStatus)) {
40256
+ const terminalType = terminalSessionTurnEventType(input.turnStatus);
40257
+ const persistedTerminal = inserted.find((event) => event.type === terminalType);
40258
+ const projection = await projectSessionRealtimeDelegationTerminalInTransaction(
40259
+ tx as unknown as Database,
40260
+ {
40261
+ accountId: session.accountId,
40262
+ workspaceId,
40263
+ sessionId: input.sessionId,
40264
+ turnId: input.turnId,
40265
+ turnStatus: input.turnStatus,
40266
+ terminalEvent: {
40267
+ type: terminalType,
40268
+ payload: persistedTerminal
40269
+ ? sessionEventPayloadRecord(persistedTerminal.payload)
40270
+ : {
40271
+ code: "delegation_terminal_event_missing",
40272
+ error: `Delegated turn reached ${input.turnStatus} without its canonical terminal event.`,
40273
+ },
40274
+ },
40275
+ now,
40276
+ },
40277
+ );
40278
+ if (projection) {
40279
+ await hooks.afterRealtimeDelegationProjection?.({
40280
+ entryId: projection.entry.id,
40281
+ turnId: input.turnId,
40282
+ });
40283
+ }
40284
+ }
39752
40285
  if (input.turnStatus === "running") {
39753
40286
  // Approval resume re-enters the same logical turn after a new fenced
39754
40287
  // dispatch has advanced its trigger. It is an authorized inference
@@ -40037,6 +40570,20 @@ export async function settleCodexCredentialLeaseLoss(
40037
40570
  if (!settlementEvent) {
40038
40571
  throw new Error("Codex lease-loss settlement did not persist its checkpoint event");
40039
40572
  }
40573
+ if (!input.checkpointDurable) {
40574
+ await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
40575
+ accountId: input.accountId,
40576
+ workspaceId: input.workspaceId,
40577
+ sessionId: input.sessionId,
40578
+ turnId: input.turnId,
40579
+ turnStatus: "failed",
40580
+ terminalEvent: {
40581
+ type: "turn.failed",
40582
+ payload: sessionEventPayloadRecord(settlementEvent.payload),
40583
+ },
40584
+ now,
40585
+ });
40586
+ }
40040
40587
 
40041
40588
  await tx
40042
40589
  .update(schema.sessionTurns)
@@ -40766,6 +41313,22 @@ export async function recoverSessionDispatch(
40766
41313
  },
40767
41314
  ])
40768
41315
  .returning();
41316
+ const failedEvent = inserted.find((event) => event.type === "turn.failed");
41317
+ if (!failedEvent) {
41318
+ throw new Error("Worker-death exhaustion did not persist its terminal event");
41319
+ }
41320
+ await projectSessionRealtimeDelegationTerminalInTransaction(tx as unknown as Database, {
41321
+ accountId: session.accountId,
41322
+ workspaceId,
41323
+ sessionId: input.sessionId,
41324
+ turnId: turn.id,
41325
+ turnStatus: "failed",
41326
+ terminalEvent: {
41327
+ type: "turn.failed",
41328
+ payload: sessionEventPayloadRecord(failedEvent.payload),
41329
+ },
41330
+ now,
41331
+ });
40769
41332
  await tx
40770
41333
  .update(schema.sessionTurns)
40771
41334
  .set({
@@ -41504,8 +42067,14 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
41504
42067
  .for("update")
41505
42068
  .limit(1);
41506
42069
  if (!workspace || !session) throw new Error(`Session not found: ${input.sessionId}`);
42070
+ const realtimeActive = await sessionRealtimeIsActiveInTransaction(
42071
+ tx as unknown as Database,
42072
+ input.workspaceId,
42073
+ input.sessionId,
42074
+ );
41507
42075
  const runnable =
41508
42076
  session.status !== "cancelled" &&
42077
+ !realtimeActive &&
41509
42078
  session.activeTurnId === null &&
41510
42079
  effectiveControl.state === "active";
41511
42080
  return runnable
@@ -41998,7 +42567,13 @@ export async function addSessionSystemUpdateWithSourceMutation(
41998
42567
  .returning();
41999
42568
  if (!event) throw new Error("Failed to create system-update pending event");
42000
42569
  await mutateSource(tx as unknown as Database, event.id);
42001
- const shouldWake = session.activeTurnId === null && effectiveControl.state === "active";
42570
+ const realtimeActive = await sessionRealtimeIsActiveInTransaction(
42571
+ tx as unknown as Database,
42572
+ input.workspaceId,
42573
+ input.sessionId,
42574
+ );
42575
+ const shouldWake =
42576
+ !realtimeActive && session.activeTurnId === null && effectiveControl.state === "active";
42002
42577
  const wake = shouldWake
42003
42578
  ? await registerInternalUpdateWakeInTransaction(tx as unknown as Database, {
42004
42579
  accountId: session.accountId,
@@ -42562,6 +43137,21 @@ export async function appendSessionEventsForTurnAttempt(
42562
43137
  };
42563
43138
  });
42564
43139
  const inserted = await tx.insert(schema.sessionEvents).values(values).returning();
43140
+ if (fence.allowed) {
43141
+ await projectSessionRealtimeDelegationProgressInTransaction(tx as unknown as Database, {
43142
+ accountId: session.accountId,
43143
+ workspaceId,
43144
+ sessionId,
43145
+ turnId,
43146
+ events: inserted.map((event) => ({
43147
+ id: event.id,
43148
+ sequence: event.sequence,
43149
+ type: event.type,
43150
+ payload: event.payload,
43151
+ })),
43152
+ now,
43153
+ });
43154
+ }
42565
43155
  await tx
42566
43156
  .update(schema.sessions)
42567
43157
  .set({
@@ -42986,6 +43576,7 @@ function mapSession(
42986
43576
  title: row.title ?? null,
42987
43577
  titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
42988
43578
  instructions: row.instructions ?? null,
43579
+ policyRole: row.policyRole ?? null,
42989
43580
  resources: row.resources as ResourceRef[],
42990
43581
  skills: (row.skills as SessionSkill[]) ?? [],
42991
43582
  tools: row.tools as ToolRef[],