@oxygen-agent/cli 1.987.20 → 1.1003.12

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 (106) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/command-manifest.d.ts +3 -2
  6. package/dist/command-manifest.js +10 -0
  7. package/dist/credentials.d.ts +1 -1
  8. package/dist/functions-commands.js +6 -2
  9. package/dist/help.d.ts +8 -0
  10. package/dist/help.js +46 -0
  11. package/dist/index.js +1569 -123
  12. package/dist/knowledge-mirror.d.ts +2 -2
  13. package/dist/runtime.d.ts +0 -15
  14. package/dist/runtime.js +1 -1
  15. package/dist/session.d.ts +4 -3
  16. package/dist/skills.d.ts +8 -7
  17. package/dist/skills.js +24 -10
  18. package/dist/transcript.d.ts +2 -1
  19. package/dist/util.d.ts +1 -1
  20. package/dist/util.js +1 -3
  21. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  22. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  23. package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
  24. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  25. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  26. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
  27. package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
  28. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  29. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  30. package/node_modules/@oxygen/shared/dist/billing.d.ts +78 -22
  31. package/node_modules/@oxygen/shared/dist/billing.js +150 -40
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
  33. package/node_modules/@oxygen/shared/dist/capability-discovery.js +89 -16
  34. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  35. package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
  36. package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
  37. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
  38. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
  39. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  40. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  41. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  42. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  43. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  44. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  45. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
  46. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
  47. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  48. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  49. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  50. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  51. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  52. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  53. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  54. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  55. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  56. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  57. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  58. package/node_modules/@oxygen/shared/dist/index.d.ts +13 -0
  59. package/node_modules/@oxygen/shared/dist/index.js +13 -0
  60. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  61. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  62. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
  63. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
  64. package/node_modules/@oxygen/shared/dist/langfuse.js +9 -0
  65. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
  66. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
  67. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  68. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  69. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  70. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  71. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  72. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  73. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
  74. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
  75. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  76. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  77. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  78. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  79. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  80. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  81. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  82. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  83. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
  84. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
  85. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  86. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  87. package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
  88. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  89. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  90. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  91. package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
  92. package/node_modules/@oxygen/shared/dist/sequences.js +110 -4
  93. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  94. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  95. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  96. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  97. package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
  98. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  99. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  100. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  101. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  102. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  103. package/node_modules/@oxygen/shared/package.json +50 -0
  104. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  105. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  106. package/package.json +2 -2
@@ -3,7 +3,7 @@
3
3
  * page's "what credits buy" table, the pricing calculator, and docs.
4
4
  *
5
5
  * Pricing Model 2.0 (founder-ratified 2026-07-14): ONE credit pool pays for
6
- * everything; 1 USD = 1,000 credits.
6
+ * everything; 1 USD = 100 credits.
7
7
  *
8
8
  * THIS FILE NO LONGER DECLARES PRICES. It used to carry hand-copied literals
9
9
  * under a comment asking the next person to remember to update them — which is
@@ -34,19 +34,19 @@
34
34
  * loadPricingBook() instead of importing this file, so a staff re-price at
35
35
  * /admin/costs takes effect without a deploy.
36
36
  */
