@opengeni/db 1.1.0 → 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 (77) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/capability-reconciliation.js +2 -2
  3. package/dist/{chunk-J4WFOVBV.js → chunk-3GAR5UN3.js} +2 -2
  4. package/dist/{chunk-Z5HJXH3X.js → chunk-3TGYFFZR.js} +2 -2
  5. package/dist/{chunk-GLA5YCQS.js → chunk-6EIEGHPK.js} +4 -4
  6. package/dist/{chunk-BLUQG54M.js → chunk-CONWNUSL.js} +2 -2
  7. package/dist/{chunk-EKXCF3FH.js → chunk-CYP72JTQ.js} +11 -4
  8. package/dist/{chunk-EKXCF3FH.js.map → chunk-CYP72JTQ.js.map} +1 -1
  9. package/dist/{chunk-VOB6TXSJ.js → chunk-FWQNHNOU.js} +39 -2
  10. package/dist/chunk-FWQNHNOU.js.map +1 -0
  11. package/dist/{chunk-7Z7ALFRE.js → chunk-GDN5TSZP.js} +2 -2
  12. package/dist/{chunk-M3SWH4MU.js → chunk-JICUP7ZR.js} +2 -2
  13. package/dist/{chunk-KJVOBLFC.js → chunk-JLVWBIEM.js} +3 -3
  14. package/dist/{chunk-BZV5ZDRY.js → chunk-LXPFNG4L.js} +2 -2
  15. package/dist/{chunk-SKYRM7R7.js → chunk-O2Y5E5M3.js} +68 -32
  16. package/dist/chunk-O2Y5E5M3.js.map +1 -0
  17. package/dist/{chunk-M2YGXCCQ.js → chunk-QFJNEXFL.js} +2 -2
  18. package/dist/{chunk-CIGHXFPD.js → chunk-UCIMHNII.js} +101 -21
  19. package/dist/chunk-UCIMHNII.js.map +1 -0
  20. package/dist/{chunk-ODKYBA3C.js → chunk-VDPCIFKL.js} +5 -5
  21. package/dist/{chunk-T5NHDEZW.js → chunk-YOYILDLY.js} +26 -1
  22. package/dist/chunk-YOYILDLY.js.map +1 -0
  23. package/dist/connect-attempts.js +3 -3
  24. package/dist/credit-balances.d.ts +18 -0
  25. package/dist/credit-promotion-policy.d.ts +10 -0
  26. package/dist/editable-artifact-durable-export.js +2 -2
  27. package/dist/editable-artifacts.js +3 -3
  28. package/dist/index.d.ts +21 -5
  29. package/dist/index.js +2369 -2247
  30. package/dist/index.js.map +1 -1
  31. package/dist/managed-auth-session-sets.js +3 -3
  32. package/dist/managed-user-setup.d.ts +1 -0
  33. package/dist/mcp-operations.js +2 -2
  34. package/dist/organization-integration-policy.js +3 -3
  35. package/dist/organization-membership-lifecycle.d.ts +1 -0
  36. package/dist/persistence-errors.d.ts +9 -6
  37. package/dist/provision-roles.js +1 -1
  38. package/dist/retained-provider-commands.js +6 -6
  39. package/dist/runtime-posture.d.ts +4 -4
  40. package/dist/schema.d.ts +121 -2
  41. package/dist/schema.js +3 -1
  42. package/dist/session-background-commands.js +4 -4
  43. package/dist/session-command-output.js +5 -5
  44. package/dist/session-event-slices.js +3 -3
  45. package/dist/session-mcp-credential-rotation.js +5 -5
  46. package/dist/session-tenancy.js +3 -3
  47. package/dist/video-generation.js +3 -3
  48. package/dist/workspace-tool-defaults.js +4 -4
  49. package/drizzle/0613_model_scoped_promotional_credits.sql +238 -0
  50. package/drizzle/0614_quiescence_command_containment.sql +7 -0
  51. package/drizzle/0615_credit_promotion_policy_validation.sql +63 -0
  52. package/package.json +7 -7
  53. package/src/credit-balances.ts +109 -0
  54. package/src/credit-promotion-policy.ts +12 -0
  55. package/src/database.ts +81 -42
  56. package/src/index.ts +201 -60
  57. package/src/managed-user-setup.ts +8 -0
  58. package/src/organization-membership-lifecycle.ts +8 -0
  59. package/src/persistence-errors.ts +17 -6
  60. package/src/provision-roles.ts +9 -0
  61. package/src/runtime-posture.ts +22 -0
  62. package/src/schema.ts +40 -1
  63. package/src/video-generation.ts +2 -7
  64. package/dist/chunk-CIGHXFPD.js.map +0 -1
  65. package/dist/chunk-SKYRM7R7.js.map +0 -1
  66. package/dist/chunk-T5NHDEZW.js.map +0 -1
  67. package/dist/chunk-VOB6TXSJ.js.map +0 -1
  68. /package/dist/{chunk-J4WFOVBV.js.map → chunk-3GAR5UN3.js.map} +0 -0
  69. /package/dist/{chunk-Z5HJXH3X.js.map → chunk-3TGYFFZR.js.map} +0 -0
  70. /package/dist/{chunk-GLA5YCQS.js.map → chunk-6EIEGHPK.js.map} +0 -0
  71. /package/dist/{chunk-BLUQG54M.js.map → chunk-CONWNUSL.js.map} +0 -0
  72. /package/dist/{chunk-7Z7ALFRE.js.map → chunk-GDN5TSZP.js.map} +0 -0
  73. /package/dist/{chunk-M3SWH4MU.js.map → chunk-JICUP7ZR.js.map} +0 -0
  74. /package/dist/{chunk-KJVOBLFC.js.map → chunk-JLVWBIEM.js.map} +0 -0
  75. /package/dist/{chunk-BZV5ZDRY.js.map → chunk-LXPFNG4L.js.map} +0 -0
  76. /package/dist/{chunk-M2YGXCCQ.js.map → chunk-QFJNEXFL.js.map} +0 -0
  77. /package/dist/{chunk-ODKYBA3C.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,
@@ -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
@@ -47853,7 +47856,7 @@ async function acquireLeaseOnce(
47853
47856
  refusal,
47854
47857
  );
47855
47858
  }
