@oxygen-agent/cli 1.844.8 → 1.846.0

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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.844.8
37
+ Version: 1.846.0
package/dist/index.js CHANGED
@@ -12342,7 +12342,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12342
12342
  }
12343
12343
  }))
12344
12344
  .addCommand(new Command("update")
12345
- .description("Update a sequence. Journey definition/channels/email binding are draft-only so revised scope receives a fresh launch approval; sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, tags, and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
12345
+ .description("Update a sequence. Journey structure/channels/email binding are draft-only so revised scope receives a fresh launch approval; a started sequence still accepts a definition that changes nothing but its wait steps (the delays between steps, including the one before step 1); sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, tags, and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
12346
12346
  .argument("<sequence>", "Sequence id or slug.")
12347
12347
  .option("--name <name>", "New human-readable sequence name.")
12348
12348
  .option("--steps-file <path>", "Draft only: path to a JSON file: { \"steps\": [...] } replacing the journey. Copy supports {{column}} interpolation; native email steps also expose {{sender_name}} / {{sender_first_name}} / {{sender_email}} from the sending mailbox (a same-named row column wins). A `branch` can also route on the lead's data with a data leaf { has_column: \"email\" } (true when that row_values column is non-empty; present:false for \"missing\").")
@@ -13522,7 +13522,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13522
13522
  await handleAsyncAction("mailboxes get", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`));
13523
13523
  }))
13524
13524
  .addCommand(new Command("delete")
13525
- .description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 1,000-credit connected-mailbox commitment for future renewals; that line is currently built but not charged, and it is not the 3,000-credit OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Managed mailboxes and live warmup/monitoring add-ons fail closed with their exact address-scoped teardown steps.")
13525
+ .description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops any separately listed 1,000-credit connected-mailbox commitment for future renewals; that line is currently built but not charged, and it is not the 1,000-credit OXYGEN Warm-up subscription. The current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Managed mailboxes and live warmup/monitoring add-ons fail closed with their exact address-scoped teardown steps.")
13526
13526
  .requiredOption("--mailboxes <list>", "Comma-separated mailbox ids or addresses (maximum 500).")
13527
13527
  .option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
13528
13528
  .option("--plan-hash <hash>", "Fresh preview plan_hash.")
@@ -13930,10 +13930,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13930
13930
  });
13931
13931
  }))
13932
13932
  .addCommand(new Command("warmup")
13933
- .description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It is managed and credit-billed at 3,000 credits per warming inbox per month ($3). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Google reuses the InboxKit-held credential only during activation and never stores or returns it; Microsoft/Azure uses exact native export. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off for Microsoft/Azure because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks either handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
13933
+ .description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It is managed and credit-billed at 1,000 credits per warming inbox per month ($1). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Google reuses the InboxKit-held credential only during activation and never stores or returns it; Microsoft/Azure uses exact native export. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off for Microsoft/Azure because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks either handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
13934
13934
  .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13935
13935
  .addCommand(new Command("enable")
13936
- .description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The current public price is 3,000 credits per warming mailbox-month; the preview returns the exact first-cycle ceiling. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Eligible managed Google targets use the InboxKit-held credential just in time without storing or returning it; Microsoft/Azure targets use native InboxKit Sequencer export with exact UIDs and auto-export off. Any non-cancelled InboxKit warmup must be cancelled before either handoff (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
13936
+ .description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is handed off, enrolled, or billed. The current public price is 1,000 credits per warming mailbox-month; the preview returns the exact first-cycle ceiling. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Eligible managed Google targets use the InboxKit-held credential just in time without storing or returning it; Microsoft/Azure targets use native InboxKit Sequencer export with exact UIDs and auto-export off. Any non-cancelled InboxKit warmup must be cancelled before either handoff (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
13937
13937
  .addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
13938
13938
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
13939
13939
  .option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
@@ -14107,7 +14107,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14107
14107
  });
14108
14108
  }))
14109
14109
  .addCommand(new Command("disable")
14110
- .description("Disable warmup and unenroll each mailbox from the rail that actually enrolled it. On a managed rail — OXYGEN Warm-up today, TrulyInbox for older enrollments — this also cancels that mailbox's warmup subscription, so billing stops with the provider removal. This is how you wind an inbox off the retired TrulyInbox rail. NOT the way to fix a rotated app password — use `mailboxes warmup reconnect` for that, which costs 0 credits instead of a new 3,000-credit month. Targets the whole pool unless --mailboxes is given.")
14110
+ .description("Disable warmup and unenroll each mailbox from the rail that actually enrolled it. On a managed rail — OXYGEN Warm-up today, TrulyInbox for older enrollments — this also cancels that mailbox's warmup subscription, so billing stops with the provider removal. This is how you wind an inbox off the retired TrulyInbox rail. NOT the way to fix a rotated app password — use `mailboxes warmup reconnect` for that, which costs 0 credits instead of a new 1,000-credit month. Targets the whole pool unless --mailboxes is given.")
14111
14111
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
14112
14112
  .option("--json", "Print a JSON envelope.")
14113
14113
  .action(async (options) => {
@@ -20,7 +20,7 @@ export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 10000;
20
20
  export declare const EGRESS_IP_CREDITS_PER_MONTH = 25000;
21
21
  export declare const SENDING_MAILBOX_CREDITS_PER_MONTH = 1000;
22
22
  export declare const MANAGED_INBOX_CREDITS_PER_MONTH = 3000;
23
- export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 3000;
23
+ export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 1000;
24
24
  export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1000;
25
25
  /** Included inbox-placement tests per subscribed inbox per cycle. */
26
26
  export declare const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
@@ -37,7 +37,7 @@ export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 10_000;
37
37
  export const EGRESS_IP_CREDITS_PER_MONTH = 25_000;
38
38
  export const SENDING_MAILBOX_CREDITS_PER_MONTH = 1_000;
39
39
  export const MANAGED_INBOX_CREDITS_PER_MONTH = 3_000;
40
- export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 3_000;
40
+ export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 1_000;
41
41
  export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1_000;
42
42
  /** Included inbox-placement tests per subscribed inbox per cycle. */
43
43
  export const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
@@ -815,6 +815,22 @@ export declare function sequenceWaitStepJitterMs(step: SequenceWaitStep, enrollm
815
815
  * spreads a same-instant batch across the jitter window.
816
816
  */
817
817
  export declare function sequenceWaitStepDelayWithJitterMs(step: SequenceWaitStep, enrollmentId: string): number;
818
+ /**
819
+ * Whether `after` changes nothing about `before` except its timing — the delays
820
+ * between steps, including the lead-in delay in front of the very first one.
821
+ *
822
+ * This is the one journey edit a LIVE sequence accepts. The journey lock exists
823
+ * so a revised journey gets a fresh launch preview and approval, and a wait
824
+ * provably cannot move that preview: estimateSequenceActions (the start route's
825
+ * estimator) skips every step with no channel, so waits contribute nothing to the
826
+ * planned actions, the channel mix, or the credit estimate the approval was
827
+ * granted against. What a delay does change is when the next step fires — exactly
828
+ * the knob a founder reaches for after watching day one of a live campaign.
829
+ *
830
+ * Inserting or removing a wait counts as timing too: a wait dispatches nothing, so
831
+ * adding one only postpones a send and removing one only brings it forward.
832
+ */
833
+ export declare function sequenceDefinitionsDifferOnlyInTiming(before: SequenceDefinition, after: SequenceDefinition): boolean;
818
834
  /**
819
835
  * Render a sequence-copy template against a row's values. Delegates to the shared
820
836
  * deterministic engine (sequence-template.ts): `{{column}}` substitution plus
@@ -1835,6 +1835,86 @@ export function sequenceWaitStepJitterMs(step, enrollmentId) {
1835
1835
  export function sequenceWaitStepDelayWithJitterMs(step, enrollmentId) {
1836
1836
  return sequenceWaitStepDelayMs(step) + sequenceWaitStepJitterMs(step, enrollmentId);
1837
1837
  }
1838
+ /**
1839
+ * Branch fields that name another step. A signal branch spells its arms `then`/
1840
+ * `else`; a connection branch spells them `then_id`/`else_id`. Both are erased
1841
+ * from the skeleton and replaced by one normalized pair, so an arm that is absent
1842
+ * on one side and explicitly null on the other still compares equal.
1843
+ */
1844
+ const BRANCH_TARGET_KEYS = ["then", "else", "then_id", "else_id"];
1845
+ /** Key-sorted JSON, so two structurally equal definitions compare equal whatever order their keys were built in. */
1846
+ function stableJson(value) {
1847
+ if (Array.isArray(value))
1848
+ return `[${value.map(stableJson).join(",")}]`;
1849
+ if (value && typeof value === "object") {
1850
+ const entries = Object.entries(value)
1851
+ .filter(([, entry]) => entry !== undefined)
1852
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
1853
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
1854
+ }
1855
+ return JSON.stringify(value) ?? "null";
1856
+ }
1857
+ /**
1858
+ * The definition with every `wait` step erased: the actions, in order, with each
1859
+ * branch arm repointed to the step its delay leads INTO. Two definitions share a
1860
+ * skeleton exactly when they perform the same journey and differ only in when.
1861
+ *
1862
+ * Waits are folded rather than simply filtered out because a delay is modelled as
1863
+ * a step, so an arm whose delay was edited legitimately repoints from one wait id
1864
+ * to another (setBranchArmDelay in the web builder does exactly that) — a naive
1865
+ * filter would read that as a rerouted branch.
1866
+ */
1867
+ function sequenceTimingSkeleton(definition) {
1868
+ const steps = Array.isArray(definition?.steps) ? definition.steps : [];
1869
+ const indexById = new Map(steps.map((step, index) => [step.id, index]));
1870
+ // The first non-wait step at or after `id`. A dangling target, an absent one,
1871
+ // and a trailing run of waits all end the path — all resolve to null.
1872
+ const throughWaits = (id) => {
1873
+ if (typeof id !== "string")
1874
+ return null;
1875
+ const from = indexById.get(id);
1876
+ if (from === undefined)
1877
+ return null;
1878
+ for (let k = from; k < steps.length; k++) {
1879
+ const step = steps[k];
1880
+ if (step.kind !== "wait")
1881
+ return step.id;
1882
+ }
1883
+ return null;
1884
+ };
1885
+ const actions = steps
1886
+ .filter((step) => step.kind !== "wait")
1887
+ .map((step) => {
1888
+ const fields = { ...step };
1889
+ const branch = step;
1890
+ for (const key of BRANCH_TARGET_KEYS)
1891
+ delete fields[key];
1892
+ return {
1893
+ ...fields,
1894
+ arm_then: throughWaits(branch.then ?? branch.then_id),
1895
+ arm_else: throughWaits(branch.else ?? branch.else_id),
1896
+ };
1897
+ });
1898
+ return stableJson(actions);
1899
+ }
1900
+ /**
1901
+ * Whether `after` changes nothing about `before` except its timing — the delays
1902
+ * between steps, including the lead-in delay in front of the very first one.
1903
+ *
1904
+ * This is the one journey edit a LIVE sequence accepts. The journey lock exists
1905
+ * so a revised journey gets a fresh launch preview and approval, and a wait
1906
+ * provably cannot move that preview: estimateSequenceActions (the start route's
1907
+ * estimator) skips every step with no channel, so waits contribute nothing to the
1908
+ * planned actions, the channel mix, or the credit estimate the approval was
1909
+ * granted against. What a delay does change is when the next step fires — exactly
1910
+ * the knob a founder reaches for after watching day one of a live campaign.
1911
+ *
1912
+ * Inserting or removing a wait counts as timing too: a wait dispatches nothing, so
1913
+ * adding one only postpones a send and removing one only brings it forward.
1914
+ */
1915
+ export function sequenceDefinitionsDifferOnlyInTiming(before, after) {
1916
+ return sequenceTimingSkeleton(before) === sequenceTimingSkeleton(after);
1917
+ }
1838
1918
  /**
1839
1919
  * Render a sequence-copy template against a row's values. Delegates to the shared
1840
1920
  * deterministic engine (sequence-template.ts): `{{column}}` substitution plus
@@ -68,6 +68,29 @@ export declare function resolveByokProviderDailyCapEnforcementMode(configured?:
68
68
  * customer who had already paid for the credits, and the refusal was invisible
69
69
  * on every surface. 20x still stops an unattended loop within hours.
70
70
  */
71
+ /**
72
+ * Credit categories the IMPLICIT org-daily guard does not count.
73
+ *
74
+ * The guard is a runaway-loop backstop. A loop cannot produce these: every one
75
+ * is a deliberate, approval-gated subscription commitment tied to a specific
76
+ * mailbox, so the count is bounded by how many mailboxes a human chose to
77
+ * enroll, not by how fast something iterates. Their shape says the same thing —
78
+ * over one week `managed_warmup` was 182 transactions carrying 273k credits,
79
+ * while `managed_enrichment` was 50,967 transactions. High value, low
80
+ * cardinality is a purchase; low value, high cardinality is a loop.
81
+ *
82
+ * Counting commitments made the guard refuse exactly the deliberate provisioning
83
+ * its own contract says it must not refuse. That failure has now happened twice:
84
+ * once at the 1x multiple (fixed by raising to 20x on 2026-08-17) and again on
85
+ * 2026-08-25, when an org holding 400k paid credits was blocked mid-enrollment
86
+ * at 249,735 of a 250,000 daily guard. Raising the multiple a third time would
87
+ * only move the wall; excluding commitments removes it for the case that was
88
+ * never a loop.
89
+ *
90
+ * This applies ONLY to the implicit default. An org that sets its own org-daily
91
+ * policy has chosen a real spend ceiling, and that ceiling counts everything.
92
+ */
93
+ export declare const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES: readonly string[];
71
94
  export declare const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
72
95
  export declare const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
73
96
  /** Resolved credit thresholds of the implicit org-daily guard for one plan. */
@@ -85,6 +85,32 @@ export function resolveByokProviderDailyCapEnforcementMode(configured = process.
85
85
  * customer who had already paid for the credits, and the refusal was invisible
86
86
  * on every surface. 20x still stops an unattended loop within hours.
87
87
  */
88
+ /**
89
+ * Credit categories the IMPLICIT org-daily guard does not count.
90
+ *
91
+ * The guard is a runaway-loop backstop. A loop cannot produce these: every one
92
+ * is a deliberate, approval-gated subscription commitment tied to a specific
93
+ * mailbox, so the count is bounded by how many mailboxes a human chose to
94
+ * enroll, not by how fast something iterates. Their shape says the same thing —
95
+ * over one week `managed_warmup` was 182 transactions carrying 273k credits,
96
+ * while `managed_enrichment` was 50,967 transactions. High value, low
97
+ * cardinality is a purchase; low value, high cardinality is a loop.
98
+ *
99
+ * Counting commitments made the guard refuse exactly the deliberate provisioning
100
+ * its own contract says it must not refuse. That failure has now happened twice:
101
+ * once at the 1x multiple (fixed by raising to 20x on 2026-08-17) and again on
102
+ * 2026-08-25, when an org holding 400k paid credits was blocked mid-enrollment
103
+ * at 249,735 of a 250,000 daily guard. Raising the multiple a third time would
104
+ * only move the wall; excluding commitments removes it for the case that was
105
+ * never a loop.
106
+ *
107
+ * This applies ONLY to the implicit default. An org that sets its own org-daily
108
+ * policy has chosen a real spend ceiling, and that ceiling counts everything.
109
+ */
110
+ export const IMPLICIT_ORG_DAILY_GUARD_EXCLUDED_CATEGORIES = [
111
+ "managed_warmup",
112
+ "managed_inbox",
113
+ ];
88
114
  export const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
89
115
  export const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
90
116
  /**
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.844.8";
1
+ export declare const OXYGEN_VERSION = "1.846.0";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
4
4
  export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.844.8";
1
+ export const OXYGEN_VERSION = "1.846.0";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.844.8",
3
+ "version": "1.846.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",