@oxygen-agent/cli 1.244.2 → 1.256.13

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.
@@ -1,3 +1,4 @@
1
+ import { type RenderTemplateOptions } from "./sequence-template.js";
1
2
  /**
2
3
  * Multichannel sequence DSL — the shared contract validated identically by CLI,
3
4
  * MCP, API, and web. A sequence is an ordered list of steps applied to each
@@ -67,6 +68,54 @@ export declare const SEQUENCE_EMAIL_COLUMN_KEYS: readonly ["email", "email_addre
67
68
  * Returns the trimmed raw value (callers normalize/lowercase as needed) or null.
68
69
  */
69
70
  export declare function recipientEmailFromRow(rowValues: Record<string, unknown> | null | undefined): string | null;
71
+ /**
72
+ * ESP (email service provider) MATCHING mode for a sequence (settings.esp_matching).
73
+ * Deliverability lore: a send lands better when the SENDING mailbox and the
74
+ * RECIPIENT sit on the same provider (Google→Google, Microsoft→Microsoft), so at
75
+ * mailbox-pick the dispatcher can bias rotation toward a same-ESP mailbox.
76
+ * - "off" — no ESP bias (default); rotation is pure LRU / domain-spread.
77
+ * - "prefer" — pick a same-ESP mailbox when one is available, else fall back to
78
+ * any sendable mailbox (never blocks a send).
79
+ * - "strict" — require a same-ESP mailbox; when none is available the action is
80
+ * DEFERRED with a visible skipped_reason rather than sent cross-provider.
81
+ */
82
+ export declare const ESP_MATCHING_MODES: readonly ["off", "prefer", "strict"];
83
+ export type EspMatchingMode = (typeof ESP_MATCHING_MODES)[number];
84
+ export declare const DEFAULT_ESP_MATCHING_MODE: EspMatchingMode;
85
+ export declare function isEspMatchingMode(value: unknown): value is EspMatchingMode;
86
+ /**
87
+ * Validate settings.esp_matching before persistence. Absent/null is valid (means
88
+ * DEFAULT_ESP_MATCHING_MODE "off"); any other value must be one of the three
89
+ * modes. Pure; throws OxygenError("invalid_sequence_settings") on a bad value so
90
+ * the CLI / MCP / API report an identical error. Called from the tenant-db
91
+ * settings validator alongside the budget/prioritization checks.
92
+ */
93
+ export declare function validateEspMatchingSetting(value: unknown): void;
94
+ /**
95
+ * The email-provider FAMILY a recipient (or sending mailbox) routes through. Kept
96
+ * intentionally aligned with EMAIL_MAILBOX_PROVIDERS in tenant-db so a mailbox's
97
+ * `provider` and an inferred recipient ESP compare directly.
98
+ */
99
+ export declare const RECIPIENT_ESPS: readonly ["google", "microsoft"];
100
+ export type RecipientEsp = (typeof RECIPIENT_ESPS)[number];
101
+ /**
102
+ * Infer a recipient's ESP from its email domain, DETERMINISTICALLY, for the
103
+ * well-known consumer domains (gmail/googlemail → google; outlook/hotmail/live/
104
+ * msn → microsoft). Returns null for any other domain — the caller then resolves
105
+ * the ESP via a cached MX lookup (custom domains fronted by Google Workspace /
106
+ * Microsoft 365 aren't decidable from the domain string alone). Pure and
107
+ * side-effect-free so it's the single source of truth for the deterministic map.
108
+ */
109
+ export declare function inferRecipientEsp(domain: string | null | undefined): RecipientEsp | null;
110
+ /**
111
+ * Map a domain's resolved MX exchange hostnames to an ESP family, for the MX-cache
112
+ * path (custom domains on Google Workspace / Microsoft 365). Google MX hosts end
113
+ * in `google.com` / `googlemail.com` (e.g. aspmx.l.google.com); Microsoft 365 MX
114
+ * hosts end in `outlook.com` / `protection.outlook.com` (e.g.
115
+ * acme-com.mail.protection.outlook.com). Returns null when no host matches either
116
+ * family. Pure so both the resolver and its tests share one mapping.
117
+ */
118
+ export declare function espFromMxHosts(hosts: readonly string[] | null | undefined): RecipientEsp | null;
70
119
  /**
71
120
  * row_values keys an enrollment's phone number may live under, in
72
121
  * send-precedence order, for WhatsApp sends. The enroll path resolves the FIRST
@@ -118,7 +167,32 @@ export type SequenceSendWindow = {
118
167
  };
119
168
  /** Base content + up to this many alternates per A/B step (base counts as variant "a"). */
