@oxygen-agent/cli 1.844.6 → 1.845.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.6
37
+ Version: 1.845.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\").")
@@ -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
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.844.6";
1
+ export declare const OXYGEN_VERSION = "1.845.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.6";
1
+ export const OXYGEN_VERSION = "1.845.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.6",
3
+ "version": "1.845.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",