bitfab 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-INA6W7OB.js → chunk-54EGXUZ3.js} +32 -3
  6. package/dist/chunk-54EGXUZ3.js.map +1 -0
  7. package/dist/{chunk-BMJNDIVA.js → chunk-K4XCWQBC.js} +1 -1
  8. package/dist/chunk-K4XCWQBC.js.map +1 -0
  9. package/dist/{chunk-A4UVO6BQ.js → chunk-PSAEQ4OW.js} +480 -15
  10. package/dist/chunk-PSAEQ4OW.js.map +1 -0
  11. package/dist/{chunk-ONZFMRRF.js → chunk-VWRCLZCA.js} +30 -3
  12. package/dist/chunk-VWRCLZCA.js.map +1 -0
  13. package/dist/{chunk-MTK6PZHA.js → chunk-WPYBJGTG.js} +21 -3
  14. package/dist/chunk-WPYBJGTG.js.map +1 -0
  15. package/dist/{chunk-LUOCT4HG.js → chunk-XGRQMQ2R.js} +38 -7
  16. package/dist/chunk-XGRQMQ2R.js.map +1 -0
  17. package/dist/{http-2R6MSRVA.js → http-C2TCBBQV.js} +2 -2
  18. package/dist/{http-KM6QWKAP.js → http-ONLJPP6W.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-6UUBJBLK.js → replay-RM24QKEA.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-A4UVO6BQ.js.map +0 -1
  34. package/dist/chunk-BMJNDIVA.js.map +0 -1
  35. package/dist/chunk-INA6W7OB.js.map +0 -1
  36. package/dist/chunk-LUOCT4HG.js.map +0 -1
  37. package/dist/chunk-MTK6PZHA.js.map +0 -1
  38. package/dist/chunk-ONZFMRRF.js.map +0 -1
  39. /package/dist/{http-2R6MSRVA.js.map → http-C2TCBBQV.js.map} +0 -0
  40. /package/dist/{http-KM6QWKAP.js.map → http-ONLJPP6W.js.map} +0 -0
  41. /package/dist/{replay-6UUBJBLK.js.map → replay-RM24QKEA.js.map} +0 -0
package/dist/node.cjs CHANGED
@@ -97,7 +97,7 @@ var __version__, __packageName__;
97
97
  var init_version_generated = __esm({
98
98
  "src/version.generated.ts"() {
99
99
  "use strict";
100
- __version__ = "0.61.2";
100
+ __version__ = "0.61.4";
101
101
  __packageName__ = "bitfab";
102
102
  }
103
103
  });
@@ -1084,6 +1084,7 @@ var init_simulationPlan = __esm({
1084
1084
  "input_meta",
1085
1085
  "output",
1086
1086
  "output_meta",
1087
+ "replay_recording",
1087
1088
  "prompt"
1088
1089
  ];
1089
1090
  SimulationPlan = class {
@@ -2292,7 +2293,7 @@ function warnDroppedExternalTrace(error) {
2292
2293
  function experimentIdWireFields(experimentId) {
2293
2294
  return { experimentId, testRunId: experimentId };
2294
2295
  }
2295
- 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;
2296
+ 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;
2296
2297
  var init_http = __esm({
2297
2298
  "src/http.ts"() {
2298
2299
  "use strict";
@@ -2311,6 +2312,8 @@ var init_http = __esm({
2311
2312
  init_warnOnce();
2312
2313
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2313
2314
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2315
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2316
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2314
2317
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2315
2318
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2316
2319
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -2898,8 +2901,11 @@ var init_http = __esm({
2898
2901
  * Start a replay session by fetching historical traces.
2899
2902
  * Blocking call - creates an experiment and returns lightweight item references.
2900
2903
  */
2901
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2904
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2902
2905
  const payload = { traceFunctionKey };
2906
+ if (experimentalSelectiveReplay !== void 0) {
2907
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2908
+ }
2903
2909
  if (limit !== void 0) {
2904
2910
  payload.limit = limit;
2905
2911
  }
@@ -3120,6 +3126,28 @@ var init_http = __esm({
3120
3126
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3121
3127
  );
3122
3128
  }
3129
+ async interruptReplay(experimentId) {
3130
+ try {
3131
+ await this.request(
3132
+ "/api/sdk/replay/interrupt",
3133
+ experimentIdWireFields(experimentId),
3134
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3135
+ );
3136
+ return true;
3137
+ } catch {
3138
+ return false;
3139
+ }
3140
+ }
3141
+ async heartbeatReplay(experimentId) {
3142
+ try {
3143
+ await this.request(
3144
+ "/api/sdk/replay/heartbeat",
3145
+ experimentIdWireFields(experimentId),
3146
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3147
+ );
3148
+ } catch {
3149
+ }
3150
+ }
3123
3151
  async getReseedSource(traceId) {
3124
3152
  return this.get(
3125
3153
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3623,6 +3651,399 @@ var init_integrationFixture = __esm({
3623
3651
  }
3624
3652
  });
3625
3653
 
3654
+ // src/replayHeartbeat.ts
3655
+ function startReplayHeartbeat(send) {
3656
+ let stopped = false;
3657
+ let inFlight;
3658
+ const timer = setInterval(() => {
3659
+ if (stopped || inFlight !== void 0) {
3660
+ return;
3661
+ }
3662
+ inFlight = send().catch(() => void 0).finally(() => {
3663
+ inFlight = void 0;
3664
+ });
3665
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3666
+ unrefTimer(timer);
3667
+ return {
3668
+ stop: async () => {
3669
+ stopped = true;
3670
+ clearInterval(timer);
3671
+ await inFlight;
3672
+ }
3673
+ };
3674
+ }
3675
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3676
+ var init_replayHeartbeat = __esm({
3677
+ "src/replayHeartbeat.ts"() {
3678
+ "use strict";
3679
+ init_unrefTimer();
3680
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3681
+ }
3682
+ });
3683
+
3684
+ // src/replayInterrupt.ts
3685
+ function signalCapableProcess() {
3686
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3687
+ return void 0;
3688
+ }
3689
+ return process;
3690
+ }
3691
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3692
+ if (markedInterrupted) {
3693
+ return `[replay] Experiment ${experimentId} interrupted.`;
3694
+ }
3695
+ 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.`;
3696
+ }
3697
+ function reportInterrupted(experimentId, markedInterrupted) {
3698
+ try {
3699
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3700
+ } catch {
3701
+ }
3702
+ }
3703
+ function installReplayInterruptHandlers(target) {
3704
+ const host = signalCapableProcess();
3705
+ if (host === void 0) {
3706
+ return { remove: () => {
3707
+ }, interrupted: () => false };
3708
+ }
3709
+ const handlers = /* @__PURE__ */ new Map();
3710
+ const removeHandlers = () => {
3711
+ for (const [signal, handler] of handlers) {
3712
+ host.removeListener(signal, handler);
3713
+ }
3714
+ handlers.clear();
3715
+ };
3716
+ let interrupting = false;
3717
+ const handleSignal = async (signal) => {
3718
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3719
+ const markedInterrupted = await target.interrupt();
3720
+ removeHandlers();
3721
+ if (target.onInterrupt !== void 0) {
3722
+ try {
3723
+ target.onInterrupt({
3724
+ experimentId: target.experimentId,
3725
+ experimentUrl: target.experimentUrl,
3726
+ signal,
3727
+ markedInterrupted
3728
+ });
3729
+ } catch {
3730
+ }
3731
+ return;
3732
+ }
3733
+ reportInterrupted(target.experimentId, markedInterrupted);
3734
+ if (!hostHandlesSignal) {
3735
+ host.kill(host.pid, signal);
3736
+ }
3737
+ };
3738
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3739
+ const handler = () => {
3740
+ if (interrupting) {
3741
+ return;
3742
+ }
3743
+ interrupting = true;
3744
+ void handleSignal(signal);
3745
+ };
3746
+ handlers.set(signal, handler);
3747
+ host.on(signal, handler);
3748
+ }
3749
+ return { remove: removeHandlers, interrupted: () => interrupting };
3750
+ }
3751
+ var REPLAY_INTERRUPT_SIGNALS;
3752
+ var init_replayInterrupt = __esm({
3753
+ "src/replayInterrupt.ts"() {
3754
+ "use strict";
3755
+ REPLAY_INTERRUPT_SIGNALS = [
3756
+ "SIGINT",
3757
+ "SIGTERM"
3758
+ ];
3759
+ }
3760
+ });
3761
+
3762
+ // src/selectiveReplay.ts
3763
+ function replayNodeKey(node) {
3764
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3765
+ }
3766
+ function jsonFingerprint(value) {
3767
+ const seen = /* @__PURE__ */ new Set();
3768
+ function encode(input) {
3769
+ if (input === null || typeof input === "boolean") {
3770
+ return JSON.stringify(input);
3771
+ }
3772
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3773
+ return JSON.stringify(input);
3774
+ }
3775
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3776
+ return JSON.stringify(input);
3777
+ }
3778
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3779
+ throw new Error("Not a lossless JSON value");
3780
+ }
3781
+ seen.add(input);
3782
+ const prototype = Object.getPrototypeOf(input);
3783
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3784
+ throw new Error("Not a plain JSON object");
3785
+ }
3786
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3787
+ throw new Error("Symbol properties are not recorded");
3788
+ }
3789
+ const keys = Object.keys(input);
3790
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3791
+ throw new Error("Non-enumerable properties are not recorded");
3792
+ }
3793
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3794
+ throw new Error("Not a dense JSON array");
3795
+ }
3796
+ const properties = keys.map((key) => {
3797
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3798
+ if (!descriptor || !("value" in descriptor)) {
3799
+ throw new Error("Accessor properties are not replay inputs");
3800
+ }
3801
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3802
+ });
3803
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3804
+ }
3805
+ try {
3806
+ return encode(value);
3807
+ } catch {
3808
+ return void 0;
3809
+ }
3810
+ }
3811
+ var init_selectiveReplay = __esm({
3812
+ "src/selectiveReplay.ts"() {
3813
+ "use strict";
3814
+ }
3815
+ });
3816
+
3817
+ // src/selectiveReplayRuntime.ts
3818
+ function validateSelectiveReplayOptions(options) {
3819
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3820
+ (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
3821
+ );
3822
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3823
+ throw new BitfabError(
3824
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3825
+ );
3826
+ }
3827
+ }
3828
+ function policyKey(options) {
3829
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3830
+ }
3831
+ var SelectiveReplayRuntime;
3832
+ var init_selectiveReplayRuntime = __esm({
3833
+ "src/selectiveReplayRuntime.ts"() {
3834
+ "use strict";
3835
+ init_errors();
3836
+ init_selectiveReplay();
3837
+ SelectiveReplayRuntime = class {
3838
+ constructor(plan, options) {
3839
+ this.plan = plan;
3840
+ this.options = options;
3841
+ this.nodes = /* @__PURE__ */ new Map();
3842
+ this.children = /* @__PURE__ */ new Map();
3843
+ this.runtimeParents = /* @__PURE__ */ new Map();
3844
+ this.consumed = /* @__PURE__ */ new Map();
3845
+ this.requiredScopes = /* @__PURE__ */ new Set();
3846
+ this.recordings = /* @__PURE__ */ new Map();
3847
+ this.reached = /* @__PURE__ */ new Set();
3848
+ this.decisions = [];
3849
+ this.assertionNames = /* @__PURE__ */ new Set();
3850
+ this.assertionScopes = /* @__PURE__ */ new Set();
3851
+ this.observedNames = /* @__PURE__ */ new Map();
3852
+ this.unresolvedAssertions = [];
3853
+ validateSelectiveReplayOptions(options);
3854
+ 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) {
3855
+ throw new BitfabError(
3856
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3857
+ );
3858
+ }
3859
+ validateSelectiveReplayOptions(plan.options);
3860
+ if (policyKey(plan.options) !== policyKey(options)) {
3861
+ throw new BitfabError(
3862
+ "Selective replay plan does not match the requested mustRun nodes."
3863
+ );
3864
+ }
3865
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3866
+ for (const node of plan.nodes) {
3867
+ 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") {
3868
+ throw new BitfabError("Invalid selective replay node.");
3869
+ }
3870
+ this.nodes.set(node.id, node);
3871
+ if (node.recording !== void 0) {
3872
+ this.recordings.set(node.id, {
3873
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3874
+ output: jsonFingerprint(node.recording.output)
3875
+ });
3876
+ }
3877
+ if (node.parentId !== null) {
3878
+ const siblings = this.children.get(node.parentId) ?? [];
3879
+ siblings.push(node);
3880
+ this.children.set(node.parentId, siblings);
3881
+ }
3882
+ }
3883
+ const root = this.nodes.get(plan.rootId);
3884
+ if (!root || root.parentId !== null || !root.mustRun) {
3885
+ throw new BitfabError("Selective replay requires a live recorded root.");
3886
+ }
3887
+ const assertionIds = /* @__PURE__ */ new Set();
3888
+ let wholeTrace = false;
3889
+ for (const assertion of plan.assertions) {
3890
+ const target = assertion?.target;
3891
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3892
+ throw new BitfabError(
3893
+ "Invalid selective replay assertion requirements."
3894
+ );
3895
+ }
3896
+ assertionIds.add(assertion.id);
3897
+ if (target === null) {
3898
+ wholeTrace = true;
3899
+ } else if (target.kind === "span") {
3900
+ 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)) {
3901
+ throw new BitfabError("Invalid selective replay assertion target.");
3902
+ }
3903
+ this.assertionNames.add(target.name);
3904
+ const count = plan.nodes.filter(
3905
+ (node) => node.spanName === target.name
3906
+ ).length;
3907
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3908
+ this.unresolvedAssertions.push(assertion.id);
3909
+ }
3910
+ }
3911
+ }
3912
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3913
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3914
+ const unresolved = options.mustRun.filter(
3915
+ (node) => !recordedKeys.has(replayNodeKey(node))
3916
+ );
3917
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3918
+ throw new BitfabError(
3919
+ "Selective replay plan has incomplete mustRun coverage."
3920
+ );
3921
+ }
3922
+ for (const node of plan.nodes) {
3923
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3924
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3925
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3926
+ let parentId = node.parentId;
3927
+ while (parentId !== null) {
3928
+ const parent = this.nodes.get(parentId);
3929
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3930
+ throw new BitfabError("Invalid selective replay ancestry.");
3931
+ }
3932
+ ancestors.add(parentId);
3933
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3934
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3935
+ parentId = parent.parentId;
3936
+ }
3937
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3938
+ throw new BitfabError("Selective replay plan could hide required code.");
3939
+ }
3940
+ if (requiredForAssertion && !node.mustRun) {
3941
+ throw new BitfabError(
3942
+ "Selective replay plan could hide assertion evidence."
3943
+ );
3944
+ }
3945
+ }
3946
+ }
3947
+ enter(params) {
3948
+ const identity = {
3949
+ traceFunctionKey: params.traceFunctionKey,
3950
+ spanName: params.spanName
3951
+ };
3952
+ const key = replayNodeKey(identity);
3953
+ const fingerprint = jsonFingerprint(params.inputs);
3954
+ let node;
3955
+ let matchReason = "unmatched-call";
3956
+ if (params.parentSpanId === null) {
3957
+ node = this.nodes.get(this.plan.rootId);
3958
+ } else {
3959
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3960
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3961
+ this.consumed.set(params.parentSpanId, consumed);
3962
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3963
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3964
+ ) : [];
3965
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3966
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3967
+ );
3968
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3969
+ if (node) {
3970
+ consumed.add(node.id);
3971
+ } else if (candidates.length > 0) {
3972
+ matchReason = "ambiguous-call";
3973
+ for (const candidate of candidates) {
3974
+ consumed.add(candidate.id);
3975
+ }
3976
+ }
3977
+ }
3978
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3979
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3980
+ if (requiredByManifest) {
3981
+ this.requiredScopes.add(params.spanId);
3982
+ }
3983
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3984
+ if (requiredForAssertion) {
3985
+ this.assertionScopes.add(params.spanId);
3986
+ }
3987
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3988
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3989
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3990
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3991
+ 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;
3992
+ const decision = (action, why) => {
3993
+ this.decisions.push({
3994
+ ...identity,
3995
+ originalSpanId: node?.id ?? null,
3996
+ action,
3997
+ reason: why
3998
+ });
3999
+ };
4000
+ if (params.safetyMock && (mustRun || !reusable)) {
4001
+ decision("blocked", `safety-conflict:${reason}`);
4002
+ this.failure = new BitfabError(
4003
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
4004
+ );
4005
+ throw this.failure;
4006
+ }
4007
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
4008
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
4009
+ return {
4010
+ mock: true,
4011
+ output: JSON.parse(recording.output)
4012
+ };
4013
+ }
4014
+ decision("run", reason);
4015
+ this.observedNames.set(
4016
+ params.spanName,
4017
+ (this.observedNames.get(params.spanName) ?? 0) + 1
4018
+ );
4019
+ if (this.required.has(key)) {
4020
+ this.reached.add(key);
4021
+ }
4022
+ return { mock: false };
4023
+ }
4024
+ assertNoSafetyConflict() {
4025
+ if (this.failure) {
4026
+ throw this.failure;
4027
+ }
4028
+ }
4029
+ report() {
4030
+ return {
4031
+ decisions: this.decisions,
4032
+ mustRunNotReached: this.options.mustRun.filter(
4033
+ (node) => !this.reached.has(replayNodeKey(node))
4034
+ ),
4035
+ unresolvedMustRun: this.plan.unresolvedMustRun,
4036
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
4037
+ unresolvedAssertions: this.unresolvedAssertions,
4038
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
4039
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
4040
+ )
4041
+ };
4042
+ }
4043
+ };
4044
+ }
4045
+ });
4046
+
3626
4047
  // src/replay.ts
3627
4048
  var replay_exports = {};
3628
4049
  __export(replay_exports, {
@@ -3878,7 +4299,7 @@ function buildMockTree(rootNode) {
3878
4299
  }
3879
4300
  return { spans };
3880
4301
  }
3881
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4302
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3882
4303
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3883
4304
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3884
4305
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3891,6 +4312,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3891
4312
  let replayError = null;
3892
4313
  let replayDurationMs = null;
3893
4314
  let replayStarted = null;
4315
+ let selectiveReplay;
3894
4316
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3895
4317
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3896
4318
  try {
@@ -3923,6 +4345,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3923
4345
  originalTraceId
3924
4346
  );
3925
4347
  }
4348
+ if (selectiveOptions !== void 0) {
4349
+ if (serverItem.selectiveReplayPlan === void 0) {
4350
+ throw new BitfabError(
4351
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4352
+ );
4353
+ }
4354
+ selectiveReplay = new SelectiveReplayRuntime(
4355
+ serverItem.selectiveReplayPlan,
4356
+ selectiveOptions
4357
+ );
4358
+ }
3926
4359
  const span = await httpClient.getExternalSpan(originalSpanId, {
3927
4360
  view: "replay"
3928
4361
  });
@@ -3942,7 +4375,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3942
4375
  });
3943
4376
  }
3944
4377
  const hasOverrides = resolvedOverrides.length > 0;
3945
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4378
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3946
4379
  const includeOutputs = mockStrategy === "all";
3947
4380
  let mockTree;
3948
4381
  if (needTree) {
@@ -4017,6 +4450,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4017
4450
  sourceBitfabTraceId: originalTraceId,
4018
4451
  replayAttempt: attempt,
4019
4452
  mockTree,
4453
+ selectiveReplay,
4020
4454
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
4021
4455
  mockStrategy,
4022
4456
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -4028,6 +4462,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4028
4462
  () => fn(...inputs)
4029
4463
  );
4030
4464
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4465
+ selectiveReplay?.assertNoSafetyConflict();
4031
4466
  replayDurationMs = Math.round(performance.now() - replayStarted);
4032
4467
  } catch (e) {
4033
4468
  if (replayStarted !== null) {
@@ -4079,6 +4514,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
4079
4514
  traceError,
4080
4515
  replayError,
4081
4516
  durationMs: replayDurationMs,
4517
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
4082
4518
  originalDurationMs,
4083
4519
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
4084
4520
  originalModel,
@@ -4183,6 +4619,14 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4183
4619
  "force only applies when resuming an experiment. Pass it together with resume."
4184
4620
  );
4185
4621
  }
4622
+ if (options?.experimentalSelectiveReplay !== void 0) {
4623
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4624
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4625
+ throw new BitfabError(
4626
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4627
+ );
4628
+ }
4629
+ }
4186
4630
  if (options?.traceIds !== void 0) {
4187
4631
  if (options.traceIds.length === 0) {
4188
4632
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4224,7 +4668,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4224
4668
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4225
4669
  let session;
4226
4670
  if (options?.resume === void 0) {
4227
- const started2 = await httpClient.startReplay(
4671
+ const started = await httpClient.startReplay(
4228
4672
  traceFunctionKey,
4229
4673
  // limit is meaningless with explicit traceIds (the ID list determines
4230
4674
  // the count), so it's omitted from the request entirely.
@@ -4244,9 +4688,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4244
4688
  requestedAttempts,
4245
4689
  options?.adaptInputs !== void 0,
4246
4690
  options?.onlyWithAssertions,
4247
- options?.judgeAssertions
4691
+ options?.judgeAssertions,
4692
+ options?.experimentalSelectiveReplay
4248
4693
  );
4249
- if (started2.items.length === 0) {
4694
+ if (started.items.length === 0) {
4250
4695
  try {
4251
4696
  console.warn(
4252
4697
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4255,10 +4700,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4255
4700
  }
4256
4701
  }
4257
4702
  session = {
4258
- experimentId: started2.experimentId,
4259
- experimentUrl: started2.experimentUrl,
4703
+ experimentId: started.experimentId,
4704
+ experimentUrl: started.experimentUrl,
4260
4705
  attempts: requestedAttempts,
4261
- work: expandReplayWork(started2.items, requestedAttempts),
4706
+ work: expandReplayWork(started.items, requestedAttempts),
4262
4707
  finished: [],
4263
4708
  includeDbBranchLease,
4264
4709
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4273,9 +4718,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4273
4718
  options.dbBranch
4274
4719
  );
4275
4720
  }
4276
- const { experimentId, experimentUrl, attempts } = session;
4721
+ const { experimentId } = session;
4722
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4723
+ const heartbeat = startReplayHeartbeat(
4724
+ () => httpClient.heartbeatReplay(experimentId)
4725
+ );
4726
+ const interruptReplay = async () => {
4727
+ await heartbeat.stop();
4728
+ return httpClient.interruptReplay(experimentId);
4729
+ };
4730
+ const interruptHandlers = installReplayInterruptHandlers({
4731
+ experimentId,
4732
+ experimentUrl: fullExperimentUrl,
4733
+ interrupt: interruptReplay,
4734
+ onInterrupt: options?.onInterrupt
4735
+ });
4736
+ let completeReturned = false;
4737
+ const markCompleteReturned = () => {
4738
+ completeReturned = true;
4739
+ void heartbeat.stop();
4740
+ };
4741
+ try {
4742
+ return await runReplaySession(
4743
+ httpClient,
4744
+ fn,
4745
+ session,
4746
+ fullExperimentUrl,
4747
+ maxConcurrency,
4748
+ interruptHandlers.remove,
4749
+ markCompleteReturned,
4750
+ interruptHandlers.interrupted,
4751
+ options,
4752
+ registeredOverrides
4753
+ );
4754
+ } catch (error) {
4755
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4756
+ await interruptReplay();
4757
+ }
4758
+ throw error;
4759
+ } finally {
4760
+ interruptHandlers.remove();
4761
+ await heartbeat.stop();
4762
+ }
4763
+ }
4764
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4765
+ if (!interrupted()) {
4766
+ return;
4767
+ }
4768
+ const cause = new BitfabError(
4769
+ `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}.`,
4770
+ experimentUrl
4771
+ );
4772
+ throw new ReplayError(
4773
+ cause.message,
4774
+ items,
4775
+ experimentId,
4776
+ experimentUrl,
4777
+ cause
4778
+ );
4779
+ }
4780
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4781
+ const { experimentId, attempts } = session;
4277
4782
  const mockStrategy = options?.mock ?? "marked";
4278
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4279
4783
  try {
4280
4784
  options?.onExperimentStart?.({
4281
4785
  experimentId,
@@ -4306,7 +4810,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4306
4810
  session.includeDbBranchLease,
4307
4811
  session.dbBranchSettings,
4308
4812
  options?.adaptInputs,
4309
- options?.dryRun === true
4813
+ options?.dryRun === true,
4814
+ options?.experimentalSelectiveReplay
4310
4815
  )
4311
4816
  );
4312
4817
  const total = tasks.length + carriedOverItems.length;
@@ -4379,6 +4884,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4379
4884
  traceError: item.traceError,
4380
4885
  replayError: item.replayError,
4381
4886
  durationMs: item.durationMs,
4887
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4382
4888
  originalDurationMs: item.originalDurationMs,
4383
4889
  originalTokens: item.originalTokens,
4384
4890
  originalModel: item.originalModel,
@@ -4423,7 +4929,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4423
4929
  );
4424
4930
  const resultItems = [...carriedOverItems, ...ranItems];
4425
4931
  if (options?.dryRun === true) {
4932
+ throwIfInterrupted(
4933
+ interrupted,
4934
+ resultItems,
4935
+ experimentId,
4936
+ fullExperimentUrl
4937
+ );
4938
+ stopInterruptingOnSignal();
4426
4939
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4940
+ markCompleteReturned();
4427
4941
  const dryResult = {
4428
4942
  items: resultItems,
4429
4943
  experimentId,
@@ -4448,12 +4962,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4448
4962
  ranItems[index].traceId = readBack;
4449
4963
  }
4450
4964
  }
4965
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4966
+ stopInterruptingOnSignal();
4451
4967
  const completeResult = await preserveReplayFailure(
4452
4968
  () => httpClient.completeReplay(experimentId),
4453
4969
  resultItems,
4454
4970
  experimentId,
4455
4971
  fullExperimentUrl
4456
4972
  );
4973
+ markCompleteReturned();
4457
4974
  const serverTraceIds = completeResult.traceIds;
4458
4975
  const replayTokens = completeResult.tokens;
4459
4976
  if (serverTraceIds !== void 0) {
@@ -4557,6 +5074,9 @@ var init_replay = __esm({
4557
5074
  init_mockOverride();
4558
5075
  init_randomUuid();
4559
5076
  init_replayContext();
5077
+ init_replayHeartbeat();
5078
+ init_replayInterrupt();
5079
+ init_selectiveReplayRuntime();
4560
5080
  init_serialize();
4561
5081
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4562
5082
  MAX_REPLAY_ATTEMPTS = 100;
@@ -4568,7 +5088,8 @@ var init_replay = __esm({
4568
5088
  "attempts",
4569
5089
  "graderIds",
4570
5090
  "onlyWithAssertions",
4571
- "dryRun"
5091
+ "dryRun",
5092
+ "experimentalSelectiveReplay"
4572
5093
  ];
4573
5094
  CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4574
5095
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
@@ -9269,6 +9790,9 @@ var Bitfab = class {
9269
9790
  ...options.mockOnReplay !== void 0 && {
9270
9791
  mockOnReplay: options.mockOnReplay
9271
9792
  },
9793
+ ...options.replayReusable !== void 0 && {
9794
+ replayReusable: options.replayReusable
9795
+ },
9272
9796
  ...options.finalize !== void 0 && { finalize: options.finalize }
9273
9797
  };
9274
9798
  }
@@ -9417,6 +9941,9 @@ var Bitfab = class {
9417
9941
  ...mockOnReplay !== void 0 && {
9418
9942
  mockOnReplay
9419
9943
  },
9944
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9945
+ replayReusable: nodeConfiguration.replayReusable
9946
+ },
9420
9947
  ...nodeConfiguration?.finalize !== void 0 && {
9421
9948
  finalize: nodeConfiguration.finalize
9422
9949
  }
@@ -10214,6 +10741,7 @@ var Bitfab = class {
10214
10741
  instrumentation: options.instrumentation ?? "span",
10215
10742
  functionId: options.functionId,
10216
10743
  captureContent: options.captureContent ?? true,
10744
+ replayReusable: options.replayReusable,
10217
10745
  autoTraceDefinition: options.autoTraceDefinition,
10218
10746
  declaredNode: options.declaredNode === true,
10219
10747
  nestedTrace: options.nestedTrace,
@@ -10333,6 +10861,27 @@ var Bitfab = class {
10333
10861
  return result;
10334
10862
  };
10335
10863
  const replayCtxForMock = getReplayContext();
10864
+ if (replayCtxForMock?.selectiveReplay) {
10865
+ const decision = replayCtxForMock.selectiveReplay.enter({
10866
+ traceFunctionKey,
10867
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10868
+ spanId,
10869
+ parentSpanId,
10870
+ inputs: args,
10871
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10872
+ reuseAllowed: options.replayReusable === true,
10873
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10874
+ });
10875
+ if (decision.mock) {
10876
+ void sendSpan({
10877
+ result: decision.output,
10878
+ mocked: true,
10879
+ mockTarget: "output",
10880
+ mockSource: "recorded"
10881
+ });
10882
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10883
+ }
10884
+ }
10336
10885
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10337
10886
  const counters = replayCtxForMock.callCounters;
10338
10887
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10756,6 +11305,9 @@ var Bitfab = class {
10756
11305
  span_origin: makeSpanOrigin(params.instrumentation),
10757
11306
  span_data: {
10758
11307
  name: params.spanName,
11308
+ ...params.replayReusable !== void 0 && {
11309
+ replay_reusable: params.replayReusable
11310
+ },
10759
11311
  type: params.spanType,
10760
11312
  ...params.functionId !== void 0 && {
10761
11313
  function_id: params.functionId,
@@ -11307,6 +11859,7 @@ init_replay();
11307
11859
  // src/replayRegistry.ts
11308
11860
  init_errors();
11309
11861
  init_replay();
11862
+ init_replayInterrupt();
11310
11863
  function defineReplayRegistry(registry) {
11311
11864
  return registry;
11312
11865
  }