@bitfab/sdk 0.61.2 → 0.61.4

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 (41) hide show
  1. package/dist/autoTrace.cjs.map +1 -1
  2. package/dist/autoTrace.d.cts +1 -0
  3. package/dist/autoTrace.d.ts +1 -0
  4. package/dist/autoTrace.js +1 -1
  5. package/dist/{chunk-HNEKBDI7.js → chunk-2ZZBNQVA.js} +38 -7
  6. package/dist/chunk-2ZZBNQVA.js.map +1 -0
  7. package/dist/{chunk-IR5KCKHF.js → chunk-3GSEECNP.js} +30 -3
  8. package/dist/chunk-3GSEECNP.js.map +1 -0
  9. package/dist/{chunk-DSVQPSAL.js → chunk-BRSQCK3F.js} +21 -3
  10. package/dist/chunk-BRSQCK3F.js.map +1 -0
  11. package/dist/{chunk-X5DRPZJJ.js → chunk-FL6BNPSO.js} +480 -15
  12. package/dist/chunk-FL6BNPSO.js.map +1 -0
  13. package/dist/{chunk-BMJNDIVA.js → chunk-K4XCWQBC.js} +1 -1
  14. package/dist/chunk-K4XCWQBC.js.map +1 -0
  15. package/dist/{chunk-SJPMWVJ3.js → chunk-THGHTVB6.js} +32 -3
  16. package/dist/chunk-THGHTVB6.js.map +1 -0
  17. package/dist/{http-OEABHRHY.js → http-J2DYDKWJ.js} +2 -2
  18. package/dist/{http-RNI6PKQ3.js → http-RH3SW32W.js} +2 -2
  19. package/dist/index.cjs +568 -15
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +131 -3
  22. package/dist/index.d.ts +131 -3
  23. package/dist/index.js +4 -4
  24. package/dist/node.cjs +568 -15
  25. package/dist/node.cjs.map +1 -1
  26. package/dist/node.d.cts +1 -1
  27. package/dist/node.d.ts +1 -1
  28. package/dist/node.js +4 -4
  29. package/dist/{replay-3ERB6W6N.js → replay-CZWFAAIY.js} +3 -3
  30. package/dist/replayCli.js +2 -2
  31. package/dist/seedCli.js +2 -2
  32. package/package.json +1 -1
  33. package/dist/chunk-BMJNDIVA.js.map +0 -1
  34. package/dist/chunk-DSVQPSAL.js.map +0 -1
  35. package/dist/chunk-HNEKBDI7.js.map +0 -1
  36. package/dist/chunk-IR5KCKHF.js.map +0 -1
  37. package/dist/chunk-SJPMWVJ3.js.map +0 -1
  38. package/dist/chunk-X5DRPZJJ.js.map +0 -1
  39. /package/dist/{http-OEABHRHY.js.map → http-J2DYDKWJ.js.map} +0 -0
  40. /package/dist/{http-RNI6PKQ3.js.map → http-RH3SW32W.js.map} +0 -0
  41. /package/dist/{replay-3ERB6W6N.js.map → replay-CZWFAAIY.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -42,7 +42,7 @@ var __version__, __packageName__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.61.2";
45
+ __version__ = "0.61.4";
46
46
  __packageName__ = "@bitfab/sdk";
47
47
  }
48
48
  });
