@oxygen-agent/cli 1.1014.777 → 1.1014.913

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +10 -1
  3. package/dist/commands/admin.js +1 -1
  4. package/dist/commands/billing.js +13 -9
  5. package/dist/commands/columns.js +3 -3
  6. package/dist/commands/crm.js +2 -2
  7. package/dist/commands/deliverability.js +18 -10
  8. package/dist/commands/domains.js +15 -3
  9. package/dist/commands/mailboxes.js +22 -20
  10. package/dist/commands/managed-inboxes.js +1 -1
  11. package/dist/commands/sequences.js +34 -8
  12. package/dist/commands/voice.d.ts +21 -0
  13. package/dist/commands/voice.js +121 -6
  14. package/dist/customer-operation-commands.js +8 -2
  15. package/dist/index.js +6 -0
  16. package/dist/run-wait.js +10 -2
  17. package/node_modules/@oxygen/formula/package.json +8 -0
  18. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +3 -0
  19. package/node_modules/@oxygen/shared/dist/agent-sessions.js +22 -1
  20. package/node_modules/@oxygen/shared/dist/billing-money.d.ts +31 -0
  21. package/node_modules/@oxygen/shared/dist/billing-money.js +60 -0
  22. package/node_modules/@oxygen/shared/dist/billing.d.ts +3 -0
  23. package/node_modules/@oxygen/shared/dist/billing.js +9 -1
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.js +2 -2
  25. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +76 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +4 -2
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -2
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.d.ts +177 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +59 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  31. package/node_modules/@oxygen/shared/dist/copilot-access.js +2 -1
  32. package/node_modules/@oxygen/shared/dist/credit-guidance.js +8 -15
  33. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +65 -0
  34. package/node_modules/@oxygen/shared/dist/domain-authentication.js +54 -0
  35. package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.d.ts +5 -0
  36. package/node_modules/@oxygen/shared/dist/email-infrastructure-policy.js +13 -0
  37. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +155 -0
  38. package/node_modules/@oxygen/shared/dist/guided-activation.fixture.d.ts +15 -0
  39. package/node_modules/@oxygen/shared/dist/guided-activation.fixture.js +21 -0
  40. package/node_modules/@oxygen/shared/dist/guided-activation.js +38 -0
  41. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +9 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +41 -0
  43. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +9 -5
  44. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -0
  45. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +58 -54
  46. package/node_modules/@oxygen/shared/dist/placement-history.d.ts +3 -0
  47. package/node_modules/@oxygen/shared/dist/placement-history.js +12 -0
  48. package/node_modules/@oxygen/shared/dist/placement-report.d.ts +7 -0
  49. package/node_modules/@oxygen/shared/dist/placement-report.js +34 -0
  50. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +1 -0
  51. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +1 -0
  52. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +1 -0
  53. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +1 -0
  54. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +1 -0
  55. package/node_modules/@oxygen/shared/dist/sending-limits.js +2 -10
  56. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  57. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +87 -2
  58. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  59. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  60. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  61. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  62. package/node_modules/@oxygen/shared/dist/voice-countries.d.ts +66 -0
  63. package/node_modules/@oxygen/shared/dist/voice-countries.js +75 -0
  64. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ export declare const GUIDED_ACTIVATION_PURPOSES: readonly ["lead_scan", "morning_briefing"];
