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