@oxygen-agent/cli 1.1014.1465 → 1.1014.1597

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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +2 -0
  3. package/dist/command-manifest.js +12 -2
  4. package/dist/commands/budget.js +2 -2
  5. package/dist/commands/capabilities.js +3 -2
  6. package/dist/commands/columns.js +1 -1
  7. package/dist/commands/companies.js +3 -0
  8. package/dist/commands/domains.js +122 -5
  9. package/dist/commands/feedback.js +1 -1
  10. package/dist/commands/feeds.js +34 -17
  11. package/dist/commands/mailboxes.js +19 -9
  12. package/dist/commands/publishing.js +1 -1
  13. package/dist/commands/senders.js +2 -2
  14. package/dist/commands/sequences.js +75 -12
  15. package/dist/commands/subscriptions.d.ts +10 -0
  16. package/dist/commands/subscriptions.js +203 -0
  17. package/dist/commands/tags.js +4 -4
  18. package/dist/customer-operation-commands.js +17 -0
  19. package/dist/mailbox-fleet-output.d.ts +3 -0
  20. package/dist/mailbox-fleet-output.js +80 -0
  21. package/dist/native-chat-host.d.ts +2 -0
  22. package/dist/native-chat-host.js +6 -0
  23. package/dist/workspace-placement-commands.js +67 -2
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +9 -0
  25. package/node_modules/@oxygen/shared/dist/capability-discovery.js +59 -11
  26. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +2 -2
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +18 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +4 -4
  29. package/node_modules/@oxygen/shared/dist/contracts/native-chat.d.ts +120 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/native-chat.js +35 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +9 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +6 -1
  33. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +5 -0
  34. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +2 -1
  35. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.d.ts +78 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.js +32 -0
  37. package/node_modules/@oxygen/shared/dist/dnc-rules.js +14 -0
  38. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +150 -0
  39. package/node_modules/@oxygen/shared/dist/email-delivery.js +246 -0
  40. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +1 -0
  41. package/node_modules/@oxygen/shared/dist/email-dsn.js +9 -6
  42. package/node_modules/@oxygen/shared/dist/email-health-summary.d.ts +29 -0
  43. package/node_modules/@oxygen/shared/dist/email-health-summary.js +28 -0
  44. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +50 -0
  45. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +80 -0
  46. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +28 -0
  47. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +47 -0
  48. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +4 -16
  49. package/node_modules/@oxygen/shared/dist/error-redaction.js +8 -17
  50. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +15 -1
  51. package/node_modules/@oxygen/shared/dist/guided-activation.js +3 -1
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +2 -0
  53. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.d.ts +5 -0
  54. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.js +9 -0
  55. package/node_modules/@oxygen/shared/dist/mailbox-standby.d.ts +43 -0
  56. package/node_modules/@oxygen/shared/dist/mailbox-standby.js +52 -0
  57. package/node_modules/@oxygen/shared/dist/native-chat-discovery.d.ts +5 -0
  58. package/node_modules/@oxygen/shared/dist/native-chat-discovery.js +27 -0
  59. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +124 -0
  60. package/node_modules/@oxygen/shared/dist/native-chat.js +1 -0
  61. package/node_modules/@oxygen/shared/dist/onboarding-plays.d.ts +14 -3
  62. package/node_modules/@oxygen/shared/dist/onboarding-plays.js +26 -13
  63. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +11 -0
  64. package/node_modules/@oxygen/shared/dist/onboarding-research.js +8 -0
  65. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +6 -0
  66. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +6 -0
  67. package/node_modules/@oxygen/shared/dist/select-options.js +1 -0
  68. package/node_modules/@oxygen/shared/dist/sequence-failures.js +6 -5
  69. package/node_modules/@oxygen/shared/dist/sequences.d.ts +8 -0
  70. package/node_modules/@oxygen/shared/dist/sequences.js +44 -2
  71. package/node_modules/@oxygen/shared/dist/tags.d.ts +7 -3
  72. package/node_modules/@oxygen/shared/dist/tags.js +31 -6
  73. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  74. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  75. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.d.ts +80 -0
  76. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.js +68 -0
  77. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +2 -1
  78. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +11 -1
  79. package/node_modules/@oxygen/shared/dist/workspace-placement.js +15 -4
  80. package/node_modules/@oxygen/shared/dist/write-rollout.d.ts +11 -0
  81. package/node_modules/@oxygen/shared/dist/write-rollout.js +59 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +176 -2
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +50 -1
  84. package/node_modules/@oxygen/workflows/dist/index.js +108 -3
  85. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -3,12 +3,15 @@ export declare const ONBOARDING_PLAY_LIMITS: {
3
3
  readonly minPlays: 2;
4
4
  readonly maxPlays: 4;
5
5
  readonly titleChars: 80;
6
+ readonly motionChars: 48;
6
7
  readonly lineChars: 240;
7
8
  readonly slugChars: 120;
8
9
  };
