@opengeni/observability 0.8.29 → 0.8.30

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.
package/dist/index.js CHANGED
@@ -1,9 +1,280 @@
1
1
  // src/index.ts
2
- import { createHash } from "crypto";
2
+ import { createHash as createHash3 } from "crypto";
3
3
  import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from "prom-client";
4
4
  import { SandboxBackend } from "@opengeni/contracts";
5
+
6
+ // src/trace-context.ts
7
+ import { AsyncLocalStorage } from "async_hooks";
8
+ import { createHash } from "crypto";
9
+ var storage = new AsyncLocalStorage();
10
+ function ownData(value, key) {
11
+ if (typeof value !== "object" || value === null) return void 0;
12
+ try {
13
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
14
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
19
+ function validTraceContext(value) {
20
+ const traceId = ownData(value, "traceId");
21
+ const spanId = ownData(value, "spanId");
22
+ if (typeof traceId !== "string" || typeof spanId !== "string") return void 0;
23
+ if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return void 0;
24
+ if (!/^[0-9a-f]{16}$/.test(spanId) || /^0+$/.test(spanId)) return void 0;
25
+ return { traceId, spanId };
26
+ }
27
+ function currentTraceContext() {
28
+ return validTraceContext(storage.getStore());
29
+ }
30
+ function withTraceContext(context, run) {
31
+ const valid = validTraceContext(context);
32
+ const addLink = ownData(context, "addLink");
33
+ return storage.run(
34
+ valid ? {
35
+ ...valid,
36
+ addLink: typeof addLink === "function" ? addLink : void 0
37
+ } : void 0,
38
+ run
39
+ );
40
+ }
41
+ function admissionTraceContext(eventId) {
42
+ if (typeof eventId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(eventId))
43
+ return void 0;
44
+ const digest = createHash("sha256").update("opengeni:accepted-event-trace:v1\0").update(eventId.toLowerCase()).digest("hex");
45
+ return validTraceContext({ traceId: digest.slice(0, 32), spanId: digest.slice(32, 48) });
46
+ }
47
+ function linkCurrentSpanToAdmission(eventId) {
48
+ const link = admissionTraceContext(eventId);
49
+ if (link) storage.getStore()?.addLink?.(link);
50
+ }
51
+ function parseTraceparent(value) {
52
+ if (typeof value !== "string") return void 0;
53
+ const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/.exec(value ?? "");
54
+ return match ? validTraceContext({ traceId: match[1], spanId: match[2] }) : void 0;
55
+ }
56
+ function traceparent(context) {
57
+ const valid = validTraceContext(context);
58
+ return valid ? `00-${valid.traceId}-${valid.spanId}-01` : void 0;
59
+ }
60
+
61
+ // src/export-queue.ts
62
+ var ExportQueue = class {
63
+ constructor(observer, capacity = 256) {
64
+ this.observer = observer;
65
+ this.capacity = capacity;
66
+ }
67
+ queue = [];
68
+ running;
69
+ observe(outcome) {
70
+ try {
71
+ this.observer(outcome);
72
+ } catch {
73
+ }
74
+ }
75
+ enqueue(send) {
76
+ if (this.queue.length >= this.capacity) {
77
+ this.observe("dropped");
78
+ return;
79
+ }
80
+ this.queue.push(send);
81
+ this.start();
82
+ }
83
+ start() {
84
+ if (!this.running) {
85
+ this.running = Promise.resolve().then(() => this.drain()).finally(() => {
86
+ this.running = void 0;
87
+ if (this.queue.length) this.start();
88
+ });
89
+ }
90
+ }
91
+ async drain() {
92
+ while (this.queue.length) {
93
+ const send = this.queue.shift();
94
+ for (let attempt = 0; attempt < 3; attempt++) {
95
+ try {
96
+ await send();
97
+ this.observe("exported");
98
+ break;
99
+ } catch {
100
+ if (attempt === 2) this.observe("failed");
101
+ else {
102
+ this.observe("retried");
103
+ await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** attempt));
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ async flush(timeoutMs = 1e3) {
110
+ if (!this.running) return;
111
+ let timer;
112
+ try {
113
+ await Promise.race([
114
+ this.running,
115
+ new Promise((resolve) => {
116
+ timer = setTimeout(
117
+ resolve,
118
+ Number.isFinite(timeoutMs) ? Math.max(1, Math.min(timeoutMs, 5e3)) : 1e3
119
+ );
120
+ })
121
+ ]);
122
+ } finally {
123
+ clearTimeout(timer);
124
+ }
125
+ }
126
+ };
127
+
128
+ // src/failure-diagnostic.ts
129
+ import { createHash as createHash2, randomUUID } from "crypto";
130
+ import { SessionEventType } from "@opengeni/contracts";
131
+ var CODES = [
132
+ "api_startup_failed",
133
+ "api_unhandled_rejection",
134
+ "api_uncaught_exception",
135
+ "db_deadlock",
136
+ "db_serialization_failure",
137
+ "db_failure",
138
+ "retained_process_fenced",
139
+ "retained_process_proof_failed"
140
+ ];
141
+ var STAGES = [
142
+ "startup",
143
+ "running",
144
+ "session_events.append_generic",
145
+ "session_events.append_for_turn_attempt",
146
+ "preclaim",
147
+ "session_attempts.claim",
148
+ "failure_settlement",
149
+ "sandbox_retained_processes.proof"
150
+ ];
151
+ var RETRIES = ["not_retryable", "exhausted", "unknown"];
152
+ var CONSTRAINTS = /* @__PURE__ */ new Set([
153
+ "session_events_workspace_account_fk",
154
+ "session_events_turn_association_check",
155
+ "session_events_payload_bytes_check",
156
+ "session_events_type_bytes_check",
157
+ "session_events_duplicate_classification_check",
158
+ "sandbox_retained_processes_reconcile_proof_check",
159
+ "sandbox_retained_processes_reconcile_claim_check",
160
+ "sandbox_retained_processes_settlement_check"
161
+ ]);
162
+ var ERROR_NAMES = /* @__PURE__ */ new Set([
163
+ "Error",
164
+ "TypeError",
165
+ "RangeError",
166
+ "AggregateError",
167
+ "PostgresError",
168
+ "DrizzleQueryError",
169
+ "SessionEventPersistenceError",
170
+ "SandboxWorkspaceMutationFencedError"
171
+ ]);
172
+ var DIAGNOSTIC_SOURCE_FILES = [
173
+ "apps/api/src/index.ts",
174
+ "apps/api/src/app.ts",
175
+ "apps/api/src/fatal-process-boundary.ts",
176
+ "apps/api/dist/process/index.js",
177
+ "apps/worker/src/activities/agent-turn/run.ts",
178
+ "apps/worker/src/activities/agent-turn/claim.ts",
179
+ "apps/worker/src/activities/agent-turn/failure-settlement.ts",
180
+ "apps/worker/src/activities/sandbox-lease.ts",
181
+ "apps/worker/dist/process/index.js",
182
+ "packages/db/src/index.ts",
183
+ "packages/db/src/persistence-errors.ts",
184
+ "packages/storage/src/index.ts",
185
+ "packages/observability/src/index.ts"
186
+ ];
187
+ var DIAGNOSTIC_POSTGRES_FUNCTIONS = ["admit_session_attempt_personal_resources"];
188
+ function reviewedSource(location) {
189
+ return DIAGNOSTIC_SOURCE_FILES.find((file) => location === file || location.endsWith(`/${file}`));
190
+ }
191
+ function postgresContext(error) {
192
+ if (errorKind(error) !== "PostgresError") return [];
193
+ const where = own(error, "where");
194
+ if (typeof where !== "string") return [];
195
+ return where.slice(0, 8192).split("\n").slice(0, 16).flatMap((value) => {
196
+ const match = /^PL\/pgSQL function (?:public\.)?([a-z_]+)\([^\n]*\) line (\d{1,7}) at /.exec(
197
+ value
198
+ );
199
+ if (!match || !DIAGNOSTIC_POSTGRES_FUNCTIONS.some((name) => name === match[1])) return [];
200
+ return [{ functionName: match[1], line: Number(match[2]) }];
201
+ }).slice(0, 8);
202
+ }
203
+ function own(value, key) {
204
+ try {
205
+ if (!value || typeof value !== "object") return void 0;
206
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
207
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
208
+ } catch {
209
+ return void 0;
210
+ }
211
+ }
212
+ function errorKind(error) {
213
+ const name = own(error, "name");
214
+ if (typeof name === "string" && ERROR_NAMES.has(name)) return name;
215
+ try {
216
+ const prototype = Object.getPrototypeOf(error);
217
+ if (prototype === TypeError.prototype) return "TypeError";
218
+ if (prototype === RangeError.prototype) return "RangeError";
219
+ if (prototype === AggregateError.prototype) return "AggregateError";
220
+ } catch {
221
+ }
222
+ return "Error";
223
+ }
224
+ function failureDiagnostic(input, revision) {
225
+ const causes = [];
226
+ const pgContext = [];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ let error = input.error;
229
+ while (error && typeof error === "object" && !seen.has(error) && causes.length < 4) {
230
+ seen.add(error);
231
+ pgContext.push(...postgresContext(error).slice(0, 8 - pgContext.length));
232
+ const stack = own(error, "stack");
233
+ const frames = typeof stack === "string" ? stack.slice(0, 16384).split("\n").slice(1, 33).flatMap((frame) => {
234
+ const match = /(?:\(|\s)([^\s()]+):(\d{1,7}):(\d{1,7})\)?$/.exec(frame);
235
+ if (!match) return [];
236
+ return [
237
+ {
238
+ locationHash: createHash2("sha256").update(match[1]).digest("hex"),
239
+ ...reviewedSource(match[1]) ? { source: reviewedSource(match[1]) } : {},
240
+ line: Number(match[2]),
241
+ column: Number(match[3])
242
+ }
243
+ ];
244
+ }) : [];
245
+ causes.push({
246
+ kind: errorKind(error),
247
+ frames
248
+ });
249
+ error = own(error, "cause");
250
+ }
251
+ const uuid = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value : void 0;
252
+ return {
253
+ schema: "opengeni.failure-diagnostic.v1",
254
+ diagnosticId: randomUUID(),
255
+ code: CODES.includes(input.code) ? input.code : "db_failure",
256
+ stage: STAGES.includes(input.stage) ? input.stage : "failure_settlement",
257
+ retryDecision: input.retryDecision && RETRIES.includes(input.retryDecision) ? input.retryDecision : "unknown",
258
+ attemptId: uuid(input.attemptId),
259
+ sessionId: uuid(input.sessionId),
260
+ processId: uuid(input.processId),
261
+ turnId: uuid(input.turnId),
262
+ attempts: Number.isSafeInteger(input.attempts) && input.attempts >= 0 && input.attempts <= 1e6 ? input.attempts : void 0,
263
+ sqlState: typeof input.sqlState === "string" && /^[0-9A-Z]{5}$/.test(input.sqlState) ? input.sqlState : void 0,
264
+ constraint: input.constraint && CONSTRAINTS.has(input.constraint) ? input.constraint : void 0,
265
+ deploymentRevision: revision && /^[0-9a-f]{40}$/.test(revision) ? revision : void 0,
266
+ causes,
267
+ postgresContext: pgContext,
268
+ eventTypes: (input.eventTypes ?? []).slice(0, 32).filter((type) => SessionEventType.safeParse(type).success)
269
+ };
270
+ }
271
+
272
+ // src/index.ts
5
273
  function sandboxLeaseTelemetryKey(workspaceId, sandboxGroupId) {
6
- return `slk_${createHash("sha256").update("opengeni:sandbox-lease-telemetry:v1\0").update(workspaceId).update("\0").update(sandboxGroupId).digest("hex").slice(0, 32)}`;
274
+ return `slk_${createHash3("sha256").update("opengeni:sandbox-lease-telemetry:v1\0").update(workspaceId).update("\0").update(sandboxGroupId).digest("hex").slice(0, 32)}`;
275
+ }
276
+ function turnExecutionTelemetryKey(workspaceId, sessionId, attemptId) {
277
+ return `turn_${createHash3("sha256").update("opengeni:turn-execution-telemetry:v1\0").update(workspaceId).update("\0").update(sessionId).update("\0").update(attemptId).digest("hex").slice(0, 32)}`;
7
278
  }
8
279
  var OPENGENI_OBSERVABILITY_DISTRIBUTION = {
9
280
  monitoringNamespaceLabel: "opengeni.ai/monitoring",
@@ -51,26 +322,6 @@ var SANDBOX_OPERATION_NAMES = /* @__PURE__ */ new Set([
51
322
  "serializeSessionState"
52
323
  ]);
53
324
  var TENANCY_COMPATIBILITY_LANES = [
54
- /**
55
- * A provider connection carrying `authority_scope = 'legacy_user'` was
56
- * resolved for accepted use: a personal row with no common authority or
57
- * grant, admitted through `legacy_user_compatibility` provenance instead of
58
- * an explicit delegation. Migration 0256 backfilled the existing rows, but
59
- * its `bind_connection_authority` trigger still classifies a NEW personal
60
- * connection this way whenever the inserting subject has no active
61
- * organization membership, so this counter is expected to stay above zero
62
- * until that classification changes. That is the finding, not a defect.
63
- */
64
- "connection_legacy_user",
65
- /**
66
- * A workspace-scope connection ref carried no connection id, so the
67
- * accepted-use authority (migration 0279) could not identify the exact row
68
- * and the request fell back to the unprivileged pre-snapshot resolution.
69
- * This lane writes no `connection_use_audit_facts` row and leaves no durable
70
- * trace of any kind, so a use counter is the only possible evidence it was
71
- * taken at all.
72
- */
73
- "connection_pre_snapshot_ref",
74
325
  /**
75
326
  * A NEW persistable `/workspace` mutation was refused because the writer
76
327
  * behind it has no recorded authority (`authority_unattributed`): either a
@@ -376,6 +627,17 @@ var PUBLIC_TELEMETRY_ERROR_ORIGINS = /* @__PURE__ */ new Set([
376
627
  "worker",
377
628
  "worker-lifecycle"
378
629
  ]);
630
+ var MATERIALIZATION_FAILURE_REASONS = /* @__PURE__ */ new Set([
631
+ "path_not_visible",
632
+ "command_failed",
633
+ "command_pending",
634
+ "invalid_response",
635
+ "command_error"
636
+ ]);
637
+ var MATERIALIZATION_FAILURE_METRIC = {
638
+ name: "opengeni_sandbox_materialization_verification_failures_total",
639
+ help: "Sandbox destination visibility-check failures by bounded reason; exact command evidence is retained in the session failure event."
640
+ };
379
641
  function createObservability(settings, options) {
380
642
  return new Observability(settings, options);
381
643
  }
@@ -409,14 +671,54 @@ var Observability = class {
409
671
  });
410
672
  this.registerSandboxRolloutConfig();
411
673
  this.registerTenancyCompatibilityLanes();
674
+ if (["worker", "worker-control", "worker-turn", "api"].includes(options.component)) {
675
+ for (const backend of [...SANDBOX_OPERATION_BACKENDS, "unknown"]) {
676
+ for (const reason of [...MATERIALIZATION_FAILURE_REASONS, "unknown"]) {
677
+ this.incrementCounter({
678
+ ...MATERIALIZATION_FAILURE_METRIC,
679
+ labels: { backend, reason },
680
+ amount: 0
681
+ });
682
+ }
683
+ }
684
+ }
685
+ if (options.component === "worker" || options.component === "worker-control") {
686
+ for (const outcome of WORKFLOW_WAKE_OUTCOMES) {
687
+ this.incrementCounter({ ...WORKFLOW_WAKE_METRIC, labels: { outcome }, amount: 0 });
688
+ }
689
+ for (const reason of [...WORKFLOW_WAKE_ADMISSION_BLOCKERS, "unknown"]) {
690
+ this.incrementCounter({ ...WORKFLOW_WAKE_BLOCKER_METRIC, labels: { reason }, amount: 0 });
691
+ }
692
+ }
412
693
  }
413
694
  }
695
+ spanBatch = [];
696
+ spanBatchScheduled = false;
697
+ diagnosticQueue = new ExportQueue((outcome) => {
698
+ this.incrementCounter({
699
+ name: "opengeni_diagnostic_exports_total",
700
+ help: "Protected diagnostic export outcomes.",
701
+ labels: { outcome }
702
+ });
703
+ });
414
704
  registry = new Registry();
415
705
  counters = /* @__PURE__ */ new Map();
416
706
  gauges = /* @__PURE__ */ new Map();
417
707
  histograms = /* @__PURE__ */ new Map();
418
708
  registrations = /* @__PURE__ */ new Map();
419
- pendingExports = /* @__PURE__ */ new Set();
709
+ exportQueue = new ExportQueue((outcome) => {
710
+ this.incrementCounter({
711
+ name: "opengeni_telemetry_exports_total",
712
+ help: "Bounded telemetry export outcomes.",
713
+ labels: { outcome }
714
+ });
715
+ if (outcome === "failed")
716
+ this.warn("OTLP span export failed", {
717
+ errorClass: "TelemetryExportError",
718
+ errorCode: "otlp_export_failed",
719
+ origin: "observability"
720
+ });
721
+ }, 8);
420
722
  now;
421
723
  exporter;
422
724
  resourceAttributes;
@@ -479,6 +781,7 @@ var Observability = class {
479
781
  }
480
782
  return;
481
783
  }
784
+ const context = currentTraceContext();
482
785
  const record = {
483
786
  timestamp: new Date(this.now()).toISOString(),
484
787
  level,
@@ -486,7 +789,8 @@ var Observability = class {
486
789
  service: this.settings.serviceName,
487
790
  environment: this.settings.environment,
488
791
  component: this.options.component,
489
- ...cleanAttributes(publicAttributes)
792
+ ...cleanAttributes(publicAttributes),
793
+ ...context ? { traceId: context.traceId, spanId: context.spanId } : {}
490
794
  };
491
795
  const serialized = JSON.stringify(record);
492
796
  if (level === "warn") {
@@ -497,14 +801,25 @@ var Observability = class {
497
801
  console.log(serialized);
498
802
  }
499
803
  }
500
- startSpan(name, attributes = {}) {
501
- const traceId = randomHex(16);
804
+ startSpan(name, attributes = {}, options = {}) {
805
+ const parent = options.parent === null ? void 0 : validTraceContext(options.parent ?? currentTraceContext());
806
+ const links = (options.links ?? []).slice(0, 8).flatMap((link) => {
807
+ const valid = validTraceContext(link);
808
+ return valid ? [valid] : [];
809
+ });
810
+ const traceId = parent?.traceId ?? randomHex(16);
502
811
  const spanId = randomHex(8);
503
- const startMs = this.now();
812
+ const now = this.now();
813
+ const startMs = Number.isFinite(options.startTimeMs) ? Math.min(now, Math.max(0, options.startTimeMs)) : now;
504
814
  let ended = false;
505
815
  return {
506
816
  traceId,
507
817
  spanId,
818
+ addLink: (context) => {
819
+ const valid = validTraceContext(context);
820
+ if (!ended && valid && links.length < 8 && !links.some((link) => link.traceId === valid.traceId && link.spanId === valid.spanId))
821
+ links.push(valid);
822
+ },
508
823
  end: (input = {}) => {
509
824
  if (ended) {
510
825
  return;
@@ -515,7 +830,9 @@ var Observability = class {
515
830
  this.exportSpan({
516
831
  traceId,
517
832
  spanId,
518
- name,
833
+ ...parent ? { parentSpanId: parent.spanId } : {},
834
+ links,
835
+ name: boundedOtlpString(name),
519
836
  startMs,
520
837
  endMs: this.now(),
521
838
  attributes: {
@@ -621,11 +938,68 @@ var Observability = class {
621
938
  }
622
939
  return await this.registry.metrics();
623
940
  }
624
- /** Wait for every OTLP export accepted before or during this drain. */
625
- async flush() {
626
- while (this.pendingExports.size > 0) {
627
- await Promise.allSettled([...this.pendingExports]);
941
+ /** Emit a content-free anchor after successful, non-replayed admission. */
942
+ recordAdmissionTrace(eventId) {
943
+ const identity = admissionTraceContext(eventId);
944
+ if (!identity) return;
945
+ const current = currentTraceContext();
946
+ const now = this.now();
947
+ this.exportSpan({
948
+ ...identity,
949
+ name: "api.turn.admitted",
950
+ startMs: now,
951
+ endMs: now,
952
+ attributes: {},
953
+ links: current ? [current] : []
954
+ });
955
+ }
956
+ /** Emit a closed protected record independently of application persistence. */
957
+ recordFailureDiagnostic(input) {
958
+ const record = failureDiagnostic(input, this.settings.deploymentRevision);
959
+ const endpoint = this.settings.observabilityDiagnosticsEndpoint;
960
+ if (!endpoint) {
961
+ this.incrementCounter({
962
+ name: "opengeni_diagnostic_exports_total",
963
+ help: "Protected diagnostic export outcomes.",
964
+ labels: { outcome: "disabled" }
965
+ });
966
+ return record.diagnosticId;
628
967
  }
968
+ const context = currentTraceContext();
969
+ const body = {
970
+ resourceLogs: [
971
+ {
972
+ resource: { attributes: otlpAttributes(this.resourceAttributes) },
973
+ scopeLogs: [
974
+ {
975
+ scope: { name: "@opengeni/observability/diagnostics", version: "1" },
976
+ logRecords: [
977
+ {
978
+ timeUnixNano: millisToNanos(this.now()),
979
+ severityNumber: 17,
980
+ severityText: "ERROR",
981
+ ...context ? { traceId: context.traceId, spanId: context.spanId } : {},
982
+ body: { stringValue: JSON.stringify(record) }
983
+ }
984
+ ]
985
+ }
986
+ ]
987
+ }
988
+ ]
989
+ };
990
+ this.diagnosticQueue.enqueue(
991
+ () => this.exporter(
992
+ `${endpoint.replace(/\/$/, "")}/v1/logs`,
993
+ body,
994
+ parseHeaders(this.settings.observabilityDiagnosticsHeaders ?? "")
995
+ )
996
+ );
997
+ return record.diagnosticId;
998
+ }
999
+ /** Best-effort bounded drain, including when a custom transport never settles. */
1000
+ async flush(timeoutMs = 1e3) {
1001
+ this.submitSpanBatch();
1002
+ await Promise.all([this.exportQueue.flush(timeoutMs), this.diagnosticQueue.flush(timeoutMs)]);
629
1003
  }
630
1004
  counter(name, help, labelNames) {
631
1005
  const existing = this.counters.get(name);
@@ -677,7 +1051,6 @@ var Observability = class {
677
1051
  if (!this.settings.observabilityOtlpEndpoint) {
678
1052
  return;
679
1053
  }
680
- const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
681
1054
  const body = {
682
1055
  resourceSpans: [
683
1056
  {
@@ -694,6 +1067,8 @@ var Observability = class {
694
1067
  {
695
1068
  traceId: span.traceId,
696
1069
  spanId: span.spanId,
1070
+ ...span.parentSpanId ? { parentSpanId: span.parentSpanId } : {},
1071
+ links: span.links,
697
1072
  name: span.name,
698
1073
  kind: 1,
699
1074
  startTimeUnixNano: millisToNanos(span.startMs),
@@ -707,19 +1082,28 @@ var Observability = class {
707
1082
  }
708
1083
  ]
709
1084
  };
710
- let pendingExport;
711
- pendingExport = Promise.resolve().then(
712
- () => this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders))
713
- ).catch(() => {
714
- this.warn("OTLP span export failed", {
715
- errorClass: "TelemetryExportError",
716
- errorCode: "otlp_export_failed",
717
- origin: "observability"
1085
+ this.spanBatch.push(body.resourceSpans[0]);
1086
+ if (!this.spanBatchScheduled) {
1087
+ this.spanBatchScheduled = true;
1088
+ queueMicrotask(() => {
1089
+ this.spanBatchScheduled = false;
1090
+ this.submitSpanBatch();
718
1091
  });
719
- }).finally(() => {
720
- this.pendingExports.delete(pendingExport);
721
- });
722
- this.pendingExports.add(pendingExport);
1092
+ }
1093
+ if (this.spanBatch.length >= 32) this.submitSpanBatch();
1094
+ }
1095
+ submitSpanBatch() {
1096
+ if (!this.spanBatch.length || !this.settings.observabilityOtlpEndpoint) return;
1097
+ const resourceSpans = this.spanBatch;
1098
+ this.spanBatch = [];
1099
+ const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
1100
+ this.exportQueue.enqueue(
1101
+ () => this.exporter(
1102
+ endpoint,
1103
+ { resourceSpans },
1104
+ parseHeaders(this.settings.observabilityOtlpHeaders)
1105
+ )
1106
+ );
723
1107
  }
724
1108
  };
725
1109
  function sandboxOperationMetricObserver(observability) {
@@ -738,6 +1122,13 @@ function sandboxOperationMetricObserver(observability) {
738
1122
  labels: { backend, op },
739
1123
  value: Math.max(0, observation.durationMs) / 1e3
740
1124
  });
1125
+ if (op === "materializeEntry" && observation.outcome === "failed" && observation.materializationFailureReason) {
1126
+ const reason = MATERIALIZATION_FAILURE_REASONS.has(observation.materializationFailureReason) ? observation.materializationFailureReason : "unknown";
1127
+ observability.incrementCounter({
1128
+ ...MATERIALIZATION_FAILURE_METRIC,
1129
+ labels: { backend, reason }
1130
+ });
1131
+ }
741
1132
  } catch {
742
1133
  try {
743
1134
  observability.incrementCounter({
@@ -750,6 +1141,32 @@ function sandboxOperationMetricObserver(observability) {
750
1141
  }
751
1142
  };
752
1143
  }
1144
+ function sandboxCaptureWaitMetricObserver(observability) {
1145
+ return (observation) => {
1146
+ const stage = observation.captureWaitStage;
1147
+ if (stage !== "admission" && stage !== "provider") return;
1148
+ if (!Number.isFinite(observation.durationMs) || observation.durationMs < 0) return;
1149
+ const backend = SANDBOX_OPERATION_BACKENDS.has(observation.backend) ? observation.backend : "unknown";
1150
+ const op = SANDBOX_OPERATION_NAMES.has(observation.op) ? observation.op : "unknown";
1151
+ const outcome = observation.outcome === "ok" ? "completed" : "failed";
1152
+ try {
1153
+ observability.observeHistogram({
1154
+ name: "opengeni_sandbox_capture_wait_duration_seconds",
1155
+ help: "Workspace capture gate wait duration across every routed operation, not only startup.",
1156
+ labels: { backend, op, stage, outcome },
1157
+ value: observation.durationMs / 1e3
1158
+ });
1159
+ observability.startSpan(
1160
+ `sandbox.capture_wait.${stage}`,
1161
+ { backend, outcome },
1162
+ {
1163
+ startTimeMs: Date.now() - observation.durationMs
1164
+ }
1165
+ ).end({ ...outcome === "failed" ? { error: true } : {} });
1166
+ } catch {
1167
+ }
1168
+ };
1169
+ }
753
1170
  function interactionOperationMetricObserver(observability) {
754
1171
  if (!observability) return () => void 0;
755
1172
  return (observation) => {
@@ -858,6 +1275,70 @@ function workspaceInsightsMetricObserver(observability) {
858
1275
  function boundedMetricEnum(allowed, value) {
859
1276
  return allowed.has(value) ? value : "unknown";
860
1277
  }
1278
+ var WORKFLOW_WAKE_ADMISSION_BLOCKERS = /* @__PURE__ */ new Set([
1279
+ "pending_agent_steer",
1280
+ "pending_prompt_turn",
1281
+ "pending_quiescence",
1282
+ "pending_machine_input",
1283
+ "pending_input_wait"
1284
+ ]);
1285
+ var WORKFLOW_WAKE_OUTCOMES = [
1286
+ "signal_accepted",
1287
+ "acknowledged",
1288
+ "pending_admission",
1289
+ "unconfirmed",
1290
+ "failed"
1291
+ ];
1292
+ var WORKFLOW_WAKE_METRIC = {
1293
+ name: "opengeni_session_workflow_wake_observations_total",
1294
+ help: "Workflow wake dispatch observations; signal acceptance overlaps with admission outcomes, and retries are counted again."
1295
+ };
1296
+ var WORKFLOW_WAKE_BLOCKER_METRIC = {
1297
+ name: "opengeni_session_workflow_wake_admission_blockers_total",
1298
+ help: "Workflow wake dispatch attempts awaiting durable admission, by bounded blocker; repeated revisions count again."
1299
+ };
1300
+ function recordWorkflowWakeReconciliation(observability, result) {
1301
+ const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
1302
+ try {
1303
+ for (const [outcome, amount] of [
1304
+ ["signal_accepted", result.signaled],
1305
+ ["acknowledged", result.delivered],
1306
+ ["pending_admission", result.pendingAdmission],
1307
+ ["unconfirmed", result.unconfirmed],
1308
+ ["failed", result.failed]
1309
+ ]) {
1310
+ observability.incrementCounter({
1311
+ ...WORKFLOW_WAKE_METRIC,
1312
+ labels: { outcome },
1313
+ amount: count(amount)
1314
+ });
1315
+ if (count(amount) > 0) {
1316
+ observability.info("Session workflow wake dispatch observed", {
1317
+ surface: "session_workflow_wake",
1318
+ outcome,
1319
+ attempts: count(amount)
1320
+ });
1321
+ }
1322
+ }
1323
+ for (const [blocker, amount] of Object.entries(result.pendingAdmissionBlockers)) {
1324
+ if (count(amount) === 0) continue;
1325
+ const reason = boundedMetricEnum(WORKFLOW_WAKE_ADMISSION_BLOCKERS, blocker);
1326
+ observability.incrementCounter({
1327
+ ...WORKFLOW_WAKE_BLOCKER_METRIC,
1328
+ labels: { reason },
1329
+ amount: count(amount)
1330
+ });
1331
+ observability.info("Session workflow wake admission pending", {
1332
+ surface: "session_workflow_wake",
1333
+ outcome: "pending_admission",
1334
+ reason,
1335
+ attempts: count(amount)
1336
+ });
1337
+ }
1338
+ } catch {
1339
+ recordObserverFailure(observability, "session_workflow_wake");
1340
+ }
1341
+ }
861
1342
  function boundedMetricDuration(durationMs) {
862
1343
  return Number.isFinite(durationMs) ? Math.max(0, durationMs) / 1e3 : 0;
863
1344
  }
@@ -874,7 +1355,7 @@ function recordObserverFailure(observability, observer) {
874
1355
  function logStartupDependencyRetry(observability, event) {
875
1356
  const dependency = PUBLIC_STARTUP_DEPENDENCIES.has(event.label) ? event.label : "Dependency";
876
1357
  observability.warn(
877
- `Startup dependency ${dependency} connection failed; retrying (${event.attempt}/${event.attempts} in ${event.delayMs}ms)`,
1358
+ `Startup dependency ${dependency} check failed; retrying (${event.attempt}/${event.attempts} in ${event.delayMs}ms)`,
878
1359
  {
879
1360
  dependency,
880
1361
  attempt: event.attempt,
@@ -905,6 +1386,7 @@ function projectPublicTelemetryAttributes(attributes) {
905
1386
  ...projectStartupDependencyAttributes(attributes),
906
1387
  ...projectPublicChannelADiagnosticAttributes(attributes),
907
1388
  ...projectApiFatalDiagnosticAttributes(attributes),
1389
+ ...projectSnapshotDiagnosticAttributes(attributes),
908
1390
  ...projectPublicDiagnosticAttributes(attributes)
909
1391
  };
910
1392
  }
@@ -958,6 +1440,42 @@ function projectPublicChannelADiagnosticAttributes(attributes) {
958
1440
  ...typeof sandboxLeaseKey === "string" && PUBLIC_TELEMETRY_OPAQUE_ATTRIBUTE_PATTERNS.get("sandboxLeaseKey")?.test(sandboxLeaseKey) ? { sandboxLeaseKey } : {}
959
1441
  };
960
1442
  }
1443
+ function projectSnapshotDiagnosticAttributes(attributes) {
1444
+ if (attributes.errorClass !== "SnapshotOperationError") return {};
1445
+ const projected = {};
1446
+ const causeName = attributes.causeName;
1447
+ if (typeof causeName === "string" && ["SnapshotTimeoutError", "SandboxProviderCaptureTimeoutError", "SandboxProviderError"].includes(
1448
+ causeName
1449
+ ))
1450
+ projected.causeName = causeName;
1451
+ const name = attributes.providerErrorName;
1452
+ if (typeof name === "string" && [
1453
+ "ClientError",
1454
+ "TimeoutError",
1455
+ "ConnectionError",
1456
+ "AuthError",
1457
+ "NotFoundError",
1458
+ "InvalidError",
1459
+ "RemoteError",
1460
+ "AbortError"
1461
+ ].includes(name))
1462
+ projected.providerErrorName = name;
1463
+ const grpc = attributes.providerGrpcCode;
1464
+ if (name === "ClientError" && typeof grpc === "number" && Number.isInteger(grpc) && grpc >= 0 && grpc <= 16)
1465
+ projected.providerGrpcCode = grpc;
1466
+ const http = attributes.providerHttpStatus;
1467
+ if (typeof http === "number" && Number.isInteger(http) && http >= 100 && http <= 599)
1468
+ projected.providerHttpStatus = http;
1469
+ if (typeof attributes.providerRetryable === "boolean")
1470
+ projected.providerRetryable = attributes.providerRetryable;
1471
+ const leaseKey = attributes.sandboxLeaseKey;
1472
+ if (typeof leaseKey === "string" && /^slk_[0-9a-f]{32}$/.test(leaseKey))
1473
+ projected.sandboxLeaseKey = leaseKey;
1474
+ const epoch = attributes.leaseEpoch;
1475
+ if (typeof epoch === "number" && Number.isSafeInteger(epoch) && epoch >= 0)
1476
+ projected.leaseEpoch = epoch;
1477
+ return projected;
1478
+ }
961
1479
  function projectPublicDiagnosticAttributes(attributes) {
962
1480
  const errorClass = attributes.errorClass;
963
1481
  const errorCode = attributes.errorCode;
@@ -1043,7 +1561,8 @@ async function defaultExporter(url, body, headers) {
1043
1561
  "content-type": "application/json",
1044
1562
  ...headers
1045
1563
  },
1046
- body: JSON.stringify(body)
1564
+ body: JSON.stringify(body),
1565
+ signal: AbortSignal.timeout(1e3)
1047
1566
  });
1048
1567
  if (!response.ok) {
1049
1568
  throw new Error(`OTLP endpoint returned HTTP ${response.status}`);
@@ -1053,15 +1572,24 @@ export {
1053
1572
  OPENGENI_OBSERVABILITY_DISTRIBUTION,
1054
1573
  Observability,
1055
1574
  TENANCY_COMPATIBILITY_LANES,
1575
+ admissionTraceContext,
1056
1576
  createObservability,
1577
+ currentTraceContext,
1057
1578
  interactionAuthMetricObserver,
1058
1579
  interactionInterventionMetricObserver,
1059
1580
  interactionOperationMetricObserver,
1581
+ linkCurrentSpanToAdmission,
1060
1582
  logStartupDependencyRetry,
1061
1583
  parseHeaders,
1584
+ parseTraceparent,
1062
1585
  recordTenancyCompatibilityLaneUse,
1586
+ recordWorkflowWakeReconciliation,
1587
+ sandboxCaptureWaitMetricObserver,
1063
1588
  sandboxLeaseTelemetryKey,
1064
1589
  sandboxOperationMetricObserver,
1590
+ traceparent,
1591
+ turnExecutionTelemetryKey,
1592
+ withTraceContext,
1065
1593
  workspaceInsightsMetricObserver
1066
1594
  };
1067
1595
  //# sourceMappingURL=index.js.map