@oxygen-agent/cli 1.1014.1464 → 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,28 @@
1
+ export const HEALTHY_SCORE_MIN = 80;
2
+ export const DEGRADED_SCORE_MIN = 50;
3
+ export function deriveEmailHealthState(score) {
4
+ if (score == null || !Number.isFinite(score))
5
+ return "unknown";
6
+ const value = Math.max(0, Math.min(100, score));
7
+ return value >= HEALTHY_SCORE_MIN ? "healthy" : value >= DEGRADED_SCORE_MIN ? "degraded" : "critical";
8
+ }
9
+ export function summarizeFleetHealth(readings) {
10
+ const scored = readings.filter(row => row.healthState !== "unknown" && row.healthScore !== null && Number.isFinite(row.healthScore));
11
+ const score = scored.length ? scored.reduce((sum, row) => sum + Math.max(0, Math.min(100, row.healthScore)), 0) / scored.length : null;
12
+ const current = scored.filter(row => row.healthFreshness === "current").length;
13
+ const stale = scored.filter(row => row.healthFreshness === "stale").length;
14
+ const unverified = scored.length - current - stale;
15
+ const scoreState = deriveEmailHealthState(score);
16
+ const critical = readings.filter(row => row.healthState === "critical").length;
17
+ const degraded = readings.filter(row => row.healthState === "degraded").length;
18
+ const coverage = scored.length === 0 ? "none" : scored.length === readings.length ? "complete" : "partial";
19
+ const freshness = scored.length === 0 ? "unmeasured" : current === scored.length ? "current" : stale === scored.length ? "stale" : "mixed";
20
+ // A strong recorded average cannot clear an unmeasured or stale fleet. Individual faults remain independent.
21
+ const fleetState = scoreState === "healthy" && (coverage !== "complete" || freshness !== "current") ? "unknown" : scoreState;
22
+ return {
23
+ score, scoreState, fleetState,
24
+ worstMailboxState: critical ? "critical" : degraded ? "degraded" : scored.length ? "healthy" : "unknown",
25
+ total: readings.length, scored: scored.length, current, stale, unverified,
26
+ unscored: readings.length - scored.length, degraded, critical, coverage, freshness,
27
+ };
28
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Subscription lists and consent for bulk email (sending-domain Sequences).
3
+ *
4
+ * A list is who agreed to hear from the workspace about one thing (the
5
+ * newsletter, product updates, onboarding). A subscription is one address on
6
+ * one list with a status; every change appends a consent event, which is the
7
+ * evidence a regulator or a mailbox provider can ask for. Sending-domain
8
+ * Sequences only send to `subscribed` addresses on their own list, on top of
9
+ * do-not-contact. Pure and browser-safe.
10
+ */
11
+ export declare const EMAIL_SUBSCRIPTION_LIST_KINDS: readonly ["marketing", "product_updates", "onboarding"];
12
+ export type EmailSubscriptionListKind = typeof EMAIL_SUBSCRIPTION_LIST_KINDS[number];
13
+ export declare const EMAIL_SUBSCRIPTION_STATUSES: readonly ["subscribed", "unsubscribed", "pending", "cleaned"];
14
+ export type EmailSubscriptionStatus = typeof EMAIL_SUBSCRIPTION_STATUSES[number];
15
+ /** Append-only consent event kinds. Status changes plus the provider facts that cause them. */
16
+ export declare const EMAIL_SUBSCRIPTION_EVENT_TYPES: readonly ["subscribed", "unsubscribed", "pending", "cleaned", "complaint", "hard_bounce"];
17
+ export type EmailSubscriptionEventType = typeof EMAIL_SUBSCRIPTION_EVENT_TYPES[number];
18
+ export declare const EMAIL_SUBSCRIPTION_LIST_KEY_RE: RegExp;
19
+ /** Most addresses one import call accepts. */
20
+ export declare const EMAIL_SUBSCRIPTION_IMPORT_LIMIT = 5000;
21
+ export declare class EmailSubscriptionInputError extends Error {
22
+ readonly code: string;
23
+ readonly field: string;
24
+ constructor(code: string, field: string, message: string);
25
+ }
26
+ export declare function isEmailSubscriptionStatus(value: unknown): value is EmailSubscriptionStatus;
27
+ export declare function isEmailSubscriptionListKind(value: unknown): value is EmailSubscriptionListKind;
28
+ /** The stored subscription key: trimmed, lowercased, one plausible address. */
29
+ export declare function normalizeSubscriptionEmail(value: unknown): string;
30
+ /**
31
+ * Consent evidence recorded with a subscription change. A change to
32
+ * `subscribed` must say where consent came from: the notice version and a hash
33
+ * of the exact notice text the person saw, or, for an import, the source and
34
+ * the person who vouches for it. IP addresses are stored hashed only.
35
+ */
36
+ export type EmailConsentEvidence = {
37
+ notice_version?: string | null;
38
+ /** sha256 hex of the exact consent notice text shown. */
39
+ notice_text_hash?: string | null;
40
+ form_url?: string | null;
41
+ /** sha256 hex of the subscriber's IP address; never the address itself. */
42
+ ip_hash?: string | null;
43
+ user_agent?: string | null;
44
+ /** Free-text provenance for imports, e.g. "Resend segment export 2026-10-20". */
45
+ basis?: string | null;
46
+ };
47
+ /** Normalize evidence; `requireForSubscribe` refuses a subscribe that names no basis. */
48
+ export declare function parseEmailConsentEvidence(value: unknown, options: {
49
+ requireForSubscribe: boolean;
50
+ }): EmailConsentEvidence;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Subscription lists and consent for bulk email (sending-domain Sequences).
3
+ *
4
+ * A list is who agreed to hear from the workspace about one thing (the
5
+ * newsletter, product updates, onboarding). A subscription is one address on
6
+ * one list with a status; every change appends a consent event, which is the
7
+ * evidence a regulator or a mailbox provider can ask for. Sending-domain
8
+ * Sequences only send to `subscribed` addresses on their own list, on top of
9
+ * do-not-contact. Pure and browser-safe.
10
+ */
11
+ export const EMAIL_SUBSCRIPTION_LIST_KINDS = ["marketing", "product_updates", "onboarding"];
12
+ export const EMAIL_SUBSCRIPTION_STATUSES = ["subscribed", "unsubscribed", "pending", "cleaned"];
13
+ /** Append-only consent event kinds. Status changes plus the provider facts that cause them. */
14
+ export const EMAIL_SUBSCRIPTION_EVENT_TYPES = [
15
+ "subscribed", "unsubscribed", "pending", "cleaned", "complaint", "hard_bounce",
16
+ ];
17
+ export const EMAIL_SUBSCRIPTION_LIST_KEY_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
18
+ /** Most addresses one import call accepts. */
19
+ export const EMAIL_SUBSCRIPTION_IMPORT_LIMIT = 5000;
20
+ export class EmailSubscriptionInputError extends Error {
21
+ code;
22
+ field;
23
+ constructor(code, field, message) {
24
+ super(message);
25
+ this.code = code;
26
+ this.field = field;
27
+ this.name = "EmailSubscriptionInputError";
28
+ }
29
+ }
30
+ export function isEmailSubscriptionStatus(value) {
31
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_STATUSES.includes(value);
32
+ }
33
+ export function isEmailSubscriptionListKind(value) {
34
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_LIST_KINDS.includes(value);
35
+ }
36
+ /** The stored subscription key: trimmed, lowercased, one plausible address. */
37
+ export function normalizeSubscriptionEmail(value) {
38
+ const email = typeof value === "string" ? value.trim().toLowerCase() : "";
39
+ if (!email || email.length > 320 || !/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/.test(email)) {
40
+ throw new EmailSubscriptionInputError("invalid_subscription_email", "email", "Each subscription needs one valid email address.");
41
+ }
42
+ return email;
43
+ }
44
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
45
+ function optionalText(value, field, max) {
46
+ if (value === undefined || value === null || value === "")
47
+ return null;
48
+ if (typeof value !== "string" || value.length > max) {
49
+ throw new EmailSubscriptionInputError("invalid_consent_evidence", `evidence.${field}`, `evidence.${field} must be text of at most ${max} characters.`);
50
+ }
51
+ return value.trim() || null;
52
+ }
53
+ function optionalHash(value, field) {
54
+ const text = optionalText(value, field, 64);
55
+ if (text === null)
56
+ return null;
57
+ if (!SHA256_HEX.test(text.toLowerCase())) {
58
+ throw new EmailSubscriptionInputError("invalid_consent_evidence", `evidence.${field}`, `evidence.${field} must be a sha256 hex digest.`);
59
+ }
60
+ return text.toLowerCase();
61
+ }
62
+ /** Normalize evidence; `requireForSubscribe` refuses a subscribe that names no basis. */
63
+ export function parseEmailConsentEvidence(value, options) {
64
+ const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ const evidence = {
66
+ notice_version: optionalText(record.notice_version, "notice_version", 80),
67
+ notice_text_hash: optionalHash(record.notice_text_hash, "notice_text_hash"),
68
+ form_url: optionalText(record.form_url, "form_url", 2048),
69
+ ip_hash: optionalHash(record.ip_hash, "ip_hash"),
70
+ user_agent: optionalText(record.user_agent, "user_agent", 512),
71
+ basis: optionalText(record.basis, "basis", 500),
72
+ };
73
+ if (options.requireForSubscribe) {
74
+ const notice = Boolean(evidence.notice_version && evidence.notice_text_hash);
75
+ if (!notice && !evidence.basis) {
76
+ throw new EmailSubscriptionInputError("consent_evidence_required", "evidence", "Subscribing someone needs consent evidence: notice_version with notice_text_hash, or a basis naming where consent came from.");
77
+ }
78
+ }
79
+ return evidence;
80
+ }
@@ -43,6 +43,34 @@ export declare function signUnsubscribeToken(payload: UnsubscribeTokenPayload, s
43
43
  * secret, tampered body, replayed garbage). Pure: the route maps null -> 401.
44
44
  */
45
45
  export declare function verifyUnsubscribeToken(token: string, secret: string): UnsubscribeTokenPayload | null;
46
+ /**
47
+ * v2: a subscription-list unsubscribe for bulk mail sent through a sending
48
+ * domain. `lst` names the list the recipient leaves; `scope` says whether the
49
+ * click leaves that one list or every list (and the do-not-contact ledger).
50
+ * v1 tokens are untouched: `verifyUnsubscribeToken` still accepts only v1.
51
+ */
52
+ export type UnsubscribeTokenPayloadV2 = {
53
+ v: 2;
54
+ org: string;
55
+ email: string;
56
+ /** Subscription list id the recipient leaves. */
57
+ lst: string;
58
+ /** `list` leaves only `lst`; `all` leaves every list and suppresses the address. */
59
+ scope: "list" | "all";
60
+ seq?: string;
61
+ enr?: string;
62
+ /** Sending-domain account id, for provenance. */
63
+ sd?: string;
64
+ iat: number;
65
+ };
66
+ export type AnyUnsubscribeTokenPayload = UnsubscribeTokenPayload | UnsubscribeTokenPayloadV2;
67
+ /** Sign a v2 (list-scoped) token. Same wire format and secret as v1. */
68
+ export declare function signUnsubscribeTokenV2(payload: UnsubscribeTokenPayloadV2, secret: string): string;
69
+ /**
70
+ * Verify a v1 OR v2 token. The one-click route uses this and branches on `v`;
71
+ * v1 callers keep `verifyUnsubscribeToken`, whose contract is unchanged.
72
+ */
73
+ export declare function verifyAnyUnsubscribeToken(token: string, secret: string): AnyUnsubscribeTokenPayload | null;
46
74
  /**
47
75
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
48
76
  * path treats null as "omit the header" (a send with no one-click link still
@@ -49,6 +49,53 @@ export function verifyUnsubscribeToken(token, secret) {
49
49
  return null;
50
50
  return parseTokenPayload(body);
51
51
  }
52
+ /** Sign a v2 (list-scoped) token. Same wire format and secret as v1. */
53
+ export function signUnsubscribeTokenV2(payload, secret) {
54
+ const normalized = { ...payload, email: payload.email.trim().toLowerCase() };
55
+ return signPayload(normalized, secret);
56
+ }
57
+ function parseTokenPayloadV2(body) {
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (!parsed || typeof parsed !== "object")
66
+ return null;
67
+ const candidate = parsed;
68
+ if (candidate.v !== 2)
69
+ return null;
70
+ const org = typeof candidate.org === "string" ? candidate.org.trim() : "";
71
+ const email = typeof candidate.email === "string" ? candidate.email.trim().toLowerCase() : "";
72
+ const lst = typeof candidate.lst === "string" ? candidate.lst.trim() : "";
73
+ if (!org || !email || !email.includes("@") || !lst)
74
+ return null;
75
+ if (candidate.scope !== "list" && candidate.scope !== "all")
76
+ return null;
77
+ return {
78
+ v: 2,
79
+ org,
80
+ email,
81
+ lst,
82
+ scope: candidate.scope,
83
+ ...(typeof candidate.seq === "string" && candidate.seq ? { seq: candidate.seq } : {}),
84
+ ...(typeof candidate.enr === "string" && candidate.enr ? { enr: candidate.enr } : {}),
85
+ ...(typeof candidate.sd === "string" && candidate.sd ? { sd: candidate.sd } : {}),
86
+ iat: typeof candidate.iat === "number" && Number.isFinite(candidate.iat) ? candidate.iat : 0,
87
+ };
88
+ }
89
+ /**
90
+ * Verify a v1 OR v2 token. The one-click route uses this and branches on `v`;
91
+ * v1 callers keep `verifyUnsubscribeToken`, whose contract is unchanged.
92
+ */
93
+ export function verifyAnyUnsubscribeToken(token, secret) {
94
+ const body = splitAndVerifyToken(token, secret);
95
+ if (body === null)
96
+ return null;
97
+ return parseTokenPayload(body) ?? parseTokenPayloadV2(body);
98
+ }
52
99
  /**
53
100
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
54
101
  * path treats null as "omit the header" (a send with no one-click link still
@@ -78,22 +78,10 @@ export declare function readWorkflowStepFailure(error: unknown): WorkflowStepFai
78
78
  */
79
79
  export declare const AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE = "automation_actions_exceeded";
80
80
  export declare function isAutomationUsageQuotaError(error: unknown): boolean;
81
- /**
82
- * The credit-era sibling of the code above, and the one most callers actually want.
83
- *
84
- * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
85
- * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
86
- * every base plan, so on current plans the admission gate in captureAutomationActions()
87
- * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
88
- * instead — steps bill automationStepCredits() each out of the one credit pool.
89
- *
90
- * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
91
- * the customer must feel it, and the run must stop cleanly." Anything else is a metering
92
- * write that failed and must be stepped over. Matching only the quota code is how a
93
- * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
94
- * taking the graceful pause path — abandoning the lease and every remaining due trigger
95
- * for that tenant's sweep.
96
- */
81
+ /** Stop dispatch on a known cap refusal or unavailable authoritative Workflow
82
+ * admission. Unavailable accounting can follow a lost commit acknowledgement;
83
+ * this predicate does not prove that the fee capture was not applied. Other
84
+ * legacy metering faults retain their best-effort behavior. */
97
85
  export declare const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
98
86
  export declare function isAutomationAdmissionRefusal(error: unknown): boolean;
99
87
  /**
@@ -216,29 +216,20 @@ export function isAutomationUsageQuotaError(error) {
216
216
  && typeof error === "object"
217
217
  && error.code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE;
218
218
  }
219
- /**
220
- * The credit-era sibling of the code above, and the one most callers actually want.
221
- *
222
- * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
223
- * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
224
- * every base plan, so on current plans the admission gate in captureAutomationActions()
225
- * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
226
- * instead — steps bill automationStepCredits() each out of the one credit pool.
227
- *
228
- * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
229
- * the customer must feel it, and the run must stop cleanly." Anything else is a metering
230
- * write that failed and must be stepped over. Matching only the quota code is how a
231
- * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
232
- * taking the graceful pause path — abandoning the lease and every remaining due trigger
233
- * for that tenant's sweep.
234
- */
219
+ /** Stop dispatch on a known cap refusal or unavailable authoritative Workflow
220
+ * admission. Unavailable accounting can follow a lost commit acknowledgement;
221
+ * this predicate does not prove that the fee capture was not applied. Other
222
+ * legacy metering faults retain their best-effort behavior. */
235
223
  export const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
236
224
  export function isAutomationAdmissionRefusal(error) {
237
225
  if (!error || typeof error !== "object")
238
226
  return false;
239
227
  const code = error.code;
240
228
  return code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE
241
- || code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE;
229
+ || code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE
230
+ || code === "budget_policy_exceeded"
231
+ || code === "workspace_credit_cap_exceeded"
232
+ || code === "automation_admission_unavailable";
242
233
  }
243
234
  const MAX_CAUSE_DEPTH = 4;
244
235
  const MAX_CAUSE_MESSAGE_LENGTH = 200;
@@ -1,4 +1,16 @@
1
1
  export declare const GUIDED_ACTIVATION_PURPOSES: readonly ["lead_scan", "morning_briefing"];
2
+ /**
3
+ * The research that runs beside the onboarding chat: its own session and turn,
4
+ * so the person can talk to the Oxygent while it works. `results_turn_id` is
5
+ * the chat turn that reported it; `results_pending` means the chat was busy
6
+ * when it finished and the report goes out after that turn.
7
+ */
8
+ export type GuidedOnboardingResearch = {
9
+ session_id: string;
10
+ turn_id?: string;
11
+ results_turn_id?: string;
12
+ results_pending?: boolean;
13
+ };
2
14
  export type GuidedActivationPurpose = typeof GUIDED_ACTIVATION_PURPOSES[number];
3
15
  export type GuidedActivationRoutine = {
4
16
  activation_id: string;
@@ -54,13 +66,15 @@ export type GuidedActivation = {
54
66
  turn_id?: string;
55
67
  event_seq?: number;
56
68
  finished_at?: string;
69
+ /** Agent-led onboarding's background research (Philipp, 2026-10-20): its hidden session and turn, then the chat turn that reported it. */
70
+ research?: GuidedOnboardingResearch;
57
71
  } | null;
58
72
  last_fault: {
59
73
  code: string;
60
74
  at: string;
61
75
  } | null;
62
76
  };
63
- /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
77
+ /** Production requires the explicit all-workspace rollout; the DEV pilot keeps its own switch. */
64
78
  export declare function guidedActivationEnabled(env?: Record<string, string | undefined>): boolean;
65
79
  export declare function guidedActivationWorkspaceAllowed(organizationId: string, env?: Record<string, string | undefined>): boolean;
66
80
  export declare function guidedActivationAvailableTo(env: Record<string, string | undefined>, viewer: {
@@ -3,8 +3,10 @@ import { isProductionData } from "./deploy-env.js";
3
3
  export const GUIDED_ACTIVATION_PURPOSES = ["lead_scan", "morning_briefing"];
4
4
  export const GUIDED_ACTIVATION_POINTERS = ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
5
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. */
6
+ /** Production requires the explicit all-workspace rollout; the DEV pilot keeps its own switch. */
7
7
  export function guidedActivationEnabled(env = process.env) {
8
+ if (env.OXYGEN_DEPLOY_ENV === "selfhost-prod")
9
+ return env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "all";
8
10
  return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && !isProductionData(env)
9
11
  && env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "staff";
10
12
  }
@@ -46,6 +46,7 @@ export type LinkedInInviteStep = LinkedInStepBase & {
46
46
  };
47
47
  export type LinkedInWaitForConnectionStep = LinkedInStepBase & {
48
48
  kind: "wait_for_connection";
49
+ withdraw_on_timeout?: boolean;
49
50
  /** Days to wait for the invite to be accepted before acting on on_timeout. */
50
51
  timeout_days: number;
51
52
  /** What to do if the invite is never accepted. Defaults to "stop". */
@@ -53,6 +54,7 @@ export type LinkedInWaitForConnectionStep = LinkedInStepBase & {
53
54
  };
54
55
  export type LinkedInWaitStep = LinkedInStepBase & {
55
56
  kind: "wait";
57
+ working_days?: boolean;
56
58
  /** Fixed delay before the next step. Provide days and/or hours (>= 1 total). */
57
59
  days?: number;
58
60
  hours?: number;
@@ -0,0 +1,5 @@
1
+ export { mailboxStandbyWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
2
+ export declare function assertMailboxStandbyMetadataWriteReady(metadata: Record<string, unknown> | null | undefined): void;
3
+ export declare function assertSequenceStandbyWriteReady(settings: {
4
+ auto_replace_mailboxes?: boolean;
5
+ } | null | undefined): void;
@@ -0,0 +1,9 @@
1
+ import { assertMailboxStandbyMetadataWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
2
+ export { mailboxStandbyWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
3
+ export function assertMailboxStandbyMetadataWriteReady(metadata) {
4
+ assertMailboxStandbyMetadataWritesReady(metadata);
5
+ }
6
+ export function assertSequenceStandbyWriteReady(settings) {
7
+ if (settings?.auto_replace_mailboxes === true)
8
+ assertMailboxStandbyWritesReady();
9
+ }
@@ -0,0 +1,43 @@
1
+ export type MailboxOutreachRole = "outreach" | "standby";
2
+ export type MailboxStandbyReadinessEvidence = {
3
+ observed_at: string;
4
+ confirmed_days: number;
5
+ warmup_running: boolean;
6
+ dns_ready: boolean;
7
+ auth_healthy: boolean;
8
+ warmup_provider: string;
9
+ warmup_vendor_ref: string;
10
+ ready_since?: string;
11
+ };
12
+ export declare const DEFAULT_MAILBOX_STANDBY_MINIMUM_DAYS = 14;
13
+ export declare const MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS: number;
14
+ export declare function mailboxOutreachRole(metadata: Record<string, unknown> | null | undefined): MailboxOutreachRole;
15
+ /** Campaign membership is authoritative DB evidence, never the promotion marker alone. */
16
+ export declare function isMailboxOutreachAllowed(input: {
17
+ metadata: Record<string, unknown> | null | undefined;
18
+ sequenceId?: string;
19
+ autoReplace?: boolean;
20
+ replacementSequenceIds?: string[];
21
+ }): boolean;
22
+ export declare function mailboxStandbyReadiness(input: {
23
+ metadata: Record<string, unknown>;
24
+ status: string;
25
+ warmupState: string;
26
+ warmupProvider: string | null;
27
+ warmupVendorRef: string | null;
28
+ senderProfileId: string | null;
29
+ senderProfileActive: boolean;
30
+ minimumDays: number;
31
+ now?: Date;
32
+ }): {
33
+ ready: boolean;
34
+ confirmed_days: number;
35
+ minimum_days: number;
36
+ blockers: string[];
37
+ };
38
+ /** The latest promoted descendant; waiting records never create a sending identity. */
39
+ export declare function resolveMailboxStandbyReplacement(rows: readonly {
40
+ oldMailboxId: string;
41
+ newMailboxId: string | null;
42
+ status: string;
43
+ }[], original: string): string | null;
@@ -0,0 +1,52 @@
1
+ export const DEFAULT_MAILBOX_STANDBY_MINIMUM_DAYS = 14;
2
+ export const MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
3
+ export function mailboxOutreachRole(metadata) {
4
+ return metadata?.outreach_role === "standby" ? "standby" : "outreach";
5
+ }
6
+ /** Campaign membership is authoritative DB evidence, never the promotion marker alone. */
7
+ export function isMailboxOutreachAllowed(input) {
8
+ if (mailboxOutreachRole(input.metadata) === "standby")
9
+ return false;
10
+ if (!Object.hasOwn(input.metadata ?? {}, "standby_promotion"))
11
+ return true;
12
+ return input.autoReplace === true && !!input.sequenceId && input.replacementSequenceIds?.includes(input.sequenceId) === true;
13
+ }
14
+ export function mailboxStandbyReadiness(input) {
15
+ const raw = input.metadata.standby_readiness;
16
+ const evidence = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
17
+ const blockers = [];
18
+ const observed = Date.parse(evidence.observed_at ?? "");
19
+ const age = (input.now ?? new Date()).getTime() - observed;
20
+ if (!Number.isFinite(observed) || age > MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS || age < -300_000)
21
+ blockers.push("readiness_evidence_stale_or_missing");
22
+ if (!evidence.warmup_provider || !evidence.warmup_vendor_ref || evidence.warmup_provider !== input.warmupProvider || evidence.warmup_vendor_ref !== input.warmupVendorRef)
23
+ blockers.push("warmup_seat_unverified");
24
+ const days = typeof evidence.confirmed_days === "number" && Number.isInteger(evidence.confirmed_days) && evidence.confirmed_days >= 0 ? evidence.confirmed_days : 0;
25
+ if (days < input.minimumDays)
26
+ blockers.push("warmup_days_remaining");
27
+ if (evidence.warmup_running !== true || !["warming", "active"].includes(input.warmupState))
28
+ blockers.push("warmup_not_running");
29
+ if (evidence.dns_ready !== true)
30
+ blockers.push("dns_not_ready");
31
+ if (evidence.auth_healthy !== true || input.metadata.oauth_grant_failure || input.metadata.mailbox_deleted_at)
32
+ blockers.push("authorization_unverified");
33
+ if (input.status !== "active" || input.metadata.auto_pause)
34
+ blockers.push("mailbox_not_active_or_safety_paused");
35
+ if (!input.senderProfileId || !input.senderProfileActive)
36
+ blockers.push("sender_profile_required");
37
+ return { ready: blockers.length === 0, confirmed_days: days, minimum_days: input.minimumDays, blockers };
38
+ }
39
+ /** The latest promoted descendant; waiting records never create a sending identity. */
40
+ export function resolveMailboxStandbyReplacement(rows, original) {
41
+ const visited = new Set([original]);
42
+ let current = original;
43
+ while (true) {
44
+ const next = rows.find(row => row.status === "promoted" && row.newMailboxId && row.oldMailboxId === current)?.newMailboxId;
45
+ if (!next)
46
+ return current === original ? null : current;
47
+ if (visited.has(next))
48
+ return null;
49
+ visited.add(next);
50
+ current = next;
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ import type { CapabilityRouteMatch } from "./capability-discovery.js";
2
+ export declare function nativeChatEnabled(env?: NodeJS.ProcessEnv): boolean;
3
+ export declare function isNativeChatTool(name: string): boolean;
4
+ /** The compiled support card includes the dev surface; hosted discovery projects it away when closed. */
5
+ export declare function withNativeChatDiscovery<T extends CapabilityRouteMatch>(route: T, enabled: boolean): T;
@@ -0,0 +1,27 @@
1
+ import { resolveDeployEnv } from "./deploy-env.js";
2
+ export function nativeChatEnabled(env = process.env) {
3
+ if (!/^(1|true|on)$/i.test(env.OXYGEN_NATIVE_CHAT_ENABLED?.trim() ?? ""))
4
+ return false;
5
+ const tier = resolveDeployEnv(env).tier;
6
+ return tier === "dev" || tier === "local";
7
+ }
8
+ export function isNativeChatTool(name) {
9
+ return name.startsWith("oxygen_chat_");
10
+ }
11
+ /** The compiled support card includes the dev surface; hosted discovery projects it away when closed. */
12
+ export function withNativeChatDiscovery(route, enabled) {
13
+ if (enabled || route.card.id !== "support-and-feedback")
14
+ return route;
15
+ return {
16
+ ...route,
17
+ card: {
18
+ ...route.card,
19
+ gatewayTools: route.card.gatewayTools.filter(name => !isNativeChatTool(name)),
20
+ gatewayCommands: route.card.gatewayCommands.filter(name => !name.startsWith("chat ")),
21
+ endpointSections: route.card.endpointSections.filter(name => name !== "chat"),
22
+ intentTerms: route.card.intentTerms.filter(name => !["support chat", "support thread", "chat attachment"].includes(name)),
23
+ },
24
+ recommendedTools: route.recommendedTools.filter(name => !isNativeChatTool(name)),
25
+ recommendedCommands: route.recommendedCommands.filter(name => !name.startsWith("chat ")),
26
+ };
27
+ }
@@ -0,0 +1,124 @@
1
+ /** Native support chat projections shared by web, CLI and MCP. */
2
+ export type ChatStatus = "todo" | "snoozed" | "done";
3
+ export type ChatPriority = "urgent" | "high" | "normal" | "low";
4
+ export type ChatAgentStage = "investigating" | "fixing" | "testing" | "awaiting_release" | "needs_human" | "completed";
5
+ export type ChatAttachment = {
6
+ id: string;
7
+ name: string;
8
+ size_bytes: number;
9
+ content_type: string;
10
+ download_url: string;
11
+ };
12
+ export type ChatAgent = {
13
+ id: string;
14
+ name: string;
15
+ stage: ChatAgentStage;
16
+ last_activity_at: string;
17
+ summary: string | null;
18
+ session_url: string | null;
19
+ };
20
+ export type ChatRelease = {
21
+ status: "none" | "dev" | "prod";
22
+ fix_sha: string | null;
23
+ pr_url: string | null;
24
+ required_roles: string[];
25
+ verified_at: string | null;
26
+ stale: boolean;
27
+ };
28
+ export type ChatTriage = {
29
+ source: "review" | "system_one" | "staff";
30
+ confidence: number | null;
31
+ reason: string | null;
32
+ message_id: string | null;
33
+ };
34
+ export type ChatThread = {
35
+ id: string;
36
+ channel_id: string;
37
+ subject: string;
38
+ status: ChatStatus;
39
+ waiting_for_customer: boolean;
40
+ snoozed_until: string | null;
41
+ assignee_email: string | null;
42
+ priority: ChatPriority;
43
+ triage: ChatTriage;
44
+ agent: ChatAgent | null;
45
+ release: ChatRelease;
46
+ context: Record<string, unknown>;
47
+ created_at: string;
48
+ updated_at: string;
49
+ last_customer_at: string | null;
50
+ unread_count: number;
51
+ };
52
+ export type ChatMessage = {
53
+ id: string;
54
+ thread_id: string;
55
+ body: string;
56
+ author: {
57
+ id: string | null;
58
+ name: string;
59
+ kind: "customer" | "staff" | "agent" | "system";
60
+ };
61
+ visibility: "public" | "internal";
62
+ source: "web" | "cli" | "mcp";
63
+ attachments: ChatAttachment[];
64
+ mentions: string[];
65
+ created_at: string;
66
+ };
67
+ export type ChatSignificance = {
68
+ plan: string | null;
69
+ mrr_cents: number | null;
70
+ net_30d_cents: number | null;
71
+ net_lifetime_cents: number | null;
72
+ currency: string | null;
73
+ billing_owner_name: string | null;
74
+ shared_billing: boolean;
75
+ captured_at: string | null;
76
+ };
77
+ export type ChatChannel = {
78
+ id: string;
79
+ organization_id: string;
80
+ name: string;
81
+ updated_at: string;
82
+ unread_count: number;
83
+ open_count: number;
84
+ priority: ChatPriority;
85
+ oldest_unanswered_at: string | null;
86
+ significance?: ChatSignificance;
87
+ };
88
+ export type ChatMember = {
89
+ id: string;
90
+ name: string;
91
+ email: string | null;
92
+ };
93
+ export type ChatChannelView = {
94
+ channel: ChatChannel;
95
+ threads: ChatThread[];
96
+ members: ChatMember[];
97
+ cursor: string;
98
+ };
99
+ export type ChatThreadView = {
100
+ thread: ChatThread;
101
+ messages: ChatMessage[];
102
+ members: ChatMember[];
103
+ cursor: string;
104
+ has_more: boolean;
105
+ };
106
+ export type ChatThreadPatch = {
107
+ status?: ChatStatus;
108
+ waiting_for_customer?: boolean;
109
+ snoozed_until?: string | null;
110
+ assignee_email?: string | null;
111
+ priority?: ChatPriority;
112
+ agent?: {
113
+ id: string;
114
+ name: string;
115
+ stage: ChatAgentStage;
116
+ summary?: string | null;
117
+ session_url?: string | null;
118
+ } | null;
119
+ release?: {
120
+ fix_sha?: string | null;
121
+ pr_url?: string | null;
122
+ required_roles?: string[];
123
+ };
124
+ };