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:
7
.changeset/import-near-duplicate-precheck.md
Normal file
7
.changeset/import-near-duplicate-precheck.md
Normal 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.
|
||||
@@ -2354,6 +2354,73 @@ export class TriageProcessor {
|
||||
* checks and finalizes. Workflow Plan Review is the single optional AI plan
|
||||
* 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> {
|
||||
/*
|
||||
FNXC:TriageStuckKill 2026-07-18-21:05:
|
||||
@@ -2373,6 +2440,11 @@ export class TriageProcessor {
|
||||
this.coordinatorAdmittedTaskIds.delete(task.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.flagImportNearDuplicateBeforePlanning(task)) {
|
||||
if (dropPreHeldExecutorSlot(task.id)) this.options.semaphore?.release();
|
||||
return;
|
||||
}
|
||||
this.processing.add(task.id);
|
||||
this.processingSince.set(task.id, Date.now());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user