@oxygen-agent/cli 1.1010.650 → 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 (114) 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 +15 -1
  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/inbox-needs-reply-notice.d.ts +12 -0
  10. package/dist/inbox-needs-reply-notice.js +51 -0
  11. package/dist/index.js +756 -177
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/skills.js +48 -22
  15. package/dist/streamed-file-import.d.ts +58 -0
  16. package/dist/streamed-file-import.js +115 -0
  17. package/dist/update.d.ts +29 -0
  18. package/dist/update.js +62 -16
  19. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  20. package/dist/workflow-plan-limit-notices.js +28 -0
  21. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
  25. package/node_modules/@oxygen/shared/dist/billing.js +418 -45
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  29. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  31. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  32. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  33. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  36. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  37. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
  38. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  39. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
  42. package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  44. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  45. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  46. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  47. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  48. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  49. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
  50. package/node_modules/@oxygen/shared/dist/index.js +4 -43
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  54. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  55. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  56. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  58. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
  59. package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
  60. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  61. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  62. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  63. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  64. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
  65. package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
  66. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
  67. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
  69. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
  70. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  71. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  72. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  74. package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
  75. package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
  76. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  77. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  78. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
  79. package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
  80. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  81. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  82. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  83. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  84. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  85. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
  86. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
  87. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
  88. package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  91. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  93. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  94. package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
  95. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  96. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  97. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  98. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  99. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  100. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  101. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  102. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  103. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  104. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  105. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  106. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  107. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  108. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  109. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  110. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  111. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  112. package/package.json +1 -1
  113. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  114. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
