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/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.1";
54
+ __version__ = "0.61.3";
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 {
@@ -2891,8 +2892,11 @@ var init_http = __esm({
2891
2892
  * Start a replay session by fetching historical traces.
2892
2893
  * Blocking call - creates an experiment and returns lightweight item references.
2893
2894
  */
2894
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2895
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2895
2896
  const payload = { traceFunctionKey };
2897
+ if (experimentalSelectiveReplay !== void 0) {
2898
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2899
+ }
2896
2900
  if (limit !== void 0) {
2897
2901
  payload.limit = limit;
2898
2902
  }
@@ -2960,6 +2964,46 @@ var init_http = __esm({
2960
2964
  }
2961
2965
  return { ...rest, experimentId, experimentUrl };
2962
2966
  }
2967
+ async resumeReplay(experimentId, traceFunctionKey, codeChangeFiles, force) {
2968
+ const payload = {
2969
+ experimentId,
2970
+ traceFunctionKey,
2971
+ lazyDbBranchLease: true
2972
+ };
2973
+ if (force === true) {
2974
+ payload.force = true;
2975
+ }
2976
+ if (codeChangeFiles !== void 0) {
2977
+ payload.codeChangeFiles = codeChangeFiles;
2978
+ }
2979
+ const git = await resolvedGitState();
2980
+ if (git && !isEmptyGitState(git)) {
2981
+ payload.git = git;
2982
+ }
2983
+ const response = await this.request(
2984
+ "/api/sdk/replay/resume",
2985
+ payload,
2986
+ { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
2987
+ );
2988
+ const { testRunId, testRunUrl, ...rest } = response;
2989
+ const resumedExperimentId = response.experimentId ?? testRunId;
2990
+ const experimentUrl = response.experimentUrl ?? testRunUrl;
2991
+ if (resumedExperimentId === void 0 || experimentUrl === void 0) {
2992
+ throw new BitfabError(
2993
+ "Replay resume response is missing the experiment ID or URL."
2994
+ );
2995
+ }
2996
+ return {
2997
+ ...rest,
2998
+ experimentId: resumedExperimentId,
2999
+ experimentUrl,
3000
+ work: rest.work ?? [],
3001
+ finished: rest.finished ?? [],
3002
+ skippedOriginalTraceIds: rest.skippedOriginalTraceIds ?? [],
3003
+ includeDbBranchLease: rest.includeDbBranchLease ?? false,
3004
+ dbBranchSettings: rest.dbBranchSettings ?? null
3005
+ };
3006
+ }
2963
3007
  /**
2964
3008
  * Fetch an external span by ID.
2965
3009
  * Blocking GET request.
@@ -3576,6 +3620,291 @@ var init_integrationFixture = __esm({
3576
3620
  }
3577
3621
  });
3578
3622
 
3623
+ // src/selectiveReplay.ts
3624
+ function replayNodeKey(node) {
3625
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3626
+ }
3627
+ function jsonFingerprint(value) {
3628
+ const seen = /* @__PURE__ */ new Set();
3629
+ function encode(input) {
3630
+ if (input === null || typeof input === "boolean") {
3631
+ return JSON.stringify(input);
3632
+ }
3633
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3634
+ return JSON.stringify(input);
3635
+ }
3636
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3637
+ return JSON.stringify(input);
3638
+ }
3639
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3640
+ throw new Error("Not a lossless JSON value");
3641
+ }
3642
+ seen.add(input);
3643
+ const prototype = Object.getPrototypeOf(input);
3644
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3645
+ throw new Error("Not a plain JSON object");
3646
+ }
3647
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3648
+ throw new Error("Symbol properties are not recorded");
3649
+ }
3650
+ const keys = Object.keys(input);
3651
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3652
+ throw new Error("Non-enumerable properties are not recorded");
3653
+ }
3654
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3655
+ throw new Error("Not a dense JSON array");
3656
+ }
3657
+ const properties = keys.map((key) => {
3658
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3659
+ if (!descriptor || !("value" in descriptor)) {
3660
+ throw new Error("Accessor properties are not replay inputs");
3661
+ }
3662
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3663
+ });
3664
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3665
+ }
3666
+ try {
3667
+ return encode(value);
3668
+ } catch {
3669
+ return void 0;
3670
+ }
3671
+ }
3672
+ var init_selectiveReplay = __esm({
3673
+ "src/selectiveReplay.ts"() {
3674
+ "use strict";
3675
+ }
3676
+ });
3677
+
3678
+ // src/selectiveReplayRuntime.ts
3679
+ function validateSelectiveReplayOptions(options) {
3680
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3681
+ (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
3682
+ );
3683
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3684
+ throw new BitfabError(
3685
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3686
+ );
3687
+ }
3688
+ }
3689
+ function policyKey(options) {
3690
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3691
+ }
3692
+ var SelectiveReplayRuntime;
3693
+ var init_selectiveReplayRuntime = __esm({
3694
+ "src/selectiveReplayRuntime.ts"() {
3695
+ "use strict";
3696
+ init_errors();
3697
+ init_selectiveReplay();
3698
+ SelectiveReplayRuntime = class {
3699
+ constructor(plan, options) {
3700
+ this.plan = plan;
3701
+ this.options = options;
3702
+ this.nodes = /* @__PURE__ */ new Map();
3703
+ this.children = /* @__PURE__ */ new Map();
3704
+ this.runtimeParents = /* @__PURE__ */ new Map();
3705
+ this.consumed = /* @__PURE__ */ new Map();
3706
+ this.requiredScopes = /* @__PURE__ */ new Set();
3707
+ this.recordings = /* @__PURE__ */ new Map();
3708
+ this.reached = /* @__PURE__ */ new Set();
3709
+ this.decisions = [];
3710
+ this.assertionNames = /* @__PURE__ */ new Set();
3711
+ this.assertionScopes = /* @__PURE__ */ new Set();
3712
+ this.observedNames = /* @__PURE__ */ new Map();
3713
+ this.unresolvedAssertions = [];
3714
+ validateSelectiveReplayOptions(options);
3715
+ 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) {
3716
+ throw new BitfabError(
3717
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3718
+ );
3719
+ }
3720
+ validateSelectiveReplayOptions(plan.options);
3721
+ if (policyKey(plan.options) !== policyKey(options)) {
3722
+ throw new BitfabError(
3723
+ "Selective replay plan does not match the requested mustRun nodes."
3724
+ );
3725
+ }
3726
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3727
+ for (const node of plan.nodes) {
3728
+ 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") {
3729
+ throw new BitfabError("Invalid selective replay node.");
3730
+ }
3731
+ this.nodes.set(node.id, node);
3732
+ if (node.recording !== void 0) {
3733
+ this.recordings.set(node.id, {
3734
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3735
+ output: jsonFingerprint(node.recording.output)
3736
+ });
3737
+ }
3738
+ if (node.parentId !== null) {
3739
+ const siblings = this.children.get(node.parentId) ?? [];
3740
+ siblings.push(node);
3741
+ this.children.set(node.parentId, siblings);
3742
+ }
3743
+ }
3744
+ const root = this.nodes.get(plan.rootId);
3745
+ if (!root || root.parentId !== null || !root.mustRun) {
3746
+ throw new BitfabError("Selective replay requires a live recorded root.");
3747
+ }
3748
+ const assertionIds = /* @__PURE__ */ new Set();
3749
+ let wholeTrace = false;
3750
+ for (const assertion of plan.assertions) {
3751
+ const target = assertion?.target;
3752
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3753
+ throw new BitfabError(
3754
+ "Invalid selective replay assertion requirements."
3755
+ );
3756
+ }
3757
+ assertionIds.add(assertion.id);
3758
+ if (target === null) {
3759
+ wholeTrace = true;
3760
+ } else if (target.kind === "span") {
3761
+ 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)) {
3762
+ throw new BitfabError("Invalid selective replay assertion target.");
3763
+ }
3764
+ this.assertionNames.add(target.name);
3765
+ const count = plan.nodes.filter(
3766
+ (node) => node.spanName === target.name
3767
+ ).length;
3768
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3769
+ this.unresolvedAssertions.push(assertion.id);
3770
+ }
3771
+ }
3772
+ }
3773
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3774
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3775
+ const unresolved = options.mustRun.filter(
3776
+ (node) => !recordedKeys.has(replayNodeKey(node))
3777
+ );
3778
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3779
+ throw new BitfabError(
3780
+ "Selective replay plan has incomplete mustRun coverage."
3781
+ );
3782
+ }
3783
+ for (const node of plan.nodes) {
3784
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3785
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3786
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3787
+ let parentId = node.parentId;
3788
+ while (parentId !== null) {
3789
+ const parent = this.nodes.get(parentId);
3790
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3791
+ throw new BitfabError("Invalid selective replay ancestry.");
3792
+ }
3793
+ ancestors.add(parentId);
3794
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3795
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3796
+ parentId = parent.parentId;
3797
+ }
3798
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3799
+ throw new BitfabError("Selective replay plan could hide required code.");
3800
+ }
3801
+ if (requiredForAssertion && !node.mustRun) {
3802
+ throw new BitfabError(
3803
+ "Selective replay plan could hide assertion evidence."
3804
+ );
3805
+ }
3806
+ }
3807
+ }
3808
+ enter(params) {
3809
+ const identity = {
3810
+ traceFunctionKey: params.traceFunctionKey,
3811
+ spanName: params.spanName
3812
+ };
3813
+ const key = replayNodeKey(identity);
3814
+ const fingerprint = jsonFingerprint(params.inputs);
3815
+ let node;
3816
+ let matchReason = "unmatched-call";
3817
+ if (params.parentSpanId === null) {
3818
+ node = this.nodes.get(this.plan.rootId);
3819
+ } else {
3820
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3821
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3822
+ this.consumed.set(params.parentSpanId, consumed);
3823
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3824
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3825
+ ) : [];
3826
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3827
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3828
+ );
3829
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3830
+ if (node) {
3831
+ consumed.add(node.id);
3832
+ } else if (candidates.length > 0) {
3833
+ matchReason = "ambiguous-call";
3834
+ for (const candidate of candidates) {
3835
+ consumed.add(candidate.id);
3836
+ }
3837
+ }
3838
+ }
3839
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3840
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3841
+ if (requiredByManifest) {
3842
+ this.requiredScopes.add(params.spanId);
3843
+ }
3844
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3845
+ if (requiredForAssertion) {
3846
+ this.assertionScopes.add(params.spanId);
3847
+ }
3848
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3849
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3850
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3851
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3852
+ 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;
3853
+ const decision = (action, why) => {
3854
+ this.decisions.push({
3855
+ ...identity,
3856
+ originalSpanId: node?.id ?? null,
3857
+ action,
3858
+ reason: why
3859
+ });
3860
+ };
3861
+ if (params.safetyMock && (mustRun || !reusable)) {
3862
+ decision("blocked", `safety-conflict:${reason}`);
3863
+ this.failure = new BitfabError(
3864
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
3865
+ );
3866
+ throw this.failure;
3867
+ }
3868
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
3869
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
3870
+ return {
3871
+ mock: true,
3872
+ output: JSON.parse(recording.output)
3873
+ };
3874
+ }
3875
+ decision("run", reason);
3876
+ this.observedNames.set(
3877
+ params.spanName,
3878
+ (this.observedNames.get(params.spanName) ?? 0) + 1
3879
+ );
3880
+ if (this.required.has(key)) {
3881
+ this.reached.add(key);
3882
+ }
3883
+ return { mock: false };
3884
+ }
3885
+ assertNoSafetyConflict() {
3886
+ if (this.failure) {
3887
+ throw this.failure;
3888
+ }
3889
+ }
3890
+ report() {
3891
+ return {
3892
+ decisions: this.decisions,
3893
+ mustRunNotReached: this.options.mustRun.filter(
3894
+ (node) => !this.reached.has(replayNodeKey(node))
3895
+ ),
3896
+ unresolvedMustRun: this.plan.unresolvedMustRun,
3897
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
3898
+ unresolvedAssertions: this.unresolvedAssertions,
3899
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
3900
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
3901
+ )
3902
+ };
3903
+ }
3904
+ };
3905
+ }
3906
+ });
3907
+
3579
3908
  // src/replay.ts