37
- import { AUTOMATION_ACTION_CREDITS as SNAPSHOT_AUTOMATION_ACTION_CREDITS, CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, DELIVERABILITY_INCLUDED_TESTS_PER_INBOX as SNAPSHOT_DELIVERABILITY_INCLUDED_TESTS_PER_INBOX, DELIVERABILITY_SEAT_CREDITS_PER_MONTH as SNAPSHOT_DELIVERABILITY_SEAT_CREDITS_PER_MONTH, EGRESS_IP_CREDITS_PER_MONTH as SNAPSHOT_EGRESS_IP_CREDITS_PER_MONTH, MANAGED_INBOX_USD_CENTS as SNAPSHOT_MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS as SNAPSHOT_SENDING_DOMAIN_USD_CENTS, SENDING_SEAT_USD_CENTS as SNAPSHOT_SENDING_SEAT_USD_CENTS, ENRICHMENT_CREDITS_PER_ROW_TYPICAL as SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL, LINKEDIN_ACCOUNT_CREDITS_PER_MONTH as SNAPSHOT_LINKEDIN_ACCOUNT_CREDITS_PER_MONTH, LINKEDIN_ACTION_CREDITS as SNAPSHOT_LINKEDIN_ACTION_CREDITS, LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH as SNAPSHOT_LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH, MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL as SNAPSHOT_MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL, MANAGED_DOMAIN_MARKUP as SNAPSHOT_MANAGED_DOMAIN_MARKUP, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_INBOX_CREDITS_PER_MONTH, MANAGED_INBOX_MARKUP as SNAPSHOT_MANAGED_INBOX_MARKUP, PLACEMENT_TEST_OVERAGE_CREDITS as SNAPSHOT_PLACEMENT_TEST_OVERAGE_CREDITS, PRICE_SHEET_VERIFIED as SNAPSHOT_PRICE_SHEET_VERIFIED, SEND_CREDITS as SNAPSHOT_SEND_CREDITS, SUBSCRIPTION_USD_PER_1K as SNAPSHOT_SUBSCRIPTION_USD_PER_1K, TOPUP_USD_PER_1K as SNAPSHOT_TOPUP_USD_PER_1K, VOICE_CREDITS_PER_AMD_CALL as SNAPSHOT_VOICE_CREDITS_PER_AMD_CALL, VOICE_CREDITS_PER_MINUTE as SNAPSHOT_VOICE_CREDITS_PER_MINUTE, VOICE_MARKUP as SNAPSHOT_VOICE_MARKUP, VOICE_NUMBER_CREDITS_PER_MONTH as SNAPSHOT_VOICE_NUMBER_CREDITS_PER_MONTH, WHATSAPP_ACCOUNT_CREDITS_PER_MONTH as SNAPSHOT_WHATSAPP_ACCOUNT_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
37
+ import { AUTOMATION_ACTION_CREDITS as SNAPSHOT_AUTOMATION_ACTION_CREDITS, CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, DELIVERABILITY_INCLUDED_TESTS_PER_INBOX as SNAPSHOT_DELIVERABILITY_INCLUDED_TESTS_PER_INBOX, DELIVERABILITY_SEAT_CREDITS_PER_MONTH as SNAPSHOT_DELIVERABILITY_SEAT_CREDITS_PER_MONTH, EGRESS_IP_CREDITS_PER_MONTH as SNAPSHOT_EGRESS_IP_CREDITS_PER_MONTH, MANAGED_INBOX_USD_CENTS as SNAPSHOT_MANAGED_INBOX_USD_CENTS, SENDING_DOMAIN_USD_CENTS as SNAPSHOT_SENDING_DOMAIN_USD_CENTS, SENDING_SEAT_USD_CENTS as SNAPSHOT_SENDING_SEAT_USD_CENTS, ENRICHMENT_CREDITS_PER_ROW_TYPICAL as SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL, LINKEDIN_ACCOUNT_CREDITS_PER_MONTH as SNAPSHOT_LINKEDIN_ACCOUNT_CREDITS_PER_MONTH, LINKEDIN_ACTION_CREDITS as SNAPSHOT_LINKEDIN_ACTION_CREDITS, LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH as SNAPSHOT_LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH, MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL as SNAPSHOT_MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL, MANAGED_DOMAIN_MARKUP as SNAPSHOT_MANAGED_DOMAIN_MARKUP, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_INBOX_CREDITS_PER_MONTH, MANAGED_INBOX_MARKUP as SNAPSHOT_MANAGED_INBOX_MARKUP, PLACEMENT_TEST_OVERAGE_CREDITS as SNAPSHOT_PLACEMENT_TEST_OVERAGE_CREDITS, PRICE_SHEET_VERIFIED as SNAPSHOT_PRICE_SHEET_VERIFIED, SEND_CREDITS as SNAPSHOT_SEND_CREDITS, SUBSCRIPTION_USD_PER_100 as SNAPSHOT_SUBSCRIPTION_USD_PER_100, TOPUP_USD_PER_100 as SNAPSHOT_TOPUP_USD_PER_100, VOICE_CREDITS_PER_AMD_CALL as SNAPSHOT_VOICE_CREDITS_PER_AMD_CALL, VOICE_CREDITS_PER_MINUTE as SNAPSHOT_VOICE_CREDITS_PER_MINUTE, VOICE_MARKUP as SNAPSHOT_VOICE_MARKUP, VOICE_NUMBER_CREDITS_PER_MONTH as SNAPSHOT_VOICE_NUMBER_CREDITS_PER_MONTH, WHATSAPP_ACCOUNT_CREDITS_PER_MONTH as SNAPSHOT_WHATSAPP_ACCOUNT_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
38
38
  export { creditsToUsd, usdToCredits } from "./pricing-snapshot.generated.js";
39
39
  export const PRICING_SHEET_VERIFIED = SNAPSHOT_PRICE_SHEET_VERIFIED;
40
40
  export const PRICING_CREDITS_PER_USD = SNAPSHOT_CREDITS_PER_USD;
41
- /** On-demand top-up price per 1,000 credits (USD); subscriptions pay $1.00. */
42
- export const TOPUP_USD_PER_1K = SNAPSHOT_TOPUP_USD_PER_1K;
41
+ /** On-demand top-up price per 100 credits (USD); subscriptions pay $1.00. */
42
+ export const TOPUP_USD_PER_100 = SNAPSHOT_TOPUP_USD_PER_100;
43
43
  // Dollar-priced CAPACITY, re-exported on the client-safe subpath so the pricing
44
44
  // calculator and the marketing sections quote the same numbers the product
45
45
  // charges. These are not credits and must never be converted as if they were.
46
46
  export const SENDING_SEAT_USD_CENTS = SNAPSHOT_SENDING_SEAT_USD_CENTS;
47
47
  export const MANAGED_INBOX_USD_CENTS = SNAPSHOT_MANAGED_INBOX_USD_CENTS;
48
48
  export const SENDING_DOMAIN_USD_CENTS = SNAPSHOT_SENDING_DOMAIN_USD_CENTS;
49
- export const SUBSCRIPTION_USD_PER_1K = SNAPSHOT_SUBSCRIPTION_USD_PER_1K;
49
+ export const SUBSCRIPTION_USD_PER_100 = SNAPSHOT_SUBSCRIPTION_USD_PER_100;
50
50
  /**
51
51
  * AI column runs, honest 5× of modeled tier COGS.
52
52
  *
@@ -56,23 +56,30 @@ export const SUBSCRIPTION_USD_PER_1K = SNAPSHOT_SUBSCRIPTION_USD_PER_1K;
56
56
  * against the live estimator, so a tier re-price fails a test rather than
57
57
  * quietly mispricing the page.
58
58
  */
59
- export const AI_COLUMN_CREDITS = { low: 15, medium: 75, high: 250 };
59
+ export const AI_COLUMN_CREDITS = { low: 1.5, medium: 7.5, high: 25 };
60
60
  /**
61
- * Web-search grounding, billed per SEARCH on top of the model — not a flat adder.
61
+ * Web-search grounding, billed per PROVIDER CALL on top of the model — not a flat
62
+ * adder.
62
63
  *
63
- * This replaces `AI_COLUMN_WEB_SEARCH_SURCHARGE_CREDITS = 25`, which was a real
64
- * under-quote on a public pricing surface: it was a leftover from the bundled
65
- * `*_with_web_search` model tiers that were removed when grounding moved onto
66
- * native providers, and the billing path never read it. Actual customer price is
67
- * the managed provider's own per-search charge (cogs-rates.ts): exa 35, parallel
68
- * 5, firecrawl 80 at the default 5 results. The default column runs exa primary
69
- * with parallel as its one fallback (WEB_SEARCH_ENGINES), so a row costs 35 when
70
- * the primary answers and reserves 40 for the case where both lanes fire.
64
+ * This replaced `AI_COLUMN_WEB_SEARCH_SURCHARGE_CREDITS = 25`, a real under-quote on
65
+ * a public surface: a leftover from the bundled `*_with_web_search` model tiers that
66
+ * were removed when grounding moved onto native providers, which the billing path
67
+ * never read.
71
68
  *
72
- * Resolver-derived like AI_COLUMN_CREDITS above, and pinned by the same test.
69
+ * The figures below describe the DEFAULT research column, which since D53 discovers
70
+ * then reads: `serper.search` (0.5) finds the URLs, then one additive
71
+ * `exa.contents` read (0.5) turns the top result's ~160-char SERP teaser into real
72
+ * page text. So a typical row spends 1 and a preview reserves 4.5, because the
73
+ * ceiling must also cover the `exa.search` fallback (3.5) firing when serper finds
74
+ * nothing. The read carries no fallback of its own — a read miss leaves the row
75
+ * grounded on the snippets it already has.
76
+ *
77
+ * Both numbers are RESOLVER-DERIVED and pinned by pricing-sheet-conformance.test.ts
78
+ * against the shipped default, so a re-price or a lane change fails a test rather
79
+ * than quietly mispricing this page. Do not hand-edit them.
73
80
  */
74
- export const AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS = 35;
75
- export const AI_COLUMN_WEB_SEARCH_RESERVED_CREDITS = 40;
81
+ export const AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS = 1;
82
+ export const AI_COLUMN_WEB_SEARCH_RESERVED_CREDITS = 4.5;
76
83
  /** Workflow/automation actions (steps, tool calls, row writes) — capacity guard. */
77
84
  export const AUTOMATION_ACTION_CREDITS = SNAPSHOT_AUTOMATION_ACTION_CREDITS;
78
85
  /**
@@ -157,13 +164,17 @@ export const SEND_CREDITS = SNAPSHOT_SEND_CREDITS;
157
164
  * pricing seed.
158
165
  */
159
166
  export const ENRICHMENT_CREDITS = {
160
- email_verify: 2.245, // MillionVerifier first pass (exactly 5x COGS)
161
- email_find_typical: 85, // Hunter-class finder; cheap waterfall steps run 15–99
162
- phone_find_typical: 990, // Prospeo/Findymail-class mobile
163
- company_enrich_typical: 99,
164
- web_search: 5, // Serper
165
- page_scrape: 40, // Firecrawl (wedge price)
166
- linkedin_profile_scrape: 10, // Managed profile credit at $0.002 COGS, exactly 5x
167
+ // The ONLY price in the 2026-09-19 re-denomination that is not an exact /10.
168
+ // 2.245 / 10 = 0.2245, and credits are numeric(18,3) — the fourth decimal does
169
+ // not exist in the ledger. Rounded UP to 0.225 (+0.22%) per this file's
170
+ // standing preference for overestimating a quote rather than under-charging.
171
+ email_verify: 0.225, // MillionVerifier first pass (~5x COGS; see note above)
172
+ email_find_typical: 8.5, // Hunter-class finder; cheap waterfall steps run 1.5–9.9
173
+ phone_find_typical: 99, // Prospeo/Findymail-class mobile
174
+ company_enrich_typical: 9.9,
175
+ web_search: 0.5, // Serper
176
+ page_scrape: 4, // Firecrawl (wedge price)
177
+ linkedin_profile_scrape: 1, // Managed profile credit at $0.002 COGS, exactly 5x
167
178
  };
168
179
  /** Editable calculator default: blended credits per fully-enriched row. */
169
180
  export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL;
@@ -1,33 +1,33 @@
1
1
  /** Period the customer-facing price sheet was last reconciled end to end. */
2
2
  export declare const PRICE_SHEET_VERIFIED = "2026-07";
3
- /** $1 = 1_000 credits (Pricing Model 2.0). */
4
- export declare const CREDITS_PER_USD = 1000;
3
+ /** $1 = 100 credits (Pricing Model 2.1). */
4
+ export declare const CREDITS_PER_USD = 100;
5
5
  /** The divisor in the markup formula; exact inverse of CREDITS_PER_USD. */
6
- export declare const CREDIT_USD_VALUE = 0.001;
7
- /** On-demand top-up, in USD cents per 1,000 credits. */
8
- export declare const TOPUP_USD_CENTS_PER_1K_CREDITS = 125;
9
- /** On-demand top-up price per 1,000 credits, in USD. */
10
- export declare const TOPUP_USD_PER_1K = 1.25;
6
+ export declare const CREDIT_USD_VALUE = 0.01;
7
+ /** On-demand top-up, in USD cents per 100 credits. */
8
+ export declare const TOPUP_USD_CENTS_PER_100_CREDITS = 125;
9
+ /** On-demand top-up price per 100 credits, in USD. */
10
+ export declare const TOPUP_USD_PER_100 = 1.25;
11
11
  /** Subscription credits convert at face value, making them exactly 20% cheaper. */
12
- export declare const SUBSCRIPTION_USD_PER_1K = 1;
12
+ export declare const SUBSCRIPTION_USD_PER_100 = 1;
13
13
  export declare const MANAGED_INBOX_MARKUP = 1.25;
14
14
  export declare const MANAGED_DOMAIN_MARKUP = 1.25;
15
15
  export declare const VOICE_MARKUP = 5;
16
16
  /** Applied to actual provider inference cost at settle time. */
17
17
  export declare const COPILOT_CREDIT_MARKUP_MULTIPLIER = 5;
18
- export declare const LINKEDIN_ACCOUNT_CREDITS_PER_MONTH = 10000;
19
- export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 10000;
20
- export declare const EGRESS_IP_CREDITS_PER_MONTH = 25000;
21
- export declare const SENDING_MAILBOX_CREDITS_PER_MONTH = 1000;
22
- export declare const MANAGED_INBOX_CREDITS_PER_MONTH = 3000;
23
- export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 1000;
24
- export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1000;
18
+ export declare const LINKEDIN_ACCOUNT_CREDITS_PER_MONTH = 1000;
19
+ export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 1000;
20
+ export declare const EGRESS_IP_CREDITS_PER_MONTH = 2500;
21
+ export declare const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
22
+ export declare const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
23
+ export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 100;
24
+ export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
25
25
  /** Included inbox-placement tests per subscribed inbox per cycle. */
26
26
  export declare const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
27
27
  /** A placement test beyond the included allowance. */
28
- export declare const PLACEMENT_TEST_OVERAGE_CREDITS = 166;
28
+ export declare const PLACEMENT_TEST_OVERAGE_CREDITS = 16.6;
29
29
  /** Representative, not billed: a typical .com at MANAGED_DOMAIN_MARKUP. */
30
- export declare const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 13050;
30
+ export declare const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 1305;
31
31
  export declare const SENDING_SEAT_USD_CENTS: {
32
32
  readonly linkedin: 3000;
33
33
  readonly whatsapp: 3000;
@@ -37,11 +37,11 @@ export declare const SENDING_SEAT_USD_CENTS: {
37
37
  export type SendingSeatKey = keyof typeof SENDING_SEAT_USD_CENTS;
38
38
  export declare const MANAGED_INBOX_USD_CENTS = 400;
39
39
  export declare const SENDING_DOMAIN_USD_CENTS = 1250;
40
- export declare const VOICE_NUMBER_CREDITS_PER_MONTH = 5000;
41
- export declare const VOICE_CREDITS_PER_MINUTE = 90;
42
- export declare const VOICE_CREDITS_PER_AMD_CALL = 38;
40
+ export declare const VOICE_NUMBER_CREDITS_PER_MONTH = 500;
41
+ export declare const VOICE_CREDITS_PER_MINUTE = 9;
42
+ export declare const VOICE_CREDITS_PER_AMD_CALL = 3.8;
43
43
  /** Workflow steps, tool calls and row writes — a capacity guard, not a margin line. */
44
- export declare const AUTOMATION_ACTION_CREDITS = 0.01;
44
+ export declare const AUTOMATION_ACTION_CREDITS = 0.001;
45
45
  /** Email, LinkedIn and WhatsApp sends are free; the pool pays for seats and inboxes. */
46
46
  export declare const SEND_CREDITS = 0;
47
47
  /** Every LinkedIn action is 0 under the pure seat model. */
@@ -54,7 +54,7 @@ export declare const LINKEDIN_ACTION_CREDITS: {
54
54
  readonly like_post: 0;
55
55
  readonly comment_post: 0;
56
56
  };
57
- export declare const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 100;
57
+ export declare const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 10;
58
58
  export declare const LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH = 0;
59
59
  export type PricingPlanSnapshot = {
60
60
  readonly planKey: string;
@@ -15,16 +15,16 @@
15
15
  /** Period the customer-facing price sheet was last reconciled end to end. */
16
16
  export const PRICE_SHEET_VERIFIED = "2026-07";
17
17
  // --- conversion -------------------------------------------------------------
18
- /** $1 = 1_000 credits (Pricing Model 2.0). */
19
- export const CREDITS_PER_USD = 1_000;
18
+ /** $1 = 100 credits (Pricing Model 2.1). */
19
+ export const CREDITS_PER_USD = 100;
20
20
  /** The divisor in the markup formula; exact inverse of CREDITS_PER_USD. */
21
- export const CREDIT_USD_VALUE = 0.001;
22
- /** On-demand top-up, in USD cents per 1,000 credits. */
23
- export const TOPUP_USD_CENTS_PER_1K_CREDITS = 125;
24
- /** On-demand top-up price per 1,000 credits, in USD. */
25
- export const TOPUP_USD_PER_1K = 1.25;
21
+ export const CREDIT_USD_VALUE = 0.01;
22
+ /** On-demand top-up, in USD cents per 100 credits. */
23
+ export const TOPUP_USD_CENTS_PER_100_CREDITS = 125;
24
+ /** On-demand top-up price per 100 credits, in USD. */
25
+ export const TOPUP_USD_PER_100 = 1.25;
26
26
  /** Subscription credits convert at face value, making them exactly 20% cheaper. */
27
- export const SUBSCRIPTION_USD_PER_1K = 1;
27
+ export const SUBSCRIPTION_USD_PER_100 = 1;
28
28
  // --- markups ----------------------------------------------------------------
29
29
  export const MANAGED_INBOX_MARKUP = 1.25;
30
30
  export const MANAGED_DOMAIN_MARKUP = 1.25;
@@ -32,19 +32,19 @@ export const VOICE_MARKUP = 5;
32
32
  /** Applied to actual provider inference cost at settle time. */
33
33
  export const COPILOT_CREDIT_MARKUP_MULTIPLIER = 5;
34
34
  // --- seats and recurring charges --------------------------------------------
35
- export const LINKEDIN_ACCOUNT_CREDITS_PER_MONTH = 10_000;
36
- export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 10_000;
37
- export const EGRESS_IP_CREDITS_PER_MONTH = 25_000;
38
- export const SENDING_MAILBOX_CREDITS_PER_MONTH = 1_000;
39
- export const MANAGED_INBOX_CREDITS_PER_MONTH = 3_000;
40
- export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 1_000;
41
- export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1_000;
35
+ export const LINKEDIN_ACCOUNT_CREDITS_PER_MONTH = 1_000;
36
+ export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 1_000;
37
+ export const EGRESS_IP_CREDITS_PER_MONTH = 2_500;
38
+ export const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
39
+ export const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
40
+ export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 100;
41
+ export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
42
42
  /** Included inbox-placement tests per subscribed inbox per cycle. */
43
43
  export const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
44
44
  /** A placement test beyond the included allowance. */
45
- export const PLACEMENT_TEST_OVERAGE_CREDITS = 166;
45
+ export const PLACEMENT_TEST_OVERAGE_CREDITS = 16.6;
46
46
  /** Representative, not billed: a typical .com at MANAGED_DOMAIN_MARKUP. */
47
- export const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 13_050;
47
+ export const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 1_305;
48
48
  // --- sending seats (USD, Stripe-billed) --------------------------------------
49
49
  //
50
50
  // The seat model (OXP-15). These are DOLLARS, not credits, and they do not
@@ -74,12 +74,12 @@ export const SENDING_SEAT_USD_CENTS = {
74
74
  export const MANAGED_INBOX_USD_CENTS = 400;
75
75
  export const SENDING_DOMAIN_USD_CENTS = 1_250;
76
76
  // --- voice ------------------------------------------------------------------
77
- export const VOICE_NUMBER_CREDITS_PER_MONTH = 5_000;
78
- export const VOICE_CREDITS_PER_MINUTE = 90;
79
- export const VOICE_CREDITS_PER_AMD_CALL = 38;
77
+ export const VOICE_NUMBER_CREDITS_PER_MONTH = 500;
78
+ export const VOICE_CREDITS_PER_MINUTE = 9;
79
+ export const VOICE_CREDITS_PER_AMD_CALL = 3.8;
80
80
  // --- per-action -------------------------------------------------------------
81
81
  /** Workflow steps, tool calls and row writes — a capacity guard, not a margin line. */
82
- export const AUTOMATION_ACTION_CREDITS = 0.01;
82
+ export const AUTOMATION_ACTION_CREDITS = 0.001;
83
83
  /** Email, LinkedIn and WhatsApp sends are free; the pool pays for seats and inboxes. */
84
84
  export const SEND_CREDITS = 0;
85
85
  /** Every LinkedIn action is 0 under the pure seat model. */
@@ -93,23 +93,29 @@ export const LINKEDIN_ACTION_CREDITS = {
93
93
  comment_post: 0,
94
94
  };
95
95
  // --- pricing-calculator defaults (not prices) -------------------------------
96
- export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 100;
96
+ export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 10;
97
97
  export const LINKEDIN_TYPICAL_ACTIONS_CREDITS_PER_MONTH = 0;
98
98
  export const PRICING_PLAN_SNAPSHOT = [
99
99
  { planKey: "free", tier: "free", isLegacy: false, monthlyPriceCents: 0, monthlyCredits: 0, weeklyCreditsLimit: null, rolloverCap: null, byokEnabled: false },
100
- { planKey: "starter", tier: "starter", isLegacy: false, monthlyPriceCents: 9_900, monthlyCredits: 99_000, weeklyCreditsLimit: null, rolloverCap: 99_000, byokEnabled: true },
101
- { planKey: "pro", tier: "pro", isLegacy: false, monthlyPriceCents: 24_900, monthlyCredits: 249_000, weeklyCreditsLimit: null, rolloverCap: 249_000, byokEnabled: true },
102
- { planKey: "team", tier: "team", isLegacy: false, monthlyPriceCents: 74_900, monthlyCredits: 749_000, weeklyCreditsLimit: null, rolloverCap: 749_000, byokEnabled: true },
103
- { planKey: "scale", tier: "scale", isLegacy: false, monthlyPriceCents: null, monthlyCredits: 10_000_000, weeklyCreditsLimit: null, rolloverCap: 10_000_000, byokEnabled: true },
100
+ { planKey: "oxygen_49", tier: "oxygen", isLegacy: false, monthlyPriceCents: 4_900, monthlyCredits: 4_900, weeklyCreditsLimit: null, rolloverCap: 4_900, byokEnabled: true },
101
+ { planKey: "oxygen_99", tier: "oxygen", isLegacy: false, monthlyPriceCents: 9_900, monthlyCredits: 9_900, weeklyCreditsLimit: null, rolloverCap: 9_900, byokEnabled: true },
102
+ { planKey: "oxygen_199", tier: "oxygen", isLegacy: false, monthlyPriceCents: 19_900, monthlyCredits: 19_900, weeklyCreditsLimit: null, rolloverCap: 19_900, byokEnabled: true },
103
+ { planKey: "oxygen_499", tier: "oxygen", isLegacy: false, monthlyPriceCents: 49_900, monthlyCredits: 49_900, weeklyCreditsLimit: null, rolloverCap: 49_900, byokEnabled: true },
104
+ { planKey: "oxygen_999", tier: "oxygen", isLegacy: false, monthlyPriceCents: 99_900, monthlyCredits: 99_900, weeklyCreditsLimit: null, rolloverCap: 99_900, byokEnabled: true },
105
+ { planKey: "oxygen_1999", tier: "oxygen", isLegacy: false, monthlyPriceCents: 199_900, monthlyCredits: 199_900, weeklyCreditsLimit: null, rolloverCap: 199_900, byokEnabled: true },
106
+ { planKey: "scale", tier: "scale", isLegacy: false, monthlyPriceCents: null, monthlyCredits: 1_000_000, weeklyCreditsLimit: null, rolloverCap: 1_000_000, byokEnabled: true },
104
107
  { planKey: "enterprise", tier: "enterprise", isLegacy: false, monthlyPriceCents: null, monthlyCredits: null, weeklyCreditsLimit: null, rolloverCap: null, byokEnabled: true },
105
- { planKey: "launch_50", tier: "starter", isLegacy: true, monthlyPriceCents: 5_000, monthlyCredits: 200_000, weeklyCreditsLimit: 200_000, rolloverCap: 200_000, byokEnabled: true },
106
- { planKey: "starter_50", tier: "starter", isLegacy: true, monthlyPriceCents: 5_000, monthlyCredits: 200_000, weeklyCreditsLimit: 200_000, rolloverCap: 200_000, byokEnabled: true },
107
- { planKey: "pro_149", tier: "pro", isLegacy: true, monthlyPriceCents: 14_900, monthlyCredits: 1_000_000, weeklyCreditsLimit: 1_000_000, rolloverCap: 1_000_000, byokEnabled: true },
108
- { planKey: "team_399", tier: "team", isLegacy: true, monthlyPriceCents: 39_900, monthlyCredits: 3_000_000, weeklyCreditsLimit: 3_000_000, rolloverCap: 3_000_000, byokEnabled: true },
109
- { planKey: "growth_250", tier: "pro", isLegacy: true, monthlyPriceCents: 25_000, monthlyCredits: 250_000, weeklyCreditsLimit: 100_000, rolloverCap: 250_000, byokEnabled: true },
110
- { planKey: "scale_750", tier: "team", isLegacy: true, monthlyPriceCents: 75_000, monthlyCredits: 750_000, weeklyCreditsLimit: 300_000, rolloverCap: 750_000, byokEnabled: true },
111
- { planKey: "starter_legacy", tier: "starter", isLegacy: true, monthlyPriceCents: null, monthlyCredits: 200_000, weeklyCreditsLimit: null, rolloverCap: 200_000, byokEnabled: true },
112
- { planKey: "scale_legacy", tier: "scale", isLegacy: true, monthlyPriceCents: null, monthlyCredits: 1_200_000, weeklyCreditsLimit: null, rolloverCap: 2_400_000, byokEnabled: true },
108
+ { planKey: "starter", tier: "starter", isLegacy: true, monthlyPriceCents: 9_900, monthlyCredits: 9_900, weeklyCreditsLimit: null, rolloverCap: 9_900, byokEnabled: true },
109
+ { planKey: "pro", tier: "pro", isLegacy: true, monthlyPriceCents: 24_900, monthlyCredits: 24_900, weeklyCreditsLimit: null, rolloverCap: 24_900, byokEnabled: true },
110
+ { planKey: "team", tier: "team", isLegacy: true, monthlyPriceCents: 74_900, monthlyCredits: 74_900, weeklyCreditsLimit: null, rolloverCap: 74_900, byokEnabled: true },
111
+ { planKey: "launch_50", tier: "starter", isLegacy: true, monthlyPriceCents: 5_000, monthlyCredits: 20_000, weeklyCreditsLimit: 20_000, rolloverCap: 20_000, byokEnabled: true },
112
+ { planKey: "starter_50", tier: "starter", isLegacy: true, monthlyPriceCents: 5_000, monthlyCredits: 20_000, weeklyCreditsLimit: 20_000, rolloverCap: 20_000, byokEnabled: true },
113
+ { planKey: "pro_149", tier: "pro", isLegacy: true, monthlyPriceCents: 14_900, monthlyCredits: 100_000, weeklyCreditsLimit: 100_000, rolloverCap: 100_000, byokEnabled: true },
114
+ { planKey: "team_399", tier: "team", isLegacy: true, monthlyPriceCents: 39_900, monthlyCredits: 300_000, weeklyCreditsLimit: 300_000, rolloverCap: 300_000, byokEnabled: true },
115
+ { planKey: "growth_250", tier: "pro", isLegacy: true, monthlyPriceCents: 25_000, monthlyCredits: 25_000, weeklyCreditsLimit: 10_000, rolloverCap: 25_000, byokEnabled: true },
116
+ { planKey: "scale_750", tier: "team", isLegacy: true, monthlyPriceCents: 75_000, monthlyCredits: 75_000, weeklyCreditsLimit: 30_000, rolloverCap: 75_000, byokEnabled: true },
117
+ { planKey: "starter_legacy", tier: "starter", isLegacy: true, monthlyPriceCents: null, monthlyCredits: 20_000, weeklyCreditsLimit: null, rolloverCap: 20_000, byokEnabled: true },
118
+ { planKey: "scale_legacy", tier: "scale", isLegacy: true, monthlyPriceCents: null, monthlyCredits: 120_000, weeklyCreditsLimit: null, rolloverCap: 240_000, byokEnabled: true },
113
119
  ];
114
120
  export function creditsToUsd(credits) {
115
121
  return credits / CREDITS_PER_USD;
@@ -24,6 +24,15 @@ export function resolveProductAnalyticsEnvironmentFromEnv(env = process.env) {
24
24
  ?? env.NEXT_PUBLIC_SENTRY_ENVIRONMENT);
25
25
  if (explicit)
26
26
  return explicit;
27
+ // The self-hosted selector is the first non-explicit signal: a self-hosted box
28
+ // has no VERCEL_ENV and no FLY_APP_NAME, so without it every environment
29
+ // (including the production shadow) would fall through to LOCAL. `shadow` is
30
+ // DEV: it carries production-shaped data but is not production traffic.
31
+ const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
32
+ if (deployEnv === "selfhost-prod")
33
+ return "PROD";
34
+ if (deployEnv === "selfhost-shadow" || deployEnv === "selfhost-dev")
35
+ return "DEV";
27
36
  const vercelEnv = env.VERCEL_ENV ?? env.NEXT_PUBLIC_VERCEL_ENV;
28
37
  if (vercelEnv === "production")
29
38
  return "PROD";
@@ -101,6 +101,21 @@ export declare const PRODUCT_EVENTS: {
101
101
  readonly HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted";
102
102
  readonly HERO_LOOKUP_RENDERED: "hero_lookup_rendered";
103
103
  readonly HERO_SIGNUP_CLICKED: "hero_signup_clicked";
104
+ /**
105
+ * Web navigation timing, measured in the browser from the click on an
106
+ * internal link (capture phase, before Next's own handler) to: the first
107
+ * visible feedback, the URL commit, and the destination's own readiness
108
+ * seam (`lib/analytics/navigation-readiness.ts`). Properties are route
109
+ * TEMPLATES (ids collapsed), a duration and enums — never a raw path, a
110
+ * query string or page content. PostHog owns these because they answer
111
+ * "how does the product feel per route per release", which is a usage
112
+ * question joinable to the funnel; a navigation that never became usable
113
+ * is a defect and is ALSO beaconed to Axiom as a client error.
114
+ */
115
+ readonly WEB_NAVIGATION_FEEDBACK: "web_navigation_feedback";
116
+ readonly WEB_NAVIGATION_MEANINGFUL: "web_navigation_meaningful";
117
+ readonly WEB_NAVIGATION_USABLE: "web_navigation_usable";
118
+ readonly WEB_NAVIGATION_USABLE_TIMEOUT: "web_navigation_usable_timeout";
104
119
  /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
105
120
  readonly POSTHOG_CANARY: "posthog_canary";
106
121
  };
@@ -101,6 +101,21 @@ export const PRODUCT_EVENTS = {
101
101
  HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted",
102
102
  HERO_LOOKUP_RENDERED: "hero_lookup_rendered",
103
103
  HERO_SIGNUP_CLICKED: "hero_signup_clicked",
104
+ /**
105
+ * Web navigation timing, measured in the browser from the click on an
106
+ * internal link (capture phase, before Next's own handler) to: the first
107
+ * visible feedback, the URL commit, and the destination's own readiness
108
+ * seam (`lib/analytics/navigation-readiness.ts`). Properties are route
109
+ * TEMPLATES (ids collapsed), a duration and enums — never a raw path, a
110
+ * query string or page content. PostHog owns these because they answer
111
+ * "how does the product feel per route per release", which is a usage
112
+ * question joinable to the funnel; a navigation that never became usable
113
+ * is a defect and is ALSO beaconed to Axiom as a client error.
114
+ */
115
+ WEB_NAVIGATION_FEEDBACK: "web_navigation_feedback",
116
+ WEB_NAVIGATION_MEANINGFUL: "web_navigation_meaningful",
117
+ WEB_NAVIGATION_USABLE: "web_navigation_usable",
118
+ WEB_NAVIGATION_USABLE_TIMEOUT: "web_navigation_usable_timeout",
104
119
  /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
105
120
  POSTHOG_CANARY: "posthog_canary",
106
121
  };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fixed-window bucketing arithmetic shared by every rate/spend limiter.
3
+ */
4
+ /** The start of the fixed `windowSeconds` window containing `now`. */
5
+ export declare function startOfWindow(now: Date, windowSeconds: number): Date;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Fixed-window bucketing arithmetic shared by every rate/spend limiter.
3
+ */
4
+ /** The start of the fixed `windowSeconds` window containing `now`. */
5
+ export function startOfWindow(now, windowSeconds) {
6
+ const windowMs = windowSeconds * 1000;
7
+ return new Date(Math.floor(now.getTime() / windowMs) * windowMs);
8
+ }
@@ -1,3 +1,4 @@
1
+ import { isRecord as isObject } from "./type-guards.js";
1
2
  const MAX_RESEARCH_SECTIONS = 20;
2
3
  const OUTPUT_SECTIONS_MARKER = /\b(?:return|provide|include|produce|output)\b.{0,80}\b(?:labeled|named|required|requested)?\s*(?:output\s+)?sections?\s*:/i;
3
4
  const LABELED_SECTION = /^(?:[-*+]\s+|\d+[.)]\s+)?([A-Za-z][A-Za-z0-9_. /&()'’+\-]{1,79})\s*:\s*.*$/;
@@ -253,6 +254,3 @@ function hasExactMembers(value, expected) {
253
254
  && value.length === expected.length
254
255
  && expected.every((member) => value.includes(member));
255
256
  }
256
- function isObject(value) {
257
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
258
- }
@@ -1,11 +1,10 @@
1
1
  // Shared search-planner vocabulary + stateless text helpers.
2
2
  //
3
3
  // Single source of truth for the constraint vocabulary and pure text helpers
4
- // consumed by BOTH the legacy web search planners
5
- // (apps/web/src/lib/company-search-plan.ts, people-search-plan.ts) and the
6
- // @oxygen/routing classifier/compiler (via packages/routing/src/vocab.ts,
7
- // which layers its named tuning deltas on top of these bases). Keeping one copy
8
- // here is what stops the two surfaces from silently drifting (OXY-414).
4
+ // consumed by the web search planners (apps/web/src/lib/company-search-plan.ts,
5
+ // people-search-plan.ts). Any other search classifier or compiler layers its
6
+ // tuning deltas on top of these bases rather than re-listing vocabulary, so the
7
+ // surfaces cannot silently drift (OXY-414).
9
8
  //
10
9
  // Everything here is a pure function or a static table: no Date.now, no
11
10
  // randomness, no I/O — safe to import from any layer.
@@ -75,7 +75,12 @@ export const IMAGE_COLUMN_SEMANTIC = "image";
75
75
  export function isImageColumnSemantic(semanticType) {
76
76
  return semanticType === "image" || semanticType === "avatar" || semanticType === "photo";
77
77
  }
78
- const MAX_OPTIONS = 200;
78
+ // A sanity ceiling, not a correctness one: it stops a malformed write from
79
+ // turning one column into an unbounded list. Raised from 200 to 300 for the
80
+ // CRM's country column, whose vocabulary is the 250-entry LinkedIn country set
81
+ // (see @oxygen/shared/linkedin-countries) — at 200 the tail of the alphabet was
82
+ // silently truncated away, which reads as "Oxygen has never heard of Vietnam".
83
+ const MAX_OPTIONS = 300;
79
84
  // Normalize a raw options array (from a column `definition.options`, CLI/MCP, or
80
85
  // the editor) into canonical SelectOptions: dedupe by value, default colors,
81
86
  // canonical 0..n ordering. Accepts strings or {value,label,color,order} objects.
@@ -1,5 +1,6 @@
1
1
  import { redactCustomerFacingDetails, redactCustomerFacingError, } from "./error-redaction.js";
2
2
  import { isProviderFundingErrorCode, isProviderRateLimitErrorCode, } from "./provider-funding-errors.js";
3
+ import { asRecordOrNull as asRecord } from "./type-guards.js";
3
4
  export const SEQUENCE_FAILURE_SCHEMA_VERSION = 1;
4
5
  export const SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS = 24 * 60 * 60 * 1000;
5
6
  export const SEQUENCE_FAILURE_CLASSES = [
@@ -318,8 +319,3 @@ function firstBoolean(records, keys) {
318
319
  }
319
320
  return null;
320
321
  }
321
- function asRecord(value) {
322
- return value !== null && typeof value === "object" && !Array.isArray(value)
323
- ? value
324
- : null;
325
- }
@@ -1176,4 +1176,27 @@ export declare function secondsUntilSendWindowEnd(window: SequenceSendWindow, no
1176
1176
  * describes this window's sends.
1177
1177
  */
1178
1178
  export declare function secondsSinceSendWindowStart(window: SequenceSendWindow, now: Date, timezone?: string): number | null;
1179
+ /**
1180
+ * The next instant at which `isWithinSendWindow` becomes true, or null when the
1181
+ * window is already open, never opens, or cannot be evaluated.
1182
+ *
1183
+ * This is what a deferral should wait for. Before it existed, the two
1184
+ * native-email send-window gates deferred a flat +30 minutes, so every pending
1185
+ * action was re-claimed, re-planned and re-written twice an hour for as long as
1186
+ * the window stayed shut — ~128 times each across a closed weekend, which is
1187
+ * where 24,261 of one fortnight's deferral log lines came from.
1188
+ *
1189
+ * Null on exactly the cases its three siblings decline: an unresolvable
1190
+ * timezone, a malformed `start`/`end`, and a zero-width window. The caller keeps
1191
+ * its own fallback delay for those, so a bad window can never park an action
1192
+ * forever — the same fail-open posture isWithinSendWindow takes.
1193
+ *
1194
+ * Computed by stepping to the next allowed local `start` and then correcting
1195
+ * against the real clock, because minutes-of-day arithmetic alone is wrong
1196
+ * across a DST boundary (the offset moves between `now` and the candidate). The
1197
+ * correction re-reads the candidate's local time and closes the gap; the result
1198
+ * is only returned once isWithinSendWindow itself agrees, so this function can
1199
+ * never hand back an instant the gate would reject.
1200
+ */
1201
+ export declare function nextSendWindowOpenAt(window: SequenceSendWindow, now: Date, timezone?: string): Date | null;
1179
1202
  export {};
@@ -1,5 +1,6 @@
1
1
  import { OxygenError } from "./cli-result.js";
2
2
  import { hashVariantKey, renderTemplate, spintaxSyntaxIssues, templateColumnKeys, } from "./sequence-template.js";
3
+ import { isRecord } from "./type-guards.js";
3
4
  /**
4
5
  * Multichannel sequence DSL — the shared contract validated identically by CLI,
5
6
  * MCP, API, and web. A sequence is an ordered list of steps applied to each
@@ -2563,6 +2564,36 @@ function hhmmToMinutes(value) {
2563
2564
  return hours * 60 + minutes;
2564
2565
  }
2565
2566
  const ISO_WEEKDAY = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
2567
+ /**
2568
+ * Does this window admit `isoWeekday`? Shared by every gate so they cannot
2569
+ * disagree about what a day list means.
2570
+ *
2571
+ * `days` is contractually ISO numbers (normalizeWeekdays rejects anything else),
2572
+ * but `settings.email_send_window` is stored without passing through it, and
2573
+ * production holds windows whose days are `["mon","tue",...]`. A raw
2574
+ * `days.includes(1)` is false for every day of the week, so such a window is
2575
+ * never open and its Sequence can never send — a silent, permanent stall that
2576
+ * also contradicts this module's fail-open posture for a malformed window.
2577
+ * Names are therefore accepted, and a list with no recognisable entry at all is
2578
+ * treated as absent (every day), which is exactly what normalizeWeekdays
2579
+ * produces for the same input.
2580
+ */
2581
+ function sendWindowAllowsWeekday(days, isoWeekday) {
2582
+ let recognised = 0;
2583
+ for (const entry of days) {
2584
+ const day = typeof entry === "number"
2585
+ ? entry
2586
+ : typeof entry === "string"
2587
+ ? ISO_WEEKDAY[`${entry.trim().slice(0, 1).toUpperCase()}${entry.trim().slice(1, 3).toLowerCase()}`] ?? Number(entry)
2588
+ : Number.NaN;
2589
+ if (!Number.isInteger(day) || day < 1 || day > 7)
2590
+ continue;
2591
+ recognised += 1;
2592
+ if (day === isoWeekday)
2593
+ return true;
2594
+ }
2595
+ return recognised === 0;
2596
+ }
2566
2597
  /**
2567
2598
  * Resolve which IANA timezone a send window evaluates in for a given lead row.
2568
2599
  * "recipient" mode reads the lead's tz from `recipient_timezone_column`, falling
@@ -2593,7 +2624,7 @@ export function isWithinSendWindow(window, now, timezone) {
2593
2624
  const endM = hhmmToMinutes(window.end);
2594
2625
  if (startM === null || endM === null)
2595
2626
  return true; // malformed window → fail open
2596
- if (window.days && window.days.length > 0 && !window.days.includes(parts.isoWeekday))
2627
+ if (window.days && window.days.length > 0 && !sendWindowAllowsWeekday(window.days, parts.isoWeekday))
2597
2628
  return false;
2598
2629
  if (startM === endM)
2599
2630
  return false; // empty window
@@ -2654,6 +2685,84 @@ export function secondsSinceSendWindowStart(window, now, timezone) {
2654
2685
  : minutes + 24 * 60 - startM; // wrapped past midnight: opened yesterday evening
2655
2686
  return elapsedMinutes >= 0 ? elapsedMinutes * 60 : null;
2656
2687
  }
2688
+ /**
2689
+ * How far ahead nextSendWindowOpenAt will look for an opening. A window with a
2690
+ * `days` list opens at least weekly, so 8 days covers every configurable gap
2691
+ * with a day of slack; anything that finds nothing inside it is a window that
2692
+ * never opens (zero-width, or a malformed tz that fails the parts read).
2693
+ */
2694
+ const SEND_WINDOW_LOOKAHEAD_DAYS = 8;
2695
+ /**
2696
+ * The next instant at which `isWithinSendWindow` becomes true, or null when the
2697
+ * window is already open, never opens, or cannot be evaluated.
2698
+ *
2699
+ * This is what a deferral should wait for. Before it existed, the two
2700
+ * native-email send-window gates deferred a flat +30 minutes, so every pending
2701
+ * action was re-claimed, re-planned and re-written twice an hour for as long as
2702
+ * the window stayed shut — ~128 times each across a closed weekend, which is
2703
+ * where 24,261 of one fortnight's deferral log lines came from.
2704
+ *
2705
+ * Null on exactly the cases its three siblings decline: an unresolvable
2706
+ * timezone, a malformed `start`/`end`, and a zero-width window. The caller keeps
2707
+ * its own fallback delay for those, so a bad window can never park an action
2708
+ * forever — the same fail-open posture isWithinSendWindow takes.
2709
+ *
2710
+ * Computed by stepping to the next allowed local `start` and then correcting
2711
+ * against the real clock, because minutes-of-day arithmetic alone is wrong
2712
+ * across a DST boundary (the offset moves between `now` and the candidate). The
2713
+ * correction re-reads the candidate's local time and closes the gap; the result
2714
+ * is only returned once isWithinSendWindow itself agrees, so this function can
2715
+ * never hand back an instant the gate would reject.
2716
+ */
2717
+ export function nextSendWindowOpenAt(window, now, timezone) {
2718
+ const tz = timezone?.trim() ? timezone.trim() : window.timezone;
2719
+ const parts = tzParts(now, tz) ?? tzParts(now, window.timezone) ?? tzParts(now, "UTC");
2720
+ if (!parts)
2721
+ return null;
2722
+ const startM = hhmmToMinutes(window.start);
2723
+ const endM = hhmmToMinutes(window.end);
2724
+ if (startM === null || endM === null || startM === endM)
2725
+ return null;
2726
+ if (isWithinSendWindow(window, now, timezone))
2727
+ return null;
2728
+ const dayAllowed = (isoWeekday) => !window.days || window.days.length === 0 || sendWindowAllowsWeekday(window.days, isoWeekday);
2729
+ const minutes = parts.hour * 60 + parts.minute;
2730
+ // Days to skip before the window's local `start` is both in the future and on
2731
+ // an allowed weekday. 0 means "later today".
2732
+ let dayOffset = minutes < startM && dayAllowed(parts.isoWeekday) ? 0 : 1;
2733
+ while (dayOffset <= SEND_WINDOW_LOOKAHEAD_DAYS && !dayAllowed(((parts.isoWeekday - 1 + dayOffset) % 7) + 1)) {
2734
+ dayOffset += 1;
2735
+ }
2736
+ if (dayOffset > SEND_WINDOW_LOOKAHEAD_DAYS)
2737
+ return null;
2738
+ let candidate = new Date(now.getTime() + ((dayOffset * 24 * 60) + startM - minutes) * 60_000);
2739
+ // DST correction: the offset that applied at `now` may not apply at the
2740
+ // candidate, so the naive arithmetic can land an hour early or late. Re-read
2741
+ // the candidate's own local time and close the gap. Two rounds are enough for
2742
+ // any real transition (the second only runs if the first lands on the shifted
2743
+ // side of the boundary); a zone with no transition converges on the first.
2744
+ for (let round = 0; round < 2; round += 1) {
2745
+ const at = tzParts(candidate, tz);
2746
+ if (!at)
2747
+ break;
2748
+ const drift = (at.hour * 60 + at.minute) - startM;
2749
+ if (drift === 0)
2750
+ break;
2751
+ // Choose the nearer direction so a ±23h reading across midnight (the window
2752
+ // opens just after a backward transition) corrects by minutes, not a day.
2753
+ const shift = drift > 12 * 60 ? drift - 24 * 60 : drift < -12 * 60 ? drift + 24 * 60 : drift;
2754
+ candidate = new Date(candidate.getTime() - shift * 60_000);
2755
+ }
2756
+ // A local `start` can be skipped entirely by a forward DST jump, and a
2757
+ // corrected candidate can land a minute on the wrong side. Walk forward to the
2758
+ // first minute the gate actually accepts rather than trusting the arithmetic.
2759
+ for (let nudge = 0; nudge <= 120; nudge += 1) {
2760
+ const at = new Date(candidate.getTime() + nudge * 60_000);
2761
+ if (at.getTime() > now.getTime() && isWithinSendWindow(window, at, timezone))
2762
+ return at;
2763
+ }
2764
+ return null;
2765
+ }
2657
2766
  function tzParts(now, tz) {
2658
2767
  try {
2659
2768
  const formatted = new Intl.DateTimeFormat("en-US", {
@@ -2852,6 +2961,3 @@ function normalizeWeekdays(value, path, issues) {
2852
2961
  }
2853
2962
  return out.length > 0 ? out.sort((a, b) => a - b) : undefined;
2854
2963
  }
2855
- function isRecord(value) {
2856
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2857
- }