bitfab 0.61.1 → 0.61.3

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 (42) 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-HKUHPLAM.js → chunk-6IMUONDN.js} +47 -3
  6. package/dist/chunk-6IMUONDN.js.map +1 -0
  7. package/dist/{chunk-I5J2FDKH.js → chunk-HVEZHLVQ.js} +46 -3
  8. package/dist/chunk-HVEZHLVQ.js.map +1 -0
  9. package/dist/{chunk-VKGLQL7V.js → chunk-IZUZ5NBB.js} +473 -51
  10. package/dist/chunk-IZUZ5NBB.js.map +1 -0
  11. package/dist/{chunk-BMJNDIVA.js → chunk-K4XCWQBC.js} +1 -1
  12. package/dist/chunk-K4XCWQBC.js.map +1 -0
  13. package/dist/{chunk-ENHRFXCR.js → chunk-PAUXHTNF.js} +38 -7
  14. package/dist/chunk-PAUXHTNF.js.map +1 -0
  15. package/dist/{chunk-S4WE6U2Y.js → chunk-WOHIAKZF.js} +122 -52
  16. package/dist/chunk-WOHIAKZF.js.map +1 -0
  17. package/dist/{http-5DA23DKC.js → http-3IYU5Z54.js} +2 -2
  18. package/dist/{http-NY6M2AK3.js → http-BYICBOWP.js} +2 -2
  19. package/dist/index.cjs +563 -52
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +146 -3
  22. package/dist/index.d.ts +146 -3
  23. package/dist/index.js +4 -4
  24. package/dist/node.cjs +563 -52
  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-KVENJRCX.js → replay-SXHX5BBU.js} +3 -3
  30. package/dist/replayCli.js +4 -3
  31. package/dist/replayCli.js.map +1 -1
  32. package/dist/seedCli.js +2 -2
  33. package/package.json +1 -1
  34. package/dist/chunk-BMJNDIVA.js.map +0 -1
  35. package/dist/chunk-ENHRFXCR.js.map +0 -1
  36. package/dist/chunk-HKUHPLAM.js.map +0 -1
  37. package/dist/chunk-I5J2FDKH.js.map +0 -1
  38. package/dist/chunk-S4WE6U2Y.js.map +0 -1
  39. package/dist/chunk-VKGLQL7V.js.map +0 -1
  40. /package/dist/{http-5DA23DKC.js.map → http-3IYU5Z54.js.map} +0 -0
  41. /package/dist/{http-NY6M2AK3.js.map → http-BYICBOWP.js.map} +0 -0
  42. /package/dist/{replay-KVENJRCX.js.map → replay-SXHX5BBU.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.1";
100
+ __version__ = "0.61.3";
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 {
@@ -2898,8 +2899,11 @@ var init_http = __esm({
2898
2899
  * Start a replay session by fetching historical traces.
2899
2900
  * Blocking call - creates an experiment and returns lightweight item references.
2900
2901
  */
2901
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2902
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2902
2903
  const payload = { traceFunctionKey };
2904
+ if (experimentalSelectiveReplay !== void 0) {
2905
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2906
+ }
2903
2907
  if (limit !== void 0) {
2904
2908
  payload.limit = limit;
2905
2909
  }
@@ -2967,6 +2971,46 @@ var init_http = __esm({
2967
2971
  }
2968
2972
  return { ...rest, experimentId, experimentUrl };
2969
2973
  }
2974
+ async resumeReplay(experimentId, traceFunctionKey, codeChangeFiles, force) {
2975
+ const payload = {
2976
+ experimentId,
2977
+ traceFunctionKey,
2978
+ lazyDbBranchLease: true
2979
+ };
2980
+ if (force === true) {
2981
+ payload.force = true;
2982
+ }
2983
+ if (codeChangeFiles !== void 0) {
2984
+ payload.codeChangeFiles = codeChangeFiles;
2985
+ }
2986
+ const git = await resolvedGitState();
2987
+ if (git && !isEmptyGitState(git)) {
2988
+ payload.git = git;
2989
+ }
2990
+ const response = await this.request(
2991
+ "/api/sdk/replay/resume",
2992
+ payload,
2993
+ { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
2994
+ );
2995
+ const { testRunId, testRunUrl, ...rest } = response;
2996
+ const resumedExperimentId = response.experimentId ?? testRunId;
2997
+ const experimentUrl = response.experimentUrl ?? testRunUrl;
2998
+ if (resumedExperimentId === void 0 || experimentUrl === void 0) {
2999
+ throw new BitfabError(
3000
+ "Replay resume response is missing the experiment ID or URL."
3001
+ );
3002
+ }
3003
+ return {
3004
+ ...rest,
3005
+ experimentId: resumedExperimentId,
3006
+ experimentUrl,
3007
+ work: rest.work ?? [],
3008
+ finished: rest.finished ?? [],
3009
+ skippedOriginalTraceIds: rest.skippedOriginalTraceIds ?? [],
3010
+ includeDbBranchLease: rest.includeDbBranchLease ?? false,
3011
+ dbBranchSettings: rest.dbBranchSettings ?? null
3012
+ };
3013
+ }
2970
3014
  /**
2971
3015
  * Fetch an external span by ID.
2972
3016
  * Blocking GET request.
@@ -3583,6 +3627,291 @@ var init_integrationFixture = __esm({
3583
3627
  }
3584
3628
  });
3585
3629
 
3630
+ // src/selectiveReplay.ts
3631
+ function replayNodeKey(node) {
3632
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3633
+ }
3634
+ function jsonFingerprint(value) {
3635
+ const seen = /* @__PURE__ */ new Set();
3636
+ function encode(input) {
3637
+ if (input === null || typeof input === "boolean") {
3638
+ return JSON.stringify(input);
3639
+ }
3640
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3641
+ return JSON.stringify(input);
3642
+ }
3643
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3644
+ return JSON.stringify(input);
3645
+ }
3646
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3647
+ throw new Error("Not a lossless JSON value");
3648
+ }
3649
+ seen.add(input);
3650
+ const prototype = Object.getPrototypeOf(input);
3651
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3652
+ throw new Error("Not a plain JSON object");
3653
+ }
3654
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3655
+ throw new Error("Symbol properties are not recorded");
3656
+ }
3657
+ const keys = Object.keys(input);
3658
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3659
+ throw new Error("Non-enumerable properties are not recorded");
3660
+ }
3661
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3662
+ throw new Error("Not a dense JSON array");
3663
+ }
3664
+ const properties = keys.map((key) => {
3665
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3666
+ if (!descriptor || !("value" in descriptor)) {
3667
+ throw new Error("Accessor properties are not replay inputs");
3668
+ }
3669
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3670
+ });
3671
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3672
+ }
3673
+ try {
3674
+ return encode(value);
3675
+ } catch {
3676
+ return void 0;
3677
+ }
3678
+ }
3679
+ var init_selectiveReplay = __esm({
3680
+ "src/selectiveReplay.ts"() {
3681
+ "use strict";
3682
+ }
3683
+ });
3684
+
3685
+ // src/selectiveReplayRuntime.ts
3686
+ function validateSelectiveReplayOptions(options) {
3687
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3688
+ (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
3689
+ );
3690
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3691
+ throw new BitfabError(
3692
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3693
+ );
3694
+ }
3695
+ }
3696
+ function policyKey(options) {
3697
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3698
+ }
3699
+ var SelectiveReplayRuntime;
3700
+ var init_selectiveReplayRuntime = __esm({
3701
+ "src/selectiveReplayRuntime.ts"() {
3702
+ "use strict";
3703
+ init_errors();
3704
+ init_selectiveReplay();
3705
+ SelectiveReplayRuntime = class {
3706
+ constructor(plan, options) {
3707
+ this.plan = plan;
3708
+ this.options = options;
3709
+ this.nodes = /* @__PURE__ */ new Map();
3710
+ this.children = /* @__PURE__ */ new Map();
3711
+ this.runtimeParents = /* @__PURE__ */ new Map();
3712
+ this.consumed = /* @__PURE__ */ new Map();
3713
+ this.requiredScopes = /* @__PURE__ */ new Set();
3714
+ this.recordings = /* @__PURE__ */ new Map();
3715
+ this.reached = /* @__PURE__ */ new Set();
3716
+ this.decisions = [];
3717
+ this.assertionNames = /* @__PURE__ */ new Set();
3718
+ this.assertionScopes = /* @__PURE__ */ new Set();
3719
+ this.observedNames = /* @__PURE__ */ new Map();
3720
+ this.unresolvedAssertions = [];
3721
+ validateSelectiveReplayOptions(options);
3722
+ 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) {
3723
+ throw new BitfabError(
3724
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3725
+ );
3726
+ }
3727
+ validateSelectiveReplayOptions(plan.options);
3728
+ if (policyKey(plan.options) !== policyKey(options)) {
3729
+ throw new BitfabError(
3730
+ "Selective replay plan does not match the requested mustRun nodes."
3731
+ );
3732
+ }
3733
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3734
+ for (const node of plan.nodes) {
3735
+ 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") {
3736
+ throw new BitfabError("Invalid selective replay node.");
3737
+ }
3738
+ this.nodes.set(node.id, node);
3739
+ if (node.recording !== void 0) {
3740
+ this.recordings.set(node.id, {
3741
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3742
+ output: jsonFingerprint(node.recording.output)
3743
+ });
3744
+ }
3745
+ if (node.parentId !== null) {
3746
+ const siblings = this.children.get(node.parentId) ?? [];
3747
+ siblings.push(node);
3748
+ this.children.set(node.parentId, siblings);
3749
+ }
3750
+ }
3751
+ const root = this.nodes.get(plan.rootId);
3752
+ if (!root || root.parentId !== null || !root.mustRun) {
3753
+ throw new BitfabError("Selective replay requires a live recorded root.");
3754
+ }
3755
+ const assertionIds = /* @__PURE__ */ new Set();
3756
+ let wholeTrace = false;
3757
+ for (const assertion of plan.assertions) {
3758
+ const target = assertion?.target;
3759
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3760
+ throw new BitfabError(
3761
+ "Invalid selective replay assertion requirements."
3762
+ );
3763
+ }
3764
+ assertionIds.add(assertion.id);
3765
+ if (target === null) {
3766
+ wholeTrace = true;
3767
+ } else if (target.kind === "span") {
3768
+ 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)) {
3769
+ throw new BitfabError("Invalid selective replay assertion target.");
3770
+ }
3771
+ this.assertionNames.add(target.name);
3772
+ const count = plan.nodes.filter(
3773
+ (node) => node.spanName === target.name
3774
+ ).length;
3775
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3776
+ this.unresolvedAssertions.push(assertion.id);
3777
+ }
3778
+ }
3779
+ }
3780
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3781
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3782
+ const unresolved = options.mustRun.filter(
3783
+ (node) => !recordedKeys.has(replayNodeKey(node))
3784
+ );
3785
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3786
+ throw new BitfabError(
3787
+ "Selective replay plan has incomplete mustRun coverage."
3788
+ );
3789
+ }
3790
+ for (const node of plan.nodes) {
3791
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3792
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3793
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3794
+ let parentId = node.parentId;
3795
+ while (parentId !== null) {
3796
+ const parent = this.nodes.get(parentId);
3797
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3798
+ throw new BitfabError("Invalid selective replay ancestry.");
3799
+ }
3800
+ ancestors.add(parentId);
3801
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3802
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3803
+ parentId = parent.parentId;
3804
+ }
3805
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3806
+ throw new BitfabError("Selective replay plan could hide required code.");
3807
+ }
3808
+ if (requiredForAssertion && !node.mustRun) {
3809
+ throw new BitfabError(
3810
+ "Selective replay plan could hide assertion evidence."
3811
+ );
3812
+ }
3813
+ }
3814
+ }
3815
+ enter(params) {
3816
+ const identity = {
3817
+ traceFunctionKey: params.traceFunctionKey,
3818
+ spanName: params.spanName
3819
+ };
3820
+ const key = replayNodeKey(identity);
3821
+ const fingerprint = jsonFingerprint(params.inputs);
3822
+ let node;
3823
+ let matchReason = "unmatched-call";
3824
+ if (params.parentSpanId === null) {
3825
+ node = this.nodes.get(this.plan.rootId);
3826
+ } else {
3827
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3828
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3829
+ this.consumed.set(params.parentSpanId, consumed);
3830
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3831
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3832
+ ) : [];
3833
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3834
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3835
+ );
3836
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3837
+ if (node) {
3838
+ consumed.add(node.id);
3839
+ } else if (candidates.length > 0) {
3840
+ matchReason = "ambiguous-call";
3841
+ for (const candidate of candidates) {
3842
+ consumed.add(candidate.id);
3843
+ }
3844
+ }
3845
+ }
3846
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3847
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3848
+ if (requiredByManifest) {
3849
+ this.requiredScopes.add(params.spanId);
3850
+ }
3851
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3852
+ if (requiredForAssertion) {
3853
+ this.assertionScopes.add(params.spanId);
3854
+ }
3855
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3856
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3857
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3858
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3859
+ 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;
3860
+ const decision = (action, why) => {
3861
+ this.decisions.push({
3862
+ ...identity,
3863
+ originalSpanId: node?.id ?? null,
3864
+ action,
3865
+ reason: why
3866
+ });
3867
+ };
3868
+ if (params.safetyMock && (mustRun || !reusable)) {
3869
+ decision("blocked", `safety-conflict:${reason}`);
3870
+ this.failure = new BitfabError(
3871
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
3872
+ );
3873
+ throw this.failure;
3874
+ }
3875
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
3876
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
3877
+ return {
3878
+ mock: true,
3879
+ output: JSON.parse(recording.output)
3880
+ };
3881
+ }
3882
+ decision("run", reason);
3883
+ this.observedNames.set(
3884
+ params.spanName,
3885
+ (this.observedNames.get(params.spanName) ?? 0) + 1
3886
+ );
3887
+ if (this.required.has(key)) {
3888
+ this.reached.add(key);
3889
+ }
3890
+ return { mock: false };
3891
+ }
3892
+ assertNoSafetyConflict() {
3893
+ if (this.failure) {
3894
+ throw this.failure;
3895
+ }
3896
+ }
3897
+ report() {
3898
+ return {
3899
+ decisions: this.decisions,
3900
+ mustRunNotReached: this.options.mustRun.filter(
3901
+ (node) => !this.reached.has(replayNodeKey(node))
3902
+ ),
3903
+ unresolvedMustRun: this.plan.unresolvedMustRun,
3904
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
3905
+ unresolvedAssertions: this.unresolvedAssertions,
3906
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
3907
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
3908
+ )
3909
+ };
3910
+ }
3911
+ };
3912
+ }
3913
+ });
3914
+
3586
3915
  // src/replay.ts
