@opengeni/db 0.23.0 → 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 (34) hide show
  1. package/dist/{chunk-3UCHDMKG.js → chunk-7WTDI7Y3.js} +109 -2
  2. package/dist/chunk-7WTDI7Y3.js.map +1 -0
  3. package/dist/{chunk-L6ADMZHE.js → chunk-KW6U54V2.js} +3 -1
  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 +68 -2
  7. package/dist/index.js +1168 -336
  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 +2 -2
  12. package/dist/schema.d.ts +262 -4
  13. package/dist/schema.js +3 -1
  14. package/dist/session-realtime-mirror.d.ts +22 -1
  15. package/dist/workspace-instruction-policies-schema.d.ts +68 -0
  16. package/dist/workspace-instruction-policies.d.ts +53 -7
  17. package/drizzle/0165_document_authority_foundation.sql +259 -0
  18. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  19. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  20. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  21. package/package.json +3 -3
  22. package/src/connection-token-resolver.ts +79 -16
  23. package/src/index.ts +419 -23
  24. package/src/preference-registry.ts +103 -0
  25. package/src/runtime-posture.ts +2 -0
  26. package/src/schema.ts +86 -0
  27. package/src/session-control.ts +48 -1
  28. package/src/session-queue-commands.ts +45 -11
  29. package/src/session-realtime-mirror.ts +117 -1
  30. package/src/session-realtime.ts +49 -1
  31. package/src/workspace-instruction-policies-schema.ts +30 -0
  32. package/src/workspace-instruction-policies.ts +438 -24
  33. package/dist/chunk-3UCHDMKG.js.map +0 -1
  34. package/dist/chunk-L6ADMZHE.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 {
@@ -232,6 +234,11 @@ import {
232
234
  projectSessionRealtimeDelegationProgressInTransaction,
233
235
  projectSessionRealtimeDelegationTerminalInTransaction,
234
236
  } from "./session-realtime-ledger";
237
+ import {
238
+ mirrorSessionRealtimeContextInTransaction,
239
+ renderRealtimeHumanInputRequestContext,
240
+ renderRealtimeHumanInputResponseContext,
241
+ } from "./session-realtime-mirror";
235
242
  import {
236
243
  listSessionRealtimeContinuityEntriesInTransaction,
237
244
  type SessionRealtimeContinuityEntry,
@@ -1197,6 +1204,55 @@ export async function withWorkspaceUsageLock<T>(
1197
1204
  });
1198
1205
  }
1199
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
+
1200
1256
  export async function withAccountRls<T>(
1201
1257
  db: Database,
1202
1258
  accountId: string,
@@ -3498,6 +3554,47 @@ export type UpdateConnectionInput = {
3498
3554
  updatedBySubjectId?: string | null;
3499
3555
  };
3500
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
+
3501
3598
  /** Server-owned verification facts; public schemas expose them read-only and nullable. */
3502
3599
  export type ConnectionMetadataWithVerification = ConnectionMetadata & {
3503
3600
  verifiedInstallAt: string | null;
@@ -3880,6 +3977,39 @@ export async function requireFile(
3880
3977
  return file;
3881
3978
  }
3882
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
+
3883
4013
  export type RetainedFileArtifact = {
3884
4014
  file: FileAsset;
3885
4015
  uploadStatus: FileUploadStatus | null;
@@ -5327,6 +5457,191 @@ export async function updateConnection(
5327
5457
  );
5328
5458
  }
5329
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
+
5330
5645
  async function revokeConnectionInScope(
5331
5646
  db: Database,
5332
5647
  workspaceId: string,
@@ -5371,13 +5686,20 @@ export async function revokeConnection(
5371
5686
  workspaceId: string,
5372
5687
  connectionId: string,
5373
5688
  updatedBySubjectId?: string | null,
5689
+ expectedVersion?: number,
5374
5690
  ): Promise<ConnectionMetadataWithVerification | null> {
5375
5691
  return await withConnectionSubjectRls(
5376
5692
  db,
5377
5693
  workspaceId,
5378
5694
  updatedBySubjectId,
5379
5695
  async (scopedDb) =>
5380
- await revokeConnectionInScope(scopedDb, workspaceId, connectionId, updatedBySubjectId),
5696
+ await revokeConnectionInScope(
5697
+ scopedDb,
5698
+ workspaceId,
5699
+ connectionId,
5700
+ updatedBySubjectId,
5701
+ expectedVersion,
5702
+ ),
5381
5703
  );
5382
5704
  }
5383
5705
 
@@ -20364,21 +20686,9 @@ function validateAnsweredHumanInput(
20364
20686
  `Text question ${question.id} accepts one value`,
20365
20687
  );
20366
20688
  }
20367
- const value = values[0] ?? "";
20368
- const minLength = question.validation?.minLength;
20369
- const maxLength = question.validation?.maxLength;
20370
- if (minLength != null && value.length < minLength) {
20371
- throw new HumanInputResponseValidationError(
20372
- "INVALID_RESPONSE",
20373
- `Question ${question.id} is shorter than its minimum length`,
20374
- );
20375
- }
20376
- if (maxLength != null && value.length > maxLength) {
20377
- throw new HumanInputResponseValidationError(
20378
- "INVALID_RESPONSE",
20379
- `Question ${question.id} exceeds its maximum length`,
20380
- );
20381
- }
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.
20382
20692
  continue;
20383
20693
  }
20384
20694
  if (other && !question.allowOther) {
@@ -20689,6 +20999,27 @@ export async function acceptSessionHumanInputResponse(
20689
20999
  })
20690
21000
  .returning();
20691
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
+ });
20692
21023
  await tx
