@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/index.cjs CHANGED
@@ -42,7 +42,7 @@ var __version__, __packageName__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.61.1";
45
+ __version__ = "0.61.3";
46
46
  __packageName__ = "@bitfab/sdk";
47
47
  }
48
48
  });
@@ -1068,6 +1068,7 @@ var init_simulationPlan = __esm({
1068
1068
  "input_meta",
1069
1069
  "output",
1070
1070
  "output_meta",
1071
+ "replay_recording",
1071
1072
  "prompt"
1072
1073
  ];
1073
1074
  SimulationPlan = class {
@@ -2882,8 +2883,11 @@ var init_http = __esm({
2882
2883
  * Start a replay session by fetching historical traces.
2883
2884
  * Blocking call - creates an experiment and returns lightweight item references.
2884
2885
  */
2885
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions) {
2886
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2886
2887
  const payload = { traceFunctionKey };
2888
+ if (experimentalSelectiveReplay !== void 0) {
2889
+ payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
2890
+ }
2887
2891
  if (limit !== void 0) {
2888
2892
  payload.limit = limit;
2889
2893
  }
@@ -2951,6 +2955,46 @@ var init_http = __esm({
2951
2955
  }
2952
2956
  return { ...rest, experimentId, experimentUrl };
2953
2957
  }
2958
+ async resumeReplay(experimentId, traceFunctionKey, codeChangeFiles, force) {
2959
+ const payload = {
2960
+ experimentId,
2961
+ traceFunctionKey,
2962
+ lazyDbBranchLease: true
2963
+ };
2964
+ if (force === true) {
2965
+ payload.force = true;
2966
+ }
2967
+ if (codeChangeFiles !== void 0) {
2968
+ payload.codeChangeFiles = codeChangeFiles;
2969
+ }
2970
+ const git = await resolvedGitState();
2971
+ if (git && !isEmptyGitState(git)) {
2972
+ payload.git = git;
2973
+ }
2974
+ const response = await this.request(
2975
+ "/api/sdk/replay/resume",
2976
+ payload,
2977
+ { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
2978
+ );
2979
+ const { testRunId, testRunUrl, ...rest } = response;
2980
+ const resumedExperimentId = response.experimentId ?? testRunId;
2981
+ const experimentUrl = response.experimentUrl ?? testRunUrl;
2982
+ if (resumedExperimentId === void 0 || experimentUrl === void 0) {
2983
+ throw new BitfabError(
2984
+ "Replay resume response is missing the experiment ID or URL."
2985
+ );
2986
+ }
2987
+ return {
2988
+ ...rest,
2989
+ experimentId: resumedExperimentId,
2990
+ experimentUrl,
2991
+ work: rest.work ?? [],
2992
+ finished: rest.finished ?? [],
2993
+ skippedOriginalTraceIds: rest.skippedOriginalTraceIds ?? [],
2994
+ includeDbBranchLease: rest.includeDbBranchLease ?? false,
2995
+ dbBranchSettings: rest.dbBranchSettings ?? null
2996
+ };
2997
+ }
2954
2998
  /**
2955
2999
  * Fetch an external span by ID.
2956
3000
  * Blocking GET request.
@@ -3567,6 +3611,291 @@ var init_integrationFixture = __esm({
3567
3611
  }
3568
3612
  });
3569
3613
 
3614
+ // src/selectiveReplay.ts
3615
+ function replayNodeKey(node) {
3616
+ return JSON.stringify([node.traceFunctionKey, node.spanName]);
3617
+ }
3618
+ function jsonFingerprint(value) {
3619
+ const seen = /* @__PURE__ */ new Set();
3620
+ function encode(input) {
3621
+ if (input === null || typeof input === "boolean") {
3622
+ return JSON.stringify(input);
3623
+ }
3624
+ if (typeof input === "string" && !input.startsWith("<unserializable")) {
3625
+ return JSON.stringify(input);
3626
+ }
3627
+ if (typeof input === "number" && Number.isFinite(input) && !Object.is(input, -0)) {
3628
+ return JSON.stringify(input);
3629
+ }
3630
+ if (typeof input !== "object" || input === null || seen.has(input)) {
3631
+ throw new Error("Not a lossless JSON value");
3632
+ }
3633
+ seen.add(input);
3634
+ const prototype = Object.getPrototypeOf(input);
3635
+ if (prototype !== (Array.isArray(input) ? Array.prototype : Object.prototype)) {
3636
+ throw new Error("Not a plain JSON object");
3637
+ }
3638
+ if (Object.getOwnPropertySymbols(input).length > 0) {
3639
+ throw new Error("Symbol properties are not recorded");
3640
+ }
3641
+ const keys = Object.keys(input);
3642
+ if (Object.getOwnPropertyNames(input).length !== keys.length + (Array.isArray(input) ? 1 : 0)) {
3643
+ throw new Error("Non-enumerable properties are not recorded");
3644
+ }
3645
+ if (Array.isArray(input) && (keys.length !== input.length || keys.some((key, index) => key !== String(index)))) {
3646
+ throw new Error("Not a dense JSON array");
3647
+ }
3648
+ const properties = keys.map((key) => {
3649
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
3650
+ if (!descriptor || !("value" in descriptor)) {
3651
+ throw new Error("Accessor properties are not replay inputs");
3652
+ }
3653
+ return `${Array.isArray(input) ? "" : `${JSON.stringify(key)}:`}${encode(descriptor.value)}`;
3654
+ });
3655
+ return Array.isArray(input) ? `[${properties.join(",")}]` : `{${properties.join(",")}}`;
3656
+ }
3657
+ try {
3658
+ return encode(value);
3659
+ } catch {
3660
+ return void 0;
3661
+ }
3662
+ }
3663
+ var init_selectiveReplay = __esm({
3664
+ "src/selectiveReplay.ts"() {
3665
+ "use strict";
3666
+ }
3667
+ });
3668
+
3669
+ // src/selectiveReplayRuntime.ts
3670
+ function validateSelectiveReplayOptions(options) {
3671
+ const validNodes = (nodes) => Array.isArray(nodes) && nodes.length <= 100 && nodes.every(
3672
+ (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
3673
+ );
3674
+ if (!options || !validNodes(options.mustRun) || options.mustRun.length === 0 || Object.keys(options).some((key) => key !== "mustRun")) {
3675
+ throw new BitfabError(
3676
+ "experimentalSelectiveReplay accepts only 1\u2013100 mustRun with traceFunctionKey and spanName. Declare replayReusable on eligible spans, not reusableNodes in the replay request."
3677
+ );
3678
+ }
3679
+ }
3680
+ function policyKey(options) {
3681
+ return JSON.stringify(options.mustRun.map(replayNodeKey).sort());
3682
+ }
3683
+ var SelectiveReplayRuntime;
3684
+ var init_selectiveReplayRuntime = __esm({
3685
+ "src/selectiveReplayRuntime.ts"() {
3686
+ "use strict";
3687
+ init_errors();
3688
+ init_selectiveReplay();
3689
+ SelectiveReplayRuntime = class {
3690
+ constructor(plan, options) {
3691
+ this.plan = plan;
3692
+ this.options = options;
3693
+ this.nodes = /* @__PURE__ */ new Map();
3694
+ this.children = /* @__PURE__ */ new Map();
3695
+ this.runtimeParents = /* @__PURE__ */ new Map();
3696
+ this.consumed = /* @__PURE__ */ new Map();
3697
+ this.requiredScopes = /* @__PURE__ */ new Set();
3698
+ this.recordings = /* @__PURE__ */ new Map();
3699
+ this.reached = /* @__PURE__ */ new Set();
3700
+ this.decisions = [];
3701
+ this.assertionNames = /* @__PURE__ */ new Set();
3702
+ this.assertionScopes = /* @__PURE__ */ new Set();
3703
+ this.observedNames = /* @__PURE__ */ new Map();
3704
+ this.unresolvedAssertions = [];
3705
+ validateSelectiveReplayOptions(options);
3706
+ 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) {
3707
+ throw new BitfabError(
3708
+ "Server did not return a compatible selective replay plan. Update the server or disable experimentalSelectiveReplay."
3709
+ );
3710
+ }
3711
+ validateSelectiveReplayOptions(plan.options);
3712
+ if (policyKey(plan.options) !== policyKey(options)) {
3713
+ throw new BitfabError(
3714
+ "Selective replay plan does not match the requested mustRun nodes."
3715
+ );
3716
+ }
3717
+ this.required = new Set(options.mustRun.map(replayNodeKey));
3718
+ for (const node of plan.nodes) {
3719
+ 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") {
3720
+ throw new BitfabError("Invalid selective replay node.");
3721
+ }
3722
+ this.nodes.set(node.id, node);
3723
+ if (node.recording !== void 0) {
3724
+ this.recordings.set(node.id, {
3725
+ input: node.recording.kwargs === void 0 ? jsonFingerprint(node.recording.input) : void 0,
3726
+ output: jsonFingerprint(node.recording.output)
3727
+ });
3728
+ }
3729
+ if (node.parentId !== null) {
3730
+ const siblings = this.children.get(node.parentId) ?? [];
3731
+ siblings.push(node);
3732
+ this.children.set(node.parentId, siblings);
3733
+ }
3734
+ }
3735
+ const root = this.nodes.get(plan.rootId);
3736
+ if (!root || root.parentId !== null || !root.mustRun) {
3737
+ throw new BitfabError("Selective replay requires a live recorded root.");
3738
+ }
3739
+ const assertionIds = /* @__PURE__ */ new Set();
3740
+ let wholeTrace = false;
3741
+ for (const assertion of plan.assertions) {
3742
+ const target = assertion?.target;
3743
+ if (typeof assertion?.id !== "string" || assertion.id.length === 0 || assertionIds.has(assertion.id) || target !== null && (typeof target !== "object" || target.kind !== "output" && target.kind !== "span")) {
3744
+ throw new BitfabError(
3745
+ "Invalid selective replay assertion requirements."
3746
+ );
3747
+ }
3748
+ assertionIds.add(assertion.id);
3749
+ if (target === null) {
3750
+ wholeTrace = true;
3751
+ } else if (target.kind === "span") {
3752
+ 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)) {
3753
+ throw new BitfabError("Invalid selective replay assertion target.");
3754
+ }
3755
+ this.assertionNames.add(target.name);
3756
+ const count = plan.nodes.filter(
3757
+ (node) => node.spanName === target.name
3758
+ ).length;
3759
+ if (count <= (typeof target.occurrence === "number" ? target.occurrence : 0)) {
3760
+ this.unresolvedAssertions.push(assertion.id);
3761
+ }
3762
+ }
3763
+ }
3764
+ this.fullTraceForAssertions = wholeTrace || this.unresolvedAssertions.length > 0;
3765
+ const recordedKeys = new Set(plan.nodes.map(replayNodeKey));
3766
+ const unresolved = options.mustRun.filter(
3767
+ (node) => !recordedKeys.has(replayNodeKey(node))
3768
+ );
3769
+ if (JSON.stringify(unresolved.map(replayNodeKey).sort()) !== JSON.stringify(plan.unresolvedMustRun.map(replayNodeKey).sort())) {
3770
+ throw new BitfabError(
3771
+ "Selective replay plan has incomplete mustRun coverage."
3772
+ );
3773
+ }
3774
+ for (const node of plan.nodes) {
3775
+ const ancestors = /* @__PURE__ */ new Set([node.id]);
3776
+ let requiredByManifest = this.required.has(replayNodeKey(node));
3777
+ let requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(node.spanName);
3778
+ let parentId = node.parentId;
3779
+ while (parentId !== null) {
3780
+ const parent = this.nodes.get(parentId);
3781
+ if (!parent || ancestors.has(parentId) || node.mustRun && !parent.mustRun) {
3782
+ throw new BitfabError("Invalid selective replay ancestry.");
3783
+ }
3784
+ ancestors.add(parentId);
3785
+ requiredByManifest || (requiredByManifest = this.required.has(replayNodeKey(parent)));
3786
+ requiredForAssertion || (requiredForAssertion = this.assertionNames.has(parent.spanName));
3787
+ parentId = parent.parentId;
3788
+ }
3789
+ if (!ancestors.has(plan.rootId) || (requiredByManifest || unresolved.length > 0) && !node.mustRun) {
3790
+ throw new BitfabError("Selective replay plan could hide required code.");
3791
+ }
3792
+ if (requiredForAssertion && !node.mustRun) {
3793
+ throw new BitfabError(
3794
+ "Selective replay plan could hide assertion evidence."
3795
+ );
3796
+ }
3797
+ }
3798
+ }
3799
+ enter(params) {
3800
+ const identity = {
3801
+ traceFunctionKey: params.traceFunctionKey,
3802
+ spanName: params.spanName
3803
+ };
3804
+ const key = replayNodeKey(identity);
3805
+ const fingerprint = jsonFingerprint(params.inputs);
3806
+ let node;
3807
+ let matchReason = "unmatched-call";
3808
+ if (params.parentSpanId === null) {
3809
+ node = this.nodes.get(this.plan.rootId);
3810
+ } else {
3811
+ const recordedParent = this.runtimeParents.get(params.parentSpanId);
3812
+ const consumed = this.consumed.get(params.parentSpanId) ?? /* @__PURE__ */ new Set();
3813
+ this.consumed.set(params.parentSpanId, consumed);
3814
+ const candidates = recordedParent ? (this.children.get(recordedParent) ?? []).filter(
3815
+ (candidate) => replayNodeKey(candidate) === key && !consumed.has(candidate.id)
3816
+ ) : [];
3817
+ const exact = fingerprint === void 0 ? [] : candidates.filter(
3818
+ (candidate) => this.recordings.get(candidate.id)?.input === fingerprint
3819
+ );
3820
+ node = exact.length === 1 ? exact[0] : exact.length === 0 && candidates.length === 1 ? candidates[0] : void 0;
3821
+ if (node) {
3822
+ consumed.add(node.id);
3823
+ } else if (candidates.length > 0) {
3824
+ matchReason = "ambiguous-call";
3825
+ for (const candidate of candidates) {
3826
+ consumed.add(candidate.id);
3827
+ }
3828
+ }
3829
+ }
3830
+ this.runtimeParents.set(params.spanId, node?.id ?? null);
3831
+ const requiredByManifest = this.required.has(key) || params.parentSpanId !== null && this.requiredScopes.has(params.parentSpanId);
3832
+ if (requiredByManifest) {
3833
+ this.requiredScopes.add(params.spanId);
3834
+ }
3835
+ const requiredForAssertion = this.fullTraceForAssertions || this.assertionNames.has(params.spanName) || params.parentSpanId !== null && this.assertionScopes.has(params.parentSpanId);
3836
+ if (requiredForAssertion) {
3837
+ this.assertionScopes.add(params.spanId);
3838
+ }
3839
+ const mustRun = params.parentSpanId === null || requiredByManifest || node?.mustRun === true || requiredForAssertion || this.plan.unresolvedMustRun.length > 0;
3840
+ const recording = node === void 0 ? void 0 : this.recordings.get(node.id);
3841
+ const equalInputs = fingerprint !== void 0 && recording?.input === fingerprint;
3842
+ const reusable = recording?.output !== void 0 && equalInputs && params.canReuseOutput;
3843
+ 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;
3844
+ const decision = (action, why) => {
3845
+ this.decisions.push({
3846
+ ...identity,
3847
+ originalSpanId: node?.id ?? null,
3848
+ action,
3849
+ reason: why
3850
+ });
3851
+ };
3852
+ if (params.safetyMock && (mustRun || !reusable)) {
3853
+ decision("blocked", `safety-conflict:${reason}`);
3854
+ this.failure = new BitfabError(
3855
+ `Selective replay cannot execute or safely reuse safety-mocked node ${params.spanName} (${reason}).`
3856
+ );
3857
+ throw this.failure;
3858
+ }
3859
+ if (!mustRun && reusable && (params.safetyMock || node?.reusable === true && params.reuseAllowed)) {
3860
+ decision("mock", params.safetyMock ? "safety-mock" : "unchanged-inputs");
3861
+ return {
3862
+ mock: true,
3863
+ output: JSON.parse(recording.output)
3864
+ };
3865
+ }
3866
+ decision("run", reason);
3867
+ this.observedNames.set(
3868
+ params.spanName,
3869
+ (this.observedNames.get(params.spanName) ?? 0) + 1
3870
+ );
3871
+ if (this.required.has(key)) {
3872
+ this.reached.add(key);
3873
+ }
3874
+ return { mock: false };
3875
+ }
3876
+ assertNoSafetyConflict() {
3877
+ if (this.failure) {
3878
+ throw this.failure;
3879
+ }
3880
+ }
3881
+ report() {
3882
+ return {
3883
+ decisions: this.decisions,
3884
+ mustRunNotReached: this.options.mustRun.filter(
3885
+ (node) => !this.reached.has(replayNodeKey(node))
3886
+ ),
3887
+ unresolvedMustRun: this.plan.unresolvedMustRun,
3888
+ assertionIds: this.plan.assertions.map((assertion) => assertion.id),
3889
+ unresolvedAssertions: this.unresolvedAssertions,
3890
+ assertionTargetsNotReached: this.plan.assertions.flatMap(
3891
+ ({ id, target }) => target?.kind === "span" && (this.observedNames.get(target.name) ?? 0) <= (typeof target.occurrence === "number" ? target.occurrence : 0) ? [id] : []
3892
+ )
3893
+ };
3894
+ }
3895
+ };
3896
+ }
3897
+ });
3898
+
3570
3899
  // src/replay.ts
3571
3900
  var replay_exports = {};
3572
3901
  __export(replay_exports, {
@@ -3586,6 +3915,88 @@ function expandReplayWork(serverItems, attempts) {
3586
3915
  (_, attempt) => serverItems.map((serverItem) => ({ serverItem, attempt }))
3587
3916
  ).flat();
3588
3917
  }
3918
+ function validateResume(resume, options) {
3919
+ if (resume.trim().length === 0) {
3920
+ throw new BitfabError("resume must be the ID of the experiment to resume.");
3921
+ }
3922
+ const conflicting = RESUME_CONFLICTING_OPTIONS.filter(
3923
+ (name) => options[name] !== void 0 && options[name] !== false
3924
+ );
3925
+ if (conflicting.length > 0) {
3926
+ throw new BitfabError(
3927
+ `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.`
3928
+ );
3929
+ }
3930
+ }
3931
+ async function resumeReplaySession(httpClient, experimentId, traceFunctionKey, codeChangeFiles, force, requestedDbBranch) {
3932
+ const response = await httpClient.resumeReplay(
3933
+ experimentId,
3934
+ traceFunctionKey,
3935
+ codeChangeFiles,
3936
+ force
3937
+ );
3938
+ const work = response.work.map(({ itemIndex, attempt }) => {
3939
+ const serverItem = response.items[itemIndex];
3940
+ if (serverItem === void 0) {
3941
+ throw new BitfabError(
3942
+ `Replay resume for experiment ${response.experimentId} named item ${itemIndex}, but the server returned only ${response.items.length} item(s).`
3943
+ );
3944
+ }
3945
+ return { serverItem, attempt };
3946
+ });
3947
+ if (response.skippedOriginalTraceIds.length > 0) {
3948
+ try {
3949
+ console.warn(
3950
+ `Bitfab: ${response.skippedOriginalTraceIds.length} trace(s) selected when experiment ${response.experimentId} started no longer exist, so resume skips them: ${response.skippedOriginalTraceIds.join(", ")}`
3951
+ );
3952
+ } catch {
3953
+ }
3954
+ }
3955
+ if (requestedDbBranch !== void 0 && dbBranchEnabled(requestedDbBranch) !== response.includeDbBranchLease) {
3956
+ try {
3957
+ console.warn(
3958
+ `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.`
3959
+ );
3960
+ } catch {
3961
+ }
3962
+ }
3963
+ return {
3964
+ experimentId: response.experimentId,
3965
+ experimentUrl: response.experimentUrl,
3966
+ attempts: response.attempts,
3967
+ work,
3968
+ finished: response.finished,
3969
+ includeDbBranchLease: response.includeDbBranchLease,
3970
+ dbBranchSettings: response.dbBranchSettings ?? void 0
3971
+ };
3972
+ }
3973
+ function carriedOverReplayItem(finished) {
3974
+ return {
3975
+ traceId: finished.traceId,
3976
+ originalTraceId: finished.originalTraceId,
3977
+ originalSpanId: "",
3978
+ sourceTraceId: finished.originalTraceId,
3979
+ sourceSpanId: "",
3980
+ attempt: finished.attempt,
3981
+ input: [],
3982
+ result: void 0,
3983
+ originalOutput: void 0,
3984
+ error: finished.hasError ? CARRIED_OVER_ERROR : null,
3985
+ traceError: null,
3986
+ replayError: null,
3987
+ durationMs: null,
3988
+ originalDurationMs: null,
3989
+ originalTokens: null,
3990
+ originalModel: null,
3991
+ tokens: null,
3992
+ model: null,
3993
+ dbSnapshotRef: null,
3994
+ dbBranchTimings: null,
3995
+ traceOutline: null,
3996
+ originalTraceOutline: null,
3997
+ carriedOver: true
3998
+ };
3999
+ }
3589
4000
  function resolveAttempts(attempts) {
3590
4001
  if (attempts === void 0) {
3591
4002
  return 1;
@@ -3740,7 +4151,7 @@ function buildMockTree(rootNode) {
3740
4151
  }
3741
4152
  return { spans };
3742
4153
  }
3743
- async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
4154
+ async function processItem(httpClient, serverItem, fn, experimentId, mockStrategy, resolvedOverrides, replayedTraceId, attempt, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun, selectiveOptions) {
3744
4155
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
3745
4156
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
3746
4157
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -3753,6 +4164,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3753
4164
  let replayError = null;
3754
4165
  let replayDurationMs = null;
3755
4166
  let replayStarted = null;
4167
+ let selectiveReplay;
3756
4168
  const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
3757
4169
  const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
3758
4170
  try {
@@ -3785,6 +4197,17 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3785
4197
  originalTraceId
3786
4198
  );
3787
4199
  }
4200
+ if (selectiveOptions !== void 0) {
4201
+ if (serverItem.selectiveReplayPlan === void 0) {
4202
+ throw new BitfabError(
4203
+ "Server did not return a selective replay plan. Update the server or disable experimentalSelectiveReplay."
4204
+ );
4205
+ }
4206
+ selectiveReplay = new SelectiveReplayRuntime(
4207
+ serverItem.selectiveReplayPlan,
4208
+ selectiveOptions
4209
+ );
4210
+ }
3788
4211
  const span = await httpClient.getExternalSpan(originalSpanId, {
3789
4212
  view: "replay"
3790
4213
  });
@@ -3804,7 +4227,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3804
4227
  });
3805
4228
  }