3587
3916
  var replay_exports = {};
3588
3917
  __export(replay_exports, {
@@ -3602,6 +3931,88 @@ function expandReplayWork(serverItems, attempts) {
3602
3931
  (_, attempt) => serverItems.map((serverItem) => ({ serverItem, attempt }))
3603
3932
  ).flat();
3604
3933
  }
3934
+ function validateResume(resume, options) {
3935
+ if (resume.trim().length === 0) {
3936
+ throw new BitfabError("resume must be the ID of the experiment to resume.");
3937
+ }
3938
+ const conflicting = RESUME_CONFLICTING_OPTIONS.filter(
3939
+ (name) => options[name] !== void 0 && options[name] !== false
3940
+ );
3941
+ if (conflicting.length > 0) {
3942
+ throw new BitfabError(
3943
+ `resume continues experiment ${resume} with the traces, attempts, and graders it started with, so it cannot be combined with ${conflicting.join(", ")}. Drop those options, or start a new replay without resume.`
3944
+ );
3945
+ }
3946
+ }
3947
+ async function resumeReplaySession(httpClient, experimentId, traceFunctionKey, codeChangeFiles, force, requestedDbBranch) {
3948
+ const response = await httpClient.resumeReplay(
3949
+ experimentId,
3950
+ traceFunctionKey,
3951
+ codeChangeFiles,
3952
+ force
3953
+ );
3954
+ const work = response.work.map(({ itemIndex, attempt }) => {
3955
+ const serverItem = response.items[itemIndex];
3956
+ if (serverItem === void 0) {
3957
+ throw new BitfabError(
3958
+ `Replay resume for experiment ${response.experimentId} named item ${itemIndex}, but the server returned only ${response.items.length} item(s).`
3959
+ );
3960
+ }
3961
+ return { serverItem, attempt };
3962
+ });
3963
+ if (response.skippedOriginalTraceIds.length > 0) {
3964
+ try {
3965
+ console.warn(
3966
+ `Bitfab: ${response.skippedOriginalTraceIds.length} trace(s) selected when experiment ${response.experimentId} started no longer exist, so resume skips them: ${response.skippedOriginalTraceIds.join(", ")}`
3967
+ );
3968
+ } catch {
3969
+ }
3970
+ }
3971
+ if (requestedDbBranch !== void 0 && dbBranchEnabled(requestedDbBranch) !== response.includeDbBranchLease) {
3972
+ try {
3973
+ console.warn(
3974
+ `Bitfab: experiment ${response.experimentId} started with database branching ${response.includeDbBranchLease ? "on" : "off"}, so resume keeps it ${response.includeDbBranchLease ? "on" : "off"} and ignores the dbBranch option passed here.`
3975
+ );
3976
+ } catch {
3977
+ }
3978
+ }
3979
+ return {
3980
+ experimentId: response.experimentId,
3981
+ experimentUrl: response.experimentUrl,
3982
+ attempts: response.attempts,
3983
+ work,
3984
+ finished: response.finished,
3985
+ includeDbBranchLease: response.includeDbBranchLease,
3986
+ dbBranchSettings: response.dbBranchSettings ?? void 0
3987
+ };
3988
+ }
3989
+ function carriedOverReplayItem(finished) {
3990
+ return {
3991
+ traceId: finished.traceId,
3992
+ originalTraceId: finished.originalTraceId,
3993
+ originalSpanId: "",
3994
+ sourceTraceId: finished.originalTraceId,
3995
+ sourceSpanId: "",
3996
+ attempt: finished.attempt,
3997
+ input: [],
3998
+ result: void 0,
3999
+ originalOutput: void 0,
4000
+ error: finished.hasError ? CARRIED_OVER_ERROR : null,
4001
+ traceError: null,
4002
+ replayError: null,
4003
+ durationMs: null,
4004
+ originalDurationMs: null,
4005
+ originalTokens: null,
4006
+ originalModel: null,
4007
+ tokens: null,
4008
+ model: null,
4009
+ dbSnapshotRef: null,
4010
+ dbBranchTimings: null,
4011
+ traceOutline: null,
4012
+ originalTraceOutline: null,
4013
+ carriedOver: true
4014
+ };
4015
+ }
3605
4016
  function resolveAttempts(attempts) {
3606
4017
  if (attempts === void 0) {
3607
4018
  return 1;
@@ -3756,7 +4167,7 @@ function buildMockTree(rootNode) {
3756
4167
  }
3757
4168
  return { spans };
3758
4169
  }
3759
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4170
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3760
4171
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3761
4172
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3762
4173
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3769,6 +4180,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3769
4180
  let replayError = null;
3770
4181
  let replayDurationMs = null;
3771
4182
  let replayStarted = null;
4183
+ let selectiveReplay;
3772
4184
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3773
4185
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3774
4186
  try {
@@ -3801,6 +4213,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3801
4213
  originalTraceId
3802
4214
  );
3803
4215
  }
4216
+ if (selectiveOptions !== void 0) {
4217
+ if (serverItem.selectiveReplayPlan === void 0) {
4218
+ throw new BitfabError(
4219
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4220
+ );
4221
+ }
4222
+ selectiveReplay = new SelectiveReplayRuntime(
4223
+ serverItem.selectiveReplayPlan,
4224
+ selectiveOptions
4225
+ );
4226
+ }
3804
4227
  const span = await httpClient.getExternalSpan(originalSpanId, {
3805
4228
  view: "replay"
3806
4229
  });
@@ -3820,7 +4243,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3820
4243
  });
