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/node.cjs CHANGED
@@ -85,25 +85,6 @@ var init_asyncStorage = __esm({
85
85
  }
86
86
  });
87
87
 
88
- // src/version.generated.ts
89
- var __version__;
90
- var init_version_generated = __esm({
91
- "src/version.generated.ts"() {
92
- "use strict";
93
- __version__ = "0.14.0";
94
- }
95
- });
96
-
97
- // src/constants.ts
98
- var DEFAULT_SERVICE_URL;
99
- var init_constants = __esm({
100
- "src/constants.ts"() {
101
- "use strict";
102
- init_version_generated();
103
- DEFAULT_SERVICE_URL = "https://bitfab.ai";
104
- }
105
- });
106
-
107
88
  // src/errors.ts
108
89
  var BitfabError;
109
90
  var init_errors = __esm({
@@ -119,343 +100,6 @@ var init_errors = __esm({
119
100
  }
120
101
  });
121
102
 
122
- // src/http.ts
123
- function awaitOnExit(promise) {
124
- pendingTracePromises.add(promise);
125
- void promise.finally(() => {
126
- pendingTracePromises.delete(promise);
127
- }).catch(() => {
128
- });
129
- return promise;
130
- }
131
- async function flushTraces(timeoutMs = 5e3) {
132
- if (pendingTracePromises.size === 0) {
133
- return;
134
- }
135
- await Promise.race([
136
- Promise.allSettled(Array.from(pendingTracePromises)),
137
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
138
- ]);
139
- }
140
- var pendingTracePromises, HttpClient;
141
- var init_http = __esm({
142
- "src/http.ts"() {
143
- "use strict";
144
- init_constants();
145
- init_errors();
146
- pendingTracePromises = /* @__PURE__ */ new Set();
147
- if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
148
- let isFlushing = false;
149
- process.on("beforeExit", () => {
150
- if (pendingTracePromises.size > 0 && !isFlushing) {
151
- isFlushing = true;
152
- Promise.allSettled(
153
- Array.from(pendingTracePromises).map(
154
- (p) => p.catch(() => {
155
- })
156
- )
157
- ).then(() => {
158
- isFlushing = false;
159
- }).catch(() => {
160
- isFlushing = false;
161
- });
162
- }
163
- });
164
- }
165
- HttpClient = class {
166
- constructor(config) {
167
- this.apiKey = config.apiKey;
168
- this.serviceUrl = config.serviceUrl;
169
- this.timeout = config.timeout ?? 12e4;
170
- }
171
- /**
172
- * Make an HTTP request to the Bitfab API. Defaults to POST; pass
173
- * `options.method` to use a different verb (e.g. "PATCH").
174
- *
175
- * @param endpoint - The API endpoint (without base URL)
176
- * @param payload - The request body
177
- * @param options - Optional request options
178
- * @returns The parsed JSON response
179
- * @throws {BitfabError} If the request fails
180
- */
181
- async request(endpoint, payload, options) {
182
- const url = `${this.serviceUrl}${endpoint}`;
183
- const timeout = options?.timeout ?? this.timeout;
184
- const method = options?.method ?? "POST";
185
- const controller = new AbortController();
186
- const timeoutId = setTimeout(() => controller.abort(), timeout);
187
- let body;
188
- let serializationError;
189
- try {
190
- body = JSON.stringify(payload);
191
- } catch (error) {
192
- serializationError = error instanceof Error ? error.message : String(error);
193
- body = JSON.stringify({
194
- ...Object.fromEntries(
195
- Object.entries(payload).filter(
196
- ([, v]) => typeof v === "string" || typeof v === "number"
197
- )
198
- ),
199
- rawSpan: {},
200
- errors: [
201
- { source: "sdk", step: "json_serialize", error: serializationError }
202
- ]
203
- });
204
- }
205
- try {
206
- const response = await fetch(url, {
207
- method,
208
- headers: {
209
- "Content-Type": "application/json",
210
- Authorization: `Bearer ${this.apiKey}`
211
- },
212
- body,
213
- signal: controller.signal
214
- });
215
- if (!response.ok) {
216
- const errorText = await response.text();
217
- throw new BitfabError(
218
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
219
- );
220
- }
221
- const result = await response.json();
222
- if (result.error) {
223
- if (result.url) {
224
- throw new BitfabError(
225
- `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
226
- result.url
227
- );
228
- }
229
- throw new BitfabError(result.error);
230
- }
231
- return result;
232
- } catch (error) {
233
- if (error instanceof BitfabError) {
234
- throw error;
235
- }
236
- if (error instanceof Error) {
237
- if (error.name === "AbortError") {
238
- throw new BitfabError(`Request timed out after ${timeout}ms`);
239
- }
240
- throw new BitfabError(error.message);
241
- }
242
- throw new BitfabError("Unknown error occurred");
243
- } finally {
244
- clearTimeout(timeoutId);
245
- }
246
- }
247
- /**
248
- * Look up a function by name.
249
- * Blocks until complete - needed for function execution.
250
- */
251
- async lookupFunction(name) {
252
- return this.request("/api/sdk/functions/lookup", { name });
253
- }
254
- /**
255
- * Send an internal trace (from BAML execution).
256
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
257
- */
258
- sendInternalTrace(functionId, payload) {
259
- void awaitOnExit(
260
- this.request(`/api/sdk/functions/${functionId}/traces`, {
261
- ...payload,
262
- sdkVersion: __version__
263
- })
264
- ).catch((error) => {
265
- try {
266
- console.error("Bitfab: Failed to create trace:", error);
267
- } catch {
268
- }
269
- });
270
- }
271
- /**
272
- * Send an external span (from withSpan wrapper or OpenAI tracing).
273
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
274
- * Returns the tracked promise so callers can optionally await it.
275
- */
276
- sendExternalSpan(payload) {
277
- return awaitOnExit(
278
- this.request("/api/sdk/externalSpans", {
279
- ...payload,
280
- sdkVersion: __version__
281
- })
282
- ).catch((error) => {
283
- try {
284
- console.error("Bitfab: Failed to create external span:", error);
285
- } catch {
286
- }
287
- });
288
- }
289
- /**
290
- * Send an external trace (from OpenAI tracing).
291
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
292
- */
293
- sendExternalTrace(payload) {
294
- void awaitOnExit(
295
- this.request("/api/sdk/externalTraces", {
296
- ...payload,
297
- sdkVersion: __version__
298
- })
299
- ).catch((error) => {
300
- try {
301
- console.error("Bitfab: Failed to create external trace:", error);
302
- } catch {
303
- }
304
- });
305
- }
306
- /**
307
- * Partial update of an existing external trace identified by sourceTraceId.
308
- * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
309
- * returns a tracked promise that callers may optionally await.
310
- */
311
- patchTrace(sourceTraceId, payload) {
312
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
313
- return awaitOnExit(
314
- this.request(endpoint, payload, { method: "PATCH" })
315
- ).catch((error) => {
316
- try {
317
- console.error("Bitfab: Failed to patch trace:", error);
318
- } catch {
319
- }
320
- });
321
- }
322
- /**
323
- * Start a replay session by fetching historical traces.
324
- * Blocking call — creates a test run and returns lightweight item references.
325
- */
326
- async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
327
- const payload = { traceFunctionKey };
328
- if (limit !== void 0) {
329
- payload.limit = limit;
330
- }
331
- if (traceIds) {
332
- payload.traceIds = traceIds;
333
- }
334
- if (codeChangeDescription !== void 0) {
335
- payload.codeChangeDescription = codeChangeDescription;
336
- }
337
- if (codeChangeFiles !== void 0) {
338
- payload.codeChangeFiles = codeChangeFiles;
339
- }
340
- if (includeDbBranchLease) {
341
- payload.includeDbBranchLease = true;
342
- }
343
- if (experimentGroupId !== void 0) {
344
- payload.experimentGroupId = experimentGroupId;
345
- }
346
- const timeout = includeDbBranchLease ? 18e4 : 3e4;
347
- return this.request("/api/sdk/replay/start", payload, {
348
- timeout
349
- });
350
- }
351
- /**
352
- * Fetch an external span by ID.
353
- * Blocking GET request.
354
- */
355
- async getExternalSpan(spanId) {
356
- const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
357
- const controller = new AbortController();
358
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
359
- try {
360
- const response = await fetch(url, {
361
- method: "GET",
362
- headers: { Authorization: `Bearer ${this.apiKey}` },
363
- signal: controller.signal
364
- });
365
- if (!response.ok) {
366
- const errorText = await response.text();
367
- throw new BitfabError(
368
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
369
- );
370
- }
371
- return await response.json();
372
- } catch (error) {
373
- if (error instanceof BitfabError) {
374
- throw error;
375
- }
376
- if (error instanceof Error) {
377
- if (error.name === "AbortError") {
378
- throw new BitfabError("Request timed out after 30000ms");
379
- }
380
- throw new BitfabError(error.message);
381
- }
382
- throw new BitfabError("Unknown error occurred");
383
- } finally {
384
- clearTimeout(timeoutId);
385
- }
386
- }
387
- /**
388
- * Fetch the span tree for a root span.
389
- * Blocking GET request.
390
- */
391
- async getSpanTree(externalSpanId) {
392
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
393
- const controller = new AbortController();
394
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
395
- try {
396
- const response = await fetch(url, {
397
- method: "GET",
398
- headers: { Authorization: `Bearer ${this.apiKey}` },
399
- signal: controller.signal
400
- });
401
- if (!response.ok) {
402
- const errorText = await response.text();
403
- throw new BitfabError(
404
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
405
- );
406
- }
407
- return await response.json();
408
- } catch (error) {
409
- if (error instanceof BitfabError) {
410
- throw error;
411
- }
412
- if (error instanceof Error) {
413
- if (error.name === "AbortError") {
414
- throw new BitfabError("Request timed out after 30000ms");
415
- }
416
- throw new BitfabError(error.message);
417
- }
418
- throw new BitfabError("Unknown error occurred");
419
- } finally {
420
- clearTimeout(timeoutId);
421
- }
422
- }
423
- /**
424
- * Mark a replay test run as completed.
425
- * Blocking call.
426
- */
427
- async completeReplay(testRunId) {
428
- return this.request(
429
- "/api/sdk/replay/complete",
430
- { testRunId },
431
- { timeout: 3e4 }
432
- );
433
- }
434
- /**
435
- * Ask the server to materialize a per-trace DB branch lease from a
436
- * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
437
- * snapshot + preview branch and polls operations to readiness, which
438
- * can take seconds.
439
- */
440
- async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
441
- return this.request(
442
- "/api/sdk/replay/resolveDbBranchLease",
443
- { testRunId, traceId, dbSnapshotRef },
444
- { timeout: 9e4 }
445
- );
446
- }
447
- /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
448
- async releaseDbBranchLease(neonBranchId) {
449
- await this.request(
450
- "/api/sdk/replay/releaseDbBranchLease",
451
- { neonBranchId },
452
- { timeout: 3e4 }
453
- );
454
- }
455
- };
456
- }
457
- });
458
-
459
103
  // src/replayContext.ts
460
104
  function getReplayContext() {
461
105
  return replayContextStorage?.getStore() ?? null;
@@ -597,6 +241,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
597
241
  let result;
598
242
  let error = null;
599
243
  const replayedTraceId = crypto.randomUUID();
244
+ const pendingPersistence = [];
600
245
  try {
601
246
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
602
247
  const spanData = span.rawData?.span_data ?? {};
@@ -619,7 +264,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
619
264
  mockTree,
620
265
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
621
266
  mockStrategy,
622
- dbBranchLease: lease
267
+ dbBranchLease: lease,
268
+ pendingPersistence
623
269
  },
624
270
  () => fn(...inputs)
625
271
  );
@@ -627,6 +273,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
627
273
  } catch (e) {
628
274
  error = e instanceof Error ? e.message : String(e);
629
275
  } finally {
276
+ await Promise.allSettled(pendingPersistence);
630
277
  if (lease) {
631
278
  try {
632
279
  await httpClient.releaseDbBranchLease(lease.neonBranchId);
@@ -714,20 +361,46 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
714
361
  )
715
362
  );
716
363
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
717
- await flushTraces();
718
- let serverTraceIds = {};
719
- try {
720
- const completeResult = await httpClient.completeReplay(testRunId);
721
- serverTraceIds = completeResult.traceIds ?? {};
722
- } catch (e) {
364
+ const completeResult = await httpClient.completeReplay(testRunId);
365
+ const serverTraceIds = completeResult.traceIds;
366
+ if (serverTraceIds === void 0) {
723
367
  try {
724
- 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
+ );
725
371
  } catch {
726
372
  }
727
- }
728
- for (const item of resultItems) {
729
- if (item.traceId) {
730
- 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
+ }
731
404
  }
732
405
  }
733
406
  return {
@@ -740,7 +413,6 @@ var init_replay = __esm({
740
413
  "src/replay.ts"() {
741
414
  "use strict";
742
415
  init_errors();
743
- init_http();
744
416
  init_replayContext();
745
417
  init_serialize();
746
418
  }
@@ -772,9 +444,346 @@ registerAsyncLocalStorageClass(
772
444
  import_node_async_hooks.AsyncLocalStorage
773
445
  );
774
446
 
447
+ // src/version.generated.ts
448
+ var __version__ = "0.15.0";
449
+
450
+ // src/constants.ts
451
+ var DEFAULT_SERVICE_URL = "https://bitfab.ai";
452
+
453
+ // src/http.ts
454
+ init_errors();
455
+ var pendingTracePromises = /* @__PURE__ */ new Set();
456
+ function awaitOnExit(promise) {
457
+ pendingTracePromises.add(promise);
458
+ void promise.finally(() => {
459
+ pendingTracePromises.delete(promise);
460
+ }).catch(() => {
461
+ });
462
+ return promise;
463
+ }
464
+ async function flushTraces(timeoutMs = 5e3) {
465
+ if (pendingTracePromises.size === 0) {
466
+ return;
467
+ }
468
+ await Promise.race([
469
+ Promise.allSettled(Array.from(pendingTracePromises)),
470
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
471
+ ]);
472
+ }
473
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
474
+ let isFlushing = false;
475
+ process.on("beforeExit", () => {
476
+ if (pendingTracePromises.size > 0 && !isFlushing) {
477
+ isFlushing = true;
478
+ Promise.allSettled(
479
+ Array.from(pendingTracePromises).map(
480
+ (p) => p.catch(() => {
481
+ })
482
+ )
483
+ ).then(() => {
484
+ isFlushing = false;
485
+ }).catch(() => {
486
+ isFlushing = false;
487
+ });
488
+ }
489
+ });
490
+ }
491
+ var HttpClient = class {
492
+ constructor(config) {
493
+ this.apiKey = config.apiKey;
494
+ this.serviceUrl = config.serviceUrl;
495
+ this.timeout = config.timeout ?? 12e4;
496
+ }
497
+ /**
498
+ * Make an HTTP request to the Bitfab API. Defaults to POST; pass
499
+ * `options.method` to use a different verb (e.g. "PATCH").
500
+ *
501
+ * @param endpoint - The API endpoint (without base URL)
502
+ * @param payload - The request body
503
+ * @param options - Optional request options
504
+ * @returns The parsed JSON response
505
+ * @throws {BitfabError} If the request fails
506
+ */
507
+ async request(endpoint, payload, options) {
508
+ const url = `${this.serviceUrl}${endpoint}`;
509
+ const timeout = options?.timeout ?? this.timeout;
510
+ const method = options?.method ?? "POST";
511
+ const controller = new AbortController();
512
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
513
+ let body;
514
+ let serializationError;
515
+ try {
516
+ body = JSON.stringify(payload);
517
+ } catch (error) {
518
+ serializationError = error instanceof Error ? error.message : String(error);
519
+ body = JSON.stringify({
520
+ ...Object.fromEntries(
521
+ Object.entries(payload).filter(
522
+ ([, v]) => typeof v === "string" || typeof v === "number"
523
+ )
524
+ ),
525
+ rawSpan: {},
526
+ errors: [
527
+ { source: "sdk", step: "json_serialize", error: serializationError }
528
+ ]
529
+ });
530
+ }
531
+ try {
532
+ const response = await fetch(url, {
533
+ method,
534
+ headers: {
535
+ "Content-Type": "application/json",
536
+ Authorization: `Bearer ${this.apiKey}`
537
+ },
538
+ body,
539
+ signal: controller.signal
540
+ });
541
+ if (!response.ok) {
542
+ const errorText = await response.text();
543
+ throw new BitfabError(
544
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
545
+ );
546
+ }
547
+ const result = await response.json();
548
+ if (result.error) {
549
+ if (result.url) {
550
+ throw new BitfabError(
551
+ `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
552
+ result.url
553
+ );
554
+ }
555
+ throw new BitfabError(result.error);
556
+ }
557
+ return result;
558
+ } catch (error) {
559
+ if (error instanceof BitfabError) {
560
+ throw error;
561
+ }
562
+ if (error instanceof Error) {
563
+ if (error.name === "AbortError") {
564
+ throw new BitfabError(`Request timed out after ${timeout}ms`);
565
+ }
566
+ throw new BitfabError(error.message);
567
+ }
568
+ throw new BitfabError("Unknown error occurred");
569
+ } finally {
570
+ clearTimeout(timeoutId);
571
+ }
572
+ }
573
+ /**
574
+ * Look up a function by name.
575
+ * Blocks until complete - needed for function execution.
576
+ */
577
+ async lookupFunction(name) {
578
+ return this.request("/api/sdk/functions/lookup", { name });
579
+ }
580
+ /**
581
+ * Send an internal trace (from BAML execution).
582
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
583
+ */
584
+ sendInternalTrace(functionId, payload) {
585
+ void awaitOnExit(
586
+ this.request(`/api/sdk/functions/${functionId}/traces`, {
587
+ ...payload,
588
+ sdkVersion: __version__
589
+ })
590
+ ).catch((error) => {
591
+ try {
592
+ console.error("Bitfab: Failed to create trace:", error);
593
+ } catch {
594
+ }
595
+ });
596
+ }
597
+ /**
598
+ * Send an external span (from withSpan wrapper or OpenAI tracing).
599
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
600
+ * Returns the tracked promise so callers can optionally await it.
601
+ */
602
+ sendExternalSpan(payload) {
603
+ return awaitOnExit(
604
+ this.request("/api/sdk/externalSpans", {
605
+ ...payload,
606
+ sdkVersion: __version__
607
+ })
608
+ ).catch((error) => {
609
+ try {
610
+ console.error("Bitfab: Failed to create external span:", error);
611
+ } catch {
612
+ }
613
+ });
614
+ }
615
+ /**
616
+ * Send an external trace (from OpenAI tracing).
617
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
618
+ * Returns the tracked promise so callers can optionally await it
619
+ * (the replay path does, so trace completions are persisted before
620
+ * `completeReplay` builds the trace-ID mapping).
621
+ */
622
+ sendExternalTrace(payload) {
623
+ return awaitOnExit(
624
+ this.request("/api/sdk/externalTraces", {
625
+ ...payload,
626
+ sdkVersion: __version__
627
+ })
628
+ ).catch((error) => {
629
+ try {
630
+ console.error("Bitfab: Failed to create external trace:", error);
631
+ } catch {
632
+ }
633
+ });
634
+ }
635
+ /**
636
+ * Partial update of an existing external trace identified by sourceTraceId.
637
+ * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
638
+ * returns a tracked promise that callers may optionally await.
639
+ */
640
+ patchTrace(sourceTraceId, payload) {
641
+ const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
642
+ return awaitOnExit(
643
+ this.request(endpoint, payload, { method: "PATCH" })
644
+ ).catch((error) => {
645
+ try {
646
+ console.error("Bitfab: Failed to patch trace:", error);
647
+ } catch {
648
+ }
649
+ });
650
+ }
651
+ /**
652
+ * Start a replay session by fetching historical traces.
653
+ * Blocking call — creates a test run and returns lightweight item references.
654
+ */
655
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
656
+ const payload = { traceFunctionKey };
657
+ if (limit !== void 0) {
658
+ payload.limit = limit;
659
+ }
660
+ if (traceIds) {
661
+ payload.traceIds = traceIds;
662
+ }
663
+ if (codeChangeDescription !== void 0) {
664
+ payload.codeChangeDescription = codeChangeDescription;
665
+ }
666
+ if (codeChangeFiles !== void 0) {
667
+ payload.codeChangeFiles = codeChangeFiles;
668
+ }
669
+ if (includeDbBranchLease) {
670
+ payload.includeDbBranchLease = true;
671
+ }
672
+ if (experimentGroupId !== void 0) {
673
+ payload.experimentGroupId = experimentGroupId;
674
+ }
675
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
676
+ return this.request("/api/sdk/replay/start", payload, {
677
+ timeout
678
+ });
679
+ }
680
+ /**
681
+ * Fetch an external span by ID.
682
+ * Blocking GET request.
683
+ */
684
+ async getExternalSpan(spanId) {
685
+ const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
686
+ const controller = new AbortController();
687
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
688
+ try {
689
+ const response = await fetch(url, {
690
+ method: "GET",
691
+ headers: { Authorization: `Bearer ${this.apiKey}` },
692
+ signal: controller.signal
693
+ });
694
+ if (!response.ok) {
695
+ const errorText = await response.text();
696
+ throw new BitfabError(
697
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
698
+ );
699
+ }
700
+ return await response.json();
701
+ } catch (error) {
702
+ if (error instanceof BitfabError) {
703
+ throw error;
704
+ }
705
+ if (error instanceof Error) {
706
+ if (error.name === "AbortError") {
707
+ throw new BitfabError("Request timed out after 30000ms");
708
+ }
709
+ throw new BitfabError(error.message);
710
+ }
711
+ throw new BitfabError("Unknown error occurred");
712
+ } finally {
713
+ clearTimeout(timeoutId);
714
+ }
715
+ }
716
+ /**
717
+ * Fetch the span tree for a root span.
718
+ * Blocking GET request.
719
+ */
720
+ async getSpanTree(externalSpanId) {
721
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
722
+ const controller = new AbortController();
723
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
724
+ try {
725
+ const response = await fetch(url, {
726
+ method: "GET",
727
+ headers: { Authorization: `Bearer ${this.apiKey}` },
728
+ signal: controller.signal
729
+ });
730
+ if (!response.ok) {
731
+ const errorText = await response.text();
732
+ throw new BitfabError(
733
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
734
+ );
735
+ }
736
+ return await response.json();
737
+ } catch (error) {
738
+ if (error instanceof BitfabError) {
739
+ throw error;
740
+ }
741
+ if (error instanceof Error) {
742
+ if (error.name === "AbortError") {
743
+ throw new BitfabError("Request timed out after 30000ms");
744
+ }
745
+ throw new BitfabError(error.message);
746
+ }
747
+ throw new BitfabError("Unknown error occurred");
748
+ } finally {
749
+ clearTimeout(timeoutId);
750
+ }
751
+ }
752
+ /**
753
+ * Mark a replay test run as completed.
754
+ * Blocking call.
755
+ */
756
+ async completeReplay(testRunId) {
757
+ return this.request(
758
+ "/api/sdk/replay/complete",
759
+ { testRunId },
760
+ { timeout: 3e4 }
761
+ );
762
+ }
763
+ /**
764
+ * Ask the server to materialize a per-trace DB branch lease from a
765
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
766
+ * snapshot + preview branch and polls operations to readiness, which
767
+ * can take seconds.
768
+ */
769
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
770
+ return this.request(
771
+ "/api/sdk/replay/resolveDbBranchLease",
772
+ { testRunId, traceId, dbSnapshotRef },
773
+ { timeout: 9e4 }
774
+ );
775
+ }
776
+ /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
777
+ async releaseDbBranchLease(neonBranchId) {
778
+ await this.request(
779
+ "/api/sdk/replay/releaseDbBranchLease",
780
+ { neonBranchId },
781
+ { timeout: 3e4 }
782
+ );
783
+ }
784
+ };
785
+
775
786
  // src/claudeAgentSdk.ts
776
- init_constants();
777
- init_http();
778
787
  function nowIso() {
779
788
  return (/* @__PURE__ */ new Date()).toISOString();
780
789
  }
@@ -1539,9 +1548,6 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1539
1548
  };
