@oxygen-agent/cli 1.1003.12 → 1.1010.1

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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/column-decision-options.d.ts +20 -0
  3. package/dist/column-decision-options.js +54 -0
  4. package/dist/command-manifest.js +15 -2
  5. package/dist/functions-commands.js +11 -11
  6. package/dist/index.js +1222 -159
  7. package/dist/search-ai-filter-notice.d.ts +17 -0
  8. package/dist/search-ai-filter-notice.js +38 -0
  9. package/dist/skills.js +34 -10
  10. package/dist/util.d.ts +9 -0
  11. package/dist/util.js +14 -0
  12. package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
  13. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
  14. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
  15. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
  16. package/node_modules/@oxygen/shared/dist/billing.d.ts +21 -0
  17. package/node_modules/@oxygen/shared/dist/billing.js +45 -0
  18. package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
  19. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +10 -0
  20. package/node_modules/@oxygen/shared/dist/capability-discovery.js +223 -13
  21. package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
  22. package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
  23. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  24. package/node_modules/@oxygen/shared/dist/column-autofill.js +5 -23
  25. package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
  26. package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
  27. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +9 -4
  28. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +11 -8
  29. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +4 -4
  30. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +4 -4
  31. package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
  32. package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
  33. package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
  34. package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
  35. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +6 -2
  36. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +13 -23
  37. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
  39. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
  40. package/node_modules/@oxygen/shared/dist/index.js +2 -0
  41. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
  42. package/node_modules/@oxygen/shared/dist/langfuse.js +407 -14
  43. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +1 -0
  44. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +2 -0
  45. package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
  46. package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
  47. package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
  48. package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
  49. package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
  50. package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
  53. package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
  54. package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
  55. package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
  56. package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
  57. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
  58. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
  59. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
  60. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
  61. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +29 -4
  62. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +189 -36
  63. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +21 -2
  64. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +21 -1
  65. package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
  66. package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
  67. package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
  68. package/node_modules/@oxygen/shared/dist/sequences.js +5 -1
  69. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
  70. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
  71. package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
  72. package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
  73. package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
  74. package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
  75. package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
  76. package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
  77. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
  78. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
  79. package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
  80. package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
  81. package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
  82. package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
  83. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
  84. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
  85. package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
  86. package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
  87. package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
  88. package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
  91. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry.js +19 -1
  93. package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
  94. package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
  95. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
  96. package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
  97. package/node_modules/@oxygen/shared/dist/version.js +8 -1
  98. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
  99. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
  100. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
  101. package/node_modules/@oxygen/shared/package.json +9 -0
  102. package/package.json +2 -1
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Which Stripe subscriptions are NOT an OXYGEN plan.
3
+ *
4
+ * OXYGEN runs three recurring rails on the same Stripe customer, and only one of
5
+ * them is a plan: the PLAN subscription (born from a Checkout Session),
6
+ * EMAIL INFRASTRUCTURE (`infra-subscription.ts`) and SENDING SEATS
7
+ * (`seat-subscription.ts`). The two rails are separate on purpose — a workspace
8
+ * with no plan at all must be able to hold managed inboxes and buy a sending
9
+ * seat — so a rail event is not a smaller version of a plan event, it is a
10
+ * different subject entirely, and plan-shaped handling of one is always wrong.
11
+ *
12
+ * THE WITNESS IS `metadata.oxygen_kind`. Both rails stamp it at
13
+ * `subscriptions.create` (the only two such call sites in the repo), and a plan
14
+ * subscription never carries it: a plan is born from a Checkout Session whose
15
+ * `subscription_data.metadata` carries organization_id/clerk_org_id and no
16
+ * `oxygen_kind`. (The third `oxygen_kind` in the codebase, `credit_topup`, rides
17
+ * a one-off `mode: "payment"` Checkout Session and never produces a
18
+ * subscription at all.) So the key's PRESENCE is a positive witness of "not a
19
+ * plan".
20
+ *
21
+ * PRESENCE, NOT A VALUE LIST, deliberately. Every previous gate matched the one
22
+ * value `"email_infra"`, which is why the seat rail — shipped later — walked
23
+ * straight into the plan handlers: adding a rail silently opted it INTO plan
24
+ * treatment, the wrong default for a safety gate. Testing presence means the
25
+ * next rail needs no edit here. This is the same reasoning `orphan-reconcile.ts`
26
+ * measured in production on 2026-09-06, where all 24 of the 24 daily
27
+ * `billing.subscription_health.unknown_plan_price` error lines were the same 4
28
+ * live sending-seat subscriptions x 6 cron runs.
29
+ *
30
+ * DELIBERATELY NARROW. This answers "did the subscription declare itself a
31
+ * non-plan rail", never "does it carry a recognised plan price". The catalog
32
+ * question stays with `tierFromPriceId` / `recognizedStripePlanItems`, and
33
+ * callers that need both keep asking both — `orphan-reconcile.ts` reclassifies
34
+ * only the zero-recognised-item half for exactly that reason.
35
+ */
36
+ /** The declared non-plan rail (`email_infra`, `sending_seats`, …), or null for a plan. */
37
+ export function nonPlanSubscriptionKind(metadata) {
38
+ const kind = metadata?.oxygen_kind;
39
+ return typeof kind === "string" && kind.length > 0 ? kind : null;
40
+ }
41
+ /** True when the subscription declared itself one of OXYGEN's non-plan rails. */
42
+ export function isNonPlanSubscriptionKind(metadata) {
43
+ return nonPlanSubscriptionKind(metadata) !== null;
44
+ }
@@ -1,4 +1,7 @@
1
1
  export declare const MAX_TABLE_ACTION_RUN_ROWS = 500000;
