bitfab 0.14.0 → 0.16.0

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.cjs CHANGED
@@ -39,25 +39,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  ));
40
40
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
41
41
 
42
- // src/version.generated.ts
43
- var __version__;
44
- var init_version_generated = __esm({
45
- "src/version.generated.ts"() {
46
- "use strict";
47
- __version__ = "0.14.0";
48
- }
49
- });
50
-
51
- // src/constants.ts
52
- var DEFAULT_SERVICE_URL;
53
- var init_constants = __esm({
54
- "src/constants.ts"() {
55
- "use strict";
56
- init_version_generated();
57
- DEFAULT_SERVICE_URL = "https://bitfab.ai";
58
- }
59
- });
60
-
61
42
  // src/errors.ts
62
43
  var BitfabError;
63
44
  var init_errors = __esm({
@@ -73,343 +54,6 @@ var init_errors = __esm({
73
54
  }
74
55
  });
75
56
 
76
- // src/http.ts
77
- function awaitOnExit(promise) {
78
- pendingTracePromises.add(promise);
79
- void promise.finally(() => {
80
- pendingTracePromises.delete(promise);
81
- }).catch(() => {
82
- });
83
- return promise;
84
- }
85
- async function flushTraces(timeoutMs = 5e3) {
86
- if (pendingTracePromises.size === 0) {
87
- return;
88
- }
89
- await Promise.race([
90
- Promise.allSettled(Array.from(pendingTracePromises)),
91
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
92
- ]);
93
- }
94
- var pendingTracePromises, HttpClient;
95
- var init_http = __esm({
96
- "src/http.ts"() {
97
- "use strict";
98
- init_constants();
99
- init_errors();
100
- pendingTracePromises = /* @__PURE__ */ new Set();
101
- if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
102
- let isFlushing = false;
103
- process.on("beforeExit", () => {
104
- if (pendingTracePromises.size > 0 && !isFlushing) {
105
- isFlushing = true;
106
- Promise.allSettled(
107
- Array.from(pendingTracePromises).map(
108
- (p) => p.catch(() => {
109
- })
110
- )
111
- ).then(() => {
112
- isFlushing = false;
113
- }).catch(() => {
114
- isFlushing = false;
115
- });
116
- }
117
- });
118
- }
119
- HttpClient = class {
120
- constructor(config) {
121
- this.apiKey = config.apiKey;
122
- this.serviceUrl = config.serviceUrl;
123
- this.timeout = config.timeout ?? 12e4;
124
- }
125
- /**
126
- * Make an HTTP request to the Bitfab API. Defaults to POST; pass
127
- * `options.method` to use a different verb (e.g. "PATCH").
128
- *
129
- * @param endpoint - The API endpoint (without base URL)
130
- * @param payload - The request body
131
- * @param options - Optional request options
132
- * @returns The parsed JSON response
133
- * @throws {BitfabError} If the request fails
134
- */
135
- async request(endpoint, payload, options) {
136
- const url = `${this.serviceUrl}${endpoint}`;
137
- const timeout = options?.timeout ?? this.timeout;
138
- const method = options?.method ?? "POST";
139
- const controller = new AbortController();
140
- const timeoutId = setTimeout(() => controller.abort(), timeout);
141
- let body;
142
- let serializationError;
143
- try {
144
- body = JSON.stringify(payload);
145
- } catch (error) {
146
- serializationError = error instanceof Error ? error.message : String(error);
147
- body = JSON.stringify({
148
- ...Object.fromEntries(
149
- Object.entries(payload).filter(
150
- ([, v]) => typeof v === "string" || typeof v === "number"
151
- )
152
- ),
153
- rawSpan: {},
154
- errors: [
155
- { source: "sdk", step: "json_serialize", error: serializationError }
156
- ]
157
- });
158
- }
159
- try {
160
- const response = await fetch(url, {
161
- method,
162
- headers: {
163
- "Content-Type": "application/json",
164
- Authorization: `Bearer ${this.apiKey}`
165
- },
166
- body,
167
- signal: controller.signal
168
- });
169
- if (!response.ok) {
170
- const errorText = await response.text();
171
- throw new BitfabError(
172
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
173
- );
174
- }
175
- const result = await response.json();
176
- if (result.error) {
177
- if (result.url) {
178
- throw new BitfabError(
179
- `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
180
- result.url
181
- );
182
- }
183
- throw new BitfabError(result.error);
184
- }
185
- return result;
186
- } catch (error) {
187
- if (error instanceof BitfabError) {
188
- throw error;
189
- }
190
- if (error instanceof Error) {
191
- if (error.name === "AbortError") {
192
- throw new BitfabError(`Request timed out after ${timeout}ms`);
193
- }
194
- throw new BitfabError(error.message);
195
- }
196
- throw new BitfabError("Unknown error occurred");
197
- } finally {
198
- clearTimeout(timeoutId);
199
- }
200
- }
201
- /**
202
- * Look up a function by name.
203
- * Blocks until complete - needed for function execution.
204
- */
205
- async lookupFunction(name) {
206
- return this.request("/api/sdk/functions/lookup", { name });
207
- }
208
- /**
209
- * Send an internal trace (from BAML execution).
210
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
211
- */
212
- sendInternalTrace(functionId, payload) {
213
- void awaitOnExit(
214
- this.request(`/api/sdk/functions/${functionId}/traces`, {
215
- ...payload,
216
- sdkVersion: __version__
217
- })
218
- ).catch((error) => {
219
- try {
220
- console.error("Bitfab: Failed to create trace:", error);
221
- } catch {
222
- }
223
- });
224
- }
225
- /**
226
- * Send an external span (from withSpan wrapper or OpenAI tracing).
227
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
228
- * Returns the tracked promise so callers can optionally await it.
229
- */
230
- sendExternalSpan(payload) {
231
- return awaitOnExit(
232
- this.request("/api/sdk/externalSpans", {
233
- ...payload,
234
- sdkVersion: __version__
235
- })
236
- ).catch((error) => {
237
- try {
238
- console.error("Bitfab: Failed to create external span:", error);
239
- } catch {
240
- }
241
- });
242
- }
243
- /**
244
- * Send an external trace (from OpenAI tracing).
245
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
246
- */
247
- sendExternalTrace(payload) {
248
- void awaitOnExit(
249
- this.request("/api/sdk/externalTraces", {
250
- ...payload,
251
- sdkVersion: __version__
252
- })
253
- ).catch((error) => {
254
- try {
255
- console.error("Bitfab: Failed to create external trace:", error);
256
- } catch {
257
- }
258
- });
259
- }
260
- /**
261
- * Partial update of an existing external trace identified by sourceTraceId.
262
- * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
263
- * returns a tracked promise that callers may optionally await.
264
- */
265
- patchTrace(sourceTraceId, payload) {
266
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
267
- return awaitOnExit(
268
- this.request(endpoint, payload, { method: "PATCH" })
269
- ).catch((error) => {
270
- try {
271
- console.error("Bitfab: Failed to patch trace:", error);
272
- } catch {
273
- }
274
- });
275
- }
276
- /**
277
- * Start a replay session by fetching historical traces.
278
- * Blocking call — creates a test run and returns lightweight item references.
279
- */
280
- async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
281
- const payload = { traceFunctionKey };
282
- if (limit !== void 0) {
283
- payload.limit = limit;
284
- }
285
- if (traceIds) {
286
- payload.traceIds = traceIds;
287
- }
288
- if (codeChangeDescription !== void 0) {
289
- payload.codeChangeDescription = codeChangeDescription;
290
- }
291
- if (codeChangeFiles !== void 0) {
292
- payload.codeChangeFiles = codeChangeFiles;
293
- }
294
- if (includeDbBranchLease) {
295
- payload.includeDbBranchLease = true;
296
- }
297
- if (experimentGroupId !== void 0) {
298
- payload.experimentGroupId = experimentGroupId;
299
- }
300
- const timeout = includeDbBranchLease ? 18e4 : 3e4;
301
- return this.request("/api/sdk/replay/start", payload, {
302
- timeout
303
- });
304
- }
305
- /**
306
- * Fetch an external span by ID.
307
- * Blocking GET request.
308
- */
309
- async getExternalSpan(spanId) {
310
- const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
311
- const controller = new AbortController();
312
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
313
- try {
314
- const response = await fetch(url, {
315
- method: "GET",
316
- headers: { Authorization: `Bearer ${this.apiKey}` },
317
- signal: controller.signal
318
- });
319
- if (!response.ok) {
320
- const errorText = await response.text();
321
- throw new BitfabError(
322
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
323
- );
324
- }
325
- return await response.json();
326
- } catch (error) {
327
- if (error instanceof BitfabError) {
328
- throw error;
329
- }
330
- if (error instanceof Error) {
331
- if (error.name === "AbortError") {
332
- throw new BitfabError("Request timed out after 30000ms");
333
- }
334
- throw new BitfabError(error.message);
335
- }
336
- throw new BitfabError("Unknown error occurred");
337
- } finally {
338
- clearTimeout(timeoutId);
339
- }
340
- }
341
- /**
342
- * Fetch the span tree for a root span.
343
- * Blocking GET request.
344
- */
345
- async getSpanTree(externalSpanId) {
346
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
347
- const controller = new AbortController();
348
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
349
- try {
350
- const response = await fetch(url, {
351
- method: "GET",
352
- headers: { Authorization: `Bearer ${this.apiKey}` },
353
- signal: controller.signal
354
- });
355
- if (!response.ok) {
356
- const errorText = await response.text();
357
- throw new BitfabError(
358
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
359
- );
360
- }
361
- return await response.json();
362
- } catch (error) {
363
- if (error instanceof BitfabError) {
364
- throw error;
365
- }
366
- if (error instanceof Error) {
367
- if (error.name === "AbortError") {
368
- throw new BitfabError("Request timed out after 30000ms");
369
- }
370
- throw new BitfabError(error.message);
371
- }
372
- throw new BitfabError("Unknown error occurred");
373
- } finally {
374
- clearTimeout(timeoutId);
375
- }
376
- }
377
- /**
378
- * Mark a replay test run as completed.
379
- * Blocking call.
380
- */
381
- async completeReplay(testRunId) {
382
- return this.request(
383
- "/api/sdk/replay/complete",
384
- { testRunId },
385
- { timeout: 3e4 }
386
- );
387
- }
388
- /**
389
- * Ask the server to materialize a per-trace DB branch lease from a
390
- * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
391
- * snapshot + preview branch and polls operations to readiness, which
392
- * can take seconds.
393
- */
394
- async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
395
- return this.request(
396
- "/api/sdk/replay/resolveDbBranchLease",
397
- { testRunId, traceId, dbSnapshotRef },
398
- { timeout: 9e4 }
399
- );
400
- }
401
- /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
402
- async releaseDbBranchLease(neonBranchId) {
403
- await this.request(
404
- "/api/sdk/replay/releaseDbBranchLease",
405
- { neonBranchId },
406
- { timeout: 3e4 }
407
- );
408
- }
409
- };
410
- }
411
- });
412
-
413
57
  // src/asyncStorage.ts
414
58
  function registerAsyncLocalStorageClass(cls) {
415
59
  if (!AsyncLocalStorageClass) {
@@ -583,18 +227,25 @@ function buildMockTree(rootNode) {
583
227
  }
584
228
  return { spans };
585
229
  }
586
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment) {
230
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
587
231
  const lease = environment ? serverItem.dbBranchLease : void 0;
588
232
  let inputs = [];
589
233
  let originalOutput;
590
234
  let result;
591
235
  let error = null;
592
236
  const replayedTraceId = crypto.randomUUID();
237
+ const pendingPersistence = [];
593
238
  try {
594
239
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
595
240
  const spanData = span.rawData?.span_data ?? {};
596
241
  inputs = deserializeInputs(spanData);
597
242
  originalOutput = deserializeOutput(spanData);
243
+ if (adaptInputs) {
244
+ inputs = adaptInputs(inputs, {
245
+ traceId: serverItem.traceId,
246
+ sourceSpanId: serverItem.externalSpanId
247
+ });
248
+ }
598
249
  let mockTree;
599
250
  if (mockStrategy === "all" || mockStrategy === "marked") {
600
251
  const treeResponse = await httpClient.getSpanTree(
@@ -612,7 +263,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
612
263
  mockTree,
613
264
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
614
265
  mockStrategy,
615
- dbBranchLease: lease
266
+ dbBranchLease: lease,
267
+ pendingPersistence
616
268
  },
617
269
  () => fn(...inputs)
618
270
  );
@@ -620,6 +272,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
620
272
  } catch (e) {
621
273
  error = e instanceof Error ? e.message : String(e);
622
274
  } finally {
275
+ await Promise.allSettled(pendingPersistence);
623
276
  if (lease) {
624
277
  try {
625
278
  await httpClient.releaseDbBranchLease(lease.neonBranchId);
@@ -703,24 +356,51 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
703
356
  fn,
704
357
  testRunId,
705
358
  mockStrategy,
706
- options?.environment
359
+ options?.environment,
360
+ options?.adaptInputs
707
361
  )
708
362
  );
709
363
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
710
- await flushTraces();
711
- let serverTraceIds = {};
712
- try {
713
- const completeResult = await httpClient.completeReplay(testRunId);
714
- serverTraceIds = completeResult.traceIds ?? {};
715
- } catch (e) {
364
+ const completeResult = await httpClient.completeReplay(testRunId);
365
+ const serverTraceIds = completeResult.traceIds;
366
+ if (serverTraceIds === void 0) {
716
367
  try {
717
- console.error("Bitfab: Failed to complete replay:", e);
368
+ console.warn(
369
+ "Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)"
370
+ );
718
371
  } catch {
719
372
  }
720
- }
721
- for (const item of resultItems) {
722
- if (item.traceId) {
723
- item.traceId = serverTraceIds[item.traceId] ?? null;
373
+ for (const item of resultItems) {
374
+ item.traceId = null;
375
+ }
376
+ } else {
377
+ const missing = [];
378
+ let completedCount = 0;
379
+ for (const item of resultItems) {
380
+ if (item.traceId) {
381
+ const mapped = serverTraceIds[item.traceId];
382
+ if (item.error === null) {
383
+ completedCount += 1;
384
+ if (mapped === void 0) {
385
+ missing.push(item.traceId);
386
+ }
387
+ }
388
+ item.traceId = mapped ?? null;
389
+ }
390
+ }
391
+ if (missing.length > 0) {
392
+ const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
393
+ if (missing.length === completedCount) {
394
+ throw new BitfabError(
395
+ `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} Trace uploads were awaited, so either the uploads failed (check for "Bitfab: Failed to create" errors above) or the replayed function is not wrapped with withSpan.`
396
+ );
397
+ }
398
+ try {
399
+ console.error(
400
+ `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}).${serverCount} Their traceId is null and verdicts cannot be persisted for them. Missing: ${missing.join(", ")}`
401
+ );
402
+ } catch {
403
+ }
724
404
  }
725
405
  }
726
406
  return {
@@ -733,7 +413,6 @@ var init_replay = __esm({
733
413
  "src/replay.ts"() {
734
414
  "use strict";
735
415
  init_errors();
736
- init_http();
737
416
  init_replayContext();
738
417
  init_serialize();
739
418
  }
@@ -758,9 +437,346 @@ __export(index_exports, {
758
437
  });
759
438
  module.exports = __toCommonJS(index_exports);
760
439
 
440
+ // src/version.generated.ts
441
+ var __version__ = "0.16.0";
442
+
443
+ // src/constants.ts
444
+ var DEFAULT_SERVICE_URL = "https://bitfab.ai";
445
+
446
+ // src/http.ts
447
+ init_errors();
448
+ var pendingTracePromises = /* @__PURE__ */ new Set();
449
+ function awaitOnExit(promise) {
450
+ pendingTracePromises.add(promise);
451
+ void promise.finally(() => {
452
+ pendingTracePromises.delete(promise);
453
+ }).catch(() => {
454
+ });
455
+ return promise;
456
+ }
457
+ async function flushTraces(timeoutMs = 5e3) {
458
+ if (pendingTracePromises.size === 0) {
459
+ return;
460
+ }
461
+ await Promise.race([
462
+ Promise.allSettled(Array.from(pendingTracePromises)),
463
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
464
+ ]);
465
+ }
466
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
467
+ let isFlushing = false;
468
+ process.on("beforeExit", () => {
469
+ if (pendingTracePromises.size > 0 && !isFlushing) {
470
+ isFlushing = true;
471
+ Promise.allSettled(
472
+ Array.from(pendingTracePromises).map(
473
+ (p) => p.catch(() => {
474
+ })
475
+ )
476
+ ).then(() => {
477
+ isFlushing = false;
478
+ }).catch(() => {
479
+ isFlushing = false;
480
+ });
481
+ }
482
+ });
483
+ }
484
+ var HttpClient = class {
485
+ constructor(config) {
486
+ this.apiKey = config.apiKey;
487
+ this.serviceUrl = config.serviceUrl;
488
+ this.timeout = config.timeout ?? 12e4;
489
+ }
490
+ /**
491
+ * Make an HTTP request to the Bitfab API. Defaults to POST; pass
492
+ * `options.method` to use a different verb (e.g. "PATCH").
493
+ *
494
+ * @param endpoint - The API endpoint (without base URL)
495
+ * @param payload - The request body
496
+ * @param options - Optional request options
497
+ * @returns The parsed JSON response
498
+ * @throws {BitfabError} If the request fails
499
+ */
500
+ async request(endpoint, payload, options) {
501
+ const url = `${this.serviceUrl}${endpoint}`;
502
+ const timeout = options?.timeout ?? this.timeout;
503
+ const method = options?.method ?? "POST";
504
+ const controller = new AbortController();
505
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
506
+ let body;
507
+ let serializationError;
508
+ try {
509
+ body = JSON.stringify(payload);
510
+ } catch (error) {
511
+ serializationError = error instanceof Error ? error.message : String(error);
512
+ body = JSON.stringify({
513
+ ...Object.fromEntries(
514
+ Object.entries(payload).filter(
515
+ ([, v]) => typeof v === "string" || typeof v === "number"
516
+ )
517
+ ),
518
+ rawSpan: {},
519
+ errors: [
520
+ { source: "sdk", step: "json_serialize", error: serializationError }
521
+ ]
522
+ });
523
+ }
524
+ try {
525
+ const response = await fetch(url, {
526
+ method,
527
+ headers: {
528
+ "Content-Type": "application/json",
529
+ Authorization: `Bearer ${this.apiKey}`
530
+ },
531
+ body,
532
+ signal: controller.signal
533
+ });
534
+ if (!response.ok) {
535
+ const errorText = await response.text();
536
+ throw new BitfabError(
537
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
538
+ );
539
+ }
540
+ const result = await response.json();
541
+ if (result.error) {
542
+ if (result.url) {
543
+ throw new BitfabError(
544
+ `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
545
+ result.url
546
+ );
547
+ }
548
+ throw new BitfabError(result.error);
549
+ }
550
+ return result;
551
+ } catch (error) {
552
+ if (error instanceof BitfabError) {
553
+ throw error;
554
+ }
555
+ if (error instanceof Error) {
556
+ if (error.name === "AbortError") {
557
+ throw new BitfabError(`Request timed out after ${timeout}ms`);
558
+ }
559
+ throw new BitfabError(error.message);
560
+ }
561
+ throw new BitfabError("Unknown error occurred");
562
+ } finally {
563
+ clearTimeout(timeoutId);
564
+ }
565
+ }
566
+ /**
567
+ * Look up a function by name.
568
+ * Blocks until complete - needed for function execution.
569
+ */
570
+ async lookupFunction(name) {
571
+ return this.request("/api/sdk/functions/lookup", { name });
572
+ }
573
+ /**
574
+ * Send an internal trace (from BAML execution).
575
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
576
+ */
577
+ sendInternalTrace(functionId, payload) {
578
+ void awaitOnExit(
579
+ this.request(`/api/sdk/functions/${functionId}/traces`, {
580
+ ...payload,
581
+ sdkVersion: __version__
582
+ })
583
+ ).catch((error) => {
584
+ try {
585
+ console.error("Bitfab: Failed to create trace:", error);
586
+ } catch {
587
+ }
588
+ });
589
+ }
590
+ /**
591
+ * Send an external span (from withSpan wrapper or OpenAI tracing).
592
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
593
+ * Returns the tracked promise so callers can optionally await it.
594
+ */
595
+ sendExternalSpan(payload) {
596
+ return awaitOnExit(
597
+ this.request("/api/sdk/externalSpans", {
598
+ ...payload,
599
+ sdkVersion: __version__
600
+ })
601
+ ).catch((error) => {
602
+ try {
603
+ console.error("Bitfab: Failed to create external span:", error);
604
+ } catch {
605
+ }
606
+ });
607
+ }
608
+ /**
609
+ * Send an external trace (from OpenAI tracing).
610
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
611
+ * Returns the tracked promise so callers can optionally await it
612
+ * (the replay path does, so trace completions are persisted before
613
+ * `completeReplay` builds the trace-ID mapping).
614
+ */
615
+ sendExternalTrace(payload) {
616
+ return awaitOnExit(
617
+ this.request("/api/sdk/externalTraces", {
618
+ ...payload,
619
+ sdkVersion: __version__
620
+ })
621
+ ).catch((error) => {
622
+ try {
623
+ console.error("Bitfab: Failed to create external trace:", error);
624
+ } catch {
625
+ }
626
+ });
627
+ }
628
+ /**
629
+ * Partial update of an existing external trace identified by sourceTraceId.
630
+ * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
631
+ * returns a tracked promise that callers may optionally await.
632
+ */
633
+ patchTrace(sourceTraceId, payload) {
634
+ const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
635
+ return awaitOnExit(
636
+ this.request(endpoint, payload, { method: "PATCH" })
637
+ ).catch((error) => {
638
+ try {
639
+ console.error("Bitfab: Failed to patch trace:", error);
640
+ } catch {
641
+ }
642
+ });
643
+ }
644
+ /**
645
+ * Start a replay session by fetching historical traces.
646
+ * Blocking call — creates a test run and returns lightweight item references.
647
+ */
648
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
649
+ const payload = { traceFunctionKey };
650
+ if (limit !== void 0) {
651
+ payload.limit = limit;
652
+ }
653
+ if (traceIds) {
654
+ payload.traceIds = traceIds;
655
+ }
656
+ if (codeChangeDescription !== void 0) {
657
+ payload.codeChangeDescription = codeChangeDescription;
658
+ }
659
+ if (codeChangeFiles !== void 0) {
660
+ payload.codeChangeFiles = codeChangeFiles;
661
+ }
662
+ if (includeDbBranchLease) {
663
+ payload.includeDbBranchLease = true;
664
+ }
665
+ if (experimentGroupId !== void 0) {
666
+ payload.experimentGroupId = experimentGroupId;
667
+ }
668
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
669
+ return this.request("/api/sdk/replay/start", payload, {
670
+ timeout
671
+ });
672
+ }
673
+ /**
674
+ * Fetch an external span by ID.
675
+ * Blocking GET request.
676
+ */
677
+ async getExternalSpan(spanId) {
678
+ const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
679
+ const controller = new AbortController();
680
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
681
+ try {
682
+ const response = await fetch(url, {
683
+ method: "GET",
684
+ headers: { Authorization: `Bearer ${this.apiKey}` },
685
+ signal: controller.signal
686
+ });
687
+ if (!response.ok) {
688
+ const errorText = await response.text();
689
+ throw new BitfabError(
690
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
691
+ );
692
+ }
693
+ return await response.json();
694
+ } catch (error) {
695
+ if (error instanceof BitfabError) {
696
+ throw error;
697
+ }
698
+ if (error instanceof Error) {
699
+ if (error.name === "AbortError") {
700
+ throw new BitfabError("Request timed out after 30000ms");
701
+ }
702
+ throw new BitfabError(error.message);
703
+ }
704
+ throw new BitfabError("Unknown error occurred");
705
+ } finally {
706
+ clearTimeout(timeoutId);
707
+ }
708
+ }
709
+ /**
710
+ * Fetch the span tree for a root span.
711
+ * Blocking GET request.
712
+ */
713
+ async getSpanTree(externalSpanId) {
714
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
715
+ const controller = new AbortController();
716
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
717
+ try {
718
+ const response = await fetch(url, {
719
+ method: "GET",
720
+ headers: { Authorization: `Bearer ${this.apiKey}` },
721
+ signal: controller.signal
722
+ });
723
+ if (!response.ok) {
724
+ const errorText = await response.text();
725
+ throw new BitfabError(
726
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
727
+ );
728
+ }
729
+ return await response.json();
730
+ } catch (error) {
731
+ if (error instanceof BitfabError) {
732
+ throw error;
733
+ }
734
+ if (error instanceof Error) {
735
+ if (error.name === "AbortError") {
736
+ throw new BitfabError("Request timed out after 30000ms");
737
+ }
738
+ throw new BitfabError(error.message);
739
+ }
740
+ throw new BitfabError("Unknown error occurred");
741
+ } finally {
742
+ clearTimeout(timeoutId);
743
+ }
744
+ }
745
+ /**
746
+ * Mark a replay test run as completed.
747
+ * Blocking call.
748
+ */
749
+ async completeReplay(testRunId) {
750
+ return this.request(
751
+ "/api/sdk/replay/complete",
752
+ { testRunId },
753
+ { timeout: 3e4 }
754
+ );
755
+ }
756
+ /**
757
+ * Ask the server to materialize a per-trace DB branch lease from a
758
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
759
+ * snapshot + preview branch and polls operations to readiness, which
760
+ * can take seconds.
761
+ */
762
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
763
+ return this.request(
764
+ "/api/sdk/replay/resolveDbBranchLease",
765
+ { testRunId, traceId, dbSnapshotRef },
766
+ { timeout: 9e4 }
767
+ );
768
+ }
769
+ /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
770
+ async releaseDbBranchLease(neonBranchId) {
771
+ await this.request(
772
+ "/api/sdk/replay/releaseDbBranchLease",
773
+ { neonBranchId },
774
+ { timeout: 3e4 }
775
+ );
776
+ }
777
+ };
778
+
761
779
  // src/claudeAgentSdk.ts