3806
4229
  const hasOverrides = resolvedOverrides.length > 0;
3807
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
4230
+ const needTree = selectiveReplay === void 0 && (mockStrategy === "all" || mockStrategy === "marked" || hasOverrides);
3808
4231
  const includeOutputs = mockStrategy === "all";
3809
4232
  let mockTree;
3810
4233
  if (needTree) {
@@ -3879,6 +4302,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3879
4302
  sourceBitfabTraceId: originalTraceId,
3880
4303
  replayAttempt: attempt,
3881
4304
  mockTree,
4305
+ selectiveReplay,
3882
4306
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
3883
4307
  mockStrategy,
3884
4308
  mockOverrides: hasOverrides ? resolvedOverrides : void 0,
@@ -3890,6 +4314,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3890
4314
  () => fn(...inputs)
3891
4315
  );
3892
4316
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
4317
+ selectiveReplay?.assertNoSafetyConflict();
3893
4318
  replayDurationMs = Math.round(performance.now() - replayStarted);
3894
4319
  } catch (e) {
3895
4320
  if (replayStarted !== null) {
@@ -3941,6 +4366,7 @@ async function processItem(httpClient, serverItem, fn, experimentId, mockStrateg
3941
4366
  traceError,
3942
4367
  replayError,
3943
4368
  durationMs: replayDurationMs,
4369
+ ...selectiveReplay === void 0 ? {} : { selectiveReplay: selectiveReplay.report() },
3944
4370
  originalDurationMs,
3945
4371
  originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
3946
4372
  originalModel,
@@ -4038,6 +4464,21 @@ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted)
4038
4464
  return results;
4039
4465
  }
4040
4466
  async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
4467
+ if (options?.resume !== void 0) {
4468
+ validateResume(options.resume, options);
4469
+ } else if (options?.force === true) {
4470
+ throw new BitfabError(
4471
+ "force only applies when resuming an experiment. Pass it together with resume."
4472
+ );
4473
+ }
4474
+ if (options?.experimentalSelectiveReplay !== void 0) {
4475
+ validateSelectiveReplayOptions(options.experimentalSelectiveReplay);
4476
+ if ((options.mock ?? "marked") !== "marked" || normalizeMockOverrides(options.mockOverride).length > 0 || registeredOverrides.length > 0) {
4477
+ throw new BitfabError(
4478
+ "experimentalSelectiveReplay requires mock: 'marked' and cannot be combined with mock overrides."
4479
+ );
4480
+ }
4481
+ }
4041
4482
  if (options?.traceIds !== void 0) {
4042
4483
  if (options.traceIds.length === 0) {
4043
4484
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4057,7 +4498,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4057
4498
  } catch {
4058
4499
  }
4059
4500
  }
4060
- const attempts = resolveAttempts(options?.attempts);
4501
+ const requestedAttempts = resolveAttempts(options?.attempts);
4061
4502
  const maxConcurrency = options?.maxConcurrency ?? 10;
4062
4503
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
4063
4504
  throw new BitfabError(
@@ -4076,40 +4517,60 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4076
4517
  }
4077
4518
  }