2
+ /** Only runs whose resolved steps are all provider-free formulas may use these ceilings. */
3
+ export declare const MAX_FORMULA_ACTION_RUN_ROWS = 1000000;
4
+ export declare const MAX_FORMULA_ACTION_RUN_ITEMS = 5000000;
2
5
  export declare const MAX_WORKSPACE_ROW_DELETE_ROWS = 50000;
3
6
  /**
4
7
  * Platform ceiling for a table action run's `max_concurrency` (T-27).
@@ -1,6 +1,9 @@
1
1
  // Shared request/action ceilings used by tenant-db enforcement and every
2
2
  // surface that resolves a symbolic row selection before invoking it.
3
3
  export const MAX_TABLE_ACTION_RUN_ROWS = 500_000;
4
+ /** Only runs whose resolved steps are all provider-free formulas may use these ceilings. */
5
+ export const MAX_FORMULA_ACTION_RUN_ROWS = 1_000_000;
6
+ export const MAX_FORMULA_ACTION_RUN_ITEMS = 5_000_000;
4
7
  export const MAX_WORKSPACE_ROW_DELETE_ROWS = 50_000;
5
8
  /**
6
9
  * Platform ceiling for a table action run's `max_concurrency` (T-27).
@@ -0,0 +1,94 @@
1
+ import { DiagLogLevel, type DiagLogger } from "@opentelemetry/api";
2
+ /**
3
+ * Makes a failing OTLP trace/metric export visible in LOGS.
4
+ *
5
+ * The SDK exporters report a failed export only through their result callback,
6
+ * and the processors hand that to `diag`, which has no logger unless one is
7
+ * installed — so a broken collector path (SigNoz over the private network) and a
8
+ * healthy one look identical from the sending process. This wraps an exporter so
9
+ * a failed export becomes one throttled `telemetry.export_failed` line and the
10
+ * first success afterwards one `telemetry.export_recovered` line, both through
11
+ * `log()` and therefore through whichever log sinks the process has.
12
+ *
13
+ * No recursion: `log()` records go to the LOG sinks, never to a trace or metric
14
+ * exporter, so a failing trace/metric lane cannot feed itself.
15
+ *
16
+ * Never throws, never changes what the SDK sees: the inner result is passed to
17
+ * the SDK callback untouched, every other exporter method is forwarded, and a
18
+ * synchronous throw from the inner exporter is recorded and rethrown exactly as
19
+ * it would have been without the wrapper.
20
+ *
21
+ * Nothing from the error's text is logged. OTLP errors carry receiver response
22
+ * bodies (`OTLPExporterError.data`) and transport errors can carry URLs; the
23
+ * failure is reduced to an HTTP status and a fixed code (see
24
+ * `classifyTelemetryExportError`), and the endpoint to origin + path.
25
+ *
26
+ * Structural types, not `@opentelemetry/core`/`sdk-metrics` imports: this
27
+ * package depends on neither, and the shapes used here (`{ code, error }`, the
28
+ * `ResourceMetrics` tree) are the SDK's stable public contract.
29
+ */
30
+ export type TelemetryExportSignal = "traces" | "metrics";
31
+ export type TelemetryExportSurface = "web" | "worker";
32
+ type ExportResultLike = {
33
+ code: number;
34
+ error?: Error | undefined;
35
+ };
36
+ type ObservableExporter = {
37
+ export(items: unknown, resultCallback: (result: ExportResultLike) => void): void;
38
+ shutdown(): Promise<void>;
39
+ };
40
+ export type TelemetryExportObserverOptions = {
41
+ signal: TelemetryExportSignal;
42
+ /** The exporter's full URL; only origin + path is ever logged. */
43
+ endpoint: string;
44
+ surface: TelemetryExportSurface;
45
+ /** At most one `telemetry.export_failed` line per window. Default 60 s. */
46
+ windowMs?: number | undefined;
47
+ now?: (() => number) | undefined;
48
+ };
49
+ /** Origin + path only: never query strings, fragments or URL credentials. */
50
+ export declare function telemetryEndpointForLog(endpoint: string): string;
51
+ /**
52
+ * Reduces an OTLP export error to `{ status?, error_code }`. `status` is the
53
+ * HTTP status an `OTLPExporterError` carries in `code` for a non-retryable
54
+ * response; a retryable one (429/502/503/504) that exhausted its retries carries
55
+ * none, hence `http_retryable`. The message is matched against the exporter's
56
+ * own fixed strings and never copied.
57
+ */
58
+ export declare function classifyTelemetryExportError(error: unknown): {
59
+ status?: number;
60
+ error_code: string;
61
+ };
62
+ export declare function observeTelemetryExporter<E extends ObservableExporter>(inner: E, options: TelemetryExportObserverOptions): E;
63
+ export type TelemetryDropDiagLoggerOptions = {
64
+ surface: TelemetryExportSurface;
65
+ /** An existing diagnostics logger that still receives every call unchanged. */
66
+ next?: DiagLogger | undefined;
67
+ windowMs?: number | undefined;
68
+ now?: (() => number) | undefined;
69
+ };
70
+ /**
71
+ * A `diag` logger that recognises exactly one message: the BatchSpanProcessor's
72
+ * `Dropped N spans because maxQueueSize reached`, its only report of queue-full
73
+ * drops. Those become a throttled `telemetry.export_dropped` line carrying the
74
+ * count; every other diag call is ignored, because SDK diagnostics can carry
75
+ * receiver responses and exported payloads (the OTLP delegate logs the items it
76
+ * is about to send at debug). Nothing but the parsed count leaves this function.
77
+ *
78
+ * The count cannot name which BatchSpanProcessor dropped: diag is process-wide,
79
+ * so when a process also runs the Axiom processor, a drop line covers both.
80
+ *
81
+ * `next` keeps an operator-enabled diagnostics logger working: it receives every
82
+ * call as before, and this logger only adds the drop count.
83
+ */
84
+ export declare function createTelemetryDropDiagLogger(options: TelemetryDropDiagLoggerOptions): DiagLogger;
85
+ /**
86
+ * Installs `createTelemetryDropDiagLogger` as the global diag logger, once per
87
+ * process. Returns false when it was already installed. Callers install it only
88
+ * when an OTLP collector endpoint is configured, so a process without one keeps
89
+ * its diag configuration exactly as it was.
90
+ */
91
+ export declare function installTelemetryDropDiagLogger(options: TelemetryDropDiagLoggerOptions & {
92
+ logLevel?: DiagLogLevel | undefined;
93
+ }): boolean;
94
+ export {};
@@ -0,0 +1,298 @@
1
+ import { diag, DiagLogLevel } from "@opentelemetry/api";
2
+ import { log } from "./log.js";
3
+ // @opentelemetry/core ExportResultCode.SUCCESS / FAILED.
4
+ const EXPORT_RESULT_SUCCESS = 0;
5
+ const DEFAULT_WINDOW_MS = 60_000;
6
+ // Methods the SDK may call on an exporter besides `export`. Forwarded only when
7
+ // the inner exporter has them, so a reader that probes for an optional method
8
+ // (PeriodicExportingMetricReader reads selectAggregationTemporality) sees
9
+ // exactly what it would have seen on the unwrapped exporter.
10
+ const FORWARDED_METHODS = [
11
+ "forceFlush",
12
+ "selectAggregationTemporality",
13
+ "selectAggregation",
14
+ ];
15
+ /** Origin + path only: never query strings, fragments or URL credentials. */
16
+ export function telemetryEndpointForLog(endpoint) {
17
+ try {
18
+ const url = new URL(endpoint);
19
+ return `${url.origin}${url.pathname}`;
20
+ }
21
+ catch {
22
+ return "invalid_url";
23
+ }
24
+ }
25
+ const SYSTEM_ERROR_CODES = {
26
+ ETIMEDOUT: "timeout",
27
+ ESOCKETTIMEDOUT: "timeout",
28
+ ECONNREFUSED: "connection_refused",
29
+ ECONNRESET: "connection_reset",
30
+ EPIPE: "connection_reset",
31
+ ENOTFOUND: "dns_failed",
32
+ EAI_AGAIN: "dns_failed",
33
+ EHOSTUNREACH: "host_unreachable",
34
+ ENETUNREACH: "host_unreachable",
35
+ };
36
+ /**
37
+ * Reduces an OTLP export error to `{ status?, error_code }`. `status` is the
38
+ * HTTP status an `OTLPExporterError` carries in `code` for a non-retryable
39
+ * response; a retryable one (429/502/503/504) that exhausted its retries carries
40
+ * none, hence `http_retryable`. The message is matched against the exporter's
41
+ * own fixed strings and never copied.
42
+ */
43
+ export function classifyTelemetryExportError(error) {
44
+ if (!error || typeof error !== "object")
45
+ return { error_code: "unknown" };
46
+ const candidate = error;
47
+ const code = candidate.code;
48
+ if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
49
+ return { status: code, error_code: code >= 500 ? "http_5xx" : `http_${code}` };
50
+ }
51
+ const systemCode = typeof code === "string"
52
+ ? code
53
+ : typeof candidate.cause?.code === "string"
54
+ ? candidate.cause.code
55
+ : undefined;
56
+ if (systemCode)
57
+ return { error_code: SYSTEM_ERROR_CODES[systemCode] ?? "transport_failed" };
58
+ const message = typeof candidate.message === "string" ? candidate.message : "";
59
+ if (candidate.name === "TimeoutError" || /timed out|timeout/i.test(message))
60
+ return { error_code: "timeout" };
61
+ if (/concurren(?:t|cy) (?:export )?limit/i.test(message))
62
+ return { error_code: "concurrency_limit" };
63
+ if (/retryable status/i.test(message))
64
+ return { error_code: "http_retryable" };
65
+ if (/exceeded size limit/i.test(message))
66
+ return { error_code: "response_too_large" };
67
+ return { error_code: "transport_failed" };
68
+ }
69
+ function countItems(signal, items) {
70
+ if (signal === "traces")
71
+ return Array.isArray(items) ? items.length : 0;
72
+ // Metric data points (= live series under cumulative temporality).
73
+ let points = 0;
74
+ const scopeMetrics = items?.scopeMetrics;
75
+ if (!Array.isArray(scopeMetrics))
76
+ return 0;
77
+ for (const scope of scopeMetrics) {
78
+ const metrics = scope?.metrics;
79
+ if (!Array.isArray(metrics))
80
+ continue;
81
+ for (const metric of metrics) {
82
+ const dataPoints = metric?.dataPoints;
83
+ if (Array.isArray(dataPoints))
84
+ points += dataPoints.length;
85
+ }
86
+ }
87
+ return points;
88
+ }
89
+ export function observeTelemetryExporter(inner, options) {
90
+ const endpoint = telemetryEndpointForLog(options.endpoint);
91
+ const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
92
+ const now = options.now ?? Date.now;
93
+ const { signal, surface } = options;
94
+ let consecutiveFailures = 0;
95
+ let outageStartedAt = 0;
96
+ let outageDropped = 0;
97
+ // Only an outage that produced an export_failed line gets a recovery line, so
98
+ // a flapping endpoint cannot bypass the failure throttle through recoveries.
99
+ let outageReported = false;
100
+ let lastFailureLogAt = Number.NEGATIVE_INFINITY;
101
+ // Accumulated since the last emitted export_failed line, reported on the next.
102
+ let suppressedFailures = 0;
103
+ let pendingDropped = 0;
104
+ const recordFailure = (error, dropped) => {
105
+ try {
106
+ const at = now();
107
+ if (consecutiveFailures === 0) {
108
+ outageStartedAt = at;
109
+ outageDropped = 0;
110
+ outageReported = false;
111
+ }
112
+ consecutiveFailures += 1;
113
+ outageDropped += dropped;
114
+ pendingDropped += dropped;
115
+ if (at - lastFailureLogAt < windowMs) {
116
+ suppressedFailures += 1;
117
+ return;
118
+ }
119
+ lastFailureLogAt = at;
120
+ outageReported = true;
121
+ const { status, error_code } = classifyTelemetryExportError(error);
122
+ log("warn", "telemetry.export_failed", {
123
+ signal,
124
+ endpoint,
125
+ ...(status === undefined ? {} : { status }),
126
+ error_code,
127
+ dropped: pendingDropped,
128
+ consecutive_failures: consecutiveFailures,
129
+ suppressed_failures: suppressedFailures,
130
+ surface,
131
+ });
132
+ pendingDropped = 0;
133
+ suppressedFailures = 0;
134
+ }
135
+ catch {
136
+ // Observation must never change export semantics.
137
+ }
138
+ };
139
+ const recordSuccess = () => {
140
+ try {
141
+ if (consecutiveFailures === 0)
142
+ return;
143
+ if (outageReported) {
144
+ log("info", "telemetry.export_recovered", {
145
+ signal,
146
+ endpoint,
147
+ failures: consecutiveFailures,
148
+ dropped: outageDropped,
149
+ outage_ms: Math.max(0, now() - outageStartedAt),
150
+ surface,
151
+ });
152
+ // Every suppressed failure since the last line belonged to this outage,
153
+ // and the recovery line has just accounted for it.
154
+ pendingDropped = 0;
155
+ suppressedFailures = 0;
156
+ }
157
+ consecutiveFailures = 0;
158
+ }
159
+ catch {
160
+ // Observation must never change export semantics.
161
+ }
162
+ };
163
+ const observed = {
164
+ export(items, resultCallback) {
165
+ let dropped = 0;
166
+ try {
167
+ dropped = countItems(signal, items);
168
+ }
169
+ catch {
170
+ // Counting is best-effort; never let it block the export itself.
171
+ }
172
+ try {
173
+ inner.export(items, (result) => {
174
+ if (result?.code === EXPORT_RESULT_SUCCESS)
175
+ recordSuccess();
176
+ else
177
+ recordFailure(result?.error, dropped);
178
+ resultCallback(result);
179
+ });
180
+ }
181
+ catch (error) {
182
+ recordFailure(error, dropped);
183
+ throw error;
184
+ }
185
+ },
186
+ shutdown() {
187
+ return inner.shutdown();
188
+ },
189
+ };
190
+ for (const method of FORWARDED_METHODS) {
191
+ const fn = inner[method];
192
+ if (typeof fn === "function")
193
+ observed[method] = fn.bind(inner);
194
+ }
195
+ return observed;
196
+ }
197
+ const DROPPED_SPANS_MESSAGE = /^Dropped (\d+) spans because maxQueueSize reached$/;
198
+ /**
199
+ * A `diag` logger that recognises exactly one message: the BatchSpanProcessor's
200
+ * `Dropped N spans because maxQueueSize reached`, its only report of queue-full
201
+ * drops. Those become a throttled `telemetry.export_dropped` line carrying the
202
+ * count; every other diag call is ignored, because SDK diagnostics can carry
203
+ * receiver responses and exported payloads (the OTLP delegate logs the items it
204
+ * is about to send at debug). Nothing but the parsed count leaves this function.
205
+ *
206
+ * The count cannot name which BatchSpanProcessor dropped: diag is process-wide,
207
+ * so when a process also runs the Axiom processor, a drop line covers both.
208
+ *
209
+ * `next` keeps an operator-enabled diagnostics logger working: it receives every
210
+ * call as before, and this logger only adds the drop count.
211
+ */
212
+ export function createTelemetryDropDiagLogger(options) {
213
+ const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
214
+ const now = options.now ?? Date.now;
215
+ const { next, surface } = options;
216
+ let pending = 0;
217
+ let lastEmitAt = Number.NEGATIVE_INFINITY;
218
+ let timer = null;
219
+ let emitting = false;
220
+ const emit = () => {
221
+ if (pending === 0 || emitting)
222
+ return;
223
+ emitting = true;
224
+ try {
225
+ lastEmitAt = now();
226
+ const dropped = pending;
227
+ pending = 0;
228
+ log("warn", "telemetry.export_dropped", { signal: "traces", dropped, reason: "queue_full", surface });
229
+ }
230
+ finally {
231
+ emitting = false;
232
+ }
233
+ };
234
+ const record = (dropped) => {
235
+ pending += dropped;
236
+ const remaining = windowMs - (now() - lastEmitAt);
237
+ if (remaining <= 0) {
238
+ emit();
239
+ return;
240
+ }
241
+ // The BatchSpanProcessor reports a drop run only once it ends, so a
242
+ // suppressed count could otherwise wait forever for another drop to carry it.
243
+ if (timer)
244
+ return;
245
+ timer = setTimeout(() => {
246
+ timer = null;
247
+ try {
248
+ emit();
249
+ }
250
+ catch {
251
+ // Telemetry about telemetry never throws.
252
+ }
253
+ }, remaining);
254
+ timer.unref?.();
255
+ };
256
+ const forward = (level) => (message, ...args) => {
257
+ if (level === "warn") {
258
+ try {
259
+ const match = typeof message === "string" ? DROPPED_SPANS_MESSAGE.exec(message) : null;
260
+ const dropped = match ? Number(match[1]) : 0;
261
+ if (Number.isSafeInteger(dropped) && dropped > 0)
262
+ record(dropped);
263
+ }
264
+ catch {
265
+ // Telemetry about telemetry never throws.
266
+ }
267
+ }
268
+ next?.[level](message, ...args);
269
+ };
270
+ return {
271
+ error: forward("error"),
272
+ warn: forward("warn"),
273
+ info: forward("info"),
274
+ debug: forward("debug"),
275
+ verbose: forward("verbose"),
276
+ };
277
+ }
278
+ // globalThis, not a module-local: Next.js bundles can load this module more
279
+ // than once per process, and a second `diag.setLogger` would replace the first
280
+ // (and, without suppressOverrideMessage, log an override warning).
281
+ const DIAG_BRIDGE_SLOT = Symbol.for("oxygen.telemetry.diag_drop_bridge");
282
+ /**
283
+ * Installs `createTelemetryDropDiagLogger` as the global diag logger, once per
284
+ * process. Returns false when it was already installed. Callers install it only
285
+ * when an OTLP collector endpoint is configured, so a process without one keeps
286
+ * its diag configuration exactly as it was.
287
+ */
288
+ export function installTelemetryDropDiagLogger(options) {
289
+ const carrier = globalThis;
290
+ if (carrier[DIAG_BRIDGE_SLOT])
291
+ return false;
292
+ carrier[DIAG_BRIDGE_SLOT] = true;
293
+ diag.setLogger(createTelemetryDropDiagLogger(options), {
294
+ logLevel: options.logLevel ?? DiagLogLevel.WARN,
295
+ suppressOverrideMessage: true,
296
+ });
297
+ return true;
298
+ }
@@ -3,6 +3,17 @@ export type WithTelemetrySpanOptions = {
3
3
  isTransient?: (error: unknown) => boolean;
4
4
  };
