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