120
169
  export declare const MAX_STEP_VARIANTS = 5;
121
- export declare const SEQUENCE_STEP_KINDS: readonly ["visit_profile", "invite", "wait_for_connection", "message", "inmail", "email_send", "email_reply", "email_enroll", "email_move", "email_stop", "whatsapp_message", "wait", "wait_for_signal", "branch", "stop"];
170
+ /**
171
+ * Metrics a step's opt-in auto-winner (A/B auto-optimize) can decide on. `reply`
172
+ * is the only metric today — open/click require tracking domains (a gated,
173
+ * separate capability), so the reversible auto-winner ships on reply first.
174
+ */
175
+ export declare const SEQUENCE_AUTO_OPTIMIZE_METRICS: readonly ["reply"];
176
+ export type SequenceAutoOptimizeMetric = (typeof SEQUENCE_AUTO_OPTIMIZE_METRICS)[number];
177
+ /** Default send floor per variant before the auto-winner may decide. */
178
+ export declare const DEFAULT_AUTO_OPTIMIZE_MIN_SENDS = 100;
179
+ /** Default conversion (reply) floor per variant before the auto-winner may decide. */
180
+ export declare const DEFAULT_AUTO_OPTIMIZE_MIN_CONVERSIONS = 5;
181
+ /**
182
+ * Opt-in auto-winner config on a variant-bearing step. When present, once EVERY
183
+ * still-active variant of the step has cleared BOTH thresholds
184
+ * (min_sends_per_variant sends AND min_conversions conversions), the worker's
185
+ * auto-winner pass pauses the statistically-losing variant(s) on `metric`,
186
+ * reversibly, stamping evidence. Absent → the step's A/B test runs forever (no
187
+ * auto pause). The pause is written to ox_sequencer.sequence_variant_state and
188
+ * can be undone by a manual reset.
189
+ */
190
+ export type SequenceAutoOptimizeConfig = {
191
+ metric: SequenceAutoOptimizeMetric;
192
+ min_sends_per_variant: number;
193
+ min_conversions: number;
194
+ };
195
+ export declare const SEQUENCE_STEP_KINDS: readonly ["visit_profile", "invite", "wait_for_connection", "message", "inmail", "follow", "like_post", "comment_post", "withdraw_invite", "email_send", "email_reply", "email_enroll", "email_move", "email_stop", "whatsapp_message", "wait", "wait_for_signal", "branch", "stop"];
122
196
  export type SequenceStepKind = (typeof SEQUENCE_STEP_KINDS)[number];