3821
4244
  }
3822
4245
  const hasOverrides = resolvedOverrides.length > 0;
3823
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4246
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3824
4247
  const includeOutputs = mockStrategy === "all";
3825
4248
  let mockTree;
3826
4249
  if (needTree) {
@@ -3895,6 +4318,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3895
4318
  sourceBitfabTraceId: originalTraceId,
3896
4319
  replayAttempt: attempt,
3897
4320
  mockTree,
4321
+ selectiveReplay,
3898
4322
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
3899
4323
  mockStrategy,
3900
4324
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -3906,6 +4330,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3906
4330
  () => fn(...inputs)
3907
4331
  );
3908
4332
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4333
+ selectiveReplay?.assertNoSafetyConflict();
3909
4334
  replayDurationMs = Math.round(performance.now() - replayStarted);
3910
4335
  } catch (e) {
3911
4336
  if (replayStarted !== null) {
@@ -3957,6 +4382,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3957
4382
  traceError,
3958
4383
  replayError,
3959
4384
  durationMs: replayDurationMs,
4385
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
3960
4386
  originalDurationMs,
3961
4387
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
3962
4388
  originalModel,
@@ -4054,6 +4480,21 @@ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted)
4054
4480
  return results;
4055
4481
  }
4056
4482
  async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