2
+ export type GuidedActivationPurpose = typeof GUIDED_ACTIVATION_PURPOSES[number];
3
+ export type GuidedActivationRoutine = {
4
+ activation_id: string;
5
+ purpose: GuidedActivationPurpose;
6
+ scan_policy?: GuidedActivationScanPolicy;
7
+ };
8
+ export declare const GUIDED_ACTIVATION_POINTERS: readonly ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
9
+ export type GuidedActivationPointer = typeof GUIDED_ACTIVATION_POINTERS[number];
10
+ export type GuidedActivationResources = Partial<Record<GuidedActivationPointer, string>>;
11
+ export declare const GUIDED_ACTIVATION_REVISIONS: readonly ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
12
+ export type GuidedActivationRevisions = Record<typeof GUIDED_ACTIVATION_REVISIONS[number], string>;
13
+ export type GuidedActivationBounds = {
14
+ recipients: number;
15
+ actions: number;
16
+ inference_credits: number;
17
+ provider_credits: number;
18
+ };
19
+ export type GuidedActivationReview = {
20
+ id: string;
21
+ purpose: "launch" | GuidedActivationPurpose;
22
+ activation_revision: number;
23
+ revisions: GuidedActivationRevisions;
24
+ bounds: GuidedActivationBounds;
25
+ input_digest: string;
26
+ expires_at: string;
27
+ actor_id: string;
28
+ authority: {
29
+ kind: "explicit" | "auto_approve";
30
+ revision: string;
31
+ };
32
+ };
33
+ export type GuidedActivation = {
34
+ id: string;
35
+ organization_id: string;
36
+ creator_id: string;
37
+ revision: number;
38
+ operating_path: "own_agent" | "oxygent";
39
+ resources: GuidedActivationResources;
40
+ outcomes: Partial<Record<"first_leads" | "sequence" | "launch" | GuidedActivationPurpose, "proposed" | "armed" | "launched" | "confirmed_sent" | "paused">>;
41
+ reviews: Partial<Record<GuidedActivationReview["purpose"], GuidedActivationReview>>;
42
+ onboarding_start: {
43
+ state: "claimed" | "posted";
44
+ chat_id: string;
45
+ turn_id?: string;
46
+ event_seq?: number;
47
+ } | null;
48
+ last_fault: {
49
+ code: string;
50
+ at: string;
51
+ } | null;
52
+ };
53
+ /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
54
+ export declare function guidedActivationEnabled(env?: Record<string, string | undefined>): boolean;
55
+ export declare function guidedActivationWorkspaceAllowed(organizationId: string, env?: Record<string, string | undefined>): boolean;
56
+ export declare function guidedActivationAvailableTo(env: Record<string, string | undefined>, viewer: {
57
+ organizationId: string;
58
+ email: string | null | undefined;
59
+ member: boolean;
60
+ oxygenRole?: string | null;
61
+ }): boolean;
62
+ export declare function assertGuidedActivationBounds(bounds: GuidedActivationBounds, purpose: GuidedActivationReview["purpose"]): void;
63
+ /** Identity survives replacement sessions/config edits and the DST repeated hour. */
64
+ export declare function guidedActivationOccurrenceKey(input: {
65
+ activationId: string;
66
+ creatorId: string;
67
+ purpose: GuidedActivationPurpose;
68
+ localDate: string;
69
+ }): string;
70
+ /** Narrow reviewed source policy. Provider adapters must refuse unsupported incremental modes. */
71
+ export type GuidedActivationScanPolicy = {
72
+ version: 1;
73
+ table_id: string;
74
+ chat_id: string;
75
+ policy_digest: string;
76
+ revisions: GuidedActivationRevisions;
77
+ sources: Array<{
78
+ source_id: string;
79
+ config_digest: string;
80
+ incremental_mode: "cursor" | "watermark";
81
+ person_key_field: string;
82
+ }>;
83
+ exclude_dnc: true;
84
+ exclude_contacted: true;
85
+ review_only: true;
86
+ max_examined: number;
87
+ max_new_leads: number;
88
+ max_provider_pages: number;
89
+ max_provider_credits: number;
90
+ max_inference_credits: number;
91
+ };
92
+ export type GuidedActivationScanSnapshot = {
93
+ version: 1;
94
+ activation_id: string;
95
+ creator_id: string;
96
+ occurrence_id: string;
97
+ run_id: string;
98
+ routine_id: string;
99
+ period: {
100
+ local_date: string;
101
+ timezone: string;
102
+ started_at: string;
103
+ settled_at: string | null;
104
+ };
105
+ policy_digest: string;
106
+ table_id: string;
107
+ status: "running" | "completed" | "partial" | "failed" | "effect_unknown" | "budget_exhausted";
108
+ reason: string | null;
109
+ counts: {
110
+ examined: number;
111
+ new: number;
112
+ deduped: number;
113
+ qualified: number;
114
+ excluded: number;
115
+ };
116
+ candidate_ids: string[];
117
+ sources: Array<{
118
+ source_id: string;
119
+ config_digest: string;
120
+ ingestion_run_ids: string[];
121
+ cursor_before: string | null;
122
+ cursor_after: string | null;
123
+ status: string;
124
+ }>;
125
+ credits: {
126
+ captured: number;
127
+ held: number;
128
+ };
129
+ };
130
+ /** C's existing native launch_review is the only launch producer. Its digest binds all material inputs server-side. */
131
+ export type GuidedActivationLaunchReview = {
132
+ digest: string;
133
+ expires_at: string;
134
+ max_live_sends: number;
135
+ max_credits: number;
136
+ };
137
+ export type GuidedActivationSequenceMarker = {
138
+ activation_id: string;
139
+ creator_id: string;
140
+ };
141
+ export type GuidedActivationManualRunPreview = {
142
+ id: string;
143
+ kind: "run_now";
144
+ activation_id: string;
145
+ purpose: GuidedActivationPurpose;
146
+ actor_id: string;
147
+ session_id: string;
148
+ session_revision: number;
149
+ config_digest: string;
150
+ per_run_credits: number;
151
+ expires_at: string;
152
+ preview_token: string;
153
+ approved_at?: string;
154
+ applied_run_id?: string;
155
+ };
@@ -0,0 +1,15 @@
1
+ import type { GuidedActivationRevisions, GuidedActivationScanSnapshot } from "./guided-activation.js";
2
+ /** Deterministic non-provider fixtures for A–G; no connection/outreach authority. */
3
+ export declare const GUIDED_ACTIVATION_FIXTURE: {
4
+ readonly organizationId: "10000000-0000-4000-8000-000000000001";
5
+ readonly actorId: "10000000-0000-4000-8000-000000000002";
6
+ readonly activationId: "10000000-0000-4000-8000-000000000003";
7
+ readonly tableId: "10000000-0000-4000-8000-000000000004";
8
+ readonly chatId: "10000000-0000-4000-8000-000000000005";
9
+ readonly routineId: "10000000-0000-4000-8000-000000000006";
10
+ readonly runId: "10000000-0000-4000-8000-000000000007";
11
+ readonly now: "2026-10-25T00:30:00.000Z";
12
+ readonly timezone: "Europe/Berlin";
13
+ };
14
+ export declare const GUIDED_ACTIVATION_REVISION_FIXTURE: GuidedActivationRevisions;
15
+ export declare const GUIDED_ACTIVATION_SCAN_FIXTURE: GuidedActivationScanSnapshot;
@@ -0,0 +1,21 @@
1
+ /** Deterministic non-provider fixtures for A–G; no connection/outreach authority. */
2
+ export const GUIDED_ACTIVATION_FIXTURE = {
3
+ organizationId: "10000000-0000-4000-8000-000000000001", actorId: "10000000-0000-4000-8000-000000000002",
4
+ activationId: "10000000-0000-4000-8000-000000000003", tableId: "10000000-0000-4000-8000-000000000004",
5
+ chatId: "10000000-0000-4000-8000-000000000005", routineId: "10000000-0000-4000-8000-000000000006",
6
+ runId: "10000000-0000-4000-8000-000000000007", now: "2026-10-25T00:30:00.000Z", timezone: "Europe/Berlin",
7
+ };
8
+ export const GUIDED_ACTIVATION_REVISION_FIXTURE = {
9
+ configuration: "config-1", sequence: "sequence-1", audience: "audience-1", recipients: "recipients-1", sender: "sender-1",
10
+ copy: "copy-1", knowledge: "knowledge-1", schedule: "schedule-1", authority: "authority-1",
11
+ };
12
+ export const GUIDED_ACTIVATION_SCAN_FIXTURE = {
13
+ version: 1, activation_id: GUIDED_ACTIVATION_FIXTURE.activationId, creator_id: GUIDED_ACTIVATION_FIXTURE.actorId,
14
+ occurrence_id: JSON.stringify([GUIDED_ACTIVATION_FIXTURE.activationId, GUIDED_ACTIVATION_FIXTURE.actorId, "lead_scan", "2026-10-25"]),
15
+ run_id: GUIDED_ACTIVATION_FIXTURE.runId, routine_id: GUIDED_ACTIVATION_FIXTURE.routineId,
16
+ period: { local_date: "2026-10-25", timezone: "Europe/Berlin", started_at: "2026-10-25T00:00:00.000Z", settled_at: "2026-10-25T00:05:00.000Z" },
17
+ policy_digest: "fixture-policy-1", table_id: GUIDED_ACTIVATION_FIXTURE.tableId, status: "completed", reason: null,
18
+ counts: { examined: 10, new: 3, deduped: 2, qualified: 3, excluded: 5 }, candidate_ids: ["lead-1", "lead-2", "lead-3"],
19
+ sources: [{ source_id: "fixture-incremental-source", config_digest: "fixture-source-1", ingestion_run_ids: [], cursor_before: "page-1", cursor_after: "page-2", status: "completed" }],
20
+ credits: { captured: 0, held: 0 },
21
+ };
@@ -0,0 +1,38 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ import { isProductionData } from "./deploy-env.js";
3
+ export const GUIDED_ACTIVATION_PURPOSES = ["lead_scan", "morning_briefing"];
4
+ export const GUIDED_ACTIVATION_POINTERS = ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
5
+ export const GUIDED_ACTIVATION_REVISIONS = ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
6
+ /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
7
+ export function guidedActivationEnabled(env = process.env) {
8
+ return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && !isProductionData(env)
9
+ && env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "staff";
10
+ }
11
+ export function guidedActivationWorkspaceAllowed(organizationId, env = process.env) {
12
+ // Philipp, 2026-10-15: every genuine DEV workspace, including future IDs.
13
+ // The old WORKSPACE_IDS inventory no longer grants or restricts admission.
14
+ return guidedActivationEnabled(env) && organizationId.trim().length > 0;
15
+ }
16
+ export function guidedActivationAvailableTo(env, viewer) {
17
+ return guidedActivationWorkspaceAllowed(viewer.organizationId, env) && viewer.member
18
+ && viewer.oxygenRole !== "client";
19
+ }
20
+ export function assertGuidedActivationBounds(bounds, purpose) {
21
+ if (!bounds || Object.values(bounds).length !== 4 || ![bounds.recipients, bounds.actions].every(value => Number.isSafeInteger(value) && value >= 0)
22
+ || ![bounds.inference_credits, bounds.provider_credits].every(value => Number.isFinite(value) && value >= 0)
23
+ || (purpose === "launch" && (bounds.actions <= 0 || bounds.recipients <= 0))
24
+ || (purpose === "lead_scan" && (bounds.recipients <= 0 || bounds.actions !== 0))
25
+ || (purpose === "morning_briefing" && (bounds.recipients !== 0 || bounds.actions !== 0 || bounds.provider_credits !== 0))) {
26
+ throw new OxygenError("activation_invalid_bounds", "Set exact action, recipient and credit ceilings for this operation.", { exitCode: 2 });
27
+ }
28
+ }
29
+ /** Identity survives replacement sessions/config edits and the DST repeated hour. */
30
+ export function guidedActivationOccurrenceKey(input) {
31
+ const { activationId, creatorId, purpose, localDate } = input;
32
+ const date = new Date(`${localDate}T00:00:00Z`);
33
+ if (!activationId || !creatorId || !GUIDED_ACTIVATION_PURPOSES.includes(purpose)
34
+ || !/^\d{4}-\d{2}-\d{2}$/.test(localDate) || !Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== localDate) {
35
+ throw new OxygenError("invalid_request", "A routine occurrence needs its activation, creator, purpose and local date.");
36
+ }
37
+ return JSON.stringify([activationId, creatorId, purpose, localDate]);
38
+ }
@@ -0,0 +1,9 @@
1
+ /** Aliases must come from the same provider person; never translate ACw to ACo. */
2
+ export type LinkedInConnectionIdentity = {
3
+ providerId: string;
4
+ profileUrl?: string | null | undefined;
5
+ publicIdentifier?: string | null | undefined;
6
+ };
7
+ export declare function linkedinConnectionKeys(identity: LinkedInConnectionIdentity): string[];
8
+ /** Return the original waiting IDs so frozen enrollment identity guards remain authoritative. */
9
+ export declare function connectedLinkedInLeadIds(targets: LinkedInConnectionIdentity[], connections: LinkedInConnectionIdentity[]): string[];
@@ -0,0 +1,41 @@
1
+ import { normalizeLinkedinProfileUrl, isLinkedinMemberIdHandle } from "./linkedin-url.js";
2
+ export function linkedinConnectionKeys(identity) {
3
+ const id = identity.providerId.trim();
4
+ const keys = id ? [`id:${id.replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, "")}`] : [];
5
+ const publicIdentifier = identity.publicIdentifier?.trim();
6
+ const publicUrl = publicIdentifier && !/[\s/?#]/.test(publicIdentifier)
7
+ ? `https://www.linkedin.com/in/${publicIdentifier}` : null;
8
+ for (const value of [identity.profileUrl, publicUrl, identity.providerId]) {
9
+ if (!value)
10
+ continue;
11
+ let path;
12
+ try {
13
+ path = new URL(value.startsWith("http") ? value : `https://${value}`).pathname;
14
+ }
15
+ catch {
16
+ continue;
17
+ }
18
+ if (!/^\/(?:in|pub)\/[^/]+\/*$/i.test(path))
19
+ continue;
20
+ const profile = normalizeLinkedinProfileUrl(value);
21
+ if (profile && !/[\s/?#]/.test(profile.dispatchHandle) && !isLinkedinMemberIdHandle(profile.dispatchHandle))
22
+ keys.push(`profile:${profile.handle}`);
23
+ }
24
+ return [...new Set(keys)];
25
+ }
26
+ /** Return the original waiting IDs so frozen enrollment identity guards remain authoritative. */
27
+ export function connectedLinkedInLeadIds(targets, connections) {
28
+ const owners = new Map();
29
+ for (const connection of connections) {
30
+ for (const key of linkedinConnectionKeys(connection)) {
31
+ const ids = owners.get(key) ?? new Set();
32
+ ids.add(connection.providerId.trim().replace(/^urn:li:(?:fs_miniProfile|fsd_profile|member):/, ""));
33
+ owners.set(key, ids);
34
+ }
35
+ }
36
+ return targets.filter(target => linkedinConnectionKeys(target).some(key => {
37
+ const ids = owners.get(key);
38
+ // Conflicting aliases are not proof of one member. Exact IDs remain usable.
39
+ return ids?.size === 1;
40
+ })).map(target => target.providerId);
41
+ }
@@ -1,6 +1,8 @@
1
1
  export const OXYGEN_WARMUP_PROVIDER = "oxygen_warmup";
2
2
  export const OXYGEN_PLACEMENT_PROVIDER = "oxygen_placement";
3
3
  export function managedEmailProviderInput(value, product) {
4
+ if (product === "placement" && typeof value === "string" && value.trim().toLowerCase() === "oxygen_deliverability")
5
+ return "emailguard";
4
6
  const publicId = product === "warmup" ? OXYGEN_WARMUP_PROVIDER : OXYGEN_PLACEMENT_PROVIDER;
5
7
  return typeof value === "string" && value.trim().toLowerCase() === publicId ? "sendkit" : value;
6
8
  }
@@ -12,7 +14,7 @@ export function isManagedPlacementProvider(value) {
12
14
  }
13
15
  // These are service-owned fields, never arbitrary customer content or opaque receipts.
14
16
  const IDENTIFIER_FIELDS = new Set([
15
- "provider", "active_provider", "warmup_provider", "warmupProvider", "warmupRail",
17
+ "provider", "health_provider", "healthProvider", "active_provider", "warmup_provider", "warmupProvider", "warmupRail",
16
18
  "action", "code", "failure_code", "error_code", "credential_source", "credentialSource",
17
19
  "method", "handoff", "reason_code", "kind", "type", "source",
18
20
  ]);
@@ -20,14 +22,14 @@ const COPY_FIELDS = new Set([
20
22
  "message", "note", "reason", "description", "hint", "transport_hint", "next_step",
21
23
  "next_action", "nextAction", "label", "title", "instruction", "instructions", "warnings",
22
24
  "recommendation", "recommendations", "limitations", "next_external_write", "price_note",
23
- "error", "errors", "summary", "blocker", "blockers", "detail", "details_text", "provider_label",
25
+ "pricing_basis", "billing", "capability", "error", "errors", "summary", "blocker", "blockers", "detail", "details_text", "provider_label", "command", "next_command", "next_commands", "needs",
24
26
  ]);
25
27
  export function managedEmailMessage(value, product = "warmup") {
26
28
  const label = product === "warmup" ? "OXYGEN Warm-up" : "OXYGEN placement evaluation";
27
29
  if (product === "placement") {
28
30
  value = value.replace(/(?:SendKit|OXYGEN placement evaluation) does not document whether inbox, spam, and missing summary values are counts or percentages; Oxygen preserves the values without conversion\./gi, "Summary units are unverified (counts or percentages); values are shown as recorded, without conversion.");
29
31
  }
30
- return value.replace(/SendKit(?:'s)?\s+warm-?up/gi, label).replace(/SendKit/gi, label);
32
+ return value.replace(/\bEMAILGUARD_[A-Z0-9_]+\b/g, "managed deliverability configuration").replace(/EmailGuard/gi, "OXYGEN deliverability").replace(/SendKit(?:'s)?\s+warm-?up/gi, label).replace(/SendKit/gi, label);
31
33
  }
32
34
  /**
33
35
  * Project only service identifiers and copy. Preserve URLs, ids, hashes, dates,
@@ -45,9 +47,11 @@ export function publicManagedEmail(value, product = "warmup") {
45
47
  return entry.replace(/^warmup:sendkit:/, "warmup:oxygen_warmup:");
46
48
  }
47
49
  if (IDENTIFIER_FIELDS.has(field)) {
50
+ if (entry.toLowerCase() === "emailguard")
51
+ return "oxygen_deliverability";
48
52
  if (entry.toLowerCase() === "sendkit")
49
53
  return publicId;
50
- return entry.replace(/sendkit_/gi, `${publicId}_`).replace(/_sendkit/gi, `_${publicId}`);
54
+ return entry.replace(/emailguard/gi, "deliverability").replace(/sendkit_/gi, `${publicId}_`).replace(/_sendkit/gi, `_${publicId}`);
51
55
  }
52
56
  return COPY_FIELDS.has(field) ? managedEmailMessage(entry, context) : entry;
53
57
  }
@@ -62,7 +66,7 @@ export function publicManagedEmail(value, product = "warmup") {
62
66
  const privateUsage = (record.provider === "sendkit" || groupKey?.provider === "sendkit") &&
63
67
  (record.type === "byok_usage" || (typeof record.byok_usage_events === "number" && record.byok_usage_events > 0));
64
68
  return Object.fromEntries(Object.entries(record).map(([key, item]) => [
65
- { sendkit_google: "oxygen_warmup_google", sendkit_microsoft: "oxygen_warmup_microsoft", sendkit_binding: "oxygen_placement_binding", sendkitBinding: "oxygen_placement_binding" }[key] ?? key,
69
+ { sendkit_google: "oxygen_warmup_google", sendkit_microsoft: "oxygen_warmup_microsoft", sendkit_binding: "oxygen_placement_binding", sendkitBinding: "oxygen_placement_binding" }[key] ?? key.replace(/emailguard/gi, "deliverability"),
66
70
  privateUsage && key === "provider" ? "private_integration"
67
71
  : privateUsage && key === "key" ? project({ ...groupKey, provider: "private_integration" }, key, context)
68
72
  : privateUsage && key === "tool_id" ? null
@@ -39,6 +39,7 @@ export type OtlpLogSinkOptions = {
39
39
  serviceName: string;
40
40
  /** Resource attributes merged after the defaults, so a caller can override them. */
41
41
  resource?: Record<string, string | null | undefined> | undefined;
42
+ /** Positive safe integer; invalid values use the default batch size. */
42
43
  batchSize?: number | undefined;
43
44
  /** `0` disables the interval timer, for runtimes that flush off a request lifecycle. */
44
45
  flushIntervalMs?: number | undefined;
@@ -210,7 +210,9 @@ function toLogRecord(record) {
210
210
  };
211
211
  }
212
212
  export function createOtlpLogSink(options) {
213
- const batchSize = options.batchSize && options.batchSize > 0 ? options.batchSize : DEFAULT_BATCH_SIZE;
213
+ const requestedBatchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
214
+ const batchSize = Number.isSafeInteger(requestedBatchSize) && requestedBatchSize > 0
215
+ ? requestedBatchSize : DEFAULT_BATCH_SIZE;
214
216
  const maxQueue = options.maxQueue && options.maxQueue > 0 ? options.maxQueue : DEFAULT_MAX_QUEUE;
215
217
  const flushIntervalMs = options.flushIntervalMs === undefined
216
218
  ? DEFAULT_FLUSH_INTERVAL_MS
@@ -332,64 +334,66 @@ export function createOtlpLogSink(options) {
332
334
  : {}),
333
335
  })));
334
336
  };
335
- const flush = async () => {
336
- if (inFlight)
337
- return inFlight;
338
- if (queue.length === 0)
339
- return undefined;
340
- const batch = queue.splice(0, batchSize);
337
+ const postBatch = async (batch) => {
341
338
  const payload = JSON.stringify({
342
- resourceLogs: [
343
- {
339
+ resourceLogs: [{
344
340
  resource: { attributes: resourceAttributes },
345
- scopeLogs: [
346
- {
347
- scope: { name: "oxygen" },
348
- logRecords: batch.map(toLogRecord),
349
- },
350
- ],
351
- },
352
- ],
341
+ scopeLogs: [{ scope: { name: "oxygen" }, logRecords: batch.map(toLogRecord) }],
342
+ }],
353
343
  });
354
- const run = (async () => {
355
- try {
356
- const response = await doFetch(options.endpoint, {
357
- method: "POST",
358
- headers: { "Content-Type": "application/json", ...options.headers },
359
- body: payload,
360
- signal: AbortSignal.timeout(EXPORT_TIMEOUT_MS),
361
- });
362
- if (response.ok) {
363
- try {
364
- const rejected = await rejectedRecords(response, batch.length);
365
- if (rejected)
366
- report(rejected, { status: response.status, error_code: "partial_rejection" });
367
- }
368
- catch {
369
- report(batch.length, { status: response.status, error_code: "invalid_response" });
344
+ try {
345
+ const response = await doFetch(options.endpoint, {
346
+ method: "POST",
347
+ headers: { "Content-Type": "application/json", ...options.headers },
348
+ body: payload,
349
+ signal: AbortSignal.timeout(EXPORT_TIMEOUT_MS),
350
+ });
351
+ if (response.ok) {
352
+ try {
353
+ const rejected = await rejectedRecords(response, batch.length);
354
+ if (rejected) {
355
+ report(rejected, { status: response.status, error_code: "partial_rejection" });
356
+ return false;
370
357
  }
371
- return;
358
+ return true;
359
+ }
360
+ catch {
361
+ report(batch.length, { status: response.status, error_code: "invalid_response" });
362
+ return false;
372
363
  }
373
- // Receivers/proxies can echo rejected payloads or credentials. Never
374
- // read arbitrary response text into operational diagnostics.
375
- await response.body?.cancel().catch(() => undefined);
376
- // The batch is gone deliberately: retrying into an unhealthy collector
377
- // is how a telemetry path turns an ingest blip into memory pressure.
378
- report(batch.length, { status: response.status, error_code: "receiver_rejected" });
379
- }
380
- catch (error) {
381
- report(batch.length, {
382
- error_code: "transport_failed",
383
- error_cause: sanitizedErrorCause(error),
384
- });
385
- }
386
- finally {
387
- reportOverflow();
388
364
  }
389
- })();
390
- inFlight = run.finally(() => {
391
- inFlight = null;
392
- });
365
+ // Receivers/proxies can echo payloads or credentials. Keep diagnostics fixed.
366
+ await response.body?.cancel().catch(() => undefined);
367
+ report(batch.length, { status: response.status, error_code: "receiver_rejected" });
368
+ return false;
369
+ }
370
+ catch (error) {
371
+ report(batch.length, {
372
+ error_code: "transport_failed",
373
+ error_cause: sanitizedErrorCause(error),
374
+ });
375
+ return false;
376
+ }
377
+ finally {
378
+ reportOverflow();
379
+ }
380
+ };
381
+ const flush = async () => {
382
+ if (inFlight)
383
+ return inFlight;
384
+ if (queue.length === 0)
385
+ return undefined;
386
+ // Keep one POST in flight. Healthy full batches continue immediately so the
387
+ // timer does not cap production at 500/2s. Failure keeps the existing loss
388
+ // accounting/backoff; neither a rejected batch nor a partial reply is retried.
389
+ const drain = async () => {
390
+ do {
391
+ const accepted = await postBatch(queue.splice(0, batchSize));
392
+ if (!accepted || queue.length < batchSize)
393
+ return;
394
+ } while (queue.length > 0);
395
+ };
396
+ inFlight = Promise.resolve().then(drain).finally(() => { inFlight = null; });
393
397
  return inFlight;
394
398
  };
395
399
  const timer = flushIntervalMs > 0
@@ -404,7 +408,7 @@ export function createOtlpLogSink(options) {
404
408
  if (timer)
405
409
  clearInterval(timer);
406
410
  // Drain rather than flush once: a shutdown's final lines are usually the
407
- // interesting ones, and one flush ships only the oldest `batchSize`.
411
+ // interesting ones. Flush the final partial batch as well as healthy full batches.
408
412
  while (queue.length > 0) {
409
413
  await flush();
410
414
  }
@@ -0,0 +1,3 @@
1
+ /** Bind a continuation to its filter. Tenant/RLS still authorizes the anchor. */
2
+ export declare function placementHistoryCursor(id: string, status?: string | null): string;
3
+ export declare function placementHistoryCursorAnchor(cursor: string, status?: string | null): string | null;
@@ -0,0 +1,12 @@
1
+ const PLACEMENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ /** Bind a continuation to its filter. Tenant/RLS still authorizes the anchor. */
3
+ export function placementHistoryCursor(id, status) {
4
+ if (!PLACEMENT_ID.test(id))
5
+ throw new Error("Invalid placement-test cursor anchor.");
6
+ return `placement:v1:${status ?? "all"}:${id}`;
7
+ }
8
+ export function placementHistoryCursorAnchor(cursor, status) {
9
+ const [kind, version, filter, id, extra] = cursor.split(":");
10
+ return kind === "placement" && version === "v1" && filter === (status ?? "all")
11
+ && id !== undefined && PLACEMENT_ID.test(id) && extra === undefined ? id : null;
12
+ }
@@ -0,0 +1,7 @@
1
+ /** Shared placement-report policy. No provider calls, sends or billing effects. */
2
+ export declare const PLACEMENT_REPORT_WAIT_MS: number;
3
+ export declare const INCOMPLETE_PLACEMENT_MESSAGE = "This test is incomplete. Available seed results are shown.";
4
+ export declare function placementReportDeadline(provider: string, results: unknown): string | null;
5
+ export declare function placementReportExpired(provider: string, results: unknown, now?: number): boolean;
6
+ /** Customer copy follows the persisted settlement receipt, never an inferred balance. */
7
+ export declare function placementRefundMessage(value: unknown): string;
@@ -0,0 +1,34 @@
1
+ /** Shared placement-report policy. No provider calls, sends or billing effects. */
2
+ export const PLACEMENT_REPORT_WAIT_MS = 2 * 60 * 60 * 1000;
3
+ export const INCOMPLETE_PLACEMENT_MESSAGE = "This test is incomplete. Available seed results are shown.";
4
+ export function placementReportDeadline(provider, results) {
5
+ if (provider !== "emailguard" || !results || typeof results !== "object")
6
+ return null;
7
+ const receipt = results.seed_delivery;
8
+ if (!receipt || typeof receipt !== "object")
9
+ return null;
10
+ const send = receipt;
11
+ if (send.status !== "sent" || typeof send.sent_at !== "string")
12
+ return null;
13
+ const sent = Date.parse(send.sent_at);
14
+ return Number.isFinite(sent) ? new Date(sent + PLACEMENT_REPORT_WAIT_MS).toISOString() : null;
15
+ }
16
+ export function placementReportExpired(provider, results, now = Date.now()) {
17
+ const deadline = placementReportDeadline(provider, results);
18
+ return deadline !== null && now >= Date.parse(deadline);
19
+ }
20
+ /** Customer copy follows the persisted settlement receipt, never an inferred balance. */
21
+ export function placementRefundMessage(value) {
22
+ if (!value || typeof value !== "object")
23
+ return "";
24
+ const receipt = value;
25
+ if (receipt.status === "pending")
26
+ return "Your test-credit refund is pending.";
27
+ if (receipt.status === "refunded")
28
+ return "The test charge was refunded.";
29
+ if (receipt.status === "allowance_restored")
30
+ return "The included test allowance has been restored.";
31
+ if (receipt.status === "not_applicable")
32
+ return "Own-key tests do not receive an OXYGEN test-credit refund.";
33
+ return "";
34
+ }
@@ -109,6 +109,7 @@ type ChannelLimit = {
109
109
  };
110
110
  export type PlanLimitsSendingLimits = {
111
111
  email_per_mailbox?: ChannelLimit;
112
+ entra_per_mailbox?: ChannelLimit;
112
113
  whatsapp_per_number?: ChannelLimit;
113
114
  voice_per_number?: ChannelLimit;
114
115
  linkedin_per_account?: {
@@ -239,6 +239,7 @@ export function planLimitsSendingRows(sending) {
239
239
  return [];
240
240
  const maximum = (limit) => limit?.max_per_day === null ? "no maximum" : formatLimitCount(limit?.max_per_day);
241
241
  return [
242
+ ...(sending.entra_per_mailbox ? [{ label: "Entra per mailbox", defaultPerDay: formatLimitCount(sending.entra_per_mailbox.default_per_day), maximumPerDay: maximum(sending.entra_per_mailbox) }] : []),
242
243
  { label: "Email per mailbox", defaultPerDay: formatLimitCount(sending.email_per_mailbox?.default_per_day), maximumPerDay: maximum(sending.email_per_mailbox) },
243
244
  { label: "WhatsApp per number", defaultPerDay: formatLimitCount(sending.whatsapp_per_number?.default_per_day), maximumPerDay: maximum(sending.whatsapp_per_number) },
244
245
  {
@@ -19,6 +19,7 @@ export declare const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3400;
19
19
  export declare const X_ACCOUNT_CREDITS_PER_MONTH = 1000;
20
20
  export declare const EGRESS_IP_CREDITS_PER_MONTH = 2500;
21
21
  export declare const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
22
+ export declare const SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH = 25;
22
23
  export declare const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
23
24
  export declare const MAILBOX_WARMUP_CREDITS_PER_MONTH = 0;
24
25
  export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
@@ -41,6 +41,7 @@ export const WHATSAPP_ACCOUNT_CREDITS_PER_MONTH = 3_400;
41
41
  export const X_ACCOUNT_CREDITS_PER_MONTH = 1_000;
42
42
  export const EGRESS_IP_CREDITS_PER_MONTH = 2_500;
43
43
  export const SENDING_MAILBOX_CREDITS_PER_MONTH = 100;
44
+ export const SENDING_ENTRA_MAILBOX_CREDITS_PER_MONTH = 25;
44
45
  export const MANAGED_INBOX_CREDITS_PER_MONTH = 300;
45
46
  export const MAILBOX_WARMUP_CREDITS_PER_MONTH = 0;
46
47
  export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 100;
@@ -23,6 +23,7 @@ export declare function emailMailboxDailyCapMaximum(_options?: RepricingOptions)
23
23
  export declare function emailMailboxDailyCapInForce(input: {
24
24
  storedCap: number;
25
25
  source: EmailMailboxDailyCapSource | null;
26
+ metadata?: unknown;
26
27
  }, options?: RepricingOptions): number;
27
28
  /** A new voice number may dial this many times a day once it has warmed up. */
28
29
  export declare const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
@@ -1,12 +1,4 @@
1
- // Per-channel sending limits that are the same on every plan (2026-09 repricing,
2
- // decision L3; specification slice S52). Limits guard, credits meter: none of
3
- // these is sold, and none charges for sending.
4
- //
5
- // Email campaign ramp: 5 / 10 / 20 based on accepted-send days, then the
6
- // configured cap (25 by default, maximum 50), independent of billing activation.
7
- // The versioned migration changes old defaults once; later choices are retained.
8
- //
9
- // Voice per number: 100 dials a day by default, adjustable up to 300.
1
+ import { mailboxInfrastructureDailyCap } from "./email-infrastructure-policy.js";
10
2
  import { EMAIL_DELIVERABILITY_DEFAULTS } from "./email-deliverability-policy.js";
11
3
  /**
12
4
  * The daily cap a new mailbox gets when nobody names one. New mailboxes were
@@ -35,7 +27,7 @@ export function emailMailboxDailyCapMaximum(_options = {}) {
35
27
  export function emailMailboxDailyCapInForce(input, options = {}) {
36
28
  const stored = Number.isFinite(input.storedCap) ? Math.max(0, Math.floor(input.storedCap)) : 0;
37
29
  const maximum = emailMailboxDailyCapMaximum(options);
38
- return maximum === null ? stored : Math.min(stored, maximum);
30
+ return Math.min(stored, maximum ?? Infinity, mailboxInfrastructureDailyCap(input.metadata) ?? Infinity);
39
31
  }
40
32
  /** A new voice number may dial this many times a day once it has warmed up. */
41
33
  export const VOICE_NUMBER_DEFAULT_DAILY_CAP = 100;
@@ -1,3 +1,4 @@
1
+ import { guidedActivationEnabled } from "./guided-activation.js";
1
2
  import { isStaffEmail } from "./staff.js";
2
3
  /**
3
4
  * AI teammates (ADR 0032): Agents people talk to, each person in one ongoing
@@ -20,7 +21,7 @@ export function teammatesEnabled(env = process.env) {
20
21
  return env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff";
21
22
  }
22
23
  export function teammatesAvailableTo(env, viewer) {
23
- return teammatesEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && isStaffEmail(viewer.email, env);
24
+ return teammatesEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && (guidedActivationEnabled(env) || isStaffEmail(viewer.email, env));
24
25
  }
25
26
  /**
26
27
  * The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an