@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/node.cjs CHANGED
@@ -88,7 +88,7 @@ var __version__, __packageName__;
88
88
  var init_version_generated = __esm({
89
89
  "src/version.generated.ts"() {
90
90
  "use strict";
91
- __version__ = "0.61.2";
91
+ __version__ = "0.61.4";
92
92
  __packageName__ = "@bitfab/sdk";
93
93
  }
94
94
  });
@@ -1075,6 +1075,7 @@ var init_simulationPlan = __esm({
1075
1075
  "input_meta",
1076
1076
  "output",
1077
1077
  "output_meta",
1078
+ "replay_recording",
1078
1079
  "prompt"
1079
1080
  ];
1080
1081
  SimulationPlan = class {
@@ -2283,7 +2284,7 @@ function warnDroppedExternalTrace(error) {
2283
2284
  function experimentIdWireFields(experimentId) {
2284
2285
  return { experimentId, testRunId: experimentId };
2285
2286
  }
2286
- 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;
2287
+ 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;
2287
2288
  var init_http = __esm({
2288
2289
  "src/http.ts"() {
2289
2290
  "use strict";
@@ -2302,6 +2303,8 @@ var init_http = __esm({
2302
2303
  init_warnOnce();
2303
2304
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2304
2305
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2306
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2307
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2305
2308
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2306
2309
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2307
2310
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -2889,8 +2892,11 @@ var init_http = __esm({
2889
2892
  * Start a replay session by fetching historical traces.
2890
2893
  * Blocking call - creates an experiment and returns lightweight item references.
2891
2894
  */
2892
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2895
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2893
2896
  const payload = { traceFunctionKey };
2897
+ if (experimentalSelectiveReplay !== void 0) {
2898
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2899
+ }
2894
2900
  if (limit !== void 0) {
2895
2901
  payload.limit = limit;
2896
2902
  }
@@ -3111,6 +3117,28 @@ var init_http = __esm({
3111
3117
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3112
3118
  );
3113
3119
  }
3120
+ async interruptReplay(experimentId) {
3121
+ try {
3122
+ await this.request(
3123
+ "/api/sdk/replay/interrupt",
3124
+ experimentIdWireFields(experimentId),
3125
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3126
+ );
3127
+ return true;
3128
+ } catch {
3129
+ return false;
3130
+ }
3131
+ }
3132
+ async heartbeatReplay(experimentId) {
3133
+ try {
3134
+ await this.request(
3135
+ "/api/sdk/replay/heartbeat",
3136
+ experimentIdWireFields(experimentId),
3137
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3138
+ );
3139
+ } catch {
3140
+ }
3141
+ }
3114
3142
  async getReseedSource(traceId) {
3115
3143
  return this.get(
3116
3144
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3614,6 +3642,399 @@ var init_integrationFixture = __esm({
3614
3642
  }
3615
3643
  });
3616
3644
 
3645
+ // src/replayHeartbeat.ts
3646
+ function startReplayHeartbeat(send) {
3647
+ let stopped = false;
3648
+ let inFlight;
3649
+ const timer = setInterval(() => {
3650
+ if (stopped || inFlight !== void 0) {
3651
+ return;
3652
+ }
3653
+ inFlight = send().catch(() => void 0).finally(() => {
3654
+ inFlight = void 0;
3655
+ });
3656
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3657
+ unrefTimer(timer);
3658
+ return {
3659
+ stop: async () => {
3660
+ stopped = true;
3661
+ clearInterval(timer);
3662
+ await inFlight;
3663
+ }
3664
+ };
3665
+ }
3666
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3667
+ var init_replayHeartbeat = __esm({
3668
+ "src/replayHeartbeat.ts"() {
3669
+ "use strict";
3670
+ init_unrefTimer();
3671
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3672
+ }
3673
+ });
3674
+
3675
+ // src/replayInterrupt.ts
3676
+ function signalCapableProcess() {
3677
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3678
+ return void 0;
3679
+ }
3680
+ return process;
3681
+ }
3682
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3683
+ if (markedInterrupted) {
3684
+ return `[replay] Experiment ${experimentId} interrupted.`;
3685
+ }
3686
+ 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.`;
3687
+ }
3688
+ function reportInterrupted(experimentId, markedInterrupted) {
3689
+ try {
3690
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3691
+ } catch {
3692
+ }
3693
+ }
3694
+ function installReplayInterruptHandlers(target) {
3695
+ const host = signalCapableProcess();
3696
+ if (host === void 0) {
3697
+ return { remove: () => {
3698
+ }, interrupted: () => false };
3699
+ }
3700
+ const handlers = /* @__PURE__ */ new Map();
3701
+ const removeHandlers = () => {
3702
+ for (const [signal, handler] of handlers) {
3703
+ host.removeListener(signal, handler);
3704
+ }
3705
+ handlers.clear();
3706
+ };
3707
+ let interrupting = false;
3708
+ const handleSignal = async (signal) => {
3709
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3710
+ const markedInterrupted = await target.interrupt();
3711
+ removeHandlers();
3712
+ if (target.onInterrupt !== void 0) {
3713
+ try {
3714
+ target.onInterrupt({
3715
+ experimentId: target.experimentId,
3716
+ experimentUrl: target.experimentUrl,
3717
+ signal,
3718
+ markedInterrupted
3719
+ });
3720
+ } catch {
3721
+ }
3722
+ return;
3723
+ }
3724
+ reportInterrupted(target.experimentId, markedInterrupted);
3725
+ if (!hostHandlesSignal) {
3726
+ host.kill(host.pid, signal);
3727
+ }
3728
+ };
3729
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3730
+ const handler = () => {
3731
+ if (interrupting) {
3732
+ return;
3733
+ }
3734
+ interrupting = true;
3735
+ void handleSignal(signal);
3736
+ };
3737
+ handlers.set(signal, handler);
3738
+ host.on(signal, handler);
3739
+ }
3740
+ return { remove: removeHandlers, interrupted: () => interrupting };
3741
+ }
3742
+ var REPLAY_INTERRUPT_SIGNALS;
3743
+ var init_replayInterrupt = __esm({
3744
+ "src/replayInterrupt.ts"() {
3745
+ "use strict";
3746
+ REPLAY_INTERRUPT_SIGNALS = [
3747
+ "SIGINT",
3748
+ "SIGTERM"
3749
+ ];
3750
+ }
3751
+ });
3752
+
3753
+ // src/selectiveReplay.ts
3754
+ function replayNodeKey(node) {
3755
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3756
+ }
3757
+ function jsonFingerprint(value) {
3758
+ const seen = /* @__PURE__ */ new Set();
3759
+ function encode(input) {
3760
+ if (input === null || typeof input === "boolean") {
3761
+ return JSON.stringify(input);
3762
+ }
3763
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3764
+ return JSON.stringify(input);
3765
+ }
3766
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3767
+ return JSON.stringify(input);
3768
+ }
3769
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3770
+ throw new Error("Not a lossless JSON value");
3771
+ }
3772
+ seen.add(input);
3773
+ const prototype = Object.getPrototypeOf(input);
3774
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3775
+ throw new Error("Not a plain JSON object");
3776
+ }
3777
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3778
+ throw new Error("Symbol properties are not recorded");
3779
+ }
3780
+ const keys = Object.keys(input);
3781
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3782
+ throw new Error("Non-enumerable properties are not recorded");
3783
+ }
3784
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3785
+ throw new Error("Not a dense JSON array");
3786
+ }
3787
+ const properties = keys.map((key) => {
3788
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3789
+ if (!descriptor || !("value" in descriptor)) {
3790
+ throw new Error("Accessor properties are not replay inputs");
3791
+ }
3792
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3793
+ });
3794
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3795
+ }
3796
+ try {
3797
+ return encode(value);
3798
+ } catch {
3799
+ return void 0;
3800
+ }
3801
+ }
3802
+ var init_selectiveReplay = __esm({
3803
+ "src/selectiveReplay.ts"() {
3804
+ "use strict";
3805
+ }
3806
+ });
3807
+
3808
+ // src/selectiveReplayRuntime.ts
3809
+ function validateSelectiveReplayOptions(options) {
3810
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3811
+ (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
3812
+ );
3813
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3814
+ throw new BitfabError(
3815
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3816
+ );
3817
+ }
3818
+ }
3819
+ function policyKey(options) {
3820
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3821
+ }
3822
+ var SelectiveReplayRuntime;
3823
+ var init_selectiveReplayRuntime = __esm({
3824
+ "src/selectiveReplayRuntime.ts"() {
3825
+ "use strict";
3826
+ init_errors();
3827
+ init_selectiveReplay();
3828
+ SelectiveReplayRuntime = class {
3829
+ constructor(plan, options) {
3830
+ this.plan = plan;
3831
+ this.options = options;
3832
+ this.nodes = /* @__PURE__ */ new Map();
3833
+ this.children = /* @__PURE__ */ new Map();
3834
+ this.runtimeParents = /* @__PURE__ */ new Map();
3835
+ this.consumed = /* @__PURE__ */ new Map();
3836
+ this.requiredScopes = /* @__PURE__ */ new Set();
3837
+ this.recordings = /* @__PURE__ */ new Map();
3838
+ this.reached = /* @__PURE__ */ new Set();
3839
+ this.decisions = [];
3840
+ this.assertionNames = /* @__PURE__ */ new Set();
3841
+ this.assertionScopes = /* @__PURE__ */ new Set();
3842
+ this.observedNames = /* @__PURE__ */ new Map();
3843
+ this.unresolvedAssertions = [];
3844
+ validateSelectiveReplayOptions(options);
3845
+ 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) {
3846
+ throw new BitfabError(
3847
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3848
+ );
3849
+ }
3850
+ validateSelectiveReplayOptions(plan.options);
3851
+ if (policyKey(plan.options) !== policyKey(options)) {
3852
+ throw new BitfabError(
3853
+ "Selective replay plan does not match the requested mustRun nodes."
3854
+ );
3855
+ }
3856
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3857
+ for (const node of plan.nodes) {
3858
+ 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") {
3859
+ throw new BitfabError("Invalid selective replay node.");
3860
+ }
3861
+ this.nodes.set(node.id, node);
3862
+ if (node.recording !== void 0) {
3863
+ this.recordings.set(node.id, {
3864
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3865
+ output: jsonFingerprint(node.recording.output)
3866
+ });
3867
+ }
3868
+ if (node.parentId !== null) {
3869
+ const siblings = this.children.get(node.parentId) ?? [];
3870
+ siblings.push(node);
3871
+ this.children.set(node.parentId, siblings);
3872
+ }
3873
+ }
3874
+ const root = this.nodes.get(plan.rootId);
3875
+ if (!root || root.parentId !== null || !root.mustRun) {
3876
+ throw new BitfabError("Selective replay requires a live recorded root.");
3877
+ }
3878
+ const assertionIds = /* @__PURE__ */ new Set();
3879
+ let wholeTrace = false;
3880
+ for (const assertion of plan.assertions) {
3881
+ const target = assertion?.target;
3882
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3883
+ throw new BitfabError(
3884
+ "Invalid selective replay assertion requirements."
3885
+ );
3886
+ }
3887
+ assertionIds.add(assertion.id);
3888
+ if (target === null) {
3889
+ wholeTrace = true;
3890
+ } else if (target.kind === "span") {
3891
+ 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)) {
3892
+ throw new BitfabError("Invalid selective replay assertion target.");
3893
+ }
3894
+ this.assertionNames.add(target.name);
3895
+ const count = plan.nodes.filter(
3896
+ (node) => node.spanName === target.name
3897
+ ).length;
3898
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3899
+ this.unresolvedAssertions.push(assertion.id);
3900
+ }
3901
+ }
3902
+ }
3903
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3904
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3905
+ const unresolved = options.mustRun.filter(
3906
+ (node) => !recordedKeys.has(replayNodeKey(node))
3907
+ );
3908
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3909
+ throw new BitfabError(
3910
+ "Selective replay plan has incomplete mustRun coverage."
3911
+ );
3912
+ }
3913
+ for (const node of plan.nodes) {
3914
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3915
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3916
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3917
+ let parentId = node.parentId;
3918
+ while (parentId !== null) {
3919
+ const parent = this.nodes.get(parentId);
3920
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3921
+ throw new BitfabError("Invalid selective replay ancestry.");
3922
+ }
3923
+ ancestors.add(parentId);
3924
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3925
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3926
+ parentId = parent.parentId;
3927
+ }
3928
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3929
+ throw new BitfabError("Selective replay plan could hide required code.");
3930
+ }
3931
+ if (requiredForAssertion && !node.mustRun) {
3932
+ throw new BitfabError(
3933
+ "Selective replay plan could hide assertion evidence."
3934
+ );
3935
+ }
3936
+ }
3937
+ }
3938
+ enter(params) {
3939
+ const identity = {
3940
+ traceFunctionKey: params.traceFunctionKey,
3941
+ spanName: params.spanName
3942
+ };
3943
+ const key = replayNodeKey(identity);
3944
+ const fingerprint = jsonFingerprint(params.inputs);
3945
+ let node;
3946
+ let matchReason = "unmatched-call";
3947
+ if (params.parentSpanId === null) {
3948
+ node = this.nodes.get(this.plan.rootId);
3949
+ } else {
3950
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3951
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3952
+ this.consumed.set(params.parentSpanId, consumed);
3953
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3954
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3955
+ ) : [];
3956
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3957
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3958
+ );
3959
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3960
+ if (node) {
3961
+ consumed.add(node.id);
3962
+ } else if (candidates.length > 0) {
3963
+ matchReason = "ambiguous-call";
3964
+ for (const candidate of candidates) {
3965
+ consumed.add(candidate.id);
3966
+ }
3967
+ }
3968
+ }
3969
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3970
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3971
+ if (requiredByManifest) {
3972
+ this.requiredScopes.add(params.spanId);
3973
+ }
3974
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3975
+ if (requiredForAssertion) {
3976
+ this.assertionScopes.add(params.spanId);
3977
+ }
3978
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3979
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3980
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3981
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3982
+ 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;
3983
+ const decision = (action, why) => {
3984
+ this.decisions.push({
3985
+ ...identity,
3986
+ originalSpanId: node?.id ?? null,
3987
+ action,
3988
+ reason: why
3989
+ });
3990
+ };
3991
+ if (params.safetyMock && (mustRun || !reusable)) {
3992
+ decision("blocked", `safety-conflict:${reason}`);
3993
+ this.failure = new BitfabError(
3994
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
3995
+ );
3996
+ throw this.failure;
3997
+ }
3998
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
3999
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
4000
+ return {
4001
+ mock: true,
4002
+ output: JSON.parse(recording.output)
4003
+ };
4004
+ }
4005
+ decision("run", reason);
4006
+ this.observedNames.set(
4007
+ params.spanName,
4008
+ (this.observedNames.get(params.spanName) ?? 0) + 1
4009
+ );
4010
+ if (this.required.has(key)) {
4011
+ this.reached.add(key);
4012
+ }
4013
+ return { mock: false };
4014
+ }
4015
+ assertNoSafetyConflict() {
4016
+ if (this.failure) {
4017
+ throw this.failure;
4018
+ }
4019
+ }
4020
+ report() {
4021
+ return {
4022
+ decisions: this.decisions,
4023
+ mustRunNotReached: this.options.mustRun.filter(
4024
+ (node) => !this.reached.has(replayNodeKey(node))
4025
+ ),
4026
+ unresolvedMustRun: this.plan.unresolvedMustRun,
4027
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
4028
+ unresolvedAssertions: this.unresolvedAssertions,
4029
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
4030
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
4031
+ )
4032
+ };
4033
+ }
4034
+ };
4035
+ }
4036
+ });
4037
+
3617
4038
  // src/replay.ts
3618
4039
  var replay_exports = {};
3619
4040
  __export(replay_exports, {
@@ -3869,7 +4290,7 @@ function buildMockTree(rootNode) {
3869
4290
  }
3870
4291
  return { spans };
3871
4292
  }
3872
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4293
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3873
4294
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3874
4295
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3875
4296
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3882,6 +4303,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3882
4303
  let replayError = null;
3883
4304
  let replayDurationMs = null;
3884
4305
  let replayStarted = null;
4306
+ let selectiveReplay;
3885
4307
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3886
4308
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3887
4309
  try {
@@ -3914,6 +4336,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3914
4336
  originalTraceId
3915
4337
  );
3916
4338
  }
4339
+ if (selectiveOptions !== void 0) {
4340
+ if (serverItem.selectiveReplayPlan === void 0) {
4341
+ throw new BitfabError(
4342
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4343
+ );
4344
+ }
4345
+ selectiveReplay = new SelectiveReplayRuntime(
4346
+ serverItem.selectiveReplayPlan,
4347
+ selectiveOptions
4348
+ );
4349
+ }
3917
4350
  const span = await httpClient.getExternalSpan(originalSpanId, {
3918
4351
  view: "replay"
3919
4352
  });
@@ -3933,7 +4366,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3933
4366
  });
3934
4367
  }
3935
4368
  const hasOverrides = resolvedOverrides.length > 0;
3936
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4369
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3937
4370
  const includeOutputs = mockStrategy === "all";
3938
4371
  let mockTree;
3939
4372
  if (needTree) {
@@ -4008,6 +4441,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4008
4441
  sourceBitfabTraceId: originalTraceId,
4009
4442
  replayAttempt: attempt,
4010
4443
  mockTree,
4444
+ selectiveReplay,
4011
4445
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
4012
4446
  mockStrategy,
4013
4447
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -4019,6 +4453,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4019
4453
  () => fn(...inputs)
4020
4454
  );
4021
4455
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4456
+ selectiveReplay?.assertNoSafetyConflict();
4022
4457
  replayDurationMs = Math.round(performance.now() - replayStarted);
4023
4458
  } catch (e) {
4024
4459
  if (replayStarted !== null) {
@@ -4070,6 +4505,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4070
4505
  traceError,
4071
4506
  replayError,
4072
4507
  durationMs: replayDurationMs,
4508
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
4073
4509
  originalDurationMs,
4074
4510
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
4075
4511
  originalModel,
@@ -4174,6 +4610,14 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4174
4610
  "force only applies when resuming an experiment. Pass it together with resume."
4175
4611
  );
4176
4612
  }
4613
+ if (options?.experimentalSelectiveReplay !== void 0) {
4614
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4615
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4616
+ throw new BitfabError(
4617
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4618
+ );
4619
+ }
4620
+ }
4177
4621
  if (options?.traceIds !== void 0) {
4178
4622
  if (options.traceIds.length === 0) {
4179
4623
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4215,7 +4659,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4215
4659
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4216
4660
  let session;
4217
4661
  if (options?.resume === void 0) {
4218
- const started2 = await httpClient.startReplay(
4662
+ const started = await httpClient.startReplay(
4219
4663
  traceFunctionKey,
4220
4664
  // limit is meaningless with explicit traceIds (the ID list determines
4221
4665
  // the count), so it's omitted from the request entirely.
@@ -4235,9 +4679,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4235
4679
  requestedAttempts,
4236
4680
  options?.adaptInputs !== void 0,
4237
4681
  options?.onlyWithAssertions,
4238
- options?.judgeAssertions
4682
+ options?.judgeAssertions,
4683
+ options?.experimentalSelectiveReplay
4239
4684
  );
4240
- if (started2.items.length === 0) {
4685
+ if (started.items.length === 0) {
4241
4686
  try {
4242
4687
  console.warn(
4243
4688
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4246,10 +4691,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4246
4691
  }
4247
4692
  }
4248
4693
  session = {
4249
- experimentId: started2.experimentId,
4250
- experimentUrl: started2.experimentUrl,
4694
+ experimentId: started.experimentId,
4695
+ experimentUrl: started.experimentUrl,
4251
4696
  attempts: requestedAttempts,
4252
- work: expandReplayWork(started2.items, requestedAttempts),
4697
+ work: expandReplayWork(started.items, requestedAttempts),
4253
4698
  finished: [],
4254
4699
  includeDbBranchLease,
4255
4700
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4264,9 +4709,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4264
4709
  options.dbBranch
4265
4710
  );
4266
4711
  }
4267
- const { experimentId, experimentUrl, attempts } = session;
4712
+ const { experimentId } = session;
4713
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4714
+ const heartbeat = startReplayHeartbeat(
4715
+ () => httpClient.heartbeatReplay(experimentId)
4716
+ );
4717
+ const interruptReplay = async () => {
4718
+ await heartbeat.stop();
4719
+ return httpClient.interruptReplay(experimentId);
4720
+ };
4721
+ const interruptHandlers = installReplayInterruptHandlers({
4722
+ experimentId,
4723
+ experimentUrl: fullExperimentUrl,
4724
+ interrupt: interruptReplay,
4725
+ onInterrupt: options?.onInterrupt
4726
+ });
4727
+ let completeReturned = false;
4728
+ const markCompleteReturned = () => {
4729
+ completeReturned = true;
4730
+ void heartbeat.stop();
4731
+ };
4732
+ try {
4733
+ return await runReplaySession(
4734
+ httpClient,
4735
+ fn,
4736
+ session,
4737
+ fullExperimentUrl,
4738
+ maxConcurrency,
4739
+ interruptHandlers.remove,
4740
+ markCompleteReturned,
4741
+ interruptHandlers.interrupted,
4742
+ options,
4743
+ registeredOverrides
4744
+ );
4745
+ } catch (error) {
4746
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4747
+ await interruptReplay();
4748
+ }
4749
+ throw error;
4750
+ } finally {
4751
+ interruptHandlers.remove();
4752
+ await heartbeat.stop();
4753
+ }
4754
+ }
4755
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4756
+ if (!interrupted()) {
4757
+ return;
4758
+ }
4759
+ const cause = new BitfabError(
4760
+ `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}.`,
4761
+ experimentUrl
4762
+ );
4763
+ throw new ReplayError(
4764
+ cause.message,
4765
+ items,
4766
+ experimentId,
4767
+ experimentUrl,
4768
+ cause
4769
+ );
4770
+ }
4771
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4772
+ const { experimentId, attempts } = session;
4268
4773
  const mockStrategy = options?.mock ?? "marked";
4269
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4270
4774
  try {
4271
4775
  options?.onExperimentStart?.({
4272
4776
  experimentId,
@@ -4297,7 +4801,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4297
4801
  session.includeDbBranchLease,
4298
4802
  session.dbBranchSettings,
4299
4803
  options?.adaptInputs,
4300
- options?.dryRun === true
4804
+ options?.dryRun === true,
4805
+ options?.experimentalSelectiveReplay
4301
4806
  )
4302
4807
  );
4303
4808
  const total = tasks.length + carriedOverItems.length;
@@ -4370,6 +4875,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4370
4875
  traceError: item.traceError,
4371
4876
  replayError: item.replayError,
4372
4877
  durationMs: item.durationMs,
4878
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4373
4879
  originalDurationMs: item.originalDurationMs,
4374
4880
  originalTokens: item.originalTokens,
4375
4881
  originalModel: item.originalModel,
@@ -4414,7 +4920,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4414
4920
  );
4415
4921
  const resultItems = [...carriedOverItems, ...ranItems];
4416
4922
  if (options?.dryRun === true) {
4923
+ throwIfInterrupted(
4924
+ interrupted,
4925
+ resultItems,
4926
+ experimentId,
4927
+ fullExperimentUrl
4928
+ );
4929
+ stopInterruptingOnSignal();
4417
4930
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4931
+ markCompleteReturned();
4418
4932
  const dryResult = {
4419
4933
  items: resultItems,
4420
4934
  experimentId,
@@ -4439,12 +4953,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4439
4953
  ranItems[index].traceId = readBack;
4440
4954
  }
4441
4955
  }
4956
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4957
+ stopInterruptingOnSignal();
4442
4958
  const completeResult = await preserveReplayFailure(
4443
4959
  () => httpClient.completeReplay(experimentId),
4444
4960
  resultItems,
4445
4961
  experimentId,
4446
4962
  fullExperimentUrl
4447
4963
  );
4964
+ markCompleteReturned();
4448
4965
  const serverTraceIds = completeResult.traceIds;
4449
4966
  const replayTokens = completeResult.tokens;
4450
4967
  if (serverTraceIds !== void 0) {
@@ -4548,6 +5065,9 @@ var init_replay = __esm({
4548
5065
  init_mockOverride();
4549
5066
  init_randomUuid();
4550
5067
  init_replayContext();
5068
+ init_replayHeartbeat();
5069
+ init_replayInterrupt();
5070
+ init_selectiveReplayRuntime();
4551
5071
  init_serialize();
4552
5072
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4553
5073
  MAX_REPLAY_ATTEMPTS = 100;
@@ -4559,7 +5079,8 @@ var init_replay = __esm({
4559
5079
  "attempts",
4560
5080
  "graderIds",
4561
5081
  "onlyWithAssertions",
4562
- "dryRun"
5082
+ "dryRun",
5083
+ "experimentalSelectiveReplay"
4563
5084
  ];
4564
5085
  CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4565
5086
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
@@ -9260,6 +9781,9 @@ var Bitfab = class {
9260
9781
  ...options.mockOnReplay !== void 0 && {
9261
9782
  mockOnReplay: options.mockOnReplay
9262
9783
  },
9784
+ ...options.replayReusable !== void 0 && {
9785
+ replayReusable: options.replayReusable
9786
+ },
9263
9787
  ...options.finalize !== void 0 && { finalize: options.finalize }
9264
9788
  };
9265
9789
  }
@@ -9408,6 +9932,9 @@ var Bitfab = class {
9408
9932
  ...mockOnReplay !== void 0 && {
9409
9933
  mockOnReplay
9410
9934
  },
9935
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9936
+ replayReusable: nodeConfiguration.replayReusable
9937
+ },
9411
9938
  ...nodeConfiguration?.finalize !== void 0 && {
9412
9939
  finalize: nodeConfiguration.finalize
9413
9940
  }
@@ -10205,6 +10732,7 @@ var Bitfab = class {
10205
10732
  instrumentation: options.instrumentation ?? "span",
10206
10733
  functionId: options.functionId,
10207
10734
  captureContent: options.captureContent ?? true,
10735
+ replayReusable: options.replayReusable,
10208
10736
  autoTraceDefinition: options.autoTraceDefinition,
10209
10737
  declaredNode: options.declaredNode === true,
10210
10738
  nestedTrace: options.nestedTrace,
@@ -10324,6 +10852,27 @@ var Bitfab = class {
10324
10852
  return result;
10325
10853
  };
10326
10854
  const replayCtxForMock = getReplayContext();
10855
+ if (replayCtxForMock?.selectiveReplay) {
10856
+ const decision = replayCtxForMock.selectiveReplay.enter({
10857
+ traceFunctionKey,
10858
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10859
+ spanId,
10860
+ parentSpanId,
10861
+ inputs: args,
10862
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10863
+ reuseAllowed: options.replayReusable === true,
10864
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10865
+ });
10866
+ if (decision.mock) {
10867
+ void sendSpan({
10868
+ result: decision.output,
10869
+ mocked: true,
10870
+ mockTarget: "output",
10871
+ mockSource: "recorded"
10872
+ });
10873
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10874
+ }
10875
+ }
10327
10876
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10328
10877
  const counters = replayCtxForMock.callCounters;
10329
10878
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10747,6 +11296,9 @@ var Bitfab = class {
10747
11296
  span_origin: makeSpanOrigin(params.instrumentation),
10748
11297
  span_data: {
10749
11298
  name: params.spanName,
11299
+ ...params.replayReusable !== void 0 && {
11300
+ replay_reusable: params.replayReusable
11301
+ },
10750
11302
  type: params.spanType,
10751
11303
  ...params.functionId !== void 0 && {
10752
11304
  function_id: params.functionId,
@@ -11298,6 +11850,7 @@ init_replay();
11298
11850
  // src/replayRegistry.ts
11299
11851
  init_errors();
11300
11852
  init_replay();
11853
+ init_replayInterrupt();
11301
11854
  function defineReplayRegistry(registry) {
11302
11855
  return registry;
11303
11856
  }