@oxygen-agent/cli 1.1010.721 → 1.1010.905

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 (100) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +14 -0
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/index.js +583 -145
  10. package/dist/run-wait.d.ts +3 -1
  11. package/dist/run-wait.js +19 -5
  12. package/dist/streamed-file-import.d.ts +58 -0
  13. package/dist/streamed-file-import.js +115 -0
  14. package/dist/update.d.ts +29 -0
  15. package/dist/update.js +62 -16
  16. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  17. package/dist/workflow-plan-limit-notices.js +28 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  19. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
  20. package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
  21. package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
  22. package/node_modules/@oxygen/shared/dist/billing.js +333 -42
  23. package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
  24. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  25. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  26. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  27. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  28. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  30. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  31. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  32. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  33. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  34. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
  35. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
  36. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  37. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  41. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  42. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  43. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  44. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  45. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
  47. package/node_modules/@oxygen/shared/dist/index.js +2 -42
  48. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  49. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  50. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  51. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  52. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
  53. package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
  54. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  55. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  56. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  57. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  58. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
  59. package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
  60. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
  61. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
  62. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
  63. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
  64. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  65. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  66. package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
  68. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  69. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  70. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
  71. package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
  72. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  73. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  74. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  75. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  76. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
  77. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
  78. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
  79. package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
  80. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  81. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  82. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  83. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  84. package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
  85. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  86. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  87. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  88. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  89. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  90. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  91. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  92. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  93. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  94. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  95. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  96. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  97. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  98. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  99. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  100. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- import { type RepricingOptions } from "./repricing.js";
1
+ import type { RepricingOptions } from "./repricing.js";
2
2
  /**
3
3
  * The daily cap a new mailbox gets when nobody names one. New mailboxes were
4
4
  * created at 18 a day before the repricing, so this is an increase and is live.
5
5
  */
6
- export declare const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = 25;
6
+ export declare const EMAIL_MAILBOX_DEFAULT_DAILY_CAP: 25;
7
7
  /**
8
8
  * Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
9
9
  * `user` = a person or agent set it. A row written before the column existed
@@ -12,15 +12,13 @@ export declare const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = 25;
12
12
  export declare const EMAIL_MAILBOX_DAILY_CAP_SOURCES: readonly ["default", "user"];
13
13
  export type EmailMailboxDailyCapSource = (typeof EMAIL_MAILBOX_DAILY_CAP_SOURCES)[number];
14
14
  export declare function readEmailMailboxDailyCapSource(value: unknown): EmailMailboxDailyCapSource | null;
15
- /** The most a mailbox may be set to send a day, or null while there is no maximum. */
16
- export declare function emailMailboxDailyCapMaximum(options?: RepricingOptions): number | null;
15
+ /** The most a mailbox may be set to send a day, independent of billing activation. */
16
+ export declare function emailMailboxDailyCapMaximum(_options?: RepricingOptions): number | null;
17
17
  /**
18
18
  * The configured cap in force for a stored one: every surface that reads a
19
19
  * mailbox's cap, and the send claim, go through this.
20
20
  *
21
- * Once the switch is in force, a cap still at the old default of 40 that nobody
22
- * set by hand sends at the new default, and any cap above the maximum sends at
23
- * the maximum. Before it, the stored cap is returned unchanged.
21
+ * Stored choices are retained up to the technical maximum.
24
22
  */