762
- init_constants();
763
- init_http();
764
780
  function nowIso() {
765
781
  return (/* @__PURE__ */ new Date()).toISOString();
766
782
  }
@@ -1525,9 +1541,6 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1525
1541
  };
1526
1542
  }
1527
1543
 
1528
- // src/client.ts
1529
- init_constants();
1530
-
1531
1544
  // src/dbSnapshot.ts
1532
1545
  init_errors();
1533
1546
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -1545,12 +1558,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1545
1558
  };
1546
1559
  }
1547
1560
 
1548
- // src/client.ts
1549
- init_http();
1550
-
1551
1561
  // src/langgraph.ts
1552
- init_constants();
1553
- init_http();
1554
1562
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1555
1563
  var LANGGRAPH_METADATA_KEYS = [
1556
1564
  "langgraph_step",
@@ -2098,8 +2106,6 @@ var ReplayEnvironment = class {
2098
2106
  init_serialize();
2099
2107
 
2100
2108
  // src/tracing.ts
2101
- init_constants();
2102
- init_http();
2103
2109
  var BitfabOpenAITracingProcessor = class {
2104
2110
  /**
2105
2111
  * Initialize the tracing processor.
@@ -2933,9 +2939,18 @@ var Bitfab = class {
2933
2939
  spanType: options.type ?? "custom"
2934
2940
  };
2935
2941
  const sendSpan = async (params) => {
2942
+ const replayCtx = getReplayContext();
2943
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
2944
+ let resolvePersistence;
2945
+ if (persistenceCollector) {
2946
+ persistenceCollector.push(
2947
+ new Promise((resolve) => {
2948
+ resolvePersistence = resolve;
2949
+ })
2950
+ );
2951
+ }
2936
2952
  try {
2937
2953
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2938
- const replayCtx = getReplayContext();
2939
2954
  const spanPromise = self.sendWrapperSpan({
2940
2955
  ...baseSpanParams,
2941
2956
  ...params,
@@ -2950,13 +2965,17 @@ var Bitfab = class {
2950
2965
  if (isRootSpan) {
2951
2966
  const pending = pendingSpanPromises.get(traceId) ?? [];
2952
2967
  pending.push(spanPromise);
2953
- await Promise.race([
2954
- Promise.allSettled(pending),
2955
- new Promise((resolve) => setTimeout(resolve, 5e3))
2956
- ]);
2968
+ if (persistenceCollector) {
2969
+ await Promise.allSettled(pending);
2970
+ } else {
2971
+ await Promise.race([
2972
+ Promise.allSettled(pending),
2973
+ new Promise((resolve) => setTimeout(resolve, 5e3))
2974
+ ]);
2975
+ }
2957
2976
  pendingSpanPromises.delete(traceId);
2958
2977
  const traceState = activeTraceStates.get(traceId);
2959
- self.sendTraceCompletion({
2978
+ const completionPromise = self.sendTraceCompletion({
2960
2979
  traceFunctionKey,
2961
2980
  traceId,
2962
2981
  startedAt: traceState?.startedAt ?? startedAt,
@@ -2969,6 +2988,9 @@ var Bitfab = class {
2969
2988
  dbSnapshotRef: traceState?.dbSnapshotRef
2970
2989
  });
2971
2990
  activeTraceStates.delete(traceId);
2991
+ if (persistenceCollector) {
2992
+ await completionPromise;
2993
+ }
2972
2994
  } else {
2973
2995
  const pending = pendingSpanPromises.get(traceId);
2974
2996
  if (pending) {
@@ -2978,6 +3000,8 @@ var Bitfab = class {
2978
3000
  }
2979
3001
  }
2980
3002
  } catch {
3003
+ } finally {
3004
+ resolvePersistence?.();
2981
3005
  }
2982
3006
  };
2983
3007
  const replayCtxForMock = getReplayContext();
@@ -3130,7 +3154,7 @@ var Bitfab = class {
3130
3154
  if (params.dbSnapshotRef) {
3131
3155
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3132
3156
  }
3133
- this.httpClient.sendExternalTrace({
3157
+ return this.httpClient.sendExternalTrace({
3134
3158
  type: "sdk-function",
3135
3159
  source: "typescript-sdk-function",
3136
3160
  traceFunctionKey: params.traceFunctionKey,
@@ -3274,10 +3298,6 @@ var BitfabFunction = class {
3274
3298
  );
3275
3299
  }
3276
3300
  };
3277
-
3278
- // src/index.ts
3279
- init_constants();
3280
- init_http();
3281
3301
  // Annotate the CommonJS export names for ESM import in node:
3282
3302
  0 && (module.exports = {
3283
3303
  Bitfab,