@@ -1068,6 +1068,7 @@ var init_simulationPlan = __esm({
1068
1068
  "input_meta",
1069
1069
  "output",
1070
1070
  "output_meta",
1071
+ "replay_recording",
1071
1072
  "prompt"
1072
1073
  ];
1073
1074
  SimulationPlan = class {
@@ -2276,7 +2277,7 @@ function warnDroppedExternalTrace(error) {
2276
2277
  function experimentIdWireFields(experimentId) {
2277
2278
  return { experimentId, testRunId: experimentId };
2278
2279
  }
2279
- 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;
2280
+ 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;
2280
2281
  var init_http = __esm({
2281
2282
  "src/http.ts"() {
2282
2283
  "use strict";
@@ -2295,6 +2296,8 @@ var init_http = __esm({
2295
2296
  init_warnOnce();
2296
2297
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2297
2298
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2299
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2300
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2298
2301
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2299
2302
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2300
2303
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -2882,8 +2885,11 @@ var init_http = __esm({
2882
2885
  * Start a replay session by fetching historical traces.
2883
2886
  * Blocking call - creates an experiment and returns lightweight item references.
2884
2887
  */
2885
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2888
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2886
2889
  const payload = { traceFunctionKey };
2890
+ if (experimentalSelectiveReplay !== void 0) {
2891
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2892
+ }
2887
2893
  if (limit !== void 0) {
2888
2894
  payload.limit = limit;
2889
2895
  }
@@ -3104,6 +3110,28 @@ var init_http = __esm({
3104
3110
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3105
3111
  );
3106
3112
  }
3113
+ async interruptReplay(experimentId) {
3114
+ try {
3115
+ await this.request(
3116
+ "/api/sdk/replay/interrupt",
3117
+ experimentIdWireFields(experimentId),
3118
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3119
+ );
3120
+ return true;
3121
+ } catch {
3122
+ return false;
3123
+ }
3124
+ }
3125
+ async heartbeatReplay(experimentId) {
3126
+ try {
3127
+ await this.request(
3128
+ "/api/sdk/replay/heartbeat",
3129
+ experimentIdWireFields(experimentId),
3130
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3131
+ );
3132
+ } catch {
3133
+ }
3134
+ }
3107
3135
  async getReseedSource(traceId) {
3108
3136
  return this.get(
3109
3137
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3607,6 +3635,399 @@ var init_integrationFixture = __esm({
3607
3635
  }
3608
3636
  });
3609
3637
 
3638
+ // src/replayHeartbeat.ts
3639
+ function startReplayHeartbeat(send) {
3640
+ let stopped = false;
3641
+ let inFlight;
3642
+ const timer = setInterval(() => {
3643
+ if (stopped || inFlight !== void 0) {
3644
+ return;
3645
+ }
3646
+ inFlight = send().catch(() => void 0).finally(() => {
3647
+ inFlight = void 0;
3648
+ });
3649
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3650
+ unrefTimer(timer);
3651
+ return {
3652
+ stop: async () => {
3653
+ stopped = true;
3654
+ clearInterval(timer);
3655
+ await inFlight;
3656
+ }
3657
+ };
3658
+ }
3659
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3660
+ var init_replayHeartbeat = __esm({
3661
+ "src/replayHeartbeat.ts"() {
3662
+ "use strict";
3663
+ init_unrefTimer();
3664
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3665
+ }
3666
+ });
3667
+
3668
+ // src/replayInterrupt.ts
3669
+ function signalCapableProcess() {
3670
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3671
+ return void 0;
3672
+ }
3673
+ return process;
3674
+ }
3675
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3676
+ if (markedInterrupted) {
3677
+ return `[replay] Experiment ${experimentId} interrupted.`;
3678
+ }
3679
+ 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.`;
3680
+ }
3681
+ function reportInterrupted(experimentId, markedInterrupted) {
3682
+ try {
3683
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3684
+ } catch {
3685
+ }
3686
+ }
3687
+ function installReplayInterruptHandlers(target) {
3688
+ const host = signalCapableProcess();
3689
+ if (host === void 0) {
3690
+ return { remove: () => {
3691
+ }, interrupted: () => false };
3692
+ }
3693
+ const handlers = /* @__PURE__ */ new Map();
3694
+ const removeHandlers = () => {
3695
+ for (const [signal, handler] of handlers) {
3696
+ host.removeListener(signal, handler);
3697
+ }
3698
+ handlers.clear();
3699
+ };
3700
+ let interrupting = false;
3701
+ const handleSignal = async (signal) => {
3702
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3703
+ const markedInterrupted = await target.interrupt();
3704
+ removeHandlers();
3705
+ if (target.onInterrupt !== void 0) {
3706
+ try {
3707
+ target.onInterrupt({
3708
+ experimentId: target.experimentId,
3709
+ experimentUrl: target.experimentUrl,
3710
+ signal,
3711
+ markedInterrupted
3712
+ });
3713
+ } catch {
3714
+ }
3715
+ return;
3716
+ }
3717
+ reportInterrupted(target.experimentId, markedInterrupted);
3718
+ if (!hostHandlesSignal) {
3719
+ host.kill(host.pid, signal);
3720
+ }
3721
+ };
3722
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3723
+ const handler = () => {
3724
+ if (interrupting) {
3725
+ return;
3726
+ }
3727
+ interrupting = true;
3728
+ void handleSignal(signal);
3729
+ };
3730
+ handlers.set(signal, handler);
3731
+ host.on(signal, handler);
3732
+ }
3733
+ return { remove: removeHandlers, interrupted: () => interrupting };
3734
+ }
3735
+ var REPLAY_INTERRUPT_SIGNALS;
3736
+ var init_replayInterrupt = __esm({
3737
+ "src/replayInterrupt.ts"() {
3738
+ "use strict";
3739
+ REPLAY_INTERRUPT_SIGNALS = [
3740
+ "SIGINT",
3741
+ "SIGTERM"
3742
+ ];
3743
+ }
3744
+ });
3745
+
3746
+ // src/selectiveReplay.ts
3747
+ function replayNodeKey(node) {
3748
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3749
+ }
3750
+ function jsonFingerprint(value) {
3751
+ const seen = /* @__PURE__ */ new Set();
3752
+ function encode(input) {
3753
+ if (input === null || typeof input === "boolean") {
3754
+ return JSON.stringify(input);
3755
+ }
3756
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3757
+ return JSON.stringify(input);
3758
+ }
3759
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3760
+ return JSON.stringify(input);
3761
+ }
3762
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3763
+ throw new Error("Not a lossless JSON value");
3764
+ }
3765
+ seen.add(input);
3766
+ const prototype = Object.getPrototypeOf(input);
3767
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3768
+ throw new Error("Not a plain JSON object");
3769
+ }
3770
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3771
+ throw new Error("Symbol properties are not recorded");
3772
+ }
3773
+ const keys = Object.keys(input);
3774
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3775
+ throw new Error("Non-enumerable properties are not recorded");
3776
+ }
3777
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3778
+ throw new Error("Not a dense JSON array");
3779
+ }
3780
+ const properties = keys.map((key) => {
3781
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3782
+ if (!descriptor || !("value" in descriptor)) {
3783
+ throw new Error("Accessor properties are not replay inputs");
3784
+ }
3785
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3786
+ });
3787
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3788
+ }
3789
+ try {
3790
+ return encode(value);
3791
+ } catch {
3792
+ return void 0;
3793
+ }
3794
+ }
3795
+ var init_selectiveReplay = __esm({
3796
+ "src/selectiveReplay.ts"() {
3797
+ "use strict";
3798
+ }
3799
+ });
3800
+
3801
+ // src/selectiveReplayRuntime.ts
3802
+ function validateSelectiveReplayOptions(options) {
3803
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3804
+ (node) => typeof node?.traceFunctionKey === "string" && node.traceFunctionKey.length > 0 && node.traceFunctionKey.length <= 500 && typeof node.spanName === "string" && node.spanName.length > 0 && node.spanName.length <= 500
3805
+ );
3806
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3807
+ throw new BitfabError(
3808
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3809
+ );
3810
+ }
3811
+ }
3812
+ function policyKey(options) {
3813
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3814
+ }
3815
+ var SelectiveReplayRuntime;
3816
+ var init_selectiveReplayRuntime = __esm({
3817
+ "src/selectiveReplayRuntime.ts"() {
3818
+ "use strict";
3819
+ init_errors();
3820
+ init_selectiveReplay();
3821
+ SelectiveReplayRuntime = class {
3822
+ constructor(plan, options) {
3823
+ this.plan = plan;
3824
+ this.options = options;
3825
+ this.nodes = /* @__PURE__ */ new Map();
3826
+ this.children = /* @__PURE__ */ new Map();
3827
+ this.runtimeParents = /* @__PURE__ */ new Map();
3828
+ this.consumed = /* @__PURE__ */ new Map();
3829
+ this.requiredScopes = /* @__PURE__ */ new Set();
3830
+ this.recordings = /* @__PURE__ */ new Map();
3831
+ this.reached = /* @__PURE__ */ new Set();
3832
+ this.decisions = [];
3833
+ this.assertionNames = /* @__PURE__ */ new Set();
3834
+ this.assertionScopes = /* @__PURE__ */ new Set();
3835
+ this.observedNames = /* @__PURE__ */ new Map();
3836
+ this.unresolvedAssertions = [];
3837
+ validateSelectiveReplayOptions(options);
3838
+ if (plan?.version !== 4 || !Array.isArray(plan.nodes) || plan.nodes.length === 0 || plan.nodes.length > 500 || !Array.isArray(plan.unresolvedMustRun) || !Array.isArray(plan.assertions) || !plan.options) {
3839
+ throw new BitfabError(
3840
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3841
+ );
3842
+ }
3843
+ validateSelectiveReplayOptions(plan.options);
3844
+ if (policyKey(plan.options) !== policyKey(options)) {
3845
+ throw new BitfabError(
3846
+ "Selective replay plan does not match the requested mustRun nodes."
3847
+ );
3848
+ }
3849
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3850
+ for (const node of plan.nodes) {
3851
+ if (typeof node.id !== "string" || this.nodes.has(node.id) || typeof node.traceFunctionKey !== "string" || typeof node.spanName !== "string" || typeof node.mustRun !== "boolean" || typeof node.reusable !== "boolean") {
3852
+ throw new BitfabError("Invalid selective replay node.");
3853
+ }
3854
+ this.nodes.set(node.id, node);
3855
+ if (node.recording !== void 0) {
3856
+ this.recordings.set(node.id, {
3857
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3858
+ output: jsonFingerprint(node.recording.output)
3859
+ });
3860
+ }
3861
+ if (node.parentId !== null) {
3862
+ const siblings = this.children.get(node.parentId) ?? [];
3863
+ siblings.push(node);
3864
+ this.children.set(node.parentId, siblings);
3865
+ }
3866
+ }
3867
+ const root = this.nodes.get(plan.rootId);
3868
+ if (!root || root.parentId !== null || !root.mustRun) {
3869
+ throw new BitfabError("Selective replay requires a live recorded root.");
3870
+ }
3871
+ const assertionIds = /* @__PURE__ */ new Set();
3872
+ let wholeTrace = false;
3873
+ for (const assertion of plan.assertions) {
3874
+ const target = assertion?.target;
3875
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3876
+ throw new BitfabError(
3877
+ "Invalid selective replay assertion requirements."
3878
+ );
3879
+ }
3880
+ assertionIds.add(assertion.id);
3881
+ if (target === null) {
3882
+ wholeTrace = true;
3883
+ } else if (target.kind === "span") {
3884
+ if (typeof target.name !== "string" || target.name.length === 0 || target.occurrence !== void 0 && target.occurrence !== "first" && target.occurrence !== "last" && !(Number.isInteger(target.occurrence) && typeof target.occurrence === "number" && target.occurrence >= 0)) {
3885
+ throw new BitfabError("Invalid selective replay assertion target.");
3886
+ }
3887
+ this.assertionNames.add(target.name);
3888
+ const count = plan.nodes.filter(
3889
+ (node) => node.spanName === target.name
3890
+ ).length;
3891
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3892
+ this.unresolvedAssertions.push(assertion.id);
3893
+ }
3894
+ }
3895
+ }
3896
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3897
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3898
+ const unresolved = options.mustRun.filter(
3899
+ (node) => !recordedKeys.has(replayNodeKey(node))
3900
+ );
3901
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3902
+ throw new BitfabError(
3903
+ "Selective replay plan has incomplete mustRun coverage."
3904
+ );
3905
+ }
3906
+ for (const node of plan.nodes) {
3907
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3908
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3909
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3910
+ let parentId = node.parentId;
3911
+ while (parentId !== null) {
3912
+ const parent = this.nodes.get(parentId);
3913
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3914
+ throw new BitfabError("Invalid selective replay ancestry.");
3915
+ }
3916
+ ancestors.add(parentId);
3917
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3918
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3919
+ parentId = parent.parentId;
3920
+ }
3921
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3922
+ throw new BitfabError("Selective replay plan could hide required code.");
3923
+ }
3924
+ if (requiredForAssertion && !node.mustRun) {
3925
+ throw new BitfabError(
3926
+ "Selective replay plan could hide assertion evidence."
3927
+ );
3928
+ }
3929
+ }
3930
+ }
3931
+ enter(params) {
3932
+ const identity = {
3933
+ traceFunctionKey: params.traceFunctionKey,
3934
+ spanName: params.spanName
3935
+ };
3936
+ const key = replayNodeKey(identity);
3937
+ const fingerprint = jsonFingerprint(params.inputs);
3938
+ let node;
3939
+ let matchReason = "unmatched-call";
3940
+ if (params.parentSpanId === null) {
3941
+ node = this.nodes.get(this.plan.rootId);
3942
+ } else {
3943
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3944
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3945
+ this.consumed.set(params.parentSpanId, consumed);
3946
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3947
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3948
+ ) : [];
3949
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3950
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3951
+ );
3952
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3953
+ if (node) {
3954
+ consumed.add(node.id);
3955
+ } else if (candidates.length > 0) {
3956
+ matchReason = "ambiguous-call";
3957
+ for (const candidate of candidates) {
3958
+ consumed.add(candidate.id);
3959
+ }
3960
+ }
3961
+ }
3962
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3963
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3964
+ if (requiredByManifest) {
3965
+ this.requiredScopes.add(params.spanId);
3966
+ }
3967
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3968
+ if (requiredForAssertion) {
3969
+ this.assertionScopes.add(params.spanId);
3970
+ }
3971
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3972
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3973
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3974
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3975
+ const reason = mustRun ? requiredByManifest ? "must-run" : requiredForAssertion ? "assertion" : node?.reason ?? "unresolved-must-run" : !node ? matchReason : !equalInputs ? "input-drift-or-missing-recording" : !params.canReuseOutput ? "unsupported-output-boundary" : !params.safetyMock && !params.reuseAllowed ? "reuse-not-declared" : node.reason;
3976
+ const decision = (action, why) => {
3977
+ this.decisions.push({
3978
+ ...identity,
3979
+ originalSpanId: node?.id ?? null,
3980
+ action,
3981
+ reason: why
3982
+ });
3983
+ };
3984
+ if (params.safetyMock && (mustRun || !reusable)) {
3985
+ decision("blocked", `safety-conflict:${reason}`);
3986
+ this.failure = new BitfabError(
3987
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
3988
+ );
3989
+ throw this.failure;
3990
+ }
3991
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
3992
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
3993
+ return {
3994
+ mock: true,
3995
+ output: JSON.parse(recording.output)
3996
+ };
3997
+ }
3998
+ decision("run", reason);
3999
+ this.observedNames.set(
4000
+ params.spanName,
4001
+ (this.observedNames.get(params.spanName) ?? 0) + 1
4002
+ );
4003
+ if (this.required.has(key)) {
4004
+ this.reached.add(key);
4005
+ }
4006
+ return { mock: false };
4007
+ }
4008
+ assertNoSafetyConflict() {
4009
+ if (this.failure) {
4010
+ throw this.failure;
4011
+ }
4012
+ }
4013
+ report() {
4014
+ return {
4015
+ decisions: this.decisions,
4016
+ mustRunNotReached: this.options.mustRun.filter(
4017
+ (node) => !this.reached.has(replayNodeKey(node))
4018
+ ),
4019
+ unresolvedMustRun: this.plan.unresolvedMustRun,
4020
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
4021
+ unresolvedAssertions: this.unresolvedAssertions,
4022
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
4023
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
4024
+ )
4025
+ };
4026
+ }
4027
+ };
4028
+ }
4029
+ });
4030
+
3610
4031
  // src/replay.ts
3611
4032
  var replay_exports = {};
3612
4033
  __export(replay_exports, {
@@ -3862,7 +4283,7 @@ function buildMockTree(rootNode) {
3862
4283
  }
3863
4284
  return { spans };
3864
4285
  }
3865
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4286
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3866
4287
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3867
4288
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3868
4289
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3875,6 +4296,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3875
4296
  let replayError = null;
3876
4297
  let replayDurationMs = null;
3877
4298
  let replayStarted = null;
4299
+ let selectiveReplay;
3878
4300
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3879
4301
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3880
4302
  try {
@@ -3907,6 +4329,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3907
4329
  originalTraceId
3908
4330
  );
3909
4331
  }
4332
+ if (selectiveOptions !== void 0) {
4333
+ if (serverItem.selectiveReplayPlan === void 0) {
4334
+ throw new BitfabError(
4335
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4336
+ );
4337
+ }
4338
+ selectiveReplay = new SelectiveReplayRuntime(
4339
+ serverItem.selectiveReplayPlan,
4340
+ selectiveOptions
4341
+ );
4342
+ }
3910
4343
  const span = await httpClient.getExternalSpan(originalSpanId, {
3911
4344
  view: "replay"
3912
4345
  });
@@ -3926,7 +4359,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3926
4359
  });
3927
4360
  }
3928
4361
  const hasOverrides = resolvedOverrides.length > 0;
3929
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4362
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3930
4363
  const includeOutputs = mockStrategy === "all";
3931
4364
  let mockTree;
3932
4365
  if (needTree) {
@@ -4001,6 +4434,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4001
4434
  sourceBitfabTraceId: originalTraceId,
4002
4435
  replayAttempt: attempt,
4003
4436
  mockTree,
4437
+ selectiveReplay,
4004
4438
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
4005
4439
  mockStrategy,
4006
4440
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -4012,6 +4446,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4012
4446
  () => fn(...inputs)
4013
4447
  );
4014
4448
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4449
+ selectiveReplay?.assertNoSafetyConflict();
4015
4450
  replayDurationMs = Math.round(performance.now() - replayStarted);
4016
4451
  } catch (e) {
4017
4452
  if (replayStarted !== null) {
@@ -4063,6 +4498,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4063
4498
  traceError,
4064
4499
  replayError,
4065
4500
  durationMs: replayDurationMs,
4501
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
4066
4502
  originalDurationMs,
4067
4503
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
4068
4504
  originalModel,
@@ -4167,6 +4603,14 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4167
4603
  "force only applies when resuming an experiment. Pass it together with resume."
4168
4604
  );
4169
4605
  }
4606
+ if (options?.experimentalSelectiveReplay !== void 0) {
4607
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4608
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4609
+ throw new BitfabError(
4610
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4611
+ );
4612
+ }
4613
+ }
4170
4614
  if (options?.traceIds !== void 0) {
4171
4615
  if (options.traceIds.length === 0) {
4172
4616
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4208,7 +4652,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4208
4652
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4209
4653
  let session;
4210
4654
  if (options?.resume === void 0) {
4211
- const started2 = await httpClient.startReplay(
4655
+ const started = await httpClient.startReplay(
4212
4656
  traceFunctionKey,
4213
4657
  // limit is meaningless with explicit traceIds (the ID list determines
4214
4658
  // the count), so it's omitted from the request entirely.
@@ -4228,9 +4672,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4228
4672
  requestedAttempts,
4229
4673
  options?.adaptInputs !== void 0,
4230
4674
  options?.onlyWithAssertions,
4231
- options?.judgeAssertions
4675
+ options?.judgeAssertions,
4676
+ options?.experimentalSelectiveReplay
4232
4677
  );
4233
- if (started2.items.length === 0) {
4678
+ if (started.items.length === 0) {
4234
4679
  try {
4235
4680
  console.warn(
4236
4681
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4239,10 +4684,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4239
4684
  }
4240
4685
  }
4241
4686
  session = {
4242
- experimentId: started2.experimentId,
4243
- experimentUrl: started2.experimentUrl,
4687
+ experimentId: started.experimentId,
4688
+ experimentUrl: started.experimentUrl,
4244
4689
  attempts: requestedAttempts,
4245
- work: expandReplayWork(started2.items, requestedAttempts),
4690
+ work: expandReplayWork(started.items, requestedAttempts),
4246
4691
  finished: [],
4247
4692
  includeDbBranchLease,
4248
4693
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4257,9 +4702,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4257
4702
  options.dbBranch
4258
4703
  );
4259
4704
  }
4260
- const { experimentId, experimentUrl, attempts } = session;
4705
+ const { experimentId } = session;
4706
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4707
+ const heartbeat = startReplayHeartbeat(
4708
+ () => httpClient.heartbeatReplay(experimentId)
4709
+ );
4710
+ const interruptReplay = async () => {
4711
+ await heartbeat.stop();
4712
+ return httpClient.interruptReplay(experimentId);
4713
+ };
4714
+ const interruptHandlers = installReplayInterruptHandlers({
4715
+ experimentId,
4716
+ experimentUrl: fullExperimentUrl,
4717
+ interrupt: interruptReplay,
4718
+ onInterrupt: options?.onInterrupt
4719
+ });
4720
+ let completeReturned = false;
4721
+ const markCompleteReturned = () => {
4722
+ completeReturned = true;
4723
+ void heartbeat.stop();
4724
+ };
4725
+ try {
4726
+ return await runReplaySession(
4727
+ httpClient,
4728
+ fn,
4729
+ session,
4730
+ fullExperimentUrl,
4731
+ maxConcurrency,
4732
+ interruptHandlers.remove,
4733
+ markCompleteReturned,
4734
+ interruptHandlers.interrupted,
4735
+ options,
4736
+ registeredOverrides
4737
+ );
4738
+ } catch (error) {
4739
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4740
+ await interruptReplay();
4741
+ }
4742
+ throw error;
4743
+ } finally {
4744
+ interruptHandlers.remove();
4745
+ await heartbeat.stop();
4746
+ }
4747
+ }
4748
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4749
+ if (!interrupted()) {
4750
+ return;
4751
+ }
4752
+ const cause = new BitfabError(
4753
+ `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}.`,
4754
+ experimentUrl
4755
+ );
4756
+ throw new ReplayError(
4757
+ cause.message,
4758
+ items,
4759
+ experimentId,
4760
+ experimentUrl,
4761
+ cause
4762
+ );
4763
+ }
4764
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4765
+ const { experimentId, attempts } = session;
4261
4766
  const mockStrategy = options?.mock ?? "marked";
4262
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4263
4767
  try {
4264
4768
  options?.onExperimentStart?.({
4265
4769
  experimentId,
@@ -4290,7 +4794,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4290
4794
  session.includeDbBranchLease,
4291
4795
  session.dbBranchSettings,
4292
4796
  options?.adaptInputs,
4293
- options?.dryRun === true
4797
+ options?.dryRun === true,
4798
+ options?.experimentalSelectiveReplay
4294
4799
  )
4295
4800
  );
4296
4801
  const total = tasks.length + carriedOverItems.length;
@@ -4363,6 +4868,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4363
4868
  traceError: item.traceError,
4364
4869
  replayError: item.replayError,
4365
4870
  durationMs: item.durationMs,
4871
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4366
4872
  originalDurationMs: item.originalDurationMs,
4367
4873
  originalTokens: item.originalTokens,
4368
4874
  originalModel: item.originalModel,
@@ -4407,7 +4913,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4407
4913
  );
4408
4914
  const resultItems = [...carriedOverItems, ...ranItems];
4409
4915
  if (options?.dryRun === true) {
4916
+ throwIfInterrupted(
4917
+ interrupted,
4918
+ resultItems,
4919
+ experimentId,
4920
+ fullExperimentUrl
4921
+ );
4922
+ stopInterruptingOnSignal();
4410
4923
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4924
+ markCompleteReturned();
4411
4925
  const dryResult = {
4412
4926
  items: resultItems,
4413
4927
  experimentId,
@@ -4432,12 +4946,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4432
4946
  ranItems[index].traceId = readBack;
4433
4947
  }
4434
4948
  }
4949
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4950
+ stopInterruptingOnSignal();
4435
4951
  const completeResult = await preserveReplayFailure(
4436
4952
  () => httpClient.completeReplay(experimentId),
4437
4953
  resultItems,
4438
4954
  experimentId,
4439
4955
  fullExperimentUrl
4440
4956
  );
4957
+ markCompleteReturned();
4441
4958
  const serverTraceIds = completeResult.traceIds;
4442
4959
  const replayTokens = completeResult.tokens;
4443
4960
  if (serverTraceIds !== void 0) {
@@ -4541,6 +5058,9 @@ var init_replay = __esm({
4541
5058
  init_mockOverride();
4542
5059
  init_randomUuid();
4543
5060
  init_replayContext();
5061
+ init_replayHeartbeat();
5062
+ init_replayInterrupt();
5063
+ init_selectiveReplayRuntime();
4544
5064
  init_serialize();
4545
5065
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4546
5066
  MAX_REPLAY_ATTEMPTS = 100;
@@ -4552,7 +5072,8 @@ var init_replay = __esm({
4552
5072
  "attempts",
4553
5073
  "graderIds",
4554
5074
  "onlyWithAssertions",
4555
- "dryRun"
5075
+ "dryRun",
5076
+ "experimentalSelectiveReplay"
4556
5077
  ];
4557
5078
  CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4558
5079
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
@@ -9246,6 +9767,9 @@ var Bitfab = class {
9246
9767
  ...options.mockOnReplay !== void 0 && {
9247
9768
  mockOnReplay: options.mockOnReplay
9248
9769
  },
9770
+ ...options.replayReusable !== void 0 && {
9771
+ replayReusable: options.replayReusable
9772
+ },
9249
9773
  ...options.finalize !== void 0 && { finalize: options.finalize }
9250
9774
  };
9251
9775
  }
@@ -9394,6 +9918,9 @@ var Bitfab = class {
9394
9918
  ...mockOnReplay !== void 0 && {
9395
9919
  mockOnReplay
9396
9920
  },
9921
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9922
+ replayReusable: nodeConfiguration.replayReusable
9923
+ },
9397
9924
  ...nodeConfiguration?.finalize !== void 0 && {
9398
9925
  finalize: nodeConfiguration.finalize
9399
9926
  }
@@ -10191,6 +10718,7 @@ var Bitfab = class {
10191
10718
  instrumentation: options.instrumentation ?? "span",
10192
10719
  functionId: options.functionId,
10193
10720
  captureContent: options.captureContent ?? true,
10721
+ replayReusable: options.replayReusable,
10194
10722
  autoTraceDefinition: options.autoTraceDefinition,
10195
10723
  declaredNode: options.declaredNode === true,
10196
10724
  nestedTrace: options.nestedTrace,
@@ -10310,6 +10838,27 @@ var Bitfab = class {
10310
10838
  return result;
10311
10839
  };
10312
10840
  const replayCtxForMock = getReplayContext();
10841
+ if (replayCtxForMock?.selectiveReplay) {
10842
+ const decision = replayCtxForMock.selectiveReplay.enter({
10843
+ traceFunctionKey,
10844
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10845
+ spanId,
10846
+ parentSpanId,
10847
+ inputs: args,
10848
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10849
+ reuseAllowed: options.replayReusable === true,
10850
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10851
+ });
10852
+ if (decision.mock) {
10853
+ void sendSpan({
10854
+ result: decision.output,
10855
+ mocked: true,
10856
+ mockTarget: "output",
10857
+ mockSource: "recorded"
10858
+ });
10859
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10860
+ }
10861
+ }
10313
10862
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10314
10863
  const counters = replayCtxForMock.callCounters;
10315
10864
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10733,6 +11282,9 @@ var Bitfab = class {
10733
11282
  span_origin: makeSpanOrigin(params.instrumentation),
10734
11283
  span_data: {
10735
11284
  name: params.spanName,
11285
+ ...params.replayReusable !== void 0 && {
11286
+ replay_reusable: params.replayReusable
11287
+ },
10736
11288
  type: params.spanType,
10737
11289
  ...params.functionId !== void 0 && {
10738
11290
  function_id: params.functionId,
@@ -11284,6 +11836,7 @@ init_replay();
11284
11836
  // src/replayRegistry.ts
11285
11837
  init_errors();
11286
11838
  init_replay();
11839
+ init_replayInterrupt();
11287
11840
  function defineReplayRegistry(registry) {
11288
11841
  return registry;
11289
11842
  }