@oxygen-agent/cli 1.987.20 → 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 (176) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/column-decision-options.d.ts +20 -0
  6. package/dist/column-decision-options.js +54 -0
  7. package/dist/command-manifest.d.ts +3 -2
  8. package/dist/command-manifest.js +25 -2
  9. package/dist/credentials.d.ts +1 -1
  10. package/dist/functions-commands.js +13 -9
  11. package/dist/help.d.ts +8 -0
  12. package/dist/help.js +46 -0
  13. package/dist/index.js +2751 -242
  14. package/dist/knowledge-mirror.d.ts +2 -2
  15. package/dist/runtime.d.ts +0 -15
  16. package/dist/runtime.js +1 -1
  17. package/dist/search-ai-filter-notice.d.ts +17 -0
  18. package/dist/search-ai-filter-notice.js +38 -0
  19. package/dist/session.d.ts +4 -3
  20. package/dist/skills.d.ts +8 -7
  21. package/dist/skills.js +58 -20
  22. package/dist/transcript.d.ts +2 -1
  23. package/dist/util.d.ts +10 -1
  24. package/dist/util.js +14 -2
  25. package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
  26. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
  27. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
  28. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  29. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  30. package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
  31. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  32. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  33. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
  34. package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
  35. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
  36. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  37. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  38. package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -22
  39. package/node_modules/@oxygen/shared/dist/billing.js +195 -40
  40. package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
  41. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +27 -0
  42. package/node_modules/@oxygen/shared/dist/capability-discovery.js +311 -28
  43. package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
  44. package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
  45. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  46. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  47. package/node_modules/@oxygen/shared/dist/column-autofill.js +62 -0
  48. package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
  49. package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
  50. package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
  51. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +113 -0
  52. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +548 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  54. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  55. package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
  56. package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
  57. package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
  58. package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
  59. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  60. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  61. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  62. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  63. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +107 -0
  64. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +809 -0
  65. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  66. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  67. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  68. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  69. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  70. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  71. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  72. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  73. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  74. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  75. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  76. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
  77. package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
  78. package/node_modules/@oxygen/shared/dist/index.d.ts +15 -0
  79. package/node_modules/@oxygen/shared/dist/index.js +15 -0
  80. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  81. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  82. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
  83. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
  84. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
  85. package/node_modules/@oxygen/shared/dist/langfuse.js +416 -14
  86. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +33 -0
  87. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +361 -0
  88. package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
  89. package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
  90. package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
  91. package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
  92. package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
  93. package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
  94. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
  95. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
  96. package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
  97. package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
  98. package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
  99. package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
  100. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  101. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  102. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  103. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  104. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
  105. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
  106. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  107. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  108. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
  109. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
  110. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +79 -0
  111. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +366 -0
  112. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  113. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  114. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  115. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  116. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  117. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  118. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  119. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  120. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +36 -2
  121. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +36 -1
  122. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  123. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  124. package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
  125. package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
  126. package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
  127. package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
  128. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  129. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  130. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  131. package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
  132. package/node_modules/@oxygen/shared/dist/sequences.js +115 -5
  133. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
  134. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
  135. package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
  136. package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
  137. package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
  138. package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
  139. package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
  140. package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
  141. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
  142. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
  143. package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
  144. package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
  145. package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
  146. package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
  147. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  148. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  149. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  150. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  151. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
  152. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
  153. package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
  154. package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
  155. package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
  156. package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
  157. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
  158. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
  159. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
  160. package/node_modules/@oxygen/shared/dist/telemetry.js +28 -2
  161. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  162. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  163. package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
  164. package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
  165. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  166. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  167. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
  168. package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
  169. package/node_modules/@oxygen/shared/dist/version.js +8 -1
  170. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
  171. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
  172. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
  173. package/node_modules/@oxygen/shared/package.json +59 -0
  174. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  175. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  176. package/package.json +3 -2
@@ -36,6 +36,8 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
36
36
  import { OXYGEN_VERSION } from "./version.js";
37
37
  import { prepareLlmPayload } from "./llm-payload.js";
38
38
  import { log } from "./log.js";
39
+ import { llmPromptLabel, registeredLlmPrompt } from "./llm-prompts.js";
40
+ import { activeTelemetryTraceId } from "./telemetry.js";
39
41
  const FLUSH_TIMEOUT_MS = 5_000;
40
42
  const WARN_THROTTLE_MS = 30_000;
