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