1540
1549
  }
1541
1550
 
1542
- // src/client.ts
1543
- init_constants();
1544
-
1545
1551
  // src/dbSnapshot.ts
1546
1552
  init_errors();
1547
1553
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -1559,12 +1565,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1559
1565
  };
1560
1566
  }
1561
1567
 
1562
- // src/client.ts
1563
- init_http();
1564
-
1565
1568
  // src/langgraph.ts
1566
- init_constants();
1567
- init_http();
1568
1569
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1569
1570
  var LANGGRAPH_METADATA_KEYS = [
1570
1571
  "langgraph_step",
@@ -2112,8 +2113,6 @@ var ReplayEnvironment = class {
2112
2113
  init_serialize();
2113
2114
 
2114
2115
  // src/tracing.ts
2115
- init_constants();
2116
- init_http();
2117
2116
  var BitfabOpenAITracingProcessor = class {
2118
2117
  /**
2119
2118
  * Initialize the tracing processor.
@@ -2947,9 +2946,18 @@ var Bitfab = class {
2947
2946
  spanType: options.type ?? "custom"
2948
2947
  };
2949
2948
  const sendSpan = async (params) => {
2949
+ const replayCtx = getReplayContext();
2950
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
2951
+ let resolvePersistence;
2952
+ if (persistenceCollector) {
2953
+ persistenceCollector.push(
2954
+ new Promise((resolve) => {
2955
+ resolvePersistence = resolve;
2956
+ })
2957
+ );
2958
+ }
2950
2959
  try {
2951
2960
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2952
- const replayCtx = getReplayContext();
2953
2961
  const spanPromise = self.sendWrapperSpan({
2954
2962
  ...baseSpanParams,
2955
2963
  ...params,
@@ -2964,13 +2972,17 @@ var Bitfab = class {
2964
2972
  if (isRootSpan) {
2965
2973
  const pending = pendingSpanPromises.get(traceId) ?? [];
2966
2974
  pending.push(spanPromise);
2967
- await Promise.race([
2968
- Promise.allSettled(pending),
2969
- new Promise((resolve) => setTimeout(resolve, 5e3))
2970
- ]);
2975
+ if (persistenceCollector) {
2976
+ await Promise.allSettled(pending);
2977
+ } else {
2978
+ await Promise.race([
2979
+ Promise.allSettled(pending),
2980
+ new Promise((resolve) => setTimeout(resolve, 5e3))
2981
+ ]);
2982
+ }
2971
2983
  pendingSpanPromises.delete(traceId);
2972
2984
  const traceState = activeTraceStates.get(traceId);
2973
- self.sendTraceCompletion({
2985
+ const completionPromise = self.sendTraceCompletion({
2974
2986
  traceFunctionKey,
2975
2987
  traceId,
2976
2988
  startedAt: traceState?.startedAt ?? startedAt,
@@ -2983,6 +2995,9 @@ var Bitfab = class {
2983
2995
  dbSnapshotRef: traceState?.dbSnapshotRef
2984
2996
  });
2985
2997
  activeTraceStates.delete(traceId);
2998
+ if (persistenceCollector) {
2999
+ await completionPromise;
3000
+ }
2986
3001
  } else {
2987
3002
  const pending = pendingSpanPromises.get(traceId);
2988
3003
  if (pending) {
@@ -2992,6 +3007,8 @@ var Bitfab = class {
2992
3007
  }
2993
3008
  }
2994
3009
  } catch {
3010
+ } finally {
3011
+ resolvePersistence?.();
2995
3012
  }
2996
3013
  };
2997
3014
  const replayCtxForMock = getReplayContext();
@@ -3144,7 +3161,7 @@ var Bitfab = class {
3144
3161
  if (params.dbSnapshotRef) {
3145
3162
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3146
3163
  }
3147
- this.httpClient.sendExternalTrace({
3164
+ return this.httpClient.sendExternalTrace({
3148
3165
  type: "sdk-function",
3149
3166
  source: "typescript-sdk-function",
3150
3167
  traceFunctionKey: params.traceFunctionKey,
@@ -3289,10 +3306,6 @@ var BitfabFunction = class {
3289
3306
  }
3290
3307
  };
3291
3308
 
3292
- // src/index.ts
3293
- init_constants();
3294
- init_http();
3295
-
3296
3309
  // src/node.ts
3297
3310
  init_asyncStorage();
3298
3311
  assertAsyncStorageRegistered();