41
43
  /**
@@ -73,6 +75,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
73
75
  const explicit = env.LANGFUSE_TRACING_ENVIRONMENT?.trim().toLowerCase();
74
76
  if (explicit)
75
77
  return explicit.replace(/[^a-z0-9_-]/g, "-");
78
+ // Self-hosted first: those processes carry neither VERCEL_ENV nor a Fly app
79
+ // name, so without this every self-hosted tier would report "development".
80
+ // selfhost-shadow stays "development" deliberately — production-shaped data,
81
+ // not production traffic.
82
+ const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
83
+ if (deployEnv === "selfhost-prod")
84
+ return "production";
85
+ if (deployEnv === "selfhost-shadow" || deployEnv === "selfhost-dev")
86
+ return "development";
76
87
  if (env.VERCEL_ENV)
77
88
  return env.VERCEL_ENV === "production" ? "production" : "development";
78
89
  const flyApp = env.FLY_APP_NAME ?? env.FLY_APP;
@@ -90,6 +101,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
90
101
  * the web app nor the worker.
91
102
  */
92
103
  export function resolveLlmTracingServiceName(env = process.env) {
104
+ const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
105
+ if (deployEnv === "selfhost-dev" || deployEnv === "selfhost-prod" || deployEnv === "selfhost-shadow") {
106
+ if (env.OXYGEN_PROCESS_ROLE === "worker") {
107
+ return deployEnv === "selfhost-prod" ? "oxygen-worker" : "oxygen-worker-dev";
108
+ }
109
+ if (env.OXYGEN_PROCESS_ROLE === "web" || env.OXYGEN_SERVICE_NAME === "oxygen-web")
110
+ return "oxygen-web";
111
+ return "oxygen-local";
112
+ }
93
113
  const flyApp = env.FLY_APP_NAME?.trim() || env.FLY_APP?.trim();
94
114
  if (flyApp)
95
115
  return flyApp.includes("-dev") ? "oxygen-worker-dev" : "oxygen-worker";
@@ -119,6 +139,9 @@ export function llmTraceIdForSeed(seed) {
119
139
  function rootSpanIdForSeed(seed) {
120
140
  return createHash("sha256").update(`langfuse-root:${seed}`, "utf8").digest("hex").slice(0, 16);
121
141
  }
142
+ function observationSpanId(parts) {
143
+ return createHash("sha256").update(JSON.stringify(parts), "utf8").digest("hex").slice(0, 16);
144
+ }
122
145
  // A score's `comment` is free text (a thumbs-down reason is user-authored and
123
146
  // unbounded) and the API types it as a STRING, so it cannot take
124
147
  // a recoverable payload envelope. It gets the same
@@ -139,9 +162,57 @@ function compact(body) {
139
162
  }
140
163
  return out;
141
164
  }
165
+ // --- Export summary ----------------------------------------------------------
166
+ // ingest_failed is throttled per process, so it is a floor on failures and says
167
+ // nothing about the healthy volume beside it. This rollup gives the ratio: how
168
+ // many spans Langfuse acknowledged, how many batches it refused, and how many
169
+ // observations never reached the exporter at all. Counts only; never content.
170
+ const EXPORT_SUMMARY_INTERVAL_MS = 60_000;
171
+ export function createLlmExportSummary(now = Date.now) {
172
+ let exported = 0;
173
+ let failed = 0;
174
+ let dropped = 0;
175
+ let windowStartedAt = now();
176
+ const report = () => {
177
+ const at = now();
178
+ if (exported + failed + dropped > 0) {
179
+ log("info", "llm_tracing.export_summary", {
180
+ provider: "langfuse",
181
+ exported,
182
+ failed,
183
+ dropped,
184
+ window_seconds: Math.max(0, Math.round((at - windowStartedAt) / 1000)),
185
+ });
186
+ }
187
+ exported = 0;
188
+ failed = 0;
189
+ dropped = 0;
190
+ windowStartedAt = at;
191
+ };
192
+ const add = (count) => (Number.isFinite(count) && count > 0 ? Math.floor(count) : 0);
193
+ return {
194
+ exported: (count) => { exported += add(count); },
195
+ failed: (count) => { failed += add(count); },
196
+ dropped: (count) => { dropped += add(count); },
197
+ maybeReport: () => { if (now() - windowStartedAt >= EXPORT_SUMMARY_INTERVAL_MS)
198
+ report(); },
199
+ reportNow: report,
200
+ };
201
+ }
202
+ /**
203
+ * A thrown value's class name is the only part of it that may leave the
204
+ * private store: messages from payload preparation can quote the payload.
205
+ */
206
+ function sanitizedErrorReason(error) {
207
+ const name = error instanceof Error ? error.name : typeof error;
208
+ return /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ? name : "unknown";
209
+ }
142
210
  function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
143
211
  return new Promise((resolve) => {
144
- const timer = setTimeout(resolve, timeoutMs);
212
+ const timer = setTimeout(() => {
213
+ warn(new Error("langfuse_flush_deadline_exceeded"));
214
+ resolve();
215
+ }, timeoutMs);
145
216
  timer.unref?.();
146
217
  rejectable
147
218
  .catch((error) => warn(error))
@@ -152,17 +223,133 @@ function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
152
223
  });
153
224
  }
