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