3580
3909
  var replay_exports = {};
3581
3910
  __export(replay_exports, {
@@ -3595,6 +3924,88 @@ function expandReplayWork(serverItems, attempts) {
3595
3924
  (_, attempt) => serverItems.map((serverItem) => ({ serverItem, attempt }))
3596
3925
  ).flat();
3597
3926
  }
3927
+ function validateResume(resume, options) {
3928
+ if (resume.trim().length === 0) {
3929
+ throw new BitfabError("resume must be the ID of the experiment to resume.");
3930
+ }
3931
+ const conflicting = RESUME_CONFLICTING_OPTIONS.filter(
3932
+ (name) => options[name] !== void 0 && options[name] !== false
3933
+ );
3934
+ if (conflicting.length > 0) {
3935
+ throw new BitfabError(
3936
+ `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.`
3937
+ );
3938
+ }
3939
+ }
3940
+ async function resumeReplaySession(httpClient, experimentId, traceFunctionKey, codeChangeFiles, force, requestedDbBranch) {
3941
+ const response = await httpClient.resumeReplay(
3942
+ experimentId,
3943
+ traceFunctionKey,
3944
+ codeChangeFiles,
3945
+ force
3946
+ );
3947
+ const work = response.work.map(({ itemIndex, attempt }) => {
3948
+ const serverItem = response.items[itemIndex];
3949
+ if (serverItem === void 0) {
3950
+ throw new BitfabError(
3951
+ `Replay resume for experiment ${response.experimentId} named item ${itemIndex}, but the server returned only ${response.items.length} item(s).`
3952
+ );
3953
+ }
3954
+ return { serverItem, attempt };
3955
+ });
3956
+ if (response.skippedOriginalTraceIds.length > 0) {
3957
+ try {
3958
+ console.warn(
3959
+ `Bitfab: ${response.skippedOriginalTraceIds.length} trace(s) selected when experiment ${response.experimentId} started no longer exist, so resume skips them: ${response.skippedOriginalTraceIds.join(", ")}`
3960
+ );
3961
+ } catch {
3962
+ }
3963
+ }
3964
+ if (requestedDbBranch !== void 0 && dbBranchEnabled(requestedDbBranch) !== response.includeDbBranchLease) {
3965
+ try {
3966
+ console.warn(
3967
+ `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.`
3968
+ );
3969
+ } catch {
3970
+ }
3971
+ }
3972
+ return {
3973
+ experimentId: response.experimentId,
3974
+ experimentUrl: response.experimentUrl,
3975
+ attempts: response.attempts,
3976
+ work,
3977
+ finished: response.finished,
3978
+ includeDbBranchLease: response.includeDbBranchLease,
3979
+ dbBranchSettings: response.dbBranchSettings ?? void 0
3980
+ };
3981
+ }
3982
+ function carriedOverReplayItem(finished) {
3983
+ return {
3984
+ traceId: finished.traceId,
3985
+ originalTraceId: finished.originalTraceId,
3986
+ originalSpanId: "",
3987
+ sourceTraceId: finished.originalTraceId,
3988
+ sourceSpanId: "",
3989
+ attempt: finished.attempt,
3990
+ input: [],
3991
+ result: void 0,
3992
+ originalOutput: void 0,
3993
+ error: finished.hasError ? CARRIED_OVER_ERROR : null,
3994
+ traceError: null,
3995
+ replayError: null,
3996
+ durationMs: null,
3997
+ originalDurationMs: null,
3998
+ originalTokens: null,
3999
+ originalModel: null,
4000
+ tokens: null,
4001
+ model: null,
4002
+ dbSnapshotRef: null,
4003
+ dbBranchTimings: null,
4004
+ traceOutline: null,
4005
+ originalTraceOutline: null,
4006
+ carriedOver: true
4007
+ };
4008
+ }
3598
4009
  function resolveAttempts(attempts) {
3599
4010
  if (attempts === void 0) {
3600
4011
  return 1;
@@ -3749,7 +4160,7 @@ function buildMockTree(rootNode) {
3749
4160
  }
3750
4161
  return { spans };
3751
4162
  }
3752
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4163
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3753
4164
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3754
4165
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3755
4166
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3762,6 +4173,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3762
4173
  let replayError = null;
3763
4174
  let replayDurationMs = null;
3764
4175
  let replayStarted = null;
4176
+ let selectiveReplay;
3765
4177
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3766
4178
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3767
4179
  try {
@@ -3794,6 +4206,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3794
4206
  originalTraceId
3795
4207
  );
3796
4208
  }
4209
+ if (selectiveOptions !== void 0) {
4210
+ if (serverItem.selectiveReplayPlan === void 0) {
4211
+ throw new BitfabError(
4212
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4213
+ );
4214
+ }
4215
+ selectiveReplay = new SelectiveReplayRuntime(
4216
+ serverItem.selectiveReplayPlan,
4217
+ selectiveOptions
4218
+ );
4219
+ }
3797
4220
  const span = await httpClient.getExternalSpan(originalSpanId, {
3798
4221
  view: "replay"
3799
4222
  });
@@ -3813,7 +4236,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3813
4236
  });
