@oxygen-agent/cli 1.1010.644 → 1.1010.721

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 (50) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -1
  3. package/dist/inbox-needs-reply-notice.d.ts +12 -0
  4. package/dist/inbox-needs-reply-notice.js +51 -0
  5. package/dist/index.js +186 -45
  6. package/dist/skills.js +48 -22
  7. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +33 -2
  8. package/node_modules/@oxygen/shared/dist/billing-anchors.js +67 -2
  9. package/node_modules/@oxygen/shared/dist/billing.d.ts +63 -9
  10. package/node_modules/@oxygen/shared/dist/billing.js +96 -14
  11. package/node_modules/@oxygen/shared/dist/capability-discovery.js +11 -1
  12. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +4 -4
  13. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +4 -4
  14. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +25 -0
  15. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  16. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +6 -1
  17. package/node_modules/@oxygen/shared/dist/feature-gates.js +7 -1
  18. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -1
  19. package/node_modules/@oxygen/shared/dist/index.js +2 -1
  20. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  21. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  22. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  23. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +118 -0
  24. package/node_modules/@oxygen/shared/dist/plan-band.js +147 -0
  25. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +131 -120
  26. package/node_modules/@oxygen/shared/dist/plan-limits.js +80 -71
  27. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +79 -14
  28. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +61 -12
  29. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +4 -3
  30. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +9 -3
  31. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  32. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  33. package/node_modules/@oxygen/shared/dist/repricing.d.ts +130 -0
  34. package/node_modules/@oxygen/shared/dist/repricing.js +320 -0
  35. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +32 -0
  36. package/node_modules/@oxygen/shared/dist/sending-limits.js +49 -0
  37. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  38. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +24 -0
  39. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +58 -1
  40. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +39 -10
  41. package/node_modules/@oxygen/shared/dist/table-capacity.js +68 -4
  42. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  43. package/node_modules/@oxygen/shared/dist/telemetry.js +36 -2
  44. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  45. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  46. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  47. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  48. package/package.json +1 -1
  49. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  50. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
@@ -19,6 +19,24 @@ const STRIPE_PRICE_OXYGEN_IDS = {
19
19
  oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_USD ?? "" },
20
20
  oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_USD ?? "" },
21
21
  };
22
+ /**
23
+ * The yearly Price of each rung (Repricing 2026-09, decision 1.2: pay 10 months,
24
+ * get 12, credits still granted monthly). A second Price on the rung's existing
25
+ * Product, since month and year are distinct intervals a Portal configuration
26
+ * accepts under one Product.
27
+ *
28
+ * DARK until the yearly Prices exist (slice S14): every env var is unset, an
29
+ * unset id is "", and tierFromPriceId refuses "" before any map is consulted, so
30
+ * no subscription resolves to a yearly interval and nothing is ever sliced.
31
+ */
32
+ const STRIPE_PRICE_OXYGEN_ANNUAL_IDS = {
33
+ oxygen_49: { usd: process.env.STRIPE_PRICE_OXYGEN_49_ANNUAL_USD ?? "" },
34
+ oxygen_99: { usd: process.env.STRIPE_PRICE_OXYGEN_99_ANNUAL_USD ?? "" },
35
+ oxygen_199: { usd: process.env.STRIPE_PRICE_OXYGEN_199_ANNUAL_USD ?? "" },
36
+ oxygen_499: { usd: process.env.STRIPE_PRICE_OXYGEN_499_ANNUAL_USD ?? "" },
37
+ oxygen_999: { usd: process.env.STRIPE_PRICE_OXYGEN_999_ANNUAL_USD ?? "" },
38
+ oxygen_1999: { usd: process.env.STRIPE_PRICE_OXYGEN_1999_ANNUAL_USD ?? "" },
39
+ };
22
40
  // Grandfathered on 2026-09-19. These three stay pointed at their LIVE prices
23
41
  // (35 Starter + 2 Pro subscriptions bill against them) rather than moving into
24
42
  // the legacy CSV lists: tierFromPriceId must keep returning "starter"/"pro"/
@@ -82,6 +100,39 @@ export function getPlanPriceId(plan, currency) {
82
100
  return (plan.priceIds[currency]
83
101
  || (currency === DEFAULT_BILLING_CURRENCY ? plan.priceId : ""));
84
102
  }
