@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
@@ -1,4 +1,4 @@
1
- import type { LogSink } from "./log.js";
1
+ import { type LogLevel, type LogSink } from "./log.js";
2
2
  /**
3
3
  * Ships `log()` records to an OTLP/HTTP logs receiver (the self-hosted SigNoz
4
4
  * collector) instead of — or alongside — Axiom.
@@ -19,9 +19,16 @@ import type { LogSink } from "./log.js";
19
19
  * inside `log()`, so it must never await, throw, or block), the queue drops the
20
20
  * OLDEST record on overflow because the newest lines describe the incident being
21
21
  * debugged, and a collector outage is a dropped batch rather than a process
22
- * failure. Failures are written to stderr directly and at most once per flush
23
- * window — reporting them through `log()` would feed this sink its own error
24
- * line and recurse.
22
+ * failure. Failures and queue overflow are reported through `log()`, at most
23
+ * once per minute each whatever the flush interval (a dead collector behind a
24
+ * 2 s flush would otherwise write ~43k lines a day into Axiom), with the
25
+ * failures and drops a suppressed report would have carried accumulated into
26
+ * the next one, under the re-entrancy guard the Axiom shippers use
27
+ * (`emittingInternal`): this sink skips the records it emits itself, so a broken
28
+ * collector cannot feed its own failure line back into its queue, while every
29
+ * sink composed beside it (Axiom under `OXYGEN_LOG_SINK=both`) and stdout still
30
+ * receive the line. `log()` always writes stdout before it calls a sink, which
31
+ * is why the former direct stderr write is not kept as a fallback.
25
32
  */
