From 09861253271e9be591290fefe47c75e975837314 Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 11 Aug 2026 19:46:16 +0200 Subject: [PATCH] fix: accept FK reviews by candidate digest across same-content runs --- .../src/workspaces/preprocessing-service.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/src/workspaces/preprocessing-service.ts b/backend/src/workspaces/preprocessing-service.ts index 2a63e486..7142ad56 100644 --- a/backend/src/workspaces/preprocessing-service.ts +++ b/backend/src/workspaces/preprocessing-service.ts @@ -297,8 +297,10 @@ export class WorkspacePreprocessingService { } const candidate = this.state(scope.runtime.workspaceId).readFkCandidates(scope.job.runId); if (candidate && !scope.job.completedStages.includes("fk_review")) { - const review = this.state(scope.runtime.workspaceId).readFkReview(scope.job.runId); - if (!review || review.reviewedCandidatesDigest !== candidate.digest) { + // The candidate content digest is authoritative: a human review accepted for ANY run + // carrying the exact same candidate digest counts as the review checkpoint for this run. + const accepted = this.findAcceptedReviewForDigest(scope.runtime.workspaceId, candidate.digest); + if (!accepted) { return baseResult(scope.runtime, "preprocess run", "blocked", "manual_review_required", { runId: scope.job.runId, childRuns: { ...scope.job.childRuns }, @@ -306,6 +308,7 @@ export class WorkspacePreprocessingService { artifactIdentities: [{ kind: "fk_candidates", digest: candidate.digest }], }); } + scope.job.reviewDigest = accepted.reviewedCandidatesDigest; scope.job.completedStages.push("fk_review"); this.state(scope.runtime.workspaceId).writeJob(scope.job); } @@ -470,6 +473,20 @@ export class WorkspacePreprocessingService { } } + private findAcceptedReviewForDigest( + workspaceId: string, + digestValue: string, + ): FkReviewRecord | undefined { + const state = this.state(workspaceId); + for (const entry of readdirSync(state.fkReviewsDirectory(), { withFileTypes: true })) { + if (!entry.isFile() || entry.isSymbolicLink() || !/^[0-9a-f]{32}\.json$/.test(entry.name)) continue; + const runId = entry.name.slice(0, -".json".length); + const review = state.readFkReview(runId); + if (review && review.reviewedCandidatesDigest === digestValue) return review; + } + return undefined; + } + private findRunIdByCandidateDigest(state: PreprocessingStateStore, digestValue: string): string | undefined { for (const entry of readdirSync(state.fkCandidatesDirectory(), { withFileTypes: true })) { if (!entry.isFile() || entry.isSymbolicLink() || !/^[0-9a-f]{32}\.yaml$/.test(entry.name)) continue;