@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.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.d.ts +0 -2
- package/dist/admin-primary-providers-render.js +1 -1
- package/dist/browser-login.js +1 -4
- package/dist/command-manifest.d.ts +3 -2
- package/dist/command-manifest.js +10 -0
- package/dist/credentials.d.ts +1 -1
- package/dist/functions-commands.js +6 -2
- package/dist/help.d.ts +8 -0
- package/dist/help.js +46 -0
- package/dist/index.js +1569 -123
- package/dist/knowledge-mirror.d.ts +2 -2
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +1 -1
- package/dist/session.d.ts +4 -3
- package/dist/skills.d.ts +8 -7
- package/dist/skills.js +24 -10
- package/dist/transcript.d.ts +2 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.js +1 -3
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
- package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
- package/node_modules/@oxygen/formula/dist/hash.js +199 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
- package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +78 -22
- package/node_modules/@oxygen/shared/dist/billing.js +150 -40
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +89 -16
- package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
- package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
- package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
- package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/index.js +13 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
- package/node_modules/@oxygen/shared/dist/langfuse.js +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +6 -1
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
- package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
- package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
- package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +110 -4
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
- package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
- package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
- package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +50 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
- package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
- 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 =
|
|
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,
|
|
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
|
|
42
|
-
export const
|
|
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
|
|
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:
|
|
59
|
+
export const AI_COLUMN_CREDITS = { low: 1.5, medium: 7.5, high: 25 };
|
|
60
60
|
/**
|
|
61
|
-
* Web-search grounding, billed per
|
|
61
|
+
* Web-search grounding, billed per PROVIDER CALL on top of the model — not a flat
|
|
62
|
+
* adder.
|
|
62
63
|
*
|
|
63
|
-
* This
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
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 =
|
|
75
|
-
export const AI_COLUMN_WEB_SEARCH_RESERVED_CREDITS =
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 =
|
|
4
|
-
export declare const CREDITS_PER_USD =
|
|
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.
|
|
7
|
-
/** On-demand top-up, in USD cents per
|
|
8
|
-
export declare const
|
|
9
|
-
/** On-demand top-up price per
|
|
10
|
-
export declare const
|
|
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
|
|
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 =
|
|
19
|
-
export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH =
|
|
20
|
-
export declare const EGRESS_IP_CREDITS_PER_MONTH =
|
|
21
|
-
export declare const SENDING_MAILBOX_CREDITS_PER_MONTH =
|
|
22
|
-
export declare const MANAGED_INBOX_CREDITS_PER_MONTH =
|
|
23
|
-
export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH =
|
|
24
|
-
export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
41
|
-
export declare const VOICE_CREDITS_PER_MINUTE =
|
|
42
|
-
export declare const VOICE_CREDITS_PER_AMD_CALL =
|
|
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.
|
|
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 =
|
|
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 =
|
|
19
|
-
export const CREDITS_PER_USD =
|
|
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.
|
|
22
|
-
/** On-demand top-up, in USD cents per
|
|
23
|
-
export const
|
|
24
|
-
/** On-demand top-up price per
|
|
25
|
-
export const
|
|
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
|
|
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 =
|
|
36
|
-
export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH =
|
|
37
|
-
export const EGRESS_IP_CREDITS_PER_MONTH =
|
|
38
|
-
export const SENDING_MAILBOX_CREDITS_PER_MONTH =
|
|
39
|
-
export const MANAGED_INBOX_CREDITS_PER_MONTH =
|
|
40
|
-
export const MAILBOX_WARMUP_CREDITS_PER_MONTH =
|
|
41
|
-
export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
78
|
-
export const VOICE_CREDITS_PER_MINUTE =
|
|
79
|
-
export const VOICE_CREDITS_PER_AMD_CALL =
|
|
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.
|
|
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 =
|
|
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: "
|
|
101
|
-
{ planKey: "
|
|
102
|
-
{ planKey: "
|
|
103
|
-
{ planKey: "
|
|
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: "
|
|
106
|
-
{ planKey: "
|
|
107
|
-
{ planKey: "
|
|
108
|
-
{ planKey: "
|
|
109
|
-
{ planKey: "
|
|
110
|
-
{ planKey: "
|
|
111
|
-
{ planKey: "
|
|
112
|
-
{ planKey: "
|
|
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,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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|