103
+ /**
104
+ * The configured Price for one rung and interval, or "" when that Price is not
105
+ * configured (every yearly Price, until S14 creates them).
106
+ */
107
+ export function getPurchasablePlanPriceId(planKey, interval, currency = DEFAULT_BILLING_CURRENCY) {
108
+ const ids = interval === "year"
109
+ ? STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]
110
+ : STRIPE_PRICE_OXYGEN_IDS[planKey];
111
+ return ids[currency] || "";
112
+ }
113
+ /**
114
+ * The billing interval of a recognized plan Price. "year" only for a configured
115
+ * yearly Oxygen Price; every other recognized Price (monthly rungs and every
116
+ * grandfathered or legacy plan) is "month"; an unrecognized id is null.
117
+ *
118
+ * This is the one definition the credit-grant paths read, so the Stripe webhook,
119
+ * the lazy grant on a balance read and the slice sweep can never disagree about
120
+ * whether a paid period is sliced.
121
+ */
122
+ export function planBillingIntervalForPriceId(priceId) {
123
+ if (!priceId)
124
+ return null;
125
+ for (const planKey of PURCHASABLE_PLAN_KEYS) {
126
+ if (Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
127
+ return "year";
128
+ }
129
+ }
130
+ return tierFromPriceId(priceId) === null ? null : "month";
131
+ }
132
+ /** Every configured yearly plan Price id (empty until S14). */
133
+ export function configuredAnnualPlanPriceIds() {
134
+ return uniqueNonEmptyPriceIds(PURCHASABLE_PLAN_KEYS.flatMap((planKey) => Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey])));
135
+ }
85
136
  /**
86
137
  * Resolve a Stripe price id to the plan key stored in `subscriptions.tier`.
87
138
  *
@@ -97,7 +148,8 @@ export function tierFromPriceId(priceId) {
97
148
  if (!priceId)
98
149
  return null;
99
150
  for (const planKey of PURCHASABLE_PLAN_KEYS) {
100
- if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)) {
151
+ if (Object.values(STRIPE_PRICE_OXYGEN_IDS[planKey]).includes(priceId)
152
+ || Object.values(STRIPE_PRICE_OXYGEN_ANNUAL_IDS[planKey]).includes(priceId)) {
101
153
  return planKey;
102
154
  }
103
155
  }
@@ -122,6 +174,11 @@ export function tierFromPriceId(priceId) {
122
174
  * Complete runtime-recognized Price set grouped by the current self-serve
123
175
  * family. Catalog audit/migration code consumes this instead of maintaining a
124
176
  * second env map that can silently omit grandfathered subscriptions.
177
+ *
178
+ * The yearly Prices are deliberately NOT listed yet: the migration planner
179
+ * matches an outgoing Oxygen Price to its target by amount, and must learn to
180
+ * compare intervals (repricing slice S16) before a yearly Price may enter its
181
+ * source set. S14 adds them when it creates the Prices.
125
182
  */