3814
4237
  }
3815
4238
  const hasOverrides = resolvedOverrides.length > 0;
3816
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4239
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3817
4240
  const includeOutputs = mockStrategy === "all";
3818
4241
  let mockTree;
3819
4242
  if (needTree) {
@@ -3888,6 +4311,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3888
4311
  sourceBitfabTraceId: originalTraceId,
3889
4312
  replayAttempt: attempt,
3890
4313
  mockTree,
4314
+ selectiveReplay,
3891
4315
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
3892
4316
  mockStrategy,
3893
4317
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -3899,6 +4323,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3899
4323
  () => fn(...inputs)
3900
4324
  );
3901
4325
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4326
+ selectiveReplay?.assertNoSafetyConflict();
3902
4327
  replayDurationMs = Math.round(performance.now() - replayStarted);
3903
4328
  } catch (e) {
3904
4329
  if (replayStarted !== null) {
@@ -3950,6 +4375,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3950
4375
  traceError,
3951
4376
  replayError,
3952
4377
  durationMs: replayDurationMs,
4378
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
3953
4379
  originalDurationMs,
3954
4380
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
3955
4381
  originalModel,
@@ -4047,6 +4473,21 @@ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted)
4047
4473
  return results;
4048
4474
  }
4049
4475
  async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