123
197
  export type SequenceLinkedInVisitProfileStep = {
124
198
  id: string;
@@ -139,6 +213,11 @@ export type SequenceLinkedInWaitForConnectionStep = {
139
213
  timeout_days: number;
140
214
  on_timeout: "stop" | "continue";
141
215
  };
216
+ /** A file to attach to a LinkedIn message (image / document), fetched at send time. */
217
+ export type SequenceLinkedInAttachment = {
218
+ url: string;
219
+ name?: string;
220
+ };
142
221
  export type SequenceLinkedInMessageStep = {
143
222
  id: string;
144
223
  channel: "linkedin";
@@ -148,6 +227,10 @@ export type SequenceLinkedInMessageStep = {
148
227
  variants?: {
149
228
  template?: string;
150
229
  }[];
230
+ /** Opt-in reversible auto-winner over the variants (reply metric). */
231
+ auto_optimize?: SequenceAutoOptimizeConfig;
232
+ /** Files attached to the message (≤ a few, fetched at dispatch). */
233
+ attachments?: SequenceLinkedInAttachment[];
151
234
  };
152
235
  export type SequenceLinkedInInMailStep = {
153
236
  id: string;
@@ -160,6 +243,35 @@ export type SequenceLinkedInInMailStep = {
160
243
  subject_template?: string;
161
244
  template?: string;
162
245
  }[];
246
+ /** Opt-in reversible auto-winner over the variants (reply metric). */
247
+ auto_optimize?: SequenceAutoOptimizeConfig;
248
+ };
249
+ export type SequenceLinkedInFollowStep = {
250
+ id: string;
251
+ channel: "linkedin";
252
+ kind: "follow";
253
+ };
254
+ export type SequenceLinkedInLikePostStep = {
255
+ id: string;
256
+ channel: "linkedin";
257
+ kind: "like_post";
258
+ reaction?: "like" | "celebrate" | "support" | "funny" | "love" | "insightful";
259
+ post_recency_days?: number;
260
+ on_no_post?: "stop";
261
+ };
262
+ export type SequenceLinkedInCommentPostStep = {
263
+ id: string;
264
+ channel: "linkedin";
265
+ kind: "comment_post";
266
+ text_template?: string;
267
+ ai_prompt?: string;
268
+ post_recency_days?: number;
269
+ on_no_post?: "stop";
270
+ };
271
+ export type SequenceLinkedInWithdrawInviteStep = {
272
+ id: string;
273
+ channel: "linkedin";
274
+ kind: "withdraw_invite";
163
275
  };
164
276
  export type SequenceEmailEnrollStep = {
165
277
  id: string;
@@ -190,13 +302,21 @@ export type SequenceEmailSendStep = {
190
302
  kind: "email_send";
191
303
  /** Subject line. Supports {{column}} interpolation. */
192
304
  subject_template: string;
193
- /** Body. Supports {{column}} interpolation. */
305
+ /** Body (plain text). Supports {{column}} interpolation. */
194
306
  body_template: string;
195
- /** A/B alternates over subject/body; the base is variant "a". */
307
+ /** Optional HTML body (B-W2): renders the send as multipart/alternative. Supports {{column}}. */
308
+ body_html_template?: string;
309
+ /** Static Cc / Bcc recipients (fixed addresses, not templated). */
310
+ cc?: string[];
311
+ bcc?: string[];
312
+ /** A/B alternates over subject/body/html; the base is variant "a". */
196
313
  variants?: {
197
314
  subject_template?: string;
198
315
  body_template?: string;
316
+ body_html_template?: string;
199
317
  }[];
318
+ /** Opt-in reversible auto-winner over the variants (reply metric). */
319
+ auto_optimize?: SequenceAutoOptimizeConfig;
200
320
  /** Per-step send window (overrides the sequence-level email_send_window). */
201
321
  send_window?: SequenceSendWindow;
202
322
  };
@@ -205,12 +325,20 @@ export type SequenceEmailReplyStep = {
205
325
  id: string;
206
326
  channel: "email";
207
327
  kind: "email_reply";
208
- /** Reply body. Supports {{column}} interpolation. */
328
+ /** Reply body (plain text). Supports {{column}} interpolation. */
209
329
  body_template: string;
210
- /** A/B alternates over the reply body; the base is variant "a". */
330
+ /** Optional HTML reply body (B-W2): renders the reply as multipart/alternative. Supports {{column}}. */
331
+ body_html_template?: string;
332
+ /** Static Cc / Bcc recipients (fixed addresses, not templated). */
333
+ cc?: string[];
334
+ bcc?: string[];
335
+ /** A/B alternates over the reply body/html; the base is variant "a". */
211
336
  variants?: {
212
337
  body_template?: string;
338
+ body_html_template?: string;
213
339
  }[];
340
+ /** Opt-in reversible auto-winner over the variants (reply metric). */
341
+ auto_optimize?: SequenceAutoOptimizeConfig;
214
342
  /** Per-step send window (overrides the sequence-level email_send_window). */
215
343
  send_window?: SequenceSendWindow;
216
344
  };
@@ -230,12 +358,23 @@ export type SequenceWhatsAppMessageStep = {
230
358
  variants?: {
231
359
  template?: string;
232
360
  }[];
361
+ /** Opt-in reversible auto-winner over the variants (reply metric). */
362
+ auto_optimize?: SequenceAutoOptimizeConfig;
233
363
  };
234
364
  export type SequenceWaitStep = {
235
365
  id: string;
236
366
  kind: "wait";
237
367
  days?: number;
238
368
  hours?: number;
369
+ /**
370
+ * Optional deterministic jitter: spread the resume time of otherwise-identical
371
+ * enrollments across a [0, jitter_hours) window so a batch enrolled at the same
372
+ * instant doesn't fire its next step in a detectable synchronized burst
373
+ * (EmailBison/Instantly "randomize delay" parity). Seeded by the enrollment id
374
+ * (via the shared FNV hash), so it's replayable across retries/crash-replays and
375
+ * previewable — never Math.random. 0/absent → no jitter.
376
+ */
377
+ jitter_hours?: number;
239
378
  };
240
379
  export type SequenceWaitForSignalStep = {
241
380
  id: string;
@@ -254,7 +393,7 @@ export type SequenceWaitForSignalStep = {
254
393
  * - already_connected: resolve the lead's current 1st-degree state at entry and
255
394
  * route connected → then_id, not → else_id (no waiting).
256
395
  */
257
- export declare const SEQUENCE_CONNECTION_BRANCH_CONDITIONS: readonly ["connection_accepted", "already_connected"];
396
+ export declare const SEQUENCE_CONNECTION_BRANCH_CONDITIONS: readonly ["connection_accepted", "already_connected", "open_profile"];
258
397
  export type SequenceConnectionBranchCondition = (typeof SEQUENCE_CONNECTION_BRANCH_CONDITIONS)[number];
259
398
  /**
260
399
  * A branch has two flavors, discriminated by the runtime type of `condition`:
@@ -313,7 +452,7 @@ export type SequenceSignalCondition = {
313
452
  } | {
314
453
  not: SequenceSignalCondition;
315
454
  };
316
- export type SequenceStep = SequenceLinkedInVisitProfileStep | SequenceLinkedInInviteStep | SequenceLinkedInWaitForConnectionStep | SequenceLinkedInMessageStep | SequenceLinkedInInMailStep | SequenceEmailSendStep | SequenceEmailReplyStep | SequenceEmailEnrollStep | SequenceEmailMoveStep | SequenceEmailStopStep | SequenceWhatsAppMessageStep | SequenceWaitStep | SequenceWaitForSignalStep | SequenceBranchStep | SequenceStopStep;
455
+ export type SequenceStep = SequenceLinkedInVisitProfileStep | SequenceLinkedInInviteStep | SequenceLinkedInWaitForConnectionStep | SequenceLinkedInMessageStep | SequenceLinkedInInMailStep | SequenceLinkedInFollowStep | SequenceLinkedInLikePostStep | SequenceLinkedInCommentPostStep | SequenceLinkedInWithdrawInviteStep | SequenceEmailSendStep | SequenceEmailReplyStep | SequenceEmailEnrollStep | SequenceEmailMoveStep | SequenceEmailStopStep | SequenceWhatsAppMessageStep | SequenceWaitStep | SequenceWaitForSignalStep | SequenceBranchStep | SequenceStopStep;
317
456
  export type SequenceDefinition = {
318
457
  steps: SequenceStep[];
319
458
  };
@@ -328,6 +467,16 @@ export type SequenceLintIssue = {
328
467
  export type ValidateSequenceOptions = {
329
468
  /** When provided, every channel-bearing step must use one of these channels. */
330
469
  allowedChannels?: SequenceChannel[];
470
+ /**
471
+ * Whether this sequence has NATIVE open/click tracking enabled (a verified
472
+ * tracking domain + EMAIL_TRACKING_SECRET, so the dispatcher injects a pixel +
473
+ * rewrites links). Default false. When true, the native-email engagement-gate
474
+ * guard is relaxed: email_opened/email_clicked wait/branch gates are PERMITTED
475
+ * because a natively-tracked send DOES accumulate those signals. When false the
476
+ * guard still rejects them (a native send with no tracking never fires them, so
477
+ * the gate would silently dead-end).
478
+ */
479
+ nativeTrackingEnabled?: boolean;
331
480
  };
332
481
  /**
333
482
  * Validate + normalize a raw sequence definition. Assigns stable ids to steps
@@ -338,11 +487,36 @@ export type ValidateSequenceOptions = {
338
487
  export declare function validateSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceDefinition;
339
488
  /** Non-throwing variant for lint surfaces. */
340
489
  export declare function lintSequenceDefinition(input: unknown, options?: ValidateSequenceOptions): SequenceLintIssue[];
341
- /** Total delay in milliseconds a wait step introduces. */
490
+ /** Total base delay in milliseconds a wait step introduces (no jitter). */
342
491
  export declare function sequenceWaitStepDelayMs(step: SequenceWaitStep): number;
343
- /** Render a {{column}} template against a row's values (reuses the LinkedIn impl). */
344
- export declare function renderSequenceTemplate(template: string, values: Record<string, unknown>): string;
345
- /** Column keys referenced by {{...}} placeholders across every step's copy. */
492
+ /**
493
+ * Deterministic jitter (ms) a wait step adds for one enrollment, in
494
+ * [0, jitter_hours) hours. Seeded by the enrollment id + step id through the
495
+ * shared FNV hash (hashVariantKey) — the SAME (enrollment, step) always resolves
496
+ * to the same offset, so it's replayable across retries/crash-replays and never
497
+ * uses Math.random. 0/absent jitter_hours → 0. Resolution is minute-grained so a
498
+ * batch spreads smoothly across the window rather than landing on the hour.
499
+ */
500
+ export declare function sequenceWaitStepJitterMs(step: SequenceWaitStep, enrollmentId: string): number;
501
+ /**
502
+ * Full delay (base + deterministic jitter) a wait step introduces for one
503
+ * enrollment. The dispatch planner uses this to compute a resume time that
504
+ * spreads a same-instant batch across the jitter window.
505
+ */
506
+ export declare function sequenceWaitStepDelayWithJitterMs(step: SequenceWaitStep, enrollmentId: string): number;
507
+ /**
508
+ * Render a sequence-copy template against a row's values. Delegates to the shared
509
+ * deterministic engine (sequence-template.ts): `{{column}}` substitution plus
510
+ * `{{column|fallback}}`, `{{RANDOM|…}}` spintax, and `{% if … %}` conditionals.
511
+ * Pass `{ seed }` to make spintax choices replayable across retries/crash-replays.
512
+ */
513
+ export declare function renderSequenceTemplate(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string;
514
+ /**
515
+ * Column keys referenced across every step's copy — including keys nested inside
516
+ * spintax options, inline fallbacks, and conditional conditions/branches (via the
517
+ * shared engine's scanner), so the start preview's variable-resolution check
518
+ * catches them wherever they appear.
519
+ */
346
520
  export declare function sequenceTemplateVariables(definition: SequenceDefinition): string[];
347
521
  /**
348
522
  * Evaluate a condition against the set of signals an enrollment has fired.
@@ -366,12 +540,60 @@ export declare function sequenceStepVariantCount(step: SequenceStep): number;
366
540
  * (key, step) always resolves to the same variant — replayable, evenly
367
541
  * distributed, and previewable in a dry run before any send. `key` is the
368
542
  * enrollment id.
543
+ *
544
+ * When `pausedVariantIds` is supplied (the auto-winner's paused set for this
545
+ * step), those variants are skipped and the deterministic split is RE-NORMALIZED
546
+ * over the remaining active variants — so pausing a losing variant shifts its
547
+ * share onto the survivors, still deterministically. If every variant would be
548
+ * paused (never expected — the winner is never paused), it fails safe by ignoring
549
+ * the paused set rather than stranding the step with nothing to send.
369
550
  */
370
- export declare function selectStepVariant(step: SequenceStep, key: string): {
551
+ export declare function selectStepVariant(step: SequenceStep, key: string, options?: {
552
+ pausedVariantIds?: Iterable<string>;
553
+ }): {
371
554
  variantId: string;
372
555
  index: number;
373
556
  content: StepVariantContent;
374
557
  };
558
+ /** The step's opt-in auto-winner config, or null when it carries none. */
559
+ export declare function stepAutoOptimizeConfig(step: SequenceStep): SequenceAutoOptimizeConfig | null;
560
+ /** Per-variant conversion counts the auto-winner decides over (sends + conversions on the chosen metric). */
561
+ export type VariantConversionStat = {
562
+ variantId: string;
563
+ sends: number;
564
+ conversions: number;
565
+ };
566
+ /** Stamped justification for an auto-winner pause — written onto each paused variant's state. */
567
+ export type VariantAutoWinnerEvidence = {
568
+ metric: SequenceAutoOptimizeMetric;
569
+ variants: {
570
+ variant_id: string;
571
+ sends: number;
572
+ conversions: number;
573
+ rate: number;
574
+ }[];
575
+ winner_id: string;
576
+ decided_at: string;
577
+ };
578
+ export type VariantAutoWinnerDecision = {
579
+ /** Variant ids to pause (every still-active variant whose rate is strictly below the winner's). */
580
+ pause: string[];
581
+ /** The winning variant id (highest conversion rate). */
582
+ winnerId: string;
583
+ /** Evidence stamped onto each paused variant. */
584
+ evidence: VariantAutoWinnerEvidence;
585
+ };
586
+ /**
587
+ * Decide the A/B auto-winner for one step from per-variant conversion counts.
588
+ * Pure. Returns null (no decision yet) unless: at least two variants are still
589
+ * active (not already paused), EVERY active variant has cleared BOTH thresholds
590
+ * (min_sends_per_variant AND min_conversions — the min-conversion floor), and the
591
+ * best conversion rate is strictly ahead of at least one other variant (a tie
592
+ * pauses nothing). When it decides, it pauses every active variant below the best
593
+ * rate and stamps evidence. Reversible: a manual reset reactivates a paused
594
+ * variant, and this re-decides from the survivors on the next pass.
595
+ */
596
+ export declare function decideVariantAutoWinner(stats: VariantConversionStat[], config: SequenceAutoOptimizeConfig, decidedAt: string, alreadyPaused?: Iterable<string>): VariantAutoWinnerDecision | null;
375
597
  /**
376
598
  * Resolve which IANA timezone a send window evaluates in for a given lead row.
377
599
  * "recipient" mode reads the lead's tz from `recipient_timezone_column`, falling