@opengeni/db 0.27.7 → 0.27.9

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 (61) hide show
  1. package/dist/{chunk-IHPCI4GV.js → chunk-2JFRTKTG.js} +1 -1
  2. package/dist/chunk-2JFRTKTG.js.map +1 -0
  3. package/dist/{chunk-EX7CDSGH.js → chunk-NX2JNJJP.js} +13 -4
  4. package/dist/chunk-NX2JNJJP.js.map +1 -0
  5. package/dist/codex-token-resolver.d.ts +50 -8
  6. package/dist/connection-token-resolver.d.ts +45 -7
  7. package/dist/database.d.ts +137 -0
  8. package/dist/index.d.ts +25 -154
  9. package/dist/index.js +2796 -2634
  10. package/dist/index.js.map +1 -1
  11. package/dist/insights.d.ts +1 -1
  12. package/dist/memory-governance.d.ts +1 -1
  13. package/dist/new-session-drafts.d.ts +1 -1
  14. package/dist/preference-registry.d.ts +1 -1
  15. package/dist/provision-roles.d.ts +1 -1
  16. package/dist/provision-roles.js +1 -1
  17. package/dist/runtime-posture.d.ts +1 -1
  18. package/dist/schema.d.ts +44 -0
  19. package/dist/schema.js +1 -1
  20. package/dist/scoped-knowledge.d.ts +1 -1
  21. package/dist/session-control.d.ts +2 -1
  22. package/dist/session-queue-commands.d.ts +1 -1
  23. package/dist/session-realtime-context.d.ts +1 -1
  24. package/dist/session-realtime-ledger.d.ts +1 -1
  25. package/dist/session-realtime-mirror.d.ts +1 -1
  26. package/dist/session-realtime-state.d.ts +1 -1
  27. package/dist/session-realtime-terminal.d.ts +1 -1
  28. package/dist/session-realtime.d.ts +1 -1
  29. package/dist/session-tool-call-settlement.d.ts +1 -1
  30. package/dist/turn-initiator.d.ts +1 -1
  31. package/dist/workspace-artifacts.d.ts +1 -1
  32. package/dist/workspace-instruction-policies.d.ts +1 -1
  33. package/drizzle/0170_session_control_wake_revision.sql +133 -0
  34. package/drizzle/0171_social_connection_subject_ownership.sql +53 -0
  35. package/package.json +3 -3
  36. package/src/codex-token-resolver.ts +102 -49
  37. package/src/connection-token-resolver.ts +67 -26
  38. package/src/database.ts +393 -0
  39. package/src/index.ts +439 -516
  40. package/src/insights.ts +2 -2
  41. package/src/memory-governance.ts +2 -2
  42. package/src/new-session-drafts.ts +1 -1
  43. package/src/preference-registry.ts +2 -2
  44. package/src/provision-roles.ts +1 -1
  45. package/src/runtime-posture.ts +1 -1
  46. package/src/schema.ts +16 -3
  47. package/src/scoped-knowledge.ts +2 -2
  48. package/src/session-control.ts +12 -5
  49. package/src/session-queue-commands.ts +4 -1
  50. package/src/session-realtime-context.ts +1 -1
  51. package/src/session-realtime-ledger.ts +1 -1
  52. package/src/session-realtime-mirror.ts +1 -1
  53. package/src/session-realtime-state.ts +1 -1
  54. package/src/session-realtime-terminal.ts +1 -1
  55. package/src/session-realtime.ts +1 -1
  56. package/src/session-tool-call-settlement.ts +1 -1
  57. package/src/turn-initiator.ts +1 -1
  58. package/src/workspace-artifacts.ts +2 -2
  59. package/src/workspace-instruction-policies.ts +2 -2
  60. package/dist/chunk-EX7CDSGH.js.map +0 -1
  61. package/dist/chunk-IHPCI4GV.js.map +0 -1
package/src/index.ts CHANGED
@@ -154,7 +154,12 @@ import {
154
154
  VERCEL_AI_GATEWAY_CONNECTION_ROLE,
155
155
  type Settings,
156
156
  } from "@opengeni/config";
157
- import { boundModelToolOutputItem, isCodexBilledModel } from "@opengeni/codex";
157
+ import {
158
+ boundModelToolOutputItem,
159
+ isCodexBilledModel,
160
+ refreshCodexToken,
161
+ type CodexFetch,
162
+ } from "@opengeni/codex";
158
163
  // Re-exported so consumers get the whole codex-billed detection surface (the pure
159
164
  // prefix test + the credential-aware predicates below) from a single import.
160
165
  export { isCodexBilledModel } from "@opengeni/codex";
@@ -187,16 +192,10 @@ import {
187
192
  type FrozenTurnInitiator,
188
193
  } from "./turn-initiator";
189
194
  export { frozenInitiatorForCommandActor, type FrozenTurnInitiator } from "./turn-initiator";
