@opengeni/db 1.4.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 (68) hide show
  1. package/dist/canonical-human-identities.js +2 -2
  2. package/dist/capability-reconciliation.js +1 -1
  3. package/dist/{chunk-7QWWTVYH.js → chunk-5ROHLEN6.js} +29 -4
  4. package/dist/chunk-5ROHLEN6.js.map +1 -0
  5. package/dist/{chunk-BA52ITIY.js → chunk-A4J7EECR.js} +2 -2
  6. package/dist/{chunk-3WUCM7YF.js → chunk-B5C5SBSI.js} +2 -2
  7. package/dist/{chunk-VCHK4D6S.js → chunk-DKFEUGM4.js} +2 -2
  8. package/dist/{chunk-EFANLEWI.js → chunk-HUKBIZGH.js} +2 -2
  9. package/dist/{chunk-27T4AP25.js → chunk-IT3EYY2C.js} +5 -3
  10. package/dist/{chunk-27T4AP25.js.map → chunk-IT3EYY2C.js.map} +1 -1
  11. package/dist/{chunk-NT7OGKYX.js → chunk-JWBEE4CL.js} +3 -3
  12. package/dist/{chunk-AMK3FNOW.js → chunk-K5MTNKJU.js} +3 -3
  13. package/dist/{chunk-GC5GUTV3.js → chunk-NZ2SP55O.js} +2 -2
  14. package/dist/{chunk-TRUEH4I2.js → chunk-OKCBARX6.js} +2 -2
  15. package/dist/{chunk-EMYA6XNE.js → chunk-OV5AUIBA.js} +2 -2
  16. package/dist/{chunk-T3CYF3DH.js → chunk-TODVDGMO.js} +35 -17
  17. package/dist/{chunk-T3CYF3DH.js.map → chunk-TODVDGMO.js.map} +1 -1
  18. package/dist/{chunk-CKDHKQS4.js → chunk-W75DRYG3.js} +4 -4
  19. package/dist/{chunk-UQELJ6OM.js → chunk-Y72DP3JF.js} +2 -2
  20. package/dist/codemode-approvals.d.ts +7 -1
  21. package/dist/connect-attempts.js +2 -2
  22. package/dist/credit-balances.d.ts +25 -4
  23. package/dist/database.d.ts +7 -1
  24. package/dist/editable-artifact-durable-export.js +1 -1
  25. package/dist/editable-artifacts.js +2 -2
  26. package/dist/index.d.ts +24 -2
  27. package/dist/index.js +2778 -2508
  28. package/dist/index.js.map +1 -1
  29. package/dist/managed-auth-session-sets.js +2 -2
  30. package/dist/mcp-operations.js +1 -1
  31. package/dist/modal-native-live-origin.d.ts +75 -0
  32. package/dist/organization-integration-policy.js +2 -2
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/retained-provider-commands.d.ts +21 -21
  35. package/dist/retained-provider-commands.js +5 -5
  36. package/dist/runtime-posture.d.ts +1 -1
  37. package/dist/sandbox-interaction-idle.d.ts +22 -0
  38. package/dist/session-background-commands.js +3 -3
  39. package/dist/session-command-output.js +4 -4
  40. package/dist/session-event-slices.js +2 -2
  41. package/dist/session-mcp-credential-rotation.js +4 -4
  42. package/dist/session-tenancy.js +2 -2
  43. package/dist/video-generation.js +2 -2
  44. package/dist/workspace-tool-defaults.js +3 -3
  45. package/drizzle/0632_modal_native_live_origin.sql +239 -0
  46. package/package.json +7 -7
  47. package/src/codemode-approvals.ts +52 -22
  48. package/src/codemode-operations.ts +198 -181
  49. package/src/credit-balances.ts +58 -17
  50. package/src/database.ts +9 -0
  51. package/src/index.ts +449 -254
  52. package/src/modal-native-live-origin.ts +148 -0
  53. package/src/provision-roles.ts +12 -0
  54. package/src/runtime-posture.ts +32 -3
  55. package/src/sandbox-interaction-idle.ts +91 -0
  56. package/src/transcription-recordings.ts +3 -1
  57. package/dist/chunk-7QWWTVYH.js.map +0 -1
  58. /package/dist/{chunk-BA52ITIY.js.map → chunk-A4J7EECR.js.map} +0 -0
  59. /package/dist/{chunk-3WUCM7YF.js.map → chunk-B5C5SBSI.js.map} +0 -0
  60. /package/dist/{chunk-VCHK4D6S.js.map → chunk-DKFEUGM4.js.map} +0 -0
  61. /package/dist/{chunk-EFANLEWI.js.map → chunk-HUKBIZGH.js.map} +0 -0
  62. /package/dist/{chunk-NT7OGKYX.js.map → chunk-JWBEE4CL.js.map} +0 -0
  63. /package/dist/{chunk-AMK3FNOW.js.map → chunk-K5MTNKJU.js.map} +0 -0
  64. /package/dist/{chunk-GC5GUTV3.js.map → chunk-NZ2SP55O.js.map} +0 -0
  65. /package/dist/{chunk-TRUEH4I2.js.map → chunk-OKCBARX6.js.map} +0 -0
  66. /package/dist/{chunk-EMYA6XNE.js.map → chunk-OV5AUIBA.js.map} +0 -0
  67. /package/dist/{chunk-CKDHKQS4.js.map → chunk-W75DRYG3.js.map} +0 -0
  68. /package/dist/{chunk-UQELJ6OM.js.map → chunk-Y72DP3JF.js.map} +0 -0
