@opengeni/db 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/artifact-catalog.d.ts +12 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-reconciliation.js +2 -2
  4. package/dist/{chunk-KH3TYDGY.js → chunk-3GAR5UN3.js} +2 -2
  5. package/dist/{chunk-UEQQ2WWU.js → chunk-3TGYFFZR.js} +3 -26
  6. package/dist/chunk-3TGYFFZR.js.map +1 -0
  7. package/dist/{chunk-F3YMVRF4.js → chunk-6EIEGHPK.js} +4 -4
  8. package/dist/{chunk-OQZOGCLG.js → chunk-CONWNUSL.js} +2 -2
  9. package/dist/{chunk-7S53WLPI.js → chunk-CYP72JTQ.js} +11 -4
  10. package/dist/{chunk-7S53WLPI.js.map → chunk-CYP72JTQ.js.map} +1 -1
  11. package/dist/{chunk-LDTQGYD2.js → chunk-FWQNHNOU.js} +54 -2
  12. package/dist/chunk-FWQNHNOU.js.map +1 -0
  13. package/dist/{chunk-KEDWSSNP.js → chunk-GDN5TSZP.js} +2 -2
  14. package/dist/{chunk-PQGTWFRF.js → chunk-JICUP7ZR.js} +2 -2
  15. package/dist/{chunk-NLSG7GGA.js → chunk-JLVWBIEM.js} +3 -3
  16. package/dist/{chunk-Y77PVZZD.js → chunk-LXPFNG4L.js} +2 -2
  17. package/dist/{chunk-56MYWKQV.js → chunk-O2Y5E5M3.js} +68 -32
  18. package/dist/chunk-O2Y5E5M3.js.map +1 -0
  19. package/dist/{chunk-7POTLB24.js → chunk-QFJNEXFL.js} +2 -2
  20. package/dist/{chunk-MACELCXG.js → chunk-UCIMHNII.js} +101 -21
  21. package/dist/chunk-UCIMHNII.js.map +1 -0
  22. package/dist/{chunk-OMHBVHX7.js → chunk-VDPCIFKL.js} +5 -5
  23. package/dist/{chunk-N5QCKHYL.js → chunk-YOYILDLY.js} +87 -19
  24. package/dist/chunk-YOYILDLY.js.map +1 -0
  25. package/dist/connect-attempts.js +3 -3
  26. package/dist/credit-balances.d.ts +18 -0
  27. package/dist/credit-promotion-policy.d.ts +10 -0
  28. package/dist/editable-artifact-durable-export.js +2 -2
  29. package/dist/editable-artifacts.js +3 -3
  30. package/dist/index.d.ts +35 -10
  31. package/dist/index.js +2432 -2255
  32. package/dist/index.js.map +1 -1
  33. package/dist/managed-auth-session-sets.js +3 -3
  34. package/dist/managed-user-setup.d.ts +1 -0
  35. package/dist/mcp-operations.js +2 -2
  36. package/dist/organization-integration-policy.js +3 -3
  37. package/dist/organization-membership-lifecycle.d.ts +1 -0
  38. package/dist/persistence-errors.d.ts +9 -6
  39. package/dist/provision-roles.js +1 -1
  40. package/dist/retained-provider-commands.js +6 -6
  41. package/dist/runtime-posture.d.ts +7 -6
  42. package/dist/schema.d.ts +214 -2
  43. package/dist/schema.js +5 -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-tenancy.d.ts +6 -1
  49. package/dist/session-tenancy.js +3 -5
  50. package/dist/video-generation.js +3 -3
  51. package/dist/workspace-tool-defaults.js +4 -4
  52. package/drizzle/0610_artifact_catalog_pins.sql +165 -0
  53. package/drizzle/0611_universal_session_tenancy_activation.sql +166 -0
  54. package/drizzle/0612_scheduled_model_unavailable_refusal.sql +21 -0
  55. package/drizzle/0613_model_scoped_promotional_credits.sql +238 -0
  56. package/drizzle/0614_quiescence_command_containment.sql +7 -0
  57. package/drizzle/0615_credit_promotion_policy_validation.sql +63 -0
  58. package/package.json +7 -7
  59. package/src/artifact-catalog.ts +31 -5
  60. package/src/credit-balances.ts +109 -0
  61. package/src/credit-promotion-policy.ts +12 -0
  62. package/src/database.ts +81 -42
  63. package/src/index.ts +295 -79
  64. package/src/managed-user-setup.ts +8 -0
  65. package/src/organization-membership-lifecycle.ts +8 -0
  66. package/src/persistence-errors.ts +17 -6
  67. package/src/provision-roles.ts +25 -0
  68. package/src/runtime-posture-cli.ts +0 -2
  69. package/src/runtime-posture.ts +86 -27
  70. package/src/schema.ts +57 -1
  71. package/src/session-tenancy.ts +9 -31
  72. package/src/video-generation.ts +2 -7
  73. package/dist/chunk-56MYWKQV.js.map +0 -1
  74. package/dist/chunk-LDTQGYD2.js.map +0 -1
  75. package/dist/chunk-MACELCXG.js.map +0 -1
  76. package/dist/chunk-N5QCKHYL.js.map +0 -1
  77. package/dist/chunk-UEQQ2WWU.js.map +0 -1
  78. /package/dist/{chunk-KH3TYDGY.js.map → chunk-3GAR5UN3.js.map} +0 -0
  79. /package/dist/{chunk-F3YMVRF4.js.map → chunk-6EIEGHPK.js.map} +0 -0
  80. /package/dist/{chunk-OQZOGCLG.js.map → chunk-CONWNUSL.js.map} +0 -0
  81. /package/dist/{chunk-KEDWSSNP.js.map → chunk-GDN5TSZP.js.map} +0 -0
  82. /package/dist/{chunk-PQGTWFRF.js.map → chunk-JICUP7ZR.js.map} +0 -0
  83. /package/dist/{chunk-NLSG7GGA.js.map → chunk-JLVWBIEM.js.map} +0 -0
  84. /package/dist/{chunk-Y77PVZZD.js.map → chunk-LXPFNG4L.js.map} +0 -0
  85. /package/dist/{chunk-7POTLB24.js.map → chunk-QFJNEXFL.js.map} +0 -0
  86. /package/dist/{chunk-OMHBVHX7.js.map → chunk-VDPCIFKL.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export { readCreditPromotionPolicy } from "./credit-promotion-policy";
2
+ import { getBillingBalance, getSpendableCreditBalance, planCreditDebit } from "./credit-balances";
3
+ export {
4
+ getBillingBalance,
5
+ getSpendableCreditBalance,
6
+ spendableCreditMicros,
7
+ } from "./credit-balances";
1
8
  import {
2
9
  agentSelectionNotes,
3
10
  loadInboxExecutionContext,
@@ -6,6 +13,7 @@ import {
6
13
  } from "./inbox-execution-context";
7
14
  import { parentOutboxAuthorityTx } from "./child-outbox-authority";
8
15
  export { SubscriptionAccountChangedError } from "./subscription-account-conflict";
16
+ import type { GoalAdmissionPausedReason } from "@opengeni/contracts";
9
17
  import {
10
18
  claudeSubscriptionAccountRepository,
11
19
  claudeSubscriptionTables,
@@ -439,7 +447,6 @@ import {
439
447
  closePrivateSessionCreateCapability,
440
448
  openPrivateChildSessionCreateCapability,
441
449
  openPrivateSessionCreateCapability,
442
- sessionTenancyProductActivated,
443
450
  } from "./session-tenancy";
444
451
  import {
445
452
  completeCodemodeOperationInTransaction,
@@ -616,6 +623,7 @@ import {
616
623
  SessionEventPersistenceError,
617
624
  type IdempotentPersistenceTransactionOptions,
618
625
  } from "./persistence-errors";
626
+ export { DatabaseTransactionError } from "./persistence-errors";
619
627
  import {
620
628
  assertCapabilityComponentVersionCanChange,
621
629
  effectiveCapabilityOwnerSql,
@@ -5701,6 +5709,7 @@ export async function applyCreditLedgerEntry(
5701
5709
  workspaceId?: string | null;
5702
5710
  type: string;
5703
5711
  amountMicros: number;
5712
+ eligibleModelIds?: string[] | undefined;
5704
5713
  sourceType?: string | null;
5705
5714
  sourceId?: string | null;
5706
5715
  idempotencyKey: string;
@@ -5719,6 +5728,7 @@ export async function applyCreditLedgerEntry(
5719
5728
  workspaceId: input.workspaceId ?? null,
5720
5729
  type: input.type,
5721
5730
  amountMicros: input.amountMicros,
5731
+ eligibleModelIds: input.eligibleModelIds ?? null,
5722
5732
  sourceType: input.sourceType ?? null,
5723
5733
  sourceId: input.sourceId ?? null,
5724
5734
  idempotencyKey: input.idempotencyKey,
@@ -5740,6 +5750,9 @@ export async function applyCreditDebitUpToBalance(
5740
5750
  workspaceId?: string | null;
5741
5751
  type: string;
5742
5752
  requestedAmountMicros: number;
5753
+ /** Canonical, accepted model ID. Omitted for non-model usage. */
5754
+ modelId?: string;
5755
+ creditPolicyRevision?: number | undefined;
5743
5756
  sourceType?: string | null;
5744
5757
  sourceId?: string | null;
5745
5758
  idempotencyKey: string;
@@ -5758,35 +5771,42 @@ export async function applyCreditDebitUpToBalance(
5758
5771
  { accountId: input.accountId, workspaceId: input.workspaceId ?? null },
5759
5772
  async (scopedDb) => {
5760
5773
  await scopedDb.execute(sql`select pg_advisory_xact_lock(hashtext(${input.accountId}))`);
5761
- const before = await getBillingBalance(scopedDb, input.accountId);
5762
- const candidateDebitMicros = Math.min(
5763
- input.requestedAmountMicros,
5764
- Math.max(0, before.balanceMicros),
5765
- );
5766
- let debitedMicros = 0;
5767
- if (candidateDebitMicros > 0) {
5768
- const inserted = await scopedDb
5769
- .insert(schema.creditLedgerEntries)
5770
- .values({
5774
+ const before = await getBillingBalance(scopedDb, input.accountId, input.creditPolicyRevision);
5775
+ const plan = planCreditDebit(before, input.requestedAmountMicros, input.modelId);
5776
+ const candidateDebitMicros = plan.debitedMicros;
5777
+ // Keep a zero-cost receipt too: retrying after a top-up must not charge
5778
+ // a response that was already settled while the eligible balance was empty.
5779
+ const [inserted] = await scopedDb
5780
+ .insert(schema.creditLedgerEntries)
5781
+ .values({
5782
+ accountId: input.accountId,
5783
+ workspaceId: input.workspaceId ?? null,
5784
+ type: input.type,
5785
+ amountMicros: -candidateDebitMicros,
5786
+ sourceType: input.sourceType ?? null,
5787
+ sourceId: input.sourceId ?? null,
5788
+ idempotencyKey: input.idempotencyKey,
5789
+ metadata: {
5790
+ ...input.metadata,
5791
+ requestedAmountMicros: input.requestedAmountMicros,
5792
+ debitedMicros: candidateDebitMicros,
5793
+ creditPolicyRevision: before.creditPolicyRevision,
5794
+ },
5795
+ occurredAt: input.occurredAt ?? new Date(),
5796
+ })
5797
+ .onConflictDoNothing({
5798
+ target: schema.creditLedgerEntries.idempotencyKey,
5799
+ })
5800
+ .returning({ id: schema.creditLedgerEntries.id });
5801
+ const debitedMicros = inserted ? candidateDebitMicros : 0;
5802
+ if (inserted && plan.allocations.length) {
5803
+ await scopedDb.insert(schema.creditDebitAllocations).values(
5804
+ plan.allocations.map((allocation) => ({
5805
+ ...allocation,
5771
5806
  accountId: input.accountId,
5772
- workspaceId: input.workspaceId ?? null,
5773
- type: input.type,
5774
- amountMicros: -candidateDebitMicros,
5775
- sourceType: input.sourceType ?? null,
5776
- sourceId: input.sourceId ?? null,
5777
- idempotencyKey: input.idempotencyKey,
5778
- metadata: {
5779
- ...input.metadata,
5780
- requestedAmountMicros: input.requestedAmountMicros,
5781
- debitedMicros: candidateDebitMicros,
5782
- },
5783
- occurredAt: input.occurredAt ?? new Date(),
5784
- })
5785
- .onConflictDoNothing({
5786
- target: schema.creditLedgerEntries.idempotencyKey,
5787
- })
5788
- .returning({ id: schema.creditLedgerEntries.id });
5789
- debitedMicros = inserted.length === 1 ? candidateDebitMicros : 0;
5807
+ debitEntryId: inserted.id,
5808
+ })),
5809
+ );
5790
5810
  }
5791
5811
  return {
5792
5812
  balance: await getBillingBalance(scopedDb, input.accountId),
@@ -6019,23 +6039,6 @@ export async function markStripeWebhookProcessed(db: Database, id: string): Prom
6019
6039
  .where(eq(schema.stripeWebhookEvents.id, id));
6020
6040
  }
6021
6041
 
6022
- export async function getBillingBalance(db: Database, accountId: string): Promise<BillingBalance> {
6023
- return await withAccountRls(db, accountId, async (scopedDb) => {
6024
- const [{ balance } = { balance: 0 }] = await scopedDb
6025
- .select({
6026
- balance: sql<number>`coalesce(sum(${schema.creditLedgerEntries.amountMicros}), 0)`,
6027
- })
6028
- .from(schema.creditLedgerEntries)
6029
- .where(eq(schema.creditLedgerEntries.accountId, accountId));
6030
- return {
6031
- accountId,
6032
- balanceMicros: Number(balance),
6033
- currency: "usd",
6034
- updatedAt: new Date().toISOString(),
6035
- };
6036
- });
6037
- }
6038
-
6039
6042
  /**
6040
6043
  * Whether the organization holds a positive OpenGeni credit balance, whatever
6041
6044
  * its source: a purchase, an operator grant, a test credit, or the one-time
@@ -18575,6 +18578,7 @@ export type ScheduledTaskAdmissionRefusalReason =
18575
18578
  | "machine_enrollment_inactive"
18576
18579
  | "variable_set_unavailable"
18577
18580
  | "rig_version_unavailable"
18581
+ | "scheduled_model_unavailable"
18578
18582
  | "insufficient_credits"
18579
18583
  | "allowance_exhausted"
18580
18584
  | "monthly_model_cost_limit"
@@ -37685,11 +37689,6 @@ async function readSessionListForSubject(
37685
37689
  throw new SessionListAccessError();
37686
37690
  }
37687
37691
 
37688
- const tenancyActivated = await sessionTenancyProductActivated(
37689
- tx as unknown as Database,
37690
- workspaceId,
37691
- );
37692
-
37693
37692
  const archiveMode = options.archiveStatus ?? (options.archivedOnly ? "archived" : "active");
37694
37693
  const sortBy = options.sortBy ?? (archiveMode === "archived" ? "archivedAt" : "updatedAt");
37695
37694
  if (options.archivedOnly && archiveMode !== "archived") {
@@ -38139,7 +38138,7 @@ async function readSessionListForSubject(
38139
38138
  ? { archivedAt: exactArchiveTimestamps.get(session.id)! }
38140
38139
  : {}),
38141
38140
  },
38142
- { subjectId: options.subjectId, activated: tenancyActivated },
38141
+ { subjectId: options.subjectId },
38143
38142
  ),
38144
38143
  ...(sortBy === "updatedAt" && exactOrdinaryTimestamps.has(session.id)
38145
38144
  ? { updatedAt: exactOrdinaryTimestamps.get(session.id)! }
@@ -38395,7 +38394,6 @@ export async function getSessionForSubject(
38395
38394
  const mcpServers = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [
38396
38395
  sessionId,
38397
38396
  ]);
38398
- const tenancyActivated = await sessionTenancyProductActivated(scopedDb, workspaceId);
38399
38397
  const failureDiagnostics = await sessionFailureDiagnostics(scopedDb, workspaceId, session);
38400
38398
  return projectSessionForRelatedAccess(
38401
38399
  {
@@ -38407,7 +38405,7 @@ export async function getSessionForSubject(
38407
38405
  mapSessionAttention(session, row.pin),
38408
38406
  mapSessionArchive(row.pin),
38409
38407
  undefined,
38410
- { subjectId, activated: tenancyActivated },
38408
+ { subjectId },
38411
38409
  )),
38412
38410
  failureDiagnostics,
38413
38411
  },
@@ -44562,6 +44560,19 @@ export async function applyContextCompaction(
44562
44560
  ),
44563
44561
  );
44564
44562
  const supersededFrom = Math.floor(Number(maxPosition)) + 1;
44563
+ // Installing a successful summary is model progress, including a
44564
+ // maintenance turn that never enters the ordinary response stream.
44565
+ // Clear only provider recovery under the same locked attempt fence.
44566
+ if (providerRecoveryCountFromTurnMetadata(fence.turn!.metadata) > 0) {
44567
+ await tx
44568
+ .update(schema.sessionTurns)
44569
+ .set({
44570
+ metadata: metadataWithoutProviderRecoveryCount(fence.turn!.metadata),
44571
+ version: fence.turn!.version + 1,
44572
+ updatedAt: new Date(),
44573
+ })
44574
+ .where(eq(schema.sessionTurns.id, input.turnId));
44575
+ }
44565
44576
  await tx
44566
44577
  .update(schema.sessionHistoryItems)
44567
44578
  .set({ active: false })
@@ -47845,7 +47856,7 @@ async function acquireLeaseOnce(
47845
47856
  refusal,
47846
47857
  );
47847
47858
  }
47848
- const balance = await getBillingBalance(tx, accountId);
47859
+ const balance = await getSpendableCreditBalance(tx, accountId);
47849
47860
  if (balance.balanceMicros <= 0) {
47850
47861
  throw new SandboxPaidComputeAdmissionError(workspaceId, sandboxGroupId);
47851
47862
  }
@@ -52806,8 +52817,8 @@ type LostProviderBlockerScope = {
52806
52817
  };
52807
52818
 
52808
52819
  /** Take the session-event write prefix (workspace control share, workspace,
52809
- * sessions, cursors) for every session whose background command is linked to
52810
- * an active process in the exact lost-provider scope. Call it before the
52820
+ * sessions, cursors) for linked background commands and attempt owners in the
52821
+ * exact lost-provider scope. Call it before the
52811
52822
  * process/admission/PTY/lease locks so terminal command delivery keeps the
52812
52823
  * canonical control -> session -> process -> admission -> lease order. */
52813
52824
  async function linkedLostProviderCommandSessionIdsTx(
@@ -52817,7 +52828,8 @@ async function linkedLostProviderCommandSessionIdsTx(
52817
52828
  const sessions = await rawRows<{ session_id: string }>(
52818
52829
  tx,
52819
52830
  sql`
52820
- select distinct command.session_id
52831
+ select distinct session_id from (
52832
+ select command.session_id
52821
52833
  from session_background_commands command
52822
52834
  join sandbox_retained_processes process on process.id = command.retained_process_id
52823
52835
  and process.workspace_id = command.workspace_id
@@ -52831,7 +52843,21 @@ async function linkedLostProviderCommandSessionIdsTx(
52831
52843
  and process.provider_instance_id = ${input.lostInstanceId}
52832
52844
  and process.state = 'active'
52833
52845
  and command.state in ('running', 'stopping')
52834
- order by command.session_id
52846
+ union
52847
+ select process.session_id
52848
+ from sandbox_retained_processes process
52849
+ join session_turn_attempts attempt on attempt.id = process.owner_attempt_id
52850
+ and attempt.workspace_id = process.workspace_id
52851
+ and attempt.session_id = process.session_id
52852
+ where process.account_id = ${input.accountId}
52853
+ and process.workspace_id = ${input.workspaceId}
52854
+ and process.lease_id = ${input.leaseId}
52855
+ and process.sandbox_group_id = ${input.sandboxGroupId}
52856
+ and process.lease_epoch = ${input.lostEpoch}
52857
+ and process.provider_backend = ${input.lostBackend}
52858
+ and process.provider_instance_id = ${input.lostInstanceId}
52859
+ and process.state = 'active'
52860
+ ) owners order by session_id
52835
52861
  `,
52836
52862
  );
52837
52863
  return sessions.map((row) => row.session_id);
@@ -53053,6 +53079,37 @@ async function settleExactLostProviderWorkspaceBlockersTx(
53053
53079
  }
53054
53080
  }
53055
53081
 
53082
+ if (lostProcesses.length) {
53083
+ // The session prefix was locked before blockers and revalidated afterward.
53084
+ // Unadopted writers have no background-command delivery to wake their owner;
53085
+ // persist that obligation atomically with physical provider settlement.
53086
+ const owners = await rawRows<{ session_id: string; temporal_workflow_id: string }>(
53087
+ tx,
53088
+ sql`
53089
+ select distinct attempt.session_id, attempt.temporal_workflow_id
53090
+ from session_turn_attempts attempt
53091
+ join sandbox_retained_processes process on process.owner_attempt_id = attempt.id
53092
+ and process.workspace_id = attempt.workspace_id
53093
+ and process.session_id = attempt.session_id
53094
+ where attempt.workspace_id = ${input.workspaceId}
53095
+ and attempt.account_id = ${input.accountId}
53096
+ and attempt.state = 'closed' and attempt.quiesced_at is null
53097
+ and attempt.temporal_workflow_id is not null
53098
+ and process.id = any(${`{${lostProcesses.map((p) => p.id).join(",")}}`}::uuid[])
53099
+ order by attempt.session_id, attempt.temporal_workflow_id
53100
+ `,
53101
+ );
53102
+ for (const owner of owners) {
53103
+ await enqueueSessionWorkflowWakeInTransaction(tx, {
53104
+ accountId: input.accountId,
53105
+ workspaceId: input.workspaceId,
53106
+ sessionId: owner.session_id,
53107
+ temporalWorkflowId: owner.temporal_workflow_id,
53108
+ reason: "attempt_writer_provider_settled",
53109
+ });
53110
+ }
53111
+ }
53112
+
53056
53113
  return {
53057
53114
  processesLost: lostProcesses.length,
53058
53115
  admissionsRejected: rejectedAdmissions.length,
@@ -53939,7 +53996,7 @@ export async function heartbeatLeaseHolderStatus(
53939
53996
  (input.billingMode ?? snapshot?.mode) === "credits" &&
53940
53997
  snapshot?.mode === "credits" &&
53941
53998
  snapshot.rateMicrosPerSecond > 0 &&
53942
- (await getBillingBalance(tx, input.accountId)).balanceMicros <= 0
53999
+ (await getSpendableCreditBalance(tx, input.accountId)).balanceMicros <= 0
53943
54000
  ) {
53944
54001
  // Touching the holder above is essential: the running writer remains
53945
54002
  // protected while it winds down. The provider/lease TTL is not renewed.
@@ -54595,23 +54652,69 @@ export async function reapStaleLeaseHolders(
54595
54652
  }
54596
54653
 
54597
54654
  /** Settlement reasons for legacy retained commands that containment stopped
54598
- * after a verified checkpoint. Neither is exit proof: the command settles
54655
+ * after a verified checkpoint. None is exit proof: the command settles
54599
54656
  * `lost` with no exit code. */
54600
54657
  export const IDLE_COMMAND_CONTAINMENT_REASON = "idle_containment";
54601
54658
  export const DEADLINE_COMMAND_CONTAINMENT_REASON = "provider_deadline_containment";
54659
+ export const QUIESCENCE_COMMAND_CONTAINMENT_REASON = "quiescence_containment";
54660
+
54661
+ type SettledCommandOwner = {
54662
+ sessionId: string;
54663
+ attemptId: string;
54664
+ temporalWorkflowId: string;
54665
+ temporalWorkflowRunId: string;
54666
+ temporalActivityId: string;
54667
+ };
54668
+
54669
+ /** Exact dispatch settlement is enrollment authority, never physical proof. */
54670
+ async function settledCommandOwnerMatchesTx(
54671
+ tx: Database,
54672
+ input: { accountId: string; workspaceId: string; sandboxGroupId: string },
54673
+ owner: SettledCommandOwner,
54674
+ ): Promise<boolean> {
54675
+ const [row] = await rawRows<{ eligible: boolean }>(
54676
+ tx,
54677
+ sql`
54678
+ select exists (
54679
+ select 1 from session_turn_attempts attempt
54680
+ join sessions session on session.id = attempt.session_id
54681
+ and session.workspace_id = attempt.workspace_id
54682
+ where attempt.account_id = ${input.accountId}
54683
+ and attempt.workspace_id = ${input.workspaceId}
54684
+ and attempt.session_id = ${owner.sessionId} and attempt.id = ${owner.attemptId}
54685
+ and attempt.temporal_workflow_id = ${owner.temporalWorkflowId}
54686
+ and attempt.temporal_workflow_run_id = ${owner.temporalWorkflowRunId}
54687
+ and attempt.temporal_activity_id = ${owner.temporalActivityId}
54688
+ and attempt.state = 'closed' and attempt.quiesced_at is null
54689
+ and session.sandbox_group_id = ${input.sandboxGroupId}
54690
+ and not exists (select 1 from session_attempt_interruptions interruption
54691
+ where interruption.workspace_id = attempt.workspace_id
54692
+ and interruption.session_id = attempt.session_id and interruption.attempt_id = attempt.id
54693
+ and interruption.state in ('pending', 'delivered', 'acknowledged'))
54694
+ ) and not exists (
54695
+ select 1 from session_turn_attempts live
54696
+ join sessions member on member.id = live.session_id and member.workspace_id = live.workspace_id
54697
+ where live.workspace_id = ${input.workspaceId}
54698
+ and member.sandbox_group_id = ${input.sandboxGroupId} and live.state <> 'closed'
54699
+ ) as eligible
54700
+ `,
54701
+ );
54702
+ return row?.eligible === true;
54703
+ }
54602
54704
 
54603
54705
  /** Bounded per-candidate containment inspection outcomes (metric labels). */
54604
54706
  export type CommandContainmentInspection =
54605
54707
  | "idle_enrolled"
54606
54708
  | "deadline_enrolled"
54709
+ | "quiescence_enrolled"
54607
54710
  | "resumed_enrolled"
54608
54711
  | "not_eligible"
54609
54712
  | "inspection_failed";
54610
54713
 
54611
54714
  export type CommandContainmentEnrollment = ReapDrainable & {
54612
- /** `idle`/`deadline` newly enrolled this call; `resumed` continues an
54715
+ /** `idle`/`deadline`/`quiescence` newly enrolled this call; `resumed` continues an
54613
54716
  * existing enrollment whose drain has not committed yet. */
54614
- mode: "idle" | "deadline" | "resumed";
54717
+ mode: "idle" | "deadline" | "quiescence" | "resumed";
54615
54718
  };
54616
54719
 
54617
54720
  /** Whole-group "unused" from durable facts only. Every session of the group,
@@ -54814,6 +54917,8 @@ export async function enrollRetainedCommandContainment(
54814
54917
  sandboxGroupId: string;
54815
54918
  /** Omit to evaluate only the provider-deadline rule. */
54816
54919
  idleCommandContainmentMs?: number | undefined;
54920
+ /** Internal recovery authority: caller verified exact Temporal settlement. */
54921
+ settledOwner?: SettledCommandOwner;
54817
54922
  },
54818
54923
  ): Promise<CommandContainmentEnrollment | null> {
54819
54924
  const screened = await withRlsContext(db, input, async (tx) => {
@@ -54826,6 +54931,8 @@ export async function enrollRetainedCommandContainment(
54826
54931
  // Enrolled drains resume; any requested rotation also refreshes command
54827
54932
  // observation backoff below and may meet the deadline rule.
54828
54933
  (Boolean(lease.unobservableCommandDrainIds?.length) ||
54934
+ (input.settledOwner !== undefined &&
54935
+ (await settledCommandOwnerMatchesTx(tx, input, input.settledOwner))) ||
54829
54936
  lease.rotationRequestedAt !== null ||
54830
54937
  (input.idleCommandContainmentMs !== undefined &&
54831
54938
  (await sandboxGroupIdleForCommandContainmentTx(tx, {
@@ -54875,11 +54982,17 @@ export async function enrollRetainedCommandContainment(
54875
54982
  supervised: boolean;
54876
54983
  owned: boolean;
54877
54984
  deadline_ready: boolean;
54985
+ adopted: boolean;
54878
54986
  }>(
54879
54987
  tx,
54880
54988
  sql`
54881
54989
  select process.id, process.session_id, process.owner_attempt_id,
54882
54990
  process.parent_admission_id, process.holder_id,
54991
+ exists (select 1 from session_background_commands background
54992
+ where background.retained_process_id = process.id
54993
+ and background.workspace_id = process.workspace_id
54994
+ and background.session_id = process.session_id
54995
+ and background.state in ('running', 'stopping')) as adopted,
54883
54996
  (coalesce(process.provider_command, '{}'::jsonb) ? 'supervision') as supervised,
54884
54997
  (process.lease_epoch = ${initial.leaseEpoch}
54885
54998
  and process.provider_instance_id = ${initial.instanceId}
@@ -54953,8 +55066,19 @@ export async function enrollRetainedCommandContainment(
54953
55066
  };
54954
55067
  if (enrolled) return { ...target, mode: "resumed" };
54955
55068
  if (lease.archive_capture_id !== null) return null;
54956
- let mode: "idle" | "deadline" | null = null;
55069
+ let mode: "idle" | "deadline" | "quiescence" | null = null;
54957
55070
  if (
55071
+ input.settledOwner !== undefined &&
55072
+ processes.every(
55073
+ (p) =>
55074
+ p.owner_attempt_id === input.settledOwner!.attemptId &&
55075
+ p.session_id === input.settledOwner!.sessionId &&
55076
+ !p.adopted,
55077
+ ) &&
55078
+ (await settledCommandOwnerMatchesTx(tx, input, input.settledOwner))
55079
+ ) {
55080
+ mode = "quiescence";
55081
+ } else if (
54958
55082
  deadlineRotation &&
54959
55083
  processes.every((p) => p.deadline_ready) &&
54960
55084
  !(await deadlineOwnerQuiescencePending(tx, input.workspaceId, processes)) &&
@@ -54978,9 +55102,11 @@ export async function enrollRetainedCommandContainment(
54978
55102
  await tx.execute(sql`
54979
55103
  update sandbox_leases set unobservable_command_drain_ids = ${`{${ids.join(",")}}`}::uuid[],
54980
55104
  command_containment_reason = ${
54981
- mode === "deadline"
54982
- ? DEADLINE_COMMAND_CONTAINMENT_REASON
54983
- : IDLE_COMMAND_CONTAINMENT_REASON
55105
+ mode === "quiescence"
55106
+ ? QUIESCENCE_COMMAND_CONTAINMENT_REASON
55107
+ : mode === "deadline"
55108
+ ? DEADLINE_COMMAND_CONTAINMENT_REASON
55109
+ : IDLE_COMMAND_CONTAINMENT_REASON
54984
55110
  },
54985
55111
  liveness = 'draining', rotation_requested_at = coalesce(rotation_requested_at, now()),
54986
55112
  rotation_reason = coalesce(rotation_reason, 'operator'), expires_at = now(), updated_at = now()
@@ -55336,7 +55462,7 @@ export async function reArmDrainingLease(
55336
55462
  ? snapshot.mode === "credits" && snapshot.rateMicrosPerSecond > 0
55337
55463
  : input.warmBilling?.rateMicrosPerSecond !== undefined &&
55338
55464
  input.warmBilling.rateMicrosPerSecond > 0) &&
55339
- (await getBillingBalance(tx, input.accountId)).balanceMicros <= 0
55465
+ (await getSpendableCreditBalance(tx, input.accountId)).balanceMicros <= 0
55340
55466
  ) {
55341
55467
  throw new SandboxPaidComputeAdmissionError(input.workspaceId, input.sandboxGroupId);
55342
55468
  }
@@ -55520,7 +55646,8 @@ export async function confirmDrainCold(
55520
55646
  !input.providerMissingBeforeCapture &&
55521
55647
  row.archive_capture_published_at !== null &&
55522
55648
  (reason === IDLE_COMMAND_CONTAINMENT_REASON ||
55523
- reason === DEADLINE_COMMAND_CONTAINMENT_REASON) &&
55649
+ reason === DEADLINE_COMMAND_CONTAINMENT_REASON ||
55650
+ reason === QUIESCENCE_COMMAND_CONTAINMENT_REASON) &&
55524
55651
  row.unobservable_command_drain_ids?.length
55525
55652
  ? row.unobservable_command_drain_ids
55526
55653
  : [];
@@ -68341,6 +68468,16 @@ export async function updateSessionTitleWithEvent(
68341
68468
  );
68342
68469
  }
68343
68470
 
68471
+ /** Read-only admission at a locked paused-to-active transition. */
68472
+ export type SessionGoalResumeValidation = (
68473
+ tx: Database,
68474
+ session: Pick<
68475
+ Session,
68476
+ "accountId" | "workspaceId" | "model" | "codexCompactionMode" | "latencyMode"
68477
+ >,
68478
+ causalTurn: { id: string; initiatingHumanSubjectId: string | null } | null,
68479
+ ) => Promise<void>;
68480
+
68344
68481
  /**
68345
68482
  * Status transition helper. Idempotent: requesting the current status returns
68346
68483
  * `changed: false` so callers can skip emitting a duplicate event. `completed`
@@ -68360,6 +68497,8 @@ export async function setSessionGoalStatus(
68360
68497
  reportDeliveries?: SessionGoalReportDelivery[];
68361
68498
  reportArtifactActor?: ReportArtifactActor;
68362
68499
  commandActor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
68500
+ /** Runs under the canonical control/session/goal locks, before a paused goal changes. */
68501
+ beforeResume?: SessionGoalResumeValidation;
68363
68502
  },
68364
68503
  ): Promise<{
68365
68504
  goal: SessionGoal;
@@ -68409,6 +68548,51 @@ export async function setSessionGoalStatus(
68409
68548
  if (existing.status === "completed") {
68410
68549
  throw new Error("session goal is completed; set a new goal to continue");
68411
68550
  }
68551
+ if (input.status === "active" && input.beforeResume) {
68552
+ const [effectiveSession] = await withEffectiveSessionPolicy(scopedDb, workspaceId, [session]);
68553
+ // An accepted running turn owns the work the goal will continue after it
68554
+ // settles. An idle Resume uses the exact latest-finished causal row.
68555
+ const causalTurnFilter = session.activeTurnId
68556
+ ? or(
68557
+ eq(schema.sessionTurns.id, session.activeTurnId),
68558
+ sql`${schema.sessionTurns.finishedAt} is not null`,
68559
+ )
68560
+ : sql`${schema.sessionTurns.finishedAt} is not null`;
68561
+ const [causalTurn] = await scopedDb
68562
+ .select({
68563
+ id: schema.sessionTurns.id,
68564
+ initiatingHumanSubjectId: schema.sessionTurns.initiatingHumanSubjectId,
68565
+ })
68566
+ .from(schema.sessionTurns)
68567
+ .where(
68568
+ and(
68569
+ eq(schema.sessionTurns.workspaceId, workspaceId),
68570
+ eq(schema.sessionTurns.sessionId, sessionId),
68571
+ causalTurnFilter,
68572
+ ),
68573
+ )
68574
+ .orderBy(
68575
+ ...(session.activeTurnId
68576
+ ? [desc(sql`${schema.sessionTurns.id} = ${session.activeTurnId}`)]
68577
+ : []),
68578
+ desc(schema.sessionTurns.finishedAt),
68579
+ desc(schema.sessionTurns.position),
68580
+ desc(schema.sessionTurns.createdAt),
68581
+ desc(schema.sessionTurns.id),
68582
+ )
68583
+ .limit(1);
68584
+ await input.beforeResume(
68585
+ scopedDb,
68586
+ {
68587
+ accountId: session.accountId,
68588
+ workspaceId,
68589
+ model: effectiveSession!.model,
68590
+ latencyMode: effectiveSession!.latencyMode as Session["latencyMode"],
68591
+ codexCompactionMode: session.codexCompactionMode as Session["codexCompactionMode"],
68592
+ },
68593
+ causalTurn ?? null,
68594
+ );
68595
+ }
68412
68596
  if (input.status === "completed") {
68413
68597
  await verifyGoalReportDeliveries(scopedDb, {
68414
68598
  accountId: session.accountId,
@@ -68502,6 +68686,7 @@ export async function setSessionGoalStatusWithEvent(
68502
68686
  reportDeliveries?: SessionGoalReportDelivery[];
68503
68687
  reportArtifactActor?: ReportArtifactActor;
68504
68688
  commandActor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
68689
+ beforeResume?: SessionGoalResumeValidation;
68505
68690
  event: SetSessionGoalStatusEvent;
68506
68691
  },
68507
68692
  ): Promise<{
@@ -68535,6 +68720,7 @@ export async function setSessionGoalStatusWithEvent(
68535
68720
  : {}),
68536
68721
  ...(input.reportArtifactActor ? { reportArtifactActor: input.reportArtifactActor } : {}),
68537
68722
  ...(input.commandActor ? { commandActor: input.commandActor } : {}),
68723
+ ...(input.beforeResume ? { beforeResume: input.beforeResume } : {}),
68538
68724
  ...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
68539
68725
  ...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
68540
68726
  ...(input.pausedReason !== undefined ? { pausedReason: input.pausedReason } : {}),
@@ -68633,7 +68819,7 @@ export type GoalContinuationDecision =
68633
68819
  | { decision: "queue" }
68634
68820
  | {
68635
68821
  decision: "paused";
68636
- reason: "max_auto_continuations" | "limits" | "allowance";
68822
+ reason: "max_auto_continuations" | "limits" | GoalAdmissionPausedReason;
68637
68823
  goal: SessionGoal;
68638
68824
  }
68639
68825
  | {
@@ -68712,7 +68898,7 @@ export async function evaluateGoalContinuation(
68712
68898
  // decision (before the counter bump) so a budget pause never consumes
68713
68899
  // continuation budget.
68714
68900
  budgetBlocked?: string | null;
68715
- budgetPausedReason?: "limits" | "allowance" | undefined;
68901
+ budgetPausedReason?: "limits" | GoalAdmissionPausedReason | undefined;
68716
68902
  },
68717
68903
  ): Promise<GoalContinuationDecision> {
68718
68904
  return await withWorkspaceRls(
@@ -68974,7 +69160,7 @@ export async function materializeGoalContinuation(
68974
69160
  workflowId: string;
68975
69161
  defaultMaxAutoContinuations?: number | null;
68976
69162
  budgetBlocked?: string | null;
68977
- budgetPausedReason?: "limits" | "allowance" | undefined;
69163
+ budgetPausedReason?: "limits" | GoalAdmissionPausedReason | undefined;
68978
69164
  /** Trusted worker admission, evaluated under the same session/goal locks
68979
69165
  * as lineage materialization. Never substitutes a mutable latest human. */
68980
69166
  admission?: (
@@ -68982,7 +69168,7 @@ export async function materializeGoalContinuation(
68982
69168
  causalTurn: { id: string; initiatingHumanSubjectId: string | null } | null,
68983
69169
  ) => Promise<{
68984
69170
  budgetBlocked: string | null;
68985
- budgetPausedReason?: "limits" | "allowance";
69171
+ budgetPausedReason?: "limits" | GoalAdmissionPausedReason;
68986
69172
  }>;
68987
69173
  idleBackoff?: GoalIdleBackoffPolicy | null;
68988
69174
  policy: {
@@ -75299,6 +75485,28 @@ export async function reconcileSessionAttemptQuiescence(
75299
75485
  };
75300
75486
  }
75301
75487
  if (!input.activitySettled || eligibility.writer_pending) {
75488
+ if (input.activitySettled && eligibility.writer_pending) {
75489
+ const group = await withRlsContext(db, input, async (tx) => {
75490
+ const [row] = await tx
75491
+ .select({ id: schema.sessions.sandboxGroupId })
75492
+ .from(schema.sessions)
75493
+ .where(
75494
+ and(
75495
+ eq(schema.sessions.workspaceId, input.workspaceId),
75496
+ eq(schema.sessions.id, input.sessionId),
75497
+ ),
75498
+ )
75499
+ .limit(1);
75500
+ return row?.id;
75501
+ });
75502
+ if (group)
75503
+ await enrollRetainedCommandContainment(db, {
75504
+ accountId: input.accountId,
75505
+ workspaceId: input.workspaceId,
75506
+ sandboxGroupId: group,
75507
+ settledOwner: input,
75508
+ });
75509
+ }
75302
75510
  return { action: "pending", events: [] };
75303
75511
  }
75304
75512
  const events = await markSessionAttemptQuiesced(db, {
@@ -78873,6 +79081,8 @@ function metadataWithoutProviderRecoveryCount(
78873
79081
  ): Record<string, unknown> {
78874
79082
  const next = { ...(metadata ?? {}) };
78875
79083
  delete next.providerRecoveryCount;
79084
+ delete next.providerRecoveryStartedAt;
79085
+ delete next.providerRecoveryReason;
78876
79086
  return next;
78877
79087
  }
78878
79088
 
@@ -81315,7 +81525,15 @@ export async function requestSessionTurnRecovery(
81315
81525
  }
81316
81526
  : {}),
81317
81527
  ...(input.providerRecoveryCount !== undefined
81318
- ? { providerRecoveryCount: input.providerRecoveryCount }
81528
+ ? {
81529
+ providerRecoveryCount: input.providerRecoveryCount,
81530
+ providerRecoveryStartedAt:
81531
+ providerRecoveryCountFromTurnMetadata(turn.metadata) > 0 &&
81532
+ typeof turn.metadata?.providerRecoveryStartedAt === "string"
81533
+ ? turn.metadata.providerRecoveryStartedAt
81534
+ : now.toISOString(),
81535
+ providerRecoveryReason: input.reason,
81536
+ }
81319
81537
  : {}),
81320
81538
  },
81321
81539
  updatedAt: now,
@@ -86759,7 +86977,7 @@ async function mapSessionWithControl(
86759
86977
  },
86760
86978
  archive: Pick<Session, "archived" | "archivedAt" | "archiveVersion"> = mapSessionArchive(null),
86761
86979
  workspaceControl?: WorkspaceControlRow,
86762
- tenancyViewer?: { subjectId: string; activated: boolean },
86980
+ tenancyViewer?: { subjectId: string },
86763
86981
  ): Promise<Session> {
86764
86982
  const controls = await sessionControlProjections(db, row.workspaceId, [row.id], workspaceControl);
86765
86983
  const control = controls.get(row.id);
@@ -86896,7 +87114,7 @@ function mapSession(
86896
87114
  attentionVersion: 0,
86897
87115
  },
86898
87116
  archive: Pick<Session, "archived" | "archivedAt" | "archiveVersion"> = mapSessionArchive(null),
86899
- tenancyViewer?: { subjectId: string; activated: boolean },
87117
+ tenancyViewer?: { subjectId: string },
86900
87118
  ): Session {
86901
87119
  const bundledSkillIds = bundledSkillSelectionFromMetadata(row.metadata);
86902
87120
  return {
@@ -86930,9 +87148,7 @@ function mapSession(
86930
87148
  toolPolicyVersion: Number(row.toolPolicyVersion),
86931
87149
  mcpApprovalPolicies: row.mcpApprovalPolicies,
86932
87150
  metadata: row.metadata,
86933
- ...(tenancyViewer?.activated
86934
- ? { tenancy: mapSessionTenancy(row, tenancyViewer.subjectId) }
86935
- : {}),
87151
+ ...(tenancyViewer ? { tenancy: mapSessionTenancy(row, tenancyViewer.subjectId) } : {}),
86936
87152
  createdBy: initiatorFromStorage(
86937
87153
  row.createdByKind,
86938
87154
  row.createdBySubjectId,