@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.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +4 -0
  3. package/dist/command-manifest.js +18 -3
  4. package/dist/command-support.js +1 -1
  5. package/dist/commands/admin.js +82 -1
  6. package/dist/commands/billing.js +1 -1
  7. package/dist/commands/copilot.js +29 -7
  8. package/dist/commands/custom-integrations.js +13 -0
  9. package/dist/commands/domains.js +7 -0
  10. package/dist/commands/email-assets.d.ts +3 -0
  11. package/dist/commands/email-assets.js +78 -0
  12. package/dist/commands/email-templates.d.ts +3 -0
  13. package/dist/commands/email-templates.js +101 -0
  14. package/dist/commands/inbox.js +1 -1
  15. package/dist/commands/integrations.js +5 -1
  16. package/dist/commands/mailboxes.js +26 -6
  17. package/dist/commands/managed-inboxes.js +2 -2
  18. package/dist/commands/senders.js +4 -4
  19. package/dist/commands/sequences.js +47 -12
  20. package/dist/commands/subscriptions.d.ts +2 -4
  21. package/dist/commands/subscriptions.js +297 -51
  22. package/dist/credential-storage/store.d.ts +10 -0
  23. package/dist/credential-storage/store.js +282 -0
  24. package/dist/credentials.d.ts +1 -3
  25. package/dist/credentials.js +3 -275
  26. package/dist/mailbox-fleet-output.js +11 -2
  27. package/dist/sequence-start-at.d.ts +7 -0
  28. package/dist/sequence-start-at.js +21 -0
  29. package/dist/sequences-brand-kit-command.d.ts +28 -0
  30. package/dist/sequences-brand-kit-command.js +83 -0
  31. package/dist/sequences-content-commands.d.ts +28 -0
  32. package/dist/sequences-content-commands.js +176 -0
  33. package/dist/sequences-newsletter-commands.d.ts +6 -0
  34. package/dist/sequences-newsletter-commands.js +108 -0
  35. package/dist/sequences-test-send-command.d.ts +3 -0
  36. package/dist/sequences-test-send-command.js +47 -0
  37. package/dist/workspace-placement-commands.js +3 -2
  38. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
  39. package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
  40. package/node_modules/@oxygen/formula/dist/expression.js +26 -4
  41. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
  42. package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
  44. package/node_modules/@oxygen/shared/dist/billing.js +5 -7
  45. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
  46. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
  47. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
  48. package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
  49. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
  50. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
  51. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
  52. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
  53. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
  54. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
  55. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
  56. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
  57. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
  58. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
  60. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
  61. package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
  62. package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
  63. package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
  64. package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
  65. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
  66. package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
  67. package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
  68. package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
  69. package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
  70. package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
  71. package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
  72. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
  73. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
  74. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
  75. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
  76. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
  77. package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
  78. package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
  79. package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
  80. package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
  81. package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
  82. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
  83. package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
  84. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
  85. package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
  86. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
  87. package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
  88. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
  89. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
  90. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
  91. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
  92. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
  93. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
  94. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
  95. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
  96. package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
  97. package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
  98. package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
  99. package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
  100. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
  101. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
  102. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
  103. package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
  104. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
  105. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
  106. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
  107. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  108. package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
  109. package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
  110. package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
  111. package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
  112. package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
  113. package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
  114. package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
  115. package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
  116. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
  117. package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
  118. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  119. package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
  120. package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
  121. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
  122. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  123. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  124. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
  125. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
  126. package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
  127. 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
- * Four capabilities stay walled under the gate:
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
- * Four capabilities stay walled under the gate:
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
- : existingVersion === SEQUENCE_FAILURE_SCHEMA_VERSION &&
164
- existingClass &&
165
- FAILURE_CLASS_SET.has(existingClass)
166
- ? existingClass
167
- : classified;
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
- /** Every lifecycle a person reads: the stored statuses plus the derived `completed`. */
2
- export declare const SEQUENCE_LIFECYCLES: readonly ["draft", "active", "completed", "paused", "archived"];
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): Status | "completed";
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";