25
23
  export declare function emailMailboxDailyCapInForce(input: {
26
24
  storedCap: number;
@@ -2,18 +2,17 @@
2
2
  // decision L3; specification slice S52). Limits guard, credits meter: none of
3
3
  // these is sold, and none charges for sending.
4
4
  //
5
- // Email per mailbox: warm-up ramp 5 / 10 / 20 (tenant-db `WARMUP_RAMP_DAILY_CAPS`),
6
- // then the mailbox's own daily cap, 25 by default. The hard maximum of 50 and the
7
- // move of mailboxes still on the old default of 40 are limit cuts, held by the
8
- // 2026-09 effective-date switch (`./repricing.ts`), so they are read through it.
5
+ // Email campaign ramp: 5 / 10 / 20 based on accepted-send days, then the
6
+ // configured cap (25 by default, maximum 50), independent of billing activation.
7
+ // The versioned migration changes old defaults once; later choices are retained.
9
8
  //
10
9
  // Voice per number: 100 dials a day by default, adjustable up to 300.
11
- import { EMAIL_MAILBOX_DAILY_CAP_MAXIMUM, EMAIL_MAILBOX_OLD_DEFAULT_DAILY_CAP, repricedValue, } from "./repricing.js";
10
+ import { EMAIL_DELIVERABILITY_DEFAULTS } from "./email-deliverability-policy.js";
12
11
  /**
13
12
  * The daily cap a new mailbox gets when nobody names one. New mailboxes were
14
13
  * created at 18 a day before the repricing, so this is an increase and is live.
15
14
  */
16
- export const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = 25;
15
+ export const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = EMAIL_DELIVERABILITY_DEFAULTS.campaignDailyCap;
17
16
  /**
18
17
  * Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
19
18
  * `user` = a person or agent set it. A row written before the column existed
@@ -23,25 +22,20 @@ export const EMAIL_MAILBOX_DAILY_CAP_SOURCES = ["default", "user"];
23
22
  export function readEmailMailboxDailyCapSource(value) {
24
23
  return value === "default" || value === "user" ? value : null;
25
24
  }
26
- /** The most a mailbox may be set to send a day, or null while there is no maximum. */
27
- export function emailMailboxDailyCapMaximum(options = {}) {
28
- return repricedValue(EMAIL_MAILBOX_DAILY_CAP_MAXIMUM, options);
25
+ /** The most a mailbox may be set to send a day, independent of billing activation. */
26
+ export function emailMailboxDailyCapMaximum(_options = {}) {
27
+ return 50;
29
28
  }
30
29
  /**
31
30
  * The configured cap in force for a stored one: every surface that reads a
32
31
  * mailbox's cap, and the send claim, go through this.
33
32
  *
34
- * Once the switch is in force, a cap still at the old default of 40 that nobody
35
- * set by hand sends at the new default, and any cap above the maximum sends at
36
- * the maximum. Before it, the stored cap is returned unchanged.
33
+ * Stored choices are retained up to the technical maximum.
37
34
  */
38
35
  export function emailMailboxDailyCapInForce(input, options = {}) {
39
36
  const stored = Number.isFinite(input.storedCap) ? Math.max(0, Math.floor(input.storedCap)) : 0;
40
- const cap = input.source !== "user" && stored === EMAIL_MAILBOX_OLD_DEFAULT_DAILY_CAP.before
41
- ? repricedValue(EMAIL_MAILBOX_OLD_DEFAULT_DAILY_CAP, options)
42
- : stored;
43
37
  const maximum = emailMailboxDailyCapMaximum(options);
44
- return maximum === null ? cap : Math.min(cap, maximum);
38
+ return maximum === null ? stored : Math.min(stored, maximum);
45
39
  }
46
40
  /** A new voice number may dial this many times a day once it has warmed up. */
47
41
  export const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
@@ -25,24 +25,15 @@
25
25
  * "skips the seat check", it is one that holds a legacy entitlement which
26
26
  * SATISFIES the seat check. The gate always runs.
27
27
  */
28
- import { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS, SENDING_SEAT_USD_CENTS, type SendingSeatKey } from "./pricing-snapshot.generated.js";
28
+ import { SENDING_SEAT_USD_CENTS, type SendingSeatKey } from "./pricing-snapshot.generated.js";
29
+ import type { CreditGateKind } from "./credit-gate.js";
29
30
  export { SENDING_SEAT_USD_CENTS };
30
31
  /**
31
- * Email infrastructure, kept next to the seats because the relationship between
32
- * them is the whole design.
33
- *
34
- * MANAGED_INBOX_USD_CENTS is ALL-IN: it already contains the sending seat and
35
- * warm-up. A customer buying an Oxygen mailbox buys ONE thing at ONE price and
36
- * can never end up holding a mailbox with no seat to send from. A customer
37
- * bringing their own mailbox pays SENDING_SEAT_USD_CENTS.email_sender instead.
38
- * The two are alternatives, never both for the same mailbox.
39
- *
40
- * The practical consequence for the connect gate: email capacity is
41
- * (email_sender seats) + (managed inbox slots). Counting only seats would refuse
42
- * a customer who bought inboxes, which is exactly the assemble-the-parts failure
43
- * this bundle exists to prevent.
32
+ * Email infrastructure is no longer sold in dollars (repricing 2026-09, S32/S33):
33
+ * a managed inbox is priced in credits, 400 for a Google mailbox plus the 100-credit
34
+ * mailbox connection every sending mailbox holds, warm-up included. A customer
35
+ * bringing their own mailbox pays the connection alone.
44
36
  */
45
- export { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS };
46
37
  export type { SendingSeatKey };
47
38
  export declare const SENDING_SEAT_KEYS: readonly ["linkedin", "whatsapp", "phone_number", "email_sender"];
48
39
  export type SendingSeatDefinition = {
@@ -64,6 +55,18 @@ export type SendingSeatDefinition = {
64
55
  export declare const SENDING_SEAT_CATALOG: {
65
56
  readonly [K in SendingSeatKey]: SendingSeatDefinition;
66
57
  };
58
+ /**
59
+ * The credit reservation each sending-seat channel becomes once seats are
60
+ * retired (repricing 2026-09, decisions 2.7 and 2.9). A seat still held keeps
61
+ * covering accounts of its kind until the seat subscription ends at renewal, and
62
+ * only then does each account start reserving credits, so an account is never
63
+ * paid for twice.
64
+ */
65
+ export declare const SEAT_RESERVATION_KIND: {
66
+ readonly [K in SendingSeatKey]: CreditGateKind;
67
+ };
68
+ /** The seat channel whose seats cover a reservation kind, or null for a kind no seat ever covered. */
69
+ export declare function seatKeyForReservationKind(kind: string): SendingSeatKey | null;
67
70
  export declare const SENDING_SEAT_LEGACY_CHARGE_KEYS: readonly string[];
68
71
  export declare function isSendingSeatKey(value: unknown): value is SendingSeatKey;
69
72
  export declare function sendingSeatMonthlyPriceCents(key: SendingSeatKey): number;
@@ -25,24 +25,8 @@
25
25
  * "skips the seat check", it is one that holds a legacy entitlement which
26
26
  * SATISFIES the seat check. The gate always runs.
27
27
  */
28
- import { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS, SENDING_SEAT_USD_CENTS, } from "./pricing-snapshot.generated.js";
28
+ import { SENDING_SEAT_USD_CENTS, } from "./pricing-snapshot.generated.js";
29
29
  export { SENDING_SEAT_USD_CENTS };
30
- /**
31
- * Email infrastructure, kept next to the seats because the relationship between
32
- * them is the whole design.
33
- *
34
- * MANAGED_INBOX_USD_CENTS is ALL-IN: it already contains the sending seat and
35
- * warm-up. A customer buying an Oxygen mailbox buys ONE thing at ONE price and
36
- * can never end up holding a mailbox with no seat to send from. A customer
37
- * bringing their own mailbox pays SENDING_SEAT_USD_CENTS.email_sender instead.
38
- * The two are alternatives, never both for the same mailbox.
39
- *
40
- * The practical consequence for the connect gate: email capacity is
41
- * (email_sender seats) + (managed inbox slots). Counting only seats would refuse
42
- * a customer who bought inboxes, which is exactly the assemble-the-parts failure
43
- * this bundle exists to prevent.
44
- */
45
- export { MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS };
46
30
  export const SENDING_SEAT_KEYS = [
47
31
  "linkedin",
48
32
  "whatsapp",
@@ -88,6 +72,27 @@ export const SENDING_SEAT_CATALOG = {
88
72
  stripePriceEnvVar: "STRIPE_PRICE_SEAT_EMAIL_USD",
89
73
  },
90
74
  };
75
+ /**
76
+ * The credit reservation each sending-seat channel becomes once seats are
77
+ * retired (repricing 2026-09, decisions 2.7 and 2.9). A seat still held keeps
78
+ * covering accounts of its kind until the seat subscription ends at renewal, and
79
+ * only then does each account start reserving credits, so an account is never
80
+ * paid for twice.
81
+ */
82
+ export const SEAT_RESERVATION_KIND = {
83
+ linkedin: "linkedin_account",
84
+ whatsapp: "whatsapp_account",
85
+ phone_number: "phone_number",
86
+ email_sender: "sending_mailbox",
87
+ };
88
+ /** The seat channel whose seats cover a reservation kind, or null for a kind no seat ever covered. */
89
+ export function seatKeyForReservationKind(kind) {
90
+ for (const seatKey of SENDING_SEAT_KEYS) {
91
+ if (SEAT_RESERVATION_KIND[seatKey] === kind)
92
+ return seatKey;
93
+ }
94
+ return null;
95
+ }
91
96
  export const SENDING_SEAT_LEGACY_CHARGE_KEYS = SENDING_SEAT_KEYS
92
97
  .map((key) => SENDING_SEAT_CATALOG[key].legacyChargeKey)
93
98
  .filter((chargeKey) => chargeKey !== null);
@@ -1,5 +1,7 @@
1
1
  import type { PlanTier } from "./billing.js";
2
+ import type { PlanBand } from "./plan-band.js";
2
3
  import type { LimitsTier } from "./plan-limits.js";
4
+ import { type RepricingOptions } from "./repricing.js";
3
5
  /**
4
6
  * Spend-safety defaults: plan-scaled ceilings applied when an autonomous
5
7
  * spend path was armed WITHOUT an explicit cap. They restore the ADR-0011
@@ -19,18 +21,29 @@ export declare const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES: ReadonlySet<string>;
19
21
  /**
20
22
  * Per-run managed-credit ceiling for a LIVE workflow run fired by an autonomous
21
23
  * trigger when neither run metadata nor the manifest declares `max_credits`.
24
+ *
25
+ * These are the ceilings each rung read BEFORE the 2026-09 repricing. The value
26
+ * in force is resolved per plan by `resolveDefaultTriggerRunCreditCeilingForPlan`
27
+ * (`plan-band.ts`): 10% of the plan's monthly credits (decision L5.1), where a
28
+ * larger default applies at once and a smaller one waits for the
29
+ * effective-date switch with this table as its old value. Read this table
30
+ * directly only to know the pre-repricing value.
22
31
  */
23
32
  export declare const DEFAULT_TRIGGER_RUN_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
24
33
  /**
25
34
  * Per-delivery credit ceiling stamped onto a standing/webhook table auto-run
26
- * batch when the armed configuration carries no explicit `max_credits`.
35
+ * batch when the armed configuration carries no explicit `max_credits`. The
36
+ * pre-repricing values, like the trigger table above; resolve the value in force
37
+ * with `resolveDefaultAutoRunBatchCreditCeilingForPlan`.
27
38
  */
28
39
  export declare const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING: Record<SpendSafetyPlanTier, number | null>;
29
40
  /**
30
41
  * Row ceiling for a BYOK AI-column run whose caller gave no explicit row bound
31
42
  * (no limit, no row_ids, selection "all"). BYOK bills the customer's own
32
- * provider account, so a credit ceiling is meaningless — row count is the
33
- * enforceable unit. An explicit limit (up to the 500k platform row cap) wins.
43
+ * provider account, so OXYGEN's credit ceiling bounds only its own platform fee
44
+ * (0.05 credits a call from the 2026-09 repricing instant, decision 6.3) and
45
+ * never the provider's bill — row count stays the enforceable unit for that.
46
+ * An explicit limit (up to the 500k platform row cap) wins.
34
47
  */
35
48
  export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<SpendSafetyPlanTier, number | null>;
36
49
  /**
@@ -89,6 +102,12 @@ export declare function resolveByokProviderDailyCapEnforcementMode(configured?:
89
102
  *
90
103
  * This applies ONLY to the implicit default. An org that sets its own org-daily
91
104
  * policy has chosen a real spend ceiling, and that ceiling counts everything.
105
+ *
106
+ * The monthly reservation renewals of the 2026-09 repricing (connected sending
107
+ * mailboxes, deliverability units, connected accounts and phone numbers, dedicated
108
+ * IPs) are the same shape and are excluded for the same reason (PROPOSED P-59).
109
+ * It matters most on the free plan, whose guard is a fixed 1,000 / 5,000 credits
110
+ * a day: one LinkedIn renewal would otherwise be most of a free day's warning.
92
111
  */
93
112
  export declare const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES: readonly string[];
94
113
  export declare const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
@@ -101,16 +120,46 @@ export type OrgDailySpendGuard = {
101
120
  /**
102
121
  * Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
103
122
  * means the guard is suppressed because the plan has no finite positive grant to
104
- * scale from (free at 0, enterprise/custom at null) — the single place that
105
- * decision is made, so enforcement and the read surfaces cannot drift apart.
123
+ * scale from (enterprise/custom at null) — the single place that decision is
124
+ * made, so enforcement and the read surfaces cannot drift apart.
125
+ *
126
+ * The free plan has no monthly grant, so it gets a fixed guard instead (decision
127
+ * L5.2: 1,000 warn / 5,000 block a UTC day). It is a new bound on free, so it
128
+ * takes effect at the 2026-09 repricing instant; before then free has none.
106
129
  */
107
- export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined): OrgDailySpendGuard | null;
130
+ export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined, options?: RepricingOptions & {
131
+ freePlan?: boolean;
132
+ }): OrgDailySpendGuard | null;
108
133
  /** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
109
134
  export declare function roundCredits(value: number): number;
110
135
  /** Credit estimates that present to 2 decimals (cent-grained customer copy). */
111
136
  export declare function roundCreditsToCents(value: number): number;
112
- export declare function resolveDefaultTriggerRunCreditCeiling(tier: SpendSafetyPlanTier): number | null;
113
- export declare function resolveDefaultAutoRunBatchCreditCeiling(tier: SpendSafetyPlanTier): number | null;
137
+ /**
138
+ * Platform-owned runaway ceiling for one attended Workspace Copilot turn: 3,000
139
+ * customer credits, $6 of upstream inference at the ratified 5x markup, the same
140
+ * on every plan. It is not a user budget or an authorization grant. It lives
141
+ * here rather than in `@oxygen/copilot` so the limits report can state it next
142
+ * to the approval-free allowance it must stay above; `@oxygen/copilot/config`
143
+ * re-exports it.
144
+ */
145
+ export declare const COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING = 3000;
146
+ /** How much paid work the attended Copilot may start without an approval card. */
147
+ export type CopilotApprovalFreeAllowance = {
148
+ /** The most one call may declare (its server-enforced `max_credits`). */
149
+ perCallCredits: number;
150
+ /** The most all such calls in one session may declare together. */
151
+ perSessionCredits: number;
152
+ };
153
+ /**
154
+ * The Copilot's approval-free spend allowance by plan size (decision L5.3). Only
155
+ * the two ends are ratified: free at 50 / 300 credits ($0.50 / $3) and $1,999 at
156
+ * 500 / 2,500 ($5 / $25). The middle sizes are PROPOSED (P-55). Every value is
157
+ * at least the 50 / 300 every plan had before, so the table ships live. The
158
+ * largest session allowance stays below the attended turn backstop above.
159
+ */
160
+ export declare const COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND: Readonly<Record<PlanBand, CopilotApprovalFreeAllowance>>;
161
+ /** A plan size's approval-free Copilot allowance. */
162
+ export declare function copilotApprovalFreeAllowanceForBand(band: PlanBand): CopilotApprovalFreeAllowance;
114
163
  export declare function resolveDefaultByokColumnRunMaxRows(tier: SpendSafetyPlanTier): number | null;
115
164
  export declare function resolveDefaultByokProviderDailyCallCap(tier: SpendSafetyPlanTier): number | null;
116
165
  /**
@@ -1,3 +1,4 @@
1
+ import { FREE_ORG_DAILY_GUARD_BLOCK_CREDITS, FREE_ORG_DAILY_GUARD_WARN_CREDITS, repricedValue, } from "./repricing.js";
1
2
  /** Workflow run trigger types that fire without a human in the loop. */
2
3
  export const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES = new Set([
3
4
  "cron",
@@ -7,6 +8,13 @@ export const AUTONOMOUS_WORKFLOW_TRIGGER_TYPES = new Set([
7
8
  /**
8
9
  * Per-run managed-credit ceiling for a LIVE workflow run fired by an autonomous
9
10
  * trigger when neither run metadata nor the manifest declares `max_credits`.
11
+ *
12
+ * These are the ceilings each rung read BEFORE the 2026-09 repricing. The value
13
+ * in force is resolved per plan by `resolveDefaultTriggerRunCreditCeilingForPlan`
14
+ * (`plan-band.ts`): 10% of the plan's monthly credits (decision L5.1), where a
15
+ * larger default applies at once and a smaller one waits for the
16
+ * effective-date switch with this table as its old value. Read this table
17
+ * directly only to know the pre-repricing value.
10
18
  */
11
19
  export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
12
20
  free: 50,
@@ -18,7 +26,9 @@ export const DEFAULT_TRIGGER_RUN_CREDIT_CEILING = {
18
26
  };
19
27
  /**
20
28
  * Per-delivery credit ceiling stamped onto a standing/webhook table auto-run
21
- * batch when the armed configuration carries no explicit `max_credits`.
29
+ * batch when the armed configuration carries no explicit `max_credits`. The
30
+ * pre-repricing values, like the trigger table above; resolve the value in force
31
+ * with `resolveDefaultAutoRunBatchCreditCeilingForPlan`.
22
32
  */
23
33
  export const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING = {
24
34
  free: 50,
@@ -31,8 +41,10 @@ export const DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING = {
31
41
  /**
32
42
  * Row ceiling for a BYOK AI-column run whose caller gave no explicit row bound
33
43
  * (no limit, no row_ids, selection "all"). BYOK bills the customer's own
34
- * provider account, so a credit ceiling is meaningless — row count is the
35
- * enforceable unit. An explicit limit (up to the 500k platform row cap) wins.
44
+ * provider account, so OXYGEN's credit ceiling bounds only its own platform fee
45
+ * (0.05 credits a call from the 2026-09 repricing instant, decision 6.3) and
46
+ * never the provider's bill — row count stays the enforceable unit for that.
47
+ * An explicit limit (up to the 500k platform row cap) wins.
36
48
  */
37
49
  export const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS = {
38
50
  free: null,
@@ -106,20 +118,38 @@ export function resolveByokProviderDailyCapEnforcementMode(configured = process.
106
118
  *
107
119
  * This applies ONLY to the implicit default. An org that sets its own org-daily
108
120
  * policy has chosen a real spend ceiling, and that ceiling counts everything.
121
+ *
122
+ * The monthly reservation renewals of the 2026-09 repricing (connected sending
123
+ * mailboxes, deliverability units, connected accounts and phone numbers, dedicated
124
+ * IPs) are the same shape and are excluded for the same reason (PROPOSED P-59).
125
+ * It matters most on the free plan, whose guard is a fixed 1,000 / 5,000 credits
126
+ * a day: one LinkedIn renewal would otherwise be most of a free day's warning.
109
127
  */
110
128
  export const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES = [
111
129
  "managed_warmup",
112
130
  "managed_inbox",
131
+ "managed_mailbox",
132
+ "managed_deliverability",
133
+ "managed_seat",
113
134
  ];
114
135
  export const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
115
136
  export const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
116
137
  /**
117
138
  * Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
118
139
  * means the guard is suppressed because the plan has no finite positive grant to
119
- * scale from (free at 0, enterprise/custom at null) — the single place that
120
- * decision is made, so enforcement and the read surfaces cannot drift apart.
140
+ * scale from (enterprise/custom at null) — the single place that decision is
141
+ * made, so enforcement and the read surfaces cannot drift apart.
142
+ *
143
+ * The free plan has no monthly grant, so it gets a fixed guard instead (decision
144
+ * L5.2: 1,000 warn / 5,000 block a UTC day). It is a new bound on free, so it
145
+ * takes effect at the 2026-09 repricing instant; before then free has none.
121
146
  */
122
- export function resolveOrgDailySpendGuard(monthlyCredits) {
147
+ export function resolveOrgDailySpendGuard(monthlyCredits, options = {}) {
148
+ if (options.freePlan) {
149
+ const warnCredits = repricedValue(FREE_ORG_DAILY_GUARD_WARN_CREDITS, options);
150
+ const blockCredits = repricedValue(FREE_ORG_DAILY_GUARD_BLOCK_CREDITS, options);
151
+ return warnCredits === null || blockCredits === null ? null : { warnCredits, blockCredits };
152
+ }
123
153
  if (typeof monthlyCredits !== "number" || !Number.isFinite(monthlyCredits) || monthlyCredits <= 0) {
124
154
  return null;
125
155
  }
@@ -136,11 +166,34 @@ export function roundCredits(value) {
136
166
  export function roundCreditsToCents(value) {
137
167
  return Math.round((value + Number.EPSILON) * 100) / 100;
138
168
  }
139
- export function resolveDefaultTriggerRunCreditCeiling(tier) {
140
- return DEFAULT_TRIGGER_RUN_CREDIT_CEILING[tier];
141
- }
142
- export function resolveDefaultAutoRunBatchCreditCeiling(tier) {
143
- return DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING[tier];
169
+ /**
170
+ * Platform-owned runaway ceiling for one attended Workspace Copilot turn: 3,000
171
+ * customer credits, $6 of upstream inference at the ratified 5x markup, the same
172
+ * on every plan. It is not a user budget or an authorization grant. It lives
173
+ * here rather than in `@oxygen/copilot` so the limits report can state it next
174
+ * to the approval-free allowance it must stay above; `@oxygen/copilot/config`
175
+ * re-exports it.
176
+ */
177
+ export const COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING = 3_000;
178
+ /**
179
+ * The Copilot's approval-free spend allowance by plan size (decision L5.3). Only
180
+ * the two ends are ratified: free at 50 / 300 credits ($0.50 / $3) and $1,999 at
181
+ * 500 / 2,500 ($5 / $25). The middle sizes are PROPOSED (P-55). Every value is
182
+ * at least the 50 / 300 every plan had before, so the table ships live. The
183
+ * largest session allowance stays below the attended turn backstop above.
184
+ */
185
+ export const COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND = Object.freeze({
186
+ free: Object.freeze({ perCallCredits: 50, perSessionCredits: 300 }),
187
+ "49": Object.freeze({ perCallCredits: 100, perSessionCredits: 500 }),
188
+ "99": Object.freeze({ perCallCredits: 100, perSessionCredits: 500 }),
189
+ "199": Object.freeze({ perCallCredits: 200, perSessionCredits: 1_000 }),
190
+ "499": Object.freeze({ perCallCredits: 200, perSessionCredits: 1_000 }),
191
+ "999": Object.freeze({ perCallCredits: 300, perSessionCredits: 1_500 }),
192
+ "1999": Object.freeze({ perCallCredits: 500, perSessionCredits: 2_500 }),
193
+ });
194
+ /** A plan size's approval-free Copilot allowance. */
195
+ export function copilotApprovalFreeAllowanceForBand(band) {
196
+ return COPILOT_APPROVAL_FREE_ALLOWANCE_BY_PLAN_BAND[band];
144
197
  }
145
198
  export function resolveDefaultByokColumnRunMaxRows(tier) {
146
199
  return DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS[tier];
@@ -1,4 +1,4 @@
1
- import { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, type BillingCurrency, type OxygenPlanKey, type PricingPlanDefinition, type SelfServePlanTier } from "./billing.js";
1
+ import { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, type BillingCurrency, type OxygenPlanKey, type PlanBillingInterval, type PricingPlanDefinition, type SelfServePlanTier } from "./billing.js";
2
2
  export { CONTACT_SALES_URL, PURCHASABLE_PLAN_KEYS, SELF_SERVE_PLAN_TIERS, normalizeBillingCurrency, };
3
3
  export type { BillingCurrency, OxygenPlanKey, SelfServePlanTier };
4
4
  export type PricingPlan = PricingPlanDefinition & {
@@ -17,8 +17,7 @@ export declare const PRICING_PLANS: {
17
17
  /** The six purchasable rungs with their Stripe price ids, cheapest first. */
18
18
  export declare const PURCHASABLE_PLANS: Record<OxygenPlanKey, PricingPlan>;
19
19
  export declare function getPlanPriceId(plan: PricingPlan, currency: BillingCurrency): string;
20
- /** How often a plan Price bills. Credits are granted monthly on both. */
21
- export type PlanBillingInterval = "month" | "year";
20
+ export type { PlanBillingInterval };
22
21
  /**
23
22
  * The configured Price for one rung and interval, or "" when that Price is not
24
23
  * configured (every yearly Price, until S14 creates them).
@@ -34,6 +33,14 @@ export declare function getPurchasablePlanPriceId(planKey: OxygenPlanKey, interv
34
33
  * whether a paid period is sliced.
35
34
  */
36
35
  export declare function planBillingIntervalForPriceId(priceId: string | null | undefined): PlanBillingInterval | null;
36
+ /**
37
+ * Whether yearly billing can be sold: every purchasable rung has a configured
38
+ * yearly Price. False until S14 creates them, which is what keeps every annual
39
+ * control (the plan picker's Yearly switch, `--interval year`, the pricing
40
+ * page's yearly line) dark. All six or nothing, so no surface ever offers a
41
+ * yearly rung whose checkout would be refused.
42
+ */
43
+ export declare function annualPlanBillingAvailable(currency?: BillingCurrency): boolean;
37
44
  /** Every configured yearly plan Price id (empty until S14). */
38
45
  export declare function configuredAnnualPlanPriceIds(): readonly string[];
39
46
  /**
@@ -50,10 +57,11 @@ export declare function tierFromPriceId(priceId: string): string | null;
50
57
  * family. Catalog audit/migration code consumes this instead of maintaining a
51
58
  * second env map that can silently omit grandfathered subscriptions.
52
59
  *
53
- * The yearly Prices are deliberately NOT listed yet: the migration planner
54
- * matches an outgoing Oxygen Price to its target by amount, and must learn to
55
- * compare intervals (repricing slice S16) before a yearly Price may enter its
56
- * source set. S14 adds them when it creates the Prices.
60
+ * The configured yearly Prices are listed with the Oxygen family (slice S14):
61
+ * the migration planner compares the billing interval before it matches a
62
+ * rung by plan key or amount (S16, `stripeMigrationTargetFor`), so a yearly
63
+ * Price in the source set resolves to itself and can never become the target
64
+ * of a monthly subscription.
57
65
  */
58
66
  export declare function recognizedStripePriceIdsBySelfServeTier(): Record<SelfServePlanTier, readonly string[]>;
59
67
  export declare function formatMonthlyPrice(priceCents: number | null, currency?: BillingCurrency): string;
@@ -25,7 +25,8 @@ const STRIPE_PRICE_OXYGEN_IDS = {
25
25
  * Product, since month and year are distinct intervals a Portal configuration
26
26
  * accepts under one Product.
27
27
  *
28
- * DARK until the yearly Prices exist (slice S14): every env var is unset, an
28
+ * DARK until the yearly Price ids are configured (`provision` creates the Prices
29
+ * and prints these names, slice S14): every env var is unset, an
29
30
  * unset id is "", and tierFromPriceId refuses "" before any map is consulted, so
30
31
  * no subscription resolves to a yearly interval and nothing is ever sliced.
31
32
  */
@@ -129,6 +130,16 @@ export function planBillingIntervalForPriceId(priceId) {
129
130
  }
130
131
  return tierFromPriceId(priceId) === null ? null : "month";
131
132
  }
133
+ /**
134
+ * Whether yearly billing can be sold: every purchasable rung has a configured
135
+ * yearly Price. False until S14 creates them, which is what keeps every annual
136
+ * control (the plan picker's Yearly switch, `--interval year`, the pricing
137
+ * page's yearly line) dark. All six or nothing, so no surface ever offers a
138
+ * yearly rung whose checkout would be refused.
139
+ */
140
+ export function annualPlanBillingAvailable(currency = DEFAULT_BILLING_CURRENCY) {
141
+ return PURCHASABLE_PLAN_KEYS.every((planKey) => getPurchasablePlanPriceId(planKey, "year", currency) !== "");
142
+ }
132
143
  /** Every configured yearly plan Price id (empty until S14). */
133
144
  export function configuredAnnualPlanPriceIds() {
134
145
  return uniqueNonEmptyPriceIds(PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey])));
@@ -175,15 +186,17 @@ export function tierFromPriceId(priceId) {
175
186
  * family. Catalog audit/migration code consumes this instead of maintaining a
176
187
  * second env map that can silently omit grandfathered subscriptions.
177
188
  *
178
- * The yearly Prices are deliberately NOT listed yet: the migration planner
179
- * matches an outgoing Oxygen Price to its target by amount, and must learn to
180
- * compare intervals (repricing slice S16) before a yearly Price may enter its
181
- * source set. S14 adds them when it creates the Prices.
189
+ * The configured yearly Prices are listed with the Oxygen family (slice S14):
190
+ * the migration planner compares the billing interval before it matches a
191
+ * rung by plan key or amount (S16, `stripeMigrationTargetFor`), so a yearly
192
+ * Price in the source set resolves to itself and can never become the target
193
+ * of a monthly subscription.
182
194
  */
183
195
  export function recognizedStripePriceIdsBySelfServeTier() {
184
196
  const byTier = {
185
197
  oxygen: [
186
198
  ...PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey])),
199
+ ...configuredAnnualPlanPriceIds(),
187
200
  ...LEGACY_CURRENT_PLAN_PRICE_IDS.oxygen,
188
201
  ],
189
202
  starter: [
@@ -25,18 +25,47 @@ export declare function workspaceDatabaseWarningBytesFor(limitBytes: number): nu
25
25
  */
26
26
  export declare const PLAN_BAND_TABLE_CAPACITY_TARGETS: Readonly<Record<PlanBand, WorkspaceTableCapacityLimits>>;
27
27
  /**
28
- * Sizes above today's envelope ship only after a capacity test at 5M and 10M
29
- * rows per Table (decision L1). Until that test passes these bands keep today's
30
- * envelope; shipping them is removing a band from this list.
28
+ * Rows per Table above today's 3,000,000 ship only after a capacity test at 5M
29
+ * and 10M rows per Table. The workspace totals of these bands (rows and
30
+ * database) are already in force (decision F.1, 2026-09-27, amending L1, which
31
+ * had gated both); shipping a band's rows per Table is removing it from this list.
31
32
  */
32
- export declare const PLAN_BANDS_AWAITING_CAPACITY_TEST: readonly PlanBand[];
33
+ export declare const PLAN_BANDS_AWAITING_TABLE_ROW_CAPACITY_TEST: readonly PlanBand[];
33
34
  /**
34
35
  * The storage limits in force for a plan band right now.
35
36
  *
36
37
  * - A limit that tightens (free, $49, $99) is a scheduled 2026-09 repricing value
37
38
  * (`storage.<band>.*` in `REPRICING_2026_09_SCHEDULE`): today's envelope until
38
39
  * the effective-date switch, the ratified ladder from it on.
39
- * - A band awaiting the capacity test keeps today's envelope.
40
+ * - A band awaiting the rows-per-Table capacity test has its ladder workspace
41
+ * totals and keeps today's rows per Table.
40
42
  * - Every other band is already at its ladder value.
41
43
  */
42
44
  export declare function resolveWorkspaceTableCapacity(band: PlanBand, options?: RepricingOptions): WorkspaceTableCapacityLimits;
45
+ /**
46
+ * Where a band's storage stands against its ratified ladder (decision F.2):
47
+ *
48
+ * - `in_force`: the band's values are in force; nothing is pending.
49
+ * - `scheduled`: the 2026-09 repricing moves them to `target` at `effectiveAt`.
50
+ * - `awaiting_capacity_test`: `target` applies once the rows-per-Table capacity
51
+ * test at that size passes; there is no date.
52
+ *
53
+ * A cut is reported only once an operator has set the repricing switch to a
54
+ * valid future instant. While the switch is unset or invalid a cutting band
55
+ * reads `in_force` and no target is revealed, the rule `describeRepricingSchedule`
56
+ * follows for every other scheduled value.
57
+ */
58
+ export type WorkspaceTableCapacityStatus = {
59
+ status: "in_force";
60
+ effectiveAt: null;
61
+ target: null;
62
+ } | {
63
+ status: "scheduled";
64
+ effectiveAt: Date;
65
+ target: WorkspaceTableCapacityLimits;
66
+ } | {
67
+ status: "awaiting_capacity_test";
68
+ effectiveAt: null;
69
+ target: WorkspaceTableCapacityLimits;
70
+ };
71
+ export declare function describeWorkspaceTableCapacityStatus(band: PlanBand, options?: RepricingOptions): WorkspaceTableCapacityStatus;
@@ -1,4 +1,4 @@
1
- import { findRepricedValue, repricedValue } from "./repricing.js";
1
+ import { findRepricedValue, readRepricingSwitch, repricedValue, } from "./repricing.js";
2
2
  const GIB = 1024 ** 3;
3
3
  /**
4
4
  * The envelope every workspace had before storage scaled with plan size, and
@@ -43,11 +43,12 @@ export const PLAN_BAND_TABLE_CAPACITY_TARGETS = Object.freeze({
43
43
  "1999": ladderRow(10_000_000, 100_000_000, 120),
44
44
  });
45
45
  /**
46
- * Sizes above today's envelope ship only after a capacity test at 5M and 10M
47
- * rows per Table (decision L1). Until that test passes these bands keep today's
48
- * envelope; shipping them is removing a band from this list.
46
+ * Rows per Table above today's 3,000,000 ship only after a capacity test at 5M
47
+ * and 10M rows per Table. The workspace totals of these bands (rows and
48
+ * database) are already in force (decision F.1, 2026-09-27, amending L1, which
49
+ * had gated both); shipping a band's rows per Table is removing it from this list.
49
50
  */
50
- export const PLAN_BANDS_AWAITING_CAPACITY_TEST = Object.freeze(["999", "1999"]);
51
+ export const PLAN_BANDS_AWAITING_TABLE_ROW_CAPACITY_TEST = Object.freeze(["999", "1999"]);
51
52
  const STORAGE_FIELDS = [
52
53
  ["tableRowLimit", "table_row_limit"],
53
54
  ["workspaceRowLimit", "workspace_row_limit"],
@@ -60,13 +61,15 @@ const STORAGE_FIELDS = [
60
61
  * - A limit that tightens (free, $49, $99) is a scheduled 2026-09 repricing value
61
62
  * (`storage.<band>.*` in `REPRICING_2026_09_SCHEDULE`): today's envelope until
62
63
  * the effective-date switch, the ratified ladder from it on.
63
- * - A band awaiting the capacity test keeps today's envelope.
64
+ * - A band awaiting the rows-per-Table capacity test has its ladder workspace
65
+ * totals and keeps today's rows per Table.
64
66
  * - Every other band is already at its ladder value.
65
67
  */
66
68
  export function resolveWorkspaceTableCapacity(band, options = {}) {
67
- if (PLAN_BANDS_AWAITING_CAPACITY_TEST.includes(band))
68
- return WORKSPACE_TABLE_CAPACITY;
69
69
  const target = PLAN_BAND_TABLE_CAPACITY_TARGETS[band];
70
+ if (PLAN_BANDS_AWAITING_TABLE_ROW_CAPACITY_TEST.includes(band)) {
71
+ return Object.freeze({ ...target, tableRowLimit: WORKSPACE_TABLE_CAPACITY.tableRowLimit });
72
+ }
70
73
  const resolved = { ...target };
71
74
  for (const [field, key] of STORAGE_FIELDS) {
72
75
  const scheduled = findRepricedValue(`storage.${band}.${key}`);
@@ -76,3 +79,17 @@ export function resolveWorkspaceTableCapacity(band, options = {}) {
76
79
  }
77
80
  return Object.freeze(resolved);
78
81
  }
82
+ export function describeWorkspaceTableCapacityStatus(band, options = {}) {
83
+ if (PLAN_BANDS_AWAITING_TABLE_ROW_CAPACITY_TEST.includes(band)) {
84
+ return { status: "awaiting_capacity_test", effectiveAt: null, target: PLAN_BAND_TABLE_CAPACITY_TARGETS[band] };
85
+ }
86
+ const state = readRepricingSwitch(options);
87
+ if (state.status === "scheduled") {
88
+ const current = resolveWorkspaceTableCapacity(band, options);
89
+ const target = resolveWorkspaceTableCapacity(band, { ...options, now: state.effectiveAt });
90
+ if (STORAGE_FIELDS.some(([field]) => current[field] !== target[field])) {
91
+ return { status: "scheduled", effectiveAt: state.effectiveAt, target };
92
+ }
93
+ }
94
+ return { status: "in_force", effectiveAt: null, target: null };
95
+ }