bitfab 0.14.0 → 0.15.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) {
@@ -590,6 +234,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
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 ?? {};
@@ -612,7 +257,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
612
257
  mockTree,
613
258
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
614
259
  mockStrategy,
615
- dbBranchLease: lease
260
+ dbBranchLease: lease,
261
+ pendingPersistence
616
262
  },
617
263
  () => fn(...inputs)
618
264
  );
@@ -620,6 +266,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
620
266
  } catch (e) {
621
267
  error = e instanceof Error ? e.message : String(e);
622
268
  } finally {
269
+ await Promise.allSettled(pendingPersistence);
623
270
  if (lease) {
624
271
  try {
625
272
  await httpClient.releaseDbBranchLease(lease.neonBranchId);
@@ -707,20 +354,46 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
707
354
  )
708
355
  );
709
356
  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) {
357
+ const completeResult = await httpClient.completeReplay(testRunId);
358
+ const serverTraceIds = completeResult.traceIds;
359
+ if (serverTraceIds === void 0) {
716
360
  try {
717
- console.error("Bitfab: Failed to complete replay:", e);
361
+ console.warn(
362
+ "Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)"
363
+ );
718
364
  } catch {
719
365
  }
720
- }
721
- for (const item of resultItems) {
722
- if (item.traceId) {
723
- item.traceId = serverTraceIds[item.traceId] ?? null;
366
+ for (const item of resultItems) {
367
+ item.traceId = null;
368
+ }
369
+ } else {
370
+ const missing = [];
371
+ let completedCount = 0;
372
+ for (const item of resultItems) {
373
+ if (item.traceId) {
374
+ const mapped = serverTraceIds[item.traceId];
375
+ if (item.error === null) {
376
+ completedCount += 1;
377
+ if (mapped === void 0) {
378
+ missing.push(item.traceId);
379
+ }
380
+ }
381
+ item.traceId = mapped ?? null;
382
+ }
383
+ }
384
+ if (missing.length > 0) {
385
+ const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
386
+ if (missing.length === completedCount) {
387
+ throw new BitfabError(
388
+ `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.`
389
+ );
390
+ }
391
+ try {
392
+ console.error(
393
+ `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(", ")}`
394
+ );
395
+ } catch {
396
+ }
724
397
  }
725
398
  }
726
399
  return {
@@ -733,7 +406,6 @@ var init_replay = __esm({
733
406
  "src/replay.ts"() {
734
407
  "use strict";
735
408
  init_errors();
736
- init_http();
737
409
  init_replayContext();
738
410
  init_serialize();
739
411
  }
@@ -758,9 +430,346 @@ __export(index_exports, {
758
430
  });
759
431
  module.exports = __toCommonJS(index_exports);
760
432
 
433
+ // src/version.generated.ts
434
+ var __version__ = "0.15.0";
435
+
436
+ // src/constants.ts
437
+ var DEFAULT_SERVICE_URL = "https://bitfab.ai";
438
+
439
+ // src/http.ts
440
+ init_errors();
441
+ var pendingTracePromises = /* @__PURE__ */ new Set();
442
+ function awaitOnExit(promise) {
443
+ pendingTracePromises.add(promise);
444
+ void promise.finally(() => {
445
+ pendingTracePromises.delete(promise);
446
+ }).catch(() => {
447
+ });
448
+ return promise;
449
+ }
450
+ async function flushTraces(timeoutMs = 5e3) {
451
+ if (pendingTracePromises.size === 0) {
452
+ return;
453
+ }
454
+ await Promise.race([
455
+ Promise.allSettled(Array.from(pendingTracePromises)),
456
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
457
+ ]);
458
+ }
459
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
460
+ let isFlushing = false;
461
+ process.on("beforeExit", () => {
462
+ if (pendingTracePromises.size > 0 && !isFlushing) {
463
+ isFlushing = true;
464
+ Promise.allSettled(
465
+ Array.from(pendingTracePromises).map(
466
+ (p) => p.catch(() => {
467
+ })
468
+ )
469
+ ).then(() => {
470
+ isFlushing = false;
471
+ }).catch(() => {
472
+ isFlushing = false;
473
+ });
474
+ }
475
+ });
476
+ }
477
+ var HttpClient = class {
478
+ constructor(config) {
479
+ this.apiKey = config.apiKey;
480
+ this.serviceUrl = config.serviceUrl;
481
+ this.timeout = config.timeout ?? 12e4;
482
+ }
483
+ /**
484
+ * Make an HTTP request to the Bitfab API. Defaults to POST; pass
485
+ * `options.method` to use a different verb (e.g. "PATCH").
486
+ *
487
+ * @param endpoint - The API endpoint (without base URL)
488
+ * @param payload - The request body
489
+ * @param options - Optional request options
490
+ * @returns The parsed JSON response
491
+ * @throws {BitfabError} If the request fails
492
+ */
493
+ async request(endpoint, payload, options) {
494
+ const url = `${this.serviceUrl}${endpoint}`;
495
+ const timeout = options?.timeout ?? this.timeout;
496
+ const method = options?.method ?? "POST";
497
+ const controller = new AbortController();
498
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
499
+ let body;
500
+ let serializationError;
501
+ try {
502
+ body = JSON.stringify(payload);
503
+ } catch (error) {
504
+ serializationError = error instanceof Error ? error.message : String(error);
505
+ body = JSON.stringify({
506
+ ...Object.fromEntries(
507
+ Object.entries(payload).filter(
508
+ ([, v]) => typeof v === "string" || typeof v === "number"
509
+ )
510
+ ),
511
+ rawSpan: {},
512
+ errors: [
513
+ { source: "sdk", step: "json_serialize", error: serializationError }
514
+ ]
515
+ });
516
+ }
517
+ try {
518
+ const response = await fetch(url, {
519
+ method,
520
+ headers: {
521
+ "Content-Type": "application/json",
522
+ Authorization: `Bearer ${this.apiKey}`
523
+ },
524
+ body,
525
+ signal: controller.signal
526
+ });
527
+ if (!response.ok) {
528
+ const errorText = await response.text();
529
+ throw new BitfabError(
530
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
531
+ );
532
+ }
533
+ const result = await response.json();
534
+ if (result.error) {
535
+ if (result.url) {
536
+ throw new BitfabError(
537
+ `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
538
+ result.url
539
+ );
540
+ }
541
+ throw new BitfabError(result.error);
542
+ }
543
+ return result;
544
+ } catch (error) {
545
+ if (error instanceof BitfabError) {
546
+ throw error;
547
+ }
548
+ if (error instanceof Error) {
549
+ if (error.name === "AbortError") {
550
+ throw new BitfabError(`Request timed out after ${timeout}ms`);
551
+ }
552
+ throw new BitfabError(error.message);
553
+ }
554
+ throw new BitfabError("Unknown error occurred");
555
+ } finally {
556
+ clearTimeout(timeoutId);
557
+ }
558
+ }
559
+ /**
560
+ * Look up a function by name.
561
+ * Blocks until complete - needed for function execution.
562
+ */
563
+ async lookupFunction(name) {
564
+ return this.request("/api/sdk/functions/lookup", { name });
565
+ }
566
+ /**
567
+ * Send an internal trace (from BAML execution).
568
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
569
+ */
570
+ sendInternalTrace(functionId, payload) {
571
+ void awaitOnExit(
572
+ this.request(`/api/sdk/functions/${functionId}/traces`, {
573
+ ...payload,
574
+ sdkVersion: __version__
575
+ })
576
+ ).catch((error) => {
577
+ try {
578
+ console.error("Bitfab: Failed to create trace:", error);
579
+ } catch {
580
+ }
581
+ });
582
+ }
583
+ /**
584
+ * Send an external span (from withSpan wrapper or OpenAI tracing).
585
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
586
+ * Returns the tracked promise so callers can optionally await it.
587
+ */
588
+ sendExternalSpan(payload) {
589
+ return awaitOnExit(
590
+ this.request("/api/sdk/externalSpans", {
591
+ ...payload,
592
+ sdkVersion: __version__
593
+ })
594
+ ).catch((error) => {
595
+ try {
596
+ console.error("Bitfab: Failed to create external span:", error);
597
+ } catch {
598
+ }
599
+ });
600
+ }
601
+ /**
602
+ * Send an external trace (from OpenAI tracing).
603
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
604
+ * Returns the tracked promise so callers can optionally await it
605
+ * (the replay path does, so trace completions are persisted before
606
+ * `completeReplay` builds the trace-ID mapping).
607
+ */
608
+ sendExternalTrace(payload) {
609
+ return awaitOnExit(
610
+ this.request("/api/sdk/externalTraces", {
611
+ ...payload,
612
+ sdkVersion: __version__
613
+ })
614
+ ).catch((error) => {
615
+ try {
616
+ console.error("Bitfab: Failed to create external trace:", error);
617
+ } catch {
618
+ }
619
+ });
620
+ }
621
+ /**
622
+ * Partial update of an existing external trace identified by sourceTraceId.
623
+ * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
624
+ * returns a tracked promise that callers may optionally await.
625
+ */
626
+ patchTrace(sourceTraceId, payload) {
627
+ const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
628
+ return awaitOnExit(
629
+ this.request(endpoint, payload, { method: "PATCH" })
630
+ ).catch((error) => {
631
+ try {
632
+ console.error("Bitfab: Failed to patch trace:", error);
633
+ } catch {
634
+ }
635
+ });
636
+ }
637
+ /**
638
+ * Start a replay session by fetching historical traces.
639
+ * Blocking call — creates a test run and returns lightweight item references.
640
+ */
641
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
642
+ const payload = { traceFunctionKey };
643
+ if (limit !== void 0) {
644
+ payload.limit = limit;
645
+ }
646
+ if (traceIds) {
647
+ payload.traceIds = traceIds;
648
+ }
649
+ if (codeChangeDescription !== void 0) {
650
+ payload.codeChangeDescription = codeChangeDescription;
651
+ }
652
+ if (codeChangeFiles !== void 0) {
653
+ payload.codeChangeFiles = codeChangeFiles;
654
+ }
655
+ if (includeDbBranchLease) {
656
+ payload.includeDbBranchLease = true;
657
+ }
658
+ if (experimentGroupId !== void 0) {
659
+ payload.experimentGroupId = experimentGroupId;
660
+ }
661
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
662
+ return this.request("/api/sdk/replay/start", payload, {
663
+ timeout
664
+ });
665
+ }
666
+ /**
667
+ * Fetch an external span by ID.
668
+ * Blocking GET request.
669
+ */
670
+ async getExternalSpan(spanId) {
671
+ const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
672
+ const controller = new AbortController();
673
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
674
+ try {
675
+ const response = await fetch(url, {
676
+ method: "GET",
677
+ headers: { Authorization: `Bearer ${this.apiKey}` },
678
+ signal: controller.signal
679
+ });
680
+ if (!response.ok) {
681
+ const errorText = await response.text();
682
+ throw new BitfabError(
683
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
684
+ );
685
+ }
686
+ return await response.json();
687
+ } catch (error) {
688
+ if (error instanceof BitfabError) {
689
+ throw error;
690
+ }
691
+ if (error instanceof Error) {
692
+ if (error.name === "AbortError") {
693
+ throw new BitfabError("Request timed out after 30000ms");
694
+ }
695
+ throw new BitfabError(error.message);
696
+ }
697
+ throw new BitfabError("Unknown error occurred");
698
+ } finally {
699
+ clearTimeout(timeoutId);
700
+ }
701
+ }
702
+ /**
703
+ * Fetch the span tree for a root span.
704
+ * Blocking GET request.
705
+ */
706
+ async getSpanTree(externalSpanId) {
707
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
708
+ const controller = new AbortController();
709
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
710
+ try {
711
+ const response = await fetch(url, {
712
+ method: "GET",
713
+ headers: { Authorization: `Bearer ${this.apiKey}` },
714
+ signal: controller.signal
715
+ });
716
+ if (!response.ok) {
717
+ const errorText = await response.text();
718
+ throw new BitfabError(
719
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
720
+ );
721
+ }
722
+ return await response.json();
723
+ } catch (error) {
724
+ if (error instanceof BitfabError) {
725
+ throw error;
726
+ }
727
+ if (error instanceof Error) {
728
+ if (error.name === "AbortError") {
729
+ throw new BitfabError("Request timed out after 30000ms");
730
+ }
731
+ throw new BitfabError(error.message);
732
+ }
733
+ throw new BitfabError("Unknown error occurred");
734
+ } finally {
735
+ clearTimeout(timeoutId);
736
+ }
737
+ }
738
+ /**
739
+ * Mark a replay test run as completed.
740
+ * Blocking call.
741
+ */
742
+ async completeReplay(testRunId) {
743
+ return this.request(
744
+ "/api/sdk/replay/complete",
745
+ { testRunId },
746
+ { timeout: 3e4 }
747
+ );
748
+ }
749
+ /**
750
+ * Ask the server to materialize a per-trace DB branch lease from a
751
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
752
+ * snapshot + preview branch and polls operations to readiness, which
753
+ * can take seconds.
754
+ */
755
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
756
+ return this.request(
757
+ "/api/sdk/replay/resolveDbBranchLease",
758
+ { testRunId, traceId, dbSnapshotRef },
759
+ { timeout: 9e4 }
760
+ );
761
+ }
762
+ /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
763
+ async releaseDbBranchLease(neonBranchId) {
764
+ await this.request(
765
+ "/api/sdk/replay/releaseDbBranchLease",
766
+ { neonBranchId },
767
+ { timeout: 3e4 }
768
+ );
769
+ }
770
+ };
771
+
761
772
  // src/claudeAgentSdk.ts
762
- init_constants();
763
- init_http();
764
773
  function nowIso() {
765
774
  return (/* @__PURE__ */ new Date()).toISOString();
766
775
  }
@@ -1525,9 +1534,6 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1525
1534
  };
1526
1535
  }
1527
1536
 
1528
- // src/client.ts
1529
- init_constants();
1530
-
1531
1537
  // src/dbSnapshot.ts
1532
1538
  init_errors();
1533
1539
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -1545,12 +1551,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1545
1551
  };
1546
1552
  }
1547
1553
 
1548
- // src/client.ts
1549
- init_http();
1550
-
1551
1554
  // src/langgraph.ts
1552
- init_constants();
1553
- init_http();
1554
1555
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1555
1556
  var LANGGRAPH_METADATA_KEYS = [
1556
1557
  "langgraph_step",
@@ -2098,8 +2099,6 @@ var ReplayEnvironment = class {
2098
2099
  init_serialize();
2099
2100
 
2100
2101
  // src/tracing.ts
2101
- init_constants();
2102
- init_http();
2103
2102
  var BitfabOpenAITracingProcessor = class {
2104
2103
  /**
2105
2104
  * Initialize the tracing processor.
@@ -2933,9 +2932,18 @@ var Bitfab = class {
2933
2932
  spanType: options.type ?? "custom"
2934
2933
  };
2935
2934
  const sendSpan = async (params) => {
2935
+ const replayCtx = getReplayContext();
2936
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
2937
+ let resolvePersistence;
2938
+ if (persistenceCollector) {
2939
+ persistenceCollector.push(
2940
+ new Promise((resolve) => {
2941
+ resolvePersistence = resolve;
2942
+ })
2943
+ );
2944
+ }
2936
2945
  try {
2937
2946
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2938
- const replayCtx = getReplayContext();
2939
2947
  const spanPromise = self.sendWrapperSpan({
2940
2948
  ...baseSpanParams,
2941
2949
  ...params,
@@ -2950,13 +2958,17 @@ var Bitfab = class {
2950
2958
  if (isRootSpan) {
2951
2959
  const pending = pendingSpanPromises.get(traceId) ?? [];
2952
2960
  pending.push(spanPromise);
2953
- await Promise.race([
2954
- Promise.allSettled(pending),
2955
- new Promise((resolve) => setTimeout(resolve, 5e3))
2956
- ]);
2961
+ if (persistenceCollector) {
2962
+ await Promise.allSettled(pending);
2963
+ } else {
2964
+ await Promise.race([
2965
+ Promise.allSettled(pending),
2966
+ new Promise((resolve) => setTimeout(resolve, 5e3))
2967
+ ]);
2968
+ }
2957
2969
  pendingSpanPromises.delete(traceId);
2958
2970
  const traceState = activeTraceStates.get(traceId);
2959
- self.sendTraceCompletion({
2971
+ const completionPromise = self.sendTraceCompletion({
2960
2972
  traceFunctionKey,
2961
2973
  traceId,
2962
2974
  startedAt: traceState?.startedAt ?? startedAt,
@@ -2969,6 +2981,9 @@ var Bitfab = class {
2969
2981
  dbSnapshotRef: traceState?.dbSnapshotRef
2970
2982
  });
2971
2983
  activeTraceStates.delete(traceId);
2984
+ if (persistenceCollector) {
2985
+ await completionPromise;
2986
+ }
2972
2987
  } else {
2973
2988
  const pending = pendingSpanPromises.get(traceId);
2974
2989
  if (pending) {
@@ -2978,6 +2993,8 @@ var Bitfab = class {
2978
2993
  }
2979
2994
  }
2980
2995
  } catch {
2996
+ } finally {
2997
+ resolvePersistence?.();
2981
2998
  }
2982
2999
  };
2983
3000
  const replayCtxForMock = getReplayContext();
@@ -3130,7 +3147,7 @@ var Bitfab = class {
3130
3147
  if (params.dbSnapshotRef) {
3131
3148
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3132
3149
  }
3133
- this.httpClient.sendExternalTrace({
3150
+ return this.httpClient.sendExternalTrace({
3134
3151
  type: "sdk-function",
3135
3152
  source: "typescript-sdk-function",
3136
3153
  traceFunctionKey: params.traceFunctionKey,
@@ -3274,10 +3291,6 @@ var BitfabFunction = class {
3274
3291
  );
3275
3292
  }
3276
3293
  };
3277
-
3278
- // src/index.ts
3279
- init_constants();
3280
- init_http();
3281
3294
  // Annotate the CommonJS export names for ESM import in node:
3282
3295
  0 && (module.exports = {
3283
3296
  Bitfab,