@opengeni/db 1.3.0 → 1.4.2

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 (91) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/capability-reconciliation.js +2 -2
  3. package/dist/{chunk-7QWWTVYH.js → chunk-5ROHLEN6.js} +29 -4
  4. package/dist/chunk-5ROHLEN6.js.map +1 -0
  5. package/dist/{chunk-AQMH2ZEH.js → chunk-A4J7EECR.js} +2 -2
  6. package/dist/{chunk-VQX54TK3.js → chunk-B5C5SBSI.js} +3 -3
  7. package/dist/{chunk-OI6YZXTU.js → chunk-DKFEUGM4.js} +2 -2
  8. package/dist/{chunk-HYBQABBJ.js → chunk-HUKBIZGH.js} +2 -2
  9. package/dist/{chunk-LRSIODCQ.js → chunk-IT3EYY2C.js} +6 -4
  10. package/dist/{chunk-LRSIODCQ.js.map → chunk-IT3EYY2C.js.map} +1 -1
  11. package/dist/{chunk-DSNEIW5E.js → chunk-JL6TBB76.js} +2 -1
  12. package/dist/chunk-JL6TBB76.js.map +1 -0
  13. package/dist/{chunk-OEFNTGR3.js → chunk-JWBEE4CL.js} +4 -4
  14. package/dist/{chunk-BRVBLQAL.js → chunk-K5MTNKJU.js} +4 -4
  15. package/dist/{chunk-6UKNJ43T.js → chunk-NZ2SP55O.js} +2 -2
  16. package/dist/{chunk-3XHC5KC5.js → chunk-OKCBARX6.js} +2 -2
  17. package/dist/{chunk-PNWWNEDM.js → chunk-OV5AUIBA.js} +2 -2
  18. package/dist/{chunk-BID3CNN3.js → chunk-TODVDGMO.js} +36 -18
  19. package/dist/{chunk-BID3CNN3.js.map → chunk-TODVDGMO.js.map} +1 -1
  20. package/dist/{chunk-6S5T6FSP.js → chunk-W75DRYG3.js} +5 -5
  21. package/dist/{chunk-7S3O33NM.js → chunk-Y72DP3JF.js} +2 -2
  22. package/dist/codemode-approvals.d.ts +7 -1
  23. package/dist/connect-attempts.js +3 -3
  24. package/dist/connection-metadata.d.ts +416 -0
  25. package/dist/credit-balances.d.ts +25 -4
  26. package/dist/database.d.ts +7 -1
  27. package/dist/editable-artifact-durable-export.js +2 -2
  28. package/dist/editable-artifacts.js +3 -3
  29. package/dist/index.d.ts +51 -8
  30. package/dist/index.js +10826 -10129
  31. package/dist/index.js.map +1 -1
  32. package/dist/legacy-tool-action-reviews.d.ts +75 -0
  33. package/dist/managed-auth-session-sets.js +3 -3
  34. package/dist/mcp-operations.js +2 -2
  35. package/dist/modal-native-live-origin.d.ts +75 -0
  36. package/dist/organization-integration-policy.js +3 -3
  37. package/dist/organization-slack-bots.d.ts +1 -1
  38. package/dist/provision-roles.js +1 -1
  39. package/dist/retained-provider-commands.d.ts +21 -21
  40. package/dist/retained-provider-commands.js +6 -6
  41. package/dist/runtime-posture.d.ts +1 -1
  42. package/dist/sandbox-interaction-idle.d.ts +22 -0
  43. package/dist/schema.js +1 -1
  44. package/dist/session-background-commands.js +4 -4
  45. package/dist/session-command-output.js +5 -5
  46. package/dist/session-event-slices.js +3 -3
  47. package/dist/session-mcp-credential-rotation.js +5 -5
  48. package/dist/session-realtime-billing.d.ts +48 -0
  49. package/dist/session-realtime.d.ts +8 -2
  50. package/dist/session-tenancy.js +3 -3
  51. package/dist/video-generation.js +3 -3
  52. package/dist/voice-transcription-settlement.d.ts +36 -0
  53. package/dist/workspace-tool-defaults.js +4 -4
  54. package/drizzle/0623_voice_transcription_attribution.sql +128 -0
  55. package/drizzle/0624_azure_live_realtime_model.sql +30 -0
  56. package/drizzle/0625_transcription_billing_refusal_codes.sql +29 -0
  57. package/drizzle/0632_modal_native_live_origin.sql +239 -0
  58. package/package.json +7 -7
  59. package/src/codemode-approvals.ts +52 -22
  60. package/src/codemode-operations.ts +198 -181
  61. package/src/connection-metadata.ts +166 -0
  62. package/src/credit-balances.ts +58 -17
  63. package/src/database.ts +9 -0
  64. package/src/index.ts +540 -412
  65. package/src/insights-unified.ts +2 -1
  66. package/src/legacy-tool-action-reviews.ts +242 -0
  67. package/src/modal-native-live-origin.ts +148 -0
  68. package/src/organization-slack-bots.ts +1 -1
  69. package/src/provision-roles.ts +12 -0
  70. package/src/runtime-posture.ts +32 -3
  71. package/src/sandbox-interaction-idle.ts +91 -0
  72. package/src/schema.ts +1 -0
  73. package/src/session-realtime-billing.ts +135 -0
  74. package/src/session-realtime-ledger.ts +9 -4
  75. package/src/session-realtime.ts +20 -2
  76. package/src/tool-action-reviews.ts +129 -18
  77. package/src/transcription-recordings.ts +3 -1
  78. package/src/voice-transcription-settlement.ts +94 -0
  79. package/dist/chunk-7QWWTVYH.js.map +0 -1
  80. package/dist/chunk-DSNEIW5E.js.map +0 -1
  81. /package/dist/{chunk-AQMH2ZEH.js.map → chunk-A4J7EECR.js.map} +0 -0
  82. /package/dist/{chunk-VQX54TK3.js.map → chunk-B5C5SBSI.js.map} +0 -0
  83. /package/dist/{chunk-OI6YZXTU.js.map → chunk-DKFEUGM4.js.map} +0 -0
  84. /package/dist/{chunk-HYBQABBJ.js.map → chunk-HUKBIZGH.js.map} +0 -0
  85. /package/dist/{chunk-OEFNTGR3.js.map → chunk-JWBEE4CL.js.map} +0 -0
  86. /package/dist/{chunk-BRVBLQAL.js.map → chunk-K5MTNKJU.js.map} +0 -0
  87. /package/dist/{chunk-6UKNJ43T.js.map → chunk-NZ2SP55O.js.map} +0 -0
  88. /package/dist/{chunk-3XHC5KC5.js.map → chunk-OKCBARX6.js.map} +0 -0
  89. /package/dist/{chunk-PNWWNEDM.js.map → chunk-OV5AUIBA.js.map} +0 -0
  90. /package/dist/{chunk-6S5T6FSP.js.map → chunk-W75DRYG3.js.map} +0 -0
  91. /package/dist/{chunk-7S3O33NM.js.map → chunk-Y72DP3JF.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,10 +1,33 @@
1
1
  export * from "./organization-slack-bots";
2
+ export * from "./voice-transcription-settlement";
3
+ import {
4
+ connectionMetadataColumns,
5
+ connectionSubjectVisibility,
6
+ getConnectionMetadata,
7
+ listConnectionsMetadata,
8
+ mapConnectionMetadata,
9
+ withConnectionSubjectRls,
10
+ type ConnectionMetadataWithVerification,
11
+ } from "./connection-metadata";
12
+ export {
13
+ getConnectionMetadata,
14
+ listConnectionsMetadata,
15
+ type ConnectionMetadataWithVerification,
16
+ } from "./connection-metadata";
2
17
  export { readCreditPromotionPolicy } from "./credit-promotion-policy";
3
- import { getBillingBalance, getSpendableCreditBalance, planCreditDebit } from "./credit-balances";
18
+ import {
19
+ getBillingBalance,
20
+ getSpendableCreditBalance,
21
+ planCreditDebit,
22
+ planPostUseCreditAllocations,
23
+ type CreditUsage,
24
+ } from "./credit-balances";
4
25
  export {
5
26
  getBillingBalance,
6
27
  getSpendableCreditBalance,
7
28
  spendableCreditMicros,
29
+ VOICE_CREDIT_USAGE,
30
+ type CreditUsage,
8
31
  } from "./credit-balances";
9
32
  import {
10
33
  lockTurnAttemptWriteFenceTx,
@@ -37,6 +60,12 @@ import {
37
60
  inboxOrigin,
38
61
  } from "./inbox-execution-context";
39
62
  import { parentOutboxAuthorityTx } from "./child-outbox-authority";
63
+ export {
64
+ lockLiveNativeOriginalOriginTx,
65
+ type ModalNativeLiveOriginScope,
66
+ type ModalNativeLiveOriginProjection,
67
+ type ModalNativeLiveOriginResult,
68
+ } from "./modal-native-live-origin";
40
69
  export { SubscriptionAccountChangedError } from "./subscription-account-conflict";
41
70
  import type { GoalAdmissionPausedReason } from "@opengeni/contracts";
42
71
  import {
@@ -802,6 +831,7 @@ export * from "./session-queue-commands";
802
831
  export * from "./session-realtime";
803
832
  export * from "./session-realtime-context";
804
833
  export * from "./session-realtime-ledger";
834
+ export * from "./session-realtime-billing";
805
835
  export * from "./new-session-drafts";
806
836
  export * from "./workspace-instruction-policies";
807
837
  export * from "./company-profile";
@@ -5862,6 +5892,11 @@ export async function applyCreditDebitAfterUse(
5862
5892
  idempotencyKey: string;
5863
5893
  metadata?: Record<string, unknown>;
5864
5894
  occurredAt?: Date;
5895
+ /**
5896
+ * What the charge paid for. When set, grants covering it pay first (oldest
5897
+ * first) and general credit takes the rest. Omitted: general credit only.
5898
+ */
5899
+ usage?: CreditUsage;
5865
5900
  },
5866
5901
  ): Promise<{ balance: BillingBalance; debitedMicros: number }> {
5867
5902
  if (!Number.isSafeInteger(input.amountMicros) || input.amountMicros <= 0) {
@@ -5871,6 +5906,17 @@ export async function applyCreditDebitAfterUse(
5871
5906
  db,
5872
5907
  { accountId: input.accountId, workspaceId: input.workspaceId ?? null },
5873
5908
  async (scopedDb) => {
5909
+ let allocations: { grantEntryId: string; amountMicros: number }[] = [];
5910
+ if (input.usage) {
5911
+ // Same account lock as the model debit, so grant remainders are not
5912
+ // allocated twice by concurrent charges.
5913
+ await scopedDb.execute(sql`select pg_advisory_xact_lock(hashtext(${input.accountId}))`);
5914
+ allocations = planPostUseCreditAllocations(
5915
+ await getBillingBalance(scopedDb, input.accountId),
5916
+ input.amountMicros,
5917
+ input.usage,
5918
+ );
5919
+ }
5874
5920
  const inserted = await scopedDb
5875
5921
  .insert(schema.creditLedgerEntries)
5876
5922
  .values({
@@ -5911,6 +5957,14 @@ export async function applyCreditDebitAfterUse(
5911
5957
  "post-use credit debit idempotency key conflicts with a different charge",
5912
5958
  );
5913
5959
  }
5960
+ } else if (allocations.length) {
5961
+ await scopedDb.insert(schema.creditDebitAllocations).values(
5962
+ allocations.map((allocation) => ({
5963
+ ...allocation,
5964
+ accountId: input.accountId,
5965
+ debitEntryId: inserted[0]!.id,
5966
+ })),
5967
+ );
5914
5968
  }
5915
5969
  return {
5916
5970
  balance: await getBillingBalance(scopedDb, input.accountId),
@@ -6545,12 +6599,6 @@ export class PersonalGitHubRepositorySelectionUnavailableError extends Error {
6545
6599
  }
6546
6600
  }
6547
6601
 
6548
- /** Server-owned verification facts; public schemas expose them read-only and nullable. */
6549
- export type ConnectionMetadataWithVerification = ConnectionMetadata & {
6550
- verifiedInstallAt: string | null;
6551
- verifiedInstallVersion: number | null;
6552
- };
6553
-
6554
6602
  export type SlackBotPostOperation = {
6555
6603
  id: string;
6556
6604
  accountId: string;
@@ -10631,37 +10679,6 @@ export function mcpServerIdForCapability(
10631
10679
  return `cap-${body}-${shortHash(capabilityId)}`;
10632
10680
  }
10633
10681
 
10634
- const connectionMetadataColumns = {
10635
- id: schema.connections.id,
10636
- authorityId: schema.connections.authorityId,
10637
- authorityGeneration: schema.connections.authorityGeneration,
10638
- accountId: schema.connections.accountId,
10639
- workspaceId: schema.connections.workspaceId,
10640
- subjectId: schema.connections.subjectId,
10641
- providerDomain: schema.connections.providerDomain,
10642
- kind: schema.connections.kind,
10643
- status: schema.connections.status,
10644
- grantedScopes: schema.connections.grantedScopes,
10645
- expiresAt: schema.connections.expiresAt,
10646
- lastRefreshAt: schema.connections.lastRefreshAt,
10647
- lastUsedAt: schema.connections.lastUsedAt,
10648
- lastError: schema.connections.lastError,
10649
- version: schema.connections.version,
10650
- verifiedInstallAt: schema.connections.verifiedInstallAt,
10651
- verifiedInstallVersion: schema.connections.verifiedInstallVersion,
10652
- metadata: schema.connections.metadata,
10653
- createdBySubjectId: schema.connections.createdBySubjectId,
10654
- updatedBySubjectId: schema.connections.updatedBySubjectId,
10655
- createdAt: schema.connections.createdAt,
10656
- updatedAt: schema.connections.updatedAt,
10657
- };
10658
-
10659
- function connectionSubjectVisibility(subjectId?: string | null): SQL {
10660
- return subjectId
10661
- ? or(isNull(schema.connections.subjectId), eq(schema.connections.subjectId, subjectId))!
10662
- : isNull(schema.connections.subjectId);
10663
- }
10664
-
10665
10682
  function connectionExactSubject(subjectId?: string | null): SQL {
10666
10683
  return subjectId
10667
10684
  ? eq(schema.connections.subjectId, subjectId)
@@ -10683,17 +10700,6 @@ function personalSlackCanonicalConnectionOrder(): SQL[] {
10683
10700
  ];
10684
10701
  }
10685
10702
 
10686
- async function withConnectionSubjectRls<T>(
10687
- db: Database,
10688
- workspaceId: string,
10689
- subjectId: string | null | undefined,
10690
- fn: (db: Database) => Promise<T>,
10691
- ): Promise<T> {
10692
- return subjectId
10693
- ? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
10694
- : await withWorkspaceRls(db, workspaceId, fn);
10695
- }
10696
-
10697
10703
  const connectionAccessPolicyColumns = {
10698
10704
  allowedModelIds: schema.connections.allowedModelIds,
10699
10705
  allowedWorkspaceIds: schema.connections.allowedWorkspaceIds,
@@ -11414,51 +11420,6 @@ export async function persistProviderOAuthConnection(
11414
11420
  );
11415
11421
  }
11416
11422
 
11417
- export async function listConnectionsMetadata(
11418
- db: Database,
11419
- workspaceId: string,
11420
- subjectId?: string | null,
11421
- ): Promise<ConnectionMetadataWithVerification[]> {
11422
- return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
11423
- const rows = await scopedDb
11424
- .select(connectionMetadataColumns)
11425
- .from(schema.connections)
11426
- .where(
11427
- and(
11428
- eq(schema.connections.workspaceId, workspaceId),
11429
- connectionSubjectVisibility(subjectId),
11430
- ),
11431
- )
11432
- // Legacy rows can share created_at. UUID DESC is the immutable stable
11433
- // tie-breaker, so every caller that intentionally selects the first row
11434
- // collapses duplicates in the same documented direction.
11435
- .orderBy(desc(schema.connections.createdAt), desc(schema.connections.id));
11436
- return rows.map(mapConnectionMetadata);
11437
- });
11438
- }
11439
-
11440
- export async function getConnectionMetadata(
11441
- db: Database,
11442
- workspaceId: string,
11443
- connectionId: string,
11444
- subjectId?: string | null,
11445
- ): Promise<ConnectionMetadataWithVerification | null> {
11446
- return await withConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
11447
- const [row] = await scopedDb
11448
- .select(connectionMetadataColumns)
11449
- .from(schema.connections)
11450
- .where(
11451
- and(
11452
- eq(schema.connections.workspaceId, workspaceId),
11453
- eq(schema.connections.id, connectionId),
11454
- connectionSubjectVisibility(subjectId),
11455
- ),
11456
- )
11457
- .limit(1);
11458
- return row ? mapConnectionMetadata(row) : null;
11459
- });
11460
- }
11461
-
11462
11423
  function personalGitHubRepositorySelectionStateFromDatabase(
11463
11424
  value: unknown,
11464
11425
  ): PersonalGitHubRepositorySelectionState | null {
@@ -33380,6 +33341,53 @@ async function priorConnectorActionPreparation(
33380
33341
  };
33381
33342
  }
33382
33343
 
33344
+ /** Exact rows of one attempt whose canonical write prefix a ledger write takes. */
33345
+ type ConnectorActionLedgerPrefix = (scope: {
33346
+ sessionId: string;
33347
+ turnId: string;
33348
+ attemptId: string;
33349
+ }) => Promise<void>;
33350
+
33351
+ /**
33352
+ * One idempotent connector-ledger transaction. Before its first ledger write it
33353
+ * takes the canonical session event-write prefix (workspace KEY SHARE, session,
33354
+ * event cursor, exact turn, exact attempt): a ledger row's attempt FK otherwise
33355
+ * KEY SHAREs the attempt outside that order while parallel tool calls of the
33356
+ * same attempt append events and settle receipts. Reads before the prefix take
33357
+ * no row locks. The whole transaction retries on 40P01/40001; it never contains
33358
+ * a provider effect, so a retry cannot replay one.
33359
+ */
33360
+ async function runConnectorActionLedgerTransaction<T>(
33361
+ db: Database,
33362
+ input: { accountId: string; workspaceId: string; stage: string; correlationId: string },
33363
+ fn: (tx: Database, lockPrefix: ConnectorActionLedgerPrefix) => Promise<T>,
33364
+ ): Promise<T> {
33365
+ return await retryRlsPersistence(
33366
+ db,
33367
+ { accountId: input.accountId, workspaceId: input.workspaceId },
33368
+ { stage: input.stage, correlationId: input.correlationId },
33369
+ async (scopedDb) =>
33370
+ await scopedDb.transaction(async (txRaw) => {
33371
+ const tx = txRaw as unknown as Database;
33372
+ let locked: string | null = null;
33373
+ return await fn(tx, async (scope) => {
33374
+ const key = `${scope.sessionId}:${scope.turnId}:${scope.attemptId}`;
33375
+ if (locked === key) return;
33376
+ if (locked) throw new Error("connector ledger transaction spans two attempts");
33377
+ await lockSessionEventWriteRows(tx, {
33378
+ workspaceId: input.workspaceId,
33379
+ controlLock: "none",
33380
+ sessionLock: "key_share",
33381
+ sessionIds: [scope.sessionId],
33382
+ turnIds: [scope.turnId],
33383
+ attemptIds: [scope.attemptId],
33384
+ });
33385
+ locked = key;
33386
+ });
33387
+ }),
33388
+ );
33389
+ }
33390
+
33383
33391
  export async function prepareConnectorActionApproval(
33384
33392
  db: Database,
33385
33393
  identity: ConnectorActionAttemptIdentity,
@@ -33389,67 +33397,66 @@ export async function prepareConnectorActionApproval(
33389
33397
  if (!normalized.connectionId) {
33390
33398
  return { managed: false, decision: "unmanaged" };
33391
33399
  }
33392
- return await withRlsContext(
33400
+ return await runConnectorActionLedgerTransaction(
33393
33401
  db,
33394
- { accountId: identity.accountId, workspaceId: identity.workspaceId },
33395
- async (scopedDb) =>
33396
- await scopedDb.transaction(async (tx) => {
33397
- const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
33398
- const prior = await priorConnectorActionPreparation(
33399
- tx as unknown as Database,
33400
- identity,
33401
- normalized,
33402
- );
33403
- if (prior) return prior;
33404
- const resolved = resolveConnectorActionPolicy(snapshot, {
33405
- connectionId: normalized.connectionId!,
33406
- serverId: normalized.serverId,
33407
- toolName: normalized.toolName,
33408
- actionName: normalized.policyActionSelector,
33409
- ...(normalized.defaultDecision !== undefined
33410
- ? { defaultDecision: normalized.defaultDecision }
33411
- : {}),
33412
- });
33413
- if (!resolved.managed) return { managed: false, decision: "unmanaged" } as const;
33414
- const durable = durableConnectorActionInvocation(
33415
- identity,
33416
- { ...normalized, connectionId: normalized.connectionId! },
33417
- resolved,
33418
- );
33419
- const decision = connectorActionPolicyDecision(resolved);
33420
- recordToolApproval(decision, resolved.source);
33421
- if (decision === "allow") {
33422
- return {
33423
- managed: true,
33424
- decision,
33425
- actionFingerprint: durable.actionFingerprint,
33426
- } as const;
33427
- }
33428
- const { row, inserted } = await insertConnectorActionRequest(tx as unknown as Database, {
33429
- identity,
33430
- invocation: durable,
33431
- resolved,
33432
- status: decision === "ask" ? "pending" : "blocked",
33433
- });
33434
- if (inserted) {
33435
- await insertConnectorActionAudit(tx as unknown as Database, {
33436
- row,
33437
- action:
33438
- decision === "ask"
33439
- ? "connector.action.approval_requested"
33440
- : "connector.action.blocked",
33441
- subjectId: identity.initiator.subjectId,
33442
- extra: { outcome: decision },
33443
- });
33444
- }
33402
+ {
33403
+ accountId: identity.accountId,
33404
+ workspaceId: identity.workspaceId,
33405
+ stage: "connector_action_prepare",
33406
+ correlationId: normalized.approvalId,
33407
+ },
33408
+ async (tx, lockPrefix) => {
33409
+ const snapshot = await connectorActionAttemptSnapshot(tx, identity);
33410
+ const prior = await priorConnectorActionPreparation(tx, identity, normalized);
33411
+ if (prior) return prior;
33412
+ const resolved = resolveConnectorActionPolicy(snapshot, {
33413
+ connectionId: normalized.connectionId!,
33414
+ serverId: normalized.serverId,
33415
+ toolName: normalized.toolName,
33416
+ actionName: normalized.policyActionSelector,
33417
+ ...(normalized.defaultDecision !== undefined
33418
+ ? { defaultDecision: normalized.defaultDecision }
33419
+ : {}),
33420
+ });
33421
+ if (!resolved.managed) return { managed: false, decision: "unmanaged" } as const;
33422
+ const durable = durableConnectorActionInvocation(
33423
+ identity,
33424
+ { ...normalized, connectionId: normalized.connectionId! },
33425
+ resolved,
33426
+ );
33427
+ const decision = connectorActionPolicyDecision(resolved);
33428
+ recordToolApproval(decision, resolved.source);
33429
+ if (decision === "allow") {
33445
33430
  return {
33446
33431
  managed: true,
33447
33432
  decision,
33448
- approvalStatus: row.status,
33449
- requestId: row.id,
33450
- actionFingerprint: row.actionFingerprint,
33433
+ actionFingerprint: durable.actionFingerprint,
33451
33434
  } as const;
33452
- }),
33435
+ }
33436
+ await lockPrefix(identity);
33437
+ const { row, inserted } = await insertConnectorActionRequest(tx, {
33438
+ identity,
33439
+ invocation: durable,
33440
+ resolved,
33441
+ status: decision === "ask" ? "pending" : "blocked",
33442
+ });
33443
+ if (inserted) {
33444
+ await insertConnectorActionAudit(tx, {
33445
+ row,
33446
+ action:
33447
+ decision === "ask" ? "connector.action.approval_requested" : "connector.action.blocked",
33448
+ subjectId: identity.initiator.subjectId,
33449
+ extra: { outcome: decision },
33450
+ });
33451
+ }
33452
+ return {
33453
+ managed: true,
33454
+ decision,
33455
+ approvalStatus: row.status,
33456
+ requestId: row.id,
33457
+ actionFingerprint: row.actionFingerprint,
33458
+ } as const;
33459
+ },
33453
33460
  );
33454
33461
  }
33455
33462
 
@@ -33503,13 +33510,28 @@ export async function beginConnectorActionExecution(
33503
33510
  if (!normalized.connectionId) {
33504
33511
  return { allowed: true, managed: false };
33505
33512
  }
33506
- return await withRlsContext(
33513
+ const resolveFrom = (snapshot: ConnectorActionPolicySnapshotEntry[]) =>
33514
+ resolveConnectorActionPolicy(snapshot, {
33515
+ connectionId: normalized.connectionId!,
33516
+ serverId: normalized.serverId,
33517
+ toolName: normalized.toolName,
33518
+ actionName: normalized.policyActionSelector,
33519
+ ...(normalized.defaultDecision !== undefined
33520
+ ? { defaultDecision: normalized.defaultDecision }
33521
+ : {}),
33522
+ });
33523
+ return await runConnectorActionLedgerTransaction(
33507
33524
  db,
33508
- { accountId: identity.accountId, workspaceId: identity.workspaceId },
33509
- async (scopedDb) =>
33510
- await scopedDb.transaction(async (tx) => {
33511
- const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
33512
- const [existing] = await tx
33525
+ {
33526
+ accountId: identity.accountId,
33527
+ workspaceId: identity.workspaceId,
33528
+ stage: "connector_action_begin",
33529
+ correlationId: normalized.approvalId,
33530
+ },
33531
+ async (tx, lockPrefix) => {
33532
+ const snapshot = await connectorActionAttemptSnapshot(tx, identity);
33533
+ const existingRequest = () =>
33534
+ tx
33513
33535
  .select()
33514
33536
  .from(schema.connectorActionRequests)
33515
33537
  .where(
@@ -33519,73 +33541,89 @@ export async function beginConnectorActionExecution(
33519
33541
  eq(schema.connectorActionRequests.turnId, identity.turnId),
33520
33542
  eq(schema.connectorActionRequests.approvalId, normalized.approvalId),
33521
33543
  ),
33522
- )
33523
- .for("update")
33524
- .limit(1);
33525
- if (existing && !connectorActionRequestMatchesLogicalCall(existing, identity, normalized)) {
33526
- throw new Error("connector action approval id conflicts with different immutable inputs");
33527
- }
33528
- let row = existing;
33529
- let inserted = false;
33530
- if (!row) {
33531
- const resolved = resolveConnectorActionPolicy(snapshot, {
33532
- connectionId: normalized.connectionId!,
33533
- serverId: normalized.serverId,
33534
- toolName: normalized.toolName,
33535
- actionName: normalized.policyActionSelector,
33536
- ...(normalized.defaultDecision !== undefined
33537
- ? { defaultDecision: normalized.defaultDecision }
33538
- : {}),
33539
- });
33540
- if (!resolved.managed) return { allowed: true, managed: false } as const;
33541
- const durable = durableConnectorActionInvocation(
33542
- identity,
33543
- { ...normalized, connectionId: normalized.connectionId! },
33544
- resolved,
33545
33544
  );
33546
- const decision = connectorActionPolicyDecision(resolved);
33547
- const created = await insertConnectorActionRequest(tx as unknown as Database, {
33548
- identity,
33549
- invocation: durable,
33550
- resolved,
33551
- status: decision === "ask" ? "pending" : decision === "block" ? "blocked" : "executing",
33552
- });
33553
- row = created.row;
33554
- inserted = created.inserted;
33555
- if (inserted) {
33556
- await insertConnectorActionAudit(tx as unknown as Database, {
33557
- row,
33558
- action:
33559
- decision === "ask"
33560
- ? "connector.action.approval_requested"
33561
- : decision === "block"
33562
- ? "connector.action.blocked"
33563
- : "connector.action.execution_started",
33564
- subjectId: identity.initiator.subjectId,
33565
- extra: { outcome: decision === "allow" ? "started" : decision },
33566
- });
33567
- }
33545
+ const [unlocked] = await existingRequest().limit(1);
33546
+ if (!unlocked) {
33547
+ const resolved = resolveFrom(snapshot);
33548
+ if (!resolved.managed) return { allowed: true, managed: false } as const;
33549
+ // A recommended Allow with no explicit rule is ordinary tool use, not
33550
+ // a reviewed action: no ledger row, audit pair, or row lock. Every Ask,
33551
+ // Block, explicit rule, and connector write keeps its execute-once record.
33552
+ if (
33553
+ resolved.source === "default" &&
33554
+ connectorActionPolicyDecision(resolved) === "allow" &&
33555
+ normalized.approvalMode === "connector"
33556
+ ) {
33557
+ return { allowed: true, managed: false } as const;
33568
33558
  }
33569
- if (row.status === "approved") {
33570
- const [executing] = await tx
33571
- .update(schema.connectorActionRequests)
33572
- .set({
33573
- status: "executing",
33574
- executionAttemptId: identity.attemptId,
33575
- executionAttemptGeneration: identity.executionGeneration,
33576
- executionStartedAt: new Date(),
33577
- updatedAt: new Date(),
33578
- })
33579
- .where(eq(schema.connectorActionRequests.id, row.id))
33580
- .returning();
33581
- if (!executing) throw new Error("Approved connector action request disappeared");
33582
- row = executing;
33583
- await insertConnectorActionAudit(tx as unknown as Database, {
33559
+ }
33560
+ await lockPrefix(identity);
33561
+ const [existing] = await existingRequest().for("update").limit(1);
33562
+ if (existing && !connectorActionRequestMatchesLogicalCall(existing, identity, normalized)) {
33563
+ throw new Error("connector action approval id conflicts with different immutable inputs");
33564
+ }
33565
+ let row = existing;
33566
+ let inserted = false;
33567
+ if (!row) {
33568
+ const resolved = resolveFrom(snapshot);
33569
+ if (!resolved.managed) return { allowed: true, managed: false } as const;
33570
+ const decision = connectorActionPolicyDecision(resolved);
33571
+ const durable = durableConnectorActionInvocation(
33572
+ identity,
33573
+ { ...normalized, connectionId: normalized.connectionId! },
33574
+ resolved,
33575
+ );
33576
+ const created = await insertConnectorActionRequest(tx, {
33577
+ identity,
33578
+ invocation: durable,
33579
+ resolved,
33580
+ status: decision === "ask" ? "pending" : decision === "block" ? "blocked" : "executing",
33581
+ });
33582
+ row = created.row;
33583
+ inserted = created.inserted;
33584
+ if (inserted) {
33585
+ await insertConnectorActionAudit(tx, {
33584
33586
  row,
33585
- action: "connector.action.execution_started",
33587
+ action:
33588
+ decision === "ask"
33589
+ ? "connector.action.approval_requested"
33590
+ : decision === "block"
33591
+ ? "connector.action.blocked"
33592
+ : "connector.action.execution_started",
33586
33593
  subjectId: identity.initiator.subjectId,
33587
- extra: { outcome: "started" },
33594
+ extra: { outcome: decision === "allow" ? "started" : decision },
33588
33595
  });
33596
+ }
33597
+ }
33598
+ if (row.status === "approved") {
33599
+ const [executing] = await tx
33600
+ .update(schema.connectorActionRequests)
33601
+ .set({
33602
+ status: "executing",
33603
+ executionAttemptId: identity.attemptId,
33604
+ executionAttemptGeneration: identity.executionGeneration,
33605
+ executionStartedAt: new Date(),
33606
+ updatedAt: new Date(),
33607
+ })
33608
+ .where(eq(schema.connectorActionRequests.id, row.id))
33609
+ .returning();
33610
+ if (!executing) throw new Error("Approved connector action request disappeared");
33611
+ row = executing;
33612
+ await insertConnectorActionAudit(tx, {
33613
+ row,
33614
+ action: "connector.action.execution_started",
33615
+ subjectId: identity.initiator.subjectId,
33616
+ extra: { outcome: "started" },
33617
+ });
33618
+ return {
33619
+ allowed: true,
33620
+ managed: true,
33621
+ requestId: row.id,
33622
+ actionFingerprint: row.actionFingerprint,
33623
+ } as const;
33624
+ }
33625
+ if (row.status === "executing") {
33626
+ if (inserted) {
33589
33627
  return {
33590
33628
  allowed: true,
33591
33629
  managed: true,
@@ -33593,58 +33631,49 @@ export async function beginConnectorActionExecution(
33593
33631
  actionFingerprint: row.actionFingerprint,
33594
33632
  } as const;
33595
33633
  }
33596
- if (row.status === "executing") {
33597
- if (inserted) {
33598
- return {
33599
- allowed: true,
33600
- managed: true,
33601
- requestId: row.id,
33602
- actionFingerprint: row.actionFingerprint,
33603
- } as const;
33604
- }
33605
- const [uncertain] = await tx
33606
- .update(schema.connectorActionRequests)
33607
- .set({
33608
- status: "uncertain",
33609
- outcome: "retry_after_execution_started",
33610
- executionFinishedAt: new Date(),
33611
- updatedAt: new Date(),
33612
- })
33613
- .where(eq(schema.connectorActionRequests.id, row.id))
33614
- .returning();
33615
- if (!uncertain) throw new Error("Executing connector action request disappeared");
33616
- await insertConnectorActionAudit(tx as unknown as Database, {
33617
- row: uncertain,
33618
- action: "connector.action.execution_uncertain",
33619
- subjectId: identity.initiator.subjectId,
33620
- extra: { outcome: "retry_denied" },
33621
- });
33622
- return {
33623
- allowed: false,
33624
- managed: true,
33625
- reason: "uncertain_retry",
33626
- requestId: uncertain.id,
33627
- actionFingerprint: uncertain.actionFingerprint,
33628
- } as const;
33629
- }
33630
- const reason =
33631
- row.status === "pending"
33632
- ? "approval_required"
33633
- : row.status === "rejected"
33634
- ? "rejected"
33635
- : row.status === "blocked"
33636
- ? "blocked"
33637
- : row.status === "failed"
33638
- ? "not_executed"
33639
- : "already_executed";
33634
+ const [uncertain] = await tx
33635
+ .update(schema.connectorActionRequests)
33636
+ .set({
33637
+ status: "uncertain",
33638
+ outcome: "retry_after_execution_started",
33639
+ executionFinishedAt: new Date(),
33640
+ updatedAt: new Date(),
33641
+ })
33642
+ .where(eq(schema.connectorActionRequests.id, row.id))
33643
+ .returning();
33644
+ if (!uncertain) throw new Error("Executing connector action request disappeared");
33645
+ await insertConnectorActionAudit(tx, {
33646
+ row: uncertain,
33647
+ action: "connector.action.execution_uncertain",
33648
+ subjectId: identity.initiator.subjectId,
33649
+ extra: { outcome: "retry_denied" },
33650
+ });
33640
33651
  return {
33641
33652
  allowed: false,
33642
33653
  managed: true,
33643
- reason,
33644
- requestId: row.id,
33645
- actionFingerprint: row.actionFingerprint,
33654
+ reason: "uncertain_retry",
33655
+ requestId: uncertain.id,
33656
+ actionFingerprint: uncertain.actionFingerprint,
33646
33657
  } as const;
33647
- }),
33658
+ }
33659
+ const reason =
33660
+ row.status === "pending"
33661
+ ? "approval_required"
33662
+ : row.status === "rejected"
33663
+ ? "rejected"
33664
+ : row.status === "blocked"
33665
+ ? "blocked"
33666
+ : row.status === "failed"
33667
+ ? "not_executed"
33668
+ : "already_executed";
33669
+ return {
33670
+ allowed: false,
33671
+ managed: true,
33672
+ reason,
33673
+ requestId: row.id,
33674
+ actionFingerprint: row.actionFingerprint,
33675
+ } as const;
33676
+ },
33648
33677
  );
33649
33678
  }
33650
33679
 
@@ -33658,12 +33687,17 @@ export async function completeConnectorActionExecution(
33658
33687
  outcome: "completed" | "uncertain" | "not_executed";
33659
33688
  },
33660
33689
  ): Promise<void> {
33661
- await withRlsContext(
33690
+ await runConnectorActionLedgerTransaction(
33662
33691
  db,
33663
- { accountId: input.accountId, workspaceId: input.workspaceId },
33664
- async (scopedDb) =>
33665
- await scopedDb.transaction(async (tx) => {
33666
- const [existing] = await tx
33692
+ {
33693
+ accountId: input.accountId,
33694
+ workspaceId: input.workspaceId,
33695
+ stage: "connector_action_complete",
33696
+ correlationId: input.requestId,
33697
+ },
33698
+ async (tx, lockPrefix) => {
33699
+ const request = () =>
33700
+ tx
33667
33701
  .select()
33668
33702
  .from(schema.connectorActionRequests)
33669
33703
  .where(
@@ -33672,43 +33706,51 @@ export async function completeConnectorActionExecution(
33672
33706
  eq(schema.connectorActionRequests.id, input.requestId),
33673
33707
  eq(schema.connectorActionRequests.executionAttemptId, input.attemptId),
33674
33708
  ),
33675
- )
33676
- .for("update")
33677
- .limit(1);
33678
- if (!existing) throw new Error("Connector action request not found for completion");
33679
- // A not-executed completion is a terminal failure whose provider
33680
- // request never happened; it keeps the existing status vocabulary.
33681
- // Reaching the terminal status is idempotent even when a concurrent
33682
- // begin already settled the row with a different uncertain outcome
33683
- // (retry_after_execution_started).
33684
- const terminalStatus = input.outcome === "not_executed" ? "failed" : input.outcome;
33685
- if (existing.status === terminalStatus) return;
33686
- if (existing.status !== "executing") {
33687
- throw new Error(`Connector action request cannot complete from ${existing.status}`);
33688
- }
33689
- const [row] = await tx
33690
- .update(schema.connectorActionRequests)
33691
- .set({
33692
- status: terminalStatus,
33693
- outcome: input.outcome,
33694
- executionFinishedAt: new Date(),
33695
- updatedAt: new Date(),
33696
- })
33697
- .where(eq(schema.connectorActionRequests.id, existing.id))
33698
- .returning();
33699
- if (!row) throw new Error("Connector action request disappeared during completion");
33700
- await insertConnectorActionAudit(tx as unknown as Database, {
33701
- row,
33702
- action:
33703
- input.outcome === "completed"
33704
- ? "connector.action.execution_completed"
33705
- : input.outcome === "not_executed"
33706
- ? "connector.action.execution_not_executed"
33707
- : "connector.action.execution_uncertain",
33708
- subjectId: row.initiatorSubjectId,
33709
- extra: { outcome: input.outcome },
33710
- });
33711
- }),
33709
+ );
33710
+ const [located] = await request().limit(1);
33711
+ if (!located) throw new Error("Connector action request not found for completion");
33712
+ // Settlement is not admission: it takes the canonical prefix but never the
33713
+ // Pause/Steer write fence, so an interrupted attempt still settles.
33714
+ await lockPrefix({
33715
+ sessionId: located.sessionId,
33716
+ turnId: located.turnId,
33717
+ attemptId: input.attemptId,
33718
+ });
33719
+ const [existing] = await request().for("update").limit(1);
33720
+ if (!existing) throw new Error("Connector action request not found for completion");
33721
+ // A not-executed completion is a terminal failure whose provider
33722
+ // request never happened; it keeps the existing status vocabulary.
33723
+ // Reaching the terminal status is idempotent even when a concurrent
33724
+ // begin already settled the row with a different uncertain outcome
33725
+ // (retry_after_execution_started).
33726
+ const terminalStatus = input.outcome === "not_executed" ? "failed" : input.outcome;
33727
+ if (existing.status === terminalStatus) return;
33728
+ if (existing.status !== "executing") {
33729
+ throw new Error(`Connector action request cannot complete from ${existing.status}`);
33730
+ }
33731
+ const [row] = await tx
33732
+ .update(schema.connectorActionRequests)
33733
+ .set({
33734
+ status: terminalStatus,
33735
+ outcome: input.outcome,
33736
+ executionFinishedAt: new Date(),
33737
+ updatedAt: new Date(),
33738
+ })
33739
+ .where(eq(schema.connectorActionRequests.id, existing.id))
33740
+ .returning();
33741
+ if (!row) throw new Error("Connector action request disappeared during completion");
33742
+ await insertConnectorActionAudit(tx, {
33743
+ row,
33744
+ action:
33745
+ input.outcome === "completed"
33746
+ ? "connector.action.execution_completed"
33747
+ : input.outcome === "not_executed"
33748
+ ? "connector.action.execution_not_executed"
33749
+ : "connector.action.execution_uncertain",
33750
+ subjectId: row.initiatorSubjectId,
33751
+ extra: { outcome: input.outcome },
33752
+ });
33753
+ },
33712
33754
  );
33713
33755
  }
33714
33756
 
@@ -43174,9 +43216,12 @@ export async function appendSessionHistoryItems(
43174
43216
  if (input.items.length === 0) {
43175
43217
  return true;
43176
43218
  }
43177
- return await withRlsContext(
43219
+ // One idempotent transaction: a deadlock/serialization victim rolled back
43220
+ // entirely, so it retries as a unit; it contains no external effect.
43221
+ return await retryRlsPersistence(
43178
43222
  db,
43179
43223
  { accountId: input.accountId, workspaceId: input.workspaceId },
43224
+ { stage: "session_history_append", correlationId: input.expectedAttemptId },
43180
43225
  async (scopedDb) => {
43181
43226
  return await scopedDb.transaction(async (tx) => {
43182
43227
  const allowed = await lockTurnAttemptWriteFenceTx(tx, {
@@ -43490,9 +43535,12 @@ export async function recordPendingSessionToolCallResult(
43490
43535
  };
43491
43536
  },
43492
43537
  ): Promise<{ accepted: boolean; recorded: boolean }> {
43493
- return await withRlsContext(
43538
+ // One idempotent transaction: a deadlock/serialization victim rolled back
43539
+ // entirely, so it retries as a unit; it contains no external effect.
43540
+ return await retryRlsPersistence(
43494
43541
  db,
43495
43542
  { accountId: input.accountId, workspaceId: input.workspaceId },
43543
+ { stage: "session_pending_tool_call_result", correlationId: input.callId },
43496
43544
  async (scopedDb) =>
43497
43545
  await scopedDb.transaction(async (tx) => {
43498
43546
  const fence = await lockTurnAttemptWriteFenceTx(tx, {
@@ -55389,7 +55437,13 @@ export async function confirmDrainCold(
55389
55437
  /** The idle window named in contained commands' agent notice. */
55390
55438
  idleCommandContainmentMs?: number | undefined;
55391
55439
  },
55392
- ): Promise<{ wentCold: boolean; backgroundCommandEvents?: SessionEvent[] }> {
55440
+ ): Promise<{
55441
+ wentCold: boolean;
55442
+ /** The missing provider was an unpublished warming replacement: its loss
55443
+ * recorded no lost workspace (see `unpublishedWarmingLoss`). */
55444
+ unpublishedProviderLost?: boolean;
55445
+ backgroundCommandEvents?: SessionEvent[];
55446
+ }> {
55393
55447
  const backgroundCommandEvents: SessionEvent[] = [];
55394
55448
  // Settling a command appends its terminal session event and agent input, so
55395
55449
  // the cold commit runs inside the session activity gate.
@@ -55512,8 +55566,23 @@ export async function confirmDrainCold(
55512
55566
  const hasArchive = current.archive.status !== "none";
55513
55567
  const archiveComplete = hasCompleteWorkspaceArchive(row);
55514
55568
  const now = new Date().toISOString();
55569
+ // A warming replacement whose creator died (worker shutdown/crash)
55570
+ // before commitWarmingToWarm was never published: no holder attached
55571
+ // to it and no workspace mutation can be admitted on a non-warm lease.
55572
+ // The reaper's warming-death drain (c2) is the only way such a box
55573
+ // reaches here. Its disappearance therefore loses nothing beyond what
55574
+ // the pre-warming cold row already recorded; mirror failWarmingToCold
55575
+ // instead of minting a lost-workspace verdict that would block every
55576
+ // later turn of the session. A published box keeps the loss contract.
55577
+ const unpublishedWarmingLoss =
55578
+ input.providerMissingBeforeCapture === true &&
55579
+ current.provider.status === "creating" &&
55580
+ current.provider.instanceId !== null &&
55581
+ current.provider.instanceId === row.instance_id;
55582
+ const workspaceLost =
55583
+ input.providerMissingBeforeCapture === true && !unpublishedWarmingLoss;
55515
55584
  const lateArchiveCapture =
55516
- input.providerMissingBeforeCapture &&
55585
+ workspaceLost &&
55517
55586
  row.archive_capture_id !== null &&
55518
55587
  row.archive_capture_provider_request_id !== null &&
55519
55588
  row.archive_capture_generation !== null &&
@@ -55538,18 +55607,27 @@ export async function confirmDrainCold(
55538
55607
  ? "pending"
55539
55608
  : hasArchive
55540
55609
  ? "degraded"
55541
- : input.providerMissingBeforeCapture || freshPending
55610
+ : workspaceLost || freshPending
55542
55611
  ? "unrecoverable"
55543
55612
  : "not_required";
55544
55613
  const recovery: SandboxRecoveryState = {
55545
- provider: {
55546
- status: input.providerMissingBeforeCapture ? "missing" : "not_created",
55547
- instanceId: input.providerMissingBeforeCapture ? row.instance_id : null,
55548
- observedAt: now,
55549
- ...(input.providerMissingBeforeCapture
55550
- ? { diagnostic: "provider_not_found_before_workspace_capture" }
55551
- : {}),
55552
- },
55614
+ provider: unpublishedWarmingLoss
55615
+ ? {
55616
+ // Same shape failWarmingToCold records for a failed replacement:
55617
+ // proves nothing about the workspace, never a loss record.
55618
+ status: "not_created",
55619
+ instanceId: row.instance_id,
55620
+ observedAt: now,
55621
+ diagnostic: REPLACEMENT_FAILED_DIAGNOSTIC,
55622
+ }
55623
+ : {
55624
+ status: workspaceLost ? "missing" : "not_created",
55625
+ instanceId: workspaceLost ? row.instance_id : null,
55626
+ observedAt: now,
55627
+ ...(workspaceLost
55628
+ ? { diagnostic: "provider_not_found_before_workspace_capture" }
55629
+ : {}),
55630
+ },
55553
55631
  archive: current.archive,
55554
55632
  restore: {
55555
55633
  status: restoreStatus,
@@ -55589,9 +55667,7 @@ export async function confirmDrainCold(
55589
55667
  archiveOnlyResumeState(
55590
55668
  row,
55591
55669
  recovery,
55592
- input.providerMissingBeforeCapture
55593
- ? providerLossRecordForLostRow(row, "drain_probe", now)
55594
- : undefined,
55670
+ workspaceLost ? providerLossRecordForLostRow(row, "drain_probe", now) : undefined,
55595
55671
  ),
55596
55672
  )
55597
55673
  : null;
@@ -55640,7 +55716,7 @@ export async function confirmDrainCold(
55640
55716
  and archive_capture_id is not distinct from ${input.expectedCaptureId ?? null}::uuid
55641
55717
  returning id
55642
55718
  `);
55643
- if (rows.length > 0 && input.providerMissingBeforeCapture) {
55719
+ if (rows.length > 0 && workspaceLost) {
55644
55720
  const eventId = crypto.randomUUID();
55645
55721
  await tx.insert(schema.auditEvents).values(
55646
55722
  withLosslessContentWriteVersion(
@@ -55670,7 +55746,10 @@ export async function confirmDrainCold(
55670
55746
  if (rows.length > 0 && row.rotation_requested_at !== null) {
55671
55747
  await wakeSandboxLifecycleWaitersTx(tx, input);
55672
55748
  }
55673
- return { wentCold: rows.length > 0 };
55749
+ return {
55750
+ wentCold: rows.length > 0,
55751
+ ...(rows.length > 0 && unpublishedWarmingLoss ? { unpublishedProviderLost: true } : {}),
55752
+ };
55674
55753
  },
55675
55754
  );
55676
55755
  });
@@ -55775,6 +55854,65 @@ export type SandboxWorkspaceMutationAdmission = {
55775
55854
 
55776
55855
  export type SandboxWorkspaceMutationProviderOutcome = "resolved" | "rejected";
55777
55856
 
55857
+ /** Evidence from a committed exact admission settlement. It establishes the
55858
+ * physical provider outcome only; mutable output authority remains rejected. */
55859
+ export type SandboxWorkspaceMutationPhysicalSettlement = Readonly<{
55860
+ accountId: string;
55861
+ workspaceId: string;
55862
+ admission: Readonly<SandboxWorkspaceMutationAdmission>;
55863
+ operation: string;
55864
+ outcome: "resolved";
55865
+ }>;
55866
+
55867
+ /** Durable terminal truth of the retained process whose concurrent settlement
55868
+ * fenced a physically settled mutation's output. */
55869
+ export type SandboxRetainedProcessTerminalState = Readonly<{
55870
+ state: "exited" | "lost";
55871
+ exitCode: number | null;
55872
+ }>;
55873
+
55874
+ /** The exact provider outcome committed before its output failed an acceptance
55875
+ * fence. A generic admission fence or failed transaction carries no receipt.
55876
+ * This error never authorizes accepting output or replaying provider work. */
55877
+ export class SandboxWorkspaceMutationOutputRejectedError extends SandboxWorkspaceMutationFencedError {
55878
+ readonly name = "SandboxWorkspaceMutationOutputRejectedError";
55879
+ readonly physicalSettlement: SandboxWorkspaceMutationPhysicalSettlement;
55880
+ /** Non-null only when the fence was the retained process itself having
55881
+ * already reached a durable terminal state (for example the reaper settled
55882
+ * the exit between admission and settlement). The output stays rejected;
55883
+ * callers may report this stored terminal truth instead of failing. */
55884
+ readonly retainedProcessTerminal: SandboxRetainedProcessTerminalState | null;
55885
+
55886
+ constructor(
55887
+ code: SandboxWorkspaceMutationFencedError["code"],
55888
+ message: string,
55889
+ physicalSettlement: SandboxWorkspaceMutationPhysicalSettlement,
55890
+ retainedProcessTerminal: SandboxRetainedProcessTerminalState | null = null,
55891
+ ) {
55892
+ super(code, message);
55893
+ this.physicalSettlement = Object.freeze({
55894
+ ...physicalSettlement,
55895
+ admission: Object.freeze({ ...physicalSettlement.admission }),
55896
+ });
55897
+ this.retainedProcessTerminal = retainedProcessTerminal
55898
+ ? Object.freeze({
55899
+ state: retainedProcessTerminal.state,
55900
+ exitCode: retainedProcessTerminal.exitCode,
55901
+ })
55902
+ : null;
55903
+ }
55904
+
55905
+ matchesPhysicalSettlement(input: {
55906
+ accountId: string;
55907
+ workspaceId: string;
55908
+ admission: SandboxWorkspaceMutationAdmission;
55909
+ operation: string;
55910
+ outcome: SandboxWorkspaceMutationProviderOutcome;
55911
+ }): boolean {
55912
+ return isDeepStrictEqual(this.physicalSettlement, input);
55913
+ }
55914
+ }
55915
+
55778
55916
  export type SandboxRetainedProcessState = "active" | "exited" | "lost";
55779
55917
 
55780
55918
  export type SandboxRetainedProcess = {
@@ -55953,6 +56091,8 @@ type SandboxWorkspaceMutationSettlementResult =
55953
56091
  | "authority_unattributed"
55954
56092
  | "authority_revoked";
55955
56093
  detail: string;
56094
+ physicallySettled?: true;
56095
+ retainedProcessTerminal?: SandboxRetainedProcessTerminalState;
55956
56096
  };
55957
56097
 
55958
56098
  type TurnWorkspaceMutationAuthority = {
@@ -57230,6 +57370,13 @@ function workspaceMutationAuthorityFailure(
57230
57370
  ) {
57231
57371
  return null;
57232
57372
  }
57373
+ if (error instanceof SandboxRetainedProcessTerminalError) {
57374
+ return {
57375
+ failure: error.code,
57376
+ detail: error.message,
57377
+ retainedProcessTerminal: { state: error.state, exitCode: error.exitCode },
57378
+ };
57379
+ }
57233
57380
  return { failure: error.code, detail: error.message };
57234
57381
  }
57235
57382
 
@@ -57308,6 +57455,10 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57308
57455
  },
57309
57456
  ): Promise<void> {
57310
57457
  const operation = normalizeWorkspaceMutationOperation(input.operation);
57458
+ const admissionSnapshot = { ...input.admission };
57459
+ const outcome = input.outcome;
57460
+ // Savepoint release cannot prove an outer caller-owned transaction committed.
57461
+ const ownsCommit = typeof (db as Database & { rollback?: unknown }).rollback !== "function";
57311
57462
  const settleOnce = async (): Promise<SandboxWorkspaceMutationSettlementResult> =>
57312
57463
  await withRlsContext(
57313
57464
  db,
@@ -57349,19 +57500,20 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57349
57500
  const admission = await selectExactAdmissionForUpdate(tx, {
57350
57501
  accountId: authorityInput.accountId,
57351
57502
  workspaceId: authorityInput.workspaceId,
57352
- admissionId: input.admission.id,
57503
+ admissionId: admissionSnapshot.id,
57353
57504
  actorKind,
57354
57505
  actorId,
57355
57506
  sessionId: authorityInput.sessionId,
57356
- admittedWorkspaceGeneration: input.admission.workspaceGeneration,
57507
+ admittedWorkspaceGeneration: admissionSnapshot.workspaceGeneration,
57357
57508
  operation,
57358
57509
  });
57359
57510
  if (
57360
57511
  !admission ||
57361
- !admissionMatchesSnapshot(admission, input.admission) ||
57362
- !admissionSnapshotMatchesAuthorityInput(input.admission, authorityInput) ||
57512
+ !admissionMatchesSnapshot(admission, admissionSnapshot) ||
57513
+ !admissionMatchesAuthorityInput(admission, authorityInput) ||
57363
57514
  admission.provider_outcome === "retained" ||
57364
- (admission.provider_outcome && admission.provider_outcome !== input.outcome)
57515
+ (admission.provider_outcome && admission.provider_outcome !== outcome) ||
57516
+ (admission.settled_at && admission.provider_outcome !== outcome)
57365
57517
  ) {
57366
57518
  return {
57367
57519
  failure: "admission_fenced" as const,
@@ -57374,11 +57526,21 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57374
57526
  authorityInput.workspaceId,
57375
57527
  authorityInput.sessionId,
57376
57528
  );
57377
- await tx.execute(sql`
57529
+ const [physical] = await tx.execute<{
57530
+ provider_outcome: string;
57531
+ settled_at: Date;
57532
+ }>(sql`
57378
57533
  update sandbox_workspace_mutation_admissions set
57379
- provider_outcome = ${input.outcome}, settled_at = now()
57380
- where id = ${input.admission.id} and settled_at is null
57534
+ provider_outcome = ${outcome}, settled_at = now()
57535
+ where id = ${admissionSnapshot.id} and settled_at is null
57536
+ returning provider_outcome, settled_at
57381
57537
  `);
57538
+ if (physical?.provider_outcome !== outcome || !physical.settled_at) {
57539
+ return {
57540
+ failure: "admission_fenced" as const,
57541
+ detail: "Workspace mutation settlement did not commit its exact provider outcome",
57542
+ };
57543
+ }
57382
57544
  if (
57383
57545
  pendingAttempt ||
57384
57546
  (authorityInput.kind !== "direct" &&
@@ -57410,12 +57572,15 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57410
57572
  });
57411
57573
  }
57412
57574
  }
57413
- if (input.outcome === "rejected") return { failure: null };
57414
- if (authorityFailure) return authorityFailure;
57575
+ if (outcome === "rejected") return { failure: null };
57576
+ if (authorityFailure) return { ...authorityFailure, physicallySettled: true };
57415
57577
  if (!authority) {
57416
57578
  throw new Error("Workspace mutation settlement lost its locked authority");
57417
57579
  }
57418
- return await verifyResolvedAdmissionAuthority(tx, authority, admission);
57580
+ const acceptance = await verifyResolvedAdmissionAuthority(tx, authority, admission);
57581
+ return acceptance.failure === null
57582
+ ? acceptance
57583
+ : { ...acceptance, physicallySettled: true };
57419
57584
  }),
57420
57585
  );
57421
57586
  const settlement = await runIdempotentPersistenceTransaction(
@@ -57426,6 +57591,23 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57426
57591
  settleOnce,
57427
57592
  );
57428
57593
  if (settlement.failure !== null) {
57594
+ if (settlement.physicallySettled && ownsCommit) {
57595
+ // This branch is reached only after the transaction committed a matching
57596
+ // terminal admission. A failed commit or contradictory receipt never
57597
+ // reaches it, even when the provider returned successfully.
57598
+ throw new SandboxWorkspaceMutationOutputRejectedError(
57599
+ settlement.failure,
57600
+ settlement.detail,
57601
+ {
57602
+ accountId: authorityInput.accountId,
57603
+ workspaceId: authorityInput.workspaceId,
57604
+ admission: admissionSnapshot,
57605
+ operation,
57606
+ outcome: "resolved",
57607
+ },
57608
+ settlement.retainedProcessTerminal ?? null,
57609
+ );
57610
+ }
57429
57611
  throw new SandboxWorkspaceMutationFencedError(settlement.failure, settlement.detail);
57430
57612
  }
57431
57613
  }
@@ -85986,20 +86168,12 @@ export async function mutateAndAppendSessionEventsForTurnAttempt(
85986
86168
  workspaceId,
85987
86169
  activityGateOpen,
85988
86170
  async (tx) => {
85989
- const mutationApplied = await runSessionEventAppendPhase(
85990
- observer,
85991
- { ...phaseInput, phase: "mutation" },
85992
- async () => await mutate(tx),
85993
- (applied) => (applied ? "ok" : "mutation_skipped"),
85994
- );
85995
- if (!mutationApplied) {
85996
- return {
85997
- events: [],
85998
- accepted: false,
85999
- mutationApplied: false,
86000
- canonicalStartupMilestones: [],
86001
- };
86002
- }
86171
+ // Canonical prefix BEFORE the caller's mutation. The mutation
86172
+ // locks a session-owned row (the Codemode journal row on
86173
+ // terminal settlement); taking it first and the session prefix
86174
+ // second inverted the order of every prefix holder that then
86175
+ // locks the same row (a Codemode re-submit or claim), which
86176
+ // deadlocked POST /codemode/calls in production (40P01).
86003
86177
  const fence = await runSessionEventAppendPhase(
86004
86178
  observer,
86005
86179
  { ...phaseInput, phase: "turn_attempt_fence" },
@@ -86020,6 +86194,20 @@ export async function mutateAndAppendSessionEventsForTurnAttempt(
86020
86194
  if (!activityGateOpen && !fence.allowed) {
86021
86195
  throw new SessionActivityGateEscalation();
86022
86196
  }
86197
+ const mutationApplied = await runSessionEventAppendPhase(
86198
+ observer,
86199
+ { ...phaseInput, phase: "mutation" },
86200
+ async () => await mutate(tx),
86201
+ (applied) => (applied ? "ok" : "mutation_skipped"),
86202
+ );
86203
+ if (!mutationApplied) {
86204
+ return {
86205
+ events: [],
86206
+ accepted: false,
86207
+ mutationApplied: false,
86208
+ canonicalStartupMilestones: [],
86209
+ };
86210
+ }
86023
86211
  let sequence = session.lastSequence;
86024
86212
  const now = new Date();
86025
86213
  const usageSourceKey = (input: AppendEventInput): string | null => {
@@ -87622,67 +87810,6 @@ function projectInstallationConfig(config: Record<string, unknown>): Record<stri
87622
87810
  return { ...rest, headerNames: Object.keys(headersEncrypted).sort() };
87623
87811
  }
87624
87812
 
87625
- function mapConnectionMetadata(row: {
87626
- id: string;
87627
- authorityId?: string | null;
87628
- authorityGeneration: number;
87629
- accountId: string;
87630
- workspaceId: string;
87631
- subjectId: string | null;
87632
- providerDomain: string;
87633
- kind: string;
87634
- status: string;
87635
- grantedScopes: string[];
87636
- expiresAt: Date | null;
87637
- lastRefreshAt: Date | null;
87638
- lastUsedAt: Date | null;
87639
- lastError: string | null;
87640
- version: number;
87641
- verifiedInstallAt: Date | null;
87642
- verifiedInstallVersion: number | null;
87643
- metadata: Record<string, unknown>;
87644
- createdBySubjectId: string | null;
87645
- updatedBySubjectId: string | null;
87646
- createdAt: Date;
87647
- updatedAt: Date;
87648
- }): ConnectionMetadataWithVerification {
87649
- const {
87650
- [OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _openRouterOperationId,
87651
- [OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _openRouterOperationDigest,
87652
- [VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _operationId,
87653
- [VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _operationDigest,
87654
- anthropicCredentialOperationId: _anthropicOperationId,
87655
- anthropicCredentialOperationDigest: _anthropicOperationDigest,
87656
- claude_subscriptionCredentialOperationId: _claudeOperationId,
87657
- claude_subscriptionCredentialOperationDigest: _claudeOperationDigest,
87658
- ...publicMetadata
87659
- } = row.metadata;
87660
- return {
87661
- id: row.id,
87662
- ...(row.subjectId !== null && row.authorityId ? { authorityId: row.authorityId } : {}),
87663
- connectionAuthorityGeneration: row.authorityGeneration,
87664
- accountId: row.accountId,
87665
- workspaceId: row.workspaceId,
87666
- subjectId: row.subjectId,
87667
- providerDomain: row.providerDomain,
87668
- kind: row.kind as ConnectionKind,
87669
- status: row.status as ConnectionStatus,
87670
- grantedScopes: row.grantedScopes,
87671
- expiresAt: row.expiresAt?.toISOString() ?? null,
87672
- lastRefreshAt: row.lastRefreshAt?.toISOString() ?? null,
87673
- lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
87674
- lastError: row.lastError,
87675
- version: row.version,
87676
- verifiedInstallAt: row.verifiedInstallAt?.toISOString() ?? null,
87677
- verifiedInstallVersion: row.verifiedInstallVersion,
87678
- metadata: publicMetadata,
87679
- createdBySubjectId: row.createdBySubjectId,
87680
- updatedBySubjectId: row.updatedBySubjectId,
87681
- createdAt: row.createdAt.toISOString(),
87682
- updatedAt: row.updatedAt.toISOString(),
87683
- };
87684
- }
87685
-
87686
87813
  function mapSlackBotPostOperation(
87687
87814
  row: typeof schema.slackBotPostOperations.$inferSelect,
87688
87815
  ): SlackBotPostOperation {
@@ -88321,6 +88448,7 @@ export * from "./codemode-approvals";
88321
88448
  export * from "./tool-action-reviews";
88322
88449
  export * from "./browser-sessions";
88323
88450
  export * from "./browser-deadline-checkpoints";
88451
+ export * from "./sandbox-interaction-idle";
88324
88452
  export * from "./computer-sessions";
88325
88453
  export * from "./browser-identities";
88326
88454
  export * from "./browser-state-artifacts";