4078
4519
  }
4079
- const {
4080
- experimentId,
4081
- experimentUrl,
4082
- items: serverItems
4083
- } = await httpClient.startReplay(
4084
- traceFunctionKey,
4085
- // limit is meaningless with explicit traceIds (the ID list determines
4086
- // the count), so it's omitted from the request entirely.
4087
- options?.traceIds ? void 0 : options?.limit ?? 5,
4088
- options?.traceIds,
4089
- options?.name,
4090
- codeChangeDescription,
4091
- codeChangeFiles,
4092
- // A dry run executes nothing, so a branch would be provisioned (and billed)
4093
- // for code that never runs, and a seeded source's refusal would fail the item
4094
- // before it could report its resolved inputs.
4095
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4096
- options?.experimentGroupId,
4097
- datasetIds,
4098
- options?.graderIds,
4099
- resolveDbBranchSettings(options?.dbBranch),
4100
- attempts,
4101
- options?.adaptInputs !== void 0,
4102
- options?.onlyWithAssertions,
4103
- options?.judgeAssertions
4104
- );
4105
- if (serverItems.length === 0) {
4106
- try {
4107
- console.warn(
4108
- `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4109
- );
4110
- } catch {
4520
+ const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4521
+ let session;
4522
+ if (options?.resume === void 0) {
4523
+ const started2 = await httpClient.startReplay(
4524
+ traceFunctionKey,
4525
+ // limit is meaningless with explicit traceIds (the ID list determines
4526
+ // the count), so it's omitted from the request entirely.
4527
+ options?.traceIds ? void 0 : options?.limit ?? 5,
4528
+ options?.traceIds,
4529
+ options?.name,
4530
+ codeChangeDescription,
4531
+ codeChangeFiles,
4532
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
4533
+ // for code that never runs, and a seeded source's refusal would fail the item
4534
+ // before it could report its resolved inputs.
4535
+ includeDbBranchLease,
4536
+ options?.experimentGroupId,
4537
+ datasetIds,
4538
+ options?.graderIds,
4539
+ resolveDbBranchSettings(options?.dbBranch),
4540
+ requestedAttempts,
4541
+ options?.adaptInputs !== void 0,
4542
+ options?.onlyWithAssertions,
4543
+ options?.judgeAssertions,
4544
+ options?.experimentalSelectiveReplay
4545
+ );
4546
+ if (started2.items.length === 0) {
4547
+ try {
4548
+ console.warn(
4549
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4550
+ );
4551
+ } catch {
4552
+ }
4111
4553
  }
4554
+ session = {
4555
+ experimentId: started2.experimentId,
4556
+ experimentUrl: started2.experimentUrl,
4557
+ attempts: requestedAttempts,
4558
+ work: expandReplayWork(started2.items, requestedAttempts),
4559
+ finished: [],
4560
+ includeDbBranchLease,
4561
+ dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
4562
+ };
4563
+ } else {
4564
+ session = await resumeReplaySession(
4565
+ httpClient,
4566
+ options.resume,
4567
+ traceFunctionKey,
4568
+ codeChangeFiles,
4569
+ options.force,
4570
+ options.dbBranch
4571
+ );
4112
4572
  }
4573
+ const { experimentId, experimentUrl, attempts } = session;
4113
4574
  const mockStrategy = options?.mock ?? "marked";
4114
4575
  const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4115
4576
  try {
@@ -4123,7 +4584,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4123
4584
  ...normalizeMockOverrides(options?.mockOverride),
4124
4585
  ...registeredOverrides
4125
4586
  ];
4126
- const workItems = expandReplayWork(serverItems, attempts);
4587
+ const workItems = session.work;
4588
+ const carriedOverItems = session.finished.map(
4589
+ (finished) => carriedOverReplayItem(finished)
4590
+ );
4127
4591
  const replayedTraceIds = workItems.map(() => randomUuid());
4128
4592
  httpClient.trackTraceDeliveries(replayedTraceIds);
4129
4593
  const tasks = workItems.map(
@@ -4136,18 +4600,19 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4136
4600
  resolvedOverrides,
4137
4601
  replayedTraceIds[index],
4138
4602
  attempt,
4139
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4140
- resolveDbBranchSettings(options?.dbBranch),
4603
+ session.includeDbBranchLease,
4604
+ session.dbBranchSettings,
4141
4605
  options?.adaptInputs,
4142
- options?.dryRun === true
4606
+ options?.dryRun === true,
4607
+ options?.experimentalSelectiveReplay
4143
4608
  )
4144
4609
  );
4145
- const total = tasks.length;
4610
+ const total = tasks.length + carriedOverItems.length;
4146
4611
  const onItemFinish = options?.onItemFinish ?? options?.onProgress;
4147
- let completed = 0;
4148
- let started = 0;
4149
- let succeeded = 0;
4150
- let errored = 0;
4612
+ let completed = carriedOverItems.length;
4613
+ let started = carriedOverItems.length;
4614
+ let errored = carriedOverItems.filter((item) => item.error !== null).length;
4615
+ let succeeded = completed - errored;
4151
4616
  let flushInFlight = null;
4152
4617
  let flushPending = false;
4153
4618
  const flushFinishedItemTrace = () => {
@@ -4167,7 +4632,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4167
4632
  }
4168
4633
  return flushInFlight;
4169
4634
  };
4170
- const resultItems = await mapWithConcurrency2(
4635
+ const ranItems = await mapWithConcurrency2(
4171
4636
  tasks,
4172
4637
  maxConcurrency,
4173
4638
  async (item, index) => {
@@ -4212,6 +4677,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4212
4677
  traceError: item.traceError,
4213
4678
  replayError: item.replayError,
4214
4679
  durationMs: item.durationMs,
4680
+ ...item.selectiveReplay === void 0 ? {} : { selectiveReplay: item.selectiveReplay },
4215
4681
  originalDurationMs: item.originalDurationMs,
4216
4682
  originalTokens: item.originalTokens,
4217
4683
  originalModel: item.originalModel,
@@ -4254,6 +4720,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4254
4720
  }
4255
4721
  } : void 0
4256
4722
  );
4723
+ const resultItems = [...carriedOverItems, ...ranItems];
4257
4724
  if (options?.dryRun === true) {
4258
4725
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4259
4726
  const dryResult = {
@@ -4273,11 +4740,11 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4273
4740
  experimentId,
4274
4741
  fullExperimentUrl
4275
4742
  );
4276
- for (let index = 0; index < resultItems.length; index += 1) {
4743
+ for (let index = 0; index < ranItems.length; index += 1) {
4277
4744
  const localId = replayedTraceIds[index];
4278
4745
  const readBack = localId ? deliveredTraceIds[localId] : void 0;
4279
4746
  if (readBack !== void 0) {
4280
- resultItems[index].traceId = readBack;
4747
+ ranItems[index].traceId = readBack;
4281
4748
  }
4282
4749
  }
4283
4750
  const completeResult = await preserveReplayFailure(
@@ -4291,8 +4758,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4291
4758
  if (serverTraceIds !== void 0) {
4292
4759
  const missing = [];
4293
4760
  let completedCount = 0;
4294
- for (let index = 0; index < resultItems.length; index += 1) {
4295
- const item = resultItems[index];
4761
+ for (let index = 0; index < ranItems.length; index += 1) {
4762
+ const item = ranItems[index];
4296
4763
  const localId = replayedTraceIds[index];
4297
4764
  const mapped = localId ? serverTraceIds[localId] : void 0;
4298
4765
  item.traceId = item.traceId ?? mapped ?? null;
@@ -4378,7 +4845,7 @@ async function writeReplayResultFile(result) {
4378
4845
  }
4379
4846
  }
4380
4847
  }
4381
- var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4848
+ var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, RESUME_CONFLICTING_OPTIONS, CARRIED_OVER_ERROR, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4382
4849
  var init_replay = __esm({
4383
4850
  "src/replay.ts"() {
4384
4851
  "use strict";
@@ -4389,9 +4856,22 @@ var init_replay = __esm({
4389
4856
  init_mockOverride();
4390
4857
  init_randomUuid();
4391
4858
  init_replayContext();
4859
+ init_selectiveReplayRuntime();
4392
4860
  init_serialize();
4393
4861
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4394
4862
  MAX_REPLAY_ATTEMPTS = 100;
4863
+ RESUME_CONFLICTING_OPTIONS = [
4864
+ "traceIds",
4865
+ "limit",
4866
+ "datasetId",
4867
+ "datasetIds",
4868
+ "attempts",
4869
+ "graderIds",
4870
+ "onlyWithAssertions",
4871
+ "dryRun",
4872
+ "experimentalSelectiveReplay"
4873
+ ];
4874
+ CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4395
4875
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
4396
4876
  ReplayError = class extends BitfabError {
4397
4877
  constructor(message, items, experimentId, experimentUrl, cause) {
@@ -9083,6 +9563,9 @@ var Bitfab = class {
9083
9563
  ...options.mockOnReplay !== void 0 && {
9084
9564
  mockOnReplay: options.mockOnReplay
9085
9565
  },
9566
+ ...options.replayReusable !== void 0 && {
9567
+ replayReusable: options.replayReusable
9568
+ },
9086
9569
  ...options.finalize !== void 0 && { finalize: options.finalize }
9087
9570
  };
9088
9571
  }
@@ -9231,6 +9714,9 @@ var Bitfab = class {
9231
9714
  ...mockOnReplay !== void 0 && {
9232
9715
  mockOnReplay
9233
9716
  },
9717
+ ...nodeConfiguration?.replayReusable !== void 0 && {
9718
+ replayReusable: nodeConfiguration.replayReusable
9719
+ },
9234
9720
  ...nodeConfiguration?.finalize !== void 0 && {
9235
9721
  finalize: nodeConfiguration.finalize
9236
9722
  }
@@ -10028,6 +10514,7 @@ var Bitfab = class {
10028
10514
  instrumentation: options.instrumentation ?? "span",
10029
10515
  functionId: options.functionId,
10030
10516
  captureContent: options.captureContent ?? true,
10517
+ replayReusable: options.replayReusable,
10031
10518
  autoTraceDefinition: options.autoTraceDefinition,
10032
10519
  declaredNode: options.declaredNode === true,
10033
10520
  nestedTrace: options.nestedTrace,
@@ -10147,6 +10634,27 @@ var Bitfab = class {
10147
10634
  return result;
10148
10635
  };
10149
10636
  const replayCtxForMock = getReplayContext();
10637
+ if (replayCtxForMock?.selectiveReplay) {
10638
+ const decision = replayCtxForMock.selectiveReplay.enter({
10639
+ traceFunctionKey,
10640
+ spanName: baseSpanParams.spanName ?? traceFunctionKey,
10641
+ spanId,
10642
+ parentSpanId,
10643
+ inputs: args,
10644
+ safetyMock: !isRootSpan && options.mockOnReplay === true,
10645
+ reuseAllowed: options.replayReusable === true,
10646
+ canReuseOutput: options.finalize === void 0 && options.replayOutput === void 0 && !fn.constructor.name.includes("Generator")
10647
+ });
10648
+ if (decision.mock) {
10649
+ void sendSpan({
10650
+ result: decision.output,
10651
+ mocked: true,
10652
+ mockTarget: "output",
10653
+ mockSource: "recorded"
10654
+ });
10655
+ return fnReturnsPromise ? Promise.resolve(decision.output) : decision.output;
10656
+ }
10657
+ }
10150
10658
  if (replayCtxForMock?.mockTree && !isRootSpan) {
10151
10659
  const counters = replayCtxForMock.callCounters;
10152
10660
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
@@ -10570,6 +11078,9 @@ var Bitfab = class {
10570
11078
  span_origin: makeSpanOrigin(params.instrumentation),
10571
11079
  span_data: {
10572
11080
  name: params.spanName,
11081
+ ...params.replayReusable !== void 0 && {
11082
+ replay_reusable: params.replayReusable
11083
+ },
10573
11084
  type: params.spanType,
10574
11085
  ...params.functionId !== void 0 && {
10575
11086
  function_id: params.functionId,