From 9680da76c84a6833635dc7a5df607806d899f804 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:50:25 -0700 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/application/EvidenceBundleCommands.ts | 10 +++++++--- src/process/ProviderDeadline.test.ts | 17 +++++++++++++++++ src/process/ProviderDeadline.ts | 3 +++ src/server/registerBundleComparisonTool.ts | 6 +++--- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/application/EvidenceBundleCommands.ts b/src/application/EvidenceBundleCommands.ts index a72d17cb5..1255ea68f 100644 --- a/src/application/EvidenceBundleCommands.ts +++ b/src/application/EvidenceBundleCommands.ts @@ -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> => { - 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> => { + // 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; diff --git a/src/process/ProviderDeadline.test.ts b/src/process/ProviderDeadline.test.ts index b43ce6654..0a9448150 100644 --- a/src/process/ProviderDeadline.test.ts +++ b/src/process/ProviderDeadline.test.ts @@ -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(); + } +}); diff --git a/src/process/ProviderDeadline.ts b/src/process/ProviderDeadline.ts index 33cb3a8d0..a6f10779a 100644 --- a/src/process/ProviderDeadline.ts +++ b/src/process/ProviderDeadline.ts @@ -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())); } diff --git a/src/server/registerBundleComparisonTool.ts b/src/server/registerBundleComparisonTool.ts index 75ef2a618..3dbc92298 100644 --- a/src/server/registerBundleComparisonTool.ts +++ b/src/server/registerBundleComparisonTool.ts @@ -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,