@oxygen-agent/cli 1.1014.1600 → 1.1014.1763
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/command-families/program.js +4 -0
- package/dist/command-manifest.js +18 -3
- package/dist/command-support.js +1 -1
- package/dist/commands/admin.js +82 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/copilot.js +29 -7
- package/dist/commands/custom-integrations.js +13 -0
- package/dist/commands/domains.js +7 -0
- package/dist/commands/email-assets.d.ts +3 -0
- package/dist/commands/email-assets.js +78 -0
- package/dist/commands/email-templates.d.ts +3 -0
- package/dist/commands/email-templates.js +101 -0
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/integrations.js +5 -1
- package/dist/commands/mailboxes.js +26 -6
- package/dist/commands/managed-inboxes.js +2 -2
- package/dist/commands/senders.js +4 -4
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/credential-storage/store.d.ts +10 -0
- package/dist/credential-storage/store.js +282 -0
- package/dist/credentials.d.ts +1 -3
- package/dist/credentials.js +3 -275
- package/dist/mailbox-fleet-output.js +11 -2
- package/dist/sequence-start-at.d.ts +7 -0
- package/dist/sequence-start-at.js +21 -0
- package/dist/sequences-brand-kit-command.d.ts +28 -0
- package/dist/sequences-brand-kit-command.js +83 -0
- package/dist/sequences-content-commands.d.ts +28 -0
- package/dist/sequences-content-commands.js +176 -0
- package/dist/sequences-newsletter-commands.d.ts +6 -0
- package/dist/sequences-newsletter-commands.js +108 -0
- package/dist/sequences-test-send-command.d.ts +3 -0
- package/dist/sequences-test-send-command.js +47 -0
- package/dist/workspace-placement-commands.js +3 -2
- package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
- package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
- package/node_modules/@oxygen/formula/dist/expression.js +26 -4
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +5 -7
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
- package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
- package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
- package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
- package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
- package/package.json +2 -2
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
import type { PlanTier, PricingPlanDefinition } from "./billing.js";
|
|
50
50
|
import { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate } from "./credit-gate.js";
|
|
51
51
|
import { type RepricingOptions } from "./repricing.js";
|
|
52
|
-
export declare const PLAN_GATED_CAPABILITIES: readonly ["sender_connect", "linkedin_connect", "sequence_dispatch", "publishing_connect", "publishing_deliver", "managed_email_infrastructure", "byok_provider_keys"];
|
|
52
|
+
export declare const PLAN_GATED_CAPABILITIES: readonly ["sender_connect", "linkedin_connect", "sequence_dispatch", "publishing_connect", "publishing_deliver", "managed_email_infrastructure", "byok_provider_keys", "bulk_email"];
|
|
53
53
|
export type PlanGatedCapability = (typeof PLAN_GATED_CAPABILITIES)[number];
|
|
54
54
|
/**
|
|
55
55
|
* Every tier except `free` unblocks every gated capability.
|
|
@@ -83,7 +83,7 @@ export declare function isPlanGatedCapability(value: unknown): value is PlanGate
|
|
|
83
83
|
* (`assertCreditGateArmed`). Until then every one of them stays walled exactly
|
|
84
84
|
* as before.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
86
|
+
* Five capabilities stay walled under the gate:
|
|
87
87
|
* - `linkedin_connect` and `sender_connect`: every sending account connects
|
|
88
88
|
* only on a plan (founder, current chat, 2026-10-12), whatever the balance.
|
|
89
89
|
* That covers a LinkedIn account, a mailbox (imported or bought), a WhatsApp
|
|
@@ -94,6 +94,8 @@ export declare function isPlanGatedCapability(value: unknown): value is PlanGate
|
|
|
94
94
|
* Instagram, TikTok, YouTube and Facebook, which have no reservation price yet
|
|
95
95
|
* (P-18). Connecting them without one would make them free and unmetered, so
|
|
96
96
|
* they stay on the plan until that price is ratified.
|
|
97
|
+
* - `bulk_email`: newsletters share one sending reputation across customers, so
|
|
98
|
+
* they stay on a plan whatever the balance (user, current chat, 2026-10-21).
|
|
97
99
|
*/
|
|
98
100
|
export { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate };
|
|
99
101
|
/**
|
|
@@ -56,6 +56,7 @@ export const PLAN_GATED_CAPABILITIES = [
|
|
|
56
56
|
"publishing_deliver",
|
|
57
57
|
"managed_email_infrastructure",
|
|
58
58
|
"byok_provider_keys",
|
|
59
|
+
"bulk_email",
|
|
59
60
|
];
|
|
60
61
|
/**
|
|
61
62
|
* Every tier except `free` unblocks every gated capability.
|
|
@@ -110,6 +111,14 @@ export const PLAN_GATED_CAPABILITY_COPY = {
|
|
|
110
111
|
freeAlternative: "Every provider in the catalog already works on Free using OXYGEN's managed keys and your credits — including enrichment, AI columns, and phone lookups. Bringing your own key is what needs a plan.",
|
|
111
112
|
deepLinkPath: "/settings/integrations",
|
|
112
113
|
},
|
|
114
|
+
// Newsletters through OXYGEN's shared sending infrastructure are paid-only
|
|
115
|
+
// (user, current chat, 2026-10-21), whatever the balance: a free workspace's
|
|
116
|
+
// list must never reach the reputation every customer shares.
|
|
117
|
+
bulk_email: {
|
|
118
|
+
label: "Sending newsletters",
|
|
119
|
+
freeAlternative: "Subscription lists and newsletter Sequences can be set up on Free. Adding a sending domain and sending to subscribers is what needs a plan.",
|
|
120
|
+
deepLinkPath: "/sequencer/accounts?tab=newsletter",
|
|
121
|
+
},
|
|
113
122
|
};
|
|
114
123
|
export function isPlanGatedCapability(value) {
|
|
115
124
|
return (typeof value === "string"
|
|
@@ -127,7 +136,7 @@ export function isPlanGatedCapability(value) {
|
|
|
127
136
|
* (`assertCreditGateArmed`). Until then every one of them stays walled exactly
|
|
128
137
|
* as before.
|
|
129
138
|
*
|
|
130
|
-
*
|
|
139
|
+
* Five capabilities stay walled under the gate:
|
|
131
140
|
* - `linkedin_connect` and `sender_connect`: every sending account connects
|
|
132
141
|
* only on a plan (founder, current chat, 2026-10-12), whatever the balance.
|
|
133
142
|
* That covers a LinkedIn account, a mailbox (imported or bought), a WhatsApp
|
|
@@ -138,6 +147,8 @@ export function isPlanGatedCapability(value) {
|
|
|
138
147
|
* Instagram, TikTok, YouTube and Facebook, which have no reservation price yet
|
|
139
148
|
* (P-18). Connecting them without one would make them free and unmetered, so
|
|
140
149
|
* they stay on the plan until that price is ratified.
|
|
150
|
+
* - `bulk_email`: newsletters share one sending reputation across customers, so
|
|
151
|
+
* they stay on a plan whatever the balance (user, current chat, 2026-10-21).
|
|
141
152
|
*/
|
|
142
153
|
export { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate };
|
|
143
154
|
/**
|
|
@@ -126,6 +126,8 @@ export type PlanLimitsSpendDefaults = {
|
|
|
126
126
|
/** The most a run approved above the default may spend (decision F.4). */
|
|
127
127
|
agent_run_approved_max_credits?: number;
|
|
128
128
|
copilot_approval_free_per_call_credits?: number;
|
|
129
|
+
/** Every plan: a step quoted strictly under this needs no approval (Philipp, 2026-10-21). */
|
|
130
|
+
copilot_approval_free_below_credits?: number;
|
|
129
131
|
copilot_approval_free_per_session_credits?: number;
|
|
130
132
|
copilot_attended_turn_backstop_credits?: number;
|
|
131
133
|
org_daily_spend_guard?: {
|
|
@@ -298,6 +298,9 @@ export function planLimitsSpendDefaults(payload) {
|
|
|
298
298
|
row("Agent run default cap", spend.agent_run_max_total_credits, "agent_run_max_total_credits", agentKey("max_total_credits")),
|
|
299
299
|
row("Agent model calls", spend.agent_run_max_inference_credits, "agent_run_max_inference_credits", agentKey("max_inference_credits")),
|
|
300
300
|
row("Copilot without approval, per step", spend.copilot_approval_free_per_call_credits, "copilot_approval_free_per_call_credits"),
|
|
301
|
+
...(typeof spend.copilot_approval_free_below_credits === "number"
|
|
302
|
+
? [row("Any step quoted under", spend.copilot_approval_free_below_credits, "copilot_approval_free_below_credits")]
|
|
303
|
+
: []),
|
|
301
304
|
row("Copilot without approval, per chat", spend.copilot_approval_free_per_session_credits, "copilot_approval_free_per_session_credits"),
|
|
302
305
|
row("Copilot turn limit", spend.copilot_attended_turn_backstop_credits, "copilot_attended_turn_backstop_credits"),
|
|
303
306
|
];
|
|
@@ -64,6 +64,11 @@ export declare const BYOK_PLATFORM_FEE_CREDITS = 0.05;
|
|
|
64
64
|
export declare const LINKEDIN_URL_FOUND_CREDITS = 5;
|
|
65
65
|
/** Email, LinkedIn and WhatsApp sends are free; the pool pays for seats and inboxes. */
|
|
66
66
|
export declare const SEND_CREDITS = 0;
|
|
67
|
+
/**
|
|
68
|
+
* One newsletter email sent through a sending domain, 5x the AWS price (user,
|
|
69
|
+
* 2026-10-21). Read through bulkEmailCredits() in email-delivery.ts.
|
|
70
|
+
*/
|
|
71
|
+
export declare const BULK_EMAIL_CREDITS_PER_EMAIL = 0.05;
|
|
67
72
|
/** Every LinkedIn action is 0 under the pure seat model. */
|
|
68
73
|
export declare const LINKEDIN_ACTION_CREDITS: {
|
|
69
74
|
readonly visit_profile: 0;
|
|
@@ -102,6 +102,11 @@ export const BYOK_PLATFORM_FEE_CREDITS = 0.05;
|
|
|
102
102
|
export const LINKEDIN_URL_FOUND_CREDITS = 5;
|
|
103
103
|
/** Email, LinkedIn and WhatsApp sends are free; the pool pays for seats and inboxes. */
|
|
104
104
|
export const SEND_CREDITS = 0;
|
|
105
|
+
/**
|
|
106
|
+
* One newsletter email sent through a sending domain, 5x the AWS price (user,
|
|
107
|
+
* 2026-10-21). Read through bulkEmailCredits() in email-delivery.ts.
|
|
108
|
+
*/
|
|
109
|
+
export const BULK_EMAIL_CREDITS_PER_EMAIL = 0.05;
|
|
105
110
|
/** Every LinkedIn action is 0 under the pure seat model. */
|
|
106
111
|
export const LINKEDIN_ACTION_CREDITS = {
|
|
107
112
|
visit_profile: 0,
|
|
@@ -40,6 +40,14 @@ export declare const PRODUCT_EVENTS: {
|
|
|
40
40
|
* Once per (workspace, person), gated by `organizations.metadata.onboarding_play_sent`.
|
|
41
41
|
*/
|
|
42
42
|
readonly ONBOARDING_PLAY_SENT: "onboarding_play_sent";
|
|
43
|
+
/**
|
|
44
|
+
* Agent-led onboarding's Start, opened from /setup/brain while the page renders:
|
|
45
|
+
* no command or browser event sees it, so a failure here was invisible in PostHog.
|
|
46
|
+
* `outcome` is opened, pending (the workspace database is still provisioning) or
|
|
47
|
+
* failed; `error_code` is the machine code of a failure; `trace_id` finds the
|
|
48
|
+
* server trace in SigNoz. Once per page visit, a handful per signup.
|
|
49
|
+
*/
|
|
50
|
+
readonly AGENT_ONBOARDING_ATTEMPTED: "agent_onboarding_attempted";
|
|
43
51
|
/**
|
|
44
52
|
* The prescribed first play (inbound-led outbound), walked on ANY surface.
|
|
45
53
|
*
|
|
@@ -97,6 +105,8 @@ export declare const PRODUCT_EVENTS: {
|
|
|
97
105
|
* Copilot is a Control surface, not a primitive, but its turns are durable
|
|
98
106
|
* worker runs like the others and it had no outcome event at all. Counts,
|
|
99
107
|
* credits and ids only — never the user message or the assistant text.
|
|
108
|
+
* `onboarding_role` is start, research or research_report on the guided
|
|
109
|
+
* onboarding turns OXYGEN funds, null otherwise.
|
|
100
110
|
*/
|
|
101
111
|
readonly COPILOT_TURN_SETTLED: "copilot_turn_settled";
|
|
102
112
|
/** Revenue lifecycle. */
|
|
@@ -158,6 +168,25 @@ export declare const PRODUCT_EVENTS: {
|
|
|
158
168
|
readonly WEB_NAVIGATION_MEANINGFUL: "web_navigation_meaningful";
|
|
159
169
|
readonly WEB_NAVIGATION_USABLE: "web_navigation_usable";
|
|
160
170
|
readonly WEB_NAVIGATION_USABLE_TIMEOUT: "web_navigation_usable_timeout";
|
|
171
|
+
/**
|
|
172
|
+
* Browser-side web-analytics signals, captured by posthog-js only for a
|
|
173
|
+
* consenting visitor, so each one carries the visit's `$session_id`.
|
|
174
|
+
*
|
|
175
|
+
* `session_engaged`: once per session, after a page has been visible for
|
|
176
|
+
* 10 s. PostHog counts a one-page session under 10 s as a bounce and measures
|
|
177
|
+
* duration between the session's events; mobile and in-app browsers often
|
|
178
|
+
* send no `$pageleave`, so a reader who stayed on one page looked like a
|
|
179
|
+
* 0-second bounce. No properties.
|
|
180
|
+
*
|
|
181
|
+
* `signup_completed`: the browser twin of `user_signed_up` (sent by the Clerk
|
|
182
|
+
* webhook, which has no session), where the ad networks' signup conversions
|
|
183
|
+
* fire: the first setup page after the onboarding survey, which carries
|
|
184
|
+
* `signup=complete`. It is the web-analytics conversion goal: channel,
|
|
185
|
+
* landing page and bounce join to it through the session. Once per account
|
|
186
|
+
* per browser; deliberately absent from KPI_TYPE_TO_EVENT.
|
|
187
|
+
*/
|
|
188
|
+
readonly SESSION_ENGAGED: "session_engaged";
|
|
189
|
+
readonly SIGNUP_COMPLETED: "signup_completed";
|
|
161
190
|
/** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
|
|
162
191
|
readonly POSTHOG_CANARY: "posthog_canary";
|
|
163
192
|
};
|
|
@@ -40,6 +40,14 @@ export const PRODUCT_EVENTS = {
|
|
|
40
40
|
* Once per (workspace, person), gated by `organizations.metadata.onboarding_play_sent`.
|
|
41
41
|
*/
|
|
42
42
|
ONBOARDING_PLAY_SENT: "onboarding_play_sent",
|
|
43
|
+
/**
|
|
44
|
+
* Agent-led onboarding's Start, opened from /setup/brain while the page renders:
|
|
45
|
+
* no command or browser event sees it, so a failure here was invisible in PostHog.
|
|
46
|
+
* `outcome` is opened, pending (the workspace database is still provisioning) or
|
|
47
|
+
* failed; `error_code` is the machine code of a failure; `trace_id` finds the
|
|
48
|
+
* server trace in SigNoz. Once per page visit, a handful per signup.
|
|
49
|
+
*/
|
|
50
|
+
AGENT_ONBOARDING_ATTEMPTED: "agent_onboarding_attempted",
|
|
43
51
|
/**
|
|
44
52
|
* The prescribed first play (inbound-led outbound), walked on ANY surface.
|
|
45
53
|
*
|
|
@@ -97,6 +105,8 @@ export const PRODUCT_EVENTS = {
|
|
|
97
105
|
* Copilot is a Control surface, not a primitive, but its turns are durable
|
|
98
106
|
* worker runs like the others and it had no outcome event at all. Counts,
|
|
99
107
|
* credits and ids only — never the user message or the assistant text.
|
|
108
|
+
* `onboarding_role` is start, research or research_report on the guided
|
|
109
|
+
* onboarding turns OXYGEN funds, null otherwise.
|
|
100
110
|
*/
|
|
101
111
|
COPILOT_TURN_SETTLED: "copilot_turn_settled",
|
|
102
112
|
/** Revenue lifecycle. */
|
|
@@ -158,6 +168,25 @@ export const PRODUCT_EVENTS = {
|
|
|
158
168
|
WEB_NAVIGATION_MEANINGFUL: "web_navigation_meaningful",
|
|
159
169
|
WEB_NAVIGATION_USABLE: "web_navigation_usable",
|
|
160
170
|
WEB_NAVIGATION_USABLE_TIMEOUT: "web_navigation_usable_timeout",
|
|
171
|
+
/**
|
|
172
|
+
* Browser-side web-analytics signals, captured by posthog-js only for a
|
|
173
|
+
* consenting visitor, so each one carries the visit's `$session_id`.
|
|
174
|
+
*
|
|
175
|
+
* `session_engaged`: once per session, after a page has been visible for
|
|
176
|
+
* 10 s. PostHog counts a one-page session under 10 s as a bounce and measures
|
|
177
|
+
* duration between the session's events; mobile and in-app browsers often
|
|
178
|
+
* send no `$pageleave`, so a reader who stayed on one page looked like a
|
|
179
|
+
* 0-second bounce. No properties.
|
|
180
|
+
*
|
|
181
|
+
* `signup_completed`: the browser twin of `user_signed_up` (sent by the Clerk
|
|
182
|
+
* webhook, which has no session), where the ad networks' signup conversions
|
|
183
|
+
* fire: the first setup page after the onboarding survey, which carries
|
|
184
|
+
* `signup=complete`. It is the web-analytics conversion goal: channel,
|
|
185
|
+
* landing page and bounce join to it through the session. Once per account
|
|
186
|
+
* per browser; deliberately absent from KPI_TYPE_TO_EVENT.
|
|
187
|
+
*/
|
|
188
|
+
SESSION_ENGAGED: "session_engaged",
|
|
189
|
+
SIGNUP_COMPLETED: "signup_completed",
|
|
161
190
|
/** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
|
|
162
191
|
POSTHOG_CANARY: "posthog_canary",
|
|
163
192
|
};
|
|
@@ -14,6 +14,8 @@ export type RecipePrimitive = typeof RECIPE_PRIMITIVES[number];
|
|
|
14
14
|
export declare const RECIPE_PREREQUISITE_KINDS: readonly ["sender", "mailbox", "domain", "integration", "knowledge", "asset", "plan", "data"];
|
|
15
15
|
export type RecipePrerequisiteKind = typeof RECIPE_PREREQUISITE_KINDS[number];
|
|
16
16
|
export declare const RECIPE_BODY_SECTIONS: readonly ["The Play", "Before You Start", "Steps", "If It Goes Wrong"];
|
|
17
|
+
export declare const RECIPE_SKILL_PLAY_BODY_SECTIONS: readonly ["When", "Numbers", "Steps", "Kill and scale"];
|
|
18
|
+
export declare const RECIPE_BODY_SHAPES: readonly (readonly string[])[];
|
|
17
19
|
export declare const RECIPE_SLUG_PATTERN: RegExp;
|
|
18
20
|
export declare function recipeWikiSlug(recipeSlug: string): string;
|
|
19
21
|
export declare const RECIPE_KIT_MAX_STAGES = 6;
|
|
@@ -77,6 +77,19 @@ export const RECIPE_BODY_SECTIONS = [
|
|
|
77
77
|
"Steps",
|
|
78
78
|
"If It Goes Wrong",
|
|
79
79
|
];
|
|
80
|
+
// The four sections of a play written inside its feature skill (the 2026-10-21
|
|
81
|
+
// sourcing, sequencer and signals plays). The lint takes the shape a body's
|
|
82
|
+
// first heading names and holds the body to exactly that shape.
|
|
83
|
+
export const RECIPE_SKILL_PLAY_BODY_SECTIONS = [
|
|
84
|
+
"When",
|
|
85
|
+
"Numbers",
|
|
86
|
+
"Steps",
|
|
87
|
+
"Kill and scale",
|
|
88
|
+
];
|
|
89
|
+
export const RECIPE_BODY_SHAPES = [
|
|
90
|
+
RECIPE_BODY_SECTIONS,
|
|
91
|
+
RECIPE_SKILL_PLAY_BODY_SECTIONS,
|
|
92
|
+
];
|
|
80
93
|
export const RECIPE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
81
94
|
// Wiki slug an install writes: slots into the starter wiki's seeded
|
|
82
95
|
// [[playbooks]] hub convention ([[playbook-outbound]] etc.).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
2
|
+
export declare const SENDING_DOMAIN_FAULT_CODES: {
|
|
3
|
+
readonly ownedElsewhere: "sending_domain_owned_elsewhere";
|
|
4
|
+
readonly reserved: "sending_domain_reserved";
|
|
5
|
+
readonly requiresDomain: "sending_domain_requires_domain";
|
|
6
|
+
readonly inUse: "sending_domain_in_use";
|
|
7
|
+
readonly workspacePaused: "bulk_email_workspace_paused";
|
|
8
|
+
readonly postalAddressRequired: "newsletter_postal_address_required";
|
|
9
|
+
};
|
|
10
|
+
/** Reasons a bulk job waits instead of sending; recorded on the job, never shown as an error. */
|
|
11
|
+
export declare const BULK_EMAIL_DEFER_REASONS: readonly ["bulk_email_daily_allowance", "send_rate", "tenant_paused"];
|
|
12
|
+
export type BulkEmailDeferReason = (typeof BULK_EMAIL_DEFER_REASONS)[number];
|
|
13
|
+
export declare function sendingDomainOwnedElsewhereError(): CliHttpError;
|
|
14
|
+
export declare function sendingDomainReservedError(): CliHttpError;
|
|
15
|
+
export declare function sendingDomainRequiresDomainError(): CliHttpError;
|
|
16
|
+
export declare function sendingDomainInUseError(): CliHttpError;
|
|
17
|
+
export declare function bulkEmailWorkspacePausedError(): CliHttpError;
|
|
18
|
+
/** Every bulk email carries the sender's postal address; a newsletter goes live only once the brand kit holds one. */
|
|
19
|
+
export declare const NEWSLETTER_POSTAL_ADDRESS_REQUIRED_MESSAGE = "Add your postal address to the brand kit before sending a newsletter.";
|
|
20
|
+
export declare const NEWSLETTER_POSTAL_ADDRESS_NEXT_STEP = "Add a postal address in Sequencer \u2192 Accounts \u2192 Newsletter \u2192 Brand kit (CLI: `oxygen sequences brand-kit set --postal-address`; MCP: oxygen_domains_sending action=brand_kit_set), then start again.";
|
|
21
|
+
export declare function newsletterPostalAddressRequiredError(details?: Record<string, unknown>): CliHttpError;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
2
|
+
// Customer-facing refusals for workspace sending domains (ADR 0023): fixed copy,
|
|
3
|
+
// a machine code and a next step. None names SES, AWS or another workspace, and
|
|
4
|
+
// an ownership refusal never says which domain it collided with.
|
|
5
|
+
export const SENDING_DOMAIN_FAULT_CODES = {
|
|
6
|
+
ownedElsewhere: "sending_domain_owned_elsewhere",
|
|
7
|
+
reserved: "sending_domain_reserved",
|
|
8
|
+
requiresDomain: "sending_domain_requires_domain",
|
|
9
|
+
inUse: "sending_domain_in_use",
|
|
10
|
+
workspacePaused: "bulk_email_workspace_paused",
|
|
11
|
+
postalAddressRequired: "newsletter_postal_address_required",
|
|
12
|
+
};
|
|
13
|
+
/** Reasons a bulk job waits instead of sending; recorded on the job, never shown as an error. */
|
|
14
|
+
export const BULK_EMAIL_DEFER_REASONS = ["bulk_email_daily_allowance", "send_rate", "tenant_paused"];
|
|
15
|
+
export function sendingDomainOwnedElsewhereError() {
|
|
16
|
+
return new CliHttpError(409, SENDING_DOMAIN_FAULT_CODES.ownedElsewhere, "This domain is already used for sending by another workspace.", {
|
|
17
|
+
details: { retryable: false, next_step: "Use a different subdomain, such as news.<your domain>, or contact support if this is your domain." },
|
|
18
|
+
exitCode: 1,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function sendingDomainReservedError() {
|
|
22
|
+
return new CliHttpError(403, SENDING_DOMAIN_FAULT_CODES.reserved, "This domain can't be used as a sending domain.", {
|
|
23
|
+
details: { retryable: false, next_step: "Use a subdomain of a domain your workspace owns." },
|
|
24
|
+
exitCode: 1,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function sendingDomainRequiresDomainError() {
|
|
28
|
+
return new CliHttpError(400, SENDING_DOMAIN_FAULT_CODES.requiresDomain, "Add the domain to your workspace before sending from it.", {
|
|
29
|
+
details: { retryable: false, next_step: "Add the domain in Sequencer → Domains, then add the sending domain again." },
|
|
30
|
+
exitCode: 1,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function sendingDomainInUseError() {
|
|
34
|
+
return new CliHttpError(409, SENDING_DOMAIN_FAULT_CODES.inUse, "This sending domain still has emails waiting to send.", {
|
|
35
|
+
details: { retryable: true, next_step: "Pause or finish the Sequences that use it, then remove it again." },
|
|
36
|
+
exitCode: 1,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function bulkEmailWorkspacePausedError() {
|
|
40
|
+
return new CliHttpError(503, SENDING_DOMAIN_FAULT_CODES.workspacePaused, "Newsletter sending is paused for this workspace.", {
|
|
41
|
+
details: { retryable: false, next_step: "Contact support to review recent bounces and complaints." },
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Every bulk email carries the sender's postal address; a newsletter goes live only once the brand kit holds one. */
|
|
46
|
+
export const NEWSLETTER_POSTAL_ADDRESS_REQUIRED_MESSAGE = "Add your postal address to the brand kit before sending a newsletter.";
|
|
47
|
+
export const NEWSLETTER_POSTAL_ADDRESS_NEXT_STEP = "Add a postal address in Sequencer → Accounts → Newsletter → Brand kit (CLI: `oxygen sequences brand-kit set --postal-address`; MCP: oxygen_domains_sending action=brand_kit_set), then start again.";
|
|
48
|
+
export function newsletterPostalAddressRequiredError(details = {}) {
|
|
49
|
+
return new CliHttpError(409, SENDING_DOMAIN_FAULT_CODES.postalAddressRequired, NEWSLETTER_POSTAL_ADDRESS_REQUIRED_MESSAGE, {
|
|
50
|
+
details: { ...details, retryable: false, next_step: NEWSLETTER_POSTAL_ADDRESS_NEXT_STEP },
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const SENDING_DOMAIN_ROLLOUT_CODE = "sending_domain_rollout_unavailable";
|
|
2
|
+
export declare const SENDING_DOMAIN_ROLLOUT_MESSAGE = "Sending-domain setup and delivery are not available yet.";
|
|
3
|
+
/** The operator switch that admits the sending-domain lane in one environment. */
|
|
4
|
+
export declare const SENDING_DOMAIN_DELIVERY_READY_ENV = "OXYGEN_SENDING_DOMAIN_DELIVERY_READY";
|
|
5
|
+
export declare function sendingDomainDeliveryAvailable(env?: Readonly<Record<string, string | undefined>>): boolean;
|
|
6
|
+
export declare function assertSendingDomainDeliveryAvailable(): void;
|
|
7
|
+
/** Inspect raw intent: invalid/partial settings must never fall back to a mailbox. */
|
|
8
|
+
export declare function hasSendingDomainDeliveryIntent(settings: unknown): boolean;
|
|
9
|
+
export declare function assertSendingDomainSequenceReady(settings: unknown): void;
|
|
10
|
+
/**
|
|
11
|
+
* Rollback fence: the only `settings.mailbox_ids` entry of a sending-domain
|
|
12
|
+
* Sequence. Older web and worker images ignore `email_delivery` (before #8332)
|
|
13
|
+
* or fell back to inboxes on partial settings (until #8449); reading this
|
|
14
|
+
* allowlist they find no mailbox and wait instead of mailing a newsletter from
|
|
15
|
+
* personal inboxes. The nil UUID passes every mailbox-id validator old and new
|
|
16
|
+
* and no mailbox row is ever created with it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SENDING_DOMAIN_MAILBOX_FENCE = "00000000-0000-0000-0000-000000000000";
|
|
19
|
+
/**
|
|
20
|
+
* Settings as stored: with sending-domain intent the mailbox allowlist is
|
|
21
|
+
* exactly the fence; without it any fence entry is removed (a Sequence that
|
|
22
|
+
* switched back to inboxes uses the whole pool unless it names inboxes again).
|
|
23
|
+
*/
|
|
24
|
+
export declare function withSendingDomainMailboxFence<T extends Record<string, unknown>>(settings: T): T;
|
|
25
|
+
/** Settings as web, CLI and MCP show them: the fence is storage, never an inbox. */
|
|
26
|
+
export declare function withoutSendingDomainMailboxFence<T extends Record<string, unknown>>(settings: T): T;
|
|
27
|
+
/** A Sequence as a read returns it, with the fence hidden from its settings. */
|
|
28
|
+
export declare function withoutSequenceMailboxFence<S extends {
|
|
29
|
+
settings?: object | null;
|
|
30
|
+
}>(sequence: S): S;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
2
|
+
export const SENDING_DOMAIN_ROLLOUT_CODE = "sending_domain_rollout_unavailable";
|
|
3
|
+
export const SENDING_DOMAIN_ROLLOUT_MESSAGE = "Sending-domain setup and delivery are not available yet.";
|
|
4
|
+
/** The operator switch that admits the sending-domain lane in one environment. */
|
|
5
|
+
export const SENDING_DOMAIN_DELIVERY_READY_ENV = "OXYGEN_SENDING_DOMAIN_DELIVERY_READY";
|
|
6
|
+
// Operator-admitted lane. An operator sets OXYGEN_SENDING_DOMAIN_DELIVERY_READY=1
|
|
7
|
+
// in one environment only after every web and worker role there runs source
|
|
8
|
+
// containing the SES ownership registry, per-workspace tenants and health,
|
|
9
|
+
// proven-volume pacing, newsletter billing and the bulk_email plan gate (this
|
|
10
|
+
// commit and later). Unlike write-rollout.ts it never defaults on outside
|
|
11
|
+
// production: unset, "true", "yes" or anything but "1" keeps it closed. Every
|
|
12
|
+
// caller reads the process environment; none passes its own env, so no request,
|
|
13
|
+
// account role or queue-family selector can qualify itself
|
|
14
|
+
// (sending-domain-rollout.test.ts scans the callers).
|
|
15
|
+
export function sendingDomainDeliveryAvailable(env = typeof process === "undefined" ? {} : process.env) {
|
|
16
|
+
return env[SENDING_DOMAIN_DELIVERY_READY_ENV] === "1";
|
|
17
|
+
}
|
|
18
|
+
export function assertSendingDomainDeliveryAvailable() {
|
|
19
|
+
if (!sendingDomainDeliveryAvailable())
|
|
20
|
+
throw new CliHttpError(503, SENDING_DOMAIN_ROLLOUT_CODE, SENDING_DOMAIN_ROLLOUT_MESSAGE, {
|
|
21
|
+
details: { retryable: false, next_step: "Keep sending-domain campaigns stopped until sending-domain delivery is available." }, exitCode: 1,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Inspect raw intent: invalid/partial settings must never fall back to a mailbox. */
|
|
25
|
+
export function hasSendingDomainDeliveryIntent(settings) {
|
|
26
|
+
if (!settings || typeof settings !== "object")
|
|
27
|
+
return false;
|
|
28
|
+
const value = settings.email_delivery;
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return false;
|
|
31
|
+
const delivery = value;
|
|
32
|
+
return delivery.mode === "sending_domain" || ["sending_domain_id", "from_address", "from_name", "reply_to_mailbox_id", "subscription_list_id"]
|
|
33
|
+
.some(key => delivery[key] !== undefined);
|
|
34
|
+
}
|
|
35
|
+
export function assertSendingDomainSequenceReady(settings) {
|
|
36
|
+
if (hasSendingDomainDeliveryIntent(settings))
|
|
37
|
+
assertSendingDomainDeliveryAvailable();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rollback fence: the only `settings.mailbox_ids` entry of a sending-domain
|
|
41
|
+
* Sequence. Older web and worker images ignore `email_delivery` (before #8332)
|
|
42
|
+
* or fell back to inboxes on partial settings (until #8449); reading this
|
|
43
|
+
* allowlist they find no mailbox and wait instead of mailing a newsletter from
|
|
44
|
+
* personal inboxes. The nil UUID passes every mailbox-id validator old and new
|
|
45
|
+
* and no mailbox row is ever created with it.
|
|
46
|
+
*/
|
|
47
|
+
export const SENDING_DOMAIN_MAILBOX_FENCE = "00000000-0000-0000-0000-000000000000";
|
|
48
|
+
/**
|
|
49
|
+
* Settings as stored: with sending-domain intent the mailbox allowlist is
|
|
50
|
+
* exactly the fence; without it any fence entry is removed (a Sequence that
|
|
51
|
+
* switched back to inboxes uses the whole pool unless it names inboxes again).
|
|
52
|
+
*/
|
|
53
|
+
export function withSendingDomainMailboxFence(settings) {
|
|
54
|
+
return hasSendingDomainDeliveryIntent(settings)
|
|
55
|
+
? { ...settings, mailbox_ids: [SENDING_DOMAIN_MAILBOX_FENCE] }
|
|
56
|
+
: withoutSendingDomainMailboxFence(settings);
|
|
57
|
+
}
|
|
58
|
+
/** Settings as web, CLI and MCP show them: the fence is storage, never an inbox. */
|
|
59
|
+
export function withoutSendingDomainMailboxFence(settings) {
|
|
60
|
+
const ids = settings.mailbox_ids;
|
|
61
|
+
if (!Array.isArray(ids))
|
|
62
|
+
return settings;
|
|
63
|
+
const kept = ids.filter((entry) => !(typeof entry === "string" && entry.trim() === SENDING_DOMAIN_MAILBOX_FENCE));
|
|
64
|
+
if (kept.length === ids.length)
|
|
65
|
+
return settings;
|
|
66
|
+
const { mailbox_ids: _fenced, ...rest } = settings;
|
|
67
|
+
return (kept.length > 0 ? { ...rest, mailbox_ids: kept } : rest);
|
|
68
|
+
}
|
|
69
|
+
/** A Sequence as a read returns it, with the fence hidden from its settings. */
|
|
70
|
+
export function withoutSequenceMailboxFence(sequence) {
|
|
71
|
+
if (!sequence.settings)
|
|
72
|
+
return sequence;
|
|
73
|
+
const view = withoutSendingDomainMailboxFence(sequence.settings);
|
|
74
|
+
return view === sequence.settings ? sequence : { ...sequence, settings: view };
|
|
75
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const SEQUENCE_FAILURE_SCHEMA_VERSION = 1;
|
|
2
2
|
export declare const SEQUENCE_FAILURE_RETRY_AFTER_MAX_MS: number;
|
|
3
3
|
export declare const WHATSAPP_UNCERTAIN_DELIVERY_MESSAGE = "WhatsApp delivery is uncertain. Automatic resending is blocked.";
|
|
4
|
-
export declare const SEQUENCE_FAILURE_CLASSES: readonly ["capacity", "recipient", "authorization_scope", "funding", "validation", "provider", "internal", "effect_unknown", "unknown"];
|
|
4
|
+
export declare const SEQUENCE_FAILURE_CLASSES: readonly ["capacity", "recipient", "authorization_scope", "integration", "funding", "validation", "provider", "internal", "effect_unknown", "unknown"];
|
|
5
5
|
export type SequenceFailureClass = (typeof SEQUENCE_FAILURE_CLASSES)[number];
|
|
6
6
|
export type SequenceFailureEffectOutcome = "not_applied" | "applied" | "unknown";
|
|
7
7
|
export type SequenceFailureEnvelope = {
|
|
@@ -58,3 +58,12 @@ export declare function hasSequenceFailureSignal(error: unknown): boolean;
|
|
|
58
58
|
* `not_applied` outcome from retryability, status, or error text.
|
|
59
59
|
*/
|
|
60
60
|
export declare function readSequenceFailure(error: unknown, overrides?: SequenceFailureOverrides): SequenceFailureEnvelope;
|
|
61
|
+
/**
|
|
62
|
+
* The class a reader shows for a stored Sequence error that already names one
|
|
63
|
+
* (the events feed, a failed lead's cause). A v1 envelope reads through
|
|
64
|
+
* {@link readSequenceFailure}, so it agrees with the Sequence summary's
|
|
65
|
+
* `failure:<class>` for the same row, including a class refined since it was
|
|
66
|
+
* written. An unversioned stored class is shown as written; no stored class is
|
|
67
|
+
* null, never a guess.
|
|
68
|
+
*/
|
|
69
|
+
export declare function storedSequenceFailureClass(error: unknown): string | null;
|
|
@@ -8,6 +8,10 @@ export const SEQUENCE_FAILURE_CLASSES = [
|
|
|
8
8
|
"capacity",
|
|
9
9
|
"recipient",
|
|
10
10
|
"authorization_scope",
|
|
11
|
+
// The connected integration does not offer an operation the step needs (an
|
|
12
|
+
// action its gateway does not expose, a tool OXYGEN has no route for).
|
|
13
|
+
// Reconnecting cannot fix it; OXYGEN has to change the integration route.
|
|
14
|
+
"integration",
|
|
11
15
|
"funding",
|
|
12
16
|
"validation",
|
|
13
17
|
"provider",
|
|
@@ -158,13 +162,21 @@ export function readSequenceFailure(error, overrides = {}) {
|
|
|
158
162
|
// do not: their stray `failure_class` fields are mapped from the underlying
|
|
159
163
|
// facts. Effect-unknown remains the fail-safe exception even for a malformed
|
|
160
164
|
// v1 envelope, because it must never be made replay-safe by a stale class.
|
|
165
|
+
// A stored class the classifier has since refined (see
|
|
166
|
+
// REFINABLE_STORED_CLASSES) reads as the refinement, so rows written before
|
|
167
|
+
// the classifier knew their code stop reading `failure:unknown` forever.
|
|
168
|
+
const storedClass = existingVersion === SEQUENCE_FAILURE_SCHEMA_VERSION &&
|
|
169
|
+
existingClass &&
|
|
170
|
+
FAILURE_CLASS_SET.has(existingClass)
|
|
171
|
+
? existingClass
|
|
172
|
+
: null;
|
|
161
173
|
const failureClass = effectOutcome === "unknown"
|
|
162
174
|
? "effect_unknown"
|
|
163
|
-
:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
: storedClass === null
|
|
176
|
+
? classified
|
|
177
|
+
: refinesStoredClass(storedClass, classified)
|
|
178
|
+
? classified
|
|
179
|
+
: storedClass;
|
|
168
180
|
return {
|
|
169
181
|
failure_schema_version: SEQUENCE_FAILURE_SCHEMA_VERSION,
|
|
170
182
|
failure_class: failureClass,
|
|
@@ -180,6 +192,48 @@ export function readSequenceFailure(error, overrides = {}) {
|
|
|
180
192
|
...(hardBounce !== null ? { hard_bounce: hardBounce } : {}),
|
|
181
193
|
};
|
|
182
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The class a reader shows for a stored Sequence error that already names one
|
|
197
|
+
* (the events feed, a failed lead's cause). A v1 envelope reads through
|
|
198
|
+
* {@link readSequenceFailure}, so it agrees with the Sequence summary's
|
|
199
|
+
* `failure:<class>` for the same row, including a class refined since it was
|
|
200
|
+
* written. An unversioned stored class is shown as written; no stored class is
|
|
201
|
+
* null, never a guess.
|
|
202
|
+
*/
|
|
203
|
+
export function storedSequenceFailureClass(error) {
|
|
204
|
+
const record = asRecord(error);
|
|
205
|
+
const stored = typeof record?.failure_class === "string" ? record.failure_class.trim() : "";
|
|
206
|
+
if (!record || !stored)
|
|
207
|
+
return null;
|
|
208
|
+
return record.failure_schema_version === SEQUENCE_FAILURE_SCHEMA_VERSION
|
|
209
|
+
? readSequenceFailure(record).failure_class
|
|
210
|
+
: stored;
|
|
211
|
+
}
|
|
212
|
+
const INTEGRATION_ACTION_MISSING_CODE = /(?:composio|provider|integration)[_-]?action[_-]?(?:missing|not[_-]?(?:found|exposed|supported))|tool[_-]?not[_-]?(?:found|executable)|composio[_-]?integration[_-]?unknown/i;
|
|
213
|
+
/**
|
|
214
|
+
* Stored v1 classes the classifier may refine at read time. `unknown` records
|
|
215
|
+
* only that the writer lacked evidence, so a diagnostic class the current
|
|
216
|
+
* classifier derives from the same stored facts replaces it. `integration` was
|
|
217
|
+
* split out of `internal`, so an `internal` row whose code names a missing
|
|
218
|
+
* integration action reads as `integration`. Classes that gate a decision
|
|
219
|
+
* (capacity recovery re-readies `capacity`, `recipient` is excluded from
|
|
220
|
+
* unresolved work and recovery, `effect_unknown` blocks replay) are never
|
|
221
|
+
* reached by refinement: those rows keep reading exactly what was written.
|
|
222
|
+
*/
|
|
223
|
+
const REFINABLE_STORED_CLASSES = {
|
|
224
|
+
unknown: new Set([
|
|
225
|
+
"integration",
|
|
226
|
+
"authorization_scope",
|
|
227
|
+
"funding",
|
|
228
|
+
"validation",
|
|
229
|
+
"provider",
|
|
230
|
+
"internal",
|
|
231
|
+
]),
|
|
232
|
+
internal: new Set(["integration"]),
|
|
233
|
+
};
|
|
234
|
+
function refinesStoredClass(stored, classified) {
|
|
235
|
+
return REFINABLE_STORED_CLASSES[stored]?.has(classified) === true;
|
|
236
|
+
}
|
|
183
237
|
function classifySequenceFailure(input) {
|
|
184
238
|
const codes = [input.code, input.providerSubtype]
|
|
185
239
|
.filter(Boolean)
|
|
@@ -205,6 +259,13 @@ function classifySequenceFailure(input) {
|
|
|
205
259
|
/auth|permission|scope|credential|forbidden|unauthori[sz]ed|not[_-]?connected|connection[_-]?(?:invalid|expired)/i.test(codes)) {
|
|
206
260
|
return "authorization_scope";
|
|
207
261
|
}
|
|
262
|
+
// An action or tool OXYGEN called that the connected integration does not
|
|
263
|
+
// expose (`composio_action_not_found`, SignalFisher 2026-10-22: crm_task's
|
|
264
|
+
// HUBSPOT_CRM_OBJECTS_GET lookup). Not the lead's fault and not the
|
|
265
|
+
// connection's permissions: checked before the recipient and 4xx bands,
|
|
266
|
+
// which a gateway's "tool not found" status would otherwise land in.
|
|
267
|
+
if (INTEGRATION_ACTION_MISSING_CODE.test(codes))
|
|
268
|
+
return "integration";
|
|
208
269
|
if (input.hardBounce === true ||
|
|
209
270
|
/crm_record_(?:not_found|missing)|recipient|hard[_-]?bounce|invalid[_-]?(?:profile|recipient|member)|lead[_-]?not[_-]?found|already[_-]?(?:connected|invited)|not[_-]?eligible|cannot[_-]?(?:message|invite)/i.test(codes)) {
|
|
210
271
|
return "recipient";
|
|
@@ -214,12 +275,13 @@ function classifySequenceFailure(input) {
|
|
|
214
275
|
/crm_(?:record_|task_)(?:mapping|association|identity|property)|validation|invalid[_-]?request|bad[_-]?request|malformed|required[_-]?field|unsupported[_-]?(?:input|value)/i.test(codes)) {
|
|
215
276
|
return "validation";
|
|
216
277
|
}
|
|
278
|
+
// A Sequence action row that disappeared is OXYGEN's own bookkeeping.
|
|
217
279
|
if (/action[_-]?not[_-]?found/i.test(codes))
|
|
218
280
|
return "internal";
|
|
219
281
|
if ((input.httpStatus !== null && input.httpStatus >= 500) ||
|
|
220
282
|
(input.httpStatus === 404 && input.retryable === true) ||
|
|
221
283
|
input.provider !== null ||
|
|
222
|
-
/provider|upstream|tool[_-]?unavailable|network|timeout|temporar(?:y|ily)[_-]?unavailable/i.test(codes)) {
|
|
284
|
+
/provider|upstream|tool[_-]?unavailable|network|timeout|temporar(?:y|ily)[_-]?unavailable|composio[_-]?(?:action|file[_-]?upload)[_-]?failed/i.test(codes)) {
|
|
223
285
|
return "provider";
|
|
224
286
|
}
|
|
225
287
|
if (/^(?:TypeError|ReferenceError|RangeError|SyntaxError)$/i.test(input.errorName ?? "") ||
|
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Every lifecycle a person reads: the stored statuses plus the derived
|
|
3
|
+
* `scheduled` and `completed`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SEQUENCE_LIFECYCLES: readonly ["draft", "scheduled", "active", "completed", "paused", "archived"];
|
|
3
6
|
export type SequenceLifecycle = (typeof SEQUENCE_LIFECYCLES)[number];
|
|
7
|
+
/** How far ahead a scheduled start may be set. */
|
|
8
|
+
export declare const SEQUENCE_START_AT_MAX_DAYS = 30;
|
|
9
|
+
/**
|
|
10
|
+
* The stored scheduled start (`settings.start_at`) when it is still ahead of
|
|
11
|
+
* `now`, else null. A past, absent or unreadable value means the Sequence runs
|
|
12
|
+
* as soon as it is active.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sequenceScheduledStartAt(settings: {
|
|
15
|
+
start_at?: unknown;
|
|
16
|
+
} | null | undefined, now?: Date): Date | null;
|
|
17
|
+
/**
|
|
18
|
+
* Read a requested scheduled start: an ISO 8601 timestamp with a timezone
|
|
19
|
+
* (`Z` or `±hh:mm`), in the future and at most {@link SEQUENCE_START_AT_MAX_DAYS}
|
|
20
|
+
* days ahead. Returns the canonical UTC form (`toISOString`) that is stored, so
|
|
21
|
+
* every stored value is one Postgres can cast.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseSequenceStartAt(value: unknown, now?: Date): {
|
|
24
|
+
ok: true;
|
|
25
|
+
startAt: string;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
4
30
|
/**
|
|
5
31
|
* The lifecycle word a person reads for a Sequence.
|
|
6
32
|
*
|
|
@@ -10,10 +36,15 @@ export type SequenceLifecycle = (typeof SEQUENCE_LIFECYCLES)[number];
|
|
|
10
36
|
* is the derived operational state; `drained` means every enrolled lead has
|
|
11
37
|
* reached an end. An active Sequence in that state reads as completed.
|
|
12
38
|
*
|
|
39
|
+
* An active Sequence whose `settings.start_at` is still ahead reads as
|
|
40
|
+
* scheduled: it was started, but the planner contacts nobody before then.
|
|
41
|
+
*
|
|
13
42
|
* Derived on read and never stored: operational health is one derived
|
|
14
43
|
* contract, not another lifecycle (docs/primitives/sequences.md, 2026-08-26).
|
|
15
44
|
* Enrolling new leads makes the same Sequence read as active again with no
|
|
16
45
|
* transition to undo. A paused Sequence stays paused, because someone decided
|
|
17
46
|
* that and the decision still holds.
|
|
18
47
|
*/
|
|
19
|
-
export declare function sequenceDisplayStatus<Status extends string>(status: Status, operationalState: string | null | undefined
|
|
48
|
+
export declare function sequenceDisplayStatus<Status extends string>(status: Status, operationalState: string | null | undefined, settings?: {
|
|
49
|
+
start_at?: unknown;
|
|
50
|
+
} | null, now?: Date): Status | "completed" | "scheduled";
|