perf(evidence): reuse sealed bundle reads in compare and export (#2715)

* perf(evidence): reuse sealed bundle reads in compare and export

`rea compare`, the compare_bundles MCP tool and `rea evidence-export`
read bundles through the mutable reader, so each consumer
re-authenticated every record again: compareBundles parsed both bundles
a second time and bundleDigest's serializeEvidenceBundle a third time;
export's writer parsed the source once more before serializing.

These consumers only read the bundle, so read it through the existing
sealed reader (readImmutableEvidenceBundle). Later parses hit the
immutable bundle cache. Ordinary mutable reads, kept deliberately by
#1721, are unchanged. Output is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(process): report no remaining time after a provider deadline fires

Node timers can fire while performance.now() is still a fraction of a
millisecond short of the monotonic deadline. ProviderStartupDeadline then
reported interruption "timeout" and an aborted signal while remainingMs()
returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0
(Hopper and Ghidra startup) could run past an elapsed deadline, and the
real-timer test "aborts a bounded wait when its real monotonic deadline
expires" failed intermittently in CI (expected 1 to be 0).

Once the deadline's own timer has classified a timeout, report zero
remaining time. A deterministic regression test fires the timer with the
monotonic clock 0.4 ms short of the deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Kaoru0822 2026-10-13 06:50:25 -07:00 committed by GitHub
parent 46f51ee6cc
commit 9680da76c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 30 additions and 6 deletions

View File

@ -11,6 +11,7 @@ import { compareBundles } from "../domain/bundleComparison.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
readEvidenceBundle,
readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
@ -33,7 +34,8 @@ export const exportEvidenceBundleCommand = async (
outputPath: string,
overwrite: boolean,
): Promise<Result<JsonValue, AnalysisError>> => {
const loaded = await readEvidenceBundle(sourcePath);
// The sealed snapshot lets the writer reuse this authentication.
const loaded = await readImmutableEvidenceBundle(sourcePath);
if (!loaded.ok) return loaded;
const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle(
@ -69,9 +71,11 @@ export const compareEvidenceBundlesCommand = async (input: {
readonly leftPath: string;
readonly rightPath: string;
}): Promise<Result<JsonValue, AnalysisError>> => {
// Sealed snapshots let comparison and its bundle digests reuse this
// authentication instead of re-validating every record twice more.
const [left, right] = await Promise.all([
readEvidenceBundle(input.leftPath),
readEvidenceBundle(input.rightPath),
readImmutableEvidenceBundle(input.leftPath),
readImmutableEvidenceBundle(input.rightPath),
]);
if (!left.ok) return left;
if (!right.ok) return right;

View File

@ -33,3 +33,20 @@ it("aborts a bounded wait when its real monotonic deadline expires", async () =>
deadline.dispose();
}
});
it("reports no remaining time once its timer fires before the monotonic deadline", () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
const now = vi.spyOn(performance, "now").mockReturnValue(1_000);
const deadline = new ProviderStartupDeadline(40);
try {
// Timers may fire while the monotonic clock is still just short of the deadline.
now.mockReturnValue(1_039.6);
vi.advanceTimersByTime(40);
expect(deadline.signal.aborted).toBe(true);
expect(deadline.interruption).toBe("timeout");
expect(deadline.remainingMs()).toBe(0);
} finally {
deadline.dispose();
vi.useRealTimers();
}
});

View File

@ -85,6 +85,9 @@ export class ProviderStartupDeadline {
/** Milliseconds still available to the next startup phase. */
remainingMs(): number {
// Node timers can fire a fraction of a millisecond before the monotonic
// deadline, so an elapsed timer must not report time left to spend.
if (this.#interruption === "timeout") return 0;
return Math.max(0, Math.ceil(this.#deadlineAt - performance.now()));
}

View File

@ -1,7 +1,7 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js";
import { readImmutableEvidenceBundle } from "../application/EvidenceBundleFiles.js";
import { toolContract } from "../contracts/toolContracts.js";
import { compareBundles } from "../domain/bundleComparison.js";
import { createEvidence } from "../domain/evidence.js";
@ -33,12 +33,12 @@ export const registerBundleComparisonTool = (
context.mcpReq.signal,
async () => {
const [left, right] = await Promise.all([
readEvidenceBundle(
readImmutableEvidenceBundle(
input.left_bundle_path,
context.mcpReq.signal,
resources,
),
readEvidenceBundle(
readImmutableEvidenceBundle(
input.right_bundle_path,
context.mcpReq.signal,
resources,