9
10
  export type OnboardingPlay = {
10
11
  id: string;
11
12
  title: string;
13
+ /** The motion in a few words, channel and kind, e.g. "Signal-led email + LinkedIn". Null on proposals made before it existed. */
14
+ motion: string | null;
12
15
  /** Who it targets: segment and buyer. */
13
16
  audience: string;
14
17
  /** The signal or trigger it acts on. */
@@ -44,12 +47,19 @@ export declare function normalizeOnboardingPlayProposal(args: Record<string, unk
44
47
  export declare function renderOnboardingPlayProposalText(proposal: OnboardingPlayProposal): string;
45
48
  /** Read a stored proposal back, refusing anything malformed. */
46
49
  export declare function readOnboardingPlayProposal(value: unknown): OnboardingPlayProposal | null;
50
+ /** How a chosen play's message starts: the turn that answers a proposal. */
51
+ export declare const ONBOARDING_PLAY_MESSAGE_PREFIX = "Run the play \"";
52
+ /** The title of a play the person wrote themselves, in place of the proposed ones. */
53
+ export declare const ONBOARDING_OWN_PLAY_TITLE = "My own play";
54
+ /** The message a play the person wrote sends: like a chosen one, so it ends onboarding the same way. */
55
+ export declare function onboardingOwnPlayMessage(description: string): string;
47
56
  /** The message a chosen play sends, as the person's own. */
48
57
  export declare function onboardingPlayMessage(play: Pick<OnboardingPlay, "title" | "firstStep" | "page">): string;
49
58
  type ProjectionTurn = {
50
59
  id: string;
51
60
  status: string;
52
61
  turnIndex: number;
62
+ userMessage?: string;
53
63
  };
54
64
  type ProjectionEvent = {
55
65
  turnId: string | null;
@@ -57,9 +67,10 @@ type ProjectionEvent = {
57
67
  payload: Record<string, unknown>;
58
68
  };
59
69
  /**
60
- * The plays the chat is waiting on, or null. Like a question, only the newest
61
- * turn can be waiting, and only when it completed by proposing: choosing one
62
- * sends a new message, which is the end of the wait.
70
+ * The plays the chat is waiting on, or null: the newest proposal of a completed
71
+ * turn, until a play is chosen from it, which sends the play's message. Unlike a
72
+ * question, a message about something else does not end the wait (Philipp,
73
+ * 2026-10-20: a "cool stuff" typed while the report streamed took the card away).
63
74
  */
64
75
  export declare function projectPendingOnboardingPlayProposal(input: {
65
76
  turns: readonly ProjectionTurn[];
@@ -10,6 +10,7 @@ export const ONBOARDING_PLAY_LIMITS = {
10
10
  minPlays: 2,
11
11
  maxPlays: 4,
12
12
  titleChars: 80,
13
+ motionChars: 48,
13
14
  lineChars: 240,
14
15
  slugChars: 120,
15
16
  };
@@ -28,14 +29,15 @@ export const ONBOARDING_PROPOSE_PLAYS_INPUT_SCHEMA = {
28
29
  additionalProperties: false,
29
30
  properties: {
30
31
  title: { type: "string", minLength: 1, maxLength: ONBOARDING_PLAY_LIMITS.titleChars, description: "Short name, e.g. \"New VP Sales at Series A SaaS\"." },
32
+ motion: { type: "string", minLength: 1, maxLength: ONBOARDING_PLAY_LIMITS.motionChars, description: "The motion in two to five words: the channel and the kind of outreach, e.g. \"Signal-led email + LinkedIn\", \"Cold email to a sourced list\", \"LinkedIn to post engagers\", \"Account-based multichannel\"." },
31
33
  audience: line("Who: the segment and the buyer persona."),
32
34
  trigger: line("The signal or event that makes now the moment, if any."),
33
35
  angle: line("What the outreach offers them, in one line."),
34
36
  why: line("Why this fits this company, from the research."),
35
- first_step: line("What running it does first, e.g. \"Find 50 matching accounts and their VP Sales\"."),
37
+ first_step: line("What running it does first, at real volume, e.g. \"Source 300 matching companies and their VP Sales into a table\"; never a handful."),
36
38
  page: { type: "string", maxLength: ONBOARDING_PLAY_LIMITS.slugChars, description: "The play's Knowledge page slug, if you wrote one." },
37
39
  },
38
- required: ["title", "audience", "angle", "first_step"],
40
+ required: ["title", "motion", "audience", "angle", "first_step"],
39
41
  },
40
42
  },
41
43
  },
@@ -56,10 +58,11 @@ export function normalizeOnboardingPlayProposal(args, mintId) {
56
58
  const firstStep = clean(record.first_step ?? record.firstStep, ONBOARDING_PLAY_LIMITS.lineChars);
57
59
  if (!title || !audience || !angle || !firstStep)
58
60
  continue;
61
+ const motion = clean(record.motion, ONBOARDING_PLAY_LIMITS.motionChars);
59
62
  const trigger = clean(record.trigger, ONBOARDING_PLAY_LIMITS.lineChars);
60
63
  const why = clean(record.why, ONBOARDING_PLAY_LIMITS.lineChars);
61
64
  const page = clean(record.page, ONBOARDING_PLAY_LIMITS.slugChars);
62
- plays.push({ id: typeof record.id === "string" && record.id ? record.id : mintId(), title, audience, trigger: trigger || null,
65
+ plays.push({ id: typeof record.id === "string" && record.id ? record.id : mintId(), title, motion: motion || null, audience, trigger: trigger || null,
63
66
  angle, why: why || null, firstStep, page: /^[a-z0-9][a-z0-9-]*$/.test(page) ? page : null });
64
67
  }
65
68
  if (plays.length < ONBOARDING_PLAY_LIMITS.minPlays || plays.length > ONBOARDING_PLAY_LIMITS.maxPlays) {
@@ -71,7 +74,7 @@ export function normalizeOnboardingPlayProposal(args, mintId) {
71
74
  export function renderOnboardingPlayProposalText(proposal) {
72
75
  return [
73
76
  "Plays I'd run first:",
74
- ...proposal.plays.map((play, index) => `${index + 1}. **${play.title}**: ${play.audience}. ${play.angle}`),
77
+ ...proposal.plays.map((play, index) => `${index + 1}. **${play.title}**${play.motion ? ` (${play.motion})` : ""}: ${play.audience}. ${play.angle}`),
75
78
  "Pick one to start it, or tell me what to change.",
76
79
  ].join("\n");
77
80
  }
@@ -85,22 +88,28 @@ export function readOnboardingPlayProposal(value) {
85
88
  const normalized = normalizeOnboardingPlayProposal(record, () => "");
86
89
  return normalized.ok && normalized.proposal.plays.every((play) => play.id) ? normalized.proposal : null;
87
90
  }
91
+ /** How a chosen play's message starts: the turn that answers a proposal. */
92
+ export const ONBOARDING_PLAY_MESSAGE_PREFIX = "Run the play \"";
93
+ /** The title of a play the person wrote themselves, in place of the proposed ones. */
94
+ export const ONBOARDING_OWN_PLAY_TITLE = "My own play";
95
+ /** The message a play the person wrote sends: like a chosen one, so it ends onboarding the same way. */
96
+ export function onboardingOwnPlayMessage(description) {
97
+ return `${ONBOARDING_PLAY_MESSAGE_PREFIX}${ONBOARDING_OWN_PLAY_TITLE}": ${description.trim()}`;
98
+ }
88
99
  /** The message a chosen play sends, as the person's own. */
89
100
  export function onboardingPlayMessage(play) {
90
- return `Run the play "${play.title}". Start with: ${play.firstStep}${play.page ? ` (see [[${play.page}]])` : ""}`;
101
+ return `${ONBOARDING_PLAY_MESSAGE_PREFIX}${play.title}". Start with: ${play.firstStep}${play.page ? ` (see [[${play.page}]])` : ""}`;
91
102
  }
92
103
  /**
93
- * The plays the chat is waiting on, or null. Like a question, only the newest
94
- * turn can be waiting, and only when it completed by proposing: choosing one
95
- * sends a new message, which is the end of the wait.
104
+ * The plays the chat is waiting on, or null: the newest proposal of a completed
105
+ * turn, until a play is chosen from it, which sends the play's message. Unlike a
106
+ * question, a message about something else does not end the wait (Philipp,
107
+ * 2026-10-20: a "cool stuff" typed while the report streamed took the card away).
96
108
  */
97
109
  export function projectPendingOnboardingPlayProposal(input) {
98
- const newest = input.turns.reduce((latest, turn) => (latest === null || turn.turnIndex > latest.turnIndex ? turn : latest), null);
99
- if (!newest || newest.status !== "completed")
100
- return null;
101
110
  for (let index = input.events.length - 1; index >= 0; index -= 1) {
102
111
  const event = input.events[index];
103
- if (!event || event.turnId !== newest.id || event.kind !== "tool_call_finished")
112
+ if (!event?.turnId || event.kind !== "tool_call_finished")
104
113
  continue;
105
114
  if (event.payload.tool !== ONBOARDING_PROPOSE_PLAYS_TOOL_NAME)
106
115
  continue;
@@ -108,7 +117,11 @@ export function projectPendingOnboardingPlayProposal(input) {
108
117
  if (!result || typeof result !== "object" || result.ok === false)
109
118
  continue;
110
119
  const proposal = readOnboardingPlayProposal(result.proposal);
111
- return proposal ? { ...proposal, turnId: newest.id } : null;
120
+ const turn = input.turns.find((entry) => entry.id === event.turnId);
121
+ if (!proposal || !turn || turn.status !== "completed")
122
+ return null;
123
+ const chosen = input.turns.some((later) => later.turnIndex > turn.turnIndex && later.userMessage?.startsWith(ONBOARDING_PLAY_MESSAGE_PREFIX));
124
+ return chosen ? null : { ...proposal, turnId: turn.id };
112
125
  }
113
126
  return null;
114
127
  }
@@ -10,6 +10,17 @@
10
10
  export declare const ONBOARDING_RESEARCH_PURPOSE = "onboarding_research";
11
11
  /** The research sub-agent the onboarding turn delegates to. */
12
12
  export declare const ONBOARDING_RESEARCH_SUBAGENT = "onboarding_researcher";
13
+ /**
14
+ * The researchers' model: the Balanced tier's model (Claude Haiku 5.5) at low
15
+ * reasoning effort. Their errand default, DeepSeek V4 Flash, wrote its tool calls
16
+ * as text in 4 of 8 researchers on a live run (2026-10-20) and timed out on the
17
+ * one before, and V4.1 Flash missed tool calls in 1 of 5 probes (hosted-ai.ts).
18
+ * Reliable calls first, then speed: one place to change once measured.
19
+ */
20
+ export declare const ONBOARDING_RESEARCH_MODEL: {
21
+ readonly level: "medium";
22
+ readonly reasoningEffort: "low";
23
+ };
13
24
  /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
14
25
  export declare const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
15
26
  /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
@@ -10,6 +10,14 @@
10
10
  export const ONBOARDING_RESEARCH_PURPOSE = "onboarding_research";
11
11
  /** The research sub-agent the onboarding turn delegates to. */
12
12
  export const ONBOARDING_RESEARCH_SUBAGENT = "onboarding_researcher";
13
+ /**
14
+ * The researchers' model: the Balanced tier's model (Claude Haiku 5.5) at low
15
+ * reasoning effort. Their errand default, DeepSeek V4 Flash, wrote its tool calls
16
+ * as text in 4 of 8 researchers on a live run (2026-10-20) and timed out on the
17
+ * one before, and V4.1 Flash missed tool calls in 1 of 5 probes (hosted-ai.ts).
18
+ * Reliable calls first, then speed: one place to change once measured.
19
+ */
20
+ export const ONBOARDING_RESEARCH_MODEL = { level: "medium", reasoningEffort: "low" };
13
21
  /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
14
22
  export const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
15
23
  /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
@@ -34,6 +34,12 @@ export declare const PRODUCT_EVENTS: {
34
34
  */
35
35
  readonly AGENT_SETUP_ACTION: "agent_setup_action";
36
36
  readonly FIRST_RECORD_CREATED: "first_record_created";
37
+ /**
38
+ * Activation (Philipp, 2026-10-20): the person sent a play's prompt in their
39
+ * onboarding chat, verified from the chat's turn when onboarding finishes.
40
+ * Once per (workspace, person), gated by `organizations.metadata.onboarding_play_sent`.
41
+ */
42
+ readonly ONBOARDING_PLAY_SENT: "onboarding_play_sent";
37
43
  /**
38
44
  * The prescribed first play (inbound-led outbound), walked on ANY surface.
39
45
  *
@@ -34,6 +34,12 @@ export const PRODUCT_EVENTS = {
34
34
  */
35
35
  AGENT_SETUP_ACTION: "agent_setup_action",
36
36
  FIRST_RECORD_CREATED: "first_record_created",
37
+ /**
38
+ * Activation (Philipp, 2026-10-20): the person sent a play's prompt in their
39
+ * onboarding chat, verified from the chat's turn when onboarding finishes.
40
+ * Once per (workspace, person), gated by `organizations.metadata.onboarding_play_sent`.
41
+ */
42
+ ONBOARDING_PLAY_SENT: "onboarding_play_sent",
37
43
  /**
38
44
  * The prescribed first play (inbound-led outbound), walked on ANY surface.
39
45
  *
@@ -30,6 +30,7 @@ export function tagColorForIndex(index) {
30
30
  // Deterministic color for a free-form label (workspace tags): same tag, same
31
31
  // color on every surface, with no stored color to sync.
32
32
  export function tagColorForLabel(label) {
33
+ label = label.toLowerCase();
33
34
  let hash = 0;
34
35
  for (let index = 0; index < label.length; index += 1) {
35
36
  hash = (hash * 31 + label.charCodeAt(index)) | 0;
@@ -122,6 +122,7 @@ export function readSequenceFailure(error, overrides = {}) {
122
122
  "statusCode",
123
123
  "provider_status",
124
124
  "providerStatus",
125
+ "upstream_status",
125
126
  ]));
126
127
  let retryable = overrides.retryable ?? firstBoolean(records, ["retryable"]);
127
128
  let effectOutcome = overrides.effectOutcome ?? readEffectOutcome(records);
@@ -205,14 +206,16 @@ function classifySequenceFailure(input) {
205
206
  return "authorization_scope";
206
207
  }
207
208
  if (input.hardBounce === true ||
208
- /recipient|hard[_-]?bounce|invalid[_-]?(?:profile|recipient|member)|lead[_-]?not[_-]?found|already[_-]?(?:connected|invited)|not[_-]?eligible|cannot[_-]?(?:message|invite)/i.test(codes)) {
209
+ /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)) {
209
210
  return "recipient";
210
211
  }
211
212
  if (input.httpStatus === 400 ||
212
213
  input.httpStatus === 422 ||
213
- /validation|invalid[_-]?request|bad[_-]?request|malformed|required[_-]?field|unsupported[_-]?(?:input|value)/i.test(codes)) {
214
+ /crm_(?:record_|task_)(?:mapping|association|identity|property)|validation|invalid[_-]?request|bad[_-]?request|malformed|required[_-]?field|unsupported[_-]?(?:input|value)/i.test(codes)) {
214
215
  return "validation";
215
216
  }
217
+ if (/action[_-]?not[_-]?found/i.test(codes))
218
+ return "internal";
216
219
  if ((input.httpStatus !== null && input.httpStatus >= 500) ||
217
220
  (input.httpStatus === 404 && input.retryable === true) ||
218
221
  input.provider !== null ||
@@ -220,9 +223,7 @@ function classifySequenceFailure(input) {
220
223
  return "provider";
221
224
  }
222
225
  if (/^(?:TypeError|ReferenceError|RangeError|SyntaxError)$/i.test(input.errorName ?? "") ||
223
- // An integration action OXYGEN called that its connection does not expose
224
- // (composio_action_not_found) is an OXYGEN routing defect, not the lead's.
225
- /internal|database|persistence|bookkeeping|invariant|concurrent[_-]?write|action[_-]?not[_-]?found/i.test(codes)) {
226
+ /internal|database|persistence|bookkeeping|invariant|concurrent[_-]?write/i.test(codes)) {
226
227
  return "internal";
227
228
  }
228
229
  return "unknown";
@@ -263,6 +263,7 @@ export type SequenceLinkedInWaitForConnectionStep = {
263
263
  kind: "wait_for_connection";
264
264
  timeout_days: number;
265
265
  on_timeout: "stop" | "continue";
266
+ withdraw_on_timeout?: boolean;
266
267
  };
267
268
  /** A file to attach to a LinkedIn message (image / document), fetched at send time. */
268
269
  export type SequenceLinkedInAttachment = {
@@ -601,6 +602,7 @@ export type SequenceCrmTaskStep = {
601
602
  export type SequenceWaitStep = {
602
603
  id: string;
603
604
  kind: "wait";
605
+ working_days?: boolean;
604
606
  days?: number;
605
607
  hours?: number;
606
608
  /**
@@ -939,6 +941,12 @@ export declare function sequenceWaitStepJitterMs(step: SequenceWaitStep, enrollm
939
941
  * spreads a same-instant batch across the jitter window.
940
942
  */
941
943
  export declare function sequenceWaitStepDelayWithJitterMs(step: SequenceWaitStep, enrollmentId: string): number;
944
+ /** Working days use the sequence schedule's weekdays and zone, Monday–Friday UTC if absent.
945
+ * Whole days preserve local wall time across DST; hours and jitter remain elapsed time. */
946
+ export declare function sequenceWaitStepReadyAt(step: SequenceWaitStep, enrollmentId: string, now: Date, schedule?: {
947
+ timezone?: string;
948
+ days?: readonly (number | string)[];
949
+ } | null): Date;
942
950
  /**
943
951
  * Whether `after` changes nothing about `before` except how long its existing
944
952
  * wait steps wait. This is the one journey edit a LIVE sequence accepts, and it
@@ -955,6 +955,8 @@ raw, index, options, issues) {
955
955
  };
956
956
  }
957
957
  case "wait_for_connection": {
958
+ if (raw.withdraw_on_timeout !== undefined && typeof raw.withdraw_on_timeout !== "boolean")
959
+ issues.push({ path: `${path}.withdraw_on_timeout`, message: "must be boolean." });
958
960
  const timeoutDays = raw.timeout_days === undefined || raw.timeout_days === null
959
961
  ? 14
960
962
  : positiveInt(raw.timeout_days, `${path}.timeout_days`, issues) ?? 14;
@@ -962,6 +964,7 @@ raw, index, options, issues) {
962
964
  id,
963
965
  channel: "linkedin",
964
966
  kind: "wait_for_connection",
967
+ ...(raw.withdraw_on_timeout === true ? { withdraw_on_timeout: true } : {}),
965
968
  timeout_days: timeoutDays,
966
969
  on_timeout: raw.on_timeout === "continue" ? "continue" : "stop",
967
970
  };
@@ -1163,10 +1166,15 @@ raw, index, options, issues) {
1163
1166
  if ((days ?? 0) + (hours ?? 0) <= 0 && !options.allowZeroWaits) {
1164
1167
  issues.push({ path, message: "A wait step needs days and/or hours totaling at least 1 hour." });
1165
1168
  }
1169
+ if (raw.working_days !== undefined && typeof raw.working_days !== "boolean")
1170
+ issues.push({ path: `${path}.working_days`, message: "must be boolean." });
1171
+ if (raw.working_days === true && (days ?? 0) > 3650)
1172
+ issues.push({ path: `${path}.days`, message: "Working-day waits must be at most 3650 days." });
1166
1173
  const jitterHours = optionalWaitJitterHours(raw.jitter_hours, `${path}.jitter_hours`, issues);
1167
1174
  return {
1168
1175
  id,
1169
1176
  kind: "wait",
1177
+ ...(raw.working_days === true ? { working_days: true } : {}),
1170
1178
  ...(days !== undefined ? { days } : {}),
1171
1179
  ...(hours !== undefined ? { hours } : {}),
1172
1180
  ...(jitterHours !== undefined ? { jitter_hours: jitterHours } : {}),
@@ -1274,7 +1282,7 @@ raw, index, options, issues) {
1274
1282
  const STEP_KIND_FIELDS = {
1275
1283
  visit_profile: [],
1276
1284
  invite: ["note_template", "ai_prompt"],
1277
- wait_for_connection: ["timeout_days", "on_timeout"],
1285
+ wait_for_connection: ["timeout_days", "on_timeout", "withdraw_on_timeout"],
1278
1286
  message: ["template", "variants", "auto_optimize", "attachments", "ai_prompt"],
1279
1287
  inmail: ["subject_template", "template", "variants", "auto_optimize"],
1280
1288
  follow: [],
@@ -1289,7 +1297,7 @@ const STEP_KIND_FIELDS = {
1289
1297
  whatsapp_message: ["template", "variants", "auto_optimize"],
1290
1298
  call_task: ["note", "priority", "timeout_days", "on_timeout"],
1291
1299
  crm_task: ["provider", "connection_id", "property_mappings", "record_mappings", "record_links", "associations"],
1292
- wait: ["days", "hours", "jitter_hours"],
1300
+ wait: ["days", "hours", "jitter_hours", "working_days"],
1293
1301
  wait_for_signal: ["signal", "timeout_days", "on_timeout"],
1294
1302
  branch: ["condition", "then", "else", "then_id", "else_id", "timeout_days"],
1295
1303
  stop: [],
@@ -2026,6 +2034,39 @@ export function sequenceWaitStepJitterMs(step, enrollmentId) {
2026
2034
  export function sequenceWaitStepDelayWithJitterMs(step, enrollmentId) {
2027
2035
  return sequenceWaitStepDelayMs(step) + sequenceWaitStepJitterMs(step, enrollmentId);
2028
2036
  }
2037
+ /** Working days use the sequence schedule's weekdays and zone, Monday–Friday UTC if absent.
2038
+ * Whole days preserve local wall time across DST; hours and jitter remain elapsed time. */
2039
+ export function sequenceWaitStepReadyAt(step, enrollmentId, now, schedule) {
2040
+ if (!step.working_days || !step.days)
2041
+ return new Date(now.getTime() + sequenceWaitStepDelayWithJitterMs(step, enrollmentId));
2042
+ const zone = schedule?.timezone || "UTC";
2043
+ const configuredDays = schedule?.days ?? [];
2044
+ const days = configuredDays.length > 0
2045
+ ? Array.from({ length: 7 }, (_, index) => index + 1).filter(day => sendWindowAllowsWeekday([...configuredDays], day))
2046
+ : [1, 2, 3, 4, 5];
2047
+ if (days.length === 0)
2048
+ days.push(1, 2, 3, 4, 5);
2049
+ let candidate = new Date(now);
2050
+ const start = tzParts(now, zone) ?? tzParts(now, "UTC");
2051
+ let remaining = step.days;
2052
+ while (remaining > 0) {
2053
+ candidate = new Date(candidate.getTime() + 86_400_000);
2054
+ // Correct a daylight-saving offset while retaining the original local hour/minute.
2055
+ for (let round = 0; round < 2; round += 1) {
2056
+ const parts = tzParts(candidate, zone) ?? tzParts(candidate, "UTC");
2057
+ let drift = (parts.hour - start.hour) * 60 + parts.minute - start.minute;
2058
+ if (drift > 720)
2059
+ drift -= 1440;
2060
+ if (drift < -720)
2061
+ drift += 1440;
2062
+ candidate = new Date(candidate.getTime() - drift * 60_000);
2063
+ }
2064
+ const parts = tzParts(candidate, zone) ?? tzParts(candidate, "UTC");
2065
+ if (sendWindowAllowsWeekday([...days], parts.isoWeekday))
2066
+ remaining -= 1;
2067
+ }
2068
+ return new Date(candidate.getTime() + (step.hours ?? 0) * 3_600_000 + sequenceWaitStepJitterMs(step, enrollmentId));
2069
+ }
2029
2070
  /** Key-sorted JSON, so two structurally equal steps compare equal whatever order their keys were built in. */
2030
2071
  function stableJson(value) {
2031
2072
  if (Array.isArray(value))
@@ -2080,6 +2121,7 @@ export function sequenceDefinitionsDifferOnlyInTiming(before, after) {
2080
2121
  delete fields.days;
2081
2122
  delete fields.hours;
2082
2123
  delete fields.jitter_hours;
2124
+ delete fields.working_days;
2083
2125
  return stableJson(fields);
2084
2126
  };
2085
2127
  return timingFree(oldStep) === timingFree(newStep);
@@ -43,13 +43,18 @@ export declare function tagKindHeading(kind: string): string;
43
43
  /** Hard cap per tagged item — matches the wiki's long-standing limit. */
44
44
  export declare const MAX_TAGS_PER_ITEM = 50;
45
45
  /**
46
- * Normalize a raw tag array: lowercase, trim, drop non-strings/empties, dedupe,
46
+ * Normalize a raw tag array: preserve case, trim, drop non-strings/empties, dedupe,
47
47
  * cap at {@link MAX_TAGS_PER_ITEM}. Callers that must reject non-arrays with a
48
48
  * typed error (the wiki does) validate the shape first and delegate here.
49
49
  */
50
50
  export declare function normalizeTagList(values: readonly unknown[]): string[];
51
51
  /** Lenient variant: `undefined`/`null`/non-arrays normalize to `[]`. */
52
52
  export declare function normalizeTags(value: unknown): string[];
53
+ /** Persistence-only normalization; browser label formatting does not activate writes. */
54
+ export declare function normalizeStoredTagList(values: readonly unknown[]): string[];
55
+ export declare function normalizeStoredTags(value: unknown): string[];
56
+ /** Merge/rename targets must use the same rollout representation as ordinary writes. */
57
+ export declare function normalizeStoredTagLabel(value: string): string;
53
58
  /**
54
59
  * Registry-only caps (ADR 0017). Deliberately NOT enforced by
55
60
  * {@link normalizeTagList}: that runs on every writer's hot path, so adding a
@@ -65,8 +70,7 @@ export declare const MAX_TAG_DESCRIPTION_LENGTH = 500;
65
70
  * over {@link MAX_TAG_LABEL_LENGTH}. Returns `null` so callers can raise their
66
71
  * own typed error.
67
72
  *
68
- * Note this lowercases and trims but does NOT slugify — "Q3 Outbound" becomes
69
- * "q3 outbound", space intact. Any character is legal in a tag.
73
+ * Keeps the entered case and spaces; identity comparisons are case-insensitive. Any character is legal in a tag.
70
74
  */
71
75
  export declare function normalizeTagLabel(value: unknown): string | null;
72
76
  /**
@@ -1,5 +1,5 @@
1
1
  // Workspace tags (Tags primitive, ADR 0010; registry per ADR 0017): free-form
2
- // lowercase labels shared across primitives so campaign knowledge links across
2
+ // case-preserving labels shared across primitives so campaign knowledge links across
3
3
  // stores — a sequence, its campaign-learnings wiki page, the source table, and
4
4
  // the workflow that feeds it can all carry `q3-outbound`. Carrier storage stays
5
5
  // per-store (`tags text[]` + GIN, the 0009/0025 pattern); this module is the one
@@ -10,6 +10,7 @@
10
10
  // `ox_context.workspace_tags`) OR CARRIED (some store's tags array holds it).
11
11
  // The registry is an ANNOTATION table — description, color override, pinned —
12
12
  // and never records which items carry a tag. Drift is legal in both directions.
13
+ import { casePreservingTagWritesReady } from "./write-rollout.js";
13
14
  /** Primitive kinds that participate in the workspace tag union today. */
14
15
  export const TAG_KINDS = [
15
16
  "knowledge_page",
@@ -120,12 +121,24 @@ export function tagKindHeading(kind) {
120
121
  /** Hard cap per tagged item — matches the wiki's long-standing limit. */
121
122
  export const MAX_TAGS_PER_ITEM = 50;
122
123
  /**
123
- * Normalize a raw tag array: lowercase, trim, drop non-strings/empties, dedupe,
124
+ * Normalize a raw tag array: preserve case, trim, drop non-strings/empties, dedupe,
124
125
  * cap at {@link MAX_TAGS_PER_ITEM}. Callers that must reject non-arrays with a
125
126
  * typed error (the wiki does) validate the shape first and delegate here.
126
127
  */
127
128
  export function normalizeTagList(values) {
128
- return Array.from(new Set(values.map((tag) => (typeof tag === "string" ? tag.trim().toLowerCase() : "")).filter(Boolean))).slice(0, MAX_TAGS_PER_ITEM);
129
+ const seen = new Set();
130
+ const result = [];
131
+ for (const value of values) {
132
+ const tag = typeof value === "string" ? value.trim() : "";
133
+ const key = tag.toLowerCase();
134
+ if (!tag || seen.has(key))
135
+ continue;
136
+ seen.add(key);
137
+ result.push(tag);
138
+ if (result.length === MAX_TAGS_PER_ITEM)
139
+ break;
140
+ }
141
+ return result;
129
142
  }
130
143
  /** Lenient variant: `undefined`/`null`/non-arrays normalize to `[]`. */
131
144
  export function normalizeTags(value) {
@@ -133,6 +146,19 @@ export function normalizeTags(value) {
133
146
  return [];
134
147
  return normalizeTagList(value);
135
148
  }
149
+ /** Persistence-only normalization; browser label formatting does not activate writes. */
150
+ export function normalizeStoredTagList(values) {
151
+ const labels = normalizeTagList(values);
152
+ return casePreservingTagWritesReady() ? labels : labels.map(label => label.toLowerCase());
153
+ }
154
+ export function normalizeStoredTags(value) {
155
+ return Array.isArray(value) ? normalizeStoredTagList(value) : [];
156
+ }
157
+ /** Merge/rename targets must use the same rollout representation as ordinary writes. */
158
+ export function normalizeStoredTagLabel(value) {
159
+ const label = value.trim();
160
+ return casePreservingTagWritesReady() ? label : label.toLowerCase();
161
+ }
136
162
  /**
137
163
  * Registry-only caps (ADR 0017). Deliberately NOT enforced by
138
164
  * {@link normalizeTagList}: that runs on every writer's hot path, so adding a
@@ -148,13 +174,12 @@ export const MAX_TAG_DESCRIPTION_LENGTH = 500;
148
174
  * over {@link MAX_TAG_LABEL_LENGTH}. Returns `null` so callers can raise their
149
175
  * own typed error.
150
176
  *
151
- * Note this lowercases and trims but does NOT slugify — "Q3 Outbound" becomes
152
- * "q3 outbound", space intact. Any character is legal in a tag.
177
+ * Keeps the entered case and spaces; identity comparisons are case-insensitive. Any character is legal in a tag.
153
178
  */
154
179
  export function normalizeTagLabel(value) {
155
180
  if (typeof value !== "string")
156
181
  return null;
157
- const normalized = value.trim().toLowerCase();
182
+ const normalized = value.trim();
158
183
  if (!normalized || normalized.length > MAX_TAG_LABEL_LENGTH)
159
184
  return null;
160
185
  return normalized;
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1014.1465";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1014.1597";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1014.1465";
2
+ export const OXYGEN_BUILD_VERSION = "1.1014.1597";
@@ -0,0 +1,80 @@
1
+ import { type WorkspacePlacementMailbox, type WorkspacePlacementPlan, type WorkspacePlacementVariant } from "./workspace-placement.js";
2
+ import { type WorkspacePlacementView } from "./workspace-placement-view.js";
3
+ export declare const MAX_BULK_PLACEMENT_MESSAGES = 20000;
4
+ export type BulkPlacementSkippedSender = {
5
+ id: string;
6
+ address: string;
7
+ reason: string;
8
+ };
9
+ export type BulkPlacementCoverage = Record<"google" | "microsoft", {
10
+ receivers: number;
11
+ planned: number;
12
+ }>;
13
+ export type WorkspacePlacementBulkPlan = {
14
+ request_id: string;
15
+ organization_id: string;
16
+ plan_hash: string;
17
+ rendered_at: string;
18
+ message_count: number;
19
+ child_count: number;
20
+ sender_count: number;
21
+ receiver_providers: Array<"google" | "microsoft">;
22
+ skipped_senders: BulkPlacementSkippedSender[];
23
+ coverage: BulkPlacementCoverage;
24
+ children: WorkspacePlacementPlan[];
25
+ };
26
+ export type WorkspacePlacementBulkPreview = Omit<WorkspacePlacementBulkPlan, "organization_id" | "children" | "skipped_senders"> & {
27
+ skipped_senders: BulkPlacementSkippedSender[];
28
+ skipped_sender_count: number;
29
+ skipped_has_more: boolean;
30
+ next_skipped_cursor: string | null;
31
+ probes: Array<{
32
+ id: string;
33
+ sender_mailbox_id: string;
34
+ sender: string;
35
+ sender_name: string | null;
36
+ sender_provider: "google" | "microsoft";
37
+ sender_transport: string;
38
+ receiver_mailbox_id: string;
39
+ receiver: string;
40
+ receiver_provider: "google" | "microsoft";
41
+ variant: string;
42
+ subject: string;
43
+ body_text_characters: number;
44
+ body_html_characters: number;
45
+ }>;
46
+ probe_count: number;
47
+ has_more: boolean;
48
+ next_cursor: string | null;
49
+ };
50
+ export type WorkspacePlacementBulkView = Omit<WorkspacePlacementBulkPreview, "probes"> & {
51
+ id: string;
52
+ status: WorkspacePlacementView["status"];
53
+ created_at: string;
54
+ web_url: string;
55
+ variants: WorkspacePlacementView["variants"];
56
+ probes: WorkspacePlacementView["probes"];
57
+ directional_note: string;
58
+ };
59
+ export declare function bulkPlacementId(value: string): string;
60
+ export declare function readBulkPlacementProviders(value: unknown): Array<"google" | "microsoft">;
61
+ /** Own-account rotation, never a sender × receiver Cartesian product. */
62
+ export declare function buildWorkspacePlacementBulkPlan(input: {
63
+ requestId: string;
64
+ organizationId: string;
65
+ renderedAt: string;
66
+ senders: WorkspacePlacementMailbox[];
67
+ receivers: WorkspacePlacementMailbox[];
68
+ receiverProviders: Array<"google" | "microsoft">;
69
+ variants: WorkspacePlacementVariant[];
70
+ allowPausedProbe: boolean;
71
+ rendering: NonNullable<WorkspacePlacementPlan["rendering"]>;
72
+ skippedSenders?: BulkPlacementSkippedSender[];
73
+ renderVariant?: (variant: WorkspacePlacementVariant, sender: WorkspacePlacementMailbox, probeId: string) => WorkspacePlacementVariant;
74
+ }): WorkspacePlacementBulkPlan;
75
+ export declare function bulkPlacementSkippedPage(plan: WorkspacePlacementBulkPlan, cursor?: unknown): {
76
+ skipped_senders: BulkPlacementSkippedSender[];
77
+ skipped_sender_count: number;
78
+ skipped_has_more: boolean;
79
+ next_skipped_cursor: string | null;
80
+ };