@@ -0,0 +1,21 @@
1
+ export type SemanticVersion = {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ };
6
+ /**
7
+ * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
8
+ * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
9
+ * Returns `null` when the input is not a parseable `major.minor.patch` string.
10
+ */
11
+ export declare function parseSemver(version: string): SemanticVersion | null;
12
+ /**
13
+ * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
14
+ * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
15
+ * fail open rather than misordering garbage.
16
+ */
17
+ export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
18
+ /** True when `a` is a strictly greater semantic version than `b`. */
19
+ export declare function isVersionGreater(a: string, b: string): boolean;
20
+ /** True when `a` is a strictly lesser semantic version than `b`. */
21
+ export declare function isVersionLess(a: string, b: string): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
3
+ * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
4
+ * Returns `null` when the input is not a parseable `major.minor.patch` string.
5
+ */
6
+ export function parseSemver(version) {
7
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
8
+ if (!match)
9
+ return null;
10
+ return {
11
+ major: Number(match[1]),
12
+ minor: Number(match[2]),
13
+ patch: Number(match[3]),
14
+ };
15
+ }
16
+ /**
17
+ * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
18
+ * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
19
+ * fail open rather than misordering garbage.
20
+ */
21
+ export function compareSemver(a, b) {
22
+ const left = parseSemver(a);
23
+ const right = parseSemver(b);
24
+ if (!left || !right)
25
+ return 0;
26
+ for (const key of ["major", "minor", "patch"]) {
27
+ if (left[key] > right[key])
28
+ return 1;
29
+ if (left[key] < right[key])
30
+ return -1;
31
+ }
32
+ return 0;
33
+ }
34
+ /** True when `a` is a strictly greater semantic version than `b`. */
35
+ export function isVersionGreater(a, b) {
36
+ return compareSemver(a, b) > 0;
37
+ }
38
+ /** True when `a` is a strictly lesser semantic version than `b`. */
39
+ export function isVersionLess(a, b) {
40
+ return compareSemver(a, b) < 0;
41
+ }
@@ -0,0 +1,30 @@
1
+ import type { RepricingOptions } from "./repricing.js";
2
+ /**
3
+ * The daily cap a new mailbox gets when nobody names one. New mailboxes were
4
+ * created at 18 a day before the repricing, so this is an increase and is live.
5
+ */
6
+ export declare const EMAIL_MAILBOX_DEFAULT_DAILY_CAP: 25;
7
+ /**
8
+ * Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
9
+ * `user` = a person or agent set it. A row written before the column existed
10
+ * has no source (null): its cap may be the old column default or a choice.
11
+ */
12
+ export declare const EMAIL_MAILBOX_DAILY_CAP_SOURCES: readonly ["default", "user"];
13
+ export type EmailMailboxDailyCapSource = (typeof EMAIL_MAILBOX_DAILY_CAP_SOURCES)[number];
14
+ export declare function readEmailMailboxDailyCapSource(value: unknown): EmailMailboxDailyCapSource | 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
+ /**
18
+ * The configured cap in force for a stored one: every surface that reads a
19
+ * mailbox's cap, and the send claim, go through this.
20
+ *
21
+ * Stored choices are retained up to the technical maximum.
22
+ */
23
+ export declare function emailMailboxDailyCapInForce(input: {
24
+ storedCap: number;
25
+ source: EmailMailboxDailyCapSource | null;
26
+ }, options?: RepricingOptions): number;
27
+ /** A new voice number may dial this many times a day once it has warmed up. */
28
+ export declare const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
29
+ /** The most a voice number may be set to dial a day. */
30
+ export declare const VOICE_NUMBER_MAX_DAILY_CAP = 300;
@@ -0,0 +1,43 @@
1
+ // Per-channel sending limits that are the same on every plan (2026-09 repricing,
2
+ // decision L3; specification slice S52). Limits guard, credits meter: none of
3
+ // these is sold, and none charges for sending.
4
+ //
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.
8
+ //
9
+ // Voice per number: 100 dials a day by default, adjustable up to 300.
10
+ import { EMAIL_DELIVERABILITY_DEFAULTS } from "./email-deliverability-policy.js";
11
+ /**
12
+ * The daily cap a new mailbox gets when nobody names one. New mailboxes were
13
+ * created at 18 a day before the repricing, so this is an increase and is live.
14
+ */
15
+ export const EMAIL_MAILBOX_DEFAULT_DAILY_CAP = EMAIL_DELIVERABILITY_DEFAULTS.campaignDailyCap;
16
+ /**
17
+ * Who set a mailbox's stored cap. `default` = OXYGEN's default at creation,
18
+ * `user` = a person or agent set it. A row written before the column existed
19
+ * has no source (null): its cap may be the old column default or a choice.
20
+ */
21
+ export const EMAIL_MAILBOX_DAILY_CAP_SOURCES = ["default", "user"];
22
+ export function readEmailMailboxDailyCapSource(value) {
23
+ return value === "default" || value === "user" ? value : null;
24
+ }
25
+ /** The most a mailbox may be set to send a day, independent of billing activation. */
26
+ export function emailMailboxDailyCapMaximum(_options = {}) {
27
+ return 50;
28
+ }
29
+ /**
30
+ * The configured cap in force for a stored one: every surface that reads a
31
+ * mailbox's cap, and the send claim, go through this.
32
+ *
33
+ * Stored choices are retained up to the technical maximum.
34
+ */
35
+ export function emailMailboxDailyCapInForce(input, options = {}) {
36
+ const stored = Number.isFinite(input.storedCap) ? Math.max(0, Math.floor(input.storedCap)) : 0;
37
+ const maximum = emailMailboxDailyCapMaximum(options);
38
+ return maximum === null ? stored : Math.min(stored, maximum);
39
+ }
40
+ /** A new voice number may dial this many times a day once it has warmed up. */
41
+ export const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
42
+ /** The most a voice number may be set to dial a day. */
43
+ export const VOICE_NUMBER_MAX_DAILY_CAP = 300;
@@ -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);
@@ -172,8 +172,11 @@ function classifySequenceFailure(input) {
172
172
  .toLowerCase();
173
173
  // Ambiguous effects win every other classification. A 429-shaped code or a
174
174
  // provider name must never make a possibly-applied write safe to replay.
175
+ // `write[_-]?ambiguous` is the LinkedIn client's `provider_write_ambiguous`:
176
+ // stored rows written before that error carried `effect_outcome` would
177
+ // otherwise read back as a recipient hard bounce.
175
178
  if (input.effectOutcome === "unknown" ||
176
- /(?:effect|outcome)[_-]?unknown|ambiguous[_-]?(?:effect|write)/i.test(codes)) {
179
+ /(?:effect|outcome)[_-]?unknown|ambiguous[_-]?(?:effect|write)|write[_-]?ambiguous/i.test(codes)) {
177
180
  return "effect_unknown";
178
181
  }
179
182
  if ([input.code, input.providerSubtype].some((code) => isProviderFundingErrorCode(code)))
@@ -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,6 +17,32 @@ 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
+ export type { PlanBillingInterval };
21
+ /**
22
+ * The configured Price for one rung and interval, or "" when that Price is not
23
+ * configured (every yearly Price, until S14 creates them).
24
+ */
25
+ export declare function getPurchasablePlanPriceId(planKey: OxygenPlanKey, interval: PlanBillingInterval, currency?: BillingCurrency): string;
26
+ /**
27
+ * The billing interval of a recognized plan Price. "year" only for a configured
28
+ * yearly Oxygen Price; every other recognized Price (monthly rungs and every
29
+ * grandfathered or legacy plan) is "month"; an unrecognized id is null.
30
+ *
31
+ * This is the one definition the credit-grant paths read, so the Stripe webhook,
32
+ * the lazy grant on a balance read and the slice sweep can never disagree about
33
+ * whether a paid period is sliced.
34
+ */
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;
44
+ /** Every configured yearly plan Price id (empty until S14). */
45
+ export declare function configuredAnnualPlanPriceIds(): readonly string[];
20
46
  /**
21
47
  * Resolve a Stripe price id to the plan key stored in `subscriptions.tier`.
22
48
  *
@@ -30,6 +56,12 @@ export declare function tierFromPriceId(priceId: string): string | null;
30
56
  * Complete runtime-recognized Price set grouped by the current self-serve
31
57
  * family. Catalog audit/migration code consumes this instead of maintaining a
32
58
  * second env map that can silently omit grandfathered subscriptions.
59
+ *
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.
33
65
  */
34
66
  export declare function recognizedStripePriceIdsBySelfServeTier(): Record<SelfServePlanTier, readonly string[]>;
35
67
  export declare function formatMonthlyPrice(priceCents: number | null, currency?: BillingCurrency): string;
@@ -19,6 +19,25 @@ const STRIPE_PRICE_OXYGEN_IDS = {
19
19
  oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_USD ?? "" },
20
20
  oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_USD ?? "" },