20693
21024
  .update(schema.sessions)
20694
21025
  .set({ lastSequence: session.lastSequence + 1, updatedAt: now })
@@ -38173,11 +38504,10 @@ export async function claimSessionWorkForAttempt(
38173
38504
  sessionId,
38174
38505
  turnId: row.id,
38175
38506
  position: Number(historyPosition),
38176
- item: sanitizeModelPayload({
38177
- type: "message",
38178
- role: "user",
38179
- content: row.prompt,
38180
- }),
38507
+ item: durableUserHistoryItem(
38508
+ row.prompt,
38509
+ Array.isArray(row.resources) ? (row.resources as ResourceRef[]) : [],
38510
+ ),
38181
38511
  producerCodexCredentialId: null,
38182
38512
  });
38183
38513
  const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
@@ -39912,11 +40242,11 @@ export async function applySessionTurnSettlement(
39912
40242
  };
39913
40243
  }
39914
40244
 
40245
+ const humanInputRequests = input.runState?.humanInputRequests ?? [];
39915
40246
  if (input.runState) {
39916
40247
  if (input.turnStatus !== "requires_action" || input.sessionStatus !== "requires_action") {
39917
40248
  throw new Error("A frozen run state requires a requires_action settlement");
39918
40249
  }
39919
- const humanInputRequests = input.runState.humanInputRequests ?? [];
39920
40250
  for (const request of humanInputRequests) {
39921
40251
  const parsedQuestions = request.questions.map((question) =>
39922
40252
  HumanInputQuestionContract.parse(question),
@@ -40168,7 +40498,10 @@ export async function applySessionTurnSettlement(
40168
40498
  eq(schema.sessionHumanInputRequests.status, "pending"),
40169
40499
  ),
40170
40500
  )
40171
- .returning({ id: schema.sessionHumanInputRequests.id })
40501
+ .returning({
40502
+ id: schema.sessionHumanInputRequests.id,
40503
+ questions: schema.sessionHumanInputRequests.questions,
40504
+ })
40172
40505
  : [];
40173
40506
  const terminalHumanInputEvents: AppendEventInput[] = terminalHumanInputRows.map(
40174
40507
  (request) => ({
@@ -40251,6 +40584,69 @@ export async function applySessionTurnSettlement(
40251
40584
  });
40252
40585
  const inserted =
40253
40586
  values.length > 0 ? await tx.insert(schema.sessionEvents).values(values).returning() : [];
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
+ }
40254
40650
  const terminal = isTerminalSessionTurnStatus(input.turnStatus);
40255
40651
  if (isTerminalSessionTurnStatus(input.turnStatus)) {
40256
40652
  const terminalType = terminalSessionTurnEventType(input.turnStatus);
@@ -558,6 +558,109 @@ export async function listPreferenceRegistry(
558
558
  });
559
559
  }
560
560
 
561
+ export type PreferenceRegistryGovernanceIdentity = Pick<
562
+ PreferenceRegistryDescriptor,
563
+ "id" | "revisionId" | "contentHash" | "activeVersion" | "scope"
564
+ >;
565
+
566
+ export type CurrentPreferenceRegistryGovernanceMetadata = {
567
+ descriptors: PreferenceRegistryGovernanceIdentity[];
568
+ truncated: boolean;
569
+ };
570
+
571
+ /**
572
+ * Read the current active descriptor identities for one exact authenticated
573
+ * subject. Full preference values and even descriptor display text stay inside
574
+ * this DB boundary; Workspace State receives only stable revision metadata.
575
+ */
576
+ export async function getCurrentPreferenceRegistryGovernanceMetadata(
577
+ db: Database,
578
+ input: { workspaceId: string; subjectId: string },
579
+ ): Promise<CurrentPreferenceRegistryGovernanceMetadata> {
580
+ return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
581
+ const rows = await scopedDb
582
+ .select({
583
+ preference: schema.preferenceRegistryPreferences,
584
+ revision: schema.preferenceRegistryRevisions,
585
+ })
586
+ .from(schema.preferenceRegistryPreferences)
587
+ .innerJoin(
588
+ schema.preferenceRegistryRevisions,
589
+ and(
590
+ eq(
591
+ schema.preferenceRegistryRevisions.id,
592
+ schema.preferenceRegistryPreferences.activeRevisionId,
593
+ ),
594
+ eq(
595
+ schema.preferenceRegistryRevisions.accountId,
596
+ schema.preferenceRegistryPreferences.accountId,
597
+ ),
598
+ ),
599
+ )
600
+ .where(
601
+ and(
602
+ eq(schema.preferenceRegistryPreferences.status, "active"),
603
+ or(
604
+ isNull(schema.preferenceRegistryRevisions.expiresAt),
605
+ gt(schema.preferenceRegistryRevisions.expiresAt, sql`transaction_timestamp()`),
606
+ ),
607
+ ),
608
+ )
609
+ .orderBy(
610
+ sql`case ${schema.preferenceRegistryPreferences.scope}
611
+ when 'organization' then 0
612
+ when 'workspace' then 1
613
+ when 'user' then 2
614
+ else 3
615
+ end`,
616
+ desc(schema.preferenceRegistryRevisions.precedenceRank),
617
+ asc(schema.preferenceRegistryPreferences.stableKey),
618
+ asc(schema.preferenceRegistryPreferences.id),
619
+ )
620
+ .limit(PREFERENCE_REGISTRY_DESCRIPTOR_MAX_COUNT + 1);
621
+
622
+ const bounded = boundPreferenceRegistryDescriptors(
623
+ rows.map(({ preference, revision }) =>
624
+ PreferenceRegistryDescriptor.parse({
625
+ id: preference.id,
626
+ stableKey: preference.stableKey,
627
+ title: revision.title,
628
+ description: revision.description,
629
+ scope: preference.scope,
630
+ activeVersion: preference.activationVersion,
631
+ revisionId: revision.id,
632
+ contentHash: revision.contentHash,
633
+ precedence: {
634
+ tier: preference.scope,
635
+ rank: revision.precedenceRank,
636
+ conflictStrategy: revision.conflictStrategy,
637
+ conflictsWith: revision.conflictsWith,
638
+ },
639
+ provenance: {
640
+ source: revision.provenanceSource,
641
+ sourceIdHash: revision.provenanceSourceId
642
+ ? contentHash(revision.provenanceSourceId)
643
+ : null,
644
+ trust: revision.trust,
645
+ },
646
+ expiresAt: revision.expiresAt ? iso(revision.expiresAt) : null,
647
+ retrievalHandle: `preference://${preference.id}/revisions/${revision.id}?sha256=${revision.contentHash}`,
648
+ }),
649
+ ),
650
+ );
651
+ return {
652
+ descriptors: bounded.descriptors.map((descriptor) => ({
653
+ id: descriptor.id,
654
+ revisionId: descriptor.revisionId,
655
+ contentHash: descriptor.contentHash,
656
+ activeVersion: descriptor.activeVersion,
657
+ scope: descriptor.scope,
658
+ })),
659
+ truncated: bounded.truncated,
660
+ };
661
+ });
662
+ }
663
+
561
664
  export async function listPreferenceRegistryForAttempt(
562
665
  db: Database,
563
666
  input: PreferenceRegistryAttemptClaims & PreferenceRegistryListInput,
@@ -24,6 +24,7 @@ export const FORCE_RLS_TABLES = [
24
24
  "codex_rotation_settings",
25
25
  "codex_subscription_credentials",
26
26
  "composer_drafts",
27
+ "connection_disconnect_operations",
27
28
  "connections",
28
29
  "connector_action_policies",
29
30
  "connector_action_requests",
@@ -176,6 +177,7 @@ export const RUNTIME_FULL_DML_TABLES = [
176
177
  "codex_rotation_settings",
177
178
  "codex_subscription_credentials",
178
179
  "composer_drafts",
180
+ "connection_disconnect_operations",
179
181
  "connections",
180
182
  "connector_action_policies",
181
183
  "connector_action_requests",