fix(engine): near-duplicate check runs before planning for imported issues

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 18:53:37 -07:00
committed by gsxdsm
parent 8d54c5e049
commit 69bc9fc7ab
2 changed files with 79 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: GitHub-issue imports are checked for near-duplicates before planning, not after.
category: fix
dev: Five issues auto-filed for one red-main event each burned a planning session before the post-plan FN-5152 backstop could see siblings (and racing concurrently, none did). Imports carry title+body at create, so specifyTask now runs the same comparator pre-planning for github_import tasks and flags against an older canonical for the operator's duplicate decision, releasing the pre-held slot. Fail-open; non-import creates keep the post-plan-only behavior.

View File

@@ -2354,6 +2354,73 @@ export class TriageProcessor {
* checks and finalizes. Workflow Plan Review is the single optional AI plan * checks and finalizes. Workflow Plan Review is the single optional AI plan
* quality gate before execution; triage does not inject a separate review tool. * quality gate before execution; triage does not inject a separate review tool.
*/ */
/*
FNXC:ImportNearDuplicatePreCheck 2026-08-01-02:55 (live incident — five same-event imports planned as five tasks):
The FN-5152 near-duplicate backstop runs POST-planning (triage→todo), so a batch of GitHub-issue
imports for one underlying event (five issues auto-filed for the same red main) each burned a
full planning session before any duplicate check ran — and racing concurrently, none saw the
others. The comparator's inputs (title + description) exist BEFORE planning for an import, which
carries the full issue body at create. Run the SAME comparator (same extractor, same
findNearDuplicates, same 7-day window, same flag metadata shape) at specifyTask entry for
import-sourced tasks; on an older-canonical hit, flag for the operator's duplicate decision and
skip the planner entirely. Scoped to imports: dashboard/API creates keep the post-plan-only
behavior, whose signature quality benefits from the planned PROMPT. Fail-open like the backstop —
a comparator error must never block planning.
*/
private async flagImportNearDuplicateBeforePlanning(task: Task): Promise<boolean> {
if (task.sourceType !== "github_import") return false;
try {
const nowMs = Date.now();
const listed = await this.store.listTasks({ slim: false, includeArchived: false });
const isTerminalCandidate = await resolveTerminalColumnsForTasks(this.store, listed);
const candidates = listed
.filter((candidate) => candidate.id !== task.id)
.filter((candidate) => !isTerminalCandidate(candidate))
.filter((candidate) => Date.parse(candidate.createdAt) >= nowMs - 7 * 24 * 60 * 60 * 1000)
.map((candidate) => ({
id: candidate.id,
title: candidate.title ?? "",
description: candidate.description ?? "",
column: candidate.column,
createdAt: Date.parse(candidate.createdAt),
} satisfies NearDuplicateCandidate));
const matches = findNearDuplicates(
{ title: task.title ?? "", description: task.description ?? "" },
candidates,
{ windowMs: 7 * 24 * 60 * 60 * 1000, nowMs },
);
const canonical = matches[0];
if (!canonical) return false;
const canonicalRow = listed.find((candidate) => candidate.id === canonical.id);
// Same tie rule as the backstop: only an older (or tie-canonical) row wins; and never flag
// against an inactive canonical (FN-6439 defense-in-depth).
if (!canonicalRow || Date.parse(canonicalRow.createdAt) > Date.parse(task.createdAt)) return false;
await this.store.updateTask(task.id, {
sourceMetadataPatch: {
nearDuplicateOf: canonical.id,
nearDuplicateScore: canonical.score,
nearDuplicateSharedTokens: canonical.sharedTokens,
},
} as Parameters<typeof this.store.updateTask>[1]);
await this.store.logEntry(
task.id,
`Flagged as near-duplicate of ${canonical.id} before planning (imported issue; awaiting user decision)`,
`Shared tokens: ${canonical.sharedTokens.join(", ")}`,
);
await this.store.recordActivity({
type: "task:near-duplicate-flagged",
taskId: task.id,
taskTitle: task.title ?? "",
details: `Near-duplicate of ${canonical.id} (pre-planning import check)`,
metadata: { canonicalTaskId: canonical.id, source: "import-pre-planning" },
});
return true;
} catch (error: unknown) {
planLog.warn(`${task.id}: import near-duplicate pre-check failed open: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
async specifyTask(task: Task): Promise<void> { async specifyTask(task: Task): Promise<void> {
/* /*
FNXC:TriageStuckKill 2026-07-18-21:05: FNXC:TriageStuckKill 2026-07-18-21:05:
@@ -2373,6 +2440,11 @@ export class TriageProcessor {
this.coordinatorAdmittedTaskIds.delete(task.id); this.coordinatorAdmittedTaskIds.delete(task.id);
return; return;
} }
if (await this.flagImportNearDuplicateBeforePlanning(task)) {
if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
return;
}
this.processing.add(task.id); this.processing.add(task.id);
this.processingSince.set(task.id, Date.now()); this.processingSince.set(task.id, Date.now());