47856
- const balance = await getBillingBalance(tx, accountId);
47859
+ const balance = await getSpendableCreditBalance(tx, accountId);
47857
47860
  if (balance.balanceMicros <= 0) {
47858
47861
  throw new SandboxPaidComputeAdmissionError(workspaceId, sandboxGroupId);
47859
47862
  }
@@ -52814,8 +52817,8 @@ type LostProviderBlockerScope = {
52814
52817
  };
52815
52818
 
52816
52819
  /** Take the session-event write prefix (workspace control share, workspace,
52817
- * sessions, cursors) for every session whose background command is linked to
52818
- * 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
52819
52822
  * process/admission/PTY/lease locks so terminal command delivery keeps the
52820
52823
  * canonical control -> session -> process -> admission -> lease order. */
52821
52824
  async function linkedLostProviderCommandSessionIdsTx(
@@ -52825,7 +52828,8 @@ async function linkedLostProviderCommandSessionIdsTx(
52825
52828
  const sessions = await rawRows<{ session_id: string }>(
52826
52829
  tx,
52827
52830
  sql`
52828
- select distinct command.session_id
52831
+ select distinct session_id from (
52832
+ select command.session_id
52829
52833
  from session_background_commands command
52830
52834
  join sandbox_retained_processes process on process.id = command.retained_process_id
52831
52835
  and process.workspace_id = command.workspace_id
@@ -52839,7 +52843,21 @@ async function linkedLostProviderCommandSessionIdsTx(
52839
52843
  and process.provider_instance_id = ${input.lostInstanceId}
52840
52844
  and process.state = 'active'
52841
52845
  and command.state in ('running', 'stopping')
52842
- 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
52843
52861
  `,
52844
52862
  );
52845
52863
  return sessions.map((row) => row.session_id);
@@ -53061,6 +53079,37 @@ async function settleExactLostProviderWorkspaceBlockersTx(
53061
53079
  }
53062
53080
  }
53063
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
+
53064
53113
  return {
53065
53114
  processesLost: lostProcesses.length,
53066
53115
  admissionsRejected: rejectedAdmissions.length,
@@ -53947,7 +53996,7 @@ export async function heartbeatLeaseHolderStatus(
53947
53996
  (input.billingMode ?? snapshot?.mode) === "credits" &&
53948
53997
  snapshot?.mode === "credits" &&
53949
53998
  snapshot.rateMicrosPerSecond > 0 &&
53950
- (await getBillingBalance(tx, input.accountId)).balanceMicros <= 0
53999
+ (await getSpendableCreditBalance(tx, input.accountId)).balanceMicros <= 0
53951
54000
  ) {
53952
54001
  // Touching the holder above is essential: the running writer remains
53953
54002
  // protected while it winds down. The provider/lease TTL is not renewed.
@@ -54603,23 +54652,69 @@ export async function reapStaleLeaseHolders(
54603
54652
  }
54604
54653
 
54605
54654
  /** Settlement reasons for legacy retained commands that containment stopped
54606
- * after a verified checkpoint. Neither is exit proof: the command settles
54655
+ * after a verified checkpoint. None is exit proof: the command settles
54607
54656
  * `lost` with no exit code. */
54608
54657
  export const IDLE_COMMAND_CONTAINMENT_REASON = "idle_containment";
54609
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
+ }
54610
54704
 
54611
54705
  /** Bounded per-candidate containment inspection outcomes (metric labels). */
54612
54706
  export type CommandContainmentInspection =
54613
54707
  | "idle_enrolled"
54614
54708
  | "deadline_enrolled"
54709
+ | "quiescence_enrolled"
54615
54710
  | "resumed_enrolled"
54616
54711
  | "not_eligible"
54617
54712
  | "inspection_failed";
54618
54713
 
54619
54714
  export type CommandContainmentEnrollment = ReapDrainable & {
54620
- /** `idle`/`deadline` newly enrolled this call; `resumed` continues an
54715
+ /** `idle`/`deadline`/`quiescence` newly enrolled this call; `resumed` continues an
54621
54716
  * existing enrollment whose drain has not committed yet. */
54622
- mode: "idle" | "deadline" | "resumed";
54717
+ mode: "idle" | "deadline" | "quiescence" | "resumed";
54623
54718
  };
54624
54719
 
54625
54720
  /** Whole-group "unused" from durable facts only. Every session of the group,
@@ -54822,6 +54917,8 @@ export async function enrollRetainedCommandContainment(
54822
54917
  sandboxGroupId: string;
54823
54918
  /** Omit to evaluate only the provider-deadline rule. */
54824
54919
  idleCommandContainmentMs?: number | undefined;
54920
+ /** Internal recovery authority: caller verified exact Temporal settlement. */
54921
+ settledOwner?: SettledCommandOwner;
54825
54922
  },
54826
54923
  ): Promise<CommandContainmentEnrollment | null> {
54827
54924
  const screened = await withRlsContext(db, input, async (tx) => {
@@ -54834,6 +54931,8 @@ export async function enrollRetainedCommandContainment(
54834
54931
  // Enrolled drains resume; any requested rotation also refreshes command
54835
54932
  // observation backoff below and may meet the deadline rule.
54836
54933
  (Boolean(lease.unobservableCommandDrainIds?.length) ||
54934
+ (input.settledOwner !== undefined &&
54935
+ (await settledCommandOwnerMatchesTx(tx, input, input.settledOwner))) ||
54837
54936
  lease.rotationRequestedAt !== null ||
54838
54937
  (input.idleCommandContainmentMs !== undefined &&
54839
54938
  (await sandboxGroupIdleForCommandContainmentTx(tx, {
@@ -54883,11 +54982,17 @@ export async function enrollRetainedCommandContainment(
54883
54982
  supervised: boolean;
54884
54983
  owned: boolean;
54885
54984
  deadline_ready: boolean;
54985
+ adopted: boolean;
54886
54986
  }>(
54887
54987
  tx,
54888
54988
  sql`
54889
54989
  select process.id, process.session_id, process.owner_attempt_id,
54890
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,
54891
54996
  (coalesce(process.provider_command, '{}'::jsonb) ? 'supervision') as supervised,
54892
54997
  (process.lease_epoch = ${initial.leaseEpoch}
54893
54998
  and process.provider_instance_id = ${initial.instanceId}
@@ -54961,8 +55066,19 @@ export async function enrollRetainedCommandContainment(
54961
55066
  };
54962
55067
  if (enrolled) return { ...target, mode: "resumed" };
54963
55068
  if (lease.archive_capture_id !== null) return null;
54964
- let mode: "idle" | "deadline" | null = null;
55069
+ let mode: "idle" | "deadline" | "quiescence" | null = null;
54965
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 (
54966
55082
  deadlineRotation &&
54967
55083
  processes.every((p) => p.deadline_ready) &&
54968
55084
  !(await deadlineOwnerQuiescencePending(tx, input.workspaceId, processes)) &&
@@ -54986,9 +55102,11 @@ export async function enrollRetainedCommandContainment(
54986
55102
  await tx.execute(sql`
54987
55103
  update sandbox_leases set unobservable_command_drain_ids = ${`{${ids.join(",")}}`}::uuid[],
54988
55104
  command_containment_reason = ${
54989
- mode === "deadline"
54990
- ? DEADLINE_COMMAND_CONTAINMENT_REASON
54991
- : 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
54992
55110
  },
54993
55111
  liveness = 'draining', rotation_requested_at = coalesce(rotation_requested_at, now()),
54994
55112
  rotation_reason = coalesce(rotation_reason, 'operator'), expires_at = now(), updated_at = now()
@@ -55344,7 +55462,7 @@ export async function reArmDrainingLease(
55344
55462
  ? snapshot.mode === "credits" && snapshot.rateMicrosPerSecond > 0
55345
55463
  : input.warmBilling?.rateMicrosPerSecond !== undefined &&
55346
55464
  input.warmBilling.rateMicrosPerSecond > 0) &&
55347
- (await getBillingBalance(tx, input.accountId)).balanceMicros <= 0
55465
+ (await getSpendableCreditBalance(tx, input.accountId)).balanceMicros <= 0
55348
55466
  ) {
55349
55467
  throw new SandboxPaidComputeAdmissionError(input.workspaceId, input.sandboxGroupId);
55350
55468
  }
@@ -55528,7 +55646,8 @@ export async function confirmDrainCold(
55528
55646
  !input.providerMissingBeforeCapture &&
55529
55647
  row.archive_capture_published_at !== null &&
55530
55648
  (reason === IDLE_COMMAND_CONTAINMENT_REASON ||
55531
- reason === DEADLINE_COMMAND_CONTAINMENT_REASON) &&
55649
+ reason === DEADLINE_COMMAND_CONTAINMENT_REASON ||
55650
+ reason === QUIESCENCE_COMMAND_CONTAINMENT_REASON) &&
55532
55651
  row.unobservable_command_drain_ids?.length
55533
55652
  ? row.unobservable_command_drain_ids
55534
55653
  : [];
@@ -75366,6 +75485,28 @@ export async function reconcileSessionAttemptQuiescence(
75366
75485
  };
75367
75486
  }
75368
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
+ }
75369
75510
  return { action: "pending", events: [] };
75370
75511
  }
75371
75512
  const events = await markSessionAttemptQuiesced(db, {
@@ -84,6 +84,7 @@ export async function completeSelfServiceOrganizationSetup(
84
84
  operationId: string;
85
85
  requestFingerprint: string;
86
86
  trialCreditsEnabled?: boolean;
87
+ trialCreditModelIds?: string[] | undefined;
87
88
  },
88
89
  ): Promise<CompleteSelfServiceOrganizationSetupResponseType> {
89
90
  return await db.transaction(async (tx) => {
@@ -99,6 +100,13 @@ export async function completeSelfServiceOrganizationSetup(
99
100
  ${input.trialCreditsEnabled === true ? "on" : "off"}, true
100
101
  )`,
101
102
  );
103
+ await rawRows(
104
+ txDb,
105
+ sql`select pg_catalog.set_config(
106
+ 'opengeni.signup_credit_model_ids',
107
+ ${input.trialCreditModelIds ? JSON.stringify(input.trialCreditModelIds) : ""}, true
108
+ )`,
109
+ );
102
110
  const [row] = await rawRows<{ result: unknown }>(
103
111
  txDb,
104
112
  sql`select complete_self_service_organization_setup(
@@ -60,6 +60,7 @@ export async function createManagedOrganization(
60
60
  name: string;
61
61
  operationId: string;
62
62
  trialCreditsEnabled?: boolean;
63
+ trialCreditModelIds?: string[] | undefined;
63
64
  },
64
65
  ): Promise<CreateOrganizationResponseType> {
65
66
  return await db.transaction(async (tx) => {
@@ -74,6 +75,13 @@ export async function createManagedOrganization(
74
75
  ${input.trialCreditsEnabled === true ? "on" : "off"}, true
75
76
  )`,
76
77
  );
78
+ await rawRows(
79
+ tx,
80
+ sql`select pg_catalog.set_config(
81
+ 'opengeni.signup_credit_model_ids',
82
+ ${input.trialCreditModelIds ? JSON.stringify(input.trialCreditModelIds) : ""}, true
83
+ )`,
84
+ );
77
85
  const [row] = await rawRows<{ result: unknown }>(
78
86
  tx,
79
87
  sql`select create_managed_organization(
@@ -213,12 +213,23 @@ export function safeDatabaseErrorFacts(error: unknown): SafeDatabaseErrorFacts {
213
213
  return facts;
214
214
  }
215
215
 
216
- /**
217
- * Typed persistence classification that retains the exact original failure as
218
- * `cause` for internal diagnostics. The ordinary Error message is deliberately
219
- * stable and source-free so a generic presentation path can never disclose SQL,
220
- * parameters, or driver detail by rendering `.message`.
221
- */
216
+ /** Own-driver transaction boundary; callback errors never establish provenance. */
217
+ export class DatabaseTransactionError extends Error {
218
+ readonly name = "DatabaseTransactionError";
219
+
220
+ constructor(
221
+ readonly stage: "admission" | "settlement",
222
+ cause: unknown,
223
+ // A rollback failure must not erase a callback's no-replay/permanent
224
+ // evidence. The recovery classifier inspects both branches for vetoes.
225
+ readonly original?: unknown,
226
+ ) {
227
+ super(`Database transaction ${stage} failed`, { cause });
228
+ }
229
+ }
230
+
231
+ /** Typed persistence classification retaining the original cause internally.
232
+ * Its ordinary message is stable and contains no SQL or driver parameters. */
222
233
  export class SessionEventPersistenceError extends Error {
223
234
  readonly name = "SessionEventPersistenceError";
224
235
 
@@ -1629,6 +1629,10 @@ BEGIN
1629
1629
  END IF;
1630
1630
  -- The trial-credit kill switch setter is operator-only (migration owner).
1631
1631
  -- Reprovisioning repairs any accidental runtime or PUBLIC grant.
1632
+ IF to_regprocedure(format('%I.set_credit_promotion_policy(jsonb,text,text)', ${literal(schema)})) IS NOT NULL THEN
1633
+ EXECUTE format('REVOKE ALL ON FUNCTION %I.set_credit_promotion_policy(jsonb,text,text) FROM PUBLIC', ${literal(schema)});
1634
+ EXECUTE format('REVOKE ALL ON FUNCTION %I.set_credit_promotion_policy(jsonb,text,text) FROM %I', ${literal(schema)}, ${literal(role)});
1635
+ END IF;
1632
1636
  IF to_regprocedure(
1633
1637
  format('%I.set_verified_signup_trial_credits_enabled(boolean,text,text)', ${literal(schema)})
1634
1638
  ) IS NOT NULL THEN
@@ -2110,6 +2114,11 @@ BEGIN
2110
2114
  REVOKE ALL (singleton, consent_enabled, release_evidence) ON TABLE opengeni_private.sandbox_recovery_rollout FROM PUBLIC;
2111
2115
  EXECUTE format('GRANT SELECT ON TABLE opengeni_private.sandbox_recovery_rollout TO %I', ${literal(role)});
2112
2116
  END IF;
2117
+ IF to_regclass('opengeni_private.credit_promotion_policy_revisions') IS NOT NULL THEN
2118
+ EXECUTE format('REVOKE ALL ON TABLE opengeni_private.credit_promotion_policy_revisions FROM %I', ${literal(role)});
2119
+ REVOKE ALL ON TABLE opengeni_private.credit_promotion_policy_revisions FROM PUBLIC;
2120
+ EXECUTE format('GRANT SELECT ON TABLE opengeni_private.credit_promotion_policy_revisions TO %I', ${literal(role)});
2121
+ END IF;
2113
2122
  IF to_regclass('opengeni_private.verified_signup_trial_switch_revisions') IS NOT NULL THEN
2114
2123
  -- Read-only for the operator gauge. Only the owner-only audited setter
2115
2124
  -- appends revisions; runtime identities and PUBLIC never write them.
@@ -794,11 +794,13 @@ export const RUNTIME_TARGET_SCHEMA_FORBIDDEN_ROUTINES = [
794
794
  SESSION_TENANCY_QUIESCENCE_ROUTINE,
795
795
  TENANCY_BACKFILL_ACTIVATION_EVIDENCE_ROUTINE,
796
796
  VERIFIED_SIGNUP_TRIAL_SWITCH_SETTER_ROUTINE,
797
+ "set_credit_promotion_policy(jsonb, text, text)",
797
798
  MANAGED_AUTH_NEW_SIGNUPS_SWITCH_SETTER_ROUTINE,
798
799
  ...DOCUMENT_MIGRATION_AUDIT_INTERNAL_ROUTINES,
799
800
  ] as const;
800
801
 
801
802
  export const RUNTIME_TARGET_SCHEMA_INVOKER_ROUTINES = [
803
+ "set_credit_promotion_policy(jsonb, text, text)",
802
804
  CLAUDE_SNAPSHOT_VALIDATOR_ROUTINE,
803
805
  "usage_allowance_period(jsonb, timestamp with time zone)",
804
806
  "validate_usage_allowance_config(jsonb)",
@@ -894,6 +896,7 @@ export const FORCE_RLS_TABLES = [
894
896
  "connections",
895
897
  "connector_action_policies",
896
898
  "connector_action_requests",
899
+ "credit_debit_allocations",
897
900
  "credit_ledger_entries",
898
901
  "device_enrollment_requests",
899
902
  "document_authority_reclassifications",
@@ -1476,6 +1479,7 @@ export const RUNTIME_READ_INSERT_TABLES = [
1476
1479
  "browser_revision_components",
1477
1480
  "browser_revisions",
1478
1481
  "company_profile_revisions",
1482
+ "credit_debit_allocations",
1479
1483
  "editable_artifact_blob_refs",
1480
1484
  "editable_artifact_idempotency_receipts",
1481
1485
  "editable_artifact_live_outbox",
@@ -2181,6 +2185,7 @@ export async function inspectRuntimeDatabasePosture(
2181
2185
  'modal_inventory_read_capabilities',
2182
2186
  ${AUTOMATIC_SESSION_TITLE_FANOUT_OUTBOX_TABLE},
2183
2187
  ${VERIFIED_SIGNUP_TRIAL_SWITCH_TABLE},
2188
+ 'credit_promotion_policy_revisions',
2184
2189
  ${MANAGED_AUTH_NEW_SIGNUPS_SWITCH_TABLE}
2185
2190
  )
2186
2191
  `),
@@ -3752,6 +3757,23 @@ export function evaluateRuntimeDatabasePosture(
3752
3757
 
3753
3758
  // The trial-credit kill switch is operator state. Runtime roles may read it
3754
3759
  // for the gauge (SELECT is optional) but must never append or rewrite it.
3760
+ const creditPolicyTable = posture.privateTables.find(
3761
+ (table) => table.name === "credit_promotion_policy_revisions",
3762
+ );
3763
+ if (!creditPolicyTable) {
3764
+ violations.push("credit promotion policy table is missing");
3765
+ } else if (!creditPolicyTable.select) {
3766
+ violations.push("runtime role cannot read the credit promotion policy");
3767
+ }
3768
+ if (
3769
+ creditPolicyTable &&
3770
+ (creditPolicyTable.owner === expectedRole ||
3771
+ creditPolicyTable.insert ||
3772
+ creditPolicyTable.update ||
3773
+ creditPolicyTable.delete)
3774
+ ) {
3775
+ violations.push("runtime role has forbidden write authority on the credit promotion policy");
3776
+ }
3755
3777
  const trialSwitchTable = posture.privateTables.find(
3756
3778
  (table) => table.name === VERIFIED_SIGNUP_TRIAL_SWITCH_TABLE,
3757
3779
  );
package/src/schema.ts CHANGED
@@ -9680,7 +9680,7 @@ export const sandboxLeases = pgTable(
9680
9680
  // Why an enrolled drain contains its commands (0547); cleared with the
9681
9681
  // enrollment. Null for a pre-0547 enrollment: settle with neutral wording.
9682
9682
  commandContainmentReason: text("command_containment_reason", {
9683
- enum: ["idle_containment", "provider_deadline_containment"],
9683
+ enum: ["idle_containment", "provider_deadline_containment", "quiescence_containment"],
9684
9684
  }),
9685
9685
  liveness: text("liveness", { enum: sandboxLeaseLivenessValues }).notNull().default("cold"),
9686
9686
  refcount: integer("refcount").notNull().default(0),
@@ -12628,6 +12628,8 @@ export const creditLedgerEntries = pgTable(
12628
12628
  }),
12629
12629
  type: text("type").notNull(),
12630
12630
  amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
12631
+ /** Null preserves the unrestricted terms of legacy credits. */
12632
+ eligibleModelIds: text("eligible_model_ids").array(),
12631
12633
  currency: text("currency").notNull().default("usd"),
12632
12634
  sourceType: text("source_type"),
12633
12635
  sourceId: text("source_id"),
@@ -12638,6 +12640,19 @@ export const creditLedgerEntries = pgTable(
12638
12640
  },
12639
12641
  (table) => ({
12640
12642
  idempotency: uniqueIndex("credit_ledger_entries_idempotency_idx").on(table.idempotencyKey),
12643
+ idAccount: uniqueIndex("credit_ledger_entries_id_account_idx").on(table.id, table.accountId),
12644
+ scopedGrants: index("credit_ledger_scoped_grants_idx")
12645
+ .on(table.accountId, table.createdAt, table.id)
12646
+ .where(sql`${table.eligibleModelIds} is not null`),
12647
+ scopeValid: check(
12648
+ "credit_ledger_scope_valid",
12649
+ sql`${table.eligibleModelIds} is null or (
12650
+ ${table.type} = 'grant' and ${table.amountMicros} > 0
12651
+ and cardinality(${table.eligibleModelIds}) between 1 and 40
12652
+ and array_position(${table.eligibleModelIds}, null) is null
12653
+ and array_position(${table.eligibleModelIds}, '') is null
12654
+ )`,
12655
+ ),
12641
12656
  modelDebitPeriod: index("credit_ledger_entries_model_debit_period_idx")
12642
12657
  .on(table.accountId, table.occurredAt, table.workspaceId)
12643
12658
  .where(
@@ -12650,6 +12665,30 @@ export const creditLedgerEntries = pgTable(
12650
12665
  }),
12651
12666
  );
12652
12667
 
12668
+ /** One debit can consume several grants; the remainder is paid by general credits. */
12669
+ export const creditDebitAllocations = pgTable(
12670
+ "credit_debit_allocations",
12671
+ {
12672
+ accountId: uuid("account_id").notNull(),
12673
+ debitEntryId: uuid("debit_entry_id").notNull(),
12674
+ grantEntryId: uuid("grant_entry_id").notNull(),
12675
+ amountMicros: bigint("amount_micros", { mode: "number" }).notNull(),
12676
+ },
12677
+ (table) => ({
12678
+ pk: primaryKey({ columns: [table.debitEntryId, table.grantEntryId] }),
12679
+ debit: foreignKey({
12680
+ columns: [table.debitEntryId, table.accountId],
12681
+ foreignColumns: [creditLedgerEntries.id, creditLedgerEntries.accountId],
12682
+ }).onDelete("cascade"),
12683
+ grant: foreignKey({
12684
+ columns: [table.grantEntryId, table.accountId],
12685
+ foreignColumns: [creditLedgerEntries.id, creditLedgerEntries.accountId],
12686
+ }).onDelete("cascade"),
12687
+ grantIndex: index("credit_debit_allocations_grant_idx").on(table.accountId, table.grantEntryId),
12688
+ positive: check("credit_debit_allocations_positive", sql`${table.amountMicros} > 0`),
12689
+ }),
12690
+ );
12691
+
12653
12692
  /** Content-free immutable accepted billing facts; no runtime table privileges. */
12654
12693
  export const usageAllowanceAttributionReceipts = opengeniPrivateSchema.table(
12655
12694
  "usage_allowance_attribution_receipts",