5
5
  export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
6
+ /**
7
+ * The active global span's trace id: the join key into SigNoz/Axiom traces.
8
+ * Null when no valid span is active (a BullMQ job outside any span, a test).
9
+ */
10
+ export declare function activeTelemetryTraceId(): string | null;
11
+ /**
12
+ * Run `fn` inside a new span only when no span is active, so work that already
13
+ * sits under a request/cycle span keeps that trace and uncorrelated background
14
+ * work (BullMQ jobs) still gets one trace id to cross-link from Langfuse.
15
+ */
16
+ export declare function withTelemetrySpanIfNone<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>): Promise<T>;
6
17
  export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
7
18
  export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
8
19
  export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
@@ -1,4 +1,4 @@
1
- import { SpanStatusCode, metrics, trace, } from "@opentelemetry/api";
1
+ import { SpanStatusCode, isSpanContextValid, metrics, trace, } from "@opentelemetry/api";
2
2
  import { deployEnvTelemetryEnvironment, deployPlatformAttribute } from "./deploy-env.js";
3
3
  import { errorId } from "./log.js";
4
4
  import { normalizeTelemetryAttributes } from "./redaction.js";
@@ -39,6 +39,24 @@ export async function withTelemetrySpan(tracerName, name, attributes, fn, option
39
39
  }
