bitfab 0.13.8 → 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.13.8";
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,340 +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, limit };
328
- if (traceIds) {
329
- payload.traceIds = traceIds;
330
- }
331
- if (codeChangeDescription !== void 0) {
332
- payload.codeChangeDescription = codeChangeDescription;
333
- }
334
- if (codeChangeFiles !== void 0) {
335
- payload.codeChangeFiles = codeChangeFiles;
336
- }
337
- if (includeDbBranchLease) {
338
- payload.includeDbBranchLease = true;
339
- }
340
- if (experimentGroupId !== void 0) {
341
- payload.experimentGroupId = experimentGroupId;
342
- }
343
- const timeout = includeDbBranchLease ? 18e4 : 3e4;
344
- return this.request("/api/sdk/replay/start", payload, {
345
- timeout
346
- });
347
- }
348
- /**
349
- * Fetch an external span by ID.
350
- * Blocking GET request.
351
- */
352
- async getExternalSpan(spanId) {
353
- const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
354
- const controller = new AbortController();
355
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
356
- try {
357
- const response = await fetch(url, {
358
- method: "GET",
359
- headers: { Authorization: `Bearer ${this.apiKey}` },
360
- signal: controller.signal
361
- });
362
- if (!response.ok) {
363
- const errorText = await response.text();
364
- throw new BitfabError(
365
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
366
- );
367
- }
368
- return await response.json();
369
- } catch (error) {
370
- if (error instanceof BitfabError) {
371
- throw error;
372
- }
373
- if (error instanceof Error) {
374
- if (error.name === "AbortError") {
375
- throw new BitfabError("Request timed out after 30000ms");
376
- }
377
- throw new BitfabError(error.message);
378
- }
379
- throw new BitfabError("Unknown error occurred");
380
- } finally {
381
- clearTimeout(timeoutId);
382
- }
383
- }
384
- /**
385
- * Fetch the span tree for a root span.
386
- * Blocking GET request.
387
- */
388
- async getSpanTree(externalSpanId) {
389
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
390
- const controller = new AbortController();
391
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
392
- try {
393
- const response = await fetch(url, {
394
- method: "GET",
395
- headers: { Authorization: `Bearer ${this.apiKey}` },
396
- signal: controller.signal
397
- });
398
- if (!response.ok) {
399
- const errorText = await response.text();
400
- throw new BitfabError(
401
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
402
- );
403
- }
404
- return await response.json();
405
- } catch (error) {
406
- if (error instanceof BitfabError) {
407
- throw error;
408
- }
409
- if (error instanceof Error) {
410
- if (error.name === "AbortError") {
411
- throw new BitfabError("Request timed out after 30000ms");
412
- }
413
- throw new BitfabError(error.message);
414
- }
415
- throw new BitfabError("Unknown error occurred");
416
- } finally {
417
- clearTimeout(timeoutId);
418
- }
419
- }
420
- /**
421
- * Mark a replay test run as completed.
422
- * Blocking call.
423
- */
424
- async completeReplay(testRunId) {
425
- return this.request(
426
- "/api/sdk/replay/complete",
427
- { testRunId },
428
- { timeout: 3e4 }
429
- );
430
- }
431
- /**
432
- * Ask the server to materialize a per-trace DB branch lease from a
433
- * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
434
- * snapshot + preview branch and polls operations to readiness, which
435
- * can take seconds.
436
- */
437
- async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
438
- return this.request(
439
- "/api/sdk/replay/resolveDbBranchLease",
440
- { testRunId, traceId, dbSnapshotRef },
441
- { timeout: 9e4 }
442
- );
443
- }
444
- /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
445
- async releaseDbBranchLease(neonBranchId) {
446
- await this.request(
447
- "/api/sdk/replay/releaseDbBranchLease",
448
- { neonBranchId },
449
- { timeout: 3e4 }
450
- );
451
- }
452
- };
453
- }
454
- });
455
-
456
103
  // src/replayContext.ts
