bitfab 0.61.3 → 0.61.5

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.
Files changed (32) hide show
  1. package/dist/{chunk-6IMUONDN.js → chunk-4TRH3X3S.js} +31 -3
  2. package/dist/chunk-4TRH3X3S.js.map +1 -0
  3. package/dist/{chunk-IZUZ5NBB.js → chunk-7OXXBUGQ.js} +178 -11
  4. package/dist/chunk-7OXXBUGQ.js.map +1 -0
  5. package/dist/{chunk-PAUXHTNF.js → chunk-KOANTEGQ.js} +6 -6
  6. package/dist/{chunk-PAUXHTNF.js.map → chunk-KOANTEGQ.js.map} +1 -1
  7. package/dist/{chunk-HVEZHLVQ.js → chunk-R5RMFRHX.js} +30 -3
  8. package/dist/{chunk-HVEZHLVQ.js.map → chunk-R5RMFRHX.js.map} +1 -1
  9. package/dist/{chunk-WOHIAKZF.js → chunk-VD5PS6WE.js} +28 -4
  10. package/dist/chunk-VD5PS6WE.js.map +1 -0
  11. package/dist/{http-BYICBOWP.js → http-CR2SEMK4.js} +2 -2
  12. package/dist/{http-3IYU5Z54.js → http-SK2QESXJ.js} +2 -2
  13. package/dist/index.cjs +220 -11
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +3 -3
  18. package/dist/node.cjs +220 -11
  19. package/dist/node.cjs.map +1 -1
  20. package/dist/node.d.cts +1 -1
  21. package/dist/node.d.ts +1 -1
  22. package/dist/node.js +3 -3
  23. package/dist/{replay-SXHX5BBU.js → replay-YSR7FD4I.js} +3 -3
  24. package/dist/replayCli.js +2 -2
  25. package/dist/seedCli.js +2 -2
  26. package/package.json +1 -1
  27. package/dist/chunk-6IMUONDN.js.map +0 -1
  28. package/dist/chunk-IZUZ5NBB.js.map +0 -1
  29. package/dist/chunk-WOHIAKZF.js.map +0 -1
  30. /package/dist/{http-3IYU5Z54.js.map → http-CR2SEMK4.js.map} +0 -0
  31. /package/dist/{http-BYICBOWP.js.map → http-SK2QESXJ.js.map} +0 -0
  32. /package/dist/{replay-SXHX5BBU.js.map → replay-YSR7FD4I.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -51,7 +51,7 @@ var __version__, __packageName__;
51
51
  var init_version_generated = __esm({
52
52
  "src/version.generated.ts"() {
53
53
  "use strict";
54
- __version__ = "0.61.3";
54
+ __version__ = "0.61.5";
55
55
  __packageName__ = "bitfab";
56
56
  }
57
57
  });
@@ -2286,7 +2286,7 @@ function warnDroppedExternalTrace(error) {
2286
2286
  function experimentIdWireFields(experimentId) {
2287
2287
  return { experimentId, testRunId: experimentId };
2288
2288
  }
2289
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2289
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS, REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2290
2290
  var init_http = __esm({
2291
2291
  "src/http.ts"() {
2292
2292
  "use strict";
@@ -2305,6 +2305,8 @@ var init_http = __esm({
2305
2305
  init_warnOnce();
2306
2306
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2307
2307
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2308
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2309
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2308
2310
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2309
2311
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2310
2312
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -2892,7 +2894,7 @@ var init_http = __esm({
2892
2894
  * Start a replay session by fetching historical traces.
2893
2895
  * Blocking call - creates an experiment and returns lightweight item references.
2894
2896
  */
2895
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2897
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay, notes) {
2896
2898
  const payload = { traceFunctionKey };
2897
2899
  if (experimentalSelectiveReplay !== void 0) {
2898
2900
  payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
@@ -2906,6 +2908,9 @@ var init_http = __esm({
2906
2908
  if (name !== void 0) {
2907
2909
  payload.name = name;
2908
2910
  }
2911
+ if (notes !== void 0) {
2912
+ payload.notes = notes;
2913
+ }
2909
2914
  if (codeChangeDescription !== void 0) {
2910
2915
  payload.codeChangeDescription = codeChangeDescription;
2911
2916
  }
@@ -3117,6 +3122,28 @@ var init_http = __esm({
3117
3122
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3118
3123
  );
3119
3124
  }
3125
+ async interruptReplay(experimentId) {
3126
+ try {
3127
+ await this.request(
3128
+ "/api/sdk/replay/interrupt",
3129
+ experimentIdWireFields(experimentId),
3130
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3131
+ );
3132
+ return true;
3133
+ } catch {
3134
+ return false;
3135
+ }
3136
+ }
3137
+ async heartbeatReplay(experimentId) {
3138
+ try {
3139
+ await this.request(
3140
+ "/api/sdk/replay/heartbeat",
3141
+ experimentIdWireFields(experimentId),
3142
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3143
+ );
3144
+ } catch {
3145
+ }
3146
+ }
3120
3147
  async getReseedSource(traceId) {
3121
3148
  return this.get(
3122
3149
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3620,6 +3647,114 @@ var init_integrationFixture = __esm({
3620
3647
  }
3621
3648
  });
3622
3649
 
3650
+ // src/replayHeartbeat.ts
3651
+ function startReplayHeartbeat(send) {
3652
+ let stopped = false;
3653
+ let inFlight;
3654
+ const timer = setInterval(() => {
3655
+ if (stopped || inFlight !== void 0) {
3656
+ return;
3657
+ }
3658
+ inFlight = send().catch(() => void 0).finally(() => {
3659
+ inFlight = void 0;
3660
+ });
3661
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3662
+ unrefTimer(timer);
3663
+ return {
3664
+ stop: async () => {
3665
+ stopped = true;
3666
+ clearInterval(timer);
3667
+ await inFlight;
3668
+ }
3669
+ };
3670
+ }
3671
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3672
+ var init_replayHeartbeat = __esm({
3673
+ "src/replayHeartbeat.ts"() {
3674
+ "use strict";
3675
+ init_unrefTimer();
3676
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3677
+ }
3678
+ });
3679
+
3680
+ // src/replayInterrupt.ts
3681
+ function signalCapableProcess() {
3682
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3683
+ return void 0;
3684
+ }
3685
+ return process;
3686
+ }
3687
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3688
+ if (markedInterrupted) {
3689
+ return `[replay] Experiment ${experimentId} interrupted.`;
3690
+ }
3691
+ return `[replay] Experiment ${experimentId} stopped, but Bitfab could not mark it interrupted. It will be marked interrupted within 15 minutes. A resume in the next two minutes may be refused as still running.`;
3692
+ }
3693
+ function reportInterrupted(experimentId, markedInterrupted) {
3694
+ try {
3695
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3696
+ } catch {
3697
+ }
3698
+ }
3699
+ function installReplayInterruptHandlers(target) {
3700
+ const host = signalCapableProcess();
3701
+ if (host === void 0) {
3702
+ return { remove: () => {
3703
+ }, interrupted: () => false };
3704
+ }
3705
+ const handlers = /* @__PURE__ */ new Map();
3706
+ const removeHandlers = () => {
3707
+ for (const [signal, handler] of handlers) {
3708
+ host.removeListener(signal, handler);
3709
+ }
3710
+ handlers.clear();
3711
+ };
3712
+ let interrupting = false;
3713
+ const handleSignal = async (signal) => {
3714
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3715
+ const markedInterrupted = await target.interrupt();
3716
+ removeHandlers();
3717
+ if (target.onInterrupt !== void 0) {
3718
+ try {
3719
+ target.onInterrupt({
3720
+ experimentId: target.experimentId,
3721
+ experimentUrl: target.experimentUrl,
3722
+ signal,
3723
+ markedInterrupted
3724
+ });
3725
+ } catch {
3726
+ }
3727
+ return;
3728
+ }
3729
+ reportInterrupted(target.experimentId, markedInterrupted);
3730
+ if (!hostHandlesSignal) {
3731
+ host.kill(host.pid, signal);
3732
+ }
3733
+ };
3734
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3735
+ const handler = () => {
3736
+ if (interrupting) {
3737
+ return;
3738
+ }
3739
+ interrupting = true;
3740
+ void handleSignal(signal);
3741
+ };
3742
+ handlers.set(signal, handler);
3743
+ host.on(signal, handler);
3744
+ }
3745
+ return { remove: removeHandlers, interrupted: () => interrupting };
3746
+ }
3747
+ var REPLAY_INTERRUPT_SIGNALS;
3748
+ var init_replayInterrupt = __esm({
3749
+ "src/replayInterrupt.ts"() {
3750
+ "use strict";
3751
+ REPLAY_INTERRUPT_SIGNALS = [
3752
+ "SIGINT",
3753
+ "SIGTERM"
3754
+ ];
3755
+ }
3756
+ });
3757
+
3623
3758
  // src/selectiveReplay.ts
3624
3759
  function replayNodeKey(node) {
3625
3760
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -4529,7 +4664,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4529
4664
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4530
4665
  let session;
4531
4666
  if (options?.resume === void 0) {
4532
- const started2 = await httpClient.startReplay(
4667
+ const started = await httpClient.startReplay(
4533
4668
  traceFunctionKey,
4534
4669
  // limit is meaningless with explicit traceIds (the ID list determines
4535
4670
  // the count), so it's omitted from the request entirely.
@@ -4550,9 +4685,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4550
4685
  options?.adaptInputs !== void 0,
4551
4686
  options?.onlyWithAssertions,
4552
4687
  options?.judgeAssertions,
4553
- options?.experimentalSelectiveReplay
4688
+ options?.experimentalSelectiveReplay,
4689
+ options?.notes
4554
4690
  );
4555
- if (started2.items.length === 0) {
4691
+ if (started.items.length === 0) {
4556
4692
  try {
4557
4693
  console.warn(
4558
4694
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4561,10 +4697,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4561
4697
  }
4562
4698
  }
4563
4699
  session = {
4564
- experimentId: started2.experimentId,
4565
- experimentUrl: started2.experimentUrl,
4700
+ experimentId: started.experimentId,
4701
+ experimentUrl: started.experimentUrl,
4566
4702
  attempts: requestedAttempts,
4567
- work: expandReplayWork(started2.items, requestedAttempts),
4703
+ work: expandReplayWork(started.items, requestedAttempts),
4568
4704
  finished: [],
4569
4705
  includeDbBranchLease,
4570
4706
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4579,9 +4715,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4579
4715
  options.dbBranch
4580
4716
  );
4581
4717
  }
4582
- const { experimentId, experimentUrl, attempts } = session;
4718
+ const { experimentId } = session;
4719
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4720
+ const heartbeat = startReplayHeartbeat(
4721
+ () => httpClient.heartbeatReplay(experimentId)
4722
+ );
4723
+ const interruptReplay = async () => {
4724
+ await heartbeat.stop();
4725
+ return httpClient.interruptReplay(experimentId);
4726
+ };
4727
+ const interruptHandlers = installReplayInterruptHandlers({
4728
+ experimentId,
4729
+ experimentUrl: fullExperimentUrl,
4730
+ interrupt: interruptReplay,
4731
+ onInterrupt: options?.onInterrupt
4732
+ });
4733
+ let completeReturned = false;
4734
+ const markCompleteReturned = () => {
4735
+ completeReturned = true;
4736
+ void heartbeat.stop();
4737
+ };
4738
+ try {
4739
+ return await runReplaySession(
4740
+ httpClient,
4741
+ fn,
4742
+ session,
4743
+ fullExperimentUrl,
4744
+ maxConcurrency,
4745
+ interruptHandlers.remove,
4746
+ markCompleteReturned,
4747
+ interruptHandlers.interrupted,
4748
+ options,
4749
+ registeredOverrides
4750
+ );
4751
+ } catch (error) {
4752
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4753
+ await interruptReplay();
4754
+ }
4755
+ throw error;
4756
+ } finally {
4757
+ interruptHandlers.remove();
4758
+ await heartbeat.stop();
4759
+ }
4760
+ }
4761
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4762
+ if (!interrupted()) {
4763
+ return;
4764
+ }
4765
+ const cause = new BitfabError(
4766
+ `Experiment ${experimentId} was interrupted, so it was not completed or graded. Resume it by passing { resume: "${experimentId}" } to replay, or with bitfab-replay --resume ${experimentId}.`,
4767
+ experimentUrl
4768
+ );
4769
+ throw new ReplayError(
4770
+ cause.message,
4771
+ items,
4772
+ experimentId,
4773
+ experimentUrl,
4774
+ cause
4775
+ );
4776
+ }
4777
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4778
+ const { experimentId, attempts } = session;
4583
4779
  const mockStrategy = options?.mock ?? "marked";
4584
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4585
4780
  try {
4586
4781
  options?.onExperimentStart?.({
4587
4782
  experimentId,
@@ -4731,7 +4926,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4731
4926
  );
4732
4927
  const resultItems = [...carriedOverItems, ...ranItems];
4733
4928
  if (options?.dryRun === true) {
4929
+ throwIfInterrupted(
4930
+ interrupted,
4931
+ resultItems,
4932
+ experimentId,
4933
+ fullExperimentUrl
4934
+ );
4935
+ stopInterruptingOnSignal();
4734
4936
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4937
+ markCompleteReturned();
4735
4938
  const dryResult = {
4736
4939
  items: resultItems,
4737
4940
  experimentId,
@@ -4756,12 +4959,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4756
4959
  ranItems[index].traceId = readBack;
4757
4960
  }
4758
4961
  }
4962
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4963
+ stopInterruptingOnSignal();
4759
4964
  const completeResult = await preserveReplayFailure(
4760
4965
  () => httpClient.completeReplay(experimentId),
4761
4966
  resultItems,
4762
4967
  experimentId,
4763
4968
  fullExperimentUrl
4764
4969
  );
4970
+ markCompleteReturned();
4765
4971
  const serverTraceIds = completeResult.traceIds;
4766
4972
  const replayTokens = completeResult.tokens;
4767
4973
  if (serverTraceIds !== void 0) {
@@ -4865,6 +5071,8 @@ var init_replay = __esm({
4865
5071
  init_mockOverride();
4866
5072
  init_randomUuid();
4867
5073
  init_replayContext();
5074
+ init_replayHeartbeat();
5075
+ init_replayInterrupt();
4868
5076
  init_selectiveReplayRuntime();
4869
5077
  init_serialize();
4870
5078
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -11641,6 +11849,7 @@ init_replay();
11641
11849
  // src/replayRegistry.ts
11642
11850
  init_errors();
11643
11851
  init_replay();
11852
+ init_replayInterrupt();
11644
11853
  function defineReplayRegistry(registry) {
11645
11854
  return registry;
11646
11855
  }