40
40
  });
41
41
  }
42
+ /**
43
+ * The active global span's trace id: the join key into SigNoz/Axiom traces.
44
+ * Null when no valid span is active (a BullMQ job outside any span, a test).
45
+ */
46
+ export function activeTelemetryTraceId() {
47
+ const spanContext = trace.getActiveSpan()?.spanContext();
48
+ return spanContext && isSpanContextValid(spanContext) ? spanContext.traceId : null;
49
+ }
50
+ /**
51
+ * Run `fn` inside a new span only when no span is active, so work that already
52
+ * sits under a request/cycle span keeps that trace and uncorrelated background
53
+ * work (BullMQ jobs) still gets one trace id to cross-link from Langfuse.
54
+ */
55
+ export async function withTelemetrySpanIfNone(tracerName, name, attributes, fn) {
56
+ if (activeTelemetryTraceId())
57
+ return await fn();
58
+ return await withTelemetrySpan(tracerName, name, attributes, fn);
59
+ }
42
60
  export function setActiveTelemetryAttributes(attributes) {
43
61
  const span = trace.getActiveSpan();
44
62
  if (!span)
@@ -1,5 +1,20 @@
1
1
  /** UGC is a Publishing capability. These are projections, never a second Posts store. */
2
2
  export type UgcApprovalMode = "creator" | "delegated";
3
+ /** Per calendar week or month, or `total`: N posts over the program's lifetime. */
4
+ export type UgcPostingTargetPeriod = "week" | "month" | "total";
5
+ export type UgcPostingTarget = {
6
+ count: number;
7
+ period: UgcPostingTargetPeriod;
8
+ };
9
+ export declare const UGC_POSTING_TARGET_MAX = 100;
10
+ export declare const UGC_POSTING_TARGET_PERIODS: readonly ["week", "month", "total"];
11
+ /** The operator-written creator brief is short by design (Philipp, 2026-09-23). */
12
+ export declare const UGC_PROGRAM_BRIEF_MAX_CHARS = 1200;
13
+ /** The program's target as its API shape, or null when none is set. */
14
+ export declare function ugcPostingTarget(program: {
15
+ postingTargetCount?: number | null | undefined;
16
+ postingTargetPeriod?: UgcPostingTargetPeriod | null | undefined;
17
+ }): UgcPostingTarget | null;
3
18
  export type UgcProgram = {
4
19
  id: string;
5
20
  name: string;
@@ -10,20 +25,17 @@ export type UgcProgram = {
10
25
  peerEngagementEnabled?: boolean;
11
26
  peerEngagementVersion?: number;
12
27
  peerEngagementEffectiveAt?: Date | null;
28
+ /** The product's icon, found from its site at create; null for older programs. */
29
+ iconUrl?: string | null;
30
+ /** Short operator-written creator brief; drafts are grounded in it. */
31
+ brief?: string | null;
32
+ /** Posts each creator is expected to publish per period; both null when unset. */
33
+ postingTargetCount?: number | null;
34
+ postingTargetPeriod?: UgcPostingTargetPeriod | null;
13
35
  status: "active" | "archived";
14
36
  createdAt: Date;
15
37
  updatedAt: Date;
16
38
  };
17
- export type UgcBrief = {
18
- id: string;
19
- programId: string;
20
- title: string;
21
- prompt: string;
22
- questions: string[];
23
- cycle: string | null;
24
- createdAt: Date;
25
- updatedAt: Date;
26
- };
27
39
  export type UgcPostProjection = {
28
40
  /** Public author identity from the exact participation, never a private profile corpus. */
29
41
  authorLinkedinUrl?: string;
@@ -84,7 +96,6 @@ export type UgcPostLink = {
84
96
  masterOrgId: string;
85
97
  brandApprovalRequired: boolean;
86
98
  sourcePostId: string;
87
- briefId: string | null;
88
99
  attribution: Record<string, unknown>;
89
100
  projection: UgcPostProjection;
90
101
  sourceRevision: string;
@@ -1,2 +1,12 @@
1
+ export const UGC_POSTING_TARGET_MAX = 100;
2
+ export const UGC_POSTING_TARGET_PERIODS = ["week", "month", "total"];
3
+ /** The operator-written creator brief is short by design (Philipp, 2026-09-23). */
4
+ export const UGC_PROGRAM_BRIEF_MAX_CHARS = 1200;
5
+ /** The program's target as its API shape, or null when none is set. */
6
+ export function ugcPostingTarget(program) {
7
+ return program.postingTargetCount && program.postingTargetPeriod
8
+ ? { count: program.postingTargetCount, period: program.postingTargetPeriod }
9
+ : null;
10
+ }
1
11
  export const UGC_MONTHLY_CREATOR_CREDITS = 10_000;
2
12
  export const UGC_SPONSORED_LINKEDIN_SEATS = 1;
@@ -0,0 +1 @@
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1010.1";
@@ -0,0 +1,2 @@
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.1";
@@ -1,6 +1,13 @@
1
+ import { OXYGEN_BUILD_VERSION } from "./version.generated.js";
1
2
  // Release metadata is a string API: a new version must not change the exported
2
3
  // declaration type and invalidate every dependent package's compiler state.
3
- export const OXYGEN_VERSION = "1.1003.12";
4
+ //
5
+ // The value is DERIVED, never pinned per commit: the repo-root `VERSION` file
6
+ // holds the series (`1.<minor>`) and the patch is the distance from the commit
7
+ // that last touched it. scripts/ci/version-stamp.mjs writes
8
+ // ./version.generated.ts (gitignored) at build, test and publish time. Change
9
+ // the series by editing VERSION; there is nothing to bump here.
10
+ export const OXYGEN_VERSION = OXYGEN_BUILD_VERSION;
4
11
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
5
12
  // operational route. Raising it hard-rejects every older CLI from the entire
6
13
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -3,7 +3,6 @@ import { CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS, CRM_WORKFLOW_EVENT_SOURCE, } fro
3
3
  import { SEQUENCE_CONTACT_ACTIVITY_EVENT, SEQUENCE_CRM_EVENT_DEFINITIONS, SEQUENCE_WORKFLOW_EVENT_SOURCE, } from "./sequence-crm-events.js";
4
4
  import { SEQUENCE_ENROLLMENT_TERMINAL_EVENT, SEQUENCE_TERMINAL_EVENT_SOURCE, SEQUENCE_TERMINAL_STATUSES, } from "./sequence-terminal-events.js";
5
5
  import { NOTETAKER_MEETING_COMPLETED_EVENT, NOTETAKER_MEETING_COMPLETED_PAYLOAD_FIELDS, NOTETAKER_WORKFLOW_EVENT_SOURCE, } from "./notetaker-events.js";
6
- import { PLAIN_SUPPORT_MESSAGE_RECEIVED_EVENT, PLAIN_SUPPORT_MESSAGE_RECEIVED_PAYLOAD_FIELDS, PLAIN_SUPPORT_WORKFLOW_EVENT_SOURCE, } from "./plain-support-events.js";
7
6
  export const WORKSPACE_EVENT_PRODUCER_READINESS_REQUIREMENTS = [
8
7
  "always",
9
8
  "notetaker_enabled",
@@ -107,15 +106,6 @@ export const WORKSPACE_EVENT_PRODUCERS = [
107
106
  readiness: "notetaker_enabled",
108
107
  payloadFields: NOTETAKER_MEETING_COMPLETED_PAYLOAD_FIELDS,
109
108
  },
110
- {
111
- id: "plain_support.message_received",
112
- source: PLAIN_SUPPORT_WORKFLOW_EVENT_SOURCE,
113
- event: PLAIN_SUPPORT_MESSAGE_RECEIVED_EVENT,
114
- delivery: "direct_durable_write",
115
- idempotency: "The verified Plain event id deduplicates metadata-only support delivery.",
116
- readiness: "always",
117
- payloadFields: PLAIN_SUPPORT_MESSAGE_RECEIVED_PAYLOAD_FIELDS,
118
- },
119
109
  ];
120
110
  const workspaceEventProducersById = new Map(WORKSPACE_EVENT_PRODUCERS.map((producer) => [producer.id, producer]));
121
111
  export function getWorkspaceEventProducer(producerId) {
@@ -224,18 +214,6 @@ const crmSignalEntries = CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS.map((definition)
224
214
  description: definition.description,
225
215
  payloadFields: definition.payloadFields,
226
216
  }));
227
- const plainSupportEntries = [
228
- {
229
- id: "plain_support.message_received",
230
- producerId: "plain_support.message_received",
231
- source: PLAIN_SUPPORT_WORKFLOW_EVENT_SOURCE,
232
- event: PLAIN_SUPPORT_MESSAGE_RECEIVED_EVENT,
233
- group: "Support · Plain",
234
- label: "Customer message received",
235
- description: "A customer sent a Chat, email, Slack, Microsoft Teams, or Discord message into a canonical Plain Thread. The event contains routing metadata only; customer content remains in Plain.",
236
- payloadFields: PLAIN_SUPPORT_MESSAGE_RECEIVED_PAYLOAD_FIELDS,
237
- },
238
- ];
239
217
  const notetakerEntries = [
240
218
  {
241
219
  id: "notetaker.meeting_completed",
@@ -257,6 +235,5 @@ export const WORKSPACE_EVENT_CATALOG = [
257
235
  ...sequenceTerminalEntries,
258
236
  ...crmActivityEntries,
259
237
  ...crmSignalEntries,
260
- ...plainSupportEntries,
261
238
  ...notetakerEntries,
262
239
  ];