457
104
  function getReplayContext() {
458
105
  return replayContextStorage?.getStore() ?? null;
@@ -594,6 +241,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
594
241
  let result;
595
242
  let error = null;
596
243
  const replayedTraceId = crypto.randomUUID();
244
+ const pendingPersistence = [];
597
245
  try {
598
246
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
599
247
  const spanData = span.rawData?.span_data ?? {};
@@ -616,7 +264,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
616
264
  mockTree,
617
265
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
618
266
  mockStrategy,
619
- dbBranchLease: lease
267
+ dbBranchLease: lease,
268
+ pendingPersistence
620
269
  },
621
270
  () => fn(...inputs)
622
271
  );
@@ -624,6 +273,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
624
273
  } catch (e) {
625
274
  error = e instanceof Error ? e.message : String(e);
626
275
  } finally {
276
+ await Promise.allSettled(pendingPersistence);
627
277
  if (lease) {
628
278
  try {
629
279
  await httpClient.releaseDbBranchLease(lease.neonBranchId);
@@ -666,6 +316,21 @@ async function mapWithConcurrency(tasks, maxConcurrency) {
666
316
  return results;
667
317
  }
668
318
  async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
319
+ if (options?.traceIds !== void 0) {
320
+ if (options.traceIds.length === 0) {
321
+ throw new BitfabError("traceIds must contain at least one trace ID.");
322
+ }
323
+ if (options.traceIds.length > 100) {
324
+ throw new BitfabError(
325
+ `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
326
+ );
327
+ }
328
+ }
329
+ if (options?.limit !== void 0 && options?.traceIds !== void 0) {
330
+ throw new BitfabError(
331
+ "Pass either limit or traceIds, not both: an explicit trace ID list already determines how many traces replay."
332
+ );
333
+ }
669
334
  await replayContextReady;
670
335
  const {
671
336
  testRunId,
@@ -673,7 +338,9 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
673
338
  items: serverItems
674
339
  } = await httpClient.startReplay(
675
340
  traceFunctionKey,
676
- options?.limit ?? 5,
341
+ // limit is meaningless with explicit traceIds (the ID list determines
342
+ // the count), so it's omitted from the request entirely.
343
+ options?.traceIds ? void 0 : options?.limit ?? 5,
677
344
  options?.traceIds,
678
345
  options?.codeChangeDescription,
679
346
  options?.codeChangeFiles,
@@ -694,20 +361,46 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
694
361
  )
695
362
  );
696
363
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
697
- await flushTraces();
698
- let serverTraceIds = {};
699
- try {
700
- const completeResult = await httpClient.completeReplay(testRunId);
701
- serverTraceIds = completeResult.traceIds ?? {};
702
- } catch (e) {
364
+ const completeResult = await httpClient.completeReplay(testRunId);
365
+ const serverTraceIds = completeResult.traceIds;
366
+ if (serverTraceIds === void 0) {
703
367
  try {
704
- 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
+ );
705
371
  } catch {
706
372
  }
707
- }
708
- for (const item of resultItems) {
709
- if (item.traceId) {
710
- 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
+ }
711
404
  }
712
405
  }
713
406
  return {
@@ -719,7 +412,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
719
412
  var init_replay = __esm({
720
413
  "src/replay.ts"() {
721
414
  "use strict";
722
- init_http();
415
+ init_errors();
723
416
  init_replayContext();
724
417
  init_serialize();
725
418
  }
@@ -751,9 +444,346 @@ registerAsyncLocalStorageClass(
751
444
  import_node_async_hooks.AsyncLocalStorage
752
445
  );
753
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
+
754
786
  // src/claudeAgentSdk.ts
755
- init_constants();
756
- init_http();
757
787
  function nowIso() {
758
788
  return (/* @__PURE__ */ new Date()).toISOString();
759
789
  }
@@ -1518,9 +1548,6 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1518
1548
  };
1519
1549
  }
1520
1550
 
1521
- // src/client.ts
1522
- init_constants();
1523
-
1524
1551
  // src/dbSnapshot.ts
1525
1552
  init_errors();
1526
1553
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -1538,12 +1565,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1538
1565
  };
1539
1566
  }
1540
1567
 
1541
- // src/client.ts
1542
- init_http();
1543
-
1544
1568
  // src/langgraph.ts
1545
- init_constants();
1546
- init_http();
1547
1569
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1548
1570
  var LANGGRAPH_METADATA_KEYS = [
1549
1571
  "langgraph_step",
@@ -2091,8 +2113,6 @@ var ReplayEnvironment = class {
2091
2113
  init_serialize();
2092
2114
 
2093
2115
  // src/tracing.ts
2094
- init_constants();
2095
- init_http();
2096
2116
  var BitfabOpenAITracingProcessor = class {
2097
2117
  /**
2098
2118
  * Initialize the tracing processor.
@@ -2926,9 +2946,18 @@ var Bitfab = class {
2926
2946
  spanType: options.type ?? "custom"
2927
2947
  };
2928
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
+ }
2929
2959
  try {
2930
2960
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2931
- const replayCtx = getReplayContext();
2932
2961
  const spanPromise = self.sendWrapperSpan({
2933
2962
  ...baseSpanParams,
2934
2963
  ...params,
@@ -2943,13 +2972,17 @@ var Bitfab = class {
2943
2972
  if (isRootSpan) {
2944
2973
  const pending = pendingSpanPromises.get(traceId) ?? [];
2945
2974
  pending.push(spanPromise);
2946
- await Promise.race([
2947
- Promise.allSettled(pending),
2948
- new Promise((resolve) => setTimeout(resolve, 5e3))
2949
- ]);
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
+ }
2950
2983
  pendingSpanPromises.delete(traceId);
2951
2984
  const traceState = activeTraceStates.get(traceId);
2952
- self.sendTraceCompletion({
2985
+ const completionPromise = self.sendTraceCompletion({
2953
2986
  traceFunctionKey,
2954
2987
  traceId,
2955
2988
  startedAt: traceState?.startedAt ?? startedAt,
@@ -2962,6 +2995,9 @@ var Bitfab = class {
2962
2995
  dbSnapshotRef: traceState?.dbSnapshotRef
2963
2996
  });
2964
2997
  activeTraceStates.delete(traceId);
2998
+ if (persistenceCollector) {
2999
+ await completionPromise;
3000
+ }
2965
3001
  } else {
2966
3002
  const pending = pendingSpanPromises.get(traceId);
2967
3003
  if (pending) {
@@ -2971,6 +3007,8 @@ var Bitfab = class {
2971
3007
  }
2972
3008
  }
2973
3009
  } catch {
3010
+ } finally {
3011
+ resolvePersistence?.();
2974
3012
  }
2975
3013
  };
2976
3014
  const replayCtxForMock = getReplayContext();
@@ -3123,7 +3161,7 @@ var Bitfab = class {
3123
3161
  if (params.dbSnapshotRef) {
3124
3162
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3125
3163
  }
3126
- this.httpClient.sendExternalTrace({
3164
+ return this.httpClient.sendExternalTrace({
3127
3165
  type: "sdk-function",
3128
3166
  source: "typescript-sdk-function",
3129
3167
  traceFunctionKey: params.traceFunctionKey,
@@ -3197,7 +3235,9 @@ var Bitfab = class {
3197
3235
  *
3198
3236
  * @param traceFunctionKey - The trace function key to replay
3199
3237
  * @param fn - The function to replay (must be the return value of `withSpan`)
3200
- * @param options - Optional replay options (limit, traceIds)
3238
+ * @param options - Optional replay options. `limit` and `traceIds` are
3239
+ * mutually exclusive — an explicit ID list already determines how many
3240
+ * traces replay, so passing both throws a BitfabError.
3201
3241
  * @returns ReplayResult with items, testRunId, and testRunUrl
3202
3242
  */
3203
3243
  async replay(traceFunctionKey, fn, options) {
@@ -3266,10 +3306,6 @@ var BitfabFunction = class {
3266
3306
  }
3267
3307
  };
3268
3308
 
3269
- // src/index.ts
3270
- init_constants();
3271
- init_http();
3272
-
3273
3309
  // src/node.ts
3274
3310
  init_asyncStorage();
3275
3311
  assertAsyncStorageRegistered();