4483
+ if (options?.resume !== void 0) {
4484
+ validateResume(options.resume, options);
4485
+ } else if (options?.force === true) {
4486
+ throw new BitfabError(
4487
+ "force only applies when resuming an experiment. Pass it together with resume."
4488
+ );
4489
+ }
4490
+ if (options?.experimentalSelectiveReplay !== void 0) {
4491
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4492
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4493
+ throw new BitfabError(
4494
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4495
+ );
4496
+ }
4497
+ }
4057
4498
  if (options?.traceIds !== void 0) {
4058
4499
  if (options.traceIds.length === 0) {
4059
4500
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4073,7 +4514,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4073
4514
  } catch {
4074
4515
  }
4075
4516
  }
4076
- const attempts = resolveAttempts(options?.attempts);
4517
+ const requestedAttempts = resolveAttempts(options?.attempts);
4077
4518
  const maxConcurrency = options?.maxConcurrency ?? 10;
4078
4519
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
4079
4520
  throw new BitfabError(
@@ -4092,40 +4533,60 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4092
4533
  }
4093
4534
  }
4094
4535
  }
4095
- const {
4096
- experimentId,
4097
- experimentUrl,
4098
- items: serverItems
4099
- } = await httpClient.startReplay(
4100
- traceFunctionKey,
4101
- // limit is meaningless with explicit traceIds (the ID list determines
4102
- // the count), so it's omitted from the request entirely.
4103
- options?.traceIds ? void 0 : options?.limit ?? 5,
4104
- options?.traceIds,
4105
- options?.name,
4106
- codeChangeDescription,
4107
- codeChangeFiles,
4108
- // A dry run executes nothing, so a branch would be provisioned (and billed)
4109
- // for code that never runs, and a seeded source's refusal would fail the item
4110
- // before it could report its resolved inputs.
4111
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4112
- options?.experimentGroupId,
4113
- datasetIds,
4114
- options?.graderIds,
4115
- resolveDbBranchSettings(options?.dbBranch),
4116
- attempts,
4117
- options?.adaptInputs !== void 0,
4118
- options?.onlyWithAssertions,
4119
- options?.judgeAssertions
4120
- );
4121
- if (serverItems.length === 0) {
4122
- try {
4123
- console.warn(
4124
- `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4125
- );
4126
- } catch {
4536
+ const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4537
+ let session;
4538
+ if (options?.resume === void 0) {
4539
+ const started2 = await httpClient.startReplay(
4540
+ traceFunctionKey,
4541
+ // limit is meaningless with explicit traceIds (the ID list determines
4542
+ // the count), so it's omitted from the request entirely.
4543
+ options?.traceIds ? void 0 : options?.limit ?? 5,
4544
+ options?.traceIds,
4545
+ options?.name,
4546
+ codeChangeDescription,
4547
+ codeChangeFiles,
4548
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
4549
+ // for code that never runs, and a seeded source's refusal would fail the item
4550
+ // before it could report its resolved inputs.
4551
+ includeDbBranchLease,
4552
+ options?.experimentGroupId,
4553
+ datasetIds,
4554
+ options?.graderIds,
4555
+ resolveDbBranchSettings(options?.dbBranch),
4556
+ requestedAttempts,
4557
+ options?.adaptInputs !== void 0,
4558
+ options?.onlyWithAssertions,
4559
+ options?.judgeAssertions,
4560
+ options?.experimentalSelectiveReplay
4561
+ );
4562
+ if (started2.items.length === 0) {
4563
+ try {
4564
+ console.warn(
4565
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4566
+ );
4567
+ } catch {
4568
+ }
4127
4569
  }
4570
+ session = {
4571
+ experimentId: started2.experimentId,
4572
+ experimentUrl: started2.experimentUrl,
4573
+ attempts: requestedAttempts,
4574
+ work: expandReplayWork(started2.items, requestedAttempts),
4575
+ finished: [],
4576
+ includeDbBranchLease,
4577
+ dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
4578
+ };
4579
+ } else {
4580
+ session = await resumeReplaySession(
4581
+ httpClient,
4582
+ options.resume,
4583
+ traceFunctionKey,
4584
+ codeChangeFiles,
4585
+ options.force,
4586
+ options.dbBranch
4587
+ );
4128
4588
  }
4589
+ const { experimentId, experimentUrl, attempts } = session;
4129
4590
  const mockStrategy = options?.mock ?? "marked";
4130
4591
  const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4131
4592
  try {
@@ -4139,7 +4600,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4139
4600
  ...normalizeMockOverrides(options?.mockOverride),
4140
4601
  ...registeredOverrides
4141
4602
  ];
4142
- const workItems = expandReplayWork(serverItems, attempts);
4603
+ const workItems = session.work;
4604
+ const carriedOverItems = session.finished.map(
4605
+ (finished) => carriedOverReplayItem(finished)
4606
+ );
4143
4607
  const replayedTraceIds = workItems.map(() => randomUuid());
4144
4608
  httpClient.trackTraceDeliveries(replayedTraceIds);
4145
4609
  const tasks = workItems.map(
@@ -4152,18 +4616,19 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4152
4616
  resolvedOverrides,
4153
4617
  replayedTraceIds[index],
4154
4618
  attempt,
4155
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4156
- resolveDbBranchSettings(options?.dbBranch),
4619
+ session.includeDbBranchLease,
4620
+ session.dbBranchSettings,
4157
4621
  options?.adaptInputs,
4158
- options?.dryRun === true
4622
+ options?.dryRun === true,
4623
+ options?.experimentalSelectiveReplay
4159
4624
  )
4160
4625
  );
4161
- const total = tasks.length;
4626
+ const total = tasks.length + carriedOverItems.length;
4162
4627
  const onItemFinish = options?.onItemFinish ?? options?.onProgress;
4163
- let completed = 0;
4164
- let started = 0;
4165
- let succeeded = 0;
4166
- let errored = 0;
4628
+ let completed = carriedOverItems.length;
4629
+ let started = carriedOverItems.length;
4630
+ let errored = carriedOverItems.filter((item) => item.error !== null).length;
4631
+ let succeeded = completed - errored;
4167
4632
  let flushInFlight = null;
4168
4633
  let flushPending = false;
4169
4634
  const flushFinishedItemTrace = () => {
@@ -4183,7 +4648,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4183
4648
  }
4184
4649
  return flushInFlight;
4185
4650
  };
4186
- const resultItems = await mapWithConcurrency2(
4651
+ const ranItems = await mapWithConcurrency2(
4187
4652
  tasks,
4188
4653
  maxConcurrency,
4189
4654
  async (item, index) => {
@@ -4228,6 +4693,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4228
4693
  traceError: item.traceError,
4229
4694
  replayError: item.replayError,
4230
4695
  durationMs: item.durationMs,
4696
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4231
4697
  originalDurationMs: item.originalDurationMs,
4232
4698
  originalTokens: item.originalTokens,
4233
4699
  originalModel: item.originalModel,
@@ -4270,6 +4736,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4270
4736
  }
4271
4737
  } : void 0
4272
4738
  );
4739
+ const resultItems = [...carriedOverItems, ...ranItems];
4273
4740
  if (options?.dryRun === true) {
4274
4741
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4275
4742
  const dryResult = {
@@ -4289,11 +4756,11 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4289
4756
  experimentId,
4290
4757
  fullExperimentUrl
4291
4758
  );
4292
- for (let index = 0; index < resultItems.length; index += 1) {
4759
+ for (let index = 0; index < ranItems.length; index += 1) {
4293
4760
  const localId = replayedTraceIds[index];
4294
4761
  const readBack = localId ? deliveredTraceIds[localId] : void 0;
4295
4762
  if (readBack !== void 0) {
4296
- resultItems[index].traceId = readBack;
4763
+ ranItems[index].traceId = readBack;
4297
4764
  }
4298
4765
  }
4299
4766
  const completeResult = await preserveReplayFailure(
@@ -4307,8 +4774,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4307
4774
  if (serverTraceIds !== void 0) {
4308
4775
  const missing = [];
4309
4776
  let completedCount = 0;
4310
- for (let index = 0; index < resultItems.length; index += 1) {
4311
- const item = resultItems[index];
4777
+ for (let index = 0; index < ranItems.length; index += 1) {
4778
+ const item = ranItems[index];
4312
4779
  const localId = replayedTraceIds[index];
4313
4780
  const mapped = localId ? serverTraceIds[localId] : void 0;
4314
4781
  item.traceId = item.traceId ?? mapped ?? null;
@@ -4394,7 +4861,7 @@ async function writeReplayResultFile(result) {
4394
4861
  }
4395
4862
  }
4396
4863
  }
4397
- var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4864
+ var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, RESUME_CONFLICTING_OPTIONS, CARRIED_OVER_ERROR, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4398
4865
  var init_replay = __esm({
4399
4866
  "src/replay.ts"() {
4400
4867
  "use strict";
@@ -4405,9 +4872,22 @@ var init_replay = __esm({
4405
4872
  init_mockOverride();
4406
4873
  init_randomUuid();
4407
4874
  init_replayContext();
4875
+ init_selectiveReplayRuntime();
4408
4876
  init_serialize();
4409
4877
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4410
4878
  MAX_REPLAY_ATTEMPTS = 100;
4879
+ RESUME_CONFLICTING_OPTIONS = [
4880
+ "traceIds",
4881
+ "limit",
4882
+ "datasetId",
4883
+ "datasetIds",
4884
+ "attempts",
4885
+ "graderIds",
4886
+ "onlyWithAssertions",
4887
+ "dryRun",
4888
+ "experimentalSelectiveReplay"
4889
+ ];
4890
+ CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4411
4891
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
4412
4892
  ReplayError = class extends BitfabError {
4413
4893
  constructor(message, items, experimentId, experimentUrl, cause) {
@@ -9106,6 +9586,9 @@ var Bitfab = class {
9106
9586
  ...options.mockOnReplay !== void 0 && {
9107
9587
  mockOnReplay: options.mockOnReplay
9108
9588
  },
9589
+ ...options.replayReusable !== void 0 && {
9590
+ replayReusable: options.replayReusable
9591
+ },
9109
9592
  ...options.finalize !== void 0 && { finalize: options.finalize }
9110
9593
  };
9111
9594
  }
@@ -9254,6 +9737,9 @@ var Bitfab = class {
9254
9737
  ...mockOnReplay !== void 0 && {
9255
9738
  mockOnReplay
9256
9739
  },
9740
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9741
+ replayReusable: nodeConfiguration.replayReusable
9742
+ },
9257
9743
  ...nodeConfiguration?.finalize !== void 0 && {
9258
9744
  finalize: nodeConfiguration.finalize
9259
9745
  }
@@ -10051,6 +10537,7 @@ var Bitfab = class {
10051
10537
  instrumentation: options.instrumentation ?? "span",
10052
10538
  functionId: options.functionId,
10053
10539
  captureContent: options.captureContent ?? true,
10540
+ replayReusable: options.replayReusable,
10054
10541
  autoTraceDefinition: options.autoTraceDefinition,
10055
10542
  declaredNode: options.declaredNode === true,
10056
10543
  nestedTrace: options.nestedTrace,
@@ -10170,6 +10657,27 @@ var Bitfab = class {
10170
10657
  return result;
10171
10658
  };
10172
10659
  const replayCtxForMock = getReplayContext();
10660
+ if (replayCtxForMock?.selectiveReplay) {
10661
+ const decision = replayCtxForMock.selectiveReplay.enter({
10662
+ traceFunctionKey,
10663
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10664
+ spanId,
10665
+ parentSpanId,
10666
+ inputs: args,
10667
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10668
+ reuseAllowed: options.replayReusable === true,
10669
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10670
+ });
10671
+ if (decision.mock) {
10672
+ void sendSpan({
10673
+ result: decision.output,
10674
+ mocked: true,
10675
+ mockTarget: "output",
10676
+ mockSource: "recorded"
10677
+ });
10678
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10679
+ }
10680
+ }
10173
10681
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10174
10682
  const counters = replayCtxForMock.callCounters;
10175
10683
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10593,6 +11101,9 @@ var Bitfab = class {
10593
11101
  span_origin: makeSpanOrigin(params.instrumentation),
10594
11102
  span_data: {
10595
11103
  name: params.spanName,
11104
+ ...params.replayReusable !== void 0 && {
11105
+ replay_reusable: params.replayReusable
11106
+ },
10596
11107
  type: params.spanType,
10597
11108
  ...params.functionId !== void 0 && {
10598
11109
  function_id: params.functionId,