21
21
  };
22
+ /**
23
+ * The yearly Price of each rung (Repricing 2026-09, decision 1.2: pay 10 months,
24
+ * get 12, credits still granted monthly). A second Price on the rung's existing
25
+ * Product, since month and year are distinct intervals a Portal configuration
26
+ * accepts under one Product.
27
+ *
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
30
+ * unset id is "", and tierFromPriceId refuses "" before any map is consulted, so
31
+ * no subscription resolves to a yearly interval and nothing is ever sliced.
32
+ */
33
+ const STRIPE_PRICE_OXYGEN_ANNUAL_IDS = {
34
+ oxygen_49: { usd: process.env.STRIPE_PRICE_OXYGEN_49_ANNUAL_USD ?? "" },
35
+ oxygen_99: { usd: process.env.STRIPE_PRICE_OXYGEN_99_ANNUAL_USD ?? "" },
36
+ oxygen_199: { usd: process.env.STRIPE_PRICE_OXYGEN_199_ANNUAL_USD ?? "" },
37
+ oxygen_499: { usd: process.env.STRIPE_PRICE_OXYGEN_499_ANNUAL_USD ?? "" },
38
+ oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_ANNUAL_USD ?? "" },
39
+ oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_ANNUAL_USD ?? "" },
40
+ };
22
41
  // Grandfathered on 2026-09-19. These three stay pointed at their LIVE prices
23
42
  // (35 Starter + 2 Pro subscriptions bill against them) rather than moving into
24
43
  // the legacy CSV lists: tierFromPriceId must keep returning "starter"/"pro"/
@@ -82,6 +101,49 @@ export function getPlanPriceId(plan, currency) {
82
101
  return (plan.priceIds[currency]
83
102
  || (currency === DEFAULT_BILLING_CURRENCY ? plan.priceId : ""));
84
103
  }
104
+ /**
105
+ * The configured Price for one rung and interval, or "" when that Price is not
106
+ * configured (every yearly Price, until S14 creates them).
107
+ */
108
+ export function getPurchasablePlanPriceId(planKey, interval, currency = DEFAULT_BILLING_CURRENCY) {
109
+ const ids = interval === "year"
110
+ ? STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]
111
+ : STRIPE_PRICE_OXYGEN_IDS[planKey];
112
+ return ids[currency] || "";
113
+ }
114
+ /**
115
+ * The billing interval of a recognized plan Price. "year" only for a configured
116
+ * yearly Oxygen Price; every other recognized Price (monthly rungs and every
117
+ * grandfathered or legacy plan) is "month"; an unrecognized id is null.
118
+ *
119
+ * This is the one definition the credit-grant paths read, so the Stripe webhook,
120
+ * the lazy grant on a balance read and the slice sweep can never disagree about
121
+ * whether a paid period is sliced.
122
+ */
123
+ export function planBillingIntervalForPriceId(priceId) {
124
+ if (!priceId)
125
+ return null;
126
+ for (const planKey of PURCHASABLE_PLAN_KEYS) {
127
+ if (Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
128
+ return "year";
129
+ }
130
+ }
131
+ return tierFromPriceId(priceId) === null ? null : "month";
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
+ }
143
+ /** Every configured yearly plan Price id (empty until S14). */
144
+ export function configuredAnnualPlanPriceIds() {
145
+ return uniqueNonEmptyPriceIds(PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey])));
146
+ }
85
147
  /**
86
148
  * Resolve a Stripe price id to the plan key stored in `subscriptions.tier`.
87
149
  *
@@ -97,7 +159,8 @@ export function tierFromPriceId(priceId) {
97
159
  if (!priceId)
98
160
  return null;
99
161
  for (const planKey of PURCHASABLE_PLAN_KEYS) {
100
- if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)) {
162
+ if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)
163
+ || Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
101
164
  return planKey;
102
165
  }
103
166
  }
@@ -122,11 +185,18 @@ export function tierFromPriceId(priceId) {
122
185
  * Complete runtime-recognized Price set grouped by the current self-serve
123
186
  * family. Catalog audit/migration code consumes this instead of maintaining a
124
187
  * second env map that can silently omit grandfathered subscriptions.
188
+ *
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.
125
194
  */
126
195
  export function recognizedStripePriceIdsBySelfServeTier() {
127
196
  const byTier = {
128
197
  oxygen: [
129
198
  ...PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey])),
199
+ ...configuredAnnualPlanPriceIds(),
130
200
  ...LEGACY_CURRENT_PLAN_PRICE_IDS.oxygen,
131
201
  ],
132
202
  starter: [