126
183
  export function recognizedStripePriceIdsBySelfServeTier() {
127
184
  const byTier = {
@@ -1,13 +1,42 @@
1
- /**
2
- * Plan-independent infrastructure envelope for durable Workspace Tables.
3
- * Plans do not sell different storage entitlements; every workspace gets the
4
- * same safety boundary and plans continue to differ through operation/rate and
5
- * credit limits.
6
- */
7
- export declare const WORKSPACE_TABLE_CAPACITY: Readonly<{
8
- tableRowLimit: 3000000;
9
- workspaceRowLimit: 25000000;
1
+ import type { PlanBand } from "./plan-band.js";
2
+ import { type RepricingOptions } from "./repricing.js";
3
+ export type WorkspaceTableCapacityLimits = {
4
+ tableRowLimit: number;
5
+ workspaceRowLimit: number;
10
6
  workspaceDatabaseWarningBytes: number;
11
7
  workspaceDatabaseLimitBytes: number;
12
- }>;
8
+ };
9
+ /**
10
+ * The envelope every workspace had before storage scaled with plan size, and
11
+ * still the value in force for any workspace whose plan-resolved limits have not
12
+ * been written to its tenant yet. The tenant capacity trigger falls back to these
13
+ * exact numbers when its per-workspace limit columns are NULL (tenant migration
14
+ * `0451_workspace_capacity_plan_limits.sql`,
15
+ * enforced by `0452_workspace_capacity_plan_limit_enforcement.sql`); a test pins the two together.
16
+ */
17
+ export declare const WORKSPACE_TABLE_CAPACITY: Readonly<WorkspaceTableCapacityLimits>;
13
18
  export declare const WORKSPACE_TABLE_DATABASE_STORAGE_SCOPE: "postgres_workspace_tables_heap_indexes_toast";
19
+ /** Two thirds of the hard limit, rounded down (PROPOSED P-48, repricing spec § 4.1). */
20
+ export declare function workspaceDatabaseWarningBytesFor(limitBytes: number): number;
21
+ /**
22
+ * Storage by plan size, as ratified (decision L1, 2026-09-26; repricing spec
23
+ * § 4.1). This is the target ladder, not what is in force: see
24
+ * `resolveWorkspaceTableCapacity`.
25
+ */
26
+ export declare const PLAN_BAND_TABLE_CAPACITY_TARGETS: Readonly<Record<PlanBand, WorkspaceTableCapacityLimits>>;
27
+ /**
28
+ * Sizes above today's envelope ship only after a capacity test at 5M and 10M
29
+ * rows per Table (decision L1). Until that test passes these bands keep today's
30
+ * envelope; shipping them is removing a band from this list.
31
+ */
32
+ export declare const PLAN_BANDS_AWAITING_CAPACITY_TEST: readonly PlanBand[];
33
+ /**
34
+ * The storage limits in force for a plan band right now.
35
+ *
36
+ * - A limit that tightens (free, $49, $99) is a scheduled 2026-09 repricing value
37
+ * (`storage.<band>.*` in `REPRICING_2026_09_SCHEDULE`): today's envelope until
38
+ * the effective-date switch, the ratified ladder from it on.
39
+ * - A band awaiting the capacity test keeps today's envelope.
40
+ * - Every other band is already at its ladder value.
41
+ */
42
+ export declare function resolveWorkspaceTableCapacity(band: PlanBand, options?: RepricingOptions): WorkspaceTableCapacityLimits;
@@ -1,9 +1,12 @@
1
+ import { findRepricedValue, repricedValue } from "./repricing.js";
1
2
  const GIB = 1024 ** 3;
2
3
  /**
3
- * Plan-independent infrastructure envelope for durable Workspace Tables.
4
- * Plans do not sell different storage entitlements; every workspace gets the
5
- * same safety boundary and plans continue to differ through operation/rate and
6
- * credit limits.
4
+ * The envelope every workspace had before storage scaled with plan size, and
5
+ * still the value in force for any workspace whose plan-resolved limits have not
6
+ * been written to its tenant yet. The tenant capacity trigger falls back to these
7
+ * exact numbers when its per-workspace limit columns are NULL (tenant migration
8
+ * `0451_workspace_capacity_plan_limits.sql`,
9
+ * enforced by `0452_workspace_capacity_plan_limit_enforcement.sql`); a test pins the two together.
7
10
  */
8
11
  export const WORKSPACE_TABLE_CAPACITY = Object.freeze({
9
12
  tableRowLimit: 3_000_000,
@@ -12,3 +15,64 @@ export const WORKSPACE_TABLE_CAPACITY = Object.freeze({
12
15
  workspaceDatabaseLimitBytes: 30 * GIB,
13
16
  });
14
17
  export const WORKSPACE_TABLE_DATABASE_STORAGE_SCOPE = "postgres_workspace_tables_heap_indexes_toast";
18
+ /** Two thirds of the hard limit, rounded down (PROPOSED P-48, repricing spec § 4.1). */
19
+ export function workspaceDatabaseWarningBytesFor(limitBytes) {
20
+ return Math.floor((limitBytes * 2) / 3);
21
+ }
22
+ function ladderRow(tableRowLimit, workspaceRowLimit, databaseGib) {
23
+ const workspaceDatabaseLimitBytes = databaseGib * GIB;
24
+ return Object.freeze({
25
+ tableRowLimit,
26
+ workspaceRowLimit,
27
+ workspaceDatabaseWarningBytes: workspaceDatabaseWarningBytesFor(workspaceDatabaseLimitBytes),
28
+ workspaceDatabaseLimitBytes,
29
+ });
30
+ }
31
+ /**
32
+ * Storage by plan size, as ratified (decision L1, 2026-09-26; repricing spec
33
+ * § 4.1). This is the target ladder, not what is in force: see
34
+ * `resolveWorkspaceTableCapacity`.
35
+ */
36
+ export const PLAN_BAND_TABLE_CAPACITY_TARGETS = Object.freeze({
37
+ free: ladderRow(100_000, 1_000_000, 2),
38
+ "49": ladderRow(1_000_000, 5_000_000, 10),
39
+ "99": ladderRow(2_000_000, 10_000_000, 20),
40
+ "199": ladderRow(3_000_000, 25_000_000, 30),
41
+ "499": ladderRow(3_000_000, 25_000_000, 30),
42
+ "999": ladderRow(5_000_000, 50_000_000, 60),
43
+ "1999": ladderRow(10_000_000, 100_000_000, 120),
44
+ });
45
+ /**
46
+ * Sizes above today's envelope ship only after a capacity test at 5M and 10M
47
+ * rows per Table (decision L1). Until that test passes these bands keep today's
48
+ * envelope; shipping them is removing a band from this list.
49
+ */
50
+ export const PLAN_BANDS_AWAITING_CAPACITY_TEST = Object.freeze(["999", "1999"]);
51
+ const STORAGE_FIELDS = [
52
+ ["tableRowLimit", "table_row_limit"],
53
+ ["workspaceRowLimit", "workspace_row_limit"],
54
+ ["workspaceDatabaseWarningBytes", "workspace_database_warning_bytes"],
55
+ ["workspaceDatabaseLimitBytes", "workspace_database_limit_bytes"],
56
+ ];
57
+ /**
58
+ * The storage limits in force for a plan band right now.
59
+ *
60
+ * - A limit that tightens (free, $49, $99) is a scheduled 2026-09 repricing value
61
+ * (`storage.<band>.*` in `REPRICING_2026_09_SCHEDULE`): today's envelope until
62
+ * the effective-date switch, the ratified ladder from it on.
63
+ * - A band awaiting the capacity test keeps today's envelope.
64
+ * - Every other band is already at its ladder value.
65
+ */
66
+ export function resolveWorkspaceTableCapacity(band, options = {}) {
67
+ if (PLAN_BANDS_AWAITING_CAPACITY_TEST.includes(band))
68
+ return WORKSPACE_TABLE_CAPACITY;
69
+ const target = PLAN_BAND_TABLE_CAPACITY_TARGETS[band];
70
+ const resolved = { ...target };
71
+ for (const [field, key] of STORAGE_FIELDS) {
72
+ const scheduled = findRepricedValue(`storage.${band}.${key}`);
73
+ if (scheduled && typeof scheduled.before === "number" && typeof scheduled.after === "number") {
74
+ resolved[field] = repricedValue({ before: scheduled.before, after: scheduled.after }, options);
75
+ }
76
+ }
77
+ return Object.freeze(resolved);
78
+ }
@@ -1,6 +1,7 @@
1
1
  export type TelemetryAttributes = Record<string, unknown>;
2
2
  export type WithTelemetrySpanOptions = {
3
3
  isTransient?: (error: unknown) => boolean;
4
+ traceparent?: string | null | undefined;
4
5
  };
5
6
  export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
6
7
  /**
@@ -18,5 +19,13 @@ export declare function setActiveTelemetryAttributes(attributes: TelemetryAttrib
18
19
  export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
19
20
  export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
20
21
  export declare function recordTelemetryHistogram(name: string, value: number, attributes?: TelemetryAttributes): void;
22
+ /**
23
+ * Records the latest observed value of a level (a queue depth, a pool size).
24
+ * A synchronous gauge rather than an observable one: the callers already sample
25
+ * on their own cadence (the BullMQ relay reads Redis counts every 10s), and a
26
+ * collection-time callback would have to reach into that loop's state. The
27
+ * exported point is the last value recorded before each export.
28
+ */
29
+ export declare function recordTelemetryGauge(name: string, value: number, attributes?: TelemetryAttributes): void;
21
30
  export declare function commonTelemetryAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
22
31
  export declare function metricSafeAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
@@ -3,14 +3,21 @@ import { deployEnvTelemetryEnvironment, deployPlatformAttribute } from "./deploy
3
3
  import { errorId } from "./log.js";
4
4
  import { normalizeTelemetryAttributes } from "./redaction.js";
5
5
  import { redactSqlParameters, sqlErrorTelemetryAttributes } from "./sql-error.js";
6
+ import { remoteParentContext } from "./trace-context.js";
6
7
  import { OXYGEN_VERSION } from "./version.js";
7
8
  const counterCache = new Map();
8
9
  const histogramCache = new Map();
10
+ const gaugeCache = new Map();
9
11
  // skipcq: JS-0116 — `async` keeps synchronous setup throws (getTracer / attribute
10
12
  // normalization) as rejections so the Promise<T> contract holds for all callers.
11
13
  export async function withTelemetrySpan(tracerName, name, attributes, fn, options) {
12
14
  const tracer = trace.getTracer(tracerName, OXYGEN_VERSION);
13
- return tracer.startActiveSpan(name, { attributes: normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)) }, async (span) => {
15
+ const remote = options?.traceparent ? remoteParentContext(options.traceparent) : null;
16
+ const spanOptions = {
17
+ attributes: normalizeTelemetryAttributes(commonTelemetryAttributes(remote ? { ...attributes, "oxygen.trace.propagated": true } : attributes)),
18
+ ...(remote && remote.links.length > 0 ? { links: remote.links } : {}),
19
+ };
20
+ const run = async (span) => {
14
21
  try {
15
22
  return await fn();
16
23
  }
@@ -37,7 +44,10 @@ export async function withTelemetrySpan(tracerName, name, attributes, fn, option
37
44
  finally {
38
45
  span.end();
39
46
  }
40
- });
47
+ };
48
+ return remote
49
+ ? tracer.startActiveSpan(name, spanOptions, remote.context, run)
50
+ : tracer.startActiveSpan(name, spanOptions, run);
41
51
  }
42
52
  /**
43
53
  * The active global span's trace id: the join key into SigNoz/Axiom traces.
@@ -84,6 +94,23 @@ export function recordTelemetryHistogram(name, value, attributes) {
84
94
  const histogram = getHistogram(name);
85
95
  histogram.record(value, normalizeTelemetryAttributes(metricSafeAttributes(commonTelemetryAttributes(attributes))));
86
96
  }
97
+ /**
98
+ * Records the latest observed value of a level (a queue depth, a pool size).
99
+ * A synchronous gauge rather than an observable one: the callers already sample
100
+ * on their own cadence (the BullMQ relay reads Redis counts every 10s), and a
101
+ * collection-time callback would have to reach into that loop's state. The
102
+ * exported point is the last value recorded before each export.
103
+ */
104
+ export function recordTelemetryGauge(name, value, attributes) {
105
+ if (!Number.isFinite(value))
106
+ return;
107
+ let gauge = gaugeCache.get(name);
108
+ if (!gauge) {
109
+ gauge = getMeter().createGauge(name);
110
+ gaugeCache.set(name, gauge);
111
+ }
112
+ gauge.record(value, normalizeTelemetryAttributes(metricSafeAttributes(commonTelemetryAttributes(attributes))));
113
+ }
87
114
  export function commonTelemetryAttributes(attributes) {
88
115
  const workerProcess = isWorkerProcess();
89
116
  // Each attribute reads an ordered env-var fallback chain that differs by
@@ -200,6 +227,13 @@ const METRIC_SAFE_ATTRIBUTE_KEYS = new Set([
200
227
  "table_action.lane",
201
228
  "tenant.ticket_reason",
202
229
  "queue.stalled",
230
+ // BullMQ execution queue dimensions (oxygen.worker.queue.*). `queue.lane` is
231
+ // the closed EXECUTION_LANES enum, `queue.role` the lane or "relay",
232
+ // `queue.state` one of BullMQ's five job-count states. Never the execution id
233
+ // or tenant id: those stay on the worker.job span and the lifecycle log.
234
+ "queue.lane",
235
+ "queue.role",
236
+ "queue.state",
203
237
  "scope",
204
238
  "rail",
205
239
  "overall_status",
@@ -0,0 +1,29 @@
1
+ import { type Context, type Link, type SpanContext } from "@opentelemetry/api";
2
+ /** Parses a W3C `traceparent`; anything malformed or all-zero yields null. */
3
+ export declare function parseTraceparent(value: unknown): SpanContext | null;
4
+ /** The validated traceparent, or undefined — for optional fields that must drop bad input. */
5
+ export declare function validTraceparent(value: unknown): string | undefined;
6
+ export declare function formatTraceparent(spanContext: SpanContext | undefined | null): string | null;
7
+ /** The `traceparent` of the currently active span, or null outside any span. */
8
+ export declare function activeTraceparent(): string | null;
9
+ /**
10
+ * The parent context for a span that continues a stored traceparent.
11
+ *
12
+ * The remote span becomes the PARENT, so request -> job -> provider.* reads as
13
+ * one trace; whatever span was active (a worker cycle, the BullMQ runtime) is
14
+ * kept as a LINK so the scheduling side stays navigable. Every non-span context
15
+ * value (baggage, suppression flags) is preserved. Null for a missing or
16
+ * invalid traceparent: the caller then starts an ordinary child of the active
17
+ * context.
18
+ */
19
+ export declare function remoteParentContext(traceparent: unknown): {
20
+ context: Context;
21
+ links: Link[];
22
+ } | null;
23
+ /**
24
+ * Links the active span to a stored enqueue traceparent. For work that is
25
+ * already inside its execution span (a BullMQ `bullmq.execute`) and only learns
26
+ * the enqueuing request once it has loaded its own row: a link joins the two
27
+ * traces without a second per-job span. Returns whether a link was added.
28
+ */
29
+ export declare function linkActiveSpanToTraceparent(traceparent: unknown): boolean;
@@ -0,0 +1,88 @@
1
+ import { context, isSpanContextValid, trace, } from "@opentelemetry/api";
2
+ /**
3
+ * W3C Trace Context carried across the durable enqueue boundary.
4
+ *
5
+ * A web request that enqueues work (a table-action run, a BullMQ execution) and
6
+ * the worker that later executes it run in different processes, minutes apart,
7
+ * with Postgres in between. Nothing propagated the OTel context across that gap,
8
+ * so a provider call made by the worker lived in a worker trace that could not
9
+ * be joined to the request that paid for it. The enqueuing request stores a
10
+ * `traceparent` string next to its correlation `trace_id`; the worker parents
11
+ * its job span on it (see `withTelemetrySpan`'s `traceparent` option).
12
+ *
13
+ * The value is built from and parsed into a span context directly rather than
14
+ * through the globally registered propagator: the propagator is whatever the
15
+ * host SDK installed (or a no-op in tests and in a process without telemetry),
16
+ * and a stored value must mean the same thing on every reader.
17
+ */
18
+ // Version 00 only: a later version may append fields this reader cannot judge.
19
+ // Lowercase hex per the spec; all-zero trace/parent ids are explicitly invalid.
20
+ const TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
21
+ /** Parses a W3C `traceparent`; anything malformed or all-zero yields null. */
22
+ export function parseTraceparent(value) {
23
+ if (typeof value !== "string")
24
+ return null;
25
+ const match = TRACEPARENT_PATTERN.exec(value);
26
+ if (!match)
27
+ return null;
28
+ const spanContext = {
29
+ traceId: match[1],
30
+ spanId: match[2],
31
+ traceFlags: Number.parseInt(match[3], 16),
32
+ isRemote: true,
33
+ };
34
+ return isSpanContextValid(spanContext) ? spanContext : null;
35
+ }
36
+ /** The validated traceparent, or undefined — for optional fields that must drop bad input. */
37
+ export function validTraceparent(value) {
38
+ return parseTraceparent(value) ? value : undefined;
39
+ }
40
+ export function formatTraceparent(spanContext) {
41
+ if (!spanContext || !isSpanContextValid(spanContext))
42
+ return null;
43
+ const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
44
+ return `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
45
+ }
46
+ /** The `traceparent` of the currently active span, or null outside any span. */
47
+ export function activeTraceparent() {
48
+ return formatTraceparent(trace.getActiveSpan()?.spanContext());
49
+ }
50
+ /**
51
+ * The parent context for a span that continues a stored traceparent.
52
+ *
53
+ * The remote span becomes the PARENT, so request -> job -> provider.* reads as
54
+ * one trace; whatever span was active (a worker cycle, the BullMQ runtime) is
55
+ * kept as a LINK so the scheduling side stays navigable. Every non-span context
56
+ * value (baggage, suppression flags) is preserved. Null for a missing or
57
+ * invalid traceparent: the caller then starts an ordinary child of the active
58
+ * context.
59
+ */
60
+ export function remoteParentContext(traceparent) {
61
+ const remote = parseTraceparent(traceparent);
62
+ if (!remote)
63
+ return null;
64
+ const active = context.active();
65
+ const current = trace.getSpanContext(active);
66
+ return {
67
+ context: trace.setSpanContext(active, remote),
68
+ links: current && isSpanContextValid(current)
69
+ ? [{ context: current, attributes: { "oxygen.link.kind": "worker.dispatch" } }]
70
+ : [],
71
+ };
72
+ }
73
+ /**
74
+ * Links the active span to a stored enqueue traceparent. For work that is
75
+ * already inside its execution span (a BullMQ `bullmq.execute`) and only learns
76
+ * the enqueuing request once it has loaded its own row: a link joins the two
77
+ * traces without a second per-job span. Returns whether a link was added.
78
+ */
79
+ export function linkActiveSpanToTraceparent(traceparent) {
80
+ const remote = parseTraceparent(traceparent);
81
+ const span = trace.getActiveSpan();
82
+ if (!remote || !span || !isSpanContextValid(span.spanContext()))
83
+ return false;
84
+ if (span.spanContext().traceId === remote.traceId)
85
+ return false;
86
+ span.addLink({ context: remote, attributes: { "oxygen.link.kind": "enqueue" } });
87
+ return true;
88
+ }
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1010.644";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1010.721";
@@ -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.1010.644";
2
+ export const OXYGEN_BUILD_VERSION = "1.1010.721";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1010.644",
3
+ "version": "1.1010.721",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,64 +0,0 @@
1
- /**
2
- * PURE warm-up readiness rule: what daily cold volume a sending mailbox can
3
- * actually carry, given its warm-up age rather than a provider's opinion of it.
4
- *
5
- * WHY it lives in @oxygen/shared and not next to the other email-health rules in
6
- * @oxygen/integrations: the tenant rollup computes this per mailbox, and
7
- * @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
8
- * on tenant-db). Duplicating the thresholds so each package could own a copy is
9
- * exactly how two surfaces start recommending different numbers, so the rule is
10
- * defined once here and re-exported from
11
- * packages/integrations/src/email-health/health-state.ts, which stays the
12
- * email-health surface every caller reads.
13
- *
14
- * DIRECTIONAL, like everything else in the deliverability cluster: it produces a
15
- * recommendation and a launch-preview warning. It never clamps a cap, never
16
- * pauses a mailbox, and never changes what a sequence sends.
17
- */
18
- /** Below this warm-up day a mailbox should carry only the starter cold volume. */
19
- export declare const WARMUP_EARLY_DAY_LIMIT = 14;
20
- /** Below this warm-up day a mailbox should stay at the reduced cold volume. */
21
- export declare const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
22
- /** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
23
- export declare const WARMUP_EARLY_DAILY_CAP = 5;
24
- /** Recommended cold sends/day between day 14 and day 28. */
25
- export declare const WARMUP_ESTABLISHED_DAILY_CAP = 10;
26
- /** A warm-up health score under this is treated as "not ready for volume". */
27
- export declare const WARMUP_HEALTH_SCORE_FLOOR = 60;
28
- /** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
29
- export declare const HARD_BOUNCE_RATE_CEILING = 0.03;
30
- /** Below this send volume a bounce rate is noise, not a signal. */
31
- export declare const HARD_BOUNCE_RATE_MIN_SENDS = 20;
32
- /**
33
- * The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
34
- * or null when nothing in the evidence argues for holding volume back.
35
- *
36
- * Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
37
- * clock-free and identical on every surface.
38
- *
39
- * - warm-up day < 14 -> 5/day
40
- * - mailbox younger than 28 days, with either no active
41
- * warm-up or no warm-up day reported at all -> 5/day
42
- * - warm-up day < 28 -> 10/day
43
- * - warm-up health score < 60 -> at most 5/day
44
- * - otherwise -> null (no advice)
45
- *
46
- * The second rule is the one that matters for a freshly provisioned fleet: a
47
- * mailbox whose warm-up never started (state "unknown") reports no day at all,
48
- * and a rule keyed only on the day number would have said nothing about the exact
49
- * mailboxes most likely to get blocked.
50
- */
51
- export declare function recommendedDailyCapForWarmup(input: {
52
- warmupState?: string | null;
53
- warmupDay?: number | null;
54
- warmupHealthScore?: number | null;
55
- mailboxAgeDays?: number | null;
56
- }): number | null;
57
- /**
58
- * True when a hard-bounce count is high enough, over enough sends, to be a real
59
- * reputation problem rather than list noise.
60
- */
61
- export declare function hardBounceRateIsHigh(input: {
62
- hardBounces: number;
63
- coldSends: number;
64
- }): boolean;
@@ -1,90 +0,0 @@
1
- /**
2
- * PURE warm-up readiness rule: what daily cold volume a sending mailbox can
3
- * actually carry, given its warm-up age rather than a provider's opinion of it.
4
- *
5
- * WHY it lives in @oxygen/shared and not next to the other email-health rules in
6
- * @oxygen/integrations: the tenant rollup computes this per mailbox, and
7
- * @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
8
- * on tenant-db). Duplicating the thresholds so each package could own a copy is
9
- * exactly how two surfaces start recommending different numbers, so the rule is
10
- * defined once here and re-exported from
11
- * packages/integrations/src/email-health/health-state.ts, which stays the
12
- * email-health surface every caller reads.
13
- *
14
- * DIRECTIONAL, like everything else in the deliverability cluster: it produces a
15
- * recommendation and a launch-preview warning. It never clamps a cap, never
16
- * pauses a mailbox, and never changes what a sequence sends.
17
- */
18
- /** Below this warm-up day a mailbox should carry only the starter cold volume. */
19
- export const WARMUP_EARLY_DAY_LIMIT = 14;
20
- /** Below this warm-up day a mailbox should stay at the reduced cold volume. */
21
- export const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
22
- /** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
23
- export const WARMUP_EARLY_DAILY_CAP = 5;
24
- /** Recommended cold sends/day between day 14 and day 28. */
25
- export const WARMUP_ESTABLISHED_DAILY_CAP = 10;
26
- /** A warm-up health score under this is treated as "not ready for volume". */
27
- export const WARMUP_HEALTH_SCORE_FLOOR = 60;
28
- /** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
29
- export const HARD_BOUNCE_RATE_CEILING = 0.03;
30
- /** Below this send volume a bounce rate is noise, not a signal. */
31
- export const HARD_BOUNCE_RATE_MIN_SENDS = 20;
32
- /** Warm-up states in which a vendor is actively conditioning the mailbox. */
33
- const ACTIVE_WARMUP_STATES = new Set(["warming", "active"]);
34
- function finiteOrNull(value) {
35
- return typeof value === "number" && Number.isFinite(value) ? value : null;
36
- }
37
- /**
38
- * The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
39
- * or null when nothing in the evidence argues for holding volume back.
40
- *
41
- * Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
42
- * clock-free and identical on every surface.
43
- *
44
- * - warm-up day < 14 -> 5/day
45
- * - mailbox younger than 28 days, with either no active
46
- * warm-up or no warm-up day reported at all -> 5/day
47
- * - warm-up day < 28 -> 10/day
48
- * - warm-up health score < 60 -> at most 5/day
49
- * - otherwise -> null (no advice)
50
- *
51
- * The second rule is the one that matters for a freshly provisioned fleet: a
52
- * mailbox whose warm-up never started (state "unknown") reports no day at all,
53
- * and a rule keyed only on the day number would have said nothing about the exact
54
- * mailboxes most likely to get blocked.
55
- */
56
- export function recommendedDailyCapForWarmup(input) {
57
- const day = finiteOrNull(input.warmupDay);
58
- const ageDays = finiteOrNull(input.mailboxAgeDays);
59
- const healthScore = finiteOrNull(input.warmupHealthScore);
60
- const warmingNow = ACTIVE_WARMUP_STATES.has((input.warmupState ?? "").trim().toLowerCase());
61
- let cap = null;
62
- if (day !== null && day < WARMUP_EARLY_DAY_LIMIT) {
63
- cap = WARMUP_EARLY_DAILY_CAP;
64
- }
65
- else if (ageDays !== null &&
66
- ageDays < WARMUP_ESTABLISHED_DAY_LIMIT &&
67
- (day === null || !warmingNow)) {
68
- // A rail that reports "warming" but no day number proves nothing about how
69
- // far the ramp got, so age still governs. Gating this on !warmingNow alone
70
- // left exactly that mailbox — enrolled, young, no telemetry — with no advice.
71
- cap = WARMUP_EARLY_DAILY_CAP;
72
- }
73
- else if (day !== null && day < WARMUP_ESTABLISHED_DAY_LIMIT) {
74
- cap = WARMUP_ESTABLISHED_DAILY_CAP;
75
- }
76
- if (healthScore !== null && healthScore < WARMUP_HEALTH_SCORE_FLOOR) {
77
- cap = cap === null ? WARMUP_EARLY_DAILY_CAP : Math.min(cap, WARMUP_EARLY_DAILY_CAP);
78
- }
79
- return cap;
80
- }
81
- /**
82
- * True when a hard-bounce count is high enough, over enough sends, to be a real
83
- * reputation problem rather than list noise.
84
- */
85
- export function hardBounceRateIsHigh(input) {
86
- if (!Number.isFinite(input.coldSends) || input.coldSends < HARD_BOUNCE_RATE_MIN_SENDS) {
87
- return false;
88
- }
89
- return input.hardBounces / input.coldSends > HARD_BOUNCE_RATE_CEILING;
90
- }