26
33
  export type OtlpLogSinkOptions = {
27
34
  /** Full receiver URL, e.g. `http://collector.internal:4318/v1/logs`. */
@@ -36,6 +43,14 @@ export type OtlpLogSinkOptions = {
36
43
  /** `0` disables the interval timer, for runtimes that flush off a request lifecycle. */
37
44
  flushIntervalMs?: number | undefined;
38
45
  maxQueue?: number | undefined;
46
+ /** Clock for the report throttle; tests only. */
47
+ now?: (() => number) | undefined;
48
+ /**
49
+ * Lowest level this sink ships; records below it never enter the queue. Null
50
+ * or absent ships every level. Sinks composed beside this one (Axiom) and
51
+ * stdout are unaffected; see `resolveOtlpLogMinLevel`.
52
+ */
53
+ minLevel?: LogLevel | null | undefined;
39
54
  fetch?: typeof globalThis.fetch | undefined;
40
55
  };
41
56
  export type OtlpLogSinkHandle = {
@@ -43,6 +58,16 @@ export type OtlpLogSinkHandle = {
43
58
  flush: () => Promise<void>;
44
59
  stop: () => Promise<void>;
45
60
  };
61
+ /**
62
+ * Reads `OXYGEN_LOG_OTLP_MIN_LEVEL` (debug|info|warn|error, case-insensitive).
63
+ *
64
+ * The OTLP collector is the one transport a noisy tier can drown, so it alone
65
+ * gets a floor; Axiom and stdout keep every level. Unset ships everything, which
66
+ * is the behaviour before the variable existed. An unrecognised value is named
67
+ * through `log()` and treated as unset: dropping more than was asked is the
68
+ * failure an operator cannot see, so a typo errs toward shipping.
69
+ */
70
+ export declare function resolveOtlpLogMinLevel(env?: NodeJS.ProcessEnv): LogLevel | null;
46
71
  /**
47
72
  * `k=v,k=v` — the same shape as `OTEL_EXPORTER_OTLP_HEADERS`, so an operator who
48
73
  * knows the OTel variable can set ours without learning a second syntax. A pair
@@ -1,14 +1,86 @@
1
+ import { log } from "./log.js";
2
+ import { resolveDeployEnv } from "./deploy-env.js";
3
+ import { sanitizeLogFields } from "./redaction.js";
4
+ import { trace } from "@opentelemetry/api";
5
+ import { operationalLogSnapshot } from "./operational-telemetry.js";
6
+ import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
1
7
  const DEFAULT_BATCH_SIZE = 500;
2
8
  const DEFAULT_FLUSH_INTERVAL_MS = 2_000;
3
9
  const DEFAULT_MAX_QUEUE = 10_000;
4
10
  const EXPORT_TIMEOUT_MS = 10_000;
5
- const NO_TIMER_REPORT_WINDOW_MS = 60_000;
6
- const MAX_ERROR_BODY = 300;
11
+ const REPORT_WINDOW_MS = 60_000;
12
+ const MAX_RESPONSE_BYTES = 65_536;
13
+ async function rejectedRecords(response, batchLength) {
14
+ const reader = response.body?.getReader();
15
+ if (!reader)
16
+ return 0;
17
+ const decoder = new TextDecoder();
18
+ let text = "";
19
+ let bytes = 0;
20
+ try {
21
+ for (;;) {
22
+ const chunk = await reader.read();
23
+ if (chunk.done)
24
+ break;
25
+ bytes += chunk.value.byteLength;
26
+ if (bytes > MAX_RESPONSE_BYTES)
27
+ throw new Error("invalid_response");
28
+ text += decoder.decode(chunk.value, { stream: true });
29
+ }
30
+ text += decoder.decode();
31
+ if (!text.trim())
32
+ return 0;
33
+ const data = JSON.parse(text);
34
+ if (!data || typeof data !== "object" || Array.isArray(data))
35
+ throw new Error("invalid_response");
36
+ const partial = data.partialSuccess;
37
+ const rejected = Number(partial?.rejectedLogRecords ?? 0);
38
+ if (!Number.isSafeInteger(rejected) || rejected < 0 || rejected > batchLength)
39
+ throw new Error("invalid_response");
40
+ return rejected;
41
+ }
42
+ finally {
43
+ await reader.cancel().catch(() => undefined);
44
+ reader.releaseLock();
45
+ }
46
+ }
7
47
  // OTLP severity numbers (logs data model): the base of each 4-value band.
8
48
  const SEVERITY_NUMBERS = { debug: 5, info: 9, warn: 13, error: 17 };
9
49
  // Set by log() on every record; they become the OTLP record's own fields rather
10
50
  // than attributes, so carrying them twice would just inflate every batch.
11
- const RECORD_FIELDS_NOT_ATTRIBUTES = new Set(["ts", "level", "msg"]);
51
+ const RECORD_FIELDS_NOT_ATTRIBUTES = new Set(["ts", "level", "msg", "trace_id", "span_id"]);
52
+ /**
53
+ * Reads `OXYGEN_LOG_OTLP_MIN_LEVEL` (debug|info|warn|error, case-insensitive).
54
+ *
55
+ * The OTLP collector is the one transport a noisy tier can drown, so it alone
56
+ * gets a floor; Axiom and stdout keep every level. Unset ships everything, which
57
+ * is the behaviour before the variable existed. An unrecognised value is named
58
+ * through `log()` and treated as unset: dropping more than was asked is the
59
+ * failure an operator cannot see, so a typo errs toward shipping.
60
+ */
61
+ export function resolveOtlpLogMinLevel(env = process.env) {
62
+ const raw = env.OXYGEN_LOG_OTLP_MIN_LEVEL?.trim().toLowerCase();
63
+ if (!raw)
64
+ return null;
65
+ if (Object.hasOwn(SEVERITY_NUMBERS, raw))
66
+ return raw;
67
+ log("warn", "otlp_log_sink.min_level_invalid", {
68
+ min_level: raw.slice(0, 32),
69
+ allowed: Object.keys(SEVERITY_NUMBERS).join("|"),
70
+ });
71
+ return null;
72
+ }
73
+ /**
74
+ * The `code` of a transport error's `cause` (undici puts ECONNREFUSED,
75
+ * UND_ERR_CONNECT_TIMEOUT and the like there), and nothing else: the message
76
+ * and the error itself can carry the request URL and, with it, collector
77
+ * tokens. Anything that is not a short identifier-shaped code is dropped.
78
+ */
79
+ function sanitizedErrorCause(error) {
80
+ const cause = error && typeof error === "object" ? error.cause : undefined;
81
+ const code = cause && typeof cause === "object" ? cause.code : undefined;
82
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]{1,63}$/.test(code) ? code : null;
83
+ }
12
84
  /**
13
85
  * `k=v,k=v` — the same shape as `OTEL_EXPORTER_OTLP_HEADERS`, so an operator who
14
86
  * knows the OTel variable can set ours without learning a second syntax. A pair
@@ -71,15 +143,37 @@ function timeUnixNano(ts) {
71
143
  // Decimal string, not a number: nanoseconds since the epoch exceed 2^53.
72
144
  return `${millis}000000`;
73
145
  }
74
- function toLogRecord(record) {
75
- const level = typeof record.level === "string" && record.level in SEVERITY_NUMBERS
146
+ /**
147
+ * The identity contract's tier when `OXYGEN_DEPLOY_ENV` is set, else null. An
148
+ * invalid value is the boot guard's to refuse; a telemetry sink created before
149
+ * that guard must not throw on it and take the logger down with it.
150
+ */
151
+ function deployTierOrNull() {
152
+ if (!process.env.OXYGEN_DEPLOY_ENV)
153
+ return null;
154
+ try {
155
+ return resolveDeployEnv().tier;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ function recordLevel(record) {
162
+ return typeof record.level === "string" && Object.hasOwn(SEVERITY_NUMBERS, record.level)
76
163
  ? record.level
77
164
  : "info";
165
+ }
166
+ function toLogRecord(record) {
167
+ const level = recordLevel(record);
78
168
  return {
79
169
  timeUnixNano: timeUnixNano(record.ts),
80
170
  severityNumber: SEVERITY_NUMBERS[level],
81
171
  severityText: level.toUpperCase(),
82
172
  body: { stringValue: typeof record.msg === "string" ? record.msg : String(record.msg ?? "") },
173
+ ...(typeof record.trace_id === "string" && /^[a-f\d]{32}$/i.test(record.trace_id)
174
+ && !/^0+$/.test(record.trace_id) ? { traceId: record.trace_id.toLowerCase() } : {}),
175
+ ...(typeof record.span_id === "string" && /^[a-f\d]{16}$/i.test(record.span_id)
176
+ && !/^0+$/.test(record.span_id) ? { spanId: record.span_id.toLowerCase() } : {}),
83
177
  attributes: toAttributes(record, RECORD_FIELDS_NOT_ATTRIBUTES),
84
178
  };
85
179
  }
@@ -90,54 +184,105 @@ export function createOtlpLogSink(options) {
90
184
  ? DEFAULT_FLUSH_INTERVAL_MS
91
185
  : options.flushIntervalMs;
92
186
  const doFetch = options.fetch ?? globalThis.fetch;
93
- const reportWindowMs = flushIntervalMs > 0 ? flushIntervalMs : NO_TIMER_REPORT_WINDOW_MS;
187
+ const now = options.now ?? Date.now;
188
+ const minSeverity = options.minLevel ? SEVERITY_NUMBERS[options.minLevel] : null;
94
189
  // A null/undefined override is dropped rather than applied, so a caller that
95
190
  // resolves its own environment and comes back empty-handed falls back to the
96
191
  // default here instead of shipping records with no `deployment.environment`.
97
192
  const resourceOverrides = Object.fromEntries(Object.entries(options.resource ?? {}).filter(([, value]) => value !== null && value !== undefined));
98
193
  const resourceAttributes = toAttributes({
99
194
  "service.name": options.serviceName,
100
- "deployment.environment": process.env.OXYGEN_DEPLOY_ENV
195
+ "deployment.environment": deployTierOrNull()
101
196
  ?? process.env.VERCEL_ENV
102
197
  ?? process.env.NODE_ENV
103
198
  ?? null,
199
+ "deployment.sha": process.env.OXYGEN_GIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA,
104
200
  ...resourceOverrides,
105
201
  });
106
202
  const queue = [];
107
203
  let inFlight = null;
108
204
  let stopped = false;
109
- let lastReportAt = 0;
110
- // Never through log(): this module IS the sink log() calls, so a log() here
111
- // would enqueue its own failure and re-fail on the next flush forever. stderr
112
- // is the one destination that cannot re-enter, and the write is guarded
113
- // because a closed/full stderr must not throw into the flush path either.
114
- const report = (fields) => {
115
- const now = Date.now();
116
- if (now - lastReportAt < reportWindowMs)
117
- return;
118
- lastReportAt = now;
205
+ let lastReportAt = Number.NEGATIVE_INFINITY;
206
+ let lastOverflowReportAt = Number.NEGATIVE_INFINITY;
207
+ // Failed exports a throttled report skipped, and the records they dropped;
208
+ // both ride on the next export_failed line, as telemetry.export_failed does.
209
+ let suppressedFailures = 0;
210
+ let pendingDropped = 0;
211
+ // Records the overflow path dropped that no report has carried yet.
212
+ let droppedOverflow = 0;
213
+ // Re-entrancy guard, as in apps/worker/src/log-shipper.ts. log() calls this
214
+ // sink synchronously, so while it is set the record on offer is this sink's
215
+ // own report: skipped here, so a failing flush never enqueues its own error
216
+ // line and re-fails on it forever.
217
+ let emittingInternal = false;
218
+ const endpointForLog = telemetryEndpointForLog(options.endpoint);
219
+ const emitInternal = (msg, fields) => {
220
+ emittingInternal = true;
119
221
  try {
120
- process.stderr.write(`${JSON.stringify({
121
- level: "error",
122
- msg: "otlp_log_sink.export_failed",
123
- endpoint: options.endpoint,
124
- ...fields,
125
- })}\n`);
222
+ log("warn", msg, { endpoint: endpointForLog, ...fields });
126
223
  }
127
224
  catch {
128
225
  // A telemetry failure that cannot even be reported is still not a fault
129
226
  // of the code being observed.
130
227
  }
228
+ finally {
229
+ emittingInternal = false;
230
+ }
231
+ };
232
+ const report = (droppedEvents, fields) => {
233
+ pendingDropped += droppedEvents;
234
+ const at = now();
235
+ if (at - lastReportAt < REPORT_WINDOW_MS) {
236
+ suppressedFailures += 1;
237
+ return;
238
+ }
239
+ lastReportAt = at;
240
+ const overflow = droppedOverflow;
241
+ const dropped = pendingDropped;
242
+ const suppressed = suppressedFailures;
243
+ droppedOverflow = 0;
244
+ pendingDropped = 0;
245
+ suppressedFailures = 0;
246
+ emitInternal("otlp_log_sink.export_failed", {
247
+ ...fields,
248
+ dropped_events: dropped,
249
+ dropped_overflow: overflow,
250
+ suppressed_failures: suppressed,
251
+ });
252
+ };
253
+ // Overflow is data loss even while every export succeeds (a burst faster than
254
+ // the collector drains), so it is reported on its own when no failure line
255
+ // has carried it.
256
+ const reportOverflow = () => {
257
+ if (droppedOverflow === 0)
258
+ return;
259
+ const at = now();
260
+ if (at - lastOverflowReportAt < REPORT_WINDOW_MS)
261
+ return;
262
+ lastOverflowReportAt = at;
263
+ const overflow = droppedOverflow;
264
+ droppedOverflow = 0;
265
+ emitInternal("otlp_log_sink.queue_overflow", { dropped_events: overflow });
131
266
  };
132
267
  const sink = (record) => {
133
- if (stopped)
268
+ if (stopped || emittingInternal)
269
+ return;
270
+ // An unknown level ships as INFO (see toLogRecord), so it filters as one.
271
+ if (minSeverity !== null && SEVERITY_NUMBERS[recordLevel(record)] < minSeverity)
134
272
  return;
135
273
  // Drop the OLDEST record and keep this one: under sustained backpressure the
136
274
  // newest lines are the ones describing the incident being debugged.
137
275
  while (queue.length >= maxQueue) {
138
276
  queue.shift();
277
+ droppedOverflow += 1;
139
278
  }
140
- queue.push(record);
279
+ const activeSpan = trace.getActiveSpan()?.spanContext();
280
+ queue.push(operationalLogSnapshot(sanitizeLogFields({
281
+ ...record,
282
+ ...(activeSpan && (!record.trace_id || record.trace_id === activeSpan.traceId)
283
+ ? { trace_id: activeSpan.traceId, span_id: activeSpan.spanId }
284
+ : {}),
285
+ })));
141
286
  };
142
287
  const flush = async () => {
143
288
  if (inFlight)
@@ -166,25 +311,33 @@ export function createOtlpLogSink(options) {
166
311
  body: payload,
167
312
  signal: AbortSignal.timeout(EXPORT_TIMEOUT_MS),
168
313
  });
169
- if (response.ok)
314
+ if (response.ok) {
315
+ try {
316
+ const rejected = await rejectedRecords(response, batch.length);
317
+ if (rejected)
318
+ report(rejected, { status: response.status, error_code: "partial_rejection" });
319
+ }
320
+ catch {
321
+ report(batch.length, { status: response.status, error_code: "invalid_response" });
322
+ }
170
323
  return;
171
- let body = "";
172
- try {
173
- body = (await response.text()).slice(0, MAX_ERROR_BODY);
174
- }
175
- catch {
176
- // An unreadable body is no worse than a status-only report.
177
324
  }
325
+ // Receivers/proxies can echo rejected payloads or credentials. Never
326
+ // read arbitrary response text into operational diagnostics.
327
+ await response.body?.cancel().catch(() => undefined);
178
328
  // The batch is gone deliberately: retrying into an unhealthy collector
179
329
  // is how a telemetry path turns an ingest blip into memory pressure.
180
- report({ status: response.status, dropped_events: batch.length, response_body: body });
330
+ report(batch.length, { status: response.status, error_code: "receiver_rejected" });
181
331
  }
182
332
  catch (error) {
183
- report({
184
- dropped_events: batch.length,
185
- error_message: error instanceof Error ? error.message : String(error),
333
+ report(batch.length, {
334
+ error_code: "transport_failed",
335
+ error_cause: sanitizedErrorCause(error),
186
336
  });
187
337
  }
338
+ finally {
339
+ reportOverflow();
340
+ }
188
341
  })();
189
342
  inFlight = run.finally(() => {
190
343
  inFlight = null;
@@ -42,7 +42,6 @@ export declare const PRODUCT_EVENTS: {
42
42
  readonly FIRST_PLAY_STARTED: "first_play_started";
43
43
  readonly FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed";
44
44
  readonly FIRST_PLAY_COMPLETED: "first_play_completed";
45
- readonly FIRST_PLAY_SKIPPED: "first_play_skipped";
46
45
  /**
47
46
  * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
48
47
  * second highest-volume event; it was REPLACED (not joined) by the reserved
@@ -70,13 +69,33 @@ export declare const PRODUCT_EVENTS: {
70
69
  readonly TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled";
71
70
  readonly AGENT_RUN_SETTLED: "agent_run_settled";
72
71
  readonly SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled";
72
+ /**
73
+ * A Workspace Copilot turn reached completed / failed / cancelled. The
74
+ * Copilot is a Control surface, not a primitive, but its turns are durable
75
+ * worker runs like the others and it had no outcome event at all. Counts,
76
+ * credits and ids only — never the user message or the assistant text.
77
+ */
78
+ readonly COPILOT_TURN_SETTLED: "copilot_turn_settled";
73
79
  /** Revenue lifecycle. */
80
+ /**
81
+ * A Stripe Checkout was opened for a first subscription. `source` says which
82
+ * surface opened it (a paywall, the sidebar Upgrade row, Billing) and
83
+ * `blocked_capability` which wall, so signup → wall → checkout →
84
+ * `subscription_activated` is one funnel instead of a gap between pageviews.
85
+ */
86
+ readonly CHECKOUT_STARTED: "checkout_started";
74
87
  readonly TRIAL_STARTED: "trial_started";
75
88
  readonly SUBSCRIPTION_ACTIVATED: "subscription_activated";
76
89
  readonly SUBSCRIPTION_CHURNED: "subscription_churned";
77
90
  readonly TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled";
78
91
  readonly TRIAL_CANCEL_RESUMED: "trial_cancel_resumed";
79
92
  readonly TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted";
93
+ /**
94
+ * A paid credit top-up was granted (the Stripe Checkout fulfilment, after the
95
+ * ledger grant committed). `uuid` derives from the checkout session, so the
96
+ * webhook, the success redirect and every redelivery collapse to one event.
97
+ */
98
+ readonly CREDITS_PURCHASED: "credits_purchased";
80
99
  /**
81
100
  * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
101
  * visitor types their website and OXYGEN spends its own credits profiling it.
@@ -127,5 +146,5 @@ export declare const PRODUCT_EVENT_NAMES: readonly ProductEventName[];
127
146
  * emitter cannot be handed a lifecycle event that only the control plane has
128
147
  * the facts for.
129
148
  */
130
- export declare const WORKER_RUN_OUTCOME_EVENTS: readonly ["workflow_run_settled", "table_action_run_settled", "table_ingestion_run_settled", "agent_run_settled", "sequence_enrollment_settled"];
149
+ export declare const WORKER_RUN_OUTCOME_EVENTS: readonly ["workflow_run_settled", "table_action_run_settled", "table_ingestion_run_settled", "agent_run_settled", "sequence_enrollment_settled", "copilot_turn_settled"];
131
150
  export type WorkerRunOutcomeEventName = (typeof WORKER_RUN_OUTCOME_EVENTS)[number];
@@ -42,7 +42,6 @@ export const PRODUCT_EVENTS = {
42
42
  FIRST_PLAY_STARTED: "first_play_started",
43
43
  FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed",
44
44
  FIRST_PLAY_COMPLETED: "first_play_completed",
45
- FIRST_PLAY_SKIPPED: "first_play_skipped",
46
45
  /**
47
46
  * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
48
47
  * second highest-volume event; it was REPLACED (not joined) by the reserved
@@ -70,13 +69,33 @@ export const PRODUCT_EVENTS = {
70
69
  TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled",
71
70
  AGENT_RUN_SETTLED: "agent_run_settled",
72
71
  SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled",
72
+ /**
73
+ * A Workspace Copilot turn reached completed / failed / cancelled. The
74
+ * Copilot is a Control surface, not a primitive, but its turns are durable
75
+ * worker runs like the others and it had no outcome event at all. Counts,
76
+ * credits and ids only — never the user message or the assistant text.
77
+ */
78
+ COPILOT_TURN_SETTLED: "copilot_turn_settled",
73
79
  /** Revenue lifecycle. */
80
+ /**
81
+ * A Stripe Checkout was opened for a first subscription. `source` says which
82
+ * surface opened it (a paywall, the sidebar Upgrade row, Billing) and
83
+ * `blocked_capability` which wall, so signup → wall → checkout →
84
+ * `subscription_activated` is one funnel instead of a gap between pageviews.
85
+ */
86
+ CHECKOUT_STARTED: "checkout_started",
74
87
  TRIAL_STARTED: "trial_started",
75
88
  SUBSCRIPTION_ACTIVATED: "subscription_activated",
76
89
  SUBSCRIPTION_CHURNED: "subscription_churned",
77
90
  TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled",
78
91
  TRIAL_CANCEL_RESUMED: "trial_cancel_resumed",
79
92
  TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted",
93
+ /**
94
+ * A paid credit top-up was granted (the Stripe Checkout fulfilment, after the
95
+ * ledger grant committed). `uuid` derives from the checkout session, so the
96
+ * webhook, the success redirect and every redelivery collapse to one event.
97
+ */
98
+ CREDITS_PURCHASED: "credits_purchased",
80
99
  /**
81
100
  * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
101
  * visitor types their website and OXYGEN spends its own credits profiling it.
@@ -132,4 +151,5 @@ export const WORKER_RUN_OUTCOME_EVENTS = Object.freeze([
132
151
  PRODUCT_EVENTS.TABLE_INGESTION_RUN_SETTLED,
133
152
  PRODUCT_EVENTS.AGENT_RUN_SETTLED,
134
153
  PRODUCT_EVENTS.SEQUENCE_ENROLLMENT_SETTLED,
154
+ PRODUCT_EVENTS.COPILOT_TURN_SETTLED,
135
155
  ]);
@@ -1,5 +1,8 @@
1
1
  const SECRET_KEY_PATTERN = /(api[_-]?key|x[_-]?key|authorization|bearer|cookie|password|secret|token|ciphertext|connection[_-]?string|connection[_-]?uri|database[_-]?url|dsn)/i;
2
- const OMITTED_KEY_PATTERN = /^(body|payload|prompt|prompts|raw_prompt|raw_prompts|row|rows|input|inputs|output|outputs|request|response|provider_payload|provider_response|customer_data)$/i;
2
+ // `content_preview` is raw model output (ai-column-runner's invalid_ai_output
3
+ // details); web routes log `error_details` on a 4xx, and model text belongs in
4
+ // Langfuse only (ADR 0014), never in operational logs.
5
+ const OMITTED_KEY_PATTERN = /^(body|payload|prompt|prompts|raw_prompt|raw_prompts|row|rows|input|inputs|output|outputs|request|response|provider_payload|provider_response|customer_data|content_preview)$/i;
3
6
  // OXY-125: keyed redaction (SECRET_KEY_PATTERN) only catches whole fields named
4
7
  // like a secret. Credentials also leak as *substrings* of otherwise-ordinary
5
8
  // fields — most commonly an Authorization header dumped into error_message /
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which key a LinkedIn scraper lane runs on (Philipp, 2026-09-25). Every lane
3
+ * defaults to OXYGEN's managed credits; a workspace may instead run it on its own
4
+ * account with the lane's vendor, which OXYGEN then never bills. The vendor is a
5
+ * property of the lane, so a caller picks only the mode.
6
+ *
7
+ * Browser-safe on purpose: the New table lanes render the menu from this list.
8
+ */
9
+ export declare const SCRAPER_LANE_CREDENTIAL_MODES: readonly ["managed", "user_api_key"];
10
+ export type ScraperLaneCredentialMode = (typeof SCRAPER_LANE_CREDENTIAL_MODES)[number];
11
+ export type ScraperLane = "linkedin_posts" | "linkedin_post_engagers" | "linkedin_company_followers";
12
+ /** The integration that stores each lane's customer key, and its display name. */
13
+ export declare const SCRAPER_LANE_KEY_VENDOR: Readonly<Record<ScraperLane, {
14
+ integrationId: "up2data" | "scrapeli";
15
+ name: string;
16
+ }>>;
17
+ /** Absent means managed; anything outside the two modes is null, for the caller to refuse. */
18
+ export declare function readScraperLaneCredentialMode(value: unknown): ScraperLaneCredentialMode | null;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Which key a LinkedIn scraper lane runs on (Philipp, 2026-09-25). Every lane
3
+ * defaults to OXYGEN's managed credits; a workspace may instead run it on its own
4
+ * account with the lane's vendor, which OXYGEN then never bills. The vendor is a
5
+ * property of the lane, so a caller picks only the mode.
6
+ *
7
+ * Browser-safe on purpose: the New table lanes render the menu from this list.
8
+ */
9
+ export const SCRAPER_LANE_CREDENTIAL_MODES = ["managed", "user_api_key"];
10
+ /** The integration that stores each lane's customer key, and its display name. */
11
+ export const SCRAPER_LANE_KEY_VENDOR = {
12
+ linkedin_posts: { integrationId: "up2data", name: "Up2Data" },
13
+ linkedin_post_engagers: { integrationId: "up2data", name: "Up2Data" },
14
+ linkedin_company_followers: { integrationId: "scrapeli", name: "ScrapeLi" },
15
+ };
16
+ /** Absent means managed; anything outside the two modes is null, for the caller to refuse. */
17
+ export function readScraperLaneCredentialMode(value) {
18
+ if (value === undefined || value === null || value === "")
19
+ return "managed";
20
+ return SCRAPER_LANE_CREDENTIAL_MODES.includes(value)
21
+ ? value
22
+ : null;
23
+ }
@@ -1,6 +1,7 @@
1
1
  import { OxygenError } from "./cli-result.js";
2
2
  import { hashVariantKey, renderTemplate, spintaxSyntaxIssues, templateColumnKeys, } from "./sequence-template.js";
3
3
  import { isRecord } from "./type-guards.js";
4
+ import { LINKEDIN_INVITE_NOTE_LIMIT } from "./linkedin-sequences.js";
4
5
  /**
5
6
  * Multichannel sequence DSL — the shared contract validated identically by CLI,
6
7
  * MCP, API, and web. A sequence is an ordered list of steps applied to each
@@ -997,7 +998,10 @@ export const SEQUENCE_CRM_IDENTITY_KINDS = [
997
998
  export const SEQUENCE_CONNECTION_BRANCH_CONDITIONS = ["connection_accepted", "already_connected", "open_profile"];
998
999
  const MAX_SEQUENCE_STEPS = 50;
999
1000
  const MAX_TEMPLATE_LENGTH = 8_000;
1000
- const MAX_NOTE_LENGTH = 300;
1001
+ // The Premium ceiling, and only an authoring hint: the limit that actually
1002
+ // binds is the SENDER's, which is lower on a Basic account and is enforced
1003
+ // against the RENDERED note at dispatch. See LINKEDIN_INVITE_NOTE_LIMIT_BASIC.
1004
+ const MAX_NOTE_LENGTH = LINKEDIN_INVITE_NOTE_LIMIT;
1001
1005
  const MAX_CRM_TASK_PROPERTIES = 100;
1002
1006
  const MAX_CRM_TASK_ASSOCIATIONS = 20;
1003
1007
  const MAX_CRM_RECORD_MAPPINGS = 10;
@@ -0,0 +1,80 @@
1
+ export type JsonRecord = Record<string, unknown>;
2
+ export type SignupLeadWebhookSource = "clerk" | "onboarding" | "backfill" | "stripe";
3
+ export type CustomerLifecycleEvent = "signup"
4
+ /** Creator research completed by the onboarding bootstrap; produced by @oxygen/control-db, never by Clerk. */
5
+ | "signup_research" | "trial_created" | "paid" | "churned";
6
+ export type SignupLeadWebhookInput = {
7
+ eventId?: string | null | undefined;
8
+ providerEventId?: string | null | undefined;
9
+ type: string;
10
+ source: SignupLeadWebhookSource;
11
+ lifecycleEvent?: CustomerLifecycleEvent | null | undefined;
12
+ occurredAt?: Date | string | number | null | undefined;
13
+ user?: {
14
+ clerkUserId?: string | null;
15
+ email?: string | null;
16
+ firstName?: string | null;
17
+ lastName?: string | null;
18
+ createdAt?: Date | string | number | null | undefined;
19
+ /** A profile URL the provider already knows for this person; the consent-recorded creator URL still wins at enqueue. */
20
+ linkedinUrl?: string | null;
21
+ };
22
+ organization?: {
23
+ clerkOrgId?: string | null;
24
+ name?: string | null;
25
+ slug?: string | null;
26
+ domain?: string | null;
27
+ createdAt?: Date | string | number | null | undefined;
28
+ /**
29
+ * Clerk user id of the organization's creator (Clerk `organization.created_by`).
30
+ * Downstream qualification only treats the exact creator as an acquisition
31
+ * lead; invited members get onboarding but no founder outreach.
32
+ */
33
+ createdBy?: string | null;
34
+ };
35
+ membership?: {
36
+ role?: string | null;
37
+ };
38
+ stripe?: {
39
+ customerId?: string | null;
40
+ subscriptionId?: string | null;
41
+ invoiceId?: string | null;
42
+ priceId?: string | null;
43
+ tier?: string | null;
44
+ status?: string | null;
45
+ trialStart?: Date | string | number | null | undefined;
46
+ trialEnd?: Date | string | number | null | undefined;
47
+ periodStart?: Date | string | number | null | undefined;
48
+ periodEnd?: Date | string | number | null | undefined;
49
+ amountPaid?: number | null;
50
+ currency?: string | null;
51
+ billingReason?: string | null;
52
+ };
53
+ metadata?: JsonRecord;
54
+ /** Allow-listed research evidence (`person_facts` / `company_facts`) when the producer already holds it. */
55
+ qualificationEvidence?: JsonRecord;
56
+ /** Research intent or status attached to the fact (`expected`, `scope`, `run_id`, ...). */
57
+ research?: JsonRecord;
58
+ rawEvent?: JsonRecord;
59
+ };
60
+ export type SignupLeadWebhookPayload = JsonRecord & {
61
+ id: string;
62
+ event_id: string;
63
+ event: string;
64
+ type: string;
65
+ occurred_at: string;
66
+ source: SignupLeadWebhookSource;
67
+ event_type: string;
68
+ provider_event_id: string;
69
+ };
70
+ export type WorkEmailClassification = {
71
+ domain: string | null;
72
+ isGenericMailbox: boolean;
73
+ };
74
+ /**
75
+ * Classifies an email address into a (likely) company work-email domain.
76
+ * Consumer/generic mailboxes return `{ domain: null, isGenericMailbox: true }`.
77
+ */
78
+ export declare function classifyWorkEmail(email: string | null | undefined): WorkEmailClassification;
79
+ export declare function isGenericMailboxDomain(domain: string | null | undefined): boolean;
80
+ export declare function buildSignupLeadWebhookPayload(input: SignupLeadWebhookInput): SignupLeadWebhookPayload;