package/src/index.ts CHANGED
@@ -15,11 +15,19 @@ export {
15
15
  type ConnectionMetadataWithVerification,
16
16
  } from "./connection-metadata";
17
17
  export { readCreditPromotionPolicy } from "./credit-promotion-policy";
18
- import { getBillingBalance, getSpendableCreditBalance, planCreditDebit } from "./credit-balances";
18
+ import {
19
+ getBillingBalance,
20
+ getSpendableCreditBalance,
21
+ planCreditDebit,
22
+ planPostUseCreditAllocations,
23
+ type CreditUsage,
24
+ } from "./credit-balances";
19
25
  export {
20
26
  getBillingBalance,
21
27
  getSpendableCreditBalance,
22
28
  spendableCreditMicros,
29
+ VOICE_CREDIT_USAGE,
30
+ type CreditUsage,
23
31
  } from "./credit-balances";
24
32
  import {
25
33
  lockTurnAttemptWriteFenceTx,
@@ -52,6 +60,12 @@ import {
52
60
  inboxOrigin,
53
61
  } from "./inbox-execution-context";
54
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";
55
69
  export { SubscriptionAccountChangedError } from "./subscription-account-conflict";
56
70
  import type { GoalAdmissionPausedReason } from "@opengeni/contracts";
57
71
  import {
@@ -5878,6 +5892,11 @@ export async function applyCreditDebitAfterUse(
5878
5892
  idempotencyKey: string;
5879
5893
  metadata?: Record<string, unknown>;
5880
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;
5881
5900
  },
5882
5901
  ): Promise<{ balance: BillingBalance; debitedMicros: number }> {
5883
5902
  if (!Number.isSafeInteger(input.amountMicros) || input.amountMicros <= 0) {
@@ -5887,6 +5906,17 @@ export async function applyCreditDebitAfterUse(
5887
5906
  db,
5888
5907
  { accountId: input.accountId, workspaceId: input.workspaceId ?? null },
5889
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
+ }
5890
5920
  const inserted = await scopedDb
5891
5921
  .insert(schema.creditLedgerEntries)
5892
5922
  .values({
@@ -5927,6 +5957,14 @@ export async function applyCreditDebitAfterUse(
5927
5957
  "post-use credit debit idempotency key conflicts with a different charge",
5928
5958
  );
5929
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
+ );
5930
5968
  }
5931
5969
  return {
5932
5970
  balance: await getBillingBalance(scopedDb, input.accountId),
@@ -33303,6 +33341,53 @@ async function priorConnectorActionPreparation(
33303
33341
  };
33304
33342
  }
33305
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
+
33306
33391
  export async function prepareConnectorActionApproval(
33307
33392
  db: Database,
33308
33393
  identity: ConnectorActionAttemptIdentity,
@@ -33312,67 +33397,66 @@ export async function prepareConnectorActionApproval(
33312
33397
  if (!normalized.connectionId) {
33313
33398
  return { managed: false, decision: "unmanaged" };
33314
33399
  }
33315
- return await withRlsContext(
33400
+ return await runConnectorActionLedgerTransaction(
33316
33401
  db,
33317
- { accountId: identity.accountId, workspaceId: identity.workspaceId },
33318
- async (scopedDb) =>
33319
- await scopedDb.transaction(async (tx) => {
33320
- const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
33321
- const prior = await priorConnectorActionPreparation(
33322
- tx as unknown as Database,
33323
- identity,
33324
- normalized,
33325
- );
33326
- if (prior) return prior;
33327
- const resolved = resolveConnectorActionPolicy(snapshot, {
33328
- connectionId: normalized.connectionId!,
33329
- serverId: normalized.serverId,
33330
- toolName: normalized.toolName,
33331
- actionName: normalized.policyActionSelector,
33332
- ...(normalized.defaultDecision !== undefined
33333
- ? { defaultDecision: normalized.defaultDecision }
33334
- : {}),
33335
- });
33336
- if (!resolved.managed) return { managed: false, decision: "unmanaged" } as const;
33337
- const durable = durableConnectorActionInvocation(
33338
- identity,
33339
- { ...normalized, connectionId: normalized.connectionId! },
33340
- resolved,
33341
- );
33342
- const decision = connectorActionPolicyDecision(resolved);
33343
- recordToolApproval(decision, resolved.source);
33344
- if (decision === "allow") {
33345
- return {
33346
- managed: true,
33347
- decision,
33348
- actionFingerprint: durable.actionFingerprint,
33349
- } as const;
33350
- }
33351
- const { row, inserted } = await insertConnectorActionRequest(tx as unknown as Database, {
33352
- identity,
33353
- invocation: durable,
33354
- resolved,
33355
- status: decision === "ask" ? "pending" : "blocked",
33356
- });
33357
- if (inserted) {
33358
- await insertConnectorActionAudit(tx as unknown as Database, {
33359
- row,
33360
- action:
33361
- decision === "ask"
33362
- ? "connector.action.approval_requested"
33363
- : "connector.action.blocked",
33364
- subjectId: identity.initiator.subjectId,
33365
- extra: { outcome: decision },
33366
- });
33367
- }
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") {
33368
33430
  return {
33369
33431
  managed: true,
33370
33432
  decision,
33371
- approvalStatus: row.status,
33372
- requestId: row.id,
33373
- actionFingerprint: row.actionFingerprint,
33433
+ actionFingerprint: durable.actionFingerprint,
33374
33434
  } as const;
33375
- }),
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
+ },
33376
33460
  );
33377
33461
  }
33378
33462
 
@@ -33426,13 +33510,28 @@ export async function beginConnectorActionExecution(
33426
33510
  if (!normalized.connectionId) {
33427
33511
  return { allowed: true, managed: false };
33428
33512
  }
33429
- 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(
33430
33524
  db,
33431
- { accountId: identity.accountId, workspaceId: identity.workspaceId },
33432
- async (scopedDb) =>
33433
- await scopedDb.transaction(async (tx) => {
33434
- const snapshot = await connectorActionAttemptSnapshot(tx as unknown as Database, identity);
33435
- 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
33436
33535
  .select()
33437
33536
  .from(schema.connectorActionRequests)
33438
33537
  .where(
@@ -33442,73 +33541,89 @@ export async function beginConnectorActionExecution(
33442
33541
  eq(schema.connectorActionRequests.turnId, identity.turnId),
33443
33542
  eq(schema.connectorActionRequests.approvalId, normalized.approvalId),
33444
33543
  ),
33445
- )
33446
- .for("update")
33447
- .limit(1);
33448
- if (existing && !connectorActionRequestMatchesLogicalCall(existing, identity, normalized)) {
33449
- throw new Error("connector action approval id conflicts with different immutable inputs");
33450
- }
33451
- let row = existing;
33452
- let inserted = false;
33453
- if (!row) {
33454
- const resolved = resolveConnectorActionPolicy(snapshot, {
33455
- connectionId: normalized.connectionId!,
33456
- serverId: normalized.serverId,
33457
- toolName: normalized.toolName,
33458
- actionName: normalized.policyActionSelector,
33459
- ...(normalized.defaultDecision !== undefined
33460
- ? { defaultDecision: normalized.defaultDecision }
33461
- : {}),
33462
- });
33463
- if (!resolved.managed) return { allowed: true, managed: false } as const;
33464
- const durable = durableConnectorActionInvocation(
33465
- identity,
33466
- { ...normalized, connectionId: normalized.connectionId! },
33467
- resolved,
33468
33544
  );
33469
- const decision = connectorActionPolicyDecision(resolved);
33470
- const created = await insertConnectorActionRequest(tx as unknown as Database, {
33471
- identity,
33472
- invocation: durable,
33473
- resolved,
33474
- status: decision === "ask" ? "pending" : decision === "block" ? "blocked" : "executing",
33475
- });
33476
- row = created.row;
33477
- inserted = created.inserted;
33478
- if (inserted) {
33479
- await insertConnectorActionAudit(tx as unknown as Database, {
33480
- row,
33481
- action:
33482
- decision === "ask"
33483
- ? "connector.action.approval_requested"
33484
- : decision === "block"
33485
- ? "connector.action.blocked"
33486
- : "connector.action.execution_started",
33487
- subjectId: identity.initiator.subjectId,
33488
- extra: { outcome: decision === "allow" ? "started" : decision },
33489
- });
33490
- }
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;
33491
33558
  }
33492
- if (row.status === "approved") {
33493
- const [executing] = await tx
33494
- .update(schema.connectorActionRequests)
33495
- .set({
33496
- status: "executing",
33497
- executionAttemptId: identity.attemptId,
33498
- executionAttemptGeneration: identity.executionGeneration,
33499
- executionStartedAt: new Date(),
33500
- updatedAt: new Date(),
33501
- })
33502
- .where(eq(schema.connectorActionRequests.id, row.id))
33503
- .returning();
33504
- if (!executing) throw new Error("Approved connector action request disappeared");
33505
- row = executing;
33506
- 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, {
33507
33586
  row,
33508
- 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",
33509
33593
  subjectId: identity.initiator.subjectId,
33510
- extra: { outcome: "started" },
33594
+ extra: { outcome: decision === "allow" ? "started" : decision },
33511
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) {
33512
33627
  return {
33513
33628
  allowed: true,
33514
33629
  managed: true,
@@ -33516,58 +33631,49 @@ export async function beginConnectorActionExecution(
33516
33631
  actionFingerprint: row.actionFingerprint,
33517
33632
  } as const;
33518
33633
  }
33519
- if (row.status === "executing") {
33520
- if (inserted) {
33521
- return {
33522
- allowed: true,
33523
- managed: true,
33524
- requestId: row.id,
33525
- actionFingerprint: row.actionFingerprint,
33526
- } as const;
33527
- }
33528
- const [uncertain] = await tx
33529
- .update(schema.connectorActionRequests)
33530
- .set({
33531
- status: "uncertain",
33532
- outcome: "retry_after_execution_started",
33533
- executionFinishedAt: new Date(),
33534
- updatedAt: new Date(),
33535
- })
33536
- .where(eq(schema.connectorActionRequests.id, row.id))
33537
- .returning();
33538
- if (!uncertain) throw new Error("Executing connector action request disappeared");
33539
- await insertConnectorActionAudit(tx as unknown as Database, {
33540
- row: uncertain,
33541
- action: "connector.action.execution_uncertain",
33542
- subjectId: identity.initiator.subjectId,
33543
- extra: { outcome: "retry_denied" },
33544
- });
33545
- return {
33546
- allowed: false,
33547
- managed: true,
33548
- reason: "uncertain_retry",
33549
- requestId: uncertain.id,
33550
- actionFingerprint: uncertain.actionFingerprint,
33551
- } as const;
33552
- }
33553
- const reason =
33554
- row.status === "pending"
33555
- ? "approval_required"
33556
- : row.status === "rejected"
33557
- ? "rejected"
33558
- : row.status === "blocked"
33559
- ? "blocked"
33560
- : row.status === "failed"
33561
- ? "not_executed"
33562
- : "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
+ });
33563
33651
  return {
33564
33652
  allowed: false,
33565
33653
  managed: true,
33566
- reason,
33567
- requestId: row.id,
33568
- actionFingerprint: row.actionFingerprint,
33654
+ reason: "uncertain_retry",
33655
+ requestId: uncertain.id,
33656
+ actionFingerprint: uncertain.actionFingerprint,
33569
33657
  } as const;
33570
- }),
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
+ },
33571
33677
  );
33572
33678
  }
33573
33679
 
@@ -33581,12 +33687,17 @@ export async function completeConnectorActionExecution(
33581
33687
  outcome: "completed" | "uncertain" | "not_executed";
33582
33688
  },
33583
33689
  ): Promise<void> {
33584
- await withRlsContext(
33690
+ await runConnectorActionLedgerTransaction(
33585
33691
  db,
33586
- { accountId: input.accountId, workspaceId: input.workspaceId },
33587
- async (scopedDb) =>
33588
- await scopedDb.transaction(async (tx) => {
33589
- 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
33590
33701
  .select()
33591
33702
  .from(schema.connectorActionRequests)
33592
33703
  .where(
@@ -33595,43 +33706,51 @@ export async function completeConnectorActionExecution(
33595
33706
  eq(schema.connectorActionRequests.id, input.requestId),
33596
33707
  eq(schema.connectorActionRequests.executionAttemptId, input.attemptId),
33597
33708
  ),
33598
- )
33599
- .for("update")
33600
- .limit(1);
33601
- if (!existing) throw new Error("Connector action request not found for completion");
33602
- // A not-executed completion is a terminal failure whose provider
33603
- // request never happened; it keeps the existing status vocabulary.
33604
- // Reaching the terminal status is idempotent even when a concurrent
33605
- // begin already settled the row with a different uncertain outcome
33606
- // (retry_after_execution_started).
33607
- const terminalStatus = input.outcome === "not_executed" ? "failed" : input.outcome;
33608
- if (existing.status === terminalStatus) return;
33609
- if (existing.status !== "executing") {
33610
- throw new Error(`Connector action request cannot complete from ${existing.status}`);
33611
- }
33612
- const [row] = await tx
33613
- .update(schema.connectorActionRequests)
33614
- .set({
33615
- status: terminalStatus,
33616
- outcome: input.outcome,
33617
- executionFinishedAt: new Date(),
33618
- updatedAt: new Date(),
33619
- })
33620
- .where(eq(schema.connectorActionRequests.id, existing.id))
33621
- .returning();
33622
- if (!row) throw new Error("Connector action request disappeared during completion");
33623
- await insertConnectorActionAudit(tx as unknown as Database, {
33624
- row,
33625
- action:
33626
- input.outcome === "completed"
33627
- ? "connector.action.execution_completed"
33628
- : input.outcome === "not_executed"
33629
- ? "connector.action.execution_not_executed"
33630
- : "connector.action.execution_uncertain",
33631
- subjectId: row.initiatorSubjectId,
33632
- extra: { outcome: input.outcome },
33633
- });
33634
- }),
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
+ },
33635
33754
  );
33636
33755
  }
33637
33756
 
@@ -43097,9 +43216,12 @@ export async function appendSessionHistoryItems(
43097
43216
  if (input.items.length === 0) {
43098
43217
  return true;
43099
43218
  }
43100
- 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(
43101
43222
  db,
43102
43223
  { accountId: input.accountId, workspaceId: input.workspaceId },
43224
+ { stage: "session_history_append", correlationId: input.expectedAttemptId },
43103
43225
  async (scopedDb) => {
43104
43226
  return await scopedDb.transaction(async (tx) => {
43105
43227
  const allowed = await lockTurnAttemptWriteFenceTx(tx, {
@@ -43413,9 +43535,12 @@ export async function recordPendingSessionToolCallResult(
43413
43535
  };
43414
43536
  },
43415
43537
  ): Promise<{ accepted: boolean; recorded: boolean }> {
43416
- 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(
43417
43541
  db,
43418
43542
  { accountId: input.accountId, workspaceId: input.workspaceId },
43543
+ { stage: "session_pending_tool_call_result", correlationId: input.callId },
43419
43544
  async (scopedDb) =>
43420
43545
  await scopedDb.transaction(async (tx) => {
43421
43546
  const fence = await lockTurnAttemptWriteFenceTx(tx, {
@@ -55312,7 +55437,13 @@ export async function confirmDrainCold(
55312
55437
  /** The idle window named in contained commands' agent notice. */
55313
55438
  idleCommandContainmentMs?: number | undefined;
55314
55439
  },
55315
- ): 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
+ }> {
55316
55447
  const backgroundCommandEvents: SessionEvent[] = [];
55317
55448
  // Settling a command appends its terminal session event and agent input, so
55318
55449
  // the cold commit runs inside the session activity gate.
@@ -55435,8 +55566,23 @@ export async function confirmDrainCold(
55435
55566
  const hasArchive = current.archive.status !== "none";
55436
55567
  const archiveComplete = hasCompleteWorkspaceArchive(row);
55437
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;
55438
55584
  const lateArchiveCapture =
55439
- input.providerMissingBeforeCapture &&
55585
+ workspaceLost &&
55440
55586
  row.archive_capture_id !== null &&
55441
55587
  row.archive_capture_provider_request_id !== null &&
55442
55588
  row.archive_capture_generation !== null &&
@@ -55461,18 +55607,27 @@ export async function confirmDrainCold(
55461
55607
  ? "pending"
55462
55608
  : hasArchive
55463
55609
  ? "degraded"
55464
- : input.providerMissingBeforeCapture || freshPending
55610
+ : workspaceLost || freshPending
55465
55611
  ? "unrecoverable"
55466
55612
  : "not_required";
55467
55613
  const recovery: SandboxRecoveryState = {
55468
- provider: {
55469
- status: input.providerMissingBeforeCapture ? "missing" : "not_created",
55470
- instanceId: input.providerMissingBeforeCapture ? row.instance_id : null,
55471
- observedAt: now,
55472
- ...(input.providerMissingBeforeCapture
55473
- ? { diagnostic: "provider_not_found_before_workspace_capture" }
55474
- : {}),
55475
- },
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
+ },
55476
55631
  archive: current.archive,
55477
55632
  restore: {
55478
55633
  status: restoreStatus,
@@ -55512,9 +55667,7 @@ export async function confirmDrainCold(
55512
55667
  archiveOnlyResumeState(
55513
55668
  row,
55514
55669
  recovery,
55515
- input.providerMissingBeforeCapture
55516
- ? providerLossRecordForLostRow(row, "drain_probe", now)
55517
- : undefined,
55670
+ workspaceLost ? providerLossRecordForLostRow(row, "drain_probe", now) : undefined,
55518
55671
  ),
55519
55672
  )
55520
55673
  : null;
@@ -55563,7 +55716,7 @@ export async function confirmDrainCold(
55563
55716
  and archive_capture_id is not distinct from ${input.expectedCaptureId ?? null}::uuid
55564
55717
  returning id
55565
55718
  `);
55566
- if (rows.length > 0 && input.providerMissingBeforeCapture) {
55719
+ if (rows.length > 0 && workspaceLost) {
55567
55720
  const eventId = crypto.randomUUID();
55568
55721
  await tx.insert(schema.auditEvents).values(
55569
55722
  withLosslessContentWriteVersion(
@@ -55593,7 +55746,10 @@ export async function confirmDrainCold(
55593
55746
  if (rows.length > 0 && row.rotation_requested_at !== null) {
55594
55747
  await wakeSandboxLifecycleWaitersTx(tx, input);
55595
55748
  }
55596
- return { wentCold: rows.length > 0 };
55749
+ return {
55750
+ wentCold: rows.length > 0,
55751
+ ...(rows.length > 0 && unpublishedWarmingLoss ? { unpublishedProviderLost: true } : {}),
55752
+ };
55597
55753
  },
55598
55754
  );
55599
55755
  });
@@ -55708,23 +55864,42 @@ export type SandboxWorkspaceMutationPhysicalSettlement = Readonly<{
55708
55864
  outcome: "resolved";
55709
55865
  }>;
55710
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
+
55711
55874
  /** The exact provider outcome committed before its output failed an acceptance
55712
55875
  * fence. A generic admission fence or failed transaction carries no receipt.
55713
55876
  * This error never authorizes accepting output or replaying provider work. */
55714
55877
  export class SandboxWorkspaceMutationOutputRejectedError extends SandboxWorkspaceMutationFencedError {
55715
55878
  readonly name = "SandboxWorkspaceMutationOutputRejectedError";
55716
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;
55717
55885
 
55718
55886
  constructor(
55719
55887
  code: SandboxWorkspaceMutationFencedError["code"],
55720
55888
  message: string,
55721
55889
  physicalSettlement: SandboxWorkspaceMutationPhysicalSettlement,
55890
+ retainedProcessTerminal: SandboxRetainedProcessTerminalState | null = null,
55722
55891
  ) {
55723
55892
  super(code, message);
55724
55893
  this.physicalSettlement = Object.freeze({
55725
55894
  ...physicalSettlement,
55726
55895
  admission: Object.freeze({ ...physicalSettlement.admission }),
55727
55896
  });
55897
+ this.retainedProcessTerminal = retainedProcessTerminal
55898
+ ? Object.freeze({
55899
+ state: retainedProcessTerminal.state,
55900
+ exitCode: retainedProcessTerminal.exitCode,
55901
+ })
55902
+ : null;
55728
55903
  }
55729
55904
 
55730
55905
  matchesPhysicalSettlement(input: {
@@ -55917,6 +56092,7 @@ type SandboxWorkspaceMutationSettlementResult =
55917
56092
  | "authority_revoked";
55918
56093
  detail: string;
55919
56094
  physicallySettled?: true;
56095
+ retainedProcessTerminal?: SandboxRetainedProcessTerminalState;
55920
56096
  };
55921
56097
 
55922
56098
  type TurnWorkspaceMutationAuthority = {
@@ -57194,6 +57370,13 @@ function workspaceMutationAuthorityFailure(
57194
57370
  ) {
57195
57371
  return null;
57196
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
+ }
57197
57380
  return { failure: error.code, detail: error.message };
57198
57381
  }
57199
57382
 
@@ -57412,13 +57595,18 @@ async function verifyWorkspaceMutationSettlementForAuthority(
57412
57595
  // This branch is reached only after the transaction committed a matching
57413
57596
  // terminal admission. A failed commit or contradictory receipt never
57414
57597
  // reaches it, even when the provider returned successfully.
57415
- throw new SandboxWorkspaceMutationOutputRejectedError(settlement.failure, settlement.detail, {
57416
- accountId: authorityInput.accountId,
57417
- workspaceId: authorityInput.workspaceId,
57418
- admission: admissionSnapshot,
57419
- operation,
57420
- outcome: "resolved",
57421
- });
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
+ );
57422
57610
  }
57423
57611
  throw new SandboxWorkspaceMutationFencedError(settlement.failure, settlement.detail);
57424
57612
  }
@@ -85980,20 +86168,12 @@ export async function mutateAndAppendSessionEventsForTurnAttempt(
85980
86168
  workspaceId,
85981
86169
  activityGateOpen,
85982
86170
  async (tx) => {
85983
- const mutationApplied = await runSessionEventAppendPhase(
85984
- observer,
85985
- { ...phaseInput, phase: "mutation" },
85986
- async () => await mutate(tx),
85987
- (applied) => (applied ? "ok" : "mutation_skipped"),
85988
- );
85989
- if (!mutationApplied) {
85990
- return {
85991
- events: [],
85992
- accepted: false,
85993
- mutationApplied: false,
85994
- canonicalStartupMilestones: [],
85995
- };
85996
- }
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).
85997
86177
  const fence = await runSessionEventAppendPhase(
85998
86178
  observer,
85999
86179
  { ...phaseInput, phase: "turn_attempt_fence" },
@@ -86014,6 +86194,20 @@ export async function mutateAndAppendSessionEventsForTurnAttempt(
86014
86194
  if (!activityGateOpen && !fence.allowed) {
86015
86195
  throw new SessionActivityGateEscalation();
86016
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
+ }
86017
86211
  let sequence = session.lastSequence;
86018
86212
  const now = new Date();
86019
86213
  const usageSourceKey = (input: AppendEventInput): string | null => {
@@ -88254,6 +88448,7 @@ export * from "./codemode-approvals";
88254
88448
  export * from "./tool-action-reviews";
88255
88449
  export * from "./browser-sessions";
88256
88450
  export * from "./browser-deadline-checkpoints";
88451
+ export * from "./sandbox-interaction-idle";
88257
88452
  export * from "./computer-sessions";
88258
88453
  export * from "./browser-identities";
88259
88454
  export * from "./browser-state-artifacts";