4476
+ if (options?.resume !== void 0) {
4477
+ validateResume(options.resume, options);
4478
+ } else if (options?.force === true) {
4479
+ throw new BitfabError(
4480
+ "force only applies when resuming an experiment. Pass it together with resume."
4481
+ );
4482
+ }
4483
+ if (options?.experimentalSelectiveReplay !== void 0) {
4484
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4485
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4486
+ throw new BitfabError(
4487
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4488
+ );
4489
+ }
4490
+ }
4050
4491
  if (options?.traceIds !== void 0) {
4051
4492
  if (options.traceIds.length === 0) {
4052
4493
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4066,7 +4507,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4066
4507
  } catch {
4067
4508
  }
4068
4509
  }
4069
- const attempts = resolveAttempts(options?.attempts);
4510
+ const requestedAttempts = resolveAttempts(options?.attempts);
4070
4511
  const maxConcurrency = options?.maxConcurrency ?? 10;
4071
4512
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
4072
4513
  throw new BitfabError(
@@ -4085,40 +4526,60 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4085
4526
  }
4086
4527
  }
4087
4528
  }
4088
- const {
4089
- experimentId,
4090
- experimentUrl,
4091
- items: serverItems
4092
- } = await httpClient.startReplay(
4093
- traceFunctionKey,
4094
- // limit is meaningless with explicit traceIds (the ID list determines
4095
- // the count), so it's omitted from the request entirely.
4096
- options?.traceIds ? void 0 : options?.limit ?? 5,
4097
- options?.traceIds,
4098
- options?.name,
4099
- codeChangeDescription,
4100
- codeChangeFiles,
4101
- // A dry run executes nothing, so a branch would be provisioned (and billed)
4102
- // for code that never runs, and a seeded source's refusal would fail the item
4103
- // before it could report its resolved inputs.
4104
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4105
- options?.experimentGroupId,
4106
- datasetIds,
4107
- options?.graderIds,
4108
- resolveDbBranchSettings(options?.dbBranch),
4109
- attempts,
4110
- options?.adaptInputs !== void 0,
4111
- options?.onlyWithAssertions,
4112
- options?.judgeAssertions
4113
- );
4114
- if (serverItems.length === 0) {
4115
- try {
4116
- console.warn(
4117
- `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4118
- );
4119
- } catch {
4529
+ const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4530
+ let session;
4531
+ if (options?.resume === void 0) {
4532
+ const started2 = await httpClient.startReplay(
4533
+ traceFunctionKey,
4534
+ // limit is meaningless with explicit traceIds (the ID list determines
4535
+ // the count), so it's omitted from the request entirely.
4536
+ options?.traceIds ? void 0 : options?.limit ?? 5,
4537
+ options?.traceIds,
4538
+ options?.name,
4539
+ codeChangeDescription,
4540
+ codeChangeFiles,
4541
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
4542
+ // for code that never runs, and a seeded source's refusal would fail the item
4543
+ // before it could report its resolved inputs.
4544
+ includeDbBranchLease,
4545
+ options?.experimentGroupId,
4546
+ datasetIds,
4547
+ options?.graderIds,
4548
+ resolveDbBranchSettings(options?.dbBranch),
4549
+ requestedAttempts,
4550
+ options?.adaptInputs !== void 0,
4551
+ options?.onlyWithAssertions,
4552
+ options?.judgeAssertions,
4553
+ options?.experimentalSelectiveReplay
4554
+ );
4555
+ if (started2.items.length === 0) {
4556
+ try {
4557
+ console.warn(
4558
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4559
+ );
4560
+ } catch {
4561
+ }
4120
4562
  }
4563
+ session = {
4564
+ experimentId: started2.experimentId,
4565
+ experimentUrl: started2.experimentUrl,
4566
+ attempts: requestedAttempts,
4567
+ work: expandReplayWork(started2.items, requestedAttempts),
4568
+ finished: [],
4569
+ includeDbBranchLease,
4570
+ dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
4571
+ };
4572
+ } else {
4573
+ session = await resumeReplaySession(
4574
+ httpClient,
4575
+ options.resume,
4576
+ traceFunctionKey,
4577
+ codeChangeFiles,
4578
+ options.force,
4579
+ options.dbBranch
4580
+ );
4121
4581
  }
4582
+ const { experimentId, experimentUrl, attempts } = session;
4122
4583
  const mockStrategy = options?.mock ?? "marked";
4123
4584
  const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4124
4585
  try {
@@ -4132,7 +4593,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4132
4593
  ...normalizeMockOverrides(options?.mockOverride),
4133
4594
  ...registeredOverrides
4134
4595
  ];
4135
- const workItems = expandReplayWork(serverItems, attempts);
4596
+ const workItems = session.work;
4597
+ const carriedOverItems = session.finished.map(
4598
+ (finished) => carriedOverReplayItem(finished)
4599
+ );
4136
4600
  const replayedTraceIds = workItems.map(() => randomUuid());
4137
4601
  httpClient.trackTraceDeliveries(replayedTraceIds);
4138
4602
  const tasks = workItems.map(
@@ -4145,18 +4609,19 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4145
4609
  resolvedOverrides,
4146
4610
  replayedTraceIds[index],
4147
4611
  attempt,
4148
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4149
- resolveDbBranchSettings(options?.dbBranch),
4612
+ session.includeDbBranchLease,
4613
+ session.dbBranchSettings,
4150
4614
  options?.adaptInputs,
4151
- options?.dryRun === true
4615
+ options?.dryRun === true,
4616
+ options?.experimentalSelectiveReplay
4152
4617
  )
4153
4618
  );
4154
- const total = tasks.length;
4619
+ const total = tasks.length + carriedOverItems.length;
4155
4620
  const onItemFinish = options?.onItemFinish ?? options?.onProgress;
4156
- let completed = 0;
4157
- let started = 0;
4158
- let succeeded = 0;
4159
- let errored = 0;
4621
+ let completed = carriedOverItems.length;
4622
+ let started = carriedOverItems.length;
4623
+ let errored = carriedOverItems.filter((item) => item.error !== null).length;
4624
+ let succeeded = completed - errored;
4160
4625
  let flushInFlight = null;
4161
4626
  let flushPending = false;
4162
4627
  const flushFinishedItemTrace = () => {
@@ -4176,7 +4641,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4176
4641
  }
4177
4642
  return flushInFlight;
4178
4643
  };
4179
- const resultItems = await mapWithConcurrency2(
4644
+ const ranItems = await mapWithConcurrency2(
4180
4645
  tasks,
4181
4646
  maxConcurrency,
4182
4647
  async (item, index) => {
@@ -4221,6 +4686,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4221
4686
  traceError: item.traceError,
4222
4687
  replayError: item.replayError,
4223
4688
  durationMs: item.durationMs,
4689
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4224
4690
  originalDurationMs: item.originalDurationMs,
4225
4691
  originalTokens: item.originalTokens,
4226
4692
  originalModel: item.originalModel,
@@ -4263,6 +4729,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4263
4729
  }
4264
4730
  } : void 0
4265
4731
  );
4732
+ const resultItems = [...carriedOverItems, ...ranItems];
4266
4733
  if (options?.dryRun === true) {
4267
4734
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4268
4735
  const dryResult = {
@@ -4282,11 +4749,11 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4282
4749
  experimentId,
4283
4750
  fullExperimentUrl
4284
4751
  );
4285
- for (let index = 0; index < resultItems.length; index += 1) {
4752
+ for (let index = 0; index < ranItems.length; index += 1) {
4286
4753
  const localId = replayedTraceIds[index];
4287
4754
  const readBack = localId ? deliveredTraceIds[localId] : void 0;
4288
4755
  if (readBack !== void 0) {
4289
- resultItems[index].traceId = readBack;
4756
+ ranItems[index].traceId = readBack;
4290
4757
  }
4291
4758
  }
4292
4759
  const completeResult = await preserveReplayFailure(
@@ -4300,8 +4767,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4300
4767
  if (serverTraceIds !== void 0) {
4301
4768
  const missing = [];
4302
4769
  let completedCount = 0;
4303
- for (let index = 0; index < resultItems.length; index += 1) {
4304
- const item = resultItems[index];
4770
+ for (let index = 0; index < ranItems.length; index += 1) {
4771
+ const item = ranItems[index];
4305
4772
  const localId = replayedTraceIds[index];
4306
4773
  const mapped = localId ? serverTraceIds[localId] : void 0;
4307
4774
  item.traceId = item.traceId ?? mapped ?? null;
@@ -4387,7 +4854,7 @@ async function writeReplayResultFile(result) {
4387
4854
  }
4388
4855
  }
4389
4856
  }
4390
- var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4857
+ var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, RESUME_CONFLICTING_OPTIONS, CARRIED_OVER_ERROR, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4391
4858
  var init_replay = __esm({
4392
4859
  "src/replay.ts"() {
4393
4860
  "use strict";
@@ -4398,9 +4865,22 @@ var init_replay = __esm({
4398
4865
  init_mockOverride();
4399
4866
  init_randomUuid();
4400
4867
  init_replayContext();
4868
+ init_selectiveReplayRuntime();
4401
4869
  init_serialize();
4402
4870
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4403
4871
  MAX_REPLAY_ATTEMPTS = 100;
4872
+ RESUME_CONFLICTING_OPTIONS = [
4873
+ "traceIds",
4874
+ "limit",
4875
+ "datasetId",
4876
+ "datasetIds",
4877
+ "attempts",
4878
+ "graderIds",
4879
+ "onlyWithAssertions",
4880
+ "dryRun",
4881
+ "experimentalSelectiveReplay"
4882
+ ];
4883
+ CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4404
4884
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
4405
4885
  ReplayError = class extends BitfabError {
4406
4886
  constructor(message, items, experimentId, experimentUrl, cause) {
@@ -9092,6 +9572,9 @@ var Bitfab = class {
9092
9572
  ...options.mockOnReplay !== void 0 && {
9093
9573
  mockOnReplay: options.mockOnReplay
9094
9574
  },
9575
+ ...options.replayReusable !== void 0 && {
9576
+ replayReusable: options.replayReusable
9577
+ },
9095
9578
  ...options.finalize !== void 0 && { finalize: options.finalize }
9096
9579
  };
9097
9580
  }
@@ -9240,6 +9723,9 @@ var Bitfab = class {
9240
9723
  ...mockOnReplay !== void 0 && {
9241
9724
  mockOnReplay
9242
9725
  },
9726
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9727
+ replayReusable: nodeConfiguration.replayReusable
9728
+ },
9243
9729
  ...nodeConfiguration?.finalize !== void 0 && {
9244
9730
  finalize: nodeConfiguration.finalize
9245
9731
  }
@@ -10037,6 +10523,7 @@ var Bitfab = class {
10037
10523
  instrumentation: options.instrumentation ?? "span",
10038
10524
  functionId: options.functionId,
10039
10525
  captureContent: options.captureContent ?? true,
10526
+ replayReusable: options.replayReusable,
10040
10527
  autoTraceDefinition: options.autoTraceDefinition,
10041
10528
  declaredNode: options.declaredNode === true,
10042
10529
  nestedTrace: options.nestedTrace,
@@ -10156,6 +10643,27 @@ var Bitfab = class {
10156
10643
  return result;
10157
10644
  };
10158
10645
  const replayCtxForMock = getReplayContext();
10646
+ if (replayCtxForMock?.selectiveReplay) {
10647
+ const decision = replayCtxForMock.selectiveReplay.enter({
10648
+ traceFunctionKey,
10649
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10650
+ spanId,
10651
+ parentSpanId,
10652
+ inputs: args,
10653
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10654
+ reuseAllowed: options.replayReusable === true,
10655
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10656
+ });
10657
+ if (decision.mock) {
10658
+ void sendSpan({
10659
+ result: decision.output,
10660
+ mocked: true,
10661
+ mockTarget: "output",
10662
+ mockSource: "recorded"
10663
+ });
10664
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10665
+ }
10666
+ }
10159
10667
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10160
10668
  const counters = replayCtxForMock.callCounters;
10161
10669
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10579,6 +11087,9 @@ var Bitfab = class {
10579
11087
  span_origin: makeSpanOrigin(params.instrumentation),
10580
11088
  span_data: {
10581
11089
  name: params.spanName,
11090
+ ...params.replayReusable !== void 0 && {
11091
+ replay_reusable: params.replayReusable
11092
+ },
10582
11093
  type: params.spanType,
10583
11094
  ...params.functionId !== void 0 && {
10584
11095
  function_id: params.functionId,