190
- import type { PgDatabase, PgTransactionConfig } from "drizzle-orm/pg-core";
191
- import { drizzle } from "drizzle-orm/postgres-js";
192
- import postgres from "postgres";
193
- import { decryptEnvironmentValue } from "./environment-crypto";
195
+ import { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
194
196
  import { sanitizeEventPayload, sanitizeModelPayload } from "./event-payload-sanitizer";
195
197
  import { seedNewSessionDraftInTransaction } from "./new-session-drafts";
196
- import {
197
- runIdempotentPersistenceTransaction,
198
- type IdempotentPersistenceTransactionOptions,
199
- } from "./persistence-errors";
198
+ import { runIdempotentPersistenceTransaction } from "./persistence-errors";
200
199
  import {
201
200
  closePendingSessionToolCallsInTransaction,
202
201
  historyCallId,
@@ -303,22 +302,58 @@ export {
303
302
  // Workspace Memory V1 pure domain surface (gates, render, canonical prompt text).
304
303
  export * from "./memory-domain";
305
304
 
306
- // §7.7 driver widening (Step I). `Database` is the structural, cross-driver
307
- // query-layer port: every helper in this file accepts `db: Database` and uses
308
- // only the methods present on drizzle's base `PgDatabase` (select/insert/update/
309
- // delete/transaction/execute). Widening from the concrete
310
- // `PostgresJsDatabase<typeof schema>` to `PgDatabase<any, typeof schema>` is a
311
- // pure TYPE change — no runtime behavior changes — that lets an embedded host
312
- // inject ANY drizzle pg driver handle (node-postgres, neon-http, etc.) bound to
313
- // OpenGeni's schema, not just the postgres-js handle `createDb` builds. The
314
- // `any` for the query-result HKT is deliberate: it keeps `db.execute(sql\`…\`)`
315
- // callable across drivers whose raw-result shapes differ (postgres-js returns a
316
- // row array; node-postgres returns `{ rows }`). The three raw `db.execute(…)`
317
- // reads that index a row array (`getManagedUserByEmail` here is the only
318
- // host-facing one — see `userLookup`) stay postgres-js-shaped for standalone;
319
- // `userLookup` is the injection seam for hosts on a different driver.
320
- // `PostgresJsDatabase<typeof schema>` is assignable to this, so standalone is
321
- // unaffected.
305
+ import {
306
+ dbBindingFor,
307
+ rawRows,
308
+ retryRlsPersistence,
309
+ retryWorkspacePersistence,
310
+ rlsContextForWorkspace,
311
+ setRlsContext,
312
+ setSubjectRlsContext,
313
+ withAccountRls,
314
+ withRlsContext,
315
+ withWorkspaceRls,
316
+ withWorkspaceSubjectRls,
317
+ withWorkspaceUsageLock,
318
+ type Database,
319
+ } from "./database";
320
+ export {
321
+ createDb,
322
+ registerDbBinding,
323
+ rlsContextForWorkspace,
324
+ rlsStrategyFor,
325
+ setRlsContext,
326
+ setSubjectRlsContext,
327
+ withAccountRls,
328
+ withDatabaseStatementTimeout,
329
+ withRlsContext,
330
+ withWorkspaceRls,
331
+ withWorkspaceSubjectRls,
332
+ withWorkspaceUsageLock,
333
+ type CreateDbOptions,
334
+ type Database,
335
+ type DbClient,
336
+ type RlsContext,
337
+ type RlsStrategy,
338
+ type UserLookup,
339
+ } from "./database";
340
+ import {
341
+ buildCodexTokenResolver as buildCodexTokenResolverCore,
342
+ fetchCodexRateLimitResetCreditsForAccount as fetchCodexRateLimitResetCreditsForAccountCore,
343
+ fetchCodexUsageForAccount as fetchCodexUsageForAccountCore,
344
+ type CodexAccountUsageSnapshot,
345
+ type CodexAuthDeps,
346
+ type CodexCredentialForRun,
347
+ type CodexCredentialTokens,
348
+ } from "./codex-token-resolver";
349
+ import {
350
+ buildConnectionTokenResolver as buildConnectionTokenResolverCore,
351
+ refreshOAuthConnectionCredential as refreshOAuthConnectionCredentialCore,
352
+ type ConnectionBrokerDeps,
353
+ type ConnectionCredentialForBroker,
354
+ type ConnectionTokenResolverOptions,
355
+ } from "./connection-token-resolver";
356
+
322
357
  function parsedPersonalConnectionDelegations(
323
358
  value: unknown,
324
359
  context: string,
@@ -330,8 +365,6 @@ function parsedPersonalConnectionDelegations(
330
365
  return parsed.data.map((delegation) => ({ ...delegation }));
331
366
  }
332
367
 
333
- export type Database = PgDatabase<any, typeof schema>;
334
-
335
368
  /** Raised when a durable session tool-policy write lost its version fence. */
336
369
  export class SessionToolPolicyVersionConflictError extends Error {
337
370
  readonly code = "SESSION_TOOL_POLICY_CONFLICT";
@@ -342,16 +375,6 @@ export class SessionToolPolicyVersionConflictError extends Error {
342
375
  }
343
376
  }
344
377
 
345
- export type DbClient = {
346
- db: Database;
347
- close: () => Promise<void>;
348
- };
349
-
350
- export type RlsContext = {
351
- accountId: string;
352
- workspaceId?: string | null;
353
- };
354
-
355
378
  export type NestedAgentDepthPolicySource = "session" | "workspace" | "deployment" | "default";
356
379
 
357
380
  export type SessionDepthPolicy = {
@@ -414,165 +437,6 @@ export class SessionSpawnDeniedDbError extends Error {
414
437
  }
415
438
  }
416
439
 
417
- /**
418
- * RLS posture for the connection OpenGeni's query layer runs over (Step I, §7.7).
419
- *
420
- * - `"force"` (DEFAULT — today's standalone behavior, byte-for-byte): OpenGeni
421
- * connects as a NON-OWNER role (`opengeni_app`) and every table carries
422
- * `FORCE ROW LEVEL SECURITY`, so the workspace/account GUCs set by
423
- * `setRlsContext` are the ONLY thing that admits rows — even the table owner
424
- * is subject to RLS. This is the Fork-A isolation guarantee.
425
- * - `"scoped"` (embedded Fork-B opt-in): the host runs OpenGeni's queries over a
426
- * role that OWNS the dedicated schema (RLS need not be forced for that role),
427
- * relying on the host's own tenant boundary. OpenGeni STILL emits the
428
- * `set_config('opengeni.account_id'/'workspace_id', …)` GUCs defensively on
429
- * every scoped query, so the application query path is byte-identical between
430
- * the two strategies and the app code is RLS-mode-agnostic. The strategy is a
431
- * declared posture (consumed by `provisionRoles` and as a documented
432
- * invariant), NOT a query-path branch — there is deliberately no `if
433
- * (strategy === …)` anywhere in the helpers below. Picking `"scoped"` does not
434
- * relax any GUC; it only changes which DB role the host provisions/connects as
435
- * and asserts that the host accepts owning the isolation boundary.
436
- */
437
- export type RlsStrategy = "force" | "scoped";
438
-
439
- /**
440
- * Resolve a host-IdP/Better-Auth user *identifier* by email. Injected via
441
- * `createDb({ userLookup })` (Step I). UNSET → today's raw parameterized select
442
- * against Better Auth's `auth_users` table (see `getManagedUserByEmail`), which
443
- * relies on the postgres-js array-shaped `db.execute` result. An embedded host
444
- * whose identity lives elsewhere (a different IdP table, a different driver, or
445
- * a non-`auth_users` user store) injects this closure so OpenGeni never touches
446
- * `auth_users` directly. Returns the user id, or null when no such user exists.
447
- */
448
- export type UserLookup = (db: Database, email: string) => Promise<string | null>;
449
-
450
- export type CreateDbOptions = {
451
- /**
452
- * The Postgres `search_path` for this connection (Step I, §7.8 runtime half).
453
- * UNSET → today's behavior: NO `search_path` startup parameter is sent, so the
454
- * server default applies (`public` for standalone, where every table + the
455
- * `vector` extension + `gen_random_uuid()` live). For an embedded dedicated
456
- * schema, pass e.g. `"opengeni,opengeni_private,public"` — postgres-js sends
457
- * it as a per-session startup parameter (the supported, query-param-free way;
458
- * URL `?search_path=` is IGNORED by postgres-js). Keep `public` LAST so the
459
- * `vector` type and `gen_random_uuid()` (which live in `public` on the
460
- * pgvector image) still resolve — the schema-isolation contract live footgun.
461
- */
462
- searchPath?: string;
463
- /** RLS posture; defaults to `"force"` (today's standalone). */
464
- rlsStrategy?: RlsStrategy;
465
- /** Host-provided user-by-email resolver; unset → today's raw `auth_users` query. */
466
- userLookup?: UserLookup;
467
- /** postgres-js pool size; defaults to today's `10`. */
468
- max?: number;
469
- /**
470
- * Connection-local default transaction isolation sent in the postgres-js
471
- * startup parameters. This is intentionally not a role/database default:
472
- * tests and embedded callers can exercise a different ambient isolation
473
- * without mutating a shared PostgreSQL role or affecting other connections.
474
- */
475
- isolationLevel?: postgres.ConnectionParameters["default_transaction_isolation"];
476
- };
477
-
478
- /**
479
- * The active RLS strategy + userLookup for an injected `Database`, recorded in a
480
- * side WeakMap so helpers (and `getManagedUserByEmail`) can consult the host's
481
- * binding without changing every call signature. A handle with no recorded
482
- * config (e.g. one built outside `createDb`, or in a test) falls back to the
483
- * standalone defaults: `rlsStrategy: "force"`, raw `auth_users` lookup.
484
- */
485
- type DbBinding = { rlsStrategy: RlsStrategy; userLookup?: UserLookup };
486
- const dbBindings = new WeakMap<object, DbBinding>();
487
-
488
- /** The strategy bound to a handle (or the `"force"` default). */
489
- export function rlsStrategyFor(db: Database): RlsStrategy {
490
- return dbBindings.get(db as unknown as object)?.rlsStrategy ?? "force";
491
- }
492
-
493
- /**
494
- * Run a raw SQL query and read its rows as a typed array.
495
- *
496
- * Why this exists: the Step I driver widening (`Database = PgDatabase<any, …>`)
497
- * deliberately sets the query-result HKT to `any` so `db.execute(…)` is callable
498
- * across drivers whose raw-result shapes differ (postgres-js → row array;
499
- * node-postgres → `{ rows }`). A side effect is that `db.execute<T>(…)` now
500
- * resolves to `any`, erasing the per-row element type at the call site. OpenGeni's
501
- * OWN internal raw queries usually run over the postgres-js handle `createDb`
502
- * builds (array result), while an embedded host may inject a node-postgres style
503
- * driver (`{ rows }`). Normalize those two standard shapes in one place; reject
504
- * an unknown driver result rather than silently treating it as an empty query.
505
- */
506
- async function rawRows<T extends Record<string, unknown>>(
507
- executor: Pick<Database, "execute">,
508
- query: SQL,
509
- ): Promise<T[]> {
510
- const result = await executor.execute<T>(query);
511
- if (Array.isArray(result)) {
512
- return result as unknown as T[];
513
- }
514
- const rows = (result as unknown as { rows?: unknown }).rows;
515
- if (Array.isArray(rows)) {
516
- return rows as T[];
517
- }
518
- throw new Error("Unsupported database execute() result shape");
519
- }
520
-
521
- export function createDb(databaseUrl: string, options: CreateDbOptions = {}): DbClient {
522
- // `prepare: false` is REQUIRED for Azure Database for PostgreSQL Flexible
523
- // Server's transaction-pooling PgBouncer: postgres-js's default named prepared
524
- // statements (`s_N`) are bound to one backend, but a transaction pooler hands
525
- // each transaction a different backend, so a later `execute` intermittently
526
- // throws `prepared statement "s_N" does not exist`. Every RLS read in this
527
- // module (set_config + SELECT inside one db.transaction) rides on this pool, so
528
- // the failure surfaces as a "worked, then didn't" credential/permission read.
529
- // idle_timeout + max_lifetime recycle connections so a pooler-recycled backend
530
- // is never reused indefinitely; application_name aids server-side diagnostics.
531
- const client = postgres(databaseUrl, {
532
- max: options.max ?? 10,
533
- prepare: false,
534
- idle_timeout: 30,
535
- max_lifetime: 1800,
536
- // `connection` carries per-session Postgres STARTUP parameters. `application_name`
537
- // (always) aids server-side diagnostics; `search_path` (embedded only) is the
538
- // supported, query-param-free way to scope a connection to a dedicated schema —
539
- // postgres-js IGNORES a URL `?search_path=`. Unset searchPath → omit it so the
540
- // server default (`public`) is unchanged for standalone.
541
- connection: {
542
- application_name: "opengeni",
543
- ...(options.searchPath ? { search_path: options.searchPath } : {}),
544
- ...(options.isolationLevel ? { default_transaction_isolation: options.isolationLevel } : {}),
545
- },
546
- });
547
- const db = drizzle(client, { schema });
548
- dbBindings.set(db as unknown as object, {
549
- rlsStrategy: options.rlsStrategy ?? "force",
550
- ...(options.userLookup ? { userLookup: options.userLookup } : {}),
551
- });
552
- return {
553
- db,
554
- close: async () => {
555
- await client.end();
556
- },
557
- };
558
- }
559
-
560
- /**
561
- * Register a host's `rlsStrategy`/`userLookup` against an externally-constructed
562
- * `Database` handle (e.g. one the embedded host built from its own driver and
563
- * injected, rather than via `createDb`). Lets the same WeakMap-backed lookups
564
- * work for injected handles. Standalone never calls this (it uses `createDb`).
565
- */
566
- export function registerDbBinding(
567
- db: Database,
568
- binding: { rlsStrategy?: RlsStrategy; userLookup?: UserLookup },
569
- ): void {
570
- dbBindings.set(db as unknown as object, {
571
- rlsStrategy: binding.rlsStrategy ?? "force",
572
- ...(binding.userLookup ? { userLookup: binding.userLookup } : {}),
573
- });
574
- }
575
-
576
440
  // ---------------------------------------------------------------------------
577
441
  // Durable host export
578
442
  // ---------------------------------------------------------------------------
@@ -1048,187 +912,6 @@ export async function getHostExportConsumerStatus(
1048
912
  };
1049
913
  }
1050
914
 
1051
- export async function setRlsContext(db: Database, context: RlsContext): Promise<void> {
1052
- // Fail loud on an empty/blank account id: a "" account would set an RLS GUC
1053
- // that matches no tenant row, silently returning zero rows from every scoped
1054
- // read (a phantom "not found" / "no active subscription"). An RLS context with
1055
- // no account is always a bug at the call site, never a valid query scope.
1056
- if (typeof context.accountId !== "string" || context.accountId.trim() === "") {
1057
- throw new Error("setRlsContext: a non-empty accountId is required to establish an RLS context");
1058
- }
1059
- await db.execute(sql`select set_config('opengeni.account_id', ${context.accountId}, true)`);
1060
- await db.execute(
1061
- sql`select set_config('opengeni.workspace_id', ${context.workspaceId ?? ""}, true)`,
1062
- );
1063
- await db.execute(sql`select set_config('opengeni.sandbox_recovery_protocol_v2', '1', true)`);
1064
- }
1065
-
1066
- export async function withRlsContext<T>(
1067
- db: Database,
1068
- context: RlsContext,
1069
- fn: (db: Database) => Promise<T>,
1070
- transactionConfig?: PgTransactionConfig,
1071
- ): Promise<T> {
1072
- return await db.transaction(async (tx) => {
1073
- const scoped = tx as unknown as Database;
1074
- await setRlsContext(scoped, context);
1075
- // Defense-in-depth: read the LOCAL GUC back on THIS backend BEFORE running
1076
- // the scoped query. The set_config and this read share one db.transaction,
1077
- // which a transaction pooler pins to a single backend — so a mismatch here
1078
- // means the context was genuinely lost (a torn transaction / pooler backend
1079
- // swap), not normal operation. Without this guard such an event runs the
1080
- // scoped read with an empty account_id and returns zero RLS-visible rows,
1081
- // manufacturing a phantom "no active subscription" from a credential that is
1082
- // in fact active. Convert that silent false into a loud, root-cause-bearing
1083
- // error so the caller can retry rather than permanently mis-decide.
1084
- const applied = await tx.execute<{
1085
- account_id: string | null;
1086
- workspace_id: string | null;
1087
- }>(
1088
- sql`select
1089
- current_setting('opengeni.account_id', true) as account_id,
1090
- current_setting('opengeni.workspace_id', true) as workspace_id`,
1091
- );
1092
- const appliedAccountId = applied[0]?.account_id ?? "";
1093
- const expectedWorkspaceId = context.workspaceId ?? "";
1094
- const appliedWorkspaceId = applied[0]?.workspace_id ?? "";
1095
- if (appliedAccountId !== context.accountId) {
1096
- throw new Error(
1097
- `RLS context not applied on the active backend: expected account ${context.accountId}, got "${appliedAccountId}"`,
1098
- );
1099
- }
1100
- if (appliedWorkspaceId !== expectedWorkspaceId) {
1101
- throw new Error(
1102
- `RLS context not applied on the active backend: expected workspace "${expectedWorkspaceId}", got "${appliedWorkspaceId}"`,
1103
- );
1104
- }
1105
- return await fn(scoped);
1106
- }, transactionConfig);
1107
- }
1108
-
1109
- /**
1110
- * Run one bounded database operation on a transaction-pinned backend.
1111
- *
1112
- * Callers that also have an application deadline should check their abort
1113
- * signal before returning from `fn`; throwing there rolls the transaction back
1114
- * even when the application deadline won a surrounding Promise race.
1115
- */
1116
- export async function withDatabaseStatementTimeout<T>(
1117
- db: Database,
1118
- timeoutMs: number,
1119
- fn: (db: Database) => Promise<T>,
1120
- ): Promise<T> {
1121
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1122
- throw new Error("withDatabaseStatementTimeout requires a positive timeout");
1123
- }
1124
- const boundedTimeoutMs = Math.max(1, Math.floor(timeoutMs));
1125
- return await db.transaction(async (tx) => {
1126
- const scoped = tx as unknown as Database;
1127
- await scoped.execute(
1128
- sql`select set_config('statement_timeout', ${`${boundedTimeoutMs}ms`}, true)`,
1129
- );
1130
- return await fn(scoped);
1131
- });
1132
- }
1133
-
1134
- export async function rlsContextForWorkspace(
1135
- db: Database,
1136
- workspaceId: string,
1137
- ): Promise<RlsContext> {
1138
- const [row] = await db
1139
- .select({ accountId: schema.workspaces.accountId })
1140
- .from(schema.workspaces)
1141
- .where(eq(schema.workspaces.id, workspaceId))
1142
- .limit(1);
1143
- if (!row) {
1144
- throw new Error(`Workspace not found: ${workspaceId}`);
1145
- }
1146
- return { accountId: row.accountId, workspaceId };
1147
- }
1148
-
1149
- export async function withWorkspaceRls<T>(
1150
- db: Database,
1151
- workspaceId: string,
1152
- fn: (db: Database) => Promise<T>,
1153
- ): Promise<T> {
1154
- return await withRlsContext(db, await rlsContextForWorkspace(db, workspaceId), fn);
1155
- }
1156
-
1157
- async function retryWorkspacePersistence<T>(
1158
- db: Database,
1159
- workspaceId: string,
1160
- options: IdempotentPersistenceTransactionOptions,
1161
- fn: (db: Database) => Promise<T>,
1162
- ): Promise<T> {
1163
- return await runIdempotentPersistenceTransaction(options, async () => {
1164
- return await withWorkspaceRls(db, workspaceId, fn);
1165
- });
1166
- }
1167
-
1168
- async function retryRlsPersistence<T>(
1169
- db: Database,
1170
- context: RlsContext,
1171
- options: IdempotentPersistenceTransactionOptions,
1172
- fn: (db: Database) => Promise<T>,
1173
- ): Promise<T> {
1174
- return await runIdempotentPersistenceTransaction(options, async () => {
1175
- return await withRlsContext(db, context, fn);
1176
- });
1177
- }
1178
-
1179
- /**
1180
- * Personal workspace data needs both tenant and authenticated-principal GUCs.
1181
- * `session_pins` uses this helper so FORCE RLS rejects another member's rows
1182
- * even if a future query accidentally omits its explicit subject predicate.
1183
- */
1184
- export async function withWorkspaceSubjectRls<T>(
1185
- db: Database,
1186
- workspaceId: string,
1187
- subjectId: string,
1188
- fn: (db: Database) => Promise<T>,
1189
- transactionConfig?: PgTransactionConfig,
1190
- ): Promise<T> {
1191
- if (!subjectId.trim()) {
1192
- throw new Error("withWorkspaceSubjectRls: a non-empty subjectId is required");
1193
- }
1194
- const context = await rlsContextForWorkspace(db, workspaceId);
1195
- return await withRlsContext(
1196
- db,
1197
- context,
1198
- async (scopedDb) => {
1199
- await setSubjectRlsContext(scopedDb, subjectId);
1200
- return await fn(scopedDb);
1201
- },
1202
- transactionConfig,
1203
- );
1204
- }
1205
-
1206
- /** Apply and verify actor-private RLS on an already transaction-pinned handle. */
1207
- export async function setSubjectRlsContext(db: Database, subjectId: string): Promise<void> {
1208
- if (!subjectId.trim()) {
1209
- throw new Error("setSubjectRlsContext: a non-empty subjectId is required");
1210
- }
1211
- await db.execute(sql`select set_config('opengeni.subject_id', ${subjectId}, true)`);
1212
- const applied = await db.execute<{ subject_id: string | null }>(
1213
- sql`select current_setting('opengeni.subject_id', true) as subject_id`,
1214
- );
1215
- if ((applied[0]?.subject_id ?? "") !== subjectId) {
1216
- throw new Error("Authenticated subject RLS context was not applied on the active backend");
1217
- }
1218
- }
1219
-
1220
- export async function withWorkspaceUsageLock<T>(
1221
- db: Database,
1222
- workspaceId: string,
1223
- fn: (db: Database) => Promise<T>,
1224
- ): Promise<T> {
1225
- const context = await rlsContextForWorkspace(db, workspaceId);
1226
- return await withRlsContext(db, context, async (scopedDb) => {
1227
- await scopedDb.execute(sql`select pg_advisory_xact_lock(hashtext(${`usage:${workspaceId}`}))`);
1228
- return await fn(scopedDb);
1229
- });
1230
- }
1231
-
1232
915
  export type DocumentIndexAuthority = {
1233
916
  authorityKind: DocumentAuthorityKind;
1234
917
  authorityWorkspaceId: string | null;
@@ -1278,14 +961,6 @@ export async function resolveDocumentIndexAuthority(
1278
961
  };
1279
962
  }
1280
963
 
1281
- export async function withAccountRls<T>(
1282
- db: Database,
1283
- accountId: string,
1284
- fn: (db: Database) => Promise<T>,
1285
- ): Promise<T> {
1286
- return await withRlsContext(db, { accountId, workspaceId: null }, fn);
1287
- }
1288
-
1289
964
  export const allWorkspacePermissions: Permission[] = [
1290
965
  "workspace:read",
1291
966
  "workspace:admin",
@@ -2096,7 +1771,7 @@ export async function removeWorkspaceMember(
2096
1771
  * unknown users are deferred.
2097
1772
  */
2098
1773
  export async function getManagedUserByEmail(db: Database, email: string): Promise<string | null> {
2099
- const binding = dbBindings.get(db as unknown as object);
1774
+ const binding = dbBindingFor(db);
2100
1775
  if (binding?.userLookup) {
2101
1776
  return await binding.userLookup(db, email);
2102
1777
  }
@@ -3499,6 +3174,7 @@ export type WorkspaceStateMemoryRecord = {
3499
3174
  export type CreateSocialConnectionInput = {
3500
3175
  accountId: string;
3501
3176
  workspaceId: string;
3177
+ subjectId?: string | null;
3502
3178
  provider: SocialProvider;
3503
3179
  accountHandle: string;
3504
3180
  accountName?: string | null;
@@ -3514,6 +3190,7 @@ export type CreateSocialConnectionInput = {
3514
3190
  export type UpsertSocialOAuthConnectionInput = {
3515
3191
  accountId: string;
3516
3192
  workspaceId: string;
3193
+ subjectId?: string | null;
3517
3194
  provider: SocialProvider;
3518
3195
  accountHandle: string;
3519
3196
  accountName?: string | null;
@@ -3526,6 +3203,7 @@ export type UpsertSocialOAuthConnectionInput = {
3526
3203
  export type UpdateSocialConnectionCredentialInput = {
3527
3204
  workspaceId: string;
3528
3205
  connectionId: string;
3206
+ subjectId?: string | null;
3529
3207
  credentialEncrypted?: string | null;
3530
3208
  status?: SocialConnectionStatus;
3531
3209
  tokenMetadata?: Record<string, unknown>;
@@ -3534,6 +3212,7 @@ export type UpdateSocialConnectionCredentialInput = {
3534
3212
  export type CreateSocialPostInput = {
3535
3213
  accountId: string;
3536
3214
  workspaceId: string;
3215
+ subjectId?: string | null;
3537
3216
  connectionId: string;
3538
3217
  externalPostId?: string | null;
3539
3218
  url?: string | null;
@@ -3672,22 +3351,6 @@ export type SlackBotDeleteOperation = {
3672
3351
  updatedAt: Date;
3673
3352
  };
3674
3353
 
3675
- export type ConnectionCredentialForBroker = {
3676
- id: string;
3677
- accountId: string;
3678
- workspaceId: string;
3679
- subjectId: string | null;
3680
- providerDomain: string;
3681
- kind: ConnectionKind;
3682
- status: ConnectionStatus;
3683
- credential: Record<string, unknown>;
3684
- grantedScopes: string[];
3685
- expiresAt: Date | null;
3686
- lastRefreshAt: Date | null;
3687
- version: number;
3688
- metadata: Record<string, unknown>;
3689
- };
3690
-
3691
3354
  export type IntegrationOAuthClientForUse = {
3692
3355
  id: string;
3693
3356
  issuer: string;
@@ -9262,11 +8925,13 @@ export async function createSocialConnection(
9262
8925
  db,
9263
8926
  { accountId: input.accountId, workspaceId: input.workspaceId },
9264
8927
  async (scopedDb) => {
8928
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9265
8929
  const [row] = await scopedDb
9266
8930
  .insert(schema.socialConnections)
9267
8931
  .values({
9268
8932
  accountId: input.accountId,
9269
8933
  workspaceId: input.workspaceId,
8934
+ subjectId: input.subjectId ?? null,
9270
8935
  provider: input.provider,
9271
8936
  accountHandle: input.accountHandle,
9272
8937
  accountName: input.accountName ?? null,
@@ -9300,11 +8965,13 @@ export async function upsertSocialOAuthConnection(
9300
8965
  db,
9301
8966
  { accountId: input.accountId, workspaceId: input.workspaceId },
9302
8967
  async (scopedDb) => {
8968
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9303
8969
  const [row] = await scopedDb
9304
8970
  .insert(schema.socialConnections)
9305
8971
  .values({
9306
8972
  accountId: input.accountId,
9307
8973
  workspaceId: input.workspaceId,
8974
+ subjectId: input.subjectId ?? null,
9308
8975
  provider: input.provider,
9309
8976
  accountHandle: input.accountHandle,
9310
8977
  accountName: input.accountName ?? null,
@@ -9315,12 +8982,21 @@ export async function upsertSocialOAuthConnection(
9315
8982
  tokenMetadata: input.tokenMetadata ?? {},
9316
8983
  })
9317
8984
  .onConflictDoUpdate({
9318
- target: [
9319
- schema.socialConnections.workspaceId,
9320
- schema.socialConnections.provider,
9321
- schema.socialConnections.accountHandle,
9322
- ],
8985
+ target: input.subjectId
8986
+ ? [
8987
+ schema.socialConnections.workspaceId,
8988
+ schema.socialConnections.subjectId,
8989
+ schema.socialConnections.provider,
8990
+ ]
8991
+ : [
8992
+ schema.socialConnections.workspaceId,
8993
+ schema.socialConnections.provider,
8994
+ schema.socialConnections.accountHandle,
8995
+ ],
8996
+ targetWhere: input.subjectId ? sql`subject_id is not null` : sql`subject_id is null`,
9323
8997
  set: {
8998
+ accountId: input.accountId,
8999
+ accountHandle: input.accountHandle,
9324
9000
  accountName: input.accountName ?? null,
9325
9001
  externalAccountId: input.externalAccountId ?? null,
9326
9002
  status: "connected",
@@ -9348,8 +9024,9 @@ export async function loadSocialConnectionCredential(
9348
9024
  db: Database,
9349
9025
  workspaceId: string,
9350
9026
  connectionId: string,
9027
+ subjectId?: string | null,
9351
9028
  ): Promise<{ connection: SocialConnection; credentialEncrypted: string | null } | null> {
9352
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9029
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9353
9030
  const [row] = await scopedDb
9354
9031
  .select()
9355
9032
  .from(schema.socialConnections)
@@ -9357,6 +9034,7 @@ export async function loadSocialConnectionCredential(
9357
9034
  and(
9358
9035
  eq(schema.socialConnections.workspaceId, workspaceId),
9359
9036
  eq(schema.socialConnections.id, connectionId),
9037
+ socialConnectionSubjectVisibility(subjectId),
9360
9038
  ),
9361
9039
  )
9362
9040
  .limit(1);
@@ -9371,26 +9049,32 @@ export async function updateSocialConnectionCredential(
9371
9049
  db: Database,
9372
9050
  input: UpdateSocialConnectionCredentialInput,
9373
9051
  ): Promise<SocialConnection | null> {
9374
- return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
9375
- const [row] = await scopedDb
9376
- .update(schema.socialConnections)
9377
- .set({
9378
- ...(input.credentialEncrypted !== undefined
9379
- ? { credentialEncrypted: input.credentialEncrypted }
9380
- : {}),
9381
- ...(input.status !== undefined ? { status: input.status } : {}),
9382
- ...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
9383
- updatedAt: new Date(),
9384
- })
9385
- .where(
9386
- and(
9387
- eq(schema.socialConnections.workspaceId, input.workspaceId),
9388
- eq(schema.socialConnections.id, input.connectionId),
9389
- ),
9390
- )
9391
- .returning();
9392
- return row ? mapSocialConnection(row) : null;
9393
- });
9052
+ return await withSocialConnectionSubjectRls(
9053
+ db,
9054
+ input.workspaceId,
9055
+ input.subjectId,
9056
+ async (scopedDb) => {
9057
+ const [row] = await scopedDb
9058
+ .update(schema.socialConnections)
9059
+ .set({
9060
+ ...(input.credentialEncrypted !== undefined
9061
+ ? { credentialEncrypted: input.credentialEncrypted }
9062
+ : {}),
9063
+ ...(input.status !== undefined ? { status: input.status } : {}),
9064
+ ...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
9065
+ updatedAt: new Date(),
9066
+ })
9067
+ .where(
9068
+ and(
9069
+ eq(schema.socialConnections.workspaceId, input.workspaceId),
9070
+ eq(schema.socialConnections.id, input.connectionId),
9071
+ socialConnectionSubjectVisibility(input.subjectId),
9072
+ ),
9073
+ )
9074
+ .returning();
9075
+ return row ? mapSocialConnection(row) : null;
9076
+ },
9077
+ );
9394
9078
  }
9395
9079
 
9396
9080
  // List/get never select credential_encrypted (same posture as the broker
@@ -9403,12 +9087,18 @@ export async function listSocialConnections(
9403
9087
  db: Database,
9404
9088
  workspaceId: string,
9405
9089
  limit = 100,
9090
+ subjectId?: string | null,
9406
9091
  ): Promise<SocialConnection[]> {
9407
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9092
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9408
9093
  const rows = await scopedDb
9409
9094
  .select(socialConnectionPublicColumns)
9410
9095
  .from(schema.socialConnections)
9411
- .where(eq(schema.socialConnections.workspaceId, workspaceId))
9096
+ .where(
9097
+ and(
9098
+ eq(schema.socialConnections.workspaceId, workspaceId),
9099
+ socialConnectionSubjectVisibility(subjectId),
9100
+ ),
9101
+ )
9412
9102
  .orderBy(desc(schema.socialConnections.createdAt))
9413
9103
  .limit(limit);
9414
9104
  return rows.map(mapSocialConnection);
@@ -9419,8 +9109,9 @@ export async function getSocialConnection(
9419
9109
  db: Database,
9420
9110
  workspaceId: string,
9421
9111
  connectionId: string,
9112
+ subjectId?: string | null,
9422
9113
  ): Promise<SocialConnection | null> {
9423
- return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
9114
+ return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
9424
9115
  const [row] = await scopedDb
9425
9116
  .select(socialConnectionPublicColumns)
9426
9117
  .from(schema.socialConnections)
@@ -9428,6 +9119,7 @@ export async function getSocialConnection(
9428
9119
  and(
9429
9120
  eq(schema.socialConnections.workspaceId, workspaceId),
9430
9121
  eq(schema.socialConnections.id, connectionId),
9122
+ socialConnectionSubjectVisibility(subjectId),
9431
9123
  ),
9432
9124
  )
9433
9125
  .limit(1);
@@ -9439,8 +9131,9 @@ export async function requireSocialConnection(
9439
9131
  db: Database,
9440
9132
  workspaceId: string,
9441
9133
  connectionId: string,
9134
+ subjectId?: string | null,
9442
9135
  ): Promise<SocialConnection> {
9443
- const connection = await getSocialConnection(db, workspaceId, connectionId);
9136
+ const connection = await getSocialConnection(db, workspaceId, connectionId, subjectId);
9444
9137
  if (!connection) {
9445
9138
  throw new Error(`Social connection not found: ${connectionId}`);
9446
9139
  }
@@ -9455,10 +9148,12 @@ export async function createSocialPost(
9455
9148
  db,
9456
9149
  { accountId: input.accountId, workspaceId: input.workspaceId },
9457
9150
  async (scopedDb) => {
9151
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9458
9152
  const connection = await requireSocialConnection(
9459
9153
  scopedDb,
9460
9154
  input.workspaceId,
9461
9155
  input.connectionId,
9156
+ input.subjectId,
9462
9157
  );
9463
9158
  const [row] = await scopedDb
9464
9159
  .insert(schema.socialPosts)
@@ -9495,6 +9190,7 @@ export async function recordSyncedSocialPosts(
9495
9190
  accountId: string;
9496
9191
  workspaceId: string;
9497
9192
  connectionId: string;
9193
+ subjectId?: string | null;
9498
9194
  posts: Array<{
9499
9195
  externalPostId: string;
9500
9196
  url?: string | null;
@@ -9513,10 +9209,12 @@ export async function recordSyncedSocialPosts(
9513
9209
  db,
9514
9210
  { accountId: input.accountId, workspaceId: input.workspaceId },
9515
9211
  async (scopedDb) => {
9212
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
9516
9213
  const connection = await requireSocialConnection(
9517
9214
  scopedDb,
9518
9215
  input.workspaceId,
9519
9216
  input.connectionId,
9217
+ input.subjectId,
9520
9218
  );
9521
9219
  const rows = await scopedDb
9522
9220
  .insert(schema.socialPosts)
@@ -9552,6 +9250,7 @@ export async function listSocialPosts(
9552
9250
  db: Database,
9553
9251
  options: {
9554
9252
  workspaceId: string;
9253
+ subjectId?: string | null;
9555
9254
  connectionIds?: string[];
9556
9255
  since?: Date;
9557
9256
  limit?: number;
@@ -9565,15 +9264,20 @@ export async function listSocialPosts(
9565
9264
  conditions.push(gte(schema.socialPosts.publishedAt, options.since));
9566
9265
  }
9567
9266
  const limit = options.limit ?? 100;
9568
- return await withWorkspaceRls(db, options.workspaceId, async (scopedDb) => {
9569
- const rows = await scopedDb
9570
- .select()
9571
- .from(schema.socialPosts)
9572
- .where(and(...conditions))
9573
- .orderBy(desc(schema.socialPosts.publishedAt))
9574
- .limit(limit);
9575
- return rows.map(mapSocialPost);
9576
- });
9267
+ return await withSocialConnectionSubjectRls(
9268
+ db,
9269
+ options.workspaceId,
9270
+ options.subjectId,
9271
+ async (scopedDb) => {
9272
+ const rows = await scopedDb
9273
+ .select()
9274
+ .from(schema.socialPosts)
9275
+ .where(and(...conditions))
9276
+ .orderBy(desc(schema.socialPosts.publishedAt))
9277
+ .limit(limit);
9278
+ return rows.map(mapSocialPost);
9279
+ },
9280
+ );
9577
9281
  }
9578
9282
 
9579
9283
  export async function createScheduledTask(
@@ -11646,27 +11350,6 @@ export type WorkspaceEnvironmentForRun = VariableSetForRun;
11646
11350
  // `getCodexCredentialStatus` returns metadata only (never the secret column).
11647
11351
  // ---------------------------------------------------------------------------
11648
11352
 
11649
- export type CodexCredentialTokens = {
11650
- accessToken: string;
11651
- refreshToken: string;
11652
- idToken: string;
11653
- };
11654
-
11655
- export type CodexCredentialForRun = {
11656
- id: string; // row id — for compare-and-set writes (P1-c)
11657
- version: number; // optimistic-concurrency version loaded with this snapshot
11658
- workspaceId: string;
11659
- tokens: CodexCredentialTokens; // decrypted — never logged, never returned by a route
11660
- chatgptAccountId: string | null;
11661
- scopes: string | null;
11662
- planType: string | null;
11663
- isFedramp: boolean;
11664
- expiresAt: Date | null;
11665
- lastRefreshAt: Date | null;
11666
- status: string;
11667
- lastError: string | null;
11668
- };
11669
-
11670
11353
  /**
11671
11354
  * Login / rotation write (multi-account P1). Caller passes the PRE-encrypted
11672
11355
  * credential blob. Keyed on the composite partial index (workspace, chatgpt
@@ -15111,16 +14794,6 @@ export async function completeCodexResetRedemption(
15111
14794
  }
15112
14795
 
15113
14796
  /** The P2 usage-cache snapshot written by the refreshing usage wrapper. */
15114
- export type CodexAccountUsageSnapshot = {
15115
- primaryUsedPercent?: number | null;
15116
- primaryResetAt?: Date | null;
15117
- secondaryUsedPercent?: number | null;
15118
- secondaryResetAt?: Date | null;
15119
- /** Present only when the quota body parsed successfully. */
15120
- checkedAt?: Date;
15121
- resetCreditAvailableCount?: number | null;
15122
- resetCreditsCheckedAt?: Date | null;
15123
- };
15124
14797
 
15125
14798
  /**
15126
14799
  * Cache-write for P2 quota bars: persist the five plaintext usage columns on a
@@ -37554,6 +37227,7 @@ export async function claimSessionWorkForAttempt(
37554
37227
  "delivered",
37555
37228
  "acknowledged",
37556
37229
  "settled",
37230
+ "rejected_stale",
37557
37231
  ]),
37558
37232
  isNull(schema.sessionTurnAttempts.quiescedAt),
37559
37233
  ),
@@ -38481,28 +38155,6 @@ export async function claimSessionWorkForAttempt(
38481
38155
  turn: mapSessionTurnForExecution(internalTurn),
38482
38156
  };
38483
38157
  }
38484
- const predecessorAttemptId = queuedSteerReplacementAttemptId(queuedTurn.metadata);
38485
- if (predecessorAttemptId) {
38486
- const [predecessor] = await tx
38487
- .select({ quiescedAt: schema.sessionTurnAttempts.quiescedAt })
38488
- .from(schema.sessionTurnAttempts)
38489
- .where(
38490
- and(
38491
- eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
38492
- eq(schema.sessionTurnAttempts.sessionId, sessionId),
38493
- eq(schema.sessionTurnAttempts.id, predecessorAttemptId),
38494
- ),
38495
- )
38496
- .limit(1);
38497
- if (!predecessor) {
38498
- throw new SessionControlInvariantError(
38499
- `Queued Steer ${id} points to missing predecessor attempt ${predecessorAttemptId}`,
38500
- );
38501
- }
38502
- if (!predecessor.quiescedAt) {
38503
- return { action: "unclaimed", reason: "control-pending" };
38504
- }
38505
- }
38506
38158
  // The database guard makes this function the only supported
38507
38159
  // queued-to-running transition. Raw or stale claimers cannot bypass the
38508
38160
  // generation/active-pointer transaction.
@@ -39136,6 +38788,14 @@ export async function settleSessionAttemptInterruptions(
39136
38788
  interruptions.map((interruption) => interruption.id),
39137
38789
  ),
39138
38790
  );
38791
+ await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
38792
+ accountId: session.accountId,
38793
+ workspaceId,
38794
+ sessionId,
38795
+ temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
38796
+ reason: "attempt_interruption_rejected_stale",
38797
+ controlRequested: true,
38798
+ });
39139
38799
  return {
39140
38800
  action: effectiveControl.state === "paused" ? "paused" : "continue",
39141
38801
  events: [],
@@ -39327,6 +38987,14 @@ export async function settleSessionAttemptInterruptions(
39327
38987
  interruptions.map((interruption) => interruption.id),
39328
38988
  ),
39329
38989
  );
38990
+ await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
38991
+ accountId: session.accountId,
38992
+ workspaceId,
38993
+ sessionId,
38994
+ temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
38995
+ reason: "attempt_interruption_settled",
38996
+ controlRequested: true,
38997
+ });
39330
38998
  return {
39331
38999
  action: effectiveControl.state === "paused" ? "paused" : "continue",
39332
39000
  events: [...closedTools.events, ...eventRows.map(mapEvent)],
@@ -39355,6 +39023,50 @@ export type SessionWorkPeek =
39355
39023
  | { kind: "cancellation-wait"; attemptId: string }
39356
39024
  | { kind: "idle" };
39357
39025
 
39026
+ /**
39027
+ * Return the oldest exact attempt whose logical interruption settled but whose
39028
+ * physical quiescence receipt is still missing. This deliberately searches all
39029
+ * attempts: a provider-recovery race can create a newer generation before the
39030
+ * predecessor receipt is reconciled, and looking only at the newest attempt
39031
+ * would strand the replacement forever.
39032
+ */
39033
+ async function nextSessionAttemptAwaitingQuiescence(
39034
+ db: Database,
39035
+ workspaceId: string,
39036
+ sessionId: string,
39037
+ ): Promise<{
39038
+ attemptId: string;
39039
+ } | null> {
39040
+ const [row] = await db
39041
+ .select({
39042
+ attemptId: schema.sessionTurnAttempts.id,
39043
+ })
39044
+ .from(schema.sessionTurnAttempts)
39045
+ .innerJoin(
39046
+ schema.sessionAttemptInterruptions,
39047
+ and(
39048
+ eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
39049
+ eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
39050
+ eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
39051
+ ),
39052
+ )
39053
+ .where(
39054
+ and(
39055
+ eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
39056
+ eq(schema.sessionTurnAttempts.sessionId, sessionId),
39057
+ eq(schema.sessionTurnAttempts.state, "closed"),
39058
+ isNull(schema.sessionTurnAttempts.quiescedAt),
39059
+ inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
39060
+ ),
39061
+ )
39062
+ .orderBy(
39063
+ asc(schema.sessionAttemptInterruptions.requestedAt),
39064
+ asc(schema.sessionAttemptInterruptions.id),
39065
+ )
39066
+ .limit(1);
39067
+ return row ?? null;
39068
+ }
39069
+
39358
39070
  async function latestSessionAttemptInterruption(
39359
39071
  db: Database,
39360
39072
  workspaceId: string,
@@ -39402,6 +39114,43 @@ async function latestSessionAttemptInterruption(
39402
39114
  : null;
39403
39115
  }
39404
39116
 
39117
+ async function queuedSteerHasUnquiescedPredecessor(
39118
+ db: Database,
39119
+ workspaceId: string,
39120
+ sessionId: string,
39121
+ predecessorAttemptIds: string[],
39122
+ ): Promise<boolean> {
39123
+ if (predecessorAttemptIds.length === 0) return false;
39124
+ const [row] = await db
39125
+ .select({ attemptId: schema.sessionTurnAttempts.id })
39126
+ .from(schema.sessionTurnAttempts)
39127
+ .innerJoin(
39128
+ schema.sessionAttemptInterruptions,
39129
+ and(
39130
+ eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
39131
+ eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
39132
+ eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
39133
+ ),
39134
+ )
39135
+ .where(
39136
+ and(
39137
+ eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
39138
+ eq(schema.sessionTurnAttempts.sessionId, sessionId),
39139
+ inArray(schema.sessionTurnAttempts.id, predecessorAttemptIds),
39140
+ isNull(schema.sessionTurnAttempts.quiescedAt),
39141
+ inArray(schema.sessionAttemptInterruptions.state, [
39142
+ "pending",
39143
+ "delivered",
39144
+ "acknowledged",
39145
+ "settled",
39146
+ "rejected_stale",
39147
+ ]),
39148
+ ),
39149
+ )
39150
+ .limit(1);
39151
+ return row !== undefined;
39152
+ }
39153
+
39405
39154
  /** Read durable session state without reserving a turn-worker slot or mutating it. */
39406
39155
  export async function peekSessionWork(
39407
39156
  db: Database,
@@ -39445,19 +39194,15 @@ export async function peekSessionWork(
39445
39194
  }
39446
39195
  if (effectiveControl.state !== "active") return { kind: "idle" };
39447
39196
 
39448
- const latestInterruption = await latestSessionAttemptInterruption(
39197
+ const awaitingQuiescence = await nextSessionAttemptAwaitingQuiescence(
39449
39198
  scopedDb,
39450
39199
  workspaceId,
39451
39200
  sessionId,
39452
39201
  );
39453
- if (
39454
- latestInterruption &&
39455
- latestInterruption.quiescedAt === null &&
39456
- latestInterruption.interruptionState === "settled"
39457
- ) {
39202
+ if (awaitingQuiescence) {
39458
39203
  return {
39459
39204
  kind: "cancellation-wait",
39460
- attemptId: latestInterruption.attemptId,
39205
+ attemptId: awaitingQuiescence.attemptId,
39461
39206
  };
39462
39207
  }
39463
39208
 
@@ -41315,6 +41060,7 @@ export type RequestSessionTurnRecoveryInput = {
41315
41060
  attemptId: string;
41316
41061
  reason: string;
41317
41062
  detail?: Record<string, unknown>;
41063
+ providerRecoveryCount?: number;
41318
41064
  fromStatuses?: SessionTurnStatus[];
41319
41065
  providerArtifactInvalidation?: {
41320
41066
  codexCredentialId: string;
@@ -41368,6 +41114,25 @@ export async function requestSessionTurnRecovery(
41368
41114
  { workspaceControl: locks.control ?? undefined },
41369
41115
  );
41370
41116
  const turnStatus = (turn?.status as SessionTurnStatus | undefined) ?? null;
41117
+ const [pendingInterruption] = attempt
41118
+ ? await tx
41119
+ .select({ id: schema.sessionAttemptInterruptions.id })
41120
+ .from(schema.sessionAttemptInterruptions)
41121
+ .where(
41122
+ and(
41123
+ eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
41124
+ eq(schema.sessionAttemptInterruptions.sessionId, input.sessionId),
41125
+ eq(schema.sessionAttemptInterruptions.attemptId, input.attemptId),
41126
+ inArray(schema.sessionAttemptInterruptions.state, [
41127
+ "pending",
41128
+ "delivered",
41129
+ "acknowledged",
41130
+ ]),
41131
+ ),
41132
+ )
41133
+ .limit(1)
41134
+ .for("update")
41135
+ : [];
41371
41136
  if (
41372
41137
  !locks.workspace ||
41373
41138
  !turn ||
@@ -41381,7 +41146,8 @@ export async function requestSessionTurnRecovery(
41381
41146
  effectiveControl.state !== "active" ||
41382
41147
  session.activeTurnId !== input.turnId ||
41383
41148
  !fromStatuses.includes(turnStatus as SessionTurnStatus) ||
41384
- turn.activeAttemptId !== input.attemptId
41149
+ turn.activeAttemptId !== input.attemptId ||
41150
+ pendingInterruption !== undefined
41385
41151
  ) {
41386
41152
  return {
41387
41153
  action: "stale" as const,
@@ -41392,6 +41158,12 @@ export async function requestSessionTurnRecovery(
41392
41158
  }
41393
41159
 
41394
41160
  const now = new Date();
41161
+ if (
41162
+ input.providerRecoveryCount !== undefined &&
41163
+ (!Number.isSafeInteger(input.providerRecoveryCount) || input.providerRecoveryCount <= 0)
41164
+ ) {
41165
+ throw new Error("providerRecoveryCount must be a positive safe integer");
41166
+ }
41395
41167
  let providerArtifactsInvalidated = 0;
41396
41168
  if (input.providerArtifactInvalidation) {
41397
41169
  const invalidatedHistory = await tx
@@ -41533,7 +41305,12 @@ export async function requestSessionTurnRecovery(
41533
41305
  cancelledBy: null,
41534
41306
  cancelReason: null,
41535
41307
  version: turn.version + 1,
41536
- metadata: metadataWithoutTurnDispatchAttempt(turn.metadata),
41308
+ metadata: {
41309
+ ...metadataWithoutTurnDispatchAttempt(turn.metadata),
41310
+ ...(input.providerRecoveryCount !== undefined
41311
+ ? { providerRecoveryCount: input.providerRecoveryCount }
41312
+ : {}),
41313
+ },
41537
41314
  updatedAt: now,
41538
41315
  })
41539
41316
  .where(
@@ -42142,12 +41919,25 @@ export async function getSessionQueueSnapshot(
42142
41919
  asc(schema.sessionSystemUpdates.createdAt),
42143
41920
  asc(schema.sessionSystemUpdates.id),
42144
41921
  );
41922
+ const items = rows.map(mapSessionTurn);
42145
41923
  const latestInterruption = await latestSessionAttemptInterruption(
42146
41924
  scopedDb,
42147
41925
  workspaceId,
42148
41926
  sessionId,
42149
41927
  );
42150
- const items = rows.map(mapSessionTurn);
41928
+ const queuedSteerPredecessorIds = items
41929
+ .map((turn) => queuedSteerReplacementAttemptId(turn.metadata))
41930
+ .filter((attemptId): attemptId is string => attemptId !== null);
41931
+ const stoppingPreviousAttempt =
41932
+ (latestInterruption !== null &&
41933
+ latestInterruption.interruptionState !== "rejected_stale" &&
41934
+ latestInterruption.quiescedAt === null) ||
41935
+ (await queuedSteerHasUnquiescedPredecessor(
41936
+ scopedDb,
41937
+ workspaceId,
41938
+ sessionId,
41939
+ queuedSteerPredecessorIds,
41940
+ ));
42151
41941
  const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
42152
41942
  const hasPendingAgentSteer = pendingInputs.some(
42153
41943
  (update) => update.kind === "agent_steer_instruction",
@@ -42159,10 +41949,7 @@ export async function getSessionQueueSnapshot(
42159
41949
  version: session.queueVersion,
42160
41950
  effectiveControl: serializeEffectiveSessionControl(effectiveControl),
42161
41951
  activePersonalConnections,
42162
- stoppingPreviousAttempt:
42163
- latestInterruption !== null &&
42164
- latestInterruption.interruptionState !== "rejected_stale" &&
42165
- latestInterruption.quiescedAt === null,
41952
+ stoppingPreviousAttempt,
42166
41953
  items,
42167
41954
  pendingInputs: pendingInputs.map((update) => {
42168
41955
  const canonical = mapSessionSystemUpdate(update);
@@ -42201,7 +41988,9 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
42201
41988
  ) {
42202
41989
  return attemptId;
42203
41990
  }
42204
- throw new SessionControlInvariantError("Queued Steer has malformed predecessor metadata");
41991
+ // The interruption ledger is authoritative. Historical or malformed display
41992
+ // metadata must not make the read-only queue projection fail.
41993
+ return null;
42205
41994
  }
42206
41995
 
42207
41996
  async function enqueueFailedChildOutboxForTurnTx(
@@ -42418,6 +42207,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
42418
42207
  temporalWorkflowId: string;
42419
42208
  reason: string;
42420
42209
  notBefore?: Date;
42210
+ controlRequested?: boolean;
42421
42211
  },
42422
42212
  ): Promise<number> {
42423
42213
  const now = new Date();
@@ -42429,6 +42219,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
42429
42219
  workspaceId: input.workspaceId,
42430
42220
  sessionId: input.sessionId,
42431
42221
  temporalWorkflowId: input.temporalWorkflowId,
42222
+ controlRevision: input.controlRequested ? 1 : 0,
42432
42223
  reason: input.reason,
42433
42224
  nextAttemptAt,
42434
42225
  })
@@ -42437,6 +42228,9 @@ export async function enqueueSessionWorkflowWakeInTransaction(
42437
42228
  set: {
42438
42229
  temporalWorkflowId: input.temporalWorkflowId,
42439
42230
  wakeRevision: sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`,
42231
+ controlRevision: input.controlRequested
42232
+ ? sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`
42233
+ : sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
42440
42234
  reason: input.reason,
42441
42235
  attempts: 0,
42442
42236
  // Coalescing a delayed retry must never postpone an already-due wake
@@ -42462,6 +42256,7 @@ export async function enqueueSessionWorkflowWake(
42462
42256
  temporalWorkflowId: string;
42463
42257
  reason: string;
42464
42258
  notBefore?: Date;
42259
+ controlRequested?: boolean;
42465
42260
  },
42466
42261
  ): Promise<number> {
42467
42262
  return await withRlsContext(
@@ -42485,6 +42280,7 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
42485
42280
  temporalWorkflowId: string;
42486
42281
  reason: string;
42487
42282
  notBefore?: Date;
42283
+ controlRequested?: boolean;
42488
42284
  },
42489
42285
  ): Promise<number | null> {
42490
42286
  return await withRlsContext(
@@ -44686,6 +44482,7 @@ function mapSocialConnection(
44686
44482
  accountHandle: row.accountHandle,
44687
44483
  accountName: row.accountName,
44688
44484
  externalAccountId: row.externalAccountId,
44485
+ ownership: row.subjectId ? "personal" : "workspace",
44689
44486
  status: row.status as SocialConnectionStatus,
44690
44487
  scopes: row.scopes,
44691
44488
  credentialRef: row.credentialRef,
@@ -44696,6 +44493,25 @@ function mapSocialConnection(
44696
44493
  };
44697
44494
  }
44698
44495
 
44496
+ function socialConnectionSubjectVisibility(subjectId?: string | null): SQL {
44497
+ return subjectId
44498
+ ? or(
44499
+ isNull(schema.socialConnections.subjectId),
44500
+ eq(schema.socialConnections.subjectId, subjectId),
44501
+ )!
44502
+ : isNull(schema.socialConnections.subjectId);
44503
+ }
44504
+ async function withSocialConnectionSubjectRls<T>(
44505
+ db: Database,
44506
+ workspaceId: string,
44507
+ subjectId: string | null | undefined,
44508
+ fn: (db: Database) => Promise<T>,
44509
+ ): Promise<T> {
44510
+ return subjectId
44511
+ ? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
44512
+ : await withWorkspaceRls(db, workspaceId, fn);
44513
+ }
44514
+
44699
44515
  function mapApiKey(row: typeof schema.apiKeys.$inferSelect): ApiKey {
44700
44516
  return {
44701
44517
  id: row.id,
@@ -44904,11 +44720,118 @@ function shortHash(value: string): string {
44904
44720
  return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 7);
44905
44721
  }
44906
44722
 
44907
- // Shared, refreshing, id-addressed Codex token resolver + the per-account usage
44908
- // wrapper (P2). Placed at the END so every accessor it orchestrates
44909
- // (loadCodexCredentialForRun / recordCodexTokenRefresh / setCodexCredentialStatus /
44910
- // recordCodexAccountUsage) is already initialized when its default-deps bag
44911
- // evaluates under the index↔resolver module cycle.
44912
- export * from "./codex-token-resolver";
44913
- export * from "./connection-token-resolver";
44723
+ // The resolver modules are cycle-free orchestration leaves. The root barrel is
44724
+ // the composition point that supplies their existing persistence accessors while
44725
+ // retaining the historical public builder/fetcher signatures.
44726
+ function codexAuthDeps(): CodexAuthDeps {
44727
+ return {
44728
+ loadCredential: loadCodexCredentialForRun,
44729
+ recordRefresh: recordCodexTokenRefresh,
44730
+ setStatus: setCodexCredentialStatus,
44731
+ refresh: refreshCodexToken,
44732
+ encrypt: encryptEnvironmentValue,
44733
+ keyBytes: environmentsEncryptionKeyBytes,
44734
+ withRefreshLock: withCodexCredentialRefreshLock,
44735
+ recordUsage: recordCodexAccountUsage,
44736
+ };
44737
+ }
44738
+
44739
+ function connectionBrokerDeps(): ConnectionBrokerDeps {
44740
+ return {
44741
+ loadCredential: loadConnectionCredentialForBroker,
44742
+ recordRefresh: recordConnectionTokenRefresh,
44743
+ setStatus: setConnectionStatus,
44744
+ recordUsed: recordConnectionUsed,
44745
+ refresh: refreshOAuthConnectionCredentialCore,
44746
+ encrypt: encryptEnvironmentValue,
44747
+ keyBytes: environmentsEncryptionKeyBytes,
44748
+ now: () => new Date(),
44749
+ };
44750
+ }
44751
+
44752
+ export function buildCodexTokenResolver(
44753
+ db: Database,
44754
+ settings: Settings,
44755
+ workspaceId: string,
44756
+ credentialId: string,
44757
+ deps: CodexAuthDeps = codexAuthDeps(),
44758
+ ): ReturnType<typeof buildCodexTokenResolverCore> {
44759
+ return buildCodexTokenResolverCore(db, settings, workspaceId, credentialId, deps);
44760
+ }
44761
+
44762
+ export async function fetchCodexUsageForAccount(
44763
+ db: Database,
44764
+ settings: Settings,
44765
+ workspaceId: string,
44766
+ credentialId: string,
44767
+ fetchImpl: CodexFetch = fetch,
44768
+ ): ReturnType<typeof fetchCodexUsageForAccountCore> {
44769
+ return await fetchCodexUsageForAccountCore(
44770
+ db,
44771
+ settings,
44772
+ workspaceId,
44773
+ credentialId,
44774
+ codexAuthDeps(),
44775
+ fetchImpl,
44776
+ );
44777
+ }
44778
+
44779
+ export async function fetchCodexRateLimitResetCreditsForAccount(
44780
+ db: Database,
44781
+ settings: Settings,
44782
+ workspaceId: string,
44783
+ credentialId: string,
44784
+ fetchImpl: CodexFetch = fetch,
44785
+ ): ReturnType<typeof fetchCodexRateLimitResetCreditsForAccountCore> {
44786
+ return await fetchCodexRateLimitResetCreditsForAccountCore(
44787
+ db,
44788
+ settings,
44789
+ workspaceId,
44790
+ credentialId,
44791
+ codexAuthDeps(),
44792
+ fetchImpl,
44793
+ );
44794
+ }
44795
+
44796
+ export function buildConnectionTokenResolver(
44797
+ db: Database,
44798
+ settings: Settings,
44799
+ deps: ConnectionBrokerDeps = connectionBrokerDeps(),
44800
+ options: ConnectionTokenResolverOptions = {},
44801
+ ): ReturnType<typeof buildConnectionTokenResolverCore> {
44802
+ return buildConnectionTokenResolverCore(db, settings, deps, options);
44803
+ }
44804
+
44805
+ export {
44806
+ withCodexTokenDeadline,
44807
+ type CodexAccountUsageSnapshot,
44808
+ type CodexAuthDeps,
44809
+ type CodexCredentialForRun,
44810
+ type CodexCredentialTokens,
44811
+ type CodexRateLimitResetCreditsAccountResult,
44812
+ type CodexTokenDeadlineClock,
44813
+ type CodexTokenDeadlineOptions,
44814
+ } from "./codex-token-resolver";
44815
+
44816
+ export {
44817
+ buildHostConnectionTokenResolver,
44818
+ ConnectionRefreshHttpError,
44819
+ HostMcpCredentialBindingError,
44820
+ HostMcpCredentialScopeError,
44821
+ isPrivateAddress,
44822
+ normalizeBearerScheme,
44823
+ refreshOAuthConnectionCredential,
44824
+ type ConnectionBrokerDeps,
44825
+ type ConnectionCredentialForBroker,
44826
+ type ConnectionCredentialLookupInput,
44827
+ type ConnectionStatusGuard,
44828
+ type ConnectionTokenRefreshInput,
44829
+ type ConnectionTokenResolverOptions,
44830
+ type HostMcpCredentialResolverContext,
44831
+ type PermanentConnectionRefreshFailure,
44832
+ type RefreshTransportOptions,
44833
+ type ResolveConnectionCredentialInput,
44834
+ type ResolveConnectionCredentialResult,
44835
+ } from "./connection-token-resolver";
44836
+
44914
44837
  export * from "./workspace-artifacts";