154
225
  /** Lazy, private, always-sampled OTel transport. Never register it globally. */
155
- function createOtelEmitter(env, warn) {
226
+ function createOtelEmitter(env, warn, summary) {
156
227
  let handle = null;
157
228
  const init = () => {
158
229
  handle ??= (async () => {
159
230
  try {
160
- const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core] = await Promise.all([
231
+ const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core, { JsonTraceSerializer }] = await Promise.all([
161
232
  import("@langfuse/otel"), import("@opentelemetry/sdk-trace-base"),
162
233
  import("@opentelemetry/resources"),
163
234
  import("@langfuse/tracing"), import("@opentelemetry/api"), import("@langfuse/core"),
235
+ import("@opentelemetry/otlp-transformer"),
164
236
  ]);
237
+ const exportUrl = `${(env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "")}/api/public/otel/v1/traces`;
238
+ const exportHeaders = {
239
+ "Content-Type": "application/json",
240
+ Authorization: `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`,
241
+ "x-langfuse-sdk-name": "javascript",
242
+ "x-langfuse-sdk-version": core.LANGFUSE_SDK_VERSION,
243
+ "x-langfuse-public-key": env.LANGFUSE_PUBLIC_KEY,
244
+ };
245
+ const pendingExports = new Set();
246
+ // The upstream HTTP exporter logs raw partial-success/malformed response
247
+ // bodies through GLOBAL diagnostics before its callback runs. Keep the
248
+ // supported OTLP JSON serializer, but own delivery and response handling
249
+ // inside this private provider. Never mutate the global diagnostic logger.
250
+ const send = async (spans) => {
251
+ const encoded = JsonTraceSerializer.serializeRequest(spans);
252
+ if (!encoded)
253
+ throw new Error("serialization_failed");
254
+ const body = new TextDecoder().decode(encoded);
255
+ const signal = AbortSignal.timeout(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000);
256
+ let response;
257
+ for (let attempt = 0; attempt < 3; attempt++) {
258
+ try {
259
+ response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
260
+ }
261
+ catch {
262
+ if (signal.aborted)
263
+ throw new Error("deadline_exceeded");
264
+ if (attempt === 2)
265
+ throw new Error("network_failed");
266
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
267
+ continue;
268
+ }
269
+ if (![429, 502, 503, 504].includes(response.status) || attempt === 2)
270
+ break;
271
+ // A retryable rejection has no useful private response to retain.
272
+ await response.body?.cancel().catch(() => { });
273
+ await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
274
+ }
275
+ if (!response?.ok) {
276
+ await response?.body?.cancel().catch(() => { });
277
+ throw new Error("http_rejected");
278
+ }
279
+ const chunks = [];
280
+ let bytes = 0;
281
+ const reader = response.body?.getReader();
282
+ if (reader) {
283
+ try {
284
+ for (;;) {
285
+ const next = await reader.read();
286
+ if (next.done)
287
+ break;
288
+ bytes += next.value.byteLength;
289
+ // OTLP acknowledgements are tiny; an echoed payload must never
290
+ // become an unbounded allocation or reach any diagnostic logger.
291
+ if (bytes > 64 * 1024)
292
+ throw new Error("response_too_large");
293
+ chunks.push(next.value);
294
+ }
295
+ }
296
+ catch (error) {
297
+ await reader.cancel().catch(() => { });
298
+ throw new Error(error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
299
+ }
300
+ finally {
301
+ reader.releaseLock();
302
+ }
303
+ }
304
+ let acknowledgement;
305
+ try {
306
+ acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
307
+ }
308
+ catch {
309
+ throw new Error("response_invalid");
310
+ }
311
+ if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
312
+ throw new Error("response_invalid");
313
+ const envelope = acknowledgement;
314
+ const partial = envelope.partialSuccess ?? envelope.partial_success;
315
+ if (partial !== undefined && partial !== null) {
316
+ if (typeof partial !== "object" || Array.isArray(partial))
317
+ throw new Error("response_invalid");
318
+ const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
319
+ if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
320
+ throw new Error("response_invalid");
321
+ if (Number(rawRejected) > 0)
322
+ throw new Error("partial_rejection");
323
+ }
324
+ };
325
+ const privateExporter = {
326
+ export: (spans, done) => {
327
+ const pending = send(spans).then(() => {
328
+ // Complete the batch first: a logging fault must never leave the
329
+ // processor waiting out its own export timeout.
330
+ done({ code: 0 });
331
+ summary.exported(spans.length);
332
+ summary.maybeReport();
333
+ }, (error) => {
334
+ const failures = new Set(["serialization_failed", "deadline_exceeded", "network_failed", "http_rejected", "response_too_large", "response_read_failed", "response_invalid", "partial_rejection"]);
335
+ const failureKind = error instanceof Error && failures.has(error.message) ? error.message : "transport_failed";
336
+ // BSP forwards failures to the global OTel error handler. It only
337
+ // receives our constant classification, never the server body.
338
+ done({ code: 1, error: new Error("langfuse_delivery_failed") });
339
+ summary.failed(spans.length);
340
+ summary.maybeReport();
341
+ warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind });
342
+ });
343
+ pendingExports.add(pending);
344
+ // Even an unexpected SDK callback failure must not create a rejected
345
+ // fire-and-forget promise in the product process.
346
+ void pending.then(() => pendingExports.delete(pending), () => pendingExports.delete(pending));
347
+ },
348
+ shutdown: async () => { await Promise.allSettled([...pendingExports]); },
349
+ forceFlush: async () => { await Promise.allSettled([...pendingExports]); },
350
+ };
165
351
  const processor = new LangfuseSpanProcessor({
352
+ exporter: privateExporter,
166
353
  publicKey: env.LANGFUSE_PUBLIC_KEY,
167
354
  secretKey: env.LANGFUSE_SECRET_KEY,
168
355
  ...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
@@ -258,7 +445,19 @@ function createOtelEmitter(env, warn) {
258
445
  return handle;
259
446
  };
260
447
  return {
261
- emit: (emission) => { void init().then((h) => h?.emit(emission)).catch((error) => warn(error, { stage: emission.kind })); },
448
+ emit: (emission) => {
449
+ void init().then((h) => {
450
+ // init already warned once; every later emission is lost the same way.
451
+ // Report from here too: a process whose transport never came up has
452
+ // no exports to carry the window, and that is when dropped matters.
453
+ if (!h) {
454
+ summary.dropped(1);
455
+ summary.maybeReport();
456
+ return;
457
+ }
458
+ h.emit(emission);
459
+ }).catch((error) => { summary.dropped(1); summary.maybeReport(); warn(error, { stage: emission.kind }); });
460
+ },
262
461
  flush: () => init().then((h) => h?.processor.forceFlush() ?? Promise.resolve()),
263
462
  shutdown: () => init().then((h) => h?.processor.shutdown() ?? Promise.resolve()),
264
463
  };
@@ -338,6 +537,126 @@ function createApiScorer(env, warn) {
338
537
  },
339
538
  };
340
539
  }
540
+ const NOOP_PROMPT_RESOLVER = { versionFor: () => undefined };
541
+ const PROMPT_FOUND_TTL_MS = 24 * 60 * 60 * 1000;
542
+ const PROMPT_MISSING_TTL_MS = 10 * 60 * 1000;
543
+ const PROMPT_ERROR_TTL_MS = 60 * 1000;
544
+ const PROMPT_CACHE_LIMIT = 512;
545
+ function promptMetadata(prompt) {
546
+ return prompt
547
+ ? { oxygen_prompt_name: prompt.name, oxygen_prompt_hash: prompt.hash, oxygen_prompt_registered: prompt.registered }
548
+ : {};
549
+ }
550
+ /**
551
+ * Only a deployed dev process talking to the self-hosted instance may create
552
+ * prompt versions. Production and shadow data never do (the environment label
553
+ * is "production" there, or the host is Langfuse Cloud), and a local process
554
+ * would publish whatever uncommitted template its checkout holds.
555
+ */
556
+ function mayPublishMissingPrompts(env, baseUrl) {
557
+ if (resolveLlmTracingEnvironment(env) !== "development")
558
+ return false;
559
+ if (resolveLlmTracingServiceName(env) === "oxygen-local")
560
+ return false;
561
+ if (env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase() !== "selfhost-dev")
562
+ return false;
563
+ let host;
564
+ try {
565
+ host = new URL(baseUrl).hostname.toLowerCase();
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ return host !== "langfuse.com" && !host.endsWith(".langfuse.com");
571
+ }
572
+ export function createApiPromptResolver(env, warn, fetchImpl = fetch, now = Date.now) {
573
+ const baseUrl = (env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "");
574
+ const authorization = `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`;
575
+ const cache = new Map();
576
+ const inflight = new Set();
577
+ const publishAttempted = new Set();
578
+ const publishes = mayPublishMissingPrompts(env, baseUrl);
579
+ // One attempt per template per process; any failure falls back to metadata only.
580
+ const publish = async (key, prompt) => {
581
+ const definition = publishes && !publishAttempted.has(key) ? registeredLlmPrompt(prompt) : undefined;
582
+ if (!definition)
583
+ return undefined;
584
+ publishAttempted.add(key);
585
+ const label = llmPromptLabel(prompt.hash);
586
+ try {
587
+ const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts`, {
588
+ method: "POST",
589
+ headers: { Authorization: authorization, "Content-Type": "application/json" },
590
+ body: JSON.stringify({
591
+ type: "text", name: definition.name, prompt: definition.template, labels: [label],
592
+ tags: ["oxygen", "code-owned"], commitMessage: `oxygen code template ${prompt.hash} (runtime publish)`,
593
+ }),
594
+ signal: AbortSignal.timeout(5_000),
595
+ redirect: "error",
596
+ });
597
+ if (!response.ok) {
598
+ await response.body?.cancel().catch(() => { });
599
+ warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish", status: response.status });
600
+ return undefined;
601
+ }
602
+ const body = await response.json();
603
+ const labels = Array.isArray(body.labels) ? body.labels : [];
604
+ return typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label) ? body.version : undefined;
605
+ }
606
+ catch {
607
+ warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish" });
608
+ return undefined;
609
+ }
610
+ };
611
+ const lookup = async (key, prompt) => {
612
+ const label = llmPromptLabel(prompt.hash);
613
+ let entry = { version: undefined, expiresAt: now() + PROMPT_ERROR_TTL_MS };
614
+ try {
615
+ const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts/${encodeURIComponent(prompt.name)}?label=${encodeURIComponent(label)}`, { headers: { Authorization: authorization }, signal: AbortSignal.timeout(5_000), redirect: "error" });
616
+ if (response.status === 404) {
617
+ await response.body?.cancel().catch(() => { });
618
+ const version = await publish(key, prompt);
619
+ entry = version === undefined
620
+ ? { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS }
621
+ : { version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
622
+ }
623
+ else if (!response.ok) {
624
+ await response.body?.cancel().catch(() => { });
625
+ warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link", status: response.status });
626
+ }
627
+ else {
628
+ const body = await response.json();
629
+ const labels = Array.isArray(body.labels) ? body.labels : [];
630
+ if (typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label)) {
631
+ entry = { version: body.version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
632
+ }
633
+ else {
634
+ entry = { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS };
635
+ }
636
+ }
637
+ }
638
+ catch {
639
+ warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link" });
640
+ }
641
+ finally {
642
+ if (cache.size >= PROMPT_CACHE_LIMIT)
643
+ cache.delete(cache.keys().next().value);
644
+ cache.set(key, entry);
645
+ inflight.delete(key);
646
+ }
647
+ };
648
+ return {
649
+ versionFor: (prompt) => {
650
+ const key = `${prompt.name}\u0000${prompt.hash}`;
651
+ const cached = cache.get(key);
652
+ if ((!cached || cached.expiresAt <= now()) && !inflight.has(key)) {
653
+ inflight.add(key);
654
+ void lookup(key, prompt);
655
+ }
656
+ return cached?.version;
657
+ },
658
+ };
659
+ }
341
660
  /**
342
661
  * Construct a fail-open Langfuse client, or `null` when tracing is disabled or
343
662
  * misconfigured. Prefer the process-wide `getLlmTracingClient` in app code;
@@ -346,24 +665,65 @@ function createApiScorer(env, warn) {
346
665
  export function createLlmTracingClient(env = process.env, options) {
347
666
  if (!options?.emitterImpl && !isLlmTracingEnabled(env))
348
667
  return null;
349
- let lastWarnAtMs = 0;
668
+ const lastWarnAtMs = new Map();
350
669
  const warn = (error, context) => {
351
670
  const nowMs = Date.now();
352
- if (nowMs - lastWarnAtMs < WARN_THROTTLE_MS)
671
+ const errorCode = error instanceof Error && ["langfuse_flush_deadline_exceeded", "langfuse_replay_identity_unavailable", "langfuse_prompt_link_failed", "langfuse_observation_build_failed"].includes(error.message)
672
+ ? error.message : "langfuse_delivery_failed";
673
+ // An identity gap must not suppress a later actual delivery failure.
674
+ const warningKey = `${errorCode}:${context?.stage ?? "unknown"}`;
675
+ if (nowMs - (lastWarnAtMs.get(warningKey) ?? -Infinity) < WARN_THROTTLE_MS)
353
676
  return;
354
- lastWarnAtMs = nowMs;
677
+ lastWarnAtMs.set(warningKey, nowMs);
355
678
  log("warn", "llm_tracing.ingest_failed", {
356
679
  provider: "langfuse",
357
- error_message: error instanceof Error ? error.message : String(error),
680
+ // Exporter/API error messages can contain the rejected request or server
681
+ // response. Only this fixed classification may leave the private store.
682
+ error_code: errorCode,
358
683
  ...context,
359
684
  });
360
685
  };
361
- const transport = options?.emitterImpl ?? createOtelEmitter(env, warn);
686
+ // Opt-in per environment; managed projects retain their existing append
687
+ // semantics. A real execution is identified by the caller's immutable start
688
+ // plus logical id, never by the logical id alone (approval/tool ids can recur).
689
+ const stableIdentity = env.LANGFUSE_OBSERVATION_IDENTITY === "stable-v1";
690
+ const physicalId = (body, kind) => {
691
+ if (!stableIdentity)
692
+ return undefined;
693
+ // Verified ledger-backed control hooks supply the exact durable dedupe key.
694
+ // It already defines identity in the authoritative session/run ledger, so
695
+ // identity hashing needs no additional clock. The durable capture adapter
696
+ // separately supplies the original ledger timestamp for v3's date key.
697
+ const durableEventKey = body.metadata?.oxygen_durable_event_key;
698
+ if (kind === "event" && typeof durableEventKey === "string" && durableEventKey.length > 0) {
699
+ return observationSpanId(["langfuse-durable-event-v1", body.traceId, body.name, durableEventKey]);
700
+ }
701
+ // Approval ids name a durable decision/request across invocation slices.
702
+ // Other event keys are not universally unique; do not infer that guarantee.
703
+ if (kind === "event" && /^apr:(requested|decided):.+/.test(body.id)) {
704
+ return observationSpanId(["langfuse-event-v1", body.traceId, body.name, body.id]);
705
+ }
706
+ if (!body.startTime || !Number.isFinite(body.startTime.getTime())) {
707
+ warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: kind });
708
+ return undefined;
709
+ }
710
+ return observationSpanId(["langfuse-observation-v1", body.traceId, kind, body.name, body.id,
711
+ body.startTime.toISOString(), body.metadata?.attempt_id ?? null, body.metadata?.call_id ?? null]);
712
+ };
713
+ const summary = createLlmExportSummary();
714
+ const transport = options?.emitterImpl ?? createOtelEmitter(env, warn, summary);
362
715
  const emitter = {
363
716
  ...transport,
364
717
  emit: (emission) => {
365
718
  const observationId = emission.observationId ?? randomBytes(8).toString("hex");
366
719
  const metadata = { ...emission.attributes.metadata, oxygen_capture_version: 2 };
720
+ // Cross-link to the operational trace (SigNoz/Axiom). Read here, still in
721
+ // the caller's synchronous context; the private provider never sees it.
722
+ if (metadata.otel_trace_id === undefined) {
723
+ const otelTraceId = activeTelemetryTraceId();
724
+ if (otelTraceId)
725
+ metadata.otel_trace_id = otelTraceId;
726
+ }
367
727
  const attributes = { ...emission.attributes, version: OXYGEN_VERSION, metadata };
368
728
  const chunkEmissions = [];
369
729
  const capture = (value, field, key) => {
@@ -371,12 +731,18 @@ export function createLlmTracingClient(env = process.env, options) {
371
731
  for (const chunk of prepared.chunks) {
372
732
  chunkEmissions.push({
373
733
  kind: "event", traceSeed: emission.traceSeed, name: "llm.payload_chunk", parentObservationId: observationId,
734
+ ...(stableIdentity ? { observationId: observationSpanId(["langfuse-payload-v1", observationId, field, chunk.payload_id, chunk.index]) } : {}),
374
735
  attributes: { version: OXYGEN_VERSION, input: chunk, metadata: {
375
736
  oxygen_capture_version: 2, oxygen_payload_id: chunk.payload_id,
376
737
  oxygen_payload_chunk_index: chunk.index, oxygen_payload_chunk_count: prepared.chunks.length,
377
738
  oxygen_observation_id: `${metadata.oxygen_observation_id ?? observationId}:payload:${field}:${chunk.index}`,
378
739
  } },
379
- correlation: emission.correlation, startTime: emission.endTime ?? emission.startTime, endTime: emission.endTime ?? emission.startTime,
740
+ // V3's replacing key includes the start date. The root's end can
741
+ // move on resume, while unchanged chunks retain their IDs; pin their
742
+ // date to the immutable owner start in stable mode as well.
743
+ correlation: emission.correlation,
744
+ startTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
745
+ endTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
380
746
  });
381
747
  }
382
748
  return prepared.value;
@@ -405,12 +771,18 @@ export function createLlmTracingClient(env = process.env, options) {
405
771
  },
406
772
  };
407
773
  const scorer = options?.scorerImpl ?? createApiScorer(env, warn);
774
+ const promptResolver = options?.promptResolverImpl ?? (options?.emitterImpl ? NOOP_PROMPT_RESOLVER : createApiPromptResolver(env, warn));
408
775
  const guarded = (fn, stage) => {
409
776
  try {
410
777
  fn();
411
778
  }
412
779
  catch (error) {
413
- warn(error, { stage });
780
+ // Building the observation failed before any transport saw it (payload
781
+ // preparation, identity hashing, a substituted emitter). That is a lost
782
+ // capture, not a delivery failure, and must not read as one.
783
+ summary.dropped(1);
784
+ summary.maybeReport();
785
+ warn(new Error("langfuse_observation_build_failed"), { stage, reason: sanitizedErrorReason(error) });
414
786
  }
415
787
  };
416
788
  // propagateAttributes rejects non-string ids and anything over 200 chars, and
@@ -433,11 +805,17 @@ export function createLlmTracingClient(env = process.env, options) {
433
805
  tags: input.tags,
434
806
  });
435
807
  };
808
+ const promptLink = (prompt) => {
809
+ if (!prompt?.registered)
810
+ return {};
811
+ const version = promptResolver.versionFor(prompt);
812
+ return version === undefined ? {} : { prompt: { name: prompt.name, version, isFallback: false } };
813
+ };
436
814
  const modelObservation = (body, kind) => guarded(() => {
437
815
  emitter.emit({
438
816
  kind,
439
817
  traceSeed: body.traceId,
440
- ...(body.observationId ? { observationId: body.observationId } : {}),
818
+ observationId: body.observationId ?? physicalId(body, kind),
441
819
  name: body.name,
442
820
  attributes: compact({
443
821
  input: body.input,
@@ -449,7 +827,8 @@ export function createLlmTracingClient(env = process.env, options) {
449
827
  completionStartTime: body.completionStartTime,
450
828
  usageDetails: body.usageDetails,
451
829
  costDetails: body.costDetails,
452
- metadata: { ...(body.metadata ?? {}), oxygen_observation_id: body.id },
830
+ ...promptLink(body.prompt),
831
+ metadata: { ...(body.metadata ?? {}), ...promptMetadata(body.prompt), oxygen_observation_id: body.id },
453
832
  }),
454
833
  // The cost-bearing observation: v5 session cost only rolls up when
455
834
  // the generation itself carries the session.
@@ -486,6 +865,7 @@ export function createLlmTracingClient(env = process.env, options) {
486
865
  const sliceStart = typeof body.metadata.slice_started_at === "string" ? new Date(body.metadata.slice_started_at) : startTime;
487
866
  emitter.emit({
488
867
  kind: "span", traceSeed: body.id, name: `${body.name}.slice`,
868
+ ...(stableIdentity ? { observationId: observationSpanId(["langfuse-slice-v1", body.id, body.metadata.slice_id, sliceStart.getTime()]) } : {}),
489
869
  attributes: compact({ input: body.input, output: body.output, metadata: { ...body.metadata, oxygen_observation_id: `${body.id}:slice:${body.metadata.slice_id}` } }),
490
870
  correlation: correlate({ sessionId: body.sessionId, userId: body.userId }),
491
871
  startTime: Number.isFinite(sliceStart.getTime()) ? sliceStart : startTime, endTime: body.endTime ?? startTime,
@@ -496,6 +876,7 @@ export function createLlmTracingClient(env = process.env, options) {
496
876
  emitter.emit({
497
877
  kind: "span",
498
878
  traceSeed: body.traceId,
879
+ observationId: physicalId(body, "span"),
499
880
  name: body.name,
500
881
  attributes: compact({
501
882
  input: body.input,
@@ -512,10 +893,22 @@ export function createLlmTracingClient(env = process.env, options) {
512
893
  generation: (body) => modelObservation(body, "generation"),
513
894
  embedding: (body) => modelObservation(body, "embedding"),
514
895
  event: (body) => guarded(() => {
896
+ const durableKey = body.metadata?.oxygen_durable_event_key;
897
+ const durableEvent = (typeof durableKey === "string" && durableKey.length > 0) || /^apr:(requested|decided):.+/.test(body.id);
898
+ if (stableIdentity && durableEvent && (!body.startTime || !Number.isFinite(body.startTime.getTime()))) {
899
+ // A stable id with "now" as its date creates another V3 observation
900
+ // after midnight. Missing canonical time is missing capture, not a
901
+ // reason to fabricate a historical event or duplicate it silently.
902
+ warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: "event" });
903
+ summary.dropped(1);
904
+ summary.maybeReport();
905
+ return;
906
+ }
515
907
  const startTime = body.startTime ?? new Date();
516
908
  emitter.emit({
517
909
  kind: "event",
518
910
  traceSeed: body.traceId,
911
+ observationId: physicalId(body, "event"),
519
912
  name: body.name,
520
913
  attributes: compact({
521
914
  input: body.input,
@@ -542,7 +935,14 @@ export function createLlmTracingClient(env = process.env, options) {
542
935
  // enforced here (15s for serverless after()) rather than inside one emitter — an emitter that throws
543
936
  // synchronously or rejects must still not escape into product code.
544
937
  flush: () => boundedNever((async () => emitter.flush())(), (error) => warn(error, { stage: "flush" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
545
- shutdown: () => boundedNever((async () => emitter.shutdown())(), (error) => warn(error, { stage: "shutdown" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
938
+ shutdown: () => boundedNever(
939
+ // The final window is reported whatever its age: nothing follows it.
940
+ (async () => { try {
941
+ await emitter.shutdown();
942
+ }
943
+ finally {
944
+ summary.reportNow();
945
+ } })(), (error) => warn(error, { stage: "shutdown" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
546
946
  };
547
947
  }
548
948
  // --- Process-wide singleton (both runtimes construct at most one client) ------
@@ -554,6 +954,8 @@ function cacheKey(env) {
554
954
  env.LANGFUSE_SECRET_KEY ?? "",
555
955
  env.LANGFUSE_BASE_URL ?? "",
556
956
  resolveLlmTracingEnvironment(env),
957
+ resolveLlmTracingServiceName(env),
958
+ env.LANGFUSE_OBSERVATION_IDENTITY ?? "",
557
959
  ].join("|");
558
960
  }
559
961
  export function getLlmTracingClient(env = process.env) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The country vocabulary behind the CRM's "Primary Location — Country" column.
3
+ *
4
+ * WHY A CHECKED-IN TABLE AND NOT `Intl.DisplayNames` AT RUNTIME: these strings
5
+ * are persisted. They become `definition.options[].value` on the column and the
6
+ * literal text stored in every cell, so deriving them from ICU would let a Node
7
+ * upgrade silently rename an option and orphan every row that already carried
8
+ * the old spelling. The table was GENERATED from CLDR English region names once
9
+ * (the 249 officially assigned ISO 3166-1 alpha-2 codes, plus XK/Kosovo, which
10
+ * LinkedIn lists and ISO does not), then frozen here with the overrides below.
11
+ *
12
+ * NAMING: LinkedIn spells a handful differently from CLDR's English short name
13
+ * — those are the entries carrying a trailing comment. Everything else is the
14
+ * CLDR name verbatim, which is what LinkedIn uses. This is a faithful
15
+ * reproduction of LinkedIn's vocabulary, not a scrape of its dropdown: the
16
+ * RESOLVER below is what makes that safe, because it accepts the alpha-2 code,
17
+ * any spelling in this table, and the aliases in COUNTRY_ALIASES. A scrape that
18
+ * returns "US", "United States", "USA" or "Congo - Kinshasa" all land on the
19
+ * one canonical option rather than on an empty cell.
20
+ */
21
+ /** Canonical LinkedIn country name for each ISO 3166-1 alpha-2 code, A→Z by name. */
22
+ export declare const LINKEDIN_COUNTRIES: ReadonlyArray<readonly [code: string, name: string]>;
23
+ /**
24
+ * Resolve a provider value — an alpha-2 code, a canonical name, or a known
25
+ * alias — to its canonical LinkedIn country name.
26
+ *
27
+ * Returns null for anything unrecognized, which is deliberate: the column is a
28
+ * select, so writing a value that is not one of its options would produce a
29
+ * cell no filter can match and no dropdown can show. An empty cell is the
30
+ * honest outcome, and the raw payload is still on the record.
31
+ */
32
+ export declare function resolveLinkedInCountry(value: unknown): string | null;
33
+ export { LINKEDIN_COUNTRY_PRIMARY_TIME_ZONES, linkedInCountryPrimaryTimeZone, } from "./linkedin-country-timezones.js";